DashboardScene: Support panel relative time overrides and timeshift (#62844)

* Things are working

* update

* Added unit tests

* More tests

* minor fix

* Update

* Support hideTimeOverride
pull/74266/head
Torkel Ödegaard 2 years ago committed by GitHub
parent 8113707dc8
commit f4c127a1d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      .betterer.results
  2. 98
      docs/sources/developers/kinds/core/dashboard/schema-reference.md
  3. 3
      kinds/dashboard/dashboard_kind.cue
  4. 2
      package.json
  5. 8
      packages/grafana-data/src/datetime/rangeutil.ts
  6. 2
      packages/grafana-data/src/types/time.ts
  7. 4
      packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts
  8. 3
      pkg/kinds/dashboard/dashboard_spec_gen.go
  9. 59
      public/app/features/dashboard-scene/scene/PanelTimeRange.test.tsx
  10. 145
      public/app/features/dashboard-scene/scene/PanelTimeRange.tsx
  11. 47
      public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap
  12. 139
      public/app/features/dashboard-scene/serialization/testfiles/dashboard_to_load1.json
  13. 27
      public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts
  14. 48
      public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts
  15. 36
      public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts
  16. 15
      public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts
  17. 10
      yarn.lock

@ -133,10 +133,9 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "8"]
],
"packages/grafana-data/src/datetime/rangeutil.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[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"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
],
"packages/grafana-data/src/datetime/timezones.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],

@ -447,30 +447,31 @@ Library panels streamline reuse of panels across multiple dashboards.
Dashboard panels are the basic visualization building blocks.
| Property | Type | Required | Default | Description |
|-------------------|---------------------------------------------------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `fieldConfig` | [FieldConfigSource](#fieldconfigsource) | **Yes** | | The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.<br/>Each column within this structure is called a field. A field can represent a single time series or table column.<br/>Field options allow you to change how the data is displayed in your visualizations. |
| `options` | [object](#options) | **Yes** | | It depends on the panel plugin. They are specified by the Options field in panel plugin schemas. |
| `transformations` | [DataTransformerConfig](#datatransformerconfig)[] | **Yes** | | List of transformations that are applied to the panel data before rendering.<br/>When there are multiple transformations, Grafana applies them in the order they are listed.<br/>Each transformation creates a result set that then passes on to the next transformation in the processing pipeline. |
| `transparent` | boolean | **Yes** | `false` | Whether to display the panel without a background. |
| `type` | string | **Yes** | | The panel plugin type id. This is used to find the plugin to display the panel.<br/>Constraint: `length >=1`. |
| `datasource` | [DataSourceRef](#datasourceref) | No | | Ref to a DataSource instance |
| `description` | string | No | | Panel description. |
| `gridPos` | [GridPos](#gridpos) | No | | Position and dimensions of a panel in the grid |
| `id` | uint32 | No | | Unique identifier of the panel. Generated by Grafana when creating a new panel. It must be unique within a dashboard, but not globally. |
| `interval` | string | No | | The min time interval setting defines a lower limit for the $__interval and $__interval_ms variables.<br/>This value must be formatted as a number followed by a valid time<br/>identifier like: "40s", "3d", etc.<br/>See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options |
| `libraryPanel` | [LibraryPanelRef](#librarypanelref) | No | | A library panel is a reusable panel that you can use in any dashboard.<br/>When you make a change to a library panel, that change propagates to all instances of where the panel is used.<br/>Library panels streamline reuse of panels across multiple dashboards. |
| `links` | [DashboardLink](#dashboardlink)[] | No | | Panel links. |
| `maxDataPoints` | number | No | | The maximum number of data points that the panel queries are retrieving. |
| `pluginVersion` | string | No | | The version of the plugin that is used for this panel. This is used to find the plugin to display the panel and to migrate old panel configs. |
| `repeatDirection` | string | No | `h` | Direction to repeat in if 'repeat' is set.<br/>`h` for horizontal, `v` for vertical.<br/>Possible values are: `h`, `v`. |
| `repeatPanelId` | integer | No | | Id of the repeating panel. |
| `repeat` | string | No | | Name of template variable to repeat for. |
| `tags` | string[] | No | | Tags for the panel. |
| `targets` | [Target](#target)[] | No | | Depends on the panel plugin. See the plugin documentation for details. |
| `timeFrom` | string | No | | Overrides the relative time range for individual panels,<br/>which causes them to be different than what is selected in<br/>the dashboard time picker in the top-right corner of the dashboard. You can use this to show metrics from different<br/>time periods or days on the same dashboard.<br/>The value is formatted as time operation like: `now-5m` (Last 5 minutes), `now/d` (the day so far),<br/>`now-5d/d`(Last 5 days), `now/w` (This week so far), `now-2y/y` (Last 2 years).<br/>Note: Panel time overrides have no effect when the dashboard’s time range is absolute.<br/>See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options |
| `timeShift` | string | No | | Overrides the time range for individual panels by shifting its start and end relative to the time picker.<br/>For example, you can shift the time range for the panel to be two hours earlier than the dashboard time picker setting `2h`.<br/>Note: Panel time overrides have no effect when the dashboard’s time range is absolute.<br/>See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options |
| `title` | string | No | | Panel title. |
| Property | Type | Required | Default | Description |
|--------------------|---------------------------------------------------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `fieldConfig` | [FieldConfigSource](#fieldconfigsource) | **Yes** | | The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.<br/>Each column within this structure is called a field. A field can represent a single time series or table column.<br/>Field options allow you to change how the data is displayed in your visualizations. |
| `options` | [object](#options) | **Yes** | | It depends on the panel plugin. They are specified by the Options field in panel plugin schemas. |
| `transformations` | [DataTransformerConfig](#datatransformerconfig)[] | **Yes** | | List of transformations that are applied to the panel data before rendering.<br/>When there are multiple transformations, Grafana applies them in the order they are listed.<br/>Each transformation creates a result set that then passes on to the next transformation in the processing pipeline. |
| `transparent` | boolean | **Yes** | `false` | Whether to display the panel without a background. |
| `type` | string | **Yes** | | The panel plugin type id. This is used to find the plugin to display the panel.<br/>Constraint: `length >=1`. |
| `datasource` | [DataSourceRef](#datasourceref) | No | | Ref to a DataSource instance |
| `description` | string | No | | Panel description. |
| `gridPos` | [GridPos](#gridpos) | No | | Position and dimensions of a panel in the grid |
| `hideTimeOverride` | boolean | No | | Controls if the timeFrom or timeShift overrides are shown in the panel header |
| `id` | uint32 | No | | Unique identifier of the panel. Generated by Grafana when creating a new panel. It must be unique within a dashboard, but not globally. |
| `interval` | string | No | | The min time interval setting defines a lower limit for the $__interval and $__interval_ms variables.<br/>This value must be formatted as a number followed by a valid time<br/>identifier like: "40s", "3d", etc.<br/>See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options |
| `libraryPanel` | [LibraryPanelRef](#librarypanelref) | No | | A library panel is a reusable panel that you can use in any dashboard.<br/>When you make a change to a library panel, that change propagates to all instances of where the panel is used.<br/>Library panels streamline reuse of panels across multiple dashboards. |
| `links` | [DashboardLink](#dashboardlink)[] | No | | Panel links. |
| `maxDataPoints` | number | No | | The maximum number of data points that the panel queries are retrieving. |
| `pluginVersion` | string | No | | The version of the plugin that is used for this panel. This is used to find the plugin to display the panel and to migrate old panel configs. |
| `repeatDirection` | string | No | `h` | Direction to repeat in if 'repeat' is set.<br/>`h` for horizontal, `v` for vertical.<br/>Possible values are: `h`, `v`. |
| `repeatPanelId` | integer | No | | Id of the repeating panel. |
| `repeat` | string | No | | Name of template variable to repeat for. |
| `tags` | string[] | No | | Tags for the panel. |
| `targets` | [Target](#target)[] | No | | Depends on the panel plugin. See the plugin documentation for details. |
| `timeFrom` | string | No | | Overrides the relative time range for individual panels,<br/>which causes them to be different than what is selected in<br/>the dashboard time picker in the top-right corner of the dashboard. You can use this to show metrics from different<br/>time periods or days on the same dashboard.<br/>The value is formatted as time operation like: `now-5m` (Last 5 minutes), `now/d` (the day so far),<br/>`now-5d/d`(Last 5 days), `now/w` (This week so far), `now-2y/y` (Last 2 years).<br/>Note: Panel time overrides have no effect when the dashboard’s time range is absolute.<br/>See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options |
| `timeShift` | string | No | | Overrides the time range for individual panels by shifting its start and end relative to the time picker.<br/>For example, you can shift the time range for the panel to be two hours earlier than the dashboard time picker setting `2h`.<br/>Note: Panel time overrides have no effect when the dashboard’s time range is absolute.<br/>See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options |
| `title` | string | No | | Panel title. |
### FieldConfigSource
@ -594,30 +595,31 @@ Support for legacy graph panel.
Dashboard panels are the basic visualization building blocks.
| Property | Type | Required | Default | Description |
|-------------------|---------------------------------------------------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `fieldConfig` | [FieldConfigSource](#fieldconfigsource) | **Yes** | | The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.<br/>Each column within this structure is called a field. A field can represent a single time series or table column.<br/>Field options allow you to change how the data is displayed in your visualizations. |
| `options` | [options](#options) | **Yes** | | It depends on the panel plugin. They are specified by the Options field in panel plugin schemas. |
| `transformations` | [DataTransformerConfig](#datatransformerconfig)[] | **Yes** | | List of transformations that are applied to the panel data before rendering.<br/>When there are multiple transformations, Grafana applies them in the order they are listed.<br/>Each transformation creates a result set that then passes on to the next transformation in the processing pipeline. |
| `transparent` | boolean | **Yes** | `false` | Whether to display the panel without a background. |
| `type` | string | **Yes** | | The panel plugin type id. This is used to find the plugin to display the panel.<br/>Constraint: `length >=1`. |
| `datasource` | [DataSourceRef](#datasourceref) | No | | Ref to a DataSource instance |
| `description` | string | No | | Panel description. |
| `gridPos` | [GridPos](#gridpos) | No | | Position and dimensions of a panel in the grid |
| `id` | uint32 | No | | Unique identifier of the panel. Generated by Grafana when creating a new panel. It must be unique within a dashboard, but not globally. |
| `interval` | string | No | | The min time interval setting defines a lower limit for the $__interval and $__interval_ms variables.<br/>This value must be formatted as a number followed by a valid time<br/>identifier like: "40s", "3d", etc.<br/>See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options |
| `libraryPanel` | [LibraryPanelRef](#librarypanelref) | No | | A library panel is a reusable panel that you can use in any dashboard.<br/>When you make a change to a library panel, that change propagates to all instances of where the panel is used.<br/>Library panels streamline reuse of panels across multiple dashboards. |
| `links` | [DashboardLink](#dashboardlink)[] | No | | Panel links. |
| `maxDataPoints` | number | No | | The maximum number of data points that the panel queries are retrieving. |
| `pluginVersion` | string | No | | The version of the plugin that is used for this panel. This is used to find the plugin to display the panel and to migrate old panel configs. |
| `repeatDirection` | string | No | `h` | Direction to repeat in if 'repeat' is set.<br/>`h` for horizontal, `v` for vertical.<br/>Possible values are: `h`, `v`. |
| `repeatPanelId` | integer | No | | Id of the repeating panel. |
| `repeat` | string | No | | Name of template variable to repeat for. |
| `tags` | string[] | No | | Tags for the panel. |
| `targets` | [Target](#target)[] | No | | Depends on the panel plugin. See the plugin documentation for details. |
| `timeFrom` | string | No | | Overrides the relative time range for individual panels,<br/>which causes them to be different than what is selected in<br/>the dashboard time picker in the top-right corner of the dashboard. You can use this to show metrics from different<br/>time periods or days on the same dashboard.<br/>The value is formatted as time operation like: `now-5m` (Last 5 minutes), `now/d` (the day so far),<br/>`now-5d/d`(Last 5 days), `now/w` (This week so far), `now-2y/y` (Last 2 years).<br/>Note: Panel time overrides have no effect when the dashboard’s time range is absolute.<br/>See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options |
| `timeShift` | string | No | | Overrides the time range for individual panels by shifting its start and end relative to the time picker.<br/>For example, you can shift the time range for the panel to be two hours earlier than the dashboard time picker setting `2h`.<br/>Note: Panel time overrides have no effect when the dashboard’s time range is absolute.<br/>See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options |
| `title` | string | No | | Panel title. |
| Property | Type | Required | Default | Description |
|--------------------|---------------------------------------------------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `fieldConfig` | [FieldConfigSource](#fieldconfigsource) | **Yes** | | The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.<br/>Each column within this structure is called a field. A field can represent a single time series or table column.<br/>Field options allow you to change how the data is displayed in your visualizations. |
| `options` | [options](#options) | **Yes** | | It depends on the panel plugin. They are specified by the Options field in panel plugin schemas. |
| `transformations` | [DataTransformerConfig](#datatransformerconfig)[] | **Yes** | | List of transformations that are applied to the panel data before rendering.<br/>When there are multiple transformations, Grafana applies them in the order they are listed.<br/>Each transformation creates a result set that then passes on to the next transformation in the processing pipeline. |
| `transparent` | boolean | **Yes** | `false` | Whether to display the panel without a background. |
| `type` | string | **Yes** | | The panel plugin type id. This is used to find the plugin to display the panel.<br/>Constraint: `length >=1`. |
| `datasource` | [DataSourceRef](#datasourceref) | No | | Ref to a DataSource instance |
| `description` | string | No | | Panel description. |
| `gridPos` | [GridPos](#gridpos) | No | | Position and dimensions of a panel in the grid |
| `hideTimeOverride` | boolean | No | | Controls if the timeFrom or timeShift overrides are shown in the panel header |
| `id` | uint32 | No | | Unique identifier of the panel. Generated by Grafana when creating a new panel. It must be unique within a dashboard, but not globally. |
| `interval` | string | No | | The min time interval setting defines a lower limit for the $__interval and $__interval_ms variables.<br/>This value must be formatted as a number followed by a valid time<br/>identifier like: "40s", "3d", etc.<br/>See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options |
| `libraryPanel` | [LibraryPanelRef](#librarypanelref) | No | | A library panel is a reusable panel that you can use in any dashboard.<br/>When you make a change to a library panel, that change propagates to all instances of where the panel is used.<br/>Library panels streamline reuse of panels across multiple dashboards. |
| `links` | [DashboardLink](#dashboardlink)[] | No | | Panel links. |
| `maxDataPoints` | number | No | | The maximum number of data points that the panel queries are retrieving. |
| `pluginVersion` | string | No | | The version of the plugin that is used for this panel. This is used to find the plugin to display the panel and to migrate old panel configs. |
| `repeatDirection` | string | No | `h` | Direction to repeat in if 'repeat' is set.<br/>`h` for horizontal, `v` for vertical.<br/>Possible values are: `h`, `v`. |
| `repeatPanelId` | integer | No | | Id of the repeating panel. |
| `repeat` | string | No | | Name of template variable to repeat for. |
| `tags` | string[] | No | | Tags for the panel. |
| `targets` | [Target](#target)[] | No | | Depends on the panel plugin. See the plugin documentation for details. |
| `timeFrom` | string | No | | Overrides the relative time range for individual panels,<br/>which causes them to be different than what is selected in<br/>the dashboard time picker in the top-right corner of the dashboard. You can use this to show metrics from different<br/>time periods or days on the same dashboard.<br/>The value is formatted as time operation like: `now-5m` (Last 5 minutes), `now/d` (the day so far),<br/>`now-5d/d`(Last 5 days), `now/w` (This week so far), `now-2y/y` (Last 2 years).<br/>Note: Panel time overrides have no effect when the dashboard’s time range is absolute.<br/>See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options |
| `timeShift` | string | No | | Overrides the time range for individual panels by shifting its start and end relative to the time picker.<br/>For example, you can shift the time range for the panel to be two hours earlier than the dashboard time picker setting `2h`.<br/>Note: Panel time overrides have no effect when the dashboard’s time range is absolute.<br/>See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options |
| `title` | string | No | | Panel title. |
### Templating

@ -572,6 +572,9 @@ lineage: schemas: [{
// See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options
timeShift?: string
// Controls if the timeFrom or timeShift overrides are shown in the panel header
hideTimeOverride?: bool
// Dynamically load the panel
libraryPanel?: #LibraryPanelRef

@ -246,7 +246,7 @@
"@grafana/lezer-traceql": "0.0.4",
"@grafana/monaco-logql": "^0.0.7",
"@grafana/runtime": "workspace:*",
"@grafana/scenes": "^0.26.0",
"@grafana/scenes": "^0.27.0",
"@grafana/schema": "workspace:*",
"@grafana/ui": "workspace:*",
"@kusto/monaco-kusto": "^7.4.0",

@ -86,7 +86,7 @@ const hiddenRangeOptions: TimeOption[] = [
{ from: 'now', to: 'now+5y', display: 'Next 5 years' },
];
const rangeIndex: Record<string, any> = {};
const rangeIndex: Record<string, TimeOption> = {};
each(rangeOptions, (frame) => {
rangeIndex[frame.from + ' to ' + frame.to] = frame;
});
@ -100,7 +100,7 @@ each(hiddenRangeOptions, (frame) => {
// now/d to now
// now/d
// if no to <expr> then to now is assumed
export function describeTextRange(expr: string) {
export function describeTextRange(expr: string): TimeOption {
const isLast = expr.indexOf('+') !== 0;
if (expr.indexOf('now') === -1) {
expr = (isLast ? 'now-' : 'now') + expr;
@ -112,9 +112,9 @@ export function describeTextRange(expr: string) {
}
if (isLast) {
opt = { from: expr, to: 'now' };
opt = { from: expr, to: 'now', display: '' };
} else {
opt = { from: 'now', to: expr };
opt = { from: 'now', to: expr, display: '' };
}
const parts = /^now([-+])(\d+)(\w)/.exec(expr);

@ -41,6 +41,8 @@ export interface TimeOption {
from: string;
to: string;
display: string;
invalid?: boolean;
section?: number;
}
/** @deprecated use TimeZone from schema */

@ -666,6 +666,10 @@ export interface Panel {
* Grid position.
*/
gridPos?: GridPos;
/**
* Controls if the timeFrom or timeShift overrides are shown in the panel header
*/
hideTimeOverride?: boolean;
/**
* Unique identifier of the panel. Generated by Grafana when creating a new panel. It must be unique within a dashboard, but not globally.
*/

@ -523,6 +523,9 @@ type Panel struct {
// Position and dimensions of a panel in the grid
GridPos *GridPos `json:"gridPos,omitempty"`
// Controls if the timeFrom or timeShift overrides are shown in the panel header
HideTimeOverride *bool `json:"hideTimeOverride,omitempty"`
// Unique identifier of the panel. Generated by Grafana when creating a new panel. It must be unique within a dashboard, but not globally.
Id *int `json:"id,omitempty"`

@ -0,0 +1,59 @@
import { advanceTo, clear } from 'jest-date-mock';
import { dateTime } from '@grafana/data';
import { SceneCanvasText, SceneFlexItem, SceneFlexLayout, SceneTimeRange } from '@grafana/scenes';
import { activateFullSceneTree } from '../utils/utils';
import { PanelTimeRange } from './PanelTimeRange';
describe('PanelTimeRange', () => {
const fakeCurrentDate = dateTime('2019-02-11T19:00:00.000Z').toDate();
beforeAll(() => {
advanceTo(fakeCurrentDate);
});
afterAll(() => {
clear();
});
it('should apply relative time override', () => {
const panelTime = new PanelTimeRange({ timeFrom: '2h' });
buildAndActivateSceneFor(panelTime);
expect(panelTime.state.value.from.toISOString()).toBe('2019-02-11T17:00:00.000Z');
expect(panelTime.state.value.to.toISOString()).toBe(fakeCurrentDate.toISOString());
expect(panelTime.state.value.raw.from).toBe('now-2h');
expect(panelTime.state.timeInfo).toBe('Last 2 hours');
});
it('should apply time shift', () => {
const panelTime = new PanelTimeRange({ timeShift: '2h' });
buildAndActivateSceneFor(panelTime);
expect(panelTime.state.value.from.toISOString()).toBe('2019-02-11T11:00:00.000Z');
expect(panelTime.state.value.to.toISOString()).toBe('2019-02-11T17:00:00.000Z');
expect(panelTime.state.timeInfo).toBe(' timeshift -2h');
});
it('should apply both relative time and time shift', () => {
const panelTime = new PanelTimeRange({ timeFrom: '2h', timeShift: '2h' });
buildAndActivateSceneFor(panelTime);
expect(panelTime.state.value.from.toISOString()).toBe('2019-02-11T15:00:00.000Z');
expect(panelTime.state.timeInfo).toBe('Last 2 hours timeshift -2h');
});
});
function buildAndActivateSceneFor(panelTime: PanelTimeRange) {
const panel = new SceneCanvasText({ text: 'Hello', $timeRange: panelTime });
const scene = new SceneFlexLayout({
$timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }),
children: [new SceneFlexItem({ body: panel })],
});
activateFullSceneTree(scene);
}

@ -0,0 +1,145 @@
import { css } from '@emotion/css';
import React from 'react';
import { dateMath, getDefaultTimeRange, GrafanaTheme2, rangeUtil, TimeRange } from '@grafana/data';
import {
SceneComponentProps,
sceneGraph,
SceneObjectBase,
SceneTimeRangeLike,
SceneTimeRangeState,
} from '@grafana/scenes';
import { Icon, PanelChrome, TimePickerTooltip, Tooltip, useStyles2 } from '@grafana/ui';
import { TimeOverrideResult } from 'app/features/dashboard/utils/panel';
interface PanelTimeRangeState extends SceneTimeRangeState {
timeFrom?: string;
timeShift?: string;
hideTimeOverride?: boolean;
timeInfo?: string;
}
export class PanelTimeRange extends SceneObjectBase<PanelTimeRangeState> implements SceneTimeRangeLike {
public static Component = PanelTimeRangeRenderer;
public constructor(state: Partial<PanelTimeRangeState> = {}) {
super({
...state,
// This time range is not valid until activation
from: 'now-6h',
to: 'now',
value: getDefaultTimeRange(),
});
this.addActivationHandler(() => this._activationHandler());
}
private getTimeOverride(parentTimeRange: TimeRange): TimeOverrideResult {
const { timeFrom, timeShift } = this.state;
const newTimeData = { timeInfo: '', timeRange: parentTimeRange };
if (timeFrom) {
const timeFromInterpolated = sceneGraph.interpolate(this, this.state.timeFrom);
const timeFromInfo = rangeUtil.describeTextRange(timeFromInterpolated);
if (timeFromInfo.invalid) {
newTimeData.timeInfo = 'invalid time override';
return newTimeData;
}
// Only evaluate if the timeFrom if parent time is relative
if (rangeUtil.isRelativeTimeRange(parentTimeRange.raw)) {
newTimeData.timeInfo = timeFromInfo.display;
newTimeData.timeRange = {
from: dateMath.parse(timeFromInfo.from)!,
to: dateMath.parse(timeFromInfo.to)!,
raw: { from: timeFromInfo.from, to: timeFromInfo.to },
};
}
}
if (timeShift) {
const timeShiftInterpolated = sceneGraph.interpolate(this, this.state.timeShift);
const timeShiftInfo = rangeUtil.describeTextRange(timeShiftInterpolated);
if (timeShiftInfo.invalid) {
newTimeData.timeInfo = 'invalid timeshift';
return newTimeData;
}
const timeShift = '-' + timeShiftInterpolated;
newTimeData.timeInfo += ' timeshift ' + timeShift;
const from = dateMath.parseDateMath(timeShift, newTimeData.timeRange.from, false)!;
const to = dateMath.parseDateMath(timeShift, newTimeData.timeRange.to, true)!;
newTimeData.timeRange = { from, to, raw: { from, to } };
}
return newTimeData;
}
private _activationHandler(): void {
const parentTimeRange = this.getParentTimeRange();
this._subs.add(parentTimeRange.subscribeToState((state) => this.handleParentTimeRangeChanged(state.value)));
this.handleParentTimeRangeChanged(parentTimeRange.state.value);
}
private handleParentTimeRangeChanged(parentTimeRange: TimeRange) {
const overrideResult = this.getTimeOverride(parentTimeRange);
this.setState({ value: overrideResult.timeRange, timeInfo: overrideResult.timeInfo });
}
private getParentTimeRange(): SceneTimeRangeLike {
if (!this.parent || !this.parent.parent) {
throw new Error('Missing parent');
}
// Need to go up two levels otherwise we will get ourselves
return sceneGraph.getTimeRange(this.parent.parent);
}
public onTimeRangeChange = (timeRange: TimeRange) => {
const parentTimeRange = this.getParentTimeRange();
parentTimeRange.onTimeRangeChange(timeRange);
};
public onRefresh(): void {
this.getParentTimeRange().onRefresh();
}
public onTimeZoneChange(timeZone: string): void {
this.getParentTimeRange().onTimeZoneChange(timeZone);
}
public getTimeZone(): string {
return this.getParentTimeRange().getTimeZone();
}
}
function PanelTimeRangeRenderer({ model }: SceneComponentProps<PanelTimeRange>) {
const { timeInfo, hideTimeOverride } = model.useState();
const styles = useStyles2(getStyles);
if (!timeInfo || hideTimeOverride) {
return null;
}
return (
<Tooltip content={<TimePickerTooltip timeRange={model.state.value} timeZone={model.getTimeZone()} />}>
<PanelChrome.TitleItem className={styles.timeshift}>
<Icon name="clock-nine" size="sm" /> {timeInfo}
</PanelChrome.TitleItem>
</Tooltip>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
timeshift: css({
color: theme.colors.text.link,
gap: theme.spacing(0.5),
}),
};
};

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`transformSceneToSaveModel Given a scene Should transfrom back to peristed model 1`] = `
exports[`transformSceneToSaveModel Given a scene Should transform back to peristed model 1`] = `
{
"editable": true,
"fiscalYearStartMonth": 0,
@ -14,56 +14,17 @@ exports[`transformSceneToSaveModel Given a scene Should transfrom back to perist
"mode": "palette-classic",
},
"custom": {
"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",
},
{
"color": "red",
"value": 80,
},
],
"lineWidth": 2,
},
},
"overrides": [],
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"w": 12,
"x": 12,
"y": 0,
},
"id": 28,

@ -33,57 +33,17 @@
"mode": "palette-classic"
},
"custom": {
"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
}
]
"lineWidth": 2
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"w": 12,
"x": 12,
"y": 0
},
"id": 28,
@ -116,10 +76,6 @@
},
{
"collapsed": false,
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"gridPos": {
"h": 1,
"w": 24,
@ -128,15 +84,6 @@
},
"id": 5,
"panels": [],
"targets": [
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "A"
}
],
"title": "Row title",
"type": "row"
},
@ -146,56 +93,7 @@
"uid": "PD8C576611E62080A"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"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
}
]
}
},
"defaults": {},
"overrides": []
},
"gridPos": {
@ -205,18 +103,7 @@
"y": 9
},
"id": 29,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"options": {},
"targets": [
{
"alias": "series",
@ -233,10 +120,6 @@
"type": "timeseries"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"gridPos": {
"h": 10,
"w": 11,
@ -255,18 +138,6 @@
"mode": "markdown"
},
"pluginVersion": "10.2.0-pre",
"targets": [
{
"alias": "__server_names",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "A",
"scenarioId": "random_walk",
"seriesCount": 6
}
],
"title": "Transparent text panel",
"transparent": true,
"type": "text"

@ -18,11 +18,12 @@ import { createPanelJSONFixture } from 'app/features/dashboard/state/__fixtures_
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types';
import { PanelTimeRange } from '../scene/PanelTimeRange';
import { ShareQueryDataProvider } from '../scene/ShareQueryDataProvider';
import {
createDashboardSceneFromDashboardModel,
createVizPanelFromPanelModel,
buildSceneFromPanelModel,
createSceneVariableFromVariableModel,
} from './transformSaveModelToScene';
@ -234,7 +235,7 @@ describe('DashboardLoader', () => {
},
],
};
const vizPanelSceneObject = createVizPanelFromPanelModel(new PanelModel(panel));
const vizPanelSceneObject = buildSceneFromPanelModel(new PanelModel(panel));
const vizPanelItelf = vizPanelSceneObject.state.body as VizPanel;
expect(vizPanelItelf?.state.title).toBe('test');
expect(vizPanelItelf?.state.pluginId).toBe('test-plugin');
@ -262,13 +263,29 @@ describe('DashboardLoader', () => {
transparent: true,
};
const gridItem = createVizPanelFromPanelModel(new PanelModel(panel));
const gridItem = buildSceneFromPanelModel(new PanelModel(panel));
const vizPanel = gridItem.state.body as VizPanel;
expect(vizPanel.state.displayMode).toEqual('transparent');
expect(vizPanel.state.hoverHeader).toEqual(true);
});
it('should set PanelTimeRange when timeFrom or timeShift is present', () => {
const panel = {
type: 'test-plugin',
timeFrom: '2h',
timeShift: '1d',
};
const gridItem = buildSceneFromPanelModel(new PanelModel(panel));
const vizPanel = gridItem.state.body as VizPanel;
const timeRange = vizPanel.state.$timeRange as PanelTimeRange;
expect(timeRange).toBeInstanceOf(PanelTimeRange);
expect(timeRange.state.timeFrom).toBe('2h');
expect(timeRange.state.timeShift).toBe('1d');
});
it('should handle a dashboard query data source', () => {
const panel = {
title: '',
@ -279,7 +296,7 @@ describe('DashboardLoader', () => {
targets: [{ refId: 'A', panelId: 10 }],
};
const vizPanel = createVizPanelFromPanelModel(new PanelModel(panel)).state.body as VizPanel;
const vizPanel = buildSceneFromPanelModel(new PanelModel(panel)).state.body as VizPanel;
expect(vizPanel.state.$data).toBeInstanceOf(ShareQueryDataProvider);
});
@ -297,7 +314,7 @@ describe('DashboardLoader', () => {
skipDataQuery: true,
}).meta;
const gridItem = createVizPanelFromPanelModel(new PanelModel(panel));
const gridItem = buildSceneFromPanelModel(new PanelModel(panel));
const vizPanel = gridItem.state.body as VizPanel;
expect(vizPanel.state.$data).toBeUndefined();

@ -24,6 +24,7 @@ import {
SceneControlsSpacer,
VizPanelMenu,
behaviors,
VizPanelState,
} from '@grafana/scenes';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { DashboardDTO } from 'app/types';
@ -31,6 +32,7 @@ import { DashboardDTO } from 'app/types';
import { DashboardScene } from '../scene/DashboardScene';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { panelMenuBehavior } from '../scene/PanelMenuBehavior';
import { PanelTimeRange } from '../scene/PanelTimeRange';
import { createPanelDataProvider } from '../utils/createPanelDataProvider';
import { getVizPanelKeyForPanelId } from '../utils/utils';
@ -68,7 +70,7 @@ export function createSceneObjectsForPanels(oldPanels: PanelModel[]): Array<Scen
title: panel.title,
isCollapsed: true,
y: panel.gridPos.y,
children: panel.panels ? panel.panels.map(createVizPanelFromPanelModel) : [],
children: panel.panels ? panel.panels.map(buildSceneFromPanelModel) : [],
})
);
} else {
@ -104,7 +106,7 @@ export function createSceneObjectsForPanels(oldPanels: PanelModel[]): Array<Scen
});
panels.push(gridItem);
} else {
const panelObject = createVizPanelFromPanelModel(panel);
const panelObject = buildSceneFromPanelModel(panel);
// when processing an expanded row, collect its panels
if (currentRow) {
@ -244,28 +246,38 @@ export function createSceneVariableFromVariableModel(variable: VariableModel): S
}
}
export function createVizPanelFromPanelModel(panel: PanelModel) {
export function buildSceneFromPanelModel(panel: PanelModel): SceneGridItem {
const vizPanelState: VizPanelState = {
key: getVizPanelKeyForPanelId(panel.id),
title: panel.title,
pluginId: panel.type,
options: panel.options ?? {},
fieldConfig: panel.fieldConfig,
pluginVersion: panel.pluginVersion,
displayMode: panel.transparent ? 'transparent' : undefined,
// To be replaced with it's own option persited option instead derived
hoverHeader: !panel.title && !panel.timeFrom && !panel.timeShift,
$data: createPanelDataProvider(panel),
menu: new VizPanelMenu({
$behaviors: [panelMenuBehavior],
}),
};
if (panel.timeFrom || panel.timeShift) {
vizPanelState.$timeRange = new PanelTimeRange({
timeFrom: panel.timeFrom,
timeShift: panel.timeShift,
hideTimeOverride: panel.hideTimeOverride,
});
}
return new SceneGridItem({
key: `grid-item-${panel.id}`,
x: panel.gridPos.x,
y: panel.gridPos.y,
width: panel.gridPos.w,
height: panel.gridPos.h,
body: new VizPanel({
key: getVizPanelKeyForPanelId(panel.id),
title: panel.title,
pluginId: panel.type,
options: panel.options ?? {},
fieldConfig: panel.fieldConfig,
pluginVersion: panel.pluginVersion,
displayMode: panel.transparent ? 'transparent' : undefined,
// To be replaced with it's own option persited option instead derived
hoverHeader: !panel.title && !panel.timeFrom && !panel.timeShift,
$data: createPanelDataProvider(panel),
menu: new VizPanelMenu({
$behaviors: [panelMenuBehavior],
}),
}),
body: new VizPanel(vizPanelState),
});
}

@ -1,14 +1,44 @@
import { SceneGridItem } from '@grafana/scenes';
import { Panel } from '@grafana/schema';
import { PanelModel } from 'app/features/dashboard/state';
import dashboard_to_load1 from './testfiles/dashboard_to_load1.json';
import { transformSaveModelToScene } from './transformSaveModelToScene';
import { transformSceneToSaveModel } from './transformSceneToSaveModel';
import { buildSceneFromPanelModel, transformSaveModelToScene } from './transformSaveModelToScene';
import { gridItemToPanel, transformSceneToSaveModel } from './transformSceneToSaveModel';
describe('transformSceneToSaveModel', () => {
describe('Given a scene', () => {
it('Should transfrom back to peristed model', () => {
it('Should transform back to peristed model', () => {
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} });
const saveModel = transformSceneToSaveModel(scene);
expect(saveModel).toMatchSnapshot();
});
});
describe('Panel options', () => {
it('Given panel with time override', () => {
const gridItem = createVizPanelFromPanelSchema({
timeFrom: '2h',
timeShift: '1d',
hideTimeOverride: true,
});
const saveModel = gridItemToPanel(gridItem);
expect(saveModel.timeFrom).toBe('2h');
expect(saveModel.timeShift).toBe('1d');
expect(saveModel.hideTimeOverride).toBe(true);
});
it('transparent panel', () => {
const gridItem = createVizPanelFromPanelSchema({ transparent: true });
const saveModel = gridItemToPanel(gridItem);
expect(saveModel.transparent).toBe(true);
});
});
});
export function createVizPanelFromPanelSchema(panel: Partial<Panel>): SceneGridItem {
return buildSceneFromPanelModel(new PanelModel(panel));
}

@ -3,6 +3,7 @@ import { Dashboard, defaultDashboard, FieldConfigSource, Panel } from '@grafana/
import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object';
import { DashboardScene } from '../scene/DashboardScene';
import { PanelTimeRange } from '../scene/PanelTimeRange';
import { getPanelIdForVizPanel } from '../utils/utils';
export function transformSceneToSaveModel(scene: DashboardScene): Dashboard {
@ -33,7 +34,7 @@ export function transformSceneToSaveModel(scene: DashboardScene): Dashboard {
return sortedDeepCloneWithoutNulls(dashboard);
}
function gridItemToPanel(gridItem: SceneGridItem): Panel {
export function gridItemToPanel(gridItem: SceneGridItem): Panel {
const vizPanel = gridItem.state.body;
if (!(vizPanel instanceof VizPanel)) {
throw new Error('SceneGridItem body expected to be VizPanel');
@ -55,5 +56,17 @@ function gridItemToPanel(gridItem: SceneGridItem): Panel {
transparent: false,
};
const panelTime = vizPanel.state.$timeRange;
if (panelTime instanceof PanelTimeRange) {
panel.timeFrom = panelTime.state.timeFrom;
panel.timeShift = panelTime.state.timeShift;
panel.hideTimeOverride = panelTime.state.hideTimeOverride;
}
if (vizPanel.state.displayMode === 'transparent') {
panel.transparent = true;
}
return panel;
}

@ -3939,9 +3939,9 @@ __metadata:
languageName: unknown
linkType: soft
"@grafana/scenes@npm:^0.26.0":
version: 0.26.0
resolution: "@grafana/scenes@npm:0.26.0"
"@grafana/scenes@npm:^0.27.0":
version: 0.27.0
resolution: "@grafana/scenes@npm:0.27.0"
dependencies:
"@grafana/e2e-selectors": 10.0.2
react-grid-layout: 1.3.4
@ -3953,7 +3953,7 @@ __metadata:
"@grafana/runtime": 10.0.3
"@grafana/schema": 10.0.3
"@grafana/ui": 10.0.3
checksum: 041458e463cde07179c75444f245411715ed50f072a5c4d519e258d0e0d57864c55e9cce3fb245943e2bc4245ee48e243701f5f0290c33e0374d4e948820eb14
checksum: 71b2ea13c6afca0d8d101e9a7d945ebb181ad2acbeb6fa5ca4018a34332a9b1e09a434feea9080665327d3d30a7e4d2542a7491a2f68a944717d56ba014fba25
languageName: node
linkType: hard
@ -19259,7 +19259,7 @@ __metadata:
"@grafana/lezer-traceql": 0.0.4
"@grafana/monaco-logql": ^0.0.7
"@grafana/runtime": "workspace:*"
"@grafana/scenes": ^0.26.0
"@grafana/scenes": ^0.27.0
"@grafana/schema": "workspace:*"
"@grafana/toolkit": "workspace:*"
"@grafana/tsconfig": ^1.3.0-rc1

Loading…
Cancel
Save