Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ShareModal: Share link redesign under newDashboardSharingComponent FF #87011

Merged
merged 15 commits into from May 3, 2024
Merged
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'}
juanicabanas marked this conversation as resolved.
Show resolved Hide resolved
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>
);
}