Chore: Define components outside of the scene class (#110180)

define components outside of the scene class
This commit is contained in:
Ashley Harrison
2025-08-29 09:05:46 +01:00
committed by GitHub
parent 9edfe7bc0b
commit de8930b92a
6 changed files with 477 additions and 491 deletions
+2 -17
View File
@@ -1734,8 +1734,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx:5381": [
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"],
[0, 0, 0, "React Hook \\"useStyles2\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"]
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"]
],
"public/app/features/dashboard-scene/pages/DashboardScenePage.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
@@ -1752,9 +1751,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx:5381": [
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"],
[0, 0, 0, "React Hook \\"useStyles2\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"],
[0, 0, 0, "React Hook \\"useToggle\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"]
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"]
],
"public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx:5381": [
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"]
@@ -1765,9 +1762,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "2"],
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "3"]
],
"public/app/features/dashboard-scene/saving/SaveDashboardDrawer.tsx:5381": [
[0, 0, 0, "React Hook \\"useIsProvisionedNG\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"]
],
"public/app/features/dashboard-scene/saving/SaveDashboardForm.tsx:5381": [
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"]
],
@@ -1866,15 +1860,6 @@ exports[`better eslint`] = {
"public/app/features/dashboard-scene/serialization/transformToV1TypesUtils.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx:5381": [
[0, 0, 0, "React Hook \\"useDashboardEditPageNav\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"]
],
"public/app/features/dashboard-scene/settings/JsonModelEditView.tsx:5381": [
[0, 0, 0, "React Hook \\"useDashboardEditPageNav\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"],
[0, 0, 0, "React Hook \\"useSaveDashboard\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"],
[0, 0, 0, "React Hook \\"useState\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"],
[0, 0, 0, "React Hook \\"useStyles2\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "3"]
],
"public/app/features/dashboard-scene/settings/annotations/AnnotationSettingsEdit.tsx:5381": [
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"],
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"],
@@ -166,48 +166,50 @@ export class InspectJsonTab extends SceneObjectBase<InspectJsonTabState> {
return dashboard.state.meta.canEdit;
}
static Component = ({ model }: SceneComponentProps<InspectJsonTab>) => {
const { source: show, jsonText } = model.useState();
const styles = useStyles2(getPanelInspectorStyles2);
const options = model.getOptions();
static Component = InspectJsonTabComponent;
}
return (
<div className={styles.wrap}>
<div className={styles.toolbar} data-testid={selectors.components.PanelInspector.Json.content}>
<Field label={t('dashboard.inspect-json.select-source', 'Select source')} className="flex-grow-1">
<Select
inputId="select-source-dropdown"
options={options}
value={options.find((v) => v.value === show) ?? options[0].value}
onChange={model.onChangeSource}
/>
</Field>
{model.isEditable() && (
<Button className={styles.toolbarItem} onClick={model.onApplyChange}>
<Trans i18nKey="dashboard-scene.inspect-json-tab.apply">Apply</Trans>
</Button>
)}
</div>
function InspectJsonTabComponent({ model }: SceneComponentProps<InspectJsonTab>) {
const { source: show, jsonText } = model.useState();
const styles = useStyles2(getPanelInspectorStyles2);
const options = model.getOptions();
<div className={styles.content}>
<AutoSizer disableWidth>
{({ height }) => (
<CodeEditor
width="100%"
height={height}
language="json"
showLineNumbers={true}
showMiniMap={jsonText.length > 100}
value={jsonText}
readOnly={!model.isEditable()}
onBlur={model.onCodeEditorBlur}
/>
)}
</AutoSizer>
</div>
return (
<div className={styles.wrap}>
<div className={styles.toolbar} data-testid={selectors.components.PanelInspector.Json.content}>
<Field label={t('dashboard.inspect-json.select-source', 'Select source')} className="flex-grow-1">
<Select
inputId="select-source-dropdown"
options={options}
value={options.find((v) => v.value === show) ?? options[0].value}
onChange={model.onChangeSource}
/>
</Field>
{model.isEditable() && (
<Button className={styles.toolbarItem} onClick={model.onApplyChange}>
<Trans i18nKey="dashboard-scene.inspect-json-tab.apply">Apply</Trans>
</Button>
)}
</div>
);
};
<div className={styles.content}>
<AutoSizer disableWidth>
{({ height }) => (
<CodeEditor
width="100%"
height={height}
language="json"
showLineNumbers={true}
showMiniMap={jsonText.length > 100}
value={jsonText}
readOnly={!model.isEditable()}
onBlur={model.onCodeEditorBlur}
/>
)}
</AutoSizer>
</div>
</div>
);
}
function getJsonText(show: ShowContent, panel: VizPanel): string {
@@ -119,78 +119,80 @@ export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
];
}
static Component = ({ model }: SceneComponentProps<PanelOptionsPane>) => {
const { isVizPickerOpen, searchQuery, listMode, panelRef } = model.useState();
const panel = panelRef.resolve();
const { pluginId } = panel.useState();
const { data } = sceneGraph.getData(panel).useState();
const styles = useStyles2(getStyles);
const isSearching = searchQuery.length > 0;
const hasFieldConfig = !isSearching && !panel.getPlugin()?.fieldConfigRegistry.isEmpty();
const [isSearchingOptions, setIsSearchingOptions] = useToggle(false);
const onlyOverrides = listMode === OptionFilter.Overrides;
static Component = PanelOptionsPaneComponent;
}
return (
<>
{!isVizPickerOpen && (
<>
<div className={styles.top}>
<Field
label={t('dashboard.panel-edit.visualization-button-label', 'Visualization')}
className={styles.vizField}
>
<Stack gap={1}>
<VisualizationButton pluginId={pluginId} onOpen={model.onToggleVizPicker} />
<Button
icon="search"
variant="secondary"
onClick={setIsSearchingOptions}
tooltip={t('dashboard.panel-edit.visualization-button-tooltip', 'Search options')}
/>
{hasFieldConfig && (
<ToolbarButton
icon="filter"
tooltip={t('dashboard.panel-edit.only-overrides-button-tooltip', 'Show only overrides')}
variant={onlyOverrides ? 'active' : 'canvas'}
onClick={() => {
model.onSetListMode(onlyOverrides ? OptionFilter.All : OptionFilter.Overrides);
}}
/>
)}
</Stack>
</Field>
function PanelOptionsPaneComponent({ model }: SceneComponentProps<PanelOptionsPane>) {
const { isVizPickerOpen, searchQuery, listMode, panelRef } = model.useState();
const panel = panelRef.resolve();
const { pluginId } = panel.useState();
const { data } = sceneGraph.getData(panel).useState();
const styles = useStyles2(getStyles);
const isSearching = searchQuery.length > 0;
const hasFieldConfig = !isSearching && !panel.getPlugin()?.fieldConfigRegistry.isEmpty();
const [isSearchingOptions, setIsSearchingOptions] = useToggle(false);
const onlyOverrides = listMode === OptionFilter.Overrides;
{isSearchingOptions && (
<FilterInput
className={styles.searchOptions}
value={searchQuery}
placeholder={t('dashboard.panel-edit.placeholder-search-options', 'Search options')}
onChange={model.onSetSearchQuery}
autoFocus={true}
onBlur={() => {
if (searchQuery.length === 0) {
setIsSearchingOptions(false);
}
}}
return (
<>
{!isVizPickerOpen && (
<>
<div className={styles.top}>
<Field
label={t('dashboard.panel-edit.visualization-button-label', 'Visualization')}
className={styles.vizField}
>
<Stack gap={1}>
<VisualizationButton pluginId={pluginId} onOpen={model.onToggleVizPicker} />
<Button
icon="search"
variant="secondary"
onClick={setIsSearchingOptions}
tooltip={t('dashboard.panel-edit.visualization-button-tooltip', 'Search options')}
/>
)}
</div>
<ScrollContainer>
<PanelOptions panel={panel} searchQuery={searchQuery} listMode={listMode} data={data} />
</ScrollContainer>
</>
)}
{isVizPickerOpen && (
<PanelVizTypePicker
panel={panel}
onChange={model.onChangePanelPlugin}
onClose={model.onToggleVizPicker}
data={data}
/>
)}
</>
);
};
{hasFieldConfig && (
<ToolbarButton
icon="filter"
tooltip={t('dashboard.panel-edit.only-overrides-button-tooltip', 'Show only overrides')}
variant={onlyOverrides ? 'active' : 'canvas'}
onClick={() => {
model.onSetListMode(onlyOverrides ? OptionFilter.All : OptionFilter.Overrides);
}}
/>
)}
</Stack>
</Field>
{isSearchingOptions && (
<FilterInput
className={styles.searchOptions}
value={searchQuery}
placeholder={t('dashboard.panel-edit.placeholder-search-options', 'Search options')}
onChange={model.onSetSearchQuery}
autoFocus={true}
onBlur={() => {
if (searchQuery.length === 0) {
setIsSearchingOptions(false);
}
}}
/>
)}
</div>
<ScrollContainer>
<PanelOptions panel={panel} searchQuery={searchQuery} listMode={listMode} data={data} />
</ScrollContainer>
</>
)}
{isVizPickerOpen && (
<PanelVizTypePicker
panel={panel}
onChange={model.onChangePanelPlugin}
onClose={model.onToggleVizPicker}
data={data}
/>
)}
</>
);
}
function getStyles(theme: GrafanaTheme2) {
@@ -45,80 +45,80 @@ export class SaveDashboardDrawer extends SceneObjectBase<SaveDashboardDrawerStat
this.setState({ saveRefresh: !this.state.saveRefresh });
};
static Component = ({ model }: SceneComponentProps<SaveDashboardDrawer>) => {
const { showDiff, saveAsCopy, saveTimeRange, saveVariables, saveRefresh } = model.useState();
static Component = SaveDashboardDrawerComponent;
}
const changeInfo = model.state.dashboardRef
.resolve()
.getDashboardChanges(saveTimeRange, saveVariables, saveRefresh);
function SaveDashboardDrawerComponent({ model }: SceneComponentProps<SaveDashboardDrawer>) {
const { showDiff, saveAsCopy, saveTimeRange, saveVariables, saveRefresh } = model.useState();
const { changedSaveModel, initialSaveModel, diffs, diffCount, hasFolderChanges, hasMigratedToV2 } = changeInfo;
const changesCount = diffCount + (hasFolderChanges ? 1 : 0);
const dashboard = model.state.dashboardRef.resolve();
const { meta } = dashboard.useState();
const { provisioned: isProvisioned, folderTitle } = meta;
const managedResourceCannotBeEdited = dashboard.managedResourceCannotBeEdited();
const isProvisionedNG = useIsProvisionedNG(dashboard);
const changeInfo = model.state.dashboardRef.resolve().getDashboardChanges(saveTimeRange, saveVariables, saveRefresh);
const tabs = (
<TabsBar>
const { changedSaveModel, initialSaveModel, diffs, diffCount, hasFolderChanges, hasMigratedToV2 } = changeInfo;
const changesCount = diffCount + (hasFolderChanges ? 1 : 0);
const dashboard = model.state.dashboardRef.resolve();
const { meta } = dashboard.useState();
const { provisioned: isProvisioned, folderTitle } = meta;
const managedResourceCannotBeEdited = dashboard.managedResourceCannotBeEdited();
const isProvisionedNG = useIsProvisionedNG(dashboard);
const tabs = (
<TabsBar>
<Tab
label={t('dashboard-scene.save-dashboard-drawer.tabs.label-details', 'Details')}
active={!showDiff}
onChangeTab={() => model.setState({ showDiff: false })}
/>
{changesCount > 0 && !managedResourceCannotBeEdited && (
<Tab
label={t('dashboard-scene.save-dashboard-drawer.tabs.label-details', 'Details')}
active={!showDiff}
onChangeTab={() => model.setState({ showDiff: false })}
label={t('dashboard-scene.save-dashboard-drawer.tabs.label-changes', 'Changes')}
active={showDiff}
onChangeTab={() => model.setState({ showDiff: true })}
counter={changesCount}
/>
{changesCount > 0 && !managedResourceCannotBeEdited && (
<Tab
label={t('dashboard-scene.save-dashboard-drawer.tabs.label-changes', 'Changes')}
active={showDiff}
onChangeTab={() => model.setState({ showDiff: true })}
counter={changesCount}
/>
)}
</TabsBar>
);
)}
</TabsBar>
);
let title = t('dashboard-scene.save-dashboard-drawer.tabs.title', 'Save dashboard');
if (saveAsCopy) {
title = t('dashboard-scene.save-dashboard-drawer.tabs.title-copy', 'Save dashboard copy');
} else if (isProvisioned || isProvisionedNG) {
title = t('dashboard-scene.save-dashboard-drawer.tabs.title-provisioned', 'Provisioned dashboard');
let title = t('dashboard-scene.save-dashboard-drawer.tabs.title', 'Save dashboard');
if (saveAsCopy) {
title = t('dashboard-scene.save-dashboard-drawer.tabs.title-copy', 'Save dashboard copy');
} else if (isProvisioned || isProvisionedNG) {
title = t('dashboard-scene.save-dashboard-drawer.tabs.title-provisioned', 'Provisioned dashboard');
}
const renderBody = () => {
if (showDiff) {
return (
<SaveDashboardDiff
diff={diffs}
oldValue={initialSaveModel}
newValue={changedSaveModel}
hasFolderChanges={hasFolderChanges}
hasMigratedToV2={hasMigratedToV2}
oldFolder={dashboard.getInitialState()?.meta.folderTitle}
newFolder={folderTitle}
/>
);
}
const renderBody = () => {
if (showDiff) {
return (
<SaveDashboardDiff
diff={diffs}
oldValue={initialSaveModel}
newValue={changedSaveModel}
hasFolderChanges={hasFolderChanges}
hasMigratedToV2={hasMigratedToV2}
oldFolder={dashboard.getInitialState()?.meta.folderTitle}
newFolder={folderTitle}
/>
);
}
if (isProvisionedNG) {
return <SaveProvisionedDashboard dashboard={dashboard} changeInfo={changeInfo} drawer={model} />;
}
if (isProvisionedNG) {
return <SaveProvisionedDashboard dashboard={dashboard} changeInfo={changeInfo} drawer={model} />;
}
if (saveAsCopy || changeInfo.isNew) {
return <SaveDashboardAsForm dashboard={dashboard} changeInfo={changeInfo} />;
}
if (saveAsCopy || changeInfo.isNew) {
return <SaveDashboardAsForm dashboard={dashboard} changeInfo={changeInfo} />;
}
if (isProvisioned || managedResourceCannotBeEdited) {
return <SaveProvisionedDashboardForm dashboard={dashboard} changeInfo={changeInfo} drawer={model} />;
}
if (isProvisioned || managedResourceCannotBeEdited) {
return <SaveProvisionedDashboardForm dashboard={dashboard} changeInfo={changeInfo} drawer={model} />;
}
return <SaveDashboardForm dashboard={dashboard} changeInfo={changeInfo} drawer={model} />;
};
return (
<Drawer title={title} subtitle={dashboard.state.title} onClose={model.onClose} tabs={tabs}>
{renderBody()}
</Drawer>
);
return <SaveDashboardForm dashboard={dashboard} changeInfo={changeInfo} drawer={model} />;
};
return (
<Drawer title={title} subtitle={dashboard.state.title} onClose={model.onClose} tabs={tabs}>
{renderBody()}
</Drawer>
);
}
@@ -182,7 +182,7 @@ export class GeneralSettingsEditView
});
};
private onMoveSuccess = (folderUID: string, folderTitle: string) => {
public onMoveSuccess = (folderUID: string, folderTitle: string) => {
const newMeta = {
...this._dashboard.state.meta,
folderUid: folderUID,
@@ -192,181 +192,176 @@ export class GeneralSettingsEditView
this.onMoveModalDismiss();
};
static Component = ({ model }: SceneComponentProps<GeneralSettingsEditView>) => {
const dashboard = model.getDashboard();
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
const { title, description, tags, meta, editable } = dashboard.useState();
const { showMoveModal, moveModalProps } = model.useState();
const { sync: graphTooltip } = model.getCursorSync()?.useState() || {};
const { timeZone, weekStart, UNSAFE_nowDelay: nowDelay } = model.getTimeRange().useState();
const { intervals } = model.getRefreshPicker().useState();
const { hideTimeControls } = model.getDashboardControls().useState();
const { enabled: liveNow } = model.getLiveNowTimer().useState();
const EDITABLE_OPTIONS = [
{
label: t('dashboard-scene.general-settings-edit-view.editable_options.label.editable', 'Editable'),
value: true,
},
{
label: t('dashboard-scene.general-settings-edit-view.editable_options.label.readonly', 'Read-only'),
value: false,
},
];
static Component = GeneralSettingsEditViewComponent;
}
const GRAPH_TOOLTIP_OPTIONS = [
{
value: 0,
label: t('dashboard-scene.general-settings-edit-view.graph_tooltip_options.label.default', 'Default'),
},
{
value: 1,
label: t(
'dashboard-scene.general-settings-edit-view.graph_tooltip_options.label.shared-crosshair',
'Shared crosshair'
),
},
{
value: 2,
label: t(
'dashboard-scene.general-settings-edit-view.graph_tooltip_options.label.shared-tooltip',
'Shared tooltip'
),
},
];
function GeneralSettingsEditViewComponent({ model }: SceneComponentProps<GeneralSettingsEditView>) {
const dashboard = model.getDashboard();
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
const { title, description, tags, meta, editable } = dashboard.useState();
const { showMoveModal, moveModalProps } = model.useState();
const { sync: graphTooltip } = model.getCursorSync()?.useState() || {};
const { timeZone, weekStart, UNSAFE_nowDelay: nowDelay } = model.getTimeRange().useState();
const { intervals } = model.getRefreshPicker().useState();
const { hideTimeControls } = model.getDashboardControls().useState();
const { enabled: liveNow } = model.getLiveNowTimer().useState();
const EDITABLE_OPTIONS = [
{
label: t('dashboard-scene.general-settings-edit-view.editable_options.label.editable', 'Editable'),
value: true,
},
{
label: t('dashboard-scene.general-settings-edit-view.editable_options.label.readonly', 'Read-only'),
value: false,
},
];
return (
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
<NavToolbarActions dashboard={dashboard} />
<div style={{ maxWidth: '600px' }}>
<Box display="flex" direction="column" gap={2} marginBottom={5}>
const GRAPH_TOOLTIP_OPTIONS = [
{
value: 0,
label: t('dashboard-scene.general-settings-edit-view.graph_tooltip_options.label.default', 'Default'),
},
{
value: 1,
label: t(
'dashboard-scene.general-settings-edit-view.graph_tooltip_options.label.shared-crosshair',
'Shared crosshair'
),
},
{
value: 2,
label: t(
'dashboard-scene.general-settings-edit-view.graph_tooltip_options.label.shared-tooltip',
'Shared tooltip'
),
},
];
return (
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
<NavToolbarActions dashboard={dashboard} />
<div style={{ maxWidth: '600px' }}>
<Box display="flex" direction="column" gap={2} marginBottom={5}>
<Field
noMargin
label={
<Stack justifyContent="space-between">
<Label htmlFor="title-input">
<Trans i18nKey="dashboard-settings.general.title-label">Title</Trans>
</Label>
{config.featureToggles.dashgpt && (
<GenAIDashTitleButton onGenerate={(title) => model.onTitleChange(title)} />
)}
</Stack>
}
>
<Input
id="title-input"
name="title"
value={title}
onChange={(e: ChangeEvent<HTMLInputElement>) => model.onTitleChange(e.target.value)}
/>
</Field>
<Field
noMargin
label={
<Stack justifyContent="space-between">
<Label htmlFor="description-input">
{t('dashboard-settings.general.description-label', 'Description')}
</Label>
{config.featureToggles.dashgpt && (
<GenAIDashDescriptionButton onGenerate={(description) => model.onDescriptionChange(description)} />
)}
</Stack>
}
>
<TextArea
id="description-input"
name="description"
value={description}
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => model.onDescriptionChange(e.target.value)}
/>
</Field>
<Field noMargin label={t('dashboard-settings.general.tags-label', 'Tags')}>
<TagsInput id="tags-input" tags={tags} onChange={model.onTagsChange} width={40} />
</Field>
<Field noMargin label={t('dashboard-settings.general.folder-label', 'Folder')}>
<FolderPicker
value={meta.folderUid}
onChange={dashboard.isManagedRepository() ? model.onProvisionedFolderChange : model.onFolderChange}
/>
</Field>
<Field
noMargin
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}
timePickerHidden={hideTimeControls}
nowDelay={nowDelay || ''}
liveNow={liveNow}
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}>
<Stack direction="column" gap={2}>
<Field
noMargin
label={
<Stack justifyContent="space-between">
<Label htmlFor="title-input">
<Trans i18nKey="dashboard-settings.general.title-label">Title</Trans>
</Label>
{config.featureToggles.dashgpt && (
<GenAIDashTitleButton onGenerate={(title) => model.onTitleChange(title)} />
)}
</Stack>
}
>
<Input
id="title-input"
name="title"
value={title}
onChange={(e: ChangeEvent<HTMLInputElement>) => model.onTitleChange(e.target.value)}
/>
</Field>
<Field
noMargin
label={
<Stack justifyContent="space-between">
<Label htmlFor="description-input">
{t('dashboard-settings.general.description-label', 'Description')}
</Label>
{config.featureToggles.dashgpt && (
<GenAIDashDescriptionButton onGenerate={(description) => model.onDescriptionChange(description)} />
)}
</Stack>
}
>
<TextArea
id="description-input"
name="description"
value={description}
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => model.onDescriptionChange(e.target.value)}
/>
</Field>
<Field noMargin label={t('dashboard-settings.general.tags-label', 'Tags')}>
<TagsInput id="tags-input" tags={tags} onChange={model.onTagsChange} width={40} />
</Field>
<Field noMargin label={t('dashboard-settings.general.folder-label', 'Folder')}>
<FolderPicker
value={meta.folderUid}
onChange={dashboard.isManagedRepository() ? model.onProvisionedFolderChange : model.onFolderChange}
/>
</Field>
<Field
noMargin
label={t('dashboard-settings.general.editable-label', 'Editable')}
label={t('dashboard-settings.general.panel-options-graph-tooltip-label', 'Graph tooltip')}
description={t(
'dashboard-settings.general.editable-description',
'Set to read-only to disable all editing. Reload the dashboard for changes to take effect'
'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 value={editable} options={EDITABLE_OPTIONS} onChange={model.onEditableChange} />
<RadioButtonGroup onChange={model.onTooltipChange} options={GRAPH_TOOLTIP_OPTIONS} value={graphTooltip} />
</Field>
</Box>
<TimePickerSettings
onTimeZoneChange={model.onTimeZoneChange}
onWeekStartChange={model.onWeekStartChange}
onRefreshIntervalChange={model.onRefreshIntervalChange}
onNowDelayChange={model.onNowDelayChange}
onHideTimePickerChange={model.onHideTimePickerChange}
onLiveNowChange={model.onLiveNowChange}
refreshIntervals={intervals}
timePickerHidden={hideTimeControls}
nowDelay={nowDelay || ''}
liveNow={liveNow}
timezone={timeZone || ''}
weekStart={weekStart}
/>
<Field
noMargin
label={t('dashboard-settings.general.panels-preload-label', 'Preload panels')}
description={t(
'dashboard-settings.general.panels-preload-description',
'When enabled all panels will start loading as soon as the dashboard has been loaded.'
)}
>
<Switch
id="preload-panels-dashboards-toggle"
value={dashboard.state.preload}
onChange={(e) => model.onPreloadChange(e.currentTarget.checked)}
/>
</Field>
</Stack>
</CollapsableSection>
{/* @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}
>
<Stack direction="column" gap={2}>
<Field
noMargin
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>
<Box marginTop={3}>{meta.canDelete && <DeleteDashboardButton dashboard={dashboard} />}</Box>
</div>
<Field
noMargin
label={t('dashboard-settings.general.panels-preload-label', 'Preload panels')}
description={t(
'dashboard-settings.general.panels-preload-description',
'When enabled all panels will start loading as soon as the dashboard has been loaded.'
)}
>
<Switch
id="preload-panels-dashboards-toggle"
value={dashboard.state.preload}
onChange={(e) => model.onPreloadChange(e.currentTarget.checked)}
/>
</Field>
</Stack>
</CollapsableSection>
<Box marginTop={3}>{meta.canDelete && <DeleteDashboardButton dashboard={dashboard} />}</Box>
</div>
{showMoveModal && moveModalProps && (
<MoveProvisionedDashboardDrawer
dashboard={dashboard}
targetFolderUID={moveModalProps.targetFolderUID}
targetFolderTitle={moveModalProps.targetFolderTitle}
onDismiss={model.onMoveModalDismiss}
onSuccess={model.onMoveSuccess}
/>
)}
</Page>
);
};
{showMoveModal && moveModalProps && (
<MoveProvisionedDashboardDrawer
dashboard={dashboard}
targetFolderUID={moveModalProps.targetFolderUID}
targetFolderTitle={moveModalProps.targetFolderTitle}
onDismiss={model.onMoveModalDismiss}
onSuccess={model.onMoveSuccess}
/>
)}
</Page>
);
}
@@ -97,148 +97,150 @@ export class JsonModelEditView extends SceneObjectBase<JsonModelEditViewState> i
dashboard.resumeTrackingChanges();
};
static Component = ({ model }: SceneComponentProps<JsonModelEditView>) => {
const { state, onSaveDashboard } = useSaveDashboard(false);
const [isSaving, setIsSaving] = useState(false);
static Component = JsonModelEditViewComponent;
}
const dashboard = model.getDashboard();
function JsonModelEditViewComponent({ model }: SceneComponentProps<JsonModelEditView>) {
const { state, onSaveDashboard } = useSaveDashboard(false);
const [isSaving, setIsSaving] = useState(false);
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
const canSave = dashboard.useState().meta.canSave;
const { jsonText } = model.useState();
const dashboard = model.getDashboard();
const onSave = async (overwrite: boolean) => {
const result = await onSaveDashboard(dashboard, {
folderUid: dashboard.state.meta.folderUid,
overwrite,
rawDashboardJSON: JSON.parse(model.state.jsonText),
k8s: dashboard.state.meta.k8s,
});
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
const canSave = dashboard.useState().meta.canSave;
const { jsonText } = model.useState();
const onSave = async (overwrite: boolean) => {
const result = await onSaveDashboard(dashboard, {
folderUid: dashboard.state.meta.folderUid,
overwrite,
rawDashboardJSON: JSON.parse(model.state.jsonText),
k8s: dashboard.state.meta.k8s,
});
setIsSaving(true);
if (result.status === 'success') {
await model.onSaveSuccess(result);
setIsSaving(false);
} else {
setIsSaving(true);
if (result.status === 'success') {
await model.onSaveSuccess(result);
setIsSaving(false);
} else {
setIsSaving(true);
}
};
const saveButton = (overwrite: boolean) => (
<Button
type="submit"
onClick={() => {
onSave(overwrite);
}}
variant={overwrite ? 'destructive' : 'primary'}
>
{overwrite ? (
<Trans i18nKey="dashboard-scene.json-model-edit-view.save-and-overwrite">'Save and overwrite'</Trans>
) : (
<Trans i18nKey="dashboard-settings.json-editor.save-button">Save changes</Trans>
)}
</Button>
);
const cancelButton = (
<Button variant="secondary" onClick={() => setIsSaving(false)} fill="outline">
<Trans i18nKey="dashboard-scene.json-model-edit-view.cancel-button.cancel">Cancel</Trans>
</Button>
);
const styles = useStyles2(getStyles);
function renderSaveButtonAndError(error?: Error) {
if (error && isSaving) {
if (isVersionMismatchError(error)) {
return (
<Alert
title={t(
'dashboard-scene.json-model-edit-view.render-save-button-and-error.title-someone-else-has-updated-this-dashboard',
'Someone else has updated this dashboard'
)}
severity="error"
>
<p>
<Trans i18nKey="dashboard-scene.json-model-edit-view.render-save-button-and-error.would-still-dashboard">
Would you still like to save this dashboard?
</Trans>
</p>
<Box paddingTop={2}>
<Stack alignItems="center">
{cancelButton}
{saveButton(true)}
</Stack>
</Box>
</Alert>
);
}
if (isNameExistsError(error)) {
return <NameAlreadyExistsError saveButton={saveButton} cancelButton={cancelButton} />;
}
if (isPluginDashboardError(error)) {
return (
<Alert
title={t(
'dashboard-scene.json-model-edit-view.render-save-button-and-error.title-plugin-dashboard',
'Plugin dashboard'
)}
severity="error"
>
<p>
<Trans i18nKey="dashboard-scene.json-model-edit-view.render-save-button-and-error.body-plugin-dashboard">
Your changes will be lost when you update the plugin. Use <strong>Save as</strong> to create custom
version.
</Trans>
</p>
<Box paddingTop={2}>
<Stack alignItems="center">{saveButton(true)}</Stack>
</Box>
</Alert>
);
}
}
return (
<>
{error && isSaving && (
<Alert
title={t(
'dashboard-scene.json-model-edit-view.render-save-button-and-error.title-failed-to-save-dashboard',
'Failed to save dashboard'
)}
severity="error"
>
<p>{error.message}</p>
</Alert>
)}
<Stack alignItems="center">{saveButton(false)}</Stack>
</>
);
}
return (
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
<NavToolbarActions dashboard={dashboard} />
<div className={styles.wrapper}>
<Trans i18nKey="dashboard-settings.json-editor.subtitle">
The JSON model below is the data structure that defines the dashboard. This includes dashboard settings,
panel settings, layout, queries, and so on.
</Trans>
<CodeEditor
width="100%"
value={jsonText}
language="json"
showLineNumbers={true}
showMiniMap={true}
containerStyles={styles.codeEditor}
onBlur={model.onCodeEditorBlur}
/>
{canSave && <Box paddingTop={2}>{renderSaveButtonAndError(state.error)}</Box>}
</div>
</Page>
);
};
const saveButton = (overwrite: boolean) => (
<Button
type="submit"
onClick={() => {
onSave(overwrite);
}}
variant={overwrite ? 'destructive' : 'primary'}
>
{overwrite ? (
<Trans i18nKey="dashboard-scene.json-model-edit-view.save-and-overwrite">'Save and overwrite'</Trans>
) : (
<Trans i18nKey="dashboard-settings.json-editor.save-button">Save changes</Trans>
)}
</Button>
);
const cancelButton = (
<Button variant="secondary" onClick={() => setIsSaving(false)} fill="outline">
<Trans i18nKey="dashboard-scene.json-model-edit-view.cancel-button.cancel">Cancel</Trans>
</Button>
);
const styles = useStyles2(getStyles);
function renderSaveButtonAndError(error?: Error) {
if (error && isSaving) {
if (isVersionMismatchError(error)) {
return (
<Alert
title={t(
'dashboard-scene.json-model-edit-view.render-save-button-and-error.title-someone-else-has-updated-this-dashboard',
'Someone else has updated this dashboard'
)}
severity="error"
>
<p>
<Trans i18nKey="dashboard-scene.json-model-edit-view.render-save-button-and-error.would-still-dashboard">
Would you still like to save this dashboard?
</Trans>
</p>
<Box paddingTop={2}>
<Stack alignItems="center">
{cancelButton}
{saveButton(true)}
</Stack>
</Box>
</Alert>
);
}
if (isNameExistsError(error)) {
return <NameAlreadyExistsError saveButton={saveButton} cancelButton={cancelButton} />;
}
if (isPluginDashboardError(error)) {
return (
<Alert
title={t(
'dashboard-scene.json-model-edit-view.render-save-button-and-error.title-plugin-dashboard',
'Plugin dashboard'
)}
severity="error"
>
<p>
<Trans i18nKey="dashboard-scene.json-model-edit-view.render-save-button-and-error.body-plugin-dashboard">
Your changes will be lost when you update the plugin. Use <strong>Save as</strong> to create custom
version.
</Trans>
</p>
<Box paddingTop={2}>
<Stack alignItems="center">{saveButton(true)}</Stack>
</Box>
</Alert>
);
}
}
return (
<>
{error && isSaving && (
<Alert
title={t(
'dashboard-scene.json-model-edit-view.render-save-button-and-error.title-failed-to-save-dashboard',
'Failed to save dashboard'
)}
severity="error"
>
<p>{error.message}</p>
</Alert>
)}
<Stack alignItems="center">{saveButton(false)}</Stack>
</>
);
}
return (
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
<NavToolbarActions dashboard={dashboard} />
<div className={styles.wrapper}>
<Trans i18nKey="dashboard-settings.json-editor.subtitle">
The JSON model below is the data structure that defines the dashboard. This includes dashboard settings, panel
settings, layout, queries, and so on.
</Trans>
<CodeEditor
width="100%"
value={jsonText}
language="json"
showLineNumbers={true}
showMiniMap={true}
containerStyles={styles.codeEditor}
onBlur={model.onCodeEditorBlur}
/>
{canSave && <Box paddingTop={2}>{renderSaveButtonAndError(state.error)}</Box>}
</div>
</Page>
);
}
const getStyles = (theme: GrafanaTheme2) => ({