Commit 998189a7 authored by David Pisek's avatar David Pisek Committed by Kushal Pandya

Add unscanned projects: Vue components

This commit adds two components to render a list of unscanned
projects within the group security dashboard.

It includes:

* Vue components
* Tests
* Translation files
parent 448d0fbf
<script>
import { GlBadge, GlTabs, GlTab, GlLink } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import UnscannedProjectsTabContent from './unscanned_projects_tab_content.vue';
export default {
components: { GlBadge, GlTabs, GlTab, GlLink, Icon, UnscannedProjectsTabContent },
props: {
endpoint: {
type: String,
required: true,
},
helpPath: {
type: String,
required: false,
default: '',
},
},
computed: {
...mapState('unscannedProjects', ['isLoading']),
...mapGetters('unscannedProjects', [
'outdatedProjects',
'outdatedProjectsCount',
'untestedProjects',
'untestedProjectsCount',
]),
hasOutdatedProjects() {
return this.outdatedProjectsCount > 0;
},
hasUntestedProjects() {
return this.untestedProjectsCount > 0;
},
},
created() {
this.fetchUnscannedProjects(this.endpoint);
},
methods: {
...mapActions('unscannedProjects', ['fetchUnscannedProjects']),
},
};
</script>
<template>
<section class="border rounded">
<header class="px-3 pt-3 mb-0">
<h4 class="my-0">
{{ s__('UnscannedProjects|Project scanning') }}
<gl-link
v-if="helpPath"
:href="helpPath"
:title="__('Project scanning help page')"
target="_blank"
><icon name="question" :size="12" class="align-top"
/></gl-link>
</h4>
<p class="text-secondary mb-0">
{{ s__('UnscannedProjects|Default branch scanning by project') }}
</p>
</header>
<div>
<gl-tabs>
<gl-tab ref="outdatedProjectsTab" title-item-class="ml-3">
<template #title>
{{ s__('UnscannedProjects|Out of date') }}
<gl-badge v-if="!isLoading" ref="outdatedProjectsCount" pill>{{
outdatedProjectsCount
}}</gl-badge>
</template>
<unscanned-projects-tab-content :is-loading="isLoading" :is-empty="!hasOutdatedProjects">
<div v-for="dateRange in outdatedProjects" :key="dateRange.fromDay" class="mb-3">
<h5 class="m-0">{{ dateRange.description }}</h5>
<ul class="list-unstyled mb-0">
<li v-for="project in dateRange.projects" :key="project.id" class="mt-1">
<gl-link target="_blank" :href="`${project.fullPath}/pipelines`">{{
project.fullName
}}</gl-link>
</li>
</ul>
</div>
</unscanned-projects-tab-content>
</gl-tab>
<gl-tab ref="untestedProjectsTab" title-item-class="ml-3">
<template #title>
{{ s__('UnscannedProjects|Untested') }}
<gl-badge v-if="!isLoading" ref="untestedProjectsCount" pill>{{
untestedProjectsCount
}}</gl-badge>
</template>
<unscanned-projects-tab-content :is-loading="isLoading" :is-empty="!hasUntestedProjects">
<ul class="list-unstyled m-0">
<li v-for="project in untestedProjects" :key="project.id" class="mb-1">
<gl-link target="_blank" :href="`${project.fullPath}/security/configuration`">{{
project.fullName
}}</gl-link>
</li>
</ul>
</unscanned-projects-tab-content>
</gl-tab>
</gl-tabs>
</div>
</section>
</template>
<script>
import { GlLoadingIcon } from '@gitlab/ui';
export default {
components: {
GlLoadingIcon,
},
props: {
isLoading: {
type: Boolean,
required: false,
default: false,
},
isEmpty: {
type: Boolean,
required: false,
default: false,
},
},
// The fixed height prevents the tab-content from jumping around and is set to match the other widgets
// within the aside.
// Details: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24242#note_282008456
wrapperHeight: '15rem',
};
</script>
<template>
<div class="mx-3 my-2 overflow-auto" :style="{ height: $options.wrapperHeight }">
<gl-loading-icon v-if="isLoading" size="sm" />
<template v-else>
<template v-if="!isEmpty">
<slot></slot>
</template>
<p v-else class="mb-0">
{{ s__('UnscannedProjects|Your projects are up do date! Nice job!') }}
</p>
</template>
</div>
</template>
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UnscannedProjects component header matches the snapshot when the "helpPath" prop is empty 1`] = `
<header
class="px-3 pt-3 mb-0"
>
<h4
class="my-0"
>
Project scanning
<!---->
</h4>
<p
class="text-secondary mb-0"
>
Default branch scanning by project
</p>
</header>
`;
exports[`UnscannedProjects component header matches the snapshot when the "helpPath" prop is not empty 1`] = `
<header
class="px-3 pt-3 mb-0"
>
<h4
class="my-0"
>
Project scanning
<a
class="gl-link"
href="/foo/bar/help"
rel="noopener"
target="_blank"
title="Project scanning help page"
>
<svg
aria-hidden="true"
class="align-top s12 ic-question"
>
<use
xlink:href="#question"
/>
</svg>
</a>
</h4>
<p
class="text-secondary mb-0"
>
Default branch scanning by project
</p>
</header>
`;
exports[`UnscannedProjects component tab content shows a list of outdated projects 1`] = `
<div
class="mx-3 my-2 overflow-auto"
style="height: 15rem;"
>
<div
class="mb-3"
>
<h5
class="m-0"
>
Outdated Projects Group 1
</h5>
<ul
class="list-unstyled mb-0"
>
<li
class="mt-1"
>
<a
class="gl-link"
href="/outdated-project-1/pipelines"
rel="noopener"
target="_blank"
>
Outdated Project One
</a>
</li>
</ul>
</div>
<div
class="mb-3"
>
<h5
class="m-0"
>
Outdated Projects Group 2
</h5>
<ul
class="list-unstyled mb-0"
>
<li
class="mt-1"
>
<a
class="gl-link"
href="/outdated-project-2/pipelines"
rel="noopener"
target="_blank"
>
Outdated Project Two
</a>
</li>
</ul>
</div>
</div>
`;
exports[`UnscannedProjects component tab content shows a list of untested projects 1`] = `
<div
class="mx-3 my-2 overflow-auto"
style="height: 15rem;"
>
<ul
class="list-unstyled m-0"
>
<li
class="mb-1"
>
<a
class="gl-link"
href="/untested-1/security/configuration"
rel="noopener"
target="_blank"
>
Untested Project One
</a>
</li>
<li
class="mb-1"
>
<a
class="gl-link"
href="/untested-2/security/configuration"
rel="noopener"
target="_blank"
>
Untested Project Two
</a>
</li>
</ul>
</div>
`;
import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import { GlTab } from '@gitlab/ui';
import UnscannedProjects from 'ee/security_dashboard/components/unscanned_projects.vue';
import TabContent from 'ee/security_dashboard/components/unscanned_projects_tab_content.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('UnscannedProjects component', () => {
let wrapper;
const defaultPropsData = { endpoint: 'foo/bar/endpoint', helpPath: 'foo/bar/help' };
const defaultState = {
isLoading: false,
projects: [],
};
const defaultGetters = {
outdatedProjects: () => [],
untestedProjects: () => [],
outdatedProjectsCount: () => 1,
untestedProjectsCount: () => 1,
};
const defaultActions = {
fetchUnscannedProjects: jest.fn(),
};
const factory = ({ getters = {}, propsData = {}, state = {} } = {}) => {
const store = new Vuex.Store({
modules: {
unscannedProjects: {
namespaced: true,
actions: defaultActions,
getters: { ...defaultGetters, ...getters },
state: { ...defaultState, ...state },
},
},
});
wrapper = mount(UnscannedProjects, {
propsData: { ...defaultPropsData, ...propsData },
store,
localVue,
sync: false,
});
};
const outdatedProjectsTab = () => wrapper.find({ ref: 'outdatedProjectsTab' });
const untestedProjectsTab = () => wrapper.find({ ref: 'untestedProjectsTab' });
const outdatedProjectsTabContent = () => outdatedProjectsTab().find(TabContent);
const untestedProjectsTabContent = () => untestedProjectsTab().find(TabContent);
const outdatedProjectsCount = () => wrapper.find({ ref: 'outdatedProjectsCount' });
const untestedProjectsCount = () => wrapper.find({ ref: 'untestedProjectsCount' });
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('lifecycle hooks', () => {
it('fetches projects from the given endpoint when the component is created', () => {
factory();
expect(defaultActions.fetchUnscannedProjects).toHaveBeenCalledTimes(1);
expect(defaultActions.fetchUnscannedProjects.mock.calls[0][1]).toBe(
defaultPropsData.endpoint,
);
});
});
describe('header', () => {
it.each`
helpPath | description
${'/foo/bar/help'} | ${'not empty'}
${null} | ${'empty'}
`('matches the snapshot when the "helpPath" prop is $description', ({ helpPath }) => {
factory({ propsData: { helpPath } });
expect(wrapper.find('header').element).toMatchSnapshot();
});
});
describe('tab buttons', () => {
it('shows a tab-button for projects which have outdated security scanning', () => {
factory();
expect(outdatedProjectsTab().is(GlTab)).toBe(true);
});
it.each`
type | projectsCount
${'outdated'} | ${outdatedProjectsCount}
${'untested'} | ${untestedProjectsCount}
`(`shows a count of $type projects`, ({ type, projectsCount }) => {
factory({
getters: {
[`${type}ProjectsCount`]: () => 99,
},
});
return wrapper.vm.$nextTick().then(() => {
expect(projectsCount().text()).toContain(99);
});
});
it('shows a tab-button for projects which have no security scanning configured', () => {
factory();
expect(untestedProjectsTab().is(GlTab)).toBe(true);
});
});
describe('tab content', () => {
beforeEach(factory);
it.each`
type | tabContent
${'outdated'} | ${outdatedProjectsTabContent}
${'untested'} | ${untestedProjectsTabContent}
`(
'passes the "isLoading" prop to the $type projects tab-content component',
({ tabContent }) => {
expect(tabContent().props('isLoading')).toBe(false);
factory({ state: { isLoading: true } });
return wrapper.vm.$nextTick(() => {
expect(tabContent().props('isLoading')).toBe(true);
});
},
);
it.each`
type | tabContent
${'outdated'} | ${outdatedProjectsTabContent}
${'untested'} | ${untestedProjectsTabContent}
`(
'passes the "isEmpty" prop to the $type projects tab-content component',
({ type, tabContent }) => {
expect(tabContent().props('isEmpty')).toBe(false);
factory({ getters: { [`${type}ProjectsCount`]: () => 0 } });
return wrapper.vm.$nextTick(() => {
expect(tabContent().props('isEmpty')).toBe(true);
});
},
);
it('shows a list of outdated projects', () => {
factory({
getters: {
outdatedProjects: () => [
{
description: 'Outdated Projects Group 1',
projects: [{ fullName: 'Outdated Project One', fullPath: '/outdated-project-1' }],
},
{
description: 'Outdated Projects Group 2',
projects: [{ fullName: 'Outdated Project Two', fullPath: '/outdated-project-2' }],
},
],
},
});
expect(outdatedProjectsTabContent().element).toMatchSnapshot();
});
it('shows a list of untested projects', () => {
factory({
getters: {
untestedProjects: () => [
{ fullName: 'Untested Project One', fullPath: '/untested-1' },
{ fullName: 'Untested Project Two', fullPath: '/untested-2' },
],
},
});
expect(untestedProjectsTabContent().element).toMatchSnapshot();
});
});
});
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import UnscannedProjectsTabContent from 'ee/security_dashboard/components/unscanned_projects_tab_content.vue';
const localVue = createLocalVue();
describe('UnscannedProjectTabContent Component', () => {
let wrapper;
const factory = (propsData = {}) => {
wrapper = shallowMount(UnscannedProjectsTabContent, {
propsData,
slots: { default: '<span class="default-slot"></span>' },
localVue,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const defaultSlot = () => wrapper.find('.default-slot');
describe('default state', () => {
beforeEach(factory);
it('does not contain a loading icon', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
});
it('renders the default slot', () => {
expect(defaultSlot().exists()).toBe(true);
});
});
describe('loading state', () => {
beforeEach(() => {
factory({ isLoading: true });
});
it('contains a loading icon', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
});
describe('empty state', () => {
beforeEach(() => {
factory({ isEmpty: true });
});
it('does not render the default slot', () => {
expect(defaultSlot().exists()).toBe(false);
});
it('contains a message to indicate that all projects are up to date', () => {
expect(wrapper.text()).toContain('Your projects are up do date');
});
});
});
...@@ -14557,6 +14557,9 @@ msgstr "" ...@@ -14557,6 +14557,9 @@ msgstr ""
msgid "Project path" msgid "Project path"
msgstr "" msgstr ""
msgid "Project scanning help page"
msgstr ""
msgid "Project security status" msgid "Project security status"
msgstr "" msgstr ""
...@@ -20371,6 +20374,21 @@ msgstr "" ...@@ -20371,6 +20374,21 @@ msgstr ""
msgid "Unresolve thread" msgid "Unresolve thread"
msgstr "" msgstr ""
msgid "UnscannedProjects|Default branch scanning by project"
msgstr ""
msgid "UnscannedProjects|Out of date"
msgstr ""
msgid "UnscannedProjects|Project scanning"
msgstr ""
msgid "UnscannedProjects|Untested"
msgstr ""
msgid "UnscannedProjects|Your projects are up do date! Nice job!"
msgstr ""
msgid "Unschedule job" msgid "Unschedule job"
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