From e721eecec04120357082cbffc016ac900339f586 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Fri, 14 Oct 2022 12:28:36 +0000 Subject: [PATCH 01/11] wip: Update resources table --- .../src/components/Resources/AgentLatency.tsx | 114 +++++++++ .../src/components/Resources/AgentVersion.tsx | 62 +++++ .../Resources/ResourceCard.stories.tsx | 76 ++++++ .../src/components/Resources/ResourceCard.tsx | 222 ++++++++++++++++++ site/src/components/Resources/Resources.tsx | 198 ++++++++++++++++ .../components/Resources/SensitiveValue.tsx | 58 +++++ site/src/components/Stack/Stack.tsx | 4 + .../Tooltips/HelpTooltip/HelpTooltip.tsx | 56 +++-- site/src/util/workspace.test.ts | 6 +- site/src/util/workspace.ts | 8 +- 10 files changed, 774 insertions(+), 30 deletions(-) create mode 100644 site/src/components/Resources/AgentLatency.tsx create mode 100644 site/src/components/Resources/AgentVersion.tsx create mode 100644 site/src/components/Resources/ResourceCard.stories.tsx create mode 100644 site/src/components/Resources/ResourceCard.tsx create mode 100644 site/src/components/Resources/SensitiveValue.tsx diff --git a/site/src/components/Resources/AgentLatency.tsx b/site/src/components/Resources/AgentLatency.tsx new file mode 100644 index 0000000000000..ab74f8271e0c6 --- /dev/null +++ b/site/src/components/Resources/AgentLatency.tsx @@ -0,0 +1,114 @@ +import { useRef, useState, FC } from "react" +import { makeStyles, Theme, useTheme } from "@material-ui/core/styles" +import { + HelpTooltipText, + HelpPopover, + HelpTooltipTitle, +} from "components/Tooltips/HelpTooltip" +import { Stack } from "components/Stack/Stack" +import { WorkspaceAgent, DERPRegion } from "api/typesGenerated" + +const getDisplayLatency = (theme: Theme, agent: WorkspaceAgent) => { + // Find the right latency to display + const latencyValues = Object.values(agent.latency ?? {}) + const latency = + latencyValues.find((derp) => derp.preferred) ?? + // Accessing an array index can return undefined as well + // for some reason TS does not handle that + (latencyValues[0] as DERPRegion | undefined) + + if (!latency) { + return undefined + } + + // Get the color + let color = theme.palette.success.light + if (latency.latency_ms >= 150 && latency.latency_ms < 300) { + color = theme.palette.warning.light + } else if (latency.latency_ms >= 300) { + color = theme.palette.error.light + } + + return { + ...latency, + color, + } +} + +export const AgentLatency: FC<{ agent: WorkspaceAgent }> = ({ agent }) => { + const theme: Theme = useTheme() + const anchorRef = useRef(null) + const [isOpen, setIsOpen] = useState(false) + const id = isOpen ? "latency-popover" : undefined + const latency = getDisplayLatency(theme, agent) + const styles = useStyles() + + if (!latency || !agent.latency) { + return null + } + + return ( + <> + setIsOpen(true)} + className={styles.trigger} + style={{ color: latency.color }} + > + {Math.round(Math.round(latency.latency_ms))}ms + + setIsOpen(true)} + onClose={() => setIsOpen(false)} + > + Latency + + Latency from relay servers, used when connections cannot connect + peer-to-peer. Star indicates the preferred relay. + + + + + {Object.keys(agent.latency).map((regionName) => { + if (!agent.latency) { + throw new Error("No latency found on agent") + } + + const region = agent.latency[regionName] + + return ( + + {regionName} + {Math.round(region.latency_ms)}ms + + ) + })} + + + + + ) +} + +const useStyles = makeStyles((theme) => ({ + trigger: { + cursor: "pointer", + }, + regions: { + marginTop: theme.spacing(2), + }, + preferred: { + color: theme.palette.text.primary, + }, +})) diff --git a/site/src/components/Resources/AgentVersion.tsx b/site/src/components/Resources/AgentVersion.tsx new file mode 100644 index 0000000000000..6926342f417ac --- /dev/null +++ b/site/src/components/Resources/AgentVersion.tsx @@ -0,0 +1,62 @@ +import { useRef, useState, FC } from "react" +import { makeStyles } from "@material-ui/core/styles" +import { + HelpTooltipText, + HelpPopover, + HelpTooltipTitle, +} from "components/Tooltips/HelpTooltip" +import { WorkspaceAgent } from "api/typesGenerated" +import { getDisplayVersionStatus } from "util/workspace" + +export const AgentVersion: FC<{ + agent: WorkspaceAgent + serverVersion: string +}> = ({ agent, serverVersion }) => { + const styles = useStyles() + const anchorRef = useRef(null) + const [isOpen, setIsOpen] = useState(false) + const id = isOpen ? "version-outdated-popover" : undefined + const { displayVersion, outdated } = getDisplayVersionStatus( + agent.version, + serverVersion, + ) + + if (!outdated) { + return {displayVersion} + } + + return ( + <> + setIsOpen(true)} + className={styles.trigger} + > + {displayVersion} + + setIsOpen(true)} + onClose={() => setIsOpen(false)} + > + Agent Outdated + + This agent is an older version than the Coder server. This can happen + after you update Coder with running workspaces. To fix this, you can + stop and start the workspace. + + + + ) +} + +const useStyles = makeStyles((theme) => ({ + trigger: { + cursor: "pointer", + color: theme.palette.warning.light, + }, +})) diff --git a/site/src/components/Resources/ResourceCard.stories.tsx b/site/src/components/Resources/ResourceCard.stories.tsx new file mode 100644 index 0000000000000..f4b44c0cfea70 --- /dev/null +++ b/site/src/components/Resources/ResourceCard.stories.tsx @@ -0,0 +1,76 @@ +import { Story } from "@storybook/react" +import { + MockWorkspace, + MockWorkspaceAgent, + MockWorkspaceResource, +} from "testHelpers/entities" +import { ResourceCard, ResourceCardProps } from "./ResourceCard" + +export default { + title: "components/ResourceCard", + component: ResourceCard, +} + +const Template: Story = (args) => + +export const Example = Template.bind({}) +Example.args = { + resource: MockWorkspaceResource, + workspace: MockWorkspace, + applicationsHost: "https://dev.coder.com", + hideSSHButton: false, + showApps: true, + serverVersion: MockWorkspaceAgent.version, +} + +export const NotShowingApps = Template.bind({}) +NotShowingApps.args = { + ...Example.args, + showApps: false, +} + +export const HideSSHButton = Template.bind({}) +HideSSHButton.args = { + ...Example.args, + hideSSHButton: true, +} + +export const BunchOfMetadata = Template.bind({}) +BunchOfMetadata.args = { + ...Example.args, + resource: { + ...MockWorkspaceResource, + metadata: [ + { key: "type", value: "kubernetes_pod", sensitive: false }, + { + key: "CPU(limits, requests)", + value: "2 cores, 500m", + sensitive: false, + }, + { key: "container image pull policy", value: "Always", sensitive: false }, + { key: "Disk", value: "10GiB", sensitive: false }, + { + key: "image", + value: "docker.io/markmilligan/pycharm-community:latest", + sensitive: false, + }, + { key: "kubernetes namespace", value: "oss", sensitive: false }, + { + key: "memory(limits, requests)", + value: "4GB, 500mi", + sensitive: false, + }, + { + key: "security context - container", + value: "run_as_user 1000", + sensitive: false, + }, + { + key: "security context - pod", + value: "run_as_user 1000 fs_group 1000", + sensitive: false, + }, + { key: "volume", value: "/home/coder", sensitive: false }, + ], + }, +} diff --git a/site/src/components/Resources/ResourceCard.tsx b/site/src/components/Resources/ResourceCard.tsx new file mode 100644 index 0000000000000..92001b2db34d9 --- /dev/null +++ b/site/src/components/Resources/ResourceCard.tsx @@ -0,0 +1,222 @@ +import { makeStyles } from "@material-ui/core/styles" +import { Skeleton } from "@material-ui/lab" +import { PortForwardButton } from "components/PortForwardButton/PortForwardButton" +import { FC } from "react" +import { Workspace, WorkspaceResource } from "../../api/typesGenerated" +import { AppLink } from "../AppLink/AppLink" +import { SSHButton } from "../SSHButton/SSHButton" +import { Stack } from "../Stack/Stack" +import { TerminalLink } from "../TerminalLink/TerminalLink" +import { ResourceAvatar } from "./ResourceAvatar" +import { SensitiveValue } from "./SensitiveValue" +import { AgentLatency } from "./AgentLatency" +import { AgentVersion } from "./AgentVersion" + +export interface ResourceCardProps { + resource: WorkspaceResource + workspace: Workspace + applicationsHost: string | undefined + showApps: boolean + hideSSHButton: boolean + serverVersion: string +} + +export const ResourceCard: FC = ({ + resource, + workspace, + applicationsHost, + showApps, + hideSSHButton, + serverVersion, +}) => { + const styles = useStyles() + + const metadataToDisplay = + resource.metadata?.filter((data) => data.key !== "type") ?? [] + + return ( +
+ +
+ +
+
+
{resource.type}
+
{resource.name}
+
+
+ + + {metadataToDisplay.map((data) => ( +
+ {data.key}: + {data.sensitive ? ( + + ) : ( + {data.value} + )} +
+ ))} +
+ +
+ {resource.agents?.map((agent) => { + return ( + + +
+
+
{agent.name}
+ + + {agent.operating_system} + + + + +
+ + + + {showApps && agent.status === "connected" && ( + <> + {applicationsHost !== undefined && ( + + )} + {!hideSSHButton && ( + + )} + + {agent.apps.map((app) => ( + + ))} + + )} + {showApps && agent.status === "connecting" && ( + <> + + + + )} + + + ) + })} +
+
+ ) +} + +const useStyles = makeStyles((theme) => ({ + resourceCard: { + background: theme.palette.background.paper, + borderRadius: theme.shape.borderRadius, + border: `1px solid ${theme.palette.divider}`, + }, + + resourceCardHeader: { + padding: theme.spacing(3, 4), + borderBottom: `1px solid ${theme.palette.divider}`, + }, + + resourceMetadata: { + padding: theme.spacing(2, 4), + borderBottom: `1px solid ${theme.palette.divider}`, + gap: theme.spacing(0.5, 2), + }, + + resourceHeader: { + fontSize: 16, + }, + + resourceHeaderLabel: { + fontSize: 12, + color: theme.palette.text.secondary, + }, + + resourceData: { + fontSize: 12, + flexShrink: 0, + }, + + resourceDataLabel: { + fontSize: 12, + color: theme.palette.text.secondary, + marginRight: theme.spacing(0.75), + }, + + agentRow: { + padding: theme.spacing(3, 4), + backgroundColor: theme.palette.background.paperLight, + fontSize: 16, + + "&:not(:last-child)": { + borderBottom: `1px solid ${theme.palette.divider}`, + }, + }, + + agentStatus: { + width: theme.spacing(1), + height: theme.spacing(1), + backgroundColor: theme.palette.success.light, + borderRadius: "100%", + }, + + agentName: { + fontWeight: 600, + }, + + agentOS: { + textTransform: "capitalize", + }, + + agentData: { + fontSize: 14, + color: theme.palette.text.secondary, + marginTop: theme.spacing(0.5), + }, +})) diff --git a/site/src/components/Resources/Resources.tsx b/site/src/components/Resources/Resources.tsx index f8833fdc11a8c..85d821069e549 100644 --- a/site/src/components/Resources/Resources.tsx +++ b/site/src/components/Resources/Resources.tsx @@ -18,7 +18,9 @@ import { FC, useState } from "react" import { getDisplayAgentStatus, getDisplayVersionStatus } from "util/workspace" import { BuildInfoResponse, + DERPRegion, Workspace, + WorkspaceAgent, WorkspaceResource, } from "../../api/typesGenerated" import { AppLink } from "../AppLink/AppLink" @@ -32,6 +34,9 @@ import { ResourcesHelpTooltip } from "../Tooltips/ResourcesHelpTooltip" import { ResourceAgentLatency } from "./ResourceAgentLatency" import { ResourceAvatarData } from "./ResourceAvatarData" import { AlertBanner } from "components/AlertBanner/AlertBanner" +import { ResourceAvatar } from "./ResourceAvatar" +import { SensitiveValue } from "./SensitiveValue" +import { AgentLatency } from "./AgentLatency" const Language = { resources: "Resources", @@ -72,8 +77,154 @@ export const Resources: FC> = ({ : resources.filter((resource) => !resource.hide) const hasHideResources = resources.some((r) => r.hide) + const getDisplayLatency = (agent: WorkspaceAgent) => { + // Find the right latency to display + const latencyValues = Object.values(agent.latency ?? {}) + const latency = + latencyValues.find((derp) => derp.preferred) ?? + // Accessing an array index can return undefined as well + // for some reason TS does not handle that + (latencyValues[0] as DERPRegion | undefined) + + if (!latency) { + return undefined + } + + // Get the color + let color = theme.palette.success.light + if (latency.latency_ms >= 150 && latency.latency_ms < 300) { + color = theme.palette.warning.light + } else if (latency.latency_ms >= 300) { + color = theme.palette.error.light + } + + return { + ...latency, + color, + } + } + return ( + {resources.map((resource) => { + // Type is already displayed on top of the resource name + const metadataToDisplay = + resource.metadata?.filter((data) => data.key !== "type") ?? [] + + return ( +
+ +
+ +
+ +
+
+ {resource.type} +
+
{resource.name}
+
+ + {metadataToDisplay.map((data) => ( +
+
{data.key}
+ {data.sensitive ? ( + + ) : ( +
{data.value}
+ )} +
+ ))} +
+
+ +
+ {resource.agents?.map((agent) => { + const latency = getDisplayLatency(agent) + + return ( + + +
+
+
{agent.name}
+ + {agent.operating_system} + {agent.version} + {latency && } + +
+ + + + {canUpdateWorkspace && agent.status === "connected" && ( + <> + {applicationsHost !== undefined && ( + + )} + {!hideSSHButton && ( + + )} + + {agent.apps.map((app) => ( + + ))} + + )} + {canUpdateWorkspace && agent.status === "connecting" && ( + <> + + + + )} + + + ) + })} +
+
+ ) + })} +
{getResourcesError ? ( @@ -336,4 +487,51 @@ const useStyles = makeStyles((theme) => ({ width: "100%", maxWidth: 260, }, + + resourceCard: { + background: theme.palette.background.paper, + borderRadius: theme.shape.borderRadius, + border: `1px solid ${theme.palette.divider}`, + }, + + resourceCardHeader: { + padding: theme.spacing(3, 4), + borderBottom: `1px solid ${theme.palette.divider}`, + }, + + resourceData: { + fontSize: 16, + }, + + resourceDataLabel: { + fontSize: 12, + color: theme.palette.text.secondary, + }, + + agentRow: { + padding: theme.spacing(3, 4), + backgroundColor: theme.palette.background.paperLight, + fontSize: 16, + + "&:not(:last-child)": { + borderBottom: `1px solid ${theme.palette.divider}`, + }, + }, + + agentStatus: { + width: theme.spacing(1), + height: theme.spacing(1), + backgroundColor: theme.palette.success.light, + borderRadius: "100%", + }, + + agentName: { + fontWeight: 600, + }, + + agentData: { + fontSize: 14, + color: theme.palette.text.secondary, + marginTop: theme.spacing(0.5), + }, })) diff --git a/site/src/components/Resources/SensitiveValue.tsx b/site/src/components/Resources/SensitiveValue.tsx new file mode 100644 index 0000000000000..d069cd4db65bb --- /dev/null +++ b/site/src/components/Resources/SensitiveValue.tsx @@ -0,0 +1,58 @@ +import IconButton from "@material-ui/core/IconButton" +import { makeStyles } from "@material-ui/core/styles" +import Tooltip from "@material-ui/core/Tooltip" +import VisibilityOffOutlined from "@material-ui/icons/VisibilityOffOutlined" +import VisibilityOutlined from "@material-ui/icons/VisibilityOutlined" +import { useState } from "react" + +const Language = { + showLabel: "Show value", + hideLabel: "Hide value", +} + +export const SensitiveValue: React.FC<{ value: string }> = ({ value }) => { + const [shouldDisplay, setShouldDisplay] = useState(false) + const styles = useStyles() + const displayValue = shouldDisplay ? value : "••••••••" + const buttonLabel = shouldDisplay ? Language.hideLabel : Language.showLabel + const icon = shouldDisplay ? ( + + ) : ( + + ) + + return ( +
+ {displayValue} + + { + setShouldDisplay((value) => !value) + }} + size="small" + aria-label={buttonLabel} + > + {icon} + + +
+ ) +} + +const useStyles = makeStyles((theme) => ({ + sensitiveValue: { + display: "flex", + alignItems: "center", + }, + + button: { + marginLeft: theme.spacing(0.5), + color: "inherit", + + "& .MuiSvgIcon-root": { + width: 16, + height: 16, + }, + }, +})) diff --git a/site/src/components/Stack/Stack.tsx b/site/src/components/Stack/Stack.tsx index d12f4e5821a56..1f181816c683d 100644 --- a/site/src/components/Stack/Stack.tsx +++ b/site/src/components/Stack/Stack.tsx @@ -12,6 +12,7 @@ export type StackProps = { spacing?: number alignItems?: CSSProperties["alignItems"] justifyContent?: CSSProperties["justifyContent"] + wrap?: CSSProperties["flexWrap"] } & React.HTMLProps type StyleProps = Omit @@ -23,6 +24,7 @@ const useStyles = makeStyles((theme) => ({ gap: ({ spacing }: StyleProps) => spacing && theme.spacing(spacing), alignItems: ({ alignItems }: StyleProps) => alignItems, justifyContent: ({ justifyContent }: StyleProps) => justifyContent, + flexWrap: ({ wrap }: StyleProps) => wrap, [theme.breakpoints.down("sm")]: { width: "100%", @@ -37,6 +39,7 @@ export const Stack: FC = ({ spacing = 2, alignItems, justifyContent, + wrap, ...divProps }) => { const styles = useStyles({ @@ -44,6 +47,7 @@ export const Stack: FC = ({ direction, alignItems, justifyContent, + wrap, }) return ( diff --git a/site/src/components/Tooltips/HelpTooltip/HelpTooltip.tsx b/site/src/components/Tooltips/HelpTooltip/HelpTooltip.tsx index dc9a555357db5..5102168ab9586 100644 --- a/site/src/components/Tooltips/HelpTooltip/HelpTooltip.tsx +++ b/site/src/components/Tooltips/HelpTooltip/HelpTooltip.tsx @@ -1,5 +1,5 @@ import Link from "@material-ui/core/Link" -import Popover from "@material-ui/core/Popover" +import Popover, { PopoverProps } from "@material-ui/core/Popover" import { makeStyles } from "@material-ui/core/styles" import HelpIcon from "@material-ui/icons/HelpOutline" import OpenInNewIcon from "@material-ui/icons/OpenInNew" @@ -35,6 +35,35 @@ const useHelpTooltip = () => { return helpTooltipContext } +export const HelpPopover: React.FC< + PopoverProps & { onOpen: () => void; onClose: () => void } +> = ({ onOpen, onClose, children, ...props }) => { + const styles = useStyles({ size: "small" }) + + return ( + + {children} + + ) +} + export const HelpTooltip: React.FC< React.PropsWithChildren > = ({ children, open, size = "medium" }) => { @@ -64,34 +93,17 @@ export const HelpTooltip: React.FC< > - { - setIsOpen(true) - }, - onMouseLeave: () => { - setIsOpen(false) - }, - }} + onOpen={() => setIsOpen(true)} + onClose={() => setIsOpen(false)} > {children} - + ) } diff --git a/site/src/util/workspace.test.ts b/site/src/util/workspace.test.ts index 6650a9090cd97..d99abcf633596 100644 --- a/site/src/util/workspace.test.ts +++ b/site/src/util/workspace.test.ts @@ -101,11 +101,11 @@ describe("util > workspace", () => { describe("getDisplayVersionStatus", () => { it.each<[string, string, string, boolean]>([ - ["", "", "(unknown)", false], - ["", "v1.2.3", "(unknown)", false], + ["", "", "unknown", false], + ["", "v1.2.3", "unknown", false], ["v1.2.3", "", "v1.2.3", false], ["v1.2.3", "v1.2.3", "v1.2.3", false], - ["v1.2.3", "v1.2.4", "v1.2.3 (outdated)", true], + ["v1.2.3", "v1.2.4", "v1.2.3", true], ["v1.2.4", "v1.2.3", "v1.2.4", false], ["foo", "bar", "foo", false], ])( diff --git a/site/src/util/workspace.ts b/site/src/util/workspace.ts index 49574e4abe5ca..1a50c5782cec8 100644 --- a/site/src/util/workspace.ts +++ b/site/src/util/workspace.ts @@ -20,8 +20,7 @@ export const DisplayWorkspaceBuildStatusLanguage = { } export const DisplayAgentVersionLanguage = { - unknown: "unknown", - outdated: "outdated", + unknown: "Unknown", } export const getDisplayWorkspaceBuildStatus = ( @@ -149,13 +148,12 @@ export const getDisplayVersionStatus = ( ): { displayVersion: string; outdated: boolean } => { if (!semver.valid(serverVersion) || !semver.valid(agentVersion)) { return { - displayVersion: - `${agentVersion}` || `(${DisplayAgentVersionLanguage.unknown})`, + displayVersion: agentVersion || DisplayAgentVersionLanguage.unknown, outdated: false, } } else if (semver.lt(agentVersion, serverVersion)) { return { - displayVersion: `${agentVersion} (${DisplayAgentVersionLanguage.outdated})`, + displayVersion: agentVersion, outdated: true, } } else { From d71f37b1a221e552e013dd46dd07fc1913b45ea0 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Mon, 17 Oct 2022 17:02:20 +0000 Subject: [PATCH 02/11] Add collapsable metadata --- site/src/components/CopyButton/CopyButton.tsx | 36 +---- .../Resources/ResourceCard.stories.tsx | 5 + .../src/components/Resources/ResourceCard.tsx | 132 ++++++++++++------ site/src/components/Resources/Resources.tsx | 1 + .../components/Resources/SensitiveValue.tsx | 12 +- site/src/hooks/useClipboard.ts | 44 ++++++ 6 files changed, 151 insertions(+), 79 deletions(-) create mode 100644 site/src/hooks/useClipboard.ts diff --git a/site/src/components/CopyButton/CopyButton.tsx b/site/src/components/CopyButton/CopyButton.tsx index 107ccc862d05d..ac8bc3cb87d47 100644 --- a/site/src/components/CopyButton/CopyButton.tsx +++ b/site/src/components/CopyButton/CopyButton.tsx @@ -2,7 +2,7 @@ import IconButton from "@material-ui/core/Button" import { makeStyles } from "@material-ui/core/styles" import Tooltip from "@material-ui/core/Tooltip" import Check from "@material-ui/icons/Check" -import React, { useState } from "react" +import { useClipboard } from "hooks/useClipboard" import { combineClasses } from "../../util/combineClasses" import { FileCopyIcon } from "../Icons/FileCopyIcon" @@ -30,39 +30,7 @@ export const CopyButton: React.FC> = ({ tooltipTitle = Language.tooltipTitle, }) => { const styles = useStyles() - const [isCopied, setIsCopied] = useState(false) - - const copyToClipboard = async (): Promise => { - try { - await window.navigator.clipboard.writeText(text) - setIsCopied(true) - window.setTimeout(() => { - setIsCopied(false) - }, 1000) - } catch (err) { - const input = document.createElement("input") - input.value = text - document.body.appendChild(input) - input.focus() - input.select() - const result = document.execCommand("copy") - document.body.removeChild(input) - if (result) { - setIsCopied(true) - window.setTimeout(() => { - setIsCopied(false) - }, 1000) - } else { - const wrappedErr = new Error( - "copyToClipboard: failed to copy text to clipboard", - ) - if (err instanceof Error) { - wrappedErr.stack = err.stack - } - console.error(wrappedErr) - } - } - } + const { isCopied, copy: copyToClipboard } = useClipboard(text) return ( diff --git a/site/src/components/Resources/ResourceCard.stories.tsx b/site/src/components/Resources/ResourceCard.stories.tsx index f4b44c0cfea70..11684a3ec4aab 100644 --- a/site/src/components/Resources/ResourceCard.stories.tsx +++ b/site/src/components/Resources/ResourceCard.stories.tsx @@ -71,6 +71,11 @@ BunchOfMetadata.args = { sensitive: false, }, { key: "volume", value: "/home/coder", sensitive: false }, + { + key: "secret", + value: "3XqfNW0b1bvsGsqud8O6OW6VabH3fwzI", + sensitive: true, + }, ], }, } diff --git a/site/src/components/Resources/ResourceCard.tsx b/site/src/components/Resources/ResourceCard.tsx index 92001b2db34d9..f4dc15133cb1e 100644 --- a/site/src/components/Resources/ResourceCard.tsx +++ b/site/src/components/Resources/ResourceCard.tsx @@ -1,7 +1,7 @@ import { makeStyles } from "@material-ui/core/styles" import { Skeleton } from "@material-ui/lab" import { PortForwardButton } from "components/PortForwardButton/PortForwardButton" -import { FC } from "react" +import { FC, useState } from "react" import { Workspace, WorkspaceResource } from "../../api/typesGenerated" import { AppLink } from "../AppLink/AppLink" import { SSHButton } from "../SSHButton/SSHButton" @@ -11,6 +11,13 @@ import { ResourceAvatar } from "./ResourceAvatar" import { SensitiveValue } from "./SensitiveValue" import { AgentLatency } from "./AgentLatency" import { AgentVersion } from "./AgentVersion" +import { + OpenDropdown, + CloseDropdown, +} from "components/DropdownArrows/DropdownArrows" +import IconButton from "@material-ui/core/IconButton" +import Tooltip from "@material-ui/core/Tooltip" +import { Maybe } from "components/Conditionals/Maybe" export interface ResourceCardProps { resource: WorkspaceResource @@ -29,43 +36,76 @@ export const ResourceCard: FC = ({ hideSSHButton, serverVersion, }) => { + const [shouldDisplayAllMetadata, setShouldDisplayAllMetadata] = + useState(false) const styles = useStyles() - const metadataToDisplay = + // Type is already displayed in the header resource.metadata?.filter((data) => data.key !== "type") ?? [] + const visibleMetadata = shouldDisplayAllMetadata + ? metadataToDisplay + : metadataToDisplay.slice(0, 4) return (
-
- -
-
-
{resource.type}
-
{resource.name}
-
-
+ +
+ +
+
+
{resource.type}
+
{resource.name}
+
+
- - {metadataToDisplay.map((data) => ( -
- {data.key}: - {data.sensitive ? ( - - ) : ( - {data.value} - )} + +
+ {visibleMetadata.map((meta) => { + return ( +
+
{meta.key}
+
+ {meta.sensitive ? ( + + ) : ( + meta.value + )} +
+
+ ) + })}
- ))} + + 4}> + + { + setShouldDisplayAllMetadata((value) => !value) + }} + > + {shouldDisplayAllMetadata ? ( + + ) : ( + + )} + + + +
@@ -132,14 +172,15 @@ export const ResourceCard: FC = ({ workspaceName={workspace.name} agentName={agent.name} health={app.health} + appSharingLevel={app.sharing_level} /> ))} )} {showApps && agent.status === "connecting" && ( <> - - + + )} @@ -158,35 +199,40 @@ const useStyles = makeStyles((theme) => ({ border: `1px solid ${theme.palette.divider}`, }, + resourceCardProfile: { + flexShrink: 0, + width: "fit-content", + }, + resourceCardHeader: { padding: theme.spacing(3, 4), borderBottom: `1px solid ${theme.palette.divider}`, }, - resourceMetadata: { - padding: theme.spacing(2, 4), - borderBottom: `1px solid ${theme.palette.divider}`, - gap: theme.spacing(0.5, 2), + metadataHeader: { + display: "grid", + gridTemplateColumns: "repeat(4, minmax(0, 1fr))", + gap: theme.spacing(5), + rowGap: theme.spacing(3), }, - resourceHeader: { + metadata: { fontSize: 16, }, - resourceHeaderLabel: { + metadataLabel: { fontSize: 12, color: theme.palette.text.secondary, + textOverflow: "ellipsis", + overflow: "hidden", + whiteSpace: "nowrap", }, - resourceData: { - fontSize: 12, - flexShrink: 0, - }, - - resourceDataLabel: { - fontSize: 12, - color: theme.palette.text.secondary, - marginRight: theme.spacing(0.75), + metadataValue: { + textOverflow: "ellipsis", + overflow: "hidden", + whiteSpace: "nowrap", + userSelect: "all", }, agentRow: { diff --git a/site/src/components/Resources/Resources.tsx b/site/src/components/Resources/Resources.tsx index e0b421f089b57..46322efd2973e 100644 --- a/site/src/components/Resources/Resources.tsx +++ b/site/src/components/Resources/Resources.tsx @@ -206,6 +206,7 @@ export const Resources: FC> = ({ workspaceName={workspace.name} agentName={agent.name} health={app.health} + appSharingLevel={app.sharing_level} /> ))} diff --git a/site/src/components/Resources/SensitiveValue.tsx b/site/src/components/Resources/SensitiveValue.tsx index d069cd4db65bb..1595ed6b061b1 100644 --- a/site/src/components/Resources/SensitiveValue.tsx +++ b/site/src/components/Resources/SensitiveValue.tsx @@ -23,7 +23,7 @@ export const SensitiveValue: React.FC<{ value: string }> = ({ value }) => { return (
- {displayValue} +
{displayValue}
= ({ value }) => { } const useStyles = makeStyles((theme) => ({ + value: { + // 22px is the button width + width: "calc(100% - 22px)", + overflow: "hidden", + whiteSpace: "nowrap", + textOverflow: "ellipsis", + }, + sensitiveValue: { display: "flex", alignItems: "center", + gap: theme.spacing(0.5), }, button: { - marginLeft: theme.spacing(0.5), color: "inherit", "& .MuiSvgIcon-root": { diff --git a/site/src/hooks/useClipboard.ts b/site/src/hooks/useClipboard.ts new file mode 100644 index 0000000000000..3737ac64ecef5 --- /dev/null +++ b/site/src/hooks/useClipboard.ts @@ -0,0 +1,44 @@ +import { useState } from "react" + +export const useClipboard = ( + text: string, +): { isCopied: boolean; copy: () => Promise } => { + const [isCopied, setIsCopied] = useState(false) + + const copy = async (): Promise => { + try { + await window.navigator.clipboard.writeText(text) + setIsCopied(true) + window.setTimeout(() => { + setIsCopied(false) + }, 1000) + } catch (err) { + const input = document.createElement("input") + input.value = text + document.body.appendChild(input) + input.focus() + input.select() + const result = document.execCommand("copy") + document.body.removeChild(input) + if (result) { + setIsCopied(true) + window.setTimeout(() => { + setIsCopied(false) + }, 1000) + } else { + const wrappedErr = new Error( + "copyToClipboard: failed to copy text to clipboard", + ) + if (err instanceof Error) { + wrappedErr.stack = err.stack + } + console.error(wrappedErr) + } + } + } + + return { + isCopied, + copy, + } +} From 4496ec2bf822e77c3c1b1fae497ca12cebebffb6 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Mon, 17 Oct 2022 17:28:32 +0000 Subject: [PATCH 03/11] Update resource status --- .../src/components/Resources/ResourceCard.tsx | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/site/src/components/Resources/ResourceCard.tsx b/site/src/components/Resources/ResourceCard.tsx index f4dc15133cb1e..928c0974e9566 100644 --- a/site/src/components/Resources/ResourceCard.tsx +++ b/site/src/components/Resources/ResourceCard.tsx @@ -1,8 +1,13 @@ -import { makeStyles } from "@material-ui/core/styles" +import { makeStyles, Theme, useTheme } from "@material-ui/core/styles" import { Skeleton } from "@material-ui/lab" import { PortForwardButton } from "components/PortForwardButton/PortForwardButton" import { FC, useState } from "react" -import { Workspace, WorkspaceResource } from "../../api/typesGenerated" +import { + Workspace, + WorkspaceAgent, + WorkspaceAgentStatus, + WorkspaceResource, +} from "../../api/typesGenerated" import { AppLink } from "../AppLink/AppLink" import { SSHButton } from "../SSHButton/SSHButton" import { Stack } from "../Stack/Stack" @@ -19,6 +24,23 @@ import IconButton from "@material-ui/core/IconButton" import Tooltip from "@material-ui/core/Tooltip" import { Maybe } from "components/Conditionals/Maybe" +const getAgentStatusColor = (theme: Theme, agent: WorkspaceAgent) => { + switch (agent.status) { + case "connected": + return theme.palette.success.light + case "connecting": + return theme.palette.info.light + case "disconnected": + return theme.palette.text.secondary + } +} + +const agentStatusLabels: Record = { + connected: "Connected", + connecting: "Connecting...", + disconnected: "Disconnected", +} + export interface ResourceCardProps { resource: WorkspaceResource workspace: Workspace @@ -36,6 +58,7 @@ export const ResourceCard: FC = ({ hideSSHButton, serverVersion, }) => { + const theme = useTheme() const [shouldDisplayAllMetadata, setShouldDisplayAllMetadata] = useState(false) const styles = useStyles() @@ -110,6 +133,9 @@ export const ResourceCard: FC = ({
{resource.agents?.map((agent) => { + const statusColor = getAgentStatusColor(theme, agent) + const statusLabel = agentStatusLabels[agent.status] + return ( = ({ className={styles.agentRow} > -
+ +
+
{agent.name}
({ agentStatus: { width: theme.spacing(1), height: theme.spacing(1), - backgroundColor: theme.palette.success.light, borderRadius: "100%", }, From c78e4194f7429e24b1bd71fdc032d5e5892daac6 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Mon, 17 Oct 2022 18:40:09 +0000 Subject: [PATCH 04/11] Refactor clickable experience --- .../CopyableValue/CopyableValue.tsx | 39 +++++++++++++++++++ .../src/components/Resources/AgentVersion.tsx | 5 +-- .../src/components/Resources/ResourceCard.tsx | 6 ++- .../components/Resources/SensitiveValue.tsx | 5 ++- site/src/hooks/useClickable.ts | 21 ++++++++++ 5 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 site/src/components/CopyableValue/CopyableValue.tsx create mode 100644 site/src/hooks/useClickable.ts diff --git a/site/src/components/CopyableValue/CopyableValue.tsx b/site/src/components/CopyableValue/CopyableValue.tsx new file mode 100644 index 0000000000000..15bd72d84ccbe --- /dev/null +++ b/site/src/components/CopyableValue/CopyableValue.tsx @@ -0,0 +1,39 @@ +import { makeStyles } from "@material-ui/core/styles" +import Tooltip from "@material-ui/core/Tooltip" +import { useClickable } from "hooks/useClickable" +import { useClipboard } from "hooks/useClipboard" +import React, { HTMLProps } from "react" +import { combineClasses } from "util/combineClasses" + +interface CopyableValueProps extends HTMLProps { + value: string +} + +export const CopyableValue: React.FC = ({ + value, + className, + ...props +}) => { + const { isCopied, copy } = useClipboard(value) + const clickableProps = useClickable(copy) + const styles = useStyles() + + return ( + + + + ) +} + +const useStyles = makeStyles(() => ({ + value: { + cursor: "pointer", + }, +})) diff --git a/site/src/components/Resources/AgentVersion.tsx b/site/src/components/Resources/AgentVersion.tsx index 6926342f417ac..aab5fa73e75d4 100644 --- a/site/src/components/Resources/AgentVersion.tsx +++ b/site/src/components/Resources/AgentVersion.tsx @@ -34,7 +34,7 @@ export const AgentVersion: FC<{ onMouseEnter={() => setIsOpen(true)} className={styles.trigger} > - {displayVersion} + Agent Outdated ({ +const useStyles = makeStyles(() => ({ trigger: { cursor: "pointer", - color: theme.palette.warning.light, }, })) diff --git a/site/src/components/Resources/ResourceCard.tsx b/site/src/components/Resources/ResourceCard.tsx index 928c0974e9566..bb45169f9d6ff 100644 --- a/site/src/components/Resources/ResourceCard.tsx +++ b/site/src/components/Resources/ResourceCard.tsx @@ -23,6 +23,7 @@ import { import IconButton from "@material-ui/core/IconButton" import Tooltip from "@material-ui/core/Tooltip" import { Maybe } from "components/Conditionals/Maybe" +import { CopyableValue } from "components/CopyableValue/CopyableValue" const getAgentStatusColor = (theme: Theme, agent: WorkspaceAgent) => { switch (agent.status) { @@ -101,7 +102,9 @@ export const ResourceCard: FC = ({ {meta.sensitive ? ( ) : ( - meta.value + + {meta.value} + )}
@@ -264,7 +267,6 @@ const useStyles = makeStyles((theme) => ({ textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap", - userSelect: "all", }, agentRow: { diff --git a/site/src/components/Resources/SensitiveValue.tsx b/site/src/components/Resources/SensitiveValue.tsx index 1595ed6b061b1..b8e7f42d761b1 100644 --- a/site/src/components/Resources/SensitiveValue.tsx +++ b/site/src/components/Resources/SensitiveValue.tsx @@ -3,6 +3,7 @@ import { makeStyles } from "@material-ui/core/styles" import Tooltip from "@material-ui/core/Tooltip" import VisibilityOffOutlined from "@material-ui/icons/VisibilityOffOutlined" import VisibilityOutlined from "@material-ui/icons/VisibilityOutlined" +import { CopyableValue } from "components/CopyableValue/CopyableValue" import { useState } from "react" const Language = { @@ -23,7 +24,9 @@ export const SensitiveValue: React.FC<{ value: string }> = ({ value }) => { return (
-
{displayValue}
+ + {displayValue} + void + onKeyDown: (event: KeyboardEvent) => void +} + +export const useClickable = (onClick: () => void): UseClickableResult => { + return { + tabIndex: 0, + role: "button", + onClick, + onKeyDown: (event: KeyboardEvent) => { + if (event.key === "Enter") { + onClick() + } + }, + } +} From a1b1f7fff9146a860cc7ee74cc87699194351f8f Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Mon, 17 Oct 2022 19:00:16 +0000 Subject: [PATCH 05/11] Improve agent status --- site/src/components/Resources/AgentStatus.tsx | 93 +++++++++++++++++++ .../src/components/Resources/ResourceCard.tsx | 45 +-------- 2 files changed, 97 insertions(+), 41 deletions(-) create mode 100644 site/src/components/Resources/AgentStatus.tsx diff --git a/site/src/components/Resources/AgentStatus.tsx b/site/src/components/Resources/AgentStatus.tsx new file mode 100644 index 0000000000000..86e8c29aeb80d --- /dev/null +++ b/site/src/components/Resources/AgentStatus.tsx @@ -0,0 +1,93 @@ +import Tooltip from "@material-ui/core/Tooltip" +import { makeStyles } from "@material-ui/core/styles" +import { combineClasses } from "util/combineClasses" +import { WorkspaceAgent } from "api/typesGenerated" +import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" + +const ConnectedStatus: React.FC = () => { + const styles = useStyles() + + return ( + +
+ + ) +} + +const DisconnectedStatus: React.FC = () => { + const styles = useStyles() + + return ( + +
+ + ) +} + +const ConnectingStatus: React.FC = () => { + const styles = useStyles() + + return ( + +
+ + ) +} + +export const AgentStatus: React.FC<{ agent: WorkspaceAgent }> = ({ agent }) => { + return ( + + + + + + + + + + + + ) +} + +const useStyles = makeStyles((theme) => ({ + status: { + width: theme.spacing(1), + height: theme.spacing(1), + borderRadius: "100%", + }, + + connected: { + backgroundColor: theme.palette.success.light, + }, + + disconnected: { + backgroundColor: theme.palette.text.secondary, + }, + + "@keyframes pulse": { + "0%": { + opacity: 0.25, + }, + "50%": { + opacity: 1, + }, + "100%": { + opacity: 0.25, + }, + }, + + connecting: { + backgroundColor: theme.palette.info.light, + animation: "$pulse 1s ease-in-out forwards infinite", + }, +})) diff --git a/site/src/components/Resources/ResourceCard.tsx b/site/src/components/Resources/ResourceCard.tsx index bb45169f9d6ff..e86f912e4d99d 100644 --- a/site/src/components/Resources/ResourceCard.tsx +++ b/site/src/components/Resources/ResourceCard.tsx @@ -1,13 +1,8 @@ -import { makeStyles, Theme, useTheme } from "@material-ui/core/styles" +import { makeStyles } from "@material-ui/core/styles" import { Skeleton } from "@material-ui/lab" import { PortForwardButton } from "components/PortForwardButton/PortForwardButton" import { FC, useState } from "react" -import { - Workspace, - WorkspaceAgent, - WorkspaceAgentStatus, - WorkspaceResource, -} from "../../api/typesGenerated" +import { Workspace, WorkspaceResource } from "../../api/typesGenerated" import { AppLink } from "../AppLink/AppLink" import { SSHButton } from "../SSHButton/SSHButton" import { Stack } from "../Stack/Stack" @@ -24,23 +19,7 @@ import IconButton from "@material-ui/core/IconButton" import Tooltip from "@material-ui/core/Tooltip" import { Maybe } from "components/Conditionals/Maybe" import { CopyableValue } from "components/CopyableValue/CopyableValue" - -const getAgentStatusColor = (theme: Theme, agent: WorkspaceAgent) => { - switch (agent.status) { - case "connected": - return theme.palette.success.light - case "connecting": - return theme.palette.info.light - case "disconnected": - return theme.palette.text.secondary - } -} - -const agentStatusLabels: Record = { - connected: "Connected", - connecting: "Connecting...", - disconnected: "Disconnected", -} +import { AgentStatus } from "./AgentStatus" export interface ResourceCardProps { resource: WorkspaceResource @@ -59,7 +38,6 @@ export const ResourceCard: FC = ({ hideSSHButton, serverVersion, }) => { - const theme = useTheme() const [shouldDisplayAllMetadata, setShouldDisplayAllMetadata] = useState(false) const styles = useStyles() @@ -136,9 +114,6 @@ export const ResourceCard: FC = ({
{resource.agents?.map((agent) => { - const statusColor = getAgentStatusColor(theme, agent) - const statusLabel = agentStatusLabels[agent.status] - return ( = ({ className={styles.agentRow} > - -
- +
{agent.name}
({ }, }, - agentStatus: { - width: theme.spacing(1), - height: theme.spacing(1), - borderRadius: "100%", - }, - agentName: { fontWeight: 600, }, From ad60b388b9cbe99cf471f36e94a5cdfca89da36d Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Mon, 17 Oct 2022 19:09:55 +0000 Subject: [PATCH 06/11] Replace table --- site/src/components/Resources/Resources.tsx | 238 ++------------------ 1 file changed, 20 insertions(+), 218 deletions(-) diff --git a/site/src/components/Resources/Resources.tsx b/site/src/components/Resources/Resources.tsx index 46322efd2973e..f03da5c5419b9 100644 --- a/site/src/components/Resources/Resources.tsx +++ b/site/src/components/Resources/Resources.tsx @@ -1,21 +1,12 @@ import Button from "@material-ui/core/Button" -import { makeStyles, Theme } from "@material-ui/core/styles" -import Table from "@material-ui/core/Table" -import TableBody from "@material-ui/core/TableBody" -import TableCell from "@material-ui/core/TableCell" -import TableContainer from "@material-ui/core/TableContainer" -import TableHead from "@material-ui/core/TableHead" -import TableRow from "@material-ui/core/TableRow" +import { makeStyles } from "@material-ui/core/styles" import { Skeleton } from "@material-ui/lab" -import useTheme from "@material-ui/styles/useTheme" import { CloseDropdown, OpenDropdown, } from "components/DropdownArrows/DropdownArrows" import { PortForwardButton } from "components/PortForwardButton/PortForwardButton" -import { TableCellDataPrimary } from "components/TableCellData/TableCellData" import { FC, useState } from "react" -import { getDisplayAgentStatus, getDisplayVersionStatus } from "util/workspace" import { BuildInfoResponse, DERPRegion, @@ -26,28 +17,27 @@ import { import { AppLink } from "../AppLink/AppLink" import { SSHButton } from "../SSHButton/SSHButton" import { Stack } from "../Stack/Stack" -import { TableHeaderRow } from "../TableHeaders/TableHeaders" import { TerminalLink } from "../TerminalLink/TerminalLink" -import { AgentHelpTooltip } from "../Tooltips/AgentHelpTooltip" -import { AgentOutdatedTooltip } from "../Tooltips/AgentOutdatedTooltip" -import { ResourcesHelpTooltip } from "../Tooltips/ResourcesHelpTooltip" -import { ResourceAgentLatency } from "./ResourceAgentLatency" -import { ResourceAvatarData } from "./ResourceAvatarData" import { AlertBanner } from "components/AlertBanner/AlertBanner" import { ResourceAvatar } from "./ResourceAvatar" import { SensitiveValue } from "./SensitiveValue" import { AgentLatency } from "./AgentLatency" -const Language = { - resources: "Resources", - resourceLabel: "Resource", - agentsLabel: "Agents", - agentLabel: "Agent", - statusLabel: "status: ", - versionLabel: "version: ", - osLabel: "os: ", -} +const getLatency = (agent: WorkspaceAgent) => { + // Find the right latency to display + const latencyValues = Object.values(agent.latency ?? {}) + const latency = + latencyValues.find((derp) => derp.preferred) ?? + // Accessing an array index can return undefined as well + // for some reason TS does not handle that + (latencyValues[0] as DERPRegion | undefined) + + if (!latency) { + return undefined + } + return latency +} interface ResourcesProps { resources: WorkspaceResource[] getResourcesError?: Error | unknown @@ -63,13 +53,10 @@ export const Resources: FC> = ({ getResourcesError, workspace, canUpdateWorkspace, - buildInfo, hideSSHButton, applicationsHost, }) => { const styles = useStyles() - const theme: Theme = useTheme() - const serverVersion = buildInfo?.version || "" const [shouldDisplayHideResources, setShouldDisplayHideResources] = useState(false) const displayResources = shouldDisplayHideResources @@ -77,36 +64,13 @@ export const Resources: FC> = ({ : resources.filter((resource) => !resource.hide) const hasHideResources = resources.some((r) => r.hide) - const getDisplayLatency = (agent: WorkspaceAgent) => { - // Find the right latency to display - const latencyValues = Object.values(agent.latency ?? {}) - const latency = - latencyValues.find((derp) => derp.preferred) ?? - // Accessing an array index can return undefined as well - // for some reason TS does not handle that - (latencyValues[0] as DERPRegion | undefined) - - if (!latency) { - return undefined - } - - // Get the color - let color = theme.palette.success.light - if (latency.latency_ms >= 150 && latency.latency_ms < 300) { - color = theme.palette.warning.light - } else if (latency.latency_ms >= 300) { - color = theme.palette.error.light - } - - return { - ...latency, - color, - } + if (getResourcesError) { + return } return ( - - {resources.map((resource) => { + + {displayResources.map((resource) => { // Type is already displayed on top of the resource name const metadataToDisplay = resource.metadata?.filter((data) => data.key !== "type") ?? [] @@ -144,7 +108,7 @@ export const Resources: FC> = ({
{resource.agents?.map((agent) => { - const latency = getDisplayLatency(agent) + const latency = getLatency(agent) return ( > = ({ ) })} -
- {getResourcesError ? ( - - ) : ( - - - - - - - {Language.resourceLabel} - - - - - - {Language.agentLabel} - - - - {canUpdateWorkspace && } - - - - {displayResources.map((resource) => { - { - /* We need to initialize the agents to display the resource */ - } - const agents = resource.agents ?? [null] - const resourceName = ( - - ) - - return agents.map((agent, agentIndex) => { - { - /* If there is no agent, just display the resource name */ - } - if ( - !agent || - workspace.latest_build.transition === "stop" - ) { - return ( - - {resourceName} - - - ) - } - const { displayVersion, outdated } = - getDisplayVersionStatus(agent.version, serverVersion) - const agentStatus = getDisplayAgentStatus(theme, agent) - return ( - - {/* We only want to display the name in the first row because we are using rowSpan */} - {/* The rowspan should be the same than the number of agents */} - {agentIndex === 0 && ( - - {resourceName} - - )} - - - - {agent.name} - -
-
- {Language.statusLabel} - - {agentStatus.status} - -
-
- {Language.osLabel} - - {agent.operating_system} - -
-
- {Language.versionLabel} - - {displayVersion} - - -
-
- -
-
-
- -
- {canUpdateWorkspace && - agent.status === "connected" && ( - <> - {applicationsHost !== undefined && ( - - )} - {!hideSSHButton && ( - - )} - - {agent.apps.map((app) => ( - - ))} - - )} - {canUpdateWorkspace && - agent.status === "connecting" && ( - <> - - - - )} -
-
-
- ) - }) - })} -
-
-
- )} -
- {hasHideResources && (
) } @@ -211,6 +216,10 @@ const useStyles = makeStyles((theme) => ({ resourceCardHeader: { padding: theme.spacing(3, 4), borderBottom: `1px solid ${theme.palette.divider}`, + + "&:last-child": { + borderBottom: 0, + }, }, metadataHeader: { diff --git a/site/src/components/Resources/Resources.tsx b/site/src/components/Resources/Resources.tsx index f03da5c5419b9..4d65e49a77d6b 100644 --- a/site/src/components/Resources/Resources.tsx +++ b/site/src/components/Resources/Resources.tsx @@ -1,50 +1,30 @@ import Button from "@material-ui/core/Button" import { makeStyles } from "@material-ui/core/styles" -import { Skeleton } from "@material-ui/lab" import { CloseDropdown, OpenDropdown, } from "components/DropdownArrows/DropdownArrows" -import { PortForwardButton } from "components/PortForwardButton/PortForwardButton" import { FC, useState } from "react" import { BuildInfoResponse, - DERPRegion, Workspace, - WorkspaceAgent, WorkspaceResource, } from "../../api/typesGenerated" -import { AppLink } from "../AppLink/AppLink" -import { SSHButton } from "../SSHButton/SSHButton" import { Stack } from "../Stack/Stack" -import { TerminalLink } from "../TerminalLink/TerminalLink" import { AlertBanner } from "components/AlertBanner/AlertBanner" -import { ResourceAvatar } from "./ResourceAvatar" -import { SensitiveValue } from "./SensitiveValue" -import { AgentLatency } from "./AgentLatency" +import { ResourceCard } from "./ResourceCard" -const getLatency = (agent: WorkspaceAgent) => { - // Find the right latency to display - const latencyValues = Object.values(agent.latency ?? {}) - const latency = - latencyValues.find((derp) => derp.preferred) ?? - // Accessing an array index can return undefined as well - // for some reason TS does not handle that - (latencyValues[0] as DERPRegion | undefined) - - if (!latency) { - return undefined - } - - return latency +const countAgents = (resource: WorkspaceResource) => { + return resource.agents ? resource.agents.length : 0 } + interface ResourcesProps { resources: WorkspaceResource[] getResourcesError?: Error | unknown workspace: Workspace canUpdateWorkspace: boolean buildInfo?: BuildInfoResponse | undefined - hideSSHButton?: boolean + hideSSHButton: boolean applicationsHost?: string } @@ -55,13 +35,18 @@ export const Resources: FC> = ({ canUpdateWorkspace, hideSSHButton, applicationsHost, + buildInfo, }) => { + const serverVersion = buildInfo?.version || "" const styles = useStyles() const [shouldDisplayHideResources, setShouldDisplayHideResources] = useState(false) const displayResources = shouldDisplayHideResources ? resources - : resources.filter((resource) => !resource.hide) + : resources + .filter((resource) => !resource.hide) + // Display the resources with agents first + .sort((a, b) => countAgents(b) - countAgents(a)) const hasHideResources = resources.some((r) => r.hide) if (getResourcesError) { @@ -69,124 +54,18 @@ export const Resources: FC> = ({ } return ( - + {displayResources.map((resource) => { - // Type is already displayed on top of the resource name - const metadataToDisplay = - resource.metadata?.filter((data) => data.key !== "type") ?? [] - return ( -
- -
- -
- -
-
- {resource.type} -
-
{resource.name}
-
- - {metadataToDisplay.map((data) => ( -
-
{data.key}
- {data.sensitive ? ( - - ) : ( -
{data.value}
- )} -
- ))} -
-
- -
- {resource.agents?.map((agent) => { - const latency = getLatency(agent) - - return ( - - -
-
-
{agent.name}
- - {agent.operating_system} - {agent.version} - {latency && } - -
- - - - {canUpdateWorkspace && agent.status === "connected" && ( - <> - {applicationsHost !== undefined && ( - - )} - {!hideSSHButton && ( - - )} - - {agent.apps.map((app) => ( - - ))} - - )} - {canUpdateWorkspace && agent.status === "connecting" && ( - <> - - - - )} - - - ) - })} -
-
+ ) })} @@ -214,72 +93,7 @@ export const Resources: FC> = ({ ) } -const useStyles = makeStyles((theme) => ({ - tableContainer: { - border: 0, - }, - - resourceAvatar: { - color: "#FFF", - backgroundColor: "#3B73D8", - }, - - resourceNameCell: { - borderRight: `1px solid ${theme.palette.divider}`, - }, - - resourceType: { - fontSize: 14, - color: theme.palette.text.secondary, - marginTop: theme.spacing(0.5), - display: "block", - }, - - // Adds some left spacing - agentColumn: { - paddingLeft: `${theme.spacing(4)}px !important`, - }, - - operatingSystem: { - display: "block", - textTransform: "capitalize", - }, - - agentVersion: { - display: "block", - }, - - accessLinks: { - display: "flex", - gap: theme.spacing(0.5), - flexWrap: "wrap", - justifyContent: "flex-end", - }, - - status: { - whiteSpace: "nowrap", - }, - - data: { - color: theme.palette.text.secondary, - fontSize: 14, - marginTop: theme.spacing(0.75), - display: "grid", - gridAutoFlow: "row", - whiteSpace: "nowrap", - gap: theme.spacing(0.75), - height: "fit-content", - }, - - dataRow: { - display: "flex", - alignItems: "center", - - "& strong": { - marginRight: theme.spacing(1), - }, - }, - +const useStyles = makeStyles(() => ({ buttonWrapper: { display: "flex", alignItems: "center", @@ -291,51 +105,4 @@ const useStyles = makeStyles((theme) => ({ width: "100%", maxWidth: 260, }, - - resourceCard: { - background: theme.palette.background.paper, - borderRadius: theme.shape.borderRadius, - border: `1px solid ${theme.palette.divider}`, - }, - - resourceCardHeader: { - padding: theme.spacing(3, 4), - borderBottom: `1px solid ${theme.palette.divider}`, - }, - - resourceData: { - fontSize: 16, - }, - - resourceDataLabel: { - fontSize: 12, - color: theme.palette.text.secondary, - }, - - agentRow: { - padding: theme.spacing(3, 4), - backgroundColor: theme.palette.background.paperLight, - fontSize: 16, - - "&:not(:last-child)": { - borderBottom: `1px solid ${theme.palette.divider}`, - }, - }, - - agentStatus: { - width: theme.spacing(1), - height: theme.spacing(1), - backgroundColor: theme.palette.success.light, - borderRadius: "100%", - }, - - agentName: { - fontWeight: 600, - }, - - agentData: { - fontSize: 14, - color: theme.palette.text.secondary, - marginTop: theme.spacing(0.5), - }, })) From 803b554e907668a7307641bafd9b244b32769b36 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Mon, 17 Oct 2022 19:39:09 +0000 Subject: [PATCH 08/11] Fix lint --- site/src/components/Resources/ResourceCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/Resources/ResourceCard.tsx b/site/src/components/Resources/ResourceCard.tsx index 0b2eec22cf76d..bc8738545534d 100644 --- a/site/src/components/Resources/ResourceCard.tsx +++ b/site/src/components/Resources/ResourceCard.tsx @@ -114,7 +114,7 @@ export const ResourceCard: FC = ({ {resource.agents && resource.agents.length > 0 && (
- {resource.agents?.map((agent) => { + {resource.agents.map((agent) => { return ( Date: Mon, 17 Oct 2022 21:08:58 +0000 Subject: [PATCH 09/11] Fix tests --- site/src/components/Resources/ResourceCard.tsx | 2 +- site/src/components/Resources/Resources.tsx | 2 +- site/src/util/workspace.test.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/site/src/components/Resources/ResourceCard.tsx b/site/src/components/Resources/ResourceCard.tsx index bc8738545534d..c1513d158fc9c 100644 --- a/site/src/components/Resources/ResourceCard.tsx +++ b/site/src/components/Resources/ResourceCard.tsx @@ -26,7 +26,7 @@ export interface ResourceCardProps { workspace: Workspace applicationsHost: string | undefined showApps: boolean - hideSSHButton: boolean + hideSSHButton?: boolean serverVersion: string } diff --git a/site/src/components/Resources/Resources.tsx b/site/src/components/Resources/Resources.tsx index 4d65e49a77d6b..d15bcf5441ea1 100644 --- a/site/src/components/Resources/Resources.tsx +++ b/site/src/components/Resources/Resources.tsx @@ -24,7 +24,7 @@ interface ResourcesProps { workspace: Workspace canUpdateWorkspace: boolean buildInfo?: BuildInfoResponse | undefined - hideSSHButton: boolean + hideSSHButton?: boolean applicationsHost?: string } diff --git a/site/src/util/workspace.test.ts b/site/src/util/workspace.test.ts index d99abcf633596..8f2a6893d13f8 100644 --- a/site/src/util/workspace.test.ts +++ b/site/src/util/workspace.test.ts @@ -101,8 +101,8 @@ describe("util > workspace", () => { describe("getDisplayVersionStatus", () => { it.each<[string, string, string, boolean]>([ - ["", "", "unknown", false], - ["", "v1.2.3", "unknown", false], + ["", "", "Unknown", false], + ["", "v1.2.3", "Unknown", false], ["v1.2.3", "", "v1.2.3", false], ["v1.2.3", "v1.2.3", "v1.2.3", false], ["v1.2.3", "v1.2.4", "v1.2.3", true], From 25a469663684034623a33854f5149b7ab3e54530 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Mon, 17 Oct 2022 21:31:50 +0000 Subject: [PATCH 10/11] Fix all tests --- site/src/components/PageHeader/PageHeader.tsx | 7 +++++-- site/src/components/Resources/AgentStatus.tsx | 10 +++++++--- .../src/pages/WorkspacePage/WorkspacePage.test.tsx | 14 ++++++++------ site/src/util/workspace.ts | 12 +++--------- .../xServices/portForward/portForwardXService.ts | 1 + 5 files changed, 24 insertions(+), 20 deletions(-) diff --git a/site/src/components/PageHeader/PageHeader.tsx b/site/src/components/PageHeader/PageHeader.tsx index c7f0e49bba152..3944f3405072d 100644 --- a/site/src/components/PageHeader/PageHeader.tsx +++ b/site/src/components/PageHeader/PageHeader.tsx @@ -15,14 +15,17 @@ export const PageHeader: React.FC> = ({ const styles = useStyles({}) return ( -
+
{children}
{actions && ( {actions} )} -
+ ) } diff --git a/site/src/components/Resources/AgentStatus.tsx b/site/src/components/Resources/AgentStatus.tsx index 86e8c29aeb80d..dd4a2490b12a4 100644 --- a/site/src/components/Resources/AgentStatus.tsx +++ b/site/src/components/Resources/AgentStatus.tsx @@ -3,14 +3,16 @@ import { makeStyles } from "@material-ui/core/styles" import { combineClasses } from "util/combineClasses" import { WorkspaceAgent } from "api/typesGenerated" import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" +import { DisplayAgentStatusLanguage } from "util/workspace" const ConnectedStatus: React.FC = () => { const styles = useStyles() return ( - +
@@ -21,9 +23,10 @@ const DisconnectedStatus: React.FC = () => { const styles = useStyles() return ( - +
@@ -34,9 +37,10 @@ const ConnectingStatus: React.FC = () => { const styles = useStyles() return ( - +
diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 3f2df80c358b1..5bc543bbcf067 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-floating-promises */ -import { fireEvent, screen, waitFor } from "@testing-library/react" +import { fireEvent, screen, waitFor, within } from "@testing-library/react" import userEvent from "@testing-library/user-event" import EventSourceMock from "eventsourcemock" import i18next from "i18next" @@ -71,7 +71,8 @@ const testStatus = async (ws: Workspace, label: string) => { ), ) await renderWorkspacePage() - const status = await screen.findByRole("status") + const header = screen.getByTestId("header") + const status = await within(header).findByRole("status") expect(status).toHaveTextContent(label) } @@ -96,7 +97,8 @@ describe("WorkspacePage", () => { await renderWorkspacePage() const workspaceName = await screen.findByText(MockWorkspace.name) expect(workspaceName).toBeDefined() - const status = await screen.findByRole("status") + const header = screen.getByTestId("header") + const status = await within(header).findByRole("status") expect(status).toHaveTextContent("Running") // wait for workspace page to finish loading await screen.findByText("stop") @@ -335,15 +337,15 @@ describe("WorkspacePage", () => { MockWorkspaceAgentDisconnected.name, ) expect(agent2Names.length).toEqual(2) - const agent1Status = await screen.findAllByText( + const agent1Status = await screen.findAllByLabelText( DisplayAgentStatusLanguage[MockWorkspaceAgent.status], ) expect(agent1Status.length).toEqual(1) - const agentDisconnected = await screen.findAllByText( + const agentDisconnected = await screen.findAllByLabelText( DisplayAgentStatusLanguage[MockWorkspaceAgentDisconnected.status], ) expect(agentDisconnected.length).toEqual(1) - const agentConnecting = await screen.findAllByText( + const agentConnecting = await screen.findAllByLabelText( DisplayAgentStatusLanguage[MockWorkspaceAgentConnecting.status], ) expect(agentConnecting.length).toEqual(1) diff --git a/site/src/util/workspace.ts b/site/src/util/workspace.ts index 1a50c5782cec8..47da0de2cc0b4 100644 --- a/site/src/util/workspace.ts +++ b/site/src/util/workspace.ts @@ -105,10 +105,9 @@ export const displayWorkspaceBuildDuration = ( } export const DisplayAgentStatusLanguage = { - loading: "Loading...", - connected: "⦿ Connected", - connecting: "⦿ Connecting", - disconnected: "◍ Disconnected", + connected: "Connected", + connecting: "Connecting...", + disconnected: "Disconnected", } export const getDisplayAgentStatus = ( @@ -119,11 +118,6 @@ export const getDisplayAgentStatus = ( status: string } => { switch (agent.status) { - case undefined: - return { - color: theme.palette.text.secondary, - status: DisplayAgentStatusLanguage.loading, - } case "connected": return { color: theme.palette.success.main, diff --git a/site/src/xServices/portForward/portForwardXService.ts b/site/src/xServices/portForward/portForwardXService.ts index 27fa2bfd4cc56..8c2494083cbbe 100644 --- a/site/src/xServices/portForward/portForwardXService.ts +++ b/site/src/xServices/portForward/portForwardXService.ts @@ -4,6 +4,7 @@ import { createMachine, assign } from "xstate" export const portForwardMachine = createMachine( { + predictableActionArguments: true, id: "portForwardMachine", schema: { context: {} as { From d437ea0b2d2c42be13ba40e1124a9ab9d78da8eb Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Tue, 18 Oct 2022 13:28:03 +0000 Subject: [PATCH 11/11] Move status string to i18n --- site/src/components/Resources/AgentStatus.tsx | 17 ++++++---- site/src/i18n/en/workspacePage.json | 5 +++ .../WorkspacePage/WorkspacePage.test.tsx | 13 +++++--- site/src/util/workspace.ts | 32 ------------------- 4 files changed, 24 insertions(+), 43 deletions(-) diff --git a/site/src/components/Resources/AgentStatus.tsx b/site/src/components/Resources/AgentStatus.tsx index dd4a2490b12a4..1a55f2255bbfb 100644 --- a/site/src/components/Resources/AgentStatus.tsx +++ b/site/src/components/Resources/AgentStatus.tsx @@ -3,16 +3,17 @@ import { makeStyles } from "@material-ui/core/styles" import { combineClasses } from "util/combineClasses" import { WorkspaceAgent } from "api/typesGenerated" import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" -import { DisplayAgentStatusLanguage } from "util/workspace" +import { useTranslation } from "react-i18next" const ConnectedStatus: React.FC = () => { const styles = useStyles() + const { t } = useTranslation("workspacePage") return ( - +
@@ -21,12 +22,13 @@ const ConnectedStatus: React.FC = () => { const DisconnectedStatus: React.FC = () => { const styles = useStyles() + const { t } = useTranslation("workspacePage") return ( - +
@@ -35,12 +37,13 @@ const DisconnectedStatus: React.FC = () => { const ConnectingStatus: React.FC = () => { const styles = useStyles() + const { t } = useTranslation("workspacePage") return ( - +
diff --git a/site/src/i18n/en/workspacePage.json b/site/src/i18n/en/workspacePage.json index 8f36e9b3d44e6..a0901d88bf857 100644 --- a/site/src/i18n/en/workspacePage.json +++ b/site/src/i18n/en/workspacePage.json @@ -33,5 +33,10 @@ "canceling": "Canceling", "deleted": "Deleted", "pending": "Pending" + }, + "agentStatus": { + "connected": "Connected", + "connecting": "Connecting...", + "disconnected": "Disconnected" } } diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 5bc543bbcf067..3986d9930f4db 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -27,7 +27,6 @@ import { renderWithAuth, } from "../../testHelpers/renderHelpers" import { server } from "../../testHelpers/server" -import { DisplayAgentStatusLanguage } from "../../util/workspace" import { WorkspacePage } from "./WorkspacePage" const { t } = i18next @@ -338,15 +337,21 @@ describe("WorkspacePage", () => { ) expect(agent2Names.length).toEqual(2) const agent1Status = await screen.findAllByLabelText( - DisplayAgentStatusLanguage[MockWorkspaceAgent.status], + t(`agentStatus.${MockWorkspaceAgent.status}`, { + ns: "workspacePage", + }), ) expect(agent1Status.length).toEqual(1) const agentDisconnected = await screen.findAllByLabelText( - DisplayAgentStatusLanguage[MockWorkspaceAgentDisconnected.status], + t(`agentStatus.${MockWorkspaceAgentDisconnected.status}`, { + ns: "workspacePage", + }), ) expect(agentDisconnected.length).toEqual(1) const agentConnecting = await screen.findAllByLabelText( - DisplayAgentStatusLanguage[MockWorkspaceAgentConnecting.status], + t(`agentStatus.${MockWorkspaceAgentConnecting.status}`, { + ns: "workspacePage", + }), ) expect(agentConnecting.length).toEqual(1) expect(getTemplateMock).toBeCalled() diff --git a/site/src/util/workspace.ts b/site/src/util/workspace.ts index 47da0de2cc0b4..1e23a1cfe3717 100644 --- a/site/src/util/workspace.ts +++ b/site/src/util/workspace.ts @@ -104,38 +104,6 @@ export const displayWorkspaceBuildDuration = ( return duration ? `${duration} seconds` : inProgressLabel } -export const DisplayAgentStatusLanguage = { - connected: "Connected", - connecting: "Connecting...", - disconnected: "Disconnected", -} - -export const getDisplayAgentStatus = ( - theme: Theme, - agent: TypesGen.WorkspaceAgent, -): { - color: string - status: string -} => { - switch (agent.status) { - case "connected": - return { - color: theme.palette.success.main, - status: DisplayAgentStatusLanguage["connected"], - } - case "connecting": - return { - color: theme.palette.primary.main, - status: DisplayAgentStatusLanguage["connecting"], - } - case "disconnected": - return { - color: theme.palette.text.secondary, - status: DisplayAgentStatusLanguage["disconnected"], - } - } -} - export const getDisplayVersionStatus = ( agentVersion: string, serverVersion: string,