Commit 4d769b70 authored by Alexander Turinske's avatar Alexander Turinske

Update UI of alert drawer per design review

- add skeleton loading
- update spacing
- add webUrl graphql property
- use skeleton loader
- update tests
parent 5df32b1b
...@@ -9,6 +9,7 @@ fragment AlertListItem on AlertManagementAlert { ...@@ -9,6 +9,7 @@ fragment AlertListItem on AlertManagementAlert {
iid iid
state state
title title
webUrl
} }
assignees { assignees {
nodes { nodes {
......
...@@ -3,6 +3,7 @@ mutation createAlertIssue($projectPath: ID!, $iid: String!) { ...@@ -3,6 +3,7 @@ mutation createAlertIssue($projectPath: ID!, $iid: String!) {
errors errors
issue { issue {
iid iid
webUrl
} }
} }
} }
<script> <script>
import { GlAlert, GlButton, GlDrawer, GlLink, GlLoadingIcon } from '@gitlab/ui'; import { GlAlert, GlButton, GlDrawer, GlLink, GlSkeletonLoader } from '@gitlab/ui';
import { capitalizeFirstCharacter, splitCamelCase } from '~/lib/utils/text_utility'; import { capitalizeFirstCharacter, splitCamelCase } from '~/lib/utils/text_utility';
import { joinPaths, visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale'; import { __ } from '~/locale';
import createIssueMutation from '~/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql'; import createIssueMutation from '~/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql';
import getAlertDetailsQuery from '~/vue_shared/alert_details/graphql/queries/alert_details.query.graphql'; import getAlertDetailsQuery from '~/vue_shared/alert_details/graphql/queries/alert_details.query.graphql';
import { HIDDEN_VALUES } from './constants'; import { ALERT_DETAILS_LOADING_ROWS, HIDDEN_VALUES } from './constants';
export default { export default {
HEADER_HEIGHT: process.env.NODE_ENV === 'development' ? '75px' : '40px', ALERT_DETAILS_LOADING_ROWS,
i18n: { i18n: {
CREATE_ISSUE: __('Create incident'), CREATE_ISSUE: __('Create incident'),
ERROR: __('There was an error.'), ERROR: __('There was an error fetching content, please refresh the page'),
}, },
components: { components: {
GlAlert, GlAlert,
GlButton, GlButton,
GlDrawer, GlDrawer,
GlLink, GlLink,
GlLoadingIcon, GlSkeletonLoader,
}, },
inject: ['projectPath'], inject: ['projectPath'],
apollo: { apollo: {
...@@ -63,8 +63,7 @@ export default { ...@@ -63,8 +63,7 @@ export default {
}, },
computed: { computed: {
alertIssuePath() { alertIssuePath() {
const issueIid = this.selectedAlert.issue?.iid; return this.selectedAlert.issue?.webUrl || '';
return issueIid ? this.getIssuePath(issueIid) : '';
}, },
curatedAlertDetails() { curatedAlertDetails() {
return Object.entries({ ...this.alertDetails, ...this.alertDetails?.details }).reduce( return Object.entries({ ...this.alertDetails, ...this.alertDetails?.details }).reduce(
...@@ -78,7 +77,7 @@ export default { ...@@ -78,7 +77,7 @@ export default {
return Boolean(this.selectedAlert.issue); return Boolean(this.selectedAlert.issue);
}, },
issueText() { issueText() {
return `#${this.selectedAlert.issue.iid}`; return `#${this.selectedAlert.issue?.iid}`;
}, },
isLoadingDetails() { isLoadingDetails() {
return this.$apollo.queries.alertDetails.loading; return this.$apollo.queries.alertDetails.loading;
...@@ -101,15 +100,12 @@ export default { ...@@ -101,15 +100,12 @@ export default {
if (errors?.length) { if (errors?.length) {
throw new Error(); throw new Error();
} }
visitUrl(this.getIssuePath(issue.iid)); visitUrl(issue.webUrl);
} catch { } catch {
this.handleAlertError(); this.handleAlertError();
this.creatingIssue = false; this.creatingIssue = false;
} }
}, },
getIssuePath(issueIid) {
return joinPaths(gon.relative_url_root || '/', this.projectPath, '-', 'issues', issueIid);
},
handleAlertError() { handleAlertError() {
this.errored = true; this.errored = true;
}, },
...@@ -122,39 +118,40 @@ export default { ...@@ -122,39 +118,40 @@ export default {
<template> <template>
<gl-drawer <gl-drawer
:z-index="252" :z-index="252"
class="gl-bg-gray-10" class="threat-monitoring-alert-drawer gl-bg-gray-10"
:open="isAlertDrawerOpen" :open="isAlertDrawerOpen"
:header-height="$options.HEADER_HEIGHT"
@close="$emit('deselect-alert')" @close="$emit('deselect-alert')"
> >
<template #header> <template #header>
<h5 class="gl-mt-2 gl-mb-5">{{ selectedAlert.title }}</h5>
<div> <div>
<h5 class="gl-mb-5">{{ selectedAlert.title }}</h5> <gl-link v-if="hasIssue" :href="alertIssuePath" data-testid="issue-link">
<div> {{ issueText }}
<gl-link v-if="hasIssue" :href="alertIssuePath" data-testid="issue-link"> </gl-link>
{{ issueText }} <gl-button
</gl-link> v-else
<gl-button category="primary"
v-else variant="confirm"
category="primary" :disabled="errored"
variant="confirm" :loading="creatingIssue"
:disabled="errored" data-testid="create-issue-button"
:loading="creatingIssue" @click="createIssue"
data-testid="create-issue-button" >
@click="createIssue" {{ $options.i18n.CREATE_ISSUE }}
> </gl-button>
{{ $options.i18n.CREATE_ISSUE }}
</gl-button>
</div>
</div> </div>
</template> </template>
<gl-alert v-if="errored" variant="danger" :dismissable="false" contained> <gl-alert v-if="errored" variant="danger" :dismissable="false" contained>
{{ $options.i18n.ERROR }} {{ $options.i18n.ERROR }}
</gl-alert> </gl-alert>
<gl-loading-icon v-if="isLoadingDetails" size="lg" color="dark" class="gl-mt-5" /> <div v-if="isLoadingDetails">
<div v-for="row in $options.ALERT_DETAILS_LOADING_ROWS" :key="row" class="gl-mb-5">
<gl-skeleton-loader :lines="2" :width="400" />
</div>
</div>
<div v-else data-testid="details-list"> <div v-else data-testid="details-list">
<div v-for="[key, value] in curatedAlertDetails" :key="key" class="gl-mb-3"> <div v-for="[key, value] in curatedAlertDetails" :key="key" class="gl-mb-5">
<div>{{ humanizeText(key) }}</div> <div class="gl-mb-2">{{ humanizeText(key) }}</div>
<b>{{ value }}</b> <b>{{ value }}</b>
</div> </div>
</div> </div>
......
...@@ -84,3 +84,5 @@ export const HIDDEN_VALUES = [ ...@@ -84,3 +84,5 @@ export const HIDDEN_VALUES = [
'status', 'status',
'todos', 'todos',
]; ];
export const ALERT_DETAILS_LOADING_ROWS = 20;
...@@ -5,3 +5,16 @@ ...@@ -5,3 +5,16 @@
background-color: $gray-50; background-color: $gray-50;
} }
} }
.threat-monitoring-alert-drawer {
// Override gl-drawer inline styles
top: $header-height !important;
.gl-drawer-header {
align-items: flex-start;
}
}
.with-performance-bar .threat-monitoring-alert-drawer {
top: $performance-bar-height + $header-height !important;
}
import { GlAlert, GlDrawer, GlLoadingIcon } from '@gitlab/ui'; import { GlAlert, GlDrawer, GlSkeletonLoader } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils'; import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import AlertDrawer from 'ee/threat_monitoring/components/alerts/alert_drawer.vue'; import AlertDrawer from 'ee/threat_monitoring/components/alerts/alert_drawer.vue';
...@@ -21,9 +21,9 @@ describe('Alert Drawer', () => { ...@@ -21,9 +21,9 @@ describe('Alert Drawer', () => {
let wrapper; let wrapper;
const DEFAULT_PROJECT_PATH = '#'; const DEFAULT_PROJECT_PATH = '#';
const mutateSpy = jest const mutateSpy = jest.fn().mockResolvedValue({
.fn() data: { createAlertIssue: { errors: [], issue: { webUrl: '/#/-/issues/03' } } },
.mockResolvedValue({ data: { createAlertIssue: { errors: [], issue: { iid: '03' } } } }); });
let querySpy; let querySpy;
const createMockApolloProvider = (query) => { const createMockApolloProvider = (query) => {
...@@ -40,7 +40,7 @@ describe('Alert Drawer', () => { ...@@ -40,7 +40,7 @@ describe('Alert Drawer', () => {
const findCreateIssueButton = () => wrapper.findByTestId('create-issue-button'); const findCreateIssueButton = () => wrapper.findByTestId('create-issue-button');
const findDrawer = () => wrapper.findComponent(GlDrawer); const findDrawer = () => wrapper.findComponent(GlDrawer);
const findIssueLink = () => wrapper.findByTestId('issue-link'); const findIssueLink = () => wrapper.findByTestId('issue-link');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findDetails = () => wrapper.findByTestId('details-list'); const findDetails = () => wrapper.findByTestId('details-list');
const createWrapper = ({ const createWrapper = ({
...@@ -88,9 +88,10 @@ describe('Alert Drawer', () => { ...@@ -88,9 +88,10 @@ describe('Alert Drawer', () => {
${'details list'} | ${'does display'} | ${findDetails} | ${true} | ${undefined} ${'details list'} | ${'does display'} | ${findDetails} | ${true} | ${undefined}
${'drawer'} | ${'does display'} | ${findDrawer} | ${true} | ${undefined} ${'drawer'} | ${'does display'} | ${findDrawer} | ${true} | ${undefined}
${'issue link'} | ${'does display'} | ${findIssueLink} | ${true} | ${undefined} ${'issue link'} | ${'does display'} | ${findIssueLink} | ${true} | ${undefined}
${'loading icon'} | ${'does not display'} | ${findLoadingIcon} | ${false} | ${mountExtended} ${'skeleton loader'} | ${'does not display'} | ${findSkeletonLoader} | ${false} | ${mountExtended}
`('$status the $component', ({ findComponent, state, mount }) => { `('$status the $component', async ({ findComponent, state, mount }) => {
createWrapper({ $apollo: shallowApolloMock({}), mount }); createWrapper({ $apollo: shallowApolloMock({}), mount });
await wrapper.vm.$nextTick();
expect(findComponent().exists()).toBe(state); expect(findComponent().exists()).toBe(state);
}); });
}); });
...@@ -103,7 +104,7 @@ describe('Alert Drawer', () => { ...@@ -103,7 +104,7 @@ describe('Alert Drawer', () => {
it('displays the loading icon when retrieving the alert details', () => { it('displays the loading icon when retrieving the alert details', () => {
createWrapper({ $apollo: shallowApolloMock({ loading: true }) }); createWrapper({ $apollo: shallowApolloMock({ loading: true }) });
expect(findLoadingIcon().exists()).toBe(true); expect(findSkeletonLoader().exists()).toBe(true);
expect(findDetails().exists()).toBe(false); expect(findDetails().exists()).toBe(false);
}); });
...@@ -121,9 +122,8 @@ describe('Alert Drawer', () => { ...@@ -121,9 +122,8 @@ describe('Alert Drawer', () => {
}); });
expect(findCreateIssueButton().exists()).toBe(true); expect(findCreateIssueButton().exists()).toBe(true);
findCreateIssueButton().vm.$emit('click'); findCreateIssueButton().vm.$emit('click');
await waitForPromises; await waitForPromises();
expect(mutateSpy).toHaveBeenCalledTimes(1); expect(mutateSpy).toHaveBeenCalledTimes(1);
await wrapper.vm.$nextTick();
expect(visitUrl).toHaveBeenCalledWith('/#/-/issues/03'); expect(visitUrl).toHaveBeenCalledWith('/#/-/issues/03');
}); });
...@@ -138,11 +138,9 @@ describe('Alert Drawer', () => { ...@@ -138,11 +138,9 @@ describe('Alert Drawer', () => {
}); });
expect(findCreateIssueButton().exists()).toBe(true); expect(findCreateIssueButton().exists()).toBe(true);
findCreateIssueButton().vm.$emit('click'); findCreateIssueButton().vm.$emit('click');
await waitForPromises; await waitForPromises();
expect(erroredMutateSpy).toHaveBeenCalledTimes(1); expect(erroredMutateSpy).toHaveBeenCalledTimes(1);
await wrapper.vm.$nextTick();
expect(visitUrl).not.toHaveBeenCalled(); expect(visitUrl).not.toHaveBeenCalled();
await wrapper.vm.$nextTick();
expect(findAlert().exists()).toBe(true); expect(findAlert().exists()).toBe(true);
}); });
}); });
......
...@@ -106,7 +106,7 @@ export const mockAlerts = [ ...@@ -106,7 +106,7 @@ export const mockAlerts = [
}, },
eventCount: '1', eventCount: '1',
issueIid: null, issueIid: null,
issue: { iid: '5', state: 'opened', title: 'Issue 01' }, issue: { iid: '5', state: 'opened', title: 'Issue 01', webUrl: 'http://test.com/05' },
title: 'Issue 01', title: 'Issue 01',
severity: 'HIGH', severity: 'HIGH',
status: 'TRIGGERED', status: 'TRIGGERED',
...@@ -117,7 +117,7 @@ export const mockAlerts = [ ...@@ -117,7 +117,7 @@ export const mockAlerts = [
eventCount: '2', eventCount: '2',
assignees: { nodes: [] }, assignees: { nodes: [] },
issueIid: null, issueIid: null,
issue: { iid: '6', state: 'closed', title: 'Issue 02' }, issue: { iid: '6', state: 'closed', title: 'Issue 02', webUrl: 'http://test.com/06' },
severity: 'CRITICAL', severity: 'CRITICAL',
title: 'Issue 02', title: 'Issue 02',
status: 'ACKNOWLEDGED', status: 'ACKNOWLEDGED',
...@@ -154,4 +154,9 @@ export const mockPageInfo = { ...@@ -154,4 +154,9 @@ export const mockPageInfo = {
startCursor: 'eyJpZCI6IjM5Iiwic3RhcnRlZF9hdCI6IjIwMjAtMTItMDQgMTg6MDE6MDcuNzY1ODgyMDAwIFVUQyJ9', startCursor: 'eyJpZCI6IjM5Iiwic3RhcnRlZF9hdCI6IjIwMjAtMTItMDQgMTg6MDE6MDcuNzY1ODgyMDAwIFVUQyJ9',
}; };
export const mockAlertDetails = { iid: '01', issue: { iid: '02' }, title: 'dropingress' }; export const mockAlertDetails = {
iid: '01',
issue: { webUrl: '/#/-/issues/02' },
title: 'dropingress',
monitorTool: 'Cilium',
};
...@@ -32678,6 +32678,9 @@ msgstr "" ...@@ -32678,6 +32678,9 @@ msgstr ""
msgid "There was an error fetching configuration for charts" msgid "There was an error fetching configuration for charts"
msgstr "" msgstr ""
msgid "There was an error fetching content, please refresh the page"
msgstr ""
msgid "There was an error fetching data for the selected stage" msgid "There was an error fetching data for the selected stage"
msgstr "" msgstr ""
...@@ -32816,9 +32819,6 @@ msgstr "" ...@@ -32816,9 +32819,6 @@ msgstr ""
msgid "There was an error with the reCAPTCHA. Please solve the reCAPTCHA again." msgid "There was an error with the reCAPTCHA. Please solve the reCAPTCHA again."
msgstr "" msgstr ""
msgid "There was an error."
msgstr ""
msgid "These dates affect how your epics appear in the roadmap. Set a fixed date or one inherited from the milestones assigned to issues in this epic." msgid "These dates affect how your epics appear in the roadmap. Set a fixed date or one inherited from the milestones assigned to issues in this epic."
msgstr "" msgstr ""
......
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