diff --git a/.betterer.results b/.betterer.results
index b5d57820a1c..b05e872e997 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -4554,6 +4554,9 @@ exports[`better eslint`] = {
"public/app/features/sandbox/TestStuffPage.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
+ "public/app/features/scenes/components/VizPanel/VizPanelRenderer.tsx:5381": [
+ [0, 0, 0, "Do not use any type assertions.", "0"]
+ ],
"public/app/features/scenes/components/layout/SceneFlexLayout.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
diff --git a/public/app/features/panel/components/PanelRenderer.tsx b/public/app/features/panel/components/PanelRenderer.tsx
index f05db6fac24..b841e210c00 100644
--- a/public/app/features/panel/components/PanelRenderer.tsx
+++ b/public/app/features/panel/components/PanelRenderer.tsx
@@ -38,7 +38,7 @@ export function PanelRenderer
(pr
const [plugin, setPlugin] = useState(syncGetPanelPlugin(pluginId));
const [error, setError] = useState();
const optionsWithDefaults = useOptionDefaults(plugin, options, fieldConfig);
- const dataWithOverrides = useFieldOverrides(plugin, optionsWithDefaults, data, timeZone);
+ const dataWithOverrides = useFieldOverrides(plugin, optionsWithDefaults?.fieldConfig, data, timeZone);
useEffect(() => {
// If we already have a plugin and it's correct one do nothing
@@ -117,13 +117,12 @@ function useOptionDefaults(
}, [plugin, fieldConfig, options]);
}
-function useFieldOverrides(
+export function useFieldOverrides(
plugin: PanelPlugin | undefined,
- defaultOptions: OptionDefaults | undefined,
+ fieldConfig: FieldConfigSource | undefined,
data: PanelData | undefined,
timeZone: string
): PanelData | undefined {
- const fieldConfig = defaultOptions?.fieldConfig;
const fieldConfigRegistry = plugin?.fieldConfigRegistry;
const theme = useTheme2();
const structureRev = useRef(0);
diff --git a/public/app/features/plugins/__mocks__/pluginMocks.ts b/public/app/features/plugins/__mocks__/pluginMocks.ts
index 372e2f49c2e..b59d49777b3 100644
--- a/public/app/features/plugins/__mocks__/pluginMocks.ts
+++ b/public/app/features/plugins/__mocks__/pluginMocks.ts
@@ -63,7 +63,7 @@ export function getPanelPlugin(
version: '',
},
hideFromList: options.hideFromList === true,
- module: '',
+ module: options.module ?? '',
baseUrl: '',
};
return plugin;
diff --git a/public/app/features/scenes/components/VizPanel.tsx b/public/app/features/scenes/components/VizPanel.tsx
deleted file mode 100644
index 2ffb21bfd90..00000000000
--- a/public/app/features/scenes/components/VizPanel.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-import React from 'react';
-import AutoSizer from 'react-virtualized-auto-sizer';
-
-import { AbsoluteTimeRange, FieldConfigSource, toUtc } from '@grafana/data';
-import { PanelRenderer } from '@grafana/runtime';
-import { Field, PanelChrome, Input } from '@grafana/ui';
-
-import { SceneObjectBase } from '../core/SceneObjectBase';
-import { sceneGraph } from '../core/sceneGraph';
-import { SceneComponentProps, SceneLayoutChildState } from '../core/types';
-import { VariableDependencyConfig } from '../variables/VariableDependencyConfig';
-
-import { SceneDragHandle } from './SceneDragHandle';
-
-export interface VizPanelState extends SceneLayoutChildState {
- title?: string;
- pluginId: string;
- options?: object;
- fieldConfig?: FieldConfigSource;
-}
-
-export class VizPanel extends SceneObjectBase {
- public static Component = ScenePanelRenderer;
- public static Editor = VizPanelEditor;
-
- protected _variableDependency = new VariableDependencyConfig(this, {
- statePaths: ['title'],
- });
-
- public onSetTimeRange = (timeRange: AbsoluteTimeRange) => {
- const sceneTimeRange = sceneGraph.getTimeRange(this);
- sceneTimeRange.setState({
- raw: {
- from: toUtc(timeRange.from),
- to: toUtc(timeRange.to),
- },
- from: toUtc(timeRange.from),
- to: toUtc(timeRange.to),
- });
- };
-}
-
-function ScenePanelRenderer({ model }: SceneComponentProps) {
- const { title, pluginId, options, fieldConfig, ...state } = model.useState();
- const { data } = sceneGraph.getData(model).useState();
-
- const layout = sceneGraph.getLayout(model);
- const isDraggable = layout.state.isDraggable ? state.isDraggable : false;
- const dragHandle = ;
-
- const titleInterpolated = sceneGraph.interpolate(model, title);
-
- return (
-
- {({ width, height }) => {
- if (width < 3 || height < 3) {
- return null;
- }
-
- return (
-
- {(innerWidth, innerHeight) => (
- <>
- {}}
- onChangeTimeRange={model.onSetTimeRange}
- />
- >
- )}
-
- );
- }}
-
- );
-}
-
-ScenePanelRenderer.displayName = 'ScenePanelRenderer';
-
-function VizPanelEditor({ model }: SceneComponentProps) {
- const { title } = model.useState();
-
- return (
-
- model.setState({ title: evt.currentTarget.value })} />
-
- );
-}
diff --git a/public/app/features/scenes/components/VizPanel/VizPanel.test.tsx b/public/app/features/scenes/components/VizPanel/VizPanel.test.tsx
new file mode 100644
index 00000000000..cdbe87edff6
--- /dev/null
+++ b/public/app/features/scenes/components/VizPanel/VizPanel.test.tsx
@@ -0,0 +1,136 @@
+import React from 'react';
+
+import { FieldConfigProperty, PanelPlugin } from '@grafana/data';
+import { getPanelPlugin } from 'app/features/plugins/__mocks__/pluginMocks';
+
+import { VizPanel } from './VizPanel';
+
+let pluginToLoad: PanelPlugin | undefined;
+
+jest.mock('app/features/plugins/importPanelPlugin', () => ({
+ syncGetPanelPlugin: jest.fn(() => pluginToLoad),
+}));
+
+interface OptionsPlugin1 {
+ showThresholds: boolean;
+ option2?: string;
+}
+
+interface FieldConfigPlugin1 {
+ customProp?: boolean;
+ customProp2?: boolean;
+ junkProp?: boolean;
+}
+
+function getTestPlugin1() {
+ const pluginToLoad = getPanelPlugin(
+ {
+ id: 'custom-plugin-id',
+ },
+ () => My custom panel
+ );
+
+ pluginToLoad.meta.info.version = '1.0.0';
+ pluginToLoad.setPanelOptions((builder) => {
+ builder.addBooleanSwitch({
+ name: 'Show thresholds',
+ path: 'showThresholds',
+ defaultValue: true,
+ });
+ builder.addTextInput({
+ name: 'option2',
+ path: 'option2',
+ defaultValue: undefined,
+ });
+ });
+
+ pluginToLoad.useFieldConfig({
+ standardOptions: {
+ [FieldConfigProperty.Unit]: {
+ defaultValue: 'flop',
+ },
+ [FieldConfigProperty.Decimals]: {
+ defaultValue: 2,
+ },
+ },
+ useCustomConfig: (builder) => {
+ builder.addBooleanSwitch({
+ name: 'CustomProp',
+ path: 'customProp',
+ defaultValue: false,
+ });
+ builder.addBooleanSwitch({
+ name: 'customProp2',
+ path: 'customProp2',
+ defaultValue: false,
+ });
+ },
+ });
+
+ pluginToLoad.setMigrationHandler((panel) => {
+ if (panel.fieldConfig.defaults.custom) {
+ panel.fieldConfig.defaults.custom.customProp2 = true;
+ }
+
+ return { option2: 'hello' };
+ });
+
+ return pluginToLoad;
+}
+
+describe('VizPanel', () => {
+ describe('when activated', () => {
+ let panel: VizPanel;
+
+ beforeAll(async () => {
+ panel = new VizPanel({
+ pluginId: 'custom-plugin-id',
+ fieldConfig: {
+ defaults: { custom: { junkProp: true } },
+ overrides: [],
+ },
+ });
+
+ pluginToLoad = getTestPlugin1();
+ panel.activate();
+ });
+
+ it('load plugin', () => {
+ expect(panel.getPlugin()).toBe(pluginToLoad);
+ });
+
+ it('should call panel migration handler', () => {
+ expect(panel.state.options.option2).toEqual('hello');
+ expect(panel.state.fieldConfig.defaults.custom?.customProp2).toEqual(true);
+ });
+
+ it('should apply option defaults', () => {
+ expect(panel.state.options.showThresholds).toEqual(true);
+ });
+
+ it('should apply fieldConfig defaults', () => {
+ expect(panel.state.fieldConfig.defaults.unit).toBe('flop');
+ expect(panel.state.fieldConfig.defaults.custom!.customProp).toBe(false);
+ });
+
+ it('should should remove props that are not defined for plugin', () => {
+ expect(panel.state.fieldConfig.defaults.custom?.junkProp).toEqual(undefined);
+ });
+ });
+
+ describe('When calling on onPanelMigration', () => {
+ const onPanelMigration = jest.fn();
+ let panel: VizPanel;
+
+ beforeAll(async () => {
+ panel = new VizPanel({ pluginId: 'custom-plugin-id' });
+ pluginToLoad = getTestPlugin1();
+ pluginToLoad.onPanelMigration = onPanelMigration;
+ panel.activate();
+ });
+
+ it('should call onPanelMigration with pluginVersion set to initial state (undefined)', () => {
+ expect(onPanelMigration.mock.calls[0][0].pluginVersion).toBe(undefined);
+ });
+ });
+});
diff --git a/public/app/features/scenes/components/VizPanel/VizPanel.tsx b/public/app/features/scenes/components/VizPanel/VizPanel.tsx
new file mode 100644
index 00000000000..c468b27709c
--- /dev/null
+++ b/public/app/features/scenes/components/VizPanel/VizPanel.tsx
@@ -0,0 +1,125 @@
+import React from 'react';
+
+import { AbsoluteTimeRange, FieldConfigSource, PanelModel, PanelPlugin, toUtc } from '@grafana/data';
+import { config } from '@grafana/runtime';
+import { Field, Input } from '@grafana/ui';
+import { importPanelPlugin, syncGetPanelPlugin } from 'app/features/plugins/importPanelPlugin';
+
+import { getPanelOptionsWithDefaults } from '../../../dashboard/state/getPanelOptionsWithDefaults';
+import { SceneObjectBase } from '../../core/SceneObjectBase';
+import { sceneGraph } from '../../core/sceneGraph';
+import { SceneComponentProps, SceneLayoutChildState } from '../../core/types';
+
+import { VizPanelRenderer } from './VizPanelRenderer';
+
+export interface VizPanelState extends SceneLayoutChildState {
+ title: string;
+ pluginId: string;
+ options: TOptions;
+ fieldConfig: FieldConfigSource;
+ pluginVersion?: string;
+ // internal state
+ pluginLoadError?: string;
+}
+
+export class VizPanel extends SceneObjectBase<
+ VizPanelState, TFieldConfig>
+> {
+ public static Component = VizPanelRenderer;
+ public static Editor = VizPanelEditor;
+
+ // Not part of state as this is not serializable
+ private _plugin?: PanelPlugin;
+
+ public constructor(state: Partial, TFieldConfig>>) {
+ super({
+ options: {},
+ fieldConfig: { defaults: {}, overrides: [] },
+ title: 'Title',
+ pluginId: 'timeseries',
+ ...state,
+ });
+ }
+
+ public activate() {
+ super.activate();
+
+ const plugin = syncGetPanelPlugin(this.state.pluginId);
+
+ if (plugin) {
+ this.pluginLoaded(plugin);
+ } else {
+ importPanelPlugin(this.state.pluginId)
+ .then((result) => this.pluginLoaded(result))
+ .catch((err: Error) => {
+ this.setState({ pluginLoadError: err.message });
+ });
+ }
+ }
+
+ private pluginLoaded(plugin: PanelPlugin) {
+ const { options, fieldConfig, title, pluginId, pluginVersion } = this.state;
+
+ const panel: PanelModel = { title, options, fieldConfig, id: 1, type: pluginId, pluginVersion: pluginVersion };
+ const currentVersion = this.getPluginVersion(plugin);
+
+ if (plugin.onPanelMigration) {
+ if (currentVersion !== this.state.pluginVersion) {
+ // These migration handlers also mutate panel.fieldConfig to migrate fieldConfig
+ panel.options = plugin.onPanelMigration(panel);
+ }
+ }
+
+ const withDefaults = getPanelOptionsWithDefaults({
+ plugin,
+ currentOptions: panel.options,
+ currentFieldConfig: panel.fieldConfig,
+ isAfterPluginChange: false,
+ });
+
+ this._plugin = plugin;
+ this.setState({
+ options: withDefaults.options,
+ fieldConfig: withDefaults.fieldConfig,
+ pluginVersion: currentVersion,
+ });
+ }
+
+ private getPluginVersion(plugin: PanelPlugin): string {
+ return plugin && plugin.meta.info.version ? plugin.meta.info.version : config.buildInfo.version;
+ }
+
+ public getPlugin(): PanelPlugin | undefined {
+ return this._plugin;
+ }
+
+ public onChangeTimeRange = (timeRange: AbsoluteTimeRange) => {
+ const sceneTimeRange = sceneGraph.getTimeRange(this);
+ sceneTimeRange.setState({
+ raw: {
+ from: toUtc(timeRange.from),
+ to: toUtc(timeRange.to),
+ },
+ from: toUtc(timeRange.from),
+ to: toUtc(timeRange.to),
+ });
+ };
+
+ public onOptionsChange = (options: TOptions) => {
+ this.setState({ options });
+ };
+
+ public onFieldConfigChange = (fieldConfig: FieldConfigSource) => {
+ this.setState({ fieldConfig });
+ };
+}
+
+function VizPanelEditor({ model }: SceneComponentProps) {
+ const { title } = model.useState();
+
+ return (
+
+ model.setState({ title: evt.currentTarget.value })} />
+
+ );
+}
diff --git a/public/app/features/scenes/components/VizPanel/VizPanelRenderer.tsx b/public/app/features/scenes/components/VizPanel/VizPanelRenderer.tsx
new file mode 100644
index 00000000000..a120f3fda44
--- /dev/null
+++ b/public/app/features/scenes/components/VizPanel/VizPanelRenderer.tsx
@@ -0,0 +1,94 @@
+import React, { RefCallback } from 'react';
+import { useMeasure } from 'react-use';
+
+import { PluginContextProvider } from '@grafana/data';
+import { PanelChrome, ErrorBoundaryAlert } from '@grafana/ui';
+import { appEvents } from 'app/core/core';
+import { useFieldOverrides } from 'app/features/panel/components/PanelRenderer';
+
+import { sceneGraph } from '../../core/sceneGraph';
+import { SceneComponentProps } from '../../core/types';
+import { SceneQueryRunner } from '../../querying/SceneQueryRunner';
+import { SceneDragHandle } from '../SceneDragHandle';
+
+import { VizPanel } from './VizPanel';
+
+export function VizPanelRenderer({ model }: SceneComponentProps) {
+ const { title, options, fieldConfig, pluginId, pluginLoadError, $data, ...state } = model.useState();
+ const [ref, { width, height }] = useMeasure();
+ const plugin = model.getPlugin();
+ const { data } = sceneGraph.getData(model).useState();
+ const layout = sceneGraph.getLayout(model);
+
+ const isDraggable = layout.state.isDraggable ? state.isDraggable : false;
+ const dragHandle = ;
+
+ const titleInterpolated = sceneGraph.interpolate(model, title);
+
+ // Not sure we need to subscribe to this state
+ const timeZone = sceneGraph.getTimeRange(model).state.timeZone;
+
+ const dataWithOverrides = useFieldOverrides(plugin, fieldConfig, data, timeZone);
+
+ if (pluginLoadError) {
+ return Failed to load plugin: {pluginLoadError}
;
+ }
+
+ if (!plugin || !plugin.hasPluginId(pluginId)) {
+ return Loading plugin panel...
;
+ }
+
+ if (!plugin.panel) {
+ return Panel plugin has no panel component
;
+ }
+
+ const PanelComponent = plugin.panel;
+
+ // Query runner needs to with for auto maxDataPoints
+ if ($data instanceof SceneQueryRunner) {
+ $data.setContainerWidth(width);
+ }
+
+ return (
+ } style={{ width: '100%', height: '100%' }}>
+
+ {(innerWidth, innerHeight) => (
+ <>
+ {!dataWithOverrides && No data...
}
+ {dataWithOverrides && (
+
+
+ str}
+ onOptionsChange={model.onOptionsChange}
+ onFieldConfigChange={model.onFieldConfigChange}
+ onChangeTimeRange={model.onChangeTimeRange}
+ eventBus={appEvents}
+ />
+
+
+ )}
+ >
+ )}
+
+
+ );
+}
+
+VizPanelRenderer.displayName = 'ScenePanelRenderer';
diff --git a/public/app/features/scenes/components/index.ts b/public/app/features/scenes/components/index.ts
new file mode 100644
index 00000000000..89aafc6064f
--- /dev/null
+++ b/public/app/features/scenes/components/index.ts
@@ -0,0 +1,10 @@
+export { VizPanel } from './VizPanel/VizPanel';
+export { NestedScene } from './NestedScene';
+export { Scene } from './Scene';
+export { SceneCanvasText } from './SceneCanvasText';
+export { SceneToolbarButton, SceneToolbarInput } from './SceneToolbarButton';
+export { SceneTimePicker } from './SceneTimePicker';
+export { ScenePanelRepeater } from './ScenePanelRepeater';
+export { SceneSubMenu } from './SceneSubMenu';
+export { SceneFlexLayout } from './layout/SceneFlexLayout';
+export { SceneGridLayout, SceneGridRow } from './layout/SceneGridLayout';
diff --git a/public/app/features/scenes/core/SceneTimeRange.tsx b/public/app/features/scenes/core/SceneTimeRange.tsx
index 596b2337637..c790d5f739c 100644
--- a/public/app/features/scenes/core/SceneTimeRange.tsx
+++ b/public/app/features/scenes/core/SceneTimeRange.tsx
@@ -1,9 +1,17 @@
-import { TimeRange, UrlQueryMap } from '@grafana/data';
+import { getDefaultTimeRange, getTimeZone, TimeRange, UrlQueryMap } from '@grafana/data';
import { SceneObjectBase } from './SceneObjectBase';
import { SceneObjectWithUrlSync, SceneTimeRangeState } from './types';
export class SceneTimeRange extends SceneObjectBase implements SceneObjectWithUrlSync {
+ public constructor(state: Partial = {}) {
+ super({
+ ...getDefaultTimeRange(),
+ timeZone: getTimeZone(),
+ ...state,
+ });
+ }
+
public onTimeRangeChange = (timeRange: TimeRange) => {
this.setState(timeRange);
};
diff --git a/public/app/features/scenes/core/sceneGraph.ts b/public/app/features/scenes/core/sceneGraph.ts
index 6cd7e4606d4..0cee433477e 100644
--- a/public/app/features/scenes/core/sceneGraph.ts
+++ b/public/app/features/scenes/core/sceneGraph.ts
@@ -108,7 +108,7 @@ export const EmptyDataNode = new SceneDataNode({
},
});
-export const DefaultTimeRange = new SceneTimeRangeImpl(getDefaultTimeRange());
+export const DefaultTimeRange = new SceneTimeRangeImpl();
export const sceneGraph = {
getVariables,
diff --git a/public/app/features/scenes/core/types.ts b/public/app/features/scenes/core/types.ts
index d818e5bba4b..3363da75d0d 100644
--- a/public/app/features/scenes/core/types.ts
+++ b/public/app/features/scenes/core/types.ts
@@ -1,7 +1,7 @@
import React from 'react';
import { Observer, Subscription, Unsubscribable } from 'rxjs';
-import { BusEvent, BusEventHandler, BusEventType, PanelData, TimeRange, UrlQueryMap } from '@grafana/data';
+import { BusEvent, BusEventHandler, BusEventType, PanelData, TimeRange, TimeZone, UrlQueryMap } from '@grafana/data';
import { SceneVariableDependencyConfigLike, SceneVariables } from '../variables/types';
@@ -128,7 +128,9 @@ interface SceneComponentEditWrapperProps {
children: React.ReactNode;
}
-export interface SceneTimeRangeState extends SceneObjectStatePlain, TimeRange {}
+export interface SceneTimeRangeState extends SceneObjectStatePlain, TimeRange {
+ timeZone: TimeZone;
+}
export interface SceneTimeRange extends SceneObject {
onTimeRangeChange(timeRange: TimeRange): void;
diff --git a/public/app/features/scenes/dashboard/DashboardsLoader.ts b/public/app/features/scenes/dashboard/DashboardsLoader.ts
index 7d56b5a78d5..94197ca42d3 100644
--- a/public/app/features/scenes/dashboard/DashboardsLoader.ts
+++ b/public/app/features/scenes/dashboard/DashboardsLoader.ts
@@ -1,12 +1,9 @@
-import { getDefaultTimeRange } from '@grafana/data';
import { StateManagerBase } from 'app/core/services/StateManagerBase';
import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { DashboardDTO } from 'app/types';
-import { SceneTimePicker } from '../components/SceneTimePicker';
-import { VizPanel } from '../components/VizPanel';
-import { SceneGridLayout, SceneGridRow } from '../components/layout/SceneGridLayout';
+import { VizPanel, SceneTimePicker, SceneGridLayout, SceneGridRow } from '../components';
import { SceneTimeRange } from '../core/SceneTimeRange';
import { SceneObject } from '../core/types';
import { SceneQueryRunner } from '../querying/SceneQueryRunner';
@@ -54,7 +51,7 @@ export class DashboardLoader extends StateManagerBase {
layout: new SceneGridLayout({
children: this.buildSceneObjectsFromDashboard(oldModel),
}),
- $timeRange: new SceneTimeRange(getDefaultTimeRange()),
+ $timeRange: new SceneTimeRange(),
actions: [new SceneTimePicker({})],
});
@@ -83,26 +80,7 @@ export class DashboardLoader extends StateManagerBase {
size: {
y: panel.gridPos.y,
},
- children: panel.panels
- ? panel.panels.map(
- (p) =>
- new VizPanel({
- title: p.title,
- pluginId: p.type,
- size: {
- x: p.gridPos.x,
- y: p.gridPos.y,
- width: p.gridPos.w,
- height: p.gridPos.h,
- },
- options: p.options,
- fieldConfig: p.fieldConfig,
- $data: new SceneQueryRunner({
- queries: p.targets,
- }),
- })
- )
- : [],
+ children: panel.panels ? panel.panels.map(createVizPanelFromPanelModel) : [],
})
);
} else {
@@ -128,21 +106,7 @@ export class DashboardLoader extends StateManagerBase {
}
}
} else {
- const panelObject = new VizPanel({
- title: panel.title,
- pluginId: panel.type,
- size: {
- x: panel.gridPos.x,
- y: panel.gridPos.y,
- width: panel.gridPos.w,
- height: panel.gridPos.h,
- },
- options: panel.options,
- fieldConfig: panel.fieldConfig,
- $data: new SceneQueryRunner({
- queries: panel.targets,
- }),
- });
+ const panelObject = createVizPanelFromPanelModel(panel);
// when processing an expanded row, collect its panels
if (currentRow) {
@@ -170,6 +134,25 @@ export class DashboardLoader extends StateManagerBase {
}
}
+function createVizPanelFromPanelModel(panel: PanelModel) {
+ return new VizPanel({
+ title: panel.title,
+ pluginId: panel.type,
+ size: {
+ x: panel.gridPos.x,
+ y: panel.gridPos.y,
+ width: panel.gridPos.w,
+ height: panel.gridPos.h,
+ },
+ options: panel.options,
+ fieldConfig: panel.fieldConfig,
+ pluginVersion: panel.pluginVersion,
+ $data: new SceneQueryRunner({
+ queries: panel.targets,
+ }),
+ });
+}
+
let loader: DashboardLoader | null = null;
export function getDashboardLoader(): DashboardLoader {
diff --git a/public/app/features/scenes/querying/SceneQueryRunner.test.ts b/public/app/features/scenes/querying/SceneQueryRunner.test.ts
new file mode 100644
index 00000000000..2e3de13a862
--- /dev/null
+++ b/public/app/features/scenes/querying/SceneQueryRunner.test.ts
@@ -0,0 +1,83 @@
+import { of } from 'rxjs';
+
+import { DataQueryRequest, DataSourceApi, getDefaultTimeRange, LoadingState, PanelData } from '@grafana/data';
+
+import { SceneTimeRange } from '../core/SceneTimeRange';
+
+import { SceneQueryRunner } from './SceneQueryRunner';
+
+const getDatasource = () => {
+ return {
+ getRef: () => ({ uid: 'test' }),
+ };
+};
+
+jest.mock('app/features/plugins/datasource_srv', () => ({
+ getDatasourceSrv: jest.fn(() => ({
+ get: getDatasource,
+ })),
+}));
+
+const runRequest = jest.fn().mockReturnValue(
+ of({
+ state: LoadingState.Done,
+ series: [],
+ timeRange: getDefaultTimeRange(),
+ })
+);
+
+let sentRequest: DataQueryRequest | undefined;
+
+jest.mock('app/features/query/state/runRequest', () => ({
+ runRequest: (ds: DataSourceApi, request: DataQueryRequest) => {
+ sentRequest = request;
+ return runRequest(ds, request);
+ },
+}));
+
+describe('SceneQueryRunner', () => {
+ describe('when activated and got no data', () => {
+ it('should run queries', async () => {
+ const queryRunner = new SceneQueryRunner({
+ queries: [{ refId: 'A' }],
+ $timeRange: new SceneTimeRange(),
+ });
+
+ expect(queryRunner.state.data).toBeUndefined();
+
+ queryRunner.activate();
+
+ await new Promise((r) => setTimeout(r, 1));
+
+ expect(queryRunner.state.data?.state).toBe(LoadingState.Done);
+ // Default max data points
+ expect(sentRequest?.maxDataPoints).toBe(500);
+ });
+ });
+
+ describe('when activated and maxDataPointsFromWidth set to true', () => {
+ it('should run queries', async () => {
+ const queryRunner = new SceneQueryRunner({
+ queries: [{ refId: 'A' }],
+ $timeRange: new SceneTimeRange(),
+ maxDataPointsFromWidth: true,
+ });
+
+ expect(queryRunner.state.data).toBeUndefined();
+
+ queryRunner.activate();
+
+ await new Promise((r) => setTimeout(r, 1));
+
+ expect(queryRunner.state.data?.state).toBeUndefined();
+
+ queryRunner.setContainerWidth(1000);
+
+ expect(queryRunner.state.data?.state).toBeUndefined();
+
+ await new Promise((r) => setTimeout(r, 1));
+
+ expect(queryRunner.state.data?.state).toBe(LoadingState.Done);
+ });
+ });
+});
diff --git a/public/app/features/scenes/querying/SceneQueryRunner.ts b/public/app/features/scenes/querying/SceneQueryRunner.ts
index ba9312f6c43..bd93f611f42 100644
--- a/public/app/features/scenes/querying/SceneQueryRunner.ts
+++ b/public/app/features/scenes/querying/SceneQueryRunner.ts
@@ -24,6 +24,11 @@ import { VariableDependencyConfig } from '../variables/VariableDependencyConfig'
export interface QueryRunnerState extends SceneObjectStatePlain {
data?: PanelData;
queries: DataQueryExtended[];
+ datasource?: DataSourceRef;
+ minInterval?: string;
+ maxDataPoints?: number;
+ // Non persisted state
+ maxDataPointsFromWidth?: boolean;
}
export interface DataQueryExtended extends DataQuery {
@@ -31,7 +36,8 @@ export interface DataQueryExtended extends DataQuery {
}
export class SceneQueryRunner extends SceneObjectBase {
- private querySub?: Unsubscribable;
+ private _querySub?: Unsubscribable;
+ private _containerWidth?: number;
protected _variableDependency = new VariableDependencyConfig(this, {
statePaths: ['queries'],
@@ -51,17 +57,52 @@ export class SceneQueryRunner extends SceneObjectBase {
})
);
- if (!this.state.data) {
+ if (this.shouldRunQueriesOnActivate()) {
this.runQueries();
}
}
+ private shouldRunQueriesOnActivate() {
+ // If we already have data, no need
+ // TODO validate that time range is similar and if not we should run queries again
+ if (this.state.data) {
+ return false;
+ }
+
+ // If no maxDataPoints specified we need might to wait for container width to be set from the outside
+ if (!this.state.maxDataPoints && this.state.maxDataPointsFromWidth && !this._containerWidth) {
+ return false;
+ }
+
+ return true;
+ }
+
public deactivate(): void {
super.deactivate();
- if (this.querySub) {
- this.querySub.unsubscribe();
- this.querySub = undefined;
+ if (this._querySub) {
+ this._querySub.unsubscribe();
+ this._querySub = undefined;
+ }
+ }
+
+ public setContainerWidth(width: number) {
+ // If we don't have a width we should run queries
+ if (!this._containerWidth) {
+ this._containerWidth = width;
+
+ // If we don't have maxDataPoints specifically set and maxDataPointsFromWidth is true
+ if (this.state.maxDataPointsFromWidth && !this.state.maxDataPoints) {
+ // As this is called from render path we need to wait for next tick before running queries
+ setTimeout(() => {
+ if (this.isActive && !this._querySub) {
+ this.runQueries();
+ }
+ }, 0);
+ }
+ } else {
+ // let's just remember the width until next query issue
+ this._containerWidth = width;
}
}
@@ -70,8 +111,12 @@ export class SceneQueryRunner extends SceneObjectBase {
this.runWithTimeRange(timeRange.state);
}
+ private getMaxDataPoints() {
+ return this.state.maxDataPoints ?? this._containerWidth ?? 500;
+ }
+
private async runWithTimeRange(timeRange: TimeRange) {
- const queries = cloneDeep(this.state.queries);
+ const { datasource, minInterval, queries } = this.state;
const request: DataQueryRequest = {
app: CoreApp.Dashboard,
@@ -82,14 +127,14 @@ export class SceneQueryRunner extends SceneObjectBase {
range: timeRange,
interval: '1s',
intervalMs: 1000,
- targets: cloneDeep(this.state.queries),
- maxDataPoints: 500,
+ targets: cloneDeep(queries),
+ maxDataPoints: this.getMaxDataPoints(),
scopedVars: {},
startTime: Date.now(),
};
try {
- const ds = await getDataSource(queries[0].datasource!, request.scopedVars);
+ const ds = await getDataSource(datasource, request.scopedVars);
// Attach the data source name to each query
request.targets = request.targets.map((query) => {
@@ -99,8 +144,9 @@ export class SceneQueryRunner extends SceneObjectBase {
return query;
});
- const lowerIntervalLimit = ds.interval;
- const norm = rangeUtil.calculateInterval(timeRange, request.maxDataPoints ?? 1000, lowerIntervalLimit);
+ // TODO interpolate minInterval
+ const lowerIntervalLimit = minInterval ? minInterval : ds.interval;
+ const norm = rangeUtil.calculateInterval(timeRange, request.maxDataPoints!, lowerIntervalLimit);
// make shallow copy of scoped vars,
// and add built in variables interval and interval_ms
@@ -112,22 +158,20 @@ export class SceneQueryRunner extends SceneObjectBase {
request.interval = norm.interval;
request.intervalMs = norm.intervalMs;
- this.querySub = runRequest(ds, request).subscribe({
- next: (data) => {
- console.log('set data', data, data.state);
- this.setState({ data });
- },
+ this._querySub = runRequest(ds, request).subscribe({
+ next: this.onDataReceived,
});
} catch (err) {
console.error('PanelQueryRunner Error', err);
}
}
+
+ private onDataReceived = (data: PanelData) => {
+ this.setState({ data });
+ };
}
-async function getDataSource(
- datasource: DataSourceRef | string | DataSourceApi | null,
- scopedVars: ScopedVars
-): Promise {
+async function getDataSource(datasource: DataSourceRef | undefined, scopedVars: ScopedVars): Promise {
if (datasource && (datasource as any).query) {
return datasource as DataSourceApi;
}
diff --git a/public/app/features/scenes/scenes/demo.tsx b/public/app/features/scenes/scenes/demo.tsx
index a4e96b97a70..bbc6bbc09f2 100644
--- a/public/app/features/scenes/scenes/demo.tsx
+++ b/public/app/features/scenes/scenes/demo.tsx
@@ -1,12 +1,12 @@
-import { getDefaultTimeRange } from '@grafana/data';
-
-import { Scene } from '../components/Scene';
-import { SceneCanvasText } from '../components/SceneCanvasText';
-import { ScenePanelRepeater } from '../components/ScenePanelRepeater';
-import { SceneTimePicker } from '../components/SceneTimePicker';
-import { SceneToolbarInput } from '../components/SceneToolbarButton';
-import { VizPanel } from '../components/VizPanel';
-import { SceneFlexLayout } from '../components/layout/SceneFlexLayout';
+import {
+ Scene,
+ SceneCanvasText,
+ ScenePanelRepeater,
+ SceneTimePicker,
+ SceneToolbarInput,
+ SceneFlexLayout,
+ VizPanel,
+} from '../components';
import { SceneTimeRange } from '../core/SceneTimeRange';
import { SceneEditManager } from '../editor/SceneEditManager';
@@ -22,8 +22,8 @@ export function getFlexLayoutTest(): Scene {
size: { minWidth: '70%' },
pluginId: 'timeseries',
title: 'Dynamic height and width',
+ $data: getQueryRunnerWithRandomWalkQuery({}, { maxDataPointsFromWidth: true }),
}),
-
new SceneFlexLayout({
direction: 'column',
children: [
@@ -51,7 +51,7 @@ export function getFlexLayoutTest(): Scene {
],
}),
$editor: new SceneEditManager({}),
- $timeRange: new SceneTimeRange(getDefaultTimeRange()),
+ $timeRange: new SceneTimeRange(),
$data: getQueryRunnerWithRandomWalkQuery(),
actions: [new SceneTimePicker({})],
});
@@ -97,7 +97,7 @@ export function getScenePanelRepeaterTest(): Scene {
}),
}),
$editor: new SceneEditManager({}),
- $timeRange: new SceneTimeRange(getDefaultTimeRange()),
+ $timeRange: new SceneTimeRange(),
$data: queryRunner,
actions: [
new SceneToolbarInput({
diff --git a/public/app/features/scenes/scenes/grid.tsx b/public/app/features/scenes/scenes/grid.tsx
index 4cf629fade4..3516ff8969c 100644
--- a/public/app/features/scenes/scenes/grid.tsx
+++ b/public/app/features/scenes/scenes/grid.tsx
@@ -1,8 +1,6 @@
-import { getDefaultTimeRange } from '@grafana/data';
-
+import { VizPanel } from '../components';
import { Scene } from '../components/Scene';
import { SceneTimePicker } from '../components/SceneTimePicker';
-import { VizPanel } from '../components/VizPanel';
import { SceneFlexLayout } from '../components/layout/SceneFlexLayout';
import { SceneGridLayout } from '../components/layout/SceneGridLayout';
import { SceneTimeRange } from '../core/SceneTimeRange';
@@ -57,7 +55,7 @@ export function getGridLayoutTest(): Scene {
],
}),
$editor: new SceneEditManager({}),
- $timeRange: new SceneTimeRange(getDefaultTimeRange()),
+ $timeRange: new SceneTimeRange(),
$data: getQueryRunnerWithRandomWalkQuery(),
actions: [new SceneTimePicker({})],
});
diff --git a/public/app/features/scenes/scenes/gridMultiTimeRange.tsx b/public/app/features/scenes/scenes/gridMultiTimeRange.tsx
index cb730ce1dbb..8ed865aa812 100644
--- a/public/app/features/scenes/scenes/gridMultiTimeRange.tsx
+++ b/public/app/features/scenes/scenes/gridMultiTimeRange.tsx
@@ -1,8 +1,8 @@
-import { dateTime, getDefaultTimeRange } from '@grafana/data';
+import { dateTime } from '@grafana/data';
+import { VizPanel } from '../components';
import { Scene } from '../components/Scene';
import { SceneTimePicker } from '../components/SceneTimePicker';
-import { VizPanel } from '../components/VizPanel';
import { SceneGridLayout, SceneGridRow } from '../components/layout/SceneGridLayout';
import { SceneTimeRange } from '../core/SceneTimeRange';
import { SceneEditManager } from '../editor/SceneEditManager';
@@ -10,7 +10,7 @@ import { SceneEditManager } from '../editor/SceneEditManager';
import { getQueryRunnerWithRandomWalkQuery } from './queries';
export function getGridWithMultipleTimeRanges(): Scene {
- const globalTimeRange = new SceneTimeRange(getDefaultTimeRange());
+ const globalTimeRange = new SceneTimeRange();
const now = dateTime();
const row1TimeRange = new SceneTimeRange({
diff --git a/public/app/features/scenes/scenes/gridMultiple.tsx b/public/app/features/scenes/scenes/gridMultiple.tsx
index 087081e576b..b16e2eb6bde 100644
--- a/public/app/features/scenes/scenes/gridMultiple.tsx
+++ b/public/app/features/scenes/scenes/gridMultiple.tsx
@@ -1,8 +1,6 @@
-import { getDefaultTimeRange } from '@grafana/data';
-
+import { VizPanel } from '../components';
import { Scene } from '../components/Scene';
import { SceneTimePicker } from '../components/SceneTimePicker';
-import { VizPanel } from '../components/VizPanel';
import { SceneFlexLayout } from '../components/layout/SceneFlexLayout';
import { SceneGridLayout } from '../components/layout/SceneGridLayout';
import { SceneTimeRange } from '../core/SceneTimeRange';
@@ -101,7 +99,7 @@ export function getMultipleGridLayoutTest(): Scene {
}),
$editor: new SceneEditManager({}),
- $timeRange: new SceneTimeRange(getDefaultTimeRange()),
+ $timeRange: new SceneTimeRange(),
$data: getQueryRunnerWithRandomWalkQuery(),
actions: [new SceneTimePicker({})],
});
diff --git a/public/app/features/scenes/scenes/gridWithMultipleData.tsx b/public/app/features/scenes/scenes/gridWithMultipleData.tsx
index f4752793079..43960d9b6da 100644
--- a/public/app/features/scenes/scenes/gridWithMultipleData.tsx
+++ b/public/app/features/scenes/scenes/gridWithMultipleData.tsx
@@ -1,8 +1,6 @@
-import { getDefaultTimeRange } from '@grafana/data';
-
+import { VizPanel } from '../components';
import { Scene } from '../components/Scene';
import { SceneTimePicker } from '../components/SceneTimePicker';
-import { VizPanel } from '../components/VizPanel';
import { SceneGridLayout, SceneGridRow } from '../components/layout/SceneGridLayout';
import { SceneTimeRange } from '../core/SceneTimeRange';
import { SceneEditManager } from '../editor/SceneEditManager';
@@ -15,7 +13,7 @@ export function getGridWithMultipleData(): Scene {
layout: new SceneGridLayout({
children: [
new SceneGridRow({
- $timeRange: new SceneTimeRange(getDefaultTimeRange()),
+ $timeRange: new SceneTimeRange(),
$data: getQueryRunnerWithRandomWalkQuery({ scenarioId: 'random_walk_table' }),
title: 'Row A - has its own query',
key: 'Row A',
@@ -95,7 +93,7 @@ export function getGridWithMultipleData(): Scene {
],
}),
$editor: new SceneEditManager({}),
- $timeRange: new SceneTimeRange(getDefaultTimeRange()),
+ $timeRange: new SceneTimeRange(),
$data: getQueryRunnerWithRandomWalkQuery(),
actions: [new SceneTimePicker({})],
});
diff --git a/public/app/features/scenes/scenes/gridWithRow.tsx b/public/app/features/scenes/scenes/gridWithRow.tsx
index 38dadce9cf1..85826d52b5b 100644
--- a/public/app/features/scenes/scenes/gridWithRow.tsx
+++ b/public/app/features/scenes/scenes/gridWithRow.tsx
@@ -1,8 +1,6 @@
-import { getDefaultTimeRange } from '@grafana/data';
-
+import { VizPanel } from '../components';
import { Scene } from '../components/Scene';
import { SceneTimePicker } from '../components/SceneTimePicker';
-import { VizPanel } from '../components/VizPanel';
import { SceneGridLayout, SceneGridRow } from '../components/layout/SceneGridLayout';
import { SceneTimeRange } from '../core/SceneTimeRange';
import { SceneEditManager } from '../editor/SceneEditManager';
@@ -78,7 +76,7 @@ export function getGridWithRowLayoutTest(): Scene {
],
}),
$editor: new SceneEditManager({}),
- $timeRange: new SceneTimeRange(getDefaultTimeRange()),
+ $timeRange: new SceneTimeRange(),
$data: getQueryRunnerWithRandomWalkQuery(),
actions: [new SceneTimePicker({})],
});
diff --git a/public/app/features/scenes/scenes/gridWithRows.tsx b/public/app/features/scenes/scenes/gridWithRows.tsx
index 1f47a22e708..1aef68fb5d4 100644
--- a/public/app/features/scenes/scenes/gridWithRows.tsx
+++ b/public/app/features/scenes/scenes/gridWithRows.tsx
@@ -1,8 +1,6 @@
-import { getDefaultTimeRange } from '@grafana/data';
-
+import { VizPanel } from '../components';
import { Scene } from '../components/Scene';
import { SceneTimePicker } from '../components/SceneTimePicker';
-import { VizPanel } from '../components/VizPanel';
import { SceneFlexLayout } from '../components/layout/SceneFlexLayout';
import { SceneGridLayout, SceneGridRow } from '../components/layout/SceneGridLayout';
import { SceneTimeRange } from '../core/SceneTimeRange';
@@ -83,7 +81,7 @@ export function getGridWithRowsTest(): Scene {
children: [cell1, cell2, row1, row2],
}),
$editor: new SceneEditManager({}),
- $timeRange: new SceneTimeRange(getDefaultTimeRange()),
+ $timeRange: new SceneTimeRange(),
$data: getQueryRunnerWithRandomWalkQuery(),
actions: [new SceneTimePicker({})],
});
diff --git a/public/app/features/scenes/scenes/nested.tsx b/public/app/features/scenes/scenes/nested.tsx
index cd4e60889fa..728346f4138 100644
--- a/public/app/features/scenes/scenes/nested.tsx
+++ b/public/app/features/scenes/scenes/nested.tsx
@@ -1,9 +1,7 @@
-import { getDefaultTimeRange } from '@grafana/data';
-
+import { VizPanel } from '../components';
import { NestedScene } from '../components/NestedScene';
import { Scene } from '../components/Scene';
import { SceneTimePicker } from '../components/SceneTimePicker';
-import { VizPanel } from '../components/VizPanel';
import { SceneFlexLayout } from '../components/layout/SceneFlexLayout';
import { SceneTimeRange } from '../core/SceneTimeRange';
@@ -23,7 +21,7 @@ export function getNestedScene(): Scene {
}),
],
}),
- $timeRange: new SceneTimeRange(getDefaultTimeRange()),
+ $timeRange: new SceneTimeRange(),
$data: getQueryRunnerWithRandomWalkQuery(),
actions: [new SceneTimePicker({})],
});
@@ -46,7 +44,7 @@ export function getInnerScene(title: string) {
}),
],
}),
- $timeRange: new SceneTimeRange(getDefaultTimeRange()),
+ $timeRange: new SceneTimeRange(),
$data: getQueryRunnerWithRandomWalkQuery(),
actions: [new SceneTimePicker({})],
});
diff --git a/public/app/features/scenes/scenes/queries.ts b/public/app/features/scenes/scenes/queries.ts
index 66f411c8b74..2aab365b2ad 100644
--- a/public/app/features/scenes/scenes/queries.ts
+++ b/public/app/features/scenes/scenes/queries.ts
@@ -1,8 +1,11 @@
import { TestDataQuery } from 'app/plugins/datasource/testdata/types';
-import { SceneQueryRunner } from '../querying/SceneQueryRunner';
+import { QueryRunnerState, SceneQueryRunner } from '../querying/SceneQueryRunner';
-export function getQueryRunnerWithRandomWalkQuery(overrides?: Partial) {
+export function getQueryRunnerWithRandomWalkQuery(
+ overrides?: Partial,
+ queryRunnerOverrides?: Partial
+) {
return new SceneQueryRunner({
queries: [
{
@@ -15,5 +18,6 @@ export function getQueryRunnerWithRandomWalkQuery(overrides?: Partial