Modals: design update (#33368)

* Modals: Style update draft

* Modal.ButtonRow to control spacing better in a centralised way

* Remove header border if no tabs

* Added border and made buttons right aligned and changed order of buttons

* Updating the overlay

* Tweaks to paddings

* Updated share modals
This commit is contained in:
Torkel Ödegaard 2021-04-26 18:26:56 +02:00 committed by GitHub
parent de65cef850
commit 4643bfa539
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 176 additions and 166 deletions

View File

@ -74,7 +74,7 @@ export function createComponents(colors: ThemeColors, shadows: ThemeShadows): Th
padding: 1, padding: 1,
}, },
overlay: { overlay: {
background: colors.mode === 'dark' ? 'rgba(0, 0, 0, 0.27)' : 'rgba(208, 209, 211, 0.24)', background: colors.mode === 'dark' ? 'rgba(0, 0, 0, 0.45)' : 'rgba(208, 209, 211, 0.24)',
}, },
}; };
} }

View File

@ -57,37 +57,35 @@ export const ConfirmModal = ({
return ( return (
<Modal className={styles.modal} title={title} icon={icon} isOpen={isOpen} onDismiss={onDismiss}> <Modal className={styles.modal} title={title} icon={icon} isOpen={isOpen} onDismiss={onDismiss}>
<div className={styles.modalContent}> <div className={styles.modalText}>
<div className={styles.modalText}> {body}
{body} {description ? <div className={styles.modalDescription}>{description}</div> : null}
{description ? <div className={styles.modalDescription}>{description}</div> : null} {confirmationText ? (
{confirmationText ? ( <div className={styles.modalConfirmationInput}>
<div className={styles.modalConfirmationInput}> <HorizontalGroup>
<HorizontalGroup justify="center"> <Input placeholder={`Type ${confirmationText} to confirm`} onChange={onConfirmationTextChange} />
<Input placeholder={`Type ${confirmationText} to confirm`} onChange={onConfirmationTextChange} /> </HorizontalGroup>
</HorizontalGroup> </div>
</div> ) : null}
) : null}
</div>
<HorizontalGroup justify="center">
{onAlternative ? (
<Button variant="primary" onClick={onAlternative}>
{alternativeText}
</Button>
) : null}
<Button
variant="destructive"
onClick={onConfirm}
disabled={disabled}
aria-label={selectors.pages.ConfirmModal.delete}
>
{confirmText}
</Button>
<Button variant="secondary" onClick={onDismiss}>
{dismissText}
</Button>
</HorizontalGroup>
</div> </div>
<Modal.ButtonRow>
<Button variant="secondary" onClick={onDismiss}>
{dismissText}
</Button>
<Button
variant="destructive"
onClick={onConfirm}
disabled={disabled}
aria-label={selectors.pages.ConfirmModal.delete}
>
{confirmText}
</Button>
{onAlternative ? (
<Button variant="primary" onClick={onAlternative}>
{alternativeText}
</Button>
) : null}
</Modal.ButtonRow>
</Modal> </Modal>
); );
}; };
@ -96,20 +94,14 @@ const getStyles = (theme: GrafanaThemeV2) => ({
modal: css` modal: css`
width: 500px; width: 500px;
`, `,
modalContent: css`
text-align: center;
`,
modalText: css({ modalText: css({
fontSize: theme.typography.h4.fontSize, fontSize: theme.typography.h5.fontSize,
color: theme.colors.text.primary, color: theme.colors.text.primary,
marginBottom: `calc(${theme.spacing(2)}*2)`,
paddingTop: theme.spacing(2),
}), }),
modalDescription: css({ modalDescription: css({
fontSize: theme.typography.h6.fontSize, fontSize: theme.typography.body.fontSize,
paddingTop: theme.spacing(2),
}), }),
modalConfirmationInput: css({ modalConfirmationInput: css({
paddingTop: theme.spacing(2), paddingTop: theme.spacing(1),
}), }),
}); });

View File

@ -26,6 +26,10 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
return { return {
wrapper: css` wrapper: css`
margin-bottom: ${theme.spacing.formSpacingBase * 4}px; margin-bottom: ${theme.spacing.formSpacingBase * 4}px;
&:last-child {
margin-bottom: 0;
}
`, `,
}; };
}); });

View File

@ -54,7 +54,7 @@ IconButton.displayName = 'IconButton';
const getStyles = stylesFactory((theme: GrafanaThemeV2, size: IconSize) => { const getStyles = stylesFactory((theme: GrafanaThemeV2, size: IconSize) => {
const hoverColor = theme.colors.action.hover; const hoverColor = theme.colors.action.hover;
const pixelSize = getSvgSize(size); const pixelSize = getSvgSize(size);
const hoverSize = pixelSize / 2; const hoverSize = Math.max(pixelSize / 3, 8);
return { return {
button: css` button: css`

View File

@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { oneLineTrim } from 'common-tags'; import { oneLineTrim } from 'common-tags';
import { Story, Meta } from '@storybook/react'; import { Story, Meta } from '@storybook/react';
import { getAvailableIcons } from '../../types'; import { getAvailableIcons } from '../../types';
import { Modal, ModalTabsHeader, TabContent } from '@grafana/ui'; import { Button, Modal, ModalTabsHeader, TabContent } from '@grafana/ui';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
@ -54,6 +54,10 @@ export const Basic: Story = ({ body, title, ...args }) => {
return ( return (
<Modal title={title} {...args}> <Modal title={title} {...args}>
{body} {body}
<Modal.ButtonRow>
<Button>Button1</Button>
<Button variant="secondary">Cancel</Button>
</Modal.ButtonRow>
</Modal> </Modal>
); );
}; };

View File

@ -1,4 +1,4 @@
import React, { FC, PropsWithChildren, useCallback, useEffect } from 'react'; import React, { PropsWithChildren, useCallback, useEffect } from 'react';
import { Portal } from '../Portal/Portal'; import { Portal } from '../Portal/Portal';
import { cx } from '@emotion/css'; import { cx } from '@emotion/css';
import { useTheme2 } from '../../themes'; import { useTheme2 } from '../../themes';
@ -6,9 +6,12 @@ import { IconName } from '../../types';
import { getModalStyles } from './getModalStyles'; import { getModalStyles } from './getModalStyles';
import { ModalHeader } from './ModalHeader'; import { ModalHeader } from './ModalHeader';
import { IconButton } from '../IconButton/IconButton'; import { IconButton } from '../IconButton/IconButton';
import { HorizontalGroup } from '../Layout/Layout';
export interface Props { export interface Props {
/** @deprecated no longer used */
icon?: IconName; icon?: IconName;
/** @deprecated no longer used */
iconTooltip?: string; iconTooltip?: string;
/** Title for the modal or custom header element */ /** Title for the modal or custom header element */
title: string | JSX.Element; title: string | JSX.Element;
@ -23,7 +26,7 @@ export interface Props {
onClickBackdrop?: () => void; onClickBackdrop?: () => void;
} }
export function Modal(props: PropsWithChildren<Props>): ReturnType<FC<Props>> { export function Modal(props: PropsWithChildren<Props>) {
const { const {
title, title,
children, children,
@ -62,14 +65,16 @@ export function Modal(props: PropsWithChildren<Props>): ReturnType<FC<Props>> {
return null; return null;
} }
const headerClass = cx(styles.modalHeader, typeof title !== 'string' && styles.modalHeaderWithTabs);
return ( return (
<Portal> <Portal>
<div className={cx(styles.modal, className)}> <div className={cx(styles.modal, className)}>
<div className={styles.modalHeader}> <div className={headerClass}>
{typeof title === 'string' && <DefaultModalHeader {...props} title={title} />} {typeof title === 'string' && <DefaultModalHeader {...props} title={title} />}
{typeof title !== 'string' && title} {typeof title !== 'string' && title}
<div className={styles.modalHeaderClose}> <div className={styles.modalHeaderClose}>
<IconButton surface="header" name="times" size="lg" onClick={onDismiss} /> <IconButton surface="header" name="times" size="xl" onClick={onDismiss} />
</div> </div>
</div> </div>
<div className={cx(styles.modalContent, contentClassName)}>{children}</div> <div className={cx(styles.modalContent, contentClassName)}>{children}</div>
@ -79,6 +84,21 @@ export function Modal(props: PropsWithChildren<Props>): ReturnType<FC<Props>> {
); );
} }
function ModalButtonRow({ children }: { children: React.ReactNode }) {
const theme = useTheme2();
const styles = getModalStyles(theme);
return (
<div className={styles.modalButtonRow}>
<HorizontalGroup justify="flex-end" spacing="md">
{children}
</HorizontalGroup>
</div>
);
}
Modal.ButtonRow = ModalButtonRow;
interface DefaultModalHeaderProps { interface DefaultModalHeaderProps {
title: string; title: string;
icon?: IconName; icon?: IconName;

View File

@ -2,29 +2,22 @@ import React from 'react';
import { getModalStyles } from './getModalStyles'; import { getModalStyles } from './getModalStyles';
import { IconName } from '../../types'; import { IconName } from '../../types';
import { useStyles2 } from '../../themes'; import { useStyles2 } from '../../themes';
import { Icon } from '../Icon/Icon';
import { Tooltip } from '..';
interface Props { interface Props {
title: string; title: string;
/** @deprecated */
icon?: IconName; icon?: IconName;
/** @deprecated */
iconTooltip?: string; iconTooltip?: string;
} }
/** @internal */
export const ModalHeader: React.FC<Props> = ({ icon, iconTooltip, title, children }) => { export const ModalHeader: React.FC<Props> = ({ icon, iconTooltip, title, children }) => {
const styles = useStyles2(getModalStyles); const styles = useStyles2(getModalStyles);
return ( return (
<> <>
<h2 className={styles.modalHeaderTitle}> <h2 className={styles.modalHeaderTitle}>{title}</h2>
{icon && !iconTooltip && <Icon name={icon} size="lg" className={styles.modalHeaderIcon} />}
{icon && iconTooltip && (
<Tooltip content={iconTooltip}>
<Icon name={icon} size="lg" className={styles.modalHeaderIcon} />
</Tooltip>
)}
{title}
</h2>
{children} {children}
</> </>
); );

View File

@ -3,7 +3,7 @@ import { GrafanaThemeV2 } from '@grafana/data';
import { stylesFactory } from '../../themes'; import { stylesFactory } from '../../themes';
export const getModalStyles = stylesFactory((theme: GrafanaThemeV2) => { export const getModalStyles = stylesFactory((theme: GrafanaThemeV2) => {
const borderRadius = theme.shape.borderRadius(2); const borderRadius = theme.shape.borderRadius(1);
return { return {
modal: css` modal: css`
@ -12,6 +12,7 @@ export const getModalStyles = stylesFactory((theme: GrafanaThemeV2) => {
background: ${theme.colors.background.primary}; background: ${theme.colors.background.primary};
box-shadow: ${theme.shadows.z3}; box-shadow: ${theme.shadows.z3};
border-radius: ${borderRadius}; border-radius: ${borderRadius};
border: 1px solid ${theme.colors.border.weak};
background-clip: padding-box; background-clip: padding-box;
outline: none; outline: none;
width: 750px; width: 750px;
@ -34,17 +35,21 @@ export const getModalStyles = stylesFactory((theme: GrafanaThemeV2) => {
`, `,
modalHeader: css` modalHeader: css`
label: modalHeader; label: modalHeader;
background: ${theme.colors.background.secondary};
border-radius: ${borderRadius} ${borderRadius} 0 0;
display: flex; display: flex;
height: 42px; align-items: center;
min-height: 42px;
margin: ${theme.spacing(1, 2, 0, 2)};
`,
modalHeaderWithTabs: css`
border-bottom: 1px solid ${theme.colors.border.weak};
`, `,
modalHeaderTitle: css` modalHeaderTitle: css`
font-size: ${theme.typography.size.lg}; font-size: ${theme.typography.size.lg};
margin: 0 ${theme.spacing(2)}; margin: ${theme.spacing(0, 4, 0, 1)};
display: flex; display: flex;
align-items: center; align-items: center;
line-height: 42px; position: relative;
top: 2px;
`, `,
modalHeaderIcon: css` modalHeaderIcon: css`
margin-right: ${theme.spacing(2)}; margin-right: ${theme.spacing(2)};
@ -57,15 +62,18 @@ export const getModalStyles = stylesFactory((theme: GrafanaThemeV2) => {
height: 100%; height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
color: ${theme.colors.text.secondary};
flex-grow: 1; flex-grow: 1;
justify-content: flex-end; justify-content: flex-end;
padding-right: ${theme.spacing(1)};
`, `,
modalContent: css` modalContent: css`
padding: ${theme.spacing(2)}; padding: ${theme.spacing(3)};
overflow: auto; overflow: auto;
width: 100%; width: 100%;
max-height: calc(90vh - ${theme.spacing(2)}); max-height: calc(90vh - ${theme.spacing(4)});
`,
modalButtonRow: css`
padding-top: ${theme.spacing(3)};
`, `,
}; };
}); });

View File

@ -10,7 +10,6 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
const getTabContentStyle = stylesFactory((theme: GrafanaThemeV2) => { const getTabContentStyle = stylesFactory((theme: GrafanaThemeV2) => {
return { return {
tabContent: css` tabContent: css`
padding: ${theme.spacing(1)};
background: ${theme.colors.background.primary}; background: ${theme.colors.background.primary};
`, `,
}; };

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { sumBy } from 'lodash'; import { sumBy } from 'lodash';
import { Modal, ConfirmModal, HorizontalGroup, Button } from '@grafana/ui'; import { Modal, ConfirmModal, Button } from '@grafana/ui';
import { DashboardModel, PanelModel } from '../../state'; import { DashboardModel, PanelModel } from '../../state';
import { useDashboardDelete } from './useDashboardDelete'; import { useDashboardDelete } from './useDashboardDelete';
import useAsyncFn from 'react-use/lib/useAsyncFn'; import useAsyncFn from 'react-use/lib/useAsyncFn';
@ -88,10 +88,10 @@ const ProvisionedDeleteModal = ({ hideModal, provisionedId }: { hideModal(): voi
<br /> <br />
File path: {provisionedId} File path: {provisionedId}
</p> </p>
<HorizontalGroup justify="center"> <Modal.ButtonRow>
<Button variant="secondary" onClick={hideModal}> <Button variant="secondary" onClick={hideModal}>
OK OK
</Button> </Button>
</HorizontalGroup> </Modal.ButtonRow>
</Modal> </Modal>
); );

View File

@ -1,5 +1,5 @@
import React, { FC, useCallback, useState } from 'react'; import React, { FC, useCallback, useState } from 'react';
import { Button, Field, Form, HorizontalGroup, Input } from '@grafana/ui'; import { Button, Field, Form, Modal, Input } from '@grafana/ui';
import { RepeatRowSelect } from '../RepeatRowSelect/RepeatRowSelect'; import { RepeatRowSelect } from '../RepeatRowSelect/RepeatRowSelect';
@ -33,12 +33,12 @@ export const RowOptionsForm: FC<Props> = ({ repeat, title, onUpdate, onCancel })
<RepeatRowSelect repeat={newRepeat} onChange={onChangeRepeat} /> <RepeatRowSelect repeat={newRepeat} onChange={onChangeRepeat} />
</Field> </Field>
<HorizontalGroup> <Modal.ButtonRow>
<Button type="submit">Update</Button> <Button type="submit">Update</Button>
<Button variant="secondary" onClick={onCancel}> <Button variant="secondary" onClick={onCancel}>
Cancel Cancel
</Button> </Button>
</HorizontalGroup> </Modal.ButtonRow>
</> </>
)} )}
</Form> </Form>

View File

@ -1,5 +1,5 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { Button, ConfirmModal, HorizontalGroup, Modal, stylesFactory, useTheme } from '@grafana/ui'; import { Button, ConfirmModal, Modal, stylesFactory, useTheme } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { DashboardModel } from 'app/features/dashboard/state'; import { DashboardModel } from 'app/features/dashboard/state';
@ -89,7 +89,7 @@ const ConfirmPluginDashboardSaveModal: React.FC<SaveDashboardModalProps> = ({ on
Use <strong>Save As</strong> to create custom version. Use <strong>Save As</strong> to create custom version.
</small> </small>
</div> </div>
<HorizontalGroup justify="center"> <Modal.ButtonRow>
<SaveDashboardAsButton dashboard={dashboard} onSaveSuccess={onDismiss} /> <SaveDashboardAsButton dashboard={dashboard} onSaveSuccess={onDismiss} />
<Button <Button
variant="destructive" variant="destructive"
@ -103,7 +103,7 @@ const ConfirmPluginDashboardSaveModal: React.FC<SaveDashboardModalProps> = ({ on
<Button variant="secondary" onClick={onDismiss}> <Button variant="secondary" onClick={onDismiss}>
Cancel Cancel
</Button> </Button>
</HorizontalGroup> </Modal.ButtonRow>
</div> </div>
</Modal> </Modal>
); );

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Button, HorizontalGroup, Modal, VerticalGroup } from '@grafana/ui'; import { Button, Modal } from '@grafana/ui';
import { SaveDashboardButton } from './SaveDashboardButton'; import { SaveDashboardButton } from './SaveDashboardButton';
import { DashboardModel } from '../../state'; import { DashboardModel } from '../../state';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
@ -27,24 +27,22 @@ export const UnsavedChangesModal: React.FC<UnsavedChangesModalProps> = ({
width: 500px; width: 500px;
`} `}
> >
<VerticalGroup align={'center'} spacing={'md'}> <h5>Do you want to save your changes?</h5>
<h4>Do you want to save your changes?</h4> <Modal.ButtonRow>
<HorizontalGroup justify="center"> <Button variant="secondary" onClick={onDismiss}>
<SaveDashboardButton dashboard={dashboard} onSaveSuccess={onSaveSuccess} /> Cancel
<Button </Button>
variant="destructive" <Button
onClick={() => { variant="destructive"
onDiscard(); onClick={() => {
onDismiss(); onDiscard();
}} onDismiss();
> }}
Discard >
</Button> Discard
<Button variant="secondary" onClick={onDismiss}> </Button>
Cancel <SaveDashboardButton dashboard={dashboard} onSaveSuccess={onSaveSuccess} />
</Button> </Modal.ButtonRow>
</HorizontalGroup>
</VerticalGroup>
</Modal> </Modal>
); );
}; };

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Button, HorizontalGroup, Input, Switch, Form, Field, InputControl } from '@grafana/ui'; import { Button, Input, Switch, Form, Field, InputControl, Modal } from '@grafana/ui';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { FolderPicker } from 'app/core/components/Select/FolderPicker'; import { FolderPicker } from 'app/core/components/Select/FolderPicker';
import { SaveDashboardFormProps } from '../types'; import { SaveDashboardFormProps } from '../types';
@ -114,14 +114,14 @@ export const SaveDashboardAsForm: React.FC<SaveDashboardFormProps & { isNew?: bo
<Field label="Copy tags"> <Field label="Copy tags">
<Switch name="copyTags" ref={register} /> <Switch name="copyTags" ref={register} />
</Field> </Field>
<HorizontalGroup> <Modal.ButtonRow>
<Button type="submit" aria-label="Save dashboard button">
Save
</Button>
<Button variant="secondary" onClick={onCancel}> <Button variant="secondary" onClick={onCancel}>
Cancel Cancel
</Button> </Button>
</HorizontalGroup> <Button type="submit" aria-label="Save dashboard button">
Save
</Button>
</Modal.ButtonRow>
</> </>
)} )}
</Form> </Form>

View File

@ -1,6 +1,6 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { Button, Checkbox, Form, HorizontalGroup, TextArea } from '@grafana/ui'; import { Button, Checkbox, Form, Modal, TextArea } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { SaveDashboardFormProps } from '../types'; import { SaveDashboardFormProps } from '../types';
@ -36,7 +36,7 @@ export const SaveDashboardForm: React.FC<SaveDashboardFormProps> = ({ dashboard,
> >
{({ register, errors }) => ( {({ register, errors }) => (
<> <>
<div className="gf-form-group"> <div>
{hasTimeChanged && ( {hasTimeChanged && (
<Checkbox <Checkbox
label="Save current time range as dashboard default" label="Save current time range as dashboard default"
@ -58,14 +58,14 @@ export const SaveDashboardForm: React.FC<SaveDashboardFormProps> = ({ dashboard,
<TextArea name="message" ref={register} placeholder="Add a note to describe your changes." autoFocus /> <TextArea name="message" ref={register} placeholder="Add a note to describe your changes." autoFocus />
</div> </div>
<HorizontalGroup> <Modal.ButtonRow>
<Button type="submit" aria-label={selectors.pages.SaveDashboardModal.save}>
Save
</Button>
<Button variant="secondary" onClick={onCancel}> <Button variant="secondary" onClick={onCancel}>
Cancel Cancel
</Button> </Button>
</HorizontalGroup> <Button type="submit" aria-label={selectors.pages.SaveDashboardModal.save}>
Save
</Button>
</Modal.ButtonRow>
</> </>
)} )}
</Form> </Form>

View File

@ -1,7 +1,7 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { saveAs } from 'file-saver'; import { saveAs } from 'file-saver';
import { Button, HorizontalGroup, stylesFactory, TextArea, useTheme, VerticalGroup } from '@grafana/ui'; import { Button, Modal, stylesFactory, TextArea, useTheme } from '@grafana/ui';
import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard'; import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard';
import { SaveDashboardFormProps } from '../types'; import { SaveDashboardFormProps } from '../types';
import { AppEvents, GrafanaTheme } from '@grafana/data'; import { AppEvents, GrafanaTheme } from '@grafana/data';
@ -29,8 +29,8 @@ export const SaveProvisionedDashboardForm: React.FC<SaveDashboardFormProps> = ({
const styles = getStyles(theme); const styles = getStyles(theme);
return ( return (
<> <>
<VerticalGroup spacing="lg"> <div>
<small> <div>
This dashboard cannot be saved from the Grafana UI because it has been provisioned from another source. Copy This dashboard cannot be saved from the Grafana UI because it has been provisioned from another source. Copy
the JSON or save it to a file below, then you can update your dashboard in the provisioning source. the JSON or save it to a file below, then you can update your dashboard in the provisioning source.
<br /> <br />
@ -46,8 +46,7 @@ export const SaveProvisionedDashboardForm: React.FC<SaveDashboardFormProps> = ({
</a>{' '} </a>{' '}
for more information about provisioning. for more information about provisioning.
</i> </i>
</small> <br /> <br />
<div>
<strong>File path: </strong> {dashboard.meta.provisionedExternalId} <strong>File path: </strong> {dashboard.meta.provisionedExternalId}
</div> </div>
<TextArea <TextArea
@ -58,16 +57,16 @@ export const SaveProvisionedDashboardForm: React.FC<SaveDashboardFormProps> = ({
}} }}
className={styles.json} className={styles.json}
/> />
<HorizontalGroup> <Modal.ButtonRow>
<Button variant="secondary" onClick={onCancel}>
Cancel
</Button>
<CopyToClipboard text={() => dashboardJSON} elType={Button} onSuccess={onCopyToClipboardSuccess}> <CopyToClipboard text={() => dashboardJSON} elType={Button} onSuccess={onCopyToClipboardSuccess}>
Copy JSON to clipboard Copy JSON to clipboard
</CopyToClipboard> </CopyToClipboard>
<Button onClick={saveToFile}>Save JSON to file</Button> <Button onClick={saveToFile}>Save JSON to file</Button>
<Button variant="secondary" onClick={onCancel}> </Modal.ButtonRow>
Cancel </div>
</Button>
</HorizontalGroup>
</VerticalGroup>
</> </>
); );
}; };

View File

@ -1,5 +1,5 @@
import React, { FormEvent, PureComponent } from 'react'; import React, { FormEvent, PureComponent } from 'react';
import { RadioButtonGroup, Switch, Field, TextArea, ClipboardButton } from '@grafana/ui'; import { RadioButtonGroup, Switch, Field, TextArea, ClipboardButton, Modal } from '@grafana/ui';
import { SelectableValue, AppEvents } from '@grafana/data'; import { SelectableValue, AppEvents } from '@grafana/data';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { appEvents } from 'app/core/core'; import { appEvents } from 'app/core/core';
@ -96,9 +96,11 @@ export class ShareEmbed extends PureComponent<Props, State> {
> >
<TextArea rows={5} value={iframeHtml} onChange={this.onIframeHtmlChange}></TextArea> <TextArea rows={5} value={iframeHtml} onChange={this.onIframeHtmlChange}></TextArea>
</Field> </Field>
<ClipboardButton variant="primary" getText={this.getIframeHtml} onClipboardCopy={this.onIframeHtmlCopy}> <Modal.ButtonRow>
Copy to clipboard <ClipboardButton variant="primary" getText={this.getIframeHtml} onClipboardCopy={this.onIframeHtmlCopy}>
</ClipboardButton> Copy to clipboard
</ClipboardButton>
</Modal.ButtonRow>
</> </>
); );
} }

View File

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { saveAs } from 'file-saver'; import { saveAs } from 'file-saver';
import { Button, Field, Switch } from '@grafana/ui'; import { Button, Field, Modal, Switch } from '@grafana/ui';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { DashboardExporter } from 'app/features/dashboard/components/DashExportModal'; import { DashboardExporter } from 'app/features/dashboard/components/DashExportModal';
import { appEvents } from 'app/core/core'; import { appEvents } from 'app/core/core';
@ -95,17 +95,17 @@ export class ShareExport extends PureComponent<Props, State> {
<Field label="Export for sharing externally"> <Field label="Export for sharing externally">
<Switch value={shareExternally} onChange={this.onShareExternallyChange} /> <Switch value={shareExternally} onChange={this.onShareExternallyChange} />
</Field> </Field>
<div className="gf-form-button-row"> <Modal.ButtonRow>
<Button variant="primary" onClick={this.onSaveAsFile}> <Button variant="secondary" onClick={onDismiss}>
Save to file Cancel
</Button> </Button>
<Button variant="secondary" onClick={this.onViewJson}> <Button variant="secondary" onClick={this.onViewJson}>
View JSON View JSON
</Button> </Button>
<Button variant="secondary" onClick={onDismiss}> <Button variant="primary" onClick={this.onSaveAsFile}>
Cancel Save to file
</Button> </Button>
</div> </Modal.ButtonRow>
</> </>
); );
} }

View File

@ -1,5 +1,5 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { Button, ClipboardButton, Icon, Spinner, Select, Input, LinkButton, Field } from '@grafana/ui'; import { Button, ClipboardButton, Icon, Spinner, Select, Input, LinkButton, Field, Modal } from '@grafana/ui';
import { AppEvents, SelectableValue } from '@grafana/data'; import { AppEvents, SelectableValue } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime'; import { getBackendSrv } from '@grafana/runtime';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
@ -233,19 +233,19 @@ export class ShareSnapshot extends PureComponent<Props, State> {
<Input type="number" width={21} value={timeoutSeconds} onChange={this.onTimeoutChange} /> <Input type="number" width={21} value={timeoutSeconds} onChange={this.onTimeoutChange} />
</Field> </Field>
<div className="gf-form-button-row"> <Modal.ButtonRow>
<Button variant="primary" disabled={isLoading} onClick={this.createSnapshot()}> <Button variant="secondary" onClick={onDismiss}>
Local Snapshot Cancel
</Button> </Button>
{externalEnabled && ( {externalEnabled && (
<Button variant="secondary" disabled={isLoading} onClick={this.createSnapshot(true)}> <Button variant="secondary" disabled={isLoading} onClick={this.createSnapshot(true)}>
{sharingButtonText} {sharingButtonText}
</Button> </Button>
)} )}
<Button variant="secondary" onClick={onDismiss}> <Button variant="primary" disabled={isLoading} onClick={this.createSnapshot()}>
Cancel Local Snapshot
</Button> </Button>
</div> </Modal.ButtonRow>
</> </>
); );
} }

View File

@ -1,8 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Button, Field, Input, Modal, useStyles } from '@grafana/ui'; import { Button, Field, Input, Modal } from '@grafana/ui';
import { FolderPicker } from 'app/core/components/Select/FolderPicker'; import { FolderPicker } from 'app/core/components/Select/FolderPicker';
import { PanelModel } from '../../../dashboard/state'; import { PanelModel } from '../../../dashboard/state';
import { css } from '@emotion/css';
import { usePanelSave } from '../../utils/usePanelSave'; import { usePanelSave } from '../../utils/usePanelSave';
interface AddLibraryPanelContentsProps { interface AddLibraryPanelContentsProps {
onDismiss: () => void; onDismiss: () => void;
@ -11,7 +10,6 @@ interface AddLibraryPanelContentsProps {
} }
export const AddLibraryPanelContents = ({ panel, initialFolderId, onDismiss }: AddLibraryPanelContentsProps) => { export const AddLibraryPanelContents = ({ panel, initialFolderId, onDismiss }: AddLibraryPanelContentsProps) => {
const styles = useStyles(getStyles);
const [folderId, setFolderId] = useState(initialFolderId); const [folderId, setFolderId] = useState(initialFolderId);
const [panelTitle, setPanelTitle] = useState(panel.title); const [panelTitle, setPanelTitle] = useState(panel.title);
const { saveLibraryPanel } = usePanelSave(); const { saveLibraryPanel } = usePanelSave();
@ -25,7 +23,7 @@ export const AddLibraryPanelContents = ({ panel, initialFolderId, onDismiss }: A
<FolderPicker onChange={({ id }) => setFolderId(id)} initialFolderId={initialFolderId} /> <FolderPicker onChange={({ id }) => setFolderId(id)} initialFolderId={initialFolderId} />
</Field> </Field>
<div className={styles.buttons}> <Modal.ButtonRow>
<Button <Button
onClick={() => { onClick={() => {
panel.title = panelTitle; panel.title = panelTitle;
@ -37,7 +35,7 @@ export const AddLibraryPanelContents = ({ panel, initialFolderId, onDismiss }: A
<Button variant="secondary" onClick={onDismiss}> <Button variant="secondary" onClick={onDismiss}>
Cancel Cancel
</Button> </Button>
</div> </Modal.ButtonRow>
</> </>
); );
}; };
@ -53,10 +51,3 @@ export const AddLibraryPanelModal: React.FC<Props> = ({ isOpen = false, panel, i
</Modal> </Modal>
); );
}; };
const getStyles = () => ({
buttons: css`
display: flex;
gap: 10px;
`,
});

View File

@ -1,5 +1,5 @@
import React, { FC, useEffect, useMemo, useReducer } from 'react'; import React, { FC, useEffect, useMemo, useReducer } from 'react';
import { Button, HorizontalGroup, Modal, useStyles } from '@grafana/ui'; import { Button, Modal, useStyles } from '@grafana/ui';
import { LoadingState } from '@grafana/data'; import { LoadingState } from '@grafana/data';
import { LibraryPanelDTO } from '../../types'; import { LibraryPanelDTO } from '../../types';
@ -35,14 +35,14 @@ export const DeleteLibraryPanelModal: FC<Props> = ({ libraryPanel, onDismiss, on
{connected ? <HasConnectedDashboards dashboardTitles={dashboardTitles} /> : null} {connected ? <HasConnectedDashboards dashboardTitles={dashboardTitles} /> : null}
{!connected ? <Confirm /> : null} {!connected ? <Confirm /> : null}
<HorizontalGroup> <Modal.ButtonRow>
<Button variant="destructive" onClick={onConfirm} disabled={connected}>
Delete
</Button>
<Button variant="secondary" onClick={onDismiss}> <Button variant="secondary" onClick={onDismiss}>
Cancel Cancel
</Button> </Button>
</HorizontalGroup> <Button variant="destructive" onClick={onConfirm} disabled={connected}>
Delete
</Button>
</Modal.ButtonRow>
</div> </div>
) : null} ) : null}
</Modal> </Modal>

View File

@ -1,5 +1,5 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { Button, HorizontalGroup, Icon, Input, Modal, useStyles } from '@grafana/ui'; import { Button, Icon, Input, Modal, useStyles } from '@grafana/ui';
import { useAsync, useDebounce } from 'react-use'; import { useAsync, useDebounce } from 'react-use';
import { getBackendSrv } from 'app/core/services/backend_srv'; import { getBackendSrv } from 'app/core/services/backend_srv';
import { usePanelSave } from '../../utils/usePanelSave'; import { usePanelSave } from '../../utils/usePanelSave';
@ -98,7 +98,13 @@ export const SaveLibraryPanelModal: React.FC<Props> = ({
</tbody> </tbody>
</table> </table>
)} )}
<HorizontalGroup> <Modal.ButtonRow>
<Button variant="secondary" onClick={onDismiss}>
Cancel
</Button>
<Button variant="destructive" onClick={discardAndClose}>
Discard
</Button>
<Button <Button
onClick={() => { onClick={() => {
saveLibraryPanel(panel, folderId).then(() => { saveLibraryPanel(panel, folderId).then(() => {
@ -109,13 +115,7 @@ export const SaveLibraryPanelModal: React.FC<Props> = ({
> >
Update all Update all
</Button> </Button>
<Button variant="destructive" onClick={discardAndClose}> </Modal.ButtonRow>
Discard
</Button>
<Button variant="secondary" onClick={onDismiss}>
Cancel
</Button>
</HorizontalGroup>
</div> </div>
</Modal> </Modal>
); );

View File

@ -132,11 +132,11 @@ export const StartModal: FC<StartModalProps> = ({ playlist, onDismiss }) => {
onChange={(e) => setAutofit(e.currentTarget.checked)} onChange={(e) => setAutofit(e.currentTarget.checked)}
/> />
</VerticalGroup> </VerticalGroup>
<div className="gf-form-button-row"> <Modal.ButtonRow>
<Button variant="primary" onClick={onStart}> <Button variant="primary" onClick={onStart}>
Start {playlist.name} Start {playlist.name}
</Button> </Button>
</div> </Modal.ButtonRow>
</Modal> </Modal>
); );
}; };