Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/monitoring')
-rw-r--r--app/assets/javascripts/monitoring/components/alert_widget.vue12
-rw-r--r--app/assets/javascripts/monitoring/components/alert_widget_form.vue40
-rw-r--r--app/assets/javascripts/monitoring/components/charts/gauge.vue122
-rw-r--r--app/assets/javascripts/monitoring/components/charts/heatmap.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/charts/options.js64
-rw-r--r--app/assets/javascripts/monitoring/components/charts/single_stat.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue17
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue32
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue291
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_header.vue287
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel.vue84
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue199
-rw-r--r--app/assets/javascripts/monitoring/components/dashboards_dropdown.vue76
-rw-r--r--app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/group_empty_state.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/links_section.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/refresh_button.vue23
-rw-r--r--app/assets/javascripts/monitoring/components/variables/dropdown_field.vue17
-rw-r--r--app/assets/javascripts/monitoring/constants.js11
-rw-r--r--app/assets/javascripts/monitoring/csv_export.js147
-rw-r--r--app/assets/javascripts/monitoring/pages/panel_new_page.vue45
-rw-r--r--app/assets/javascripts/monitoring/queries/getAnnotations.query.graphql1
-rw-r--r--app/assets/javascripts/monitoring/queries/getEnvironments.query.graphql1
-rw-r--r--app/assets/javascripts/monitoring/requests/index.js46
-rw-r--r--app/assets/javascripts/monitoring/router/constants.js9
-rw-r--r--app/assets/javascripts/monitoring/router/routes.js13
-rw-r--r--app/assets/javascripts/monitoring/services/alerts_service.js25
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js130
-rw-r--r--app/assets/javascripts/monitoring/stores/embed_group/actions.js3
-rw-r--r--app/assets/javascripts/monitoring/stores/embed_group/getters.js3
-rw-r--r--app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js3
-rw-r--r--app/assets/javascripts/monitoring/stores/getters.js3
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js14
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js76
-rw-r--r--app/assets/javascripts/monitoring/stores/state.js13
-rw-r--r--app/assets/javascripts/monitoring/stores/utils.js12
-rw-r--r--app/assets/javascripts/monitoring/utils.js6
-rw-r--r--app/assets/javascripts/monitoring/validators.js13
38 files changed, 1409 insertions, 439 deletions
diff --git a/app/assets/javascripts/monitoring/components/alert_widget.vue b/app/assets/javascripts/monitoring/components/alert_widget.vue
index 5562981fe1c..909ae2980d2 100644
--- a/app/assets/javascripts/monitoring/components/alert_widget.vue
+++ b/app/assets/javascripts/monitoring/components/alert_widget.vue
@@ -1,12 +1,12 @@
<script>
import { GlBadge, GlLoadingIcon, GlModalDirective, GlIcon, GlTooltip, GlSprintf } from '@gitlab/ui';
+import { values, get } from 'lodash';
import { s__ } from '~/locale';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import AlertWidgetForm from './alert_widget_form.vue';
import AlertsService from '../services/alerts_service';
import { alertsValidator, queriesValidator } from '../validators';
import { OPERATORS } from '../constants';
-import { values, get } from 'lodash';
export default {
components: {
@@ -174,8 +174,8 @@ export default {
handleSetApiAction(apiAction) {
this.apiAction = apiAction;
},
- handleCreate({ operator, threshold, prometheus_metric_id }) {
- const newAlert = { operator, threshold, prometheus_metric_id };
+ handleCreate({ operator, threshold, prometheus_metric_id, runbookUrl }) {
+ const newAlert = { operator, threshold, prometheus_metric_id, runbookUrl };
this.isLoading = true;
this.service
.createAlert(newAlert)
@@ -189,8 +189,8 @@ export default {
this.isLoading = false;
});
},
- handleUpdate({ alert, operator, threshold }) {
- const updatedAlert = { operator, threshold };
+ handleUpdate({ alert, operator, threshold, runbookUrl }) {
+ const updatedAlert = { operator, threshold, runbookUrl };
this.isLoading = true;
this.service
.updateAlert(alert, updatedAlert)
diff --git a/app/assets/javascripts/monitoring/components/alert_widget_form.vue b/app/assets/javascripts/monitoring/components/alert_widget_form.vue
index b2d7ca0c4e0..5fa0da53a04 100644
--- a/app/assets/javascripts/monitoring/components/alert_widget_form.vue
+++ b/app/assets/javascripts/monitoring/components/alert_widget_form.vue
@@ -7,8 +7,8 @@ import {
GlButtonGroup,
GlFormGroup,
GlFormInput,
- GlDropdown,
- GlDropdownItem,
+ GlNewDropdown as GlDropdown,
+ GlNewDropdownItem as GlDropdownItem,
GlModal,
GlTooltipDirective,
} from '@gitlab/ui';
@@ -88,6 +88,7 @@ export default {
operator: null,
threshold: null,
prometheusMetricId: null,
+ runbookUrl: null,
selectedAlert: {},
alertQuery: '',
};
@@ -116,7 +117,8 @@ export default {
this.operator &&
this.threshold === Number(this.threshold) &&
(this.operator !== this.selectedAlert.operator ||
- this.threshold !== this.selectedAlert.threshold)
+ this.threshold !== this.selectedAlert.threshold ||
+ this.runbookUrl !== this.selectedAlert.runbookUrl)
);
},
submitAction() {
@@ -153,13 +155,17 @@ export default {
const existingAlert = this.alertsToManage[existingAlertPath];
if (existingAlert) {
+ const { operator, threshold, runbookUrl } = existingAlert;
+
this.selectedAlert = existingAlert;
- this.operator = existingAlert.operator;
- this.threshold = existingAlert.threshold;
+ this.operator = operator;
+ this.threshold = threshold;
+ this.runbookUrl = runbookUrl;
} else {
this.selectedAlert = {};
this.operator = this.operators.greaterThan;
this.threshold = null;
+ this.runbookUrl = null;
}
this.prometheusMetricId = queryId;
@@ -168,13 +174,13 @@ export default {
this.resetAlertData();
this.$emit('cancel');
},
- handleSubmit(e) {
- e.preventDefault();
+ handleSubmit() {
this.$emit(this.submitAction, {
alert: this.selectedAlert.alert_path,
operator: this.operator,
threshold: this.threshold,
prometheus_metric_id: this.prometheusMetricId,
+ runbookUrl: this.runbookUrl,
});
},
handleShown() {
@@ -189,6 +195,7 @@ export default {
this.threshold = null;
this.prometheusMetricId = null;
this.selectedAlert = {};
+ this.runbookUrl = null;
},
getAlertFormActionTrackingOption() {
const label = `${this.submitAction}_alert`;
@@ -217,7 +224,7 @@ export default {
:modal-id="modalId"
:ok-variant="submitAction === 'delete' ? 'danger' : 'success'"
:ok-disabled="formDisabled"
- @ok="handleSubmit"
+ @ok.prevent="handleSubmit"
@hidden="handleHidden"
@shown="handleShown"
>
@@ -247,7 +254,7 @@ export default {
<gl-dropdown
id="alert-query-dropdown"
:text="queryDropdownLabel"
- toggle-class="dropdown-menu-toggle qa-alert-query-dropdown"
+ toggle-class="dropdown-menu-toggle gl-border-1! qa-alert-query-dropdown"
>
<gl-dropdown-item
v-for="query in relevantQueries"
@@ -259,7 +266,7 @@ export default {
</gl-dropdown-item>
</gl-dropdown>
</gl-form-group>
- <gl-button-group class="mb-2" :label="s__('PrometheusAlerts|Operator')">
+ <gl-button-group class="mb-3" :label="s__('PrometheusAlerts|Operator')">
<gl-deprecated-button
:class="{ active: operator === operators.greaterThan }"
:disabled="formDisabled"
@@ -294,6 +301,19 @@ export default {
data-qa-selector="alert_threshold_field"
/>
</gl-form-group>
+ <gl-form-group
+ :label="s__('PrometheusAlerts|Runbook URL (optional)')"
+ label-for="alert-runbook"
+ >
+ <gl-form-input
+ id="alert-runbook"
+ v-model="runbookUrl"
+ :disabled="formDisabled"
+ data-testid="alertRunbookField"
+ type="text"
+ :placeholder="s__('PrometheusAlerts|https://gitlab.com/gitlab-com/runbooks')"
+ />
+ </gl-form-group>
</div>
<template #modal-ok>
<gl-link
diff --git a/app/assets/javascripts/monitoring/components/charts/gauge.vue b/app/assets/javascripts/monitoring/components/charts/gauge.vue
new file mode 100644
index 00000000000..63fa60bbdf0
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/charts/gauge.vue
@@ -0,0 +1,122 @@
+<script>
+import { GlResizeObserverDirective } from '@gitlab/ui';
+import { GlGaugeChart } from '@gitlab/ui/dist/charts';
+import { isFinite, isArray, isInteger } from 'lodash';
+import { graphDataValidatorForValues } from '../../utils';
+import { getValidThresholds } from './options';
+import { getFormatter, SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
+
+export default {
+ components: {
+ GlGaugeChart,
+ },
+ directives: {
+ GlResizeObserverDirective,
+ },
+ props: {
+ graphData: {
+ type: Object,
+ required: true,
+ validator: graphDataValidatorForValues.bind(null, true),
+ },
+ },
+ data() {
+ return {
+ width: 0,
+ };
+ },
+ computed: {
+ rangeValues() {
+ let min = 0;
+ let max = 100;
+
+ const { minValue, maxValue } = this.graphData;
+
+ const isValidMinMax = () => {
+ return isFinite(minValue) && isFinite(maxValue) && minValue < maxValue;
+ };
+
+ if (isValidMinMax()) {
+ min = minValue;
+ max = maxValue;
+ }
+
+ return {
+ min,
+ max,
+ };
+ },
+ validThresholds() {
+ const { mode, values } = this.graphData?.thresholds || {};
+ const range = this.rangeValues;
+
+ if (!isArray(values)) {
+ return [];
+ }
+
+ return getValidThresholds({ mode, range, values });
+ },
+ queryResult() {
+ return this.graphData?.metrics[0]?.result[0]?.value[1];
+ },
+ splitValue() {
+ const { split } = this.graphData;
+ const defaultValue = 10;
+
+ return isInteger(split) && split > 0 ? split : defaultValue;
+ },
+ textValue() {
+ const formatFromPanel = this.graphData.format;
+ const defaultFormat = SUPPORTED_FORMATS.engineering;
+ const format = SUPPORTED_FORMATS[formatFromPanel] ?? defaultFormat;
+ const { queryResult } = this;
+
+ const formatter = getFormatter(format);
+
+ return isFinite(queryResult) ? formatter(queryResult) : '--';
+ },
+ thresholdsValue() {
+ /**
+ * If there are no valid thresholds, a default threshold
+ * will be set at 90% of the gauge arcs' max value
+ */
+ const { min, max } = this.rangeValues;
+
+ const defaultThresholdValue = [(max - min) * 0.95];
+ return this.validThresholds.length ? this.validThresholds : defaultThresholdValue;
+ },
+ value() {
+ /**
+ * The gauge chart gitlab-ui component expects a value
+ * of type number.
+ *
+ * So, if the query result is undefined,
+ * we pass the gauge chart a value of NaN.
+ */
+ return this.queryResult || NaN;
+ },
+ },
+ methods: {
+ onResize() {
+ if (!this.$refs.gaugeChart) return;
+ const { width } = this.$refs.gaugeChart.$el.getBoundingClientRect();
+ this.width = width;
+ },
+ },
+};
+</script>
+<template>
+ <div v-gl-resize-observer-directive="onResize">
+ <gl-gauge-chart
+ ref="gaugeChart"
+ v-bind="$attrs"
+ :value="value"
+ :min="rangeValues.min"
+ :max="rangeValues.max"
+ :thresholds="thresholdsValue"
+ :text="textValue"
+ :split-number="splitValue"
+ :width="width"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/charts/heatmap.vue b/app/assets/javascripts/monitoring/components/charts/heatmap.vue
index ddb44f7b1be..7003e2d37cf 100644
--- a/app/assets/javascripts/monitoring/components/charts/heatmap.vue
+++ b/app/assets/javascripts/monitoring/components/charts/heatmap.vue
@@ -36,7 +36,7 @@ export default {
);
},
xAxisName() {
- return this.graphData.x_label || '';
+ return this.graphData.xLabel || '';
},
yAxisName() {
return this.graphData.y_label || '';
diff --git a/app/assets/javascripts/monitoring/components/charts/options.js b/app/assets/javascripts/monitoring/components/charts/options.js
index 42252dd5897..0cd4a02311c 100644
--- a/app/assets/javascripts/monitoring/components/charts/options.js
+++ b/app/assets/javascripts/monitoring/components/charts/options.js
@@ -1,6 +1,8 @@
+import { isFinite, uniq, sortBy, includes } from 'lodash';
import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format';
import { __, s__ } from '~/locale';
import { formatDate, timezones, formats } from '../../format_date';
+import { thresholdModeTypes } from '../../constants';
const yAxisBoundaryGap = [0.1, 0.1];
/**
@@ -109,3 +111,65 @@ export const getTooltipFormatter = ({
const formatter = getFormatter(format);
return num => formatter(num, precision);
};
+
+// Thresholds
+
+/**
+ *
+ * Used to find valid thresholds for the gauge chart
+ *
+ * An array of thresholds values is
+ * - duplicate values are removed;
+ * - filtered for invalid values;
+ * - sorted in ascending order;
+ * - only first two values are used.
+ */
+export const getValidThresholds = ({ mode, range = {}, values = [] } = {}) => {
+ const supportedModes = [thresholdModeTypes.ABSOLUTE, thresholdModeTypes.PERCENTAGE];
+ const { min, max } = range;
+
+ /**
+ * return early if min and max have invalid values
+ * or mode has invalid value
+ */
+ if (!isFinite(min) || !isFinite(max) || min >= max || !includes(supportedModes, mode)) {
+ return [];
+ }
+
+ const uniqueThresholds = uniq(values);
+
+ const numberThresholds = uniqueThresholds.filter(threshold => isFinite(threshold));
+
+ const validThresholds = numberThresholds.filter(threshold => {
+ let isValid;
+
+ if (mode === thresholdModeTypes.PERCENTAGE) {
+ isValid = threshold > 0 && threshold < 100;
+ } else if (mode === thresholdModeTypes.ABSOLUTE) {
+ isValid = threshold > min && threshold < max;
+ }
+
+ return isValid;
+ });
+
+ const transformedThresholds = validThresholds.map(threshold => {
+ let transformedThreshold;
+
+ if (mode === 'percentage') {
+ transformedThreshold = (threshold / 100) * (max - min);
+ } else {
+ transformedThreshold = threshold;
+ }
+
+ return transformedThreshold;
+ });
+
+ const sortedThresholds = sortBy(transformedThresholds);
+
+ const reducedThresholdsArray =
+ sortedThresholds.length > 2
+ ? [sortedThresholds[0], sortedThresholds[1]]
+ : [...sortedThresholds];
+
+ return reducedThresholdsArray;
+};
diff --git a/app/assets/javascripts/monitoring/components/charts/single_stat.vue b/app/assets/javascripts/monitoring/components/charts/single_stat.vue
index 106c76a97dc..a8ab41ebf26 100644
--- a/app/assets/javascripts/monitoring/components/charts/single_stat.vue
+++ b/app/assets/javascripts/monitoring/components/charts/single_stat.vue
@@ -50,7 +50,7 @@ export default {
}
formatter = getFormatter(SUPPORTED_FORMATS.number);
- return `${formatter(this.queryResult, defaultPrecision)}${this.queryInfo.unit}`;
+ return `${formatter(this.queryResult, defaultPrecision)}${this.queryInfo.unit ?? ''}`;
},
graphTitle() {
return this.queryInfo.label;
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
index f2add429a80..054111c203e 100644
--- a/app/assets/javascripts/monitoring/components/charts/time_series.vue
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -1,6 +1,6 @@
<script>
-import { omit, throttle } from 'lodash';
-import { GlLink, GlDeprecatedButton, GlTooltip, GlResizeObserverDirective } from '@gitlab/ui';
+import { isEmpty, omit, throttle } from 'lodash';
+import { GlLink, GlTooltip, GlResizeObserverDirective } from '@gitlab/ui';
import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import { s__ } from '~/locale';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
@@ -25,7 +25,6 @@ export default {
GlAreaChart,
GlLineChart,
GlTooltip,
- GlDeprecatedButton,
GlChartSeriesLabel,
GlLink,
Icon,
@@ -45,6 +44,11 @@ export default {
required: false,
default: () => ({}),
},
+ timeRange: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
seriesConfig: {
type: Object,
required: false,
@@ -174,10 +178,17 @@ export default {
chartOptions() {
const { yAxis, xAxis } = this.option;
const option = omit(this.option, ['series', 'yAxis', 'xAxis']);
+ const xAxisBounds = isEmpty(this.timeRange)
+ ? {}
+ : {
+ min: this.timeRange.start,
+ max: this.timeRange.end,
+ };
const timeXAxis = {
...getTimeAxisOptions({ timezone: this.timezone }),
...xAxis,
+ ...xAxisBounds,
};
const dataYAxis = {
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index bde62275797..24aa7b3f504 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -2,12 +2,12 @@
import { mapActions, mapState, mapGetters } from 'vuex';
import VueDraggable from 'vuedraggable';
import Mousetrap from 'mousetrap';
-import { GlIcon, GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
+import { 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 { deprecatedCreateFlash as createFlash } from '~/flash';
+import { ESC_KEY } from '~/lib/utils/keys';
import { mergeUrlParams, updateHistory } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
import Icon from '~/vue_shared/components/icon.vue';
@@ -34,7 +34,6 @@ export default {
DashboardHeader,
DashboardPanel,
Icon,
- GlIcon,
GlButton,
GraphGroup,
EmptyState,
@@ -48,11 +47,6 @@ export default {
TrackEvent: TrackEventDirective,
},
props: {
- externalDashboardUrl: {
- type: String,
- required: false,
- default: '',
- },
hasMetrics: {
type: Boolean,
required: false,
@@ -72,10 +66,6 @@ export default {
type: String,
required: true,
},
- addDashboardDocumentationPath: {
- type: String,
- required: true,
- },
settingsPath: {
type: String,
required: true,
@@ -320,7 +310,7 @@ export default {
},
onKeyup(event) {
const { key } = event;
- if (key === ESC_KEY || key === ESC_KEY_IE11) {
+ if (key === ESC_KEY) {
this.clearExpandedPanel();
}
},
@@ -398,7 +388,8 @@ export default {
},
},
i18n: {
- goBackLabel: s__('Metrics|Go back (Esc)'),
+ collapsePanelLabel: s__('Metrics|Collapse panel'),
+ collapsePanelTooltip: s__('Metrics|Collapse panel (Esc)'),
},
};
</script>
@@ -409,14 +400,11 @@ export default {
v-if="showHeader"
ref="prometheusGraphsHeader"
class="prometheus-graphs-header d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 border-bottom bg-gray-light"
- :add-dashboard-documentation-path="addDashboardDocumentationPath"
:default-branch="defaultBranch"
:rearrange-panels-available="rearrangePanelsAvailable"
:custom-metrics-available="customMetricsAvailable"
: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"
@@ -441,14 +429,10 @@ export default {
ref="goBackBtn"
v-gl-tooltip
class="mr-3 my-3"
- :title="$options.i18n.goBackLabel"
+ :title="$options.i18n.collapsePanelTooltip"
@click="onGoBack"
>
- <gl-icon
- name="arrow-left"
- :aria-label="$options.i18n.goBackLabel"
- class="text-secondary"
- />
+ {{ $options.i18n.collapsePanelLabel }}
</gl-button>
</template>
</dashboard-panel>
diff --git a/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
new file mode 100644
index 00000000000..68afa2ace01
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
@@ -0,0 +1,291 @@
+<script>
+import { mapState, mapGetters, mapActions } from 'vuex';
+import {
+ GlDeprecatedButton,
+ GlNewDropdown,
+ GlNewDropdownDivider,
+ GlNewDropdownItem,
+ GlModal,
+ GlIcon,
+ GlModalDirective,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
+import { PANEL_NEW_PAGE } from '../router/constants';
+import DuplicateDashboardModal from './duplicate_dashboard_modal.vue';
+import CreateDashboardModal from './create_dashboard_modal.vue';
+import { s__ } from '~/locale';
+import invalidUrl from '~/lib/utils/invalid_url';
+import { redirectTo } from '~/lib/utils/url_utility';
+import TrackEventDirective from '~/vue_shared/directives/track_event';
+import { getAddMetricTrackingOptions } from '../utils';
+
+export default {
+ components: {
+ GlDeprecatedButton,
+ GlNewDropdown,
+ GlNewDropdownDivider,
+ GlNewDropdownItem,
+ GlModal,
+ GlIcon,
+ DuplicateDashboardModal,
+ CreateDashboardModal,
+ CustomMetricsFormFields,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ GlTooltip: GlTooltipDirective,
+ TrackEvent: TrackEventDirective,
+ },
+ props: {
+ addingMetricsAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ customMetricsPath: {
+ type: String,
+ required: false,
+ default: invalidUrl,
+ },
+ validateQueryPath: {
+ type: String,
+ required: false,
+ default: invalidUrl,
+ },
+ defaultBranch: {
+ type: String,
+ required: true,
+ },
+ isOotbDashboard: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return { customMetricsFormIsValid: null };
+ },
+ computed: {
+ ...mapState('monitoringDashboard', [
+ 'projectPath',
+ 'isUpdatingStarredValue',
+ 'addDashboardDocumentationPath',
+ ]),
+ ...mapGetters('monitoringDashboard', ['selectedDashboard']),
+ isOutOfTheBoxDashboard() {
+ return this.selectedDashboard?.out_of_the_box_dashboard;
+ },
+ isMenuItemEnabled() {
+ return {
+ addPanel: !this.isOotbDashboard,
+ createDashboard: Boolean(this.projectPath),
+ editDashboard: this.selectedDashboard?.can_edit,
+ };
+ },
+ isMenuItemShown() {
+ return {
+ duplicateDashboard: this.isOutOfTheBoxDashboard,
+ };
+ },
+ newPanelPageLocation() {
+ // Retains params/query if any
+ const { params, query } = this.$route ?? {};
+ return { name: PANEL_NEW_PAGE, params, query };
+ },
+ },
+ methods: {
+ ...mapActions('monitoringDashboard', ['toggleStarredValue']),
+ setFormValidity(isValid) {
+ this.customMetricsFormIsValid = isValid;
+ },
+ hideAddMetricModal() {
+ this.$refs.addMetricModal.hide();
+ },
+ getAddMetricTrackingOptions,
+ submitCustomMetricsForm() {
+ this.$refs.customMetricsForm.submit();
+ },
+ selectDashboard(dashboard) {
+ // Once the sidebar See metrics link is updated to the new URL,
+ // this sort of hardcoding will not be necessary.
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/229277
+ const baseURL = `${this.projectPath}/-/metrics`;
+ const dashboardPath = encodeURIComponent(
+ dashboard.out_of_the_box_dashboard ? dashboard.path : dashboard.display_name,
+ );
+ redirectTo(`${baseURL}/${dashboardPath}`);
+ },
+ },
+
+ modalIds: {
+ addMetric: 'addMetric',
+ createDashboard: 'createDashboard',
+ duplicateDashboard: 'duplicateDashboard',
+ },
+ i18n: {
+ actionsMenu: s__('Metrics|More actions'),
+ duplicateDashboard: s__('Metrics|Duplicate current dashboard'),
+ starDashboard: s__('Metrics|Star dashboard'),
+ unstarDashboard: s__('Metrics|Unstar dashboard'),
+ addMetric: s__('Metrics|Add metric'),
+ addPanel: s__('Metrics|Add panel'),
+ addPanelInfo: s__('Metrics|Duplicate this dashboard to add panel or edit dashboard YAML.'),
+ editDashboardInfo: s__('Metrics|Duplicate this dashboard to add panel or edit dashboard YAML.'),
+ editDashboard: s__('Metrics|Edit dashboard YAML'),
+ createDashboard: s__('Metrics|Create new dashboard'),
+ },
+};
+</script>
+
+<template>
+ <!--
+ This component should be replaced with a variant developed
+ as part of https://gitlab.com/gitlab-org/gitlab-ui/-/issues/936
+ The variant will create a dropdown with an icon, no text and no caret
+ -->
+ <gl-new-dropdown
+ v-gl-tooltip
+ data-testid="actions-menu"
+ data-qa-selector="actions_menu_dropdown"
+ right
+ no-caret
+ toggle-class="gl-px-3!"
+ :title="$options.i18n.actionsMenu"
+ >
+ <template #button-content>
+ <gl-icon class="gl-mr-0!" name="ellipsis_v" />
+ </template>
+
+ <template v-if="addingMetricsAvailable">
+ <gl-new-dropdown-item
+ v-gl-modal="$options.modalIds.addMetric"
+ data-qa-selector="add_metric_button"
+ data-testid="add-metric-item"
+ >
+ {{ $options.i18n.addMetric }}
+ </gl-new-dropdown-item>
+ <gl-modal
+ ref="addMetricModal"
+ :modal-id="$options.modalIds.addMetric"
+ :title="$options.i18n.addMetric"
+ data-testid="add-metric-modal"
+ >
+ <form ref="customMetricsForm" :action="customMetricsPath" method="post">
+ <custom-metrics-form-fields
+ :validate-query-path="validateQueryPath"
+ form-operation="post"
+ @formValidation="setFormValidity"
+ />
+ </form>
+ <div slot="modal-footer">
+ <gl-deprecated-button @click="hideAddMetricModal">
+ {{ __('Cancel') }}
+ </gl-deprecated-button>
+ <gl-deprecated-button
+ v-track-event="getAddMetricTrackingOptions()"
+ data-testid="add-metric-modal-submit-button"
+ :disabled="!customMetricsFormIsValid"
+ variant="success"
+ @click="submitCustomMetricsForm"
+ >
+ {{ __('Save changes') }}
+ </gl-deprecated-button>
+ </div>
+ </gl-modal>
+ </template>
+
+ <gl-new-dropdown-item
+ v-if="isMenuItemEnabled.addPanel"
+ data-testid="add-panel-item-enabled"
+ :to="newPanelPageLocation"
+ >
+ {{ $options.i18n.addPanel }}
+ </gl-new-dropdown-item>
+
+ <!--
+ wrapper for tooltip as button can be `disabled`
+ https://bootstrap-vue.org/docs/components/tooltip#disabled-elements
+ -->
+ <div v-else v-gl-tooltip :title="$options.i18n.addPanelInfo">
+ <gl-new-dropdown-item
+ :alt="$options.i18n.addPanelInfo"
+ :to="newPanelPageLocation"
+ data-testid="add-panel-item-disabled"
+ disabled
+ class="gl-cursor-not-allowed"
+ >
+ <span class="gl-text-gray-400">{{ $options.i18n.addPanel }}</span>
+ </gl-new-dropdown-item>
+ </div>
+
+ <gl-new-dropdown-item
+ v-if="isMenuItemEnabled.editDashboard"
+ :href="selectedDashboard ? selectedDashboard.project_blob_path : null"
+ data-qa-selector="edit_dashboard_button_enabled"
+ data-testid="edit-dashboard-item-enabled"
+ >
+ {{ $options.i18n.editDashboard }}
+ </gl-new-dropdown-item>
+
+ <!--
+ wrapper for tooltip as button can be `disabled`
+ https://bootstrap-vue.org/docs/components/tooltip#disabled-elements
+ -->
+ <div v-else v-gl-tooltip :title="$options.i18n.editDashboardInfo">
+ <gl-new-dropdown-item
+ :alt="$options.i18n.editDashboardInfo"
+ :href="selectedDashboard ? selectedDashboard.project_blob_path : null"
+ data-testid="edit-dashboard-item-disabled"
+ disabled
+ class="gl-cursor-not-allowed"
+ >
+ <span class="gl-text-gray-400">{{ $options.i18n.editDashboard }}</span>
+ </gl-new-dropdown-item>
+ </div>
+
+ <template v-if="isMenuItemShown.duplicateDashboard">
+ <gl-new-dropdown-item
+ v-gl-modal="$options.modalIds.duplicateDashboard"
+ data-testid="duplicate-dashboard-item"
+ >
+ {{ $options.i18n.duplicateDashboard }}
+ </gl-new-dropdown-item>
+
+ <duplicate-dashboard-modal
+ :default-branch="defaultBranch"
+ :modal-id="$options.modalIds.duplicateDashboard"
+ data-testid="duplicate-dashboard-modal"
+ @dashboardDuplicated="selectDashboard"
+ />
+ </template>
+
+ <gl-new-dropdown-item
+ v-if="selectedDashboard"
+ data-testid="star-dashboard-item"
+ :disabled="isUpdatingStarredValue"
+ @click="toggleStarredValue()"
+ >
+ {{ selectedDashboard.starred ? $options.i18n.unstarDashboard : $options.i18n.starDashboard }}
+ </gl-new-dropdown-item>
+
+ <gl-new-dropdown-divider />
+
+ <gl-new-dropdown-item
+ v-gl-modal="$options.modalIds.createDashboard"
+ data-testid="create-dashboard-item"
+ :disabled="!isMenuItemEnabled.createDashboard"
+ :class="{ 'monitoring-actions-item-disabled': !isMenuItemEnabled.createDashboard }"
+ >
+ {{ $options.i18n.createDashboard }}
+ </gl-new-dropdown-item>
+
+ <template v-if="isMenuItemEnabled.createDashboard">
+ <create-dashboard-modal
+ data-testid="create-dashboard-modal"
+ :add-dashboard-documentation-path="addDashboardDocumentationPath"
+ :modal-id="$options.modalIds.createDashboard"
+ :project-path="projectPath"
+ />
+ </template>
+ </gl-new-dropdown>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/dashboard_header.vue b/app/assets/javascripts/monitoring/components/dashboard_header.vue
index fe6ca3a2a07..6a7bf81c643 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_header.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_header.vue
@@ -3,23 +3,14 @@ import { debounce } from 'lodash';
import { mapActions, mapState, mapGetters } from 'vuex';
import {
GlButton,
- GlIcon,
- GlDeprecatedButton,
- GlDropdown,
- GlDropdownItem,
- GlDropdownHeader,
- GlDropdownDivider,
GlNewDropdown,
- GlNewDropdownDivider,
- GlNewDropdownItem,
- GlModal,
GlLoadingIcon,
+ GlNewDropdownItem,
+ GlNewDropdownHeader,
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';
@@ -27,11 +18,9 @@ import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_p
import DashboardsDropdown from './dashboards_dropdown.vue';
import RefreshButton from './refresh_button.vue';
-import CreateDashboardModal from './create_dashboard_modal.vue';
-import DuplicateDashboardModal from './duplicate_dashboard_modal.vue';
+import ActionsMenu from './dashboard_actions_menu.vue';
-import TrackEventDirective from '~/vue_shared/directives/track_event';
-import { getAddMetricTrackingOptions, timeRangeToUrl } from '../utils';
+import { timeRangeToUrl } from '../utils';
import { timeRanges } from '~/vue_shared/constants';
import { timezones } from '../format_date';
@@ -39,30 +28,22 @@ export default {
components: {
Icon,
GlButton,
- GlIcon,
- GlDeprecatedButton,
- GlDropdown,
- GlLoadingIcon,
- GlDropdownItem,
- GlDropdownHeader,
- GlDropdownDivider,
GlNewDropdown,
- GlNewDropdownDivider,
+ GlLoadingIcon,
GlNewDropdownItem,
+ GlNewDropdownHeader,
+
GlSearchBoxByType,
- GlModal,
- CustomMetricsFormFields,
DateTimePicker,
DashboardsDropdown,
RefreshButton,
- DuplicateDashboardModal,
- CreateDashboardModal,
+
+ ActionsMenu,
},
directives: {
GlModal: GlModalDirective,
GlTooltip: GlTooltipDirective,
- TrackEvent: TrackEventDirective,
},
props: {
defaultBranch: {
@@ -89,16 +70,6 @@ export default {
required: false,
default: invalidUrl,
},
- externalDashboardUrl: {
- type: String,
- required: false,
- default: '',
- },
- hasMetrics: {
- type: Boolean,
- required: false,
- default: true,
- },
isRearrangingPanels: {
type: Boolean,
required: true,
@@ -107,32 +78,20 @@ export default {
type: Object,
required: true,
},
- addDashboardDocumentationPath: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- formIsValid: null,
- };
},
computed: {
...mapState('monitoringDashboard', [
'emptyState',
'environmentsLoading',
'currentEnvironmentName',
- 'isUpdatingStarredValue',
'dashboardTimezone',
'projectPath',
'canAccessOperationsSettings',
'operationsSettingsPath',
'currentDashboard',
+ 'externalDashboardUrl',
]),
...mapGetters('monitoringDashboard', ['selectedDashboard', 'filteredEnvironments']),
- isOutOfTheBoxDashboard() {
- return this.selectedDashboard?.out_of_the_box_dashboard;
- },
shouldShowEmptyState() {
return Boolean(this.emptyState);
},
@@ -146,24 +105,27 @@ export default {
// 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
+ this.selectedDashboard?.out_of_the_box_dashboard
);
},
showRearrangePanelsBtn() {
return !this.shouldShowEmptyState && this.rearrangePanelsAvailable;
},
+ environmentDropdownText() {
+ return this.currentEnvironmentName ?? '';
+ },
displayUtc() {
return this.dashboardTimezone === timezones.UTC;
},
- shouldShowActionsMenu() {
- return Boolean(this.projectPath);
- },
shouldShowSettingsButton() {
return this.canAccessOperationsSettings && this.operationsSettingsPath;
},
+ isOOTBDashboard() {
+ return this.selectedDashboard?.out_of_the_box_dashboard ?? false;
+ },
},
methods: {
- ...mapActions('monitoringDashboard', ['filterEnvironments', 'toggleStarredValue']),
+ ...mapActions('monitoringDashboard', ['filterEnvironments']),
selectDashboard(dashboard) {
// Once the sidebar See metrics link is updated to the new URL,
// this sort of hardcoding will not be necessary.
@@ -187,16 +149,6 @@ export default {
toggleRearrangingPanels() {
this.$emit('setRearrangingPanels', !this.isRearrangingPanels);
},
- setFormValidity(isValid) {
- this.formIsValid = isValid;
- },
- hideAddMetricModal() {
- this.$refs.addMetricModal.hide();
- },
- getAddMetricTrackingOptions,
- submitCustomMetricsForm() {
- this.$refs.customMetricsForm.submit();
- },
getEnvironmentPath(environment) {
// Once the sidebar See metrics link is updated to the new URL,
// this sort of hardcoding will not be necessary.
@@ -209,16 +161,6 @@ export default {
return mergeUrlParams({ environment }, url);
},
},
- modalIds: {
- addMetric: 'addMetric',
- createDashboard: 'createDashboard',
- duplicateDashboard: 'duplicateDashboard',
- },
- i18n: {
- starDashboard: s__('Metrics|Star dashboard'),
- unstarDashboard: s__('Metrics|Unstar dashboard'),
- addMetric: s__('Metrics|Add metric'),
- },
timeRanges,
};
</script>
@@ -232,7 +174,6 @@ export default {
class="flex-grow-1"
toggle-class="dropdown-menu-toggle"
:default-branch="defaultBranch"
- :modal-id="$options.modalIds.duplicateDashboard"
@selectDashboard="selectDashboard"
/>
</div>
@@ -240,39 +181,30 @@ export default {
<span aria-hidden="true" class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block"></span>
<div class="mb-2 pr-2 d-flex d-sm-block">
- <gl-dropdown
+ <gl-new-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"
+ :text="environmentDropdownText"
>
<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"
- />
+ <gl-new-dropdown-header>{{ __('Environment') }}</gl-new-dropdown-header>
+ <gl-search-box-by-type class="m-2" @input="debouncedEnvironmentsSearch" />
+
+ <gl-loading-icon v-if="environmentsLoading" :inline="true" />
<div v-else class="flex-fill overflow-auto">
- <gl-dropdown-item
+ <gl-new-dropdown-item
v-for="environment in filteredEnvironments"
:key="environment.id"
- :active="environment.name === currentEnvironmentName"
- active-class="is-active"
+ :is-check-item="true"
+ :is-checked="environment.name === currentEnvironmentName"
:href="getEnvironmentPath(environment.id)"
- >{{ environment.name }}</gl-dropdown-item
>
+ {{ environment.name }}
+ </gl-new-dropdown-item>
</div>
<div
v-show="shouldShowEnvironmentsDropdownNoMatchedMsg"
@@ -282,7 +214,7 @@ export default {
{{ __('No matching results') }}
</div>
</div>
- </gl-dropdown>
+ </gl-new-dropdown>
</div>
<div class="mb-2 pr-2 d-flex d-sm-block">
@@ -305,163 +237,56 @@ export default {
<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
+ <gl-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.modalIds.addMetric"
- variant="outline-success"
- data-qa-selector="add_metric_button"
- class="flex-grow-1"
- >
- {{ $options.i18n.addMetric }}
- </gl-deprecated-button>
- <gl-modal
- ref="addMetricModal"
- :modal-id="$options.modalIds.addMetric"
- :title="$options.i18n.addMetric"
- >
- <form ref="customMetricsForm" :action="customMetricsPath" method="post">
- <custom-metrics-form-fields
- :validate-query-path="validateQueryPath"
- form-operation="post"
- @formValidation="setFormValidity"
- />
- </form>
- <div slot="modal-footer">
- <gl-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>
+ </gl-button>
</div>
<div
v-if="externalDashboardUrl && externalDashboardUrl.length"
class="mb-2 mr-2 d-flex d-sm-block"
>
- <gl-deprecated-button
+ <gl-button
class="flex-grow-1 js-external-dashboard-link"
- variant="primary"
+ variant="info"
+ category="primary"
:href="externalDashboardUrl"
target="_blank"
rel="noopener noreferrer"
>
{{ __('View full dashboard') }} <icon name="external-link" />
- </gl-deprecated-button>
+ </gl-button>
</div>
- <!-- This separator should be displayed only if at least one of the action menu or settings button are displayed -->
- <span
- v-if="shouldShowActionsMenu || shouldShowSettingsButton"
- aria-hidden="true"
- class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block"
- ></span>
+ <div class="gl-mb-3 gl-mr-3 d-flex d-sm-block">
+ <actions-menu
+ :adding-metrics-available="addingMetricsAvailable"
+ :custom-metrics-path="customMetricsPath"
+ :validate-query-path="validateQueryPath"
+ :default-branch="defaultBranch"
+ :is-ootb-dashboard="isOOTBDashboard"
+ />
+ </div>
- <div v-if="shouldShowActionsMenu" class="gl-mb-3 gl-mr-3 d-flex d-sm-block">
- <gl-new-dropdown
- v-gl-tooltip
- right
- class="gl-flex-grow-1"
- data-testid="actions-menu"
- :title="s__('Metrics|Create dashboard')"
- :icon="'plus-square'"
- >
- <gl-new-dropdown-item
- v-gl-modal="$options.modalIds.createDashboard"
- data-testid="action-create-dashboard"
- >{{ s__('Metrics|Create new dashboard') }}</gl-new-dropdown-item
- >
+ <template v-if="shouldShowSettingsButton">
+ <span aria-hidden="true" class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block"></span>
- <create-dashboard-modal
- data-testid="create-dashboard-modal"
- :add-dashboard-documentation-path="addDashboardDocumentationPath"
- :modal-id="$options.modalIds.createDashboard"
- :project-path="projectPath"
+ <div class="mb-2 mr-2 d-flex d-sm-block">
+ <gl-button
+ v-gl-tooltip
+ data-testid="metrics-settings-button"
+ icon="settings"
+ :href="operationsSettingsPath"
+ :title="s__('Metrics|Metrics Settings')"
/>
-
- <template v-if="isOutOfTheBoxDashboard">
- <gl-new-dropdown-divider />
- <gl-new-dropdown-item
- ref="duplicateDashboardItem"
- v-gl-modal="$options.modalIds.duplicateDashboard"
- data-testid="action-duplicate-dashboard"
- >
- {{ s__('Metrics|Duplicate current dashboard') }}
- </gl-new-dropdown-item>
- </template>
- </gl-new-dropdown>
- </div>
-
- <div v-if="shouldShowSettingsButton" class="mb-2 mr-2 d-flex d-sm-block">
- <gl-button
- v-gl-tooltip
- data-testid="metrics-settings-button"
- icon="settings"
- :href="operationsSettingsPath"
- :title="s__('Metrics|Metrics Settings')"
- />
- </div>
+ </div>
+ </template>
</div>
- <duplicate-dashboard-modal
- :default-branch="defaultBranch"
- :modal-id="$options.modalIds.duplicateDashboard"
- @dashboardDuplicated="selectDashboard"
- />
</div>
</template>
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel.vue b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
index 3e3c8408de3..278858d3a94 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_panel.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
@@ -1,20 +1,23 @@
<script>
import { mapState } from 'vuex';
-import { pickBy } from 'lodash';
-import invalidUrl from '~/lib/utils/invalid_url';
-import { relativePathToAbsolute, getBaseURL, visitUrl, isSafeURL } from '~/lib/utils/url_utility';
+import { mapValues, pickBy } from 'lodash';
import {
GlResizeObserverDirective,
GlIcon,
+ GlLink,
GlLoadingIcon,
GlNewDropdown as GlDropdown,
GlNewDropdownItem as GlDropdownItem,
GlNewDropdownDivider as GlDropdownDivider,
GlModal,
GlModalDirective,
+ GlSprintf,
GlTooltip,
GlTooltipDirective,
} from '@gitlab/ui';
+import invalidUrl from '~/lib/utils/invalid_url';
+import { convertToFixedRange } from '~/lib/utils/datetime_range';
+import { relativePathToAbsolute, getBaseURL, visitUrl, isSafeURL } from '~/lib/utils/url_utility';
import { __, n__ } from '~/locale';
import { panelTypes } from '../constants';
@@ -22,6 +25,7 @@ import MonitorEmptyChart from './charts/empty_chart.vue';
import MonitorTimeSeriesChart from './charts/time_series.vue';
import MonitorAnomalyChart from './charts/anomaly.vue';
import MonitorSingleStatChart from './charts/single_stat.vue';
+import MonitorGaugeChart from './charts/gauge.vue';
import MonitorHeatmapChart from './charts/heatmap.vue';
import MonitorColumnChart from './charts/column.vue';
import MonitorBarChart from './charts/bar.vue';
@@ -30,6 +34,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 { graphDataToCsv } from '../csv_export';
const events = {
timeRangeZoom: 'timerangezoom',
@@ -41,12 +46,14 @@ export default {
MonitorEmptyChart,
AlertWidget,
GlIcon,
+ GlLink,
GlLoadingIcon,
GlTooltip,
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlModal,
+ GlSprintf,
},
directives: {
GlResizeObserver: GlResizeObserverDirective,
@@ -128,6 +135,15 @@ export default {
return getters[`${this.namespace}/selectedDashboard`];
},
}),
+ fixedCurrentTimeRange() {
+ // convertToFixedRange throws an error if the time range
+ // is not properly set.
+ try {
+ return convertToFixedRange(this.timeRange);
+ } catch {
+ return {};
+ }
+ },
title() {
return this.graphData?.title || '';
},
@@ -148,13 +164,10 @@ export default {
return null;
},
csvText() {
- const chartData = this.graphData?.metrics[0].result[0].values || [];
- const yLabel = this.graphData.y_label;
- const header = `timestamp,${yLabel}\r\n`; // eslint-disable-line @gitlab/require-i18n-strings
- return chartData.reduce((csv, data) => {
- const row = data.join(',');
- return `${csv}${row}\r\n`;
- }, header);
+ if (this.graphData) {
+ return graphDataToCsv(this.graphData);
+ }
+ return null;
},
downloadCsv() {
const data = new Blob([this.csvText], { type: 'text/plain' });
@@ -172,6 +185,9 @@ export default {
if (this.isPanelType(panelTypes.SINGLE_STAT)) {
return MonitorSingleStatChart;
}
+ if (this.isPanelType(panelTypes.GAUGE_CHART)) {
+ return MonitorGaugeChart;
+ }
if (this.isPanelType(panelTypes.HEATMAP)) {
return MonitorHeatmapChart;
}
@@ -217,7 +233,8 @@ export default {
return (
this.isPanelType(panelTypes.AREA_CHART) ||
this.isPanelType(panelTypes.LINE_CHART) ||
- this.isPanelType(panelTypes.SINGLE_STAT)
+ this.isPanelType(panelTypes.SINGLE_STAT) ||
+ this.isPanelType(panelTypes.GAUGE_CHART)
);
},
editCustomMetricLink() {
@@ -328,6 +345,19 @@ export default {
this.$refs.copyChartLink.$el.firstChild.click();
}
},
+ getAlertRunbooks(queries) {
+ const hasRunbook = alert => Boolean(alert.runbookUrl);
+ const graphAlertsWithRunbooks = pickBy(this.getGraphAlerts(queries), hasRunbook);
+ const alertToRunbookTransform = alert => {
+ const alertQuery = queries.find(query => query.metricId === alert.metricId);
+ return {
+ key: alert.metricId,
+ href: alert.runbookUrl,
+ label: alertQuery.label,
+ };
+ };
+ return mapValues(graphAlertsWithRunbooks, alertToRunbookTransform);
+ },
},
panelTypes,
};
@@ -364,15 +394,21 @@ export default {
data-qa-selector="prometheus_graph_widgets"
>
<div data-testid="dropdown-wrapper" class="d-flex align-items-center">
+ <!--
+ This component should be replaced with a variant developed
+ as part of https://gitlab.com/gitlab-org/gitlab-ui/-/issues/936
+ The variant will create a dropdown with an icon, no text and no caret
+ -->
<gl-dropdown
v-gl-tooltip
- toggle-class="shadow-none border-0"
+ toggle-class="gl-px-3!"
+ no-caret
data-qa-selector="prometheus_widgets_dropdown"
right
:title="__('More actions')"
>
- <template slot="button-content">
- <gl-icon name="ellipsis_v" class="dropdown-icon text-secondary" />
+ <template #button-content>
+ <gl-icon class="gl-mr-0!" name="ellipsis_v" />
</template>
<gl-dropdown-item
v-if="expandBtnAvailable"
@@ -423,6 +459,25 @@ export default {
>
{{ __('Alerts') }}
</gl-dropdown-item>
+ <gl-dropdown-item
+ v-for="runbook in getAlertRunbooks(graphData.metrics)"
+ :key="runbook.key"
+ :href="safeUrl(runbook.href)"
+ data-testid="runbookLink"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <span class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
+ <span>
+ <gl-sprintf :message="s__('Metrics|View runbook - %{label}')">
+ <template #label>
+ {{ runbook.label }}
+ </template>
+ </gl-sprintf>
+ </span>
+ <gl-icon name="external-link" />
+ </span>
+ </gl-dropdown-item>
<template v-if="graphData.links && graphData.links.length">
<gl-dropdown-divider />
@@ -465,6 +520,7 @@ export default {
:thresholds="getGraphAlertValues(graphData.metrics)"
:group-id="groupId"
:timezone="dashboardTimezone"
+ :time-range="fixedCurrentTimeRange"
v-bind="$attrs"
v-on="$listeners"
@datazoom="onDatazoom"
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue b/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue
new file mode 100644
index 00000000000..88d5a35146f
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue
@@ -0,0 +1,199 @@
+<script>
+import { mapActions, mapState } from 'vuex';
+import {
+ GlCard,
+ GlForm,
+ GlFormGroup,
+ GlFormTextarea,
+ GlButton,
+ GlSprintf,
+ GlAlert,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
+import { timeRanges } from '~/vue_shared/constants';
+import DashboardPanel from './dashboard_panel.vue';
+
+const initialYml = `title: Go heap size
+type: area-chart
+y_axis:
+ format: 'bytes'
+metrics:
+ - metric_id: 'go_memstats_alloc_bytes_1'
+ query_range: 'go_memstats_alloc_bytes'
+`;
+
+export default {
+ components: {
+ GlCard,
+ GlForm,
+ GlFormGroup,
+ GlFormTextarea,
+ GlButton,
+ GlSprintf,
+ GlAlert,
+ DashboardPanel,
+ DateTimePicker,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ data() {
+ return {
+ yml: initialYml,
+ };
+ },
+ computed: {
+ ...mapState('monitoringDashboard', [
+ 'panelPreviewIsLoading',
+ 'panelPreviewError',
+ 'panelPreviewGraphData',
+ 'panelPreviewTimeRange',
+ 'panelPreviewIsShown',
+ 'projectPath',
+ 'addDashboardDocumentationPath',
+ ]),
+ },
+ methods: {
+ ...mapActions('monitoringDashboard', [
+ 'fetchPanelPreview',
+ 'fetchPanelPreviewMetrics',
+ 'setPanelPreviewTimeRange',
+ ]),
+ onSubmit() {
+ this.fetchPanelPreview(this.yml);
+ },
+ onDateTimePickerInput(timeRange) {
+ this.setPanelPreviewTimeRange(timeRange);
+ // refetch data only if preview has been clicked
+ // and there are no errors
+ if (this.panelPreviewIsShown && !this.panelPreviewError) {
+ this.fetchPanelPreviewMetrics();
+ }
+ },
+ onRefresh() {
+ // refetch data only if preview has been clicked
+ // and there are no errors
+ if (this.panelPreviewIsShown && !this.panelPreviewError) {
+ this.fetchPanelPreviewMetrics();
+ }
+ },
+ },
+ timeRanges,
+};
+</script>
+<template>
+ <div class="prometheus-panel-builder">
+ <div class="gl-xs-flex-direction-column gl-display-flex gl-mx-n3">
+ <gl-card class="gl-flex-grow-1 gl-flex-basis-0 gl-mx-3">
+ <template #header>
+ <h2 class="gl-font-size-h2 gl-my-3">{{ s__('Metrics|1. Define and preview panel') }}</h2>
+ </template>
+ <template #default>
+ <p>{{ s__('Metrics|Define panel YAML below to preview panel.') }}</p>
+ <gl-form @submit.prevent="onSubmit">
+ <gl-form-group :label="s__('Metrics|Panel YAML')" label-for="panel-yml-input">
+ <gl-form-textarea
+ id="panel-yml-input"
+ v-model="yml"
+ class="gl-h-200! gl-font-monospace! gl-font-size-monospace!"
+ />
+ </gl-form-group>
+ <div class="gl-text-right">
+ <gl-button
+ ref="clipboardCopyBtn"
+ variant="success"
+ category="secondary"
+ :data-clipboard-text="yml"
+ class="gl-xs-w-full gl-xs-mb-3"
+ @click="$toast.show(s__('Metrics|Panel YAML copied'))"
+ >
+ {{ s__('Metrics|Copy YAML') }}
+ </gl-button>
+ <gl-button
+ type="submit"
+ variant="success"
+ :disabled="panelPreviewIsLoading"
+ class="js-no-auto-disable gl-xs-w-full"
+ >
+ {{ s__('Metrics|Preview panel') }}
+ </gl-button>
+ </div>
+ </gl-form>
+ </template>
+ </gl-card>
+
+ <gl-card
+ class="gl-flex-grow-1 gl-flex-basis-0 gl-mx-3"
+ body-class="gl-display-flex gl-flex-direction-column"
+ >
+ <template #header>
+ <h2 class="gl-font-size-h2 gl-my-3">
+ {{ s__('Metrics|2. Paste panel YAML into dashboard') }}
+ </h2>
+ </template>
+ <template #default>
+ <div
+ class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-justify-content-center"
+ >
+ <p>
+ {{ s__('Metrics|Copy and paste the panel YAML into your dashboard YAML file.') }}
+ <br />
+ <gl-sprintf
+ :message="
+ s__(
+ 'Metrics|Dashboard files can be found in %{codeStart}.gitlab/dashboards%{codeEnd} at the root of this project.',
+ )
+ "
+ >
+ <template #code="{content}">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ </div>
+
+ <div class="gl-text-right">
+ <gl-button
+ ref="viewDocumentationBtn"
+ category="secondary"
+ class="gl-xs-w-full gl-xs-mb-3"
+ variant="info"
+ target="_blank"
+ :href="addDashboardDocumentationPath"
+ >
+ {{ s__('Metrics|View documentation') }}
+ </gl-button>
+ <gl-button
+ ref="openRepositoryBtn"
+ variant="success"
+ :href="projectPath"
+ class="gl-xs-w-full"
+ >
+ {{ s__('Metrics|Open repository') }}
+ </gl-button>
+ </div>
+ </template>
+ </gl-card>
+ </div>
+
+ <gl-alert v-if="panelPreviewError" variant="warning" :dismissible="false">
+ {{ panelPreviewError }}
+ </gl-alert>
+ <date-time-picker
+ ref="dateTimePicker"
+ class="gl-flex-grow-1 preview-date-time-picker gl-xs-mb-3"
+ :value="panelPreviewTimeRange"
+ :options="$options.timeRanges"
+ @input="onDateTimePickerInput"
+ />
+ <gl-button
+ v-gl-tooltip
+ data-testid="previewRefreshButton"
+ icon="retry"
+ :title="s__('Metrics|Refresh Prometheus data')"
+ @click="onRefresh"
+ />
+ <dashboard-panel :graph-data="panelPreviewGraphData" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
index 574f48a72fe..aed27b5ea51 100644
--- a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
+++ b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
@@ -1,11 +1,11 @@
<script>
-import { mapState, mapActions, mapGetters } from 'vuex';
+import { mapState, mapGetters } from 'vuex';
import {
GlIcon,
- GlDropdown,
- GlDropdownItem,
- GlDropdownHeader,
- GlDropdownDivider,
+ GlNewDropdown,
+ GlNewDropdownItem,
+ GlNewDropdownHeader,
+ GlNewDropdownDivider,
GlSearchBoxByType,
GlModalDirective,
} from '@gitlab/ui';
@@ -17,10 +17,10 @@ const events = {
export default {
components: {
GlIcon,
- GlDropdown,
- GlDropdownItem,
- GlDropdownHeader,
- GlDropdownDivider,
+ GlNewDropdown,
+ GlNewDropdownItem,
+ GlNewDropdownHeader,
+ GlNewDropdownDivider,
GlSearchBoxByType,
},
directives: {
@@ -31,10 +31,6 @@ export default {
type: String,
required: true,
},
- modalId: {
- type: String,
- required: true,
- },
},
data() {
return {
@@ -44,9 +40,6 @@ export default {
computed: {
...mapState('monitoringDashboard', ['allDashboards']),
...mapGetters('monitoringDashboard', ['selectedDashboard']),
- isOutOfTheBoxDashboard() {
- return this.selectedDashboard?.out_of_the_box_dashboard;
- },
selectedDashboardText() {
return this.selectedDashboard?.display_name;
},
@@ -70,7 +63,6 @@ export default {
},
},
methods: {
- ...mapActions('monitoringDashboard', ['duplicateSystemDashboard']),
dashboardDisplayName(dashboard) {
return dashboard.display_name || dashboard.path || '';
},
@@ -81,16 +73,13 @@ export default {
};
</script>
<template>
- <gl-dropdown
+ <gl-new-dropdown
toggle-class="dropdown-menu-toggle"
menu-class="monitor-dashboard-dropdown-menu"
:text="selectedDashboardText"
>
<div class="d-flex flex-column overflow-hidden">
- <gl-dropdown-header class="monitor-dashboard-dropdown-header text-center">{{
- __('Dashboard')
- }}</gl-dropdown-header>
- <gl-dropdown-divider />
+ <gl-new-dropdown-header>{{ __('Dashboard') }}</gl-new-dropdown-header>
<gl-search-box-by-type
ref="monitorDashboardsDropdownSearch"
v-model="searchTerm"
@@ -98,33 +87,36 @@ export default {
/>
<div class="flex-fill overflow-auto">
- <gl-dropdown-item
+ <gl-new-dropdown-item
v-for="dashboard in starredDashboards"
:key="dashboard.path"
- :active="dashboard.path === selectedDashboardPath"
- active-class="is-active"
+ :is-check-item="true"
+ :is-checked="dashboard.path === selectedDashboardPath"
@click="selectDashboard(dashboard)"
>
- <div class="d-flex">
- {{ dashboardDisplayName(dashboard) }}
- <gl-icon class="text-muted ml-auto" name="star" />
+ <div class="gl-display-flex">
+ <div class="gl-flex-grow-1 gl-min-w-0">
+ <div class="gl-word-break-all">
+ {{ dashboardDisplayName(dashboard) }}
+ </div>
+ </div>
+ <gl-icon class="text-muted gl-flex-shrink-0" name="star" />
</div>
- </gl-dropdown-item>
-
- <gl-dropdown-divider
+ </gl-new-dropdown-item>
+ <gl-new-dropdown-divider
v-if="starredDashboards.length && nonStarredDashboards.length"
ref="starredListDivider"
/>
- <gl-dropdown-item
+ <gl-new-dropdown-item
v-for="dashboard in nonStarredDashboards"
:key="dashboard.path"
- :active="dashboard.path === selectedDashboardPath"
- active-class="is-active"
+ :is-check-item="true"
+ :is-checked="dashboard.path === selectedDashboardPath"
@click="selectDashboard(dashboard)"
>
{{ dashboardDisplayName(dashboard) }}
- </gl-dropdown-item>
+ </gl-new-dropdown-item>
</div>
<div
@@ -134,18 +126,6 @@ export default {
>
{{ __('No matching results') }}
</div>
-
- <!--
- This Duplicate Dashboard item will be removed from the dashboards dropdown
- in https://gitlab.com/gitlab-org/gitlab/-/issues/223223
- -->
- <template v-if="isOutOfTheBoxDashboard">
- <gl-dropdown-divider />
-
- <gl-dropdown-item v-gl-modal="modalId" data-testid="duplicateDashboardItem">
- {{ s__('Metrics|Duplicate dashboard') }}
- </gl-dropdown-item>
- </template>
</div>
- </gl-dropdown>
+ </gl-new-dropdown>
</template>
diff --git a/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue b/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue
index 001cd0d47f1..db5b853d451 100644
--- a/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue
+++ b/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue
@@ -1,7 +1,7 @@
<script>
-import { __, s__, sprintf } from '~/locale';
import { GlFormGroup, GlFormInput, GlFormRadioGroup, GlFormTextarea } from '@gitlab/ui';
import { escape as esc } from 'lodash';
+import { __, s__, sprintf } from '~/locale';
const defaultFileName = dashboard => dashboard.path.split('/').reverse()[0];
diff --git a/app/assets/javascripts/monitoring/components/group_empty_state.vue b/app/assets/javascripts/monitoring/components/group_empty_state.vue
index dee4e5998ee..9cf492dd537 100644
--- a/app/assets/javascripts/monitoring/components/group_empty_state.vue
+++ b/app/assets/javascripts/monitoring/components/group_empty_state.vue
@@ -1,6 +1,6 @@
<script>
-import { __, sprintf } from '~/locale';
import { GlEmptyState } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
import { metricStates } from '../constants';
export default {
diff --git a/app/assets/javascripts/monitoring/components/links_section.vue b/app/assets/javascripts/monitoring/components/links_section.vue
index 98b07d17694..ca1e9c4d0d4 100644
--- a/app/assets/javascripts/monitoring/components/links_section.vue
+++ b/app/assets/javascripts/monitoring/components/links_section.vue
@@ -23,7 +23,7 @@ export default {
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" />{{
+ ><gl-icon name="link" class="gl-text-gray-500 gl-vertical-align-text-bottom gl-mr-2" />{{
link.title
}}
</gl-link>
diff --git a/app/assets/javascripts/monitoring/components/refresh_button.vue b/app/assets/javascripts/monitoring/components/refresh_button.vue
index 5481806c3e0..0e9605450ed 100644
--- a/app/assets/javascripts/monitoring/components/refresh_button.vue
+++ b/app/assets/javascripts/monitoring/components/refresh_button.vue
@@ -1,7 +1,6 @@
<script>
-import { n__, __ } from '~/locale';
+import Visibility from 'visibilityjs';
import { mapActions } from 'vuex';
-
import {
GlButtonGroup,
GlButton,
@@ -10,6 +9,9 @@ import {
GlNewDropdownDivider,
GlTooltipDirective,
} from '@gitlab/ui';
+import { n__, __ } from '~/locale';
+
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const makeInterval = (length = 0, unit = 's') => {
const shortLabel = `${length}${unit}`;
@@ -53,6 +55,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [glFeatureFlagsMixin()],
data() {
return {
refreshInterval: null,
@@ -60,6 +63,12 @@ export default {
};
},
computed: {
+ disableMetricDashboardRefreshRate() {
+ // Can refresh rates impact performance?
+ // Add "negative" feature flag called `disable_metric_dashboard_refresh_rate`
+ // See more at: https://gitlab.com/gitlab-org/gitlab/-/issues/229831
+ return this.glFeatures.disableMetricDashboardRefreshRate;
+ },
dropdownText() {
return this.refreshInterval?.shortLabel ?? __('Off');
},
@@ -90,7 +99,8 @@ export default {
};
this.stopAutoRefresh();
- if (document.hidden) {
+
+ if (Visibility.hidden()) {
// Inactive tab? Skip fetch and schedule again
schedule();
} else {
@@ -142,7 +152,12 @@ export default {
icon="retry"
@click="refresh"
/>
- <gl-new-dropdown v-gl-tooltip :title="s__('Metrics|Set refresh rate')" :text="dropdownText">
+ <gl-new-dropdown
+ v-if="!disableMetricDashboardRefreshRate"
+ v-gl-tooltip
+ :title="s__('Metrics|Set refresh rate')"
+ :text="dropdownText"
+ >
<gl-new-dropdown-item
:is-check-item="true"
:is-checked="refreshInterval === null"
diff --git a/app/assets/javascripts/monitoring/components/variables/dropdown_field.vue b/app/assets/javascripts/monitoring/components/variables/dropdown_field.vue
index 4e48292c48d..5563a27301d 100644
--- a/app/assets/javascripts/monitoring/components/variables/dropdown_field.vue
+++ b/app/assets/javascripts/monitoring/components/variables/dropdown_field.vue
@@ -1,11 +1,11 @@
<script>
-import { GlFormGroup, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlFormGroup, GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
export default {
components: {
GlFormGroup,
- GlDropdown,
- GlDropdownItem,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
},
props: {
name: {
@@ -41,13 +41,16 @@ export default {
</script>
<template>
<gl-form-group :label="label">
- <gl-dropdown toggle-class="dropdown-menu-toggle" :text="text || s__('Metrics|Select a value')">
- <gl-dropdown-item
+ <gl-deprecated-dropdown
+ toggle-class="dropdown-menu-toggle"
+ :text="text || s__('Metrics|Select a value')"
+ >
+ <gl-deprecated-dropdown-item
v-for="val in options.values"
:key="val.value"
@click="onUpdate(val.value)"
- >{{ val.text }}</gl-dropdown-item
+ >{{ val.text }}</gl-deprecated-dropdown-item
>
- </gl-dropdown>
+ </gl-deprecated-dropdown>
</gl-form-group>
</template>
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
index afeb3318eb9..81ad3137b8b 100644
--- a/app/assets/javascripts/monitoring/constants.js
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -87,6 +87,10 @@ export const panelTypes = {
*/
SINGLE_STAT: 'single-stat',
/**
+ * Gauge
+ */
+ GAUGE_CHART: 'gauge',
+ /**
* Heatmap
*/
HEATMAP: 'heatmap',
@@ -213,7 +217,7 @@ export const annotationsSymbolIcon = 'path://m5 229 5 8h-10z';
* This technical debt is being tracked here
* https://gitlab.com/gitlab-org/gitlab/-/issues/214671
*/
-export const DEFAULT_DASHBOARD_PATH = 'config/prometheus/common_metrics.yml';
+export const OVERVIEW_DASHBOARD_PATH = 'config/prometheus/common_metrics.yml';
/**
* GitLab provide metrics dashboards that are available to a user once
@@ -272,3 +276,8 @@ export const keyboardShortcutKeys = {
DOWNLOAD_CSV: 'd',
CHART_COPY: 'c',
};
+
+export const thresholdModeTypes = {
+ ABSOLUTE: 'absolute',
+ PERCENTAGE: 'percentage',
+};
diff --git a/app/assets/javascripts/monitoring/csv_export.js b/app/assets/javascripts/monitoring/csv_export.js
new file mode 100644
index 00000000000..734e8dc07a7
--- /dev/null
+++ b/app/assets/javascripts/monitoring/csv_export.js
@@ -0,0 +1,147 @@
+import { getSeriesLabel } from '~/helpers/monitor_helper';
+
+/**
+ * Returns a label for a header of the csv.
+ *
+ * Includes double quotes ("") in case the header includes commas or other separator.
+ *
+ * @param {String} axisLabel
+ * @param {String} metricLabel
+ * @param {Object} metricAttributes
+ */
+const csvHeader = (axisLabel, metricLabel, metricAttributes = {}) =>
+ `${axisLabel} > ${getSeriesLabel(metricLabel, metricAttributes)}`;
+
+/**
+ * Returns an array with the header labels given a list of metrics
+ *
+ * ```
+ * metrics = [
+ * {
+ * label: "..." // user-defined label
+ * result: [
+ * {
+ * metric: { ... } // metricAttributes
+ * },
+ * ...
+ * ]
+ * },
+ * ...
+ * ]
+ * ```
+ *
+ * When metrics have a `label` or `metricAttributes`, they are
+ * used to generate the column name.
+ *
+ * @param {String} axisLabel - Main label
+ * @param {Array} metrics - Metrics with results
+ */
+const csvMetricHeaders = (axisLabel, metrics) =>
+ metrics.flatMap(({ label, result }) =>
+ // The `metric` in a `result` is a map of `metricAttributes`
+ // contains key-values to identify the series, rename it
+ // here for clarity.
+ result.map(({ metric: metricAttributes }) => {
+ return csvHeader(axisLabel, label, metricAttributes);
+ }),
+ );
+
+/**
+ * Returns a (flat) array with all the values arrays in each
+ * metric and series
+ *
+ * ```
+ * metrics = [
+ * {
+ * result: [
+ * {
+ * values: [ ... ] // `values`
+ * },
+ * ...
+ * ]
+ * },
+ * ...
+ * ]
+ * ```
+ *
+ * @param {Array} metrics - Metrics with results
+ */
+const csvMetricValues = metrics =>
+ metrics.flatMap(({ result }) => result.map(res => res.values || []));
+
+/**
+ * Returns headers and rows for csv, sorted by their timestamp.
+ *
+ * {
+ * headers: ["timestamp", "<col_1_name>", "col_2_name"],
+ * rows: [
+ * [ <timestamp>, <col_1_value>, <col_2_value> ],
+ * [ <timestamp>, <col_1_value>, <col_2_value> ]
+ * ...
+ * ]
+ * }
+ *
+ * @param {Array} metricHeaders
+ * @param {Array} metricValues
+ */
+const csvData = (metricHeaders, metricValues) => {
+ const rowsByTimestamp = {};
+
+ metricValues.forEach((values, colIndex) => {
+ values.forEach(([timestamp, value]) => {
+ if (!rowsByTimestamp[timestamp]) {
+ rowsByTimestamp[timestamp] = [];
+ }
+ // `value` should be in the right column
+ rowsByTimestamp[timestamp][colIndex] = value;
+ });
+ });
+
+ const rows = Object.keys(rowsByTimestamp)
+ .sort()
+ .map(timestamp => {
+ // force each row to have the same number of entries
+ rowsByTimestamp[timestamp].length = metricHeaders.length;
+ // add timestamp as the first entry
+ return [timestamp, ...rowsByTimestamp[timestamp]];
+ });
+
+ // Escape double quotes and enclose headers:
+ // "If double-quotes are used to enclose fields, then a double-quote
+ // appearing inside a field must be escaped by preceding it with
+ // another double quote."
+ // https://tools.ietf.org/html/rfc4180#page-2
+ const headers = metricHeaders.map(header => `"${header.replace(/"/g, '""')}"`);
+
+ return {
+ headers: ['timestamp', ...headers],
+ rows,
+ };
+};
+
+/**
+ * Returns dashboard panel's data in a string in CSV format
+ *
+ * @param {Object} graphData - Panel contents
+ * @returns {String}
+ */
+// eslint-disable-next-line import/prefer-default-export
+export const graphDataToCsv = graphData => {
+ const delimiter = ',';
+ const br = '\r\n';
+ const { metrics = [], y_label: axisLabel } = graphData;
+
+ const metricsWithResults = metrics.filter(metric => metric.result);
+ const metricHeaders = csvMetricHeaders(axisLabel, metricsWithResults);
+ const metricValues = csvMetricValues(metricsWithResults);
+ const { headers, rows } = csvData(metricHeaders, metricValues);
+
+ if (rows.length === 0) {
+ return '';
+ }
+
+ const headerLine = headers.join(delimiter) + br;
+ const lines = rows.map(row => row.join(delimiter));
+
+ return headerLine + lines.join(br) + br;
+};
diff --git a/app/assets/javascripts/monitoring/pages/panel_new_page.vue b/app/assets/javascripts/monitoring/pages/panel_new_page.vue
new file mode 100644
index 00000000000..8ff6adb47ca
--- /dev/null
+++ b/app/assets/javascripts/monitoring/pages/panel_new_page.vue
@@ -0,0 +1,45 @@
+<script>
+import { mapState } from 'vuex';
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { DASHBOARD_PAGE } from '../router/constants';
+import DashboardPanelBuilder from '../components/dashboard_panel_builder.vue';
+
+export default {
+ components: {
+ GlButton,
+ DashboardPanelBuilder,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ computed: {
+ ...mapState('monitoringDashboard', ['panelPreviewYml']),
+ dashboardPageLocation() {
+ return {
+ ...this.$route,
+ name: DASHBOARD_PAGE,
+ };
+ },
+ },
+ i18n: {
+ backToDashboard: s__('Metrics|Back to dashboard'),
+ },
+};
+</script>
+<template>
+ <div class="gl-mt-5">
+ <div class="gl-display-flex gl-align-items-baseline gl-mb-5">
+ <gl-button
+ v-gl-tooltip
+ icon="go-back"
+ :to="dashboardPageLocation"
+ :aria-label="$options.i18n.backToDashboard"
+ :title="$options.i18n.backToDashboard"
+ class="gl-mr-5"
+ />
+ <h1 class="gl-font-size-h1 gl-my-0">{{ s__('Metrics|Add panel') }}</h1>
+ </div>
+ <dashboard-panel-builder />
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/queries/getAnnotations.query.graphql b/app/assets/javascripts/monitoring/queries/getAnnotations.query.graphql
index 27b49860b8a..32b982ff195 100644
--- a/app/assets/javascripts/monitoring/queries/getAnnotations.query.graphql
+++ b/app/assets/javascripts/monitoring/queries/getAnnotations.query.graphql
@@ -5,6 +5,7 @@ query getAnnotations(
$startingFrom: Time!
) {
project(fullPath: $projectPath) {
+ id
environments(name: $environmentName) {
nodes {
id
diff --git a/app/assets/javascripts/monitoring/queries/getEnvironments.query.graphql b/app/assets/javascripts/monitoring/queries/getEnvironments.query.graphql
index 17cd1b2c342..48d0a780fc7 100644
--- a/app/assets/javascripts/monitoring/queries/getEnvironments.query.graphql
+++ b/app/assets/javascripts/monitoring/queries/getEnvironments.query.graphql
@@ -1,5 +1,6 @@
query getEnvironments($projectPath: ID!, $search: String, $states: [String!]) {
project(fullPath: $projectPath) {
+ id
data: environments(search: $search, states: $states) {
environments: nodes {
name
diff --git a/app/assets/javascripts/monitoring/requests/index.js b/app/assets/javascripts/monitoring/requests/index.js
new file mode 100644
index 00000000000..28064361768
--- /dev/null
+++ b/app/assets/javascripts/monitoring/requests/index.js
@@ -0,0 +1,46 @@
+import axios from '~/lib/utils/axios_utils';
+import statusCodes from '~/lib/utils/http_status';
+import { backOff } from '~/lib/utils/common_utils';
+import { PROMETHEUS_TIMEOUT } from '../constants';
+
+const cancellableBackOffRequest = makeRequestCallback =>
+ backOff((next, stop) => {
+ makeRequestCallback()
+ .then(resp => {
+ if (resp.status === statusCodes.NO_CONTENT) {
+ next();
+ } else {
+ stop(resp);
+ }
+ })
+ // If the request is cancelled by axios
+ // then consider it as noop so that its not
+ // caught by subsequent catches
+ .catch(thrown => (axios.isCancel(thrown) ? undefined : stop(thrown)));
+ }, PROMETHEUS_TIMEOUT);
+
+export const getDashboard = (dashboardEndpoint, params) =>
+ cancellableBackOffRequest(() => axios.get(dashboardEndpoint, { params })).then(
+ axiosResponse => axiosResponse.data,
+ );
+
+export const getPrometheusQueryData = (prometheusEndpoint, params, opts) =>
+ cancellableBackOffRequest(() => axios.get(prometheusEndpoint, { params, ...opts }))
+ .then(axiosResponse => axiosResponse.data)
+ .then(prometheusResponse => prometheusResponse.data)
+ .catch(error => {
+ // Prometheus returns errors in specific cases
+ // https://prometheus.io/docs/prometheus/latest/querying/api/#format-overview
+ const { response = {} } = error;
+ if (
+ response.status === statusCodes.BAD_REQUEST ||
+ response.status === statusCodes.UNPROCESSABLE_ENTITY ||
+ response.status === statusCodes.SERVICE_UNAVAILABLE
+ ) {
+ const { data } = response;
+ if (data?.status === 'error' && data?.error) {
+ throw new Error(data.error);
+ }
+ }
+ throw error;
+ });
diff --git a/app/assets/javascripts/monitoring/router/constants.js b/app/assets/javascripts/monitoring/router/constants.js
index fedfebe33e9..7834c14a65d 100644
--- a/app/assets/javascripts/monitoring/router/constants.js
+++ b/app/assets/javascripts/monitoring/router/constants.js
@@ -1,4 +1,7 @@
-export const BASE_DASHBOARD_PAGE = 'dashboard';
-export const CUSTOM_DASHBOARD_PAGE = 'custom_dashboard';
+export const DASHBOARD_PAGE = 'dashboard';
+export const PANEL_NEW_PAGE = 'panel_new';
-export default {};
+export default {
+ DASHBOARD_PAGE,
+ PANEL_NEW_PAGE,
+};
diff --git a/app/assets/javascripts/monitoring/router/routes.js b/app/assets/javascripts/monitoring/router/routes.js
index 4b82791178a..cc43fd8622a 100644
--- a/app/assets/javascripts/monitoring/router/routes.js
+++ b/app/assets/javascripts/monitoring/router/routes.js
@@ -1,6 +1,7 @@
import DashboardPage from '../pages/dashboard_page.vue';
+import PanelNewPage from '../pages/panel_new_page.vue';
-import { BASE_DASHBOARD_PAGE, CUSTOM_DASHBOARD_PAGE } from './constants';
+import { DASHBOARD_PAGE, PANEL_NEW_PAGE } from './constants';
/**
* Because the cluster health page uses the dashboard
@@ -11,13 +12,13 @@ import { BASE_DASHBOARD_PAGE, CUSTOM_DASHBOARD_PAGE } from './constants';
*/
export default [
{
- name: BASE_DASHBOARD_PAGE,
- path: '/',
- component: DashboardPage,
+ name: PANEL_NEW_PAGE,
+ path: '/:dashboard(.+)?/panel/new',
+ component: PanelNewPage,
},
{
- name: CUSTOM_DASHBOARD_PAGE,
- path: '/:dashboard(.*)',
+ name: DASHBOARD_PAGE,
+ path: '/:dashboard(.+)?',
component: DashboardPage,
},
];
diff --git a/app/assets/javascripts/monitoring/services/alerts_service.js b/app/assets/javascripts/monitoring/services/alerts_service.js
index 4b7337972fe..a67675f1a3d 100644
--- a/app/assets/javascripts/monitoring/services/alerts_service.js
+++ b/app/assets/javascripts/monitoring/services/alerts_service.js
@@ -1,28 +1,39 @@
import axios from '~/lib/utils/axios_utils';
+const mapAlert = ({ runbook_url, ...alert }) => {
+ return { runbookUrl: runbook_url, ...alert };
+};
+
export default class AlertsService {
constructor({ alertsEndpoint }) {
this.alertsEndpoint = alertsEndpoint;
}
getAlerts() {
- return axios.get(this.alertsEndpoint).then(resp => resp.data);
+ return axios.get(this.alertsEndpoint).then(resp => mapAlert(resp.data));
}
- createAlert({ prometheus_metric_id, operator, threshold }) {
+ createAlert({ prometheus_metric_id, operator, threshold, runbookUrl }) {
return axios
- .post(this.alertsEndpoint, { prometheus_metric_id, operator, threshold })
- .then(resp => resp.data);
+ .post(this.alertsEndpoint, {
+ prometheus_metric_id,
+ operator,
+ threshold,
+ runbook_url: runbookUrl,
+ })
+ .then(resp => mapAlert(resp.data));
}
// eslint-disable-next-line class-methods-use-this
readAlert(alertPath) {
- return axios.get(alertPath).then(resp => resp.data);
+ return axios.get(alertPath).then(resp => mapAlert(resp.data));
}
// eslint-disable-next-line class-methods-use-this
- updateAlert(alertPath, { operator, threshold }) {
- return axios.put(alertPath, { operator, threshold }).then(resp => resp.data);
+ updateAlert(alertPath, { operator, threshold, runbookUrl }) {
+ return axios
+ .put(alertPath, { operator, threshold, runbook_url: runbookUrl })
+ .then(resp => mapAlert(resp.data));
}
// eslint-disable-next-line class-methods-use-this
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index a441882a47d..16a685305dc 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -1,7 +1,7 @@
import * as Sentry from '@sentry/browser';
import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import {
gqClient,
@@ -13,16 +13,14 @@ import trackDashboardLoad from '../monitoring_tracking_helper';
import getEnvironments from '../queries/getEnvironments.query.graphql';
import getAnnotations from '../queries/getAnnotations.query.graphql';
import getDashboardValidationWarnings from '../queries/getDashboardValidationWarnings.query.graphql';
-import statusCodes from '../../lib/utils/http_status';
-import { backOff, convertObjectPropsToCamelCase } from '../../lib/utils/common_utils';
+import { convertObjectPropsToCamelCase } from '../../lib/utils/common_utils';
import { s__, sprintf } from '../../locale';
+import { getDashboard, getPrometheusQueryData } from '../requests';
-import {
- PROMETHEUS_TIMEOUT,
- ENVIRONMENT_AVAILABLE_STATE,
- DEFAULT_DASHBOARD_PATH,
- VARIABLE_TYPES,
-} from '../constants';
+import { ENVIRONMENT_AVAILABLE_STATE, OVERVIEW_DASHBOARD_PATH, VARIABLE_TYPES } from '../constants';
+
+const axiosCancelToken = axios.CancelToken;
+let cancelTokenSource;
function prometheusMetricQueryParams(timeRange) {
const { start, end } = convertToFixedRange(timeRange);
@@ -38,29 +36,18 @@ function prometheusMetricQueryParams(timeRange) {
};
}
-function backOffRequest(makeRequestCallback) {
- return backOff((next, stop) => {
- makeRequestCallback()
- .then(resp => {
- if (resp.status === statusCodes.NO_CONTENT) {
- next();
- } else {
- stop(resp);
- }
- })
- .catch(stop);
- }, PROMETHEUS_TIMEOUT);
-}
-
-function getPrometheusQueryData(prometheusEndpoint, params) {
- return backOffRequest(() => axios.get(prometheusEndpoint, { params }))
- .then(res => res.data)
- .then(response => {
- if (response.status === 'error') {
- throw new Error(response.error);
- }
- return response.data;
- });
+/**
+ * Extract error messages from API or HTTP request errors.
+ *
+ * - API errors are in `error.response.data.message`
+ * - HTTP (axios) errors are in `error.messsage`
+ *
+ * @param {Object} error
+ * @returns {String} User friendly error message
+ */
+function extractErrorMessage(error) {
+ const message = error?.response?.data?.message;
+ return message ?? error.message;
}
// Setup
@@ -126,8 +113,7 @@ export const fetchDashboard = ({ state, commit, dispatch, getters }) => {
params.dashboard = getters.fullDashboardPath;
}
- return backOffRequest(() => axios.get(state.dashboardEndpoint, { params }))
- .then(resp => resp.data)
+ return getDashboard(state.dashboardEndpoint, params)
.then(response => {
dispatch('receiveMetricsDashboardSuccess', { response });
/**
@@ -329,7 +315,7 @@ export const receiveEnvironmentsDataFailure = ({ commit }) => {
export const fetchAnnotations = ({ state, dispatch, getters }) => {
const { start } = convertToFixedRange(state.timeRange);
- const dashboardPath = getters.fullDashboardPath || DEFAULT_DASHBOARD_PATH;
+ const dashboardPath = getters.fullDashboardPath || OVERVIEW_DASHBOARD_PATH;
return gqClient
.mutate({
mutation: getAnnotations,
@@ -362,12 +348,12 @@ export const receiveAnnotationsFailure = ({ commit }) => commit(types.RECEIVE_AN
export const fetchDashboardValidationWarnings = ({ state, dispatch, getters }) => {
/**
- * Normally, the default dashboard won't throw any validation warnings.
+ * Normally, the overview dashboard won't throw any validation warnings.
*
- * However, if a bug sneaks into the default dashboard making it invalid,
+ * However, if a bug sneaks into the overview dashboard making it invalid,
* this might come handy for our clients
*/
- const dashboardPath = getters.fullDashboardPath || DEFAULT_DASHBOARD_PATH;
+ const dashboardPath = getters.fullDashboardPath || OVERVIEW_DASHBOARD_PATH;
return gqClient
.mutate({
mutation: getDashboardValidationWarnings,
@@ -484,12 +470,10 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery
if (variable.type === VARIABLE_TYPES.metric_label_values) {
const { prometheusEndpointPath, label } = variable.options;
- const optionsRequest = backOffRequest(() =>
- axios.get(prometheusEndpointPath, {
- params: { start_time, end_time },
- }),
- )
- .then(({ data }) => data.data)
+ const optionsRequest = getPrometheusQueryData(prometheusEndpointPath, {
+ start_time,
+ end_time,
+ })
.then(data => {
commit(types.UPDATE_VARIABLE_METRIC_LABEL_VALUES, { variable, label, data });
})
@@ -507,5 +491,59 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery
return Promise.all(optionsRequests);
};
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
+// Panel Builder
+
+export const setPanelPreviewTimeRange = ({ commit }, timeRange) => {
+ commit(types.SET_PANEL_PREVIEW_TIME_RANGE, timeRange);
+};
+
+export const fetchPanelPreview = ({ state, commit, dispatch }, panelPreviewYml) => {
+ if (!panelPreviewYml) {
+ return null;
+ }
+
+ commit(types.SET_PANEL_PREVIEW_IS_SHOWN, true);
+ commit(types.REQUEST_PANEL_PREVIEW, panelPreviewYml);
+
+ return axios
+ .post(state.panelPreviewEndpoint, { panel_yaml: panelPreviewYml })
+ .then(({ data }) => {
+ commit(types.RECEIVE_PANEL_PREVIEW_SUCCESS, data);
+
+ dispatch('fetchPanelPreviewMetrics');
+ })
+ .catch(error => {
+ commit(types.RECEIVE_PANEL_PREVIEW_FAILURE, extractErrorMessage(error));
+ });
+};
+
+export const fetchPanelPreviewMetrics = ({ state, commit }) => {
+ if (cancelTokenSource) {
+ cancelTokenSource.cancel();
+ }
+ cancelTokenSource = axiosCancelToken.source();
+
+ const defaultQueryParams = prometheusMetricQueryParams(state.panelPreviewTimeRange);
+
+ state.panelPreviewGraphData.metrics.forEach((metric, index) => {
+ commit(types.REQUEST_PANEL_PREVIEW_METRIC_RESULT, { index });
+
+ const params = { ...defaultQueryParams };
+ if (metric.step) {
+ params.step = metric.step;
+ }
+ return getPrometheusQueryData(metric.prometheusEndpointPath, params, {
+ cancelToken: cancelTokenSource.token,
+ })
+ .then(data => {
+ commit(types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS, { index, data });
+ })
+ .catch(error => {
+ Sentry.captureException(error);
+
+ commit(types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE, { index, error });
+ // Continue to throw error so the panel builder can notify using createFlash
+ throw error;
+ });
+ });
+};
diff --git a/app/assets/javascripts/monitoring/stores/embed_group/actions.js b/app/assets/javascripts/monitoring/stores/embed_group/actions.js
index cbe0950d954..4a7572bdbd9 100644
--- a/app/assets/javascripts/monitoring/stores/embed_group/actions.js
+++ b/app/assets/javascripts/monitoring/stores/embed_group/actions.js
@@ -1,5 +1,4 @@
import * as types from './mutation_types';
+// eslint-disable-next-line import/prefer-default-export
export const addModule = ({ commit }, data) => commit(types.ADD_MODULE, data);
-
-export default () => {};
diff --git a/app/assets/javascripts/monitoring/stores/embed_group/getters.js b/app/assets/javascripts/monitoring/stores/embed_group/getters.js
index 9b08cf762c1..096d8d03096 100644
--- a/app/assets/javascripts/monitoring/stores/embed_group/getters.js
+++ b/app/assets/javascripts/monitoring/stores/embed_group/getters.js
@@ -1,4 +1,3 @@
+// eslint-disable-next-line import/prefer-default-export
export const metricsWithData = (state, getters, rootState, rootGetters) =>
state.modules.map(module => rootGetters[`${module}/metricsWithData`]().length);
-
-export default () => {};
diff --git a/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js b/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js
index e7a425d3623..7fd3f0f8647 100644
--- a/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js
@@ -1,3 +1,2 @@
+// eslint-disable-next-line import/prefer-default-export
export const ADD_MODULE = 'ADD_MODULE';
-
-export default () => {};
diff --git a/app/assets/javascripts/monitoring/stores/getters.js b/app/assets/javascripts/monitoring/stores/getters.js
index 3aa711a0509..8ed83cf02fe 100644
--- a/app/assets/javascripts/monitoring/stores/getters.js
+++ b/app/assets/javascripts/monitoring/stores/getters.js
@@ -170,6 +170,3 @@ export const getCustomVariablesParams = state =>
*/
export const fullDashboardPath = state =>
normalizeCustomDashboardPath(state.currentDashboard, state.customDashboardBasePath);
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
index d408628fc4d..1d7279912cc 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -46,3 +46,17 @@ export const SET_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER';
export const SET_PANEL_GROUP_METRICS = 'SET_PANEL_GROUP_METRICS';
export const SET_ENVIRONMENTS_FILTER = 'SET_ENVIRONMENTS_FILTER';
export const SET_EXPANDED_PANEL = 'SET_EXPANDED_PANEL';
+
+// Panel preview
+export const REQUEST_PANEL_PREVIEW = 'REQUEST_PANEL_PREVIEW';
+export const RECEIVE_PANEL_PREVIEW_SUCCESS = 'RECEIVE_PANEL_PREVIEW_SUCCESS';
+export const RECEIVE_PANEL_PREVIEW_FAILURE = 'RECEIVE_PANEL_PREVIEW_FAILURE';
+
+export const REQUEST_PANEL_PREVIEW_METRIC_RESULT = 'REQUEST_PANEL_PREVIEW_METRIC_RESULT';
+export const RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS =
+ 'RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS';
+export const RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE =
+ 'RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE';
+
+export const SET_PANEL_PREVIEW_TIME_RANGE = 'SET_PANEL_PREVIEW_TIME_RANGE';
+export const SET_PANEL_PREVIEW_IS_SHOWN = 'SET_PANEL_PREVIEW_IS_SHOWN';
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index 744441c8935..09a5861b475 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import { pick } from 'lodash';
import * as types from './mutation_types';
-import { mapToDashboardViewModel, normalizeQueryResponseData } from './utils';
+import { mapToDashboardViewModel, mapPanelToViewModel, normalizeQueryResponseData } from './utils';
import httpStatusCodes from '~/lib/utils/http_status';
-import { BACKOFF_TIMEOUT } from '../../lib/utils/common_utils';
+import { BACKOFF_TIMEOUT } from '~/lib/utils/common_utils';
import { dashboardEmptyStates, endpointKeys, initialStateKeys, metricStates } from '../constants';
import { optionsFromSeriesData } from './variable_mapping';
@@ -53,6 +53,14 @@ const emptyStateFromError = error => {
return metricStates.UNKNOWN_ERROR;
};
+export const metricStateFromData = data => {
+ if (data?.result?.length) {
+ const result = normalizeQueryResponseData(data);
+ return { state: metricStates.OK, result: Object.freeze(result) };
+ }
+ return { state: metricStates.NO_DATA, result: null };
+};
+
export default {
/**
* Dashboard panels structure and global state
@@ -154,17 +162,11 @@ export default {
},
[types.RECEIVE_METRIC_RESULT_SUCCESS](state, { metricId, data }) {
const metric = findMetricInDashboard(metricId, state.dashboard);
- metric.loading = false;
+ const metricState = metricStateFromData(data);
- if (!data.result || data.result.length === 0) {
- metric.state = metricStates.NO_DATA;
- metric.result = null;
- } else {
- const result = normalizeQueryResponseData(data);
-
- metric.state = metricStates.OK;
- metric.result = Object.freeze(result);
- }
+ metric.loading = false;
+ metric.state = metricState.state;
+ metric.result = metricState.result;
},
[types.RECEIVE_METRIC_RESULT_FAILURE](state, { metricId, error }) {
const metric = findMetricInDashboard(metricId, state.dashboard);
@@ -218,4 +220,54 @@ export default {
// Add new options with assign to ensure Vue reactivity
Object.assign(variable.options, { values });
},
+
+ [types.REQUEST_PANEL_PREVIEW](state, panelPreviewYml) {
+ state.panelPreviewIsLoading = true;
+
+ state.panelPreviewYml = panelPreviewYml;
+ state.panelPreviewGraphData = null;
+ state.panelPreviewError = null;
+ },
+ [types.RECEIVE_PANEL_PREVIEW_SUCCESS](state, payload) {
+ state.panelPreviewIsLoading = false;
+
+ state.panelPreviewGraphData = mapPanelToViewModel(payload);
+ state.panelPreviewError = null;
+ },
+ [types.RECEIVE_PANEL_PREVIEW_FAILURE](state, error) {
+ state.panelPreviewIsLoading = false;
+
+ state.panelPreviewGraphData = null;
+ state.panelPreviewError = error;
+ },
+
+ [types.REQUEST_PANEL_PREVIEW_METRIC_RESULT](state, { index }) {
+ const metric = state.panelPreviewGraphData.metrics[index];
+
+ metric.loading = true;
+ if (!metric.result) {
+ metric.state = metricStates.LOADING;
+ }
+ },
+ [types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS](state, { index, data }) {
+ const metric = state.panelPreviewGraphData.metrics[index];
+ const metricState = metricStateFromData(data);
+
+ metric.loading = false;
+ metric.state = metricState.state;
+ metric.result = metricState.result;
+ },
+ [types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE](state, { index, error }) {
+ const metric = state.panelPreviewGraphData.metrics[index];
+
+ metric.loading = false;
+ metric.state = emptyStateFromError(error);
+ metric.result = null;
+ },
+ [types.SET_PANEL_PREVIEW_TIME_RANGE](state, timeRange) {
+ state.panelPreviewTimeRange = timeRange;
+ },
+ [types.SET_PANEL_PREVIEW_IS_SHOWN](state, isPreviewShown) {
+ state.panelPreviewIsShown = isPreviewShown;
+ },
};
diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js
index 89738756ffe..ef8b1adb624 100644
--- a/app/assets/javascripts/monitoring/stores/state.js
+++ b/app/assets/javascripts/monitoring/stores/state.js
@@ -1,12 +1,14 @@
import invalidUrl from '~/lib/utils/invalid_url';
import { timezones } from '../format_date';
import { dashboardEmptyStates } from '../constants';
+import { defaultTimeRange } from '~/vue_shared/constants';
export default () => ({
// API endpoints
deploymentsEndpoint: null,
dashboardEndpoint: invalidUrl,
dashboardsEndpoint: invalidUrl,
+ panelPreviewEndpoint: invalidUrl,
// Dashboard request parameters
timeRange: null,
@@ -59,6 +61,15 @@ export default () => ({
* via the dashboard yml file.
*/
links: [],
+
+ // Panel editor / builder
+ panelPreviewYml: '',
+ panelPreviewIsLoading: false,
+ panelPreviewGraphData: null,
+ panelPreviewError: null,
+ panelPreviewTimeRange: defaultTimeRange,
+ panelPreviewIsShown: false,
+
// Other project data
dashboardTimezone: timezones.LOCAL,
annotations: [],
@@ -69,9 +80,11 @@ export default () => ({
currentEnvironmentName: null,
// GitLab paths to other pages
+ externalDashboardUrl: '',
projectPath: null,
operationsSettingsPath: '',
logsPath: invalidUrl,
+ addDashboardDocumentationPath: '',
// static paths
customDashboardBasePath: '',
diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js
index 51562593ee8..df7f22e622f 100644
--- a/app/assets/javascripts/monitoring/stores/utils.js
+++ b/app/assets/javascripts/monitoring/stores/utils.js
@@ -176,7 +176,11 @@ export const mapPanelToViewModel = ({
field,
metrics = [],
links = [],
+ min_value,
max_value,
+ split,
+ thresholds,
+ format,
}) => {
// Both `x_axis.name` and `x_label` are supported for now
// https://gitlab.com/gitlab-org/gitlab/issues/210521
@@ -195,7 +199,11 @@ export const mapPanelToViewModel = ({
yAxis,
xAxis,
field,
+ minValue: min_value,
maxValue: max_value,
+ split,
+ thresholds,
+ format,
links: links.map(mapLinksToViewModel),
metrics: mapToMetricsViewModel(metrics),
};
@@ -465,9 +473,9 @@ export const addPrefixToCustomVariableParams = name => `variables[${name}]`;
* metrics dashboard to work with custom dashboard file names instead
* of the entire path.
*
- * If dashboard is empty, it is the default dashboard.
+ * If dashboard is empty, it is the overview dashboard.
* If dashboard is set, it usually is a custom dashboard unless
- * explicitly it is set to default dashboard path.
+ * explicitly it is set to overview dashboard path.
*
* @param {String} dashboard dashboard path
* @param {String} dashboardPrefix custom dashboard directory prefix
diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js
index 0c6fcad9dd0..92bbce498d5 100644
--- a/app/assets/javascripts/monitoring/utils.js
+++ b/app/assets/javascripts/monitoring/utils.js
@@ -24,13 +24,16 @@ export const stateAndPropsFromDataset = (dataset = {}) => {
deploymentsEndpoint,
dashboardEndpoint,
dashboardsEndpoint,
+ panelPreviewEndpoint,
dashboardTimezone,
canAccessOperationsSettings,
operationsSettingsPath,
projectPath,
logsPath,
+ externalDashboardUrl,
currentEnvironmentName,
customDashboardBasePath,
+ addDashboardDocumentationPath,
...dataProps
} = dataset;
@@ -45,13 +48,16 @@ export const stateAndPropsFromDataset = (dataset = {}) => {
deploymentsEndpoint,
dashboardEndpoint,
dashboardsEndpoint,
+ panelPreviewEndpoint,
dashboardTimezone,
canAccessOperationsSettings,
operationsSettingsPath,
projectPath,
logsPath,
+ externalDashboardUrl,
currentEnvironmentName,
customDashboardBasePath,
+ addDashboardDocumentationPath,
},
dataProps,
};
diff --git a/app/assets/javascripts/monitoring/validators.js b/app/assets/javascripts/monitoring/validators.js
index cd426f1a221..c6b323f6360 100644
--- a/app/assets/javascripts/monitoring/validators.js
+++ b/app/assets/javascripts/monitoring/validators.js
@@ -1,3 +1,12 @@
+import { isSafeURL } from '~/lib/utils/url_utility';
+
+const isRunbookUrlValid = runbookUrl => {
+ if (!runbookUrl) {
+ return true;
+ }
+ return isSafeURL(runbookUrl);
+};
+
// Prop validator for alert information, expecting an object like the example below.
//
// {
@@ -8,6 +17,7 @@
// query: "rate(http_requests_total[5m])[30m:1m]",
// threshold: 0.002,
// title: "Core Usage (Total)",
+// runbookUrl: "https://www.gitlab.com/my-project/-/wikis/runbook"
// }
// }
export function alertsValidator(value) {
@@ -19,7 +29,8 @@ export function alertsValidator(value) {
alert.metricId &&
typeof alert.metricId === 'string' &&
alert.operator &&
- typeof alert.threshold === 'number'
+ typeof alert.threshold === 'number' &&
+ isRunbookUrlValid(alert.runbookUrl)
);
});
}