From dc918f7e9120f736d0e1b048ae0fc699b25f84c9 Mon Sep 17 00:00:00 2001 From: Giordano Ricci Date: Tue, 29 Nov 2022 16:18:55 +0000 Subject: [PATCH] Grafana UI: Add experimental InteractiveTable component (#58223) * wip * move table * refine example * move to experimental * add row expansion example * add expanded row to kitchen sink * add column prop docs * add props docs * remove useless example * WIP * use unique id per row & proper aria attrs for expander * add custom cell rendering example * Remove multisort * rename shrink to disableGrow * move isTruthy type guard to @grafana/data * add missing prop from TableData interface * make column id required * fix correlations table * expand on docs * remove leftover comment * rename to InteractiveTable * add some tests * add expansion tests * fix tests * revert unneeded changes * remove extra header rule --- packages/grafana-data/src/types/data.ts | 3 + .../InteractiveTable}/ExpanderCell.tsx | 10 +- .../InteractiveTable/InteractiveTable.mdx | 166 +++++++++++++++++ .../InteractiveTable.story.tsx | 142 +++++++++++++++ .../InteractiveTable.test.tsx | 95 ++++++++++ .../InteractiveTable/InteractiveTable.tsx | 170 +++++++++++++----- .../src/components/InteractiveTable/types.ts | 29 +++ .../src/components/InteractiveTable}/utils.ts | 11 +- packages/grafana-ui/src/components/index.ts | 1 + packages/grafana-ui/src/types/index.ts | 1 + .../grafana-ui/src/types/interactiveTable.ts | 2 + public/app/core/utils/types.ts | 3 - .../correlations/CorrelationsPage.test.tsx | 13 +- .../correlations/CorrelationsPage.tsx | 24 ++- public/app/features/explore/Wrapper.tsx | 2 +- .../configuration/ElasticDetails.tsx | 3 +- 16 files changed, 605 insertions(+), 70 deletions(-) rename {public/app/features/correlations/components/Table => packages/grafana-ui/src/components/InteractiveTable}/ExpanderCell.tsx (60%) create mode 100644 packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.mdx create mode 100644 packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.story.tsx create mode 100644 packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.test.tsx rename public/app/features/correlations/components/Table/index.tsx => packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.tsx (50%) create mode 100644 packages/grafana-ui/src/components/InteractiveTable/types.ts rename {public/app/features/correlations/components/Table => packages/grafana-ui/src/components/InteractiveTable}/utils.ts (84%) create mode 100644 packages/grafana-ui/src/types/interactiveTable.ts delete mode 100644 public/app/core/utils/types.ts diff --git a/packages/grafana-data/src/types/data.ts b/packages/grafana-data/src/types/data.ts index 360a299c564..d1f2e4614e1 100644 --- a/packages/grafana-data/src/types/data.ts +++ b/packages/grafana-data/src/types/data.ts @@ -177,3 +177,6 @@ export interface DataConfigSource { getFieldOverrideOptions: () => ApplyFieldOverrideOptions | undefined; snapshotData?: DataFrameDTO[]; } + +type Truthy = T extends false | '' | 0 | null | undefined ? never : T; +export const isTruthy = (value: T): value is Truthy => Boolean(value); diff --git a/public/app/features/correlations/components/Table/ExpanderCell.tsx b/packages/grafana-ui/src/components/InteractiveTable/ExpanderCell.tsx similarity index 60% rename from public/app/features/correlations/components/Table/ExpanderCell.tsx rename to packages/grafana-ui/src/components/InteractiveTable/ExpanderCell.tsx index fdf945490b1..69873f54e13 100644 --- a/public/app/features/correlations/components/Table/ExpanderCell.tsx +++ b/packages/grafana-ui/src/components/InteractiveTable/ExpanderCell.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import React from 'react'; import { CellProps } from 'react-table'; -import { IconButton } from '@grafana/ui'; +import { IconButton } from '../IconButton/IconButton'; const expanderContainerStyles = css` display: flex; @@ -10,14 +10,18 @@ const expanderContainerStyles = css` height: 100%; `; -export function ExpanderCell({ row }: CellProps) { +export function ExpanderCell({ row, __rowID }: CellProps & { __rowID: string }) { return (
); diff --git a/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.mdx b/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.mdx new file mode 100644 index 00000000000..00962db19ec --- /dev/null +++ b/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.mdx @@ -0,0 +1,166 @@ +import { Meta, Props, Story, Canvas } from '@storybook/addon-docs/blocks'; + +import { InteractiveTable } from './InteractiveTable'; +import { Badge } from '../Badge/Badge'; + + + +# InteractiveTable + + + +The InteractiveTable is used to display and select data efficiently. +It allows for the display and modification of detailed information. +With additional functionality it allows for batch editing, as needed by your feature's users. + +It is a wrapper around [React Table](https://react-table-v7.tanstack.com/), for more informations about it, refer to the [official documentation](https://react-table.tanstack.com/docs/overview). + +### When to use + +The InteractiveTable can be used to allow users to perform administrative tasks workflows. + +### When not to use + +Avoid using the InteractiveTable where mobile or responsiveness may be a requirement. +Consider an alternative pattern where the user is presented with a summary list and can click/tap to an individual page for each row in that list. + +### Usage + + + +#### About `columns` and `data` Props + +To avoid unnecessary rerenders, `columns` and `data` must be memoized. + +Columns are rendered in the same order defined in the `columns` prop. +Each Cell's content is automatically rendered by matching the `id` of the column to the key of each object in the `data` array prop. + +##### Example + +```tsx +interface TableData { + projectName: string; + repository: string; +} + +const columns = useMemo>>( + () => [ + id: 'projectName' + header: "Project Name" + ], + [ + id: 'repository', + header: "Repository" + ], + [] +); + +const data = useMemo>( + () => [ + { + projectName: 'Grafana', + repository: 'https://github.com/grafana/grafana', + } + ], + [ + { + projectName: 'Loki'; + repository: 'https://github.com/grafana/loki'; + } + ], + [] +); +``` + +## Examples + +### With row expansion + +Individual rows can be expanded to display additional details or reconfigure properties previously defined when the row was created. +The expanded row area should be used to declutter the primary presentation of data, carefully consider what the user needs to know at first glance and what can be hidden behind the Row Expander button. + +In general, data-types that are consistent across all dataset are in the primary table, variances are pushed to the expanded section for each individual row. + + + +Row expansion is enabled whenever the `renderExpanded` prop is provided. The `renderExpanded` function is called with the row's data and should return a ReactNode. + +```tsx +interface TableData { + datasource: string; + repo: string; + description: string; +} + +const tableData: TableData[] = [ + //... +]; + +const columns: Array> = [ + //... +]; + +const ExpandedCell = ({ description }: TableData) => { + return

{description}

; +}; + +export const MyComponent = () => { + return ( + r.datasource} + renderExpandedRow={ExpandedCell} + /> + ); +}; +``` + +### Custom Cell Rendering + +Individual cells can be rendered using custom content dy defining a `cell` property on the column definition. + + + +```tsx +interface TableData { + datasource: string; + repo: string; +} + +const RepoCell = ({ + row: { + original: { repo }, + }, +}: CellProps) => { + return ( + + Open on GitHub + + ); +}; + +const tableData: WithCustomCellData[] = [ + { + datasource: 'Prometheus', + repo: 'https://github.com/prometheus/prometheus', + }, + { + datasource: 'Loki', + repo: 'https://github.com/grafana/loki', + }, + { + datasource: 'Tempo', + repo: 'https://github.com/grafana/tempo', + }, +]; + +const columns: Array> = [ + { id: 'datasource', header: 'Data Source' }, + { id: 'repo', header: 'Repo', cell: RepoCell }, +]; + +export const MyComponent = () => { + return r.datasource} />; +}; +``` diff --git a/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.story.tsx b/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.story.tsx new file mode 100644 index 00000000000..74982f76248 --- /dev/null +++ b/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.story.tsx @@ -0,0 +1,142 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import React, { useMemo } from 'react'; + +import { InteractiveTable, Column, CellProps, LinkButton } from '@grafana/ui'; + +import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; + +import mdx from './InteractiveTable.mdx'; + +const EXCLUDED_PROPS = ['className', 'renderExpandedRow', 'getRowId']; + +const meta: ComponentMeta = { + title: 'Experimental/InteractiveTable', + component: InteractiveTable, + decorators: [withCenteredStory], + parameters: { + docs: { + page: mdx, + }, + controls: { + exclude: EXCLUDED_PROPS, + }, + }, + args: {}, + argTypes: {}, +}; + +interface TableData { + header1: string; + header2?: number; + noheader?: string; +} + +export const Basic: ComponentStory = (args) => { + const columns = useMemo>>( + () => [ + { id: 'header2', header: 'With missing values', sortType: 'number', disableGrow: true }, + { + id: 'noheader', + sortType: 'number', + }, + ], + [] + ); + const data: TableData[] = useMemo( + () => [ + { header1: 'a', header2: 1 }, + { header1: 'b', noheader: "This column doesn't have an header" }, + { header1: 'c', noheader: "But it's still sortable" }, + ], + [] + ); + + return r.header1} />; +}; + +interface WithRowExpansionData { + datasource: string; + repo: string; + description: string; +} + +const ExpandedCell = ({ description }: WithRowExpansionData) => { + return

{description}

; +}; + +export const WithRowExpansion: ComponentStory = (args) => { + const tableData: WithRowExpansionData[] = [ + { + datasource: 'Prometheus', + repo: 'https://github.com/prometheus/prometheus', + description: 'Open source time series database & alerting.', + }, + { + datasource: 'Loki', + repo: 'https://github.com/grafana/loki', + description: 'Like Prometheus but for logs. OSS logging solution from Grafana Labs.', + }, + { + datasource: 'Tempo', + repo: 'https://github.com/grafana/tempo', + description: 'High volume, minimal dependency trace storage. OSS tracing solution from Grafana Labs.', + }, + ]; + + const columns: Array> = [ + { id: 'datasource', header: 'Data Source' }, + { id: 'repo', header: 'Repo' }, + ]; + + return ( + r.datasource} + renderExpandedRow={ExpandedCell} + /> + ); +}; + +interface WithCustomCellData { + datasource: string; + repo: string; +} + +const RepoCell = ({ + row: { + original: { repo }, + }, +}: CellProps) => { + return ( + + Open on GithHub + + ); +}; + +export const WithCustomCell: ComponentStory = (args) => { + const tableData: WithCustomCellData[] = [ + { + datasource: 'Prometheus', + repo: 'https://github.com/prometheus/prometheus', + }, + { + datasource: 'Loki', + repo: 'https://github.com/grafana/loki', + }, + { + datasource: 'Tempo', + repo: 'https://github.com/grafana/tempo', + }, + ]; + + const columns: Array> = [ + { id: 'datasource', header: 'Data Source' }, + { id: 'repo', header: 'Repo', cell: RepoCell }, + ]; + + return r.datasource} />; +}; + +export default meta; diff --git a/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.test.tsx b/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.test.tsx new file mode 100644 index 00000000000..579b5e5793d --- /dev/null +++ b/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.test.tsx @@ -0,0 +1,95 @@ +import { fireEvent, getByRole, render, screen } from '@testing-library/react'; +import React from 'react'; + +import { InteractiveTable } from './InteractiveTable'; +import { Column } from './types'; + +interface TableData { + id: string; + country?: string; + value?: string; +} +function getRowId(row: TableData) { + return row.id; +} + +describe('InteractiveTable', () => { + it('should not render hidden columns', () => { + const columns: Array> = [ + { id: 'id', header: 'ID' }, + { id: 'country', header: 'Country', visible: () => false }, + ]; + const data: TableData[] = [ + { id: '1', country: 'Sweden' }, + { id: '2', country: 'Portugal' }, + ]; + render(); + + expect(screen.getByRole('columnheader', { name: 'ID' })).toBeInTheDocument(); + expect(screen.queryByRole('columnheader', { name: 'Country' })).not.toBeInTheDocument(); + }); + + it('should correctly sort rows', () => { + // We are not testing the sorting logic here since it is already tested in react-table, + // but instead we are testing that the sorting is applied correctly to the table and correct aria attributes are set + // according to https://www.w3.org/WAI/ARIA/apg/example-index/table/sortable-table + const columns: Array> = [ + { id: 'id', header: 'ID' }, + { id: 'value', header: 'Value', sortType: 'string' }, + { id: 'country', header: 'Country', sortType: 'number' }, + ]; + const data: TableData[] = [ + { id: '1', value: '1', country: 'Sweden' }, + { id: '2', value: '3', country: 'Portugal' }, + { id: '3', value: '2', country: 'Italy' }, + ]; + render(); + + const valueColumnHeader = screen.getByRole('columnheader', { name: 'Value' }); + const countryColumnHeader = screen.getByRole('columnheader', { name: 'Country' }); + const valueColumnSortButton = getByRole(valueColumnHeader, 'button'); + const countryColumnSortButton = getByRole(countryColumnHeader, 'button'); + + expect(valueColumnHeader).not.toHaveAttribute('aria-sort'); + expect(countryColumnHeader).not.toHaveAttribute('aria-sort'); + + fireEvent.click(countryColumnSortButton); + expect(valueColumnHeader).not.toHaveAttribute('aria-sort'); + expect(countryColumnHeader).toHaveAttribute('aria-sort', 'ascending'); + + fireEvent.click(valueColumnSortButton); + expect(valueColumnHeader).toHaveAttribute('aria-sort', 'ascending'); + expect(countryColumnHeader).not.toHaveAttribute('aria-sort'); + + fireEvent.click(valueColumnSortButton); + expect(valueColumnHeader).toHaveAttribute('aria-sort', 'descending'); + expect(countryColumnHeader).not.toHaveAttribute('aria-sort'); + + fireEvent.click(valueColumnSortButton); + expect(valueColumnHeader).not.toHaveAttribute('aria-sort'); + expect(countryColumnHeader).not.toHaveAttribute('aria-sort'); + }); + + it('correctly expands rows', () => { + const columns: Array> = [{ id: 'id', header: 'ID' }]; + const data: TableData[] = [{ id: '1', value: '1', country: 'Sweden' }]; + render( +
{row.country}
} + /> + ); + + const expanderButton = screen.getByRole('button', { name: /toggle row expanded/i }); + fireEvent.click(expanderButton); + + expect(screen.getByTestId('test-1')).toHaveTextContent('Sweden'); + + expect(expanderButton.getAttribute('aria-controls')).toBe( + // anchestor tr's id should match the expander button's aria-controls attribute + screen.getByTestId('test-1').parentElement?.parentElement?.id + ); + }); +}); diff --git a/public/app/features/correlations/components/Table/index.tsx b/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.tsx similarity index 50% rename from public/app/features/correlations/components/Table/index.tsx rename to packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.tsx index 91aa3ed2f97..8801e8ec529 100644 --- a/public/app/features/correlations/components/Table/index.tsx +++ b/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.tsx @@ -1,20 +1,14 @@ import { cx, css } from '@emotion/css'; -import React, { useMemo, Fragment, ReactNode } from 'react'; -import { - CellProps, - SortByFn, - useExpanded, - useSortBy, - useTable, - DefaultSortTypes, - TableOptions, - IdType, -} from 'react-table'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { Icon, useStyles2 } from '@grafana/ui'; -import { isTruthy } from 'app/core/utils/types'; +import { uniqueId } from 'lodash'; +import React, { useMemo, Fragment, ReactNode, useCallback } from 'react'; +import { useExpanded, useSortBy, useTable, TableOptions, Row, HeaderGroup } from 'react-table'; +import { GrafanaTheme2, isTruthy } from '@grafana/data'; + +import { useStyles2 } from '../../themes'; +import { Icon } from '../Icon/Icon'; + +import { Column } from './types'; import { EXPANDER_CELL_ID, getColumns } from './utils'; const getStyles = (theme: GrafanaTheme2) => ({ @@ -23,47 +17,74 @@ const getStyles = (theme: GrafanaTheme2) => ({ border: solid 1px ${theme.colors.border.weak}; background-color: ${theme.colors.background.secondary}; width: 100%; + + td { + padding: ${theme.spacing(1)}; + } + td, th { - padding: ${theme.spacing(1)}; min-width: ${theme.spacing(3)}; } `, evenRow: css` background: ${theme.colors.background.primary}; `, - shrink: css` + disableGrow: css` width: 0%; `, + header: css` + &, + & > button { + position: relative; + white-space: nowrap; + padding: ${theme.spacing(1)}; + } + & > button { + &:after { + content: '\\00a0'; + } + width: 100%; + height: 100%; + background: none; + border: none; + padding-right: ${theme.spacing(2.5)}; + text-align: left; + &:hover { + background-color: ${theme.colors.emphasize(theme.colors.background.secondary, 0.05)}; + } + } + `, + sortableHeader: css` + /* increases selector's specificity so that it always takes precedence over default styles */ + && { + padding: 0; + } + `, }); -export interface Column { +interface Props { /** - * ID of the column. - * Set this to the matching object key of your data or `undefined` if the column doesn't have any associated data with it. - * This must be unique among all other columns. + * Table's columns definition. Must be memoized. */ - id?: IdType; - cell?: (props: CellProps) => ReactNode; - header?: (() => ReactNode | string) | string; - sortType?: DefaultSortTypes | SortByFn; - shrink?: boolean; - visible?: (col: TableData[]) => boolean; -} - -interface Props { columns: Array>; + /** + * The data to display in the table. Must be memoized. + */ data: TableData[]; - renderExpandedRow?: (row: TableData) => JSX.Element; + /** + * Render function for the expanded row. if not provided, the tables rows will not be expandable. + */ + renderExpandedRow?: (row: TableData) => ReactNode; className?: string; + /** + * Must return a unique id for each row + */ getRowId: TableOptions['getRowId']; } -/** - * non-viz table component. - * Will need most likely to be moved in @grafana/ui - */ -export function Table({ +/** @alpha */ +export function InteractiveTable({ data, className, columns, @@ -75,6 +96,13 @@ export function Table({ const cols = getColumns(columns); return cols; }, [columns]); + const id = useUniqueId(); + const getRowHTMLID = useCallback( + (row: Row) => { + return `${id}-${row.id}`.replace(/\s/g, ''); + }, + [id] + ); const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable( { @@ -82,12 +110,13 @@ export function Table({ data, autoResetExpanded: false, autoResetSortBy: false, + disableMultiSort: true, getRowId, initialState: { hiddenColumns: [ !renderExpandedRow && EXPANDER_CELL_ID, ...tableColumns - .filter((col) => !(col.visible?.(data) ?? true)) + .filter((col) => !(col.visible ? col.visible(data) : true)) .map((c) => c.id) .filter(isTruthy), ].filter(isTruthy), @@ -96,6 +125,7 @@ export function Table({ useSortBy, useExpanded ); + // This should be called only for rows thar we'd want to actually render, which is all at this stage. // We may want to revisit this if we decide to add pagination and/or virtualized tables. rows.forEach(prepareRow); @@ -109,16 +139,19 @@ export function Table({ return ( {headerGroup.headers.map((column) => { - // TODO: if the column is a function, it should also provide an accessible name as a string to be used a the column title in getSortByToggleProps - const { key, ...headerCellProps } = column.getHeaderProps( - column.canSort ? column.getSortByToggleProps() : undefined - ); + const { key, ...headerCellProps } = column.getHeaderProps(); return ( - - {column.render('Header')} - - {column.isSorted && } + + ); })} @@ -131,6 +164,7 @@ export function Table({ {rows.map((row, rowIndex) => { const className = cx(rowIndex % 2 === 0 && styles.evenRow); const { key, ...otherRowProps } = row.getRowProps(); + const rowId = getRowHTMLID(row); return ( @@ -139,7 +173,7 @@ export function Table({ const { key, ...otherCellProps } = cell.getCellProps(); return ( - {cell.render('Cell')} + {cell.render('Cell', { __rowID: rowId })} ); })} @@ -147,7 +181,7 @@ export function Table({ { // @ts-expect-error react-table doesn't ship with useExpanded types and we can't use declaration merging without affecting the table viz row.isExpanded && renderExpandedRow && ( - + {renderExpandedRow(row.original)} ) @@ -159,3 +193,45 @@ export function Table({ ); } + +const useUniqueId = () => { + return useMemo(() => uniqueId('InteractiveTable'), []); +}; + +const getColumnheaderStyles = (theme: GrafanaTheme2) => ({ + sortIcon: css` + position: absolute; + top: ${theme.spacing(1)}; + `, +}); + +function ColumnHeader({ + column: { canSort, render, isSorted, isSortedDesc, getSortByToggleProps }, +}: { + column: HeaderGroup; +}) { + const styles = useStyles2(getColumnheaderStyles); + const { onClick } = getSortByToggleProps(); + + const children = ( + <> + {render('Header')} + + {isSorted && ( + + )} + + ); + + if (canSort) { + return ( + + ); + } + + return children; +} diff --git a/packages/grafana-ui/src/components/InteractiveTable/types.ts b/packages/grafana-ui/src/components/InteractiveTable/types.ts new file mode 100644 index 00000000000..47263d4730e --- /dev/null +++ b/packages/grafana-ui/src/components/InteractiveTable/types.ts @@ -0,0 +1,29 @@ +import { ReactNode } from 'react'; +import { CellProps, DefaultSortTypes, IdType, SortByFn } from 'react-table'; + +export interface Column { + /** + * ID of the column. Must be unique among all other columns + */ + id: IdType; + /** + * Custom render function for te cell + */ + cell?: (props: CellProps) => ReactNode; + /** + * Header name. if `undefined` the header will be empty. Useful for action columns. + */ + header?: string; + /** + * Column sort type. If `undefined` the column will not be sortable. + * */ + sortType?: DefaultSortTypes | SortByFn; + /** + * If `true` prevents the column from growing more than its content. + */ + disableGrow?: boolean; + /** + * If the provided function returns `false` the column will be hidden. + */ + visible?: (data: TableData[]) => boolean; +} diff --git a/public/app/features/correlations/components/Table/utils.ts b/packages/grafana-ui/src/components/InteractiveTable/utils.ts similarity index 84% rename from public/app/features/correlations/components/Table/utils.ts rename to packages/grafana-ui/src/components/InteractiveTable/utils.ts index 551157b87a7..7eb970008b1 100644 --- a/public/app/features/correlations/components/Table/utils.ts +++ b/packages/grafana-ui/src/components/InteractiveTable/utils.ts @@ -1,11 +1,9 @@ -import { uniqueId } from 'lodash'; import { Column as RTColumn } from 'react-table'; import { ExpanderCell } from './ExpanderCell'; +import { Column } from './types'; -import { Column } from '.'; - -export const EXPANDER_CELL_ID = '__expander'; +export const EXPANDER_CELL_ID = '__expander' as const; type InternalColumn = RTColumn & { visible?: (data: T[]) => boolean; @@ -24,11 +22,12 @@ export function getColumns(columns: Array>): Array ({ + id: column.id, + accessor: column.id, Header: column.header || (() => null), - accessor: column.id || uniqueId(), sortType: column.sortType || 'alphanumeric', disableSortBy: !Boolean(column.sortType), - width: column.shrink ? 0 : undefined, + width: column.disableGrow ? 0 : undefined, visible: column.visible, ...(column.cell && { Cell: column.cell }), })), diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index be1987ed037..8497191c78f 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -42,6 +42,7 @@ export { } from './DateTimePickers/DatePickerWithInput/DatePickerWithInput'; export { DateTimePicker } from './DateTimePickers/DateTimePicker/DateTimePicker'; export { List } from './List/List'; +export { InteractiveTable } from './InteractiveTable/InteractiveTable'; export { TagsInput } from './TagsInput/TagsInput'; export { Pagination } from './Pagination/Pagination'; export { Tag, type OnTagClick } from './Tags/Tag'; diff --git a/packages/grafana-ui/src/types/index.ts b/packages/grafana-ui/src/types/index.ts index 79e87f3c5b6..042ee209e34 100644 --- a/packages/grafana-ui/src/types/index.ts +++ b/packages/grafana-ui/src/types/index.ts @@ -6,3 +6,4 @@ export * from './forms'; export * from './icon'; export * from './select'; export * from './size'; +export * from './interactiveTable'; diff --git a/packages/grafana-ui/src/types/interactiveTable.ts b/packages/grafana-ui/src/types/interactiveTable.ts new file mode 100644 index 00000000000..f999cb9aa5b --- /dev/null +++ b/packages/grafana-ui/src/types/interactiveTable.ts @@ -0,0 +1,2 @@ +export type { Column } from '../components/InteractiveTable/types'; +export type { CellProps, SortByFn } from 'react-table'; diff --git a/public/app/core/utils/types.ts b/public/app/core/utils/types.ts deleted file mode 100644 index 2bd160c6857..00000000000 --- a/public/app/core/utils/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -type Truthy = T extends false | '' | 0 | null | undefined ? never : T; - -export const isTruthy = (value: T): value is Truthy => Boolean(value); diff --git a/public/app/features/correlations/CorrelationsPage.test.tsx b/public/app/features/correlations/CorrelationsPage.test.tsx index 2de67a10d60..d891c39bee5 100644 --- a/public/app/features/correlations/CorrelationsPage.test.tsx +++ b/public/app/features/correlations/CorrelationsPage.test.tsx @@ -1,4 +1,13 @@ -import { render, waitFor, screen, fireEvent, waitForElementToBeRemoved, within, Matcher } from '@testing-library/react'; +import { + render, + waitFor, + screen, + fireEvent, + waitForElementToBeRemoved, + within, + Matcher, + getByRole, +} from '@testing-library/react'; import { merge, uniqueId } from 'lodash'; import React from 'react'; import { DeepPartial } from 'react-hook-form'; @@ -411,7 +420,7 @@ describe('CorrelationsPage', () => { }); it('correctly sorts by source', async () => { - const sourceHeader = getHeaderByName('Source'); + const sourceHeader = getByRole(getHeaderByName('Source'), 'button'); fireEvent.click(sourceHeader); let cells = queryCellsByColumnName('Source'); cells.forEach((cell, i, allCells) => { diff --git a/public/app/features/correlations/CorrelationsPage.tsx b/public/app/features/correlations/CorrelationsPage.tsx index 730b32c9952..303d0827862 100644 --- a/public/app/features/correlations/CorrelationsPage.tsx +++ b/public/app/features/correlations/CorrelationsPage.tsx @@ -1,11 +1,22 @@ import { css } from '@emotion/css'; import { negate } from 'lodash'; import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; -import { CellProps, SortByFn } from 'react-table'; import { GrafanaTheme2 } from '@grafana/data'; import { isFetchError, reportInteraction } from '@grafana/runtime'; -import { Badge, Button, DeleteButton, HorizontalGroup, LoadingPlaceholder, useStyles2, Alert } from '@grafana/ui'; +import { + Badge, + Button, + DeleteButton, + HorizontalGroup, + LoadingPlaceholder, + useStyles2, + Alert, + InteractiveTable, + type Column, + type CellProps, + type SortByFn, +} from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; import { useNavModel } from 'app/core/hooks/useNavModel'; @@ -14,7 +25,6 @@ import { AccessControlAction } from 'app/types'; import { AddCorrelationForm } from './Forms/AddCorrelationForm'; import { EditCorrelationForm } from './Forms/EditCorrelationForm'; import { EmptyCorrelationsCTA } from './components/EmptyCorrelationsCTA'; -import { Column, Table } from './components/Table'; import type { RemoveCorrelationParams } from './types'; import { CorrelationData, useCorrelations } from './useCorrelations'; @@ -97,8 +107,9 @@ export default function CorrelationsPage() { const columns = useMemo>>( () => [ { + id: 'info', cell: InfoCell, - shrink: true, + disableGrow: true, visible: (data) => data.some(isSourceReadOnly), }, { @@ -115,8 +126,9 @@ export default function CorrelationsPage() { }, { id: 'label', header: 'Label', sortType: 'alphanumeric' }, { + id: 'actions', cell: RowActions, - shrink: true, + disableGrow: true, visible: (data) => canWriteCorrelations && data.some(negate(isSourceReadOnly)), }, ], @@ -166,7 +178,7 @@ export default function CorrelationsPage() { {isAdding && setIsAdding(false)} onCreated={handleAdded} />} {data && data.length >= 1 && ( - (