diff --git a/logstable/cue.mod/module.cue b/logstable/cue.mod/module.cue index f293e8b1..6ffb7cfb 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.2" + default: true + } +} diff --git a/logstable/schemas/logstable.cue b/logstable/schemas/logstable.cue index c9460147..b683fc1e 100644 --- a/logstable/schemas/logstable.cue +++ b/logstable/schemas/logstable.cue @@ -13,9 +13,15 @@ package model +import ( + "github.com/perses/shared/cue/common" +) + kind: "LogsTable" spec: close({ allowWrap?: bool enableDetails?: bool showTime?: bool + selection?: common.#selection + actions?: common.#actions }) diff --git a/logstable/src/LogsTable.ts b/logstable/src/LogsTable.ts index 050ef140..a5e39a76 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 { LogsTableItemSelectionActionsEditor } from './LogsTableItemSelectionActionsEditor'; 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: 'Item Actions', content: LogsTableItemSelectionActionsEditor }, + ], supportedQueryTypes: ['LogQuery'], createInitialOptions: () => ({ showTime: true, diff --git a/logstable/src/LogsTableItemSelectionActionsEditor.tsx b/logstable/src/LogsTableItemSelectionActionsEditor.tsx new file mode 100644 index 00000000..e94560c7 --- /dev/null +++ b/logstable/src/LogsTableItemSelectionActionsEditor.tsx @@ -0,0 +1,35 @@ +// 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 { ActionOptions, ItemSelectionActionsEditor, SelectionOptions } from '@perses-dev/plugin-system'; +import { ReactElement } from 'react'; +import { LogsTableSettingsEditorProps } from './model'; + +export function LogsTableItemSelectionActionsEditor({ value, onChange }: LogsTableSettingsEditorProps): ReactElement { + function handleActionsChange(actions: ActionOptions | undefined): void { + onChange({ ...value, actions: actions }); + } + + function handleSelectionChange(selection: SelectionOptions | undefined): void { + onChange({ ...value, selection: selection }); + } + + return ( + + ); +} diff --git a/logstable/src/LogsTablePanel.test.tsx b/logstable/src/LogsTablePanel.test.tsx index 2d75c9d0..8a02797c 100644 --- a/logstable/src/LogsTablePanel.test.tsx +++ b/logstable/src/LogsTablePanel.test.tsx @@ -11,11 +11,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ChartsProvider, SnackbarProvider, testChartsTheme } from '@perses-dev/components'; -import { render, screen, fireEvent } from '@testing-library/react'; -import { MOCK_LOGS_QUERY_RESULT, MOCK_LOGS_QUERY_DEFINITION, MOCK_LOGS_QUERY_RESULTS } from './test/mock-query-results'; +import { + ChartsProvider, + ItemActionsProvider, + SelectionProvider, + SnackbarProvider, + testChartsTheme, +} from '@perses-dev/components'; +import { VariableProvider } from '@perses-dev/dashboards'; +import { TimeRangeProviderBasic } from '@perses-dev/plugin-system'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { fireEvent, render, screen } from '@testing-library/react'; import { LogsTablePanel } from './LogsTablePanel'; import { LogsQueryData, LogsTableProps } from './model'; +import { MOCK_LOGS_QUERY_DEFINITION, MOCK_LOGS_QUERY_RESULT, MOCK_LOGS_QUERY_RESULTS } from './test/mock-query-results'; // Mock clipboard API Object.assign(navigator, { @@ -38,18 +47,28 @@ describe('LogsTablePanel', () => { // Helper to render the panel with some context set const renderPanel = (data: LogsQueryData | LogsQueryData[]): void => { render( - - - ({ definition: MOCK_LOGS_QUERY_DEFINITION, data: d })) - } - /> - - + + + + + + + + ({ definition: MOCK_LOGS_QUERY_DEFINITION, data: d })) + } + /> + + + + + + + ); }; diff --git a/logstable/src/LogsTableSettingsEditor.tsx b/logstable/src/LogsTableSettingsEditor.tsx index cf04b26e..64b56d28 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/LogRow/LogRow.tsx b/logstable/src/components/LogRow/LogRow.tsx index 0d0ba729..6efef10f 100644 --- a/logstable/src/components/LogRow/LogRow.tsx +++ b/logstable/src/components/LogRow/LogRow.tsx @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, { memo, useCallback, useState, useRef, useEffect } from 'react'; +import React, { memo, useCallback, useState, useRef, useEffect, ReactNode } from 'react'; import { Box, Collapse, @@ -48,6 +48,7 @@ interface LogRowProps { allowWrap?: boolean; isSelected?: boolean; onSelect?: (index: number, event: React.MouseEvent) => void; + itemActionButtons?: ReactNode[]; } const DefaultLogRow: React.FC = ({ @@ -60,6 +61,7 @@ const DefaultLogRow: React.FC = ({ allowWrap = false, isSelected = false, onSelect, + itemActionButtons, }) => { const theme = useTheme(); const severityColor = useSeverityColor(log); @@ -143,6 +145,8 @@ const DefaultLogRow: React.FC = ({ if (!log) return null; + const hasRowActions = itemActionButtons && itemActionButtons.length > 0; + return ( = ({ onMouseDown={handleRowMouseDown} isExpandable={isExpandable} isHighlighted={Boolean(anchorEl)} + hasRowActions={hasRowActions} isSelected={isSelected} > {isExpandable && ( @@ -223,6 +228,20 @@ const DefaultLogRow: React.FC = ({ )} + {hasRowActions && ( + + {itemActionButtons} + + )} prop !== 'isExpandable' && prop !== 'isHighlighted' && prop !== 'isSelected', -})<{ isExpandable: boolean; isHighlighted?: boolean; isSelected?: boolean }>( - ({ theme, isExpandable, isHighlighted, isSelected }) => ({ + shouldForwardProp: (prop) => + prop !== 'isExpandable' && prop !== 'isHighlighted' && prop !== 'isSelected' && prop !== 'hasRowActions', +})<{ isExpandable: boolean; isHighlighted?: boolean; isSelected?: boolean; hasRowActions?: boolean }>( + ({ theme, isExpandable, isHighlighted, isSelected, hasRowActions }) => ({ display: 'grid', - gridTemplateColumns: isExpandable ? '16px minmax(160px, max-content) 1fr' : 'minmax(160px, max-content) 1fr', + gridTemplateColumns: isExpandable + ? `16px minmax(160px, max-content) 1fr ${hasRowActions ? 'min-content' : ''}` + : `minmax(160px, max-content) 1fr ${hasRowActions ? 'min-content' : ''}`, alignItems: 'flex-start', padding: '4px 8px', cursor: 'default', diff --git a/logstable/src/components/VirtualizedLogsList.tsx b/logstable/src/components/VirtualizedLogsList.tsx index 8bc33f49..47da13ba 100644 --- a/logstable/src/components/VirtualizedLogsList.tsx +++ b/logstable/src/components/VirtualizedLogsList.tsx @@ -11,11 +11,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, { useCallback, useState, useEffect, useRef } from 'react'; +import React, { useCallback, useState, useEffect, useRef, ReactNode } from 'react'; import { Box, useTheme, Popover, Button, ButtonGroup, IconButton } from '@mui/material'; 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 { useSelectionItemActions } from '@perses-dev/dashboards'; +import { ActionOptions, useAllVariableValues } from '@perses-dev/plugin-system'; import { formatLogEntries, formatLogMessage } from '../utils/copyHelpers'; import { LogsTableOptions } from '../model'; import { LogRow } from './LogRow'; @@ -55,11 +58,40 @@ export const VirtualizedLogsList: React.FC = ({ } }); - // Keep ref in sync with state + const selectionEnabled = spec.selection?.enabled ?? false; + const { setSelection, clearSelection } = useSelection(); + + const allVariables = useAllVariableValues(); + const itemActionsConfig = spec.actions ? (spec.actions as ActionOptions) : undefined; + const itemActionsListConfig = + itemActionsConfig?.enabled && itemActionsConfig.displayWithItem ? itemActionsConfig.actionsList : []; + + const { getItemActionButtons, confirmDialog } = useSelectionItemActions({ + actions: itemActionsListConfig, + variableState: allVariables, + }); + 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 { @@ -166,6 +198,10 @@ export const VirtualizedLogsList: React.FC = ({ const log = logs[index]; if (!log) return null; + const itemActionButtons: ReactNode[] = itemActionsListConfig?.length + ? getItemActionButtons({ id: index, data: log as unknown as Record }) + : []; + return ( = ({ showTime={spec.showTime} isSelected={selectedRows.has(index)} onSelect={handleRowSelect} + itemActionButtons={itemActionButtons} /> ); }; @@ -257,163 +294,169 @@ export const VirtualizedLogsList: React.FC = ({ }, []); return ( - - {!isHintsDismissed && ( - - - - {isMac ? '⌘' : 'Ctrl'}+Click to select - - - • - - - Shift+Click for range - - - • - - - {isMac ? '⌘' : 'Ctrl'}+C to copy - - - • - - - Esc to clear - - - - - - - )} - - + {confirmDialog} + - + {!isHintsDismissed && ( - ✓ Copied {lastCopiedCount} {lastCopiedCount === 1 ? 'log' : 'logs'} as{' '} - - {lastCopiedFormat === 'full' ? 'Full' : lastCopiedFormat === 'message' ? 'Message' : 'JSON'} + + + {isMac ? '⌘' : 'Ctrl'}+Click to select + + + • + + + Shift+Click for range + + + • + + + {isMac ? '⌘' : 'Ctrl'}+C to copy + + + • + + + Esc to clear + - - - - - - - - - + ✓ Copied {lastCopiedCount} {lastCopiedCount === 1 ? 'log' : 'logs'} as{' '} + + {lastCopiedFormat === 'full' ? 'Full' : lastCopiedFormat === 'message' ? 'Message' : 'JSON'} + + + + + + + + + + + ); }; diff --git a/logstable/src/model.ts b/logstable/src/model.ts index 321956cb..fb07adc6 100644 --- a/logstable/src/model.ts +++ b/logstable/src/model.ts @@ -12,7 +12,13 @@ // limitations under the License. import { LogData, ThresholdOptions } from '@perses-dev/core'; -import { PanelProps, LegendSpecOptions } from '@perses-dev/plugin-system'; +import { + PanelProps, + LegendSpecOptions, + SelectionOptions, + OptionsEditorProps, + ActionOptions, +} from '@perses-dev/plugin-system'; export type LogsTableProps = PanelProps; @@ -27,4 +33,8 @@ export interface LogsTableOptions { enableDetails?: boolean; showTime?: boolean; showAll?: boolean; + selection?: SelectionOptions; + actions?: ActionOptions; } + +export type LogsTableSettingsEditorProps = OptionsEditorProps;