Commit 77d2f7e0 authored by Illya Klymov's avatar Illya Klymov

Merge branch '341364-dast-view-scans-all-tab-states' into 'master'

Implement loading and error state in DAST scans view

See merge request gitlab-org/gitlab!72994
parents 55e88eaa 315b3848
<script>
import { GlTab, GlBadge, GlLink, GlTable, GlKeysetPagination } from '@gitlab/ui';
import {
GlTab,
GlBadge,
GlLink,
GlTable,
GlKeysetPagination,
GlAlert,
GlSkeletonLoader,
} from '@gitlab/ui';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { DAST_SHORT_NAME } from '~/security_configuration/components/constants';
import { __ } from '~/locale';
import { __, s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { scrollToElement } from '~/lib/utils/common_utils';
import EmptyState from '../empty_state.vue';
......@@ -26,6 +34,8 @@ export default {
GlLink,
GlTable,
GlKeysetPagination,
GlAlert,
GlSkeletonLoader,
CiBadgeLink,
TimeAgoTooltip,
EmptyState,
......@@ -78,6 +88,9 @@ export default {
}
return pipelines;
},
error() {
this.hasError = true;
},
pollInterval: PIPELINES_POLL_INTERVAL,
},
},
......@@ -99,8 +112,14 @@ export default {
};
},
computed: {
pipelineNodes() {
return this.pipelines?.nodes ?? [];
},
hasPipelines() {
return Boolean(this.pipelines?.nodes?.length);
return this.pipelineNodes.length > 0;
},
pageInfo() {
return this.pipelines?.pageInfo;
},
tableFields() {
return this.fields.map(({ key, label }) => ({
......@@ -111,6 +130,13 @@ export default {
}));
},
},
watch: {
hasPipelines(hasPipelines) {
if (this.hasError && hasPipelines) {
this.hasError = false;
}
},
},
methods: {
resetCursor() {
this.cursor = { ...defaultCursor };
......@@ -142,6 +168,9 @@ export default {
i18n: {
previousPage: __('Prev'),
nextPage: __('Next'),
errorMessage: s__(
'OnDemandScans|Could not fetch on-demand scans. Please refresh the page, or try again later.',
),
},
};
</script>
......@@ -152,13 +181,24 @@ export default {
{{ title }}
<gl-badge size="sm" class="gl-tab-counter-badge">{{ itemsCount }}</gl-badge>
</template>
<template v-if="hasPipelines">
<template v-if="$apollo.queries.pipelines.loading || hasPipelines">
<gl-table
thead-class="gl-border-b-solid gl-border-gray-100 gl-border-1"
:fields="tableFields"
:items="pipelines.nodes"
:items="pipelineNodes"
:busy="$apollo.queries.pipelines.loading"
stacked="md"
>
<template #table-busy>
<gl-skeleton-loader v-for="i in 20" :key="i" :width="1000" :height="45">
<rect width="85" height="20" x="0" y="5" rx="4" />
<rect width="100" height="20" x="150" y="5" rx="4" />
<rect width="150" height="20" x="300" y="5" rx="4" />
<rect width="100" height="20" x="500" y="5" rx="4" />
<rect width="150" height="20" x="655" y="5" rx="4" />
<rect width="70" height="20" x="855" y="5" rx="4" />
</gl-skeleton-loader>
</template>
<template #cell(detailedStatus)="{ item }">
<div class="gl-my-3">
<ci-badge-link :status="item.detailedStatus" />
......@@ -181,7 +221,7 @@ export default {
<div class="gl-display-flex gl-justify-content-center">
<gl-keyset-pagination
data-testid="pagination"
v-bind="pipelines.pageInfo"
v-bind="pageInfo"
:prev-text="$options.i18n.previousPage"
:next-text="$options.i18n.nextPage"
@prev="prevPage"
......@@ -189,6 +229,15 @@ export default {
/>
</div>
</template>
<gl-alert
v-else-if="hasError"
variant="danger"
:dismissible="false"
class="gl-my-4"
data-testid="error-alert"
>
{{ $options.i18n.errorMessage }}
</gl-alert>
<empty-state v-else :title="emptyStateTitle" :text="emptyStateText" no-primary-button />
</gl-tab>
</template>
import { GlTab, GlTable } from '@gitlab/ui';
import { GlTab, GlTable, GlAlert } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import allPipelinesWithPipelinesMock from 'test_fixtures/graphql/on_demand_scans/graphql/on_demand_scans.query.graphql.with_pipelines.json';
......@@ -32,12 +32,18 @@ describe('BaseTab', () => {
const findTable = () => wrapper.findComponent(GlTable);
const findEmptyState = () => wrapper.findComponent(EmptyState);
const findPagination = () => wrapper.findByTestId('pagination');
const findErrorAlert = () => wrapper.findComponent(GlAlert);
// Helpers
const createMockApolloProvider = () => {
return createMockApollo([[onDemandScansQuery, requestHandler]]);
};
const navigateToPage = (direction) => {
findPagination().vm.$emit(direction);
return wrapper.vm.$nextTick();
};
const createComponent = (propsData) => {
router = createRouter();
wrapper = shallowMountExtended(BaseTab, {
......@@ -66,7 +72,7 @@ describe('BaseTab', () => {
`,
}),
GlTable: stubComponent(GlTable, {
props: ['items'],
props: ['items', 'busy'],
}),
},
});
......@@ -95,6 +101,16 @@ describe('BaseTab', () => {
});
});
it('puts the table in the busy state until the request resolves', async () => {
createComponent();
expect(findTable().props('busy')).toBe(true);
await waitForPromises();
expect(findTable().props('busy')).toBe(false);
});
it('resets the route if no pipeline matches the cursor', async () => {
setWindowLocation('#?after=nothingToSeeHere');
requestHandler = jest.fn().mockResolvedValue(allPipelinesWithoutPipelinesMock);
......@@ -128,8 +144,8 @@ describe('BaseTab', () => {
);
});
it('when navigating to another page, scrolls back to the top', () => {
findPagination().vm.$emit('next');
it('when navigating to another page, scrolls back to the top', async () => {
await navigateToPage('next');
expect(scrollToElement).toHaveBeenCalledWith(wrapper.vm.$el);
});
......@@ -138,18 +154,16 @@ describe('BaseTab', () => {
expect(Object.keys(router.currentRoute.query)).not.toContain('after');
expect(requestHandler).toHaveBeenCalledTimes(1);
findPagination().vm.$emit('next');
await wrapper.vm.$nextTick();
await navigateToPage('next');
expect(Object.keys(router.currentRoute.query)).toContain('after');
expect(requestHandler).toHaveBeenCalledTimes(2);
});
it('when navigating back to the previous page, the route is updated and pipelines are fetched', async () => {
findPagination().vm.$emit('next');
await wrapper.vm.$nextTick();
findPagination().vm.$emit('prev');
await wrapper.vm.$nextTick();
await navigateToPage('next');
await waitForPromises();
await navigateToPage('prev');
expect(Object.keys(router.currentRoute.query)).not.toContain('after');
expect(Object.keys(router.currentRoute.query)).toContain('before');
......@@ -167,4 +181,34 @@ describe('BaseTab', () => {
expect(findEmptyState().exists()).toBe(true);
});
});
describe('when the request errors out', () => {
let respondWithError;
beforeEach(async () => {
respondWithError = true;
requestHandler = () => {
const response = respondWithError
? Promise.reject()
: Promise.resolve(allPipelinesWithPipelinesMock);
respondWithError = false;
return response;
};
createComponent();
await waitForPromises();
});
it('shows an error alert', () => {
expect(findErrorAlert().exists()).toBe(true);
});
it('removes the alert if the next request succeeds', async () => {
expect(findErrorAlert().exists()).toBe(true);
wrapper.vm.$apollo.queries.pipelines.refetch();
await waitForPromises();
expect(findErrorAlert().exists()).toBe(false);
});
});
});
......@@ -23913,6 +23913,9 @@ msgstr ""
msgid "OnCallSchedules|Your schedule has been successfully created. To add individual users to this schedule, use the Add a rotation button. To enable notifications for this schedule, you must also create an %{linkStart}escalation policy%{linkEnd}."
msgstr ""
msgid "OnDemandScans|Could not fetch on-demand scans. Please refresh the page, or try again later."
msgstr ""
msgid "OnDemandScans|Could not fetch scanner profiles. Please refresh the page, or try again later."
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