ui(admin): allow editing allocations for servers
This commit is contained in:
parent
656ac62ad2
commit
a6ab61adba
|
@ -6,6 +6,8 @@ use Pterodactyl\Models\Node;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Pterodactyl\Models\Allocation;
|
use Pterodactyl\Models\Allocation;
|
||||||
use Spatie\QueryBuilder\QueryBuilder;
|
use Spatie\QueryBuilder\QueryBuilder;
|
||||||
|
use Spatie\QueryBuilder\AllowedFilter;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Pterodactyl\Services\Allocations\AssignmentService;
|
use Pterodactyl\Services\Allocations\AssignmentService;
|
||||||
use Pterodactyl\Services\Allocations\AllocationDeletionService;
|
use Pterodactyl\Services\Allocations\AllocationDeletionService;
|
||||||
use Pterodactyl\Exceptions\Http\QueryValueOutOfRangeHttpException;
|
use Pterodactyl\Exceptions\Http\QueryValueOutOfRangeHttpException;
|
||||||
|
@ -46,7 +48,16 @@ class AllocationController extends ApplicationApiController
|
||||||
}
|
}
|
||||||
|
|
||||||
$allocations = QueryBuilder::for(Allocation::query()->where('node_id', '=', $node->id))
|
$allocations = QueryBuilder::for(Allocation::query()->where('node_id', '=', $node->id))
|
||||||
->allowedFilters(['id', 'ip', 'port', 'alias', 'server_id'])
|
->allowedFilters([
|
||||||
|
'id', 'ip', 'port', 'alias',
|
||||||
|
AllowedFilter::callback('server_id', function (Builder $query, $value) {
|
||||||
|
if ($value === '0') {
|
||||||
|
$query->whereNull('server_id');
|
||||||
|
} else {
|
||||||
|
$query->where('server_id', '=', $value);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
])
|
||||||
->allowedSorts(['id', 'ip', 'port', 'server_id'])
|
->allowedSorts(['id', 'ip', 'port', 'server_id'])
|
||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,12 @@ class UpdateServerRequest extends ApplicationApiRequest
|
||||||
'databases' => $rules['database_limit'],
|
'databases' => $rules['database_limit'],
|
||||||
'allocations' => $rules['allocation_limit'],
|
'allocations' => $rules['allocation_limit'],
|
||||||
'backups' => $rules['backup_limit'],
|
'backups' => $rules['backup_limit'],
|
||||||
|
|
||||||
|
'allocation_id' => 'bail|exists:allocations,id',
|
||||||
|
'add_allocations' => 'bail|array',
|
||||||
|
'add_allocations.*' => 'integer',
|
||||||
|
'remove_allocations' => 'bail|array',
|
||||||
|
'remove_allocations.*' => 'integer',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +58,10 @@ class UpdateServerRequest extends ApplicationApiRequest
|
||||||
'database_limit' => array_get($data, 'databases'),
|
'database_limit' => array_get($data, 'databases'),
|
||||||
'allocation_limit' => array_get($data, 'allocations'),
|
'allocation_limit' => array_get($data, 'allocations'),
|
||||||
'backup_limit' => array_get($data, 'backups'),
|
'backup_limit' => array_get($data, 'backups'),
|
||||||
|
|
||||||
|
'allocation_id' => array_get($data, 'allocation_id'),
|
||||||
|
'add_allocations' => array_get($data, 'add_allocations'),
|
||||||
|
'remove_allocations' => array_get($data, 'remove_allocations'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import http from '@/api/http';
|
import http from '@/api/http';
|
||||||
import { Allocation, rawDataToAllocation } from '@/api/admin/nodes/allocations/getAllocations';
|
import { Allocation, rawDataToAllocation } from '@/api/admin/nodes/getAllocations';
|
||||||
|
|
||||||
export interface Values {
|
export interface Values {
|
||||||
ip: string;
|
ip: string;
|
||||||
|
|
|
@ -1,35 +1,9 @@
|
||||||
import { Server, rawDataToServer } from '@/api/admin/servers/getServers';
|
import { Allocation, rawDataToAllocation } from '@/api/admin/nodes/getAllocations';
|
||||||
import http, { FractalResponseData, getPaginationSet, PaginatedResult } from '@/api/http';
|
import http, { getPaginationSet, PaginatedResult } from '@/api/http';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { createContext } from '@/api/admin';
|
import { createContext } from '@/api/admin';
|
||||||
|
|
||||||
export interface Allocation {
|
|
||||||
id: number;
|
|
||||||
ip: string;
|
|
||||||
port: number;
|
|
||||||
alias: string | null;
|
|
||||||
serverId: number | null;
|
|
||||||
assigned: boolean;
|
|
||||||
|
|
||||||
relations: {
|
|
||||||
server?: Server;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const rawDataToAllocation = ({ attributes }: FractalResponseData): Allocation => ({
|
|
||||||
id: attributes.id,
|
|
||||||
ip: attributes.ip,
|
|
||||||
port: attributes.port,
|
|
||||||
alias: attributes.ip_alias || null,
|
|
||||||
serverId: attributes.server_id,
|
|
||||||
assigned: attributes.assigned,
|
|
||||||
|
|
||||||
relations: {
|
|
||||||
server: attributes.relationships?.server?.object === 'server' ? rawDataToServer(attributes.relationships.server as FractalResponseData) : undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export interface Filters {
|
export interface Filters {
|
||||||
id?: string;
|
id?: string;
|
||||||
ip?: string;
|
ip?: string;
|
||||||
|
|
|
@ -1,26 +1,50 @@
|
||||||
import http, { FractalResponseData } from '@/api/http';
|
import http, { FractalResponseData } from '@/api/http';
|
||||||
|
import { rawDataToServer, Server } from '@/api/admin/servers/getServers';
|
||||||
|
|
||||||
export interface Allocation {
|
export interface Allocation {
|
||||||
id: number;
|
id: number;
|
||||||
ip: string;
|
ip: string;
|
||||||
alias: string | null;
|
|
||||||
port: number;
|
port: number;
|
||||||
notes: string | null;
|
alias: string | null;
|
||||||
|
serverId: number | null;
|
||||||
assigned: boolean;
|
assigned: boolean;
|
||||||
|
|
||||||
|
relations: {
|
||||||
|
server?: Server;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rawDataToAllocation = (data: FractalResponseData): Allocation => ({
|
export const rawDataToAllocation = ({ attributes }: FractalResponseData): Allocation => ({
|
||||||
id: data.attributes.id,
|
id: attributes.id,
|
||||||
ip: data.attributes.ip,
|
ip: attributes.ip,
|
||||||
alias: data.attributes.ip_alias,
|
port: attributes.port,
|
||||||
port: data.attributes.port,
|
alias: attributes.ip_alias || null,
|
||||||
notes: data.attributes.notes,
|
serverId: attributes.server_id,
|
||||||
assigned: data.attributes.assigned,
|
assigned: attributes.assigned,
|
||||||
|
|
||||||
|
relations: {
|
||||||
|
server: attributes.relationships?.server?.object === 'server' ? rawDataToServer(attributes.relationships.server as FractalResponseData) : undefined,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default (id: string | number): Promise<Allocation[]> => {
|
export interface Filters {
|
||||||
|
ip?: string
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
server_id?: string;
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (id: string | number, filters: Filters = {}, include: string[] = []): Promise<Allocation[]> => {
|
||||||
|
const params = {};
|
||||||
|
if (filters !== null) {
|
||||||
|
Object.keys(filters).forEach(key => {
|
||||||
|
// @ts-ignore
|
||||||
|
params['filter[' + key + ']'] = filters[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.get(`/api/application/nodes/${id}/allocations`)
|
http.get(`/api/application/nodes/${id}/allocations`, { params: { include: include.join(','), ...params } })
|
||||||
.then(({ data }) => resolve((data.data || []).map(rawDataToAllocation)))
|
.then(({ data }) => resolve((data.data || []).map(rawDataToAllocation)))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
import { Allocation, rawDataToAllocation } from '@/api/admin/nodes/getAllocations';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { createContext } from '@/api/admin';
|
import { createContext } from '@/api/admin';
|
||||||
import http, { FractalResponseData, getPaginationSet, PaginatedResult } from '@/api/http';
|
import http, { FractalResponseData, FractalResponseList, getPaginationSet, PaginatedResult } from '@/api/http';
|
||||||
import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg';
|
import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg';
|
||||||
import { Node, rawDataToNode } from '@/api/admin/nodes/getNodes';
|
import { Node, rawDataToNode } from '@/api/admin/nodes/getNodes';
|
||||||
import { User, rawDataToUser } from '@/api/admin/users/getUsers';
|
import { User, rawDataToUser } from '@/api/admin/users/getUsers';
|
||||||
|
@ -48,6 +49,7 @@ export interface Server {
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|
||||||
relations: {
|
relations: {
|
||||||
|
allocations?: Allocation[];
|
||||||
egg?: Egg;
|
egg?: Egg;
|
||||||
node?: Node;
|
node?: Node;
|
||||||
user?: User;
|
user?: User;
|
||||||
|
@ -96,6 +98,7 @@ export const rawDataToServer = ({ attributes }: FractalResponseData): Server =>
|
||||||
updatedAt: new Date(attributes.updated_at),
|
updatedAt: new Date(attributes.updated_at),
|
||||||
|
|
||||||
relations: {
|
relations: {
|
||||||
|
allocations: ((attributes.relationships?.allocations as FractalResponseList | undefined)?.data || []).map(rawDataToAllocation),
|
||||||
egg: attributes.relationships?.egg?.object === 'egg' ? rawDataToEgg(attributes.relationships.egg as FractalResponseData) : undefined,
|
egg: attributes.relationships?.egg?.object === 'egg' ? rawDataToEgg(attributes.relationships.egg as FractalResponseData) : undefined,
|
||||||
node: attributes.relationships?.node?.object === 'node' ? rawDataToNode(attributes.relationships.node as FractalResponseData) : undefined,
|
node: attributes.relationships?.node?.object === 'node' ? rawDataToNode(attributes.relationships.node as FractalResponseData) : undefined,
|
||||||
user: attributes.relationships?.user?.object === 'user' ? rawDataToUser(attributes.relationships.user as FractalResponseData) : undefined,
|
user: attributes.relationships?.user?.object === 'user' ? rawDataToUser(attributes.relationships.user as FractalResponseData) : undefined,
|
||||||
|
|
|
@ -17,6 +17,10 @@ export interface Values {
|
||||||
databases: number;
|
databases: number;
|
||||||
allocations: number;
|
allocations: number;
|
||||||
backups: number;
|
backups: number;
|
||||||
|
|
||||||
|
allocationId: number;
|
||||||
|
addAllocations: number[];
|
||||||
|
removeAllocations: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (id: number, server: Partial<Values>, include: string[] = []): Promise<Server> => {
|
export default (id: number, server: Partial<Values>, include: string[] = []): Promise<Server> => {
|
||||||
|
|
|
@ -40,7 +40,7 @@ const ServerRouter = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearFlashes('server');
|
clearFlashes('server');
|
||||||
|
|
||||||
getServer(Number(match.params?.id), [ 'user' ])
|
getServer(Number(match.params?.id), [ 'allocations', 'user' ])
|
||||||
.then(server => setServer(server))
|
.then(server => setServer(server))
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
|
import getAllocations from '@/api/admin/nodes/getAllocations';
|
||||||
import { Server } from '@/api/admin/servers/getServers';
|
import { Server } from '@/api/admin/servers/getServers';
|
||||||
import ServerDeleteButton from '@/components/admin/servers/ServerDeleteButton';
|
import ServerDeleteButton from '@/components/admin/servers/ServerDeleteButton';
|
||||||
|
import Label from '@/components/elements/Label';
|
||||||
|
import Select from '@/components/elements/Select';
|
||||||
|
import SelectField, { AsyncSelectField, Option } from '@/components/elements/SelectField';
|
||||||
import { faBalanceScale, faCogs, faConciergeBell, faNetworkWired } from '@fortawesome/free-solid-svg-icons';
|
import { faBalanceScale, faCogs, faConciergeBell, faNetworkWired } from '@fortawesome/free-solid-svg-icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import AdminBox from '@/components/admin/AdminBox';
|
import AdminBox from '@/components/admin/AdminBox';
|
||||||
|
@ -63,7 +67,7 @@ export function ServerResourceContainer () {
|
||||||
const { isSubmitting } = useFormikContext();
|
const { isSubmitting } = useFormikContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminBox icon={faBalanceScale} title={'Resource Management'} css={tw`relative w-full`}>
|
<AdminBox icon={faBalanceScale} title={'Resources'} css={tw`relative w-full`}>
|
||||||
<SpinnerOverlay visible={isSubmitting}/>
|
<SpinnerOverlay visible={isSubmitting}/>
|
||||||
|
|
||||||
<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-row`}>
|
||||||
|
@ -72,8 +76,8 @@ export function ServerResourceContainer () {
|
||||||
id={'cpu'}
|
id={'cpu'}
|
||||||
name={'cpu'}
|
name={'cpu'}
|
||||||
label={'CPU Limit'}
|
label={'CPU Limit'}
|
||||||
type={'string'}
|
type={'text'}
|
||||||
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.'}
|
description={'Each thread on the system is considered to be 100%. Setting this value to 0 will allow the server to use CPU time without restriction.'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -82,8 +86,8 @@ export function ServerResourceContainer () {
|
||||||
id={'threads'}
|
id={'threads'}
|
||||||
name={'threads'}
|
name={'threads'}
|
||||||
label={'CPU Pinning'}
|
label={'CPU Pinning'}
|
||||||
type={'string'}
|
type={'text'}
|
||||||
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.'}
|
description={'Advanced: Enter the specific CPU cores that this server can run on, or leave blank to allow all cores. This can be a single number, and or a comma seperated list, and or a dashed range. Example: 0, 0-1,3, or 0,1,3,4. It is recommended to leave this value blank and let the CPU handle balancing the load.'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -132,11 +136,11 @@ export function ServerResourceContainer () {
|
||||||
</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-row`}>
|
||||||
<div css={tw`mt-6 bg-neutral-800 border border-neutral-900 shadow-inner p-4 rounded`}>
|
<div css={tw`bg-neutral-800 border border-neutral-900 shadow-inner p-4 rounded`}>
|
||||||
<FormikSwitch
|
<FormikSwitch
|
||||||
name={'oomKiller'}
|
name={'oomKiller'}
|
||||||
label={'Out of Memory Killer'}
|
label={'Out of Memory Killer'}
|
||||||
description={'Enabling OOM killer may cause server processes to exit unexpectedly.'}
|
description={'Enabling the Out of Memory Killer may cause server processes to exit unexpectedly.'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -157,7 +161,7 @@ export function ServerSettingsContainer ({ server }: { server?: Server }) {
|
||||||
id={'name'}
|
id={'name'}
|
||||||
name={'name'}
|
name={'name'}
|
||||||
label={'Server Name'}
|
label={'Server Name'}
|
||||||
type={'string'}
|
type={'text'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -166,7 +170,7 @@ export function ServerSettingsContainer ({ server }: { server?: Server }) {
|
||||||
id={'externalId'}
|
id={'externalId'}
|
||||||
name={'externalId'}
|
name={'externalId'}
|
||||||
label={'External Identifier'}
|
label={'External Identifier'}
|
||||||
type={'number'}
|
type={'text'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -180,12 +184,52 @@ export function ServerSettingsContainer ({ server }: { server?: Server }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ServerAllocationsContainer () {
|
export function ServerAllocationsContainer ({ server }: { server: Server }) {
|
||||||
const { isSubmitting } = useFormikContext();
|
const { isSubmitting } = useFormikContext();
|
||||||
|
|
||||||
|
const loadOptions = async (inputValue: string, callback: (options: Option[]) => void) => {
|
||||||
|
const allocations = await getAllocations(server.nodeId, { ip: inputValue, server_id: '0' });
|
||||||
|
callback(allocations.map(a => {
|
||||||
|
return { value: a.id.toString(), label: a.ip + ':' + a.port };
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminBox icon={faNetworkWired} title={'Allocations'} css={tw`relative w-full`}>
|
<AdminBox icon={faNetworkWired} title={'Networking'} css={tw`relative w-full`}>
|
||||||
<SpinnerOverlay visible={isSubmitting}/>
|
<SpinnerOverlay visible={isSubmitting}/>
|
||||||
|
|
||||||
|
<div css={tw`mb-6`}>
|
||||||
|
<Label>Primary Allocation</Label>
|
||||||
|
<Select
|
||||||
|
id={'allocationId'}
|
||||||
|
name={'allocationId'}
|
||||||
|
>
|
||||||
|
{server.relations?.allocations?.map(a => (
|
||||||
|
<option key={a.id} value={a.id}>{a.ip}:{a.port}</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AsyncSelectField
|
||||||
|
id={'addAllocations'}
|
||||||
|
name={'addAllocations'}
|
||||||
|
label={'Add Allocations'}
|
||||||
|
loadOptions={loadOptions}
|
||||||
|
isMulti
|
||||||
|
css={tw`mb-6`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectField
|
||||||
|
id={'removeAllocations'}
|
||||||
|
name={'removeAllocations'}
|
||||||
|
label={'Remove Allocations'}
|
||||||
|
options={server.relations?.allocations?.map(a => {
|
||||||
|
return { value: a.id.toString(), label: a.ip + ':' + a.port };
|
||||||
|
}) || []}
|
||||||
|
isMulti
|
||||||
|
isSearchable
|
||||||
|
css={tw`mb-2`}
|
||||||
|
/>
|
||||||
</AdminBox>
|
</AdminBox>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -206,11 +250,17 @@ export default function ServerSettingsContainer2 () {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const submit = (values: Values2, { setSubmitting }: FormikHelpers<Values2>) => {
|
const submit = (values: Values2, { setSubmitting, setFieldValue }: FormikHelpers<Values2>) => {
|
||||||
clearFlashes('server');
|
clearFlashes('server');
|
||||||
|
|
||||||
updateServer(server.id, { ...values, oomDisabled: !values.oomKiller })
|
updateServer(server.id, { ...values, oomDisabled: !values.oomKiller }, [ 'allocations', 'user' ])
|
||||||
.then(() => setServer({ ...server, ...values }))
|
.then(s => {
|
||||||
|
setServer({ ...server, ...s });
|
||||||
|
|
||||||
|
// TODO: Figure out how to properly clear react-selects for allocations.
|
||||||
|
setFieldValue('addAllocations', []);
|
||||||
|
setFieldValue('removeAllocations', []);
|
||||||
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
clearAndAddHttpError({ key: 'server', error });
|
clearAndAddHttpError({ key: 'server', error });
|
||||||
|
@ -239,6 +289,10 @@ export default function ServerSettingsContainer2 () {
|
||||||
databases: server.featureLimits.databases,
|
databases: server.featureLimits.databases,
|
||||||
allocations: server.featureLimits.allocations,
|
allocations: server.featureLimits.allocations,
|
||||||
backups: server.featureLimits.backups,
|
backups: server.featureLimits.backups,
|
||||||
|
|
||||||
|
allocationId: server.allocationId,
|
||||||
|
addAllocations: [] as number[],
|
||||||
|
removeAllocations: [] as number[],
|
||||||
}}
|
}}
|
||||||
validationSchema={object().shape({
|
validationSchema={object().shape({
|
||||||
})}
|
})}
|
||||||
|
@ -256,7 +310,7 @@ export default function ServerSettingsContainer2 () {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div css={tw`flex mb-6`}>
|
<div css={tw`flex mb-6`}>
|
||||||
<ServerAllocationsContainer/>
|
<ServerAllocationsContainer server={server}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div css={tw`bg-neutral-700 rounded shadow-md py-2 px-6`}>
|
<div css={tw`bg-neutral-700 rounded shadow-md py-2 px-6`}>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { Egg } from '@/api/admin/eggs/getEgg';
|
|
||||||
import EggSelect from '@/components/admin/servers/EggSelect';
|
import EggSelect from '@/components/admin/servers/EggSelect';
|
||||||
import NestSelect from '@/components/admin/servers/NestSelect';
|
import NestSelect from '@/components/admin/servers/NestSelect';
|
||||||
import FormikSwitch from '@/components/elements/FormikSwitch';
|
import FormikSwitch from '@/components/elements/FormikSwitch';
|
||||||
|
@ -107,7 +106,7 @@ function ServerImageContainer () {
|
||||||
export default function ServerStartupContainer () {
|
export default function ServerStartupContainer () {
|
||||||
const { clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
const { clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||||
|
|
||||||
const [ egg, setEgg ] = useState<Egg | null>(null);
|
// const [ egg, setEgg ] = useState<Egg | null>(null);
|
||||||
|
|
||||||
const server = Context.useStoreState(state => state.server);
|
const server = Context.useStoreState(state => state.server);
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { Field as FormikField, FieldProps } from 'formik';
|
||||||
import Input from '@/components/elements/Input';
|
import Input from '@/components/elements/Input';
|
||||||
import Label from '@/components/elements/Label';
|
import Label from '@/components/elements/Label';
|
||||||
import InputError from '@/components/elements/InputError';
|
import InputError from '@/components/elements/InputError';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -20,7 +21,10 @@ const Field = forwardRef<HTMLInputElement, Props>(({ id, name, light = false, la
|
||||||
({ field, form: { errors, touched } }: FieldProps) => (
|
({ field, form: { errors, touched } }: FieldProps) => (
|
||||||
<div>
|
<div>
|
||||||
{label &&
|
{label &&
|
||||||
|
<div css={tw`flex flex-row`} title={description}>
|
||||||
<Label htmlFor={id} isLight={light}>{label}</Label>
|
<Label htmlFor={id} isLight={light}>{label}</Label>
|
||||||
|
{/*{description && <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" css={tw`h-4 w-4 ml-1 cursor-pointer`}><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>}*/}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
<Input
|
<Input
|
||||||
id={id}
|
id={id}
|
||||||
|
|
|
@ -5,7 +5,7 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Select = styled.select<Props>`
|
const Select = styled.select<Props>`
|
||||||
${tw`shadow-none block p-3 pr-8 rounded border w-full text-sm transition-colors duration-150 ease-linear`};
|
${tw`shadow-none block p-3 pr-8 rounded border-2 w-full text-sm transition-colors duration-150 ease-linear`};
|
||||||
|
|
||||||
&, &:hover:not(:disabled), &:focus {
|
&, &:hover:not(:disabled), &:focus {
|
||||||
${tw`outline-none`};
|
${tw`outline-none`};
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { CSSObject } from '@emotion/serialize';
|
||||||
import { Field as FormikField, FieldProps } from 'formik';
|
import { Field as FormikField, FieldProps } from 'formik';
|
||||||
import React, { forwardRef } from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
import Select, { ContainerProps, ControlProps, GroupProps, IndicatorContainerProps, IndicatorProps, InputProps, MenuListComponentProps, MenuProps, MultiValueProps, OptionProps, PlaceholderProps, SingleValueProps, StylesConfig, ValueContainerProps } from 'react-select';
|
import Select, { ContainerProps, ControlProps, GroupProps, IndicatorContainerProps, IndicatorProps, InputProps, MenuListComponentProps, MenuProps, MultiValueProps, OptionProps, PlaceholderProps, SingleValueProps, StylesConfig, ValueContainerProps } from 'react-select';
|
||||||
|
import Async from 'react-select/async';
|
||||||
import Creatable from 'react-select/creatable';
|
import Creatable from 'react-select/creatable';
|
||||||
import tw, { theme } from 'twin.macro';
|
import tw, { theme } from 'twin.macro';
|
||||||
import Label from '@/components/elements/Label';
|
import Label from '@/components/elements/Label';
|
||||||
|
@ -219,7 +220,7 @@ export interface Option {
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface SelectFieldProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
@ -242,7 +243,8 @@ interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SelectField = forwardRef<HTMLElement, Props>(({ id, name, label, description, validate, className, isMulti, isCreatable, ...props }, ref) => {
|
const SelectField = forwardRef<HTMLElement, SelectFieldProps>(
|
||||||
|
function Select2 ({ id, name, label, description, validate, className, isMulti, isCreatable, ...props }, ref) {
|
||||||
const { options } = props;
|
const { options } = props;
|
||||||
|
|
||||||
const onChange = (options: Option | Option[], name: string, setFieldValue: (field: string, value: any, shouldValidate?: boolean) => void) => {
|
const onChange = (options: Option | Option[], name: string, setFieldValue: (field: string, value: any, shouldValidate?: boolean) => void) => {
|
||||||
|
@ -256,12 +258,9 @@ const SelectField = forwardRef<HTMLElement, Props>(({ id, name, label, descripti
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormikField innerRef={ref} name={name} validate={validate}>
|
<FormikField innerRef={ref} name={name} validate={validate}>
|
||||||
{
|
{({ field, form: { errors, touched, setFieldValue } }: FieldProps) => (
|
||||||
({ field, form: { errors, touched, setFieldValue } }: FieldProps) => (
|
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{label &&
|
{label && <Label htmlFor={id}>{label}</Label>}
|
||||||
<Label htmlFor={id}>{label}</Label>
|
|
||||||
}
|
|
||||||
{isCreatable ?
|
{isCreatable ?
|
||||||
<Creatable
|
<Creatable
|
||||||
{...field}
|
{...field}
|
||||||
|
@ -291,11 +290,64 @@ const SelectField = forwardRef<HTMLElement, Props>(({ id, name, label, descripti
|
||||||
description ? <p css={tw`text-neutral-400 text-xs mt-1`}>{description}</p> : null
|
description ? <p css={tw`text-neutral-400 text-xs mt-1`}>{description}</p> : null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
</FormikField>
|
</FormikField>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
SelectField.displayName = 'SelectField';
|
);
|
||||||
|
|
||||||
|
interface AsyncSelectFieldProps {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
label?: string;
|
||||||
|
description?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
validate?: (value: any) => undefined | string | Promise<any>;
|
||||||
|
|
||||||
|
isMulti?: boolean;
|
||||||
|
|
||||||
|
className?: string;
|
||||||
|
|
||||||
|
loadOptions(inputValue: string, callback: (options: Array<Option>) => void): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AsyncSelectField = forwardRef<HTMLElement, AsyncSelectFieldProps>(
|
||||||
|
function AsyncSelect2 ({ id, name, label, description, validate, className, isMulti, ...props }, ref) {
|
||||||
|
const onChange = (options: Option | Option[], name: string, setFieldValue: (field: string, value: any, shouldValidate?: boolean) => void) => {
|
||||||
|
if (isMulti) {
|
||||||
|
setFieldValue(name, (options as Option[]).map(o => Number(o.value)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFieldValue(name, Number((options as Option).value));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormikField innerRef={ref} name={name} validate={validate}>
|
||||||
|
{({ field, form: { errors, touched, setFieldValue } }: FieldProps) => (
|
||||||
|
<div className={className}>
|
||||||
|
{label && <Label htmlFor={id}>{label}</Label>}
|
||||||
|
<Async
|
||||||
|
{...props}
|
||||||
|
id={id}
|
||||||
|
name={name}
|
||||||
|
styles={SelectStyle}
|
||||||
|
onChange={o => onChange(o, name, setFieldValue)}
|
||||||
|
isMulti={isMulti}
|
||||||
|
/>
|
||||||
|
{touched[field.name] && errors[field.name] ?
|
||||||
|
<p css={tw`text-red-200 text-xs mt-1`}>
|
||||||
|
{(errors[field.name] as string).charAt(0).toUpperCase() + (errors[field.name] as string).slice(1)}
|
||||||
|
</p>
|
||||||
|
:
|
||||||
|
description ? <p css={tw`text-neutral-400 text-xs mt-1`}>{description}</p> : null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FormikField>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default SelectField;
|
export default SelectField;
|
||||||
|
export { AsyncSelectField };
|
||||||
|
|
Loading…
Reference in New Issue