mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
de65cef850
commit
4643bfa539
@ -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)',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -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`
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -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)};
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -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};
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
`,
|
|
||||||
});
|
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user