PublicDashboards: Configuration modal redesign (#63211)

Configuration modal redesign

---------

Co-authored-by: kay delaney <kay@grafana.com>
This commit is contained in:
juanicabanas 2023-02-24 12:36:29 -03:00 committed by GitHub
parent af987ae636
commit 9df4a39195
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 837 additions and 728 deletions

View File

@ -18,32 +18,42 @@ e2e.scenario({
// Select public dashboards tab
e2e.pages.ShareDashboardModal.PublicDashboard.Tab().click();
// Saving button should be disabled
e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().should('be.disabled');
// Create button should be disabled
e2e.pages.ShareDashboardModal.PublicDashboard.CreateButton().should('be.disabled');
// Create flow shouldn't show these elements
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput().should('not.exist');
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton().should('not.exist');
e2e.pages.ShareDashboardModal.PublicDashboard.EnableAnnotationsSwitch().should('not.exist');
e2e.pages.ShareDashboardModal.PublicDashboard.EnableTimeRangeSwitch().should('not.exist');
e2e.pages.ShareDashboardModal.PublicDashboard.PauseSwitch().should('not.exist');
e2e.pages.ShareDashboardModal.PublicDashboard.DeleteButton().should('not.exist');
// Acknowledge checkboxes
e2e.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox().should('be.enabled').click({ force: true });
e2e.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox().should('be.enabled').click({ force: true });
e2e.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox().should('be.enabled').click({ force: true });
e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().should('be.disabled');
e2e.pages.ShareDashboardModal.PublicDashboard.CreateButton().should('be.enabled');
// Switch on enabling toggle
e2e.pages.ShareDashboardModal.PublicDashboard.EnableSwitch().should('be.enabled').click({ force: true });
e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().should('be.enabled');
// Save public dashboard
// Create public dashboard
e2e().intercept('POST', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('save');
e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().click();
e2e.pages.ShareDashboardModal.PublicDashboard.CreateButton().click();
e2e().wait('@save');
// Checkboxes should be disabled after saving public dashboard
e2e.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox().should('be.disabled');
e2e.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox().should('be.disabled');
e2e.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox().should('be.disabled');
// These elements shouldn't be rendered after creating public dashboard
e2e.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox().should('not.exist');
e2e.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox().should('not.exist');
e2e.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox().should('not.exist');
e2e.pages.ShareDashboardModal.PublicDashboard.CreateButton().should('not.exist');
// Save public dashboard button should still be enabled
e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().should('be.enabled');
// These elements should be rendered
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.EnableAnnotationsSwitch().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.EnableTimeRangeSwitch().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.PauseSwitch().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.DeleteButton().should('exist');
},
});
@ -70,7 +80,12 @@ e2e.scenario({
e2e.pages.ShareDashboardModal.PublicDashboard.Tab().click();
e2e().wait('@query-public-dashboard');
e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().should('be.enabled');
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.EnableAnnotationsSwitch().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.EnableTimeRangeSwitch().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.PauseSwitch().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.DeleteButton().should('exist');
// Make a request to public dashboards api endpoint without authentication
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput()
@ -106,29 +121,20 @@ e2e.scenario({
e2e.pages.ShareDashboardModal.PublicDashboard.Tab().click();
e2e().wait('@query-public-dashboard');
// All checkboxes should be disabled
e2e.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox().should('be.disabled');
e2e.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox().should('be.disabled');
e2e.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox().should('be.disabled');
// Saving button should be enabled
e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().should('be.enabled');
// save url before disabling public dashboard
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput()
.invoke('val')
.then((text) => e2e().wrap(text).as('url'));
// Switch off enabling toggle
e2e.pages.ShareDashboardModal.PublicDashboard.EnableSwitch().should('be.enabled').click({ force: true });
// Save public dashboard
e2e().intercept('PUT', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards/*').as('update');
e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().click();
// Switch off enabling toggle
e2e.pages.ShareDashboardModal.PublicDashboard.PauseSwitch().should('be.enabled').click({ force: true });
e2e().wait('@update');
// Url should be hidden
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput().should('not.exist');
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput().should('be.disabled');
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton().should('be.disabled');
// Make a request to public dashboards api endpoint without authentication
e2e()

View File

@ -1,8 +1,8 @@
import { e2e } from '@grafana/e2e';
e2e.scenario({
describeName: 'Create a public dashboard with template variables is disabled',
itName: 'Create a public dashboard with template variables is disabled',
describeName: 'Create a public dashboard with template variables shows a template variable warning',
itName: 'Create a public dashboard with template variables shows a template variable warning',
addScenarioDataSource: false,
addScenarioDashBoard: false,
skipScenario: false,
@ -20,10 +20,11 @@ e2e.scenario({
e2e.pages.ShareDashboardModal.PublicDashboard.TemplateVariablesWarningAlert().should('be.visible');
// Configuration elements for public dashboards should not exist
e2e.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox().should('not.exist');
e2e.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox().should('not.exist');
e2e.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox().should('not.exist');
e2e.pages.ShareDashboardModal.PublicDashboard.EnableSwitch().should('not.exist');
e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().should('not.exist');
e2e.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.CreateButton().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.PauseSwitch().should('not.exist');
},
});

View File

@ -190,14 +190,15 @@ export const Pages = {
WillBePublicCheckbox: 'data-testid public dashboard will be public checkbox',
LimitedDSCheckbox: 'data-testid public dashboard limited datasources checkbox',
CostIncreaseCheckbox: 'data-testid public dashboard cost may increase checkbox',
EnableSwitch: 'data-testid public dashboard on off switch',
PauseSwitch: 'data-testid public dashboard pause switch',
EnableAnnotationsSwitch: 'data-testid public dashboard on off switch for annotations',
SaveConfigButton: 'data-testid public dashboard save config button',
CreateButton: 'data-testid public dashboard create button',
DeleteButton: 'data-testid public dashboard delete button',
CopyUrlInput: 'data-testid public dashboard copy url input',
CopyUrlButton: 'data-testid public dashboard copy url button',
TemplateVariablesWarningAlert: 'data-testid public dashboard disabled template variables alert',
UnsupportedDatasourcesWarningAlert: 'data-testid public dashboard unsupported datasources',
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',
},
},

View File

@ -30,7 +30,7 @@ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
return (
<label className={cx(styles.wrapper, className)}>
<div>
<div className={styles.checkboxWrapper}>
<input
type="checkbox"
className={styles.input}
@ -43,10 +43,8 @@ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
/>
<span className={styles.checkmark} />
</div>
<div>
{label && <span className={styles.label}>{label}</span>}
{description && <span className={styles.description}>{description}</span>}
</div>
{label && <span className={styles.label}>{label}</span>}
{description && <span className={styles.description}>{description}</span>}
</label>
);
}
@ -59,12 +57,12 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme2) => {
return {
wrapper: css`
display: inline-flex;
gap: ${theme.spacing(labelPadding)};
align-items: baseline;
display: grid;
align-items: center;
column-gap: ${theme.spacing(labelPadding)};
grid-template-columns: auto 1fr;
grid-template-rows: auto auto;
position: relative;
vertical-align: middle;
font-size: 0;
`,
input: css`
position: absolute;
@ -125,6 +123,12 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme2) => {
}
}
`,
checkboxWrapper: css`
display: flex;
align-items: center;
grid-column-start: 1;
grid-row-start: 1;
`,
checkmark: css`
position: relative; /* Checkbox should be layered on top of the invisible input so it recieves :hover */
z-index: 2;
@ -143,10 +147,11 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme2) => {
label: cx(
labelStyles.label,
css`
grid-column-start: 2;
grid-row-start: 1;
position: relative;
z-index: 2;
cursor: pointer;
top: -3px;
max-width: fit-content;
line-height: ${theme.typography.bodySmall.lineHeight};
margin-bottom: 0;
@ -155,6 +160,8 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme2) => {
description: cx(
labelStyles.description,
css`
grid-column-start: 2;
grid-row-start: 2;
line-height: ${theme.typography.bodySmall.lineHeight};
margin-top: 0; /* The margin effectively comes from the top: -2px on the label above it */
`

View File

@ -8,6 +8,7 @@ import { Tooltip } from '../../../Tooltip/Tooltip';
export interface Props {
label: string;
checked: boolean;
disabled?: boolean;
className?: string;
labelClass?: string;
switchClass?: string;
@ -37,6 +38,7 @@ export class Switch extends PureComponent<Props, State> {
switchClass = '',
label,
checked,
disabled,
transparent,
className,
tooltip,
@ -63,7 +65,13 @@ export class Switch extends PureComponent<Props, State> {
</div>
)}
<div className={switchClassName}>
<input id={labelId} type="checkbox" checked={checked} onChange={this.internalOnChange} />
<input
disabled={disabled}
id={labelId}
type="checkbox"
checked={checked}
onChange={this.internalOnChange}
/>
<span className="gf-form-switch__slider" />
</div>
</label>

View File

@ -4,7 +4,10 @@ import { lastValueFrom } from 'rxjs';
import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime/src';
import { notifyApp } from 'app/core/actions';
import { createErrorNotification, createSuccessNotification } from 'app/core/copy/appNotification';
import { PublicDashboard } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
import {
PublicDashboard,
PublicDashboardSettings,
} from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
import { DashboardModel } from 'app/features/dashboard/state';
import { ListPublicDashboardResponse } from 'app/features/manage-dashboards/types';
@ -55,7 +58,10 @@ export const publicDashboardApi = createApi({
},
providesTags: (result, error, dashboardUid) => [{ type: 'PublicDashboard', id: dashboardUid }],
}),
createPublicDashboard: builder.mutation<PublicDashboard, { dashboard: DashboardModel; payload: PublicDashboard }>({
createPublicDashboard: builder.mutation<
PublicDashboard,
{ dashboard: DashboardModel; payload: Partial<PublicDashboardSettings> }
>({
query: (params) => ({
url: `/uid/${params.dashboard.uid}/public-dashboards`,
method: 'POST',
@ -63,7 +69,7 @@ export const publicDashboardApi = createApi({
}),
async onQueryStarted({ dashboard, payload }, { dispatch, queryFulfilled }) {
const { data } = await queryFulfilled;
dispatch(notifyApp(createSuccessNotification('Public dashboard created!')));
dispatch(notifyApp(createSuccessNotification('Dashboard is public!')));
// Update runtime meta flag
dashboard.updateMeta({
@ -71,7 +77,7 @@ export const publicDashboardApi = createApi({
publicDashboardEnabled: data.isEnabled,
});
},
invalidatesTags: (result, error, { payload }) => [{ type: 'PublicDashboard', id: payload.dashboardUid }],
invalidatesTags: (result, error, { dashboard }) => [{ type: 'PublicDashboard', id: dashboard.uid }],
}),
updatePublicDashboard: builder.mutation<PublicDashboard, { dashboard: DashboardModel; payload: PublicDashboard }>({
query: (params) => ({

View File

@ -1,82 +0,0 @@
import React from 'react';
import { UseFormRegister } from 'react-hook-form';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { Checkbox, FieldSet, HorizontalGroup, LinkButton, VerticalGroup } from '@grafana/ui/src';
import { SharePublicDashboardAcknowledgmentInputs, SharePublicDashboardInputs } from './SharePublicDashboard';
type Acknowledge = {
type: keyof SharePublicDashboardAcknowledgmentInputs;
description: string;
testId: string;
info: {
href: string;
tooltip: string;
};
};
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
const ACKNOWLEDGES: Acknowledge[] = [
{
type: 'publicAcknowledgment',
description: 'Your entire dashboard will be public',
testId: selectors.WillBePublicCheckbox,
info: {
href: 'https://grafana.com/docs/grafana/latest/dashboards/dashboard-public/',
tooltip: 'Learn more about public dashboards',
},
},
{
type: 'dataSourcesAcknowledgment',
description: 'Publishing currently only works with a subset of datasources',
testId: selectors.LimitedDSCheckbox,
info: {
href: 'https://grafana.com/docs/grafana/latest/datasources/',
tooltip: 'Learn more about public datasources',
},
},
{
type: 'usageAcknowledgment',
description: 'Making a dashboard public causes queries to run each time it is viewed, which may increase costs',
testId: selectors.CostIncreaseCheckbox,
info: {
href: 'https://grafana.com/docs/grafana/latest/enterprise/query-caching/',
tooltip: 'Learn more about query caching',
},
},
];
export const AcknowledgeCheckboxes = ({
disabled,
register,
}: {
disabled: boolean;
register: UseFormRegister<SharePublicDashboardInputs>;
}) => (
<>
<p>Before you click Save, please acknowledge the following information:</p>
<FieldSet disabled={disabled}>
<VerticalGroup spacing="md">
{ACKNOWLEDGES.map((acknowledge) => (
<HorizontalGroup key={acknowledge.type} spacing="none" align="center">
<Checkbox
{...register(acknowledge.type)}
label={acknowledge.description}
data-testid={acknowledge.testId}
/>
<LinkButton
variant="primary"
href={acknowledge.info.href}
target="_blank"
fill="text"
icon="info-circle"
rel="noopener noreferrer"
tooltip={acknowledge.info.tooltip}
/>
</HorizontalGroup>
))}
</VerticalGroup>
</FieldSet>
</>
);

View File

@ -0,0 +1,204 @@
import { css } from '@emotion/css';
import cx from 'classnames';
import React, { useContext } from 'react';
import { useForm } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { reportInteraction } from '@grafana/runtime/src';
import {
ClipboardButton,
Field,
HorizontalGroup,
Input,
Label,
ModalsContext,
Spinner,
Switch,
useStyles2,
} from '@grafana/ui/src';
import { Layout } from '@grafana/ui/src/components/Layout/Layout';
import { contextSrv } from '../../../../../../core/services/context_srv';
import { AccessControlAction, useSelector } from '../../../../../../types';
import { DeletePublicDashboardButton } from '../../../../../manage-dashboards/components/PublicDashboardListTable/DeletePublicDashboardButton';
import { isOrgAdmin } from '../../../../../plugins/admin/permissions';
import { useGetPublicDashboardQuery, useUpdatePublicDashboardMutation } from '../../../../api/publicDashboardApi';
import { useIsDesktop } from '../../../../utils/screen';
import { ShareModal } from '../../ShareModal';
import { NoUpsertPermissionsAlert } from '../ModalAlerts/NoUpsertPermissionsAlert';
import { SaveDashboardChangesAlert } from '../ModalAlerts/SaveDashboardChangesAlert';
import { UnsupportedDataSourcesAlert } from '../ModalAlerts/UnsupportedDataSourcesAlert';
import { UnsupportedTemplateVariablesAlert } from '../ModalAlerts/UnsupportedTemplateVariablesAlert';
import {
dashboardHasTemplateVariables,
generatePublicDashboardUrl,
getUnsupportedDashboardDatasources,
} from '../SharePublicDashboardUtils';
import { Configuration } from './Configuration';
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
export interface ConfigPublicDashoardForm {
isAnnotationsEnabled: boolean;
isTimeSelectionEnabled: boolean;
isPaused: boolean;
}
const ConfigPublicDashboard = () => {
const styles = useStyles2(getStyles);
const { showModal, hideModal } = useContext(ModalsContext);
const isDesktop = useIsDesktop();
const hasWritePermissions = contextSrv.hasAccess(AccessControlAction.DashboardsPublicWrite, isOrgAdmin());
const dashboardState = useSelector((store) => store.dashboard);
const dashboard = dashboardState.getModel()!;
const dashboardVariables = dashboard.getVariables();
const unsupportedDataSources = getUnsupportedDashboardDatasources(dashboard.panels);
const { data: publicDashboard, isFetching: isGetLoading } = useGetPublicDashboardQuery(dashboard.uid);
const [update, { isLoading: isUpdateLoading }] = useUpdatePublicDashboardMutation();
const disableInputs = !hasWritePermissions || isUpdateLoading || isGetLoading;
const { handleSubmit, setValue, register } = useForm<ConfigPublicDashoardForm>({
defaultValues: {
isAnnotationsEnabled: publicDashboard?.annotationsEnabled,
isTimeSelectionEnabled: publicDashboard?.timeSelectionEnabled,
isPaused: !publicDashboard?.isEnabled,
},
});
const onUpdate = async (values: ConfigPublicDashoardForm) => {
const { isAnnotationsEnabled, isTimeSelectionEnabled, isPaused } = values;
const req = {
dashboard,
payload: {
...publicDashboard!,
annotationsEnabled: isAnnotationsEnabled,
timeSelectionEnabled: isTimeSelectionEnabled,
isEnabled: !isPaused,
},
};
update(req);
};
const onChange = async (name: keyof ConfigPublicDashoardForm, value: boolean) => {
setValue(name, value);
await handleSubmit((data) => onUpdate(data))();
};
const onDismissDelete = () => {
showModal(ShareModal, {
dashboard,
onDismiss: hideModal,
activeTab: 'share',
});
};
return (
<div>
{hasWritePermissions && dashboard.hasUnsavedChanges() && <SaveDashboardChangesAlert />}
{!hasWritePermissions && <NoUpsertPermissionsAlert mode="edit" />}
{dashboardHasTemplateVariables(dashboardVariables) && <UnsupportedTemplateVariablesAlert />}
{!!unsupportedDataSources.length && (
<UnsupportedDataSourcesAlert unsupportedDataSources={unsupportedDataSources.join(', ')} />
)}
<div className={styles.titleContainer}>
<HorizontalGroup spacing="sm" align="center">
<h4 className={styles.title}>Settings</h4>
{(isUpdateLoading || isGetLoading) && <Spinner size={14} />}
</HorizontalGroup>
</div>
<Configuration disabled={disableInputs} onChange={onChange} register={register} />
<hr />
<Field label="Dashboard URL" className={styles.publicUrl}>
<Input
value={generatePublicDashboardUrl(publicDashboard!)}
readOnly
disabled={!publicDashboard?.isEnabled}
data-testid={selectors.CopyUrlInput}
addonAfter={
<ClipboardButton
data-testid={selectors.CopyUrlButton}
variant="primary"
disabled={!publicDashboard?.isEnabled}
getText={() => generatePublicDashboardUrl(publicDashboard!)}
>
Copy
</ClipboardButton>
}
/>
</Field>
<Layout
orientation={isDesktop ? 0 : 1}
justify={isDesktop ? 'flex-end' : 'flex-start'}
align={isDesktop ? 'center' : 'normal'}
>
<HorizontalGroup spacing="sm">
<Switch
{...register('isPaused')}
disabled={disableInputs}
onChange={(e) => {
reportInteraction('grafana_dashboards_public_enable_clicked', {
action: e.currentTarget.checked ? 'disable' : 'enable',
});
onChange('isPaused', e.currentTarget.checked);
}}
data-testid={selectors.PauseSwitch}
/>
<Label
className={css`
margin-bottom: 0;
`}
>
Pause sharing dashboard
</Label>
</HorizontalGroup>
<HorizontalGroup justify="flex-end">
<DeletePublicDashboardButton
className={cx(styles.deleteButton, { [styles.deleteButtonMobile]: !isDesktop })}
type="button"
disabled={disableInputs}
data-testid={selectors.DeleteButton}
onDismiss={onDismissDelete}
variant="destructive"
fill="outline"
dashboard={dashboard}
publicDashboard={{
uid: publicDashboard!.uid,
dashboardUid: dashboard.uid,
title: dashboard.title,
}}
>
Revoke public URL
</DeletePublicDashboardButton>
</HorizontalGroup>
</Layout>
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
titleContainer: css`
margin-bottom: ${theme.spacing(2)};
`,
title: css`
margin: 0;
`,
publicUrl: css`
width: 100%;
padding-top: ${theme.spacing(1)};
margin-bottom: ${theme.spacing(3)};
`,
deleteButton: css`
margin-left: ${theme.spacing(3)}; ;
`,
deleteButtonMobile: css`
margin-top: ${theme.spacing(2)}; ;
`,
});
export default ConfigPublicDashboard;

View File

@ -7,69 +7,60 @@ import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { reportInteraction } from '@grafana/runtime/src';
import { FieldSet, Label, Switch, TimeRangeInput, useStyles2, VerticalGroup } from '@grafana/ui/src';
import { Layout } from '@grafana/ui/src/components/Layout/Layout';
import { DashboardModel } from 'app/features/dashboard/state';
import { useIsDesktop } from 'app/features/dashboard/utils/screen';
import { getTimeRange } from 'app/features/dashboard/utils/timeRange';
import { SharePublicDashboardInputs } from './SharePublicDashboard';
import { useSelector } from '../../../../../../types';
import { ConfigPublicDashoardForm } from './ConfigPublicDashboard';
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
export const Configuration = ({
disabled,
dashboard,
onChange,
register,
}: {
disabled: boolean;
dashboard: DashboardModel;
register: UseFormRegister<SharePublicDashboardInputs>;
onChange: (name: keyof ConfigPublicDashoardForm, value: boolean) => void;
register: UseFormRegister<ConfigPublicDashoardForm>;
}) => {
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
const styles = useStyles2(getStyles);
const isDesktop = useIsDesktop();
const dashboardState = useSelector((store) => store.dashboard);
const dashboard = dashboardState.getModel()!;
const timeRange = getTimeRange(dashboard.getDefaultTime(), dashboard);
return (
<>
<h4 className={styles.title}>Public dashboard configuration</h4>
<FieldSet disabled={disabled} className={styles.dashboardConfig}>
<VerticalGroup spacing="md">
<Layout orientation={isDesktop ? 0 : 1} spacing="xs" justify="space-between">
<Label description="The public dashboard uses the default time settings of the dashboard">
<Layout orientation={1} spacing="xs" justify="space-between">
<Label description="The public dashboard uses the default time range settings of the dashboard">
Default time range
</Label>
<TimeRangeInput value={timeRange} disabled onChange={() => {}} />
</Layout>
<Layout orientation={isDesktop ? 0 : 1} spacing="xs" justify="space-between">
<Layout orientation={0} spacing="sm">
<Switch
{...register('isTimeSelectionEnabled')}
data-testid={selectors.EnableTimeRangeSwitch}
onChange={(e) => onChange('isTimeSelectionEnabled', e.currentTarget.checked)}
/>
<Label description="Allow viewers to change time range">Time range picker enabled</Label>
<Switch {...register('isTimeRangeEnabled')} data-testid={selectors.EnableTimeRangeSwitch} />
</Layout>
<Layout orientation={isDesktop ? 0 : 1} spacing="xs" justify="space-between">
<Label description="Show annotations on public dashboard">Show annotations</Label>
<Layout orientation={0} spacing="sm">
<Switch
{...register('isAnnotationsEnabled')}
onChange={(e) => {
const { onChange } = register('isAnnotationsEnabled');
reportInteraction('grafana_dashboards_annotations_clicked', {
action: e.currentTarget.checked ? 'enable' : 'disable',
});
onChange(e);
onChange('isAnnotationsEnabled', e.currentTarget.checked);
}}
data-testid={selectors.EnableAnnotationsSwitch}
/>
</Layout>
<Layout orientation={isDesktop ? 0 : 1} spacing="xs" justify="space-between">
<Label description="Configures whether current dashboard can be available publicly">Enabled</Label>
<Switch
{...register('enabledSwitch')}
onChange={(e) => {
const { onChange } = register('enabledSwitch');
reportInteraction('grafana_dashboards_public_enable_clicked', {
action: e.currentTarget.checked ? 'enable' : 'disable',
});
onChange(e);
}}
data-testid={selectors.EnableSwitch}
/>
<Label description="Show annotations on public dashboard">Show annotations</Label>
</Layout>
</VerticalGroup>
</FieldSet>
@ -78,16 +69,7 @@ export const Configuration = ({
};
const getStyles = (theme: GrafanaTheme2) => ({
title: css`
margin-bottom: ${theme.spacing(2)};
`,
dashboardConfig: css`
margin: ${theme.spacing(0, 0, 3, 0)};
`,
timeRange: css`
margin-bottom: ${theme.spacing(0)};
`,
timeRangeDisabledText: css`
font-size: ${theme.typography.bodySmall.fontSize};
`,
});

View File

@ -0,0 +1,94 @@
import { css } from '@emotion/css';
import React from 'react';
import { UseFormRegister } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { Checkbox, FieldSet, HorizontalGroup, LinkButton, useStyles2, VerticalGroup } from '@grafana/ui/src';
import { SharePublicDashboardAcknowledgmentInputs } from './CreatePublicDashboard';
type Acknowledge = {
type: keyof SharePublicDashboardAcknowledgmentInputs;
description: string;
testId: string;
info: {
href: string;
tooltip: string;
};
};
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
const ACKNOWLEDGES: Acknowledge[] = [
{
type: 'publicAcknowledgment',
description: 'Your entire dashboard will be public*',
testId: selectors.WillBePublicCheckbox,
info: {
href: 'https://grafana.com/docs/grafana/latest/dashboards/dashboard-public/',
tooltip: 'Learn more about public dashboards',
},
},
{
type: 'dataSourcesAcknowledgment',
description: 'Publishing currently only works with a subset of data sources*',
testId: selectors.LimitedDSCheckbox,
info: {
href: 'https://grafana.com/docs/grafana/latest/datasources/',
tooltip: 'Learn more about public datasources',
},
},
{
type: 'usageAcknowledgment',
description: 'Making a dashboard public will cause queries to run each time is viewed, which may increase costs*',
testId: selectors.CostIncreaseCheckbox,
info: {
href: 'https://grafana.com/docs/grafana/latest/enterprise/query-caching/',
tooltip: 'Learn more about query caching',
},
},
];
export const AcknowledgeCheckboxes = ({
disabled,
register,
}: {
disabled: boolean;
register: UseFormRegister<SharePublicDashboardAcknowledgmentInputs>;
}) => {
const styles = useStyles2(getStyles);
return (
<>
<p className={styles.title}>Before you make the dashboard public, acknowledge the following:</p>
<FieldSet disabled={disabled}>
<VerticalGroup spacing="md">
{ACKNOWLEDGES.map((acknowledge) => (
<HorizontalGroup key={acknowledge.type} spacing="none" align="center">
<Checkbox
{...register(acknowledge.type, { required: true })}
label={acknowledge.description}
data-testid={acknowledge.testId}
/>
<LinkButton
variant="primary"
href={acknowledge.info.href}
target="_blank"
fill="text"
icon="info-circle"
rel="noopener noreferrer"
tooltip={acknowledge.info.tooltip}
/>
</HorizontalGroup>
))}
</VerticalGroup>
</FieldSet>
</>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
title: css`
font-weight: ${theme.typography.fontWeightBold};
`,
});

View File

@ -0,0 +1,96 @@
import { css } from '@emotion/css';
import React from 'react';
import { FormState, UseFormRegister } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { reportInteraction } from '@grafana/runtime/src';
import { Button, Form, Spinner, useStyles2 } from '@grafana/ui/src';
import { contextSrv } from '../../../../../../core/services/context_srv';
import { AccessControlAction, useSelector } from '../../../../../../types';
import { isOrgAdmin } from '../../../../../plugins/admin/permissions';
import { useCreatePublicDashboardMutation } from '../../../../api/publicDashboardApi';
import { NoUpsertPermissionsAlert } from '../ModalAlerts/NoUpsertPermissionsAlert';
import { UnsupportedDataSourcesAlert } from '../ModalAlerts/UnsupportedDataSourcesAlert';
import { UnsupportedTemplateVariablesAlert } from '../ModalAlerts/UnsupportedTemplateVariablesAlert';
import { dashboardHasTemplateVariables, getUnsupportedDashboardDatasources } from '../SharePublicDashboardUtils';
import { AcknowledgeCheckboxes } from './AcknowledgeCheckboxes';
import { Description } from './Description';
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
export type SharePublicDashboardAcknowledgmentInputs = {
publicAcknowledgment: boolean;
dataSourcesAcknowledgment: boolean;
usageAcknowledgment: boolean;
};
const CreatePublicDashboard = ({ isError }: { isError: boolean }) => {
const styles = useStyles2(getStyles);
const hasWritePermissions = contextSrv.hasAccess(AccessControlAction.DashboardsPublicWrite, isOrgAdmin());
const dashboardState = useSelector((store) => store.dashboard);
const dashboard = dashboardState.getModel()!;
const unsupportedDataSources = getUnsupportedDashboardDatasources(dashboard.panels);
const [createPublicDashboard, { isLoading: isSaveLoading }] = useCreatePublicDashboardMutation();
const disableInputs = !hasWritePermissions || isSaveLoading || isError;
const onCreate = async () => {
reportInteraction('grafana_dashboards_public_create_clicked');
createPublicDashboard({ dashboard, payload: { isEnabled: true } });
};
return (
<div>
<p className={styles.title}>Welcome to public dashboards alpha!</p>
<Description />
{!hasWritePermissions && <NoUpsertPermissionsAlert mode="create" />}
{dashboardHasTemplateVariables(dashboard.getVariables()) && <UnsupportedTemplateVariablesAlert />}
{!!unsupportedDataSources.length && (
<UnsupportedDataSourcesAlert unsupportedDataSources={unsupportedDataSources.join(', ')} />
)}
<Form onSubmit={onCreate} validateOn="onChange" maxWidth="none">
{({
register,
formState: { isValid },
}: {
register: UseFormRegister<SharePublicDashboardAcknowledgmentInputs>;
formState: FormState<SharePublicDashboardAcknowledgmentInputs>;
}) => (
<>
<div className={styles.checkboxes}>
<AcknowledgeCheckboxes disabled={disableInputs} register={register} />
</div>
<div className={styles.buttonContainer}>
<Button type="submit" disabled={disableInputs || !isValid} data-testid={selectors.CreateButton}>
Generate public URL {isSaveLoading && <Spinner className={styles.loadingSpinner} />}
</Button>
</div>
</>
)}
</Form>
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
title: css`
font-size: ${theme.typography.h4.fontSize};
margin: ${theme.spacing(0, 0, 2)};
`,
checkboxes: css`
margin: ${theme.spacing(0, 0, 4)};
`,
buttonContainer: css`
display: flex;
justify-content: end;
`,
loadingSpinner: css`
margin-left: ${theme.spacing(1)};
`,
});
export default CreatePublicDashboard;

View File

@ -0,0 +1,38 @@
import { css } from '@emotion/css';
import cx from 'classnames';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data/src';
import { useStyles2 } from '@grafana/ui/src';
export const Description = () => {
const styles = useStyles2(getStyles);
return (
<div className={styles.container}>
<p className={styles.description}>Currently, we dont support template variables or frontend data sources</p>
<p className={styles.description}>
We&apos;d love your feedback. To share, please comment on this{' '}
<a
href="https://github.com/grafana/grafana/discussions/49253"
target="_blank"
rel="noreferrer"
className={cx('text-link', styles.description)}
>
GitHub discussion
</a>
.
</p>
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
container: css`
margin-bottom: ${theme.spacing(3)};
`,
description: css`
color: ${theme.colors.text.secondary};
margin-bottom: ${theme.spacing(1)};
`,
});

View File

@ -1,22 +0,0 @@
import React from 'react';
export const Description = () => (
<>
<p>
To allow the current dashboard to be published publicly, toggle the switch. For now we do not support template
variables or frontend datasources.
</p>
<p>
We&apos;d love your feedback. To share, please comment on this{' '}
<a
href="https://github.com/grafana/grafana/discussions/49253"
target="_blank"
rel="noreferrer"
className="text-link"
>
GitHub discussion
</a>
.
</p>
</>
);

View File

@ -0,0 +1,16 @@
import React from 'react';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { Alert } from '@grafana/ui/src';
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
export const NoUpsertPermissionsAlert = ({ mode }: { mode: 'create' | 'edit' }) => (
<Alert
severity="info"
title={`You dont have permission to ${mode} a public dashboard`}
data-testid={selectors.NoUpsertPermissionsWarningAlert}
>
Contact your admin to get permission to {mode} create public dashboards
</Alert>
);

View File

@ -0,0 +1,7 @@
import React from 'react';
import { Alert } from '@grafana/ui/src';
export const SaveDashboardChangesAlert = () => (
<Alert title="Please save your dashboard changes before updating the public configuration" severity="warning" />
);

View File

@ -0,0 +1,39 @@
import { css } from '@emotion/css';
import cx from 'classnames';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { Alert, useStyles2 } from '@grafana/ui/src';
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
export const UnsupportedDataSourcesAlert = ({ unsupportedDataSources }: { unsupportedDataSources: string }) => {
const styles = useStyles2(getStyles);
return (
<Alert
severity="warning"
title="Unsupported data sources"
data-testid={selectors.UnsupportedDataSourcesWarningAlert}
>
<p className={styles.unsupportedDataSourceDescription}>
There are data sources in this dashboard that are unsupported for public dashboards. Panels that use these data
sources may not function properly: {unsupportedDataSources}.
</p>
<a
href="https://grafana.com/docs/grafana/latest/dashboards/dashboard-public/"
className={cx('text-link', styles.unsupportedDataSourceDescription)}
>
Read more about supported data sources
</a>
</Alert>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
unsupportedDataSourceDescription: css`
color: ${theme.colors.text.secondary};
margin-bottom: ${theme.spacing(1)};
`,
});

View File

@ -0,0 +1,16 @@
import React from 'react';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { Alert } from '@grafana/ui/src';
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
export const UnsupportedTemplateVariablesAlert = () => (
<Alert
severity="warning"
title="Template variables are not supported"
data-testid={selectors.TemplateVariablesWarningAlert}
>
This public dashboard may not work since it uses template variables
</Alert>
);

View File

@ -17,9 +17,10 @@ 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 { PublicDashboard } from './SharePublicDashboardUtils';
import * as sharePublicDashboardUtils from './SharePublicDashboardUtils';
const server = setupServer();
@ -28,17 +29,39 @@ jest.mock('@grafana/runtime', () => ({
getBackendSrv: () => backendSrv,
}));
const renderSharePublicDashboard = async (props: React.ComponentProps<typeof ShareModal>, isEnabled = true) => {
const store = configureStore();
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 {...props} />
<ShareModal {...newProps} />
</Provider>
);
await waitFor(() => screen.getByText('Link'));
isEnabled && fireEvent.click(screen.getByText('Public dashboard'));
if (isEnabled) {
fireEvent.click(screen.getByText('Public dashboard'));
await waitForElementToBeRemoved(screen.getByText('Loading configuration'));
}
};
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
@ -97,139 +120,54 @@ afterEach(() => {
server.resetHandlers();
});
describe('SharePublic', () => {
const pubdashResponse: PublicDashboard = {
isEnabled: true,
annotationsEnabled: true,
timeSelectionEnabled: true,
uid: 'a-uid',
dashboardUid: '',
accessToken: 'an-access-token',
};
const pubdashResponse: sharePublicDashboardUtils.PublicDashboard = {
isEnabled: true,
annotationsEnabled: true,
timeSelectionEnabled: true,
uid: 'a-uid',
dashboardUid: '',
accessToken: 'an-access-token',
};
beforeEach(() => {
server.use(
rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
...pubdashResponse,
dashboardUid: req.params.dashboardUid,
})
);
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,
})
);
});
it('does not render share panel when public dashboards feature is disabled', async () => {
config.featureToggles.publicDashboards = false;
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} }, false);
expect(screen.getByRole('tablist')).toHaveTextContent('Link');
expect(screen.getByRole('tablist')).not.toHaveTextContent('Public dashboard');
const getNonExistentPublicDashboardResponse = () =>
rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => {
return res(
ctx.status(404),
ctx.json({
message: 'Public dashboard not found',
messageId: 'publicdashboards.notFound',
statusCode: 404,
traceID: '',
})
);
});
it('renders share panel when public dashboards feature is enabled', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
expect(screen.getByRole('tablist')).toHaveTextContent('Link');
expect(screen.getByRole('tablist')).toHaveTextContent('Public dashboard');
fireEvent.click(screen.getByText('Public dashboard'));
await screen.findByText('Welcome to Grafana public dashboards alpha!');
expect(screen.getByText('Create public dashboard')).toBeInTheDocument();
expect(screen.queryByTestId(selectors.DeleteButton)).not.toBeInTheDocument();
const getErrorPublicDashboardResponse = () =>
rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => {
return res(ctx.status(500));
});
it('renders public dashboard modal without delete button because no public dashboard was already created', async () => {
const alertTests = () => {
it('when user has no write permissions, warning is shown', async () => {
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(false);
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
expect(screen.getByRole('tablist')).toHaveTextContent('Link');
expect(screen.getByRole('tablist')).toHaveTextContent('Public dashboard');
fireEvent.click(screen.getByText('Public dashboard'));
await screen.findByText('Welcome to Grafana public dashboards alpha!');
expect(screen.getByText('Create public dashboard')).toBeInTheDocument();
expect(screen.queryByTestId(selectors.DeleteButton)).not.toBeInTheDocument();
await renderSharePublicDashboard();
expect(screen.queryByTestId(selectors.NoUpsertPermissionsWarningAlert)).toBeInTheDocument();
});
it('renders default relative time in input', async () => {
expect(mockDashboard.time).toEqual({ from: 'now-6h', to: 'now' });
it('when dashboard has template variables, warning is shown', async () => {
jest.spyOn(sharePublicDashboardUtils, 'dashboardHasTemplateVariables').mockReturnValue(true);
//@ts-ignore
mockDashboard.originalTime = { from: 'now-6h', to: 'now' };
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await screen.findByText('Welcome to Grafana public dashboards alpha!');
expect(screen.getByText('Last 6 hours')).toBeInTheDocument();
});
it('renders default absolute time in input 2', async () => {
mockDashboard.time = { from: '2022-08-30T03:00:00.000Z', to: '2022-09-04T02:59:59.000Z' };
//@ts-ignore
mockDashboard.originalTime = { from: '2022-08-30T06:00:00.000Z', to: '2022-09-04T06:59:59.000Z' };
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await screen.findByText('Welcome to Grafana public dashboards alpha!');
expect(screen.getByText('2022-08-30 00:00:00 to 2022-09-04 00:59:59')).toBeInTheDocument();
});
it('when modal is opened, then loader spinner appears and inputs are disabled', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
screen.getAllByTestId('Spinner');
expect(screen.getByText('Create public dashboard')).toBeInTheDocument();
expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.EnableTimeRangeSwitch)).toBeDisabled();
expect(screen.getByTestId(selectors.EnableSwitch)).toBeDisabled();
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
expect(screen.queryByTestId(selectors.DeleteButton)).not.toBeInTheDocument();
});
it('when fetch errors happen, then all inputs remain disabled', async () => {
server.use(
rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => {
return res(ctx.status(500));
})
);
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.EnableSwitch)).toBeDisabled();
expect(screen.getByTestId(selectors.EnableTimeRangeSwitch)).toBeDisabled();
expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeDisabled();
expect(screen.getByText('Create public dashboard')).toBeInTheDocument();
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
expect(screen.queryByTestId(selectors.DeleteButton)).not.toBeInTheDocument();
});
});
describe('SharePublic - New config setup', () => {
beforeEach(() => {
server.use(
rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => {
return res(
ctx.status(404),
ctx.json({
message: 'Public dashboard not found',
messageId: 'publicdashboards.notFound',
statusCode: 404,
traceID: '',
})
);
})
);
});
it('when modal is opened, then save button is disabled', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
await renderSharePublicDashboard();
expect(screen.queryByTestId(selectors.TemplateVariablesWarningAlert)).toBeInTheDocument();
});
it('when dashboard has unsupported datasources, warning is shown', async () => {
const panelModel = {
@ -244,108 +182,136 @@ describe('SharePublic - New config setup', () => {
panels: [panelModel],
});
await renderSharePublicDashboard({ panel: mockPanel, dashboard, onDismiss: () => {} });
expect(screen.queryByTestId(selectors.UnsupportedDatasourcesWarningAlert)).toBeInTheDocument();
await renderSharePublicDashboard({ dashboard });
expect(screen.queryByTestId(selectors.UnsupportedDataSourcesWarningAlert)).toBeInTheDocument();
});
it('when fetch is done, then no loader spinner appears, inputs are enabled and save button is disabled', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
};
describe('SharePublic', () => {
beforeEach(() => {
server.use(getExistentPublicDashboardResponse());
});
it('does not render share panel when public dashboards feature is disabled', async () => {
config.featureToggles.publicDashboards = false;
await renderSharePublicDashboard(undefined, false);
expect(screen.getByRole('tablist')).toHaveTextContent('Link');
expect(screen.getByRole('tablist')).not.toHaveTextContent('Public dashboard');
});
it('renders default relative time in input', async () => {
expect(mockDashboard.time).toEqual({ from: 'now-6h', to: 'now' });
//@ts-ignore
mockDashboard.originalTime = { from: 'now-6h', to: 'now' };
await renderSharePublicDashboard();
expect(screen.getByText('Last 6 hours')).toBeInTheDocument();
});
it('renders default absolute time in input 2', async () => {
mockDashboard.time = { from: '2022-08-30T03:00:00.000Z', to: '2022-09-04T02:59:59.000Z' };
//@ts-ignore
mockDashboard.originalTime = { from: '2022-08-30T06:00:00.000Z', to: '2022-09-04T06:59:59.000Z' };
await renderSharePublicDashboard();
expect(screen.getByText('2022-08-30 00:00:00 to 2022-09-04 00:59:59')).toBeInTheDocument();
});
it('when modal is opened, then checkboxes are enabled but create button is disabled', async () => {
server.use(getNonExistentPublicDashboardResponse());
await renderSharePublicDashboard();
expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeEnabled();
expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeEnabled();
expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeEnabled();
expect(screen.getByTestId(selectors.EnableSwitch)).toBeEnabled();
expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeEnabled();
expect(screen.getByTestId(selectors.EnableTimeRangeSwitch)).toBeEnabled();
expect(screen.getByTestId(selectors.CreateButton)).toBeDisabled();
expect(screen.queryByTestId(selectors.DeleteButton)).not.toBeInTheDocument();
expect(screen.getByText('Create public dashboard')).toBeInTheDocument();
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
});
it('when checkboxes are filled, then save button remains disabled', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
it('when fetch errors happen, then all inputs remain disabled', async () => {
server.use(getErrorPublicDashboardResponse());
fireEvent.click(screen.getByTestId(selectors.WillBePublicCheckbox));
fireEvent.click(screen.getByTestId(selectors.LimitedDSCheckbox));
fireEvent.click(screen.getByTestId(selectors.CostIncreaseCheckbox));
await renderSharePublicDashboard();
expect(screen.getByText('Create public dashboard')).toBeInTheDocument();
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
});
it('when checkboxes and switch are filled, then save button is enabled', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
fireEvent.click(screen.getByTestId(selectors.WillBePublicCheckbox));
fireEvent.click(screen.getByTestId(selectors.LimitedDSCheckbox));
fireEvent.click(screen.getByTestId(selectors.CostIncreaseCheckbox));
fireEvent.click(screen.getByTestId(selectors.EnableSwitch));
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeEnabled();
});
it('when hasPublicDashboard flag is false, then button text is Create', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
expect(screen.getByText('Create public dashboard')).toBeInTheDocument();
expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.CreateButton)).toBeDisabled();
expect(screen.queryByTestId(selectors.DeleteButton)).not.toBeInTheDocument();
});
});
describe('SharePublic - Already persisted', () => {
const pubdashResponse: PublicDashboard = {
isEnabled: true,
annotationsEnabled: true,
timeSelectionEnabled: true,
uid: 'a-uid',
dashboardUid: '',
accessToken: 'an-access-token',
};
describe('SharePublic - New config setup', () => {
beforeEach(() => {
server.use(
rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
...pubdashResponse,
dashboardUid: req.params.dashboardUid,
})
);
})
);
server.use(getNonExistentPublicDashboardResponse());
});
it('when modal is opened, then save button and delete button are enabled', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
it('renders when public dashboards feature is enabled', async () => {
await renderSharePublicDashboard();
expect(screen.getByTestId(selectors.DeleteButton)).toBeEnabled();
expect(screen.getByText('Save public dashboard')).toBeInTheDocument();
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeEnabled();
});
it('delete button is not rendered because lack of permissions', async () => {
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(false);
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
await screen.findByText('Welcome to public dashboards alpha!');
expect(screen.getByText('Generate public URL')).toBeInTheDocument();
expect(screen.queryByTestId(selectors.WillBePublicCheckbox)).toBeInTheDocument();
expect(screen.queryByTestId(selectors.LimitedDSCheckbox)).toBeInTheDocument();
expect(screen.queryByTestId(selectors.CostIncreaseCheckbox)).toBeInTheDocument();
expect(screen.queryByTestId(selectors.CreateButton)).toBeInTheDocument();
expect(screen.queryByTestId(selectors.DeleteButton)).not.toBeInTheDocument();
});
it('when modal is opened, then annotations toggle is enabled and checked when its enabled in the db', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
it('when modal is opened, then create button is disabled', async () => {
await renderSharePublicDashboard();
expect(screen.getByTestId(selectors.CreateButton)).toBeDisabled();
});
it('when checkboxes are filled, then create button is enabled', async () => {
await renderSharePublicDashboard();
fireEvent.click(screen.getByTestId(selectors.WillBePublicCheckbox));
fireEvent.click(screen.getByTestId(selectors.LimitedDSCheckbox));
fireEvent.click(screen.getByTestId(selectors.CostIncreaseCheckbox));
await waitFor(() => expect(screen.getByTestId(selectors.CreateButton)).toBeEnabled());
});
alertTests();
});
describe('SharePublic - Already persisted', () => {
beforeEach(() => {
server.use(getExistentPublicDashboardResponse());
});
it('when modal is opened, then delete button is enabled', async () => {
await renderSharePublicDashboard();
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
expect(screen.getByTestId(selectors.DeleteButton)).toBeEnabled();
});
it('when fetch is done, then inputs are checked and delete button is enabled', async () => {
await renderSharePublicDashboard();
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
expect(screen.getByTestId(selectors.EnableTimeRangeSwitch)).toBeEnabled();
expect(screen.getByTestId(selectors.EnableTimeRangeSwitch)).toBeChecked();
expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeEnabled();
expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeChecked();
});
it('when modal is opened, then time range switch is enabled and checked when its checked in the db', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
const enableTimeRangeSwitch = screen.getByTestId(selectors.EnableTimeRangeSwitch);
expect(enableTimeRangeSwitch).toBeEnabled();
expect(enableTimeRangeSwitch).toBeChecked();
});
expect(screen.getByTestId(selectors.PauseSwitch)).toBeEnabled();
expect(screen.getByTestId(selectors.PauseSwitch)).not.toBeChecked();
expect(screen.getByTestId(selectors.DeleteButton)).toBeEnabled();
});
it('inputs and delete button are disabled because of lack of permissions', async () => {
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(false);
await renderSharePublicDashboard();
expect(screen.getByTestId(selectors.EnableTimeRangeSwitch)).toBeDisabled();
expect(screen.getByTestId(selectors.EnableTimeRangeSwitch)).toBeChecked();
expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeDisabled();
expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeChecked();
expect(screen.getByTestId(selectors.PauseSwitch)).toBeDisabled();
expect(screen.getByTestId(selectors.PauseSwitch)).not.toBeChecked();
expect(screen.queryByTestId(selectors.DeleteButton)).toBeDisabled();
});
it('when modal is opened, then time range switch is enabled and not checked when its not checked in the db', async () => {
server.use(
rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => {
@ -359,33 +325,18 @@ describe('SharePublic - Already persisted', () => {
})
);
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await renderSharePublicDashboard();
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
const enableTimeRangeSwitch = screen.getByTestId(selectors.EnableTimeRangeSwitch);
expect(enableTimeRangeSwitch).toBeEnabled();
expect(enableTimeRangeSwitch).not.toBeChecked();
});
it('when fetch is done, then loader spinner is gone, inputs are disabled and save button is enabled', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.EnableSwitch)).toBeEnabled();
expect(screen.getByTestId(selectors.EnableTimeRangeSwitch)).toBeEnabled();
expect(screen.getByText('Save public dashboard')).toBeInTheDocument();
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeEnabled();
expect(screen.getByTestId(selectors.DeleteButton)).toBeEnabled();
});
it('when pubdash is enabled, then link url is available', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
await renderSharePublicDashboard();
expect(screen.getByTestId(selectors.CopyUrlInput)).toBeInTheDocument();
});
it('when pubdash is disabled in the db, then link url is not available and annotations toggle is disabled', async () => {
it('when pubdash is disabled in the db, then link url is not copyable and switch is checked', async () => {
server.use(
rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => {
return res(
@ -401,22 +352,13 @@ describe('SharePublic - Already persisted', () => {
})
);
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await renderSharePublicDashboard();
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
expect(screen.queryByTestId(selectors.CopyUrlInput)).not.toBeInTheDocument();
expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).not.toBeChecked();
});
it('when pubdash is disabled by the user, then link url is not available', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
expect(screen.queryByTestId(selectors.CopyUrlInput)).toBeInTheDocument();
expect(screen.queryByTestId(selectors.CopyUrlButton)).not.toBeChecked();
fireEvent.click(screen.getByTestId(selectors.EnableSwitch));
expect(screen.queryByTestId(selectors.CopyUrlInput)).not.toBeInTheDocument();
});
it('when hasPublicDashboard flag is true, then button text is Save', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
expect(screen.getByText('Save public dashboard')).toBeInTheDocument();
expect(screen.getByTestId(selectors.PauseSwitch)).toBeChecked();
});
alertTests();
});

View File

@ -1,307 +1,60 @@
import { css } from '@emotion/css';
import React, { useContext, useEffect, useMemo } from 'react';
import { useForm } from 'react-hook-form';
import { Subscription } from 'rxjs';
import React, { useEffect } from 'react';
import { GrafanaTheme2 } from '@grafana/data/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { reportInteraction } from '@grafana/runtime/src';
import {
Alert,
Button,
ClipboardButton,
Field,
HorizontalGroup,
Input,
useStyles2,
Spinner,
ModalsContext,
useForceUpdate,
} from '@grafana/ui/src';
import { Layout } from '@grafana/ui/src/components/Layout/Layout';
import { contextSrv } from 'app/core/services/context_srv';
import {
useGetPublicDashboardQuery,
useCreatePublicDashboardMutation,
useUpdatePublicDashboardMutation,
} from 'app/features/dashboard/api/publicDashboardApi';
import { AcknowledgeCheckboxes } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/AcknowledgeCheckboxes';
import { Configuration } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/Configuration';
import { Description } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/Description';
import {
dashboardHasTemplateVariables,
generatePublicDashboardUrl,
getUnsupportedDashboardDatasources,
publicDashboardPersisted,
} from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
import { Spinner, useStyles2 } from '@grafana/ui/src';
import { useGetPublicDashboardQuery } from 'app/features/dashboard/api/publicDashboardApi';
import { publicDashboardPersisted } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
import { ShareModalTabProps } from 'app/features/dashboard/components/ShareModal/types';
import { useIsDesktop } from 'app/features/dashboard/utils/screen';
import { DeletePublicDashboardButton } from 'app/features/manage-dashboards/components/PublicDashboardListTable/DeletePublicDashboardButton';
import { isOrgAdmin } from 'app/features/plugins/admin/permissions';
import { AccessControlAction } from 'app/types';
import { DashboardMetaChangedEvent } from '../../../../../types/events';
import { ShareModal } from '../ShareModal';
import { HorizontalGroup } from '../../../../plugins/admin/components/HorizontalGroup';
import ConfigPublicDashboard from './ConfigPublicDashboard/ConfigPublicDashboard';
import CreatePublicDashboard from './CreatePublicDashboard/CreatePublicDashboard';
interface Props extends ShareModalTabProps {}
export type SharePublicDashboardAcknowledgmentInputs = {
publicAcknowledgment: boolean;
dataSourcesAcknowledgment: boolean;
usageAcknowledgment: boolean;
const Loader = () => {
const styles = useStyles2(getStyles);
return (
<HorizontalGroup className={styles.loadingContainer}>
<>
Loading configuration
<Spinner size={20} className={styles.spinner} />
</>
</HorizontalGroup>
);
};
export type SharePublicDashboardInputs = {
isAnnotationsEnabled: boolean;
enabledSwitch: boolean;
isTimeRangeEnabled: boolean;
} & SharePublicDashboardAcknowledgmentInputs;
export const SharePublicDashboard = (props: Props) => {
const forceUpdate = useForceUpdate();
const styles = useStyles2(getStyles);
const { showModal, hideModal } = useContext(ModalsContext);
const isDesktop = useIsDesktop();
const dashboardVariables = props.dashboard.getVariables();
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
const {
isLoading: isGetLoading,
data: publicDashboard,
isError: isGetError,
isFetching,
} = useGetPublicDashboardQuery(props.dashboard.uid);
const {
reset,
handleSubmit,
watch,
register,
formState: { dirtyFields },
} = useForm<SharePublicDashboardInputs>({
defaultValues: {
publicAcknowledgment: false,
dataSourcesAcknowledgment: false,
usageAcknowledgment: false,
isAnnotationsEnabled: false,
isTimeRangeEnabled: false,
enabledSwitch: false,
},
});
const [createPublicDashboard, { isLoading: isSaveLoading }] = useCreatePublicDashboardMutation();
const [updatePublicDashboard, { isLoading: isUpdateLoading }] = useUpdatePublicDashboardMutation();
const { data: publicDashboard, isLoading, isError } = useGetPublicDashboardQuery(props.dashboard.uid);
useEffect(() => {
const eventSubs = new Subscription();
eventSubs.add(props.dashboard.events.subscribe(DashboardMetaChangedEvent, forceUpdate));
reportInteraction('grafana_dashboards_public_share_viewed');
return () => eventSubs.unsubscribe();
}, [props.dashboard.events, forceUpdate]);
useEffect(() => {
const isPublicDashboardPersisted = publicDashboardPersisted(publicDashboard);
reset({
publicAcknowledgment: isPublicDashboardPersisted,
dataSourcesAcknowledgment: isPublicDashboardPersisted,
usageAcknowledgment: isPublicDashboardPersisted,
isAnnotationsEnabled: publicDashboard?.annotationsEnabled,
isTimeRangeEnabled: publicDashboard?.timeSelectionEnabled,
enabledSwitch: publicDashboard?.isEnabled,
});
}, [publicDashboard, reset]);
const isLoading = isGetLoading || isSaveLoading || isUpdateLoading;
const hasWritePermissions = contextSrv.hasAccess(AccessControlAction.DashboardsPublicWrite, isOrgAdmin());
const acknowledged =
watch('publicAcknowledgment') && watch('dataSourcesAcknowledgment') && watch('usageAcknowledgment');
const isSaveDisabled = useMemo(
() =>
!hasWritePermissions ||
!acknowledged ||
props.dashboard.hasUnsavedChanges() ||
isLoading ||
isFetching ||
isGetError ||
(!publicDashboardPersisted(publicDashboard) && !dirtyFields.enabledSwitch),
[
hasWritePermissions,
acknowledged,
props.dashboard,
isLoading,
isGetError,
publicDashboard,
isFetching,
dirtyFields.enabledSwitch,
]
);
const isDeleteDisabled = isLoading || isFetching || isGetError;
const onSavePublicConfig = async (values: SharePublicDashboardInputs) => {
reportInteraction('grafana_dashboards_public_create_clicked');
const req = {
dashboard: props.dashboard,
payload: {
...publicDashboard!,
isEnabled: values.enabledSwitch,
annotationsEnabled: values.isAnnotationsEnabled,
timeSelectionEnabled: values.isTimeRangeEnabled,
},
};
// create or update based on whether we have existing uid
!!publicDashboard ? updatePublicDashboard(req) : createPublicDashboard(req);
};
const onDismissDelete = () => {
showModal(ShareModal, {
dashboard: props.dashboard,
onDismiss: hideModal,
activeTab: 'share',
});
};
}, []);
return (
<>
<HorizontalGroup>
<p
className={css`
margin: 0;
`}
>
Welcome to Grafana public dashboards alpha!
</p>
{(isGetLoading || isFetching) && <Spinner />}
</HorizontalGroup>
<div className={styles.content}>
{getUnsupportedDashboardDatasources(props.dashboard.panels).length > 0 ? (
<Alert
severity="warning"
title="Unsupported Datasources"
data-testid={selectors.UnsupportedDatasourcesWarningAlert}
>
<div>
{`There are datasources in this dashboard that are unsupported for public dashboards. Panels that use these datasources may not function properly: ${getUnsupportedDashboardDatasources(
props.dashboard.panels
).join(', ')}. See the `}
<a href="https://grafana.com/docs/grafana/latest/dashboards/dashboard-public/" className="text-link">
docs
</a>{' '}
for supported datasources.
</div>
</Alert>
) : null}
{dashboardHasTemplateVariables(dashboardVariables) && !publicDashboardPersisted(publicDashboard) ? (
<Alert
severity="warning"
title="dashboard cannot be public"
data-testid={selectors.TemplateVariablesWarningAlert}
>
This dashboard cannot be made public because it has template variables
</Alert>
) : (
<form onSubmit={handleSubmit(onSavePublicConfig)}>
<Description />
<hr />
<div className={styles.checkboxes}>
<AcknowledgeCheckboxes
disabled={publicDashboardPersisted(publicDashboard) || !hasWritePermissions || isLoading || isGetError}
register={register}
/>
</div>
<hr />
<Configuration
register={register}
dashboard={props.dashboard}
disabled={!hasWritePermissions || isLoading || isGetError}
/>
{publicDashboardPersisted(publicDashboard) && watch('enabledSwitch') && (
<Field label="Link URL" className={styles.publicUrl}>
<Input
disabled={isLoading}
value={generatePublicDashboardUrl(publicDashboard!)}
readOnly
data-testid={selectors.CopyUrlInput}
addonAfter={
<ClipboardButton
data-testid={selectors.CopyUrlButton}
variant="primary"
icon="copy"
getText={() => generatePublicDashboardUrl(publicDashboard!)}
>
Copy
</ClipboardButton>
}
/>
</Field>
)}
{hasWritePermissions ? (
props.dashboard.hasUnsavedChanges() ? (
<Alert
title="Please save your dashboard changes before updating the public configuration"
severity="warning"
/>
) : (
dashboardHasTemplateVariables(dashboardVariables) && (
<Alert
title="This public dashboard may not work since it uses template variables"
severity="warning"
/>
)
)
) : (
<Alert title="You don't have permissions to create or update a public dashboard" severity="warning" />
)}
<HorizontalGroup>
<Layout orientation={isDesktop ? 0 : 1}>
<Button type="submit" disabled={isSaveDisabled} data-testid={selectors.SaveConfigButton}>
{!!publicDashboard ? 'Save public dashboard' : 'Create public dashboard'}
</Button>
{publicDashboard && hasWritePermissions && (
<DeletePublicDashboardButton
type="button"
disabled={isDeleteDisabled}
data-testid={selectors.DeleteButton}
onDismiss={onDismissDelete}
variant="destructive"
dashboard={props.dashboard}
publicDashboard={{
uid: publicDashboard.uid,
dashboardUid: props.dashboard.uid,
title: props.dashboard.title,
}}
>
Delete public dashboard
</DeletePublicDashboardButton>
)}
</Layout>
{(isSaveLoading || isFetching) && <Spinner />}
</HorizontalGroup>
</form>
)}
</div>
{isLoading ? (
<Loader />
) : !publicDashboardPersisted(publicDashboard) ? (
<CreatePublicDashboard isError={isError} />
) : (
<ConfigPublicDashboard />
)}
</>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
content: css`
margin: ${theme.spacing(1, 0, 0, 0)};
loadingContainer: css`
height: 280px;
align-items: center;
justify-content: center;
gap: ${theme.spacing(1)};
`,
checkboxes: css`
margin: ${theme.spacing(2, 0)};
`,
timeRange: css`
padding: ${theme.spacing(1, 1)};
margin: ${theme.spacing(0, 0, 2, 0)};
`,
publicUrl: css`
width: 100%;
margin-bottom: ${theme.spacing(0, 0, 3, 0)};
spinner: css`
margin-bottom: ${theme.spacing(0)};
`,
});

View File

@ -1,24 +1,21 @@
import { getConfig } from 'app/core/config';
import { VariableModel } from 'app/features/variables/types';
import { DashboardDataDTO, DashboardMeta } from 'app/types/dashboard';
import { PanelModel } from '../../../state';
import { supportedDatasources } from './SupportedPubdashDatasources';
export interface PublicDashboard {
accessToken?: string;
export interface PublicDashboardSettings {
annotationsEnabled: boolean;
isEnabled: boolean;
uid: string;
dashboardUid: string;
timeSettings?: object;
timeSelectionEnabled: boolean;
}
export interface DashboardResponse {
dashboard: DashboardDataDTO;
meta: DashboardMeta;
export interface PublicDashboard extends PublicDashboardSettings {
accessToken?: string;
uid: string;
dashboardUid: string;
timeSettings?: object;
}
// Instance methods

View File

@ -41,8 +41,8 @@ export const DeletePublicDashboardButton = ({
<ModalsController>
{({ showModal, hideModal }) => (
<Button
aria-label="Delete public dashboard"
title="Delete public dashboard"
aria-label="Revoke public URL"
title="Revoke public URL"
onClick={() =>
showModal(DeletePublicDashboardModal, {
dashboardTitle: publicDashboard.title,