mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DashboardScene: Saving updates (provisioned dashboard and fixes) (#81471)
* Save provisioned dashboard * Update * fixes
This commit is contained in:
parent
02acbd795d
commit
715143d4ed
@ -29,7 +29,7 @@ export interface Props {
|
|||||||
export function SaveDashboardAsForm({ dashboard, drawer, changeInfo }: Props) {
|
export function SaveDashboardAsForm({ dashboard, drawer, changeInfo }: Props) {
|
||||||
const { changedSaveModel } = changeInfo;
|
const { changedSaveModel } = changeInfo;
|
||||||
|
|
||||||
const { register, handleSubmit, setValue, formState, getValues } = useForm<SaveDashboardAsFormDTO>({
|
const { register, handleSubmit, setValue, formState, getValues, watch } = useForm<SaveDashboardAsFormDTO>({
|
||||||
mode: 'onBlur',
|
mode: 'onBlur',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
title: changeInfo.isNew ? changedSaveModel.title! : `${changedSaveModel.title} Copy`,
|
title: changeInfo.isNew ? changedSaveModel.title! : `${changedSaveModel.title} Copy`,
|
||||||
@ -41,8 +41,9 @@ export function SaveDashboardAsForm({ dashboard, drawer, changeInfo }: Props) {
|
|||||||
copyTags: false,
|
copyTags: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { errors, isValid, defaultValues } = formState;
|
const { errors, isValid, defaultValues } = formState;
|
||||||
const formValues = getValues();
|
const formValues = watch();
|
||||||
|
|
||||||
const { state, onSaveDashboard } = useDashboardSave(false);
|
const { state, onSaveDashboard } = useDashboardSave(false);
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ describe('SaveDashboardDrawer', () => {
|
|||||||
setup().openAndRender();
|
setup().openAndRender();
|
||||||
|
|
||||||
expect(await screen.findByText('Save dashboard')).toBeInTheDocument();
|
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.getByText('No changes to save')).toBeInTheDocument();
|
||||||
expect(screen.queryByLabelText('Tab Changes')).not.toBeInTheDocument();
|
expect(screen.queryByLabelText('Tab Changes')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@ -49,7 +49,7 @@ describe('SaveDashboardDrawer', () => {
|
|||||||
openAndRender();
|
openAndRender();
|
||||||
|
|
||||||
expect(await screen.findByText('Save dashboard')).toBeInTheDocument();
|
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 () => {
|
it('Should update diff when including time range is', async () => {
|
||||||
@ -60,7 +60,7 @@ describe('SaveDashboardDrawer', () => {
|
|||||||
openAndRender();
|
openAndRender();
|
||||||
|
|
||||||
expect(await screen.findByText('Save dashboard')).toBeInTheDocument();
|
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();
|
expect(screen.queryByLabelText('Tab Changes')).not.toBeInTheDocument();
|
||||||
|
|
||||||
await userEvent.click(screen.getByLabelText(selectors.pages.SaveDashboardModal.saveTimerange));
|
await userEvent.click(screen.getByLabelText(selectors.pages.SaveDashboardModal.saveTimerange));
|
||||||
|
@ -8,6 +8,7 @@ import { DashboardScene } from '../scene/DashboardScene';
|
|||||||
|
|
||||||
import { SaveDashboardAsForm } from './SaveDashboardAsForm';
|
import { SaveDashboardAsForm } from './SaveDashboardAsForm';
|
||||||
import { SaveDashboardForm } from './SaveDashboardForm';
|
import { SaveDashboardForm } from './SaveDashboardForm';
|
||||||
|
import { SaveProvisionedDashboardForm } from './SaveProvisionedDashboardForm';
|
||||||
import { getSaveDashboardChange } from './getSaveDashboardChange';
|
import { getSaveDashboardChange } from './getSaveDashboardChange';
|
||||||
|
|
||||||
interface SaveDashboardDrawerState extends SceneObjectState {
|
interface SaveDashboardDrawerState extends SceneObjectState {
|
||||||
@ -36,6 +37,7 @@ export class SaveDashboardDrawer extends SceneObjectBase<SaveDashboardDrawerStat
|
|||||||
const changeInfo = getSaveDashboardChange(model.state.dashboardRef.resolve(), saveTimeRange, saveVariables);
|
const changeInfo = getSaveDashboardChange(model.state.dashboardRef.resolve(), saveTimeRange, saveVariables);
|
||||||
const { changedSaveModel, initialSaveModel, diffs, diffCount } = changeInfo;
|
const { changedSaveModel, initialSaveModel, diffs, diffCount } = changeInfo;
|
||||||
const dashboard = model.state.dashboardRef.resolve();
|
const dashboard = model.state.dashboardRef.resolve();
|
||||||
|
const isProvisioned = dashboard.state.meta.provisioned;
|
||||||
|
|
||||||
const tabs = (
|
const tabs = (
|
||||||
<TabsBar>
|
<TabsBar>
|
||||||
@ -54,12 +56,10 @@ export class SaveDashboardDrawer extends SceneObjectBase<SaveDashboardDrawerStat
|
|||||||
let title = 'Save dashboard';
|
let title = 'Save dashboard';
|
||||||
if (saveAsCopy) {
|
if (saveAsCopy) {
|
||||||
title = 'Save dashboard copy';
|
title = 'Save dashboard copy';
|
||||||
|
} else if (isProvisioned) {
|
||||||
|
title = 'Provisioned dashboard';
|
||||||
}
|
}
|
||||||
|
|
||||||
// else if (isProvisioned) {
|
|
||||||
// title = 'Provisioned dashboard';
|
|
||||||
// }
|
|
||||||
|
|
||||||
const renderBody = () => {
|
const renderBody = () => {
|
||||||
if (showDiff) {
|
if (showDiff) {
|
||||||
return <SaveDashboardDiff diff={diffs} oldValue={initialSaveModel} newValue={changedSaveModel} />;
|
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} />;
|
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} />;
|
return <SaveDashboardForm dashboard={dashboard} changeInfo={changeInfo} drawer={model} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -24,8 +24,7 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SaveDashboardForm({ dashboard, drawer, changeInfo }: Props) {
|
export function SaveDashboardForm({ dashboard, drawer, changeInfo }: Props) {
|
||||||
const { saveVariables = false, saveTimeRange = false } = drawer.useState();
|
const { changedSaveModel, hasChanges } = changeInfo;
|
||||||
const { changedSaveModel, hasChanges, hasTimeChanges, hasVariableValueChanges } = changeInfo;
|
|
||||||
|
|
||||||
const { state, onSaveDashboard } = useDashboardSave(false);
|
const { state, onSaveDashboard } = useDashboardSave(false);
|
||||||
const [options, setOptions] = useState<SaveDashboardOptions>({
|
const [options, setOptions] = useState<SaveDashboardOptions>({
|
||||||
@ -102,29 +101,9 @@ export function SaveDashboardForm({ dashboard, drawer, changeInfo }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap={0} direction="column">
|
<Stack gap={0} direction="column">
|
||||||
{hasTimeChanges && (
|
<SaveDashboardFormCommonOptions drawer={drawer} changeInfo={changeInfo} />
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
<Field label="Message">
|
<Field label="Message">
|
||||||
{/* config.featureToggles.dashgpt * TOOD GenAIDashboardChangesButton */}
|
{/* config.featureToggles.dashgpt * TOOD GenAIDashboardChangesButton */}
|
||||||
|
|
||||||
<TextArea
|
<TextArea
|
||||||
aria-label="message"
|
aria-label="message"
|
||||||
value={options.message ?? ''}
|
value={options.message ?? ''}
|
||||||
@ -143,3 +122,38 @@ export function SaveDashboardForm({ dashboard, drawer, changeInfo }: Props) {
|
|||||||
</Stack>
|
</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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -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',
|
||||||
|
}),
|
||||||
|
};
|
@ -185,7 +185,7 @@ exports[`transformSceneToSaveModel Given a scene with rows Should transform back
|
|||||||
"type": "row",
|
"type": "row",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"schemaVersion": 36,
|
"schemaVersion": 39,
|
||||||
"tags": [
|
"tags": [
|
||||||
"templating",
|
"templating",
|
||||||
"gdev",
|
"gdev",
|
||||||
@ -479,7 +479,7 @@ exports[`transformSceneToSaveModel Given a simple scene with custom settings Sho
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"schemaVersion": 36,
|
"schemaVersion": 39,
|
||||||
"tags": [
|
"tags": [
|
||||||
"tag1",
|
"tag1",
|
||||||
"tag2",
|
"tag2",
|
||||||
@ -796,7 +796,7 @@ exports[`transformSceneToSaveModel Given a simple scene with variables Should tr
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"schemaVersion": 36,
|
"schemaVersion": 39,
|
||||||
"tags": [
|
"tags": [
|
||||||
"gdev",
|
"gdev",
|
||||||
"graph-ng",
|
"graph-ng",
|
||||||
|
@ -28,6 +28,7 @@ import {
|
|||||||
} from '@grafana/schema';
|
} from '@grafana/schema';
|
||||||
import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object';
|
import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object';
|
||||||
import { getPanelDataFrames } from 'app/features/dashboard/components/HelpWizard/utils';
|
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 { GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
|
||||||
|
|
||||||
import { DashboardControls } from '../scene/DashboardControls';
|
import { DashboardControls } from '../scene/DashboardControls';
|
||||||
@ -141,6 +142,7 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa
|
|||||||
tags: state.tags,
|
tags: state.tags,
|
||||||
links: state.links,
|
links: state.links,
|
||||||
graphTooltip,
|
graphTooltip,
|
||||||
|
schemaVersion: DASHBOARD_SCHEMA_VERSION,
|
||||||
};
|
};
|
||||||
|
|
||||||
return sortedDeepCloneWithoutNulls(dashboard);
|
return sortedDeepCloneWithoutNulls(dashboard);
|
||||||
|
Loading…
Reference in New Issue
Block a user