Skip to content

Commit

Permalink
ShareModal: Share link redesign under newDashboardSharingComponent
Browse files Browse the repository at this point in the history
…FF (#87011)
  • Loading branch information
juanicabanas committed May 3, 2024
1 parent 196134b commit d1434fa
Show file tree
Hide file tree
Showing 17 changed files with 334 additions and 36 deletions.
Expand Up @@ -180,6 +180,7 @@ Experimental features might be changed or removed without prior notice.
| `queryLibrary` | Enables Query Library feature in Explore |
| `autofixDSUID` | Automatically migrates invalid datasource UIDs |
| `logsExploreTableDefaultVisualization` | Sets the logs table as default visualisation in logs explore |
| `newDashboardSharingComponent` | Enables the new sharing drawer design |

## Development feature toggles

Expand Down
1 change: 1 addition & 0 deletions packages/grafana-data/src/types/featureToggles.gen.ts
Expand Up @@ -183,4 +183,5 @@ export interface FeatureToggles {
queryLibrary?: boolean;
autofixDSUID?: boolean;
logsExploreTableDefaultVisualization?: boolean;
newDashboardSharingComponent?: boolean;
}
9 changes: 9 additions & 0 deletions packages/grafana-e2e-selectors/src/selectors/pages.ts
Expand Up @@ -58,6 +58,15 @@ export const Pages = {
publicDashboardTag: 'data-testid public dashboard tag',
shareButton: 'data-testid share-button',
scrollContainer: 'data-testid Dashboard canvas scroll container',
newShareButton: {
container: 'data-testid new share button',
shareLink: 'data-testid new share link-button',
arrowMenu: 'data-testid new share button arrow menu',
menu: {
container: 'data-testid new share button menu',
shareInternally: 'data-testid new share button share internally',
},
},
playlistControls: {
prev: 'data-testid playlist previous dashboard button',
stop: 'data-testid playlist stop dashboard button',
Expand Down
7 changes: 7 additions & 0 deletions pkg/services/featuremgmt/registry.go
Expand Up @@ -1233,6 +1233,13 @@ var (
Owner: grafanaObservabilityLogsSquad,
FrontendOnly: true,
},
{
Name: "newDashboardSharingComponent",
Description: "Enables the new sharing drawer design",
Stage: FeatureStageExperimental,
Owner: grafanaSharingSquad,
FrontendOnly: true,
},
}
)

Expand Down
1 change: 1 addition & 0 deletions pkg/services/featuremgmt/toggles_gen.csv
Expand Up @@ -164,3 +164,4 @@ grafanaManagedRecordingRules,experimental,@grafana/alerting-squad,false,false,fa
queryLibrary,experimental,@grafana/explore-squad,false,false,false
autofixDSUID,experimental,@grafana/plugins-platform-backend,false,false,false
logsExploreTableDefaultVisualization,experimental,@grafana/observability-logs,false,false,true
newDashboardSharingComponent,experimental,@grafana/sharing-squad,false,false,true
4 changes: 4 additions & 0 deletions pkg/services/featuremgmt/toggles_gen.go
Expand Up @@ -666,4 +666,8 @@ const (
// FlagLogsExploreTableDefaultVisualization
// Sets the logs table as default visualisation in logs explore
FlagLogsExploreTableDefaultVisualization = "logsExploreTableDefaultVisualization"

// FlagNewDashboardSharingComponent
// Enables the new sharing drawer design
FlagNewDashboardSharingComponent = "newDashboardSharingComponent"
)
15 changes: 14 additions & 1 deletion pkg/services/featuremgmt/toggles_gen.json
Expand Up @@ -2136,6 +2136,19 @@
"codeowner": "@grafana/observability-logs",
"frontend": true
}
},
{
"metadata": {
"name": "newDashboardSharingComponent",
"resourceVersion": "1713982966391",
"creationTimestamp": "2024-04-24T18:22:46Z"
},
"spec": {
"description": "Enables the new sharing drawer design",
"stage": "experimental",
"codeowner": "@grafana/sharing-squad",
"frontend": true
}
}
]
}
}
4 changes: 1 addition & 3 deletions public/app/core/utils/shortLinks.test.ts
@@ -1,16 +1,14 @@
import { createShortLink, createAndCopyShortLink } from './shortLinks';

jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => {
return {
post: () => {
return Promise.resolve({ url: 'www.short.com' });
},
};
},
config: {
appSubUrl: '',
},
}));

describe('createShortLink', () => {
Expand Down
57 changes: 56 additions & 1 deletion public/app/core/utils/shortLinks.ts
@@ -1,8 +1,12 @@
import memoizeOne from 'memoize-one';

import { getBackendSrv, config } from '@grafana/runtime';
import { UrlQueryMap } from '@grafana/data';
import { getBackendSrv, config, locationService } from '@grafana/runtime';
import { sceneGraph, SceneTimeRangeLike, VizPanel } from '@grafana/scenes';
import { notifyApp } from 'app/core/actions';
import { createErrorNotification, createSuccessNotification } from 'app/core/copy/appNotification';
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
import { getDashboardUrl } from 'app/features/dashboard-scene/utils/urlBuilders';
import { dispatch } from 'app/store/store';

import { copyStringToClipboard } from './explore';
Expand Down Expand Up @@ -37,3 +41,54 @@ export const createAndCopyShortLink = async (path: string) => {
dispatch(notifyApp(createErrorNotification('Error generating shortened link')));
}
};

export const createAndCopyDashboardShortLink = async (
dashboard: DashboardScene,
opts: { useAbsoluteTimeRange: boolean; theme: string },
panel?: VizPanel
) => {
const shareUrl = await createDashboardShareUrl(dashboard, opts, panel);
await createAndCopyShortLink(shareUrl);
};

export const createDashboardShareUrl = async (
dashboard: DashboardScene,
opts: { useAbsoluteTimeRange: boolean; theme: string },
panel?: VizPanel
) => {
const location = locationService.getLocation();
const timeRange = sceneGraph.getTimeRange(panel ?? dashboard);

const urlParamsUpdate = getShareUrlParams(opts, timeRange, panel);

return getDashboardUrl({
uid: dashboard.state.uid,
slug: dashboard.state.meta.slug,
currentQueryParams: location.search,
updateQuery: urlParamsUpdate,
absolute: true,
});
};

export const getShareUrlParams = (
opts: { useAbsoluteTimeRange: boolean; theme: string },
timeRange: SceneTimeRangeLike,
panel?: VizPanel
) => {
const urlParamsUpdate: UrlQueryMap = {};

if (panel) {
urlParamsUpdate.viewPanel = panel.state.key;
}

if (opts.useAbsoluteTimeRange) {
urlParamsUpdate.from = timeRange.state.value.from.toISOString();
urlParamsUpdate.to = timeRange.state.value.to.toISOString();
}

if (opts.theme !== 'current') {
urlParamsUpdate.theme = opts.theme;
}

return urlParamsUpdate;
};
Expand Up @@ -5,6 +5,7 @@ import { TestProvider } from 'test/helpers/TestProvider';
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';

import { selectors } from '@grafana/e2e-selectors';
import { config } from '@grafana/runtime';
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';

import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
Expand All @@ -25,7 +26,7 @@ jest.mock('app/features/playlist/PlaylistSrv', () => ({
}));

describe('NavToolbarActions', () => {
describe('Give an already saved dashboard', () => {
describe('Given an already saved dashboard', () => {
it('Should show correct buttons when not in editing', async () => {
setup();

Expand All @@ -35,7 +36,7 @@ describe('NavToolbarActions', () => {
expect(await screen.findByText('Share')).toBeInTheDocument();
});

it('Should the correct buttons when playing a playlist', async () => {
it('Should show the correct buttons when playing a playlist', async () => {
jest.mocked(playlistSrv).useState.mockReturnValueOnce({ isPlaying: true });
setup();

Expand Down Expand Up @@ -101,6 +102,24 @@ describe('NavToolbarActions', () => {
expect(screen.queryByText(selectors.pages.Dashboard.DashNav.playlistControls.next)).not.toBeInTheDocument();
});
});

describe('Given new sharing button', () => {
it('Should show old share button when newDashboardSharingComponent FF is disabled', async () => {
setup();

expect(await screen.findByText('Share')).toBeInTheDocument();
const newShareButton = screen.queryByTestId(selectors.pages.Dashboard.DashNav.newShareButton.container);
expect(newShareButton).not.toBeInTheDocument();
});
it('Should show new share button when newDashboardSharingComponent FF is enabled', async () => {
config.featureToggles.newDashboardSharingComponent = true;
setup();

expect(screen.queryByTestId(selectors.pages.Dashboard.DashNav.shareButton)).not.toBeInTheDocument();
const newShareButton = screen.getByTestId(selectors.pages.Dashboard.DashNav.newShareButton.container);
expect(newShareButton).toBeInTheDocument();
});
});
});

let cleanUp = () => {};
Expand Down
12 changes: 10 additions & 2 deletions public/app/features/dashboard-scene/scene/NavToolbarActions.tsx
Expand Up @@ -23,6 +23,7 @@ import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';

import { PanelEditor } from '../panel-edit/PanelEditor';
import ShareButton from '../sharing/ShareButton/ShareButton';
import { ShareModal } from '../sharing/ShareModal';
import { DashboardInteractions } from '../utils/interactions';
import { DynamicDashNavButtonModel, dynamicDashNavActions } from '../utils/registerDynamicDashNavAction';
Expand Down Expand Up @@ -303,9 +304,10 @@ export function ToolbarActions({ dashboard }: Props) {
),
});

const showShareButton = uid && !isEditing && !meta.isSnapshot && !isPlaying;
toolbarActions.push({
group: 'main-buttons',
condition: uid && !isEditing && !meta.isSnapshot && !isPlaying,
condition: !config.featureToggles.newDashboardSharingComponent && showShareButton,
render: () => (
<Button
key="share-dashboard-button"
Expand Down Expand Up @@ -335,7 +337,7 @@ export function ToolbarActions({ dashboard }: Props) {
tooltip="Enter edit mode"
key="edit"
className={styles.buttonWithExtraMargin}
variant="primary"
variant={config.featureToggles.newDashboardSharingComponent ? 'secondary' : 'primary'}
size="sm"
data-testid={selectors.components.NavToolbar.editDashboard.editButton}
>
Expand All @@ -344,6 +346,12 @@ export function ToolbarActions({ dashboard }: Props) {
),
});

toolbarActions.push({
group: 'new-share-dashboard-button',
condition: config.featureToggles.newDashboardSharingComponent && showShareButton,
render: () => <ShareButton key="new-share-dashboard-button" dashboard={dashboard} />,
});

toolbarActions.push({
group: 'settings',
condition: isEditing && dashboard.canEditDashboard() && isShowingDashboard,
Expand Down
@@ -0,0 +1,73 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';

import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { SceneGridLayout, SceneTimeRange, VizPanel } from '@grafana/scenes';

import { DashboardGridItem } from '../../scene/DashboardGridItem';
import { DashboardScene } from '../../scene/DashboardScene';

import ShareButton from './ShareButton';

const createAndCopyDashboardShortLinkMock = jest.fn();
jest.mock('app/core/utils/shortLinks', () => ({
...jest.requireActual('app/core/utils/shortLinks'),
createAndCopyDashboardShortLink: () => createAndCopyDashboardShortLinkMock(),
}));

const selector = e2eSelectors.pages.Dashboard.DashNav.newShareButton;
describe('ShareButton', () => {
it('should render share link button and menu', async () => {
setup();

expect(await screen.findByTestId(selector.shareLink)).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 () => {
setup();

const arrowMenu = await screen.findByTestId(selector.arrowMenu);
await userEvent.click(arrowMenu);

expect(await screen.findByTestId(selector.menu.container)).toBeInTheDocument();
});
});

function setup() {
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 DashboardGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 10,
height: 12,
body: panel,
}),
],
}),
});

render(<ShareButton dashboard={dashboard} />);
}
@@ -0,0 +1,42 @@
import React, { useCallback, useState } from 'react';
import { useAsyncFn } from 'react-use';

import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { Button, ButtonGroup, Dropdown } from '@grafana/ui';
import { createAndCopyDashboardShortLink } from 'app/core/utils/shortLinks';

import { DashboardScene } from '../../scene/DashboardScene';
import { DashboardInteractions } from '../../utils/interactions';

import ShareMenu from './ShareMenu';

const newShareButtonSelector = e2eSelectors.pages.Dashboard.DashNav.newShareButton;

export default function ShareButton({ dashboard }: { dashboard: DashboardScene }) {
const [isOpen, setIsOpen] = useState(false);

const [_, buildUrl] = useAsyncFn(async () => {
return await createAndCopyDashboardShortLink(dashboard, { useAbsoluteTimeRange: true, theme: 'current' });
}, [dashboard]);

const onMenuClick = useCallback((isOpen: boolean) => {
if (isOpen) {
DashboardInteractions.toolbarShareClick();
}

setIsOpen(isOpen);
}, []);

const MenuActions = () => <ShareMenu dashboard={dashboard} />;

return (
<ButtonGroup data-testid={newShareButtonSelector.container}>
<Button data-testid={newShareButtonSelector.shareLink} size="sm" tooltip="Copy shortened URL" onClick={buildUrl}>
Share
</Button>
<Dropdown overlay={MenuActions} placement="bottom-end" onVisibleChange={onMenuClick}>
<Button data-testid={newShareButtonSelector.arrowMenu} size="sm" icon={isOpen ? 'angle-up' : 'angle-down'} />
</Dropdown>
</ButtonGroup>
);
}

0 comments on commit d1434fa

Please sign in to comment.