mirror of
https://github.com/grafana/grafana.git
synced 2025-07-29 01:32:17 +08:00
Scenes: Refactor original snapshot button in a new component (#82199)
This commit is contained in:

committed by
GitHub

parent
ccb4533a86
commit
dbde08b03c
@ -186,6 +186,7 @@ Sensitive information stripped: queries (metric, template,annotation) and panel
|
|||||||
| `key` | string | **Yes** | | Optional, defined the unique key of the snapshot, required if external is true |
|
| `key` | string | **Yes** | | Optional, defined the unique key of the snapshot, required if external is true |
|
||||||
| `name` | string | **Yes** | | Optional, name of the snapshot |
|
| `name` | string | **Yes** | | Optional, name of the snapshot |
|
||||||
| `orgId` | uint32 | **Yes** | | org id of the snapshot |
|
| `orgId` | uint32 | **Yes** | | org id of the snapshot |
|
||||||
|
| `originalUrl` | string | **Yes** | | original url, url of the dashboard that was snapshotted |
|
||||||
| `updated` | string | **Yes** | | last time when the snapshot was updated |
|
| `updated` | string | **Yes** | | last time when the snapshot was updated |
|
||||||
| `userId` | uint32 | **Yes** | | user id of the snapshot creator |
|
| `userId` | uint32 | **Yes** | | user id of the snapshot creator |
|
||||||
| `url` | string | No | | url of the snapshot, if snapshot was shared internally |
|
| `url` | string | No | | url of the snapshot, if snapshot was shared internally |
|
||||||
|
@ -494,6 +494,8 @@ lineage: schemas: [{
|
|||||||
external: bool @grafanamaturity(NeedsExpertReview)
|
external: bool @grafanamaturity(NeedsExpertReview)
|
||||||
// external url, if snapshot was shared in external grafana instance
|
// external url, if snapshot was shared in external grafana instance
|
||||||
externalUrl: string @grafanamaturity(NeedsExpertReview)
|
externalUrl: string @grafanamaturity(NeedsExpertReview)
|
||||||
|
// original url, url of the dashboard that was snapshotted
|
||||||
|
originalUrl: string @grafanamaturity(NeedsExpertReview)
|
||||||
// Unique identifier of the snapshot
|
// Unique identifier of the snapshot
|
||||||
id: uint32 @grafanamaturity(NeedsExpertReview)
|
id: uint32 @grafanamaturity(NeedsExpertReview)
|
||||||
// Optional, defined the unique key of the snapshot, required if external is true
|
// Optional, defined the unique key of the snapshot, required if external is true
|
||||||
|
@ -1088,6 +1088,10 @@ export interface Dashboard {
|
|||||||
* external url, if snapshot was shared in external grafana instance
|
* external url, if snapshot was shared in external grafana instance
|
||||||
*/
|
*/
|
||||||
externalUrl: string;
|
externalUrl: string;
|
||||||
|
/**
|
||||||
|
* original url, url of the dashboard that was snapshotted
|
||||||
|
*/
|
||||||
|
originalUrl: string;
|
||||||
/**
|
/**
|
||||||
* Unique identifier of the snapshot
|
* Unique identifier of the snapshot
|
||||||
*/
|
*/
|
||||||
|
@ -694,6 +694,9 @@ type Snapshot struct {
|
|||||||
// OrgId org id of the snapshot
|
// OrgId org id of the snapshot
|
||||||
OrgId int `json:"orgId"`
|
OrgId int `json:"orgId"`
|
||||||
|
|
||||||
|
// OriginalUrl original url, url of the dashboard that was snapshotted
|
||||||
|
OriginalUrl string `json:"originalUrl"`
|
||||||
|
|
||||||
// Updated last time when the snapshot was updated
|
// Updated last time when the snapshot was updated
|
||||||
Updated time.Time `json:"updated"`
|
Updated time.Time `json:"updated"`
|
||||||
|
|
||||||
|
@ -374,7 +374,7 @@
|
|||||||
0
|
0
|
||||||
],
|
],
|
||||||
"description": "A Grafana dashboard.",
|
"description": "A Grafana dashboard.",
|
||||||
"grafanaMaturityCount": 103,
|
"grafanaMaturityCount": 105,
|
||||||
"lineageIsGroup": false,
|
"lineageIsGroup": false,
|
||||||
"links": {
|
"links": {
|
||||||
"docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/dashboard/schema-reference",
|
"docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/dashboard/schema-reference",
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { CoreApp } from '@grafana/data';
|
import { CoreApp } from '@grafana/data';
|
||||||
import { config, locationService } from '@grafana/runtime';
|
|
||||||
import {
|
import {
|
||||||
sceneGraph,
|
sceneGraph,
|
||||||
SceneGridItem,
|
SceneGridItem,
|
||||||
@ -12,12 +11,10 @@ import {
|
|||||||
VizPanel,
|
VizPanel,
|
||||||
} from '@grafana/scenes';
|
} from '@grafana/scenes';
|
||||||
import { Dashboard } from '@grafana/schema';
|
import { Dashboard } from '@grafana/schema';
|
||||||
import { ConfirmModal } from '@grafana/ui';
|
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||||
import { VariablesChanged } from 'app/features/variables/types';
|
import { VariablesChanged } from 'app/features/variables/types';
|
||||||
|
|
||||||
import { ShowModalReactEvent } from '../../../types/events';
|
|
||||||
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||||
import { DecoratedRevisionModel } from '../settings/VersionsEditView';
|
import { DecoratedRevisionModel } from '../settings/VersionsEditView';
|
||||||
import { historySrv } from '../settings/version-history/HistorySrv';
|
import { historySrv } from '../settings/version-history/HistorySrv';
|
||||||
@ -208,63 +205,6 @@ 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>) {
|
function buildTestScene(overrides?: Partial<DashboardSceneState>) {
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import { css } from '@emotion/css';
|
|
||||||
import * as H from 'history';
|
import * as H from 'history';
|
||||||
import React from 'react';
|
|
||||||
import { Unsubscribable } from 'rxjs';
|
import { Unsubscribable } from 'rxjs';
|
||||||
|
|
||||||
import { AppEvents, CoreApp, DataQueryRequest, NavIndex, NavModelItem, locationUtil, textUtil } from '@grafana/data';
|
import { AppEvents, CoreApp, DataQueryRequest, NavIndex, NavModelItem, locationUtil } from '@grafana/data';
|
||||||
import { locationService, config } from '@grafana/runtime';
|
import { locationService } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
dataLayers,
|
dataLayers,
|
||||||
getUrlSyncManager,
|
getUrlSyncManager,
|
||||||
@ -25,7 +23,6 @@ import {
|
|||||||
VizPanel,
|
VizPanel,
|
||||||
} from '@grafana/scenes';
|
} from '@grafana/scenes';
|
||||||
import { Dashboard, DashboardLink } from '@grafana/schema';
|
import { Dashboard, DashboardLink } from '@grafana/schema';
|
||||||
import { ConfirmModal } from '@grafana/ui';
|
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
||||||
import { getNavModel } from 'app/core/selectors/navModel';
|
import { getNavModel } from 'app/core/selectors/navModel';
|
||||||
@ -35,7 +32,7 @@ import { DashboardModel } from 'app/features/dashboard/state';
|
|||||||
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
||||||
import { VariablesChanged } from 'app/features/variables/types';
|
import { VariablesChanged } from 'app/features/variables/types';
|
||||||
import { DashboardDTO, DashboardMeta, SaveDashboardResponseDTO } from 'app/types';
|
import { DashboardDTO, DashboardMeta, SaveDashboardResponseDTO } from 'app/types';
|
||||||
import { ShowModalReactEvent, ShowConfirmModalEvent } from 'app/types/events';
|
import { ShowConfirmModalEvent } from 'app/types/events';
|
||||||
|
|
||||||
import { PanelEditor } from '../panel-edit/PanelEditor';
|
import { PanelEditor } from '../panel-edit/PanelEditor';
|
||||||
import { SaveDashboardDrawer } from '../saving/SaveDashboardDrawer';
|
import { SaveDashboardDrawer } from '../saving/SaveDashboardDrawer';
|
||||||
@ -503,47 +500,6 @@ 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 = () => {
|
public onOpenSettings = () => {
|
||||||
locationService.partial({ editview: 'settings' });
|
locationService.partial({ editview: 'settings' });
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,60 @@
|
|||||||
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { config, locationService } from '@grafana/runtime';
|
||||||
|
import { ConfirmModal } from '@grafana/ui';
|
||||||
|
|
||||||
|
import appEvents from '../../../core/app_events';
|
||||||
|
import { ShowModalReactEvent } from '../../../types/events';
|
||||||
|
|
||||||
|
import { GoToSnapshotOriginButton } from './GoToSnapshotOriginButton';
|
||||||
|
|
||||||
|
describe('GoToSnapshotOriginButton component', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
locationService.push('/');
|
||||||
|
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('renders button and triggers onClick redirects to the original dashboard', () => {
|
||||||
|
render(<GoToSnapshotOriginButton originalURL={'/d/c0d2742f-b827-466d-9269-fb34d6af24ff'} />);
|
||||||
|
|
||||||
|
// Check if the button renders with the correct testid
|
||||||
|
expect(screen.getByTestId('button-snapshot')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Simulate a button click
|
||||||
|
fireEvent.click(screen.getByTestId('button-snapshot'));
|
||||||
|
|
||||||
|
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('renders button and triggers onClick opens a confirmation modal', () => {
|
||||||
|
render(<GoToSnapshotOriginButton originalURL={'http://www.anotherdomain.com/'} />);
|
||||||
|
|
||||||
|
// Check if the button renders with the correct testid
|
||||||
|
expect(screen.getByTestId('button-snapshot')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Simulate a button click
|
||||||
|
fireEvent.click(screen.getByTestId('button-snapshot'));
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,62 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { textUtil } from '@grafana/data';
|
||||||
|
import { config, locationService } from '@grafana/runtime';
|
||||||
|
import { ConfirmModal, ToolbarButton } from '@grafana/ui';
|
||||||
|
|
||||||
|
import appEvents from '../../../core/app_events';
|
||||||
|
import { t } from '../../../core/internationalization';
|
||||||
|
import { ShowModalReactEvent } from '../../../types/events';
|
||||||
|
|
||||||
|
export function GoToSnapshotOriginButton(props: { originalURL: string }) {
|
||||||
|
return (
|
||||||
|
<ToolbarButton
|
||||||
|
key="button-snapshot"
|
||||||
|
data-testid="button-snapshot"
|
||||||
|
tooltip={t('dashboard.toolbar.open-original', 'Open original dashboard')}
|
||||||
|
icon="link"
|
||||||
|
onClick={() => onOpenSnapshotOriginalDashboard(props.originalURL)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onOpenSnapshotOriginalDashboard = (originalUrl: string) => {
|
||||||
|
const relativeURL = 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);
|
||||||
|
}
|
||||||
|
};
|
@ -15,6 +15,7 @@ import { DashboardInteractions } from '../utils/interactions';
|
|||||||
import { dynamicDashNavActions } from '../utils/registerDynamicDashNavAction';
|
import { dynamicDashNavActions } from '../utils/registerDynamicDashNavAction';
|
||||||
|
|
||||||
import { DashboardScene } from './DashboardScene';
|
import { DashboardScene } from './DashboardScene';
|
||||||
|
import { GoToSnapshotOriginButton } from './GoToSnapshotOriginButton';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
dashboard: DashboardScene;
|
dashboard: DashboardScene;
|
||||||
@ -89,14 +90,7 @@ export function ToolbarActions({ dashboard }: Props) {
|
|||||||
group: 'icon-actions',
|
group: 'icon-actions',
|
||||||
condition: meta.isSnapshot && !isEditing,
|
condition: meta.isSnapshot && !isEditing,
|
||||||
render: () => (
|
render: () => (
|
||||||
<ToolbarButton
|
<GoToSnapshotOriginButton originalURL={dashboard.getInitialSaveModel()?.snapshot?.originalUrl ?? ''} />
|
||||||
key="button-snapshot"
|
|
||||||
tooltip={t('dashboard.toolbar.open-original', 'Open original dashboard')}
|
|
||||||
icon="link"
|
|
||||||
onClick={() => {
|
|
||||||
dashboard.onOpenSnapshotOriginalDashboard();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user