diff --git a/site/src/components/Resources/AgentButton.tsx b/site/src/components/Resources/AgentButton.tsx index 58f0e533b8095..2c0b52e67fe14 100644 --- a/site/src/components/Resources/AgentButton.tsx +++ b/site/src/components/Resources/AgentButton.tsx @@ -1,32 +1,28 @@ import Button, { type ButtonProps } from "@mui/material/Button"; -import { useTheme } from "@emotion/react"; import { forwardRef } from "react"; // eslint-disable-next-line react/display-name -- Name is inferred from variable name export const AgentButton = forwardRef( (props, ref) => { const { children, ...buttonProps } = props; - const theme = useTheme(); return ( diff --git a/site/src/components/Resources/AgentMetadata.tsx b/site/src/components/Resources/AgentMetadata.tsx index ab78842854fb1..886862d96d119 100644 --- a/site/src/components/Resources/AgentMetadata.tsx +++ b/site/src/components/Resources/AgentMetadata.tsx @@ -24,71 +24,6 @@ type ItemStatus = "stale" | "valid" | "loading"; export const WatchAgentMetadataContext = createContext(watchAgentMetadata); -interface MetadataItemProps { - item: WorkspaceAgentMetadata; -} - -const MetadataItem: FC = ({ item }) => { - if (item.result === undefined) { - throw new Error("Metadata item result is undefined"); - } - if (item.description === undefined) { - throw new Error("Metadata item description is undefined"); - } - - const staleThreshold = Math.max( - item.description.interval + item.description.timeout * 2, - // In case there is intense backpressure, we give a little bit of slack. - 5, - ); - - const status: ItemStatus = (() => { - const year = dayjs(item.result.collected_at).year(); - if (year <= 1970 || isNaN(year)) { - return "loading"; - } - // There is a special circumstance for metadata with `interval: 0`. It is - // expected that they run once and never again, so never display them as - // stale. - if (item.result.age > staleThreshold && item.description.interval > 0) { - return "stale"; - } - return "valid"; - })(); - - // Stale data is as good as no data. Plus, we want to build confidence in our - // users that what's shown is real. If times aren't correctly synced this - // could be buggy. But, how common is that anyways? - const value = - status === "loading" ? ( - - ) : status === "stale" ? ( - - - {item.result.value} - - - ) : ( - - {item.result.value} - - ); - - return ( -
-
{item.description.display_name}
-
{value}
-
- ); -}; - export interface AgentMetadataViewProps { metadata: WorkspaceAgentMetadata[]; } @@ -98,16 +33,11 @@ export const AgentMetadataView: FC = ({ metadata }) => { return null; } return ( -
- - {metadata.map((m) => { - if (m.description === undefined) { - throw new Error("Metadata item description is undefined"); - } - return ; - })} - -
+
+ {metadata.map((m) => ( + + ))} +
); }; @@ -162,13 +92,19 @@ export const AgentMetadata: FC = ({ if (metadata === undefined) { return ( -
+
-
+ ); } - return ; + return ( + + a.description.display_name.localeCompare(b.description.display_name), + )} + /> + ); }; export const AgentMetadataSkeleton: FC = () => { @@ -192,6 +128,64 @@ export const AgentMetadataSkeleton: FC = () => { ); }; +interface MetadataItemProps { + item: WorkspaceAgentMetadata; +} + +const MetadataItem: FC = ({ item }) => { + const staleThreshold = Math.max( + item.description.interval + item.description.timeout * 2, + // In case there is intense backpressure, we give a little bit of slack. + 5, + ); + + const status: ItemStatus = (() => { + const year = dayjs(item.result.collected_at).year(); + if (year <= 1970 || isNaN(year)) { + return "loading"; + } + // There is a special circumstance for metadata with `interval: 0`. It is + // expected that they run once and never again, so never display them as + // stale. + if (item.result.age > staleThreshold && item.description.interval > 0) { + return "stale"; + } + return "valid"; + })(); + + // Stale data is as good as no data. Plus, we want to build confidence in our + // users that what's shown is real. If times aren't correctly synced this + // could be buggy. But, how common is that anyways? + const value = + status === "loading" ? ( + + ) : status === "stale" ? ( + + + {item.result.value} + + + ) : ( + + {item.result.value} + + ); + + return ( +
+
{item.description.display_name}
+
{value}
+
+ ); +}; + const StaticWidth: FC> = ({ children, ...attrs @@ -221,25 +215,20 @@ const StaticWidth: FC> = ({ // These are more or less copied from // site/src/components/Resources/ResourceCard.tsx const styles = { - root: (theme) => ({ - padding: "20px 32px", - borderTop: `1px solid ${theme.palette.divider}`, - overflowX: "auto", - scrollPadding: "0 32px", - }), + root: { + display: "flex", + alignItems: "baseline", + flexWrap: "wrap", + gap: 32, + rowGap: 16, + }, metadata: { - fontSize: 12, - lineHeight: "normal", + lineHeight: "1.6", display: "flex", flexDirection: "column", - gap: 4, overflow: "visible", - - // Because of scrolling - "&:last-child": { - paddingRight: 32, - }, + flexShrink: 0, }, metadataLabel: (theme) => ({ @@ -247,7 +236,7 @@ const styles = { textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap", - fontWeight: 500, + fontSize: 13, }), metadataValue: { @@ -259,9 +248,7 @@ const styles = { }, metadataValueSuccess: (theme) => ({ - // color: theme.palette.success.light, - color: theme.experimental.roles.success.fill, - // color: theme.experimental.roles.success.text, + color: theme.experimental.roles.success.outline, }), metadataValueError: (theme) => ({ diff --git a/site/src/components/Resources/AgentRow.stories.tsx b/site/src/components/Resources/AgentRow.stories.tsx index b760899683304..4b74c35eb9226 100644 --- a/site/src/components/Resources/AgentRow.stories.tsx +++ b/site/src/components/Resources/AgentRow.stories.tsx @@ -20,6 +20,7 @@ import { MockWorkspaceAgentDeprecated, MockWorkspaceApp, MockProxyLatencies, + MockListeningPortsResponse, } from "testHelpers/entities"; import { AgentRow, LineWithID } from "./AgentRow"; import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"; @@ -103,7 +104,15 @@ const storybookLogs: LineWithID[] = [ const meta: Meta = { title: "components/AgentRow", - parameters: { chromatic }, + parameters: { + chromatic, + queries: [ + { + key: ["portForward", MockWorkspaceAgent.id], + data: MockListeningPortsResponse, + }, + ], + }, component: AgentRow, args: { storybookLogs, diff --git a/site/src/components/Resources/AgentRow.test.tsx b/site/src/components/Resources/AgentRow.test.tsx new file mode 100644 index 0000000000000..bdedcce222fb7 --- /dev/null +++ b/site/src/components/Resources/AgentRow.test.tsx @@ -0,0 +1,102 @@ +import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities"; +import { AgentRow, AgentRowProps } from "./AgentRow"; +import { DisplayAppNameMap } from "./AppLink/AppLink"; +import { screen } from "@testing-library/react"; +import { + renderWithAuth, + waitForLoaderToBeRemoved, +} from "testHelpers/renderHelpers"; + +jest.mock("components/Resources/AgentMetadata", () => { + const AgentMetadata = () => <>; + return { AgentMetadata }; +}); + +describe.each<{ + result: "visible" | "hidden"; + props: Partial; +}>([ + { + result: "visible", + props: { + showApps: true, + agent: { + ...MockWorkspaceAgent, + display_apps: ["vscode", "vscode_insiders"], + status: "connected", + }, + hideVSCodeDesktopButton: false, + }, + }, + { + result: "hidden", + props: { + showApps: false, + agent: { + ...MockWorkspaceAgent, + display_apps: ["vscode", "vscode_insiders"], + status: "connected", + }, + hideVSCodeDesktopButton: false, + }, + }, + { + result: "hidden", + props: { + showApps: true, + agent: { + ...MockWorkspaceAgent, + display_apps: [], + status: "connected", + }, + hideVSCodeDesktopButton: false, + }, + }, + { + result: "hidden", + props: { + showApps: true, + agent: { + ...MockWorkspaceAgent, + display_apps: ["vscode", "vscode_insiders"], + status: "disconnected", + }, + hideVSCodeDesktopButton: false, + }, + }, + { + result: "hidden", + props: { + showApps: true, + agent: { + ...MockWorkspaceAgent, + display_apps: ["vscode", "vscode_insiders"], + status: "connected", + }, + hideVSCodeDesktopButton: true, + }, + }, +])("VSCode button visibility", ({ props: testProps, result }) => { + const props: AgentRowProps = { + agent: MockWorkspaceAgent, + workspace: MockWorkspace, + showApps: false, + serverVersion: "", + serverAPIVersion: "", + onUpdateAgent: function (): void { + throw new Error("Function not implemented."); + }, + ...testProps, + }; + + test(`visibility: ${result}, showApps: ${props.showApps}, hideVSCodeDesktopButton: ${props.hideVSCodeDesktopButton}, display apps: ${props.agent.display_apps}`, async () => { + renderWithAuth(); + await waitForLoaderToBeRemoved(); + + if (result === "visible") { + expect(screen.getByText(DisplayAppNameMap["vscode"])).toBeVisible(); + } else { + expect(screen.queryByText(DisplayAppNameMap["vscode"])).toBeNull(); + } + }); +}); diff --git a/site/src/components/Resources/AgentRow.tsx b/site/src/components/Resources/AgentRow.tsx index 36358d7f921dd..81abc9fbb45b6 100644 --- a/site/src/components/Resources/AgentRow.tsx +++ b/site/src/components/Resources/AgentRow.tsx @@ -31,12 +31,12 @@ import { FixedSizeList as List, ListOnScrollProps } from "react-window"; import { Stack } from "../Stack/Stack"; import { AgentLatency } from "./AgentLatency"; import { AgentMetadata } from "./AgentMetadata"; -import { AgentStatus } from "./AgentStatus"; import { AgentVersion } from "./AgentVersion"; import { AppLink } from "./AppLink/AppLink"; import { PortForwardButton } from "./PortForwardButton"; import { SSHButton } from "./SSHButton/SSHButton"; import { TerminalLink } from "./TerminalLink/TerminalLink"; +import { AgentStatus } from "./AgentStatus"; // Logs are stored as the Line interface to make rendering // much more efficient. Instead of mapping objects each time, we're @@ -79,6 +79,11 @@ export const AgentRow: FC = ({ showApps && ((agent.status === "connected" && hasAppsToDisplay) || agent.status === "connecting"); + const hasVSCodeApp = + agent.display_apps.includes("vscode") || + agent.display_apps.includes("vscode_insiders"); + const showVSCode = hasVSCodeApp && !hideVSCodeDesktopButton; + const logSourceByID = useMemo(() => { const sources: { [id: string]: WorkspaceAgentLogSource } = {}; for (const source of agent.log_sources) { @@ -163,54 +168,68 @@ export const AgentRow: FC = ({ styles[`agentRow-lifecycle-${agent.lifecycle_state}`], ]} > -
-
-
+
+
+
-
{agent.name}
- - {agent.status === "connected" && ( - <> - {agent.operating_system} - - - - )} - {agent.status === "connecting" && ( - <> - - - - )} - + {agent.name}
+ {agent.status === "connected" && ( + <> + + + + )} + {agent.status === "connecting" && ( + <> + + + + )}
+ {showBuiltinApps && ( +
+ {!hideSSHButton && agent.display_apps.includes("ssh_helper") && ( + + )} + {proxy.preferredWildcardHostname && + proxy.preferredWildcardHostname !== "" && + agent.display_apps.includes("port_forwarding_helper") && ( + + )} +
+ )} +
+ +
{agent.status === "connected" && ( -
+
{shouldDisplayApps && ( <> - {(agent.display_apps.includes("vscode") || - agent.display_apps.includes("vscode_insiders")) && - !hideVSCodeDesktopButton && ( - - )} + {showVSCode && ( + + )} {agent.apps.map((app) => ( = ({ )} - {showBuiltinApps && ( - <> - {agent.display_apps.includes("web_terminal") && ( - - )} - {!hideSSHButton && - agent.display_apps.includes("ssh_helper") && ( - - )} - {proxy.preferredWildcardHostname && - proxy.preferredWildcardHostname !== "" && - agent.display_apps.includes("port_forwarding_helper") && ( - - )} - + {showBuiltinApps && agent.display_apps.includes("web_terminal") && ( + )} -
+ )} {agent.status === "connecting" && ( -
+
= ({ variant="rectangular" css={styles.buttonSkeleton} /> -
+ )} -
- + +
{hasStartupFeatures && ( -
+
({ borderTop: `1px solid ${theme.palette.divider}` })} + > {({ width }) => ( @@ -430,16 +432,14 @@ export const AgentRow: FC = ({ -
- -
-
+ + )} ); @@ -505,78 +505,85 @@ const useAgentLogs = ( const styles = { agentRow: (theme) => ({ - fontSize: 16, - borderLeft: `2px solid ${theme.palette.text.secondary}`, - - "&:not(:first-of-type)": { - borderTop: `2px solid ${theme.palette.divider}`, - }, + fontSize: 14, + border: `1px solid ${theme.palette.text.secondary}`, + backgroundColor: theme.palette.background.default, + borderRadius: 8, }), "agentRow-connected": (theme) => ({ - borderLeftColor: theme.palette.success.light, + borderColor: theme.palette.success.light, }), "agentRow-disconnected": (theme) => ({ - borderLeftColor: theme.palette.text.secondary, + borderColor: theme.palette.divider, }), "agentRow-connecting": (theme) => ({ - borderLeftColor: theme.palette.info.light, + borderColor: theme.palette.info.light, }), "agentRow-timeout": (theme) => ({ - borderLeftColor: theme.palette.warning.light, + borderColor: theme.palette.warning.light, }), "agentRow-lifecycle-created": {}, "agentRow-lifecycle-starting": (theme) => ({ - borderLeftColor: theme.palette.info.light, + borderColor: theme.palette.info.light, }), "agentRow-lifecycle-ready": (theme) => ({ - borderLeftColor: theme.palette.success.light, + borderColor: theme.palette.success.light, }), "agentRow-lifecycle-start_timeout": (theme) => ({ - borderLeftColor: theme.palette.warning.light, + borderColor: theme.palette.warning.light, }), "agentRow-lifecycle-start_error": (theme) => ({ - borderLeftColor: theme.palette.error.light, + borderColor: theme.palette.error.light, }), "agentRow-lifecycle-shutting_down": (theme) => ({ - borderLeftColor: theme.palette.info.light, + borderColor: theme.palette.info.light, }), "agentRow-lifecycle-shutdown_timeout": (theme) => ({ - borderLeftColor: theme.palette.warning.light, + borderColor: theme.palette.warning.light, }), "agentRow-lifecycle-shutdown_error": (theme) => ({ - borderLeftColor: theme.palette.error.light, + borderColor: theme.palette.error.light, }), "agentRow-lifecycle-off": (theme) => ({ - borderLeftColor: theme.palette.text.secondary, + borderColor: theme.palette.divider, }), - agentInfo: (theme) => ({ - padding: "24px 32px", + header: (theme) => ({ + padding: "12px 24px", display: "flex", - gap: 16, + gap: 24, alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", - backgroundColor: theme.palette.background.paper, + lineHeight: "1.5", + borderBottom: `1px solid ${theme.palette.divider}`, [theme.breakpoints.down("md")]: { gap: 16, }, }), + agentInfo: (theme) => ({ + display: "flex", + alignItems: "center", + gap: 24, + color: theme.palette.text.secondary, + fontSize: 13, + }), + agentNameAndInfo: (theme) => ({ display: "flex", alignItems: "center", @@ -588,11 +595,22 @@ const styles = { }, }), - agentButtons: (theme) => ({ + content: { + padding: "32px 24px", display: "flex", - gap: 8, + flexDirection: "column", + gap: 32, + }, + + apps: (theme) => ({ + display: "flex", + gap: 16, flexWrap: "wrap", + "&:empty": { + display: "none", + }, + [theme.breakpoints.down("md")]: { marginLeft: 0, justifyContent: "flex-start", @@ -619,7 +637,7 @@ const styles = { agentNameAndStatus: (theme) => ({ display: "flex", alignItems: "center", - gap: 32, + gap: 12, [theme.breakpoints.down("md")]: { width: "100%", @@ -632,9 +650,10 @@ const styles = { textOverflow: "ellipsis", maxWidth: 260, fontWeight: 600, - fontSize: 16, flexShrink: 0, width: "fit-content", + fontSize: 14, + color: theme.palette.text.primary, [theme.breakpoints.down("md")]: { overflow: "unset", @@ -658,16 +677,12 @@ const styles = { }, }), - logsPanel: (theme) => ({ - borderTop: `1px solid ${theme.palette.divider}`, - }), - logsPanelButton: (theme) => ({ textAlign: "left", background: "transparent", border: 0, fontFamily: "inherit", - padding: "12px 32px", + padding: "12px 24px", color: theme.palette.text.secondary, cursor: "pointer", display: "flex", @@ -675,6 +690,8 @@ const styles = { gap: 8, whiteSpace: "nowrap", width: "100%", + borderBottomLeftRadius: 8, + borderBottomRightRadius: 8, "&:hover": { color: theme.palette.text.primary, diff --git a/site/src/components/Resources/AgentRowPreview.tsx b/site/src/components/Resources/AgentRowPreview.tsx index e4372a131571c..f088b5ca77f08 100644 --- a/site/src/components/Resources/AgentRowPreview.tsx +++ b/site/src/components/Resources/AgentRowPreview.tsx @@ -6,6 +6,7 @@ import { AppPreview } from "./AppLink/AppPreview"; import { BaseIcon } from "./AppLink/BaseIcon"; import { VSCodeIcon } from "components/Icons/VSCodeIcon"; import { DisplayAppNameMap } from "./AppLink/AppLink"; +import { TerminalIcon } from "components/Icons/TerminalIcon"; interface AgentRowPreviewStyles { // Helpful when there are more than one row so the values are aligned @@ -101,7 +102,10 @@ export const AgentRowPreview: FC = ({ {/* Additionally, we display any apps that are visible, e.g. apps that are included in agent.display_apps */} {agent.display_apps.includes("web_terminal") && ( - {DisplayAppNameMap["web_terminal"]} + + + {DisplayAppNameMap["web_terminal"]} + )} {agent.display_apps.includes("ssh_helper") && ( {DisplayAppNameMap["ssh_helper"]} diff --git a/site/src/components/Resources/AgentStatus.tsx b/site/src/components/Resources/AgentStatus.tsx index 0793377994b5c..ffb56953efa4a 100644 --- a/site/src/components/Resources/AgentStatus.tsx +++ b/site/src/components/Resources/AgentStatus.tsx @@ -272,8 +272,8 @@ export const AgentStatus: FC = ({ agent }) => { const styles = { status: { - width: 8, - height: 8, + width: 6, + height: 6, borderRadius: "100%", flexShrink: 0, }, @@ -306,15 +306,15 @@ const styles = { timeoutWarning: (theme) => ({ color: theme.palette.warning.light, - width: 16, - height: 16, + width: 14, + height: 14, position: "relative", }), errorWarning: (theme) => ({ color: theme.palette.error.main, - width: 16, - height: 16, + width: 14, + height: 14, position: "relative", }), } satisfies Record>; diff --git a/site/src/components/Resources/AppLink/AppLink.tsx b/site/src/components/Resources/AppLink/AppLink.tsx index 24afe3cef541f..75d03f6c477bc 100644 --- a/site/src/components/Resources/AppLink/AppLink.tsx +++ b/site/src/components/Resources/AppLink/AppLink.tsx @@ -67,7 +67,21 @@ export const AppLink: FC = ({ app, workspace, agent }) => { let primaryTooltip = ""; if (app.health === "initializing") { canClick = false; - icon = ; + icon = ( + // This is a hack to make the spinner appear in the center of the start + // icon space + + + + ); primaryTooltip = "Initializing..."; } if (app.health === "unhealthy") { @@ -93,75 +107,57 @@ export const AppLink: FC = ({ app, workspace, agent }) => { const isPrivateApp = app.sharing_level === "owner"; - const button = ( - } - disabled={!canClick} - > - - {appDisplayName} - - - ); - return ( - - { - event.preventDefault(); - // This is an external URI like "vscode://", so - // it needs to be opened with the browser protocol handler. - if (app.external && !app.url.startsWith("http")) { - // If the protocol is external the browser does not - // redirect the user from the page. + } + disabled={!canClick} + href={href} + target="_blank" + css={{ + pointerEvents: canClick ? undefined : "none", + textDecoration: "none !important", + }} + onClick={async (event) => { + if (!canClick) { + return; + } - // This is a magic undocumented string that is replaced - // with a brand-new session token from the backend. - // This only exists for external URLs, and should only - // be used internally, and is highly subject to break. - const magicTokenString = "$SESSION_TOKEN"; - const hasMagicToken = href.indexOf(magicTokenString); - let url = href; - if (hasMagicToken !== -1) { - setFetchingSessionToken(true); - const key = await getApiKey(); - url = href.replaceAll(magicTokenString, key.key); - setFetchingSessionToken(false); - } - window.location.href = url; - } else { - window.open( - href, - Language.appTitle( - appDisplayName, - generateRandomString(12), - ), - "width=900,height=600", - ); - } - } - : undefined + event.preventDefault(); + // This is an external URI like "vscode://", so + // it needs to be opened with the browser protocol handler. + if (app.external && !app.url.startsWith("http")) { + // If the protocol is external the browser does not + // redirect the user from the page. + + // This is a magic undocumented string that is replaced + // with a brand-new session token from the backend. + // This only exists for external URLs, and should only + // be used internally, and is highly subject to break. + const magicTokenString = "$SESSION_TOKEN"; + const hasMagicToken = href.indexOf(magicTokenString); + let url = href; + if (hasMagicToken !== -1) { + setFetchingSessionToken(true); + const key = await getApiKey(); + url = href.replaceAll(magicTokenString, key.key); + setFetchingSessionToken(false); + } + window.location.href = url; + } else { + window.open( + href, + Language.appTitle(appDisplayName, generateRandomString(12)), + "width=900,height=600", + ); } - > - {button} - - + }} + > + {appDisplayName} + ); }; diff --git a/site/src/components/Resources/PortForwardButton.tsx b/site/src/components/Resources/PortForwardButton.tsx index 2b284586eaf45..40a9cc11dc624 100644 --- a/site/src/components/Resources/PortForwardButton.tsx +++ b/site/src/components/Resources/PortForwardButton.tsx @@ -20,13 +20,12 @@ import { HelpTooltipText, HelpTooltipTitle, } from "components/HelpTooltip/HelpTooltip"; -import { AgentButton } from "components/Resources/AgentButton"; import { Popover, PopoverContent, PopoverTrigger, } from "components/Popover/Popover"; -import { DisplayAppNameMap } from "./AppLink/AppLink"; +import KeyboardArrowDown from "@mui/icons-material/KeyboardArrowDown"; export interface PortForwardButtonProps { host: string; @@ -59,14 +58,24 @@ export const PortForwardButton: FC = (props) => { return ( - - {DisplayAppNameMap["port_forwarding_helper"]} - {data ? ( -
{data.ports.length}
- ) : ( - - )} -
+
@@ -214,8 +223,7 @@ const styles = { display: "flex", alignItems: "center", justifyContent: "center", - backgroundColor: theme.experimental.l2.background, - marginLeft: 8, + backgroundColor: theme.palette.action.selected, }), portLink: (theme) => ({ diff --git a/site/src/components/Resources/ResourceCard.stories.tsx b/site/src/components/Resources/ResourceCard.stories.tsx index 576ef68db7de2..56c373c2081e8 100644 --- a/site/src/components/Resources/ResourceCard.stories.tsx +++ b/site/src/components/Resources/ResourceCard.stories.tsx @@ -1,14 +1,12 @@ -import { action } from "@storybook/addon-actions"; import { MockProxyLatencies, - MockWorkspace, MockWorkspaceResource, } from "testHelpers/entities"; -import { AgentRow } from "./AgentRow"; import { ResourceCard } from "./ResourceCard"; import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"; import type { Meta, StoryObj } from "@storybook/react"; import { type WorkspaceAgent } from "api/typesGenerated"; +import { AgentRowPreview } from "./AgentRowPreview"; const meta: Meta = { title: "components/Resources/ResourceCard", @@ -93,15 +91,7 @@ function getAgentRow(agent: WorkspaceAgent): JSX.Element { }, }} > - + ); } diff --git a/site/src/components/Resources/ResourceCard.tsx b/site/src/components/Resources/ResourceCard.tsx index 17a4505801199..6e7188230a10d 100644 --- a/site/src/components/Resources/ResourceCard.tsx +++ b/site/src/components/Resources/ResourceCard.tsx @@ -1,7 +1,7 @@ import { type FC, type PropsWithChildren, useState } from "react"; import IconButton from "@mui/material/IconButton"; import Tooltip from "@mui/material/Tooltip"; -import { type CSSObject, type Interpolation, type Theme } from "@emotion/react"; +import { type Interpolation, type Theme } from "@emotion/react"; import { Children } from "react"; import type { WorkspaceAgent, WorkspaceResource } from "api/typesGenerated"; import { DropdownArrow } from "../DropdownArrow/DropdownArrow"; @@ -13,14 +13,28 @@ import { SensitiveValue } from "./SensitiveValue"; const styles = { resourceCard: (theme) => ({ - borderRadius: 8, border: `1px solid ${theme.palette.divider}`, background: theme.palette.background.default, + + "&:not(:last-child)": { + borderBottom: 0, + }, + + "&:first-child": { + borderTopLeftRadius: 8, + borderTopRightRadius: 8, + }, + + "&:last-child": { + borderBottomLeftRadius: 8, + borderBottomRightRadius: 8, + }, }), resourceCardProfile: { flexShrink: 0, width: "fit-content", + minWidth: 220, }, resourceCardHeader: (theme) => ({ @@ -37,9 +51,9 @@ const styles = { }, }), - metadata: (theme) => ({ - ...(theme.typography.body2 as CSSObject), - lineHeight: "120%", + metadata: () => ({ + lineHeight: "1.5", + fontSize: 14, }), metadataLabel: (theme) => ({ @@ -50,11 +64,10 @@ const styles = { whiteSpace: "nowrap", }), - metadataValue: (theme) => ({ + metadataValue: () => ({ textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap", - ...(theme.typography.body1 as CSSObject), }), } satisfies Record>; diff --git a/site/src/components/Resources/Resources.stories.tsx b/site/src/components/Resources/Resources.stories.tsx index 0a7693f5b4ead..8141b6516cc1d 100644 --- a/site/src/components/Resources/Resources.stories.tsx +++ b/site/src/components/Resources/Resources.stories.tsx @@ -1,15 +1,13 @@ -import { action } from "@storybook/addon-actions"; import { MockProxyLatencies, - MockWorkspace, MockWorkspaceResource, MockWorkspaceResourceMultipleAgents, } from "testHelpers/entities"; -import { AgentRow } from "./AgentRow"; import { Resources } from "./Resources"; import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"; import type { Meta, StoryObj } from "@storybook/react"; import { type WorkspaceAgent } from "api/typesGenerated"; +import { AgentRowPreview } from "./AgentRowPreview"; const meta: Meta = { title: "components/Resources/Resources", @@ -189,15 +187,7 @@ function getAgentRow(agent: WorkspaceAgent): JSX.Element { }, }} > - + ); } diff --git a/site/src/components/Resources/SSHButton/SSHButton.tsx b/site/src/components/Resources/SSHButton/SSHButton.tsx index 9788618df6406..62b55e9ab3764 100644 --- a/site/src/components/Resources/SSHButton/SSHButton.tsx +++ b/site/src/components/Resources/SSHButton/SSHButton.tsx @@ -14,8 +14,8 @@ import { PopoverTrigger, } from "components/Popover/Popover"; import { Stack } from "components/Stack/Stack"; -import { AgentButton } from "../AgentButton"; -import { DisplayAppNameMap } from "../AppLink/AppLink"; +import Button from "@mui/material/Button"; +import KeyboardArrowDown from "@mui/icons-material/KeyboardArrowDown"; export interface SSHButtonProps { workspaceName: string; @@ -35,7 +35,14 @@ export const SSHButton: FC> = ({ return ( - {DisplayAppNameMap["ssh_helper"]} + diff --git a/site/src/components/Resources/TerminalLink/TerminalLink.tsx b/site/src/components/Resources/TerminalLink/TerminalLink.tsx index d1a8e4e9b170b..d73d7d5fe61cb 100644 --- a/site/src/components/Resources/TerminalLink/TerminalLink.tsx +++ b/site/src/components/Resources/TerminalLink/TerminalLink.tsx @@ -4,6 +4,7 @@ import { FC } from "react"; import * as TypesGen from "api/typesGenerated"; import { generateRandomString } from "utils/random"; import { DisplayAppNameMap } from "../AppLink/AppLink"; +import { TerminalIcon } from "components/Icons/TerminalIcon"; export const Language = { terminalTitle: (identifier: string): string => `Terminal - ${identifier}`, @@ -34,6 +35,10 @@ export const TerminalLink: FC> = ({ return ( } href={href} target="_blank" onClick={(event) => { @@ -46,7 +51,7 @@ export const TerminalLink: FC> = ({ }} data-testid="terminal" > - {DisplayAppNameMap["web_terminal"]} + {DisplayAppNameMap["web_terminal"]} ); }; diff --git a/site/src/components/Resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx b/site/src/components/Resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx index 33f4b8a0c3d35..3e96b67f2e144 100644 --- a/site/src/components/Resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx +++ b/site/src/components/Resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx @@ -48,16 +48,7 @@ export const VSCodeDesktopButton: FC< return includesVSCodeDesktop && includesVSCodeInsiders ? (
- button:hover + button": { - borderLeft: "1px solid #FFF", - }, - }} - > + {variant === "vscode" ? ( ) : ( diff --git a/site/src/pages/WorkspacePage/Workspace.stories.tsx b/site/src/pages/WorkspacePage/Workspace.stories.tsx index 47d1fad733552..07a320c56c513 100644 --- a/site/src/pages/WorkspacePage/Workspace.stories.tsx +++ b/site/src/pages/WorkspacePage/Workspace.stories.tsx @@ -28,6 +28,14 @@ const meta: Meta = { title: "pages/WorkspacePage/Workspace", args: { permissions }, component: Workspace, + parameters: { + queries: [ + { + key: ["portForward", Mocks.MockWorkspaceAgent.id], + data: Mocks.MockListeningPortsResponse, + }, + ], + }, decorators: [ (Story) => ( void; @@ -184,6 +186,9 @@ export const Workspace: FC = ({
+ {selectedResource && ( + + )}
= ({ {buildLogs} {selectedResource && ( - ( +
+ {selectedResource.agents?.map((agent) => ( = ({ serverAPIVersion={buildInfo?.agent_api_version || ""} onUpdateAgent={handleUpdate} // On updating the workspace the agent version is also updated /> + ))} + + {(!selectedResource.agents || + selectedResource.agents?.length === 0) && ( +
+
+

+ No agents are currently assigned to this resource. +

+
+
)} - /> +
)}
@@ -257,6 +282,55 @@ export const Workspace: FC = ({ ); }; +const WorkspaceResourceData: FC<{ resource: TypesGen.WorkspaceResource }> = ({ + resource, +}) => { + const metadata = resource.metadata ? [...resource.metadata] : []; + + if (resource.daily_cost > 0) { + metadata.push({ + key: "Daily cost", + value: resource.daily_cost.toString(), + sensitive: false, + }); + } + + if (metadata.length === 0) { + return null; + } + + return ( +
+ {metadata.map((meta) => { + return ( +
+
+ {meta.sensitive ? ( + + ) : ( + + {meta.value} + + )} +
+
{meta.key}
+
+ ); + })} +
+ ); +}; + +const MetaValue = ({ children }: PropsWithChildren) => { + const childrenArray = Children.toArray(children); + if (childrenArray.every((child) => typeof child === "string")) { + return ( + {children} + ); + } + return <>{children}; +}; + const countAgents = (resource: TypesGen.WorkspaceResource) => { return resource.agents ? resource.agents.length : 0; }; @@ -266,6 +340,7 @@ const styles = { padding: 24, gridArea: "content", overflowY: "auto", + position: "relative", }, dotBackground: (theme) => ({ @@ -290,4 +365,34 @@ const styles = { flexDirection: "column", }, }), + + resourceData: (theme) => ({ + padding: 24, + margin: "-48px 0 0 -48px", + display: "flex", + flexWrap: "wrap", + gap: 48, + rowGap: 24, + marginBottom: 24, + fontSize: 14, + background: `linear-gradient(180deg, ${theme.palette.background.default} 0%, rgba(0, 0, 0, 0) 100%)`, + }), + + resourceDataItem: () => ({ + lineHeight: "1.5", + }), + + resourceDataItemLabel: (theme) => ({ + fontSize: 13, + color: theme.palette.text.secondary, + textOverflow: "ellipsis", + overflow: "hidden", + whiteSpace: "nowrap", + }), + + resourceDataItemValue: () => ({ + textOverflow: "ellipsis", + overflow: "hidden", + whiteSpace: "nowrap", + }), } satisfies Record>; diff --git a/site/src/theme/mui.ts b/site/src/theme/mui.ts index 502561b472082..5393ace0788c9 100644 --- a/site/src/theme/mui.ts +++ b/site/src/theme/mui.ts @@ -109,6 +109,14 @@ export const components = { }, ["sizeXlarge" as any]: { height: BUTTON_XL_HEIGHT, + + // With higher size we need to increase icon spacing. + "& .MuiButton-startIcon": { + marginRight: 12, + }, + "& .MuiButton-endIcon": { + marginLeft: 12, + }, }, outlined: ({ theme }) => ({ ":hover": { @@ -144,9 +152,6 @@ export const components = { fontSize: 13, }, }, - startIcon: { - marginLeft: "-2px", - }, }, }, MuiButtonGroup: { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy