diff --git a/.betterer.results b/.betterer.results index 444ed4c4f6f..bb23979e018 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2433,10 +2433,6 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/settings/variables/utils.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/features/dashboard-scene/settings/version-history/useDashboardRestore.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] - ], "public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], @@ -2760,47 +2756,10 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] ], - "public/app/features/dashboard/components/VersionHistory/DiffGroup.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"] - ], - "public/app/features/dashboard/components/VersionHistory/DiffTitle.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"], - [0, 0, 0, "Styles should be written using objects.", "4"], - [0, 0, 0, "Styles should be written using objects.", "5"], - [0, 0, 0, "Styles should be written using objects.", "6"], - [0, 0, 0, "Styles should be written using objects.", "7"], - [0, 0, 0, "Styles should be written using objects.", "8"] - ], - "public/app/features/dashboard/components/VersionHistory/DiffValues.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] - ], - "public/app/features/dashboard/components/VersionHistory/DiffViewer.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] - ], - "public/app/features/dashboard/components/VersionHistory/VersionHistoryComparison.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"] - ], - "public/app/features/dashboard/components/VersionHistory/VersionHistoryHeader.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] - ], - "public/app/features/dashboard/components/VersionHistory/VersionHistoryTable.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] - ], "public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "public/app/features/dashboard/components/VersionHistory/utils.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] - ], "public/app/features/dashboard/containers/DashboardPage.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -7013,9 +6972,6 @@ exports[`no gf-form usage`] = { "public/app/features/dashboard/components/SubMenu/SubMenuItems.tsx:5381": [ [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] ], - "public/app/features/dashboard/components/VersionHistory/VersionHistoryTable.tsx:5381": [ - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] - ], "public/app/features/datasources/components/BasicSettings.tsx:5381": [ [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 8cd992def2e..483fb38ebd7 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -68,6 +68,8 @@ export interface DashboardSceneState extends SceneObjectState { isDirty?: boolean; /** meta flags */ meta: DashboardMeta; + /** Version of the dashboard */ + version?: number; /** Panel to inspect */ inspectPanelKey?: string; /** Panel to view in fullscreen */ diff --git a/public/app/features/dashboard-scene/serialization/SaveDashboardDrawer.tsx b/public/app/features/dashboard-scene/serialization/SaveDashboardDrawer.tsx index 7106a9ce3cd..358d857f441 100644 --- a/public/app/features/dashboard-scene/serialization/SaveDashboardDrawer.tsx +++ b/public/app/features/dashboard-scene/serialization/SaveDashboardDrawer.tsx @@ -3,9 +3,9 @@ import React from 'react'; import { SceneComponentProps, SceneObjectBase, SceneObjectState, SceneObjectRef } from '@grafana/scenes'; import { Drawer } from '@grafana/ui'; import { SaveDashboardDiff } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardDiff'; -import { jsonDiff } from 'app/features/dashboard/components/VersionHistory/utils'; import { DashboardScene } from '../scene/DashboardScene'; +import { jsonDiff } from '../settings/version-history/utils'; import { transformSceneToSaveModel } from './transformSceneToSaveModel'; diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap index d3453ad2b32..13bd90846e5 100644 --- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap +++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap @@ -263,6 +263,7 @@ exports[`transformSceneToSaveModel Given a scene with rows Should transform back "timezone": "", "title": "Repeating rows", "uid": "Repeating-rows-uid", + "version": 1, "weekStart": "", } `; @@ -593,6 +594,7 @@ exports[`transformSceneToSaveModel Given a simple scene with custom settings Sho "timezone": "America/New_York", "title": "My custom title", "uid": "nP8rcffGkasd", + "version": 2, "weekStart": "monday", } `; @@ -915,6 +917,7 @@ exports[`transformSceneToSaveModel Given a simple scene with variables Should tr "timezone": "America/New_York", "title": "Dashboard to load1", "uid": "nP8rcffGkasd", + "version": 2, "weekStart": "saturday", } `; diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 0e051c19809..566b6847ca0 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -241,6 +241,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) description: oldModel.description, editable: oldModel.editable, meta: oldModel.meta, + version: oldModel.version, body: new SceneGridLayout({ isLazy: true, children: createSceneObjectsForPanels(oldModel.panels), diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index 1b6bd05c12a..d484c6db76a 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -134,6 +134,7 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa templating: { list: variables, }, + version: state.version, timezone: timeRange.timeZone, fiscalYearStartMonth: timeRange.fiscalYearStartMonth, weekStart: timeRange.weekStart, diff --git a/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx b/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx index 7b927360567..51b6fb8fd91 100644 --- a/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx @@ -9,9 +9,15 @@ import { historySrv } from './version-history'; jest.mock('./version-history/HistorySrv'); describe('VersionsEditView', () => { - describe('Dashboard Versions state', () => { + describe('Dashboard versions state', () => { let dashboard: DashboardScene; let versionsView: VersionsEditView; + const mockEvent = { + preventDefault: jest.fn(), + currentTarget: { + checked: true, + }, + } as unknown as React.FormEvent; beforeEach(async () => { jest.mocked(historySrv.getHistoryList).mockResolvedValue(getVersions()); @@ -32,11 +38,13 @@ describe('VersionsEditView', () => { it('should return the decorated list of versions', () => { const versions = versionsView.versions; - expect(versions).toHaveLength(2); + expect(versions).toHaveLength(3); expect(versions[0].createdDateString).toBe('2017-02-22 20:43:01'); expect(versions[0].ageString).toBe('7 years ago'); expect(versions[1].createdDateString).toBe('2017-02-22 20:43:01'); expect(versions[1].ageString).toBe('7 years ago'); + expect(versions[2].createdDateString).toBe('2017-02-23 20:43:01'); + expect(versions[2].ageString).toBe('7 years ago'); }); it('should bump the start threshold when fetching more versions', async () => { @@ -47,6 +55,59 @@ describe('VersionsEditView', () => { expect(versionsView.start).toBe(VERSIONS_FETCH_LIMIT * 2); }); + + it('should set the state of a version as checked when onCheck is called', () => { + versionsView.onCheck(mockEvent, 3); + + expect(versionsView.versions[0].checked).toBe(false); + expect(versionsView.versions[1].checked).toBe(true); + expect(versionsView.versions[2].checked).toBe(false); + }); + + it('should reset the state of all versions when reset is called', () => { + versionsView.onCheck(mockEvent, 3); + + expect(versionsView.versions[1].checked).toBe(true); + + versionsView.reset(); + + expect(versionsView.versions[0].checked).toBe(false); + expect(versionsView.versions[1].checked).toBe(false); + expect(versionsView.versions[2].checked).toBe(false); + }); + + it('should set the diffData', async () => { + versionsView.onCheck(mockEvent, 3); + versionsView.onCheck(mockEvent, 4); + + jest + .mocked(historySrv.getDashboardVersion) + .mockResolvedValueOnce({ data: 'lhs' }) + .mockResolvedValue({ data: 'rhs' }); + + await versionsView.getDiff(); + + expect(versionsView.diffData).toEqual({ + lhs: 'lhs', + rhs: 'rhs', + }); + expect(versionsView.state.baseInfo).toHaveProperty('version', 3); + expect(versionsView.state.newInfo).toHaveProperty('version', 4); + }); + + it('should set the isNewLatest flag if the new selected version is latest', async () => { + versionsView.onCheck(mockEvent, 4); + versionsView.onCheck(mockEvent, 2); + + jest + .mocked(historySrv.getDashboardVersion) + .mockResolvedValueOnce({ data: 'lhs' }) + .mockResolvedValue({ data: 'rhs' }); + + await versionsView.getDiff(); + + expect(versionsView.state.isNewLatest).toBe(true); + }); }); }); @@ -62,6 +123,7 @@ function getVersions() { created: '2017-02-22T17:43:01-08:00', createdBy: 'admin', message: '', + checked: false, }, { id: 3, @@ -73,6 +135,19 @@ function getVersions() { created: '2017-02-22T17:43:01-08:00', createdBy: 'admin', message: '', + checked: false, + }, + { + id: 2, + dashboardId: 1, + dashboardUID: '_U4zObQMz', + parentVersion: 1, + restoredFrom: 1, + version: 2, + created: '2017-02-23T17:43:01-08:00', + createdBy: 'admin', + message: '', + checked: false, }, ]; } @@ -83,6 +158,7 @@ async function buildTestScene() { $timeRange: new SceneTimeRange({}), title: 'hello', uid: 'dash-1', + version: 4, meta: { canEdit: true, }, diff --git a/public/app/features/dashboard-scene/settings/VersionsEditView.tsx b/public/app/features/dashboard-scene/settings/VersionsEditView.tsx index e5f69fee760..81925076cf6 100644 --- a/public/app/features/dashboard-scene/settings/VersionsEditView.tsx +++ b/public/app/features/dashboard-scene/settings/VersionsEditView.tsx @@ -9,7 +9,14 @@ import { DashboardScene } from '../scene/DashboardScene'; import { getDashboardSceneFor } from '../utils/utils'; import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils'; -import { RevisionsModel, VersionHistoryTable, historySrv } from './version-history'; +import { + RevisionsModel, + VersionHistoryComparison, + VersionHistoryHeader, + VersionHistoryTable, + VersionsHistoryButtons, + historySrv, +} from './version-history'; export const VERSIONS_FETCH_LIMIT = 10; @@ -22,6 +29,11 @@ export interface VersionsEditViewState extends DashboardEditViewState { versions?: DecoratedRevisionModel[]; isLoading?: boolean; isAppending?: boolean; + viewMode?: 'list' | 'compare'; + diffData?: { lhs: string; rhs: string }; + newInfo?: DecoratedRevisionModel; + baseInfo?: DecoratedRevisionModel; + isNewLatest?: boolean; } export class VersionsEditView extends SceneObjectBase implements DashboardEditView { @@ -35,6 +47,12 @@ export class VersionsEditView extends SceneObjectBase imp versions: [], isLoading: true, isAppending: true, + viewMode: 'list', + isNewLatest: false, + diffData: { + lhs: '', + rhs: '', + }, }); this.addActivationHandler(() => { @@ -46,6 +64,10 @@ export class VersionsEditView extends SceneObjectBase imp return getDashboardSceneFor(this); } + public get diffData(): { lhs: string; rhs: string } { + return this.state.diffData ?? { lhs: '', rhs: '' }; + } + public get versions(): DecoratedRevisionModel[] { return this.state.versions ?? []; } @@ -92,6 +114,57 @@ export class VersionsEditView extends SceneObjectBase imp .finally(() => this.setState({ isAppending: false })); } + public getDiff = async () => { + const selectedVersions = this.versions.filter((version) => version.checked); + const [newInfo, baseInfo] = selectedVersions; + const isNewLatest = newInfo.version === this._dashboard.state.version; + + this.setState({ + isLoading: true, + }); + + if (!this._dashboard.state.uid) { + return; + } + + const lhs = await historySrv.getDashboardVersion(this._dashboard.state.uid, baseInfo.version); + const rhs = await historySrv.getDashboardVersion(this._dashboard.state.uid, newInfo.version); + + this.setState({ + baseInfo, + isLoading: false, + isNewLatest, + newInfo, + viewMode: 'compare', + diffData: { + lhs: lhs.data, + rhs: rhs.data, + }, + }); + }; + + public reset = () => { + this.setState({ + baseInfo: undefined, + diffData: { + lhs: '', + rhs: '', + }, + isNewLatest: false, + newInfo: undefined, + versions: this.versions.map((version) => ({ ...version, checked: false })), + viewMode: 'list', + }); + }; + + public onCheck = (ev: React.FormEvent, versionId: number) => { + this.setState({ + versions: this.versions.map((version) => + version.id === versionId ? { ...version, checked: ev.currentTarget.checked } : version + ), + }); + }; + private decorateVersions(versions: RevisionsModel[]): DecoratedRevisionModel[] { const timeZone = this.getTimeRange().getTimeZone(); @@ -108,30 +181,58 @@ export class VersionsEditView extends SceneObjectBase imp function VersionsEditorSettingsListView({ model }: SceneComponentProps) { const dashboard = model.getDashboard(); - const { isLoading, isAppending } = model.useState(); + const { isLoading, isAppending, viewMode, baseInfo, newInfo, isNewLatest } = model.useState(); const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey()); - const canCompare = model.versions.filter((version) => version.checked).length === 2; + const showButtons = model.versions.length > 1; + const hasMore = model.versions.length >= model.limit; + const isLastPage = model.versions.find((rev) => rev.version === 1); + + if (viewMode === 'compare') { + return ( + + + {isLoading ? ( + + ) : ( + + )} + + ); + } return ( {isLoading ? ( ) : ( - { - console.log('todo'); - }} - canCompare={canCompare} - /> + )} {isAppending && } + {showButtons && ( + + )} ); } -export const VersionsHistorySpinner = ({ msg }: { msg: string }) => ( +const VersionsHistorySpinner = ({ msg }: { msg: string }) => ( {msg} diff --git a/public/app/features/dashboard-scene/settings/version-history/DiffGroup.tsx b/public/app/features/dashboard-scene/settings/version-history/DiffGroup.tsx index d5d67c34d83..5d56d58ac53 100644 --- a/public/app/features/dashboard-scene/settings/version-history/DiffGroup.tsx +++ b/public/app/features/dashboard-scene/settings/version-history/DiffGroup.tsx @@ -43,15 +43,15 @@ export const DiffGroup = ({ diffs, title }: DiffGroupProps) => { const getStyles = (theme: GrafanaTheme2) => ({ container: css({ - 'background-color': theme.colors.background.secondary, - 'font-size': theme.typography.h6.fontSize, - 'margin-bottom': theme.spacing(2), + backgroundColor: theme.colors.background.secondary, + fontSize: theme.typography.h6.fontSize, + marginBottom: theme.spacing(2), padding: theme.spacing(2), }), list: css({ - 'margin-left': theme.spacing(4), + marginLeft: theme.spacing(4), }), listItem: css({ - 'margin-bottom': theme.spacing(1), + marginBottom: theme.spacing(1), }), }); diff --git a/public/app/features/dashboard-scene/settings/version-history/DiffTitle.tsx b/public/app/features/dashboard-scene/settings/version-history/DiffTitle.tsx index 7622da29b7f..db0fc5c7390 100644 --- a/public/app/features/dashboard-scene/settings/version-history/DiffTitle.tsx +++ b/public/app/features/dashboard-scene/settings/version-history/DiffTitle.tsx @@ -32,7 +32,7 @@ export const DiffTitle = ({ diff, title }: DiffTitleProps) => { const getDiffTitleStyles = (theme: GrafanaTheme2) => ({ embolden: css({ - 'font-weight': `${theme.typography.fontWeightBold}`, + fontWeight: theme.typography.fontWeightBold, }), add: css({ color: theme.colors.success.main, @@ -56,6 +56,6 @@ const getDiffTitleStyles = (theme: GrafanaTheme2) => ({ color: theme.colors.success.main, }), withoutDiff: css({ - 'margin-bottom': theme.spacing(2), + marginBottom: theme.spacing(2), }), }); diff --git a/public/app/features/dashboard-scene/settings/version-history/DiffValues.tsx b/public/app/features/dashboard-scene/settings/version-history/DiffValues.tsx index 181b9c07541..d17976ee12d 100644 --- a/public/app/features/dashboard-scene/settings/version-history/DiffValues.tsx +++ b/public/app/features/dashboard-scene/settings/version-history/DiffValues.tsx @@ -28,10 +28,10 @@ export const DiffValues = ({ diff }: DiffProps) => { const getStyles = (theme: GrafanaTheme2) => css({ - 'background-color': theme.colors.action.hover, - 'border-radius': theme.shape.radius.default, + backgroundColor: theme.colors.action.hover, + borderRadius: theme.shape.radius.default, color: theme.colors.text.primary, - 'font-size': theme.typography.body.fontSize, + fontSize: theme.typography.body.fontSize, margin: `0 ${theme.spacing(0.5)}`, padding: theme.spacing(0.5, 1), }); diff --git a/public/app/features/dashboard-scene/settings/version-history/DiffViewer.tsx b/public/app/features/dashboard-scene/settings/version-history/DiffViewer.tsx index 89060012b00..27ce79b8c82 100644 --- a/public/app/features/dashboard-scene/settings/version-history/DiffViewer.tsx +++ b/public/app/features/dashboard-scene/settings/version-history/DiffViewer.tsx @@ -42,18 +42,18 @@ export const DiffViewer = ({ oldValue, newValue }: ReactDiffViewerProps) => { codeFold: { fontSize: theme.typography.bodySmall.fontSize, }, - gutter: ` - pre { - color: ${tinycolor(theme.colors.text.disabled).setAlpha(1).toString()}; - opacity: 0.61; - } - `, + gutter: { + pre: { + color: tinycolor(theme.colors.text.disabled).setAlpha(1).toString(), + opacity: 0.61, + }, + }, }; return (
void; - version: number; -} - -export const RevertDashboardModal = ({ hideModal, version }: RevertDashboardModalProps) => { - // TODO: how should state.error be handled? - const { state, onRestoreDashboard } = useDashboardRestore(version); - - useEffect(() => { - if (!state.loading && state.value) { - hideModal(); - } - }, [state, hideModal]); - - return ( - Are you sure you want to restore the dashboard to version {version}? All unsaved changes will be lost.

- } - confirmText={`Yes, restore to version ${version}`} - /> - ); -}; diff --git a/public/app/features/dashboard-scene/settings/version-history/VersionHistoryComparison.tsx b/public/app/features/dashboard-scene/settings/version-history/VersionHistoryComparison.tsx index a2239fa60a2..218ac2d0a29 100644 --- a/public/app/features/dashboard-scene/settings/version-history/VersionHistoryComparison.tsx +++ b/public/app/features/dashboard-scene/settings/version-history/VersionHistoryComparison.tsx @@ -3,12 +3,12 @@ import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Button, ModalsController, CollapsableSection, HorizontalGroup, useStyles2 } from '@grafana/ui'; +import { RevertDashboardModal } from 'app/features/dashboard/components/VersionHistory/RevertDashboardModal'; import { DecoratedRevisionModel } from '../VersionsEditView'; import { DiffGroup } from './DiffGroup'; import { DiffViewer } from './DiffViewer'; -import { RevertDashboardModal } from './RevertDashboardModal'; import { jsonDiff } from './utils'; type DiffViewProps = { @@ -70,13 +70,13 @@ export const VersionHistoryComparison = ({ baseInfo, newInfo, diffData, isNewLat const getStyles = (theme: GrafanaTheme2) => ({ spacer: css({ - 'margin-bottom': theme.spacing(4), + marginBottom: theme.spacing(4), }), versionInfo: css({ color: theme.colors.text.secondary, - 'font-size': theme.typography.bodySmall.fontSize, + fontSize: theme.typography.bodySmall.fontSize, }), noMarginBottom: css({ - 'margin-bottom': 0, + marginBottom: 0, }), }); diff --git a/public/app/features/dashboard-scene/settings/version-history/VersionHistoryHeader.tsx b/public/app/features/dashboard-scene/settings/version-history/VersionHistoryHeader.tsx index ccfdaa6d17e..721b7b6dade 100644 --- a/public/app/features/dashboard-scene/settings/version-history/VersionHistoryHeader.tsx +++ b/public/app/features/dashboard-scene/settings/version-history/VersionHistoryHeader.tsx @@ -33,9 +33,9 @@ export const VersionHistoryHeader = ({ const getStyles = (theme: GrafanaTheme2) => ({ header: css({ - 'font-size': theme.typography.h3.fontSize, + fontSize: theme.typography.h3.fontSize, display: 'flex', gap: theme.spacing(2), - 'margin-bottom': theme.spacing(3), + marginBottom: theme.spacing(3), }), }); diff --git a/public/app/features/dashboard-scene/settings/version-history/VersionHistoryTable.tsx b/public/app/features/dashboard-scene/settings/version-history/VersionHistoryTable.tsx index b766bdac061..f664a36fd26 100644 --- a/public/app/features/dashboard-scene/settings/version-history/VersionHistoryTable.tsx +++ b/public/app/features/dashboard-scene/settings/version-history/VersionHistoryTable.tsx @@ -1,73 +1,87 @@ import { css } from '@emotion/css'; import React from 'react'; -import { Checkbox, Button, Tag, ModalsController } from '@grafana/ui'; +import { GrafanaTheme2 } from '@grafana/data'; +import { Checkbox, Button, Tag, ModalsController, useStyles2 } from '@grafana/ui'; +import { RevertDashboardModal } from 'app/features/dashboard/components/VersionHistory/RevertDashboardModal'; import { DecoratedRevisionModel } from '../VersionsEditView'; -import { RevertDashboardModal } from './RevertDashboardModal'; - type VersionsTableProps = { versions: DecoratedRevisionModel[]; canCompare: boolean; onCheck: (ev: React.FormEvent, versionId: number) => void; }; -export const VersionHistoryTable = ({ versions, canCompare, onCheck }: VersionsTableProps) => ( - - - - - - - - - - - - - {versions.map((version, idx) => ( - - - - - - - + + ))} + +
VersionDateUpdated byNotes
- onCheck(ev, version.id)} - disabled={!version.checked && canCompare} - /> - {version.version}{version.createdDateString}{version.createdBy}{version.message} - {idx === 0 ? ( - - ) : ( - - {({ showModal, hideModal }) => ( - +export const VersionHistoryTable = ({ versions, canCompare, onCheck }: VersionsTableProps) => { + const styles = useStyles2(getStyles); + + return ( +
+ + + + + + + + + + + + + {versions.map((version, idx) => ( + + + + + + + - - ))} - -
VersionDateUpdated byNotes
+ onCheck(ev, version.id)} + disabled={!version.checked && canCompare} + /> + {version.version}{version.createdDateString}{version.createdBy}{version.message} + {idx === 0 ? ( + + ) : ( + + {({ showModal, hideModal }) => ( + + )} + )} - - )} -
-); +
+
+ ); +}; + +function getStyles(theme: GrafanaTheme2) { + return { + margin: css({ + marginBottom: theme.spacing(4), + }), + }; +} diff --git a/public/app/features/dashboard-scene/settings/version-history/useDashboardRestore.tsx b/public/app/features/dashboard-scene/settings/version-history/useDashboardRestore.tsx deleted file mode 100644 index 447cd2fe392..00000000000 --- a/public/app/features/dashboard-scene/settings/version-history/useDashboardRestore.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { useEffect } from 'react'; -import { useAsyncFn } from 'react-use'; - -import { locationUtil } from '@grafana/data'; -import { locationService } from '@grafana/runtime'; -import { useAppNotification } from 'app/core/copy/appNotification'; -import { DashboardModel } from 'app/features/dashboard/state'; -import { useSelector } from 'app/types'; - -import { dashboardWatcher } from '../../../live/dashboard/dashboardWatcher'; - -import { historySrv } from './HistorySrv'; - -const restoreDashboard = async (version: number, dashboard: DashboardModel) => { - // Skip the watcher logic for this save since it's handled by the hook - dashboardWatcher.ignoreNextSave(); - return await historySrv.restoreDashboard(dashboard, version); -}; - -export const useDashboardRestore = (version: number) => { - const dashboard = useSelector((state) => state.dashboard.getModel()); - const [state, onRestoreDashboard] = useAsyncFn(async () => await restoreDashboard(version, dashboard!), []); - const notifyApp = useAppNotification(); - - useEffect(() => { - if (state.value) { - const location = locationService.getLocation(); - const newUrl = locationUtil.stripBaseFromUrl(state.value.url); - const prevState = (location.state as any)?.routeReloadCounter; - locationService.replace({ - ...location, - pathname: newUrl, - state: { routeReloadCounter: prevState ? prevState + 1 : 1 }, - }); - notifyApp.success('Dashboard restored', `Restored from version ${version}`); - } - }, [state, version, notifyApp]); - return { state, onRestoreDashboard }; -}; diff --git a/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.test.tsx b/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.test.tsx index 2ca3aae4e00..a9b34053458 100644 --- a/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.test.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.test.tsx @@ -6,15 +6,15 @@ import { BrowserRouter } from 'react-router-dom'; import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; import { GrafanaContext } from 'app/core/context/GrafanaContext'; +import { historySrv } from 'app/features/dashboard-scene/settings/version-history/HistorySrv'; import { configureStore } from '../../../../store/configureStore'; import { createDashboardModelFixture } from '../../state/__fixtures__/dashboardFixtures'; -import { historySrv } from '../VersionHistory/HistorySrv'; import { VersionsSettings, VERSIONS_FETCH_LIMIT } from './VersionsSettings'; import { versions, diffs } from './__mocks__/versions'; -jest.mock('../VersionHistory/HistorySrv'); +jest.mock('app/features/dashboard-scene/settings/version-history/HistorySrv'); const queryByFullText = (text: string) => screen.queryByText((_, node: Element | undefined | null) => { diff --git a/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx index bd4585b2c15..4faed3ced15 100644 --- a/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx @@ -3,7 +3,6 @@ import React, { PureComponent } from 'react'; import { config } from '@grafana/runtime'; import { Spinner, HorizontalGroup } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; - import { historySrv, RevisionsModel, @@ -11,7 +10,7 @@ import { VersionHistoryHeader, VersionsHistoryButtons, VersionHistoryComparison, -} from '../VersionHistory'; +} from 'app/features/dashboard-scene/settings/version-history'; import { SettingsPageProps } from './types'; @@ -22,7 +21,7 @@ type State = { isAppending: boolean; versions: DecoratedRevisionModel[]; viewMode: 'list' | 'compare'; - diffData: { lhs: unknown; rhs: unknown }; + diffData: { lhs: string; rhs: string }; newInfo?: DecoratedRevisionModel; baseInfo?: DecoratedRevisionModel; isNewLatest: boolean; @@ -50,8 +49,8 @@ export class VersionsSettings extends PureComponent { viewMode: 'list', isNewLatest: false, diffData: { - lhs: {}, - rhs: {}, + lhs: '', + rhs: '', }, }; } @@ -124,8 +123,8 @@ export class VersionsSettings extends PureComponent { this.setState({ baseInfo: undefined, diffData: { - lhs: {}, - rhs: {}, + lhs: '', + rhs: '', }, isNewLatest: false, newInfo: undefined, diff --git a/public/app/features/dashboard/components/SaveDashboard/SaveDashboardDiff.tsx b/public/app/features/dashboard/components/SaveDashboard/SaveDashboardDiff.tsx index 1b3746d87c5..850804f19f1 100644 --- a/public/app/features/dashboard/components/SaveDashboard/SaveDashboardDiff.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/SaveDashboardDiff.tsx @@ -4,10 +4,10 @@ import { useAsync } from 'react-use'; import { GrafanaTheme2 } from '@grafana/data'; import { Spinner, useStyles2 } from '@grafana/ui'; +import { Diffs } from 'app/features/dashboard-scene/settings/version-history/utils'; -import { DiffGroup } from '../VersionHistory/DiffGroup'; -import { DiffViewer } from '../VersionHistory/DiffViewer'; -import { Diffs } from '../VersionHistory/utils'; +import { DiffGroup } from '../../../dashboard-scene/settings/version-history/DiffGroup'; +import { DiffViewer } from '../../../dashboard-scene/settings/version-history/DiffViewer'; interface SaveDashboardDiffProps { oldValue?: unknown; diff --git a/public/app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer.tsx b/public/app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer.tsx index 1ee7e8aa335..1e5c0d5dfdf 100644 --- a/public/app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer.tsx @@ -2,8 +2,7 @@ import React, { useMemo, useState } from 'react'; import { config, isFetchError } from '@grafana/runtime'; import { Drawer, Tab, TabsBar } from '@grafana/ui'; - -import { jsonDiff } from '../VersionHistory/utils'; +import { jsonDiff } from 'app/features/dashboard-scene/settings/version-history/utils'; import DashboardValidation from './DashboardValidation'; import { SaveDashboardDiff } from './SaveDashboardDiff'; diff --git a/public/app/features/dashboard/components/SaveDashboard/types.ts b/public/app/features/dashboard/components/SaveDashboard/types.ts index 0baff9a2d89..8644d471b95 100644 --- a/public/app/features/dashboard/components/SaveDashboard/types.ts +++ b/public/app/features/dashboard/components/SaveDashboard/types.ts @@ -1,9 +1,8 @@ import { Dashboard } from '@grafana/schema'; import { CloneOptions, DashboardModel } from 'app/features/dashboard/state/DashboardModel'; +import { Diffs } from 'app/features/dashboard-scene/settings/version-history/utils'; import { DashboardDataDTO } from 'app/types'; -import { Diffs } from '../VersionHistory/utils'; - export interface SaveDashboardData { clone: Dashboard; // cloned copy diff: Diffs; diff --git a/public/app/features/dashboard/components/VersionHistory/DiffGroup.tsx b/public/app/features/dashboard/components/VersionHistory/DiffGroup.tsx deleted file mode 100644 index 5e5896e0ca0..00000000000 --- a/public/app/features/dashboard/components/VersionHistory/DiffGroup.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { css } from '@emotion/css'; -import { last } from 'lodash'; -import React from 'react'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { useStyles2 } from '@grafana/ui'; - -import { DiffTitle } from './DiffTitle'; -import { DiffValues } from './DiffValues'; -import { Diff, getDiffText } from './utils'; - -type DiffGroupProps = { - diffs: Diff[]; - title: string; -}; - -export const DiffGroup = ({ diffs, title }: DiffGroupProps) => { - const styles = useStyles2(getStyles); - - if (diffs.length === 1) { - return ( -
- -
- ); - } - - return ( -
- -
    - {diffs.map((diff: Diff, idx: number) => { - return ( -
  • - {getDiffText(diff)} -
  • - ); - })} -
-
- ); -}; - -const getStyles = (theme: GrafanaTheme2) => ({ - container: css` - background-color: ${theme.colors.background.secondary}; - font-size: ${theme.typography.h6.fontSize}; - margin-bottom: ${theme.spacing(2)}; - padding: ${theme.spacing(2)}; - `, - list: css` - margin-left: ${theme.spacing(4)}; - `, - listItem: css` - margin-bottom: ${theme.spacing(1)}; - `, -}); diff --git a/public/app/features/dashboard/components/VersionHistory/DiffTitle.tsx b/public/app/features/dashboard/components/VersionHistory/DiffTitle.tsx deleted file mode 100644 index c25c78c7361..00000000000 --- a/public/app/features/dashboard/components/VersionHistory/DiffTitle.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { css } from '@emotion/css'; -import React from 'react'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { useStyles2, Icon } from '@grafana/ui'; - -import { DiffValues } from './DiffValues'; -import { Diff, getDiffText } from './utils'; - -type DiffTitleProps = { - diff?: Diff; - title: string; -}; - -const replaceDiff: Diff = { op: 'replace', originalValue: undefined, path: [''], value: undefined, startLineNumber: 0 }; - -export const DiffTitle = ({ diff, title }: DiffTitleProps) => { - const styles = useStyles2(getDiffTitleStyles); - - return diff ? ( - <> - {title}{' '} - {getDiffText(diff, diff.path.length > 1)} - - ) : ( -
- {title}{' '} - {getDiffText(replaceDiff, false)} -
- ); -}; - -const getDiffTitleStyles = (theme: GrafanaTheme2) => ({ - embolden: css` - font-weight: ${theme.typography.fontWeightBold}; - `, - add: css` - color: ${theme.colors.success.main}; - `, - replace: css` - color: ${theme.colors.success.main}; - `, - move: css` - color: ${theme.colors.success.main}; - `, - copy: css` - color: ${theme.colors.success.main}; - `, - _get: css` - color: ${theme.colors.success.main}; - `, - test: css` - color: ${theme.colors.success.main}; - `, - remove: css` - color: ${theme.colors.success.main}; - `, - withoutDiff: css` - margin-bottom: ${theme.spacing(2)}; - `, -}); diff --git a/public/app/features/dashboard/components/VersionHistory/DiffValues.tsx b/public/app/features/dashboard/components/VersionHistory/DiffValues.tsx deleted file mode 100644 index cff84ade26d..00000000000 --- a/public/app/features/dashboard/components/VersionHistory/DiffValues.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { css } from '@emotion/css'; -import { isArray, isObject, isUndefined } from 'lodash'; -import React from 'react'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { useStyles2, Icon } from '@grafana/ui'; - -import { Diff } from './utils'; - -type DiffProps = { - diff: Diff; -}; - -export const DiffValues = ({ diff }: DiffProps) => { - const styles = useStyles2(getStyles); - const hasLeftValue = - !isUndefined(diff.originalValue) && !isArray(diff.originalValue) && !isObject(diff.originalValue); - const hasRightValue = !isUndefined(diff.value) && !isArray(diff.value) && !isObject(diff.value); - - return ( - <> - {hasLeftValue && {String(diff.originalValue)}} - {hasLeftValue && hasRightValue ? : null} - {hasRightValue && {String(diff.value)}} - - ); -}; - -const getStyles = (theme: GrafanaTheme2) => css` - background-color: ${theme.colors.action.hover}; - border-radius: ${theme.shape.radius.default}; - color: ${theme.colors.text.primary}; - font-size: ${theme.typography.body.fontSize}; - margin: 0 ${theme.spacing(0.5)}; - padding: ${theme.spacing(0.5, 1)}; -`; diff --git a/public/app/features/dashboard/components/VersionHistory/DiffViewer.tsx b/public/app/features/dashboard/components/VersionHistory/DiffViewer.tsx deleted file mode 100644 index b12014aa448..00000000000 --- a/public/app/features/dashboard/components/VersionHistory/DiffViewer.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { css } from '@emotion/css'; -import React from 'react'; -import ReactDiffViewer, { ReactDiffViewerProps, DiffMethod } from 'react-diff-viewer'; -import tinycolor from 'tinycolor2'; - -import { useTheme2 } from '@grafana/ui'; - -export const DiffViewer = ({ oldValue, newValue }: ReactDiffViewerProps) => { - const theme = useTheme2(); - - const styles = { - variables: { - // the light theme supplied by ReactDiffViewer is very similar to Grafana - // the dark theme needs some tweaks. - dark: { - diffViewerBackground: theme.colors.background.canvas, - diffViewerColor: theme.colors.text.primary, - addedBackground: tinycolor(theme.v1.palette.greenShade).setAlpha(0.3).toString(), - addedColor: 'white', - removedBackground: tinycolor(theme.v1.palette.redShade).setAlpha(0.3).toString(), - removedColor: 'white', - wordAddedBackground: tinycolor(theme.v1.palette.greenBase).setAlpha(0.4).toString(), - wordRemovedBackground: tinycolor(theme.v1.palette.redBase).setAlpha(0.4).toString(), - addedGutterBackground: tinycolor(theme.v1.palette.greenShade).setAlpha(0.2).toString(), - removedGutterBackground: tinycolor(theme.v1.palette.redShade).setAlpha(0.2).toString(), - gutterBackground: theme.colors.background.primary, - gutterBackgroundDark: theme.colors.background.primary, - highlightBackground: tinycolor(theme.colors.primary.main).setAlpha(0.4).toString(), - highlightGutterBackground: tinycolor(theme.colors.primary.shade).setAlpha(0.2).toString(), - codeFoldGutterBackground: theme.colors.background.secondary, - codeFoldBackground: theme.colors.background.secondary, - emptyLineBackground: theme.colors.background.secondary, - gutterColor: theme.colors.text.disabled, - addedGutterColor: theme.colors.text.primary, - removedGutterColor: theme.colors.text.primary, - codeFoldContentColor: theme.colors.text.disabled, - diffViewerTitleBackground: theme.colors.background.secondary, - diffViewerTitleColor: theme.colors.text.disabled, - diffViewerTitleBorderColor: theme.colors.border.strong, - }, - }, - codeFold: { - fontSize: theme.typography.bodySmall.fontSize, - }, - gutter: ` - pre { - color: ${tinycolor(theme.colors.text.disabled).setAlpha(1).toString()}; - opacity: 0.61; - } - `, - }; - - return ( -
- -
- ); -}; diff --git a/public/app/features/dashboard/components/VersionHistory/HistorySrv.test.ts b/public/app/features/dashboard/components/VersionHistory/HistorySrv.test.ts deleted file mode 100644 index ab89796f738..00000000000 --- a/public/app/features/dashboard/components/VersionHistory/HistorySrv.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { createDashboardModelFixture } from '../../state/__fixtures__/dashboardFixtures'; - -import { HistorySrv } from './HistorySrv'; -import { restore, versions } from './__mocks__/dashboardHistoryMocks'; - -const getMock = jest.fn().mockResolvedValue({}); -const postMock = jest.fn().mockResolvedValue({}); - -jest.mock('app/core/store'); -jest.mock('@grafana/runtime', () => { - const original = jest.requireActual('@grafana/runtime'); - - return { - ...original, - getBackendSrv: () => ({ - post: postMock, - get: getMock, - }), - }; -}); - -describe('historySrv', () => { - const versionsResponse = versions(); - const restoreResponse = restore; - - let historySrv = new HistorySrv(); - - const dash = createDashboardModelFixture({ uid: '_U4zObQMz' }); - const emptyDash = createDashboardModelFixture(); - const historyListOpts = { limit: 10, start: 0 }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('getHistoryList', () => { - it('should return a versions array for the given dashboard id', () => { - getMock.mockImplementation(() => Promise.resolve(versionsResponse)); - historySrv = new HistorySrv(); - - return historySrv.getHistoryList(dash.uid, historyListOpts).then((versions) => { - expect(versions).toEqual(versionsResponse); - }); - }); - - it('should return an empty array when not given an id', () => { - return historySrv.getHistoryList(emptyDash.uid, historyListOpts).then((versions) => { - expect(versions).toEqual([]); - }); - }); - - it('should return an empty array when not given a dashboard id', () => { - return historySrv.getHistoryList(null as unknown as string, historyListOpts).then((versions) => { - expect(versions).toEqual([]); - }); - }); - }); - - describe('restoreDashboard', () => { - it('should return a success response given valid parameters', () => { - const version = 6; - postMock.mockImplementation(() => Promise.resolve(restoreResponse(version))); - historySrv = new HistorySrv(); - return historySrv.restoreDashboard(dash, version).then((response) => { - expect(response).toEqual(restoreResponse(version)); - }); - }); - - it('should return an empty object when not given an id', async () => { - historySrv = new HistorySrv(); - const rsp = await historySrv.restoreDashboard(emptyDash, 6); - expect(rsp).toEqual({}); - }); - }); -}); diff --git a/public/app/features/dashboard/components/VersionHistory/HistorySrv.ts b/public/app/features/dashboard/components/VersionHistory/HistorySrv.ts deleted file mode 100644 index bb3aaec7abe..00000000000 --- a/public/app/features/dashboard/components/VersionHistory/HistorySrv.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { isNumber } from 'lodash'; - -import { getBackendSrv } from '@grafana/runtime'; - -import { DashboardModel } from '../../state/DashboardModel'; - -export interface HistoryListOpts { - limit: number; - start: number; -} - -export interface RevisionsModel { - id: number; - checked: boolean; - dashboardUID: string; - parentVersion: number; - version: number; - created: Date; - createdBy: string; - message: string; -} - -export interface DiffTarget { - dashboardUID: string; - version: number; - unsavedDashboard?: DashboardModel; // when doing diffs against unsaved dashboard version -} - -export class HistorySrv { - getHistoryList(dashboardUID: string, options: HistoryListOpts) { - if (typeof dashboardUID !== 'string') { - return Promise.resolve([]); - } - - return getBackendSrv().get(`api/dashboards/uid/${dashboardUID}/versions`, options); - } - - getDashboardVersion(uid: string, version: number) { - return getBackendSrv().get(`api/dashboards/uid/${uid}/versions/${version}`); - } - - restoreDashboard(dashboard: DashboardModel, version: number) { - const uid = dashboard && dashboard.uid ? dashboard.uid : void 0; - const url = `api/dashboards/uid/${uid}/restore`; - - return uid && isNumber(version) ? getBackendSrv().post(url, { version }) : Promise.resolve({}); - } -} - -const historySrv = new HistorySrv(); -export { historySrv }; diff --git a/public/app/features/dashboard/components/VersionHistory/VersionHistoryButtons.tsx b/public/app/features/dashboard/components/VersionHistory/VersionHistoryButtons.tsx deleted file mode 100644 index 073b9c64d8d..00000000000 --- a/public/app/features/dashboard/components/VersionHistory/VersionHistoryButtons.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; - -import { Tooltip, Button, Stack } from '@grafana/ui'; - -type VersionsButtonsType = { - hasMore: boolean; - canCompare: boolean; - getVersions: (append: boolean) => void; - getDiff: () => void; - isLastPage: boolean; -}; -export const VersionsHistoryButtons = ({ - hasMore, - canCompare, - getVersions, - getDiff, - isLastPage, -}: VersionsButtonsType) => ( - - {hasMore && ( - - )} - - - - -); diff --git a/public/app/features/dashboard/components/VersionHistory/VersionHistoryComparison.tsx b/public/app/features/dashboard/components/VersionHistory/VersionHistoryComparison.tsx deleted file mode 100644 index 1690197d87d..00000000000 --- a/public/app/features/dashboard/components/VersionHistory/VersionHistoryComparison.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { css, cx } from '@emotion/css'; -import React from 'react'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { Button, ModalsController, CollapsableSection, HorizontalGroup, useStyles2 } from '@grafana/ui'; - -import { DecoratedRevisionModel } from '../DashboardSettings/VersionsSettings'; - -import { DiffGroup } from './DiffGroup'; -import { DiffViewer } from './DiffViewer'; -import { RevertDashboardModal } from './RevertDashboardModal'; -import { jsonDiff } from './utils'; - -type DiffViewProps = { - isNewLatest: boolean; - newInfo: DecoratedRevisionModel; - baseInfo: DecoratedRevisionModel; - diffData: { lhs: unknown; rhs: unknown }; -}; - -export const VersionHistoryComparison = ({ baseInfo, newInfo, diffData, isNewLatest }: DiffViewProps) => { - const diff = jsonDiff(diffData.lhs, diffData.rhs); - const styles = useStyles2(getStyles); - - return ( -
-
- -
-

- Version {newInfo.version} updated by {newInfo.createdBy} {newInfo.ageString} -{' '} - {newInfo.message} -

-

- Version {baseInfo.version} updated by {baseInfo.createdBy} {baseInfo.ageString} -{' '} - {baseInfo.message} -

-
- {isNewLatest && ( - - {({ showModal, hideModal }) => ( - - )} - - )} -
-
-
- {Object.entries(diff).map(([key, diffs]) => ( - - ))} -
- - - -
- ); -}; - -const getStyles = (theme: GrafanaTheme2) => ({ - spacer: css` - margin-bottom: ${theme.spacing(4)}; - `, - versionInfo: css` - color: ${theme.colors.text.secondary}; - font-size: ${theme.typography.bodySmall.fontSize}; - `, - noMarginBottom: css` - margin-bottom: 0; - `, -}); diff --git a/public/app/features/dashboard/components/VersionHistory/VersionHistoryHeader.tsx b/public/app/features/dashboard/components/VersionHistory/VersionHistoryHeader.tsx deleted file mode 100644 index 1205e084f9d..00000000000 --- a/public/app/features/dashboard/components/VersionHistory/VersionHistoryHeader.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { css } from '@emotion/css'; -import { noop } from 'lodash'; -import React from 'react'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { Icon, IconButton, useStyles2 } from '@grafana/ui'; - -type VersionHistoryHeaderProps = { - onClick?: () => void; - baseVersion?: number; - newVersion?: number; - isNewLatest?: boolean; -}; - -export const VersionHistoryHeader = ({ - onClick = noop, - baseVersion = 0, - newVersion = 0, - isNewLatest = false, -}: VersionHistoryHeaderProps) => { - const styles = useStyles2(getStyles); - - return ( -

- - - Comparing {baseVersion} {newVersion}{' '} - {isNewLatest && (Latest)} - -

- ); -}; - -const getStyles = (theme: GrafanaTheme2) => ({ - header: css` - font-size: ${theme.typography.h3.fontSize}; - display: flex; - gap: ${theme.spacing(2)}; - margin-bottom: ${theme.spacing(3)}; - `, -}); diff --git a/public/app/features/dashboard/components/VersionHistory/VersionHistoryTable.tsx b/public/app/features/dashboard/components/VersionHistory/VersionHistoryTable.tsx deleted file mode 100644 index 61f4c34878d..00000000000 --- a/public/app/features/dashboard/components/VersionHistory/VersionHistoryTable.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { css } from '@emotion/css'; -import React from 'react'; - -import { Checkbox, Button, Tag, ModalsController } from '@grafana/ui'; - -import { DecoratedRevisionModel } from '../DashboardSettings/VersionsSettings'; - -import { RevertDashboardModal } from './RevertDashboardModal'; - -type VersionsTableProps = { - versions: DecoratedRevisionModel[]; - canCompare: boolean; - onCheck: (ev: React.FormEvent, versionId: number) => void; -}; - -export const VersionHistoryTable = ({ versions, canCompare, onCheck }: VersionsTableProps) => ( - - - - - - - - - - - - - {versions.map((version, idx) => ( - - - - - - - - - ))} - -
VersionDateUpdated byNotes
- onCheck(ev, version.id)} - disabled={!version.checked && canCompare} - /> - {version.version}{version.createdDateString}{version.createdBy}{version.message} - {idx === 0 ? ( - - ) : ( - - {({ showModal, hideModal }) => ( - - )} - - )} -
-); diff --git a/public/app/features/dashboard/components/VersionHistory/__mocks__/dashboardHistoryMocks.ts b/public/app/features/dashboard/components/VersionHistory/__mocks__/dashboardHistoryMocks.ts deleted file mode 100644 index 0de97c47a95..00000000000 --- a/public/app/features/dashboard/components/VersionHistory/__mocks__/dashboardHistoryMocks.ts +++ /dev/null @@ -1,176 +0,0 @@ -export function versions() { - return [ - { - id: 4, - dashboardId: 1, - dashboardUID: '_U4zObQMz', - parentVersion: 3, - restoredFrom: 0, - version: 4, - created: '2017-02-22T17:43:01-08:00', - createdBy: 'admin', - message: '', - }, - { - id: 3, - dashboardId: 1, - dashboardUID: '_U4zObQMz', - parentVersion: 1, - restoredFrom: 1, - version: 3, - created: '2017-02-22T17:43:01-08:00', - createdBy: 'admin', - message: '', - }, - { - id: 2, - dashboardId: 1, - dashboardUID: '_U4zObQMz', - parentVersion: 0, - restoredFrom: -1, - version: 2, - created: '2017-02-22T17:29:52-08:00', - createdBy: 'admin', - message: '', - }, - { - id: 1, - dashboardId: 1, - dashboardUID: '_U4zObQMz', - parentVersion: 0, - restoredFrom: -1, - slug: 'history-dashboard', - version: 1, - created: '2017-02-22T17:06:37-08:00', - createdBy: 'admin', - message: '', - }, - ]; -} - -export function restore(version: number, restoredFrom?: number) { - return { - dashboard: { - meta: { - type: 'db', - canSave: true, - canEdit: true, - canStar: true, - slug: 'history-dashboard', - expires: '0001-01-01T00:00:00Z', - created: '2017-02-21T18:40:45-08:00', - updated: '2017-04-11T21:31:22.59219665-07:00', - updatedBy: 'admin', - createdBy: 'admin', - version: version, - }, - dashboard: { - annotations: { - list: [], - }, - description: 'A random dashboard for implementing the history list', - editable: true, - gnetId: null, - graphTooltip: 0, - id: 1, - uid: '_U4zObQMz', - links: [], - restoredFrom: restoredFrom, - rows: [ - { - collapse: false, - height: '250px', - panels: [ - { - aliasColors: {}, - bars: false, - datasource: null, - fill: 1, - id: 1, - legend: { - avg: false, - current: false, - max: false, - min: false, - show: true, - total: false, - values: false, - }, - lines: true, - linewidth: 1, - nullPointMode: 'null', - percentage: false, - pointradius: 5, - points: false, - renderer: 'flot', - seriesOverrides: [], - span: 12, - stack: false, - steppedLine: false, - targets: [{}], - thresholds: [], - timeFrom: null, - timeShift: null, - title: 'Panel Title', - tooltip: { - shared: true, - sort: 0, - value_type: 'individual', - }, - type: 'graph', - xaxis: { - mode: 'time', - name: null, - show: true, - values: [], - }, - yaxes: [ - { - format: 'short', - label: null, - logBase: 1, - max: null, - min: null, - show: true, - }, - { - format: 'short', - label: null, - logBase: 1, - max: null, - min: null, - show: true, - }, - ], - }, - ], - repeat: null, - repeatIteration: null, - repeatRowId: null, - showTitle: false, - title: 'Dashboard Row', - titleSize: 'h6', - }, - ], - schemaVersion: 14, - tags: ['development'], - templating: { - list: [], - }, - time: { - from: 'now-6h', - to: 'now', - }, - timepicker: { - refresh_intervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'], - time_options: ['5m', '15m', '1h', '6h', '12h', '24h', '2d', '7d', '30d'], - }, - timezone: 'utc', - title: 'History Dashboard', - version: version, - }, - }, - message: 'Dashboard restored to version ' + version, - version: version, - }; -} diff --git a/public/app/features/dashboard/components/VersionHistory/index.ts b/public/app/features/dashboard/components/VersionHistory/index.ts deleted file mode 100644 index c87d2d0b9b7..00000000000 --- a/public/app/features/dashboard/components/VersionHistory/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { HistorySrv, historySrv, RevisionsModel } from './HistorySrv'; -export { VersionHistoryTable } from './VersionHistoryTable'; -export { VersionHistoryHeader } from './VersionHistoryHeader'; -export { VersionsHistoryButtons } from './VersionHistoryButtons'; -export { VersionHistoryComparison } from './VersionHistoryComparison'; diff --git a/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx b/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx index b5f22045ad5..2d700d32452 100644 --- a/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx +++ b/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx @@ -4,13 +4,12 @@ import { useAsyncFn } from 'react-use'; import { locationUtil } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { useAppNotification } from 'app/core/copy/appNotification'; +import { historySrv } from 'app/features/dashboard-scene/settings/version-history'; import { useSelector } from 'app/types'; import { dashboardWatcher } from '../../../live/dashboard/dashboardWatcher'; import { DashboardModel } from '../../state'; -import { historySrv } from './HistorySrv'; - const restoreDashboard = async (version: number, dashboard: DashboardModel) => { // Skip the watcher logic for this save since it's handled by the hook dashboardWatcher.ignoreNextSave(); diff --git a/public/app/features/dashboard/components/VersionHistory/utils.test.ts b/public/app/features/dashboard/components/VersionHistory/utils.test.ts deleted file mode 100644 index 5523229b7dc..00000000000 --- a/public/app/features/dashboard/components/VersionHistory/utils.test.ts +++ /dev/null @@ -1,293 +0,0 @@ -import { Diff, getDiffOperationText, getDiffText, jsonDiff } from './utils'; - -describe('getDiffOperationText', () => { - const cases = [ - ['add', 'added'], - ['remove', 'deleted'], - ['replace', 'changed'], - ['byDefault', 'changed'], - ]; - - test.each(cases)('it returns the correct verb for an operation', (operation, expected) => { - expect(getDiffOperationText(operation)).toBe(expected); - }); -}); - -type DiffTextCase = [Partial, string]; -describe('getDiffText', () => { - const addEmptyArray: DiffTextCase = [ - { op: 'add', value: [], path: ['annotations', 'list'], startLineNumber: 24 }, - 'added list', - ]; - const addArrayNumericProp: DiffTextCase = [ - { - op: 'add', - value: ['tag'], - path: ['panels', '3'], - }, - 'added item 3', - ]; - const addArrayProp: DiffTextCase = [ - { - op: 'add', - value: [{ name: 'dummy target 1' }, { name: 'dummy target 2' }], - path: ['panels', '3', 'targets'], - }, - 'added 2 targets', - ]; - const addValueNumericProp: DiffTextCase = [ - { - op: 'add', - value: 'foo', - path: ['panels', '3'], - }, - 'added item 3', - ]; - const addValueProp: DiffTextCase = [ - { - op: 'add', - value: 'foo', - path: ['panels', '3', 'targets'], - }, - 'added targets', - ]; - - const removeEmptyArray: DiffTextCase = [ - { op: 'remove', originalValue: [], path: ['annotations', 'list'], startLineNumber: 24 }, - 'deleted list', - ]; - const removeArrayNumericProp: DiffTextCase = [ - { - op: 'remove', - originalValue: ['tag'], - path: ['panels', '3'], - }, - 'deleted item 3', - ]; - const removeArrayProp: DiffTextCase = [ - { - op: 'remove', - originalValue: [{ name: 'dummy target 1' }, { name: 'dummy target 2' }], - path: ['panels', '3', 'targets'], - }, - 'deleted 2 targets', - ]; - const removeValueNumericProp: DiffTextCase = [ - { - op: 'remove', - originalValue: 'foo', - path: ['panels', '3'], - }, - 'deleted item 3', - ]; - const removeValueProp: DiffTextCase = [ - { - op: 'remove', - originalValue: 'foo', - path: ['panels', '3', 'targets'], - }, - 'deleted targets', - ]; - const replaceValueNumericProp: DiffTextCase = [ - { - op: 'replace', - originalValue: 'foo', - value: 'bar', - path: ['panels', '3'], - }, - 'changed item 3', - ]; - const replaceValueProp: DiffTextCase = [ - { - op: 'replace', - originalValue: 'foo', - value: 'bar', - path: ['panels', '3', 'targets'], - }, - 'changed targets', - ]; - - const cases = [ - addEmptyArray, - addArrayNumericProp, - addArrayProp, - addValueNumericProp, - addValueProp, - removeEmptyArray, - removeArrayNumericProp, - removeArrayProp, - removeValueNumericProp, - removeValueProp, - replaceValueNumericProp, - replaceValueProp, - ]; - - test.each(cases)( - 'returns a semantic message based on the type of diff, the values and the location of the change', - (diff: Partial, expected: string) => { - expect(getDiffText(diff as unknown as Diff)).toBe(expected); - } - ); -}); - -describe('jsonDiff', () => { - it('returns data related to each change', () => { - const lhs = { - annotations: { - list: [ - { - builtIn: 1, - datasource: '-- Grafana --', - enable: true, - hide: true, - iconColor: 'rgba(0, 211, 255, 1)', - name: 'Annotations & Alerts', - type: 'dashboard', - }, - ], - }, - editable: true, - gnetId: null, - graphTooltip: 0, - id: 141, - links: [], - panels: [], - schemaVersion: 27, - tags: [], - templating: { - list: [], - }, - time: { - from: 'now-6h', - to: 'now', - }, - timepicker: {}, - timezone: '', - title: 'test dashboard', - uid: '_U4zObQMz', - version: 2, - }; - - const rhs = { - annotations: { - list: [ - { - builtIn: 1, - datasource: '-- Grafana --', - enable: true, - hide: true, - iconColor: 'rgba(0, 211, 255, 1)', - name: 'Annotations & Alerts', - type: 'dashboard', - }, - ], - }, - description: 'a description', - editable: true, - gnetId: null, - graphTooltip: 1, - id: 141, - links: [], - panels: [ - { - type: 'graph', - }, - ], - schemaVersion: 27, - tags: ['the tag'], - templating: { - list: [], - }, - time: { - from: 'now-6h', - to: 'now', - }, - timepicker: { - refresh_intervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d', '2d'], - }, - timezone: 'utc', - title: 'My favourite dashboard', - uid: '_U4zObQMz', - version: 3, - }; - - const expected = { - description: [ - { - op: 'add', - originalValue: undefined, - path: ['description'], - startLineNumber: 14, - value: 'a description', - }, - ], - graphTooltip: [ - { - op: 'replace', - originalValue: 0, - path: ['graphTooltip'], - startLineNumber: 17, - value: 1, - }, - ], - panels: [ - { - op: 'add', - originalValue: undefined, - path: ['panels', '0'], - startLineNumber: 21, - value: { - type: 'graph', - }, - }, - ], - tags: [ - { - op: 'add', - originalValue: undefined, - path: ['tags', '0'], - startLineNumber: 27, - value: 'the tag', - }, - ], - timepicker: [ - { - op: 'add', - originalValue: undefined, - path: ['timepicker', 'refresh_intervals'], - startLineNumber: 37, - value: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d', '2d'], - }, - ], - timezone: [ - { - op: 'replace', - originalValue: '', - path: ['timezone'], - startLineNumber: 51, - value: 'utc', - }, - ], - title: [ - { - op: 'replace', - originalValue: 'test dashboard', - path: ['title'], - startLineNumber: 52, - value: 'My favourite dashboard', - }, - ], - version: [ - { - op: 'replace', - originalValue: 2, - path: ['version'], - startLineNumber: 54, - value: 3, - }, - ], - }; - - expect(jsonDiff(lhs, rhs)).toStrictEqual(expected); - }); -}); diff --git a/public/app/features/dashboard/components/VersionHistory/utils.ts b/public/app/features/dashboard/components/VersionHistory/utils.ts deleted file mode 100644 index 9700c8a524c..00000000000 --- a/public/app/features/dashboard/components/VersionHistory/utils.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { compare, Operation } from 'fast-json-patch'; -// @ts-ignore -import jsonMap from 'json-source-map'; -import { flow, get, isArray, isEmpty, last, sortBy, tail, toNumber, isNaN } from 'lodash'; - -export type Diff = { - op: 'add' | 'replace' | 'remove' | 'copy' | 'test' | '_get' | 'move'; - value: unknown; - originalValue: unknown; - path: string[]; - startLineNumber: number; -}; - -export type Diffs = { - [key: string]: Diff[]; -}; - -export const jsonDiff = (lhs: any, rhs: any): Diffs => { - const diffs = compare(lhs, rhs); - const lhsMap = jsonMap.stringify(lhs, null, 2); - const rhsMap = jsonMap.stringify(rhs, null, 2); - - const getDiffInformation = (diffs: Operation[]): Diff[] => { - return diffs.map((diff) => { - let originalValue = undefined; - let value = undefined; - let startLineNumber = 0; - - const path = tail(diff.path.split('/')); - - if (diff.op === 'replace' && rhsMap.pointers[diff.path]) { - originalValue = get(lhs, path); - value = diff.value; - startLineNumber = rhsMap.pointers[diff.path].value.line; - } - if (diff.op === 'add' && rhsMap.pointers[diff.path]) { - value = diff.value; - startLineNumber = rhsMap.pointers[diff.path].value.line; - } - if (diff.op === 'remove' && lhsMap.pointers[diff.path]) { - originalValue = get(lhs, path); - startLineNumber = lhsMap.pointers[diff.path].value.line; - } - - return { - op: diff.op, - value, - path, - originalValue, - startLineNumber, - }; - }); - }; - - const sortByLineNumber = (diffs: Diff[]) => sortBy(diffs, 'startLineNumber'); - const groupByPath = (diffs: Diff[]) => - diffs.reduce>((acc, value) => { - const groupKey: string = value.path[0]; - if (!acc[groupKey]) { - acc[groupKey] = []; - } - acc[groupKey].push(value); - return acc; - }, {}); - - return flow([getDiffInformation, sortByLineNumber, groupByPath])(diffs); -}; - -export const getDiffText = (diff: Diff, showProp = true) => { - const prop = last(diff.path)!; - const propIsNumeric = isNumeric(prop); - const val = diff.op === 'remove' ? diff.originalValue : diff.value; - let text = getDiffOperationText(diff.op); - - if (showProp) { - if (propIsNumeric) { - text += ` item ${prop}`; - } else { - if (isArray(val) && !isEmpty(val)) { - text += ` ${val.length} ${prop}`; - } else { - text += ` ${prop}`; - } - } - } - - return text; -}; - -const isNumeric = (value: string) => !isNaN(toNumber(value)); - -export const getDiffOperationText = (operation: string): string => { - if (operation === 'add') { - return 'added'; - } - if (operation === 'remove') { - return 'deleted'; - } - return 'changed'; -}; diff --git a/public/app/features/dashboard/index.ts b/public/app/features/dashboard/index.ts index 1a39f7495ec..12c86883b58 100644 --- a/public/app/features/dashboard/index.ts +++ b/public/app/features/dashboard/index.ts @@ -4,5 +4,4 @@ import './services/DashboardSrv'; // Components import './components/DashExportModal'; import './components/DashNav'; -import './components/VersionHistory'; import './components/DashboardSettings';