VizPanel: Support panel migrations and state changes (#58501)

* VizPanel: Make it more real

* Updates

* Progress on query runner and max data points from width

* Updated

* Update

* Tests

* Fixed issue with migration

* Moving VizPanel

* Fixed migration issue due to pluginVersion not being set

* Update public/app/features/scenes/querying/SceneQueryRunner.test.ts

Co-authored-by: Ivan Ortega Alba <ivanortegaalba@gmail.com>

* Some minor review fixes

* Added expect

Co-authored-by: Ivan Ortega Alba <ivanortegaalba@gmail.com>
This commit is contained in:
Torkel Ödegaard 2022-11-21 15:31:01 +01:00 committed by GitHub
parent 15561b83e4
commit 156ed4b56c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 595 additions and 219 deletions

View File

@ -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"]
],

View File

@ -38,7 +38,7 @@ export function PanelRenderer<P extends object = any, F extends object = any>(pr
const [plugin, setPlugin] = useState(syncGetPanelPlugin(pluginId));
const [error, setError] = useState<string | undefined>();
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<P extends object = any, F extends object = any>(
}, [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);

View File

@ -63,7 +63,7 @@ export function getPanelPlugin(
version: '',
},
hideFromList: options.hideFromList === true,
module: '',
module: options.module ?? '',
baseUrl: '',
};
return plugin;

View File

@ -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<VizPanelState> {
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<VizPanel>) {
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 = <SceneDragHandle layoutKey={layout.state.key!} />;
const titleInterpolated = sceneGraph.interpolate(model, title);
return (
<AutoSizer>
{({ width, height }) => {
if (width < 3 || height < 3) {
return null;
}
return (
<PanelChrome
title={titleInterpolated}
width={width}
height={height}
leftItems={isDraggable ? [dragHandle] : undefined}
>
{(innerWidth, innerHeight) => (
<>
<PanelRenderer
title="Raw data"
pluginId={pluginId}
width={innerWidth}
height={innerHeight}
data={data}
options={options}
fieldConfig={fieldConfig}
onOptionsChange={() => {}}
onChangeTimeRange={model.onSetTimeRange}
/>
</>
)}
</PanelChrome>
);
}}
</AutoSizer>
);
}
ScenePanelRenderer.displayName = 'ScenePanelRenderer';
function VizPanelEditor({ model }: SceneComponentProps<VizPanel>) {
const { title } = model.useState();
return (
<Field label="Title">
<Input defaultValue={title} onBlur={(evt) => model.setState({ title: evt.currentTarget.value })} />
</Field>
);
}

View File

@ -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',
},
() => <div>My custom panel</div>
);
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<OptionsPlugin1, FieldConfigPlugin1>;
beforeAll(async () => {
panel = new VizPanel<OptionsPlugin1, FieldConfigPlugin1>({
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<OptionsPlugin1, FieldConfigPlugin1>;
beforeAll(async () => {
panel = new VizPanel<OptionsPlugin1, FieldConfigPlugin1>({ 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);
});
});
});

View File

@ -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<TOptions = {}, TFieldConfig = {}> extends SceneLayoutChildState {
title: string;
pluginId: string;
options: TOptions;
fieldConfig: FieldConfigSource<TFieldConfig>;
pluginVersion?: string;
// internal state
pluginLoadError?: string;
}
export class VizPanel<TOptions = {}, TFieldConfig = {}> extends SceneObjectBase<
VizPanelState<Partial<TOptions>, 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<VizPanelState<Partial<TOptions>, 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<VizPanel>) {
const { title } = model.useState();
return (
<Field label="Title">
<Input defaultValue={title} onBlur={(evt) => model.setState({ title: evt.currentTarget.value })} />
</Field>
);
}

View File

@ -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<VizPanel>) {
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 = <SceneDragHandle layoutKey={layout.state.key!} />;
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 <div>Failed to load plugin: {pluginLoadError}</div>;
}
if (!plugin || !plugin.hasPluginId(pluginId)) {
return <div>Loading plugin panel...</div>;
}
if (!plugin.panel) {
return <div>Panel plugin has no panel component</div>;
}
const PanelComponent = plugin.panel;
// Query runner needs to with for auto maxDataPoints
if ($data instanceof SceneQueryRunner) {
$data.setContainerWidth(width);
}
return (
<div ref={ref as RefCallback<HTMLDivElement>} style={{ width: '100%', height: '100%' }}>
<PanelChrome
title={titleInterpolated}
width={width}
height={height}
leftItems={isDraggable ? [dragHandle] : undefined}
>
{(innerWidth, innerHeight) => (
<>
{!dataWithOverrides && <div>No data...</div>}
{dataWithOverrides && (
<ErrorBoundaryAlert dependencies={[plugin, data]}>
<PluginContextProvider meta={plugin.meta}>
<PanelComponent
id={1}
data={dataWithOverrides}
title={title}
timeRange={dataWithOverrides.timeRange}
timeZone={timeZone}
options={options}
fieldConfig={fieldConfig}
transparent={false}
width={innerWidth}
height={innerHeight}
renderCounter={0}
replaceVariables={(str: string) => str}
onOptionsChange={model.onOptionsChange}
onFieldConfigChange={model.onFieldConfigChange}
onChangeTimeRange={model.onChangeTimeRange}
eventBus={appEvents}
/>
</PluginContextProvider>
</ErrorBoundaryAlert>
)}
</>
)}
</PanelChrome>
</div>
);
}
VizPanelRenderer.displayName = 'ScenePanelRenderer';

View File

@ -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';

View File

@ -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<SceneTimeRangeState> implements SceneObjectWithUrlSync {
public constructor(state: Partial<SceneTimeRangeState> = {}) {
super({
...getDefaultTimeRange(),
timeZone: getTimeZone(),
...state,
});
}
public onTimeRangeChange = (timeRange: TimeRange) => {
this.setState(timeRange);
};

View File

@ -108,7 +108,7 @@ export const EmptyDataNode = new SceneDataNode({
},
});
export const DefaultTimeRange = new SceneTimeRangeImpl(getDefaultTimeRange());
export const DefaultTimeRange = new SceneTimeRangeImpl();
export const sceneGraph = {
getVariables,

View File

@ -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<SceneTimeRangeState> {
onTimeRangeChange(timeRange: TimeRange): void;

View File

@ -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<DashboardLoaderState> {
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<DashboardLoaderState> {
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<DashboardLoaderState> {
}
}
} 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<DashboardLoaderState> {
}
}
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 {

View File

@ -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<PanelData>({
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);
});
});
});

View File

@ -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<QueryRunnerState> {
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<QueryRunnerState> {
})
);
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<QueryRunnerState> {
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<QueryRunnerState> {
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<QueryRunnerState> {
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<QueryRunnerState> {
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<DataSourceApi> {
async function getDataSource(datasource: DataSourceRef | undefined, scopedVars: ScopedVars): Promise<DataSourceApi> {
if (datasource && (datasource as any).query) {
return datasource as DataSourceApi;
}

View File

@ -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({

View File

@ -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({})],
});

View File

@ -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({

View File

@ -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({})],
});

View File

@ -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({})],
});

View File

@ -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({})],
});

View File

@ -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({})],
});

View File

@ -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({})],
});

View File

@ -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<TestDataQuery>) {
export function getQueryRunnerWithRandomWalkQuery(
overrides?: Partial<TestDataQuery>,
queryRunnerOverrides?: Partial<QueryRunnerState>
) {
return new SceneQueryRunner({
queries: [
{
@ -15,5 +18,6 @@ export function getQueryRunnerWithRandomWalkQuery(overrides?: Partial<TestDataQu
...overrides,
},
],
...queryRunnerOverrides,
});
}

View File

@ -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';
import { SceneEditManager } from '../editor/SceneEditManager';
@ -55,7 +53,7 @@ export function getSceneWithRows(): Scene {
],
}),
$editor: new SceneEditManager({}),
$timeRange: new SceneTimeRange(getDefaultTimeRange()),
$timeRange: new SceneTimeRange(),
$data: getQueryRunnerWithRandomWalkQuery(),
actions: [new SceneTimePicker({})],
});

View File

@ -1,10 +1,8 @@
import { getDefaultTimeRange } from '@grafana/data';
import { VizPanel } from '../components';
import { Scene } from '../components/Scene';
import { SceneCanvasText } from '../components/SceneCanvasText';
import { SceneSubMenu } from '../components/SceneSubMenu';
import { SceneTimePicker } from '../components/SceneTimePicker';
import { VizPanel } from '../components/VizPanel';
import { SceneFlexLayout } from '../components/layout/SceneFlexLayout';
import { SceneTimeRange } from '../core/SceneTimeRange';
import { VariableValueSelectors } from '../variables/components/VariableValueSelectors';
@ -68,7 +66,7 @@ export function getVariablesDemo(): Scene {
}),
],
}),
$timeRange: new SceneTimeRange(getDefaultTimeRange()),
$timeRange: new SceneTimeRange(),
actions: [new SceneTimePicker({})],
subMenu: new SceneSubMenu({
children: [new VariableValueSelectors({})],