From 2cead422d06d73a59d02296a894dc050129cf288 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 24 Jan 2024 17:24:49 +0000 Subject: [PATCH 01/16] init --- site/src/components/Popover/Popover.tsx | 2 +- .../modules/resources/PortForwardButton.tsx | 216 ++++++++++++++++-- site/src/testHelpers/entities.ts | 4 +- 3 files changed, 199 insertions(+), 23 deletions(-) diff --git a/site/src/components/Popover/Popover.tsx b/site/src/components/Popover/Popover.tsx index d3af0d98bb775..f910be5e1fc89 100644 --- a/site/src/components/Popover/Popover.tsx +++ b/site/src/components/Popover/Popover.tsx @@ -151,7 +151,7 @@ export const PopoverContent: FC = ({ marginTop: hoverMode ? undefined : 8, pointerEvents: hoverMode ? "none" : undefined, "& .MuiPaper-root": { - minWidth: 320, + minWidth: 520, fontSize: 14, pointerEvents: hoverMode ? "auto" : undefined, }, diff --git a/site/src/modules/resources/PortForwardButton.tsx b/site/src/modules/resources/PortForwardButton.tsx index 40a9cc11dc624..e5eaf2d4f6659 100644 --- a/site/src/modules/resources/PortForwardButton.tsx +++ b/site/src/modules/resources/PortForwardButton.tsx @@ -26,6 +26,17 @@ import { PopoverTrigger, } from "components/Popover/Popover"; import KeyboardArrowDown from "@mui/icons-material/KeyboardArrowDown"; +import Stack from "@mui/material/Stack"; +import Select from "@mui/material/Select"; +import MenuItem from "@mui/material/MenuItem"; +import FormControl from "@mui/material/FormControl"; +import TextField from "@mui/material/TextField"; +import SensorsIcon from '@mui/icons-material/Sensors'; +import Add from '@mui/icons-material/Add'; +import IconButton from "@mui/material/IconButton"; +import LockIcon from '@mui/icons-material/Lock'; +import LockOpenIcon from '@mui/icons-material/LockOpen'; +import DeleteIcon from '@mui/icons-material/Delete'; export interface PortForwardButtonProps { host: string; @@ -96,6 +107,16 @@ export const PortForwardPopoverView: FC = ({ ports, }) => { const theme = useTheme(); + const sharedPorts = [ + { + port: 8090, + share_level: "Authenticated", + }, + { + port: 8091, + share_level: "Public", + } + ]; return ( <> @@ -105,13 +126,69 @@ export const PortForwardPopoverView: FC = ({ borderBottom: `1px solid ${theme.palette.divider}`, }} > - Forwarded ports + + Listening ports + + Learn more + + {ports?.length === 0 ? "No open ports were detected." - : "The forwarded ports are exclusively accessible to you."} + : "The listening ports are exclusively accessible to you." + } + -
+
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const port = Number(formData.get("portNumber")); + const url = portForwardURL( + host, + port, + agent.name, + workspaceName, + username, + ); + window.open(url, "_blank"); + }} + > + + +
+
{ports?.map((port) => { const url = portForwardURL( host, @@ -123,24 +200,123 @@ export const PortForwardPopoverView: FC = ({ const label = port.process_name !== "" ? port.process_name : port.port; return ( - - - {label} - {port.port} - + + + + {label} + + + {port.port} + + + ); })}
+
+
+ Shared Ports + + {ports?.length === 0 + ? "No ports are shared." + : "Ports can be shared with other Coder users or with the public."} + +
+ {sharedPorts?.map((port) => { + const url = portForwardURL( + host, + port.port, + agent.name, + workspaceName, + username, + ); + const label = port.port; + return ( + + + {port.share_level === "Public" ? + ( + + ) + : ( + + )} + {label} + + + + + + + + + ); + })} +
+ + + + + + +
-
+ + {/*
Forward port Access ports running on the agent: @@ -198,7 +374,7 @@ export const PortForwardPopoverView: FC = ({ Learn more -
+
*/} ); }; @@ -232,8 +408,8 @@ const styles = { display: "flex", alignItems: "center", gap: 8, - paddingTop: 4, - paddingBottom: 4, + paddingTop: 8, + paddingBottom: 8, fontWeight: 500, }), @@ -247,7 +423,7 @@ const styles = { newPortForm: (theme) => ({ border: `1px solid ${theme.palette.divider}`, borderRadius: "4px", - marginTop: 16, + marginTop: 8, display: "flex", alignItems: "center", "&:focus-within": { diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index ce4f9d836aa0e..f116dd7ff834f 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -3238,8 +3238,8 @@ export const MockHealth: TypesGen.HealthcheckReport = { export const MockListeningPortsResponse: TypesGen.WorkspaceAgentListeningPortsResponse = { ports: [ - { process_name: "web", network: "", port: 3000 }, - { process_name: "go", network: "", port: 8080 }, + { process_name: "webb", network: "", port: 3000 }, + { process_name: "gogo", network: "", port: 8080 }, { process_name: "", network: "", port: 8081 }, ], }; From fb23e77eb14f2805b01341b5002c584ea35f09c6 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 8 Feb 2024 20:07:55 +0000 Subject: [PATCH 02/16] add query: --- site/src/api/api.ts | 9 + site/src/components/Popover/Popover.tsx | 2 +- site/src/modules/resources/AgentRow.tsx | 1 + .../resources/PortForwardButton.stories.tsx | 2 +- .../modules/resources/PortForwardButton.tsx | 198 ++++++++++-------- .../PortForwardPopoverView.stories.tsx | 7 +- site/src/testHelpers/entities.ts | 16 ++ 7 files changed, 140 insertions(+), 95 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 0a2e421ecc891..17cab9a7eef83 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1163,6 +1163,15 @@ export const getAgentListeningPorts = async ( return response.data; }; +export const getWorkspaceAgentSharedPorts = async ( + workspaceID: string, +): Promise => { + const response = await axios.get( + `/api/v2/workspaces/${workspaceID}/shared-ports`, + ); + return response.data; +}; + // getDeploymentSSHConfig is used by the VSCode-Extension. export const getDeploymentSSHConfig = async (): Promise => { diff --git a/site/src/components/Popover/Popover.tsx b/site/src/components/Popover/Popover.tsx index f910be5e1fc89..d3af0d98bb775 100644 --- a/site/src/components/Popover/Popover.tsx +++ b/site/src/components/Popover/Popover.tsx @@ -151,7 +151,7 @@ export const PopoverContent: FC = ({ marginTop: hoverMode ? undefined : 8, pointerEvents: hoverMode ? "none" : undefined, "& .MuiPaper-root": { - minWidth: 520, + minWidth: 320, fontSize: 14, pointerEvents: hoverMode ? "auto" : undefined, }, diff --git a/site/src/modules/resources/AgentRow.tsx b/site/src/modules/resources/AgentRow.tsx index b20ad6649dfaf..73b81bc9338c2 100644 --- a/site/src/modules/resources/AgentRow.tsx +++ b/site/src/modules/resources/AgentRow.tsx @@ -220,6 +220,7 @@ export const AgentRow: FC = ({ workspaceName={workspace.name} agent={agent} username={workspace.owner_name} + workspaceID={workspace.id} /> )} diff --git a/site/src/modules/resources/PortForwardButton.stories.tsx b/site/src/modules/resources/PortForwardButton.stories.tsx index 8ff4321c9135e..3417aa959d108 100644 --- a/site/src/modules/resources/PortForwardButton.stories.tsx +++ b/site/src/modules/resources/PortForwardButton.stories.tsx @@ -19,7 +19,7 @@ type Story = StoryObj; export const Example: Story = { args: { storybook: { - portsQueryData: MockListeningPortsResponse, + listeningPortsQueryData: MockListeningPortsResponse, }, }, }; diff --git a/site/src/modules/resources/PortForwardButton.tsx b/site/src/modules/resources/PortForwardButton.tsx index e5eaf2d4f6659..6e656f6cfbfb2 100644 --- a/site/src/modules/resources/PortForwardButton.tsx +++ b/site/src/modules/resources/PortForwardButton.tsx @@ -6,17 +6,18 @@ import { type Interpolation, type Theme, useTheme } from "@emotion/react"; import type { FC } from "react"; import { useQuery } from "react-query"; import { docs } from "utils/docs"; -import { getAgentListeningPorts } from "api/api"; +import { getAgentListeningPorts, getWorkspaceAgentSharedPorts } from "api/api"; import type { WorkspaceAgent, WorkspaceAgentListeningPort, WorkspaceAgentListeningPortsResponse, + WorkspaceAgentPortShare, + WorkspaceAgentPortShares, } from "api/typesGenerated"; import { portForwardURL } from "utils/portForward"; import { type ClassName, useClassName } from "hooks/useClassName"; import { HelpTooltipLink, - HelpTooltipLinksGroup, HelpTooltipText, HelpTooltipTitle, } from "components/HelpTooltip/HelpTooltip"; @@ -31,29 +32,28 @@ import Select from "@mui/material/Select"; import MenuItem from "@mui/material/MenuItem"; import FormControl from "@mui/material/FormControl"; import TextField from "@mui/material/TextField"; -import SensorsIcon from '@mui/icons-material/Sensors'; -import Add from '@mui/icons-material/Add'; -import IconButton from "@mui/material/IconButton"; -import LockIcon from '@mui/icons-material/Lock'; -import LockOpenIcon from '@mui/icons-material/LockOpen'; -import DeleteIcon from '@mui/icons-material/Delete'; +import SensorsIcon from "@mui/icons-material/Sensors"; +import LockIcon from "@mui/icons-material/Lock"; +import LockOpenIcon from "@mui/icons-material/LockOpen"; export interface PortForwardButtonProps { host: string; username: string; workspaceName: string; + workspaceID: string; agent: WorkspaceAgent; /** * Only for use in Storybook */ storybook?: { - portsQueryData?: WorkspaceAgentListeningPortsResponse; + listeningPortsQueryData?: WorkspaceAgentListeningPortsResponse; + sharedPortsQueryData?: WorkspaceAgentPortShares; }; } export const PortForwardButton: FC = (props) => { - const { agent, storybook } = props; + const { agent, workspaceID, storybook } = props; const paper = useClassName(classNames.paper, []); @@ -64,21 +64,34 @@ export const PortForwardButton: FC = (props) => { refetchInterval: 5_000, }); - const data = storybook ? storybook.portsQueryData : portsQuery.data; + const sharedPortsQuery = useQuery({ + queryKey: ["sharedPorts", agent.id], + queryFn: () => getWorkspaceAgentSharedPorts(workspaceID), + enabled: !storybook && agent.status === "connected", + }); + + const listeningPorts = storybook + ? storybook.listeningPortsQueryData + : portsQuery.data; + const sharedPorts = storybook + ? storybook.sharedPortsQueryData + : sharedPortsQuery.data; return ( - + ); }; interface PortForwardPopoverViewProps extends PortForwardButtonProps { - ports?: WorkspaceAgentListeningPort[]; + listeningPorts?: WorkspaceAgentListeningPort[]; + sharedPorts?: WorkspaceAgentPortShare[]; } export const PortForwardPopoverView: FC = ({ @@ -104,19 +122,10 @@ export const PortForwardPopoverView: FC = ({ workspaceName, agent, username, - ports, + listeningPorts, + sharedPorts, }) => { const theme = useTheme(); - const sharedPorts = [ - { - port: 8090, - share_level: "Authenticated", - }, - { - port: 8091, - share_level: "Public", - } - ]; return ( <> @@ -126,18 +135,20 @@ export const PortForwardPopoverView: FC = ({ borderBottom: `1px solid ${theme.palette.divider}`, }} > - - Listening ports + + Listening ports Learn more - {ports?.length === 0 + {listeningPorts?.length === 0 ? "No open ports were detected." - : "The listening ports are exclusively accessible to you." - } - + : "The listening ports are exclusively accessible to you."}
= ({
- {ports?.map((port) => { + css={{ + paddingTop: 10, + }} + > + {listeningPorts?.map((port) => { const url = portForwardURL( host, port.port, @@ -200,7 +212,12 @@ export const PortForwardPopoverView: FC = ({ const label = port.process_name !== "" ? port.process_name : port.port; return ( - + = ({ ); })}
- -
+
+ }} + > Shared Ports - {ports?.length === 0 + {listeningPorts?.length === 0 ? "No ports are shared." : "Ports can be shared with other Coder users or with the public."}
- {sharedPorts?.map((port) => { + {sharedPorts?.map((share) => { const url = portForwardURL( host, - port.port, + share.port, agent.name, workspaceName, username, ); - const label = port.port; + const label = share.port; return ( - + = ({ target="_blank" rel="noreferrer" > - {port.share_level === "Public" ? - ( + {share.share_level === "public" ? ( - ) - : ( + ) : ( )} {label} - - - + + + - ); })} -
- - +
+ + - + - +
- {/*
Forward port diff --git a/site/src/modules/resources/PortForwardPopoverView.stories.tsx b/site/src/modules/resources/PortForwardPopoverView.stories.tsx index 95b5e6a770580..82fdfed6352f8 100644 --- a/site/src/modules/resources/PortForwardPopoverView.stories.tsx +++ b/site/src/modules/resources/PortForwardPopoverView.stories.tsx @@ -2,6 +2,7 @@ import { PortForwardPopoverView } from "./PortForwardButton"; import type { Meta, StoryObj } from "@storybook/react"; import { MockListeningPortsResponse, + MockSharedPortsResponse, MockWorkspaceAgent, } from "testHelpers/entities"; @@ -32,12 +33,14 @@ type Story = StoryObj; export const WithPorts: Story = { args: { - ports: MockListeningPortsResponse.ports, + listeningPorts: MockListeningPortsResponse.ports, + sharedPorts: MockSharedPortsResponse.shares, }, }; export const Empty: Story = { args: { - ports: [], + listeningPorts: [], + sharedPorts: [], }, }; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index f116dd7ff834f..cd06f286725bc 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -3244,6 +3244,22 @@ export const MockListeningPortsResponse: TypesGen.WorkspaceAgentListeningPortsRe ], }; +export const MockSharedPortsResponse: TypesGen.WorkspaceAgentPortShares = { + shares: [ + { + agent_name: "a-workspace-agent", + port: 4000, + share_level: "authenticated", + }, + { + agent_name: "a-workspace-agent", + port: 8080, + share_level: "authenticated", + }, + { agent_name: "a-workspace-agent", port: 8081, share_level: "public" }, + ], +}; + export const DeploymentHealthUnhealthy: TypesGen.HealthcheckReport = { healthy: false, severity: "ok", From c9c18d1a5b60924f6eb167bf3d6565d01f9dc03a Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 8 Feb 2024 22:01:59 +0000 Subject: [PATCH 03/16] style --- .../modules/resources/PortForwardButton.tsx | 124 ++++++------------ 1 file changed, 38 insertions(+), 86 deletions(-) diff --git a/site/src/modules/resources/PortForwardButton.tsx b/site/src/modules/resources/PortForwardButton.tsx index 6e656f6cfbfb2..665821586e2b6 100644 --- a/site/src/modules/resources/PortForwardButton.tsx +++ b/site/src/modules/resources/PortForwardButton.tsx @@ -35,6 +35,9 @@ import TextField from "@mui/material/TextField"; import SensorsIcon from "@mui/icons-material/Sensors"; import LockIcon from "@mui/icons-material/Lock"; import LockOpenIcon from "@mui/icons-material/LockOpen"; +import IconButton from "@mui/material/IconButton"; +import CloseIcon from "@mui/icons-material/Close"; +import Grid from "@mui/material/Grid"; export interface PortForwardButtonProps { host: string; @@ -215,8 +218,8 @@ export const PortForwardPopoverView: FC = ({ = ({ {label} + = ({ + ); })} @@ -252,9 +257,7 @@ export const PortForwardPopoverView: FC = ({ > Shared Ports - {listeningPorts?.length === 0 - ? "No ports are shared." - : "Ports can be shared with other Coder users or with the public."} + Ports can be shared with other Coder users or with the public.
{sharedPorts?.map((share) => { @@ -287,27 +290,36 @@ export const PortForwardPopoverView: FC = ({ )} {label} - - - - + + + + + + + ); @@ -328,69 +340,9 @@ export const PortForwardPopoverView: FC = ({ Public - +
- - {/*
- Forward port - - Access ports running on the agent: - - -
{ - e.preventDefault(); - const formData = new FormData(e.currentTarget); - const port = Number(formData.get("portNumber")); - const url = portForwardURL( - host, - port, - agent.name, - workspaceName, - username, - ); - window.open(url, "_blank"); - }} - > - - -
- - - - Learn more - - -
*/} ); }; From 37009c77189c07cb0d7235f61f496ea951fec7f5 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 8 Feb 2024 22:09:19 +0000 Subject: [PATCH 04/16] add filter --- .../modules/resources/PortForwardButton.tsx | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/site/src/modules/resources/PortForwardButton.tsx b/site/src/modules/resources/PortForwardButton.tsx index 665821586e2b6..2a5b90d32d322 100644 --- a/site/src/modules/resources/PortForwardButton.tsx +++ b/site/src/modules/resources/PortForwardButton.tsx @@ -130,6 +130,23 @@ export const PortForwardPopoverView: FC = ({ }) => { const theme = useTheme(); + // we don't want to show listening ports if it's already a shared port + const filteredListeningPorts = listeningPorts?.filter( + (port) => { + if (sharedPorts === undefined) { + return true; + } + + for (let i = 0; i < sharedPorts.length; i++) { + if (sharedPorts[i].port === port.port && sharedPorts[i].agent_name === agent.name) { + return false; + } + } + + return true; + } + ); + return ( <>
= ({ - {listeningPorts?.length === 0 + {filteredListeningPorts?.length === 0 ? "No open ports were detected." : "The listening ports are exclusively accessible to you."} @@ -204,7 +221,7 @@ export const PortForwardPopoverView: FC = ({ paddingTop: 10, }} > - {listeningPorts?.map((port) => { + {filteredListeningPorts?.map((port) => { const url = portForwardURL( host, port.port, From 7839a2c72bee1f0a3c8aecfc286e67356375153e Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 9 Feb 2024 00:58:48 +0000 Subject: [PATCH 05/16] Add mutations --- site/src/api/api.ts | 24 ++++++++++ .../modules/resources/PortForwardButton.tsx | 48 +++++++++++++++---- 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 17cab9a7eef83..a34b3fa13fe01 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1172,6 +1172,30 @@ export const getWorkspaceAgentSharedPorts = async ( return response.data; }; +export const postWorkspaceAgentSharedPort = async ( + workspaceID: string, + req: TypesGen.UpdateWorkspaceAgentPortShareRequest, +): Promise => { + const response = await axios.post( + `/api/v2/workspaces/${workspaceID}/shared-port`, + req + ); + return response.data; +}; + +export const deleteWorkspaceAgentSharedPort = async ( + workspaceID: string, + req: TypesGen.DeleteWorkspaceAgentPortShareRequest, +): Promise => { + const response = await axios.delete( + `/api/v2/workspaces/${workspaceID}/shared-port`, + { + data: req, + } + ); + return response.data; +}; + // getDeploymentSSHConfig is used by the VSCode-Extension. export const getDeploymentSSHConfig = async (): Promise => { diff --git a/site/src/modules/resources/PortForwardButton.tsx b/site/src/modules/resources/PortForwardButton.tsx index 2a5b90d32d322..765a82a9c6126 100644 --- a/site/src/modules/resources/PortForwardButton.tsx +++ b/site/src/modules/resources/PortForwardButton.tsx @@ -4,14 +4,17 @@ import CircularProgress from "@mui/material/CircularProgress"; import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined"; import { type Interpolation, type Theme, useTheme } from "@emotion/react"; import type { FC } from "react"; -import { useQuery } from "react-query"; +import { useQuery, useMutation } from "react-query"; import { docs } from "utils/docs"; -import { getAgentListeningPorts, getWorkspaceAgentSharedPorts } from "api/api"; +import { deleteWorkspaceAgentSharedPort, getAgentListeningPorts, getWorkspaceAgentSharedPorts, postWorkspaceAgentSharedPort } from "api/api"; import type { + DeleteWorkspaceAgentPortShareRequest, + UpdateWorkspaceAgentPortShareRequest, WorkspaceAgent, WorkspaceAgentListeningPort, WorkspaceAgentListeningPortsResponse, WorkspaceAgentPortShare, + WorkspaceAgentPortShareLevel, WorkspaceAgentPortShares, } from "api/typesGenerated"; import { portForwardURL } from "utils/portForward"; @@ -37,7 +40,6 @@ import LockIcon from "@mui/icons-material/Lock"; import LockOpenIcon from "@mui/icons-material/LockOpen"; import IconButton from "@mui/material/IconButton"; import CloseIcon from "@mui/icons-material/Close"; -import Grid from "@mui/material/Grid"; export interface PortForwardButtonProps { host: string; @@ -68,7 +70,7 @@ export const PortForwardButton: FC = (props) => { }); const sharedPortsQuery = useQuery({ - queryKey: ["sharedPorts", agent.id], + queryKey: ["sharedPorts", workspaceID], queryFn: () => getWorkspaceAgentSharedPorts(workspaceID), enabled: !storybook && agent.status === "connected", }); @@ -123,6 +125,7 @@ interface PortForwardPopoverViewProps extends PortForwardButtonProps { export const PortForwardPopoverView: FC = ({ host, workspaceName, + workspaceID, agent, username, listeningPorts, @@ -130,6 +133,19 @@ export const PortForwardPopoverView: FC = ({ }) => { const theme = useTheme(); + + const createSharedPortMutation = useMutation({ + mutationFn: async (options: UpdateWorkspaceAgentPortShareRequest) => { + await postWorkspaceAgentSharedPort(workspaceID, options); + }, + }); + + const deleteSharedPortMutation = useMutation({ + mutationFn: async (options: DeleteWorkspaceAgentPortShareRequest) => { + await deleteWorkspaceAgentSharedPort(workspaceID, options); + }, + }); + // we don't want to show listening ports if it's already a shared port const filteredListeningPorts = listeningPorts?.filter( (port) => { @@ -258,7 +274,15 @@ export const PortForwardPopoverView: FC = ({ > {port.port} - @@ -328,7 +352,12 @@ export const PortForwardPopoverView: FC = ({ Public - + { + deleteSharedPortMutation.mutate({ + agent_name: agent.name, + port: share.port, + }); + }}> = ({ > - + Authenticated + Public + {/* How do I use the value from the select in the mutation? */}
From 174214ac4ca319bb4338b16055dc25ca76f0ea1c Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 14 Feb 2024 21:08:12 +0000 Subject: [PATCH 06/16] get form working --- coderd/workspaceagentportshare.go | 2 +- site/src/api/api.ts | 10 +-- .../resources/PortForwardButton.stories.tsx | 2 + .../modules/resources/PortForwardButton.tsx | 75 ++++++++++++------- .../PortForwardPopoverView.stories.tsx | 6 +- .../TemplateSettingsForm.tsx | 48 +++++++++++- .../TemplateSettingsPageView.tsx | 2 + site/src/testHelpers/entities.ts | 4 +- 8 files changed, 110 insertions(+), 39 deletions(-) diff --git a/coderd/workspaceagentportshare.go b/coderd/workspaceagentportshare.go index 545a417368a2c..730089e445431 100644 --- a/coderd/workspaceagentportshare.go +++ b/coderd/workspaceagentportshare.go @@ -156,7 +156,7 @@ func (api *API) deleteWorkspaceAgentPortShare(rw http.ResponseWriter, r *http.Re } func convertPortShares(shares []database.WorkspaceAgentPortShare) []codersdk.WorkspaceAgentPortShare { - var converted []codersdk.WorkspaceAgentPortShare + converted := []codersdk.WorkspaceAgentPortShare{} for _, share := range shares { converted = append(converted, convertPortShare(share)) } diff --git a/site/src/api/api.ts b/site/src/api/api.ts index a34b3fa13fe01..83a2008e403c8 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1167,17 +1167,17 @@ export const getWorkspaceAgentSharedPorts = async ( workspaceID: string, ): Promise => { const response = await axios.get( - `/api/v2/workspaces/${workspaceID}/shared-ports`, + `/api/v2/workspaces/${workspaceID}/port-share`, ); return response.data; }; -export const postWorkspaceAgentSharedPort = async ( +export const upsertWorkspaceAgentSharedPort = async ( workspaceID: string, - req: TypesGen.UpdateWorkspaceAgentPortShareRequest, + req: TypesGen.UpsertWorkspaceAgentPortShareRequest, ): Promise => { const response = await axios.post( - `/api/v2/workspaces/${workspaceID}/shared-port`, + `/api/v2/workspaces/${workspaceID}/port-share`, req ); return response.data; @@ -1188,7 +1188,7 @@ export const deleteWorkspaceAgentSharedPort = async ( req: TypesGen.DeleteWorkspaceAgentPortShareRequest, ): Promise => { const response = await axios.delete( - `/api/v2/workspaces/${workspaceID}/shared-port`, + `/api/v2/workspaces/${workspaceID}/port-share`, { data: req, } diff --git a/site/src/modules/resources/PortForwardButton.stories.tsx b/site/src/modules/resources/PortForwardButton.stories.tsx index 3417aa959d108..c8f4e28a58410 100644 --- a/site/src/modules/resources/PortForwardButton.stories.tsx +++ b/site/src/modules/resources/PortForwardButton.stories.tsx @@ -2,6 +2,7 @@ import { PortForwardButton } from "./PortForwardButton"; import type { Meta, StoryObj } from "@storybook/react"; import { MockListeningPortsResponse, + MockSharedPortsResponse, MockWorkspaceAgent, } from "testHelpers/entities"; @@ -20,6 +21,7 @@ export const Example: Story = { args: { storybook: { listeningPortsQueryData: MockListeningPortsResponse, + sharedPortsQueryData: MockSharedPortsResponse, }, }, }; diff --git a/site/src/modules/resources/PortForwardButton.tsx b/site/src/modules/resources/PortForwardButton.tsx index 765a82a9c6126..c9fd2db52af3f 100644 --- a/site/src/modules/resources/PortForwardButton.tsx +++ b/site/src/modules/resources/PortForwardButton.tsx @@ -3,17 +3,16 @@ import Link from "@mui/material/Link"; import CircularProgress from "@mui/material/CircularProgress"; import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined"; import { type Interpolation, type Theme, useTheme } from "@emotion/react"; -import type { FC } from "react"; +import { useState, type FC } from "react"; import { useQuery, useMutation } from "react-query"; import { docs } from "utils/docs"; -import { deleteWorkspaceAgentSharedPort, getAgentListeningPorts, getWorkspaceAgentSharedPorts, postWorkspaceAgentSharedPort } from "api/api"; +import { deleteWorkspaceAgentSharedPort, getAgentListeningPorts, getWorkspaceAgentSharedPorts, upsertWorkspaceAgentSharedPort } from "api/api"; import type { DeleteWorkspaceAgentPortShareRequest, - UpdateWorkspaceAgentPortShareRequest, + UpsertWorkspaceAgentPortShareRequest, WorkspaceAgent, WorkspaceAgentListeningPort, WorkspaceAgentListeningPortsResponse, - WorkspaceAgentPortShare, WorkspaceAgentPortShareLevel, WorkspaceAgentPortShares, } from "api/typesGenerated"; @@ -58,7 +57,7 @@ export interface PortForwardButtonProps { } export const PortForwardButton: FC = (props) => { - const { agent, workspaceID, storybook } = props; + const { agent, storybook } = props; const paper = useClassName(classNames.paper, []); @@ -69,18 +68,9 @@ export const PortForwardButton: FC = (props) => { refetchInterval: 5_000, }); - const sharedPortsQuery = useQuery({ - queryKey: ["sharedPorts", workspaceID], - queryFn: () => getWorkspaceAgentSharedPorts(workspaceID), - enabled: !storybook && agent.status === "connected", - }); - const listeningPorts = storybook ? storybook.listeningPortsQueryData : portsQuery.data; - const sharedPorts = storybook - ? storybook.sharedPortsQueryData - : sharedPortsQuery.data; return ( @@ -110,7 +100,6 @@ export const PortForwardButton: FC = (props) => { @@ -119,7 +108,6 @@ export const PortForwardButton: FC = (props) => { interface PortForwardPopoverViewProps extends PortForwardButtonProps { listeningPorts?: WorkspaceAgentListeningPort[]; - sharedPorts?: WorkspaceAgentPortShare[]; } export const PortForwardPopoverView: FC = ({ @@ -129,14 +117,24 @@ export const PortForwardPopoverView: FC = ({ agent, username, listeningPorts, - sharedPorts, + storybook, }) => { const theme = useTheme(); + const [selectedShareLevel, setSelectedShareLevel] = useState("authenticated"); + const [selectedPort, setSelectedPort] = useState(""); + const sharedPortsQuery = useQuery({ + queryKey: ["sharedPorts", workspaceID], + queryFn: () => getWorkspaceAgentSharedPorts(workspaceID), + enabled: !storybook && agent.status === "connected", + }); + const sharedPorts = storybook + ? storybook.sharedPortsQueryData?.shares || [] + : sharedPortsQuery.data?.shares || []; const createSharedPortMutation = useMutation({ - mutationFn: async (options: UpdateWorkspaceAgentPortShareRequest) => { - await postWorkspaceAgentSharedPort(workspaceID, options); + mutationFn: async (options: UpsertWorkspaceAgentPortShareRequest) => { + await upsertWorkspaceAgentSharedPort(workspaceID, options); }, }); @@ -149,10 +147,6 @@ export const PortForwardPopoverView: FC = ({ // we don't want to show listening ports if it's already a shared port const filteredListeningPorts = listeningPorts?.filter( (port) => { - if (sharedPorts === undefined) { - return true; - } - for (let i = 0; i < sharedPorts.length; i++) { if (sharedPorts[i].port === port.port && sharedPorts[i].agent_name === agent.name) { return false; @@ -352,11 +346,12 @@ export const PortForwardPopoverView: FC = ({ Public - { - deleteSharedPortMutation.mutate({ + { + await deleteSharedPortMutation.mutateAsync({ agent_name: agent.name, port: share.port, }); + await sharedPortsQuery.refetch(); }}> = ({ marginTop: 2, }} > - + { + setSelectedPort(event.target.value); + }} + /> - { + setSelectedShareLevel(event.target.value as WorkspaceAgentPortShareLevel); + }}> Authenticated Public - {/* How do I use the value from the select in the mutation? */} - +
diff --git a/site/src/modules/resources/PortForwardPopoverView.stories.tsx b/site/src/modules/resources/PortForwardPopoverView.stories.tsx index 82fdfed6352f8..c6e31f3afb4ea 100644 --- a/site/src/modules/resources/PortForwardPopoverView.stories.tsx +++ b/site/src/modules/resources/PortForwardPopoverView.stories.tsx @@ -34,13 +34,15 @@ type Story = StoryObj; export const WithPorts: Story = { args: { listeningPorts: MockListeningPortsResponse.ports, - sharedPorts: MockSharedPortsResponse.shares, + storybook: { + sharedPortsQueryData: MockSharedPortsResponse + }, }, }; export const Empty: Story = { args: { listeningPorts: [], - sharedPorts: [], + }, }; diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx index 5845538f61ee7..c92ebfc30e5a2 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx @@ -1,6 +1,6 @@ import { type Interpolation, type Theme } from "@emotion/react"; import TextField from "@mui/material/TextField"; -import type { Template, UpdateTemplateMeta } from "api/typesGenerated"; +import { WorkspaceAppSharingLevels, type Template, type UpdateTemplateMeta } from "api/typesGenerated"; import { type FormikContextType, type FormikTouched, useFormik } from "formik"; import { type FC } from "react"; import { @@ -43,6 +43,8 @@ export const getValidationSchema = (): Yup.AnyObjectSchema => allow_user_cancel_workspace_jobs: Yup.boolean(), icon: iconValidator, require_active_version: Yup.boolean(), + deprecation_message: Yup.string(), + max_port_sharing_level: Yup.string().oneOf(WorkspaceAppSharingLevels), }); export interface TemplateSettingsForm { @@ -54,6 +56,8 @@ export interface TemplateSettingsForm { // Helpful to show field errors on Storybook initialTouched?: FormikTouched; accessControlEnabled: boolean; + portSharingExperimentEnabled: boolean; + portSharingControlsEnabled: boolean; } export const TemplateSettingsForm: FC = ({ @@ -64,6 +68,8 @@ export const TemplateSettingsForm: FC = ({ isSubmitting, initialTouched, accessControlEnabled, + portSharingExperimentEnabled, + portSharingControlsEnabled, }) => { const validationSchema = getValidationSchema(); const form: FormikContextType = @@ -257,6 +263,46 @@ export const TemplateSettingsForm: FC = ({ + {portSharingExperimentEnabled && ( + + + + + Maximum Port Sharing Level + + + The maximum level of port sharing allowed for workspaces. + + + + {!portSharingControlsEnabled && ( + + + + Enterprise license required to control max port sharing level. + + + )} + + + )} + ); diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.tsx index 5eac1759d5ca2..a1037431eb23e 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.tsx @@ -38,6 +38,8 @@ export const TemplateSettingsPageView: FC = ({ onCancel={onCancel} error={submitError} accessControlEnabled={accessControlEnabled} + portSharingExperimentEnabled + portSharingControlsEnabled /> ); diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index cd06f286725bc..02f3b0f6dba7f 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -3247,16 +3247,18 @@ export const MockListeningPortsResponse: TypesGen.WorkspaceAgentListeningPortsRe export const MockSharedPortsResponse: TypesGen.WorkspaceAgentPortShares = { shares: [ { + workspace_id: MockWorkspace.id, agent_name: "a-workspace-agent", port: 4000, share_level: "authenticated", }, { + workspace_id: MockWorkspace.id, agent_name: "a-workspace-agent", port: 8080, share_level: "authenticated", }, - { agent_name: "a-workspace-agent", port: 8081, share_level: "public" }, + { workspace_id: MockWorkspace.id, agent_name: "a-workspace-agent", port: 8081, share_level: "public" }, ], }; From 43be95ef43b267a6840fa6dbaccd471a4c366e3a Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 15 Feb 2024 18:07:00 +0000 Subject: [PATCH 07/16] fix template settings input --- site/src/api/queries/workspaceportsharing.ts | 25 ++ site/src/modules/resources/AgentRow.test.tsx | 3 +- site/src/modules/resources/AgentRow.tsx | 4 + .../modules/resources/PortForwardButton.tsx | 307 ++++++++++-------- .../PortForwardPopoverView.stories.tsx | 1 - .../TemplateSettingsForm.tsx | 17 +- .../TemplateSettingsPage.tsx | 6 +- .../TemplateSettingsPageView.tsx | 8 +- site/src/pages/WorkspacePage/Workspace.tsx | 1 + 9 files changed, 227 insertions(+), 145 deletions(-) create mode 100644 site/src/api/queries/workspaceportsharing.ts diff --git a/site/src/api/queries/workspaceportsharing.ts b/site/src/api/queries/workspaceportsharing.ts new file mode 100644 index 0000000000000..535bc4c67e540 --- /dev/null +++ b/site/src/api/queries/workspaceportsharing.ts @@ -0,0 +1,25 @@ +import { deleteWorkspaceAgentSharedPort, getWorkspaceAgentSharedPorts, upsertWorkspaceAgentSharedPort } from "api/api" +import { DeleteWorkspaceAgentPortShareRequest, UpsertWorkspaceAgentPortShareRequest } from "api/typesGenerated" + +export const workspacePortShares = (workspaceId: string) => { + return { + queryKey: ["sharedPorts", workspaceId], + queryFn: () => getWorkspaceAgentSharedPorts(workspaceId), + } +} + +export const upsertWorkspacePortShare = (workspaceId: string) => { + return { + mutationFn: async (options: UpsertWorkspaceAgentPortShareRequest) => { + await upsertWorkspaceAgentSharedPort(workspaceId, options); + }, + } +} + +export const deleteWorkspacePortShare = (workspaceId: string) => { + return { + mutationFn: async (options: DeleteWorkspaceAgentPortShareRequest) => { + await deleteWorkspaceAgentSharedPort(workspaceId, options); + }, + } +} diff --git a/site/src/modules/resources/AgentRow.test.tsx b/site/src/modules/resources/AgentRow.test.tsx index 77260083279f4..0815305d0dbdd 100644 --- a/site/src/modules/resources/AgentRow.test.tsx +++ b/site/src/modules/resources/AgentRow.test.tsx @@ -1,4 +1,4 @@ -import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities"; +import { MockTemplate, MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities"; import { AgentRow, AgentRowProps } from "./AgentRow"; import { DisplayAppNameMap } from "./AppLink/AppLink"; import { screen } from "@testing-library/react"; @@ -80,6 +80,7 @@ describe.each<{ const props: AgentRowProps = { agent: MockWorkspaceAgent, workspace: MockWorkspace, + template: MockTemplate, showApps: false, serverVersion: "", serverAPIVersion: "", diff --git a/site/src/modules/resources/AgentRow.tsx b/site/src/modules/resources/AgentRow.tsx index 73b81bc9338c2..9ddecff973f5c 100644 --- a/site/src/modules/resources/AgentRow.tsx +++ b/site/src/modules/resources/AgentRow.tsx @@ -15,6 +15,7 @@ import AutoSizer from "react-virtualized-auto-sizer"; import { FixedSizeList as List, ListOnScrollProps } from "react-window"; import * as API from "api/api"; import type { + Template, Workspace, WorkspaceAgent, WorkspaceAgentLogSource, @@ -59,6 +60,7 @@ export interface AgentRowProps { serverVersion: string; serverAPIVersion: string; onUpdateAgent: () => void; + template: Template; storybookLogs?: LineWithID[]; storybookAgentMetadata?: WorkspaceAgentMetadata[]; } @@ -66,6 +68,7 @@ export interface AgentRowProps { export const AgentRow: FC = ({ agent, workspace, + template, showApps, showBuiltinApps = true, hideSSHButton, @@ -221,6 +224,7 @@ export const AgentRow: FC = ({ agent={agent} username={workspace.owner_name} workspaceID={workspace.id} + template={template} /> )} diff --git a/site/src/modules/resources/PortForwardButton.tsx b/site/src/modules/resources/PortForwardButton.tsx index c9fd2db52af3f..17c6bf3d3c123 100644 --- a/site/src/modules/resources/PortForwardButton.tsx +++ b/site/src/modules/resources/PortForwardButton.tsx @@ -6,10 +6,9 @@ import { type Interpolation, type Theme, useTheme } from "@emotion/react"; import { useState, type FC } from "react"; import { useQuery, useMutation } from "react-query"; import { docs } from "utils/docs"; -import { deleteWorkspaceAgentSharedPort, getAgentListeningPorts, getWorkspaceAgentSharedPorts, upsertWorkspaceAgentSharedPort } from "api/api"; +import { getAgentListeningPorts } from "api/api"; import type { - DeleteWorkspaceAgentPortShareRequest, - UpsertWorkspaceAgentPortShareRequest, + Template, WorkspaceAgent, WorkspaceAgentListeningPort, WorkspaceAgentListeningPortsResponse, @@ -39,6 +38,8 @@ import LockIcon from "@mui/icons-material/Lock"; import LockOpenIcon from "@mui/icons-material/LockOpen"; import IconButton from "@mui/material/IconButton"; import CloseIcon from "@mui/icons-material/Close"; +import { deleteWorkspacePortShare, upsertWorkspacePortShare, workspacePortShares } from "api/queries/workspaceportsharing"; +import { useDashboard } from "modules/dashboard/useDashboard"; export interface PortForwardButtonProps { host: string; @@ -46,6 +47,7 @@ export interface PortForwardButtonProps { workspaceName: string; workspaceID: string; agent: WorkspaceAgent; + template: Template; /** * Only for use in Storybook @@ -58,7 +60,7 @@ export interface PortForwardButtonProps { export const PortForwardButton: FC = (props) => { const { agent, storybook } = props; - + const { entitlements, experiments } = useDashboard(); const paper = useClassName(classNames.paper, []); const portsQuery = useQuery({ @@ -100,6 +102,8 @@ export const PortForwardButton: FC = (props) => { @@ -108,6 +112,8 @@ export const PortForwardButton: FC = (props) => { interface PortForwardPopoverViewProps extends PortForwardButtonProps { listeningPorts?: WorkspaceAgentListeningPort[]; + portSharingExperimentEnabled: boolean; + portSharingControlsEnabled: boolean; } export const PortForwardPopoverView: FC = ({ @@ -115,36 +121,35 @@ export const PortForwardPopoverView: FC = ({ workspaceName, workspaceID, agent, + template, username, listeningPorts, + portSharingExperimentEnabled, + portSharingControlsEnabled, storybook, }) => { const theme = useTheme(); + // TODO: use form const [selectedShareLevel, setSelectedShareLevel] = useState("authenticated"); const [selectedPort, setSelectedPort] = useState(""); const sharedPortsQuery = useQuery({ - queryKey: ["sharedPorts", workspaceID], - queryFn: () => getWorkspaceAgentSharedPorts(workspaceID), + ...workspacePortShares(workspaceID), enabled: !storybook && agent.status === "connected", }); const sharedPorts = storybook ? storybook.sharedPortsQueryData?.shares || [] : sharedPortsQuery.data?.shares || []; - const createSharedPortMutation = useMutation({ - mutationFn: async (options: UpsertWorkspaceAgentPortShareRequest) => { - await upsertWorkspaceAgentSharedPort(workspaceID, options); - }, - }); + const upsertSharedPortMutation = useMutation( + upsertWorkspacePortShare(workspaceID), + ); - const deleteSharedPortMutation = useMutation({ - mutationFn: async (options: DeleteWorkspaceAgentPortShareRequest) => { - await deleteWorkspaceAgentSharedPort(workspaceID, options); - }, - }); + const deleteSharedPortMutation = useMutation( + deleteWorkspacePortShare(workspaceID), + ); - // we don't want to show listening ports if it's already a shared port + // we don't want to show listening ports if it's a shared port const filteredListeningPorts = listeningPorts?.filter( (port) => { for (let i = 0; i < sharedPorts.length; i++) { @@ -156,6 +161,8 @@ export const PortForwardPopoverView: FC = ({ return true; } ); + // only disable the form if shared port controls are entitled and the template doesn't allow sharing ports + const canSharePorts = !(portSharingControlsEnabled && template.max_port_share_level === "owner"); return ( <> @@ -269,12 +276,13 @@ export const PortForwardPopoverView: FC = ({ {port.port} -
- + { + setSelectedPort(event.target.value); + }} + value={selectedPort} + disabled={!canSharePorts} + /> + + + + + + + )} ); }; diff --git a/site/src/modules/resources/PortForwardPopoverView.stories.tsx b/site/src/modules/resources/PortForwardPopoverView.stories.tsx index c6e31f3afb4ea..f40ba8694666c 100644 --- a/site/src/modules/resources/PortForwardPopoverView.stories.tsx +++ b/site/src/modules/resources/PortForwardPopoverView.stories.tsx @@ -43,6 +43,5 @@ export const WithPorts: Story = { export const Empty: Story = { args: { listeningPorts: [], - }, }; diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx index c92ebfc30e5a2..7f80433e55647 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx @@ -27,6 +27,8 @@ import { HelpTooltipTrigger, } from "components/HelpTooltip/HelpTooltip"; import { EnterpriseBadge } from "components/Badges/Badges"; +import Select from "@mui/material/Select"; +import MenuItem from "@mui/material/MenuItem"; const MAX_DESCRIPTION_CHAR_LIMIT = 128; const MAX_DESCRIPTION_MESSAGE = @@ -86,6 +88,7 @@ export const TemplateSettingsForm: FC = ({ require_active_version: template.require_active_version, deprecation_message: template.deprecation_message, disable_everyone_group_access: false, + max_port_share_level: template.max_port_share_level, }, validationSchema, onSubmit, @@ -285,12 +288,20 @@ export const TemplateSettingsForm: FC = ({ The maximum level of port sharing allowed for workspaces. - + onChange={form.handleChange} + value={form.values.max_port_share_level} + > + Owner + Authenticated + Public + {!portSharingControlsEnabled && ( diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx index 572b788aacbab..1d298082142af 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx @@ -18,8 +18,10 @@ export const TemplateSettingsPage: FC = () => { const orgId = useOrganizationId(); const { template } = useTemplateSettings(); const queryClient = useQueryClient(); - const { entitlements } = useDashboard(); + const { entitlements, experiments } = useDashboard(); const accessControlEnabled = entitlements.features.access_control.enabled; + const sharedPortsExperimentEnabled = experiments.includes("shared-ports"); + const sharedPortControlsEnabled = entitlements.features.control_shared_ports.enabled; const { mutate: updateTemplate, @@ -67,6 +69,8 @@ export const TemplateSettingsPage: FC = () => { }); }} accessControlEnabled={accessControlEnabled} + sharedPortsExperimentEnabled={sharedPortsExperimentEnabled} + sharedPortControlsEnabled={sharedPortControlsEnabled} /> ); diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.tsx index a1037431eb23e..46222b8de2ec1 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.tsx @@ -13,6 +13,8 @@ export interface TemplateSettingsPageViewProps { typeof TemplateSettingsForm >["initialTouched"]; accessControlEnabled: boolean; + sharedPortsExperimentEnabled: boolean; + sharedPortControlsEnabled: boolean; } export const TemplateSettingsPageView: FC = ({ @@ -23,6 +25,8 @@ export const TemplateSettingsPageView: FC = ({ submitError, initialTouched, accessControlEnabled, + sharedPortsExperimentEnabled, + sharedPortControlsEnabled, }) => { return ( <> @@ -38,8 +42,8 @@ export const TemplateSettingsPageView: FC = ({ onCancel={onCancel} error={submitError} accessControlEnabled={accessControlEnabled} - portSharingExperimentEnabled - portSharingControlsEnabled + portSharingExperimentEnabled={sharedPortsExperimentEnabled} + portSharingControlsEnabled={sharedPortControlsEnabled} /> ); diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index 31efc004c2aa7..37d6771d4ffb9 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -245,6 +245,7 @@ export const Workspace: FC = ({ key={agent.id} agent={agent} workspace={workspace} + template={template} sshPrefix={sshPrefix} showApps={permissions.updateWorkspace} showBuiltinApps={permissions.updateWorkspace} From 9f267ec5d0fb452b1924ebadfdd14dab887ff639 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 16 Feb 2024 19:09:14 +0000 Subject: [PATCH 08/16] storybook --- site/src/api/api.ts | 4 +- site/src/api/queries/workspaceportsharing.ts | 23 +- site/src/modules/resources/AgentRow.test.tsx | 6 +- .../modules/resources/PortForwardButton.tsx | 330 ++++++++++-------- .../PortForwardPopoverView.stories.tsx | 50 ++- .../TemplateSettingsForm.tsx | 6 +- .../TemplateSettingsPage.tsx | 3 +- site/src/testHelpers/entities.ts | 9 +- 8 files changed, 275 insertions(+), 156 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 83a2008e403c8..3a454e4389cd3 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1178,7 +1178,7 @@ export const upsertWorkspaceAgentSharedPort = async ( ): Promise => { const response = await axios.post( `/api/v2/workspaces/${workspaceID}/port-share`, - req + req, ); return response.data; }; @@ -1191,7 +1191,7 @@ export const deleteWorkspaceAgentSharedPort = async ( `/api/v2/workspaces/${workspaceID}/port-share`, { data: req, - } + }, ); return response.data; }; diff --git a/site/src/api/queries/workspaceportsharing.ts b/site/src/api/queries/workspaceportsharing.ts index 535bc4c67e540..7f921f90d7999 100644 --- a/site/src/api/queries/workspaceportsharing.ts +++ b/site/src/api/queries/workspaceportsharing.ts @@ -1,25 +1,32 @@ -import { deleteWorkspaceAgentSharedPort, getWorkspaceAgentSharedPorts, upsertWorkspaceAgentSharedPort } from "api/api" -import { DeleteWorkspaceAgentPortShareRequest, UpsertWorkspaceAgentPortShareRequest } from "api/typesGenerated" +import { + deleteWorkspaceAgentSharedPort, + getWorkspaceAgentSharedPorts, + upsertWorkspaceAgentSharedPort, +} from "api/api"; +import { + DeleteWorkspaceAgentPortShareRequest, + UpsertWorkspaceAgentPortShareRequest, +} from "api/typesGenerated"; export const workspacePortShares = (workspaceId: string) => { return { queryKey: ["sharedPorts", workspaceId], queryFn: () => getWorkspaceAgentSharedPorts(workspaceId), - } -} + }; +}; export const upsertWorkspacePortShare = (workspaceId: string) => { return { mutationFn: async (options: UpsertWorkspaceAgentPortShareRequest) => { await upsertWorkspaceAgentSharedPort(workspaceId, options); }, - } -} + }; +}; export const deleteWorkspacePortShare = (workspaceId: string) => { return { mutationFn: async (options: DeleteWorkspaceAgentPortShareRequest) => { await deleteWorkspaceAgentSharedPort(workspaceId, options); }, - } -} + }; +}; diff --git a/site/src/modules/resources/AgentRow.test.tsx b/site/src/modules/resources/AgentRow.test.tsx index 0815305d0dbdd..23907798f28ba 100644 --- a/site/src/modules/resources/AgentRow.test.tsx +++ b/site/src/modules/resources/AgentRow.test.tsx @@ -1,4 +1,8 @@ -import { MockTemplate, MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities"; +import { + MockTemplate, + MockWorkspace, + MockWorkspaceAgent, +} from "testHelpers/entities"; import { AgentRow, AgentRowProps } from "./AgentRow"; import { DisplayAppNameMap } from "./AppLink/AppLink"; import { screen } from "@testing-library/react"; diff --git a/site/src/modules/resources/PortForwardButton.tsx b/site/src/modules/resources/PortForwardButton.tsx index 17c6bf3d3c123..c01d20183da63 100644 --- a/site/src/modules/resources/PortForwardButton.tsx +++ b/site/src/modules/resources/PortForwardButton.tsx @@ -3,17 +3,19 @@ import Link from "@mui/material/Link"; import CircularProgress from "@mui/material/CircularProgress"; import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined"; import { type Interpolation, type Theme, useTheme } from "@emotion/react"; -import { useState, type FC } from "react"; +import { type FC } from "react"; import { useQuery, useMutation } from "react-query"; import { docs } from "utils/docs"; import { getAgentListeningPorts } from "api/api"; -import type { - Template, - WorkspaceAgent, - WorkspaceAgentListeningPort, - WorkspaceAgentListeningPortsResponse, - WorkspaceAgentPortShareLevel, - WorkspaceAgentPortShares, +import { + WorkspaceAppSharingLevels, + type Template, + type WorkspaceAgent, + type WorkspaceAgentListeningPort, + type WorkspaceAgentListeningPortsResponse, + type WorkspaceAgentPortShareLevel, + type WorkspaceAgentPortShares, + UpsertWorkspaceAgentPortShareRequest, } from "api/typesGenerated"; import { portForwardURL } from "utils/portForward"; import { type ClassName, useClassName } from "hooks/useClassName"; @@ -36,10 +38,16 @@ import TextField from "@mui/material/TextField"; import SensorsIcon from "@mui/icons-material/Sensors"; import LockIcon from "@mui/icons-material/Lock"; import LockOpenIcon from "@mui/icons-material/LockOpen"; -import IconButton from "@mui/material/IconButton"; import CloseIcon from "@mui/icons-material/Close"; -import { deleteWorkspacePortShare, upsertWorkspacePortShare, workspacePortShares } from "api/queries/workspaceportsharing"; +import { + deleteWorkspacePortShare, + upsertWorkspacePortShare, + workspacePortShares, +} from "api/queries/workspaceportsharing"; import { useDashboard } from "modules/dashboard/useDashboard"; +import * as Yup from "yup"; +import { FormikContextType, useFormik } from "formik"; +import { getFormHelpers } from "utils/formUtils"; export interface PortForwardButtonProps { host: string; @@ -103,13 +111,21 @@ export const PortForwardButton: FC = (props) => { {...props} listeningPorts={listeningPorts?.ports} portSharingExperimentEnabled={experiments.includes("shared-ports")} - portSharingControlsEnabled={entitlements.features.control_shared_ports.enabled} + portSharingControlsEnabled={ + entitlements.features.control_shared_ports.enabled + } /> ); }; +export const getValidationSchema = (): Yup.AnyObjectSchema => + Yup.object({ + port: Yup.number().required().min(0).max(65535), + sharing_level: Yup.string().required().oneOf(WorkspaceAppSharingLevels), + }); + interface PortForwardPopoverViewProps extends PortForwardButtonProps { listeningPorts?: WorkspaceAgentListeningPort[]; portSharingExperimentEnabled: boolean; @@ -129,17 +145,14 @@ export const PortForwardPopoverView: FC = ({ storybook, }) => { const theme = useTheme(); - // TODO: use form - const [selectedShareLevel, setSelectedShareLevel] = useState("authenticated"); - const [selectedPort, setSelectedPort] = useState(""); const sharedPortsQuery = useQuery({ ...workspacePortShares(workspaceID), enabled: !storybook && agent.status === "connected", }); const sharedPorts = storybook - ? storybook.sharedPortsQueryData?.shares || [] - : sharedPortsQuery.data?.shares || []; + ? storybook.sharedPortsQueryData?.shares || [] + : sharedPortsQuery.data?.shares || []; const upsertSharedPortMutation = useMutation( upsertWorkspacePortShare(workspaceID), @@ -149,27 +162,53 @@ export const PortForwardPopoverView: FC = ({ deleteWorkspacePortShare(workspaceID), ); + // share port form + const { + mutateAsync: upsertWorkspacePortShareForm, + isLoading: isSubmitting, + error: submitError, + } = useMutation(upsertWorkspacePortShare(workspaceID)); + const validationSchema = getValidationSchema(); + const form: FormikContextType = + useFormik({ + initialValues: { + agent_name: agent.name, + port: 0, + share_level: "authenticated", + }, + validationSchema, + onSubmit: async (values) => { + await upsertWorkspacePortShareForm(values); + await sharedPortsQuery.refetch(); + }, + }); + const getFieldHelpers = getFormHelpers(form, submitError); + // we don't want to show listening ports if it's a shared port - const filteredListeningPorts = listeningPorts?.filter( - (port) => { - for (let i = 0; i < sharedPorts.length; i++) { - if (sharedPorts[i].port === port.port && sharedPorts[i].agent_name === agent.name) { - return false; - } + const filteredListeningPorts = listeningPorts?.filter((port) => { + for (let i = 0; i < sharedPorts.length; i++) { + if ( + sharedPorts[i].port === port.port && + sharedPorts[i].agent_name === agent.name + ) { + return false; } - - return true; } - ); + + return true; + }); // only disable the form if shared port controls are entitled and the template doesn't allow sharing ports - const canSharePorts = !(portSharingControlsEnabled && template.max_port_share_level === "owner"); + const canSharePorts = + portSharingExperimentEnabled && + !(portSharingControlsEnabled && template.max_port_share_level === "owner"); + const canSharePortsPublic = + canSharePorts && template.max_port_share_level === "public"; return ( <>
= ({ {label} - - - {port.port} - - + + {port.port} + + {canSharePorts && ( + + )} ); @@ -297,13 +345,16 @@ export const PortForwardPopoverView: FC = ({
Shared Ports - Ports can be shared with other Coder users or with the public. + {canSharePorts + ? "Ports can be shared with other Coder users or with the public." + : "This workspace template does not allow sharing ports. Contact a template administrator to enable port sharing."} - {canSharePorts ? ( + {canSharePorts && (
{sharedPorts?.map((share) => { const url = portForwardURL( @@ -336,108 +387,91 @@ export const PortForwardPopoverView: FC = ({ {label} - - { + await upsertSharedPortMutation.mutateAsync({ + agent_name: agent.name, + port: share.port, + share_level: event.target + .value as WorkspaceAgentPortShareLevel, + }); + await sharedPortsQuery.refetch(); + }} + > + + Authenticated + + + Public + + + + ); })} +
+ + + + + + + +
- ) : ( - // TODO: format this better - - Template does not allow sharing ports. Contact a template administrator to enable port sharing. - )} - - - { - setSelectedPort(event.target.value); - }} - value={selectedPort} - disabled={!canSharePorts} - /> - - - - -
)} @@ -485,6 +519,22 @@ const styles = { fontWeight: 400, }), + shareLevelSelect: () => ({ + boxShadow: "none", + ".MuiOutlinedInput-notchedOutline": { border: 0 }, + "&.MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline": { + border: 0, + }, + "&.MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline": { + border: 0, + }, + }), + + deleteButton: () => ({ + minWidth: 30, + padding: 0, + }), + newPortForm: (theme) => ({ border: `1px solid ${theme.palette.divider}`, borderRadius: "4px", diff --git a/site/src/modules/resources/PortForwardPopoverView.stories.tsx b/site/src/modules/resources/PortForwardPopoverView.stories.tsx index f40ba8694666c..7c1239490f4c0 100644 --- a/site/src/modules/resources/PortForwardPopoverView.stories.tsx +++ b/site/src/modules/resources/PortForwardPopoverView.stories.tsx @@ -3,6 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { MockListeningPortsResponse, MockSharedPortsResponse, + MockTemplate, MockWorkspaceAgent, } from "testHelpers/entities"; @@ -25,6 +26,9 @@ const meta: Meta = { ], args: { agent: MockWorkspaceAgent, + template: MockTemplate, + portSharingExperimentEnabled: true, + portSharingControlsEnabled: true, }, }; @@ -35,7 +39,7 @@ export const WithPorts: Story = { args: { listeningPorts: MockListeningPortsResponse.ports, storybook: { - sharedPortsQueryData: MockSharedPortsResponse + sharedPortsQueryData: MockSharedPortsResponse, }, }, }; @@ -43,5 +47,49 @@ export const WithPorts: Story = { export const Empty: Story = { args: { listeningPorts: [], + storybook: { + sharedPortsQueryData: {shares:[]}, + }, + }, +}; + +export const NoPortSharingExperiment: Story = { + args: { + listeningPorts: MockListeningPortsResponse.ports, + portSharingExperimentEnabled: false, }, }; + +export const AGPLPortSharing: Story = { + args: { + listeningPorts: MockListeningPortsResponse.ports, + storybook: { + sharedPortsQueryData: MockSharedPortsResponse, + }, + portSharingControlsEnabled: false, + }, +}; + +export const EnterprisePortSharingControlsOwner: Story = { + args: { + listeningPorts: MockListeningPortsResponse.ports, + template: { + ...MockTemplate, + max_port_share_level: "owner", + }, + }, +}; + +export const EnterprisePortSharingControlsAuthenticated: Story = { + args: { + listeningPorts: MockListeningPortsResponse.ports, + storybook: { + sharedPortsQueryData: MockSharedPortsResponse, + }, + template: { + ...MockTemplate, + max_port_share_level: "authenticated", + }, + }, +}; + diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx index 7f80433e55647..f73d2f6cc64dc 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx @@ -1,6 +1,10 @@ import { type Interpolation, type Theme } from "@emotion/react"; import TextField from "@mui/material/TextField"; -import { WorkspaceAppSharingLevels, type Template, type UpdateTemplateMeta } from "api/typesGenerated"; +import { + WorkspaceAppSharingLevels, + type Template, + type UpdateTemplateMeta, +} from "api/typesGenerated"; import { type FormikContextType, type FormikTouched, useFormik } from "formik"; import { type FC } from "react"; import { diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx index 1d298082142af..44cd7eae4747f 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx @@ -21,7 +21,8 @@ export const TemplateSettingsPage: FC = () => { const { entitlements, experiments } = useDashboard(); const accessControlEnabled = entitlements.features.access_control.enabled; const sharedPortsExperimentEnabled = experiments.includes("shared-ports"); - const sharedPortControlsEnabled = entitlements.features.control_shared_ports.enabled; + const sharedPortControlsEnabled = + entitlements.features.control_shared_ports.enabled; const { mutate: updateTemplate, diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 02f3b0f6dba7f..dcaf18aa546e5 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -489,7 +489,7 @@ export const MockTemplate: TypesGen.Template = { require_active_version: false, deprecated: false, deprecation_message: "", - max_port_share_level: "owner", + max_port_share_level: "public", }; export const MockTemplateVersionFiles: TemplateVersionFiles = { @@ -3258,7 +3258,12 @@ export const MockSharedPortsResponse: TypesGen.WorkspaceAgentPortShares = { port: 8080, share_level: "authenticated", }, - { workspace_id: MockWorkspace.id, agent_name: "a-workspace-agent", port: 8081, share_level: "public" }, + { + workspace_id: MockWorkspace.id, + agent_name: "a-workspace-agent", + port: 8081, + share_level: "public", + }, ], }; From ace1c1a9f11581732721badfacd5a501d1e2dc01 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 16 Feb 2024 19:09:49 +0000 Subject: [PATCH 09/16] make fmt --- site/src/modules/resources/PortForwardPopoverView.stories.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/site/src/modules/resources/PortForwardPopoverView.stories.tsx b/site/src/modules/resources/PortForwardPopoverView.stories.tsx index 7c1239490f4c0..21469fde5d1a3 100644 --- a/site/src/modules/resources/PortForwardPopoverView.stories.tsx +++ b/site/src/modules/resources/PortForwardPopoverView.stories.tsx @@ -48,7 +48,7 @@ export const Empty: Story = { args: { listeningPorts: [], storybook: { - sharedPortsQueryData: {shares:[]}, + sharedPortsQueryData: { shares: [] }, }, }, }; @@ -92,4 +92,3 @@ export const EnterprisePortSharingControlsAuthenticated: Story = { }, }, }; - From 9deb05cef13eda705a7278984f8c1604fd93073f Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 16 Feb 2024 19:46:25 +0000 Subject: [PATCH 10/16] filter shared ports correctly --- site/src/modules/resources/PortForwardButton.tsx | 11 +++++++---- .../resources/PortForwardPopoverView.stories.tsx | 6 +++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/site/src/modules/resources/PortForwardButton.tsx b/site/src/modules/resources/PortForwardButton.tsx index c01d20183da63..80705661c8746 100644 --- a/site/src/modules/resources/PortForwardButton.tsx +++ b/site/src/modules/resources/PortForwardButton.tsx @@ -184,12 +184,15 @@ export const PortForwardPopoverView: FC = ({ }); const getFieldHelpers = getFormHelpers(form, submitError); + // filter out shared ports that are not from this agent + const filteredSharedPorts = sharedPorts.filter( + (port) => port.agent_name === agent.name, + ); // we don't want to show listening ports if it's a shared port const filteredListeningPorts = listeningPorts?.filter((port) => { - for (let i = 0; i < sharedPorts.length; i++) { + for (let i = 0; i < filteredSharedPorts.length; i++) { if ( - sharedPorts[i].port === port.port && - sharedPorts[i].agent_name === agent.name + filteredSharedPorts[i].port === port.port ) { return false; } @@ -356,7 +359,7 @@ export const PortForwardPopoverView: FC = ({ {canSharePorts && (
- {sharedPorts?.map((share) => { + {filteredSharedPorts?.map((share) => { const url = portForwardURL( host, share.port, diff --git a/site/src/modules/resources/PortForwardPopoverView.stories.tsx b/site/src/modules/resources/PortForwardPopoverView.stories.tsx index 21469fde5d1a3..271caa01c28c0 100644 --- a/site/src/modules/resources/PortForwardPopoverView.stories.tsx +++ b/site/src/modules/resources/PortForwardPopoverView.stories.tsx @@ -84,7 +84,11 @@ export const EnterprisePortSharingControlsAuthenticated: Story = { args: { listeningPorts: MockListeningPortsResponse.ports, storybook: { - sharedPortsQueryData: MockSharedPortsResponse, + sharedPortsQueryData: { + shares: MockSharedPortsResponse.shares.filter((share) => { + return share.share_level === "authenticated"; + }), + }, }, template: { ...MockTemplate, From 267405b7ba953b70d430747c22ec8e8be88377ce Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Tue, 20 Feb 2024 15:34:34 +0000 Subject: [PATCH 11/16] work on form submission --- .../resources/PortForwardButton.stories.tsx | 23 +++--- .../modules/resources/PortForwardButton.tsx | 73 ++++++++----------- .../PortForwardPopoverView.stories.tsx | 54 ++++++++++---- .../TemplateSettingsForm.tsx | 33 +++------ 4 files changed, 93 insertions(+), 90 deletions(-) diff --git a/site/src/modules/resources/PortForwardButton.stories.tsx b/site/src/modules/resources/PortForwardButton.stories.tsx index c8f4e28a58410..6d09f321c4cd6 100644 --- a/site/src/modules/resources/PortForwardButton.stories.tsx +++ b/site/src/modules/resources/PortForwardButton.stories.tsx @@ -3,6 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { MockListeningPortsResponse, MockSharedPortsResponse, + MockWorkspace, MockWorkspaceAgent, } from "testHelpers/entities"; @@ -18,16 +19,18 @@ export default meta; type Story = StoryObj; export const Example: Story = { - args: { - storybook: { - listeningPortsQueryData: MockListeningPortsResponse, - sharedPortsQueryData: MockSharedPortsResponse, - }, + parameters: { + queries: [ + { + key: ["portForward", MockWorkspaceAgent.id], + data: MockListeningPortsResponse, + }, + { + key: ["sharedPorts", MockWorkspace.id], + data: MockSharedPortsResponse, + }, + ], }, }; -export const Loading: Story = { - args: { - storybook: {}, - }, -}; +export const Loading: Story = {}; diff --git a/site/src/modules/resources/PortForwardButton.tsx b/site/src/modules/resources/PortForwardButton.tsx index 80705661c8746..c8fcb283c892c 100644 --- a/site/src/modules/resources/PortForwardButton.tsx +++ b/site/src/modules/resources/PortForwardButton.tsx @@ -12,9 +12,7 @@ import { type Template, type WorkspaceAgent, type WorkspaceAgentListeningPort, - type WorkspaceAgentListeningPortsResponse, type WorkspaceAgentPortShareLevel, - type WorkspaceAgentPortShares, UpsertWorkspaceAgentPortShareRequest, } from "api/typesGenerated"; import { portForwardURL } from "utils/portForward"; @@ -56,46 +54,34 @@ export interface PortForwardButtonProps { workspaceID: string; agent: WorkspaceAgent; template: Template; - - /** - * Only for use in Storybook - */ - storybook?: { - listeningPortsQueryData?: WorkspaceAgentListeningPortsResponse; - sharedPortsQueryData?: WorkspaceAgentPortShares; - }; } export const PortForwardButton: FC = (props) => { - const { agent, storybook } = props; + const { agent } = props; const { entitlements, experiments } = useDashboard(); const paper = useClassName(classNames.paper, []); const portsQuery = useQuery({ queryKey: ["portForward", agent.id], queryFn: () => getAgentListeningPorts(agent.id), - enabled: !storybook && agent.status === "connected", + enabled: agent.status === "connected", refetchInterval: 5_000, }); - const listeningPorts = storybook - ? storybook.listeningPortsQueryData - : portsQuery.data; - return ( diff --git a/site/src/modules/resources/PortForwardPopoverView.stories.tsx b/site/src/modules/resources/PortForwardPopoverView.stories.tsx index 271caa01c28c0..94e101e66b91d 100644 --- a/site/src/modules/resources/PortForwardPopoverView.stories.tsx +++ b/site/src/modules/resources/PortForwardPopoverView.stories.tsx @@ -4,6 +4,7 @@ import { MockListeningPortsResponse, MockSharedPortsResponse, MockTemplate, + MockWorkspace, MockWorkspaceAgent, } from "testHelpers/entities"; @@ -27,6 +28,7 @@ const meta: Meta = { args: { agent: MockWorkspaceAgent, template: MockTemplate, + workspaceID: MockWorkspace.id, portSharingExperimentEnabled: true, portSharingControlsEnabled: true, }, @@ -38,18 +40,28 @@ type Story = StoryObj; export const WithPorts: Story = { args: { listeningPorts: MockListeningPortsResponse.ports, - storybook: { - sharedPortsQueryData: MockSharedPortsResponse, - }, + }, + parameters: { + queries: [ + { + key: ["sharedPorts", MockWorkspace.id], + data: MockSharedPortsResponse, + }, + ], }, }; export const Empty: Story = { args: { listeningPorts: [], - storybook: { - sharedPortsQueryData: { shares: [] }, - }, + }, + parameters: { + queries: [ + { + key: ["sharedPorts", MockWorkspace.id], + data: { shares: [] }, + }, + ], }, }; @@ -63,11 +75,16 @@ export const NoPortSharingExperiment: Story = { export const AGPLPortSharing: Story = { args: { listeningPorts: MockListeningPortsResponse.ports, - storybook: { - sharedPortsQueryData: MockSharedPortsResponse, - }, portSharingControlsEnabled: false, }, + parameters: { + queries: [ + { + key: ["sharedPorts", MockWorkspace.id], + data: MockSharedPortsResponse, + }, + ], + }, }; export const EnterprisePortSharingControlsOwner: Story = { @@ -83,16 +100,21 @@ export const EnterprisePortSharingControlsOwner: Story = { export const EnterprisePortSharingControlsAuthenticated: Story = { args: { listeningPorts: MockListeningPortsResponse.ports, - storybook: { - sharedPortsQueryData: { - shares: MockSharedPortsResponse.shares.filter((share) => { - return share.share_level === "authenticated"; - }), - }, - }, template: { ...MockTemplate, max_port_share_level: "authenticated", }, }, + parameters: { + queries: [ + { + key: ["sharedPorts", MockWorkspace.id], + data: { + shares: MockSharedPortsResponse.shares.filter((share) => { + return share.share_level === "authenticated"; + }), + }, + }, + ], + }, }; diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx index f73d2f6cc64dc..3c540c952214b 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx @@ -31,7 +31,6 @@ import { HelpTooltipTrigger, } from "components/HelpTooltip/HelpTooltip"; import { EnterpriseBadge } from "components/Badges/Badges"; -import Select from "@mui/material/Select"; import MenuItem from "@mui/material/MenuItem"; const MAX_DESCRIPTION_CHAR_LIMIT = 128; @@ -279,33 +278,25 @@ export const TemplateSettingsForm: FC = ({ only be accessed by the workspace owner." > - - - Maximum Port Sharing Level - - - The maximum level of port sharing allowed for workspaces. - - - + {!portSharingControlsEnabled && ( From 1baa1a49973d03a6b89914e7669709e9735cc7a5 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Tue, 20 Feb 2024 15:37:00 +0000 Subject: [PATCH 12/16] remove export --- site/src/modules/resources/PortForwardButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/modules/resources/PortForwardButton.tsx b/site/src/modules/resources/PortForwardButton.tsx index c8fcb283c892c..54e41b0bb8b4f 100644 --- a/site/src/modules/resources/PortForwardButton.tsx +++ b/site/src/modules/resources/PortForwardButton.tsx @@ -106,7 +106,7 @@ export const PortForwardButton: FC = (props) => { ); }; -export const getValidationSchema = (): Yup.AnyObjectSchema => +const getValidationSchema = (): Yup.AnyObjectSchema => Yup.object({ port: Yup.number().required().min(0).max(65535), sharing_level: Yup.string().required().oneOf(WorkspaceAppSharingLevels), From 2e10ecc9d2aebcdba4767784a277103de256bcab Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Tue, 20 Feb 2024 16:55:51 +0000 Subject: [PATCH 13/16] get form to behave --- .../modules/resources/PortForwardButton.tsx | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/site/src/modules/resources/PortForwardButton.tsx b/site/src/modules/resources/PortForwardButton.tsx index 54e41b0bb8b4f..ad8cf96106dfe 100644 --- a/site/src/modules/resources/PortForwardButton.tsx +++ b/site/src/modules/resources/PortForwardButton.tsx @@ -46,6 +46,7 @@ import { useDashboard } from "modules/dashboard/useDashboard"; import * as Yup from "yup"; import { FormikContextType, useFormik } from "formik"; import { getFormHelpers } from "utils/formUtils"; +import { LoadingButton } from "@mui/lab"; export interface PortForwardButtonProps { host: string; @@ -109,7 +110,7 @@ export const PortForwardButton: FC = (props) => { const getValidationSchema = (): Yup.AnyObjectSchema => Yup.object({ port: Yup.number().required().min(0).max(65535), - sharing_level: Yup.string().required().oneOf(WorkspaceAppSharingLevels), + share_level: Yup.string().required().oneOf(WorkspaceAppSharingLevels), }); interface PortForwardPopoverViewProps extends PortForwardButtonProps { @@ -118,6 +119,8 @@ interface PortForwardPopoverViewProps extends PortForwardButtonProps { portSharingControlsEnabled: boolean; } +type Optional = Pick, K> & Omit; + export const PortForwardPopoverView: FC = ({ host, workspaceName, @@ -153,16 +156,25 @@ export const PortForwardPopoverView: FC = ({ } = useMutation(upsertWorkspacePortShare(workspaceID)); const validationSchema = getValidationSchema(); // TODO: do partial here - const form: FormikContextType = - useFormik({ + const form: FormikContextType> = + useFormik>({ initialValues: { agent_name: agent.name, - port: 0, + port: undefined, share_level: "authenticated", }, validationSchema, onSubmit: async (values) => { - await upsertWorkspacePortShareForm(values); + // we need port to be optional in the initialValues so it appears empty instead of 0. + // because of this we need to reset the form to clear the port field manually. + form.resetForm(); + await form.setFieldValue("port", ""); + + const port = Number(values.port); + await upsertWorkspacePortShareForm({ + ...values, + port, + }); await sharedPortsQuery.refetch(); }, }); @@ -437,6 +449,7 @@ export const PortForwardPopoverView: FC = ({ size="small" variant="outlined" type="number" + value={form.values.port} /> = ({ Public - +
From 74a65d3225c443fe20642d83103e0736ead90b62 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Tue, 20 Feb 2024 16:56:10 +0000 Subject: [PATCH 14/16] fmt --- .../modules/resources/PortForwardButton.tsx | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/site/src/modules/resources/PortForwardButton.tsx b/site/src/modules/resources/PortForwardButton.tsx index ad8cf96106dfe..62fed379b2667 100644 --- a/site/src/modules/resources/PortForwardButton.tsx +++ b/site/src/modules/resources/PortForwardButton.tsx @@ -156,28 +156,29 @@ export const PortForwardPopoverView: FC = ({ } = useMutation(upsertWorkspacePortShare(workspaceID)); const validationSchema = getValidationSchema(); // TODO: do partial here - const form: FormikContextType> = - useFormik>({ - initialValues: { - agent_name: agent.name, - port: undefined, - share_level: "authenticated", - }, - validationSchema, - onSubmit: async (values) => { - // we need port to be optional in the initialValues so it appears empty instead of 0. - // because of this we need to reset the form to clear the port field manually. - form.resetForm(); - await form.setFieldValue("port", ""); + const form: FormikContextType< + Optional + > = useFormik>({ + initialValues: { + agent_name: agent.name, + port: undefined, + share_level: "authenticated", + }, + validationSchema, + onSubmit: async (values) => { + // we need port to be optional in the initialValues so it appears empty instead of 0. + // because of this we need to reset the form to clear the port field manually. + form.resetForm(); + await form.setFieldValue("port", ""); - const port = Number(values.port); - await upsertWorkspacePortShareForm({ - ...values, - port, - }); - await sharedPortsQuery.refetch(); - }, - }); + const port = Number(values.port); + await upsertWorkspacePortShareForm({ + ...values, + port, + }); + await sharedPortsQuery.refetch(); + }, + }); const getFieldHelpers = getFormHelpers(form, submitError); // filter out shared ports that are not from this agent From d3927f05476e2b83e3c8777bca1b478d204508ab Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Tue, 20 Feb 2024 17:27:02 +0000 Subject: [PATCH 15/16] Add basic test --- .../resources/PortForwardPopoverView.test.tsx | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 site/src/modules/resources/PortForwardPopoverView.test.tsx diff --git a/site/src/modules/resources/PortForwardPopoverView.test.tsx b/site/src/modules/resources/PortForwardPopoverView.test.tsx new file mode 100644 index 0000000000000..c48465ecc4f5f --- /dev/null +++ b/site/src/modules/resources/PortForwardPopoverView.test.tsx @@ -0,0 +1,33 @@ +import { screen } from "@testing-library/react"; +import { MockListeningPortsResponse, MockTemplate, MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities"; +import { renderComponent } from "testHelpers/renderHelpers"; +import { PortForwardPopoverView } from "./PortForwardButton"; +import { QueryClientProvider, QueryClient } from "react-query"; + +describe("Port Forward Popover View", () => { + it("renders component", async () => { + renderComponent( + + + + ); + + expect( + screen.getByText(MockListeningPortsResponse.ports[0].port), + ).toBeInTheDocument(); + + expect( + screen.getByText(MockListeningPortsResponse.ports[0].process_name), + ).toBeInTheDocument(); + }); +}); From a5dbe20b698ec4f8f2ad5c3480c025a068e9fec9 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Tue, 20 Feb 2024 17:34:57 +0000 Subject: [PATCH 16/16] fmt --- .../modules/resources/PortForwardPopoverView.test.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/site/src/modules/resources/PortForwardPopoverView.test.tsx b/site/src/modules/resources/PortForwardPopoverView.test.tsx index c48465ecc4f5f..c04cd74b3724c 100644 --- a/site/src/modules/resources/PortForwardPopoverView.test.tsx +++ b/site/src/modules/resources/PortForwardPopoverView.test.tsx @@ -1,5 +1,10 @@ import { screen } from "@testing-library/react"; -import { MockListeningPortsResponse, MockTemplate, MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities"; +import { + MockListeningPortsResponse, + MockTemplate, + MockWorkspace, + MockWorkspaceAgent, +} from "testHelpers/entities"; import { renderComponent } from "testHelpers/renderHelpers"; import { PortForwardPopoverView } from "./PortForwardButton"; import { QueryClientProvider, QueryClient } from "react-query"; @@ -19,7 +24,7 @@ describe("Port Forward Popover View", () => { username="username" workspaceName="workspaceName" /> - + , ); expect(