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 ""
msgid "Project path"
msgstr ""
msgid "Project scanning help page"
msgstr ""
msgid "Project security status"
msgstr ""
......@@ -20371,6 +20374,21 @@ msgstr ""
msgid "Unresolve thread"
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"
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