From 351a85006855ea91638b72585b96c5edc1544d45 Mon Sep 17 00:00:00 2001 From: vladimir-cucu Date: Fri, 2 Feb 2024 12:41:28 +0200 Subject: [PATCH 1/4] fix: Fix displaying the first colorless part of the message in WebCLI --- src/components/WebCLI/Output.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/components/WebCLI/Output.tsx b/src/components/WebCLI/Output.tsx index 21ec59d70..77aac25e5 100644 --- a/src/components/WebCLI/Output.tsx +++ b/src/components/WebCLI/Output.tsx @@ -57,16 +57,13 @@ const colorize = (content: string) => { } 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) { + if (color.index !== 0 && index === 0) { // Add the content up until the first colour without wrapping it. - colorizedContent = - colorizedContent + content.substring(previousIndex, color.index); - previousIndex = color.index ?? 0; + colorizedContent += content.substring(0, color.index); } const endIndex = colors[index + 1]?.index || content.length; let part = content @@ -77,14 +74,13 @@ const colorize = (content: string) => { part = `${part}`; } 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"]; From 03bb47cad43e7081403f3a569f9b26a7e508ff7e Mon Sep 17 00:00:00 2001 From: vladimir-cucu Date: Fri, 2 Feb 2024 15:31:09 +0200 Subject: [PATCH 2/4] test: Add Output tests --- src/components/WebCLI/Output.test.tsx | 52 +++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/components/WebCLI/Output.test.tsx diff --git a/src/components/WebCLI/Output.test.tsx b/src/components/WebCLI/Output.test.tsx new file mode 100644 index 000000000..86bf25e3a --- /dev/null +++ b/src/components/WebCLI/Output.test.tsx @@ -0,0 +1,52 @@ +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( + , + ); + expect(screen.getByText("Output")).toBeInTheDocument(); + expect(screen.queryByText("Help message")).not.toBeInTheDocument(); + }); + + it("should not display content and display help message", () => { + renderComponent( + , + ); + expect(screen.queryByRole("Output")).not.toBeInTheDocument(); + expect(screen.getByText("Help message")).toBeInTheDocument(); + }); + + it("should display the content with correct color", () => { + renderComponent( + , + ); + expect(screen.getByText("Regular output")).toBeInTheDocument(); + expect(screen.getByText("Red output")).toHaveStyle({ + color: "rgb(205,49,49)", + }); + expect(screen.getByText("Blue output")).toHaveStyle({ + color: "rgb(26,114,200)", + }); + expect(screen.queryByText("Help message")).not.toBeInTheDocument(); + }); +}); From fc89c79e0182dea3cc0849778c454d83b1ec72b9 Mon Sep 17 00:00:00 2001 From: vladimir-cucu Date: Fri, 2 Feb 2024 15:38:12 +0200 Subject: [PATCH 3/4] refactor: Remove hardcoded rgb values in Output tests --- src/components/WebCLI/Output.test.tsx | 6 +++--- src/components/WebCLI/Output.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/WebCLI/Output.test.tsx b/src/components/WebCLI/Output.test.tsx index 86bf25e3a..2424ff4f8 100644 --- a/src/components/WebCLI/Output.test.tsx +++ b/src/components/WebCLI/Output.test.tsx @@ -2,7 +2,7 @@ import { screen } from "@testing-library/react"; import { renderComponent } from "testing/utils"; -import Output from "./Output"; +import Output, { ansiColors } from "./Output"; describe("Output", () => { it("should display content and not display help message", () => { @@ -42,10 +42,10 @@ describe("Output", () => { ); expect(screen.getByText("Regular output")).toBeInTheDocument(); expect(screen.getByText("Red output")).toHaveStyle({ - color: "rgb(205,49,49)", + color: `rgb(${ansiColors[31]})`, }); expect(screen.getByText("Blue output")).toHaveStyle({ - color: "rgb(26,114,200)", + color: `rgb(${ansiColors[34]})`, }); expect(screen.queryByText("Help message")).not.toBeInTheDocument(); }); diff --git a/src/components/WebCLI/Output.tsx b/src/components/WebCLI/Output.tsx index 77aac25e5..88109e192 100644 --- a/src/components/WebCLI/Output.tsx +++ b/src/components/WebCLI/Output.tsx @@ -16,7 +16,7 @@ type Props = { // Colors taken from the VSCode section of // https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit -const ansiColors = { +export const ansiColors = { 0: null, // reset 30: "0,0,0", // Black 31: "205,49,49", // Red From 8775719de64fbf6bfc79a691e3ab815d04256097 Mon Sep 17 00:00:00 2001 From: vladimir-cucu Date: Mon, 5 Feb 2024 11:24:46 +0200 Subject: [PATCH 4/4] refactor: Use ansi-to-html instead of custom code --- .eslintrc | 3 +- package.json | 1 + src/components/WebCLI/Output.test.tsx | 22 +++--- src/components/WebCLI/Output.tsx | 75 ++----------------- .../WebCLI/__snapshots__/WebCLI.test.tsx.snap | 2 +- src/panels/ActionsPanel/ActionsPanel.tsx | 1 - yarn.lock | 14 +++- 7 files changed, 35 insertions(+), 83 deletions(-) diff --git a/.eslintrc b/.eslintrc index 3c3672d9d..3e276307c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -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": { diff --git a/package.json b/package.json index 060c7823f..b27db8295 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/WebCLI/Output.test.tsx b/src/components/WebCLI/Output.test.tsx index 2424ff4f8..a3986fc07 100644 --- a/src/components/WebCLI/Output.test.tsx +++ b/src/components/WebCLI/Output.test.tsx @@ -2,7 +2,7 @@ import { screen } from "@testing-library/react"; import { renderComponent } from "testing/utils"; -import Output, { ansiColors } from "./Output"; +import Output from "./Output"; describe("Output", () => { it("should display content and not display help message", () => { @@ -31,22 +31,26 @@ describe("Output", () => { expect(screen.getByText("Help message")).toBeInTheDocument(); }); - it("should display the content with correct color", () => { + it("should display the content with correct formatting", () => { + const content = `\u001b[1;39mApp\n\u001b[0m\u001b[33munknown`; renderComponent( , ); - expect(screen.getByText("Regular output")).toBeInTheDocument(); - expect(screen.getByText("Red output")).toHaveStyle({ - color: `rgb(${ansiColors[31]})`, + 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("Blue output")).toHaveStyle({ - color: `rgb(${ansiColors[34]})`, + expect(screen.getByText("unknown")).toHaveStyle({ + color: "#A50", }); - expect(screen.queryByText("Help message")).not.toBeInTheDocument(); }); }); diff --git a/src/components/WebCLI/Output.tsx b/src/components/WebCLI/Output.tsx index 88109e192..db6387f3d 100644 --- a/src/components/WebCLI/Output.tsx +++ b/src/components/WebCLI/Output.tsx @@ -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", @@ -14,70 +15,6 @@ type Props = { setShouldShowHelp: (showHelp: boolean) => void; }; -// Colors taken from the VSCode section of -// https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit -export 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 = ""; - colors.forEach((color, index) => { - const ansiCode = color[0]; - const ansiCodeNumber = ansiCode.replace("[", "").replace("m", ""); - - if (color.index !== 0 && index === 0) { - // Add the content up until the first colour without wrapping it. - colorizedContent += content.substring(0, color.index); - } - 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 = `${part}`; - } - colorizedContent = colorizedContent + part; - }); - return colorizedContent; -}; - const DEFAULT_HEIGHT = 300; // 20 is a magic number, sometimes the browser stops firing the drag at // an inopportune time and the element isn't left completely closed. @@ -91,6 +28,8 @@ const WebCLIOutput = ({ showHelp, setShouldShowHelp, }: Props) => { + const convert = new Convert(); + const resizeDeltaY = useRef(0); const [height, setHeight] = useState(1); @@ -182,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 (
@@ -201,7 +136,7 @@ const WebCLIOutput = ({ ) : ( )} diff --git a/src/components/WebCLI/__snapshots__/WebCLI.test.tsx.snap b/src/components/WebCLI/__snapshots__/WebCLI.test.tsx.snap index e631aa09d..cbe9e64a1 100644 --- a/src/components/WebCLI/__snapshots__/WebCLI.test.tsx.snap +++ b/src/components/WebCLI/__snapshots__/WebCLI.test.tsx.snap @@ -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 " `; diff --git a/src/panels/ActionsPanel/ActionsPanel.tsx b/src/panels/ActionsPanel/ActionsPanel.tsx index 0c21ff3da..a4f78bd44 100644 --- a/src/panels/ActionsPanel/ActionsPanel.tsx +++ b/src/panels/ActionsPanel/ActionsPanel.tsx @@ -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) { diff --git a/yarn.lock b/yarn.lock index abddb0ed5..a98bd6c7c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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 @@ -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"