mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
				
	
	
		
			466 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
			
		
		
	
	
			466 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
| import { uniqueId } from 'lodash';
 | |
| 
 | |
| import { DataFrameDTO, DataFrameJSON } from '@grafana/data';
 | |
| import { config, logMeasurement, reportInteraction } from '@grafana/runtime';
 | |
| import {
 | |
|   VizPanel,
 | |
|   SceneTimePicker,
 | |
|   SceneGridLayout,
 | |
|   SceneGridRow,
 | |
|   SceneTimeRange,
 | |
|   SceneVariableSet,
 | |
|   SceneRefreshPicker,
 | |
|   SceneObject,
 | |
|   VizPanelMenu,
 | |
|   behaviors,
 | |
|   VizPanelState,
 | |
|   SceneGridItemLike,
 | |
|   SceneDataLayerProvider,
 | |
|   UserActionEvent,
 | |
|   SceneInteractionProfileEvent,
 | |
|   SceneObjectState,
 | |
| } from '@grafana/scenes';
 | |
| import { isWeekStart } from '@grafana/ui';
 | |
| import { contextSrv } from 'app/core/core';
 | |
| import { K8S_V1_DASHBOARD_API_CONFIG } from 'app/features/dashboard/api/v1';
 | |
| import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
 | |
| import { PanelModel } from 'app/features/dashboard/state/PanelModel';
 | |
| import { DashboardDTO, DashboardDataDTO } from 'app/types';
 | |
| 
 | |
| import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior';
 | |
| import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer';
 | |
| import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
 | |
| import { DashboardControls } from '../scene/DashboardControls';
 | |
| import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
 | |
| import { registerDashboardMacro } from '../scene/DashboardMacro';
 | |
| import { DashboardReloadBehavior } from '../scene/DashboardReloadBehavior';
 | |
| import { DashboardScene } from '../scene/DashboardScene';
 | |
| import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
 | |
| import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
 | |
| import { panelLinksBehavior, panelMenuBehavior } from '../scene/PanelMenuBehavior';
 | |
| import { PanelNotices } from '../scene/PanelNotices';
 | |
| import { PanelTimeRange } from '../scene/PanelTimeRange';
 | |
| import { DashboardGridItem, RepeatDirection } from '../scene/layout-default/DashboardGridItem';
 | |
| import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
 | |
| import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior';
 | |
| import { RowActions } from '../scene/layout-default/row-actions/RowActions';
 | |
| import { setDashboardPanelContext } from '../scene/setDashboardPanelContext';
 | |
| import { createPanelDataProvider } from '../utils/createPanelDataProvider';
 | |
| import { preserveDashboardSceneStateInLocalStorage } from '../utils/dashboardSessionState';
 | |
| import { DashboardInteractions } from '../utils/interactions';
 | |
| import { getVizPanelKeyForPanelId } from '../utils/utils';
 | |
| import { createVariablesForDashboard, createVariablesForSnapshot } from '../utils/variables';
 | |
| 
 | |
| import { getAngularPanelMigrationHandler } from './angularMigration';
 | |
| import { GRAFANA_DATASOURCE_REF } from './const';
 | |
| 
 | |
| export interface DashboardLoaderState {
 | |
|   dashboard?: DashboardScene;
 | |
|   isLoading?: boolean;
 | |
|   loadError?: string;
 | |
| }
 | |
| 
 | |
| export interface SaveModelToSceneOptions {
 | |
|   isEmbedded?: boolean;
 | |
| }
 | |
| 
 | |
| export function transformSaveModelToScene(rsp: DashboardDTO): DashboardScene {
 | |
|   // Just to have migrations run
 | |
|   const oldModel = new DashboardModel(rsp.dashboard, rsp.meta);
 | |
| 
 | |
|   const scene = createDashboardSceneFromDashboardModel(oldModel, rsp.dashboard);
 | |
|   // TODO: refactor createDashboardSceneFromDashboardModel to work on Dashboard schema model
 | |
| 
 | |
|   const apiVersion = config.featureToggles.kubernetesDashboards
 | |
|     ? `${K8S_V1_DASHBOARD_API_CONFIG.group}/${K8S_V1_DASHBOARD_API_CONFIG.version}`
 | |
|     : undefined;
 | |
| 
 | |
|   scene.setInitialSaveModel(rsp.dashboard, rsp.meta, apiVersion);
 | |
| 
 | |
|   return scene;
 | |
| }
 | |
| 
 | |
| export function createSceneObjectsForPanels(oldPanels: PanelModel[]): SceneGridItemLike[] {
 | |
|   // collects all panels and rows
 | |
|   const panels: SceneGridItemLike[] = [];
 | |
| 
 | |
|   // indicates expanded row that's currently processed
 | |
|   let currentRow: PanelModel | null = null;
 | |
|   // collects panels in the currently processed, expanded row
 | |
|   let currentRowPanels: SceneGridItemLike[] = [];
 | |
| 
 | |
|   for (const panel of oldPanels) {
 | |
|     if (panel.type === 'row') {
 | |
|       if (!currentRow) {
 | |
|         if (Boolean(panel.collapsed)) {
 | |
|           // collapsed rows contain their panels within the row model
 | |
|           panels.push(createRowFromPanelModel(panel, []));
 | |
|         } else {
 | |
|           // indicate new row to be processed
 | |
|           currentRow = panel;
 | |
|         }
 | |
|       } else {
 | |
|         // when a row has been processed, and we hit a next one for processing
 | |
|         if (currentRow.id !== panel.id) {
 | |
|           // commit previous row panels
 | |
|           panels.push(createRowFromPanelModel(currentRow, currentRowPanels));
 | |
| 
 | |
|           if (Boolean(panel.collapsed)) {
 | |
|             // collapsed rows contain their panels within the row model
 | |
|             panels.push(createRowFromPanelModel(panel, []));
 | |
|             currentRow = null;
 | |
|           } else {
 | |
|             // indicate new row to be processed
 | |
|             currentRow = panel;
 | |
|           }
 | |
| 
 | |
|           currentRowPanels = [];
 | |
|         }
 | |
|       }
 | |
|     } else {
 | |
|       // when rendering a snapshot created with the legacy Dashboards convert data to new snapshot format to be compatible with Scenes
 | |
|       if (panel.snapshotData) {
 | |
|         convertOldSnapshotToScenesSnapshot(panel);
 | |
|       }
 | |
| 
 | |
|       const panelObject = buildGridItemForPanel(panel);
 | |
| 
 | |
|       // when processing an expanded row, collect its panels
 | |
|       if (currentRow) {
 | |
|         currentRowPanels.push(panelObject);
 | |
|       } else {
 | |
|         panels.push(panelObject);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // commit a row if it's the last one
 | |
|   if (currentRow) {
 | |
|     panels.push(createRowFromPanelModel(currentRow, currentRowPanels));
 | |
|   }
 | |
| 
 | |
|   return panels;
 | |
| }
 | |
| 
 | |
| function createRowFromPanelModel(row: PanelModel, content: SceneGridItemLike[]): SceneGridItemLike {
 | |
|   if (Boolean(row.collapsed)) {
 | |
|     if (row.panels) {
 | |
|       content = row.panels.map((saveModel) => {
 | |
|         // Collapsed panels are not actually PanelModel instances
 | |
|         if (!(saveModel instanceof PanelModel)) {
 | |
|           saveModel = new PanelModel(saveModel);
 | |
|         }
 | |
| 
 | |
|         return buildGridItemForPanel(saveModel);
 | |
|       });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   let behaviors: SceneObject[] | undefined;
 | |
|   let children = content;
 | |
| 
 | |
|   if (row.repeat) {
 | |
|     // For repeated rows the children are stored in the behavior
 | |
|     behaviors = [new RowRepeaterBehavior({ variableName: row.repeat })];
 | |
|   }
 | |
| 
 | |
|   return new SceneGridRow({
 | |
|     key: getVizPanelKeyForPanelId(row.id),
 | |
|     title: row.title,
 | |
|     y: row.gridPos.y,
 | |
|     isCollapsed: row.collapsed,
 | |
|     children: children,
 | |
|     $behaviors: behaviors,
 | |
|     actions: new RowActions({}),
 | |
|   });
 | |
| }
 | |
| 
 | |
| export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel, dto: DashboardDataDTO) {
 | |
|   let variables: SceneVariableSet | undefined;
 | |
|   let annotationLayers: SceneDataLayerProvider[] = [];
 | |
|   let alertStatesLayer: AlertStatesDataLayer | undefined;
 | |
|   const uid = oldModel.uid;
 | |
|   const serializerVersion = config.featureToggles.dashboardNewLayouts ? 'v2' : 'v1';
 | |
| 
 | |
|   if (oldModel.templating?.list?.length) {
 | |
|     if (oldModel.meta.isSnapshot) {
 | |
|       variables = createVariablesForSnapshot(oldModel);
 | |
|     } else {
 | |
|       variables = createVariablesForDashboard(oldModel);
 | |
|     }
 | |
|   } else {
 | |
|     // Create empty variable set
 | |
|     variables = new SceneVariableSet({
 | |
|       variables: [],
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   if (oldModel.annotations?.list?.length && !oldModel.isSnapshot()) {
 | |
|     annotationLayers = oldModel.annotations?.list.map((a) => {
 | |
|       // Each annotation query is an individual data layer
 | |
|       return new DashboardAnnotationsDataLayer({
 | |
|         key: uniqueId('annotations-'),
 | |
|         query: a,
 | |
|         name: a.name,
 | |
|         isEnabled: Boolean(a.enable),
 | |
|         isHidden: Boolean(a.hide),
 | |
|       });
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   let shouldUseAlertStatesLayer = config.unifiedAlertingEnabled;
 | |
|   if (!shouldUseAlertStatesLayer) {
 | |
|     if (oldModel.panels.find((panel) => Boolean(panel.alert))) {
 | |
|       shouldUseAlertStatesLayer = true;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if (shouldUseAlertStatesLayer) {
 | |
|     alertStatesLayer = new AlertStatesDataLayer({
 | |
|       key: 'alert-states',
 | |
|       name: 'Alert States',
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   const scopeMeta =
 | |
|     config.featureToggles.scopeFilters && oldModel.scopeMeta
 | |
|       ? {
 | |
|           trait: oldModel.scopeMeta.trait,
 | |
|           groups: oldModel.scopeMeta.groups,
 | |
|         }
 | |
|       : undefined;
 | |
| 
 | |
|   const behaviorList: SceneObjectState['$behaviors'] = [
 | |
|     new behaviors.CursorSync({
 | |
|       sync: oldModel.graphTooltip,
 | |
|     }),
 | |
|     new behaviors.SceneQueryController({
 | |
|       enableProfiling:
 | |
|         config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === oldModel.uid) !== -1,
 | |
|       onProfileComplete: getDashboardInteractionCallback(oldModel.uid, oldModel.title),
 | |
|     }),
 | |
|     registerDashboardMacro,
 | |
|     registerPanelInteractionsReporter,
 | |
|     new behaviors.LiveNowTimer({ enabled: oldModel.liveNow }),
 | |
|     preserveDashboardSceneStateInLocalStorage,
 | |
|     addPanelsOnLoadBehavior,
 | |
|     new DashboardReloadBehavior({
 | |
|       reloadOnParamsChange: config.featureToggles.reloadDashboardsOnParamsChange && oldModel.meta.reloadOnParamsChange,
 | |
|       uid,
 | |
|       version: oldModel.version,
 | |
|     }),
 | |
|   ];
 | |
|   const dashboardScene = new DashboardScene(
 | |
|     {
 | |
|       uid,
 | |
|       description: oldModel.description,
 | |
|       editable: oldModel.editable,
 | |
|       preload: dto.preload ?? false,
 | |
|       id: oldModel.id,
 | |
|       isDirty: false,
 | |
|       links: oldModel.links || [],
 | |
|       meta: oldModel.meta,
 | |
|       tags: oldModel.tags || [],
 | |
|       title: oldModel.title,
 | |
|       version: oldModel.version,
 | |
|       scopeMeta,
 | |
|       body: new DefaultGridLayoutManager({
 | |
|         grid: new SceneGridLayout({
 | |
|           isLazy: !(dto.preload || contextSrv.user.authenticatedBy === 'render'),
 | |
|           children: createSceneObjectsForPanels(oldModel.panels),
 | |
|         }),
 | |
|       }),
 | |
|       $timeRange: new SceneTimeRange({
 | |
|         from: oldModel.time.from,
 | |
|         to: oldModel.time.to,
 | |
|         fiscalYearStartMonth: oldModel.fiscalYearStartMonth,
 | |
|         timeZone: oldModel.timezone,
 | |
|         weekStart: isWeekStart(oldModel.weekStart) ? oldModel.weekStart : undefined,
 | |
|         UNSAFE_nowDelay: oldModel.timepicker?.nowDelay,
 | |
|       }),
 | |
|       $variables: variables,
 | |
|       $behaviors: behaviorList,
 | |
|       $data: new DashboardDataLayerSet({ annotationLayers, alertStatesLayer }),
 | |
|       controls: new DashboardControls({
 | |
|         timePicker: new SceneTimePicker({
 | |
|           quickRanges: oldModel.timepicker.quick_ranges,
 | |
|         }),
 | |
|         refreshPicker: new SceneRefreshPicker({
 | |
|           refresh: oldModel.refresh,
 | |
|           intervals: oldModel.timepicker.refresh_intervals,
 | |
|           withText: true,
 | |
|         }),
 | |
|         hideTimeControls: oldModel.timepicker.hidden,
 | |
|       }),
 | |
|     },
 | |
|     serializerVersion
 | |
|   );
 | |
| 
 | |
|   return dashboardScene;
 | |
| }
 | |
| 
 | |
| export function buildGridItemForPanel(panel: PanelModel): DashboardGridItem {
 | |
|   const repeatOptions: Partial<{ variableName: string; repeatDirection: RepeatDirection }> = panel.repeat
 | |
|     ? {
 | |
|         variableName: panel.repeat,
 | |
|         repeatDirection: panel.repeatDirection === 'v' ? 'v' : 'h',
 | |
|       }
 | |
|     : {};
 | |
| 
 | |
|   const titleItems: SceneObject[] = [];
 | |
| 
 | |
|   titleItems.push(
 | |
|     new VizPanelLinks({
 | |
|       rawLinks: panel.links,
 | |
|       menu: new VizPanelLinksMenu({ $behaviors: [panelLinksBehavior] }),
 | |
|     })
 | |
|   );
 | |
| 
 | |
|   titleItems.push(new PanelNotices());
 | |
| 
 | |
|   const timeOverrideShown = (panel.timeFrom || panel.timeShift) && !panel.hideTimeOverride;
 | |
| 
 | |
|   const vizPanelState: VizPanelState = {
 | |
|     key: getVizPanelKeyForPanelId(panel.id),
 | |
|     title: panel.title?.substring(0, 5000),
 | |
|     description: panel.description,
 | |
|     pluginId: panel.type ?? 'timeseries',
 | |
|     options: panel.options ?? {},
 | |
|     fieldConfig: panel.fieldConfig,
 | |
|     pluginVersion: panel.pluginVersion,
 | |
|     seriesLimit: config.panelSeriesLimit,
 | |
|     displayMode: panel.transparent ? 'transparent' : undefined,
 | |
|     // To be replaced with it's own option persited option instead derived
 | |
|     hoverHeader: !panel.title && !timeOverrideShown,
 | |
|     hoverHeaderOffset: 0,
 | |
|     $data: createPanelDataProvider(panel),
 | |
|     titleItems,
 | |
|     $behaviors: [],
 | |
|     extendPanelContext: setDashboardPanelContext,
 | |
|     _UNSAFE_customMigrationHandler: getAngularPanelMigrationHandler(panel),
 | |
|   };
 | |
| 
 | |
|   if (panel.libraryPanel) {
 | |
|     vizPanelState.$behaviors!.push(
 | |
|       new LibraryPanelBehavior({ uid: panel.libraryPanel.uid, name: panel.libraryPanel.name })
 | |
|     );
 | |
|     vizPanelState.pluginId = LibraryPanelBehavior.LOADING_VIZ_PANEL_PLUGIN_ID;
 | |
|     vizPanelState.$data = undefined;
 | |
|   }
 | |
| 
 | |
|   if (!config.publicDashboardAccessToken) {
 | |
|     vizPanelState.menu = new VizPanelMenu({
 | |
|       $behaviors: [panelMenuBehavior],
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   if (panel.timeFrom || panel.timeShift) {
 | |
|     vizPanelState.$timeRange = new PanelTimeRange({
 | |
|       timeFrom: panel.timeFrom,
 | |
|       timeShift: panel.timeShift,
 | |
|       hideTimeOverride: panel.hideTimeOverride,
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   const body = new VizPanel(vizPanelState);
 | |
| 
 | |
|   return new DashboardGridItem({
 | |
|     key: `grid-item-${panel.id}`,
 | |
|     x: panel.gridPos.x,
 | |
|     y: panel.gridPos.y,
 | |
|     width: repeatOptions.repeatDirection === 'h' ? 24 : panel.gridPos.w,
 | |
|     height: panel.gridPos.h,
 | |
|     itemHeight: panel.gridPos.h,
 | |
|     body,
 | |
|     maxPerRow: panel.maxPerRow,
 | |
|     ...repeatOptions,
 | |
|   });
 | |
| }
 | |
| 
 | |
| export function registerPanelInteractionsReporter(scene: DashboardScene) {
 | |
|   // Subscriptions set with subscribeToEvent are automatically unsubscribed when the scene deactivated
 | |
|   scene.subscribeToEvent(UserActionEvent, (e) => {
 | |
|     const { interaction } = e.payload;
 | |
|     switch (interaction) {
 | |
|       case 'panel-status-message-clicked':
 | |
|         DashboardInteractions.panelStatusMessageClicked();
 | |
|         break;
 | |
|       case 'panel-cancel-query-clicked':
 | |
|         DashboardInteractions.panelCancelQueryClicked();
 | |
|         break;
 | |
|     }
 | |
|   });
 | |
| }
 | |
| 
 | |
| const convertSnapshotData = (snapshotData: DataFrameDTO[]): DataFrameJSON[] => {
 | |
|   return snapshotData.map((data) => {
 | |
|     return {
 | |
|       data: {
 | |
|         values: data.fields.map((field) => field.values).filter((values): values is unknown[] => values !== undefined),
 | |
|       },
 | |
|       schema: {
 | |
|         fields: data.fields.map((field) => ({
 | |
|           name: field.name,
 | |
|           type: field.type,
 | |
|           config: field.config,
 | |
|         })),
 | |
|       },
 | |
|     };
 | |
|   });
 | |
| };
 | |
| 
 | |
| // override panel datasource and targets with snapshot data using the Grafana datasource
 | |
| export const convertOldSnapshotToScenesSnapshot = (panel: PanelModel) => {
 | |
|   // only old snapshots created with old dashboards contains snapshotData
 | |
|   if (panel.snapshotData) {
 | |
|     panel.datasource = GRAFANA_DATASOURCE_REF;
 | |
|     panel.targets = [
 | |
|       {
 | |
|         refId: panel.snapshotData[0]?.refId ?? '',
 | |
|         datasource: panel.datasource,
 | |
|         queryType: 'snapshot',
 | |
|         // @ts-ignore
 | |
|         snapshot: convertSnapshotData(panel.snapshotData),
 | |
|       },
 | |
|     ];
 | |
|     panel.snapshotData = [];
 | |
|   }
 | |
| };
 | |
| 
 | |
| function getDashboardInteractionCallback(uid: string, title: string) {
 | |
|   return (e: SceneInteractionProfileEvent) => {
 | |
|     let interactionType = '';
 | |
| 
 | |
|     if (e.origin === 'SceneTimeRange') {
 | |
|       interactionType = 'time-range-change';
 | |
|     } else if (e.origin === 'SceneRefreshPicker') {
 | |
|       interactionType = 'refresh';
 | |
|     } else if (e.origin === 'DashboardScene') {
 | |
|       interactionType = 'view';
 | |
|     } else if (e.origin.indexOf('Variable') > -1) {
 | |
|       interactionType = 'variable-change';
 | |
|     }
 | |
|     reportInteraction('dashboard-render', {
 | |
|       interactionType,
 | |
|       duration: e.duration,
 | |
|       networkDuration: e.networkDuration,
 | |
|       totalJSHeapSize: e.totalJSHeapSize,
 | |
|       usedJSHeapSize: e.usedJSHeapSize,
 | |
|       jsHeapSizeLimit: e.jsHeapSizeLimit,
 | |
|     });
 | |
| 
 | |
|     logMeasurement(
 | |
|       `dashboard.${interactionType}`,
 | |
|       {
 | |
|         duration: e.duration,
 | |
|         networkDuration: e.networkDuration,
 | |
|         totalJSHeapSize: e.totalJSHeapSize,
 | |
|         usedJSHeapSize: e.usedJSHeapSize,
 | |
|         jsHeapSizeLimit: e.jsHeapSizeLimit,
 | |
|         timeSinceBoot: performance.measure('time_since_boot', 'frontend_boot_js_done_time_seconds').duration,
 | |
|       },
 | |
|       { dashboard: uid, title: title }
 | |
|     );
 | |
|   };
 | |
| }
 |