diff --git a/examples/templates/jfrog/docker/main.tf b/examples/templates/jfrog/docker/main.tf index f5bcb6728cf59..59bb728d91c4c 100644 --- a/examples/templates/jfrog/docker/main.tf +++ b/examples/templates/jfrog/docker/main.tf @@ -98,6 +98,9 @@ resource "coder_agent" "main" { index-url = https://${local.artifactory_username}:${artifactory_scoped_token.me.access_token}@${var.jfrog_host}/artifactory/api/pypi/${local.artifactory_repository_keys["python"]}/simple EOF + # project is a small Go project for jf audit to analyze. + cd ~ + git clone https://github.com/jfrog/jfrog-cli EOT # Set GOPROXY to use the Artifactory "go" repository. env = { @@ -109,6 +112,26 @@ resource "coder_agent" "main" { JFROG_IDE_ACCESS_TOKEN : "${artifactory_scoped_token.me.access_token}" JFROG_IDE_STORE_CONNECTION : "true" } + + metadata { + key = "cpu" + display_name = "CPU" + script = "coder stat cpu" + timeout = 1 + interval = 1 + } + + metadata { + key = "jfrog" + display_name = "terminal:JFrog Audit" + script = <<-EOT + export CI=true + cd jfrog-cli + script -qec 'jf audit 2>/dev/null' /dev/null + EOT + timeout = 300 + interval = 300 + } } resource "coder_app" "code-server" { diff --git a/site/package.json b/site/package.json index 8976fe4659399..093047d484db0 100644 --- a/site/package.json +++ b/site/package.json @@ -103,7 +103,9 @@ "xterm": "5.2.1", "xterm-addon-canvas": "0.4.0", "xterm-addon-fit": "0.7.0", + "xterm-addon-unicode11": "0.5.0", "xterm-addon-web-links": "0.8.0", + "xterm-addon-webgl": "0.15.0", "yup": "1.2.0" }, "devDependencies": { diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 00d76bb9c3efc..4cc98c7074168 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -225,9 +225,15 @@ dependencies: xterm-addon-fit: specifier: 0.7.0 version: 0.7.0(xterm@5.2.1) + xterm-addon-unicode11: + specifier: 0.5.0 + version: 0.5.0(xterm@5.2.1) xterm-addon-web-links: specifier: 0.8.0 version: 0.8.0(xterm@5.2.1) + xterm-addon-webgl: + specifier: 0.15.0 + version: 0.15.0(xterm@5.2.1) yup: specifier: 1.2.0 version: 1.2.0 @@ -13581,6 +13587,14 @@ packages: xterm: 5.2.1 dev: false + /xterm-addon-unicode11@0.5.0(xterm@5.2.1): + resolution: {integrity: sha512-Jm4/g4QiTxiKiTbYICQgC791ubhIZyoIwxAIgOW8z8HWFNY+lwk+dwaKEaEeGBfM48Vk8fklsUW9u/PlenYEBg==} + peerDependencies: + xterm: ^5.0.0 + dependencies: + xterm: 5.2.1 + dev: false + /xterm-addon-web-links@0.8.0(xterm@5.2.1): resolution: {integrity: sha512-J4tKngmIu20ytX9SEJjAP3UGksah7iALqBtfTwT9ZnmFHVplCumYQsUJfKuS+JwMhjsjH61YXfndenLNvjRrEw==} peerDependencies: @@ -13589,6 +13603,14 @@ packages: xterm: 5.2.1 dev: false + /xterm-addon-webgl@0.15.0(xterm@5.2.1): + resolution: {integrity: sha512-ZLcqogMFHr4g/YRhcCh3xE8tTklnyut/M+O/XhVsFBRB/YCvYhPdLQ5/AQk54V0wjWAQpa8CF3W8DVR9OqyMCg==} + peerDependencies: + xterm: ^5.0.0 + dependencies: + xterm: 5.2.1 + dev: false + /xterm@5.2.1: resolution: {integrity: sha512-cs5Y1fFevgcdoh2hJROMVIWwoBHD80P1fIP79gopLHJIE4kTzzblanoivxTiQ4+92YM9IxS36H1q0MxIJXQBcA==} dev: false diff --git a/site/src/components/Resources/AgentMetadata.stories.tsx b/site/src/components/Resources/AgentMetadata.stories.tsx index 9e5bedfd907e9..26f1d6f9cc8f5 100644 --- a/site/src/components/Resources/AgentMetadata.stories.tsx +++ b/site/src/components/Resources/AgentMetadata.stories.tsx @@ -105,3 +105,21 @@ Example.args = { }, ], } + +export const Terminal = Template.bind({}) +Terminal.args = { + metadata: [ + { + result: { + ...resultDefaults, + value: "\x1b[32mHello, World!\x1b[0m", + }, + description: { + ...descriptionDefaults, + display_name: "terminal:hello world", + key: "term", + script: "echo hello world", + }, + }, + ], +} diff --git a/site/src/components/Resources/AgentMetadata.tsx b/site/src/components/Resources/AgentMetadata.tsx index 0f5d45c624615..f65cc5ad72cb8 100644 --- a/site/src/components/Resources/AgentMetadata.tsx +++ b/site/src/components/Resources/AgentMetadata.tsx @@ -1,26 +1,142 @@ +import Box, { BoxProps } from "@mui/material/Box" +import Popover from "@mui/material/Popover" +import Skeleton from "@mui/material/Skeleton" +import Tooltip from "@mui/material/Tooltip" import makeStyles from "@mui/styles/makeStyles" import { watchAgentMetadata } from "api/api" -import { WorkspaceAgent, WorkspaceAgentMetadata } from "api/typesGenerated" +import { + WorkspaceAgent, + WorkspaceAgentMetadata, + WorkspaceAgentMetadataResult, +} from "api/typesGenerated" import { Stack } from "components/Stack/Stack" import dayjs from "dayjs" import { - createContext, FC, + createContext, useContext, useEffect, useRef, useState, } from "react" -import Skeleton from "@mui/material/Skeleton" +import { colors } from "theme/colors" import { MONOSPACE_FONT_FAMILY } from "theme/constants" import { combineClasses } from "utils/combineClasses" -import Tooltip from "@mui/material/Tooltip" -import Box, { BoxProps } from "@mui/material/Box" +import * as XTerm from "xterm" +import { FitAddon } from "xterm-addon-fit" +import { WebglAddon } from "xterm-addon-webgl" +import { Unicode11Addon } from "xterm-addon-unicode11" + +import "xterm/css/xterm.css" type ItemStatus = "stale" | "valid" | "loading" export const WatchAgentMetadataContext = createContext(watchAgentMetadata) +const MetadataTerminalPopover: FC<{ + id: string + result: WorkspaceAgentMetadataResult +}> = ({ id, result }) => { + const styles = useStyles() + + const viewTermRef = useRef(null) + const [open, setOpen] = useState(false) + + const [xtermRef, setXtermRef] = useState(null) + const [terminal, setTerminal] = useState(null) + const [fitAddon, setFitAddon] = useState(null) + + const writeTerminal = () => { + if (!terminal || !fitAddon) { + return + } + + // We write the clearCode with the new value to avoid a flash of blankness + // when the result value updates. + const clearCode = "\x1B[2J\x1B[H" + terminal.write(clearCode + result.value, () => { + fitAddon.fit() + }) + } + + // Create the terminal. + // Largely taken from TerminalPage. + useEffect(() => { + if (!xtermRef) { + return + } + const terminal = new XTerm.Terminal({ + allowTransparency: true, + allowProposedApi: true, + disableStdin: true, + fontFamily: MONOSPACE_FONT_FAMILY, + fontSize: 16, + theme: { + background: colors.gray[16], + }, + }) + terminal.loadAddon(new WebglAddon()) + terminal.loadAddon(new FitAddon()) + + // This addon fixes multi-width codepoint rendering such as + // 🟢. + terminal.loadAddon(new Unicode11Addon()) + terminal.unicode.activeVersion = "11" + + const fitAddon = new FitAddon() + setTerminal(terminal) + setFitAddon(fitAddon) + terminal.open(xtermRef) + writeTerminal() + + const resizeInterval = setInterval(() => { + window.dispatchEvent(new Event("resize")) + }, 100) + + return () => { + clearInterval(resizeInterval) + terminal.dispose() + } + }, [xtermRef, open]) + + useEffect(() => { + writeTerminal() + }, [xtermRef, open, result]) + + return ( + <> +
{ + setOpen(true) + }} + > + View Terminal +
+ + setOpen(false)} + anchorEl={viewTermRef.current} + anchorOrigin={{ + vertical: "bottom", + horizontal: "left", + }} + > +
{ + setXtermRef(el) + }} + data-testid="terminal" + /> + + + ) +} + const MetadataItem: FC<{ item: WorkspaceAgentMetadata }> = ({ item }) => { const styles = useStyles() @@ -31,6 +147,13 @@ const MetadataItem: FC<{ item: WorkspaceAgentMetadata }> = ({ item }) => { throw new Error("Metadata item description is undefined") } + const terminalPrefix = "terminal:" + const isTerminal = item.description.display_name.startsWith(terminalPrefix) + + const displayName = isTerminal + ? item.description.display_name.slice(terminalPrefix.length) + : item.description.display_name + const staleThreshold = Math.max( item.description.interval + item.description.timeout * 2, // In case there is intense backpressure, we give a little bit of slack. @@ -88,10 +211,15 @@ const MetadataItem: FC<{ item: WorkspaceAgentMetadata }> = ({ item }) => { return (
-
- {item.description.display_name} -
- {value} +
{displayName}
+ {isTerminal ? ( + + ) : ( + {value} + )}
) } @@ -105,6 +233,7 @@ export const AgentMetadataView: FC = ({ metadata }) => { if (metadata.length === 0) { return <> } + return (
@@ -228,6 +357,53 @@ const useStyles = makeStyles((theme) => ({ scrollPadding: theme.spacing(0, 4), }, + viewTerminal: { + fontFamily: MONOSPACE_FONT_FAMILY, + display: "inline-block", + textDecoration: "underline", + fontWeight: 600, + margin: 0, + fontSize: 14, + borderRadius: 4, + color: theme.palette.text.primary, + }, + + terminal: { + width: "80ch", + overflow: "auto", + backgroundColor: theme.palette.background.paper, + // flex: 1, + padding: theme.spacing(1), + // These styles attempt to mimic the VS Code scrollbar. + "& .xterm": { + padding: 4, + width: "100vw", + height: "40vh", + }, + "& .xterm-viewport": { + // This is required to force full-width on the terminal. + // Otherwise there's a small white bar to the right of the scrollbar. + width: "auto !important", + }, + "& .xterm-viewport::-webkit-scrollbar": { + width: "10px", + }, + "& .xterm-viewport::-webkit-scrollbar-track": { + backgroundColor: "inherit", + }, + "& .xterm-viewport::-webkit-scrollbar-thumb": { + minHeight: 20, + backgroundColor: "rgba(255, 255, 255, 0.18)", + }, + }, + + popover: { + padding: 0, + width: theme.spacing(38), + color: theme.palette.text.secondary, + marginTop: theme.spacing(0.5), + }, + metadata: { fontSize: 12, lineHeight: "normal", diff --git a/site/src/pages/TerminalPage/TerminalPage.tsx b/site/src/pages/TerminalPage/TerminalPage.tsx index a09f289da9805..81b828f6c21ab 100644 --- a/site/src/pages/TerminalPage/TerminalPage.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.tsx @@ -9,6 +9,7 @@ import { v4 as uuidv4 } from "uuid" import * as XTerm from "xterm" import { CanvasAddon } from "xterm-addon-canvas" import { FitAddon } from "xterm-addon-fit" +import { Unicode11Addon } from "xterm-addon-unicode11" import { WebLinksAddon } from "xterm-addon-web-links" import "xterm/css/xterm.css" import { MONOSPACE_FONT_FAMILY } from "../../theme/constants" @@ -176,6 +177,7 @@ const TerminalPage: FC = ({ renderer }) => { return } const terminal = new XTerm.Terminal({ + allowProposedApi: true, allowTransparency: true, disableStdin: false, fontFamily: MONOSPACE_FONT_FAMILY, @@ -191,6 +193,10 @@ const TerminalPage: FC = ({ renderer }) => { const fitAddon = new FitAddon() setFitAddon(fitAddon) terminal.loadAddon(fitAddon) + // This addon fixes multi-width codepoint rendering such as + // 🔵. + terminal.loadAddon(new Unicode11Addon()) + terminal.unicode.activeVersion = "11" terminal.loadAddon( new WebLinksAddon((_, uri) => { handleWebLink(uri)