1<template> 2 <b-container fluid="xl"> 3 <page-title /> 4 <b-row class="align-items-start"> 5 <b-col sm="8" xl="6" class="d-sm-flex align-items-end mb-4"> 6 <search 7 :placeholder="$t('pageEventLogs.table.searchLogs')" 8 data-test-id="eventLogs-input-searchLogs" 9 @change-search="onChangeSearchInput" 10 @clear-search="onClearSearchInput" 11 /> 12 <div class="ms-sm-4"> 13 <table-cell-count 14 :filtered-items-count="filteredRows" 15 :total-number-of-cells="allLogs.length" 16 ></table-cell-count> 17 </div> 18 </b-col> 19 <b-col sm="8" md="7" xl="6"> 20 <table-date-filter @change="onChangeDateTimeFilter" /> 21 </b-col> 22 </b-row> 23 <b-row> 24 <b-col class="text-end"> 25 <table-filter :filters="tableFilters" @filter-change="onFilterChange" /> 26 <b-button 27 variant="link" 28 :disabled="allLogs.length === 0" 29 @click="deleteAllLogs" 30 > 31 <icon-delete /> {{ $t('global.action.deleteAll') }} 32 </b-button> 33 <b-button 34 variant="primary" 35 :class="{ disabled: allLogs.length === 0 }" 36 :download="exportFileNameByDate()" 37 :href="href" 38 > 39 <icon-export /> {{ $t('global.action.exportAll') }} 40 </b-button> 41 </b-col> 42 </b-row> 43 <b-row> 44 <b-col> 45 <table-toolbar 46 ref="toolbar" 47 :selected-items-count="selectedRows.length" 48 :actions="batchActions" 49 @clear-selected="clearSelectedRows($refs.table)" 50 @batch-action="onBatchAction" 51 > 52 <template #toolbar-buttons> 53 <b-button v-if="!hideToggle" variant="primary" @click="resolveLogs"> 54 {{ $t('pageEventLogs.resolve') }} 55 </b-button> 56 <b-button 57 v-if="!hideToggle" 58 variant="primary" 59 @click="unresolveLogs" 60 > 61 {{ $t('pageEventLogs.unresolve') }} 62 </b-button> 63 <table-toolbar-export 64 :data="batchExportData" 65 :file-name="exportFileNameByDate()" 66 /> 67 </template> 68 </table-toolbar> 69 <b-table 70 id="table-event-logs" 71 ref="table" 72 responsive="md" 73 selectable 74 no-select-on-click 75 sort-icon-left 76 hover 77 must-sort 78 thead-class="table-light" 79 :sort-desc="[true]" 80 show-empty 81 :sort-by="['id']" 82 :fields="fields" 83 :items="filteredLogs" 84 :empty-text="$t('global.table.emptyMessage')" 85 :empty-filtered-text="$t('global.table.emptySearchMessage')" 86 :per-page="perPage" 87 :current-page="currentPage" 88 :filter="searchFilter" 89 :busy="isBusy" 90 @filtered="onFiltered" 91 @row-selected="onRowSelected($event, filteredLogs.length)" 92 > 93 <!-- Checkbox column --> 94 <template #head(checkbox)> 95 <b-form-checkbox 96 v-model="tableHeaderCheckboxModel" 97 data-test-id="eventLogs-checkbox-selectAll" 98 :indeterminate="tableHeaderCheckboxIndeterminate" 99 @change="onChangeHeaderCheckbox($refs.table, $event)" 100 > 101 <span class="visually-hidden-focusable"> 102 {{ $t('global.table.selectAll') }} 103 </span> 104 </b-form-checkbox> 105 </template> 106 <template #cell(checkbox)="row"> 107 <b-form-checkbox 108 v-model="row.rowSelected" 109 :data-test-id="`eventLogs-checkbox-selectRow-${row.index}`" 110 @change="toggleSelectRow($refs.table, row.index)" 111 > 112 <span class="visually-hidden-focusable"> 113 {{ $t('global.table.selectItem') }} 114 </span> 115 </b-form-checkbox> 116 </template> 117 118 <!-- Expand chevron icon --> 119 <template #cell(expandRow)="row"> 120 <b-button 121 variant="link" 122 :aria-label="expandRowLabel" 123 :title="expandRowLabel" 124 class="btn-icon-only" 125 @click="toggleRowDetails(row)" 126 > 127 <icon-chevron v-if="!row.detailsShowing" /> 128 <icon-chevron-up v-else /> 129 </b-button> 130 </template> 131 132 <template #row-details="{ item }"> 133 <b-container fluid> 134 <b-row> 135 <b-col> 136 <dl> 137 <!-- Name --> 138 <dt>{{ $t('pageEventLogs.table.name') }}:</dt> 139 <dd>{{ dataFormatter(item.name) }}</dd> 140 </dl> 141 <dl> 142 <!-- Type --> 143 <dt>{{ $t('pageEventLogs.table.type') }}:</dt> 144 <dd>{{ dataFormatter(item.type) }}</dd> 145 </dl> 146 </b-col> 147 <b-col> 148 <dl> 149 <!-- Modified date --> 150 <dt>{{ $t('pageEventLogs.table.modifiedDate') }}:</dt> 151 <dd v-if="item.modifiedDate"> 152 {{ $filters.formatDate(item.modifiedDate) }} 153 {{ $filters.formatTime(item.modifiedDate) }} 154 </dd> 155 <dd v-else>--</dd> 156 </dl> 157 </b-col> 158 <b-col class="text-nowrap"> 159 <b-button @click="downloadEntry(item.additionalDataUri)"> 160 <icon-download />{{ $t('pageEventLogs.additionalDataUri') }} 161 </b-button> 162 </b-col> 163 </b-row> 164 </b-container> 165 </template> 166 167 <!-- Severity column --> 168 <template #cell(severity)="{ value }"> 169 <status-icon v-if="value" :status="statusIcon(value)" /> 170 {{ value }} 171 </template> 172 <!-- Date column --> 173 <template #cell(date)="{ value }"> 174 <p class="mb-0">{{ $filters.formatDate(value) }}</p> 175 <p class="mb-0">{{ $filters.formatTime(value) }}</p> 176 </template> 177 178 <!-- Status column --> 179 <template #cell(status)="row"> 180 <b-form-checkbox 181 v-model="row.item.status" 182 name="switch" 183 switch 184 @change="changelogStatus(row.item)" 185 > 186 <span v-if="row.item.status"> 187 {{ $t('pageEventLogs.resolved') }} 188 </span> 189 <span v-else> 190 {{ $t('pageEventLogs.unresolved') }} 191 </span> 192 </b-form-checkbox> 193 </template> 194 <template #cell(filterByStatus)="{ value }"> 195 {{ value }} 196 </template> 197 198 <!-- Actions column --> 199 <template #cell(actions)="row"> 200 <table-row-action 201 v-for="(action, index) in row.item.actions" 202 :key="index" 203 :value="action.value" 204 :title="action.title" 205 :row-data="row.item" 206 :export-name="exportFileNameByDate('export')" 207 :data-test-id="`eventLogs-button-deleteRow-${row.index}`" 208 @click-table-action="onTableRowAction($event, row.item)" 209 > 210 <template #icon> 211 <icon-export v-if="action.value === 'export'" /> 212 <icon-trashcan v-if="action.value === 'delete'" /> 213 </template> 214 </table-row-action> 215 </template> 216 </b-table> 217 </b-col> 218 </b-row> 219 220 <!-- Table pagination --> 221 <b-row> 222 <b-col sm="6"> 223 <b-form-group 224 class="table-pagination-select" 225 :label="$t('global.table.itemsPerPage')" 226 label-for="pagination-items-per-page" 227 > 228 <b-form-select 229 id="pagination-items-per-page" 230 v-model="perPage" 231 :options="itemsPerPageOptions" 232 /> 233 </b-form-group> 234 </b-col> 235 <b-col sm="6"> 236 <b-pagination 237 v-model="currentPage" 238 first-number 239 last-number 240 :per-page="perPage" 241 :total-rows="getTotalRowCount(filteredRows)" 242 aria-controls="table-event-logs" 243 /> 244 </b-col> 245 </b-row> 246 </b-container> 247</template> 248 249<script> 250import IconDelete from '@carbon/icons-vue/es/trash-can/20'; 251import IconTrashcan from '@carbon/icons-vue/es/trash-can/20'; 252import IconExport from '@carbon/icons-vue/es/document--export/20'; 253import IconChevron from '@carbon/icons-vue/es/chevron--down/20'; 254import IconChevronUp from '@carbon/icons-vue/es/chevron--up/20'; 255import IconDownload from '@carbon/icons-vue/es/download/20'; 256import { omit } from 'lodash'; 257 258import PageTitle from '@/components/Global/PageTitle'; 259import StatusIcon from '@/components/Global/StatusIcon'; 260import Search from '@/components/Global/Search'; 261import TableCellCount from '@/components/Global/TableCellCount'; 262import TableDateFilter from '@/components/Global/TableDateFilter'; 263import TableFilter from '@/components/Global/TableFilter'; 264import TableRowAction from '@/components/Global/TableRowAction'; 265import TableToolbar from '@/components/Global/TableToolbar'; 266import TableToolbarExport from '@/components/Global/TableToolbarExport'; 267 268import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin'; 269import TableFilterMixin from '@/components/Mixins/TableFilterMixin'; 270import BVPaginationMixin, { 271 currentPage, 272 perPage, 273 itemsPerPageOptions, 274} from '@/components/Mixins/BVPaginationMixin'; 275import BVTableSelectableMixin, { 276 selectedRows, 277 tableHeaderCheckboxModel, 278 tableHeaderCheckboxIndeterminate, 279} from '@/components/Mixins/BVTableSelectableMixin'; 280import BVToastMixin from '@/components/Mixins/BVToastMixin'; 281import DataFormatterMixin from '@/components/Mixins/DataFormatterMixin'; 282import TableSortMixin from '@/components/Mixins/TableSortMixin'; 283import TableRowExpandMixin, { 284 expandRowLabel, 285} from '@/components/Mixins/TableRowExpandMixin'; 286import SearchFilterMixin, { 287 searchFilter, 288} from '@/components/Mixins/SearchFilterMixin'; 289import { useI18n } from 'vue-i18n'; 290import i18n from '@/i18n'; 291import { useModal } from 'bootstrap-vue-next'; 292 293export default { 294 components: { 295 IconDelete, 296 IconExport, 297 IconTrashcan, 298 IconChevron, 299 IconChevronUp, 300 IconDownload, 301 PageTitle, 302 Search, 303 StatusIcon, 304 TableCellCount, 305 TableFilter, 306 TableRowAction, 307 TableToolbar, 308 TableToolbarExport, 309 TableDateFilter, 310 }, 311 mixins: [ 312 BVPaginationMixin, 313 BVTableSelectableMixin, 314 BVToastMixin, 315 LoadingBarMixin, 316 TableFilterMixin, 317 DataFormatterMixin, 318 TableSortMixin, 319 TableRowExpandMixin, 320 SearchFilterMixin, 321 ], 322 beforeRouteLeave(to, from, next) { 323 // Hide loader if the user navigates to another page 324 // before request is fulfilled. 325 this.hideLoader(); 326 next(); 327 }, 328 setup() { 329 const bvModal = useModal(); 330 return { bvModal }; 331 }, 332 data() { 333 return { 334 $t: useI18n().t, 335 isBusy: true, 336 fields: [ 337 { 338 key: 'expandRow', 339 label: '', 340 tdClass: 'table-row-expand', 341 }, 342 { 343 key: 'checkbox', 344 sortable: false, 345 }, 346 { 347 key: 'id', 348 label: i18n.global.t('pageEventLogs.table.id'), 349 sortable: true, 350 }, 351 { 352 key: 'severity', 353 label: i18n.global.t('pageEventLogs.table.severity'), 354 sortable: true, 355 tdClass: 'text-nowrap', 356 }, 357 { 358 key: 'date', 359 label: i18n.global.t('pageEventLogs.table.date'), 360 sortable: true, 361 tdClass: 'text-nowrap', 362 }, 363 { 364 key: 'description', 365 label: i18n.global.t('pageEventLogs.table.description'), 366 tdClass: 'text-break', 367 }, 368 process.env.VUE_APP_EVENT_LOGS_TOGGLE_BUTTON_DISABLED === 'true' 369 ? {} 370 : { 371 key: 'status', 372 label: i18n.global.t('pageEventLogs.table.status'), 373 }, 374 { 375 key: 'actions', 376 sortable: false, 377 label: '', 378 tdClass: 'text-end text-nowrap', 379 }, 380 ], 381 tableFilters: 382 process.env.VUE_APP_EVENT_LOGS_TOGGLE_BUTTON_DISABLED === 'true' 383 ? [ 384 { 385 key: 'severity', 386 label: i18n.global.t('pageEventLogs.table.severity'), 387 values: ['OK', 'Warning', 'Critical'], 388 }, 389 ] 390 : [ 391 { 392 key: 'severity', 393 label: i18n.global.t('pageEventLogs.table.severity'), 394 values: ['OK', 'Warning', 'Critical'], 395 }, 396 { 397 key: 'filterByStatus', 398 label: i18n.global.t('pageEventLogs.table.status'), 399 values: ['Resolved', 'Unresolved'], 400 }, 401 ], 402 expandRowLabel, 403 activeFilters: [], 404 batchActions: 405 process.env.VUE_APP_EVENT_LOGS_DELETE_BUTTON_DISABLED === 'true' 406 ? [] 407 : [ 408 { 409 value: 'delete', 410 label: i18n.global.t('global.action.delete'), 411 }, 412 ], 413 currentPage: currentPage, 414 filterStartDate: null, 415 filterEndDate: null, 416 itemsPerPageOptions: itemsPerPageOptions, 417 perPage: perPage, 418 searchFilter: searchFilter, 419 searchTotalFilteredRows: 0, 420 selectedRows: selectedRows, 421 tableHeaderCheckboxModel: tableHeaderCheckboxModel, 422 tableHeaderCheckboxIndeterminate: tableHeaderCheckboxIndeterminate, 423 hideToggle: 424 process.env.VUE_APP_EVENT_LOGS_TOGGLE_BUTTON_DISABLED === 'true', 425 hideDelete: 426 process.env.VUE_APP_EVENT_LOGS_DELETE_BUTTON_DISABLED === 'true', 427 }; 428 }, 429 computed: { 430 href() { 431 return `data:text/json;charset=utf-8,${this.exportAllLogs()}`; 432 }, 433 filteredRows() { 434 return this.searchFilter 435 ? this.searchTotalFilteredRows 436 : this.filteredLogs.length; 437 }, 438 allLogs() { 439 return this.$store.getters['eventLog/allEvents'].map((event) => { 440 return { 441 ...event, 442 actions: this.hideDelete 443 ? [ 444 { 445 value: 'export', 446 title: i18n.global.t('global.action.export'), 447 }, 448 ] 449 : [ 450 { 451 value: 'export', 452 title: i18n.global.t('global.action.export'), 453 }, 454 { 455 value: 'delete', 456 title: i18n.global.t('global.action.delete'), 457 }, 458 ], 459 }; 460 }); 461 }, 462 batchExportData() { 463 return this.selectedRows.map((row) => omit(row, 'actions')); 464 }, 465 filteredLogsByDate() { 466 return this.getFilteredTableDataByDate( 467 this.allLogs, 468 this.filterStartDate, 469 this.filterEndDate, 470 ); 471 }, 472 filteredLogs() { 473 return this.getFilteredTableData( 474 this.filteredLogsByDate, 475 this.activeFilters, 476 ); 477 }, 478 }, 479 created() { 480 this.startLoader(); 481 this.$store.dispatch('eventLog/getEventLogData').finally(() => { 482 this.endLoader(); 483 this.isBusy = false; 484 }); 485 }, 486 methods: { 487 downloadEntry(uri) { 488 let filename = uri?.split('LogServices/')?.[1]; 489 filename.replace(RegExp('/', 'g'), '_'); 490 this.$store 491 .dispatch('eventLog/downloadEntry', uri) 492 .then((blob) => { 493 const link = document.createElement('a'); 494 link.href = URL.createObjectURL(blob); 495 link.download = filename; 496 link.click(); 497 URL.revokeObjectURL(link.href); 498 }) 499 .catch(({ message }) => this.errorToast(message)); 500 }, 501 changelogStatus(row) { 502 this.$store 503 .dispatch('eventLog/updateEventLogStatus', { 504 uri: row.uri, 505 status: row.status, 506 }) 507 .then((success) => { 508 this.successToast(success); 509 }) 510 .catch(({ message }) => this.errorToast(message)); 511 }, 512 async deleteAllLogs() { 513 const ok = await this.confirmDialog( 514 i18n.global.t('pageEventLogs.modal.deleteAllMessage'), 515 { 516 title: i18n.global.t('pageEventLogs.modal.deleteAllTitle'), 517 okTitle: i18n.global.t('global.action.delete'), 518 okVariant: 'danger', 519 cancelTitle: i18n.global.t('global.action.cancel'), 520 autoFocusButton: 'cancel', 521 }, 522 ); 523 if (ok) { 524 this.$store 525 .dispatch('eventLog/deleteAllEventLogs', this.allLogs) 526 .then((message) => this.successToast(message)) 527 .catch(({ message }) => this.errorToast(message)); 528 } 529 }, 530 deleteLogs(uris) { 531 this.$store 532 .dispatch('eventLog/deleteEventLogs', uris) 533 .then((messages) => { 534 messages.forEach(({ type, message }) => { 535 if (type === 'success') { 536 this.successToast(message); 537 } else if (type === 'error') { 538 this.errorToast(message); 539 } 540 }); 541 }); 542 }, 543 exportAllLogs() { 544 { 545 return this.$store.getters['eventLog/allEvents'].map((eventLogs) => { 546 const allEventLogsString = JSON.stringify(eventLogs); 547 return allEventLogsString; 548 }); 549 } 550 }, 551 onFilterChange({ activeFilters }) { 552 this.activeFilters = activeFilters; 553 }, 554 onTableRowAction(action, { uri }) { 555 if (action === 'delete') { 556 this.confirmDialog(i18n.global.t('pageEventLogs.modal.deleteMessage'), { 557 title: i18n.global.t('pageEventLogs.modal.deleteTitle'), 558 okTitle: i18n.global.t('global.action.delete'), 559 cancelTitle: i18n.global.t('global.action.cancel'), 560 autoFocusButton: 'ok', 561 }).then((deleteConfirmed) => { 562 if (deleteConfirmed) this.deleteLogs([uri]); 563 }); 564 } 565 }, 566 async onBatchAction(action) { 567 if (action === 'delete') { 568 const uris = this.selectedRows.map((row) => row.uri); 569 const count = this.selectedRows.length; 570 const ok = await this.confirmDialog( 571 i18n.global.t('pageEventLogs.modal.deleteMessage', count), 572 { 573 title: i18n.global.t('pageEventLogs.modal.deleteTitle', count), 574 okTitle: i18n.global.t('global.action.delete'), 575 cancelTitle: i18n.global.t('global.action.cancel'), 576 autoFocusButton: 'ok', 577 }, 578 ); 579 if (ok) { 580 if (this.selectedRows.length === this.allLogs.length) { 581 this.$store 582 .dispatch('eventLog/deleteAllEventLogs', this.selectedRows.length) 583 .then(() => { 584 this.successToast( 585 i18n.global.t( 586 'pageEventLogs.toast.successDelete', 587 uris.length, 588 ), 589 ); 590 }) 591 .catch(({ message }) => this.errorToast(message)); 592 } else { 593 this.deleteLogs(uris); 594 } 595 } 596 } 597 }, 598 onChangeDateTimeFilter({ fromDate, toDate }) { 599 this.filterStartDate = fromDate; 600 this.filterEndDate = toDate; 601 }, 602 onFiltered(filteredItems) { 603 this.searchTotalFilteredRows = filteredItems.length; 604 }, 605 // Create export file name based on date 606 exportFileNameByDate(value) { 607 let date = new Date(); 608 date = 609 date.toISOString().slice(0, 10) + 610 '_' + 611 date.toString().split(':').join('-').split(' ')[4]; 612 let fileName; 613 if (value === 'export') { 614 fileName = 'event_log_'; 615 } else { 616 fileName = 'all_event_logs_'; 617 } 618 return fileName + date; 619 }, 620 resolveLogs() { 621 this.$store 622 .dispatch('eventLog/resolveEventLogs', this.selectedRows) 623 .then((messages) => { 624 messages.forEach(({ type, message }) => { 625 if (type === 'success') { 626 this.successToast(message); 627 } else if (type === 'error') { 628 this.errorToast(message); 629 } 630 }); 631 }); 632 }, 633 unresolveLogs() { 634 this.$store 635 .dispatch('eventLog/unresolveEventLogs', this.selectedRows) 636 .then((messages) => { 637 messages.forEach(({ type, message }) => { 638 if (type === 'success') { 639 this.successToast(message); 640 } else if (type === 'error') { 641 this.errorToast(message); 642 } 643 }); 644 }); 645 }, 646 confirmDialog(message, options = {}) { 647 return this.$confirm({ message, ...options }); 648 }, 649 }, 650}; 651</script> 652