mirror of
https://github.com/grafana/grafana.git
synced 2025-07-28 19:02:29 +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 |
|
||||
| `name` | string | **Yes** | | Optional, name 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 |
|
||||
| `userId` | uint32 | **Yes** | | user id of the snapshot creator |
|
||||
| `url` | string | No | | url of the snapshot, if snapshot was shared internally |
|
||||
|
@ -494,6 +494,8 @@ lineage: schemas: [{
|
||||
external: bool @grafanamaturity(NeedsExpertReview)
|
||||
// external url, if snapshot was shared in external grafana instance
|
||||
externalUrl: string @grafanamaturity(NeedsExpertReview)
|
||||
// original url, url of the dashboard that was snapshotted
|
||||
originalUrl: string @grafanamaturity(NeedsExpertReview)
|
||||
// Unique identifier of the snapshot
|
||||
id: uint32 @grafanamaturity(NeedsExpertReview)
|
||||
// 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
|
||||
*/
|
||||
externalUrl: string;
|
||||
/**
|
||||
* original url, url of the dashboard that was snapshotted
|
||||
*/
|
||||
originalUrl: string;
|
||||
/**
|
||||
* Unique identifier of the snapshot
|
||||
*/
|
||||
|
@ -694,6 +694,9 @@ type Snapshot struct {
|
||||
// OrgId org id of the snapshot
|
||||
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 time.Time `json:"updated"`
|
||||
|
||||
|
@ -374,7 +374,7 @@
|
||||
0
|
||||
],
|
||||
"description": "A Grafana dashboard.",
|
||||
"grafanaMaturityCount": 103,
|
||||
"grafanaMaturityCount": 105,
|
||||
"lineageIsGroup": false,
|
||||
"links": {
|
||||
"docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/dashboard/schema-reference",
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { CoreApp } from '@grafana/data';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import {
|
||||
sceneGraph,
|
||||
SceneGridItem,
|
||||
@ -12,12 +11,10 @@ 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';
|
||||
@ -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>) {
|
||||
|
@ -1,10 +1,8 @@
|
||||
import { css } from '@emotion/css';
|
||||
import * as H from 'history';
|
||||
import React from 'react';
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
|
||||
import { AppEvents, CoreApp, DataQueryRequest, NavIndex, NavModelItem, locationUtil, textUtil } from '@grafana/data';
|
||||
import { locationService, config } from '@grafana/runtime';
|
||||
import { AppEvents, CoreApp, DataQueryRequest, NavIndex, NavModelItem, locationUtil } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import {
|
||||
dataLayers,
|
||||
getUrlSyncManager,
|
||||
@ -25,7 +23,6 @@ import {
|
||||
VizPanel,
|
||||
} from '@grafana/scenes';
|
||||
import { Dashboard, DashboardLink } from '@grafana/schema';
|
||||
import { ConfirmModal } from '@grafana/ui';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
||||
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 { VariablesChanged } from 'app/features/variables/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 { 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 = () => {
|
||||
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 { DashboardScene } from './DashboardScene';
|
||||
import { GoToSnapshotOriginButton } from './GoToSnapshotOriginButton';
|
||||
|
||||
interface Props {
|
||||
dashboard: DashboardScene;
|
||||
@ -89,14 +90,7 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
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();
|
||||
}}
|
||||
/>
|
||||
<GoToSnapshotOriginButton originalURL={dashboard.getInitialSaveModel()?.snapshot?.originalUrl ?? ''} />
|
||||
),
|
||||
});
|
||||
|
||||
|
Reference in New Issue
Block a user