DashboardScene: Saving updates (provisioned dashboard and fixes) (#81471)

* Save provisioned dashboard

* Update

* fixes
This commit is contained in:
Torkel Ödegaard 2024-01-29 20:03:57 +01:00 committed by GitHub
parent 02acbd795d
commit 715143d4ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 153 additions and 35 deletions

View File

@ -29,7 +29,7 @@ export interface Props {
export function SaveDashboardAsForm({ dashboard, drawer, changeInfo }: Props) {
const { changedSaveModel } = changeInfo;
const { register, handleSubmit, setValue, formState, getValues } = useForm<SaveDashboardAsFormDTO>({
const { register, handleSubmit, setValue, formState, getValues, watch } = useForm<SaveDashboardAsFormDTO>({
mode: 'onBlur',
defaultValues: {
title: changeInfo.isNew ? changedSaveModel.title! : `${changedSaveModel.title} Copy`,
@ -41,8 +41,9 @@ export function SaveDashboardAsForm({ dashboard, drawer, changeInfo }: Props) {
copyTags: false,
},
});
const { errors, isValid, defaultValues } = formState;
const formValues = getValues();
const formValues = watch();
const { state, onSaveDashboard } = useDashboardSave(false);

View File

@ -31,7 +31,7 @@ describe('SaveDashboardDrawer', () => {
setup().openAndRender();
expect(await screen.findByText('Save dashboard')).toBeInTheDocument();
expect(screen.queryByText('Save current time range')).not.toBeInTheDocument();
expect(screen.queryByLabelText(selectors.pages.SaveDashboardModal.saveTimerange)).not.toBeInTheDocument();
expect(screen.getByText('No changes to save')).toBeInTheDocument();
expect(screen.queryByLabelText('Tab Changes')).not.toBeInTheDocument();
});
@ -49,7 +49,7 @@ describe('SaveDashboardDrawer', () => {
openAndRender();
expect(await screen.findByText('Save dashboard')).toBeInTheDocument();
expect(screen.queryByText('Save current time range')).toBeInTheDocument();
expect(screen.queryByLabelText(selectors.pages.SaveDashboardModal.saveTimerange)).toBeInTheDocument();
});
it('Should update diff when including time range is', async () => {
@ -60,7 +60,7 @@ describe('SaveDashboardDrawer', () => {
openAndRender();
expect(await screen.findByText('Save dashboard')).toBeInTheDocument();
expect(screen.queryByText('Save current time range')).toBeInTheDocument();
expect(screen.queryByLabelText(selectors.pages.SaveDashboardModal.saveTimerange)).toBeInTheDocument();
expect(screen.queryByLabelText('Tab Changes')).not.toBeInTheDocument();
await userEvent.click(screen.getByLabelText(selectors.pages.SaveDashboardModal.saveTimerange));

View File

@ -8,6 +8,7 @@ import { DashboardScene } from '../scene/DashboardScene';
import { SaveDashboardAsForm } from './SaveDashboardAsForm';
import { SaveDashboardForm } from './SaveDashboardForm';
import { SaveProvisionedDashboardForm } from './SaveProvisionedDashboardForm';
import { getSaveDashboardChange } from './getSaveDashboardChange';
interface SaveDashboardDrawerState extends SceneObjectState {
@ -36,6 +37,7 @@ export class SaveDashboardDrawer extends SceneObjectBase<SaveDashboardDrawerStat
const changeInfo = getSaveDashboardChange(model.state.dashboardRef.resolve(), saveTimeRange, saveVariables);
const { changedSaveModel, initialSaveModel, diffs, diffCount } = changeInfo;
const dashboard = model.state.dashboardRef.resolve();
const isProvisioned = dashboard.state.meta.provisioned;
const tabs = (
<TabsBar>
@ -54,12 +56,10 @@ export class SaveDashboardDrawer extends SceneObjectBase<SaveDashboardDrawerStat
let title = 'Save dashboard';
if (saveAsCopy) {
title = 'Save dashboard copy';
} else if (isProvisioned) {
title = 'Provisioned dashboard';
}
// else if (isProvisioned) {
// title = 'Provisioned dashboard';
// }
const renderBody = () => {
if (showDiff) {
return <SaveDashboardDiff diff={diffs} oldValue={initialSaveModel} newValue={changedSaveModel} />;
@ -69,6 +69,10 @@ export class SaveDashboardDrawer extends SceneObjectBase<SaveDashboardDrawerStat
return <SaveDashboardAsForm dashboard={dashboard} changeInfo={changeInfo} drawer={model} />;
}
if (isProvisioned) {
return <SaveProvisionedDashboardForm dashboard={dashboard} changeInfo={changeInfo} drawer={model} />;
}
return <SaveDashboardForm dashboard={dashboard} changeInfo={changeInfo} drawer={model} />;
};

View File

@ -24,8 +24,7 @@ export interface Props {
}
export function SaveDashboardForm({ dashboard, drawer, changeInfo }: Props) {
const { saveVariables = false, saveTimeRange = false } = drawer.useState();
const { changedSaveModel, hasChanges, hasTimeChanges, hasVariableValueChanges } = changeInfo;
const { changedSaveModel, hasChanges } = changeInfo;
const { state, onSaveDashboard } = useDashboardSave(false);
const [options, setOptions] = useState<SaveDashboardOptions>({
@ -102,29 +101,9 @@ export function SaveDashboardForm({ dashboard, drawer, changeInfo }: Props) {
return (
<Stack gap={0} direction="column">
{hasTimeChanges && (
<Field label="Save current time range" description="Will make current time range the new default">
<Checkbox
id="save-timerange"
checked={saveTimeRange}
onChange={drawer.onToggleSaveTimeRange}
aria-label={selectors.pages.SaveDashboardModal.saveTimerange}
/>
</Field>
)}
{hasVariableValueChanges && (
<Field label="Save current variable values" description="Will make the current values the new default">
<Checkbox
id="save-variables"
checked={saveVariables}
onChange={drawer.onToggleSaveVariables}
aria-label={selectors.pages.SaveDashboardModal.saveVariables}
/>
</Field>
)}
<SaveDashboardFormCommonOptions drawer={drawer} changeInfo={changeInfo} />
<Field label="Message">
{/* config.featureToggles.dashgpt * TOOD GenAIDashboardChangesButton */}
<TextArea
aria-label="message"
value={options.message ?? ''}
@ -143,3 +122,38 @@ export function SaveDashboardForm({ dashboard, drawer, changeInfo }: Props) {
</Stack>
);
}
export interface SaveDashboardFormCommonOptionsProps {
drawer: SaveDashboardDrawer;
changeInfo: DashboardChangeInfo;
}
export function SaveDashboardFormCommonOptions({ drawer, changeInfo }: SaveDashboardFormCommonOptionsProps) {
const { saveVariables = false, saveTimeRange = false } = drawer.useState();
const { hasTimeChanges, hasVariableValueChanges } = changeInfo;
return (
<>
{hasTimeChanges && (
<Field label="Update default time range" description="Will make current time range the new default">
<Checkbox
id="save-timerange"
checked={saveTimeRange}
onChange={drawer.onToggleSaveTimeRange}
aria-label={selectors.pages.SaveDashboardModal.saveTimerange}
/>
</Field>
)}
{hasVariableValueChanges && (
<Field label="Update default variable values" description="Will make the current values the new default">
<Checkbox
id="save-variables"
checked={saveVariables}
onChange={drawer.onToggleSaveVariables}
aria-label={selectors.pages.SaveDashboardModal.saveVariables}
/>
</Field>
)}
</>
);
}

View File

@ -0,0 +1,97 @@
import { css } from '@emotion/css';
import { saveAs } from 'file-saver';
import React, { useCallback, useMemo } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { Button, ClipboardButton, Stack, CodeEditor, Box } from '@grafana/ui';
import { DashboardScene } from '../scene/DashboardScene';
import { SaveDashboardDrawer } from './SaveDashboardDrawer';
import { SaveDashboardFormCommonOptions } from './SaveDashboardForm';
import { DashboardChangeInfo } from './shared';
export interface Props {
dashboard: DashboardScene;
drawer: SaveDashboardDrawer;
changeInfo: DashboardChangeInfo;
}
export function SaveProvisionedDashboardForm({ dashboard, drawer, changeInfo }: Props) {
const dashboardJSON = useMemo(() => JSON.stringify(changeInfo.changedSaveModel, null, 2), [changeInfo]);
const saveToFile = useCallback(() => {
const blob = new Blob([dashboardJSON], {
type: 'application/json;charset=utf-8',
});
saveAs(blob, changeInfo.changedSaveModel.title + '-' + new Date().getTime() + '.json');
}, [changeInfo.changedSaveModel, dashboardJSON]);
return (
<div className={styles.container}>
<Stack direction="column" gap={2} grow={1}>
<div>
This dashboard cannot be saved from the Grafana UI because it has been provisioned from another source. Copy
the JSON or save it to a file below, then you can update your dashboard in the provisioning source.
<br />
<i>
See{' '}
<a
className="external-link"
href="https://grafana.com/docs/grafana/latest/administration/provisioning/#dashboards"
target="_blank"
rel="noreferrer"
>
documentation
</a>{' '}
for more information about provisioning.
</i>
<br /> <br />
<strong>File path: </strong> {dashboard.state.meta.provisionedExternalId}
</div>
<Stack direction="column" gap={0}>
<SaveDashboardFormCommonOptions drawer={drawer} changeInfo={changeInfo} />
</Stack>
<div className={styles.json}>
<AutoSizer disableWidth>
{({ height }) => (
<CodeEditor
width="100%"
height={height}
language="json"
showLineNumbers={true}
showMiniMap={dashboardJSON.length > 100}
value={dashboardJSON}
readOnly={true}
/>
)}
</AutoSizer>
</div>
<Box paddingTop={2}>
<Stack gap={2}>
<Button variant="secondary" onClick={drawer.onClose} fill="outline">
Cancel
</Button>
<ClipboardButton icon="copy" getText={() => dashboardJSON}>
Copy JSON to clipboard
</ClipboardButton>
<Button type="submit" onClick={saveToFile}>
Save JSON to file
</Button>
</Stack>
</Box>
</Stack>
</div>
);
}
const styles = {
container: css({
height: '100%',
display: 'flex',
}),
json: css({
flexGrow: 1,
maxHeight: '800px',
}),
};

View File

@ -185,7 +185,7 @@ exports[`transformSceneToSaveModel Given a scene with rows Should transform back
"type": "row",
},
],
"schemaVersion": 36,
"schemaVersion": 39,
"tags": [
"templating",
"gdev",
@ -479,7 +479,7 @@ exports[`transformSceneToSaveModel Given a simple scene with custom settings Sho
"type": "text",
},
],
"schemaVersion": 36,
"schemaVersion": 39,
"tags": [
"tag1",
"tag2",
@ -796,7 +796,7 @@ exports[`transformSceneToSaveModel Given a simple scene with variables Should tr
"type": "text",
},
],
"schemaVersion": 36,
"schemaVersion": 39,
"tags": [
"gdev",
"graph-ng",

View File

@ -28,6 +28,7 @@ import {
} from '@grafana/schema';
import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object';
import { getPanelDataFrames } from 'app/features/dashboard/components/HelpWizard/utils';
import { DASHBOARD_SCHEMA_VERSION } from 'app/features/dashboard/state/DashboardMigrator';
import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
import { DashboardControls } from '../scene/DashboardControls';
@ -141,6 +142,7 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa
tags: state.tags,
links: state.links,
graphTooltip,
schemaVersion: DASHBOARD_SCHEMA_VERSION,
};
return sortedDeepCloneWithoutNulls(dashboard);