From dbde08b03c05356c740d3adb02276651a085748b Mon Sep 17 00:00:00 2001 From: Ezequiel Victorero Date: Tue, 13 Feb 2024 14:15:55 -0300 Subject: [PATCH] Scenes: Refactor original snapshot button in a new component (#82199) --- .../kinds/core/dashboard/schema-reference.md | 1 + kinds/dashboard/dashboard_kind.cue | 2 + .../raw/dashboard/x/dashboard_types.gen.ts | 4 ++ pkg/kinds/dashboard/dashboard_spec_gen.go | 3 + pkg/kindsysreport/codegen/report.json | 2 +- .../scene/DashboardScene.test.tsx | 60 ------------------ .../dashboard-scene/scene/DashboardScene.tsx | 50 +-------------- .../scene/GoToSnapshotOriginButton.test.tsx | 60 ++++++++++++++++++ .../scene/GoToSnapshotOriginButton.tsx | 62 +++++++++++++++++++ .../scene/NavToolbarActions.tsx | 10 +-- 10 files changed, 138 insertions(+), 116 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.test.tsx create mode 100644 public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.tsx diff --git a/docs/sources/developers/kinds/core/dashboard/schema-reference.md b/docs/sources/developers/kinds/core/dashboard/schema-reference.md index eb287875f5c..561580fd2fd 100644 --- a/docs/sources/developers/kinds/core/dashboard/schema-reference.md +++ b/docs/sources/developers/kinds/core/dashboard/schema-reference.md @@ -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 | diff --git a/kinds/dashboard/dashboard_kind.cue b/kinds/dashboard/dashboard_kind.cue index 6cace8d26ff..fe963ef5bab 100644 --- a/kinds/dashboard/dashboard_kind.cue +++ b/kinds/dashboard/dashboard_kind.cue @@ -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 diff --git a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts index c34707a521c..3e7d114de76 100644 --- a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts +++ b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts @@ -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 */ diff --git a/pkg/kinds/dashboard/dashboard_spec_gen.go b/pkg/kinds/dashboard/dashboard_spec_gen.go index dfd9bf19629..3014bba97ef 100644 --- a/pkg/kinds/dashboard/dashboard_spec_gen.go +++ b/pkg/kinds/dashboard/dashboard_spec_gen.go @@ -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"` diff --git a/pkg/kindsysreport/codegen/report.json b/pkg/kindsysreport/codegen/report.json index 015db1369bd..a44e34a9472 100644 --- a/pkg/kindsysreport/codegen/report.json +++ b/pkg/kindsysreport/codegen/report.json @@ -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", diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index c41f368b4dd..85c0986fcf9 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -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) { diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 7f1c7706f1a..11402d317ae 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -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 { } } - 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: ( - <> -

- {`This link connects to an external website at`} {relativeURL} -

-

{"Are you sure you'd like to proceed?"}

- - ), - 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' }); }; diff --git a/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.test.tsx b/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.test.tsx new file mode 100644 index 00000000000..6e55b4d0ab0 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.test.tsx @@ -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(); + + // 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(); + + // 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'); + }); +}); diff --git a/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.tsx b/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.tsx new file mode 100644 index 00000000000..28c5c802f47 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.tsx @@ -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 ( + 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: ( + <> +

+ {`This link connects to an external website at`} {relativeURL} +

+

{"Are you sure you'd like to proceed?"}

+ + ), + confirmVariant: 'primary', + confirmText: 'Proceed', + onConfirm: () => { + window.location.href = sanitizedAppUrl.href; + }, + }, + }) + ); + } else { + locationService.push(sanitizedRelativeURL); + } + } catch (err) { + console.error('Failed to open original dashboard', err); + } +}; diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 8b1e01af6b7..540bc7fefb0 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -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: () => ( - { - dashboard.onOpenSnapshotOriginalDashboard(); - }} - /> + ), });