mirror of https://github.com/grafana/grafana
PublicDashboards: Email sharing (#63762)
Feature for sharing a public dashboard by emailpull/63853/head
parent
89ad81b15a
commit
4e74768530
@ -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; |
||||
`,
|
||||
}); |
||||
@ -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; |
||||
}; |
||||
Loading…
Reference in new issue