From 42cc6b1842c7b3dd8d1cf10ec1acd019231c6cd3 Mon Sep 17 00:00:00 2001 From: Joao Silva <100691367+JoaoSilvaGrafana@users.noreply.github.com> Date: Thu, 21 Sep 2023 17:13:19 +0100 Subject: [PATCH] GrafanaUI: Create Box component (#73637) --- .../grafana-ui/src/components/Alert/Alert.tsx | 109 +++---- .../Layout/Box/Box.internal.story.tsx | 138 +++++++++ .../src/components/Layout/Box/Box.mdx | 39 +++ .../src/components/Layout/Box/Box.tsx | 267 +++++++++++++++++ .../{ => Layout}/Flex/Flex.internal.story.tsx | 28 +- .../src/components/{ => Layout}/Flex/Flex.mdx | 0 .../src/components/{ => Layout}/Flex/Flex.tsx | 43 ++- .../Stack/Stack.internal.story.tsx | 6 + .../components/{ => Layout}/Stack/Stack.mdx | 0 .../components/{ => Layout}/Stack/Stack.tsx | 5 +- .../Layout/utils/responsiveness.tsx | 65 +++++ packages/grafana-ui/src/unstable.ts | 5 +- .../storybook/themeStorybookControls.tsx | 1 + .../dashboard/dashgrid/DashboardEmpty.tsx | 271 +++++++----------- .../dataquery.cue | 2 +- 15 files changed, 734 insertions(+), 245 deletions(-) create mode 100644 packages/grafana-ui/src/components/Layout/Box/Box.internal.story.tsx create mode 100644 packages/grafana-ui/src/components/Layout/Box/Box.mdx create mode 100644 packages/grafana-ui/src/components/Layout/Box/Box.tsx rename packages/grafana-ui/src/components/{ => Layout}/Flex/Flex.internal.story.tsx (87%) rename packages/grafana-ui/src/components/{ => Layout}/Flex/Flex.mdx (100%) rename packages/grafana-ui/src/components/{ => Layout}/Flex/Flex.tsx (56%) rename packages/grafana-ui/src/components/{ => Layout}/Stack/Stack.internal.story.tsx (75%) rename packages/grafana-ui/src/components/{ => Layout}/Stack/Stack.mdx (100%) rename packages/grafana-ui/src/components/{ => Layout}/Stack/Stack.tsx (71%) create mode 100644 packages/grafana-ui/src/components/Layout/utils/responsiveness.tsx create mode 100644 packages/grafana-ui/src/utils/storybook/themeStorybookControls.tsx diff --git a/packages/grafana-ui/src/components/Alert/Alert.tsx b/packages/grafana-ui/src/components/Alert/Alert.tsx index bfec7a96e8f..4595aad88c3 100644 --- a/packages/grafana-ui/src/components/Alert/Alert.tsx +++ b/packages/grafana-ui/src/components/Alert/Alert.tsx @@ -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( return (
-
- -
+ + +
+ +
+
-
-
{title}
- {children &&
{children}
} -
+ + {title} + {children &&
{children}
} +
+ {/* If onRemove is specified, giving preference to onRemove */} + {onRemove && !buttonContent && ( +
+
+ )} - {/* If onRemove is specified, giving preference to onRemove */} - {onRemove && !buttonContent && ( -
-
- )} - - {onRemove && buttonContent && ( -
- -
- )} + {onRemove && buttonContent && ( + + + + )} +
); } @@ -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, diff --git a/packages/grafana-ui/src/components/Layout/Box/Box.internal.story.tsx b/packages/grafana-ui/src/components/Layout/Box/Box.internal.story.tsx new file mode 100644 index 00000000000..6e9b265a01e --- /dev/null +++ b/packages/grafana-ui/src/components/Layout/Box/Box.internal.story.tsx @@ -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 = { + title: 'General/Layout/Box', + component: Box, + parameters: { + docs: { + page: mdx, + }, + controls: { exclude: ['element'] }, + }, +}; + +const Item = ({ background }: { background?: string }) => { + return ( +
+ ); +}; + +export const Basic: StoryFn = (args) => { + return ( +
+ + + +
+ ); +}; + +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 = () => { + return ( + + {backgroundOptions.map((background) => ( + + {background} + + + + + ))} + + ); +}; + +export const Border: StoryFn = () => { + return ( + +
+ Border Color + + {borderColorOptions.map((border) => ( + + {border} + + + + + ))} + +
+
+ Border Style + + {borderStyleOptions.map((border) => ( + + {border} + + + + + ))} + +
+
+ ); +}; + +export const Shadow: StoryFn = () => { + return ( + + {boxShadowOptions.map((shadow) => ( + + {shadow} + + + + + ))} + + ); +}; + +export default meta; diff --git a/packages/grafana-ui/src/components/Layout/Box/Box.mdx b/packages/grafana-ui/src/components/Layout/Box/Box.mdx new file mode 100644 index 00000000000..b789bead6d1 --- /dev/null +++ b/packages/grafana-ui/src/components/Layout/Box/Box.mdx @@ -0,0 +1,39 @@ +import { Meta, ArgTypes } from '@storybook/blocks'; +import { Box } from './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`. +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 + + diff --git a/packages/grafana-ui/src/components/Layout/Box/Box.tsx b/packages/grafana-ui/src/components/Layout/Box/Box.tsx new file mode 100644 index 00000000000..6c604f8b10a --- /dev/null +++ b/packages/grafana-ui/src/components/Layout/Box/Box.tsx @@ -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; + /** Sets the properties `margin-top` and `margin-bottom`. Higher priority than margin. */ + marginX?: ResponsiveProp; + /** Sets the properties `margin-left` and `margin-right`. Higher priority than margin. */ + marginY?: ResponsiveProp; + /** Sets the property `margin-top`. Higher priority than margin and marginY. */ + marginTop?: ResponsiveProp; + /** Sets the property `margin-bottom`. Higher priority than margin and marginXY */ + marginBottom?: ResponsiveProp; + /** Sets the property `margin-left`. Higher priority than margin and marginX. */ + marginLeft?: ResponsiveProp; + /** Sets the property `margin-right`. Higher priority than margin and marginX. */ + marginRight?: ResponsiveProp; + + // Padding props + /** Sets the property `padding` */ + padding?: ResponsiveProp; + /** Sets the properties `padding-top` and `padding-bottom`. Higher priority than padding. */ + paddingX?: ResponsiveProp; + /** Sets the properties `padding-left` and `padding-right`. Higher priority than padding. */ + paddingY?: ResponsiveProp; + /** Sets the property `padding-top`. Higher priority than padding and paddingY. */ + paddingTop?: ResponsiveProp; + /** Sets the property `padding-bottom`. Higher priority than padding and paddingY. */ + paddingBottom?: ResponsiveProp; + /** Sets the property `padding-left`. Higher priority than padding and paddingX. */ + paddingLeft?: ResponsiveProp; + /** Sets the property `padding-right`. Higher priority than padding and paddingX. */ + paddingRight?: ResponsiveProp; + + // Border Props + borderStyle?: ResponsiveProp; + borderColor?: ResponsiveProp; + borderRadius?: ResponsiveProp; + + // Flex Props + /** Sets the property `flex` */ + grow?: ResponsiveProp; + /** Sets the property `flex-shrink` */ + shrink?: ResponsiveProp; + alignItems?: ResponsiveProp; + justifyContent?: ResponsiveProp; + + // Other props + backgroundColor?: ResponsiveProp; + display?: ResponsiveProp; + boxShadow?: ResponsiveProp; + /** 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) => { + 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 {children}; +}; + +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], + })), + ]), + }; +}; diff --git a/packages/grafana-ui/src/components/Flex/Flex.internal.story.tsx b/packages/grafana-ui/src/components/Layout/Flex/Flex.internal.story.tsx similarity index 87% rename from packages/grafana-ui/src/components/Flex/Flex.internal.story.tsx rename to packages/grafana-ui/src/components/Layout/Flex/Flex.internal.story.tsx index cad3c1bc4a5..4bada87b32b 100644 --- a/packages/grafana-ui/src/components/Flex/Flex.internal.story.tsx +++ b/packages/grafana-ui/src/components/Layout/Flex/Flex.internal.story.tsx @@ -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 = ({ 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 = () => { const theme = useTheme2(); diff --git a/packages/grafana-ui/src/components/Flex/Flex.mdx b/packages/grafana-ui/src/components/Layout/Flex/Flex.mdx similarity index 100% rename from packages/grafana-ui/src/components/Flex/Flex.mdx rename to packages/grafana-ui/src/components/Layout/Flex/Flex.mdx diff --git a/packages/grafana-ui/src/components/Flex/Flex.tsx b/packages/grafana-ui/src/components/Layout/Flex/Flex.tsx similarity index 56% rename from packages/grafana-ui/src/components/Flex/Flex.tsx rename to packages/grafana-ui/src/components/Layout/Flex/Flex.tsx index 78425134d41..11cf9208205 100644 --- a/packages/grafana-ui/src/components/Flex/Flex.tsx +++ b/packages/grafana-ui/src/components/Layout/Flex/Flex.tsx @@ -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; + alignItems?: ResponsiveProp; + justifyContent?: ResponsiveProp; + direction?: ResponsiveProp; + wrap?: ResponsiveProp; 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(theme, direction, (val) => ({ + flexDirection: val, + })), + getResponsiveStyle(theme, wrap, (val) => ({ + flexWrap: val, + })), + getResponsiveStyle(theme, alignItems, (val) => ({ + alignItems: val, + })), + getResponsiveStyle(theme, justifyContent, (val) => ({ + justifyContent: val, + })), + getResponsiveStyle(theme, gap, (val) => ({ + gap: theme.spacing(val), + })), + ]), }; }; diff --git a/packages/grafana-ui/src/components/Stack/Stack.internal.story.tsx b/packages/grafana-ui/src/components/Layout/Stack/Stack.internal.story.tsx similarity index 75% rename from packages/grafana-ui/src/components/Stack/Stack.internal.story.tsx rename to packages/grafana-ui/src/components/Layout/Stack/Stack.internal.story.tsx index 44b564062e4..852df476317 100644 --- a/packages/grafana-ui/src/components/Stack/Stack.internal.story.tsx +++ b/packages/grafana-ui/src/components/Layout/Stack/Stack.internal.story.tsx @@ -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 = { page: mdx, }, }, + argTypes: { + gap: SpacingTokenControl, + direction: { control: 'select', options: ['row', 'row-reverse', 'column', 'column-reverse'] }, + }, }; const Item = ({ children }: { children: ReactNode }) => ( diff --git a/packages/grafana-ui/src/components/Stack/Stack.mdx b/packages/grafana-ui/src/components/Layout/Stack/Stack.mdx similarity index 100% rename from packages/grafana-ui/src/components/Stack/Stack.mdx rename to packages/grafana-ui/src/components/Layout/Stack/Stack.mdx diff --git a/packages/grafana-ui/src/components/Stack/Stack.tsx b/packages/grafana-ui/src/components/Layout/Stack/Stack.tsx similarity index 71% rename from packages/grafana-ui/src/components/Stack/Stack.tsx rename to packages/grafana-ui/src/components/Layout/Stack/Stack.tsx index c2b0120568c..c660bfa9ac9 100644 --- a/packages/grafana-ui/src/components/Stack/Stack.tsx +++ b/packages/grafana-ui/src/components/Layout/Stack/Stack.tsx @@ -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; + gap?: ResponsiveProp; } export const Stack = ({ gap = 1, direction = 'column', children }: React.PropsWithChildren) => { diff --git a/packages/grafana-ui/src/components/Layout/utils/responsiveness.tsx b/packages/grafana-ui/src/components/Layout/utils/responsiveness.tsx new file mode 100644 index 00000000000..8c3a5a632a6 --- /dev/null +++ b/packages/grafana-ui/src/components/Layout/utils/responsiveness.tsx @@ -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`. + */ +export type ResponsiveProp = T | Responsive; + +type Responsive = { + xs: T; + sm?: T; + md?: T; + lg?: T; + xl?: T; + xxl?: T; +}; + +function breakpointCSS( + theme: GrafanaTheme2, + prop: Responsive, + 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( + theme: GrafanaTheme2, + prop: ResponsiveProp | 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'), + ]; +} diff --git a/packages/grafana-ui/src/unstable.ts b/packages/grafana-ui/src/unstable.ts index f1a9f2aac04..7104a81873a 100644 --- a/packages/grafana-ui/src/unstable.ts +++ b/packages/grafana-ui/src/unstable.ts @@ -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'; diff --git a/packages/grafana-ui/src/utils/storybook/themeStorybookControls.tsx b/packages/grafana-ui/src/utils/storybook/themeStorybookControls.tsx new file mode 100644 index 00000000000..c02841f32bb --- /dev/null +++ b/packages/grafana-ui/src/utils/storybook/themeStorybookControls.tsx @@ -0,0 +1 @@ +export const SpacingTokenControl = { control: 'select', options: [0, 0.25, 0.5, 1, 1.5, 2, 3, 4, 5, 6, 8, 10] }; diff --git a/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx b/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx index 94a68131af0..3b5c057a314 100644 --- a/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx @@ -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 ( -
-
-
-
- - - Start your new dashboard by adding a visualization - - -
-
- - - Select a data source and then query and visualize your data with charts, stats and tables or create - lists, markdowns and other widgets. - - -
- -
-
- {config.featureToggles.vizAndWidgetSplit && ( -
-
- - Add a widget - -
-
+ +
+ + + + + + Start your new dashboard by adding a visualization + + + - Create lists, markdowns and other widgets + + Select a data source and then query and visualize your data with charts, stats and tables or create + lists, markdowns and other widgets. + -
+ -
- )} -
-
- - Import panel - -
-
- - - Add visualizations that are shared with other dashboards. - - -
- -
-
-
- - Import a dashboard - -
-
- - - Import dashboards from files or grafana.com. - - -
- -
-
+ + + + {config.featureToggles.vizAndWidgetSplit && ( + + + + Add a widget + + + + Create lists, markdowns and other widgets + + + + + + )} + + + + Import panel + + + + + Add visualizations that are shared with other dashboards. + + + + + + + + + + Import a dashboard + + + + + Import dashboards from files or + grafana.com. + + + + + + + +
-
+ ); }; @@ -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, - }), }; } diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/dataquery.cue b/public/app/plugins/datasource/grafana-pyroscope-datasource/dataquery.cue index 3a2b2149978..6fca4f95739 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/dataquery.cue +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/dataquery.cue @@ -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") } }]