From 4e7476853046aa4da7cc368f82adb23d261d346c Mon Sep 17 00:00:00 2001 From: juanicabanas Date: Tue, 28 Feb 2023 09:02:23 -0300 Subject: [PATCH] PublicDashboards: Email sharing (#63762) Feature for sharing a public dashboard by email --- .../dashboards/dashboard-public/index.md | 21 +- .../src/selectors/pages.ts | 6 + .../dashboard/api/publicDashboardApi.ts | 54 ++++- .../ConfigPublicDashboard.tsx | 7 +- .../EmailSharingConfiguration.tsx | 228 ++++++++++++++++++ .../SharePublicDashboard.test.tsx | 86 ++----- .../SharePublicDashboardUtils.ts | 9 + .../SharePublicDashboard/utilsTest.tsx | 81 +++++++ .../DeletePublicDashboardModal.tsx | 19 +- 9 files changed, 406 insertions(+), 105 deletions(-) create mode 100644 public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/EmailSharingConfiguration.tsx create mode 100644 public/app/features/dashboard/components/ShareModal/SharePublicDashboard/utilsTest.tsx diff --git a/docs/sources/dashboards/dashboard-public/index.md b/docs/sources/dashboards/dashboard-public/index.md index bd40a262b55..e0a5a578ed7 100644 --- a/docs/sources/dashboards/dashboard-public/index.md +++ b/docs/sources/dashboards/dashboard-public/index.md @@ -43,21 +43,26 @@ If you are using Docker, use an environment variable to enable public dashboards #### Make a dashboard public - Click on the sharing icon to the right of the dashboard title. -- Click on the Public Dashboard tab. +- Click on the **Public dashboard** tab. - Acknowledge the implications of making the dashboard public by checking all the checkboxes. -- Turn on the Enabled toggle. -- Click `Save Sharing Configuration` to make the dashboard public and make your link live. +- Click **Generate public URL** to make the dashboard public and make your link live. - Copy the public dashboard link if you'd like to share it. You can always come back later for it. +#### Pause access + +- Click on the sharing icon to the right of the dashboard title. +- Click on the **Public dashboard** tab. +- Enable the **Pause sharing dashboard** toggle. +- The dashboard is no longer accessible, even with the link, until you make it shareable again. + #### Revoke access - Click on the sharing icon to the right of the dashboard title. -- Click on the Public Dashboard tab. -- Turn off the Enabled toggle. -- Click `Save Sharing Configuration` to save your changes. -- Anyone with the link will not be able to access the dashboard publicly anymore. +- Click on the **Public dashboard** tab. +- Click **Revoke public URL** to delete the public dashboard. +- The link no longer works. You must create a new public URL as in [Make a dashboard public](#make-a-dashboard-public). -#### Supported Datasources +#### Supported datasources Public dashboards _should_ work with any datasource that has the properties `backend` and `alerting` both set to true in it's `package.json`. However, this cannot always be guaranteed because plugin developers can override this functionality. The following lists include data sources confirmed to work with public dashboards and data sources that should work but have not been confirmed as compatible. diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index de149ca2e6a..69ec204810e 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -200,6 +200,12 @@ export const Pages = { UnsupportedDataSourcesWarningAlert: 'data-testid public dashboard unsupported data sources alert', NoUpsertPermissionsWarningAlert: 'data-testid public dashboard no upsert permissions alert', EnableTimeRangeSwitch: 'data-testid public dashboard on off switch for time range', + EmailSharingConfiguration: { + ShareType: 'data-testid public dashboard share type', + EmailSharingInput: 'data-testid public dashboard email sharing input', + EmailSharingInviteButton: 'data-testid public dashboard email sharing invite button', + EmailSharingList: 'data-testid public dashboard email sharing list', + }, }, }, Explore: { diff --git a/public/app/features/dashboard/api/publicDashboardApi.ts b/public/app/features/dashboard/api/publicDashboardApi.ts index b4cd1508e8a..8eed4f279ae 100644 --- a/public/app/features/dashboard/api/publicDashboardApi.ts +++ b/public/app/features/dashboard/api/publicDashboardApi.ts @@ -1,7 +1,7 @@ -import { BaseQueryFn, createApi, retry } from '@reduxjs/toolkit/query/react'; +import { BaseQueryFn, createApi } from '@reduxjs/toolkit/query/react'; import { lastValueFrom } from 'rxjs'; -import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime/src'; +import { BackendSrvRequest, FetchError, getBackendSrv, isFetchError } from '@grafana/runtime/src'; import { notifyApp } from 'app/core/actions'; import { createErrorNotification, createSuccessNotification } from 'app/core/copy/appNotification'; import { @@ -16,6 +16,10 @@ type ReqOptions = { showErrorAlert?: boolean; }; +function isFetchBaseQueryError(error: unknown): error is { error: FetchError } { + return typeof error === 'object' && error != null && 'error' in error; +} + const backendSrvBaseQuery = ({ baseUrl }: { baseUrl: string }): BaseQueryFn => async (requestOptions) => { @@ -33,17 +37,17 @@ const backendSrvBaseQuery = } }; -const getConfigError = (err: { status: number }) => ({ error: err.status !== 404 ? err : null }); +const getConfigError = (err: unknown) => ({ error: isFetchError(err) && err.status !== 404 ? err : null }); export const publicDashboardApi = createApi({ reducerPath: 'publicDashboardApi', - baseQuery: retry(backendSrvBaseQuery({ baseUrl: '/api/dashboards' }), { maxRetries: 0 }), + baseQuery: backendSrvBaseQuery({ baseUrl: '/api' }), tagTypes: ['PublicDashboard', 'AuditTablePublicDashboard'], refetchOnMountOrArgChange: true, endpoints: (builder) => ({ getPublicDashboard: builder.query({ query: (dashboardUid) => ({ - url: `/uid/${dashboardUid}/public-dashboards`, + url: `/dashboards/uid/${dashboardUid}/public-dashboards`, manageError: getConfigError, showErrorAlert: false, }), @@ -51,9 +55,9 @@ export const publicDashboardApi = createApi({ try { await queryFulfilled; } catch (e) { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const customError = e as { error: { data: { message: string } } }; - dispatch(notifyApp(createErrorNotification(customError?.error?.data?.message))); + if (isFetchBaseQueryError(e) && isFetchError(e.error)) { + dispatch(notifyApp(createErrorNotification(e.error.data.message))); + } } }, providesTags: (result, error, dashboardUid) => [{ type: 'PublicDashboard', id: dashboardUid }], @@ -63,7 +67,7 @@ export const publicDashboardApi = createApi({ { dashboard: DashboardModel; payload: Partial } >({ query: (params) => ({ - url: `/uid/${params.dashboard.uid}/public-dashboards`, + url: `/dashboards/uid/${params.dashboard.uid}/public-dashboards`, method: 'POST', data: params.payload, }), @@ -81,11 +85,10 @@ export const publicDashboardApi = createApi({ }), updatePublicDashboard: builder.mutation({ query: (params) => ({ - url: `/uid/${params.dashboard.uid}/public-dashboards/${params.payload.uid}`, + url: `/dashboards/uid/${params.dashboard.uid}/public-dashboards/${params.payload.uid}`, method: 'PUT', data: params.payload, }), - extraOptions: { maxRetries: 0 }, async onQueryStarted({ dashboard, payload }, { dispatch, queryFulfilled }) { const { data } = await queryFulfilled; dispatch(notifyApp(createSuccessNotification('Public dashboard updated!'))); @@ -98,15 +101,38 @@ export const publicDashboardApi = createApi({ }, invalidatesTags: (result, error, { payload }) => [{ type: 'PublicDashboard', id: payload.dashboardUid }], }), + addEmailSharing: builder.mutation({ + query: ({ recipient, uid }) => ({ + url: `/public-dashboards/${uid}/share/recipients`, + method: 'POST', + data: { recipient }, + }), + async onQueryStarted(_, { dispatch, queryFulfilled }) { + await queryFulfilled; + dispatch(notifyApp(createSuccessNotification('Invite sent!'))); + }, + invalidatesTags: (result, error, { dashboardUid }) => [{ type: 'PublicDashboard', id: dashboardUid }], + }), + deleteEmailSharing: builder.mutation({ + query: ({ uid, recipient }) => ({ + url: `/public-dashboards/${uid}/share/recipients/${recipient}`, + method: 'DELETE', + }), + async onQueryStarted(_, { dispatch, queryFulfilled }) { + await queryFulfilled; + dispatch(notifyApp(createSuccessNotification('User revoked'))); + }, + invalidatesTags: (result, error, { dashboardUid }) => [{ type: 'PublicDashboard', id: dashboardUid }], + }), listPublicDashboards: builder.query({ query: () => ({ - url: '/public-dashboards', + url: '/dashboards/public-dashboards', }), providesTags: ['AuditTablePublicDashboard'], }), deletePublicDashboard: builder.mutation({ query: (params) => ({ - url: `/uid/${params.dashboardUid}/public-dashboards/${params.uid}`, + url: `/dashboards/uid/${params.dashboardUid}/public-dashboards/${params.uid}`, method: 'DELETE', }), async onQueryStarted({ dashboard, uid }, { dispatch, queryFulfilled }) { @@ -132,4 +158,6 @@ export const { useUpdatePublicDashboardMutation, useDeletePublicDashboardMutation, useListPublicDashboardsQuery, + useAddEmailSharingMutation, + useDeleteEmailSharingMutation, } = publicDashboardApi; diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx index 0a7d0e16f2a..d9040289053 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx @@ -4,8 +4,9 @@ import React, { useContext } from 'react'; import { useForm } from 'react-hook-form'; import { GrafanaTheme2 } from '@grafana/data/src'; +import { GrafanaEdition } from '@grafana/data/src/types/config'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src'; -import { reportInteraction } from '@grafana/runtime/src'; +import { config, reportInteraction } from '@grafana/runtime/src'; import { ClipboardButton, Field, @@ -37,6 +38,7 @@ import { } from '../SharePublicDashboardUtils'; import { Configuration } from './Configuration'; +import { EmailSharingConfiguration } from './EmailSharingConfiguration'; const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard; @@ -52,6 +54,8 @@ const ConfigPublicDashboard = () => { const isDesktop = useIsDesktop(); const hasWritePermissions = contextSrv.hasAccess(AccessControlAction.DashboardsPublicWrite, isOrgAdmin()); + const hasEmailSharingEnabled = + config.licenseInfo.edition === GrafanaEdition.Enterprise && !!config.featureToggles.publicDashboardsEmailSharing; const dashboardState = useSelector((store) => store.dashboard); const dashboard = dashboardState.getModel()!; const dashboardVariables = dashboard.getVariables(); @@ -114,6 +118,7 @@ const ConfigPublicDashboard = () => {
+ {hasEmailSharingEnabled && } > = [ + { label: 'Anyone with a link', value: PublicDashboardShareType.PUBLIC }, + { label: 'Only specified people', value: PublicDashboardShareType.EMAIL }, +]; + +const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard.EmailSharingConfiguration; + +const EmailList = ({ + recipients, + dashboardUid, + publicDashboardUid, +}: { + recipients: string[]; + dashboardUid: string; + publicDashboardUid: string; +}) => { + const styles = useStyles2(getStyles); + const [deleteEmail, { isLoading: isDeleteLoading }] = useDeleteEmailSharingMutation(); + + const onDeleteEmail = (email: string) => { + deleteEmail({ recipient: email, dashboardUid: dashboardUid, uid: publicDashboardUid }); + }; + + return ( + + + {recipients.map((recipient) => ( + + + + + ))} + +
{recipient} + + + +
+ ); +}; + +export const EmailSharingConfiguration = () => { + const styles = useStyles2(getStyles); + const dashboardState = useSelector((store) => store.dashboard); + const dashboard = dashboardState.getModel()!; + + const { data: publicDashboard } = useGetPublicDashboardQuery(dashboard.uid); + const [updateShareType] = useUpdatePublicDashboardMutation(); + const [addEmail, { isLoading: isAddEmailLoading }] = useAddEmailSharingMutation(); + + const { + register, + setValue, + control, + watch, + handleSubmit, + formState: { isValid, errors }, + reset, + } = useForm({ + defaultValues: { + shareType: publicDashboard?.share || PublicDashboardShareType.PUBLIC, + email: '', + }, + mode: 'onChange', + }); + + const onShareTypeChange = (shareType: PublicDashboardShareType) => { + const req = { + dashboard, + payload: { + ...publicDashboard!, + share: shareType, + }, + }; + + updateShareType(req); + }; + + const onSubmit = async (data: EmailSharingConfigurationForm) => { + await addEmail({ recipient: data.email, uid: publicDashboard!.uid, dashboardUid: dashboard.uid }).unwrap(); + reset({ email: '', shareType: PublicDashboardShareType.EMAIL }); + }; + + return ( +
+ + { + const { ref, ...rest } = field; + return ( + { + setValue('shareType', shareType); + onShareTypeChange(shareType); + }} + /> + ); + }} + /> + + {watch('shareType') === PublicDashboardShareType.EMAIL && ( + <> + +
+ + +
+
+ {!!publicDashboard?.recipients?.length && ( + + )} + + )} + + ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + container: css` + margin-bottom: ${theme.spacing(2)}; + `, + emailContainer: css` + display: flex; + gap: ${theme.spacing(1)}; + `, + emailInput: css` + flex-grow: 1; + `, + table: css` + display: flex; + max-height: 220px; + overflow-y: scroll; + margin-bottom: ${theme.spacing(1)}; + + & tbody { + display: flex; + flex-direction: column; + flex-grow: 1; + } + + & tr { + min-height: 40px; + display: flex; + align-items: center; + justify-content: space-between; + padding: ${theme.spacing(0.5, 1)}; + + :nth-child(odd) { + background: ${theme.colors.background.secondary}; + } + } + `, + tableButtonsContainer: css` + display: flex; + justify-content: end; + `, +}); diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.test.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.test.tsx index fd38c03bbbd..5c4428e3490 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.test.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.test.tsx @@ -1,8 +1,6 @@ -import { fireEvent, render, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react'; +import { fireEvent, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import React from 'react'; -import { Provider } from 'react-redux'; import 'whatwg-fetch'; import { BootData, DataQuery } from '@grafana/data/src'; @@ -13,14 +11,15 @@ import config from 'app/core/config'; import { backendSrv } from 'app/core/services/backend_srv'; import { contextSrv } from 'app/core/services/context_srv'; import { Echo } from 'app/core/services/echo/Echo'; -import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { createDashboardModelFixture } from 'app/features/dashboard/state/__fixtures__/dashboardFixtures'; -import { configureStore } from 'app/store/configureStore'; - -import { DashboardInitPhase } from '../../../../../types'; -import { ShareModal } from '../ShareModal'; import * as sharePublicDashboardUtils from './SharePublicDashboardUtils'; +import { + getExistentPublicDashboardResponse, + mockDashboard, + pubdashResponse, + renderSharePublicDashboard, +} from './utilsTest'; const server = setupServer(); @@ -29,46 +28,9 @@ jest.mock('@grafana/runtime', () => ({ getBackendSrv: () => backendSrv, })); -const renderSharePublicDashboard = async ( - props?: Partial>, - isEnabled = true -) => { - const store = configureStore({ - dashboard: { - getModel: () => props?.dashboard || mockDashboard, - permissions: [], - initError: null, - initPhase: DashboardInitPhase.Completed, - }, - }); - - const newProps = Object.assign( - { - panel: mockPanel, - dashboard: mockDashboard, - onDismiss: () => {}, - }, - props - ); - - render( - - - - ); - - await waitFor(() => screen.getByText('Link')); - if (isEnabled) { - fireEvent.click(screen.getByText('Public dashboard')); - await waitForElementToBeRemoved(screen.getByText('Loading configuration')); - } -}; - const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard; let originalBootData: BootData; -let mockDashboard: DashboardModel; -let mockPanel: PanelModel; beforeAll(() => { setEchoSrv(new Echo()); @@ -96,14 +58,6 @@ beforeAll(() => { beforeEach(() => { config.featureToggles.publicDashboards = true; - mockDashboard = createDashboardModelFixture({ - uid: 'mockDashboardUid', - timezone: 'utc', - }); - - mockPanel = new PanelModel({ - id: 'mockPanelId', - }); jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(true); jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true); @@ -120,25 +74,6 @@ afterEach(() => { server.resetHandlers(); }); -const pubdashResponse: sharePublicDashboardUtils.PublicDashboard = { - isEnabled: true, - annotationsEnabled: true, - timeSelectionEnabled: true, - uid: 'a-uid', - dashboardUid: '', - accessToken: 'an-access-token', -}; - -const getExistentPublicDashboardResponse = () => - rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => { - return res( - ctx.status(200), - ctx.json({ - ...pubdashResponse, - dashboardUid: req.params.dashboardUid, - }) - ); - }); const getNonExistentPublicDashboardResponse = () => rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => { return res( @@ -360,5 +295,12 @@ describe('SharePublic - Already persisted', () => { expect(screen.getByTestId(selectors.PauseSwitch)).toBeChecked(); }); + it('does not render email sharing section', async () => { + await renderSharePublicDashboard(); + + expect(screen.queryByTestId(selectors.EmailSharingConfiguration.EmailSharingInput)).not.toBeInTheDocument(); + expect(screen.queryByTestId(selectors.EmailSharingConfiguration.EmailSharingInviteButton)).not.toBeInTheDocument(); + expect(screen.queryByTestId(selectors.EmailSharingConfiguration.EmailSharingList)).not.toBeInTheDocument(); + }); alertTests(); }); diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils.ts b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils.ts index cc732f8ed13..c89a902c199 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils.ts +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils.ts @@ -5,6 +5,11 @@ import { PanelModel } from '../../../state'; import { supportedDatasources } from './SupportedPubdashDatasources'; +export enum PublicDashboardShareType { + PUBLIC = 'public', + EMAIL = 'email', +} + export interface PublicDashboardSettings { annotationsEnabled: boolean; isEnabled: boolean; @@ -16,6 +21,8 @@ export interface PublicDashboard extends PublicDashboardSettings { uid: string; dashboardUid: string; timeSettings?: object; + share: PublicDashboardShareType; + recipients?: string[]; } // Instance methods @@ -56,3 +63,5 @@ export const getUnsupportedDashboardDatasources = (panels: PanelModel[]): string export const generatePublicDashboardUrl = (publicDashboard: PublicDashboard): string => { return `${getConfig().appUrl}public-dashboards/${publicDashboard.accessToken}`; }; + +export const validEmailRegex = /^[A-Z\d._%+-]+@[A-Z\d.-]+\.[A-Z]{2,}$/i; diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/utilsTest.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/utilsTest.tsx new file mode 100644 index 00000000000..f3578f28bc2 --- /dev/null +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/utilsTest.tsx @@ -0,0 +1,81 @@ +import { fireEvent, render, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react'; +import { rest } from 'msw'; +import React from 'react'; +import { Provider } from 'react-redux'; + +import { configureStore } from '../../../../../store/configureStore'; +import { DashboardInitPhase } from '../../../../../types'; +import { DashboardModel, PanelModel } from '../../../state'; +import { createDashboardModelFixture } from '../../../state/__fixtures__/dashboardFixtures'; +import { ShareModal } from '../ShareModal'; + +import * as sharePublicDashboardUtils from './SharePublicDashboardUtils'; +import { PublicDashboard, PublicDashboardShareType } from './SharePublicDashboardUtils'; + +export const mockDashboard: DashboardModel = createDashboardModelFixture({ + uid: 'mockDashboardUid', + timezone: 'utc', +}); + +export const mockPanel = new PanelModel({ + id: 'mockPanelId', +}); + +export const pubdashResponse: sharePublicDashboardUtils.PublicDashboard = { + isEnabled: true, + annotationsEnabled: true, + timeSelectionEnabled: true, + uid: 'a-uid', + dashboardUid: '', + accessToken: 'an-access-token', + share: PublicDashboardShareType.PUBLIC, +}; + +export const getExistentPublicDashboardResponse = (publicDashboard?: Partial) => + rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + ...pubdashResponse, + ...publicDashboard, + dashboardUid: req.params.dashboardUid, + }) + ); + }); + +export const renderSharePublicDashboard = async ( + props?: Partial>, + isEnabled = true +) => { + const store = configureStore({ + dashboard: { + getModel: () => props?.dashboard || mockDashboard, + permissions: [], + initError: null, + initPhase: DashboardInitPhase.Completed, + }, + }); + + const newProps = Object.assign( + { + panel: mockPanel, + dashboard: mockDashboard, + onDismiss: () => {}, + }, + props + ); + + const renderResult = render( + + + + ); + + await waitFor(() => screen.getByText('Link')); + if (isEnabled) { + fireEvent.click(screen.getByText('Public dashboard')); + await waitForElementToBeRemoved(screen.getByText('Loading configuration')); + } + + return renderResult; +}; diff --git a/public/app/features/manage-dashboards/components/PublicDashboardListTable/DeletePublicDashboardModal.tsx b/public/app/features/manage-dashboards/components/PublicDashboardListTable/DeletePublicDashboardModal.tsx index bd2ef614546..780d13c27d8 100644 --- a/public/app/features/manage-dashboards/components/PublicDashboardListTable/DeletePublicDashboardModal.tsx +++ b/public/app/features/manage-dashboards/components/PublicDashboardListTable/DeletePublicDashboardModal.tsx @@ -8,14 +8,11 @@ const Body = ({ title }: { title?: string }) => { const styles = useStyles2(getStyles); return ( - <> -

Do you want to delete this public dashboard?

-

- {title - ? `This will delete the public dashboard for "${title}". Your dashboard will not be deleted.` - : 'Orphaned public dashboard will be deleted'} -

- +

+ {title + ? 'Are you sure you want to revoke this URL? The dashboard will no longer be public.' + : 'Orphaned public dashboard will no longer be public.'} +

); }; @@ -33,9 +30,9 @@ export const DeletePublicDashboardModal = ({ body={} onConfirm={onConfirm} onDismiss={onDismiss} - title="Delete" + title="Revoke public URL" icon="trash-alt" - confirmText="Delete" + confirmText="Revoke public URL" /> ); @@ -44,6 +41,6 @@ const getStyles = (theme: GrafanaTheme2) => ({ margin-bottom: ${theme.spacing(1)}; `, description: css` - font-size: ${theme.typography.bodySmall.fontSize}; + font-size: ${theme.typography.body.fontSize}; `, });