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/charts/anomaly.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/charts/heatmap.vue5
-rw-r--r--app/assets/javascripts/monitoring/components/charts/options.js18
-rw-r--r--app/assets/javascripts/monitoring/components/charts/single_stat.vue11
-rw-r--r--app/assets/javascripts/monitoring/components/charts/stacked_column.vue4
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/create_dashboard_modal.vue66
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue121
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_header.vue168
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel.vue67
-rw-r--r--app/assets/javascripts/monitoring/components/dashboards_dropdown.vue85
-rw-r--r--app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue95
-rw-r--r--app/assets/javascripts/monitoring/components/empty_state.vue74
-rw-r--r--app/assets/javascripts/monitoring/components/graph_group.vue19
-rw-r--r--app/assets/javascripts/monitoring/components/refresh_button.vue163
-rw-r--r--app/assets/javascripts/monitoring/components/variables/dropdown_field.vue (renamed from app/assets/javascripts/monitoring/components/variables/custom_variable.vue)19
-rw-r--r--app/assets/javascripts/monitoring/components/variables/text_field.vue (renamed from app/assets/javascripts/monitoring/components/variables/text_variable.vue)2
-rw-r--r--app/assets/javascripts/monitoring/components/variables_section.vue33
18 files changed, 747 insertions, 207 deletions
diff --git a/app/assets/javascripts/monitoring/components/charts/anomaly.vue b/app/assets/javascripts/monitoring/components/charts/anomaly.vue
index 34da5885c97..ac401c6e381 100644
--- a/app/assets/javascripts/monitoring/components/charts/anomaly.vue
+++ b/app/assets/javascripts/monitoring/components/charts/anomaly.vue
@@ -218,7 +218,7 @@ export default {
<gl-chart-series-label :color="content.color">
{{ content.name }}
</gl-chart-series-label>
- <div class="prepend-left-32">
+ <div class="gl-ml-7">
{{ yValueFormatted(seriesIndex, content.dataIndex) }}
</div>
</div>
diff --git a/app/assets/javascripts/monitoring/components/charts/heatmap.vue b/app/assets/javascripts/monitoring/components/charts/heatmap.vue
index f6f266dacf3..ddb44f7b1be 100644
--- a/app/assets/javascripts/monitoring/components/charts/heatmap.vue
+++ b/app/assets/javascripts/monitoring/components/charts/heatmap.vue
@@ -48,7 +48,10 @@ export default {
return this.result.values.map(val => {
const [yLabel] = val;
- return formatDate(new Date(yLabel), { format: formats.shortTime, timezone: this.timezone });
+ return formatDate(new Date(yLabel), {
+ format: formats.shortTime,
+ timezone: this.timezone,
+ });
});
},
result() {
diff --git a/app/assets/javascripts/monitoring/components/charts/options.js b/app/assets/javascripts/monitoring/components/charts/options.js
index f7822e69b1d..42252dd5897 100644
--- a/app/assets/javascripts/monitoring/components/charts/options.js
+++ b/app/assets/javascripts/monitoring/components/charts/options.js
@@ -17,7 +17,9 @@ const defaultTooltipFormat = defaultFormat;
const defaultTooltipPrecision = 3;
// Give enough space for y-axis with units and name.
-const chartGridLeft = 75;
+const chartGridLeft = 63; // larger gap than gitlab-ui's default to fit formatted numbers
+const chartGridRight = 10; // half of the scroll-handle icon for data zoom
+const yAxisNameGap = chartGridLeft - 12; // offset the axis label line-height
// Axis options
@@ -62,7 +64,7 @@ export const getYAxisOptions = ({
precision = defaultYAxisPrecision,
} = {}) => {
return {
- nameGap: 63, // larger gap than gitlab-ui's default to fit with formatted numbers
+ nameGap: yAxisNameGap,
scale: true,
boundaryGap: yAxisBoundaryGap,
@@ -74,11 +76,14 @@ export const getYAxisOptions = ({
};
};
-export const getTimeAxisOptions = ({ timezone = timezones.LOCAL } = {}) => ({
+export const getTimeAxisOptions = ({
+ timezone = timezones.LOCAL,
+ format = formats.shortDateTime,
+} = {}) => ({
name: __('Time'),
type: axisTypes.time,
axisLabel: {
- formatter: date => formatDate(date, { format: formats.shortTime, timezone }),
+ formatter: date => formatDate(date, { format, timezone }),
},
axisPointer: {
snap: false,
@@ -90,7 +95,10 @@ export const getTimeAxisOptions = ({ timezone = timezones.LOCAL } = {}) => ({
/**
* Grid with enough room to display chart.
*/
-export const getChartGrid = ({ left = chartGridLeft } = {}) => ({ left });
+export const getChartGrid = ({ left = chartGridLeft, right = chartGridRight } = {}) => ({
+ left,
+ right,
+});
// Tooltip options
diff --git a/app/assets/javascripts/monitoring/components/charts/single_stat.vue b/app/assets/javascripts/monitoring/components/charts/single_stat.vue
index eee5eaa5eca..106c76a97dc 100644
--- a/app/assets/javascripts/monitoring/components/charts/single_stat.vue
+++ b/app/assets/javascripts/monitoring/components/charts/single_stat.vue
@@ -1,9 +1,11 @@
<script>
import { GlSingleStat } from '@gitlab/ui/dist/charts';
+import { __ } from '~/locale';
import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format';
import { graphDataValidatorForValues } from '../../utils';
const defaultPrecision = 2;
+const emptyStateMsg = __('No data to display');
export default {
components: {
@@ -21,6 +23,9 @@ export default {
queryInfo() {
return this.graphData.metrics[0];
},
+ queryMetric() {
+ return this.queryInfo.result[0]?.metric;
+ },
queryResult() {
return this.queryInfo.result[0]?.value[1];
},
@@ -33,6 +38,12 @@ export default {
statValue() {
let formatter;
+ // if field is present the metric value is not displayed. Hence
+ // the early exit without formatting.
+ if (this.graphData?.field) {
+ return this.queryMetric?.[this.graphData.field] ?? emptyStateMsg;
+ }
+
if (this.graphData?.maxValue) {
formatter = getFormatter(SUPPORTED_FORMATS.percent);
return formatter(this.queryResult / Number(this.graphData.maxValue), defaultPrecision);
diff --git a/app/assets/javascripts/monitoring/components/charts/stacked_column.vue b/app/assets/javascripts/monitoring/components/charts/stacked_column.vue
index ac31d107e63..9bcd4419a14 100644
--- a/app/assets/javascripts/monitoring/components/charts/stacked_column.vue
+++ b/app/assets/javascripts/monitoring/components/charts/stacked_column.vue
@@ -6,7 +6,7 @@ import { chartHeight, legendLayoutTypes } from '../../constants';
import { s__ } from '~/locale';
import { graphDataValidatorForValues } from '../../utils';
import { getTimeAxisOptions, axisTypes } from './options';
-import { timezones } from '../../format_date';
+import { formats, timezones } from '../../format_date';
export default {
components: {
@@ -97,7 +97,7 @@ export default {
chartOptions() {
return {
xAxis: {
- ...getTimeAxisOptions({ timezone: this.timezone }),
+ ...getTimeAxisOptions({ timezone: this.timezone, format: formats.shortTime }),
type: this.xAxisType,
},
dataZoom: [this.dataZoomConfig],
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
index 28af2d8ba77..f2add429a80 100644
--- a/app/assets/javascripts/monitoring/components/charts/time_series.vue
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -415,7 +415,7 @@ export default {
<gl-chart-series-label :color="isMultiSeries ? content.color : ''">
{{ content.name }}
</gl-chart-series-label>
- <div class="prepend-left-32">
+ <div class="gl-ml-7">
{{ content.value }}
</div>
</div>
diff --git a/app/assets/javascripts/monitoring/components/create_dashboard_modal.vue b/app/assets/javascripts/monitoring/components/create_dashboard_modal.vue
new file mode 100644
index 00000000000..74799002b17
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/create_dashboard_modal.vue
@@ -0,0 +1,66 @@
+<script>
+import { GlButton, GlModal, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { isSafeURL } from '~/lib/utils/url_utility';
+
+export default {
+ components: { GlButton, GlModal, GlSprintf },
+ props: {
+ modalId: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ validator: isSafeURL,
+ },
+ addDashboardDocumentationPath: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ cancelHandler() {
+ this.$refs.modal.hide();
+ },
+ },
+ i18n: {
+ titleText: s__('Metrics|Create your dashboard configuration file'),
+ mainText: s__(
+ 'Metrics|To create a new dashboard, add a new YAML file to %{codeStart}.gitlab/dashboards%{codeEnd} at the root of this project.',
+ ),
+ },
+};
+</script>
+
+<template>
+ <gl-modal ref="modal" :modal-id="modalId" :title="$options.i18n.titleText">
+ <p>
+ <gl-sprintf :message="$options.i18n.mainText">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ <template #modal-footer>
+ <gl-button category="secondary" @click="cancelHandler">{{ s__('Metrics|Cancel') }}</gl-button>
+ <gl-button
+ category="secondary"
+ variant="info"
+ target="_blank"
+ :href="addDashboardDocumentationPath"
+ data-testid="create-dashboard-modal-docs-button"
+ >
+ {{ s__('Metrics|View documentation') }}
+ </gl-button>
+ <gl-button
+ variant="success"
+ data-testid="create-dashboard-modal-repo-button"
+ :href="projectPath"
+ >
+ {{ s__('Metrics|Open repository') }}
+ </gl-button>
+ </template>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index f54319d283e..bde62275797 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -1,6 +1,7 @@
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import VueDraggable from 'vuedraggable';
+import Mousetrap from 'mousetrap';
import { GlIcon, GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
import DashboardHeader from './dashboard_header.vue';
import DashboardPanel from './dashboard_panel.vue';
@@ -24,7 +25,7 @@ import {
expandedPanelPayloadFromUrl,
convertVariablesForURL,
} from '../utils';
-import { metricStates } from '../constants';
+import { metricStates, keyboardShortcutKeys } from '../constants';
import { defaultTimeRange } from '~/vue_shared/constants';
export default {
@@ -71,6 +72,10 @@ export default {
type: String,
required: true,
},
+ addDashboardDocumentationPath: {
+ type: String,
+ required: true,
+ },
settingsPath: {
type: String,
required: true,
@@ -149,21 +154,25 @@ export default {
selectedTimeRange: timeRangeFromUrl() || defaultTimeRange,
isRearrangingPanels: false,
originalDocumentTitle: document.title,
+ hoveredPanel: '',
};
},
computed: {
...mapState('monitoringDashboard', [
'dashboard',
'emptyState',
- 'showEmptyState',
'expandedPanel',
'variables',
'links',
'currentDashboard',
+ 'hasDashboardValidationWarnings',
]),
...mapGetters('monitoringDashboard', ['selectedDashboard', 'getMetricStates']),
+ shouldShowEmptyState() {
+ return Boolean(this.emptyState);
+ },
shouldShowVariablesSection() {
- return Object.keys(this.variables).length > 0;
+ return Boolean(this.variables.length);
},
shouldShowLinksSection() {
return Object.keys(this.links).length > 0;
@@ -197,12 +206,29 @@ export default {
selectedDashboard(dashboard) {
this.prependToDocumentTitle(dashboard?.display_name);
},
+ hasDashboardValidationWarnings(hasWarnings) {
+ /**
+ * This watcher is set for future SPA behaviour of the dashboard
+ */
+ if (hasWarnings) {
+ createFlash(
+ s__(
+ 'Metrics|Your dashboard schema is invalid. Edit the dashboard to correct the YAML schema.',
+ ),
+ 'warning',
+ );
+ }
+ },
},
created() {
window.addEventListener('keyup', this.onKeyup);
+
+ Mousetrap.bind(Object.values(keyboardShortcutKeys), this.runShortcut);
},
destroyed() {
window.removeEventListener('keyup', this.onKeyup);
+
+ Mousetrap.unbind(Object.values(keyboardShortcutKeys));
},
mounted() {
if (!this.hasMetrics) {
@@ -254,6 +280,14 @@ export default {
return null;
},
/**
+ * Return true if the entire group is loading.
+ * @param {String} groupKey - Identifier for group
+ * @returns {boolean}
+ */
+ isGroupLoading(groupKey) {
+ return this.groupSingleEmptyState(groupKey) === metricStates.LOADING;
+ },
+ /**
* A group should be not collapsed if any metric is loaded (OK)
*
* @param {String} groupKey - Identifier for group
@@ -302,6 +336,66 @@ export default {
// As a fallback, switch to default time range instead
this.selectedTimeRange = defaultTimeRange;
},
+ isPanelHalfWidth(panelIndex, totalPanels) {
+ /**
+ * A single panel on a row should take the full width of its parent.
+ * All others should have half the width their parent.
+ */
+ const isNumberOfPanelsEven = totalPanels % 2 === 0;
+ const isLastPanel = panelIndex === totalPanels - 1;
+
+ return isNumberOfPanelsEven || !isLastPanel;
+ },
+ /**
+ * TODO: Investigate this to utilize the eventBus from Vue
+ * The intentation behind this cleanup is to allow for better tests
+ * as well as use the correct eventBus facilities that are compatible
+ * with Vue 3
+ * https://gitlab.com/gitlab-org/gitlab/-/issues/225583
+ */
+ //
+ runShortcut(e) {
+ const panel = this.$refs[this.hoveredPanel];
+
+ if (!panel) return;
+
+ const [panelInstance] = panel;
+ let actionToRun = '';
+
+ switch (e.key) {
+ case keyboardShortcutKeys.EXPAND:
+ actionToRun = 'onExpandFromKeyboardShortcut';
+ break;
+
+ case keyboardShortcutKeys.VISIT_LOGS:
+ actionToRun = 'visitLogsPageFromKeyboardShortcut';
+ break;
+
+ case keyboardShortcutKeys.SHOW_ALERT:
+ actionToRun = 'showAlertModalFromKeyboardShortcut';
+ break;
+
+ case keyboardShortcutKeys.DOWNLOAD_CSV:
+ actionToRun = 'downloadCsvFromKeyboardShortcut';
+ break;
+
+ case keyboardShortcutKeys.CHART_COPY:
+ actionToRun = 'copyChartLinkFromKeyboardShotcut';
+ break;
+
+ default:
+ actionToRun = 'onExpandFromKeyboardShortcut';
+ break;
+ }
+
+ panelInstance[actionToRun]();
+ },
+ setHoveredPanel(groupKey, graphIndex) {
+ this.hoveredPanel = `dashboard-panel-${groupKey}-${graphIndex}`;
+ },
+ clearHoveredPanel() {
+ this.hoveredPanel = '';
+ },
},
i18n: {
goBackLabel: s__('Metrics|Go back (Esc)'),
@@ -315,6 +409,7 @@ 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"
@@ -327,9 +422,9 @@ export default {
@dateTimePickerInvalid="onDateTimePickerInvalid"
@setRearrangingPanels="onSetRearrangingPanels"
/>
- <variables-section v-if="shouldShowVariablesSection && !showEmptyState" />
- <links-section v-if="shouldShowLinksSection && !showEmptyState" />
- <div v-if="!showEmptyState">
+ <template v-if="!shouldShowEmptyState">
+ <variables-section v-if="shouldShowVariablesSection" />
+ <links-section v-if="shouldShowLinksSection" />
<dashboard-panel
v-show="expandedPanel.panel"
ref="expandedPanel"
@@ -364,6 +459,7 @@ export default {
:key="`${groupData.group}.${groupData.priority}`"
:name="groupData.group"
:show-panels="showPanels"
+ :is-loading="isGroupLoading(groupData.key)"
:collapse-group="collapseGroup(groupData.key)"
>
<vue-draggable
@@ -377,8 +473,14 @@ export default {
<div
v-for="(graphData, graphIndex) in groupData.panels"
:key="`dashboard-panel-${graphIndex}`"
- class="col-12 col-lg-6 px-2 mb-2 draggable"
- :class="{ 'draggable-enabled': isRearrangingPanels }"
+ data-testid="dashboard-panel-layout-wrapper"
+ class="col-12 px-2 mb-2 draggable"
+ :class="{
+ 'draggable-enabled': isRearrangingPanels,
+ 'col-lg-6': isPanelHalfWidth(graphIndex, groupData.panels.length),
+ }"
+ @mouseover="setHoveredPanel(groupData.key, graphIndex)"
+ @mouseout="clearHoveredPanel"
>
<div class="position-relative draggable-panel js-draggable-panel">
<div
@@ -392,6 +494,7 @@ export default {
</div>
<dashboard-panel
+ :ref="`dashboard-panel-${groupData.key}-${graphIndex}`"
:settings-path="settingsPath"
:clipboard-text="generatePanelUrl(groupData.group, graphData)"
:graph-data="graphData"
@@ -414,7 +517,7 @@ export default {
</div>
</graph-group>
</div>
- </div>
+ </template>
<empty-state
v-else
:selected-state="emptyState"
diff --git a/app/assets/javascripts/monitoring/components/dashboard_header.vue b/app/assets/javascripts/monitoring/components/dashboard_header.vue
index 16a21ae0d3c..fe6ca3a2a07 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_header.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_header.vue
@@ -2,12 +2,16 @@
import { debounce } from 'lodash';
import { mapActions, mapState, mapGetters } from 'vuex';
import {
+ GlButton,
GlIcon,
GlDeprecatedButton,
GlDropdown,
GlDropdownItem,
GlDropdownHeader,
GlDropdownDivider,
+ GlNewDropdown,
+ GlNewDropdownDivider,
+ GlNewDropdownItem,
GlModal,
GlLoadingIcon,
GlSearchBoxByType,
@@ -22,6 +26,9 @@ import Icon from '~/vue_shared/components/icon.vue';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import DashboardsDropdown from './dashboards_dropdown.vue';
+import RefreshButton from './refresh_button.vue';
+import CreateDashboardModal from './create_dashboard_modal.vue';
+import DuplicateDashboardModal from './duplicate_dashboard_modal.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import { getAddMetricTrackingOptions, timeRangeToUrl } from '../utils';
@@ -31,6 +38,7 @@ import { timezones } from '../format_date';
export default {
components: {
Icon,
+ GlButton,
GlIcon,
GlDeprecatedButton,
GlDropdown,
@@ -38,12 +46,18 @@ export default {
GlDropdownItem,
GlDropdownHeader,
GlDropdownDivider,
+ GlNewDropdown,
+ GlNewDropdownDivider,
+ GlNewDropdownItem,
GlSearchBoxByType,
GlModal,
CustomMetricsFormFields,
DateTimePicker,
DashboardsDropdown,
+ RefreshButton,
+ DuplicateDashboardModal,
+ CreateDashboardModal,
},
directives: {
GlModal: GlModalDirective,
@@ -93,6 +107,10 @@ export default {
type: Object,
required: true,
},
+ addDashboardDocumentationPath: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -101,20 +119,30 @@ export default {
},
computed: {
...mapState('monitoringDashboard', [
+ 'emptyState',
'environmentsLoading',
'currentEnvironmentName',
'isUpdatingStarredValue',
- 'showEmptyState',
'dashboardTimezone',
+ 'projectPath',
+ 'canAccessOperationsSettings',
+ 'operationsSettingsPath',
+ 'currentDashboard',
]),
...mapGetters('monitoringDashboard', ['selectedDashboard', 'filteredEnvironments']),
+ isOutOfTheBoxDashboard() {
+ return this.selectedDashboard?.out_of_the_box_dashboard;
+ },
+ shouldShowEmptyState() {
+ return Boolean(this.emptyState);
+ },
shouldShowEnvironmentsDropdownNoMatchedMsg() {
return !this.environmentsLoading && this.filteredEnvironments.length === 0;
},
addingMetricsAvailable() {
return (
this.customMetricsAvailable &&
- !this.showEmptyState &&
+ !this.shouldShowEmptyState &&
// 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
@@ -122,23 +150,29 @@ export default {
);
},
showRearrangePanelsBtn() {
- return !this.showEmptyState && this.rearrangePanelsAvailable;
+ return !this.shouldShowEmptyState && this.rearrangePanelsAvailable;
},
displayUtc() {
return this.dashboardTimezone === timezones.UTC;
},
+ shouldShowActionsMenu() {
+ return Boolean(this.projectPath);
+ },
+ shouldShowSettingsButton() {
+ return this.canAccessOperationsSettings && this.operationsSettingsPath;
+ },
},
methods: {
- ...mapActions('monitoringDashboard', [
- 'filterEnvironments',
- 'fetchDashboardData',
- 'toggleStarredValue',
- ]),
+ ...mapActions('monitoringDashboard', ['filterEnvironments', 'toggleStarredValue']),
selectDashboard(dashboard) {
- const params = {
- dashboard: dashboard.path,
- };
- redirectTo(mergeUrlParams(params, window.location.href));
+ // 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}`);
},
debouncedEnvironmentsSearch: debounce(function environmentsSearchOnInput(searchTerm) {
this.filterEnvironments(searchTerm);
@@ -149,9 +183,6 @@ export default {
onDateTimePickerInvalid() {
this.$emit('dateTimePickerInvalid');
},
- refreshDashboard() {
- this.fetchDashboardData();
- },
toggleRearrangingPanels() {
this.$emit('setRearrangingPanels', !this.isRearrangingPanels);
@@ -166,14 +197,27 @@ export default {
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.
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/229277
+ const baseURL = `${this.projectPath}/-/metrics`;
+ const dashboardPath = encodeURIComponent(this.currentDashboard || '');
+ // The environment_metrics_spec.rb requires the URL to not have
+ // slashes. Hence, this additional check.
+ const url = dashboardPath ? `${baseURL}/${dashboardPath}` : baseURL;
+ return mergeUrlParams({ environment }, url);
+ },
},
- addMetric: {
- title: s__('Metrics|Add metric'),
- modalId: 'add-metric',
+ modalIds: {
+ addMetric: 'addMetric',
+ createDashboard: 'createDashboard',
+ duplicateDashboard: 'duplicateDashboard',
},
i18n: {
starDashboard: s__('Metrics|Star dashboard'),
unstarDashboard: s__('Metrics|Unstar dashboard'),
+ addMetric: s__('Metrics|Add metric'),
},
timeRanges,
};
@@ -181,17 +225,20 @@ export default {
<template>
<div ref="prometheusGraphsHeader">
- <div class="mb-2 pr-2 d-flex d-sm-block">
+ <div class="mb-2 mr-2 d-flex d-sm-block">
<dashboards-dropdown
id="monitor-dashboards-dropdown"
data-qa-selector="dashboards_filter_dropdown"
class="flex-grow-1"
toggle-class="dropdown-menu-toggle"
:default-branch="defaultBranch"
+ :modal-id="$options.modalIds.duplicateDashboard"
@selectDashboard="selectDashboard"
/>
</div>
+ <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
id="monitor-environments-dropdown"
@@ -223,7 +270,7 @@ export default {
:key="environment.id"
:active="environment.name === currentEnvironmentName"
active-class="is-active"
- :href="environment.metrics_path"
+ :href="getEnvironmentPath(environment.id)"
>{{ environment.name }}</gl-dropdown-item
>
</div>
@@ -252,16 +299,7 @@ export default {
</div>
<div class="mb-2 pr-2 d-flex d-sm-block">
- <gl-deprecated-button
- ref="refreshDashboardBtn"
- v-gl-tooltip
- class="flex-grow-1"
- variant="default"
- :title="s__('Metrics|Refresh dashboard')"
- @click="refreshDashboard"
- >
- <icon name="retry" />
- </gl-deprecated-button>
+ <refresh-button />
</div>
<div class="flex-grow-1"></div>
@@ -304,17 +342,17 @@ export default {
<div v-if="addingMetricsAvailable" class="mb-2 mr-2 d-flex d-sm-block">
<gl-deprecated-button
ref="addMetricBtn"
- v-gl-modal="$options.addMetric.modalId"
+ v-gl-modal="$options.modalIds.addMetric"
variant="outline-success"
data-qa-selector="add_metric_button"
class="flex-grow-1"
>
- {{ $options.addMetric.title }}
+ {{ $options.i18n.addMetric }}
</gl-deprecated-button>
<gl-modal
ref="addMetricModal"
- :modal-id="$options.addMetric.modalId"
- :title="$options.addMetric.title"
+ :modal-id="$options.modalIds.addMetric"
+ :title="$options.i18n.addMetric"
>
<form ref="customMetricsForm" :action="customMetricsPath" method="post">
<custom-metrics-form-fields
@@ -353,7 +391,10 @@ export default {
</gl-deprecated-button>
</div>
- <div v-if="externalDashboardUrl.length" class="mb-2 mr-2 d-flex d-sm-block">
+ <div
+ v-if="externalDashboardUrl && externalDashboardUrl.length"
+ class="mb-2 mr-2 d-flex d-sm-block"
+ >
<gl-deprecated-button
class="flex-grow-1 js-external-dashboard-link"
variant="primary"
@@ -364,6 +405,63 @@ export default {
{{ __('View full dashboard') }} <icon name="external-link" />
</gl-deprecated-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 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
+ >
+
+ <create-dashboard-modal
+ data-testid="create-dashboard-modal"
+ :add-dashboard-documentation-path="addDashboardDocumentationPath"
+ :modal-id="$options.modalIds.createDashboard"
+ :project-path="projectPath"
+ />
+
+ <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>
+ <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 9545a211bbd..3e3c8408de3 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_panel.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
@@ -2,6 +2,7 @@
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 {
GlResizeObserverDirective,
GlIcon,
@@ -29,7 +30,6 @@ import MonitorStackedColumnChart from './charts/stacked_column.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import AlertWidget from './alert_widget.vue';
import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
-import { isSafeURL } from '~/lib/utils/url_utility';
const events = {
timeRangeZoom: 'timerangezoom',
@@ -132,7 +132,8 @@ export default {
return this.graphData?.title || '';
},
graphDataHasResult() {
- return this.graphData?.metrics?.[0]?.result?.length > 0;
+ const metrics = this.graphData?.metrics || [];
+ return metrics.some(({ result }) => result?.length > 0);
},
graphDataIsLoading() {
const metrics = this.graphData?.metrics || [];
@@ -207,7 +208,17 @@ export default {
return MonitorTimeSeriesChart;
},
isContextualMenuShown() {
- return Boolean(this.graphDataHasResult && !this.basicChartComponent);
+ if (!this.graphDataHasResult) {
+ return false;
+ }
+ // Only a few charts have a contextual menu, support
+ // for more chart types planned at:
+ // https://gitlab.com/groups/gitlab-org/-/epics/3573
+ return (
+ this.isPanelType(panelTypes.AREA_CHART) ||
+ this.isPanelType(panelTypes.LINE_CHART) ||
+ this.isPanelType(panelTypes.SINGLE_STAT)
+ );
},
editCustomMetricLink() {
if (this.graphData.metrics.length > 1) {
@@ -223,13 +234,19 @@ export default {
return metrics.some(({ metricId }) => this.metricsSavedToDb.includes(metricId));
},
alertWidgetAvailable() {
+ const supportsAlerts =
+ this.isPanelType(panelTypes.AREA_CHART) || this.isPanelType(panelTypes.LINE_CHART);
return (
+ supportsAlerts &&
this.prometheusAlertsAvailable &&
this.alertsEndpoint &&
this.graphData &&
this.hasMetricsInDb
);
},
+ alertModalId() {
+ return `alert-modal-${this.graphData.id}`;
+ },
},
mounted() {
this.refreshTitleTooltip();
@@ -268,6 +285,11 @@ export default {
onExpand() {
this.$emit(events.expand);
},
+ onExpandFromKeyboardShortcut() {
+ if (this.isContextualMenuShown) {
+ this.onExpand();
+ }
+ },
setAlerts(alertPath, alertAttributes) {
if (alertAttributes) {
this.$set(this.allAlerts, alertPath, alertAttributes);
@@ -278,18 +300,45 @@ export default {
safeUrl(url) {
return isSafeURL(url) ? url : '#';
},
+ showAlertModal() {
+ this.$root.$emit('bv::show::modal', this.alertModalId);
+ },
+ showAlertModalFromKeyboardShortcut() {
+ if (this.isContextualMenuShown) {
+ this.showAlertModal();
+ }
+ },
+ visitLogsPage() {
+ if (this.logsPathWithTimeRange) {
+ visitUrl(relativePathToAbsolute(this.logsPathWithTimeRange, getBaseURL()));
+ }
+ },
+ visitLogsPageFromKeyboardShortcut() {
+ if (this.isContextualMenuShown) {
+ this.visitLogsPage();
+ }
+ },
+ downloadCsvFromKeyboardShortcut() {
+ if (this.csvText && this.isContextualMenuShown) {
+ this.$refs.downloadCsvLink.$el.firstChild.click();
+ }
+ },
+ copyChartLinkFromKeyboardShotcut() {
+ if (this.clipboardText && this.isContextualMenuShown) {
+ this.$refs.copyChartLink.$el.firstChild.click();
+ }
+ },
},
panelTypes,
};
</script>
<template>
<div v-gl-resize-observer="onResize" class="prometheus-graph">
- <div class="d-flex align-items-center mr-3">
+ <div class="d-flex align-items-center">
<slot name="topLeft"></slot>
<h5
ref="graphTitle"
class="prometheus-graph-title gl-font-lg font-weight-bold text-truncate gl-mr-3"
- tabindex="0"
>
{{ title }}
</h5>
@@ -299,7 +348,7 @@ export default {
<alert-widget
v-if="isContextualMenuShown && alertWidgetAvailable"
class="mx-1"
- :modal-id="`alert-modal-${graphData.id}`"
+ :modal-id="alertModalId"
:alerts-endpoint="alertsEndpoint"
:relevant-queries="graphData.metrics"
:alerts-to-manage="getGraphAlerts(graphData.metrics)"
@@ -314,7 +363,7 @@ export default {
ref="contextualMenu"
data-qa-selector="prometheus_graph_widgets"
>
- <div class="d-flex align-items-center">
+ <div data-testid="dropdown-wrapper" class="d-flex align-items-center">
<gl-dropdown
v-gl-tooltip
toggle-class="shadow-none border-0"
@@ -369,13 +418,13 @@ export default {
</gl-dropdown-item>
<gl-dropdown-item
v-if="alertWidgetAvailable"
- v-gl-modal="`alert-modal-${graphData.id}`"
+ v-gl-modal="alertModalId"
data-qa-selector="alert_widget_menu_item"
>
{{ __('Alerts') }}
</gl-dropdown-item>
- <template v-if="graphData.links.length">
+ <template v-if="graphData.links && graphData.links.length">
<gl-dropdown-divider />
<gl-dropdown-item
v-for="(link, index) in graphData.links"
diff --git a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
index 8b86890715f..574f48a72fe 100644
--- a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
+++ b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
@@ -1,19 +1,14 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import {
- GlAlert,
GlIcon,
GlDropdown,
GlDropdownItem,
GlDropdownHeader,
GlDropdownDivider,
GlSearchBoxByType,
- GlModal,
- GlLoadingIcon,
GlModalDirective,
} from '@gitlab/ui';
-import { s__ } from '~/locale';
-import DuplicateDashboardForm from './duplicate_dashboard_form.vue';
const events = {
selectDashboard: 'selectDashboard',
@@ -21,16 +16,12 @@ const events = {
export default {
components: {
- GlAlert,
GlIcon,
GlDropdown,
GlDropdownItem,
GlDropdownHeader,
GlDropdownDivider,
GlSearchBoxByType,
- GlModal,
- GlLoadingIcon,
- DuplicateDashboardForm,
},
directives: {
GlModal: GlModalDirective,
@@ -40,20 +31,21 @@ export default {
type: String,
required: true,
},
+ modalId: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
- alert: null,
- loading: false,
- form: {},
searchTerm: '',
};
},
computed: {
...mapState('monitoringDashboard', ['allDashboards']),
...mapGetters('monitoringDashboard', ['selectedDashboard']),
- isSystemDashboard() {
- return this.selectedDashboard?.system_dashboard;
+ isOutOfTheBoxDashboard() {
+ return this.selectedDashboard?.out_of_the_box_dashboard;
},
selectedDashboardText() {
return this.selectedDashboard?.display_name;
@@ -76,10 +68,6 @@ export default {
nonStarredDashboards() {
return this.filteredDashboards.filter(({ starred }) => !starred);
},
-
- okButtonText() {
- return this.loading ? s__('Metrics|Duplicating...') : s__('Metrics|Duplicate');
- },
},
methods: {
...mapActions('monitoringDashboard', ['duplicateSystemDashboard']),
@@ -89,37 +77,6 @@ export default {
selectDashboard(dashboard) {
this.$emit(events.selectDashboard, dashboard);
},
- ok(bvModalEvt) {
- // Prevent modal from hiding in case submit fails
- bvModalEvt.preventDefault();
-
- this.loading = true;
- this.alert = null;
- this.duplicateSystemDashboard(this.form)
- .then(createdDashboard => {
- this.loading = false;
- this.alert = null;
-
- // Trigger hide modal as submit is successful
- this.$refs.duplicateDashboardModal.hide();
-
- // Dashboards in the default branch become available immediately.
- // Not so in other branches, so we refresh the current dashboard
- const dashboard =
- this.form.branch === this.defaultBranch ? createdDashboard : this.selectedDashboard;
- this.$emit(events.selectDashboard, dashboard);
- })
- .catch(error => {
- this.loading = false;
- this.alert = error;
- });
- },
- hide() {
- this.alert = null;
- },
- formChange(form) {
- this.form = form;
- },
},
};
</script>
@@ -178,32 +135,14 @@ export default {
{{ __('No matching results') }}
</div>
- <template v-if="isSystemDashboard">
+ <!--
+ 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-modal
- ref="duplicateDashboardModal"
- modal-id="duplicateDashboardModal"
- :title="s__('Metrics|Duplicate dashboard')"
- ok-variant="success"
- @ok="ok"
- @hide="hide"
- >
- <gl-alert v-if="alert" class="mb-3" variant="danger" @dismiss="alert = null">
- {{ alert }}
- </gl-alert>
- <duplicate-dashboard-form
- :dashboard="selectedDashboard"
- :default-branch="defaultBranch"
- @change="formChange"
- />
- <template #modal-ok>
- <gl-loading-icon v-if="loading" inline color="light" />
- {{ okButtonText }}
- </template>
- </gl-modal>
-
- <gl-dropdown-item ref="duplicateDashboardItem" v-gl-modal="'duplicateDashboardModal'">
+ <gl-dropdown-item v-gl-modal="modalId" data-testid="duplicateDashboardItem">
{{ s__('Metrics|Duplicate dashboard') }}
</gl-dropdown-item>
</template>
diff --git a/app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue b/app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue
new file mode 100644
index 00000000000..e64afc01fd9
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue
@@ -0,0 +1,95 @@
+<script>
+import { mapActions, mapGetters } from 'vuex';
+import { GlAlert, GlLoadingIcon, GlModal } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import DuplicateDashboardForm from './duplicate_dashboard_form.vue';
+
+const events = {
+ dashboardDuplicated: 'dashboardDuplicated',
+};
+
+export default {
+ components: { GlAlert, GlLoadingIcon, GlModal, DuplicateDashboardForm },
+ props: {
+ defaultBranch: {
+ type: String,
+ required: true,
+ },
+ modalId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ alert: null,
+ loading: false,
+ form: {},
+ };
+ },
+ computed: {
+ ...mapGetters('monitoringDashboard', ['selectedDashboard']),
+ okButtonText() {
+ return this.loading ? s__('Metrics|Duplicating...') : s__('Metrics|Duplicate');
+ },
+ },
+ methods: {
+ ...mapActions('monitoringDashboard', ['duplicateSystemDashboard']),
+ ok(bvModalEvt) {
+ // Prevent modal from hiding in case submit fails
+ bvModalEvt.preventDefault();
+
+ this.loading = true;
+ this.alert = null;
+ this.duplicateSystemDashboard(this.form)
+ .then(createdDashboard => {
+ this.loading = false;
+ this.alert = null;
+
+ // Trigger hide modal as submit is successful
+ this.$refs.duplicateDashboardModal.hide();
+
+ // Dashboards in the default branch become available immediately.
+ // Not so in other branches, so we refresh the current dashboard
+ const dashboard =
+ this.form.branch === this.defaultBranch ? createdDashboard : this.selectedDashboard;
+ this.$emit(events.dashboardDuplicated, dashboard);
+ })
+ .catch(error => {
+ this.loading = false;
+ this.alert = error;
+ });
+ },
+ hide() {
+ this.alert = null;
+ },
+ formChange(form) {
+ this.form = form;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ ref="duplicateDashboardModal"
+ :modal-id="modalId"
+ :title="s__('Metrics|Duplicate dashboard')"
+ ok-variant="success"
+ @ok="ok"
+ @hide="hide"
+ >
+ <gl-alert v-if="alert" class="mb-3" variant="danger" @dismiss="alert = null">
+ {{ alert }}
+ </gl-alert>
+ <duplicate-dashboard-form
+ :dashboard="selectedDashboard"
+ :default-branch="defaultBranch"
+ @change="formChange"
+ />
+ <template #modal-ok>
+ <gl-loading-icon v-if="loading" inline color="light" />
+ {{ okButtonText }}
+ </template>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue
index d3157b731b2..5e7c9b5d906 100644
--- a/app/assets/javascripts/monitoring/components/empty_state.vue
+++ b/app/assets/javascripts/monitoring/components/empty_state.vue
@@ -1,12 +1,19 @@
<script>
-import { GlEmptyState } from '@gitlab/ui';
+import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
import { __ } from '~/locale';
+import { dashboardEmptyStates } from '../constants';
export default {
components: {
+ GlLoadingIcon,
GlEmptyState,
},
props: {
+ selectedState: {
+ type: String,
+ required: true,
+ validator: state => Object.values(dashboardEmptyStates).includes(state),
+ },
documentationPath: {
type: String,
required: true,
@@ -21,10 +28,6 @@ export default {
required: false,
default: '',
},
- selectedState: {
- type: String,
- required: true,
- },
emptyGettingStartedSvgPath: {
type: String,
required: true,
@@ -53,52 +56,49 @@ export default {
},
data() {
return {
+ /**
+ * Possible empty states.
+ * Keys in each state must match GlEmptyState props
+ */
states: {
- gettingStarted: {
- svgUrl: this.emptyGettingStartedSvgPath,
+ [dashboardEmptyStates.GETTING_STARTED]: {
+ svgPath: this.emptyGettingStartedSvgPath,
title: __('Get started with performance monitoring'),
description: __(`Stay updated about the performance and health
of your environment by configuring Prometheus to monitor your deployments.`),
- buttonText: __('Install on clusters'),
- buttonPath: this.clustersPath,
+ primaryButtonText: __('Install on clusters'),
+ primaryButtonLink: this.clustersPath,
secondaryButtonText: __('Configure existing installation'),
- secondaryButtonPath: this.settingsPath,
+ secondaryButtonLink: this.settingsPath,
},
- loading: {
- svgUrl: this.emptyLoadingSvgPath,
- title: __('Waiting for performance data'),
- description: __(`Creating graphs uses the data from the Prometheus server.
- If this takes a long time, ensure that data is available.`),
- buttonText: __('View documentation'),
- buttonPath: this.documentationPath,
- secondaryButtonText: '',
- secondaryButtonPath: '',
- },
- noData: {
- svgUrl: this.emptyNoDataSvgPath,
+ [dashboardEmptyStates.NO_DATA]: {
+ svgPath: this.emptyNoDataSvgPath,
title: __('No data found'),
description: __(`You are connected to the Prometheus server, but there is currently
no data to display.`),
- buttonText: __('Configure Prometheus'),
- buttonPath: this.settingsPath,
+ primaryButtonText: __('Configure Prometheus'),
+ primaryButtonLink: this.settingsPath,
secondaryButtonText: '',
- secondaryButtonPath: '',
+ secondaryButtonLink: '',
},
- unableToConnect: {
- svgUrl: this.emptyUnableToConnectSvgPath,
+ [dashboardEmptyStates.UNABLE_TO_CONNECT]: {
+ svgPath: this.emptyUnableToConnectSvgPath,
title: __('Unable to connect to Prometheus server'),
description: __(
'Ensure connectivity is available from the GitLab server to the Prometheus server',
),
- buttonText: __('View documentation'),
- buttonPath: this.documentationPath,
+ primaryButtonText: __('View documentation'),
+ primaryButtonLink: this.documentationPath,
secondaryButtonText: __('Configure Prometheus'),
- secondaryButtonPath: this.settingsPath,
+ secondaryButtonLink: this.settingsPath,
},
},
};
},
computed: {
+ isLoading() {
+ return this.selectedState === dashboardEmptyStates.LOADING;
+ },
currentState() {
return this.states[this.selectedState];
},
@@ -107,14 +107,8 @@ export default {
</script>
<template>
- <gl-empty-state
- :title="currentState.title"
- :description="currentState.description"
- :primary-button-text="currentState.buttonText"
- :primary-button-link="currentState.buttonPath"
- :secondary-button-text="currentState.secondaryButtonText"
- :secondary-button-link="currentState.secondaryButtonPath"
- :svg-path="currentState.svgUrl"
- :compact="compact"
- />
+ <div>
+ <gl-loading-icon v-if="isLoading" size="xl" class="gl-my-9" />
+ <gl-empty-state v-if="currentState" v-bind="currentState" :compact="compact" />
+ </div>
</template>
diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue
index 08fcfa3bc56..ecb8ef4a0d0 100644
--- a/app/assets/javascripts/monitoring/components/graph_group.vue
+++ b/app/assets/javascripts/monitoring/components/graph_group.vue
@@ -1,9 +1,10 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
export default {
components: {
- Icon,
+ GlLoadingIcon,
+ GlIcon,
},
props: {
name: {
@@ -15,6 +16,11 @@ export default {
required: false,
default: true,
},
+ isLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
/**
* Initial value of collapse on mount.
*/
@@ -52,18 +58,21 @@ export default {
</script>
<template>
- <div v-if="showPanels" ref="graph-group" class="card prometheus-panel" tabindex="0">
+ <div v-if="showPanels" ref="graph-group" class="card prometheus-panel">
<div class="card-header d-flex align-items-center">
<h4 class="flex-grow-1">{{ name }}</h4>
+ <gl-loading-icon v-if="isLoading" name="loading" />
<a
data-testid="group-toggle-button"
+ :aria-label="__('Toggle collapse')"
+ :icon="caretIcon"
role="button"
- class="js-graph-group-toggle gl-text-gray-900"
+ class="js-graph-group-toggle gl-display-flex gl-ml-2 gl-text-gray-900"
tabindex="0"
@click="collapse"
@keyup.enter="collapse"
>
- <icon :size="16" :aria-label="__('Toggle collapse')" :name="caretIcon" />
+ <gl-icon :name="caretIcon" />
</a>
</div>
<div
diff --git a/app/assets/javascripts/monitoring/components/refresh_button.vue b/app/assets/javascripts/monitoring/components/refresh_button.vue
new file mode 100644
index 00000000000..5481806c3e0
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/refresh_button.vue
@@ -0,0 +1,163 @@
+<script>
+import { n__, __ } from '~/locale';
+import { mapActions } from 'vuex';
+
+import {
+ GlButtonGroup,
+ GlButton,
+ GlNewDropdown,
+ GlNewDropdownItem,
+ GlNewDropdownDivider,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+
+const makeInterval = (length = 0, unit = 's') => {
+ const shortLabel = `${length}${unit}`;
+ switch (unit) {
+ case 'd':
+ return {
+ interval: length * 24 * 60 * 60 * 1000,
+ shortLabel,
+ label: n__('%d day', '%d days', length),
+ };
+ case 'h':
+ return {
+ interval: length * 60 * 60 * 1000,
+ shortLabel,
+ label: n__('%d hour', '%d hours', length),
+ };
+ case 'm':
+ return {
+ interval: length * 60 * 1000,
+ shortLabel,
+ label: n__('%d minute', '%d minutes', length),
+ };
+ case 's':
+ default:
+ return {
+ interval: length * 1000,
+ shortLabel,
+ label: n__('%d second', '%d seconds', length),
+ };
+ }
+};
+
+export default {
+ components: {
+ GlButtonGroup,
+ GlButton,
+ GlNewDropdown,
+ GlNewDropdownItem,
+ GlNewDropdownDivider,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ data() {
+ return {
+ refreshInterval: null,
+ timeoutId: null,
+ };
+ },
+ computed: {
+ dropdownText() {
+ return this.refreshInterval?.shortLabel ?? __('Off');
+ },
+ },
+ watch: {
+ refreshInterval() {
+ if (this.refreshInterval !== null) {
+ this.startAutoRefresh();
+ } else {
+ this.stopAutoRefresh();
+ }
+ },
+ },
+ destroyed() {
+ this.stopAutoRefresh();
+ },
+ methods: {
+ ...mapActions('monitoringDashboard', ['fetchDashboardData']),
+
+ refresh() {
+ this.fetchDashboardData();
+ },
+ startAutoRefresh() {
+ const schedule = () => {
+ if (this.refreshInterval) {
+ this.timeoutId = setTimeout(this.startAutoRefresh, this.refreshInterval.interval);
+ }
+ };
+
+ this.stopAutoRefresh();
+ if (document.hidden) {
+ // Inactive tab? Skip fetch and schedule again
+ schedule();
+ } else {
+ // Active tab! Fetch data and then schedule when settled
+ // eslint-disable-next-line promise/catch-or-return
+ this.fetchDashboardData().finally(schedule);
+ }
+ },
+ stopAutoRefresh() {
+ clearTimeout(this.timeoutId);
+ this.timeoutId = null;
+ },
+
+ setRefreshInterval(option) {
+ this.refreshInterval = option;
+ },
+ removeRefreshInterval() {
+ this.refreshInterval = null;
+ },
+ isChecked(option) {
+ if (this.refreshInterval) {
+ return option.interval === this.refreshInterval.interval;
+ }
+ return false;
+ },
+ },
+
+ refreshIntervals: [
+ makeInterval(5),
+ makeInterval(10),
+ makeInterval(30),
+ makeInterval(5, 'm'),
+ makeInterval(30, 'm'),
+ makeInterval(1, 'h'),
+ makeInterval(2, 'h'),
+ makeInterval(12, 'h'),
+ makeInterval(1, 'd'),
+ ],
+};
+</script>
+
+<template>
+ <gl-button-group>
+ <gl-button
+ v-gl-tooltip
+ class="gl-flex-grow-1"
+ variant="default"
+ :title="s__('Metrics|Refresh dashboard')"
+ icon="retry"
+ @click="refresh"
+ />
+ <gl-new-dropdown v-gl-tooltip :title="s__('Metrics|Set refresh rate')" :text="dropdownText">
+ <gl-new-dropdown-item
+ :is-check-item="true"
+ :is-checked="refreshInterval === null"
+ @click="removeRefreshInterval()"
+ >{{ __('Off') }}</gl-new-dropdown-item
+ >
+ <gl-new-dropdown-divider />
+ <gl-new-dropdown-item
+ v-for="(option, i) in $options.refreshIntervals"
+ :key="i"
+ :is-check-item="true"
+ :is-checked="isChecked(option)"
+ @click="setRefreshInterval(option)"
+ >{{ option.label }}</gl-new-dropdown-item
+ >
+ </gl-new-dropdown>
+ </gl-button-group>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/variables/custom_variable.vue b/app/assets/javascripts/monitoring/components/variables/dropdown_field.vue
index 0ac7c0b80df..4e48292c48d 100644
--- a/app/assets/javascripts/monitoring/components/variables/custom_variable.vue
+++ b/app/assets/javascripts/monitoring/components/variables/dropdown_field.vue
@@ -22,29 +22,32 @@ export default {
default: '',
},
options: {
- type: Array,
+ type: Object,
required: true,
},
},
computed: {
- defaultText() {
- const selectedOpt = this.options.find(opt => opt.value === this.value);
+ text() {
+ const selectedOpt = this.options.values?.find(opt => opt.value === this.value);
return selectedOpt?.text || this.value;
},
},
methods: {
onUpdate(value) {
- this.$emit('onUpdate', this.name, value);
+ this.$emit('input', value);
},
},
};
</script>
<template>
<gl-form-group :label="label">
- <gl-dropdown toggle-class="dropdown-menu-toggle" :text="defaultText">
- <gl-dropdown-item v-for="(opt, key) in options" :key="key" @click="onUpdate(opt.value)">{{
- opt.text
- }}</gl-dropdown-item>
+ <gl-dropdown toggle-class="dropdown-menu-toggle" :text="text || s__('Metrics|Select a value')">
+ <gl-dropdown-item
+ v-for="val in options.values"
+ :key="val.value"
+ @click="onUpdate(val.value)"
+ >{{ val.text }}</gl-dropdown-item
+ >
</gl-dropdown>
</gl-form-group>
</template>
diff --git a/app/assets/javascripts/monitoring/components/variables/text_variable.vue b/app/assets/javascripts/monitoring/components/variables/text_field.vue
index ce0d19760e2..a0418806e5f 100644
--- a/app/assets/javascripts/monitoring/components/variables/text_variable.vue
+++ b/app/assets/javascripts/monitoring/components/variables/text_field.vue
@@ -22,7 +22,7 @@ export default {
},
methods: {
onUpdate(event) {
- this.$emit('onUpdate', this.name, event.target.value);
+ this.$emit('input', event.target.value);
},
},
};
diff --git a/app/assets/javascripts/monitoring/components/variables_section.vue b/app/assets/javascripts/monitoring/components/variables_section.vue
index 3d1d111d5b3..25d900b07ad 100644
--- a/app/assets/javascripts/monitoring/components/variables_section.vue
+++ b/app/assets/javascripts/monitoring/components/variables_section.vue
@@ -1,13 +1,14 @@
<script>
import { mapState, mapActions } from 'vuex';
-import CustomVariable from './variables/custom_variable.vue';
-import TextVariable from './variables/text_variable.vue';
+import DropdownField from './variables/dropdown_field.vue';
+import TextField from './variables/text_field.vue';
import { setCustomVariablesFromUrl } from '../utils';
+import { VARIABLE_TYPES } from '../constants';
export default {
components: {
- CustomVariable,
- TextVariable,
+ DropdownField,
+ TextField,
},
computed: {
...mapState('monitoringDashboard', ['variables']),
@@ -15,10 +16,9 @@ export default {
methods: {
...mapActions('monitoringDashboard', ['updateVariablesAndFetchData']),
refreshDashboard(variable, value) {
- if (this.variables[variable].value !== value) {
- const changedVariable = { key: variable, value };
+ if (variable.value !== value) {
+ this.updateVariablesAndFetchData({ name: variable.name, value });
// update the Vuex store
- this.updateVariablesAndFetchData(changedVariable);
// the below calls can ideally be moved out of the
// component and into the actions and let the
// mutation respond directly.
@@ -27,27 +27,26 @@ export default {
setCustomVariablesFromUrl(this.variables);
}
},
- variableComponent(type) {
- const types = {
- text: TextVariable,
- custom: CustomVariable,
- };
- return types[type] || TextVariable;
+ variableField(type) {
+ if (type === VARIABLE_TYPES.custom || type === VARIABLE_TYPES.metric_label_values) {
+ return DropdownField;
+ }
+ return TextField;
},
},
};
</script>
<template>
<div ref="variablesSection" class="d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 variables-section">
- <div v-for="(variable, key) in variables" :key="key" class="mb-1 pr-2 d-flex d-sm-block">
+ <div v-for="variable in variables" :key="variable.name" class="mb-1 pr-2 d-flex d-sm-block">
<component
- :is="variableComponent(variable.type)"
+ :is="variableField(variable.type)"
class="mb-0 flex-grow-1"
:label="variable.label"
:value="variable.value"
- :name="key"
+ :name="variable.name"
:options="variable.options"
- @onUpdate="refreshDashboard"
+ @input="refreshDashboard(variable, $event)"
/>
</div>
</div>