Commit 750845e9 authored by Denys Mishunov's avatar Denys Mishunov

Merge branch '341359-dast-view-scans-empty-tabs' into 'master'

DAST on-demand scans empty tabs

See merge request gitlab-org/gitlab!71920
parents 3ff08c6c 736aee5a
......@@ -9,25 +9,50 @@ export default {
GlLink,
},
inject: ['newDastScanPath', 'helpPagePath', 'emptyStateSvgPath'],
props: {
title: {
type: String,
required: false,
default: s__('OnDemandScans|On-demand scans'),
},
text: {
type: String,
required: false,
default: s__(
'OnDemandScans|On-demand scans run outside of DevOps cycle and find vulnerabilities in your projects. %{learnMoreLinkStart}Lean more%{learnMoreLinkEnd}.',
),
},
noPrimaryButton: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
emptyStateProps() {
const props = {
title: this.title,
svgPath: this.emptyStateSvgPath,
};
if (!this.noPrimaryButton) {
props.primaryButtonText = this.$options.i18n.primaryButtonText;
props.primaryButtonLink = this.newDastScanPath;
}
return props;
},
},
i18n: {
title: s__('OnDemandScans|On-demand scans'),
primaryButtonText: s__('OnDemandScans|New DAST scan'),
text: s__(
'OnDemandScans|On-demand scans run outside of DevOps cycle and find vulnerabilities in your projects. %{learnMoreLinkStart}Lean more%{learnMoreLinkEnd}.',
),
},
};
</script>
<template>
<gl-empty-state
:title="$options.i18n.title"
:svg-path="emptyStateSvgPath"
:primary-button-text="$options.i18n.primaryButtonText"
:primary-button-link="newDastScanPath"
>
<gl-empty-state v-bind="emptyStateProps">
<template #description>
<gl-sprintf :message="$options.i18n.text">
<gl-sprintf :message="text">
<template #learnMoreLink="{ content }">
<gl-link :href="helpPagePath">{{ content }}</gl-link>
</template>
......
<script>
import { GlTabs } from '@gitlab/ui';
import AllTab from './tabs/all.vue';
import RunningTab from './tabs/running.vue';
import FinishedTab from './tabs/finished.vue';
import ScheduledTab from './tabs/scheduled.vue';
import EmptyState from './empty_state.vue';
const TABS = {
all: {
component: AllTab,
},
running: {
component: RunningTab,
},
finished: {
component: FinishedTab,
},
scheduled: {
component: ScheduledTab,
},
};
export default {
TABS,
components: {
GlTabs,
AllTab,
RunningTab,
FinishedTab,
ScheduledTab,
EmptyState,
},
data() {
return {
activeTabIndex: 0,
hasData: false,
};
},
computed: {
activeTab: {
set(newTabIndex) {
const newTabId = Object.keys(TABS)[newTabIndex];
if (this.$route.params.tabId !== newTabId) {
this.$router.push(`/${newTabId}`);
}
this.activeTabIndex = newTabIndex;
},
get() {
return this.activeTabIndex;
},
},
},
created() {
const tabIndex = Object.keys(TABS).findIndex((tab) => tab === this.$route.params.tabId);
if (tabIndex !== -1) {
this.activeTabIndex = tabIndex;
}
},
};
</script>
<template>
<empty-state />
<gl-tabs v-if="hasData" v-model="activeTab">
<component :is="tab.component" v-for="(tab, key) in $options.TABS" :key="key" :item-count="0" />
</gl-tabs>
<empty-state v-else />
</template>
<script>
import { __ } from '~/locale';
import BaseTab from './base_tab.vue';
export default {
components: {
BaseTab,
},
i18n: {
title: __('All'),
},
};
</script>
<template>
<base-tab :title="$options.i18n.title" v-bind="$attrs" />
</template>
<script>
import { GlTab, GlBadge } from '@gitlab/ui';
import EmptyState from '../empty_state.vue';
export default {
components: {
GlTab,
GlBadge,
EmptyState,
},
props: {
title: {
type: String,
required: true,
},
itemCount: {
type: Number,
required: true,
},
emptyStateTitle: {
type: String,
required: false,
default: undefined,
},
emptyStateText: {
type: String,
required: false,
default: undefined,
},
},
};
</script>
<template>
<gl-tab v-bind="$attrs">
<template #title>
{{ title }}
<gl-badge size="sm" class="gl-tab-counter-badge">{{ itemCount }}</gl-badge>
</template>
<empty-state :title="emptyStateTitle" :text="emptyStateText" no-primary-button />
</gl-tab>
</template>
<script>
import { __, s__ } from '~/locale';
import BaseTab from './base_tab.vue';
export default {
components: {
BaseTab,
},
i18n: {
title: __('Finished'),
emptyStateTitle: s__('OnDemandScans|There are no finished scans.'),
emptyStateText: s__(
'OnDemandScans|%{learnMoreLinkStart}Lean more about on-demand scans%{learnMoreLinkEnd}.',
),
},
};
</script>
<template>
<base-tab
:title="$options.i18n.title"
:empty-state-title="$options.i18n.emptyStateTitle"
:empty-state-text="$options.i18n.emptyStateText"
v-bind="$attrs"
/>
</template>
<script>
import { __, s__ } from '~/locale';
import BaseTab from './base_tab.vue';
export default {
components: {
BaseTab,
},
i18n: {
title: __('Running'),
emptyStateTitle: s__('OnDemandScans|There are no running scans.'),
emptyStateText: s__(
'OnDemandScans|%{learnMoreLinkStart}Lean more about on-demand scans%{learnMoreLinkEnd}.',
),
},
};
</script>
<template>
<base-tab
:title="$options.i18n.title"
:empty-state-title="$options.i18n.emptyStateTitle"
:empty-state-text="$options.i18n.emptyStateText"
v-bind="$attrs"
/>
</template>
<script>
import { __, s__ } from '~/locale';
import BaseTab from './base_tab.vue';
export default {
components: {
BaseTab,
},
i18n: {
title: __('Scheduled'),
emptyStateTitle: s__('OnDemandScans|There are no scheduled scans.'),
emptyStateText: s__(
'OnDemandScans|%{learnMoreLinkStart}Lean more about on-demand scans%{learnMoreLinkEnd}.',
),
},
};
</script>
<template>
<base-tab
:title="$options.i18n.title"
:empty-state-title="$options.i18n.emptyStateTitle"
:empty-state-text="$options.i18n.emptyStateText"
v-bind="$attrs"
/>
</template>
import Vue from 'vue';
import { createRouter } from './router';
import OnDemandScans from './components/on_demand_scans.vue';
export default () => {
......@@ -11,6 +12,7 @@ export default () => {
return new Vue({
el,
router: createRouter(),
provide: {
newDastScanPath,
helpPagePath,
......
import Vue from 'vue';
import VueRouter from 'vue-router';
import { joinPaths } from '~/lib/utils/url_utility';
Vue.use(VueRouter);
export const createRouter = (base) =>
new VueRouter({
mode: 'hash',
base: joinPaths(gon.relative_url_root || '', base),
routes: [
{
path: '/:tabId',
name: 'tab',
},
],
});
import { mount } from '@vue/test-utils';
import { GlEmptyState } from '@gitlab/ui';
import EmptyState from 'ee/on_demand_scans/components/empty_state.vue';
describe('EmptyState', () => {
let wrapper;
const createComponent = () => {
// Finders
const findGlEmptyState = () => wrapper.find(GlEmptyState);
// Helpers
const defaultGlEmptyStateProp = (prop) => GlEmptyState.props[prop].default;
const createComponent = (propsData = {}) => {
wrapper = mount(EmptyState, {
provide: {
newDastScanPath: '/on_demand_scans/new',
helpPagePath: '/help/page/path',
emptyStateSvgPath: '/empty/state/svg/path',
},
propsData,
});
};
......@@ -23,4 +31,35 @@ describe('EmptyState', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('passes custom title down to GlEmptyState', () => {
const title = 'Custom title';
createComponent({
title,
});
expect(findGlEmptyState().props('title')).toBe(title);
});
it('passes custom text down to GlEmptyState', () => {
const text = 'Custom text';
createComponent({
text,
});
expect(findGlEmptyState().text()).toMatch(text);
});
it('does not pass primary props when no-primary-button is true', () => {
createComponent({
noPrimaryButton: true,
});
expect(findGlEmptyState().props('primaryButtonLink')).toBe(
defaultGlEmptyStateProp('primaryButtonLink'),
);
expect(findGlEmptyState().props('primaryButtonLink')).toBe(
defaultGlEmptyStateProp('primaryButtonLink'),
);
});
});
import { shallowMount } from '@vue/test-utils';
import { GlTabs } from '@gitlab/ui';
import OnDemandScans from 'ee/on_demand_scans/components/on_demand_scans.vue';
import { createRouter } from 'ee/on_demand_scans/router';
import AllTab from 'ee/on_demand_scans/components/tabs/all.vue';
import RunningTab from 'ee/on_demand_scans/components/tabs/running.vue';
import FinishedTab from 'ee/on_demand_scans/components/tabs/finished.vue';
import ScheduledTab from 'ee/on_demand_scans/components/tabs/scheduled.vue';
import EmptyState from 'ee/on_demand_scans/components/empty_state.vue';
describe('OnDemandScans', () => {
let wrapper;
let router;
// Finders
const findTabs = () => wrapper.findComponent(GlTabs);
const findAllTab = () => wrapper.findComponent(AllTab);
const findRunningTab = () => wrapper.findComponent(RunningTab);
const findFinishedTab = () => wrapper.findComponent(FinishedTab);
const findScheduledTab = () => wrapper.findComponent(ScheduledTab);
const findEmptyState = () => wrapper.findComponent(EmptyState);
const createComponent = () => {
wrapper = shallowMount(OnDemandScans);
wrapper = shallowMount(OnDemandScans, {
router,
});
};
beforeEach(() => {
router = createRouter();
});
afterEach(() => {
wrapper.destroy();
});
it('renders an empty state', () => {
it('renders an empty state when there is no data', () => {
createComponent();
expect(findEmptyState().exists()).toBe(true);
});
describe('when there is data', () => {
beforeEach(() => {
createComponent();
wrapper.setData({ hasData: true });
});
it('renders the tabs if there is data', async () => {
expect(findAllTab().exists()).toBe(true);
expect(findRunningTab().exists()).toBe(true);
expect(findFinishedTab().exists()).toBe(true);
expect(findScheduledTab().exists()).toBe(true);
});
it('updates the route when the active tab changes', async () => {
const finishedTabIndex = 2;
findTabs().vm.$emit('input', finishedTabIndex);
await wrapper.vm.$nextTick();
expect(router.currentRoute.path).toBe('/finished');
});
});
});
import { shallowMount } from '@vue/test-utils';
import AllTab from 'ee/on_demand_scans/components/tabs/all.vue';
import BaseTab from 'ee/on_demand_scans/components/tabs/base_tab.vue';
describe('AllTab', () => {
let wrapper;
// Finders
const findBaseTab = () => wrapper.findComponent(BaseTab);
const createComponent = (propsData) => {
wrapper = shallowMount(AllTab, {
propsData,
});
};
beforeEach(() => {
createComponent({
itemCount: 12,
});
});
it('renders the base tab with the correct props', () => {
expect(findBaseTab().props('title')).toBe('All');
expect(findBaseTab().props('itemCount')).toBe(12);
});
});
import { GlTab } from '@gitlab/ui';
import { stubComponent } from 'helpers/stub_component';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import BaseTab from 'ee/on_demand_scans/components/tabs/base_tab.vue';
import EmptyState from 'ee/on_demand_scans/components/empty_state.vue';
describe('BaseTab', () => {
let wrapper;
// Finders
const findTitle = () => wrapper.findByTestId('tab-title');
const findEmptyState = () => wrapper.findComponent(EmptyState);
const createComponent = (propsData) => {
wrapper = shallowMountExtended(BaseTab, {
propsData,
stubs: {
GlTab: stubComponent(GlTab, {
template: `
<div>
<span data-testid="tab-title">
<slot name="title" />
</span>
<slot />
</div>
`,
}),
},
});
};
beforeEach(() => {
createComponent({
title: 'All',
itemCount: 12,
});
});
it('renders the title with the item count', () => {
expect(findTitle().text()).toMatchInterpolatedText('All 12');
});
it('renders an empty state', () => {
expect(findEmptyState().exists()).toBe(true);
});
});
import { shallowMount } from '@vue/test-utils';
import RunningTab from 'ee/on_demand_scans/components/tabs/running.vue';
import FinishedTab from 'ee/on_demand_scans/components/tabs/finished.vue';
import ScheduledTab from 'ee/on_demand_scans/components/tabs/scheduled.vue';
import BaseTab from 'ee/on_demand_scans/components/tabs/base_tab.vue';
describe.each`
title | component
${'Running'} | ${RunningTab}
${'Finished'} | ${FinishedTab}
${'Scheduled'} | ${ScheduledTab}
`('$title tab', ({ title, component }) => {
let wrapper;
// Props
const itemCount = 12;
// Finders
const findBaseTab = () => wrapper.findComponent(BaseTab);
const createComponent = (propsData) => {
wrapper = shallowMount(component, {
propsData,
});
};
beforeEach(() => {
createComponent({
itemCount,
});
});
it('renders the base tab with the correct props', () => {
expect(findBaseTab().props('title')).toBe(title);
expect(findBaseTab().props('itemCount')).toBe(itemCount);
expect(findBaseTab().props('emptyStateTitle')).toBe(wrapper.vm.$options.i18n.emptyStateTitle);
expect(findBaseTab().props('emptyStateText')).toBe(wrapper.vm.$options.i18n.emptyStateText);
});
});
......@@ -23727,6 +23727,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|%{learnMoreLinkStart}Lean more about on-demand scans%{learnMoreLinkEnd}."
msgstr ""
msgid "OnDemandScans|Could not fetch scanner profiles. Please refresh the page, or try again later."
msgstr ""
......@@ -23811,6 +23814,15 @@ msgstr ""
msgid "OnDemandScans|Start time"
msgstr ""
msgid "OnDemandScans|There are no finished scans."
msgstr ""
msgid "OnDemandScans|There are no running scans."
msgstr ""
msgid "OnDemandScans|There are no scheduled scans."
msgstr ""
msgid "OnDemandScans|Use existing scanner profile"
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