Compare commits

...

1 Commits

Author SHA1 Message Date
Ishan Uniyal
d09ed1dca4 feat: logs keyboard handle feature 2026-02-09 10:18:42 +05:30
18 changed files with 644 additions and 188 deletions

View File

@@ -1,8 +1,10 @@
/* eslint-disable no-nested-ternary */
import { useCallback, useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { Virtuoso } from 'react-virtuoso';
import { Card } from 'antd';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
@@ -10,6 +12,7 @@ import LogsError from 'container/LogsError/LogsError';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { FontSize } from 'container/OptionsMenu/types';
import { useHandleLogsPagination } from 'hooks/infraMonitoring/useHandleLogsPagination';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { ILog } from 'types/api/logs/log';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
@@ -28,6 +31,16 @@ interface Props {
}
function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
const {
activeLog,
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
} = useActiveLog();
const [selectedTab, setSelectedTab] = useState<
typeof VIEW_TYPES[keyof typeof VIEW_TYPES] | undefined
>();
const basePayload = getHostLogsQueryPayload(
timeRange.startTime,
timeRange.endTime,
@@ -72,6 +85,25 @@ function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
setIsPaginating(false);
}, [data, setIsPaginating]);
const handleSetActiveLogWithTab = useCallback(
(log: ILog): void => {
// If clicking the same log that's already active, close it
if (activeLog?.id === log.id) {
onClearActiveLog();
setSelectedTab(undefined);
return;
}
onSetActiveLog(log);
setSelectedTab(VIEW_TYPES.OVERVIEW);
},
[onSetActiveLog, activeLog, onClearActiveLog],
);
const handleCloseLogDetail = useCallback((): void => {
onClearActiveLog();
setSelectedTab(undefined);
}, [onClearActiveLog]);
const getItemContent = useCallback(
(_: number, logToRender: ILog): JSX.Element => (
<RawLogView
@@ -92,9 +124,13 @@ function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
name: 'timestamp',
},
]}
onSetActiveLog={handleSetActiveLogWithTab}
onClearActiveLog={handleCloseLogDetail}
isActiveLog={activeLog?.id === logToRender.id}
managedExternally
/>
),
[],
[activeLog, handleSetActiveLogWithTab, handleCloseLogDetail],
);
const renderFooter = useCallback(
@@ -141,6 +177,17 @@ function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
{!isLoading && !isError && logs.length > 0 && (
<div className="host-metrics-logs-list-container">{renderContent}</div>
)}
{selectedTab && activeLog && (
<LogDetail
log={activeLog}
onClose={handleCloseLogDetail}
logs={logs}
onNavigateLog={handleSetActiveLogWithTab}
selectedTab={selectedTab}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
/>
)}
</div>
);
}

View File

@@ -13,6 +13,8 @@ export type LogDetailProps = {
handleChangeSelectedView?: ChangeViewFunctionType;
isListViewPanel?: boolean;
listViewPanelSelectedFields?: IField[] | null;
logs?: ILog[];
onNavigateLog?: (log: ILog) => void;
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
Pick<DrawerProps, 'onClose'>;

View File

@@ -66,6 +66,10 @@
margin-bottom: 16px;
}
.log-detail-drawer__content {
height: 100%;
}
.log-detail-drawer__log {
width: 100%;
display: flex;
@@ -183,6 +187,34 @@
.ant-drawer-close {
padding: 0px;
}
.log-detail-drawer__footer-hint {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 8px 16px;
text-align: left;
color: var(--text-vanilla-200);
background: var(--bg-ink-400);
z-index: 10;
.log-detail-drawer__footer-hint-content {
display: flex;
align-items: center;
gap: 4px;
}
.log-detail-drawer__footer-hint-icon {
display: inline;
vertical-align: middle;
color: var(--text-vanilla-200);
}
.log-detail-drawer__footer-hint-text {
font-size: 13px;
margin: 0;
}
}
}
.lightMode {
@@ -252,4 +284,33 @@
color: var(--text-ink-300);
}
}
.log-detail-drawer__footer-hint {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 8px 16px;
text-align: left;
color: var(--text-vanilla-700);
background: var(--bg-vanilla-100);
z-index: 10;
.log-detail-drawer__footer-hint-content {
display: flex;
align-items: center;
gap: 4px;
}
.log-detail-drawer__footer-hint-icon {
display: inline;
vertical-align: middle;
color: var(--text-vanilla-700);
}
.log-detail-drawer__footer-hint-text {
font-size: 13px;
margin: 0;
}
}
}

View File

@@ -1,5 +1,5 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useCopyToClipboard, useLocation } from 'react-use';
import { Color, Spacing } from '@signozhq/design-tokens';
@@ -32,6 +32,8 @@ import { useSafeNavigate } from 'hooks/useSafeNavigate';
import createQueryParams from 'lib/createQueryParams';
import { cloneDeep } from 'lodash-es';
import {
ArrowDown,
ArrowUp,
BarChart2,
Braces,
Compass,
@@ -60,6 +62,8 @@ function LogDetailInner({
isListViewPanel = false,
listViewPanelSelectedFields,
handleChangeSelectedView,
logs,
onNavigateLog,
}: LogDetailInnerProps): JSX.Element {
const initialContextQuery = useInitialQuery(log);
const [contextQuery, setContextQuery] = useState<Query | undefined>(
@@ -74,6 +78,72 @@ function LogDetailInner({
const [isEdit, setIsEdit] = useState<boolean>(false);
const { stagedQuery, updateAllQueriesOperators } = useQueryBuilder();
// Handle clicks outside to close drawer, except on explicitly ignored regions
useEffect(() => {
const handleClickOutside = (e: MouseEvent): void => {
const target = e.target as HTMLElement;
// Don't close if clicking on explicitly ignored regions
if (target.closest('[data-log-detail-ignore="true"]')) {
return;
}
// Close the drawer for any other outside click
onClose?.(e as any);
};
// Add listener after a small delay to avoid immediate closure
const timeoutId = setTimeout(() => {
document.addEventListener('mousedown', handleClickOutside);
}, 100);
return (): void => {
clearTimeout(timeoutId);
document.removeEventListener('mousedown', handleClickOutside);
};
}, [onClose]);
// Keyboard navigation - handle up/down arrow keys
// Only listen when in OVERVIEW tab
useEffect(() => {
if (
!logs ||
!onNavigateLog ||
logs.length === 0 ||
selectedView !== VIEW_TYPES.OVERVIEW
) {
return;
}
const handleKeyDown = (e: KeyboardEvent): void => {
const currentIndex = logs.findIndex((l) => l.id === log.id);
if (currentIndex === -1) {
return;
}
if (e.key === 'ArrowUp') {
e.preventDefault();
e.stopPropagation();
// Navigate to previous log
if (currentIndex > 0) {
onNavigateLog(logs[currentIndex - 1]);
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
e.stopPropagation();
// Navigate to next log
if (currentIndex < logs.length - 1) {
onNavigateLog(logs[currentIndex + 1]);
}
}
};
document.addEventListener('keydown', handleKeyDown);
return (): void => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [log.id, logs, onNavigateLog, selectedView]);
const listQuery = useMemo(() => {
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) {
return null;
@@ -231,9 +301,10 @@ function LogDetailInner({
return (
<Drawer
width="60%"
maskStyle={{ background: 'none' }}
mask={false}
maskClosable={false}
title={
<div className="log-detail-drawer__title">
<div className="log-detail-drawer__title" data-log-detail-ignore="true">
<div className="log-detail-drawer__title-left">
<Divider type="vertical" className={cx('log-type-indicator', LogType)} />
<Typography.Text className="title">Log details</Typography.Text>
@@ -252,7 +323,6 @@ function LogDetailInner({
</div>
}
placement="right"
// closable
onClose={drawerCloseHandler}
open={log !== null}
style={{
@@ -263,138 +333,164 @@ function LogDetailInner({
destroyOnClose
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
>
<div className="log-detail-drawer__log">
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
<Tooltip title={removeEscapeCharacters(log?.body)} placement="left">
<div className="log-body" dangerouslySetInnerHTML={htmlBody} />
</Tooltip>
<div className="log-detail-drawer__content" data-log-detail-ignore="true">
<div className="log-detail-drawer__log">
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
<Tooltip title={removeEscapeCharacters(log?.body)} placement="left">
<div className="log-body" dangerouslySetInnerHTML={htmlBody} />
</Tooltip>
<div className="log-overflow-shadow">&nbsp;</div>
</div>
<div className="log-overflow-shadow">&nbsp;</div>
</div>
<div className="tabs-and-search">
<Radio.Group
className="views-tabs"
onChange={handleModeChange}
value={selectedView}
>
<Radio.Button
className={
// eslint-disable-next-line sonarjs/no-duplicate-string
selectedView === VIEW_TYPES.OVERVIEW ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.OVERVIEW}
<div className="tabs-and-search">
<Radio.Group
className="views-tabs"
onChange={handleModeChange}
value={selectedView}
>
<div className="view-title">
<Table size={14} />
Overview
</div>
</Radio.Button>
<Radio.Button
className={selectedView === VIEW_TYPES.JSON ? 'selected_view tab' : 'tab'}
value={VIEW_TYPES.JSON}
>
<div className="view-title">
<Braces size={14} />
JSON
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.CONTEXT ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.CONTEXT}
>
<div className="view-title">
<TextSelect size={14} />
Context
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.INFRAMETRICS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.INFRAMETRICS}
>
<div className="view-title">
<BarChart2 size={14} />
Metrics
</div>
</Radio.Button>
</Radio.Group>
<Radio.Button
className={
// eslint-disable-next-line sonarjs/no-duplicate-string
selectedView === VIEW_TYPES.OVERVIEW ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.OVERVIEW}
>
<div className="view-title">
<Table size={14} />
Overview
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.JSON ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.JSON}
>
<div className="view-title">
<Braces size={14} />
JSON
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.CONTEXT ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.CONTEXT}
>
<div className="view-title">
<TextSelect size={14} />
Context
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.INFRAMETRICS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.INFRAMETRICS}
>
<div className="view-title">
<BarChart2 size={14} />
Metrics
</div>
</Radio.Button>
</Radio.Group>
<div className="log-detail-drawer__actions">
{selectedView === VIEW_TYPES.CONTEXT && (
<Tooltip
title="Show Filters"
placement="topLeft"
aria-label="Show Filters"
>
<Button
className="action-btn"
icon={<Filter size={16} />}
onClick={handleFilterVisible}
/>
</Tooltip>
)}
<div className="log-detail-drawer__actions">
{selectedView === VIEW_TYPES.CONTEXT && (
<Tooltip
title="Show Filters"
title={selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'}
placement="topLeft"
aria-label="Show Filters"
aria-label={
selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'
}
>
<Button
className="action-btn"
icon={<Filter size={16} />}
onClick={handleFilterVisible}
icon={<Copy size={16} />}
onClick={selectedView === VIEW_TYPES.JSON ? handleJSONCopy : onLogCopy}
/>
</Tooltip>
)}
<Tooltip
title={selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'}
placement="topLeft"
aria-label={
selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'
}
>
<Button
className="action-btn"
icon={<Copy size={16} />}
onClick={selectedView === VIEW_TYPES.JSON ? handleJSONCopy : onLogCopy}
</div>
</div>
{isFilterVisible && contextQuery?.builder.queryData[0] && (
<div className="log-detail-drawer-query-container">
<QuerySearch
onChange={(value): void => handleQueryExpressionChange(value, 0)}
dataSource={DataSource.LOGS}
queryData={contextQuery?.builder.queryData[0]}
onRun={handleRunQuery}
/>
</Tooltip>
</div>
</div>
{isFilterVisible && contextQuery?.builder.queryData[0] && (
<div className="log-detail-drawer-query-container">
<QuerySearch
onChange={(value): void => handleQueryExpressionChange(value, 0)}
dataSource={DataSource.LOGS}
queryData={contextQuery?.builder.queryData[0]}
onRun={handleRunQuery}
</div>
)}
{selectedView === VIEW_TYPES.OVERVIEW && (
<Overview
logData={log}
onAddToQuery={onAddToQuery}
onClickActionItem={onClickActionItem}
isListViewPanel={isListViewPanel}
selectedOptions={options}
listViewPanelSelectedFields={listViewPanelSelectedFields}
handleChangeSelectedView={handleChangeSelectedView}
/>
</div>
)}
)}
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}
{selectedView === VIEW_TYPES.OVERVIEW && (
<Overview
logData={log}
onAddToQuery={onAddToQuery}
onClickActionItem={onClickActionItem}
isListViewPanel={isListViewPanel}
selectedOptions={options}
listViewPanelSelectedFields={listViewPanelSelectedFields}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}
{selectedView === VIEW_TYPES.CONTEXT && (
<ContextView
log={log}
filters={filters}
contextQuery={contextQuery}
isEdit={isEdit}
/>
)}
{selectedView === VIEW_TYPES.INFRAMETRICS && (
<InfraMetrics
clusterName={log.resources_string?.[RESOURCE_KEYS.CLUSTER_NAME] || ''}
podName={log.resources_string?.[RESOURCE_KEYS.POD_NAME] || ''}
nodeName={log.resources_string?.[RESOURCE_KEYS.NODE_NAME] || ''}
hostName={log.resources_string?.[RESOURCE_KEYS.HOST_NAME] || ''}
timestamp={log.timestamp.toString()}
dataSource={DataSource.LOGS}
/>
)}
{selectedView === VIEW_TYPES.CONTEXT && (
<ContextView
log={log}
filters={filters}
contextQuery={contextQuery}
isEdit={isEdit}
/>
)}
{selectedView === VIEW_TYPES.INFRAMETRICS && (
<InfraMetrics
clusterName={log.resources_string?.[RESOURCE_KEYS.CLUSTER_NAME] || ''}
podName={log.resources_string?.[RESOURCE_KEYS.POD_NAME] || ''}
nodeName={log.resources_string?.[RESOURCE_KEYS.NODE_NAME] || ''}
hostName={log.resources_string?.[RESOURCE_KEYS.HOST_NAME] || ''}
timestamp={log.timestamp.toString()}
dataSource={DataSource.LOGS}
/>
)}
{selectedView === VIEW_TYPES.OVERVIEW && (
<div className="log-detail-drawer__footer-hint">
<div className="log-detail-drawer__footer-hint-content">
<Typography.Text
type="secondary"
className="log-detail-drawer__footer-hint-text"
>
Use
</Typography.Text>
<ArrowUp size={14} className="log-detail-drawer__footer-hint-icon" />
<span>/</span>
<ArrowDown size={14} className="log-detail-drawer__footer-hint-icon" />
<Typography.Text
type="secondary"
className="log-detail-drawer__footer-hint-text"
>
to view previous/next log
</Typography.Text>
</div>
</div>
)}
</div>
</Drawer>
);
}

View File

@@ -110,6 +110,9 @@ type ListLogViewProps = {
linesPerRow: number;
fontSize: FontSize;
handleChangeSelectedView?: ChangeViewFunctionType;
isActiveLog?: boolean;
onClearActiveLog?: () => void;
logs?: ILog[];
};
function ListLogView({
@@ -121,6 +124,9 @@ function ListLogView({
linesPerRow,
fontSize,
handleChangeSelectedView,
isActiveLog: isActiveLogProp,
onClearActiveLog: onClearActiveLogProp,
logs,
}: ListLogViewProps): JSX.Element {
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
@@ -134,6 +140,7 @@ function ListLogView({
onAddToQuery: handleAddToQuery,
onSetActiveLog: handleSetActiveContextLog,
onClearActiveLog: handleClearActiveContextLog,
onClearActiveLog: onClearActiveLogHook,
} = useActiveLog();
const isDarkMode = useIsDarkMode();
@@ -148,8 +155,20 @@ function ListLogView({
);
const handleDetailedView = useCallback(() => {
if (isActiveLogProp) {
const clearActiveFn = onClearActiveLogProp || onClearActiveLogHook;
clearActiveFn();
return;
}
onSetActiveLog(logData);
}, [logData, onSetActiveLog]);
}, [
logData,
onSetActiveLog,
isActiveLogProp,
onClearActiveLogProp,
onClearActiveLogHook,
]);
const handleShowContext = useCallback(
(event: React.MouseEvent) => {
@@ -258,6 +277,8 @@ function ListLogView({
selectedTab={VIEW_TYPES.CONTEXT}
onClose={handlerClearActiveContextLog}
handleChangeSelectedView={handleChangeSelectedView}
logs={logs}
onNavigateLog={onSetActiveLog}
/>
)}
</>

View File

@@ -40,6 +40,9 @@ function RawLogView({
fontSize,
onLogClick,
handleChangeSelectedView,
onSetActiveLog: onSetActiveLogProp,
onClearActiveLog: onClearActiveLogProp,
managedExternally = false,
}: RawLogViewProps): JSX.Element {
const {
isHighlighted: isUrlHighlighted,
@@ -50,8 +53,8 @@ function RawLogView({
const {
activeLog,
onSetActiveLog,
onClearActiveLog,
onSetActiveLog: onSetActiveLogHook,
onClearActiveLog: onClearActiveLogHook,
onAddToQuery,
} = useActiveLog();
@@ -134,12 +137,35 @@ function RawLogView({
// Use custom click handler if provided, otherwise use default behavior
if (onLogClick) {
onLogClick(data, event);
} else {
onSetActiveLog(data);
setSelectedTab(VIEW_TYPES.OVERVIEW);
return;
}
if (managedExternally) {
// If the log is already active, clear it. Otherwise, set it as active.
if (isActiveLog) {
const clearActiveFn = onClearActiveLogProp || onClearActiveLogHook;
clearActiveFn();
return;
}
const setActiveFn = onSetActiveLogProp || onSetActiveLogHook;
setActiveFn(data);
return;
}
onSetActiveLogHook(data);
setSelectedTab(VIEW_TYPES.OVERVIEW);
},
[isReadOnly, data, onSetActiveLog, onLogClick],
[
isReadOnly,
data,
onSetActiveLogProp,
onSetActiveLogHook,
onClearActiveLogProp,
onClearActiveLogHook,
onLogClick,
isActiveLog,
managedExternally,
],
);
const handleCloseLogDetail: DrawerProps['onClose'] = useCallback(
@@ -149,21 +175,22 @@ function RawLogView({
event.preventDefault();
event.stopPropagation();
onClearActiveLog();
onClearActiveLogHook();
setSelectedTab(undefined);
},
[onClearActiveLog],
[onClearActiveLogHook],
);
const handleShowContext: MouseEventHandler<HTMLElement> = useCallback(
(event) => {
event.preventDefault();
event.stopPropagation();
// handleSetActiveContextLog(data);
const setActiveFn = onSetActiveLogProp || onSetActiveLogHook;
setSelectedTab(VIEW_TYPES.CONTEXT);
onSetActiveLog(data);
setActiveFn(data);
},
[data, onSetActiveLog],
[data, onSetActiveLogProp, onSetActiveLogHook],
);
const html = useMemo(
@@ -173,6 +200,9 @@ function RawLogView({
[text],
);
// Only render LogDetail when not managed externally and this log is active
const showLogDetail = !managedExternally && isActiveLog && selectedTab;
return (
<RawLogViewContainer
onClick={handleClickExpand}
@@ -219,7 +249,7 @@ function RawLogView({
/>
)}
{selectedTab && (
{showLogDetail && (
<LogDetail
selectedTab={selectedTab}
log={activeLog}

View File

@@ -45,9 +45,6 @@ export const RawLogViewContainer = styled(Row)<{
: `margin: 2px 0;`}
}
${({ $isActiveLog, $logType }): string =>
getActiveLogBackground($isActiveLog, true, $logType)}
${({ $isReadOnly, $isActiveLog, $isDarkMode, $logType }): string =>
$isActiveLog
? getActiveLogBackground($isActiveLog, $isDarkMode, $logType)

View File

@@ -16,6 +16,9 @@ export interface RawLogViewProps {
selectedFields?: IField[];
onLogClick?: (log: ILog, event: MouseEvent) => void;
handleChangeSelectedView?: ChangeViewFunctionType;
onSetActiveLog?: (log: ILog) => void;
onClearActiveLog?: () => void;
managedExternally?: boolean;
}
export interface RawLogContentProps {

View File

@@ -1,8 +1,10 @@
/* eslint-disable no-nested-ternary */
import { useCallback, useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { Virtuoso } from 'react-virtuoso';
import { Card } from 'antd';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
@@ -11,6 +13,7 @@ import LogsError from 'container/LogsError/LogsError';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { FontSize } from 'container/OptionsMenu/types';
import { useHandleLogsPagination } from 'hooks/infraMonitoring/useHandleLogsPagination';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { ILog } from 'types/api/logs/log';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
@@ -40,6 +43,16 @@ function EntityLogs({
category,
queryKeyFilters,
}: Props): JSX.Element {
const {
activeLog,
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
} = useActiveLog();
const [selectedTab, setSelectedTab] = useState<
typeof VIEW_TYPES[keyof typeof VIEW_TYPES] | undefined
>();
const basePayload = getEntityEventsOrLogsQueryPayload(
timeRange.startTime,
timeRange.endTime,
@@ -62,6 +75,25 @@ function EntityLogs({
basePayload,
});
const handleSetActiveLogWithTab = useCallback(
(log: ILog): void => {
// If clicking the same log that's already active, close it
if (activeLog?.id === log.id) {
onClearActiveLog();
setSelectedTab(undefined);
return;
}
onSetActiveLog(log);
setSelectedTab(VIEW_TYPES.OVERVIEW);
},
[onSetActiveLog, activeLog, onClearActiveLog],
);
const handleCloseLogDetail = useCallback((): void => {
onClearActiveLog();
setSelectedTab(undefined);
}, [onClearActiveLog]);
const getItemContent = useCallback(
(_: number, logToRender: ILog): JSX.Element => (
<RawLogView
@@ -82,9 +114,13 @@ function EntityLogs({
name: 'timestamp',
},
]}
onSetActiveLog={handleSetActiveLogWithTab}
onClearActiveLog={handleCloseLogDetail}
isActiveLog={activeLog?.id === logToRender.id}
managedExternally
/>
),
[],
[activeLog, handleSetActiveLogWithTab, handleCloseLogDetail],
);
const { data, isLoading, isFetching, isError } = useQuery({
@@ -156,6 +192,17 @@ function EntityLogs({
{!isLoading && !isError && logs.length > 0 && (
<div className="entity-logs-list-container">{renderContent}</div>
)}
{selectedTab && activeLog && (
<LogDetail
log={activeLog}
onClose={handleCloseLogDetail}
logs={logs}
onNavigateLog={handleSetActiveLogWithTab}
selectedTab={selectedTab}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
/>
)}
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import { Card, Typography } from 'antd';
import LogDetail from 'components/LogDetail';
@@ -43,6 +43,10 @@ function LiveLogsList({
onSetActiveLog,
} = useActiveLog();
const [selectedTab, setSelectedTab] = useState<
typeof VIEW_TYPES[keyof typeof VIEW_TYPES] | undefined
>();
// get only data from the logs object
const formattedLogs: ILog[] = useMemo(
() => logs.map((log) => log?.data).flat(),
@@ -65,6 +69,19 @@ function LiveLogsList({
...options.selectColumns,
]);
const handleSetActiveLogWithTab = useCallback(
(log: ILog) => {
onSetActiveLog(log);
setSelectedTab(VIEW_TYPES.OVERVIEW);
},
[onSetActiveLog],
);
const handleCloseLogDetail = useCallback(() => {
onClearActiveLog();
setSelectedTab(undefined);
}, [onClearActiveLog]);
const getItemContent = useCallback(
(_: number, log: ILog): JSX.Element => {
if (options.format === 'raw') {
@@ -72,10 +89,14 @@ function LiveLogsList({
<RawLogView
key={log.id}
data={log}
isActiveLog={activeLog?.id === log.id}
linesPerRow={options.maxLines}
selectedFields={selectedFields}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
onSetActiveLog={handleSetActiveLogWithTab}
onClearActiveLog={handleCloseLogDetail}
managedExternally
/>
);
}
@@ -84,23 +105,29 @@ function LiveLogsList({
<ListLogView
key={log.id}
logData={log}
isActiveLog={activeLog?.id === log.id}
selectedFields={selectedFields}
linesPerRow={options.maxLines}
onAddToQuery={onAddToQuery}
onSetActiveLog={onSetActiveLog}
onSetActiveLog={handleSetActiveLogWithTab}
onClearActiveLog={handleCloseLogDetail}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
logs={formattedLogs}
/>
);
},
[
handleChangeSelectedView,
onAddToQuery,
onSetActiveLog,
options.fontSize,
options.format,
options.maxLines,
options.fontSize,
activeLog?.id,
selectedFields,
onAddToQuery,
handleSetActiveLogWithTab,
handleCloseLogDetail,
handleChangeSelectedView,
formattedLogs,
],
);
@@ -156,6 +183,10 @@ function LiveLogsList({
activeLogIndex,
}}
handleChangeSelectedView={handleChangeSelectedView}
logs={formattedLogs}
onSetActiveLog={handleSetActiveLogWithTab}
onClearActiveLog={handleCloseLogDetail}
activeLog={activeLog}
/>
) : (
<Card style={{ width: '100%' }} bodyStyle={CARD_BODY_STYLE}>
@@ -173,14 +204,16 @@ function LiveLogsList({
</InfinityWrapperStyled>
)}
{activeLog && (
{activeLog && selectedTab && (
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={onClearActiveLog}
onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
logs={formattedLogs}
onNavigateLog={handleSetActiveLogWithTab}
/>
)}
</div>

View File

@@ -325,7 +325,7 @@ export default function TableViewActions(
onOpenChange={setIsOpen}
arrow={false}
content={
<div>
<div data-log-detail-ignore="true">
<Button
className="group-by-clause"
type="text"
@@ -403,7 +403,7 @@ export default function TableViewActions(
onOpenChange={setIsOpen}
arrow={false}
content={
<div>
<div data-log-detail-ignore="true">
<Button
className="group-by-clause"
type="text"

View File

@@ -27,6 +27,8 @@ interface TableRowProps {
logs: ILog[];
hasActions: boolean;
fontSize: FontSize;
isActiveLog?: boolean;
onClearActiveLog?: () => void;
}
export default function TableRow({
@@ -38,6 +40,8 @@ export default function TableRow({
logs,
hasActions,
fontSize,
isActiveLog,
onClearActiveLog,
}: TableRowProps): JSX.Element {
const isDarkMode = useIsDarkMode();
@@ -62,11 +66,21 @@ export default function TableRow({
);
const handleShowLogDetails = useCallback(() => {
if (!onShowLogDetails || !currentLog) {
if (!currentLog) {
return;
}
onShowLogDetails(currentLog);
}, [currentLog, onShowLogDetails]);
// If this log is already active, close the detail drawer
if (isActiveLog && onClearActiveLog) {
onClearActiveLog();
return;
}
// Otherwise, open the detail drawer for this log
if (onShowLogDetails) {
onShowLogDetails(currentLog);
}
}, [currentLog, onShowLogDetails, isActiveLog, onClearActiveLog]);
const hasSingleColumn =
tableColumns.filter((column) => column.key !== 'state-indicator').length ===

View File

@@ -58,7 +58,16 @@ const CustomTableRow: TableComponents<ILog>['TableRow'] = ({
const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
function InfinityTableView(
{ isLoading, tableViewProps, infitiyTableProps, handleChangeSelectedView },
{
isLoading,
tableViewProps,
infitiyTableProps,
handleChangeSelectedView,
logs,
onSetActiveLog: onSetActiveLogProp,
onClearActiveLog: onClearActiveLogProp,
activeLog: activeLogProp,
},
ref,
): JSX.Element | null {
const {
@@ -69,11 +78,30 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
} = useActiveLog();
const {
activeLog,
onSetActiveLog,
onClearActiveLog,
onSetActiveLog: onSetActiveLogHook,
onClearActiveLog: onClearActiveLogHook,
onAddToQuery,
} = useActiveLog();
const hasExternalActiveLog =
onSetActiveLogProp !== undefined ||
onClearActiveLogProp !== undefined ||
activeLogProp !== undefined;
const activeOverviewLog = activeLogProp ?? activeLog;
const onSetActiveLog = useCallback(
(log: ILog) => {
onSetActiveLogProp?.(log);
onSetActiveLogHook(log);
},
[onSetActiveLogProp, onSetActiveLogHook],
);
const onClearActiveLog = useCallback(() => {
onClearActiveLogProp?.();
onClearActiveLogHook();
}, [onClearActiveLogProp, onClearActiveLogHook]);
const { dataSource, columns } = useTableView({
...tableViewProps,
onClickExpand: onSetActiveLog,
@@ -108,6 +136,8 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
hasActions
fontSize={tableViewProps.fontSize}
onShowLogDetails={onSetActiveLog}
isActiveLog={activeOverviewLog?.id === log.id}
onClearActiveLog={onClearActiveLog}
/>
),
[
@@ -116,9 +146,10 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
tableViewProps.fontSize,
tableViewProps.logs,
onSetActiveLog,
activeOverviewLog?.id,
onClearActiveLog,
],
);
const tableHeader = useCallback(
() => (
<tr>
@@ -167,7 +198,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
...props,
context: {
activeContextLogId: activeContextLog?.id,
activeLogId: activeLog?.id,
activeLogId: activeOverviewLog?.id,
},
} as any),
}}
@@ -189,14 +220,18 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
/>
{!hasExternalActiveLog && (
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
logs={logs}
onNavigateLog={onSetActiveLog}
/>
)}
</>
);
},

View File

@@ -1,5 +1,6 @@
import { UseTableViewProps } from 'components/Logs/TableView/types';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ILog } from 'types/api/logs/log';
export type InfinityTableProps = {
isLoading?: boolean;
@@ -8,4 +9,8 @@ export type InfinityTableProps = {
onEndReached: (index: number) => void;
};
handleChangeSelectedView?: ChangeViewFunctionType;
logs?: ILog[];
onSetActiveLog?: (log: ILog) => void;
onClearActiveLog?: () => void;
activeLog?: ILog | null;
};

View File

@@ -1,4 +1,4 @@
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import { Card } from 'antd';
import logEvent from 'api/common/logEvent';
@@ -60,6 +60,10 @@ function LogsExplorerList({
onSetActiveLog,
} = useActiveLog();
const [selectedTab, setSelectedTab] = useState<
typeof VIEW_TYPES[keyof typeof VIEW_TYPES] | undefined
>();
const { options } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: DataSource.LOGS,
@@ -82,6 +86,20 @@ function LogsExplorerList({
() => convertKeysToColumnFields(options.selectColumns),
[options],
);
const handleSetActiveLogWithTab = useCallback(
(log: ILog) => {
onSetActiveLog(log);
setSelectedTab(VIEW_TYPES.OVERVIEW);
},
[onSetActiveLog],
);
const handleCloseLogDetail = useCallback(() => {
onClearActiveLog();
setSelectedTab(undefined);
}, [onClearActiveLog]);
useEffect(() => {
if (!isLoading && !isFetching && !isError && logs.length !== 0) {
logEvent('Logs Explorer: Data present', {
@@ -97,10 +115,14 @@ function LogsExplorerList({
<RawLogView
key={log.id}
data={log}
isActiveLog={activeLog?.id === log.id}
linesPerRow={options.maxLines}
selectedFields={selectedFields}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
onSetActiveLog={handleSetActiveLogWithTab}
onClearActiveLog={handleCloseLogDetail}
managedExternally
/>
);
}
@@ -109,21 +131,26 @@ function LogsExplorerList({
<ListLogView
key={log.id}
logData={log}
isActiveLog={activeLog?.id === log.id}
selectedFields={selectedFields}
onAddToQuery={onAddToQuery}
onSetActiveLog={onSetActiveLog}
onSetActiveLog={handleSetActiveLogWithTab}
activeLog={activeLog}
fontSize={options.fontSize}
linesPerRow={options.maxLines}
handleChangeSelectedView={handleChangeSelectedView}
onClearActiveLog={handleCloseLogDetail}
logs={logs}
/>
);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[
activeLog,
handleChangeSelectedView,
onAddToQuery,
onSetActiveLog,
handleSetActiveLogWithTab,
options.fontSize,
options.format,
options.maxLines,
@@ -153,6 +180,10 @@ function LogsExplorerList({
}}
infitiyTableProps={{ onEndReached }}
handleChangeSelectedView={handleChangeSelectedView}
logs={logs}
onSetActiveLog={handleSetActiveLogWithTab}
onClearActiveLog={handleCloseLogDetail}
activeLog={activeLog}
/>
);
}
@@ -199,6 +230,9 @@ function LogsExplorerList({
getItemContent,
selectedFields,
handleChangeSelectedView,
handleSetActiveLogWithTab,
handleCloseLogDetail,
activeLog,
]);
const isTraceToLogsNavigation = useMemo(() => {
@@ -278,14 +312,18 @@ function LogsExplorerList({
{renderContent}
</InfinityWrapperStyled>
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
/>
{selectedTab && activeLog && (
<LogDetail
selectedTab={selectedTab}
log={activeLog}
onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
logs={logs}
onNavigateLog={handleSetActiveLogWithTab}
/>
)}
</>
)}
</div>

View File

@@ -466,7 +466,10 @@ function LogsExplorerViewsContainer({
</div>
)}
<div className="logs-explorer-views-type-content">
<div
className="logs-explorer-views-type-content"
data-log-detail-ignore="true"
>
{showLiveLogs && (
<LiveLogs handleChangeSelectedView={handleChangeSelectedView} />
)}

View File

@@ -567,6 +567,15 @@ body {
border: 1px solid var(--bg-vanilla-300);
}
.ant-tooltip {
--antd-arrow-background-color: var(--bg-vanilla-100);
.ant-tooltip-inner {
background-color: var(--bg-vanilla-100);
color: var(---bg-ink-500);
}
}
.ant-dropdown-menu {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);

View File

@@ -9,10 +9,9 @@ export const getDefaultLogBackground = (
if (isReadOnly) {
return '';
}
// TODO handle the light mode here
return `&:hover {
background-color: ${
isDarkMode ? 'rgba(171, 189, 255, 0.04)' : 'var(--bg-vanilla-200)'
isDarkMode ? 'rgba(171, 189, 255, 0.04)' : 'rgba(0, 0, 0, 0.04)'
};
}`;
};
@@ -28,22 +27,38 @@ export const getActiveLogBackground = (
if (isDarkMode) {
switch (logType) {
case LogType.INFO:
return `background-color: ${Color.BG_ROBIN_500}10 !important;`;
return `background-color: ${Color.BG_ROBIN_500}40 !important;`;
case LogType.WARN:
return `background-color: ${Color.BG_AMBER_500}10 !important;`;
return `background-color: ${Color.BG_AMBER_500}40 !important;`;
case LogType.ERROR:
return `background-color: ${Color.BG_CHERRY_500}10 !important;`;
return `background-color: ${Color.BG_CHERRY_500}40 !important;`;
case LogType.TRACE:
return `background-color: ${Color.BG_FOREST_400}10 !important;`;
return `background-color: ${Color.BG_FOREST_400}40 !important;`;
case LogType.DEBUG:
return `background-color: ${Color.BG_AQUA_500}10 !important;`;
return `background-color: ${Color.BG_AQUA_500}40 !important;`;
case LogType.FATAL:
return `background-color: ${Color.BG_SAKURA_500}10 !important;`;
return `background-color: ${Color.BG_SAKURA_500}40 !important;`;
default:
return `background-color: ${Color.BG_SLATE_200} !important;`;
return `background-color: ${Color.BG_ROBIN_500}40 !important;`;
}
}
return `background-color: ${Color.BG_VANILLA_400}!important; color: ${Color.TEXT_SLATE_400} !important;`;
// Light mode - use lighter background colors
switch (logType) {
case LogType.INFO:
return `background-color: ${Color.BG_ROBIN_100} !important;`;
case LogType.WARN:
return `background-color: ${Color.BG_AMBER_100} !important;`;
case LogType.ERROR:
return `background-color: ${Color.BG_CHERRY_100} !important;`;
case LogType.TRACE:
return `background-color: ${Color.BG_FOREST_200} !important;`;
case LogType.DEBUG:
return `background-color: ${Color.BG_AQUA_100} !important;`;
case LogType.FATAL:
return `background-color: ${Color.BG_SAKURA_100} !important;`;
default:
return `background-color: ${Color.BG_VANILLA_300} !important;`;
}
};
export const getHightLightedLogBackground = (