Skip to content

Commit 174214a

Browse files
committed
get form working
1 parent 7839a2c commit 174214a

File tree

8 files changed

+110
-39
lines changed

8 files changed

+110
-39
lines changed

coderd/workspaceagentportshare.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ func (api *API) deleteWorkspaceAgentPortShare(rw http.ResponseWriter, r *http.Re
156156
}
157157

158158
func convertPortShares(shares []database.WorkspaceAgentPortShare) []codersdk.WorkspaceAgentPortShare {
159-
var converted []codersdk.WorkspaceAgentPortShare
159+
converted := []codersdk.WorkspaceAgentPortShare{}
160160
for _, share := range shares {
161161
converted = append(converted, convertPortShare(share))
162162
}

site/src/api/api.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,17 +1167,17 @@ export const getWorkspaceAgentSharedPorts = async (
11671167
workspaceID: string,
11681168
): Promise<TypesGen.WorkspaceAgentPortShares> => {
11691169
const response = await axios.get(
1170-
`/api/v2/workspaces/${workspaceID}/shared-ports`,
1170+
`/api/v2/workspaces/${workspaceID}/port-share`,
11711171
);
11721172
return response.data;
11731173
};
11741174

1175-
export const postWorkspaceAgentSharedPort = async (
1175+
export const upsertWorkspaceAgentSharedPort = async (
11761176
workspaceID: string,
1177-
req: TypesGen.UpdateWorkspaceAgentPortShareRequest,
1177+
req: TypesGen.UpsertWorkspaceAgentPortShareRequest,
11781178
): Promise<TypesGen.WorkspaceAgentPortShares> => {
11791179
const response = await axios.post(
1180-
`/api/v2/workspaces/${workspaceID}/shared-port`,
1180+
`/api/v2/workspaces/${workspaceID}/port-share`,
11811181
req
11821182
);
11831183
return response.data;
@@ -1188,7 +1188,7 @@ export const deleteWorkspaceAgentSharedPort = async (
11881188
req: TypesGen.DeleteWorkspaceAgentPortShareRequest,
11891189
): Promise<TypesGen.WorkspaceAgentPortShares> => {
11901190
const response = await axios.delete(
1191-
`/api/v2/workspaces/${workspaceID}/shared-port`,
1191+
`/api/v2/workspaces/${workspaceID}/port-share`,
11921192
{
11931193
data: req,
11941194
}

site/src/modules/resources/PortForwardButton.stories.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { PortForwardButton } from "./PortForwardButton";
22
import type { Meta, StoryObj } from "@storybook/react";
33
import {
44
MockListeningPortsResponse,
5+
MockSharedPortsResponse,
56
MockWorkspaceAgent,
67
} from "testHelpers/entities";
78

@@ -20,6 +21,7 @@ export const Example: Story = {
2021
args: {
2122
storybook: {
2223
listeningPortsQueryData: MockListeningPortsResponse,
24+
sharedPortsQueryData: MockSharedPortsResponse,
2325
},
2426
},
2527
};

site/src/modules/resources/PortForwardButton.tsx

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,16 @@ import Link from "@mui/material/Link";
33
import CircularProgress from "@mui/material/CircularProgress";
44
import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined";
55
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
6-
import type { FC } from "react";
6+
import { useState, type FC } from "react";
77
import { useQuery, useMutation } from "react-query";
88
import { docs } from "utils/docs";
9-
import { deleteWorkspaceAgentSharedPort, getAgentListeningPorts, getWorkspaceAgentSharedPorts, postWorkspaceAgentSharedPort } from "api/api";
9+
import { deleteWorkspaceAgentSharedPort, getAgentListeningPorts, getWorkspaceAgentSharedPorts, upsertWorkspaceAgentSharedPort } from "api/api";
1010
import type {
1111
DeleteWorkspaceAgentPortShareRequest,
12-
UpdateWorkspaceAgentPortShareRequest,
12+
UpsertWorkspaceAgentPortShareRequest,
1313
WorkspaceAgent,
1414
WorkspaceAgentListeningPort,
1515
WorkspaceAgentListeningPortsResponse,
16-
WorkspaceAgentPortShare,
1716
WorkspaceAgentPortShareLevel,
1817
WorkspaceAgentPortShares,
1918
} from "api/typesGenerated";
@@ -58,7 +57,7 @@ export interface PortForwardButtonProps {
5857
}
5958

6059
export const PortForwardButton: FC<PortForwardButtonProps> = (props) => {
61-
const { agent, workspaceID, storybook } = props;
60+
const { agent, storybook } = props;
6261

6362
const paper = useClassName(classNames.paper, []);
6463

@@ -69,18 +68,9 @@ export const PortForwardButton: FC<PortForwardButtonProps> = (props) => {
6968
refetchInterval: 5_000,
7069
});
7170

72-
const sharedPortsQuery = useQuery({
73-
queryKey: ["sharedPorts", workspaceID],
74-
queryFn: () => getWorkspaceAgentSharedPorts(workspaceID),
75-
enabled: !storybook && agent.status === "connected",
76-
});
77-
7871
const listeningPorts = storybook
7972
? storybook.listeningPortsQueryData
8073
: portsQuery.data;
81-
const sharedPorts = storybook
82-
? storybook.sharedPortsQueryData
83-
: sharedPortsQuery.data;
8474

8575
return (
8676
<Popover>
@@ -110,7 +100,6 @@ export const PortForwardButton: FC<PortForwardButtonProps> = (props) => {
110100
<PortForwardPopoverView
111101
{...props}
112102
listeningPorts={listeningPorts?.ports}
113-
sharedPorts={sharedPorts?.shares}
114103
/>
115104
</PopoverContent>
116105
</Popover>
@@ -119,7 +108,6 @@ export const PortForwardButton: FC<PortForwardButtonProps> = (props) => {
119108

120109
interface PortForwardPopoverViewProps extends PortForwardButtonProps {
121110
listeningPorts?: WorkspaceAgentListeningPort[];
122-
sharedPorts?: WorkspaceAgentPortShare[];
123111
}
124112

125113
export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
@@ -129,14 +117,24 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
129117
agent,
130118
username,
131119
listeningPorts,
132-
sharedPorts,
120+
storybook,
133121
}) => {
134122
const theme = useTheme();
123+
const [selectedShareLevel, setSelectedShareLevel] = useState<WorkspaceAgentPortShareLevel>("authenticated");
124+
const [selectedPort, setSelectedPort] = useState<string>("");
135125

126+
const sharedPortsQuery = useQuery({
127+
queryKey: ["sharedPorts", workspaceID],
128+
queryFn: () => getWorkspaceAgentSharedPorts(workspaceID),
129+
enabled: !storybook && agent.status === "connected",
130+
});
131+
const sharedPorts = storybook
132+
? storybook.sharedPortsQueryData?.shares || []
133+
: sharedPortsQuery.data?.shares || [];
136134

137135
const createSharedPortMutation = useMutation({
138-
mutationFn: async (options: UpdateWorkspaceAgentPortShareRequest) => {
139-
await postWorkspaceAgentSharedPort(workspaceID, options);
136+
mutationFn: async (options: UpsertWorkspaceAgentPortShareRequest) => {
137+
await upsertWorkspaceAgentSharedPort(workspaceID, options);
140138
},
141139
});
142140

@@ -149,10 +147,6 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
149147
// we don't want to show listening ports if it's already a shared port
150148
const filteredListeningPorts = listeningPorts?.filter(
151149
(port) => {
152-
if (sharedPorts === undefined) {
153-
return true;
154-
}
155-
156150
for (let i = 0; i < sharedPorts.length; i++) {
157151
if (sharedPorts[i].port === port.port && sharedPorts[i].agent_name === agent.name) {
158152
return false;
@@ -352,11 +346,12 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
352346
<MenuItem value="public">Public</MenuItem>
353347
</Select>
354348
</FormControl>
355-
<IconButton onClick={() => {
356-
deleteSharedPortMutation.mutate({
349+
<IconButton onClick={async () => {
350+
await deleteSharedPortMutation.mutateAsync({
357351
agent_name: agent.name,
358352
port: share.port,
359353
});
354+
await sharedPortsQuery.refetch();
360355
}}>
361356
<CloseIcon
362357
css={{
@@ -379,15 +374,37 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
379374
marginTop: 2,
380375
}}
381376
>
382-
<TextField label="Port" variant="outlined" size="small" />
377+
<TextField
378+
label="Port"
379+
variant="outlined"
380+
size="small"
381+
onChange={(event) => {
382+
setSelectedPort(event.target.value);
383+
}}
384+
/>
383385
<FormControl size="small">
384-
<Select value="authenticated">
386+
<Select
387+
value={selectedShareLevel}
388+
onChange={(event) => {
389+
setSelectedShareLevel(event.target.value as WorkspaceAgentPortShareLevel);
390+
}}>
385391
<MenuItem value="authenticated">Authenticated</MenuItem>
386392
<MenuItem value="public">Public</MenuItem>
387393
</Select>
388394
</FormControl>
389-
{/* How do I use the value from the select in the mutation? */}
390-
<Button variant="contained">Share Port</Button>
395+
<Button
396+
variant="contained"
397+
onClick={async () => {
398+
await createSharedPortMutation.mutateAsync({
399+
agent_name: agent.name,
400+
port: Number(selectedPort),
401+
share_level: selectedShareLevel,
402+
});
403+
await sharedPortsQuery.refetch();
404+
}}
405+
>
406+
Share Port
407+
</Button>
391408
</Stack>
392409
</div>
393410
</>

site/src/modules/resources/PortForwardPopoverView.stories.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,15 @@ type Story = StoryObj<typeof PortForwardPopoverView>;
3434
export const WithPorts: Story = {
3535
args: {
3636
listeningPorts: MockListeningPortsResponse.ports,
37-
sharedPorts: MockSharedPortsResponse.shares,
37+
storybook: {
38+
sharedPortsQueryData: MockSharedPortsResponse
39+
},
3840
},
3941
};
4042

4143
export const Empty: Story = {
4244
args: {
4345
listeningPorts: [],
44-
sharedPorts: [],
46+
4547
},
4648
};

site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type Interpolation, type Theme } from "@emotion/react";
22
import TextField from "@mui/material/TextField";
3-
import type { Template, UpdateTemplateMeta } from "api/typesGenerated";
3+
import { WorkspaceAppSharingLevels, type Template, type UpdateTemplateMeta } from "api/typesGenerated";
44
import { type FormikContextType, type FormikTouched, useFormik } from "formik";
55
import { type FC } from "react";
66
import {
@@ -43,6 +43,8 @@ export const getValidationSchema = (): Yup.AnyObjectSchema =>
4343
allow_user_cancel_workspace_jobs: Yup.boolean(),
4444
icon: iconValidator,
4545
require_active_version: Yup.boolean(),
46+
deprecation_message: Yup.string(),
47+
max_port_sharing_level: Yup.string().oneOf(WorkspaceAppSharingLevels),
4648
});
4749

4850
export interface TemplateSettingsForm {
@@ -54,6 +56,8 @@ export interface TemplateSettingsForm {
5456
// Helpful to show field errors on Storybook
5557
initialTouched?: FormikTouched<UpdateTemplateMeta>;
5658
accessControlEnabled: boolean;
59+
portSharingExperimentEnabled: boolean;
60+
portSharingControlsEnabled: boolean;
5761
}
5862

5963
export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
@@ -64,6 +68,8 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
6468
isSubmitting,
6569
initialTouched,
6670
accessControlEnabled,
71+
portSharingExperimentEnabled,
72+
portSharingControlsEnabled,
6773
}) => {
6874
const validationSchema = getValidationSchema();
6975
const form: FormikContextType<UpdateTemplateMeta> =
@@ -257,6 +263,46 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
257263
</FormFields>
258264
</FormSection>
259265

266+
{portSharingExperimentEnabled && (
267+
<FormSection
268+
title="Port Sharing"
269+
description="Shared ports with the Public sharing level can be accessed by anyone,
270+
while ports with the Authenticated sharing level can only be accessed
271+
by authenticated Coder users. Ports with the Owner sharing level can
272+
only be accessed by the workspace owner."
273+
>
274+
<FormFields>
275+
<Stack direction="column" spacing={0.5}>
276+
<Stack
277+
direction="row"
278+
alignItems="center"
279+
spacing={0.5}
280+
css={styles.optionText}
281+
>
282+
Maximum Port Sharing Level
283+
</Stack>
284+
<span css={styles.optionHelperText}>
285+
The maximum level of port sharing allowed for workspaces.
286+
</span>
287+
</Stack>
288+
<TextField
289+
{...getFieldHelpers("max_port_share_level")}
290+
disabled={isSubmitting || !portSharingControlsEnabled}
291+
fullWidth
292+
label="Maximum Port Sharing Level"
293+
/>
294+
{!portSharingControlsEnabled && (
295+
<Stack direction="row">
296+
<EnterpriseBadge />
297+
<span css={styles.optionHelperText}>
298+
Enterprise license required to control max port sharing level.
299+
</span>
300+
</Stack>
301+
)}
302+
</FormFields>
303+
</FormSection>
304+
)}
305+
260306
<FormFooter onCancel={onCancel} isLoading={isSubmitting} />
261307
</HorizontalForm>
262308
);

site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export const TemplateSettingsPageView: FC<TemplateSettingsPageViewProps> = ({
3838
onCancel={onCancel}
3939
error={submitError}
4040
accessControlEnabled={accessControlEnabled}
41+
portSharingExperimentEnabled
42+
portSharingControlsEnabled
4143
/>
4244
</>
4345
);

site/src/testHelpers/entities.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3247,16 +3247,18 @@ export const MockListeningPortsResponse: TypesGen.WorkspaceAgentListeningPortsRe
32473247
export const MockSharedPortsResponse: TypesGen.WorkspaceAgentPortShares = {
32483248
shares: [
32493249
{
3250+
workspace_id: MockWorkspace.id,
32503251
agent_name: "a-workspace-agent",
32513252
port: 4000,
32523253
share_level: "authenticated",
32533254
},
32543255
{
3256+
workspace_id: MockWorkspace.id,
32553257
agent_name: "a-workspace-agent",
32563258
port: 8080,
32573259
share_level: "authenticated",
32583260
},
3259-
{ agent_name: "a-workspace-agent", port: 8081, share_level: "public" },
3261+
{ workspace_id: MockWorkspace.id, agent_name: "a-workspace-agent", port: 8081, share_level: "public" },
32603262
],
32613263
};
32623264

0 commit comments

Comments
 (0)