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/components')
-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
18 files changed, 922 insertions, 352 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>