mirror of https://github.com/grafana/grafana
DashboardScene: ShareModal + link sharing (#74955)
* DashboardScene: Panel menu updates, adding explore action * DashboardScene: Panel menu updates, adding explore action * Initial test * Update * share modal * Update * rename * Update tests * Fix test * update * Fix tooltip wording * Update translation file * fix e2e * Extract ShareLinkTab component * rename to overlay --------- Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>pull/75230/head
parent
6a37a56d68
commit
1d1bdaab37
@ -0,0 +1,121 @@ |
|||||||
|
import { act, render, screen } from '@testing-library/react'; |
||||||
|
import userEvent from '@testing-library/user-event'; |
||||||
|
import { advanceTo, clear } from 'jest-date-mock'; |
||||||
|
import React from 'react'; |
||||||
|
|
||||||
|
import { dateTime } from '@grafana/data'; |
||||||
|
import { selectors } from '@grafana/e2e-selectors'; |
||||||
|
import { config, locationService } from '@grafana/runtime'; |
||||||
|
import { SceneGridItem, SceneGridLayout, SceneTimeRange, VizPanel } from '@grafana/scenes'; |
||||||
|
|
||||||
|
import { DashboardScene } from '../scene/DashboardScene'; |
||||||
|
|
||||||
|
import { ShareLinkTab } from './ShareLinkTab'; |
||||||
|
|
||||||
|
jest.mock('app/core/utils/shortLinks', () => ({ |
||||||
|
createShortLink: jest.fn().mockResolvedValue(`http://localhost:3000/goto/shortend-uid`), |
||||||
|
})); |
||||||
|
|
||||||
|
describe('ShareLinkTab', () => { |
||||||
|
const fakeCurrentDate = dateTime('2019-02-11T19:00:00.000Z').toDate(); |
||||||
|
|
||||||
|
afterAll(() => { |
||||||
|
clear(); |
||||||
|
}); |
||||||
|
|
||||||
|
beforeAll(() => { |
||||||
|
advanceTo(fakeCurrentDate); |
||||||
|
|
||||||
|
config.appUrl = 'http://dashboards.grafana.com/grafana/'; |
||||||
|
config.rendererAvailable = true; |
||||||
|
config.bootData.user.orgId = 1; |
||||||
|
locationService.push('/scenes/dashboard/dash-1?from=now-6h&to=now'); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('with locked time range (absolute) range', () => { |
||||||
|
it('should generate share url absolute time', async () => { |
||||||
|
buildAndRenderScenario({}); |
||||||
|
|
||||||
|
expect(await screen.findByRole('textbox', { name: 'Link URL' })).toHaveValue( |
||||||
|
'http://dashboards.grafana.com/grafana/scenes/dashboard/dash-1?from=2019-02-11T13:00:00.000Z&to=2019-02-11T19:00:00.000Z&viewPanel=panel-12' |
||||||
|
); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('with disabled locked range range', () => { |
||||||
|
it('should generate share url with relative time', async () => { |
||||||
|
const tab = buildAndRenderScenario({}); |
||||||
|
act(() => tab.onToggleLockedTime()); |
||||||
|
|
||||||
|
expect(await screen.findByRole('textbox', { name: 'Link URL' })).toHaveValue( |
||||||
|
'http://dashboards.grafana.com/grafana/scenes/dashboard/dash-1?from=now-6h&to=now&viewPanel=panel-12' |
||||||
|
); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should add theme when specified', async () => { |
||||||
|
const tab = buildAndRenderScenario({}); |
||||||
|
act(() => tab.onThemeChange('light')); |
||||||
|
|
||||||
|
expect(await screen.findByRole('textbox', { name: 'Link URL' })).toHaveValue( |
||||||
|
'http://dashboards.grafana.com/grafana/scenes/dashboard/dash-1?from=2019-02-11T13:00:00.000Z&to=2019-02-11T19:00:00.000Z&viewPanel=panel-12&theme=light' |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should shorten url', async () => { |
||||||
|
buildAndRenderScenario({}); |
||||||
|
|
||||||
|
await userEvent.click(await screen.findByLabelText('Shorten URL')); |
||||||
|
|
||||||
|
expect(await screen.findByRole('textbox', { name: 'Link URL' })).toHaveValue( |
||||||
|
`http://localhost:3000/goto/shortend-uid` |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should generate render url', async () => { |
||||||
|
buildAndRenderScenario({}); |
||||||
|
|
||||||
|
expect( |
||||||
|
await screen.findByRole('link', { name: selectors.pages.SharePanelModal.linkToRenderedImage }) |
||||||
|
).toHaveAttribute( |
||||||
|
'href', |
||||||
|
'http://dashboards.grafana.com/grafana/render/d-solo/dash-1?from=2019-02-11T13:00:00.000Z&to=2019-02-11T19:00:00.000Z&viewPanel=panel-12&width=1000&height=500&tz=Pacific%2FEaster' |
||||||
|
); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
interface ScenarioOptions { |
||||||
|
withPanel?: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
function buildAndRenderScenario(options: ScenarioOptions) { |
||||||
|
const panel = new VizPanel({ |
||||||
|
title: 'Panel A', |
||||||
|
pluginId: 'table', |
||||||
|
key: 'panel-12', |
||||||
|
}); |
||||||
|
|
||||||
|
const dashboard = new DashboardScene({ |
||||||
|
title: 'hello', |
||||||
|
uid: 'dash-1', |
||||||
|
$timeRange: new SceneTimeRange({}), |
||||||
|
body: new SceneGridLayout({ |
||||||
|
children: [ |
||||||
|
new SceneGridItem({ |
||||||
|
key: 'griditem-1', |
||||||
|
x: 0, |
||||||
|
y: 0, |
||||||
|
width: 10, |
||||||
|
height: 12, |
||||||
|
body: panel, |
||||||
|
}), |
||||||
|
], |
||||||
|
}), |
||||||
|
}); |
||||||
|
|
||||||
|
const tab = new ShareLinkTab({ dashboardRef: dashboard.getRef(), panelRef: panel.getRef() }); |
||||||
|
|
||||||
|
render(<tab.Component model={tab} />); |
||||||
|
|
||||||
|
return tab; |
||||||
|
} |
@ -0,0 +1,255 @@ |
|||||||
|
import React from 'react'; |
||||||
|
|
||||||
|
import { dateTime, UrlQueryMap } from '@grafana/data'; |
||||||
|
import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; |
||||||
|
import { config, locationService } from '@grafana/runtime'; |
||||||
|
import { |
||||||
|
SceneComponentProps, |
||||||
|
SceneObjectBase, |
||||||
|
SceneObjectState, |
||||||
|
SceneObjectRef, |
||||||
|
VizPanel, |
||||||
|
sceneGraph, |
||||||
|
} from '@grafana/scenes'; |
||||||
|
import { TimeZone } from '@grafana/schema'; |
||||||
|
import { Alert, ClipboardButton, Field, FieldSet, Icon, Input, Switch } from '@grafana/ui'; |
||||||
|
import { t, Trans } from 'app/core/internationalization'; |
||||||
|
import { createShortLink } from 'app/core/utils/shortLinks'; |
||||||
|
import { ThemePicker } from 'app/features/dashboard/components/ShareModal/ThemePicker'; |
||||||
|
import { trackDashboardSharingActionPerType } from 'app/features/dashboard/components/ShareModal/analytics'; |
||||||
|
import { shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils'; |
||||||
|
|
||||||
|
import { DashboardScene } from '../scene/DashboardScene'; |
||||||
|
import { getDashboardUrl } from '../utils/utils'; |
||||||
|
|
||||||
|
export interface ShareLinkTabState extends SceneObjectState, ShareOptions { |
||||||
|
panelRef?: SceneObjectRef<VizPanel>; |
||||||
|
dashboardRef: SceneObjectRef<DashboardScene>; |
||||||
|
} |
||||||
|
|
||||||
|
interface ShareOptions { |
||||||
|
useLockedTime: boolean; |
||||||
|
useShortUrl: boolean; |
||||||
|
selectedTheme: string; |
||||||
|
shareUrl: string; |
||||||
|
imageUrl: string; |
||||||
|
} |
||||||
|
|
||||||
|
export class ShareLinkTab extends SceneObjectBase<ShareLinkTabState> { |
||||||
|
static Component = ShareLinkTabRenderer; |
||||||
|
|
||||||
|
constructor(state: Omit<ShareLinkTabState, keyof ShareOptions>) { |
||||||
|
super({ |
||||||
|
...state, |
||||||
|
useLockedTime: true, |
||||||
|
useShortUrl: false, |
||||||
|
selectedTheme: 'current', |
||||||
|
shareUrl: '', |
||||||
|
imageUrl: '', |
||||||
|
}); |
||||||
|
|
||||||
|
this.addActivationHandler(() => { |
||||||
|
this.buildUrl(); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
async buildUrl() { |
||||||
|
const { panelRef, dashboardRef, useLockedTime: useAbsoluteTimeRange, useShortUrl, selectedTheme } = this.state; |
||||||
|
const dashboard = dashboardRef.resolve(); |
||||||
|
const panel = panelRef?.resolve(); |
||||||
|
const location = locationService.getLocation(); |
||||||
|
const timeRange = sceneGraph.getTimeRange(panel ?? dashboard); |
||||||
|
|
||||||
|
const urlParamsUpdate: UrlQueryMap = {}; |
||||||
|
|
||||||
|
if (panel) { |
||||||
|
urlParamsUpdate.viewPanel = panel.state.key; |
||||||
|
} |
||||||
|
|
||||||
|
if (useAbsoluteTimeRange) { |
||||||
|
urlParamsUpdate.from = timeRange.state.value.from.toISOString(); |
||||||
|
urlParamsUpdate.to = timeRange.state.value.to.toISOString(); |
||||||
|
} |
||||||
|
|
||||||
|
if (selectedTheme !== 'current') { |
||||||
|
urlParamsUpdate.theme = selectedTheme!; |
||||||
|
} |
||||||
|
|
||||||
|
let shareUrl = getDashboardUrl({ |
||||||
|
uid: dashboard.state.uid, |
||||||
|
currentQueryParams: location.search, |
||||||
|
updateQuery: urlParamsUpdate, |
||||||
|
absolute: true, |
||||||
|
}); |
||||||
|
|
||||||
|
if (useShortUrl) { |
||||||
|
shareUrl = await createShortLink(shareUrl); |
||||||
|
} |
||||||
|
|
||||||
|
const imageUrl = getDashboardUrl({ |
||||||
|
uid: dashboard.state.uid, |
||||||
|
currentQueryParams: location.search, |
||||||
|
updateQuery: urlParamsUpdate, |
||||||
|
absolute: true, |
||||||
|
|
||||||
|
soloRoute: true, |
||||||
|
render: true, |
||||||
|
timeZone: getRenderTimeZone(timeRange.getTimeZone()), |
||||||
|
}); |
||||||
|
|
||||||
|
this.setState({ shareUrl, imageUrl }); |
||||||
|
} |
||||||
|
|
||||||
|
public getTabLabel() { |
||||||
|
return t('share-modal.tab-title.link', 'Link'); |
||||||
|
} |
||||||
|
|
||||||
|
onToggleLockedTime = () => { |
||||||
|
this.setState({ useLockedTime: !this.state.useLockedTime }); |
||||||
|
this.buildUrl(); |
||||||
|
}; |
||||||
|
|
||||||
|
onUrlShorten = () => { |
||||||
|
this.setState({ useShortUrl: !this.state.useShortUrl }); |
||||||
|
this.buildUrl(); |
||||||
|
}; |
||||||
|
|
||||||
|
onThemeChange = (value: string) => { |
||||||
|
this.setState({ selectedTheme: value }); |
||||||
|
this.buildUrl(); |
||||||
|
}; |
||||||
|
|
||||||
|
getShareUrl = () => { |
||||||
|
return this.state.shareUrl; |
||||||
|
}; |
||||||
|
|
||||||
|
onCopy() { |
||||||
|
trackDashboardSharingActionPerType('copy_link', shareDashboardType.link); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function ShareLinkTabRenderer({ model }: SceneComponentProps<ShareLinkTab>) { |
||||||
|
const state = model.useState(); |
||||||
|
const { panelRef, dashboardRef } = state; |
||||||
|
|
||||||
|
const dashboard = dashboardRef.resolve(); |
||||||
|
const panel = panelRef?.resolve(); |
||||||
|
|
||||||
|
const timeRange = sceneGraph.getTimeRange(panel ?? dashboard); |
||||||
|
const isRelativeTime = timeRange.state.to === 'now' ? true : false; |
||||||
|
|
||||||
|
const { useLockedTime, useShortUrl, selectedTheme, shareUrl, imageUrl } = state; |
||||||
|
|
||||||
|
const selectors = e2eSelectors.pages.SharePanelModal; |
||||||
|
const isDashboardSaved = Boolean(dashboard.state.uid); |
||||||
|
|
||||||
|
const lockTimeRangeLabel = t('share-modal.link.time-range-label', `Lock time range`); |
||||||
|
|
||||||
|
const lockTimeRangeDescription = t( |
||||||
|
'share-modal.link.time-range-description', |
||||||
|
`Transforms the current relative time range to an absolute time range` |
||||||
|
); |
||||||
|
|
||||||
|
const shortenURLTranslation = t('share-modal.link.shorten-url', `Shorten URL`); |
||||||
|
|
||||||
|
const linkURLTranslation = t('share-modal.link.link-url', `Link URL`); |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<p className="share-modal-info-text"> |
||||||
|
<Trans i18nKey="share-modal.link.info-text"> |
||||||
|
Create a direct link to this dashboard or panel, customized with the options below. |
||||||
|
</Trans> |
||||||
|
</p> |
||||||
|
<FieldSet> |
||||||
|
<Field label={lockTimeRangeLabel} description={isRelativeTime ? lockTimeRangeDescription : ''}> |
||||||
|
<Switch id="share-current-time-range" value={useLockedTime} onChange={model.onToggleLockedTime} /> |
||||||
|
</Field> |
||||||
|
<ThemePicker selectedTheme={selectedTheme} onChange={model.onThemeChange} /> |
||||||
|
<Field label={shortenURLTranslation}> |
||||||
|
<Switch id="share-shorten-url" value={useShortUrl} onChange={model.onUrlShorten} /> |
||||||
|
</Field> |
||||||
|
|
||||||
|
<Field label={linkURLTranslation}> |
||||||
|
<Input |
||||||
|
id="link-url-input" |
||||||
|
value={shareUrl} |
||||||
|
readOnly |
||||||
|
addonAfter={ |
||||||
|
<ClipboardButton icon="copy" variant="primary" getText={model.getShareUrl} onClipboardCopy={model.onCopy}> |
||||||
|
<Trans i18nKey="share-modal.link.copy-link-button">Copy</Trans> |
||||||
|
</ClipboardButton> |
||||||
|
} |
||||||
|
/> |
||||||
|
</Field> |
||||||
|
</FieldSet> |
||||||
|
|
||||||
|
{panel && config.rendererAvailable && ( |
||||||
|
<> |
||||||
|
{isDashboardSaved && ( |
||||||
|
<div className="gf-form"> |
||||||
|
<a href={imageUrl} target="_blank" rel="noreferrer" aria-label={selectors.linkToRenderedImage}> |
||||||
|
<Icon name="camera" /> |
||||||
|
|
||||||
|
<Trans i18nKey="share-modal.link.rendered-image">Direct link rendered image</Trans> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
|
||||||
|
{!isDashboardSaved && ( |
||||||
|
<Alert severity="info" title={t('share-modal.link.save-alert', 'Dashboard is not saved')} bottomSpacing={0}> |
||||||
|
<Trans i18nKey="share-modal.link.save-dashboard"> |
||||||
|
To render a panel image, you must save the dashboard first. |
||||||
|
</Trans> |
||||||
|
</Alert> |
||||||
|
)} |
||||||
|
</> |
||||||
|
)} |
||||||
|
|
||||||
|
{panel && !config.rendererAvailable && ( |
||||||
|
<Alert |
||||||
|
severity="info" |
||||||
|
title={t('share-modal.link.render-alert', 'Image renderer plugin not installed')} |
||||||
|
bottomSpacing={0} |
||||||
|
> |
||||||
|
<Trans i18nKey="share-modal.link.render-instructions"> |
||||||
|
To render a panel image, you must install the |
||||||
|
<a |
||||||
|
href="https://grafana.com/grafana/plugins/grafana-image-renderer" |
||||||
|
target="_blank" |
||||||
|
rel="noopener noreferrer" |
||||||
|
className="external-link" |
||||||
|
> |
||||||
|
Grafana image renderer plugin |
||||||
|
</a> |
||||||
|
. Please contact your Grafana administrator to install the plugin. |
||||||
|
</Trans> |
||||||
|
</Alert> |
||||||
|
)} |
||||||
|
</> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
function getRenderTimeZone(timeZone: TimeZone): string { |
||||||
|
const utcOffset = 'UTC' + encodeURIComponent(dateTime().format('Z')); |
||||||
|
|
||||||
|
if (timeZone === 'utc') { |
||||||
|
return 'UTC'; |
||||||
|
} |
||||||
|
|
||||||
|
if (timeZone === 'browser') { |
||||||
|
if (!window.Intl) { |
||||||
|
return utcOffset; |
||||||
|
} |
||||||
|
|
||||||
|
const dateFormat = window.Intl.DateTimeFormat(); |
||||||
|
const options = dateFormat.resolvedOptions(); |
||||||
|
if (!options.timeZone) { |
||||||
|
return utcOffset; |
||||||
|
} |
||||||
|
|
||||||
|
return options.timeZone; |
||||||
|
} |
||||||
|
|
||||||
|
return timeZone; |
||||||
|
} |
@ -0,0 +1,117 @@ |
|||||||
|
import React, { ComponentProps } from 'react'; |
||||||
|
|
||||||
|
import { config } from '@grafana/runtime'; |
||||||
|
import { SceneComponentProps, SceneObjectBase, SceneObjectState, VizPanel, SceneObjectRef } from '@grafana/scenes'; |
||||||
|
import { Modal, ModalTabsHeader, TabContent } from '@grafana/ui'; |
||||||
|
import { contextSrv } from 'app/core/core'; |
||||||
|
import { t } from 'app/core/internationalization'; |
||||||
|
|
||||||
|
import { DashboardScene } from '../scene/DashboardScene'; |
||||||
|
import { getDashboardSceneFor } from '../utils/utils'; |
||||||
|
|
||||||
|
import { ShareLinkTab } from './ShareLinkTab'; |
||||||
|
import { ShareSnapshotTab } from './ShareSnapshotTab'; |
||||||
|
import { SceneShareTab } from './types'; |
||||||
|
|
||||||
|
interface ShareModalState extends SceneObjectState { |
||||||
|
dashboardRef: SceneObjectRef<DashboardScene>; |
||||||
|
panelRef?: SceneObjectRef<VizPanel>; |
||||||
|
tabs?: SceneShareTab[]; |
||||||
|
activeTab: string; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Used for full dashboard share modal and the panel level share modal |
||||||
|
*/ |
||||||
|
export class ShareModal extends SceneObjectBase<ShareModalState> { |
||||||
|
static Component = SharePanelModalRenderer; |
||||||
|
|
||||||
|
constructor(state: Omit<ShareModalState, 'activeTab'>) { |
||||||
|
super({ |
||||||
|
...state, |
||||||
|
activeTab: 'Link', |
||||||
|
}); |
||||||
|
|
||||||
|
this.addActivationHandler(() => this.buildTabs()); |
||||||
|
} |
||||||
|
|
||||||
|
private buildTabs() { |
||||||
|
const { dashboardRef, panelRef } = this.state; |
||||||
|
|
||||||
|
const tabs: SceneShareTab[] = [new ShareLinkTab({ dashboardRef, panelRef })]; |
||||||
|
|
||||||
|
if (contextSrv.isSignedIn && config.snapshotEnabled) { |
||||||
|
tabs.push(new ShareSnapshotTab({ panelRef })); |
||||||
|
} |
||||||
|
|
||||||
|
this.setState({ tabs }); |
||||||
|
|
||||||
|
// if (panel) {
|
||||||
|
// const embedLabel = t('share-modal.tab-title.embed', 'Embed');
|
||||||
|
// tabs.push({ label: embedLabel, value: shareDashboardType.embed, component: ShareEmbed });
|
||||||
|
|
||||||
|
// if (!isPanelModelLibraryPanel(panel)) {
|
||||||
|
// const libraryPanelLabel = t('share-modal.tab-title.library-panel', 'Library panel');
|
||||||
|
// tabs.push({ label: libraryPanelLabel, value: shareDashboardType.libraryPanel, component: ShareLibraryPanel });
|
||||||
|
// }
|
||||||
|
// tabs.push(...customPanelTabs);
|
||||||
|
// } else {
|
||||||
|
// const exportLabel = t('share-modal.tab-title.export', 'Export');
|
||||||
|
// tabs.push({
|
||||||
|
// label: exportLabel,
|
||||||
|
// value: shareDashboardType.export,
|
||||||
|
// component: ShareExport,
|
||||||
|
// });
|
||||||
|
// tabs.push(...customDashboardTabs);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (Boolean(config.featureToggles['publicDashboards'])) {
|
||||||
|
// tabs.push({
|
||||||
|
// label: 'Public dashboard',
|
||||||
|
// value: shareDashboardType.publicDashboard,
|
||||||
|
// component: SharePublicDashboard,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
} |
||||||
|
|
||||||
|
onClose = () => { |
||||||
|
const dashboard = getDashboardSceneFor(this); |
||||||
|
dashboard.closeModal(); |
||||||
|
}; |
||||||
|
|
||||||
|
onChangeTab: ComponentProps<typeof ModalTabsHeader>['onChangeTab'] = (tab) => { |
||||||
|
this.setState({ activeTab: tab.value }); |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function SharePanelModalRenderer({ model }: SceneComponentProps<ShareModal>) { |
||||||
|
const { panelRef, tabs, activeTab } = model.useState(); |
||||||
|
const title = panelRef ? t('share-modal.panel.title', 'Share Panel') : t('share-modal.dashboard.title', 'Share'); |
||||||
|
|
||||||
|
if (!tabs) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const modalTabs = tabs?.map((tab) => ({ |
||||||
|
label: tab.getTabLabel(), |
||||||
|
value: tab.getTabLabel(), |
||||||
|
})); |
||||||
|
|
||||||
|
const header = ( |
||||||
|
<ModalTabsHeader |
||||||
|
title={title} |
||||||
|
icon="share-alt" |
||||||
|
tabs={modalTabs} |
||||||
|
activeTab={activeTab} |
||||||
|
onChangeTab={model.onChangeTab} |
||||||
|
/> |
||||||
|
); |
||||||
|
|
||||||
|
const currentTab = tabs.find((t) => t.getTabLabel() === activeTab); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Modal isOpen={true} title={header} onDismiss={model.onClose}> |
||||||
|
<TabContent>{currentTab && <currentTab.Component model={currentTab} />}</TabContent> |
||||||
|
</Modal> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
import React from 'react'; |
||||||
|
|
||||||
|
import { SceneComponentProps, SceneObjectBase, SceneObjectState, SceneObjectRef, VizPanel } from '@grafana/scenes'; |
||||||
|
import { t } from 'app/core/internationalization'; |
||||||
|
|
||||||
|
export interface ShareSnapshotTabState extends SceneObjectState { |
||||||
|
panelRef?: SceneObjectRef<VizPanel>; |
||||||
|
} |
||||||
|
|
||||||
|
export class ShareSnapshotTab extends SceneObjectBase<ShareSnapshotTabState> { |
||||||
|
public getTabLabel() { |
||||||
|
return t('share-modal.tab-title.snapshot', 'Snapshot'); |
||||||
|
} |
||||||
|
|
||||||
|
static Component = ({ model }: SceneComponentProps<ShareSnapshotTab>) => { |
||||||
|
return <div>Snapshot</div>; |
||||||
|
}; |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
import { SceneObject, SceneObjectState } from '@grafana/scenes'; |
||||||
|
|
||||||
|
export interface SceneShareTab<T extends SceneObjectState = SceneObjectState> extends SceneObject<T> { |
||||||
|
getTabLabel(): string; |
||||||
|
} |
Loading…
Reference in new issue