diff --git a/eslint-suppressions.json b/eslint-suppressions.json
index 708c8f9fbcc..fdcf848a354 100644
--- a/eslint-suppressions.json
+++ b/eslint-suppressions.json
@@ -927,11 +927,6 @@
"count": 1
}
},
- "packages/grafana-ui/src/components/Table/TableCellInspector.tsx": {
- "@typescript-eslint/no-explicit-any": {
- "count": 1
- }
- },
"packages/grafana-ui/src/components/Table/TableNG/TableNG.test.tsx": {
"@typescript-eslint/no-explicit-any": {
"count": 2
diff --git a/packages/grafana-ui/src/components/Table/TableCellInspector.tsx b/packages/grafana-ui/src/components/Table/TableCellInspector.tsx
index ed7f9111773..10169328ba4 100644
--- a/packages/grafana-ui/src/components/Table/TableCellInspector.tsx
+++ b/packages/grafana-ui/src/components/Table/TableCellInspector.tsx
@@ -1,8 +1,10 @@
-import { isString } from 'lodash';
+import { css } from '@emotion/css';
import { useState } from 'react';
+import { GrafanaTheme2 } from '@grafana/data';
import { t, Trans } from '@grafana/i18n';
+import { useStyles2 } from '../../themes/ThemeContext';
import { ClipboardButton } from '../ClipboardButton/ClipboardButton';
import { Drawer } from '../Drawer/Drawer';
import { Stack } from '../Layout/Stack/Stack';
@@ -17,34 +19,15 @@ export enum TableCellInspectorMode {
interface TableCellInspectorProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- value: any;
+ value: string;
onDismiss: () => void;
mode: TableCellInspectorMode;
}
export function TableCellInspector({ value, onDismiss, mode }: TableCellInspectorProps) {
- let displayValue = value;
const [currentMode, setMode] = useState(mode);
-
- if (isString(value)) {
- const trimmedValue = value.trim();
- // Exclude numeric strings like '123' from being displayed in code/JSON mode
- if (trimmedValue[0] === '{' || trimmedValue[0] === '[' || mode === 'code') {
- try {
- value = JSON.parse(value);
- displayValue = JSON.stringify(value, null, ' ');
- } catch (error: any) {
- // Display helpful error to help folks diagnose json errors
- console.log(
- 'Failed to parse JSON in Table cell inspector (this will cause JSON to not print nicely): ',
- error.message
- );
- }
- }
- } else {
- displayValue = JSON.stringify(value);
- }
- let text = displayValue;
+ const text = value.trim();
+ const styles = useStyles2(getStyles);
const tabs = [
{
@@ -81,15 +64,23 @@ export function TableCellInspector({ value, onDismiss, mode }: TableCellInspecto
height={500}
language="json"
showLineNumbers={true}
- showMiniMap={(text && text.length) > 100}
+ showMiniMap={(text ? text.length : 0) > 100}
value={text}
readOnly={true}
wordWrap={true}
/>
) : (
-
{text}
+ {text}
)}
);
}
+
+// TODO: should we have different empty styles?
+const getStyles = (theme: GrafanaTheme2) => ({
+ textContainer: css({
+ color: theme.colors.text.secondary,
+ minHeight: 42,
+ }),
+});
diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/SparklineCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/SparklineCell.tsx
index 08f8b8b6cd1..acedb2091a7 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/Cells/SparklineCell.tsx
+++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/SparklineCell.tsx
@@ -1,15 +1,7 @@
import { css } from '@emotion/css';
import * as React from 'react';
-import {
- FieldType,
- FieldConfig,
- getMinMaxAndDelta,
- FieldSparkline,
- isDataFrame,
- Field,
- isDataFrameWithValue,
-} from '@grafana/data';
+import { FieldConfig, getMinMaxAndDelta, Field, isDataFrameWithValue } from '@grafana/data';
import { t } from '@grafana/i18n';
import {
BarAlignment,
@@ -26,7 +18,7 @@ import { measureText } from '../../../../utils/measureText';
import { FormattedValueDisplay } from '../../../FormattedValueDisplay/FormattedValueDisplay';
import { Sparkline } from '../../../Sparkline/Sparkline';
import { SparklineCellProps, TableCellStyles } from '../types';
-import { getAlignmentFactor, getCellOptions } from '../utils';
+import { getAlignmentFactor, getCellOptions, prepareSparklineValue } from '../utils';
export const defaultSparklineCellConfig: TableSparklineCellOptions = {
type: TableCellDisplayMode.Sparkline,
@@ -43,7 +35,7 @@ export const defaultSparklineCellConfig: TableSparklineCellOptions = {
export const SparklineCell = (props: SparklineCellProps) => {
const { field, value, theme, timeRange, rowIdx, width } = props;
- const sparkline = getSparkline(value, field);
+ const sparkline = prepareSparklineValue(value, field);
if (!sparkline) {
return <>{field.config.noValue || t('grafana-ui.table.sparkline.no-data', 'no data')}>;
@@ -102,30 +94,6 @@ export const SparklineCell = (props: SparklineCellProps) => {
);
};
-function getSparkline(value: unknown, field: Field): FieldSparkline | undefined {
- if (Array.isArray(value)) {
- return {
- y: {
- name: `${field.name}-sparkline`,
- type: FieldType.number,
- values: value,
- config: {},
- },
- };
- }
-
- if (isDataFrame(value)) {
- const timeField = value.fields.find((x) => x.type === FieldType.time);
- const numberField = value.fields.find((x) => x.type === FieldType.number);
-
- if (timeField && numberField) {
- return { x: timeField, y: numberField };
- }
- }
-
- return;
-}
-
function getTableSparklineCellOptions(field: Field): TableSparklineCellOptions {
let options = getCellOptions(field);
if (options.type === TableCellDisplayMode.Auto) {
diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx
index f5fb37b68d9..5f64c58773a 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx
+++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx
@@ -563,7 +563,6 @@ export function TableNG(props: TableNGProps) {
(
+ ({ field, value, setInspectCell, onCellFilterAdded, className, cellInspect, showFilters }: TableCellActionsProps) => (
// stopping propagation to prevent clicks within the actions menu from triggering the cell click events
// for things like the data links tooltip.
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
@@ -31,24 +17,8 @@ export const TableCellActions = memo(
name="eye"
aria-label={t('grafana-ui.table.cell-inspect-tooltip', 'Inspect value')}
onClick={() => {
- let inspectValue = value;
- let mode = TableCellInspectorMode.text;
-
- if (field.type === FieldType.geo && value instanceof Geometry) {
- inspectValue = new WKT().writeGeometry(value, {
- featureProjection: 'EPSG:3857',
- dataProjection: 'EPSG:4326',
- });
- mode = TableCellInspectorMode.code;
- }
- if (cellOptions.type === TableCellDisplayMode.JSONView) {
- mode = TableCellInspectorMode.code;
- }
-
- setInspectCell({
- value: String(inspectValue ?? ''),
- mode,
- });
+ const [inspectValue, mode] = buildInspectValue(value, field);
+ setInspectCell({ value: inspectValue, mode });
}}
/>
)}
diff --git a/packages/grafana-ui/src/components/Table/TableNG/types.ts b/packages/grafana-ui/src/components/Table/TableNG/types.ts
index 376fe895ba2..c4d63b2826b 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/types.ts
+++ b/packages/grafana-ui/src/components/Table/TableNG/types.ts
@@ -167,12 +167,12 @@ export type InspectCellProps = {
rowIdx?: number;
value: string;
mode?: TableCellInspectorMode.code | TableCellInspectorMode.text;
+ preformatted?: boolean;
};
export interface TableCellActionsProps {
field: Field;
value: TableCellValue;
- cellOptions: TableCellOptions;
displayName: string;
cellInspect: boolean;
showFilters: boolean;
diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts
index c6060814abe..d788ccdcb63 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts
+++ b/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts
@@ -1,3 +1,4 @@
+import { Point } from 'ol/geom';
import { SortColumn } from 'react-data-grid';
import {
@@ -49,6 +50,8 @@ import {
parseStyleJson,
calculateFooterHeight,
displayJsonValue,
+ prepareSparklineValue,
+ buildInspectValue,
} from './utils';
describe('TableNG utils', () => {
@@ -1531,4 +1534,152 @@ describe('TableNG utils', () => {
expect(parseStyleJson('{"notARealStyle": "someValue"}')).toEqual({ notARealStyle: 'someValue' });
});
});
+
+ describe('prepareSparklineValue', () => {
+ it('should return an array of numbers when given an array of numbers', () => {
+ expect(
+ prepareSparklineValue([1, 2, 3, 4, 5], {
+ name: 'test',
+ type: FieldType.number,
+ values: [1, 2, 3, 4, 5],
+ config: {},
+ })
+ ).toEqual({
+ y: {
+ name: `test-sparkline`,
+ type: FieldType.number,
+ values: [1, 2, 3, 4, 5],
+ config: {},
+ },
+ });
+ });
+
+ it('should parse the x and y values from a dataframe', () => {
+ const frame = createDataFrame({
+ fields: [
+ { name: 'x', type: FieldType.time, values: [0, 1000, 2000, 3000, 4000] },
+ { name: 'y', type: FieldType.number, values: [10, 20, 30, 40, 50] },
+ ],
+ });
+ expect(
+ prepareSparklineValue(frame, {
+ name: 'test',
+ type: FieldType.frame,
+ values: [frame],
+ config: {},
+ })
+ ).toEqual({
+ x: {
+ name: 'x',
+ type: FieldType.time,
+ values: [0, 1000, 2000, 3000, 4000],
+ config: {},
+ },
+ y: {
+ name: 'y',
+ type: FieldType.number,
+ values: [10, 20, 30, 40, 50],
+ config: {},
+ },
+ });
+ });
+
+ it('should return undefined for non-array and non-dataframe values', () => {
+ expect(
+ prepareSparklineValue('not an array or dataframe', {
+ name: 'test',
+ type: FieldType.string,
+ values: ['a', 'b', 'c'],
+ config: {},
+ })
+ ).toBeUndefined();
+ });
+ });
+
+ describe('buildInspectValue', () => {
+ const numberFieldWithNulls: Field = {
+ name: 'numbers-with-nulls',
+ type: FieldType.number,
+ values: [0, 1, 2, null, NaN],
+ config: {},
+ };
+ const stringField: Field = {
+ name: 'string',
+ type: FieldType.string,
+ values: ['foo', 'bar', 'baz', null],
+ config: {},
+ };
+ const jsonStringField: Field = {
+ ...stringField,
+ config: { custom: { cellOptions: { type: TableCellDisplayMode.JSONView } } },
+ };
+ const booleanField: Field = {
+ name: 'boolean-field',
+ type: FieldType.boolean,
+ values: [true, false, true],
+ config: {},
+ };
+ const sparklineField: Field = {
+ name: 'sparkline-field',
+ type: FieldType.frame,
+ values: [
+ createDataFrame({
+ fields: [
+ { name: 'x', type: FieldType.time, values: [0, 1000, 2000] },
+ { name: 'y', type: FieldType.number, values: [10, 20, 30] },
+ ],
+ }),
+ ],
+ config: {},
+ };
+ const sparklineFieldNoX: Field = {
+ name: 'sparkline-field-no-x',
+ type: FieldType.other,
+ values: [[2, 4, 6, 8, 10]],
+ config: {
+ custom: { cellOptions: { type: TableCellDisplayMode.Sparkline } },
+ },
+ };
+ const arrayField: Field = {
+ name: 'array-field',
+ type: FieldType.other,
+ values: [
+ ['foo', 'bar', 'baz'],
+ ['one', 'two', 'three'],
+ ],
+ config: {},
+ };
+ const objectField: Field = {
+ name: 'array-field',
+ type: FieldType.other,
+ values: [
+ { foo: true, b: 'baz' },
+ { foo: false, b: 'qux' },
+ ],
+ config: {},
+ };
+ const geoField: Field = {
+ name: 'geo-field',
+ type: FieldType.geo,
+ values: [new Point([0, -74.1])],
+ config: {},
+ };
+ it.each([
+ { name: 'numbers', input: { valueIdx: 0, field: numberFieldWithNulls } },
+ { name: 'string', input: { valueIdx: 0, field: stringField } },
+ { name: 'string w/ JSON', input: { valueIdx: 2, field: jsonStringField } },
+ { name: 'boolean', input: { valueIdx: 0, field: booleanField } },
+ { name: 'NaN', input: { valueIdx: 4, field: numberFieldWithNulls } },
+ { name: 'null', input: { valueIdx: 3, field: numberFieldWithNulls } },
+ { name: 'null w/ JSON', input: { valueIdx: 3, field: jsonStringField } },
+ { name: 'undefined', input: { valueIdx: 6, field: numberFieldWithNulls } },
+ { name: 'sparkline', input: { valueIdx: 0, field: sparklineField } },
+ { name: 'sparkline (no x)', input: { valueIdx: 0, field: sparklineFieldNoX } },
+ { name: 'array', input: { valueIdx: 0, field: arrayField } },
+ { name: 'object', input: { valueIdx: 0, field: objectField } },
+ { name: 'geo', input: { valueIdx: 0, field: geoField } },
+ ])('should handle $name', ({ input: { field, valueIdx = 0 } }) => {
+ expect(buildInspectValue(field.values[valueIdx], field)).toMatchSnapshot();
+ });
+ });
});
diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.ts
index d2dd402217d..e3ab23b3e0e 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/utils.ts
+++ b/packages/grafana-ui/src/components/Table/TableNG/utils.ts
@@ -1,5 +1,7 @@
import { Property } from 'csstype';
import memoize from 'micro-memoize';
+import WKT from 'ol/format/WKT';
+import Geometry from 'ol/geom/Geometry';
import { CSSProperties } from 'react';
import { SortColumn } from 'react-data-grid';
import tinycolor from 'tinycolor2';
@@ -15,6 +17,8 @@ import {
DisplayValueAlignmentFactors,
DataFrame,
DisplayProcessor,
+ isDataFrame,
+ FieldSparkline,
DecimalCount,
} from '@grafana/data';
import {
@@ -26,10 +30,11 @@ import {
} from '@grafana/schema';
import { getTextColorForAlphaBackground } from '../../../utils/colors';
+import { TableCellInspectorMode } from '../TableCellInspector';
import { TableCellOptions } from '../types';
import { inferPills } from './Cells/PillCell';
-import { AutoCellRenderer, getCellRenderer } from './Cells/renderers';
+import { AutoCellRenderer, getAutoRendererDisplayMode, getCellRenderer } from './Cells/renderers';
import { COLUMN, TABLE } from './constants';
import {
TableRow,
@@ -991,6 +996,75 @@ export const displayJsonValue: (field: Field) => DisplayProcessor = (field: Fiel
};
};
+export function prepareSparklineValue(value: unknown, field: Field): FieldSparkline | undefined {
+ if (Array.isArray(value)) {
+ return {
+ y: {
+ name: `${field.name}-sparkline`,
+ type: FieldType.number,
+ values: value,
+ config: {},
+ },
+ };
+ }
+
+ if (isDataFrame(value)) {
+ const timeField = value.fields.find((x) => x.type === FieldType.time);
+ const numberField = value.fields.find((x) => x.type === FieldType.number);
+
+ if (timeField && numberField) {
+ return { x: timeField, y: numberField };
+ }
+ }
+
+ return;
+}
+
+function isPlainObject(value: unknown): value is object {
+ return typeof value === 'object' && value != null && !Array.isArray(value);
+}
+
+export function buildInspectValue(value: unknown, field: Field): [string, TableCellInspectorMode] {
+ const cellOptions = getCellOptions(field);
+
+ let inspectValue: string;
+ let mode = TableCellInspectorMode.text;
+
+ if (field.type === FieldType.geo && value instanceof Geometry) {
+ inspectValue = new WKT().writeGeometry(value, {
+ featureProjection: 'EPSG:3857',
+ dataProjection: 'EPSG:4326',
+ });
+ mode = TableCellInspectorMode.code;
+ } else if (
+ cellOptions.type === TableCellDisplayMode.Sparkline ||
+ getAutoRendererDisplayMode(field) === TableCellDisplayMode.Sparkline
+ ) {
+ // rather than JSON.stringify this, manually format it to make the coordinate tuples more legible to the user.
+ const fieldSparkline = prepareSparklineValue(value, field);
+ inspectValue = '[';
+ if (fieldSparkline != null) {
+ // if an x value exists, render as a tuple [x,y], otherwise just y
+ const buildValString: (idx: number) => string =
+ fieldSparkline.x != null
+ ? (idx) => `[${fieldSparkline.x!.values[idx] ?? 'null'}, ${fieldSparkline.y.values[idx] ?? 'null'}]`
+ : (idx) => `${fieldSparkline.y.values[idx] ?? 'null'}`;
+ for (let i = 0; i < fieldSparkline.y.values.length; i++) {
+ inspectValue += `\n ${buildValString(i)}${i === fieldSparkline.y.values.length - 1 ? '\n' : ','}`;
+ }
+ }
+ inspectValue += ']';
+ mode = TableCellInspectorMode.code;
+ } else if (cellOptions.type === TableCellDisplayMode.JSONView || Array.isArray(value) || isPlainObject(value)) {
+ inspectValue = JSON.stringify(value, null, ' ');
+ mode = TableCellInspectorMode.code;
+ } else {
+ inspectValue = String(value ?? '');
+ }
+
+ return [inspectValue, mode];
+}
+
export function getSummaryCellTextAlign(textAlign: TextAlign, cellType: TableCellDisplayMode): TextAlign {
// gauge is weird. left-aligned gauge has the viz on the left and its numbers on the right, and vice-versa.
// if you center-aligned your gauge... ok.