Commit 0dcdce58 authored by Miguel Rincon's avatar Miguel Rincon Committed by Fatih Acet

Add ability to duplicate system dashboards

Adds a new item to the dashboard dropdown to allow the user
to duplicate the dashboard into their projects.

Add a modal to create a new dashboard based on a system dashboard,
the user can provide file name and commit message for the new file.

Refactors the dropdown into a separate component to reduce
the dashboard complexity.
parent a1d66cf7
......@@ -17,10 +17,13 @@ import createFlash from '~/flash';
import Icon from '~/vue_shared/components/icon.vue';
import { getParameterValues, mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
import DateTimePicker from './date_time_picker/date_time_picker.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
import GroupEmptyState from './group_empty_state.vue';
import DashboardsDropdown from './dashboards_dropdown.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import { getTimeDiff, getAddMetricTrackingOptions } from '../utils';
import { metricStates } from '../constants';
......@@ -31,16 +34,18 @@ export default {
components: {
VueDraggable,
PanelType,
GraphGroup,
EmptyState,
GroupEmptyState,
Icon,
GlButton,
GlDropdown,
GlDropdownItem,
GlFormGroup,
GlModal,
DateTimePicker,
GraphGroup,
EmptyState,
GroupEmptyState,
DashboardsDropdown,
},
directives: {
GlModal: GlModalDirective,
......@@ -83,6 +88,10 @@ export default {
type: String,
required: true,
},
defaultBranch: {
type: String,
required: true,
},
metricsEndpoint: {
type: String,
required: true,
......@@ -140,6 +149,11 @@ export default {
required: false,
default: invalidUrl,
},
dashboardsEndpoint: {
type: String,
required: false,
default: invalidUrl,
},
currentDashboard: {
type: String,
required: false,
......@@ -199,9 +213,6 @@ export default {
selectedDashboard() {
return this.allDashboards.find(d => d.path === this.currentDashboard) || this.firstDashboard;
},
selectedDashboardText() {
return this.selectedDashboard.display_name;
},
showRearrangePanelsBtn() {
return !this.showEmptyState && this.rearrangePanelsAvailable;
},
......@@ -223,6 +234,7 @@ export default {
environmentsEndpoint: this.environmentsEndpoint,
deploymentsEndpoint: this.deploymentsEndpoint,
dashboardEndpoint: this.dashboardEndpoint,
dashboardsEndpoint: this.dashboardsEndpoint,
currentDashboard: this.currentDashboard,
projectPath: this.projectPath,
});
......@@ -314,6 +326,13 @@ export default {
return !this.getMetricStates(groupKey).includes(metricStates.OK);
},
getAddMetricTrackingOptions,
selectDashboard(dashboard) {
const params = {
dashboard: dashboard.path,
};
redirectTo(mergeUrlParams(params, window.location.href));
},
},
addMetric: {
title: s__('Metrics|Add metric'),
......@@ -333,21 +352,14 @@ export default {
label-for="monitor-dashboards-dropdown"
class="col-sm-12 col-md-6 col-lg-2"
>
<gl-dropdown
<dashboards-dropdown
id="monitor-dashboards-dropdown"
class="mb-0 d-flex js-dashboards-dropdown"
class="mb-0 d-flex"
toggle-class="dropdown-menu-toggle"
:text="selectedDashboardText"
>
<gl-dropdown-item
v-for="dashboard in allDashboards"
:key="dashboard.path"
:active="dashboard.path === currentDashboard"
active-class="is-active"
:href="`?dashboard=${dashboard.path}`"
>{{ dashboard.display_name || dashboard.path }}</gl-dropdown-item
>
</gl-dropdown>
:default-branch="defaultBranch"
:selected-dashboard="selectedDashboard"
@selectDashboard="selectDashboard($event)"
/>
</gl-form-group>
<gl-form-group
......
<script>
import { mapState, mapActions } from 'vuex';
import {
GlAlert,
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlModal,
GlLoadingIcon,
GlModalDirective,
} from '@gitlab/ui';
import DuplicateDashboardForm from './duplicate_dashboard_form.vue';
const events = {
selectDashboard: 'selectDashboard',
};
export default {
components: {
GlAlert,
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlModal,
GlLoadingIcon,
DuplicateDashboardForm,
},
directives: {
GlModal: GlModalDirective,
},
props: {
selectedDashboard: {
type: Object,
required: false,
default: () => ({}),
},
defaultBranch: {
type: String,
required: true,
},
},
data() {
return {
alert: null,
loading: false,
form: {},
};
},
computed: {
...mapState('monitoringDashboard', ['allDashboards']),
isSystemDashboard() {
return this.selectedDashboard.system_dashboard;
},
selectedDashboardText() {
return this.selectedDashboard.display_name;
},
},
methods: {
...mapActions('monitoringDashboard', ['duplicateSystemDashboard']),
selectDashboard(dashboard) {
this.$emit(events.selectDashboard, dashboard);
},
ok(bvModalEvt) {
// Prevent modal from hiding in case submit fails
bvModalEvt.preventDefault();
this.loading = true;
this.alert = null;
this.duplicateSystemDashboard(this.form)
.then(createdDashboard => {
this.loading = false;
this.alert = null;
// Trigger hide modal as submit is successful
this.$refs.duplicateDashboardModal.hide();
// Dashboards in the default branch become available immediately.
// Not so in other branches, so we refresh the current dashboard
const dashboard =
this.form.branch === this.defaultBranch ? createdDashboard : this.selectedDashboard;
this.$emit(events.selectDashboard, dashboard);
})
.catch(error => {
this.loading = false;
this.alert = error;
});
},
hide() {
this.alert = null;
},
formChange(form) {
this.form = form;
},
},
};
</script>
<template>
<gl-dropdown toggle-class="dropdown-menu-toggle" :text="selectedDashboardText">
<gl-dropdown-item
v-for="dashboard in allDashboards"
:key="dashboard.path"
:active="dashboard.path === selectedDashboard.path"
active-class="is-active"
@click="selectDashboard(dashboard)"
>
{{ dashboard.display_name || dashboard.path }}
</gl-dropdown-item>
<template v-if="isSystemDashboard">
<gl-dropdown-divider />
<gl-modal
ref="duplicateDashboardModal"
modal-id="duplicateDashboardModal"
:title="s__('Metrics|Duplicate dashboard')"
ok-variant="success"
@ok="ok"
@hide="hide"
>
<gl-alert v-if="alert" class="mb-3" variant="danger" @dismiss="alert = null">
{{ alert }}
</gl-alert>
<duplicate-dashboard-form
:dashboard="selectedDashboard"
:default-branch="defaultBranch"
@change="formChange"
/>
<template #modal-ok>
<gl-loading-icon v-if="loading" inline color="light" />
{{ loading ? s__('Metrics|Duplicating...') : s__('Metrics|Duplicate') }}
</template>
</gl-modal>
<gl-dropdown-item ref="duplicateDashboardItem" v-gl-modal="'duplicateDashboardModal'">
{{ s__('Metrics|Duplicate dashboard') }}
</gl-dropdown-item>
</template>
</gl-dropdown>
</template>
<script>
import { __, s__, sprintf } from '~/locale';
import { GlFormGroup, GlFormInput, GlFormRadioGroup, GlFormTextarea } from '@gitlab/ui';
const defaultFileName = dashboard => dashboard.path.split('/').reverse()[0];
export default {
components: {
GlFormGroup,
GlFormInput,
GlFormRadioGroup,
GlFormTextarea,
},
props: {
dashboard: {
type: Object,
required: true,
},
defaultBranch: {
type: String,
required: true,
},
},
radioVals: {
/* Use the default branch (e.g. master) */
DEFAULT: 'DEFAULT',
/* Create a new branch */
NEW: 'NEW',
},
data() {
return {
form: {
dashboard: this.dashboard.path,
fileName: defaultFileName(this.dashboard),
commitMessage: '',
},
branchName: '',
branchOption: this.$options.radioVals.NEW,
branchOptions: [
{
value: this.$options.radioVals.DEFAULT,
html: sprintf(
__('Commit to %{branchName} branch'),
{
branchName: `<strong>${this.defaultBranch}</strong>`,
},
false,
),
},
{ value: this.$options.radioVals.NEW, text: __('Create new branch') },
],
};
},
computed: {
defaultCommitMsg() {
return sprintf(s__('Metrics|Create custom dashboard %{fileName}'), {
fileName: this.form.fileName,
});
},
fileNameState() {
// valid if empty or *.yml
return !(this.form.fileName && !this.form.fileName.endsWith('.yml'));
},
fileNameFeedback() {
return !this.fileNameState ? s__('The file name should have a .yml extension') : '';
},
},
mounted() {
this.change();
},
methods: {
change() {
this.$emit('change', {
...this.form,
commitMessage: this.form.commitMessage || this.defaultCommitMsg,
branch:
this.branchOption === this.$options.radioVals.NEW ? this.branchName : this.defaultBranch,
});
},
focus(option) {
if (option === this.$options.radioVals.NEW) {
this.$nextTick(() => {
this.$refs.branchName.$el.focus();
});
}
},
},
};
</script>
<template>
<form @change="change">
<p class="text-muted">
{{
s__(`Metrics|You can save a copy of this dashboard to your repository
so it can be customized. Select a file name and branch to
save it.`)
}}
</p>
<gl-form-group
ref="fileNameFormGroup"
:label="__('File name')"
:state="fileNameState"
:invalid-feedback="fileNameFeedback"
label-size="sm"
label-for="fileName"
>
<gl-form-input id="fileName" ref="fileName" v-model="form.fileName" :required="true" />
</gl-form-group>
<gl-form-group :label="__('Branch')" label-size="sm" label-for="branch">
<gl-form-radio-group
ref="branchOption"
v-model="branchOption"
:checked="$options.radioVals.NEW"
:stacked="true"
:options="branchOptions"
@change="focus"
/>
<gl-form-input
v-show="branchOption === $options.radioVals.NEW"
id="branchName"
ref="branchName"
v-model="branchName"
/>
</gl-form-group>
<gl-form-group
:label="__('Commit message (optional)')"
label-size="sm"
label-for="commitMessage"
>
<gl-form-textarea
id="commitMessage"
ref="commitMessage"
v-model="form.commitMessage"
:placeholder="defaultCommitMsg"
/>
</gl-form-group>
</form>
</template>
......@@ -214,5 +214,29 @@ export const setPanelGroupMetrics = ({ commit }, data) => {
commit(types.SET_PANEL_GROUP_METRICS, data);
};
export const duplicateSystemDashboard = ({ state }, payload) => {
const params = {
dashboard: payload.dashboard,
file_name: payload.fileName,
branch: payload.branch,
commit_message: payload.commitMessage,
};
return axios
.post(state.dashboardsEndpoint, params)
.then(response => response.data)
.then(data => data.dashboard)
.catch(error => {
const { response } = error;
if (response && response.data && response.data.error) {
throw sprintf(s__('Metrics|There was an error creating the dashboard. %{error}'), {
error: response.data.error,
});
} else {
throw s__('Metrics|There was an error creating the dashboard.');
}
});
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -175,6 +175,7 @@ export default {
state.environmentsEndpoint = endpoints.environmentsEndpoint;
state.deploymentsEndpoint = endpoints.deploymentsEndpoint;
state.dashboardEndpoint = endpoints.dashboardEndpoint;
state.dashboardsEndpoint = endpoints.dashboardsEndpoint;
state.currentDashboard = endpoints.currentDashboard;
state.projectPath = endpoints.projectPath;
},
......
......@@ -29,6 +29,7 @@ module EnvironmentsHelper
"empty-no-data-small-svg-path" => image_path('illustrations/chart-empty-state-small.svg'),
"empty-unable-to-connect-svg-path" => image_path('illustrations/monitoring/unable_to_connect.svg'),
"metrics-endpoint" => additional_metrics_project_environment_path(project, environment, format: :json),
"dashboards-endpoint" => project_performance_monitoring_dashboards_path(project, format: :json),
"dashboard-endpoint" => metrics_dashboard_project_environment_path(project, environment, format: :json),
"deployments-endpoint" => project_environment_deployments_path(project, environment, format: :json),
"default-branch" => project.default_branch,
......
---
title: Add ability to duplicate the common metrics dashboard
merge_request: 21929
author:
type: added
......@@ -4675,6 +4675,9 @@ msgstr ""
msgid "Commit message"
msgstr ""
msgid "Commit message (optional)"
msgstr ""
msgid "Commit statistics for %{ref} %{start_time} - %{end_time}"
msgstr ""
......@@ -8016,6 +8019,9 @@ msgstr ""
msgid "File moved"
msgstr ""
msgid "File name"
msgstr ""
msgid "File templates"
msgstr ""
......@@ -11608,6 +11614,9 @@ msgstr ""
msgid "Metrics|Check out the CI/CD documentation on deploying to an environment"
msgstr ""
msgid "Metrics|Create custom dashboard %{fileName}"
msgstr ""
msgid "Metrics|Create metric"
msgstr ""
......@@ -11617,6 +11626,15 @@ msgstr ""
msgid "Metrics|Delete metric?"
msgstr ""
msgid "Metrics|Duplicate"
msgstr ""
msgid "Metrics|Duplicate dashboard"
msgstr ""
msgid "Metrics|Duplicating..."
msgstr ""
msgid "Metrics|Edit metric"
msgstr ""
......@@ -11653,6 +11671,12 @@ msgstr ""
msgid "Metrics|Show last"
msgstr ""
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
msgid "Metrics|There was an error creating the dashboard. %{error}"
msgstr ""
msgid "Metrics|There was an error fetching the environments data, please try again"
msgstr ""
......@@ -11692,6 +11716,9 @@ msgstr ""
msgid "Metrics|Y-axis label"
msgstr ""
msgid "Metrics|You can save a copy of this dashboard to your repository so it can be customized. Select a file name and branch to save it."
msgstr ""
msgid "Metrics|You're about to permanently delete this metric. This cannot be undone."
msgstr ""
......
......@@ -6,6 +6,8 @@ import axios from '~/lib/utils/axios_utils';
import statusCodes from '~/lib/utils/http_status';
import { metricStates } from '~/monitoring/constants';
import Dashboard from '~/monitoring/components/dashboard.vue';
import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
import DateTimePicker from '~/monitoring/components/date_time_picker/date_time_picker.vue';
import GroupEmptyState from '~/monitoring/components/group_empty_state.vue';
import { createStore } from '~/monitoring/stores';
......@@ -465,7 +467,7 @@ describe('Dashboard', () => {
wrapper.vm
.$nextTick()
.then(() => {
const dashboardDropdown = wrapper.find('.js-dashboards-dropdown');
const dashboardDropdown = wrapper.find(DashboardsDropdown);
expect(dashboardDropdown.exists()).toBe(true);
done();
......
import { shallowMount } from '@vue/test-utils';
import { GlDropdownItem, GlModal, GlLoadingIcon, GlAlert } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
import DuplicateDashboardForm from '~/monitoring/components/duplicate_dashboard_form.vue';
import { dashboardGitResponse } from '../mock_data';
const defaultBranch = 'master';
function createComponent(props, opts = {}) {
const storeOpts = {
methods: {
duplicateSystemDashboard: jest.fn(),
},
computed: {
allDashboards: () => dashboardGitResponse,
},
};
return shallowMount(DashboardsDropdown, {
propsData: {
...props,
defaultBranch,
},
sync: false,
...storeOpts,
...opts,
});
}
describe('DashboardsDropdown', () => {
let wrapper;
const findItems = () => wrapper.findAll(GlDropdownItem);
const findItemAt = i => wrapper.findAll(GlDropdownItem).at(i);
describe('when it receives dashboards data', () => {
beforeEach(() => {
wrapper = createComponent();
});
it('displays an item for each dashboard', () => {
expect(wrapper.findAll(GlDropdownItem).length).toEqual(dashboardGitResponse.length);
});
it('displays items with the dashboard display name', () => {
expect(findItemAt(0).text()).toBe(dashboardGitResponse[0].display_name);
expect(findItemAt(1).text()).toBe(dashboardGitResponse[1].display_name);
expect(findItemAt(2).text()).toBe(dashboardGitResponse[2].display_name);
});
});
describe('when a system dashboard is selected', () => {
let duplicateDashboardAction;
let modalDirective;
beforeEach(() => {
modalDirective = jest.fn();
duplicateDashboardAction = jest.fn().mockResolvedValue();
wrapper = createComponent(
{
selectedDashboard: dashboardGitResponse[0],
},
{
directives: {
GlModal: modalDirective,
},
methods: {
// Mock vuex actions
duplicateSystemDashboard: duplicateDashboardAction,
},
},
);
wrapper.vm.$refs.duplicateDashboardModal.hide = jest.fn();
});
it('displays an item for each dashboard plus a "duplicate dashboard" item', () => {
const item = wrapper.findAll({ ref: 'duplicateDashboardItem' });
expect(findItems().length).toEqual(dashboardGitResponse.length + 1);
expect(item.length).toBe(1);
});
describe('modal form', () => {
let okEvent;
const findModal = () => wrapper.find(GlModal);
const findAlert = () => wrapper.find(GlAlert);
beforeEach(() => {
okEvent = {
preventDefault: jest.fn(),
};
});
it('exists and contains a form to duplicate a dashboard', () => {
expect(findModal().exists()).toBe(true);
expect(findModal().contains(DuplicateDashboardForm)).toBe(true);
});
it('saves a new dashboard', done => {
findModal().vm.$emit('ok', okEvent);
waitForPromises()
.then(() => {
expect(okEvent.preventDefault).toHaveBeenCalled();
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.vm.$refs.duplicateDashboardModal.hide).toHaveBeenCalled();
expect(wrapper.emitted().selectDashboard).toBeTruthy();
expect(findAlert().exists()).toBe(false);
done();
})
.catch(done.fail);
});
describe('when a new dashboard is saved succesfully', () => {
const newDashboard = {
can_edit: true,
default: false,
display_name: 'A new dashboard',
system_dashboard: false,
};
const submitForm = formVals => {
duplicateDashboardAction.mockResolvedValueOnce(newDashboard);
findModal()
.find(DuplicateDashboardForm)
.vm.$emit('change', {
dashboard: 'common_metrics.yml',
commitMessage: 'A commit message',
...formVals,
});
findModal().vm.$emit('ok', okEvent);
};
it('to the default branch, redirects to the new dashboard', done => {
submitForm({
branch: defaultBranch,
});
waitForPromises()
.then(() => {
expect(wrapper.emitted().selectDashboard[0][0]).toEqual(newDashboard);
done();
})
.catch(done.fail);
});
it('to a new branch refreshes in the current dashboard', done => {
submitForm({
branch: 'another-branch',
});
waitForPromises()
.then(() => {
expect(wrapper.emitted().selectDashboard[0][0]).toEqual(dashboardGitResponse[0]);
done();
})
.catch(done.fail);
});
});
it('handles error when a new dashboard is not saved', done => {
const errMsg = 'An error occurred';
duplicateDashboardAction.mockRejectedValueOnce(errMsg);
findModal().vm.$emit('ok', okEvent);
waitForPromises()
.then(() => {
expect(okEvent.preventDefault).toHaveBeenCalled();
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe(errMsg);
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.vm.$refs.duplicateDashboardModal.hide).not.toHaveBeenCalled();
done();
})
.catch(done.fail);
});
it('id is correct, as the value of modal directive binding matches modal id', () => {
expect(modalDirective).toHaveBeenCalledTimes(1);
// Binding's second argument contains the modal id
expect(modalDirective.mock.calls[0][1]).toEqual(
expect.objectContaining({
value: findModal().props('modalId'),
}),
);
});
it('updates the form on changes', () => {
const formVals = {
dashboard: 'common_metrics.yml',
commitMessage: 'A commit message',
};
findModal()
.find(DuplicateDashboardForm)
.vm.$emit('change', formVals);
// Binding's second argument contains the modal id
expect(wrapper.vm.form).toEqual(formVals);
});
});
});
describe('when a custom dashboard is selected', () => {
const findModal = () => wrapper.find(GlModal);
beforeEach(() => {
wrapper = createComponent({
selectedDashboard: dashboardGitResponse[1],
});
});
it('displays an item for each dashboard', () => {
const item = wrapper.findAll({ ref: 'duplicateDashboardItem' });
expect(findItems().length).toEqual(dashboardGitResponse.length);
expect(item.length).toBe(0);
});
it('modal form does not exist and contains a form to duplicate a dashboard', () => {
expect(findModal().exists()).toBe(false);
});
});
describe('when a dashboard gets selected by the user', () => {
beforeEach(() => {
wrapper = createComponent();
findItemAt(1).vm.$emit('click');
});
it('emits a "selectDashboard" event', () => {
expect(wrapper.emitted().selectDashboard).toBeTruthy();
});
it('emits a "selectDashboard" event with dashboard information', () => {
expect(wrapper.emitted().selectDashboard[0]).toEqual([dashboardGitResponse[1]]);
});
});
});
import { mount } from '@vue/test-utils';
import DuplicateDashboardForm from '~/monitoring/components/duplicate_dashboard_form.vue';
import { dashboardGitResponse } from '../mock_data';
describe('DuplicateDashboardForm', () => {
let wrapper;
const defaultBranch = 'master';
const findByRef = ref => wrapper.find({ ref });
const setValue = (ref, val) => {
findByRef(ref).setValue(val);
};
const setChecked = value => {
const input = wrapper.find(`.form-check-input[value="${value}"]`);
input.element.checked = true;
input.trigger('click');
input.trigger('change');
};
beforeEach(() => {
// Use `mount` to render native input elements
wrapper = mount(DuplicateDashboardForm, {
propsData: {
dashboard: dashboardGitResponse[0],
defaultBranch,
},
sync: false,
});
});
it('renders correctly', () => {
expect(wrapper.exists()).toEqual(true);
});
it('renders form elements', () => {
expect(findByRef('fileName').exists()).toEqual(true);
expect(findByRef('branchName').exists()).toEqual(true);
expect(findByRef('branchOption').exists()).toEqual(true);
expect(findByRef('commitMessage').exists()).toEqual(true);
});
describe('validates the file name', () => {
const findInvalidFeedback = () => findByRef('fileNameFormGroup').find('.invalid-feedback');
it('when is empty', done => {
setValue('fileName', '');
wrapper.vm.$nextTick(() => {
expect(findByRef('fileNameFormGroup').is('.is-valid')).toBe(true);
expect(findInvalidFeedback().exists()).toBe(false);
done();
});
});
it('when is valid', done => {
setValue('fileName', 'my_dashboard.yml');
wrapper.vm.$nextTick(() => {
expect(findByRef('fileNameFormGroup').is('.is-valid')).toBe(true);
expect(findInvalidFeedback().exists()).toBe(false);
done();
});
});
it('when is not valid', done => {
setValue('fileName', 'my_dashboard.exe');
wrapper.vm.$nextTick(() => {
expect(findByRef('fileNameFormGroup').is('.is-invalid')).toBe(true);
expect(findInvalidFeedback().text()).toBeTruthy();
done();
});
});
});
describe('emits `change` event', () => {
const lastChange = () =>
wrapper.vm.$nextTick().then(() => {
wrapper.find('form').trigger('change');
// Resolves to the last emitted change
const changes = wrapper.emitted().change;
return changes[changes.length - 1][0];
});
it('with the inital form values', () => {
expect(wrapper.emitted().change).toHaveLength(1);
expect(lastChange()).resolves.toEqual({
branch: '',
commitMessage: expect.any(String),
dashboard: dashboardGitResponse[0].path,
fileName: 'common_metrics.yml',
});
});
it('containing an inputted file name', () => {
setValue('fileName', 'my_dashboard.yml');
expect(lastChange()).resolves.toMatchObject({
fileName: 'my_dashboard.yml',
});
});
it('containing a default commit message when no message is set', () => {
setValue('commitMessage', '');
expect(lastChange()).resolves.toMatchObject({
commitMessage: expect.stringContaining('Create custom dashboard'),
});
});
it('containing an inputted commit message', () => {
setValue('commitMessage', 'My commit message');
expect(lastChange()).resolves.toMatchObject({
commitMessage: expect.stringContaining('My commit message'),
});
});
it('containing an inputted branch name', () => {
setValue('branchName', 'a-new-branch');
expect(lastChange()).resolves.toMatchObject({
branch: 'a-new-branch',
});
});
it('when a `default` branch option is set, branch input is invisible and ignored', done => {
setChecked(wrapper.vm.$options.radioVals.DEFAULT);
setValue('branchName', 'a-new-branch');
expect(lastChange()).resolves.toMatchObject({
branch: defaultBranch,
});
wrapper.vm.$nextTick(() => {
expect(findByRef('branchName').isVisible()).toBe(false);
done();
});
});
it('when `new` branch option is chosen, focuses on the branch name input', done => {
setChecked(wrapper.vm.$options.radioVals.NEW);
wrapper.vm
.$nextTick()
.then(() => {
wrapper.find('form').trigger('change');
expect(findByRef('branchName').is(':focus')).toBe(true);
})
.then(done)
.catch(done.fail);
});
});
});
......@@ -15,6 +15,7 @@ export const propsData = {
clustersPath: '/path/to/clusters',
tagsPath: '/path/to/tags',
projectPath: '/path/to/project',
defaultBranch: 'master',
metricsEndpoint: mockApiEndpoint,
deploymentsEndpoint: null,
emptyGettingStartedSvgPath: '/path/to/getting-started.svg',
......
......@@ -522,6 +522,7 @@ export const dashboardGitResponse = [
default: true,
display_name: 'Default',
can_edit: false,
system_dashboard: true,
project_blob_path: null,
path: 'config/prometheus/common_metrics.yml',
},
......@@ -529,6 +530,7 @@ export const dashboardGitResponse = [
default: false,
display_name: 'Custom Dashboard 1',
can_edit: true,
system_dashboard: false,
project_blob_path: `${mockProjectDir}/blob/master/dashboards/.gitlab/dashboards/dashboard_1.yml`,
path: '.gitlab/dashboards/dashboard_1.yml',
},
......@@ -536,6 +538,7 @@ export const dashboardGitResponse = [
default: false,
display_name: 'Custom Dashboard 2',
can_edit: true,
system_dashboard: false,
project_blob_path: `${mockProjectDir}/blob/master/dashboards/.gitlab/dashboards/dashboard_2.yml`,
path: '.gitlab/dashboards/dashboard_2.yml',
},
......
......@@ -18,6 +18,7 @@ import {
fetchPrometheusMetric,
setEndpoints,
setGettingStartedEmptyState,
duplicateSystemDashboard,
} from '~/monitoring/stores/actions';
import storeState from '~/monitoring/stores/state';
import {
......@@ -544,4 +545,85 @@ describe('Monitoring store actions', () => {
});
});
});
describe('duplicateSystemDashboard', () => {
let state;
beforeEach(() => {
state = storeState();
state.dashboardsEndpoint = '/dashboards.json';
});
it('Succesful POST request resolves', done => {
mock.onPost(state.dashboardsEndpoint).reply(statusCodes.CREATED, {
dashboard: dashboardGitResponse[1],
});
testAction(duplicateSystemDashboard, {}, state, [], [])
.then(() => {
expect(mock.history.post).toHaveLength(1);
done();
})
.catch(done.fail);
});
it('Succesful POST request resolves to a dashboard', done => {
const mockCreatedDashboard = dashboardGitResponse[1];
const params = {
dashboard: 'my-dashboard',
fileName: 'file-name.yml',
branch: 'my-new-branch',
commitMessage: 'A new commit message',
};
const expectedPayload = JSON.stringify({
dashboard: 'my-dashboard',
file_name: 'file-name.yml',
branch: 'my-new-branch',
commit_message: 'A new commit message',
});
mock.onPost(state.dashboardsEndpoint).reply(statusCodes.CREATED, {
dashboard: mockCreatedDashboard,
});
testAction(duplicateSystemDashboard, params, state, [], [])
.then(result => {
expect(mock.history.post).toHaveLength(1);
expect(mock.history.post[0].data).toEqual(expectedPayload);
expect(result).toEqual(mockCreatedDashboard);
done();
})
.catch(done.fail);
});
it('Failed POST request throws an error', done => {
mock.onPost(state.dashboardsEndpoint).reply(statusCodes.BAD_REQUEST);
testAction(duplicateSystemDashboard, {}, state, [], []).catch(err => {
expect(mock.history.post).toHaveLength(1);
expect(err).toEqual(expect.any(String));
done();
});
});
it('Failed POST request throws an error with a description', done => {
const backendErrorMsg = 'This file already exists!';
mock.onPost(state.dashboardsEndpoint).reply(statusCodes.BAD_REQUEST, {
error: backendErrorMsg,
});
testAction(duplicateSystemDashboard, {}, state, [], []).catch(err => {
expect(mock.history.post).toHaveLength(1);
expect(err).toEqual(expect.any(String));
expect(err).toEqual(expect.stringContaining(backendErrorMsg));
done();
});
});
});
});
......@@ -22,6 +22,7 @@ const propsData = {
clustersPath: '/path/to/clusters',
tagsPath: '/path/to/tags',
projectPath: '/path/to/project',
defaultBranch: 'master',
metricsEndpoint: mockApiEndpoint,
deploymentsEndpoint: null,
emptyGettingStartedSvgPath: '/path/to/getting-started.svg',
......
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