diff --git a/.betterer.results b/.betterer.results index 84fde18b403..3bffa9c22c8 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2438,6 +2438,20 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx:5381": [ [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"] ], + "public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"], + [0, 0, 0, "Unexpected any. Specify a different type.", "4"], + [0, 0, 0, "Unexpected any. Specify a different type.", "5"], + [0, 0, 0, "Unexpected any. Specify a different type.", "6"] + ], + "public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Do not use any type assertions.", "2"] + ], "public/app/features/dashboard-scene/scene/DashboardScene.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], @@ -4176,14 +4190,14 @@ exports[`better eslint`] = { ], "public/app/features/query/components/QueryGroup.tsx:5381": [ [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"], - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"], + [0, 0, 0, "Styles should be written using objects.", "1"], [0, 0, 0, "Styles should be written using objects.", "2"], [0, 0, 0, "Styles should be written using objects.", "3"], [0, 0, 0, "Styles should be written using objects.", "4"], [0, 0, 0, "Styles should be written using objects.", "5"], [0, 0, 0, "Styles should be written using objects.", "6"], [0, 0, 0, "Styles should be written using objects.", "7"], - [0, 0, 0, "Styles should be written using objects.", "8"] + [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "8"] ], "public/app/features/query/components/QueryGroupOptions.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] diff --git a/package.json b/package.json index 2451d5a22a9..0e007f4d3af 100644 --- a/package.json +++ b/package.json @@ -255,7 +255,7 @@ "@grafana/lezer-traceql": "0.0.12", "@grafana/monaco-logql": "^0.0.7", "@grafana/runtime": "workspace:*", - "@grafana/scenes": "1.27.0", + "@grafana/scenes": "1.28.0", "@grafana/schema": "workspace:*", "@grafana/ui": "workspace:*", "@kusto/monaco-kusto": "^7.4.0", diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.tsx index a3a54960bc4..e7d147c19c0 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.tsx @@ -3,16 +3,32 @@ import React from 'react'; import { IconName } from '@grafana/data'; import { SceneObjectBase, SceneComponentProps } from '@grafana/scenes'; +import { VizPanelManager } from '../VizPanelManager'; + import { PanelDataPaneTabState, PanelDataPaneTab } from './types'; export class PanelDataAlertingTab extends SceneObjectBase implements PanelDataPaneTab { static Component = PanelDataAlertingTabRendered; tabId = 'alert'; icon: IconName = 'bell'; + private _panelManager: VizPanelManager; + constructor(panelManager: VizPanelManager) { + super({}); + + this._panelManager = panelManager; + } getTabLabel() { return 'Alert'; } + + getItemsCount() { + return 0; + } + + get panelManager() { + return this._panelManager; + } } function PanelDataAlertingTabRendered(props: SceneComponentProps) { diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx index a71b20f7a02..24976dcbc0b 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx @@ -1,24 +1,29 @@ import React from 'react'; +import { Unsubscribable } from 'rxjs'; import { SceneComponentProps, + SceneDataTransformer, SceneObjectBase, - SceneObjectRef, SceneObjectState, SceneObjectUrlSyncConfig, SceneObjectUrlValues, + SceneQueryRunner, VizPanel, + sceneGraph, } from '@grafana/scenes'; import { Tab, TabContent, TabsBar } from '@grafana/ui'; import { shouldShowAlertingTab } from 'app/features/dashboard/components/PanelEditor/state/selectors'; +import { ShareQueryDataProvider } from '../../scene/ShareQueryDataProvider'; +import { VizPanelManager } from '../VizPanelManager'; + import { PanelDataAlertingTab } from './PanelDataAlertingTab'; import { PanelDataQueriesTab } from './PanelDataQueriesTab'; import { PanelDataTransformationsTab } from './PanelDataTransformationsTab'; import { PanelDataPaneTab } from './types'; export interface PanelDataPaneState extends SceneObjectState { - panelRef: SceneObjectRef; tabs?: PanelDataPaneTab[]; tab?: string; } @@ -26,6 +31,9 @@ export interface PanelDataPaneState extends SceneObjectState { export class PanelDataPane extends SceneObjectBase { static Component = PanelDataPaneRendered; protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['tab'] }); + private _initialTabsBuilt = false; + private panelSubscription: Unsubscribable | undefined; + public panelManager: VizPanelManager; getUrlState() { return { @@ -42,36 +50,81 @@ export class PanelDataPane extends SceneObjectBase { } } - constructor(state: Omit & { tab?: string }) { + constructor(panelMgr: VizPanelManager) { super({ tab: 'queries', - ...state, }); - const { panelRef } = this.state; - const panel = panelRef.resolve(); + this.panelManager = panelMgr; + this.addActivationHandler(() => this.onActivate()); + } - if (panel) { - // The subscription below is needed because the plugin may not be loaded when this pane is mounted. - // This can happen i.e. when the user opens the panel editor directly via an URL. - this._subs.add( - panel.subscribeToState((n, p) => { - if (n.pluginVersion || p.pluginId !== n.pluginId) { - this.buildTabs(); - } - }) - ); + private onActivate() { + const panel = this.panelManager.state.panel; + this.setupPanelSubscription(panel); + this.buildTabs(); + + this._subs.add( + // Setup subscription for the case when panel type changed + this.panelManager.subscribeToState((n, p) => { + if (n.panel !== p.panel) { + this.buildTabs(); + this.setupPanelSubscription(n.panel); + } + }) + ); + + return () => { + if (this.panelSubscription) { + this.panelSubscription.unsubscribe(); + this.panelSubscription = undefined; + } + }; + } + + private setupPanelSubscription(panel: VizPanel) { + if (this.panelSubscription) { + this._initialTabsBuilt = false; + this.panelSubscription.unsubscribe(); } - this.addActivationHandler(() => this.buildTabs()); + this.panelSubscription = panel.subscribeToState(() => { + if (panel.getPlugin() && !this._initialTabsBuilt) { + this.buildTabs(); + this._initialTabsBuilt = true; + } + }); + } + + private getDataObjects(): [SceneQueryRunner | ShareQueryDataProvider | undefined, SceneDataTransformer | undefined] { + const dataObj = sceneGraph.getData(this.panelManager.state.panel); + + let runner: SceneQueryRunner | ShareQueryDataProvider | undefined; + let transformer: SceneDataTransformer | undefined; + + if (dataObj instanceof SceneQueryRunner || dataObj instanceof ShareQueryDataProvider) { + runner = dataObj; + } + + if (dataObj instanceof SceneDataTransformer) { + transformer = dataObj; + if (transformer.state.$data instanceof SceneQueryRunner) { + runner = transformer.state.$data; + } + } + + return [runner, transformer]; } private buildTabs() { - const { panelRef } = this.state; + const panelManager = this.panelManager; + const panel = panelManager.state.panel; + const [runner] = this.getDataObjects(); const tabs: PanelDataPaneTab[] = []; - if (panelRef) { - const plugin = panelRef.resolve().getPlugin(); + if (panel) { + const plugin = panel.getPlugin(); + if (!plugin) { this.setState({ tabs }); return; @@ -80,11 +133,14 @@ export class PanelDataPane extends SceneObjectBase { this.setState({ tabs }); return; } else { - tabs.push(new PanelDataQueriesTab({})); - tabs.push(new PanelDataTransformationsTab({})); + if (runner) { + tabs.push(new PanelDataQueriesTab(this.panelManager)); + } + + tabs.push(new PanelDataTransformationsTab(this.panelManager)); if (shouldShowAlertingTab(plugin)) { - tabs.push(new PanelDataAlertingTab({})); + tabs.push(new PanelDataAlertingTab(this.panelManager)); } } } @@ -115,7 +171,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { key={`${t.getTabLabel()}-${index}`} label={t.getTabLabel()} icon={t.icon} - // suffix={} + counter={t.getItemsCount?.()} active={t.tabId === tab} onChangeTab={() => model.onChangeTab(t)} /> diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx index 7fd6a5c6703..6a8f082824c 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx @@ -1,20 +1,172 @@ import React from 'react'; -import { IconName } from '@grafana/data'; -import { SceneObjectBase, SceneComponentProps } from '@grafana/scenes'; +import { DataSourceApi, DataSourceInstanceSettings, IconName } from '@grafana/data'; +import { SceneObjectBase, SceneComponentProps, SceneQueryRunner, sceneGraph } from '@grafana/scenes'; +import { DataQuery } from '@grafana/schema'; +import { QueryEditorRows } from 'app/features/query/components/QueryEditorRows'; +import { QueryGroupTopSection } from 'app/features/query/components/QueryGroup'; +import { GrafanaQuery } from 'app/plugins/datasource/grafana/types'; +import { QueryGroupOptions } from 'app/types'; + +import { PanelTimeRange } from '../../scene/PanelTimeRange'; +import { ShareQueryDataProvider } from '../../scene/ShareQueryDataProvider'; +import { VizPanelManager } from '../VizPanelManager'; import { PanelDataPaneTabState, PanelDataPaneTab } from './types'; -export class PanelDataQueriesTab extends SceneObjectBase implements PanelDataPaneTab { +interface PanelDataQueriesTabState extends PanelDataPaneTabState { + // dataRef: SceneObjectRef; + datasource?: DataSourceApi; + dsSettings?: DataSourceInstanceSettings; +} +export class PanelDataQueriesTab extends SceneObjectBase implements PanelDataPaneTab { static Component = PanelDataQueriesTabRendered; tabId = 'queries'; icon: IconName = 'database'; + private _panelManager: VizPanelManager; getTabLabel() { return 'Queries'; } + + getItemsCount() { + const dataObj = this._panelManager.state.panel.state.$data!; + + if (dataObj instanceof ShareQueryDataProvider) { + return 1; + } + + if (dataObj instanceof SceneQueryRunner) { + return dataObj.state.queries.length; + } + + return null; + } + + constructor(panelManager: VizPanelManager) { + super({}); + + this._panelManager = panelManager; + } + + buildQueryOptions(): QueryGroupOptions { + const panelManager = this._panelManager; + const panelObj = this._panelManager.state.panel; + const dataObj = panelObj.state.$data!; + const queryRunner = this._panelManager.queryRunner; + + const timeRangeObj = sceneGraph.getTimeRange(panelObj); + + let timeRangeOpts: QueryGroupOptions['timeRange'] = { + from: undefined, + shift: undefined, + hide: undefined, + }; + + if (timeRangeObj instanceof PanelTimeRange) { + timeRangeOpts = { + from: timeRangeObj.state.timeFrom, + shift: timeRangeObj.state.timeShift, + hide: timeRangeObj.state.hideTimeOverride, + }; + } + + let queries: QueryGroupOptions['queries'] = []; + if (dataObj instanceof ShareQueryDataProvider) { + queries = [dataObj.state.query]; + } + + if (dataObj instanceof SceneQueryRunner) { + queries = dataObj.state.queries; + } + + return { + // TODO + // cacheTimeout: dsSettings?.meta.queryOptions?.cacheTimeout ? panel.cacheTimeout : undefined, + // queryCachingTTL: dsSettings?.cachingConfig?.enabled ? panel.queryCachingTTL : undefined, + dataSource: { + default: panelManager.state.dsSettings?.isDefault, + type: panelManager.state.dsSettings?.type, + uid: panelManager.state.dsSettings?.uid, + }, + queries, + maxDataPoints: queryRunner.state.maxDataPoints, + minInterval: queryRunner.state.minInterval, + timeRange: timeRangeOpts, + }; + } + + onOpenInspector = () => { + this._panelManager.inspectPanel(); + }; + + onChangeDataSource = async ( + newSettings: DataSourceInstanceSettings, + defaultQueries?: DataQuery[] | GrafanaQuery[] + ) => { + this._panelManager.changePanelDataSource(newSettings, defaultQueries); + }; + + onQueryOptionsChange = (options: QueryGroupOptions) => { + this._panelManager.changeQueryOptions(options); + }; + + onQueriesChange = (queries: DataQuery[]) => { + this._panelManager.changeQueries(queries); + }; + + getQueries() { + const dataObj = this._panelManager.state.panel.state.$data!; + + if (dataObj instanceof ShareQueryDataProvider) { + return [dataObj.state.query]; + } + return this._panelManager.queryRunner.state.queries; + } + + get panelManager() { + return this._panelManager; + } } -function PanelDataQueriesTabRendered(props: SceneComponentProps) { - return
TODO Queries
; +function PanelDataQueriesTabRendered({ model }: SceneComponentProps) { + const { panel, datasource, dsSettings } = model.panelManager.useState(); + const { $data: dataObj } = panel.useState(); + + if (!dataObj) { + return; + } + + const { data } = dataObj!.useState(); + + if (!datasource || !dsSettings || !data) { + return null; + } + + return ( + <> + + + {dataObj instanceof ShareQueryDataProvider ? ( +

TODO: DashboardQueryEditor

+ ) : ( + {}} + onQueriesChange={model.onQueriesChange} + onRunQueries={() => {}} + /> + )} + + ); } diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx index 9bf9a76a22c..c0025078ed7 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx @@ -3,18 +3,44 @@ import React from 'react'; import { IconName } from '@grafana/data'; import { SceneObjectBase, SceneComponentProps } from '@grafana/scenes'; +import { VizPanelManager } from '../VizPanelManager'; + import { PanelDataPaneTabState, PanelDataPaneTab } from './types'; -export class PanelDataTransformationsTab extends SceneObjectBase implements PanelDataPaneTab { +interface PanelDataTransformationsTabState extends PanelDataPaneTabState {} + +export class PanelDataTransformationsTab + extends SceneObjectBase + implements PanelDataPaneTab +{ static Component = PanelDataTransformationsTabRendered; tabId = 'transformations'; icon: IconName = 'process'; + private _panelManager: VizPanelManager; getTabLabel() { return 'Transformations'; } + + getItemsCount() { + return 0; + } + + constructor(panelManager: VizPanelManager) { + super({}); + + this._panelManager = panelManager; + } + + get panelManager() { + return this._panelManager; + } } -function PanelDataTransformationsTabRendered(props: SceneComponentProps) { +function PanelDataTransformationsTabRendered({ model }: SceneComponentProps) { + // const { dataRef } = model.useState(); + // const dataObj = dataRef.resolve(); + // // const { transformations } = dataObj.useState(); + return
TODO Transformations
; } diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/types.ts b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/types.ts index 851820c5065..973251b6287 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/types.ts +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/types.ts @@ -5,6 +5,7 @@ export interface PanelDataPaneTabState extends SceneObjectState {} export interface PanelDataPaneTab extends SceneObject { getTabLabel(): string; + getItemsCount?(): number | null; tabId: string; icon: IconName; } diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx index f9b824471b8..1392b03b025 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx @@ -14,12 +14,15 @@ import { SplitLayout, VizPanel, } from '@grafana/scenes'; +import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { DashboardScene } from '../scene/DashboardScene'; +import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper'; import { getDashboardUrl } from '../utils/urlBuilders'; import { PanelDataPane } from './PanelDataPane/PanelDataPane'; import { PanelEditorRenderer } from './PanelEditorRenderer'; +import { PanelEditorUrlSync } from './PanelEditorUrlSync'; import { PanelOptionsPane } from './PanelOptionsPane'; import { PanelVizTypePicker } from './PanelVizTypePicker'; import { VizPanelManager } from './VizPanelManager'; @@ -29,9 +32,9 @@ export interface PanelEditorState extends SceneObjectState { controls?: SceneObject[]; isDirty?: boolean; /** Panel to inspect */ - inspectPanelId?: string; + inspectPanelKey?: string; /** Scene object that handles the current drawer */ - drawer?: SceneObject; + overlay?: SceneObject; dashboardRef: SceneObjectRef; sourcePanelRef: SceneObjectRef; @@ -41,6 +44,11 @@ export interface PanelEditorState extends SceneObjectState { export class PanelEditor extends SceneObjectBase { static Component = PanelEditorRenderer; + /** + * Handles url sync + */ + protected _urlSync = new PanelEditorUrlSync(this); + public constructor(state: PanelEditorState) { super(state); @@ -48,6 +56,10 @@ export class PanelEditor extends SceneObjectBase { } private _activationHandler() { + const oldDashboardWrapper = new DashboardModelCompatibilityWrapper(this.state.dashboardRef.resolve()); + // @ts-expect-error + getDashboardSrv().setCurrent(oldDashboardWrapper); + // Deactivation logic return () => { getUrlSyncManager().cleanUp(this); @@ -85,13 +97,15 @@ export class PanelEditor extends SceneObjectBase { private _commitChanges() { const dashboard = this.state.dashboardRef.resolve(); const sourcePanel = this.state.sourcePanelRef.resolve(); - const panel = this.state.panelRef.resolve(); + + const panelMngr = this.state.panelRef.resolve(); if (!dashboard.state.isEditing) { dashboard.onEnterEditMode(); } - const newState = sceneUtils.cloneSceneObjectState(panel.state); + const newState = sceneUtils.cloneSceneObjectState(panelMngr.state.panel.state); + sourcePanel.setState(newState); // preserve time range and variables state @@ -107,6 +121,7 @@ export class PanelEditor extends SceneObjectBase { getDashboardUrl({ uid: this.state.dashboardRef.resolve().state.uid, currentQueryParams: locationService.getLocation().search, + useExperimentalURL: true, }) ); } @@ -114,7 +129,8 @@ export class PanelEditor extends SceneObjectBase { export function buildPanelEditScene(dashboard: DashboardScene, panel: VizPanel): PanelEditor { const panelClone = panel.clone(); - const vizPanelMgr = new VizPanelManager(panelClone); + + const vizPanelMgr = new VizPanelManager(panelClone, dashboard.getRef()); const dashboardStateCloned = sceneUtils.cloneSceneObjectState(dashboard.state); return new PanelEditor({ @@ -130,10 +146,10 @@ export function buildPanelEditScene(dashboard: DashboardScene, panel: VizPanel): direction: 'column', primary: new SceneFlexLayout({ direction: 'column', - children: [panelClone], + children: [vizPanelMgr], }), secondary: new SceneFlexItem({ - body: new PanelDataPane({ panelRef: panelClone.getRef() }), + body: new PanelDataPane(vizPanelMgr), }), }), secondary: new SceneFlexLayout({ diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx index d44801cbaa5..109b3db72de 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx @@ -13,7 +13,7 @@ import { useSelector } from 'app/types/store'; import { PanelEditor } from './PanelEditor'; export function PanelEditorRenderer({ model }: SceneComponentProps) { - const { body, controls, drawer } = model.useState(); + const { body, controls, overlay } = model.useState(); const styles = useStyles2(getStyles); const location = useLocation(); const navIndex = useSelector((state) => state.navIndex); @@ -34,7 +34,7 @@ export function PanelEditorRenderer({ model }: SceneComponentProps) - {drawer && } + {overlay && } ); } diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditorUrlSync.ts b/public/app/features/dashboard-scene/panel-edit/PanelEditorUrlSync.ts new file mode 100644 index 00000000000..d6b5d51e4eb --- /dev/null +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditorUrlSync.ts @@ -0,0 +1,49 @@ +import { AppEvents } from '@grafana/data'; +import { locationService } from '@grafana/runtime'; +import { SceneObjectUrlSyncHandler, SceneObjectUrlValues } from '@grafana/scenes'; +import appEvents from 'app/core/app_events'; + +import { PanelInspectDrawer } from '../inspect/PanelInspectDrawer'; +import { findVizPanelByKey } from '../utils/utils'; + +import { PanelEditor, PanelEditorState } from './PanelEditor'; + +export class PanelEditorUrlSync implements SceneObjectUrlSyncHandler { + constructor(private _scene: PanelEditor) {} + + getKeys(): string[] { + return ['inspect']; + } + + getUrlState(): SceneObjectUrlValues { + const state = this._scene.state; + return { + inspect: state.inspectPanelKey, + }; + } + + updateFromUrl(values: SceneObjectUrlValues): void { + const { inspectPanelKey } = this._scene.state; + const update: Partial = {}; + + // Handle inspect object state + if (typeof values.inspect === 'string') { + const panel = findVizPanelByKey(this._scene, values.inspect); + if (!panel) { + appEvents.emit(AppEvents.alertError, ['Panel not found']); + locationService.partial({ inspect: null }); + return; + } + + update.inspectPanelKey = values.inspect; + update.overlay = new PanelInspectDrawer({ panelRef: panel.getRef() }); + } else if (inspectPanelKey) { + update.inspectPanelKey = undefined; + update.overlay = undefined; + } + + if (Object.keys(update).length > 0) { + this._scene.setState(update); + } + } +} diff --git a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx index 2a74318ad32..d0a7c50bc3c 100644 --- a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx @@ -1,31 +1,178 @@ -import { FieldConfigSource } from '@grafana/data'; -import { DeepPartial, SceneQueryRunner, VizPanel } from '@grafana/scenes'; +import { map, of } from 'rxjs'; + +import { DataQueryRequest, DataSourceApi, LoadingState, PanelData } from '@grafana/data'; +import { locationService } from '@grafana/runtime'; +import { SceneDataTransformer, SceneQueryRunner, VizPanel } from '@grafana/scenes'; +import { DataQuery, DataSourceJsonData, DataSourceRef } from '@grafana/schema'; +import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; +import { InspectTab } from 'app/features/inspector/types'; +import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard'; +import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types'; + +import { DashboardScene } from '../scene/DashboardScene'; +import { PanelTimeRange, PanelTimeRangeState } from '../scene/PanelTimeRange'; +import { ShareQueryDataProvider } from '../scene/ShareQueryDataProvider'; +import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; +import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper'; +import { findVizPanelByKey } from '../utils/utils'; import { VizPanelManager } from './VizPanelManager'; +import testDashboard from './testfiles/testDashboard.json'; + +const runRequestMock = jest.fn().mockImplementation((ds: DataSourceApi, request: DataQueryRequest) => { + const result: PanelData = { + state: LoadingState.Loading, + series: [], + timeRange: request.range, + }; + + return of([]).pipe( + map(() => { + result.state = LoadingState.Done; + result.series = []; + + return result; + }) + ); +}); + +const ds1Mock: DataSourceApi = { + meta: { + id: 'grafana-testdata-datasource', + }, + name: 'grafana-testdata-datasource', + type: 'grafana-testdata-datasource', + uid: 'gdev-testdata', + getRef: () => { + return { type: 'grafana-testdata-datasource', uid: 'gdev-testdata' }; + }, +} as DataSourceApi; + +const ds2Mock: DataSourceApi = { + meta: { + id: 'grafana-prometheus-datasource', + }, + name: 'grafana-prometheus-datasource', + type: 'grafana-prometheus-datasource', + uid: 'gdev-prometheus', + getRef: () => { + return { type: 'grafana-prometheus-datasource', uid: 'gdev-prometheus' }; + }, +} as DataSourceApi; + +const ds3Mock: DataSourceApi = { + meta: { + id: DASHBOARD_DATASOURCE_PLUGIN_ID, + }, + name: SHARED_DASHBOARD_QUERY, + type: SHARED_DASHBOARD_QUERY, + uid: SHARED_DASHBOARD_QUERY, + getRef: () => { + return { type: SHARED_DASHBOARD_QUERY, uid: SHARED_DASHBOARD_QUERY }; + }, +} as DataSourceApi; + +const instance1SettingsMock = { + id: 1, + uid: 'gdev-testdata', + name: 'testDs1', + type: 'grafana-testdata-datasource', + meta: { + id: 'grafana-testdata-datasource', + }, +}; + +const instance2SettingsMock = { + id: 1, + uid: 'gdev-prometheus', + name: 'testDs2', + type: 'grafana-prometheus-datasource', + meta: { + id: 'grafana-prometheus-datasource', + }, +}; + +// Mock the store module +jest.mock('app/core/store', () => ({ + exists: jest.fn(), + get: jest.fn(), + getObject: jest.fn(), + setObject: jest.fn(), +})); + +const store = jest.requireMock('app/core/store'); + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => { + return runRequestMock(ds, request); + }, + getDataSourceSrv: () => ({ + get: async (ref: DataSourceRef) => { + if (ref.uid === 'gdev-testdata') { + return ds1Mock; + } + + if (ref.uid === 'gdev-prometheus') { + return ds2Mock; + } + + if (ref.uid === SHARED_DASHBOARD_QUERY) { + return ds3Mock; + } + + return null; + }, + getInstanceSettings: (ref: DataSourceRef) => { + if (ref.uid === 'gdev-testdata') { + return instance1SettingsMock; + } + + if (ref.uid === 'gdev-prometheus') { + return instance2SettingsMock; + } + + return null; + }, + }), + locationService: { + partial: jest.fn(), + }, +})); describe('VizPanelManager', () => { describe('changePluginType', () => { it('Should successfully change from one viz type to another', () => { - const vizPanelManager = getVizPanelManager(); - expect(vizPanelManager.state.panel.state.pluginId).toBe('table'); - vizPanelManager.changePluginType('timeseries'); + const vizPanelManager = setupTest('panel-1'); expect(vizPanelManager.state.panel.state.pluginId).toBe('timeseries'); + vizPanelManager.changePluginType('table'); + expect(vizPanelManager.state.panel.state.pluginId).toBe('table'); }); it('Should clear custom options', () => { + const dashboardSceneMock = new DashboardScene({}); const overrides = [ { matcher: { id: 'matcherOne' }, properties: [{ id: 'custom.propertyOne' }, { id: 'custom.propertyTwo' }, { id: 'standardProperty' }], }, ]; - const vizPanelManager = getVizPanelManager(undefined, { - defaults: { - custom: 'Custom', + const vizPanel = new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), + options: undefined, + fieldConfig: { + defaults: { + custom: 'Custom', + }, + overrides, }, - overrides, }); + const vizPanelManager = new VizPanelManager(vizPanel, dashboardSceneMock.getRef()); + expect(vizPanelManager.state.panel.state.fieldConfig.defaults.custom).toBe('Custom'); expect(vizPanelManager.state.panel.state.fieldConfig.overrides).toBe(overrides); @@ -37,14 +184,21 @@ describe('VizPanelManager', () => { }); it('Should restore cached options/fieldConfig if they exist', () => { - const vizPanelManager = getVizPanelManager( - { + const dashboardSceneMock = new DashboardScene({}); + const vizPanel = new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), + options: { customOption: 'A', }, - { defaults: { custom: 'Custom' }, overrides: [] } - ); + fieldConfig: { defaults: { custom: 'Custom' }, overrides: [] }, + }); - vizPanelManager.changePluginType('timeseries'); + const vizPanelManager = new VizPanelManager(vizPanel, dashboardSceneMock.getRef()); + + vizPanelManager.changePluginType('timeseties'); //@ts-ignore expect(vizPanelManager.state.panel.state.options['customOption']).toBeUndefined(); expect(vizPanelManager.state.panel.state.fieldConfig.defaults.custom).toStrictEqual({}); @@ -56,22 +210,412 @@ describe('VizPanelManager', () => { expect(vizPanelManager.state.panel.state.fieldConfig.defaults.custom).toBe('Custom'); }); }); + + describe('query options', () => { + beforeEach(() => { + store.setObject.mockClear(); + }); + + describe('activation', () => { + it('should load data source', async () => { + const vizPanelManager = setupTest('panel-1'); + vizPanelManager.activate(); + await Promise.resolve(); + + expect(vizPanelManager.state.datasource).toEqual(ds1Mock); + expect(vizPanelManager.state.dsSettings).toEqual(instance1SettingsMock); + }); + + it('should store loaded data source in local storage', async () => { + const vizPanelManager = setupTest('panel-1'); + vizPanelManager.activate(); + await Promise.resolve(); + + expect(store.setObject).toHaveBeenCalledWith('grafana.dashboards.panelEdit.lastUsedDatasource', { + dashboardUid: 'ffbe00e2-803c-4d49-adb7-41aad336234f', + datasourceUid: 'gdev-testdata', + }); + }); + }); + + describe('data source change', () => { + it('should load new data source', async () => { + const vizPanelManager = setupTest('panel-1'); + vizPanelManager.activate(); + await Promise.resolve(); + + const dataObj = vizPanelManager.queryRunner; + dataObj.setState({ + datasource: { + type: 'grafana-prometheus-datasource', + uid: 'gdev-prometheus', + }, + }); + + await Promise.resolve(); + + expect(store.setObject).toHaveBeenCalledTimes(2); + expect(store.setObject).toHaveBeenLastCalledWith('grafana.dashboards.panelEdit.lastUsedDatasource', { + dashboardUid: 'ffbe00e2-803c-4d49-adb7-41aad336234f', + datasourceUid: 'gdev-prometheus', + }); + + expect(vizPanelManager.state.datasource).toEqual(ds2Mock); + expect(vizPanelManager.state.dsSettings).toEqual(instance2SettingsMock); + }); + }); + + describe('query options change', () => { + describe('time overrides', () => { + it('should create PanelTimeRange object', async () => { + const vizPanelManager = setupTest('panel-1'); + vizPanelManager.activate(); + await Promise.resolve(); + + const panel = vizPanelManager.state.panel; + + expect(panel.state.$timeRange).toBeUndefined(); + + vizPanelManager.changeQueryOptions({ + dataSource: { + name: 'grafana-testdata', + type: 'grafana-testdata-datasource', + default: true, + }, + queries: [], + timeRange: { + from: '1h', + }, + }); + + expect(panel.state.$timeRange).toBeInstanceOf(PanelTimeRange); + }); + it('should update PanelTimeRange object on time options update', async () => { + const vizPanelManager = setupTest('panel-1'); + vizPanelManager.activate(); + await Promise.resolve(); + + const panel = vizPanelManager.state.panel; + + expect(panel.state.$timeRange).toBeUndefined(); + + vizPanelManager.changeQueryOptions({ + dataSource: { + name: 'grafana-testdata', + type: 'grafana-testdata-datasource', + default: true, + }, + queries: [], + timeRange: { + from: '1h', + }, + }); + + expect(panel.state.$timeRange).toBeInstanceOf(PanelTimeRange); + expect((panel.state.$timeRange?.state as PanelTimeRangeState).timeFrom).toBe('1h'); + + vizPanelManager.changeQueryOptions({ + dataSource: { + name: 'grafana-testdata', + type: 'grafana-testdata-datasource', + default: true, + }, + queries: [], + timeRange: { + from: '2h', + }, + }); + + expect((panel.state.$timeRange?.state as PanelTimeRangeState).timeFrom).toBe('2h'); + }); + + it('should remove PanelTimeRange object on time options cleared', async () => { + const vizPanelManager = setupTest('panel-1'); + vizPanelManager.activate(); + await Promise.resolve(); + + const panel = vizPanelManager.state.panel; + + expect(panel.state.$timeRange).toBeUndefined(); + + vizPanelManager.changeQueryOptions({ + dataSource: { + name: 'grafana-testdata', + type: 'grafana-testdata-datasource', + default: true, + }, + queries: [], + timeRange: { + from: '1h', + }, + }); + + expect(panel.state.$timeRange).toBeInstanceOf(PanelTimeRange); + + vizPanelManager.changeQueryOptions({ + dataSource: { + name: 'grafana-testdata', + type: 'grafana-testdata-datasource', + default: true, + }, + queries: [], + timeRange: { + from: null, + }, + }); + + expect(panel.state.$timeRange).toBeUndefined(); + }); + }); + + describe('max data points and interval', () => { + it('max data points', async () => { + const vizPanelManager = setupTest('panel-1'); + vizPanelManager.activate(); + await Promise.resolve(); + + const dataObj = vizPanelManager.queryRunner; + + expect(dataObj.state.maxDataPoints).toBeUndefined(); + + vizPanelManager.changeQueryOptions({ + dataSource: { + name: 'grafana-testdata', + type: 'grafana-testdata-datasource', + default: true, + }, + queries: [], + maxDataPoints: 100, + }); + + expect(dataObj.state.maxDataPoints).toBe(100); + }); + + it('max data points', async () => { + const vizPanelManager = setupTest('panel-1'); + vizPanelManager.activate(); + await Promise.resolve(); + + const dataObj = vizPanelManager.queryRunner; + + expect(dataObj.state.maxDataPoints).toBeUndefined(); + + vizPanelManager.changeQueryOptions({ + dataSource: { + name: 'grafana-testdata', + type: 'grafana-testdata-datasource', + default: true, + }, + queries: [], + minInterval: '1s', + }); + + expect(dataObj.state.minInterval).toBe('1s'); + }); + }); + }); + + describe('query inspection', () => { + it('allows query inspection from the tab', async () => { + const vizPanelManager = setupTest('panel-1'); + vizPanelManager.inspectPanel(); + + expect(locationService.partial).toHaveBeenCalledWith({ inspect: 1, inspectTab: InspectTab.Query }); + }); + }); + + describe('data source change', () => { + it('changing from one plugin to another', async () => { + const vizPanelManager = setupTest('panel-1'); + vizPanelManager.activate(); + await Promise.resolve(); + + const panel = vizPanelManager.state.panel; + + expect(panel.state.$data).toBeInstanceOf(SceneQueryRunner); + + expect((panel.state.$data as SceneQueryRunner).state.datasource).toEqual({ + uid: 'gdev-testdata', + type: 'grafana-testdata-datasource', + }); + + await vizPanelManager.changePanelDataSource({ + name: 'grafana-prometheus', + type: 'grafana-prometheus-datasource', + uid: 'gdev-prometheus', + meta: { + name: 'Prometheus', + module: 'prometheus', + id: 'grafana-prometheus-datasource', + }, + } as any); + + expect((panel.state.$data as SceneQueryRunner).state.datasource).toEqual({ + uid: 'gdev-prometheus', + type: 'grafana-prometheus-datasource', + }); + }); + + it('changing from a plugin to a dashboard data source', async () => { + const vizPanelManager = setupTest('panel-1'); + vizPanelManager.activate(); + await Promise.resolve(); + + const panel = vizPanelManager.state.panel; + + expect(panel.state.$data).toBeInstanceOf(SceneQueryRunner); + + expect((panel.state.$data as SceneQueryRunner).state.datasource).toEqual({ + uid: 'gdev-testdata', + type: 'grafana-testdata-datasource', + }); + + await vizPanelManager.changePanelDataSource({ + name: SHARED_DASHBOARD_QUERY, + type: 'datasource', + uid: SHARED_DASHBOARD_QUERY, + meta: { + name: 'Prometheus', + module: 'prometheus', + id: DASHBOARD_DATASOURCE_PLUGIN_ID, + }, + } as any); + + expect(panel.state.$data).toBeInstanceOf(ShareQueryDataProvider); + }); + + it('changing from dashboard data source to a plugin', async () => { + const vizPanelManager = setupTest('panel-3'); + vizPanelManager.activate(); + await Promise.resolve(); + + const panel = vizPanelManager.state.panel; + + expect(panel.state.$data).toBeInstanceOf(ShareQueryDataProvider); + + await vizPanelManager.changePanelDataSource({ + name: 'grafana-prometheus', + type: 'grafana-prometheus-datasource', + uid: 'gdev-prometheus', + meta: { + name: 'Prometheus', + module: 'prometheus', + id: 'grafana-prometheus-datasource', + }, + } as any); + + expect(panel.state.$data).toBeInstanceOf(SceneQueryRunner); + expect((panel.state.$data as SceneQueryRunner).state.datasource).toEqual({ + uid: 'gdev-prometheus', + type: 'grafana-prometheus-datasource', + }); + }); + + describe('with transformations', () => { + it('changing from one plugin to another', async () => { + const vizPanelManager = setupTest('panel-2'); + vizPanelManager.activate(); + await Promise.resolve(); + + const panel = vizPanelManager.state.panel; + + expect(panel.state.$data).toBeInstanceOf(SceneDataTransformer); + + expect((panel.state.$data?.state.$data as SceneQueryRunner).state.datasource).toEqual({ + uid: 'gdev-testdata', + type: 'grafana-testdata-datasource', + }); + + await vizPanelManager.changePanelDataSource({ + name: 'grafana-prometheus', + type: 'grafana-prometheus-datasource', + uid: 'gdev-prometheus', + meta: { + name: 'Prometheus', + module: 'prometheus', + id: 'grafana-prometheus-datasource', + }, + } as any); + + expect(panel.state.$data).toBeInstanceOf(SceneDataTransformer); + expect((panel.state.$data?.state.$data as SceneQueryRunner).state.datasource).toEqual({ + uid: 'gdev-prometheus', + type: 'grafana-prometheus-datasource', + }); + }); + }); + + it('changing from a plugin to dashboard data source', async () => { + const vizPanelManager = setupTest('panel-2'); + vizPanelManager.activate(); + await Promise.resolve(); + + const panel = vizPanelManager.state.panel; + + expect(panel.state.$data).toBeInstanceOf(SceneDataTransformer); + + expect((panel.state.$data?.state.$data as SceneQueryRunner).state.datasource).toEqual({ + uid: 'gdev-testdata', + type: 'grafana-testdata-datasource', + }); + + await vizPanelManager.changePanelDataSource({ + name: SHARED_DASHBOARD_QUERY, + type: 'datasource', + uid: SHARED_DASHBOARD_QUERY, + meta: { + name: 'Prometheus', + module: 'prometheus', + id: DASHBOARD_DATASOURCE_PLUGIN_ID, + }, + } as any); + + expect(panel.state.$data).toBeInstanceOf(SceneDataTransformer); + expect(panel.state.$data?.state.$data).toBeInstanceOf(ShareQueryDataProvider); + }); + + it('changing from a dashboard data source to a plugin', async () => { + const vizPanelManager = setupTest('panel-4'); + vizPanelManager.activate(); + await Promise.resolve(); + + const panel = vizPanelManager.state.panel; + + expect(panel.state.$data).toBeInstanceOf(SceneDataTransformer); + expect(panel.state.$data?.state.$data).toBeInstanceOf(ShareQueryDataProvider); + + await vizPanelManager.changePanelDataSource({ + name: 'grafana-prometheus', + type: 'grafana-prometheus-datasource', + uid: 'gdev-prometheus', + meta: { + name: 'Prometheus', + module: 'prometheus', + id: 'grafana-prometheus-datasource', + }, + } as any); + + expect(panel.state.$data).toBeInstanceOf(SceneDataTransformer); + expect(panel.state.$data?.state.$data).toBeInstanceOf(SceneQueryRunner); + expect((panel.state.$data?.state.$data as SceneQueryRunner).state.datasource).toEqual({ + uid: 'gdev-prometheus', + type: 'grafana-prometheus-datasource', + }); + }); + }); + }); }); -function getVizPanelManager( - options: {} = {}, - fieldConfig: FieldConfigSource> = { overrides: [], defaults: {} } -) { - const vizPanel = new VizPanel({ - title: 'Panel A', - key: 'panel-1', - pluginId: 'table', - $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), - options, - fieldConfig, - }); +const setupTest = (panelId: string) => { + const scene = transformSaveModelToScene({ dashboard: testDashboard as any, meta: {} }); - const vizPanelManager = new VizPanelManager(vizPanel); + // The following happens on DahsboardScene activation. For the needs of this test this activation aint needed hence we hand-call it + // @ts-expect-error + getDashboardSrv().setCurrent(new DashboardModelCompatibilityWrapper(scene)); + + const panel = findVizPanelByKey(scene, panelId)!; + + const vizPanelManager = new VizPanelManager(panel.clone(), scene.getRef()); return vizPanelManager; -} +}; diff --git a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx index 0fe2b61b01f..e3ce3700aa9 100644 --- a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx +++ b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx @@ -1,12 +1,18 @@ import React from 'react'; +import { Unsubscribable } from 'rxjs'; import { + DataSourceApi, + DataSourceInstanceSettings, FieldConfigSource, + LoadingState, PanelModel, filterFieldConfigOverrides, + getDefaultTimeRange, isStandardFieldProp, restoreCustomOverrideRules, } from '@grafana/data'; +import { getDataSourceSrv, locationService } from '@grafana/runtime'; import { SceneObjectState, VizPanel, @@ -14,21 +20,143 @@ import { SceneComponentProps, sceneUtils, DeepPartial, + SceneObjectRef, + SceneObject, + SceneQueryRunner, + sceneGraph, + SceneDataTransformer, + SceneDataProvider, } from '@grafana/scenes'; +import { DataQuery, DataSourceRef } from '@grafana/schema'; import { getPluginVersion } from 'app/features/dashboard/state/PanelModel'; +import { storeLastUsedDataSourceInLocalStorage } from 'app/features/datasources/components/picker/utils'; +import { updateQueries } from 'app/features/query/state/updateQueries'; +import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard'; +import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types'; +import { GrafanaQuery } from 'app/plugins/datasource/grafana/types'; +import { QueryGroupOptions } from 'app/types'; + +import { DashboardScene } from '../scene/DashboardScene'; +import { PanelTimeRange, PanelTimeRangeState } from '../scene/PanelTimeRange'; +import { ShareQueryDataProvider, findObjectInScene } from '../scene/ShareQueryDataProvider'; +import { getPanelIdForVizPanel, getVizPanelKeyForPanelId } from '../utils/utils'; interface VizPanelManagerState extends SceneObjectState { panel: VizPanel; + datasource?: DataSourceApi; + dsSettings?: DataSourceInstanceSettings; } +// VizPanelManager serves as an API to manipulate VizPanel state from the outside. It allows panel type, options and data maniulation. export class VizPanelManager extends SceneObjectBase { + public static Component = ({ model }: SceneComponentProps) => { + const { panel } = model.useState(); + + return ; + }; + private _cachedPluginOptions: Record< string, { options: DeepPartial<{}>; fieldConfig: FieldConfigSource> } | undefined > = {}; - public constructor(panel: VizPanel) { + private _dataObjectSubscription: Unsubscribable | undefined; + + public constructor(panel: VizPanel, dashboardRef: SceneObjectRef) { super({ panel }); + + /** + * If the panel uses a shared query, we clone the source runner and attach it as a data provider for the shared one. + * This way the source panel does not to be present in the edit scene hierarchy. + */ + if (panel.state.$data instanceof ShareQueryDataProvider) { + const sharedProvider = panel.state.$data; + if (sharedProvider.state.query.panelId) { + const keyToFind = getVizPanelKeyForPanelId(sharedProvider.state.query.panelId); + const source = findObjectInScene(dashboardRef.resolve(), (scene: SceneObject) => scene.state.key === keyToFind); + if (source) { + sharedProvider.setState({ + $data: source.state.$data!.clone(), + }); + } + } + } + + this.addActivationHandler(() => this._onActivate()); + } + + private _onActivate() { + this.setupDataObjectSubscription(); + + this.loadDataSource(); + + return () => { + this._dataObjectSubscription?.unsubscribe(); + }; + } + + /** + * The subscription is updated whenever the data source type is changed so that we can update manager's stored + * data source and data source instance settings, which are needed for the query options and editors + */ + private setupDataObjectSubscription() { + const runner = this.queryRunner; + + if (this._dataObjectSubscription) { + this._dataObjectSubscription.unsubscribe(); + } + + this._dataObjectSubscription = runner.subscribeToState((n, p) => { + if (n.datasource !== p.datasource) { + this.loadDataSource(); + } + }); + } + + private async loadDataSource() { + const dataObj = this.state.panel.state.$data; + + if (!dataObj) { + return; + } + + let datasourceToLoad: DataSourceRef | undefined; + + if (dataObj instanceof ShareQueryDataProvider) { + datasourceToLoad = { + uid: SHARED_DASHBOARD_QUERY, + type: DASHBOARD_DATASOURCE_PLUGIN_ID, + }; + } else { + datasourceToLoad = this.queryRunner.state.datasource; + } + + if (!datasourceToLoad) { + return; + } + + try { + // TODO: Handle default/last used datasource selection for new panel + // Ref: PanelEditorQueries / componentDidMount + const datasource = await getDataSourceSrv().get(datasourceToLoad); + const dsSettings = getDataSourceSrv().getInstanceSettings(datasourceToLoad); + + if (datasource && dsSettings) { + this.setState({ + datasource, + dsSettings, + }); + + storeLastUsedDataSourceInLocalStorage( + { + type: dsSettings.type, + uid: dsSettings.uid, + } || { default: true } + ); + } + } catch (err) { + console.error(err); + } } public changePluginType(pluginType: string) { @@ -79,11 +207,172 @@ export class VizPanelManager extends SceneObjectBase { } this.setState({ panel: newPanel }); + this.setupDataObjectSubscription(); } - public static Component = ({ model }: SceneComponentProps) => { - const { panel } = model.useState(); + public async changePanelDataSource( + newSettings: DataSourceInstanceSettings, + defaultQueries?: DataQuery[] | GrafanaQuery[] + ) { + const { panel, dsSettings } = this.state; + const dataObj = panel.state.$data; + if (!dataObj) { + return; + } - return ; - }; + const currentDS = dsSettings ? await getDataSourceSrv().get({ uid: dsSettings.uid }) : undefined; + const nextDS = await getDataSourceSrv().get({ uid: newSettings.uid }); + + const currentQueries = []; + if (dataObj instanceof SceneQueryRunner) { + currentQueries.push(...dataObj.state.queries); + } else if (dataObj instanceof ShareQueryDataProvider) { + currentQueries.push(dataObj.state.query); + } + + // We need to pass in newSettings.uid as well here as that can be a variable expression and we want to store that in the query model not the current ds variable value + const queries = defaultQueries || (await updateQueries(nextDS, newSettings.uid, currentQueries, currentDS)); + + if (dataObj instanceof SceneQueryRunner) { + // Changing to Dashboard data source + if (newSettings.uid === SHARED_DASHBOARD_QUERY) { + // Changing from one plugin to another + const sharedProvider = new ShareQueryDataProvider({ + query: queries[0], + $data: new SceneQueryRunner({ + queries: [], + }), + data: { + series: [], + state: LoadingState.NotStarted, + timeRange: getDefaultTimeRange(), + }, + }); + panel.setState({ $data: sharedProvider }); + this.setupDataObjectSubscription(); + this.loadDataSource(); + } else { + dataObj.setState({ + datasource: { + type: newSettings.type, + uid: newSettings.uid, + }, + queries, + }); + if (defaultQueries) { + dataObj.runQueries(); + } + } + } else if (dataObj instanceof ShareQueryDataProvider && newSettings.uid !== SHARED_DASHBOARD_QUERY) { + const dataProvider = new SceneQueryRunner({ + datasource: { + type: newSettings.type, + uid: newSettings.uid, + }, + queries, + }); + panel.setState({ $data: dataProvider }); + this.setupDataObjectSubscription(); + this.loadDataSource(); + } else if (dataObj instanceof SceneDataTransformer) { + const data = dataObj.clone(); + + let provider: SceneDataProvider = new SceneQueryRunner({ + datasource: { + type: newSettings.type, + uid: newSettings.uid, + }, + queries, + }); + + if (newSettings.uid === SHARED_DASHBOARD_QUERY) { + provider = new ShareQueryDataProvider({ + query: queries[0], + $data: new SceneQueryRunner({ + queries: [], + }), + data: { + series: [], + state: LoadingState.NotStarted, + timeRange: getDefaultTimeRange(), + }, + }); + } + + data.setState({ + $data: provider, + }); + + panel.setState({ $data: data }); + + this.setupDataObjectSubscription(); + this.loadDataSource(); + } + } + + public changeQueryOptions(options: QueryGroupOptions) { + const panelObj = this.state.panel; + const dataObj = this.queryRunner; + let timeRangeObj = sceneGraph.getTimeRange(panelObj); + + const dataObjStateUpdate: Partial = {}; + const timeRangeObjStateUpdate: Partial = {}; + + if (options.maxDataPoints !== dataObj.state.maxDataPoints) { + dataObjStateUpdate.maxDataPoints = options.maxDataPoints ?? undefined; + } + if (options.minInterval !== dataObj.state.minInterval && options.minInterval !== null) { + dataObjStateUpdate.minInterval = options.minInterval; + } + if (options.timeRange) { + timeRangeObjStateUpdate.timeFrom = options.timeRange.from ?? undefined; + timeRangeObjStateUpdate.timeShift = options.timeRange.shift ?? undefined; + timeRangeObjStateUpdate.hideTimeOverride = options.timeRange.hide; + } + if (timeRangeObj instanceof PanelTimeRange) { + if (timeRangeObjStateUpdate.timeFrom !== undefined || timeRangeObjStateUpdate.timeShift !== undefined) { + // update time override + timeRangeObj.setState(timeRangeObjStateUpdate); + } else { + // remove time override + panelObj.setState({ $timeRange: undefined }); + } + } else { + // no time override present on the panel, let's create one first + panelObj.setState({ $timeRange: new PanelTimeRange(timeRangeObjStateUpdate) }); + } + + dataObj.setState(dataObjStateUpdate); + dataObj.runQueries(); + } + + public changeQueries(queries: DataQuery[]) { + const dataObj = this.queryRunner; + dataObj.setState({ queries }); + // TODO: Handle dashboard query + } + + public inspectPanel() { + const panel = this.state.panel; + const panelId = getPanelIdForVizPanel(panel); + + locationService.partial({ + inspect: panelId, + inspectTab: 'query', + }); + } + + get queryRunner(): SceneQueryRunner { + const dataObj = this.state.panel.state.$data; + + if (dataObj instanceof ShareQueryDataProvider) { + return dataObj.state.$data as SceneQueryRunner; + } + + if (dataObj instanceof SceneDataTransformer) { + return dataObj.state.$data as SceneQueryRunner; + } + + return dataObj as SceneQueryRunner; + } } diff --git a/public/app/features/dashboard-scene/panel-edit/testfiles/testDashboard.json b/public/app/features/dashboard-scene/panel-edit/testfiles/testDashboard.json new file mode 100644 index 00000000000..6a0c8377f76 --- /dev/null +++ b/public/app/features/dashboard-scene/panel-edit/testfiles/testDashboard.json @@ -0,0 +1,372 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 2378, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "gdev-testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "gdev-testdata" + }, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 1 + } + ], + "title": "Source panel", + "type": "timeseries" + }, + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "gdev-testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "10.3.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "gdev-testdata" + }, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 1 + } + ], + "title": "Panel with transforms", + "transformations": [ + { + "id": "reduce", + "options": {} + } + ], + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 1, + "refId": "A" + } + ], + "title": "Dashboard query", + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "10.3.0-pre", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 1, + "refId": "A" + } + ], + "title": "Dashboard query with transformations", + "transformations": [ + { + "id": "reduce", + "options": {} + } + ], + "type": "table" + } + ], + "refresh": "", + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Scenes/PanelEdit/Queries: Edit", + "uid": "ffbe00e2-803c-4d49-adb7-41aad336234f", + "version": 6, + "weekStart": "" +} diff --git a/public/app/features/dashboard-scene/scene/PanelTimeRange.tsx b/public/app/features/dashboard-scene/scene/PanelTimeRange.tsx index 6e943e97f11..8a217c49fdb 100644 --- a/public/app/features/dashboard-scene/scene/PanelTimeRange.tsx +++ b/public/app/features/dashboard-scene/scene/PanelTimeRange.tsx @@ -12,7 +12,7 @@ import { import { Icon, PanelChrome, TimePickerTooltip, Tooltip, useStyles2 } from '@grafana/ui'; import { TimeOverrideResult } from 'app/features/dashboard/utils/panel'; -interface PanelTimeRangeState extends SceneTimeRangeState { +export interface PanelTimeRangeState extends SceneTimeRangeState { timeFrom?: string; timeShift?: string; hideTimeOverride?: boolean; @@ -30,8 +30,21 @@ export class PanelTimeRange extends SceneTimeRangeTransformerBase this._onActivate()); } + private _onActivate() { + this._subs.add( + this.subscribeToState((n, p) => { + // Listen to own changes and update time info when required + if (n.timeFrom !== p.timeFrom || n.timeShift !== p.timeShift) { + const { timeInfo } = this.getTimeOverride(this.getAncestorTimeRange().state.value); + this.setState({ timeInfo }); + } + }) + ); + } protected ancestorTimeRangeChanged(timeRange: SceneTimeRangeState): void { const overrideResult = this.getTimeOverride(timeRange.value); this.setState({ value: overrideResult.timeRange, timeInfo: overrideResult.timeInfo }); diff --git a/public/app/features/dashboard-scene/scene/ShareQueryDataProvider.ts b/public/app/features/dashboard-scene/scene/ShareQueryDataProvider.ts index 876c28c788f..854f616c795 100644 --- a/public/app/features/dashboard-scene/scene/ShareQueryDataProvider.ts +++ b/public/app/features/dashboard-scene/scene/ShareQueryDataProvider.ts @@ -56,22 +56,27 @@ export class ShareQueryDataProvider extends SceneObjectBase scene.state.key === keyToFind); + const keyToFind = getVizPanelKeyForPanelId(query.panelId); + const source = findObjectInScene(this.getRoot(), (scene: SceneObject) => scene.state.key === keyToFind); - if (!source) { - console.log('Shared dashboard query refers to a panel that does not exist in the scene'); - return; - } + if (!source) { + console.log('Shared dashboard query refers to a panel that does not exist in the scene'); + return; + } - this._sourceProvider = source.state.$data; - if (!this._sourceProvider) { - console.log('No source data found for shared dashboard query'); - return; + this._sourceProvider = source.state.$data; + if (!this._sourceProvider) { + console.log('No source data found for shared dashboard query'); + return; + } } // If the source is not active we need to pass the container width diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx index 7720a5cc5b7..13babf80fad 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx @@ -2,18 +2,14 @@ import React, { PureComponent } from 'react'; import { DataQuery, getDataSourceRef } from '@grafana/data'; import { locationService } from '@grafana/runtime'; +import { storeLastUsedDataSourceInLocalStorage } from 'app/features/datasources/components/picker/utils'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { QueryGroup } from 'app/features/query/components/QueryGroup'; -import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { QueryGroupDataSource, QueryGroupOptions } from 'app/types'; import { getDashboardSrv } from '../../services/DashboardSrv'; import { PanelModel } from '../../state'; -import { - getLastUsedDatasourceFromStorage, - initLastUsedDatasourceKeyForDashboard, - setLastUsedDatasourceKeyForDashboard, -} from '../../utils/dashboard'; +import { getLastUsedDatasourceFromStorage } from '../../utils/dashboard'; interface Props { /** Current panel */ @@ -29,16 +25,7 @@ export class PanelEditorQueries extends PureComponent { // store last used datasource in local storage updateLastUsedDatasource = (datasource: QueryGroupDataSource) => { - if (!datasource.uid) { - return; - } - - const dashboardUid = getDashboardSrv().getCurrent()?.uid ?? ''; - // if datasource is MIXED reset datasource uid in storage, because Mixed datasource can contain multiple ds - if (datasource.uid === MIXED_DATASOURCE_NAME) { - return initLastUsedDatasourceKeyForDashboard(dashboardUid!); - } - setLastUsedDatasourceKeyForDashboard(dashboardUid, datasource.uid); + storeLastUsedDataSourceInLocalStorage(datasource); }; buildQueryOptions(panel: PanelModel): QueryGroupOptions { diff --git a/public/app/features/datasources/components/picker/utils.ts b/public/app/features/datasources/components/picker/utils.ts index f41e2383c58..c626047631c 100644 --- a/public/app/features/datasources/components/picker/utils.ts +++ b/public/app/features/datasources/components/picker/utils.ts @@ -1,4 +1,11 @@ import { DataSourceInstanceSettings, DataSourceJsonData, DataSourceRef } from '@grafana/data'; +import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; +import { + initLastUsedDatasourceKeyForDashboard, + setLastUsedDatasourceKeyForDashboard, +} from 'app/features/dashboard/utils/dashboard'; +import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; +import { QueryGroupDataSource } from 'app/types'; export function isDataSourceMatch( ds: DataSourceInstanceSettings | undefined, @@ -97,3 +104,17 @@ export function getDataSourceCompareFn( export function matchDataSourceWithSearch(ds: DataSourceInstanceSettings, searchTerm = '') { return ds.name.toLowerCase().includes(searchTerm.toLowerCase()); } + +export function storeLastUsedDataSourceInLocalStorage(datasource: QueryGroupDataSource) { + if (!datasource.uid) { + return; + } + + const dashboardUid = getDashboardSrv().getCurrent()?.uid ?? ''; + // if datasource is MIXED reset datasource uid in storage, because Mixed datasource can contain multiple ds + if (datasource.uid === MIXED_DATASOURCE_NAME) { + return initLastUsedDatasourceKeyForDashboard(dashboardUid!); + } + + setLastUsedDatasourceKeyForDashboard(dashboardUid, datasource.uid); +} diff --git a/public/app/features/query/components/QueryGroup.tsx b/public/app/features/query/components/QueryGroup.tsx index 3d58d544110..8b8aab4222c 100644 --- a/public/app/features/query/components/QueryGroup.tsx +++ b/public/app/features/query/components/QueryGroup.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import React, { PureComponent } from 'react'; +import React, { PureComponent, useEffect, useState } from 'react'; import { Unsubscribable } from 'rxjs'; import { @@ -84,11 +84,6 @@ export class QueryGroup extends PureComponent { }); this.setNewQueriesAndDatasource(options); - - // Clean up the first panel flag since the modal is now open - if (!!locationService.getSearchObject().firstPanel) { - locationService.partial({ firstPanel: null }, true); - } } componentWillUnmount() { @@ -213,58 +208,21 @@ export class QueryGroup extends PureComponent { renderTopSection(styles: QueriesTabStyles) { const { onOpenQueryInspector, options } = this.props; - const { dataSource, data } = this.state; + const { dataSource, data, dsSettings } = this.state; + if (!dsSettings || !dataSource) { + return null; + } return ( -
-
- - Data source - -
{this.renderDataSourcePickerWithPrompt()}
- {dataSource && ( - <> -
-
-
- -
- {onOpenQueryInspector && ( -
- -
- )} - - )} -
- {dataSource && isAngularDatasourcePluginAndNotHidden(dataSource.uid) && ( - - )} -
+ ); } @@ -280,32 +238,6 @@ export class QueryGroup extends PureComponent { this.setState({ isDataSourceModalOpen: false }); }; - renderDataSourcePickerWithPrompt = () => { - const { isDataSourceModalOpen } = this.state; - - const commonProps = { - metrics: true, - mixed: true, - dashboard: true, - variables: true, - current: this.props.options.dataSource, - uploadFile: true, - onChange: async (ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => { - await this.onChangeDataSource(ds, defaultQueries); - this.onCloseDataSourceModal(); - }, - }; - - return ( - <> - {isDataSourceModalOpen && config.featureToggles.advancedDataSourcePicker && ( - - )} - - - ); - }; - onAddQuery = (query: Partial) => { const { dsSettings, queries } = this.state; this.onQueriesChange(addQuery(queries, query, { type: dsSettings?.type, uid: dsSettings?.uid })); @@ -452,3 +384,134 @@ const getStyles = stylesFactory(() => { }); type QueriesTabStyles = ReturnType; + +interface QueryGroupTopSectionProps { + data: PanelData; + dataSource: DataSourceApi; + dsSettings: DataSourceInstanceSettings; + options: QueryGroupOptions; + onOpenQueryInspector?: () => void; + onOptionsChange?: (options: QueryGroupOptions) => void; + onDataSourceChange?: (ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => Promise; +} + +export function QueryGroupTopSection({ + dataSource, + options, + data, + dsSettings, + onDataSourceChange, + onOptionsChange, + onOpenQueryInspector, +}: QueryGroupTopSectionProps) { + const styles = getStyles(); + const [isHelpOpen, setIsHelpOpen] = useState(false); + return ( + <> +
+
+ + Data source + +
+ { + return await onDataSourceChange?.(ds, defaultQueries); + }} + isDataSourceModalOpen={Boolean(locationService.getSearchObject().firstPanel)} + /> +
+ {dataSource && ( + <> +
+
+
+ { + onOptionsChange?.(opts); + }} + /> +
+ {onOpenQueryInspector && ( +
+ +
+ )} + + )} +
+ {dataSource && isAngularDatasourcePluginAndNotHidden(dataSource.uid) && ( + + )} +
+ {isHelpOpen && ( + setIsHelpOpen(false)}> + + + )} + + ); +} + +interface DataSourcePickerWithPromptProps { + isDataSourceModalOpen?: boolean; + options: QueryGroupOptions; + onChange: (ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => Promise; +} + +function DataSourcePickerWithPrompt({ options, onChange, ...otherProps }: DataSourcePickerWithPromptProps) { + const [isDataSourceModalOpen, setIsDataSourceModalOpen] = useState(Boolean(otherProps.isDataSourceModalOpen)); + + useEffect(() => { + // Clean up the first panel flag since the modal is now open + if (!!locationService.getSearchObject().firstPanel) { + locationService.partial({ firstPanel: null }, true); + } + }, []); + + const commonProps = { + metrics: true, + mixed: true, + dashboard: true, + variables: true, + current: options.dataSource, + uploadFile: true, + onChange: async (ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => { + await onChange(ds, defaultQueries); + setIsDataSourceModalOpen(false); + }, + }; + + return ( + <> + {isDataSourceModalOpen && config.featureToggles.advancedDataSourcePicker && ( + setIsDataSourceModalOpen(false)}> + )} + + + + ); +} diff --git a/public/app/plugins/datasource/loki/components/LokiContextUi.test.tsx b/public/app/plugins/datasource/loki/components/LokiContextUi.test.tsx index 2ad3c1b4949..db1207a9b9a 100644 --- a/public/app/plugins/datasource/loki/components/LokiContextUi.test.tsx +++ b/public/app/plugins/datasource/loki/components/LokiContextUi.test.tsx @@ -19,6 +19,7 @@ jest.mock('@grafana/runtime', () => ({ jest.mock('app/core/store', () => { return { set() {}, + get() {}, getBool(key: string, defaultValue?: boolean) { const item = window.localStorage.getItem(key); if (item === null) { diff --git a/yarn.lock b/yarn.lock index 4e77669d53a..cc9b122b1ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3290,9 +3290,9 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes@npm:1.27.0": - version: 1.27.0 - resolution: "@grafana/scenes@npm:1.27.0" +"@grafana/scenes@npm:1.28.0": + version: 1.28.0 + resolution: "@grafana/scenes@npm:1.28.0" dependencies: "@grafana/e2e-selectors": "npm:10.0.2" react-grid-layout: "npm:1.3.4" @@ -3304,7 +3304,7 @@ __metadata: "@grafana/runtime": 10.0.3 "@grafana/schema": 10.0.3 "@grafana/ui": 10.0.3 - checksum: e19aa28d6297316676cb4805dde9bc3a52d2d28a3231e2fcdedf610356fc5c1b24eecfec36e8089cf4defc388705d47fce32736f8a86c9176e5bd4e4b7da9acb + checksum: 0973206c4485cad15ceb41f031e96e0f1f075be24570f527bbcb17dd56d5cd362385c04acef8f7aa240c3bb8b045d2270fab2dbb2f18e7e2850ab67a13a3d268 languageName: node linkType: hard @@ -17317,7 +17317,7 @@ __metadata: "@grafana/lezer-traceql": "npm:0.0.12" "@grafana/monaco-logql": "npm:^0.0.7" "@grafana/runtime": "workspace:*" - "@grafana/scenes": "npm:1.27.0" + "@grafana/scenes": "npm:1.28.0" "@grafana/schema": "workspace:*" "@grafana/tsconfig": "npm:^1.3.0-rc1" "@grafana/ui": "workspace:*"