Stack Trace and Breadcrumbs table polish (#112)

This commit is contained in:
Shorpo 2023-11-20 22:43:01 -07:00 committed by GitHub
parent 77c1019432
commit 9c2e279012
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 858 additions and 384 deletions

View file

@ -0,0 +1,5 @@
---
'@hyperdx/app': minor
---
feat: Log Side Panel styling

View file

@ -78,6 +78,7 @@ export default function MyApp({ Component, pageProps }: AppProps) {
name="viewport"
content="width=device-width, initial-scale=0.75"
/>
<meta name="theme-color" content="#25292e"></meta>
</Head>
<SSRProvider>

View file

@ -1,13 +1,13 @@
import {
ColumnDef,
flexRender,
getCoreRowModel,
Row as TableRow,
useReactTable,
} from '@tanstack/react-table';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useState, useCallback, useRef, useEffect, useMemo, memo } from 'react';
import { useCallback, useRef, memo } from 'react';
import cx from 'classnames';
import { UNDEFINED_WIDTH } from './tableUtils';
import api from './api';
import { AggFn } from './ChartUtils';
@ -19,9 +19,6 @@ const Table = ({
data: any[];
valueColumnName: string;
}) => {
// https://github.com/TanStack/table/discussions/3192#discussioncomment-3873093
const UNDEFINED_WIDTH = 99999;
//we need a reference to the scrolling element for logic down below
const tableContainerRef = useRef<HTMLDivElement>(null);

View file

@ -8,7 +8,6 @@ import cx from 'classnames';
import get from 'lodash/get';
import isPlainObject from 'lodash/isPlainObject';
import mapValues from 'lodash/mapValues';
import omit from 'lodash/omit';
import pickBy from 'lodash/pickBy';
import stripAnsi from 'strip-ansi';
import { ErrorBoundary } from 'react-error-boundary';
@ -20,6 +19,19 @@ import { toast } from 'react-toastify';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useQueryParam } from 'use-query-params';
import type { StacktraceFrame, StacktraceBreadcrumb } from './types';
import {
networkColumns,
stacktraceColumns,
breadcrumbColumns,
headerColumns,
StacktraceValue,
CollapsibleSection,
SectionWrapper,
NetworkBody,
LogSidePanelKbdShortcuts,
} from './LogSidePanelElements';
import { Table } from './components/Table';
import api from './api';
import LogLevel from './LogLevel';
@ -27,7 +39,6 @@ import SearchInput from './SearchInput';
import TabBar from './TabBar';
import TimelineChart from './TimelineChart';
import SessionSubpanel from './SessionSubpanel';
import LogSidePanelKbdShortcuts from './LogSidePanelKbdShortcuts';
import {
formatDistanceToNowStrictShort,
useFirstNonNullValue,
@ -48,7 +59,7 @@ const HDX_BODY_FIELD = '_hdx_body';
// https://github.com/reduxjs/redux-devtools/blob/f11383d294c1139081f119ef08aa1169bd2ad5ff/packages/react-json-tree/src/createStylingFromTheme.ts
const JSON_TREE_THEME = {
base00: '#0F1216',
base00: '#00000000',
base01: '#383830',
base02: '#49483e',
base03: '#75715e',
@ -548,6 +559,31 @@ function TraceSubpanel({
// Allows us to determine if the user has changed the search query
const searchedQuery = _searchedQuery ?? '';
const isException = isExceptionSpan({ logData: selectedLogData });
const exceptionBreadcrumbs = useMemo<StacktraceBreadcrumb[]>(() => {
try {
return JSON.parse(
selectedLogData?.['string.values']?.[
selectedLogData?.['string.names']?.indexOf('breadcrumbs')
] ?? '[]',
);
} catch (e) {
return [];
}
}, [selectedLogData]);
const exceptionValues = useMemo<any[]>(() => {
try {
return JSON.parse(
selectedLogData?.['string.values']?.[
selectedLogData?.['string.names']?.indexOf('exception.values')
] ?? '[]',
);
} catch (e) {
return [];
}
}, [selectedLogData]);
// Clear search query when we close the panel
// TODO: This doesn't work because it breaks navigation to things like the sessions page,
// probably due to a race condition. Need to fix later.
@ -596,13 +632,14 @@ function TraceSubpanel({
setSelectedLog({ id, sortKey });
}}
/>
<div className="mt-1 border-top border-dark mb-2">
<div className="border-top border-dark mb-4">
{selectedLogData != null ? (
<>
<div className="my-3">
<span className="text-muted">
{selectedLogData.type === 'span' ? 'Span' : 'Log'} Details for:{' '}
</span>
<div className="text-slate-200 fs-7 mb-2 mt-3">
{selectedLogData.type === 'span' ? 'Span' : 'Log'} Details
</div>
<span>
[<LogLevel level={selectedLogData.severity_text} />]
</span>{' '}
@ -655,44 +692,32 @@ function TraceSubpanel({
)}
>
<ExceptionSubpanel
breadcrumbs={JSON.parse(
selectedLogData?.['string.values']?.[
selectedLogData?.['string.names']?.indexOf('breadcrumbs')
],
)}
exceptionValues={JSON.parse(
selectedLogData?.['string.values']?.[
selectedLogData?.['string.names']?.indexOf(
'exception.values',
)
],
)}
breadcrumbs={exceptionBreadcrumbs}
exceptionValues={exceptionValues}
/>
</ErrorBoundary>
)}
{!isException && (
<>
<ErrorBoundary
onError={err => {
console.error(err);
}}
fallbackRender={() => (
<div className="text-danger px-2 py-1 m-2 fs-7 font-monospace bg-danger-transparent">
An error occurred while rendering event properties.
</div>
)}
>
<PropertySubpanel
logData={selectedLogData}
onPropertyAddClick={onPropertyAddClick}
generateSearchUrl={generateSearchUrl}
onClose={onClose}
generateChartUrl={generateChartUrl}
displayedColumns={displayedColumns}
toggleColumn={toggleColumn}
/>
</ErrorBoundary>
</>
<ErrorBoundary
onError={err => {
console.error(err);
}}
fallbackRender={() => (
<div className="text-danger px-2 py-1 m-2 fs-7 font-monospace bg-danger-transparent">
An error occurred while rendering event properties.
</div>
)}
>
<PropertySubpanel
logData={selectedLogData}
onPropertyAddClick={onPropertyAddClick}
generateSearchUrl={generateSearchUrl}
onClose={onClose}
generateChartUrl={generateChartUrl}
displayedColumns={displayedColumns}
toggleColumn={toggleColumn}
/>
</ErrorBoundary>
)}
<ErrorBoundary
onError={err => {
@ -822,7 +847,7 @@ function EventTagSubpanel({
return (
<div className="my-3">
<div className="fw-bold mb-1">Event Tags</div>
<div className="fw-bold mb-1 mt-2">Event Tags</div>
<div className="d-flex flex-wrap">
{Object.entries(properties).map(([key, value]) => {
let commandArgs = '';
@ -891,50 +916,6 @@ function ExceptionEvent({
);
}
function CollapsibleSection({
title,
children,
initiallyCollapsed,
}: {
title: string;
children: React.ReactNode;
initiallyCollapsed?: boolean;
}) {
const [collapsed, setCollapsed] = useState(initiallyCollapsed ?? false);
return (
<div className="my-3">
<div
className="d-flex align-items-center mb-1 text-white-hover"
role="button"
onClick={() => setCollapsed(!collapsed)}
>
<i className={`bi bi-chevron-${collapsed ? 'right' : 'down'} me-2`}></i>
<div className="fs-7">{title}</div>
</div>
{collapsed ? null : children}
</div>
);
}
function NetworkPropertyRow({
label,
value,
width,
className,
}: {
label: React.ReactNode;
value: React.ReactNode;
width: number;
className?: string;
}) {
return (
<div className={`d-flex ${className ?? ''}`}>
<div style={{ width, minWidth: width }}>{label}</div>
<div className="text-muted">{value}</div>
</div>
);
}
const parseHeaders = (
keyPrefix: string,
parsedProperties: any,
@ -1080,7 +1061,6 @@ function NetworkPropertySubpanel({
field: string;
groupBy: string[];
}) => string;
onPropertyAddClick?: (key: string, value: string) => void;
}) {
const parsedProperties = useParsedLogProperties(logData);
@ -1093,8 +1073,6 @@ function NetworkPropertySubpanel({
return parseHeaders('http.response.header.', parsedProperties);
}, [parsedProperties]);
const LABEL_WIDTH = 200;
const url = parsedProperties['http.url'];
const remoteAddress = parsedProperties['net.peer.ip'];
const statusCode = parsedProperties['http.status_code'];
@ -1145,100 +1123,61 @@ function NetworkPropertySubpanel({
</Button>
</Link>
</div>
<NetworkPropertyRow
className="mb-1"
label="URL"
value={url}
width={LABEL_WIDTH}
/>
<NetworkPropertyRow
className="mb-1"
label="Method"
value={method}
width={LABEL_WIDTH}
/>
{remoteAddress != null && (
<NetworkPropertyRow
className="mb-1"
label="Remote Address"
value={remoteAddress}
width={LABEL_WIDTH}
/>
)}
{statusCode != null && (
<NetworkPropertyRow
className="mb-1"
label="Status"
value={
<span
className={`${
<SectionWrapper>
<Table
borderless
density="compact"
columns={networkColumns}
data={[
url && { label: 'URL', value: url },
method && { label: 'Method', value: method },
remoteAddress && { label: 'Remote Address', value: remoteAddress },
statusCode && {
label: 'Status',
value: `${statusCode} ${
parsedProperties['http.status_text'] ?? ''
}`,
className:
statusCode >= 500
? 'text-danger'
: statusCode >= 400
? 'text-warning'
: 'text-success'
}`}
>
{statusCode} {parsedProperties['http.status_text'] ?? ''}
</span>
}
width={LABEL_WIDTH}
: 'text-success',
},
].filter(Boolean)}
hideHeader
/>
)}
</SectionWrapper>
{requestHeaders.length > 0 && (
<CollapsibleSection
title={`Request Headers (${requestHeaders.length})`}
initiallyCollapsed
>
{requestHeaders.length > 0 ? (
requestHeaders.map(([key, value]) => (
<NetworkPropertyRow
key={key}
className="mb-1"
label={key}
value={value}
width={LABEL_WIDTH}
/>
))
) : (
<div className="text-muted">No request headers collected</div>
)}
<SectionWrapper>
<Table
borderless
hideHeader
density="compact"
columns={headerColumns}
data={requestHeaders}
emptyMessage="No request headers collected"
/>
</SectionWrapper>
</CollapsibleSection>
)}
{requestBody != null && (
<CollapsibleSection title={`Request Body`}>
{requestBody != null ? (
<div className="mb-1">
<pre className="mb-0">
{typeof requestBody === 'string' ? (
requestBody
) : (
<JSONTree
hideRoot
invertTheme={false}
// shouldExpandNode={() => true}
data={requestBody}
theme={JSON_TREE_THEME}
valueRenderer={(raw, value, ...keyPath) => {
return (
<pre
className="d-inline text-break"
style={{
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
}}
>
{raw}
</pre>
);
}}
/>
)}
</pre>
</div>
) : (
<div className="text-muted">No request body collected</div>
)}
<CollapsibleSection title="Request Body">
<SectionWrapper>
<NetworkBody
body={requestBody}
theme={JSON_TREE_THEME}
emptyMessage="Empty request"
notCollectedMessage="No request body collected"
/>
</SectionWrapper>
</CollapsibleSection>
)}
{responseHeaders.length > 0 && (
@ -1246,57 +1185,28 @@ function NetworkPropertySubpanel({
title={`Response Headers (${responseHeaders.length})`}
initiallyCollapsed
>
{responseHeaders.length > 0 ? (
responseHeaders.map(([key, value]) => (
<NetworkPropertyRow
key={key}
className="mb-1"
label={key}
value={value}
width={LABEL_WIDTH}
/>
))
) : (
<div className="text-muted">No response headers collected</div>
)}
<SectionWrapper>
<Table
borderless
hideHeader
density="compact"
columns={headerColumns}
data={responseHeaders}
emptyMessage="No response headers collected"
/>
</SectionWrapper>
</CollapsibleSection>
)}
{responseBody != null && (
<CollapsibleSection title={`Response Body`}>
{responseBody != null && responseBody != '' ? (
<div className="mb-1">
<pre className="mb-0">
{typeof responseBody === 'string' ? (
responseBody
) : (
<JSONTree
hideRoot
invertTheme={false}
// shouldExpandNode={() => true}
data={responseBody}
theme={JSON_TREE_THEME}
valueRenderer={raw => {
return (
<pre
className="d-inline text-break"
style={{
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
}}
>
{raw}
</pre>
);
}}
/>
)}
</pre>
</div>
) : responseBody === '' ? (
<div className="text-muted">Empty response</div>
) : (
<div className="text-muted">No response body collected</div>
)}
<CollapsibleSection title="Response Body">
<SectionWrapper>
<NetworkBody
body={responseBody}
theme={JSON_TREE_THEME}
emptyMessage="Empty response"
notCollectedMessage="No response body collected"
/>
</SectionWrapper>
</CollapsibleSection>
)}
</div>
@ -1939,131 +1849,97 @@ const ExceptionSubpanel = ({
breadcrumbs,
exceptionValues,
}: {
breadcrumbs: {
type?: string;
level?: string;
event_id?: string;
category?: string;
message?: string;
data?: { [key: string]: any };
timestamp: number;
}[];
breadcrumbs?: StacktraceBreadcrumb[];
exceptionValues: {
type: string;
value: string;
mechanism?: {
type: string;
handled: boolean;
data?: {
// TODO: Are these fields dynamic?
function?: string;
handler?: string;
target?: string;
};
};
stacktrace?: {
frames: {
filename: string;
function: string;
module?: string;
lineno: number;
colno: number;
in_app: boolean;
context_line?: string;
pre_context?: string[];
post_context?: string[];
}[];
frames: StacktraceFrame[];
};
}[];
}) => {
const firstException = exceptionValues[0];
const stacktraceFrames = useMemo(
() => firstException.stacktrace?.frames.reverse() ?? [],
[firstException.stacktrace?.frames],
);
// TODO: show all frames (stackable)
return (
<div>
<CollapsibleSection title="Stack Trace" initiallyCollapsed={false}>
{firstException ? (
<div>
<div className="fw-bold fs-8">{firstException.type}</div>
<div className="text-muted">{firstException.value}</div>
<div className="text-muted">
<span>mechanism: {firstException.mechanism?.type}</span>
<span className="ms-2">
handled:{' '}
{firstException.mechanism?.handled ? (
<span className="text-success">true</span>
) : (
<span className="text-danger">false</span>
)}
</span>
</div>
{firstException.stacktrace?.frames?.reverse().map((frame, i) => (
<div key={frame.filename + frame.lineno} className="mt-3">
<div className="fw-bold fs-8">
{frame.filename} in {frame.function} at line {frame.lineno}:
{frame.colno}
</div>
<pre className="mt-3">
{frame.pre_context?.map((line, i) => (
<div key={line} className="text-muted">
{(frame.lineno ?? 0) -
(frame.pre_context?.length ?? 0) +
i}{' '}
{line}
</div>
))}
{frame.context_line && (
<div
className="fw-bold"
style={{ backgroundColor: '#1f2429' }}
>
{frame.lineno} {frame.context_line}
</div>
)}
{frame.post_context?.map((line, i) => (
<div key={line} className="text-muted">
{frame.lineno + i + 1} {line}
</div>
))}
</pre>
<CollapsibleSection title="Stack Trace">
<SectionWrapper
title={
<>
<div className="pb-3">
<div className="fw-bold fs-8">{firstException.type}</div>
<div className="text-muted">{firstException.value}</div>
</div>
))}
</div>
) : (
<div className="text-muted">No Stack Trace Found</div>
)}
<div className="d-flex gap-2 flex-wrap">
<StacktraceValue
label="mechanism"
value={firstException.mechanism?.type}
/>
<StacktraceValue
label="handled"
value={
firstException.mechanism?.handled ? (
<span className="text-success">true</span>
) : (
<span className="text-danger">false</span>
)
}
/>
{firstException.mechanism?.data?.function ? (
<StacktraceValue
label="function"
value={firstException.mechanism.data.function}
/>
) : null}
{firstException.mechanism?.data?.handler ? (
<StacktraceValue
label="handler"
value={firstException.mechanism.data.handler}
/>
) : null}
{firstException.mechanism?.data?.target ? (
<StacktraceValue
label="target"
value={firstException.mechanism.data.target}
/>
) : null}
</div>
</>
}
>
<Table
hideHeader
columns={stacktraceColumns}
data={stacktraceFrames}
emptyMessage="No stack trace found"
/>
</SectionWrapper>
</CollapsibleSection>
<CollapsibleSection title="Breadcrumbs" initiallyCollapsed>
{breadcrumbs.length > 0 ? (
breadcrumbs.map((event, i) => (
<div
key={i}
className={cx({
'd-flex align-items-center': 0,
})}
>
<div className="text-muted mt-3 mb-1">
{format(new Date(event.timestamp * 1000), 'MMM d HH:mm:ss.SSS')}
</div>
<JSONTree
hideRoot
invertTheme={false}
shouldExpandNode={() => true}
data={omit(event, ['timestamp'])}
theme={JSON_TREE_THEME}
valueRenderer={(raw, value, ...keyPath) => {
return (
<pre
className="d-inline text-break"
style={{
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
}}
>
{raw}
</pre>
);
}}
/>
</div>
))
) : (
<div className="text-muted">No Breadcrumbs Found</div>
)}
<CollapsibleSection title="Breadcrumbs">
<SectionWrapper>
<Table
columns={breadcrumbColumns}
data={breadcrumbs}
emptyMessage="No breadcrumbs found"
/>
</SectionWrapper>
</CollapsibleSection>
</div>
);
@ -2213,7 +2089,7 @@ export default function LogSidePanel({
onClose={_onClose}
/>
<TabBar
className="fs-8 mb-2 mt-2"
className="fs-8 mt-2"
items={[
{
text: 'Parsed Properties',
@ -2296,7 +2172,7 @@ export default function LogSidePanel({
{/* Trace */}
{displayedTab === 'trace' ? (
<div
className="flex-grow-1 px-4 mt-3 bg-body overflow-auto"
className="flex-grow-1 px-4 pt-3 bg-body overflow-auto"
style={{ minHeight: 0 }}
>
<TraceSubpanel

View file

@ -0,0 +1,340 @@
import * as React from 'react';
import { format } from 'date-fns';
import { JSONTree } from 'react-json-tree';
import type { StacktraceFrame, StacktraceBreadcrumb } from './types';
import styles from '../styles/LogSidePanel.module.scss';
import { CloseButton } from 'react-bootstrap';
import { useLocalStorage } from './utils';
import { ColumnDef, Row } from '@tanstack/react-table';
import { TableCellButton } from './components/Table';
import LogLevel from './LogLevel';
import { UNDEFINED_WIDTH } from './tableUtils';
export const CollapsibleSection = ({
title,
children,
initiallyCollapsed,
}: {
title: string;
children: React.ReactNode;
initiallyCollapsed?: boolean;
}) => {
const [collapsed, setCollapsed] = React.useState(initiallyCollapsed ?? false);
return (
<div className="my-3">
<div
className={`d-flex align-items-center mb-1 text-white-hover`}
role="button"
onClick={() => setCollapsed(!collapsed)}
>
<i className={`bi bi-chevron-${collapsed ? 'right' : 'down'} me-2`}></i>
<div className="fs-7 text-slate-200">{title}</div>
</div>
{collapsed ? null : <div className="mb-4">{children}</div>}
</div>
);
};
export const SectionWrapper: React.FC<{ title?: React.ReactNode }> = ({
children,
title,
}) => (
<div className={styles.panelSectionWrapper}>
{title && <div className={styles.panelSectionWrapperTitle}>{title}</div>}
{children}
</div>
);
/**
* Stacktrace elements
*/
export const StacktraceValue = ({
label,
value,
}: {
label: React.ReactNode;
value: React.ReactNode;
}) => {
return (
<div
style={{
paddingRight: 20,
marginRight: 12,
borderRight: '1px solid #ffffff20',
}}
>
<div className="text-slate-400">{label}</div>
<div className="fs-7">{value}</div>
</div>
);
};
const StacktraceRowExpandButton = ({
onClick,
isOpen,
}: {
onClick: VoidFunction;
isOpen: boolean;
}) => {
return (
<TableCellButton
label={isOpen ? 'Hide context' : 'Show context'}
biIcon={isOpen ? 'chevron-up' : 'chevron-down'}
onClick={onClick}
/>
);
};
export const StacktraceRow = ({ row }: { row: Row<StacktraceFrame> }) => {
const [lineContextOpen, setLineContextOpen] = React.useState(true);
const frame = row.original;
const hasContext = !!frame.context_line;
const handleToggleContext = React.useCallback(() => {
setLineContextOpen(!lineContextOpen);
}, [lineContextOpen]);
return (
<>
<div className="w-100 d-flex justify-content-between align-items-center">
<div>
{frame.filename}
<span className="text-slate-400">{' in '}</span>
{frame.function}
{frame.lineno || frame.colno ? (
<>
<span className="text-slate-400">{' at line '}</span>
<span className="text-slate-300">
{frame.lineno}:{frame.colno}
</span>
</>
) : null}
</div>
{hasContext && (
<StacktraceRowExpandButton
onClick={handleToggleContext}
isOpen={lineContextOpen}
/>
)}
</div>
{lineContextOpen && hasContext && (
<pre className={styles.lineContext}>
{frame.pre_context?.map((line, i) => (
<div key={line}>
<span className={styles.lineContextLineNo}>
{(frame.lineno ?? 0) - (frame.pre_context?.length ?? 0) + i}
</span>
{line}
</div>
))}
{frame.context_line && (
<div className={styles.lineContextCurrentLine}>
<span className={styles.lineContextLineNo}>{frame.lineno}</span>
{frame.context_line}
</div>
)}
{frame.post_context?.map((line, i) => (
<div key={line}>
<span className={styles.lineContextLineNo}>
{frame.lineno + i + 1}
</span>
{line}
</div>
))}
</pre>
)}
</>
);
};
export const stacktraceColumns: ColumnDef<StacktraceFrame>[] = [
{
accessorKey: 'filename',
cell: StacktraceRow,
},
];
export const breadcrumbColumns: ColumnDef<StacktraceBreadcrumb>[] = [
{
accessorKey: 'category',
header: 'Category',
cell: ({ row }) => (
<span className="text-slate-300">{row.original.category}</span>
),
},
{
accessorKey: 'message',
header: 'Message',
size: UNDEFINED_WIDTH,
cell: ({ row }) =>
row.original.message || <span className="text-slate-500">Empty</span>,
},
{
accessorKey: 'level',
header: '',
size: 140,
cell: ({ row }) =>
row.original.level ? <LogLevel level={row.original.level} /> : null,
},
{
header: 'Timestamp',
size: 220,
cell: ({ row }) => (
<span className="text-slate-500">
{format(new Date(row.original.timestamp * 1000), 'MMM d HH:mm:ss.SSS')}
</span>
),
},
];
/**
* Request / Response Headers elements
*/
export const headerColumns: ColumnDef<[string, string]>[] = [
{
accessorKey: '0',
header: 'Header',
size: 260,
cell: ({ row }) => (
<span className="text-slate-300 text-truncate" title={row.original[0]}>
{row.original[0]}
</span>
),
},
{
size: UNDEFINED_WIDTH,
accessorKey: '1',
header: 'Value',
},
];
/**
* Network Subpanel
*/
export const networkColumns: ColumnDef<{
label: string;
value: string;
className?: string;
}>[] = [
{
accessorKey: 'label',
header: 'Label',
size: 260,
cell: ({ row }) => (
<span className="text-slate-300">{row.original.label}</span>
),
},
{
size: UNDEFINED_WIDTH,
accessorKey: 'value',
header: 'Value',
cell: ({ row }) => (
<span className={row.original.className}>{row.original.value}</span>
),
},
];
export const NetworkBody = ({
body,
theme,
emptyMessage,
notCollectedMessage,
}: {
body: any;
theme?: any;
emptyMessage?: string;
notCollectedMessage?: string;
}) => {
const valueRenderer = React.useCallback(raw => {
return (
<pre
className="d-inline text-break"
style={{
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
}}
>
{raw}
</pre>
);
}, []);
return (
<>
{body != null && body != '' ? (
<pre
className="m-0 px-4 py-3"
style={{
wordBreak: 'break-all',
wordWrap: 'break-word',
whiteSpace: 'pre-wrap',
}}
>
{typeof body === 'string' ? (
body
) : (
<JSONTree
hideRoot
invertTheme={false}
data={body}
theme={theme}
valueRenderer={valueRenderer}
/>
)}
</pre>
) : body === '' ? (
<div className="text-slate-400 px-4 py-3">{emptyMessage}</div>
) : (
<div className="text-slate-400 px-4 py-3">{notCollectedMessage}</div>
)}
</>
);
};
/**
* Keyboard shortcuts
*/
const Kbd = ({ children }: { children: string }) => (
<div className={styles.kbd}>{children}</div>
);
export const LogSidePanelKbdShortcuts = () => {
const [isDismissed, setDismissed] = useLocalStorage<boolean>(
'kbd-shortcuts-dismissed',
false,
);
const handleDismiss = React.useCallback(() => {
setDismissed(true);
}, [setDismissed]);
if (isDismissed) {
return null;
}
return (
<div className={styles.kbdShortcuts}>
<div className="d-flex justify-content-between align-items-center ">
<div className="d-flex align-items-center gap-3">
<div>
Use <Kbd></Kbd>
<Kbd></Kbd> arrow keys to move through events
</div>
<div className={styles.kbdDivider} />
<div>
<Kbd>ESC</Kbd> to close
</div>
</div>
<CloseButton
variant="white"
aria-label="Hide"
onClick={handleDismiss}
/>
</div>
</div>
);
};

View file

@ -1,45 +0,0 @@
import styles from '../styles/LogSidePanel.module.scss';
import { CloseButton } from 'react-bootstrap';
import { useLocalStorage } from './utils';
import * as React from 'react';
const Kbd = ({ children }: { children: string }) => (
<div className={styles.kbd}>{children}</div>
);
export default function LogSidePanelKbdShortcuts() {
const [isDismissed, setDismissed] = useLocalStorage<boolean>(
'kbd-shortcuts-dismissed',
false,
);
const handleDismiss = React.useCallback(() => {
setDismissed(true);
}, []);
if (isDismissed) {
return null;
}
return (
<div className={styles.kbdShortcuts}>
<div className="d-flex justify-content-between align-items-center ">
<div className="d-flex align-items-center gap-3">
<div>
Use <Kbd></Kbd>
<Kbd></Kbd> arrow keys to move through events
</div>
<div className={styles.kbdDivider} />
<div>
<Kbd>ESC</Kbd> to close
</div>
</div>
<CloseButton
variant="white"
aria-label="Hide"
onClick={handleDismiss}
/>
</div>
</div>
);
}

View file

@ -25,6 +25,7 @@ import api from './api';
import { useLocalStorage, usePrevious, useWindowSize } from './utils';
import { useSearchEventStream } from './search';
import { useHotkeys } from 'react-hotkeys-hook';
import { UNDEFINED_WIDTH } from './tableUtils';
import styles from '../styles/LogTable.module.scss';
@ -279,8 +280,6 @@ export const RawLogTable = memo(
const tsFormat = 'MMM d HH:mm:ss.SSS';
const tsShortFormat = 'HH:mm:ss';
// https://github.com/TanStack/table/discussions/3192#discussioncomment-3873093
const UNDEFINED_WIDTH = 99999;
//once the user has scrolled within 500px of the bottom of the table, fetch more data if there is any
const FETCH_NEXT_PAGE_PX = 500;

View file

@ -25,6 +25,7 @@ import api from './api';
import { useWindowSize } from './utils';
import { Pattern } from './PatternSidePanel';
import { Granularity, timeBucketByGranularity } from './ChartUtils';
import { UNDEFINED_WIDTH } from './tableUtils';
const PatternTrendChartTooltip = (props: any) => {
return null;
@ -150,9 +151,6 @@ const MemoPatternTable = memo(
const { width } = useWindowSize();
const isSmallScreen = (width ?? 1000) < 900;
// https://github.com/TanStack/table/discussions/3192#discussioncomment-3873093
const UNDEFINED_WIDTH = 99999;
//we need a reference to the scrolling element for logic down below
const tableContainerRef = useRef<HTMLDivElement>(null);

View file

@ -0,0 +1,101 @@
@import '../../styles/variables';
$compactVerticalPadding: 6px;
$normalVerticalPadding: 10px;
$comfortableVerticalPadding: 14px;
$horizontalPadding: 12px;
.tableWrapper {
table {
width: 100%;
th,
td {
&:first-child {
padding-left: $horizontalPadding * 2;
}
&:last-child {
padding-right: $horizontalPadding * 2;
}
}
tr {
border-bottom: 1px solid $slate-950;
}
tbody {
tr:last-child {
border-bottom: none;
}
}
th {
color: $slate-300;
font-family: Inter;
text-transform: uppercase;
font-size: 9px;
font-weight: 500;
letter-spacing: 1px;
padding: $normalVerticalPadding $horizontalPadding;
}
td {
padding: $normalVerticalPadding $horizontalPadding;
word-wrap: break-word;
word-break: break-all;
vertical-align: top;
}
}
// Borderless
&.tableBorderless {
padding: 4px 0;
tr {
border-bottom: none;
}
}
// Density
&.tableDensityCompact {
th,
td {
padding: $compactVerticalPadding $horizontalPadding;
}
}
&.tableDensityComfortable {
th,
td {
padding: $comfortableVerticalPadding $horizontalPadding;
}
}
}
.emptyMessage {
color: $slate-500;
text-align: center;
padding: 10px;
}
.tableCellButton {
display: flex;
gap: 4px;
border-radius: 4px;
background-color: $slate-950;
border: 1px solid $slate-900;
color: $slate-300;
text-align: center;
font-size: 11px;
font-family: Inter;
&:hover {
background-color: $slate-900;
border-color: $slate-800;
}
&:active {
background-color: $slate-950;
border-color: $slate-950;
}
}

View file

@ -0,0 +1,108 @@
import * as React from 'react';
import cx from 'classnames';
import {
useReactTable,
getCoreRowModel,
flexRender,
ColumnDef,
} from '@tanstack/react-table';
import { UNDEFINED_WIDTH } from '../tableUtils';
import styles from './Table.module.scss';
type TableProps<T extends object> = {
data?: T[];
columns: ColumnDef<T>[];
emptyMessage?: string;
hideHeader?: boolean;
borderless?: boolean;
density?: 'compact' | 'normal' | 'comfortable';
};
export const Table = <T extends object>({
data = [],
columns,
emptyMessage,
hideHeader,
borderless,
density = 'normal',
}: TableProps<T>) => {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
if (!data.length) {
return <div className={styles.emptyMessage}>{emptyMessage}</div>;
}
return (
<div
className={cx(styles.tableWrapper, {
[styles.tableBorderless]: borderless,
[styles.tableDensityCompact]: density === 'compact',
[styles.tableDensityComfortable]: density === 'comfortable',
})}
>
<table>
{!hideHeader && (
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</th>
))}
</tr>
))}
</thead>
)}
<tbody>
{table.getRowModel().rows.map(row => (
<tr key={row.id}>
{row.getVisibleCells().map(cell => (
<td
key={cell.id}
style={{
width:
cell.column.getSize() === UNDEFINED_WIDTH
? '100%'
: cell.column.getSize(),
// Allow unknown width columns to shrink to 0
minWidth:
cell.column.getSize() === UNDEFINED_WIDTH
? 0
: cell.column.getSize(),
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
};
export const TableCellButton: React.VFC<{
title?: string;
label: React.ReactNode;
biIcon?: string;
onClick: VoidFunction;
}> = ({ onClick, title, label, biIcon }) => {
return (
<button className={styles.tableCellButton} title={title} onClick={onClick}>
<span>{label}</span>
{biIcon ? <i className={`bi bi-${biIcon}`} /> : null}
</button>
);
};

View file

@ -0,0 +1,2 @@
// https://github.com/TanStack/table/discussions/3192#discussioncomment-3873093
export const UNDEFINED_WIDTH = 99999;

View file

@ -104,3 +104,25 @@ export type Session = {
export type Dictionary<T> = {
[key: string]: T;
};
export type StacktraceFrame = {
filename: string;
function: string;
module?: string;
lineno: number;
colno: number;
in_app: boolean;
context_line?: string;
pre_context?: string[];
post_context?: string[];
};
export type StacktraceBreadcrumb = {
type?: string;
level?: string;
event_id?: string;
category?: string;
message?: string;
data?: { [key: string]: any };
timestamp: number;
};

View file

@ -1,7 +1,6 @@
import { format as fnsFormat, formatDistanceToNowStrict } from 'date-fns';
import { useRouter } from 'next/router';
import { useState, useEffect, useRef } from 'react';
import Convert from 'ansi-to-html';
import type { MutableRefObject } from 'react';

View file

@ -23,6 +23,7 @@ $padding-x: 24px;
.panelDetails {
padding: $padding-x;
padding-bottom: 16px;
display: flex;
flex-direction: column;
gap: 12px;
@ -48,6 +49,18 @@ $padding-x: 24px;
flex-grow: 1;
}
.panelSectionWrapper {
background-color: #14171b;
margin: 10px -24px 24px;
border-top: 1px solid $slate-950;
border-bottom: 1px solid $slate-950;
}
.panelSectionWrapperTitle {
padding: 18px 24px;
border-bottom: 1px solid $slate-950;
}
.kbdShortcuts {
background: $slate-950;
border-top: 1px solid $slate-900;
@ -71,3 +84,37 @@ $padding-x: 24px;
width: 1px;
background: $slate-800;
}
.lineContext {
color: $slate-300;
background-color: #ffffff08;
padding: 8px 0;
border-radius: 4px;
margin-top: 8px;
}
.lineContextCurrentLine {
background-color: $slate-800;
color: $slate-100;
font-weight: 600;
.lineContextLineNo {
color: $slate-100;
border-right-color: $slate-600;
}
}
.lineContextLineNo {
display: inline-block;
width: 60px;
text-align: right;
border-right: 1px solid $slate-950;
padding-right: 12px;
margin-right: 12px;
}
.emptyMessage {
color: $slate-500;
text-align: center;
padding: 10px;
}

View file

@ -74,6 +74,30 @@ html[class~='dark'] body {
color: $gray-600;
}
.text-slate-700 {
color: $slate-700;
}
.text-slate-600 {
color: $slate-600;
}
.text-slate-500 {
color: $slate-500;
}
.text-slate-400 {
color: $slate-400;
}
.text-slate-300 {
color: $slate-300;
}
.text-slate-200 {
color: $slate-200;
}
.text-muted-hover {
color: $text-muted;
transition: color 0.2s ease;

View file

@ -34,7 +34,7 @@ $slate-600: #526077;
$slate-700: #434e61;
$slate-800: #3a4352;
$slate-900: #343a46;
$slate-950: #1a1d23;
$slate-950: #25292e;
/**
Spacing