Skip to content

Commit

Permalink
Add Trace component (#1312)
Browse files Browse the repository at this point in the history
  • Loading branch information
goodoldneon committed Apr 30, 2024
1 parent 3b3e685 commit c954b7c
Show file tree
Hide file tree
Showing 9 changed files with 601 additions and 1 deletion.
3 changes: 2 additions & 1 deletion ui/packages/components/src/Card/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type PropsWithChildren } from 'react';
import { cn } from '@inngest/components/utils/classNames';

import { cn } from '../utils/classNames';

type Props = PropsWithChildren<{
accentColor?: string;
Expand Down
45 changes: 45 additions & 0 deletions ui/packages/components/src/TimelineV2/InlineSpans.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { cn } from '../utils/classNames';
import { formatMilliseconds } from '../utils/date';
import { Span } from './Span';
import { toMaybeDate } from './utils';

type Props = {
className?: string;
maxTime: Date;
minTime: Date;
spans: React.ComponentProps<typeof Span>['trace'][];
widths: {
before: number;
queued: number;
running: number;
after: number;
};
};

export function InlineSpans({ className, minTime, maxTime, spans, widths }: Props) {
const startedAt = toMaybeDate(spans[0]?.startedAt);
let durationText;
if (startedAt) {
const endedAt = toMaybeDate(spans[spans.length - 1]?.endedAt) ?? maxTime;
durationText = formatMilliseconds(endedAt.getTime() - startedAt.getTime());
}

return (
<div className={cn('flex h-fit grow items-center', className)}>
<div className="h-0.5 bg-slate-400" style={{ flexGrow: widths.before }}></div>

<div
className="flex"
style={{
flexGrow: widths.queued + widths.running,
}}
>
{spans.map((item) => {
return <Span isInline key={item.id} maxTime={maxTime} minTime={minTime} trace={item} />;
})}
</div>

<div className="h-0.5 bg-slate-400" style={{ flexGrow: widths.after }}></div>
</div>
);
}
77 changes: 77 additions & 0 deletions ui/packages/components/src/TimelineV2/Span.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { cn } from '../utils/classNames';
import { formatMilliseconds } from '../utils/date';
import { StepStatus, isStepStatus } from './types';
import { createSpanWidths, toMaybeDate } from './utils';

type Props = {
className?: string;
isInline?: boolean;
maxTime: Date;
minTime: Date;
trace: {
endedAt: string | null;
id: string;
queuedAt: string;
startedAt: string | null;
status: string;
};
};

export function Span({ className, isInline, maxTime, minTime, trace }: Props) {
const widths = createSpanWidths({
maxTime,
minTime,
trace: {
endedAt: trace.endedAt ? new Date(trace.endedAt) : null,
queuedAt: new Date(trace.queuedAt),
startedAt: trace.startedAt ? new Date(trace.startedAt) : null,
},
});

if (isInline) {
widths.after = 0;
widths.before = 0;
}

const totalWidth = widths.before + widths.queued + widths.running + widths.after;

return (
<div
className={cn('flex flex-grow items-center', className)}
style={{
flexGrow: totalWidth,
}}
>
{/* Gray line to the left of the span */}
<div className="h-0.5 bg-slate-200" style={{ flexGrow: widths.before }}></div>

{/* Queued part of the span */}
<div className="h-2 bg-slate-500" style={{ flexGrow: widths.queued }}></div>

{/* Running part of the span */}
<div
className={cn('h-5 rounded', getStatusColor(trace.status))}
style={{ flexGrow: widths.running }}
></div>

{/* Gray line to the right of the span */}
<div className="h-0.5 bg-slate-200" style={{ flexGrow: widths.after }}></div>
</div>
);
}

const statusColors: { [key in StepStatus | 'UNKNOWN']: string } = {
[StepStatus.Cancelled]: 'bg-slate-400',
[StepStatus.Failed]: 'bg-rose-600',
[StepStatus.Queued]: 'bg-amber-500',
[StepStatus.Running]: 'bg-sky-500',
[StepStatus.Succeeded]: 'bg-teal-500',
UNKNOWN: 'bg-slate-500',
};

function getStatusColor(status: string): string {
if (isStepStatus(status)) {
return statusColors[status];
}
return statusColors['UNKNOWN'];
}
76 changes: 76 additions & 0 deletions ui/packages/components/src/TimelineV2/Trace.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { Meta, StoryObj } from '@storybook/react';

import { Trace } from './Trace';

const meta = {
title: 'Components/Trace',
component: Trace,
parameters: {
themes: {
themeOverride: 'light',
},
},
} satisfies Meta<typeof Trace>;

export default meta;

async function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

async function getOutput(outputID: string) {
// Simulate fetch delay
await sleep(200);

return JSON.stringify({ foo: { bar: 'baz' } });
}

type Story = StoryObj<typeof Trace>;

export const Retry: Story = {
args: {
depth: 0,
getOutput,
trace: {
attempts: 2,
endedAt: '2024-04-23T11:26:43.260Z',
id: '01HW5CY6F5Q3C15CY53Z104WTN',
isRoot: false,
name: 'Step_id_1',
outputID: 'MDFIVzVDWTZGNVEzQzE1Q1k1M1oxMDRXVE4=',
queuedAt: '2024-04-23T11:26:40.460Z',
startedAt: '2024-04-23T11:26:40.560Z',
status: 'SUCCEEDED',
stepInfo: null,
stepOp: 'RUN',
childrenSpans: [
{
attempts: 1,
endedAt: '2024-04-23T11:26:42.060Z',
id: '01HW5D1GRC1AAYJXSNFPC16BHM',
isRoot: false,
name: 'Attempt 1',
outputID: 'MDFIVzVEMUdSQzFBQVlKWFNORlBDMTZCSE0=',
queuedAt: '2024-04-23T11:26:40.460Z',
startedAt: '2024-04-23T11:26:40.560Z',
status: 'FAILED',
stepInfo: null,
stepOp: 'RUN',
},
{
attempts: 1,
endedAt: '2024-04-23T11:26:43.260Z',
id: '01HW5D23MY4MCX421M0RJ7W3AJ',
isRoot: false,
name: 'Attempt 2',
outputID: 'MDFIVzVEMjNNWTRNQ1g0MjFNMFJKN1czQUo=',
queuedAt: '2024-04-23T11:26:42.060Z',
startedAt: '2024-04-23T11:26:42.160Z',
status: 'SUCCEEDED',
stepInfo: null,
stepOp: 'RUN',
},
],
},
},
};
126 changes: 126 additions & 0 deletions ui/packages/components/src/TimelineV2/Trace.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { useEffect, useState } from 'react';

import { CodeBlock } from '../CodeBlock';
import { cn } from '../utils/classNames';
import { InlineSpans } from './InlineSpans';
import { TraceHeading } from './TraceHeading';
import { TraceInfo } from './TraceInfo';
import type { Trace } from './types';
import { createSpanWidths } from './utils';

type Props = {
depth: number;
getOutput: (outputID: string) => Promise<string | null>;
isExpandable?: boolean;
minTime?: Date;
maxTime?: Date;
trace: Trace;
};

export function Trace({ depth, getOutput, isExpandable = true, maxTime, minTime, trace }: Props) {
const [isExpanded, setIsExpanded] = useState(false);
const [output, setOutput] = useState<string>();

useEffect(() => {
if (isExpanded && !output && trace.outputID) {
getOutput(trace.outputID).then((data) => {
setOutput(data ?? undefined);
});
}
}, [isExpanded, output]);

if (!minTime) {
minTime = new Date(trace.queuedAt);
}

if (!maxTime) {
maxTime = new Date(trace.endedAt ?? new Date());
}

const widths = createSpanWidths({
minTime,
maxTime,
trace: {
endedAt: trace.endedAt ? new Date(trace.endedAt) : new Date(),
queuedAt: new Date(trace.queuedAt),
startedAt: trace.startedAt ? new Date(trace.startedAt) : null,
},
});

let spans = [trace];
if (!trace.isRoot && trace.childrenSpans && trace.childrenSpans.length > 0) {
spans = trace.childrenSpans;
}

return (
<div
className={cn(
'py-2',
// We don't want borders or horizontal padding on step attempts
depth === 0 && 'border-b border-slate-300 px-4',
isExpanded && 'bg-blue-50'
)}
>
<div className="flex gap-2">
<div
className={cn(
// Steps and attempts need different widths, since attempts are
// indented
depth === 0 && 'w-72',
depth === 1 && 'w-64'
)}
>
<TraceHeading
isExpandable={isExpandable}
isExpanded={isExpanded}
onClickExpandToggle={() => setIsExpanded((prev) => !prev)}
trace={trace}
/>
</div>

<InlineSpans
className="my-2"
maxTime={maxTime}
minTime={minTime}
spans={spans}
widths={widths}
/>
</div>

{isExpanded && (
<div className="ml-8">
<TraceInfo className="my-4 grow" trace={trace} />

{output && (
<div className="mb-4">
<CodeBlock
tabs={[
{
label: 'Output',
content: output,
},
]}
/>
</div>
)}

{trace.childrenSpans?.map((child, i) => {
return (
<div className="flex">
<div className="grow">
<Trace
depth={depth + 1}
getOutput={getOutput}
maxTime={maxTime}
minTime={minTime}
trace={child}
/>
</div>
</div>
);
})}
</div>
)}
</div>
);
}
Loading

0 comments on commit c954b7c

Please sign in to comment.