ShareDrawer: Share Internally (#89315)

This commit is contained in:
Juan Cabanas
2024-06-24 16:31:42 -03:00
committed by GitHub
parent ca1afff886
commit d07dc3bf40
24 changed files with 293 additions and 103 deletions

View File

@@ -3082,9 +3082,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not re-export imported variable (\`./VersionHistoryButtons\`)", "3"], [0, 0, 0, "Do not re-export imported variable (\`./VersionHistoryButtons\`)", "3"],
[0, 0, 0, "Do not re-export imported variable (\`./VersionHistoryComparison\`)", "4"] [0, 0, 0, "Do not re-export imported variable (\`./VersionHistoryComparison\`)", "4"]
], ],
"public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
],
"public/app/features/dashboard-scene/sharing/ShareExportTab.tsx:5381": [ "public/app/features/dashboard-scene/sharing/ShareExportTab.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"] [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
], ],

View File

@@ -9,6 +9,9 @@ import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScen
import { getDashboardUrl } from 'app/features/dashboard-scene/utils/urlBuilders'; import { getDashboardUrl } from 'app/features/dashboard-scene/utils/urlBuilders';
import { dispatch } from 'app/store/store'; import { dispatch } from 'app/store/store';
import { ShareLinkConfiguration } from '../../features/dashboard-scene/sharing/ShareButton/utils';
import { t } from '../internationalization';
import { copyStringToClipboard } from './explore'; import { copyStringToClipboard } from './explore';
function buildHostUrl() { function buildHostUrl() {
@@ -42,20 +45,21 @@ export const createAndCopyShortLink = async (path: string) => {
} }
}; };
export const createAndCopyDashboardShortLink = async ( export const createAndCopyShareDashboardLink = async (
dashboard: DashboardScene, dashboard: DashboardScene,
opts: { useAbsoluteTimeRange: boolean; theme: string }, opts: ShareLinkConfiguration,
panel?: VizPanel panel?: VizPanel
) => { ) => {
const shareUrl = await createDashboardShareUrl(dashboard, opts, panel); const shareUrl = createDashboardShareUrl(dashboard, opts, panel);
await createAndCopyShortLink(shareUrl); if (opts.useShortUrl) {
return await createAndCopyShortLink(shareUrl);
} else {
copyStringToClipboard(shareUrl);
dispatch(notifyApp(createSuccessNotification(t('link.share.copy-to-clipboard', 'Link copied to clipboard'))));
}
}; };
export const createDashboardShareUrl = async ( export const createDashboardShareUrl = (dashboard: DashboardScene, opts: ShareLinkConfiguration, panel?: VizPanel) => {
dashboard: DashboardScene,
opts: { useAbsoluteTimeRange: boolean; theme: string },
panel?: VizPanel
) => {
const location = locationService.getLocation(); const location = locationService.getLocation();
const timeRange = sceneGraph.getTimeRange(panel ?? dashboard); const timeRange = sceneGraph.getTimeRange(panel ?? dashboard);

View File

@@ -321,7 +321,7 @@ export function ToolbarActions({ dashboard }: Props) {
fill="outline" fill="outline"
onClick={() => { onClick={() => {
DashboardInteractions.toolbarShareClick(); DashboardInteractions.toolbarShareClick();
dashboard.showModal(new ShareModal({ dashboardRef: dashboard.getRef() })); dashboard.showModal(new ShareModal({}));
}} }}
data-testid={selectors.components.NavToolbar.shareDashboard} data-testid={selectors.components.NavToolbar.shareDashboard}
> >

View File

@@ -84,7 +84,7 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) {
iconClassName: 'share-alt', iconClassName: 'share-alt',
onClick: () => { onClick: () => {
DashboardInteractions.panelMenuItemClicked('share'); DashboardInteractions.panelMenuItemClicked('share');
dashboard.showModal(new ShareModal({ panelRef: panel.getRef(), dashboardRef: dashboard.getRef() })); dashboard.showModal(new ShareModal({ panelRef: panel.getRef() }));
}, },
shortcut: 'p s', shortcut: 'p s',
}); });
@@ -139,7 +139,6 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) {
dashboard.showModal( dashboard.showModal(
new ShareModal({ new ShareModal({
panelRef: panel.getRef(), panelRef: panel.getRef(),
dashboardRef: dashboard.getRef(),
activeTab: shareDashboardType.libraryPanel, activeTab: shareDashboardType.libraryPanel,
}) })
); );

View File

@@ -60,7 +60,7 @@ export function setupKeyboardShortcuts(scene: DashboardScene) {
keybindings.addBinding({ keybindings.addBinding({
key: 'p s', key: 'p s',
onTrigger: withFocusedPanel(scene, async (vizPanel: VizPanel) => { onTrigger: withFocusedPanel(scene, async (vizPanel: VizPanel) => {
scene.showModal(new ShareModal({ panelRef: vizPanel.getRef(), dashboardRef: scene.getRef() })); scene.showModal(new ShareModal({ panelRef: vizPanel.getRef() }));
}), }),
}); });

View File

@@ -24,16 +24,6 @@ describe('ShareButton', () => {
expect(await screen.findByTestId(selector.shareLink)).toBeInTheDocument(); expect(await screen.findByTestId(selector.shareLink)).toBeInTheDocument();
expect(await screen.findByTestId(selector.arrowMenu)).toBeInTheDocument(); expect(await screen.findByTestId(selector.arrowMenu)).toBeInTheDocument();
}); });
it('should call createAndCopyDashboardShortLink when share link clicked', async () => {
setup();
const shareLink = await screen.findByTestId(selector.shareLink);
await userEvent.click(shareLink);
expect(createAndCopyDashboardShortLinkMock).toHaveBeenCalled();
});
it('should render menu when arrow button clicked', async () => { it('should render menu when arrow button clicked', async () => {
setup(); setup();

View File

@@ -4,6 +4,7 @@ import { useAsyncFn } from 'react-use';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { VizPanel } from '@grafana/scenes'; import { VizPanel } from '@grafana/scenes';
import { Button, ButtonGroup, Dropdown } from '@grafana/ui'; import { Button, ButtonGroup, Dropdown } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { DashboardScene } from '../../scene/DashboardScene'; import { DashboardScene } from '../../scene/DashboardScene';
import { DashboardInteractions } from '../../utils/interactions'; import { DashboardInteractions } from '../../utils/interactions';
@@ -32,8 +33,13 @@ export default function ShareButton({ dashboard, panel }: { dashboard: Dashboard
return ( return (
<ButtonGroup data-testid={newShareButtonSelector.container}> <ButtonGroup data-testid={newShareButtonSelector.container}>
<Button data-testid={newShareButtonSelector.shareLink} size="sm" tooltip="Copy shortened URL" onClick={buildUrl}> <Button
Share data-testid={newShareButtonSelector.shareLink}
size="sm"
tooltip={t('share-dashboard.share-button-tooltip', 'Copy shortened link')}
onClick={buildUrl}
>
<Trans i18nKey="share-dashboard.share-button">Share</Trans>
</Button> </Button>
<Dropdown overlay={MenuActions} placement="bottom-end" onVisibleChange={onMenuClick}> <Dropdown overlay={MenuActions} placement="bottom-end" onVisibleChange={onMenuClick}>
<Button data-testid={newShareButtonSelector.arrowMenu} size="sm" icon={isOpen ? 'angle-up' : 'angle-down'} /> <Button data-testid={newShareButtonSelector.arrowMenu} size="sm" icon={isOpen ? 'angle-up' : 'angle-down'} />

View File

@@ -1,5 +1,4 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react'; import React from 'react';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
@@ -35,14 +34,6 @@ describe('ShareMenu', () => {
expect(await screen.queryByTestId(selector.shareExternally)).not.toBeInTheDocument(); expect(await screen.queryByTestId(selector.shareExternally)).not.toBeInTheDocument();
}); });
it('should call createAndCopyDashboardShortLink when share internally clicked', async () => {
setup();
const shareLink = await screen.findByTestId(selector.shareInternally);
await userEvent.click(shareLink);
expect(createAndCopyDashboardShortLinkMock).toHaveBeenCalled();
});
}); });
function setup() { function setup() {

View File

@@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import { useAsyncFn } from 'react-use';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { VizPanel } from '@grafana/scenes'; import { VizPanel } from '@grafana/scenes';
@@ -11,15 +10,20 @@ import { DashboardScene } from '../../scene/DashboardScene';
import { ShareDrawer } from '../ShareDrawer/ShareDrawer'; import { ShareDrawer } from '../ShareDrawer/ShareDrawer';
import { ShareExternally } from './share-externally/ShareExternally'; import { ShareExternally } from './share-externally/ShareExternally';
import { ShareInternally } from './share-internally/ShareInternally';
import { ShareSnapshot } from './share-snapshot/ShareSnapshot'; import { ShareSnapshot } from './share-snapshot/ShareSnapshot';
import { buildShareUrl } from './utils';
const newShareButtonSelector = e2eSelectors.pages.Dashboard.DashNav.newShareButton.menu; const newShareButtonSelector = e2eSelectors.pages.Dashboard.DashNav.newShareButton.menu;
export default function ShareMenu({ dashboard, panel }: { dashboard: DashboardScene; panel?: VizPanel }) { export default function ShareMenu({ dashboard, panel }: { dashboard: DashboardScene; panel?: VizPanel }) {
const [_, buildUrl] = useAsyncFn(async () => { const onShareInternallyClick = () => {
return await buildShareUrl(dashboard, panel); const drawer = new ShareDrawer({
}, [dashboard]); title: t('share-dashboard.menu.share-internally-title', 'Share internally'),
body: new ShareInternally({ panelRef: panel?.getRef() }),
});
dashboard.showModal(drawer);
};
const onShareExternallyClick = () => { const onShareExternallyClick = () => {
const drawer = new ShareDrawer({ const drawer = new ShareDrawer({
@@ -46,7 +50,7 @@ export default function ShareMenu({ dashboard, panel }: { dashboard: DashboardSc
label={t('share-dashboard.menu.share-internally-title', 'Share internally')} label={t('share-dashboard.menu.share-internally-title', 'Share internally')}
description={t('share-dashboard.menu.share-internally-description', 'Advanced settings')} description={t('share-dashboard.menu.share-internally-description', 'Advanced settings')}
icon="building" icon="building"
onClick={buildUrl} onClick={onShareInternallyClick}
/> />
{isPublicDashboardsEnabled() && ( {isPublicDashboardsEnabled() && (
<Menu.Item <Menu.Item

View File

@@ -0,0 +1,104 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneComponentProps, SceneObjectRef, VizPanel } from '@grafana/scenes';
import { Alert, ClipboardButton, Divider, Label, Spinner, Stack, Switch, Text, useStyles2 } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { ThemePicker } from 'app/features/dashboard/components/ShareModal/ThemePicker';
import { ShareLinkTab } from '../../ShareLinkTab';
import { getShareLinkConfiguration } from '../utils';
export class ShareInternally extends ShareLinkTab {
static Component = ShareInternallyRenderer;
constructor(state: { panelRef?: SceneObjectRef<VizPanel> }) {
const { useAbsoluteTimeRange, useShortUrl, theme } = getShareLinkConfiguration();
super({
...state,
useLockedTime: useAbsoluteTimeRange,
useShortUrl,
selectedTheme: theme,
});
}
}
function ShareInternallyRenderer({ model }: SceneComponentProps<ShareInternally>) {
const styles = useStyles2(getStyles);
const { useLockedTime, useShortUrl, selectedTheme, isBuildUrlLoading } = model.useState();
return (
<>
<Alert severity="info" title={t('link.share.config-alert-title', 'Link configuration')}>
<Trans i18nKey="link.share.config-alert-description">
Updating your settings will modify the default copy link to include these changes.
</Trans>
</Alert>
<div className={styles.configDescription}>
<Text variant="body">
<Trans i18nKey="link.share.config-description">
Create a personalized, direct link to share your dashboard within your organization, with the following
customization settings:
</Trans>
</Text>
</div>
<Stack justifyContent="space-between">
<Stack gap={2} direction="column">
<Stack gap={1} direction="column">
<Stack gap={1} alignItems="start">
<Switch
label={t('link.share.time-range-label', 'Lock time range')}
id="share-current-time-range"
value={useLockedTime}
onChange={model.onToggleLockedTime}
/>
<Label
description={t(
'link.share.time-range-description',
'Change the current relative time range to an absolute time range'
)}
>
<Trans i18nKey="link.share.time-range-label">Lock time range</Trans>
</Label>
</Stack>
<Stack gap={1} alignItems="start">
<Switch
id="share-short-url"
value={useShortUrl}
label={t('link.share.short-url-label', 'Shorten link')}
onChange={model.onUrlShorten}
/>
<Label>
<Trans i18nKey="link.share.short-url-label">Shorten link</Trans>
</Label>
</Stack>
</Stack>
<ThemePicker selectedTheme={selectedTheme} onChange={model.onThemeChange} />
</Stack>
{isBuildUrlLoading && <Spinner />}
</Stack>
<Divider spacing={1} />
<ClipboardButton
icon="link"
variant="primary"
fill="outline"
disabled={isBuildUrlLoading}
getText={model.getShareUrl}
onClipboardCopy={model.onCopy}
className={styles.copyButtonContainer}
>
<Trans i18nKey="link.share.copy-link-button">Copy link</Trans>
</ClipboardButton>
</>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
configDescription: css({
marginBottom: theme.spacing(2),
}),
copyButtonContainer: css({
marginTop: theme.spacing(2),
}),
});

View File

@@ -1,16 +1,48 @@
import { VizPanel } from '@grafana/scenes'; import { VizPanel } from '@grafana/scenes';
import { createAndCopyDashboardShortLink } from 'app/core/utils/shortLinks'; import { createAndCopyShareDashboardLink } from 'app/core/utils/shortLinks';
import { getTrackingSource } from 'app/features/dashboard/components/ShareModal/utils'; import { getTrackingSource } from 'app/features/dashboard/components/ShareModal/utils';
import store from '../../../../core/store';
import { DashboardScene } from '../../scene/DashboardScene'; import { DashboardScene } from '../../scene/DashboardScene';
import { DashboardInteractions } from '../../utils/interactions'; import { DashboardInteractions } from '../../utils/interactions';
export type ShareLinkConfiguration = {
useAbsoluteTimeRange: boolean;
useShortUrl: boolean;
theme: string;
};
const DEFAULT_SHARE_LINK_CONFIGURATION: ShareLinkConfiguration = {
useAbsoluteTimeRange: true,
useShortUrl: true,
theme: 'current',
};
export const buildShareUrl = async (dashboard: DashboardScene, panel?: VizPanel) => { export const buildShareUrl = async (dashboard: DashboardScene, panel?: VizPanel) => {
const { useAbsoluteTimeRange, useShortUrl, theme } = getShareLinkConfiguration();
DashboardInteractions.shareLinkCopied({ DashboardInteractions.shareLinkCopied({
currentTimeRange: true, currentTimeRange: useAbsoluteTimeRange,
theme: 'current', theme,
shortenURL: true, shortenURL: useShortUrl,
shareResource: getTrackingSource(panel?.getRef()), shareResource: getTrackingSource(panel?.getRef()),
}); });
return await createAndCopyDashboardShortLink(dashboard, { useAbsoluteTimeRange: true, theme: 'current' }); return await createAndCopyShareDashboardLink(dashboard, {
useAbsoluteTimeRange,
theme,
useShortUrl,
});
}; };
const SHARE_LINK_CONFIGURATION = 'grafana.dashboard.link.shareConfiguration';
// Function that returns share link configuration from local storage
export function getShareLinkConfiguration(): ShareLinkConfiguration {
if (store.exists(SHARE_LINK_CONFIGURATION)) {
return store.getObject(SHARE_LINK_CONFIGURATION) || DEFAULT_SHARE_LINK_CONFIGURATION;
}
return DEFAULT_SHARE_LINK_CONFIGURATION;
}
export function updateShareLinkConfiguration(config: ShareLinkConfiguration) {
store.setObject(SHARE_LINK_CONFIGURATION, config);
}

View File

@@ -13,6 +13,7 @@ import { DashboardModel } from 'app/features/dashboard/state';
import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel'; import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
import { getVariablesCompatibility } from '../utils/getVariablesCompatibility'; import { getVariablesCompatibility } from '../utils/getVariablesCompatibility';
import { DashboardInteractions } from '../utils/interactions'; import { DashboardInteractions } from '../utils/interactions';
import { getDashboardSceneFor } from '../utils/utils';
import { SceneShareTabState } from './types'; import { SceneShareTabState } from './types';
@@ -56,8 +57,8 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> {
} }
public async getExportableDashboardJson() { public async getExportableDashboardJson() {
const { dashboardRef, isSharingExternally } = this.state; const { isSharingExternally } = this.state;
const saveModel = transformSceneToSaveModel(dashboardRef.resolve()); const saveModel = transformSceneToSaveModel(getDashboardSceneFor(this));
const exportable = isSharingExternally const exportable = isSharingExternally
? await this._exporter.makeExportable( ? await this._exporter.makeExportable(

View File

@@ -9,6 +9,7 @@ import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardGridItem } from '../scene/DashboardGridItem';
import { gridItemToPanel, transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel'; import { gridItemToPanel, transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
import { getDashboardSceneFor } from '../utils/utils';
import { SceneShareTabState } from './types'; import { SceneShareTabState } from './types';
@@ -26,7 +27,7 @@ export class ShareLibraryPanelTab extends SceneObjectBase<ShareLibraryPanelTabSt
} }
function ShareLibraryPanelTabRenderer({ model }: SceneComponentProps<ShareLibraryPanelTab>) { function ShareLibraryPanelTabRenderer({ model }: SceneComponentProps<ShareLibraryPanelTab>) {
const { panelRef, dashboardRef, modalRef } = model.useState(); const { panelRef, modalRef } = model.useState();
if (!panelRef) { if (!panelRef) {
return null; return null;
@@ -36,7 +37,7 @@ function ShareLibraryPanelTabRenderer({ model }: SceneComponentProps<ShareLibrar
const parent = panel.parent; const parent = panel.parent;
if (parent instanceof DashboardGridItem) { if (parent instanceof DashboardGridItem) {
const dashboardScene = dashboardRef.resolve(); const dashboardScene = getDashboardSceneFor(model);
const panelJson = gridItemToPanel(parent); const panelJson = gridItemToPanel(parent);
const panelModel = new PanelModel(panelJson); const panelModel = new PanelModel(panelJson);

View File

@@ -4,12 +4,14 @@ import { advanceTo, clear } from 'jest-date-mock';
import React from 'react'; import React from 'react';
import { dateTime } from '@grafana/data'; import { dateTime } from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { config, locationService } from '@grafana/runtime'; import { config, locationService, setPluginImportUtils } from '@grafana/runtime';
import { SceneGridLayout, SceneTimeRange, VizPanel } from '@grafana/scenes'; import { SceneGridLayout, SceneTimeRange, VizPanel } from '@grafana/scenes';
import { DashboardGridItem } from '../scene/DashboardGridItem'; import { DashboardGridItem } from '../scene/DashboardGridItem';
import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScene } from '../scene/DashboardScene';
import { activateFullSceneTree } from '../utils/test-utils';
import { ShareLinkTab } from './ShareLinkTab'; import { ShareLinkTab } from './ShareLinkTab';
@@ -18,6 +20,11 @@ jest.mock('app/core/utils/shortLinks', () => ({
createShortLink: jest.fn().mockResolvedValue(`http://localhost:3000/goto/shortend-uid`), createShortLink: jest.fn().mockResolvedValue(`http://localhost:3000/goto/shortend-uid`),
})); }));
setPluginImportUtils({
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
getPanelPluginFromCache: (id: string) => undefined,
});
describe('ShareLinkTab', () => { describe('ShareLinkTab', () => {
const fakeCurrentDate = dateTime('2019-02-11T19:00:00.000Z').toDate(); const fakeCurrentDate = dateTime('2019-02-11T19:00:00.000Z').toDate();
@@ -48,7 +55,7 @@ describe('ShareLinkTab', () => {
describe('with disabled locked range range', () => { describe('with disabled locked range range', () => {
it('should generate share url with relative time', async () => { it('should generate share url with relative time', async () => {
const tab = buildAndRenderScenario({}); const tab = buildAndRenderScenario({});
act(() => tab.onToggleLockedTime()); await act(() => tab.onToggleLockedTime());
expect(await screen.findByRole('textbox', { name: 'Link URL' })).toHaveValue( expect(await screen.findByRole('textbox', { name: 'Link URL' })).toHaveValue(
'http://dashboards.grafana.com/grafana/d/dash-1?from=now-6h&to=now&viewPanel=panel-12' 'http://dashboards.grafana.com/grafana/d/dash-1?from=now-6h&to=now&viewPanel=panel-12'
@@ -58,7 +65,7 @@ describe('ShareLinkTab', () => {
it('should add theme when specified', async () => { it('should add theme when specified', async () => {
const tab = buildAndRenderScenario({}); const tab = buildAndRenderScenario({});
act(() => tab.onThemeChange('light')); await act(() => tab.onThemeChange('light'));
expect(await screen.findByRole('textbox', { name: 'Link URL' })).toHaveValue( expect(await screen.findByRole('textbox', { name: 'Link URL' })).toHaveValue(
'http://dashboards.grafana.com/grafana/d/dash-1?from=2019-02-11T13:00:00.000Z&to=2019-02-11T19:00:00.000Z&viewPanel=panel-12&theme=light' 'http://dashboards.grafana.com/grafana/d/dash-1?from=2019-02-11T13:00:00.000Z&to=2019-02-11T19:00:00.000Z&viewPanel=panel-12&theme=light'
@@ -97,8 +104,8 @@ function buildAndRenderScenario(options: ScenarioOptions) {
pluginId: 'table', pluginId: 'table',
key: 'panel-12', key: 'panel-12',
}); });
const tab = new ShareLinkTab({ panelRef: panel.getRef() });
const dashboard = new DashboardScene({ const scene = new DashboardScene({
title: 'hello', title: 'hello',
uid: 'dash-1', uid: 'dash-1',
meta: { meta: {
@@ -117,9 +124,10 @@ function buildAndRenderScenario(options: ScenarioOptions) {
}), }),
], ],
}), }),
overlay: tab,
}); });
const tab = new ShareLinkTab({ dashboardRef: dashboard.getRef(), panelRef: panel.getRef() }); activateFullSceneTree(scene);
render(<tab.Component model={tab} />); render(<tab.Component model={tab} />);

View File

@@ -13,18 +13,24 @@ import { getTrackingSource, shareDashboardType } from 'app/features/dashboard/co
import { DashboardInteractions } from '../utils/interactions'; import { DashboardInteractions } from '../utils/interactions';
import { getDashboardUrl } from '../utils/urlBuilders'; import { getDashboardUrl } from '../utils/urlBuilders';
import { getDashboardSceneFor } from '../utils/utils';
import { updateShareLinkConfiguration } from './ShareButton/utils';
import { SceneShareTabState } from './types'; import { SceneShareTabState } from './types';
export interface ShareLinkTabState extends SceneShareTabState, ShareOptions { export interface ShareLinkTabState extends SceneShareTabState, ShareOptions {
panelRef?: SceneObjectRef<VizPanel>; panelRef?: SceneObjectRef<VizPanel>;
} }
interface ShareOptions { export interface ShareLinkConfiguration {
useLockedTime: boolean; useLockedTime: boolean;
useShortUrl: boolean; useShortUrl: boolean;
selectedTheme: string; selectedTheme: string;
}
interface ShareOptions extends ShareLinkConfiguration {
shareUrl: string; shareUrl: string;
imageUrl: string; imageUrl: string;
isBuildUrlLoading: boolean;
} }
export class ShareLinkTab extends SceneObjectBase<ShareLinkTabState> { export class ShareLinkTab extends SceneObjectBase<ShareLinkTabState> {
@@ -32,14 +38,15 @@ export class ShareLinkTab extends SceneObjectBase<ShareLinkTabState> {
static Component = ShareLinkTabRenderer; static Component = ShareLinkTabRenderer;
constructor(state: Omit<ShareLinkTabState, keyof ShareOptions>) { constructor(state: Partial<ShareLinkTabState>) {
super({ super({
...state, ...state,
useLockedTime: true, useLockedTime: state.useLockedTime ?? true,
useShortUrl: false, useShortUrl: state.useShortUrl ?? false,
selectedTheme: 'current', selectedTheme: state.selectedTheme ?? 'current',
shareUrl: '', shareUrl: '',
imageUrl: '', imageUrl: '',
isBuildUrlLoading: false,
}); });
this.addActivationHandler(() => { this.addActivationHandler(() => {
@@ -48,12 +55,13 @@ export class ShareLinkTab extends SceneObjectBase<ShareLinkTabState> {
} }
async buildUrl() { async buildUrl() {
const { panelRef, dashboardRef, useLockedTime: useAbsoluteTimeRange, useShortUrl, selectedTheme } = this.state; this.setState({ isBuildUrlLoading: true });
const dashboard = dashboardRef.resolve(); const { panelRef, useLockedTime: useAbsoluteTimeRange, useShortUrl, selectedTheme } = this.state;
const dashboard = getDashboardSceneFor(this);
const panel = panelRef?.resolve(); const panel = panelRef?.resolve();
const opts = { useAbsoluteTimeRange, theme: selectedTheme }; const opts = { useAbsoluteTimeRange, theme: selectedTheme, useShortUrl };
let shareUrl = await createDashboardShareUrl(dashboard, opts, panel); let shareUrl = createDashboardShareUrl(dashboard, opts, panel);
if (useShortUrl) { if (useShortUrl) {
shareUrl = await createShortLink(shareUrl); shareUrl = await createShortLink(shareUrl);
@@ -81,26 +89,43 @@ export class ShareLinkTab extends SceneObjectBase<ShareLinkTabState> {
timeZone: getRenderTimeZone(timeRange.getTimeZone()), timeZone: getRenderTimeZone(timeRange.getTimeZone()),
}); });
this.setState({ shareUrl, imageUrl }); this.setState({ shareUrl, imageUrl, isBuildUrlLoading: false });
} }
public getTabLabel() { public getTabLabel() {
return t('share-modal.tab-title.link', 'Link'); return t('share-modal.tab-title.link', 'Link');
} }
onToggleLockedTime = () => { onToggleLockedTime = async () => {
this.setState({ useLockedTime: !this.state.useLockedTime }); const useLockedTime = !this.state.useLockedTime;
this.buildUrl(); updateShareLinkConfiguration({
useAbsoluteTimeRange: useLockedTime,
useShortUrl: this.state.useShortUrl,
theme: this.state.selectedTheme,
});
this.setState({ useLockedTime });
await this.buildUrl();
}; };
onUrlShorten = () => { onUrlShorten = async () => {
this.setState({ useShortUrl: !this.state.useShortUrl }); const useShortUrl = !this.state.useShortUrl;
this.buildUrl(); this.setState({ useShortUrl });
updateShareLinkConfiguration({
useShortUrl,
useAbsoluteTimeRange: this.state.useLockedTime,
theme: this.state.selectedTheme,
});
await this.buildUrl();
}; };
onThemeChange = (value: string) => { onThemeChange = async (value: string) => {
this.setState({ selectedTheme: value }); this.setState({ selectedTheme: value });
this.buildUrl(); updateShareLinkConfiguration({
theme: value,
useShortUrl: this.state.useShortUrl,
useAbsoluteTimeRange: this.state.useLockedTime,
});
await this.buildUrl();
}; };
getShareUrl = () => { getShareUrl = () => {
@@ -119,9 +144,9 @@ export class ShareLinkTab extends SceneObjectBase<ShareLinkTabState> {
function ShareLinkTabRenderer({ model }: SceneComponentProps<ShareLinkTab>) { function ShareLinkTabRenderer({ model }: SceneComponentProps<ShareLinkTab>) {
const state = model.useState(); const state = model.useState();
const { panelRef, dashboardRef } = state; const { panelRef } = state;
const dashboard = dashboardRef.resolve(); const dashboard = getDashboardSceneFor(model);
const panel = panelRef?.resolve(); const panel = panelRef?.resolve();
const timeRange = sceneGraph.getTimeRange(panel ?? dashboard); const timeRange = sceneGraph.getTimeRange(panel ?? dashboard);

View File

@@ -8,7 +8,6 @@ import { t } from 'app/core/internationalization';
import { isPublicDashboardsEnabled } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; import { isPublicDashboardsEnabled } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
import { getTrackingSource } from '../../dashboard/components/ShareModal/utils'; import { getTrackingSource } from '../../dashboard/components/ShareModal/utils';
import { DashboardScene } from '../scene/DashboardScene';
import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { DashboardInteractions } from '../utils/interactions'; import { DashboardInteractions } from '../utils/interactions';
import { getDashboardSceneFor } from '../utils/utils'; import { getDashboardSceneFor } from '../utils/utils';
@@ -22,7 +21,6 @@ import { SharePublicDashboardTab } from './public-dashboards/SharePublicDashboar
import { ModalSceneObjectLike, SceneShareTab, SceneShareTabState } from './types'; import { ModalSceneObjectLike, SceneShareTab, SceneShareTabState } from './types';
interface ShareModalState extends SceneObjectState { interface ShareModalState extends SceneObjectState {
dashboardRef: SceneObjectRef<DashboardScene>;
panelRef?: SceneObjectRef<VizPanel>; panelRef?: SceneObjectRef<VizPanel>;
tabs?: SceneShareTab[]; tabs?: SceneShareTab[];
activeTab: string; activeTab: string;
@@ -51,36 +49,36 @@ export class ShareModal extends SceneObjectBase<ShareModalState> implements Moda
} }
private buildTabs() { private buildTabs() {
const { dashboardRef, panelRef } = this.state; const { panelRef } = this.state;
const modalRef = this.getRef(); const modalRef = this.getRef();
const tabs: SceneShareTab[] = [new ShareLinkTab({ dashboardRef, panelRef, modalRef })]; const tabs: SceneShareTab[] = [new ShareLinkTab({ panelRef, modalRef })];
const dashboard = getDashboardSceneFor(this); const dashboard = getDashboardSceneFor(this);
if (!panelRef) { if (!panelRef) {
tabs.push(new ShareExportTab({ dashboardRef, modalRef })); tabs.push(new ShareExportTab({ modalRef }));
} }
if (contextSrv.isSignedIn && config.snapshotEnabled && dashboard.canEditDashboard()) { if (contextSrv.isSignedIn && config.snapshotEnabled && dashboard.canEditDashboard()) {
tabs.push(new ShareSnapshotTab({ panelRef, dashboardRef, modalRef })); tabs.push(new ShareSnapshotTab({ panelRef, dashboardRef: dashboard.getRef(), modalRef }));
} }
if (panelRef) { if (panelRef) {
tabs.push(new SharePanelEmbedTab({ panelRef, dashboardRef })); tabs.push(new SharePanelEmbedTab({ panelRef }));
const panel = panelRef.resolve(); const panel = panelRef.resolve();
const isLibraryPanel = panel.parent instanceof LibraryVizPanel; const isLibraryPanel = panel.parent instanceof LibraryVizPanel;
if (panel instanceof VizPanel) { if (panel instanceof VizPanel) {
if (!isLibraryPanel) { if (!isLibraryPanel) {
tabs.push(new ShareLibraryPanelTab({ panelRef, dashboardRef, modalRef })); tabs.push(new ShareLibraryPanelTab({ panelRef, modalRef }));
} }
} }
} }
if (!panelRef) { if (!panelRef) {
tabs.push(...customDashboardTabs.map((Tab) => new Tab({ dashboardRef, modalRef }))); tabs.push(...customDashboardTabs.map((Tab) => new Tab({ modalRef })));
if (isPublicDashboardsEnabled()) { if (isPublicDashboardsEnabled()) {
tabs.push(new SharePublicDashboardTab({ dashboardRef, modalRef })); tabs.push(new SharePublicDashboardTab({ modalRef }));
} }
} }

View File

@@ -9,7 +9,7 @@ import { buildParams, shareDashboardType } from 'app/features/dashboard/componen
import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScene } from '../scene/DashboardScene';
import { PanelTimeRange } from '../scene/PanelTimeRange'; import { PanelTimeRange } from '../scene/PanelTimeRange';
import { getDashboardUrl } from '../utils/urlBuilders'; import { getDashboardUrl } from '../utils/urlBuilders';
import { getPanelIdForVizPanel } from '../utils/utils'; import { getDashboardSceneFor, getPanelIdForVizPanel } from '../utils/utils';
import { SceneShareTabState } from './types'; import { SceneShareTabState } from './types';
@@ -31,10 +31,10 @@ export class SharePanelEmbedTab extends SceneObjectBase<SharePanelEmbedTabState>
} }
function SharePanelEmbedTabRenderer({ model }: SceneComponentProps<SharePanelEmbedTab>) { function SharePanelEmbedTabRenderer({ model }: SceneComponentProps<SharePanelEmbedTab>) {
const { panelRef, dashboardRef } = model.useState(); const { panelRef } = model.useState();
const p = panelRef.resolve(); const p = panelRef.resolve();
const dash = dashboardRef.resolve(); const dash = getDashboardSceneFor(model);
const { uid: dashUid } = dash.useState(); const { uid: dashUid } = dash.useState();
const id = getPanelIdForVizPanel(p); const id = getPanelIdForVizPanel(p);
const timeRangeState = sceneGraph.getTimeRange(p); const timeRangeState = sceneGraph.getTimeRange(p);

View File

@@ -13,6 +13,7 @@ import { getTrackingSource, shareDashboardType } from 'app/features/dashboard/co
import { getDashboardSnapshotSrv, SnapshotSharingOptions } from 'app/features/dashboard/services/SnapshotSrv'; import { getDashboardSnapshotSrv, SnapshotSharingOptions } from 'app/features/dashboard/services/SnapshotSrv';
import { dispatch } from 'app/store/store'; import { dispatch } from 'app/store/store';
import { DashboardScene } from '../scene/DashboardScene';
import { transformSceneToSaveModel, trimDashboardForSnapshot } from '../serialization/transformSceneToSaveModel'; import { transformSceneToSaveModel, trimDashboardForSnapshot } from '../serialization/transformSceneToSaveModel';
import { DashboardInteractions } from '../utils/interactions'; import { DashboardInteractions } from '../utils/interactions';
@@ -48,10 +49,10 @@ const getDefaultExpireOption = () => {
}; };
export interface ShareSnapshotTabState extends SceneShareTabState { export interface ShareSnapshotTabState extends SceneShareTabState {
dashboardRef: SceneObjectRef<DashboardScene>;
panelRef?: SceneObjectRef<VizPanel>; panelRef?: SceneObjectRef<VizPanel>;
snapshotName: string; snapshotName: string;
selectedExpireOption: SelectableValue<number>; selectedExpireOption: SelectableValue<number>;
snapshotSharingOptions?: SnapshotSharingOptions; snapshotSharingOptions?: SnapshotSharingOptions;
} }

View File

@@ -10,6 +10,8 @@ import { ConfigPublicDashboardBase } from 'app/features/dashboard/components/Sha
import { PublicDashboard } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; import { PublicDashboard } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
import { AccessControlAction } from 'app/types'; import { AccessControlAction } from 'app/types';
import { shareDashboardType } from '../../../dashboard/components/ShareModal/utils';
import { getDashboardSceneFor } from '../../utils/utils';
import { ShareModal } from '../ShareModal'; import { ShareModal } from '../ShareModal';
import { ConfirmModal } from './ConfirmModal'; import { ConfirmModal } from './ConfirmModal';
@@ -25,8 +27,8 @@ export function ConfigPublicDashboard({ model, publicDashboard, isGetLoading }:
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const hasWritePermissions = contextSrv.hasPermission(AccessControlAction.DashboardsPublicWrite); const hasWritePermissions = contextSrv.hasPermission(AccessControlAction.DashboardsPublicWrite);
const { dashboardRef } = model.useState();
const dashboard = dashboardRef.resolve(); const dashboard = getDashboardSceneFor(model);
const { isDirty } = dashboard.useState(); const { isDirty } = dashboard.useState();
const [deletePublicDashboard] = useDeletePublicDashboardMutation(); const [deletePublicDashboard] = useDeletePublicDashboardMutation();
const hasTemplateVariables = (dashboard.state.$variables?.state.variables.length ?? 0) > 0; const hasTemplateVariables = (dashboard.state.$variables?.state.variables.length ?? 0) > 0;
@@ -52,7 +54,7 @@ export function ConfigPublicDashboard({ model, publicDashboard, isGetLoading }:
</p> </p>
), ),
onDismiss: () => { onDismiss: () => {
dashboard.showModal(new ShareModal({ dashboardRef, activeTab: 'Public Dashboard' })); dashboard.showModal(new ShareModal({ activeTab: shareDashboardType.publicDashboard }));
}, },
onConfirm: () => { onConfirm: () => {
deletePublicDashboard({ dashboard, dashboardUid: dashboard.state.uid!, uid: publicDashboard!.uid }); deletePublicDashboard({ dashboard, dashboardUid: dashboard.state.uid!, uid: publicDashboard!.uid });

View File

@@ -3,12 +3,13 @@ import React from 'react';
import { SceneComponentProps } from '@grafana/scenes'; import { SceneComponentProps } from '@grafana/scenes';
import { CreatePublicDashboardBase } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/CreatePublicDashboard'; import { CreatePublicDashboardBase } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/CreatePublicDashboard';
import { getDashboardSceneFor } from '../../utils/utils';
import { SharePublicDashboardTab } from './SharePublicDashboardTab'; import { SharePublicDashboardTab } from './SharePublicDashboardTab';
import { useUnsupportedDatasources } from './hooks'; import { useUnsupportedDatasources } from './hooks';
export function CreatePublicDashboard({ model }: SceneComponentProps<SharePublicDashboardTab>) { export function CreatePublicDashboard({ model }: SceneComponentProps<SharePublicDashboardTab>) {
const { dashboardRef } = model.useState(); const dashboard = getDashboardSceneFor(model);
const dashboard = dashboardRef.resolve();
const unsupportedDataSources = useUnsupportedDatasources(dashboard); const unsupportedDataSources = useUnsupportedDatasources(dashboard);
const hasTemplateVariables = (dashboard.state.$variables?.state.variables.length ?? 0) > 0; const hasTemplateVariables = (dashboard.state.$variables?.state.variables.length ?? 0) > 0;

View File

@@ -7,6 +7,7 @@ import { Loader } from 'app/features/dashboard/components/ShareModal/SharePublic
import { publicDashboardPersisted } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; import { publicDashboardPersisted } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
import { shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils'; import { shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils';
import { getDashboardSceneFor } from '../../utils/utils';
import { SceneShareTabState } from '../types'; import { SceneShareTabState } from '../types';
import { ConfigPublicDashboard } from './ConfigPublicDashboard'; import { ConfigPublicDashboard } from './ConfigPublicDashboard';
@@ -23,7 +24,7 @@ export class SharePublicDashboardTab extends SceneObjectBase<SceneShareTabState>
function SharePublicDashboardTabRenderer({ model }: SceneComponentProps<SharePublicDashboardTab>) { function SharePublicDashboardTabRenderer({ model }: SceneComponentProps<SharePublicDashboardTab>) {
const { data: publicDashboard, isLoading: isGetLoading } = useGetPublicDashboardQuery( const { data: publicDashboard, isLoading: isGetLoading } = useGetPublicDashboardQuery(
model.state.dashboardRef.resolve().state.uid! getDashboardSceneFor(model).state.uid!
); );
return ( return (

View File

@@ -1,13 +1,10 @@
import { SceneObject, SceneObjectRef, SceneObjectState } from '@grafana/scenes'; import { SceneObject, SceneObjectRef, SceneObjectState } from '@grafana/scenes';
import { DashboardScene } from '../scene/DashboardScene';
export interface ModalSceneObjectLike { export interface ModalSceneObjectLike {
onDismiss: () => void; onDismiss: () => void;
} }
export interface SceneShareTabState extends SceneObjectState { export interface SceneShareTabState extends SceneObjectState {
dashboardRef: SceneObjectRef<DashboardScene>;
modalRef?: SceneObjectRef<ModalSceneObjectLike>; modalRef?: SceneObjectRef<ModalSceneObjectLike>;
} }

View File

@@ -797,6 +797,18 @@
"success": "Library panel saved" "success": "Library panel saved"
} }
}, },
"link": {
"share": {
"config-alert-description": "Updating your settings will modify the default copy link to include these changes.",
"config-alert-title": "Link configuration",
"config-description": "Create a personalized, direct link to share your dashboard within your organization, with the following customization settings:",
"copy-link-button": "Copy link",
"copy-to-clipboard": "Link copied to clipboard",
"short-url-label": "Shorten link",
"time-range-description": "Change the current relative time range to an absolute time range",
"time-range-label": "Lock time range"
}
},
"login": { "login": {
"error": { "error": {
"blocked": "You have exceeded the number of login attempts for this user. Please try again later.", "blocked": "You have exceeded the number of login attempts for this user. Please try again later.",
@@ -1701,7 +1713,9 @@
"share-internally-description": "Advanced settings", "share-internally-description": "Advanced settings",
"share-internally-title": "Share internally", "share-internally-title": "Share internally",
"share-snapshot-title": "Share snapshot" "share-snapshot-title": "Share snapshot"
} },
"share-button": "Share",
"share-button-tooltip": "Copy shortened link"
}, },
"share-drawer": { "share-drawer": {
"confirm-action": { "confirm-action": {

View File

@@ -797,6 +797,18 @@
"success": "Ŀįþřäřy päʼnęľ şävęđ" "success": "Ŀįþřäřy päʼnęľ şävęđ"
} }
}, },
"link": {
"share": {
"config-alert-description": "Ůpđäŧįʼnģ yőūř şęŧŧįʼnģş ŵįľľ mőđįƒy ŧĥę đęƒäūľŧ čőpy ľįʼnĸ ŧő įʼnčľūđę ŧĥęşę čĥäʼnģęş.",
"config-alert-title": "Ŀįʼnĸ čőʼnƒįģūřäŧįőʼn",
"config-description": "Cřęäŧę ä pęřşőʼnäľįžęđ, đįřęčŧ ľįʼnĸ ŧő şĥäřę yőūř đäşĥþőäřđ ŵįŧĥįʼn yőūř őřģäʼnįžäŧįőʼn, ŵįŧĥ ŧĥę ƒőľľőŵįʼnģ čūşŧőmįžäŧįőʼn şęŧŧįʼnģş:",
"copy-link-button": "Cőpy ľįʼnĸ",
"copy-to-clipboard": "Ŀįʼnĸ čőpįęđ ŧő čľįpþőäřđ",
"short-url-label": "Ŝĥőřŧęʼn ľįʼnĸ",
"time-range-description": "Cĥäʼnģę ŧĥę čūřřęʼnŧ řęľäŧįvę ŧįmę řäʼnģę ŧő äʼn äþşőľūŧę ŧįmę řäʼnģę",
"time-range-label": "Ŀőčĸ ŧįmę řäʼnģę"
}
},
"login": { "login": {
"error": { "error": {
"blocked": "Ÿőū ĥävę ęχčęęđęđ ŧĥę ʼnūmþęř őƒ ľőģįʼn äŧŧęmpŧş ƒőř ŧĥįş ūşęř. Pľęäşę ŧřy äģäįʼn ľäŧęř.", "blocked": "Ÿőū ĥävę ęχčęęđęđ ŧĥę ʼnūmþęř őƒ ľőģįʼn äŧŧęmpŧş ƒőř ŧĥįş ūşęř. Pľęäşę ŧřy äģäįʼn ľäŧęř.",
@@ -1701,7 +1713,9 @@
"share-internally-description": "Åđväʼnčęđ şęŧŧįʼnģş", "share-internally-description": "Åđväʼnčęđ şęŧŧįʼnģş",
"share-internally-title": "Ŝĥäřę įʼnŧęřʼnäľľy", "share-internally-title": "Ŝĥäřę įʼnŧęřʼnäľľy",
"share-snapshot-title": "Ŝĥäřę şʼnäpşĥőŧ" "share-snapshot-title": "Ŝĥäřę şʼnäpşĥőŧ"
} },
"share-button": "Ŝĥäřę",
"share-button-tooltip": "Cőpy şĥőřŧęʼnęđ ľįʼnĸ"
}, },
"share-drawer": { "share-drawer": {
"confirm-action": { "confirm-action": {