import './graph'; import './series_overrides_ctrl'; import './thresholds_form'; import './time_regions_form'; import './annotation_tooltip'; import './event_editor'; import { auto } from 'angular'; import { defaults, find, without } from 'lodash'; import { DataFrame, FieldConfigProperty, PanelEvents, PanelPlugin } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { MetricsPanelCtrl } from 'app/angular/panel/metrics_panel_ctrl'; import config from 'app/core/config'; import TimeSeries from 'app/core/time_series2'; import { ThresholdMapper } from 'app/features/alerting/state/ThresholdMapper'; import { changePanelPlugin } from 'app/features/panel/state/actions'; import { dispatch } from 'app/store/store'; import { appEvents } from '../../../core/core'; import { loadSnapshotData } from '../../../features/dashboard/utils/loadSnapshotData'; import { annotationsFromDataFrames } from '../../../features/query/state/DashboardQueryRunner/utils'; import { ZoomOutEvent } from '../../../types/events'; import { GraphContextMenuCtrl } from './GraphContextMenuCtrl'; import { graphPanelMigrationHandler } from './GraphMigrations'; import { axesEditorComponent } from './axes_editor'; import { DataProcessor } from './data_processor'; import template from './template'; import { DataWarning, GraphFieldConfig, GraphPanelOptions } from './types'; import { getDataTimeRange } from './utils'; export class GraphCtrl extends MetricsPanelCtrl { static template = template; renderError = false; hiddenSeries: any = {}; hiddenSeriesTainted = false; seriesList: TimeSeries[] = []; dataList: DataFrame[] = []; annotations: any = []; alertState: any; dataWarning?: DataWarning; colors: any = []; subTabIndex = 0; processor: DataProcessor; contextMenuCtrl: GraphContextMenuCtrl; panelDefaults: any = { // datasource name, null = default datasource datasource: null, // sets client side (flot) or native graphite png renderer (png) renderer: 'flot', yaxes: [ { label: null, show: true, logBase: 1, min: null, max: null, format: 'short', }, { label: null, show: true, logBase: 1, min: null, max: null, format: 'short', }, ], xaxis: { show: true, mode: 'time', name: null, values: [], buckets: null, }, yaxis: { align: false, alignLevel: null, }, // show/hide lines lines: true, // fill factor fill: 1, // fill gradient fillGradient: 0, // line width in pixels linewidth: 1, // show/hide dashed line dashes: false, // show/hide line hiddenSeries: false, // length of a dash dashLength: 10, // length of space between two dashes spaceLength: 10, // show hide points points: false, // point radius in pixels pointradius: 2, // show hide bars bars: false, // enable/disable stacking stack: false, // stack percentage mode percentage: false, // legend options legend: { show: true, // disable/enable legend values: false, // disable/enable legend values min: false, max: false, current: false, total: false, avg: false, }, // how null points should be handled nullPointMode: 'null', // staircase line mode steppedLine: false, // tooltip options tooltip: { value_type: 'individual', shared: true, sort: 0, }, // time overrides timeFrom: null, timeShift: null, // metric queries targets: [{}], // series color overrides aliasColors: {}, // other style overrides seriesOverrides: [], thresholds: [], timeRegions: [], options: { // show/hide alert threshold lines and fill alertThreshold: true, }, }; /** @ngInject */ constructor($scope: any, $injector: auto.IInjectorService) { super($scope, $injector); defaults(this.panel, this.panelDefaults); defaults(this.panel.tooltip, this.panelDefaults.tooltip); defaults(this.panel.legend, this.panelDefaults.legend); defaults(this.panel.xaxis, this.panelDefaults.xaxis); defaults(this.panel.options, this.panelDefaults.options); this.useDataFrames = true; this.processor = new DataProcessor(this.panel); this.contextMenuCtrl = new GraphContextMenuCtrl($scope); this.events.on(PanelEvents.render, this.onRender.bind(this)); this.events.on(PanelEvents.dataFramesReceived, this.onDataFramesReceived.bind(this)); this.events.on(PanelEvents.dataSnapshotLoad, this.onDataSnapshotLoad.bind(this)); this.events.on(PanelEvents.editModeInitialized, this.onInitEditMode.bind(this)); this.events.on(PanelEvents.initPanelActions, this.onInitPanelActions.bind(this)); // set axes format from field config const fieldConfigUnit = this.panel.fieldConfig.defaults.unit; if (fieldConfigUnit) { this.panel.yaxes[0].format = fieldConfigUnit; } } onInitEditMode() { this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html'); this.addEditorTab('Series overrides', 'public/app/plugins/panel/graph/tab_series_overrides.html'); this.addEditorTab('Axes', axesEditorComponent); this.addEditorTab('Legend', 'public/app/plugins/panel/graph/tab_legend.html'); this.addEditorTab('Thresholds', 'public/app/plugins/panel/graph/tab_thresholds.html'); this.addEditorTab('Time regions', 'public/app/plugins/panel/graph/tab_time_regions.html'); this.subTabIndex = 0; this.hiddenSeriesTainted = false; } onInitPanelActions(actions: any[]) { actions.push({ text: 'Toggle legend', click: 'ctrl.toggleLegend()', shortcut: 'p l' }); } zoomOut(evt: any) { appEvents.publish(new ZoomOutEvent({ scale: 2 })); } onDataSnapshotLoad(snapshotData: any) { const { series, annotations } = loadSnapshotData(this.panel, this.dashboard); this.panelData!.annotations = annotations; this.onDataFramesReceived(series); } onDataFramesReceived(data: DataFrame[]) { this.dataList = data; this.seriesList = this.processor.getSeriesList({ dataList: this.dataList, range: this.range, }); this.dataWarning = this.getDataWarning(); this.alertState = undefined; (this.seriesList as any).alertState = undefined; if (this.panelData!.alertState) { this.alertState = this.panelData!.alertState; (this.seriesList as any).alertState = this.alertState.state; } this.annotations = []; if (this.panelData!.annotations?.length) { this.annotations = annotationsFromDataFrames(this.panelData!.annotations); } this.loading = false; this.render(this.seriesList); } getDataWarning(): DataWarning | undefined { const datapointsCount = this.seriesList.reduce((prev, series) => { return prev + series.datapoints.length; }, 0); if (datapointsCount === 0) { if (this.dataList) { for (const frame of this.dataList) { if (frame.length && frame.fields?.length) { return { title: 'Unable to graph data', tip: 'Data exists, but is not timeseries', actionText: 'Switch to table view', action: () => { dispatch(changePanelPlugin({ panel: this.panel, pluginId: 'table' })); }, }; } } } return { title: 'No data', tip: 'No data returned from query', }; } // If any data is in range, do not return an error for (const series of this.seriesList) { if (!series.isOutsideRange) { return undefined; } } // All data is outside the time range const dataWarning: DataWarning = { title: 'Data outside time range', tip: 'Can be caused by timezone mismatch or missing time filter in query', }; const range = getDataTimeRange(this.dataList); if (range) { dataWarning.actionText = 'Zoom to data'; dataWarning.action = () => { locationService.partial({ from: range.from, to: range.to, }); }; } return dataWarning; } onRender() { if (!this.seriesList) { return; } ThresholdMapper.alertToGraphThresholds(this.panel); for (const series of this.seriesList) { series.applySeriesOverrides(this.panel.seriesOverrides); // Always use the configured field unit if (series.unit) { this.panel.yaxes[series.yaxis - 1].format = series.unit; } if (this.hiddenSeriesTainted === false && series.hiddenSeries === true) { this.hiddenSeries[series.alias] = true; } } } onColorChange = (series: any, color: string) => { series.setColor(config.theme.visualization.getColorByName(color)); this.panel.aliasColors[series.alias] = color; this.render(); }; onToggleSeries = (hiddenSeries: any) => { this.hiddenSeriesTainted = true; this.hiddenSeries = hiddenSeries; this.render(); }; onToggleSort = (sortBy: any, sortDesc: any) => { this.panel.legend.sort = sortBy; this.panel.legend.sortDesc = sortDesc; this.render(); }; onToggleAxis = (info: { alias: any; yaxis: any }) => { let override: any = find(this.panel.seriesOverrides, { alias: info.alias }); if (!override) { override = { alias: info.alias }; this.panel.seriesOverrides.push(override); } override.yaxis = info.yaxis; this.render(); }; addSeriesOverride(override: any) { this.panel.seriesOverrides.push(override || {}); } removeSeriesOverride(override: any) { this.panel.seriesOverrides = without(this.panel.seriesOverrides, override); this.render(); } toggleLegend() { this.panel.legend.show = !this.panel.legend.show; this.render(); } legendValuesOptionChanged() { const legend = this.panel.legend; legend.values = legend.min || legend.max || legend.avg || legend.current || legend.total; this.render(); } onContextMenuClose = () => { this.contextMenuCtrl.toggleMenu(); }; getTimeZone = () => this.dashboard.getTimezone(); getDataFrameByRefId = (refId: string) => { return this.dataList.filter((dataFrame) => dataFrame.refId === refId)[0]; }; migrateToReact() { this.onPluginTypeChange(config.panels['timeseries']); } } // Use new react style configuration export const plugin = new PanelPlugin(null) .useFieldConfig({ disableStandardOptions: [ FieldConfigProperty.NoValue, FieldConfigProperty.Thresholds, FieldConfigProperty.Max, FieldConfigProperty.Min, FieldConfigProperty.Decimals, FieldConfigProperty.Color, FieldConfigProperty.Mappings, ], }) .setDataSupport({ annotations: true, alertStates: true }) .setMigrationHandler(graphPanelMigrationHandler); // Use the angular ctrt rather than a react one plugin.angularPanelCtrl = GraphCtrl;