TableNG: Markdown cell, plus auto row height (#107549)
* TableNG: Markdown cell, plus custom row height * tab indentation in cue file * fix i18n * trying an auto height with the updated RDG * get auto cellHeight working * i18n updates * hoor disable_sanitize_html flag in MarkdownCell * update react-data-grid version to attempt to support page up and down * removing custom height * use the latest experimental RDG with paging up and down * TableNG: Wrap text for DataLinks and Pills; groundwork for max wrap length * disable editing max wrapped lines for now * disable wrap text line limit e2e * new i18n extract after commenting out input * wip * kill max wrapped lines for now * more cleanup * remove targeting classes added for max wrapped lines * fix Pill test * couple more style cleanups * fix e2es given these updates * add a couple tests * wip: tests * add tests * bump up capital letters in lorem ipsum * fix copy-pasta mistake * whoops, mis-merged the selector * use a local count instead of getCellLinks * use react-data-grid on react-18 branch * fix linting on test * gdev dashboard and smoketest for Markdown table * remove cellHeightCustom * reorganize in light of recent and upcoming changes * remove one more reference to cellHeightCustom * put getDefaultRowHeight back into a util * clean up test * swap cell height back to a radio * revert ImageCell change, we'll do it in the getStyles PR * don't memo defaultRowHeight * final couple of style cleanups * different approach to managing the auto height part of this * kill console.log * update i18n * reorganized once more * i18n * guard against rowHeight being auto for virtualization * may as well memoize the defaultRowHeight * get rid of the enableVirtualization initializer thing * fixes from CI * fix test * fix test * just omit third arg for that test * remove nonsensical test case * this file didn't get re-gen'd * fixes from review * row expander doesn't need height * remove console.log * fix e2e after we fixed pagination toggle bug
This commit is contained in:
@@ -3932,6 +3932,9 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"],
|
||||
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"]
|
||||
],
|
||||
"public/app/plugins/panel/table/table-new/cells/MarkdownCellOptionsEditor.tsx:5381": [
|
||||
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/table/table-new/cells/SparklineCellOptionsEditor.tsx:5381": [
|
||||
[0, 0, 0, "\'VerticalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"],
|
||||
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"]
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": 1735,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "gdev-testdata"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"align": "auto",
|
||||
"cellOptions": {
|
||||
"type": "auto"
|
||||
},
|
||||
"inspect": false,
|
||||
"wrapHeaderText": false
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "markdown"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.cellOptions",
|
||||
"value": {
|
||||
"type": "markdown",
|
||||
"dynamicHeight": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 18,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 1,
|
||||
"options": {
|
||||
"cellHeight": "lg",
|
||||
"footer": {
|
||||
"enablePagination": true,
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": [
|
||||
"sum"
|
||||
],
|
||||
"show": false
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"csvContent": "id,markdown\n1,\"<dl>\n <dt>Definition list</dt>\n <dd>Is something people use sometimes.</dd>\n <dt>Markdown in HTML</dt>\n <dd>Does *not* work **very** well. Use HTML <em onclick=\"\"alert('hi');\"\">tags</em>.</dd>\n</dl>\"\n2,\"Three or more...\n\n---\n\nHyphens\n\n***\n\nAsterisks\n\n___\n\nUnderscores\"\n3,\"Here's a line for us to start with.\n\nThis line is separated from the one above by two newlines, so it will be a *separate paragraph*.\n\nThis line is also a separate paragraph, but...\nThis line is only separated by a single newline, so it's a separate line in the *same paragraph*.\"\n4,\"<span style='color:red'>red</span>, <span style='color:green'>green</span>, <span style='color:blue'>blue</span>\"\n5,\"<img src='https://placecats.com/bella/300/180'>\"\n6,\"[Link](https://grafana.com), or <a href=\"\"https://grafana.com\"\" target=\"\"_blank\"\" ref=\"\"noopener noreferrer\"\">HTML link</a>\"\n7,\"1. foo\n1. bar\n - baz\n * bim\n3. bip\"\n8,\"# heading 1\n## heading 2\n### heading 3\n#### heading 4\n##### heading 5\n###### heading 6\"\n9,\"Emphasis, aka italics, with *asterisks* or _underscores_.\n\nStrong emphasis, aka bold, with **asterisks** or __underscores__.\n\nCombined emphasis with **asterisks and _underscores_**.\n\nStrikethrough uses two tildes. ~~Scratch this.~~\n\n<u>underline does require an HTML element tho.</u>\"\n10,\"```javascript\nvar s = 'JavaScript syntax highlighting';\nalert(s);\n```\"\n11,\"<style>thead { border-bottom: 1px solid lightgray; } td, th { padding: 6px; }</style>\n| Month | Savings |\n| -------- | ------- |\n| I heard | $250 |\n| you like | $80 |\n| tables | $365 |\"\n",
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "gdev-testdata"
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_content"
|
||||
}
|
||||
],
|
||||
"title": "Markdown Table",
|
||||
"type": "table"
|
||||
}
|
||||
],
|
||||
"preload": false,
|
||||
"schemaVersion": 41,
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Panel Tests - Table - Markdown",
|
||||
"uid": "2769f5d8-0094-4ac4-a4f0-f68f620339cc",
|
||||
"version": 1
|
||||
}
|
||||
@@ -94,6 +94,7 @@
|
||||
"shared_queries": (import '../dev-dashboards/panel-common/shared_queries.json'),
|
||||
"slow_queries_and_annotations": (import '../dev-dashboards/scenarios/slow_queries_and_annotations.json'),
|
||||
"table_kitchen_sink": (import '../dev-dashboards/panel-table/table_kitchen_sink.json'),
|
||||
"table_markdown": (import '../dev-dashboards/panel-table/table_markdown.json'),
|
||||
"table_pagination": (import '../dev-dashboards/panel-table/table_pagination.json'),
|
||||
"table_sparkline_cell": (import '../dev-dashboards/panel-table/table_sparkline_cell.json'),
|
||||
"table_tests": (import '../dev-dashboards/panel-table/table_tests.json'),
|
||||
|
||||
@@ -225,7 +225,7 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table']
|
||||
// in a second, though.
|
||||
const smallRowStatus = await getRowStatus(page);
|
||||
expect(smallRowStatus.end).toBeGreaterThan(1);
|
||||
expect(page.getByRole('grid').getByRole('row')).toHaveCount(smallRowStatus.end + 1);
|
||||
expect(page.getByRole('grid').getByRole('row')).toHaveCount(smallRowStatus.end + 2); // +2 for header and footer rows
|
||||
|
||||
// change cell height to Large
|
||||
await dashboardPage
|
||||
@@ -235,7 +235,7 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table']
|
||||
.click();
|
||||
const largeRowStatus = await getRowStatus(page);
|
||||
expect(largeRowStatus.end).toBeLessThan(smallRowStatus.end);
|
||||
expect(page.getByRole('grid').getByRole('row')).toHaveCount(largeRowStatus.end + 1);
|
||||
expect(page.getByRole('grid').getByRole('row')).toHaveCount(largeRowStatus.end + 2); // +2 for header and footer rows
|
||||
|
||||
// click a page over with the directional nav
|
||||
await page.getByLabel('next page').click();
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
test.use({
|
||||
viewport: { width: 1280, height: 1080 },
|
||||
featureToggles: {
|
||||
tableNextGen: true,
|
||||
},
|
||||
});
|
||||
|
||||
test.describe(
|
||||
'Panels test: Table - Markdown',
|
||||
{
|
||||
tag: ['@panels', '@table'],
|
||||
},
|
||||
() => {
|
||||
test('Tests Markdown tables are successfully rendered', async ({ gotoDashboardPage, page }) => {
|
||||
await gotoDashboardPage({
|
||||
uid: '2769f5d8-0094-4ac4-a4f0-f68f620339cc',
|
||||
queryParams: new URLSearchParams({ editPanel: '1' }),
|
||||
});
|
||||
|
||||
await expect(page.getByRole('grid')).toBeVisible();
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -708,6 +708,7 @@ export enum TableCellDisplayMode {
|
||||
Image = 'image',
|
||||
JSONView = 'json-view',
|
||||
LcdGauge = 'lcd-gauge',
|
||||
Markdown = 'markdown',
|
||||
Pill = 'pill',
|
||||
Sparkline = 'sparkline',
|
||||
}
|
||||
@@ -836,6 +837,11 @@ export interface TablePillCellOptions extends TableWrapTextOptions {
|
||||
type: TableCellDisplayMode.Pill;
|
||||
}
|
||||
|
||||
export interface TableMarkdownCellOptions {
|
||||
dynamicHeight?: boolean;
|
||||
type: TableCellDisplayMode.Markdown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Height of a table cell
|
||||
*/
|
||||
@@ -850,7 +856,7 @@ 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 | TablePillCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions);
|
||||
export type TableCellOptions = (TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TablePillCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions | TableMarkdownCellOptions);
|
||||
|
||||
/**
|
||||
* Use UTC/GMT timezone
|
||||
|
||||
@@ -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" | "pill" @cuetsy(kind="enum",memberNames="Auto|ColorText|ColorBackground|ColorBackgroundSolid|GradientGauge|LcdGauge|JSONView|BasicGauge|Image|Gauge|Sparkline|DataLinks|Custom|Actions|Pill")
|
||||
TableCellDisplayMode: "auto" | "color-text" | "color-background" | "color-background-solid" | "gradient-gauge" | "lcd-gauge" | "json-view" | "basic" | "image" | "gauge" | "sparkline" | "data-links" | "custom" | "actions" | "pill" | "markdown" @cuetsy(kind="enum",memberNames="Auto|ColorText|ColorBackground|ColorBackgroundSolid|GradientGauge|LcdGauge|JSONView|BasicGauge|Image|Gauge|Sparkline|DataLinks|Custom|Actions|Pill|Markdown")
|
||||
|
||||
// Display mode to the "Colored Background" display
|
||||
// mode for table cells. Either displays a solid color (basic mode)
|
||||
@@ -91,12 +91,17 @@ TablePillCellOptions: {
|
||||
type: TableCellDisplayMode & "pill"
|
||||
} & TableWrapTextOptions @cuetsy(kind="interface")
|
||||
|
||||
TableMarkdownCellOptions: {
|
||||
type: TableCellDisplayMode & "markdown"
|
||||
dynamicHeight?: bool
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
// Height of a table cell
|
||||
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 | TablePillCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions @cuetsy(kind="type")
|
||||
TableCellOptions: TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TablePillCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions | TableMarkdownCellOptions @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,7 +109,7 @@
|
||||
"react-calendar": "^6.0.0",
|
||||
"react-colorful": "5.6.1",
|
||||
"react-custom-scrollbars-2": "4.5.0",
|
||||
"react-data-grid": "grafana/react-data-grid#de920f0105cb2b7d774444e7443a675f3b568ad6",
|
||||
"react-data-grid": "grafana/react-data-grid#a922856b5ede21d55db3fdffb6d38dc76bdc7c58",
|
||||
"react-dropzone": "14.3.8",
|
||||
"react-highlight-words": "0.21.0",
|
||||
"react-hook-form": "^7.49.2",
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { renderMarkdown } from '@grafana/data';
|
||||
|
||||
import { MaybeWrapWithLink } from '../MaybeWrapWithLink';
|
||||
import { MarkdownCellProps } from '../types';
|
||||
|
||||
export function MarkdownCell({ field, rowIdx, disableSanitizeHtml }: MarkdownCellProps) {
|
||||
return (
|
||||
<MaybeWrapWithLink field={field} rowIdx={rowIdx}>
|
||||
<div
|
||||
className="markdown-container"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: renderMarkdown(field.values[rowIdx], { noSanitize: disableSanitizeHtml }).trim(),
|
||||
}}
|
||||
/>
|
||||
</MaybeWrapWithLink>
|
||||
);
|
||||
}
|
||||
@@ -7,8 +7,8 @@ import { useStyles2 } from '../../../../themes/ThemeContext';
|
||||
import { Icon } from '../../../Icon/Icon';
|
||||
import { RowExpanderNGProps } from '../types';
|
||||
|
||||
export function RowExpander({ height, onCellExpand, isExpanded }: RowExpanderNGProps) {
|
||||
const styles = useStyles2(getStyles, height);
|
||||
export function RowExpander({ onCellExpand, isExpanded }: RowExpanderNGProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
function handleKeyDown(e: React.KeyboardEvent<HTMLSpanElement>) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
@@ -30,12 +30,11 @@ export function RowExpander({ height, onCellExpand, isExpanded }: RowExpanderNGP
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2, rowHeight: number) => ({
|
||||
const getStyles = (_theme: GrafanaTheme2) => ({
|
||||
expanderCell: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
height: `${rowHeight}px`,
|
||||
cursor: 'pointer',
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { BarGaugeCell } from './BarGaugeCell';
|
||||
import { DataLinksCell } from './DataLinksCell';
|
||||
import { GeoCell } from './GeoCell';
|
||||
import { ImageCell } from './ImageCell';
|
||||
import { MarkdownCell } from './MarkdownCell';
|
||||
import { PillCell } from './PillCell';
|
||||
import { SparklineCell } from './SparklineCell';
|
||||
|
||||
@@ -78,6 +79,10 @@ const CUSTOM_RENDERER: TableCellRenderer = (props) => {
|
||||
return <CustomCellComponent field={props.field} rowIndex={props.rowIdx} frame={props.frame} value={props.value} />;
|
||||
};
|
||||
|
||||
const MARKDOWN_RENDERER: TableCellRenderer = (props) => (
|
||||
<MarkdownCell field={props.field} rowIdx={props.rowIdx} disableSanitizeHtml={props.disableSanitizeHtml} />
|
||||
);
|
||||
|
||||
const CELL_RENDERERS: Record<TableCellOptions['type'], TableCellRenderer> = {
|
||||
[TableCellDisplayMode.Sparkline]: SPARKLINE_RENDERER,
|
||||
[TableCellDisplayMode.Gauge]: GAUGE_RENDERER,
|
||||
@@ -89,9 +94,16 @@ const CELL_RENDERERS: Record<TableCellOptions['type'], TableCellRenderer> = {
|
||||
[TableCellDisplayMode.ColorText]: AUTO_RENDERER,
|
||||
[TableCellDisplayMode.ColorBackground]: AUTO_RENDERER,
|
||||
[TableCellDisplayMode.Auto]: AUTO_RENDERER,
|
||||
[TableCellDisplayMode.Markdown]: MARKDOWN_RENDERER,
|
||||
[TableCellDisplayMode.Pill]: PILL_RENDERER,
|
||||
};
|
||||
|
||||
// TODO: come up with a more elegant way to handle this.
|
||||
const STRING_ONLY_RENDERERS = new Set<TableCellOptions['type']>([
|
||||
TableCellDisplayMode.Markdown,
|
||||
TableCellDisplayMode.Pill,
|
||||
]);
|
||||
|
||||
/** @internal */
|
||||
export function getCellRenderer(field: Field, cellOptions: TableCellOptions): TableCellRenderer {
|
||||
const cellType = cellOptions?.type ?? TableCellDisplayMode.Auto;
|
||||
@@ -99,8 +111,7 @@ export function getCellRenderer(field: Field, cellOptions: TableCellOptions): Ta
|
||||
return getAutoRendererResult(field);
|
||||
}
|
||||
|
||||
// TODO: add support boolean, enum, (maybe int). but for now just string fields
|
||||
if (cellType === TableCellDisplayMode.Pill && field.type !== FieldType.string) {
|
||||
if (STRING_ONLY_RENDERERS.has(cellType) && field.type !== FieldType.string) {
|
||||
return AUTO_RENDERER;
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ export function TableNG(props: TableNGProps) {
|
||||
const {
|
||||
cellHeight,
|
||||
data,
|
||||
disableSanitizeHtml,
|
||||
enablePagination = false,
|
||||
enableSharedCrosshair = false,
|
||||
enableVirtualization,
|
||||
@@ -160,12 +161,15 @@ export function TableNG(props: TableNGProps) {
|
||||
setSortColumns,
|
||||
} = useSortedRows(filteredRows, data.fields, { hasNestedFrames, initialSortBy });
|
||||
|
||||
const defaultRowHeight = getDefaultRowHeight(theme, cellHeight);
|
||||
const [isInspecting, setIsInspecting] = useState(false);
|
||||
const [expandedRows, setExpandedRows] = useState(() => new Set<number>());
|
||||
|
||||
// vt scrollbar accounting for column auto-sizing
|
||||
const visibleFields = useMemo(() => getVisibleFields(data.fields), [data.fields]);
|
||||
const defaultRowHeight = useMemo(
|
||||
() => getDefaultRowHeight(theme, visibleFields, cellHeight),
|
||||
[theme, visibleFields, cellHeight]
|
||||
);
|
||||
const gridRef = useRef<DataGridHandle>(null);
|
||||
const scrollbarWidth = useScrollbarWidth(gridRef, height, sortedRows);
|
||||
const availableWidth = useMemo(
|
||||
@@ -212,7 +216,7 @@ export function TableNG(props: TableNGProps) {
|
||||
width: availableWidth,
|
||||
height,
|
||||
headerHeight,
|
||||
footerHeight: hasFooter ? defaultRowHeight : 0,
|
||||
footerHeight: hasFooter ? (typeof defaultRowHeight === 'number' ? defaultRowHeight : TABLE.MAX_CELL_HEIGHT) : 0,
|
||||
rowHeight,
|
||||
});
|
||||
|
||||
@@ -224,6 +228,17 @@ export function TableNG(props: TableNGProps) {
|
||||
});
|
||||
const applyToRowBgFn = useMemo(() => getApplyToRowBgFn(data.fields, theme) ?? undefined, [data.fields, theme]);
|
||||
|
||||
// normalize the row height into a function which returns a number, so we avoid a bunch of conditionals during rendering.
|
||||
const rowHeightFn = useMemo((): ((row: TableRow) => number) => {
|
||||
if (typeof rowHeight === 'function') {
|
||||
return rowHeight;
|
||||
}
|
||||
if (typeof rowHeight === 'string') {
|
||||
return () => TABLE.MAX_CELL_HEIGHT;
|
||||
}
|
||||
return () => rowHeight;
|
||||
}, [rowHeight]);
|
||||
|
||||
const renderRow = useMemo(
|
||||
() => renderRowFactory(data.fields, panelContext, expandedRows, enableSharedCrosshair),
|
||||
[data, enableSharedCrosshair, expandedRows, panelContext]
|
||||
@@ -232,7 +247,7 @@ export function TableNG(props: TableNGProps) {
|
||||
const commonDataGridProps = useMemo(
|
||||
() =>
|
||||
({
|
||||
enableVirtualization,
|
||||
enableVirtualization: enableVirtualization !== false && rowHeight !== 'auto',
|
||||
defaultColumnOptions: {
|
||||
minWidth: 50,
|
||||
resizable: true,
|
||||
@@ -348,8 +363,8 @@ export function TableNG(props: TableNGProps) {
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const shouldOverflow = shouldTextOverflow(field);
|
||||
const shouldWrap = shouldTextWrap(field);
|
||||
const shouldOverflow = rowHeight !== 'auto' && shouldTextOverflow(field);
|
||||
const shouldWrap = rowHeight === 'auto' || shouldTextWrap(field);
|
||||
const withTooltip = withDataLinksActionsTooltip(field, cellType);
|
||||
const canBeColorized =
|
||||
cellType === TableCellDisplayMode.ColorBackground || cellType === TableCellDisplayMode.ColorText;
|
||||
@@ -368,6 +383,7 @@ export function TableNG(props: TableNGProps) {
|
||||
case TableCellDisplayMode.DataLinks:
|
||||
case TableCellDisplayMode.JSONView:
|
||||
case TableCellDisplayMode.Pill:
|
||||
case TableCellDisplayMode.Markdown:
|
||||
cellClass = getCellStyles(
|
||||
theme,
|
||||
cellType,
|
||||
@@ -428,7 +444,9 @@ export function TableNG(props: TableNGProps) {
|
||||
const value = props.row[props.column.key];
|
||||
// TODO: it would be nice to get rid of passing height down as a prop. but this value
|
||||
// is cached so the cost of calling for every cell is low.
|
||||
const height = typeof rowHeight === 'function' ? rowHeight(props.row) : rowHeight;
|
||||
// NOTE: some cell types still require a height to be passed down, so that's why string-based
|
||||
// cell types are going to just pass down the max cell height as a numeric height for those cells.
|
||||
const height = rowHeightFn(props.row);
|
||||
const frame = data;
|
||||
|
||||
return (
|
||||
@@ -446,6 +464,7 @@ export function TableNG(props: TableNGProps) {
|
||||
cellInspect,
|
||||
showFilters,
|
||||
getActions: getCellActions,
|
||||
disableSanitizeHtml,
|
||||
})}
|
||||
{showActions && (
|
||||
<TableCellActions
|
||||
@@ -555,7 +574,6 @@ export function TableNG(props: TableNGProps) {
|
||||
|
||||
return (
|
||||
<RowExpander
|
||||
height={defaultRowHeight}
|
||||
isExpanded={expandedRows.has(rowIdx)}
|
||||
onCellExpand={() => {
|
||||
if (expandedRows.has(rowIdx)) {
|
||||
@@ -608,7 +626,7 @@ export function TableNG(props: TableNGProps) {
|
||||
crossFilterOrder,
|
||||
crossFilterRows,
|
||||
data,
|
||||
defaultRowHeight,
|
||||
disableSanitizeHtml,
|
||||
enableSharedCrosshair,
|
||||
expandedRows,
|
||||
filter,
|
||||
@@ -618,6 +636,7 @@ export function TableNG(props: TableNGProps) {
|
||||
onCellFilterAdded,
|
||||
panelContext,
|
||||
rowHeight,
|
||||
rowHeightFn,
|
||||
rows,
|
||||
setFilter,
|
||||
showTypeIcons,
|
||||
@@ -947,16 +966,26 @@ const getCellStyles = (
|
||||
shouldOverflow: boolean,
|
||||
isColorized: boolean,
|
||||
isMonospace: boolean
|
||||
) =>
|
||||
css({
|
||||
) => {
|
||||
const whiteSpace: CSSProperties['whiteSpace'] = (() => {
|
||||
if (isMonospace) {
|
||||
return 'pre';
|
||||
}
|
||||
if (cellType === TableCellDisplayMode.Markdown) {
|
||||
return 'normal';
|
||||
}
|
||||
return 'pre-line';
|
||||
})();
|
||||
|
||||
return css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
textAlign,
|
||||
justifyContent: getJustifyContent(textAlign),
|
||||
minHeight: '100%',
|
||||
backgroundClip: 'padding-box !important', // helps when cells have a bg color
|
||||
|
||||
...(shouldWrap && { whiteSpace: isMonospace ? 'pre' : 'pre-line' }),
|
||||
...(isColorized && { backgroundClip: 'padding-box !important' }),
|
||||
...(shouldOverflow && { minHeight: '100%' }),
|
||||
...(shouldWrap && { whiteSpace }),
|
||||
...(isMonospace && { fontFamily: 'monospace' }),
|
||||
|
||||
'&:hover, &[aria-selected=true]': {
|
||||
@@ -965,7 +994,7 @@ const getCellStyles = (
|
||||
},
|
||||
...(shouldOverflow && {
|
||||
zIndex: theme.zIndex.tooltip - 2,
|
||||
whiteSpace: isMonospace ? 'pre' : 'pre-line',
|
||||
whiteSpace,
|
||||
height: 'fit-content',
|
||||
minWidth: 'fit-content',
|
||||
...(cellType === TableCellDisplayMode.Pill && {
|
||||
@@ -1025,4 +1054,22 @@ const getCellStyles = (
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
}),
|
||||
|
||||
...(cellType === TableCellDisplayMode.Markdown && {
|
||||
'& ol, & ul': {
|
||||
paddingLeft: theme.spacing(1.5),
|
||||
},
|
||||
'& p': {
|
||||
whiteSpace: 'pre-line',
|
||||
},
|
||||
'& a': {
|
||||
color: theme.colors.primary.text,
|
||||
},
|
||||
// for elements like `p`, `h*`, etc. which have an inherent margin,
|
||||
// we want to remove the bottom margin for the last one in the container.
|
||||
'& > .markdown-container > *:last-child': {
|
||||
marginBottom: 0,
|
||||
},
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useMemo, useEffect, useCallback, useRef, useLayoutEffect, RefObject } from 'react';
|
||||
import { useState, useMemo, useCallback, useRef, useLayoutEffect, RefObject, CSSProperties } from 'react';
|
||||
import { Column, DataGridHandle, DataGridProps, SortColumn } from 'react-data-grid';
|
||||
|
||||
import { Field, fieldReducers, FieldType, formattedValueToString, reduceField } from '@grafana/data';
|
||||
@@ -135,7 +135,7 @@ export function useSortedRows(
|
||||
export interface PaginatedRowsOptions {
|
||||
height: number;
|
||||
width: number;
|
||||
rowHeight: number | ((row: TableRow) => number);
|
||||
rowHeight: NonNullable<CSSProperties['height']> | ((row: TableRow) => number);
|
||||
headerHeight: number;
|
||||
footerHeight: number;
|
||||
paginationHeight?: number;
|
||||
@@ -174,6 +174,12 @@ export function usePaginatedRows(
|
||||
return rowHeight;
|
||||
}
|
||||
|
||||
// when using auto-sized rows, we're just going to have to pick a number. the alternative
|
||||
// is to measure each row, which we could do but would be expensive.
|
||||
if (typeof rowHeight === 'string') {
|
||||
return TABLE.MAX_CELL_HEIGHT;
|
||||
}
|
||||
|
||||
// we'll just measure 100 rows to estimate
|
||||
return rows.slice(0, 100).reduce((avg, row, _, { length }) => avg + rowHeight(row) / length, 0);
|
||||
}, [rows, rowHeight, enabled]);
|
||||
@@ -214,7 +220,7 @@ export function usePaginatedRows(
|
||||
}, [width, height, headerHeight, footerHeight, avgRowHeight, enabled, numRows, page]);
|
||||
|
||||
// safeguard against page overflow on panel resize or other factors
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
@@ -381,7 +387,7 @@ interface UseRowHeightOptions {
|
||||
columnWidths: number[];
|
||||
fields: Field[];
|
||||
hasNestedFrames: boolean;
|
||||
defaultHeight: number;
|
||||
defaultHeight: NonNullable<CSSProperties['height']>;
|
||||
expandedRows: Set<number>;
|
||||
typographyCtx: TypographyCtx;
|
||||
}
|
||||
@@ -393,7 +399,7 @@ export function useRowHeight({
|
||||
defaultHeight,
|
||||
expandedRows,
|
||||
typographyCtx,
|
||||
}: UseRowHeightOptions): number | ((row: TableRow) => number) {
|
||||
}: UseRowHeightOptions): NonNullable<CSSProperties['height']> | ((row: TableRow) => number) {
|
||||
const lineCounters = useMemo(() => buildRowLineCounters(fields, typographyCtx), [fields, typographyCtx]);
|
||||
const hasWrappedCols = useMemo(() => lineCounters?.length ?? 0 > 0, [lineCounters]);
|
||||
|
||||
@@ -404,7 +410,7 @@ export function useRowHeight({
|
||||
|
||||
const rowHeight = useMemo(() => {
|
||||
// row height is only complicated when there are nested frames or wrapped columns.
|
||||
if (!hasNestedFrames && !hasWrappedCols) {
|
||||
if ((!hasNestedFrames && !hasWrappedCols) || typeof defaultHeight === 'string') {
|
||||
return defaultHeight;
|
||||
}
|
||||
|
||||
|
||||
@@ -141,6 +141,8 @@ export interface BaseTableProps {
|
||||
getActions?: GetActionsFunction;
|
||||
// Used solely for testing as RTL can't correctly render the table otherwise
|
||||
enableVirtualization?: boolean;
|
||||
// for MarkdownCell, this flag disables sanitization of HTML content. Configured via config.ini.
|
||||
disableSanitizeHtml?: boolean;
|
||||
}
|
||||
|
||||
/* ---------------------------- Table cell props ---------------------------- */
|
||||
@@ -161,6 +163,7 @@ export interface TableCellRendererProps {
|
||||
showFilters: boolean;
|
||||
justifyContent: Property.JustifyContent;
|
||||
getActions?: GetActionsFunctionLocal;
|
||||
disableSanitizeHtml?: boolean;
|
||||
}
|
||||
|
||||
export type ContextMenuProps = {
|
||||
@@ -186,7 +189,6 @@ export interface TableCellActionsProps {
|
||||
|
||||
/* ------------------------- Specialized Cell Props ------------------------- */
|
||||
export interface RowExpanderNGProps {
|
||||
height: number;
|
||||
onCellExpand: (e: SyntheticEvent) => void;
|
||||
isExpanded?: boolean;
|
||||
}
|
||||
@@ -242,6 +244,12 @@ export interface AutoCellProps {
|
||||
rowIdx: number;
|
||||
}
|
||||
|
||||
export interface MarkdownCellProps {
|
||||
field: Field;
|
||||
rowIdx: number;
|
||||
disableSanitizeHtml?: boolean;
|
||||
}
|
||||
|
||||
export interface ActionCellProps {
|
||||
field: Field;
|
||||
rowIdx: number;
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
getCellLinks,
|
||||
getCellOptions,
|
||||
getComparator,
|
||||
getDefaultRowHeight,
|
||||
getIsNestedTable,
|
||||
getAlignment,
|
||||
getJustifyContent,
|
||||
@@ -44,6 +43,7 @@ import {
|
||||
wrapUwrapCount,
|
||||
getDataLinksCounter,
|
||||
getPillLineCounter,
|
||||
getDefaultRowHeight,
|
||||
} from './utils';
|
||||
|
||||
describe('TableNG utils', () => {
|
||||
@@ -787,23 +787,46 @@ describe('TableNG utils', () => {
|
||||
describe('getDefaultRowHeight', () => {
|
||||
const theme = createTheme();
|
||||
|
||||
it('returns correct height for TableCellHeight.Sm', () => {
|
||||
const result = getDefaultRowHeight(theme, TableCellHeight.Sm);
|
||||
expect(result).toBe(36);
|
||||
it.each([
|
||||
{ input: TableCellHeight.Sm, expected: 36 },
|
||||
{ input: TableCellHeight.Md, expected: 42 },
|
||||
{ input: TableCellHeight.Lg, expected: TABLE.MAX_CELL_HEIGHT },
|
||||
])('returns "$expected" for "$input"', ({ input, expected }) => {
|
||||
const result = getDefaultRowHeight(theme, [], input);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it('returns correct height for TableCellHeight.Md', () => {
|
||||
const result = getDefaultRowHeight(theme, TableCellHeight.Md);
|
||||
expect(result).toBe(42);
|
||||
});
|
||||
|
||||
it('returns correct height for TableCellHeight.Lg', () => {
|
||||
const result = getDefaultRowHeight(theme, TableCellHeight.Lg);
|
||||
expect(result).toBe(TABLE.MAX_CELL_HEIGHT);
|
||||
it('returns "auto" if a field is present with the dynamicHeight cellOption is false', () => {
|
||||
expect(
|
||||
getDefaultRowHeight(
|
||||
theme,
|
||||
[
|
||||
{
|
||||
name: 'test1',
|
||||
type: FieldType.string,
|
||||
config: {},
|
||||
values: ['value1'],
|
||||
},
|
||||
{
|
||||
name: 'test2',
|
||||
type: FieldType.string,
|
||||
config: { custom: { cellOptions: { type: TableCellDisplayMode.Markdown, dynamicHeight: true } } },
|
||||
values: ['value1'],
|
||||
},
|
||||
{
|
||||
name: 'test3',
|
||||
type: FieldType.number,
|
||||
config: { custom: { cellOptions: { type: TableCellDisplayMode.JSONView } } },
|
||||
values: [3],
|
||||
},
|
||||
],
|
||||
TableCellHeight.Sm
|
||||
)
|
||||
).toBe('auto');
|
||||
});
|
||||
|
||||
it('calculates height based on theme when cellHeight is undefined', () => {
|
||||
const result = getDefaultRowHeight(theme, undefined as unknown as TableCellHeight);
|
||||
const result = getDefaultRowHeight(theme, []);
|
||||
|
||||
// Calculate the expected result based on the theme values
|
||||
const expected = TABLE.CELL_PADDING * 2 + theme.typography.fontSize * theme.typography.body.lineHeight;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Property } from 'csstype';
|
||||
import { CSSProperties } from 'react';
|
||||
import { SortColumn } from 'react-data-grid';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { Count, varPreLine } from 'uwrap';
|
||||
@@ -45,9 +46,14 @@ export type CellNumLinesCalculator = (text: string, cellWidth: number) => number
|
||||
* @internal
|
||||
* Returns the default row height based on the theme and cell height setting.
|
||||
*/
|
||||
export function getDefaultRowHeight(theme: GrafanaTheme2, cellHeight?: TableCellHeight): number {
|
||||
const bodyFontSize = theme.typography.fontSize;
|
||||
const lineHeight = theme.typography.body.lineHeight;
|
||||
export function getDefaultRowHeight(
|
||||
theme: GrafanaTheme2,
|
||||
fields?: Field[],
|
||||
cellHeight?: TableCellHeight
|
||||
): NonNullable<CSSProperties['height']> {
|
||||
if (fields?.some((field) => field.config?.custom?.cellOptions?.dynamicHeight)) {
|
||||
return 'auto';
|
||||
}
|
||||
|
||||
switch (cellHeight) {
|
||||
case TableCellHeight.Sm:
|
||||
@@ -58,7 +64,7 @@ export function getDefaultRowHeight(theme: GrafanaTheme2, cellHeight?: TableCell
|
||||
return TABLE.MAX_CELL_HEIGHT;
|
||||
}
|
||||
|
||||
return TABLE.CELL_PADDING * 2 + bodyFontSize * lineHeight;
|
||||
return TABLE.CELL_PADDING * 2 + theme.typography.fontSize * theme.typography.body.lineHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,11 +4,8 @@ import { StandardEditorProps } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Switch } from '@grafana/ui';
|
||||
|
||||
export function PaginationEditor({ onChange, value, context }: StandardEditorProps<boolean>) {
|
||||
export function PaginationEditor({ onChange, value }: StandardEditorProps<boolean>) {
|
||||
const changeValue = (event: React.FormEvent<HTMLInputElement> | undefined) => {
|
||||
if (event?.currentTarget.checked) {
|
||||
context.options.footer.show = false;
|
||||
}
|
||||
onChange(event?.currentTarget.checked);
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Combobox, ComboboxOption, Field, TableCellDisplayMode, useStyles2 } fro
|
||||
import { BarGaugeCellOptionsEditor } from './cells/BarGaugeCellOptionsEditor';
|
||||
import { ColorBackgroundCellOptionsEditor } from './cells/ColorBackgroundCellOptionsEditor';
|
||||
import { ImageCellOptionsEditor } from './cells/ImageCellOptionsEditor';
|
||||
import { MarkdownCellOptionsEditor } from './cells/MarkdownCellOptionsEditor';
|
||||
import { SparklineCellOptionsEditor } from './cells/SparklineCellOptionsEditor';
|
||||
import { TextWrapOptionsEditor } from './cells/TextWrapOptionsEditor';
|
||||
|
||||
@@ -54,6 +55,7 @@ export const TableCellOptionEditor = ({ value, onChange }: Props) => {
|
||||
{ value: TableCellDisplayMode.Sparkline, label: t('table.cell-types.sparkline', 'Sparkline') },
|
||||
{ value: TableCellDisplayMode.JSONView, label: t('table.cell-types.json', 'JSON View') },
|
||||
{ value: TableCellDisplayMode.Pill, label: t('table.cell-types.pill', 'Pill') },
|
||||
{ value: TableCellDisplayMode.Markdown, label: t('table.cell-types.markdown', 'Markdown + HTML') },
|
||||
{ value: TableCellDisplayMode.Image, label: t('table.cell-types.image', 'Image') },
|
||||
{ value: TableCellDisplayMode.Actions, label: t('table.cell-types.actions', 'Actions') },
|
||||
];
|
||||
@@ -105,6 +107,9 @@ export const TableCellOptionEditor = ({ value, onChange }: Props) => {
|
||||
{cellType === TableCellDisplayMode.Image && (
|
||||
<ImageCellOptionsEditor cellOptions={value} onChange={onCellOptionsChange} />
|
||||
)}
|
||||
{cellType === TableCellDisplayMode.Markdown && (
|
||||
<MarkdownCellOptionsEditor cellOptions={value} onChange={onCellOptionsChange} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ import { config, PanelDataErrorView } from '@grafana/runtime';
|
||||
import { Select, usePanelContext, useTheme2 } from '@grafana/ui';
|
||||
import { TableSortByFieldState } from '@grafana/ui/internal';
|
||||
import { TableNG } from '@grafana/ui/unstable';
|
||||
import { getConfig } from 'app/core/config';
|
||||
|
||||
import { getActions } from '../../../../features/actions/utils';
|
||||
|
||||
@@ -61,6 +62,8 @@ export function TablePanel(props: Props) {
|
||||
|
||||
const enableSharedCrosshair = panelContext.sync && panelContext.sync() !== DashboardCursorSync.Off;
|
||||
|
||||
const disableSanitizeHtml = getConfig().disableSanitizeHtml;
|
||||
|
||||
const tableElement = (
|
||||
<TableNG
|
||||
height={tableHeight}
|
||||
@@ -82,6 +85,7 @@ export function TablePanel(props: Props) {
|
||||
getActions={_getActions}
|
||||
structureRev={data.structureRev}
|
||||
transparent={transparent}
|
||||
disableSanitizeHtml={disableSanitizeHtml}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { FormEvent } from 'react';
|
||||
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { TableMarkdownCellOptions } from '@grafana/schema';
|
||||
import { Badge, Field, Label, Switch } from '@grafana/ui';
|
||||
|
||||
import { TableCellEditorProps } from '../TableCellOptionEditor';
|
||||
|
||||
export const MarkdownCellOptionsEditor = ({
|
||||
cellOptions,
|
||||
onChange,
|
||||
}: TableCellEditorProps<TableMarkdownCellOptions>) => {
|
||||
const onDynamicHeightChange = (e: FormEvent<HTMLInputElement>) => {
|
||||
cellOptions.dynamicHeight = e.currentTarget.checked;
|
||||
onChange(cellOptions);
|
||||
};
|
||||
|
||||
return (
|
||||
<Field
|
||||
label={
|
||||
<Label
|
||||
description={t(
|
||||
'table.markdown-cell-options-editor.description-dynamic-height',
|
||||
'We recommend enabling pagination with this option to avoid performance issues.'
|
||||
)}
|
||||
>
|
||||
<Trans i18nKey="table.markdown-cell-options-editor.label-dynamic-height">Dynamic height</Trans>{' '}
|
||||
<Badge
|
||||
text={t('table.markdown-cell-options-editor.label.text-alpha', 'Alpha')}
|
||||
color="blue"
|
||||
style={{ fontSize: '11px', marginLeft: '5px', lineHeight: '1.2' }}
|
||||
/>
|
||||
</Label>
|
||||
}
|
||||
>
|
||||
<Switch onChange={onDynamicHeightChange} value={cellOptions.dynamicHeight} />
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
@@ -12529,6 +12529,7 @@
|
||||
"gauge": "Gauge",
|
||||
"image": "Image",
|
||||
"json": "JSON View",
|
||||
"markdown": "Markdown + HTML",
|
||||
"pill": "Pill",
|
||||
"sparkline": "Sparkline"
|
||||
},
|
||||
@@ -12566,6 +12567,13 @@
|
||||
"label-alt-text": "Alt text",
|
||||
"label-title-text": "Title text"
|
||||
},
|
||||
"markdown-cell-options-editor": {
|
||||
"description-dynamic-height": "We recommend enabling pagination with this option to avoid performance issues.",
|
||||
"label": {
|
||||
"text-alpha": "Alpha"
|
||||
},
|
||||
"label-dynamic-height": "Dynamic height"
|
||||
},
|
||||
"name-calculation": "Calculation",
|
||||
"name-cell-height": "Cell height",
|
||||
"name-cell-type": "Cell type",
|
||||
|
||||
@@ -3829,7 +3829,7 @@ __metadata:
|
||||
react-calendar: "npm:^6.0.0"
|
||||
react-colorful: "npm:5.6.1"
|
||||
react-custom-scrollbars-2: "npm:4.5.0"
|
||||
react-data-grid: "grafana/react-data-grid#de920f0105cb2b7d774444e7443a675f3b568ad6"
|
||||
react-data-grid: "grafana/react-data-grid#a922856b5ede21d55db3fdffb6d38dc76bdc7c58"
|
||||
react-dom: "npm:18.3.1"
|
||||
react-dropzone: "npm:14.3.8"
|
||||
react-highlight-words: "npm:0.21.0"
|
||||
@@ -26823,15 +26823,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-data-grid@grafana/react-data-grid#de920f0105cb2b7d774444e7443a675f3b568ad6":
|
||||
"react-data-grid@grafana/react-data-grid#a922856b5ede21d55db3fdffb6d38dc76bdc7c58":
|
||||
version: 7.0.0-beta.56
|
||||
resolution: "react-data-grid@https://github.com/grafana/react-data-grid.git#commit=de920f0105cb2b7d774444e7443a675f3b568ad6"
|
||||
resolution: "react-data-grid@https://github.com/grafana/react-data-grid.git#commit=a922856b5ede21d55db3fdffb6d38dc76bdc7c58"
|
||||
dependencies:
|
||||
clsx: "npm:^2.0.0"
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0
|
||||
react-dom: ^18.0 || ^19.0
|
||||
checksum: 10/efc1dcb764fa5f3549d012737e79d423b34e6ad7b0a122849d757f59b45e648afe7c5cfeb1a61464245aa7b393f9251ff91fd49f8f857139d7df185c23d68b68
|
||||
checksum: 10/18d368ff52151c1e7900819b0d92bb554b3d26135a5d4401a6e3c044fd62ca72a4177b63ba705b93901fa489458a965ce1d1deb2e4f98aa5fb39c276d2b12d38
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user