mirror of
https://github.com/grafana/grafana.git
synced 2025-02-13 00:55:47 -06:00
PublicDashboards: Email sharing (#63762)
Feature for sharing a public dashboard by email
This commit is contained in:
parent
89ad81b15a
commit
4e74768530
@ -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.
|
||||
|
@ -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: {
|
||||
|
@ -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<BackendSrvRequest & ReqOptions> =>
|
||||
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<PublicDashboard | undefined, string>({
|
||||
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<PublicDashboardSettings> }
|
||||
>({
|
||||
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<PublicDashboard, { dashboard: DashboardModel; payload: PublicDashboard }>({
|
||||
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<void, { recipient: string; dashboardUid: string; uid: string }>({
|
||||
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<void, { recipient: string; dashboardUid: string; uid: string }>({
|
||||
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<ListPublicDashboardResponse[], void>({
|
||||
query: () => ({
|
||||
url: '/public-dashboards',
|
||||
url: '/dashboards/public-dashboards',
|
||||
}),
|
||||
providesTags: ['AuditTablePublicDashboard'],
|
||||
}),
|
||||
deletePublicDashboard: builder.mutation<void, { dashboard?: DashboardModel; dashboardUid: string; uid: string }>({
|
||||
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;
|
||||
|
@ -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 = () => {
|
||||
</div>
|
||||
<Configuration disabled={disableInputs} onChange={onChange} register={register} />
|
||||
<hr />
|
||||
{hasEmailSharingEnabled && <EmailSharingConfiguration />}
|
||||
<Field label="Dashboard URL" className={styles.publicUrl}>
|
||||
<Input
|
||||
value={generatePublicDashboardUrl(publicDashboard!)}
|
||||
|
@ -0,0 +1,228 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data/src';
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Field,
|
||||
Input,
|
||||
InputControl,
|
||||
RadioButtonGroup,
|
||||
Spinner,
|
||||
useStyles2,
|
||||
} from '@grafana/ui/src';
|
||||
import {
|
||||
useAddEmailSharingMutation,
|
||||
useDeleteEmailSharingMutation,
|
||||
useGetPublicDashboardQuery,
|
||||
useUpdatePublicDashboardMutation,
|
||||
} from 'app/features/dashboard/api/publicDashboardApi';
|
||||
import { useSelector } from 'app/types';
|
||||
|
||||
import { PublicDashboardShareType, validEmailRegex } from '../SharePublicDashboardUtils';
|
||||
|
||||
interface EmailSharingConfigurationForm {
|
||||
shareType: PublicDashboardShareType;
|
||||
email: string;
|
||||
}
|
||||
|
||||
const options: Array<SelectableValue<PublicDashboardShareType>> = [
|
||||
{ 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 (
|
||||
<table className={styles.table} data-testid={selectors.EmailSharingList}>
|
||||
<tbody>
|
||||
{recipients.map((recipient) => (
|
||||
<tr key={recipient}>
|
||||
<td>{recipient}</td>
|
||||
<td>
|
||||
<ButtonGroup className={styles.tableButtonsContainer}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
fill="text"
|
||||
aria-label="Revoke"
|
||||
title="Revoke"
|
||||
size="sm"
|
||||
disabled={isDeleteLoading}
|
||||
onClick={() => onDeleteEmail(recipient)}
|
||||
>
|
||||
Revoke
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
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<EmailSharingConfigurationForm>({
|
||||
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 (
|
||||
<form className={styles.container} onSubmit={handleSubmit(onSubmit)}>
|
||||
<Field label="Can view dashboard">
|
||||
<InputControl
|
||||
name="shareType"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const { ref, ...rest } = field;
|
||||
return (
|
||||
<RadioButtonGroup
|
||||
{...rest}
|
||||
options={options}
|
||||
onChange={(shareType: PublicDashboardShareType) => {
|
||||
setValue('shareType', shareType);
|
||||
onShareTypeChange(shareType);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
{watch('shareType') === PublicDashboardShareType.EMAIL && (
|
||||
<>
|
||||
<Field
|
||||
label="Invite"
|
||||
description="Invite people by email"
|
||||
error={errors.email?.message}
|
||||
invalid={!!errors.email?.message || undefined}
|
||||
>
|
||||
<div className={styles.emailContainer}>
|
||||
<Input
|
||||
className={styles.emailInput}
|
||||
placeholder="email"
|
||||
autoCapitalize="none"
|
||||
{...register('email', {
|
||||
required: 'Email is required',
|
||||
pattern: { value: validEmailRegex, message: 'Invalid email' },
|
||||
})}
|
||||
data-testid={selectors.EmailSharingInput}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={!isValid || isAddEmailLoading}
|
||||
data-testid={selectors.EmailSharingInviteButton}
|
||||
>
|
||||
Invite {isAddEmailLoading && <Spinner />}
|
||||
</Button>
|
||||
</div>
|
||||
</Field>
|
||||
{!!publicDashboard?.recipients?.length && (
|
||||
<EmailList
|
||||
recipients={publicDashboard.recipients}
|
||||
dashboardUid={dashboard.uid}
|
||||
publicDashboardUid={publicDashboard.uid}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
`,
|
||||
});
|
@ -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<React.ComponentProps<typeof ShareModal>>,
|
||||
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(
|
||||
<Provider store={store}>
|
||||
<ShareModal {...newProps} />
|
||||
</Provider>
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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<PublicDashboard>) =>
|
||||
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<React.ComponentProps<typeof ShareModal>>,
|
||||
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(
|
||||
<Provider store={store}>
|
||||
<ShareModal {...newProps} />
|
||||
</Provider>
|
||||
);
|
||||
|
||||
await waitFor(() => screen.getByText('Link'));
|
||||
if (isEnabled) {
|
||||
fireEvent.click(screen.getByText('Public dashboard'));
|
||||
await waitForElementToBeRemoved(screen.getByText('Loading configuration'));
|
||||
}
|
||||
|
||||
return renderResult;
|
||||
};
|
@ -8,14 +8,11 @@ const Body = ({ title }: { title?: string }) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className={styles.title}>Do you want to delete this public dashboard?</p>
|
||||
<p className={styles.description}>
|
||||
{title
|
||||
? `This will delete the public dashboard for "${title}". Your dashboard will not be deleted.`
|
||||
: 'Orphaned public dashboard will be deleted'}
|
||||
</p>
|
||||
</>
|
||||
<p className={styles.description}>
|
||||
{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.'}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
@ -33,9 +30,9 @@ export const DeletePublicDashboardModal = ({
|
||||
body={<Body title={dashboardTitle} />}
|
||||
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};
|
||||
`,
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user