forked from Alex/Pterodactyl-Panel
ui(admin): too many changes, not enough commits
This commit is contained in:
parent
bca2338863
commit
8aa9641ec2
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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),
|
||||
|
25
package.json
25
package.json
@ -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",
|
||||
|
23
resources/scripts/api/admin/nodes/getAllocations.ts
Normal file
23
resources/scripts/api/admin/nodes/getAllocations.ts
Normal 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);
|
||||
});
|
||||
};
|
@ -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),
|
||||
|
@ -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,
|
||||
},
|
||||
|
12
resources/scripts/api/admin/servers/updateServer.ts
Normal file
12
resources/scripts/api/admin/servers/updateServer.ts
Normal 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);
|
||||
});
|
||||
};
|
25
resources/scripts/api/admin/users/searchUsers.ts
Normal file
25
resources/scripts/api/admin/users/searchUsers.ts
Normal 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);
|
||||
});
|
||||
};
|
@ -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);
|
||||
|
@ -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`}>
|
||||
|
@ -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 => {
|
||||
|
@ -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`}/>
|
||||
|
@ -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 => {
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
117
resources/scripts/components/admin/nests/eggs/EggRouter.tsx
Normal file
117
resources/scripts/components/admin/nests/eggs/EggRouter.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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}/>
|
||||
|
@ -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}/>
|
||||
|
@ -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>
|
||||
);
|
||||
|
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,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>
|
||||
);
|
||||
};
|
||||
|
99
resources/scripts/components/elements/Editor2.tsx
Normal file
99
resources/scripts/components/elements/Editor2.tsx
Normal 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}/>
|
||||
);
|
||||
};
|
144
resources/scripts/components/elements/EditorTheme.ts
Normal file
144
resources/scripts/components/elements/EditorTheme.ts
Normal 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 ];
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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 || ''),
|
||||
|
@ -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 => {
|
||||
|
@ -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 }));
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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 => {
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(),
|
||||
}),
|
||||
|
@ -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);
|
||||
|
@ -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
329
yarn.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user