Commit 5a69d75a authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'extract-common-store-from-operations-dashbord' into 'master'

Extract common store from operations dashbord

Closes #11443 and #3713

See merge request gitlab-org/gitlab-ee!11702
parents 3a0bcca8 a9b23d10
<script>
import _ from 'underscore';
import { mapState, mapActions } from 'vuex';
import {
GlLoadingIcon,
GlModal,
......@@ -59,19 +60,17 @@ export default {
required: true,
},
},
data() {
return {
projects: [],
projectTokens: '',
isLoadingProjects: false,
selectedProjects: [],
projectSearchResults: [],
searchCount: 0,
searchQuery: '',
messages: {},
};
},
computed: {
...mapState([
'projects',
'projectTokens',
'isLoadingProjects',
'selectedProjects',
'projectSearchResults',
'searchCount',
'searchQuery',
'messages',
]),
isSearchingProjects() {
return this.searchCount > 0;
},
......@@ -87,13 +86,15 @@ export default {
this.fetchProjects();
},
methods: {
fetchSearchResults() {},
addProjectsToDashboard() {},
fetchProjects() {},
setProjectEndpoints() {},
clearSearchResults() {},
toggleSelectedProject() {},
setSearchQuery() {},
...mapActions([
'fetchSearchResults',
'addProjectsToDashboard',
'fetchProjects',
'setProjectEndpoints',
'clearSearchResults',
'toggleSelectedProject',
'setSearchQuery',
]),
addProjects() {
this.addProjectsToDashboard();
},
......
import Vue from 'vue';
import EnvironmentDashboardComponent from 'ee/environments_dashboard/components/dashboard/dashboard.vue';
import createStore from 'ee/vue_shared/dashboards/store';
document.addEventListener(
'DOMContentLoaded',
() =>
new Vue({
el: '#js-environments',
store: createStore(),
components: {
EnvironmentDashboardComponent,
},
......
import Vue from 'vue';
import store from 'ee/operations/store';
import createStore from 'ee/vue_shared/dashboards/store';
import DashboardComponent from 'ee/operations/components/dashboard/dashboard.vue';
document.addEventListener(
......@@ -7,7 +7,7 @@ document.addEventListener(
() =>
new Vue({
el: '#js-operations',
store,
store: createStore(),
components: {
DashboardComponent,
},
......
......@@ -48,24 +48,32 @@ export const receiveAddProjectsToDashboardSuccess = ({ dispatch, state }, data)
const { added, invalid } = data;
if (invalid.length) {
const projectNames = state.selectedProjects.reduce((accumulator, project) => {
if (invalid.includes(project.id)) {
accumulator.push(project.name);
}
return accumulator;
}, []);
const [firstProject, secondProject, ...rest] = state.selectedProjects
.filter(project => invalid.includes(project.id))
.map(project => project.name);
const translationValues = {
firstProject,
secondProject,
rest: rest.join(', '),
};
let invalidProjects;
if (projectNames.length > 2) {
invalidProjects = `${projectNames.slice(0, -1).join(', ')}, and ${projectNames.pop()}`;
} else if (projectNames.length > 1) {
invalidProjects = projectNames.join(' and ');
if (rest.length > 0) {
invalidProjects = sprintf(
s__('Dashboard|%{firstProject}, %{rest}, and %{secondProject}'),
translationValues,
);
} else if (secondProject) {
invalidProjects = sprintf(
s__('Dashboard|%{firstProject} and %{secondProject}'),
translationValues,
);
} else {
[invalidProjects] = projectNames;
invalidProjects = firstProject;
}
createFlash(
sprintf(
s__(
'OperationsDashboard|Unable to add %{invalidProjects}. The Operations Dashboard is available for public projects, and private projects in groups with a Gold plan.',
'Dashboard|Unable to add %{invalidProjects}. This dashboard is available for public projects, and private projects in groups with a Gold plan.',
),
{
invalidProjects,
......@@ -124,7 +132,7 @@ export const receiveProjectsSuccess = ({ commit }, data) => {
export const receiveProjectsError = ({ commit }) => {
commit(types.RECEIVE_PROJECTS_ERROR);
createFlash(__('Something went wrong, unable to get operations projects'));
createFlash(__('Something went wrong, unable to get projects'));
};
export const removeProject = ({ dispatch }, removePath) => {
......
......@@ -6,8 +6,9 @@ import * as actions from './actions';
Vue.use(Vuex);
export default new Vuex.Store({
state,
mutations,
actions,
});
export default () =>
new Vuex.Store({
state,
mutations,
actions,
});
---
title: Add Frontend Store and UI For Environments Dashboard MVC
merge_request: 11702
author:
type: added
......@@ -4,9 +4,19 @@ exports[`dashboard should match the snapshot 1`] = `
<div
class="operations-dashboard"
>
<div>
<!---->
</div>
<glmodal-stub
modalid="add-projects-modal"
ok-disabled="true"
ok-title="Add projects"
ok-variant="success"
title="Add projects"
titletag="h4"
>
<projectselector-stub
projectsearchresults=""
selectedprojects=""
/>
</glmodal-stub>
<div
class="page-title-holder flex-fill d-flex align-items-center"
......@@ -19,14 +29,14 @@ exports[`dashboard should match the snapshot 1`] = `
</h1>
<button
class="btn js-add-projects-button btn btn-success btn-secondary"
type="button"
<glbutton-stub
class="js-add-projects-button btn btn-success"
role="button"
>
Add projects
</button>
</glbutton-stub>
</div>
<div
......
import { mount, createLocalVue } from '@vue/test-utils';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { GlButton } from '@gitlab/ui';
import { GlButton, GlModal } from '@gitlab/ui';
import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue';
import createStore from 'ee/vue_shared/dashboards/store/index';
import state from 'ee/vue_shared/dashboards/store/state';
import component from 'ee/environments_dashboard/components/dashboard/dashboard.vue';
import ProjectHeader from 'ee/environments_dashboard/components/dashboard/project_header.vue';
import Environment from 'ee/environments_dashboard/components/dashboard/environment.vue';
......@@ -13,10 +15,19 @@ localVue.use(Vuex);
describe('dashboard', () => {
const Component = localVue.extend(component);
let actionSpies;
const store = createStore();
let wrapper;
let propsData;
beforeEach(() => {
actionSpies = {
addProjectsToDashboard: jest.fn(),
clearSearchResults: jest.fn(),
setSearchQuery: jest.fn(),
fetchSearchResults: jest.fn(),
toggleSelectedProject: jest.fn(),
};
propsData = {
addPath: 'mock-addPath',
listPath: 'mock-listPath',
......@@ -24,16 +35,21 @@ describe('dashboard', () => {
emptyDashboardHelpPath: '/help/user/operations_dashboard/index.html',
};
wrapper = mount(Component, {
wrapper = shallowMount(Component, {
propsData,
localVue,
store,
methods: {
fetchProjects: () => {},
...actionSpies,
},
sync: false,
});
});
afterEach(() => {
store.replaceState(state());
});
it('should match the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
......@@ -52,19 +68,11 @@ describe('dashboard', () => {
it('is labelled correctly', () => {
expect(button.text()).toBe('Add projects');
});
it('should show the modal on click', done => {
button.trigger('click');
wrapper.vm.$nextTick(() => {
expect(wrapper.find(ProjectSelector)).toExist();
done();
});
});
});
describe('wrapped components', () => {
beforeEach(done => {
wrapper.vm.projects = [
beforeEach(() => {
store.state.projects = [
{
id: 0,
name: 'test',
......@@ -73,7 +81,6 @@ describe('dashboard', () => {
},
{ id: 1, name: 'test', namespace: { name: 'test', id: 0 }, environments: [environment] },
];
wrapper.vm.$nextTick(() => done());
});
describe('project header', () => {
......@@ -89,5 +96,36 @@ describe('dashboard', () => {
expect(environments.length).toBe(3);
});
});
describe('project selector modal', () => {
beforeEach(() => {
wrapper.find(GlButton).trigger('click');
});
it('should fire the add projects action on ok', () => {
wrapper.find(GlModal).vm.$emit('ok');
expect(actionSpies.addProjectsToDashboard).toHaveBeenCalled();
});
it('should fire clear search when the modal is hidden', () => {
wrapper.find(GlModal).vm.$emit('hidden');
expect(actionSpies.clearSearchResults).toHaveBeenCalled();
});
it('should set the search query when searching', () => {
wrapper.find(ProjectSelector).vm.$emit('searched', 'test');
expect(actionSpies.setSearchQuery).toHaveBeenCalledWith('test');
});
it('should fetch query results when searching', () => {
wrapper.find(ProjectSelector).vm.$emit('searched', 'test');
expect(actionSpies.fetchSearchResults).toHaveBeenCalled();
});
it('should toggle a project when clicked', () => {
wrapper.find(ProjectSelector).vm.$emit('projectClicked', { name: 'test', id: 1 });
expect(actionSpies.toggleSelectedProject).toHaveBeenCalledWith({ name: 'test', id: 1 });
});
});
});
});
......@@ -4,7 +4,7 @@ import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import Project from 'ee/operations/components/dashboard/project.vue';
import Dashboard from 'ee/operations/components/dashboard/dashboard.vue';
import store from 'ee/operations/store';
import createStore from 'ee/vue_shared/dashboards/store';
import timeoutPromise from 'spec/helpers/set_timeout_promise_helper';
import { trimText } from 'spec/helpers/vue_component_helper';
import { mockProjectData, mockText } from '../../mock_data';
......@@ -16,6 +16,7 @@ describe('dashboard component', () => {
const mockAddEndpoint = 'mock-addPath';
const mockListEndpoint = 'mock-listPath';
const DashboardComponent = localVue.extend(Dashboard);
const store = createStore();
let wrapper;
let mockAxios;
......@@ -83,7 +84,7 @@ describe('dashboard component', () => {
.then(timeoutPromise)
.then(() => {
expect(store.state.projects.length).toEqual(2);
expect(wrapper.element.querySelectorAll('.js-dashboard-project').length).toEqual(2);
expect(wrapper.findAll(Project).length).toEqual(2);
done();
})
.catch(done.fail);
......@@ -102,7 +103,7 @@ describe('dashboard component', () => {
});
it('includes a dashboard project component for each project', () => {
const projectComponents = wrapper.element.querySelectorAll('.js-dashboard-project');
const projectComponents = wrapper.findAll(Project);
expect(projectComponents.length).toBe(projectCount);
});
......@@ -125,7 +126,7 @@ describe('dashboard component', () => {
timeoutPromise()
.then(() => {
expect(store.state.projects.length).toEqual(0);
expect(wrapper.element.querySelectorAll('.js-dashboard-project').length).toEqual(0);
expect(wrapper.findAll(Project).length).toEqual(0);
done();
})
.catch(done.fail);
......
......@@ -4,7 +4,7 @@ import Commit from '~/vue_shared/components/commit.vue';
import Project from 'ee/operations/components/dashboard/project.vue';
import ProjectHeader from 'ee/operations/components/dashboard/project_header.vue';
import Alerts from 'ee/vue_shared/dashboards/components/alerts.vue';
import store from 'ee/operations/store';
import store from 'ee/vue_shared/dashboards/store';
import { mockOneProject } from '../../mock_data';
const localVue = createLocalVue();
......
import state from 'ee/operations/store/state';
export function clearState(store) {
store.replaceState(state());
}
/**
* @deprecated
* DO NOT USE! This causes issues when `vue-test-utils` is used elsewhere.
......
import mockPipelineData from 'ee_spec/vue_shared/dashboards/mock_data';
import { mockProjectData, mockText as text } from 'ee_spec/vue_shared/dashboards/mock_data';
export { mockProjectData } from 'ee_spec/vue_shared/dashboards/mock_data';
export const mockText = {
...text,
ADD_PROJECTS: 'Add projects',
ADD_PROJECTS_ERROR: 'Something went wrong, unable to add projects to dashboard',
REMOVE_PROJECT_ERROR: 'Something went wrong, unable to remove project',
DASHBOARD_TITLE: 'Operations Dashboard',
EMPTY_TITLE: 'Add a project to the dashboard',
EMPTY_SUBTITLE:
"The operations dashboard provides a summary of each project's operational health, including pipeline and alert statuses. More information",
EMPTY_SVG_SOURCE: '/assets/illustrations/operations-dashboard_empty.svg',
NO_SEARCH_RESULTS: 'Sorry, no projects matched your search',
RECEIVE_PROJECTS_ERROR: 'Something went wrong, unable to get operations projects',
REMOVE_PROJECT: 'Remove',
SEARCH_PROJECTS: 'Search your projects',
SEARCH_DESCRIPTION_SUFFIX: 'in projects',
};
export function mockProjectData(
projectCount = 1,
currentPipelineStatus = 'success',
upstreamStatus = 'success',
alertCount = 0,
) {
return Array(projectCount)
.fill(null)
.map((_, index) => ({
id: index,
description: '',
name: 'test-project',
name_with_namespace: 'Test / test-project',
path: 'test-project',
path_with_namespace: 'test/test-project',
created_at: '2019-02-01T15:40:27.522Z',
default_branch: 'master',
tag_list: [],
avatar_url: null,
web_url: 'https://mock-web_url/',
namespace: {
id: 1,
name: 'test',
path: 'test',
kind: 'user',
full_path: 'user',
parent_id: null,
},
remove_path: '/-/operations?project_id=1',
last_pipeline: mockPipelineData(currentPipelineStatus),
upstream_pipeline: mockPipelineData(upstreamStatus),
downstream_pipelines: [],
alert_count: alertCount,
}));
}
export const [mockOneProject] = mockProjectData(1);
import { mount, createLocalVue } from '@vue/test-utils';
import ProjectPipeline from 'ee/vue_shared/dashboards/components/project_pipeline.vue';
import mockPipelineData from '../mock_data';
import { mockPipelineData } from '../mock_data';
const localVue = createLocalVue();
......
import state from 'ee/vue_shared/dashboards/store/state';
export default function clearState(store) {
store.replaceState(state());
}
......@@ -2,7 +2,14 @@ import { TEST_HOST } from 'spec/test_constants';
const AVATAR_URL = `${TEST_HOST}/dummy.jpg`;
export default function mockPipelineData(
export const mockText = {
ADD_PROJECTS_ERROR: 'Something went wrong, unable to add projects to dashboard',
REMOVE_PROJECT_ERROR: 'Something went wrong, unable to remove project',
NO_SEARCH_RESULTS: 'Sorry, no projects matched your search',
RECEIVE_PROJECTS_ERROR: 'Something went wrong, unable to get projects',
};
export function mockPipelineData(
status = 'success',
id = 1,
finishedTimeStamp = new Date(Date.now() - 86400000).toISOString(),
......@@ -66,3 +73,39 @@ export default function mockPipelineData(
},
};
}
export function mockProjectData(
projectCount = 1,
currentPipelineStatus = 'success',
upstreamStatus = 'success',
alertCount = 0,
) {
return Array(projectCount)
.fill(null)
.map((_, index) => ({
id: index,
description: '',
name: 'test-project',
name_with_namespace: 'Test / test-project',
path: 'test-project',
path_with_namespace: 'test/test-project',
created_at: '2019-02-01T15:40:27.522Z',
default_branch: 'master',
tag_list: [],
avatar_url: null,
web_url: 'https://mock-web_url/',
namespace: {
id: 1,
name: 'test',
path: 'test',
kind: 'user',
full_path: 'user',
parent_id: null,
},
remove_path: '/-/operations?project_id=1',
last_pipeline: mockPipelineData(currentPipelineStatus),
upstream_pipeline: mockPipelineData(upstreamStatus),
downstream_pipelines: [],
alert_count: alertCount,
}));
}
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import store from 'ee/operations/store/index';
import * as types from 'ee/operations/store/mutation_types';
import defaultActions, * as actions from 'ee/operations/store/actions';
import createStore from 'ee/vue_shared/dashboards/store/index';
import * as types from 'ee/vue_shared/dashboards/store/mutation_types';
import defaultActions, * as actions from 'ee/vue_shared/dashboards/store/actions';
import testAction from 'spec/helpers/vuex_action_helper';
import { clearState } from '../helpers';
import clearState from '../helpers';
import { mockText, mockProjectData } from '../mock_data';
describe('actions', () => {
......@@ -13,9 +13,11 @@ describe('actions', () => {
const mockResponse = { data: 'mock-data' };
const mockProjects = mockProjectData(1);
const [mockOneProject] = mockProjects;
let store;
let mockAxios;
beforeEach(() => {
store = createStore();
mockAxios = new MockAdapter(axios);
});
......@@ -118,7 +120,7 @@ describe('actions', () => {
});
const errorMessage =
'The Operations Dashboard is available for public projects, and private projects in groups with a Gold plan.';
'This dashboard is available for public projects, and private projects in groups with a Gold plan.';
const selectProjects = count => {
for (let i = 0; i < count; i += 1) {
store.dispatch('toggleSelectedProject', {
......@@ -303,7 +305,7 @@ describe('actions', () => {
});
describe('receiveRemoveProjectSuccess', () => {
it('fetches operations dashboard projects', done => {
it('fetches dashboard projects', done => {
testAction(
actions.receiveRemoveProjectSuccess,
null,
......
import state from 'ee/operations/store/state';
import mutations from 'ee/operations/store/mutations';
import * as types from 'ee/operations/store/mutation_types';
import state from 'ee/vue_shared/dashboards/store/state';
import mutations from 'ee/vue_shared/dashboards/store/mutations';
import * as types from 'ee/vue_shared/dashboards/store/mutation_types';
import { mockProjectData } from '../mock_data';
describe('mutations', () => {
......
......@@ -3705,6 +3705,15 @@ msgstr ""
msgid "Dashboards"
msgstr ""
msgid "Dashboard|%{firstProject} and %{secondProject}"
msgstr ""
msgid "Dashboard|%{firstProject}, %{rest}, and %{secondProject}"
msgstr ""
msgid "Dashboard|Unable to add %{invalidProjects}. This dashboard is available for public projects, and private projects in groups with a Gold plan."
msgstr ""
msgid "Data is still calculating..."
msgstr ""
......@@ -8542,9 +8551,6 @@ msgstr ""
msgid "OperationsDashboard|The operations dashboard provides a summary of each project's operational health, including pipeline and alert statuses."
msgstr ""
msgid "OperationsDashboard|Unable to add %{invalidProjects}. The Operations Dashboard is available for public projects, and private projects in groups with a Gold plan."
msgstr ""
msgid "Optional"
msgstr ""
......@@ -11309,7 +11315,7 @@ msgstr ""
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
msgid "Something went wrong, unable to get operations projects"
msgid "Something went wrong, unable to get projects"
msgstr ""
msgid "Something went wrong, unable to remove project"
......
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