mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Scenes: Add snapshots view support (#81522)
This commit is contained in:
parent
47546a4c72
commit
83ea51f241
@ -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…
Reference in New Issue
Block a user