mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
edb7d0e0d8
commit
1110cb4d44
@ -54,10 +54,14 @@ e2e.scenario({
|
|||||||
// These elements should be rendered
|
// These elements should be rendered
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput().should('exist');
|
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput().should('exist');
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton().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.PauseSwitch().should('exist');
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.DeleteButton().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.CopyUrlInput().should('exist');
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton().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.PauseSwitch().should('exist');
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.DeleteButton().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
|
// Make a request to public dashboards api endpoint without authentication
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput()
|
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput()
|
||||||
|
@ -213,6 +213,7 @@ export const Pages = {
|
|||||||
DeleteButton: 'data-testid public dashboard delete button',
|
DeleteButton: 'data-testid public dashboard delete button',
|
||||||
CopyUrlInput: 'data-testid public dashboard copy url input',
|
CopyUrlInput: 'data-testid public dashboard copy url input',
|
||||||
CopyUrlButton: 'data-testid public dashboard copy url button',
|
CopyUrlButton: 'data-testid public dashboard copy url button',
|
||||||
|
SettingsDropdown: 'data-testid public dashboard settings dropdown',
|
||||||
TemplateVariablesWarningAlert: 'data-testid public dashboard disabled template variables alert',
|
TemplateVariablesWarningAlert: 'data-testid public dashboard disabled template variables alert',
|
||||||
UnsupportedDataSourcesWarningAlert: 'data-testid public dashboard unsupported data sources alert',
|
UnsupportedDataSourcesWarningAlert: 'data-testid public dashboard unsupported data sources alert',
|
||||||
NoUpsertPermissionsWarningAlert: 'data-testid public dashboard no upsert permissions alert',
|
NoUpsertPermissionsWarningAlert: 'data-testid public dashboard no upsert permissions alert',
|
||||||
|
@ -125,6 +125,7 @@ const getStyles = (
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
alert: css({
|
alert: css({
|
||||||
|
label: 'alert',
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
borderRadius,
|
borderRadius,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import React, { FormEvent, MouseEvent, useState } from 'react';
|
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 { selectors } from '@grafana/e2e-selectors';
|
||||||
|
|
||||||
import { stylesFactory } from '../../themes';
|
import { stylesFactory } from '../../themes';
|
||||||
@ -10,13 +10,10 @@ import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper'
|
|||||||
import { Icon } from '../Icon/Icon';
|
import { Icon } from '../Icon/Icon';
|
||||||
import { getInputStyles } from '../Input/Input';
|
import { getInputStyles } from '../Input/Input';
|
||||||
|
|
||||||
import { TimePickerButtonLabel } from './TimeRangePicker';
|
|
||||||
import { TimePickerContent } from './TimeRangePicker/TimePickerContent';
|
import { TimePickerContent } from './TimeRangePicker/TimePickerContent';
|
||||||
|
import { TimeRangeLabel } from './TimeRangePicker/TimeRangeLabel';
|
||||||
import { quickOptions } from './options';
|
import { quickOptions } from './options';
|
||||||
|
import { isValidTimeRange } from './utils';
|
||||||
const isValidTimeRange = (range: TimeRange) => {
|
|
||||||
return dateMath.isValid(range.from) && dateMath.isValid(range.to);
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface TimeRangeInputProps {
|
export interface TimeRangeInputProps {
|
||||||
value: TimeRange;
|
value: TimeRange;
|
||||||
@ -87,11 +84,8 @@ export const TimeRangeInput = ({
|
|||||||
onClick={onOpen}
|
onClick={onOpen}
|
||||||
>
|
>
|
||||||
{showIcon && <Icon name="clock-nine" size={'sm'} className={styles.icon} />}
|
{showIcon && <Icon name="clock-nine" size={'sm'} className={styles.icon} />}
|
||||||
{isValidTimeRange(value) ? (
|
|
||||||
<TimePickerButtonLabel value={value} timeZone={timeZone} />
|
<TimeRangeLabel value={value} timeZone={timeZone} placeholder={placeholder} />
|
||||||
) : (
|
|
||||||
<span className={styles.placeholder}>{placeholder}</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!disabled && (
|
{!disabled && (
|
||||||
<span className={styles.caretIcon}>
|
<span className={styles.caretIcon}>
|
||||||
|
@ -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,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
@ -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 {
|
export function isValid(value: string, roundUp?: boolean, timeZone?: TimeZone): boolean {
|
||||||
if (isDateTime(value)) {
|
if (isDateTime(value)) {
|
||||||
@ -12,3 +12,7 @@ export function isValid(value: string, roundUp?: boolean, timeZone?: TimeZone):
|
|||||||
const parsed = dateTimeParse(value, { roundUp, timeZone });
|
const parsed = dateTimeParse(value, { roundUp, timeZone });
|
||||||
return parsed.isValid();
|
return parsed.isValid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isValidTimeRange(range: TimeRange) {
|
||||||
|
return dateMath.isValid(range.from) && dateMath.isValid(range.to);
|
||||||
|
}
|
||||||
|
@ -28,7 +28,7 @@ export const Spinner = ({ className, inline = false, iconClassName, style, size
|
|||||||
const styles = getStyles(size, inline);
|
const styles = getStyles(size, inline);
|
||||||
return (
|
return (
|
||||||
<div data-testid="Spinner" style={style} className={cx(styles, className)}>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -35,6 +35,7 @@ export { StatsPicker } from './StatsPicker/StatsPicker';
|
|||||||
export { RefreshPicker, defaultIntervals } from './RefreshPicker/RefreshPicker';
|
export { RefreshPicker, defaultIntervals } from './RefreshPicker/RefreshPicker';
|
||||||
export { TimeRangePicker, type TimeRangePickerProps } from './DateTimePickers/TimeRangePicker';
|
export { TimeRangePicker, type TimeRangePickerProps } from './DateTimePickers/TimeRangePicker';
|
||||||
export { TimePickerTooltip } from './DateTimePickers/TimeRangePicker';
|
export { TimePickerTooltip } from './DateTimePickers/TimeRangePicker';
|
||||||
|
export { TimeRangeLabel } from './DateTimePickers/TimeRangePicker/TimeRangeLabel';
|
||||||
export { TimeOfDayPicker } from './DateTimePickers/TimeOfDayPicker';
|
export { TimeOfDayPicker } from './DateTimePickers/TimeOfDayPicker';
|
||||||
export { TimeZonePicker } from './DateTimePickers/TimeZonePicker';
|
export { TimeZonePicker } from './DateTimePickers/TimeZonePicker';
|
||||||
export { WeekStartPicker } from './DateTimePickers/WeekStartPicker';
|
export { WeekStartPicker } from './DateTimePickers/WeekStartPicker';
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { reportInteraction } from '@grafana/runtime/src';
|
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 { config } from 'app/core/config';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
import { t } from 'app/core/internationalization';
|
import { t } from 'app/core/internationalization';
|
||||||
@ -63,7 +65,7 @@ function getTabs(panel?: PanelModel, activeTab?: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props extends Themeable2 {
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
panel?: PanelModel;
|
panel?: PanelModel;
|
||||||
activeTab?: string;
|
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) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = getInitialState(props);
|
this.state = getInitialState(props);
|
||||||
@ -122,12 +124,19 @@ export class ShareModal extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { dashboard, panel } = this.props;
|
const { dashboard, panel, theme } = this.props;
|
||||||
|
const styles = getStyles(theme);
|
||||||
const activeTabModel = this.getActiveTab();
|
const activeTabModel = this.getActiveTab();
|
||||||
const ActiveTab = activeTabModel.component;
|
const ActiveTab = activeTabModel.component;
|
||||||
|
|
||||||
return (
|
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>
|
<TabContent>
|
||||||
<ActiveTab dashboard={dashboard} panel={panel} onDismiss={this.props.onDismiss} />
|
<ActiveTab dashboard={dashboard} panel={panel} onDismiss={this.props.onDismiss} />
|
||||||
</TabContent>
|
</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),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import cx from 'classnames';
|
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
@ -13,11 +12,11 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
ModalsContext,
|
ModalsContext,
|
||||||
Spinner,
|
|
||||||
Switch,
|
Switch,
|
||||||
useStyles2,
|
useStyles2,
|
||||||
} from '@grafana/ui/src';
|
} from '@grafana/ui/src';
|
||||||
import { Layout } from '@grafana/ui/src/components/Layout/Layout';
|
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 { contextSrv } from '../../../../../../core/services/context_srv';
|
||||||
import { AccessControlAction, useSelector } from '../../../../../../types';
|
import { AccessControlAction, useSelector } from '../../../../../../types';
|
||||||
@ -38,6 +37,8 @@ import {
|
|||||||
|
|
||||||
import { Configuration } from './Configuration';
|
import { Configuration } from './Configuration';
|
||||||
import { EmailSharingConfiguration } from './EmailSharingConfiguration';
|
import { EmailSharingConfiguration } from './EmailSharingConfiguration';
|
||||||
|
import { SettingsBar } from './SettingsBar';
|
||||||
|
import { SettingsSummary } from './SettingsSummary';
|
||||||
|
|
||||||
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
|
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
|
||||||
|
|
||||||
@ -49,8 +50,8 @@ export interface ConfigPublicDashboardForm {
|
|||||||
|
|
||||||
const ConfigPublicDashboard = () => {
|
const ConfigPublicDashboard = () => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const { showModal, hideModal } = useContext(ModalsContext);
|
|
||||||
const isDesktop = useIsDesktop();
|
const isDesktop = useIsDesktop();
|
||||||
|
const { showModal, hideModal } = useContext(ModalsContext);
|
||||||
|
|
||||||
const hasWritePermissions = contextSrv.hasAccess(AccessControlAction.DashboardsPublicWrite, isOrgAdmin());
|
const hasWritePermissions = contextSrv.hasAccess(AccessControlAction.DashboardsPublicWrite, isOrgAdmin());
|
||||||
const hasEmailSharingEnabled =
|
const hasEmailSharingEnabled =
|
||||||
@ -62,7 +63,9 @@ const ConfigPublicDashboard = () => {
|
|||||||
|
|
||||||
const { data: publicDashboard, isFetching: isGetLoading } = useGetPublicDashboardQuery(dashboard.uid);
|
const { data: publicDashboard, isFetching: isGetLoading } = useGetPublicDashboardQuery(dashboard.uid);
|
||||||
const [update, { isLoading: isUpdateLoading }] = useUpdatePublicDashboardMutation();
|
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>({
|
const { handleSubmit, setValue, register } = useForm<ConfigPublicDashboardForm>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@ -102,23 +105,17 @@ const ConfigPublicDashboard = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={styles.configContainer}>
|
||||||
{hasWritePermissions && dashboard.hasUnsavedChanges() && <SaveDashboardChangesAlert />}
|
{hasWritePermissions && dashboard.hasUnsavedChanges() && <SaveDashboardChangesAlert />}
|
||||||
{!hasWritePermissions && <NoUpsertPermissionsAlert mode="edit" />}
|
{!hasWritePermissions && <NoUpsertPermissionsAlert mode="edit" />}
|
||||||
{dashboardHasTemplateVariables(dashboardVariables) && <UnsupportedTemplateVariablesAlert />}
|
{dashboardHasTemplateVariables(dashboardVariables) && <UnsupportedTemplateVariablesAlert />}
|
||||||
{!!unsupportedDataSources.length && (
|
{!!unsupportedDataSources.length && (
|
||||||
<UnsupportedDataSourcesAlert unsupportedDataSources={unsupportedDataSources.join(', ')} />
|
<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 />}
|
{hasEmailSharingEnabled && <EmailSharingConfiguration />}
|
||||||
<Field label="Dashboard URL" className={styles.publicUrl}>
|
|
||||||
|
<Field label="Dashboard URL" className={styles.fieldSpace}>
|
||||||
<Input
|
<Input
|
||||||
value={generatePublicDashboardUrl(publicDashboard!.accessToken!)}
|
value={generatePublicDashboardUrl(publicDashboard!.accessToken!)}
|
||||||
readOnly
|
readOnly
|
||||||
@ -136,12 +133,9 @@ const ConfigPublicDashboard = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Layout
|
|
||||||
orientation={isDesktop ? 0 : 1}
|
<Field className={styles.fieldSpace}>
|
||||||
justify={isDesktop ? 'flex-end' : 'flex-start'}
|
<Layout>
|
||||||
align={isDesktop ? 'center' : 'normal'}
|
|
||||||
>
|
|
||||||
<HorizontalGroup spacing="sm">
|
|
||||||
<Switch
|
<Switch
|
||||||
{...register('isPaused')}
|
{...register('isPaused')}
|
||||||
disabled={disableInputs}
|
disabled={disableInputs}
|
||||||
@ -160,10 +154,34 @@ const ConfigPublicDashboard = () => {
|
|||||||
>
|
>
|
||||||
Pause sharing dashboard
|
Pause sharing dashboard
|
||||||
</Label>
|
</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">
|
<HorizontalGroup justify="flex-end">
|
||||||
<DeletePublicDashboardButton
|
<DeletePublicDashboardButton
|
||||||
className={cx(styles.deleteButton, { [styles.deleteButtonMobile]: !isDesktop })}
|
|
||||||
type="button"
|
type="button"
|
||||||
disabled={disableInputs}
|
disabled={disableInputs}
|
||||||
data-testid={selectors.DeleteButton}
|
data-testid={selectors.DeleteButton}
|
||||||
@ -186,23 +204,21 @@ const ConfigPublicDashboard = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
titleContainer: css`
|
configContainer: css`
|
||||||
margin-bottom: ${theme.spacing(2)};
|
label: config container;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: ${theme.spacing(3)};
|
||||||
`,
|
`,
|
||||||
title: css`
|
fieldSpace: css`
|
||||||
margin: 0;
|
label: field space;
|
||||||
`,
|
|
||||||
publicUrl: css`
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-top: ${theme.spacing(1)};
|
margin-bottom: 0;
|
||||||
margin-bottom: ${theme.spacing(3)};
|
|
||||||
`,
|
|
||||||
deleteButton: css`
|
|
||||||
margin-left: ${theme.spacing(3)};
|
|
||||||
`,
|
|
||||||
deleteButtonMobile: css`
|
|
||||||
margin-top: ${theme.spacing(2)};
|
|
||||||
`,
|
`,
|
||||||
|
timeRange: css({
|
||||||
|
display: 'inline-block',
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default ConfigPublicDashboard;
|
export default ConfigPublicDashboard;
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
import { css } from '@emotion/css';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { UseFormRegister } from 'react-hook-form';
|
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 { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
||||||
import { reportInteraction } from '@grafana/runtime/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 { Layout } from '@grafana/ui/src/components/Layout/Layout';
|
||||||
import { getTimeRange } from 'app/features/dashboard/utils/timeRange';
|
|
||||||
|
|
||||||
import { useSelector } from '../../../../../../types';
|
|
||||||
|
|
||||||
import { ConfigPublicDashboardForm } from './ConfigPublicDashboard';
|
import { ConfigPublicDashboardForm } from './ConfigPublicDashboard';
|
||||||
|
|
||||||
@ -19,21 +15,16 @@ export const Configuration = ({
|
|||||||
disabled,
|
disabled,
|
||||||
onChange,
|
onChange,
|
||||||
register,
|
register,
|
||||||
|
timeRange,
|
||||||
}: {
|
}: {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
onChange: (name: keyof ConfigPublicDashboardForm, value: boolean) => void;
|
onChange: (name: keyof ConfigPublicDashboardForm, value: boolean) => void;
|
||||||
register: UseFormRegister<ConfigPublicDashboardForm>;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<FieldSet disabled={disabled} className={styles.dashboardConfig}>
|
<FieldSet disabled={disabled}>
|
||||||
<VerticalGroup spacing="md">
|
<VerticalGroup spacing="md">
|
||||||
<Layout orientation={1} spacing="xs" justify="space-between">
|
<Layout orientation={1} spacing="xs" justify="space-between">
|
||||||
<Label description="The public dashboard uses the default time range settings of the dashboard">
|
<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)};
|
|
||||||
`,
|
|
||||||
});
|
|
||||||
|
@ -154,7 +154,7 @@ export const EmailSharingConfiguration = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form data-testid={selectors.Container} className={styles.container} onSubmit={handleSubmit(onSubmit)}>
|
<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
|
<InputControl
|
||||||
name="shareType"
|
name="shareType"
|
||||||
control={control}
|
control={control}
|
||||||
@ -184,6 +184,7 @@ export const EmailSharingConfiguration = () => {
|
|||||||
description="Invite people by email"
|
description="Invite people by email"
|
||||||
error={errors.email?.message}
|
error={errors.email?.message}
|
||||||
invalid={!!errors.email?.message || undefined}
|
invalid={!!errors.email?.message || undefined}
|
||||||
|
className={styles.field}
|
||||||
>
|
>
|
||||||
<div className={styles.emailContainer}>
|
<div className={styles.emailContainer}>
|
||||||
<Input
|
<Input
|
||||||
@ -221,20 +222,30 @@ export const EmailSharingConfiguration = () => {
|
|||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
container: css`
|
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`
|
emailContainer: css`
|
||||||
|
label: emailContainer;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: ${theme.spacing(1)};
|
gap: ${theme.spacing(1)};
|
||||||
`,
|
`,
|
||||||
emailInput: css`
|
emailInput: css`
|
||||||
|
label: emailInput;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
`,
|
`,
|
||||||
table: css`
|
table: css`
|
||||||
|
label: table;
|
||||||
display: flex;
|
display: flex;
|
||||||
max-height: 220px;
|
max-height: 220px;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
margin-bottom: ${theme.spacing(1)};
|
|
||||||
|
|
||||||
& tbody {
|
& tbody {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -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),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
@ -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',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
@ -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',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
@ -10,6 +10,7 @@ export const NoUpsertPermissionsAlert = ({ mode }: { mode: 'create' | 'edit' })
|
|||||||
severity="info"
|
severity="info"
|
||||||
title={`You don’t have permission to ${mode} a public dashboard`}
|
title={`You don’t have permission to ${mode} a public dashboard`}
|
||||||
data-testid={selectors.NoUpsertPermissionsWarningAlert}
|
data-testid={selectors.NoUpsertPermissionsWarningAlert}
|
||||||
|
bottomSpacing={0}
|
||||||
>
|
>
|
||||||
Contact your admin to get permission to {mode} create public dashboards
|
Contact your admin to get permission to {mode} create public dashboards
|
||||||
</Alert>
|
</Alert>
|
||||||
|
@ -3,5 +3,9 @@ import React from 'react';
|
|||||||
import { Alert } from '@grafana/ui/src';
|
import { Alert } from '@grafana/ui/src';
|
||||||
|
|
||||||
export const SaveDashboardChangesAlert = () => (
|
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}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
@ -16,6 +16,7 @@ export const UnsupportedDataSourcesAlert = ({ unsupportedDataSources }: { unsupp
|
|||||||
severity="warning"
|
severity="warning"
|
||||||
title="Unsupported data sources"
|
title="Unsupported data sources"
|
||||||
data-testid={selectors.UnsupportedDataSourcesWarningAlert}
|
data-testid={selectors.UnsupportedDataSourcesWarningAlert}
|
||||||
|
bottomSpacing={0}
|
||||||
>
|
>
|
||||||
<p className={styles.unsupportedDataSourceDescription}>
|
<p className={styles.unsupportedDataSourceDescription}>
|
||||||
There are data sources in this dashboard that are unsupported for public dashboards. Panels that use these data
|
There are data sources in this dashboard that are unsupported for public dashboards. Panels that use these data
|
||||||
|
@ -10,6 +10,7 @@ export const UnsupportedTemplateVariablesAlert = () => (
|
|||||||
severity="warning"
|
severity="warning"
|
||||||
title="Template variables are not supported"
|
title="Template variables are not supported"
|
||||||
data-testid={selectors.TemplateVariablesWarningAlert}
|
data-testid={selectors.TemplateVariablesWarningAlert}
|
||||||
|
bottomSpacing={0}
|
||||||
>
|
>
|
||||||
This public dashboard may not work since it uses template variables
|
This public dashboard may not work since it uses template variables
|
||||||
</Alert>
|
</Alert>
|
||||||
|
@ -135,15 +135,28 @@ describe('SharePublic', () => {
|
|||||||
expect(screen.getByRole('tablist')).toHaveTextContent('Link');
|
expect(screen.getByRole('tablist')).toHaveTextContent('Link');
|
||||||
expect(screen.getByRole('tablist')).not.toHaveTextContent('Public dashboard');
|
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' });
|
expect(mockDashboard.time).toEqual({ from: 'now-6h', to: 'now' });
|
||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
mockDashboard.originalTime = { from: 'now-6h', to: 'now' };
|
mockDashboard.originalTime = { from: 'now-6h', to: 'now' };
|
||||||
|
|
||||||
await renderSharePublicDashboard();
|
await renderSharePublicDashboard();
|
||||||
|
await waitFor(() => screen.getByText('Time range ='));
|
||||||
|
|
||||||
expect(screen.getByText('Last 6 hours')).toBeInTheDocument();
|
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 () => {
|
it('when modal is opened, then checkboxes are enabled but create button is disabled', async () => {
|
||||||
server.use(getNonExistentPublicDashboardResponse());
|
server.use(getNonExistentPublicDashboardResponse());
|
||||||
await renderSharePublicDashboard();
|
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 () => {
|
it('when fetch is done, then inputs are checked and delete button is enabled', async () => {
|
||||||
await renderSharePublicDashboard();
|
await renderSharePublicDashboard();
|
||||||
|
await userEvent.click(screen.getByText('Settings'));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId(selectors.EnableTimeRangeSwitch)).toBeEnabled();
|
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 () => {
|
it('inputs and delete button are disabled because of lack of permissions', async () => {
|
||||||
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(false);
|
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(false);
|
||||||
await renderSharePublicDashboard();
|
await renderSharePublicDashboard();
|
||||||
|
await userEvent.click(screen.getByText('Settings'));
|
||||||
|
|
||||||
expect(await screen.findByTestId(selectors.EnableTimeRangeSwitch)).toBeDisabled();
|
expect(await screen.findByTestId(selectors.EnableTimeRangeSwitch)).toBeDisabled();
|
||||||
expect(screen.getByTestId(selectors.EnableTimeRangeSwitch)).toBeChecked();
|
expect(screen.getByTestId(selectors.EnableTimeRangeSwitch)).toBeChecked();
|
||||||
@ -257,6 +272,7 @@ describe('SharePublic - Already persisted', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await renderSharePublicDashboard();
|
await renderSharePublicDashboard();
|
||||||
|
await userEvent.click(screen.getByText('Settings'));
|
||||||
|
|
||||||
const enableTimeRangeSwitch = await screen.findByTestId(selectors.EnableTimeRangeSwitch);
|
const enableTimeRangeSwitch = await screen.findByTestId(selectors.EnableTimeRangeSwitch);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@ -320,6 +336,8 @@ describe('SharePublic - Report interactions', () => {
|
|||||||
|
|
||||||
it('reports interaction when time range is clicked', async () => {
|
it('reports interaction when time range is clicked', async () => {
|
||||||
await renderSharePublicDashboard();
|
await renderSharePublicDashboard();
|
||||||
|
await userEvent.click(screen.getByText('Settings'));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId(selectors.EnableTimeRangeSwitch)).toBeEnabled();
|
expect(screen.getByTestId(selectors.EnableTimeRangeSwitch)).toBeEnabled();
|
||||||
});
|
});
|
||||||
@ -333,6 +351,8 @@ describe('SharePublic - Report interactions', () => {
|
|||||||
});
|
});
|
||||||
it('reports interaction when show annotations is clicked', async () => {
|
it('reports interaction when show annotations is clicked', async () => {
|
||||||
await renderSharePublicDashboard();
|
await renderSharePublicDashboard();
|
||||||
|
await userEvent.click(screen.getByText('Settings'));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeEnabled();
|
expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user