mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Dashboards: show changes in save dialog (#46557)
Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
@@ -1,50 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { Modal } from '@grafana/ui';
|
||||
import { SaveDashboardAsForm } from './forms/SaveDashboardAsForm';
|
||||
import { SaveDashboardErrorProxy } from './SaveDashboardErrorProxy';
|
||||
import { useDashboardSave } from './useDashboardSave';
|
||||
import { SaveDashboardModalProps } from './types';
|
||||
|
||||
export const SaveDashboardAsModal: React.FC<
|
||||
SaveDashboardModalProps & {
|
||||
isNew?: boolean;
|
||||
}
|
||||
> = ({ dashboard, onDismiss, isNew }) => {
|
||||
const { state, onDashboardSave } = useDashboardSave(dashboard);
|
||||
const [dashboardSaveModelClone, setDashboardSaveModelClone] = useState();
|
||||
return (
|
||||
<>
|
||||
{state.error && (
|
||||
<SaveDashboardErrorProxy
|
||||
error={state.error}
|
||||
dashboard={dashboard}
|
||||
dashboardSaveModel={dashboardSaveModelClone}
|
||||
onDismiss={onDismiss}
|
||||
/>
|
||||
)}
|
||||
{!state.error && (
|
||||
<Modal
|
||||
isOpen={true}
|
||||
title="Save dashboard as..."
|
||||
icon="copy"
|
||||
onDismiss={onDismiss}
|
||||
className={css`
|
||||
width: 500px;
|
||||
`}
|
||||
>
|
||||
<SaveDashboardAsForm
|
||||
dashboard={dashboard}
|
||||
onCancel={onDismiss}
|
||||
onSuccess={onDismiss}
|
||||
onSubmit={(clone, options, dashboard) => {
|
||||
setDashboardSaveModelClone(clone);
|
||||
return onDashboardSave(clone, options, dashboard);
|
||||
}}
|
||||
isNew={isNew}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Button, ButtonVariant, ModalsController, FullWidthButtonContainer } from '@grafana/ui';
|
||||
import { DashboardModel } from 'app/features/dashboard/state';
|
||||
import { SaveDashboardAsModal } from './SaveDashboardAsModal';
|
||||
import { SaveDashboardModalProxy } from './SaveDashboardModalProxy';
|
||||
import { SaveDashboardDrawer } from './SaveDashboardDrawer';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
interface SaveDashboardButtonProps {
|
||||
@@ -17,7 +16,7 @@ export const SaveDashboardButton: React.FC<SaveDashboardButtonProps> = ({ dashbo
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
showModal(SaveDashboardModalProxy, {
|
||||
showModal(SaveDashboardDrawer, {
|
||||
dashboard,
|
||||
onSaveSuccess,
|
||||
onDismiss: hideModal,
|
||||
@@ -45,10 +44,11 @@ export const SaveDashboardAsButton: React.FC<SaveDashboardButtonProps & { varian
|
||||
<FullWidthButtonContainer>
|
||||
<Button
|
||||
onClick={() => {
|
||||
showModal(SaveDashboardAsModal, {
|
||||
showModal(SaveDashboardDrawer, {
|
||||
dashboard,
|
||||
onSaveSuccess,
|
||||
onDismiss: hideModal,
|
||||
isCopy: true,
|
||||
});
|
||||
}}
|
||||
variant={variant}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { Spinner, useStyles2 } from '@grafana/ui';
|
||||
import { Diffs } from '../VersionHistory/utils';
|
||||
import { DiffGroup } from '../VersionHistory/DiffGroup';
|
||||
import { DiffViewer } from '../VersionHistory/DiffViewer';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
interface SaveDashboardDiffProps {
|
||||
oldValue?: any;
|
||||
newValue?: any;
|
||||
|
||||
// calculated by parent so we can see summary in tabs
|
||||
diff?: Diffs;
|
||||
}
|
||||
|
||||
export const SaveDashboardDiff = ({ diff, oldValue, newValue }: SaveDashboardDiffProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const loader = useAsync(async () => {
|
||||
const oldJSON = JSON.stringify(oldValue ?? {}, null, 2);
|
||||
const newJSON = JSON.stringify(newValue ?? {}, null, 2);
|
||||
return {
|
||||
oldJSON,
|
||||
newJSON,
|
||||
diffs: Object.entries(diff ?? []).map(([key, diffs]) => (
|
||||
<DiffGroup diffs={diffs} key={key} title={key} /> // this takes a long time for large diffs
|
||||
)),
|
||||
};
|
||||
}, [diff, oldValue, newValue]);
|
||||
|
||||
const { value } = loader;
|
||||
if (!value || !oldValue) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
if (!value.diffs.length) {
|
||||
return <div>No changes in this dashboard</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.spacer}>{value.diffs}</div>
|
||||
|
||||
<h4>JSON Diff</h4>
|
||||
<DiffViewer oldValue={value.oldJSON} newValue={value.newJSON} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
spacer: css`
|
||||
margin-bottom: ${theme.v1.spacing.xl};
|
||||
`,
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Drawer, Tab, TabsBar } from '@grafana/ui';
|
||||
import { SaveDashboardData, SaveDashboardModalProps, SaveDashboardOptions } from './types';
|
||||
import { jsonDiff } from '../VersionHistory/utils';
|
||||
import { useAsync } from 'react-use';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { useDashboardSave } from './useDashboardSave';
|
||||
import { SaveProvisionedDashboardForm } from './forms/SaveProvisionedDashboardForm';
|
||||
import { SaveDashboardErrorProxy } from './SaveDashboardErrorProxy';
|
||||
import { SaveDashboardAsForm } from './forms/SaveDashboardAsForm';
|
||||
import { SaveDashboardForm } from './forms/SaveDashboardForm';
|
||||
import { SaveDashboardDiff } from './SaveDashboardDiff';
|
||||
|
||||
export const SaveDashboardDrawer = ({ dashboard, onDismiss, onSaveSuccess, isCopy }: SaveDashboardModalProps) => {
|
||||
const [options, setOptions] = useState<SaveDashboardOptions>({});
|
||||
|
||||
const isProvisioned = dashboard.meta.provisioned;
|
||||
const isNew = dashboard.version === 0;
|
||||
|
||||
const previous = useAsync(async () => {
|
||||
if (isNew) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const result = await backendSrv.getDashboardByUid(dashboard.uid);
|
||||
return result.dashboard;
|
||||
}, [dashboard, isNew]);
|
||||
|
||||
const data = useMemo<SaveDashboardData>(() => {
|
||||
const clone = dashboard.getSaveModelClone({
|
||||
saveTimerange: Boolean(options.saveTimerange),
|
||||
saveVariables: Boolean(options.saveVariables),
|
||||
});
|
||||
|
||||
if (!previous.value) {
|
||||
return { clone, diff: {}, diffCount: 0, hasChanges: false };
|
||||
}
|
||||
|
||||
const cloneJSON = JSON.stringify(clone, null, 2);
|
||||
const cloneSafe = JSON.parse(cloneJSON); // avoids undefined issues
|
||||
|
||||
const diff = jsonDiff(previous.value, cloneSafe);
|
||||
let diffCount = 0;
|
||||
for (const d of Object.values(diff)) {
|
||||
diffCount += d.length;
|
||||
}
|
||||
|
||||
return {
|
||||
clone,
|
||||
diff,
|
||||
diffCount,
|
||||
hasChanges: diffCount > 0 && !isNew,
|
||||
};
|
||||
}, [dashboard, previous.value, options, isNew]);
|
||||
|
||||
const [showDiff, setShowDiff] = useState(false);
|
||||
const { state, onDashboardSave } = useDashboardSave(dashboard);
|
||||
const onSuccess = onSaveSuccess
|
||||
? () => {
|
||||
onDismiss();
|
||||
onSaveSuccess();
|
||||
}
|
||||
: onDismiss;
|
||||
|
||||
const renderBody = () => {
|
||||
if (showDiff) {
|
||||
return <SaveDashboardDiff diff={data.diff} oldValue={previous.value} newValue={data.clone} />;
|
||||
}
|
||||
|
||||
if (isNew || isCopy) {
|
||||
return (
|
||||
<SaveDashboardAsForm
|
||||
dashboard={dashboard}
|
||||
onCancel={onDismiss}
|
||||
onSuccess={onSuccess}
|
||||
onSubmit={onDashboardSave}
|
||||
isNew={isNew}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isProvisioned) {
|
||||
return <SaveProvisionedDashboardForm dashboard={dashboard} onCancel={onDismiss} onSuccess={onSuccess} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SaveDashboardForm
|
||||
dashboard={dashboard}
|
||||
saveModel={data}
|
||||
onCancel={onDismiss}
|
||||
onSuccess={onSuccess}
|
||||
onSubmit={onDashboardSave}
|
||||
options={options}
|
||||
onOptionsChange={setOptions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
if (state.error) {
|
||||
return (
|
||||
<SaveDashboardErrorProxy
|
||||
error={state.error}
|
||||
dashboard={dashboard}
|
||||
dashboardSaveModel={data.clone}
|
||||
onDismiss={onDismiss}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={isCopy ? 'Save dashboard copy' : 'Save dashboard'}
|
||||
onClose={onDismiss}
|
||||
width={'40%'}
|
||||
subtitle={dashboard.title}
|
||||
tabs={
|
||||
<TabsBar>
|
||||
<Tab label={'Details'} active={!showDiff} onChangeTab={() => setShowDiff(false)} />
|
||||
<Tab label={'Changes'} active={showDiff} onChangeTab={() => setShowDiff(true)} counter={data.diffCount} />
|
||||
</TabsBar>
|
||||
}
|
||||
expandable
|
||||
scrollableContent
|
||||
>
|
||||
{renderBody()}
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Modal } from '@grafana/ui';
|
||||
import { css } from '@emotion/css';
|
||||
import { SaveDashboardForm } from './forms/SaveDashboardForm';
|
||||
import { SaveDashboardErrorProxy } from './SaveDashboardErrorProxy';
|
||||
import { useDashboardSave } from './useDashboardSave';
|
||||
import { SaveDashboardModalProps } from './types';
|
||||
|
||||
export const SaveDashboardModal: React.FC<SaveDashboardModalProps> = ({ dashboard, onDismiss, onSaveSuccess }) => {
|
||||
const { state, onDashboardSave } = useDashboardSave(dashboard);
|
||||
const [dashboardSaveModelClone, setDashboardSaveModelClone] = useState();
|
||||
|
||||
return (
|
||||
<>
|
||||
{state.error && (
|
||||
<SaveDashboardErrorProxy
|
||||
error={state.error}
|
||||
dashboard={dashboard}
|
||||
dashboardSaveModel={dashboardSaveModelClone}
|
||||
onDismiss={onDismiss}
|
||||
/>
|
||||
)}
|
||||
{!state.error && (
|
||||
<Modal
|
||||
isOpen={true}
|
||||
title="Save dashboard"
|
||||
icon="copy"
|
||||
onDismiss={onDismiss}
|
||||
className={css`
|
||||
width: 500px;
|
||||
`}
|
||||
>
|
||||
<SaveDashboardForm
|
||||
dashboard={dashboard}
|
||||
onCancel={onDismiss}
|
||||
onSuccess={() => {
|
||||
onDismiss();
|
||||
if (onSaveSuccess) {
|
||||
onSaveSuccess();
|
||||
}
|
||||
}}
|
||||
onSubmit={(clone, options, dashboard) => {
|
||||
setDashboardSaveModelClone(clone);
|
||||
return onDashboardSave(clone, options, dashboard);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import React from 'react';
|
||||
import { SaveProvisionedDashboard } from './SaveProvisionedDashboard';
|
||||
import { SaveDashboardAsModal } from './SaveDashboardAsModal';
|
||||
import { SaveDashboardModalProps } from './types';
|
||||
import { SaveDashboardModal } from './SaveDashboardModal';
|
||||
|
||||
export const SaveDashboardModalProxy: React.FC<SaveDashboardModalProps> = ({ dashboard, onDismiss, onSaveSuccess }) => {
|
||||
const isProvisioned = dashboard.meta.provisioned;
|
||||
const isNew = dashboard.version === 0;
|
||||
const isChanged = dashboard.version > 0;
|
||||
|
||||
const modalProps = {
|
||||
dashboard,
|
||||
onDismiss,
|
||||
onSaveSuccess,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isChanged && !isProvisioned && <SaveDashboardModal {...modalProps} />}
|
||||
{isProvisioned && <SaveProvisionedDashboard {...modalProps} />}
|
||||
{isNew && <SaveDashboardAsModal {...modalProps} isNew />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Modal } from '@grafana/ui';
|
||||
import { SaveProvisionedDashboardForm } from './forms/SaveProvisionedDashboardForm';
|
||||
import { SaveDashboardModalProps } from './types';
|
||||
|
||||
export const SaveProvisionedDashboard: React.FC<SaveDashboardModalProps> = ({ dashboard, onDismiss }) => {
|
||||
return (
|
||||
<Modal isOpen={true} title="Cannot save provisioned dashboard" icon="copy" onDismiss={onDismiss}>
|
||||
<SaveProvisionedDashboardForm dashboard={dashboard} onCancel={onDismiss} onSuccess={onDismiss} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Button, Input, Switch, Form, Field, InputControl, Modal } from '@grafana/ui';
|
||||
import { Button, Input, Switch, Form, Field, InputControl, HorizontalGroup } from '@grafana/ui';
|
||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
||||
import { SaveDashboardFormProps } from '../types';
|
||||
@@ -119,17 +119,19 @@ export const SaveDashboardAsForm: React.FC<SaveDashboardAsFormProps> = ({
|
||||
name="$folder"
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Copy tags">
|
||||
<Switch {...register('copyTags')} />
|
||||
</Field>
|
||||
<Modal.ButtonRow>
|
||||
{!isNew && (
|
||||
<Field label="Copy tags">
|
||||
<Switch {...register('copyTags')} />
|
||||
</Field>
|
||||
)}
|
||||
<HorizontalGroup>
|
||||
<Button type="button" variant="secondary" onClick={onCancel} fill="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" aria-label="Save dashboard button">
|
||||
Save
|
||||
</Button>
|
||||
</Modal.ButtonRow>
|
||||
</HorizontalGroup>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mount } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { DashboardModel } from 'app/features/dashboard/state';
|
||||
import { SaveDashboardForm } from './SaveDashboardForm';
|
||||
import { SaveDashboardOptions } from '../types';
|
||||
|
||||
const prepareDashboardMock = (
|
||||
timeChanged: boolean,
|
||||
@@ -36,6 +37,16 @@ const renderAndSubmitForm = async (dashboard: any, submitSpy: any) => {
|
||||
submitSpy(jsonModel);
|
||||
return { status: 'success' };
|
||||
}}
|
||||
saveModel={{
|
||||
clone: dashboard,
|
||||
diff: {},
|
||||
diffCount: 0,
|
||||
hasChanges: true,
|
||||
}}
|
||||
options={{}}
|
||||
onOptionsChange={(opts: SaveDashboardOptions) => {
|
||||
return;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -56,6 +67,16 @@ describe('SaveDashboardAsForm', () => {
|
||||
onSubmit={async () => {
|
||||
return {};
|
||||
}}
|
||||
saveModel={{
|
||||
clone: prepareDashboardMock(true, true, jest.fn(), jest.fn()) as any,
|
||||
diff: {},
|
||||
diffCount: 0,
|
||||
hasChanges: true,
|
||||
}}
|
||||
options={{}}
|
||||
onOptionsChange={(opts: SaveDashboardOptions) => {
|
||||
return;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,70 +1,106 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
import { Button, Checkbox, Form, Modal, TextArea } from '@grafana/ui';
|
||||
import { Button, Checkbox, Form, TextArea } from '@grafana/ui';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { SaveDashboardFormProps } from '../types';
|
||||
import { DashboardModel } from 'app/features/dashboard/state';
|
||||
import { SaveDashboardData, SaveDashboardOptions } from '../types';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
|
||||
interface SaveDashboardFormDTO {
|
||||
interface FormDTO {
|
||||
message: string;
|
||||
saveVariables: boolean;
|
||||
saveTimerange: boolean;
|
||||
}
|
||||
|
||||
export const SaveDashboardForm: React.FC<SaveDashboardFormProps> = ({ dashboard, onCancel, onSuccess, onSubmit }) => {
|
||||
type Props = {
|
||||
dashboard: DashboardModel; // original
|
||||
saveModel: SaveDashboardData; // already cloned
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
onSubmit?: (clone: any, options: SaveDashboardOptions, dashboard: DashboardModel) => Promise<any>;
|
||||
options: SaveDashboardOptions;
|
||||
onOptionsChange: (opts: SaveDashboardOptions) => void;
|
||||
};
|
||||
|
||||
export const SaveDashboardForm = ({
|
||||
dashboard,
|
||||
saveModel,
|
||||
options,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
onOptionsChange,
|
||||
}: Props) => {
|
||||
const hasTimeChanged = useMemo(() => dashboard.hasTimeChanged(), [dashboard]);
|
||||
const hasVariableChanged = useMemo(() => dashboard.hasVariableValuesChanged(), [dashboard]);
|
||||
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
return (
|
||||
<Form
|
||||
onSubmit={async (data: SaveDashboardFormDTO) => {
|
||||
onSubmit={async (data: FormDTO) => {
|
||||
if (!onSubmit) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await onSubmit(dashboard.getSaveModelClone(data), data, dashboard);
|
||||
setSaving(true);
|
||||
const result = await onSubmit(saveModel.clone, options, dashboard);
|
||||
if (result.status === 'success') {
|
||||
if (data.saveVariables) {
|
||||
if (options.saveVariables) {
|
||||
dashboard.resetOriginalVariables();
|
||||
}
|
||||
if (data.saveTimerange) {
|
||||
if (options.saveTimerange) {
|
||||
dashboard.resetOriginalTime();
|
||||
}
|
||||
onSuccess();
|
||||
}
|
||||
setSaving(false);
|
||||
}}
|
||||
>
|
||||
{({ register, errors }) => (
|
||||
<>
|
||||
<div>
|
||||
{hasTimeChanged && (
|
||||
<Checkbox
|
||||
{...register('saveTimerange')}
|
||||
label="Save current time range as dashboard default"
|
||||
aria-label={selectors.pages.SaveDashboardModal.saveTimerange}
|
||||
/>
|
||||
)}
|
||||
{hasVariableChanged && (
|
||||
<Checkbox
|
||||
{...register('saveVariables')}
|
||||
label="Save current variable values as dashboard default"
|
||||
aria-label={selectors.pages.SaveDashboardModal.saveVariables}
|
||||
/>
|
||||
)}
|
||||
{(hasVariableChanged || hasTimeChanged) && <div className="gf-form-group" />}
|
||||
<Stack direction="column" gap={2}>
|
||||
{hasTimeChanged && (
|
||||
<Checkbox
|
||||
checked={options.saveTimerange}
|
||||
onChange={() =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
saveTimerange: !options.saveTimerange,
|
||||
})
|
||||
}
|
||||
label="Save current time range as dashboard default"
|
||||
aria-label={selectors.pages.SaveDashboardModal.saveTimerange}
|
||||
/>
|
||||
)}
|
||||
{hasVariableChanged && (
|
||||
<Checkbox
|
||||
checked={options.saveVariables}
|
||||
onChange={() =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
saveVariables: !options.saveVariables,
|
||||
})
|
||||
}
|
||||
label="Save current variable values as dashboard default"
|
||||
aria-label={selectors.pages.SaveDashboardModal.saveVariables}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TextArea {...register('message')} placeholder="Add a note to describe your changes." autoFocus />
|
||||
</div>
|
||||
<TextArea {...register('message')} placeholder="Add a note to describe your changes." autoFocus rows={5} />
|
||||
|
||||
<Modal.ButtonRow>
|
||||
<Stack alignItems="center">
|
||||
<Button variant="secondary" onClick={onCancel} fill="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" aria-label={selectors.pages.SaveDashboardModal.save}>
|
||||
Save
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!saveModel.hasChanges}
|
||||
icon={saving ? 'fa fa-spinner' : undefined}
|
||||
aria-label={selectors.pages.SaveDashboardModal.save}
|
||||
>
|
||||
{saving ? '' : 'Save'}
|
||||
</Button>
|
||||
</Modal.ButtonRow>
|
||||
</>
|
||||
{!saveModel.hasChanges && <div>No changes to save</div>}
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { Button, ClipboardButton, Modal, stylesFactory, TextArea, useTheme } from '@grafana/ui';
|
||||
import { Button, ClipboardButton, HorizontalGroup, stylesFactory, TextArea, useTheme } from '@grafana/ui';
|
||||
import { SaveDashboardFormProps } from '../types';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
|
||||
export const SaveProvisionedDashboardForm: React.FC<SaveDashboardFormProps> = ({ dashboard, onCancel }) => {
|
||||
const theme = useTheme();
|
||||
@@ -29,7 +30,7 @@ export const SaveProvisionedDashboardForm: React.FC<SaveDashboardFormProps> = ({
|
||||
const styles = getStyles(theme);
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Stack direction="column" gap={2}>
|
||||
<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.
|
||||
@@ -57,7 +58,7 @@ export const SaveProvisionedDashboardForm: React.FC<SaveDashboardFormProps> = ({
|
||||
}}
|
||||
className={styles.json}
|
||||
/>
|
||||
<Modal.ButtonRow>
|
||||
<HorizontalGroup>
|
||||
<Button variant="secondary" onClick={onCancel} fill="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
@@ -65,8 +66,8 @@ export const SaveProvisionedDashboardForm: React.FC<SaveDashboardFormProps> = ({
|
||||
Copy JSON to clipboard
|
||||
</ClipboardButton>
|
||||
<Button onClick={saveToFile}>Save JSON to file</Button>
|
||||
</Modal.ButtonRow>
|
||||
</div>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { CloneOptions, DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||
import { Diffs } from '../VersionHistory/utils';
|
||||
|
||||
export interface SaveDashboardData {
|
||||
clone: DashboardModel; // cloned copy
|
||||
diff: Diffs;
|
||||
diffCount: number; // cumulative count
|
||||
hasChanges: boolean; // not new and has changes
|
||||
}
|
||||
|
||||
export interface SaveDashboardOptions extends CloneOptions {
|
||||
folderId?: number;
|
||||
@@ -18,4 +26,5 @@ export interface SaveDashboardModalProps {
|
||||
dashboard: DashboardModel;
|
||||
onDismiss: () => void;
|
||||
onSaveSuccess?: () => void;
|
||||
isCopy?: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user