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