diff options
Diffstat (limited to 'app/assets/javascripts/monitoring/components')
18 files changed, 747 insertions, 207 deletions
diff --git a/app/assets/javascripts/monitoring/components/charts/anomaly.vue b/app/assets/javascripts/monitoring/components/charts/anomaly.vue index 34da5885c97..ac401c6e381 100644 --- a/app/assets/javascripts/monitoring/components/charts/anomaly.vue +++ b/app/assets/javascripts/monitoring/components/charts/anomaly.vue @@ -218,7 +218,7 @@ export default { <gl-chart-series-label :color="content.color"> {{ content.name }} </gl-chart-series-label> - <div class="prepend-left-32"> + <div class="gl-ml-7"> {{ yValueFormatted(seriesIndex, content.dataIndex) }} </div> </div> diff --git a/app/assets/javascripts/monitoring/components/charts/heatmap.vue b/app/assets/javascripts/monitoring/components/charts/heatmap.vue index f6f266dacf3..ddb44f7b1be 100644 --- a/app/assets/javascripts/monitoring/components/charts/heatmap.vue +++ b/app/assets/javascripts/monitoring/components/charts/heatmap.vue @@ -48,7 +48,10 @@ export default { return this.result.values.map(val => { const [yLabel] = val; - return formatDate(new Date(yLabel), { format: formats.shortTime, timezone: this.timezone }); + return formatDate(new Date(yLabel), { + format: formats.shortTime, + timezone: this.timezone, + }); }); }, result() { diff --git a/app/assets/javascripts/monitoring/components/charts/options.js b/app/assets/javascripts/monitoring/components/charts/options.js index f7822e69b1d..42252dd5897 100644 --- a/app/assets/javascripts/monitoring/components/charts/options.js +++ b/app/assets/javascripts/monitoring/components/charts/options.js @@ -17,7 +17,9 @@ const defaultTooltipFormat = defaultFormat; const defaultTooltipPrecision = 3; // Give enough space for y-axis with units and name. -const chartGridLeft = 75; +const chartGridLeft = 63; // larger gap than gitlab-ui's default to fit formatted numbers +const chartGridRight = 10; // half of the scroll-handle icon for data zoom +const yAxisNameGap = chartGridLeft - 12; // offset the axis label line-height // Axis options @@ -62,7 +64,7 @@ export const getYAxisOptions = ({ precision = defaultYAxisPrecision, } = {}) => { return { - nameGap: 63, // larger gap than gitlab-ui's default to fit with formatted numbers + nameGap: yAxisNameGap, scale: true, boundaryGap: yAxisBoundaryGap, @@ -74,11 +76,14 @@ export const getYAxisOptions = ({ }; }; -export const getTimeAxisOptions = ({ timezone = timezones.LOCAL } = {}) => ({ +export const getTimeAxisOptions = ({ + timezone = timezones.LOCAL, + format = formats.shortDateTime, +} = {}) => ({ name: __('Time'), type: axisTypes.time, axisLabel: { - formatter: date => formatDate(date, { format: formats.shortTime, timezone }), + formatter: date => formatDate(date, { format, timezone }), }, axisPointer: { snap: false, @@ -90,7 +95,10 @@ export const getTimeAxisOptions = ({ timezone = timezones.LOCAL } = {}) => ({ /** * Grid with enough room to display chart. */ -export const getChartGrid = ({ left = chartGridLeft } = {}) => ({ left }); +export const getChartGrid = ({ left = chartGridLeft, right = chartGridRight } = {}) => ({ + left, + right, +}); // Tooltip options diff --git a/app/assets/javascripts/monitoring/components/charts/single_stat.vue b/app/assets/javascripts/monitoring/components/charts/single_stat.vue index eee5eaa5eca..106c76a97dc 100644 --- a/app/assets/javascripts/monitoring/components/charts/single_stat.vue +++ b/app/assets/javascripts/monitoring/components/charts/single_stat.vue @@ -1,9 +1,11 @@ <script> import { GlSingleStat } from '@gitlab/ui/dist/charts'; +import { __ } from '~/locale'; import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format'; import { graphDataValidatorForValues } from '../../utils'; const defaultPrecision = 2; +const emptyStateMsg = __('No data to display'); export default { components: { @@ -21,6 +23,9 @@ export default { queryInfo() { return this.graphData.metrics[0]; }, + queryMetric() { + return this.queryInfo.result[0]?.metric; + }, queryResult() { return this.queryInfo.result[0]?.value[1]; }, @@ -33,6 +38,12 @@ export default { statValue() { let formatter; + // if field is present the metric value is not displayed. Hence + // the early exit without formatting. + if (this.graphData?.field) { + return this.queryMetric?.[this.graphData.field] ?? emptyStateMsg; + } + if (this.graphData?.maxValue) { formatter = getFormatter(SUPPORTED_FORMATS.percent); return formatter(this.queryResult / Number(this.graphData.maxValue), defaultPrecision); diff --git a/app/assets/javascripts/monitoring/components/charts/stacked_column.vue b/app/assets/javascripts/monitoring/components/charts/stacked_column.vue index ac31d107e63..9bcd4419a14 100644 --- a/app/assets/javascripts/monitoring/components/charts/stacked_column.vue +++ b/app/assets/javascripts/monitoring/components/charts/stacked_column.vue @@ -6,7 +6,7 @@ import { chartHeight, legendLayoutTypes } from '../../constants'; import { s__ } from '~/locale'; import { graphDataValidatorForValues } from '../../utils'; import { getTimeAxisOptions, axisTypes } from './options'; -import { timezones } from '../../format_date'; +import { formats, timezones } from '../../format_date'; export default { components: { @@ -97,7 +97,7 @@ export default { chartOptions() { return { xAxis: { - ...getTimeAxisOptions({ timezone: this.timezone }), + ...getTimeAxisOptions({ timezone: this.timezone, format: formats.shortTime }), type: this.xAxisType, }, dataZoom: [this.dataZoomConfig], diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue index 28af2d8ba77..f2add429a80 100644 --- a/app/assets/javascripts/monitoring/components/charts/time_series.vue +++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue @@ -415,7 +415,7 @@ export default { <gl-chart-series-label :color="isMultiSeries ? content.color : ''"> {{ content.name }} </gl-chart-series-label> - <div class="prepend-left-32"> + <div class="gl-ml-7"> {{ content.value }} </div> </div> diff --git a/app/assets/javascripts/monitoring/components/create_dashboard_modal.vue b/app/assets/javascripts/monitoring/components/create_dashboard_modal.vue new file mode 100644 index 00000000000..74799002b17 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/create_dashboard_modal.vue @@ -0,0 +1,66 @@ +<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> diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index f54319d283e..bde62275797 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -1,6 +1,7 @@ <script> import { mapActions, mapState, mapGetters } from 'vuex'; import VueDraggable from 'vuedraggable'; +import Mousetrap from 'mousetrap'; import { GlIcon, GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui'; import DashboardHeader from './dashboard_header.vue'; import DashboardPanel from './dashboard_panel.vue'; @@ -24,7 +25,7 @@ import { expandedPanelPayloadFromUrl, convertVariablesForURL, } from '../utils'; -import { metricStates } from '../constants'; +import { metricStates, keyboardShortcutKeys } from '../constants'; import { defaultTimeRange } from '~/vue_shared/constants'; export default { @@ -71,6 +72,10 @@ export default { type: String, required: true, }, + addDashboardDocumentationPath: { + type: String, + required: true, + }, settingsPath: { type: String, required: true, @@ -149,21 +154,25 @@ export default { selectedTimeRange: timeRangeFromUrl() || defaultTimeRange, isRearrangingPanels: false, originalDocumentTitle: document.title, + hoveredPanel: '', }; }, computed: { ...mapState('monitoringDashboard', [ 'dashboard', 'emptyState', - 'showEmptyState', 'expandedPanel', 'variables', 'links', 'currentDashboard', + 'hasDashboardValidationWarnings', ]), ...mapGetters('monitoringDashboard', ['selectedDashboard', 'getMetricStates']), + shouldShowEmptyState() { + return Boolean(this.emptyState); + }, shouldShowVariablesSection() { - return Object.keys(this.variables).length > 0; + return Boolean(this.variables.length); }, shouldShowLinksSection() { return Object.keys(this.links).length > 0; @@ -197,12 +206,29 @@ export default { selectedDashboard(dashboard) { this.prependToDocumentTitle(dashboard?.display_name); }, + hasDashboardValidationWarnings(hasWarnings) { + /** + * This watcher is set for future SPA behaviour of the dashboard + */ + if (hasWarnings) { + createFlash( + s__( + 'Metrics|Your dashboard schema is invalid. Edit the dashboard to correct the YAML schema.', + ), + 'warning', + ); + } + }, }, created() { window.addEventListener('keyup', this.onKeyup); + + Mousetrap.bind(Object.values(keyboardShortcutKeys), this.runShortcut); }, destroyed() { window.removeEventListener('keyup', this.onKeyup); + + Mousetrap.unbind(Object.values(keyboardShortcutKeys)); }, mounted() { if (!this.hasMetrics) { @@ -254,6 +280,14 @@ export default { return null; }, /** + * Return true if the entire group is loading. + * @param {String} groupKey - Identifier for group + * @returns {boolean} + */ + isGroupLoading(groupKey) { + return this.groupSingleEmptyState(groupKey) === metricStates.LOADING; + }, + /** * A group should be not collapsed if any metric is loaded (OK) * * @param {String} groupKey - Identifier for group @@ -302,6 +336,66 @@ export default { // As a fallback, switch to default time range instead this.selectedTimeRange = defaultTimeRange; }, + isPanelHalfWidth(panelIndex, totalPanels) { + /** + * A single panel on a row should take the full width of its parent. + * All others should have half the width their parent. + */ + const isNumberOfPanelsEven = totalPanels % 2 === 0; + const isLastPanel = panelIndex === totalPanels - 1; + + return isNumberOfPanelsEven || !isLastPanel; + }, + /** + * TODO: Investigate this to utilize the eventBus from Vue + * The intentation behind this cleanup is to allow for better tests + * as well as use the correct eventBus facilities that are compatible + * with Vue 3 + * https://gitlab.com/gitlab-org/gitlab/-/issues/225583 + */ + // + runShortcut(e) { + const panel = this.$refs[this.hoveredPanel]; + + if (!panel) return; + + const [panelInstance] = panel; + let actionToRun = ''; + + switch (e.key) { + case keyboardShortcutKeys.EXPAND: + actionToRun = 'onExpandFromKeyboardShortcut'; + break; + + case keyboardShortcutKeys.VISIT_LOGS: + actionToRun = 'visitLogsPageFromKeyboardShortcut'; + break; + + case keyboardShortcutKeys.SHOW_ALERT: + actionToRun = 'showAlertModalFromKeyboardShortcut'; + break; + + case keyboardShortcutKeys.DOWNLOAD_CSV: + actionToRun = 'downloadCsvFromKeyboardShortcut'; + break; + + case keyboardShortcutKeys.CHART_COPY: + actionToRun = 'copyChartLinkFromKeyboardShotcut'; + break; + + default: + actionToRun = 'onExpandFromKeyboardShortcut'; + break; + } + + panelInstance[actionToRun](); + }, + setHoveredPanel(groupKey, graphIndex) { + this.hoveredPanel = `dashboard-panel-${groupKey}-${graphIndex}`; + }, + clearHoveredPanel() { + this.hoveredPanel = ''; + }, }, i18n: { goBackLabel: s__('Metrics|Go back (Esc)'), @@ -315,6 +409,7 @@ export default { v-if="showHeader" 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" + :add-dashboard-documentation-path="addDashboardDocumentationPath" :default-branch="defaultBranch" :rearrange-panels-available="rearrangePanelsAvailable" :custom-metrics-available="customMetricsAvailable" @@ -327,9 +422,9 @@ export default { @dateTimePickerInvalid="onDateTimePickerInvalid" @setRearrangingPanels="onSetRearrangingPanels" /> - <variables-section v-if="shouldShowVariablesSection && !showEmptyState" /> - <links-section v-if="shouldShowLinksSection && !showEmptyState" /> - <div v-if="!showEmptyState"> + <template v-if="!shouldShowEmptyState"> + <variables-section v-if="shouldShowVariablesSection" /> + <links-section v-if="shouldShowLinksSection" /> <dashboard-panel v-show="expandedPanel.panel" ref="expandedPanel" @@ -364,6 +459,7 @@ export default { :key="`${groupData.group}.${groupData.priority}`" :name="groupData.group" :show-panels="showPanels" + :is-loading="isGroupLoading(groupData.key)" :collapse-group="collapseGroup(groupData.key)" > <vue-draggable @@ -377,8 +473,14 @@ export default { <div v-for="(graphData, graphIndex) in groupData.panels" :key="`dashboard-panel-${graphIndex}`" - class="col-12 col-lg-6 px-2 mb-2 draggable" - :class="{ 'draggable-enabled': isRearrangingPanels }" + data-testid="dashboard-panel-layout-wrapper" + class="col-12 px-2 mb-2 draggable" + :class="{ + 'draggable-enabled': isRearrangingPanels, + 'col-lg-6': isPanelHalfWidth(graphIndex, groupData.panels.length), + }" + @mouseover="setHoveredPanel(groupData.key, graphIndex)" + @mouseout="clearHoveredPanel" > <div class="position-relative draggable-panel js-draggable-panel"> <div @@ -392,6 +494,7 @@ export default { </div> <dashboard-panel + :ref="`dashboard-panel-${groupData.key}-${graphIndex}`" :settings-path="settingsPath" :clipboard-text="generatePanelUrl(groupData.group, graphData)" :graph-data="graphData" @@ -414,7 +517,7 @@ export default { </div> </graph-group> </div> - </div> + </template> <empty-state v-else :selected-state="emptyState" diff --git a/app/assets/javascripts/monitoring/components/dashboard_header.vue b/app/assets/javascripts/monitoring/components/dashboard_header.vue index 16a21ae0d3c..fe6ca3a2a07 100644 --- a/app/assets/javascripts/monitoring/components/dashboard_header.vue +++ b/app/assets/javascripts/monitoring/components/dashboard_header.vue @@ -2,12 +2,16 @@ import { debounce } from 'lodash'; import { mapActions, mapState, mapGetters } from 'vuex'; import { + GlButton, GlIcon, GlDeprecatedButton, GlDropdown, GlDropdownItem, GlDropdownHeader, GlDropdownDivider, + GlNewDropdown, + GlNewDropdownDivider, + GlNewDropdownItem, GlModal, GlLoadingIcon, GlSearchBoxByType, @@ -22,6 +26,9 @@ import Icon from '~/vue_shared/components/icon.vue'; import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue'; import DashboardsDropdown from './dashboards_dropdown.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 { getAddMetricTrackingOptions, timeRangeToUrl } from '../utils'; @@ -31,6 +38,7 @@ import { timezones } from '../format_date'; export default { components: { Icon, + GlButton, GlIcon, GlDeprecatedButton, GlDropdown, @@ -38,12 +46,18 @@ export default { GlDropdownItem, GlDropdownHeader, GlDropdownDivider, + GlNewDropdown, + GlNewDropdownDivider, + GlNewDropdownItem, GlSearchBoxByType, GlModal, CustomMetricsFormFields, DateTimePicker, DashboardsDropdown, + RefreshButton, + DuplicateDashboardModal, + CreateDashboardModal, }, directives: { GlModal: GlModalDirective, @@ -93,6 +107,10 @@ export default { type: Object, required: true, }, + addDashboardDocumentationPath: { + type: String, + required: true, + }, }, data() { return { @@ -101,20 +119,30 @@ export default { }, computed: { ...mapState('monitoringDashboard', [ + 'emptyState', 'environmentsLoading', 'currentEnvironmentName', 'isUpdatingStarredValue', - 'showEmptyState', 'dashboardTimezone', + 'projectPath', + 'canAccessOperationsSettings', + 'operationsSettingsPath', + 'currentDashboard', ]), ...mapGetters('monitoringDashboard', ['selectedDashboard', 'filteredEnvironments']), + isOutOfTheBoxDashboard() { + return this.selectedDashboard?.out_of_the_box_dashboard; + }, + shouldShowEmptyState() { + return Boolean(this.emptyState); + }, shouldShowEnvironmentsDropdownNoMatchedMsg() { return !this.environmentsLoading && this.filteredEnvironments.length === 0; }, addingMetricsAvailable() { return ( this.customMetricsAvailable && - !this.showEmptyState && + !this.shouldShowEmptyState && // Custom metrics only avaialble on system dashboards because // they are stored in the database. This can be improved. See: // https://gitlab.com/gitlab-org/gitlab/-/issues/28241 @@ -122,23 +150,29 @@ export default { ); }, showRearrangePanelsBtn() { - return !this.showEmptyState && this.rearrangePanelsAvailable; + return !this.shouldShowEmptyState && this.rearrangePanelsAvailable; }, displayUtc() { return this.dashboardTimezone === timezones.UTC; }, + shouldShowActionsMenu() { + return Boolean(this.projectPath); + }, + shouldShowSettingsButton() { + return this.canAccessOperationsSettings && this.operationsSettingsPath; + }, }, methods: { - ...mapActions('monitoringDashboard', [ - 'filterEnvironments', - 'fetchDashboardData', - 'toggleStarredValue', - ]), + ...mapActions('monitoringDashboard', ['filterEnvironments', 'toggleStarredValue']), selectDashboard(dashboard) { - const params = { - dashboard: dashboard.path, - }; - redirectTo(mergeUrlParams(params, window.location.href)); + // 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}`); }, debouncedEnvironmentsSearch: debounce(function environmentsSearchOnInput(searchTerm) { this.filterEnvironments(searchTerm); @@ -149,9 +183,6 @@ export default { onDateTimePickerInvalid() { this.$emit('dateTimePickerInvalid'); }, - refreshDashboard() { - this.fetchDashboardData(); - }, toggleRearrangingPanels() { this.$emit('setRearrangingPanels', !this.isRearrangingPanels); @@ -166,14 +197,27 @@ export default { submitCustomMetricsForm() { this.$refs.customMetricsForm.submit(); }, + getEnvironmentPath(environment) { + // 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(this.currentDashboard || ''); + // The environment_metrics_spec.rb requires the URL to not have + // slashes. Hence, this additional check. + const url = dashboardPath ? `${baseURL}/${dashboardPath}` : baseURL; + return mergeUrlParams({ environment }, url); + }, }, - addMetric: { - title: s__('Metrics|Add metric'), - modalId: 'add-metric', + modalIds: { + addMetric: 'addMetric', + createDashboard: 'createDashboard', + duplicateDashboard: 'duplicateDashboard', }, i18n: { starDashboard: s__('Metrics|Star dashboard'), unstarDashboard: s__('Metrics|Unstar dashboard'), + addMetric: s__('Metrics|Add metric'), }, timeRanges, }; @@ -181,17 +225,20 @@ export default { <template> <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 id="monitor-dashboards-dropdown" data-qa-selector="dashboards_filter_dropdown" class="flex-grow-1" toggle-class="dropdown-menu-toggle" :default-branch="defaultBranch" + :modal-id="$options.modalIds.duplicateDashboard" @selectDashboard="selectDashboard" /> </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"> <gl-dropdown id="monitor-environments-dropdown" @@ -223,7 +270,7 @@ export default { :key="environment.id" :active="environment.name === currentEnvironmentName" active-class="is-active" - :href="environment.metrics_path" + :href="getEnvironmentPath(environment.id)" >{{ environment.name }}</gl-dropdown-item > </div> @@ -252,16 +299,7 @@ export default { </div> <div class="mb-2 pr-2 d-flex d-sm-block"> - <gl-deprecated-button - ref="refreshDashboardBtn" - v-gl-tooltip - class="flex-grow-1" - variant="default" - :title="s__('Metrics|Refresh dashboard')" - @click="refreshDashboard" - > - <icon name="retry" /> - </gl-deprecated-button> + <refresh-button /> </div> <div class="flex-grow-1"></div> @@ -304,17 +342,17 @@ export default { <div v-if="addingMetricsAvailable" class="mb-2 mr-2 d-flex d-sm-block"> <gl-deprecated-button ref="addMetricBtn" - v-gl-modal="$options.addMetric.modalId" + v-gl-modal="$options.modalIds.addMetric" variant="outline-success" data-qa-selector="add_metric_button" class="flex-grow-1" > - {{ $options.addMetric.title }} + {{ $options.i18n.addMetric }} </gl-deprecated-button> <gl-modal ref="addMetricModal" - :modal-id="$options.addMetric.modalId" - :title="$options.addMetric.title" + :modal-id="$options.modalIds.addMetric" + :title="$options.i18n.addMetric" > <form ref="customMetricsForm" :action="customMetricsPath" method="post"> <custom-metrics-form-fields @@ -353,7 +391,10 @@ export default { </gl-deprecated-button> </div> - <div v-if="externalDashboardUrl.length" class="mb-2 mr-2 d-flex d-sm-block"> + <div + v-if="externalDashboardUrl && externalDashboardUrl.length" + class="mb-2 mr-2 d-flex d-sm-block" + > <gl-deprecated-button class="flex-grow-1 js-external-dashboard-link" variant="primary" @@ -364,6 +405,63 @@ export default { {{ __('View full dashboard') }} <icon name="external-link" /> </gl-deprecated-button> </div> + + <!-- This separator should be displayed only if at least one of the action menu or settings button are displayed --> + <span + v-if="shouldShowActionsMenu || shouldShowSettingsButton" + aria-hidden="true" + class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block" + ></span> + + <div v-if="shouldShowActionsMenu" 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="isOutOfTheBoxDashboard"> + <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> + + <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> + <duplicate-dashboard-modal + :default-branch="defaultBranch" + :modal-id="$options.modalIds.duplicateDashboard" + @dashboardDuplicated="selectDashboard" + /> </div> </template> diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel.vue b/app/assets/javascripts/monitoring/components/dashboard_panel.vue index 9545a211bbd..3e3c8408de3 100644 --- a/app/assets/javascripts/monitoring/components/dashboard_panel.vue +++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue @@ -2,6 +2,7 @@ import { mapState } from 'vuex'; import { pickBy } from 'lodash'; import invalidUrl from '~/lib/utils/invalid_url'; +import { relativePathToAbsolute, getBaseURL, visitUrl, isSafeURL } from '~/lib/utils/url_utility'; import { GlResizeObserverDirective, GlIcon, @@ -29,7 +30,6 @@ import MonitorStackedColumnChart from './charts/stacked_column.vue'; import TrackEventDirective from '~/vue_shared/directives/track_event'; import AlertWidget from './alert_widget.vue'; import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '../utils'; -import { isSafeURL } from '~/lib/utils/url_utility'; const events = { timeRangeZoom: 'timerangezoom', @@ -132,7 +132,8 @@ export default { return this.graphData?.title || ''; }, graphDataHasResult() { - return this.graphData?.metrics?.[0]?.result?.length > 0; + const metrics = this.graphData?.metrics || []; + return metrics.some(({ result }) => result?.length > 0); }, graphDataIsLoading() { const metrics = this.graphData?.metrics || []; @@ -207,7 +208,17 @@ export default { return MonitorTimeSeriesChart; }, isContextualMenuShown() { - return Boolean(this.graphDataHasResult && !this.basicChartComponent); + if (!this.graphDataHasResult) { + return false; + } + // Only a few charts have a contextual menu, support + // for more chart types planned at: + // https://gitlab.com/groups/gitlab-org/-/epics/3573 + return ( + this.isPanelType(panelTypes.AREA_CHART) || + this.isPanelType(panelTypes.LINE_CHART) || + this.isPanelType(panelTypes.SINGLE_STAT) + ); }, editCustomMetricLink() { if (this.graphData.metrics.length > 1) { @@ -223,13 +234,19 @@ export default { return metrics.some(({ metricId }) => this.metricsSavedToDb.includes(metricId)); }, alertWidgetAvailable() { + const supportsAlerts = + this.isPanelType(panelTypes.AREA_CHART) || this.isPanelType(panelTypes.LINE_CHART); return ( + supportsAlerts && this.prometheusAlertsAvailable && this.alertsEndpoint && this.graphData && this.hasMetricsInDb ); }, + alertModalId() { + return `alert-modal-${this.graphData.id}`; + }, }, mounted() { this.refreshTitleTooltip(); @@ -268,6 +285,11 @@ export default { onExpand() { this.$emit(events.expand); }, + onExpandFromKeyboardShortcut() { + if (this.isContextualMenuShown) { + this.onExpand(); + } + }, setAlerts(alertPath, alertAttributes) { if (alertAttributes) { this.$set(this.allAlerts, alertPath, alertAttributes); @@ -278,18 +300,45 @@ export default { safeUrl(url) { return isSafeURL(url) ? url : '#'; }, + showAlertModal() { + this.$root.$emit('bv::show::modal', this.alertModalId); + }, + showAlertModalFromKeyboardShortcut() { + if (this.isContextualMenuShown) { + this.showAlertModal(); + } + }, + visitLogsPage() { + if (this.logsPathWithTimeRange) { + visitUrl(relativePathToAbsolute(this.logsPathWithTimeRange, getBaseURL())); + } + }, + visitLogsPageFromKeyboardShortcut() { + if (this.isContextualMenuShown) { + this.visitLogsPage(); + } + }, + downloadCsvFromKeyboardShortcut() { + if (this.csvText && this.isContextualMenuShown) { + this.$refs.downloadCsvLink.$el.firstChild.click(); + } + }, + copyChartLinkFromKeyboardShotcut() { + if (this.clipboardText && this.isContextualMenuShown) { + this.$refs.copyChartLink.$el.firstChild.click(); + } + }, }, panelTypes, }; </script> <template> <div v-gl-resize-observer="onResize" class="prometheus-graph"> - <div class="d-flex align-items-center mr-3"> + <div class="d-flex align-items-center"> <slot name="topLeft"></slot> <h5 ref="graphTitle" class="prometheus-graph-title gl-font-lg font-weight-bold text-truncate gl-mr-3" - tabindex="0" > {{ title }} </h5> @@ -299,7 +348,7 @@ export default { <alert-widget v-if="isContextualMenuShown && alertWidgetAvailable" class="mx-1" - :modal-id="`alert-modal-${graphData.id}`" + :modal-id="alertModalId" :alerts-endpoint="alertsEndpoint" :relevant-queries="graphData.metrics" :alerts-to-manage="getGraphAlerts(graphData.metrics)" @@ -314,7 +363,7 @@ export default { ref="contextualMenu" data-qa-selector="prometheus_graph_widgets" > - <div class="d-flex align-items-center"> + <div data-testid="dropdown-wrapper" class="d-flex align-items-center"> <gl-dropdown v-gl-tooltip toggle-class="shadow-none border-0" @@ -369,13 +418,13 @@ export default { </gl-dropdown-item> <gl-dropdown-item v-if="alertWidgetAvailable" - v-gl-modal="`alert-modal-${graphData.id}`" + v-gl-modal="alertModalId" data-qa-selector="alert_widget_menu_item" > {{ __('Alerts') }} </gl-dropdown-item> - <template v-if="graphData.links.length"> + <template v-if="graphData.links && graphData.links.length"> <gl-dropdown-divider /> <gl-dropdown-item v-for="(link, index) in graphData.links" diff --git a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue index 8b86890715f..574f48a72fe 100644 --- a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue +++ b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue @@ -1,19 +1,14 @@ <script> import { mapState, mapActions, mapGetters } from 'vuex'; import { - GlAlert, GlIcon, GlDropdown, GlDropdownItem, GlDropdownHeader, GlDropdownDivider, GlSearchBoxByType, - GlModal, - GlLoadingIcon, GlModalDirective, } from '@gitlab/ui'; -import { s__ } from '~/locale'; -import DuplicateDashboardForm from './duplicate_dashboard_form.vue'; const events = { selectDashboard: 'selectDashboard', @@ -21,16 +16,12 @@ const events = { export default { components: { - GlAlert, GlIcon, GlDropdown, GlDropdownItem, GlDropdownHeader, GlDropdownDivider, GlSearchBoxByType, - GlModal, - GlLoadingIcon, - DuplicateDashboardForm, }, directives: { GlModal: GlModalDirective, @@ -40,20 +31,21 @@ export default { type: String, required: true, }, + modalId: { + type: String, + required: true, + }, }, data() { return { - alert: null, - loading: false, - form: {}, searchTerm: '', }; }, computed: { ...mapState('monitoringDashboard', ['allDashboards']), ...mapGetters('monitoringDashboard', ['selectedDashboard']), - isSystemDashboard() { - return this.selectedDashboard?.system_dashboard; + isOutOfTheBoxDashboard() { + return this.selectedDashboard?.out_of_the_box_dashboard; }, selectedDashboardText() { return this.selectedDashboard?.display_name; @@ -76,10 +68,6 @@ export default { nonStarredDashboards() { return this.filteredDashboards.filter(({ starred }) => !starred); }, - - okButtonText() { - return this.loading ? s__('Metrics|Duplicating...') : s__('Metrics|Duplicate'); - }, }, methods: { ...mapActions('monitoringDashboard', ['duplicateSystemDashboard']), @@ -89,37 +77,6 @@ export default { 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> @@ -178,32 +135,14 @@ export default { {{ __('No matching results') }} </div> - <template v-if="isSystemDashboard"> + <!-- + This Duplicate Dashboard item will be removed from the dashboards dropdown + in https://gitlab.com/gitlab-org/gitlab/-/issues/223223 + --> + <template v-if="isOutOfTheBoxDashboard"> <gl-dropdown-divider /> - <gl-modal - ref="duplicateDashboardModal" - modal-id="duplicateDashboardModal" - :title="s__('Metrics|Duplicate dashboard')" - ok-variant="success" - @ok="ok" - @hide="hide" - > - <gl-alert v-if="alert" class="mb-3" variant="danger" @dismiss="alert = null"> - {{ alert }} - </gl-alert> - <duplicate-dashboard-form - :dashboard="selectedDashboard" - :default-branch="defaultBranch" - @change="formChange" - /> - <template #modal-ok> - <gl-loading-icon v-if="loading" inline color="light" /> - {{ okButtonText }} - </template> - </gl-modal> - - <gl-dropdown-item ref="duplicateDashboardItem" v-gl-modal="'duplicateDashboardModal'"> + <gl-dropdown-item v-gl-modal="modalId" data-testid="duplicateDashboardItem"> {{ s__('Metrics|Duplicate dashboard') }} </gl-dropdown-item> </template> diff --git a/app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue b/app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue new file mode 100644 index 00000000000..e64afc01fd9 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue @@ -0,0 +1,95 @@ +<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> diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue index d3157b731b2..5e7c9b5d906 100644 --- a/app/assets/javascripts/monitoring/components/empty_state.vue +++ b/app/assets/javascripts/monitoring/components/empty_state.vue @@ -1,12 +1,19 @@ <script> -import { GlEmptyState } from '@gitlab/ui'; +import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui'; import { __ } from '~/locale'; +import { dashboardEmptyStates } from '../constants'; export default { components: { + GlLoadingIcon, GlEmptyState, }, props: { + selectedState: { + type: String, + required: true, + validator: state => Object.values(dashboardEmptyStates).includes(state), + }, documentationPath: { type: String, required: true, @@ -21,10 +28,6 @@ export default { required: false, default: '', }, - selectedState: { - type: String, - required: true, - }, emptyGettingStartedSvgPath: { type: String, required: true, @@ -53,52 +56,49 @@ export default { }, data() { return { + /** + * Possible empty states. + * Keys in each state must match GlEmptyState props + */ states: { - gettingStarted: { - svgUrl: this.emptyGettingStartedSvgPath, + [dashboardEmptyStates.GETTING_STARTED]: { + svgPath: this.emptyGettingStartedSvgPath, title: __('Get started with performance monitoring'), description: __(`Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments.`), - buttonText: __('Install on clusters'), - buttonPath: this.clustersPath, + primaryButtonText: __('Install on clusters'), + primaryButtonLink: this.clustersPath, secondaryButtonText: __('Configure existing installation'), - secondaryButtonPath: this.settingsPath, + secondaryButtonLink: this.settingsPath, }, - loading: { - svgUrl: this.emptyLoadingSvgPath, - title: __('Waiting for performance data'), - description: __(`Creating graphs uses the data from the Prometheus server. - If this takes a long time, ensure that data is available.`), - buttonText: __('View documentation'), - buttonPath: this.documentationPath, - secondaryButtonText: '', - secondaryButtonPath: '', - }, - noData: { - svgUrl: this.emptyNoDataSvgPath, + [dashboardEmptyStates.NO_DATA]: { + svgPath: this.emptyNoDataSvgPath, title: __('No data found'), description: __(`You are connected to the Prometheus server, but there is currently no data to display.`), - buttonText: __('Configure Prometheus'), - buttonPath: this.settingsPath, + primaryButtonText: __('Configure Prometheus'), + primaryButtonLink: this.settingsPath, secondaryButtonText: '', - secondaryButtonPath: '', + secondaryButtonLink: '', }, - unableToConnect: { - svgUrl: this.emptyUnableToConnectSvgPath, + [dashboardEmptyStates.UNABLE_TO_CONNECT]: { + svgPath: this.emptyUnableToConnectSvgPath, title: __('Unable to connect to Prometheus server'), description: __( 'Ensure connectivity is available from the GitLab server to the Prometheus server', ), - buttonText: __('View documentation'), - buttonPath: this.documentationPath, + primaryButtonText: __('View documentation'), + primaryButtonLink: this.documentationPath, secondaryButtonText: __('Configure Prometheus'), - secondaryButtonPath: this.settingsPath, + secondaryButtonLink: this.settingsPath, }, }, }; }, computed: { + isLoading() { + return this.selectedState === dashboardEmptyStates.LOADING; + }, currentState() { return this.states[this.selectedState]; }, @@ -107,14 +107,8 @@ export default { </script> <template> - <gl-empty-state - :title="currentState.title" - :description="currentState.description" - :primary-button-text="currentState.buttonText" - :primary-button-link="currentState.buttonPath" - :secondary-button-text="currentState.secondaryButtonText" - :secondary-button-link="currentState.secondaryButtonPath" - :svg-path="currentState.svgUrl" - :compact="compact" - /> + <div> + <gl-loading-icon v-if="isLoading" size="xl" class="gl-my-9" /> + <gl-empty-state v-if="currentState" v-bind="currentState" :compact="compact" /> + </div> </template> diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue index 08fcfa3bc56..ecb8ef4a0d0 100644 --- a/app/assets/javascripts/monitoring/components/graph_group.vue +++ b/app/assets/javascripts/monitoring/components/graph_group.vue @@ -1,9 +1,10 @@ <script> -import Icon from '~/vue_shared/components/icon.vue'; +import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; export default { components: { - Icon, + GlLoadingIcon, + GlIcon, }, props: { name: { @@ -15,6 +16,11 @@ export default { required: false, default: true, }, + isLoading: { + type: Boolean, + required: false, + default: false, + }, /** * Initial value of collapse on mount. */ @@ -52,18 +58,21 @@ export default { </script> <template> - <div v-if="showPanels" ref="graph-group" class="card prometheus-panel" tabindex="0"> + <div v-if="showPanels" ref="graph-group" class="card prometheus-panel"> <div class="card-header d-flex align-items-center"> <h4 class="flex-grow-1">{{ name }}</h4> + <gl-loading-icon v-if="isLoading" name="loading" /> <a data-testid="group-toggle-button" + :aria-label="__('Toggle collapse')" + :icon="caretIcon" role="button" - class="js-graph-group-toggle gl-text-gray-900" + class="js-graph-group-toggle gl-display-flex gl-ml-2 gl-text-gray-900" tabindex="0" @click="collapse" @keyup.enter="collapse" > - <icon :size="16" :aria-label="__('Toggle collapse')" :name="caretIcon" /> + <gl-icon :name="caretIcon" /> </a> </div> <div diff --git a/app/assets/javascripts/monitoring/components/refresh_button.vue b/app/assets/javascripts/monitoring/components/refresh_button.vue new file mode 100644 index 00000000000..5481806c3e0 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/refresh_button.vue @@ -0,0 +1,163 @@ +<script> +import { n__, __ } from '~/locale'; +import { mapActions } from 'vuex'; + +import { + GlButtonGroup, + GlButton, + GlNewDropdown, + GlNewDropdownItem, + GlNewDropdownDivider, + GlTooltipDirective, +} from '@gitlab/ui'; + +const makeInterval = (length = 0, unit = 's') => { + const shortLabel = `${length}${unit}`; + switch (unit) { + case 'd': + return { + interval: length * 24 * 60 * 60 * 1000, + shortLabel, + label: n__('%d day', '%d days', length), + }; + case 'h': + return { + interval: length * 60 * 60 * 1000, + shortLabel, + label: n__('%d hour', '%d hours', length), + }; + case 'm': + return { + interval: length * 60 * 1000, + shortLabel, + label: n__('%d minute', '%d minutes', length), + }; + case 's': + default: + return { + interval: length * 1000, + shortLabel, + label: n__('%d second', '%d seconds', length), + }; + } +}; + +export default { + components: { + GlButtonGroup, + GlButton, + GlNewDropdown, + GlNewDropdownItem, + GlNewDropdownDivider, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + data() { + return { + refreshInterval: null, + timeoutId: null, + }; + }, + computed: { + dropdownText() { + return this.refreshInterval?.shortLabel ?? __('Off'); + }, + }, + watch: { + refreshInterval() { + if (this.refreshInterval !== null) { + this.startAutoRefresh(); + } else { + this.stopAutoRefresh(); + } + }, + }, + destroyed() { + this.stopAutoRefresh(); + }, + methods: { + ...mapActions('monitoringDashboard', ['fetchDashboardData']), + + refresh() { + this.fetchDashboardData(); + }, + startAutoRefresh() { + const schedule = () => { + if (this.refreshInterval) { + this.timeoutId = setTimeout(this.startAutoRefresh, this.refreshInterval.interval); + } + }; + + this.stopAutoRefresh(); + if (document.hidden) { + // Inactive tab? Skip fetch and schedule again + schedule(); + } else { + // Active tab! Fetch data and then schedule when settled + // eslint-disable-next-line promise/catch-or-return + this.fetchDashboardData().finally(schedule); + } + }, + stopAutoRefresh() { + clearTimeout(this.timeoutId); + this.timeoutId = null; + }, + + setRefreshInterval(option) { + this.refreshInterval = option; + }, + removeRefreshInterval() { + this.refreshInterval = null; + }, + isChecked(option) { + if (this.refreshInterval) { + return option.interval === this.refreshInterval.interval; + } + return false; + }, + }, + + refreshIntervals: [ + makeInterval(5), + makeInterval(10), + makeInterval(30), + makeInterval(5, 'm'), + makeInterval(30, 'm'), + makeInterval(1, 'h'), + makeInterval(2, 'h'), + makeInterval(12, 'h'), + makeInterval(1, 'd'), + ], +}; +</script> + +<template> + <gl-button-group> + <gl-button + v-gl-tooltip + class="gl-flex-grow-1" + variant="default" + :title="s__('Metrics|Refresh dashboard')" + icon="retry" + @click="refresh" + /> + <gl-new-dropdown v-gl-tooltip :title="s__('Metrics|Set refresh rate')" :text="dropdownText"> + <gl-new-dropdown-item + :is-check-item="true" + :is-checked="refreshInterval === null" + @click="removeRefreshInterval()" + >{{ __('Off') }}</gl-new-dropdown-item + > + <gl-new-dropdown-divider /> + <gl-new-dropdown-item + v-for="(option, i) in $options.refreshIntervals" + :key="i" + :is-check-item="true" + :is-checked="isChecked(option)" + @click="setRefreshInterval(option)" + >{{ option.label }}</gl-new-dropdown-item + > + </gl-new-dropdown> + </gl-button-group> +</template> diff --git a/app/assets/javascripts/monitoring/components/variables/custom_variable.vue b/app/assets/javascripts/monitoring/components/variables/dropdown_field.vue index 0ac7c0b80df..4e48292c48d 100644 --- a/app/assets/javascripts/monitoring/components/variables/custom_variable.vue +++ b/app/assets/javascripts/monitoring/components/variables/dropdown_field.vue @@ -22,29 +22,32 @@ export default { default: '', }, options: { - type: Array, + type: Object, required: true, }, }, computed: { - defaultText() { - const selectedOpt = this.options.find(opt => opt.value === this.value); + text() { + const selectedOpt = this.options.values?.find(opt => opt.value === this.value); return selectedOpt?.text || this.value; }, }, methods: { onUpdate(value) { - this.$emit('onUpdate', this.name, value); + this.$emit('input', value); }, }, }; </script> <template> <gl-form-group :label="label"> - <gl-dropdown toggle-class="dropdown-menu-toggle" :text="defaultText"> - <gl-dropdown-item v-for="(opt, key) in options" :key="key" @click="onUpdate(opt.value)">{{ - opt.text - }}</gl-dropdown-item> + <gl-dropdown toggle-class="dropdown-menu-toggle" :text="text || s__('Metrics|Select a value')"> + <gl-dropdown-item + v-for="val in options.values" + :key="val.value" + @click="onUpdate(val.value)" + >{{ val.text }}</gl-dropdown-item + > </gl-dropdown> </gl-form-group> </template> diff --git a/app/assets/javascripts/monitoring/components/variables/text_variable.vue b/app/assets/javascripts/monitoring/components/variables/text_field.vue index ce0d19760e2..a0418806e5f 100644 --- a/app/assets/javascripts/monitoring/components/variables/text_variable.vue +++ b/app/assets/javascripts/monitoring/components/variables/text_field.vue @@ -22,7 +22,7 @@ export default { }, methods: { onUpdate(event) { - this.$emit('onUpdate', this.name, event.target.value); + this.$emit('input', event.target.value); }, }, }; diff --git a/app/assets/javascripts/monitoring/components/variables_section.vue b/app/assets/javascripts/monitoring/components/variables_section.vue index 3d1d111d5b3..25d900b07ad 100644 --- a/app/assets/javascripts/monitoring/components/variables_section.vue +++ b/app/assets/javascripts/monitoring/components/variables_section.vue @@ -1,13 +1,14 @@ <script> import { mapState, mapActions } from 'vuex'; -import CustomVariable from './variables/custom_variable.vue'; -import TextVariable from './variables/text_variable.vue'; +import DropdownField from './variables/dropdown_field.vue'; +import TextField from './variables/text_field.vue'; import { setCustomVariablesFromUrl } from '../utils'; +import { VARIABLE_TYPES } from '../constants'; export default { components: { - CustomVariable, - TextVariable, + DropdownField, + TextField, }, computed: { ...mapState('monitoringDashboard', ['variables']), @@ -15,10 +16,9 @@ export default { methods: { ...mapActions('monitoringDashboard', ['updateVariablesAndFetchData']), refreshDashboard(variable, value) { - if (this.variables[variable].value !== value) { - const changedVariable = { key: variable, value }; + if (variable.value !== value) { + this.updateVariablesAndFetchData({ name: variable.name, value }); // update the Vuex store - this.updateVariablesAndFetchData(changedVariable); // the below calls can ideally be moved out of the // component and into the actions and let the // mutation respond directly. @@ -27,27 +27,26 @@ export default { setCustomVariablesFromUrl(this.variables); } }, - variableComponent(type) { - const types = { - text: TextVariable, - custom: CustomVariable, - }; - return types[type] || TextVariable; + variableField(type) { + if (type === VARIABLE_TYPES.custom || type === VARIABLE_TYPES.metric_label_values) { + return DropdownField; + } + return TextField; }, }, }; </script> <template> <div ref="variablesSection" class="d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 variables-section"> - <div v-for="(variable, key) in variables" :key="key" class="mb-1 pr-2 d-flex d-sm-block"> + <div v-for="variable in variables" :key="variable.name" class="mb-1 pr-2 d-flex d-sm-block"> <component - :is="variableComponent(variable.type)" + :is="variableField(variable.type)" class="mb-0 flex-grow-1" :label="variable.label" :value="variable.value" - :name="key" + :name="variable.name" :options="variable.options" - @onUpdate="refreshDashboard" + @input="refreshDashboard(variable, $event)" /> </div> </div> |