Commit d0155a7c authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch 'lm-sorting-list' into 'master'

Implement sorting by column in alert management list

See merge request gitlab-org/gitlab!32478
parents 69d6ac52 8ad897d1
...@@ -21,11 +21,12 @@ import getAlerts from '../graphql/queries/get_alerts.query.graphql'; ...@@ -21,11 +21,12 @@ import getAlerts from '../graphql/queries/get_alerts.query.graphql';
import getAlertsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql'; import getAlertsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
import { ALERTS_STATUS, ALERTS_STATUS_TABS, ALERTS_SEVERITY_LABELS } from '../constants'; import { ALERTS_STATUS, ALERTS_STATUS_TABS, ALERTS_SEVERITY_LABELS } from '../constants';
import updateAlertStatus from '../graphql/mutations/update_alert_status.graphql'; import updateAlertStatus from '../graphql/mutations/update_alert_status.graphql';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; import { capitalizeFirstCharacter, convertToSnakeCase } from '~/lib/utils/text_utility';
const tdClass = 'table-col d-flex d-md-table-cell align-items-center'; const tdClass = 'table-col d-flex d-md-table-cell align-items-center';
const bodyTrClass = 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 hover-bg-blue-50 hover-gl-cursor-pointer hover-gl-border-b-solid hover-gl-border-blue-200';
const findDefaultSortColumn = () => document.querySelector('.js-started-at');
export default { export default {
i18n: { i18n: {
...@@ -41,34 +42,41 @@ export default { ...@@ -41,34 +42,41 @@ export default {
key: 'severity', key: 'severity',
label: s__('AlertManagement|Severity'), label: s__('AlertManagement|Severity'),
tdClass: `${tdClass} rounded-top text-capitalize`, tdClass: `${tdClass} rounded-top text-capitalize`,
sortable: true,
}, },
{ {
key: 'startedAt', key: 'startTime',
label: s__('AlertManagement|Start time'), label: s__('AlertManagement|Start time'),
thClass: 'js-started-at',
tdClass, tdClass,
sortable: true,
}, },
{ {
key: 'endedAt', key: 'endTime',
label: s__('AlertManagement|End time'), label: s__('AlertManagement|End time'),
tdClass, tdClass,
sortable: true,
}, },
{ {
key: 'title', key: 'title',
label: s__('AlertManagement|Alert'), label: s__('AlertManagement|Alert'),
thClass: 'w-30p', thClass: 'w-30p alert-title',
tdClass, tdClass,
sortable: false,
}, },
{ {
key: 'eventCount', key: 'eventsCount',
label: s__('AlertManagement|Events'), label: s__('AlertManagement|Events'),
thClass: 'text-right gl-pr-9', thClass: 'text-right gl-pr-9 w-3rem',
tdClass: `${tdClass} text-md-right`, tdClass: `${tdClass} text-md-right`,
sortable: true,
}, },
{ {
key: 'status', key: 'status',
thClass: 'w-15p', thClass: 'w-15p',
label: s__('AlertManagement|Status'), label: s__('AlertManagement|Status'),
tdClass: `${tdClass} rounded-bottom`, tdClass: `${tdClass} rounded-bottom`,
sortable: true,
}, },
], ],
statuses: { statuses: {
...@@ -122,6 +130,7 @@ export default { ...@@ -122,6 +130,7 @@ export default {
return { return {
projectPath: this.projectPath, projectPath: this.projectPath,
statuses: this.statusFilter, statuses: this.statusFilter,
sort: this.sort,
}; };
}, },
update(data) { update(data) {
...@@ -148,6 +157,7 @@ export default { ...@@ -148,6 +157,7 @@ export default {
errored: false, errored: false,
isAlertDismissed: false, isAlertDismissed: false,
isErrorAlertDismissed: false, isErrorAlertDismissed: false,
sort: 'START_TIME_ASC',
statusFilter: this.$options.statusTabs[4].filters, statusFilter: this.$options.statusTabs[4].filters,
}; };
}, },
...@@ -170,10 +180,22 @@ export default { ...@@ -170,10 +180,22 @@ export default {
return !this.loading && this.hasAlerts ? bodyTrClass : ''; return !this.loading && this.hasAlerts ? bodyTrClass : '';
}, },
}, },
mounted() {
findDefaultSortColumn().ariaSort = 'ascending';
},
methods: { methods: {
filterAlertsByStatus(tabIndex) { filterAlertsByStatus(tabIndex) {
this.statusFilter = this.$options.statusTabs[tabIndex].filters; this.statusFilter = this.$options.statusTabs[tabIndex].filters;
}, },
fetchSortedData({ sortBy, sortDesc }) {
const sortDirection = sortDesc ? 'DESC' : 'ASC';
const sortColumn = convertToSnakeCase(sortBy).toUpperCase();
if (sortBy !== 'startTime') {
findDefaultSortColumn().ariaSort = 'none';
}
this.sort = `${sortColumn}_${sortDirection}`;
},
capitalizeFirstCharacter, capitalizeFirstCharacter,
updateAlertStatus(status, iid) { updateAlertStatus(status, iid) {
this.$apollo this.$apollo
...@@ -235,7 +257,10 @@ export default { ...@@ -235,7 +257,10 @@ export default {
:busy="loading" :busy="loading"
stacked="md" stacked="md"
:tbody-tr-class="tbodyTrClass" :tbody-tr-class="tbodyTrClass"
:no-local-sorting="true"
sort-icon-left
@row-clicked="navigateToAlertDetails" @row-clicked="navigateToAlertDetails"
@sort-changed="fetchSortedData"
> >
<template #cell(severity)="{ item }"> <template #cell(severity)="{ item }">
<div <div
...@@ -252,13 +277,17 @@ export default { ...@@ -252,13 +277,17 @@ export default {
</div> </div>
</template> </template>
<template #cell(startedAt)="{ item }"> <template #cell(startTime)="{ item }">
<time-ago v-if="item.startedAt" :time="item.startedAt" /> <time-ago v-if="item.startedAt" :time="item.startedAt" />
</template> </template>
<template #cell(endedAt)="{ item }"> <template #cell(endTime)="{ item }">
<time-ago v-if="item.endedAt" :time="item.endedAt" /> <time-ago v-if="item.endedAt" :time="item.endedAt" />
</template> </template>
<!-- TODO: Remove after: https://gitlab.com/gitlab-org/gitlab/-/issues/218467 -->
<template #cell(eventsCount)="{ item }">
{{ item.eventCount }}
</template>
<template #cell(title)="{ item }"> <template #cell(title)="{ item }">
<div class="gl-max-w-full text-truncate">{{ item.title }}</div> <div class="gl-max-w-full text-truncate">{{ item.title }}</div>
......
#import "../fragments/list_item.fragment.graphql" #import "../fragments/list_item.fragment.graphql"
query getAlerts($projectPath: ID!, $statuses: [AlertManagementStatus!]) { query getAlerts($projectPath: ID!, $statuses: [AlertManagementStatus!], $sort: AlertManagementAlertSort ) {
project(fullPath: $projectPath) { project(fullPath: $projectPath) {
alertManagementAlerts(statuses: $statuses) { alertManagementAlerts(statuses: $statuses, sort: $sort) {
nodes { nodes {
...AlertListItem ...AlertListItem
} }
......
...@@ -55,3 +55,26 @@ $tooltip-padding-y: 0.5rem; ...@@ -55,3 +55,26 @@ $tooltip-padding-y: 0.5rem;
$tooltip-padding-x: 0.75rem; $tooltip-padding-x: 0.75rem;
$tooltip-arrow-height: 0.5rem; $tooltip-arrow-height: 0.5rem;
$tooltip-arrow-width: 1rem; $tooltip-arrow-width: 1rem;
$b-table-sort-icon-bg-ascending: url('data:image/svg+xml, <svg \
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="4 0 8 16"> \
<path style="fill: #666;" fill-rule="evenodd" d="M11.707085,11.7071 \
L7.999975,15.4142 L4.292875,11.7071 C3.902375,11.3166 3.902375, \
10.6834 4.292875,10.2929 C4.683375,9.90237 \
5.316575,9.90237 5.707075,10.2929 L6.999975, \
11.5858 L6.999975,2 C6.999975,1.44771 \
7.447695,1 7.999975,1 C8.552255,1 8.999975,1.44771 \
8.999975,2 L8.999975,11.5858 L10.292865,10.2929 C10.683395 \
,9.90237 11.316555,9.90237 11.707085,10.2929 \
C12.097605,10.6834 12.097605,11.3166 11.707085,11.7071 Z"/> \
</svg>') !default;
$b-table-sort-icon-bg-descending: url('data:image/svg+xml,<svg \
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="4 0 8 16"> \
<path style="fill: #666;" fill-rule="evenodd" d="M4.29289,4.2971 L8,0.59 \
L11.7071,4.2971 C12.0976,4.6876 \
12.0976,5.3208 11.7071,5.7113 C11.3166,6.10183 10.6834, \
6.10183 10.2929,5.7113 L9,4.4184 L9,14.0042 C9,14.55649 \
8.55228,15.0042 8,15.0042 C7.44772,15.0042 7,14.55649 \
7,14.0042 L7,4.4184 L5.70711,5.7113 C5.31658,6.10183 4.68342,6.10183 4.29289,5.7113 \
C3.90237,5.3208 3.90237,4.6876 4.29289,4.2971 Z"/> \
</svg> ') !default;
$b-table-sort-icon-bg-not-sorted: '';
...@@ -28,8 +28,19 @@ ...@@ -28,8 +28,19 @@
td, td,
th { th {
@include gl-p-5; // TODO: There is no gl-pl-9 utlity for this padding, to be done and then removed.
padding-left: 1.25rem;
@include gl-py-5;
@include gl-outline-none;
border: 0; // Remove cell border styling so that we can set border styling per row border: 0; // Remove cell border styling so that we can set border styling per row
&.event-count {
@include gl-pr-9;
}
&.alert-title {
@include gl-pointer-events-none;
}
} }
th { th {
......
---
title: Adds sorting by column to alert management list
merge_request: 32478
author:
type: added
...@@ -38,6 +38,7 @@ describe('AlertManagementList', () => { ...@@ -38,6 +38,7 @@ describe('AlertManagementList', () => {
const findDateFields = () => wrapper.findAll(TimeAgo); const findDateFields = () => wrapper.findAll(TimeAgo);
const findFirstStatusOption = () => findStatusDropdown().find(GlDropdownItem); const findFirstStatusOption = () => findStatusDropdown().find(GlDropdownItem);
const findSeverityFields = () => wrapper.findAll('[data-testid="severityField"]'); const findSeverityFields = () => wrapper.findAll('[data-testid="severityField"]');
const findSeverityColumnHeader = () => wrapper.findAll('th').at(0);
const alertsCount = { const alertsCount = {
acknowledged: 6, acknowledged: 6,
...@@ -80,7 +81,10 @@ describe('AlertManagementList', () => { ...@@ -80,7 +81,10 @@ describe('AlertManagementList', () => {
}); });
} }
const mockStartedAtCol = {};
beforeEach(() => { beforeEach(() => {
jest.spyOn(document, 'querySelector').mockReturnValue(mockStartedAtCol);
mountComponent(); mountComponent();
}); });
...@@ -284,6 +288,34 @@ describe('AlertManagementList', () => { ...@@ -284,6 +288,34 @@ describe('AlertManagementList', () => {
}); });
}); });
describe('sorting the alert list by column', () => {
beforeEach(() => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: mockAlerts, errored: false, sort: 'START_TIME_ASC', alertsCount },
loading: false,
});
});
it('updates sort with new direction and column key', () => {
findSeverityColumnHeader().trigger('click');
expect(wrapper.vm.$data.sort).toEqual('SEVERITY_ASC');
findSeverityColumnHeader().trigger('click');
expect(wrapper.vm.$data.sort).toEqual('SEVERITY_DESC');
});
it('updates the `ariaSort` attribute so the sort icon appears in the proper column', () => {
expect(mockStartedAtCol.ariaSort).toEqual('ascending');
findSeverityColumnHeader().trigger('click');
expect(mockStartedAtCol.ariaSort).toEqual('none');
});
});
describe('updating the alert status', () => { describe('updating the alert status', () => {
const iid = '1527542'; const iid = '1527542';
const mockUpdatedMutationResult = { const mockUpdatedMutationResult = {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment