diff --git a/e2e/dashboards-suite/dashboard-public-create.spec.ts b/e2e/dashboards-suite/dashboard-public-create.spec.ts index 8c0e2b2b407..6fcb58141a9 100644 --- a/e2e/dashboards-suite/dashboard-public-create.spec.ts +++ b/e2e/dashboards-suite/dashboard-public-create.spec.ts @@ -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() diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index c0f2f36bbf5..82d7c605e83 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -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', diff --git a/packages/grafana-ui/src/components/Alert/Alert.tsx b/packages/grafana-ui/src/components/Alert/Alert.tsx index 89c6f56e22b..d426697aaea 100644 --- a/packages/grafana-ui/src/components/Alert/Alert.tsx +++ b/packages/grafana-ui/src/components/Alert/Alert.tsx @@ -125,6 +125,7 @@ const getStyles = ( return { alert: css({ + label: 'alert', flexGrow: 1, position: 'relative', borderRadius, diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.tsx index 06ce21f9715..1e60b37a3e1 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.tsx @@ -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 && } - {isValidTimeRange(value) ? ( - - ) : ( - {placeholder} - )} + + {!disabled && ( diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeLabel.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeLabel.tsx new file mode 100644 index 00000000000..d3c7d5cc2f4 --- /dev/null +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeLabel.tsx @@ -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 & { + placeholder?: string; + className?: string; +}; + +export const TimeRangeLabel = memo(function TimePickerLabel({ + hideText, + value, + timeZone = 'browser', + placeholder = 'No time range selected', + className, +}) { + const styles = useStyles2(getLabelStyles); + + if (hideText) { + return null; + } + + return ( + + {isValidTimeRange(value) ? ( + + ) : ( + {placeholder} + )} + + ); +}); + +const getLabelStyles = (theme: GrafanaTheme2) => { + return { + placeholder: css({ + color: theme.colors.text.disabled, + opacity: 1, + }), + }; +}; diff --git a/packages/grafana-ui/src/components/DateTimePickers/utils.ts b/packages/grafana-ui/src/components/DateTimePickers/utils.ts index 83f95224ed4..de89beeb6be 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/utils.ts +++ b/packages/grafana-ui/src/components/DateTimePickers/utils.ts @@ -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); +} diff --git a/packages/grafana-ui/src/components/Spinner/Spinner.tsx b/packages/grafana-ui/src/components/Spinner/Spinner.tsx index acffc8f4997..b27a4675b47 100644 --- a/packages/grafana-ui/src/components/Spinner/Spinner.tsx +++ b/packages/grafana-ui/src/components/Spinner/Spinner.tsx @@ -28,7 +28,7 @@ export const Spinner = ({ className, inline = false, iconClassName, style, size const styles = getStyles(size, inline); return (
- +
); }; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index ca826cc946f..3df11419199 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -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'; diff --git a/public/app/features/dashboard/components/ShareModal/ShareModal.tsx b/public/app/features/dashboard/components/ShareModal/ShareModal.tsx index 521b794fb7f..6c583dbc0bb 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareModal.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareModal.tsx @@ -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 { +class UnthemedShareModal extends React.Component { constructor(props: Props) { super(props); this.state = getInitialState(props); @@ -122,12 +124,19 @@ export class ShareModal extends React.Component { } 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 ( - + @@ -135,3 +144,18 @@ export class ShareModal extends React.Component { ); } } + +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), + }), + }; +}; diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx index 7740894da1b..8215c8c852e 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx @@ -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({ defaultValues: { @@ -102,23 +105,17 @@ const ConfigPublicDashboard = () => { }; return ( -
+
{hasWritePermissions && dashboard.hasUnsavedChanges() && } {!hasWritePermissions && } {dashboardHasTemplateVariables(dashboardVariables) && } {!!unsupportedDataSources.length && ( )} -
- -

Settings

- {(isUpdateLoading || isGetLoading) && } -
-
- -
+ {hasEmailSharingEnabled && } - + + { } /> - - + + + { > Pause sharing dashboard - + + + + + ( + + )} + data-testid={selectors.SettingsDropdown} + > + + + + + { }; 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; diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/Configuration.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/Configuration.tsx index 4fe88b07249..75a6bf1b76f 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/Configuration.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/Configuration.tsx @@ -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; + timeRange: TimeRange; }) => { - const styles = useStyles2(getStyles); - - const dashboardState = useSelector((store) => store.dashboard); - const dashboard = dashboardState.getModel()!; - - const timeRange = getTimeRange(dashboard.getDefaultTime(), dashboard); - return ( <> -
+