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 {
iid
state
title
webUrl
}
assignees {
nodes {
......
......@@ -3,6 +3,7 @@ mutation createAlertIssue($projectPath: ID!, $iid: String!) {
errors
issue {
iid
webUrl
}
}
}
<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 { joinPaths, visitUrl } from '~/lib/utils/url_utility';
import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
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 { HIDDEN_VALUES } from './constants';
import { ALERT_DETAILS_LOADING_ROWS, HIDDEN_VALUES } from './constants';
export default {
HEADER_HEIGHT: process.env.NODE_ENV === 'development' ? '75px' : '40px',
ALERT_DETAILS_LOADING_ROWS,
i18n: {
CREATE_ISSUE: __('Create incident'),
ERROR: __('There was an error.'),
ERROR: __('There was an error fetching content, please refresh the page'),
},
components: {
GlAlert,
GlButton,
GlDrawer,
GlLink,
GlLoadingIcon,
GlSkeletonLoader,
},
inject: ['projectPath'],
apollo: {
......@@ -63,8 +63,7 @@ export default {
},
computed: {
alertIssuePath() {
const issueIid = this.selectedAlert.issue?.iid;
return issueIid ? this.getIssuePath(issueIid) : '';
return this.selectedAlert.issue?.webUrl || '';
},
curatedAlertDetails() {
return Object.entries({ ...this.alertDetails, ...this.alertDetails?.details }).reduce(
......@@ -78,7 +77,7 @@ export default {
return Boolean(this.selectedAlert.issue);
},
issueText() {
return `#${this.selectedAlert.issue.iid}`;
return `#${this.selectedAlert.issue?.iid}`;
},
isLoadingDetails() {
return this.$apollo.queries.alertDetails.loading;
......@@ -101,15 +100,12 @@ export default {
if (errors?.length) {
throw new Error();
}
visitUrl(this.getIssuePath(issue.iid));
visitUrl(issue.webUrl);
} catch {
this.handleAlertError();
this.creatingIssue = false;
}
},
getIssuePath(issueIid) {
return joinPaths(gon.relative_url_root || '/', this.projectPath, '-', 'issues', issueIid);
},
handleAlertError() {
this.errored = true;
},
......@@ -122,39 +118,40 @@ export default {
<template>
<gl-drawer
:z-index="252"
class="gl-bg-gray-10"
class="threat-monitoring-alert-drawer gl-bg-gray-10"
:open="isAlertDrawerOpen"
:header-height="$options.HEADER_HEIGHT"
@close="$emit('deselect-alert')"
>
<template #header>
<h5 class="gl-mt-2 gl-mb-5">{{ selectedAlert.title }}</h5>
<div>
<h5 class="gl-mb-5">{{ selectedAlert.title }}</h5>
<div>
<gl-link v-if="hasIssue" :href="alertIssuePath" data-testid="issue-link">
{{ issueText }}
</gl-link>
<gl-button
v-else
category="primary"
variant="confirm"
:disabled="errored"
:loading="creatingIssue"
data-testid="create-issue-button"
@click="createIssue"
>
{{ $options.i18n.CREATE_ISSUE }}
</gl-button>
</div>
<gl-link v-if="hasIssue" :href="alertIssuePath" data-testid="issue-link">
{{ issueText }}
</gl-link>
<gl-button
v-else
category="primary"
variant="confirm"
:disabled="errored"
:loading="creatingIssue"
data-testid="create-issue-button"
@click="createIssue"
>
{{ $options.i18n.CREATE_ISSUE }}
</gl-button>
</div>
</template>
<gl-alert v-if="errored" variant="danger" :dismissable="false" contained>
{{ $options.i18n.ERROR }}
</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-for="[key, value] in curatedAlertDetails" :key="key" class="gl-mb-3">
<div>{{ humanizeText(key) }}</div>
<div v-for="[key, value] in curatedAlertDetails" :key="key" class="gl-mb-5">
<div class="gl-mb-2">{{ humanizeText(key) }}</div>
<b>{{ value }}</b>
</div>
</div>
......
......@@ -84,3 +84,5 @@ export const HIDDEN_VALUES = [
'status',
'todos',
];
export const ALERT_DETAILS_LOADING_ROWS = 20;
......@@ -5,3 +5,16 @@
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 VueApollo from 'vue-apollo';
import AlertDrawer from 'ee/threat_monitoring/components/alerts/alert_drawer.vue';
......@@ -21,9 +21,9 @@ describe('Alert Drawer', () => {
let wrapper;
const DEFAULT_PROJECT_PATH = '#';
const mutateSpy = jest
.fn()
.mockResolvedValue({ data: { createAlertIssue: { errors: [], issue: { iid: '03' } } } });
const mutateSpy = jest.fn().mockResolvedValue({
data: { createAlertIssue: { errors: [], issue: { webUrl: '/#/-/issues/03' } } },
});
let querySpy;
const createMockApolloProvider = (query) => {
......@@ -40,7 +40,7 @@ describe('Alert Drawer', () => {
const findCreateIssueButton = () => wrapper.findByTestId('create-issue-button');
const findDrawer = () => wrapper.findComponent(GlDrawer);
const findIssueLink = () => wrapper.findByTestId('issue-link');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findDetails = () => wrapper.findByTestId('details-list');
const createWrapper = ({
......@@ -88,9 +88,10 @@ describe('Alert Drawer', () => {
${'details list'} | ${'does display'} | ${findDetails} | ${true} | ${undefined}
${'drawer'} | ${'does display'} | ${findDrawer} | ${true} | ${undefined}
${'issue link'} | ${'does display'} | ${findIssueLink} | ${true} | ${undefined}
${'loading icon'} | ${'does not display'} | ${findLoadingIcon} | ${false} | ${mountExtended}
`('$status the $component', ({ findComponent, state, mount }) => {
${'skeleton loader'} | ${'does not display'} | ${findSkeletonLoader} | ${false} | ${mountExtended}
`('$status the $component', async ({ findComponent, state, mount }) => {
createWrapper({ $apollo: shallowApolloMock({}), mount });
await wrapper.vm.$nextTick();
expect(findComponent().exists()).toBe(state);
});
});
......@@ -103,7 +104,7 @@ describe('Alert Drawer', () => {
it('displays the loading icon when retrieving the alert details', () => {
createWrapper({ $apollo: shallowApolloMock({ loading: true }) });
expect(findLoadingIcon().exists()).toBe(true);
expect(findSkeletonLoader().exists()).toBe(true);
expect(findDetails().exists()).toBe(false);
});
......@@ -121,9 +122,8 @@ describe('Alert Drawer', () => {
});
expect(findCreateIssueButton().exists()).toBe(true);
findCreateIssueButton().vm.$emit('click');
await waitForPromises;
await waitForPromises();
expect(mutateSpy).toHaveBeenCalledTimes(1);
await wrapper.vm.$nextTick();
expect(visitUrl).toHaveBeenCalledWith('/#/-/issues/03');
});
......@@ -138,11 +138,9 @@ describe('Alert Drawer', () => {
});
expect(findCreateIssueButton().exists()).toBe(true);
findCreateIssueButton().vm.$emit('click');
await waitForPromises;
await waitForPromises();
expect(erroredMutateSpy).toHaveBeenCalledTimes(1);
await wrapper.vm.$nextTick();
expect(visitUrl).not.toHaveBeenCalled();
await wrapper.vm.$nextTick();
expect(findAlert().exists()).toBe(true);
});
});
......
......@@ -106,7 +106,7 @@ export const mockAlerts = [
},
eventCount: '1',
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',
severity: 'HIGH',
status: 'TRIGGERED',
......@@ -117,7 +117,7 @@ export const mockAlerts = [
eventCount: '2',
assignees: { nodes: [] },
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',
title: 'Issue 02',
status: 'ACKNOWLEDGED',
......@@ -154,4 +154,9 @@ export const mockPageInfo = {
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 ""
msgid "There was an error fetching configuration for charts"
msgstr ""
msgid "There was an error fetching content, please refresh the page"
msgstr ""
msgid "There was an error fetching data for the selected stage"
msgstr ""
......@@ -32816,9 +32819,6 @@ msgstr ""
msgid "There was an error with the reCAPTCHA. Please solve the reCAPTCHA again."
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."
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