diff --git a/.betterer.results b/.betterer.results index 16a3e9e1ad6..ff015aeab35 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2970,9 +2970,6 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"] ], - "public/app/features/connections/tabs/ConnectData/CardGrid/CardGrid.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"] - ], "public/app/features/connections/tabs/ConnectData/CategoryHeader/CategoryHeader.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"] @@ -4781,10 +4778,6 @@ exports[`better eslint`] = { "public/app/features/plugins/admin/components/PluginDetailsSignature.tsx:5381": [ [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"] ], - "public/app/features/plugins/admin/components/PluginList.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"] - ], "public/app/features/plugins/admin/components/PluginListItem.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"], diff --git a/packages/grafana-ui/src/components/Layout/Grid/Grid.internal.story.tsx b/packages/grafana-ui/src/components/Layout/Grid/Grid.internal.story.tsx new file mode 100644 index 00000000000..2c73d9d0f43 --- /dev/null +++ b/packages/grafana-ui/src/components/Layout/Grid/Grid.internal.story.tsx @@ -0,0 +1,64 @@ +import { Meta, StoryFn } from '@storybook/react'; +import React from 'react'; + +import { useTheme2 } from '../../../themes'; + +import { Grid } from './Grid'; +import mdx from './Grid.mdx'; + +const meta: Meta = { + title: 'General/Layout/Grid', + component: Grid, + parameters: { + docs: { + page: mdx, + }, + }, + args: { + gap: 1, + }, +}; + +export const ColumnsNumber: StoryFn = (args) => { + const theme = useTheme2(); + return ( + + {Array.from({ length: 9 }).map((_, i) => ( +
+ N# {i} +
+ ))} +
+ ); +}; +ColumnsNumber.args = { + columns: 3, +}; +ColumnsNumber.parameters = { + controls: { + exclude: ['minColumnWidth'], + }, +}; + +export const ColumnsMinWidth: StoryFn = (args) => { + const theme = useTheme2(); + return ( + + {Array.from({ length: 9 }).map((_, i) => ( +
+ N# {i} +
+ ))} +
+ ); +}; +ColumnsMinWidth.args = { + minColumnWidth: 21, +}; +ColumnsMinWidth.parameters = { + controls: { + exclude: ['columns'], + }, +}; + +export default meta; diff --git a/packages/grafana-ui/src/components/Layout/Grid/Grid.mdx b/packages/grafana-ui/src/components/Layout/Grid/Grid.mdx new file mode 100644 index 00000000000..34acd4b15f1 --- /dev/null +++ b/packages/grafana-ui/src/components/Layout/Grid/Grid.mdx @@ -0,0 +1,32 @@ +import { Meta, ArgTypes } from '@storybook/blocks'; +import { Grid } from './Grid'; + + + +# Grid + +The `Grid` component allows for the organized layout and alignment of content into a grid-based structure. + +## Usage + +### When to use + +Use the Grid component when you want to create structured and organized layouts where content or elements need to be aligned in rows and columns for clarity and consistency. + +### When not to use + +Use the `Stack` component instead for these use cases: + +- **Simple layouts:** When you need to arrange elements in a linear format, either vertically or horizontally. +- **Regular flow:** When you want a "regular" site flow but with standardized spacing between the elements. + +Use the `Flex` component instead for these use cases: + +- **Alignment:** More options for item alignment. +- **Flex items:** Custom flex basis or configure how items stretch and wrap. + +## Properties + +_Note: There is no support for using `columns` and `minColumnWidth` props at the same time. The correct behaviour is working just with one of them not both._ + + diff --git a/packages/grafana-ui/src/components/Layout/Grid/Grid.tsx b/packages/grafana-ui/src/components/Layout/Grid/Grid.tsx new file mode 100644 index 00000000000..00be87bda87 --- /dev/null +++ b/packages/grafana-ui/src/components/Layout/Grid/Grid.tsx @@ -0,0 +1,56 @@ +import { css } from '@emotion/css'; +import React, { forwardRef, HTMLAttributes } from 'react'; + +import { GrafanaTheme2, ThemeSpacingTokens } from '@grafana/data'; + +import { useStyles2 } from '../../../themes'; + +interface GridProps extends Omit, 'className'> { + children: NonNullable; + + /** Specifies the gutters between columns and rows. It is overwritten when a column or row gap has a value */ + gap?: ThemeSpacingTokens; + + /** Number of columns */ + columns?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; + + /** For a responsive layout, fit as many columns while maintaining this minimum column width. + * The real width will be calculated based on the theme spacing tokens: `theme.spacing(minColumnWidth)` + */ + minColumnWidth?: 1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 | 44 | 55 | 72 | 89 | 144; +} + +export const Grid = forwardRef((props, ref) => { + const { children, gap, columns, minColumnWidth, ...rest } = props; + const styles = useStyles2(getGridStyles, gap, columns, minColumnWidth); + + return ( +
+ {children} +
+ ); +}); + +Grid.displayName = 'Grid'; + +const getGridStyles = ( + theme: GrafanaTheme2, + gap: GridProps['gap'], + columns: GridProps['columns'], + minColumnWidth: GridProps['minColumnWidth'] +) => { + return { + grid: css([ + { + display: 'grid', + gap: gap ? theme.spacing(gap) : undefined, + }, + minColumnWidth && { + gridTemplateColumns: `repeat(auto-fill, minmax(${theme.spacing(minColumnWidth)}, 1fr))`, + }, + columns && { + gridTemplateColumns: `repeat(${columns}, 1fr)`, + }, + ]), + }; +}; diff --git a/packages/grafana-ui/src/unstable.ts b/packages/grafana-ui/src/unstable.ts index 644f570f4fd..680689fa9c7 100644 --- a/packages/grafana-ui/src/unstable.ts +++ b/packages/grafana-ui/src/unstable.ts @@ -12,4 +12,5 @@ export * from './components/Layout/Box/Box'; export * from './components/Layout/Flex/Flex'; +export { Grid } from './components/Layout/Grid/Grid'; export { Stack, HorizontalStack } from './components/Layout/Stack'; diff --git a/public/app/features/connections/tabs/ConnectData/CardGrid/CardGrid.tsx b/public/app/features/connections/tabs/ConnectData/CardGrid/CardGrid.tsx index 887da273155..211cb81e4ac 100644 --- a/public/app/features/connections/tabs/ConnectData/CardGrid/CardGrid.tsx +++ b/public/app/features/connections/tabs/ConnectData/CardGrid/CardGrid.tsx @@ -3,16 +3,10 @@ import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Card, useStyles2 } from '@grafana/ui'; +import { Grid } from '@grafana/ui/src/unstable'; import { PluginAngularBadge } from 'app/features/plugins/admin/components/Badges'; const getStyles = (theme: GrafanaTheme2) => ({ - sourcesList: css` - display: grid; - grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); - gap: 12px; - list-style: none; - margin-bottom: 80px; - `, heading: css({ fontSize: theme.typography.h5.fontSize, fontWeight: 'inherit', @@ -55,6 +49,7 @@ export type CardGridItem = { logo?: string; angularDetected?: boolean; }; + export interface CardGridProps { items: CardGridItem[]; onClickItem?: (e: React.MouseEvent, item: CardGridItem) => void; @@ -64,7 +59,7 @@ export const CardGrid = ({ items, onClickItem }: CardGridProps) => { const styles = useStyles2(getStyles); return ( -
    + {items.map((item) => ( { ) : null} ))} -
+ ); }; diff --git a/public/app/features/plugins/admin/components/PluginList.tsx b/public/app/features/plugins/admin/components/PluginList.tsx index afa87074e14..b793a39f5b5 100644 --- a/public/app/features/plugins/admin/components/PluginList.tsx +++ b/public/app/features/plugins/admin/components/PluginList.tsx @@ -1,10 +1,8 @@ -import { css, cx } from '@emotion/css'; import React from 'react'; import { useLocation } from 'react-router-dom'; -import { GrafanaTheme2 } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { useStyles2 } from '@grafana/ui'; +import { Grid } from '@grafana/ui/src/unstable'; import { CatalogPlugin, PluginListDisplayMode } from '../types'; @@ -17,28 +15,14 @@ interface Props { export const PluginList = ({ plugins, displayMode }: Props) => { const isList = displayMode === PluginListDisplayMode.List; - const styles = useStyles2(getStyles); const { pathname } = useLocation(); const pathName = config.appSubUrl + (pathname.endsWith('/') ? pathname.slice(0, -1) : pathname); return ( -
+ {plugins.map((plugin) => ( ))} -
+ ); }; - -const getStyles = (theme: GrafanaTheme2) => { - return { - container: css` - display: grid; - grid-template-columns: repeat(auto-fill, minmax(288px, 1fr)); - gap: ${theme.spacing(3)}; - `, - list: css` - grid-template-columns: 1fr; - `, - }; -};