diff --git a/packages/grafana-schema/src/common/common.gen.ts b/packages/grafana-schema/src/common/common.gen.ts index 7986476fa56..9e1d0fd99a8 100644 --- a/packages/grafana-schema/src/common/common.gen.ts +++ b/packages/grafana-schema/src/common/common.gen.ts @@ -964,6 +964,10 @@ export interface TableFieldOptions { inspect: boolean; minWidth?: number; width?: number; + /** + * Enables text wrapping for the display name in the table header. + */ + wrapHeaderText?: boolean; } export const defaultTableFieldOptions: Partial = { diff --git a/packages/grafana-schema/src/common/table.cue b/packages/grafana-schema/src/common/table.cue index 7548275ada7..bdb537c8504 100644 --- a/packages/grafana-schema/src/common/table.cue +++ b/packages/grafana-schema/src/common/table.cue @@ -105,5 +105,7 @@ TableFieldOptions: { filterable?: bool // Hides any header for a column, useful for columns that show some static content or buttons. hideHeader?: bool + // Enables text wrapping for the display name in the table header. + wrapHeaderText?: bool } @cuetsy(kind="interface") diff --git a/packages/grafana-schema/src/veneer/common.types.ts b/packages/grafana-schema/src/veneer/common.types.ts index 8a73c1149f9..fb3bfe18c57 100644 --- a/packages/grafana-schema/src/veneer/common.types.ts +++ b/packages/grafana-schema/src/veneer/common.types.ts @@ -41,6 +41,7 @@ export * from '../common/common.gen'; export const defaultTableFieldOptions: raw.TableFieldOptions = { align: 'auto', inspect: false, + wrapHeaderText: false, cellOptions: { type: raw.TableCellDisplayMode.Auto, }, diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/HeaderCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/HeaderCell.tsx index c5f7b79400f..a62cbeb6beb 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/HeaderCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/HeaderCell.tsx @@ -1,5 +1,5 @@ -import { css } from '@emotion/css'; -import React, { useEffect, useMemo } from 'react'; +import { css, cx } from '@emotion/css'; +import React, { useEffect } from 'react'; import { Column, SortDirection } from 'react-data-grid'; import { Field, GrafanaTheme2 } from '@grafana/data'; @@ -34,9 +34,10 @@ const HeaderCell: React.FC = ({ crossFilterRows, showTypeIcons, }) => { - const styles = useStyles2(getStyles); - const displayName = useMemo(() => getDisplayName(field), [field]); - const filterable = useMemo(() => field.config.custom?.filterable ?? false, [field]); + const headerCellWrap = field.config.custom?.wrapHeaderText ?? false; + const styles = useStyles2(getStyles, headerCellWrap); + const displayName = getDisplayName(field); + const filterable = field.config.custom?.filterable ?? false; // we have to remove/reset the filter if the column is not filterable useEffect(() => { @@ -50,15 +51,18 @@ const HeaderCell: React.FC = ({ }, [filterable, displayName, filter, setFilter]); return ( - // eslint-disable-next-line jsx-a11y/no-static-element-interactions <> - - {showTypeIcons && } - {/* Used cached displayName if available, otherwise use the column name (nested tables) */} -
{getDisplayName(field)}
- {direction && (direction === 'ASC' ? : )} -
- + {showTypeIcons && ( + + )} + {getDisplayName(field)} + {direction && ( + + )} {filterable && ( = ({ field={field} crossFilterOrder={crossFilterOrder} crossFilterRows={crossFilterRows} + iconClassName={styles.headerCellIcon} /> )} ); }; -const getStyles = (theme: GrafanaTheme2) => ({ +const getStyles = (theme: GrafanaTheme2, headerTextWrap?: boolean) => ({ headerCellLabel: css({ - border: 'none', - padding: 0, - background: 'inherit', cursor: 'pointer', - whiteSpace: 'nowrap', + fontWeight: theme.typography.fontWeightMedium, + color: theme.colors.text.secondary, overflow: 'hidden', textOverflow: 'ellipsis', - fontWeight: theme.typography.fontWeightMedium, - display: 'flex', - alignItems: 'center', - color: theme.colors.text.secondary, - gap: theme.spacing(1), - + whiteSpace: headerTextWrap ? 'pre-line' : 'nowrap', '&:hover': { textDecoration: 'underline', color: theme.colors.text.link, }, }), + headerCellIcon: css({ + marginBottom: theme.spacing(0.5), + alignSelf: 'flex-end', + color: theme.colors.text.secondary, + }), + headerSortIcon: css({ + marginBottom: theme.spacing(0.25), + }), }); export { HeaderCell }; diff --git a/packages/grafana-ui/src/components/Table/TableNG/Filter/Filter.tsx b/packages/grafana-ui/src/components/Table/TableNG/Filter/Filter.tsx index acde579e401..6f2c55fc3d7 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Filter/Filter.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Filter/Filter.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { useMemo, useRef, useState } from 'react'; +import { useRef, useState } from 'react'; import { Field, GrafanaTheme2, SelectableValue } from '@grafana/data'; @@ -19,9 +19,19 @@ interface Props { field?: Field; crossFilterOrder: string[]; crossFilterRows: { [key: string]: TableRow[] }; + iconClassName?: string; } -export const Filter = ({ name, rows, filter, setFilter, field, crossFilterOrder, crossFilterRows }: Props) => { +export const Filter = ({ + name, + rows, + filter, + setFilter, + field, + crossFilterOrder, + crossFilterRows, + iconClassName, +}: Props) => { const filterValue = filter[name]?.filtered; // get rows for cross filtering @@ -42,13 +52,13 @@ export const Filter = ({ name, rows, filter, setFilter, field, crossFilterOrder, const ref = useRef(null); const [isPopoverVisible, setPopoverVisible] = useState(false); const styles = useStyles2(getStyles); - const filterEnabled = useMemo(() => Boolean(filterValue), [filterValue]); + const filterEnabled = Boolean(filterValue); const [searchFilter, setSearchFilter] = useState(filter[name]?.searchFilter || ''); const [operator, setOperator] = useState>(filter[name]?.operator || REGEX_OPERATOR); return (