YouTube.vue 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. <template>
  2. <div class="admin-tab container">
  3. <page-metadata title="Admin | YouTube" />
  4. <div class="card tab-info">
  5. <div class="info-row">
  6. <h1>YouTube API</h1>
  7. <p>
  8. Analyze YouTube quota usage and API requests made on this
  9. instance
  10. </p>
  11. </div>
  12. <div class="button-row">
  13. <run-job-dropdown :jobs="jobs" />
  14. </div>
  15. </div>
  16. <div class="card charts">
  17. <div class="chart">
  18. <h4 class="has-text-centered">Quota Usage</h4>
  19. <line-chart
  20. chart-id="youtube-quota-usage"
  21. :data="charts.quotaUsage"
  22. />
  23. </div>
  24. <div class="chart">
  25. <h4 class="has-text-centered">API Requests</h4>
  26. <line-chart
  27. chart-id="youtube-api-requests"
  28. :data="charts.apiRequests"
  29. />
  30. </div>
  31. </div>
  32. <div class="card">
  33. <h4>Quota Stats</h4>
  34. <hr class="section-horizontal-rule" />
  35. <div class="card-content">
  36. <p v-if="fromDate">As of {{ fromDate }}</p>
  37. <div
  38. v-for="[quotaName, quotaObject] in Object.entries(
  39. quotaStatus
  40. )"
  41. :key="quotaName"
  42. >
  43. <p>{{ quotaName }}</p>
  44. <hr />
  45. <p>Quota used: {{ quotaObject.quotaUsed }}</p>
  46. <p>Limit: {{ quotaObject.limit }}</p>
  47. <p>Quota exceeded: {{ quotaObject.quotaExceeded }}</p>
  48. </div>
  49. </div>
  50. </div>
  51. <div class="card">
  52. <h4>API Requests</h4>
  53. <hr class="section-horizontal-rule" />
  54. <advanced-table
  55. :column-default="columnDefault"
  56. :columns="columns"
  57. :filters="filters"
  58. :events="events"
  59. data-action="youtube.getApiRequests"
  60. name="admin-youtube-api-requests"
  61. :max-width="1140"
  62. >
  63. <template #column-options="slotProps">
  64. <div class="row-options">
  65. <button
  66. class="button is-primary icon-with-button material-icons"
  67. @click="
  68. openModal({
  69. modal: 'viewApiRequest',
  70. data: {
  71. requestId: slotProps.item._id,
  72. removeAction:
  73. 'youtube.removeStoredApiRequest'
  74. }
  75. })
  76. "
  77. :disabled="slotProps.item.removed"
  78. content="View API Request"
  79. v-tippy
  80. >
  81. open_in_full
  82. </button>
  83. <quick-confirm
  84. @confirm="removeApiRequest(slotProps.item._id)"
  85. :disabled="slotProps.item.removed"
  86. >
  87. <button
  88. class="button is-danger icon-with-button material-icons"
  89. content="Remove API Request"
  90. v-tippy
  91. >
  92. delete_forever
  93. </button>
  94. </quick-confirm>
  95. </div>
  96. </template>
  97. <template #column-_id="slotProps">
  98. <span :title="slotProps.item._id">{{
  99. slotProps.item._id
  100. }}</span>
  101. </template>
  102. <template #column-quotaCost="slotProps">
  103. <span :title="slotProps.item.quotaCost">{{
  104. slotProps.item.quotaCost
  105. }}</span>
  106. </template>
  107. <template #column-timestamp="slotProps">
  108. <span :title="new Date(slotProps.item.date)">{{
  109. getDateFormatted(slotProps.item.date)
  110. }}</span>
  111. </template>
  112. <template #column-url="slotProps">
  113. <span :title="slotProps.item.url">{{
  114. slotProps.item.url
  115. }}</span>
  116. </template>
  117. </advanced-table>
  118. </div>
  119. </div>
  120. </template>
  121. <script>
  122. import { mapActions, mapGetters } from "vuex";
  123. import Toast from "toasters";
  124. import AdvancedTable from "@/components/AdvancedTable.vue";
  125. import RunJobDropdown from "@/components/RunJobDropdown.vue";
  126. import LineChart from "@/components/LineChart.vue";
  127. import ws from "@/ws";
  128. export default {
  129. components: {
  130. AdvancedTable,
  131. RunJobDropdown,
  132. LineChart
  133. },
  134. data() {
  135. return {
  136. quotaStatus: {},
  137. fromDate: null,
  138. columnDefault: {
  139. sortable: true,
  140. hidable: true,
  141. defaultVisibility: "shown",
  142. draggable: true,
  143. resizable: true,
  144. minWidth: 150,
  145. maxWidth: 600
  146. },
  147. columns: [
  148. {
  149. name: "options",
  150. displayName: "Options",
  151. properties: ["_id"],
  152. sortable: false,
  153. hidable: false,
  154. resizable: false,
  155. minWidth: 85,
  156. defaultWidth: 85
  157. },
  158. {
  159. name: "quotaCost",
  160. displayName: "Quota Cost",
  161. properties: ["quotaCost"],
  162. sortProperty: ["quotaCost"],
  163. minWidth: 150,
  164. defaultWidth: 150
  165. },
  166. {
  167. name: "timestamp",
  168. displayName: "Timestamp",
  169. properties: ["date"],
  170. sortProperty: ["date"],
  171. minWidth: 150,
  172. defaultWidth: 150
  173. },
  174. {
  175. name: "url",
  176. displayName: "URL",
  177. properties: ["url"],
  178. sortProperty: ["url"]
  179. },
  180. {
  181. name: "_id",
  182. displayName: "Request ID",
  183. properties: ["_id"],
  184. sortProperty: ["_id"],
  185. minWidth: 230,
  186. defaultWidth: 230
  187. }
  188. ],
  189. filters: [
  190. {
  191. name: "_id",
  192. displayName: "Request ID",
  193. property: "_id",
  194. filterTypes: ["exact"],
  195. defaultFilterType: "exact"
  196. },
  197. {
  198. name: "quotaCost",
  199. displayName: "Quota Cost",
  200. property: "quotaCost",
  201. filterTypes: [
  202. "numberLesserEqual",
  203. "numberLesser",
  204. "numberGreater",
  205. "numberGreaterEqual",
  206. "numberEquals"
  207. ],
  208. defaultFilterType: "numberLesser"
  209. },
  210. {
  211. name: "timestamp",
  212. displayName: "Timestamp",
  213. property: "date",
  214. filterTypes: ["datetimeBefore", "datetimeAfter"],
  215. defaultFilterType: "datetimeBefore"
  216. },
  217. {
  218. name: "url",
  219. displayName: "URL",
  220. property: "url",
  221. filterTypes: ["contains", "exact", "regex"],
  222. defaultFilterType: "contains"
  223. }
  224. ],
  225. events: {
  226. adminRoom: "youtube",
  227. removed: {
  228. event: "admin.youtubeApiRequest.removed",
  229. id: "requestId"
  230. }
  231. },
  232. charts: {
  233. quotaUsage: {
  234. labels: [
  235. "Mon",
  236. "Tues",
  237. "Wed",
  238. "Thurs",
  239. "Fri",
  240. "Sat",
  241. "Sun"
  242. ],
  243. datasets: [
  244. {
  245. label: "Type A",
  246. data: [300, 122, 0, 67, 23, 280, 185],
  247. fill: true,
  248. borderColor: "rgb(2, 166, 242)"
  249. }
  250. ]
  251. },
  252. apiRequests: {
  253. labels: [
  254. "Mon",
  255. "Tues",
  256. "Wed",
  257. "Thurs",
  258. "Fri",
  259. "Sat",
  260. "Sun"
  261. ],
  262. datasets: [
  263. {
  264. label: "Type A",
  265. data: [30, 6, 0, 9, 4, 26, 19],
  266. borderColor: "rgb(2, 166, 242)"
  267. }
  268. ]
  269. }
  270. },
  271. jobs: [
  272. {
  273. name: "Reset stored API requests",
  274. socket: "youtube.resetStoredApiRequests"
  275. }
  276. ]
  277. };
  278. },
  279. computed: mapGetters({
  280. socket: "websockets/getSocket"
  281. }),
  282. mounted() {
  283. ws.onConnect(this.init);
  284. },
  285. methods: {
  286. init() {
  287. if (this.$route.query.fromDate)
  288. this.fromDate = this.$route.query.fromDate;
  289. this.socket.dispatch(
  290. "youtube.getQuotaStatus",
  291. this.fromDate,
  292. res => {
  293. if (res.status === "success")
  294. this.quotaStatus = res.data.status;
  295. }
  296. );
  297. },
  298. getDateFormatted(createdAt) {
  299. const date = new Date(createdAt);
  300. const year = date.getFullYear();
  301. const month = `${date.getMonth() + 1}`.padStart(2, 0);
  302. const day = `${date.getDate()}`.padStart(2, 0);
  303. const hour = `${date.getHours()}`.padStart(2, 0);
  304. const minute = `${date.getMinutes()}`.padStart(2, 0);
  305. return `${year}-${month}-${day} ${hour}:${minute}`;
  306. },
  307. removeApiRequest(requestId) {
  308. this.socket.dispatch(
  309. "youtube.removeStoredApiRequest",
  310. requestId,
  311. res => new Toast(res.message)
  312. );
  313. },
  314. ...mapActions("modalVisibility", ["openModal"])
  315. }
  316. };
  317. </script>
  318. <style lang="less" scoped>
  319. .night-mode {
  320. .table {
  321. color: var(--light-grey-2);
  322. background-color: var(--dark-grey-3);
  323. thead tr {
  324. background: var(--dark-grey-3);
  325. td {
  326. color: var(--white);
  327. }
  328. }
  329. tbody tr:hover {
  330. background-color: var(--dark-grey-4) !important;
  331. }
  332. tbody tr:nth-child(even) {
  333. background-color: var(--dark-grey-2);
  334. }
  335. strong {
  336. color: var(--light-grey-2);
  337. }
  338. }
  339. }
  340. td {
  341. vertical-align: middle;
  342. }
  343. .is-primary:focus {
  344. background-color: var(--primary-color) !important;
  345. }
  346. .card.charts {
  347. flex-direction: row !important;
  348. .chart {
  349. width: 50%;
  350. }
  351. @media screen and (max-width: 1100px) {
  352. flex-direction: column !important;
  353. .chart {
  354. width: unset;
  355. &:not(:first-child) {
  356. margin-top: 10px;
  357. }
  358. }
  359. }
  360. }
  361. </style>