Tables: Pills for Table Cells (#107485)
* v2 of pills for tables * cleanup bettererrrrr * cleanup pretty * i18n changes * add in the option for value mapping * value mapping * change to just use the value mapping from the table component * tests fixed * betterer all better now * fix pretty * i18n * fix gen issue * i18n * fix merge issue * Refactor pillcell to an interface for said pill, cleanup tests * mind the space says prettier
This commit is contained in:
@@ -708,6 +708,7 @@ export enum TableCellDisplayMode {
|
||||
Image = 'image',
|
||||
JSONView = 'json-view',
|
||||
LcdGauge = 'lcd-gauge',
|
||||
Pill = 'pill',
|
||||
Sparkline = 'sparkline',
|
||||
}
|
||||
|
||||
@@ -838,7 +839,38 @@ export enum TableCellHeight {
|
||||
* Table cell options. Each cell has a display mode
|
||||
* and other potential options for that display.
|
||||
*/
|
||||
export type TableCellOptions = (TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions);
|
||||
export type TableCellOptions = (TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TablePillCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions);
|
||||
|
||||
/**
|
||||
* Field options for each field within a table (e.g 10, "The String", 64.20, etc.)
|
||||
* Generally defines alignment, filtering capabilties, display options, etc.
|
||||
*/
|
||||
export interface TableFieldOptions {
|
||||
align: FieldTextAlignment;
|
||||
cellOptions: TableCellOptions;
|
||||
/**
|
||||
* This field is deprecated in favor of using cellOptions
|
||||
*/
|
||||
displayMode?: TableCellDisplayMode;
|
||||
filterable?: boolean;
|
||||
hidden?: boolean; // ?? default is missing or false ??
|
||||
/**
|
||||
* Hides any header for a column, useful for columns that show some static content or buttons.
|
||||
*/
|
||||
hideHeader?: boolean;
|
||||
inspect: boolean;
|
||||
minWidth?: number;
|
||||
width?: number;
|
||||
/**
|
||||
* Enables text wrapping for column headers
|
||||
*/
|
||||
wrapHeaderText?: boolean;
|
||||
}
|
||||
|
||||
export const defaultTableFieldOptions: Partial<TableFieldOptions> = {
|
||||
align: 'auto',
|
||||
inspect: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Use UTC/GMT timezone
|
||||
@@ -944,37 +976,12 @@ export enum ComparisonOperation {
|
||||
NEQ = 'neq',
|
||||
}
|
||||
|
||||
/**
|
||||
* Field options for each field within a table (e.g 10, "The String", 64.20, etc.)
|
||||
* Generally defines alignment, filtering capabilties, display options, etc.
|
||||
*/
|
||||
export interface TableFieldOptions {
|
||||
align: FieldTextAlignment;
|
||||
cellOptions: TableCellOptions;
|
||||
/**
|
||||
* This field is deprecated in favor of using cellOptions
|
||||
*/
|
||||
displayMode?: TableCellDisplayMode;
|
||||
filterable?: boolean;
|
||||
hidden?: boolean; // ?? default is missing or false ??
|
||||
/**
|
||||
* Hides any header for a column, useful for columns that show some static content or buttons.
|
||||
*/
|
||||
hideHeader?: boolean;
|
||||
inspect: boolean;
|
||||
minWidth?: number;
|
||||
width?: number;
|
||||
/**
|
||||
* Enables text wrapping for column headers
|
||||
*/
|
||||
wrapHeaderText?: boolean;
|
||||
export interface TablePillCellOptions {
|
||||
color?: string;
|
||||
colorMode?: ('auto' | 'fixed' | 'mapped');
|
||||
type: TableCellDisplayMode.Pill;
|
||||
}
|
||||
|
||||
export const defaultTableFieldOptions: Partial<TableFieldOptions> = {
|
||||
align: 'auto',
|
||||
inspect: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* A specific timezone from https://en.wikipedia.org/wiki/Tz_database
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,7 @@ package common
|
||||
// in the table such as colored text, JSON, gauge, etc.
|
||||
// The color-background-solid, gradient-gauge, and lcd-gauge
|
||||
// modes are deprecated in favor of new cell subOptions
|
||||
TableCellDisplayMode: "auto" | "color-text" | "color-background" | "color-background-solid" | "gradient-gauge" | "lcd-gauge" | "json-view" | "basic" | "image" | "gauge" | "sparkline" | "data-links" | "custom" | "actions" @cuetsy(kind="enum",memberNames="Auto|ColorText|ColorBackground|ColorBackgroundSolid|GradientGauge|LcdGauge|JSONView|BasicGauge|Image|Gauge|Sparkline|DataLinks|Custom|Actions")
|
||||
TableCellDisplayMode: "auto" | "color-text" | "color-background" | "color-background-solid" | "gradient-gauge" | "lcd-gauge" | "json-view" | "basic" | "image" | "gauge" | "sparkline" | "data-links" | "custom" | "actions" | "pill" @cuetsy(kind="enum",memberNames="Auto|ColorText|ColorBackground|ColorBackgroundSolid|GradientGauge|LcdGauge|JSONView|BasicGauge|Image|Gauge|Sparkline|DataLinks|Custom|Actions|Pill")
|
||||
|
||||
// Display mode to the "Colored Background" display
|
||||
// mode for table cells. Either displays a solid color (basic mode)
|
||||
@@ -89,7 +89,7 @@ TableCellHeight: "sm" | "md" | "lg" | "auto" @cuetsy(kind="enum")
|
||||
|
||||
// Table cell options. Each cell has a display mode
|
||||
// and other potential options for that display.
|
||||
TableCellOptions: TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions @cuetsy(kind="type")
|
||||
TableCellOptions: TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TablePillCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions @cuetsy(kind="type")
|
||||
|
||||
// Field options for each field within a table (e.g 10, "The String", 64.20, etc.)
|
||||
// Generally defines alignment, filtering capabilties, display options, etc.
|
||||
@@ -109,3 +109,9 @@ TableFieldOptions: {
|
||||
wrapHeaderText?: bool
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
TablePillCellOptions: {
|
||||
type: TableCellDisplayMode & "pill"
|
||||
color?: string
|
||||
colorMode?: "auto" | "fixed" | "mapped"
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { DataFrame, Field, FieldType, GrafanaTheme2, MappingType, createTheme } from '@grafana/data';
|
||||
import { TableCellDisplayMode, TablePillCellOptions } from '@grafana/schema';
|
||||
|
||||
import { mockThemeContext } from '../../../../themes/ThemeContext';
|
||||
|
||||
import { PillCell, inferPills } from './PillCell';
|
||||
|
||||
describe('PillCell', () => {
|
||||
let restoreThemeContext: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
restoreThemeContext = mockThemeContext(createTheme());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
restoreThemeContext();
|
||||
});
|
||||
|
||||
const mockCellOptions: TablePillCellOptions = {
|
||||
type: TableCellDisplayMode.Pill,
|
||||
colorMode: 'auto',
|
||||
};
|
||||
|
||||
const mockField: Field = {
|
||||
name: 'test',
|
||||
type: FieldType.string,
|
||||
values: [],
|
||||
config: {},
|
||||
};
|
||||
|
||||
const mockFrame: DataFrame = {
|
||||
name: 'test',
|
||||
fields: [mockField],
|
||||
length: 1,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
value: 'test-value',
|
||||
field: mockField,
|
||||
justifyContent: 'flex-start' as const,
|
||||
cellOptions: mockCellOptions,
|
||||
rowIdx: 0,
|
||||
frame: mockFrame,
|
||||
height: 30,
|
||||
width: 100,
|
||||
theme: {} as GrafanaTheme2,
|
||||
cellInspect: false,
|
||||
showFilters: false,
|
||||
};
|
||||
|
||||
describe('pill parsing', () => {
|
||||
it('should render pills for single values', () => {
|
||||
render(<PillCell {...defaultProps} />);
|
||||
expect(screen.getByText('test-value')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render pills for CSV values', () => {
|
||||
render(<PillCell {...defaultProps} value="value1,value2,value3" />);
|
||||
expect(screen.getByText('value1')).toBeInTheDocument();
|
||||
expect(screen.getByText('value2')).toBeInTheDocument();
|
||||
expect(screen.getByText('value3')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render pills for JSON array values', () => {
|
||||
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', () => {
|
||||
render(<PillCell {...defaultProps} value="" />);
|
||||
expect(screen.getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show dash for null values', () => {
|
||||
render(<PillCell {...defaultProps} value={null as unknown as string} />);
|
||||
expect(screen.getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
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', () => {
|
||||
const mappedOptions: TablePillCellOptions = {
|
||||
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', () => {
|
||||
const mappedOptions: TablePillCellOptions = {
|
||||
type: TableCellDisplayMode.Pill,
|
||||
colorMode: 'mapped',
|
||||
};
|
||||
|
||||
// Mock field with value mappings
|
||||
const fieldWithMappings: Field = {
|
||||
...mockField,
|
||||
config: {
|
||||
...mockField.config,
|
||||
mappings: [
|
||||
{
|
||||
type: MappingType.ValueToText,
|
||||
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', () => {
|
||||
const autoOptions: TablePillCellOptions = {
|
||||
type: TableCellDisplayMode.Pill,
|
||||
colorMode: 'auto',
|
||||
};
|
||||
|
||||
render(<PillCell {...defaultProps} cellOptions={autoOptions} />);
|
||||
expect(screen.getByText('test-value')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('inferPills', () => {
|
||||
// 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', () => {
|
||||
expect(inferPills('test')).toEqual(['test']);
|
||||
expect(inferPills('"quoted"')).toEqual(['quoted']);
|
||||
expect(inferPills("'quoted'")).toEqual(['quoted']);
|
||||
});
|
||||
|
||||
it('should parse CSV strings', () => {
|
||||
expect(inferPills('value1,value2,value3')).toEqual(['value1', 'value2', 'value3']);
|
||||
expect(inferPills(' value1 , value2 , value3 ')).toEqual(['value1', 'value2', 'value3']);
|
||||
expect(inferPills('value1, ,value3')).toEqual(['value1', 'value3']);
|
||||
});
|
||||
|
||||
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']);
|
||||
});
|
||||
|
||||
it('should handle mixed content', () => {
|
||||
// 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']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,185 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { Property } from 'csstype';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { GrafanaTheme2, isDataFrame, classicColors, colorManipulator, Field } from '@grafana/data';
|
||||
import { TablePillCellOptions } from '@grafana/schema';
|
||||
|
||||
import { useStyles2 } from '../../../../themes/ThemeContext';
|
||||
import { TableCellRendererProps } from '../types';
|
||||
|
||||
const DEFAULT_PILL_BG_COLOR = '#FF780A';
|
||||
|
||||
interface Pill {
|
||||
value: string;
|
||||
key: string;
|
||||
bgColor: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
function createPills(pillValues: string[], cellOptions: TableCellRendererProps['cellOptions'], field: Field): Pill[] {
|
||||
return pillValues.map((pill, index) => {
|
||||
const bgColor = getPillColor(pill, cellOptions, field);
|
||||
const textColor = colorManipulator.getContrastRatio('#FFFFFF', bgColor) >= 4.5 ? '#FFFFFF' : '#000000';
|
||||
return {
|
||||
value: pill,
|
||||
key: `${pill}-${index}`,
|
||||
bgColor,
|
||||
color: textColor,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function PillCell({ value, field, justifyContent, cellOptions }: TableCellRendererProps) {
|
||||
const styles = useStyles2(getStyles, justifyContent);
|
||||
|
||||
const pills: Pill[] = useMemo(() => {
|
||||
const pillValues = inferPills(value);
|
||||
return createPills(pillValues, cellOptions, field);
|
||||
}, [value, cellOptions, field]);
|
||||
|
||||
if (pills.length === 0) {
|
||||
return <div className={styles.cell}>-</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.cell}>
|
||||
<div className={styles.pillsContainer}>
|
||||
{pills.map((pill) => (
|
||||
<span
|
||||
key={pill.key}
|
||||
className={styles.pill}
|
||||
style={{
|
||||
backgroundColor: pill.bgColor,
|
||||
color: pill.color,
|
||||
}}
|
||||
>
|
||||
{pill.value}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function inferPills(value: unknown): string[] {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Handle DataFrame - not supported for pills
|
||||
if (isDataFrame(value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Handle different value types
|
||||
const stringValue = String(value);
|
||||
|
||||
// Try to parse as JSON first
|
||||
try {
|
||||
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 [stringValue.replace(/["'`]/g, '').trim()];
|
||||
}
|
||||
|
||||
function isPillCellOptions(cellOptions: TableCellRendererProps['cellOptions']): cellOptions is TablePillCellOptions {
|
||||
return cellOptions?.type === 'pill';
|
||||
}
|
||||
|
||||
function getPillColor(pill: string, cellOptions: TableCellRendererProps['cellOptions'], field: Field): string {
|
||||
if (!isPillCellOptions(cellOptions)) {
|
||||
return getDeterministicColor(pill);
|
||||
}
|
||||
|
||||
const colorMode = cellOptions.colorMode || 'auto';
|
||||
|
||||
// Fixed color mode (highest priority)
|
||||
if (colorMode === 'fixed' && cellOptions.color) {
|
||||
return cellOptions.color;
|
||||
}
|
||||
|
||||
// Mapped color mode - use field's value mappings
|
||||
if (colorMode === 'mapped') {
|
||||
// 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 {
|
||||
// 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({
|
||||
display: 'inline-block',
|
||||
padding: theme.spacing(0.25, 0.75),
|
||||
borderRadius: theme.shape.radius.default,
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
lineHeight: theme.typography.bodySmall.lineHeight,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
whiteSpace: 'nowrap',
|
||||
textAlign: 'center',
|
||||
minWidth: 'fit-content',
|
||||
}),
|
||||
});
|
||||
@@ -12,6 +12,7 @@ import { DataLinksCell } from './DataLinksCell';
|
||||
import { GeoCell } from './GeoCell';
|
||||
import { ImageCell } from './ImageCell';
|
||||
import { JSONCell } from './JSONCell';
|
||||
import { PillCell } from './PillCell';
|
||||
import { SparklineCell } from './SparklineCell';
|
||||
|
||||
export type TableCellRenderer = (props: TableCellRendererProps) => ReactNode;
|
||||
@@ -81,6 +82,8 @@ const DATA_LINKS_RENDERER: TableCellRenderer = (props) => <DataLinksCell field={
|
||||
|
||||
const ACTIONS_RENDERER: TableCellRenderer = (props) => <ActionsCell actions={props.actions} />;
|
||||
|
||||
const PILL_RENDERER: TableCellRenderer = (props) => <PillCell {...props} />;
|
||||
|
||||
function isCustomCellOptions(options: TableCellOptions): options is TableCustomCellOptions {
|
||||
return options.type === TableCellDisplayMode.Custom;
|
||||
}
|
||||
@@ -104,6 +107,7 @@ const CELL_RENDERERS: Record<TableCellOptions['type'], TableCellRenderer> = {
|
||||
[TableCellDisplayMode.ColorText]: AUTO_RENDERER,
|
||||
[TableCellDisplayMode.ColorBackground]: AUTO_RENDERER,
|
||||
[TableCellDisplayMode.Auto]: AUTO_RENDERER,
|
||||
[TableCellDisplayMode.Pill]: PILL_RENDERER,
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
|
||||
@@ -196,6 +196,8 @@ export function getCellComponent(displayMode: TableCellDisplayMode, field: Field
|
||||
return DataLinksCell;
|
||||
case TableCellDisplayMode.Actions:
|
||||
return ActionsCell;
|
||||
case TableCellDisplayMode.Pill:
|
||||
return DefaultCell; // Legacy table doesn't support pill cells, fallback to default
|
||||
}
|
||||
|
||||
if (field.type === FieldType.geo) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { AutoCellOptionsEditor } from './cells/AutoCellOptionsEditor';
|
||||
import { BarGaugeCellOptionsEditor } from './cells/BarGaugeCellOptionsEditor';
|
||||
import { ColorBackgroundCellOptionsEditor } from './cells/ColorBackgroundCellOptionsEditor';
|
||||
import { ImageCellOptionsEditor } from './cells/ImageCellOptionsEditor';
|
||||
import { PillCellOptionsEditor } from './cells/PillCellOptionsEditor';
|
||||
import { SparklineCellOptionsEditor } from './cells/SparklineCellOptionsEditor';
|
||||
|
||||
// The props that any cell type editor are expected
|
||||
@@ -77,6 +78,9 @@ export const TableCellOptionEditor = ({ value, onChange }: Props) => {
|
||||
{cellType === TableCellDisplayMode.Image && (
|
||||
<ImageCellOptionsEditor cellOptions={value} onChange={onCellOptionsChange} />
|
||||
)}
|
||||
{cellType === TableCellDisplayMode.Pill && (
|
||||
<PillCellOptionsEditor cellOptions={value} onChange={onCellOptionsChange} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -91,6 +95,7 @@ let cellDisplayModeOptions: Array<SelectableValue<TableCellOptions>> = [
|
||||
{ value: { type: TableCellDisplayMode.JSONView }, label: 'JSON View' },
|
||||
{ value: { type: TableCellDisplayMode.Image }, label: 'Image' },
|
||||
{ value: { type: TableCellDisplayMode.Actions }, label: 'Actions' },
|
||||
{ value: { type: TableCellDisplayMode.Pill }, label: 'Pill' },
|
||||
];
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
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> </div>
|
||||
</Field>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -11768,6 +11768,14 @@
|
||||
"name-show-table-footer": "Show table footer",
|
||||
"name-show-table-header": "Show table header",
|
||||
"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-fields": "All Numeric Fields"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user