diff options
Diffstat (limited to 'app/assets/javascripts/monitoring/components')
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> |