Commit 21c48641 authored by Andrei Stoicescu's avatar Andrei Stoicescu

Modify actions menu in monitoring dashboard header

 - add new actions
 - change UI according to new designs
parent b1e4c446
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import {
GlDeprecatedButton,
GlNewDropdown,
GlNewDropdownDivider,
GlNewDropdownItem,
GlModal,
GlIcon,
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
import DuplicateDashboardModal from './duplicate_dashboard_modal.vue';
import CreateDashboardModal from './create_dashboard_modal.vue';
import { s__ } from '~/locale';
import invalidUrl from '~/lib/utils/invalid_url';
import { redirectTo } from '~/lib/utils/url_utility';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import { getAddMetricTrackingOptions } from '../utils';
export default {
components: {
GlDeprecatedButton,
GlNewDropdown,
GlNewDropdownDivider,
GlNewDropdownItem,
GlModal,
GlIcon,
DuplicateDashboardModal,
CreateDashboardModal,
CustomMetricsFormFields,
},
directives: {
GlModal: GlModalDirective,
GlTooltip: GlTooltipDirective,
TrackEvent: TrackEventDirective,
},
props: {
addingMetricsAvailable: {
type: Boolean,
required: false,
default: false,
},
customMetricsPath: {
type: String,
required: false,
default: invalidUrl,
},
validateQueryPath: {
type: String,
required: false,
default: invalidUrl,
},
defaultBranch: {
type: String,
required: true,
},
},
data() {
return { customMetricsFormIsValid: null };
},
computed: {
...mapState('monitoringDashboard', [
'projectPath',
'isUpdatingStarredValue',
'addDashboardDocumentationPath',
]),
...mapGetters('monitoringDashboard', ['selectedDashboard']),
isOutOfTheBoxDashboard() {
return this.selectedDashboard?.out_of_the_box_dashboard;
},
isMenuItemEnabled() {
return {
createDashboard: Boolean(this.projectPath),
editDashboard: this.selectedDashboard?.can_edit,
};
},
isMenuItemShown() {
return {
duplicateDashboard: this.isOutOfTheBoxDashboard,
};
},
},
methods: {
...mapActions('monitoringDashboard', ['toggleStarredValue']),
setFormValidity(isValid) {
this.customMetricsFormIsValid = isValid;
},
hideAddMetricModal() {
this.$refs.addMetricModal.hide();
},
getAddMetricTrackingOptions,
submitCustomMetricsForm() {
this.$refs.customMetricsForm.submit();
},
selectDashboard(dashboard) {
// Once the sidebar See metrics link is updated to the new URL,
// this sort of hardcoding will not be necessary.
// https://gitlab.com/gitlab-org/gitlab/-/issues/229277
const baseURL = `${this.projectPath}/-/metrics`;
const dashboardPath = encodeURIComponent(
dashboard.out_of_the_box_dashboard ? dashboard.path : dashboard.display_name,
);
redirectTo(`${baseURL}/${dashboardPath}`);
},
},
modalIds: {
addMetric: 'addMetric',
createDashboard: 'createDashboard',
duplicateDashboard: 'duplicateDashboard',
},
i18n: {
actionsMenu: s__('Metrics|More actions'),
duplicateDashboard: s__('Metrics|Duplicate current dashboard'),
starDashboard: s__('Metrics|Star dashboard'),
unstarDashboard: s__('Metrics|Unstar dashboard'),
addMetric: s__('Metrics|Add metric'),
editDashboardInfo: s__('Metrics|Duplicate this dashboard to edit dashboard YAML'),
editDashboard: s__('Metrics|Edit dashboard YAML'),
createDashboard: s__('Metrics|Create new dashboard'),
},
};
</script>
<template>
<gl-new-dropdown
v-gl-tooltip
data-testid="actions-menu"
data-qa-selector="actions_menu_dropdown"
right
no-caret
toggle-class="gl-px-3!"
:title="$options.i18n.actionsMenu"
>
<template #button-content>
<gl-icon class="gl-mr-0!" name="ellipsis_v" />
</template>
<template v-if="addingMetricsAvailable">
<gl-new-dropdown-item
v-gl-modal="$options.modalIds.addMetric"
data-qa-selector="add_metric_button"
data-testid="add-metric-item"
>
{{ $options.i18n.addMetric }}
</gl-new-dropdown-item>
<gl-modal
ref="addMetricModal"
:modal-id="$options.modalIds.addMetric"
:title="$options.i18n.addMetric"
data-testid="add-metric-modal"
>
<form ref="customMetricsForm" :action="customMetricsPath" method="post">
<custom-metrics-form-fields
:validate-query-path="validateQueryPath"
form-operation="post"
@formValidation="setFormValidity"
/>
</form>
<div slot="modal-footer">
<gl-deprecated-button @click="hideAddMetricModal">
{{ __('Cancel') }}
</gl-deprecated-button>
<gl-deprecated-button
v-track-event="getAddMetricTrackingOptions()"
data-testid="add-metric-modal-submit-button"
:disabled="!customMetricsFormIsValid"
variant="success"
@click="submitCustomMetricsForm"
>
{{ __('Save changes') }}
</gl-deprecated-button>
</div>
</gl-modal>
</template>
<gl-new-dropdown-item
v-if="isMenuItemEnabled.editDashboard"
:href="selectedDashboard ? selectedDashboard.project_blob_path : null"
data-qa-selector="edit_dashboard_button_enabled"
data-testid="edit-dashboard-item-enabled"
>
{{ $options.i18n.editDashboard }}
</gl-new-dropdown-item>
<!--
wrapper for tooltip as button can be `disabled`
https://bootstrap-vue.org/docs/components/tooltip#disabled-elements
-->
<div v-else v-gl-tooltip :title="$options.i18n.editDashboardInfo">
<gl-new-dropdown-item
:alt="$options.i18n.editDashboardInfo"
:href="selectedDashboard ? selectedDashboard.project_blob_path : null"
data-testid="edit-dashboard-item-disabled"
disabled
class="gl-cursor-not-allowed"
>
<span class="gl-text-gray-400">{{ $options.i18n.editDashboard }}</span>
</gl-new-dropdown-item>
</div>
<template v-if="isMenuItemShown.duplicateDashboard">
<gl-new-dropdown-item
v-gl-modal="$options.modalIds.duplicateDashboard"
data-testid="duplicate-dashboard-item"
>
{{ $options.i18n.duplicateDashboard }}
</gl-new-dropdown-item>
<duplicate-dashboard-modal
:default-branch="defaultBranch"
:modal-id="$options.modalIds.duplicateDashboard"
data-testid="duplicate-dashboard-modal"
@dashboardDuplicated="selectDashboard"
/>
</template>
<gl-new-dropdown-item
v-if="selectedDashboard"
data-testid="star-dashboard-item"
:disabled="isUpdatingStarredValue"
@click="toggleStarredValue()"
>
{{ selectedDashboard.starred ? $options.i18n.unstarDashboard : $options.i18n.starDashboard }}
</gl-new-dropdown-item>
<gl-new-dropdown-divider />
<gl-new-dropdown-item
v-gl-modal="$options.modalIds.createDashboard"
data-testid="create-dashboard-item"
:disabled="!isMenuItemEnabled.createDashboard"
:class="{ 'monitoring-actions-item-disabled': !isMenuItemEnabled.createDashboard }"
>
{{ $options.i18n.createDashboard }}
</gl-new-dropdown-item>
<template v-if="isMenuItemEnabled.createDashboard">
<create-dashboard-modal
data-testid="create-dashboard-modal"
:add-dashboard-documentation-path="addDashboardDocumentationPath"
:modal-id="$options.modalIds.createDashboard"
:project-path="projectPath"
/>
</template>
</gl-new-dropdown>
</template>
...@@ -7,17 +7,11 @@ import { ...@@ -7,17 +7,11 @@ import {
GlDeprecatedDropdownItem, GlDeprecatedDropdownItem,
GlDeprecatedDropdownHeader, GlDeprecatedDropdownHeader,
GlDeprecatedDropdownDivider, GlDeprecatedDropdownDivider,
GlNewDropdown,
GlNewDropdownDivider,
GlNewDropdownItem,
GlModal,
GlLoadingIcon, GlLoadingIcon,
GlSearchBoxByType, GlSearchBoxByType,
GlModalDirective, GlModalDirective,
GlTooltipDirective, GlTooltipDirective,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { s__ } from '~/locale';
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility'; import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url'; import invalidUrl from '~/lib/utils/invalid_url';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
...@@ -25,11 +19,9 @@ import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_p ...@@ -25,11 +19,9 @@ 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 ActionsMenu from './dashboard_actions_menu.vue';
import DuplicateDashboardModal from './duplicate_dashboard_modal.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event'; import { timeRangeToUrl } from '../utils';
import { getAddMetricTrackingOptions, timeRangeToUrl } from '../utils';
import { timeRanges } from '~/vue_shared/constants'; import { timeRanges } from '~/vue_shared/constants';
import { timezones } from '../format_date'; import { timezones } from '../format_date';
...@@ -42,23 +34,17 @@ export default { ...@@ -42,23 +34,17 @@ export default {
GlDeprecatedDropdownItem, GlDeprecatedDropdownItem,
GlDeprecatedDropdownHeader, GlDeprecatedDropdownHeader,
GlDeprecatedDropdownDivider, GlDeprecatedDropdownDivider,
GlNewDropdown,
GlNewDropdownDivider,
GlNewDropdownItem,
GlSearchBoxByType, GlSearchBoxByType,
GlModal,
CustomMetricsFormFields,
DateTimePicker, DateTimePicker,
DashboardsDropdown, DashboardsDropdown,
RefreshButton, RefreshButton,
DuplicateDashboardModal,
CreateDashboardModal, ActionsMenu,
}, },
directives: { directives: {
GlModal: GlModalDirective, GlModal: GlModalDirective,
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
TrackEvent: TrackEventDirective,
}, },
props: { props: {
defaultBranch: { defaultBranch: {
...@@ -94,29 +80,19 @@ export default { ...@@ -94,29 +80,19 @@ export default {
required: true, required: true,
}, },
}, },
data() {
return {
formIsValid: null,
};
},
computed: { computed: {
...mapState('monitoringDashboard', [ ...mapState('monitoringDashboard', [
'emptyState', 'emptyState',
'environmentsLoading', 'environmentsLoading',
'currentEnvironmentName', 'currentEnvironmentName',
'isUpdatingStarredValue',
'dashboardTimezone', 'dashboardTimezone',
'projectPath', 'projectPath',
'canAccessOperationsSettings', 'canAccessOperationsSettings',
'operationsSettingsPath', 'operationsSettingsPath',
'currentDashboard', 'currentDashboard',
'addDashboardDocumentationPath',
'externalDashboardUrl', 'externalDashboardUrl',
]), ]),
...mapGetters('monitoringDashboard', ['selectedDashboard', 'filteredEnvironments']), ...mapGetters('monitoringDashboard', ['selectedDashboard', 'filteredEnvironments']),
isOutOfTheBoxDashboard() {
return this.selectedDashboard?.out_of_the_box_dashboard;
},
shouldShowEmptyState() { shouldShowEmptyState() {
return Boolean(this.emptyState); return Boolean(this.emptyState);
}, },
...@@ -130,7 +106,7 @@ export default { ...@@ -130,7 +106,7 @@ export default {
// Custom metrics only avaialble on system dashboards because // Custom metrics only avaialble on system dashboards because
// they are stored in the database. This can be improved. See: // they are stored in the database. This can be improved. See:
// https://gitlab.com/gitlab-org/gitlab/-/issues/28241 // https://gitlab.com/gitlab-org/gitlab/-/issues/28241
this.selectedDashboard?.system_dashboard this.selectedDashboard?.out_of_the_box_dashboard
); );
}, },
showRearrangePanelsBtn() { showRearrangePanelsBtn() {
...@@ -139,15 +115,12 @@ export default { ...@@ -139,15 +115,12 @@ export default {
displayUtc() { displayUtc() {
return this.dashboardTimezone === timezones.UTC; return this.dashboardTimezone === timezones.UTC;
}, },
shouldShowActionsMenu() {
return Boolean(this.projectPath);
},
shouldShowSettingsButton() { shouldShowSettingsButton() {
return this.canAccessOperationsSettings && this.operationsSettingsPath; return this.canAccessOperationsSettings && this.operationsSettingsPath;
}, },
}, },
methods: { methods: {
...mapActions('monitoringDashboard', ['filterEnvironments', 'toggleStarredValue']), ...mapActions('monitoringDashboard', ['filterEnvironments']),
selectDashboard(dashboard) { selectDashboard(dashboard) {
// Once the sidebar See metrics link is updated to the new URL, // Once the sidebar See metrics link is updated to the new URL,
// this sort of hardcoding will not be necessary. // this sort of hardcoding will not be necessary.
...@@ -171,16 +144,6 @@ export default { ...@@ -171,16 +144,6 @@ export default {
toggleRearrangingPanels() { toggleRearrangingPanels() {
this.$emit('setRearrangingPanels', !this.isRearrangingPanels); this.$emit('setRearrangingPanels', !this.isRearrangingPanels);
}, },
setFormValidity(isValid) {
this.formIsValid = isValid;
},
hideAddMetricModal() {
this.$refs.addMetricModal.hide();
},
getAddMetricTrackingOptions,
submitCustomMetricsForm() {
this.$refs.customMetricsForm.submit();
},
getEnvironmentPath(environment) { getEnvironmentPath(environment) {
// Once the sidebar See metrics link is updated to the new URL, // Once the sidebar See metrics link is updated to the new URL,
// this sort of hardcoding will not be necessary. // this sort of hardcoding will not be necessary.
...@@ -193,16 +156,6 @@ export default { ...@@ -193,16 +156,6 @@ export default {
return mergeUrlParams({ environment }, url); return mergeUrlParams({ environment }, url);
}, },
}, },
modalIds: {
addMetric: 'addMetric',
createDashboard: 'createDashboard',
duplicateDashboard: 'duplicateDashboard',
},
i18n: {
starDashboard: s__('Metrics|Star dashboard'),
unstarDashboard: s__('Metrics|Unstar dashboard'),
addMetric: s__('Metrics|Add metric'),
},
timeRanges, timeRanges,
}; };
</script> </script>
...@@ -280,29 +233,6 @@ export default { ...@@ -280,29 +233,6 @@ export default {
<div class="flex-grow-1"></div> <div class="flex-grow-1"></div>
<div class="d-sm-flex"> <div class="d-sm-flex">
<div v-if="selectedDashboard" class="mb-2 mr-2 d-flex">
<!--
wrapper for tooltip as button can be `disabled`
https://bootstrap-vue.org/docs/components/tooltip#disabled-elements
-->
<div
v-gl-tooltip
class="flex-grow-1"
:title="
selectedDashboard.starred ? $options.i18n.unstarDashboard : $options.i18n.starDashboard
"
>
<gl-button
ref="toggleStarBtn"
class="w-100"
:disabled="isUpdatingStarredValue"
variant="default"
:icon="selectedDashboard.starred ? 'star' : 'star-o'"
@click="toggleStarredValue()"
/>
</div>
</div>
<div v-if="showRearrangePanelsBtn" class="mb-2 mr-2 d-flex"> <div v-if="showRearrangePanelsBtn" class="mb-2 mr-2 d-flex">
<gl-button <gl-button
:pressed="isRearrangingPanels" :pressed="isRearrangingPanels"
...@@ -313,58 +243,6 @@ export default { ...@@ -313,58 +243,6 @@ export default {
{{ __('Arrange charts') }} {{ __('Arrange charts') }}
</gl-button> </gl-button>
</div> </div>
<div v-if="addingMetricsAvailable" class="mb-2 mr-2 d-flex d-sm-block">
<gl-button
ref="addMetricBtn"
v-gl-modal="$options.modalIds.addMetric"
variant="default"
data-qa-selector="add_metric_button"
class="flex-grow-1"
>
{{ $options.i18n.addMetric }}
</gl-button>
<gl-modal
ref="addMetricModal"
:modal-id="$options.modalIds.addMetric"
:title="$options.i18n.addMetric"
>
<form ref="customMetricsForm" :action="customMetricsPath" method="post">
<custom-metrics-form-fields
:validate-query-path="validateQueryPath"
form-operation="post"
@formValidation="setFormValidity"
/>
</form>
<div slot="modal-footer">
<gl-button @click="hideAddMetricModal">
{{ __('Cancel') }}
</gl-button>
<gl-button
ref="submitCustomMetricsFormBtn"
v-track-event="getAddMetricTrackingOptions()"
:disabled="!formIsValid"
variant="success"
category="primary"
@click="submitCustomMetricsForm"
>
{{ __('Save changes') }}
</gl-button>
</div>
</gl-modal>
</div>
<div
v-if="selectedDashboard && selectedDashboard.can_edit"
class="mb-2 mr-2 d-flex d-sm-block"
>
<gl-button
class="flex-grow-1 js-edit-link"
:href="selectedDashboard.project_blob_path"
data-qa-selector="edit_dashboard_button"
>
{{ __('Edit dashboard') }}
</gl-button>
</div>
<div <div
v-if="externalDashboardUrl && externalDashboardUrl.length" v-if="externalDashboardUrl && externalDashboardUrl.length"
...@@ -382,65 +260,28 @@ export default { ...@@ -382,65 +260,28 @@ export default {
</gl-button> </gl-button>
</div> </div>
<!-- This separator should be displayed only if at least one of the action menu or settings button are displayed --> <div class="gl-mb-3 gl-mr-3 d-flex d-sm-block">
<span <actions-menu
v-if="shouldShowActionsMenu || shouldShowSettingsButton" :adding-metrics-available="addingMetricsAvailable"
aria-hidden="true" :custom-metrics-path="customMetricsPath"
class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block" :validate-query-path="validateQueryPath"
></span> :default-branch="defaultBranch"
/>
</div>
<div v-if="shouldShowActionsMenu" class="gl-mb-3 gl-mr-3 d-flex d-sm-block"> <template v-if="shouldShowSettingsButton">
<gl-new-dropdown <span aria-hidden="true" class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block"></span>
v-gl-tooltip
right
class="gl-flex-grow-1"
data-testid="actions-menu"
data-qa-selector="actions_menu_dropdown"
: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 <div class="mb-2 mr-2 d-flex d-sm-block">
data-testid="create-dashboard-modal" <gl-button
:add-dashboard-documentation-path="addDashboardDocumentationPath" v-gl-tooltip
:modal-id="$options.modalIds.createDashboard" data-testid="metrics-settings-button"
:project-path="projectPath" icon="settings"
:href="operationsSettingsPath"
:title="s__('Metrics|Metrics Settings')"
/> />
</div>
<template v-if="isOutOfTheBoxDashboard"> </template>
<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>
<duplicate-dashboard-modal
:default-branch="defaultBranch"
:modal-id="$options.modalIds.duplicateDashboard"
@dashboardDuplicated="selectDashboard"
/>
</template>
</gl-new-dropdown>
</div>
<div v-if="shouldShowSettingsButton" class="mb-2 mr-2 d-flex d-sm-block">
<gl-button
v-gl-tooltip
data-testid="metrics-settings-button"
icon="settings"
:href="operationsSettingsPath"
:title="s__('Metrics|Metrics Settings')"
/>
</div>
</div> </div>
</div> </div>
</template> </template>
---
title: Change UI and add new actions to monitor dashboard actions menu
merge_request: 38946
author:
type: changed
...@@ -20,7 +20,7 @@ The metrics as defined below do not support alerts, unlike ...@@ -20,7 +20,7 @@ The metrics as defined below do not support alerts, unlike
## Add a new dashboard to your project ## Add a new dashboard to your project
> UI option [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/223204) in GitLab 13.2. > UI option [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/228856) in GitLab 13.3.
You can configure a custom dashboard by adding a new YAML file into your project's You can configure a custom dashboard by adding a new YAML file into your project's
`.gitlab/dashboards/` directory. For the dashboard to display on your project's **Operations > Metrics** page, the files must have a `.yml` `.gitlab/dashboards/` directory. For the dashboard to display on your project's **Operations > Metrics** page, the files must have a `.yml`
...@@ -31,9 +31,9 @@ To create a new dashboard from the GitLab user interface: ...@@ -31,9 +31,9 @@ To create a new dashboard from the GitLab user interface:
1. Sign in to GitLab as a user with Maintainer or Owner 1. Sign in to GitLab as a user with Maintainer or Owner
[permissions](../../../user/permissions.md#project-members-permissions). [permissions](../../../user/permissions.md#project-members-permissions).
1. Navigate to your dashboard at **Operations > Metrics**. 1. Navigate to your dashboard at **Operations > Metrics**.
1. In the top-right corner of your dashboard, click the **{file-addition-solid}** **Actions** menu, 1. In the top-right corner of your dashboard, click the **{{ellipsis_v}}** **More actions** menu,
and select **Create new**: and select **Create new**:
![Monitoring Dashboard actions menu with create new item](img/actions_menu_create_new_dashboard_v13_2.png) ![Monitoring Dashboard actions menu with create new item](img/actions_menu_create_new_dashboard_v13_3.png)
1. In the modal window, click **Open Repository**, then follow the instructions 1. In the modal window, click **Open Repository**, then follow the instructions
for creating a new dashboard from the command line. for creating a new dashboard from the command line.
...@@ -82,7 +82,7 @@ The resulting `.yml` file can be customized and adapted to your project. ...@@ -82,7 +82,7 @@ The resulting `.yml` file can be customized and adapted to your project.
You can decide to save the dashboard `.yml` file in the project's **default** branch or in a You can decide to save the dashboard `.yml` file in the project's **default** branch or in a
new branch. new branch.
1. Click **Duplicate dashboard** in the actions menu. 1. Click **Duplicate current dashboard** in the **{{ellipsis_v}}** **More actions** menu.
NOTE: **Note:** NOTE: **Note:**
You can duplicate only GitLab-defined dashboards. You can duplicate only GitLab-defined dashboards.
...@@ -105,7 +105,7 @@ To manage the settings for your metrics dashboard: ...@@ -105,7 +105,7 @@ To manage the settings for your metrics dashboard:
1. Navigate to your dashboard at **Operations > Metrics**. 1. Navigate to your dashboard at **Operations > Metrics**.
1. In the top-right corner of your dashboard, click **Metrics Settings**: 1. In the top-right corner of your dashboard, click **Metrics Settings**:
![Monitoring Dashboard actions menu with create new item](img/metrics_settings_button_v13_2.png) ![Monitoring Dashboard actions menu with create new item](img/metrics_settings_button_v13_3.png)
## Chart Context Menu ## Chart Context Menu
......
...@@ -25,7 +25,7 @@ To view the metrics dashboard for an environment that has ...@@ -25,7 +25,7 @@ To view the metrics dashboard for an environment that has
GitLab displays the default metrics dashboard for the environment, like the GitLab displays the default metrics dashboard for the environment, like the
following example: following example:
![Example of metrics dashboard](img/example-dashboard_v13_1.png) ![Example of metrics dashboard](img/example-dashboard_v13_3.png)
The top of the dashboard contains a navigation bar. From left to right, the The top of the dashboard contains a navigation bar. From left to right, the
navigation bar contains: navigation bar contains:
...@@ -37,15 +37,19 @@ navigation bar contains: ...@@ -37,15 +37,19 @@ navigation bar contains:
- **Range** - The time period of data to display. - **Range** - The time period of data to display.
- **Refresh dashboard** **{retry}** - Reload the dashboard with current data. - **Refresh dashboard** **{retry}** - Reload the dashboard with current data.
- **Set refresh rate** - Set a time frame for refreshing the data displayed. - **Set refresh rate** - Set a time frame for refreshing the data displayed.
- **Star dashboard** **{star-o}** - Click to mark a dashboard as a favorite. - **More actions** **{ellipsis_v}** - More dashboard actions
- **Add metric** - Adds a [custom metric](#adding-custom-metrics). Only available on GitLab-defined dashboards.
([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34779) in GitLab 12.5.)
- **Edit dashboard YAML** - Edit the source YAML file of a custom dashboard. Only available on
[custom dashboards](dashboards/index.md).
([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34779) in GitLab 12.5.)
- **Duplicate current dashboard** - Save a [complete copy of a dashboard](dashboards/index.md#duplicate-a-gitlab-defined-dashboard). Only available on GitLab-defined dashboards.
- **Star dashboard** **{star-o}** - Click to mark a dashboard as a favorite.
Starred dashboards display a solid star **{star}** button, and display first Starred dashboards display a solid star **{star}** button, and display first
in the **Dashboard** dropdown list. in the **Dashboard** dropdown list.
([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214582) in GitLab 13.0.) ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214582) in GitLab 13.0.)
- **Edit dashboard** - Edit the source YAML file of a custom dashboard. Only available on - **Create new dashboard** - Create a [new custom dashboard for your project](dashboards/index.md#add-a-new-dashboard-to-your-project).
[custom dashboards](dashboards/index.md). ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/228856) in GitLab 13.3.)
([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34779) in GitLab 12.5.)
- **Create dashboard** **{file-addition-solid}** - Create a
[new custom dashboard for your project](dashboards/index.md#add-a-new-dashboard-to-your-project).
- **Metrics settings** - Configure the - **Metrics settings** - Configure the
[settings for this dashboard](dashboards/index.md#manage-the-metrics-dashboard-settings). [settings for this dashboard](dashboards/index.md#manage-the-metrics-dashboard-settings).
...@@ -70,7 +74,7 @@ helps quickly create a deployment: ...@@ -70,7 +74,7 @@ helps quickly create a deployment:
1. When the pipeline has run successfully, graphs are available on the 1. When the pipeline has run successfully, graphs are available on the
**Operations > Metrics** page. **Operations > Metrics** page.
![Monitoring Dashboard](img/prometheus_monitoring_dashboard_v13_1.png) ![Monitoring Dashboard](img/prometheus_monitoring_dashboard_v13_3.png)
## Customize your metrics dashboard ## Customize your metrics dashboard
......
...@@ -8801,9 +8801,6 @@ msgstr "" ...@@ -8801,9 +8801,6 @@ msgstr ""
msgid "Edit comment" msgid "Edit comment"
msgstr "" msgstr ""
msgid "Edit dashboard"
msgstr ""
msgid "Edit description" msgid "Edit description"
msgstr "" msgstr ""
...@@ -15233,9 +15230,6 @@ msgstr "" ...@@ -15233,9 +15230,6 @@ 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 ""
...@@ -15269,9 +15263,15 @@ msgstr "" ...@@ -15269,9 +15263,15 @@ msgstr ""
msgid "Metrics|Duplicate dashboard" msgid "Metrics|Duplicate dashboard"
msgstr "" msgstr ""
msgid "Metrics|Duplicate this dashboard to edit dashboard YAML"
msgstr ""
msgid "Metrics|Duplicating..." msgid "Metrics|Duplicating..."
msgstr "" msgstr ""
msgid "Metrics|Edit dashboard YAML"
msgstr ""
msgid "Metrics|Edit metric" msgid "Metrics|Edit metric"
msgid_plural "Metrics|Edit metrics" msgid_plural "Metrics|Edit metrics"
msgstr[0] "" msgstr[0] ""
...@@ -15313,6 +15313,9 @@ msgstr "" ...@@ -15313,6 +15313,9 @@ msgstr ""
msgid "Metrics|Min" msgid "Metrics|Min"
msgstr "" msgstr ""
msgid "Metrics|More actions"
msgstr ""
msgid "Metrics|Must be a valid PromQL query." msgid "Metrics|Must be a valid PromQL query."
msgstr "" msgstr ""
......
...@@ -18,9 +18,12 @@ module QA ...@@ -18,9 +18,12 @@ module QA
view 'app/assets/javascripts/monitoring/components/dashboard_header.vue' do view 'app/assets/javascripts/monitoring/components/dashboard_header.vue' do
element :dashboards_filter_dropdown element :dashboards_filter_dropdown
element :environments_dropdown element :environments_dropdown
element :edit_dashboard_button
element :range_picker_dropdown element :range_picker_dropdown
end
view 'app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue' do
element :actions_menu_dropdown element :actions_menu_dropdown
element :edit_dashboard_button_enabled
end end
view 'app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue' do view 'app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue' do
...@@ -56,7 +59,7 @@ module QA ...@@ -56,7 +59,7 @@ module QA
def has_edit_dashboard_enabled? def has_edit_dashboard_enabled?
within_element :prometheus_graphs do within_element :prometheus_graphs do
has_element? :edit_dashboard_button has_element? :edit_dashboard_button_enabled
end end
end end
......
...@@ -99,34 +99,19 @@ exports[`Dashboard template matches the default snapshot 1`] = ` ...@@ -99,34 +99,19 @@ exports[`Dashboard template matches the default snapshot 1`] = `
<div <div
class="d-sm-flex" class="d-sm-flex"
> >
<div
class="mb-2 mr-2 d-flex"
>
<div
class="flex-grow-1"
title="Star dashboard"
>
<gl-button-stub
category="tertiary"
class="w-100"
icon="star-o"
size="medium"
variant="default"
/>
</div>
</div>
<!---->
<!---->
<!---->
<!----> <!---->
<!----> <!---->
<!----> <div
class="gl-mb-3 gl-mr-3 d-flex d-sm-block"
>
<actions-menu-stub
custommetricspath="/monitoring/monitor-project/prometheus/metrics"
defaultbranch="master"
validatequerypath="/monitoring/monitor-project/prometheus/metrics/validate_query"
/>
</div>
<!----> <!---->
</div> </div>
......
import { shallowMount } from '@vue/test-utils';
import { createStore } from '~/monitoring/stores';
import { setupAllDashboards, setupStoreWithData } from '../store_utils';
import { redirectTo } from '~/lib/utils/url_utility';
import Tracking from '~/tracking';
import ActionsMenu from '~/monitoring/components/dashboard_actions_menu.vue';
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
import { dashboardActionsMenuProps, dashboardGitResponse } from '../mock_data';
import * as types from '~/monitoring/stores/mutation_types';
jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
queryToObject: jest.fn(),
}));
describe('Actions menu', () => {
const ootbDashboards = [dashboardGitResponse[0], dashboardGitResponse[2]];
const customDashboard = dashboardGitResponse[1];
let store;
let wrapper;
const findAddMetricItem = () => wrapper.find('[data-testid="add-metric-item"]');
const findAddMetricModal = () => wrapper.find('[data-testid="add-metric-modal"]');
const findAddMetricModalSubmitButton = () =>
wrapper.find('[data-testid="add-metric-modal-submit-button"]');
const findStarDashboardItem = () => wrapper.find('[data-testid="star-dashboard-item"]');
const findEditDashboardItemEnabled = () =>
wrapper.find('[data-testid="edit-dashboard-item-enabled"]');
const findEditDashboardItemDisabled = () =>
wrapper.find('[data-testid="edit-dashboard-item-disabled"]');
const findDuplicateDashboardItem = () => wrapper.find('[data-testid="duplicate-dashboard-item"]');
const findDuplicateDashboardModal = () =>
wrapper.find('[data-testid="duplicate-dashboard-modal"]');
const findCreateDashboardItem = () => wrapper.find('[data-testid="create-dashboard-item"]');
const findCreateDashboardModal = () => wrapper.find('[data-testid="create-dashboard-modal"]');
const createShallowWrapper = (props = {}, options = {}) => {
wrapper = shallowMount(ActionsMenu, {
propsData: { ...dashboardActionsMenuProps, ...props },
store,
...options,
});
};
beforeEach(() => {
store = createStore();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('add metric item', () => {
it('is rendered when custom metrics are available', () => {
createShallowWrapper();
return wrapper.vm.$nextTick(() => {
expect(findAddMetricItem().exists()).toBe(true);
});
});
it('is not rendered when custom metrics are not available', () => {
createShallowWrapper({
addingMetricsAvailable: false,
});
return wrapper.vm.$nextTick(() => {
expect(findAddMetricItem().exists()).toBe(false);
});
});
describe('when available', () => {
beforeEach(() => {
createShallowWrapper();
});
it('modal for custom metrics form is rendered', () => {
expect(findAddMetricModal().exists()).toBe(true);
expect(findAddMetricModal().attributes().modalid).toBe('addMetric');
});
it('add metric modal submit button exists', () => {
expect(findAddMetricModalSubmitButton().exists()).toBe(true);
});
it('renders custom metrics form fields', () => {
expect(wrapper.find(CustomMetricsFormFields).exists()).toBe(true);
});
});
describe('when not available', () => {
beforeEach(() => {
createShallowWrapper({ addingMetricsAvailable: false });
});
it('modal for custom metrics form is not rendered', () => {
expect(findAddMetricModal().exists()).toBe(false);
});
});
describe('adding new metric from modal', () => {
let origPage;
beforeEach(done => {
jest.spyOn(Tracking, 'event').mockReturnValue();
createShallowWrapper();
setupStoreWithData(store);
origPage = document.body.dataset.page;
document.body.dataset.page = 'projects:environments:metrics';
wrapper.vm.$nextTick(done);
});
afterEach(() => {
document.body.dataset.page = origPage;
});
it('is tracked', done => {
const submitButton = findAddMetricModalSubmitButton().vm;
wrapper.vm.$nextTick(() => {
submitButton.$el.click();
wrapper.vm.$nextTick(() => {
expect(Tracking.event).toHaveBeenCalledWith(
document.body.dataset.page,
'click_button',
{
label: 'add_new_metric',
property: 'modal',
value: undefined,
},
);
done();
});
});
});
});
});
describe('edit dashboard yml item', () => {
beforeEach(() => {
createShallowWrapper();
});
describe('when current dashboard is custom', () => {
beforeEach(() => {
setupAllDashboards(store, customDashboard.path);
});
it('enabled item is rendered and has falsy disabled attribute', () => {
expect(findEditDashboardItemEnabled().exists()).toBe(true);
expect(findEditDashboardItemEnabled().attributes('disabled')).toBe(undefined);
});
it('enabled item links to their edit path', () => {
expect(findEditDashboardItemEnabled().attributes('href')).toBe(
customDashboard.project_blob_path,
);
});
it('disabled item is not rendered', () => {
expect(findEditDashboardItemDisabled().exists()).toBe(false);
});
});
describe.each(ootbDashboards)('when current dashboard is OOTB', dashboard => {
beforeEach(() => {
setupAllDashboards(store, dashboard.path);
});
it('disabled item is rendered and has disabled attribute set on it', () => {
expect(findEditDashboardItemDisabled().exists()).toBe(true);
expect(findEditDashboardItemDisabled().attributes('disabled')).toBe('');
});
it('enabled item is not rendered', () => {
expect(findEditDashboardItemEnabled().exists()).toBe(false);
});
});
});
describe('duplicate dashboard item', () => {
beforeEach(() => {
createShallowWrapper();
});
describe.each(ootbDashboards)('when current dashboard is OOTB', dashboard => {
beforeEach(() => {
setupAllDashboards(store, dashboard.path);
});
it('is rendered', () => {
expect(findDuplicateDashboardItem().exists()).toBe(true);
});
it('duplicate dashboard modal is rendered', () => {
expect(findDuplicateDashboardModal().exists()).toBe(true);
});
it('clicking on item opens up the duplicate dashboard modal', () => {
const modalId = 'duplicateDashboard';
const modalTrigger = findDuplicateDashboardItem();
const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
modalTrigger.trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(rootEmit.mock.calls[0]).toContainEqual(modalId);
});
});
});
describe('when current dashboard is custom', () => {
beforeEach(() => {
setupAllDashboards(store, customDashboard.path);
});
it('is not rendered', () => {
expect(findDuplicateDashboardItem().exists()).toBe(false);
});
it('duplicate dashboard modal is not rendered', () => {
expect(findDuplicateDashboardModal().exists()).toBe(false);
});
});
describe('when no dashboard is set', () => {
it('is not rendered', () => {
expect(findDuplicateDashboardItem().exists()).toBe(false);
});
it('duplicate dashboard modal is not rendered', () => {
expect(findDuplicateDashboardModal().exists()).toBe(false);
});
});
describe('when a dashboard has been duplicated in the duplicate dashboard modal', () => {
beforeEach(() => {
store.state.monitoringDashboard.projectPath = 'root/sandbox';
setupAllDashboards(store, dashboardGitResponse[0].path);
});
it('redirects to the newly created dashboard', () => {
delete window.location;
window.location = new URL('https://localhost');
const newDashboard = dashboardGitResponse[1];
const newDashboardUrl = 'root/sandbox/-/metrics/dashboard.yml';
findDuplicateDashboardModal().vm.$emit('dashboardDuplicated', newDashboard);
return wrapper.vm.$nextTick().then(() => {
expect(redirectTo).toHaveBeenCalled();
expect(redirectTo).toHaveBeenCalledWith(newDashboardUrl);
});
});
});
});
describe('star dashboard item', () => {
beforeEach(() => {
createShallowWrapper();
setupAllDashboards(store);
jest.spyOn(store, 'dispatch').mockResolvedValue();
});
it('is shown', () => {
expect(findStarDashboardItem().exists()).toBe(true);
});
it('is not disabled', () => {
expect(findStarDashboardItem().attributes('disabled')).toBeFalsy();
});
it('is disabled when starring is taking place', () => {
store.commit(`monitoringDashboard/${types.REQUEST_DASHBOARD_STARRING}`);
return wrapper.vm.$nextTick(() => {
expect(findStarDashboardItem().exists()).toBe(true);
expect(findStarDashboardItem().attributes('disabled')).toBe('true');
});
});
it('on click it dispatches a toggle star action', () => {
findStarDashboardItem().vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(store.dispatch).toHaveBeenCalledWith(
'monitoringDashboard/toggleStarredValue',
undefined,
);
});
});
describe('when dashboard is not starred', () => {
beforeEach(() => {
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
currentDashboard: dashboardGitResponse[0].path,
});
return wrapper.vm.$nextTick();
});
it('item text shows "Star dashboard"', () => {
expect(findStarDashboardItem().html()).toMatch(/Star dashboard/);
});
});
describe('when dashboard is starred', () => {
beforeEach(() => {
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
currentDashboard: dashboardGitResponse[1].path,
});
return wrapper.vm.$nextTick();
});
it('item text shows "Unstar dashboard"', () => {
expect(findStarDashboardItem().html()).toMatch(/Unstar dashboard/);
});
});
});
describe('create dashboard item', () => {
beforeEach(() => {
createShallowWrapper();
});
it('is rendered by default but it is disabled', () => {
expect(findCreateDashboardItem().attributes('disabled')).toBe('true');
});
describe('when project path is set', () => {
const mockProjectPath = 'root/sandbox';
const mockAddDashboardDocPath = '/doc/add-dashboard';
beforeEach(() => {
store.state.monitoringDashboard.projectPath = mockProjectPath;
store.state.monitoringDashboard.addDashboardDocumentationPath = mockAddDashboardDocPath;
});
it('is not disabled', () => {
expect(findCreateDashboardItem().attributes('disabled')).toBe(undefined);
});
it('renders a modal for creating a dashboard', () => {
expect(findCreateDashboardModal().exists()).toBe(true);
});
it('clicking opens up the modal', () => {
const modalId = 'createDashboard';
const modalTrigger = findCreateDashboardItem();
const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
modalTrigger.trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(rootEmit.mock.calls[0]).toContainEqual(modalId);
});
});
it('modal gets passed correct props', () => {
expect(findCreateDashboardModal().props('projectPath')).toBe(mockProjectPath);
expect(findCreateDashboardModal().props('addDashboardDocumentationPath')).toBe(
mockAddDashboardDocPath,
);
});
});
describe('when project path is not set', () => {
beforeEach(() => {
store.state.monitoringDashboard.projectPath = null;
});
it('is disabled', () => {
expect(findCreateDashboardItem().attributes('disabled')).toBe('true');
});
it('does not render a modal for creating a dashboard', () => {
expect(findCreateDashboardModal().exists()).toBe(false);
});
});
});
});
...@@ -6,8 +6,7 @@ import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_p ...@@ -6,8 +6,7 @@ import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_p
import RefreshButton from '~/monitoring/components/refresh_button.vue'; import RefreshButton from '~/monitoring/components/refresh_button.vue';
import DashboardHeader from '~/monitoring/components/dashboard_header.vue'; import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue'; import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
import DuplicateDashboardModal from '~/monitoring/components/duplicate_dashboard_modal.vue'; import ActionsMenu from '~/monitoring/components/dashboard_actions_menu.vue';
import CreateDashboardModal from '~/monitoring/components/create_dashboard_modal.vue';
import { setupAllDashboards, setupStoreWithDashboard, setupStoreWithData } from '../store_utils'; import { setupAllDashboards, setupStoreWithDashboard, setupStoreWithData } from '../store_utils';
import { import {
environmentData, environmentData,
...@@ -18,7 +17,6 @@ import { ...@@ -18,7 +17,6 @@ import {
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
const mockProjectPath = 'https://path/to/project'; const mockProjectPath = 'https://path/to/project';
const mockAddDashboardDocPath = '/doc/add-dashboard';
jest.mock('~/lib/utils/url_utility', () => ({ jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(), redirectTo: jest.fn(),
...@@ -41,13 +39,7 @@ describe('Dashboard header', () => { ...@@ -41,13 +39,7 @@ describe('Dashboard header', () => {
const findDateTimePicker = () => wrapper.find(DateTimePicker); const findDateTimePicker = () => wrapper.find(DateTimePicker);
const findRefreshButton = () => wrapper.find(RefreshButton); const findRefreshButton = () => wrapper.find(RefreshButton);
const findActionsMenu = () => wrapper.find('[data-testid="actions-menu"]'); const findActionsMenu = () => wrapper.find(ActionsMenu);
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 setSearchTerm = searchTerm => { const setSearchTerm = searchTerm => {
store.commit(`monitoringDashboard/${types.SET_ENVIRONMENTS_FILTER}`, searchTerm); store.commit(`monitoringDashboard/${types.SET_ENVIRONMENTS_FILTER}`, searchTerm);
...@@ -264,31 +256,6 @@ describe('Dashboard header', () => { ...@@ -264,31 +256,6 @@ describe('Dashboard header', () => {
}); });
}); });
describe('when a dashboard has been duplicated in the duplicate dashboard modal', () => {
beforeEach(() => {
store.state.monitoringDashboard.projectPath = 'root/sandbox';
setupAllDashboards(store, dashboardGitResponse[0].path);
});
it('redirects to the newly created dashboard', () => {
delete window.location;
window.location = new URL('https://localhost');
const newDashboard = dashboardGitResponse[1];
createShallowWrapper();
const newDashboardUrl = 'root/sandbox/-/metrics/dashboard.yml';
findDuplicateDashboardModal().vm.$emit('dashboardDuplicated', newDashboard);
return wrapper.vm.$nextTick().then(() => {
expect(redirectTo).toHaveBeenCalled();
expect(redirectTo).toHaveBeenCalledWith(newDashboardUrl);
});
});
});
describe('external dashboard link', () => { describe('external dashboard link', () => {
beforeEach(() => { beforeEach(() => {
store.state.monitoringDashboard.externalDashboardUrl = '/mockUrl'; store.state.monitoringDashboard.externalDashboardUrl = '/mockUrl';
...@@ -307,113 +274,97 @@ describe('Dashboard header', () => { ...@@ -307,113 +274,97 @@ describe('Dashboard header', () => {
}); });
describe('actions menu', () => { describe('actions menu', () => {
beforeEach(() => { const ootbDashboards = [
store.state.monitoringDashboard.projectPath = ''; dashboardGitResponse[0].path,
createShallowWrapper(); selfMonitoringDashboardGitResponse[0].path,
}); ];
const customDashboards = [
dashboardGitResponse[1].path,
selfMonitoringDashboardGitResponse[1].path,
];
it('is rendered if projectPath is set in store', () => { it('is rendered', () => {
store.state.monitoringDashboard.projectPath = mockProjectPath; createShallowWrapper();
return wrapper.vm.$nextTick().then(() => { expect(findActionsMenu().exists()).toBe(true);
expect(findActionsMenu().exists()).toBe(true);
});
}); });
it('is not rendered if projectPath is not set in store', () => { describe('adding metrics prop', () => {
expect(findActionsMenu().exists()).toBe(false); it.each(ootbDashboards)('gets passed true if current dashboard is OOTB', dashboardPath => {
}); createShallowWrapper({ customMetricsAvailable: true });
it('contains the create dashboard modal', () => { store.state.monitoringDashboard.emptyState = false;
store.state.monitoringDashboard.projectPath = mockProjectPath; setupAllDashboards(store, dashboardPath);
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(findActionsMenu().contains(CreateDashboardModal)).toBe(true); expect(findActionsMenu().props('addingMetricsAvailable')).toBe(true);
});
}); });
});
const duplicableCases = [ it.each(customDashboards)(
null, // When no path is specified, it uses the overview dashboard path. 'gets passed false if current dashboard is custom',
dashboardGitResponse[0].path, dashboardPath => {
dashboardGitResponse[2].path, createShallowWrapper({ customMetricsAvailable: true });
selfMonitoringDashboardGitResponse[0].path,
];
describe.each(duplicableCases)( store.state.monitoringDashboard.emptyState = false;
'when the selected dashboard can be duplicated',
dashboardPath => {
it('contains menu items for "Create New", "Duplicate Dashboard" and a modal for duplicating dashboards', () => {
store.state.monitoringDashboard.projectPath = mockProjectPath;
setupAllDashboards(store, dashboardPath); setupAllDashboards(store, dashboardPath);
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(findCreateDashboardMenuItem().exists()).toBe(true); expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false);
expect(findCreateDashboardDuplicateItem().exists()).toBe(true);
expect(findDuplicateDashboardModal().exists()).toBe(true);
}); });
}); },
}, );
);
const nonDuplicableCases = [ it('gets passed false if empty state is shown', () => {
dashboardGitResponse[1].path, createShallowWrapper({ customMetricsAvailable: true });
selfMonitoringDashboardGitResponse[1].path,
];
describe.each(nonDuplicableCases)( store.state.monitoringDashboard.emptyState = true;
'when the selected dashboard cannot be duplicated', setupAllDashboards(store, ootbDashboards[0]);
dashboardPath => {
it('contains a "Create New" menu item, but no "Duplicate Dashboard" menu item and modal', () => {
store.state.monitoringDashboard.projectPath = mockProjectPath;
setupAllDashboards(store, dashboardPath);
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(findCreateDashboardMenuItem().exists()).toBe(true); expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false);
expect(findCreateDashboardDuplicateItem().exists()).toBe(false);
expect(findDuplicateDashboardModal().exists()).toBe(false);
});
}); });
}, });
);
});
describe('actions menu modals', () => { it('gets passed false if custom metrics are not available', () => {
beforeEach(() => { createShallowWrapper({ customMetricsAvailable: false });
store.state.monitoringDashboard.projectPath = mockProjectPath;
store.state.monitoringDashboard.addDashboardDocumentationPath = mockAddDashboardDocPath;
setupAllDashboards(store);
createShallowWrapper(); store.state.monitoringDashboard.emptyState = false;
setupAllDashboards(store, ootbDashboards[0]);
return wrapper.vm.$nextTick().then(() => {
expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false);
});
});
}); });
it('Clicking on "Create New" opens up a modal', () => { it('custom metrics path gets passed', () => {
const modalId = 'createDashboard'; const path = 'https://path/to/customMetrics';
const modalTrigger = findCreateDashboardMenuItem();
const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
modalTrigger.trigger('click'); createShallowWrapper({ customMetricsPath: path });
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(rootEmit.mock.calls[0]).toContainEqual(modalId); expect(findActionsMenu().props('customMetricsPath')).toBe(path);
}); });
}); });
it('"Create new dashboard" modal contains correct buttons', () => { it('validate query path gets passed', () => {
expect(findCreateDashboardModal().props('projectPath')).toBe(mockProjectPath); const path = 'https://path/to/validateQuery';
expect(findCreateDashboardModal().props('addDashboardDocumentationPath')).toBe(
mockAddDashboardDocPath, createShallowWrapper({ validateQueryPath: path });
);
return wrapper.vm.$nextTick().then(() => {
expect(findActionsMenu().props('validateQueryPath')).toBe(path);
});
}); });
it('"Duplicate Dashboard" opens up a modal', () => { it('default branch gets passed', () => {
const modalId = 'duplicateDashboard'; const branch = 'branchName';
const modalTrigger = findCreateDashboardDuplicateItem();
const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
modalTrigger.trigger('click'); createShallowWrapper({ defaultBranch: branch });
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(rootEmit.mock.calls[0]).toContainEqual(modalId); expect(findActionsMenu().props('defaultBranch')).toBe(branch);
}); });
}); });
}); });
...@@ -465,72 +416,4 @@ describe('Dashboard header', () => { ...@@ -465,72 +416,4 @@ describe('Dashboard header', () => {
}); });
}); });
}); });
describe('Add metric button', () => {
const findAddMetricButton = () => wrapper.find('[data-qa-selector="add_metric_button"]');
it('is not rendered when custom metrics are not available', () => {
store.state.monitoringDashboard.emptyState = false;
createShallowWrapper({
customMetricsAvailable: false,
});
setupAllDashboards(store, dashboardGitResponse[0].path);
return wrapper.vm.$nextTick(() => {
expect(findAddMetricButton().exists()).toBe(false);
});
});
it('is not rendered when displaying empty state', () => {
store.state.monitoringDashboard.emptyState = true;
createShallowWrapper({
customMetricsAvailable: true,
});
setupAllDashboards(store, dashboardGitResponse[0].path);
return wrapper.vm.$nextTick(() => {
expect(findAddMetricButton().exists()).toBe(false);
});
});
describe('system dashboards', () => {
const systemDashboards = [
dashboardGitResponse[0].path,
selfMonitoringDashboardGitResponse[0].path,
];
const nonSystemDashboards = [
dashboardGitResponse[1].path,
dashboardGitResponse[2].path,
selfMonitoringDashboardGitResponse[1].path,
];
beforeEach(() => {
store.state.monitoringDashboard.emptyState = false;
createShallowWrapper({
customMetricsAvailable: true,
});
});
it.each(systemDashboards)('is rendered for system dashboards', dashboardPath => {
setupAllDashboards(store, dashboardPath);
return wrapper.vm.$nextTick(() => {
expect(findAddMetricButton().exists()).toBe(true);
});
});
it.each(nonSystemDashboards)('is not rendered for non-system dashboards', dashboardPath => {
setupAllDashboards(store, dashboardPath);
return wrapper.vm.$nextTick(() => {
expect(findAddMetricButton().exists()).toBe(false);
});
});
});
});
}); });
import { shallowMount, mount } from '@vue/test-utils'; import { shallowMount, mount } from '@vue/test-utils';
import Tracking from '~/tracking';
import { ESC_KEY, ESC_KEY_IE11 } from '~/lib/utils/keys'; import { ESC_KEY, ESC_KEY_IE11 } from '~/lib/utils/keys';
import { GlModal } from '@gitlab/ui';
import { objectToQuery } from '~/lib/utils/url_utility'; import { objectToQuery } from '~/lib/utils/url_utility';
import VueDraggable from 'vuedraggable'; import VueDraggable from 'vuedraggable';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
...@@ -10,7 +8,6 @@ import { dashboardEmptyStates, metricStates } from '~/monitoring/constants'; ...@@ -10,7 +8,6 @@ import { dashboardEmptyStates, metricStates } from '~/monitoring/constants';
import Dashboard from '~/monitoring/components/dashboard.vue'; import Dashboard from '~/monitoring/components/dashboard.vue';
import DashboardHeader from '~/monitoring/components/dashboard_header.vue'; import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
import EmptyState from '~/monitoring/components/empty_state.vue'; import EmptyState from '~/monitoring/components/empty_state.vue';
import GroupEmptyState from '~/monitoring/components/group_empty_state.vue'; import GroupEmptyState from '~/monitoring/components/group_empty_state.vue';
import DashboardPanel from '~/monitoring/components/dashboard_panel.vue'; import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
...@@ -42,8 +39,6 @@ describe('Dashboard', () => { ...@@ -42,8 +39,6 @@ describe('Dashboard', () => {
let wrapper; let wrapper;
let mock; let mock;
const findDashboardHeader = () => wrapper.find(DashboardHeader);
const createShallowWrapper = (props = {}, options = {}) => { const createShallowWrapper = (props = {}, options = {}) => {
wrapper = shallowMount(Dashboard, { wrapper = shallowMount(Dashboard, {
propsData: { ...dashboardProps, ...props }, propsData: { ...dashboardProps, ...props },
...@@ -446,84 +441,6 @@ describe('Dashboard', () => { ...@@ -446,84 +441,6 @@ describe('Dashboard', () => {
}); });
}); });
describe('star dashboards', () => {
const findToggleStar = () => findDashboardHeader().find({ ref: 'toggleStarBtn' });
beforeEach(() => {
createShallowWrapper();
setupAllDashboards(store);
});
it('toggle star button is shown', () => {
expect(findToggleStar().exists()).toBe(true);
expect(findToggleStar().props('disabled')).toBe(false);
});
it('toggle star button is disabled when starring is taking place', () => {
store.commit(`monitoringDashboard/${types.REQUEST_DASHBOARD_STARRING}`);
return wrapper.vm.$nextTick(() => {
expect(findToggleStar().exists()).toBe(true);
expect(findToggleStar().props('disabled')).toBe(true);
});
});
describe('when the dashboard list is loaded', () => {
// Tooltip element should wrap directly
const getToggleTooltip = () => findToggleStar().element.parentElement.getAttribute('title');
beforeEach(() => {
setupAllDashboards(store);
jest.spyOn(store, 'dispatch');
});
it('dispatches a toggle star action', () => {
findToggleStar().vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(store.dispatch).toHaveBeenCalledWith(
'monitoringDashboard/toggleStarredValue',
undefined,
);
});
});
describe('when dashboard is not starred', () => {
beforeEach(() => {
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
currentDashboard: dashboardGitResponse[0].path,
});
return wrapper.vm.$nextTick();
});
it('toggle star button shows "Star dashboard"', () => {
expect(getToggleTooltip()).toBe('Star dashboard');
});
it('toggle star button shows an unstarred state', () => {
expect(findToggleStar().attributes('icon')).toBe('star-o');
});
});
describe('when dashboard is starred', () => {
beforeEach(() => {
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
currentDashboard: dashboardGitResponse[1].path,
});
return wrapper.vm.$nextTick();
});
it('toggle star button shows "Star dashboard"', () => {
expect(getToggleTooltip()).toBe('Unstar dashboard');
});
it('toggle star button shows a starred state', () => {
expect(findToggleStar().attributes('icon')).toBe('star');
});
});
});
});
describe('variables section', () => { describe('variables section', () => {
beforeEach(() => { beforeEach(() => {
createShallowWrapper({ hasMetrics: true }); createShallowWrapper({ hasMetrics: true });
...@@ -800,33 +717,6 @@ describe('Dashboard', () => { ...@@ -800,33 +717,6 @@ describe('Dashboard', () => {
}); });
}); });
describe('dashboard edit link', () => {
const findEditLink = () => wrapper.find('.js-edit-link');
beforeEach(() => {
createShallowWrapper({ hasMetrics: true });
setupAllDashboards(store);
return wrapper.vm.$nextTick();
});
it('is not present for the overview dashboard', () => {
expect(findEditLink().exists()).toBe(false);
});
it('is present for a custom dashboard, and links to its edit_path', () => {
const dashboard = dashboardGitResponse[1];
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
currentDashboard: dashboard.path,
});
return wrapper.vm.$nextTick().then(() => {
expect(findEditLink().exists()).toBe(true);
expect(findEditLink().attributes('href')).toBe(dashboard.project_blob_path);
});
});
});
describe('document title', () => { describe('document title', () => {
const originalTitle = 'Original Title'; const originalTitle = 'Original Title';
const overviewDashboardName = dashboardGitResponse[0].display_name; const overviewDashboardName = dashboardGitResponse[0].display_name;
...@@ -940,74 +830,4 @@ describe('Dashboard', () => { ...@@ -940,74 +830,4 @@ describe('Dashboard', () => {
expect(dashboardPanel.exists()).toBe(true); expect(dashboardPanel.exists()).toBe(true);
}); });
}); });
describe('add custom metrics', () => {
const findAddMetricButton = () => findDashboardHeader().find({ ref: 'addMetricBtn' });
describe('when not available', () => {
beforeEach(() => {
createShallowWrapper({
hasMetrics: true,
customMetricsPath: '/endpoint',
});
});
it('does not render add button on the dashboard', () => {
expect(findAddMetricButton().exists()).toBe(false);
});
});
describe('when available', () => {
let origPage;
beforeEach(done => {
jest.spyOn(Tracking, 'event').mockReturnValue();
createShallowWrapper({
hasMetrics: true,
customMetricsPath: '/endpoint',
customMetricsAvailable: true,
});
setupStoreWithData(store);
origPage = document.body.dataset.page;
document.body.dataset.page = 'projects:environments:metrics';
wrapper.vm.$nextTick(done);
});
afterEach(() => {
document.body.dataset.page = origPage;
});
it('renders add button on the dashboard', () => {
expect(findAddMetricButton()).toBeDefined();
});
it('uses modal for custom metrics form', () => {
expect(wrapper.find(GlModal).exists()).toBe(true);
expect(wrapper.find(GlModal).attributes().modalid).toBe('addMetric');
});
it('adding new metric is tracked', done => {
const submitButton = wrapper
.find(DashboardHeader)
.find({ ref: 'submitCustomMetricsFormBtn' }).vm;
wrapper.vm.$nextTick(() => {
submitButton.$el.click();
wrapper.vm.$nextTick(() => {
expect(Tracking.event).toHaveBeenCalledWith(
document.body.dataset.page,
'click_button',
{
label: 'add_new_metric',
property: 'modal',
value: undefined,
},
);
done();
});
});
});
it('renders custom metrics form fields', () => {
expect(wrapper.find(CustomMetricsFormFields).exists()).toBe(true);
});
});
});
}); });
...@@ -622,3 +622,10 @@ export const dashboardHeaderProps = { ...@@ -622,3 +622,10 @@ export const dashboardHeaderProps = {
end: '2020-01-01T01:00:00.000Z', end: '2020-01-01T01:00:00.000Z',
}, },
}; };
export const dashboardActionsMenuProps = {
defaultBranch: 'master',
addingMetricsAvailable: true,
customMetricsPath: 'https://path/to/customMetrics',
validateQueryPath: 'https://path/to/validateQuery',
};
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