Public Dashboard: Redesign modal (v2) (#71151)

* Update public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/SettingsBar.tsx

Co-authored-by: Juan Cabanas <juan.cabanas@grafana.com>

* revert modal styling and add specific styling to Sharing

* Update public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/SettingsBar.tsx

Co-authored-by: Juan Cabanas <juan.cabanas@grafana.com>

* functions > const

* put a gat between all items in email config, instead of margins for each item

* fix html semantic elements

* ad theme to class component ShareModal

* add labels

* fix failing tests; now Settings has a summary and has to be opened to be able to see the On/Off toggles

* fix dashboard-public-create test with settings dropdown
This commit is contained in:
Polina Boneva 2023-07-25 13:17:39 +03:00 committed by GitHub
parent edb7d0e0d8
commit 1110cb4d44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 396 additions and 83 deletions

View File

@ -54,10 +54,14 @@ e2e.scenario({
// 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');
e2e.pages.ShareDashboardModal.PublicDashboard.SettingsDropdown().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.SettingsDropdown().click();
// There elements should be rendered once the Settings dropdown is opened
e2e.pages.ShareDashboardModal.PublicDashboard.EnableAnnotationsSwitch().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.EnableTimeRangeSwitch().should('exist');
},
});
@ -91,10 +95,14 @@ e2e.scenario({
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');
e2e.pages.ShareDashboardModal.PublicDashboard.SettingsDropdown().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.SettingsDropdown().click();
// There elements should be rendered once the Settings dropdown is opened
e2e.pages.ShareDashboardModal.PublicDashboard.EnableTimeRangeSwitch().should('exist');
e2e.pages.ShareDashboardModal.PublicDashboard.EnableAnnotationsSwitch().should('exist');
// Make a request to public dashboards api endpoint without authentication
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput()

View File

@ -213,6 +213,7 @@ export const Pages = {
DeleteButton: 'data-testid public dashboard delete button',
CopyUrlInput: 'data-testid public dashboard copy url input',
CopyUrlButton: 'data-testid public dashboard copy url button',
SettingsDropdown: 'data-testid public dashboard settings dropdown',
TemplateVariablesWarningAlert: 'data-testid public dashboard disabled template variables alert',
UnsupportedDataSourcesWarningAlert: 'data-testid public dashboard unsupported data sources alert',
NoUpsertPermissionsWarningAlert: 'data-testid public dashboard no upsert permissions alert',

View File

@ -125,6 +125,7 @@ const getStyles = (
return {
alert: css({
label: 'alert',
flexGrow: 1,
position: 'relative',
borderRadius,

View File

@ -1,7 +1,7 @@
import { css, cx } from '@emotion/css';
import React, { FormEvent, MouseEvent, useState } from 'react';
import { dateMath, dateTime, getDefaultTimeRange, GrafanaTheme2, TimeRange, TimeZone } from '@grafana/data';
import { dateTime, getDefaultTimeRange, GrafanaTheme2, TimeRange, TimeZone } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { stylesFactory } from '../../themes';
@ -10,13 +10,10 @@ import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper'
import { Icon } from '../Icon/Icon';
import { getInputStyles } from '../Input/Input';
import { TimePickerButtonLabel } from './TimeRangePicker';
import { TimePickerContent } from './TimeRangePicker/TimePickerContent';
import { TimeRangeLabel } from './TimeRangePicker/TimeRangeLabel';
import { quickOptions } from './options';
const isValidTimeRange = (range: TimeRange) => {
return dateMath.isValid(range.from) && dateMath.isValid(range.to);
};
import { isValidTimeRange } from './utils';
export interface TimeRangeInputProps {
value: TimeRange;
@ -87,11 +84,8 @@ export const TimeRangeInput = ({
onClick={onOpen}
>
{showIcon && <Icon name="clock-nine" size={'sm'} className={styles.icon} />}
{isValidTimeRange(value) ? (
<TimePickerButtonLabel value={value} timeZone={timeZone} />
) : (
<span className={styles.placeholder}>{placeholder}</span>
)}
<TimeRangeLabel value={value} timeZone={timeZone} placeholder={placeholder} />
{!disabled && (
<span className={styles.caretIcon}>

View File

@ -0,0 +1,46 @@
import { css } from '@emotion/css';
import React, { memo } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../../../src/themes';
import { TimePickerButtonLabel, TimeRangePickerProps } from '../TimeRangePicker';
import { isValidTimeRange } from '../utils';
type LabelProps = Pick<TimeRangePickerProps, 'hideText' | 'value' | 'timeZone'> & {
placeholder?: string;
className?: string;
};
export const TimeRangeLabel = memo<LabelProps>(function TimePickerLabel({
hideText,
value,
timeZone = 'browser',
placeholder = 'No time range selected',
className,
}) {
const styles = useStyles2(getLabelStyles);
if (hideText) {
return null;
}
return (
<span className={className}>
{isValidTimeRange(value) ? (
<TimePickerButtonLabel value={value} timeZone={timeZone} />
) : (
<span className={styles.placeholder}>{placeholder}</span>
)}
</span>
);
});
const getLabelStyles = (theme: GrafanaTheme2) => {
return {
placeholder: css({
color: theme.colors.text.disabled,
opacity: 1,
}),
};
};

View File

@ -1,4 +1,4 @@
import { dateMath, dateTimeParse, isDateTime, TimeZone } from '@grafana/data';
import { dateMath, dateTimeParse, isDateTime, TimeRange, TimeZone } from '@grafana/data';
export function isValid(value: string, roundUp?: boolean, timeZone?: TimeZone): boolean {
if (isDateTime(value)) {
@ -12,3 +12,7 @@ export function isValid(value: string, roundUp?: boolean, timeZone?: TimeZone):
const parsed = dateTimeParse(value, { roundUp, timeZone });
return parsed.isValid();
}
export function isValidTimeRange(range: TimeRange) {
return dateMath.isValid(range.from) && dateMath.isValid(range.to);
}

View File

@ -28,7 +28,7 @@ export const Spinner = ({ className, inline = false, iconClassName, style, size
const styles = getStyles(size, inline);
return (
<div data-testid="Spinner" style={style} className={cx(styles, className)}>
<Icon className={cx('fa-spin', iconClassName)} name="fa fa-spinner" />
<Icon className={cx('fa-spin', iconClassName)} name="fa fa-spinner" aria-label="loading spinner" />
</div>
);
};

View File

@ -35,6 +35,7 @@ export { StatsPicker } from './StatsPicker/StatsPicker';
export { RefreshPicker, defaultIntervals } from './RefreshPicker/RefreshPicker';
export { TimeRangePicker, type TimeRangePickerProps } from './DateTimePickers/TimeRangePicker';
export { TimePickerTooltip } from './DateTimePickers/TimeRangePicker';
export { TimeRangeLabel } from './DateTimePickers/TimeRangePicker/TimeRangeLabel';
export { TimeOfDayPicker } from './DateTimePickers/TimeOfDayPicker';
export { TimeZonePicker } from './DateTimePickers/TimeZonePicker';
export { WeekStartPicker } from './DateTimePickers/WeekStartPicker';

View File

@ -1,7 +1,9 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime/src';
import { Modal, ModalTabsHeader, TabContent } from '@grafana/ui';
import { Modal, ModalTabsHeader, TabContent, Themeable2, withTheme2 } from '@grafana/ui';
import { config } from 'app/core/config';
import { contextSrv } from 'app/core/core';
import { t } from 'app/core/internationalization';
@ -63,7 +65,7 @@ function getTabs(panel?: PanelModel, activeTab?: string) {
};
}
interface Props {
interface Props extends Themeable2 {
dashboard: DashboardModel;
panel?: PanelModel;
activeTab?: string;
@ -85,7 +87,7 @@ function getInitialState(props: Props): State {
};
}
export class ShareModal extends React.Component<Props, State> {
class UnthemedShareModal extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = getInitialState(props);
@ -122,12 +124,19 @@ export class ShareModal extends React.Component<Props, State> {
}
render() {
const { dashboard, panel } = this.props;
const { dashboard, panel, theme } = this.props;
const styles = getStyles(theme);
const activeTabModel = this.getActiveTab();
const ActiveTab = activeTabModel.component;
return (
<Modal isOpen={true} title={this.renderTitle()} onDismiss={this.props.onDismiss}>
<Modal
isOpen={true}
title={this.renderTitle()}
onDismiss={this.props.onDismiss}
className={styles.container}
contentClassName={styles.content}
>
<TabContent>
<ActiveTab dashboard={dashboard} panel={panel} onDismiss={this.props.onDismiss} />
</TabContent>
@ -135,3 +144,18 @@ export class ShareModal extends React.Component<Props, State> {
);
}
}
export const ShareModal = withTheme2(UnthemedShareModal);
const getStyles = (theme: GrafanaTheme2) => {
return {
container: css({
label: 'shareModalContainer',
paddingTop: theme.spacing(1),
}),
content: css({
label: 'shareModalContent',
padding: theme.spacing(3, 2, 2, 2),
}),
};
};

View File

@ -1,5 +1,4 @@
import { css } from '@emotion/css';
import cx from 'classnames';
import React, { useContext } from 'react';
import { useForm } from 'react-hook-form';
@ -13,11 +12,11 @@ import {
Input,
Label,
ModalsContext,
Spinner,
Switch,
useStyles2,
} from '@grafana/ui/src';
import { Layout } from '@grafana/ui/src/components/Layout/Layout';
import { getTimeRange } from 'app/features/dashboard/utils/timeRange';
import { contextSrv } from '../../../../../../core/services/context_srv';
import { AccessControlAction, useSelector } from '../../../../../../types';
@ -38,6 +37,8 @@ import {
import { Configuration } from './Configuration';
import { EmailSharingConfiguration } from './EmailSharingConfiguration';
import { SettingsBar } from './SettingsBar';
import { SettingsSummary } from './SettingsSummary';
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
@ -49,8 +50,8 @@ export interface ConfigPublicDashboardForm {
const ConfigPublicDashboard = () => {
const styles = useStyles2(getStyles);
const { showModal, hideModal } = useContext(ModalsContext);
const isDesktop = useIsDesktop();
const { showModal, hideModal } = useContext(ModalsContext);
const hasWritePermissions = contextSrv.hasAccess(AccessControlAction.DashboardsPublicWrite, isOrgAdmin());
const hasEmailSharingEnabled =
@ -62,7 +63,9 @@ const ConfigPublicDashboard = () => {
const { data: publicDashboard, isFetching: isGetLoading } = useGetPublicDashboardQuery(dashboard.uid);
const [update, { isLoading: isUpdateLoading }] = useUpdatePublicDashboardMutation();
const disableInputs = !hasWritePermissions || isUpdateLoading || isGetLoading;
const isDataLoading = isUpdateLoading || isGetLoading;
const disableInputs = !hasWritePermissions || isDataLoading;
const timeRange = getTimeRange(dashboard.getDefaultTime(), dashboard);
const { handleSubmit, setValue, register } = useForm<ConfigPublicDashboardForm>({
defaultValues: {
@ -102,23 +105,17 @@ const ConfigPublicDashboard = () => {
};
return (
<div>
<div className={styles.configContainer}>
{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 />
{hasEmailSharingEnabled && <EmailSharingConfiguration />}
<Field label="Dashboard URL" className={styles.publicUrl}>
<Field label="Dashboard URL" className={styles.fieldSpace}>
<Input
value={generatePublicDashboardUrl(publicDashboard!.accessToken!)}
readOnly
@ -136,12 +133,9 @@ const ConfigPublicDashboard = () => {
}
/>
</Field>
<Layout
orientation={isDesktop ? 0 : 1}
justify={isDesktop ? 'flex-end' : 'flex-start'}
align={isDesktop ? 'center' : 'normal'}
>
<HorizontalGroup spacing="sm">
<Field className={styles.fieldSpace}>
<Layout>
<Switch
{...register('isPaused')}
disabled={disableInputs}
@ -160,10 +154,34 @@ const ConfigPublicDashboard = () => {
>
Pause sharing dashboard
</Label>
</HorizontalGroup>
</Layout>
</Field>
<Field className={styles.fieldSpace}>
<SettingsBar
title="Settings"
headerElement={({ className }) => (
<SettingsSummary
className={className}
isDataLoading={isDataLoading}
timeRange={timeRange}
timeSelectionEnabled={publicDashboard?.timeSelectionEnabled}
annotationsEnabled={publicDashboard?.annotationsEnabled}
/>
)}
data-testid={selectors.SettingsDropdown}
>
<Configuration disabled={disableInputs} onChange={onChange} register={register} timeRange={timeRange} />
</SettingsBar>
</Field>
<Layout
orientation={isDesktop ? 0 : 1}
justify={isDesktop ? 'flex-end' : 'flex-start'}
align={isDesktop ? 'center' : 'normal'}
>
<HorizontalGroup justify="flex-end">
<DeletePublicDashboardButton
className={cx(styles.deleteButton, { [styles.deleteButtonMobile]: !isDesktop })}
type="button"
disabled={disableInputs}
data-testid={selectors.DeleteButton}
@ -186,23 +204,21 @@ const ConfigPublicDashboard = () => {
};
const getStyles = (theme: GrafanaTheme2) => ({
titleContainer: css`
margin-bottom: ${theme.spacing(2)};
configContainer: css`
label: config container;
display: flex;
flex-direction: column;
flex-wrap: wrap;
gap: ${theme.spacing(3)};
`,
title: css`
margin: 0;
`,
publicUrl: css`
fieldSpace: css`
label: field space;
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)};
margin-bottom: 0;
`,
timeRange: css({
display: 'inline-block',
}),
});
export default ConfigPublicDashboard;

View File

@ -1,15 +1,11 @@
import { css } from '@emotion/css';
import React from 'react';
import { UseFormRegister } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data/src';
import { TimeRange } from '@grafana/data/src';
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 { FieldSet, Label, Switch, TimeRangeInput, VerticalGroup } from '@grafana/ui/src';
import { Layout } from '@grafana/ui/src/components/Layout/Layout';
import { getTimeRange } from 'app/features/dashboard/utils/timeRange';
import { useSelector } from '../../../../../../types';
import { ConfigPublicDashboardForm } from './ConfigPublicDashboard';
@ -19,21 +15,16 @@ export const Configuration = ({
disabled,
onChange,
register,
timeRange,
}: {
disabled: boolean;
onChange: (name: keyof ConfigPublicDashboardForm, value: boolean) => void;
register: UseFormRegister<ConfigPublicDashboardForm>;
timeRange: TimeRange;
}) => {
const styles = useStyles2(getStyles);
const dashboardState = useSelector((store) => store.dashboard);
const dashboard = dashboardState.getModel()!;
const timeRange = getTimeRange(dashboard.getDefaultTime(), dashboard);
return (
<>
<FieldSet disabled={disabled} className={styles.dashboardConfig}>
<FieldSet disabled={disabled}>
<VerticalGroup spacing="md">
<Layout orientation={1} spacing="xs" justify="space-between">
<Label description="The public dashboard uses the default time range settings of the dashboard">
@ -72,9 +63,3 @@ export const Configuration = ({
</>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
dashboardConfig: css`
margin: ${theme.spacing(0, 0, 3, 0)};
`,
});

View File

@ -154,7 +154,7 @@ export const EmailSharingConfiguration = () => {
return (
<form data-testid={selectors.Container} className={styles.container} onSubmit={handleSubmit(onSubmit)}>
<Field label="Can view dashboard">
<Field label="Can view dashboard" className={styles.field}>
<InputControl
name="shareType"
control={control}
@ -184,6 +184,7 @@ export const EmailSharingConfiguration = () => {
description="Invite people by email"
error={errors.email?.message}
invalid={!!errors.email?.message || undefined}
className={styles.field}
>
<div className={styles.emailContainer}>
<Input
@ -221,20 +222,30 @@ export const EmailSharingConfiguration = () => {
const getStyles = (theme: GrafanaTheme2) => ({
container: css`
margin-bottom: ${theme.spacing(2)};
label: emailConfigContainer;
display: flex;
flex-direction: column;
flex-wrap: wrap;
gap: ${theme.spacing(3)};
`,
field: css`
label: field-noMargin;
margin-bottom: 0;
`,
emailContainer: css`
label: emailContainer;
display: flex;
gap: ${theme.spacing(1)};
`,
emailInput: css`
label: emailInput;
flex-grow: 1;
`,
table: css`
label: table;
display: flex;
max-height: 220px;
overflow-y: scroll;
margin-bottom: ${theme.spacing(1)};
& tbody {
display: flex;

View File

@ -0,0 +1,44 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { SettingsBarHeader, Props as SettingsBarHeaderProps } from './SettingsBarHeader';
export interface Props extends Pick<SettingsBarHeaderProps, 'headerElement' | 'title'> {
children: React.ReactNode;
}
export function SettingsBar({ children, title, headerElement, ...rest }: Props) {
const styles = useStyles2(getStyles);
const [isContentVisible, setIsContentVisible] = useState(false);
function onRowToggle() {
setIsContentVisible((prevState) => !prevState);
}
return (
<>
<SettingsBarHeader
onRowToggle={onRowToggle}
isContentVisible={isContentVisible}
title={title}
headerElement={headerElement}
{...rest}
/>
{isContentVisible && <div className={styles.content}>{children}</div>}
</>
);
}
SettingsBar.displayName = 'SettingsBar';
const getStyles = (theme: GrafanaTheme2) => {
return {
content: css({
marginTop: theme.spacing(1),
marginLeft: theme.spacing(4),
}),
};
};

View File

@ -0,0 +1,94 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { IconButton, ReactUtils, useStyles2 } from '@grafana/ui';
export interface Props {
onRowToggle: () => void;
isContentVisible?: boolean;
title?: string;
headerElement?: React.ReactNode | ((props: { className?: string }) => React.ReactNode);
}
export function SettingsBarHeader({ headerElement, isContentVisible = false, onRowToggle, title, ...rest }: Props) {
const styles = useStyles2(getStyles);
const headerElementRendered =
headerElement && ReactUtils.renderOrCallToRender(headerElement, { className: styles.summaryWrapper });
return (
<div className={styles.wrapper}>
<div className={styles.header}>
<IconButton
name={isContentVisible ? 'angle-down' : 'angle-right'}
tooltip={isContentVisible ? 'Collapse settings' : 'Expand settings'}
className={styles.collapseIcon}
onClick={onRowToggle}
aria-expanded={isContentVisible}
{...rest}
/>
{title && (
// disabling the a11y rules here as the IconButton above handles keyboard interactions
// this is just to provide a better experience for mouse users
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div className={styles.titleWrapper} onClick={onRowToggle}>
<span className={styles.title}>{title}</span>
</div>
)}
{headerElementRendered}
</div>
</div>
);
}
SettingsBarHeader.displayName = 'SettingsBarHeader';
function getStyles(theme: GrafanaTheme2) {
return {
wrapper: css({
label: 'header',
padding: theme.spacing(0.5, 0.5),
borderRadius: theme.shape.borderRadius(1),
background: theme.colors.background.secondary,
minHeight: theme.spacing(4),
'&:focus': {
outline: 'none',
},
}),
header: css({
label: 'column',
display: 'flex',
alignItems: 'center',
whiteSpace: 'nowrap',
}),
collapseIcon: css({
marginLeft: theme.spacing(0.5),
color: theme.colors.text.disabled,
}),
titleWrapper: css({
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
overflow: 'hidden',
marginRight: `${theme.spacing(0.5)}`,
[theme.breakpoints.down('sm')]: {
flex: '1 1',
},
}),
title: css({
fontWeight: theme.typography.fontWeightBold,
marginLeft: theme.spacing(0.5),
overflow: 'hidden',
textOverflow: 'ellipsis',
}),
summaryWrapper: css({
display: 'flex',
flexWrap: 'wrap',
[theme.breakpoints.down('sm')]: {
flex: '2 2',
},
}),
};
}

View File

@ -0,0 +1,57 @@
import { css, cx } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2, TimeRange } from '@grafana/data';
import { Spinner, TimeRangeLabel, useStyles2 } from '@grafana/ui';
export interface Props {
timeRange: TimeRange;
className?: string;
isDataLoading?: boolean;
timeSelectionEnabled?: boolean;
annotationsEnabled?: boolean;
}
export function SettingsSummary({
className,
isDataLoading = false,
timeRange,
timeSelectionEnabled,
annotationsEnabled,
}: Props) {
const styles = useStyles2(getStyles);
return isDataLoading ? (
<div className={cx(styles.summaryWrapper, className)}>
<Spinner className={styles.summary} inline={true} size={14} />
</div>
) : (
<div className={cx(styles.summaryWrapper, className)}>
<span className={styles.summary}>
{'Time range = '}
<TimeRangeLabel className={styles.timeRange} value={timeRange} />
</span>
<span className={styles.summary}>{`Time range picker = ${timeSelectionEnabled ? 'enabled' : 'disabled'}`}</span>
<span className={styles.summary}>{`Annotations = ${annotationsEnabled ? 'show' : 'hide'}`}</span>
</div>
);
}
SettingsSummary.displayName = 'SettingsSummary';
const getStyles = (theme: GrafanaTheme2) => {
return {
summaryWrapper: css({
display: 'flex',
}),
summary: css`
label: collapsedText;
margin-left: ${theme.spacing.gridSize * 2}px;
font-size: ${theme.typography.bodySmall.fontSize};
color: ${theme.colors.text.secondary};
`,
timeRange: css({
display: 'inline-block',
}),
};
};

View File

@ -10,6 +10,7 @@ export const NoUpsertPermissionsAlert = ({ mode }: { mode: 'create' | 'edit' })
severity="info"
title={`You dont have permission to ${mode} a public dashboard`}
data-testid={selectors.NoUpsertPermissionsWarningAlert}
bottomSpacing={0}
>
Contact your admin to get permission to {mode} create public dashboards
</Alert>

View File

@ -3,5 +3,9 @@ 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" />
<Alert
title="Please save your dashboard changes before updating the public configuration"
severity="warning"
bottomSpacing={0}
/>
);

View File

@ -16,6 +16,7 @@ export const UnsupportedDataSourcesAlert = ({ unsupportedDataSources }: { unsupp
severity="warning"
title="Unsupported data sources"
data-testid={selectors.UnsupportedDataSourcesWarningAlert}
bottomSpacing={0}
>
<p className={styles.unsupportedDataSourceDescription}>
There are data sources in this dashboard that are unsupported for public dashboards. Panels that use these data

View File

@ -10,6 +10,7 @@ export const UnsupportedTemplateVariablesAlert = () => (
severity="warning"
title="Template variables are not supported"
data-testid={selectors.TemplateVariablesWarningAlert}
bottomSpacing={0}
>
This public dashboard may not work since it uses template variables
</Alert>

View File

@ -135,15 +135,28 @@ describe('SharePublic', () => {
expect(screen.getByRole('tablist')).toHaveTextContent('Link');
expect(screen.getByRole('tablist')).not.toHaveTextContent('Public dashboard');
});
it('renders default relative time in input', async () => {
it('renders default relative time in settings summary when they are closed', async () => {
expect(mockDashboard.time).toEqual({ from: 'now-6h', to: 'now' });
//@ts-ignore
mockDashboard.originalTime = { from: 'now-6h', to: 'now' };
await renderSharePublicDashboard();
await waitFor(() => screen.getByText('Time range ='));
expect(screen.getByText('Last 6 hours')).toBeInTheDocument();
});
it('renders default relative time in settings when they are open', async () => {
expect(mockDashboard.time).toEqual({ from: 'now-6h', to: 'now' });
//@ts-ignore
mockDashboard.originalTime = { from: 'now-6h', to: 'now' };
await renderSharePublicDashboard();
await userEvent.click(screen.getByText('Settings'));
expect(screen.queryAllByText('Last 6 hours')).toHaveLength(2);
});
it('when modal is opened, then checkboxes are enabled but create button is disabled', async () => {
server.use(getNonExistentPublicDashboardResponse());
await renderSharePublicDashboard();
@ -214,6 +227,7 @@ describe('SharePublic - Already persisted', () => {
});
it('when fetch is done, then inputs are checked and delete button is enabled', async () => {
await renderSharePublicDashboard();
await userEvent.click(screen.getByText('Settings'));
await waitFor(() => {
expect(screen.getByTestId(selectors.EnableTimeRangeSwitch)).toBeEnabled();
@ -231,6 +245,7 @@ describe('SharePublic - Already persisted', () => {
it('inputs and delete button are disabled because of lack of permissions', async () => {
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(false);
await renderSharePublicDashboard();
await userEvent.click(screen.getByText('Settings'));
expect(await screen.findByTestId(selectors.EnableTimeRangeSwitch)).toBeDisabled();
expect(screen.getByTestId(selectors.EnableTimeRangeSwitch)).toBeChecked();
@ -257,6 +272,7 @@ describe('SharePublic - Already persisted', () => {
);
await renderSharePublicDashboard();
await userEvent.click(screen.getByText('Settings'));
const enableTimeRangeSwitch = await screen.findByTestId(selectors.EnableTimeRangeSwitch);
await waitFor(() => {
@ -320,6 +336,8 @@ describe('SharePublic - Report interactions', () => {
it('reports interaction when time range is clicked', async () => {
await renderSharePublicDashboard();
await userEvent.click(screen.getByText('Settings'));
await waitFor(() => {
expect(screen.getByTestId(selectors.EnableTimeRangeSwitch)).toBeEnabled();
});
@ -333,6 +351,8 @@ describe('SharePublic - Report interactions', () => {
});
it('reports interaction when show annotations is clicked', async () => {
await renderSharePublicDashboard();
await userEvent.click(screen.getByText('Settings'));
await waitFor(() => {
expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeEnabled();
});