summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/monitoring
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/monitoring')
-rw-r--r--app/assets/javascripts/monitoring/components/alert_widget.vue6
-rw-r--r--app/assets/javascripts/monitoring/components/alert_widget_form.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/charts/column.vue13
-rw-r--r--app/assets/javascripts/monitoring/components/charts/empty_chart.vue4
-rw-r--r--app/assets/javascripts/monitoring/components/charts/heatmap.vue9
-rw-r--r--app/assets/javascripts/monitoring/components/charts/options.js29
-rw-r--r--app/assets/javascripts/monitoring/components/charts/stacked_column.vue68
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue60
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue407
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_header.vue369
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel.vue43
-rw-r--r--app/assets/javascripts/monitoring/components/graph_group.vue11
-rw-r--r--app/assets/javascripts/monitoring/components/links_section.vue32
-rw-r--r--app/assets/javascripts/monitoring/components/variables_section.vue16
-rw-r--r--app/assets/javascripts/monitoring/constants.js23
-rw-r--r--app/assets/javascripts/monitoring/format_date.js39
-rw-r--r--app/assets/javascripts/monitoring/monitoring_app.js59
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js32
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle_with_alerts.js13
-rw-r--r--app/assets/javascripts/monitoring/pages/dashboard_page.vue18
-rw-r--r--app/assets/javascripts/monitoring/router/constants.js3
-rw-r--r--app/assets/javascripts/monitoring/router/index.js15
-rw-r--r--app/assets/javascripts/monitoring/router/routes.js18
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js16
-rw-r--r--app/assets/javascripts/monitoring/stores/getters.js22
-rw-r--r--app/assets/javascripts/monitoring/stores/index.js12
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js26
-rw-r--r--app/assets/javascripts/monitoring/stores/state.js17
-rw-r--r--app/assets/javascripts/monitoring/stores/utils.js110
-rw-r--r--app/assets/javascripts/monitoring/stores/variable_mapping.js2
-rw-r--r--app/assets/javascripts/monitoring/utils.js16
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) {