diff --git a/site/src/api/queries/deployment.ts b/site/src/api/queries/deployment.ts index 4b65b20da82cc..4d1610bd57b46 100644 --- a/site/src/api/queries/deployment.ts +++ b/site/src/api/queries/deployment.ts @@ -1,4 +1,5 @@ import { API } from "api/api"; +import { disabledRefetchOptions } from "./util"; export const deploymentConfigQueryKey = ["deployment", "config"]; @@ -26,6 +27,7 @@ export const deploymentStats = () => { export const deploymentSSHConfig = () => { return { + ...disabledRefetchOptions, queryKey: ["deployment", "sshConfig"], queryFn: API.getDeploymentSSHConfig, }; diff --git a/site/src/modules/resources/AgentMetadata.tsx b/site/src/modules/resources/AgentMetadata.tsx index f8eba334f09ce..5c6986a4dc618 100644 --- a/site/src/modules/resources/AgentMetadata.tsx +++ b/site/src/modules/resources/AgentMetadata.tsx @@ -42,24 +42,24 @@ export const AgentMetadataView: FC = ({ metadata }) => { interface AgentMetadataProps { agent: WorkspaceAgent; - storybookMetadata?: WorkspaceAgentMetadata[]; + initialMetadata?: WorkspaceAgentMetadata[]; } const maxSocketErrorRetryCount = 3; export const AgentMetadata: FC = ({ agent, - storybookMetadata, + initialMetadata, }) => { - const [activeMetadata, setActiveMetadata] = useState(storybookMetadata); + const [activeMetadata, setActiveMetadata] = useState(initialMetadata); useEffect(() => { // This is an unfortunate pitfall with this component's testing setup, - // but even though we use the value of storybookMetadata as the initial + // but even though we use the value of initialMetadata as the initial // value of the activeMetadata, we cannot put activeMetadata itself into // the dependency array. If we did, we would destroy and rebuild each // connection every single time a new message comes in from the socket, // because the socket has to be wired up to the state setter - if (storybookMetadata !== undefined) { + if (initialMetadata !== undefined) { return; } @@ -118,7 +118,7 @@ export const AgentMetadata: FC = ({ window.clearTimeout(timeoutId); activeSocket?.close(); }; - }, [agent.id, storybookMetadata]); + }, [agent.id, initialMetadata]); if (activeMetadata === undefined) { return ( diff --git a/site/src/modules/resources/AgentRow.stories.tsx b/site/src/modules/resources/AgentRow.stories.tsx index c08d28f295cd9..9d889ab0203eb 100644 --- a/site/src/modules/resources/AgentRow.stories.tsx +++ b/site/src/modules/resources/AgentRow.stories.tsx @@ -1,4 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; +import { spyOn } from "@storybook/test"; +import { API } from "api/api"; import { getPreferredProxy } from "contexts/ProxyContext"; import { chromatic } from "testHelpers/chromatic"; import * as M from "testHelpers/entities"; @@ -95,8 +97,7 @@ const meta: Meta = { logs_length: logs.length, }, workspace: M.MockWorkspace, - showApps: true, - storybookAgentMetadata: defaultAgentMetadata, + initialMetadata: defaultAgentMetadata, }, decorators: [withProxyProvider(), withDashboardProvider, withWebSocket], parameters: { @@ -121,24 +122,6 @@ type Story = StoryObj; export const Example: Story = {}; -export const HideSSHButton: Story = { - args: { - hideSSHButton: true, - }, -}; - -export const HideVSCodeDesktopButton: Story = { - args: { - hideVSCodeDesktopButton: true, - }, -}; - -export const NotShowingApps: Story = { - args: { - showApps: false, - }, -}; - export const BunchOfApps: Story = { args: { agent: { @@ -155,14 +138,13 @@ export const BunchOfApps: Story = { ], }, workspace: M.MockWorkspace, - showApps: true, }, }; export const Connecting: Story = { args: { agent: M.MockWorkspaceAgentConnecting, - storybookAgentMetadata: [], + initialMetadata: [], }, }; @@ -190,7 +172,7 @@ export const Started: Story = { export const StartedNoMetadata: Story = { args: { ...Started.args, - storybookAgentMetadata: [], + initialMetadata: [], }, }; @@ -243,20 +225,30 @@ export const ShowingPortForward: Story = { }; export const Outdated: Story = { + beforeEach: () => { + spyOn(API, "getBuildInfo").mockResolvedValue({ + ...M.MockBuildInfo, + version: "v99.999.9999+c1cdf14", + agent_api_version: "1.0", + }); + }, args: { agent: M.MockWorkspaceAgentOutdated, workspace: M.MockWorkspace, - serverVersion: "v99.999.9999+c1cdf14", - serverAPIVersion: "1.0", }, }; export const Deprecated: Story = { + beforeEach: () => { + spyOn(API, "getBuildInfo").mockResolvedValue({ + ...M.MockBuildInfo, + version: "v99.999.9999+c1cdf14", + agent_api_version: "2.0", + }); + }, args: { agent: M.MockWorkspaceAgentDeprecated, workspace: M.MockWorkspace, - serverVersion: "v99.999.9999+c1cdf14", - serverAPIVersion: "2.0", }, }; diff --git a/site/src/modules/resources/AgentRow.test.tsx b/site/src/modules/resources/AgentRow.test.tsx deleted file mode 100644 index 55be57bbc2c2b..0000000000000 --- a/site/src/modules/resources/AgentRow.test.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { screen } from "@testing-library/react"; -import { - MockTemplate, - MockWorkspace, - MockWorkspaceAgent, - MockWorkspaceApp, -} from "testHelpers/entities"; -import { - renderWithAuth, - waitForLoaderToBeRemoved, -} from "testHelpers/renderHelpers"; -import type { AgentRowProps } from "./AgentRow"; -import { AgentRow } from "./AgentRow"; -import { DisplayAppNameMap } from "./AppLink/AppLink"; - -jest.mock("modules/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, - template: MockTemplate, - showApps: false, - serverVersion: "", - serverAPIVersion: "", - onUpdateAgent: () => { - 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(); - } - }); -}); - -describe.each<{ - props: Partial; -}>([ - { - props: { - agent: { - ...MockWorkspaceAgent, - apps: [ - { - ...MockWorkspaceApp, - display_name: `${MockWorkspaceApp.display_name} Not Hidden`, - hidden: false, - }, - { - ...MockWorkspaceApp, - display_name: `${MockWorkspaceApp.display_name} Is Hidden`, - hidden: true, - }, - ], - }, - }, - }, -])("hidden hides App button", ({ props: testProps }) => { - const props: AgentRowProps = { - agent: MockWorkspaceAgent, - workspace: MockWorkspace, - template: MockTemplate, - showApps: true, - serverVersion: "", - serverAPIVersion: "", - onUpdateAgent: () => { - throw new Error("Function not implemented."); - }, - ...testProps, - }; - - test(`apps: ${props.agent.apps}`, async () => { - renderWithAuth(); - await waitForLoaderToBeRemoved(); - - for (const app of props.agent.apps) { - if (app.hidden) { - expect(screen.queryByText(app.display_name as string)).toBeNull(); - } else { - expect(screen.getByText(app.display_name as string)).toBeVisible(); - } - } - }); -}); diff --git a/site/src/modules/resources/AgentRow.tsx b/site/src/modules/resources/AgentRow.tsx index 40b6dcf3bea71..b8a80885eca62 100644 --- a/site/src/modules/resources/AgentRow.tsx +++ b/site/src/modules/resources/AgentRow.tsx @@ -15,6 +15,7 @@ import { DropdownArrow } from "components/DropdownArrow/DropdownArrow"; import type { Line } from "components/Logs/LogLine"; import { Stack } from "components/Stack/Stack"; import { useProxy } from "contexts/ProxyContext"; +import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import { AppStatuses } from "pages/WorkspacePage/AppStatuses"; import { type FC, @@ -43,46 +44,32 @@ import { TerminalLink } from "./TerminalLink/TerminalLink"; import { VSCodeDesktopButton } from "./VSCodeDesktopButton/VSCodeDesktopButton"; import { useAgentLogs } from "./useAgentLogs"; -export interface AgentRowProps { +interface AgentRowProps { agent: WorkspaceAgent; workspace: Workspace; - showApps: boolean; - showBuiltinApps?: boolean; - sshPrefix?: string; - hideSSHButton?: boolean; - hideVSCodeDesktopButton?: boolean; - serverVersion: string; - serverAPIVersion: string; - onUpdateAgent: () => void; template: Template; - storybookAgentMetadata?: WorkspaceAgentMetadata[]; + initialMetadata?: WorkspaceAgentMetadata[]; + onUpdateAgent: () => void; } export const AgentRow: FC = ({ agent, workspace, template, - showApps, - showBuiltinApps = true, - hideSSHButton, - hideVSCodeDesktopButton, - serverVersion, - serverAPIVersion, onUpdateAgent, - storybookAgentMetadata, - sshPrefix, + initialMetadata, }) => { // Apps visibility + const { browser_only } = useFeatureVisibility(); const visibleApps = agent.apps.filter((app) => !app.hidden); - const hasAppsToDisplay = !hideVSCodeDesktopButton || visibleApps.length > 0; + const hasAppsToDisplay = !browser_only && visibleApps.length > 0; const shouldDisplayApps = - showApps && - ((agent.status === "connected" && hasAppsToDisplay) || - agent.status === "connecting"); + (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 showVSCode = hasVSCodeApp && !browser_only; const hasStartupFeatures = Boolean(agent.logs_length); const { proxy } = useProxy(); @@ -184,12 +171,7 @@ export const AgentRow: FC = ({ {agent.status === "connected" && ( <> - + )} @@ -201,28 +183,23 @@ export const AgentRow: FC = ({ )} - {showBuiltinApps && ( -
- {!hideSSHButton && agent.display_apps.includes("ssh_helper") && ( - + {!browser_only && agent.display_apps.includes("ssh_helper") && ( + + )} + {proxy.preferredWildcardHostname !== "" && + agent.display_apps.includes("port_forwarding_helper") && ( + )} - {proxy.preferredWildcardHostname !== "" && - agent.display_apps.includes("port_forwarding_helper") && ( - - )} -
- )} +
@@ -257,7 +234,7 @@ export const AgentRow: FC = ({ )} - {showBuiltinApps && agent.display_apps.includes("web_terminal") && ( + {agent.display_apps.includes("web_terminal") && ( = ({ )} - +
{hasStartupFeatures && ( diff --git a/site/src/modules/resources/AgentVersion.tsx b/site/src/modules/resources/AgentVersion.tsx index 744bb7077a36e..0111bdde1f379 100644 --- a/site/src/modules/resources/AgentVersion.tsx +++ b/site/src/modules/resources/AgentVersion.tsx @@ -1,26 +1,27 @@ +import { buildInfo } from "api/queries/buildInfo"; import type { WorkspaceAgent } from "api/typesGenerated"; +import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; import type { FC } from "react"; +import { useQuery } from "react-query"; import { agentVersionStatus, getDisplayVersionStatus } from "utils/workspace"; import { AgentOutdatedTooltip } from "./AgentOutdatedTooltip"; interface AgentVersionProps { agent: WorkspaceAgent; - serverVersion: string; - serverAPIVersion: string; onUpdate: () => void; } -export const AgentVersion: FC = ({ - agent, - serverVersion, - serverAPIVersion, - onUpdate, -}) => { +export const AgentVersion: FC = ({ agent, onUpdate }) => { + const { metadata } = useEmbeddedMetadata(); + const { data: build } = useQuery(buildInfo(metadata["build-info"])); + const serverVersion = build?.version ?? ""; + const apiServerVersion = build?.agent_api_version ?? ""; + const { status } = getDisplayVersionStatus( agent.version, serverVersion, agent.api_version, - serverAPIVersion, + apiServerVersion, ); if (status === agentVersionStatus.Updated) { diff --git a/site/src/modules/resources/PortForwardButton.stories.tsx b/site/src/modules/resources/PortForwardButton.stories.tsx index 76de2903e83cf..09cfcc8796b51 100644 --- a/site/src/modules/resources/PortForwardButton.stories.tsx +++ b/site/src/modules/resources/PortForwardButton.stories.tsx @@ -2,6 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { MockListeningPortsResponse, MockSharedPortsResponse, + MockTemplate, MockWorkspace, MockWorkspaceAgent, } from "testHelpers/entities"; @@ -14,6 +15,8 @@ const meta: Meta = { decorators: [withDashboardProvider], args: { agent: MockWorkspaceAgent, + workspace: MockWorkspace, + template: MockTemplate, }, }; diff --git a/site/src/modules/resources/PortForwardButton.tsx b/site/src/modules/resources/PortForwardButton.tsx index 916c8f2338130..2a3f5da93173c 100644 --- a/site/src/modules/resources/PortForwardButton.tsx +++ b/site/src/modules/resources/PortForwardButton.tsx @@ -17,9 +17,10 @@ import { } from "api/queries/workspaceportsharing"; import { type Template, - type UpsertWorkspaceAgentPortShareRequest, + type Workspace, type WorkspaceAgent, type WorkspaceAgentListeningPort, + type WorkspaceAgentPortShare, type WorkspaceAgentPortShareLevel, type WorkspaceAgentPortShareProtocol, WorkspaceAppSharingLevels, @@ -42,7 +43,7 @@ import { PopoverContent, PopoverTrigger, } from "components/deprecated/Popover/Popover"; -import { type FormikContextType, useFormik } from "formik"; +import { useFormik } from "formik"; import { type ClassName, useClassName } from "hooks/useClassName"; import { ChevronDownIcon, @@ -64,31 +65,40 @@ import * as Yup from "yup"; interface PortForwardButtonProps { host: string; - username: string; - workspaceName: string; - workspaceID: string; + workspace: Workspace; agent: WorkspaceAgent; template: Template; } -export const PortForwardButton: FC = (props) => { - const { agent } = props; +export const PortForwardButton: FC = ({ + host, + workspace, + template, + agent, +}) => { const { entitlements } = useDashboard(); const paper = useClassName(classNames.paper, []); - const portsQuery = useQuery({ + const { data: listeningPorts } = useQuery({ queryKey: ["portForward", agent.id], queryFn: () => API.getAgentListeningPorts(agent.id), enabled: agent.status === "connected", refetchInterval: 5_000, + select: (res) => res.ports, + }); + + const { data: sharedPorts, refetch: refetchSharedPorts } = useQuery({ + ...workspacePortShares(workspace.id), + enabled: agent.status === "connected", + select: (res) => res.shares, }); return ( -