Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
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.vue216
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