@@ -18,8 +18,10 @@ import {
18
18
import {
19
19
type Template ,
20
20
type UpsertWorkspaceAgentPortShareRequest ,
21
+ type Workspace ,
21
22
type WorkspaceAgent ,
22
23
type WorkspaceAgentListeningPort ,
24
+ type WorkspaceAgentPortShare ,
23
25
type WorkspaceAgentPortShareLevel ,
24
26
type WorkspaceAgentPortShareProtocol ,
25
27
WorkspaceAppSharingLevels ,
@@ -64,121 +66,131 @@ import * as Yup from "yup";
64
66
65
67
interface PortForwardButtonProps {
66
68
host : string ;
67
- username : string ;
68
- workspaceName : string ;
69
- workspaceID : string ;
69
+ workspace : Workspace ;
70
70
agent : WorkspaceAgent ;
71
71
template : Template ;
72
72
}
73
73
74
- export const PortForwardButton : FC < PortForwardButtonProps > = ( props ) => {
75
- const { agent } = props ;
74
+ export const PortForwardButton : FC < PortForwardButtonProps > = ( {
75
+ host,
76
+ workspace,
77
+ template,
78
+ agent,
79
+ } ) => {
76
80
const { entitlements } = useDashboard ( ) ;
77
81
const paper = useClassName ( classNames . paper , [ ] ) ;
78
82
79
- const portsQuery = useQuery ( {
83
+ const { data : listeningPorts } = useQuery ( {
80
84
queryKey : [ "portForward" , agent . id ] ,
81
85
queryFn : ( ) => API . getAgentListeningPorts ( agent . id ) ,
82
86
enabled : agent . status === "connected" ,
83
87
refetchInterval : 5_000 ,
88
+ select : ( res ) => res . ports ,
89
+ } ) ;
90
+
91
+ const { data : sharedPorts , refetch : refetchSharedPorts } = useQuery ( {
92
+ ...workspacePortShares ( workspace . id ) ,
93
+ enabled : agent . status === "connected" ,
94
+ select : ( res ) => res . shares ,
95
+ initialData : { shares : [ ] } ,
84
96
} ) ;
85
97
86
98
return (
87
99
< Popover >
88
100
< PopoverTrigger >
89
- < Button disabled = { ! portsQuery . data } size = "sm" variant = "subtle" >
90
- < Spinner loading = { ! portsQuery . data } >
91
- < span css = { styles . portCount } > { portsQuery . data ?. ports . length } </ span >
101
+ < Button disabled = { ! listeningPorts } size = "sm" variant = "subtle" >
102
+ < Spinner loading = { ! listeningPorts } >
103
+ < span css = { styles . portCount } > { listeningPorts ? .length } </ span >
92
104
</ Spinner >
93
105
Open ports
94
106
< ChevronDownIcon className = "size-4" />
95
107
</ Button >
96
108
</ PopoverTrigger >
97
109
< PopoverContent horizontal = "right" classes = { { paper } } >
98
110
< PortForwardPopoverView
99
- { ...props }
100
- listeningPorts = { portsQuery . data ?. ports }
111
+ host = { host }
112
+ agent = { agent }
113
+ workspace = { workspace }
114
+ template = { template }
115
+ sharedPorts = { sharedPorts }
116
+ listeningPorts = { listeningPorts ?? [ ] }
101
117
portSharingControlsEnabled = {
102
118
entitlements . features . control_shared_ports . enabled
103
119
}
120
+ refetchSharedPorts = { refetchSharedPorts }
104
121
/>
105
122
</ PopoverContent >
106
123
</ Popover >
107
124
) ;
108
125
} ;
109
126
110
- const getValidationSchema = ( ) : Yup . AnyObjectSchema =>
127
+ const openPortSchema = ( ) : Yup . AnyObjectSchema =>
111
128
Yup . object ( {
112
129
port : Yup . number ( ) . required ( ) . min ( 9 ) . max ( 65535 ) ,
113
130
share_level : Yup . string ( ) . required ( ) . oneOf ( WorkspaceAppSharingLevels ) ,
114
131
} ) ;
115
132
116
- interface PortForwardPopoverViewProps extends PortForwardButtonProps {
117
- listeningPorts ?: readonly WorkspaceAgentListeningPort [ ] ;
133
+ interface PortForwardPopoverViewProps {
134
+ host : string ;
135
+ workspace : Workspace ;
136
+ agent : WorkspaceAgent ;
137
+ template : Template ;
138
+ sharedPorts : readonly WorkspaceAgentPortShare [ ] ;
139
+ listeningPorts : readonly WorkspaceAgentListeningPort [ ] ;
118
140
portSharingControlsEnabled : boolean ;
141
+ refetchSharedPorts : ( ) => void ;
119
142
}
120
143
121
- type Optional < T , K extends keyof T > = Pick < Partial < T > , K > & Omit < T , K > ;
122
-
123
144
export const PortForwardPopoverView : FC < PortForwardPopoverViewProps > = ( {
124
145
host,
125
- workspaceName,
126
- workspaceID,
146
+ workspace,
127
147
agent,
128
148
template,
129
- username ,
149
+ sharedPorts ,
130
150
listeningPorts,
131
151
portSharingControlsEnabled,
152
+ refetchSharedPorts,
132
153
} ) => {
133
154
const theme = useTheme ( ) ;
134
155
const [ listeningPortProtocol , setListeningPortProtocol ] = useState (
135
- getWorkspaceListeningPortsProtocol ( workspaceID ) ,
156
+ getWorkspaceListeningPortsProtocol ( workspace . id ) ,
136
157
) ;
137
158
138
- const sharedPortsQuery = useQuery ( {
139
- ...workspacePortShares ( workspaceID ) ,
140
- enabled : agent . status === "connected" ,
159
+ const upsertSharedPortMutation = useMutation ( {
160
+ ...upsertWorkspacePortShare ( workspace . id ) ,
161
+ onSuccess : refetchSharedPorts ,
141
162
} ) ;
142
- const sharedPorts = sharedPortsQuery . data ?. shares || [ ] ;
143
163
144
- const upsertSharedPortMutation = useMutation (
145
- upsertWorkspacePortShare ( workspaceID ) ,
146
- ) ;
147
-
148
- const deleteSharedPortMutation = useMutation (
149
- deleteWorkspacePortShare ( workspaceID ) ,
150
- ) ;
164
+ const deleteSharedPortMutation = useMutation ( {
165
+ ...deleteWorkspacePortShare ( workspace . id ) ,
166
+ onSuccess : refetchSharedPorts ,
167
+ } ) ;
151
168
152
- // share port form
153
169
const {
154
170
mutateAsync : upsertWorkspacePortShareForm ,
155
171
isPending : isSubmitting ,
156
172
error : submitError ,
157
- } = useMutation ( upsertWorkspacePortShare ( workspaceID ) ) ;
158
- const validationSchema = getValidationSchema ( ) ;
159
- // TODO: do partial here
160
- const form : FormikContextType <
161
- Optional < UpsertWorkspaceAgentPortShareRequest , "port" >
162
- > = useFormik < Optional < UpsertWorkspaceAgentPortShareRequest , "port" > > ( {
173
+ } = useMutation ( {
174
+ ... upsertWorkspacePortShare ( workspace . id ) ,
175
+ onSuccess : refetchSharedPorts ,
176
+ } ) ;
177
+
178
+ const form = useFormik ( {
163
179
initialValues : {
164
180
agent_name : agent . name ,
165
- port : undefined ,
181
+ port : "" ,
166
182
protocol : "http" ,
167
183
share_level : "authenticated" ,
168
184
} ,
169
- validationSchema,
170
- onSubmit : async ( values ) => {
171
- // we need port to be optional in the initialValues so it appears empty instead of 0.
172
- // because of this we need to reset the form to clear the port field manually.
173
- form . resetForm ( ) ;
174
- await form . setFieldValue ( "port" , "" ) ;
175
-
176
- const port = Number ( values . port ) ;
185
+ validationSchema : openPortSchema ( ) ,
186
+ onSubmit : async ( values , { resetForm } ) => {
187
+ resetForm ( ) ;
177
188
await upsertWorkspacePortShareForm ( {
178
- ...values ,
179
- port,
189
+ agent_name : values . agent_name ,
190
+ port : Number ( values . port ) ,
191
+ share_level : values . share_level as WorkspaceAgentPortShareLevel ,
192
+ protocol : values . protocol as WorkspaceAgentPortShareProtocol ,
180
193
} ) ;
181
- await sharedPortsQuery . refetch ( ) ;
182
194
} ,
183
195
} ) ;
184
196
const getFieldHelpers = getFormHelpers ( form , submitError ) ;
@@ -188,7 +200,7 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
188
200
( port ) => port . agent_name === agent . name ,
189
201
) ;
190
202
// we don't want to show listening ports if it's a shared port
191
- const filteredListeningPorts = ( listeningPorts ?? [ ] ) . filter ( ( port ) =>
203
+ const filteredListeningPorts = listeningPorts . filter ( ( port ) =>
192
204
filteredSharedPorts . every ( ( sharedPort ) => sharedPort . port !== port . port ) ,
193
205
) ;
194
206
// only disable the form if shared port controls are entitled and the template doesn't allow sharing ports
@@ -257,7 +269,7 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
257
269
| "https" ;
258
270
setListeningPortProtocol ( selectedProtocol ) ;
259
271
saveWorkspaceListeningPortsProtocol (
260
- workspaceID ,
272
+ workspace . id ,
261
273
selectedProtocol ,
262
274
) ;
263
275
} }
@@ -276,8 +288,8 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
276
288
host ,
277
289
port ,
278
290
agent . name ,
279
- workspaceName ,
280
- username ,
291
+ workspace . name ,
292
+ workspace . owner_name ,
281
293
listeningPortProtocol ,
282
294
) ;
283
295
window . open ( url , "_blank" ) ;
@@ -317,8 +329,8 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
317
329
host ,
318
330
port . port ,
319
331
agent . name ,
320
- workspaceName ,
321
- username ,
332
+ workspace . name ,
333
+ workspace . owner_name ,
322
334
listeningPortProtocol ,
323
335
) ;
324
336
const label =
@@ -371,7 +383,6 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
371
383
protocol : listeningPortProtocol ,
372
384
share_level : "authenticated" ,
373
385
} ) ;
374
- await sharedPortsQuery . refetch ( ) ;
375
386
} }
376
387
>
377
388
< ShareIcon />
@@ -407,8 +418,8 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
407
418
host ,
408
419
share . port ,
409
420
agent . name ,
410
- workspaceName ,
411
- username ,
421
+ workspace . name ,
422
+ workspace . owner_name ,
412
423
share . protocol ,
413
424
) ;
414
425
const label = share . port ;
@@ -445,7 +456,6 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
445
456
. value as WorkspaceAgentPortShareProtocol ,
446
457
share_level : share . share_level ,
447
458
} ) ;
448
- await sharedPortsQuery . refetch ( ) ;
449
459
} }
450
460
>
451
461
< MenuItem value = "http" > HTTP</ MenuItem >
@@ -469,7 +479,6 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
469
479
share_level : event . target
470
480
. value as WorkspaceAgentPortShareLevel ,
471
481
} ) ;
472
- await sharedPortsQuery . refetch ( ) ;
473
482
} }
474
483
>
475
484
< MenuItem value = "authenticated" > Authenticated</ MenuItem >
@@ -488,7 +497,6 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
488
497
agent_name : agent . name ,
489
498
port : share . port ,
490
499
} ) ;
491
- await sharedPortsQuery . refetch ( ) ;
492
500
} }
493
501
>
494
502
< XIcon
0 commit comments