Commit 63bcee63 authored by Daniel Tian's avatar Daniel Tian Committed by Scott Hampton

Move dashboard settings to project manager component

Removes an unnecessary component and moves the logic it was
handling into a more appropriate component.
parent 7983e146
<script>
import { GlAlert } from '@gitlab/ui';
import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue';
import instanceProjectsQuery from 'ee/security_dashboard/graphql/queries/instance_projects.query.graphql';
import { PROJECT_LOADING_ERROR_MESSAGE } from '../helpers';
import ProjectManager from './first_class_project_manager/project_manager.vue';
export default {
components: {
ProjectManager,
SecurityDashboardLayout,
GlAlert,
},
apollo: {
projects: {
query: instanceProjectsQuery,
update(data) {
return data.instanceSecurityDashboard.projects.nodes;
},
error() {
this.hasError = true;
},
},
},
data() {
return {
projects: [],
hasError: false,
};
},
PROJECT_LOADING_ERROR_MESSAGE,
};
</script>
<template>
<security-dashboard-layout>
<gl-alert v-if="hasError" variant="danger">
{{ $options.PROJECT_LOADING_ERROR_MESSAGE }}
</gl-alert>
<div v-else class="gl-display-flex gl-justify-content-center">
<project-manager :projects="projects" />
</div>
</security-dashboard-layout>
</template>
<script>
import { GlBadge, GlButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import { PROJECT_LOADING_ERROR_MESSAGE } from 'ee/security_dashboard/helpers';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue';
import projectsQuery from '../../graphql/queries/instance_projects.query.graphql';
export default {
i18n: {
projectsAdded: s__('SecurityReports|Projects added'),
removeLabel: s__('SecurityReports|Remove project from dashboard'),
emptyMessage: s__(
'SecurityReports|Select a project to add by using the project search field above.',
),
},
components: {
GlBadge,
......@@ -16,57 +23,73 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
apollo: {
projects: {
type: Array,
required: true,
query: projectsQuery,
update(data) {
const projects = data?.instanceSecurityDashboard?.projects?.nodes;
if (projects === undefined) {
this.showErrorFlash();
}
return projects || [];
},
error() {
this.showErrorFlash();
},
},
showLoadingIndicator: {
type: Boolean,
required: true,
},
data() {
return {
projects: [],
};
},
computed: {
isLoadingProjects() {
return this.$apollo.queries.projects.loading;
},
},
methods: {
projectRemoved(project) {
this.$emit('projectRemoved', project);
},
showErrorFlash() {
createFlash({ message: PROJECT_LOADING_ERROR_MESSAGE });
},
},
};
</script>
<template>
<section>
<div>
<h4 class="h5 font-weight-bold text-secondary border-bottom mb-3 pb-2">
{{ s__('SecurityReports|Projects added') }}
<gl-badge class="gl-font-weight-bold">{{ projects.length }}</gl-badge>
<gl-loading-icon v-if="showLoadingIndicator" size="sm" class="float-right" />
</h4>
<ul v-if="projects.length" class="list-unstyled">
<li
v-for="project in projects"
:key="project.id"
class="d-flex align-items-center py-1 js-projects-list-project-item"
>
<project-avatar class="flex-shrink-0" :project="project" :size="32" />
<span>
{{ project.name_with_namespace || project.nameWithNamespace }}
</span>
<gl-button
v-gl-tooltip
icon="remove"
class="gl-ml-auto js-projects-list-project-remove"
:title="$options.i18n.removeLabel"
:aria-label="$options.i18n.removeLabel"
@click="projectRemoved(project)"
/>
</li>
</ul>
<p v-else class="text-secondary js-projects-list-empty-message">
{{
s__('SecurityReports|Select a project to add by using the project search field above.')
}}
</p>
</div>
<h5
class="gl-font-weight-bold gl-text-gray-500 gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-mb-5 gl-pb-3"
>
{{ $options.i18n.projectsAdded }}
<gl-badge class="gl-font-weight-bold">{{ projects.length }}</gl-badge>
</h5>
<gl-loading-icon v-if="isLoadingProjects" size="lg" />
<ul v-else-if="projects.length" class="gl-p-0">
<li
v-for="project in projects"
:key="project.id"
class="gl-display-flex gl-align-items-center gl-py-2 js-projects-list-project-item"
>
<project-avatar class="gl-flex-shrink-0" :project="project" :size="32" />
{{ project.nameWithNamespace }}
<gl-button
v-gl-tooltip
icon="remove"
class="gl-ml-auto js-projects-list-project-remove"
:title="$options.i18n.removeLabel"
:aria-label="$options.i18n.removeLabel"
@click="projectRemoved(project)"
/>
</li>
</ul>
<p v-else class="gl-text-gray-500 js-projects-list-empty-message" data-testid="empty-message">
{{ $options.i18n.emptyMessage }}
</p>
</section>
</template>
......@@ -19,17 +19,6 @@ export default {
ProjectList,
ProjectSelector,
},
props: {
isManipulatingProjects: {
type: Boolean,
required: false,
default: false,
},
projects: {
type: Array,
required: true,
},
},
data() {
return {
searchQuery: '',
......@@ -49,7 +38,7 @@ export default {
},
computed: {
canAddProjects() {
return !this.isManipulatingProjects && this.selectedProjects.length > 0;
return this.selectedProjects.length > 0;
},
isSearchingProjects() {
return this.searchCount > 0;
......@@ -288,12 +277,7 @@ export default {
</div>
</div>
<div class="row justify-content-center mt-md-3">
<project-list
:projects="projects"
:show-loading-indicator="isManipulatingProjects"
class="col col-lg-7"
@projectRemoved="removeProject"
/>
<project-list class="col col-lg-7" @projectRemoved="removeProject" />
</div>
</section>
</template>
import Vue from 'vue';
import InstanceSecurityDashboardSettings from './components/first_class_instance_security_dashboard_settings.vue';
import ProjectManager from './components/first_class_project_manager/project_manager.vue';
import apolloProvider from './graphql/provider';
export default (el) => {
......@@ -11,7 +11,7 @@ export default (el) => {
el,
apolloProvider,
render(createElement) {
return createElement(InstanceSecurityDashboardSettings);
return createElement(ProjectManager);
},
});
};
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import FirstClassInstanceDashboardSettings from 'ee/security_dashboard/components/first_class_instance_security_dashboard_settings.vue';
import ProjectManager from 'ee/security_dashboard/components/first_class_project_manager/project_manager.vue';
describe('First Class Instance Dashboard Component', () => {
let wrapper;
const defaultMocks = ({ loading = false } = {}) => ({
$apollo: { queries: { projects: { loading } } },
});
const findProjectManager = () => wrapper.find(ProjectManager);
const findAlert = () => wrapper.find(GlAlert);
const createWrapper = ({ mocks = defaultMocks(), data = {} }) => {
return shallowMount(FirstClassInstanceDashboardSettings, {
mocks,
data() {
return data;
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when there is no error', () => {
beforeEach(() => {
wrapper = createWrapper({});
});
it('displays the project manager', () => {
expect(findProjectManager().exists()).toBe(true);
});
it('does not render the alert component', () => {
expect(findAlert().exists()).toBe(false);
});
});
describe('when there is a loading error', () => {
beforeEach(() => {
wrapper = createWrapper({ data: { hasError: true } });
});
it('does not display the project manager', () => {
expect(findProjectManager().exists()).toBe(false);
});
it('renders the alert component', () => {
expect(findAlert().text()).toBe('An error occurred while retrieving projects.');
});
});
});
import { GlBadge, GlButton, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { GlBadge, GlLoadingIcon } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import ProjectList from 'ee/security_dashboard/components/first_class_project_manager/project_list.vue';
import projectsQuery from 'ee/security_dashboard/graphql/queries/instance_projects.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue';
const getArrayWithLength = (n) => [...Array(n).keys()];
const generateMockProjects = (projectsCount, mockProject = {}) =>
getArrayWithLength(projectsCount).map((id) => ({ id, ...mockProject }));
const localVue = createLocalVue();
localVue.use(VueApollo);
const generateMockProjects = (count) => {
const projects = [];
for (let i = 0; i < count; i += 1) {
projects.push({
id: i,
name: `project${i}`,
nameWithNamespace: `group/project${i}`,
});
}
return projects;
};
describe('Project List component', () => {
let wrapper;
const factory = ({ projects = [], stubs = {}, showLoadingIndicator = false } = {}) => {
wrapper = shallowMount(ProjectList, {
stubs,
propsData: {
projects,
showLoadingIndicator,
const getMockData = (projects) => ({
data: {
instanceSecurityDashboard: {
projects: {
nodes: projects,
},
},
});
},
});
const createWrapper = ({ projects }) => {
const mockData = getMockData(projects);
wrapper = extendedWrapper(
shallowMount(ProjectList, {
localVue,
apolloProvider: createMockApollo([[projectsQuery, jest.fn().mockResolvedValue(mockData)]]),
}),
);
};
const getAllProjectItems = () => wrapper.findAll('.js-projects-list-project-item');
const getFirstProjectItem = () => wrapper.find('.js-projects-list-project-item');
const getFirstRemoveButton = () => getFirstProjectItem().find('.js-projects-list-project-remove');
const getLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
afterEach(() => wrapper.destroy());
it('shows an empty state if there are no projects', () => {
factory();
it('shows an empty state if there are no projects', async () => {
createWrapper({ projects: [] });
await wrapper.vm.$nextTick();
expect(wrapper.text()).toContain(
'Select a project to add by using the project search field above.',
);
expect(wrapper.findByTestId('empty-message').exists()).toBe(true);
});
it('does not show a loading indicator when showLoadingIndicator = false', () => {
factory();
describe('loading indicator', () => {
it('shows the loading indicator when query is loading', () => {
createWrapper({ projects: [] });
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
});
expect(getLoadingIcon().exists()).toBe(true);
});
it('shows a loading indicator when showLoadingIndicator = true', () => {
factory({ showLoadingIndicator: true });
it('hides the loading indicator when query is not loading', async () => {
createWrapper({ projects: [] });
await wrapper.vm.$nextTick();
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(getLoadingIcon().exists()).toBe(false);
});
});
it.each([0, 1, 2])(
'renders a list of projects and displays a count of how many there are',
(projectsCount) => {
factory({ projects: generateMockProjects(projectsCount) });
'renders a list of projects and displays the correct count for %s projects',
async (projectsCount) => {
createWrapper({ projects: generateMockProjects(projectsCount) });
await wrapper.vm.$nextTick();
expect(getAllProjectItems()).toHaveLength(projectsCount);
expect(wrapper.find(GlBadge).text()).toBe(`${projectsCount}`);
expect(wrapper.find(GlBadge).text()).toBe(projectsCount.toString());
},
);
it('renders a project-item with an avatar', () => {
factory({ projects: generateMockProjects(1) });
expect(getFirstProjectItem().find(ProjectAvatar).exists()).toBe(true);
});
it('renders a project-item with a project name', () => {
const projectNameWithNamespace = 'foo';
describe('project item', () => {
const projects = generateMockProjects(1);
factory({
projects: generateMockProjects(1, { name_with_namespace: projectNameWithNamespace }),
beforeEach(() => {
createWrapper({ projects });
});
expect(getFirstProjectItem().text()).toContain(projectNameWithNamespace);
});
it('renders a project-item with a GraphQL project name', () => {
const projectNameWithNamespace = 'foo';
factory({
projects: generateMockProjects(1, { nameWithNamespace: projectNameWithNamespace }),
it('renders a project item with an avatar', () => {
expect(getFirstProjectItem().find(ProjectAvatar).exists()).toBe(true);
});
expect(getFirstProjectItem().text()).toContain(projectNameWithNamespace);
});
it('renders a project-item with a remove button', () => {
factory({ projects: generateMockProjects(1) });
expect(getFirstRemoveButton().exists()).toBe(true);
});
it(`emits a 'projectRemoved' event when a project's remove button has been clicked`, () => {
const mockProjects = generateMockProjects(1);
const [projectData] = mockProjects;
it('renders a project item with a project name', () => {
expect(getFirstProjectItem().text()).toContain(projects[0].nameWithNamespace);
});
factory({ projects: mockProjects, stubs: { GlButton } });
it('renders a project item with a remove button', () => {
expect(getFirstRemoveButton().exists()).toBe(true);
});
getFirstRemoveButton().vm.$emit('click');
it(`emits a 'projectRemoved' event when a project's remove button has been clicked`, () => {
getFirstRemoveButton().vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('projectRemoved')).toHaveLength(1);
expect(wrapper.emitted('projectRemoved')).toEqual([[projectData]]);
expect(wrapper.emitted('projectRemoved')[0][0]).toEqual(projects[0]);
});
});
});
......@@ -32,11 +32,6 @@ describe('Project Manager component', () => {
},
};
const defaultProps = {
isManipulatingProjects: false,
projects: [],
};
const createWrapper = ({ data = {}, mocks = {}, props = {} }) => {
spyQuery = defaultMocks.$apollo.query;
spyMutate = defaultMocks.$apollo.mutate;
......@@ -45,7 +40,7 @@ describe('Project Manager component', () => {
return { ...data };
},
mocks: { ...defaultMocks, ...mocks },
propsData: { ...defaultProps, ...props },
propsData: props,
});
};
......
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