diff options
Diffstat (limited to 'app/assets/javascripts/monitoring')
32 files changed, 1011 insertions, 501 deletions
diff --git a/app/assets/javascripts/monitoring/components/alert_widget.vue b/app/assets/javascripts/monitoring/components/alert_widget.vue index 86a793c854e..5562981fe1c 100644 --- a/app/assets/javascripts/monitoring/components/alert_widget.vue +++ b/app/assets/javascripts/monitoring/components/alert_widget.vue @@ -234,11 +234,7 @@ export default { class="alert-current-setting cursor-pointer d-flex" @click="showModal" > - <gl-badge - :variant="isFiring ? 'danger' : 'secondary'" - pill - class="d-flex-center text-truncate" - > + <gl-badge :variant="isFiring ? 'danger' : 'neutral'" class="d-flex-center text-truncate"> <gl-icon name="warning" :size="16" class="flex-shrink-0" /> <span class="text-truncate gl-pl-1-deprecated-no-really-do-not-use-me"> <gl-sprintf diff --git a/app/assets/javascripts/monitoring/components/alert_widget_form.vue b/app/assets/javascripts/monitoring/components/alert_widget_form.vue index 74324daa1e3..b2d7ca0c4e0 100644 --- a/app/assets/javascripts/monitoring/components/alert_widget_form.vue +++ b/app/assets/javascripts/monitoring/components/alert_widget_form.vue @@ -238,7 +238,7 @@ export default { <icon v-gl-tooltip="$options.alertQueryText.descriptionTooltip" name="question" - class="prepend-left-4" + class="gl-ml-2" /> </div> </template> diff --git a/app/assets/javascripts/monitoring/components/charts/column.vue b/app/assets/javascripts/monitoring/components/charts/column.vue index 7a2e3e1b511..d7d01def45e 100644 --- a/app/assets/javascripts/monitoring/components/charts/column.vue +++ b/app/assets/javascripts/monitoring/components/charts/column.vue @@ -5,7 +5,8 @@ import { getSvgIconPathContent } from '~/lib/utils/icon_utils'; import { chartHeight } from '../../constants'; import { makeDataSeries } from '~/helpers/monitor_helper'; import { graphDataValidatorForValues } from '../../utils'; -import { getYAxisOptions, getChartGrid } from './options'; +import { getTimeAxisOptions, getYAxisOptions, getChartGrid } from './options'; +import { timezones } from '../../format_date'; export default { components: { @@ -20,6 +21,11 @@ export default { required: true, validator: graphDataValidatorForValues.bind(null, false), }, + timezone: { + type: String, + required: false, + default: timezones.LOCAL, + }, }, data() { return { @@ -43,6 +49,8 @@ export default { }; }, chartOptions() { + const xAxis = getTimeAxisOptions({ timezone: this.timezone }); + const yAxis = { ...getYAxisOptions(this.graphData.yAxis), scale: false, @@ -50,8 +58,9 @@ export default { return { grid: getChartGrid(), + xAxis, yAxis, - dataZoom: this.dataZoomConfig, + dataZoom: [this.dataZoomConfig], }; }, xAxisTitle() { diff --git a/app/assets/javascripts/monitoring/components/charts/empty_chart.vue b/app/assets/javascripts/monitoring/components/charts/empty_chart.vue index e015ef32d8c..ad176637538 100644 --- a/app/assets/javascripts/monitoring/components/charts/empty_chart.vue +++ b/app/assets/javascripts/monitoring/components/charts/empty_chart.vue @@ -23,10 +23,10 @@ export default { <template> <div class="d-flex flex-column justify-content-center"> <div - class="prepend-top-8 svg-w-100 d-flex align-items-center" + class="gl-mt-3 svg-w-100 d-flex align-items-center" :style="svgContainerStyle" v-html="chartEmptyStateIllustration" ></div> - <h5 class="text-center prepend-top-8">{{ __('No data to display') }}</h5> + <h5 class="text-center gl-mt-3">{{ __('No data to display') }}</h5> </div> </template> diff --git a/app/assets/javascripts/monitoring/components/charts/heatmap.vue b/app/assets/javascripts/monitoring/components/charts/heatmap.vue index 55a25ee09fd..f6f266dacf3 100644 --- a/app/assets/javascripts/monitoring/components/charts/heatmap.vue +++ b/app/assets/javascripts/monitoring/components/charts/heatmap.vue @@ -1,8 +1,8 @@ <script> import { GlResizeObserverDirective } from '@gitlab/ui'; import { GlHeatmap } from '@gitlab/ui/dist/charts'; -import dateformat from 'dateformat'; import { graphDataValidatorForValues } from '../../utils'; +import { formatDate, timezones, formats } from '../../format_date'; export default { components: { @@ -17,6 +17,11 @@ export default { required: true, validator: graphDataValidatorForValues.bind(null, false), }, + timezone: { + type: String, + required: false, + default: timezones.LOCAL, + }, }, data() { return { @@ -43,7 +48,7 @@ export default { return this.result.values.map(val => { const [yLabel] = val; - return dateformat(new Date(yLabel), 'HH:MM:ss'); + 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 09b03774580..f7822e69b1d 100644 --- a/app/assets/javascripts/monitoring/components/charts/options.js +++ b/app/assets/javascripts/monitoring/components/charts/options.js @@ -1,5 +1,6 @@ import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format'; -import { s__ } from '~/locale'; +import { __, s__ } from '~/locale'; +import { formatDate, timezones, formats } from '../../format_date'; const yAxisBoundaryGap = [0.1, 0.1]; /** @@ -21,6 +22,21 @@ const chartGridLeft = 75; // Axis options /** + * Axis types + * @see https://echarts.apache.org/en/option.html#xAxis.type + */ +export const axisTypes = { + /** + * Category axis, suitable for discrete category data. + */ + category: 'category', + /** + * Time axis, suitable for continuous time series data. + */ + time: 'time', +}; + +/** * Converts .yml parameters to echarts axis options for data axis * @param {Object} param - Dashboard .yml definition options */ @@ -58,6 +74,17 @@ export const getYAxisOptions = ({ }; }; +export const getTimeAxisOptions = ({ timezone = timezones.LOCAL } = {}) => ({ + name: __('Time'), + type: axisTypes.time, + axisLabel: { + formatter: date => formatDate(date, { format: formats.shortTime, timezone }), + }, + axisPointer: { + snap: false, + }, +}); + // Chart grid /** diff --git a/app/assets/javascripts/monitoring/components/charts/stacked_column.vue b/app/assets/javascripts/monitoring/components/charts/stacked_column.vue index 66ba20c125f..ac31d107e63 100644 --- a/app/assets/javascripts/monitoring/components/charts/stacked_column.vue +++ b/app/assets/javascripts/monitoring/components/charts/stacked_column.vue @@ -2,8 +2,11 @@ import { GlResizeObserverDirective } from '@gitlab/ui'; import { GlStackedColumnChart } from '@gitlab/ui/dist/charts'; import { getSvgIconPathContent } from '~/lib/utils/icon_utils'; -import { chartHeight } from '../../constants'; +import { chartHeight, legendLayoutTypes } from '../../constants'; +import { s__ } from '~/locale'; import { graphDataValidatorForValues } from '../../utils'; +import { getTimeAxisOptions, axisTypes } from './options'; +import { timezones } from '../../format_date'; export default { components: { @@ -18,6 +21,36 @@ export default { required: true, validator: graphDataValidatorForValues.bind(null, false), }, + timezone: { + type: String, + required: false, + default: timezones.LOCAL, + }, + legendLayout: { + type: String, + required: false, + default: legendLayoutTypes.table, + }, + legendAverageText: { + type: String, + required: false, + default: s__('Metrics|Avg'), + }, + legendCurrentText: { + type: String, + required: false, + default: s__('Metrics|Current'), + }, + legendMaxText: { + type: String, + required: false, + default: s__('Metrics|Max'), + }, + legendMinText: { + type: String, + required: false, + default: s__('Metrics|Min'), + }, }, data() { return { @@ -28,7 +61,14 @@ export default { }, computed: { chartData() { - return this.graphData.metrics.map(metric => metric.result[0].values.map(val => val[1])); + return this.graphData.metrics.map(({ result }) => { + // This needs a fix. Not only metrics[0] should be shown. + // See https://gitlab.com/gitlab-org/gitlab/-/issues/220492 + if (!result || result.length === 0) { + return []; + } + return result[0].values.map(val => val[1]); + }); }, xAxisTitle() { return this.graphData.x_label !== undefined ? this.graphData.x_label : ''; @@ -37,10 +77,17 @@ export default { return this.graphData.y_label !== undefined ? this.graphData.y_label : ''; }, xAxisType() { - return this.graphData.x_type !== undefined ? this.graphData.x_type : 'category'; + // stacked-column component requires the x-axis to be of type `category` + return axisTypes.category; }, groupBy() { - return this.graphData.metrics[0].result[0].values.map(val => val[0]); + // This needs a fix. Not only metrics[0] should be shown. + // See https://gitlab.com/gitlab-org/gitlab/-/issues/220492 + const { result } = this.graphData.metrics[0]; + if (!result || result.length === 0) { + return []; + } + return result[0].values.map(val => val[0]); }, dataZoomConfig() { const handleIcon = this.svgs['scroll-handle']; @@ -49,11 +96,15 @@ export default { }, chartOptions() { return { - dataZoom: this.dataZoomConfig, + xAxis: { + ...getTimeAxisOptions({ timezone: this.timezone }), + type: this.xAxisType, + }, + dataZoom: [this.dataZoomConfig], }; }, seriesNames() { - return this.graphData.metrics.map(metric => metric.series_name); + return this.graphData.metrics.map(metric => metric.label); }, }, created() { @@ -94,6 +145,11 @@ export default { :width="width" :height="height" :series-names="seriesNames" + :legend-layout="legendLayout" + :legend-average-text="legendAverageText" + :legend-current-text="legendCurrentText" + :legend-max-text="legendMaxText" + :legend-min-text="legendMinText" /> </div> </template> diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue index 8f37a12af75..28af2d8ba77 100644 --- a/app/assets/javascripts/monitoring/components/charts/time_series.vue +++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue @@ -2,18 +2,19 @@ import { omit, throttle } from 'lodash'; import { GlLink, GlDeprecatedButton, GlTooltip, GlResizeObserverDirective } from '@gitlab/ui'; import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts'; -import dateFormat from 'dateformat'; -import { s__, __ } from '~/locale'; +import { s__ } from '~/locale'; import { getSvgIconPathContent } from '~/lib/utils/icon_utils'; import Icon from '~/vue_shared/components/icon.vue'; -import { panelTypes, chartHeight, lineTypes, lineWidths, dateFormats } from '../../constants'; -import { getYAxisOptions, getChartGrid, getTooltipFormatter } from './options'; +import { panelTypes, chartHeight, lineTypes, lineWidths, legendLayoutTypes } from '../../constants'; +import { getYAxisOptions, getTimeAxisOptions, getChartGrid, getTooltipFormatter } from './options'; import { annotationsYAxis, generateAnnotationsSeries } from './annotations'; import { makeDataSeries } from '~/helpers/monitor_helper'; import { graphDataValidatorForValues } from '../../utils'; +import { formatDate, timezones } from '../../format_date'; + +export const timestampToISODate = timestamp => new Date(timestamp).toISOString(); const THROTTLED_DATAZOOM_WAIT = 1000; // milliseconds -const timestampToISODate = timestamp => new Date(timestamp).toISOString(); const events = { datazoom: 'datazoom', @@ -74,21 +75,41 @@ export default { required: false, default: () => [], }, + legendLayout: { + type: String, + required: false, + default: legendLayoutTypes.table, + }, legendAverageText: { type: String, required: false, default: s__('Metrics|Avg'), }, + legendCurrentText: { + type: String, + required: false, + default: s__('Metrics|Current'), + }, legendMaxText: { type: String, required: false, default: s__('Metrics|Max'), }, + legendMinText: { + type: String, + required: false, + default: s__('Metrics|Min'), + }, groupId: { type: String, required: false, default: '', }, + timezone: { + type: String, + required: false, + default: timezones.LOCAL, + }, }, data() { return { @@ -154,23 +175,16 @@ export default { const { yAxis, xAxis } = this.option; const option = omit(this.option, ['series', 'yAxis', 'xAxis']); + const timeXAxis = { + ...getTimeAxisOptions({ timezone: this.timezone }), + ...xAxis, + }; + const dataYAxis = { ...getYAxisOptions(this.graphData.yAxis), ...yAxis, }; - const timeXAxis = { - name: __('Time'), - type: 'time', - axisLabel: { - formatter: date => dateFormat(date, dateFormats.timeOfDay), - }, - axisPointer: { - snap: true, - }, - ...xAxis, - }; - return { series: this.chartOptionSeries, xAxis: timeXAxis, @@ -271,12 +285,13 @@ export default { */ formatAnnotationsTooltipText(params) { return { - title: dateFormat(params.data?.tooltipData?.title, dateFormats.default), + title: formatDate(params.data?.tooltipData?.title, { timezone: this.timezone }), content: params.data?.tooltipData?.content, }; }, formatTooltipText(params) { - this.tooltip.title = dateFormat(params.value, dateFormats.default); + this.tooltip.title = formatDate(params.value, { timezone: this.timezone }); + this.tooltip.content = []; params.seriesData.forEach(dataPoint => { @@ -368,8 +383,11 @@ export default { :thresholds="thresholds" :width="width" :height="height" - :average-text="legendAverageText" - :max-text="legendMaxText" + :legend-layout="legendLayout" + :legend-average-text="legendAverageText" + :legend-current-text="legendCurrentText" + :legend-max-text="legendMaxText" + :legend-min-text="legendMinText" @created="onChartCreated" @updated="onChartUpdated" > diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 2018c706b11..f54319d283e 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -1,73 +1,45 @@ <script> -import { debounce } from 'lodash'; import { mapActions, mapState, mapGetters } from 'vuex'; import VueDraggable from 'vuedraggable'; -import { - GlIcon, - GlButton, - GlDeprecatedButton, - GlDropdown, - GlDropdownItem, - GlDropdownHeader, - GlDropdownDivider, - GlModal, - GlLoadingIcon, - GlSearchBoxByType, - GlModalDirective, - GlTooltipDirective, -} from '@gitlab/ui'; +import { GlIcon, GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui'; +import DashboardHeader from './dashboard_header.vue'; import DashboardPanel from './dashboard_panel.vue'; import { s__ } from '~/locale'; import createFlash from '~/flash'; import { ESC_KEY, ESC_KEY_IE11 } from '~/lib/utils/keys'; -import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue'; -import { mergeUrlParams, redirectTo, updateHistory } from '~/lib/utils/url_utility'; +import { mergeUrlParams, updateHistory } from '~/lib/utils/url_utility'; import invalidUrl from '~/lib/utils/invalid_url'; import Icon from '~/vue_shared/components/icon.vue'; -import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue'; import GraphGroup from './graph_group.vue'; import EmptyState from './empty_state.vue'; import GroupEmptyState from './group_empty_state.vue'; -import DashboardsDropdown from './dashboards_dropdown.vue'; import VariablesSection from './variables_section.vue'; +import LinksSection from './links_section.vue'; import TrackEventDirective from '~/vue_shared/directives/track_event'; import { - getAddMetricTrackingOptions, - timeRangeToUrl, timeRangeFromUrl, panelToUrl, expandedPanelPayloadFromUrl, convertVariablesForURL, } from '../utils'; import { metricStates } from '../constants'; -import { defaultTimeRange, timeRanges } from '~/vue_shared/constants'; +import { defaultTimeRange } from '~/vue_shared/constants'; export default { components: { VueDraggable, + DashboardHeader, DashboardPanel, Icon, GlIcon, GlButton, - GlDeprecatedButton, - GlDropdown, - GlLoadingIcon, - GlDropdownItem, - GlDropdownHeader, - GlDropdownDivider, - GlSearchBoxByType, - GlModal, - CustomMetricsFormFields, - - DateTimePicker, GraphGroup, EmptyState, GroupEmptyState, - DashboardsDropdown, - VariablesSection, + LinksSection, }, directives: { GlModal: GlModalDirective, @@ -111,27 +83,10 @@ export default { type: String, required: true, }, - projectPath: { - type: String, - required: true, - }, - logsPath: { - type: String, - required: false, - default: invalidUrl, - }, defaultBranch: { type: String, - required: true, - }, - metricsEndpoint: { - type: String, - required: true, - }, - deploymentsEndpoint: { - type: String, required: false, - default: null, + default: '', }, emptyGettingStartedSvgPath: { type: String, @@ -153,10 +108,6 @@ export default { type: String, required: true, }, - currentEnvironmentName: { - type: String, - required: true, - }, customMetricsAvailable: { type: Boolean, required: false, @@ -172,21 +123,6 @@ export default { required: false, default: invalidUrl, }, - dashboardEndpoint: { - type: String, - required: false, - default: invalidUrl, - }, - dashboardsEndpoint: { - type: String, - required: false, - default: invalidUrl, - }, - currentDashboard: { - type: String, - required: false, - default: '', - }, smallEmptyState: { type: Boolean, required: false, @@ -210,11 +146,9 @@ export default { }, data() { return { - formIsValid: null, selectedTimeRange: timeRangeFromUrl() || defaultTimeRange, - hasValidDates: true, - timeRanges, isRearrangingPanels: false, + originalDocumentTitle: document.title, }; }, computed: { @@ -222,36 +156,17 @@ export default { 'dashboard', 'emptyState', 'showEmptyState', - 'useDashboardEndpoint', - 'allDashboards', - 'environmentsLoading', 'expandedPanel', - 'promVariables', - 'isUpdatingStarredValue', - ]), - ...mapGetters('monitoringDashboard', [ - 'selectedDashboard', - 'getMetricStates', - 'filteredEnvironments', + 'variables', + 'links', + 'currentDashboard', ]), - showRearrangePanelsBtn() { - return !this.showEmptyState && this.rearrangePanelsAvailable; - }, - addingMetricsAvailable() { - return ( - this.customMetricsAvailable && - !this.showEmptyState && - // 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 - this.selectedDashboard?.system_dashboard - ); - }, - shouldShowEnvironmentsDropdownNoMatchedMsg() { - return !this.environmentsLoading && this.filteredEnvironments.length === 0; - }, + ...mapGetters('monitoringDashboard', ['selectedDashboard', 'getMetricStates']), shouldShowVariablesSection() { - return Object.keys(this.promVariables).length > 0; + return Object.keys(this.variables).length > 0; + }, + shouldShowLinksSection() { + return Object.keys(this.links).length > 0; }, }, watch: { @@ -273,24 +188,17 @@ export default { handler({ group, panel }) { const dashboardPath = this.currentDashboard || this.selectedDashboard?.path; updateHistory({ - url: panelToUrl(dashboardPath, convertVariablesForURL(this.promVariables), group, panel), + url: panelToUrl(dashboardPath, convertVariablesForURL(this.variables), group, panel), title: document.title, }); }, deep: true, }, + selectedDashboard(dashboard) { + this.prependToDocumentTitle(dashboard?.display_name); + }, }, created() { - this.setInitialState({ - metricsEndpoint: this.metricsEndpoint, - deploymentsEndpoint: this.deploymentsEndpoint, - dashboardEndpoint: this.dashboardEndpoint, - dashboardsEndpoint: this.dashboardsEndpoint, - currentDashboard: this.currentDashboard, - projectPath: this.projectPath, - logsPath: this.logsPath, - currentEnvironmentName: this.currentEnvironmentName, - }); window.addEventListener('keyup', this.onKeyup); }, destroyed() { @@ -308,14 +216,10 @@ export default { ...mapActions('monitoringDashboard', [ 'setTimeRange', 'fetchData', - 'fetchDashboardData', 'setGettingStartedEmptyState', - 'setInitialState', 'setPanelGroupMetrics', - 'filterEnvironments', 'setExpandedPanel', 'clearExpandedPanel', - 'toggleStarredValue', ]), updatePanels(key, panels) { this.setPanelGroupMetrics({ @@ -329,37 +233,9 @@ export default { key, }); }, - - onDateTimePickerInput(timeRange) { - redirectTo(timeRangeToUrl(timeRange)); - }, - onDateTimePickerInvalid() { - createFlash( - s__( - 'Metrics|Link contains an invalid time window, please verify the link to see the requested time range.', - ), - ); - // As a fallback, switch to default time range instead - this.selectedTimeRange = defaultTimeRange; - }, generatePanelUrl(groupKey, panel) { const dashboardPath = this.currentDashboard || this.selectedDashboard?.path; - return panelToUrl(dashboardPath, convertVariablesForURL(this.promVariables), groupKey, panel); - }, - hideAddMetricModal() { - this.$refs.addMetricModal.hide(); - }, - toggleRearrangingPanels() { - this.isRearrangingPanels = !this.isRearrangingPanels; - }, - setFormValidity(isValid) { - this.formIsValid = isValid; - }, - debouncedEnvironmentsSearch: debounce(function environmentsSearchOnInput(searchTerm) { - this.filterEnvironments(searchTerm); - }, 500), - submitCustomMetricsForm() { - this.$refs.customMetricsForm.submit(); + return panelToUrl(dashboardPath, convertVariablesForURL(this.variables), groupKey, panel); }, /** * Return a single empty state for a group. @@ -387,25 +263,20 @@ export default { // Collapse group if no data is available return !this.getMetricStates(groupKey).includes(metricStates.OK); }, - getAddMetricTrackingOptions, - - selectDashboard(dashboard) { - const params = { - dashboard: dashboard.path, - }; - redirectTo(mergeUrlParams(params, window.location.href)); - }, - - refreshDashboard() { - this.fetchDashboardData(); + prependToDocumentTitle(text) { + if (text) { + document.title = `${text} · ${this.originalDocumentTitle}`; + } }, - onTimeRangeZoom({ start, end }) { updateHistory({ url: mergeUrlParams({ start, end }, window.location.href), title: document.title, }); this.selectedTimeRange = { start, end }; + // keep the current dashboard time range + // in sync with the Vuex store + this.setTimeRange(this.selectedTimeRange); }, onExpandPanel(group, panel) { this.setExpandedPanel({ group, panel }); @@ -419,213 +290,45 @@ export default { this.clearExpandedPanel(); } }, - }, - addMetric: { - title: s__('Metrics|Add metric'), - modalId: 'add-metric', + onSetRearrangingPanels(isRearrangingPanels) { + this.isRearrangingPanels = isRearrangingPanels; + }, + onDateTimePickerInvalid() { + createFlash( + s__( + 'Metrics|Link contains an invalid time window, please verify the link to see the requested time range.', + ), + ); + // As a fallback, switch to default time range instead + this.selectedTimeRange = defaultTimeRange; + }, }, i18n: { goBackLabel: s__('Metrics|Go back (Esc)'), - starDashboard: s__('Metrics|Star dashboard'), - unstarDashboard: s__('Metrics|Unstar dashboard'), }, }; </script> <template> <div class="prometheus-graphs" data-qa-selector="prometheus_graphs"> - <div + <dashboard-header 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" - > - <div class="mb-2 pr-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" - @selectDashboard="selectDashboard($event)" - /> - </div> - - <div class="mb-2 pr-2 d-flex d-sm-block"> - <gl-dropdown - id="monitor-environments-dropdown" - ref="monitorEnvironmentsDropdown" - class="flex-grow-1" - data-qa-selector="environments_dropdown" - toggle-class="dropdown-menu-toggle" - menu-class="monitor-environment-dropdown-menu" - :text="currentEnvironmentName" - > - <div class="d-flex flex-column overflow-hidden"> - <gl-dropdown-header class="monitor-environment-dropdown-header text-center"> - {{ __('Environment') }} - </gl-dropdown-header> - <gl-dropdown-divider /> - <gl-search-box-by-type - ref="monitorEnvironmentsDropdownSearch" - class="m-2" - @input="debouncedEnvironmentsSearch" - /> - <gl-loading-icon - v-if="environmentsLoading" - ref="monitorEnvironmentsDropdownLoading" - :inline="true" - /> - <div v-else class="flex-fill overflow-auto"> - <gl-dropdown-item - v-for="environment in filteredEnvironments" - :key="environment.id" - :active="environment.name === currentEnvironmentName" - active-class="is-active" - :href="environment.metrics_path" - >{{ environment.name }}</gl-dropdown-item - > - </div> - <div - v-show="shouldShowEnvironmentsDropdownNoMatchedMsg" - ref="monitorEnvironmentsDropdownMsg" - class="text-secondary no-matches-message" - > - {{ __('No matching results') }} - </div> - </div> - </gl-dropdown> - </div> - - <div class="mb-2 pr-2 d-flex d-sm-block"> - <date-time-picker - ref="dateTimePicker" - class="flex-grow-1 show-last-dropdown" - data-qa-selector="range_picker_dropdown" - :value="selectedTimeRange" - :options="timeRanges" - @input="onDateTimePickerInput" - @invalid="onDateTimePickerInvalid" - /> - </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> - </div> - - <div class="flex-grow-1"></div> - - <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-deprecated-button - ref="toggleStarBtn" - class="w-100" - :disabled="isUpdatingStarredValue" - variant="default" - @click="toggleStarredValue()" - > - <gl-icon :name="selectedDashboard.starred ? 'star' : 'star-o'" /> - </gl-deprecated-button> - </div> - </div> - - <div v-if="showRearrangePanelsBtn" class="mb-2 mr-2 d-flex"> - <gl-deprecated-button - :pressed="isRearrangingPanels" - variant="default" - class="flex-grow-1 js-rearrange-button" - @click="toggleRearrangingPanels" - > - {{ __('Arrange charts') }} - </gl-deprecated-button> - </div> - <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" - variant="outline-success" - data-qa-selector="add_metric_button" - class="flex-grow-1" - > - {{ $options.addMetric.title }} - </gl-deprecated-button> - <gl-modal - ref="addMetricModal" - :modal-id="$options.addMetric.modalId" - :title="$options.addMetric.title" - > - <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 - ref="submitCustomMetricsFormBtn" - v-track-event="getAddMetricTrackingOptions()" - :disabled="!formIsValid" - variant="success" - @click="submitCustomMetricsForm" - > - {{ __('Save changes') }} - </gl-deprecated-button> - </div> - </gl-modal> - </div> - - <div - v-if="selectedDashboard && selectedDashboard.can_edit" - class="mb-2 mr-2 d-flex d-sm-block" - > - <gl-deprecated-button - class="flex-grow-1 js-edit-link" - :href="selectedDashboard.project_blob_path" - data-qa-selector="edit_dashboard_button" - > - {{ __('Edit dashboard') }} - </gl-deprecated-button> - </div> - - <div v-if="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" - :href="externalDashboardUrl" - target="_blank" - rel="noopener noreferrer" - > - {{ __('View full dashboard') }} <icon name="external-link" /> - </gl-deprecated-button> - </div> - </div> - </div> + :default-branch="defaultBranch" + :rearrange-panels-available="rearrangePanelsAvailable" + :custom-metrics-available="customMetricsAvailable" + :custom-metrics-path="customMetricsPath" + :validate-query-path="validateQueryPath" + :external-dashboard-url="externalDashboardUrl" + :has-metrics="hasMetrics" + :is-rearranging-panels="isRearrangingPanels" + :selected-time-range="selectedTimeRange" + @dateTimePickerInvalid="onDateTimePickerInvalid" + @setRearrangingPanels="onSetRearrangingPanels" + /> <variables-section v-if="shouldShowVariablesSection && !showEmptyState" /> + <links-section v-if="shouldShowLinksSection && !showEmptyState" /> <div v-if="!showEmptyState"> <dashboard-panel v-show="expandedPanel.panel" diff --git a/app/assets/javascripts/monitoring/components/dashboard_header.vue b/app/assets/javascripts/monitoring/components/dashboard_header.vue new file mode 100644 index 00000000000..16a21ae0d3c --- /dev/null +++ b/app/assets/javascripts/monitoring/components/dashboard_header.vue @@ -0,0 +1,369 @@ +<script> +import { debounce } from 'lodash'; +import { mapActions, mapState, mapGetters } from 'vuex'; +import { + GlIcon, + GlDeprecatedButton, + GlDropdown, + GlDropdownItem, + GlDropdownHeader, + GlDropdownDivider, + GlModal, + GlLoadingIcon, + GlSearchBoxByType, + GlModalDirective, + GlTooltipDirective, +} 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 invalidUrl from '~/lib/utils/invalid_url'; +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 TrackEventDirective from '~/vue_shared/directives/track_event'; +import { getAddMetricTrackingOptions, timeRangeToUrl } from '../utils'; +import { timeRanges } from '~/vue_shared/constants'; +import { timezones } from '../format_date'; + +export default { + components: { + Icon, + GlIcon, + GlDeprecatedButton, + GlDropdown, + GlLoadingIcon, + GlDropdownItem, + GlDropdownHeader, + GlDropdownDivider, + GlSearchBoxByType, + GlModal, + CustomMetricsFormFields, + + DateTimePicker, + DashboardsDropdown, + }, + directives: { + GlModal: GlModalDirective, + GlTooltip: GlTooltipDirective, + TrackEvent: TrackEventDirective, + }, + props: { + defaultBranch: { + type: String, + required: true, + }, + rearrangePanelsAvailable: { + type: Boolean, + required: false, + default: false, + }, + customMetricsAvailable: { + type: Boolean, + required: false, + default: false, + }, + customMetricsPath: { + type: String, + required: false, + default: invalidUrl, + }, + validateQueryPath: { + type: String, + required: false, + default: invalidUrl, + }, + externalDashboardUrl: { + type: String, + required: false, + default: '', + }, + hasMetrics: { + type: Boolean, + required: false, + default: true, + }, + isRearrangingPanels: { + type: Boolean, + required: true, + }, + selectedTimeRange: { + type: Object, + required: true, + }, + }, + data() { + return { + formIsValid: null, + }; + }, + computed: { + ...mapState('monitoringDashboard', [ + 'environmentsLoading', + 'currentEnvironmentName', + 'isUpdatingStarredValue', + 'showEmptyState', + 'dashboardTimezone', + ]), + ...mapGetters('monitoringDashboard', ['selectedDashboard', 'filteredEnvironments']), + shouldShowEnvironmentsDropdownNoMatchedMsg() { + return !this.environmentsLoading && this.filteredEnvironments.length === 0; + }, + addingMetricsAvailable() { + return ( + this.customMetricsAvailable && + !this.showEmptyState && + // 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 + this.selectedDashboard?.system_dashboard + ); + }, + showRearrangePanelsBtn() { + return !this.showEmptyState && this.rearrangePanelsAvailable; + }, + displayUtc() { + return this.dashboardTimezone === timezones.UTC; + }, + }, + methods: { + ...mapActions('monitoringDashboard', [ + 'filterEnvironments', + 'fetchDashboardData', + 'toggleStarredValue', + ]), + selectDashboard(dashboard) { + const params = { + dashboard: dashboard.path, + }; + redirectTo(mergeUrlParams(params, window.location.href)); + }, + debouncedEnvironmentsSearch: debounce(function environmentsSearchOnInput(searchTerm) { + this.filterEnvironments(searchTerm); + }, 500), + onDateTimePickerInput(timeRange) { + redirectTo(timeRangeToUrl(timeRange)); + }, + onDateTimePickerInvalid() { + this.$emit('dateTimePickerInvalid'); + }, + refreshDashboard() { + this.fetchDashboardData(); + }, + + toggleRearrangingPanels() { + this.$emit('setRearrangingPanels', !this.isRearrangingPanels); + }, + setFormValidity(isValid) { + this.formIsValid = isValid; + }, + hideAddMetricModal() { + this.$refs.addMetricModal.hide(); + }, + getAddMetricTrackingOptions, + submitCustomMetricsForm() { + this.$refs.customMetricsForm.submit(); + }, + }, + addMetric: { + title: s__('Metrics|Add metric'), + modalId: 'add-metric', + }, + i18n: { + starDashboard: s__('Metrics|Star dashboard'), + unstarDashboard: s__('Metrics|Unstar dashboard'), + }, + timeRanges, +}; +</script> + +<template> + <div ref="prometheusGraphsHeader"> + <div class="mb-2 pr-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" + @selectDashboard="selectDashboard" + /> + </div> + + <div class="mb-2 pr-2 d-flex d-sm-block"> + <gl-dropdown + id="monitor-environments-dropdown" + ref="monitorEnvironmentsDropdown" + class="flex-grow-1" + data-qa-selector="environments_dropdown" + toggle-class="dropdown-menu-toggle" + menu-class="monitor-environment-dropdown-menu" + :text="currentEnvironmentName" + > + <div class="d-flex flex-column overflow-hidden"> + <gl-dropdown-header class="monitor-environment-dropdown-header text-center"> + {{ __('Environment') }} + </gl-dropdown-header> + <gl-dropdown-divider /> + <gl-search-box-by-type + ref="monitorEnvironmentsDropdownSearch" + class="m-2" + @input="debouncedEnvironmentsSearch" + /> + <gl-loading-icon + v-if="environmentsLoading" + ref="monitorEnvironmentsDropdownLoading" + :inline="true" + /> + <div v-else class="flex-fill overflow-auto"> + <gl-dropdown-item + v-for="environment in filteredEnvironments" + :key="environment.id" + :active="environment.name === currentEnvironmentName" + active-class="is-active" + :href="environment.metrics_path" + >{{ environment.name }}</gl-dropdown-item + > + </div> + <div + v-show="shouldShowEnvironmentsDropdownNoMatchedMsg" + ref="monitorEnvironmentsDropdownMsg" + class="text-secondary no-matches-message" + > + {{ __('No matching results') }} + </div> + </div> + </gl-dropdown> + </div> + + <div class="mb-2 pr-2 d-flex d-sm-block"> + <date-time-picker + ref="dateTimePicker" + class="flex-grow-1 show-last-dropdown" + data-qa-selector="range_picker_dropdown" + :value="selectedTimeRange" + :options="$options.timeRanges" + :utc="displayUtc" + @input="onDateTimePickerInput" + @invalid="onDateTimePickerInvalid" + /> + </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> + </div> + + <div class="flex-grow-1"></div> + + <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-deprecated-button + ref="toggleStarBtn" + class="w-100" + :disabled="isUpdatingStarredValue" + variant="default" + @click="toggleStarredValue()" + > + <gl-icon :name="selectedDashboard.starred ? 'star' : 'star-o'" /> + </gl-deprecated-button> + </div> + </div> + + <div v-if="showRearrangePanelsBtn" class="mb-2 mr-2 d-flex"> + <gl-deprecated-button + :pressed="isRearrangingPanels" + variant="default" + class="flex-grow-1 js-rearrange-button" + @click="toggleRearrangingPanels" + > + {{ __('Arrange charts') }} + </gl-deprecated-button> + </div> + <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" + variant="outline-success" + data-qa-selector="add_metric_button" + class="flex-grow-1" + > + {{ $options.addMetric.title }} + </gl-deprecated-button> + <gl-modal + ref="addMetricModal" + :modal-id="$options.addMetric.modalId" + :title="$options.addMetric.title" + > + <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 + ref="submitCustomMetricsFormBtn" + v-track-event="getAddMetricTrackingOptions()" + :disabled="!formIsValid" + variant="success" + @click="submitCustomMetricsForm" + > + {{ __('Save changes') }} + </gl-deprecated-button> + </div> + </gl-modal> + </div> + + <div + v-if="selectedDashboard && selectedDashboard.can_edit" + class="mb-2 mr-2 d-flex d-sm-block" + > + <gl-deprecated-button + class="flex-grow-1 js-edit-link" + :href="selectedDashboard.project_blob_path" + data-qa-selector="edit_dashboard_button" + > + {{ __('Edit dashboard') }} + </gl-deprecated-button> + </div> + + <div v-if="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" + :href="externalDashboardUrl" + target="_blank" + rel="noopener noreferrer" + > + {{ __('View full dashboard') }} <icon name="external-link" /> + </gl-deprecated-button> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel.vue b/app/assets/javascripts/monitoring/components/dashboard_panel.vue index 48825fda5c8..9545a211bbd 100644 --- a/app/assets/javascripts/monitoring/components/dashboard_panel.vue +++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue @@ -6,8 +6,9 @@ import { GlResizeObserverDirective, GlIcon, GlLoadingIcon, - GlDropdown, - GlDropdownItem, + GlNewDropdown as GlDropdown, + GlNewDropdownItem as GlDropdownItem, + GlNewDropdownDivider as GlDropdownDivider, GlModal, GlModalDirective, GlTooltip, @@ -28,6 +29,7 @@ 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', @@ -43,6 +45,7 @@ export default { GlTooltip, GlDropdown, GlDropdownItem, + GlDropdownDivider, GlModal, }, directives: { @@ -115,9 +118,15 @@ export default { timeRange(state) { return state[this.namespace].timeRange; }, + dashboardTimezone(state) { + return state[this.namespace].dashboardTimezone; + }, metricsSavedToDb(state, getters) { return getters[`${this.namespace}/metricsSavedToDb`]; }, + selectedDashboard(state, getters) { + return getters[`${this.namespace}/selectedDashboard`]; + }, }), title() { return this.graphData?.title || ''; @@ -266,6 +275,9 @@ export default { this.$delete(this.allAlerts, alertPath); } }, + safeUrl(url) { + return isSafeURL(url) ? url : '#'; + }, }, panelTypes, }; @@ -276,7 +288,8 @@ export default { <slot name="topLeft"></slot> <h5 ref="graphTitle" - class="prometheus-graph-title gl-font-lg font-weight-bold text-truncate append-right-8" + class="prometheus-graph-title gl-font-lg font-weight-bold text-truncate gl-mr-3" + tabindex="0" > {{ title }} </h5> @@ -304,14 +317,13 @@ export default { <div class="d-flex align-items-center"> <gl-dropdown v-gl-tooltip - toggle-class="btn btn-transparent border-0" + toggle-class="shadow-none border-0" data-qa-selector="prometheus_widgets_dropdown" right - no-caret :title="__('More actions')" > <template slot="button-content"> - <gl-icon name="ellipsis_v" class="text-secondary" /> + <gl-icon name="ellipsis_v" class="dropdown-icon text-secondary" /> </template> <gl-dropdown-item v-if="expandBtnAvailable" @@ -362,6 +374,23 @@ export default { > {{ __('Alerts') }} </gl-dropdown-item> + + <template v-if="graphData.links.length"> + <gl-dropdown-divider /> + <gl-dropdown-item + v-for="(link, index) in graphData.links" + :key="index" + :href="safeUrl(link.url)" + class="text-break" + >{{ link.title }}</gl-dropdown-item + > + </template> + <template v-if="selectedDashboard && selectedDashboard.can_edit"> + <gl-dropdown-divider /> + <gl-dropdown-item ref="manageLinksItem" :href="selectedDashboard.project_blob_path">{{ + s__('Metrics|Manage chart links') + }}</gl-dropdown-item> + </template> </gl-dropdown> </div> </div> @@ -372,6 +401,7 @@ export default { :is="basicChartComponent" v-else-if="basicChartComponent" :graph-data="graphData" + :timezone="dashboardTimezone" v-bind="$attrs" v-on="$listeners" /> @@ -385,6 +415,7 @@ export default { :project-path="projectPath" :thresholds="getGraphAlertValues(graphData.metrics)" :group-id="groupId" + :timezone="dashboardTimezone" v-bind="$attrs" v-on="$listeners" @datazoom="onDatazoom" diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue index 5a7981b6534..08fcfa3bc56 100644 --- a/app/assets/javascripts/monitoring/components/graph_group.vue +++ b/app/assets/javascripts/monitoring/components/graph_group.vue @@ -52,10 +52,17 @@ export default { </script> <template> - <div v-if="showPanels" ref="graph-group" class="card prometheus-panel"> + <div v-if="showPanels" ref="graph-group" class="card prometheus-panel" tabindex="0"> <div class="card-header d-flex align-items-center"> <h4 class="flex-grow-1">{{ name }}</h4> - <a role="button" class="js-graph-group-toggle" @click="collapse"> + <a + data-testid="group-toggle-button" + role="button" + class="js-graph-group-toggle gl-text-gray-900" + tabindex="0" + @click="collapse" + @keyup.enter="collapse" + > <icon :size="16" :aria-label="__('Toggle collapse')" :name="caretIcon" /> </a> </div> diff --git a/app/assets/javascripts/monitoring/components/links_section.vue b/app/assets/javascripts/monitoring/components/links_section.vue new file mode 100644 index 00000000000..98b07d17694 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/links_section.vue @@ -0,0 +1,32 @@ +<script> +import { mapGetters } from 'vuex'; +import { GlIcon, GlLink } from '@gitlab/ui'; + +export default { + components: { + GlIcon, + GlLink, + }, + computed: { + ...mapGetters('monitoringDashboard', { links: 'linksWithMetadata' }), + }, +}; +</script> +<template> + <div + ref="linksSection" + class="gl-display-sm-flex gl-flex-sm-wrap gl-mt-5 gl-p-3 gl-bg-gray-10 border gl-rounded-base links-section" + > + <div + v-for="(link, key) in links" + :key="key" + class="gl-mb-1 gl-mr-5 gl-display-flex gl-display-sm-block gl-hover-text-blue-600-children gl-word-break-all" + > + <gl-link :href="link.url" class="gl-text-gray-900 gl-text-decoration-none!" + ><gl-icon name="link" class="gl-text-gray-700 gl-vertical-align-text-bottom gl-mr-2" />{{ + link.title + }} + </gl-link> + </div> + </div> +</template> diff --git a/app/assets/javascripts/monitoring/components/variables_section.vue b/app/assets/javascripts/monitoring/components/variables_section.vue index e054c9d8e26..3d1d111d5b3 100644 --- a/app/assets/javascripts/monitoring/components/variables_section.vue +++ b/app/assets/javascripts/monitoring/components/variables_section.vue @@ -2,7 +2,7 @@ import { mapState, mapActions } from 'vuex'; import CustomVariable from './variables/custom_variable.vue'; import TextVariable from './variables/text_variable.vue'; -import { setPromCustomVariablesFromUrl } from '../utils'; +import { setCustomVariablesFromUrl } from '../utils'; export default { components: { @@ -10,23 +10,21 @@ export default { TextVariable, }, computed: { - ...mapState('monitoringDashboard', ['promVariables']), + ...mapState('monitoringDashboard', ['variables']), }, methods: { - ...mapActions('monitoringDashboard', ['fetchDashboardData', 'updateVariableValues']), + ...mapActions('monitoringDashboard', ['updateVariablesAndFetchData']), refreshDashboard(variable, value) { - if (this.promVariables[variable].value !== value) { + if (this.variables[variable].value !== value) { const changedVariable = { key: variable, value }; // update the Vuex store - this.updateVariableValues(changedVariable); + this.updateVariablesAndFetchData(changedVariable); // the below calls can ideally be moved out of the // component and into the actions and let the // mutation respond directly. // This can be further investigate in // https://gitlab.com/gitlab-org/gitlab/-/issues/217713 - setPromCustomVariablesFromUrl(this.promVariables); - // fetch data - this.fetchDashboardData(); + setCustomVariablesFromUrl(this.variables); } }, variableComponent(type) { @@ -41,7 +39,7 @@ export default { </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 promVariables" :key="key" class="mb-1 pr-2 d-flex d-sm-block"> + <div v-for="(variable, key) in variables" :key="key" class="mb-1 pr-2 d-flex d-sm-block"> <component :is="variableComponent(variable.type)" class="mb-0 flex-grow-1" diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js index 0c2eafeed54..50330046c99 100644 --- a/app/assets/javascripts/monitoring/constants.js +++ b/app/assets/javascripts/monitoring/constants.js @@ -127,9 +127,25 @@ export const lineWidths = { default: 2, }; -export const dateFormats = { - timeOfDay: 'h:MM TT', - default: 'dd mmm yyyy, h:MMTT', +/** + * User-defined links can be passed in dashboard yml file. + * These are the supported type of links. + */ +export const linkTypes = { + GRAFANA: 'grafana', +}; + +/** + * These are the supported values for the GitLab-UI + * chart legend layout. + * + * Currently defined in + * https://gitlab.com/gitlab-org/gitlab-ui/-/blob/master/src/utils/charts/constants.js + * + */ +export const legendLayoutTypes = { + inline: 'inline', + table: 'table', }; /** @@ -140,7 +156,6 @@ export const dateFormats = { * Currently used in `receiveMetricsDashboardSuccess` action. */ export const endpointKeys = [ - 'metricsEndpoint', 'deploymentsEndpoint', 'dashboardEndpoint', 'dashboardsEndpoint', diff --git a/app/assets/javascripts/monitoring/format_date.js b/app/assets/javascripts/monitoring/format_date.js new file mode 100644 index 00000000000..a50d441a09e --- /dev/null +++ b/app/assets/javascripts/monitoring/format_date.js @@ -0,0 +1,39 @@ +import dateFormat from 'dateformat'; + +export const timezones = { + /** + * Renders a date with a local timezone + */ + LOCAL: 'LOCAL', + + /** + * Renders at date with UTC + */ + UTC: 'UTC', +}; + +export const formats = { + shortTime: 'h:MM TT', + default: 'dd mmm yyyy, h:MMTT (Z)', +}; + +/** + * Formats a date for a metric dashboard or chart. + * + * Convenience wrapper of dateFormat with default formats + * and settings. + * + * dateFormat has some limitations and we could use `toLocaleString` instead + * See: https://gitlab.com/gitlab-org/gitlab/-/issues/219246 + * + * @param {Date|String|Number} date + * @param {Object} options - Formatting options + * @param {string} options.format - Format or mask from `formats`. + * @param {string} options.timezone - Timezone abbreviation. + * Accepts "LOCAL" for the client local timezone. + */ +export const formatDate = (date, options = {}) => { + const { format = formats.default, timezone = timezones.LOCAL } = options; + const useUTC = timezone === timezones.UTC; + return dateFormat(date, format, useUTC); +}; diff --git a/app/assets/javascripts/monitoring/monitoring_app.js b/app/assets/javascripts/monitoring/monitoring_app.js new file mode 100644 index 00000000000..08543fa6eb3 --- /dev/null +++ b/app/assets/javascripts/monitoring/monitoring_app.js @@ -0,0 +1,59 @@ +import Vue from 'vue'; +import { GlToast } from '@gitlab/ui'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import { getParameterValues } from '~/lib/utils/url_utility'; +import { createStore } from './stores'; +import createRouter from './router'; + +Vue.use(GlToast); + +export default (props = {}) => { + const el = document.getElementById('prometheus-graphs'); + + if (el && el.dataset) { + const [currentDashboard] = getParameterValues('dashboard'); + + const { + deploymentsEndpoint, + dashboardEndpoint, + dashboardsEndpoint, + projectPath, + logsPath, + currentEnvironmentName, + dashboardTimezone, + metricsDashboardBasePath, + ...dataProps + } = el.dataset; + + const store = createStore({ + currentDashboard, + deploymentsEndpoint, + dashboardEndpoint, + dashboardsEndpoint, + dashboardTimezone, + projectPath, + logsPath, + currentEnvironmentName, + }); + + // HTML attributes are always strings, parse other types. + dataProps.hasMetrics = parseBoolean(dataProps.hasMetrics); + dataProps.customMetricsAvailable = parseBoolean(dataProps.customMetricsAvailable); + dataProps.prometheusAlertsAvailable = parseBoolean(dataProps.prometheusAlertsAvailable); + + const router = createRouter(metricsDashboardBasePath); + + // eslint-disable-next-line no-new + new Vue({ + el, + store, + router, + data() { + return { + dashboardProps: { ...dataProps, ...props }, + }; + }, + template: `<router-view :dashboardProps="dashboardProps"/>`, + }); + } +}; diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js deleted file mode 100644 index 2bbf9ef9d78..00000000000 --- a/app/assets/javascripts/monitoring/monitoring_bundle.js +++ /dev/null @@ -1,32 +0,0 @@ -import Vue from 'vue'; -import { GlToast } from '@gitlab/ui'; -import Dashboard from '~/monitoring/components/dashboard.vue'; -import { parseBoolean } from '~/lib/utils/common_utils'; -import { getParameterValues } from '~/lib/utils/url_utility'; -import store from './stores'; - -Vue.use(GlToast); - -export default (props = {}) => { - const el = document.getElementById('prometheus-graphs'); - - if (el && el.dataset) { - const [currentDashboard] = getParameterValues('dashboard'); - - // eslint-disable-next-line no-new - new Vue({ - el, - store, - render(createElement) { - return createElement(Dashboard, { - props: { - ...el.dataset, - currentDashboard, - hasMetrics: parseBoolean(el.dataset.hasMetrics), - ...props, - }, - }); - }, - }); - } -}; diff --git a/app/assets/javascripts/monitoring/monitoring_bundle_with_alerts.js b/app/assets/javascripts/monitoring/monitoring_bundle_with_alerts.js deleted file mode 100644 index afe5ee0938d..00000000000 --- a/app/assets/javascripts/monitoring/monitoring_bundle_with_alerts.js +++ /dev/null @@ -1,13 +0,0 @@ -import { parseBoolean } from '~/lib/utils/common_utils'; -import initCeBundle from '~/monitoring/monitoring_bundle'; - -export default () => { - const el = document.getElementById('prometheus-graphs'); - - if (el && el.dataset) { - initCeBundle({ - customMetricsAvailable: parseBoolean(el.dataset.customMetricsAvailable), - prometheusAlertsAvailable: parseBoolean(el.dataset.prometheusAlertsAvailable), - }); - } -}; diff --git a/app/assets/javascripts/monitoring/pages/dashboard_page.vue b/app/assets/javascripts/monitoring/pages/dashboard_page.vue new file mode 100644 index 00000000000..519a20d7be3 --- /dev/null +++ b/app/assets/javascripts/monitoring/pages/dashboard_page.vue @@ -0,0 +1,18 @@ +<script> +import Dashboard from '../components/dashboard.vue'; + +export default { + components: { + Dashboard, + }, + props: { + dashboardProps: { + type: Object, + required: true, + }, + }, +}; +</script> +<template> + <dashboard v-bind="{ ...dashboardProps }" /> +</template> diff --git a/app/assets/javascripts/monitoring/router/constants.js b/app/assets/javascripts/monitoring/router/constants.js new file mode 100644 index 00000000000..acfcd03f928 --- /dev/null +++ b/app/assets/javascripts/monitoring/router/constants.js @@ -0,0 +1,3 @@ +export const BASE_DASHBOARD_PAGE = 'dashboard'; + +export default {}; diff --git a/app/assets/javascripts/monitoring/router/index.js b/app/assets/javascripts/monitoring/router/index.js new file mode 100644 index 00000000000..12692612bbc --- /dev/null +++ b/app/assets/javascripts/monitoring/router/index.js @@ -0,0 +1,15 @@ +import Vue from 'vue'; +import VueRouter from 'vue-router'; +import routes from './routes'; + +Vue.use(VueRouter); + +export default function createRouter(base) { + const router = new VueRouter({ + base, + mode: 'history', + routes, + }); + + return router; +} diff --git a/app/assets/javascripts/monitoring/router/routes.js b/app/assets/javascripts/monitoring/router/routes.js new file mode 100644 index 00000000000..1e0cc1715a7 --- /dev/null +++ b/app/assets/javascripts/monitoring/router/routes.js @@ -0,0 +1,18 @@ +import DashboardPage from '../pages/dashboard_page.vue'; + +import { BASE_DASHBOARD_PAGE } from './constants'; + +/** + * Because the cluster health page uses the dashboard + * app instead the of the dashboard component, hitting + * `/` route is not possible. Hence using `*` until the + * health page is refactored. + * https://gitlab.com/gitlab-org/gitlab/-/issues/221096 + */ +export default [ + { + name: BASE_DASHBOARD_PAGE, + path: '*', + component: DashboardPage, + }, +]; diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js index 9e3edfb495d..3a9cccec438 100644 --- a/app/assets/javascripts/monitoring/stores/actions.js +++ b/app/assets/javascripts/monitoring/stores/actions.js @@ -3,8 +3,6 @@ import * as types from './mutation_types'; import axios from '~/lib/utils/axios_utils'; import createFlash from '~/flash'; import { convertToFixedRange } from '~/lib/utils/datetime_range'; -import { parseTemplatingVariables } from './variable_mapping'; -import { mergeURLVariables } from '../utils'; import { gqClient, parseEnvironmentsResponse, @@ -161,7 +159,6 @@ export const receiveMetricsDashboardSuccess = ({ commit, dispatch }, { response commit(types.SET_ALL_DASHBOARDS, all_dashboards); commit(types.RECEIVE_METRICS_DASHBOARD_SUCCESS, dashboard); - commit(types.SET_VARIABLES, mergeURLVariables(parseTemplatingVariables(dashboard.templating))); commit(types.SET_ENDPOINTS, convertObjectPropsToCamelCase(metrics_data)); return dispatch('fetchDashboardData'); @@ -223,7 +220,7 @@ export const fetchPrometheusMetric = ( queryParams.step = metric.step; } - if (Object.keys(state.promVariables).length > 0) { + if (Object.keys(state.variables).length > 0) { queryParams = { ...queryParams, ...getters.getCustomVariablesParams, @@ -317,8 +314,7 @@ export const receiveEnvironmentsDataFailure = ({ commit }) => { export const fetchAnnotations = ({ state, dispatch }) => { const { start } = convertToFixedRange(state.timeRange); - const dashboardPath = - state.currentDashboard === '' ? DEFAULT_DASHBOARD_PATH : state.currentDashboard; + const dashboardPath = state.currentDashboard || DEFAULT_DASHBOARD_PATH; return gqClient .mutate({ mutation: getAnnotations, @@ -373,7 +369,7 @@ export const toggleStarredValue = ({ commit, state, getters }) => { method, }) .then(() => { - commit(types.RECEIVE_DASHBOARD_STARRING_SUCCESS, newStarredValue); + commit(types.RECEIVE_DASHBOARD_STARRING_SUCCESS, { selectedDashboard, newStarredValue }); }) .catch(() => { commit(types.RECEIVE_DASHBOARD_STARRING_FAILURE); @@ -419,8 +415,10 @@ export const duplicateSystemDashboard = ({ state }, payload) => { // Variables manipulation -export const updateVariableValues = ({ commit }, updatedVariable) => { - commit(types.UPDATE_VARIABLE_VALUES, updatedVariable); +export const updateVariablesAndFetchData = ({ commit, dispatch }, updatedVariable) => { + commit(types.UPDATE_VARIABLES, updatedVariable); + + return dispatch('fetchDashboardData'); }; // prevent babel-plugin-rewire from generating an invalid default during karma tests diff --git a/app/assets/javascripts/monitoring/stores/getters.js b/app/assets/javascripts/monitoring/stores/getters.js index f309addee6b..b7681012472 100644 --- a/app/assets/javascripts/monitoring/stores/getters.js +++ b/app/assets/javascripts/monitoring/stores/getters.js @@ -1,5 +1,5 @@ import { NOT_IN_DB_PREFIX } from '../constants'; -import { addPrefixToCustomVariableParams } from './utils'; +import { addPrefixToCustomVariableParams, addDashboardMetaDataToLink } from './utils'; const metricsIdsInPanel = panel => panel.metrics.filter(metric => metric.metricId && metric.result).map(metric => metric.metricId); @@ -113,6 +113,22 @@ export const filteredEnvironments = state => ); /** + * User-defined links from the yml file can have other + * dashboard-related metadata baked into it. This method + * returns modified links which will get rendered in the + * metrics dashboard + * + * @param {Object} state + * @returns {Array} modified array of links + */ +export const linksWithMetadata = state => { + const metadata = { + timeRange: state.timeRange, + }; + return state.links?.map(addDashboardMetaDataToLink(metadata)); +}; + +/** * Maps an variables object to an array along with stripping * the variable prefix. * @@ -133,8 +149,8 @@ export const filteredEnvironments = state => */ export const getCustomVariablesParams = state => - Object.keys(state.promVariables).reduce((acc, variable) => { - acc[addPrefixToCustomVariableParams(variable)] = state.promVariables[variable]?.value; + Object.keys(state.variables).reduce((acc, variable) => { + acc[addPrefixToCustomVariableParams(variable)] = state.variables[variable]?.value; return acc; }, {}); diff --git a/app/assets/javascripts/monitoring/stores/index.js b/app/assets/javascripts/monitoring/stores/index.js index f08a6402aa6..213a8508aa2 100644 --- a/app/assets/javascripts/monitoring/stores/index.js +++ b/app/assets/javascripts/monitoring/stores/index.js @@ -15,11 +15,15 @@ export const monitoringDashboard = { state, }; -export const createStore = () => +export const createStore = (initState = {}) => new Vuex.Store({ modules: { - monitoringDashboard, + monitoringDashboard: { + ...monitoringDashboard, + state: { + ...state(), + ...initState, + }, + }, }, }); - -export default createStore(); diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js index d60334609fd..4593461776b 100644 --- a/app/assets/javascripts/monitoring/stores/mutation_types.js +++ b/app/assets/javascripts/monitoring/stores/mutation_types.js @@ -3,7 +3,7 @@ export const REQUEST_METRICS_DASHBOARD = 'REQUEST_METRICS_DASHBOARD'; export const RECEIVE_METRICS_DASHBOARD_SUCCESS = 'RECEIVE_METRICS_DASHBOARD_SUCCESS'; export const RECEIVE_METRICS_DASHBOARD_FAILURE = 'RECEIVE_METRICS_DASHBOARD_FAILURE'; export const SET_VARIABLES = 'SET_VARIABLES'; -export const UPDATE_VARIABLE_VALUES = 'UPDATE_VARIABLE_VALUES'; +export const UPDATE_VARIABLES = 'UPDATE_VARIABLES'; export const REQUEST_DASHBOARD_STARRING = 'REQUEST_DASHBOARD_STARRING'; export const RECEIVE_DASHBOARD_STARRING_SUCCESS = 'RECEIVE_DASHBOARD_STARRING_SUCCESS'; diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js index f41cf3fc477..2d63fdd6e34 100644 --- a/app/assets/javascripts/monitoring/stores/mutations.js +++ b/app/assets/javascripts/monitoring/stores/mutations.js @@ -1,7 +1,6 @@ import Vue from 'vue'; import { pick } from 'lodash'; import * as types from './mutation_types'; -import { selectedDashboard } from './getters'; import { mapToDashboardViewModel, normalizeQueryResult } from './utils'; import { BACKOFF_TIMEOUT } from '../../lib/utils/common_utils'; import { endpointKeys, initialStateKeys, metricStates } from '../constants'; @@ -61,8 +60,14 @@ export default { state.emptyState = 'loading'; state.showEmptyState = true; }, - [types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, dashboard) { - state.dashboard = mapToDashboardViewModel(dashboard); + [types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, dashboardYML) { + const { dashboard, panelGroups, variables, links } = mapToDashboardViewModel(dashboardYML); + state.dashboard = { + dashboard, + panelGroups, + }; + state.variables = variables; + state.links = links; if (!state.dashboard.panelGroups.length) { state.emptyState = 'noData'; @@ -76,15 +81,14 @@ export default { [types.REQUEST_DASHBOARD_STARRING](state) { state.isUpdatingStarredValue = true; }, - [types.RECEIVE_DASHBOARD_STARRING_SUCCESS](state, newStarredValue) { - const dashboard = selectedDashboard(state); - const index = state.allDashboards.findIndex(d => d === dashboard); + [types.RECEIVE_DASHBOARD_STARRING_SUCCESS](state, { selectedDashboard, newStarredValue }) { + const index = state.allDashboards.findIndex(d => d === selectedDashboard); state.isUpdatingStarredValue = false; // Trigger state updates in the reactivity system for this change // https://vuejs.org/v2/guide/reactivity.html#For-Arrays - Vue.set(state.allDashboards, index, { ...dashboard, starred: newStarredValue }); + Vue.set(state.allDashboards, index, { ...selectedDashboard, starred: newStarredValue }); }, [types.RECEIVE_DASHBOARD_STARRING_FAILURE](state) { state.isUpdatingStarredValue = false; @@ -189,11 +193,11 @@ export default { state.expandedPanel.panel = panel; }, [types.SET_VARIABLES](state, variables) { - state.promVariables = variables; + state.variables = variables; }, - [types.UPDATE_VARIABLE_VALUES](state, updatedVariable) { - Object.assign(state.promVariables[updatedVariable.key], { - ...state.promVariables[updatedVariable.key], + [types.UPDATE_VARIABLES](state, updatedVariable) { + Object.assign(state.variables[updatedVariable.key], { + ...state.variables[updatedVariable.key], value: updatedVariable.value, }); }, diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js index 9ae1da93e5f..8000f27c0d5 100644 --- a/app/assets/javascripts/monitoring/stores/state.js +++ b/app/assets/javascripts/monitoring/stores/state.js @@ -1,10 +1,11 @@ import invalidUrl from '~/lib/utils/invalid_url'; +import { timezones } from '../format_date'; export default () => ({ // API endpoints - metricsEndpoint: null, deploymentsEndpoint: null, dashboardEndpoint: invalidUrl, + dashboardsEndpoint: invalidUrl, // Dashboard request parameters timeRange: null, @@ -34,14 +35,24 @@ export default () => ({ panel: null, }, allDashboards: [], - promVariables: {}, - + /** + * User-defined custom variables are passed + * via the dashboard yml file. + */ + variables: {}, + /** + * User-defined custom links are passed + * via the dashboard yml file. + */ + links: [], // Other project data + dashboardTimezone: timezones.LOCAL, annotations: [], deploymentData: [], environments: [], environmentsSearchTerm: '', environmentsLoading: false, + currentEnvironmentName: null, // GitLab paths to other pages projectPath: null, diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js index b6817e7279a..058fab5f4fc 100644 --- a/app/assets/javascripts/monitoring/stores/utils.js +++ b/app/assets/javascripts/monitoring/stores/utils.js @@ -2,7 +2,11 @@ import { slugify } from '~/lib/utils/text_utility'; import createGqClient, { fetchPolicies } from '~/lib/graphql'; import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { NOT_IN_DB_PREFIX } from '../constants'; +import { parseTemplatingVariables } from './variable_mapping'; +import { NOT_IN_DB_PREFIX, linkTypes } from '../constants'; +import { DATETIME_RANGE_TYPES } from '~/lib/utils/constants'; +import { timeRangeToParams, getRangeType } from '~/lib/utils/datetime_range'; +import { isSafeURL, mergeUrlParams } from '~/lib/utils/url_utility'; export const gqClient = createGqClient( {}, @@ -138,6 +142,24 @@ const mapYAxisToViewModel = ({ }; /** + * Maps a link to its view model, expects an url and + * (optionally) a title. + * + * Unsafe URLs are ignored. + * + * @param {Object} Link + * @returns {Object} Link object with a `title`, `url` and `type` + * + */ +const mapLinksToViewModel = ({ url = null, title = '', type } = {}) => { + return { + title: title || String(url), + type, + url: url && isSafeURL(url) ? String(url) : '#', + }; +}; + +/** * Maps a metrics panel to its view model * * @param {Object} panel - Metrics panel @@ -152,6 +174,7 @@ const mapPanelToViewModel = ({ y_label, y_axis = {}, metrics = [], + links = [], max_value, }) => { // Both `x_axis.name` and `x_label` are supported for now @@ -171,7 +194,8 @@ const mapPanelToViewModel = ({ yAxis, xAxis, maxValue: max_value, - metrics: mapToMetricsViewModel(metrics, yAxis.name), + links: links.map(mapLinksToViewModel), + metrics: mapToMetricsViewModel(metrics), }; }; @@ -190,6 +214,66 @@ const mapToPanelGroupViewModel = ({ group = '', panels = [] }, i) => { }; /** + * Convert dashboard time range to Grafana + * dashboards time range. + * + * @param {Object} timeRange + * @returns {Object} + */ +export const convertToGrafanaTimeRange = timeRange => { + const timeRangeType = getRangeType(timeRange); + if (timeRangeType === DATETIME_RANGE_TYPES.fixed) { + return { + from: new Date(timeRange.start).getTime(), + to: new Date(timeRange.end).getTime(), + }; + } else if (timeRangeType === DATETIME_RANGE_TYPES.rolling) { + const { seconds } = timeRange.duration; + return { + from: `now-${seconds}s`, + to: 'now', + }; + } + // fallback to returning the time range as is + return timeRange; +}; + +/** + * Convert dashboard time ranges to other supported + * link formats. + * + * @param {Object} timeRange metrics dashboard time range + * @param {String} type type of link + * @returns {String} + */ +export const convertTimeRanges = (timeRange, type) => { + if (type === linkTypes.GRAFANA) { + return convertToGrafanaTimeRange(timeRange); + } + return timeRangeToParams(timeRange); +}; + +/** + * Adds dashboard-related metadata to the user-defined links. + * + * As of %13.1, metadata only includes timeRange but in the + * future more info will be added to the links. + * + * @param {Object} metadata + * @returns {Function} + */ +export const addDashboardMetaDataToLink = metadata => link => { + let modifiedLink = { ...link }; + if (metadata.timeRange) { + modifiedLink = { + ...modifiedLink, + url: mergeUrlParams(convertTimeRanges(metadata.timeRange, link.type), link.url), + }; + } + return modifiedLink; +}; + +/** * Maps a dashboard json object to its view model * * @param {Object} dashboard - Dashboard object @@ -197,13 +281,33 @@ const mapToPanelGroupViewModel = ({ group = '', panels = [] }, i) => { * @param {Array} dashboard.panel_groups - Panel groups array * @returns {Object} */ -export const mapToDashboardViewModel = ({ dashboard = '', panel_groups = [] }) => { +export const mapToDashboardViewModel = ({ + dashboard = '', + templating = {}, + links = [], + panel_groups = [], +}) => { return { dashboard, + variables: parseTemplatingVariables(templating), + links: links.map(mapLinksToViewModel), panelGroups: panel_groups.map(mapToPanelGroupViewModel), }; }; +/** + * Processes a single Range vector, part of the result + * of type `matrix` in the form: + * + * { + * "metric": { "<label_name>": "<label_value>", ... }, + * "values": [ [ <unix_time>, "<sample_value>" ], ... ] + * }, + * + * See https://prometheus.io/docs/prometheus/latest/querying/api/#range-vectors + * + * @param {*} timeSeries + */ export const normalizeQueryResult = timeSeries => { let normalizedResult = {}; diff --git a/app/assets/javascripts/monitoring/stores/variable_mapping.js b/app/assets/javascripts/monitoring/stores/variable_mapping.js index bfb469da19e..66b9899f673 100644 --- a/app/assets/javascripts/monitoring/stores/variable_mapping.js +++ b/app/assets/javascripts/monitoring/stores/variable_mapping.js @@ -47,7 +47,7 @@ const textAdvancedVariableParser = advTextVar => ({ */ const normalizeCustomVariableOptions = ({ default: defaultOpt = false, text, value }) => ({ default: defaultOpt, - text, + text: text || value, value, }); diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js index 1f028ffbcad..95d544bd6d4 100644 --- a/app/assets/javascripts/monitoring/utils.js +++ b/app/assets/javascripts/monitoring/utils.js @@ -151,7 +151,7 @@ export const removePrefixFromLabel = label => /** * Convert parsed template variables to an object - * with just keys and values. Prepare the promVariables + * with just keys and values. Prepare the variables * to be added to the URL. Keys of the object will * have a prefix so that these params can be * differentiated from other URL params. @@ -183,15 +183,15 @@ export const getPromCustomVariablesFromUrl = (search = window.location.search) = }; /** - * Update the URL with promVariables. This usually get triggered when + * Update the URL with variables. This usually get triggered when * the user interacts with the dynamic input elements in the monitoring * dashboard header. * - * @param {Object} promVariables user defined variables + * @param {Object} variables user defined variables */ -export const setPromCustomVariablesFromUrl = promVariables => { +export const setCustomVariablesFromUrl = variables => { // prep the variables to append to URL - const parsedVariables = convertVariablesForURL(promVariables); + const parsedVariables = convertVariablesForURL(variables); // update the URL updateHistory({ url: mergeUrlParams(parsedVariables, window.location.href), @@ -262,7 +262,7 @@ export const expandedPanelPayloadFromUrl = (dashboard, search = window.location. * If no group/panel is set, the dashboard URL is returned. * * @param {?String} dashboard - Dashboard path, used as identifier for a dashboard - * @param {?Object} promVariables - Custom variables that came from the URL + * @param {?Object} variables - Custom variables that came from the URL * @param {?String} group - Group Identifier * @param {?Object} panel - Panel object from the dashboard * @param {?String} url - Base URL including current search params @@ -270,14 +270,14 @@ export const expandedPanelPayloadFromUrl = (dashboard, search = window.location. */ export const panelToUrl = ( dashboard = null, - promVariables, + variables, group, panel, url = window.location.href, ) => { const params = { dashboard, - ...promVariables, + ...variables, }; if (group && panel) { |