diff options
Diffstat (limited to 'app/assets/javascripts/alert_management/components/alert_management_list.vue')
-rw-r--r-- | app/assets/javascripts/alert_management/components/alert_management_list.vue | 216 |
1 files changed, 187 insertions, 29 deletions
diff --git a/app/assets/javascripts/alert_management/components/alert_management_list.vue b/app/assets/javascripts/alert_management/components/alert_management_list.vue index 74fc19ff3d4..37901c21f9b 100644 --- a/app/assets/javascripts/alert_management/components/alert_management_list.vue +++ b/app/assets/javascripts/alert_management/components/alert_management_list.vue @@ -10,23 +10,41 @@ import { GlDropdownItem, GlTabs, GlTab, + GlBadge, + GlPagination, } from '@gitlab/ui'; import createFlash from '~/flash'; import { s__ } from '~/locale'; import { joinPaths, visitUrl } from '~/lib/utils/url_utility'; +import { fetchPolicies } from '~/lib/graphql'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; -import getAlerts from '../graphql/queries/getAlerts.query.graphql'; -import { ALERTS_STATUS, ALERTS_STATUS_TABS, ALERTS_SEVERITY_LABELS } from '../constants'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import getAlerts from '../graphql/queries/get_alerts.query.graphql'; +import getAlertsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql'; +import { + ALERTS_STATUS_TABS, + ALERTS_SEVERITY_LABELS, + DEFAULT_PAGE_SIZE, + trackAlertListViewsOptions, + trackAlertStatusUpdateOptions, +} from '../constants'; import updateAlertStatus from '../graphql/mutations/update_alert_status.graphql'; -import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; +import { convertToSnakeCase } from '~/lib/utils/text_utility'; +import Tracking from '~/tracking'; -const tdClass = 'table-col d-flex d-md-table-cell align-items-center'; +const tdClass = 'table-col gl-display-flex d-md-table-cell gl-align-items-center'; +const thClass = 'gl-hover-bg-blue-50'; const bodyTrClass = - 'gl-border-1 gl-border-t-solid gl-border-gray-100 hover-bg-blue-50 hover-gl-cursor-pointer hover-gl-border-b-solid hover-gl-border-blue-200'; + 'gl-border-1 gl-border-t-solid gl-border-gray-100 gl-hover-bg-blue-50 gl-hover-cursor-pointer gl-hover-border-b-solid gl-hover-border-blue-200'; + +const initialPaginationState = { + currentPage: 1, + prevPageCursor: '', + nextPageCursor: '', + firstPageSize: DEFAULT_PAGE_SIZE, + lastPageSize: null, +}; export default { - bodyTrClass, i18n: { noAlertsMsg: s__( "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page.", @@ -40,40 +58,54 @@ export default { key: 'severity', label: s__('AlertManagement|Severity'), tdClass: `${tdClass} rounded-top text-capitalize`, + thClass, + sortable: true, }, { key: 'startedAt', label: s__('AlertManagement|Start time'), + thClass: `${thClass} js-started-at`, tdClass, + sortable: true, }, { key: 'endedAt', label: s__('AlertManagement|End time'), + thClass, tdClass, + sortable: true, }, { key: 'title', label: s__('AlertManagement|Alert'), - thClass: 'w-30p', + thClass: `${thClass} w-30p gl-pointer-events-none`, tdClass, + sortable: false, }, { key: 'eventCount', label: s__('AlertManagement|Events'), - thClass: 'text-right event-count', - tdClass: `${tdClass} text-md-right event-count`, + thClass: `${thClass} text-right gl-pr-9 w-3rem`, + tdClass: `${tdClass} text-md-right`, + sortable: true, + }, + { + key: 'assignees', + label: s__('AlertManagement|Assignees'), + tdClass, }, { key: 'status', - thClass: 'w-15p', + thClass: `${thClass} w-15p`, label: s__('AlertManagement|Status'), tdClass: `${tdClass} rounded-bottom`, + sortable: true, }, ], statuses: { - [ALERTS_STATUS.TRIGGERED]: s__('AlertManagement|Triggered'), - [ALERTS_STATUS.ACKNOWLEDGED]: s__('AlertManagement|Acknowledged'), - [ALERTS_STATUS.RESOLVED]: s__('AlertManagement|Resolved'), + TRIGGERED: s__('AlertManagement|Triggered'), + ACKNOWLEDGED: s__('AlertManagement|Acknowledged'), + RESOLVED: s__('AlertManagement|Resolved'), }, severityLabels: ALERTS_SEVERITY_LABELS, statusTabs: ALERTS_STATUS_TABS, @@ -89,8 +121,9 @@ export default { GlIcon, GlTabs, GlTab, + GlBadge, + GlPagination, }, - mixins: [glFeatureFlagsMixin()], props: { projectPath: { type: String, @@ -115,33 +148,63 @@ export default { }, apollo: { alerts: { + fetchPolicy: fetchPolicies.CACHE_AND_NETWORK, query: getAlerts, variables() { return { projectPath: this.projectPath, statuses: this.statusFilter, + sort: this.sort, + firstPageSize: this.pagination.firstPageSize, + lastPageSize: this.pagination.lastPageSize, + prevPageCursor: this.pagination.prevPageCursor, + nextPageCursor: this.pagination.nextPageCursor, }; }, update(data) { - return data.project.alertManagementAlerts.nodes; + const { alertManagementAlerts: { nodes: list = [], pageInfo = {} } = {} } = + data.project || {}; + + return { + list, + pageInfo, + }; }, error() { this.errored = true; }, }, + alertsCount: { + query: getAlertsCountByStatus, + variables() { + return { + projectPath: this.projectPath, + }; + }, + update(data) { + return data.project?.alertManagementAlertStatusCounts; + }, + }, }, data() { return { - alerts: null, errored: false, isAlertDismissed: false, isErrorAlertDismissed: false, - statusFilter: this.$options.statusTabs[4].filters, + sort: 'STARTED_AT_DESC', + statusFilter: [], + filteredByStatus: '', + pagination: initialPaginationState, + sortBy: 'startedAt', + sortDesc: true, + sortDirection: 'desc', }; }, computed: { showNoAlertsMsg() { - return !this.errored && !this.loading && !this.alerts?.length && !this.isAlertDismissed; + return ( + !this.errored && !this.loading && this.alertsCount?.all === 0 && !this.isAlertDismissed + ); }, showErrorMsg() { return this.errored && !this.isErrorAlertDismissed; @@ -149,12 +212,43 @@ export default { loading() { return this.$apollo.queries.alerts.loading; }, + hasAlerts() { + return this.alerts?.list?.length; + }, + tbodyTrClass() { + return !this.loading && this.hasAlerts ? bodyTrClass : ''; + }, + showPaginationControls() { + return Boolean(this.prevPage || this.nextPage); + }, + alertsForCurrentTab() { + return this.alertsCount ? this.alertsCount[this.filteredByStatus.toLowerCase()] : 0; + }, + prevPage() { + return Math.max(this.pagination.currentPage - 1, 0); + }, + nextPage() { + const nextPage = this.pagination.currentPage + 1; + return nextPage > Math.ceil(this.alertsForCurrentTab / DEFAULT_PAGE_SIZE) ? null : nextPage; + }, + }, + mounted() { + this.trackPageViews(); }, methods: { filterAlertsByStatus(tabIndex) { - this.statusFilter = this.$options.statusTabs[tabIndex].filters; + this.resetPagination(); + const { filters, status } = this.$options.statusTabs[tabIndex]; + this.statusFilter = filters; + this.filteredByStatus = status; + }, + fetchSortedData({ sortBy, sortDesc }) { + const sortingDirection = sortDesc ? 'DESC' : 'ASC'; + const sortingColumn = convertToSnakeCase(sortBy).toUpperCase(); + + this.resetPagination(); + this.sort = `${sortingColumn}_${sortingDirection}`; }, - capitalizeFirstCharacter, updateAlertStatus(status, iid) { this.$apollo .mutate({ @@ -166,7 +260,10 @@ export default { }, }) .then(() => { + this.trackStatusUpdate(status); this.$apollo.queries.alerts.refetch(); + this.$apollo.queries.alertsCount.refetch(); + this.resetPagination(); }) .catch(() => { createFlash( @@ -179,6 +276,42 @@ export default { navigateToAlertDetails({ iid }) { return visitUrl(joinPaths(window.location.pathname, iid, 'details')); }, + trackPageViews() { + const { category, action } = trackAlertListViewsOptions; + Tracking.event(category, action); + }, + trackStatusUpdate(status) { + const { category, action, label } = trackAlertStatusUpdateOptions; + Tracking.event(category, action, { label, property: status }); + }, + getAssignees(assignees) { + // TODO: Update to show list of assignee(s) after https://gitlab.com/gitlab-org/gitlab/-/issues/218405 + return assignees.nodes?.length > 0 + ? assignees.nodes[0]?.username + : s__('AlertManagement|Unassigned'); + }, + handlePageChange(page) { + const { startCursor, endCursor } = this.alerts.pageInfo; + + if (page > this.pagination.currentPage) { + this.pagination = { + ...initialPaginationState, + nextPageCursor: endCursor, + currentPage: page, + }; + } else { + this.pagination = { + lastPageSize: DEFAULT_PAGE_SIZE, + firstPageSize: null, + prevPageCursor: startCursor, + nextPageCursor: '', + currentPage: page, + }; + } + }, + resetPagination() { + this.pagination = initialPaginationState; + }, }, }; </script> @@ -192,10 +325,13 @@ export default { {{ $options.i18n.errorMsg }} </gl-alert> - <gl-tabs v-if="glFeatures.alertListStatusFilteringEnabled" @input="filterAlertsByStatus"> + <gl-tabs @input="filterAlertsByStatus"> <gl-tab v-for="tab in $options.statusTabs" :key="tab.status"> <template slot="title"> <span>{{ tab.title }}</span> + <gl-badge v-if="alertsCount" pill size="sm" class="gl-tab-counter-badge"> + {{ alertsCount[tab.status.toLowerCase()] }} + </gl-badge> </template> </gl-tab> </gl-tabs> @@ -205,13 +341,19 @@ export default { </h4> <gl-table class="alert-management-table mt-3" - :items="alerts" + :items="alerts ? alerts.list : []" :fields="$options.fields" :show-empty="true" :busy="loading" stacked="md" - :tbody-tr-class="$options.bodyTrClass" + :tbody-tr-class="tbodyTrClass" + :no-local-sorting="true" + :sort-direction="sortDirection" + :sort-desc.sync="sortDesc" + :sort-by.sync="sortBy" + sort-icon-left @row-clicked="navigateToAlertDetails" + @sort-changed="fetchSortedData" > <template #cell(severity)="{ item }"> <div @@ -236,16 +378,22 @@ export default { <time-ago v-if="item.endedAt" :time="item.endedAt" /> </template> + <template #cell(eventCount)="{ item }"> + {{ item.eventCount }} + </template> + <template #cell(title)="{ item }"> <div class="gl-max-w-full text-truncate">{{ item.title }}</div> </template> + <template #cell(assignees)="{ item }"> + <div class="gl-max-w-full text-truncate" data-testid="assigneesField"> + {{ getAssignees(item.assignees) }} + </div> + </template> + <template #cell(status)="{ item }"> - <gl-dropdown - :text="capitalizeFirstCharacter(item.status.toLowerCase())" - class="w-100" - right - > + <gl-dropdown :text="$options.statuses[item.status]" class="w-100" right> <gl-dropdown-item v-for="(label, field) in $options.statuses" :key="field" @@ -271,6 +419,16 @@ export default { <gl-loading-icon size="lg" color="dark" class="mt-3" /> </template> </gl-table> + + <gl-pagination + v-if="showPaginationControls" + :value="pagination.currentPage" + :prev-page="prevPage" + :next-page="nextPage" + align="center" + class="gl-pagination prepend-top-default" + @input="handlePageChange" + /> </div> <gl-empty-state v-else |