Scenes: Add snapshots view support (#81522)

pull/82022/head
Ezequiel Victorero 1 year ago committed by GitHub
parent 47546a4c72
commit 83ea51f241
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 24
      public/app/features/dashboard-scene/pages/DashboardScenePage.tsx
  2. 10
      public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts
  3. 21
      public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts
  4. 60
      public/app/features/dashboard-scene/scene/DashboardScene.test.tsx
  5. 50
      public/app/features/dashboard-scene/scene/DashboardScene.tsx
  6. 25
      public/app/features/dashboard-scene/scene/NavToolbarActions.tsx
  7. 2
      public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts
  8. 2
      public/app/features/dashboard/components/DashNav/DashNav.tsx
  9. 4
      public/app/features/dashboard/containers/DashboardPageProxy.tsx

@ -19,16 +19,28 @@ export function DashboardScenePage({ match, route, queryParams, history }: Props
const routeReloadCounter = (history.location.state as any)?.routeReloadCounter;
useEffect(() => {
stateManager.loadDashboard({
uid: match.params.uid ?? '',
route: route.routeName as DashboardRoutes,
urlFolderUid: queryParams.folderUid,
});
if (route.routeName === DashboardRoutes.Normal && match.params.type === 'snapshot') {
stateManager.loadSnapshot(match.params.slug!);
} else {
stateManager.loadDashboard({
uid: match.params.uid ?? '',
route: route.routeName as DashboardRoutes,
urlFolderUid: queryParams.folderUid,
});
}
return () => {
stateManager.clearState();
};
}, [stateManager, match.params.uid, route.routeName, queryParams.folderUid, routeReloadCounter]);
}, [
stateManager,
match.params.uid,
route.routeName,
queryParams.folderUid,
routeReloadCounter,
match.params.slug,
match.params.type,
]);
if (!dashboard) {
return (

@ -56,6 +56,16 @@ describe('DashboardScenePageStateManager', () => {
expect(loader.state.isLoading).toBe(false);
});
it('should use DashboardScene creator to initialize the snapshot scene', async () => {
setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} });
const loader = new DashboardScenePageStateManager({});
await loader.loadSnapshot('fake-slug');
expect(loader.state.dashboard).toBeInstanceOf(DashboardScene);
expect(loader.state.isLoading).toBe(false);
});
it('should initialize url sync', async () => {
setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} });

@ -116,6 +116,27 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
return rsp;
}
public async loadSnapshot(slug: string) {
try {
const dashboard = await this.loadSnapshotScene(slug);
this.setState({ dashboard: dashboard, isLoading: false });
} catch (err) {
this.setState({ isLoading: false, loadError: String(err) });
}
}
private async loadSnapshotScene(slug: string): Promise<DashboardScene> {
const rsp = await dashboardLoaderSrv.loadDashboard('snapshot', slug, '');
if (rsp?.dashboard) {
const scene = transformSaveModelToScene(rsp);
return scene;
}
throw new Error('Snapshot not found');
}
public async loadDashboard(options: LoadDashboardOptions) {
try {
const dashboard = await this.loadScene(options);

@ -1,4 +1,5 @@
import { CoreApp } from '@grafana/data';
import { config, locationService } from '@grafana/runtime';
import {
sceneGraph,
SceneGridItem,
@ -11,10 +12,12 @@ import {
VizPanel,
} from '@grafana/scenes';
import { Dashboard } from '@grafana/schema';
import { ConfirmModal } from '@grafana/ui';
import appEvents from 'app/core/app_events';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { VariablesChanged } from 'app/features/variables/types';
import { ShowModalReactEvent } from '../../../types/events';
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
import { DecoratedRevisionModel } from '../settings/VersionsEditView';
import { historySrv } from '../settings/version-history/HistorySrv';
@ -205,6 +208,63 @@ describe('DashboardScene', () => {
}
});
});
describe('when opening a dashboard from a snapshot', () => {
let scene: DashboardScene;
beforeEach(async () => {
scene = buildTestScene();
locationService.push('/');
// mockLocationHref('http://snapshots.grafana.com/snapshots/dashboard/abcdefghi/my-dash');
const location = window.location;
//@ts-ignore
delete window.location;
window.location = {
...location,
href: 'http://snapshots.grafana.com/snapshots/dashboard/abcdefghi/my-dash',
};
jest.spyOn(appEvents, 'publish');
});
config.appUrl = 'http://snapshots.grafana.com/';
it('redirects to the original dashboard', () => {
scene.setInitialSaveModel({
// @ts-ignore
snapshot: { originalUrl: '/d/c0d2742f-b827-466d-9269-fb34d6af24ff' },
});
// Call the function
scene.onOpenSnapshotOriginalDashboard();
// Assertions
expect(appEvents.publish).toHaveBeenCalledTimes(0);
expect(locationService.getLocation().pathname).toEqual('/d/c0d2742f-b827-466d-9269-fb34d6af24ff');
expect(window.location.href).toBe('http://snapshots.grafana.com/snapshots/dashboard/abcdefghi/my-dash');
});
it('opens a confirmation modal', () => {
scene.setInitialSaveModel({
// @ts-ignore
snapshot: { originalUrl: 'http://www.anotherdomain.com/' },
});
// Call the function
scene.onOpenSnapshotOriginalDashboard();
// Assertions
expect(appEvents.publish).toHaveBeenCalledTimes(1);
expect(appEvents.publish).toHaveBeenCalledWith(
new ShowModalReactEvent(
expect.objectContaining({
component: ConfirmModal,
})
)
);
expect(locationService.getLocation().pathname).toEqual('/');
expect(window.location.href).toBe('http://snapshots.grafana.com/snapshots/dashboard/abcdefghi/my-dash');
});
});
});
function buildTestScene(overrides?: Partial<DashboardSceneState>) {

@ -1,8 +1,10 @@
import { css } from '@emotion/css';
import * as H from 'history';
import React from 'react';
import { Unsubscribable } from 'rxjs';
import { CoreApp, DataQueryRequest, NavIndex, NavModelItem, locationUtil } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { CoreApp, DataQueryRequest, NavIndex, NavModelItem, locationUtil, textUtil } from '@grafana/data';
import { locationService, config } from '@grafana/runtime';
import {
getUrlSyncManager,
SceneFlexLayout,
@ -19,6 +21,7 @@ import {
SceneVariableDependencyConfigLike,
} from '@grafana/scenes';
import { Dashboard, DashboardLink } from '@grafana/schema';
import { ConfirmModal } from '@grafana/ui';
import appEvents from 'app/core/app_events';
import { getNavModel } from 'app/core/selectors/navModel';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
@ -26,7 +29,7 @@ import { DashboardModel } from 'app/features/dashboard/state';
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
import { VariablesChanged } from 'app/features/variables/types';
import { DashboardDTO, DashboardMeta, SaveDashboardResponseDTO } from 'app/types';
import { ShowConfirmModalEvent } from 'app/types/events';
import { ShowModalReactEvent, ShowConfirmModalEvent } from 'app/types/events';
import { PanelEditor } from '../panel-edit/PanelEditor';
import { SaveDashboardDrawer } from '../saving/SaveDashboardDrawer';
@ -423,6 +426,47 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
}
}
public onOpenSnapshotOriginalDashboard = () => {
// @ts-ignore
const relativeURL = this.getInitialSaveModel()?.snapshot?.originalUrl ?? '';
const sanitizedRelativeURL = textUtil.sanitizeUrl(relativeURL);
try {
const sanitizedAppUrl = new URL(sanitizedRelativeURL, config.appUrl);
const appUrl = new URL(config.appUrl);
if (sanitizedAppUrl.host !== appUrl.host) {
appEvents.publish(
new ShowModalReactEvent({
component: ConfirmModal,
props: {
title: 'Proceed to external site?',
modalClass: css({
width: 'max-content',
maxWidth: '80vw',
}),
body: (
<>
<p>
{`This link connects to an external website at`} <code>{relativeURL}</code>
</p>
<p>{"Are you sure you'd like to proceed?"}</p>
</>
),
confirmVariant: 'primary',
confirmText: 'Proceed',
onConfirm: () => {
window.location.href = sanitizedAppUrl.href;
},
},
})
);
} else {
locationService.push(sanitizedRelativeURL);
}
} catch (err) {
console.error('Failed to open original dashboard', err);
}
};
public onOpenSettings = () => {
locationService.partial({ editview: 'settings' });
};

@ -72,7 +72,28 @@ export function ToolbarActions({ dashboard }: Props) {
key="view-in-old-dashboard-button"
tooltip={'Switch to old dashboard page'}
icon="apps"
onClick={() => locationService.push(`/d/${uid}`)}
onClick={() => {
if (meta.isSnapshot) {
locationService.partial({ scenes: null });
} else {
locationService.push(`/d/${uid}`);
}
}}
/>
),
});
toolbarActions.push({
group: 'icon-actions',
condition: meta.isSnapshot && !isEditing,
render: () => (
<ToolbarButton
key="button-snapshot"
tooltip={t('dashboard.toolbar.open-original', 'Open original dashboard')}
icon="link"
onClick={() => {
dashboard.onOpenSnapshotOriginalDashboard();
}}
/>
),
});
@ -132,7 +153,7 @@ export function ToolbarActions({ dashboard }: Props) {
toolbarActions.push({
group: 'main-buttons',
condition: uid && !isEditing,
condition: uid && !isEditing && !meta.isSnapshot,
render: () => (
<Button
key="share-dashboard-button"

@ -211,7 +211,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel)
});
}
if (oldModel.annotations?.list?.length) {
if (oldModel.annotations?.list?.length && !oldModel.isSnapshot()) {
layers = oldModel.annotations?.list.map((a) => {
// Each annotation query is an individual data layer
return new DashboardAnnotationsDataLayer({

@ -198,7 +198,7 @@ export const DashNav = React.memo<Props>((props) => {
);
}
if (config.featureToggles.scenes && !dashboard.isSnapshot()) {
if (config.featureToggles.scenes) {
buttons.push(
<DashNavButton
key="button-scenes"

@ -32,6 +32,10 @@ function DashboardPageProxy(props: DashboardPageProxyProps) {
// To avoid querying single dashboard multiple times, stateManager.fetchDashboard uses a simple, short-lived cache.
// eslint-disable-next-line react-hooks/rules-of-hooks
const dashboard = useAsync(async () => {
if (props.match.params.type === 'snapshot') {
return null;
}
return stateManager.fetchDashboard({
route: props.route.routeName as DashboardRoutes,
uid: props.match.params.uid ?? '',

Loading…
Cancel
Save