debian-mirror-gitlab/app/assets/javascripts/monitoring/components/dashboard_panel.vue

530 lines
16 KiB
Vue
Raw Normal View History

2019-09-30 21:07:59 +05:30
<script>
import { mapState } from 'vuex';
2020-10-24 23:57:45 +05:30
import { mapValues, pickBy } from 'lodash';
2019-10-12 21:52:04 +05:30
import {
2020-04-08 14:13:33 +05:30
GlResizeObserverDirective,
2020-04-22 19:07:51 +05:30
GlIcon,
2020-10-24 23:57:45 +05:30
GlLink,
2020-04-22 19:07:51 +05:30
GlLoadingIcon,
2020-11-24 15:15:51 +05:30
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
2019-10-12 21:52:04 +05:30
GlModal,
GlModalDirective,
2020-10-24 23:57:45 +05:30
GlSprintf,
2020-04-08 14:13:33 +05:30
GlTooltip,
2019-10-12 21:52:04 +05:30
GlTooltipDirective,
} from '@gitlab/ui';
2020-10-24 23:57:45 +05:30
import invalidUrl from '~/lib/utils/invalid_url';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { relativePathToAbsolute, getBaseURL, visitUrl, isSafeURL } from '~/lib/utils/url_utility';
2020-04-08 14:13:33 +05:30
import { __, n__ } from '~/locale';
2020-05-24 23:13:21 +05:30
import { panelTypes } from '../constants';
import MonitorEmptyChart from './charts/empty_chart.vue';
2019-12-04 20:38:33 +05:30
import MonitorTimeSeriesChart from './charts/time_series.vue';
2019-12-26 22:10:19 +05:30
import MonitorAnomalyChart from './charts/anomaly.vue';
2019-09-30 21:07:59 +05:30
import MonitorSingleStatChart from './charts/single_stat.vue';
2020-10-24 23:57:45 +05:30
import MonitorGaugeChart from './charts/gauge.vue';
2019-12-26 22:10:19 +05:30
import MonitorHeatmapChart from './charts/heatmap.vue';
2020-03-13 15:44:24 +05:30
import MonitorColumnChart from './charts/column.vue';
2020-04-22 19:07:51 +05:30
import MonitorBarChart from './charts/bar.vue';
2020-03-13 15:44:24 +05:30
import MonitorStackedColumnChart from './charts/stacked_column.vue';
2020-05-24 23:13:21 +05:30
2019-12-21 20:55:43 +05:30
import TrackEventDirective from '~/vue_shared/directives/track_event';
2020-05-24 23:13:21 +05:30
import AlertWidget from './alert_widget.vue';
2020-03-13 15:44:24 +05:30
import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
2020-10-24 23:57:45 +05:30
import { graphDataToCsv } from '../csv_export';
2019-09-30 21:07:59 +05:30
2020-04-22 19:07:51 +05:30
const events = {
timeRangeZoom: 'timerangezoom',
2020-05-24 23:13:21 +05:30
expand: 'expand',
2020-04-22 19:07:51 +05:30
};
2019-09-30 21:07:59 +05:30
export default {
components: {
2019-10-12 21:52:04 +05:30
MonitorEmptyChart,
2020-05-24 23:13:21 +05:30
AlertWidget,
2020-04-22 19:07:51 +05:30
GlIcon,
2020-10-24 23:57:45 +05:30
GlLink,
2020-04-22 19:07:51 +05:30
GlLoadingIcon,
2020-04-08 14:13:33 +05:30
GlTooltip,
2019-10-12 21:52:04 +05:30
GlDropdown,
GlDropdownItem,
2020-06-23 00:09:42 +05:30
GlDropdownDivider,
2019-10-12 21:52:04 +05:30
GlModal,
2020-10-24 23:57:45 +05:30
GlSprintf,
2019-10-12 21:52:04 +05:30
},
directives: {
2020-04-08 14:13:33 +05:30
GlResizeObserver: GlResizeObserverDirective,
2019-10-12 21:52:04 +05:30
GlModal: GlModalDirective,
GlTooltip: GlTooltipDirective,
2019-12-21 20:55:43 +05:30
TrackEvent: TrackEventDirective,
2019-09-30 21:07:59 +05:30
},
props: {
2019-10-12 21:52:04 +05:30
clipboardText: {
type: String,
2020-01-01 13:55:28 +05:30
required: false,
default: '',
2019-10-12 21:52:04 +05:30
},
2019-09-30 21:07:59 +05:30
graphData: {
type: Object,
2019-10-12 21:52:04 +05:30
required: false,
2020-05-24 23:13:21 +05:30
default: null,
2019-10-12 21:52:04 +05:30
},
2020-01-01 13:55:28 +05:30
groupId: {
type: String,
required: false,
2020-05-24 23:13:21 +05:30
default: 'dashboard-panel',
2020-01-01 13:55:28 +05:30
},
2020-04-22 19:07:51 +05:30
namespace: {
type: String,
required: false,
default: 'monitoringDashboard',
},
2020-05-24 23:13:21 +05:30
alertsEndpoint: {
type: String,
required: false,
default: null,
},
prometheusAlertsAvailable: {
type: Boolean,
required: false,
default: false,
},
settingsPath: {
type: String,
required: false,
default: null,
},
2019-09-30 21:07:59 +05:30
},
2020-03-13 15:44:24 +05:30
data() {
return {
2020-04-08 14:13:33 +05:30
showTitleTooltip: false,
2020-03-13 15:44:24 +05:30
zoomedTimeRange: null,
2020-05-24 23:13:21 +05:30
allAlerts: {},
expandBtnAvailable: Boolean(this.$listeners[events.expand]),
2020-03-13 15:44:24 +05:30
};
},
2019-09-30 21:07:59 +05:30
computed: {
2020-04-22 19:07:51 +05:30
// Use functions to support dynamic namespaces in mapXXX helpers. Pattern described
// in https://github.com/vuejs/vuex/issues/863#issuecomment-329510765
...mapState({
deploymentData(state) {
return state[this.namespace].deploymentData;
},
annotations(state) {
return state[this.namespace].annotations;
},
projectPath(state) {
return state[this.namespace].projectPath;
},
logsPath(state) {
return state[this.namespace].logsPath;
},
timeRange(state) {
return state[this.namespace].timeRange;
},
2020-06-23 00:09:42 +05:30
dashboardTimezone(state) {
return state[this.namespace].dashboardTimezone;
},
2020-05-24 23:13:21 +05:30
metricsSavedToDb(state, getters) {
return getters[`${this.namespace}/metricsSavedToDb`];
},
2020-06-23 00:09:42 +05:30
selectedDashboard(state, getters) {
return getters[`${this.namespace}/selectedDashboard`];
},
2020-04-22 19:07:51 +05:30
}),
2020-10-24 23:57:45 +05:30
fixedCurrentTimeRange() {
// convertToFixedRange throws an error if the time range
// is not properly set.
try {
return convertToFixedRange(this.timeRange);
} catch {
return {};
}
},
2020-04-08 14:13:33 +05:30
title() {
2020-05-24 23:13:21 +05:30
return this.graphData?.title || '';
2019-09-30 21:07:59 +05:30
},
2020-04-22 19:07:51 +05:30
graphDataHasResult() {
2020-07-28 23:09:34 +05:30
const metrics = this.graphData?.metrics || [];
return metrics.some(({ result }) => result?.length > 0);
2019-10-12 21:52:04 +05:30
},
2020-04-22 19:07:51 +05:30
graphDataIsLoading() {
2020-05-24 23:13:21 +05:30
const metrics = this.graphData?.metrics || [];
2020-04-22 19:07:51 +05:30
return metrics.some(({ loading }) => loading);
},
2020-03-13 15:44:24 +05:30
logsPathWithTimeRange() {
const timeRange = this.zoomedTimeRange || this.timeRange;
if (this.logsPath && this.logsPath !== invalidUrl && timeRange) {
return timeRangeToUrl(timeRange, this.logsPath);
}
return null;
},
2019-10-12 21:52:04 +05:30
csvText() {
2020-10-24 23:57:45 +05:30
if (this.graphData) {
return graphDataToCsv(this.graphData);
}
return null;
2019-10-12 21:52:04 +05:30
},
downloadCsv() {
const data = new Blob([this.csvText], { type: 'text/plain' });
return window.URL.createObjectURL(data);
},
2020-05-24 23:13:21 +05:30
/**
* A chart is "basic" if it doesn't support
* the same features as the TimeSeries based components
* such as "annotations".
*
* @returns Vue Component wrapping a basic visualization
*/
basicChartComponent() {
if (this.isPanelType(panelTypes.SINGLE_STAT)) {
return MonitorSingleStatChart;
}
2020-10-24 23:57:45 +05:30
if (this.isPanelType(panelTypes.GAUGE_CHART)) {
return MonitorGaugeChart;
}
2020-05-24 23:13:21 +05:30
if (this.isPanelType(panelTypes.HEATMAP)) {
return MonitorHeatmapChart;
}
if (this.isPanelType(panelTypes.BAR)) {
return MonitorBarChart;
}
if (this.isPanelType(panelTypes.COLUMN)) {
return MonitorColumnChart;
}
if (this.isPanelType(panelTypes.STACKED_COLUMN)) {
return MonitorStackedColumnChart;
}
if (this.isPanelType(panelTypes.ANOMALY_CHART)) {
return MonitorAnomalyChart;
}
return null;
},
/**
* In monitoring, Time Series charts typically support
* a larger feature set like "annotations", "deployment
* data", alert "thresholds" and "datazoom".
*
* This is intentional as Time Series are more frequently
* used.
*
* @returns Vue Component wrapping a time series visualization,
* Area Charts are rendered by default.
*/
timeSeriesChartComponent() {
if (this.isPanelType(panelTypes.ANOMALY_CHART)) {
2019-12-26 22:10:19 +05:30
return MonitorAnomalyChart;
}
return MonitorTimeSeriesChart;
},
2020-04-08 14:13:33 +05:30
isContextualMenuShown() {
2020-07-28 23:09:34 +05:30
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) ||
2020-10-24 23:57:45 +05:30
this.isPanelType(panelTypes.SINGLE_STAT) ||
this.isPanelType(panelTypes.GAUGE_CHART)
2020-07-28 23:09:34 +05:30
);
2020-04-08 14:13:33 +05:30
},
editCustomMetricLink() {
2020-05-24 23:13:21 +05:30
if (this.graphData.metrics.length > 1) {
return this.settingsPath;
}
2020-04-08 14:13:33 +05:30
return this.graphData?.metrics[0].edit_path;
},
editCustomMetricLinkText() {
return n__('Metrics|Edit metric', 'Metrics|Edit metrics', this.graphData.metrics.length);
},
2020-05-24 23:13:21 +05:30
hasMetricsInDb() {
const { metrics = [] } = this.graphData;
return metrics.some(({ metricId }) => this.metricsSavedToDb.includes(metricId));
},
alertWidgetAvailable() {
2020-07-28 23:09:34 +05:30
const supportsAlerts =
this.isPanelType(panelTypes.AREA_CHART) || this.isPanelType(panelTypes.LINE_CHART);
2020-05-24 23:13:21 +05:30
return (
2020-07-28 23:09:34 +05:30
supportsAlerts &&
2020-05-24 23:13:21 +05:30
this.prometheusAlertsAvailable &&
this.alertsEndpoint &&
this.graphData &&
this.hasMetricsInDb
);
},
2020-07-28 23:09:34 +05:30
alertModalId() {
return `alert-modal-${this.graphData.id}`;
},
2020-04-08 14:13:33 +05:30
},
mounted() {
this.refreshTitleTooltip();
2019-09-30 21:07:59 +05:30
},
methods: {
getGraphAlerts(queries) {
if (!this.allAlerts) return {};
const metricIdsForChart = queries.map(q => q.metricId);
2020-03-13 15:44:24 +05:30
return pickBy(this.allAlerts, alert => metricIdsForChart.includes(alert.metricId));
2019-09-30 21:07:59 +05:30
},
getGraphAlertValues(queries) {
return Object.values(this.getGraphAlerts(queries));
},
isPanelType(type) {
2020-05-24 23:13:21 +05:30
return this.graphData?.type === type;
2019-09-30 21:07:59 +05:30
},
2019-10-12 21:52:04 +05:30
showToast() {
2019-12-21 20:55:43 +05:30
this.$toast.show(__('Link copied'));
2019-10-12 21:52:04 +05:30
},
2020-04-08 14:13:33 +05:30
refreshTitleTooltip() {
const { graphTitle } = this.$refs;
this.showTitleTooltip =
Boolean(graphTitle) && graphTitle.scrollWidth > graphTitle.offsetWidth;
},
2019-12-21 20:55:43 +05:30
downloadCSVOptions,
generateLinkToChartOptions,
2020-03-13 15:44:24 +05:30
2020-04-08 14:13:33 +05:30
onResize() {
this.refreshTitleTooltip();
},
2020-03-13 15:44:24 +05:30
onDatazoom({ start, end }) {
this.zoomedTimeRange = { start, end };
2020-04-22 19:07:51 +05:30
this.$emit(events.timeRangeZoom, { start, end });
2020-03-13 15:44:24 +05:30
},
2020-05-24 23:13:21 +05:30
onExpand() {
this.$emit(events.expand);
},
2020-07-28 23:09:34 +05:30
onExpandFromKeyboardShortcut() {
if (this.isContextualMenuShown) {
this.onExpand();
}
},
2020-05-24 23:13:21 +05:30
setAlerts(alertPath, alertAttributes) {
if (alertAttributes) {
this.$set(this.allAlerts, alertPath, alertAttributes);
} else {
this.$delete(this.allAlerts, alertPath);
}
},
2020-06-23 00:09:42 +05:30
safeUrl(url) {
return isSafeURL(url) ? url : '#';
},
2020-07-28 23:09:34 +05:30
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();
}
},
2020-10-24 23:57:45 +05:30
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);
},
2019-09-30 21:07:59 +05:30
},
2020-05-24 23:13:21 +05:30
panelTypes,
2019-09-30 21:07:59 +05:30
};
</script>
<template>
2020-04-08 14:13:33 +05:30
<div v-gl-resize-observer="onResize" class="prometheus-graph">
2020-07-28 23:09:34 +05:30
<div class="d-flex align-items-center">
2020-05-24 23:13:21 +05:30
<slot name="topLeft"></slot>
2020-04-08 14:13:33 +05:30
<h5
ref="graphTitle"
2020-06-23 00:09:42 +05:30
class="prometheus-graph-title gl-font-lg font-weight-bold text-truncate gl-mr-3"
2019-10-12 21:52:04 +05:30
>
2020-04-08 14:13:33 +05:30
{{ title }}
</h5>
<gl-tooltip :target="() => $refs.graphTitle" :disabled="!showTitleTooltip">
{{ title }}
</gl-tooltip>
2020-04-22 19:07:51 +05:30
<alert-widget
v-if="isContextualMenuShown && alertWidgetAvailable"
class="mx-1"
2020-07-28 23:09:34 +05:30
:modal-id="alertModalId"
2020-04-22 19:07:51 +05:30
:alerts-endpoint="alertsEndpoint"
:relevant-queries="graphData.metrics"
:alerts-to-manage="getGraphAlerts(graphData.metrics)"
@setAlerts="setAlerts"
/>
<div class="flex-grow-1"></div>
<div v-if="graphDataIsLoading" class="mx-1 mt-1">
<gl-loading-icon />
</div>
2020-04-08 14:13:33 +05:30
<div
v-if="isContextualMenuShown"
2020-04-22 19:07:51 +05:30
ref="contextualMenu"
2020-04-08 14:13:33 +05:30
data-qa-selector="prometheus_graph_widgets"
>
2020-07-28 23:09:34 +05:30
<div data-testid="dropdown-wrapper" class="d-flex align-items-center">
2020-10-24 23:57:45 +05:30
<!--
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
-->
2020-04-08 14:13:33 +05:30
<gl-dropdown
v-gl-tooltip
2020-10-24 23:57:45 +05:30
toggle-class="gl-px-3!"
no-caret
2020-04-08 14:13:33 +05:30
data-qa-selector="prometheus_widgets_dropdown"
right
:title="__('More actions')"
>
2020-10-24 23:57:45 +05:30
<template #button-content>
<gl-icon class="gl-mr-0!" name="ellipsis_v" />
2020-04-08 14:13:33 +05:30
</template>
2020-05-24 23:13:21 +05:30
<gl-dropdown-item
v-if="expandBtnAvailable"
ref="expandBtn"
:href="clipboardText"
@click.prevent="onExpand"
>
{{ s__('Metrics|Expand panel') }}
</gl-dropdown-item>
2020-04-08 14:13:33 +05:30
<gl-dropdown-item
v-if="editCustomMetricLink"
ref="editMetricLink"
:href="editCustomMetricLink"
>
{{ editCustomMetricLinkText }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="logsPathWithTimeRange"
ref="viewLogsLink"
:href="logsPathWithTimeRange"
>
{{ s__('Metrics|View logs') }}
</gl-dropdown-item>
2020-03-13 15:44:24 +05:30
2020-04-08 14:13:33 +05:30
<gl-dropdown-item
v-if="csvText"
ref="downloadCsvLink"
v-track-event="downloadCSVOptions(title)"
:href="downloadCsv"
download="chart_metrics.csv"
>
{{ __('Download CSV') }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="clipboardText"
ref="copyChartLink"
v-track-event="generateLinkToChartOptions(clipboardText)"
:data-clipboard-text="clipboardText"
2020-05-24 23:13:21 +05:30
data-qa-selector="generate_chart_link_menu_item"
2020-04-08 14:13:33 +05:30
@click="showToast(clipboardText)"
>
2020-04-22 19:07:51 +05:30
{{ __('Copy link to chart') }}
2020-04-08 14:13:33 +05:30
</gl-dropdown-item>
<gl-dropdown-item
v-if="alertWidgetAvailable"
2020-07-28 23:09:34 +05:30
v-gl-modal="alertModalId"
2020-04-08 14:13:33 +05:30
data-qa-selector="alert_widget_menu_item"
>
{{ __('Alerts') }}
</gl-dropdown-item>
2020-10-24 23:57:45 +05:30
<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>
2020-06-23 00:09:42 +05:30
2020-07-28 23:09:34 +05:30
<template v-if="graphData.links && graphData.links.length">
2020-06-23 00:09:42 +05:30
<gl-dropdown-divider />
<gl-dropdown-item
v-for="(link, index) in graphData.links"
:key="index"
:href="safeUrl(link.url)"
class="text-break"
>{{ link.title }}</gl-dropdown-item
>
</template>
<template v-if="selectedDashboard && selectedDashboard.can_edit">
<gl-dropdown-divider />
<gl-dropdown-item ref="manageLinksItem" :href="selectedDashboard.project_blob_path">{{
s__('Metrics|Manage chart links')
}}</gl-dropdown-item>
</template>
2020-04-08 14:13:33 +05:30
</gl-dropdown>
</div>
</div>
2019-10-12 21:52:04 +05:30
</div>
2020-04-08 14:13:33 +05:30
2020-05-24 23:13:21 +05:30
<monitor-empty-chart v-if="!graphDataHasResult" />
<component
:is="basicChartComponent"
v-else-if="basicChartComponent"
2020-04-08 14:13:33 +05:30
:graph-data="graphData"
2020-06-23 00:09:42 +05:30
:timezone="dashboardTimezone"
2020-05-24 23:13:21 +05:30
v-bind="$attrs"
v-on="$listeners"
2020-04-08 14:13:33 +05:30
/>
<component
2020-05-24 23:13:21 +05:30
:is="timeSeriesChartComponent"
v-else
ref="timeSeriesChart"
2020-04-08 14:13:33 +05:30
:graph-data="graphData"
:deployment-data="deploymentData"
2020-04-22 19:07:51 +05:30
:annotations="annotations"
2020-04-08 14:13:33 +05:30
:project-path="projectPath"
:thresholds="getGraphAlertValues(graphData.metrics)"
:group-id="groupId"
2020-06-23 00:09:42 +05:30
:timezone="dashboardTimezone"
2020-10-24 23:57:45 +05:30
:time-range="fixedCurrentTimeRange"
2020-05-24 23:13:21 +05:30
v-bind="$attrs"
v-on="$listeners"
2020-04-08 14:13:33 +05:30
@datazoom="onDatazoom"
/>
</div>
2019-09-30 21:07:59 +05:30
</template>