55 */
66
77import { OrganizationSettings } from "@gitpod/gitpod-protocol" ;
8- import React , { useCallback , useState , useEffect } from "react" ;
8+ import React , { Children , ReactNode , useCallback , useMemo , useState } from "react" ;
99import Alert from "../components/Alert" ;
1010import { Button } from "../components/Button" ;
1111import { CheckboxInputField } from "../components/forms/CheckboxInputField" ;
@@ -21,8 +21,10 @@ import { teamsService } from "../service/public-api";
2121import { gitpodHostUrl } from "../service/service" ;
2222import { useCurrentUser } from "../user-context" ;
2323import { OrgSettingsPage } from "./OrgSettingsPage" ;
24- import { useToast } from "../components/toasts/Toasts" ;
2524import { 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" ;
2628
2729export default function TeamSettingsPage ( ) {
2830 const user = useCurrentUser ( ) ;
@@ -172,15 +174,8 @@ function OrgSettingsForm(props: { org?: OrganizationInfo }) {
172174 const { data : settings , isLoading } = useOrgSettingsQuery ( ) ;
173175 const { data : globalDefaultImage } = useDefaultWorkspaceImageQuery ( ) ;
174176 const updateTeamSettings = useUpdateOrgSettingsMutation ( ) ;
175- const [ defaultWorkspaceImage , setDefaultWorkspaceImage ] = useState ( settings ?. defaultWorkspaceImage ?? "" ) ;
176- const { toast } = useToast ( ) ;
177177
178- useEffect ( ( ) => {
179- if ( ! settings ) {
180- return ;
181- }
182- setDefaultWorkspaceImage ( settings . defaultWorkspaceImage ?? "" ) ;
183- } , [ settings ] ) ;
178+ const [ showImageEditModal , setShowImageEditModal ] = useState ( false ) ;
184179
185180 const handleUpdateTeamSettings = useCallback (
186181 async ( newSettings : Partial < OrganizationSettings > ) => {
@@ -195,32 +190,21 @@ function OrgSettingsForm(props: { org?: OrganizationInfo }) {
195190 ...settings ,
196191 ...newSettings ,
197192 } ) ;
198- if ( newSettings . defaultWorkspaceImage !== undefined ) {
199- toast ( "Default workspace image has been updated." ) ;
200- }
201193 } catch ( error ) {
202194 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- ) ;
208195 }
209196 } ,
210- [ updateTeamSettings , org ?. id , org ?. isOwner , settings , toast ] ,
197+ [ updateTeamSettings , org ?. id , org ?. isOwner , settings ] ,
211198 ) ;
212199
213200 return (
214201 < form
215202 onSubmit = { ( e ) => {
216203 e . preventDefault ( ) ;
217- handleUpdateTeamSettings ( { defaultWorkspaceImage } ) ;
204+ // handleUpdateTeamSettings({ defaultWorkspaceImage });
218205 } }
219206 >
220207 < 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 >
224208
225209 { updateTeamSettings . isError && (
226210 < Alert type = "error" closable = { true } className = "mb-2 max-w-xl rounded-md" >
@@ -237,22 +221,173 @@ function OrgSettingsForm(props: { org?: OrganizationInfo }) {
237221 disabled = { isLoading || ! org ?. isOwner }
238222 />
239223
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 ) }
249234 />
250235
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+ />
255242 ) }
256243 </ form >
257244 ) ;
258245}
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