Scenes: Refactor original snapshot button in a new component (#82199)

This commit is contained in:
Ezequiel Victorero
2024-02-13 14:15:55 -03:00
committed by GitHub
parent ccb4533a86
commit dbde08b03c
10 changed files with 138 additions and 116 deletions

View File

@ -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 |

View File

@ -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

View File

@ -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
*/ */

View File

@ -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"`

View File

@ -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",

View File

@ -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>) {

View File

@ -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' });
}; };

View File

@ -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');
});
});

View File

@ -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);
}
};

View File

@ -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();
}}
/>
), ),
}); });