YouTube.vue 8.5 KB

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