Skip to content

Commit f78b5c1

Browse files
chore(site): refactor logs and add stories (coder#12553)
1 parent 0723dd3 commit f78b5c1

File tree

11 files changed

+278
-201
lines changed

11 files changed

+278
-201
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { chromatic } from "testHelpers/chromatic";
3+
import { LogLine, LogLinePrefix } from "./LogLine";
4+
5+
const meta: Meta<typeof LogLine> = {
6+
title: "components/Logs/LogLine",
7+
parameters: { chromatic },
8+
component: LogLine,
9+
args: {
10+
level: "info",
11+
children: (
12+
<>
13+
<LogLinePrefix>13:45:31.072</LogLinePrefix>
14+
<span>info: Starting build</span>
15+
</>
16+
),
17+
},
18+
};
19+
20+
export default meta;
21+
22+
type Story = StoryObj<typeof LogLine>;
23+
24+
export const Info: Story = {};
25+
26+
export const Debug: Story = {
27+
args: {
28+
level: "debug",
29+
},
30+
};
31+
32+
export const Error: Story = {
33+
args: {
34+
level: "error",
35+
},
36+
};
37+
38+
export const Trace: Story = {
39+
args: {
40+
level: "trace",
41+
},
42+
};
43+
44+
export const Warn: Story = {
45+
args: {
46+
level: "warn",
47+
},
48+
};

site/src/components/Logs/LogLine.tsx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import type { Interpolation, Theme } from "@emotion/react";
2+
import type { FC, HTMLAttributes } from "react";
3+
import type { LogLevel } from "api/typesGenerated";
4+
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
5+
6+
export const DEFAULT_LOG_LINE_SIDE_PADDING = 24;
7+
8+
export interface Line {
9+
time: string;
10+
output: string;
11+
level: LogLevel;
12+
sourceId: string;
13+
}
14+
15+
type LogLineProps = {
16+
level: LogLevel;
17+
} & HTMLAttributes<HTMLPreElement>;
18+
19+
export const LogLine: FC<LogLineProps> = ({ level, ...divProps }) => {
20+
return (
21+
<pre
22+
css={styles.line}
23+
className={`${level} ${divProps.className} logs-line`}
24+
{...divProps}
25+
/>
26+
);
27+
};
28+
29+
export const LogLinePrefix: FC<HTMLAttributes<HTMLSpanElement>> = (props) => {
30+
return <pre css={styles.prefix} {...props} />;
31+
};
32+
33+
const styles = {
34+
line: (theme) => ({
35+
margin: 0,
36+
wordBreak: "break-all",
37+
display: "flex",
38+
alignItems: "center",
39+
fontSize: 13,
40+
color: theme.palette.text.primary,
41+
fontFamily: MONOSPACE_FONT_FAMILY,
42+
height: "auto",
43+
padding: `0 var(--log-line-side-padding, ${DEFAULT_LOG_LINE_SIDE_PADDING}px)`,
44+
45+
"&.error": {
46+
backgroundColor: theme.roles.error.background,
47+
color: theme.roles.error.text,
48+
49+
"& .dashed-line": {
50+
backgroundColor: theme.roles.error.outline,
51+
},
52+
},
53+
54+
"&.debug": {
55+
backgroundColor: theme.roles.info.background,
56+
color: theme.roles.info.text,
57+
58+
"& .dashed-line": {
59+
backgroundColor: theme.roles.info.outline,
60+
},
61+
},
62+
63+
"&.warn": {
64+
backgroundColor: theme.roles.warning.background,
65+
color: theme.roles.warning.text,
66+
67+
"& .dashed-line": {
68+
backgroundColor: theme.roles.warning.outline,
69+
},
70+
},
71+
}),
72+
73+
prefix: (theme) => ({
74+
userSelect: "none",
75+
margin: 0,
76+
display: "inline-block",
77+
color: theme.palette.text.secondary,
78+
marginRight: 24,
79+
}),
80+
} satisfies Record<string, Interpolation<Theme>>;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { chromatic } from "testHelpers/chromatic";
3+
import { MockWorkspaceBuildLogs } from "testHelpers/entities";
4+
import { Logs } from "./Logs";
5+
6+
const meta: Meta<typeof Logs> = {
7+
title: "components/Logs",
8+
parameters: { chromatic },
9+
component: Logs,
10+
args: {
11+
lines: MockWorkspaceBuildLogs.map((log) => ({
12+
level: log.log_level,
13+
time: log.created_at,
14+
output: log.output,
15+
sourceId: log.log_source,
16+
})),
17+
},
18+
};
19+
20+
export default meta;
21+
22+
type Story = StoryObj<typeof Logs>;
23+
24+
const Default: Story = {};
25+
26+
export { Default as Logs };

site/src/components/Logs/Logs.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { Interpolation, Theme } from "@emotion/react";
2+
import dayjs from "dayjs";
3+
import type { FC } from "react";
4+
import { LogLinePrefix, LogLine, type Line } from "./LogLine";
5+
6+
export const DEFAULT_LOG_LINE_SIDE_PADDING = 24;
7+
8+
export interface LogsProps {
9+
lines: Line[];
10+
hideTimestamps?: boolean;
11+
className?: string;
12+
}
13+
14+
export const Logs: FC<LogsProps> = ({
15+
hideTimestamps,
16+
lines,
17+
className = "",
18+
}) => {
19+
return (
20+
<div css={styles.root} className={`${className} logs-container`}>
21+
<div css={{ minWidth: "fit-content" }}>
22+
{lines.map((line, idx) => (
23+
<LogLine key={idx} level={line.level}>
24+
{!hideTimestamps && (
25+
<LogLinePrefix>
26+
{dayjs(line.time).format(`HH:mm:ss.SSS`)}
27+
</LogLinePrefix>
28+
)}
29+
<span>{line.output}</span>
30+
</LogLine>
31+
))}
32+
</div>
33+
</div>
34+
);
35+
};
36+
37+
const styles = {
38+
root: (theme) => ({
39+
minHeight: 156,
40+
padding: "8px 0",
41+
borderRadius: 8,
42+
overflowX: "auto",
43+
background: theme.palette.background.default,
44+
45+
"&:not(:last-child)": {
46+
borderBottom: `1px solid ${theme.palette.divider}`,
47+
borderRadius: 0,
48+
},
49+
}),
50+
} satisfies Record<string, Interpolation<Theme>>;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { Interpolation, Theme } from "@emotion/react";
2+
import AnsiToHTML from "ansi-to-html";
3+
import { type FC, type ReactNode, useMemo } from "react";
4+
import { type Line, LogLine, LogLinePrefix } from "components/Logs/LogLine";
5+
6+
const convert = new AnsiToHTML();
7+
8+
interface AgentLogLineProps {
9+
line: Line;
10+
number: number;
11+
style: React.CSSProperties;
12+
sourceIcon: ReactNode;
13+
maxLineNumber: number;
14+
}
15+
16+
export const AgentLogLine: FC<AgentLogLineProps> = ({
17+
line,
18+
number,
19+
maxLineNumber,
20+
sourceIcon,
21+
style,
22+
}) => {
23+
const output = useMemo(() => {
24+
return convert.toHtml(line.output.split(/\r/g).pop() as string);
25+
}, [line.output]);
26+
27+
return (
28+
<LogLine css={{ paddingLeft: 16 }} level={line.level} style={style}>
29+
{sourceIcon}
30+
<LogLinePrefix
31+
css={styles.number}
32+
style={{
33+
minWidth: `${maxLineNumber.toString().length - 1}em`,
34+
}}
35+
>
36+
{number}
37+
</LogLinePrefix>
38+
<span
39+
// Output contains HTML to represent ANSI-code formatting
40+
dangerouslySetInnerHTML={{
41+
__html: output,
42+
}}
43+
/>
44+
</LogLine>
45+
);
46+
};
47+
48+
const styles = {
49+
number: (theme) => ({
50+
width: 32,
51+
textAlign: "right",
52+
flexShrink: 0,
53+
color: theme.palette.text.disabled,
54+
}),
55+
} satisfies Record<string, Interpolation<Theme>>;

site/src/modules/resources/AgentLogs.tsx

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,8 @@ import {
1010
import { FixedSizeList as List } from "react-window";
1111
import * as API from "api/api";
1212
import type { WorkspaceAgentLogSource } from "api/typesGenerated";
13-
import {
14-
LogLine,
15-
logLineHeight,
16-
} from "modules/workspaces/WorkspaceBuildLogs/Logs";
17-
import type { LineWithID } from "./AgentRow";
13+
import { AgentLogLine } from "./AgentLogLine";
14+
import { AGENT_LOG_LINE_HEIGHT, type LineWithID } from "./AgentRow";
1815

1916
type AgentLogsProps = Omit<
2017
ComponentProps<typeof List>,
@@ -39,7 +36,7 @@ export const AgentLogs = forwardRef<List, AgentLogsProps>(
3936
ref={ref}
4037
css={styles.logs}
4138
itemCount={logs.length}
42-
itemSize={logLineHeight}
39+
itemSize={AGENT_LOG_LINE_HEIGHT}
4340
{...listProps}
4441
>
4542
{({ index, style }) => {
@@ -58,7 +55,7 @@ export const AgentLogs = forwardRef<List, AgentLogsProps>(
5855
}
5956
);
6057
};
61-
const logSource = getLogSource(log.source_id);
58+
const logSource = getLogSource(log.sourceId);
6259

6360
let assignedIcon = false;
6461
let icon: JSX.Element;
@@ -98,7 +95,7 @@ export const AgentLogs = forwardRef<List, AgentLogsProps>(
9895
let nextChangesSource = false;
9996
if (index < logs.length - 1) {
10097
nextChangesSource =
101-
getLogSource(logs[index + 1].source_id).id !== log.source_id;
98+
getLogSource(logs[index + 1].sourceId).id !== log.sourceId;
10299
}
103100
// We don't want every line to repeat the icon, because
104101
// that is ugly and repetitive. This removes the icon
@@ -107,7 +104,7 @@ export const AgentLogs = forwardRef<List, AgentLogsProps>(
107104
// same source.
108105
if (
109106
index > 0 &&
110-
getLogSource(logs[index - 1].source_id).id === log.source_id
107+
getLogSource(logs[index - 1].sourceId).id === log.sourceId
111108
) {
112109
icon = (
113110
<div
@@ -149,10 +146,10 @@ export const AgentLogs = forwardRef<List, AgentLogsProps>(
149146
}
150147

151148
return (
152-
<LogLine
149+
<AgentLogLine
153150
line={logs[index]}
154151
number={index + 1}
155-
maxNumber={logs.length}
152+
maxLineNumber={logs.length}
156153
style={style}
157154
sourceIcon={
158155
<Tooltip
@@ -208,7 +205,7 @@ export const useAgentLogs = (
208205
level: log.level || "info",
209206
output: log.output,
210207
time: log.created_at,
211-
source_id: log.source_id,
208+
sourceId: log.source_id,
212209
}));
213210

214211
if (!previousLogs) {

site/src/modules/resources/AgentRow.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ const storybookLogs: LineWithID[] = [
7979
level: "info",
8080
output: line,
8181
time: "",
82-
source_id: M.MockWorkspaceAgentLogSource.id,
82+
sourceId: M.MockWorkspaceAgentLogSource.id,
8383
}));
8484

8585
const meta: Meta<typeof AgentRow> = {

site/src/modules/resources/AgentRow.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,9 @@ import type {
2121
WorkspaceAgentMetadata,
2222
} from "api/typesGenerated";
2323
import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
24+
import type { Line } from "components/Logs/LogLine";
2425
import { Stack } from "components/Stack/Stack";
2526
import { useProxy } from "contexts/ProxyContext";
26-
import {
27-
type Line,
28-
logLineHeight,
29-
} from "modules/workspaces/WorkspaceBuildLogs/Logs";
3027
import { AgentLatency } from "./AgentLatency";
3128
import { AgentLogs, useAgentLogs } from "./AgentLogs";
3229
import { AgentMetadata } from "./AgentMetadata";
@@ -39,6 +36,9 @@ import { TerminalLink } from "./TerminalLink/TerminalLink";
3936
import { VSCodeDesktopButton } from "./VSCodeDesktopButton/VSCodeDesktopButton";
4037
import { XRayScanAlert } from "./XRayScanAlert";
4138

39+
// Approximate height of a log line. Used to control virtualized list height.
40+
export const AGENT_LOG_LINE_HEIGHT = 20;
41+
4242
// Logs are stored as the Line interface to make rendering
4343
// much more efficient. Instead of mapping objects each time, we're
4444
// able to just pass the array of logs to the component.
@@ -115,7 +115,7 @@ export const AgentRow: FC<AgentRowProps> = ({
115115
level: "error",
116116
output: "Startup logs exceeded the max size of 1MB!",
117117
time: new Date().toISOString(),
118-
source_id: "",
118+
sourceId: "",
119119
});
120120
}
121121
return logs;
@@ -154,7 +154,7 @@ export const AgentRow: FC<AgentRowProps> = ({
154154
const distanceFromBottom =
155155
logListDivRef.current.scrollHeight -
156156
(props.scrollOffset + parent.clientHeight);
157-
setBottomOfLogs(distanceFromBottom < logLineHeight);
157+
setBottomOfLogs(distanceFromBottom < AGENT_LOG_LINE_HEIGHT);
158158
},
159159
[logListDivRef],
160160
);

0 commit comments

Comments
 (0)