GeneralSettings: Edit general dashboards settings to scenes (#78492)

This commit is contained in:
Ivan Ortega Alba 2023-12-01 16:04:56 +01:00 committed by GitHub
parent c354c7bfff
commit e56a252158
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1169 additions and 133 deletions

View File

@ -719,7 +719,8 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"]
[0, 0, 0, "Do not use any type assertions.", "5"],
[0, 0, 0, "Do not use any type assertions.", "6"]
],
"packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
@ -2655,6 +2656,9 @@ exports[`better eslint`] = {
"public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
],
"public/app/features/dashboard-scene/scene/DashboardScene.test.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
@ -2680,7 +2684,8 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"]
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"]
],
"public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],

View File

@ -88,7 +88,7 @@ extraFields is reserved for any fields that are pulled from the API server metad
| `tags` | string[] | No | | Tags associated with dashboard. |
| `templating` | [object](#templating) | No | | Configured template variables |
| `time` | [object](#time) | No | | Time range for dashboard.<br/>Accepted values are relative time strings like {from: 'now-6h', to: 'now'} or absolute time strings like {from: '2020-07-10T08:00:00.000Z', to: '2020-07-10T14:00:00.000Z'}. |
| `timepicker` | [object](#timepicker) | No | | Configuration of the time picker shown at the top of a dashboard. |
| `timepicker` | [TimePickerConfig](#timepickerconfig) | No | | Time picker configuration<br/>It defines the default config for the time picker and the refresh picker for the specific dashboard. |
| `timezone` | string | No | `browser` | Timezone of dashboard. Accepted values are IANA TZDB zone ID or "browser" or "utc". |
| `title` | string | No | | Title of dashboard. |
| `uid` | string | No | | Unique dashboard identifier that can be generated by anyone. string (8-40) |
@ -190,6 +190,17 @@ Sensitive information stripped: queries (metric, template,annotation) and panel
| `userId` | uint32 | **Yes** | | user id of the snapshot creator |
| `url` | string | No | | url of the snapshot, if snapshot was shared internally |
### TimePickerConfig
Time picker configuration
It defines the default config for the time picker and the refresh picker for the specific dashboard.
| Property | Type | Required | Default | Description |
|---------------------|----------|----------|---------------------------------------|---------------------------------------------------------------------------------------------------|
| `hidden` | boolean | **Yes** | `false` | Whether timepicker is visible or not. |
| `refresh_intervals` | string[] | **Yes** | `[5s 10s 30s 1m 5m 15m 30m 1h 2h 1d]` | Interval options available in the refresh picker dropdown. |
| `time_options` | string[] | **Yes** | `[5m 15m 1h 6h 12h 24h 2d 7d 30d]` | Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard. |
### Panels
| Property | Type | Required | Default | Description |
@ -625,17 +636,6 @@ Accepted values are relative time strings like {from: 'now-6h', to: 'now'} or ab
| `from` | string | **Yes** | `now-6h` | |
| `to` | string | **Yes** | `now` | |
### Timepicker
Configuration of the time picker shown at the top of a dashboard.
| Property | Type | Required | Default | Description |
|---------------------|----------|----------|---------------------------------------|---------------------------------------------------------------------------------------------------|
| `collapse` | boolean | **Yes** | `false` | Whether timepicker is collapsed or not. Has no effect on provisioned dashboard. |
| `hidden` | boolean | **Yes** | `false` | Whether timepicker is visible or not. |
| `refresh_intervals` | string[] | **Yes** | `[5s 10s 30s 1m 5m 15m 30m 1h 2h 1d]` | Interval options available in the refresh picker dropdown. |
| `time_options` | string[] | **Yes** | `[5m 15m 1h 6h 12h 24h 2d 7d 30d]` | Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard. |
### Status
| Property | Type | Required | Default | Description |

View File

@ -56,16 +56,7 @@ lineage: schemas: [{
}
// Configuration of the time picker shown at the top of a dashboard.
timepicker?: {
// Whether timepicker is visible or not.
hidden: bool | *false
// Interval options available in the refresh picker dropdown.
refresh_intervals: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"]
// Whether timepicker is collapsed or not. Has no effect on provisioned dashboard.
collapse: bool | *false
// Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.
time_options: [...string] | *["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
}
timepicker?: #TimePickerConfig
// The month that the fiscal year starts on. 0 = January, 11 = December
fiscalYearStartMonth?: uint8 & <12 | *0
@ -452,6 +443,17 @@ lineage: schemas: [{
options: _
} @cuetsy(kind="interface") @grafana(TSVeneer="type")
// Time picker configuration
// It defines the default config for the time picker and the refresh picker for the specific dashboard.
#TimePickerConfig: {
// Whether timepicker is visible or not.
hidden: bool | *false
// Interval options available in the refresh picker dropdown.
refresh_intervals: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"]
// Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.
time_options: [...string] | *["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
} @cuetsy(kind="interface") @grafana(TSVeneer="type")
// 0 for no shared crosshair or tooltip (default).
// 1 for shared crosshair.
// 2 for shared crosshair AND shared tooltip.

View File

@ -73,6 +73,7 @@ export type {
VariableModel,
DataSourceRef,
DataTransformerConfig,
TimePickerConfig,
Panel,
FieldConfigSource,
MatcherConfig,
@ -95,6 +96,7 @@ export {
defaultAnnotationQuery,
defaultVariableModel,
VariableHide,
defaultTimePickerConfig,
defaultPanel,
defaultFieldConfigSource,
defaultMatcherConfig,

View File

@ -625,6 +625,31 @@ export interface DataTransformerConfig {
options: unknown;
}
/**
* Time picker configuration
* It defines the default config for the time picker and the refresh picker for the specific dashboard.
*/
export interface TimePickerConfig {
/**
* Whether timepicker is visible or not.
*/
hidden: boolean;
/**
* Interval options available in the refresh picker dropdown.
*/
refresh_intervals: Array<string>;
/**
* Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.
*/
time_options: Array<string>;
}
export const defaultTimePickerConfig: Partial<TimePickerConfig> = {
hidden: false,
refresh_intervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'],
time_options: ['5m', '15m', '1h', '6h', '12h', '24h', '2d', '7d', '30d'],
};
/**
* 0 for no shared crosshair or tooltip (default).
* 1 for shared crosshair.
@ -1096,24 +1121,7 @@ export interface Dashboard {
/**
* Configuration of the time picker shown at the top of a dashboard.
*/
timepicker?: {
/**
* Whether timepicker is visible or not.
*/
hidden: boolean;
/**
* Interval options available in the refresh picker dropdown.
*/
refresh_intervals: Array<string>;
/**
* Whether timepicker is collapsed or not. Has no effect on provisioned dashboard.
*/
collapse: boolean;
/**
* Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.
*/
time_options: Array<string>;
};
timepicker?: TimePickerConfig;
/**
* Timezone of dashboard. Accepted values are IANA TZDB zone ID or "browser" or "utc".
*/

View File

@ -61,10 +61,13 @@ export interface DataTransformerConfig<TOptions = any> extends raw.DataTransform
options: TOptions;
}
export interface TimePickerConfig extends raw.TimePickerConfig {}
export const defaultDashboard = raw.defaultDashboard as Dashboard;
export const defaultVariableModel = {
...raw.defaultVariableModel,
} as VariableModel;
export const defaultTimePickerConfig = raw.defaultTimePickerConfig as TimePickerConfig;
export const defaultPanel: Partial<Panel> = raw.defaultPanel;
export const defaultRowPanel: Partial<Panel> = raw.defaultRowPanel;
export const defaultFieldConfig: Partial<FieldConfig> = raw.defaultFieldConfig;

View File

@ -767,20 +767,9 @@ type Spec struct {
To string `json:"to"`
} `json:"time,omitempty"`
// Configuration of the time picker shown at the top of a dashboard.
Timepicker *struct {
// Whether timepicker is collapsed or not. Has no effect on provisioned dashboard.
Collapse bool `json:"collapse"`
// Whether timepicker is visible or not.
Hidden bool `json:"hidden"`
// Interval options available in the refresh picker dropdown.
RefreshIntervals []string `json:"refresh_intervals"`
// Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.
TimeOptions []string `json:"time_options"`
} `json:"timepicker,omitempty"`
// Time picker configuration
// It defines the default config for the time picker and the refresh picker for the specific dashboard.
Timepicker *TimePickerConfig `json:"timepicker,omitempty"`
// Timezone of dashboard. Accepted values are IANA TZDB zone ID or "browser" or "utc".
Timezone *string `json:"timezone,omitempty"`
@ -850,6 +839,19 @@ type ThresholdsConfig struct {
// Thresholds can either be `absolute` (specific number) or `percentage` (relative to min or max, it will be values between 0 and 1).
type ThresholdsMode string
// Time picker configuration
// It defines the default config for the time picker and the refresh picker for the specific dashboard.
type TimePickerConfig struct {
// Whether timepicker is visible or not.
Hidden bool `json:"hidden"`
// Interval options available in the refresh picker dropdown.
RefreshIntervals []string `json:"refresh_intervals"`
// Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.
TimeOptions []string `json:"time_options"`
}
// Maps text values to a color or different display text and color.
// For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.
type ValueMap struct {

View File

@ -3,6 +3,8 @@ import {
sceneGraph,
SceneGridItem,
SceneGridLayout,
SceneRefreshPicker,
SceneTimeRange,
SceneQueryRunner,
SceneVariableSet,
TestVariable,
@ -12,8 +14,11 @@ import appEvents from 'app/core/app_events';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { VariablesChanged } from 'app/features/variables/types';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { djb2Hash } from '../utils/djb2Hash';
import { DashboardControls } from './DashboardControls';
import { DashboardLinksControls } from './DashboardLinksControls';
import { DashboardScene, DashboardSceneState } from './DashboardScene';
describe('DashboardScene', () => {
@ -45,12 +50,54 @@ describe('DashboardScene', () => {
expect(scene.state.isDirty).toBe(true);
// verify can discard change
scene.onDiscard();
const gridItem2 = sceneGraph.findObject(scene, (p) => p.state.key === 'griditem-1') as SceneGridItem;
expect(gridItem2.state.x).toBe(0);
});
it.each`
prop | value
${'title'} | ${'new title'}
${'description'} | ${'new description'}
${'tags'} | ${['tag1', 'tag2']}
${'editable'} | ${false}
${'graphTooltip'} | ${1}
`(
'A change to $prop should set isDirty true',
({ prop, value }: { prop: keyof DashboardSceneState; value: any }) => {
scene.setState({ [prop]: value });
expect(scene.state.isDirty).toBe(true);
// TODO: Discard doesn't restore the previous state
// scene.onDiscard();
// expect(scene.state[prop]).toBe(prevState);
}
);
// TODO: Make the dashboard to restore the defaults on discard
it.skip('A change to refresh picker interval settings should set isDirty true', () => {
const refreshPicker = dashboardSceneGraph.getRefreshPicker(scene)!;
const prevState = refreshPicker.state.intervals;
refreshPicker.setState({ intervals: ['10s'] });
expect(scene.state.isDirty).toBe(true);
scene.onDiscard();
expect(refreshPicker.state.intervals).toEqual(prevState);
});
// TODO: Make the dashboard to restore the defaults on discard
it.skip('A change to time zone should set isDirty true', () => {
const timeRange = scene.state.$timeRange!;
const prevState = timeRange.state.timeZone;
timeRange.setState({ timeZone: 'UTC' });
expect(scene.state.isDirty).toBe(true);
scene.onDiscard();
expect(timeRange.state.timeZone).toBe(prevState);
});
});
});
@ -101,6 +148,18 @@ function buildTestScene(overrides?: Partial<DashboardSceneState>) {
const scene = new DashboardScene({
title: 'hello',
uid: 'dash-1',
$timeRange: new SceneTimeRange({}),
controls: [
new DashboardControls({
variableControls: [],
linkControls: new DashboardLinksControls({}),
timeControls: [
new SceneRefreshPicker({
intervals: ['1s'],
}),
],
}),
],
body: new SceneGridLayout({
children: [
new SceneGridItem({

View File

@ -12,6 +12,8 @@ import {
SceneObjectBase,
SceneObjectState,
SceneObjectStateChangedEvent,
SceneRefreshPicker,
SceneTimeRange,
sceneUtils,
SceneVariable,
SceneVariableDependencyConfigLike,
@ -35,13 +37,19 @@ import { DashboardSceneUrlSync } from './DashboardSceneUrlSync';
import { ViewPanelScene } from './ViewPanelScene';
import { setupKeyboardShortcuts } from './keyboardShortcuts';
export const PERSISTED_PROPS = ['title', 'description', 'tags', 'editable', 'graphTooltip'];
export interface DashboardSceneState extends SceneObjectState {
/** The title */
title: string;
/** The description */
description?: string;
/** Tags */
tags?: string[];
/** Links */
links?: DashboardLink[];
/** Is editable */
editable?: boolean;
/** A uid when saved */
uid?: string;
/** @deprecated */
@ -69,6 +77,7 @@ export interface DashboardSceneState extends SceneObjectState {
}
export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
static listenToChangesInProps = PERSISTED_PROPS;
static Component = DashboardSceneRenderer;
/**
@ -97,6 +106,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
super({
title: 'Dashboard',
meta: {},
editable: true,
body: state.body ?? new SceneFlexLayout({ children: [] }),
...state,
});
@ -223,9 +233,22 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
this._changeTrackerSub = this.subscribeToEvent(
SceneObjectStateChangedEvent,
(event: SceneObjectStateChangedEvent) => {
if (event.payload.changedObject instanceof SceneRefreshPicker) {
if (Object.prototype.hasOwnProperty.call(event.payload.partialUpdate, 'intervals')) {
this.setIsDirty();
}
}
if (event.payload.changedObject instanceof SceneGridItem) {
this.setIsDirty();
}
if (event.payload.changedObject instanceof DashboardScene) {
if (Object.keys(event.payload.partialUpdate).some((key) => PERSISTED_PROPS.includes(key))) {
this.setIsDirty();
}
}
if (event.payload.changedObject instanceof SceneTimeRange) {
this.setIsDirty();
}
}
);
}

View File

@ -186,7 +186,10 @@ exports[`transformSceneToSaveModel Given a scene with rows Should transform back
},
],
"schemaVersion": 36,
"tags": [],
"tags": [
"templating",
"gdev",
],
"templating": {
"list": [
{
@ -231,6 +234,32 @@ exports[`transformSceneToSaveModel Given a scene with rows Should transform back
"from": "now-6h",
"to": "now",
},
"timepicker": {
"hidden": false,
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d",
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d",
],
},
"timezone": "",
"title": "Repeating rows",
"uid": "Repeating-rows-uid",
@ -238,6 +267,328 @@ exports[`transformSceneToSaveModel Given a scene with rows Should transform back
}
`;
exports[`transformSceneToSaveModel Given a simple scene with custom settings Should transform back to persisted model 1`] = `
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "datasource",
"uid": "grafana",
},
"enable": true,
"hide": false,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard",
},
{
"datasource": {
"type": "testdata",
"uid": "gdev-testdata",
},
"enable": true,
"hide": false,
"iconColor": "red",
"name": "Enabled",
"target": {
"lines": 4,
"refId": "Anno",
"scenarioId": "annotations",
},
},
{
"datasource": {
"type": "testdata",
"uid": "gdev-testdata",
},
"enable": false,
"hide": false,
"iconColor": "yellow",
"name": "Disabled",
"target": {
"lines": 5,
"refId": "Anno",
"scenarioId": "annotations",
},
},
{
"datasource": {
"type": "testdata",
"uid": "gdev-testdata",
},
"enable": true,
"hide": true,
"iconColor": "dark-purple",
"name": "Hidden",
"target": {
"lines": 6,
"refId": "Anno",
"scenarioId": "annotations",
},
},
],
},
"description": "My custom description",
"editable": false,
"fiscalYearStartMonth": 1,
"graphTooltip": 0,
"id": 1351,
"links": [],
"panels": [
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A",
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic",
},
"custom": {
"fillOpacity": 0,
"gradientMode": "none",
"lineWidth": 2,
},
},
"overrides": [],
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0,
},
"id": 28,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true,
},
"tooltip": {
"mode": "single",
"sort": "none",
},
},
"targets": [
{
"alias": "series",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A",
},
"refId": "A",
"scenarioId": "random_walk",
"seriesCount": 1,
},
],
"title": "Simple time series graph ",
"transformations": [],
"transparent": false,
"type": "timeseries",
},
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 8,
},
"id": 5,
"panels": [],
"title": "Row title",
"type": "row",
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A",
},
"fieldConfig": {
"defaults": {},
"overrides": [],
},
"gridPos": {
"h": 10,
"w": 12,
"x": 0,
"y": 9,
},
"id": 29,
"options": {},
"targets": [
{
"alias": "series",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A",
},
"refId": "A",
"scenarioId": "random_walk",
"seriesCount": 1,
},
],
"title": "panel inside row",
"transformations": [],
"transparent": false,
"type": "timeseries",
},
{
"fieldConfig": {
"defaults": {},
"overrides": [],
},
"gridPos": {
"h": 10,
"w": 11,
"x": 12,
"y": 9,
},
"id": 25,
"options": {
"code": {
"language": "plaintext",
"showLineNumbers": false,
"showMiniMap": false,
},
"content": "content",
"mode": "markdown",
},
"title": "Transparent text panel",
"transformations": [],
"transparent": true,
"type": "text",
},
],
"schemaVersion": 36,
"tags": [
"tag1",
"tag2",
],
"templating": {
"list": [
{
"auto": true,
"auto_count": 30,
"auto_min": "10s",
"current": {
"text": "1m",
"value": "1m",
},
"hide": 2,
"name": "intervalVar",
"query": "1m,10m,30m,1h,6h,12h,1d,7d,14d,30d",
"refresh": 2,
"type": "interval",
},
{
"current": {
"text": [
"a",
],
"value": [
"a",
],
},
"includeAll": true,
"multi": true,
"name": "customVar",
"options": [],
"query": "a, b, c",
"type": "custom",
},
{
"current": {
"text": "gdev-testdata",
"value": "PD8C576611E62080A",
},
"includeAll": false,
"name": "dsVar",
"options": [],
"query": "grafana-testdata-datasource",
"refresh": 1,
"regex": "",
"type": "datasource",
},
{
"current": {
"text": "A",
"value": "A",
},
"includeAll": false,
"name": "query0",
"options": [],
"query": {
"query": "*",
"refId": "StandardVariableQuery",
},
"refresh": 1,
"regex": "",
"type": "query",
},
{
"current": {
"text": "test",
"value": "test",
},
"hide": 2,
"name": "constant",
"query": "test",
"skipUrlSync": true,
"type": "constant",
},
{
"datasource": {
"type": "prometheus",
"uid": "wc2AL7L7k",
},
"name": "Filters",
"type": "adhoc",
},
],
},
"time": {
"from": "now-5m",
"to": "now",
},
"timepicker": {
"hidden": false,
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d",
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d",
],
},
"timezone": "America/New_York",
"title": "My custom title",
"uid": "nP8rcffGkasd",
"weekStart": "monday",
}
`;
exports[`transformSceneToSaveModel Given a simple scene with variables Should transform back to persisted model 1`] = `
{
"annotations": {
@ -436,7 +787,11 @@ exports[`transformSceneToSaveModel Given a simple scene with variables Should tr
},
],
"schemaVersion": 36,
"tags": [],
"tags": [
"gdev",
"graph-ng",
"demo",
],
"templating": {
"list": [
{
@ -523,6 +878,31 @@ exports[`transformSceneToSaveModel Given a simple scene with variables Should tr
"from": "now-5m",
"to": "now",
},
"timepicker": {
"hidden": false,
"refresh_intervals": [
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d",
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d",
],
},
"timezone": "America/New_York",
"title": "Dashboard to load1",
"uid": "nP8rcffGkasd",

View File

@ -60,7 +60,7 @@
},
"editable": true,
"fiscalYearStartMonth": 1,
"graphTooltip": 0,
"graphTooltip": 1,
"id": 1351,
"links": [],
"liveNow": false,

View File

@ -229,6 +229,8 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel)
links: oldModel.links || [],
uid: oldModel.uid,
id: oldModel.id,
description: oldModel.description,
editable: oldModel.editable,
meta: oldModel.meta,
body: new SceneGridLayout({
isLazy: true,

View File

@ -168,6 +168,31 @@ describe('transformSceneToSaveModel', () => {
getPluginLinkExtensionsMock.mockReturnValue({ extensions: [] });
});
describe('Given a simple scene with custom settings', () => {
it('Should transform back to persisted model', () => {
const dashboardWithCustomSettings = {
...dashboard_to_load1,
title: 'My custom title',
description: 'My custom description',
tags: ['tag1', 'tag2'],
timezone: 'America/New_York',
weekStart: 'monday',
graphTooltip: 1,
editable: false,
timepicker: {
...dashboard_to_load1.timepicker,
refresh_intervals: ['5m', '15m', '30m', '1h'],
time_options: ['5m', '15m', '30m'],
hidden: true,
},
};
const scene = transformSaveModelToScene({ dashboard: dashboardWithCustomSettings as any, meta: {} });
const saveModel = transformSceneToSaveModel(scene);
expect(saveModel).toMatchSnapshot();
});
});
describe('Given a simple scene with variables', () => {
it('Should transform back to persisted model', () => {
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} });

View File

@ -1,5 +1,6 @@
import { isEmptyObject, ScopedVars, TimeRange } from '@grafana/data';
import {
behaviors,
SceneDataLayers,
SceneGridItem,
SceneGridItemLike,
@ -11,12 +12,14 @@ import {
SceneVariableSet,
AdHocFilterSet,
LocalValueVariable,
SceneRefreshPicker,
} from '@grafana/scenes';
import {
AnnotationQuery,
Dashboard,
DataTransformerConfig,
defaultDashboard,
defaultTimePickerConfig,
FieldConfigSource,
Panel,
RowPanel,
@ -47,8 +50,9 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa
const data = state.$data;
const variablesSet = state.$variables;
const body = state.body;
let refresh_intervals = defaultTimePickerConfig.refresh_intervals;
let panels: Panel[] = [];
let graphTooltip = defaultDashboard.graphTooltip;
let variables: VariableModel[] = [];
if (body instanceof SceneGridLayout) {
@ -83,6 +87,12 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa
}
if (state.controls && state.controls[0] instanceof DashboardControls) {
const timeControls = state.controls[0].state.timeControls;
for (const control of timeControls) {
if (control instanceof SceneRefreshPicker && control.state.intervals) {
refresh_intervals = control.state.intervals;
}
}
const variableControls = state.controls[0].state.variableControls;
for (const control of variableControls) {
if (control instanceof AdHocFilterSet) {
@ -95,15 +105,25 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa
}
}
if (state.$behaviors && state.$behaviors[0] instanceof behaviors.CursorSync) {
graphTooltip = state.$behaviors[0].state.sync;
}
const dashboard: Dashboard = {
...defaultDashboard,
title: state.title,
description: state.description || undefined,
uid: state.uid,
id: state.id,
editable: state.editable,
time: {
from: timeRange.from,
to: timeRange.to,
},
timepicker: {
...defaultTimePickerConfig,
refresh_intervals,
},
panels,
annotations: {
list: annotations,
@ -114,6 +134,8 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa
timezone: timeRange.timeZone,
fiscalYearStartMonth: timeRange.fiscalYearStartMonth,
weekStart: timeRange.weekStart,
tags: state.tags,
graphTooltip,
};
return sortedDeepCloneWithoutNulls(dashboard);

View File

@ -7,7 +7,6 @@ import { Page } from 'app/core/components/Page/Page';
import { NavToolbarActions } from '../scene/NavToolbarActions';
import { getDashboardSceneFor } from '../utils/utils';
import { GeneralSettingsEditView } from './GeneralSettings';
import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils';
export interface AnnotationsEditViewState extends DashboardEditViewState {}
@ -17,7 +16,7 @@ export class AnnotationsEditView extends SceneObjectBase<AnnotationsEditViewStat
return 'annotations';
}
static Component = ({ model }: SceneComponentProps<GeneralSettingsEditView>) => {
static Component = ({ model }: SceneComponentProps<AnnotationsEditView>) => {
const dashboard = getDashboardSceneFor(model);
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());

View File

@ -1,33 +0,0 @@
import React from 'react';
import { PageLayoutType } from '@grafana/data';
import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes';
import { Page } from 'app/core/components/Page/Page';
import { NavToolbarActions } from '../scene/NavToolbarActions';
import { getDashboardSceneFor } from '../utils/utils';
import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils';
export interface GeneralSettingsEditViewState extends DashboardEditViewState {}
export class GeneralSettingsEditView
extends SceneObjectBase<GeneralSettingsEditViewState>
implements DashboardEditView
{
public getUrlKey(): string {
return 'settings';
}
static Component = ({ model }: SceneComponentProps<GeneralSettingsEditView>) => {
const dashboard = getDashboardSceneFor(model);
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
return (
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
<NavToolbarActions dashboard={dashboard} />
<div>General todo</div>
</Page>
);
};
}

View File

@ -0,0 +1,160 @@
import { behaviors, SceneGridLayout, SceneGridItem, SceneRefreshPicker, SceneTimeRange } from '@grafana/scenes';
import { DashboardCursorSync } from '@grafana/schema';
import { DashboardControls } from '../scene/DashboardControls';
import { DashboardLinksControls } from '../scene/DashboardLinksControls';
import { DashboardScene } from '../scene/DashboardScene';
import { activateFullSceneTree } from '../utils/test-utils';
import { GeneralSettingsEditView } from './GeneralSettingsEditView';
describe('GeneralSettingsEditView', () => {
describe('Dashboard state', () => {
let dashboard: DashboardScene;
let settings: GeneralSettingsEditView;
beforeEach(async () => {
const result = await buildTestScene();
dashboard = result.dashboard;
settings = result.settings;
});
it('should return the correct urlKey', () => {
expect(settings.getUrlKey()).toBe('settings');
});
it('should return the dashboard', () => {
expect(settings.getDashboard()).toBe(dashboard);
});
it('should return the dashboard time range', () => {
expect(settings.getTimeRange()).toBe(dashboard.state.$timeRange);
});
it('should return the dashboard refresh picker', () => {
expect(settings.getRefreshPicker()).toBe(
(dashboard.state?.controls?.[0] as DashboardControls)?.state?.timeControls?.[0]
);
});
it('should return the cursor sync', () => {
expect(settings.getCursorSync()).toBe(dashboard.state.$behaviors?.[0]);
});
});
describe('Dashboard updates', () => {
let dashboard: DashboardScene;
let settings: GeneralSettingsEditView;
beforeEach(async () => {
const result = await buildTestScene();
dashboard = result.dashboard;
settings = result.settings;
});
it('should have isDirty false', () => {
expect(dashboard.state.isDirty).toBeFalsy();
});
it('A change to title updates the dashboard state', () => {
settings.onTitleChange('new title');
expect(dashboard.state.title).toBe('new title');
});
it('A change to description updates the dashboard state', () => {
settings.onDescriptionChange('new description');
expect(dashboard.state.description).toBe('new description');
});
it('A change to description updates the dashboard state', () => {
settings.onTagsChange(['tag1', 'tag2']);
expect(dashboard.state.tags).toEqual(['tag1', 'tag2']);
});
it('A change to editable permissions updates the dashboard state', () => {
settings.onEditableChange(false);
expect(dashboard.state.editable).toBe(false);
});
it('A change to timezone updates the dashboard state', () => {
settings.onTimeZoneChange('UTC');
expect(dashboard.state.$timeRange?.state.timeZone).toBe('UTC');
});
it('A change to week start updates the dashboard state', () => {
settings.onWeekStartChange('monday');
expect(settings.getTimeRange().state.weekStart).toBe('monday');
});
it('A change to refresh interval updates the dashboard state', () => {
settings.onRefreshIntervalChange(['5s']);
expect(settings.getRefreshPicker()?.state?.intervals).toEqual(['5s']);
});
it('A change to folder updates the dashboard state', () => {
settings.onFolderChange('folder-2', 'folder 2');
expect(dashboard.state.meta.folderUid).toBe('folder-2');
expect(dashboard.state.meta.folderTitle).toBe('folder 2');
});
it('A change to tooltip settings updates the dashboard state', () => {
settings.onTooltipChange(DashboardCursorSync.Crosshair);
expect(settings.getCursorSync()?.state.sync).toBe(DashboardCursorSync.Crosshair);
});
});
});
async function buildTestScene() {
const dashboard = new DashboardScene({
$timeRange: new SceneTimeRange({}),
$behaviors: [new behaviors.CursorSync({ sync: DashboardCursorSync.Off })],
controls: [
new DashboardControls({
variableControls: [],
linkControls: new DashboardLinksControls({}),
timeControls: [
new SceneRefreshPicker({
intervals: ['1s'],
}),
],
}),
],
title: 'hello',
uid: 'dash-1',
meta: {
canEdit: true,
},
body: new SceneGridLayout({
children: [
new SceneGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 10,
height: 12,
body: undefined,
}),
],
}),
});
const settings = new GeneralSettingsEditView({
dashboardRef: dashboard.getRef(),
});
activateFullSceneTree(dashboard);
await new Promise((r) => setTimeout(r, 1));
dashboard.onEnterEditMode();
settings.activate();
return { dashboard, settings };
}

View File

@ -0,0 +1,276 @@
import React, { ChangeEvent } from 'react';
import { PageLayoutType } from '@grafana/data';
import {
behaviors,
SceneComponentProps,
SceneObjectBase,
SceneObjectRef,
SceneTimePicker,
sceneGraph,
} from '@grafana/scenes';
import { TimeZone } from '@grafana/schema';
import {
Box,
CollapsableSection,
Field,
HorizontalGroup,
Input,
Label,
RadioButtonGroup,
TagsInput,
TextArea,
} from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
import { t, Trans } from 'app/core/internationalization';
import { TimePickerSettings } from 'app/features/dashboard/components/DashboardSettings/TimePickerSettings';
import { DeleteDashboardButton } from 'app/features/dashboard/components/DeleteDashboard/DeleteDashboardButton';
import { DashboardControls } from '../scene/DashboardControls';
import { DashboardScene } from '../scene/DashboardScene';
import { NavToolbarActions } from '../scene/NavToolbarActions';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils';
export interface GeneralSettingsEditViewState extends DashboardEditViewState {
dashboardRef: SceneObjectRef<DashboardScene>;
}
const EDITABLE_OPTIONS = [
{ label: 'Editable', value: true },
{ label: 'Read-only', value: false },
];
const GRAPH_TOOLTIP_OPTIONS = [
{ value: 0, label: 'Default' },
{ value: 1, label: 'Shared crosshair' },
{ value: 2, label: 'Shared Tooltip' },
];
export class GeneralSettingsEditView
extends SceneObjectBase<GeneralSettingsEditViewState>
implements DashboardEditView
{
private get _dashboard(): DashboardScene {
return this.state.dashboardRef.resolve();
}
public getUrlKey(): string {
return 'settings';
}
public getDashboard(): DashboardScene {
return this._dashboard;
}
public getTimeRange() {
return sceneGraph.getTimeRange(this._dashboard);
}
public getRefreshPicker() {
return dashboardSceneGraph.getRefreshPicker(this._dashboard);
}
public getCursorSync() {
const cursorSync = this._dashboard.state.$behaviors?.find((b) => b instanceof behaviors.CursorSync);
if (cursorSync instanceof behaviors.CursorSync) {
return cursorSync;
}
return;
}
public onTitleChange = (value: string) => {
this._dashboard.setState({ title: value });
};
public onDescriptionChange = (value: string) => {
this._dashboard.setState({ description: value });
};
public onTagsChange = (value: string[]) => {
this._dashboard.setState({ tags: value });
};
public onFolderChange = (newUID: string, newTitle: string) => {
const newMeta = {
...this._dashboard.state.meta,
folderUid: newUID || this._dashboard.state.meta.folderUid,
folderTitle: newTitle || this._dashboard.state.meta.folderTitle,
hasUnsavedFolderChange: true,
};
this._dashboard.setState({ meta: newMeta });
};
public onEditableChange = (value: boolean) => {
this._dashboard.setState({ editable: value });
};
public onTimeZoneChange = (value: TimeZone) => {
this.getTimeRange().setState({
timeZone: value,
});
};
public onWeekStartChange = (value: string) => {
this.getTimeRange().setState({
weekStart: value,
});
};
public onRefreshIntervalChange = (value: string[]) => {
const control = this.getRefreshPicker();
control?.setState({
intervals: value,
});
};
public onNowDelayChange = (value: string) => {
// TODO: Figure out how to store nowDelay in Dashboard Scene
};
public onHideTimePickerChange = (value: boolean) => {
if (this._dashboard.state.controls instanceof DashboardControls) {
for (const control of this._dashboard.state.controls.state.timeControls) {
if (control instanceof SceneTimePicker) {
control.setState({
// TODO: Control visibility from DashboardControls
// hidden: value,
});
}
}
}
};
public onLiveNowChange = (value: boolean) => {
// TODO: Figure out how to store liveNow in Dashboard Scene
};
public onTooltipChange = (value: number) => {
this.getCursorSync()?.setState({ sync: value });
};
static Component = ({ model }: SceneComponentProps<GeneralSettingsEditView>) => {
const { navModel, pageNav } = useDashboardEditPageNav(model.getDashboard(), model.getUrlKey());
const { title, description, tags, meta, editable, overlay } = model.getDashboard().useState();
const { sync: graphTooltip } = model.getCursorSync()?.useState() || {};
const { timeZone, weekStart } = model.getTimeRange().useState();
const { intervals } = model.getRefreshPicker()?.useState() || {};
return (
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
<NavToolbarActions dashboard={model.getDashboard()} />
<div style={{ maxWidth: '600px' }}>
<Box marginBottom={5}>
<Field
label={
<HorizontalGroup justify="space-between">
<Label htmlFor="title-input">
<Trans i18nKey="dashboard-settings.general.title-label">Title</Trans>
</Label>
{/* TODO: Make the component use persisted model */}
{/* {config.featureToggles.dashgpt && (
<GenAIDashTitleButton onGenerate={onTitleChange} dashboard={dashboard} />
)} */}
</HorizontalGroup>
}
>
<Input
id="title-input"
name="title"
defaultValue={title}
onBlur={(e: ChangeEvent<HTMLInputElement>) => model.onTitleChange(e.target.value)}
/>
</Field>
<Field
label={
<HorizontalGroup justify="space-between">
<Label htmlFor="description-input">
{t('dashboard-settings.general.description-label', 'Description')}
</Label>
{/* {config.featureToggles.dashgpt && (
<GenAIDashDescriptionButton onGenerate={onDescriptionChange} dashboard={dashboard} />
)} */}
</HorizontalGroup>
}
>
<TextArea
id="description-input"
name="description"
defaultValue={description}
onBlur={(e: ChangeEvent<HTMLTextAreaElement>) => model.onDescriptionChange(e.target.value)}
/>
</Field>
<Field label={t('dashboard-settings.general.tags-label', 'Tags')}>
<TagsInput id="tags-input" tags={tags} onChange={model.onTagsChange} width={40} />
</Field>
<Field label={t('dashboard-settings.general.folder-label', 'Folder')}>
<FolderPicker
value={meta.folderUid}
onChange={model.onFolderChange}
// TODO deprecated props that can be removed once NestedFolderPicker is enabled by default
initialTitle={meta.folderTitle}
inputId="dashboard-folder-input"
enableCreateNew
skipInitialLoad
/>
</Field>
<Field
label={t('dashboard-settings.general.editable-label', 'Editable')}
description={t(
'dashboard-settings.general.editable-description',
'Set to read-only to disable all editing. Reload the dashboard for changes to take effect'
)}
>
<RadioButtonGroup value={editable} options={EDITABLE_OPTIONS} onChange={model.onEditableChange} />
</Field>
</Box>
<TimePickerSettings
onTimeZoneChange={model.onTimeZoneChange}
onWeekStartChange={model.onWeekStartChange}
onRefreshIntervalChange={model.onRefreshIntervalChange}
onNowDelayChange={model.onNowDelayChange}
onHideTimePickerChange={model.onHideTimePickerChange}
onLiveNowChange={model.onLiveNowChange}
refreshIntervals={intervals}
// TODO: Control visibility of time picker
// timePickerHidden={timepicker?.state?.hidden}
// TODO: Implement this in dashboard scene
// nowDelay={timepicker.nowDelay || ''}
// TODO: Implement this in dashboard scene
// liveNow={liveNow}
liveNow={false}
timezone={timeZone || ''}
weekStart={weekStart || ''}
/>
{/* @todo: Update "Graph tooltip" description to remove prompt about reloading when resolving #46581 */}
<CollapsableSection
label={t('dashboard-settings.general.panel-options-label', 'Panel options')}
isOpen={true}
>
<Field
label={t('dashboard-settings.general.panel-options-graph-tooltip-label', 'Graph tooltip')}
description={t(
'dashboard-settings.general.panel-options-graph-tooltip-description',
'Controls tooltip and hover highlight behavior across different panels. Reload the dashboard for changes to take effect'
)}
>
<RadioButtonGroup onChange={model.onTooltipChange} options={GRAPH_TOOLTIP_OPTIONS} value={graphTooltip} />
</Field>
</CollapsableSection>
<Box marginTop={3}>{meta.canDelete && <DeleteDashboardButton />}</Box>
</div>
{overlay && <overlay.Component model={overlay} />}
</Page>
);
};
}

View File

@ -7,7 +7,6 @@ import { Page } from 'app/core/components/Page/Page';
import { NavToolbarActions } from '../scene/NavToolbarActions';
import { getDashboardSceneFor } from '../utils/utils';
import { GeneralSettingsEditView } from './GeneralSettings';
import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils';
export interface VariablesEditViewState extends DashboardEditViewState {}
@ -17,7 +16,7 @@ export class VariablesEditView extends SceneObjectBase<VariablesEditViewState> i
return 'variables';
}
static Component = ({ model }: SceneComponentProps<GeneralSettingsEditView>) => {
static Component = ({ model }: SceneComponentProps<VariablesEditView>) => {
const dashboard = getDashboardSceneFor(model);
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());

View File

@ -10,7 +10,7 @@ import { DashboardScene } from '../scene/DashboardScene';
import { AnnotationsEditView } from './AnnotationsEditView';
import { DashboardLinksEditView } from './DashboardLinksEditView';
import { GeneralSettingsEditView } from './GeneralSettings';
import { GeneralSettingsEditView } from './GeneralSettingsEditView';
import { VariablesEditView } from './VariablesEditView';
export interface DashboardEditViewState extends SceneObjectState {

View File

@ -1,7 +1,17 @@
import { TimeRangeUpdatedEvent } from '@grafana/runtime';
import { behaviors, SceneGridItem, SceneGridLayout, SceneQueryRunner, SceneTimeRange, VizPanel } from '@grafana/scenes';
import {
behaviors,
SceneGridItem,
SceneGridLayout,
SceneRefreshPicker,
SceneQueryRunner,
SceneTimeRange,
VizPanel,
} from '@grafana/scenes';
import { DashboardCursorSync } from '@grafana/schema';
import { DashboardControls } from '../scene/DashboardControls';
import { DashboardLinksControls } from '../scene/DashboardLinksControls';
import { DashboardScene } from '../scene/DashboardScene';
import { DashboardModelCompatibilityWrapper } from './DashboardModelCompatibilityWrapper';
@ -12,8 +22,14 @@ describe('DashboardModelCompatibilityWrapper', () => {
expect(wrapper.uid).toBe('dash-1');
expect(wrapper.title).toBe('hello');
expect(wrapper.description).toBe('hello description');
expect(wrapper.editable).toBe(false);
expect(wrapper.graphTooltip).toBe(DashboardCursorSync.Off);
expect(wrapper.tags).toEqual(['hello-tag']);
expect(wrapper.time.from).toBe('now-6h');
expect(wrapper.timezone).toBe('America/New_York');
expect(wrapper.weekStart).toBe('friday');
expect(wrapper.timepicker.refresh_intervals).toEqual(['1s']);
});
it('Shared tooltip functions', () => {
@ -25,6 +41,7 @@ describe('DashboardModelCompatibilityWrapper', () => {
expect(wrapper.sharedTooltipModeEnabled()).toBe(true);
expect(wrapper.sharedCrosshairModeOnly()).toBe(true);
expect(wrapper.graphTooltip).toBe(DashboardCursorSync.Crosshair);
});
it('Get timezone from time range', () => {
@ -60,10 +77,25 @@ describe('DashboardModelCompatibilityWrapper', () => {
function setup() {
const scene = new DashboardScene({
title: 'hello',
description: 'hello description',
tags: ['hello-tag'],
uid: 'dash-1',
editable: false,
$timeRange: new SceneTimeRange({
weekStart: 'friday',
timeZone: 'America/New_York',
}),
controls: [
new DashboardControls({
variableControls: [],
linkControls: new DashboardLinksControls({}),
timeControls: [
new SceneRefreshPicker({
intervals: ['1s'],
}),
],
}),
],
body: new SceneGridLayout({
children: [
new SceneGridItem({

View File

@ -14,6 +14,7 @@ import {
import { DashboardScene } from '../scene/DashboardScene';
import { dashboardSceneGraph } from './dashboardSceneGraph';
import { findVizPanelByKey, getPanelIdForVizPanel, getVizPanelKeyForPanelId } from './utils';
/**
@ -47,6 +48,36 @@ export class DashboardModelCompatibilityWrapper {
return this._scene.state.title;
}
public get description() {
return this._scene.state.description;
}
public get editable() {
return this._scene.state.editable;
}
public get graphTooltip() {
return this._getSyncMode();
}
public get timepicker() {
return {
refresh_intervals: dashboardSceneGraph.getRefreshPicker(this._scene)?.state.intervals,
};
}
public get timezone() {
return this.getTimezone();
}
public get weekStart() {
return sceneGraph.getTimeRange(this._scene).state.weekStart;
}
public get tags() {
return this._scene.state.tags;
}
public get meta() {
return this._scene.state.meta;
}

View File

@ -12,7 +12,6 @@ describe('dashboardSceneGraph', () => {
...(dashboard_to_load as unknown as DashboardDataDTO),
timepicker: {
hidden: true,
collapse: false,
refresh_intervals: [],
time_options: [],
},
@ -36,4 +35,34 @@ describe('dashboardSceneGraph', () => {
expect(timePicker).not.toBeNull();
});
});
describe('getRefreshPicker', () => {
it('should return null if no refresh picker', () => {
const dashboard: DashboardDataDTO = {
...(dashboard_to_load as unknown as DashboardDataDTO),
timepicker: {
hidden: true,
refresh_intervals: [],
time_options: [],
},
};
const scene = transformSaveModelToScene({
dashboard: dashboard as unknown as DashboardDataDTO,
meta: {},
});
const refreshPicker = dashboardSceneGraph.getRefreshPicker(scene);
expect(refreshPicker).toBeNull();
});
it('should return refresh picker', () => {
const scene = transformSaveModelToScene({
dashboard: dashboard_to_load as unknown as DashboardDataDTO,
meta: {},
});
const refreshPicker = dashboardSceneGraph.getRefreshPicker(scene);
expect(refreshPicker).not.toBeNull();
});
});
});

View File

@ -1,4 +1,4 @@
import { SceneTimePicker } from '@grafana/scenes';
import { SceneTimePicker, SceneRefreshPicker } from '@grafana/scenes';
import { DashboardControls } from '../scene/DashboardControls';
import { DashboardScene } from '../scene/DashboardScene';
@ -17,6 +17,18 @@ function getTimePicker(scene: DashboardScene) {
return null;
}
function getRefreshPicker(scene: DashboardScene) {
if (scene.state.controls?.[0] instanceof DashboardControls) {
for (const control of scene.state.controls[0].state.timeControls) {
if (control instanceof SceneRefreshPicker) {
return control;
}
}
}
return null;
}
export const dashboardSceneGraph = {
getTimePicker,
getRefreshPicker,
};

View File

@ -6,7 +6,7 @@ import { t } from 'app/core/internationalization';
import { getTimeSrv } from '../../services/TimeSrv';
export interface Props {
refreshIntervals: string[];
refreshIntervals?: string[];
onRefreshIntervalChange: (interval: string[]) => void;
getIntervalsFunc?: typeof getValidIntervals;
validateIntervalsFunc?: typeof validateIntervals;

View File

@ -30,7 +30,6 @@ const setupTestContext = (options: Partial<Props>) => {
timepicker: {
refresh_intervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d', '2d'],
time_options: ['5m', '15m', '1h', '6h', '12h', '24h', '2d', '7d', '30d'],
collapse: true,
hidden: false,
},
timezone: 'utc',

View File

@ -222,7 +222,7 @@ export function GeneralSettingsUnconnected({
</Field>
</CollapsableSection>
<Box marginTop={3}>{dashboard.meta.canDelete && <DeleteDashboardButton dashboard={dashboard} />}</Box>
<Box marginTop={3}>{dashboard.meta.canDelete && <DeleteDashboardButton />}</Box>
</div>
</Page>
);

View File

@ -15,9 +15,9 @@ interface Props {
onNowDelayChange: (nowDelay: string) => void;
onHideTimePickerChange: (hide: boolean) => void;
onLiveNowChange: (liveNow: boolean) => void;
refreshIntervals: string[];
timePickerHidden: boolean;
nowDelay: string;
refreshIntervals?: string[];
timePickerHidden?: boolean;
nowDelay?: string;
timezone: TimeZone;
weekStart: string;
liveNow: boolean;

View File

@ -3,29 +3,28 @@ import React from 'react';
import { Button, ModalsController } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { DashboardModel } from '../../state';
import { getDashboardSrv } from '../../services/DashboardSrv';
import { DeleteDashboardModal } from './DeleteDashboardModal';
type Props = {
dashboard: DashboardModel;
export const DeleteDashboardButton = () => {
const dashboard = getDashboardSrv().getCurrent()!;
return (
<ModalsController>
{({ showModal, hideModal }) => (
<Button
variant="destructive"
onClick={() => {
showModal(DeleteDashboardModal, {
dashboard,
hideModal,
});
}}
aria-label="Dashboard settings page delete dashboard button"
>
<Trans i18nKey="dashboard-settings.dashboard-delete-button">Delete Dashboard</Trans>
</Button>
)}
</ModalsController>
);
};
export const DeleteDashboardButton = ({ dashboard }: Props) => (
<ModalsController>
{({ showModal, hideModal }) => (
<Button
variant="destructive"
onClick={() => {
showModal(DeleteDashboardModal, {
dashboard,
hideModal,
});
}}
aria-label="Dashboard settings page delete dashboard button"
>
<Trans i18nKey="dashboard-settings.dashboard-delete-button">Delete Dashboard</Trans>
</Button>
)}
</ModalsController>
);

View File

@ -249,7 +249,7 @@ describe('PublicDashboardPage', () => {
...dashboardBase,
getModel: () =>
getTestDashboard({
timepicker: { hidden: false, collapse: false, refresh_intervals: [], time_options: [] },
timepicker: { hidden: false, refresh_intervals: [], time_options: [] },
}),
},
});