mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-28 16:50:40 +01:00
Compare commits
1 Commits
refactor/q
...
feat/log-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4360950aa0 |
@@ -0,0 +1,49 @@
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tooltipContent {
|
||||
--tooltip-z-index: 2100;
|
||||
}
|
||||
|
||||
.dropdownContent {
|
||||
--dropdown-menu-content-z-index: 2100;
|
||||
}
|
||||
|
||||
.leftSection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-family: 'Geist Mono', monospace;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-normal);
|
||||
color: var(--l1-foreground);
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.arrows {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Divider } from '@signozhq/ui/divider';
|
||||
import { DropdownMenuSimple as Dropdown } from '@signozhq/ui/dropdown-menu';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { aggregateAttributesResourcesToString } from 'container/LogDetailedView/utils';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Compass,
|
||||
Copy,
|
||||
Ellipsis,
|
||||
Link,
|
||||
} from '@signozhq/icons';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { MouseEvent } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
|
||||
import styles from './LogDetailsHeader.module.scss';
|
||||
|
||||
const TOOLTIP_CONTENT_PROPS = { className: styles.tooltipContent };
|
||||
|
||||
interface LogDetailsHeaderProps {
|
||||
log: ILog;
|
||||
onNavigatePrev: () => void;
|
||||
onNavigateNext: () => void;
|
||||
isPrevDisabled: boolean;
|
||||
isNextDisabled: boolean;
|
||||
showOpenInExplorer?: boolean;
|
||||
onOpenInExplorer?: () => void;
|
||||
}
|
||||
|
||||
function LogDetailsHeader({
|
||||
log,
|
||||
onNavigatePrev,
|
||||
onNavigateNext,
|
||||
isPrevDisabled,
|
||||
isNextDisabled,
|
||||
showOpenInExplorer = false,
|
||||
onOpenInExplorer,
|
||||
}: LogDetailsHeaderProps): JSX.Element {
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
const { onLogCopy } = useCopyLogLink(log?.id);
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
|
||||
const handleCopyLog = (): void => {
|
||||
copyToClipboard(aggregateAttributesResourcesToString(log));
|
||||
toast.success('Copied to clipboard', { position: 'bottom-right' });
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
key: 'copy-log',
|
||||
label: 'Copy log',
|
||||
icon: <Copy size={14} />,
|
||||
onClick: handleCopyLog,
|
||||
},
|
||||
{
|
||||
key: 'copy-link',
|
||||
label: 'Copy link to log',
|
||||
icon: <Link size={14} />,
|
||||
onClick: (): void => onLogCopy(),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.header} data-log-detail-ignore="true">
|
||||
<div className={styles.leftSection}>
|
||||
<Divider type="vertical" className={styles.divider} />
|
||||
<Typography.Text
|
||||
className={styles.timestamp}
|
||||
data-testid="log-details-header-timestamp"
|
||||
>
|
||||
{formatTimezoneAdjustedTimestamp(
|
||||
log.date ?? log.timestamp,
|
||||
DATE_TIME_FORMATS.DASH_DATETIME,
|
||||
)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
{showOpenInExplorer && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
prefix={<Compass size={16} />}
|
||||
onClick={onOpenInExplorer}
|
||||
>
|
||||
Open in Explorer
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Dropdown
|
||||
menu={{ items: menuItems }}
|
||||
align="end"
|
||||
className={styles.dropdownContent}
|
||||
onClick={(e: MouseEvent): void => e.stopPropagation()}
|
||||
>
|
||||
<Button
|
||||
variant="link"
|
||||
color="secondary"
|
||||
prefix={<Ellipsis size={16} />}
|
||||
data-testid="log-details-header-menu"
|
||||
/>
|
||||
</Dropdown>
|
||||
|
||||
<div className={styles.arrows}>
|
||||
<TooltipSimple
|
||||
title="Move to previous log"
|
||||
side="top"
|
||||
open={isPrevDisabled ? false : undefined}
|
||||
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
prefix={<ChevronUp size={14} />}
|
||||
disabled={isPrevDisabled}
|
||||
onClick={onNavigatePrev}
|
||||
data-testid="log-details-header-prev"
|
||||
/>
|
||||
</TooltipSimple>
|
||||
<TooltipSimple
|
||||
title="Move to next log"
|
||||
side="top"
|
||||
open={isNextDisabled ? false : undefined}
|
||||
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
prefix={<ChevronDown size={14} />}
|
||||
disabled={isNextDisabled}
|
||||
onClick={onNavigateNext}
|
||||
data-testid="log-details-header-next"
|
||||
/>
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
LogDetailsHeader.defaultProps = {
|
||||
showOpenInExplorer: false,
|
||||
onOpenInExplorer: undefined,
|
||||
};
|
||||
|
||||
export default LogDetailsHeader;
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
interface UseLogNavigationParams {
|
||||
logs?: ILog[];
|
||||
activeLogId: string;
|
||||
onNavigateLog?: (log: ILog) => void;
|
||||
onScrollToLog?: (id: string) => void;
|
||||
}
|
||||
|
||||
interface UseLogNavigationReturn {
|
||||
goToPrev: () => void;
|
||||
goToNext: () => void;
|
||||
isPrevDisabled: boolean;
|
||||
isNextDisabled: boolean;
|
||||
}
|
||||
|
||||
export function useLogNavigation({
|
||||
logs,
|
||||
activeLogId,
|
||||
onNavigateLog,
|
||||
onScrollToLog,
|
||||
}: UseLogNavigationParams): UseLogNavigationReturn {
|
||||
const currentIndex = useMemo(
|
||||
() => logs?.findIndex((l) => l.id === activeLogId) ?? -1,
|
||||
[logs, activeLogId],
|
||||
);
|
||||
|
||||
const canNavigate = !!logs?.length && !!onNavigateLog && currentIndex !== -1;
|
||||
const isPrevDisabled = !canNavigate || currentIndex <= 0;
|
||||
const isNextDisabled = !canNavigate || currentIndex >= (logs?.length ?? 0) - 1;
|
||||
|
||||
const goToPrev = useCallback((): void => {
|
||||
if (isPrevDisabled || !logs) {
|
||||
return;
|
||||
}
|
||||
const prev = logs[currentIndex - 1];
|
||||
onNavigateLog?.(prev);
|
||||
onScrollToLog?.(prev.id);
|
||||
}, [isPrevDisabled, logs, currentIndex, onNavigateLog, onScrollToLog]);
|
||||
|
||||
const goToNext = useCallback((): void => {
|
||||
if (isNextDisabled || !logs) {
|
||||
return;
|
||||
}
|
||||
const next = logs[currentIndex + 1];
|
||||
onNavigateLog?.(next);
|
||||
onScrollToLog?.(next.id);
|
||||
}, [isNextDisabled, logs, currentIndex, onNavigateLog, onScrollToLog]);
|
||||
|
||||
return { goToPrev, goToNext, isPrevDisabled, isNextDisabled };
|
||||
}
|
||||
165
frontend/src/components/LogDetail/__tests__/LogDetail.test.tsx
Normal file
165
frontend/src/components/LogDetail/__tests__/LogDetail.test.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import LogDetail from '..';
|
||||
import { VIEW_TYPES } from '../constants';
|
||||
import { LogDetailProps } from '../LogDetail.interfaces';
|
||||
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
toast: { success: jest.fn(), error: jest.fn() },
|
||||
}));
|
||||
|
||||
// The flag to be removed later
|
||||
jest.mock('../constants', () => ({
|
||||
...jest.requireActual('../constants'),
|
||||
isLogDetailsV2: true,
|
||||
}));
|
||||
|
||||
const mockLog: ILog = {
|
||||
id: 'log-1',
|
||||
timestamp: '2024-01-15T09:45:30Z',
|
||||
date: '2024-01-15T09:45:30Z',
|
||||
body: 'test log body',
|
||||
severityText: 'INFO',
|
||||
severityNumber: 9,
|
||||
traceFlags: 0,
|
||||
traceId: '',
|
||||
spanID: '',
|
||||
attributesString: {},
|
||||
attributesInt: {},
|
||||
attributesFloat: {},
|
||||
resources_string: {},
|
||||
scope_string: {},
|
||||
attributes_string: {},
|
||||
severity_text: 'INFO',
|
||||
severity_number: 9,
|
||||
};
|
||||
|
||||
const makeLog = (id: string): ILog => ({ ...mockLog, id });
|
||||
|
||||
function renderDrawer(props: Partial<LogDetailProps> = {}): void {
|
||||
render(
|
||||
<LogDetail
|
||||
log={mockLog}
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
onAddToQuery={jest.fn()}
|
||||
onClickActionItem={jest.fn()}
|
||||
onClose={jest.fn()}
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('LogDetail drawer — header (isLogDetailsV2)', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
localStorage.clear();
|
||||
// Reset the window path (the infra "Open in Explorer" test mutates it).
|
||||
window.history.pushState({}, '', '/');
|
||||
});
|
||||
|
||||
it('renders the revamped header when a log is provided', () => {
|
||||
renderDrawer();
|
||||
|
||||
expect(screen.getByTestId('log-details-header-menu')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('log-details-header-prev')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('log-details-header-next')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the log timestamp formatted (DASH_DATETIME) in the header', () => {
|
||||
// Pin the timezone to UTC so the formatted output is deterministic across
|
||||
// machines/CI (Jest doesn't fix a TZ).
|
||||
localStorage.setItem(LOCALSTORAGE.PREFERRED_TIMEZONE, 'UTC');
|
||||
|
||||
renderDrawer();
|
||||
|
||||
// mockLog date is 2024-01-15T09:45:30Z → DASH_DATETIME in UTC.
|
||||
expect(screen.getByTestId('log-details-header-timestamp')).toHaveTextContent(
|
||||
'Jan 15, 2024 ⎯ 09:45:30',
|
||||
);
|
||||
});
|
||||
|
||||
it('copies the log link from the ⋯ menu', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
renderDrawer();
|
||||
|
||||
await user.click(screen.getByTestId('log-details-header-menu'));
|
||||
await user.click(await screen.findByText('Copy link to log'));
|
||||
|
||||
expect(toast.success).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('copies the log from the ⋯ menu', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
renderDrawer();
|
||||
|
||||
await user.click(screen.getByTestId('log-details-header-menu'));
|
||||
await user.click(await screen.findByText('Copy log'));
|
||||
|
||||
expect(toast.success).toHaveBeenCalledWith('Copied to clipboard', {
|
||||
position: 'bottom-right',
|
||||
});
|
||||
});
|
||||
|
||||
it('shows "Open in Explorer" on infrastructure-monitoring routes', () => {
|
||||
window.history.pushState({}, '', '/infrastructure-monitoring');
|
||||
|
||||
renderDrawer();
|
||||
|
||||
expect(screen.getByText('Open in Explorer')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides "Open in Explorer" outside infrastructure-monitoring routes', () => {
|
||||
renderDrawer();
|
||||
|
||||
expect(screen.queryByText('Open in Explorer')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('navigates to the next / previous log with the Down / Up arrow keys', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const logs = [makeLog('log-0'), makeLog('log-1'), makeLog('log-2')];
|
||||
const onNavigateLog = jest.fn();
|
||||
const onScrollToLog = jest.fn();
|
||||
|
||||
// Active log is the middle one so both directions are available.
|
||||
renderDrawer({ log: logs[1], logs, onNavigateLog, onScrollToLog });
|
||||
|
||||
await user.keyboard('{ArrowDown}');
|
||||
expect(onNavigateLog).toHaveBeenLastCalledWith(logs[2]);
|
||||
expect(onScrollToLog).toHaveBeenLastCalledWith('log-2');
|
||||
|
||||
await user.keyboard('{ArrowUp}');
|
||||
expect(onNavigateLog).toHaveBeenLastCalledWith(logs[0]);
|
||||
expect(onScrollToLog).toHaveBeenLastCalledWith('log-0');
|
||||
});
|
||||
|
||||
it('does not navigate past the first log on ArrowUp', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const logs = [makeLog('log-0'), makeLog('log-1')];
|
||||
const onNavigateLog = jest.fn();
|
||||
|
||||
renderDrawer({ log: logs[0], logs, onNavigateLog });
|
||||
|
||||
await user.keyboard('{ArrowUp}');
|
||||
expect(onNavigateLog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('navigates via the header up / down buttons and disables them at boundaries', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const logs = [makeLog('log-0'), makeLog('log-1')];
|
||||
const onNavigateLog = jest.fn();
|
||||
|
||||
// Active log is the first one.
|
||||
renderDrawer({ log: logs[0], logs, onNavigateLog });
|
||||
|
||||
expect(screen.getByTestId('log-details-header-prev')).toBeDisabled();
|
||||
expect(screen.getByTestId('log-details-header-next')).toBeEnabled();
|
||||
|
||||
await user.click(screen.getByTestId('log-details-header-next'));
|
||||
expect(onNavigateLog).toHaveBeenLastCalledWith(logs[1]);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,10 @@
|
||||
import getLocalStorage from 'api/browser/localstorage/get';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
|
||||
// Temp feature flag before actual roll-out
|
||||
export const isLogDetailsV2 =
|
||||
getLocalStorage(LOCALSTORAGE.LOG_DETAILS_V2) === 'true';
|
||||
|
||||
export const VIEW_TYPES = {
|
||||
OVERVIEW: 'OVERVIEW',
|
||||
JSON: 'JSON',
|
||||
|
||||
@@ -9,7 +9,9 @@ import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
|
||||
import { Divider } from '@signozhq/ui/divider';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
|
||||
import LogStateIndicator, {
|
||||
LogType,
|
||||
} from 'components/Logs/LogStateIndicator/LogStateIndicator';
|
||||
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
|
||||
import { convertExpressionToFilters } from 'components/QueryBuilderV2/utils';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
@@ -27,6 +29,7 @@ import {
|
||||
} from 'container/LogDetailedView/utils';
|
||||
import useInitialQuery from 'container/LogsExplorerContext/useInitialQuery';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
@@ -57,8 +60,10 @@ import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
|
||||
import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
|
||||
import { isLogDetailsV2, RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
|
||||
import { LogDetailInnerProps, LogDetailProps } from './LogDetail.interfaces';
|
||||
import LogDetailsHeader from './LogDetailsHeader/LogDetailsHeader';
|
||||
import { useLogNavigation } from './LogDetailsHeader/useLogNavigation';
|
||||
|
||||
import './LogDetails.styles.scss';
|
||||
|
||||
@@ -103,7 +108,8 @@ function LogDetailInner({
|
||||
target.closest('[data-log-detail-ignore="true"]') ||
|
||||
target.closest('.cm-tooltip-autocomplete') ||
|
||||
target.closest('.drawer-popover') ||
|
||||
target.closest('.query-status-popover')
|
||||
target.closest('.query-status-popover') ||
|
||||
target.closest('[data-radix-popper-content-wrapper]')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -119,49 +125,30 @@ function LogDetailInner({
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
// Keyboard navigation - handle up/down arrow keys
|
||||
// Only listen when in OVERVIEW tab
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const { goToPrev, goToNext, isPrevDisabled, isNextDisabled } =
|
||||
useLogNavigation({
|
||||
logs,
|
||||
activeLogId: log.id,
|
||||
onNavigateLog,
|
||||
onScrollToLog,
|
||||
});
|
||||
|
||||
// Keyboard navigation - handle up/down arrow keys. Only listen in the OVERVIEW
|
||||
// tab so we don't hijack arrow keys from the JSON editor / context view.
|
||||
useEffect(() => {
|
||||
if (
|
||||
!logs ||
|
||||
!onNavigateLog ||
|
||||
logs.length === 0 ||
|
||||
selectedView !== VIEW_TYPES.OVERVIEW
|
||||
) {
|
||||
return;
|
||||
if (selectedView !== VIEW_TYPES.OVERVIEW) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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) {
|
||||
const prevLog = logs[currentIndex - 1];
|
||||
onNavigateLog(prevLog);
|
||||
// Trigger scroll to the log element
|
||||
if (onScrollToLog) {
|
||||
onScrollToLog(prevLog.id);
|
||||
}
|
||||
}
|
||||
goToPrev();
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// Navigate to next log
|
||||
if (currentIndex < logs.length - 1) {
|
||||
const nextLog = logs[currentIndex + 1];
|
||||
onNavigateLog(nextLog);
|
||||
// Trigger scroll to the log element
|
||||
if (onScrollToLog) {
|
||||
onScrollToLog(nextLog.id);
|
||||
}
|
||||
}
|
||||
goToNext();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -169,7 +156,7 @@ function LogDetailInner({
|
||||
return (): void => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [log.id, logs, onNavigateLog, onScrollToLog, selectedView]);
|
||||
}, [selectedView, goToPrev, goToNext]);
|
||||
|
||||
const listQuery = useMemo(() => {
|
||||
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) {
|
||||
@@ -341,33 +328,6 @@ function LogDetailInner({
|
||||
);
|
||||
|
||||
const logType = log?.attributes_string?.log_level || LogType.INFO;
|
||||
const currentLogIndex = logs ? logs.findIndex((l) => l.id === log.id) : -1;
|
||||
const isPrevDisabled =
|
||||
!logs || !onNavigateLog || logs.length === 0 || currentLogIndex <= 0;
|
||||
const isNextDisabled =
|
||||
!logs ||
|
||||
!onNavigateLog ||
|
||||
logs.length === 0 ||
|
||||
currentLogIndex === logs.length - 1;
|
||||
|
||||
type HandleNavigateLogParams = {
|
||||
direction: 'next' | 'previous';
|
||||
};
|
||||
|
||||
const handleNavigateLog = ({ direction }: HandleNavigateLogParams): void => {
|
||||
if (!logs || !onNavigateLog || currentLogIndex === -1) {
|
||||
return;
|
||||
}
|
||||
if (direction === 'previous' && !isPrevDisabled) {
|
||||
const prevLog = logs[currentLogIndex - 1];
|
||||
onNavigateLog(prevLog);
|
||||
onScrollToLog?.(prevLog.id);
|
||||
} else if (direction === 'next' && !isNextDisabled) {
|
||||
const nextLog = logs[currentLogIndex + 1];
|
||||
onNavigateLog(nextLog);
|
||||
onScrollToLog?.(nextLog.id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
@@ -375,57 +335,69 @@ function LogDetailInner({
|
||||
mask={false}
|
||||
maskClosable={false}
|
||||
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>
|
||||
</div>
|
||||
<div className="log-detail-drawer__title-right">
|
||||
<div className="log-arrows">
|
||||
<Tooltip
|
||||
title={isPrevDisabled ? '' : 'Move to previous log'}
|
||||
placement="top"
|
||||
mouseLeaveDelay={0}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
prefix={<ChevronUp size={14} />}
|
||||
className="log-arrow-btn log-arrow-btn-up"
|
||||
disabled={isPrevDisabled}
|
||||
onClick={(): void => handleNavigateLog({ direction: 'previous' })}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={isNextDisabled ? '' : 'Move to next log'}
|
||||
placement="top"
|
||||
mouseLeaveDelay={0}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
prefix={<ChevronDown size={14} />}
|
||||
className="log-arrow-btn log-arrow-btn-down"
|
||||
disabled={isNextDisabled}
|
||||
onClick={(): void => handleNavigateLog({ direction: 'next' })}
|
||||
/>
|
||||
</Tooltip>
|
||||
isLogDetailsV2 ? (
|
||||
<LogDetailsHeader
|
||||
log={log}
|
||||
onNavigatePrev={goToPrev}
|
||||
onNavigateNext={goToNext}
|
||||
isPrevDisabled={isPrevDisabled}
|
||||
isNextDisabled={isNextDisabled}
|
||||
showOpenInExplorer={showOpenInExplorerBtn}
|
||||
onOpenInExplorer={handleOpenInExplorer}
|
||||
/>
|
||||
) : (
|
||||
<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>
|
||||
</div>
|
||||
{showOpenInExplorerBtn && (
|
||||
<div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
prefix={<Compass size={16} />}
|
||||
className="open-in-explorer-btn"
|
||||
onClick={handleOpenInExplorer}
|
||||
<div className="log-detail-drawer__title-right">
|
||||
<div className="log-arrows">
|
||||
<Tooltip
|
||||
title={isPrevDisabled ? '' : 'Move to previous log'}
|
||||
placement="top"
|
||||
mouseLeaveDelay={0}
|
||||
>
|
||||
Open in Explorer
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
prefix={<ChevronUp size={14} />}
|
||||
className="log-arrow-btn log-arrow-btn-up"
|
||||
disabled={isPrevDisabled}
|
||||
onClick={goToPrev}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={isNextDisabled ? '' : 'Move to next log'}
|
||||
placement="top"
|
||||
mouseLeaveDelay={0}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
prefix={<ChevronDown size={14} />}
|
||||
className="log-arrow-btn log-arrow-btn-down"
|
||||
disabled={isNextDisabled}
|
||||
onClick={goToNext}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{showOpenInExplorerBtn && (
|
||||
<div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
prefix={<Compass size={16} />}
|
||||
className="open-in-explorer-btn"
|
||||
onClick={handleOpenInExplorer}
|
||||
>
|
||||
Open in Explorer
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
placement="right"
|
||||
onClose={drawerCloseHandler}
|
||||
@@ -440,7 +412,15 @@ function LogDetailInner({
|
||||
>
|
||||
<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)} />
|
||||
{isLogDetailsV2 ? (
|
||||
<LogStateIndicator
|
||||
severityText={log.severity_text}
|
||||
severityNumber={log.severity_number}
|
||||
fontSize={options?.fontSize ?? FontSize.MEDIUM}
|
||||
/>
|
||||
) : (
|
||||
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
|
||||
)}
|
||||
<Tooltip
|
||||
title={removeEscapeCharacters(logBody)}
|
||||
placement="left"
|
||||
@@ -516,22 +496,25 @@ function LogDetailInner({
|
||||
</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'
|
||||
}
|
||||
mouseLeaveDelay={0}
|
||||
>
|
||||
<Button
|
||||
variant="link"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
prefix={<Copy size={12} />}
|
||||
onClick={selectedView === VIEW_TYPES.JSON ? handleJSONCopy : onLogCopy}
|
||||
/>
|
||||
</Tooltip>
|
||||
{/* V2 moves copy actions into the header ⋯ menu */}
|
||||
{!isLogDetailsV2 && (
|
||||
<Tooltip
|
||||
title={selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'}
|
||||
placement="topLeft"
|
||||
aria-label={
|
||||
selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'
|
||||
}
|
||||
mouseLeaveDelay={0}
|
||||
>
|
||||
<Button
|
||||
variant="link"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
prefix={<Copy size={12} />}
|
||||
onClick={selectedView === VIEW_TYPES.JSON ? handleJSONCopy : onLogCopy}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isFilterVisible && contextQuery?.builder.queryData[0] && (
|
||||
|
||||
@@ -13,6 +13,7 @@ export enum LOCALSTORAGE {
|
||||
TRACES_LIST_COLUMNS = 'TRACES_LIST_COLUMNS',
|
||||
LOGS_LIST_COLUMNS = 'LOGS_LIST_COLUMNS',
|
||||
LOGS_LIST_COLUMN_SIZING = 'LOGS_LIST_COLUMN_SIZING',
|
||||
LOG_DETAILS_V2 = 'LOG_DETAILS_V2',
|
||||
LOGGED_IN_USER_NAME = 'LOGGED_IN_USER_NAME',
|
||||
LOGGED_IN_USER_EMAIL = 'LOGGED_IN_USER_EMAIL',
|
||||
CHAT_SUPPORT = 'CHAT_SUPPORT',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MouseEventHandler } from 'react';
|
||||
import { MouseEvent } from 'react';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
@@ -11,7 +11,7 @@ export type UseCopyLogLink = {
|
||||
isHighlighted: boolean;
|
||||
isLogsExplorerPage: boolean;
|
||||
activeLogId: string | null;
|
||||
onLogCopy: MouseEventHandler<HTMLElement>;
|
||||
onLogCopy: (event?: MouseEvent<HTMLElement>) => void;
|
||||
onClearActiveLog: () => void;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
MouseEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { MouseEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
@@ -46,14 +40,14 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
|
||||
[pathname],
|
||||
);
|
||||
|
||||
const onLogCopy: MouseEventHandler<HTMLElement> = useCallback(
|
||||
(event) => {
|
||||
const onLogCopy = useCallback(
|
||||
(event?: MouseEvent<HTMLElement>): void => {
|
||||
if (!logId) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
||||
urlQuery.delete(QueryParams.activeLogId);
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
@@ -66,7 +60,7 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
|
||||
|
||||
setCopy(link);
|
||||
|
||||
toast.success('Copied to clipboard', { position: 'top-right' });
|
||||
toast.success('Copied to clipboard', { position: 'bottom-right' });
|
||||
},
|
||||
[logId, urlQuery, minTime, maxTime, pathname, setCopy],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user