GrafanaUI: Create Box component (#73637)

This commit is contained in:
Joao Silva 2023-09-21 17:13:19 +01:00 committed by GitHub
parent 7e4ae5fdb6
commit 42cc6b1842
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 734 additions and 245 deletions

View File

@ -1,6 +1,5 @@
import { css, cx } from '@emotion/css';
import React, { AriaRole, HTMLAttributes, ReactNode } from 'react';
import tinycolor2 from 'tinycolor2';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
@ -9,6 +8,8 @@ import { useTheme2 } from '../../themes';
import { IconName } from '../../types/icon';
import { Button } from '../Button/Button';
import { Icon } from '../Icon/Icon';
import { Box } from '../Layout/Box/Box';
import { Text } from '../Text/Text';
export type AlertVariant = 'success' | 'warning' | 'error' | 'info';
@ -55,42 +56,55 @@ export const Alert = React.forwardRef<HTMLDivElement, Props>(
return (
<div
ref={ref}
className={cx(styles.alert, className)}
className={cx(styles.wrapper, className)}
data-testid={selectors.components.Alert.alertV2(severity)}
role={role}
aria-label={ariaLabel}
{...restProps}
>
<div className={styles.icon}>
<Icon size="xl" name={getIconFromSeverity(severity)} />
</div>
<Box
display="flex"
backgroundColor={severity}
borderRadius="default"
paddingY={1}
paddingX={2}
borderStyle="solid"
borderColor={severity}
alignItems="stretch"
boxShadow={elevated ? 'z3' : undefined}
>
<Box paddingTop={1} paddingRight={2}>
<div className={styles.icon}>
<Icon size="xl" name={getIconFromSeverity(severity)} />
</div>
</Box>
<div className={styles.body}>
<div className={styles.title}>{title}</div>
{children && <div className={styles.content}>{children}</div>}
</div>
<Box paddingY={1} grow={1}>
<Text weight="medium">{title}</Text>
{children && <div className={styles.content}>{children}</div>}
</Box>
{/* If onRemove is specified, giving preference to onRemove */}
{onRemove && !buttonContent && (
<div className={styles.close}>
<Button
aria-label="Close alert"
icon="times"
onClick={onRemove}
type="button"
fill="text"
variant="secondary"
/>
</div>
)}
{/* If onRemove is specified, giving preference to onRemove */}
{onRemove && !buttonContent && (
<div className={styles.close}>
<Button
aria-label="Close alert"
icon="times"
onClick={onRemove}
type="button"
fill="text"
variant="secondary"
/>
</div>
)}
{onRemove && buttonContent && (
<div className={styles.buttonWrapper}>
<Button aria-label="Close alert" variant="secondary" onClick={onRemove} type="button">
{buttonContent}
</Button>
</div>
)}
{onRemove && buttonContent && (
<Box marginLeft={1} display="flex" alignItems="center">
<Button aria-label="Close alert" variant="secondary" onClick={onRemove} type="button">
{buttonContent}
</Button>
</Box>
)}
</Box>
</div>
);
}
@ -120,24 +134,13 @@ const getStyles = (
topSpacing?: number
) => {
const color = theme.colors[severity];
const borderRadius = theme.shape.radius.default;
const borderColor = tinycolor2(color.border).setAlpha(0.2).toString();
return {
alert: css({
label: 'alert',
wrapper: css({
flexGrow: 1,
position: 'relative',
borderRadius,
display: 'flex',
flexDirection: 'row',
alignItems: 'stretch',
background: color.transparent,
boxShadow: elevated ? theme.shadows.z3 : 'none',
padding: theme.spacing(1, 2),
border: `1px solid ${borderColor}`,
marginBottom: theme.spacing(bottomSpacing ?? 2),
marginTop: theme.spacing(topSpacing ?? 0),
position: 'relative',
'&:before': {
content: '""',
@ -151,33 +154,13 @@ const getStyles = (
},
}),
icon: css({
padding: theme.spacing(1, 2, 0, 0),
color: color.text,
display: 'flex',
}),
title: css({
fontWeight: theme.typography.fontWeightMedium,
}),
body: css({
padding: theme.spacing(1, 0),
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
overflowWrap: 'break-word',
wordBreak: 'break-word',
}),
content: css({
paddingTop: hasTitle ? theme.spacing(0.5) : 0,
maxHeight: '50vh',
overflowY: 'auto',
}),
buttonWrapper: css({
marginLeft: theme.spacing(1),
display: 'flex',
alignItems: 'center',
alignSelf: 'center',
}),
close: css({
position: 'relative',
color: theme.colors.text.secondary,

View File

@ -0,0 +1,138 @@
import { Meta, StoryFn } from '@storybook/react';
import React from 'react';
import { SpacingTokenControl } from '../../../utils/storybook/themeStorybookControls';
import { Text } from '../../Text/Text';
import { Flex } from '../Flex/Flex';
import { Box, BackgroundColor, BorderColor, BorderStyle, BorderRadius, BoxShadow } from './Box';
import mdx from './Box.mdx';
const backgroundOptions: BackgroundColor[] = ['primary', 'secondary', 'canvas', 'error', 'success', 'warning', 'info'];
const borderColorOptions: BorderColor[] = ['weak', 'medium', 'strong', 'error', 'success', 'warning', 'info'];
const borderStyleOptions: BorderStyle[] = ['dashed', 'solid'];
const borderRadiusOptions: BorderRadius[] = ['default', 'pill', 'circle'];
const boxShadowOptions: BoxShadow[] = ['z1', 'z2', 'z3'];
const meta: Meta<typeof Box> = {
title: 'General/Layout/Box',
component: Box,
parameters: {
docs: {
page: mdx,
},
controls: { exclude: ['element'] },
},
};
const Item = ({ background }: { background?: string }) => {
return (
<div
style={{
width: '50px',
height: '50px',
background,
}}
/>
);
};
export const Basic: StoryFn<typeof Box> = (args) => {
return (
<div style={{ backgroundColor: 'green' }}>
<Box {...args}>
<Item background="red" />
</Box>
</div>
);
};
Basic.argTypes = {
grow: { control: 'number' },
shrink: { control: 'number' },
margin: SpacingTokenControl,
marginX: SpacingTokenControl,
marginY: SpacingTokenControl,
marginTop: SpacingTokenControl,
marginBottom: SpacingTokenControl,
marginLeft: SpacingTokenControl,
marginRight: SpacingTokenControl,
padding: SpacingTokenControl,
paddingX: SpacingTokenControl,
paddingY: SpacingTokenControl,
paddingTop: SpacingTokenControl,
paddingBottom: SpacingTokenControl,
paddingLeft: SpacingTokenControl,
paddingRight: SpacingTokenControl,
display: { control: 'select', options: ['flex', 'block', 'inline', 'none'] },
backgroundColor: { control: 'select', options: backgroundOptions },
borderStyle: { control: 'select', options: borderStyleOptions },
borderColor: { control: 'select', options: borderColorOptions },
borderRadius: { control: 'select', options: borderRadiusOptions },
boxShadow: { control: 'select', options: boxShadowOptions },
};
export const Background: StoryFn<typeof Box> = () => {
return (
<Flex gap={4}>
{backgroundOptions.map((background) => (
<Flex key={background} direction="column" alignItems="flex-start">
{background}
<Box backgroundColor={background} borderColor="strong" borderStyle="solid">
<Item />
</Box>
</Flex>
))}
</Flex>
);
};
export const Border: StoryFn<typeof Box> = () => {
return (
<Flex direction="column" gap={4}>
<div>
<Text variant="h4">Border Color</Text>
<Flex gap={4} wrap="wrap">
{borderColorOptions.map((border) => (
<Flex key={border} direction="column" alignItems="flex-start">
{border}
<Box borderColor={border} borderStyle="solid">
<Item />
</Box>
</Flex>
))}
</Flex>
</div>
<div>
<Text variant="h4">Border Style</Text>
<Flex gap={4} wrap="wrap">
{borderStyleOptions.map((border) => (
<Flex key={border} direction="column" alignItems="flex-start">
{border}
<Box borderColor="info" borderStyle={border}>
<Item />
</Box>
</Flex>
))}
</Flex>
</div>
</Flex>
);
};
export const Shadow: StoryFn<typeof Box> = () => {
return (
<Flex gap={4}>
{boxShadowOptions.map((shadow) => (
<Flex key={shadow} direction="column" alignItems="flex-start">
{shadow}
<Box boxShadow={shadow} borderColor="strong" borderStyle="solid">
<Item />
</Box>
</Flex>
))}
</Flex>
);
};
export default meta;

View File

@ -0,0 +1,39 @@
import { Meta, ArgTypes } from '@storybook/blocks';
import { Box } from './Box';
<Meta title="MDX|Box" component={Box} />
# Box
The Box Component is the most basic layout component. It can be used to build more complex components and layouts with properties
that use our design tokens instead of using CSS.
### Usage
#### When to use
Use it whenever you would use custom CSS.
#### When not to use
If you need layout styles, use the Stack, Flex or Grid components instead.
### How to add a prop to Box
1. Make sure you absolutely need this prop. If in doubt, ask someone from the design system team.
2. Add the prop to the `BoxProps` interface in `Box.tsx`.
- Make sure it is strictly typed, making use of design tokens if needed. Instead of `[propName]: number`, use `[propName]: ThemeSpacingTokens`;
- If it is a CSS prop, you should make it responsive. To do so, instead of defining it as `[propName]: ThemeSpacingTokens`,
define it as `[propName]: ResponsiveProp<ThemeSpacingTokens>`.
3. Add it to the CSS array in `getStyles` in `Box.tsx`.
- If it is a `ResponsiveProp`, you should use the `getResponsiveStyle` helper function
```
getResponsiveStyle(theme, [propName], (val) => ({
[cssProp]: theme.spacing(val),
})),
```
4. Add it to the `Box` story in `Box.internal.story.tsx`, by explicity adding it to `Basic.argTypes`
### Props
<ArgTypes of={Box} />

View File

@ -0,0 +1,267 @@
import { css } from '@emotion/css';
import React, { ElementType } from 'react';
import { GrafanaTheme2, ThemeSpacingTokens, ThemeShape, ThemeShadows } from '@grafana/data';
import { useStyles2 } from '../../../themes';
import { AlignItems, JustifyContent } from '../Flex/Flex';
import { ResponsiveProp, getResponsiveStyle } from '../utils/responsiveness';
type Display = 'flex' | 'block' | 'inline' | 'none';
export type BackgroundColor = keyof GrafanaTheme2['colors']['background'] | 'error' | 'success' | 'warning' | 'info';
export type BorderStyle = 'solid' | 'dashed';
export type BorderColor = keyof GrafanaTheme2['colors']['border'] | 'error' | 'success' | 'warning' | 'info';
export type BorderRadius = keyof ThemeShape['radius'];
export type BoxShadow = keyof ThemeShadows;
interface BoxProps {
// Margin props
/** Sets the property `margin` */
margin?: ResponsiveProp<ThemeSpacingTokens>;
/** Sets the properties `margin-top` and `margin-bottom`. Higher priority than margin. */
marginX?: ResponsiveProp<ThemeSpacingTokens>;
/** Sets the properties `margin-left` and `margin-right`. Higher priority than margin. */
marginY?: ResponsiveProp<ThemeSpacingTokens>;
/** Sets the property `margin-top`. Higher priority than margin and marginY. */
marginTop?: ResponsiveProp<ThemeSpacingTokens>;
/** Sets the property `margin-bottom`. Higher priority than margin and marginXY */
marginBottom?: ResponsiveProp<ThemeSpacingTokens>;
/** Sets the property `margin-left`. Higher priority than margin and marginX. */
marginLeft?: ResponsiveProp<ThemeSpacingTokens>;
/** Sets the property `margin-right`. Higher priority than margin and marginX. */
marginRight?: ResponsiveProp<ThemeSpacingTokens>;
// Padding props
/** Sets the property `padding` */
padding?: ResponsiveProp<ThemeSpacingTokens>;
/** Sets the properties `padding-top` and `padding-bottom`. Higher priority than padding. */
paddingX?: ResponsiveProp<ThemeSpacingTokens>;
/** Sets the properties `padding-left` and `padding-right`. Higher priority than padding. */
paddingY?: ResponsiveProp<ThemeSpacingTokens>;
/** Sets the property `padding-top`. Higher priority than padding and paddingY. */
paddingTop?: ResponsiveProp<ThemeSpacingTokens>;
/** Sets the property `padding-bottom`. Higher priority than padding and paddingY. */
paddingBottom?: ResponsiveProp<ThemeSpacingTokens>;
/** Sets the property `padding-left`. Higher priority than padding and paddingX. */
paddingLeft?: ResponsiveProp<ThemeSpacingTokens>;
/** Sets the property `padding-right`. Higher priority than padding and paddingX. */
paddingRight?: ResponsiveProp<ThemeSpacingTokens>;
// Border Props
borderStyle?: ResponsiveProp<BorderStyle>;
borderColor?: ResponsiveProp<BorderColor>;
borderRadius?: ResponsiveProp<BorderRadius>;
// Flex Props
/** Sets the property `flex` */
grow?: ResponsiveProp<number>;
/** Sets the property `flex-shrink` */
shrink?: ResponsiveProp<number>;
alignItems?: ResponsiveProp<AlignItems>;
justifyContent?: ResponsiveProp<JustifyContent>;
// Other props
backgroundColor?: ResponsiveProp<BackgroundColor>;
display?: ResponsiveProp<Display>;
boxShadow?: ResponsiveProp<BoxShadow>;
/** Sets the HTML element that will be rendered as a Box. Defaults to 'div' */
element?: ElementType;
}
export const Box = ({
children,
margin,
marginX,
marginY,
marginTop,
marginBottom,
marginLeft,
marginRight,
padding,
paddingX,
paddingY,
paddingTop,
paddingBottom,
paddingLeft,
paddingRight,
display,
backgroundColor,
grow,
shrink,
borderColor,
borderStyle,
borderRadius,
justifyContent,
alignItems,
boxShadow,
element,
}: React.PropsWithChildren<BoxProps>) => {
const styles = useStyles2(
getStyles,
margin,
marginX,
marginY,
marginTop,
marginBottom,
marginLeft,
marginRight,
padding,
paddingX,
paddingY,
paddingTop,
paddingBottom,
paddingLeft,
paddingRight,
display,
backgroundColor,
grow,
shrink,
borderColor,
borderStyle,
borderRadius,
justifyContent,
alignItems,
boxShadow
);
const Element = element ?? 'div';
return <Element className={styles.root}>{children}</Element>;
};
Box.displayName = 'Box';
const customBorderColor = (color: BorderColor, theme: GrafanaTheme2) => {
switch (color) {
case 'error':
case 'success':
case 'info':
case 'warning':
return theme.colors[color].border;
default:
return color ? theme.colors.border[color] : undefined;
}
};
const customBackgroundColor = (color: BackgroundColor, theme: GrafanaTheme2) => {
switch (color) {
case 'error':
case 'success':
case 'info':
case 'warning':
return theme.colors[color].transparent;
default:
return color ? theme.colors.background[color] : undefined;
}
};
const getStyles = (
theme: GrafanaTheme2,
margin: BoxProps['margin'],
marginX: BoxProps['marginX'],
marginY: BoxProps['marginY'],
marginTop: BoxProps['marginTop'],
marginBottom: BoxProps['marginBottom'],
marginLeft: BoxProps['marginLeft'],
marginRight: BoxProps['marginRight'],
padding: BoxProps['padding'],
paddingX: BoxProps['paddingX'],
paddingY: BoxProps['paddingY'],
paddingTop: BoxProps['paddingTop'],
paddingBottom: BoxProps['paddingBottom'],
paddingLeft: BoxProps['paddingLeft'],
paddingRight: BoxProps['paddingRight'],
display: BoxProps['display'],
backgroundColor: BoxProps['backgroundColor'],
grow: BoxProps['grow'],
shrink: BoxProps['shrink'],
borderColor: BoxProps['borderColor'],
borderStyle: BoxProps['borderStyle'],
borderRadius: BoxProps['borderRadius'],
justifyContent: BoxProps['justifyContent'],
alignItems: BoxProps['alignItems'],
boxShadow: BoxProps['boxShadow']
) => {
return {
root: css([
getResponsiveStyle(theme, margin, (val) => ({
margin: theme.spacing(val),
})),
getResponsiveStyle(theme, marginX, (val) => ({
marginLeft: theme.spacing(val),
marginRight: theme.spacing(val),
})),
getResponsiveStyle(theme, marginY, (val) => ({
marginTop: theme.spacing(val),
marginBottom: theme.spacing(val),
})),
getResponsiveStyle(theme, marginTop, (val) => ({
marginTop: theme.spacing(val),
})),
getResponsiveStyle(theme, marginBottom, (val) => ({
marginBottom: theme.spacing(val),
})),
getResponsiveStyle(theme, marginLeft, (val) => ({
marginLeft: theme.spacing(val),
})),
getResponsiveStyle(theme, marginRight, (val) => ({
marginRight: theme.spacing(val),
})),
getResponsiveStyle(theme, padding, (val) => ({
padding: theme.spacing(val),
})),
getResponsiveStyle(theme, paddingX, (val) => ({
paddingLeft: theme.spacing(val),
paddingRight: theme.spacing(val),
})),
getResponsiveStyle(theme, paddingY, (val) => ({
paddingTop: theme.spacing(val),
paddingBottom: theme.spacing(val),
})),
getResponsiveStyle(theme, paddingTop, (val) => ({
paddingTop: theme.spacing(val),
})),
getResponsiveStyle(theme, paddingBottom, (val) => ({
paddingBottom: theme.spacing(val),
})),
getResponsiveStyle(theme, paddingLeft, (val) => ({
paddingLeft: theme.spacing(val),
})),
getResponsiveStyle(theme, paddingRight, (val) => ({
paddingRight: theme.spacing(val),
})),
getResponsiveStyle(theme, display, (val) => ({
display: val,
})),
getResponsiveStyle(theme, backgroundColor, (val) => ({
backgroundColor: customBackgroundColor(val, theme),
})),
getResponsiveStyle(theme, grow, (val) => ({
flex: val,
})),
getResponsiveStyle(theme, shrink, (val) => ({
flexShrink: val,
})),
getResponsiveStyle(theme, borderStyle, (val) => ({
borderStyle: val,
})),
getResponsiveStyle(theme, borderColor, (val) => ({
borderColor: customBorderColor(val, theme),
})),
(borderStyle || borderColor) && {
borderWidth: '1px',
},
getResponsiveStyle(theme, justifyContent, (val) => ({
justifyContent: val,
})),
getResponsiveStyle(theme, alignItems, (val) => ({
alignItems: val,
})),
getResponsiveStyle(theme, borderRadius, (val) => ({
borderRadius: theme.shape.radius[val],
})),
getResponsiveStyle(theme, boxShadow, (val) => ({
boxShadow: theme.shadows[val],
})),
]),
};
};

View File

@ -3,7 +3,8 @@ import React from 'react';
import { ThemeSpacingTokens } from '@grafana/data';
import { useTheme2 } from '../../themes';
import { useTheme2 } from '../../../themes';
import { SpacingTokenControl } from '../../../utils/storybook/themeStorybookControls';
import { Flex, JustifyContent, Wrap, Direction } from './Flex';
import mdx from './Flex.mdx';
@ -48,6 +49,31 @@ export const Basic: StoryFn<typeof Flex> = ({ direction, wrap, alignItems, justi
);
};
Basic.argTypes = {
gap: SpacingTokenControl,
direction: { control: 'select', options: ['row', 'row-reverse', 'column', 'column-reverse'] },
wrap: { control: 'select', options: ['nowrap', 'wrap', 'wrap-reverse'] },
alignItems: {
control: 'select',
options: ['stretch', 'flex-start', 'flex-end', 'center', 'baseline', 'start', 'end', 'self-start', 'self-end'],
},
justifyContent: {
control: 'select',
options: [
'flex-start',
'flex-end',
'center',
'space-between',
'space-around',
'space-evenly',
'start',
'end',
'left',
'right',
],
},
};
export const AlignItemsExamples: StoryFn<typeof Flex> = () => {
const theme = useTheme2();

View File

@ -3,7 +3,8 @@ import React from 'react';
import { GrafanaTheme2, ThemeSpacingTokens } from '@grafana/data';
import { useStyles2 } from '../../themes';
import { useStyles2 } from '../../../themes';
import { ResponsiveProp, getResponsiveStyle } from '../utils/responsiveness';
export type AlignItems =
| 'stretch'
@ -33,11 +34,11 @@ export type Direction = 'row' | 'row-reverse' | 'column' | 'column-reverse';
export type Wrap = 'nowrap' | 'wrap' | 'wrap-reverse';
interface FlexProps {
gap?: ThemeSpacingTokens;
alignItems?: AlignItems;
justifyContent?: JustifyContent;
direction?: Direction;
wrap?: Wrap;
gap?: ResponsiveProp<ThemeSpacingTokens>;
alignItems?: ResponsiveProp<AlignItems>;
justifyContent?: ResponsiveProp<JustifyContent>;
direction?: ResponsiveProp<Direction>;
wrap?: ResponsiveProp<Wrap>;
children?: React.ReactNode;
}
@ -57,20 +58,32 @@ Flex.displayName = 'Flex';
const getStyles = (
theme: GrafanaTheme2,
gap: ThemeSpacingTokens,
gap: FlexProps['gap'],
alignItems: FlexProps['alignItems'],
justifyContent: FlexProps['justifyContent'],
direction: FlexProps['direction'],
wrap: FlexProps['wrap']
) => {
return {
flex: css({
display: 'flex',
flexDirection: direction,
flexWrap: wrap,
alignItems: alignItems,
justifyContent: justifyContent,
gap: theme.spacing(gap),
}),
flex: css([
{
display: 'flex',
},
getResponsiveStyle<Direction>(theme, direction, (val) => ({
flexDirection: val,
})),
getResponsiveStyle<Wrap>(theme, wrap, (val) => ({
flexWrap: val,
})),
getResponsiveStyle<AlignItems>(theme, alignItems, (val) => ({
alignItems: val,
})),
getResponsiveStyle<JustifyContent>(theme, justifyContent, (val) => ({
justifyContent: val,
})),
getResponsiveStyle<ThemeSpacingTokens>(theme, gap, (val) => ({
gap: theme.spacing(val),
})),
]),
};
};

View File

@ -1,6 +1,8 @@
import { Meta, StoryFn } from '@storybook/react';
import React, { ReactNode } from 'react';
import { SpacingTokenControl } from '../../../utils/storybook/themeStorybookControls';
import { Stack } from './Stack';
import mdx from './Stack.mdx';
@ -12,6 +14,10 @@ const meta: Meta<typeof Stack> = {
page: mdx,
},
},
argTypes: {
gap: SpacingTokenControl,
direction: { control: 'select', options: ['row', 'row-reverse', 'column', 'column-reverse'] },
},
};
const Item = ({ children }: { children: ReactNode }) => (

View File

@ -3,10 +3,11 @@ import React from 'react';
import { ThemeSpacingTokens } from '@grafana/data';
import { Direction, Flex } from '../Flex/Flex';
import { ResponsiveProp } from '../utils/responsiveness';
interface StackProps {
direction?: Direction;
gap?: ThemeSpacingTokens;
direction?: ResponsiveProp<Direction>;
gap?: ResponsiveProp<ThemeSpacingTokens>;
}
export const Stack = ({ gap = 1, direction = 'column', children }: React.PropsWithChildren<StackProps>) => {

View File

@ -0,0 +1,65 @@
import { CSSInterpolation } from '@emotion/css';
import { GrafanaTheme2, ThemeBreakpointsKey } from '@grafana/data';
/**
* Type that represents a prop that can be responsive.
*
* @example To turn a prop like `margin: number` responsive, change it to `margin: ResponsiveProp<number>`.
*/
export type ResponsiveProp<T> = T | Responsive<T>;
type Responsive<T> = {
xs: T;
sm?: T;
md?: T;
lg?: T;
xl?: T;
xxl?: T;
};
function breakpointCSS<T>(
theme: GrafanaTheme2,
prop: Responsive<T>,
getCSS: (val: T) => CSSInterpolation,
key: ThemeBreakpointsKey
) {
const value = prop[key];
if (value !== undefined && value !== null) {
return {
[theme.breakpoints.up(key)]: getCSS(value),
};
}
return;
}
/**
* Function that converts a ResponsiveProp object into CSS
*
* @param theme Grafana theme object
* @param prop Prop as it is passed to the component
* @param getCSS Function that returns the css block for the prop
* @returns The CSS block repeated for each breakpoint
*
* @example To get the responsive css equivalent of `margin && { margin }`, you can write `getResponsiveStyle(theme, margin, (val) => { margin: val })`
*/
export function getResponsiveStyle<T>(
theme: GrafanaTheme2,
prop: ResponsiveProp<T> | undefined,
getCSS: (val: T) => CSSInterpolation
): CSSInterpolation {
if (prop === undefined || prop === null) {
return null;
}
if (typeof prop !== 'object' || !('xs' in prop)) {
return getCSS(prop);
}
return [
breakpointCSS(theme, prop, getCSS, 'xs'),
breakpointCSS(theme, prop, getCSS, 'sm'),
breakpointCSS(theme, prop, getCSS, 'md'),
breakpointCSS(theme, prop, getCSS, 'lg'),
breakpointCSS(theme, prop, getCSS, 'xl'),
breakpointCSS(theme, prop, getCSS, 'xxl'),
];
}

View File

@ -9,6 +9,7 @@
* be subject to the standard policies
*/
export * from './components/Flex/Flex';
export * from './components/Layout/Box/Box';
export * from './components/Layout/Flex/Flex';
export { Stack } from './components/Stack/Stack';
export { Stack } from './components/Layout/Stack/Stack';

View File

@ -0,0 +1 @@
export const SpacingTokenControl = { control: 'select', options: [0, 0.25, 0.5, 1, 1.5, 2, 3, 4, 5, 6, 8, 10] };

View File

@ -1,10 +1,11 @@
import { css, cx } from '@emotion/css';
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { config, locationService, reportInteraction } from '@grafana/runtime';
import { Button, useStyles2, Text } from '@grafana/ui';
import { Box, Flex } from '@grafana/ui/src/unstable';
import { Trans } from 'app/core/internationalization';
import { DashboardModel } from 'app/features/dashboard/state';
import { onAddLibraryPanel, onCreateNewPanel, onImportDashboard } from 'app/features/dashboard/utils/dashboard';
@ -23,121 +24,124 @@ const DashboardEmpty = ({ dashboard, canCreate }: Props) => {
const initialDatasource = useSelector((state) => state.dashboard.initialDatasource);
return (
<div className={styles.centeredContent}>
<div className={cx(styles.centeredContent, styles.wrapper)}>
<div className={cx(styles.containerBox, styles.centeredContent, styles.visualizationContainer)}>
<div className={styles.headerBig}>
<Text element="h1" textAlignment="center" weight="medium">
<Trans i18nKey="dashboard.empty.add-visualization-header">
Start your new dashboard by adding a visualization
</Trans>
</Text>
</div>
<div className={styles.bodyBig}>
<Text element="p" textAlignment="center" color="secondary">
<Trans i18nKey="dashboard.empty.add-visualization-body">
Select a data source and then query and visualize your data with charts, stats and tables or create
lists, markdowns and other widgets.
</Trans>
</Text>
</div>
<Button
size="lg"
icon="plus"
data-testid={selectors.pages.AddDashboard.itemButton('Create new panel button')}
onClick={() => {
const id = onCreateNewPanel(dashboard, initialDatasource);
reportInteraction('dashboards_emptydashboard_clicked', { item: 'add_visualization' });
locationService.partial({ editPanel: id, firstPanel: true });
dispatch(setInitialDatasource(undefined));
}}
disabled={!canCreate}
>
<Trans i18nKey="dashboard.empty.add-visualization-button">Add visualization</Trans>
</Button>
</div>
<div className={cx(styles.centeredContent, styles.others)}>
{config.featureToggles.vizAndWidgetSplit && (
<div className={cx(styles.containerBox, styles.centeredContent, styles.widgetContainer)}>
<div className={styles.headerSmall}>
<Text element="h3" textAlignment="center" weight="medium">
<Trans i18nKey="dashboard.empty.add-widget-header">Add a widget</Trans>
</Text>
</div>
<div className={styles.bodySmall}>
<Flex alignItems="center" justifyContent="center">
<div className={styles.wrapper}>
<Flex alignItems="stretch" justifyContent="center" gap={4} direction="column">
<Box borderStyle="dashed" borderColor="info" padding={4}>
<Flex direction="column" alignItems="center" gap={2}>
<Text element="h1" textAlignment="center" weight="medium">
<Trans i18nKey="dashboard.empty.add-visualization-header">
Start your new dashboard by adding a visualization
</Trans>
</Text>
<Box marginBottom={2} paddingX={4}>
<Text element="p" textAlignment="center" color="secondary">
<Trans i18nKey="dashboard.empty.add-widget-body">Create lists, markdowns and other widgets</Trans>
<Trans i18nKey="dashboard.empty.add-visualization-body">
Select a data source and then query and visualize your data with charts, stats and tables or create
lists, markdowns and other widgets.
</Trans>
</Text>
</div>
</Box>
<Button
size="lg"
icon="plus"
fill="outline"
data-testid={selectors.pages.AddDashboard.itemButton('Create new widget button')}
data-testid={selectors.pages.AddDashboard.itemButton('Create new panel button')}
onClick={() => {
reportInteraction('dashboards_emptydashboard_clicked', { item: 'add_widget' });
locationService.partial({ addWidget: true });
const id = onCreateNewPanel(dashboard, initialDatasource);
reportInteraction('dashboards_emptydashboard_clicked', { item: 'add_visualization' });
locationService.partial({ editPanel: id, firstPanel: true });
dispatch(setInitialDatasource(undefined));
}}
disabled={!canCreate}
>
<Trans i18nKey="dashboard.empty.add-widget-button">Add widget</Trans>
<Trans i18nKey="dashboard.empty.add-visualization-button">Add visualization</Trans>
</Button>
</div>
)}
<div className={cx(styles.containerBox, styles.centeredContent, styles.libraryContainer)}>
<div className={styles.headerSmall}>
<Text element="h3" textAlignment="center" weight="medium">
<Trans i18nKey="dashboard.empty.add-library-panel-header">Import panel</Trans>
</Text>
</div>
<div className={styles.bodySmall}>
<Text element="p" textAlignment="center" color="secondary">
<Trans i18nKey="dashboard.empty.add-library-panel-body">
Add visualizations that are shared with other dashboards.
</Trans>
</Text>
</div>
<Button
icon="plus"
fill="outline"
data-testid={selectors.pages.AddDashboard.itemButton('Add a panel from the panel library button')}
onClick={() => {
reportInteraction('dashboards_emptydashboard_clicked', { item: 'import_from_library' });
onAddLibraryPanel(dashboard);
}}
disabled={!canCreate}
>
<Trans i18nKey="dashboard.empty.add-library-panel-button">Add library panel</Trans>
</Button>
</div>
<div className={cx(styles.containerBox, styles.centeredContent, styles.rowContainer)}>
<div className={styles.headerSmall}>
<Text element="h3" textAlignment="center" weight="medium">
<Trans i18nKey="dashboard.empty.import-a-dashboard-header">Import a dashboard</Trans>
</Text>
</div>
<div className={styles.bodySmall}>
<Text element="p" textAlignment="center" color="secondary">
<Trans i18nKey="dashboard.empty.import-a-dashboard-body">
Import dashboards from files or <a href="https://grafana.com/grafana/dashboards/">grafana.com</a>.
</Trans>
</Text>
</div>
<Button
icon="upload"
fill="outline"
data-testid={selectors.pages.AddDashboard.itemButton('Import dashboard button')}
onClick={() => {
reportInteraction('dashboards_emptydashboard_clicked', { item: 'import_dashboard' });
onImportDashboard();
}}
disabled={!canCreate}
>
<Trans i18nKey="dashboard.empty.import-dashboard-button">Import dashboard</Trans>
</Button>
</div>
</div>
</Flex>
</Box>
<Flex direction={{ xs: 'column', md: 'row' }} wrap="wrap" gap={4}>
{config.featureToggles.vizAndWidgetSplit && (
<Box borderStyle="dashed" borderColor="info" padding={3} grow={1}>
<Flex direction="column" alignItems="center" gap={1}>
<Text element="h3" textAlignment="center" weight="medium">
<Trans i18nKey="dashboard.empty.add-widget-header">Add a widget</Trans>
</Text>
<Box marginBottom={2}>
<Text element="p" textAlignment="center" color="secondary">
<Trans i18nKey="dashboard.empty.add-widget-body">Create lists, markdowns and other widgets</Trans>
</Text>
</Box>
<Button
icon="plus"
fill="outline"
data-testid={selectors.pages.AddDashboard.itemButton('Create new widget button')}
onClick={() => {
reportInteraction('dashboards_emptydashboard_clicked', { item: 'add_widget' });
locationService.partial({ addWidget: true });
}}
disabled={!canCreate}
>
<Trans i18nKey="dashboard.empty.add-widget-button">Add widget</Trans>
</Button>
</Flex>
</Box>
)}
<Box borderStyle="dashed" borderColor="info" padding={3} grow={1}>
<Flex direction="column" alignItems="center" gap={1}>
<Text element="h3" textAlignment="center" weight="medium">
<Trans i18nKey="dashboard.empty.add-library-panel-header">Import panel</Trans>
</Text>
<Box marginBottom={2}>
<Text element="p" textAlignment="center" color="secondary">
<Trans i18nKey="dashboard.empty.add-library-panel-body">
Add visualizations that are shared with other dashboards.
</Trans>
</Text>
</Box>
<Button
icon="plus"
fill="outline"
data-testid={selectors.pages.AddDashboard.itemButton('Add a panel from the panel library button')}
onClick={() => {
reportInteraction('dashboards_emptydashboard_clicked', { item: 'import_from_library' });
onAddLibraryPanel(dashboard);
}}
disabled={!canCreate}
>
<Trans i18nKey="dashboard.empty.add-library-panel-button">Add library panel</Trans>
</Button>
</Flex>
</Box>
<Box borderStyle="dashed" borderColor="info" padding={3} grow={1}>
<Flex direction="column" alignItems="center" gap={1}>
<Text element="h3" textAlignment="center" weight="medium">
<Trans i18nKey="dashboard.empty.import-a-dashboard-header">Import a dashboard</Trans>
</Text>
<Box marginBottom={2}>
<Text element="p" textAlignment="center" color="secondary">
<Trans i18nKey="dashboard.empty.import-a-dashboard-body">
Import dashboards from files or
<a href="https://grafana.com/grafana/dashboards/">grafana.com</a>.
</Trans>
</Text>
</Box>
<Button
icon="upload"
fill="outline"
data-testid={selectors.pages.AddDashboard.itemButton('Import dashboard button')}
onClick={() => {
reportInteraction('dashboards_emptydashboard_clicked', { item: 'import_dashboard' });
onImportDashboard();
}}
disabled={!canCreate}
>
<Trans i18nKey="dashboard.empty.import-dashboard-button">Import dashboard</Trans>
</Button>
</Flex>
</Box>
</Flex>
</Flex>
</div>
</div>
</Flex>
);
};
@ -156,60 +160,5 @@ function getStyles(theme: GrafanaTheme2) {
paddingTop: theme.spacing(12),
},
}),
containerBox: css({
label: 'container-box',
flexDirection: 'column',
boxSizing: 'border-box',
border: '1px dashed rgba(110, 159, 255, 0.5)',
}),
centeredContent: css({
label: 'centered',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}),
visualizationContainer: css({
label: 'visualization-container',
padding: theme.spacing.gridSize * 4,
}),
others: css({
width: '100%',
label: 'others-wrapper',
alignItems: 'stretch',
flexDirection: 'row',
gap: theme.spacing.gridSize * 4,
[theme.breakpoints.down('md')]: {
flexDirection: 'column',
},
}),
widgetContainer: css({
label: 'widget-container',
padding: theme.spacing.gridSize * 3,
flex: 1,
}),
rowContainer: css({
label: 'row-container',
padding: theme.spacing.gridSize * 3,
flex: 1,
}),
libraryContainer: css({
label: 'library-container',
padding: theme.spacing.gridSize * 3,
flex: 1,
}),
headerBig: css({
marginBottom: theme.spacing.gridSize * 2,
}),
headerSmall: css({
marginBottom: theme.spacing.gridSize,
}),
bodyBig: css({
maxWidth: '75%',
marginBottom: theme.spacing.gridSize * 4,
}),
bodySmall: css({
marginBottom: theme.spacing.gridSize * 3,
}),
};
}

View File

@ -36,7 +36,7 @@ composableKinds: DataQuery: {
// Allows to group the results.
groupBy: [...string]
// Sets the maximum number of nodes in the flamegraph.
maxNodes?: int64
maxNodes?: int64
#PyroscopeQueryType: "metrics" | "profile" | *"both" @cuetsy(kind="type")
}
}]