1- import React , { useEffect } from 'react' ;
1+ import React , { useCallback , useEffect , useState } from 'react' ;
22import TitledGreyBox from '@/components/elements/TitledGreyBox' ;
33import tw from 'twin.macro' ;
44import VariableBox from '@/components/server/startup/VariableBox' ;
@@ -9,15 +9,32 @@ import ServerError from '@/components/screens/ServerError';
99import { httpErrorToHuman } from '@/api/http' ;
1010import { ServerContext } from '@/state/server' ;
1111import { useDeepCompareEffect } from '@/plugins/useDeepCompareEffect' ;
12+ import Select from '@/components/elements/Select' ;
13+ import isEqual from 'react-fast-compare' ;
14+ import Input from '@/components/elements/Input' ;
15+ import setSelectedDockerImage from '@/api/server/setSelectedDockerImage' ;
16+ import InputSpinner from '@/components/elements/InputSpinner' ;
17+ import useFlash from '@/plugins/useFlash' ;
1218
1319const StartupContainer = ( ) => {
20+ const [ loading , setLoading ] = useState ( false ) ;
21+ const { clearFlashes, clearAndAddHttpError } = useFlash ( ) ;
22+
1423 const uuid = ServerContext . useStoreState ( state => state . server . data ! . uuid ) ;
15- const invocation = ServerContext . useStoreState ( state => state . server . data ! . invocation ) ;
16- const variables = ServerContext . useStoreState ( state => state . server . data ! . variables ) ;
24+ const variables = ServerContext . useStoreState ( ( { server } ) => ( {
25+ variables : server . data ! . variables ,
26+ invocation : server . data ! . invocation ,
27+ dockerImage : server . data ! . dockerImage ,
28+ // @ts -ignore
29+ } ) , isEqual ) ;
1730
18- const { data, error, isValidating, mutate } = getServerStartup ( uuid , { invocation, variables } ) ;
31+ const { data, error, isValidating, mutate } = getServerStartup ( uuid , {
32+ ...variables ,
33+ dockerImages : [ variables . dockerImage ] ,
34+ } ) ;
1935
2036 const setServerFromState = ServerContext . useStoreActions ( actions => actions . server . setServerFromState ) ;
37+ const isCustomImage = data && ! data . dockerImages . map ( v => v . toLowerCase ( ) ) . includes ( variables . dockerImage . toLowerCase ( ) ) ;
2138
2239 useEffect ( ( ) => {
2340 // Since we're passing in initial data this will not trigger on mount automatically. We
@@ -36,6 +53,20 @@ const StartupContainer = () => {
3653 } ) ) ;
3754 } , [ data ] ) ;
3855
56+ const updateSelectedDockerImage = useCallback ( ( v : React . ChangeEvent < HTMLSelectElement > ) => {
57+ setLoading ( true ) ;
58+ clearFlashes ( 'startup:image' ) ;
59+
60+ const image = v . currentTarget . value ;
61+ setSelectedDockerImage ( uuid , image )
62+ . then ( ( ) => setServerFromState ( s => ( { ...s , dockerImage : image } ) ) )
63+ . catch ( error => {
64+ console . error ( error ) ;
65+ clearAndAddHttpError ( { key : 'startup:image' , error } ) ;
66+ } )
67+ . then ( ( ) => setLoading ( false ) ) ;
68+ } , [ uuid ] ) ;
69+
3970 return (
4071 ! data ?
4172 ( ! error || ( error && isValidating ) ) ?
@@ -47,15 +78,49 @@ const StartupContainer = () => {
4778 onRetry = { ( ) => mutate ( ) }
4879 />
4980 :
50- < ServerContentBlock title = { 'Startup Settings' } >
51- < TitledGreyBox title = { 'Startup Command' } >
52- < div css = { tw `px-1 py-2` } >
53- < p css = { tw `font-mono bg-neutral-900 rounded py-2 px-4` } >
54- { data . invocation }
55- </ p >
56- </ div >
57- </ TitledGreyBox >
58- < div css = { tw `grid gap-8 md:grid-cols-2 mt-10` } >
81+ < ServerContentBlock title = { 'Startup Settings' } showFlashKey = { 'startup:image' } >
82+ < div css = { tw `flex` } >
83+ < TitledGreyBox title = { 'Startup Command' } css = { tw `flex-1` } >
84+ < div css = { tw `px-1 py-2` } >
85+ < p css = { tw `font-mono bg-neutral-900 rounded py-2 px-4` } >
86+ { data . invocation }
87+ </ p >
88+ </ div >
89+ </ TitledGreyBox >
90+ < TitledGreyBox title = { 'Docker Image' } css = { tw `flex-1 lg:flex-none lg:w-1/3 ml-10` } >
91+ { data . dockerImages . length > 1 && ! isCustomImage ?
92+ < >
93+ < InputSpinner visible = { loading } >
94+ < Select
95+ disabled = { data . dockerImages . length < 2 }
96+ onChange = { updateSelectedDockerImage }
97+ defaultValue = { variables . dockerImage }
98+ >
99+ { data . dockerImages . map ( image => (
100+ < option key = { image } value = { image } > { image } </ option >
101+ ) ) }
102+ </ Select >
103+ </ InputSpinner >
104+ < p css = { tw `text-xs text-neutral-300 mt-2` } >
105+ This is an advanced feature allowing you to select a Docker image to use when
106+ running this server instance.
107+ </ p >
108+ </ >
109+ :
110+ < >
111+ < Input disabled readOnly value = { variables . dockerImage } />
112+ { isCustomImage &&
113+ < p css = { tw `text-xs text-neutral-300 mt-2` } >
114+ This { 'server\'s' } Docker image has been manually set by an administrator and cannot
115+ be changed through this UI.
116+ </ p >
117+ }
118+ </ >
119+ }
120+ </ TitledGreyBox >
121+ </ div >
122+ < h3 css = { tw `mt-8 mb-2 text-2xl` } > Variables</ h3 >
123+ < div css = { tw `grid gap-8 md:grid-cols-2` } >
59124 { data . variables . map ( variable => < VariableBox key = { variable . envVariable } variable = { variable } /> ) }
60125 </ div >
61126 </ ServerContentBlock >
0 commit comments