mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
PublicDashboards: Configuration modal redesign (#63211)
Configuration modal redesign --------- Co-authored-by: kay delaney <kay@grafana.com>
This commit is contained in:
parent
af987ae636
commit
9df4a39195
@ -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()
|
||||
|
@ -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');
|
||||
},
|
||||
});
|
||||
|
@ -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',
|
||||
},
|
||||
},
|
||||
|
@ -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 */
|
||||
`
|
||||
|
@ -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>
|
||||
|
@ -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) => ({
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
@ -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;
|
@ -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};
|
||||
`,
|
||||
});
|
@ -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};
|
||||
`,
|
||||
});
|
@ -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;
|
@ -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 don’t support template variables or frontend data sources</p>
|
||||
<p className={styles.description}>
|
||||
We'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)};
|
||||
`,
|
||||
});
|
@ -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'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>
|
||||
</>
|
||||
);
|
@ -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 don’t have permission to ${mode} a public dashboard`}
|
||||
data-testid={selectors.NoUpsertPermissionsWarningAlert}
|
||||
>
|
||||
Contact your admin to get permission to {mode} create public dashboards
|
||||
</Alert>
|
||||
);
|
@ -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" />
|
||||
);
|
@ -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)};
|
||||
`,
|
||||
});
|
@ -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>
|
||||
);
|
@ -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();
|
||||
});
|
||||
|
@ -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)};
|
||||
`,
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user