diff --git a/.betterer.results b/.betterer.results index cf0dab6b1da..33cd396f9b9 100644 --- a/.betterer.results +++ b/.betterer.results @@ -649,25 +649,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], - "packages/grafana-ui/src/components/Table/Filter.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/grafana-ui/src/components/Table/FilterPopup.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/grafana-ui/src/components/Table/FooterRow.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] - ], - "packages/grafana-ui/src/components/Table/HeaderRow.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/grafana-ui/src/components/Table/Table.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] - ], - "packages/grafana-ui/src/components/Table/TableCell.tsx:5381": [ + "packages/grafana-ui/src/components/Table/Cells/TableCell.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], [0, 0, 0, "Do not use any type assertions.", "2"], @@ -676,6 +658,80 @@ exports[`better eslint`] = { "packages/grafana-ui/src/components/Table/TableCellInspector.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], + "packages/grafana-ui/src/components/Table/TableNG/Cells/HeaderCell.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"] + ], + "packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellNG.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] + ], + "packages/grafana-ui/src/components/Table/TableNG/Filter/Filter.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] + ], + "packages/grafana-ui/src/components/Table/TableNG/Filter/FilterList.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] + ], + "packages/grafana-ui/src/components/Table/TableNG/Filter/FilterPopup.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"] + ], + "packages/grafana-ui/src/components/Table/TableNG/Filter/utils.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], + "packages/grafana-ui/src/components/Table/TableNG/TableNG.test.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"], + [0, 0, 0, "Unexpected any. Specify a different type.", "4"], + [0, 0, 0, "Unexpected any. Specify a different type.", "5"], + [0, 0, 0, "Unexpected any. Specify a different type.", "6"], + [0, 0, 0, "Unexpected any. Specify a different type.", "7"], + [0, 0, 0, "Unexpected any. Specify a different type.", "8"] + ], + "packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"] + ], + "packages/grafana-ui/src/components/Table/TableNG/utils.test.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"], + [0, 0, 0, "Unexpected any. Specify a different type.", "4"], + [0, 0, 0, "Unexpected any. Specify a different type.", "5"], + [0, 0, 0, "Unexpected any. Specify a different type.", "6"], + [0, 0, 0, "Unexpected any. Specify a different type.", "7"], + [0, 0, 0, "Unexpected any. Specify a different type.", "8"] + ], + "packages/grafana-ui/src/components/Table/TableNG/utils.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"] + ], + "packages/grafana-ui/src/components/Table/TableRT/Filter.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], + "packages/grafana-ui/src/components/Table/TableRT/FilterPopup.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], + "packages/grafana-ui/src/components/Table/TableRT/FooterRow.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"] + ], + "packages/grafana-ui/src/components/Table/TableRT/HeaderRow.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], + "packages/grafana-ui/src/components/Table/TableRT/Table.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] + ], "packages/grafana-ui/src/components/Table/reducer.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] @@ -5070,8 +5126,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/search/page/components/SearchResultsTable.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], "public/app/features/search/page/components/columns.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], diff --git a/devenv/dev-dashboards/panel-table/table_tests_new.json b/devenv/dev-dashboards/panel-table/table_tests_new.json index ba7632bc5f5..c87e2b799e6 100644 --- a/devenv/dev-dashboards/panel-table/table_tests_new.json +++ b/devenv/dev-dashboards/panel-table/table_tests_new.json @@ -18,14 +18,11 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, + "id": 89, "links": [], - "liveNow": false, "panels": [ { - "datasource": { - "type": "datasource", - "uid": "grafana" - }, + "collapsed": false, "gridPos": { "h": 1, "w": 24, @@ -33,15 +30,7 @@ "y": 0 }, "id": 7, - "targets": [ - { - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "refId": "A" - } - ], + "panels": [], "title": "Cell styles", "type": "row" }, @@ -164,7 +153,7 @@ } ] }, - "pluginVersion": "9.5.0-pre", + "pluginVersion": "11.6.0-pre", "targets": [ { "datasource": { @@ -286,6 +275,7 @@ }, "id": 2, "options": { + "cellHeight": "sm", "footer": { "countRows": false, "fields": "", @@ -303,7 +293,7 @@ } ] }, - "pluginVersion": "9.5.0-pre", + "pluginVersion": "11.6.0-pre", "targets": [ { "datasource": { @@ -376,7 +366,7 @@ { "matcher": { "id": "byName", - "options": "rate" + "options": "Trend #A" }, "properties": [ { @@ -448,7 +438,7 @@ "showRowNums": false, "sortBy": [] }, - "pluginVersion": "9.5.0-pre", + "pluginVersion": "11.6.0-pre", "targets": [ { "datasource": { @@ -625,7 +615,7 @@ "showHeader": true, "showRowNums": false }, - "pluginVersion": "9.5.0-pre", + "pluginVersion": "11.6.0-pre", "targets": [ { "datasource": { @@ -683,10 +673,7 @@ "type": "table" }, { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, + "collapsed": false, "gridPos": { "h": 1, "w": 24, @@ -694,15 +681,7 @@ "y": 24 }, "id": 9, - "targets": [ - { - "datasource": { - "type": "testdata", - "uid": "gdev-testdata" - }, - "refId": "A" - } - ], + "panels": [], "title": "Data links", "type": "row" }, @@ -752,18 +731,19 @@ { "matcher": { "id": "byName", - "options": "Time" + "options": "time" }, "properties": [ { - "id": "custom.align" + "id": "custom.align", + "value": "center" } ] }, { "matcher": { "id": "byName", - "options": "{name=\"S1\", server=\"A\"}" + "options": "S1 A" }, "properties": [ { @@ -799,7 +779,7 @@ "showHeader": true, "showRowNums": false }, - "pluginVersion": "9.5.0-pre", + "pluginVersion": "11.6.0-pre", "targets": [ { "alias": "S1", @@ -916,7 +896,7 @@ } ] }, - "pluginVersion": "9.5.0-pre", + "pluginVersion": "11.6.0-pre", "targets": [ { "datasource": { @@ -1005,7 +985,7 @@ "showHeader": true, "showRowNums": false }, - "pluginVersion": "9.5.0-pre", + "pluginVersion": "11.6.0-pre", "targets": [ { "datasource": { @@ -1019,9 +999,9 @@ "type": "table" } ], + "preload": false, "refresh": "", - "revision": 1, - "schemaVersion": 38, + "schemaVersion": 41, "tags": [ "gdev", "panel-tests" @@ -1049,6 +1029,5 @@ "timezone": "", "title": "Panel Tests - React Table", "uid": "U_bZIMRMk", - "version": 7, - "weekStart": "" + "version": 17 } diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 84bc6c9d3b3..63ebab39687 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -193,6 +193,7 @@ Experimental features might be changed or removed without prior notice. | `alertingCentralAlertHistory` | Enables the new central alert history. | | `failWrongDSUID` | Throws an error if a datasource has an invalid UIDs | | `dataplaneAggregator` | Enable grafana dataplane aggregator | +| `tableNextGen` | Allows access to the new react-data-grid based table component. | | `lokiSendDashboardPanelNames` | Send dashboard and panel names to Loki when querying | | `alertingPrometheusRulesPrimary` | Uses Prometheus rules as the primary source of truth for ruler-enabled data sources | | `exploreLogsShardSplitting` | Used in Logs Drilldown to split queries into multiple queries based on the number of shards | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 46f4da08a67..9a6fd04b4cd 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -747,6 +747,10 @@ export interface FeatureToggles { */ newFiltersUI?: boolean; /** + * Allows access to the new react-data-grid based table component. + */ + tableNextGen?: boolean; + /** * Send dashboard and panel names to Loki when querying */ lokiSendDashboardPanelNames?: boolean; diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 55785d7de92..54d7a59ad97 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -107,6 +107,7 @@ "react-calendar": "^5.1.0", "react-colorful": "5.6.1", "react-custom-scrollbars-2": "4.5.0", + "react-data-grid": "7.0.0-beta.46", "react-dropzone": "14.3.5", "react-highlight-words": "0.21.0", "react-hook-form": "^7.49.2", diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx index 043578ff10a..d2579339535 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx @@ -527,7 +527,6 @@ export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles const barStyles: CSSProperties = { borderRadius: theme.shape.radius.default, position: 'relative', - zIndex: 1, }; const emptyBar: CSSProperties = { diff --git a/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx b/packages/grafana-ui/src/components/Table/Cells/BarGaugeCell.tsx similarity index 93% rename from packages/grafana-ui/src/components/Table/BarGaugeCell.tsx rename to packages/grafana-ui/src/components/Table/Cells/BarGaugeCell.tsx index a427451f0c8..3ab8121c8d2 100644 --- a/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx +++ b/packages/grafana-ui/src/components/Table/Cells/BarGaugeCell.tsx @@ -3,11 +3,10 @@ import { isFunction } from 'lodash'; import { ThresholdsConfig, ThresholdsMode, VizOrientation, getFieldConfigWithMinMax } from '@grafana/data'; import { BarGaugeDisplayMode, BarGaugeValueMode, TableCellDisplayMode } from '@grafana/schema'; -import { BarGauge } from '../BarGauge/BarGauge'; -import { DataLinksContextMenu, DataLinksContextMenuApi } from '../DataLinks/DataLinksContextMenu'; - -import { TableCellProps } from './types'; -import { getAlignmentFactor, getCellOptions } from './utils'; +import { BarGauge } from '../../BarGauge/BarGauge'; +import { DataLinksContextMenu, DataLinksContextMenuApi } from '../../DataLinks/DataLinksContextMenu'; +import { TableCellProps } from '../types'; +import { getAlignmentFactor, getCellOptions } from '../utils'; const defaultScale: ThresholdsConfig = { mode: ThresholdsMode.Absolute, diff --git a/packages/grafana-ui/src/components/Table/DataLinksCell.tsx b/packages/grafana-ui/src/components/Table/Cells/DataLinksCell.tsx similarity index 88% rename from packages/grafana-ui/src/components/Table/DataLinksCell.tsx rename to packages/grafana-ui/src/components/Table/Cells/DataLinksCell.tsx index 9a6383039e1..3e95d3946b4 100644 --- a/packages/grafana-ui/src/components/Table/DataLinksCell.tsx +++ b/packages/grafana-ui/src/components/Table/Cells/DataLinksCell.tsx @@ -1,6 +1,5 @@ -import { getCellLinks } from '../../utils'; - -import { TableCellProps } from './types'; +import { getCellLinks } from '../../../utils'; +import { TableCellProps } from '../types'; export const DataLinksCell = (props: TableCellProps) => { const { field, row, cellProps, tableStyles } = props; diff --git a/packages/grafana-ui/src/components/Table/DefaultCell.tsx b/packages/grafana-ui/src/components/Table/Cells/DefaultCell.tsx similarity index 89% rename from packages/grafana-ui/src/components/Table/DefaultCell.tsx rename to packages/grafana-ui/src/components/Table/Cells/DefaultCell.tsx index 2ce27572494..5b0cf8bcc5e 100644 --- a/packages/grafana-ui/src/components/Table/DefaultCell.tsx +++ b/packages/grafana-ui/src/components/Table/Cells/DefaultCell.tsx @@ -5,16 +5,15 @@ import * as React from 'react'; import { DisplayValue, formattedValueToString } from '@grafana/data'; import { TableCellDisplayMode } from '@grafana/schema'; -import { useStyles2 } from '../../themes'; -import { getCellLinks } from '../../utils'; -import { clearLinkButtonStyles } from '../Button'; -import { DataLinksContextMenu } from '../DataLinks/DataLinksContextMenu'; - -import { CellActions } from './CellActions'; -import { TableCellInspectorMode } from './TableCellInspector'; -import { TableStyles } from './styles'; -import { TableCellProps, CustomCellRendererProps, TableCellOptions } from './types'; -import { getCellColors, getCellOptions } from './utils'; +import { useStyles2 } from '../../../themes'; +import { getCellLinks } from '../../../utils'; +import { clearLinkButtonStyles } from '../../Button'; +import { DataLinksContextMenu } from '../../DataLinks/DataLinksContextMenu'; +import { CellActions } from '../CellActions'; +import { TableCellInspectorMode } from '../TableCellInspector'; +import { TableStyles } from '../TableRT/styles'; +import { TableCellProps, CustomCellRendererProps, TableCellOptions } from '../types'; +import { getCellColors, getCellOptions } from '../utils'; export const DefaultCell = (props: TableCellProps) => { const { field, cell, tableStyles, row, cellProps, frame, rowStyled, rowExpanded, textWrapped, height } = props; @@ -124,7 +123,7 @@ function getCellStyle( let bgHoverColor: string | undefined = undefined; // Get colors - const colors = getCellColors(tableStyles, cellOptions, displayValue); + const colors = getCellColors(tableStyles.theme, cellOptions, displayValue); textColor = colors.textColor; bgColor = colors.bgColor; bgHoverColor = colors.bgHoverColor; diff --git a/packages/grafana-ui/src/components/Table/FooterCell.tsx b/packages/grafana-ui/src/components/Table/Cells/FooterCell.tsx similarity index 96% rename from packages/grafana-ui/src/components/Table/FooterCell.tsx rename to packages/grafana-ui/src/components/Table/Cells/FooterCell.tsx index 72393d91140..7bec3fca642 100644 --- a/packages/grafana-ui/src/components/Table/FooterCell.tsx +++ b/packages/grafana-ui/src/components/Table/Cells/FooterCell.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { KeyValue } from '@grafana/data'; -import { FooterItem } from './types'; +import { FooterItem } from '../types'; export interface FooterProps { value: FooterItem; diff --git a/packages/grafana-ui/src/components/Table/GeoCell.tsx b/packages/grafana-ui/src/components/Table/Cells/GeoCell.tsx similarity index 93% rename from packages/grafana-ui/src/components/Table/GeoCell.tsx rename to packages/grafana-ui/src/components/Table/Cells/GeoCell.tsx index d5e156f8b70..16d37fc3f4e 100644 --- a/packages/grafana-ui/src/components/Table/GeoCell.tsx +++ b/packages/grafana-ui/src/components/Table/Cells/GeoCell.tsx @@ -1,7 +1,7 @@ import WKT from 'ol/format/WKT'; import { Geometry } from 'ol/geom'; -import { TableCellProps } from './types'; +import { TableCellProps } from '../types'; export function GeoCell(props: TableCellProps): JSX.Element { const { cell, tableStyles, cellProps } = props; diff --git a/packages/grafana-ui/src/components/Table/ImageCell.tsx b/packages/grafana-ui/src/components/Table/Cells/ImageCell.tsx similarity index 89% rename from packages/grafana-ui/src/components/Table/ImageCell.tsx rename to packages/grafana-ui/src/components/Table/Cells/ImageCell.tsx index 40f8b03dd29..12616308f24 100644 --- a/packages/grafana-ui/src/components/Table/ImageCell.tsx +++ b/packages/grafana-ui/src/components/Table/Cells/ImageCell.tsx @@ -1,10 +1,9 @@ import * as React from 'react'; -import { getCellLinks } from '../../utils'; -import { DataLinksContextMenu } from '../DataLinks/DataLinksContextMenu'; - -import { TableCellDisplayMode, TableCellProps } from './types'; -import { getCellOptions } from './utils'; +import { getCellLinks } from '../../../utils'; +import { DataLinksContextMenu } from '../../DataLinks/DataLinksContextMenu'; +import { TableCellDisplayMode, TableCellProps } from '../types'; +import { getCellOptions } from '../utils'; const DATALINKS_HEIGHT_OFFSET = 10; diff --git a/packages/grafana-ui/src/components/Table/JSONViewCell.tsx b/packages/grafana-ui/src/components/Table/Cells/JSONViewCell.tsx similarity index 80% rename from packages/grafana-ui/src/components/Table/JSONViewCell.tsx rename to packages/grafana-ui/src/components/Table/Cells/JSONViewCell.tsx index e5f9e5916a8..de62a86ddd9 100644 --- a/packages/grafana-ui/src/components/Table/JSONViewCell.tsx +++ b/packages/grafana-ui/src/components/Table/Cells/JSONViewCell.tsx @@ -1,14 +1,13 @@ import { css, cx } from '@emotion/css'; import { isString } from 'lodash'; -import { useStyles2 } from '../../themes'; -import { getCellLinks } from '../../utils'; -import { Button, clearLinkButtonStyles } from '../Button'; -import { DataLinksContextMenu } from '../DataLinks/DataLinksContextMenu'; - -import { CellActions } from './CellActions'; -import { TableCellInspectorMode } from './TableCellInspector'; -import { TableCellProps } from './types'; +import { useStyles2 } from '../../../themes'; +import { getCellLinks } from '../../../utils'; +import { Button, clearLinkButtonStyles } from '../../Button'; +import { DataLinksContextMenu } from '../../DataLinks/DataLinksContextMenu'; +import { CellActions } from '../CellActions'; +import { TableCellInspectorMode } from '../TableCellInspector'; +import { TableCellProps } from '../types'; export function JSONViewCell(props: TableCellProps): JSX.Element { const { cell, tableStyles, cellProps, field, row } = props; diff --git a/packages/grafana-ui/src/components/Table/SparklineCell.tsx b/packages/grafana-ui/src/components/Table/Cells/SparklineCell.tsx similarity index 92% rename from packages/grafana-ui/src/components/Table/SparklineCell.tsx rename to packages/grafana-ui/src/components/Table/Cells/SparklineCell.tsx index 0d6c26c4a1d..8df085d542a 100644 --- a/packages/grafana-ui/src/components/Table/SparklineCell.tsx +++ b/packages/grafana-ui/src/components/Table/Cells/SparklineCell.tsx @@ -21,13 +21,12 @@ import { VisibilityMode, } from '@grafana/schema'; -import { useTheme2 } from '../../themes'; -import { measureText } from '../../utils'; -import { FormattedValueDisplay } from '../FormattedValueDisplay/FormattedValueDisplay'; -import { Sparkline } from '../Sparkline/Sparkline'; - -import { TableCellProps } from './types'; -import { getAlignmentFactor, getCellOptions } from './utils'; +import { useTheme2 } from '../../../themes'; +import { measureText } from '../../../utils'; +import { FormattedValueDisplay } from '../../FormattedValueDisplay/FormattedValueDisplay'; +import { Sparkline } from '../../Sparkline/Sparkline'; +import { TableCellProps } from '../types'; +import { getAlignmentFactor, getCellOptions } from '../utils'; export const defaultSparklineCellConfig: TableSparklineCellOptions = { type: TableCellDisplayMode.Sparkline, diff --git a/packages/grafana-ui/src/components/Table/TableCell.tsx b/packages/grafana-ui/src/components/Table/Cells/TableCell.tsx similarity index 95% rename from packages/grafana-ui/src/components/Table/TableCell.tsx rename to packages/grafana-ui/src/components/Table/Cells/TableCell.tsx index 1ca230db4d4..5fe1aed50d1 100644 --- a/packages/grafana-ui/src/components/Table/TableCell.tsx +++ b/packages/grafana-ui/src/components/Table/Cells/TableCell.tsx @@ -2,8 +2,8 @@ import { Cell } from 'react-table'; import { TimeRange, DataFrame, InterpolateFunction } from '@grafana/data'; -import { TableStyles } from './styles'; -import { GetActionsFunction, GrafanaTableColumn, TableFilterActionCallback, TableInspectCellCallback } from './types'; +import { TableStyles } from '../TableRT/styles'; +import { GetActionsFunction, GrafanaTableColumn, TableFilterActionCallback, TableInspectCellCallback } from '../types'; export interface Props { cell: Cell; diff --git a/packages/grafana-ui/src/components/Table/Table.test.tsx b/packages/grafana-ui/src/components/Table/Table.test.tsx index c59b8dbb798..1dc458fe404 100644 --- a/packages/grafana-ui/src/components/Table/Table.test.tsx +++ b/packages/grafana-ui/src/components/Table/Table.test.tsx @@ -5,8 +5,8 @@ import { applyFieldOverrides, createTheme, DataFrame, FieldType, toDataFrame } f import { Icon } from '../Icon/Icon'; -import { Table } from './Table'; -import { CustomHeaderRendererProps, Props } from './types'; +import { Table } from './TableRT/Table'; +import { CustomHeaderRendererProps, BaseTableProps } from './types'; // mock transition styles to ensure consistent behaviour in unit tests jest.mock('@floating-ui/react', () => ({ @@ -101,11 +101,11 @@ function applyOverrides(dataFrame: DataFrame) { return dataFrames[0]; } -function getTestContext(propOverrides: Partial = {}) { +function getTestContext(propOverrides: Partial = {}) { const onSortByChange = jest.fn(); const onCellFilterAdded = jest.fn(); const onColumnResize = jest.fn(); - const props: Props = { + const props: BaseTableProps = { ariaLabel: 'aria-label', data: getDataFrame(fullDataFrame), height: 600, @@ -415,7 +415,7 @@ describe('Table', () => { const onSortByChange = jest.fn(); const onCellFilterAdded = jest.fn(); const onColumnResize = jest.fn(); - const props: Props = { + const props: BaseTableProps = { ariaLabel: 'aria-label', data: getDataFrame(fullDataFrame), height: 600, @@ -557,7 +557,7 @@ describe('Table', () => { const onSortByChange = jest.fn(); const onCellFilterAdded = jest.fn(); const onColumnResize = jest.fn(); - const props: Props = { + const props: BaseTableProps = { ariaLabel: 'aria-label', data: getDataFrame(fullDataFrame), height: 600, diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index 7aafd46b6df..91c49c871df 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -1,411 +1,8 @@ -import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { - useAbsoluteLayout, - useExpanded, - useFilters, - usePagination, - useResizeColumns, - useSortBy, - useTable, -} from 'react-table'; -import { VariableSizeList } from 'react-window'; +import { TableNG } from './TableNG/TableNG'; +import { Table as TableRT } from './TableRT/Table'; +import { GeneralTableProps } from './types'; -import { FieldType, ReducerID, getRowUniqueId, getFieldMatcher, Field } from '@grafana/data'; -import { selectors } from '@grafana/e2e-selectors'; -import { TableCellHeight } from '@grafana/schema'; - -import { useTheme2 } from '../../themes'; -import { Trans } from '../../utils/i18n'; -import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar'; -import { Pagination } from '../Pagination/Pagination'; - -import { FooterRow } from './FooterRow'; -import { HeaderRow } from './HeaderRow'; -import { RowsList } from './RowsList'; -import { TableCellInspector } from './TableCellInspector'; -import { useFixScrollbarContainer, useResetVariableListSizeCache } from './hooks'; -import { getInitialState, useTableStateReducer } from './reducer'; -import { useTableStyles } from './styles'; -import { FooterItem, GrafanaTableState, InspectCell, Props } from './types'; -import { - getColumns, - sortCaseInsensitive, - sortNumber, - getFooterItems, - createFooterCalculationValues, - guessLongestField, -} from './utils'; - -const COLUMN_MIN_WIDTH = 150; -const FOOTER_ROW_HEIGHT = 36; -const NO_DATA_TEXT = 'No data'; - -export const Table = memo((props: Props) => { - const { - ariaLabel, - data, - height, - onCellFilterAdded, - onColumnResize, - width, - columnMinWidth = COLUMN_MIN_WIDTH, - noHeader, - resizable = true, - initialSortBy, - footerOptions, - showTypeIcons, - footerValues, - enablePagination, - cellHeight = TableCellHeight.Sm, - timeRange, - enableSharedCrosshair = false, - initialRowIndex = undefined, - fieldConfig, - getActions, - replaceVariables, - } = props; - - const listRef = useRef(null); - const tableDivRef = useRef(null); - const variableSizeListScrollbarRef = useRef(null); - const theme = useTheme2(); - const tableStyles = useTableStyles(theme, cellHeight); - const headerHeight = noHeader ? 0 : tableStyles.rowHeight; - const [footerItems, setFooterItems] = useState(footerValues); - const noValuesDisplayText = fieldConfig?.defaults?.noValue ?? NO_DATA_TEXT; - const [inspectCell, setInspectCell] = useState(null); - - const footerHeight = useMemo(() => { - const EXTENDED_ROW_HEIGHT = FOOTER_ROW_HEIGHT; - let length = 0; - - if (!footerItems) { - return 0; - } - - for (const fv of footerItems) { - if (Array.isArray(fv) && fv.length > length) { - length = fv.length; - } - } - - if (length > 1) { - return EXTENDED_ROW_HEIGHT * length; - } - - return EXTENDED_ROW_HEIGHT; - }, [footerItems]); - - // React table data array. This data acts just like a dummy array to let react-table know how many rows exist. - // The cells use the field to look up values, therefore this is simply a length/size placeholder. - const memoizedData = useMemo(() => { - if (!data.fields.length) { - return []; - } - // As we only use this to fake the length of our data set for react-table we need to make sure we always return an array - // filled with values at each index otherwise we'll end up trying to call accessRow for null|undefined value in - // https://github.com/tannerlinsley/react-table/blob/7be2fc9d8b5e223fc998af88865ae86a88792fdb/src/hooks/useTable.js#L585 - return Array(data.length).fill(0); - }, [data]); - - // This checks whether `Show table footer` is toggled on, the `Calculation` is set to `Count`, and finally, whether `Count rows` is toggled on. - const isCountRowsSet = Boolean( - footerOptions?.countRows && - footerOptions.reducer && - footerOptions.reducer.length && - footerOptions.reducer[0] === ReducerID.count - ); - - const nestedDataField = data.fields.find((f) => f.type === FieldType.nestedFrames); - const hasNestedData = nestedDataField !== undefined; - - // React-table column definitions - const memoizedColumns = useMemo( - () => getColumns(data, width, columnMinWidth, hasNestedData, footerItems, isCountRowsSet), - [data, width, columnMinWidth, hasNestedData, footerItems, isCountRowsSet] - ); - - // we need a ref to later store the `toggleAllRowsExpanded` function, returned by `useTable`. - // We cannot simply use a variable because we need to use such function in the initialization of - // `useTableStateReducer`, which is needed to construct options for `useTable` (the hook that returns - // `toggleAllRowsExpanded`), and if we used a variable, that variable would be undefined at the time - // we initialize `useTableStateReducer`. - const toggleAllRowsExpandedRef = useRef<(value?: boolean) => void>(); - - // Internal react table state reducer - const stateReducer = useTableStateReducer({ - onColumnResize, - onSortByChange: (state) => { - // Collapse all rows. This prevents a known bug that causes the size of the rows to be incorrect due to - // using `VariableSizeList` and `useExpanded` together. - toggleAllRowsExpandedRef.current!(false); - - if (props.onSortByChange) { - props.onSortByChange(state); - } - }, - data, - }); - - const hasUniqueId = !!data.meta?.uniqueRowIdFields?.length; - - const options: any = useMemo(() => { - // This is a bit hard to type with the react-table types here, the reducer does not actually match with the - // TableOptions. - const options: any = { - columns: memoizedColumns, - data: memoizedData, - disableResizing: !resizable, - stateReducer: stateReducer, - autoResetPage: false, - initialState: getInitialState(initialSortBy, memoizedColumns), - autoResetFilters: false, - sortTypes: { - // the builtin number type on react-table does not handle NaN values - number: sortNumber, - // should be replaced with the builtin string when react-table is upgraded, - // see https://github.com/tannerlinsley/react-table/pull/3235 - 'alphanumeric-insensitive': sortCaseInsensitive, - }, - }; - if (hasUniqueId) { - // row here is just always 0 because here we don't use real data but just a dummy array filled with 0. - // See memoizedData variable above. - options.getRowId = (row: Record, relativeIndex: number) => getRowUniqueId(data, relativeIndex); - - // If we have unique field we assume we can count on it as being globally unique, and we don't need to reset when - // data changes. - options.autoResetExpanded = false; - } - return options; - }, [initialSortBy, memoizedColumns, memoizedData, resizable, stateReducer, hasUniqueId, data]); - - const { - getTableProps, - headerGroups, - footerGroups, - rows, - prepareRow, - totalColumnsWidth, - page, - state, - gotoPage, - setPageSize, - pageOptions, - toggleAllRowsExpanded, - } = useTable(options, useFilters, useSortBy, useAbsoluteLayout, useResizeColumns, useExpanded, usePagination); - - const extendedState = state as GrafanaTableState; - toggleAllRowsExpandedRef.current = toggleAllRowsExpanded; - - /* - Footer value calculation is being moved in the Table component and the footerValues prop will be deprecated. - The footerValues prop is still used in the Table component for backwards compatibility. Adding the - footerOptions prop will switch the Table component to use the new footer calculation. Using both props will - result in the footerValues prop being ignored. - */ - useEffect(() => { - if (!footerOptions) { - setFooterItems(footerValues); - } - }, [footerValues, footerOptions]); - - useEffect(() => { - if (!footerOptions) { - return; - } - - if (!footerOptions.show) { - setFooterItems(undefined); - return; - } - - if (isCountRowsSet) { - const footerItemsCountRows: FooterItem[] = []; - footerItemsCountRows[0] = rows.length.toString() ?? data.length.toString(); - setFooterItems(footerItemsCountRows); - return; - } - - const footerItems = getFooterItems( - headerGroups[0].headers, - createFooterCalculationValues(rows), - footerOptions, - theme - ); - - setFooterItems(footerItems); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [footerOptions, theme, state.filters, data]); - - let listHeight = height - (headerHeight + footerHeight); - - if (enablePagination) { - listHeight -= tableStyles.cellHeight; - } - - const pageSize = Math.round(listHeight / tableStyles.rowHeight) - 1; - - useEffect(() => { - // Don't update the page size if it is less than 1 - if (pageSize <= 0) { - return; - } - setPageSize(pageSize); - }, [pageSize, setPageSize]); - - useEffect(() => { - // Reset page index when data changes - // This is needed because react-table does not do this automatically - // autoResetPage is set to false because setting it to true causes the issue described in - // https://github.com/grafana/grafana/pull/67477 - if (data.length / pageSize < state.pageIndex) { - gotoPage(0); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [data]); - - useResetVariableListSizeCache(extendedState, listRef, data, hasUniqueId); - useFixScrollbarContainer(variableSizeListScrollbarRef, tableDivRef); - - const onNavigate = useCallback( - (toPage: number) => { - gotoPage(toPage - 1); - }, - [gotoPage] - ); - - const itemCount = enablePagination ? page.length : rows.length; - let paginationEl = null; - if (enablePagination) { - const itemsRangeStart = state.pageIndex * state.pageSize + 1; - let itemsRangeEnd = itemsRangeStart + state.pageSize - 1; - const isSmall = width < 550; - if (itemsRangeEnd > data.length) { - itemsRangeEnd = data.length; - } - const numRows = rows.length; - const displayedEnd = itemsRangeEnd < rows.length ? itemsRangeEnd : rows.length; - paginationEl = ( -
- - {isSmall ? null : ( -
- - {{ itemsRangeStart }} - {{ displayedEnd }} of {{ numRows }} rows - -
- )} -
- ); - } - - // Try to determine the longet field - // TODO: do we wrap only one field? - // What if there are multiple fields with long text? - let longestField: Field | undefined = undefined; - let textWrapField = undefined; - - if (fieldConfig) { - longestField = guessLongestField(fieldConfig, data); - - data.fields.forEach((field) => { - fieldConfig.overrides.forEach((override) => { - const matcher = getFieldMatcher(override.matcher); - if (matcher(field, data, [data])) { - for (const property of override.properties) { - if (property.id === 'custom.cellOptions' && property.value.wrapText) { - textWrapField = field; - } - } - } - }); - }); - } - - return ( - <> -
- -
- {!noHeader && ( - - )} - {itemCount > 0 ? ( -
- -
- ) : ( -
- {noValuesDisplayText} -
- )} - {footerItems && ( - - )} -
-
- {paginationEl} -
- - {inspectCell !== null && ( - { - setInspectCell(null); - }} - /> - )} - - ); -}); - -Table.displayName = 'Table'; +export function Table(props: GeneralTableProps) { + let table = props.useTableNg ? : ; + return table; +} diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/ActionsCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/ActionsCell.tsx new file mode 100644 index 00000000000..128224b6261 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/ActionsCell.tsx @@ -0,0 +1,24 @@ +import { css } from '@emotion/css'; + +import { GrafanaTheme2 } from '@grafana/data'; + +import { useStyles2 } from '../../../../themes'; +import { ActionButton } from '../../../Actions/ActionButton'; +import { ActionCellProps } from '../types'; + +export const ActionsCell = ({ actions }: ActionCellProps) => { + const styles = useStyles2(getStyles); + + return ( +
+ {actions && actions.map((action, i) => )} +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + buttonsGap: css({ + display: 'flex', + gap: 6, + }), +}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx new file mode 100644 index 00000000000..c484601c8b6 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx @@ -0,0 +1,94 @@ +import { css, cx } from '@emotion/css'; +import { Property } from 'csstype'; + +import { GrafanaTheme2, formattedValueToString } from '@grafana/data'; +import { TableCellDisplayMode, TableCellOptions } from '@grafana/schema'; + +import { useStyles2 } from '../../../../themes'; +import { clearLinkButtonStyles } from '../../../Button'; +import { DataLinksContextMenu } from '../../../DataLinks/DataLinksContextMenu'; +import { AutoCellProps } from '../types'; +import { getCellLinks } from '../utils'; + +export default function AutoCell({ value, field, justifyContent, rowIdx, cellOptions }: AutoCellProps) { + const styles = useStyles2(getStyles, justifyContent); + + const displayValue = field.display!(value); + const formattedValue = formattedValueToString(displayValue); + const hasLinks = Boolean(getCellLinks(field, rowIdx)?.length); + const clearButtonStyle = useStyles2(clearLinkButtonStyles); + + return ( +
+ {hasLinks ? ( + getCellLinks(field, rowIdx) || []}> + {(api) => { + if (api.openMenu) { + return ( + + ); + } else { + return
{formattedValue}
; + } + }} +
+ ) : ( + formattedValue + )} +
+ ); +} + +const getLinkStyle = ( + styles: ReturnType, + cellOptions: TableCellOptions, + targetClassName: string | undefined +) => { + if (cellOptions.type === TableCellDisplayMode.Auto) { + return cx(styles.linkCell, targetClassName); + } + + return cx(styles.cellLinkForColoredCell, targetClassName); +}; + +const getStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent | undefined) => ({ + cell: css({ + display: 'flex', + justifyContent: justifyContent, + + a: { + color: 'inherit', + }, + }), + cellLinkForColoredCell: css({ + cursor: 'pointer', + overflow: 'hidden', + textOverflow: 'ellipsis', + userSelect: 'text', + whiteSpace: 'nowrap', + fontWeight: theme.typography.fontWeightMedium, + textDecoration: 'underline', + }), + linkCell: css({ + cursor: 'pointer', + overflow: 'hidden', + textOverflow: 'ellipsis', + userSelect: 'text', + whiteSpace: 'nowrap', + color: theme.colors.text.link, + fontWeight: theme.typography.fontWeightMedium, + paddingRight: theme.spacing(1.5), + a: { + color: theme.colors.text.link, + }, + '&:hover': { + textDecoration: 'underline', + color: theme.colors.text.link, + }, + }), +}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx new file mode 100644 index 00000000000..d18c4631ae0 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx @@ -0,0 +1,89 @@ +import { ThresholdsConfig, ThresholdsMode, VizOrientation, getFieldConfigWithMinMax } from '@grafana/data'; +import { BarGaugeDisplayMode, BarGaugeValueMode, TableCellDisplayMode } from '@grafana/schema'; + +import { BarGauge } from '../../../BarGauge/BarGauge'; +import { DataLinksContextMenu, DataLinksContextMenuApi } from '../../../DataLinks/DataLinksContextMenu'; +import { BarGaugeCellProps } from '../types'; +import { extractPixelValue, getCellOptions, getAlignmentFactor, getCellLinks } from '../utils'; + +const defaultScale: ThresholdsConfig = { + mode: ThresholdsMode.Absolute, + steps: [ + { + color: 'blue', + value: -Infinity, + }, + { + color: 'green', + value: 20, + }, + ], +}; + +export const BarGaugeCell = ({ value, field, theme, height, width, rowIdx }: BarGaugeCellProps) => { + const displayValue = field.display!(value); + const cellOptions = getCellOptions(field); + const heightOffset = extractPixelValue(theme.spacing(1)); + + let config = getFieldConfigWithMinMax(field, false); + if (!config.thresholds) { + config = { + ...config, + thresholds: defaultScale, + }; + } + + // Set default display mode and update if defined + // and update the valueMode if defined + let barGaugeMode: BarGaugeDisplayMode = BarGaugeDisplayMode.Gradient; + let valueDisplayMode: BarGaugeValueMode | undefined = undefined; + + if (cellOptions.type === TableCellDisplayMode.Gauge) { + barGaugeMode = cellOptions.mode ?? BarGaugeDisplayMode.Gradient; + valueDisplayMode = + cellOptions.valueDisplayMode !== undefined ? cellOptions.valueDisplayMode : BarGaugeValueMode.Text; + } + + const hasLinks = Boolean(getCellLinks(field, rowIdx)?.length); + + const alignmentFactors = getAlignmentFactor(field, displayValue, rowIdx!); + + const renderComponent = (menuProps: DataLinksContextMenuApi) => { + const { openMenu } = menuProps; + + return ( + + ); + }; + + // @TODO: Actions + return ( + <> + {hasLinks ? ( + getCellLinks(field, rowIdx) || []} + style={{ display: 'flex', width: '100%' }} + > + {(api) => renderComponent(api)} + + ) : ( + renderComponent({}) + )} + + ); +}; diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/DataLinksCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/DataLinksCell.tsx new file mode 100644 index 00000000000..858faa49b4d --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/DataLinksCell.tsx @@ -0,0 +1,49 @@ +import { css } from '@emotion/css'; + +import { GrafanaTheme2 } from '@grafana/data'; + +import { useStyles2 } from '../../../../themes'; +import { DataLinksCellProps } from '../types'; +import { getCellLinks } from '../utils'; + +export const DataLinksCell = ({ field, rowIdx }: DataLinksCellProps) => { + const styles = useStyles2(getStyles); + + const links = getCellLinks(field, rowIdx!); + + return ( +
+ {links && + links.map((link, idx) => { + return ( + // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions + + + {link.title} + + + ); + })} +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + linkCell: css({ + cursor: 'pointer', + overflow: 'hidden', + textOverflow: 'ellipsis', + userSelect: 'text', + whiteSpace: 'nowrap', + color: theme.colors.text.link, + fontWeight: theme.typography.fontWeightMedium, + paddingRight: theme.spacing(1.5), + a: { + color: theme.colors.text.link, + }, + '&:hover': { + textDecoration: 'underline', + color: theme.colors.text.link, + }, + }), +}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/FooterCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/FooterCell.tsx new file mode 100644 index 00000000000..61e62f2dedc --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/FooterCell.tsx @@ -0,0 +1,80 @@ +import { css } from '@emotion/css'; +import { Property } from 'csstype'; + +import { fieldReducers, KeyValue, ReducerID } from '@grafana/data'; + +export type FooterItem = Array> | string | undefined; + +export interface FooterProps { + value: FooterItem; + justifyContent?: Property.JustifyContent; +} + +export const FooterCell = (props: FooterProps) => { + const cell = css({ + width: '100%', + listStyle: 'none', + }); + + const item = css({ + display: 'flex', + flexDirection: 'row', + justifyContent: props.justifyContent || 'space-between', + }); + + const list = css({ + width: '100%', + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + }); + + if (props.value && !Array.isArray(props.value)) { + return {props.value}; + } + + if (props.value && Array.isArray(props.value) && props.value.length > 0) { + return ( +
    + {props.value.map((v: KeyValue, i) => { + const key = Object.keys(v)[0]; + return ( +
  • + {key} + {v[key]} +
  • + ); + })} +
+ ); + } + + return EmptyCell; +}; + +export const EmptyCell = () => { + return  ; +}; + +export function getFooterValue( + index: number, + footerValues?: FooterItem[], + isCountRowsSet?: boolean, + justifyContent?: Property.JustifyContent +) { + if (footerValues === undefined) { + return EmptyCell; + } + + if (isCountRowsSet) { + if (footerValues[index] === undefined) { + return EmptyCell; + } + + const key = fieldReducers.get(ReducerID.count).name; + + return FooterCell({ value: [{ [key]: String(footerValues[index]) }] }); + } + + return FooterCell({ value: footerValues[index], justifyContent }); +} diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/HeaderCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/HeaderCell.tsx new file mode 100644 index 00000000000..824a71f9186 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/HeaderCell.tsx @@ -0,0 +1,163 @@ +import { css } from '@emotion/css'; +import { Property } from 'csstype'; +import React, { useLayoutEffect, useRef, useEffect } from 'react'; +import { Column, SortDirection } from 'react-data-grid'; + +import { Field, GrafanaTheme2 } from '@grafana/data'; + +import { useStyles2 } from '../../../../themes'; +import { getFieldTypeIcon } from '../../../../types'; +import { Icon } from '../../../Icon/Icon'; +import { Filter } from '../Filter/Filter'; +import { TableColumnResizeActionCallback, FilterType, TableRow, TableSummaryRow } from '../types'; + +interface HeaderCellProps { + column: Column; + rows: TableRow[]; + field: Field; + onSort: (columnKey: string, direction: SortDirection, isMultiSort: boolean) => void; + direction?: SortDirection; + justifyContent: Property.JustifyContent; + filter: FilterType; + setFilter: React.Dispatch>; + filterable: boolean; + onColumnResize?: TableColumnResizeActionCallback; + headerCellRefs: React.MutableRefObject>; + crossFilterOrder: React.MutableRefObject; + crossFilterRows: React.MutableRefObject<{ [key: string]: TableRow[] }>; + showTypeIcons?: boolean; +} + +const HeaderCell: React.FC = ({ + column, + rows, + field, + onSort, + direction, + justifyContent, + filter, + setFilter, + filterable, + onColumnResize, + headerCellRefs, + crossFilterOrder, + crossFilterRows, + showTypeIcons, +}) => { + const styles = useStyles2(getStyles); + const headerRef = useRef(null); + + let isColumnFilterable = filterable; + if (field.config.custom?.filterable !== filterable) { + isColumnFilterable = field.config.custom?.filterable || false; + } + // we have to remove/reset the filter if the column is not filterable + if (!isColumnFilterable && filter[field.name]) { + setFilter((filter: FilterType) => { + const newFilter = { ...filter }; + delete newFilter[field.name]; + return newFilter; + }); + } + + const handleSort = (event: React.MouseEvent) => { + const isMultiSort = event.shiftKey; + onSort(column.key as string, direction === 'ASC' ? 'DESC' : 'ASC', isMultiSort); + }; + + // collecting header cell refs to handle manual column resize + useLayoutEffect(() => { + if (headerRef.current) { + headerCellRefs.current[column.key] = headerRef.current; + } + }, [headerRef, column.key]); // eslint-disable-line react-hooks/exhaustive-deps + + // TODO: this is a workaround to handle manual column resize; + useEffect(() => { + const headerCellParent = headerRef.current?.parentElement; + if (headerCellParent) { + // `lastElement` is an HTML element added by react-data-grid for resizing columns. + // We add event listeners to `lastElement` to handle the resize operation. + const lastElement = headerCellParent.lastElementChild; + if (lastElement) { + const handleMouseUp = () => { + let newWidth = headerCellParent.clientWidth; + onColumnResize?.(column.key as string, newWidth); + }; + + lastElement.addEventListener('click', handleMouseUp); + + return () => { + lastElement.removeEventListener('click', handleMouseUp); + }; + } + } + // to handle "Not all code paths return a value." error + return; + }, [column]); // eslint-disable-line react-hooks/exhaustive-deps + + return ( +
{ + if (event.key === ' ') { + event.stopPropagation(); + } + }} + > + + + {isColumnFilterable && ( + + )} +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + headerCellLabel: css({ + border: 'none', + padding: 0, + background: 'inherit', + cursor: 'pointer', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + fontWeight: theme.typography.fontWeightMedium, + display: 'flex', + alignItems: 'center', + marginRight: theme.spacing(0.5), + color: theme.colors.text.secondary, + gap: theme.spacing(1), + + '&:hover': { + textDecoration: 'underline', + color: theme.colors.text.link, + }, + }), + sortIcon: css({ + marginLeft: theme.spacing(0.5), + }), +}); + +export { HeaderCell }; diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx new file mode 100644 index 00000000000..1165d960019 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx @@ -0,0 +1,69 @@ +import { css } from '@emotion/css'; +import { Property } from 'csstype'; +import * as React from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { TableCellDisplayMode } from '@grafana/schema'; + +import { useStyles2 } from '../../../../themes'; +import { DataLinksContextMenu } from '../../../DataLinks/DataLinksContextMenu'; +import { ImageCellProps } from '../types'; +import { getCellLinks } from '../utils'; + +const DATALINKS_HEIGHT_OFFSET = 10; + +export const ImageCell = ({ cellOptions, field, height, justifyContent, value, rowIdx }: ImageCellProps) => { + const calculatedHeight = height - DATALINKS_HEIGHT_OFFSET; + const styles = useStyles2(getStyles, calculatedHeight, justifyContent); + const hasLinks = Boolean(getCellLinks(field, rowIdx)?.length); + + const { text } = field.display!(value); + const { alt, title } = + cellOptions.type === TableCellDisplayMode.Image ? cellOptions : { alt: undefined, title: undefined }; + + const img = {alt}; + + // TODO: Implement actions + return ( +
+ {hasLinks ? ( + getCellLinks(field, rowIdx) || []}> + {(api) => { + if (api.openMenu) { + return ( +
{ + if (e.key === 'Enter' && api.openMenu) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions + api.openMenu(e as any); + } + }} + > + {img} +
+ ); + } else { + return img; + } + }} +
+ ) : ( + img + )} +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2, height: number, justifyContent: Property.JustifyContent) => ({ + image: css({ + height, + width: 'auto', + }), + imageContainer: css({ + display: 'flex', + justifyContent, + }), +}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/JSONCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/JSONCell.tsx new file mode 100644 index 00000000000..d207d710448 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/JSONCell.tsx @@ -0,0 +1,69 @@ +import { css, cx } from '@emotion/css'; +import { Property } from 'csstype'; + +import { GrafanaTheme2 } from '@grafana/data'; + +import { useStyles2 } from '../../../../themes'; +import { Button, clearLinkButtonStyles } from '../../../Button'; +import { DataLinksContextMenu } from '../../../DataLinks/DataLinksContextMenu'; +import { JSONCellProps } from '../types'; +import { getCellLinks } from '../utils'; + +export const JSONCell = ({ value, justifyContent, field, rowIdx }: JSONCellProps) => { + const styles = useStyles2(getStyles, justifyContent); + const clearButtonStyle = useStyles2(clearLinkButtonStyles); + + let displayValue = value; + + // Handle string values that might be JSON + if (typeof value === 'string') { + try { + const parsed = JSON.parse(value); + displayValue = JSON.stringify(parsed, null, ' '); + } catch { + displayValue = value; // Keep original if not valid JSON + } + } else { + // For non-string values, stringify them + try { + displayValue = JSON.stringify(value, null, ' '); + } catch (error) { + // Handle circular references or other stringify errors + displayValue = String(value); + } + } + + const hasLinks = Boolean(getCellLinks(field, rowIdx)?.length); + + // TODO: Implement actions + return ( +
+ {hasLinks ? ( + getCellLinks(field, rowIdx) || []}> + {(api) => { + if (api.openMenu) { + return ( + + ); + } else { + return <>{displayValue}; + } + }} + + ) : ( + displayValue + )} +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent) => ({ + jsonText: css({ + display: 'flex', + cursor: 'pointer', + fontFamily: 'monospace', + justifyContent: justifyContent, + }), +}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/RowExpander.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/RowExpander.tsx new file mode 100644 index 00000000000..9088017d980 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/RowExpander.tsx @@ -0,0 +1,36 @@ +import { css } from '@emotion/css'; + +import { GrafanaTheme2 } from '@grafana/data'; + +import { useStyles2 } from '../../../../themes'; +import { Icon } from '../../../Icon/Icon'; +import { RowExpanderNGProps } from '../types'; + +export function RowExpander({ height, onCellExpand, isExpanded }: RowExpanderNGProps) { + const styles = useStyles2(getStyles, height); + function handleKeyDown(e: React.KeyboardEvent) { + if (e.key === ' ' || e.key === 'Enter') { + e.preventDefault(); + onCellExpand(); + } + } + return ( +
+ +
+ ); +} + +const getStyles = (theme: GrafanaTheme2, rowHeight: number) => ({ + expanderCell: css({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + height: `${rowHeight}px`, + cursor: 'pointer', + }), +}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/SparklineCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/SparklineCell.tsx new file mode 100644 index 00000000000..f020ad830d5 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/SparklineCell.tsx @@ -0,0 +1,160 @@ +import { css } from '@emotion/css'; +import { Property } from 'csstype'; +import * as React from 'react'; + +import { + FieldType, + FieldConfig, + getMinMaxAndDelta, + FieldSparkline, + isDataFrame, + Field, + isDataFrameWithValue, + GrafanaTheme2, +} from '@grafana/data'; +import { + BarAlignment, + GraphDrawStyle, + GraphFieldConfig, + GraphGradientMode, + LineInterpolation, + TableSparklineCellOptions, + TableCellDisplayMode, + VisibilityMode, +} from '@grafana/schema'; + +import { useStyles2 } from '../../../../themes'; +import { measureText } from '../../../../utils'; +import { FormattedValueDisplay } from '../../../FormattedValueDisplay/FormattedValueDisplay'; +import { Sparkline } from '../../../Sparkline/Sparkline'; +import { SparklineCellProps } from '../types'; +import { getAlignmentFactor, getCellOptions } from '../utils'; + +export const defaultSparklineCellConfig: TableSparklineCellOptions = { + type: TableCellDisplayMode.Sparkline, + drawStyle: GraphDrawStyle.Line, + lineInterpolation: LineInterpolation.Smooth, + lineWidth: 1, + fillOpacity: 17, + gradientMode: GraphGradientMode.Hue, + pointSize: 2, + barAlignment: BarAlignment.Center, + showPoints: VisibilityMode.Never, + hideValue: false, +}; + +export const SparklineCell = (props: SparklineCellProps) => { + const { field, value, theme, timeRange, rowIdx, justifyContent, width } = props; + const styles = useStyles2(getStyles, justifyContent); + const sparkline = getSparkline(value); + + if (!sparkline) { + return <>{field.config.noValue || 'no data'}; + } + + // Get the step from the first two values to null-fill the x-axis based on timerange + if (sparkline.x && !sparkline.x.config.interval && sparkline.x.values.length > 1) { + sparkline.x.config.interval = sparkline.x.values[1] - sparkline.x.values[0]; + } + + // Remove non-finite values, e.g: NaN, +/-Infinity + sparkline.y.values = sparkline.y.values.map((v) => { + if (!Number.isFinite(v)) { + return null; + } else { + return v; + } + }); + + const range = getMinMaxAndDelta(sparkline.y); + sparkline.y.config.min = range.min; + sparkline.y.config.max = range.max; + sparkline.y.state = { range }; + sparkline.timeRange = timeRange; + + const cellOptions = getTableSparklineCellOptions(field); + + const config: FieldConfig = { + color: field.config.color, + custom: { + ...defaultSparklineCellConfig, + ...cellOptions, + }, + }; + + const hideValue = field.config.custom?.cellOptions?.hideValue; + let valueWidth = 0; + let valueElement: React.ReactNode = null; + if (!hideValue) { + const newValue = isDataFrameWithValue(value) ? value.value : null; + const displayValue = field.display!(newValue); + const alignmentFactor = getAlignmentFactor(field, displayValue, rowIdx!); + + valueWidth = + measureText(`${alignmentFactor.prefix ?? ''}${alignmentFactor.text}${alignmentFactor.suffix ?? ''}`, 16).width + + theme.spacing.gridSize; + + valueElement = ( + + ); + } + + // @TODO update width, height + return ( +
+ {valueElement} + +
+ ); +}; + +function getSparkline(value: unknown): FieldSparkline | undefined { + if (Array.isArray(value)) { + return { + y: { + name: 'test', + 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) { + options = { ...options, type: TableCellDisplayMode.Sparkline }; + } + if (options.type === TableCellDisplayMode.Sparkline) { + return options; + } + throw new Error(`Expected options type ${TableCellDisplayMode.Sparkline} but got ${options.type}`); +} + +const getStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent | undefined) => ({ + cellContainer: css({ + display: 'flex', + width: '100%', + alignItems: 'center', + justifyContent, + }), +}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellNG.tsx new file mode 100644 index 00000000000..3b95dd91599 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellNG.tsx @@ -0,0 +1,210 @@ +import { css } from '@emotion/css'; +import { ReactNode, useLayoutEffect, useMemo, useRef, useState } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { TableAutoCellOptions, TableCellDisplayMode } from '@grafana/schema'; + +import { useStyles2 } from '../../../../themes'; +import { IconButton } from '../../../IconButton/IconButton'; +import { TableCellInspectorMode } from '../../TableCellInspector'; +import { CellColors, TableCellNGProps } from '../types'; +import { getCellColors, getTextAlign } from '../utils'; + +import { ActionsCell } from './ActionsCell'; +import AutoCell from './AutoCell'; +import { BarGaugeCell } from './BarGaugeCell'; +import { DataLinksCell } from './DataLinksCell'; +import { ImageCell } from './ImageCell'; +import { JSONCell } from './JSONCell'; +import { SparklineCell } from './SparklineCell'; + +export function TableCellNG(props: TableCellNGProps) { + const { + field, + frame, + value, + theme, + timeRange, + height, + rowIdx, + justifyContent, + shouldTextOverflow, + setIsInspecting, + setContextMenuProps, + cellInspect, + getActions, + rowBg, + } = props; + + const { config: fieldConfig } = field; + const defaultCellOptions: TableAutoCellOptions = { type: TableCellDisplayMode.Auto }; + const cellOptions = fieldConfig.custom?.cellOptions ?? defaultCellOptions; + const { type: cellType } = cellOptions; + + const isRightAligned = getTextAlign(field) === 'flex-end'; + const displayValue = field.display!(value); + let colors: CellColors = { bgColor: '', textColor: '', bgHoverColor: '' }; + if (rowBg) { + colors = rowBg(rowIdx); + } else { + colors = useMemo(() => getCellColors(theme, cellOptions, displayValue), [theme, cellOptions, displayValue]); + } + const styles = useStyles2(getStyles, isRightAligned, colors); + + // TODO + // TableNG provides either an overridden cell width or 'auto' as the cell width value. + // While the overridden value gives the exact cell width, 'auto' does not. + // Therefore, we need to determine the actual cell width from the DOM. + const divWidthRef = useRef(null); + const [divWidth, setDivWidth] = useState(0); + const [isHovered, setIsHovered] = useState(false); + + const actions = getActions ? getActions(frame, field, rowIdx) : []; + + useLayoutEffect(() => { + if (divWidthRef.current && divWidthRef.current.clientWidth !== 0) { + setDivWidth(divWidthRef.current.clientWidth); + } + }, [divWidthRef.current]); // eslint-disable-line react-hooks/exhaustive-deps + + // Get the correct cell type + let cell: ReactNode = null; + switch (cellType) { + case TableCellDisplayMode.Sparkline: + cell = ( + + ); + break; + case TableCellDisplayMode.Gauge: + case TableCellDisplayMode.BasicGauge: + case TableCellDisplayMode.GradientGauge: + case TableCellDisplayMode.LcdGauge: + cell = ( + + ); + break; + case TableCellDisplayMode.Image: + cell = ( + + ); + break; + case TableCellDisplayMode.JSONView: + cell = ; + break; + case TableCellDisplayMode.DataLinks: + cell = ; + break; + case TableCellDisplayMode.Actions: + cell = ; + break; + case TableCellDisplayMode.Auto: + default: + cell = ( + + ); + } + + const handleMouseEnter = () => { + setIsHovered(true); + if (shouldTextOverflow()) { + // TODO: The table cell styles in TableNG do not update dynamically even if we change the state + const div = divWidthRef.current; + const tableCellDiv = div?.parentElement; + tableCellDiv?.style.setProperty('position', 'absolute'); + tableCellDiv?.style.setProperty('top', '0'); + tableCellDiv?.style.setProperty('z-index', String(theme.zIndex.tooltip)); + tableCellDiv?.style.setProperty('white-space', 'normal'); + tableCellDiv?.style.setProperty('min-height', `${height}px`); + } + }; + + const handleMouseLeave = () => { + setIsHovered(false); + if (shouldTextOverflow()) { + // TODO: The table cell styles in TableNG do not update dynamically even if we change the state + const div = divWidthRef.current; + const tableCellDiv = div?.parentElement; + tableCellDiv?.style.setProperty('position', 'relative'); + tableCellDiv?.style.removeProperty('top'); + tableCellDiv?.style.removeProperty('z-index'); + tableCellDiv?.style.setProperty('white-space', 'nowrap'); + } + }; + + return ( +
+ {cell} + {cellInspect && isHovered && ( +
+ { + setContextMenuProps({ + value: String(value ?? ''), + mode: + cellType === TableCellDisplayMode.JSONView + ? TableCellInspectorMode.code + : TableCellInspectorMode.text, + }); + setIsInspecting(true); + }} + /> +
+ )} +
+ ); +} + +const getStyles = (theme: GrafanaTheme2, isRightAligned: boolean, color: CellColors) => ({ + cell: css({ + height: '100%', + alignContent: 'center', + paddingInline: '8px', + // TODO: follow-up on this: change styles on hover on table row level + background: color.bgColor || 'none', + color: color.textColor, + '&:hover': { background: color.bgHoverColor }, + }), + cellActions: css({ + display: 'flex', + position: 'absolute', + top: '1px', + left: isRightAligned ? 0 : undefined, + right: isRightAligned ? undefined : 0, + margin: 'auto', + height: '100%', + background: theme.colors.background.secondary, + color: theme.colors.text.primary, + padding: '4px 0px 4px 4px', + }), +}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Filter/Filter.tsx b/packages/grafana-ui/src/components/Table/TableNG/Filter/Filter.tsx new file mode 100644 index 00000000000..dfbda8354fe --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/Filter/Filter.tsx @@ -0,0 +1,99 @@ +import { css, cx } from '@emotion/css'; +import { useCallback, useMemo, useRef, useState } from 'react'; + +import { Field, GrafanaTheme2, SelectableValue } from '@grafana/data'; + +import { useStyles2 } from '../../../../themes'; +import { Icon } from '../../../Icon/Icon'; +import { Popover } from '../../../Tooltip/Popover'; +import { TableRow } from '../types'; + +import { REGEX_OPERATOR } from './FilterList'; +import { FilterPopup } from './FilterPopup'; + +interface Props { + name: string; + rows: any[]; + filter: any; + setFilter: (value: any) => void; + field?: Field; + crossFilterOrder: string[]; + crossFilterRows: { [key: string]: TableRow[] }; +} + +export const Filter = ({ name, rows, filter, setFilter, field, crossFilterOrder, crossFilterRows }: Props) => { + const filterValue = filter[name]?.filtered; + + // get rows for cross filtering + const filterIndex = crossFilterOrder.indexOf(name); + let filteredRows: TableRow[]; + if (filterIndex > 0) { + // current filter list should be based on the previous filter list + const previousFilterName = crossFilterOrder[filterIndex - 1]; + filteredRows = crossFilterRows[previousFilterName]; + } else if (filterIndex === -1 && crossFilterOrder.length > 0) { + // current filter list should be based on the last filter list + const previousFilterName = crossFilterOrder[crossFilterOrder.length - 1]; + filteredRows = crossFilterRows[previousFilterName]; + } else { + filteredRows = rows; + } + + const ref = useRef(null); + const [isPopoverVisible, setPopoverVisible] = useState(false); + const styles = useStyles2(getStyles); + const filterEnabled = useMemo(() => Boolean(filterValue), [filterValue]); + const onShowPopover = useCallback(() => setPopoverVisible(true), [setPopoverVisible]); + const onClosePopover = useCallback(() => setPopoverVisible(false), [setPopoverVisible]); + const [searchFilter, setSearchFilter] = useState(filter[name]?.searchFilter || ''); + const [operator, setOperator] = useState>(filter[name]?.operator || REGEX_OPERATOR); + + return ( + + ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + headerFilter: css({ + background: 'transparent', + border: 'none', + label: 'headerFilter', + padding: 0, + }), + filterIconEnabled: css({ + label: 'filterIconEnabled', + color: theme.colors.primary.text, + }), + filterIconDisabled: css({ + label: 'filterIconDisabled', + color: theme.colors.text.disabled, + }), +}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Filter/FilterList.tsx b/packages/grafana-ui/src/components/Table/TableNG/Filter/FilterList.tsx new file mode 100644 index 00000000000..ae3c4014472 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/Filter/FilterList.tsx @@ -0,0 +1,268 @@ +import { css, cx } from '@emotion/css'; +import { useCallback, useMemo } from 'react'; +import * as React from 'react'; +import { FixedSizeList as List, ListChildComponentProps } from 'react-window'; + +import { GrafanaTheme2, formattedValueToString, getValueFormat, SelectableValue } from '@grafana/data'; + +import { ButtonSelect, Checkbox, FilterInput, Label, Stack } from '../../..'; +import { useStyles2, useTheme2 } from '../../../../themes'; +import { Trans } from '../../../../utils/i18n'; + +interface Props { + values: SelectableValue[]; + options: SelectableValue[]; + onChange: (options: SelectableValue[]) => void; + caseSensitive?: boolean; + showOperators?: boolean; + searchFilter: string; + setSearchFilter: (value: string) => void; + operator: SelectableValue; + setOperator: (item: SelectableValue) => void; +} + +const ITEM_HEIGHT = 28; +const MIN_HEIGHT = ITEM_HEIGHT * 5; + +const operatorSelectableValues: { [key: string]: SelectableValue } = { + Contains: { label: 'Contains', value: 'Contains', description: 'Contains' }, + '=': { label: '=', value: '=', description: 'Equals' }, + '!=': { label: '!=', value: '!=', description: 'Not equals' }, + '>': { label: '>', value: '>', description: 'Greater' }, + '>=': { label: '>=', value: '>=', description: 'Greater or Equal' }, + '<': { label: '<', value: '<', description: 'Less' }, + '<=': { label: '<=', value: '<=', description: 'Less or Equal' }, + Expression: { + label: 'Expression', + value: 'Expression', + description: 'Bool Expression (Char $ represents the column value in the expression, e.g. "$ >= 10 && $ <= 12")', + }, +}; +const OPERATORS = Object.values(operatorSelectableValues); +export const REGEX_OPERATOR = operatorSelectableValues['Contains']; +const XPR_OPERATOR = operatorSelectableValues['Expression']; + +const comparableValue = (value: string): string | number | Date | boolean => { + value = value.trim().replace(/\\/g, ''); + + // Does it look like a Date (Starting with pattern YYYY-MM-DD* or YYYY/MM/DD*)? + if (/^(\d{4}-\d{2}-\d{2}|\d{4}\/\d{2}\/\d{2})/.test(value)) { + const date = new Date(value); + if (!isNaN(date.getTime())) { + const fmt = getValueFormat('dateTimeAsIso'); + return formattedValueToString(fmt(date.getTime())); + } + } + // Does it look like a Number? + const num = parseFloat(value); + if (!isNaN(num)) { + return num; + } + // Does it look like a Bool? + const lvalue = value.toLowerCase(); + if (lvalue === 'true' || lvalue === 'false') { + return lvalue === 'true'; + } + // Anything else + return value; +}; + +export const FilterList = ({ + options, + values, + caseSensitive, + showOperators, + onChange, + searchFilter, + setSearchFilter, + operator, + setOperator, +}: Props) => { + const regex = useMemo(() => new RegExp(searchFilter, caseSensitive ? undefined : 'i'), [searchFilter, caseSensitive]); + const items = useMemo( + () => + options.filter((option) => { + if (!showOperators || !searchFilter || operator.value === REGEX_OPERATOR.value) { + if (option.label === undefined) { + return false; + } + return regex.test(option.label); + } else if (operator.value === XPR_OPERATOR.value) { + if (option.value === undefined) { + return false; + } + try { + const xpr = searchFilter.replace(/\\/g, ''); + const fnc = new Function('$', `'use strict'; return ${xpr};`); + const val = comparableValue(option.value); + return fnc(val); + } catch (_) {} + return false; + } else { + if (option.value === undefined) { + return false; + } + + const value1 = comparableValue(option.value); + const value2 = comparableValue(searchFilter); + + switch (operator.value) { + case '=': + return value1 === value2; + case '!=': + return value1 !== value2; + case '>': + return value1 > value2; + case '>=': + return value1 >= value2; + case '<': + return value1 < value2; + case '<=': + return value1 <= value2; + } + return false; + } + }), + [options, regex, showOperators, operator, searchFilter] + ); + const selectedItems = useMemo(() => items.filter((item) => values.includes(item)), [items, values]); + + const selectCheckValue = useMemo(() => items.length === selectedItems.length, [items, selectedItems]); + const selectCheckIndeterminate = useMemo( + () => selectedItems.length > 0 && items.length > selectedItems.length, + [items, selectedItems] + ); + const selectCheckLabel = useMemo( + () => (selectedItems.length ? `${selectedItems.length} selected` : `Select all`), + [selectedItems] + ); + const selectCheckDescription = useMemo( + () => + items.length !== selectedItems.length + ? 'Add all displayed values to the filter' + : 'Remove all displayed values from the filter', + [items, selectedItems] + ); + + const styles = useStyles2(getStyles); + const theme = useTheme2(); + const gutter = theme.spacing.gridSize; + const height = useMemo(() => Math.min(items.length * ITEM_HEIGHT, MIN_HEIGHT) + gutter, [gutter, items.length]); + + const onCheckedChanged = useCallback( + (option: SelectableValue) => (event: React.FormEvent) => { + const newValues = event.currentTarget.checked + ? values.concat(option) + : values.filter((c) => c.value !== option.value); + + onChange(newValues); + }, + [onChange, values] + ); + + const onSelectChanged = useCallback(() => { + if (items.length === selectedItems.length) { + const newValues = values.filter((item) => !items.includes(item)); + onChange(newValues); + } else { + const newValues = [...new Set([...values, ...items])]; + onChange(newValues); + } + }, [onChange, values, items, selectedItems]); + + return ( + + {!showOperators && } + {showOperators && ( + + + + + )} + {items.length > 0 ? ( + <> + + {ItemRenderer} + + +
+
+ +
+ + + ) : ( + + )} + + ); +}; + +interface ItemRendererProps extends ListChildComponentProps { + data: { + onCheckedChanged: (option: SelectableValue) => (event: React.FormEvent) => void; + items: SelectableValue[]; + values: SelectableValue[]; + className: string; + }; +} + +function ItemRenderer({ index, style, data: { onCheckedChanged, items, values, className } }: ItemRendererProps) { + const option = items[index]; + const { value, label } = option; + const isChecked = values.find((s) => s.value === value) !== undefined; + + return ( +
+ +
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + filterList: css({ + label: 'filterList', + }), + filterListRow: css({ + label: 'filterListRow', + cursor: 'pointer', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + padding: theme.spacing(0.5), + + ':hover': { + backgroundColor: theme.colors.action.hover, + }, + }), + selectDivider: css({ + label: 'selectDivider', + width: '100%', + borderTop: `1px solid ${theme.colors.border.medium}`, + padding: theme.spacing(0.5, 2), + }), + noValuesLabel: css({ + paddingTop: theme.spacing(1), + }), +}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Filter/FilterPopup.tsx b/packages/grafana-ui/src/components/Table/TableNG/Filter/FilterPopup.tsx new file mode 100644 index 00000000000..364254673a1 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/Filter/FilterPopup.tsx @@ -0,0 +1,167 @@ +import { css, cx } from '@emotion/css'; +import React, { useCallback, useMemo, useState } from 'react'; + +import { Field, GrafanaTheme2, SelectableValue } from '@grafana/data'; + +import { Button, ClickOutsideWrapper, IconButton, Label, Stack } from '../../..'; +import { useStyles2, useTheme2 } from '../../../../themes'; +import { Trans } from '../../../../utils/i18n'; +import { FilterType } from '../types'; + +import { FilterList } from './FilterList'; +import { calculateUniqueFieldValues, getFilteredOptions, valuesToOptions } from './utils'; + +interface Props { + name: string; + rows: any[]; + filterValue: any; + setFilter: (value: any) => void; + onClose: () => void; + field?: Field; + searchFilter: string; + setSearchFilter: (value: string) => void; + operator: SelectableValue; + setOperator: (item: SelectableValue) => void; +} + +export const FilterPopup = ({ + name, + rows, + filterValue, + setFilter, + onClose, + field, + searchFilter, + setSearchFilter, + operator, + setOperator, +}: Props) => { + const theme = useTheme2(); + const uniqueValues = useMemo(() => calculateUniqueFieldValues(rows, field), [rows, field]); + const options = useMemo(() => valuesToOptions(uniqueValues), [uniqueValues]); + const filteredOptions = useMemo(() => getFilteredOptions(options, filterValue), [options, filterValue]); + const [values, setValues] = useState(filteredOptions); + const [matchCase, setMatchCase] = useState(false); + + const onCancel = useCallback((event?: React.MouseEvent) => onClose(), [onClose]); + + const onFilter = useCallback( + (event: React.MouseEvent) => { + if (values.length !== 0) { + // create a Set for faster filtering + const filteredSet = new Set(values.map((item) => item.value)); + + setFilter((filter: FilterType) => ({ + ...filter, + [name]: { filtered: values, filteredSet, searchFilter, operator }, + })); + } else { + setFilter((filter: FilterType) => { + const newFilter = { ...filter }; + delete newFilter[name]; + return newFilter; + }); + } + onClose(); + }, + [setFilter, values, onClose] // eslint-disable-line react-hooks/exhaustive-deps + ); + + const onClearFilter = useCallback( + (event: React.MouseEvent) => { + setFilter((filter: FilterType) => { + const newFilter = { ...filter }; + delete newFilter[name]; + return newFilter; + }); + onClose(); + }, + [setFilter, onClose] // eslint-disable-line react-hooks/exhaustive-deps + ); + + const clearFilterVisible = useMemo(() => filterValue !== undefined, [filterValue]); + const styles = useStyles2(getStyles); + + return ( + + {/* This is just blocking click events from bubbeling and should not have a keyboard interaction. */} + {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */} +
+ + + + + { + setMatchCase((s) => !s); + }} + /> + +
+ + + + + + + + {clearFilterVisible && ( + + + + )} + + +
+ + ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + filterContainer: css({ + label: 'filterContainer', + width: '100%', + minWidth: '250px', + height: '100%', + maxHeight: '400px', + backgroundColor: theme.colors.background.primary, + border: `1px solid ${theme.colors.border.weak}`, + padding: theme.spacing(2), + boxShadow: theme.shadows.z3, + borderRadius: theme.shape.radius.default, + }), + listDivider: css({ + label: 'listDivider', + width: '100%', + borderTop: `1px solid ${theme.colors.border.medium}`, + padding: theme.spacing(0.5, 2), + }), + label: css({ + marginBottom: 0, + }), +}); + +const stopPropagation = (event: React.MouseEvent) => { + event.stopPropagation(); +}; diff --git a/packages/grafana-ui/src/components/Table/TableNG/Filter/utils.ts b/packages/grafana-ui/src/components/Table/TableNG/Filter/utils.ts new file mode 100644 index 00000000000..41f77a188f4 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/Filter/utils.ts @@ -0,0 +1,58 @@ +import { Field, formattedValueToString, SelectableValue } from '@grafana/data'; + +export function calculateUniqueFieldValues(rows: any[], field?: Field) { + if (!field || rows.length === 0) { + return {}; + } + + const set: Record = {}; + + for (let index = 0; index < rows.length; index++) { + const row = rows[index]; + const fieldValue = row[field.name]; + const displayValue = field.display ? field.display(fieldValue) : fieldValue; + const value = field.display ? formattedValueToString(displayValue) : displayValue; + + set[value || '(Blanks)'] = value; + } + + return set; +} + +export function getFilteredOptions(options: SelectableValue[], filterValues?: SelectableValue[]): SelectableValue[] { + if (!filterValues) { + return []; + } + + return options.filter((option) => filterValues.some((filtered) => filtered.value === option.value)); +} + +export function valuesToOptions(unique: Record): SelectableValue[] { + return Object.keys(unique) + .map((key) => ({ value: unique[key], label: key })) + .sort(sortOptions); +} + +function sortOptions(a: SelectableValue, b: SelectableValue): number { + if (a.label === undefined && b.label === undefined) { + return 0; + } + + if (a.label === undefined && b.label !== undefined) { + return -1; + } + + if (a.label !== undefined && b.label === undefined) { + return 1; + } + + if (a.label! < b.label!) { + return -1; + } + + if (a.label! > b.label!) { + return 1; + } + + return 0; +} diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.test.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.test.tsx new file mode 100644 index 00000000000..8c2c53c1b08 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.test.tsx @@ -0,0 +1,1524 @@ +import { render, screen, fireEvent, act } from '@testing-library/react'; + +import { applyFieldOverrides, createTheme, DataFrame, FieldType, toDataFrame, EventBus } from '@grafana/data'; +import { TableCellDisplayMode } from '@grafana/schema'; + +import { PanelContext } from '../../PanelChrome'; + +import { TableNG, onRowHover, onRowLeave } from './TableNG'; + +// Create a basic data frame for testing +const createBasicDataFrame = (): DataFrame => { + const frame = toDataFrame({ + name: 'TestData', + length: 3, + fields: [ + { + name: 'Column A', + type: FieldType.string, + values: ['A1', 'A2', 'A3'], + config: { + custom: { + width: 150, + cellOptions: { + type: TableCellDisplayMode.Auto, + wrapText: false, + }, + }, + }, + // Add display function + display: (value: any) => ({ + text: String(value), + numeric: 0, + color: undefined, + prefix: undefined, + suffix: undefined, + }), + // Add state and getLinks + state: {}, + getLinks: () => [], + }, + { + name: 'Column B', + type: FieldType.number, + values: [1, 2, 3], + config: { + custom: { + width: 150, + cellOptions: { + type: TableCellDisplayMode.Auto, + wrapText: false, + }, + }, + }, + // Add display function + display: (value: any) => ({ + text: String(value), + numeric: Number(value), + color: undefined, + prefix: undefined, + suffix: undefined, + }), + // Add state and getLinks + state: {}, + getLinks: () => [], + }, + ], + }); + + // The applyFieldOverrides should add display processors, but we'll keep our explicit ones too + return applyFieldOverrides({ + data: [frame], + fieldConfig: { + defaults: {}, + overrides: [], + }, + replaceVariables: (value) => value, + timeZone: 'utc', + theme: createTheme(), + })[0]; +}; + +// Create a nested data frame for testing expandable rows +const createNestedDataFrame = (): DataFrame => { + const nestedFrame = toDataFrame({ + name: 'NestedData', + fields: [ + { + name: 'Nested A', + type: FieldType.string, + values: ['N1', 'N2'], + config: { custom: {} }, + }, + { + name: 'Nested B', + type: FieldType.number, + values: [10, 20], + config: { custom: {} }, + }, + ], + }); + + const processedNestedFrame = applyFieldOverrides({ + data: [nestedFrame], + fieldConfig: { + defaults: {}, + overrides: [], + }, + replaceVariables: (value) => value, + timeZone: 'utc', + theme: createTheme(), + })[0]; + + const frame = toDataFrame({ + name: 'TestData', + length: 2, + fields: [ + { + name: 'Column A', + type: FieldType.string, + values: ['A1', 'A2'], + config: { custom: {} }, + }, + { + name: 'Column B', + type: FieldType.number, + values: [1, 2], + config: { custom: {} }, + }, + // Add special fields for nested table functionality + { + name: '__depth', + type: FieldType.number, + values: [0, 0], + config: { custom: { hidden: true } }, + }, + { + name: '__index', + type: FieldType.number, + values: [0, 1], + config: { custom: { hidden: true } }, + }, + { + name: 'Nested frames', + type: FieldType.nestedFrames, + values: [[processedNestedFrame], [processedNestedFrame]], + config: { custom: {} }, + }, + ], + }); + + return applyFieldOverrides({ + data: [frame], + fieldConfig: { + defaults: {}, + overrides: [], + }, + replaceVariables: (value) => value, + timeZone: 'utc', + theme: createTheme(), + })[0]; +}; + +// Create a data frame specifically for testing multi-column sorting +const createSortingTestDataFrame = (): DataFrame => { + const frame = toDataFrame({ + name: 'SortingTestData', + length: 5, + fields: [ + { + name: 'Category', + type: FieldType.string, + values: ['A', 'B', 'A', 'B', 'A'], + config: { + custom: { + width: 150, + cellOptions: { + type: TableCellDisplayMode.Auto, + wrapText: false, + }, + }, + }, + display: (value: any) => ({ + text: String(value), + numeric: 0, + color: undefined, + prefix: undefined, + suffix: undefined, + }), + state: {}, + getLinks: () => [], + }, + { + name: 'Value', + type: FieldType.number, + values: [5, 3, 1, 4, 2], + config: { + custom: { + width: 150, + cellOptions: { + type: TableCellDisplayMode.Auto, + wrapText: false, + }, + }, + }, + display: (value: any) => ({ + text: String(value), + numeric: Number(value), + color: undefined, + prefix: undefined, + suffix: undefined, + }), + state: {}, + getLinks: () => [], + }, + { + name: 'Name', + type: FieldType.string, + values: ['John', 'Jane', 'Bob', 'Alice', 'Charlie'], + config: { + custom: { + width: 150, + cellOptions: { + type: TableCellDisplayMode.Auto, + wrapText: false, + }, + }, + }, + display: (value: any) => ({ + text: String(value), + numeric: 0, + color: undefined, + prefix: undefined, + suffix: undefined, + }), + state: {}, + getLinks: () => [], + }, + ], + }); + + return applyFieldOverrides({ + data: [frame], + fieldConfig: { + defaults: {}, + overrides: [], + }, + replaceVariables: (value) => value, + timeZone: 'utc', + theme: createTheme(), + })[0]; +}; + +// Create a data frame with time field for testing crosshair sharing functionality +const createTimeDataFrame = (): DataFrame => { + const frame = toDataFrame({ + name: 'TimeTestData', + length: 3, + fields: [ + { + name: 'Time', + type: FieldType.time, + values: [ + new Date('2024-03-20T10:00:00Z').getTime(), + new Date('2024-03-20T10:01:00Z').getTime(), + new Date('2024-03-20T10:02:00Z').getTime(), + ], + config: { custom: {} }, + }, + { + name: 'Value', + type: FieldType.number, + values: [1, 2, 3], + config: { custom: {} }, + }, + ], + }); + + return applyFieldOverrides({ + data: [frame], + fieldConfig: { + defaults: {}, + overrides: [], + }, + replaceVariables: (value) => value, + timeZone: 'utc', + theme: createTheme(), + })[0]; +}; + +describe('TableNG', () => { + describe('Basic TableNG rendering', () => { + it('renders a simple table with columns and rows', () => { + const { container } = render( + + ); + + // Check for the data grid container + const dataGridContainer = container.querySelector('[role="grid"]'); + expect(dataGridContainer).toBeInTheDocument(); + + // Check for column headers + const headers = container.querySelectorAll('[role="columnheader"]'); + expect(headers.length).toBe(2); + + // Check for cell values + const cells = container.querySelectorAll('[role="gridcell"]'); + expect(cells.length).toBe(6); // 3 rows x 2 columns + + // Check for specific text content + const expectedContent = ['Column A', 'Column B', 'A1', 'A2', 'A3', '1', '2', '3']; + expectedContent.forEach((text) => { + expect(screen.getByText(text)).toBeInTheDocument(); + }); + }); + }); + + describe('Nested tables', () => { + it('renders table with nested data structure', () => { + const { container } = render( + + ); + + const expectedContent = ['Column A', 'Column B', 'A1', 'A2']; + expectedContent.forEach((text) => { + expect(screen.getByText(text)).toBeInTheDocument(); + }); + + const grid = container.querySelector('[role="grid"]'); + expect(grid).toBeInTheDocument(); + + const expandIcons = container.querySelectorAll('svg[aria-label="Expand row"]'); + expect(expandIcons.length).toBeGreaterThan(0); + }); + + it('expands nested data when clicking expand button', () => { + // Mock scrollIntoView + window.HTMLElement.prototype.scrollIntoView = jest.fn(); + + const { container } = render( + + ); + + // Verify initial state + const expectedContent = ['Column A', 'Column B', 'A1', 'A2']; + expectedContent.forEach((text) => { + expect(screen.getByText(text)).toBeInTheDocument(); + }); + + // Count initial rows + const initialRows = container.querySelectorAll('[role="row"]'); + const initialRowCount = initialRows.length; + + // Find the expand button + const expandButton = container.querySelector('svg[aria-label="Expand row"]'); + expect(expandButton).toBeInTheDocument(); + + // Click the expand button + if (expandButton) { + fireEvent.click(expandButton); + + // After expansion, we should have more rows + const expandedRows = container.querySelectorAll('[role="row"]'); + expect(expandedRows.length).toBeGreaterThan(initialRowCount); + + // Check for nested data by looking for specific cell content + const expectedExpandedContent = ['N1', 'N2']; + expectedExpandedContent.forEach((text) => { + expect(screen.getByText(text)).toBeInTheDocument(); + }); + + // Check if the expanded row has the aria-expanded attribute + const expandedRow = container.querySelector('[aria-expanded="true"]'); + expect(expandedRow).toBeInTheDocument(); + } + }); + }); + + describe('Header options', () => { + it('defaults to showing headers', () => { + const { container } = render( + + ); + + // Check for column headers + const headers = container.querySelectorAll('[role="columnheader"]'); + expect(headers.length).toBe(2); + }); + + it('hides headers when noHeader is true', () => { + const { container } = render( + + ); + + // Get the grid container + const gridContainer = container.querySelector('[role="grid"]'); + expect(gridContainer).toBeInTheDocument(); + + if (gridContainer) { + // Check that the --rdg-header-row-height CSS variable is set to 0px + const computedStyle = window.getComputedStyle(gridContainer); + const headerRowHeight = computedStyle.getPropertyValue('--rdg-header-row-height'); + expect(headerRowHeight).toBe('0px'); + } + + // Cell values should still be visible + expect(screen.getByText('A1')).toBeInTheDocument(); + expect(screen.getByText('1')).toBeInTheDocument(); + }); + }); + + describe('Footer options', () => { + it('defaults to not showing footer', () => { + const { container } = render( + + ); + expect(container.querySelector('.rdg-summary-row')).not.toBeInTheDocument(); + }); + + it('renders footer with aggregations when footerOptions are provided', () => { + const { container } = render( + + ); + + // Check for footer row + const footerRow = container.querySelector('.rdg-summary-row'); + expect(footerRow).toBeInTheDocument(); + + // Sum of Column B values (1+2+3=6) + expect(screen.getByText('6')).toBeInTheDocument(); + }); + + it('renders row count in footer when countRows is true', () => { + const { container } = render( + + ); + + // Check for footer row + const footerRow = container.querySelector('.rdg-summary-row'); + expect(footerRow).toBeInTheDocument(); + + // Get the text content of the footer cells + const footerCells = footerRow?.querySelectorAll('[role="gridcell"]'); + const footerTexts = Array.from(footerCells || []).map((cell) => cell.textContent); + + // The first cell should contain the row count (3 rows) + expect(footerTexts[0]).toBe('Count3'); + + // There should be no other footer cells + expect(footerTexts[1]).toBe(''); + }); + }); + + describe('Pagination', () => { + it('defaults to not showing pagination', () => { + const { container } = render( + + ); + expect(container.querySelector('.table-ng-pagination')).not.toBeInTheDocument(); + }); + + it('shows pagination controls when enabled', () => { + // Create a data frame with many rows + const fields = [ + { + name: 'Index', + type: FieldType.number, + values: Array.from({ length: 100 }, (_, i) => i), + config: { custom: {} }, + }, + { + name: 'Value', + type: FieldType.string, + values: Array.from({ length: 100 }, (_, i) => `Value ${i}`), + config: { custom: {} }, + }, + ]; + + const largeFrame = toDataFrame({ name: 'LargeData', fields }); + const processedFrame = applyFieldOverrides({ + data: [largeFrame], + fieldConfig: { + defaults: {}, + overrides: [], + }, + replaceVariables: (value) => value, + timeZone: 'utc', + theme: createTheme(), + })[0]; + + const { container } = render( + + ); + + // Check for pagination controls using the specific class name + const pagination = container.querySelector('.table-ng-pagination'); + expect(pagination).toBeInTheDocument(); + + // Verify that pagination summary text is shown + const paginationText = container.textContent; + expect(paginationText).toContain('of 100 rows'); + }); + + it('navigates between pages when pagination controls are clicked', async () => { + // Create a data frame with many rows + const fields = [ + { + name: 'Index', + type: FieldType.number, + values: Array.from({ length: 100 }, (_, i) => i), + config: { custom: {} }, + display: (v: number) => ({ text: String(v), numeric: Number(v) }), + }, + { + name: 'Value', + type: FieldType.string, + values: Array.from({ length: 100 }, (_, i) => `Value ${i}`), + config: { custom: {} }, + display: (v: string) => ({ text: String(v), numeric: 0 }), + }, + ]; + + const largeFrame = toDataFrame({ name: 'LargeData', fields }); + const processedFrame = applyFieldOverrides({ + data: [largeFrame], + fieldConfig: { + defaults: {}, + overrides: [], + }, + replaceVariables: (value) => value, + timeZone: 'utc', + theme: createTheme(), + })[0]; + + const { container } = render( + + ); + + // Get all cell content on the first page + const initialCells = container.querySelectorAll('[role="gridcell"]'); + const initialCellTexts = Array.from(initialCells).map((cell) => cell.textContent); + + // Store the first page's first visible row index + const firstPageFirstIndex = initialCellTexts[0]; + + // Store the first page content for comparison + const firstPageContent = container.textContent || ''; + + // Find and click next page button + const nextButton = container.querySelector('[aria-label="next page" i], [aria-label*="Next" i]'); + expect(nextButton).toBeInTheDocument(); + + if (nextButton) { + // Click to go to the next page + fireEvent.click(nextButton); + + // Get all cell content on the second page + const newCells = container.querySelectorAll('[role="gridcell"]'); + const newCellTexts = Array.from(newCells).map((cell) => cell.textContent); + + // The first cell on the second page should be different from the first page + const secondPageFirstIndex = newCellTexts[0]; + expect(secondPageFirstIndex).not.toBe(firstPageFirstIndex); + + // The content should have changed + const secondPageContent = container.textContent || ''; + expect(secondPageContent).not.toBe(firstPageContent); + + // Check that the pagination summary shows we're on a different page + // The format appears to be "X - Y of Z rows" where X and Y are the row range + expect(container.textContent).toMatch(/\d+ - \d+ of 100 rows/); + + // Verify that the pagination summary has changed + const paginationSummary = container.querySelector('.paginationSummary, [class*="paginationSummary"]'); + if (paginationSummary) { + const summaryText = paginationSummary.textContent || ''; + expect(summaryText).toContain('of 100 rows'); + } else { + // If we can't find the pagination summary by class, just check the container text + expect(container.textContent).toContain('of 100 rows'); + } + } + }); + }); + + describe('Sorting', () => { + it('allows sorting when clicking on column headers', async () => { + // Mock scrollIntoView + window.HTMLElement.prototype.scrollIntoView = jest.fn(); + + const { container } = render( + + ); + + // Ensure there are column headers + const columnHeader = container.querySelector('[role="columnheader"]'); + expect(columnHeader).toBeInTheDocument(); + + // Find the sort button within the first header + if (columnHeader) { + // Store the initial state of the header + const initialSortAttribute = columnHeader.getAttribute('aria-sort'); + + // Look for a button inside the header + const sortButton = columnHeader.querySelector('button') || columnHeader; + + // Click the sort button + fireEvent.click(sortButton); + + // After clicking, the header should have an aria-sort attribute + const newSortAttribute = columnHeader.getAttribute('aria-sort'); + + // The sort attribute should have changed + expect(newSortAttribute).not.toBe(initialSortAttribute); + + // The sort attribute should be either 'ascending' or 'descending' + expect(['ascending', 'descending']).toContain(newSortAttribute); + + // Also verify the data is sorted by checking cell values + const cells = container.querySelectorAll('[role="gridcell"]'); + const firstColumnCells = Array.from(cells).filter((_, index) => index % 2 === 0); + + // Get the text content of the first column cells + const cellValues = firstColumnCells.map((cell) => cell.textContent); + + // Verify we have values to check + expect(cellValues.length).toBeGreaterThan(0); + + // Verify the values are in sorted order based on the aria-sort attribute + const sortedValues = [...cellValues].sort(); + + if (newSortAttribute === 'ascending') { + expect(JSON.stringify(cellValues)).toBe(JSON.stringify(sortedValues)); + } else if (newSortAttribute === 'descending') { + expect(JSON.stringify(cellValues)).toBe(JSON.stringify([...sortedValues].reverse())); + } + } + }); + + it('cycles through ascending, descending, and no sort states', () => { + // Mock scrollIntoView + window.HTMLElement.prototype.scrollIntoView = jest.fn(); + + const { container } = render( + + ); + + // Get the first column header + const columnHeader = container.querySelector('[role="columnheader"]'); + expect(columnHeader).toBeInTheDocument(); + + if (columnHeader) { + const sortButton = columnHeader.querySelector('button') || columnHeader; + + // Initial state - no sort + expect(columnHeader.getAttribute('aria-sort')).toBeNull(); + + // First click - should sort ascending + fireEvent.click(sortButton); + expect(columnHeader.getAttribute('aria-sort')).toBe('ascending'); + + // Second click - should sort descending + fireEvent.click(sortButton); + expect(columnHeader.getAttribute('aria-sort')).toBe('descending'); + + // Third click - should remove sort + fireEvent.click(sortButton); + expect(columnHeader.getAttribute('aria-sort')).toBeNull(); + } + }); + + it('supports multi-column sorting with shift key', () => { + // Mock scrollIntoView + window.HTMLElement.prototype.scrollIntoView = jest.fn(); + + const { container } = render( + + ); + + // Get all column headers + const columnHeaders = container.querySelectorAll('[role="columnheader"]'); + expect(columnHeaders.length).toBe(3); // Category, Value, Name + + // Extract text from all cells before sorting + const getCellTextContent = () => { + const cells = container.querySelectorAll('[role="gridcell"]'); + const rows: string[][] = []; + let currentRow: string[] = []; + + // Group cells into rows (3 cells per row) + Array.from(cells).forEach((cell, index) => { + currentRow.push(cell.textContent || ''); + if ((index + 1) % 3 === 0) { + rows.push([...currentRow]); + currentRow = []; + } + }); + + return rows; + }; + + // Initial unsorted data + const initialRows = getCellTextContent(); + expect(initialRows.length).toBe(5); + + // Log the initial unsorted data order for reference + // The data should be in the original order: + // ['A', '5', 'John'], ['B', '3', 'Jane'], ['A', '1', 'Bob'], ['B', '4', 'Alice'], ['A', '2', 'Charlie'] + expect(initialRows[0][0]).toBe('A'); + expect(initialRows[0][1]).toBe('5'); + expect(initialRows[0][2]).toBe('John'); + + expect(initialRows[1][0]).toBe('B'); + expect(initialRows[1][1]).toBe('3'); + expect(initialRows[1][2]).toBe('Jane'); + + expect(initialRows[2][0]).toBe('A'); + expect(initialRows[2][1]).toBe('1'); + expect(initialRows[2][2]).toBe('Bob'); + + expect(initialRows[3][0]).toBe('B'); + expect(initialRows[3][1]).toBe('4'); + expect(initialRows[3][2]).toBe('Alice'); + + expect(initialRows[4][0]).toBe('A'); + expect(initialRows[4][1]).toBe('2'); + expect(initialRows[4][2]).toBe('Charlie'); + + // First column button (Category) + const categoryColumnButton = columnHeaders[0].querySelector('button') || columnHeaders[0]; + // Second column button (Value) + const valueColumnButton = columnHeaders[1].querySelector('button') || columnHeaders[1]; + + // 1. First sort by Category (ascending) + fireEvent.click(categoryColumnButton); + + // Check data is sorted by Category + const categoryOnlySortedRows = getCellTextContent(); + expect(categoryOnlySortedRows.length).toBe(5); + + // First 3 rows should be 'A' category, then 2 rows of 'B' category + // The expected order should be: + // A rows: (still unsorted within categories) + // ['A', '5', 'John'], ['A', '1', 'Bob'], ['A', '2', 'Charlie'] + // B rows: (still unsorted within categories) + // ['B', '3', 'Jane'], ['B', '4', 'Alice'] + + // Check Category A rows + expect(categoryOnlySortedRows[0][0]).toBe('A'); + expect(categoryOnlySortedRows[1][0]).toBe('A'); + expect(categoryOnlySortedRows[2][0]).toBe('A'); + + // Check Category B rows + expect(categoryOnlySortedRows[3][0]).toBe('B'); + expect(categoryOnlySortedRows[4][0]).toBe('B'); + + // Find all values in Category A to check if they contain the expected values + // (order within category may vary as we haven't sorted by Value yet) + const categoryAValues = categoryOnlySortedRows.slice(0, 3).map((row) => row[1]); + expect(categoryAValues).toContain('5'); + expect(categoryAValues).toContain('1'); + expect(categoryAValues).toContain('2'); + + // Find all values in Category B to check if they contain the expected values + const categoryBValues = categoryOnlySortedRows.slice(3, 5).map((row) => row[1]); + expect(categoryBValues).toContain('3'); + expect(categoryBValues).toContain('4'); + + // 2. Now add second sort column (Value) with shift key + fireEvent.click(valueColumnButton, { shiftKey: true }); + + // Check data is sorted by Category and then by Value + const multiSortedRows = getCellTextContent(); + expect(multiSortedRows.length).toBe(5); + + // Now the rows should be perfectly ordered by Category, then by Value (ascending) + // Expected order: + // ['A', '1', 'Bob'], ['A', '2', 'Charlie'], ['A', '5', 'John'] + // ['B', '3', 'Jane'], ['B', '4', 'Alice'] + + // Check Category A rows with ascending Value + expect(multiSortedRows[0][0]).toBe('A'); + expect(multiSortedRows[0][1]).toBe('1'); + expect(multiSortedRows[0][2]).toBe('Bob'); + + expect(multiSortedRows[1][0]).toBe('A'); + expect(multiSortedRows[1][1]).toBe('2'); + expect(multiSortedRows[1][2]).toBe('Charlie'); + + expect(multiSortedRows[2][0]).toBe('A'); + expect(multiSortedRows[2][1]).toBe('5'); + expect(multiSortedRows[2][2]).toBe('John'); + + // Check Category B rows with ascending Value + expect(multiSortedRows[3][0]).toBe('B'); + expect(multiSortedRows[3][1]).toBe('3'); + expect(multiSortedRows[3][2]).toBe('Jane'); + + expect(multiSortedRows[4][0]).toBe('B'); + expect(multiSortedRows[4][1]).toBe('4'); + expect(multiSortedRows[4][2]).toBe('Alice'); + + // 3. Change Value sort direction to descending + fireEvent.click(valueColumnButton, { shiftKey: true }); + + // Check data is sorted by Category (asc) and then by Value (desc) + const multiSortedRowsDesc = getCellTextContent(); + expect(multiSortedRowsDesc.length).toBe(5); + + // Now the rows should be ordered by Category, then by Value (descending) + // Expected order: + // ['A', '5', 'John'], ['A', '2', 'Charlie'], ['A', '1', 'Bob'] + // ['B', '4', 'Alice'], ['B', '3', 'Jane'] + + // Check Category A rows with descending Value + expect(multiSortedRowsDesc[0][0]).toBe('A'); + expect(multiSortedRowsDesc[0][1]).toBe('5'); + expect(multiSortedRowsDesc[0][2]).toBe('John'); + + expect(multiSortedRowsDesc[1][0]).toBe('A'); + expect(multiSortedRowsDesc[1][1]).toBe('2'); + expect(multiSortedRowsDesc[1][2]).toBe('Charlie'); + + expect(multiSortedRowsDesc[2][0]).toBe('A'); + expect(multiSortedRowsDesc[2][1]).toBe('1'); + expect(multiSortedRowsDesc[2][2]).toBe('Bob'); + + // Check Category B rows with descending Value + expect(multiSortedRowsDesc[3][0]).toBe('B'); + expect(multiSortedRowsDesc[3][1]).toBe('4'); + expect(multiSortedRowsDesc[3][2]).toBe('Alice'); + + expect(multiSortedRowsDesc[4][0]).toBe('B'); + expect(multiSortedRowsDesc[4][1]).toBe('3'); + expect(multiSortedRowsDesc[4][2]).toBe('Jane'); + + // 4. Test removing the secondary sort by clicking a third time + fireEvent.click(valueColumnButton, { shiftKey: true }); + + // The data should still be sorted by Category only + const singleSortRows = getCellTextContent(); + + // First 3 rows should still be 'A' category, but values might be in original order + expect(singleSortRows[0][0]).toBe('A'); + expect(singleSortRows[1][0]).toBe('A'); + expect(singleSortRows[2][0]).toBe('A'); + + // Last 2 rows should still be 'B' category + expect(singleSortRows[3][0]).toBe('B'); + expect(singleSortRows[4][0]).toBe('B'); + }); + + it('correctly sorts different data types', () => { + // Create a data frame with different data types + const mixedDataFrame = toDataFrame({ + name: 'MixedData', + fields: [ + { + name: 'String', + type: FieldType.string, + values: ['C', 'A', 'B'], + config: { custom: {} }, + display: (v: string) => ({ text: v, numeric: 0 }), + }, + { + name: 'Number', + type: FieldType.number, + values: [3, 1, 2], + config: { custom: {} }, + display: (v: number) => ({ text: String(v), numeric: v }), + }, + ], + }); + + const processedFrame = applyFieldOverrides({ + data: [mixedDataFrame], + fieldConfig: { defaults: {}, overrides: [] }, + replaceVariables: (value) => value, + timeZone: 'utc', + theme: createTheme(), + })[0]; + + const { container } = render( + + ); + + // Get column headers + const columnHeaders = container.querySelectorAll('[role="columnheader"]'); + + // Test string column sorting + const stringColumnButton = columnHeaders[0].querySelector('button') || columnHeaders[0]; + fireEvent.click(stringColumnButton); + + // Get cell values after sorting + let cells = container.querySelectorAll('[role="gridcell"]'); + let stringColumnCells = Array.from(cells).filter((_, index) => index % 2 === 0); + let stringValues = stringColumnCells.map((cell) => cell.textContent); + + // Verify string values are sorted alphabetically + expect(stringValues).toEqual(['A', 'B', 'C']); + + // Test number column sorting + const numberColumnButton = columnHeaders[1].querySelector('button') || columnHeaders[1]; + fireEvent.click(numberColumnButton); + + // Get cell values after sorting + cells = container.querySelectorAll('[role="gridcell"]'); + let numberColumnCells = Array.from(cells).filter((_, index) => index % 2 === 1); + let numberValues = numberColumnCells.map((cell) => cell.textContent); + + // Verify number values are sorted numerically + expect(numberValues).toEqual(['1', '2', '3']); + }); + }); + + describe('Filtering', () => { + it('filters rows based on text filter', () => { + const baseFrame = createBasicDataFrame(); + // Create a filter function that only shows rows with A1 + const filteredFrame = { + ...baseFrame, + length: 1, + fields: createBasicDataFrame().fields.map((field) => ({ + ...field, + values: field.name === 'Column A' ? ['A1'] : field.name === 'Column B' ? [1] : field.values, + })), + }; + + // First render with unfiltered data + const { container, rerender } = render( + + ); + + // Check initial row count + const initialRows = container.querySelectorAll('[role="row"]'); + const initialRowCount = initialRows.length - 1; // Subtract header row + expect(initialRowCount).toBe(3); // Our basic frame has 3 rows + + // Rerender with filtered data + rerender(); + + // Check filtered row count + const filteredRows = container.querySelectorAll('[role="row"]'); + const filteredRowCount = filteredRows.length - 1; // Subtract header row + + // Should only show one row (with A1) + expect(filteredRowCount).toBe(1); + + // Verify the visible row contains "A1" + const visibleCells = container.querySelectorAll('[role="gridcell"]'); + const cellTexts = Array.from(visibleCells).map((cell) => cell.textContent); + expect(cellTexts).toContain('A1'); + expect(cellTexts).not.toContain('A2'); + expect(cellTexts).not.toContain('A3'); + }); + + it('filters rows based on numeric filter', () => { + // Create a filtered frame with only rows where Column B > 1 + const baseFrame = createBasicDataFrame(); + const filteredFrame = { + ...baseFrame, + length: 2, + fields: baseFrame.fields.map((field) => ({ + ...field, + values: + field.name === 'Column A' ? ['A2', 'A3'] : field.name === 'Column B' ? [2, 3] : field.values.slice(1, 3), + })), + }; + + // First render with unfiltered data + const { container, rerender } = render( + + ); + + // Check initial row count + const initialRows = container.querySelectorAll('[role="row"]'); + const initialRowCount = initialRows.length - 1; // Subtract header row + expect(initialRowCount).toBe(3); + + // Rerender with filtered data + rerender(); + + // Check filtered row count + const filteredRows = container.querySelectorAll('[role="row"]'); + const filteredRowCount = filteredRows.length - 1; // Subtract header row + expect(filteredRowCount).toBe(2); + + // Verify the visible rows contain the expected values + const visibleCells = container.querySelectorAll('[role="gridcell"]'); + const cellTexts = Array.from(visibleCells).map((cell) => cell.textContent); + expect(cellTexts).toContain('A2'); + expect(cellTexts).toContain('A3'); + expect(cellTexts).not.toContain('A1'); + expect(cellTexts).not.toContain('1'); + }); + + it('updates footer calculations when rows are filtered', () => { + // Create a filtered frame with only the first row + const baseFrame = createBasicDataFrame(); + const filteredFrame = { + ...baseFrame, + length: 1, + fields: baseFrame.fields.map((field) => ({ + ...field, + values: field.name === 'Column A' ? ['A1'] : field.name === 'Column B' ? [1] : field.values.slice(0, 1), + })), + }; + + // Render with unfiltered data and footer options + const { container, rerender } = render( + + ); + + // Check initial footer sum (1+2+3=6) + const initialFooter = container.querySelector('.rdg-summary-row'); + expect(initialFooter).toBeInTheDocument(); + + // Get the text content of the footer cells + const initialFooterCells = initialFooter?.querySelectorAll('[role="gridcell"]'); + const initialFooterTexts = Array.from(initialFooterCells || []).map((cell) => cell.textContent); + + // The second cell should contain the sum (6) + expect(initialFooterTexts[1]).toBe('6'); + + // Rerender with filtered data + rerender( + + ); + + // Check filtered footer sum (should be 1) + const filteredFooter = container.querySelector('.rdg-summary-row'); + expect(filteredFooter).toBeInTheDocument(); + + // Get the text content of the footer cells + const filteredFooterCells = filteredFooter?.querySelectorAll('[role="gridcell"]'); + const filteredFooterTexts = Array.from(filteredFooterCells || []).map((cell) => cell.textContent); + + // The second cell should contain the sum (1) + expect(filteredFooterTexts[1]).toBe('1'); + }); + + it('filters rows with case-insensitive text matching', () => { + // Create a case-insensitive filtered frame (filtering for 'a1' should match 'A1') + const baseFrame = createBasicDataFrame(); + const filteredFrame = { + ...baseFrame, + length: 1, + fields: baseFrame.fields.map((field) => ({ + ...field, + values: field.name === 'Column A' ? ['A1'] : field.name === 'Column B' ? [1] : field.values.slice(0, 1), + })), + }; + + // First render with unfiltered data + const { container, rerender } = render( + + ); + + // Rerender with filtered data + rerender(); + + // Check filtered row count + const filteredRows = container.querySelectorAll('[role="row"]'); + const filteredRowCount = filteredRows.length - 1; // Subtract header row + expect(filteredRowCount).toBe(1); + + // Verify the visible row contains "A1" + const visibleCells = container.querySelectorAll('[role="gridcell"]'); + const cellTexts = Array.from(visibleCells).map((cell) => cell.textContent); + expect(cellTexts).toContain('A1'); + expect(cellTexts).not.toContain('A2'); + }); + }); + + describe('Resizing', () => { + it('calls onColumnResize when column is resized', () => { + const onColumnResize = jest.fn(); + + const { container } = render( + + ); + + // Find resize handle + const resizeHandles = container.querySelectorAll('.rdg-header-row > [role="columnheader"] .rdg-resizer'); + + if (resizeHandles.length > 0) { + // Simulate resize by triggering mousedown, mousemove, mouseup + fireEvent.mouseDown(resizeHandles[0]); + fireEvent.mouseMove(resizeHandles[0], { clientX: 250 }); + fireEvent.mouseUp(resizeHandles[0]); + + // Check that onColumnResize was called + expect(onColumnResize).toHaveBeenCalled(); + } + }); + }); + + describe('Text wrapping', () => { + it('defaults to not wrapping text', () => { + const { container } = render( + + ); + + const cells = container.querySelectorAll('[role="gridcell"]'); + const cellStyles = window.getComputedStyle(cells[0]); + expect(cellStyles.getPropertyValue('white-space')).toBe('nowrap'); + }); + + it('applies text wrapping styles when wrapText is true', () => { + // Create a frame with text wrapping enabled + const frame = createBasicDataFrame(); + frame.fields.forEach((field) => { + if (field.config?.custom) { + field.config.custom.cellOptions = { + ...field.config.custom.cellOptions, + wrapText: true, + }; + } + }); + + const { container } = render( + + ); + + // Check for cells with wrap styling + const cells = container.querySelectorAll('[role="gridcell"]'); + const cellStyles = window.getComputedStyle(cells[0]); + + // In the getStyles function, when textWrap is true, whiteSpace is set to 'break-spaces' + expect(cellStyles.getPropertyValue('white-space')).toBe('break-spaces'); + }); + }); + + describe('Context menu', () => { + beforeEach(() => { + // Mock ResizeObserver + global.ResizeObserver = class ResizeObserver { + constructor(callback: any) { + // Store the callback + this.callback = callback; + } + callback: any; + observe() { + // Do nothing + } + unobserve() { + // Do nothing + } + disconnect() { + // Do nothing + } + }; + + window.HTMLElement.prototype.scrollIntoView = jest.fn(); + }); + + it('should show context menu on right-click', async () => { + const { container } = render( + + ); + + const cell = container.querySelector('[role="gridcell"]'); + expect(cell).toBeInTheDocument(); + + // Trigger context menu directly on the cell element + if (cell) { + fireEvent.contextMenu(cell); + } + + // Check that context menu is shown + const menu = await screen.findByRole('menu'); + expect(menu).toBeInTheDocument(); + + // Check for the Inspect value menu item + const menuItem = await screen.findByText('Inspect value'); + expect(menuItem).toBeInTheDocument(); + }); + }); + + describe('Cell inspection', () => { + it('shows inspect icon when hovering over a cell with inspection enabled', () => { + const fieldConfig = { + defaults: { + custom: { + inspect: true, + cellOptions: { + wrapText: false, + }, + }, + }, + overrides: [], + }; + + // Render the component + const { container } = render( + + ); + + // Find a cell to hover over + const cell = container.querySelector('[role="gridcell"]'); + expect(cell).toBeInTheDocument(); + + if (cell) { + // Find the first div inside the cell (the actual content container) + const cellContent = cell.querySelector('div'); + expect(cellContent).toBeInTheDocument(); + + if (cellContent) { + // Trigger mouse enter on the cell content + fireEvent.mouseEnter(cellContent); + + // Look for the inspect icon + const inspectIcon = container.querySelector('[aria-label="Inspect value"]'); + expect(inspectIcon).toBeInTheDocument(); + } + } + }); + }); + + describe('Accessibility', () => { + it('has proper ARIA attributes for accessibility', () => { + const { container } = render( + + ); + + // Check that the table has a grid role + const grid = container.querySelector('[role="grid"]'); + expect(grid).toBeInTheDocument(); + + // Check for row and column headers with proper roles + const rows = container.querySelectorAll('[role="row"]'); + expect(rows.length).toBeGreaterThan(0); + + const columnHeaders = container.querySelectorAll('[role="columnheader"]'); + expect(columnHeaders.length).toBeGreaterThan(0); + + // Check for grid cells + const cells = container.querySelectorAll('[role="gridcell"]'); + expect(cells.length).toBeGreaterThan(0); + }); + }); + + describe('Cell display modes', () => { + it('renders color background cells correctly', () => { + // Create a frame with color background cells + const frame = createBasicDataFrame(); + frame.fields[0].config.custom = { + ...frame.fields[0].config.custom, + cellOptions: { + type: TableCellDisplayMode.ColorBackground, + wrapText: false, + mode: TableCellDisplayMode.BasicGauge, + applyToRow: false, + }, + }; + + // Add color to the display values + const originalDisplay = frame.fields[0].display; + const expectedColor = '#ff0000'; // Red color + frame.fields[0].display = (value: any) => { + const displayValue = originalDisplay ? originalDisplay(value) : { text: String(value), numeric: 0 }; + return { + ...displayValue, + color: expectedColor, + }; + }; + + const { container } = render(); + + // Find cells in the first column + const cells = container.querySelectorAll('[role="gridcell"]'); + expect(cells.length).toBeGreaterThan(0); + + // Check the first div inside the cell for style attributes + const div = cells[0].querySelectorAll('div')[0]; + const styleAttr = window.getComputedStyle(div); + + // Expected color is red + expect(styleAttr.background).toBe('rgb(255, 0, 0)'); + }); + + it('renders color text cells correctly', () => { + // Create a frame with color text cells + const frame = createBasicDataFrame(); + const expectedColor = '#ff0000'; // Red color + + frame.fields[0].config.custom = { + ...frame.fields[0].config.custom, + cellOptions: { + type: TableCellDisplayMode.ColorText, + wrapText: false, + }, + }; + + // Add color to the display values + const originalDisplay = frame.fields[0].display; + frame.fields[0].display = (value: any) => { + const displayValue = originalDisplay ? originalDisplay(value) : { text: String(value), numeric: 0 }; + return { + ...displayValue, + color: expectedColor, + }; + }; + + const { container } = render(); + + // Find cells in the first column + const cells = container.querySelectorAll('[role="gridcell"]'); + expect(cells.length).toBeGreaterThan(0); + + // Check the first div inside the cell for style attributes + const div = cells[0].querySelectorAll('div')[0]; + const computedStyle = window.getComputedStyle(div); + + // Expected color is red + expect(computedStyle.color).toBe('rgb(255, 0, 0)'); + }); + }); + + describe('Row hover functionality for shared crosshair', () => { + const mockEventBus: EventBus = { + publish: jest.fn(), + getStream: jest.fn(), + subscribe: jest.fn(), + removeAllListeners: jest.fn(), + newScopedBus: jest.fn(), + }; + + const mockPanelContext: PanelContext = { + eventsScope: 'test', + eventBus: mockEventBus, + onSeriesColorChange: jest.fn(), + onToggleSeriesVisibility: jest.fn(), + canAddAnnotations: jest.fn(), + canEditAnnotations: jest.fn(), + canDeleteAnnotations: jest.fn(), + onAnnotationCreate: jest.fn(), + onAnnotationUpdate: jest.fn(), + onAnnotationDelete: jest.fn(), + onSelectRange: jest.fn(), + onAddAdHocFilter: jest.fn(), + canEditThresholds: false, + showThresholds: false, + onThresholdsChange: jest.fn(), + instanceState: {}, + onInstanceStateChange: jest.fn(), + onToggleLegendSort: jest.fn(), + onUpdateData: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should publish DataHoverEvent when hovering over a row with time field', () => { + const frame = createTimeDataFrame(); + const idx = 1; + + onRowHover(idx, mockPanelContext, frame, true); + + expect(mockEventBus.publish).toHaveBeenCalledWith( + expect.objectContaining({ + payload: { + point: { + time: new Date('2024-03-20T10:01:00Z').getTime(), + }, + }, + type: 'data-hover', + }) + ); + }); + + it('should not publish DataHoverEvent when enableSharedCrosshair is false', () => { + const frame = createTimeDataFrame(); + const idx = 1; + + onRowHover(idx, mockPanelContext, frame, false); + + expect(mockEventBus.publish).not.toHaveBeenCalled(); + }); + + it('should not publish DataHoverEvent when time field is not present', () => { + const frame = createBasicDataFrame(); + const idx = 1; + + onRowHover(idx, mockPanelContext, frame, true); + + expect(mockEventBus.publish).not.toHaveBeenCalled(); + }); + + it('should publish DataHoverClearEvent when leaving a row', () => { + onRowLeave(mockPanelContext, true); + + expect(mockEventBus.publish).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'data-hover-clear', + }) + ); + }); + + it('should not publish DataHoverClearEvent when enableSharedCrosshair is false', () => { + onRowLeave(mockPanelContext, false); + + expect(mockEventBus.publish).not.toHaveBeenCalled(); + }); + }); + describe('scroll position persistence', () => { + it('should persist scroll position after revId change', () => { + const data = createBasicDataFrame(); + const { rerender } = render(); + + // Find the DataGrid element + const dataGrid = screen.getByRole('grid'); + + // Simulate scrolling + act(() => { + fireEvent.scroll(dataGrid, { + target: { + scrollLeft: 100, + scrollTop: 50, + }, + }); + }); + + // Rerender with the same data but different fieldConfig to trigger revId change + rerender( + + ); + + // Verify scroll position was restored + expect(dataGrid.scrollLeft).toBe(100); + expect(dataGrid.scrollTop).toBe(50); + }); + }); +}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx new file mode 100644 index 00000000000..29b25853bcd --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx @@ -0,0 +1,948 @@ +import 'react-data-grid/lib/styles.css'; +import { css } from '@emotion/css'; +import { useMemo, useState, useLayoutEffect, useCallback, useRef, useEffect } from 'react'; +import DataGrid, { RenderCellProps, RenderRowProps, Row, SortColumn, DataGridHandle } from 'react-data-grid'; +import { useMeasure } from 'react-use'; + +import { + DataFrame, + DataHoverClearEvent, + DataHoverEvent, + Field, + fieldReducers, + FieldType, + formattedValueToString, + getDefaultTimeRange, + GrafanaTheme2, + ReducerID, +} from '@grafana/data'; +import { TableCellDisplayMode } from '@grafana/schema'; + +import { useStyles2, useTheme2 } from '../../../themes'; +import { Trans } from '../../../utils/i18n'; +import { ContextMenu } from '../../ContextMenu/ContextMenu'; +import { MenuItem } from '../../Menu/MenuItem'; +import { Pagination } from '../../Pagination/Pagination'; +import { PanelContext, usePanelContext } from '../../PanelChrome'; +import { TableCellInspector, TableCellInspectorMode } from '../TableCellInspector'; + +import { HeaderCell } from './Cells/HeaderCell'; +import { RowExpander } from './Cells/RowExpander'; +import { TableCellNG } from './Cells/TableCellNG'; +import { COLUMN, TABLE } from './constants'; +import { + TableNGProps, + FilterType, + TableRow, + TableSummaryRow, + ColumnTypes, + TableColumnResizeActionCallback, + TableColumn, + TableFieldOptionsType, + ScrollPosition, + CellColors, +} from './types'; +import { + frameToRecords, + getCellColors, + getComparator, + getDefaultRowHeight, + getFooterItemNG, + getFooterStyles, + getIsNestedTable, + getRowHeight, + getTextAlign, + handleSort, + MapFrameToGridOptions, + shouldTextOverflow, +} from './utils'; + +export function TableNG(props: TableNGProps) { + const { + cellHeight, + enablePagination, + enableVirtualization = true, + fieldConfig, + footerOptions, + height, + noHeader, + onColumnResize, + width, + data, + enableSharedCrosshair, + showTypeIcons, + } = props; + + /* ------------------------------- Local state ------------------------------ */ + const [revId, setRevId] = useState(0); + const [contextMenuProps, setContextMenuProps] = useState<{ + rowIdx?: number; + value: string; + mode?: TableCellInspectorMode.code | TableCellInspectorMode.text; + top?: number; + left?: number; + } | null>(null); + const [isInspecting, setIsInspecting] = useState(false); + const [isContextMenuOpen, setIsContextMenuOpen] = useState(false); + const [filter, setFilter] = useState({}); + const [page, setPage] = useState(0); + // This state will trigger re-render for recalculating row heights + const [, setResizeTrigger] = useState(0); + const [, setReadyForRowHeightCalc] = useState(false); + const [sortColumns, setSortColumns] = useState([]); + const [expandedRows, setExpandedRows] = useState([]); + const [isNestedTable, setIsNestedTable] = useState(false); + const scrollPositionRef = useRef({ x: 0, y: 0 }); + const [hasScroll, setHasScroll] = useState(false); + + /* ------------------------------- Local refs ------------------------------- */ + const crossFilterOrder = useRef([]); + const crossFilterRows = useRef>({}); + const headerCellRefs = useRef>({}); + // TODO: This ref persists sortColumns between renders. setSortColumns is still used to trigger re-render + const sortColumnsRef = useRef(sortColumns); + const prevProps = useRef(props); + const calcsRef = useRef([]); + const [paginationWrapperRef, { height: paginationHeight }] = useMeasure(); + + const textWrap = fieldConfig?.defaults?.custom?.cellOptions?.wrapText ?? false; + + const theme = useTheme2(); + const styles = useStyles2(getStyles, textWrap); + const panelContext = usePanelContext(); + + const isFooterVisible = Boolean(footerOptions?.show && footerOptions.reducer?.length); + const isCountRowsSet = Boolean( + footerOptions?.countRows && + footerOptions.reducer && + footerOptions.reducer.length && + footerOptions.reducer[0] === ReducerID.count + ); + const tableRef = useRef(null); + + /* --------------------------------- Effects -------------------------------- */ + useEffect(() => { + // TODO: there is a use case when adding a new column to the table doesn't update the table + if ( + prevProps.current.data.fields.length !== props.data.fields.length || + prevProps.current.fieldConfig?.overrides !== fieldConfig?.overrides || + prevProps.current.fieldConfig?.defaults !== fieldConfig?.defaults + ) { + setRevId(revId + 1); + } + prevProps.current = props; + }, [props, revId, fieldConfig?.overrides, fieldConfig?.defaults]); // eslint-disable-line react-hooks/exhaustive-deps + + useLayoutEffect(() => { + if (!isContextMenuOpen) { + return; + } + + function onClick(event: MouseEvent) { + setIsContextMenuOpen(false); + } + + addEventListener('click', onClick); + + return () => { + removeEventListener('click', onClick); + }; + }, [isContextMenuOpen]); + + useEffect(() => { + const hasNestedFrames = getIsNestedTable(props.data); + setIsNestedTable(hasNestedFrames); + }, [props.data]); + + useEffect(() => { + const el = tableRef.current; + if (el) { + const gridElement = el?.element; + if (gridElement) { + setHasScroll( + gridElement.scrollHeight > gridElement.clientHeight || gridElement.scrollWidth > gridElement.clientWidth + ); + } + } + }, []); + + // TODO: this is a hack to force the column width to update when the fieldConfig changes + const columnWidth = useMemo(() => { + setRevId(revId + 1); + return fieldConfig?.defaults?.custom?.width || 'auto'; + }, [fieldConfig]); // eslint-disable-line react-hooks/exhaustive-deps + + // Create off-screen canvas for measuring rows for virtualized rendering + // This line is like this because Jest doesn't have OffscreenCanvas mocked + // nor is it a part of the jest-canvas-mock package + let osContext = null; + if (window.OffscreenCanvas !== undefined) { + // The canvas size is defined arbitrarily + // As we never actually visualize rendered content + // from the offscreen canvas, only perform text measurements + osContext = new OffscreenCanvas(256, 1024).getContext('2d'); + } + + // Set font property using theme info + // This will make text measurement accurate + if (osContext !== undefined && osContext !== null) { + osContext.font = `${theme.typography.fontSize}px ${theme.typography.body.fontFamily}`; + } + + const defaultRowHeight = getDefaultRowHeight(theme, cellHeight); + const defaultLineHeight = theme.typography.body.lineHeight * theme.typography.fontSize; + const panelPaddingHeight = theme.components.panel.padding * theme.spacing.gridSize * 2; + + /* ------------------------------ Rows & Columns ----------------------------- */ + const rows = useMemo(() => frameToRecords(props.data), [frameToRecords, props.data]); // eslint-disable-line react-hooks/exhaustive-deps + + // Create a map of column key to column type + const columnTypes = useMemo(() => { + return props.data.fields.reduce((acc, field) => { + acc[field.name] = field.type; + return acc; + }, {} as ColumnTypes); + }, [props.data.fields]); + + const getDisplayedValue = (row: TableRow, key: string) => { + const field = props.data.fields.find((field) => field.name === key)!; + const displayedValue = formattedValueToString(field.display!(row[key])); + return displayedValue; + }; + + // Filter rows + const filteredRows = useMemo(() => { + const filterValues = Object.entries(filter); + if (filterValues.length === 0) { + // reset cross filter order + crossFilterOrder.current = []; + return rows; + } + + // Update crossFilterOrder + const filterKeys = new Set(filterValues.map(([key]) => key)); + filterKeys.forEach((key) => { + if (!crossFilterOrder.current.includes(key)) { + // Each time a filter is added or removed, it is always a single filter. + // When adding a new filter, it is always appended to the end, maintaining the order. + crossFilterOrder.current.push(key); + } + }); + // Remove keys from crossFilterOrder that are no longer present in the current filter values + crossFilterOrder.current = crossFilterOrder.current.filter((key) => filterKeys.has(key)); + + // reset crossFilterRows + crossFilterRows.current = {}; + + return rows.filter((row) => { + for (const [key, value] of filterValues) { + const displayedValue = getDisplayedValue(row, key); + if (!value.filteredSet.has(displayedValue)) { + return false; + } + // collect rows for crossFilter + if (!crossFilterRows.current[key]) { + crossFilterRows.current[key] = [row]; + } else { + crossFilterRows.current[key].push(row); + } + } + return true; + }); + }, [rows, filter, props.data.fields]); // eslint-disable-line react-hooks/exhaustive-deps + + // Sort rows + const sortedRows = useMemo(() => { + const comparators = sortColumns.map(({ columnKey }) => getComparator(columnTypes[columnKey])); + const sortDirs = sortColumns.map(({ direction }) => (direction === 'ASC' ? 1 : -1)); + + if (sortColumns.length === 0) { + return filteredRows; + } + + return filteredRows.slice().sort((a, b) => { + let result = 0; + let sortIndex = 0; + + for (const { columnKey } of sortColumns) { + const compare = comparators[sortIndex]; + result = sortDirs[sortIndex] * compare(a[columnKey], b[columnKey]); + + if (result !== 0) { + break; + } + + sortIndex += 1; + } + + return result; + }); + }, [filteredRows, sortColumns, columnTypes]); + + // Paginated rows + // TODO consolidate calculations into pagination wrapper component and only use when needed + const numRows = sortedRows.length; + // calculate number of rowsPerPage based on height stack + let headerCellHeight = TABLE.MAX_CELL_HEIGHT; + if (noHeader) { + headerCellHeight = 0; + } else if (!noHeader && Object.keys(headerCellRefs.current).length > 0) { + headerCellHeight = headerCellRefs.current[Object.keys(headerCellRefs.current)[0]].getBoundingClientRect().height; + } + let rowsPerPage = Math.floor( + (height - headerCellHeight - TABLE.SCROLL_BAR_WIDTH - paginationHeight - panelPaddingHeight) / defaultRowHeight + ); + // if footer calcs are on, remove one row per page + if (isFooterVisible) { + rowsPerPage -= 1; + } + if (rowsPerPage < 1) { + // avoid 0 or negative rowsPerPage + rowsPerPage = 1; + } + const numberOfPages = Math.ceil(numRows / rowsPerPage); + if (page > numberOfPages) { + // resets pagination to end + setPage(numberOfPages - 1); + } + // calculate row range for pagination summary display + const itemsRangeStart = page * rowsPerPage + 1; + let displayedEnd = itemsRangeStart + rowsPerPage - 1; + if (displayedEnd > numRows) { + displayedEnd = numRows; + } + const smallPagination = width < TABLE.PAGINATION_LIMIT; + + const paginatedRows = useMemo(() => { + const pageOffset = page * rowsPerPage; + return sortedRows.slice(pageOffset, pageOffset + rowsPerPage); + }, [rows, sortedRows, page, rowsPerPage]); // eslint-disable-line react-hooks/exhaustive-deps + + useMemo(() => { + calcsRef.current = props.data.fields.map((field, index) => { + if (field.state?.calcs) { + delete field.state?.calcs; + } + if (isCountRowsSet) { + return index === 0 ? `${sortedRows.length}` : ''; + } + if (index === 0) { + const footerCalcReducer = footerOptions?.reducer?.[0]; + return footerCalcReducer ? fieldReducers.get(footerCalcReducer).name : ''; + } + return getFooterItemNG(sortedRows, field, footerOptions); + }); + }, [sortedRows, props.data.fields, footerOptions, isCountRowsSet]); // eslint-disable-line react-hooks/exhaustive-deps + + const onCellExpand = (rowIdx: number) => { + if (!expandedRows.includes(rowIdx)) { + setExpandedRows([...expandedRows, rowIdx]); + } else { + const currentExpandedRows = expandedRows; + const indexToRemove = currentExpandedRows.indexOf(rowIdx); + if (indexToRemove > -1) { + currentExpandedRows.splice(indexToRemove, 1); + setExpandedRows(currentExpandedRows); + } + } + setResizeTrigger((prev) => prev + 1); + }; + + const columns = useMemo( + () => + mapFrameToDataGrid({ + frame: props.data, + calcsRef, + options: { + columnTypes, + columnWidth, + crossFilterOrder, + crossFilterRows, + defaultLineHeight, + defaultRowHeight, + expandedRows, + filter, + headerCellRefs, + isCountRowsSet, + osContext, + // INFO: sortedRows is for correct row indexing for cell background coloring + rows: sortedRows, + setContextMenuProps, + setFilter, + setIsInspecting, + setSortColumns, + sortColumnsRef, + styles, + textWrap, + theme, + showTypeIcons, + ...props, + }, + handlers: { + onCellExpand, + onColumnResize: onColumnResize!, + }, + // Adjust table width to account for the scroll bar width + availableWidth: width - (hasScroll ? TABLE.SCROLL_BAR_WIDTH + TABLE.SCROLL_BAR_MARGIN : 0), + }), + [props.data, calcsRef, filter, expandedRows, expandedRows.length, footerOptions, width, hasScroll, sortedRows] // eslint-disable-line react-hooks/exhaustive-deps + ); + + // This effect needed to set header cells refs before row height calculation + useLayoutEffect(() => { + setReadyForRowHeightCalc(Object.keys(headerCellRefs.current).length > 0); + }, [columns]); + + const renderMenuItems = () => { + return ( + <> + { + setIsInspecting(true); + }} + className={styles.menuItem} + /> + + ); + }; + + const calculateRowHeight = useCallback( + (row: TableRow) => { + // Logic for sub-tables + if (Number(row.__depth) === 1 && !expandedRows.includes(Number(row.__index))) { + return 0; + } else if (Number(row.__depth) === 1 && expandedRows.includes(Number(row.__index))) { + const headerCount = row?.data?.meta?.custom?.noHeader ? 0 : 1; + return defaultRowHeight * (row.data?.length ?? 0 + headerCount); // TODO this probably isn't very robust + } + return getRowHeight( + row, + columnTypes, + headerCellRefs, + osContext, + defaultLineHeight, + defaultRowHeight, + TABLE.CELL_PADDING + ); + }, + [expandedRows, defaultRowHeight, columnTypes, headerCellRefs, osContext, defaultLineHeight] + ); + + const handleScroll = (event: React.UIEvent) => { + const target = event.target as HTMLDivElement; + scrollPositionRef.current = { + x: target.scrollLeft, + y: target.scrollTop, + }; + }; + + // Restore scroll position after re-renders + useEffect(() => { + if (tableRef.current?.element) { + tableRef.current.element.scrollLeft = scrollPositionRef.current.x; + tableRef.current.element.scrollTop = scrollPositionRef.current.y; + } + }, [revId]); + + return ( + <> + + ref={tableRef} + className={styles.dataGrid} + // Default to true, overridden to false for testing + enableVirtualization={enableVirtualization} + key={`DataGrid${revId}`} + rows={enablePagination ? paginatedRows : sortedRows} + columns={columns} + headerRowHeight={noHeader ? 0 : undefined} + defaultColumnOptions={{ + sortable: true, + resizable: true, + }} + rowHeight={textWrap || isNestedTable ? calculateRowHeight : defaultRowHeight} + // TODO: This doesn't follow current table behavior + style={{ width, height: height - (enablePagination ? paginationHeight : 0) }} + renderers={{ + renderRow: (key, props) => + myRowRenderer(key, props, expandedRows, panelContext, data, enableSharedCrosshair ?? false), + }} + onScroll={handleScroll} + onCellContextMenu={({ row, column }, event) => { + event.preventGridDefault(); + // Do not show the default context menu + event.preventDefault(); + + const cellValue = row[column.key]; + setContextMenuProps({ + // rowIdx: rows.indexOf(row), + value: String(cellValue ?? ''), + top: event.clientY, + left: event.clientX, + }); + setIsContextMenuOpen(true); + }} + // sorting + sortColumns={sortColumns} + // footer + // TODO figure out exactly how this works - some array needs to be here for it to render regardless of renderSummaryCell() + bottomSummaryRows={isFooterVisible ? [{}] : undefined} + onColumnResize={() => { + // NOTE: This method is called continuously during the column resize drag operation, + // providing the current column width. There is no separate event for the end of the drag operation. + if (textWrap) { + // This is needed only when textWrap is enabled + // TODO: this is a hack to force rowHeight re-calculation + setResizeTrigger((prev) => prev + 1); + } + }} + /> + + {enablePagination && ( +
+ { + setPage(toPage - 1); + }} + /> + {!smallPagination && ( +
+ + {{ itemsRangeStart }} - {{ displayedEnd }} of {{ numRows }} rows + +
+ )} +
+ )} + + {isContextMenuOpen && ( + + )} + + {isInspecting && ( + { + setIsInspecting(false); + setContextMenuProps(null); + }} + /> + )} + + ); +} + +export function mapFrameToDataGrid({ + frame, + calcsRef, + options, + handlers, + availableWidth, +}: { + frame: DataFrame; + calcsRef: React.MutableRefObject; + options: MapFrameToGridOptions; + handlers: { onCellExpand: (rowIdx: number) => void; onColumnResize: TableColumnResizeActionCallback }; + availableWidth: number; +}): TableColumn[] { + const { + columnTypes, + crossFilterOrder, + crossFilterRows, + defaultLineHeight, + defaultRowHeight, + expandedRows, + filter, + headerCellRefs, + isCountRowsSet, + osContext, + rows, + setContextMenuProps, + setFilter, + setIsInspecting, + setSortColumns, + sortColumnsRef, + styles, + textWrap, + fieldConfig, + theme, + timeRange, + getActions, + showTypeIcons, + } = options; + const { onCellExpand, onColumnResize } = handlers; + + const columns: TableColumn[] = []; + const hasNestedFrames = getIsNestedTable(frame); + + const cellInspect = fieldConfig?.defaults?.custom?.inspect ?? false; + const filterable = fieldConfig?.defaults?.custom?.filterable ?? false; + + // If nested frames, add expansion control column + if (hasNestedFrames) { + const expanderField: Field = { + name: '', + type: FieldType.other, + config: {}, + values: [], + }; + columns.push({ + key: 'expanded', + name: '', + field: expanderField, + cellClass: styles.cell, + colSpan(args) { + return args.type === 'ROW' && Number(args.row.__depth) === 1 ? frame.fields.length : 1; + }, + renderCell: ({ row }) => { + // TODO add TableRow type extension to include row depth and optional data + if (Number(row.__depth) === 0) { + const rowIdx = Number(row.__index); + return ( + onCellExpand(rowIdx)} + isExpanded={expandedRows.includes(rowIdx)} + /> + ); + } + // If it's a child, render entire DataGrid at first column position + let expandedColumns: TableColumn[] = []; + let expandedRecords: TableRow[] = []; + + // Type guard to check if data exists as it's optional + if (row.data) { + expandedColumns = mapFrameToDataGrid({ + frame: row.data, + calcsRef, + options: { ...options }, + handlers: { onCellExpand, onColumnResize }, + availableWidth: availableWidth - COLUMN.EXPANDER_WIDTH, + }); + expandedRecords = frameToRecords(row.data); + } + + // TODO add renderHeaderCell HeaderCell's here and handle all features + return ( + + rows={expandedRecords} + columns={expandedColumns} + rowHeight={defaultRowHeight} + style={{ height: '100%', overflow: 'visible', marginLeft: COLUMN.EXPANDER_WIDTH }} + headerRowHeight={row.data?.meta?.custom?.noHeader ? 0 : undefined} + /> + ); + }, + width: COLUMN.EXPANDER_WIDTH, + minWidth: COLUMN.EXPANDER_WIDTH, + }); + + availableWidth -= COLUMN.EXPANDER_WIDTH; + } + + // Row background color function + let rowBg: Function | undefined = undefined; + for (const field of frame.fields) { + const fieldOptions = field.config.custom; + const cellOptionsExist = fieldOptions !== undefined && fieldOptions.cellOptions !== undefined; + + if ( + cellOptionsExist && + fieldOptions.cellOptions.type === TableCellDisplayMode.ColorBackground && + fieldOptions.cellOptions.applyToRow + ) { + rowBg = (rowIndex: number): CellColors => { + const display = field.display!(field.values.get(rows[rowIndex].__index)); + const colors = getCellColors(theme, fieldOptions.cellOptions, display); + return colors; + }; + } + } + + let fieldCountWithoutWidth = 0; + frame.fields.map((field, fieldIndex) => { + if (field.type === FieldType.nestedFrames) { + // Don't render nestedFrames type field + return; + } + const fieldTableOptions: TableFieldOptionsType = field.config.custom || {}; + const key = field.name; + const justifyColumnContent = getTextAlign(field); + const footerStyles = getFooterStyles(justifyColumnContent); + + // current/old table width logic calculations + if (fieldTableOptions.width) { + availableWidth -= fieldTableOptions.width; + } else { + fieldCountWithoutWidth++; + } + + // Add a column for each field + columns.push({ + key, + name: field.name, + field, + cellClass: styles.cell, + renderCell: (props: RenderCellProps): JSX.Element => { + const { row, rowIdx } = props; + const cellType = field.config?.custom?.cellOptions?.type ?? TableCellDisplayMode.Auto; + const value = row[key]; + // Cell level rendering here + return ( + + shouldTextOverflow( + key, + row, + columnTypes, + headerCellRefs, + osContext, + defaultLineHeight, + defaultRowHeight, + TABLE.CELL_PADDING, + textWrap, + cellInspect, + cellType + ) + } + setIsInspecting={setIsInspecting} + setContextMenuProps={setContextMenuProps} + cellInspect={cellInspect} + getActions={getActions} + rowBg={rowBg} + /> + ); + }, + renderSummaryCell: () => { + if (isCountRowsSet && fieldIndex === 0) { + return ( +
+ + Count + + {calcsRef.current[fieldIndex]} +
+ ); + } + return
{calcsRef.current[fieldIndex]}
; + }, + renderHeaderCell: ({ column, sortDirection }): JSX.Element => ( + + handleSort(columnKey, direction, isMultiSort, setSortColumns, sortColumnsRef) + } + direction={sortDirection} + justifyContent={justifyColumnContent} + filter={filter} + setFilter={setFilter} + filterable={filterable} + onColumnResize={onColumnResize} + headerCellRefs={headerCellRefs} + crossFilterOrder={crossFilterOrder} + crossFilterRows={crossFilterRows} + showTypeIcons={showTypeIcons} + /> + ), + width: fieldTableOptions.width, + minWidth: fieldTableOptions.minWidth || COLUMN.DEFAULT_WIDTH, + }); + }); + + // INFO: This loop calculates the width for each column in less than a millisecond. + let sharedWidth = availableWidth / fieldCountWithoutWidth; + + // First pass: Assign minimum widths to columns that need it + columns.forEach((column) => { + if (!column.width && column.minWidth! > sharedWidth) { + column.width = column.minWidth; + availableWidth -= column.width!; + fieldCountWithoutWidth -= 1; + } + }); + + // Recalculate shared width after assigning minimum widths + sharedWidth = availableWidth / fieldCountWithoutWidth; + + // Second pass: Assign shared width to remaining columns + columns.forEach((column) => { + if (!column.width) { + column.width = sharedWidth; + } + column.minWidth = COLUMN.MIN_WIDTH; // Ensure min-width is always set + }); + + return columns; +} + +export function myRowRenderer( + key: React.Key, + props: RenderRowProps, + expandedRows: number[], + panelContext: PanelContext, + data: DataFrame, + enableSharedCrosshair: boolean +): React.ReactNode { + // Let's render row level things here! + // i.e. we can look at row styles and such here + const { row } = props; + const rowIdx = Number(row.__index); + const isExpanded = expandedRows.includes(rowIdx); + + // Don't render non expanded child rows + if (Number(row.__depth) === 1 && !isExpanded) { + return null; + } + + // Add aria-expanded to parent rows that have nested data + if (row.data) { + return ; + } + + return ( + onRowHover(rowIdx, panelContext, data, enableSharedCrosshair)} + onMouseLeave={() => onRowLeave(panelContext, enableSharedCrosshair)} + /> + ); +} + +export function onRowHover(idx: number, panelContext: PanelContext, frame: DataFrame, enableSharedCrosshair: boolean) { + if (!enableSharedCrosshair) { + return; + } + + const timeField: Field = frame!.fields.find((f) => f.type === FieldType.time)!; + + if (!timeField) { + return; + } + + panelContext.eventBus.publish( + new DataHoverEvent({ + point: { + time: timeField.values[idx], + }, + }) + ); +} + +export function onRowLeave(panelContext: PanelContext, enableSharedCrosshair: boolean) { + if (!enableSharedCrosshair) { + return; + } + + panelContext.eventBus.publish(new DataHoverClearEvent()); +} + +const getStyles = (theme: GrafanaTheme2, textWrap: boolean) => ({ + dataGrid: css({ + '--rdg-background-color': theme.colors.background.primary, + '--rdg-header-background-color': theme.colors.background.primary, + '--rdg-border-color': 'transparent', + '--rdg-color': theme.colors.text.primary, + '&:hover': { + '--rdg-row-hover-background-color': theme.colors.emphasize(theme.colors.action.hover, 0.6), + }, + + // If we rely solely on borderInlineEnd which is added from data grid, we + // get a small gap where the gridCell borders meet the column header borders. + // To avoid this, we can unset borderInlineEnd and set borderRight instead. + '.rdg-cell': { + borderInlineEnd: 'unset', + borderRight: `1px solid ${theme.colors.border.medium}`, + + '&:last-child': { + borderRight: 'none', + }, + }, + + '.rdg-summary-row': { + backgroundColor: theme.colors.background.primary, + '--rdg-summary-border-color': theme.colors.border.medium, + + '.rdg-cell': { + borderRight: 'none', + }, + }, + + // Due to stylistic choices, we do not want borders on the column headers + // other than the bottom border. + 'div[role=columnheader]': { + borderBottom: `1px solid ${theme.colors.border.medium}`, + borderInlineEnd: 'unset', + + '.r1y6ywlx7-0-0-beta-46': { + '&:hover': { + borderRight: `3px solid ${theme.colors.text.link}`, + }, + }, + }, + + '::-webkit-scrollbar': { + width: TABLE.SCROLL_BAR_WIDTH, + height: TABLE.SCROLL_BAR_WIDTH, + }, + '::-webkit-scrollbar-thumb': { + backgroundColor: 'rgba(204, 204, 220, 0.16)', + borderRadius: '4px', + }, + '::-webkit-scrollbar-track': { + background: 'transparent', + }, + '::-webkit-scrollbar-corner': { + backgroundColor: 'transparent', + }, + }), + menuItem: css({ + maxWidth: '200px', + }), + cell: css({ + '--rdg-border-color': theme.colors.border.medium, + borderLeft: 'none', + whiteSpace: `${textWrap ? 'break-spaces' : 'nowrap'}`, + wordWrap: 'break-word', + overflow: 'hidden', + textOverflow: 'ellipsis', + + // Reset default cell styles for custom cell component styling + paddingInline: '0', + }), + paginationContainer: css({ + alignItems: 'center', + display: 'flex', + justifyContent: 'center', + marginTop: '8px', + width: '100%', + }), + paginationSummary: css({ + color: theme.colors.text.secondary, + fontSize: theme.typography.bodySmall.fontSize, + display: 'flex', + justifyContent: 'flex-end', + padding: theme.spacing(0, 1, 0, 2), + }), +}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/constants.ts b/packages/grafana-ui/src/components/Table/TableNG/constants.ts new file mode 100644 index 00000000000..9f370f290de --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/constants.ts @@ -0,0 +1,16 @@ +/** Column width and sizing configuration */ +export const COLUMN = { + DEFAULT_WIDTH: 150, + EXPANDER_WIDTH: 50, + // This will need to eventually change to 36 + MIN_WIDTH: 50, +}; + +/** Table layout and display constants */ +export const TABLE = { + CELL_PADDING: 6, + MAX_CELL_HEIGHT: 48, + PAGINATION_LIMIT: 750, + SCROLL_BAR_WIDTH: 8, + SCROLL_BAR_MARGIN: 2, +}; diff --git a/packages/grafana-ui/src/components/Table/TableNG/types.ts b/packages/grafana-ui/src/components/Table/TableNG/types.ts new file mode 100644 index 00000000000..7bdc40fce38 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/types.ts @@ -0,0 +1,231 @@ +import { Property } from 'csstype'; +import { Column } from 'react-data-grid'; + +import { + DataFrame, + Field, + GrafanaTheme2, + KeyValue, + TimeRange, + FieldConfigSource, + ActionModel, + InterpolateFunction, + FieldType, +} from '@grafana/data'; +import { TableCellOptions, TableCellHeight, TableFieldOptions } from '@grafana/schema'; + +import { TableCellInspectorMode } from '../TableCellInspector'; + +export const FILTER_FOR_OPERATOR = '='; +export const FILTER_OUT_OPERATOR = '!='; + +export type AdHocFilterOperator = typeof FILTER_FOR_OPERATOR | typeof FILTER_OUT_OPERATOR; +export type AdHocFilterItem = { key: string; value: string; operator: AdHocFilterOperator }; +export type TableFilterActionCallback = (item: AdHocFilterItem) => void; +export type TableColumnResizeActionCallback = (fieldDisplayName: string, width: number) => void; +export type TableSortByActionCallback = (state: TableSortByFieldState[]) => void; +export type FooterItem = Array> | string | undefined; + +export type GetActionsFunction = ( + frame: DataFrame, + field: Field, + rowIndex: number, + replaceVariables?: InterpolateFunction +) => ActionModel[]; + +export type TableFieldOptionsType = Omit & { + cellOptions: TableCellOptions; + headerComponent?: React.ComponentType; +}; + +export type FilterType = { + [key: string]: { + filteredSet: Set; + }; +}; + +/* ----------------------------- Table specific types ----------------------------- */ +export interface TableSummaryRow { + [columnName: string]: string | number | undefined; +} + +export interface TableColumn extends Column { + key: string; // Unique identifier used by DataGrid + name: string; // Display name in header + field: Field; // Grafana field data/config + width?: number | string; // Column width + minWidth?: number; // Min width constraint + cellClass?: string; // CSS styling +} + +// Possible values for table cells based on field types +export type TableCellValue = + | string // FieldType.string, FieldType.enum + | number // FieldType.number + | boolean // FieldType.boolean + | Date // FieldType.time + | DataFrame // For nested data + | DataFrame[] // For nested frames + | undefined; // For undefined values + +export interface TableRow { + // Required metadata properties + __depth: number; + __index: number; + + // Nested table properties + data?: DataFrame; + 'Nested frames'?: DataFrame[]; + + // Generic typing for column values + [columnName: string]: TableCellValue; +} + +export interface CustomHeaderRendererProps { + field: Field; + defaultContent: React.ReactNode; +} + +export interface TableSortByFieldState { + displayName: string; + desc?: boolean; +} + +export interface TableFooterCalc { + show: boolean; + reducer?: string[]; // Make this optional + fields?: string[]; + enablePagination?: boolean; + countRows?: boolean; +} + +export interface BaseTableProps { + ariaLabel?: string; + data: DataFrame; + width: number; + height: number; + maxHeight?: number; + /** Minimal column width specified in pixels */ + columnMinWidth?: number; + noHeader?: boolean; + showTypeIcons?: boolean; + resizable?: boolean; + initialSortBy?: TableSortByFieldState[]; + onColumnResize?: TableColumnResizeActionCallback; + onSortByChange?: TableSortByActionCallback; + onCellFilterAdded?: TableFilterActionCallback; + footerOptions?: TableFooterCalc; + footerValues?: FooterItem[]; + enablePagination?: boolean; + cellHeight?: TableCellHeight; + /** @alpha Used by SparklineCell when provided */ + timeRange?: TimeRange; + enableSharedCrosshair?: boolean; + // The index of the field value that the table will initialize scrolled to + initialRowIndex?: number; + fieldConfig?: FieldConfigSource; + getActions?: GetActionsFunction; + replaceVariables?: InterpolateFunction; + // Used solely for testing as RTL can't correctly render the table otherwise + enableVirtualization?: boolean; +} + +/* ---------------------------- Table cell props ---------------------------- */ +export interface TableNGProps extends BaseTableProps {} + +export interface TableCellNGProps { + cellInspect: boolean; + field: Field; + frame: DataFrame; + getActions?: GetActionsFunction; + height: number; + justifyContent: Property.JustifyContent; + rowIdx: number; + setContextMenuProps: (props: { value: string; top?: number; left?: number; mode?: TableCellInspectorMode }) => void; + setIsInspecting: (isInspecting: boolean) => void; + shouldTextOverflow: () => boolean; + theme: GrafanaTheme2; + timeRange: TimeRange; + value: TableCellValue; + rowBg: Function | undefined; +} + +/* ------------------------- Specialized Cell Props ------------------------- */ +export interface RowExpanderNGProps { + height: number; + onCellExpand: () => void; + isExpanded?: boolean; +} + +export interface SparklineCellProps { + field: Field; + justifyContent: Property.JustifyContent; + rowIdx: number; + theme: GrafanaTheme2; + timeRange: TimeRange; + value: TableCellValue; + width: number; +} + +export interface BarGaugeCellProps { + field: Field; + height: number; + rowIdx: number; + theme: GrafanaTheme2; + value: TableCellValue; + width: number; + timeRange: TimeRange; +} + +export interface ImageCellProps { + cellOptions: TableCellOptions; + field: Field; + height: number; + justifyContent: Property.JustifyContent; + value: TableCellValue; + rowIdx: number; +} + +export interface JSONCellProps { + justifyContent: Property.JustifyContent; + value: TableCellValue; + field: Field; + rowIdx: number; +} + +export interface DataLinksCellProps { + field: Field; + rowIdx: number; +} + +export interface ActionCellProps { + actions?: ActionModel[]; +} + +export interface CellColors { + textColor?: string; + bgColor?: string; + bgHoverColor?: string; +} + +export interface AutoCellProps { + value: TableCellValue; + field: Field; + justifyContent: Property.JustifyContent; + rowIdx: number; + cellOptions: TableCellOptions; +} + +// Comparator for sorting table values +export type Comparator = (a: TableCellValue, b: TableCellValue) => number; + +// Type for converting a DataFrame into an array of TableRows +export type FrameToRowsConverter = (frame: DataFrame) => TableRow[]; + +// Type for mapping column names to their field types +export type ColumnTypes = Record; + +export interface ScrollPosition { + x: number; + y: number; +} diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts new file mode 100644 index 00000000000..31612c1bbd0 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts @@ -0,0 +1,1795 @@ +import { SortColumn } from 'react-data-grid'; + +import { + createDataFrame, + createTheme, + DataFrame, + DisplayProcessor, + DisplayValue, + Field, + FieldType, + GrafanaTheme2, + LinkModel, + ValueLinkConfig, +} from '@grafana/data'; +import { + BarGaugeDisplayMode, + TableCellBackgroundDisplayMode, + TableCellDisplayMode, + TableCellHeight, +} from '@grafana/schema'; + +import { Trans } from '../../../utils/i18n'; +import { PanelContext } from '../../PanelChrome'; + +import { mapFrameToDataGrid, myRowRenderer } from './TableNG'; +import { COLUMN, TABLE } from './constants'; +import { TableColumn } from './types'; +import { + convertRGBAToHex, + extractPixelValue, + frameToRecords, + getAlignmentFactor, + getCellColors, + getCellHeight, + getCellLinks, + getCellOptions, + getComparator, + getDefaultRowHeight, + getFooterItemNG, + getFooterStyles, + getIsNestedTable, + getRowHeight, + getTextAlign, + handleSort, + isTextCell, + migrateTableDisplayModeToCellOptions, + shouldTextOverflow, +} from './utils'; + +const data = createDataFrame({ + fields: [ + { + name: 'Time', + type: FieldType.time, + values: [], + config: { + custom: { + width: undefined, // For width distribution testing + displayMode: 'auto', + }, + }, + }, + { + name: 'Value', + type: FieldType.number, + values: [], + display: ((v: any) => ({ + text: String(v), + numeric: v, + color: undefined, + prefix: undefined, + suffix: undefined, + })) as DisplayProcessor, + config: { + custom: { + width: 100, + displayMode: 'basic', + }, + }, + }, + { + name: 'Message', + type: FieldType.string, + values: [], + config: { + custom: { + align: 'center', + }, + }, + }, + ], + meta: { + custom: { + noHeader: false, // For header rendering tests + }, + }, +}); + +const calcsRef = { current: [] }; +const headerCellRefs = { current: {} }; +const crossFilterOrder = { current: [] }; +const crossFilterRows = { current: {} }; +const sortColumnsRef = { current: [] }; + +const mockOptions = { + osContext: null, + rows: [], + setContextMenuProps: () => {}, + setFilter: () => {}, + setIsInspecting: () => {}, + data, + width: 800, + height: 600, + fieldConfig: { + defaults: { + custom: { + width: 'auto', + minWidth: COLUMN.MIN_WIDTH, + cellOptions: { + wrapText: false, + }, + }, + }, + overrides: [ + { + matcher: { id: 'byName', options: 'Value' }, + properties: [{ id: 'width', value: 100 }], + }, + ], + }, + columnTypes: {}, + columnWidth: 'auto', + defaultLineHeight: 40, + defaultRowHeight: 40, + expandedRows: [], + filter: {}, + headerCellRefs, + crossFilterOrder, + crossFilterRows, + isCountRowsSet: false, + styles: { cell: '' }, + theme: createTheme(), + setSortColumns: () => {}, + sortColumnsRef, + textWrap: false, +}; + +describe('TableNG utils', () => { + describe('mapFrameToDataGrid', () => { + it('take data frame and return array of columns', () => { + const columns = mapFrameToDataGrid({ + frame: data, + calcsRef, + options: mockOptions, + handlers: { onCellExpand: () => {}, onColumnResize: () => {} }, + availableWidth: mockOptions.width, + }); + + // Test column structure + expect(columns).toHaveLength(3); + + // Test Time column + expect(columns[0]).toMatchObject({ + key: 'Time', + name: 'Time', + field: expect.objectContaining({ + name: 'Time', + type: FieldType.time, + }), + }); + + // Test Value column with custom width + expect(columns[1]).toMatchObject({ + key: 'Value', + name: 'Value', + width: 100, + field: expect.objectContaining({ + name: 'Value', + type: FieldType.number, + }), + }); + + // Test Message column alignment + expect(columns[2]).toMatchObject({ + key: 'Message', + name: 'Message', + field: expect.objectContaining({ + name: 'Message', + type: FieldType.string, + config: expect.objectContaining({ + custom: expect.objectContaining({ + align: 'center', + }), + }), + }), + }); + }); + }); + + describe('column building', () => { + it('should build basic column structure', () => { + const columns = mapFrameToDataGrid({ + frame: data, + calcsRef, + options: mockOptions, + handlers: { onCellExpand: () => {}, onColumnResize: () => {} }, + availableWidth: mockOptions.width, + }); + + expect(columns).toHaveLength(3); + columns.forEach((column: TableColumn) => { + expect(column).toHaveProperty('key'); + expect(column).toHaveProperty('name'); + expect(column).toHaveProperty('field'); + expect(column).toHaveProperty('cellClass'); + expect(column).toHaveProperty('renderCell'); + expect(column).toHaveProperty('renderHeaderCell'); + }); + }); + + it('should handle column width configurations', () => { + const columns = mapFrameToDataGrid({ + frame: data, + calcsRef, + options: mockOptions, + handlers: { onCellExpand: () => {}, onColumnResize: () => {} }, + availableWidth: mockOptions.width, + }); + + // Default width + expect(columns[0].width).toBe(350); + // Explicit width from field config + expect(columns[1].width).toBe(100); + // Default width with min width + expect(columns[2].minWidth).toBe(COLUMN.MIN_WIDTH); + }); + + it('should handle cell alignment', () => { + const columns = mapFrameToDataGrid({ + frame: data, + calcsRef, + options: mockOptions, + handlers: { onCellExpand: () => {}, onColumnResize: () => {} }, + availableWidth: mockOptions.width, + }); + + const messageColumn = columns[2]; + expect(messageColumn.field.config.custom.align).toBe('center'); + }); + + it('should handle footer/summary rows', () => { + const options = { + ...mockOptions, + isCountRowsSet: true, + }; + + const columns = mapFrameToDataGrid({ + frame: data, + calcsRef: { current: ['3', '', ''] }, + options, + handlers: { onCellExpand: () => {}, onColumnResize: () => {} }, + availableWidth: mockOptions.width, + }); + + // First column should show count + const firstCell = columns[0].renderSummaryCell?.({ + row: { __depth: 0, __index: 0 }, + column: { + ...columns[0], + frozen: false, + idx: 0, + parent: undefined, + level: 0, + sortable: true, + minWidth: 100, + draggable: true, + renderCell: () => null, + renderHeaderCell: () => null, + resizable: true, + width: 100, + maxWidth: undefined, + headerCellClass: undefined, + summaryCellClass: undefined, + }, + tabIndex: 0, + }); + + expect(firstCell).toBeDefined(); + + // Check the div structure and content + const divElement = firstCell as JSX.Element; + expect(divElement.props.style).toEqual({ display: 'flex', justifyContent: 'space-between' }); + + // Check that we have two spans with correct content + const [countSpan, valueSpan] = divElement.props.children; + expect(countSpan.type).toBe('span'); + expect(countSpan.props.children.type).toBe(Trans); + expect(countSpan.props.children.props.i18nKey).toBe('grafana-ui.table.count'); + expect(valueSpan.props.children).toBe('3'); + }); + }); + + describe('nested frames', () => { + const nestedData = createDataFrame({ + fields: [ + { name: 'Time', type: FieldType.time, values: [1, 2] }, + { name: 'Value', type: FieldType.number, values: [10, 20] }, + { + name: 'Nested frames', + type: FieldType.nestedFrames, + values: [ + [ + createDataFrame({ + fields: [ + { name: 'Nested Time', type: FieldType.time, values: [3] }, + { name: 'Nested Value', type: FieldType.number, values: [30] }, + ], + }), + ], + ], + }, + ], + }); + + it('should add expander column for nested frames', () => { + const columns = mapFrameToDataGrid({ + frame: nestedData, + calcsRef, + options: mockOptions, + handlers: { onCellExpand: () => {}, onColumnResize: () => {} }, + availableWidth: mockOptions.width, + }); + + // First column should be expander + expect(columns[0]).toMatchObject({ + key: 'expanded', + name: '', + width: COLUMN.EXPANDER_WIDTH, + minWidth: COLUMN.EXPANDER_WIDTH, + }); + }); + + it('should not render nested frame type fields', () => { + const columns = mapFrameToDataGrid({ + frame: nestedData, + calcsRef, + options: mockOptions, + handlers: { onCellExpand: () => {}, onColumnResize: () => {} }, + availableWidth: mockOptions.width, + }); + + // Should only have expander + Time + Value (not Nested frames column) + expect(columns).toHaveLength(3); + + // No column should be of type nestedFrames + const hasNestedFrameColumn = columns.some((col: TableColumn) => col.field.type === FieldType.nestedFrames); + expect(hasNestedFrameColumn).toBe(false); + }); + + it('should render nested frame data when expanded', () => { + const expandedRows = [0]; + const columns = mapFrameToDataGrid({ + frame: nestedData, + calcsRef, + options: { ...mockOptions, expandedRows }, + handlers: { onCellExpand: () => {}, onColumnResize: () => {} }, + availableWidth: mockOptions.width, + }); + + // Get the rendered content of first row's expander cell + const expanderCell = columns[0].renderCell?.({ + row: { + __depth: 1, + __index: 0, + data: nestedData.fields[2].values[0][0], + }, + rowIdx: 0, + column: { + ...columns[0], + frozen: false, + idx: 0, + parent: undefined, + level: 0, + sortable: true, + minWidth: 100, + draggable: true, + renderCell: () => null, + renderHeaderCell: () => null, + resizable: true, + width: 100, + maxWidth: undefined, + headerCellClass: undefined, + summaryCellClass: undefined, + }, + isCellEditable: false, + tabIndex: 0, + onRowChange: () => {}, + }); + + expect(expanderCell).toBeDefined(); + }); + }); + + describe('getFooterItemNG', () => { + const rows = [ + { Field1: 1, Text: 'a', __depth: 0, __index: 0 }, + { Field1: 2, Text: 'b', __depth: 0, __index: 1 }, + { Field1: 3, Text: 'c', __depth: 0, __index: 2 }, + { Field2: 3, Text: 'd', __depth: 0, __index: 3 }, + { Field2: 10, Text: 'e', __depth: 0, __index: 4 }, + ]; + + const numericField: Field = { + name: 'Field1', + type: FieldType.number, + values: [1, 2, 3], + config: { + custom: {}, + }, + display: (value: unknown) => ({ + text: String(value), + numeric: Number(value), + color: undefined, + prefix: undefined, + suffix: undefined, + }), + state: {}, + getLinks: undefined, + }; + + const numericField2: Field = { + name: 'Field2', + type: FieldType.number, + values: [3, 10], + config: { custom: {} }, + display: (value: unknown) => ({ + text: String(value), + numeric: Number(value), + color: undefined, + prefix: undefined, + suffix: undefined, + }), + state: {}, + getLinks: undefined, + }; + + const textField: Field = { + name: 'Text', + type: FieldType.string, + values: ['a', 'b', 'c'], + config: { custom: {} }, + display: (value: unknown) => ({ + text: String(value), + numeric: 0, + color: undefined, + prefix: undefined, + suffix: undefined, + }), + state: {}, + getLinks: undefined, + }; + + it('should calculate sum for numeric fields', () => { + const result = getFooterItemNG(rows, numericField, { + show: true, + reducer: ['sum'], + }); + + expect(result).toBe('6'); // 1 + 2 + 3 + }); + + it('should calculate mean for numeric fields', () => { + const result = getFooterItemNG(rows, numericField, { + show: true, + reducer: ['mean'], + }); + + expect(result).toBe('2'); // (1 + 2 + 3) / 3 + }); + + it('should return empty string for non-numeric fields', () => { + const result = getFooterItemNG(rows, textField, { + show: true, + reducer: ['sum'], + }); + + expect(result).toBe(''); + }); + + it('should return empty string when footer not shown', () => { + const result = getFooterItemNG(rows, numericField, undefined); + + expect(result).toBe(''); + }); + + it('should return empty string when reducer is undefined', () => { + const result = getFooterItemNG(rows, numericField, { + show: true, + reducer: undefined, + }); + expect(result).toBe(''); + }); + + it('should correctly calculate sum for numeric fields based on selected fields', () => { + const numericField1Result = getFooterItemNG(rows, numericField, { + show: true, + reducer: ['sum'], + fields: ['Field1'], + }); + + const numericField2Result = getFooterItemNG(rows, numericField2, { + show: true, + reducer: ['sum'], + fields: ['Field2'], + }); + + expect(numericField1Result).toBe('6'); // 1 + 2 + 3 + expect(numericField2Result).toBe('13'); // 3 + 10 + }); + }); + + describe('text alignment', () => { + it('should map alignment options to flex values', () => { + // Test 'left' alignment + const leftField = { + name: 'Value', + type: FieldType.string, + values: [], + config: { + custom: { + align: 'left', + }, + }, + }; + expect(getTextAlign(leftField)).toBe('flex-start'); + + // Test 'center' alignment + const centerField = { + name: 'Value', + type: FieldType.string, + values: [], + config: { + custom: { + align: 'center', + }, + }, + }; + expect(getTextAlign(centerField)).toBe('center'); + + // Test 'right' alignment + const rightField = { + name: 'Value', + type: FieldType.string, + values: [], + config: { + custom: { + align: 'right', + }, + }, + }; + expect(getTextAlign(rightField)).toBe('flex-end'); + }); + + it('should default to flex-start when no alignment specified', () => { + const field = { + name: 'Value', + type: FieldType.string, + values: [], + config: { + custom: {}, + }, + }; + expect(getTextAlign(field)).toBe('flex-start'); + }); + + it('should default to flex-start when no field is specified', () => { + expect(getTextAlign(undefined)).toBe('flex-start'); + }); + + it('should default to flex-end for number types', () => { + const field = { + name: 'Value', + type: FieldType.number, + values: [], + config: { + custom: {}, + }, + }; + expect(getTextAlign(field)).toBe('flex-end'); + }); + + it('should default to flex-start for string types', () => { + const field = { + name: 'String', + type: FieldType.string, + values: [], + config: { + custom: {}, + }, + }; + expect(getTextAlign(field)).toBe('flex-start'); + }); + + it('should default to flex-start for enum types', () => { + const field = { + name: 'Enum', + type: FieldType.enum, + values: [], + config: { + custom: {}, + }, + }; + expect(getTextAlign(field)).toBe('flex-start'); + }); + + it('should default to flex-start for time types', () => { + const field = { + name: 'Time', + type: FieldType.time, + values: [], + config: { + custom: {}, + }, + }; + expect(getTextAlign(field)).toBe('flex-start'); + }); + + it('should default to flex-start for boolean types', () => { + const field = { + name: 'Active', + type: FieldType.boolean, + values: [], + config: { + custom: {}, + }, + }; + expect(getTextAlign(field)).toBe('flex-start'); + }); + }); + + describe('cell display mode', () => { + const theme = { + colors: { + isDark: true, + mode: 'dark', + primary: { + text: '#FFFFFF', + main: '#FF0000', + }, + background: { + canvas: '#000000', + primary: '#111111', + }, + text: { + primary: '#FFFFFF', + }, + action: { + hover: '#FF0000', + }, + }, + } as unknown as GrafanaTheme2; + + it('should handle color background mode', () => { + const field = { + type: TableCellDisplayMode.ColorBackground as const, + mode: TableCellBackgroundDisplayMode.Basic, + }; + + const displayValue = { + text: '100', + numeric: 100, + color: '#ff0000', + }; + + const colors = getCellColors(theme, field, displayValue); + expect(colors.bgColor).toBe('rgb(255, 0, 0)'); + expect(colors.textColor).toBe('rgb(247, 248, 250)'); + expect(colors.bgHoverColor).toBe('rgb(255, 36, 36)'); + }); + + it('should handle color background gradient mode', () => { + const field = { + type: TableCellDisplayMode.ColorBackground as const, + mode: TableCellBackgroundDisplayMode.Gradient, + }; + + const displayValue = { + text: '100', + numeric: 100, + color: '#ff0000', + }; + + const colors = getCellColors(theme, field, displayValue); + expect(colors.bgColor).toBe('linear-gradient(120deg, rgb(255, 54, 36), #ff0000)'); + expect(colors.textColor).toBe('rgb(247, 248, 250)'); + expect(colors.bgHoverColor).toBe('linear-gradient(120deg, rgb(255, 54, 36), rgb(255, 54, 54))'); + }); + }); + + describe('frame to records conversion', () => { + it('should convert DataFrame to TableRows', () => { + const frame = createDataFrame({ + fields: [ + { name: 'time', values: [1, 2] }, + { name: 'value', values: [10, 20] }, + ], + }); + + const records = frameToRecords(frame); + expect(records).toHaveLength(2); + expect(records[0]).toEqual({ + __depth: 0, + __index: 0, + time: 1, + value: 10, + }); + }); + }); + + describe('handleSort', () => { + const setSortColumns = jest.fn(); + const sortColumnsRef: { current: SortColumn[] } = { current: [] }; + + beforeEach(() => { + setSortColumns.mockClear(); + sortColumnsRef.current = []; + }); + + it('should set initial sort', () => { + handleSort('Value', 'ASC', false, setSortColumns, sortColumnsRef); + + expect(setSortColumns).toHaveBeenCalledWith([{ columnKey: 'Value', direction: 'ASC' }]); + }); + + it('should toggle sort direction on same column', () => { + // Initial sort + sortColumnsRef.current = [{ columnKey: 'Value', direction: 'ASC' }] as const; + + handleSort('Value', 'DESC', false, setSortColumns, sortColumnsRef); + + expect(setSortColumns).toHaveBeenCalledWith([{ columnKey: 'Value', direction: 'DESC' }]); + }); + + it('should handle multi-sort with shift key', () => { + // Initial sort + sortColumnsRef.current = [{ columnKey: 'Time', direction: 'ASC' }] as const; + + handleSort('Value', 'ASC', true, setSortColumns, sortColumnsRef); + + expect(setSortColumns).toHaveBeenCalledWith([ + { columnKey: 'Time', direction: 'ASC' }, + { columnKey: 'Value', direction: 'ASC' }, + ]); + }); + + it('should remove sort when toggling through all states', () => { + // Initial ASC sort + sortColumnsRef.current = [{ columnKey: 'Value', direction: 'ASC' }] as const; + + // Toggle to DESC + handleSort('Value', 'DESC', false, setSortColumns, sortColumnsRef); + expect(setSortColumns).toHaveBeenCalledWith([{ columnKey: 'Value', direction: 'DESC' }]); + + // Toggle to no sort + handleSort('Value', 'DESC', false, setSortColumns, sortColumnsRef); + expect(setSortColumns).toHaveBeenCalledWith([]); + }); + }); + + describe('getAlignmentFactor', () => { + it('should create a new alignment factor when none exists', () => { + // Create a field with no existing alignment factor + const field: Field = { + name: 'test', + type: FieldType.number, + config: {}, + values: [1, 22, 333, 4444], + // No state property initially + display: (value: any) => ({ + text: String(value), + numeric: Number(value), + }), + }; + + // Create a display value + const displayValue: DisplayValue = { + text: '1', + numeric: 1, + }; + + // Call getAlignmentFactor with the first row + const result = getAlignmentFactor(field, displayValue, 0); + + // Verify the result has the text property + expect(result).toEqual( + expect.objectContaining({ + text: '1', + }) + ); + + // Verify that field.state was created and contains the alignment factor + expect(field.state).toBeDefined(); + expect(field.state?.alignmentFactors).toBeDefined(); + expect(field.state?.alignmentFactors).toEqual( + expect.objectContaining({ + text: '1', + }) + ); + }); + + it('should update alignment factor when a longer value is found', () => { + // Create a field with an existing alignment factor + const field: Field = { + name: 'test', + type: FieldType.number, + config: {}, + values: [1, 22, 333, 4444], + state: { + alignmentFactors: { + text: '1', + }, + }, + display: (value: any) => ({ + text: String(value), + numeric: Number(value), + }), + }; + + // Create a display value that is longer than the existing alignment factor + const displayValue: DisplayValue = { + text: '4444', + numeric: 4444, + }; + + // Call getAlignmentFactor + const result = getAlignmentFactor(field, displayValue, 3); + + // Verify the result is updated to the longer value + expect(result).toEqual( + expect.objectContaining({ + text: '4444', + }) + ); + + // Verify that field.state.alignmentFactors was updated + expect(field.state?.alignmentFactors).toEqual( + expect.objectContaining({ + text: '4444', + }) + ); + }); + + it('should not update alignment factor when a shorter value is found', () => { + // Create a field with an existing alignment factor for a long value + const field: Field = { + name: 'test', + type: FieldType.number, + config: {}, + values: [1, 22, 333, 4444], + state: { + alignmentFactors: { + text: '4444', + }, + }, + display: (value: any) => ({ + text: String(value), + numeric: Number(value), + }), + }; + + // Create a display value that is shorter than the existing alignment factor + const displayValue: DisplayValue = { + text: '1', + numeric: 1, + }; + + // Call getAlignmentFactor + const result = getAlignmentFactor(field, displayValue, 0); + + // Verify the result is still the longer value + expect(result).toEqual( + expect.objectContaining({ + text: '4444', + }) + ); + + // Verify that field.state.alignmentFactors was not changed + expect(field.state?.alignmentFactors).toEqual( + expect.objectContaining({ + text: '4444', + }) + ); + }); + + it('should add alignment factor to existing field state', () => { + // Create a field with existing state but no alignment factors yet + const field: Field = { + name: 'test', + type: FieldType.number, + config: {}, + values: [1, 22, 333, 4444], + // Field has state but no alignmentFactors + state: { + // Use a valid property for FieldState + // For example, if calcs is a valid property: + calcs: { sum: 4460 }, + // Or if noValue is a valid property: + // noValue: true + }, + display: (value: any) => ({ + text: String(value), + numeric: Number(value), + }), + }; + + // Create a display value + const displayValue: DisplayValue = { + text: '1', + numeric: 1, + }; + + // Call getAlignmentFactor with the first row + const result = getAlignmentFactor(field, displayValue, 0); + + // Verify the result has the text property + expect(result).toEqual( + expect.objectContaining({ + text: '1', + }) + ); + + // Verify that field.state was preserved and alignment factor was added + expect(field.state).toBeDefined(); + // Check for the valid property we used + expect(field.state?.calcs).toBeDefined(); + expect(field.state?.alignmentFactors).toBeDefined(); + expect(field.state?.alignmentFactors).toEqual( + expect.objectContaining({ + text: '1', + }) + ); + }); + }); + + describe('getIsNestedTable', () => { + it('should detect nested frames', () => { + const frame: DataFrame = { + fields: [ + { type: FieldType.string, name: 'stringCol', config: {}, values: [] }, + { type: FieldType.nestedFrames, name: 'nestedCol', config: {}, values: [] }, + ], + length: 0, + name: 'test', + }; + expect(getIsNestedTable(frame)).toBe(true); + }); + + it('should return false for regular frames', () => { + const frame: DataFrame = { + fields: [ + { type: FieldType.string, name: 'stringCol', config: {}, values: [] }, + { type: FieldType.number, name: 'numberCol', config: {}, values: [] }, + ], + length: 0, + name: 'test', + }; + expect(getIsNestedTable(frame)).toBe(false); + }); + }); + + describe('getComparator', () => { + it('should compare numbers correctly', () => { + const comparator = getComparator(FieldType.number); + expect(comparator(1, 2)).toBeLessThan(0); + expect(comparator(2, 1)).toBeGreaterThan(0); + expect(comparator(1, 1)).toBe(0); + }); + + it('should handle undefined values', () => { + const comparator = getComparator(FieldType.number); + expect(comparator(undefined, 1)).toBeLessThan(0); + expect(comparator(1, undefined)).toBeGreaterThan(0); + expect(comparator(undefined, undefined)).toBe(0); + }); + + it('should compare strings case-insensitively', () => { + const comparator = getComparator(FieldType.string); + expect(comparator('a', 'B')).toBeLessThan(0); + expect(comparator('B', 'a')).toBeGreaterThan(0); + expect(comparator('a', 'a')).toBe(0); + }); + + it('should handle time values', () => { + const comparator = getComparator(FieldType.time); + const t1 = 1672531200000; // 2023-01-01 + const t2 = 1672617600000; // 2023-01-02 + + expect(comparator(t1, t2)).toBeLessThan(0); + expect(comparator(t2, t1)).toBeGreaterThan(0); + expect(comparator(t1, t1)).toBe(0); + }); + + it('should handle boolean values', () => { + const comparator = getComparator(FieldType.boolean); + expect(comparator(false, true)).toBeLessThan(0); + expect(comparator(true, false)).toBeGreaterThan(0); + expect(comparator(true, true)).toBe(0); + }); + }); + + describe('shouldTextOverflow', () => { + const mockContext = { + font: '', + measureText: (text: string) => ({ + // Each character is 8px wide in our mock context + width: text.length * 8, + }), + }; + const osContext = mockContext as unknown as OffscreenCanvasRenderingContext2D; + + const headerCellRefs = { + current: { + column1: { + getBoundingClientRect: () => ({ width: 100 }), + offsetWidth: 100, + }, + } as unknown as Record, + }; + + it('should return true when text exceeds cell width', () => { + const row = { + __depth: 0, + __index: 0, + // 43*8 = 344px wide cell, it should overflow as it's greater than 100px + column1: 'This is a very long text that should overflow', + }; + const columnTypes = { column1: FieldType.string }; + + const result = shouldTextOverflow( + 'column1', + row, + columnTypes, + headerCellRefs, + osContext, + 20, // lineHeight + 40, // defaultRowHeight + 8, // padding + false, // textWrap + false, // cellInspect + TableCellDisplayMode.Auto // cellType + ); + + expect(result).toBe(true); + }); + + it('should return false when text fits cell width', () => { + const row = { + __depth: 0, + __index: 0, + // 9*8 = 72px wide cell, it should fit as it's less than 100px + column1: 'Short text', + }; + const columnTypes = { column1: FieldType.string }; + + const result = shouldTextOverflow( + 'column1', + row, + columnTypes, + headerCellRefs, + osContext, + 20, // lineHeight + 40, // defaultRowHeight + 8, // padding + false, // textWrap + false, // cellInspect + TableCellDisplayMode.Auto // cellType + ); + + expect(result).toBe(false); + }); + + it('should return false when text wrapping is enabled', () => { + const row = { + __depth: 0, + __index: 0, + column1: 'This is a very long text that should wrap instead of overflow', + }; + const columnTypes = { column1: FieldType.string }; + + const result = shouldTextOverflow( + 'column1', + row, + columnTypes, + headerCellRefs, + osContext, + 20, // lineHeight + 40, // defaultRowHeight + 8, // padding + true, // textWrap ENABLED + false, // cellInspect + TableCellDisplayMode.Auto // cellType + ); + + expect(result).toBe(false); + }); + + it('should return false when cell inspection is enabled', () => { + const row = { + __depth: 0, + __index: 0, + column1: 'This is a very long text', + }; + const columnTypes = { column1: FieldType.string }; + + const result = shouldTextOverflow( + 'column1', + row, + columnTypes, + headerCellRefs, + osContext, + 20, // lineHeight + 40, // defaultRowHeight + 8, // padding + false, // textWrap + true, // cellInspect ENABLED + TableCellDisplayMode.Auto // cellType + ); + + expect(result).toBe(false); + }); + }); + + describe('getRowHeight', () => { + const mockContext = { + font: '', + measureText: (text: string) => ({ + width: text.length * 8, + }), + }; + const osContext = mockContext as unknown as OffscreenCanvasRenderingContext2D; + + const headerCellRefs = { + current: { + stringCol: { offsetWidth: 100 }, + numberCol: { offsetWidth: 100 }, + } as unknown as Record, + }; + + it('should return default height when no text cells present', () => { + const row = { + __depth: 0, + __index: 0, + numberCol: 123, + }; + const columnTypes = { numberCol: FieldType.number }; + + const height = getRowHeight( + row, + columnTypes, + headerCellRefs, + osContext, + 20, // lineHeight + 40, // defaultRowHeight + 8 // padding + ); + + expect(height).toBe(40); + }); + + it('should calculate height based on longest text cell', () => { + const row = { + __depth: 0, + __index: 0, + stringCol: 'This is a very long text that should wrap', + numberCol: 123, + }; + const columnTypes = { + stringCol: FieldType.string, + numberCol: FieldType.number, + }; + + const height = getRowHeight(row, columnTypes, headerCellRefs, osContext, 20, 40, 8); + + expect(height).toBeGreaterThan(40); + expect(height).toBe(112); + }); + + it('should handle empty header cell refs', () => { + const row = { + __depth: 0, + __index: 0, + stringCol: 'Some text', + }; + const columnTypes = { stringCol: FieldType.string }; + const emptyRefs = { current: {} } as unknown as React.MutableRefObject>; + + const height = getRowHeight(row, columnTypes, emptyRefs, osContext, 20, 40, 8); + + expect(height).toBe(40); + }); + }); + + describe('isTextCell', () => { + it('should return true for string fields', () => { + expect(isTextCell('column', { column: FieldType.string })).toBe(true); + }); + + it('should return false for non-string fields', () => { + expect(isTextCell('column', { column: FieldType.number })).toBe(false); + expect(isTextCell('column', { column: FieldType.time })).toBe(false); + expect(isTextCell('column', { column: FieldType.boolean })).toBe(false); + }); + + it('should handle unknown fields', () => { + expect(isTextCell('unknown', { column: FieldType.string })).toBe(false); + }); + }); + + describe('migrateTableDisplayModeToCellOptions', () => { + it('should migrate basic to gauge mode', () => { + const result = migrateTableDisplayModeToCellOptions(TableCellDisplayMode.BasicGauge); + expect(result).toEqual({ + type: TableCellDisplayMode.Gauge, + mode: BarGaugeDisplayMode.Basic, + }); + }); + + it('should migrate gradient-gauge to gauge mode with gradient', () => { + const result = migrateTableDisplayModeToCellOptions(TableCellDisplayMode.GradientGauge); + expect(result).toEqual({ + type: TableCellDisplayMode.Gauge, + mode: BarGaugeDisplayMode.Gradient, + }); + }); + + it('should migrate color-background to color background with gradient', () => { + const result = migrateTableDisplayModeToCellOptions(TableCellDisplayMode.ColorBackground); + expect(result).toEqual({ + type: TableCellDisplayMode.ColorBackground, + mode: TableCellBackgroundDisplayMode.Gradient, + }); + }); + + it('should handle other display modes', () => { + const result = migrateTableDisplayModeToCellOptions(TableCellDisplayMode.ColorText); + expect(result).toEqual({ + type: TableCellDisplayMode.ColorText, + }); + }); + }); + + describe('getCellOptions', () => { + it('should return default options when no custom config is provided', () => { + const field: Field = { + name: 'test', + type: FieldType.string, + config: {}, + values: [], + }; + + const options = getCellOptions(field); + + // Check that default options are returned + expect(options).toEqual({ type: TableCellDisplayMode.Auto }); + }); + + it('should extract cell options from field config', () => { + const field: Field = { + name: 'test', + type: FieldType.string, + config: { + custom: { + cellOptions: { + type: TableCellDisplayMode.ColorText, + inspectEnabled: false, + wrapText: true, + }, + }, + }, + values: [], + }; + + const options = getCellOptions(field); + + expect(options).toEqual({ + type: TableCellDisplayMode.ColorText, + inspectEnabled: false, + wrapText: true, + }); + }); + + it('should handle legacy displayMode property', () => { + const field: Field = { + name: 'test', + type: FieldType.string, + config: { + custom: { + displayMode: 'color-background', + }, + }, + values: [], + }; + + const options = getCellOptions(field); + + // The legacy displayMode should be converted to the new format + expect(options.type).toBe(TableCellDisplayMode.ColorBackground); + }); + + it('should prioritize cellOptions over legacy displayMode', () => { + const field: Field = { + name: 'test', + type: FieldType.string, + config: { + custom: { + displayMode: 'color-background', + cellOptions: { + type: TableCellDisplayMode.ColorText, + }, + }, + }, + values: [], + }; + + const options = getCellOptions(field); + + expect(options.type).toBe(TableCellDisplayMode.ColorBackground); + }); + + it('should handle image display mode', () => { + const field: Field = { + name: 'test', + type: FieldType.string, + config: { + custom: { + cellOptions: { + type: TableCellDisplayMode.Image, + // Add image-specific options if they exist + }, + }, + }, + values: [], + }; + + const options = getCellOptions(field); + + expect(options.type).toBe(TableCellDisplayMode.Image); + }); + + it('should handle JSON display mode', () => { + const field: Field = { + name: 'test', + type: FieldType.string, + config: { + custom: { + cellOptions: { + type: TableCellDisplayMode.JSONView, + }, + }, + }, + values: [], + }; + + const options = getCellOptions(field); + + expect(options.type).toBe(TableCellDisplayMode.JSONView); + }); + }); + + describe('getCellLinks', () => { + it('should return undefined when field has no getLinks function', () => { + const field: Field = { + name: 'test', + type: FieldType.string, + config: {}, + values: ['value'], + }; + + const links = getCellLinks(field, 0); + expect(links).toEqual(undefined); + }); + + it('should return links from field getLinks function', () => { + const mockLinks: LinkModel[] = [ + { title: 'Link 1', href: 'http://example.com/1', target: '_blank', origin: { datasourceUid: 'test' } }, + { title: 'Link 2', href: 'http://example.com/2', target: '_self', origin: { datasourceUid: 'test' } }, + ]; + + const field: Field = { + name: 'test', + type: FieldType.string, + config: {}, + values: ['value1', 'value2'], + getLinks: (config: ValueLinkConfig) => { + return config.valueRowIndex === 0 ? mockLinks : []; + }, + }; + + const links = getCellLinks(field, 0); + expect(links).toEqual(mockLinks); + }); + + it('should return empty array for out of bounds index', () => { + const mockLinks: LinkModel[] = [ + { title: 'Link 1', href: 'http://example.com/1', target: '_blank', origin: { datasourceUid: 'test' } }, + ]; + + const field: Field = { + name: 'test', + type: FieldType.string, + config: {}, + values: ['value1'], + getLinks: (config: ValueLinkConfig) => { + return config.valueRowIndex === 0 ? mockLinks : []; + }, + }; + + // Index out of bounds + const links = getCellLinks(field, 1); + expect(links).toEqual([]); + }); + + it('should handle getLinks returning undefined', () => { + const field: Field = { + name: 'test', + type: FieldType.string, + config: {}, + values: ['value1'], + getLinks: (config: ValueLinkConfig) => { + return []; + }, + }; + + const links = getCellLinks(field, 0); + expect(links).toEqual([]); + }); + + it('should handle different link configurations', () => { + // Create links with different valid configurations + const mockLinks: LinkModel[] = [ + // Standard link with href + { + title: 'External Link', + href: 'http://example.com/full', + target: '_blank', + origin: { datasourceUid: 'test' }, + }, + // Internal link with onClick handler + { + title: 'Internal Link', + href: '', // Empty href for internal links + onClick: jest.fn(), + target: '_self', + origin: { datasourceUid: 'test' }, + }, + ]; + + const field: Field = { + name: 'test', + type: FieldType.string, + config: {}, + values: ['value1'], + getLinks: () => mockLinks, + }; + + const links = getCellLinks(field, 0); + + // Verify links are returned unmodified + expect(links).toEqual(mockLinks); + + // Verify we have both types of links + expect(links?.find((link) => link.onClick !== undefined)).toBeDefined(); + expect(links?.find((link) => link.href === 'http://example.com/full')).toBeDefined(); + }); + }); + + describe('getCellHeight', () => { + // Create a mock OffscreenCanvasRenderingContext2D + const createMockContext = () => { + return { + measureText: jest.fn((text) => { + // Simple mock that returns width based on text length + // This is a simplification - real browser would be more complex + return { width: text.length * 8 }; // Assume 8px per character + }), + } as unknown as OffscreenCanvasRenderingContext2D; + }; + + it('should return default row height when osContext is null', () => { + const defaultRowHeight = 40; + const height = getCellHeight('Some text', 100, null, 20, defaultRowHeight); + expect(height).toBe(defaultRowHeight); + }); + + it('should return default row height for text that fits in one line', () => { + const mockContext = createMockContext(); + const defaultRowHeight = 40; + const cellWidth = 100; // 100px width + const text = 'Short'; // 5 chars * 8px = 40px, fits in cellWidth + + const height = getCellHeight(text, cellWidth, mockContext, 20, defaultRowHeight); + + // Since text fits in one line, should return default height + expect(height).toBe(defaultRowHeight); + expect(mockContext.measureText).toHaveBeenCalled(); + }); + + it('should calculate height for text that wraps to multiple lines', () => { + const mockContext = createMockContext(); + const defaultRowHeight = 40; + const lineHeight = 20; + const cellWidth = 100; // 100px width + // This text is long enough to wrap to multiple lines + const text = 'This is a very long text that will definitely need to wrap to multiple lines in our table cell'; + + const height = getCellHeight(text, cellWidth, mockContext, lineHeight, defaultRowHeight); + + // Should be greater than default height since text wraps + expect(height).toBeGreaterThan(defaultRowHeight); + expect(height).toBe(180); + // Height should be a multiple of line height plus padding + expect(height % lineHeight).toBe(0); + expect(mockContext.measureText).toHaveBeenCalled(); + }); + + it('should account for padding when calculating height', () => { + const mockContext = createMockContext(); + const defaultRowHeight = 40; + const lineHeight = 20; + const cellWidth = 100; + const padding = 10; + const text = 'This is a very long text that will wrap to multiple lines'; + + const heightWithoutPadding = getCellHeight(text, cellWidth, mockContext, lineHeight, defaultRowHeight); + const heightWithPadding = getCellHeight(text, cellWidth, mockContext, lineHeight, defaultRowHeight, padding); + + // Height with padding should be greater than without padding + expect(heightWithPadding).toBeGreaterThan(heightWithoutPadding); + // The difference should be related to the padding (padding is applied twice in the function) + expect(heightWithPadding - heightWithoutPadding).toBe(padding * 2 * 2); + }); + + it('should handle empty text', () => { + const mockContext = createMockContext(); + const defaultRowHeight = 40; + + const height = getCellHeight('', 100, mockContext, 20, defaultRowHeight); + + // Empty text should return default height + expect(height).toBe(defaultRowHeight); + }); + }); + + describe('getFooterStyles', () => { + it('should create an emotion css class', () => { + const styles = getFooterStyles('flex-start'); + + // Check that the footerCell style has been created + expect(styles.footerCell).toBeDefined(); + + // Get the CSS string representation + const cssString = styles.footerCell.toString(); + + // Verify it's an Emotion CSS class + expect(cssString).toContain('css-'); + }); + + it('should use the provided justification value', () => { + const styles = getFooterStyles('center'); + + // Create a DOM element and apply the CSS class + document.body.innerHTML = `
Test
`; + const element = document.querySelector('div'); + + // Get the computed style + const computedStyle = window.getComputedStyle(element!); + + // Check the CSS property + expect(computedStyle.justifyContent).toBe('center'); + }); + + it('should default to space-between when no justification is provided', () => { + const styles = getFooterStyles(undefined as any); + + // Create a DOM element and apply the CSS class + document.body.innerHTML = `
Test
`; + const element = document.querySelector('div'); + + // Get the computed style + const computedStyle = window.getComputedStyle(element!); + + // Check the CSS property + expect(computedStyle.justifyContent).toBe('space-between'); + }); + + // Clean up after all tests + afterAll(() => { + document.body.innerHTML = ''; + }); + }); + + describe('extractPixelValue', () => { + it('should extract numeric value from pixel string', () => { + expect(extractPixelValue('100px')).toBe(100); + expect(extractPixelValue('42px')).toBe(42); + expect(extractPixelValue('0px')).toBe(0); + }); + + it('should handle numeric input', () => { + expect(extractPixelValue(100)).toBe(100); + expect(extractPixelValue(42)).toBe(42); + expect(extractPixelValue(0)).toBe(0); + }); + + it('should handle string numbers without units', () => { + expect(extractPixelValue('100')).toBe(100); + expect(extractPixelValue('42')).toBe(42); + expect(extractPixelValue('0')).toBe(0); + }); + + it('should handle decimal values', () => { + expect(extractPixelValue('100.5px')).toBe(100.5); + expect(extractPixelValue('42.75px')).toBe(42.75); + expect(extractPixelValue(100.5)).toBe(100.5); + }); + + it('should handle negative values', () => { + expect(extractPixelValue('-100px')).toBe(-100); + expect(extractPixelValue('-42px')).toBe(-42); + expect(extractPixelValue(-100)).toBe(-100); + }); + + it('should handle other CSS units by removing them', () => { + expect(extractPixelValue('100em')).toBe(100); + expect(extractPixelValue('42rem')).toBe(42); + expect(extractPixelValue('10vh')).toBe(10); + expect(extractPixelValue('20vw')).toBe(20); + }); + + it('should handle whitespace', () => { + expect(extractPixelValue(' 100px ')).toBe(100); + expect(extractPixelValue(' 42 px ')).toBe(42); + }); + + it('should return 0 for invalid input when no default is provided', () => { + expect(extractPixelValue('not-a-number')).toBe(0); + expect(extractPixelValue('px')).toBe(0); + expect(extractPixelValue('')).toBe(0); + expect(extractPixelValue(null as any)).toBe(0); + expect(extractPixelValue(undefined as any)).toBe(0); + }); + }); + + describe('convertRGBAToHex', () => { + it('should convert RGBA format to hex with alpha', () => { + expect(convertRGBAToHex('#181b1f', 'rgba(255, 0, 0, 1)')).toBe('#ff0000'); + expect(convertRGBAToHex('#181b1f', 'rgba(0, 255, 0, 0.5)')).toBe('#0c8d10'); + expect(convertRGBAToHex('#181b1f', 'rgba(0, 0, 255, 0)')).toBe('#181b1f'); + }); + }); + + describe('getDefaultRowHeight', () => { + const theme = createTheme(); + + it('returns correct height for TableCellHeight.Sm', () => { + const result = getDefaultRowHeight(theme, TableCellHeight.Sm); + expect(result).toBe(36); + }); + + 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('calculates height based on theme when cellHeight is undefined', () => { + const result = getDefaultRowHeight(theme, undefined as unknown as TableCellHeight); + + // Calculate the expected result based on the theme values + const expected = TABLE.CELL_PADDING * 2 + theme.typography.fontSize * theme.typography.body.lineHeight; + + expect(result).toBe(expected); + }); + }); + + describe('myRowRenderer', () => { + // Create mock props for testing + const createMockProps = (depth: number, hasData: boolean, index: number) => { + return { + row: { + __depth: depth, + __index: index, + data: hasData ? { length: 2 } : undefined, + }, + viewportColumns: [], + rowIdx: 0, + isRowSelected: false, + onRowClick: jest.fn(), + onRowDoubleClick: jest.fn(), + rowClass: '', + top: 0, + height: 40, + 'aria-rowindex': 1, + 'aria-selected': false, + gridRowStart: 1, + isLastRow: false, + selectedCellIdx: undefined, + selectCell: jest.fn(), + lastFrozenColumnIndex: -1, + copiedCellIdx: undefined, + draggedOverCellIdx: undefined, + setDraggedOverRowIdx: jest.fn(), + onRowChange: jest.fn(), + rowArray: [], + selectedPosition: { idx: 0, rowIdx: 0, mode: 'SELECT' }, + } as any; + }; + + const mockPanelContext = { + id: 1, + title: 'Test Panel', + description: 'Test Description', + width: 800, + height: 600, + timeRange: { from: 'now-6h', to: 'now' }, + timeZone: 'browser', + onTimeRangeChange: jest.fn(), + onOptionsChange: jest.fn(), + onFieldConfigChange: jest.fn(), + onInstanceStateChange: jest.fn(), + replaceVariables: jest.fn(), + eventBus: { + publish: jest.fn(), + subscribe: jest.fn(), + unsubscribe: jest.fn(), + }, + } as unknown as PanelContext; + + const mockData = createDataFrame({ + fields: [ + { name: 'Time', type: FieldType.time, values: [] }, + { name: 'Value', type: FieldType.number, values: [] }, + ], + }); + + it('returns null for non-expanded child rows', () => { + const props = createMockProps(1, false, 0); + const expandedRows: number[] = []; // No expanded rows + + const result = myRowRenderer('key-0', props, expandedRows, mockPanelContext, mockData, false); + + expect(result).toBeNull(); + }); + + it('renders child rows when parent is expanded', () => { + const props = createMockProps(1, false, 0); + const expandedRows: number[] = [0]; // Row 0 is expanded + + const result = myRowRenderer('key-0', props, expandedRows, mockPanelContext, mockData, false); + + expect(result).not.toBeNull(); + }); + + it('adds aria-expanded attribute to parent rows with nested data', () => { + const props = createMockProps(0, true, 0); + const expandedRows: number[] = [0]; // Row 0 is expanded + + const result = myRowRenderer('key-0', props, expandedRows, mockPanelContext, mockData, false) as JSX.Element; + + expect(result.props['aria-expanded']).toBe(true); + }); + + it('sets aria-expanded to false when parent row is not expanded', () => { + const props = createMockProps(0, true, 0); + const expandedRows: number[] = []; // No expanded rows + + const result = myRowRenderer('key-0', props, expandedRows, mockPanelContext, mockData, false) as JSX.Element; + + expect(result.props['aria-expanded']).toBe(false); + }); + + it('renders regular rows without aria-expanded attribute', () => { + const props = createMockProps(0, false, 0); + const expandedRows: number[] = []; + + const result = myRowRenderer('key-0', props, expandedRows, mockPanelContext, mockData, false) as JSX.Element; + + expect(result.props['aria-expanded']).toBeUndefined(); + }); + }); +}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.ts new file mode 100644 index 00000000000..a84045c47c0 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/utils.ts @@ -0,0 +1,570 @@ +import { css } from '@emotion/css'; +import { Property } from 'csstype'; +import React from 'react'; +import { SortColumn, SortDirection } from 'react-data-grid'; +import tinycolor from 'tinycolor2'; + +import { + FieldType, + Field, + formattedValueToString, + reduceField, + GrafanaTheme2, + DisplayValue, + LinkModel, + DisplayValueAlignmentFactors, + DataFrame, +} from '@grafana/data'; +import { + BarGaugeDisplayMode, + TableAutoCellOptions, + TableCellBackgroundDisplayMode, + TableCellDisplayMode, + TableCellHeight, + TableCellOptions, +} from '@grafana/schema'; + +import { TableCellInspectorMode } from '../..'; +import { getTextColorForAlphaBackground } from '../../../utils'; + +import { TABLE } from './constants'; +import { + CellColors, + TableRow, + TableFieldOptionsType, + ColumnTypes, + FilterType, + FrameToRowsConverter, + TableNGProps, + Comparator, + TableFooterCalc, +} from './types'; + +/* ---------------------------- Cell calculations --------------------------- */ +export function getCellHeight( + text: string, + cellWidth: number, // width of the cell without padding + osContext: OffscreenCanvasRenderingContext2D | null, + lineHeight: number, + defaultRowHeight: number, + padding = 0 +) { + const PADDING = padding * 2; + + if (osContext !== null && typeof text === 'string') { + const words = text.split(/\s/); + const lines = []; + let currentLine = ''; + + // Let's just wrap the lines and see how well the measurement works + for (let i = 0; i < words.length; i++) { + const currentWord = words[i]; + // TODO: this method is not accurate + let lineWidth = osContext.measureText(currentLine + ' ' + currentWord).width; + + // if line width is less than the cell width, add the word to the current line and continue + // else add the current line to the lines array and start a new line with the current word + if (lineWidth < cellWidth) { + currentLine += ' ' + currentWord; + } else { + lines.push({ + width: lineWidth, + line: currentLine, + }); + + currentLine = currentWord; + } + + // if we are at the last word, add the current line to the lines array + if (i === words.length - 1) { + lines.push({ + width: lineWidth, + line: currentLine, + }); + } + } + + if (lines.length === 1) { + return defaultRowHeight; + } + + // TODO: double padding to adjust osContext.measureText() results + const height = lines.length * lineHeight + PADDING * 2; + + return height; + } + + return defaultRowHeight; +} + +export function getDefaultRowHeight(theme: GrafanaTheme2, cellHeight: TableCellHeight | undefined): number { + const bodyFontSize = theme.typography.fontSize; + const lineHeight = theme.typography.body.lineHeight; + + switch (cellHeight) { + case TableCellHeight.Sm: + return 36; + case TableCellHeight.Md: + return 42; + case TableCellHeight.Lg: + return TABLE.MAX_CELL_HEIGHT; + } + + return TABLE.CELL_PADDING * 2 + bodyFontSize * lineHeight; +} + +/** + * getRowHeight determines cell height based on cell width + text length. Used + * for when textWrap is enabled. + */ +export function getRowHeight( + row: TableRow, + columnTypes: ColumnTypes, + headerCellRefs: React.MutableRefObject>, + osContext: OffscreenCanvasRenderingContext2D | null, + lineHeight: number, + defaultRowHeight: number, + padding: number +): number { + /** + * 0. loop through all cells in row + * 1. find text cell in row + * 2. find width of text cell + * 3. calculate height based on width and text length + * 4. return biggest height + */ + + let biggestHeight = defaultRowHeight; + + for (const key in row) { + if (isTextCell(key, columnTypes)) { + if (Object.keys(headerCellRefs.current).length === 0 || !headerCellRefs.current[key]) { + return biggestHeight; + } + const cellWidth = headerCellRefs.current[key].offsetWidth; + const cellText = String(row[key] ?? ''); + const newCellHeight = getCellHeight(cellText, cellWidth, osContext, lineHeight, defaultRowHeight, padding); + + if (newCellHeight > biggestHeight) { + biggestHeight = newCellHeight; + } + } + } + + return biggestHeight; +} + +export function isTextCell(key: string, columnTypes: Record): boolean { + return columnTypes[key] === FieldType.string; +} + +export function shouldTextOverflow( + key: string, + row: TableRow, + columnTypes: ColumnTypes, + headerCellRefs: React.MutableRefObject>, + osContext: OffscreenCanvasRenderingContext2D | null, + lineHeight: number, + defaultRowHeight: number, + padding: number, + textWrap: boolean, + cellInspect: boolean, + cellType: TableCellDisplayMode +): boolean { + // 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 fo type image + if (textWrap || cellInspect || cellType === TableCellDisplayMode.Image || !isTextCell(key, columnTypes)) { + return false; + } + + const cellWidth = headerCellRefs.current[key].offsetWidth; + const cellText = String(row[key] ?? ''); + const newCellHeight = getCellHeight(cellText, cellWidth, osContext, lineHeight, defaultRowHeight, padding); + + return newCellHeight > defaultRowHeight; +} + +export function getTextAlign(field?: Field): Property.JustifyContent { + if (!field) { + return 'flex-start'; + } + + if (field.config.custom) { + const custom: TableFieldOptionsType = field.config.custom; + + switch (custom.align) { + case 'right': + return 'flex-end'; + case 'left': + return 'flex-start'; + case 'center': + return 'center'; + } + } + + if (field.type === FieldType.number) { + return 'flex-end'; + } + + return 'flex-start'; +} + +const defaultCellOptions: TableAutoCellOptions = { type: TableCellDisplayMode.Auto }; + +export function getCellOptions(field: Field): TableCellOptions { + if (field.config.custom?.displayMode) { + return migrateTableDisplayModeToCellOptions(field.config.custom?.displayMode); + } + + if (!field.config.custom?.cellOptions) { + return defaultCellOptions; + } + + return field.config.custom.cellOptions; +} + +/** + * Getting gauge or sparkline values to align is very tricky without looking at all values and passing them through display processor. + * For very large tables that could pretty expensive. So this is kind of a compromise. We look at the first 1000 rows and cache the longest value. + * If we have a cached value we just check if the current value is longer and update the alignmentFactor. This can obviously still lead to + * unaligned gauges but it should a lot less common. + **/ +export function getAlignmentFactor( + field: Field, + displayValue: DisplayValue, + rowIndex: number +): DisplayValueAlignmentFactors { + let alignmentFactor = field.state?.alignmentFactors; + + if (alignmentFactor) { + // check if current alignmentFactor is still the longest + if (formattedValueToString(alignmentFactor).length < formattedValueToString(displayValue).length) { + alignmentFactor = { ...displayValue }; + field.state!.alignmentFactors = alignmentFactor; + } + return alignmentFactor; + } else { + // look at the next 1000 rows + alignmentFactor = { ...displayValue }; + const maxIndex = Math.min(field.values.length, rowIndex + 1000); + + for (let i = rowIndex + 1; i < maxIndex; i++) { + const nextDisplayValue = field.display!(field.values[i]); + if (formattedValueToString(alignmentFactor).length > formattedValueToString(nextDisplayValue).length) { + alignmentFactor.text = displayValue.text; + } + } + + if (field.state) { + field.state.alignmentFactors = alignmentFactor; + } else { + field.state = { alignmentFactors: alignmentFactor }; + } + + return alignmentFactor; + } +} + +/* ------------------------------ Footer calculations ------------------------------ */ +export function getFooterItemNG(rows: TableRow[], field: Field, options: TableFooterCalc | undefined): string { + if (options === undefined) { + return ''; + } + + if (field.type !== FieldType.number) { + return ''; + } + + // Check if reducer array exists and has at least one element + if (!options.reducer || !options.reducer.length) { + return ''; + } + + // If fields array is specified, only show footer for fields included in that array + if (options.fields && options.fields.length > 0) { + if (!options.fields.includes(field.name)) { + return ''; + } + } + + const calc = options.reducer[0]; + const value = reduceField({ + field: { + ...field, + values: rows.map((row) => row[field.name]), + }, + reducers: options.reducer, + })[calc]; + + const formattedValue = formattedValueToString(field.display!(value)); + + return formattedValue; +} + +export const getFooterStyles = (justifyContent: Property.JustifyContent) => ({ + footerCell: css({ + display: 'flex', + justifyContent: justifyContent || 'space-between', + }), +}); + +/* ------------------------- Cell color calculation ------------------------- */ +const CELL_COLOR_DARKENING_MULTIPLIER = 10; +const CELL_GRADIENT_DARKENING_MULTIPLIER = 15; +const CELL_GRADIENT_HUE_ROTATION_DEGREES = 5; + +export function getCellColors( + theme: GrafanaTheme2, + cellOptions: TableCellOptions, + displayValue: DisplayValue +): CellColors { + // Convert RGBA hover color to hex to prevent transparency issues on cell hover + const autoCellBackgroundHoverColor = convertRGBAToHex(theme.colors.background.primary, theme.colors.action.hover); + + // How much to darken elements depends upon if we're in dark mode + const darkeningFactor = theme.isDark ? 1 : -0.7; + + // Setup color variables + let textColor: string | undefined = undefined; + let bgColor: string | undefined = undefined; + let bgHoverColor: string = autoCellBackgroundHoverColor; + + if (cellOptions.type === TableCellDisplayMode.ColorText) { + textColor = displayValue.color; + } else if (cellOptions.type === TableCellDisplayMode.ColorBackground) { + const mode = cellOptions.mode ?? TableCellBackgroundDisplayMode.Gradient; + + if (mode === TableCellBackgroundDisplayMode.Basic) { + textColor = getTextColorForAlphaBackground(displayValue.color!, theme.isDark); + bgColor = tinycolor(displayValue.color).toRgbString(); + bgHoverColor = tinycolor(displayValue.color) + .darken(CELL_COLOR_DARKENING_MULTIPLIER * darkeningFactor) + .toRgbString(); + } else if (mode === TableCellBackgroundDisplayMode.Gradient) { + const hoverColor = tinycolor(displayValue.color) + .darken(CELL_GRADIENT_DARKENING_MULTIPLIER * darkeningFactor) + .toRgbString(); + const bgColor2 = tinycolor(displayValue.color) + .darken(CELL_COLOR_DARKENING_MULTIPLIER * darkeningFactor) + .spin(CELL_GRADIENT_HUE_ROTATION_DEGREES); + textColor = getTextColorForAlphaBackground(displayValue.color!, theme.isDark); + bgColor = `linear-gradient(120deg, ${bgColor2.toRgbString()}, ${displayValue.color})`; + bgHoverColor = `linear-gradient(120deg, ${bgColor2.toRgbString()}, ${hoverColor})`; + } + } + + return { textColor, bgColor, bgHoverColor }; +} + +/** Extracts numeric pixel value from theme spacing */ +export const extractPixelValue = (spacing: string | number): number => { + return typeof spacing === 'number' ? spacing : parseFloat(spacing) || 0; +}; + +/** Converts an RGBA color to hex by blending it with a background color */ +export const convertRGBAToHex = (backgroundColor: string, rgbaColor: string): string => { + const bg = tinycolor(backgroundColor); + const rgba = tinycolor(rgbaColor); + return tinycolor.mix(bg, rgba, rgba.getAlpha() * 100).toHexString(); +}; + +/* ------------------------------- Data links ------------------------------- */ +/** + * @internal + */ +export const getCellLinks = (field: Field, rowIdx: number) => { + let links: Array> | undefined; + if (field.getLinks) { + links = field.getLinks({ + valueRowIndex: rowIdx, + }); + } + + if (!links) { + return; + } + + for (let i = 0; i < links?.length; i++) { + if (links[i].onClick) { + const origOnClick = links[i].onClick; + + links[i].onClick = (event) => { + // Allow opening in new tab + if (!(event.ctrlKey || event.metaKey || event.shiftKey)) { + event.preventDefault(); + origOnClick!(event, { + field, + rowIndex: rowIdx, + }); + } + }; + } + } + + return links; +}; + +/* ----------------------------- Data grid sorting ---------------------------- */ +export const handleSort = ( + columnKey: string, + direction: SortDirection, + isMultiSort: boolean, + setSortColumns: React.Dispatch>, + sortColumnsRef: React.MutableRefObject +) => { + let currentSortColumn: SortColumn | undefined; + + const updatedSortColumns = sortColumnsRef.current.filter((column) => { + const isCurrentColumn = column.columnKey === columnKey; + if (isCurrentColumn) { + currentSortColumn = column; + } + return !isCurrentColumn; + }); + + // sorted column exists and is descending -> remove it to reset sorting + if (currentSortColumn && currentSortColumn.direction === 'DESC') { + setSortColumns(updatedSortColumns); + sortColumnsRef.current = updatedSortColumns; + } else { + // new sort column or changed direction + if (isMultiSort) { + setSortColumns([...updatedSortColumns, { columnKey, direction }]); + sortColumnsRef.current = [...updatedSortColumns, { columnKey, direction }]; + } else { + setSortColumns([{ columnKey, direction }]); + sortColumnsRef.current = [{ columnKey, direction }]; + } + } +}; + +/* ----------------------------- Data grid mapping ---------------------------- */ +export const frameToRecords = (frame: DataFrame): TableRow[] => { + const fnBody = ` + const rows = Array(frame.length); + const values = frame.fields.map(f => f.values); + let rowCount = 0; + for (let i = 0; i < frame.length; i++) { + rows[rowCount] = { + __depth: 0, + __index: i, + ${frame.fields.map((field, fieldIdx) => `${JSON.stringify(field.name)}: values[${fieldIdx}][i]`).join(',')} + }; + rowCount += 1; + if (rows[rowCount-1]['Nested frames']){ + const childFrame = rows[rowCount-1]['Nested frames']; + rows[rowCount] = {__depth: 1, __index: i, data: childFrame[0]} + rowCount += 1; + } + } + return rows; + `; + + // Creates a function that converts a DataFrame into an array of TableRows + // Uses new Function() for performance as it's faster than creating rows using loops + const convert = new Function('frame', fnBody) as unknown as FrameToRowsConverter; + return convert(frame); +}; + +export interface MapFrameToGridOptions extends TableNGProps { + columnTypes: ColumnTypes; + columnWidth: number | string; + crossFilterOrder: React.MutableRefObject; + crossFilterRows: React.MutableRefObject<{ [key: string]: TableRow[] }>; + defaultLineHeight: number; + defaultRowHeight: number; + expandedRows: number[]; + filter: FilterType; + headerCellRefs: React.MutableRefObject>; + isCountRowsSet: boolean; + osContext: OffscreenCanvasRenderingContext2D | null; + rows: TableRow[]; + setContextMenuProps: (props: { value: string; top?: number; left?: number; mode?: TableCellInspectorMode }) => void; + setFilter: React.Dispatch>; + setIsInspecting: (isInspecting: boolean) => void; + setSortColumns: React.Dispatch>; + sortColumnsRef: React.MutableRefObject; + styles: { cell: string }; + textWrap: boolean; + theme: GrafanaTheme2; + showTypeIcons?: boolean; +} + +/* ----------------------------- Data grid comparator ---------------------------- */ +const compare = new Intl.Collator('en', { sensitivity: 'base' }).compare; +export function getComparator(sortColumnType: FieldType): Comparator { + switch (sortColumnType) { + case FieldType.time: + case FieldType.number: + case FieldType.boolean: + return (a, b) => { + if (a === b) { + return 0; + } + if (a == null) { + return -1; + } + if (b == null) { + return 1; + } + return Number(a) - Number(b); + }; + case FieldType.string: + case FieldType.enum: + default: + return (a, b) => compare(String(a ?? ''), String(b ?? '')); + } +} + +/* ---------------------------- Miscellaneous ---------------------------- */ +/** + * Migrates table cell display mode to new object format. + * + * @param displayMode The display mode of the cell + * @returns TableCellOptions object in the correct format + * relative to the old display mode. + */ +export function migrateTableDisplayModeToCellOptions(displayMode: TableCellDisplayMode): TableCellOptions { + switch (displayMode) { + // In the case of the gauge we move to a different option + case 'basic': + case 'gradient-gauge': + case 'lcd-gauge': + let gaugeMode = BarGaugeDisplayMode.Basic; + + if (displayMode === 'gradient-gauge') { + gaugeMode = BarGaugeDisplayMode.Gradient; + } else if (displayMode === 'lcd-gauge') { + gaugeMode = BarGaugeDisplayMode.Lcd; + } + + return { + type: TableCellDisplayMode.Gauge, + mode: gaugeMode, + }; + // Also true in the case of the color background + case 'color-background': + case 'color-background-solid': + let mode = TableCellBackgroundDisplayMode.Basic; + + // Set the new mode field, somewhat confusingly the + // color-background mode is for gradient display + if (displayMode === 'color-background') { + mode = TableCellBackgroundDisplayMode.Gradient; + } + + return { + type: TableCellDisplayMode.ColorBackground, + mode: mode, + }; + default: + return { + // @ts-ignore + type: displayMode, + }; + } +} + +/** Returns true if the DataFrame contains nested frames */ +export const getIsNestedTable = (dataFrame: DataFrame): boolean => + dataFrame.fields.some(({ type }) => type === FieldType.nestedFrames); diff --git a/packages/grafana-ui/src/components/Table/ExpandedRow.tsx b/packages/grafana-ui/src/components/Table/TableRT/ExpandedRow.tsx similarity index 96% rename from packages/grafana-ui/src/components/Table/ExpandedRow.tsx rename to packages/grafana-ui/src/components/Table/TableRT/ExpandedRow.tsx index 44e7b2cf292..9b73a42a6a2 100644 --- a/packages/grafana-ui/src/components/Table/ExpandedRow.tsx +++ b/packages/grafana-ui/src/components/Table/TableRT/ExpandedRow.tsx @@ -5,11 +5,11 @@ import * as React from 'react'; import { DataFrame, Field, GrafanaTheme2 } from '@grafana/data'; import { TableCellHeight } from '@grafana/schema'; -import { useStyles2, useTheme2 } from '../../themes'; +import { useStyles2, useTheme2 } from '../../../themes'; +import { EXPANDER_WIDTH } from '../utils'; import { Table } from './Table'; import { TableStyles } from './styles'; -import { EXPANDER_WIDTH } from './utils'; export interface Props { nestedData: Field; diff --git a/packages/grafana-ui/src/components/Table/Filter.tsx b/packages/grafana-ui/src/components/Table/TableRT/Filter.tsx similarity index 94% rename from packages/grafana-ui/src/components/Table/Filter.tsx rename to packages/grafana-ui/src/components/Table/TableRT/Filter.tsx index 572fa6f5347..67c185695db 100644 --- a/packages/grafana-ui/src/components/Table/Filter.tsx +++ b/packages/grafana-ui/src/components/Table/TableRT/Filter.tsx @@ -3,9 +3,9 @@ import { useCallback, useMemo, useRef, useState } from 'react'; import { Field, GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { Popover } from '..'; -import { useStyles2 } from '../../themes'; -import { Icon } from '../Icon/Icon'; +import { Popover } from '../..'; +import { useStyles2 } from '../../../themes'; +import { Icon } from '../../Icon/Icon'; import { REGEX_OPERATOR } from './FilterList'; import { FilterPopup } from './FilterPopup'; diff --git a/packages/grafana-ui/src/components/Table/FilterList.tsx b/packages/grafana-ui/src/components/Table/TableRT/FilterList.tsx similarity index 98% rename from packages/grafana-ui/src/components/Table/FilterList.tsx rename to packages/grafana-ui/src/components/Table/TableRT/FilterList.tsx index 9aa254062cc..32615a9070a 100644 --- a/packages/grafana-ui/src/components/Table/FilterList.tsx +++ b/packages/grafana-ui/src/components/Table/TableRT/FilterList.tsx @@ -5,9 +5,9 @@ import { FixedSizeList as List, ListChildComponentProps } from 'react-window'; import { GrafanaTheme2, formattedValueToString, getValueFormat, SelectableValue } from '@grafana/data'; -import { ButtonSelect, Checkbox, FilterInput, Label, Stack } from '..'; -import { useStyles2, useTheme2 } from '../../themes'; -import { t, Trans } from '../../utils/i18n'; +import { ButtonSelect, Checkbox, FilterInput, Label, Stack } from '../..'; +import { useStyles2, useTheme2 } from '../../../themes'; +import { t, Trans } from '../../../utils/i18n'; interface Props { values: SelectableValue[]; diff --git a/packages/grafana-ui/src/components/Table/FilterPopup.tsx b/packages/grafana-ui/src/components/Table/TableRT/FilterPopup.tsx similarity index 97% rename from packages/grafana-ui/src/components/Table/FilterPopup.tsx rename to packages/grafana-ui/src/components/Table/TableRT/FilterPopup.tsx index f80f9fb0627..6eaeac0ee90 100644 --- a/packages/grafana-ui/src/components/Table/FilterPopup.tsx +++ b/packages/grafana-ui/src/components/Table/TableRT/FilterPopup.tsx @@ -4,13 +4,13 @@ import * as React from 'react'; import { Field, GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { Button, ClickOutsideWrapper, IconButton, Label, Stack } from '..'; -import { useStyles2, useTheme2 } from '../../themes'; -import { t, Trans } from '../../utils/i18n'; +import { Button, ClickOutsideWrapper, IconButton, Label, Stack } from '../..'; +import { useStyles2, useTheme2 } from '../../../themes'; +import { t, Trans } from '../../../utils/i18n'; +import { calculateUniqueFieldValues, getFilteredOptions, valuesToOptions } from '../utils'; import { FilterList } from './FilterList'; import { TableStyles } from './styles'; -import { calculateUniqueFieldValues, getFilteredOptions, valuesToOptions } from './utils'; interface Props { column: any; diff --git a/packages/grafana-ui/src/components/Table/FooterRow.tsx b/packages/grafana-ui/src/components/Table/TableRT/FooterRow.tsx similarity index 95% rename from packages/grafana-ui/src/components/Table/FooterRow.tsx rename to packages/grafana-ui/src/components/Table/TableRT/FooterRow.tsx index e69e3610737..61ea59baeb8 100644 --- a/packages/grafana-ui/src/components/Table/FooterRow.tsx +++ b/packages/grafana-ui/src/components/Table/TableRT/FooterRow.tsx @@ -3,9 +3,10 @@ import { ColumnInstance, HeaderGroup } from 'react-table'; import { fieldReducers, ReducerID } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { EmptyCell, FooterCell } from './FooterCell'; +import { EmptyCell, FooterCell } from '../Cells/FooterCell'; +import { FooterItem } from '../types'; + import { TableStyles } from './styles'; -import { FooterItem } from './types'; export interface FooterRowProps { totalColumnsWidth: number; diff --git a/packages/grafana-ui/src/components/Table/HeaderRow.tsx b/packages/grafana-ui/src/components/Table/TableRT/HeaderRow.tsx similarity index 95% rename from packages/grafana-ui/src/components/Table/HeaderRow.tsx rename to packages/grafana-ui/src/components/Table/TableRT/HeaderRow.tsx index 6222f50630f..323a11562f6 100644 --- a/packages/grafana-ui/src/components/Table/HeaderRow.tsx +++ b/packages/grafana-ui/src/components/Table/TableRT/HeaderRow.tsx @@ -3,12 +3,12 @@ import { HeaderGroup, Column } from 'react-table'; import { Field } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { getFieldTypeIcon } from '../../types'; -import { Icon } from '../Icon/Icon'; +import { getFieldTypeIcon } from '../../../types'; +import { Icon } from '../../Icon/Icon'; +import { TableFieldOptions } from '../types'; import { Filter } from './Filter'; import { TableStyles } from './styles'; -import { TableFieldOptions } from './types'; export interface HeaderRowProps { headerGroups: HeaderGroup[]; diff --git a/packages/grafana-ui/src/components/Table/RowExpander.tsx b/packages/grafana-ui/src/components/Table/TableRT/RowExpander.tsx similarity index 84% rename from packages/grafana-ui/src/components/Table/RowExpander.tsx rename to packages/grafana-ui/src/components/Table/TableRT/RowExpander.tsx index ca678472cab..ac36355ca8b 100644 --- a/packages/grafana-ui/src/components/Table/RowExpander.tsx +++ b/packages/grafana-ui/src/components/Table/TableRT/RowExpander.tsx @@ -1,7 +1,7 @@ -import { Icon } from '../Icon/Icon'; +import { Icon } from '../../Icon/Icon'; +import { GrafanaTableRow } from '../types'; import { TableStyles } from './styles'; -import { GrafanaTableRow } from './types'; export interface Props { row: GrafanaTableRow; diff --git a/packages/grafana-ui/src/components/Table/RowsList.tsx b/packages/grafana-ui/src/components/Table/TableRT/RowsList.tsx similarity index 97% rename from packages/grafana-ui/src/components/Table/RowsList.tsx rename to packages/grafana-ui/src/components/Table/TableRT/RowsList.tsx index 3eeb13a9414..77f744acf91 100644 --- a/packages/grafana-ui/src/components/Table/RowsList.tsx +++ b/packages/grafana-ui/src/components/Table/TableRT/RowsList.tsx @@ -17,26 +17,26 @@ import { } from '@grafana/data'; import { TableCellDisplayMode, TableCellHeight } from '@grafana/schema'; -import { useTheme2 } from '../../themes'; -import CustomScrollbar from '../CustomScrollbar/CustomScrollbar'; -import { usePanelContext } from '../PanelChrome'; - -import { ExpandedRow, getExpandedRowHeight } from './ExpandedRow'; -import { TableCell } from './TableCell'; -import { TableStyles } from './styles'; +import { useTheme2 } from '../../../themes'; +import CustomScrollbar from '../../CustomScrollbar/CustomScrollbar'; +import { usePanelContext } from '../../PanelChrome'; +import { TableCell } from '../Cells/TableCell'; import { CellColors, GetActionsFunction, TableFieldOptions, TableFilterActionCallback, TableInspectCellCallback, -} from './types'; +} from '../types'; import { calculateAroundPointThreshold, getCellColors, isPointTimeValAroundTableTimeVal, guessTextBoundingBox, -} from './utils'; +} from '../utils'; + +import { ExpandedRow, getExpandedRowHeight } from './ExpandedRow'; +import { TableStyles } from './styles'; interface RowsListProps { data: DataFrame; @@ -264,7 +264,7 @@ export const RowsList = (props: RowsListProps) => { ) { rowBg = (rowIndex: number): CellColors => { const display = field.display!(field.values.get(rowIndex)); - const colors = getCellColors(tableStyles, fieldOptions.cellOptions, display); + const colors = getCellColors(tableStyles.theme, fieldOptions.cellOptions, display); return colors; }; } diff --git a/packages/grafana-ui/src/components/Table/TableRT/Table.tsx b/packages/grafana-ui/src/components/Table/TableRT/Table.tsx new file mode 100644 index 00000000000..3513c06d3dc --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableRT/Table.tsx @@ -0,0 +1,390 @@ +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { + useAbsoluteLayout, + useExpanded, + useFilters, + usePagination, + useResizeColumns, + useSortBy, + useTable, +} from 'react-table'; +import { VariableSizeList } from 'react-window'; + +import { FieldType, ReducerID, getRowUniqueId, getFieldMatcher } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; +import { TableCellHeight } from '@grafana/schema'; + +import { useTheme2 } from '../../../themes'; +import { Trans } from '../../../utils/i18n'; +import { CustomScrollbar } from '../../CustomScrollbar/CustomScrollbar'; +import { Pagination } from '../../Pagination/Pagination'; +import { useFixScrollbarContainer, useResetVariableListSizeCache } from '../hooks'; +import { getInitialState, useTableStateReducer } from '../reducer'; +import { FooterItem, GrafanaTableState, BaseTableProps as Props } from '../types'; +import { + getColumns, + sortCaseInsensitive, + sortNumber, + getFooterItems, + createFooterCalculationValues, + guessLongestField, +} from '../utils'; + +import { FooterRow } from './FooterRow'; +import { HeaderRow } from './HeaderRow'; +import { RowsList } from './RowsList'; +import { useTableStyles } from './styles'; + +const COLUMN_MIN_WIDTH = 150; +const FOOTER_ROW_HEIGHT = 36; +const NO_DATA_TEXT = 'No data'; + +export const Table = memo((props: Props) => { + const { + ariaLabel, + data, + height, + onCellFilterAdded, + onColumnResize, + width, + columnMinWidth = COLUMN_MIN_WIDTH, + noHeader, + resizable = true, + initialSortBy, + footerOptions, + showTypeIcons, + footerValues, + enablePagination, + cellHeight = TableCellHeight.Sm, + timeRange, + enableSharedCrosshair = false, + initialRowIndex = undefined, + fieldConfig, + getActions, + replaceVariables, + } = props; + + const listRef = useRef(null); + const tableDivRef = useRef(null); + const variableSizeListScrollbarRef = useRef(null); + const theme = useTheme2(); + const tableStyles = useTableStyles(theme, cellHeight); + const headerHeight = noHeader ? 0 : tableStyles.rowHeight; + const [footerItems, setFooterItems] = useState(footerValues); + const noValuesDisplayText = fieldConfig?.defaults?.noValue ?? NO_DATA_TEXT; + + const footerHeight = useMemo(() => { + const EXTENDED_ROW_HEIGHT = FOOTER_ROW_HEIGHT; + let length = 0; + + if (!footerItems) { + return 0; + } + + for (const fv of footerItems) { + if (Array.isArray(fv) && fv.length > length) { + length = fv.length; + } + } + + if (length > 1) { + return EXTENDED_ROW_HEIGHT * length; + } + + return EXTENDED_ROW_HEIGHT; + }, [footerItems]); + + // React table data array. This data acts just like a dummy array to let react-table know how many rows exist. + // The cells use the field to look up values, therefore this is simply a length/size placeholder. + const memoizedData = useMemo(() => { + if (!data.fields.length) { + return []; + } + // As we only use this to fake the length of our data set for react-table we need to make sure we always return an array + // filled with values at each index otherwise we'll end up trying to call accessRow for null|undefined value in + // https://github.com/tannerlinsley/react-table/blob/7be2fc9d8b5e223fc998af88865ae86a88792fdb/src/hooks/useTable.js#L585 + return Array(data.length).fill(0); + }, [data]); + + // This checks whether `Show table footer` is toggled on, the `Calculation` is set to `Count`, and finally, whether `Count rows` is toggled on. + const isCountRowsSet = Boolean( + footerOptions?.countRows && + footerOptions.reducer && + footerOptions.reducer.length && + footerOptions.reducer[0] === ReducerID.count + ); + + const nestedDataField = data.fields.find((f) => f.type === FieldType.nestedFrames); + const hasNestedData = nestedDataField !== undefined; + + // React-table column definitions + const memoizedColumns = useMemo( + () => getColumns(data, width, columnMinWidth, hasNestedData, footerItems, isCountRowsSet), + [data, width, columnMinWidth, hasNestedData, footerItems, isCountRowsSet] + ); + + // we need a ref to later store the `toggleAllRowsExpanded` function, returned by `useTable`. + // We cannot simply use a variable because we need to use such function in the initialization of + // `useTableStateReducer`, which is needed to construct options for `useTable` (the hook that returns + // `toggleAllRowsExpanded`), and if we used a variable, that variable would be undefined at the time + // we initialize `useTableStateReducer`. + const toggleAllRowsExpandedRef = useRef<(value?: boolean) => void>(); + + // Internal react table state reducer + const stateReducer = useTableStateReducer({ + onColumnResize, + onSortByChange: (state) => { + // Collapse all rows. This prevents a known bug that causes the size of the rows to be incorrect due to + // using `VariableSizeList` and `useExpanded` together. + toggleAllRowsExpandedRef.current!(false); + + if (props.onSortByChange) { + props.onSortByChange(state); + } + }, + data, + }); + + const hasUniqueId = !!data.meta?.uniqueRowIdFields?.length; + + const options: any = useMemo(() => { + // This is a bit hard to type with the react-table types here, the reducer does not actually match with the + // TableOptions. + const options: any = { + columns: memoizedColumns, + data: memoizedData, + disableResizing: !resizable, + stateReducer: stateReducer, + autoResetPage: false, + initialState: getInitialState(initialSortBy, memoizedColumns), + autoResetFilters: false, + sortTypes: { + // the builtin number type on react-table does not handle NaN values + number: sortNumber, + // should be replaced with the builtin string when react-table is upgraded, + // see https://github.com/tannerlinsley/react-table/pull/3235 + 'alphanumeric-insensitive': sortCaseInsensitive, + }, + }; + if (hasUniqueId) { + // row here is just always 0 because here we don't use real data but just a dummy array filled with 0. + // See memoizedData variable above. + options.getRowId = (row: Record, relativeIndex: number) => getRowUniqueId(data, relativeIndex); + + // If we have unique field we assume we can count on it as being globally unique, and we don't need to reset when + // data changes. + options.autoResetExpanded = false; + } + return options; + }, [initialSortBy, memoizedColumns, memoizedData, resizable, stateReducer, hasUniqueId, data]); + + const { + getTableProps, + headerGroups, + footerGroups, + rows, + prepareRow, + totalColumnsWidth, + page, + state, + gotoPage, + setPageSize, + pageOptions, + toggleAllRowsExpanded, + } = useTable(options, useFilters, useSortBy, useAbsoluteLayout, useResizeColumns, useExpanded, usePagination); + + const extendedState = state as GrafanaTableState; + toggleAllRowsExpandedRef.current = toggleAllRowsExpanded; + + /* + Footer value calculation is being moved in the Table component and the footerValues prop will be deprecated. + The footerValues prop is still used in the Table component for backwards compatibility. Adding the + footerOptions prop will switch the Table component to use the new footer calculation. Using both props will + result in the footerValues prop being ignored. + */ + useEffect(() => { + if (!footerOptions) { + setFooterItems(footerValues); + } + }, [footerValues, footerOptions]); + + useEffect(() => { + if (!footerOptions) { + return; + } + + if (!footerOptions.show) { + setFooterItems(undefined); + return; + } + + if (isCountRowsSet) { + const footerItemsCountRows: FooterItem[] = []; + footerItemsCountRows[0] = rows.length.toString() ?? data.length.toString(); + setFooterItems(footerItemsCountRows); + return; + } + + const footerItems = getFooterItems( + headerGroups[0].headers, + createFooterCalculationValues(rows), + footerOptions, + theme + ); + + setFooterItems(footerItems); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [footerOptions, theme, state.filters, data]); + + let listHeight = height - (headerHeight + footerHeight); + + if (enablePagination) { + listHeight -= tableStyles.cellHeight; + } + + const pageSize = Math.round(listHeight / tableStyles.rowHeight) - 1; + + useEffect(() => { + // Don't update the page size if it is less than 1 + if (pageSize <= 0) { + return; + } + setPageSize(pageSize); + }, [pageSize, setPageSize]); + + useEffect(() => { + // Reset page index when data changes + // This is needed because react-table does not do this automatically + // autoResetPage is set to false because setting it to true causes the issue described in + // https://github.com/grafana/grafana/pull/67477 + if (data.length / pageSize < state.pageIndex) { + gotoPage(0); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data]); + + useResetVariableListSizeCache(extendedState, listRef, data, hasUniqueId); + useFixScrollbarContainer(variableSizeListScrollbarRef, tableDivRef); + + const onNavigate = useCallback( + (toPage: number) => { + gotoPage(toPage - 1); + }, + [gotoPage] + ); + + const itemCount = enablePagination ? page.length : rows.length; + let paginationEl = null; + if (enablePagination) { + const itemsRangeStart = state.pageIndex * state.pageSize + 1; + let itemsRangeEnd = itemsRangeStart + state.pageSize - 1; + const isSmall = width < 550; + if (itemsRangeEnd > data.length) { + itemsRangeEnd = data.length; + } + const numRows = rows.length; + const displayedEnd = itemsRangeEnd < rows.length ? itemsRangeEnd : rows.length; + paginationEl = ( +
+ + {isSmall ? null : ( +
+ + {{ itemsRangeStart }} - {{ displayedEnd }} of {{ numRows }} rows + +
+ )} +
+ ); + } + + // Try to determine the longest field + // TODO: do we wrap only one field? + // What if there are multiple fields with long text? + const longestField = fieldConfig ? guessLongestField(fieldConfig, data) : undefined; + let textWrapField = undefined; + if (fieldConfig !== undefined) { + data.fields.forEach((field) => { + fieldConfig.overrides.forEach((override) => { + const matcher = getFieldMatcher(override.matcher); + if (matcher(field, data, [data])) { + for (const property of override.properties) { + if (property.id === 'custom.cellOptions' && property.value.wrapText) { + textWrapField = field; + } + } + } + }); + }); + } + + return ( +
+ +
+ {!noHeader && ( + + )} + {itemCount > 0 ? ( +
+ +
+ ) : ( +
+ {noValuesDisplayText} +
+ )} + {footerItems && ( + + )} +
+
+ {paginationEl} +
+ ); +}); + +Table.displayName = 'Table'; diff --git a/packages/grafana-ui/src/components/Table/styles.ts b/packages/grafana-ui/src/components/Table/TableRT/styles.ts similarity index 100% rename from packages/grafana-ui/src/components/Table/styles.ts rename to packages/grafana-ui/src/components/Table/TableRT/styles.ts diff --git a/packages/grafana-ui/src/components/Table/reducer.ts b/packages/grafana-ui/src/components/Table/reducer.ts index 97ebf8869dd..13247ab71b4 100644 --- a/packages/grafana-ui/src/components/Table/reducer.ts +++ b/packages/grafana-ui/src/components/Table/reducer.ts @@ -2,7 +2,13 @@ import { useCallback } from 'react'; import { getFieldDisplayName } from '@grafana/data'; -import { TableSortByFieldState, GrafanaTableColumn, GrafanaTableState, TableStateReducerProps, Props } from './types'; +import { + TableSortByFieldState, + GrafanaTableColumn, + GrafanaTableState, + TableStateReducerProps, + GeneralTableProps, +} from './types'; export interface ActionType { type: string; @@ -63,7 +69,7 @@ export function useTableStateReducer({ onColumnResize, onSortByChange, data }: T } export function getInitialState( - initialSortBy: Props['initialSortBy'], + initialSortBy: GeneralTableProps['initialSortBy'], columns: GrafanaTableColumn[] ): Partial { const state: Partial = {}; diff --git a/packages/grafana-ui/src/components/Table/types.ts b/packages/grafana-ui/src/components/Table/types.ts index ce1a0809f9d..0a5a24b0fc1 100644 --- a/packages/grafana-ui/src/components/Table/types.ts +++ b/packages/grafana-ui/src/components/Table/types.ts @@ -15,7 +15,7 @@ import { import * as schema from '@grafana/schema'; import { TableCellInspectorMode } from './TableCellInspector'; -import { TableStyles } from './styles'; +import { TableStyles } from './TableRT/styles'; export { type FieldTextAlignment, @@ -96,7 +96,8 @@ export interface TableStateReducerProps { data: DataFrame; } -export interface Props { +// export interface Props { +export interface BaseTableProps { ariaLabel?: string; data: DataFrame; width: number; @@ -125,6 +126,22 @@ export interface Props { replaceVariables?: InterpolateFunction; } +export interface GeneralTableProps extends BaseTableProps { + // Should the next generation table based off of react-data-grid be used + // 🗻 BIG 🗻 if true + useTableNg?: boolean; +} + +/** + * Props for the react-data-grid based table. + */ +export interface TableNGProps extends BaseTableProps {} + +/** + * Props for the react-table based table. + */ +export interface TableRTProps extends BaseTableProps {} + /** * @alpha * Props that will be passed to the TableCustomCellOptions.cellComponent when rendered. diff --git a/packages/grafana-ui/src/components/Table/utils.ts b/packages/grafana-ui/src/components/Table/utils.ts index 07c13c203ef..4031d019e87 100644 --- a/packages/grafana-ui/src/components/Table/utils.ts +++ b/packages/grafana-ui/src/components/Table/utils.ts @@ -32,16 +32,15 @@ import { import { getTextColorForAlphaBackground } from '../../utils'; import { ActionsCell } from './ActionsCell'; -import { BarGaugeCell } from './BarGaugeCell'; -import { DataLinksCell } from './DataLinksCell'; -import { DefaultCell } from './DefaultCell'; -import { getFooterValue } from './FooterRow'; -import { GeoCell } from './GeoCell'; -import { ImageCell } from './ImageCell'; -import { JSONViewCell } from './JSONViewCell'; -import { RowExpander } from './RowExpander'; -import { SparklineCell } from './SparklineCell'; -import { TableStyles } from './styles'; +import { BarGaugeCell } from './Cells/BarGaugeCell'; +import { DataLinksCell } from './Cells/DataLinksCell'; +import { DefaultCell } from './Cells/DefaultCell'; +import { GeoCell } from './Cells/GeoCell'; +import { ImageCell } from './Cells/ImageCell'; +import { JSONViewCell } from './Cells/JSONViewCell'; +import { SparklineCell } from './Cells/SparklineCell'; +import { getFooterValue } from './TableRT/FooterRow'; +import { RowExpander } from './TableRT/RowExpander'; import { CellColors, CellComponent, @@ -604,12 +603,12 @@ export function calculateAroundPointThreshold(timeField: Field): number { * @returns CellColors */ export function getCellColors( - tableStyles: TableStyles, + theme: GrafanaTheme2, cellOptions: TableCellOptions, displayValue: DisplayValue ): CellColors { // How much to darken elements depends upon if we're in dark mode - const darkeningFactor = tableStyles.theme.isDark ? 1 : -0.7; + const darkeningFactor = theme.isDark ? 1 : -0.7; // Setup color variables let textColor: string | undefined = undefined; @@ -622,7 +621,7 @@ export function getCellColors( const mode = cellOptions.mode ?? TableCellBackgroundDisplayMode.Gradient; if (mode === TableCellBackgroundDisplayMode.Basic) { - textColor = getTextColorForAlphaBackground(displayValue.color!, tableStyles.theme.isDark); + textColor = getTextColorForAlphaBackground(displayValue.color!, theme.isDark); bgColor = tinycolor(displayValue.color).toRgbString(); bgHoverColor = tinycolor(displayValue.color).setAlpha(1).toRgbString(); } else if (mode === TableCellBackgroundDisplayMode.Gradient) { @@ -630,7 +629,7 @@ export function getCellColors( const bgColor2 = tinycolor(displayValue.color) .darken(10 * darkeningFactor) .spin(5); - textColor = getTextColorForAlphaBackground(displayValue.color!, tableStyles.theme.isDark); + textColor = getTextColorForAlphaBackground(displayValue.color!, theme.isDark); bgColor = `linear-gradient(120deg, ${bgColor2.toRgbString()}, ${displayValue.color})`; bgHoverColor = `linear-gradient(120deg, ${bgColor2.setAlpha(1).toRgbString()}, ${hoverColor})`; } diff --git a/packages/grafana-ui/src/internal/index.ts b/packages/grafana-ui/src/internal/index.ts index 5dee05d7bb1..b49a258b021 100644 --- a/packages/grafana-ui/src/internal/index.ts +++ b/packages/grafana-ui/src/internal/index.ts @@ -63,9 +63,9 @@ export { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR, } from '../components/Table/types'; -export { defaultSparklineCellConfig } from '../components/Table/SparklineCell'; -export { TableCell } from '../components/Table/TableCell'; -export { useTableStyles } from '../components/Table/styles'; +export { defaultSparklineCellConfig } from '../components/Table/Cells/SparklineCell'; +export { TableCell } from '../components/Table/Cells/TableCell'; +export { useTableStyles } from '../components/Table/TableRT/styles'; export { migrateTableDisplayModeToCellOptions } from '../components/Table/utils'; export { type DataLinksContextMenuApi } from '../components/DataLinks/DataLinksContextMenu'; export { MenuDivider } from '../components/Menu/MenuDivider'; diff --git a/pkg/services/featuremgmt/codeowners.go b/pkg/services/featuremgmt/codeowners.go index a201b5e4b23..a7bddf657ee 100644 --- a/pkg/services/featuremgmt/codeowners.go +++ b/pkg/services/featuremgmt/codeowners.go @@ -28,6 +28,7 @@ const ( enterpriseDatasourcesSquad codeowner = "@grafana/enterprise-datasources" grafanaSharingSquad codeowner = "@grafana/sharing-squad" grafanaDatabasesFrontend codeowner = "@grafana/databases-frontend" + grafanaMesaVerde codeowner = "@grafana/mesa-verde" grafanaOSSBigTent codeowner = "@grafana/oss-big-tent" growthAndOnboarding codeowner = "@grafana/growth-and-onboarding" grafanaDatasourcesCoreServicesSquad codeowner = "@grafana/grafana-datasources-core-services" diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 89f2d6f8220..f0ca3f1b85d 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1283,6 +1283,12 @@ var ( Owner: grafanaDashboardsSquad, Expression: "true", // enabled by default }, + { + Name: "tableNextGen", + Description: "Allows access to the new react-data-grid based table component.", + Stage: FeatureStageExperimental, + Owner: grafanaDatavizSquad, + }, { Name: "lokiSendDashboardPanelNames", Description: "Send dashboard and panel names to Loki when querying", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 62bd4844236..cdd1398bfef 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -167,6 +167,7 @@ prometheusAzureOverrideAudience,deprecated,@grafana/partner-datasources,false,fa alertingFilterV2,experimental,@grafana/alerting-squad,false,false,false dataplaneAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,false newFiltersUI,GA,@grafana/dashboards-squad,false,false,false +tableNextGen,experimental,@grafana/dataviz-squad,false,false,false lokiSendDashboardPanelNames,experimental,@grafana/observability-logs,false,false,false alertingPrometheusRulesPrimary,experimental,@grafana/alerting-squad,false,false,true exploreLogsShardSplitting,experimental,@grafana/observability-logs,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index b1d5e756e5b..44583a1b4ee 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -679,6 +679,10 @@ const ( // Enables new combobox style UI for the Ad hoc filters variable in scenes architecture FlagNewFiltersUI = "newFiltersUI" + // FlagTableNextGen + // Allows access to the new react-data-grid based table component. + FlagTableNextGen = "tableNextGen" + // FlagLokiSendDashboardPanelNames // Send dashboard and panel names to Loki when querying FlagLokiSendDashboardPanelNames = "lokiSendDashboardPanelNames" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 1d0e357c378..e937785be85 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -3978,6 +3978,31 @@ "codeowner": "@grafana/search-and-storage" } }, + { + "metadata": { + "name": "tableNG", + "resourceVersion": "1742417098717", + "creationTimestamp": "2025-03-19T20:44:58Z", + "deletionTimestamp": "2025-03-25T17:19:06Z" + }, + "spec": { + "description": "Allows access to the new react-data-grid based table component.", + "stage": "experimental", + "codeowner": "@grafana/dataviz-squad" + } + }, + { + "metadata": { + "name": "tableNextGen", + "resourceVersion": "1742923146714", + "creationTimestamp": "2025-03-25T17:19:06Z" + }, + "spec": { + "description": "Allows access to the new react-data-grid based table component.", + "stage": "experimental", + "codeowner": "@grafana/dataviz-squad" + } + }, { "metadata": { "name": "tableSharedCrosshair", diff --git a/public/app/features/search/page/components/SearchResultsTable.test.tsx b/public/app/features/search/page/components/SearchResultsTable.test.tsx index dc4045b5bcf..d0f6e6c7992 100644 --- a/public/app/features/search/page/components/SearchResultsTable.test.tsx +++ b/public/app/features/search/page/components/SearchResultsTable.test.tsx @@ -150,7 +150,7 @@ describe('SearchResultsTable', () => { width={1000} /> ); - const noData = await screen.findByText('No data'); + const noData = await screen.findByText('No values'); expect(noData).toBeInTheDocument(); expect(screen.queryByRole('table', { name: 'Search results table' })).not.toBeInTheDocument(); }); diff --git a/public/app/features/search/page/components/SearchResultsTable.tsx b/public/app/features/search/page/components/SearchResultsTable.tsx index 7ec0a601da6..cd6b0564436 100644 --- a/public/app/features/search/page/components/SearchResultsTable.tsx +++ b/public/app/features/search/page/components/SearchResultsTable.tsx @@ -10,6 +10,7 @@ import { Field, GrafanaTheme2 } from '@grafana/data'; import { TableCellHeight } from '@grafana/schema'; import { useStyles2, useTheme2 } from '@grafana/ui'; import { useTableStyles, TableCell } from '@grafana/ui/internal'; +import { Trans } from 'app/core/internationalization'; import { useCustomFlexLayout } from 'app/features/browse-dashboards/components/customFlexTableLayout'; import { useSearchKeyboardNavigation } from '../../hooks/useSearchKeyboardSelection'; @@ -168,7 +169,11 @@ export const SearchResultsTable = React.memo( ); if (!rows.length) { - return
No data
; + return ( +
+ No values +
+ ); } return ( diff --git a/public/app/plugins/panel/table/TablePanel.tsx b/public/app/plugins/panel/table/TablePanel.tsx index a5dfbf92c63..5ec38ca084a 100644 --- a/public/app/plugins/panel/table/TablePanel.tsx +++ b/public/app/plugins/panel/table/TablePanel.tsx @@ -34,6 +34,7 @@ export function TablePanel(props: Props) { const hasFields = frames.some((frame) => frame.fields.length > 0); const currentIndex = getCurrentFrameIndex(frames, options); const main = frames[currentIndex]; + const useTableNg = config.featureToggles.tableNextGen; let tableHeight = height; @@ -68,6 +69,7 @@ export function TablePanel(props: Props) { timeRange={timeRange} enableSharedCrosshair={config.featureToggles.tableSharedCrosshair && enableSharedCrosshair} fieldConfig={fieldConfig} + useTableNg={useTableNg} getActions={getCellActions} replaceVariables={replaceVariables} /> diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index bf22d37294e..5e041f22713 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -2647,6 +2647,7 @@ "cell-filter-out": "Filter out value", "cell-inspect": "Inspect value", "copy": "Copy to Clipboard", + "count": "Count", "csv-counts": "Rows:{{rows}}, Columns:{{columns}} <5>", "csv-placeholder": "Enter CSV here...", "filter-placeholder": "Filter values", diff --git a/yarn.lock b/yarn.lock index 9e0701d955e..0266de2b468 100644 --- a/yarn.lock +++ b/yarn.lock @@ -81,7 +81,7 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.3, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.24.2, @babel/code-frame@npm:^7.26.2": +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.3, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.24.2, @babel/code-frame@npm:^7.25.9, @babel/code-frame@npm:^7.26.2": version: 7.26.2 resolution: "@babel/code-frame@npm:7.26.2" dependencies: @@ -362,6 +362,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.25.9": + version: 7.26.7 + resolution: "@babel/parser@npm:7.26.7" + dependencies: + "@babel/types": "npm:^7.26.7" + bin: + parser: ./bin/babel-parser.js + checksum: 10/3ccc384366ca9a9b49c54f5b24c9d8cff9a505f2fbdd1cfc04941c8e1897084cc32f100e77900c12bc14a176cf88daa3c155faad680d9a23491b997fd2a59ffc + languageName: node + linkType: hard + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.25.9": version: 7.25.9 resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.25.9" @@ -1407,12 +1418,12 @@ __metadata: linkType: hard "@babel/runtime-corejs3@npm:^7.16.5, @babel/runtime-corejs3@npm:^7.20.7, @babel/runtime-corejs3@npm:^7.22.15, @babel/runtime-corejs3@npm:^7.24.7": - version: 7.26.9 - resolution: "@babel/runtime-corejs3@npm:7.26.9" + version: 7.26.0 + resolution: "@babel/runtime-corejs3@npm:7.26.0" dependencies: core-js-pure: "npm:^3.30.2" regenerator-runtime: "npm:^0.14.0" - checksum: 10/8e70aa88d68f3bf689d022b1fa93ea271a8accddea7136ddd1da3aae79edac597ff9444cabbbe8f89b2552729889282c9a6a0176e92e69d47fe96c4db8bbfedd + checksum: 10/fd813d8b5bfc412c083033638c937e13f621b3223161c4a20bb8532d77ae622b620915476bd265670f6a8fc1a76a017ffd738ad25ad24431953e3725247c6520 languageName: node linkType: hard @@ -1434,7 +1445,18 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.22.5, @babel/template@npm:^7.24.7, @babel/template@npm:^7.25.9, @babel/template@npm:^7.26.9, @babel/template@npm:^7.3.3": +"@babel/template@npm:^7.22.5, @babel/template@npm:^7.24.7, @babel/template@npm:^7.25.9, @babel/template@npm:^7.3.3": + version: 7.25.9 + resolution: "@babel/template@npm:7.25.9" + dependencies: + "@babel/code-frame": "npm:^7.25.9" + "@babel/parser": "npm:^7.25.9" + "@babel/types": "npm:^7.25.9" + checksum: 10/e861180881507210150c1335ad94aff80fd9e9be6202e1efa752059c93224e2d5310186ddcdd4c0f0b0fc658ce48cb47823f15142b5c00c8456dde54f5de80b2 + languageName: node + linkType: hard + +"@babel/template@npm:^7.26.9": version: 7.26.9 resolution: "@babel/template@npm:7.26.9" dependencies: @@ -1470,6 +1492,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.26.7": + version: 7.26.7 + resolution: "@babel/types@npm:7.26.7" + dependencies: + "@babel/helper-string-parser": "npm:^7.25.9" + "@babel/helper-validator-identifier": "npm:^7.25.9" + checksum: 10/2264efd02cc261ca5d1c5bc94497c8995238f28afd2b7483b24ea64dd694cf46b00d51815bf0c87f0d0061ea221569c77893aeecb0d4b4bb254e9c2f938d7669 + languageName: node + linkType: hard + "@bcoe/v8-coverage@npm:^0.2.3": version: 0.2.3 resolution: "@bcoe/v8-coverage@npm:0.2.3" @@ -1867,7 +1899,20 @@ __metadata: languageName: node linkType: hard -"@emotion/cache@npm:^11.11.0, @emotion/cache@npm:^11.13.5, @emotion/cache@npm:^11.14.0, @emotion/cache@npm:^11.4.0": +"@emotion/cache@npm:^11.11.0, @emotion/cache@npm:^11.13.5, @emotion/cache@npm:^11.4.0": + version: 11.13.5 + resolution: "@emotion/cache@npm:11.13.5" + dependencies: + "@emotion/memoize": "npm:^0.9.0" + "@emotion/sheet": "npm:^1.4.0" + "@emotion/utils": "npm:^1.4.2" + "@emotion/weak-memoize": "npm:^0.4.0" + stylis: "npm:4.2.0" + checksum: 10/d91139453d279cfd6f6f38180d3af2fdcee8c0fc6d9a6faa2cdce9a1211b294a8019ef45365bf1171e0687d1744a70ff760637b88ed46f7a9fe74db9dc36f4df + languageName: node + linkType: hard + +"@emotion/cache@npm:^11.14.0": version: 11.14.0 resolution: "@emotion/cache@npm:11.14.0" dependencies: @@ -2291,9 +2336,9 @@ __metadata: linkType: hard "@faker-js/faker@npm:^9.0.0": - version: 9.4.0 - resolution: "@faker-js/faker@npm:9.4.0" - checksum: 10/f9f3c984d941a1e086c97bbf35def701e2d35e0e36e321569a6ac2ae0b7d2b1f2bee868362751c011f0eb917204fd85e53f178b1b2a1775df1670dfb30527a5b + version: 9.2.0 + resolution: "@faker-js/faker@npm:9.2.0" + checksum: 10/c233aaa41ebc4f4bed36c1304c399c424b9e5abca9cb8fe54ce73d4162ec7acd66f294b90c85ed7a5df774400e44f0a10364bdd41f49e72cb519a48b5948939f languageName: node linkType: hard @@ -3699,6 +3744,7 @@ __metadata: react-calendar: "npm:^5.1.0" react-colorful: "npm:5.6.1" react-custom-scrollbars-2: "npm:4.5.0" + react-data-grid: "npm:7.0.0-beta.46" react-dom: "npm:18.3.1" react-dropzone: "npm:14.3.5" react-highlight-words: "npm:0.21.0" @@ -6624,8 +6670,8 @@ __metadata: linkType: hard "@rollup/plugin-typescript@npm:^12.1.0": - version: 12.1.2 - resolution: "@rollup/plugin-typescript@npm:12.1.2" + version: 12.1.1 + resolution: "@rollup/plugin-typescript@npm:12.1.1" dependencies: "@rollup/pluginutils": "npm:^5.1.0" resolve: "npm:^1.22.1" @@ -6638,7 +6684,7 @@ __metadata: optional: true tslib: optional: true - checksum: 10/1fd201b9430125686bcdb3eecfa4f8d9d5005a9d454f2d733c0fdc0f58e28f772209ecbfe4f5d1a42a2a7ac25746a5f37d15897d695326eaea0fe3a30857375d + checksum: 10/838d5e67d1b383154fab7ae1b0c58e91844c70380210b12c1d5f2ed5d2264d4fbd21ff991a13a4a72078dce897b5c482c70554a21671269219aa9d2525f14dcd languageName: node linkType: hard @@ -6671,135 +6717,128 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.32.1": - version: 4.32.1 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.32.1" +"@rollup/rollup-android-arm-eabi@npm:4.26.0": + version: 4.26.0 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.26.0" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.32.1": - version: 4.32.1 - resolution: "@rollup/rollup-android-arm64@npm:4.32.1" +"@rollup/rollup-android-arm64@npm:4.26.0": + version: 4.26.0 + resolution: "@rollup/rollup-android-arm64@npm:4.26.0" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.32.1": - version: 4.32.1 - resolution: "@rollup/rollup-darwin-arm64@npm:4.32.1" +"@rollup/rollup-darwin-arm64@npm:4.26.0": + version: 4.26.0 + resolution: "@rollup/rollup-darwin-arm64@npm:4.26.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.32.1": - version: 4.32.1 - resolution: "@rollup/rollup-darwin-x64@npm:4.32.1" +"@rollup/rollup-darwin-x64@npm:4.26.0": + version: 4.26.0 + resolution: "@rollup/rollup-darwin-x64@npm:4.26.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-freebsd-arm64@npm:4.32.1": - version: 4.32.1 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.32.1" +"@rollup/rollup-freebsd-arm64@npm:4.26.0": + version: 4.26.0 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.26.0" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-freebsd-x64@npm:4.32.1": - version: 4.32.1 - resolution: "@rollup/rollup-freebsd-x64@npm:4.32.1" +"@rollup/rollup-freebsd-x64@npm:4.26.0": + version: 4.26.0 + resolution: "@rollup/rollup-freebsd-x64@npm:4.26.0" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.32.1": - version: 4.32.1 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.32.1" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.26.0": + version: 4.26.0 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.26.0" conditions: os=linux & cpu=arm & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.32.1": - version: 4.32.1 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.32.1" +"@rollup/rollup-linux-arm-musleabihf@npm:4.26.0": + version: 4.26.0 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.26.0" conditions: os=linux & cpu=arm & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.32.1": - version: 4.32.1 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.32.1" +"@rollup/rollup-linux-arm64-gnu@npm:4.26.0": + version: 4.26.0 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.26.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.32.1": - version: 4.32.1 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.32.1" +"@rollup/rollup-linux-arm64-musl@npm:4.26.0": + version: 4.26.0 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.26.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-loongarch64-gnu@npm:4.32.1": - version: 4.32.1 - resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.32.1" - conditions: os=linux & cpu=loong64 & libc=glibc - languageName: node - linkType: hard - -"@rollup/rollup-linux-powerpc64le-gnu@npm:4.32.1": - version: 4.32.1 - resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.32.1" +"@rollup/rollup-linux-powerpc64le-gnu@npm:4.26.0": + version: 4.26.0 + resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.26.0" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.32.1": - version: 4.32.1 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.32.1" +"@rollup/rollup-linux-riscv64-gnu@npm:4.26.0": + version: 4.26.0 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.26.0" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.32.1": - version: 4.32.1 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.32.1" +"@rollup/rollup-linux-s390x-gnu@npm:4.26.0": + version: 4.26.0 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.26.0" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.32.1": - version: 4.32.1 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.32.1" +"@rollup/rollup-linux-x64-gnu@npm:4.26.0": + version: 4.26.0 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.26.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.32.1": - version: 4.32.1 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.32.1" +"@rollup/rollup-linux-x64-musl@npm:4.26.0": + version: 4.26.0 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.26.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.32.1": - version: 4.32.1 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.32.1" +"@rollup/rollup-win32-arm64-msvc@npm:4.26.0": + version: 4.26.0 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.26.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.32.1": - version: 4.32.1 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.32.1" +"@rollup/rollup-win32-ia32-msvc@npm:4.26.0": + version: 4.26.0 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.26.0" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.32.1": - version: 4.32.1 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.32.1" +"@rollup/rollup-win32-x64-msvc@npm:4.26.0": + version: 4.26.0 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.26.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -7807,6 +7846,20 @@ __metadata: languageName: node linkType: hard +"@swagger-api/apidom-ast@npm:^1.0.0-beta.11, @swagger-api/apidom-ast@npm:^1.0.0-beta.5": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-ast@npm:1.0.0-beta.11" + dependencies: + "@babel/runtime-corejs3": "npm:^7.20.7" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.11" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + unraw: "npm:^3.0.0" + checksum: 10/efe7caf37735f6b1a9b56ca780ce59134540c00c50ef00606607a458905142b7794466cacbffdcd9afe11ece9f5a99651f2f62d24b05f71727d86718e2dea91c + languageName: node + linkType: hard + "@swagger-api/apidom-ast@npm:^1.0.0-beta.12": version: 1.0.0-beta.12 resolution: "@swagger-api/apidom-ast@npm:1.0.0-beta.12" @@ -7821,7 +7874,24 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-core@npm:>=1.0.0-beta.12 <1.0.0-rc.0, @swagger-api/apidom-core@npm:^1.0.0-beta.12": +"@swagger-api/apidom-core@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-core@npm:^1.0.0-beta.11, @swagger-api/apidom-core@npm:^1.0.0-beta.5": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-core@npm:1.0.0-beta.11" + dependencies: + "@babel/runtime-corejs3": "npm:^7.20.7" + "@swagger-api/apidom-ast": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.11" + "@types/ramda": "npm:~0.30.0" + minim: "npm:~0.23.8" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + short-unique-id: "npm:^5.0.2" + ts-mixer: "npm:^6.0.3" + checksum: 10/bb81c1ef603b80985c8e7ad8808a12eaaf2daa3de2a41ac2f9f8c041c7a7f26bcbfa9713738248db198af1a6b424127b10abd5ac7e6fcad95d852cea8fb8a526 + languageName: node + linkType: hard + +"@swagger-api/apidom-core@npm:^1.0.0-beta.12": version: 1.0.0-beta.12 resolution: "@swagger-api/apidom-core@npm:1.0.0-beta.12" dependencies: @@ -7838,7 +7908,16 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-error@npm:>=1.0.0-beta.12 <1.0.0-rc.0, @swagger-api/apidom-error@npm:^1.0.0-beta.12, @swagger-api/apidom-error@npm:^1.0.0-beta.3 <1.0.0-rc.0": +"@swagger-api/apidom-error@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-error@npm:^1.0.0-beta.11, @swagger-api/apidom-error@npm:^1.0.0-beta.5": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-error@npm:1.0.0-beta.11" + dependencies: + "@babel/runtime-corejs3": "npm:^7.20.7" + checksum: 10/d63ec68067a17c10fa51b2929c18d18f397d91ab45961e398ad99341b45e87d60a0ec6f634ac3883733ea7f4a5155e9929207dd44572f65754fa0f5b7d004466 + languageName: node + linkType: hard + +"@swagger-api/apidom-error@npm:^1.0.0-beta.12, @swagger-api/apidom-error@npm:^1.0.0-beta.3 <1.0.0-rc.0": version: 1.0.0-beta.12 resolution: "@swagger-api/apidom-error@npm:1.0.0-beta.12" dependencies: @@ -7847,7 +7926,21 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-json-pointer@npm:>=1.0.0-beta.12 <1.0.0-rc.0, @swagger-api/apidom-json-pointer@npm:^1.0.0-beta.12, @swagger-api/apidom-json-pointer@npm:^1.0.0-beta.3 <1.0.0-rc.0": +"@swagger-api/apidom-json-pointer@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-json-pointer@npm:^1.0.0-beta.11": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-json-pointer@npm:1.0.0-beta.11" + dependencies: + "@babel/runtime-corejs3": "npm:^7.20.7" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.11" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + checksum: 10/dee858ee4b4a0f93ab082451a703c8b9e0250f7b486d8d9ef243b77f92acc49741cfdd1e10a9cc44d946b64e469aa4016a1380c7a28d660fe048a2897098afc7 + languageName: node + linkType: hard + +"@swagger-api/apidom-json-pointer@npm:^1.0.0-beta.12, @swagger-api/apidom-json-pointer@npm:^1.0.0-beta.3 <1.0.0-rc.0": version: 1.0.0-beta.12 resolution: "@swagger-api/apidom-json-pointer@npm:1.0.0-beta.12" dependencies: @@ -7861,34 +7954,50 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-ns-api-design-systems@npm:^1.0.0-beta.12": - version: 1.0.0-beta.12 - resolution: "@swagger-api/apidom-ns-api-design-systems@npm:1.0.0-beta.12" +"@swagger-api/apidom-ns-api-design-systems@npm:^1.0.0-beta.5": + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-ns-api-design-systems@npm:1.0.0-beta.5" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-error": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-ns-openapi-3-1": "npm:^1.0.0-beta.12" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ns-openapi-3-1": "npm:^1.0.0-beta.5" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" ts-mixer: "npm:^6.0.3" - checksum: 10/8e63c9e46a65f672fd9ac04f4ad826fa0ed89041b0b7f32e90569b953f6917cb75b6c72124ee9f6b98542ab4f85a9f88e285c66b461e6afcfbcf0c69011ea898 + checksum: 10/4477a3a152efc98a76ce84eb3b3df1ca2cb5f4080c5b93b271178ae2050e2ea2a917e80fe8095cb6e4a6f6036fdc6203cbe8b870db669935ac669e55346873e9 languageName: node linkType: hard -"@swagger-api/apidom-ns-asyncapi-2@npm:^1.0.0-beta.12, @swagger-api/apidom-ns-asyncapi-2@npm:^1.0.0-beta.3 <1.0.0-rc.0": - version: 1.0.0-beta.12 - resolution: "@swagger-api/apidom-ns-asyncapi-2@npm:1.0.0-beta.12" +"@swagger-api/apidom-ns-asyncapi-2@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-ns-asyncapi-2@npm:^1.0.0-beta.5": + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-ns-asyncapi-2@npm:1.0.0-beta.5" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-ns-json-schema-draft-7": "npm:^1.0.0-beta.12" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ns-json-schema-draft-7": "npm:^1.0.0-beta.5" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" ts-mixer: "npm:^6.0.3" - checksum: 10/0046da6df4e6c492aa5cd901bfe685a10d3580b8a7f0b9abe003e2f4e7531d15633b39fa056dae86c6a09602e7898dd0cce3c6182b9698533f2f458eb06d769a + checksum: 10/e1d29ce06f34ae863230241c42706673855c3835779e51e11e9d80e3935f0084c489cf134804c7c0e4af97ca3dbf80c36f05920ebc9246b4d3627fcf9e596a17 + languageName: node + linkType: hard + +"@swagger-api/apidom-ns-json-schema-2019-09@npm:^1.0.0-beta.11": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-ns-json-schema-2019-09@npm:1.0.0-beta.11" + dependencies: + "@babel/runtime-corejs3": "npm:^7.20.7" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-ns-json-schema-draft-7": "npm:^1.0.0-beta.11" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + ts-mixer: "npm:^6.0.4" + checksum: 10/b91f90e7376922f1112752520bcac265c20245c1d2d682cd5ba9344a3eb7287962f18e3430f501af4d4db43397a2977267b15ef02a3bcd6e8ba384901e195672 languageName: node linkType: hard @@ -7908,6 +8017,22 @@ __metadata: languageName: node linkType: hard +"@swagger-api/apidom-ns-json-schema-2020-12@npm:^1.0.0-beta.11": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-ns-json-schema-2020-12@npm:1.0.0-beta.11" + dependencies: + "@babel/runtime-corejs3": "npm:^7.20.7" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-ns-json-schema-2019-09": "npm:^1.0.0-beta.11" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + ts-mixer: "npm:^6.0.4" + checksum: 10/847103b0923fa7a0fcff124f5359f735f56e38a88666051e5959403e0060e33bea63b30098dec99d36bcda6619b2a3d13ce61c26e6ca03c508ea8518e871c123 + languageName: node + linkType: hard + "@swagger-api/apidom-ns-json-schema-2020-12@npm:^1.0.0-beta.12": version: 1.0.0-beta.12 resolution: "@swagger-api/apidom-ns-json-schema-2020-12@npm:1.0.0-beta.12" @@ -7924,6 +8049,21 @@ __metadata: languageName: node linkType: hard +"@swagger-api/apidom-ns-json-schema-draft-4@npm:^1.0.0-beta.11, @swagger-api/apidom-ns-json-schema-draft-4@npm:^1.0.0-beta.5": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-ns-json-schema-draft-4@npm:1.0.0-beta.11" + dependencies: + "@babel/runtime-corejs3": "npm:^7.20.7" + "@swagger-api/apidom-ast": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + ts-mixer: "npm:^6.0.4" + checksum: 10/9e0ba9e29849915c0067227a82d8d9a741221b3ac73592b73e23d3cfe0c57548b21cbbb66cdeba914218c5f675f4ec497232d465880013f8aec8f486e4c3d91c + languageName: node + linkType: hard + "@swagger-api/apidom-ns-json-schema-draft-4@npm:^1.0.0-beta.12": version: 1.0.0-beta.12 resolution: "@swagger-api/apidom-ns-json-schema-draft-4@npm:1.0.0-beta.12" @@ -7939,6 +8079,22 @@ __metadata: languageName: node linkType: hard +"@swagger-api/apidom-ns-json-schema-draft-6@npm:^1.0.0-beta.11": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-ns-json-schema-draft-6@npm:1.0.0-beta.11" + dependencies: + "@babel/runtime-corejs3": "npm:^7.20.7" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-ns-json-schema-draft-4": "npm:^1.0.0-beta.11" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + ts-mixer: "npm:^6.0.4" + checksum: 10/0df53880202cdb8dbe1e534314842c2c10fe4cd03f6eecd56d67174492807df03134f360e1c0724610fc4531520c49bb41d81e6fd7373118031ad883e0101338 + languageName: node + linkType: hard + "@swagger-api/apidom-ns-json-schema-draft-6@npm:^1.0.0-beta.12": version: 1.0.0-beta.12 resolution: "@swagger-api/apidom-ns-json-schema-draft-6@npm:1.0.0-beta.12" @@ -7955,6 +8111,22 @@ __metadata: languageName: node linkType: hard +"@swagger-api/apidom-ns-json-schema-draft-7@npm:^1.0.0-beta.11, @swagger-api/apidom-ns-json-schema-draft-7@npm:^1.0.0-beta.5": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-ns-json-schema-draft-7@npm:1.0.0-beta.11" + dependencies: + "@babel/runtime-corejs3": "npm:^7.20.7" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-ns-json-schema-draft-6": "npm:^1.0.0-beta.11" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + ts-mixer: "npm:^6.0.4" + checksum: 10/9ef5261e58b2e6798a3c22ad01940c613475a6515d2cd79d2f150b02152118036ef472bb75a271c0d1c3b617d51ccb2262680469824335f27150600f695eb6d8 + languageName: node + linkType: hard + "@swagger-api/apidom-ns-json-schema-draft-7@npm:^1.0.0-beta.12": version: 1.0.0-beta.12 resolution: "@swagger-api/apidom-ns-json-schema-draft-7@npm:1.0.0-beta.12" @@ -7971,19 +8143,35 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-ns-openapi-2@npm:^1.0.0-beta.12, @swagger-api/apidom-ns-openapi-2@npm:^1.0.0-beta.3 <1.0.0-rc.0": - version: 1.0.0-beta.12 - resolution: "@swagger-api/apidom-ns-openapi-2@npm:1.0.0-beta.12" +"@swagger-api/apidom-ns-openapi-2@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-2@npm:^1.0.0-beta.5": + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-ns-openapi-2@npm:1.0.0-beta.5" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-error": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-ns-json-schema-draft-4": "npm:^1.0.0-beta.12" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ns-json-schema-draft-4": "npm:^1.0.0-beta.5" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" ts-mixer: "npm:^6.0.3" - checksum: 10/93a3d01f4aa8b7e189cae03b0a8c250e3e5b3e915321d3f500707c8f198b3f77d94fdde59f703a66949d5bb6f94ea740730badc6600b1b314c9b8a1b7ee143d5 + checksum: 10/4799019b8535e8c38da5b53219a2ecedebe3db9b943f2fa91370d23f6297e90c0c1b516a436f3d0669b652e3002d6f10d5952c3ef59cb41173ef1e24e63c65df + languageName: node + linkType: hard + +"@swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-beta.11, @swagger-api/apidom-ns-openapi-3-0@npm:^1.0.0-beta.5": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-ns-openapi-3-0@npm:1.0.0-beta.11" + dependencies: + "@babel/runtime-corejs3": "npm:^7.20.7" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-ns-json-schema-draft-4": "npm:^1.0.0-beta.11" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + ts-mixer: "npm:^6.0.3" + checksum: 10/44eabda02fb8ad965b7756931bba7ac2ae9f49ef720f401179efb9eafbf94ff6b3c8cb8fd260c9ddb1affe35c3876f780cef4cb090b755736847adb7eba699c8 languageName: node linkType: hard @@ -8003,7 +8191,25 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-ns-openapi-3-1@npm:>=1.0.0-beta.12 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-1@npm:^1.0.0-beta.12, @swagger-api/apidom-ns-openapi-3-1@npm:^1.0.0-beta.3 <1.0.0-rc.0": +"@swagger-api/apidom-ns-openapi-3-1@npm:>=1.0.0-beta.11 <1.0.0-rc.0, @swagger-api/apidom-ns-openapi-3-1@npm:^1.0.0-beta.5": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-ns-openapi-3-1@npm:1.0.0-beta.11" + dependencies: + "@babel/runtime-corejs3": "npm:^7.20.7" + "@swagger-api/apidom-ast": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-json-pointer": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-ns-json-schema-2020-12": "npm:^1.0.0-beta.11" + "@swagger-api/apidom-ns-openapi-3-0": "npm:^1.0.0-beta.11" + "@types/ramda": "npm:~0.30.0" + ramda: "npm:~0.30.0" + ramda-adjunct: "npm:^5.0.0" + ts-mixer: "npm:^6.0.3" + checksum: 10/6ed3a3d7bc231c8f3273c60dcd459cdb117246e372ed9d0d2483601a966a3109b9617a4d8f327f494a31013d5bcf2bc372c5f3297bbf953c61ff4770c87414a5 + languageName: node + linkType: hard + +"@swagger-api/apidom-ns-openapi-3-1@npm:^1.0.0-beta.3 <1.0.0-rc.0": version: 1.0.0-beta.12 resolution: "@swagger-api/apidom-ns-openapi-3-1@npm:1.0.0-beta.12" dependencies: @@ -8021,89 +8227,89 @@ __metadata: languageName: node linkType: hard -"@swagger-api/apidom-ns-workflows-1@npm:^1.0.0-beta.12, @swagger-api/apidom-ns-workflows-1@npm:^1.0.0-beta.3 <1.0.0-rc.0": - version: 1.0.0-beta.12 - resolution: "@swagger-api/apidom-ns-workflows-1@npm:1.0.0-beta.12" +"@swagger-api/apidom-ns-workflows-1@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-ns-workflows-1@npm:^1.0.0-beta.5": + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-ns-workflows-1@npm:1.0.0-beta.5" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-ns-json-schema-2020-12": "npm:^1.0.0-beta.12" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ns-openapi-3-1": "npm:^1.0.0-beta.5" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" ts-mixer: "npm:^6.0.3" - checksum: 10/d682ef83795636e504f09c7d07e453524fb1270b335ce45e088ca815c356f47a8bb5e6db378f02b20c7883aab17b4c85d9a8402afa23a1518b2d9141663d7f25 + checksum: 10/6dc23a232e3ad175577ddb9da8e9b480321624bbca2b4f039dc988afebcb94510d4575b223b46ec93192b6330db9bce26a55e3100f6c2ebded22ca763d3bab0d languageName: node linkType: hard "@swagger-api/apidom-parser-adapter-api-design-systems-json@npm:^1.0.0-beta.3 <1.0.0-rc.0": - version: 1.0.0-beta.12 - resolution: "@swagger-api/apidom-parser-adapter-api-design-systems-json@npm:1.0.0-beta.12" + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-parser-adapter-api-design-systems-json@npm:1.0.0-beta.5" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-ns-api-design-systems": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-parser-adapter-json": "npm:^1.0.0-beta.12" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ns-api-design-systems": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-parser-adapter-json": "npm:^1.0.0-beta.5" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/278ddcc839a1dc16e399f43e0c84520eb215b3ebc013bc0f759f53ab36d7051dc0374cba183363a2f5458d3afc7075159cb1fe68230c1d9643dc41ca03048d98 + checksum: 10/4b28a92919c1f7669c6fbf296bc37d52f505f9adaa2763be429a3f837f6c8bd2c030dd012d0b5bc8188cd82ed51f31a64f2bea75a52628265aded0e599e14921 languageName: node linkType: hard "@swagger-api/apidom-parser-adapter-api-design-systems-yaml@npm:^1.0.0-beta.3 <1.0.0-rc.0": - version: 1.0.0-beta.12 - resolution: "@swagger-api/apidom-parser-adapter-api-design-systems-yaml@npm:1.0.0-beta.12" + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-parser-adapter-api-design-systems-yaml@npm:1.0.0-beta.5" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-ns-api-design-systems": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.0.0-beta.12" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ns-api-design-systems": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.0.0-beta.5" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/89629b285133f871f277e4e59ca4d73dcbc031b2caf68c84d496db4d253072856d4ff4980000f81375b1f83d682e4638e9026e70a955e135a75f00b04e265877 + checksum: 10/a1b70b4dfeb7087538bededf6254da653b72bffc6e0e44df6dc23fa063d28293ee7b9c894015639ec323f1ec4c88c93c3deae7c47a48ee752c34995eb25c3fcb languageName: node linkType: hard "@swagger-api/apidom-parser-adapter-asyncapi-json-2@npm:^1.0.0-beta.3 <1.0.0-rc.0": - version: 1.0.0-beta.12 - resolution: "@swagger-api/apidom-parser-adapter-asyncapi-json-2@npm:1.0.0-beta.12" + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-parser-adapter-asyncapi-json-2@npm:1.0.0-beta.5" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-ns-asyncapi-2": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-parser-adapter-json": "npm:^1.0.0-beta.12" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ns-asyncapi-2": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-parser-adapter-json": "npm:^1.0.0-beta.5" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/8bfd376425cc7de931441655b6b234537d5de1b363d6813d44d2ae0e9a1460884779be335ccc09551ddd5917101cc8ad1f6c135ba7fbed36c6d8516bb8e4c27c + checksum: 10/29025bef9e0d507bebb855506b410bc41073c64fcce4d22edb514919a6df8598ea46d127f07ceeec7d6d9891f9ff806fd28440a4d851e8d398faaa655a965aba languageName: node linkType: hard "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2@npm:^1.0.0-beta.3 <1.0.0-rc.0": - version: 1.0.0-beta.12 - resolution: "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2@npm:1.0.0-beta.12" + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2@npm:1.0.0-beta.5" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-ns-asyncapi-2": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.0.0-beta.12" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ns-asyncapi-2": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.0.0-beta.5" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/6156cfc431d76e6d6c6676715e240d618239d170c8b024bd337b16b0eae1d3356582701376149481154d45ee0aa82fe04dd6b21541e403a263ee4ec8073ca8f0 + checksum: 10/c226759fc2d4006dd7ee296f770dda39610f4c5fcdb5bf8f1dd8fc8a15c854742193429ea7a72a95a3adf51e1d19cf8d5856404a4d1fdf94b87ad88489366b40 languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-json@npm:^1.0.0-beta.12, @swagger-api/apidom-parser-adapter-json@npm:^1.0.0-beta.3 <1.0.0-rc.0": - version: 1.0.0-beta.12 - resolution: "@swagger-api/apidom-parser-adapter-json@npm:1.0.0-beta.12" +"@swagger-api/apidom-parser-adapter-json@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-parser-adapter-json@npm:^1.0.0-beta.5": + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-parser-adapter-json@npm:1.0.0-beta.5" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-ast": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-error": "npm:^1.0.0-beta.12" + "@swagger-api/apidom-ast": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" "@types/ramda": "npm:~0.30.0" node-gyp: "npm:latest" ramda: "npm:~0.30.0" @@ -8111,138 +8317,138 @@ __metadata: tree-sitter: "npm:=0.22.1" tree-sitter-json: "npm:=0.24.8" web-tree-sitter: "npm:=0.24.5" - checksum: 10/4f498f493f2467b8da527246e3c75161e21d59557c026fa3799c9e2a5930afff6fbe24b7291f2829610397550627bd5024f8f7bd0732f6310deba7184d81ce4b + checksum: 10/dc28419cbc068f5e2ecc58b0ccf807bb8717625c6043d7661d1488c054617384154d9990d6e5abed52f60209806f5b2a9eb5e20f3cd6d839290e69f6acc5871b languageName: node linkType: hard "@swagger-api/apidom-parser-adapter-openapi-json-2@npm:^1.0.0-beta.3 <1.0.0-rc.0": - version: 1.0.0-beta.12 - resolution: "@swagger-api/apidom-parser-adapter-openapi-json-2@npm:1.0.0-beta.12" + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-parser-adapter-openapi-json-2@npm:1.0.0-beta.5" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-ns-openapi-2": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-parser-adapter-json": "npm:^1.0.0-beta.12" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ns-openapi-2": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-parser-adapter-json": "npm:^1.0.0-beta.5" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/8c645926de9e257211180e91042e477556b5e5e47b4a0d629797343a7eae3d70154b760d85267fbf9bc88e1155278a4be757cbdc4438af0cdce841ee1f086cbc + checksum: 10/4ca82e7adacd632efda6019a4891676570c0f2ce7532ad926ab0e20ab1f7052b2321ae42099e01ca3e63bd16d8c1585eb31fcff3952e9584772289ef49600cf2 languageName: node linkType: hard "@swagger-api/apidom-parser-adapter-openapi-json-3-0@npm:^1.0.0-beta.3 <1.0.0-rc.0": - version: 1.0.0-beta.12 - resolution: "@swagger-api/apidom-parser-adapter-openapi-json-3-0@npm:1.0.0-beta.12" + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-parser-adapter-openapi-json-3-0@npm:1.0.0-beta.5" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-ns-openapi-3-0": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-parser-adapter-json": "npm:^1.0.0-beta.12" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ns-openapi-3-0": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-parser-adapter-json": "npm:^1.0.0-beta.5" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/5a0e050860c304c8825163a6f3bf46bc218e42ae7ac7c8e17d28886ae21662f1e6de726fbccfb844e02935c76822eb4fca419845c8e462a2cea9e415f1b17137 + checksum: 10/5db07e4f4c4ac43f04c75382ee88dbcddcfb0a0d78f52fda74cb7d48148eb55f3f54d65de770b0d17b12c5bec381c2a5a1d6f438eb0270fa9dc4b311fb3b0f51 languageName: node linkType: hard "@swagger-api/apidom-parser-adapter-openapi-json-3-1@npm:^1.0.0-beta.3 <1.0.0-rc.0": - version: 1.0.0-beta.12 - resolution: "@swagger-api/apidom-parser-adapter-openapi-json-3-1@npm:1.0.0-beta.12" + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-parser-adapter-openapi-json-3-1@npm:1.0.0-beta.5" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-ns-openapi-3-1": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-parser-adapter-json": "npm:^1.0.0-beta.12" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ns-openapi-3-1": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-parser-adapter-json": "npm:^1.0.0-beta.5" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/648a7575c992aac39a7f1a9e0a446700a5a7dfdfe75d44e5012a49d9571ef1748b4db38b11498c15f79e19a62d53550d931f7bff273e6b85e0a7edb3413de532 + checksum: 10/7f900a4d0d8ccc4503d5e159ae7ce0bc543f3b2318f6ac828937ea0018b8fb33a8fe0063a41d11a8d07b6e5dbc86671eebcba0de6a651825dce832a2a5c010a3 languageName: node linkType: hard "@swagger-api/apidom-parser-adapter-openapi-yaml-2@npm:^1.0.0-beta.3 <1.0.0-rc.0": - version: 1.0.0-beta.12 - resolution: "@swagger-api/apidom-parser-adapter-openapi-yaml-2@npm:1.0.0-beta.12" + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-parser-adapter-openapi-yaml-2@npm:1.0.0-beta.5" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-ns-openapi-2": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.0.0-beta.12" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ns-openapi-2": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.0.0-beta.5" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/72c808e812a7c5e0855ca2f9f2f735666bf26817ac8c49052157aaf9c2c8b317af4d17f56b6a82448f1062c631dda92630f90bbea1b7ce9cc4a46cd51206cbc9 + checksum: 10/fe98a67837e2fe5a705b5f3e7134099bbfa5aa185a3b4250bf20928fa0614b3d3fe51bbd174f2d9612d394e644b5c0e849bc48a07178a3c02ee103f3dd7ded1e languageName: node linkType: hard "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0@npm:^1.0.0-beta.3 <1.0.0-rc.0": - version: 1.0.0-beta.12 - resolution: "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0@npm:1.0.0-beta.12" + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0@npm:1.0.0-beta.5" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-ns-openapi-3-0": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.0.0-beta.12" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ns-openapi-3-0": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.0.0-beta.5" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/497e675e2b6d76b844ee8b5857c50f4d0de039a0e62ead8f3b8a98a6bab37106417d7b1413d8cd1e19237154c855d7522a1ed08cbeedc03fb77873ce51320e10 + checksum: 10/796aaab721880ad52ae09cf2ca71869d4e4f42fc77a4d4de1cc088b357302a299a59c43f08562bd1ad8b4f9d1e9d1545a4c28631da51bed3e66c59e0abd37cbb languageName: node linkType: hard "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1@npm:^1.0.0-beta.3 <1.0.0-rc.0": - version: 1.0.0-beta.12 - resolution: "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1@npm:1.0.0-beta.12" + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1@npm:1.0.0-beta.5" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-ns-openapi-3-1": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.0.0-beta.12" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ns-openapi-3-1": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.0.0-beta.5" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/b19532d2e7df227302e147d39ddfc448dde7c2f40cf3e2930cb18b9e421ee6a3e8ff44d565b130540f5207cda2f974672cfa8af5200256b87703fba928566735 + checksum: 10/c55d16cacbe8e9482f02f1806a15022e471b5fe664fe5554388e989b0ff120646a5f122249fcc10d772c1c7d43b7ef73cec6a47b9b1367762d65c61f1c753e0b languageName: node linkType: hard "@swagger-api/apidom-parser-adapter-workflows-json-1@npm:^1.0.0-beta.3 <1.0.0-rc.0": - version: 1.0.0-beta.12 - resolution: "@swagger-api/apidom-parser-adapter-workflows-json-1@npm:1.0.0-beta.12" + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-parser-adapter-workflows-json-1@npm:1.0.0-beta.5" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-ns-workflows-1": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-parser-adapter-json": "npm:^1.0.0-beta.12" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ns-workflows-1": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-parser-adapter-json": "npm:^1.0.0-beta.5" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/b5c41121bceb126aec2787baded7f519c34c71a4ac879533a7b9110ffdd5c74f11b6d96d00fe86517c39c9a2f5f44030cd800688a2663c9f80f9719a613a309f + checksum: 10/5030ac987a5d60de46d83eeadafddb9f3531882a40eacca332a3316bb7531dd4c332324cd9617454249c92b3eaeafd092b01d10b4b3356cc5b796c2e660aab87 languageName: node linkType: hard "@swagger-api/apidom-parser-adapter-workflows-yaml-1@npm:^1.0.0-beta.3 <1.0.0-rc.0": - version: 1.0.0-beta.12 - resolution: "@swagger-api/apidom-parser-adapter-workflows-yaml-1@npm:1.0.0-beta.12" + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-parser-adapter-workflows-yaml-1@npm:1.0.0-beta.5" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-ns-workflows-1": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.0.0-beta.12" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-ns-workflows-1": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-parser-adapter-yaml-1-2": "npm:^1.0.0-beta.5" "@types/ramda": "npm:~0.30.0" ramda: "npm:~0.30.0" ramda-adjunct: "npm:^5.0.0" - checksum: 10/b3e8d718a57b2fa1e571e345bdc97f8ee290554bf1065c089b0d99013d477a3e3130a5971a7b147109318d8b8aa0e9ba924a6238855565d0042ee372777b2ef6 + checksum: 10/00a1a6d3626ba2ff2bde0c1a9a3af2b89014bdae5f0b1af518a6d0d7e6718db1b9c0417a16d7e641c71e28a1807708c1d1e3d70965a08bd974f3b2b765f31546 languageName: node linkType: hard -"@swagger-api/apidom-parser-adapter-yaml-1-2@npm:^1.0.0-beta.12, @swagger-api/apidom-parser-adapter-yaml-1-2@npm:^1.0.0-beta.3 <1.0.0-rc.0": - version: 1.0.0-beta.12 - resolution: "@swagger-api/apidom-parser-adapter-yaml-1-2@npm:1.0.0-beta.12" +"@swagger-api/apidom-parser-adapter-yaml-1-2@npm:^1.0.0-beta.3 <1.0.0-rc.0, @swagger-api/apidom-parser-adapter-yaml-1-2@npm:^1.0.0-beta.5": + version: 1.0.0-beta.5 + resolution: "@swagger-api/apidom-parser-adapter-yaml-1-2@npm:1.0.0-beta.5" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-ast": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.12" - "@swagger-api/apidom-error": "npm:^1.0.0-beta.12" + "@swagger-api/apidom-ast": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.5" + "@swagger-api/apidom-error": "npm:^1.0.0-beta.5" "@tree-sitter-grammars/tree-sitter-yaml": "npm:=0.7.0" "@types/ramda": "npm:~0.30.0" node-gyp: "npm:latest" @@ -8250,16 +8456,16 @@ __metadata: ramda-adjunct: "npm:^5.0.0" tree-sitter: "npm:=0.22.1" web-tree-sitter: "npm:=0.24.5" - checksum: 10/7b0293de33a7585f1df9fc3ef2acbe9fb549c6166fb4b2a89b16babe583d3a3e41b9709836ea23f36b992ba710b11bc6a29b78daebb8115910101422092c728b + checksum: 10/8f14014d18a674447aa1e2fac4251794538f05cd86c79debcbd7cbfa7efaa3403385d292da6f96988b3d9f60b07b81e6cfce7a1bc779240513d6526160f7c116 languageName: node linkType: hard -"@swagger-api/apidom-reference@npm:>=1.0.0-beta.12 <1.0.0-rc.0": - version: 1.0.0-beta.12 - resolution: "@swagger-api/apidom-reference@npm:1.0.0-beta.12" +"@swagger-api/apidom-reference@npm:>=1.0.0-beta.11 <1.0.0-rc.0": + version: 1.0.0-beta.11 + resolution: "@swagger-api/apidom-reference@npm:1.0.0-beta.11" dependencies: "@babel/runtime-corejs3": "npm:^7.20.7" - "@swagger-api/apidom-core": "npm:^1.0.0-beta.12" + "@swagger-api/apidom-core": "npm:^1.0.0-beta.11" "@swagger-api/apidom-error": "npm:^1.0.0-beta.3 <1.0.0-rc.0" "@swagger-api/apidom-json-pointer": "npm:^1.0.0-beta.3 <1.0.0-rc.0" "@swagger-api/apidom-ns-asyncapi-2": "npm:^1.0.0-beta.3 <1.0.0-rc.0" @@ -8330,16 +8536,16 @@ __metadata: optional: true "@swagger-api/apidom-parser-adapter-yaml-1-2": optional: true - checksum: 10/566ff08a28bdf7854ee34bc33bae973fb57709a7ba0ba5b3ea0fc631ea980d1cedf773d9a8d19e4a74ee3fddcd847dd184b94f5ccee93776ed10bf4c45a04886 + checksum: 10/1ae856fd2d13884f8f063421c010f166da1602075ae3b08c041c98199c724615b389110672e2c1b129b7b551a21b8738d32df87c066cc105669a03b6b21897e6 languageName: node linkType: hard -"@swaggerexpert/cookie@npm:^2.0.2": - version: 2.0.2 - resolution: "@swaggerexpert/cookie@npm:2.0.2" +"@swaggerexpert/cookie@npm:^1.4.1": + version: 1.4.1 + resolution: "@swaggerexpert/cookie@npm:1.4.1" dependencies: apg-lite: "npm:^1.0.3" - checksum: 10/cd13848c944d381007a7d15955d5bdf261e956a8d825a9b0847d5ab37e9898082f11aaa558e324e0400be1cf03c408c14935c02150d9987da9bd3ff5b2982974 + checksum: 10/936590cb70fb7af4ec988e7ee7c5e965b66d09046d3fd2b52c140f9777cda1452f2f24d80eb64ff9678b79f566dd0ef51699050aea9c9a3fd5345dcc73a3fa7a languageName: node linkType: hard @@ -8601,21 +8807,21 @@ __metadata: linkType: hard "@tanstack/react-virtual@npm:^3.5.1, @tanstack/react-virtual@npm:^3.9.0": - version: 3.11.3 - resolution: "@tanstack/react-virtual@npm:3.11.3" + version: 3.10.9 + resolution: "@tanstack/react-virtual@npm:3.10.9" dependencies: - "@tanstack/virtual-core": "npm:3.11.3" + "@tanstack/virtual-core": "npm:3.10.9" peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10/eb39f8a015f4dc98070f0c18bbb1f9c094b7182133554ef3ee31d2678cd3a66edd28ce854d533e830f88f1f0ad1d5b065de184438a08fe774a9acc1dc62da436 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 10/a6c90118d0b084aedf0a2b02bc718df5cc1e594fb60d1dededf8a393d3e519e574e2ba67bb7adcaf8b4d6b206f6a10b37166f006bc7e50ad566475323d545b8c languageName: node linkType: hard -"@tanstack/virtual-core@npm:3.11.3": - version: 3.11.3 - resolution: "@tanstack/virtual-core@npm:3.11.3" - checksum: 10/24a3369dd0290d4f19aa1af7d0a6fb1b843741d722c6a5cf786416657bbf978f4f82a0b257eaee867d0798d8334374f5e940868a7b71dc065939fb7eeee19ad1 +"@tanstack/virtual-core@npm:3.10.9": + version: 3.10.9 + resolution: "@tanstack/virtual-core@npm:3.10.9" + checksum: 10/15140fc41c728ed08486eba4c9caadbdb3c594f02d3b55fddca63813bc32e8cde64faf6ca6385f9815aeeedbc441dd8c9590aca4319c16a91f39b1937ef4eac7 languageName: node linkType: hard @@ -8911,9 +9117,9 @@ __metadata: linkType: hard "@types/babel__preset-env@npm:^7": - version: 7.10.0 - resolution: "@types/babel__preset-env@npm:7.10.0" - checksum: 10/7d4d12758d89708afe327079d7d7580e8af3292295f087b8a9a48e12ac1d90aadc18ac3bc00f9b0cbc8778f3ce9fe778801d4d49b7691a75e3f13a901b69fd07 + version: 7.9.7 + resolution: "@types/babel__preset-env@npm:7.9.7" + checksum: 10/624425a84d9149aec04795fed6b1ac2f27dfd5d7976fde479bb1a4d754de34c92cdc28a1a373a5826382a68127b536420a0e090aa5fae522cb62724b7a571cb5 languageName: node linkType: hard @@ -9748,12 +9954,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:22.12.0, @types/node@npm:>=10.0.0, @types/node@npm:>=13.7.0, @types/node@npm:>=13.7.4": - version: 22.12.0 - resolution: "@types/node@npm:22.12.0" +"@types/node@npm:*, @types/node@npm:>=13.7.0": + version: 22.7.8 + resolution: "@types/node@npm:22.7.8" dependencies: - undici-types: "npm:~6.20.0" - checksum: 10/aac2b6f6a845ec3540c3d979b3150efe3162165bfda953af10b579df2d1cc4f5c48506922bf6bf661a2e5a7ebb571c5729bf1f9f12488a810bb1a5fa9522ef9d + undici-types: "npm:~6.19.2" + checksum: 10/22a7cb6da6a1cf914016bdcfb1d20399ec549a6220a9fd03253dc995848fa328c9dbeaf291b63bd60b0cf44387044adc80fc8d97401c647711b0b839d0ed4fe5 languageName: node linkType: hard @@ -9773,6 +9979,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:22.12.0, @types/node@npm:>=10.0.0, @types/node@npm:>=13.7.4": + version: 22.12.0 + resolution: "@types/node@npm:22.12.0" + dependencies: + undici-types: "npm:~6.20.0" + checksum: 10/aac2b6f6a845ec3540c3d979b3150efe3162165bfda953af10b579df2d1cc4f5c48506922bf6bf661a2e5a7ebb571c5729bf1f9f12488a810bb1a5fa9522ef9d + languageName: node + linkType: hard + "@types/nodemailer@npm:*": version: 6.4.15 resolution: "@types/nodemailer@npm:6.4.15" @@ -9857,11 +10072,11 @@ __metadata: linkType: hard "@types/ramda@npm:~0.30.0": - version: 0.30.2 - resolution: "@types/ramda@npm:0.30.2" + version: 0.30.1 + resolution: "@types/ramda@npm:0.30.1" dependencies: types-ramda: "npm:^0.30.1" - checksum: 10/528e62da967adb38b7b6be3314ee11009f4e7312e4fbb9670f4556bb2f640754f08ae68ede87822ae255aba747e67296f40b37d53bfb427c58ab82f5b7a4989e + checksum: 10/3975599065ebfb4a923566ec17e04e5c59ab3b010dc09fb4462393c0e1b6962ff88c852d43f0f524788e589efea09d0ccdf242306c32872d48caaf7ff0362934 languageName: node linkType: hard @@ -10219,9 +10434,9 @@ __metadata: linkType: hard "@types/unist@npm:^2": - version: 2.0.11 - resolution: "@types/unist@npm:2.0.11" - checksum: 10/6d436e832bc35c6dde9f056ac515ebf2b3384a1d7f63679d12358766f9b313368077402e9c1126a14d827f10370a5485e628bf61aa91117cf4fc882423191a4e + version: 2.0.10 + resolution: "@types/unist@npm:2.0.10" + checksum: 10/e2924e18dedf45f68a5c6ccd6015cd62f1643b1b43baac1854efa21ae9e70505db94290434a23da1137d9e31eb58e54ca175982005698ac37300a1c889f6c4aa languageName: node linkType: hard @@ -10276,9 +10491,9 @@ __metadata: linkType: hard "@types/webpack-env@npm:^1.18.4": - version: 1.18.8 - resolution: "@types/webpack-env@npm:1.18.8" - checksum: 10/f3932f3d6c2530f644cfc898eda1ab8182d6ae57f555c2f0179d813549b639078671b71e4041831fc306c5ebe61f5cdac794fe4ceae281fce8bf67e23661a488 + version: 1.18.5 + resolution: "@types/webpack-env@npm:1.18.5" + checksum: 10/3c8dd0b23d45e2d33abdfbae7f1d8f75ce23d54588b08943e833f4dba81eb683ac68672a75eccbdba8e008bc1647638803c1bcadc8cdfd1dd7142fa2c3f612de languageName: node linkType: hard @@ -10363,6 +10578,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:8.14.0": + version: 8.14.0 + resolution: "@typescript-eslint/scope-manager@npm:8.14.0" + dependencies: + "@typescript-eslint/types": "npm:8.14.0" + "@typescript-eslint/visitor-keys": "npm:8.14.0" + checksum: 10/48ff44a790254b5a98c17bf15176fbdc1408b58eb3ccd8eda9c5707811786de25e1bccc5c490dcc05cbd34b685e162ee4e92b28f57b071c522274fa97f23c98c + languageName: node + linkType: hard + "@typescript-eslint/scope-manager@npm:8.22.0, @typescript-eslint/scope-manager@npm:^8.15.0": version: 8.22.0 resolution: "@typescript-eslint/scope-manager@npm:8.22.0" @@ -10395,6 +10620,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:8.14.0": + version: 8.14.0 + resolution: "@typescript-eslint/types@npm:8.14.0" + checksum: 10/1924aef8efdf5399d6cc9ef3a5307fda39b1a2be129ab8cb24a46dc0a37156230e77f2809ab709d5d0a43891b6ffd67ce45292724e8f8164ac19e1786c5f4644 + languageName: node + linkType: hard + "@typescript-eslint/types@npm:8.22.0, @typescript-eslint/types@npm:^8.9.0": version: 8.22.0 resolution: "@typescript-eslint/types@npm:8.22.0" @@ -10420,6 +10652,25 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:8.14.0": + version: 8.14.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.14.0" + dependencies: + "@typescript-eslint/types": "npm:8.14.0" + "@typescript-eslint/visitor-keys": "npm:8.14.0" + debug: "npm:^4.3.4" + fast-glob: "npm:^3.3.2" + is-glob: "npm:^4.0.3" + minimatch: "npm:^9.0.4" + semver: "npm:^7.6.0" + ts-api-utils: "npm:^1.3.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/b0b9f228071b6338dbf5e2ac52848fa6af630e8d84d4102e1cccaae67114f2bff82bd027af2818e3ad778668e3c3d4a2fb31b7f4c8a9796295e5aa87903fb313 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:8.22.0": version: 8.22.0 resolution: "@typescript-eslint/typescript-estree@npm:8.22.0" @@ -10438,7 +10689,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.22.0, @typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/utils@npm:^8.13.0, @typescript-eslint/utils@npm:^8.15.0, @typescript-eslint/utils@npm:^8.9.0": +"@typescript-eslint/utils@npm:8.22.0, @typescript-eslint/utils@npm:^8.15.0": version: 8.22.0 resolution: "@typescript-eslint/utils@npm:8.22.0" dependencies: @@ -10471,6 +10722,20 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/utils@npm:^8.13.0, @typescript-eslint/utils@npm:^8.9.0": + version: 8.14.0 + resolution: "@typescript-eslint/utils@npm:8.14.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.4.0" + "@typescript-eslint/scope-manager": "npm:8.14.0" + "@typescript-eslint/types": "npm:8.14.0" + "@typescript-eslint/typescript-estree": "npm:8.14.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + checksum: 10/6d3b2583c473b452dd8f978524802aabd275055f98d461cc71ee6a9424291f4481d2a3416a3f77b2458939dd38a39c0fd8e0c9b47915141c8409e63528a1216b + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/visitor-keys@npm:5.62.0" @@ -10481,6 +10746,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:8.14.0": + version: 8.14.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.14.0" + dependencies: + "@typescript-eslint/types": "npm:8.14.0" + eslint-visitor-keys: "npm:^3.4.3" + checksum: 10/735cc9c2ce3235e543d03afe0de740022888e69ed9f5027564e1c959a3a087106bcf21b5b8d3ac872171c0a585744f0442b76fe6ba68341a735a4b4a15f52a46 + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:8.22.0": version: 8.22.0 resolution: "@typescript-eslint/visitor-keys@npm:8.22.0" @@ -11024,7 +11299,7 @@ __metadata: languageName: node linkType: hard -"acorn-walk@npm:8.3.4, acorn-walk@npm:^8.0.0, acorn-walk@npm:^8.0.2, acorn-walk@npm:^8.1.1": +"acorn-walk@npm:8.3.4": version: 8.3.4 resolution: "acorn-walk@npm:8.3.4" dependencies: @@ -11033,6 +11308,13 @@ __metadata: languageName: node linkType: hard +"acorn-walk@npm:^8.0.0, acorn-walk@npm:^8.0.2, acorn-walk@npm:^8.1.1": + version: 8.2.0 + resolution: "acorn-walk@npm:8.2.0" + checksum: 10/e69f7234f2adfeb16db3671429a7c80894105bd7534cb2032acf01bb26e6a847952d11a062d071420b43f8d82e33d2e57f26fe87d9cce0853e8143d8910ff1de + languageName: node + linkType: hard + "acorn@npm:^8.0.4, acorn@npm:^8.1.0, acorn@npm:^8.10.0, acorn@npm:^8.11.0, acorn@npm:^8.14.0, acorn@npm:^8.4.1, acorn@npm:^8.7.1, acorn@npm:^8.8.0, acorn@npm:^8.8.2": version: 8.14.0 resolution: "acorn@npm:8.14.0" @@ -14517,13 +14799,20 @@ __metadata: languageName: node linkType: hard -"dayjs@npm:1.11.13, dayjs@npm:^1.10.4": +"dayjs@npm:1.11.13": version: 1.11.13 resolution: "dayjs@npm:1.11.13" checksum: 10/7374d63ab179b8d909a95e74790def25c8986e329ae989840bacb8b1888be116d20e1c4eee75a69ea0dfbae13172efc50ef85619d304ee7ca3c01d5878b704f5 languageName: node linkType: hard +"dayjs@npm:^1.10.4": + version: 1.11.7 + resolution: "dayjs@npm:1.11.7" + checksum: 10/341d7dc917a4ddc79c836684f7632a769ad8ae3c56506e62b97c27d7bb8a379b52b5589180b80f514eca9beb0b8789303bd32ce3107ba62055078800f9871e38 + languageName: node + linkType: hard + "debounce-promise@npm:3.1.2": version: 3.1.2 resolution: "debounce-promise@npm:3.1.2" @@ -14547,15 +14836,15 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.3.7, debug@npm:^4.4.0": - version: 4.4.0 - resolution: "debug@npm:4.4.0" +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.3.7, debug@npm:~4.3.1, debug@npm:~4.3.2, debug@npm:~4.3.4": + version: 4.3.7 + resolution: "debug@npm:4.3.7" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10/1847944c2e3c2c732514b93d11886575625686056cd765336212dc15de2d2b29612b6cd80e1afba767bb8e1803b778caf9973e98169ef1a24a7a7009e1820367 + checksum: 10/71168908b9a78227ab29d5d25fe03c5867750e31ce24bf2c44a86efc5af041758bb56569b0a3d48a9b5344c00a24a777e6f4100ed6dfd9534a42c1dde285125a languageName: node linkType: hard @@ -14568,15 +14857,15 @@ __metadata: languageName: node linkType: hard -"debug@npm:~4.3.1, debug@npm:~4.3.2, debug@npm:~4.3.4": - version: 4.3.7 - resolution: "debug@npm:4.3.7" +"debug@npm:^4.4.0": + version: 4.4.0 + resolution: "debug@npm:4.4.0" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10/71168908b9a78227ab29d5d25fe03c5867750e31ce24bf2c44a86efc5af041758bb56569b0a3d48a9b5344c00a24a777e6f4100ed6dfd9534a42c1dde285125a + checksum: 10/1847944c2e3c2c732514b93d11886575625686056cd765336212dc15de2d2b29612b6cd80e1afba767bb8e1803b778caf9973e98169ef1a24a7a7009e1820367 languageName: node linkType: hard @@ -14597,13 +14886,20 @@ __metadata: languageName: node linkType: hard -"decimal.js@npm:10, decimal.js@npm:^10.4.1": +"decimal.js@npm:10": version: 10.5.0 resolution: "decimal.js@npm:10.5.0" checksum: 10/714d49cf2f2207b268221795ede330e51452b7c451a0c02a770837d2d4faed47d603a729c2aa1d952eb6c4102d999e91c9b952c1aa016db3c5cba9fc8bf4cda2 languageName: node linkType: hard +"decimal.js@npm:^10.4.1": + version: 10.4.2 + resolution: "decimal.js@npm:10.4.2" + checksum: 10/f762ffe7191102584bcaa00eabf24f5ceaba4dafa9aea600dc1d1d73bf88d15d126d40b649449523f71a2439defedfc7788a2bfffe36b2c721c75e24d750c3ba + languageName: node + linkType: hard + "decode-uri-component@npm:^0.2.0": version: 0.2.2 resolution: "decode-uri-component@npm:0.2.2" @@ -15321,12 +15617,12 @@ __metadata: linkType: hard "enhanced-resolve@npm:^5.17.1": - version: 5.18.0 - resolution: "enhanced-resolve@npm:5.18.0" + version: 5.17.1 + resolution: "enhanced-resolve@npm:5.17.1" dependencies: graceful-fs: "npm:^4.2.4" tapable: "npm:^2.2.0" - checksum: 10/e88463ef97b68d40d0da0cd0c572e23f43dba0be622d6d44eae5cafed05f0c5dac43e463a83a86c4f70186d029357f82b56d9e1e47e8fc91dce3d6602f8bd6ce + checksum: 10/e8e03cb7a4bf3c0250a89afbd29e5ec20e90ba5fcd026066232a0754864d7d0a393fa6fc0e5379314a6529165a1834b36731147080714459d98924520410d8f5 languageName: node linkType: hard @@ -15838,8 +16134,8 @@ __metadata: linkType: hard "eslint-plugin-jest-dom@npm:^5.4.0": - version: 5.5.0 - resolution: "eslint-plugin-jest-dom@npm:5.5.0" + version: 5.4.0 + resolution: "eslint-plugin-jest-dom@npm:5.4.0" dependencies: "@babel/runtime": "npm:^7.16.3" requireindex: "npm:^1.2.0" @@ -15849,7 +16145,7 @@ __metadata: peerDependenciesMeta: "@testing-library/dom": optional: true - checksum: 10/73aaaa6117abbe3b197bc6b1e45839aaa9c2b4c86e7efbc4ff29f03318ec7f019a8e32652c06c61f06fdb22fb296c068a802a268e7b88aa6b71d3477d949b2c6 + checksum: 10/b8b0b0249d066658a75723892bc6f52d6bcf03ff0a69fc5020548c49f740613a8f3acce647f8f04b292606d2bd0ab3372a695aa3d90b4efb19e71870bbddf637 languageName: node linkType: hard @@ -16032,7 +16328,7 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^3.3.0": +"eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.3": version: 3.4.3 resolution: "eslint-visitor-keys@npm:3.4.3" checksum: 10/3f357c554a9ea794b094a09bd4187e5eacd1bc0d0653c3adeb87962c548e6a1ab8f982b86963ae1337f5d976004146536dcee5d0e2806665b193fbfbf1a9231b @@ -16910,13 +17206,13 @@ __metadata: linkType: hard "form-data@npm:^4.0.0": - version: 4.0.1 - resolution: "form-data@npm:4.0.1" + version: 4.0.0 + resolution: "form-data@npm:4.0.0" dependencies: asynckit: "npm:^0.4.0" combined-stream: "npm:^1.0.8" mime-types: "npm:^2.1.12" - checksum: 10/6adb1cff557328bc6eb8a68da205f9ae44ab0e88d4d9237aaf91eed591ffc64f77411efb9016af7d87f23d0a038c45a788aa1c6634e51175c4efa36c2bc53774 + checksum: 10/7264aa760a8cf09482816d8300f1b6e2423de1b02bba612a136857413fdc96d7178298ced106817655facc6b89036c6e12ae31c9eb5bdc16aabf502ae8a5d805 languageName: node linkType: hard @@ -17301,7 +17597,7 @@ __metadata: languageName: node linkType: hard -"get-tsconfig@npm:^4.10.0, get-tsconfig@npm:^4.7.0": +"get-tsconfig@npm:^4.10.0": version: 4.10.0 resolution: "get-tsconfig@npm:4.10.0" dependencies: @@ -17310,6 +17606,15 @@ __metadata: languageName: node linkType: hard +"get-tsconfig@npm:^4.7.0": + version: 4.8.1 + resolution: "get-tsconfig@npm:4.8.1" + dependencies: + resolve-pkg-maps: "npm:^1.0.0" + checksum: 10/3fb5a8ad57b9633eaea085d81661e9e5c9f78b35d8f8689eaf8b8b45a2a3ebf3b3422266d4d7df765e308cc1e6231648d114803ab3d018332e29916f2c1de036 + languageName: node + linkType: hard + "get-user-locale@npm:^2.2.1": version: 2.3.0 resolution: "get-user-locale@npm:2.3.0" @@ -18232,13 +18537,6 @@ __metadata: languageName: node linkType: hard -"highlightjs-vue@npm:^1.0.0": - version: 1.0.0 - resolution: "highlightjs-vue@npm:1.0.0" - checksum: 10/44c9187a19fa3c7eac16bf1d327c03cb07c4b444f744624eaf873eb55e4e449a0bb6573b8ba5982006b65743707d6cad39cfc404f3fe5fb8aeb740a57ff6bc24 - languageName: node - linkType: hard - "history@npm:4.10.1, history@npm:^4.9.0": version: 4.10.1 resolution: "history@npm:4.10.1" @@ -18853,7 +19151,7 @@ __metadata: languageName: node linkType: hard -"immutable@npm:5.0.3, immutable@npm:^5.0.2": +"immutable@npm:5.0.3": version: 5.0.3 resolution: "immutable@npm:5.0.3" checksum: 10/9aca1c783951bb204d7036fbcefac6dd42e7c8ad77ff54b38c5fc0924e6e16ce2d123c95db47c1170ba63dd3f6fc7aa74a29be7adef984031936c4cd1e9e8554 @@ -18874,6 +19172,13 @@ __metadata: languageName: node linkType: hard +"immutable@npm:^5.0.2": + version: 5.0.2 + resolution: "immutable@npm:5.0.2" + checksum: 10/89b1117c610024b7a9214eade8b9f1ed38b00c82235f119515cfa5eaf26270eccbc803296d4c3c12f53e50802f042f84d811998910b866363913720da768472e + languageName: node + linkType: hard + "import-fresh@npm:^3.2.1, import-fresh@npm:^3.3.0": version: 3.3.0 resolution: "import-fresh@npm:3.3.0" @@ -19613,7 +19918,16 @@ __metadata: languageName: node linkType: hard -"is-typed-array@npm:^1.1.13, is-typed-array@npm:^1.1.14, is-typed-array@npm:^1.1.15, is-typed-array@npm:^1.1.3": +"is-typed-array@npm:^1.1.13, is-typed-array@npm:^1.1.3": + version: 1.1.13 + resolution: "is-typed-array@npm:1.1.13" + dependencies: + which-typed-array: "npm:^1.1.14" + checksum: 10/f850ba08286358b9a11aee6d93d371a45e3c59b5953549ee1c1a9a55ba5c1dd1bd9952488ae194ad8f32a9cf5e79c8fa5f0cc4d78c00720aa0bbcf238b38062d + languageName: node + linkType: hard + +"is-typed-array@npm:^1.1.14, is-typed-array@npm:^1.1.15": version: 1.1.15 resolution: "is-typed-array@npm:1.1.15" dependencies: @@ -21533,7 +21847,7 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:11.0.2, lru-cache@npm:^11.0.0": +"lru-cache@npm:11.0.2": version: 11.0.2 resolution: "lru-cache@npm:11.0.2" checksum: 10/25fcb66e9d91eaf17227c6abfe526a7bed5903de74f93bfde380eb8a13410c5e8d3f14fe447293f3f322a7493adf6f9f015c6f1df7a235ff24ec30f366e1c058 @@ -21547,6 +21861,13 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^11.0.0": + version: 11.0.0 + resolution: "lru-cache@npm:11.0.0" + checksum: 10/41f36fbff8b6f199cce3e9cb2b625714f97a535dfd7f16d0988c2627f9ed4c38b6dc8f9ea7fdba19262a7c917ba41c89cad15ca3e3791fc9a2068af472b5bc8d + languageName: node + linkType: hard + "lru-cache@npm:^5.1.1": version: 5.1.1 resolution: "lru-cache@npm:5.1.1" @@ -21922,9 +22243,9 @@ __metadata: linkType: hard "micro-memoize@npm:^4.1.2": - version: 4.1.3 - resolution: "micro-memoize@npm:4.1.3" - checksum: 10/4e9c7767911cc76ae9c9779584ec87844437af9446b295a01774640a732c2c7f91944794027f44625031f7330ab7f9147740d0a9fb612680d1d2d858dad43402 + version: 4.1.2 + resolution: "micro-memoize@npm:4.1.2" + checksum: 10/027e90c3147c97c07224440ea50ede27eb7d888149e4925820397b466d16efc525f5ec3981e4cadec3258a8d36dfd5e7e7c8e660879fbe2e47106785be9bc570 languageName: node linkType: hard @@ -22536,11 +22857,11 @@ __metadata: linkType: hard "nanoid@npm:^5.0.4": - version: 5.0.9 - resolution: "nanoid@npm:5.0.9" + version: 5.0.8 + resolution: "nanoid@npm:5.0.8" bin: nanoid: bin/nanoid.js - checksum: 10/8a3f9104f81095e3e4785f58caae47a05755599824b8611b9730cbf73db706b664f100e6189f8303f08764f144d499613d8e4a39e83125c53f4b4986d6576621 + checksum: 10/df131a515465053ff25c8cf0450ef191e1db83b45fe125af43f50d39feddf1f161d3b2abb34cb993df35a76b427f8d6d982e16e47d67b2fbe843664af025b5e2 languageName: node linkType: hard @@ -22609,11 +22930,11 @@ __metadata: linkType: hard "node-addon-api@npm:^8.2.1, node-addon-api@npm:^8.2.2, node-addon-api@npm:^8.3.0": - version: 8.3.1 - resolution: "node-addon-api@npm:8.3.1" + version: 8.3.0 + resolution: "node-addon-api@npm:8.3.0" dependencies: node-gyp: "npm:latest" - checksum: 10/2a07995b00eb92ae472c3677155281aa95a8f6da5c9d27b07519557a5ac7cdd1eecd9b7bbc1c07fa00dd32f5a2bbf6713fdeae50af40a9f501d609d5585207aa + checksum: 10/b1c2218e794c149011d8f14e5f14b2ffd5f260c08b2982d4163a0f881069dc390458de7703602b9940a1130c1ad87c3f9d35cd7bb116e2f2a134ac0a0c0036ca languageName: node linkType: hard @@ -25432,7 +25753,7 @@ __metadata: languageName: node linkType: hard -"rc-util@npm:^5.0.1, rc-util@npm:^5.0.6, rc-util@npm:^5.15.0, rc-util@npm:^5.16.1, rc-util@npm:^5.21.0, rc-util@npm:^5.24.4, rc-util@npm:^5.26.0, rc-util@npm:^5.36.0, rc-util@npm:^5.37.0, rc-util@npm:^5.38.0, rc-util@npm:^5.38.1, rc-util@npm:^5.43.0, rc-util@npm:^5.44.1, rc-util@npm:^5.44.3": +"rc-util@npm:^5.0.1, rc-util@npm:^5.0.6, rc-util@npm:^5.26.0, rc-util@npm:^5.44.1, rc-util@npm:^5.44.3": version: 5.44.3 resolution: "rc-util@npm:5.44.3" dependencies: @@ -25445,6 +25766,19 @@ __metadata: languageName: node linkType: hard +"rc-util@npm:^5.15.0, rc-util@npm:^5.16.1, rc-util@npm:^5.21.0, rc-util@npm:^5.24.4, rc-util@npm:^5.36.0, rc-util@npm:^5.37.0, rc-util@npm:^5.38.0, rc-util@npm:^5.38.1, rc-util@npm:^5.43.0": + version: 5.43.0 + resolution: "rc-util@npm:5.43.0" + dependencies: + "@babel/runtime": "npm:^7.18.3" + react-is: "npm:^18.2.0" + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 10/6d5be9d79182c6b4c5a033ad6517b2940d3d2ac42a8e77ef5735591d182f8236f61bc7628d61e82a122d2046ec849462f3fe57c08d3a2a20279646785c34ec4a + languageName: node + linkType: hard + "rc-virtual-list@npm:^3.5.1, rc-virtual-list@npm:^3.5.2": version: 3.5.3 resolution: "rc-virtual-list@npm:3.5.3" @@ -25595,6 +25929,18 @@ __metadata: languageName: node linkType: hard +"react-data-grid@npm:7.0.0-beta.46": + version: 7.0.0-beta.46 + resolution: "react-data-grid@npm:7.0.0-beta.46" + dependencies: + clsx: "npm:^2.0.0" + peerDependencies: + react: ^18.0 || ^19.0 + react-dom: ^18.0 || ^19.0 + checksum: 10/d8a679b5e22a07293923894591adcac5ad57b8bd28dcc93f7438563384f6acc2287f14394a3ef360bc1dc901defa00b4030aa9af37c70cf53cf121437e0882bc + languageName: node + linkType: hard + "react-debounce-input@npm:=3.3.0": version: 3.3.0 resolution: "react-debounce-input@npm:3.3.0" @@ -25771,11 +26117,11 @@ __metadata: linkType: hard "react-hook-form@npm:^7.49.2": - version: 7.54.2 - resolution: "react-hook-form@npm:7.54.2" + version: 7.53.2 + resolution: "react-hook-form@npm:7.53.2" peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - checksum: 10/b156d15b6246c76d0275e5722d9056014693e014d0e3dec06e44bf2672ee549aaba4366de5144d18c4cab29e631f3b2b84269d4fd5727ca17aad9b970fde6960 + checksum: 10/172589ac5871bd41717e8c6bc4c825d149db88eb02e356a8fef4115615a56b21bf66ad16954575b1d6302ed265b8780f1975a2730d5aa10a53e412b56d9f72ca languageName: node linkType: hard @@ -26199,18 +26545,17 @@ __metadata: linkType: hard "react-syntax-highlighter@npm:^15.5.0": - version: 15.6.1 - resolution: "react-syntax-highlighter@npm:15.6.1" + version: 15.5.0 + resolution: "react-syntax-highlighter@npm:15.5.0" dependencies: "@babel/runtime": "npm:^7.3.1" highlight.js: "npm:^10.4.1" - highlightjs-vue: "npm:^1.0.0" lowlight: "npm:^1.17.0" prismjs: "npm:^1.27.0" refractor: "npm:^3.6.0" peerDependencies: react: ">= 0.14.0" - checksum: 10/9a89c81f7dcc109b038dc2a73189fa1ea916e6485d8a39856ab3d01d2c753449b5ae1c0df9c9ee0ed5c8c9808a68422b19af9a168ec091a274bddc7ad092eb86 + checksum: 10/14291a92672a79cf167e6cf2dba2547b920c24573729a95ae24035bece43f7e00e3429477be7b87455e8ce018682c8992545c405a915421eb772c5cd07c00576 languageName: node linkType: hard @@ -26390,12 +26735,12 @@ __metadata: linkType: hard "react-zoom-pan-pinch@npm:^3.3.0": - version: 3.7.0 - resolution: "react-zoom-pan-pinch@npm:3.7.0" + version: 3.6.1 + resolution: "react-zoom-pan-pinch@npm:3.6.1" peerDependencies: react: "*" react-dom: "*" - checksum: 10/5ae7f1ffea86fd19ae57f7b4c6818b282e13e00523200d23759db95a1518333044017583c2af3c179b2634c88c76162aa0ce8de79cd26359f07339415842ba34 + checksum: 10/9146aa5c427dd6d0c8a4ebe3db0c720718eef6262d1b4b36033ee433bc76a9c84e30ca91311211ab95446305d3e2813d9abc576d093efbf5562be984431896cb languageName: node linkType: hard @@ -27183,28 +27528,27 @@ __metadata: linkType: hard "rollup@npm:^4.22.4": - version: 4.32.1 - resolution: "rollup@npm:4.32.1" + version: 4.26.0 + resolution: "rollup@npm:4.26.0" dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.32.1" - "@rollup/rollup-android-arm64": "npm:4.32.1" - "@rollup/rollup-darwin-arm64": "npm:4.32.1" - "@rollup/rollup-darwin-x64": "npm:4.32.1" - "@rollup/rollup-freebsd-arm64": "npm:4.32.1" - "@rollup/rollup-freebsd-x64": "npm:4.32.1" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.32.1" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.32.1" - "@rollup/rollup-linux-arm64-gnu": "npm:4.32.1" - "@rollup/rollup-linux-arm64-musl": "npm:4.32.1" - "@rollup/rollup-linux-loongarch64-gnu": "npm:4.32.1" - "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.32.1" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.32.1" - "@rollup/rollup-linux-s390x-gnu": "npm:4.32.1" - "@rollup/rollup-linux-x64-gnu": "npm:4.32.1" - "@rollup/rollup-linux-x64-musl": "npm:4.32.1" - "@rollup/rollup-win32-arm64-msvc": "npm:4.32.1" - "@rollup/rollup-win32-ia32-msvc": "npm:4.32.1" - "@rollup/rollup-win32-x64-msvc": "npm:4.32.1" + "@rollup/rollup-android-arm-eabi": "npm:4.26.0" + "@rollup/rollup-android-arm64": "npm:4.26.0" + "@rollup/rollup-darwin-arm64": "npm:4.26.0" + "@rollup/rollup-darwin-x64": "npm:4.26.0" + "@rollup/rollup-freebsd-arm64": "npm:4.26.0" + "@rollup/rollup-freebsd-x64": "npm:4.26.0" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.26.0" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.26.0" + "@rollup/rollup-linux-arm64-gnu": "npm:4.26.0" + "@rollup/rollup-linux-arm64-musl": "npm:4.26.0" + "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.26.0" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.26.0" + "@rollup/rollup-linux-s390x-gnu": "npm:4.26.0" + "@rollup/rollup-linux-x64-gnu": "npm:4.26.0" + "@rollup/rollup-linux-x64-musl": "npm:4.26.0" + "@rollup/rollup-win32-arm64-msvc": "npm:4.26.0" + "@rollup/rollup-win32-ia32-msvc": "npm:4.26.0" + "@rollup/rollup-win32-x64-msvc": "npm:4.26.0" "@types/estree": "npm:1.0.6" fsevents: "npm:~2.3.2" dependenciesMeta: @@ -27228,8 +27572,6 @@ __metadata: optional: true "@rollup/rollup-linux-arm64-musl": optional: true - "@rollup/rollup-linux-loongarch64-gnu": - optional: true "@rollup/rollup-linux-powerpc64le-gnu": optional: true "@rollup/rollup-linux-riscv64-gnu": @@ -27250,7 +27592,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 10/5a64860df9d0c1b88d142b8502cb2e858e8314025ed35c605c70dc5c7c099fcecc9340cac269412c9a8b53705b911f1454b01164d23400c7d84cafb241be255f + checksum: 10/aec4d876617298400c0c03d35fed67e5193addc82a76f2b2a2f4c2b000cafbca84a33cf2e686dea1d1caa06fe4028dd94b8e6cd1f5bc3bbd19026a188bb2ec55 languageName: node linkType: hard @@ -27467,7 +27809,7 @@ __metadata: languageName: node linkType: hard -"schema-utils@npm:>1.0.0, schema-utils@npm:^4.0.0, schema-utils@npm:^4.2.0, schema-utils@npm:^4.3.0": +"schema-utils@npm:>1.0.0, schema-utils@npm:^4.3.0": version: 4.3.0 resolution: "schema-utils@npm:4.3.0" dependencies: @@ -27490,6 +27832,18 @@ __metadata: languageName: node linkType: hard +"schema-utils@npm:^4.0.0, schema-utils@npm:^4.2.0": + version: 4.2.0 + resolution: "schema-utils@npm:4.2.0" + dependencies: + "@types/json-schema": "npm:^7.0.9" + ajv: "npm:^8.9.0" + ajv-formats: "npm:^2.1.1" + ajv-keywords: "npm:^5.1.0" + checksum: 10/808784735eeb153ab7f3f787f840aa3bc63f423d2a5a7e96c9e70a0e53d0bc62d7b37ea396fc598ce19196e4fb86a72f897154b7c6ce2358bbc426166f205e14 + languageName: node + linkType: hard + "screenfull@npm:^5.1.0": version: 5.1.0 resolution: "screenfull@npm:5.1.0" @@ -29156,17 +29510,17 @@ __metadata: linkType: hard "swagger-client@npm:^3.34.0": - version: 3.34.1 - resolution: "swagger-client@npm:3.34.1" + version: 3.34.0 + resolution: "swagger-client@npm:3.34.0" dependencies: "@babel/runtime-corejs3": "npm:^7.22.15" "@scarf/scarf": "npm:=1.4.0" - "@swagger-api/apidom-core": "npm:>=1.0.0-beta.12 <1.0.0-rc.0" - "@swagger-api/apidom-error": "npm:>=1.0.0-beta.12 <1.0.0-rc.0" - "@swagger-api/apidom-json-pointer": "npm:>=1.0.0-beta.12 <1.0.0-rc.0" - "@swagger-api/apidom-ns-openapi-3-1": "npm:>=1.0.0-beta.12 <1.0.0-rc.0" - "@swagger-api/apidom-reference": "npm:>=1.0.0-beta.12 <1.0.0-rc.0" - "@swaggerexpert/cookie": "npm:^2.0.2" + "@swagger-api/apidom-core": "npm:>=1.0.0-beta.11 <1.0.0-rc.0" + "@swagger-api/apidom-error": "npm:>=1.0.0-beta.11 <1.0.0-rc.0" + "@swagger-api/apidom-json-pointer": "npm:>=1.0.0-beta.11 <1.0.0-rc.0" + "@swagger-api/apidom-ns-openapi-3-1": "npm:>=1.0.0-beta.11 <1.0.0-rc.0" + "@swagger-api/apidom-reference": "npm:>=1.0.0-beta.11 <1.0.0-rc.0" + "@swaggerexpert/cookie": "npm:^1.4.1" deepmerge: "npm:~4.3.0" fast-json-patch: "npm:^3.0.0-1" js-yaml: "npm:^4.1.0" @@ -29177,7 +29531,7 @@ __metadata: openapi-server-url-templating: "npm:^1.2.0" ramda: "npm:^0.30.1" ramda-adjunct: "npm:^5.0.0" - checksum: 10/d01dbb84f46799b09544e2b4c040294715187223b5fe21643ad4690e242ed31cb432f629d429ecd24ff4e89f333c406adf411c5f2880c31baf117d698b657cf0 + checksum: 10/ed89c44ba172abb9cd6a36a189cf2810ef9b1983cb71657772ede94ea804105b07549b90ba36bbead87e4ac71e609b51f0977d9f9c90112952756c7f4f8bde82 languageName: node linkType: hard @@ -29756,6 +30110,15 @@ __metadata: languageName: node linkType: hard +"ts-api-utils@npm:^1.3.0": + version: 1.3.0 + resolution: "ts-api-utils@npm:1.3.0" + peerDependencies: + typescript: ">=4.2.0" + checksum: 10/3ee44faa24410cd649b5c864e068d438aa437ef64e9e4a66a41646a6d3024d3097a695eeb3fb26ee364705d3cb9653a65756d009e6a53badb6066a5f447bf7ed + languageName: node + linkType: hard + "ts-api-utils@npm:^2.0.0": version: 2.0.0 resolution: "ts-api-utils@npm:2.0.0" @@ -30046,9 +30409,9 @@ __metadata: linkType: hard "type-fest@npm:^4.18.2, type-fest@npm:^4.26.1": - version: 4.33.0 - resolution: "type-fest@npm:4.33.0" - checksum: 10/0d179e66fa765bd0a25a785b12dc797f90f2f92bdb8c9c8a789f3fd8e5a4492444e7ef83551b3b8463aeab24fd6195761e26b03174722de636b4b75aa5726fb7 + version: 4.26.1 + resolution: "type-fest@npm:4.26.1" + checksum: 10/b82676194f80af228cb852e320d2ea8381c89d667d2e4d9f2bdfc8f254bccc039c7741a90c53617a4de0c9fdca8265ed18eb0888cd628f391c5c381c33a9f94b languageName: node linkType: hard @@ -30230,6 +30593,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.19.2": + version: 6.19.8 + resolution: "undici-types@npm:6.19.8" + checksum: 10/cf0b48ed4fc99baf56584afa91aaffa5010c268b8842f62e02f752df209e3dea138b372a60a963b3b2576ed932f32329ce7ddb9cb5f27a6c83040d8cd74b7a70 + languageName: node + linkType: hard + "undici-types@npm:~6.20.0": version: 6.20.0 resolution: "undici-types@npm:6.20.0" @@ -30238,9 +30608,9 @@ __metadata: linkType: hard "undici@npm:^6.19.5": - version: 6.21.1 - resolution: "undici@npm:6.21.1" - checksum: 10/eeccc07e9073ae8e755fdc0dc8cdfaa426c01ec6f815425c3ecedba2e5394cea4993962c040dd168951714a82f0d001a13018c3ae3ad4534f0fa97afe425c08d + version: 6.19.8 + resolution: "undici@npm:6.19.8" + checksum: 10/19ae4ba38b029a664d99fd330935ef59136cf99edb04ed821042f27b5a9e84777265fb744c8a7abc83f2059afb019446c69a4ebef07bbc0ed6b2de8d67ef4090 languageName: node linkType: hard @@ -30535,7 +30905,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:11.0.5, uuid@npm:^11.0.0, uuid@npm:^11.0.2": +"uuid@npm:11.0.5": version: 11.0.5 resolution: "uuid@npm:11.0.5" bin: @@ -30553,7 +30923,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^11.0.5": +"uuid@npm:^11.0.0, uuid@npm:^11.0.5": version: 11.1.0 resolution: "uuid@npm:11.1.0" bin: @@ -30562,6 +30932,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^11.0.2": + version: 11.0.3 + resolution: "uuid@npm:11.0.3" + bin: + uuid: dist/esm/bin/uuid + checksum: 10/251385563195709eb0697c74a834764eef28e1656d61174e35edbd129288acb4d95a43f4ce8a77b8c2fc128e2b55924296a0945f964b05b9173469d045625ff2 + languageName: node + linkType: hard + "uuid@npm:^8.3.2": version: 8.3.2 resolution: "uuid@npm:8.3.2" @@ -31328,6 +31707,19 @@ __metadata: languageName: node linkType: hard +"which-typed-array@npm:^1.1.14": + version: 1.1.15 + resolution: "which-typed-array@npm:1.1.15" + dependencies: + available-typed-arrays: "npm:^1.0.7" + call-bind: "npm:^1.0.7" + for-each: "npm:^0.3.3" + gopd: "npm:^1.0.1" + has-tostringtag: "npm:^1.0.2" + checksum: 10/c3b6a99beadc971baa53c3ee5b749f2b9bdfa3b3b9a70650dd8511a48b61d877288b498d424712e9991d16019633086bd8b5923369460d93463c5825fa36c448 + languageName: node + linkType: hard + "which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.18, which-typed-array@npm:^1.1.2": version: 1.1.18 resolution: "which-typed-array@npm:1.1.18" @@ -31657,11 +32049,11 @@ __metadata: linkType: hard "yaml@npm:^2.0.0, yaml@npm:^2.3.4": - version: 2.7.0 - resolution: "yaml@npm:2.7.0" + version: 2.5.1 + resolution: "yaml@npm:2.5.1" bin: yaml: bin.mjs - checksum: 10/c8c314c62fbd49244a6a51b06482f6d495b37ab10fa685fcafa1bbaae7841b7233ee7d12cab087bcca5a0b28adc92868b6e437322276430c28d00f1c1732eeec + checksum: 10/0eecb679db75ea6a989ad97715a9fa5d946972945aa6aa7d2175bca66c213b5564502ccb1cdd04b1bf816ee38b5c43e4e2fda3ff6f5e09da24dabb51ae92c57d languageName: node linkType: hard