Skip to content

Commit

Permalink
Add customization for column header (#141)
Browse files Browse the repository at this point in the history
* ui customization for column header

* update e2e test for dashboard scene variant

* update layout and font size option

* update to header: { fontSize, fontColor, backgroundColor }

* update using fontSize

* use separate editor for color picker

* Update CHANGELOG.md

* Remove unused code and apply size to header buttons

* Formatting

---------

Co-authored-by: Mikhail Volkov <[email protected]>
Co-authored-by: asimonok <[email protected]>
  • Loading branch information
3 people authored Oct 30, 2024
1 parent 93df2bc commit 5c5b743
Show file tree
Hide file tree
Showing 19 changed files with 500 additions and 65 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- Updated to Grafana 11.3. and dependencies (#137)
- Added colored Text and colored Background for aggregated rows (#136)
- Added Handling Data Source Request Errors (#140)
- Added customization for column header (#141)

## 1.5.0 (2024-10-08)

Expand Down
21 changes: 21 additions & 0 deletions src/__mocks__/@grafana/ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,25 @@ const ConfirmModalMock = ({ onConfirm, onDismiss, isOpen = true, ...restProps }:

const ConfirmModal = jest.fn(ConfirmModalMock);

/**
* Mock ColorPickerInput component
*/
const ColorPickerMock = ({ onChange, value, ...restProps }: any) => {
return (
<input
data-testid={restProps['data-testid']}
value={value}
onChange={(event) => {
if (onChange) {
onChange(event.target.value);
}
}}
/>
);
};

const ColorPicker = jest.fn(ColorPickerMock);

beforeEach(() => {
Button.mockImplementation(ButtonMock);
Select.mockImplementation(SelectMock);
Expand All @@ -268,6 +287,7 @@ beforeEach(() => {
DataLinksContextMenu.mockImplementation(DataLinksContextMenuMock);
StatsPicker.mockImplementation(StatsPickerMock);
ConfirmModal.mockImplementation(ConfirmModalMock);
ColorPicker.mockImplementation(ColorPickerMock);
});

module.exports = {
Expand All @@ -284,4 +304,5 @@ module.exports = {
DataLinksContextMenu,
StatsPicker,
ConfirmModal,
ColorPicker,
};
18 changes: 18 additions & 0 deletions src/components/Table/Table.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,24 @@ export const getStyles = (theme: GrafanaTheme2) => {
border-right: 1px solid ${borderColor};
}
`,
sizeLg: css`
font-size: ${theme.typography.pxToRem(18)};
min-height: ${theme.spacing(4)};
padding: ${theme.spacing(1)};
`,
sizeMd: css`
font-size: ${theme.typography.pxToRem(14)};
min-height: ${theme.spacing(3.5)};
padding: ${theme.spacing(0.75)};
`,
sizeSm: css`
font-size: ${theme.typography.pxToRem(12)};
min-height: ${theme.spacing(3)};
`,
sizeXs: css`
font-size: ${theme.typography.pxToRem(10)};
min-height: ${theme.spacing(2.5)};
`,
body: css`
display: grid;
position: relative; //needed for absolute positioning of rows
Expand Down
48 changes: 30 additions & 18 deletions src/components/Table/Table.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { cx } from '@emotion/css';
import { EventBus, GrafanaTheme2 } from '@grafana/data';
import { Pagination, useStyles2, useTheme2 } from '@grafana/ui';
import {
Expand All @@ -22,7 +23,7 @@ import React, { CSSProperties, MutableRefObject, RefObject, useCallback, useEffe

import { ButtonSelect } from '@/components';
import { TEST_IDS } from '@/constants';
import { ColumnPinDirection, Pagination as PaginationOptions } from '@/types';
import { ColumnHeaderFontSize, ColumnPinDirection, Pagination as PaginationOptions } from '@/types';

import { TableHeaderCell, TableRow } from './components';
import { useEditableData, useSortState, useSyncedColumnFilters } from './hooks';
Expand Down Expand Up @@ -349,23 +350,34 @@ export const Table = <TData,>({
<thead className={styles.header} ref={tableHeaderRef} style={{ top: topOffset }}>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id} className={styles.headerRow} {...TEST_IDS.table.headerRow.apply(headerGroup.id)}>
{headerGroup.headers.map((header) => (
<th
key={header.id}
className={styles.headerCell}
style={{
maxWidth: header.column.columnDef.maxSize,
minWidth: header.column.columnDef.minSize,
width: header.getSize(),
textAlign: header.column.columnDef.meta?.config.appearance.alignment,
justifyContent: header.column.columnDef.meta?.config.appearance.alignment,
...getPinnedHeaderColumnStyle(theme, header.column),
}}
{...TEST_IDS.table.headerCell.apply(header.id)}
>
<TableHeaderCell header={header} />
</th>
))}
{headerGroup.headers.map((header) => {
const bgColor = header.column.columnDef.meta?.config.appearance.header?.backgroundColor;
const fontSize =
header.column.columnDef.meta?.config.appearance.header.fontSize || ColumnHeaderFontSize.MD;
return (
<th
key={header.id}
className={cx(styles.headerCell, {
[styles.sizeLg]: fontSize === ColumnHeaderFontSize.LG,
[styles.sizeMd]: fontSize === ColumnHeaderFontSize.MD,
[styles.sizeSm]: fontSize === ColumnHeaderFontSize.SM,
[styles.sizeXs]: fontSize === ColumnHeaderFontSize.XS,
})}
style={{
maxWidth: header.column.columnDef.maxSize,
minWidth: header.column.columnDef.minSize,
background: bgColor,
width: header.getSize(),
textAlign: header.column.columnDef.meta?.config.appearance.alignment,
justifyContent: header.column.columnDef.meta?.config.appearance.alignment,
...getPinnedHeaderColumnStyle(theme, header.column),
}}
{...TEST_IDS.table.headerCell.apply(header.id)}
>
<TableHeaderCell header={header} size={fontSize} />
</th>
);
})}
</tr>
))}
</thead>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { flexRender, Header } from '@tanstack/react-table';
import React from 'react';

import { ACTIONS_COLUMN_ID, TEST_IDS } from '@/constants';
import { ColumnHeaderFontSize } from '@/types';

import { getStyles } from './TableHeaderCell.styles';
import { TableHeaderCellFilter } from './TableHeaderCellFilter';
Expand All @@ -16,18 +17,25 @@ interface Props<TData> {
* Header
*/
header: Header<TData, unknown>;

/**
* Size
*
* @type {ColumnHeaderFontSize}
*/
size: ColumnHeaderFontSize;
}

/**
* Table Header Cell
*/
export const TableHeaderCell = <TData,>({ header }: Props<TData>) => {
export const TableHeaderCell = <TData,>({ header, size }: Props<TData>) => {
/**
* Styles
*/
const styles = useStyles2(getStyles);

const sort = header.column.getIsSorted();
const fontColor = header.column.columnDef.meta?.config.appearance.header.fontColor || 'inherit';

/**
* Actions Header
Expand All @@ -43,17 +51,21 @@ export const TableHeaderCell = <TData,>({ header }: Props<TData>) => {
className={cx({
[styles.labelSortable]: header.column.getCanSort(),
})}
style={{
color: fontColor,
}}
{...TEST_IDS.tableHeaderCell.root.apply()}
>
{flexRender(header.column.columnDef.header, header.getContext())}
{!!sort && (
<Icon
name={sort === 'asc' ? 'arrow-up' : 'arrow-down'}
size={size}
{...TEST_IDS.tableHeaderCell.sortIcon.apply(sort === 'asc' ? 'arrow-up' : 'arrow-down')}
/>
)}
</div>
{header.column.columnDef.enableColumnFilter && <TableHeaderCellFilter header={header} />}
{header.column.columnDef.enableColumnFilter && <TableHeaderCellFilter header={header} size={size} />}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Header } from '@tanstack/react-table';
import React, { useCallback, useRef, useState } from 'react';

import { TEST_IDS } from '@/constants';
import { ColumnHeaderFontSize } from '@/types';

import { FilterPopup } from '../FilterPopup';
import { getStyles } from './TableHeaderCellFilter.styles';
Expand All @@ -15,12 +16,19 @@ interface Props<TData> {
* Header
*/
header: Header<TData, unknown>;

/**
* Size
*
* @type {ColumnHeaderFontSize}
*/
size: ColumnHeaderFontSize;
}

/**
* Table Header Cell Filter
*/
export const TableHeaderCellFilter = <TData,>({ header }: Props<TData>) => {
export const TableHeaderCellFilter = <TData,>({ header, size }: Props<TData>) => {
/**
* Styles
*/
Expand Down Expand Up @@ -58,6 +66,7 @@ export const TableHeaderCellFilter = <TData,>({ header }: Props<TData>) => {
>
<Icon
name="filter"
size={size}
style={{
color: header.column.getIsFiltered() ? theme.colors.primary.text : theme.colors.secondary.text,
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,12 @@ import React, { useMemo } from 'react';

import { FieldPicker } from '@/components';
import { TEST_IDS } from '@/constants';
import { FieldSource, QueryOptionsMapper } from '@/types';
import { EditorProps, FieldSource, QueryOptionsMapper } from '@/types';

/**
* Properties
*/
interface Props {
/**
* Value
*
* @type {QueryOptionsMapper | undefined}
*/
value: QueryOptionsMapper | undefined;

/**
* On Change
*/
onChange: (value: QueryOptionsMapper | undefined) => void;

interface Props extends EditorProps<QueryOptionsMapper | undefined> {
/**
* Data
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';

/**
* Styles
*/
export const getStyles = (theme: GrafanaTheme2) => {
return {
colorPickerContainer: css`
align-items: center;
`,
colorPickerButtons: css`
display: flex;
justify-content: flex-start;
align-items: center;
gap: ${theme.spacing(1)};
padding: ${theme.spacing(0, 1)};
min-height: ${theme.spacing(4)};
`,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { getJestSelectors } from '@volkovlabs/jest-selectors';
import React from 'react';

import { ColorEditor, testIds } from './ColorEditor';

type Props = React.ComponentProps<typeof ColorEditor>;

describe('ColorEditor', () => {
/**
* Selectors
*/
const getSelectors = getJestSelectors(testIds);
const selectors = getSelectors(screen);

/**
* Change
*/
const onChange = jest.fn();

/**
* Get component
*/
const getComponent = (props: Partial<Props>) => {
return <ColorEditor onChange={onChange} value={undefined} {...props} />;
};

it('Should allow to change value', () => {
render(getComponent({ value: '#ffffff', onChange }));

expect(selectors.fieldValue()).toBeInTheDocument();
fireEvent.change(selectors.fieldValue(), { target: { value: '#000' } });
expect(onChange).toHaveBeenCalledWith('#000');
});

it('Should allow to reset value', () => {
render(getComponent({ value: '#ffffff', onChange }));

expect(selectors.fieldValue()).toBeInTheDocument();
expect(selectors.buttonClear()).toBeInTheDocument();

fireEvent.click(selectors.buttonClear());
expect(onChange).toHaveBeenCalledWith(undefined);
});

it('Should not allow to reset value if empty', () => {
render(getComponent({ value: undefined, onChange }));

expect(selectors.buttonClear(true)).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { ColorPicker, IconButton, useStyles2 } from '@grafana/ui';
import React from 'react';

import { TEST_IDS } from '@/constants';
import { EditorProps } from '@/types';

import { getStyles } from './ColorEditor.styles';

/**
* Properties
*/
interface Props extends EditorProps<string | undefined> {
/**
* Test ID
*
* @type {string}
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
'data-testid'?: string;
}

/**
* Test Ids
*/
export const testIds = TEST_IDS.colorEditor;

/**
* Color Editor
*/
export const ColorEditor: React.FC<Props> = ({ onChange, value, ...restProps }) => {
/**
* Styles
*/
const styles = useStyles2(getStyles);

return (
<div className={styles.colorPickerButtons}>
<ColorPicker
color={value || 'transparent'}
onChange={(color) => {
onChange(color);
}}
{...(restProps['data-testid'] ? { 'data-testid': restProps['data-testid'] } : testIds.fieldValue.apply())}
/>
{value && (
<IconButton
name="times"
size="md"
variant="secondary"
tooltip="Clear"
onClick={() => onChange(undefined)}
{...testIds.buttonClear.apply()}
/>
)}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ColorEditor';
Loading

0 comments on commit 5c5b743

Please sign in to comment.