ui(admin): too many changes, not enough commits
This commit is contained in:
parent
bca2338863
commit
8aa9641ec2
44 changed files with 1955 additions and 334 deletions
51
resources/scripts/components/admin/servers/OwnerSelect.tsx
Normal file
51
resources/scripts/components/admin/servers/OwnerSelect.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { User } from '@/api/admin/users/getUsers';
|
||||
import searchUsers from '@/api/admin/users/searchUsers';
|
||||
import SearchableSelect, { Option } from '@/components/elements/SearchableSelect';
|
||||
|
||||
export default ({ selected }: { selected: User | null }) => {
|
||||
const context = useFormikContext();
|
||||
|
||||
const [ user, setUser ] = useState<User | null>(selected);
|
||||
const [ users, setUsers ] = useState<User[] | null>(null);
|
||||
|
||||
const onSearch = (query: string): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
searchUsers({ username: query, email: query }).then((users) => {
|
||||
setUsers(users);
|
||||
return resolve();
|
||||
}).catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
const onSelect = (user: User | null) => {
|
||||
setUser(user);
|
||||
context.setFieldValue('ownerId', user?.id || null);
|
||||
};
|
||||
|
||||
const getSelectedText = (user: User | null): string => {
|
||||
return user?.username || '';
|
||||
};
|
||||
|
||||
return (
|
||||
<SearchableSelect
|
||||
id="user"
|
||||
name="Owner"
|
||||
items={users}
|
||||
selected={user}
|
||||
setSelected={setUser}
|
||||
setItems={setUsers}
|
||||
onSearch={onSearch}
|
||||
onSelect={onSelect}
|
||||
getSelectedText={getSelectedText}
|
||||
nullable
|
||||
>
|
||||
{users?.map(d => (
|
||||
<Option key={d.id} selectId="user" id={d.id} item={d} active={d.id === user?.id}>
|
||||
{d.username}
|
||||
</Option>
|
||||
))}
|
||||
</SearchableSelect>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,68 @@
|
|||
import React from 'react';
|
||||
import AdminBox from '@/components/admin/AdminBox';
|
||||
import tw from 'twin.macro';
|
||||
import { Context } from '@/components/admin/servers/ServerRouter';
|
||||
import Button from '@/components/elements/Button';
|
||||
|
||||
const ServerManageContainer = () => {
|
||||
const server = Context.useStoreState(state => state.server);
|
||||
|
||||
if (server === undefined) {
|
||||
return (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div css={tw`grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-x-2 gap-y-2`}>
|
||||
<div css={tw`h-auto flex flex-col`}>
|
||||
<AdminBox title={'Reinstall Server'} css={tw`relative w-full`}>
|
||||
<div css={tw`flex flex-row text-red-500 justify-start items-center mb-4`}>
|
||||
<div css={tw`w-12 mr-2`}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<p css={tw`text-sm`}>
|
||||
Danger! This could overwrite server data.
|
||||
</p>
|
||||
</div>
|
||||
<Button size={'large'} color={'red'} css={tw`w-full`}>Reinstall Server</Button>
|
||||
<p css={tw`text-xs text-neutral-400 mt-2`}>
|
||||
This will reinstall the server with the assigned service scripts.
|
||||
</p>
|
||||
</AdminBox>
|
||||
</div>
|
||||
<div css={tw`h-auto flex flex-col`}>
|
||||
<AdminBox title={'Install Status'} css={tw`relative w-full`}>
|
||||
<Button size={'large'} color={'primary'} css={tw`w-full`}>Set Server as Installing</Button>
|
||||
<p css={tw`text-xs text-neutral-400 mt-2`}>
|
||||
If you need to change the install status from uninstalled to installed, or vice versa, you may do so with the button below.
|
||||
</p>
|
||||
</AdminBox>
|
||||
</div>
|
||||
<div css={tw`h-auto flex flex-col`}>
|
||||
<AdminBox title={'Suspend Server '} css={tw`relative w-full`}>
|
||||
<Button size={'large'} color={'primary'} css={tw`w-full`}>Suspend Server</Button>
|
||||
<p css={tw`text-xs text-neutral-400 mt-2`}>
|
||||
This will suspend the server, stop any running processes, and immediately block the user from being able to access their files or otherwise manage the server through the panel or API.
|
||||
</p>
|
||||
</AdminBox>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const server = Context.useStoreState(state => state.server);
|
||||
|
||||
if (server === undefined) {
|
||||
return (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ServerManageContainer/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,130 @@
|
|||
import ServerManageContainer from '@/components/admin/servers/ServerManageContainer';
|
||||
import ServerStartupContainer from '@/components/admin/servers/ServerStartupContainer';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router';
|
||||
import tw from 'twin.macro';
|
||||
import { Route, Switch, useRouteMatch } from 'react-router-dom';
|
||||
import { action, Action, Actions, createContextStore, useStoreActions } from 'easy-peasy';
|
||||
import { Server } from '@/api/admin/servers/getServers';
|
||||
import getServer from '@/api/admin/servers/getServer';
|
||||
import AdminContentBlock from '@/components/admin/AdminContentBlock';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import { SubNavigation, SubNavigationLink } from '@/components/admin/SubNavigation';
|
||||
import ServerSettingsContainer from '@/components/admin/servers/ServerSettingsContainer';
|
||||
|
||||
interface ctx {
|
||||
server: Server | undefined;
|
||||
setServer: Action<ctx, Server | undefined>;
|
||||
}
|
||||
|
||||
export const Context = createContextStore<ctx>({
|
||||
server: undefined,
|
||||
|
||||
setServer: action((state, payload) => {
|
||||
state.server = payload;
|
||||
}),
|
||||
});
|
||||
|
||||
const ServerRouter = () => {
|
||||
const location = useLocation();
|
||||
const match = useRouteMatch<{ id?: string }>();
|
||||
|
||||
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
const [ loading, setLoading ] = useState(true);
|
||||
|
||||
const server = Context.useStoreState(state => state.server);
|
||||
const setServer = Context.useStoreActions(actions => actions.setServer);
|
||||
|
||||
useEffect(() => {
|
||||
clearFlashes('server');
|
||||
|
||||
getServer(Number(match.params?.id), [ 'egg' ])
|
||||
.then(server => setServer(server))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
clearAndAddHttpError({ key: 'server', error });
|
||||
})
|
||||
.then(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
if (loading || server === undefined) {
|
||||
return (
|
||||
<AdminContentBlock>
|
||||
<FlashMessageRender byKey={'node'} css={tw`mb-4`}/>
|
||||
|
||||
<div css={tw`w-full flex flex-col items-center justify-center`} style={{ height: '24rem' }}>
|
||||
<Spinner size={'base'}/>
|
||||
</div>
|
||||
</AdminContentBlock>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminContentBlock title={'Node - ' + server.name}>
|
||||
<div css={tw`w-full flex flex-row items-center mb-4`}>
|
||||
<div css={tw`flex flex-col flex-shrink`} style={{ minWidth: '0' }}>
|
||||
<h2 css={tw`text-2xl text-neutral-50 font-header font-medium`}>{server.name}</h2>
|
||||
<p css={tw`text-base text-neutral-400 whitespace-nowrap overflow-ellipsis overflow-hidden`}>{server.uuid}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FlashMessageRender byKey={'node'} css={tw`mb-4`}/>
|
||||
|
||||
<SubNavigation>
|
||||
<SubNavigationLink to={`${match.url}`} name={'Settings'}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path clipRule="evenodd" fillRule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" />
|
||||
</svg>
|
||||
</SubNavigationLink>
|
||||
|
||||
<SubNavigationLink to={`${match.url}/startup`} name={'Startup'}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M5 4a1 1 0 00-2 0v7.268a2 2 0 000 3.464V16a1 1 0 102 0v-1.268a2 2 0 000-3.464V4zM11 4a1 1 0 10-2 0v1.268a2 2 0 000 3.464V16a1 1 0 102 0V8.732a2 2 0 000-3.464V4zM16 3a1 1 0 011 1v7.268a2 2 0 010 3.464V16a1 1 0 11-2 0v-1.268a2 2 0 010-3.464V4a1 1 0 011-1z" />
|
||||
</svg>
|
||||
</SubNavigationLink>
|
||||
|
||||
<SubNavigationLink to={`${match.url}/databases`} name={'Databases'}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path clipRule="evenodd" fillRule="evenodd" d="M3 12v3c0 1.657 3.134 3 7 3s7-1.343 7-3v-3c0 1.657-3.134 3-7 3s-7-1.343-7-3z" />
|
||||
<path clipRule="evenodd" fillRule="evenodd" d="M3 7v3c0 1.657 3.134 3 7 3s7-1.343 7-3V7c0 1.657-3.134 3-7 3S3 8.657 3 7z" />
|
||||
<path clipRule="evenodd" fillRule="evenodd" d="M17 5c0 1.657-3.134 3-7 3S3 6.657 3 5s3.134-3 7-3 7 1.343 7 3z" />
|
||||
</svg>
|
||||
</SubNavigationLink>
|
||||
|
||||
<SubNavigationLink to={`${match.url}/mounts`} name={'Mounts'}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"/>
|
||||
</svg>
|
||||
</SubNavigationLink>
|
||||
|
||||
<SubNavigationLink to={`${match.url}/manage`} name={'Manage'}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 1.944A11.954 11.954 0 012.166 5C2.056 5.649 2 6.319 2 7c0 5.225 3.34 9.67 8 11.317C14.66 16.67 18 12.225 18 7c0-.682-.057-1.35-.166-2.001A11.954 11.954 0 0110 1.944zM11 14a1 1 0 11-2 0 1 1 0 012 0zm0-7a1 1 0 10-2 0v3a1 1 0 102 0V7z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</SubNavigationLink>
|
||||
</SubNavigation>
|
||||
|
||||
<Switch location={location}>
|
||||
<Route path={`${match.path}`} exact>
|
||||
<ServerSettingsContainer/>
|
||||
</Route>
|
||||
<Route path={`${match.path}/startup`} exact>
|
||||
<ServerStartupContainer/>
|
||||
</Route>
|
||||
<Route path={`${match.path}/manage`} exact>
|
||||
<ServerManageContainer/>
|
||||
</Route>
|
||||
</Switch>
|
||||
</AdminContentBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<Context.Provider>
|
||||
<ServerRouter/>
|
||||
</Context.Provider>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,331 @@
|
|||
import React from 'react';
|
||||
import AdminBox from '@/components/admin/AdminBox';
|
||||
import tw from 'twin.macro';
|
||||
import { object } from 'yup';
|
||||
import updateServer from '@/api/admin/servers/updateServer';
|
||||
import Field from '@/components/elements/Field';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
||||
import { Context } from '@/components/admin/servers/ServerRouter';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import OwnerSelect from '@/components/admin/servers/OwnerSelect';
|
||||
import Button from '@/components/elements/Button';
|
||||
import FormikSwitch from '@/components/elements/FormikSwitch';
|
||||
|
||||
interface Values {
|
||||
id: number;
|
||||
externalId: string;
|
||||
uuid: string;
|
||||
identifier: string;
|
||||
name: string;
|
||||
description: string;
|
||||
|
||||
memory: number;
|
||||
swap: number;
|
||||
disk: number;
|
||||
io: number;
|
||||
cpu: number;
|
||||
threads: string;
|
||||
|
||||
databases: number;
|
||||
allocations: number;
|
||||
backups: number;
|
||||
|
||||
ownerId: number;
|
||||
nodeId: number;
|
||||
allocationId: number;
|
||||
nestId: number;
|
||||
eggId: number;
|
||||
}
|
||||
|
||||
const ServerFeatureContainer = () => {
|
||||
const { isSubmitting } = useFormikContext();
|
||||
|
||||
const server = Context.useStoreState(state => state.server);
|
||||
|
||||
if (server === undefined) {
|
||||
return (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminBox title={'Feature Limits'} css={tw`relative w-full`}>
|
||||
<SpinnerOverlay visible={isSubmitting}/>
|
||||
|
||||
<Form css={tw`mb-0`}>
|
||||
<div css={tw`mb-6 md:w-full md:flex md:flex-row`}>
|
||||
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:mr-4 md:mb-0`}>
|
||||
<Field
|
||||
id={'databases'}
|
||||
name={'databases'}
|
||||
label={'Database Limit'}
|
||||
type={'number'}
|
||||
description={'The total number of databases a user is allowed to create for this server.'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:ml-4 md:mb-0`}>
|
||||
<Field
|
||||
id={'allocations'}
|
||||
name={'allocations'}
|
||||
label={'Allocation Limit'}
|
||||
type={'number'}
|
||||
description={'The total number of allocations a user is allowed to create for this server.'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div css={tw`mb-6 md:w-full md:flex md:flex-row`}>
|
||||
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:mb-0`}>
|
||||
<Field
|
||||
id={'backups'}
|
||||
name={'backup'}
|
||||
label={'Backup Limit'}
|
||||
type={'number'}
|
||||
description={'The total number of backups that can be created for this server.'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Form>
|
||||
</AdminBox>
|
||||
);
|
||||
};
|
||||
|
||||
const ServerResourceContainer = () => {
|
||||
const { isSubmitting } = useFormikContext();
|
||||
|
||||
const server = Context.useStoreState(state => state.server);
|
||||
|
||||
if (server === undefined) {
|
||||
return (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminBox title={'Resource Management'} css={tw`relative w-full`}>
|
||||
<SpinnerOverlay visible={isSubmitting}/>
|
||||
|
||||
<Form css={tw`mb-0`}>
|
||||
<div css={tw`mb-6 md:w-full md:flex md:flex-row`}>
|
||||
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:mr-4 md:mb-0`}>
|
||||
<Field
|
||||
id={'cpu'}
|
||||
name={'cpu'}
|
||||
label={'CPU Limit'}
|
||||
type={'string'}
|
||||
description={'Each physical core on the system is considered to be 100%. Setting this value to 0 will allow a server to use CPU time without restrictions.'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:ml-4 md:mb-0`}>
|
||||
<Field
|
||||
id={'threads'}
|
||||
name={'threads'}
|
||||
label={'CPU Pinnings'}
|
||||
type={'string'}
|
||||
description={'Advanced: Enter the specific CPU cores that this process can run on, or leave blank to allow all cores. This can be a single number, or a comma seperated list. Example: 0, 0-1,3, or 0,1,3,4.'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div css={tw`mb-6 md:w-full md:flex md:flex-row`}>
|
||||
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:mr-4 md:mb-0`}>
|
||||
<Field
|
||||
id={'memory'}
|
||||
name={'memory'}
|
||||
label={'Memory Limit'}
|
||||
type={'number'}
|
||||
description={'The maximum amount of memory allowed for this container. Setting this to 0 will allow unlimited memory in a container.'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:ml-4 md:mb-0`}>
|
||||
<Field
|
||||
id={'swap'}
|
||||
name={'swap'}
|
||||
label={'Swap Limit'}
|
||||
type={'number'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div css={tw`mb-6 md:w-full md:flex md:flex-row`}>
|
||||
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:mr-4 md:mb-0`}>
|
||||
<Field
|
||||
id={'disk'}
|
||||
name={'disk'}
|
||||
label={'Disk Limit'}
|
||||
type={'number'}
|
||||
description={'This server will not be allowed to boot if it is using more than this amount of space. If a server goes over this limit while running it will be safely stopped and locked until enough space is available. Set to 0 to allow unlimited disk usage.'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:ml-4 md:mb-0`}>
|
||||
<Field
|
||||
id={'io'}
|
||||
name={'io'}
|
||||
label={'Block IO Proportion'}
|
||||
type={'number'}
|
||||
description={'Advanced: The IO performance of this server relative to other running containers on the system. Value should be between 10 and 1000.'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div css={tw`mb-6 md:w-full md:flex md:flex-row`}>
|
||||
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:mr-4 md:mb-0`}>
|
||||
<FormikSwitch
|
||||
name={'oom'}
|
||||
label={'Out of Memory Killer'}
|
||||
description={'Enabling OOM killer may cause server processes to exit unexpectedly. '}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Form>
|
||||
</AdminBox>
|
||||
);
|
||||
};
|
||||
|
||||
const ServerSettingsContainer = () => {
|
||||
const { isSubmitting } = useFormikContext();
|
||||
|
||||
const server = Context.useStoreState(state => state.server);
|
||||
|
||||
if (server === undefined) {
|
||||
return (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminBox title={'Settings'} css={tw`relative w-full`}>
|
||||
<SpinnerOverlay visible={isSubmitting}/>
|
||||
|
||||
<Form css={tw`mb-0`}>
|
||||
<div css={tw`mb-6 md:w-full md:flex md:flex-row`}>
|
||||
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:mr-4 md:mb-0`}>
|
||||
<Field
|
||||
id={'name'}
|
||||
name={'name'}
|
||||
label={'Server Name'}
|
||||
type={'string'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:ml-4 md:mb-0`}>
|
||||
<Field
|
||||
id={'externalId'}
|
||||
name={'externalId'}
|
||||
label={'External Identifier'}
|
||||
type={'number'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div css={tw`mb-6 md:w-full md:flex md:flex-row`}>
|
||||
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:mr-4 md:mb-0`}>
|
||||
<OwnerSelect selected={null}/>
|
||||
</div>
|
||||
|
||||
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:ml-4 md:mb-0`}>
|
||||
<Field
|
||||
id={'description'}
|
||||
name={'description'}
|
||||
label={'Server Description'}
|
||||
type={'string'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</AdminBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
|
||||
const server = Context.useStoreState(state => state.server);
|
||||
const setServer = Context.useStoreActions(actions => actions.setServer);
|
||||
|
||||
if (server === undefined) {
|
||||
return (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
|
||||
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||
clearFlashes('server');
|
||||
|
||||
updateServer(server.id, values)
|
||||
.then(() => setServer({ ...server, ...values }))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
clearAndAddHttpError({ key: 'server', error });
|
||||
})
|
||||
.then(() => setSubmitting(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
onSubmit={submit}
|
||||
initialValues={{
|
||||
id: server.id,
|
||||
externalId: server.externalId || '',
|
||||
uuid: server.uuid,
|
||||
identifier: server.identifier,
|
||||
name: server.name,
|
||||
description: server.description,
|
||||
|
||||
memory: server.limits.memory,
|
||||
swap: server.limits.swap,
|
||||
disk: server.limits.disk,
|
||||
io: server.limits.io,
|
||||
cpu: server.limits.cpu,
|
||||
threads: server.limits.threads,
|
||||
|
||||
databases: server.featureLimits.databases,
|
||||
allocations: server.featureLimits.allocations,
|
||||
backups: server.featureLimits.backups,
|
||||
|
||||
ownerId: server.ownerId,
|
||||
nodeId: server.nodeId,
|
||||
allocationId: server.allocationId,
|
||||
nestId: server.nestId,
|
||||
eggId: server.eggId,
|
||||
}}
|
||||
validationSchema={object().shape({
|
||||
})}
|
||||
>
|
||||
{
|
||||
({ isSubmitting, isValid }) => (
|
||||
<div css={tw`flex flex-col lg:flex-row`}>
|
||||
<div css={tw`flex flex-col w-full mt-4 ml-0 lg:w-1/2 lg:ml-2 lg:mt-0`}>
|
||||
<div css={tw`flex flex-col w-full mr-0 lg:mr-2`}>
|
||||
<ServerSettingsContainer/>
|
||||
</div>
|
||||
<div css={tw`flex flex-col w-full mt-4 mr-0 lg:mr-2`}>
|
||||
<ServerFeatureContainer/>
|
||||
</div>
|
||||
</div>
|
||||
<div css={tw`flex flex-col w-full mt-4 ml-0 lg:w-1/2 lg:ml-2 lg:mt-0`}>
|
||||
<div css={tw`flex flex-col w-full mr-0 lg:mr-2`}>
|
||||
<ServerResourceContainer/>
|
||||
</div>
|
||||
<div css={tw`py-2 pr-6 mt-4 rounded shadow-md bg-neutral-700`}>
|
||||
<div css={tw`flex flex-row`}>
|
||||
<Button type="submit" size="small" css={tw`ml-auto`} disabled={isSubmitting || !isValid}>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Formik>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,203 @@
|
|||
import React from 'react';
|
||||
import Button from '@/components/elements/Button';
|
||||
import FormikSwitch from '@/components/elements/FormikSwitch';
|
||||
import Input from '@/components/elements/Input';
|
||||
import AdminBox from '@/components/admin/AdminBox';
|
||||
import tw from 'twin.macro';
|
||||
import { object } from 'yup';
|
||||
import updateServer from '@/api/admin/servers/updateServer';
|
||||
import Field from '@/components/elements/Field';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
||||
import { Context } from '@/components/admin/servers/ServerRouter';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import Label from '@/components/elements/Label';
|
||||
// import { ServerEggVariable } from '@/api/server/types';
|
||||
|
||||
/* interface Props {
|
||||
variable: ServerEggVariable;
|
||||
} */
|
||||
|
||||
interface Values {
|
||||
startupCommand: string;
|
||||
nestId: number;
|
||||
eggId: number;
|
||||
}
|
||||
|
||||
/* const VariableBox = ({ variable }: Props) => {
|
||||
const { isSubmitting } = useFormikContext();
|
||||
|
||||
const server = Context.useStoreState(state => state.server);
|
||||
|
||||
if (server === undefined) {
|
||||
return (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminBox title={'Service Configuration'} css={tw`relative w-full`}>
|
||||
<SpinnerOverlay visible={isSubmitting}/>
|
||||
|
||||
<Form css={tw`mb-0`}>
|
||||
<div css={tw`md:w-full md:flex md:flex-col`}>
|
||||
<Field
|
||||
name={variable.envVariable}
|
||||
defaultValue={variable.serverValue}
|
||||
placeholder={variable.defaultValue}
|
||||
description={variable.description}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
</AdminBox>
|
||||
);
|
||||
}; */
|
||||
|
||||
const ServerServiceContainer = () => {
|
||||
const { isSubmitting } = useFormikContext();
|
||||
|
||||
const server = Context.useStoreState(state => state.server);
|
||||
|
||||
if (server === undefined) {
|
||||
return (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminBox title={'Service Configuration'} css={tw`relative w-full`}>
|
||||
<SpinnerOverlay visible={isSubmitting}/>
|
||||
|
||||
<Form css={tw`mb-0`}>
|
||||
<div css={tw`md:w-full md:flex md:flex-col`}>
|
||||
<div css={tw`flex-1`}>
|
||||
<div css={tw`p-3 mb-6 border-l-4 border-red-500`}>
|
||||
<p css={tw`text-xs text-neutral-200`}>
|
||||
This is a destructive operation in many cases. This server will be stopped immediately in order for this action to proceed.
|
||||
</p>
|
||||
</div>
|
||||
<div css={tw`p-3 mb-6 border-l-4 border-red-500`}>
|
||||
<p css={tw`text-xs text-neutral-200`}>
|
||||
Changing any of the below values will result in the server processing a re-install command. The server will be stopped and will then proceed. If you would like the service scripts to not run, ensure the box is checked at the bottom.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div css={tw`pb-4 mb-6 md:w-full md:flex md:flex-col md:mb-0`}>
|
||||
Nest/Egg Selector HERE
|
||||
</div>
|
||||
<div css={tw`pb-4 mb-6 md:w-full md:flex md:flex-col md:mb-0`}>
|
||||
<FormikSwitch
|
||||
name={'oom'}
|
||||
label={'Skip Egg Install Script'}
|
||||
description={'If the selected Egg has an install script attached to it, the script will run during install. If you would like to skip this step, check this box.'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</AdminBox>
|
||||
);
|
||||
};
|
||||
|
||||
const ServerStartupContainer = () => {
|
||||
const { isSubmitting } = useFormikContext();
|
||||
|
||||
const server = Context.useStoreState(state => state.server);
|
||||
|
||||
if (server === undefined) {
|
||||
return (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminBox title={'Startup Command'} css={tw`relative w-full`}>
|
||||
<SpinnerOverlay visible={isSubmitting}/>
|
||||
|
||||
<Form css={tw`mb-0`}>
|
||||
<div css={tw`mb-6 md:w-full md:flex md:flex-col`}>
|
||||
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:mr-4`}>
|
||||
<Field
|
||||
id={'startupCommand'}
|
||||
name={'startupCommand'}
|
||||
label={'Startup Command'}
|
||||
type={'string'}
|
||||
description={'Edit your server\'s startup command here. The following variables are available by default: {{SERVER_MEMORY}}, {{SERVER_IP}}, and {{SERVER_PORT}}.'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:mb-0`}>
|
||||
<div>
|
||||
<Label>Default Startup Command</Label>
|
||||
<Input
|
||||
disabled
|
||||
value={server.relations.egg?.configStartup || ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</AdminBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
|
||||
const server = Context.useStoreState(state => state.server);
|
||||
const setServer = Context.useStoreActions(actions => actions.setServer);
|
||||
|
||||
if (server === undefined) {
|
||||
return (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
|
||||
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||
clearFlashes('server');
|
||||
|
||||
updateServer(server.id, values)
|
||||
.then(() => setServer({ ...server, ...values }))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
clearAndAddHttpError({ key: 'server', error });
|
||||
})
|
||||
.then(() => setSubmitting(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
onSubmit={submit}
|
||||
initialValues={{
|
||||
startupCommand: server.container.startupCommand,
|
||||
nestId: server.nestId,
|
||||
eggId: server.eggId,
|
||||
}}
|
||||
validationSchema={object().shape({
|
||||
})}
|
||||
>
|
||||
{
|
||||
({ isSubmitting, isValid }) => (
|
||||
<div css={tw`flex flex-col`}>
|
||||
<div css={tw`flex flex-col w-full mb-4 mr-0 lg:mr-2`}>
|
||||
<ServerStartupContainer/>
|
||||
</div>
|
||||
<div css={tw`flex flex-col w-1/2 mr-0 lg:mr-2`}>
|
||||
<ServerServiceContainer/>
|
||||
</div>
|
||||
<div css={tw`flex flex-col w-1/2 mr-0 lg:mr-2`}>
|
||||
Server Startup variables go here
|
||||
</div>
|
||||
<div css={tw`py-2 pr-6 mt-4 rounded shadow-md bg-neutral-700`}>
|
||||
<div css={tw`flex flex-row`}>
|
||||
<Button type="submit" size="small" css={tw`ml-auto`} disabled={isSubmitting || !isValid}>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Formik>
|
||||
);
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import getServers, { Context as ServersContext } from '@/api/admin/servers/getServers';
|
||||
import getServers, { Context as ServersContext, Filters } from '@/api/admin/servers/getServers';
|
||||
import AdminCheckbox from '@/components/admin/AdminCheckbox';
|
||||
import AdminTable, { ContentWrapper, Loading, NoItems, Pagination, TableBody, TableHead, TableHeader } from '@/components/admin/AdminTable';
|
||||
import AdminTable, { ContentWrapper, Loading, Pagination, TableBody, TableHead, TableHeader } from '@/components/admin/AdminTable';
|
||||
import Button from '@/components/elements/Button';
|
||||
import CopyOnClick from '@/components/elements/CopyOnClick';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
|
@ -31,21 +31,12 @@ const RowCheckbox = ({ id }: { id: number }) => {
|
|||
);
|
||||
};
|
||||
|
||||
const UsersContainer = () => {
|
||||
const ServersContainer = () => {
|
||||
const match = useRouteMatch();
|
||||
|
||||
const { page, setPage } = useContext(ServersContext);
|
||||
const { page, setPage, setFilters, sort, setSort, sortDirection } = useContext(ServersContext);
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
const { data: servers, error, isValidating } = getServers([ 'node', 'user' ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!error) {
|
||||
clearFlashes('servers');
|
||||
return;
|
||||
}
|
||||
|
||||
clearAndAddHttpError({ key: 'servers', error });
|
||||
}, [ error ]);
|
||||
const { data: servers, error } = getServers([ 'node', 'user' ]);
|
||||
|
||||
const length = servers?.items?.length || 0;
|
||||
|
||||
|
@ -56,10 +47,30 @@ const UsersContainer = () => {
|
|||
setSelectedServers(e.currentTarget.checked ? (servers?.items?.map(server => server.id) || []) : []);
|
||||
};
|
||||
|
||||
const onSearch = (query: string): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
if (query.length < 2) {
|
||||
setFilters(null);
|
||||
} else {
|
||||
setFilters({ name: query });
|
||||
}
|
||||
return resolve();
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedServers([]);
|
||||
}, [ page ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!error) {
|
||||
clearFlashes('servers');
|
||||
return;
|
||||
}
|
||||
|
||||
clearAndAddHttpError({ key: 'servers', error });
|
||||
}, [ error ]);
|
||||
|
||||
return (
|
||||
<AdminContentBlock title={'Servers'}>
|
||||
<div css={tw`w-full flex flex-row items-center mb-8`}>
|
||||
|
@ -80,102 +91,103 @@ const UsersContainer = () => {
|
|||
<FlashMessageRender byKey={'servers'} css={tw`mb-4`}/>
|
||||
|
||||
<AdminTable>
|
||||
{ servers === undefined || (error && isValidating) ?
|
||||
<Loading/>
|
||||
:
|
||||
length < 1 ?
|
||||
<NoItems/>
|
||||
<ContentWrapper
|
||||
checked={selectedServerLength === (length === 0 ? -1 : length)}
|
||||
onSelectAllClick={onSelectAllClick}
|
||||
onSearch={onSearch}
|
||||
>
|
||||
{servers === undefined ?
|
||||
<Loading/>
|
||||
:
|
||||
<ContentWrapper
|
||||
checked={selectedServerLength === (length === 0 ? -1 : length)}
|
||||
onSelectAllClick={onSelectAllClick}
|
||||
>
|
||||
<Pagination data={servers} onPageSelect={setPage}>
|
||||
<div css={tw`overflow-x-auto`}>
|
||||
<table css={tw`w-full table-auto`}>
|
||||
<TableHead>
|
||||
<TableHeader name={'Identifier'}/>
|
||||
<TableHeader name={'Name'}/>
|
||||
<TableHeader name={'Owner'}/>
|
||||
<TableHeader name={'Node'}/>
|
||||
<TableHeader name={'Status'}/>
|
||||
</TableHead>
|
||||
// length < 1 ?
|
||||
// <NoItems/>
|
||||
// :
|
||||
<Pagination data={servers} onPageSelect={setPage}>
|
||||
<div css={tw`overflow-x-auto`}>
|
||||
<table css={tw`w-full table-auto`}>
|
||||
<TableHead>
|
||||
<TableHeader name={'Identifier'} direction={sort === 'uuidShort' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('uuidShort')}/>
|
||||
<TableHeader name={'Name'} direction={sort === 'name' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('name')}/>
|
||||
<TableHeader name={'Owner'} direction={sort === 'owner_id' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('owner_id')}/>
|
||||
<TableHeader name={'Node'} direction={sort === 'node_id' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('node_id')}/>
|
||||
<TableHeader name={'Status'} direction={sort === 'status' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('status')}/>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{
|
||||
servers.items.map(server => (
|
||||
<tr key={server.id} css={tw`h-14 hover:bg-neutral-600`}>
|
||||
<td css={tw`pl-6`}>
|
||||
<RowCheckbox id={server.id}/>
|
||||
</td>
|
||||
<TableBody>
|
||||
{
|
||||
servers?.items.map(server => (
|
||||
<tr key={server.id} css={tw`h-14 hover:bg-neutral-600`}>
|
||||
<td css={tw`pl-6`}>
|
||||
<RowCheckbox id={server.id}/>
|
||||
</td>
|
||||
|
||||
<td css={tw`px-6 text-sm text-neutral-200 text-left whitespace-nowrap`}>
|
||||
<CopyOnClick text={server.identifier}>
|
||||
<code css={tw`font-mono bg-neutral-900 rounded py-1 px-2`}>{server.identifier}</code>
|
||||
</CopyOnClick>
|
||||
</td>
|
||||
<td css={tw`px-6 text-sm text-neutral-200 text-left whitespace-nowrap`}>
|
||||
<CopyOnClick text={server.identifier}>
|
||||
<code css={tw`font-mono bg-neutral-900 rounded py-1 px-2`}>{server.identifier}</code>
|
||||
</CopyOnClick>
|
||||
</td>
|
||||
|
||||
<td css={tw`px-6 text-sm text-left whitespace-nowrap`}>
|
||||
<NavLink to={`${match.url}/${server.id}`} css={tw`text-primary-400 hover:text-primary-300`}>
|
||||
{server.name}
|
||||
</NavLink>
|
||||
</td>
|
||||
<td css={tw`px-6 text-sm text-left whitespace-nowrap`}>
|
||||
<NavLink to={`${match.url}/${server.id}`} css={tw`text-primary-400 hover:text-primary-300`}>
|
||||
{server.name}
|
||||
</NavLink>
|
||||
</td>
|
||||
|
||||
{/* TODO: Have permission check for displaying user information. */}
|
||||
<td css={tw`px-6 text-sm text-left whitespace-nowrap`}>
|
||||
<NavLink to={`/admin/users/${server.relations.user?.id}`} css={tw`text-primary-400 hover:text-primary-300`}>
|
||||
<div css={tw`text-sm text-neutral-200`}>
|
||||
{server.relations.user?.firstName} {server.relations.user?.lastName}
|
||||
</div>
|
||||
{/* TODO: Have permission check for displaying user information. */}
|
||||
<td css={tw`px-6 text-sm text-left whitespace-nowrap`}>
|
||||
<NavLink to={`/admin/users/${server.relations.user?.id}`} css={tw`text-primary-400 hover:text-primary-300`}>
|
||||
<div css={tw`text-sm text-neutral-200`}>
|
||||
{server.relations.user?.firstName} {server.relations.user?.lastName}
|
||||
</div>
|
||||
|
||||
<div css={tw`text-sm text-neutral-400`}>
|
||||
{server.relations.user?.email}
|
||||
</div>
|
||||
</NavLink>
|
||||
</td>
|
||||
<div css={tw`text-sm text-neutral-400`}>
|
||||
{server.relations.user?.email}
|
||||
</div>
|
||||
</NavLink>
|
||||
</td>
|
||||
|
||||
{/* TODO: Have permission check for displaying node information. */}
|
||||
<td css={tw`px-6 text-sm text-left whitespace-nowrap`}>
|
||||
<NavLink to={`/admin/nodes/${server.relations.node?.id}`} css={tw`text-primary-400 hover:text-primary-300`}>
|
||||
<div css={tw`text-sm text-neutral-200`}>
|
||||
{server.relations.node?.name}
|
||||
</div>
|
||||
{/* TODO: Have permission check for displaying node information. */}
|
||||
<td css={tw`px-6 text-sm text-left whitespace-nowrap`}>
|
||||
<NavLink to={`/admin/nodes/${server.relations.node?.id}`} css={tw`text-primary-400 hover:text-primary-300`}>
|
||||
<div css={tw`text-sm text-neutral-200`}>
|
||||
{server.relations.node?.name}
|
||||
</div>
|
||||
|
||||
<div css={tw`text-sm text-neutral-400`}>
|
||||
{server.relations.node?.fqdn}
|
||||
</div>
|
||||
</NavLink>
|
||||
</td>
|
||||
<div css={tw`text-sm text-neutral-400`}>
|
||||
{server.relations.node?.fqdn}
|
||||
</div>
|
||||
</NavLink>
|
||||
</td>
|
||||
|
||||
<td css={tw`px-6 whitespace-nowrap`}>
|
||||
{ server.isInstalling ?
|
||||
<td css={tw`px-6 whitespace-nowrap`}>
|
||||
{server.status === 'installing' ?
|
||||
<span css={tw`px-2 inline-flex text-xs leading-5 font-medium rounded-full bg-yellow-200 text-yellow-800`}>
|
||||
Installing
|
||||
</span>
|
||||
:
|
||||
server.status === 'transferring' ?
|
||||
<span css={tw`px-2 inline-flex text-xs leading-5 font-medium rounded-full bg-yellow-200 text-yellow-800`}>
|
||||
Installing
|
||||
Transferring
|
||||
</span>
|
||||
:
|
||||
server.isTransferring ?
|
||||
<span css={tw`px-2 inline-flex text-xs leading-5 font-medium rounded-full bg-yellow-200 text-yellow-800`}>
|
||||
Transferring
|
||||
: server.status === 'suspended' ?
|
||||
<span css={tw`px-2 inline-flex text-xs leading-5 font-medium rounded-full bg-red-200 text-red-800`}>
|
||||
Suspended
|
||||
</span>
|
||||
: server.isSuspended ?
|
||||
<span css={tw`px-2 inline-flex text-xs leading-5 font-medium rounded-full bg-red-200 text-red-800`}>
|
||||
Suspended
|
||||
</span>
|
||||
:
|
||||
<span css={tw`px-2 inline-flex text-xs leading-5 font-medium rounded-full bg-green-100 text-green-800`}>
|
||||
Active
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</TableBody>
|
||||
</table>
|
||||
</div>
|
||||
</Pagination>
|
||||
</ContentWrapper>
|
||||
}
|
||||
:
|
||||
<span css={tw`px-2 inline-flex text-xs leading-5 font-medium rounded-full bg-green-100 text-green-800`}>
|
||||
Active
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</TableBody>
|
||||
</table>
|
||||
</div>
|
||||
</Pagination>
|
||||
}
|
||||
</ContentWrapper>
|
||||
</AdminTable>
|
||||
</AdminContentBlock>
|
||||
);
|
||||
|
@ -183,10 +195,22 @@ const UsersContainer = () => {
|
|||
|
||||
export default () => {
|
||||
const [ page, setPage ] = useState<number>(1);
|
||||
const [ filters, setFilters ] = useState<Filters | null>(null);
|
||||
const [ sort, setSortState ] = useState<string | null>(null);
|
||||
const [ sortDirection, setSortDirection ] = useState<boolean>(false);
|
||||
|
||||
const setSort = (newSort: string | null) => {
|
||||
if (sort === newSort) {
|
||||
setSortDirection(!sortDirection);
|
||||
} else {
|
||||
setSortState(newSort);
|
||||
setSortDirection(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ServersContext.Provider value={{ page, setPage }}>
|
||||
<UsersContainer/>
|
||||
<ServersContext.Provider value={{ page, setPage, filters, setFilters, sort, setSort, sortDirection, setSortDirection }}>
|
||||
<ServersContainer/>
|
||||
</ServersContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue