5
5
*/
6
6
7
7
import { OrganizationSettings } from "@gitpod/gitpod-protocol" ;
8
- import React , { useCallback , useState , useEffect } from "react" ;
8
+ import React , { Children , ReactNode , useCallback , useMemo , useState } from "react" ;
9
9
import Alert from "../components/Alert" ;
10
10
import { Button } from "../components/Button" ;
11
11
import { CheckboxInputField } from "../components/forms/CheckboxInputField" ;
@@ -21,8 +21,10 @@ import { teamsService } from "../service/public-api";
21
21
import { gitpodHostUrl } from "../service/service" ;
22
22
import { useCurrentUser } from "../user-context" ;
23
23
import { OrgSettingsPage } from "./OrgSettingsPage" ;
24
- import { useToast } from "../components/toasts/Toasts" ;
25
24
import { useDefaultWorkspaceImageQuery } from "../data/workspaces/default-workspace-image-query" ;
25
+ import Modal , { ModalBody , ModalFooter , ModalHeader } from "../components/Modal" ;
26
+ import { InputField } from "../components/forms/InputField" ;
27
+ import { ReactComponent as Stack } from "../icons/Stack.svg" ;
26
28
27
29
export default function TeamSettingsPage ( ) {
28
30
const user = useCurrentUser ( ) ;
@@ -172,15 +174,8 @@ function OrgSettingsForm(props: { org?: OrganizationInfo }) {
172
174
const { data : settings , isLoading } = useOrgSettingsQuery ( ) ;
173
175
const { data : globalDefaultImage } = useDefaultWorkspaceImageQuery ( ) ;
174
176
const updateTeamSettings = useUpdateOrgSettingsMutation ( ) ;
175
- const [ defaultWorkspaceImage , setDefaultWorkspaceImage ] = useState ( settings ?. defaultWorkspaceImage ?? "" ) ;
176
- const { toast } = useToast ( ) ;
177
177
178
- useEffect ( ( ) => {
179
- if ( ! settings ) {
180
- return ;
181
- }
182
- setDefaultWorkspaceImage ( settings . defaultWorkspaceImage ?? "" ) ;
183
- } , [ settings ] ) ;
178
+ const [ showImageEditModal , setShowImageEditModal ] = useState ( false ) ;
184
179
185
180
const handleUpdateTeamSettings = useCallback (
186
181
async ( newSettings : Partial < OrganizationSettings > ) => {
@@ -195,32 +190,21 @@ function OrgSettingsForm(props: { org?: OrganizationInfo }) {
195
190
...settings ,
196
191
...newSettings ,
197
192
} ) ;
198
- if ( newSettings . defaultWorkspaceImage !== undefined ) {
199
- toast ( "Default workspace image has been updated." ) ;
200
- }
201
193
} catch ( error ) {
202
194
console . error ( error ) ;
203
- toast (
204
- error . message
205
- ? "Failed to update organization settings: " + error . message
206
- : "Oh no, there was a problem with our service." ,
207
- ) ;
208
195
}
209
196
} ,
210
- [ updateTeamSettings , org ?. id , org ?. isOwner , settings , toast ] ,
197
+ [ updateTeamSettings , org ?. id , org ?. isOwner , settings ] ,
211
198
) ;
212
199
213
200
return (
214
201
< form
215
202
onSubmit = { ( e ) => {
216
203
e . preventDefault ( ) ;
217
- handleUpdateTeamSettings ( { defaultWorkspaceImage } ) ;
204
+ // handleUpdateTeamSettings({ defaultWorkspaceImage });
218
205
} }
219
206
>
220
207
< Heading2 className = "pt-12" > Collaboration & Sharing </ Heading2 >
221
- < Subheading className = "max-w-2xl" >
222
- Choose which workspace images you want to use for your workspaces.
223
- </ Subheading >
224
208
225
209
{ updateTeamSettings . isError && (
226
210
< Alert type = "error" closable = { true } className = "mb-2 max-w-xl rounded-md" >
@@ -237,22 +221,173 @@ function OrgSettingsForm(props: { org?: OrganizationInfo }) {
237
221
disabled = { isLoading || ! org ?. isOwner }
238
222
/>
239
223
240
- < Heading2 className = "pt-12" > Workspace Settings</ Heading2 >
241
- < TextInputField
242
- label = "Default Image"
243
- // TODO: Provide document links
244
- hint = "Use any official Gitpod Docker image, or Docker image reference"
245
- placeholder = { globalDefaultImage }
246
- value = { defaultWorkspaceImage }
247
- onChange = { setDefaultWorkspaceImage }
248
- disabled = { isLoading || ! org ?. isOwner }
224
+ < Heading2 className = "pt-12" > Workspace Images</ Heading2 >
225
+ < Subheading className = "max-w-2xl" >
226
+ Choose a default image for all workspaces in the organization.
227
+ </ Subheading >
228
+
229
+ < WorkspaceImageButton
230
+ disabled = { ! org ?. isOwner }
231
+ settings = { settings }
232
+ defaultWorkspaceImage = { globalDefaultImage }
233
+ onClick = { ( ) => setShowImageEditModal ( true ) }
249
234
/>
250
235
251
- { org ?. isOwner && (
252
- < Button htmlType = "submit" className = "mt-4" disabled = { ! org . isOwner } >
253
- Update Default Image
254
- </ Button >
236
+ { showImageEditModal && (
237
+ < OrgDefaultWorkspaceImageModal
238
+ settings = { settings }
239
+ globalDefaultImage = { globalDefaultImage }
240
+ onClose = { ( ) => setShowImageEditModal ( false ) }
241
+ />
255
242
) }
256
243
</ form >
257
244
) ;
258
245
}
246
+
247
+ function WorkspaceImageButton ( props : {
248
+ settings ?: OrganizationSettings ;
249
+ defaultWorkspaceImage ?: string ;
250
+ onClick : ( ) => void ;
251
+ disabled ?: boolean ;
252
+ } ) {
253
+ function parseDockerImage ( image : string ) {
254
+ // https://docs.docker.com/registry/spec/api/
255
+ let registry , repository , tag ;
256
+ let parts = image . split ( "/" ) ;
257
+
258
+ if ( parts . length > 1 && parts [ 0 ] . includes ( "." ) ) {
259
+ registry = parts . shift ( ) ;
260
+ } else {
261
+ registry = "docker.io" ;
262
+ }
263
+
264
+ const remaining = parts . join ( "/" ) ;
265
+ [ repository , tag ] = remaining . split ( ":" ) ;
266
+ if ( ! tag ) {
267
+ tag = "latest" ;
268
+ }
269
+ return {
270
+ registry,
271
+ repository,
272
+ tag,
273
+ } ;
274
+ }
275
+
276
+ const image = props . settings ?. defaultWorkspaceImage ?? props . defaultWorkspaceImage ?? "" ;
277
+
278
+ const descList = useMemo ( ( ) => {
279
+ const arr : ReactNode [ ] = [ ] ;
280
+ if ( ! props . settings ?. defaultWorkspaceImage ) {
281
+ arr . push ( < span className = "font-medium" > Default image</ span > ) ;
282
+ }
283
+ if ( props . disabled ) {
284
+ arr . push (
285
+ < >
286
+ Requires < span className = "font-medium" > Owner</ span > permissions to change
287
+ </ > ,
288
+ ) ;
289
+ }
290
+ return arr ;
291
+ } , [ props . settings , props . disabled ] ) ;
292
+
293
+ const renderedDescription = useMemo ( ( ) => {
294
+ return Children . toArray ( descList ) . reduce ( ( acc : ReactNode [ ] , child , index ) => {
295
+ acc . push ( child ) ;
296
+ if ( index < descList . length - 1 ) {
297
+ acc . push ( < > · </ > ) ;
298
+ }
299
+ return acc ;
300
+ } , [ ] ) ;
301
+ } , [ descList ] ) ;
302
+
303
+ return (
304
+ < InputField disabled = { props . disabled } className = "w-full max-w-lg" >
305
+ < div className = "flex flex-col bg-gray-50 dark:bg-gray-800 p-3 rounded-lg" >
306
+ < div className = "flex items-center justify-between" >
307
+ < div className = "flex items-center overflow-hidden h-8" title = { image } >
308
+ < span className = "w-5 h-5 mr-1" >
309
+ < Stack />
310
+ </ span >
311
+ < span className = "truncate font-medium text-gray-700 dark:text-gray-200" >
312
+ { parseDockerImage ( image ) . repository }
313
+ </ span >
314
+ ·
315
+ < span className = "flex-none w-16 truncate text-gray-500 dark:text-gray-400" >
316
+ { parseDockerImage ( image ) . tag }
317
+ </ span >
318
+ </ div >
319
+ { ! props . disabled && (
320
+ < Button htmlType = "button" type = "transparent" className = "text-blue-500" onClick = { props . onClick } >
321
+ Change
322
+ </ Button >
323
+ ) }
324
+ </ div >
325
+ { descList . length > 0 && (
326
+ < div className = "mx-6 text-gray-400 dark:text-gray-500 truncate" > { renderedDescription } </ div >
327
+ ) }
328
+ </ div >
329
+ </ InputField >
330
+ ) ;
331
+ }
332
+
333
+ interface OrgDefaultWorkspaceImageModalProps {
334
+ globalDefaultImage : string | undefined ;
335
+ settings : OrganizationSettings | undefined ;
336
+ onClose : ( ) => void ;
337
+ }
338
+
339
+ function OrgDefaultWorkspaceImageModal ( props : OrgDefaultWorkspaceImageModalProps ) {
340
+ const [ errorMsg , setErrorMsg ] = useState ( "" ) ;
341
+ const [ defaultWorkspaceImage , setDefaultWorkspaceImage ] = useState ( props . settings ?. defaultWorkspaceImage ?? "" ) ;
342
+ const updateTeamSettings = useUpdateOrgSettingsMutation ( ) ;
343
+
344
+ const handleUpdateTeamSettings = useCallback (
345
+ async ( newSettings : Partial < OrganizationSettings > ) => {
346
+ try {
347
+ await updateTeamSettings . mutateAsync ( {
348
+ ...props . settings ,
349
+ ...newSettings ,
350
+ } ) ;
351
+ props . onClose ( ) ;
352
+ } catch ( error ) {
353
+ console . error ( error ) ;
354
+ setErrorMsg ( error . message ) ;
355
+ }
356
+ } ,
357
+ [ updateTeamSettings , props ] ,
358
+ ) ;
359
+
360
+ return (
361
+ < Modal
362
+ visible
363
+ closeable
364
+ onClose = { props . onClose }
365
+ onSubmit = { ( ) => handleUpdateTeamSettings ( { defaultWorkspaceImage } ) }
366
+ >
367
+ < ModalHeader > Workspace Default Image</ ModalHeader >
368
+ < ModalBody >
369
+ < Alert type = "warning" className = "mb-2" >
370
+ < span className = "font-medium" > Warning:</ span > You are setting a default image for all workspaces
371
+ within the organization.
372
+ </ Alert >
373
+ { errorMsg . length > 0 && (
374
+ < Alert type = "error" className = "mb-2" >
375
+ { errorMsg }
376
+ </ Alert >
377
+ ) }
378
+ < div className = "mt-4" >
379
+ < TextInputField
380
+ label = "Default Image"
381
+ hint = "Use any official or custom workspace image from Docker Hub or any private container registry that the Gitpod instance can access."
382
+ placeholder = { props . globalDefaultImage }
383
+ value = { defaultWorkspaceImage }
384
+ onChange = { setDefaultWorkspaceImage }
385
+ />
386
+ </ div >
387
+ </ ModalBody >
388
+ < ModalFooter >
389
+ < Button htmlType = "submit" > Update Workspace Default Image</ Button >
390
+ </ ModalFooter >
391
+ </ Modal >
392
+ ) ;
393
+ }
0 commit comments