Commit 9371898f authored by David O'Regan's avatar David O'Regan Committed by Kushal Pandya

Refractor alert status dropdown

We now use a SFC to handle the shared
alert status dropdown between the details
page and list page.
parent f403e299
...@@ -336,7 +336,7 @@ export default { ...@@ -336,7 +336,7 @@ export default {
:sidebar-collapsed="sidebarCollapsed" :sidebar-collapsed="sidebarCollapsed"
@alert-refresh="alertRefresh" @alert-refresh="alertRefresh"
@toggle-sidebar="toggleSidebar" @toggle-sidebar="toggleSidebar"
@alert-sidebar-error="handleAlertSidebarError" @alert-error="handleAlertSidebarError"
/> />
</div> </div>
</div> </div>
......
...@@ -6,8 +6,6 @@ import { ...@@ -6,8 +6,6 @@ import {
GlTable, GlTable,
GlAlert, GlAlert,
GlIcon, GlIcon,
GlDropdown,
GlDropdownItem,
GlLink, GlLink,
GlTabs, GlTabs,
GlTab, GlTab,
...@@ -16,12 +14,13 @@ import { ...@@ -16,12 +14,13 @@ import {
GlSearchBoxByType, GlSearchBoxByType,
GlSprintf, GlSprintf,
} from '@gitlab/ui'; } from '@gitlab/ui';
import createFlash from '~/flash';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import { debounce, trim } from 'lodash'; import { debounce, trim } from 'lodash';
import { joinPaths, visitUrl } from '~/lib/utils/url_utility'; import { joinPaths, visitUrl } from '~/lib/utils/url_utility';
import { fetchPolicies } from '~/lib/graphql'; import { fetchPolicies } from '~/lib/graphql';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { convertToSnakeCase } from '~/lib/utils/text_utility';
import Tracking from '~/tracking';
import getAlerts from '../graphql/queries/get_alerts.query.graphql'; 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 { import {
...@@ -31,9 +30,7 @@ import { ...@@ -31,9 +30,7 @@ import {
trackAlertListViewsOptions, trackAlertListViewsOptions,
trackAlertStatusUpdateOptions, trackAlertStatusUpdateOptions,
} from '../constants'; } from '../constants';
import updateAlertStatus from '../graphql/mutations/update_alert_status.graphql'; import AlertStatus from './alert_status.vue';
import { convertToSnakeCase } from '~/lib/utils/text_utility';
import Tracking from '~/tracking';
const tdClass = 'table-col gl-display-flex d-md-table-cell gl-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 thClass = 'gl-hover-bg-blue-50';
...@@ -107,11 +104,6 @@ export default { ...@@ -107,11 +104,6 @@ export default {
sortable: true, sortable: true,
}, },
], ],
statuses: {
TRIGGERED: s__('AlertManagement|Triggered'),
ACKNOWLEDGED: s__('AlertManagement|Acknowledged'),
RESOLVED: s__('AlertManagement|Resolved'),
},
severityLabels: ALERTS_SEVERITY_LABELS, severityLabels: ALERTS_SEVERITY_LABELS,
statusTabs: ALERTS_STATUS_TABS, statusTabs: ALERTS_STATUS_TABS,
components: { components: {
...@@ -121,8 +113,6 @@ export default { ...@@ -121,8 +113,6 @@ export default {
GlAlert, GlAlert,
GlDeprecatedButton, GlDeprecatedButton,
TimeAgo, TimeAgo,
GlDropdown,
GlDropdownItem,
GlIcon, GlIcon,
GlLink, GlLink,
GlTabs, GlTabs,
...@@ -131,6 +121,7 @@ export default { ...@@ -131,6 +121,7 @@ export default {
GlPagination, GlPagination,
GlSearchBoxByType, GlSearchBoxByType,
GlSprintf, GlSprintf,
AlertStatus,
}, },
props: { props: {
projectPath: { projectPath: {
...@@ -204,6 +195,7 @@ export default { ...@@ -204,6 +195,7 @@ export default {
return { return {
searchTerm: '', searchTerm: '',
errored: false, errored: false,
errorMessage: '',
isAlertDismissed: false, isAlertDismissed: false,
isErrorAlertDismissed: false, isErrorAlertDismissed: false,
sort: 'STARTED_AT_DESC', sort: 'STARTED_AT_DESC',
...@@ -275,30 +267,6 @@ export default { ...@@ -275,30 +267,6 @@ export default {
this.searchTerm = trimmedInput; this.searchTerm = trimmedInput;
} }
}, 500), }, 500),
updateAlertStatus(status, iid) {
this.$apollo
.mutate({
mutation: updateAlertStatus,
variables: {
iid,
status: status.toUpperCase(),
projectPath: this.projectPath,
},
})
.then(() => {
this.trackStatusUpdate(status);
this.$apollo.queries.alerts.refetch();
this.$apollo.queries.alertsCount.refetch();
this.resetPagination();
})
.catch(() => {
createFlash(
s__(
'AlertManagement|There was an error while updating the status of the alert. Please try again.',
),
);
});
},
navigateToAlertDetails({ iid }) { navigateToAlertDetails({ iid }) {
return visitUrl(joinPaths(window.location.pathname, iid, 'details')); return visitUrl(joinPaths(window.location.pathname, iid, 'details'));
}, },
...@@ -338,6 +306,14 @@ export default { ...@@ -338,6 +306,14 @@ export default {
resetPagination() { resetPagination() {
this.pagination = initialPaginationState; this.pagination = initialPaginationState;
}, },
handleAlertError(errorMessage) {
this.errored = true;
this.errorMessage = errorMessage;
},
dismissError() {
this.isErrorAlertDismissed = true;
this.errorMessage = '';
},
}, },
}; };
</script> </script>
...@@ -357,8 +333,13 @@ export default { ...@@ -357,8 +333,13 @@ export default {
</template> </template>
</gl-sprintf> </gl-sprintf>
</gl-alert> </gl-alert>
<gl-alert v-if="showErrorMsg" variant="danger" @dismiss="isErrorAlertDismissed = true"> <gl-alert
{{ $options.i18n.errorMsg }} v-if="showErrorMsg"
variant="danger"
data-testid="alert-error"
@dismiss="dismissError"
>
{{ errorMessage || $options.i18n.errorMsg }}
</gl-alert> </gl-alert>
<gl-tabs content-class="gl-p-0" @input="filterAlertsByStatus"> <gl-tabs content-class="gl-p-0" @input="filterAlertsByStatus">
...@@ -437,22 +418,12 @@ export default { ...@@ -437,22 +418,12 @@ export default {
</template> </template>
<template #cell(status)="{ item }"> <template #cell(status)="{ item }">
<gl-dropdown :text="$options.statuses[item.status]" class="w-100" right> <alert-status
<gl-dropdown-item :alert="item"
v-for="(label, field) in $options.statuses" :project-path="projectPath"
:key="field" :is-sidebar="false"
@click="updateAlertStatus(label, item.iid)" @alert-error="handleAlertError"
> />
<span class="d-flex">
<gl-icon
class="flex-shrink-0 append-right-4"
:class="{ invisible: label.toUpperCase() !== item.status }"
name="mobile-issue-close"
/>
{{ label }}
</span>
</gl-dropdown-item>
</gl-dropdown>
</template> </template>
<template #empty> <template #empty>
......
...@@ -49,7 +49,7 @@ export default { ...@@ -49,7 +49,7 @@ export default {
:project-path="projectPath" :project-path="projectPath"
:alert="alert" :alert="alert"
@toggle-sidebar="$emit('toggle-sidebar')" @toggle-sidebar="$emit('toggle-sidebar')"
@alert-sidebar-error="$emit('alert-sidebar-error', $event)" @alert-error="$emit('alert-error', $event)"
/> />
<sidebar-assignees <sidebar-assignees
:project-path="projectPath" :project-path="projectPath"
...@@ -58,7 +58,7 @@ export default { ...@@ -58,7 +58,7 @@ export default {
:sidebar-collapsed="sidebarCollapsed" :sidebar-collapsed="sidebarCollapsed"
@alert-refresh="$emit('alert-refresh')" @alert-refresh="$emit('alert-refresh')"
@toggle-sidebar="$emit('toggle-sidebar')" @toggle-sidebar="$emit('toggle-sidebar')"
@alert-sidebar-error="$emit('alert-sidebar-error', $event)" @alert-error="$emit('alert-error', $event)"
/> />
<div class="block"></div> <div class="block"></div>
</div> </div>
......
<script>
import { GlDropdown, GlDropdownItem, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
import { trackAlertStatusUpdateOptions } from '../constants';
import updateAlertStatus from '../graphql/mutations/update_alert_status.graphql';
export default {
statuses: {
TRIGGERED: s__('AlertManagement|Triggered'),
ACKNOWLEDGED: s__('AlertManagement|Acknowledged'),
RESOLVED: s__('AlertManagement|Resolved'),
},
components: {
GlDropdown,
GlDropdownItem,
GlButton,
},
props: {
projectPath: {
type: String,
required: true,
},
alert: {
type: Object,
required: true,
},
isDropdownShowing: {
type: Boolean,
required: false,
},
isSidebar: {
type: Boolean,
required: true,
},
},
computed: {
dropdownClass() {
// eslint-disable-next-line no-nested-ternary
return this.isSidebar ? (this.isDropdownShowing ? 'show' : 'gl-display-none') : '';
},
},
methods: {
updateAlertStatus(status) {
this.$emit('handle-updating', true);
this.$apollo
.mutate({
mutation: updateAlertStatus,
variables: {
iid: this.alert.iid,
status: status.toUpperCase(),
projectPath: this.projectPath,
},
})
.then(() => {
this.trackStatusUpdate(status);
this.$emit('hide-dropdown');
})
.catch(() => {
this.$emit(
'alert-error',
s__(
'AlertManagement|There was an error while updating the status of the alert. Please try again.',
),
);
})
.finally(() => {
this.$emit('handle-updating', false);
});
},
trackStatusUpdate(status) {
const { category, action, label } = trackAlertStatusUpdateOptions;
Tracking.event(category, action, { label, property: status });
},
},
};
</script>
<template>
<div class="dropdown dropdown-menu-selectable" :class="dropdownClass">
<gl-dropdown
ref="dropdown"
right
:text="$options.statuses[alert.status]"
class="w-100"
toggle-class="dropdown-menu-toggle"
variant="outline-default"
@keydown.esc.native="$emit('hide-dropdown')"
@hide="$emit('hide-dropdown')"
>
<div class="dropdown-title text-center">
<span class="alert-title">{{ s__('AlertManagement|Assign status') }}</span>
<gl-button
:aria-label="__('Close')"
variant="link"
class="dropdown-title-button dropdown-menu-close"
icon="close"
@click="$emit('hide-dropdown')"
/>
</div>
<div class="dropdown-content dropdown-body">
<gl-dropdown-item
v-for="(label, field) in $options.statuses"
:key="field"
data-testid="statusDropdownItem"
class="gl-vertical-align-middle"
:active="label.toUpperCase() === alert.status"
:active-class="'is-active'"
@click="updateAlertStatus(label)"
>
{{ label }}
</gl-dropdown-item>
</div>
</gl-dropdown>
</div>
</template>
...@@ -142,7 +142,7 @@ export default { ...@@ -142,7 +142,7 @@ export default {
this.users = data; this.users = data;
}) })
.catch(() => { .catch(() => {
this.$emit('alert-sidebar-error', this.$options.FETCH_USERS_ERROR); this.$emit('alert-error', this.$options.FETCH_USERS_ERROR);
}) })
.finally(() => { .finally(() => {
this.isDropdownSearching = false; this.isDropdownSearching = false;
...@@ -172,7 +172,7 @@ export default { ...@@ -172,7 +172,7 @@ export default {
return this.$emit('alert-refresh'); return this.$emit('alert-refresh');
}) })
.catch(() => { .catch(() => {
this.$emit('alert-sidebar-error', this.$options.UPDATE_ALERT_ASSIGNEES_ERROR); this.$emit('alert-error', this.$options.UPDATE_ALERT_ASSIGNEES_ERROR);
}) })
.finally(() => { .finally(() => {
this.isUpdating = false; this.isUpdating = false;
......
<script> <script>
import { import { GlIcon, GlLoadingIcon, GlTooltip, GlSprintf } from '@gitlab/ui';
GlIcon,
GlDropdown,
GlDropdownItem,
GlLoadingIcon,
GlTooltip,
GlButton,
GlSprintf,
} from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import Tracking from '~/tracking'; import AlertStatus from '../alert_status.vue';
import { trackAlertStatusUpdateOptions } from '../../constants';
import updateAlertStatus from '../../graphql/mutations/update_alert_status.graphql';
export default { export default {
statuses: { statuses: {
...@@ -21,12 +11,10 @@ export default { ...@@ -21,12 +11,10 @@ export default {
}, },
components: { components: {
GlIcon, GlIcon,
GlDropdown,
GlDropdownItem,
GlLoadingIcon, GlLoadingIcon,
GlTooltip, GlTooltip,
GlButton,
GlSprintf, GlSprintf,
AlertStatus,
}, },
props: { props: {
projectPath: { projectPath: {
...@@ -60,44 +48,13 @@ export default { ...@@ -60,44 +48,13 @@ export default {
}, },
toggleFormDropdown() { toggleFormDropdown() {
this.isDropdownShowing = !this.isDropdownShowing; this.isDropdownShowing = !this.isDropdownShowing;
const { dropdown } = this.$refs.dropdown.$refs; const { dropdown } = this.$children[2].$refs.dropdown.$refs;
if (dropdown && this.isDropdownShowing) { if (dropdown && this.isDropdownShowing) {
dropdown.show(); dropdown.show();
} }
}, },
isSelected(status) { handleUpdating(updating) {
return this.alert.status === status; this.isUpdating = updating;
},
updateAlertStatus(status) {
this.isUpdating = true;
this.$apollo
.mutate({
mutation: updateAlertStatus,
variables: {
iid: this.alert.iid,
status: status.toUpperCase(),
projectPath: this.projectPath,
},
})
.then(() => {
this.trackStatusUpdate(status);
this.hideDropdown();
})
.catch(() => {
this.$emit(
'alert-sidebar-error',
s__(
'AlertManagement|There was an error while updating the status of the alert. Please try again.',
),
);
})
.finally(() => {
this.isUpdating = false;
});
},
trackStatusUpdate(status) {
const { category, action, label } = trackAlertStatusUpdateOptions;
Tracking.event(category, action, { label, property: status });
}, },
}, },
}; };
...@@ -132,41 +89,15 @@ export default { ...@@ -132,41 +89,15 @@ export default {
</a> </a>
</p> </p>
<div class="dropdown dropdown-menu-selectable" :class="dropdownClass"> <alert-status
<gl-dropdown :alert="alert"
ref="dropdown" :project-path="projectPath"
:text="$options.statuses[alert.status]" :is-dropdown-showing="isDropdownShowing"
class="w-100" :is-sidebar="true"
toggle-class="dropdown-menu-toggle" @alert-error="$emit('alert-error', $event)"
variant="outline-default" @hide-dropdown="hideDropdown"
@keydown.esc.native="hideDropdown" @handle-updating="handleUpdating"
@hide="hideDropdown" />
>
<div class="dropdown-title">
<span class="alert-title">{{ s__('AlertManagement|Assign status') }}</span>
<gl-button
:aria-label="__('Close')"
variant="link"
class="dropdown-title-button dropdown-menu-close"
icon="close"
@click="hideDropdown"
/>
</div>
<div class="dropdown-content dropdown-body">
<gl-dropdown-item
v-for="(label, field) in $options.statuses"
:key="field"
data-testid="statusDropdownItem"
class="gl-vertical-align-middle"
:active="label.toUpperCase() === alert.status"
:active-class="'is-active'"
@click="updateAlertStatus(label)"
>
{{ label }}
</gl-dropdown-item>
</div>
</gl-dropdown>
</div>
<gl-loading-icon v-if="isUpdating" :inline="true" /> <gl-loading-icon v-if="isUpdating" :inline="true" />
<p <p
......
...@@ -74,7 +74,7 @@ ...@@ -74,7 +74,7 @@
content: none !important; content: none !important;
} }
div { div:not(.dropdown-title) {
width: 100% !important; width: 100% !important;
padding: 0 !important; padding: 0 !important;
} }
......
...@@ -15,7 +15,6 @@ import { ...@@ -15,7 +15,6 @@ import {
} from '@gitlab/ui'; } from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import createFlash from '~/flash';
import AlertManagementList from '~/alert_management/components/alert_management_list.vue'; import AlertManagementList from '~/alert_management/components/alert_management_list.vue';
import { import {
ALERTS_STATUS_TABS, ALERTS_STATUS_TABS,
...@@ -26,8 +25,6 @@ import updateAlertStatus from '~/alert_management/graphql/mutations/update_alert ...@@ -26,8 +25,6 @@ import updateAlertStatus from '~/alert_management/graphql/mutations/update_alert
import mockAlerts from '../mocks/alerts.json'; import mockAlerts from '../mocks/alerts.json';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility', () => ({ jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn().mockName('visitUrlMock'), visitUrl: jest.fn().mockName('visitUrlMock'),
joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths, joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths,
...@@ -391,14 +388,15 @@ describe('AlertManagementList', () => { ...@@ -391,14 +388,15 @@ describe('AlertManagementList', () => {
}); });
}); });
it('calls `createFlash` when request fails', () => { it('shows an error when request fails', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error())); jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error()));
findFirstStatusOption().vm.$emit('click'); findFirstStatusOption().vm.$emit('click');
wrapper.setData({
errored: true,
});
setImmediate(() => { wrapper.vm.$nextTick(() => {
expect(createFlash).toHaveBeenCalledWith( expect(wrapper.find('[data-testid="alert-error"]').exists()).toBe(true);
'There was an error while updating the status of the alert. Please try again.',
);
}); });
}); });
}); });
......
import { shallowMount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { GlDropdownItem, GlLoadingIcon } from '@gitlab/ui'; import { GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
import { trackAlertStatusUpdateOptions } from '~/alert_management/constants'; import { trackAlertStatusUpdateOptions } from '~/alert_management/constants';
import AlertSidebarStatus from '~/alert_management/components/sidebar/sidebar_status.vue'; import AlertSidebarStatus from '~/alert_management/components/sidebar/sidebar_status.vue';
...@@ -14,7 +14,7 @@ describe('Alert Details Sidebar Status', () => { ...@@ -14,7 +14,7 @@ describe('Alert Details Sidebar Status', () => {
const findStatusLoadingIcon = () => wrapper.find(GlLoadingIcon); const findStatusLoadingIcon = () => wrapper.find(GlLoadingIcon);
function mountComponent({ data, sidebarCollapsed = true, loading = false, stubs = {} } = {}) { function mountComponent({ data, sidebarCollapsed = true, loading = false, stubs = {} } = {}) {
wrapper = shallowMount(AlertSidebarStatus, { wrapper = mount(AlertSidebarStatus, {
propsData: { propsData: {
alert: { ...mockAlert }, alert: { ...mockAlert },
...data, ...data,
......
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