mirror of
https://github.com/coder/coder.git
synced 2026-10-29 17:44:33 +08:00
fix(site): replace ansi-to-html in the log viewer (#27206)
This commit is contained in:
+1
-1
@@ -67,7 +67,6 @@
|
||||
"@xterm/addon-web-links": "0.12.0",
|
||||
"@xterm/addon-webgl": "0.19.0",
|
||||
"@xterm/xterm": "5.5.0",
|
||||
"ansi-to-html": "0.7.2",
|
||||
"axios": "1.16.1",
|
||||
"chroma-js": "2.6.0",
|
||||
"class-variance-authority": "0.7.1",
|
||||
@@ -79,6 +78,7 @@
|
||||
"dayjs": "1.11.20",
|
||||
"diff": "8.0.4",
|
||||
"emoji-mart": "5.6.0",
|
||||
"fancy-ansi": "0.1.3",
|
||||
"file-saver": "2.0.5",
|
||||
"formik": "2.4.9",
|
||||
"front-matter": "4.0.2",
|
||||
|
||||
Generated
+10
-17
@@ -122,9 +122,6 @@ importers:
|
||||
'@xterm/xterm':
|
||||
specifier: 5.5.0
|
||||
version: 5.5.0
|
||||
ansi-to-html:
|
||||
specifier: 0.7.2
|
||||
version: 0.7.2
|
||||
axios:
|
||||
specifier: 1.16.1
|
||||
version: 1.16.1
|
||||
@@ -158,6 +155,9 @@ importers:
|
||||
emoji-mart:
|
||||
specifier: 5.6.0
|
||||
version: 5.6.0
|
||||
fancy-ansi:
|
||||
specifier: 0.1.3
|
||||
version: 0.1.3
|
||||
file-saver:
|
||||
specifier: 2.0.5
|
||||
version: 2.0.5
|
||||
@@ -3097,11 +3097,6 @@ packages:
|
||||
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==, tarball: https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ansi-to-html@0.7.2:
|
||||
resolution: {integrity: sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==, tarball: https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz}
|
||||
engines: {node: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
any-promise@1.3.0:
|
||||
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==, tarball: https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz}
|
||||
|
||||
@@ -3864,9 +3859,6 @@ packages:
|
||||
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==, tarball: https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
entities@2.2.0:
|
||||
resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==, tarball: https://registry.npmjs.org/entities/-/entities-2.2.0.tgz}
|
||||
|
||||
entities@6.0.1:
|
||||
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==, tarball: https://registry.npmjs.org/entities/-/entities-6.0.1.tgz}
|
||||
engines: {node: '>=0.12'}
|
||||
@@ -3955,6 +3947,9 @@ packages:
|
||||
extend@3.0.2:
|
||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==, tarball: https://registry.npmjs.org/extend/-/extend-3.0.2.tgz}
|
||||
|
||||
fancy-ansi@0.1.3:
|
||||
resolution: {integrity: sha512-tRQVTo5jjdSIiydqgzIIEZpKddzSsfGLsSVt6vWdjVm7fbvDTiQkyoPu6Z3dIPlAM4OZk0jP5jmTCX4G8WGgBw==, tarball: https://registry.npmjs.org/fancy-ansi/-/fancy-ansi-0.1.3.tgz}
|
||||
|
||||
fast-equals@5.3.2:
|
||||
resolution: {integrity: sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==, tarball: https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.2.tgz}
|
||||
engines: {node: '>=6.0.0'}
|
||||
@@ -9313,10 +9308,6 @@ snapshots:
|
||||
|
||||
ansi-styles@6.2.3: {}
|
||||
|
||||
ansi-to-html@0.7.2:
|
||||
dependencies:
|
||||
entities: 2.2.0
|
||||
|
||||
any-promise@1.3.0: {}
|
||||
|
||||
anymatch@3.1.3:
|
||||
@@ -10078,8 +10069,6 @@ snapshots:
|
||||
|
||||
encodeurl@2.0.0: {}
|
||||
|
||||
entities@2.2.0: {}
|
||||
|
||||
entities@6.0.1: {}
|
||||
|
||||
error-ex@1.3.2:
|
||||
@@ -10210,6 +10199,10 @@ snapshots:
|
||||
|
||||
extend@3.0.2: {}
|
||||
|
||||
fancy-ansi@0.1.3:
|
||||
dependencies:
|
||||
escape-html: 1.0.3
|
||||
|
||||
fast-equals@5.3.2: {}
|
||||
|
||||
fast-glob@3.3.3:
|
||||
|
||||
@@ -3,21 +3,50 @@ import type { Line } from "#/components/Logs/LogLine";
|
||||
import { renderComponent } from "#/testHelpers/renderHelpers";
|
||||
import { AgentLogLine } from "./AgentLogLine";
|
||||
|
||||
const line: Line = {
|
||||
const makeLine = (output: string): Line => ({
|
||||
id: 1,
|
||||
level: "info",
|
||||
output: 'safe <span data-testid="agent-log-xss">xss</span>',
|
||||
output,
|
||||
sourceId: "source-id",
|
||||
time: "2024-03-14T11:31:04.090715Z",
|
||||
};
|
||||
});
|
||||
|
||||
const renderLine = (output: string) =>
|
||||
renderComponent(
|
||||
<AgentLogLine line={makeLine(output)} sourceIcon={null} style={{}} />,
|
||||
);
|
||||
|
||||
describe("AgentLogLine", () => {
|
||||
it("renders log HTML as escaped text", () => {
|
||||
renderComponent(<AgentLogLine line={line} sourceIcon={null} style={{}} />);
|
||||
renderLine('safe <span data-testid="agent-log-xss">xss</span>');
|
||||
|
||||
expect(screen.queryByTestId("agent-log-xss")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/safe <span data-testid="agent-log-xss">xss<\/span>/),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders ANSI color codes as styled markup", () => {
|
||||
renderLine("\u001b[31mred\u001b[0m plain");
|
||||
|
||||
const colored = screen.getByText("red");
|
||||
expect(colored.tagName).toBe("SPAN");
|
||||
expect(colored.getAttribute("style")).toContain("--ansi-red");
|
||||
expect(screen.getByText(/plain/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows only the text after the last carriage return", () => {
|
||||
renderLine("downloading... 50%\rdownloading... 100%");
|
||||
|
||||
expect(screen.getByText("downloading... 100%")).toBeInTheDocument();
|
||||
expect(screen.queryByText(/50%/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a ReDoS payload without hanging", () => {
|
||||
// Cure53 CDM-02-004: this pattern caused catastrophic backtracking in
|
||||
// ansi-to-html. fancy-ansi parses it in linear time.
|
||||
const start = performance.now();
|
||||
renderLine(`\u001b[${"1".repeat(50000)}`);
|
||||
expect(performance.now() - start).toBeLessThan(1000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import AnsiToHTML from "ansi-to-html";
|
||||
import dayjs from "dayjs";
|
||||
import { AnsiHtml } from "fancy-ansi/react";
|
||||
import { type FC, type ReactNode, useMemo } from "react";
|
||||
import { type Line, LogLine, LogLinePrefix } from "#/components/Logs/LogLine";
|
||||
// Approximate height of a log line. Used to control virtualized list height.
|
||||
export const AGENT_LOG_LINE_HEIGHT = 20;
|
||||
|
||||
const convert = new AnsiToHTML({ escapeXML: true });
|
||||
|
||||
interface AgentLogLineProps {
|
||||
line: Line;
|
||||
style: React.CSSProperties;
|
||||
@@ -18,9 +16,13 @@ export const AgentLogLine: FC<AgentLogLineProps> = ({
|
||||
sourceIcon,
|
||||
style,
|
||||
}) => {
|
||||
const output = useMemo(() => {
|
||||
return convert.toHtml(line.output.split(/\r/g).pop() as string);
|
||||
}, [line.output]);
|
||||
// Only render the text after the last carriage return so progress-bar style
|
||||
// output that redraws a single line shows its final state.
|
||||
const lastCarriageReturn = line.output.lastIndexOf("\r");
|
||||
const output =
|
||||
lastCarriageReturn === -1
|
||||
? line.output
|
||||
: line.output.slice(lastCarriageReturn + 1);
|
||||
const timestamp = useMemo(() => {
|
||||
return dayjs(line.time).format("HH:mm:ss.SSS");
|
||||
}, [line.time]);
|
||||
@@ -29,12 +31,7 @@ export const AgentLogLine: FC<AgentLogLineProps> = ({
|
||||
<LogLine className="pl-4" level={line.level} style={style}>
|
||||
{sourceIcon}
|
||||
<LogLinePrefix>{timestamp}</LogLinePrefix>
|
||||
<span
|
||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: Output contains HTML to represent ANSI-code formatting
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: output,
|
||||
}}
|
||||
/>
|
||||
<AnsiHtml text={output} />
|
||||
</LogLine>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import type { Line } from "#/components/Logs/LogLine";
|
||||
import { AGENT_LOG_LINE_HEIGHT } from "./AgentLogLine";
|
||||
import { AgentLogs } from "./AgentLogs";
|
||||
import { MockLogs, MockSources } from "./mocks";
|
||||
@@ -28,3 +29,39 @@ export const Overflowed: Story = {
|
||||
overflowed: true,
|
||||
},
|
||||
};
|
||||
|
||||
const sourceId = MockSources[0].id;
|
||||
|
||||
// Demonstrates how ANSI escape sequences and carriage-return progress-bar
|
||||
// redraws are rendered in the log viewer.
|
||||
const AnsiLogs: readonly Line[] = [
|
||||
{
|
||||
id: 1,
|
||||
level: "info",
|
||||
output:
|
||||
"\u001b[31mred\u001b[0m \u001b[32mgreen\u001b[0m \u001b[34mblue\u001b[0m \u001b[1mbold\u001b[0m \u001b[3mitalic\u001b[0m",
|
||||
time: "2024-03-14T11:31:04.090715Z",
|
||||
sourceId,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
level: "info",
|
||||
output: "\u001b[43m\u001b[30m warning \u001b[0m background colors",
|
||||
time: "2024-03-14T11:31:04.090715Z",
|
||||
sourceId,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
level: "info",
|
||||
output: "downloading... 50%\rdownloading... 100%",
|
||||
time: "2024-03-14T11:31:04.090715Z",
|
||||
sourceId,
|
||||
},
|
||||
];
|
||||
|
||||
export const AnsiFormatting: Story = {
|
||||
args: {
|
||||
logs: AnsiLogs,
|
||||
height: AnsiLogs.length * AGENT_LOG_LINE_HEIGHT,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user