TableNG: Simplify Pill cellType and reduce scope (#107934)

Co-authored-by: Paul Marbach <paul.marbach@grafana.com>
pull/108051/head
Leon Sorokin 1 week ago committed by GitHub
parent b0e85a637f
commit 7344c1c555
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      packages/grafana-data/src/field/fieldColor.ts
  2. 2
      packages/grafana-data/src/index.ts
  3. 2
      packages/grafana-schema/src/common/common.gen.ts
  4. 3
      packages/grafana-schema/src/common/table.cue
  5. 238
      packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.test.tsx
  6. 187
      packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.tsx
  7. 6
      packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.tsx
  8. 35
      packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx
  9. 5
      packages/grafana-ui/src/components/Table/TableNG/utils.ts
  10. 4
      public/app/plugins/panel/table/table-new/TableCellOptionEditor.tsx
  11. 66
      public/app/plugins/panel/table/table-new/cells/PillCellOptionsEditor.tsx
  12. 8
      public/locales/en-US/grafana.json

@ -218,7 +218,7 @@ export class FieldColorSchemeMode implements FieldColorMode {
} }
} else if (this.useSeriesName) { } else if (this.useSeriesName) {
return (_: number, _percent: number, _threshold?: Threshold) => { return (_: number, _percent: number, _threshold?: Threshold) => {
return colors[Math.abs(stringHash(field.state?.displayName ?? field.name)) % colors.length]; return getColorByStringHash(colors, field.state?.displayName ?? field.name);
}; };
} else { } else {
return (_: number, _percent: number, _threshold?: Threshold) => { return (_: number, _percent: number, _threshold?: Threshold) => {
@ -263,6 +263,10 @@ export function getFieldSeriesColor(field: Field, theme: GrafanaTheme2): ColorSc
return scale(value); return scale(value);
} }
export function getColorByStringHash(colors: string[], string: string) {
return colors[Math.abs(stringHash(string)) % colors.length];
}
function getFixedColor(field: Field, theme: GrafanaTheme2) { function getFixedColor(field: Field, theme: GrafanaTheme2) {
return () => { return () => {
return theme.visualization.getColorByName(field.config.color?.fixedColor ?? FALLBACK_COLOR); return theme.visualization.getColorByName(field.config.color?.fixedColor ?? FALLBACK_COLOR);

@ -137,6 +137,8 @@ export {
fieldColorModeRegistry, fieldColorModeRegistry,
type FieldColorMode, type FieldColorMode,
getFieldSeriesColor, getFieldSeriesColor,
/** @internal */
getColorByStringHash,
} from './field/fieldColor'; } from './field/fieldColor';
export { FieldConfigOptionsRegistry } from './field/FieldConfigOptionsRegistry'; export { FieldConfigOptionsRegistry } from './field/FieldConfigOptionsRegistry';
export { sortThresholds, getActiveThreshold } from './field/thresholds'; export { sortThresholds, getActiveThreshold } from './field/thresholds';

@ -977,8 +977,6 @@ export enum ComparisonOperation {
} }
export interface TablePillCellOptions { export interface TablePillCellOptions {
color?: string;
colorMode?: ('auto' | 'fixed' | 'mapped');
type: TableCellDisplayMode.Pill; type: TableCellDisplayMode.Pill;
} }

@ -111,7 +111,4 @@ TableFieldOptions: {
TablePillCellOptions: { TablePillCellOptions: {
type: TableCellDisplayMode & "pill" type: TableCellDisplayMode & "pill"
color?: string
colorMode?: "auto" | "fixed" | "mapped"
} @cuetsy(kind="interface") } @cuetsy(kind="interface")

@ -1,16 +1,18 @@
import { render, screen } from '@testing-library/react'; import { render, RenderResult } from '@testing-library/react';
import { DataFrame, Field, FieldType, GrafanaTheme2, MappingType, createTheme } from '@grafana/data'; import { DataFrame, Field, FieldType, GrafanaTheme2, MappingType, createTheme } from '@grafana/data';
import { TableCellDisplayMode, TablePillCellOptions } from '@grafana/schema'; import { TableCellDisplayMode, TablePillCellOptions } from '@grafana/schema';
import { mockThemeContext } from '../../../../themes/ThemeContext'; import { mockThemeContext } from '../../../../themes/ThemeContext';
import { PillCell, inferPills } from './PillCell'; import { PillCell, getStyles } from './PillCell';
describe('PillCell', () => { describe('PillCell', () => {
let pillClass: string;
let restoreThemeContext: () => void; let restoreThemeContext: () => void;
beforeEach(() => { beforeEach(() => {
pillClass = getStyles(createTheme()).pill;
restoreThemeContext = mockThemeContext(createTheme()); restoreThemeContext = mockThemeContext(createTheme());
}); });
@ -20,7 +22,6 @@ describe('PillCell', () => {
const mockCellOptions: TablePillCellOptions = { const mockCellOptions: TablePillCellOptions = {
type: TableCellDisplayMode.Pill, type: TableCellDisplayMode.Pill,
colorMode: 'auto',
}; };
const mockField: Field = { const mockField: Field = {
@ -37,7 +38,6 @@ describe('PillCell', () => {
}; };
const defaultProps = { const defaultProps = {
value: 'test-value',
field: mockField, field: mockField,
justifyContent: 'flex-start' as const, justifyContent: 'flex-start' as const,
cellOptions: mockCellOptions, cellOptions: mockCellOptions,
@ -50,170 +50,102 @@ describe('PillCell', () => {
showFilters: false, showFilters: false,
}; };
describe('pill parsing', () => { const ser = new XMLSerializer();
it('should render pills for single values', () => {
render(<PillCell {...defaultProps} />);
expect(screen.getByText('test-value')).toBeInTheDocument();
});
it('should render pills for CSV values', () => { const expectHTML = (result: RenderResult, expected: string) => {
render(<PillCell {...defaultProps} value="value1,value2,value3" />); let actual = ser.serializeToString(result.asFragment()).replace(/xmlns=".*?" /g, '');
expect(screen.getByText('value1')).toBeInTheDocument(); expect(actual).toEqual(expected.replace(/^\s*|\n/gm, ''));
expect(screen.getByText('value2')).toBeInTheDocument(); };
expect(screen.getByText('value3')).toBeInTheDocument();
});
it('should render pills for JSON array values', () => { // one class for lightTextPill, darkTextPill
render(<PillCell {...defaultProps} value='["item1","item2","item3"]' />);
expect(screen.getByText('item1')).toBeInTheDocument();
expect(screen.getByText('item2')).toBeInTheDocument();
expect(screen.getByText('item3')).toBeInTheDocument();
});
it('should show dash for empty values', () => { describe('Color by hash (classic palette)', () => {
render(<PillCell {...defaultProps} value="" />); const props = { ...defaultProps };
expect(screen.getByText('-')).toBeInTheDocument();
});
it('should show dash for null values', () => { it('single value', () => {
render(<PillCell {...defaultProps} value={null as unknown as string} />); expectHTML(
expect(screen.getByText('-')).toBeInTheDocument(); render(<PillCell {...props} value="value1" />),
`<span class="${pillClass}" style="background-color: rgb(63, 43, 91); color: rgb(255, 255, 255);">value1</span>`
);
}); });
});
describe('color mapping', () => {
// These tests primarily ensure the color logic executes without throwing.
// For true color verification, visual regression tests would be needed.
it('should use mapped colors when colorMode is mapped', () => { it('empty string', () => {
const mappedOptions: TablePillCellOptions = { expectHTML(render(<PillCell {...props} value="" />), '');
type: TableCellDisplayMode.Pill,
colorMode: 'mapped',
};
render(<PillCell {...defaultProps} value="success,error,warning,unknown" cellOptions={mappedOptions} />);
const successPill = screen.getByText('success');
const errorPill = screen.getByText('error');
const warningPill = screen.getByText('warning');
const unknownPill = screen.getByText('unknown');
expect(successPill).toBeInTheDocument();
expect(errorPill).toBeInTheDocument();
expect(warningPill).toBeInTheDocument();
expect(unknownPill).toBeInTheDocument();
}); });
it('should use field-level value mappings when available', () => { // it('null', () => {
const mappedOptions: TablePillCellOptions = { // expectHTML(
type: TableCellDisplayMode.Pill, // render(<PillCell {...props} value={null} />),
colorMode: 'mapped', // '<span class="${pillClass}" style="background-color: rgb(63, 43, 91); color: rgb(255, 255, 255);">value1</span>'
}; // );
// });
// Mock field with value mappings
const fieldWithMappings: Field = { it('CSV values', () => {
...mockField, expectHTML(
config: { render(<PillCell {...props} value="value1,value2,value3" />),
...mockField.config, `
mappings: [ <span class="${pillClass}" style="background-color: rgb(63, 43, 91); color: rgb(255, 255, 255);">value1</span>
{ <span class="${pillClass}" style="background-color: rgb(252, 226, 222); color: rgb(0, 0, 0);">value2</span>
type: MappingType.ValueToText, <span class="${pillClass}" style="background-color: rgb(81, 149, 206); color: rgb(0, 0, 0);">value3</span>
options: { `
success: { color: '#00FF00' },
error: { color: '#FF0000' },
warning: { color: '#FFFF00' },
},
},
],
},
display: (value: unknown) => ({
text: String(value),
color:
String(value) === 'success'
? '#00FF00'
: String(value) === 'error'
? '#FF0000'
: String(value) === 'warning'
? '#FFFF00'
: '#FF780A',
numeric: 0,
}),
};
render(
<PillCell
{...defaultProps}
value="success,error,warning,unknown"
cellOptions={mappedOptions}
field={fieldWithMappings}
/>
); );
const successPill = screen.getByText('success');
const errorPill = screen.getByText('error');
const warningPill = screen.getByText('warning');
const unknownPill = screen.getByText('unknown');
expect(successPill).toBeInTheDocument();
expect(errorPill).toBeInTheDocument();
expect(warningPill).toBeInTheDocument();
expect(unknownPill).toBeInTheDocument();
});
it('should use fixed color when colorMode is fixed', () => {
const fixedOptions: TablePillCellOptions = {
type: TableCellDisplayMode.Pill,
colorMode: 'fixed',
color: '#FF00FF',
};
render(<PillCell {...defaultProps} cellOptions={fixedOptions} />);
expect(screen.getByText('test-value')).toBeInTheDocument();
}); });
it('should use auto color when colorMode is auto', () => { it('JSON array values', () => {
const autoOptions: TablePillCellOptions = { expectHTML(
type: TableCellDisplayMode.Pill, render(<PillCell {...props} value='["value1","value2","value3"]' />),
colorMode: 'auto', `
}; <span class="${pillClass}" style="background-color: rgb(63, 43, 91); color: rgb(255, 255, 255);">value1</span>
<span class="${pillClass}" style="background-color: rgb(252, 226, 222); color: rgb(0, 0, 0);">value2</span>
render(<PillCell {...defaultProps} cellOptions={autoOptions} />); <span class="${pillClass}" style="background-color: rgb(81, 149, 206); color: rgb(0, 0, 0);">value3</span>
expect(screen.getByText('test-value')).toBeInTheDocument(); `
);
}); });
});
});
describe('inferPills', () => { // TODO: handle null values?
// These tests verify the pill parsing logic handles various input formats correctly.
// They ensure the function can extract pill values from different data structures.
it('should return empty array for null/undefined values', () => {
expect(inferPills(null)).toEqual([]);
expect(inferPills(undefined)).toEqual([]);
expect(inferPills('')).toEqual([]);
}); });
it('should parse single values', () => { describe('Color by value mappings', () => {
expect(inferPills('test')).toEqual(['test']); const field: Field = {
expect(inferPills('"quoted"')).toEqual(['quoted']); ...mockField,
expect(inferPills("'quoted'")).toEqual(['quoted']); config: {
}); ...mockField.config,
mappings: [
it('should parse CSV strings', () => { {
expect(inferPills('value1,value2,value3')).toEqual(['value1', 'value2', 'value3']); type: MappingType.ValueToText,
expect(inferPills(' value1 , value2 , value3 ')).toEqual(['value1', 'value2', 'value3']); options: {
expect(inferPills('value1, ,value3')).toEqual(['value1', 'value3']); success: { color: '#00FF00' },
}); error: { color: '#FF0000' },
warning: { color: '#FFFF00' },
it('should parse JSON arrays', () => { },
expect(inferPills('["item1","item2","item3"]')).toEqual(['item1', 'item2', 'item3']); },
expect(inferPills('["item1", "item2", "item3"]')).toEqual(['item1', 'item2', 'item3']); ],
expect(inferPills('["item1", null, "item3"]')).toEqual(['item1', 'item3']); },
}); display: (value: unknown) => ({
text: String(value),
color:
value === 'success' ? '#00FF00' : value === 'error' ? '#FF0000' : value === 'warning' ? '#FFFF00' : '#FF780A',
numeric: 0,
}),
};
const props = {
...defaultProps,
field,
};
it('CSV values', () => {
expectHTML(
render(<PillCell {...props} value="success,error,warning,unknown" />),
`
<span class="${pillClass}" style="background-color: rgb(0, 255, 0); color: rgb(0, 0, 0);">success</span>
<span class="${pillClass}" style="background-color: rgb(255, 0, 0); color: rgb(0, 0, 0);">error</span>
<span class="${pillClass}" style="background-color: rgb(255, 255, 0); color: rgb(0, 0, 0);">warning</span>
<span class="${pillClass}" style="background-color: rgb(255, 120, 10); color: rgb(0, 0, 0);">unknown</span>
`
);
});
it('should handle mixed content', () => { // TODO: handle null values?
// When JSON parsing fails, it falls back to CSV parsing
expect(inferPills('["item1", "item2"],extra')).toEqual(['["item1"', '"item2"]', 'extra']);
expect(inferPills('not-json,value')).toEqual(['not-json', 'value']);
}); });
}); });

@ -1,15 +1,19 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { Property } from 'csstype';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { GrafanaTheme2, isDataFrame, classicColors, colorManipulator, Field } from '@grafana/data'; import {
import { TablePillCellOptions } from '@grafana/schema'; GrafanaTheme2,
classicColors,
import { useStyles2 } from '../../../../themes/ThemeContext'; colorManipulator,
Field,
getColorByStringHash,
FALLBACK_COLOR,
} from '@grafana/data';
import { FieldColorModeId } from '@grafana/schema';
import { useStyles2, useTheme2 } from '../../../../themes/ThemeContext';
import { TableCellRendererProps } from '../types'; import { TableCellRendererProps } from '../types';
const DEFAULT_PILL_BG_COLOR = '#FF780A';
interface Pill { interface Pill {
value: string; value: string;
key: string; key: string;
@ -17,9 +21,9 @@ interface Pill {
color: string; color: string;
} }
function createPills(pillValues: string[], cellOptions: TableCellRendererProps['cellOptions'], field: Field): Pill[] { function createPills(pillValues: string[], field: Field, theme: GrafanaTheme2): Pill[] {
return pillValues.map((pill, index) => { return pillValues.map((pill, index) => {
const bgColor = getPillColor(pill, cellOptions, field); const bgColor = getPillColor(pill, field, theme);
const textColor = colorManipulator.getContrastRatio('#FFFFFF', bgColor) >= 4.5 ? '#FFFFFF' : '#000000'; const textColor = colorManipulator.getContrastRatio('#FFFFFF', bgColor) >= 4.5 ? '#FFFFFF' : '#000000';
return { return {
value: pill, value: pill,
@ -30,156 +34,73 @@ function createPills(pillValues: string[], cellOptions: TableCellRendererProps['
}); });
} }
export function PillCell({ value, field, justifyContent, cellOptions }: TableCellRendererProps) { export function PillCell({ value, field }: TableCellRendererProps) {
const styles = useStyles2(getStyles, justifyContent); const styles = useStyles2(getStyles);
const theme = useTheme2();
const pills: Pill[] = useMemo(() => { const pills: Pill[] = useMemo(() => {
const pillValues = inferPills(value); const pillValues = inferPills(String(value));
return createPills(pillValues, cellOptions, field); return createPills(pillValues, field, theme);
}, [value, cellOptions, field]); }, [value, field, theme]);
if (pills.length === 0) { return pills.map((pill) => (
return <div className={styles.cell}>-</div>; <span
} key={pill.key}
className={styles.pill}
return ( style={{
<div className={styles.cell}> backgroundColor: pill.bgColor,
<div className={styles.pillsContainer}> color: pill.color,
{pills.map((pill) => ( border: pill.bgColor === TRANSPARENT ? `1px solid ${theme.colors.border.strong}` : undefined,
<span }}
key={pill.key} >
className={styles.pill} {pill.value}
style={{ </span>
backgroundColor: pill.bgColor, ));
color: pill.color,
}}
>
{pill.value}
</span>
))}
</div>
</div>
);
} }
export function inferPills(value: unknown): string[] { const SPLIT_RE = /\s*,\s*/;
if (!value) { const TRANSPARENT = 'rgba(0,0,0,0)';
return [];
}
// Handle DataFrame - not supported for pills export function inferPills(value: string): string[] {
if (isDataFrame(value)) { if (value === '') {
return []; return [];
} }
// Handle different value types if (value[0] === '[') {
const stringValue = String(value); try {
return JSON.parse(value);
// Try to parse as JSON first } catch {
try { return value.trim().split(SPLIT_RE);
const parsed = JSON.parse(stringValue);
if (Array.isArray(parsed)) {
// JSON array of strings
return parsed
.filter((item) => item != null && item !== '')
.map(String)
.map((text) => text.trim())
.filter((item) => item !== '');
} }
} catch {
// Not valid JSON, continue with other parsing
}
// Handle CSV string
if (stringValue.includes(',')) {
return stringValue
.split(',')
.map((text) => text.trim())
.filter((item) => item !== '');
} }
// Single value - strip quotes return value.trim().split(SPLIT_RE);
return [stringValue.replace(/["'`]/g, '').trim()];
} }
function isPillCellOptions(cellOptions: TableCellRendererProps['cellOptions']): cellOptions is TablePillCellOptions { function getPillColor(value: string, field: Field, theme: GrafanaTheme2): string {
return cellOptions?.type === 'pill'; const cfg = field.config;
}
function getPillColor(pill: string, cellOptions: TableCellRendererProps['cellOptions'], field: Field): string { if (cfg.mappings?.length ?? 0 > 0) {
if (!isPillCellOptions(cellOptions)) { return field.display!(value).color ?? FALLBACK_COLOR;
return getDeterministicColor(pill);
} }
const colorMode = cellOptions.colorMode || 'auto'; if (cfg.color?.mode === FieldColorModeId.Fixed) {
return theme.visualization.getColorByName(cfg.color?.fixedColor ?? FALLBACK_COLOR);
// Fixed color mode (highest priority)
if (colorMode === 'fixed' && cellOptions.color) {
return cellOptions.color;
} }
// Mapped color mode - use field's value mappings // TODO: instead of classicColors we need to pull colors from theme, same way as FieldColorModeId.PaletteClassicByName (see fieldColor.ts)
if (colorMode === 'mapped') { return getColorByStringHash(classicColors, value);
// Check if field has value mappings
if (field.config.mappings && field.config.mappings.length > 0) {
// Use the field's display processor to get the mapped value
const displayValue = field.display!(pill);
if (displayValue.color) {
return displayValue.color;
}
}
// Fallback to default color for unmapped values
return cellOptions.color || DEFAULT_PILL_BG_COLOR;
}
// Auto mode - deterministic color assignment based on string hash
if (colorMode === 'auto') {
return getDeterministicColor(pill);
}
// Default color for unknown values or fallback
return DEFAULT_PILL_BG_COLOR;
} }
function getDeterministicColor(text: string): string { export const getStyles = (theme: GrafanaTheme2) => ({
// Create a simple hash of the string to get consistent colors
let hash = 0;
for (let i = 0; i < text.length; i++) {
const char = text.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32-bit integer
}
// Use absolute value and modulo to get a consistent index
const colorValues = Object.values(classicColors);
const index = Math.abs(hash) % colorValues.length;
return colorValues[index];
}
const getStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent | undefined) => ({
cell: css({
display: 'flex',
justifyContent: justifyContent || 'flex-start',
alignItems: 'center',
height: '100%',
padding: theme.spacing(0.5),
}),
pillsContainer: css({
display: 'flex',
flexWrap: 'wrap',
gap: theme.spacing(0.5),
maxWidth: '100%',
}),
pill: css({ pill: css({
display: 'inline-block', display: 'inline-block',
padding: theme.spacing(0.25, 0.75), padding: theme.spacing(0.25, 0.75),
marginInlineEnd: theme.spacing(0.5),
marginBlock: theme.spacing(0.5),
borderRadius: theme.shape.radius.default, borderRadius: theme.shape.radius.default,
fontSize: theme.typography.bodySmall.fontSize, fontSize: theme.typography.bodySmall.fontSize,
lineHeight: theme.typography.bodySmall.lineHeight, lineHeight: theme.typography.bodySmall.lineHeight,
fontWeight: theme.typography.fontWeightMedium,
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
textAlign: 'center',
minWidth: 'fit-content',
}), }),
}); });

@ -109,6 +109,12 @@ export function getCellRenderer(field: Field, cellOptions: TableCellOptions): Ta
if (cellType === TableCellDisplayMode.Auto) { if (cellType === TableCellDisplayMode.Auto) {
return getAutoRendererResult(field); return getAutoRendererResult(field);
} }
// TODO: add support boolean, enum, (maybe int). but for now just string fields
if (cellType === TableCellDisplayMode.Pill && field.type !== FieldType.string) {
return AUTO_RENDERER;
}
return CELL_RENDERERS[cellType] ?? AUTO_RENDERER; return CELL_RENDERERS[cellType] ?? AUTO_RENDERER;
} }

@ -14,9 +14,18 @@ import {
SortColumn, SortColumn,
} from 'react-data-grid'; } from 'react-data-grid';
import { DataHoverClearEvent, DataHoverEvent, Field, FieldType, GrafanaTheme2, ReducerID } from '@grafana/data'; import {
DataHoverClearEvent,
DataHoverEvent,
FALLBACK_COLOR,
Field,
FieldType,
getDisplayProcessor,
GrafanaTheme2,
ReducerID,
} from '@grafana/data';
import { t, Trans } from '@grafana/i18n'; import { t, Trans } from '@grafana/i18n';
import { TableCellHeight } from '@grafana/schema'; import { FieldColorModeId, TableCellHeight } from '@grafana/schema';
import { useStyles2, useTheme2 } from '../../../themes/ThemeContext'; import { useStyles2, useTheme2 } from '../../../themes/ThemeContext';
import { ContextMenu } from '../../ContextMenu/ContextMenu'; import { ContextMenu } from '../../ContextMenu/ContextMenu';
@ -272,11 +281,30 @@ export function TableNG(props: TableNGProps) {
let _rowHeight = 0; let _rowHeight = 0;
f.forEach((field, i) => { f.forEach((field, i) => {
const cellOptions = getCellOptions(field);
const cellType = cellOptions.type;
// make sure we use mappings exclusively if they exist, ignore default thresholds mode
// we hack this by using the single color mode calculator
if (cellType === TableCellDisplayMode.Pill && (field.config.mappings?.length ?? 0 > 0)) {
field = {
...field,
config: {
...field.config,
color: {
...field.config.color,
mode: FieldColorModeId.Fixed,
fixedColor: field.config.color?.fixedColor ?? FALLBACK_COLOR,
},
},
};
field.display = getDisplayProcessor({ field, theme });
}
const justifyContent = getTextAlign(field); const justifyContent = getTextAlign(field);
const footerStyles = getFooterStyles(justifyContent); const footerStyles = getFooterStyles(justifyContent);
const displayName = getDisplayName(field); const displayName = getDisplayName(field);
const headerCellClass = getHeaderCellStyles(theme, justifyContent).headerCell; const headerCellClass = getHeaderCellStyles(theme, justifyContent).headerCell;
const cellOptions = getCellOptions(field);
const renderFieldCell = getCellRenderer(field, cellOptions); const renderFieldCell = getCellRenderer(field, cellOptions);
const cellInspect = isCellInspectEnabled(field); const cellInspect = isCellInspectEnabled(field);
@ -293,7 +321,6 @@ export function TableNG(props: TableNGProps) {
) )
: undefined; : undefined;
const cellType = cellOptions.type;
const shouldOverflow = shouldTextOverflow(field); const shouldOverflow = shouldTextOverflow(field);
const shouldWrap = shouldTextWrap(field); const shouldWrap = shouldTextWrap(field);
const withTooltip = withDataLinksActionsTooltip(field, cellType); const withTooltip = withDataLinksActionsTooltip(field, cellType);

@ -128,11 +128,14 @@ export function getMaxWrapCell(
* Returns true if text overflow handling should be applied to the cell. * Returns true if text overflow handling should be applied to the cell.
*/ */
export function shouldTextOverflow(field: Field): boolean { export function shouldTextOverflow(field: Field): boolean {
let type = getCellOptions(field).type;
return ( return (
field.type === FieldType.string && field.type === FieldType.string &&
// Tech debt: Technically image cells are of type string, which is misleading (kinda?) // Tech debt: Technically image cells are of type string, which is misleading (kinda?)
// so we need to ensure we don't apply overflow hover states for type image // so we need to ensure we don't apply overflow hover states for type image
getCellOptions(field).type !== TableCellDisplayMode.Image && type !== TableCellDisplayMode.Image &&
type !== TableCellDisplayMode.Pill &&
!shouldTextWrap(field) && !shouldTextWrap(field) &&
!isCellInspectEnabled(field) !isCellInspectEnabled(field)
); );

@ -10,7 +10,6 @@ import { AutoCellOptionsEditor } from './cells/AutoCellOptionsEditor';
import { BarGaugeCellOptionsEditor } from './cells/BarGaugeCellOptionsEditor'; import { BarGaugeCellOptionsEditor } from './cells/BarGaugeCellOptionsEditor';
import { ColorBackgroundCellOptionsEditor } from './cells/ColorBackgroundCellOptionsEditor'; import { ColorBackgroundCellOptionsEditor } from './cells/ColorBackgroundCellOptionsEditor';
import { ImageCellOptionsEditor } from './cells/ImageCellOptionsEditor'; import { ImageCellOptionsEditor } from './cells/ImageCellOptionsEditor';
import { PillCellOptionsEditor } from './cells/PillCellOptionsEditor';
import { SparklineCellOptionsEditor } from './cells/SparklineCellOptionsEditor'; import { SparklineCellOptionsEditor } from './cells/SparklineCellOptionsEditor';
// The props that any cell type editor are expected // The props that any cell type editor are expected
@ -78,9 +77,6 @@ export const TableCellOptionEditor = ({ value, onChange }: Props) => {
{cellType === TableCellDisplayMode.Image && ( {cellType === TableCellDisplayMode.Image && (
<ImageCellOptionsEditor cellOptions={value} onChange={onCellOptionsChange} /> <ImageCellOptionsEditor cellOptions={value} onChange={onCellOptionsChange} />
)} )}
{cellType === TableCellDisplayMode.Pill && (
<PillCellOptionsEditor cellOptions={value} onChange={onCellOptionsChange} />
)}
</div> </div>
); );
}; };

@ -1,66 +0,0 @@
import { t } from '@grafana/i18n';
import { TablePillCellOptions } from '@grafana/schema';
import { Field, ColorPicker, RadioButtonGroup, Stack } from '@grafana/ui';
import { TableCellEditorProps } from '../TableCellOptionEditor';
const colorModeOptions: Array<{ value: 'auto' | 'fixed' | 'mapped'; label: string }> = [
{ value: 'auto', label: 'Auto' },
{ value: 'fixed', label: 'Fixed color' },
{ value: 'mapped', label: 'Value mapping' },
];
export const PillCellOptionsEditor = ({ cellOptions, onChange }: TableCellEditorProps<TablePillCellOptions>) => {
const colorMode = cellOptions.colorMode || 'auto';
const onColorModeChange = (mode: 'auto' | 'fixed' | 'mapped') => {
const updatedOptions = { ...cellOptions, colorMode: mode };
onChange(updatedOptions);
};
const onColorChange = (color: string) => {
const updatedOptions = { ...cellOptions, color };
onChange(updatedOptions);
};
return (
<Stack direction="column" gap={1}>
<Field
label={t('table.pill-cell-options-editor.label-color-mode', 'Color Mode')}
description={t(
'table.pill-cell-options-editor.description-color-mode',
'Choose how colors are assigned to pills'
)}
noMargin
>
<RadioButtonGroup value={colorMode} onChange={onColorModeChange} options={colorModeOptions} />
</Field>
{colorMode === 'fixed' && (
<Field
label={t('table.pill-cell-options-editor.label-fixed-color', 'Fixed Color')}
description={t(
'table.pill-cell-options-editor.description-fixed-color',
'All pills in this column will use this color'
)}
noMargin
>
<ColorPicker color={cellOptions.color || '#FF780A'} onChange={onColorChange} enableNamedColors={false} />
</Field>
)}
{colorMode === 'mapped' && (
<Field
label={t('table.pill-cell-options-editor.label-value-mappings-info', 'Value Mappings')}
description={t(
'table.pill-cell-options-editor.description-value-mappings-info',
'For Value Mappings either use the global table Value Mappings or the Field overrides Value Mappings. The default will fall back to the Color Scheme. '
)}
noMargin
>
<div>&nbsp;</div>
</Field>
)}
</Stack>
);
};

@ -11871,14 +11871,6 @@
"name-show-table-footer": "Show table footer", "name-show-table-footer": "Show table footer",
"name-show-table-header": "Show table header", "name-show-table-header": "Show table header",
"name-wrap-header-text": "Wrap header text", "name-wrap-header-text": "Wrap header text",
"pill-cell-options-editor": {
"description-color-mode": "Choose how colors are assigned to pills",
"description-fixed-color": "All pills in this column will use this color",
"description-value-mappings-info": "For Value Mappings either use the global table Value Mappings or the Field overrides Value Mappings. The default will fall back to the Color Scheme. ",
"label-color-mode": "Color Mode",
"label-fixed-color": "Fixed Color",
"label-value-mappings-info": "Value Mappings"
},
"placeholder-column-width": "auto", "placeholder-column-width": "auto",
"placeholder-fields": "All Numeric Fields" "placeholder-fields": "All Numeric Fields"
}, },

Loading…
Cancel
Save