Commit a31f94f1 authored by Mark Florian's avatar Mark Florian Committed by Fatih Acet

Add Dependency List Vuex store

This forms part of the [Dependency List MVC][1].

[1]: https://gitlab.com/gitlab-org/gitlab-ee/issues/10075
parent 707f3604
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
import createFlash from '~/flash';
import { FETCH_ERROR_MESSAGE } from './constants';
import { isValidResponse } from './utils';
import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
export const setDependenciesEndpoint = ({ commit }, endpoint) =>
commit(types.SET_DEPENDENCIES_ENDPOINT, endpoint);
export const requestDependencies = ({ commit }) => commit(types.REQUEST_DEPENDENCIES);
export const receiveDependenciesSuccess = ({ commit }, { headers, data }) => {
const normalizedHeaders = normalizeHeaders(headers);
const pageInfo = parseIntPagination(normalizedHeaders);
const { dependencies, report: reportInfo } = data;
commit(types.RECEIVE_DEPENDENCIES_SUCCESS, { dependencies, reportInfo, pageInfo });
};
export const receiveDependenciesError = ({ commit }, error) =>
commit(types.RECEIVE_DEPENDENCIES_ERROR, error);
export const fetchDependencies = ({ state, dispatch }, params = {}) => {
if (!state.endpoint) {
return;
}
dispatch('requestDependencies');
axios
.get(state.endpoint, {
params: {
sort_by: state.sortField,
sort: state.sortOrder,
page: state.pageInfo.page || 1,
...params,
},
})
.then(response => {
if (isValidResponse(response)) {
dispatch('receiveDependenciesSuccess', response);
} else {
throw new Error('Invalid server response');
}
})
.catch(error => {
dispatch('receiveDependenciesError', error);
createFlash(FETCH_ERROR_MESSAGE);
});
};
export const setSortField = ({ commit, dispatch }, id) => {
commit(types.SET_SORT_FIELD, id);
dispatch('fetchDependencies', { page: 1 });
};
export const toggleSortOrder = ({ commit, dispatch }) => {
commit(types.TOGGLE_SORT_ORDER);
dispatch('fetchDependencies', { page: 1 });
};
import { __, s__ } from '~/locale';
export const SORT_FIELDS = {
name: s__('Dependencies|Component name'),
packager: s__('Dependencies|Packager'),
};
export const SORT_ORDER = {
ascending: 'asc',
descending: 'desc',
};
export const REPORT_STATUS = {
ok: 'ok',
jobNotSetUp: 'job_not_set_up',
jobFailed: 'job_failed',
noDependencies: 'no_dependencies',
incomplete: 'no_dependency_files',
};
export const FETCH_ERROR_MESSAGE = __(
'Error fetching the dependency list. Please check your network connection and try again.',
);
import { REPORT_STATUS } from './constants';
export const isJobNotSetUp = state => state.reportInfo.status === REPORT_STATUS.jobNotSetUp;
export const isJobFailed = state =>
[REPORT_STATUS.jobFailed, REPORT_STATUS.noDependencies].includes(state.reportInfo.status);
export const isIncomplete = state => state.reportInfo.status === REPORT_STATUS.incomplete;
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';
Vue.use(Vuex);
export default () =>
new Vuex.Store({
actions,
getters,
mutations,
state,
});
export const SET_DEPENDENCIES_ENDPOINT = 'SET_DEPENDENCIES_ENDPOINT';
export const REQUEST_DEPENDENCIES = 'REQUEST_DEPENDENCIES';
export const RECEIVE_DEPENDENCIES_SUCCESS = 'RECEIVE_DEPENDENCIES_SUCCESS';
export const RECEIVE_DEPENDENCIES_ERROR = 'RECEIVE_DEPENDENCIES_ERROR';
export const SET_SORT_FIELD = 'SET_SORT_FIELD';
export const TOGGLE_SORT_ORDER = 'TOGGLE_SORT_ORDER';
import * as types from './mutation_types';
import { REPORT_STATUS, SORT_ORDER } from './constants';
export default {
[types.SET_DEPENDENCIES_ENDPOINT](state, payload) {
state.endpoint = payload;
},
[types.REQUEST_DEPENDENCIES](state) {
state.isLoading = true;
state.errorLoading = false;
},
[types.RECEIVE_DEPENDENCIES_SUCCESS](state, { dependencies, reportInfo, pageInfo }) {
state.dependencies = dependencies;
state.pageInfo = pageInfo;
state.isLoading = false;
state.errorLoading = false;
state.reportInfo.status = reportInfo.status;
state.reportInfo.jobPath = reportInfo.job_path;
state.initialized = true;
},
[types.RECEIVE_DEPENDENCIES_ERROR](state) {
state.isLoading = false;
state.errorLoading = true;
state.dependencies = [];
state.pageInfo = {};
state.reportInfo = {
status: REPORT_STATUS.ok,
jobPath: '',
};
state.initialized = true;
},
[types.SET_SORT_FIELD](state, payload) {
state.sortField = payload;
},
[types.TOGGLE_SORT_ORDER](state) {
state.sortOrder =
state.sortOrder === SORT_ORDER.ascending ? SORT_ORDER.descending : SORT_ORDER.ascending;
},
};
import { REPORT_STATUS, SORT_FIELDS, SORT_ORDER } from './constants';
export default () => ({
endpoint: '',
initialized: false,
isLoading: false,
errorLoading: false,
dependencies: [],
pageInfo: {},
reportInfo: {
status: REPORT_STATUS.ok,
jobPath: '',
},
sortField: 'name',
sortFields: SORT_FIELDS,
sortOrder: SORT_ORDER.ascending,
});
import { REPORT_STATUS } from './constants';
export const hasDependencyList = ({ dependencies }) => Array.isArray(dependencies);
export const hasReportStatus = ({ report }) =>
Boolean(report && Object.values(REPORT_STATUS).includes(report.status));
export const isValidResponse = ({ data }) =>
Boolean(data && hasDependencyList(data) && hasReportStatus(data));
import _ from 'underscore';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'helpers/test_constants';
import * as actions from 'ee/dependencies/store/actions';
import * as types from 'ee/dependencies/store/mutation_types';
import getInitialState from 'ee/dependencies/store/state';
import { SORT_ORDER, FETCH_ERROR_MESSAGE } from 'ee/dependencies/store/constants';
import createFlash from '~/flash';
import mockDependenciesResponse from './data/mock_dependencies';
jest.mock('~/flash', () => jest.fn());
describe('Dependencies actions', () => {
const pageInfo = {
page: 3,
nextPage: 2,
previousPage: 1,
perPage: 20,
total: 100,
totalPages: 5,
};
const headers = {
'X-Next-Page': pageInfo.nextPage,
'X-Page': pageInfo.page,
'X-Per-Page': pageInfo.perPage,
'X-Prev-Page': pageInfo.previousPage,
'X-Total': pageInfo.total,
'X-Total-Pages': pageInfo.totalPages,
};
afterEach(() => {
createFlash.mockClear();
});
describe('setDependenciesEndpoint', () => {
it('commits the SET_DEPENDENCIES_ENDPOINT mutation', () =>
testAction(
actions.setDependenciesEndpoint,
TEST_HOST,
getInitialState(),
[
{
type: types.SET_DEPENDENCIES_ENDPOINT,
payload: TEST_HOST,
},
],
[],
));
});
describe('requestDependencies', () => {
it('commits the REQUEST_DEPENDENCIES mutation', () =>
testAction(
actions.requestDependencies,
undefined,
getInitialState(),
[
{
type: types.REQUEST_DEPENDENCIES,
},
],
[],
));
});
describe('receiveDependenciesSuccess', () => {
it('commits the RECEIVE_DEPENDENCIES_SUCCESS mutation', () =>
testAction(
actions.receiveDependenciesSuccess,
{ headers, data: mockDependenciesResponse },
getInitialState(),
[
{
type: types.RECEIVE_DEPENDENCIES_SUCCESS,
payload: {
dependencies: mockDependenciesResponse.dependencies,
reportInfo: mockDependenciesResponse.report,
pageInfo,
},
},
],
[],
));
});
describe('receiveDependenciesError', () => {
it('commits the RECEIVE_DEPENDENCIES_ERROR mutation', () => {
const error = { error: true };
return testAction(
actions.receiveDependenciesError,
error,
getInitialState(),
[
{
type: types.RECEIVE_DEPENDENCIES_ERROR,
payload: error,
},
],
[],
);
});
});
describe('fetchDependencies', () => {
const dependenciesPackagerDescending = {
...mockDependenciesResponse,
dependencies: _.sortBy(mockDependenciesResponse.dependencies, 'packager').reverse(),
};
let state;
let mock;
beforeEach(() => {
state = getInitialState();
state.endpoint = `${TEST_HOST}/dependencies`;
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('when endpoint is empty', () => {
beforeEach(() => {
state.endpoint = '';
});
it('does nothing', () => testAction(actions.fetchDependencies, undefined, state, [], []));
});
describe('on success', () => {
describe('given no params', () => {
beforeEach(() => {
state.pageInfo = { ...pageInfo };
const paramsDefault = {
sort_by: state.sortField,
sort: state.sortOrder,
page: state.pageInfo.page,
};
mock
.onGet(state.endpoint, { params: paramsDefault })
.replyOnce(200, mockDependenciesResponse, headers);
});
it('uses default sorting params from state', () =>
testAction(
actions.fetchDependencies,
undefined,
state,
[],
[
{
type: 'requestDependencies',
},
{
type: 'receiveDependenciesSuccess',
payload: expect.objectContaining({ data: mockDependenciesResponse, headers }),
},
],
));
});
describe('given params', () => {
const paramsGiven = { sort_by: 'packager', sort: SORT_ORDER.descending, page: 4 };
beforeEach(() => {
mock
.onGet(state.endpoint, { params: paramsGiven })
.replyOnce(200, dependenciesPackagerDescending, headers);
});
it('overrides default params', () =>
testAction(
actions.fetchDependencies,
paramsGiven,
state,
[],
[
{
type: 'requestDependencies',
},
{
type: 'receiveDependenciesSuccess',
payload: expect.objectContaining({ data: dependenciesPackagerDescending, headers }),
},
],
));
});
});
describe.each`
responseType | responseDetails
${'an invalid response'} | ${[200, { foo: 'bar' }]}
${'a response error'} | ${[500]}
`('given $responseType', ({ responseDetails }) => {
beforeEach(() => {
mock.onGet(state.endpoint).replyOnce(...responseDetails);
});
it('dispatches the receiveDependenciesError action and creates a flash', () =>
testAction(
actions.fetchDependencies,
undefined,
state,
[],
[
{
type: 'requestDependencies',
},
{
type: 'receiveDependenciesError',
payload: expect.any(Error),
},
],
).then(() => {
expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith(FETCH_ERROR_MESSAGE);
}));
});
});
describe('setSortField', () => {
it('commits the SET_SORT_FIELD mutation and dispatch the fetchDependencies action', () => {
const field = 'packager';
return testAction(
actions.setSortField,
field,
getInitialState(),
[
{
type: types.SET_SORT_FIELD,
payload: field,
},
],
[
{
type: 'fetchDependencies',
payload: { page: 1 },
},
],
);
});
});
describe('toggleSortOrder', () => {
it('commits the TOGGLE_SORT_ORDER mutation and dispatch the fetchDependencies action', () =>
testAction(
actions.toggleSortOrder,
undefined,
getInitialState(),
[
{
type: types.TOGGLE_SORT_ORDER,
},
],
[
{
type: 'fetchDependencies',
payload: { page: 1 },
},
],
));
});
});
{
"dependencies": [
{
"name": "underscore",
"packager": "JavaScript (npm)",
"location": {
"blob_path": "/a/b/blob/da39a3ee5e6b4b0d3255bfef95601890afd80709/web/yarn.lock",
"path": "web/yarn.lock"
},
"version": "1.9.0"
},
{
"name": "rails",
"packager": "Ruby (gem)",
"location": {
"blob_path": "/c/d/blob/b6589fc6ab0dc82cf12099d1c2d40ab994e8410c/Gemfile.lock",
"path": "Gemfile.lock"
},
"version": "5.2.3"
},
{
"name": "accepts",
"packager": "Java (maven)",
"location": {
"blob_path": "/e/f/blob/356a192b7913b04c54574d18c28d46e6395428ab/src/server/Pom.xml",
"path": "src/server/Pom.xml"
},
"version": "1.3.4"
}
],
"report": {
"status": "ok",
"job_path": ""
}
}
import * as getters from 'ee/dependencies/store/getters';
import { REPORT_STATUS } from 'ee/dependencies/store/constants';
describe('Dependencies getters', () => {
describe.each`
getterName | reportStatus | outcome
${'isJobNotSetUp'} | ${REPORT_STATUS.jobNotSetUp} | ${true}
${'isJobNotSetUp'} | ${REPORT_STATUS.ok} | ${false}
${'isJobFailed'} | ${REPORT_STATUS.jobFailed} | ${true}
${'isJobFailed'} | ${REPORT_STATUS.noDependencies} | ${true}
${'isJobFailed'} | ${REPORT_STATUS.ok} | ${false}
${'isIncomplete'} | ${REPORT_STATUS.incomplete} | ${true}
${'isIncomplete'} | ${REPORT_STATUS.ok} | ${false}
`('$getterName when report status is $reportStatus', ({ getterName, reportStatus, outcome }) => {
it(`returns ${outcome}`, () => {
expect(
getters[getterName]({
reportInfo: {
status: reportStatus,
},
}),
).toBe(outcome);
});
});
});
import * as types from 'ee/dependencies/store/mutation_types';
import mutations from 'ee/dependencies/store/mutations';
import getInitialState from 'ee/dependencies/store/state';
import { REPORT_STATUS, SORT_ORDER } from 'ee/dependencies/store/constants';
import { TEST_HOST } from 'helpers/test_constants';
describe('Dependencies mutations', () => {
let state;
beforeEach(() => {
state = getInitialState();
});
describe(types.SET_DEPENDENCIES_ENDPOINT, () => {
it('sets the endpoint and download endpoint', () => {
mutations[types.SET_DEPENDENCIES_ENDPOINT](state, TEST_HOST);
expect(state.endpoint).toBe(TEST_HOST);
});
});
describe(types.REQUEST_DEPENDENCIES, () => {
beforeEach(() => {
mutations[types.REQUEST_DEPENDENCIES](state);
});
it('correctly mutates the state', () => {
expect(state.isLoading).toBe(true);
expect(state.errorLoading).toBe(false);
});
});
describe(types.RECEIVE_DEPENDENCIES_SUCCESS, () => {
const dependencies = [];
const pageInfo = {};
const reportInfo = {
status: REPORT_STATUS.jobFailed,
job_path: 'foo',
};
beforeEach(() => {
mutations[types.RECEIVE_DEPENDENCIES_SUCCESS](state, { dependencies, reportInfo, pageInfo });
});
it('correctly mutates the state', () => {
expect(state.isLoading).toBe(false);
expect(state.errorLoading).toBe(false);
expect(state.dependencies).toBe(dependencies);
expect(state.pageInfo).toBe(pageInfo);
expect(state.initialized).toBe(true);
expect(state.reportInfo).toEqual({
status: REPORT_STATUS.jobFailed,
jobPath: 'foo',
});
});
});
describe(types.RECEIVE_DEPENDENCIES_ERROR, () => {
beforeEach(() => {
mutations[types.RECEIVE_DEPENDENCIES_ERROR](state);
});
it('correctly mutates the state', () => {
expect(state.isLoading).toBe(false);
expect(state.errorLoading).toBe(true);
expect(state.dependencies).toEqual([]);
expect(state.pageInfo).toEqual({});
expect(state.initialized).toBe(true);
expect(state.reportInfo).toEqual({
status: REPORT_STATUS.ok,
jobPath: '',
});
});
});
describe(types.SET_SORT_FIELD, () => {
it('sets the sort field', () => {
const field = 'foo';
mutations[types.SET_SORT_FIELD](state, field);
expect(state.sortField).toBe(field);
});
});
describe(types.TOGGLE_SORT_ORDER, () => {
it('toggles the sort order', () => {
const sortState = { sortOrder: SORT_ORDER.ascending };
mutations[types.TOGGLE_SORT_ORDER](sortState);
expect(sortState.sortOrder).toBe(SORT_ORDER.descending);
mutations[types.TOGGLE_SORT_ORDER](sortState);
expect(sortState.sortOrder).toBe(SORT_ORDER.ascending);
});
});
});
......@@ -4067,6 +4067,9 @@ msgstr ""
msgid "Dependencies|Component"
msgstr ""
msgid "Dependencies|Component name"
msgstr ""
msgid "Dependencies|Job failed to generate the dependency list"
msgstr ""
......@@ -5005,6 +5008,9 @@ msgstr ""
msgid "Error fetching refs"
msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
msgid "Error fetching usage ping data."
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