feat: show/hide timestamp and body fields in logs explorer (raw, default, column views) (#6903)

* feat: show/hide timestamp and body fields in logs explorer (raw, default, column views)

* fix: add width to log indicator column to ensure that a single column doesn't take half the space

* fix: handle edge cases and fix issues for show/hide body and timestamp in logs explorer
This commit is contained in:
Shaheer Kochai 2025-01-27 20:54:59 +04:30 committed by GitHub
parent 61a6c21edb
commit 98cdbcd711
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 246 additions and 117 deletions

View File

@ -220,12 +220,14 @@ function ListLogView({
<LogStateIndicator type={logType} fontSize={fontSize} /> <LogStateIndicator type={logType} fontSize={fontSize} />
<div> <div>
<LogContainer fontSize={fontSize}> <LogContainer fontSize={fontSize}>
{updatedSelecedFields.some((field) => field.name === 'body') && (
<LogGeneralField <LogGeneralField
fieldKey="Log" fieldKey="Log"
fieldValue={flattenLogData.body} fieldValue={flattenLogData.body}
linesPerRow={linesPerRow} linesPerRow={linesPerRow}
fontSize={fontSize} fontSize={fontSize}
/> />
)}
{flattenLogData.stream && ( {flattenLogData.stream && (
<LogGeneralField <LogGeneralField
fieldKey="Stream" fieldKey="Stream"
@ -233,13 +235,17 @@ function ListLogView({
fontSize={fontSize} fontSize={fontSize}
/> />
)} )}
{updatedSelecedFields.some((field) => field.name === 'timestamp') && (
<LogGeneralField <LogGeneralField
fieldKey="Timestamp" fieldKey="Timestamp"
fieldValue={timestampValue} fieldValue={timestampValue}
fontSize={fontSize} fontSize={fontSize}
/> />
)}
{updatedSelecedFields.map((field) => {updatedSelecedFields
.filter((field) => !['timestamp', 'body'].includes(field.name))
.map((field) =>
isValidLogField(flattenLogData[field.name] as never) ? ( isValidLogField(flattenLogData[field.name] as never) ? (
<LogSelectedField <LogSelectedField
key={field.name} key={field.name}

View File

@ -74,6 +74,7 @@ function RawLogView({
); );
const attributesValues = updatedSelecedFields const attributesValues = updatedSelecedFields
.filter((field) => !['timestamp', 'body'].includes(field.name))
.map((field) => flattenLogData[field.name]) .map((field) => flattenLogData[field.name])
.filter((attribute) => { .filter((attribute) => {
// loadash isEmpty doesnot work with numbers // loadash isEmpty doesnot work with numbers
@ -93,6 +94,13 @@ function RawLogView({
const { formatTimezoneAdjustedTimestamp } = useTimezone(); const { formatTimezoneAdjustedTimestamp } = useTimezone();
const text = useMemo(() => { const text = useMemo(() => {
const parts = [];
// Check if timestamp is selected
const showTimestamp = selectedFields.some(
(field) => field.name === 'timestamp',
);
if (showTimestamp) {
const date = const date =
typeof data.timestamp === 'string' typeof data.timestamp === 'string'
? formatTimezoneAdjustedTimestamp( ? formatTimezoneAdjustedTimestamp(
@ -103,12 +111,23 @@ function RawLogView({
data.timestamp / 1e6, data.timestamp / 1e6,
DATE_TIME_FORMATS.ISO_DATETIME_MS, DATE_TIME_FORMATS.ISO_DATETIME_MS,
); );
parts.push(date);
}
return `${date} | ${attributesText} ${data.body}`; // Check if body is selected
const showBody = selectedFields.some((field) => field.name === 'body');
if (showBody) {
parts.push(`${attributesText} ${data.body}`);
} else {
parts.push(attributesText);
}
return parts.join(' | ');
}, [ }, [
selectedFields,
attributesText,
data.timestamp, data.timestamp,
data.body, data.body,
attributesText,
formatTimezoneAdjustedTimestamp, formatTimezoneAdjustedTimestamp,
]); ]);

View File

@ -49,7 +49,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => { const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
const fieldColumns: ColumnsType<Record<string, unknown>> = fields const fieldColumns: ColumnsType<Record<string, unknown>> = fields
.filter((e) => e.name !== 'id') .filter((e) => !['id', 'body', 'timestamp'].includes(e.name))
.map(({ name }) => ({ .map(({ name }) => ({
title: name, title: name,
dataIndex: name, dataIndex: name,
@ -92,12 +92,16 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
), ),
}), }),
}, },
...(fields.some((field) => field.name === 'timestamp')
? [
{ {
title: 'timestamp', title: 'timestamp',
dataIndex: 'timestamp', dataIndex: 'timestamp',
key: 'timestamp', key: 'timestamp',
// https://github.com/ant-design/ant-design/discussions/36886 // https://github.com/ant-design/ant-design/discussions/36886
render: (field): ColumnTypeRender<Record<string, unknown>> => { render: (
field: string | number,
): ColumnTypeRender<Record<string, unknown>> => {
const date = const date =
typeof field === 'string' typeof field === 'string'
? formatTimezoneAdjustedTimestamp( ? formatTimezoneAdjustedTimestamp(
@ -119,12 +123,18 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
}; };
}, },
}, },
]
: []),
...(appendTo === 'center' ? fieldColumns : []), ...(appendTo === 'center' ? fieldColumns : []),
...(fields.some((field) => field.name === 'body')
? [
{ {
title: 'body', title: 'body',
dataIndex: 'body', dataIndex: 'body',
key: 'body', key: 'body',
render: (field): ColumnTypeRender<Record<string, unknown>> => ({ render: (
field: string | number,
): ColumnTypeRender<Record<string, unknown>> => ({
props: { props: {
style: defaultTableStyle, style: defaultTableStyle,
}, },
@ -132,7 +142,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
<TableBodyContent <TableBodyContent
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: convert.toHtml( __html: convert.toHtml(
dompurify.sanitize(unescapeString(field), { dompurify.sanitize(unescapeString(field as string), {
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS], FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
}), }),
), ),
@ -144,6 +154,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
), ),
}), }),
}, },
]
: []),
...(appendTo === 'end' ? fieldColumns : []), ...(appendTo === 'end' ? fieldColumns : []),
]; ];
}, [ }, [

View File

@ -417,11 +417,13 @@ export default function LogsFormatOptionsMenu({
{key} {key}
</Tooltip> </Tooltip>
</div> </div>
{addColumn?.value?.length > 1 && (
<X <X
className="delete-btn" className="delete-btn"
size={14} size={14}
onClick={(): void => addColumn.onRemove(id as string)} onClick={(): void => addColumn.onRemove(id as string)}
/> />
)}
</div> </div>
))} ))}
{addColumn && addColumn?.value?.length === 0 && ( {addColumn && addColumn?.value?.length === 0 && (

View File

@ -37,7 +37,7 @@ import useErrorNotification from 'hooks/useErrorNotification';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange'; import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery'; import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
import { cloneDeep, isEqual } from 'lodash-es'; import { cloneDeep, isEqual, omit } from 'lodash-es';
import { import {
Check, Check,
ConciergeBell, ConciergeBell,
@ -256,13 +256,17 @@ function ExplorerOptions({
const { handleExplorerTabChange } = useHandleExplorerTabChange(); const { handleExplorerTabChange } = useHandleExplorerTabChange();
const { options, handleOptionsChange } = useOptionsMenu({ const { options, handleOptionsChange } = useOptionsMenu({
storageKey: LOCALSTORAGE.TRACES_LIST_OPTIONS, storageKey:
dataSource: DataSource.TRACES, sourcepage === DataSource.TRACES
? LOCALSTORAGE.TRACES_LIST_OPTIONS
: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: sourcepage,
aggregateOperator: StringOperators.NOOP, aggregateOperator: StringOperators.NOOP,
}); });
type ExtraData = { type ExtraData = {
selectColumns?: BaseAutocompleteData[]; selectColumns?: BaseAutocompleteData[];
version?: number;
}; };
const updateOrRestoreSelectColumns = ( const updateOrRestoreSelectColumns = (
@ -283,14 +287,20 @@ function ExplorerOptions({
console.error('Error parsing extraData:', error); console.error('Error parsing extraData:', error);
} }
let backwardCompatibleOptions = options;
if (!extraData?.version) {
backwardCompatibleOptions = omit(options, 'version');
}
if (extraData.selectColumns?.length) { if (extraData.selectColumns?.length) {
handleOptionsChange({ handleOptionsChange({
...options, ...backwardCompatibleOptions,
selectColumns: extraData.selectColumns, selectColumns: extraData.selectColumns,
}); });
} else if (!isEqual(defaultTraceSelectedColumns, options.selectColumns)) { } else if (!isEqual(defaultTraceSelectedColumns, options.selectColumns)) {
handleOptionsChange({ handleOptionsChange({
...options, ...backwardCompatibleOptions,
selectColumns: defaultTraceSelectedColumns, selectColumns: defaultTraceSelectedColumns,
}); });
} }
@ -423,6 +433,7 @@ function ExplorerOptions({
extraData: JSON.stringify({ extraData: JSON.stringify({
color, color,
selectColumns: options.selectColumns, selectColumns: options.selectColumns,
version: 1,
}), }),
notifications, notifications,
panelType: panelType || PANEL_TYPES.LIST, panelType: panelType || PANEL_TYPES.LIST,

View File

@ -121,7 +121,9 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
const tableHeader = useCallback( const tableHeader = useCallback(
() => ( () => (
<tr> <tr>
{tableColumns.map((column) => { {tableColumns
.filter((column) => column.key)
.map((column) => {
const isDragColumn = column.key !== 'expand'; const isDragColumn = column.key !== 'expand';
return ( return (

View File

@ -29,7 +29,7 @@ export const TableCellStyled = styled.td<TableHeaderCellStyledProps>`
props.$isDarkMode ? 'inherit' : themeColors.whiteCream}; props.$isDarkMode ? 'inherit' : themeColors.whiteCream};
${({ $isLogIndicator }): string => ${({ $isLogIndicator }): string =>
$isLogIndicator ? 'padding: 0 0 0 8px;' : ''} $isLogIndicator ? 'padding: 0 0 0 8px;width: 15px;' : ''}
color: ${(props): string => color: ${(props): string =>
props.$isDarkMode ? themeColors.white : themeColors.bckgGrey}; props.$isDarkMode ? themeColors.white : themeColors.bckgGrey};
`; `;

View File

@ -11,6 +11,27 @@ export const defaultOptionsQuery: OptionsQuery = {
fontSize: FontSize.SMALL, fontSize: FontSize.SMALL,
}; };
export const defaultLogsSelectedColumns = [
{
key: 'timestamp',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
isJSON: false,
id: 'timestamp--string--tag--true',
isIndexed: false,
},
{
key: 'body',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
isJSON: false,
id: 'body--string--tag--true',
isIndexed: false,
},
];
export const defaultTraceSelectedColumns = [ export const defaultTraceSelectedColumns = [
{ {
key: 'serviceName', key: 'serviceName',

View File

@ -17,6 +17,7 @@ export interface OptionsQuery {
maxLines: number; maxLines: number;
format: LogViewMode; format: LogViewMode;
fontSize: FontSize; fontSize: FontSize;
version?: number;
} }
export interface InitialOptions export interface InitialOptions

View File

@ -21,6 +21,7 @@ import {
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
import { import {
defaultLogsSelectedColumns,
defaultOptionsQuery, defaultOptionsQuery,
defaultTraceSelectedColumns, defaultTraceSelectedColumns,
URL_OPTIONS, URL_OPTIONS,
@ -169,6 +170,15 @@ const useOptionsMenu = ({
const searchedAttributeKeys = useMemo(() => { const searchedAttributeKeys = useMemo(() => {
if (searchedAttributesData?.payload?.attributeKeys?.length) { if (searchedAttributesData?.payload?.attributeKeys?.length) {
if (dataSource === DataSource.LOGS) {
// add timestamp and body to the list of attributes
return [
...defaultLogsSelectedColumns,
...searchedAttributesData.payload.attributeKeys.filter(
(attribute) => attribute.key !== 'body',
),
];
}
return searchedAttributesData.payload.attributeKeys; return searchedAttributesData.payload.attributeKeys;
} }
if (dataSource === DataSource.TRACES) { if (dataSource === DataSource.TRACES) {
@ -198,12 +208,17 @@ const useOptionsMenu = ({
); );
const optionsFromAttributeKeys = useMemo(() => { const optionsFromAttributeKeys = useMemo(() => {
const filteredAttributeKeys = searchedAttributeKeys.filter( const filteredAttributeKeys = searchedAttributeKeys.filter((item) => {
(item) => item.key !== 'body', // For other data sources, only filter out 'body' if it exists
); if (dataSource !== DataSource.LOGS) {
return item.key !== 'body';
}
// For LOGS, keep all keys
return true;
});
return getOptionsFromKeys(filteredAttributeKeys, selectedColumnKeys); return getOptionsFromKeys(filteredAttributeKeys, selectedColumnKeys);
}, [searchedAttributeKeys, selectedColumnKeys]); }, [dataSource, searchedAttributeKeys, selectedColumnKeys]);
const handleRedirectWithOptionsData = useCallback( const handleRedirectWithOptionsData = useCallback(
(newQueryData: OptionsQuery) => { (newQueryData: OptionsQuery) => {

View File

@ -9,11 +9,18 @@ import QuickFilters from 'components/QuickFilters/QuickFilters';
import { LOCALSTORAGE } from 'constants/localStorage'; import { LOCALSTORAGE } from 'constants/localStorage';
import LogExplorerQuerySection from 'container/LogExplorerQuerySection'; import LogExplorerQuerySection from 'container/LogExplorerQuerySection';
import LogsExplorerViews from 'container/LogsExplorerViews'; import LogsExplorerViews from 'container/LogsExplorerViews';
import {
defaultLogsSelectedColumns,
defaultOptionsQuery,
URL_OPTIONS,
} from 'container/OptionsMenu/constants';
import { OptionsQuery } from 'container/OptionsMenu/types';
import LeftToolbarActions from 'container/QueryBuilder/components/ToolbarActions/LeftToolbarActions'; import LeftToolbarActions from 'container/QueryBuilder/components/ToolbarActions/LeftToolbarActions';
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions'; import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
import Toolbar from 'container/Toolbar/Toolbar'; import Toolbar from 'container/Toolbar/Toolbar';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { isNull } from 'lodash-es'; import useUrlQueryData from 'hooks/useUrlQueryData';
import { isEqual, isNull } from 'lodash-es';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback'; import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
@ -73,6 +80,39 @@ function LogsExplorer(): JSX.Element {
} }
}, [currentQuery.builder.queryData, currentQuery.builder.queryData.length]); }, [currentQuery.builder.queryData, currentQuery.builder.queryData.length]);
const {
queryData: optionsQueryData,
redirectWithQuery: redirectWithOptionsData,
} = useUrlQueryData<OptionsQuery>(URL_OPTIONS, defaultOptionsQuery);
const migrateOptionsQuery = (query: OptionsQuery): OptionsQuery => {
// If version is missing AND timestamp/body are not in selectColumns, this is an old URL
if (
!query.version &&
!query.selectColumns.some((col) => col.key === 'timestamp') &&
!query.selectColumns.some((col) => col.key === 'body')
) {
return {
...query,
version: 1,
selectColumns: [
// Add default timestamp and body columns
...defaultLogsSelectedColumns,
...query.selectColumns,
],
};
}
return query;
};
useEffect(() => {
const migratedQuery = migrateOptionsQuery(optionsQueryData);
// Only redirect if the query was actually modified
if (!isEqual(migratedQuery, optionsQueryData)) {
redirectWithOptionsData(migratedQuery);
}
}, [optionsQueryData, redirectWithOptionsData]);
const isMultipleQueries = useMemo( const isMultipleQueries = useMemo(
() => () =>
currentQuery.builder.queryData?.length > 1 || currentQuery.builder.queryData?.length > 1 ||