mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
GeneralSettings: Edit general dashboards settings to scenes (#78492)
This commit is contained in:
parent
c354c7bfff
commit
e56a252158
@ -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"],
|
||||
|
@ -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 |
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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".
|
||||
*/
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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({
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -60,7 +60,7 @@
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 1,
|
||||
"graphTooltip": 0,
|
||||
"graphTooltip": 1,
|
||||
"id": 1351,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
|
@ -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,
|
||||
|
@ -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: {} });
|
||||
|
@ -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);
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
@ -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 };
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
@ -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());
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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({
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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: [] },
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user