Commit 6bf4543e authored by Andrei Stoicescu's avatar Andrei Stoicescu

Add actions menu to monitoring dashboard header

 - add menu with "Create new" and "Duplicate dashboard" items
 - add tests
parent d32086eb
<script>
import { GlButton, GlModal, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
import { isSafeURL } from '~/lib/utils/url_utility';
export default {
components: { GlButton, GlModal, GlSprintf },
props: {
modalId: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
validator: isSafeURL,
},
addDashboardDocumentationPath: {
type: String,
required: true,
},
},
methods: {
cancelHandler() {
this.$refs.modal.hide();
},
},
i18n: {
titleText: s__('Metrics|Create your dashboard configuration file'),
mainText: s__(
'Metrics|To create a new dashboard, add a new YAML file to %{codeStart}.gitlab/dashboards%{codeEnd} at the root of this project.',
),
},
};
</script>
<template>
<gl-modal ref="modal" :modal-id="modalId" :title="$options.i18n.titleText">
<p>
<gl-sprintf :message="$options.i18n.mainText">
<template #code="{ content }">
<code>{{ content }}</code>
</template>
</gl-sprintf>
</p>
<template #modal-footer>
<gl-button category="secondary" @click="cancelHandler">{{ s__('Metrics|Cancel') }}</gl-button>
<gl-button
category="secondary"
variant="info"
target="_blank"
:href="addDashboardDocumentationPath"
data-testid="create-dashboard-modal-docs-button"
>
{{ s__('Metrics|View documentation') }}
</gl-button>
<gl-button
variant="success"
data-testid="create-dashboard-modal-repo-button"
:href="projectPath"
>
{{ s__('Metrics|Open repository') }}
</gl-button>
</template>
</gl-modal>
</template>
...@@ -72,6 +72,10 @@ export default { ...@@ -72,6 +72,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
addDashboardDocumentationPath: {
type: String,
required: true,
},
settingsPath: { settingsPath: {
type: String, type: String,
required: true, required: true,
...@@ -395,6 +399,7 @@ export default { ...@@ -395,6 +399,7 @@ export default {
v-if="showHeader" v-if="showHeader"
ref="prometheusGraphsHeader" ref="prometheusGraphsHeader"
class="prometheus-graphs-header d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 border-bottom bg-gray-light" class="prometheus-graphs-header d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 border-bottom bg-gray-light"
:add-dashboard-documentation-path="addDashboardDocumentationPath"
:default-branch="defaultBranch" :default-branch="defaultBranch"
:rearrange-panels-available="rearrangePanelsAvailable" :rearrange-panels-available="rearrangePanelsAvailable"
:custom-metrics-available="customMetricsAvailable" :custom-metrics-available="customMetricsAvailable"
......
...@@ -8,6 +8,9 @@ import { ...@@ -8,6 +8,9 @@ import {
GlDropdownItem, GlDropdownItem,
GlDropdownHeader, GlDropdownHeader,
GlDropdownDivider, GlDropdownDivider,
GlNewDropdown,
GlNewDropdownDivider,
GlNewDropdownItem,
GlModal, GlModal,
GlLoadingIcon, GlLoadingIcon,
GlSearchBoxByType, GlSearchBoxByType,
...@@ -23,6 +26,8 @@ import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_p ...@@ -23,6 +26,8 @@ import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_p
import DashboardsDropdown from './dashboards_dropdown.vue'; import DashboardsDropdown from './dashboards_dropdown.vue';
import RefreshButton from './refresh_button.vue'; import RefreshButton from './refresh_button.vue';
import CreateDashboardModal from './create_dashboard_modal.vue';
import DuplicateDashboardModal from './duplicate_dashboard_modal.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event'; import TrackEventDirective from '~/vue_shared/directives/track_event';
import { getAddMetricTrackingOptions, timeRangeToUrl } from '../utils'; import { getAddMetricTrackingOptions, timeRangeToUrl } from '../utils';
...@@ -39,6 +44,9 @@ export default { ...@@ -39,6 +44,9 @@ export default {
GlDropdownItem, GlDropdownItem,
GlDropdownHeader, GlDropdownHeader,
GlDropdownDivider, GlDropdownDivider,
GlNewDropdown,
GlNewDropdownDivider,
GlNewDropdownItem,
GlSearchBoxByType, GlSearchBoxByType,
GlModal, GlModal,
CustomMetricsFormFields, CustomMetricsFormFields,
...@@ -46,6 +54,8 @@ export default { ...@@ -46,6 +54,8 @@ export default {
DateTimePicker, DateTimePicker,
DashboardsDropdown, DashboardsDropdown,
RefreshButton, RefreshButton,
DuplicateDashboardModal,
CreateDashboardModal,
}, },
directives: { directives: {
GlModal: GlModalDirective, GlModal: GlModalDirective,
...@@ -95,6 +105,10 @@ export default { ...@@ -95,6 +105,10 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
addDashboardDocumentationPath: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -108,8 +122,12 @@ export default { ...@@ -108,8 +122,12 @@ export default {
'isUpdatingStarredValue', 'isUpdatingStarredValue',
'showEmptyState', 'showEmptyState',
'dashboardTimezone', 'dashboardTimezone',
'projectPath',
]), ]),
...mapGetters('monitoringDashboard', ['selectedDashboard', 'filteredEnvironments']), ...mapGetters('monitoringDashboard', ['selectedDashboard', 'filteredEnvironments']),
isSystemDashboard() {
return this.selectedDashboard?.system_dashboard;
},
shouldShowEnvironmentsDropdownNoMatchedMsg() { shouldShowEnvironmentsDropdownNoMatchedMsg() {
return !this.environmentsLoading && this.filteredEnvironments.length === 0; return !this.environmentsLoading && this.filteredEnvironments.length === 0;
}, },
...@@ -129,6 +147,9 @@ export default { ...@@ -129,6 +147,9 @@ export default {
displayUtc() { displayUtc() {
return this.dashboardTimezone === timezones.UTC; return this.dashboardTimezone === timezones.UTC;
}, },
shouldShowActionsMenu() {
return Boolean(this.projectPath);
},
}, },
methods: { methods: {
...mapActions('monitoringDashboard', ['filterEnvironments', 'toggleStarredValue']), ...mapActions('monitoringDashboard', ['filterEnvironments', 'toggleStarredValue']),
...@@ -136,6 +157,7 @@ export default { ...@@ -136,6 +157,7 @@ export default {
const params = { const params = {
dashboard: encodeURIComponent(dashboard.path), dashboard: encodeURIComponent(dashboard.path),
}; };
redirectTo(mergeUrlParams(params, window.location.href)); redirectTo(mergeUrlParams(params, window.location.href));
}, },
debouncedEnvironmentsSearch: debounce(function environmentsSearchOnInput(searchTerm) { debouncedEnvironmentsSearch: debounce(function environmentsSearchOnInput(searchTerm) {
...@@ -162,13 +184,15 @@ export default { ...@@ -162,13 +184,15 @@ export default {
this.$refs.customMetricsForm.submit(); this.$refs.customMetricsForm.submit();
}, },
}, },
addMetric: { modalIds: {
title: s__('Metrics|Add metric'), addMetric: 'addMetric',
modalId: 'add-metric', createDashboard: 'createDashboard',
duplicateDashboard: 'duplicateDashboard',
}, },
i18n: { i18n: {
starDashboard: s__('Metrics|Star dashboard'), starDashboard: s__('Metrics|Star dashboard'),
unstarDashboard: s__('Metrics|Unstar dashboard'), unstarDashboard: s__('Metrics|Unstar dashboard'),
addMetric: s__('Metrics|Add metric'),
}, },
timeRanges, timeRanges,
}; };
...@@ -176,17 +200,20 @@ export default { ...@@ -176,17 +200,20 @@ export default {
<template> <template>
<div ref="prometheusGraphsHeader"> <div ref="prometheusGraphsHeader">
<div class="mb-2 pr-2 d-flex d-sm-block"> <div class="mb-2 mr-2 d-flex d-sm-block">
<dashboards-dropdown <dashboards-dropdown
id="monitor-dashboards-dropdown" id="monitor-dashboards-dropdown"
data-qa-selector="dashboards_filter_dropdown" data-qa-selector="dashboards_filter_dropdown"
class="flex-grow-1" class="flex-grow-1"
toggle-class="dropdown-menu-toggle" toggle-class="dropdown-menu-toggle"
:default-branch="defaultBranch" :default-branch="defaultBranch"
:modal-id="$options.modalIds.duplicateDashboard"
@selectDashboard="selectDashboard" @selectDashboard="selectDashboard"
/> />
</div> </div>
<span aria-hidden="true" class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block"></span>
<div class="mb-2 pr-2 d-flex d-sm-block"> <div class="mb-2 pr-2 d-flex d-sm-block">
<gl-dropdown <gl-dropdown
id="monitor-environments-dropdown" id="monitor-environments-dropdown"
...@@ -290,17 +317,17 @@ export default { ...@@ -290,17 +317,17 @@ export default {
<div v-if="addingMetricsAvailable" class="mb-2 mr-2 d-flex d-sm-block"> <div v-if="addingMetricsAvailable" class="mb-2 mr-2 d-flex d-sm-block">
<gl-deprecated-button <gl-deprecated-button
ref="addMetricBtn" ref="addMetricBtn"
v-gl-modal="$options.addMetric.modalId" v-gl-modal="$options.modalIds.addMetric"
variant="outline-success" variant="outline-success"
data-qa-selector="add_metric_button" data-qa-selector="add_metric_button"
class="flex-grow-1" class="flex-grow-1"
> >
{{ $options.addMetric.title }} {{ $options.i18n.addMetric }}
</gl-deprecated-button> </gl-deprecated-button>
<gl-modal <gl-modal
ref="addMetricModal" ref="addMetricModal"
:modal-id="$options.addMetric.modalId" :modal-id="$options.modalIds.addMetric"
:title="$options.addMetric.title" :title="$options.i18n.addMetric"
> >
<form ref="customMetricsForm" :action="customMetricsPath" method="post"> <form ref="customMetricsForm" :action="customMetricsPath" method="post">
<custom-metrics-form-fields <custom-metrics-form-fields
...@@ -353,6 +380,50 @@ export default { ...@@ -353,6 +380,50 @@ export default {
{{ __('View full dashboard') }} <icon name="external-link" /> {{ __('View full dashboard') }} <icon name="external-link" />
</gl-deprecated-button> </gl-deprecated-button>
</div> </div>
<template v-if="shouldShowActionsMenu">
<span aria-hidden="true" class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block"></span>
<div class="gl-mb-3 gl-mr-3 d-flex d-sm-block">
<gl-new-dropdown
v-gl-tooltip
right
class="gl-flex-grow-1"
data-testid="actions-menu"
:title="s__('Metrics|Create dashboard')"
:icon="'plus-square'"
>
<gl-new-dropdown-item
v-gl-modal="$options.modalIds.createDashboard"
data-testid="action-create-dashboard"
>{{ s__('Metrics|Create new dashboard') }}</gl-new-dropdown-item
>
<create-dashboard-modal
data-testid="create-dashboard-modal"
:add-dashboard-documentation-path="addDashboardDocumentationPath"
:modal-id="$options.modalIds.createDashboard"
:project-path="projectPath"
/>
<template v-if="isSystemDashboard">
<gl-new-dropdown-divider />
<gl-new-dropdown-item
ref="duplicateDashboardItem"
v-gl-modal="$options.modalIds.duplicateDashboard"
data-testid="action-duplicate-dashboard"
>
{{ s__('Metrics|Duplicate current dashboard') }}
</gl-new-dropdown-item>
</template>
</gl-new-dropdown>
</div>
</template>
</div> </div>
<duplicate-dashboard-modal
:default-branch="defaultBranch"
:modal-id="$options.modalIds.duplicateDashboard"
@dashboardDuplicated="selectDashboard"
/>
</div> </div>
</template> </template>
<script> <script>
import { mapState, mapActions, mapGetters } from 'vuex'; import { mapState, mapActions, mapGetters } from 'vuex';
import { import {
GlAlert,
GlIcon, GlIcon,
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlDropdownHeader, GlDropdownHeader,
GlDropdownDivider, GlDropdownDivider,
GlSearchBoxByType, GlSearchBoxByType,
GlModal,
GlLoadingIcon,
GlModalDirective, GlModalDirective,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { s__ } from '~/locale';
import DuplicateDashboardForm from './duplicate_dashboard_form.vue';
const events = { const events = {
selectDashboard: 'selectDashboard', selectDashboard: 'selectDashboard',
...@@ -21,16 +16,12 @@ const events = { ...@@ -21,16 +16,12 @@ const events = {
export default { export default {
components: { components: {
GlAlert,
GlIcon, GlIcon,
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlDropdownHeader, GlDropdownHeader,
GlDropdownDivider, GlDropdownDivider,
GlSearchBoxByType, GlSearchBoxByType,
GlModal,
GlLoadingIcon,
DuplicateDashboardForm,
}, },
directives: { directives: {
GlModal: GlModalDirective, GlModal: GlModalDirective,
...@@ -40,12 +31,13 @@ export default { ...@@ -40,12 +31,13 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
modalId: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
alert: null,
loading: false,
form: {},
searchTerm: '', searchTerm: '',
}; };
}, },
...@@ -76,10 +68,6 @@ export default { ...@@ -76,10 +68,6 @@ export default {
nonStarredDashboards() { nonStarredDashboards() {
return this.filteredDashboards.filter(({ starred }) => !starred); return this.filteredDashboards.filter(({ starred }) => !starred);
}, },
okButtonText() {
return this.loading ? s__('Metrics|Duplicating...') : s__('Metrics|Duplicate');
},
}, },
methods: { methods: {
...mapActions('monitoringDashboard', ['duplicateSystemDashboard']), ...mapActions('monitoringDashboard', ['duplicateSystemDashboard']),
...@@ -89,37 +77,6 @@ export default { ...@@ -89,37 +77,6 @@ export default {
selectDashboard(dashboard) { selectDashboard(dashboard) {
this.$emit(events.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> </script>
...@@ -178,32 +135,14 @@ export default { ...@@ -178,32 +135,14 @@ export default {
{{ __('No matching results') }} {{ __('No matching results') }}
</div> </div>
<!--
This Duplicate Dashboard item will be removed from the dashboards dropdown
in https://gitlab.com/gitlab-org/gitlab/-/issues/223223
-->
<template v-if="isSystemDashboard"> <template v-if="isSystemDashboard">
<gl-dropdown-divider /> <gl-dropdown-divider />
<gl-modal <gl-dropdown-item v-gl-modal="modalId" data-testid="duplicateDashboardItem">
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" />
{{ okButtonText }}
</template>
</gl-modal>
<gl-dropdown-item ref="duplicateDashboardItem" v-gl-modal="'duplicateDashboardModal'">
{{ s__('Metrics|Duplicate dashboard') }} {{ s__('Metrics|Duplicate dashboard') }}
</gl-dropdown-item> </gl-dropdown-item>
</template> </template>
......
<script>
import { mapActions, mapGetters } from 'vuex';
import { GlAlert, GlLoadingIcon, GlModal } from '@gitlab/ui';
import { s__ } from '~/locale';
import DuplicateDashboardForm from './duplicate_dashboard_form.vue';
const events = {
dashboardDuplicated: 'dashboardDuplicated',
};
export default {
components: { GlAlert, GlLoadingIcon, GlModal, DuplicateDashboardForm },
props: {
defaultBranch: {
type: String,
required: true,
},
modalId: {
type: String,
required: true,
},
},
data() {
return {
alert: null,
loading: false,
form: {},
};
},
computed: {
...mapGetters('monitoringDashboard', ['selectedDashboard']),
okButtonText() {
return this.loading ? s__('Metrics|Duplicating...') : s__('Metrics|Duplicate');
},
},
methods: {
...mapActions('monitoringDashboard', ['duplicateSystemDashboard']),
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.dashboardDuplicated, dashboard);
})
.catch(error => {
this.loading = false;
this.alert = error;
});
},
hide() {
this.alert = null;
},
formChange(form) {
this.form = form;
},
},
};
</script>
<template>
<gl-modal
ref="duplicateDashboardModal"
:modal-id="modalId"
: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" />
{{ okButtonText }}
</template>
</gl-modal>
</template>
...@@ -92,6 +92,7 @@ module EnvironmentsHelper ...@@ -92,6 +92,7 @@ module EnvironmentsHelper
def static_metrics_data def static_metrics_data
{ {
'documentation-path' => help_page_path('administration/monitoring/prometheus/index.md'), 'documentation-path' => help_page_path('administration/monitoring/prometheus/index.md'),
'add-dashboard-documentation-path' => help_page_path('user/project/integrations/prometheus.md', anchor: 'adding-a-new-dashboard-to-your-project'),
'empty-getting-started-svg-path' => image_path('illustrations/monitoring/getting_started.svg'), 'empty-getting-started-svg-path' => image_path('illustrations/monitoring/getting_started.svg'),
'empty-loading-svg-path' => image_path('illustrations/monitoring/loading.svg'), 'empty-loading-svg-path' => image_path('illustrations/monitoring/loading.svg'),
'empty-no-data-svg-path' => image_path('illustrations/monitoring/no_data.svg'), 'empty-no-data-svg-path' => image_path('illustrations/monitoring/no_data.svg'),
......
---
title: Add metrics settings menu to dashboard header
merge_request: 35028
author:
type: added
...@@ -14504,15 +14504,27 @@ msgstr "" ...@@ -14504,15 +14504,27 @@ msgstr ""
msgid "Metrics|Avg" msgid "Metrics|Avg"
msgstr "" msgstr ""
msgid "Metrics|Cancel"
msgstr ""
msgid "Metrics|Check out the CI/CD documentation on deploying to an environment" msgid "Metrics|Check out the CI/CD documentation on deploying to an environment"
msgstr "" msgstr ""
msgid "Metrics|Create custom dashboard %{fileName}" msgid "Metrics|Create custom dashboard %{fileName}"
msgstr "" msgstr ""
msgid "Metrics|Create dashboard"
msgstr ""
msgid "Metrics|Create metric" msgid "Metrics|Create metric"
msgstr "" msgstr ""
msgid "Metrics|Create new dashboard"
msgstr ""
msgid "Metrics|Create your dashboard configuration file"
msgstr ""
msgid "Metrics|Current" msgid "Metrics|Current"
msgstr "" msgstr ""
...@@ -14525,6 +14537,9 @@ msgstr "" ...@@ -14525,6 +14537,9 @@ msgstr ""
msgid "Metrics|Duplicate" msgid "Metrics|Duplicate"
msgstr "" msgstr ""
msgid "Metrics|Duplicate current dashboard"
msgstr ""
msgid "Metrics|Duplicate dashboard" msgid "Metrics|Duplicate dashboard"
msgstr "" msgstr ""
...@@ -14575,6 +14590,9 @@ msgstr "" ...@@ -14575,6 +14590,9 @@ msgstr ""
msgid "Metrics|New metric" msgid "Metrics|New metric"
msgstr "" msgstr ""
msgid "Metrics|Open repository"
msgstr ""
msgid "Metrics|PromQL query is valid" msgid "Metrics|PromQL query is valid"
msgstr "" msgstr ""
...@@ -14629,6 +14647,9 @@ msgstr "" ...@@ -14629,6 +14647,9 @@ msgstr ""
msgid "Metrics|There was an error while retrieving metrics. %{message}" msgid "Metrics|There was an error while retrieving metrics. %{message}"
msgstr "" msgstr ""
msgid "Metrics|To create a new dashboard, add a new YAML file to %{codeStart}.gitlab/dashboards%{codeEnd} at the root of this project."
msgstr ""
msgid "Metrics|Unexpected deployment data response from prometheus endpoint" msgid "Metrics|Unexpected deployment data response from prometheus endpoint"
msgstr "" msgstr ""
...@@ -14650,6 +14671,9 @@ msgstr "" ...@@ -14650,6 +14671,9 @@ msgstr ""
msgid "Metrics|Values" msgid "Metrics|Values"
msgstr "" msgstr ""
msgid "Metrics|View documentation"
msgstr ""
msgid "Metrics|View logs" msgid "Metrics|View logs"
msgstr "" msgstr ""
......
...@@ -13,17 +13,23 @@ exports[`Dashboard template matches the default snapshot 1`] = ` ...@@ -13,17 +13,23 @@ exports[`Dashboard template matches the default snapshot 1`] = `
class="prometheus-graphs-header d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 border-bottom bg-gray-light" class="prometheus-graphs-header d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 border-bottom bg-gray-light"
> >
<div <div
class="mb-2 pr-2 d-flex d-sm-block" class="mb-2 mr-2 d-flex d-sm-block"
> >
<dashboards-dropdown-stub <dashboards-dropdown-stub
class="flex-grow-1" class="flex-grow-1"
data-qa-selector="dashboards_filter_dropdown" data-qa-selector="dashboards_filter_dropdown"
defaultbranch="master" defaultbranch="master"
id="monitor-dashboards-dropdown" id="monitor-dashboards-dropdown"
modalid="duplicateDashboard"
toggle-class="dropdown-menu-toggle" toggle-class="dropdown-menu-toggle"
/> />
</div> </div>
<span
aria-hidden="true"
class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block"
/>
<div <div
class="mb-2 pr-2 d-flex d-sm-block" class="mb-2 pr-2 d-flex d-sm-block"
> >
...@@ -121,7 +127,14 @@ exports[`Dashboard template matches the default snapshot 1`] = ` ...@@ -121,7 +127,14 @@ exports[`Dashboard template matches the default snapshot 1`] = `
<!----> <!---->
<!----> <!---->
<!---->
</div> </div>
<duplicate-dashboard-modal-stub
defaultbranch="master"
modalid="duplicateDashboard"
/>
</div> </div>
<!----> <!---->
......
import { shallowMount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
import CreateDashboardModal from '~/monitoring/components/create_dashboard_modal.vue';
describe('Create dashboard modal', () => {
let wrapper;
const defaultProps = {
modalId: 'id',
projectPath: 'https://localhost/',
addDashboardDocumentationPath: 'https://link/to/docs',
};
const findDocsButton = () => wrapper.find('[data-testid="create-dashboard-modal-docs-button"]');
const findRepoButton = () => wrapper.find('[data-testid="create-dashboard-modal-repo-button"]');
const createWrapper = (props = {}, options = {}) => {
wrapper = shallowMount(CreateDashboardModal, {
propsData: { ...defaultProps, ...props },
stubs: {
GlModal,
},
...options,
});
};
beforeEach(() => {
createWrapper();
});
afterEach(() => {
wrapper.destroy();
});
it('has button that links to the project url', () => {
findRepoButton().trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(findRepoButton().exists()).toBe(true);
expect(findRepoButton().attributes('href')).toBe(defaultProps.projectPath);
});
});
it('has button that links to the docs', () => {
expect(findDocsButton().exists()).toBe(true);
expect(findDocsButton().attributes('href')).toBe(defaultProps.addDashboardDocumentationPath);
});
});
import { shallowMount } from '@vue/test-utils';
import { createStore } from '~/monitoring/stores';
import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
import DuplicateDashboardModal from '~/monitoring/components/duplicate_dashboard_modal.vue';
import CreateDashboardModal from '~/monitoring/components/create_dashboard_modal.vue';
import { setupAllDashboards } from '../store_utils';
import { dashboardGitResponse, dashboardHeaderProps } from '../mock_data';
import { redirectTo, mergeUrlParams } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
queryToObject: jest.fn(),
mergeUrlParams: jest.requireActual('~/lib/utils/url_utility').mergeUrlParams,
}));
describe('Dashboard header', () => {
let store;
let wrapper;
const findActionsMenu = () => wrapper.find('[data-testid="actions-menu"]');
const findCreateDashboardMenuItem = () =>
findActionsMenu().find('[data-testid="action-create-dashboard"]');
const findCreateDashboardDuplicateItem = () =>
findActionsMenu().find('[data-testid="action-duplicate-dashboard"]');
const findDuplicateDashboardModal = () => wrapper.find(DuplicateDashboardModal);
const findCreateDashboardModal = () => wrapper.find('[data-testid="create-dashboard-modal"]');
const createShallowWrapper = (props = {}, options = {}) => {
wrapper = shallowMount(DashboardHeader, {
propsData: { ...dashboardHeaderProps, ...props },
store,
...options,
});
};
beforeEach(() => {
store = createStore();
});
afterEach(() => {
wrapper.destroy();
});
describe('when a dashboard has been duplicated in the duplicate dashboard modal', () => {
/**
* The duplicate dashboard modal gets called both by a menu item from the
* dashboards dropdown and by an item from the actions menu.
*
* This spec is context agnostic, so it addresses all cases where the
* duplicate dashboard modal gets called.
*/
it('redirects to the newly created dashboard', () => {
delete window.location;
window.location = new URL('https://localhost');
const newDashboard = dashboardGitResponse[1];
const params = {
dashboard: encodeURIComponent(newDashboard.path),
};
const newDashboardUrl = mergeUrlParams(params, window.location.href);
createShallowWrapper();
findDuplicateDashboardModal().vm.$emit('dashboardDuplicated', newDashboard);
return wrapper.vm.$nextTick().then(() => {
expect(redirectTo).toHaveBeenCalled();
expect(redirectTo).toHaveBeenCalledWith(newDashboardUrl);
});
});
});
describe('actions menu', () => {
beforeEach(() => {
store.state.monitoringDashboard.projectPath = '';
createShallowWrapper();
});
it('is rendered if projectPath is set in store', () => {
store.state.monitoringDashboard.projectPath = 'https://path/to/project';
return wrapper.vm.$nextTick().then(() => {
expect(findActionsMenu().exists()).toBe(true);
});
});
it('is not rendered if projectPath is not set in store', () => {
expect(findActionsMenu().exists()).toBe(false);
});
it('contains a modal', () => {
store.state.monitoringDashboard.projectPath = 'https://path/to/project';
return wrapper.vm.$nextTick().then(() => {
expect(findActionsMenu().contains(CreateDashboardModal)).toBe(true);
});
});
describe('when the selected dashboard is the system dashboard', () => {
it('contains a "Create New" menu item and a "Duplicate Dashboard" menu item', () => {
store.state.monitoringDashboard.projectPath = 'https://path/to/project';
setupAllDashboards(store);
return wrapper.vm.$nextTick().then(() => {
expect(findCreateDashboardMenuItem().exists()).toBe(true);
expect(findCreateDashboardDuplicateItem().exists()).toBe(true);
});
});
});
describe('when the selected dashboard is not the system dashboard', () => {
it('contains a "Create New" menu item and no "Duplicate Dashboard" menu item', () => {
store.state.monitoringDashboard.projectPath = 'https://path/to/project';
return wrapper.vm.$nextTick().then(() => {
expect(findCreateDashboardMenuItem().exists()).toBe(true);
expect(findCreateDashboardDuplicateItem().exists()).toBe(false);
});
});
});
});
describe('actions menu modals', () => {
const url = 'https://path/to/project';
beforeEach(() => {
store.state.monitoringDashboard.projectPath = url;
setupAllDashboards(store);
createShallowWrapper();
});
it('Clicking on "Create New" opens up a modal', () => {
const modalId = 'createDashboard';
const modalTrigger = findCreateDashboardMenuItem();
const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
modalTrigger.trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(rootEmit.mock.calls[0]).toContainEqual(modalId);
});
});
it('"Create new dashboard" modal contains correct buttons', () => {
expect(findCreateDashboardModal().props('projectPath')).toBe(url);
});
it('"Duplicate Dashboard" opens up a modal', () => {
const modalId = 'duplicateDashboard';
const modalTrigger = findCreateDashboardDuplicateItem();
const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
modalTrigger.trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(rootEmit.mock.calls[0]).toContainEqual(modalId);
});
});
});
});
...@@ -1237,7 +1237,7 @@ describe('Dashboard', () => { ...@@ -1237,7 +1237,7 @@ describe('Dashboard', () => {
it('uses modal for custom metrics form', () => { it('uses modal for custom metrics form', () => {
expect(wrapper.find(GlModal).exists()).toBe(true); expect(wrapper.find(GlModal).exists()).toBe(true);
expect(wrapper.find(GlModal).attributes().modalid).toBe('add-metric'); expect(wrapper.find(GlModal).attributes().modalid).toBe('addMetric');
}); });
it('adding new metric is tracked', done => { it('adding new metric is tracked', done => {
const submitButton = wrapper const submitButton = wrapper
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlDropdownItem, GlModal, GlLoadingIcon, GlAlert, GlIcon } from '@gitlab/ui'; import { GlDropdownItem, GlIcon } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue'; import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
import DuplicateDashboardForm from '~/monitoring/components/duplicate_dashboard_form.vue';
import { dashboardGitResponse } from '../mock_data'; import { dashboardGitResponse } from '../mock_data';
const defaultBranch = 'master'; const defaultBranch = 'master';
const modalId = 'duplicateDashboardModalId';
const starredDashboards = dashboardGitResponse.filter(({ starred }) => starred); const starredDashboards = dashboardGitResponse.filter(({ starred }) => starred);
const notStarredDashboards = dashboardGitResponse.filter(({ starred }) => !starred); const notStarredDashboards = dashboardGitResponse.filter(({ starred }) => !starred);
...@@ -32,6 +30,7 @@ describe('DashboardsDropdown', () => { ...@@ -32,6 +30,7 @@ describe('DashboardsDropdown', () => {
propsData: { propsData: {
...props, ...props,
defaultBranch, defaultBranch,
modalId,
}, },
sync: false, sync: false,
...storeOpts, ...storeOpts,
...@@ -82,7 +81,7 @@ describe('DashboardsDropdown', () => { ...@@ -82,7 +81,7 @@ describe('DashboardsDropdown', () => {
const searchTerm = 'Default'; const searchTerm = 'Default';
setSearchTerm(searchTerm); setSearchTerm(searchTerm);
return wrapper.vm.$nextTick(() => { return wrapper.vm.$nextTick().then(() => {
expect(findItems()).toHaveLength(1); expect(findItems()).toHaveLength(1);
}); });
}); });
...@@ -91,7 +90,7 @@ describe('DashboardsDropdown', () => { ...@@ -91,7 +90,7 @@ describe('DashboardsDropdown', () => {
const searchTerm = 'does-not-exist'; const searchTerm = 'does-not-exist';
setSearchTerm(searchTerm); setSearchTerm(searchTerm);
return wrapper.vm.$nextTick(() => { return wrapper.vm.$nextTick().then(() => {
expect(findNoItemsMsg().isVisible()).toBe(true); expect(findNoItemsMsg().isVisible()).toBe(true);
}); });
}); });
...@@ -151,7 +150,7 @@ describe('DashboardsDropdown', () => { ...@@ -151,7 +150,7 @@ describe('DashboardsDropdown', () => {
}); });
}); });
describe('when a system dashboard is selected', () => { describe('when the selected dashboard can be duplicated', () => {
let duplicateDashboardAction; let duplicateDashboardAction;
let modalDirective; let modalDirective;
...@@ -172,151 +171,53 @@ describe('DashboardsDropdown', () => { ...@@ -172,151 +171,53 @@ describe('DashboardsDropdown', () => {
}, },
}, },
); );
wrapper.vm.$refs.duplicateDashboardModal.hide = jest.fn();
}); });
it('displays an item for each dashboard plus a "duplicate dashboard" item', () => { it('displays a dropdown item for each dashboard', () => {
const item = wrapper.findAll({ ref: 'duplicateDashboardItem' });
expect(findItems().length).toEqual(dashboardGitResponse.length + 1); expect(findItems().length).toEqual(dashboardGitResponse.length + 1);
expect(item.length).toBe(1);
}); });
describe('modal form', () => { it('displays one "duplicate dashboard" dropdown item with a directive attached', () => {
let okEvent; const item = wrapper.findAll('[data-testid="duplicateDashboardItem"]');
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', () => {
findModal().vm.$emit('ok', okEvent);
return 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);
});
});
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', () => {
submitForm({
branch: defaultBranch,
});
return waitForPromises().then(() => {
expect(wrapper.emitted().selectDashboard[0][0]).toEqual(newDashboard);
});
});
it('to a new branch refreshes in the current dashboard', () => {
submitForm({
branch: 'another-branch',
});
return waitForPromises().then(() => {
expect(wrapper.emitted().selectDashboard[0][0]).toEqual(dashboardGitResponse[0]);
});
});
});
it('handles error when a new dashboard is not saved', () => {
const errMsg = 'An error occurred';
duplicateDashboardAction.mockRejectedValueOnce(errMsg); expect(item.length).toBe(1);
findModal().vm.$emit('ok', okEvent); });
return waitForPromises().then(() => {
expect(okEvent.preventDefault).toHaveBeenCalled();
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe(errMsg);
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); it('"duplicate dashboard" dropdown item directive works', () => {
expect(wrapper.vm.$refs.duplicateDashboardModal.hide).not.toHaveBeenCalled(); const item = wrapper.find('[data-testid="duplicateDashboardItem"]');
});
});
it('id is correct, as the value of modal directive binding matches modal id', () => { item.trigger('click');
expect(modalDirective).toHaveBeenCalledTimes(1);
// Binding's second argument contains the modal id return wrapper.vm.$nextTick().then(() => {
expect(modalDirective.mock.calls[0][1]).toEqual( expect(modalDirective).toHaveBeenCalled();
expect.objectContaining({
value: findModal().props('modalId'),
}),
);
}); });
});
it('updates the form on changes', () => { it('id is correct, as the value of modal directive binding matches modal id', () => {
const formVals = { expect(modalDirective).toHaveBeenCalledTimes(1);
dashboard: 'common_metrics.yml',
commitMessage: 'A commit message',
};
findModal()
.find(DuplicateDashboardForm)
.vm.$emit('change', formVals);
// Binding's second argument contains the modal id // Binding's second argument contains the modal id
expect(wrapper.vm.form).toEqual(formVals); expect(modalDirective.mock.calls[0][1]).toEqual(
}); expect.objectContaining({
value: modalId,
}),
);
}); });
}); });
describe('when a custom dashboard is selected', () => { describe('when the selected dashboard can not be duplicated', () => {
const findModal = () => wrapper.find(GlModal);
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ [, mockSelectedDashboard] = dashboardGitResponse;
selectedDashboard: dashboardGitResponse[1],
}); wrapper = createComponent();
}); });
it('displays an item for each dashboard', () => { it('displays a dropdown list item for each dashboard, but no list item for "duplicate dashboard"', () => {
const item = wrapper.findAll({ ref: 'duplicateDashboardItem' }); const item = wrapper.findAll('[data-testid="duplicateDashboardItem"]');
expect(findItems()).toHaveLength(dashboardGitResponse.length); expect(findItems()).toHaveLength(dashboardGitResponse.length);
expect(item.length).toBe(0); 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', () => { describe('when a dashboard gets selected by the user', () => {
......
import { shallowMount } from '@vue/test-utils';
import { GlAlert, GlLoadingIcon, GlModal } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import DuplicateDashboardModal from '~/monitoring/components/duplicate_dashboard_modal.vue';
import DuplicateDashboardForm from '~/monitoring/components/duplicate_dashboard_form.vue';
import { dashboardGitResponse } from '../mock_data';
describe('duplicate dashboard modal', () => {
let wrapper;
let mockDashboards;
let mockSelectedDashboard;
let duplicateDashboardAction;
let okEvent;
function createComponent(opts = {}) {
const storeOpts = {
methods: {
duplicateSystemDashboard: jest.fn(),
},
computed: {
allDashboards: () => mockDashboards,
selectedDashboard: () => mockSelectedDashboard,
},
};
return shallowMount(DuplicateDashboardModal, {
propsData: {
defaultBranch: 'master',
modalId: 'id',
},
sync: false,
...storeOpts,
...opts,
});
}
const findAlert = () => wrapper.find(GlAlert);
const findModal = () => wrapper.find(GlModal);
const findDuplicateDashboardForm = () => wrapper.find(DuplicateDashboardForm);
beforeEach(() => {
mockDashboards = dashboardGitResponse;
[mockSelectedDashboard] = dashboardGitResponse;
duplicateDashboardAction = jest.fn().mockResolvedValue();
okEvent = {
preventDefault: jest.fn(),
};
wrapper = createComponent({
methods: {
// Mock vuex actions
duplicateSystemDashboard: duplicateDashboardAction,
},
});
wrapper.vm.$refs.duplicateDashboardModal.hide = jest.fn();
});
it('contains a form to duplicate a dashboard', () => {
expect(findDuplicateDashboardForm().exists()).toBe(true);
});
it('saves a new dashboard', () => {
findModal().vm.$emit('ok', okEvent);
return waitForPromises().then(() => {
expect(okEvent.preventDefault).toHaveBeenCalled();
expect(wrapper.emitted().dashboardDuplicated).toBeTruthy();
expect(wrapper.emitted().dashboardDuplicated[0]).toEqual([dashboardGitResponse[0]]);
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.vm.$refs.duplicateDashboardModal.hide).toHaveBeenCalled();
expect(findAlert().exists()).toBe(false);
});
});
it('handles error when a new dashboard is not saved', () => {
const errMsg = 'An error occurred';
duplicateDashboardAction.mockRejectedValueOnce(errMsg);
findModal().vm.$emit('ok', okEvent);
return 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();
});
});
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);
});
});
...@@ -805,3 +805,13 @@ export const storeVariables = [ ...@@ -805,3 +805,13 @@ export const storeVariables = [
...storeCustomVariables, ...storeCustomVariables,
...storeMetricLabelValuesVariables, ...storeMetricLabelValuesVariables,
]; ];
export const dashboardHeaderProps = {
defaultBranch: 'master',
addDashboardDocumentationPath: 'https://path/to/docs',
isRearrangingPanels: false,
selectedTimeRange: {
start: '2020-01-01T00:00:00.000Z',
end: '2020-01-01T01:00:00.000Z',
},
};
...@@ -386,6 +386,7 @@ describe('Monitoring mutations', () => { ...@@ -386,6 +386,7 @@ describe('Monitoring mutations', () => {
}); });
}); });
}); });
describe('SET_ALL_DASHBOARDS', () => { describe('SET_ALL_DASHBOARDS', () => {
it('stores `undefined` dashboards as an empty array', () => { it('stores `undefined` dashboards as an empty array', () => {
mutations[types.SET_ALL_DASHBOARDS](stateCopy, undefined); mutations[types.SET_ALL_DASHBOARDS](stateCopy, undefined);
......
...@@ -23,6 +23,7 @@ RSpec.describe EnvironmentsHelper do ...@@ -23,6 +23,7 @@ RSpec.describe EnvironmentsHelper do
'metrics-dashboard-base-path' => environment_metrics_path(environment), 'metrics-dashboard-base-path' => environment_metrics_path(environment),
'current-environment-name' => environment.name, 'current-environment-name' => environment.name,
'documentation-path' => help_page_path('administration/monitoring/prometheus/index.md'), 'documentation-path' => help_page_path('administration/monitoring/prometheus/index.md'),
'add-dashboard-documentation-path' => help_page_path('user/project/integrations/prometheus.md', anchor: 'adding-a-new-dashboard-to-your-project'),
'empty-getting-started-svg-path' => match_asset_path('/assets/illustrations/monitoring/getting_started.svg'), 'empty-getting-started-svg-path' => match_asset_path('/assets/illustrations/monitoring/getting_started.svg'),
'empty-loading-svg-path' => match_asset_path('/assets/illustrations/monitoring/loading.svg'), 'empty-loading-svg-path' => match_asset_path('/assets/illustrations/monitoring/loading.svg'),
'empty-no-data-svg-path' => match_asset_path('/assets/illustrations/monitoring/no_data.svg'), 'empty-no-data-svg-path' => match_asset_path('/assets/illustrations/monitoring/no_data.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