mirror of https://github.com/grafana/grafana
GeneralSettings: Edit general dashboards settings to scenes (#78492)
parent
c354c7bfff
commit
e56a252158
@ -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> |
||||
); |
||||
}; |
||||
} |
Loading…
Reference in new issue