mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
Stack Trace and Breadcrumbs table polish (#112)
This commit is contained in:
parent
77c1019432
commit
9c2e279012
16 changed files with 858 additions and 384 deletions
5
.changeset/tame-birds-thank.md
Normal file
5
.changeset/tame-birds-thank.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hyperdx/app': minor
|
||||
---
|
||||
|
||||
feat: Log Side Panel styling
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
340
packages/app/src/LogSidePanelElements.tsx
Normal file
340
packages/app/src/LogSidePanelElements.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
101
packages/app/src/components/Table.module.scss
Normal file
101
packages/app/src/components/Table.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
108
packages/app/src/components/Table.tsx
Normal file
108
packages/app/src/components/Table.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
2
packages/app/src/tableUtils.tsx
Normal file
2
packages/app/src/tableUtils.tsx
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// https://github.com/TanStack/table/discussions/3192#discussioncomment-3873093
|
||||
export const UNDEFINED_WIDTH = 99999;
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ $slate-600: #526077;
|
|||
$slate-700: #434e61;
|
||||
$slate-800: #3a4352;
|
||||
$slate-900: #343a46;
|
||||
$slate-950: #1a1d23;
|
||||
$slate-950: #25292e;
|
||||
|
||||
/**
|
||||
Spacing
|
||||
|
|
|
|||
Loading…
Reference in a new issue