xref: /openbmc/webui-vue/src/views/Logs/EventLogs/EventLogs.vue (revision d36ac8a8be8636ddd0e64ce005d507b21bcdeb00)
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