diff --git a/logstable/cue.mod/module.cue b/logstable/cue.mod/module.cue index f293e8b1..94a141e4 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 c9460147..ddaea413 100644 --- a/logstable/schemas/logstable.cue +++ b/logstable/schemas/logstable.cue @@ -13,9 +13,12 @@ package model +import ("github.com/perses/shared/cue/common") + kind: "LogsTable" spec: close({ allowWrap?: bool enableDetails?: bool showTime?: bool + transforms?: [...common.#transform] }) diff --git a/logstable/src/LogsTable.ts b/logstable/src/LogsTable.ts index 050ef140..132d05fd 100644 --- a/logstable/src/LogsTable.ts +++ b/logstable/src/LogsTable.ts @@ -15,10 +15,14 @@ import { PanelPlugin } from '@perses-dev/plugin-system'; import { LogsTableComponent } from './LogsTableComponent'; import { LogsTableOptions, LogsTableProps } from './model'; import { LogsTableSettingsEditor } from './LogsTableSettingsEditor'; +import { LogsTableTransforms } from './components/LogsTableTransforms'; export const LogsTable: PanelPlugin = { PanelComponent: LogsTableComponent, - panelOptionsEditorComponents: [{ label: 'Settings', content: LogsTableSettingsEditor }], + panelOptionsEditorComponents: [ + { label: 'Settings', content: LogsTableSettingsEditor }, + { label: 'Transforms', content: LogsTableTransforms }, + ], supportedQueryTypes: ['LogQuery'], createInitialOptions: () => ({ showTime: true, diff --git a/logstable/src/LogsTableComponent.tsx b/logstable/src/LogsTableComponent.tsx index fdcb99ef..ee3447ce 100644 --- a/logstable/src/LogsTableComponent.tsx +++ b/logstable/src/LogsTableComponent.tsx @@ -17,7 +17,7 @@ import { LogsTableProps } from './model'; import { LogsList } from './components/LogsList'; export function LogsTableComponent(props: LogsTableProps): ReactElement | null { - const { queryResults, spec } = props; + const { queryResults, spec, contentDimensions } = props; // all queries results must be included const logs = queryResults @@ -39,5 +39,5 @@ export function LogsTableComponent(props: LogsTableProps): ReactElement | null { ); } - return ; + return ; } diff --git a/logstable/src/LogsTableSettingsEditor.tsx b/logstable/src/LogsTableSettingsEditor.tsx index cf04b26e..17337be9 100644 --- a/logstable/src/LogsTableSettingsEditor.tsx +++ b/logstable/src/LogsTableSettingsEditor.tsx @@ -17,11 +17,9 @@ import { ThresholdsEditor, ThresholdsEditorProps, } from '@perses-dev/components'; -import { LegendOptionsEditor, LegendOptionsEditorProps, OptionsEditorProps } from '@perses-dev/plugin-system'; +import { LegendOptionsEditor, LegendOptionsEditorProps } from '@perses-dev/plugin-system'; import { ReactElement } from 'react'; -import { LogsTableOptions } from './model'; - -type LogsTableSettingsEditorProps = OptionsEditorProps; +import { LogsTableSettingsEditorProps } from './model'; export function LogsTableSettingsEditor(props: LogsTableSettingsEditorProps): ReactElement { const { onChange, value } = props; diff --git a/logstable/src/components/LogsList.tsx b/logstable/src/components/LogsList.tsx index 9aa9df58..b954bef5 100644 --- a/logstable/src/components/LogsList.tsx +++ b/logstable/src/components/LogsList.tsx @@ -12,23 +12,12 @@ // limitations under the License. import React from 'react'; +// import { useExpandedRows } from './hooks/useExpandedRows'; import { LogEntry } from '@perses-dev/core'; -import { LogsTableOptions } from '../model'; -import { EmptyLogsState } from './EmptyLogsState'; -import { useExpandedRows } from './hooks/useExpandedRows'; +import { LogsTableProps } from '../model'; import { VirtualizedLogsList } from './VirtualizedLogsList'; -interface LogsListProps { - logs: LogEntry[]; - spec: LogsTableOptions; -} - -export const LogsList: React.FC = ({ logs, spec }) => { - const { expandedRows, toggleExpand } = useExpandedRows(); - - if (!logs.length) { - return ; - } - - return ; +type Props = Pick & { logs: LogEntry[] }; +export const LogsList: React.FC = ({ spec, logs, contentDimensions }) => { + return ; }; diff --git a/logstable/src/components/LogsTableTransforms.tsx b/logstable/src/components/LogsTableTransforms.tsx new file mode 100644 index 00000000..befc9a8e --- /dev/null +++ b/logstable/src/components/LogsTableTransforms.tsx @@ -0,0 +1,26 @@ +// 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 { TransformsEditor, TransformsEditorProps } from '@perses-dev/components'; +import { ReactElement } from 'react'; +import { LogsTableSettingsEditorProps } from '../model'; + +export const LogsTableTransforms = (props: LogsTableSettingsEditorProps): ReactElement => { + const { onChange, value } = props; + + const handleTransformsChange: TransformsEditorProps['onChange'] = (transforms) => { + onChange({ ...value, transforms }); + }; + + return ; +}; diff --git a/logstable/src/components/VirtualizedLogsList-obsolete.tsx b/logstable/src/components/VirtualizedLogsList-obsolete.tsx new file mode 100644 index 00000000..441481f9 --- /dev/null +++ b/logstable/src/components/VirtualizedLogsList-obsolete.tsx @@ -0,0 +1,422 @@ +// 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 React, { useCallback, useState, useEffect, useRef } 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 { formatLogEntries, formatLogMessage } from '../utils/copyHelpers'; +import { LogsTableOptionsObsolete } from '../model'; +import { LogRow } from './LogRow'; + +const PERSES_LOGSTABLE_HINTS_DISMISSED = 'PERSES_LOGSTABLE_HINTS_DISMISSED'; +const COPY_TOAST_DURATION_MS = 5000; + +// Detect Mac for keyboard shortcuts display +const isMac = /(Mac|iPhone|iPod|iPad)/i.test(navigator.userAgent); + +interface VirtualizedLogsListProps { + logs: LogEntry[]; + spec: LogsTableOptionsObsolete; + expandedRows: Set; + onToggleExpand: (index: number) => void; +} + +/** + * @deprecated Use the new `calculateTotal` function instead. + */ +export const VirtualizedLogsList: React.FC = ({ + logs, + spec, + expandedRows, + onToggleExpand, +}) => { + const theme = useTheme(); + const [selectedRows, setSelectedRows] = useState>(new Set()); + const [lastSelectedIndex, setLastSelectedIndex] = useState(null); + const selectedRowsRef = useRef>(selectedRows); + const [copyPopoverAnchor, setCopyPopoverAnchor] = useState<{ x: number; y: number } | null>(null); + const [lastCopiedFormat, setLastCopiedFormat] = useState<'full' | 'message' | 'json'>('full'); + const [lastCopiedCount, setLastCopiedCount] = useState(0); + const copyPopoverTimerRef = useRef(null); + const [isHintsDismissed, setIsHintsDismissed] = useState(() => { + try { + return localStorage.getItem(PERSES_LOGSTABLE_HINTS_DISMISSED) === 'true'; + } catch { + return false; + } + }); + + // Keep ref in sync with state + useEffect(() => { + selectedRowsRef.current = selectedRows; + }, [selectedRows]); + + const handleDismissHints = useCallback(() => { + setIsHintsDismissed(true); + try { + localStorage.setItem(PERSES_LOGSTABLE_HINTS_DISMISSED, 'true'); + } catch { + // Ignore localStorage errors + } + }, []); + + const showCopyPopover = useCallback((format: 'full' | 'message' | 'json' = 'full', count: number) => { + // Show toast at bottom-right corner + const x = window.innerWidth - 32; + const y = window.innerHeight - 32; + setCopyPopoverAnchor({ x, y }); + setLastCopiedFormat(format); + setLastCopiedCount(count); + + // Clear existing timer + if (copyPopoverTimerRef.current) { + window.clearTimeout(copyPopoverTimerRef.current); + } + + // Auto-dismiss after configured duration + copyPopoverTimerRef.current = window.setTimeout(() => { + setCopyPopoverAnchor(null); + }, COPY_TOAST_DURATION_MS); + }, []); + + const handleCloseCopyPopover = useCallback(() => { + if (copyPopoverTimerRef.current) { + window.clearTimeout(copyPopoverTimerRef.current); + } + setCopyPopoverAnchor(null); + }, []); + + const handleCopyInFormat = useCallback( + async (format: 'full' | 'message' | 'json') => { + const selectedLogs = Array.from(selectedRowsRef.current) + .sort((a, b) => a - b) + .map((index) => logs[index]) + .filter((log) => log !== undefined); + + let text: string; + if (format === 'message') { + text = selectedLogs.map(formatLogMessage).join('\n'); + } else if (format === 'json') { + text = JSON.stringify(selectedLogs, null, 2); + } else { + text = formatLogEntries(selectedLogs); + } + + await navigator.clipboard.writeText(text); + showCopyPopover(format, selectedLogs.length); + }, + [logs, showCopyPopover] + ); + + const handleRowSelect = useCallback( + (index: number, event: React.MouseEvent) => { + if (event.shiftKey) { + // Prevent text selection during shift-click + event.preventDefault(); + window.getSelection()?.removeAllRanges(); + + if (lastSelectedIndex !== null) { + // Range selection: select all rows between anchor and current + const start = Math.min(lastSelectedIndex, index); + const end = Math.max(lastSelectedIndex, index); + const newSelection = new Set(); + for (let i = start; i <= end; i++) { + newSelection.add(i); + } + setSelectedRows(newSelection); + } else { + // No anchor set: just select this row and set as anchor + const newSelection = new Set([index]); + setSelectedRows(newSelection); + setLastSelectedIndex(index); + } + } else if (event.ctrlKey || event.metaKey) { + // Prevent text selection during cmd/ctrl-click + event.preventDefault(); + window.getSelection()?.removeAllRanges(); + + // Toggle individual row (additive selection) + const newSelection = new Set(selectedRows); + if (newSelection.has(index)) { + newSelection.delete(index); + } else { + newSelection.add(index); + } + setSelectedRows(newSelection); + setLastSelectedIndex(index); + } else { + // Plain click: set as anchor for future shift-clicks + // Don't prevent default to allow text selection + setLastSelectedIndex(index); + } + }, + [selectedRows, lastSelectedIndex] + ); + + const renderLogRow = (index: number) => { + const log = logs[index]; + if (!log) return null; + + return ( + + ); + }; + + const handleCopy = (e: React.ClipboardEvent) => { + const selection = window.getSelection(); + const hasTextSelection = selection && selection.rangeCount > 0 && selection.toString().length > 0; + + // If user has text selected, let browser handle it normally + if (hasTextSelection) { + return; + } + + // If rows are selected, copy those + const currentSelectedRows = selectedRowsRef.current; + if (currentSelectedRows.size > 0) { + e.preventDefault(); + const selectedLogs = Array.from(currentSelectedRows) + .sort((a, b) => a - b) + .map((index) => logs[index]) + .filter((log) => log !== undefined); + const formattedText = formatLogEntries(selectedLogs); + e.clipboardData.setData('text/plain', formattedText); + } + }; + + // Keyboard shortcuts for selection + useEffect(() => { + const handleKeyDown = async (e: KeyboardEvent) => { + // Cmd/Ctrl+A: Select all logs + if ((e.metaKey || e.ctrlKey) && e.key === 'a') { + e.preventDefault(); + const allIndices = new Set(logs.map((_, index) => index)); + setSelectedRows(allIndices); + if (logs.length > 0) { + setLastSelectedIndex(logs.length - 1); + } + } + + // Cmd/Ctrl+C: Copy selected rows + if ((e.metaKey || e.ctrlKey) && e.key === 'c') { + const selection = window.getSelection(); + const hasTextSelection = selection && selection.rangeCount > 0 && selection.toString().length > 0; + + // Only handle if we have selected rows and no text selection + if (selectedRowsRef.current.size > 0 && !hasTextSelection) { + e.preventDefault(); + const selectedLogs = Array.from(selectedRowsRef.current) + .sort((a, b) => a - b) + .map((index) => logs[index]) + .filter((log) => log !== undefined); + const formattedText = formatLogEntries(selectedLogs); + await navigator.clipboard.writeText(formattedText); + showCopyPopover('full', selectedLogs.length); + } + } + + // Escape: Clear selection + if (e.key === 'Escape' && selectedRows.size > 0) { + setSelectedRows(new Set()); + setLastSelectedIndex(null); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [logs, selectedRows, showCopyPopover]); + + // Cleanup timer on unmount + useEffect(() => { + return () => { + if (copyPopoverTimerRef.current) { + window.clearTimeout(copyPopoverTimerRef.current); + } + }; + }, []); + + return ( + + {!isHintsDismissed && ( + + + + {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/components/VirtualizedLogsList.tsx b/logstable/src/components/VirtualizedLogsList.tsx index 8bc33f49..21bd6999 100644 --- a/logstable/src/components/VirtualizedLogsList.tsx +++ b/logstable/src/components/VirtualizedLogsList.tsx @@ -1,419 +1,64 @@ -// 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 React, { useCallback, useState, useEffect, useRef } 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 { formatLogEntries, formatLogMessage } from '../utils/copyHelpers'; +import { LogEntry, transformData } from '@perses-dev/core'; +// import { useTheme } from '@emotion/react'; +import { useMemo, useState } from 'react'; +import { PaginationState } from '@tanstack/react-table'; +import { Table, TableColumnConfig } from '@perses-dev/components'; import { LogsTableOptions } from '../model'; -import { LogRow } from './LogRow'; - -const PERSES_LOGSTABLE_HINTS_DISMISSED = 'PERSES_LOGSTABLE_HINTS_DISMISSED'; -const COPY_TOAST_DURATION_MS = 5000; - -// Detect Mac for keyboard shortcuts display -const isMac = /(Mac|iPhone|iPod|iPad)/i.test(navigator.userAgent); +import { convertLogEntriesToLogTableRows } from './utils'; interface VirtualizedLogsListProps { logs: LogEntry[]; spec: LogsTableOptions; - expandedRows: Set; - onToggleExpand: (index: number) => void; + contentDimensions?: { width: number; height: number }; } export const VirtualizedLogsList: React.FC = ({ - logs, - spec, - expandedRows, - onToggleExpand, + logs: logsEntries, + spec: { transforms }, + contentDimensions, }) => { - const theme = useTheme(); - const [selectedRows, setSelectedRows] = useState>(new Set()); - const [lastSelectedIndex, setLastSelectedIndex] = useState(null); - const selectedRowsRef = useRef>(selectedRows); - const [copyPopoverAnchor, setCopyPopoverAnchor] = useState<{ x: number; y: number } | null>(null); - const [lastCopiedFormat, setLastCopiedFormat] = useState<'full' | 'message' | 'json'>('full'); - const [lastCopiedCount, setLastCopiedCount] = useState(0); - const copyPopoverTimerRef = useRef(null); - const [isHintsDismissed, setIsHintsDismissed] = useState(() => { - try { - return localStorage.getItem(PERSES_LOGSTABLE_HINTS_DISMISSED) === 'true'; - } catch { - return false; - } + /* In case we have more than a query, there might be different results with different set of labels + Clearly some cells may may remain empty because of none-overlapping results + */ + const allColumns = useMemo((): string[] => { + const set = new Set(['timestamp']); + logsEntries.forEach(({ labels }) => { + Object.keys(labels).forEach((k) => { + set.add(k); + }); + }); + set.add('line'); + return Array.from(set); + }, [logsEntries]); + + const columnsConfig = useMemo(() => { + const columnsConfig: Array> = []; + allColumns.forEach((column) => { + columnsConfig.push({ header: column, accessorKey: column, width: 'auto' }); + }); + return columnsConfig; + }, [allColumns]); + + const logsTableRecords: Array> = useMemo(() => { + convertLogEntriesToLogTableRows(logsEntries, allColumns); + return transformData(convertLogEntriesToLogTableRows(logsEntries, allColumns), transforms ?? []); + }, [logsEntries, allColumns, transforms]); + + const [paginationSetting, setPaginationSetting] = useState({ + pageIndex: 0, + pageSize: 10, }); - // Keep ref in sync with state - useEffect(() => { - selectedRowsRef.current = selectedRows; - }, [selectedRows]); - - const handleDismissHints = useCallback(() => { - setIsHintsDismissed(true); - try { - localStorage.setItem(PERSES_LOGSTABLE_HINTS_DISMISSED, 'true'); - } catch { - // Ignore localStorage errors - } - }, []); - - const showCopyPopover = useCallback((format: 'full' | 'message' | 'json' = 'full', count: number) => { - // Show toast at bottom-right corner - const x = window.innerWidth - 32; - const y = window.innerHeight - 32; - setCopyPopoverAnchor({ x, y }); - setLastCopiedFormat(format); - setLastCopiedCount(count); - - // Clear existing timer - if (copyPopoverTimerRef.current) { - window.clearTimeout(copyPopoverTimerRef.current); - } - - // Auto-dismiss after configured duration - copyPopoverTimerRef.current = window.setTimeout(() => { - setCopyPopoverAnchor(null); - }, COPY_TOAST_DURATION_MS); - }, []); - - const handleCloseCopyPopover = useCallback(() => { - if (copyPopoverTimerRef.current) { - window.clearTimeout(copyPopoverTimerRef.current); - } - setCopyPopoverAnchor(null); - }, []); - - const handleCopyInFormat = useCallback( - async (format: 'full' | 'message' | 'json') => { - const selectedLogs = Array.from(selectedRowsRef.current) - .sort((a, b) => a - b) - .map((index) => logs[index]) - .filter((log) => log !== undefined); - - let text: string; - if (format === 'message') { - text = selectedLogs.map(formatLogMessage).join('\n'); - } else if (format === 'json') { - text = JSON.stringify(selectedLogs, null, 2); - } else { - text = formatLogEntries(selectedLogs); - } - - await navigator.clipboard.writeText(text); - showCopyPopover(format, selectedLogs.length); - }, - [logs, showCopyPopover] - ); - - const handleRowSelect = useCallback( - (index: number, event: React.MouseEvent) => { - if (event.shiftKey) { - // Prevent text selection during shift-click - event.preventDefault(); - window.getSelection()?.removeAllRanges(); - - if (lastSelectedIndex !== null) { - // Range selection: select all rows between anchor and current - const start = Math.min(lastSelectedIndex, index); - const end = Math.max(lastSelectedIndex, index); - const newSelection = new Set(); - for (let i = start; i <= end; i++) { - newSelection.add(i); - } - setSelectedRows(newSelection); - } else { - // No anchor set: just select this row and set as anchor - const newSelection = new Set([index]); - setSelectedRows(newSelection); - setLastSelectedIndex(index); - } - } else if (event.ctrlKey || event.metaKey) { - // Prevent text selection during cmd/ctrl-click - event.preventDefault(); - window.getSelection()?.removeAllRanges(); - - // Toggle individual row (additive selection) - const newSelection = new Set(selectedRows); - if (newSelection.has(index)) { - newSelection.delete(index); - } else { - newSelection.add(index); - } - setSelectedRows(newSelection); - setLastSelectedIndex(index); - } else { - // Plain click: set as anchor for future shift-clicks - // Don't prevent default to allow text selection - setLastSelectedIndex(index); - } - }, - [selectedRows, lastSelectedIndex] - ); - - const renderLogRow = (index: number) => { - const log = logs[index]; - if (!log) return null; - - return ( - - ); - }; - - const handleCopy = (e: React.ClipboardEvent) => { - const selection = window.getSelection(); - const hasTextSelection = selection && selection.rangeCount > 0 && selection.toString().length > 0; - - // If user has text selected, let browser handle it normally - if (hasTextSelection) { - return; - } - - // If rows are selected, copy those - const currentSelectedRows = selectedRowsRef.current; - if (currentSelectedRows.size > 0) { - e.preventDefault(); - const selectedLogs = Array.from(currentSelectedRows) - .sort((a, b) => a - b) - .map((index) => logs[index]) - .filter((log) => log !== undefined); - const formattedText = formatLogEntries(selectedLogs); - e.clipboardData.setData('text/plain', formattedText); - } - }; - - // Keyboard shortcuts for selection - useEffect(() => { - const handleKeyDown = async (e: KeyboardEvent) => { - // Cmd/Ctrl+A: Select all logs - if ((e.metaKey || e.ctrlKey) && e.key === 'a') { - e.preventDefault(); - const allIndices = new Set(logs.map((_, index) => index)); - setSelectedRows(allIndices); - if (logs.length > 0) { - setLastSelectedIndex(logs.length - 1); - } - } - - // Cmd/Ctrl+C: Copy selected rows - if ((e.metaKey || e.ctrlKey) && e.key === 'c') { - const selection = window.getSelection(); - const hasTextSelection = selection && selection.rangeCount > 0 && selection.toString().length > 0; - - // Only handle if we have selected rows and no text selection - if (selectedRowsRef.current.size > 0 && !hasTextSelection) { - e.preventDefault(); - const selectedLogs = Array.from(selectedRowsRef.current) - .sort((a, b) => a - b) - .map((index) => logs[index]) - .filter((log) => log !== undefined); - const formattedText = formatLogEntries(selectedLogs); - await navigator.clipboard.writeText(formattedText); - showCopyPopover('full', selectedLogs.length); - } - } - - // Escape: Clear selection - if (e.key === 'Escape' && selectedRows.size > 0) { - setSelectedRows(new Set()); - setLastSelectedIndex(null); - } - }; - - window.addEventListener('keydown', handleKeyDown); - return () => { - window.removeEventListener('keydown', handleKeyDown); - }; - }, [logs, selectedRows, showCopyPopover]); - - // Cleanup timer on unmount - useEffect(() => { - return () => { - if (copyPopoverTimerRef.current) { - window.clearTimeout(copyPopoverTimerRef.current); - } - }; - }, []); + console.log(contentDimensions?.width, contentDimensions?.height); return ( - - {!isHintsDismissed && ( - - - - {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/components/utils.ts b/logstable/src/components/utils.ts index 99e57c61..b9dfb98c 100644 --- a/logstable/src/components/utils.ts +++ b/logstable/src/components/utils.ts @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { LogEntry } from '@perses-dev/core'; +import { Labels, LogEntry } from '@perses-dev/core'; export type Severity = 'critical' | 'error' | 'warning' | 'info' | 'debug' | 'trace' | 'unknown' | 'other'; @@ -39,3 +39,21 @@ export const getSeverity = (log: LogEntry): Severity => { return 'unknown'; }; + +export const convertLogEntriesToLogTableRows = ( + logsEntries: LogEntry[], + allColumns: string[] +): Array> => { + const records: Array> = []; + logsEntries.forEach((e) => { + const { timestamp, line, labels } = e; + const _labels: Labels = { ...labels, timestamp: String(timestamp), line }; + const logTableRow: Record = {}; + allColumns.forEach((col) => { + const cellValue = _labels[col]; + Object.assign(logTableRow, { [col]: cellValue ?? '' }); + }); + records.push(logTableRow); + }); + return records; +}; diff --git a/logstable/src/model.ts b/logstable/src/model.ts index 321956cb..5365eab5 100644 --- a/logstable/src/model.ts +++ b/logstable/src/model.ts @@ -11,8 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { LogData, ThresholdOptions } from '@perses-dev/core'; -import { PanelProps, LegendSpecOptions } from '@perses-dev/plugin-system'; +import { LogData, ThresholdOptions, Transform } from '@perses-dev/core'; +import { PanelProps, LegendSpecOptions, OptionsEditorProps } from '@perses-dev/plugin-system'; export type LogsTableProps = PanelProps; @@ -21,10 +21,23 @@ export interface LogsQueryData { } export interface LogsTableOptions { + legend?: LegendSpecOptions; + thresholds?: ThresholdOptions; + allowWrap?: boolean; + enableDetails?: boolean; + pagination?: boolean; + showLogLine?: boolean; + transforms?: Transform[]; +} + +export interface LogsTableOptionsObsolete { legend?: LegendSpecOptions; thresholds?: ThresholdOptions; allowWrap?: boolean; enableDetails?: boolean; showTime?: boolean; showAll?: boolean; + transforms?: Transform[]; } + +export type LogsTableSettingsEditorProps = OptionsEditorProps;