Skip to content
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

WD-8617 - fix: Fix displaying the first colorless part of the message in WebCLI #1697

Merged
merged 4 commits into from
Feb 6, 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
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@
"default-case": 0,
"no-param-reassign": 0,
"no-case-declarations": 0,
"prefer-destructuring": 0
"prefer-destructuring": 0,
"promise/catch-or-return": ["error", { "allowFinally": true }]
},
"settings": {
"import/resolver": {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@canonical/react-components": "0.47.1",
"@reduxjs/toolkit": "2.0.1",
"@sentry/browser": "7.93.0",
"ansi-to-html": "0.7.2",
"async-limiter": "2.0.0",
"classnames": "2.5.1",
"clone-deep": "4.0.1",
Expand Down
56 changes: 56 additions & 0 deletions src/components/WebCLI/Output.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { screen } from "@testing-library/react";

import { renderComponent } from "testing/utils";

import Output from "./Output";

describe("Output", () => {
it("should display content and not display help message", () => {
renderComponent(
<Output
content="Output"
helpMessage="Help message"
showHelp={false}
setShouldShowHelp={jest.fn()}
/>,
);
expect(screen.getByText("Output")).toBeInTheDocument();
expect(screen.queryByText("Help message")).not.toBeInTheDocument();
});

it("should not display content and display help message", () => {
renderComponent(
<Output
content="Output"
helpMessage="Help message"
showHelp={true}
setShouldShowHelp={jest.fn()}
/>,
);
expect(screen.queryByRole("Output")).not.toBeInTheDocument();
expect(screen.getByText("Help message")).toBeInTheDocument();
});

it("should display the content with correct formatting", () => {
const content = `\u001b[1;39mApp\n\u001b[0m\u001b[33munknown`;
renderComponent(
<Output
content={content}
helpMessage="Help message"
showHelp={false}
setShouldShowHelp={jest.fn()}
/>,
);
const boldElements = screen.getAllByText(/.*/, { selector: "b" });
expect(boldElements).toHaveLength(1);
expect(boldElements[0].childNodes).toHaveLength(1);
const appSpanElement = boldElements[0].childNodes[0];
expect(appSpanElement).toHaveTextContent("App");
expect(appSpanElement).toHaveStyle({
color: "#FFF",
});
expect(screen.getByText("unknown")).toHaveStyle({
color: "#A50",
});
});
});
81 changes: 6 additions & 75 deletions src/components/WebCLI/Output.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Convert from "ansi-to-html";
import type { ReactNode } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";

export enum TestId {
CODE = "output-code",
Expand All @@ -14,77 +15,9 @@ type Props = {
setShouldShowHelp: (showHelp: boolean) => void;
};

// Colors taken from the VSCode section of
// https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit
const ansiColors = {
0: null, // reset
30: "0,0,0", // Black
31: "205,49,49", // Red
32: "13,188,121", // Green
33: "229,229,16", // Yellow
34: "26,114,200", // Blue
35: "188,63,188", // Magenta
36: "17,168,205", // Cyan
37: "229,229,229", // White
90: "102,102,102", // Bright Black (Gray)
91: "241,76,76", // Bright Red
92: "35,209,139", // Bright Green
93: "245,245,67", // Bright Yellow
94: "59,142,234", // Bright Blue
95: "214,112,214", // Bright Magenta
96: "41,184,219", // Bright Cyan
97: "229,229,229", // Bright White
};

const findANSICode = /\[\d{1,3}m/g;

const getStyle = (ansiCode: number) => {
const fgColor = ansiColors[ansiCode as keyof typeof ansiColors];
if (fgColor) {
return `color: rgb(${fgColor})`;
}
// We may have been provided a background color.
const bgColor = ansiColors[(ansiCode - 10) as keyof typeof ansiColors];
if (bgColor) {
return `color: rgb(${bgColor})`;
}
};

const colorize = (content: string) => {
const colors = Array.from(content.matchAll(findANSICode));
if (colors.length === 0) {
return content;
}

let colorizedContent = "";
let previousIndex = 0;
colors.forEach((color, index) => {
const ansiCode = color[0];
const ansiCodeNumber = ansiCode.replace("[", "").replace("m", "");

if (color.index !== 0 && previousIndex === 0) {
// Add the content up until the first colour without wrapping it.
colorizedContent =
colorizedContent + content.substring(previousIndex, color.index);
previousIndex = color.index ?? 0;
}
const endIndex = colors[index + 1]?.index || content.length;
let part = content
.substring(color.index ?? 0, endIndex)
.replace(ansiCode, "");
const style = getStyle(Number(ansiCodeNumber));
if (style) {
part = `<span style="${style}">${part}</span>`;
}
colorizedContent = colorizedContent + part;
previousIndex = color.index ?? 0;
});
return colorizedContent;
};

const DEFAULT_HEIGHT = 300;
// 20 is a magic number, sometimes the browser stops firing the drag at
// an inoportune time and the element isn't left completely closed.
// an inopportune time and the element isn't left completely closed.
const CONSIDER_CLOSED = 20;
const HELP_HEIGHT = 50;
const dragHandles = ["webcli__output-dragarea", "webcli__output-handle"];
Expand All @@ -95,6 +28,8 @@ const WebCLIOutput = ({
showHelp,
setShouldShowHelp,
}: Props) => {
const convert = new Convert();

const resizeDeltaY = useRef(0);
const [height, setHeight] = useState(1);

Expand Down Expand Up @@ -186,10 +121,6 @@ const WebCLIOutput = ({
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [content]);

// Strip any color escape codes from the content.
content = content.replace(/\\u001b/gi, "");
const colorizedContent = useMemo(() => colorize(content), [content]);

return (
<div className="webcli__output" style={{ height: `${height}px` }}>
<div className="webcli__output-dragarea" aria-hidden={true}>
Expand All @@ -205,7 +136,7 @@ const WebCLIOutput = ({
) : (
<code
data-testid={TestId.CODE}
dangerouslySetInnerHTML={{ __html: colorizedContent }}
dangerouslySetInnerHTML={{ __html: convert.toHtml(content) }}
></code>
)}
</pre>
Expand Down
2 changes: 1 addition & 1 deletion src/components/WebCLI/__snapshots__/WebCLI.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Model Controller Cloud/Region Version SLA Timestamp
controller google-us-east1 google/us-east1 2.9-beta1 unsupported 17:44:14Z

Machine State DNS Inst id Series AZ Message
0 started 35.190.153.209 juju-3686b9-0 focal us-east1-b RUNNING
0 started 35.190.153.209 juju-3686b9-0 focal us-east1-b RUNNING
"
`;

Expand Down
1 change: 0 additions & 1 deletion src/panels/ActionsPanel/ActionsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ export default function ActionsPanel(): JSX.Element {
const getActionsForApplicationCallback = useCallback(() => {
setFetchingActionData(true);
if (appName && modelUUID) {
// eslint-disable-next-line promise/catch-or-return
getActionsForApplication(appName, modelUUID, appStore.getState())
.then((actions) => {
if (actions?.results?.[0]?.actions) {
Expand Down
14 changes: 13 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4752,6 +4752,17 @@ __metadata:
languageName: node
linkType: hard

"ansi-to-html@npm:0.7.2":
version: 0.7.2
resolution: "ansi-to-html@npm:0.7.2"
dependencies:
entities: "npm:^2.2.0"
bin:
ansi-to-html: bin/ansi-to-html
checksum: 031da78f716e7c6b0e391c64f7bc5e95f2d37123dcc3237d8c592dc35830dd0da05e0c3f3e3f8179856cfe5fd85c689d2ad85024b71b50014da9ef6e8fa021cf
languageName: node
linkType: hard

"any-promise@npm:^1.0.0":
version: 1.3.0
resolution: "any-promise@npm:1.3.0"
Expand Down Expand Up @@ -7335,7 +7346,7 @@ __metadata:
languageName: node
linkType: hard

"entities@npm:^2.0.0":
"entities@npm:^2.0.0, entities@npm:^2.2.0":
version: 2.2.0
resolution: "entities@npm:2.2.0"
checksum: 7fba6af1f116300d2ba1c5673fc218af1961b20908638391b4e1e6d5850314ee2ac3ec22d741b3a8060479911c99305164aed19b6254bde75e7e6b1b2c3f3aa3
Expand Down Expand Up @@ -11064,6 +11075,7 @@ __metadata:
"@types/redux-mock-store": "npm:1.0.6"
"@typescript-eslint/eslint-plugin": "npm:6.18.1"
"@typescript-eslint/parser": "npm:6.18.1"
ansi-to-html: "npm:0.7.2"
async-limiter: "npm:2.0.0"
classnames: "npm:2.5.1"
clone-deep: "npm:4.0.1"
Expand Down
Loading