Support updating docker image for a server from the frontend
This commit is contained in:
parent
1dacd703df
commit
5bbb36b3cf
12 changed files with 171 additions and 19 deletions
|
@ -2,16 +2,28 @@ import React from 'react';
|
|||
import Spinner from '@/components/elements/Spinner';
|
||||
import Fade from '@/components/elements/Fade';
|
||||
import tw from 'twin.macro';
|
||||
import styled, { css } from 'styled-components/macro';
|
||||
import Select from '@/components/elements/Select';
|
||||
|
||||
const Container = styled.div<{ visible?: boolean }>`
|
||||
${tw`relative`};
|
||||
|
||||
${props => props.visible && css`
|
||||
& ${Select} {
|
||||
background-image: none;
|
||||
}
|
||||
`};
|
||||
`;
|
||||
|
||||
const InputSpinner = ({ visible, children }: { visible: boolean, children: React.ReactNode }) => (
|
||||
<div css={tw`relative`}>
|
||||
<Container visible={visible}>
|
||||
<Fade appear unmountOnExit in={visible} timeout={150}>
|
||||
<div css={tw`absolute right-0 h-full flex items-center justify-end pr-3`}>
|
||||
<Spinner size={'small'}/>
|
||||
</div>
|
||||
</Fade>
|
||||
{children}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
|
||||
export default InputSpinner;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import TitledGreyBox from '@/components/elements/TitledGreyBox';
|
||||
import tw from 'twin.macro';
|
||||
import VariableBox from '@/components/server/startup/VariableBox';
|
||||
|
@ -9,15 +9,32 @@ import ServerError from '@/components/screens/ServerError';
|
|||
import { httpErrorToHuman } from '@/api/http';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import { useDeepCompareEffect } from '@/plugins/useDeepCompareEffect';
|
||||
import Select from '@/components/elements/Select';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import Input from '@/components/elements/Input';
|
||||
import setSelectedDockerImage from '@/api/server/setSelectedDockerImage';
|
||||
import InputSpinner from '@/components/elements/InputSpinner';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
|
||||
const StartupContainer = () => {
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const invocation = ServerContext.useStoreState(state => state.server.data!.invocation);
|
||||
const variables = ServerContext.useStoreState(state => state.server.data!.variables);
|
||||
const [ loading, setLoading ] = useState(false);
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
|
||||
const { data, error, isValidating, mutate } = getServerStartup(uuid, { invocation, variables });
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const variables = ServerContext.useStoreState(({ server }) => ({
|
||||
variables: server.data!.variables,
|
||||
invocation: server.data!.invocation,
|
||||
dockerImage: server.data!.dockerImage,
|
||||
// @ts-ignore
|
||||
}), isEqual);
|
||||
|
||||
const { data, error, isValidating, mutate } = getServerStartup(uuid, {
|
||||
...variables,
|
||||
dockerImages: [ variables.dockerImage ],
|
||||
});
|
||||
|
||||
const setServerFromState = ServerContext.useStoreActions(actions => actions.server.setServerFromState);
|
||||
const isCustomImage = data && !data.dockerImages.map(v => v.toLowerCase()).includes(variables.dockerImage.toLowerCase());
|
||||
|
||||
useEffect(() => {
|
||||
// Since we're passing in initial data this will not trigger on mount automatically. We
|
||||
|
@ -36,6 +53,20 @@ const StartupContainer = () => {
|
|||
}));
|
||||
}, [ data ]);
|
||||
|
||||
const updateSelectedDockerImage = useCallback((v: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setLoading(true);
|
||||
clearFlashes('startup:image');
|
||||
|
||||
const image = v.currentTarget.value;
|
||||
setSelectedDockerImage(uuid, image)
|
||||
.then(() => setServerFromState(s => ({ ...s, dockerImage: image })))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
clearAndAddHttpError({ key: 'startup:image', error });
|
||||
})
|
||||
.then(() => setLoading(false));
|
||||
}, [ uuid ]);
|
||||
|
||||
return (
|
||||
!data ?
|
||||
(!error || (error && isValidating)) ?
|
||||
|
@ -47,15 +78,49 @@ const StartupContainer = () => {
|
|||
onRetry={() => mutate()}
|
||||
/>
|
||||
:
|
||||
<ServerContentBlock title={'Startup Settings'}>
|
||||
<TitledGreyBox title={'Startup Command'}>
|
||||
<div css={tw`px-1 py-2`}>
|
||||
<p css={tw`font-mono bg-neutral-900 rounded py-2 px-4`}>
|
||||
{data.invocation}
|
||||
</p>
|
||||
</div>
|
||||
</TitledGreyBox>
|
||||
<div css={tw`grid gap-8 md:grid-cols-2 mt-10`}>
|
||||
<ServerContentBlock title={'Startup Settings'} showFlashKey={'startup:image'}>
|
||||
<div css={tw`flex`}>
|
||||
<TitledGreyBox title={'Startup Command'} css={tw`flex-1`}>
|
||||
<div css={tw`px-1 py-2`}>
|
||||
<p css={tw`font-mono bg-neutral-900 rounded py-2 px-4`}>
|
||||
{data.invocation}
|
||||
</p>
|
||||
</div>
|
||||
</TitledGreyBox>
|
||||
<TitledGreyBox title={'Docker Image'} css={tw`flex-1 lg:flex-none lg:w-1/3 ml-10`}>
|
||||
{data.dockerImages.length > 1 && !isCustomImage ?
|
||||
<>
|
||||
<InputSpinner visible={loading}>
|
||||
<Select
|
||||
disabled={data.dockerImages.length < 2}
|
||||
onChange={updateSelectedDockerImage}
|
||||
defaultValue={variables.dockerImage}
|
||||
>
|
||||
{data.dockerImages.map(image => (
|
||||
<option key={image} value={image}>{image}</option>
|
||||
))}
|
||||
</Select>
|
||||
</InputSpinner>
|
||||
<p css={tw`text-xs text-neutral-300 mt-2`}>
|
||||
This is an advanced feature allowing you to select a Docker image to use when
|
||||
running this server instance.
|
||||
</p>
|
||||
</>
|
||||
:
|
||||
<>
|
||||
<Input disabled readOnly value={variables.dockerImage}/>
|
||||
{isCustomImage &&
|
||||
<p css={tw`text-xs text-neutral-300 mt-2`}>
|
||||
This {'server\'s'} Docker image has been manually set by an administrator and cannot
|
||||
be changed through this UI.
|
||||
</p>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</TitledGreyBox>
|
||||
</div>
|
||||
<h3 css={tw`mt-8 mb-2 text-2xl`}>Variables</h3>
|
||||
<div css={tw`grid gap-8 md:grid-cols-2`}>
|
||||
{data.variables.map(variable => <VariableBox key={variable.envVariable} variable={variable}/>)}
|
||||
</div>
|
||||
</ServerContentBlock>
|
||||
|
|
|
@ -32,8 +32,9 @@ const VariableBox = ({ variable }: Props) => {
|
|||
|
||||
updateStartupVariable(uuid, variable.envVariable, value)
|
||||
.then(([ response, invocation ]) => mutate(data => ({
|
||||
...data,
|
||||
invocation,
|
||||
variables: data.variables.map(v => v.envVariable === response.envVariable ? response : v),
|
||||
variables: (data.variables || []).map(v => v.envVariable === response.envVariable ? response : v),
|
||||
}), false))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
|
@ -67,7 +68,7 @@ const VariableBox = ({ variable }: Props) => {
|
|||
placeholder={variable.defaultValue}
|
||||
/>
|
||||
</InputSpinner>
|
||||
<p css={tw`mt-1 text-xs text-neutral-400`}>
|
||||
<p css={tw`mt-1 text-xs text-neutral-300`}>
|
||||
{variable.description}
|
||||
</p>
|
||||
</TitledGreyBox>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue