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/dashboard_panel.vue')
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel.vue393
1 files changed, 393 insertions, 0 deletions
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel.vue b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
new file mode 100644
index 00000000000..48825fda5c8
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
@@ -0,0 +1,393 @@
+<script>
+import { mapState } from 'vuex';
+import { pickBy } from 'lodash';
+import invalidUrl from '~/lib/utils/invalid_url';
+import {
+ GlResizeObserverDirective,
+ GlIcon,
+ GlLoadingIcon,
+ GlDropdown,
+ GlDropdownItem,
+ GlModal,
+ GlModalDirective,
+ GlTooltip,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import { __, n__ } from '~/locale';
+import { panelTypes } from '../constants';
+
+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 MonitorHeatmapChart from './charts/heatmap.vue';
+import MonitorColumnChart from './charts/column.vue';
+import MonitorBarChart from './charts/bar.vue';
+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';
+
+const events = {
+ timeRangeZoom: 'timerangezoom',
+ expand: 'expand',
+};
+
+export default {
+ components: {
+ MonitorEmptyChart,
+ AlertWidget,
+ GlIcon,
+ GlLoadingIcon,
+ GlTooltip,
+ GlDropdown,
+ GlDropdownItem,
+ GlModal,
+ },
+ directives: {
+ GlResizeObserver: GlResizeObserverDirective,
+ GlModal: GlModalDirective,
+ GlTooltip: GlTooltipDirective,
+ TrackEvent: TrackEventDirective,
+ },
+ props: {
+ clipboardText: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ graphData: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ groupId: {
+ type: String,
+ required: false,
+ default: 'dashboard-panel',
+ },
+ namespace: {
+ type: String,
+ required: false,
+ default: 'monitoringDashboard',
+ },
+ alertsEndpoint: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ prometheusAlertsAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ settingsPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ showTitleTooltip: false,
+ zoomedTimeRange: null,
+ allAlerts: {},
+ expandBtnAvailable: Boolean(this.$listeners[events.expand]),
+ };
+ },
+ computed: {
+ // 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;
+ },
+ metricsSavedToDb(state, getters) {
+ return getters[`${this.namespace}/metricsSavedToDb`];
+ },
+ }),
+ title() {
+ return this.graphData?.title || '';
+ },
+ graphDataHasResult() {
+ return this.graphData?.metrics?.[0]?.result?.length > 0;
+ },
+ graphDataIsLoading() {
+ const metrics = this.graphData?.metrics || [];
+ return metrics.some(({ loading }) => loading);
+ },
+ logsPathWithTimeRange() {
+ const timeRange = this.zoomedTimeRange || this.timeRange;
+
+ if (this.logsPath && this.logsPath !== invalidUrl && timeRange) {
+ return timeRangeToUrl(timeRange, this.logsPath);
+ }
+ 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);
+ },
+ downloadCsv() {
+ const data = new Blob([this.csvText], { type: 'text/plain' });
+ return window.URL.createObjectURL(data);
+ },
+
+ /**
+ * 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;
+ }
+ 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)) {
+ return MonitorAnomalyChart;
+ }
+ return MonitorTimeSeriesChart;
+ },
+ isContextualMenuShown() {
+ return Boolean(this.graphDataHasResult && !this.basicChartComponent);
+ },
+ editCustomMetricLink() {
+ if (this.graphData.metrics.length > 1) {
+ return this.settingsPath;
+ }
+ return this.graphData?.metrics[0].edit_path;
+ },
+ editCustomMetricLinkText() {
+ return n__('Metrics|Edit metric', 'Metrics|Edit metrics', this.graphData.metrics.length);
+ },
+ hasMetricsInDb() {
+ const { metrics = [] } = this.graphData;
+ return metrics.some(({ metricId }) => this.metricsSavedToDb.includes(metricId));
+ },
+ alertWidgetAvailable() {
+ return (
+ this.prometheusAlertsAvailable &&
+ this.alertsEndpoint &&
+ this.graphData &&
+ this.hasMetricsInDb
+ );
+ },
+ },
+ mounted() {
+ this.refreshTitleTooltip();
+ },
+ methods: {
+ getGraphAlerts(queries) {
+ if (!this.allAlerts) return {};
+ const metricIdsForChart = queries.map(q => q.metricId);
+ return pickBy(this.allAlerts, alert => metricIdsForChart.includes(alert.metricId));
+ },
+ getGraphAlertValues(queries) {
+ return Object.values(this.getGraphAlerts(queries));
+ },
+ isPanelType(type) {
+ return this.graphData?.type === type;
+ },
+ showToast() {
+ this.$toast.show(__('Link copied'));
+ },
+ refreshTitleTooltip() {
+ const { graphTitle } = this.$refs;
+ this.showTitleTooltip =
+ Boolean(graphTitle) && graphTitle.scrollWidth > graphTitle.offsetWidth;
+ },
+
+ downloadCSVOptions,
+ generateLinkToChartOptions,
+
+ onResize() {
+ this.refreshTitleTooltip();
+ },
+ onDatazoom({ start, end }) {
+ this.zoomedTimeRange = { start, end };
+ this.$emit(events.timeRangeZoom, { start, end });
+ },
+ onExpand() {
+ this.$emit(events.expand);
+ },
+ setAlerts(alertPath, alertAttributes) {
+ if (alertAttributes) {
+ this.$set(this.allAlerts, alertPath, alertAttributes);
+ } else {
+ this.$delete(this.allAlerts, alertPath);
+ }
+ },
+ },
+ panelTypes,
+};
+</script>
+<template>
+ <div v-gl-resize-observer="onResize" class="prometheus-graph">
+ <div class="d-flex align-items-center mr-3">
+ <slot name="topLeft"></slot>
+ <h5
+ ref="graphTitle"
+ class="prometheus-graph-title gl-font-lg font-weight-bold text-truncate append-right-8"
+ >
+ {{ title }}
+ </h5>
+ <gl-tooltip :target="() => $refs.graphTitle" :disabled="!showTitleTooltip">
+ {{ title }}
+ </gl-tooltip>
+ <alert-widget
+ v-if="isContextualMenuShown && alertWidgetAvailable"
+ class="mx-1"
+ :modal-id="`alert-modal-${graphData.id}`"
+ :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>
+ <div
+ v-if="isContextualMenuShown"
+ ref="contextualMenu"
+ data-qa-selector="prometheus_graph_widgets"
+ >
+ <div class="d-flex align-items-center">
+ <gl-dropdown
+ v-gl-tooltip
+ toggle-class="btn btn-transparent border-0"
+ data-qa-selector="prometheus_widgets_dropdown"
+ right
+ no-caret
+ :title="__('More actions')"
+ >
+ <template slot="button-content">
+ <gl-icon name="ellipsis_v" class="text-secondary" />
+ </template>
+ <gl-dropdown-item
+ v-if="expandBtnAvailable"
+ ref="expandBtn"
+ :href="clipboardText"
+ @click.prevent="onExpand"
+ >
+ {{ s__('Metrics|Expand panel') }}
+ </gl-dropdown-item>
+ <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>
+
+ <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"
+ data-qa-selector="generate_chart_link_menu_item"
+ @click="showToast(clipboardText)"
+ >
+ {{ __('Copy link to chart') }}
+ </gl-dropdown-item>
+ <gl-dropdown-item
+ v-if="alertWidgetAvailable"
+ v-gl-modal="`alert-modal-${graphData.id}`"
+ data-qa-selector="alert_widget_menu_item"
+ >
+ {{ __('Alerts') }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+ </div>
+ </div>
+
+ <monitor-empty-chart v-if="!graphDataHasResult" />
+ <component
+ :is="basicChartComponent"
+ v-else-if="basicChartComponent"
+ :graph-data="graphData"
+ v-bind="$attrs"
+ v-on="$listeners"
+ />
+ <component
+ :is="timeSeriesChartComponent"
+ v-else
+ ref="timeSeriesChart"
+ :graph-data="graphData"
+ :deployment-data="deploymentData"
+ :annotations="annotations"
+ :project-path="projectPath"
+ :thresholds="getGraphAlertValues(graphData.metrics)"
+ :group-id="groupId"
+ v-bind="$attrs"
+ v-on="$listeners"
+ @datazoom="onDatazoom"
+ />
+ </div>
+</template>