ui(admin): too many changes, not enough commits

This commit is contained in:
Matthew Penner 2021-05-20 16:00:46 -06:00
parent bca2338863
commit 8aa9641ec2
44 changed files with 1955 additions and 334 deletions

View File

@ -56,8 +56,8 @@ class NodeController extends ApplicationApiController
}
$nodes = QueryBuilder::for(Node::query())
->allowedFilters(['uuid', 'name', 'fqdn', 'daemon_token_id'])
->allowedSorts(['id', 'uuid', 'memory', 'disk'])
->allowedFilters(['id', 'uuid', 'name', 'fqdn', 'daemon_token_id'])
->allowedSorts(['id', 'uuid', 'name', 'location_id', 'fqdn', 'memory', 'disk'])
->paginate($perPage);
return $this->fractal->collection($nodes)

View File

@ -48,7 +48,7 @@ class ServerController extends ApplicationApiController
$servers = QueryBuilder::for(Server::query())
->allowedFilters(['uuid', 'name', 'image', 'external_id'])
->allowedSorts(['id', 'uuid', 'owner_id', 'node_id', 'status'])
->allowedSorts(['id', 'uuid', 'uuidShort', 'name', 'owner_id', 'node_id', 'status'])
->paginate($perPage);
return $this->fractal->collection($servers)

View File

@ -60,8 +60,6 @@ class ServerTransformer extends BaseTransformer
'name' => $model->name,
'description' => $model->description,
'status' => $model->status,
// This field is deprecated, please use "status".
'suspended' => $model->isSuspended(),
'limits' => [
'memory' => $model->memory,
'swap' => $model->swap,
@ -75,16 +73,14 @@ class ServerTransformer extends BaseTransformer
'allocations' => $model->allocation_limit,
'backups' => $model->backup_limit,
],
'user' => $model->owner_id,
'node' => $model->node_id,
'allocation' => $model->allocation_id,
'nest' => $model->nest_id,
'egg' => $model->egg_id,
'owner_id' => $model->owner_id,
'node_id' => $model->node_id,
'allocation_id' => $model->allocation_id,
'nest_id' => $model->nest_id,
'egg_id' => $model->egg_id,
'container' => [
'startup_command' => $model->startup,
'image' => $model->image,
// This field is deprecated, please use "status".
'installed' => $model->isInstalled() ? 1 : 0,
'environment' => $this->environmentService->handle($model),
],
$model->getUpdatedAtColumn() => $this->formatTimestamp($model->updated_at),

View File

@ -1,6 +1,25 @@
{
"name": "pterodactyl-panel",
"dependencies": {
"@codemirror/autocomplete": "^0.18.5",
"@codemirror/closebrackets": "^0.18.0",
"@codemirror/commands": "^0.18.2",
"@codemirror/comment": "^0.18.1",
"@codemirror/fold": "^0.18.1",
"@codemirror/gutter": "^0.18.3",
"@codemirror/highlight": "^0.18.4",
"@codemirror/history": "^0.18.1",
"@codemirror/lang-json": "^0.18.0",
"@codemirror/language": "^0.18.1",
"@codemirror/legacy-modes": "^0.18.0",
"@codemirror/lint": "^0.18.3",
"@codemirror/matchbrackets": "^0.18.0",
"@codemirror/rectangular-selection": "^0.18.0",
"@codemirror/search": "^0.18.3",
"@codemirror/state": "^0.18.7",
"@codemirror/stream-parser": "^0.18.2",
"@codemirror/theme-one-dark": "^0.18.1",
"@codemirror/view": "^0.18.12",
"@fortawesome/fontawesome-svg-core": "^1.2.34",
"@fortawesome/free-solid-svg-icons": "^5.15.2",
"@fortawesome/react-fontawesome": "^0.1.14",
@ -34,16 +53,16 @@
"sockette": "^2.0.6",
"styled-components": "^5.2.1",
"styled-components-breakpoint": "^3.0.0-preview.20",
"swr": "^0.2.3",
"swr": "^0.5.6",
"tailwindcss": "^2.0.2",
"uuid": "^3.3.2",
"xterm": "^4.10.0",
"xterm": "^4.12.0",
"xterm-addon-attach": "^0.6.0",
"xterm-addon-fit": "^0.5.0",
"xterm-addon-search": "^0.8.0",
"xterm-addon-search-bar": "^0.2.0",
"xterm-addon-web-links": "^0.4.0",
"yup": "^0.29.1"
"yup": "^0.32.9"
},
"devDependencies": {
"@babel/core": "^7.12.1",

View File

@ -0,0 +1,23 @@
import http from '@/api/http';
import { rawDataToServerAllocation } from '@/api/transformers';
export interface Allocation {
id: number;
ip: string;
alias: string | null;
port: number;
notes: string | null;
isDefault: boolean;
}
export default (uuid: string): Promise<[ Allocation, string[] ]> => {
return new Promise((resolve, reject) => {
http.get(`/api/application/allocations/${uuid}`)
.then(({ data }) => resolve([
rawDataToServerAllocation(data),
// eslint-disable-next-line camelcase
data.meta?.is_allocation_owner ? [ '*' ] : (data.meta?.user_permissions || []),
]))
.catch(reject);
});
};

View File

@ -67,18 +67,61 @@ export const rawDataToNode = ({ attributes }: FractalResponseData): Node => ({
},
});
export interface Filters {
uuid?: string;
name?: string;
image?: string;
/* eslint-disable camelcase */
external_id?: string;
/* eslint-enable camelcase */
}
interface ctx {
page: number;
setPage: (value: number | ((s: number) => number)) => void;
filters: Filters | null;
setFilters: (filters: Filters | null) => void;
sort: string | null;
setSort: (sort: string | null) => void;
sortDirection: boolean;
setSortDirection: (direction: boolean) => void;
}
export const Context = createContext<ctx>({ page: 1, setPage: () => 1 });
export const Context = createContext<ctx>({
page: 1,
setPage: () => 1,
filters: null,
setFilters: () => null,
sort: null,
setSort: () => null,
sortDirection: false,
setSortDirection: () => false,
});
export default (include: string[] = []) => {
const { page } = useContext(Context);
const { page, filters, sort, sortDirection } = useContext(Context);
return useSWR<PaginatedResult<Node>>([ 'nodes', page ], async () => {
const { data } = await http.get('/api/application/nodes', { params: { include: include.join(','), page } });
const params = {};
if (filters !== null) {
Object.keys(filters).forEach(key => {
// @ts-ignore
params['filter[' + key + ']'] = filters[key];
});
}
if (sort !== null) {
// @ts-ignore
params.sort = (sortDirection ? '-' : '') + sort;
}
return useSWR<PaginatedResult<Node>>([ 'nodes', page, filters, sort, sortDirection ], async () => {
const { data } = await http.get('/api/application/nodes', { params: { include: include.join(','), page, ...params } });
return ({
items: (data.data || []).map(rawDataToNode),

View File

@ -1,3 +1,4 @@
import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg';
import http, { FractalResponseData, getPaginationSet, PaginatedResult } from '@/api/http';
import { createContext, useContext } from 'react';
import useSWR from 'swr';
@ -6,18 +7,46 @@ import { User, rawDataToUser } from '@/api/admin/users/getUsers';
export interface Server {
id: number;
externalId: string;
externalId: string | null
uuid: string;
identifier: string;
name: string;
description: string;
isSuspended: boolean;
isInstalling: boolean;
isTransferring: boolean;
status: string;
limits: {
memory: number;
swap: number;
disk: number;
io: number;
cpu: number;
threads: string;
}
featureLimits: {
databases: number;
allocations: number;
backups: number;
}
ownerId: number;
nodeId: number;
allocationId: number;
nestId: number;
eggId: number;
container: {
startupCommand: string;
defaultStartup: string;
image: string;
environment: Map<string, string>;
}
createdAt: Date;
updatedAt: Date;
relations: {
egg: Egg | undefined;
node: Node | undefined;
user: User | undefined;
};
@ -30,13 +59,41 @@ export const rawDataToServer = ({ attributes }: FractalResponseData): Server =>
identifier: attributes.identifier,
name: attributes.name,
description: attributes.description,
isSuspended: attributes.is_suspended,
isInstalling: attributes.is_installing,
isTransferring: attributes.is_transferring,
status: attributes.status,
limits: {
memory: attributes.limits.memory,
swap: attributes.limits.swap,
disk: attributes.limits.disk,
io: attributes.limits.io,
cpu: attributes.limits.cpu,
threads: attributes.limits.threads,
},
featureLimits: {
databases: attributes.feature_limits.databases,
allocations: attributes.feature_limits.allocations,
backups: attributes.feature_limits.backups,
},
ownerId: attributes.owner_id,
nodeId: attributes.node_id,
allocationId: attributes.allocation_id,
nestId: attributes.nest_id,
eggId: attributes.egg_id,
container: {
startupCommand: attributes.container.startup_command,
defaultStartup: '',
image: attributes.container.image,
environment: attributes.container.environment,
},
createdAt: new Date(attributes.created_at),
updatedAt: new Date(attributes.updated_at),
relations: {
egg: attributes.relationships?.egg !== undefined ? rawDataToEgg(attributes.relationships.egg as FractalResponseData) : undefined,
node: attributes.relationships?.node !== undefined ? rawDataToNode(attributes.relationships.node as FractalResponseData) : undefined,
user: attributes.relationships?.user !== undefined ? rawDataToUser(attributes.relationships.user as FractalResponseData) : undefined,
},

View File

@ -0,0 +1,12 @@
import http from '@/api/http';
import { Server, rawDataToServer } from '@/api/admin/servers/getServers';
export default (id: number, server: Partial<Server>, include: string[] = []): Promise<Server> => {
return new Promise((resolve, reject) => {
http.patch(`/api/application/servers/${id}`, {
...server,
}, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToServer(data)))
.catch(reject);
});
};

View File

@ -0,0 +1,25 @@
import http from '@/api/http';
import { User, rawDataToUser } from '@/api/admin/users/getUsers';
interface Filters {
username?: string;
email?: string;
}
export default (filters?: Filters): Promise<User[]> => {
const params = {};
if (filters !== undefined) {
Object.keys(filters).forEach(key => {
// @ts-ignore
params['filter[' + key + ']'] = filters[key];
});
}
return new Promise((resolve, reject) => {
http.get('/api/application/users', { params: { ...params } })
.then(response => resolve(
(response.data.data || []).map(rawDataToUser)
))
.catch(reject);
});
};

View File

@ -8,10 +8,16 @@ interface Props {
icon?: IconProp;
title: string | React.ReactNode;
className?: string;
padding?: boolean;
children: React.ReactNode;
}
const AdminBox = ({ icon, title, children, className }: Props) => (
const AdminBox = ({ icon, title, className, padding, children }: Props) => {
if (padding === undefined) {
padding = true;
}
return (
<div css={tw`rounded shadow-md bg-neutral-700`} className={className}>
<div css={tw`bg-neutral-900 rounded-t px-6 py-3 border-b border-black`}>
{typeof title === 'string' ?
@ -22,10 +28,11 @@ const AdminBox = ({ icon, title, children, className }: Props) => (
title
}
</div>
<div css={tw`px-6 py-4`}>
<div css={padding ? tw`px-6 py-4` : undefined}>
{children}
</div>
</div>
);
);
};
export default memo(AdminBox, isEqual);

View File

@ -1,6 +1,4 @@
import LocationDeleteButton from '@/components/admin/locations/LocationDeleteButton';
import React, { useEffect, useState } from 'react';
import { useHistory } from 'react-router';
import tw from 'twin.macro';
import { useRouteMatch } from 'react-router-dom';
import { action, Action, Actions, createContextStore, useStoreActions } from 'easy-peasy';
@ -37,7 +35,7 @@ interface Values {
}
const EditInformationContainer = () => {
const history = useHistory();
// const history = useHistory();
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
@ -101,10 +99,10 @@ const EditInformationContainer = () => {
<div css={tw`w-full flex flex-row items-center mt-6`}>
<div css={tw`flex`}>
<LocationDeleteButton
locationId={location.id}
onDeleted={() => history.push('/admin/locations')}
/>
{/* <LocationDeleteButton */}
{/* locationId={location.id} */}
{/* onDeleted={() => history.push('/admin/locations')} */}
{/* /> */}
</div>
<div css={tw`flex ml-auto`}>

View File

@ -34,7 +34,7 @@ export default () => {
createLocation(short, long)
.then(location => {
mutate(data => ({ ...data, items: data.items.concat(location) }), false);
mutate(data => ({ ...data!, items: data!.items.concat(location) }), false);
setVisible(false);
})
.catch(error => {

View File

@ -270,6 +270,12 @@ const NestEditContainer = () => {
<p css={tw`text-base text-neutral-400 whitespace-nowrap overflow-ellipsis overflow-hidden`}>{nest.description}</p>
}
</div>
<div css={tw`flex ml-auto pl-4`}>
<Button type={'button'} size={'large'} css={tw`h-10 px-4 py-0 whitespace-nowrap`}>
New Egg
</Button>
</div>
</div>
<FlashMessageRender byKey={'nest'} css={tw`mb-4`}/>

View File

@ -34,7 +34,7 @@ export default () => {
createNest(name, description)
.then(nest => {
mutate(data => ({ ...data, items: data.items.concat(nest) }), false);
mutate(data => ({ ...data!, items: data!.items.concat(nest) }), false);
setVisible(false);
})
.catch(error => {

View File

@ -1,84 +0,0 @@
import React, { useEffect, useState } from 'react';
import tw from 'twin.macro';
import { useRouteMatch } from 'react-router-dom';
import { action, Action, Actions, createContextStore, useStoreActions } from 'easy-peasy';
import getEgg, { Egg } from '@/api/admin/eggs/getEgg';
import AdminContentBlock from '@/components/admin/AdminContentBlock';
import Spinner from '@/components/elements/Spinner';
import FlashMessageRender from '@/components/FlashMessageRender';
import { ApplicationStore } from '@/state';
interface ctx {
egg: Egg | undefined;
setEgg: Action<ctx, Egg | undefined>;
}
export const Context = createContextStore<ctx>({
egg: undefined,
setEgg: action((state, payload) => {
state.egg = payload;
}),
});
const EggEditContainer = () => {
const match = useRouteMatch<{ nestId?: string, id?: string }>();
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const [ loading, setLoading ] = useState(true);
const egg = Context.useStoreState(state => state.egg);
const setEgg = Context.useStoreActions(actions => actions.setEgg);
useEffect(() => {
clearFlashes('egg');
getEgg(Number(match.params?.id))
.then(egg => setEgg(egg))
.catch(error => {
console.error(error);
clearAndAddHttpError({ key: 'egg', error });
})
.then(() => setLoading(false));
}, []);
if (loading || egg === undefined) {
return (
<AdminContentBlock>
<FlashMessageRender byKey={'egg'} 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={'Egg - ' + egg.name}>
<div css={tw`w-full flex flex-row items-center mb-8`}>
<div css={tw`flex flex-col flex-shrink`} style={{ minWidth: '0' }}>
<h2 css={tw`text-2xl text-neutral-50 font-header font-medium`}>{egg.name}</h2>
{
(egg.description || '').length < 1 ?
<p css={tw`text-base text-neutral-400`}>
<span css={tw`italic`}>No description</span>
</p>
:
<p css={tw`text-base text-neutral-400 whitespace-nowrap overflow-ellipsis overflow-hidden`}>{egg.description}</p>
}
</div>
</div>
<FlashMessageRender byKey={'egg'} css={tw`mb-4`}/>
</AdminContentBlock>
);
};
export default () => {
return (
<Context.Provider>
<EggEditContainer/>
</Context.Provider>
);
};

View File

@ -0,0 +1,57 @@
import React from 'react';
import tw from 'twin.macro';
import AdminBox from '@/components/admin/AdminBox';
import { Context } from '@/components/admin/nests/eggs/EggRouter';
import Editor2 from '@/components/elements/Editor2';
import { shell } from '@codemirror/legacy-modes/mode/shell';
import Button from '@/components/elements/Button';
import Input from '@/components/elements/Input';
import Label from '@/components/elements/Label';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
const initialContent = `#!/bin/ash
curl -s https://cdn.pterodactyl.io/releases/latest.json | jq
`;
export default () => {
const egg = Context.useStoreState(state => state.egg);
if (egg === undefined) {
return (
<></>
);
}
return (
<AdminBox title={'Install Script'} padding={false}>
<div css={tw`relative pb-4`}>
<SpinnerOverlay visible={false}/>
<Editor2 overrides={tw`h-96 mb-4`} mode={shell} initialContent={initialContent}/>
<div css={tw`mx-6 mb-4`}>
<div css={tw`grid grid-cols-3 gap-x-8 gap-y-6`}>
<div>
<Label>Install Container</Label>
<Input type="text" defaultValue={'ghcr.io/pterodactyl/installers:alpine'}/>
<p className={'input-help'}>The Docker image to use for running this installation script.</p>
</div>
<div>
<Label>Install Entrypoint</Label>
<Input type="text" defaultValue={'/bin/ash'}/>
<p className={'input-help'}>The command that should be used to run this script inside of the installation container.</p>
</div>
</div>
</div>
<div css={tw`flex flex-row border-t border-neutral-600`}>
<Button type={'button'} size={'small'} css={tw`ml-auto mr-6 mt-4`}>
Save Changes
</Button>
</div>
</div>
</AdminBox>
);
};

View File

@ -0,0 +1,117 @@
import EggInstallContainer from '@/components/admin/nests/eggs/EggInstallContainer';
import EggVariablesContainer from '@/components/admin/nests/eggs/EggVariablesContainer';
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 getEgg, { Egg } from '@/api/admin/eggs/getEgg';
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 EggSettingsContainer from '@/components/admin/nests/eggs/EggSettingsContainer';
interface ctx {
egg: Egg | undefined;
setEgg: Action<ctx, Egg | undefined>;
}
export const Context = createContextStore<ctx>({
egg: undefined,
setEgg: action((state, payload) => {
state.egg = payload;
}),
});
const EggRouter = () => {
const location = useLocation();
const match = useRouteMatch<{ id?: string }>();
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const [ loading, setLoading ] = useState(true);
const egg = Context.useStoreState(state => state.egg);
const setEgg = Context.useStoreActions(actions => actions.setEgg);
useEffect(() => {
clearFlashes('egg');
getEgg(Number(match.params?.id))
.then(egg => setEgg(egg))
.catch(error => {
console.error(error);
clearAndAddHttpError({ key: 'egg', error });
})
.then(() => setLoading(false));
}, []);
if (loading || egg === undefined) {
return (
<AdminContentBlock>
<FlashMessageRender byKey={'egg'} 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={'Egg - ' + egg.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`}>{egg.name}</h2>
<p css={tw`text-base text-neutral-400 whitespace-nowrap overflow-ellipsis overflow-hidden`}>{egg.uuid}</p>
</div>
</div>
<FlashMessageRender byKey={'egg'} css={tw`mb-4`}/>
<SubNavigation>
<SubNavigationLink to={`${match.url}`} name={'About'}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path clipRule="evenodd" fillRule="evenodd" d="M4 4a2 2 0 012-2h8a2 2 0 012 2v12a1 1 0 110 2h-3a1 1 0 01-1-1v-2a1 1 0 00-1-1H9a1 1 0 00-1 1v2a1 1 0 01-1 1H4a1 1 0 110-2V4zm3 1h2v2H7V5zm2 4H7v2h2V9zm2-4h2v2h-2V5zm2 4h-2v2h2V9z" />
</svg>
</SubNavigationLink>
<SubNavigationLink to={`${match.url}/variables`} name={'Variables'}>
<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}/install`} name={'Install Script'}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path clipRule="evenodd" fillRule="evenodd" d="M2 5a2 2 0 012-2h12a2 2 0 012 2v10a2 2 0 01-2 2H4a2 2 0 01-2-2V5zm3.293 1.293a1 1 0 011.414 0l3 3a1 1 0 010 1.414l-3 3a1 1 0 01-1.414-1.414L7.586 10 5.293 7.707a1 1 0 010-1.414zM11 12a1 1 0 100 2h3a1 1 0 100-2h-3z" />
</svg>
</SubNavigationLink>
</SubNavigation>
<Switch location={location}>
<Route path={`${match.path}`} exact>
<EggSettingsContainer/>
</Route>
<Route path={`${match.path}/variables`} exact>
<EggVariablesContainer/>
</Route>
<Route path={`${match.path}/install`} exact>
<EggInstallContainer/>
</Route>
</Switch>
</AdminContentBlock>
);
};
export default () => {
return (
<Context.Provider>
<EggRouter/>
</Context.Provider>
);
};

View File

@ -0,0 +1,19 @@
import React from 'react';
import AdminBox from '@/components/admin/AdminBox';
import { Context } from '@/components/admin/nests/eggs/EggRouter';
export default () => {
const egg = Context.useStoreState(state => state.egg);
if (egg === undefined) {
return (
<></>
);
}
return (
<AdminBox title={'Egg Information'}>
</AdminBox>
);
};

View File

@ -0,0 +1,19 @@
import React from 'react';
import AdminBox from '@/components/admin/AdminBox';
import { Context } from '@/components/admin/nests/eggs/EggRouter';
export default () => {
const egg = Context.useStoreState(state => state.egg);
if (egg === undefined) {
return (
<></>
);
}
return (
<AdminBox title={'Variables'}>
</AdminBox>
);
};

View File

@ -4,19 +4,10 @@ import tw from 'twin.macro';
import Field from '@/components/elements/Field';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import { Form, useFormikContext } from 'formik';
import { Context } from '@/components/admin/nodes/NodeRouter';
export default () => {
const { isSubmitting } = useFormikContext();
const node = Context.useStoreState(state => state.node);
if (node === undefined) {
return (
<></>
);
}
return (
<AdminBox title={'Limits'} css={tw`w-full relative`}>
<SpinnerOverlay visible={isSubmitting}/>

View File

@ -4,19 +4,10 @@ import tw from 'twin.macro';
import Field from '@/components/elements/Field';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import { Form, useFormikContext } from 'formik';
import { Context } from '@/components/admin/nodes/NodeRouter';
export default () => {
const { isSubmitting } = useFormikContext();
const node = Context.useStoreState(state => state.node);
if (node === undefined) {
return (
<></>
);
}
return (
<AdminBox title={'Listen'} css={tw`w-full relative`}>
<SpinnerOverlay visible={isSubmitting}/>

View File

@ -1,3 +1,4 @@
import { Filters } from '@/api/admin/servers/getServers';
import React, { useContext, useEffect, useState } from 'react';
import getNodes, { Context as NodesContext } from '@/api/admin/nodes/getNodes';
import FlashMessageRender from '@/components/FlashMessageRender';
@ -35,7 +36,7 @@ const RowCheckbox = ({ id }: { id: number}) => {
const NodesContainer = () => {
const match = useRouteMatch();
const { page, setPage } = useContext(NodesContext);
const { page, setPage, setFilters, sort, setSort, sortDirection } = useContext(NodesContext);
const { clearFlashes, clearAndAddHttpError } = useFlash();
const { data: nodes, error, isValidating } = getNodes([ 'location' ]);
@ -57,6 +58,17 @@ const NodesContainer = () => {
setSelectedNodes(e.currentTarget.checked ? (nodes?.items?.map(node => node.id) || []) : []);
};
const onSearch = (query: string): Promise<void> => {
return new Promise((resolve) => {
if (query.length < 2) {
setFilters(null);
} else {
setFilters({ name: query });
}
return resolve();
});
};
useEffect(() => {
setSelectedNodes([]);
}, [ page ]);
@ -90,17 +102,18 @@ const NodesContainer = () => {
<ContentWrapper
checked={selectedNodesLength === (length === 0 ? -1 : length)}
onSelectAllClick={onSelectAllClick}
onSearch={onSearch}
>
<Pagination data={nodes} onPageSelect={setPage}>
<div css={tw`overflow-x-auto`}>
<table css={tw`w-full table-auto`}>
<TableHead>
<TableHeader name={'ID'}/>
<TableHeader name={'Name'}/>
<TableHeader name={'Location'}/>
<TableHeader name={'FQDN'}/>
<TableHeader name={'Total Memory'}/>
<TableHeader name={'Total Disk'}/>
<TableHeader name={'ID'} direction={sort === 'id' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('id')}/>
<TableHeader name={'Name'} direction={sort === 'name' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('name')}/>
<TableHeader name={'Location'} direction={sort === 'location_id' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('location_id')}/>
<TableHeader name={'FQDN'} direction={sort === 'fqdn' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('fqdn')}/>
<TableHeader name={'Total Memory'} direction={sort === 'memory' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('memory')}/>
<TableHeader name={'Total Disk'} direction={sort === 'disk' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('disk')}/>
<TableHeader/>
<TableHeader/>
</TableHead>
@ -181,9 +194,21 @@ const NodesContainer = () => {
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 (
<NodesContext.Provider value={{ page, setPage }}>
<NodesContext.Provider value={{ page, setPage, filters, setFilters, sort, setSort, sortDirection, setSortDirection }}>
<NodesContainer/>
</NodesContext.Provider>
);

View 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>
);
};

View File

@ -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/>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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,30 +91,31 @@ 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/>
:
// 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'}/>
<TableHeader name={'Name'}/>
<TableHeader name={'Owner'}/>
<TableHeader name={'Node'}/>
<TableHeader name={'Status'}/>
<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 => (
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}/>
@ -148,16 +160,16 @@ const UsersContainer = () => {
</td>
<td css={tw`px-6 whitespace-nowrap`}>
{ server.isInstalling ?
{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.isTransferring ?
server.status === 'transferring' ?
<span css={tw`px-2 inline-flex text-xs leading-5 font-medium rounded-full bg-yellow-200 text-yellow-800`}>
Transferring
</span>
: server.isSuspended ?
: 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>
@ -174,8 +186,8 @@ const UsersContainer = () => {
</table>
</div>
</Pagination>
</ContentWrapper>
}
</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>
);
};

View File

@ -0,0 +1,99 @@
import React, { useCallback, useState } from 'react';
import styled from 'styled-components/macro';
import tw, { TwStyle } from 'twin.macro';
import { autocompletion, completionKeymap } from '@codemirror/autocomplete';
import { closeBrackets, closeBracketsKeymap } from '@codemirror/closebrackets';
import { defaultKeymap, defaultTabBinding } from '@codemirror/commands';
import { commentKeymap } from '@codemirror/comment';
import { foldGutter, foldKeymap } from '@codemirror/fold';
import { lineNumbers, highlightActiveLineGutter } from '@codemirror/gutter';
import { defaultHighlightStyle } from '@codemirror/highlight';
import { history, historyKeymap } from '@codemirror/history';
import { indentOnInput, LezerLanguage } from '@codemirror/language';
import { lintKeymap } from '@codemirror/lint';
import { bracketMatching } from '@codemirror/matchbrackets';
import { rectangularSelection } from '@codemirror/rectangular-selection';
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
import { Extension, EditorState } from '@codemirror/state';
import { StreamLanguage, StreamParser } from '@codemirror/stream-parser';
import { keymap, highlightSpecialChars, drawSelection, highlightActiveLine, EditorView } from '@codemirror/view';
import { ayuMirage } from '@/components/elements/EditorTheme';
const extensions: Extension = [
ayuMirage,
lineNumbers(),
highlightActiveLineGutter(),
highlightSpecialChars(),
history(),
foldGutter(),
drawSelection(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
defaultHighlightStyle.fallback,
bracketMatching(),
closeBrackets(),
autocompletion(),
rectangularSelection(),
highlightActiveLine(),
highlightSelectionMatches(),
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...foldKeymap,
...commentKeymap,
...completionKeymap,
...lintKeymap,
defaultTabBinding,
]),
EditorState.tabSize.of(4),
];
const EditorContainer = styled.div<{ overrides?: TwStyle }>`
min-height: 12rem;
${tw`relative`};
& > div {
${props => props.overrides};
&.cm-focused {
outline: none;
}
}
`;
export interface Props {
className?: string;
overrides?: TwStyle;
mode: LezerLanguage | StreamParser<any>;
initialContent?: string;
}
export default ({ className, overrides, mode, initialContent }: Props) => {
const [ state ] = useState<EditorState>(EditorState.create({
doc: initialContent,
extensions: [ ...extensions, (mode instanceof LezerLanguage) ? mode : StreamLanguage.define(mode) ],
}));
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [ view, setView ] = useState<EditorView>();
const ref = useCallback((node) => {
if (!node) {
return;
}
const view = new EditorView({
state: state,
parent: node,
});
setView(view);
}, []);
return (
<EditorContainer className={className} overrides={overrides} ref={ref}/>
);
};

View File

@ -0,0 +1,144 @@
import { EditorView } from '@codemirror/view';
import { Extension } from '@codemirror/state';
import { HighlightStyle, tags as t } from '@codemirror/highlight';
const highlightBackground = 'transparent';
const background = '#1F2430';
const selection = '#34455A';
const cursor = '#FFCC66';
export const ayuMirageTheme = EditorView.theme({
'&': {
color: '#CBCCC6',
backgroundColor: background,
},
'.cm-content': {
caretColor: cursor,
},
'&.cm-focused .cm-cursor': { borderLeftColor: cursor },
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection': { backgroundColor: selection },
'.cm-panels': { backgroundColor: '#232834', color: '#CBCCC6' },
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
'.cm-searchMatch': {
backgroundColor: '#72a1ff59',
outline: '1px solid #457dff',
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: '#6199ff2f',
},
'.cm-activeLine': { backgroundColor: highlightBackground },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'.cm-matchingBracket, .cm-nonmatchingBracket': {
backgroundColor: '#bad0f847',
outline: '1px solid #515a6b',
},
'.cm-gutters': {
backgroundColor: 'transparent',
color: '#FF3333',
border: 'none',
},
'.cm-gutterElement': {
color: 'rgba(61, 66, 77, 99)',
},
'.cm-activeLineGutter': {
backgroundColor: highlightBackground,
},
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: '#ddd',
},
'.cm-tooltip': {
border: '1px solid #181a1f',
backgroundColor: '#232834',
},
'.cm-tooltip-autocomplete': {
'& > ul > li[aria-selected]': {
backgroundColor: highlightBackground,
color: '#CBCCC6',
},
},
}, { dark: true });
export const ayuMirageHighlightStyle = HighlightStyle.define([
{
tag: t.keyword,
color: '#FFA759',
},
{
tag: [ t.name, t.deleted, t.character, t.propertyName, t.macroName ],
color: '#5CCFE6',
},
{
tag: [ t.function(t.variableName), t.labelName ],
color: '#CBCCC6',
},
{
tag: [ t.color, t.constant(t.name), t.standard(t.name) ],
color: '#F29E74',
},
{
tag: [ t.definition(t.name), t.separator ],
color: '#CBCCC6B3',
},
{
tag: [ t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace ],
color: '#FFCC66',
},
{
tag: [ t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string) ],
color: '#5CCFE6',
},
{
tag: [ t.meta, t.comment ],
color: '#5C6773',
},
{
tag: t.strong,
fontWeight: 'bold',
},
{
tag: t.emphasis,
fontStyle: 'italic',
},
{
tag: t.strikethrough,
textDecoration: 'line-through',
},
{
tag: t.link,
color: '#FF3333',
textDecoration: 'underline',
},
{
tag: t.heading,
fontWeight: 'bold',
color: '#BAE67E',
},
{
tag: [ t.atom, t.bool, t.special(t.variableName) ],
color: '#5CCFE6',
},
{
tag: [ t.processingInstruction, t.string, t.inserted ],
color: '#BAE67E',
},
{
tag: t.invalid,
color: '#FF3333',
},
]);
export const ayuMirage: Extension = [ ayuMirageTheme, ayuMirageHighlightStyle ];

View File

@ -43,7 +43,7 @@ const inputStyle = css<Props>`
& + .input-help {
${tw`mt-1 text-xs`};
${props => props.hasError ? tw`text-red-200` : tw`text-neutral-200`};
${props => props.hasError ? tw`text-red-200` : tw`text-neutral-400`};
}
&:required, &:invalid {

View File

@ -1,12 +1,5 @@
import React, { useState } from 'react';
import {
faBoxOpen,
faCloudDownloadAlt,
faEllipsisH,
faLock,
faTrashAlt,
faUnlock,
} from '@fortawesome/free-solid-svg-icons';
import { faBoxOpen, faCloudDownloadAlt, faEllipsisH, faLock, faTrashAlt, faUnlock } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import DropdownMenu, { DropdownButtonRow } from '@/components/elements/DropdownMenu';
import getBackupDownloadUrl from '@/api/server/backups/getBackupDownloadUrl';
@ -56,8 +49,8 @@ export default ({ backup }: Props) => {
clearFlashes('backups');
deleteBackup(uuid, backup.uuid)
.then(() => mutate(data => ({
...data,
items: data.items.filter(b => b.uuid !== backup.uuid),
...data!,
items: data!.items.filter(b => b.uuid !== backup.uuid),
}), false))
.catch(error => {
console.error(error);
@ -90,8 +83,8 @@ export default ({ backup }: Props) => {
http.post(`/api/client/servers/${uuid}/backups/${backup.uuid}/lock`)
.then(() => mutate(data => ({
...data,
items: data.items.map(b => b.uuid !== backup.uuid ? b : {
...data!,
items: data!.items.map(b => b.uuid !== backup.uuid ? b : {
...b,
isLocked: !b.isLocked,
}),
@ -124,13 +117,13 @@ export default ({ backup }: Props) => {
not be able to control the server power state, access the file manager, or create additional backups
until it has completed.
</p>
<p css={tw`text-neutral-300 mt-4`}>
<p css={tw`mt-4 text-neutral-300`}>
Are you sure you want to continue?
</p>
<p css={tw`mt-4 -mb-2 bg-neutral-900 p-3 rounded`}>
<p css={tw`p-3 mt-4 -mb-2 rounded bg-neutral-900`}>
<label
htmlFor={'restore_truncate'}
css={tw`text-base text-neutral-200 flex items-center cursor-pointer`}
css={tw`flex items-center text-base cursor-pointer text-neutral-200`}
>
<Input
type={'checkbox'}
@ -160,7 +153,7 @@ export default ({ backup }: Props) => {
renderToggle={onClick => (
<button
onClick={onClick}
css={tw`text-neutral-200 transition-colors duration-150 hover:text-neutral-100 p-2`}
css={tw`p-2 transition-colors duration-150 text-neutral-200 hover:text-neutral-100`}
>
<FontAwesomeIcon icon={faEllipsisH}/>
</button>
@ -185,7 +178,7 @@ export default ({ backup }: Props) => {
<FontAwesomeIcon
fixedWidth
icon={backup.isLocked ? faUnlock : faLock}
css={tw`text-xs mr-2`}
css={tw`mr-2 text-xs`}
/>
{backup.isLocked ? 'Unlock' : 'Lock'}
</DropdownButtonRow>
@ -202,7 +195,7 @@ export default ({ backup }: Props) => {
:
<button
onClick={() => setModal('delete')}
css={tw`text-neutral-200 transition-colors duration-150 hover:text-neutral-100 p-2`}
css={tw`p-2 transition-colors duration-150 text-neutral-200 hover:text-neutral-100`}
>
<FontAwesomeIcon icon={faTrashAlt}/>
</button>

View File

@ -26,8 +26,8 @@ export default ({ backup, className }: Props) => {
const parsed = JSON.parse(data);
mutate(data => ({
...data,
items: data.items.map(b => b.uuid !== backup.uuid ? b : ({
...data!,
items: data!.items.map(b => b.uuid !== backup.uuid ? b : ({
...b,
isSuccessful: parsed.is_successful || true,
checksum: (parsed.checksum_type || '') + ':' + (parsed.checksum || ''),

View File

@ -81,7 +81,7 @@ export default () => {
clearFlashes('backups:create');
createServerBackup(uuid, values)
.then(backup => {
mutate(data => ({ ...data, items: data.items.concat(backup) }), false);
mutate(data => ({ ...data!, items: data!.items.concat(backup) }), false);
setVisible(false);
})
.catch(error => {

View File

@ -31,7 +31,7 @@ const ChmodFileModal = ({ files, ...props }: OwnProps) => {
const submit = ({ mode }: FormikValues, { setSubmitting }: FormikHelpers<FormikValues>) => {
clearFlashes('files');
mutate(data => data.map(f => f.name === files[0].file ? { ...f, mode: fileBitsToString(mode, !f.isFile), modeBits: mode } : f), false);
mutate(data => data!.map(f => f.name === files[0].file ? { ...f, mode: fileBitsToString(mode, !f.isFile), modeBits: mode } : f), false);
const data = files.map(f => ({ file: f.file, mode: mode }));

View File

@ -75,7 +75,7 @@ const FileDropdownMenu = ({ file }: { file: FileObject }) => {
// For UI speed, immediately remove the file from the listing before calling the deletion function.
// If the delete actually fails, we'll fetch the current directory contents again automatically.
mutate(files => files.filter(f => f.key !== file.key), false);
mutate(files => files!.filter(f => f.key !== file.key), false);
deleteFiles(uuid, directory, [ file.name ]).catch(error => {
mutate();

View File

@ -51,7 +51,7 @@ const MassActionsBar = () => {
deleteFiles(uuid, directory, selectedFiles)
.then(() => {
mutate(files => files.filter(f => selectedFiles.indexOf(f.name) < 0), false);
mutate(files => files!.filter(f => selectedFiles.indexOf(f.name) < 0), false);
setSelectedFiles([]);
})
.catch(error => {

View File

@ -55,7 +55,7 @@ export default ({ className }: WithClassname) => {
const submit = ({ directoryName }: Values, { setSubmitting }: FormikHelpers<Values>) => {
createDirectory(uuid, directory, directoryName)
.then(() => mutate(data => [ ...data, generateDirectoryData(directoryName) ], false))
.then(() => mutate(data => [ ...data!, generateDirectoryData(directoryName) ], false))
.then(() => setVisible(false))
.catch(error => {
console.error(error);

View File

@ -30,10 +30,10 @@ const RenameFileModal = ({ files, useMoveTerminology, ...props }: OwnProps) => {
if (files.length === 1) {
if (!useMoveTerminology && len === 1) {
// Rename the file within this directory.
mutate(data => data.map(f => f.name === files[0] ? { ...f, name } : f), false);
mutate(data => data!.map(f => f.name === files[0] ? { ...f, name } : f), false);
} else if ((useMoveTerminology || len > 1)) {
// Remove the file from this directory since they moved it elsewhere.
mutate(data => data.filter(f => f.name !== files[0]), false);
mutate(data => data!.filter(f => f.name !== files[0]), false);
}
}

View File

@ -35,7 +35,7 @@ interface Values {
const schema = object().shape({
action: string().required().oneOf([ 'command', 'power', 'backup' ]),
payload: string().when('action', {
is: v => v !== 'backup',
is: (v: string) => v !== 'backup',
then: string().required('A task payload must be provided.'),
otherwise: string(),
}),

View File

@ -32,9 +32,9 @@ const VariableBox = ({ variable }: Props) => {
updateStartupVariable(uuid, variable.envVariable, value)
.then(([ response, invocation ]) => mutate(data => ({
...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);

View File

@ -1,5 +1,3 @@
import ServerRouter
from '@/components/admin/servers/ServerRouter';
import React, { useState } from 'react';
import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom';
import { State, useStoreState } from 'easy-peasy';
@ -29,9 +27,10 @@ import RolesContainer from '@/components/admin/roles/RolesContainer';
import RoleEditContainer from '@/components/admin/roles/RoleEditContainer';
import NestsContainer from '@/components/admin/nests/NestsContainer';
import NestEditContainer from '@/components/admin/nests/NestEditContainer';
import EggEditContainer from '@/components/admin/nests/eggs/EggEditContainer';
import MountsContainer from '@/components/admin/mounts/MountsContainer';
import MountEditContainer from '@/components/admin/mounts/MountEditContainer';
import EggRouter from '@/components/admin/nests/eggs/EggRouter';
import ServerRouter from '@/components/admin/servers/ServerRouter';
const Sidebar = styled.div<{ collapsed?: boolean }>`
${tw`fixed h-screen hidden md:flex flex-col items-center flex-shrink-0 bg-neutral-900 overflow-x-hidden transition-all duration-250 ease-linear`};
@ -260,8 +259,7 @@ const AdminRouter = ({ location, match }: RouteComponentProps) => {
/>
<Route
path={`${match.path}/nests/:nestId/eggs/:id`}
component={EggEditContainer}
exact
component={EggRouter}
/>
<Route path={`${match.path}/mounts`} component={MountsContainer} exact/>

329
yarn.lock
View File

@ -889,6 +889,226 @@
lodash "^4.17.19"
to-fast-properties "^2.0.0"
"@codemirror/autocomplete@^0.18.5":
version "0.18.5"
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-0.18.5.tgz#5c25ddbef858503920fa4912b48bf78be93ee462"
integrity sha512-Zp/HMTwvaP4B01HQx+5Ga0xBBLAwNaAlbALip355NPRBkYX9PZheX5b/F5IUAdI6kIrF2eYz1xAOChXgYg9maw==
dependencies:
"@codemirror/language" "^0.18.0"
"@codemirror/state" "^0.18.0"
"@codemirror/text" "^0.18.0"
"@codemirror/tooltip" "^0.18.4"
"@codemirror/view" "^0.18.0"
lezer-tree "^0.13.0"
"@codemirror/closebrackets@^0.18.0":
version "0.18.0"
resolved "https://registry.yarnpkg.com/@codemirror/closebrackets/-/closebrackets-0.18.0.tgz#4bd7e9227ed6e90e590fa6d289d34b0c065cb8cf"
integrity sha512-O1RAgUkzF4nq/B8IyXenZKZ1rJi2Mc7I6y4IhWhELiTnjyQy7YdAthTsJ40mNr8kZ6gRbasYe3K7TraITElZJA==
dependencies:
"@codemirror/language" "^0.18.0"
"@codemirror/rangeset" "^0.18.0"
"@codemirror/state" "^0.18.0"
"@codemirror/text" "^0.18.0"
"@codemirror/view" "^0.18.0"
"@codemirror/commands@^0.18.2":
version "0.18.2"
resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-0.18.2.tgz#a90067b1e3127ffe2c1be2daa68c0f4aeda59308"
integrity sha512-NeIpsQe5yUIhG7sD1HPaw/9TO5V7miMKwGwhT/0tkgkmgnMtJcgnguM1gjaUlaZ09BBJO6s61D8JHNDUvBI6tA==
dependencies:
"@codemirror/language" "^0.18.0"
"@codemirror/matchbrackets" "^0.18.0"
"@codemirror/state" "^0.18.0"
"@codemirror/text" "^0.18.0"
"@codemirror/view" "^0.18.0"
lezer-tree "^0.13.0"
"@codemirror/comment@^0.18.1":
version "0.18.1"
resolved "https://registry.yarnpkg.com/@codemirror/comment/-/comment-0.18.1.tgz#e39c8b6937b86852246decb3441683c66b03abf4"
integrity sha512-Inhqs0F24WE28Fcp1dBZghwixBGv1HDwY9MjE0d5tpMY/IPGI6uT30fGyHAXrir6hUqk7eJRkO4UYnODGOnoIA==
dependencies:
"@codemirror/state" "^0.18.0"
"@codemirror/text" "^0.18.0"
"@codemirror/view" "^0.18.0"
"@codemirror/fold@^0.18.1":
version "0.18.1"
resolved "https://registry.yarnpkg.com/@codemirror/fold/-/fold-0.18.1.tgz#118019ad79f6e0d48dc932823385d4d9f2e0eaf5"
integrity sha512-vvMUgDeSmeVow7/75YoNTERxPsdnIBeEw1JL2YVpLyscsUlalqwuxdhiHDLT5zjAu6JvMoTC103mwqgAYwM9tA==
dependencies:
"@codemirror/gutter" "^0.18.0"
"@codemirror/language" "^0.18.0"
"@codemirror/rangeset" "^0.18.0"
"@codemirror/state" "^0.18.0"
"@codemirror/view" "^0.18.0"
"@codemirror/gutter@^0.18.0", "@codemirror/gutter@^0.18.3":
version "0.18.3"
resolved "https://registry.yarnpkg.com/@codemirror/gutter/-/gutter-0.18.3.tgz#d195e2f69c6bdba037ec9c4d97eee0b46b1f02b7"
integrity sha512-9ZLpR3s3MCPtB5fpRAy8af89GjO763XB4GE4HLBnESS7E1CHAuHFSTyd/ZqKHZogueF1mJhv9IQnCamEYcNMQw==
dependencies:
"@codemirror/rangeset" "^0.18.0"
"@codemirror/state" "^0.18.0"
"@codemirror/view" "^0.18.0"
"@codemirror/highlight@^0.18.0", "@codemirror/highlight@^0.18.4":
version "0.18.4"
resolved "https://registry.yarnpkg.com/@codemirror/highlight/-/highlight-0.18.4.tgz#83dfd402d7cbfe67dc9d0cb93a25755321014829"
integrity sha512-3azJntqWrShOIq/0kVcdMc9k7ACL0LQErgK+A6aWXmCj5Mx0gShq+Iajy8AMQ2zB0v3nhCBgFaniL1LLD5m5hQ==
dependencies:
"@codemirror/language" "^0.18.0"
"@codemirror/rangeset" "^0.18.0"
"@codemirror/state" "^0.18.0"
"@codemirror/view" "^0.18.0"
lezer-tree "^0.13.0"
style-mod "^4.0.0"
"@codemirror/history@^0.18.1":
version "0.18.1"
resolved "https://registry.yarnpkg.com/@codemirror/history/-/history-0.18.1.tgz#853cde4b138b172235d58f945871f0fc08b7310a"
integrity sha512-Aad3p4zs6UYKCUMXYjh7cvPK0ajuL+rMib9yBZ61w81LLl6OkM31Xrn9J6CLJmPxCwP3OJFiqBmNSBQ05oIsTw==
dependencies:
"@codemirror/state" "^0.18.3"
"@codemirror/view" "^0.18.0"
"@codemirror/lang-json@^0.18.0":
version "0.18.0"
resolved "https://registry.yarnpkg.com/@codemirror/lang-json/-/lang-json-0.18.0.tgz#775f2dc95039a86fe3e6e55e4467e10c2ecae414"
integrity sha512-1xquwhwrocZEcURmccMYk1C3g2oj6mu0np1LT1dDGmvd/VVXC0Z9j6NV78zCSyJCIdL2y+RRBh4LVWW6J6NU6Q==
dependencies:
"@codemirror/highlight" "^0.18.0"
"@codemirror/language" "^0.18.0"
lezer-json "^0.13.0"
"@codemirror/language@^0.18.0", "@codemirror/language@^0.18.1":
version "0.18.1"
resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-0.18.1.tgz#23682324228606c4ae5b6a9f7cd0a4b9fdff83dd"
integrity sha512-j/TWV8sNmzU79kk/hPLb9NqSPoH59850kQSpgY11LxOEBlKVRKgaWabgNtUCSeVCAnfisGekupk3aq2ftILqug==
dependencies:
"@codemirror/state" "^0.18.0"
"@codemirror/text" "^0.18.0"
"@codemirror/view" "^0.18.0"
lezer "^0.13.4"
lezer-tree "^0.13.0"
"@codemirror/legacy-modes@^0.18.0":
version "0.18.0"
resolved "https://registry.yarnpkg.com/@codemirror/legacy-modes/-/legacy-modes-0.18.0.tgz#64bb6819758a50ccf777e4d64d59dec0fac2e169"
integrity sha512-ME/FnBTRf+nriCB1Racmwhl3tSSnIOobhLyK0kOX6mogKdcdkRxSVpeo1fAC8DddsXqi/NKRn8GUr/vUiHefdg==
dependencies:
"@codemirror/stream-parser" "^0.18.0"
"@codemirror/lint@^0.18.3":
version "0.18.3"
resolved "https://registry.yarnpkg.com/@codemirror/lint/-/lint-0.18.3.tgz#31c629485e910b3f145e2b819d2f012cd17be016"
integrity sha512-ORD8bGQRv5FATJ/LUmhaVPz69ucNjMCQ53WF8TqYMw6KtlTwAFsi+GvEhYyav7CdXPAXS/Z/j0C0Op8d1Dp+lQ==
dependencies:
"@codemirror/panel" "^0.18.1"
"@codemirror/state" "^0.18.0"
"@codemirror/tooltip" "^0.18.4"
"@codemirror/view" "^0.18.0"
crelt "^1.0.5"
"@codemirror/matchbrackets@^0.18.0":
version "0.18.0"
resolved "https://registry.yarnpkg.com/@codemirror/matchbrackets/-/matchbrackets-0.18.0.tgz#64a493090d942de19f15a9ed3cb0fa19ed55f18b"
integrity sha512-dPDopnZVkD54sSYdmQbyQbPdiuIA83p7XxX6Hp1ScEkOjukwCiFXiA/84x10FUTsQpUYp8bDzm7gwII119bGIw==
dependencies:
"@codemirror/language" "^0.18.0"
"@codemirror/state" "^0.18.0"
"@codemirror/view" "^0.18.0"
lezer-tree "^0.13.0"
"@codemirror/panel@^0.18.1":
version "0.18.2"
resolved "https://registry.yarnpkg.com/@codemirror/panel/-/panel-0.18.2.tgz#f82dd69fc82d752ec5d6269bbdecbbdb8df69529"
integrity sha512-ea/g2aAKtfmie1kD7C8GDutD/5u+uzRJr/varUiAbHKr1sAdjtz5xYvC3GBAMYMan1GOh0vD5zP1yEupJl3b3Q==
dependencies:
"@codemirror/state" "^0.18.0"
"@codemirror/view" "^0.18.0"
"@codemirror/rangeset@^0.18.0":
version "0.18.1"
resolved "https://registry.yarnpkg.com/@codemirror/rangeset/-/rangeset-0.18.1.tgz#0e51e1117fa5aae0134073735a07c659f334a699"
integrity sha512-Q+t92KlvDDD9oNXOvYHNL3kEUF1Y595OjSsl5GNZYI2JPY8IW9PoH1z37sgqgxMMuWQrDNp6AOrnd2j/7uBhFA==
dependencies:
"@codemirror/state" "^0.18.0"
"@codemirror/rectangular-selection@^0.18.0":
version "0.18.0"
resolved "https://registry.yarnpkg.com/@codemirror/rectangular-selection/-/rectangular-selection-0.18.0.tgz#87e1a4d319b5d55b4e97294e6df0070164e836c0"
integrity sha512-BQ4pp2zhXCVZNqct5LtLR3AOWVseENBF/3oOgBmwsCKH7c11NfTqIqgWG5EW8NLOXp8HP8cDm3np8eWez0VkGQ==
dependencies:
"@codemirror/state" "^0.18.0"
"@codemirror/text" "^0.18.0"
"@codemirror/view" "^0.18.0"
"@codemirror/search@^0.18.3":
version "0.18.3"
resolved "https://registry.yarnpkg.com/@codemirror/search/-/search-0.18.3.tgz#6c5f51eb7f973b45bd88a9913f03c1756fbdc606"
integrity sha512-9s8ltRtBHcEZmE9lHBmYArfdj9bpsCkxLYjvrzOVqix3wv5DVAgcfk3Kd1WkahhkTzycYULC+r9KKmelUEyTbg==
dependencies:
"@codemirror/panel" "^0.18.1"
"@codemirror/rangeset" "^0.18.0"
"@codemirror/state" "^0.18.6"
"@codemirror/text" "^0.18.0"
"@codemirror/view" "^0.18.0"
crelt "^1.0.5"
"@codemirror/state@^0.18.0", "@codemirror/state@^0.18.3", "@codemirror/state@^0.18.6", "@codemirror/state@^0.18.7":
version "0.18.7"
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-0.18.7.tgz#3339a732387bb2c034987c57ccf0649ef2f7c4c1"
integrity sha512-cVyTiAC9vv90NKmGOfNtBjyIem3BqKui1L5Hfcxurp8K9votQj2oH9COcgWPnQ2Xs64yC70tEuTt9DF1pj5PFQ==
dependencies:
"@codemirror/text" "^0.18.0"
"@codemirror/stream-parser@^0.18.0", "@codemirror/stream-parser@^0.18.2":
version "0.18.2"
resolved "https://registry.yarnpkg.com/@codemirror/stream-parser/-/stream-parser-0.18.2.tgz#d96ac5724650719c4a7784f2b94449366b22130e"
integrity sha512-3RTRmhIixcC2ps/G8So+BL0qJkwaspjyYt4smVYlSn4eNbxGK9K2RCnSmOPRv0SkuQMu3oUFbprFI/SbtZrPKg==
dependencies:
"@codemirror/highlight" "^0.18.0"
"@codemirror/language" "^0.18.0"
"@codemirror/state" "^0.18.0"
"@codemirror/text" "^0.18.0"
lezer "^0.13.0"
lezer-tree "^0.13.0"
"@codemirror/text@^0.18.0":
version "0.18.0"
resolved "https://registry.yarnpkg.com/@codemirror/text/-/text-0.18.0.tgz#a4a98862989ccef5545e730b269136d524c6a7c7"
integrity sha512-HMzHNIAbjCiCf3tEJMRg6ul01KPuXxQGNiHlHgAnqPguq/CX+L4Nvj5JlWQAI91Pupk18zhmM1c6eaazX4YeTg==
"@codemirror/theme-one-dark@^0.18.1":
version "0.18.1"
resolved "https://registry.yarnpkg.com/@codemirror/theme-one-dark/-/theme-one-dark-0.18.1.tgz#fa625db384418e89b778d2ae49824c44d5280e3f"
integrity sha512-0XRfWYDfwUlPlN8yrO7bDB+EuHFqUNhTJwgp2iIixZWejuZQK0NxKmjuhkiGsEz25w7toM12uUsNJ5mo7iFQcA==
dependencies:
"@codemirror/highlight" "^0.18.0"
"@codemirror/state" "^0.18.0"
"@codemirror/view" "^0.18.0"
"@codemirror/tooltip@^0.18.4":
version "0.18.4"
resolved "https://registry.yarnpkg.com/@codemirror/tooltip/-/tooltip-0.18.4.tgz#981bc0ced792c6754148edbc1f60092f3fa54207"
integrity sha512-LDlDOSEfjoG24uapLN7exK3Z3JchYFKUwWqo1x/9YdlAkmD1ik7cMSQZboCquP1uJVcXhtbpKmaO6vENGVaarg==
dependencies:
"@codemirror/state" "^0.18.0"
"@codemirror/view" "^0.18.0"
"@codemirror/view@^0.18.0", "@codemirror/view@^0.18.12":
version "0.18.12"
resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-0.18.12.tgz#28d6fdefd7362481bff3b05ab893c10710311e13"
integrity sha512-8PLqxl136aRiNO9dIKuB4CIMP6pgHAMXIbM0HxYMdFiLKaAH+nnvVFJVKCY3DUqaEWBB9R+OvUPVa0Rx/LpqLw==
dependencies:
"@codemirror/rangeset" "^0.18.0"
"@codemirror/state" "^0.18.0"
"@codemirror/text" "^0.18.0"
style-mod "^4.0.0"
w3c-keyname "^2.2.4"
"@emotion/is-prop-valid@^0.8.8":
version "0.8.8"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a"
@ -1056,6 +1276,11 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
"@types/lodash@^4.14.165":
version "4.14.169"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.169.tgz#83c217688f07a4d9ef8f28a3ebd1d318f6ff4cbb"
integrity sha512-DvmZHoHTFJ8zhVYwCLWbQ7uAbYQEk52Ev2/ZiQ7Y7gQGeV9pjBqjnQpECMHfKS1rCYAhMI7LHVxwyZLZinJgdw==
"@types/minimatch@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
@ -2500,6 +2725,11 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
crelt@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.5.tgz#57c0d52af8c859e354bace1883eb2e1eb182bb94"
integrity sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==
cross-env@^7.0.2:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
@ -2725,6 +2955,11 @@ depd@~1.1.2:
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
dequal@2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d"
integrity sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==
des.js@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
@ -3335,11 +3570,6 @@ extglob@^2.0.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
fast-deep-equal@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
fast-deep-equal@^3.1.1:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@ -3512,11 +3742,6 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
fn-name@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/fn-name/-/fn-name-3.0.0.tgz#0596707f635929634d791f452309ab41558e3c5c"
integrity sha512-eNMNr5exLoavuAMhIUVsOKF79SWd/zG104ef6sxBTSw+cZc6BXdQXDvYcGvp0VbxVVSp1XDUNoz7mg1xMtSznA==
follow-redirects@^1.0.0, follow-redirects@^1.10.0:
version "1.13.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.2.tgz#dd73c8effc12728ba5cf4259d760ea5fb83e3147"
@ -4571,6 +4796,25 @@ levn@^0.4.1:
prelude-ls "^1.2.1"
type-check "~0.4.0"
lezer-json@^0.13.0:
version "0.13.1"
resolved "https://registry.yarnpkg.com/lezer-json/-/lezer-json-0.13.1.tgz#d48b272d4f4a6eefa8183d603aa22115f8ecd7ce"
integrity sha512-iXIXSjqa/+jmaDKD4yPgpwfwQlbOcu/IaFgZ+3dgo3oVtitEVlAJYcfw8g9ywJtae3CYQlyOX9mjyi0VCNPlNg==
dependencies:
lezer "^0.13.0"
lezer-tree@^0.13.0, lezer-tree@^0.13.2:
version "0.13.2"
resolved "https://registry.yarnpkg.com/lezer-tree/-/lezer-tree-0.13.2.tgz#00f4671309b15c27b131f637e430ce2d4d5f7065"
integrity sha512-15ZxW8TxVNAOkHIo43Iouv4zbSkQQ5chQHBpwXcD2bBFz46RB4jYLEEww5l1V0xyIx9U2clSyyrLes+hAUFrGQ==
lezer@^0.13.0, lezer@^0.13.4:
version "0.13.5"
resolved "https://registry.yarnpkg.com/lezer/-/lezer-0.13.5.tgz#6000536bca7e24a5bd62e8cb4feff28b37e7dd8f"
integrity sha512-cAiMQZGUo2BD8mpcz7Nv1TlKzWP7YIdIRrX41CiP5bk5t4GHxskOxWUx2iAOuHlz8dO+ivbuXr0J1bfHsWD+lQ==
dependencies:
lezer-tree "^0.13.2"
lines-and-columns@^1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
@ -4632,11 +4876,16 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
lodash-es@^4.17.11, lodash-es@^4.17.14:
lodash-es@^4.17.14:
version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.20.tgz#29f6332eefc60e849f869c264bc71126ad61e8f7"
integrity sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA==
lodash-es@^4.17.15:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
lodash.flatmap@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.flatmap/-/lodash.flatmap-4.5.0.tgz#ef8cbf408f6e48268663345305c6acc0b778702e"
@ -5020,6 +5269,11 @@ nan@^2.12.1:
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
nanoclone@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4"
integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==
nanoid@^3.1.20:
version "3.1.20"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
@ -5719,7 +5973,7 @@ prop-types@^15.5.0, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1,
object-assign "^4.1.1"
react-is "^16.8.1"
property-expr@^2.0.2:
property-expr@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.4.tgz#37b925478e58965031bb612ec5b3260f8241e910"
integrity sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg==
@ -6900,6 +7154,11 @@ style-loader@^1.2.1:
loader-utils "^2.0.0"
schema-utils "^2.7.0"
style-mod@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.0.0.tgz#97e7c2d68b592975f2ca7a63d0dd6fcacfe35a01"
integrity sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==
styled-components-breakpoint@^3.0.0-preview.20:
version "3.0.0-preview.20"
resolved "https://registry.yarnpkg.com/styled-components-breakpoint/-/styled-components-breakpoint-3.0.0-preview.20.tgz#877e88a00c0cf66976f610a1d347839a1a0b6d70"
@ -6950,12 +7209,12 @@ svg-url-loader@^6.0.0:
file-loader "~6.0.0"
loader-utils "~2.0.0"
swr@^0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/swr/-/swr-0.2.3.tgz#e0fb260d27f12fafa2388312083368f45127480d"
integrity sha512-JhuuD5ojqgjAQpZAhoPBd8Di0Mr1+ykByVKuRJdtKaxkUX/y8kMACWKkLgLQc8pcDOKEAnbIreNjU7HfqI9nHQ==
swr@^0.5.6:
version "0.5.6"
resolved "https://registry.yarnpkg.com/swr/-/swr-0.5.6.tgz#70bfe9bc9d7ac49a064be4a0f4acf57982e55a31"
integrity sha512-Bmx3L4geMZjYT5S2Z6EE6/5Cx6v1Ka0LhqZKq8d6WL2eu9y6gHWz3dUzfIK/ymZVHVfwT/EweFXiYGgfifei3w==
dependencies:
fast-deep-equal "2.0.1"
dequal "2.0.2"
symbol-observable@^1.2.0:
version "1.2.0"
@ -6967,11 +7226,6 @@ symbol-observable@^2.0.3:
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-2.0.3.tgz#5b521d3d07a43c351055fa43b8355b62d33fd16a"
integrity sha512-sQV7phh2WCYAn81oAkakC5qjq2Ml0g8ozqz03wOGnx9dDlG1de6yrF+0RAzSJD8fPUow3PTSMf2SAbOGxb93BA==
synchronous-promise@^2.0.13:
version "2.0.15"
resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.15.tgz#07ca1822b9de0001f5ff73595f3d08c4f720eb8e"
integrity sha512-k8uzYIkIVwmT+TcglpdN50pS2y1BDcUnBPK9iJeGu0Pl1lOI8pD6wtzgw91Pjpe+RxtTncw32tLxs/R0yNL2Mg==
table@^6.0.4:
version "6.0.7"
resolved "https://registry.yarnpkg.com/table/-/table-6.0.7.tgz#e45897ffbcc1bcf9e8a87bf420f2c9e5a7a52a34"
@ -7421,6 +7675,11 @@ void-elements@^2.0.1:
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=
w3c-keyname@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.4.tgz#4ade6916f6290224cdbd1db8ac49eab03d0eef6b"
integrity sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw==
watchpack-chokidar2@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957"
@ -7694,10 +7953,10 @@ xterm-addon-web-links@^0.4.0:
resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.4.0.tgz#265cbf8221b9b315d0a748e1323bee331cd5da03"
integrity sha512-xv8GeiINmx0zENO9hf5k+5bnkaE8mRzF+OBAr9WeFq2eLaQSudioQSiT34M1ofKbzcdjSsKiZm19Rw3i4eXamg==
xterm@^4.10.0:
version "4.10.0"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.10.0.tgz#fc4f554e3e718aff9b83622e858e64b0953067bb"
integrity sha512-Wn66I8YpSVkgP3R95GjABC6Eb21pFfnCSnyIqKIIoUI13ohvwd0KGVzUDfyEFfSAzKbPJfrT2+vt7SfUXBZQKQ==
xterm@^4.12.0:
version "4.12.0"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.12.0.tgz#db09b425b4dcae5b96f8cbbaaa93b3bc60997ca9"
integrity sha512-K5mF/p3txUV18mjiZFlElagoHFpqXrm5OYHeoymeXSu8GG/nMaOO/+NRcNCwfdjzAbdQ5VLF32hEHiWWKKm0bw==
y18n@^4.0.0:
version "4.0.1"
@ -7757,15 +8016,15 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
yup@^0.29.1:
version "0.29.3"
resolved "https://registry.yarnpkg.com/yup/-/yup-0.29.3.tgz#69a30fd3f1c19f5d9e31b1cf1c2b851ce8045fea"
integrity sha512-RNUGiZ/sQ37CkhzKFoedkeMfJM0vNQyaz+wRZJzxdKE7VfDeVKH8bb4rr7XhRLbHJz5hSjoDNwMEIaKhuMZ8gQ==
yup@^0.32.9:
version "0.32.9"
resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.9.tgz#9367bec6b1b0e39211ecbca598702e106019d872"
integrity sha512-Ci1qN+i2H0XpY7syDQ0k5zKQ/DoxO0LzPg8PAR/X4Mpj6DqaeCoIYEEjDJwhArh3Fa7GWbQQVDZKeXYlSH4JMg==
dependencies:
"@babel/runtime" "^7.10.5"
fn-name "~3.0.0"
lodash "^4.17.15"
lodash-es "^4.17.11"
property-expr "^2.0.2"
synchronous-promise "^2.0.13"
"@types/lodash" "^4.14.165"
lodash "^4.17.20"
lodash-es "^4.17.15"
nanoclone "^0.2.1"
property-expr "^2.0.4"
toposort "^2.0.2"