From e1d4e796f22ea427b7affbaa0fa714ce99fbf05a Mon Sep 17 00:00:00 2001 From: Gabriel Bernal Date: Tue, 20 Jan 2026 12:00:41 +0100 Subject: [PATCH 1/2] [FEATURE] allow table plugins to use the selection context Signed-off-by: Gabriel Bernal --- logstable/cue.mod/module.cue | 6 ++ logstable/schemas/logstable.cue | 5 ++ logstable/src/LogsTable.ts | 8 +- logstable/src/LogsTableSelectionsEditor.tsx | 24 ++++++ logstable/src/LogsTableSettingsEditor.tsx | 2 +- .../src/components/VirtualizedLogsList.tsx | 22 +++++- logstable/src/model.ts | 5 +- package-lock.json | 1 + .../PyroscopeProfileQuery.ts | 12 ++- table/schemas/table.cue | 1 + table/src/Table.ts | 8 +- table/src/components/TablePanel.tsx | 47 +++++++++++- .../src/components/TableSelectionsEditor.tsx | 24 ++++++ table/src/models/table-model.ts | 4 +- tracetable/cue.mod/module.cue | 7 ++ tracetable/schemas/trace-table.cue | 9 ++- tracetable/src/DataTable.tsx | 76 ++++++++++++++----- tracetable/src/TraceTable.ts | 3 + tracetable/src/TraceTableSelectionsEditor.tsx | 28 +++++++ tracetable/src/trace-table-model.ts | 3 + 20 files changed, 261 insertions(+), 34 deletions(-) create mode 100644 logstable/src/LogsTableSelectionsEditor.tsx create mode 100644 table/src/components/TableSelectionsEditor.tsx create mode 100644 tracetable/src/TraceTableSelectionsEditor.tsx diff --git a/logstable/cue.mod/module.cue b/logstable/cue.mod/module.cue index f293e8b1c..94a141e4c 100644 --- a/logstable/cue.mod/module.cue +++ b/logstable/cue.mod/module.cue @@ -5,3 +5,9 @@ language: { source: { kind: "git" } +deps: { + "github.com/perses/shared/cue@v0": { + v: "v0.53.0-rc.1" + default: true + } +} diff --git a/logstable/schemas/logstable.cue b/logstable/schemas/logstable.cue index c94601473..b40004235 100644 --- a/logstable/schemas/logstable.cue +++ b/logstable/schemas/logstable.cue @@ -13,9 +13,14 @@ package model +import ( + "github.com/perses/local/cue/common" +) + kind: "LogsTable" spec: close({ allowWrap?: bool enableDetails?: bool showTime?: bool + selection?: common.#selection }) diff --git a/logstable/src/LogsTable.ts b/logstable/src/LogsTable.ts index 050ef140a..942e5c939 100644 --- a/logstable/src/LogsTable.ts +++ b/logstable/src/LogsTable.ts @@ -13,12 +13,16 @@ import { PanelPlugin } from '@perses-dev/plugin-system'; import { LogsTableComponent } from './LogsTableComponent'; -import { LogsTableOptions, LogsTableProps } from './model'; +import { LogsTableSelectionsEditor } from './LogsTableSelectionsEditor'; import { LogsTableSettingsEditor } from './LogsTableSettingsEditor'; +import { LogsTableOptions, LogsTableProps } from './model'; export const LogsTable: PanelPlugin = { PanelComponent: LogsTableComponent, - panelOptionsEditorComponents: [{ label: 'Settings', content: LogsTableSettingsEditor }], + panelOptionsEditorComponents: [ + { label: 'Settings', content: LogsTableSettingsEditor }, + { label: 'Selections', content: LogsTableSelectionsEditor }, + ], supportedQueryTypes: ['LogQuery'], createInitialOptions: () => ({ showTime: true, diff --git a/logstable/src/LogsTableSelectionsEditor.tsx b/logstable/src/LogsTableSelectionsEditor.tsx new file mode 100644 index 000000000..1a411426f --- /dev/null +++ b/logstable/src/LogsTableSelectionsEditor.tsx @@ -0,0 +1,24 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { SelectionOptions, SelectionOptionsEditor } from '@perses-dev/plugin-system'; +import { ReactElement } from 'react'; +import { LogsTableSettingsEditorProps } from './model'; + +export function LogsTableSelectionsEditor({ value, onChange }: LogsTableSettingsEditorProps): ReactElement { + function handleSelectionsChange(selection: SelectionOptions | undefined): void { + onChange({ ...value, selection: selection }); + } + + return ; +} diff --git a/logstable/src/LogsTableSettingsEditor.tsx b/logstable/src/LogsTableSettingsEditor.tsx index cf04b26e0..64b56d289 100644 --- a/logstable/src/LogsTableSettingsEditor.tsx +++ b/logstable/src/LogsTableSettingsEditor.tsx @@ -12,8 +12,8 @@ // limitations under the License. import { - OptionsEditorGrid, OptionsEditorColumn, + OptionsEditorGrid, ThresholdsEditor, ThresholdsEditorProps, } from '@perses-dev/components'; diff --git a/logstable/src/components/VirtualizedLogsList.tsx b/logstable/src/components/VirtualizedLogsList.tsx index 8bc33f492..209eaf9a0 100644 --- a/logstable/src/components/VirtualizedLogsList.tsx +++ b/logstable/src/components/VirtualizedLogsList.tsx @@ -16,6 +16,7 @@ import { Box, useTheme, Popover, Button, ButtonGroup, IconButton } from '@mui/ma import CloseIcon from 'mdi-material-ui/Close'; import { Virtuoso } from 'react-virtuoso'; import { LogEntry } from '@perses-dev/core'; +import { useSelection } from '@perses-dev/components'; import { formatLogEntries, formatLogMessage } from '../utils/copyHelpers'; import { LogsTableOptions } from '../model'; import { LogRow } from './LogRow'; @@ -55,11 +56,30 @@ export const VirtualizedLogsList: React.FC = ({ } }); - // Keep ref in sync with state + const selectionEnabled = spec.selection?.enabled ?? false; + const { setSelection, clearSelection } = useSelection(); + useEffect(() => { selectedRowsRef.current = selectedRows; }, [selectedRows]); + // Sync local selection state with context when selection is enabled + useEffect(() => { + if (!selectionEnabled) return; + + if (selectedRows.size === 0) { + clearSelection(); + } else { + const selectionItems = Array.from(selectedRows) + .map((index) => { + const log = logs[index]; + return log ? { id: index, item: log } : null; + }) + .filter((entry): entry is { id: number; item: LogEntry } => entry !== null); + setSelection(selectionItems); + } + }, [selectedRows, logs, selectionEnabled, setSelection, clearSelection]); + const handleDismissHints = useCallback(() => { setIsHintsDismissed(true); try { diff --git a/logstable/src/model.ts b/logstable/src/model.ts index 321956cbb..ae4e4373a 100644 --- a/logstable/src/model.ts +++ b/logstable/src/model.ts @@ -12,7 +12,7 @@ // limitations under the License. import { LogData, ThresholdOptions } from '@perses-dev/core'; -import { PanelProps, LegendSpecOptions } from '@perses-dev/plugin-system'; +import { PanelProps, LegendSpecOptions, SelectionOptions, OptionsEditorProps } from '@perses-dev/plugin-system'; export type LogsTableProps = PanelProps; @@ -27,4 +27,7 @@ export interface LogsTableOptions { enableDetails?: boolean; showTime?: boolean; showAll?: boolean; + selection?: SelectionOptions; } + +export type LogsTableSettingsEditorProps = OptionsEditorProps; diff --git a/package-lock.json b/package-lock.json index 1f0b7159e..b89f791f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13524,6 +13524,7 @@ "version": "6.30.3", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", "dependencies": { "@remix-run/router": "1.23.2", "react-router": "6.30.3" diff --git a/pyroscope/src/plugins/pyroscope-profile-query/PyroscopeProfileQuery.ts b/pyroscope/src/plugins/pyroscope-profile-query/PyroscopeProfileQuery.ts index 61a3228e8..0d3851904 100644 --- a/pyroscope/src/plugins/pyroscope-profile-query/PyroscopeProfileQuery.ts +++ b/pyroscope/src/plugins/pyroscope-profile-query/PyroscopeProfileQuery.ts @@ -15,7 +15,17 @@ import { LabelFilter } from '../../utils/types'; import { getProfileData } from './get-profile-data'; import { PyroscopeProfileQueryEditor } from './PyroscopeProfileQueryEditor'; -export const PyroscopeProfileQuery = { +export const PyroscopeProfileQuery: { + getProfileData: typeof getProfileData; + OptionsEditorComponent: typeof PyroscopeProfileQueryEditor; + createInitialOptions: () => { + maxNodes: number; + datasource?: string; + service: string; + profileType: string; + filters: LabelFilter[]; + }; +} = { getProfileData, OptionsEditorComponent: PyroscopeProfileQueryEditor, createInitialOptions: (): { diff --git a/table/schemas/table.cue b/table/schemas/table.cue index 4632668e5..483fdd165 100644 --- a/table/schemas/table.cue +++ b/table/schemas/table.cue @@ -27,6 +27,7 @@ spec: close({ defaultColumnHidden?: bool pagination?: bool enableFiltering?: bool + selection?: common.#selection columnSettings?: [...#columnSettings] cellSettings?: [...#cellSettings] transforms?: [...common.#transform] diff --git a/table/src/Table.ts b/table/src/Table.ts index 1a1354465..1b3f6b43d 100644 --- a/table/src/Table.ts +++ b/table/src/Table.ts @@ -12,16 +12,17 @@ // limitations under the License. import { PanelPlugin } from '@perses-dev/plugin-system'; -import { createInitialTableOptions, TableOptions } from './models'; import { getTablePanelQueryOptions, + TableCellsEditor, + TableColumnsEditor, TablePanel, TableProps, - TableColumnsEditor, TableSettingsEditor, - TableCellsEditor, TableTransformsEditor, } from './components'; +import { TableSelectionsEditor } from './components/TableSelectionsEditor'; +import { createInitialTableOptions, TableOptions } from './models'; /** * The core TimeSeriesTable panel plugin for Perses. @@ -35,6 +36,7 @@ export const Table: PanelPlugin = { { label: 'Column Settings', content: TableColumnsEditor }, { label: 'Cell Settings', content: TableCellsEditor }, { label: 'Transforms', content: TableTransformsEditor }, + { label: 'Selections', content: TableSelectionsEditor }, ], createInitialOptions: createInitialTableOptions, }; diff --git a/table/src/components/TablePanel.tsx b/table/src/components/TablePanel.tsx index 6b532cc6d..0709fc6b9 100644 --- a/table/src/components/TablePanel.tsx +++ b/table/src/components/TablePanel.tsx @@ -18,10 +18,10 @@ import { useAllVariableValues, VariableStateMap, } from '@perses-dev/plugin-system'; -import { Table, TableCellConfigs, TableColumnConfig } from '@perses-dev/components'; -import { ReactElement, useEffect, useMemo, useState } from 'react'; +import { Table, TableCellConfigs, TableColumnConfig, useSelection } from '@perses-dev/components'; +import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { formatValue, Labels, QueryDataType, TimeSeries, TimeSeriesData, transformData } from '@perses-dev/core'; -import { PaginationState, SortingState, ColumnFiltersState } from '@tanstack/react-table'; +import { PaginationState, RowSelectionState, SortingState, ColumnFiltersState } from '@tanstack/react-table'; import { useTheme, Theme, Typography, Box } from '@mui/material'; import { ColumnSettings, TableOptions, evaluateConditionalFormatting } from '../models'; import { EmbeddedPanel } from './EmbeddedPanel'; @@ -211,6 +211,41 @@ export function TablePanel({ contentDimensions, spec, queryResults }: TableProps const theme = useTheme(); const allVariables = useAllVariableValues(); + const selectionEnabled = spec.selection?.enabled ?? false; + const { selectionMap, setSelection, clearSelection } = useSelection, string>(); + + const filteredDataRef = useRef>>([]); + + // Convert selectionMap to TanStack's RowSelectionState format + const rowSelection = useMemo((): RowSelectionState => { + const result: RowSelectionState = {}; + selectionMap.forEach((_, id) => { + result[id as string] = true; + }); + return result; + }, [selectionMap]); + + const handleRowSelectionChange = useCallback( + (newRowSelection: RowSelectionState) => { + const newSelection: Array<{ id: string; item: Record }> = []; + for (const [id, isSelected] of Object.entries(newRowSelection)) { + if (isSelected) { + const index = parseInt(id, 10); + if (filteredDataRef.current[index] !== undefined) { + newSelection.push({ id, item: filteredDataRef.current[index] }); + } + } + } + + if (newSelection.length === 0) { + clearSelection(); + } else { + setSelection(newSelection); + } + }, + [setSelection, clearSelection] + ); + // TODO: handle other query types const queryMode = getTablePanelQueryOptions(spec).mode; const rawData: Array> = useMemo(() => { @@ -452,6 +487,9 @@ export function TablePanel({ contentDimensions, spec, queryResults }: TableProps return filtered; }, [data, columnFilters, spec.enableFiltering]); + // Keep ref in sync with filtered data for use in selection handler + filteredDataRef.current = filteredData; + const [pagination, setPagination] = useState( spec.pagination ? { pageIndex: 0, pageSize: 10 } : undefined ); @@ -592,6 +630,9 @@ export function TablePanel({ contentDimensions, spec, queryResults }: TableProps onSortingChange={setSorting} pagination={pagination} onPaginationChange={setPagination} + checkboxSelection={selectionEnabled} + rowSelection={rowSelection} + onRowSelectionChange={handleRowSelectionChange} /> ); diff --git a/table/src/components/TableSelectionsEditor.tsx b/table/src/components/TableSelectionsEditor.tsx new file mode 100644 index 000000000..47c119f6f --- /dev/null +++ b/table/src/components/TableSelectionsEditor.tsx @@ -0,0 +1,24 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { SelectionOptions, SelectionOptionsEditor } from '@perses-dev/plugin-system'; +import { ReactElement } from 'react'; +import { TableSettingsEditorProps } from '../models'; + +export function TableSelectionsEditor({ value, onChange }: TableSettingsEditorProps): ReactElement { + function handleSelectionsChange(selection: SelectionOptions | undefined): void { + onChange({ ...value, selection: selection }); + } + + return ; +} diff --git a/table/src/models/table-model.ts b/table/src/models/table-model.ts index a8f32a210..790771058 100644 --- a/table/src/models/table-model.ts +++ b/table/src/models/table-model.ts @@ -13,7 +13,7 @@ import { Definition, FormatOptions, Transform, UnknownSpec } from '@perses-dev/core'; import { TableDensity, TableCellConfig } from '@perses-dev/components'; -import { OptionsEditorProps } from '@perses-dev/plugin-system'; +import { OptionsEditorProps, SelectionOptions } from '@perses-dev/plugin-system'; import React from 'react'; import { TextField, Stack, MenuItem, Typography } from '@mui/material'; @@ -138,6 +138,8 @@ export interface TableOptions { pagination?: boolean; // Enable filtering for individual columns. enableFiltering?: boolean; + // Enable row selection. + selection?: SelectionOptions; // Customize column display and order them by their index in the array. columnSettings?: ColumnSettings[]; // Customize cell display based on their value. diff --git a/tracetable/cue.mod/module.cue b/tracetable/cue.mod/module.cue index d076cb462..d9ae26860 100644 --- a/tracetable/cue.mod/module.cue +++ b/tracetable/cue.mod/module.cue @@ -5,3 +5,10 @@ language: { source: { kind: "git" } +deps: { + "github.com/perses/shared/cue@v0": { + v: "v0.53.0-rc.1" + default: true + } +} + diff --git a/tracetable/schemas/trace-table.cue b/tracetable/schemas/trace-table.cue index 9d022e3cf..686ab6f41 100644 --- a/tracetable/schemas/trace-table.cue +++ b/tracetable/schemas/trace-table.cue @@ -13,6 +13,10 @@ package model +import ( + "github.com/perses/shared/cue/common" +) + #palette: { mode: "auto" | "categorical" } @@ -27,6 +31,7 @@ package model kind: "TraceTable" spec: close({ - visual?: #visual - links?: #links + visual?: #visual + links?: #links + selection?: common.#selection }) diff --git a/tracetable/src/DataTable.tsx b/tracetable/src/DataTable.tsx index f2418e319..211762234 100644 --- a/tracetable/src/DataTable.tsx +++ b/tracetable/src/DataTable.tsx @@ -22,8 +22,8 @@ import { } from '@perses-dev/core'; import { PanelData, replaceVariablesInString, useAllVariableValues, useRouterContext } from '@perses-dev/plugin-system'; import InformationIcon from 'mdi-material-ui/Information'; -import { useChartsTheme } from '@perses-dev/components'; -import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { useChartsTheme, useSelection } from '@perses-dev/components'; +import { DataGrid, GridColDef, GridRowSelectionModel } from '@mui/x-data-grid'; import { ReactElement, useCallback, useMemo } from 'react'; import { getServiceColor } from './utils/utils'; import { TraceTableOptions } from './trace-table-model'; @@ -55,30 +55,65 @@ export function DataTable(props: DataTableProps): ReactElement { const chartsTheme = useChartsTheme(); const variableValues = useAllVariableValues(); + const selectionEnabled = options.selection?.enabled ?? false; + const { selectionMap, setSelection, clearSelection } = useSelection(); + + // Convert selectionMap to DataGrid's row selection model + const rowSelectionModel = useMemo((): GridRowSelectionModel => { + return Array.from(selectionMap.keys()) as string[]; + }, [selectionMap]); + const paletteMode = options.visual?.palette?.mode; const serviceColorGenerator = useCallback( (serviceName: string) => getServiceColor(muiTheme, chartsTheme, paletteMode, serviceName), [muiTheme, chartsTheme, paletteMode] ); - const rows: Row[] = []; - for (const query of result) { - const pluginSpec = query.definition.spec.plugin.spec as { datasource?: { name?: string } } | undefined; - const datasourceName = pluginSpec?.datasource?.name; + const rows: Row[] = useMemo(() => { + const result_rows: Row[] = []; + for (const query of result) { + const pluginSpec = query.definition.spec.plugin.spec as { datasource?: { name?: string } } | undefined; + const datasourceName = pluginSpec?.datasource?.name; - for (const trace of query.data?.searchResult || []) { - const traceLink = options.links?.trace - ? replaceVariablesInString(options.links.trace, variableValues, { - datasourceName: datasourceName ?? '', - traceId: trace.traceId, - }) - : undefined; - rows.push({ - ...trace, - traceLink, - }); + for (const trace of query.data?.searchResult || []) { + const traceLink = options.links?.trace + ? replaceVariablesInString(options.links.trace, variableValues, { + datasourceName: datasourceName ?? '', + traceId: trace.traceId, + }) + : undefined; + result_rows.push({ + ...trace, + traceLink, + }); + } } - } + return result_rows; + }, [result, options.links?.trace, variableValues]); + + const rowsById = useMemo(() => { + const map = new Map(); + rows.forEach((row) => map.set(row.traceId, row)); + return map; + }, [rows]); + + const handleRowSelectionModelChange = useCallback( + (newSelectionModel: GridRowSelectionModel) => { + const selectedIds = newSelectionModel as string[]; + if (selectedIds.length === 0) { + clearSelection(); + } else { + const newSelection = selectedIds + .map((id) => { + const row = rowsById.get(id); + return row ? { id, item: row } : null; + }) + .filter((entry): entry is { id: string; item: Row } => entry !== null); + setSelection(newSelection); + } + }, + [rowsById, setSelection, clearSelection] + ); const columns = useMemo>>( () => [ @@ -182,7 +217,10 @@ export function DataTable(props: DataTableProps): ReactElement { getRowId={(row) => row.traceId} getRowHeight={() => 'auto'} getEstimatedRowHeight={() => 66} - disableRowSelectionOnClick={true} + checkboxSelection={selectionEnabled} + rowSelectionModel={selectionEnabled ? rowSelectionModel : undefined} + onRowSelectionModelChange={selectionEnabled ? handleRowSelectionModelChange : undefined} + disableRowSelectionOnClick={!selectionEnabled} pageSizeOptions={[10, 20, 50, 100]} initialState={{ pagination: { paginationModel: { pageSize: 20 } }, diff --git a/tracetable/src/TraceTable.ts b/tracetable/src/TraceTable.ts index 0790c018b..9a984f6aa 100644 --- a/tracetable/src/TraceTable.ts +++ b/tracetable/src/TraceTable.ts @@ -13,6 +13,7 @@ import { PanelPlugin } from '@perses-dev/plugin-system'; import { TraceTablePanel, TraceTablePanelProps } from './TraceTablePanel'; +import { TraceTableSelectionsEditor } from './TraceTableSelectionsEditor'; import { TraceTableOptions, createInitialTraceTableOptions } from './trace-table-model'; /** @@ -20,6 +21,8 @@ import { TraceTableOptions, createInitialTraceTableOptions } from './trace-table */ export const TraceTable: PanelPlugin = { PanelComponent: TraceTablePanel, + OptionsEditorComponent: TraceTableSelectionsEditor, + panelOptionsEditorComponents: [{ label: 'Selections', content: TraceTableSelectionsEditor }], supportedQueryTypes: ['TraceQuery'], createInitialOptions: createInitialTraceTableOptions, }; diff --git a/tracetable/src/TraceTableSelectionsEditor.tsx b/tracetable/src/TraceTableSelectionsEditor.tsx new file mode 100644 index 000000000..d7fcd92cd --- /dev/null +++ b/tracetable/src/TraceTableSelectionsEditor.tsx @@ -0,0 +1,28 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { OptionsEditorProps, SelectionOptions, SelectionOptionsEditor } from '@perses-dev/plugin-system'; +import { ReactElement } from 'react'; +import { TraceTableOptions } from './trace-table-model'; + +type TraceTableSelectionsEditorProps = OptionsEditorProps; + +export function TraceTableSelectionsEditor(props: TraceTableSelectionsEditorProps): ReactElement { + const { onChange, value } = props; + + const handleSelectionChange = (selection?: SelectionOptions): void => { + onChange({ ...value, selection }); + }; + + return ; +} diff --git a/tracetable/src/trace-table-model.ts b/tracetable/src/trace-table-model.ts index b102adcb1..2957feb03 100644 --- a/tracetable/src/trace-table-model.ts +++ b/tracetable/src/trace-table-model.ts @@ -11,6 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { SelectionOptions } from '@perses-dev/plugin-system'; + /** * The Options object type supported by the TraceTable panel plugin. */ @@ -18,6 +20,7 @@ export interface TraceTableOptions { visual?: TraceTableVisualOptions; links?: TraceTableCustomLinks; + selection?: SelectionOptions; } export interface TraceTableVisualOptions { From da2e0c797c539bb23b9fe6144d7afe75f83ffe4f Mon Sep 17 00:00:00 2001 From: Gabriel Bernal Date: Fri, 23 Jan 2026 09:15:21 +0100 Subject: [PATCH 2/2] [FEATURE] add item actions in table panel Signed-off-by: Gabriel Bernal --- .gitignore | 4 ++++ table/schemas/table.cue | 11 +++++---- table/src/Table.ts | 2 ++ .../src/components/TableItemActionsEditor.tsx | 24 +++++++++++++++++++ table/src/components/TablePanel.tsx | 24 +++++++++++++++---- table/src/components/index.ts | 2 ++ table/src/models/table-model.ts | 4 +++- 7 files changed, 60 insertions(+), 11 deletions(-) create mode 100644 table/src/components/TableItemActionsEditor.tsx diff --git a/.gitignore b/.gitignore index eac3285a0..565a1a266 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,7 @@ dist/ # external CUE dependencies /*/cue.mod/pkg/ + +# ignore shared package links +.plugins-shared-link-bk.json +.plugins-shared-link-lock-bk.json diff --git a/table/schemas/table.cue b/table/schemas/table.cue index 483fdd165..6eff16e8e 100644 --- a/table/schemas/table.cue +++ b/table/schemas/table.cue @@ -26,11 +26,12 @@ spec: close({ defaultColumnHeight?: "auto" | number defaultColumnHidden?: bool pagination?: bool - enableFiltering?: bool - selection?: common.#selection - columnSettings?: [...#columnSettings] - cellSettings?: [...#cellSettings] - transforms?: [...common.#transform] + enableFiltering?: bool + selection?: common.#selection + actions?: common.#actions + columnSettings?: [...#columnSettings] + cellSettings?: [...#cellSettings] + transforms?: [...common.#transform] }) #columnSettings: { diff --git a/table/src/Table.ts b/table/src/Table.ts index 1b3f6b43d..fcd94af68 100644 --- a/table/src/Table.ts +++ b/table/src/Table.ts @@ -23,6 +23,7 @@ import { } from './components'; import { TableSelectionsEditor } from './components/TableSelectionsEditor'; import { createInitialTableOptions, TableOptions } from './models'; +import { TableItemActionsEditor } from './components/TableItemActionsEditor'; /** * The core TimeSeriesTable panel plugin for Perses. @@ -37,6 +38,7 @@ export const Table: PanelPlugin = { { label: 'Cell Settings', content: TableCellsEditor }, { label: 'Transforms', content: TableTransformsEditor }, { label: 'Selections', content: TableSelectionsEditor }, + { label: 'Item Actions', content: TableItemActionsEditor }, ], createInitialOptions: createInitialTableOptions, }; diff --git a/table/src/components/TableItemActionsEditor.tsx b/table/src/components/TableItemActionsEditor.tsx new file mode 100644 index 000000000..00182f9c7 --- /dev/null +++ b/table/src/components/TableItemActionsEditor.tsx @@ -0,0 +1,24 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { ActionsOptions, ItemActionsEditor } from '@perses-dev/plugin-system'; +import { ReactElement } from 'react'; +import { TableSettingsEditorProps } from '../models'; + +export function TableItemActionsEditor({ value, onChange }: TableSettingsEditorProps): ReactElement { + function handleActionsChange(actions: ActionsOptions | undefined): void { + onChange({ ...value, actions: actions }); + } + + return ; +} diff --git a/table/src/components/TablePanel.tsx b/table/src/components/TablePanel.tsx index 0709fc6b9..b03a48d5f 100644 --- a/table/src/components/TablePanel.tsx +++ b/table/src/components/TablePanel.tsx @@ -11,19 +11,21 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { Box, Theme, Typography, useTheme } from '@mui/material'; +import { Table, TableCellConfigs, TableColumnConfig, useSelection } from '@perses-dev/components'; +import { formatValue, Labels, QueryDataType, TimeSeries, TimeSeriesData, transformData } from '@perses-dev/core'; +import { useSelectionItemActions } from '@perses-dev/dashboards'; import { + ActionsOptions, PanelData, PanelProps, replaceVariablesInString, useAllVariableValues, VariableStateMap, } from '@perses-dev/plugin-system'; -import { Table, TableCellConfigs, TableColumnConfig, useSelection } from '@perses-dev/components'; +import { ColumnFiltersState, PaginationState, RowSelectionState, SortingState } from '@tanstack/react-table'; import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { formatValue, Labels, QueryDataType, TimeSeries, TimeSeriesData, transformData } from '@perses-dev/core'; -import { PaginationState, RowSelectionState, SortingState, ColumnFiltersState } from '@tanstack/react-table'; -import { useTheme, Theme, Typography, Box } from '@mui/material'; -import { ColumnSettings, TableOptions, evaluateConditionalFormatting } from '../models'; +import { ColumnSettings, evaluateConditionalFormatting, TableOptions } from '../models'; import { EmbeddedPanel } from './EmbeddedPanel'; function generateCellContentConfig( @@ -214,6 +216,15 @@ export function TablePanel({ contentDimensions, spec, queryResults }: TableProps const selectionEnabled = spec.selection?.enabled ?? false; const { selectionMap, setSelection, clearSelection } = useSelection, string>(); + const itemActionsConfig = spec.actions ? (spec.actions as ActionsOptions) : undefined; + const itemActionsListConfig = + itemActionsConfig?.enabled && itemActionsConfig.displayWithItem ? itemActionsConfig.actionsList : []; + + const { getItemActionButtons, confirmDialog, actionButtons } = useSelectionItemActions({ + actions: itemActionsListConfig, + variableState: allVariables, + }); + const filteredDataRef = useRef>>([]); // Convert selectionMap to TanStack's RowSelectionState format @@ -524,6 +535,7 @@ export function TablePanel({ contentDimensions, spec, queryResults }: TableProps return ( <> + {confirmDialog} {spec.enableFiltering && (
getItemActionButtons({ id, data: data as Record })} + hasItemActions={actionButtons && actionButtons.length > 0} /> ); diff --git a/table/src/components/index.ts b/table/src/components/index.ts index 51f220c0b..a7eca2314 100644 --- a/table/src/components/index.ts +++ b/table/src/components/index.ts @@ -16,6 +16,8 @@ export * from './ColumnsEditor'; export * from './EmbeddedPanel'; export * from './TableCellsEditor'; export * from './TableColumnsEditor'; +export * from './TableItemActionsEditor'; export * from './TablePanel'; +export * from './TableSelectionsEditor'; export * from './TableSettingsEditor'; export * from './TableTransformsEditor'; diff --git a/table/src/models/table-model.ts b/table/src/models/table-model.ts index 790771058..316914a3b 100644 --- a/table/src/models/table-model.ts +++ b/table/src/models/table-model.ts @@ -13,7 +13,7 @@ import { Definition, FormatOptions, Transform, UnknownSpec } from '@perses-dev/core'; import { TableDensity, TableCellConfig } from '@perses-dev/components'; -import { OptionsEditorProps, SelectionOptions } from '@perses-dev/plugin-system'; +import { ActionsOptions, OptionsEditorProps, SelectionOptions } from '@perses-dev/plugin-system'; import React from 'react'; import { TextField, Stack, MenuItem, Typography } from '@mui/material'; @@ -140,6 +140,8 @@ export interface TableOptions { enableFiltering?: boolean; // Enable row selection. selection?: SelectionOptions; + // Customize actions available for selected rows. + actions?: ActionsOptions; // Customize column display and order them by their index in the array. columnSettings?: ColumnSettings[]; // Customize cell display based on their value.