Skip to content

chore(site): refactor logs and add stories #12553

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions site/src/components/Logs/LogLine.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { Meta, StoryObj } from "@storybook/react";
import { chromatic } from "testHelpers/chromatic";
import { LogLine, LogLinePrefix } from "./LogLine";

const meta: Meta<typeof LogLine> = {
title: "components/Logs/LogLine",
parameters: { chromatic },
component: LogLine,
args: {
level: "info",
children: (
<>
<LogLinePrefix>13:45:31.072</LogLinePrefix>
<span>info: Starting build</span>
</>
),
},
};

export default meta;

type Story = StoryObj<typeof LogLine>;

export const Info: Story = {};

export const Debug: Story = {
args: {
level: "debug",
},
};

export const Error: Story = {
args: {
level: "error",
},
};

export const Trace: Story = {
args: {
level: "trace",
},
};

export const Warn: Story = {
args: {
level: "warn",
},
};
80 changes: 80 additions & 0 deletions site/src/components/Logs/LogLine.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { Interpolation, Theme } from "@emotion/react";
import type { FC, HTMLAttributes } from "react";
import type { LogLevel } from "api/typesGenerated";
import { MONOSPACE_FONT_FAMILY } from "theme/constants";

export const DEFAULT_LOG_LINE_SIDE_PADDING = 24;

export interface Line {
time: string;
output: string;
level: LogLevel;
sourceId: string;
}

type LogLineProps = {
level: LogLevel;
} & HTMLAttributes<HTMLPreElement>;

export const LogLine: FC<LogLineProps> = ({ level, ...divProps }) => {
return (
<pre
css={styles.line}
className={`${level} ${divProps.className} logs-line`}
{...divProps}
/>
);
};

export const LogLinePrefix: FC<HTMLAttributes<HTMLSpanElement>> = (props) => {
return <pre css={styles.prefix} {...props} />;
};

const styles = {
line: (theme) => ({
margin: 0,
wordBreak: "break-all",
display: "flex",
alignItems: "center",
fontSize: 13,
color: theme.palette.text.primary,
fontFamily: MONOSPACE_FONT_FAMILY,
height: "auto",
padding: `0 var(--log-line-side-padding, ${DEFAULT_LOG_LINE_SIDE_PADDING}px)`,

"&.error": {
backgroundColor: theme.roles.error.background,
color: theme.roles.error.text,

"& .dashed-line": {
backgroundColor: theme.roles.error.outline,
},
},

"&.debug": {
backgroundColor: theme.roles.info.background,
color: theme.roles.info.text,

"& .dashed-line": {
backgroundColor: theme.roles.info.outline,
},
},

"&.warn": {
backgroundColor: theme.roles.warning.background,
color: theme.roles.warning.text,

"& .dashed-line": {
backgroundColor: theme.roles.warning.outline,
},
},
}),

prefix: (theme) => ({
userSelect: "none",
margin: 0,
display: "inline-block",
color: theme.palette.text.secondary,
marginRight: 24,
}),
} satisfies Record<string, Interpolation<Theme>>;
26 changes: 26 additions & 0 deletions site/src/components/Logs/Logs.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Meta, StoryObj } from "@storybook/react";
import { chromatic } from "testHelpers/chromatic";
import { MockWorkspaceBuildLogs } from "testHelpers/entities";
import { Logs } from "./Logs";

const meta: Meta<typeof Logs> = {
title: "components/Logs",
parameters: { chromatic },
component: Logs,
args: {
lines: MockWorkspaceBuildLogs.map((log) => ({
level: log.log_level,
time: log.created_at,
output: log.output,
sourceId: log.log_source,
})),
},
};

export default meta;

type Story = StoryObj<typeof Logs>;

const Default: Story = {};

export { Default as Logs };
50 changes: 50 additions & 0 deletions site/src/components/Logs/Logs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { Interpolation, Theme } from "@emotion/react";
import dayjs from "dayjs";
import type { FC } from "react";
import { LogLinePrefix, LogLine, type Line } from "./LogLine";

export const DEFAULT_LOG_LINE_SIDE_PADDING = 24;

export interface LogsProps {
lines: Line[];
hideTimestamps?: boolean;
className?: string;
}

export const Logs: FC<LogsProps> = ({
hideTimestamps,
lines,
className = "",
}) => {
return (
<div css={styles.root} className={`${className} logs-container`}>
<div css={{ minWidth: "fit-content" }}>
{lines.map((line, idx) => (
<LogLine key={idx} level={line.level}>
{!hideTimestamps && (
<LogLinePrefix>
{dayjs(line.time).format(`HH:mm:ss.SSS`)}
</LogLinePrefix>
)}
<span>{line.output}</span>
</LogLine>
))}
</div>
</div>
);
};

const styles = {
root: (theme) => ({
minHeight: 156,
padding: "8px 0",
borderRadius: 8,
overflowX: "auto",
background: theme.palette.background.default,

"&:not(:last-child)": {
borderBottom: `1px solid ${theme.palette.divider}`,
borderRadius: 0,
},
}),
} satisfies Record<string, Interpolation<Theme>>;
55 changes: 55 additions & 0 deletions site/src/modules/resources/AgentLogLine.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { Interpolation, Theme } from "@emotion/react";
import AnsiToHTML from "ansi-to-html";
import { type FC, type ReactNode, useMemo } from "react";
import { type Line, LogLine, LogLinePrefix } from "components/Logs/LogLine";

const convert = new AnsiToHTML();

interface AgentLogLineProps {
line: Line;
number: number;
style: React.CSSProperties;
sourceIcon: ReactNode;
maxLineNumber: number;
}

export const AgentLogLine: FC<AgentLogLineProps> = ({
line,
number,
maxLineNumber,
sourceIcon,
style,
}) => {
const output = useMemo(() => {
return convert.toHtml(line.output.split(/\r/g).pop() as string);
}, [line.output]);

return (
<LogLine css={{ paddingLeft: 16 }} level={line.level} style={style}>
{sourceIcon}
<LogLinePrefix
css={styles.number}
style={{
minWidth: `${maxLineNumber.toString().length - 1}em`,
}}
>
{number}
</LogLinePrefix>
<span
// Output contains HTML to represent ANSI-code formatting
dangerouslySetInnerHTML={{
__html: output,
}}
/>
</LogLine>
);
};

const styles = {
number: (theme) => ({
width: 32,
textAlign: "right",
flexShrink: 0,
color: theme.palette.text.disabled,
}),
} satisfies Record<string, Interpolation<Theme>>;
21 changes: 9 additions & 12 deletions site/src/modules/resources/AgentLogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,8 @@ import {
import { FixedSizeList as List } from "react-window";
import * as API from "api/api";
import type { WorkspaceAgentLogSource } from "api/typesGenerated";
import {
LogLine,
logLineHeight,
} from "modules/workspaces/WorkspaceBuildLogs/Logs";
import type { LineWithID } from "./AgentRow";
import { AgentLogLine } from "./AgentLogLine";
import { AGENT_LOG_LINE_HEIGHT, type LineWithID } from "./AgentRow";

type AgentLogsProps = Omit<
ComponentProps<typeof List>,
Expand All @@ -39,7 +36,7 @@ export const AgentLogs = forwardRef<List, AgentLogsProps>(
ref={ref}
css={styles.logs}
itemCount={logs.length}
itemSize={logLineHeight}
itemSize={AGENT_LOG_LINE_HEIGHT}
{...listProps}
>
{({ index, style }) => {
Expand All @@ -58,7 +55,7 @@ export const AgentLogs = forwardRef<List, AgentLogsProps>(
}
);
};
const logSource = getLogSource(log.source_id);
const logSource = getLogSource(log.sourceId);

let assignedIcon = false;
let icon: JSX.Element;
Expand Down Expand Up @@ -98,7 +95,7 @@ export const AgentLogs = forwardRef<List, AgentLogsProps>(
let nextChangesSource = false;
if (index < logs.length - 1) {
nextChangesSource =
getLogSource(logs[index + 1].source_id).id !== log.source_id;
getLogSource(logs[index + 1].sourceId).id !== log.sourceId;
}
// We don't want every line to repeat the icon, because
// that is ugly and repetitive. This removes the icon
Expand All @@ -107,7 +104,7 @@ export const AgentLogs = forwardRef<List, AgentLogsProps>(
// same source.
if (
index > 0 &&
getLogSource(logs[index - 1].source_id).id === log.source_id
getLogSource(logs[index - 1].sourceId).id === log.sourceId
) {
icon = (
<div
Expand Down Expand Up @@ -149,10 +146,10 @@ export const AgentLogs = forwardRef<List, AgentLogsProps>(
}

return (
<LogLine
<AgentLogLine
line={logs[index]}
number={index + 1}
maxNumber={logs.length}
maxLineNumber={logs.length}
style={style}
sourceIcon={
<Tooltip
Expand Down Expand Up @@ -208,7 +205,7 @@ export const useAgentLogs = (
level: log.level || "info",
output: log.output,
time: log.created_at,
source_id: log.source_id,
sourceId: log.source_id,
}));

if (!previousLogs) {
Expand Down
2 changes: 1 addition & 1 deletion site/src/modules/resources/AgentRow.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ const storybookLogs: LineWithID[] = [
level: "info",
output: line,
time: "",
source_id: M.MockWorkspaceAgentLogSource.id,
sourceId: M.MockWorkspaceAgentLogSource.id,
}));

const meta: Meta<typeof AgentRow> = {
Expand Down
12 changes: 6 additions & 6 deletions site/src/modules/resources/AgentRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,9 @@ import type {
WorkspaceAgentMetadata,
} from "api/typesGenerated";
import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
import type { Line } from "components/Logs/LogLine";
import { Stack } from "components/Stack/Stack";
import { useProxy } from "contexts/ProxyContext";
import {
type Line,
logLineHeight,
} from "modules/workspaces/WorkspaceBuildLogs/Logs";
import { AgentLatency } from "./AgentLatency";
import { AgentLogs, useAgentLogs } from "./AgentLogs";
import { AgentMetadata } from "./AgentMetadata";
Expand All @@ -39,6 +36,9 @@ import { TerminalLink } from "./TerminalLink/TerminalLink";
import { VSCodeDesktopButton } from "./VSCodeDesktopButton/VSCodeDesktopButton";
import { XRayScanAlert } from "./XRayScanAlert";

// Approximate height of a log line. Used to control virtualized list height.
export const AGENT_LOG_LINE_HEIGHT = 20;

// Logs are stored as the Line interface to make rendering
// much more efficient. Instead of mapping objects each time, we're
// able to just pass the array of logs to the component.
Expand Down Expand Up @@ -115,7 +115,7 @@ export const AgentRow: FC<AgentRowProps> = ({
level: "error",
output: "Startup logs exceeded the max size of 1MB!",
time: new Date().toISOString(),
source_id: "",
sourceId: "",
});
}
return logs;
Expand Down Expand Up @@ -154,7 +154,7 @@ export const AgentRow: FC<AgentRowProps> = ({
const distanceFromBottom =
logListDivRef.current.scrollHeight -
(props.scrollOffset + parent.clientHeight);
setBottomOfLogs(distanceFromBottom < logLineHeight);
setBottomOfLogs(distanceFromBottom < AGENT_LOG_LINE_HEIGHT);
},
[logListDivRef],
);
Expand Down
Loading