diff --git a/.betterer.results b/.betterer.results index ab123de9188..3d5bbb93146 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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"], diff --git a/docs/sources/developers/kinds/core/dashboard/schema-reference.md b/docs/sources/developers/kinds/core/dashboard/schema-reference.md index 43fb8657064..afd60ee06bb 100644 --- a/docs/sources/developers/kinds/core/dashboard/schema-reference.md +++ b/docs/sources/developers/kinds/core/dashboard/schema-reference.md @@ -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.
Each column within this structure is called a field. A field can represent a single time series or table column.
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.
When there are multiple transformations, Grafana applies them in the order they are listed.
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.
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.
This value must be formatted as a number followed by a valid time
identifier like: "40s", "3d", etc.
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.
When you make a change to a library panel, that change propagates to all instances of where the panel is used.
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.
`h` for horizontal, `v` for vertical.
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,
which causes them to be different than what is selected in
the dashboard time picker in the top-right corner of the dashboard. You can use this to show metrics from different
time periods or days on the same dashboard.
The value is formatted as time operation like: `now-5m` (Last 5 minutes), `now/d` (the day so far),
`now-5d/d`(Last 5 days), `now/w` (This week so far), `now-2y/y` (Last 2 years).
Note: Panel time overrides have no effect when the dashboard’s time range is absolute.
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.
For example, you can shift the time range for the panel to be two hours earlier than the dashboard time picker setting `2h`.
Note: Panel time overrides have no effect when the dashboard’s time range is absolute.
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.
Each column within this structure is called a field. A field can represent a single time series or table column.
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.
When there are multiple transformations, Grafana applies them in the order they are listed.
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.
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.
This value must be formatted as a number followed by a valid time
identifier like: "40s", "3d", etc.
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.
When you make a change to a library panel, that change propagates to all instances of where the panel is used.
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.
`h` for horizontal, `v` for vertical.
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,
which causes them to be different than what is selected in
the dashboard time picker in the top-right corner of the dashboard. You can use this to show metrics from different
time periods or days on the same dashboard.
The value is formatted as time operation like: `now-5m` (Last 5 minutes), `now/d` (the day so far),
`now-5d/d`(Last 5 days), `now/w` (This week so far), `now-2y/y` (Last 2 years).
Note: Panel time overrides have no effect when the dashboard’s time range is absolute.
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.
For example, you can shift the time range for the panel to be two hours earlier than the dashboard time picker setting `2h`.
Note: Panel time overrides have no effect when the dashboard’s time range is absolute.
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.
Each column within this structure is called a field. A field can represent a single time series or table column.
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.
When there are multiple transformations, Grafana applies them in the order they are listed.
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.
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.
This value must be formatted as a number followed by a valid time
identifier like: "40s", "3d", etc.
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.
When you make a change to a library panel, that change propagates to all instances of where the panel is used.
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.
`h` for horizontal, `v` for vertical.
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,
which causes them to be different than what is selected in
the dashboard time picker in the top-right corner of the dashboard. You can use this to show metrics from different
time periods or days on the same dashboard.
The value is formatted as time operation like: `now-5m` (Last 5 minutes), `now/d` (the day so far),
`now-5d/d`(Last 5 days), `now/w` (This week so far), `now-2y/y` (Last 2 years).
Note: Panel time overrides have no effect when the dashboard’s time range is absolute.
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.
For example, you can shift the time range for the panel to be two hours earlier than the dashboard time picker setting `2h`.
Note: Panel time overrides have no effect when the dashboard’s time range is absolute.
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.
Each column within this structure is called a field. A field can represent a single time series or table column.
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.
When there are multiple transformations, Grafana applies them in the order they are listed.
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.
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.
This value must be formatted as a number followed by a valid time
identifier like: "40s", "3d", etc.
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.
When you make a change to a library panel, that change propagates to all instances of where the panel is used.
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.
`h` for horizontal, `v` for vertical.
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,
which causes them to be different than what is selected in
the dashboard time picker in the top-right corner of the dashboard. You can use this to show metrics from different
time periods or days on the same dashboard.
The value is formatted as time operation like: `now-5m` (Last 5 minutes), `now/d` (the day so far),
`now-5d/d`(Last 5 days), `now/w` (This week so far), `now-2y/y` (Last 2 years).
Note: Panel time overrides have no effect when the dashboard’s time range is absolute.
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.
For example, you can shift the time range for the panel to be two hours earlier than the dashboard time picker setting `2h`.
Note: Panel time overrides have no effect when the dashboard’s time range is absolute.
See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options | +| `title` | string | No | | Panel title. | ### Templating diff --git a/kinds/dashboard/dashboard_kind.cue b/kinds/dashboard/dashboard_kind.cue index 9a8ab1a218c..e5a857f2223 100644 --- a/kinds/dashboard/dashboard_kind.cue +++ b/kinds/dashboard/dashboard_kind.cue @@ -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 diff --git a/package.json b/package.json index 6d8fbd435e3..e7202cf808d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/grafana-data/src/datetime/rangeutil.ts b/packages/grafana-data/src/datetime/rangeutil.ts index 29e4d820cc3..00ff5f81c97 100644 --- a/packages/grafana-data/src/datetime/rangeutil.ts +++ b/packages/grafana-data/src/datetime/rangeutil.ts @@ -86,7 +86,7 @@ const hiddenRangeOptions: TimeOption[] = [ { from: 'now', to: 'now+5y', display: 'Next 5 years' }, ]; -const rangeIndex: Record = {}; +const rangeIndex: Record = {}; 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 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); diff --git a/packages/grafana-data/src/types/time.ts b/packages/grafana-data/src/types/time.ts index 3ff7a360d68..edc92429ca6 100644 --- a/packages/grafana-data/src/types/time.ts +++ b/packages/grafana-data/src/types/time.ts @@ -41,6 +41,8 @@ export interface TimeOption { from: string; to: string; display: string; + invalid?: boolean; + section?: number; } /** @deprecated use TimeZone from schema */ diff --git a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts index 3c4319454c1..72aead958dd 100644 --- a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts +++ b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts @@ -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. */ diff --git a/pkg/kinds/dashboard/dashboard_spec_gen.go b/pkg/kinds/dashboard/dashboard_spec_gen.go index b8a357d8874..be217d196b9 100644 --- a/pkg/kinds/dashboard/dashboard_spec_gen.go +++ b/pkg/kinds/dashboard/dashboard_spec_gen.go @@ -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"` diff --git a/public/app/features/dashboard-scene/scene/PanelTimeRange.test.tsx b/public/app/features/dashboard-scene/scene/PanelTimeRange.test.tsx new file mode 100644 index 00000000000..0be845adc83 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/PanelTimeRange.test.tsx @@ -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); +} diff --git a/public/app/features/dashboard-scene/scene/PanelTimeRange.tsx b/public/app/features/dashboard-scene/scene/PanelTimeRange.tsx new file mode 100644 index 00000000000..f33d723c56a --- /dev/null +++ b/public/app/features/dashboard-scene/scene/PanelTimeRange.tsx @@ -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 implements SceneTimeRangeLike { + public static Component = PanelTimeRangeRenderer; + + public constructor(state: Partial = {}) { + 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) { + const { timeInfo, hideTimeOverride } = model.useState(); + const styles = useStyles2(getStyles); + + if (!timeInfo || hideTimeOverride) { + return null; + } + + return ( + }> + + {timeInfo} + + + ); +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + timeshift: css({ + color: theme.colors.text.link, + gap: theme.spacing(0.5), + }), + }; +}; diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap index 47fe78b5b92..73b4e697eb5 100644 --- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap +++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap @@ -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, diff --git a/public/app/features/dashboard-scene/serialization/testfiles/dashboard_to_load1.json b/public/app/features/dashboard-scene/serialization/testfiles/dashboard_to_load1.json index ddb900f0020..0127c5df5f8 100644 --- a/public/app/features/dashboard-scene/serialization/testfiles/dashboard_to_load1.json +++ b/public/app/features/dashboard-scene/serialization/testfiles/dashboard_to_load1.json @@ -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" diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts index ded61fd16c5..e8b530779f4 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts @@ -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(); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 27d7767c5ff..1d4538233c1 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -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 { 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): SceneGridItem { + return buildSceneFromPanelModel(new PanelModel(panel)); +} diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index 251ebe867d1..03c9601d5ba 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -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; } diff --git a/yarn.lock b/yarn.lock index 079f74c49e8..20d638d7c02 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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