Cleanup typing for server and expose more useful endpoint and transformer logic
This commit is contained in:
parent
3afd8b9f03
commit
00d0f49ede
11 changed files with 257 additions and 90 deletions
|
@ -43,6 +43,10 @@ rules:
|
||||||
array-bracket-spacing:
|
array-bracket-spacing:
|
||||||
- warn
|
- warn
|
||||||
- always
|
- always
|
||||||
|
"@typescript-eslint/no-unused-vars":
|
||||||
|
- warn
|
||||||
|
- argsIgnorePattern: '^_'
|
||||||
|
varsIgnorePattern: '^_'
|
||||||
# Remove errors for not having newlines between operands of ternary expressions https://eslint.org/docs/rules/multiline-ternary
|
# Remove errors for not having newlines between operands of ternary expressions https://eslint.org/docs/rules/multiline-ternary
|
||||||
multiline-ternary: 0
|
multiline-ternary: 0
|
||||||
"react-hooks/rules-of-hooks":
|
"react-hooks/rules-of-hooks":
|
||||||
|
|
|
@ -1,5 +1,30 @@
|
||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
export interface Model {
|
||||||
|
relationships: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UUID = string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the provided relationships keys as present in the given model
|
||||||
|
* rather than being optional to improve typing responses.
|
||||||
|
*/
|
||||||
|
export type WithRelationships<M extends Model, R extends string> = Omit<M, 'relationships'> & {
|
||||||
|
relationships: Omit<M['relationships'], keyof R> & {
|
||||||
|
[K in R]: NonNullable<M['relationships'][K]>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function that just returns the model you pass in, but types the model
|
||||||
|
* such that TypeScript understands the relationships on it. This is just to help
|
||||||
|
* reduce the amount of duplicated type casting all over the codebase.
|
||||||
|
*/
|
||||||
|
export const withRelationships = <M extends Model, R extends string> (model: M, ..._keys: R[]) => {
|
||||||
|
return model as unknown as WithRelationships<M, R>;
|
||||||
|
};
|
||||||
|
|
||||||
export interface ListContext<T> {
|
export interface ListContext<T> {
|
||||||
page: number;
|
page: number;
|
||||||
setPage: (page: ((p: number) => number) | number) => void;
|
setPage: (page: ((p: number) => number) | number) => void;
|
||||||
|
|
95
resources/scripts/api/admin/server.ts
Normal file
95
resources/scripts/api/admin/server.ts
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
import { Allocation } from '@/api/admin/nodes/getAllocations';
|
||||||
|
import { Egg } from '@/api/admin/eggs/getEgg';
|
||||||
|
import { User } from '@/api/admin/users/getUsers';
|
||||||
|
import { Node } from '@/api/admin/nodes/getNodes';
|
||||||
|
import { rawDataToServer, ServerVariable } from '@/api/admin/servers/getServers';
|
||||||
|
import useSWR, { SWRResponse } from 'swr';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { useRouteMatch } from 'react-router-dom';
|
||||||
|
import http from '@/api/http';
|
||||||
|
import { Model, UUID, withRelationships, WithRelationships } from '@/api/admin/index';
|
||||||
|
import { AdminTransformers } from '@/api/admin/transformers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the limits for a server that exists on the Panel.
|
||||||
|
*/
|
||||||
|
interface ServerLimits {
|
||||||
|
memory: number;
|
||||||
|
swap: number;
|
||||||
|
disk: number;
|
||||||
|
io: number;
|
||||||
|
cpu: number;
|
||||||
|
threads: string | null;
|
||||||
|
oomDisabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a single server instance that is returned from the Panel's admin
|
||||||
|
* API endpoints.
|
||||||
|
*/
|
||||||
|
export interface Server extends Model {
|
||||||
|
id: number;
|
||||||
|
uuid: UUID;
|
||||||
|
externalId: string | null;
|
||||||
|
identifier: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
status: string;
|
||||||
|
userId: number;
|
||||||
|
nodeId: number;
|
||||||
|
allocationId: number;
|
||||||
|
eggId: number;
|
||||||
|
limits: ServerLimits;
|
||||||
|
featureLimits: {
|
||||||
|
databases: number;
|
||||||
|
allocations: number;
|
||||||
|
backups: number;
|
||||||
|
};
|
||||||
|
container: {
|
||||||
|
startup: string;
|
||||||
|
image: string;
|
||||||
|
environment: Record<string, string>;
|
||||||
|
};
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
relationships: {
|
||||||
|
allocations?: Allocation[];
|
||||||
|
egg?: Egg;
|
||||||
|
node?: Node;
|
||||||
|
user?: User;
|
||||||
|
variables?: ServerVariable[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A standard API response with the minimum viable details for the frontend
|
||||||
|
* to correctly render a server.
|
||||||
|
*/
|
||||||
|
type LoadedServer = WithRelationships<Server, 'allocations' | 'user' | 'node'>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a server from the API and ensures that the allocations, user, and
|
||||||
|
* node data is loaded.
|
||||||
|
*/
|
||||||
|
export const getServer = async (id: number | string): Promise<LoadedServer> => {
|
||||||
|
const { data } = await http.get(`/api/application/servers/${id}`, {
|
||||||
|
params: {
|
||||||
|
includes: [ 'allocations', 'user', 'node' ],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return withRelationships(AdminTransformers.toServer(data), 'allocations', 'user', 'node');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an SWR instance by automatically loading in the server for the currently
|
||||||
|
* loaded route match in the admin area.
|
||||||
|
*/
|
||||||
|
export const useServerFromRoute = (): SWRResponse<LoadedServer, AxiosError> => {
|
||||||
|
const { params } = useRouteMatch<{ id: string }>();
|
||||||
|
|
||||||
|
return useSWR(`/api/application/servers/${params.id}`, async () => getServer(params.id), {
|
||||||
|
revalidateOnMount: false,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
});
|
||||||
|
};
|
|
@ -23,7 +23,7 @@ export interface ServerVariable {
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawDataToServerVariable = ({ attributes }: FractalResponseData): ServerVariable => ({
|
export const rawDataToServerVariable = ({ attributes }: FractalResponseData): ServerVariable => ({
|
||||||
id: attributes.id,
|
id: attributes.id,
|
||||||
eggId: attributes.egg_id,
|
eggId: attributes.egg_id,
|
||||||
name: attributes.name,
|
name: attributes.name,
|
||||||
|
|
56
resources/scripts/api/admin/transformers.ts
Normal file
56
resources/scripts/api/admin/transformers.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
import { Server } from '@/api/admin/server';
|
||||||
|
import { FractalResponseData, FractalResponseList } from '@/api/http';
|
||||||
|
import { rawDataToAllocation } from '@/api/admin/nodes/getAllocations';
|
||||||
|
import { rawDataToEgg } from '@/api/admin/eggs/getEgg';
|
||||||
|
import { rawDataToNode } from '@/api/admin/nodes/getNodes';
|
||||||
|
import { rawDataToUser } from '@/api/admin/users/getUsers';
|
||||||
|
import { rawDataToServerVariable } from '@/api/admin/servers/getServers';
|
||||||
|
|
||||||
|
const isList = (data: FractalResponseList | FractalResponseData): data is FractalResponseList => data.object === 'list';
|
||||||
|
|
||||||
|
function transform<T, M = undefined> (data: undefined, transformer: (callback: FractalResponseData) => T, missing?: M): undefined;
|
||||||
|
function transform<T, M> (data: FractalResponseData | undefined, transformer: (callback: FractalResponseData) => T, missing?: M): T | M | undefined;
|
||||||
|
function transform<T, M> (data: FractalResponseList | undefined, transformer: (callback: FractalResponseData) => T, missing?: M): T[] | undefined;
|
||||||
|
function transform<T> (data: FractalResponseData | FractalResponseList | undefined, transformer: (callback: FractalResponseData) => T, missing = undefined) {
|
||||||
|
if (data === undefined) return undefined;
|
||||||
|
|
||||||
|
if (isList(data)) {
|
||||||
|
return data.data.map(transformer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !data ? missing : transformer(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AdminTransformers {
|
||||||
|
static toServer = ({ attributes }: FractalResponseData): Server => {
|
||||||
|
const { oom_disabled, ...limits } = attributes.limits;
|
||||||
|
const { allocations, egg, node, user, variables } = attributes.relationships || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: attributes.id,
|
||||||
|
uuid: attributes.uuid,
|
||||||
|
externalId: attributes.external_id,
|
||||||
|
identifier: attributes.identifier,
|
||||||
|
name: attributes.name,
|
||||||
|
description: attributes.description,
|
||||||
|
status: attributes.status,
|
||||||
|
userId: attributes.owner_id,
|
||||||
|
nodeId: attributes.node_id,
|
||||||
|
allocationId: attributes.allocation_id,
|
||||||
|
eggId: attributes.egg_id,
|
||||||
|
limits: { ...limits, oomDisabled: oom_disabled },
|
||||||
|
featureLimits: attributes.feature_limits,
|
||||||
|
container: attributes.container,
|
||||||
|
createdAt: new Date(attributes.created_at),
|
||||||
|
updatedAt: new Date(attributes.updated_at),
|
||||||
|
relationships: {
|
||||||
|
allocations: transform(allocations as FractalResponseList | undefined, rawDataToAllocation),
|
||||||
|
egg: transform(egg as FractalResponseData | undefined, rawDataToEgg),
|
||||||
|
node: transform(node as FractalResponseData | undefined, rawDataToNode),
|
||||||
|
user: transform(user as FractalResponseData | undefined, rawDataToUser),
|
||||||
|
variables: transform(variables as FractalResponseList | undefined, rawDataToServerVariable),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,19 +0,0 @@
|
||||||
import useSWR, { SWRResponse } from 'swr';
|
|
||||||
import http from '@/api/http';
|
|
||||||
import { rawDataToServer, Server } from '@/api/admin/servers/getServers';
|
|
||||||
import { useRouteMatch } from 'react-router-dom';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
|
|
||||||
export default (): SWRResponse<Server, AxiosError> => {
|
|
||||||
const { params } = useRouteMatch<{ id: string }>();
|
|
||||||
|
|
||||||
return useSWR(`/api/application/servers/${params.id}`, async (key) => {
|
|
||||||
const { data } = await http.get(key, {
|
|
||||||
params: {
|
|
||||||
includes: [ 'allocations', 'user', 'variables' ],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return rawDataToServer(data);
|
|
||||||
}, { revalidateOnMount: false, revalidateOnFocus: false });
|
|
||||||
};
|
|
|
@ -11,8 +11,8 @@ import Spinner from '@/components/elements/Spinner';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
import { SubNavigation, SubNavigationLink } from '@/components/admin/SubNavigation';
|
import { SubNavigation, SubNavigationLink } from '@/components/admin/SubNavigation';
|
||||||
import ServerSettingsContainer from '@/components/admin/servers/ServerSettingsContainer';
|
import ServerSettingsContainer from '@/components/admin/servers/ServerSettingsContainer';
|
||||||
import getServerDetails from '@/api/swr/admin/getServerDetails';
|
|
||||||
import useFlash from '@/plugins/useFlash';
|
import useFlash from '@/plugins/useFlash';
|
||||||
|
import { useServerFromRoute } from '@/api/admin/server';
|
||||||
|
|
||||||
export const ServerIncludes = [ 'allocations', 'user', 'variables' ];
|
export const ServerIncludes = [ 'allocations', 'user', 'variables' ];
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ const ServerRouter = () => {
|
||||||
const match = useRouteMatch<{ id?: string }>();
|
const match = useRouteMatch<{ id?: string }>();
|
||||||
|
|
||||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||||
const { data: server, error, isValidating, mutate } = getServerDetails();
|
const { data: server, error, isValidating, mutate } = useServerFromRoute();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mutate();
|
mutate();
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
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 { faBalanceScale } from '@fortawesome/free-solid-svg-icons';
|
||||||
import Select from '@/components/elements/Select';
|
|
||||||
import SelectField, { AsyncSelectField, Option } from '@/components/elements/SelectField';
|
|
||||||
import { faBalanceScale, 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';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
@ -21,56 +17,7 @@ import Button from '@/components/elements/Button';
|
||||||
import FormikSwitch from '@/components/elements/FormikSwitch';
|
import FormikSwitch from '@/components/elements/FormikSwitch';
|
||||||
import BaseSettingsBox from '@/components/admin/servers/settings/BaseSettingsBox';
|
import BaseSettingsBox from '@/components/admin/servers/settings/BaseSettingsBox';
|
||||||
import FeatureLimitsBox from '@/components/admin/servers/settings/FeatureLimitsBox';
|
import FeatureLimitsBox from '@/components/admin/servers/settings/FeatureLimitsBox';
|
||||||
|
import NetworkingBox from '@/components/admin/servers/settings/NetworkingBox';
|
||||||
export function ServerAllocationsContainer ({ server }: { server: Server }) {
|
|
||||||
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.getDisplayText() };
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AdminBox icon={faNetworkWired} title={'Networking'} css={tw`relative w-full`}>
|
|
||||||
<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.getDisplayText()}</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.getDisplayText() };
|
|
||||||
}) || []}
|
|
||||||
isMulti
|
|
||||||
isSearchable
|
|
||||||
css={tw`mb-2`}
|
|
||||||
/>
|
|
||||||
</AdminBox>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ServerResourceContainer () {
|
export function ServerResourceContainer () {
|
||||||
const { isSubmitting } = useFormikContext();
|
const { isSubmitting } = useFormikContext();
|
||||||
|
@ -160,7 +107,10 @@ export function ServerResourceContainer () {
|
||||||
export default function ServerSettingsContainer2 ({ server }: { server: Server }) {
|
export default function ServerSettingsContainer2 ({ server }: { server: Server }) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
const {
|
||||||
|
clearFlashes,
|
||||||
|
clearAndAddHttpError,
|
||||||
|
} = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||||
|
|
||||||
const setServer = Context.useStoreActions(actions => actions.setServer);
|
const setServer = Context.useStoreActions(actions => actions.setServer);
|
||||||
|
|
||||||
|
@ -216,8 +166,7 @@ export default function ServerSettingsContainer2 ({ server }: { server: Server }
|
||||||
addAllocations: [] as number[],
|
addAllocations: [] as number[],
|
||||||
removeAllocations: [] as number[],
|
removeAllocations: [] as number[],
|
||||||
}}
|
}}
|
||||||
validationSchema={object().shape({
|
validationSchema={object().shape({})}
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
{({ isSubmitting, isValid }) => (
|
{({ isSubmitting, isValid }) => (
|
||||||
<Form>
|
<Form>
|
||||||
|
@ -225,9 +174,8 @@ export default function ServerSettingsContainer2 ({ server }: { server: Server }
|
||||||
<div css={tw`grid grid-cols-1 gap-y-6`}>
|
<div css={tw`grid grid-cols-1 gap-y-6`}>
|
||||||
<BaseSettingsBox/>
|
<BaseSettingsBox/>
|
||||||
<FeatureLimitsBox/>
|
<FeatureLimitsBox/>
|
||||||
<ServerAllocationsContainer server={server}/>
|
<NetworkingBox/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div css={tw`flex flex-col`}>
|
<div css={tw`flex flex-col`}>
|
||||||
<ServerResourceContainer/>
|
<ServerResourceContainer/>
|
||||||
|
|
||||||
|
@ -237,7 +185,12 @@ export default function ServerSettingsContainer2 ({ server }: { server: Server }
|
||||||
serverId={server?.id}
|
serverId={server?.id}
|
||||||
onDeleted={() => history.push('/admin/servers')}
|
onDeleted={() => history.push('/admin/servers')}
|
||||||
/>
|
/>
|
||||||
<Button type="submit" size="small" css={tw`ml-auto`} disabled={isSubmitting || !isValid}>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="small"
|
||||||
|
css={tw`ml-auto`}
|
||||||
|
disabled={isSubmitting || !isValid}
|
||||||
|
>
|
||||||
Save Changes
|
Save Changes
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,10 +5,10 @@ import AdminBox from '@/components/admin/AdminBox';
|
||||||
import { faCogs } from '@fortawesome/free-solid-svg-icons';
|
import { faCogs } from '@fortawesome/free-solid-svg-icons';
|
||||||
import Field from '@/components/elements/Field';
|
import Field from '@/components/elements/Field';
|
||||||
import OwnerSelect from '@/components/admin/servers/OwnerSelect';
|
import OwnerSelect from '@/components/admin/servers/OwnerSelect';
|
||||||
import getServerDetails from '@/api/swr/admin/getServerDetails';
|
import { useServerFromRoute } from '@/api/admin/server';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { data: server } = getServerDetails();
|
const { data: server } = useServerFromRoute();
|
||||||
const { isSubmitting } = useFormikContext();
|
const { isSubmitting } = useFormikContext();
|
||||||
|
|
||||||
if (!server) return null;
|
if (!server) return null;
|
||||||
|
@ -18,7 +18,7 @@ export default () => {
|
||||||
<div css={tw`grid grid-cols-1 xl:grid-cols-2 gap-4 lg:gap-6`}>
|
<div css={tw`grid grid-cols-1 xl:grid-cols-2 gap-4 lg:gap-6`}>
|
||||||
<Field id={'name'} name={'name'} label={'Server Name'} type={'text'}/>
|
<Field id={'name'} name={'name'} label={'Server Name'} type={'text'}/>
|
||||||
<Field id={'externalId'} name={'externalId'} label={'External Identifier'} type={'text'}/>
|
<Field id={'externalId'} name={'externalId'} label={'External Identifier'} type={'text'}/>
|
||||||
<OwnerSelect selected={server.relations.user || null}/>
|
<OwnerSelect selected={server.relationships.user}/>
|
||||||
</div>
|
</div>
|
||||||
</AdminBox>
|
</AdminBox>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,14 +4,10 @@ import AdminBox from '@/components/admin/AdminBox';
|
||||||
import { faConciergeBell } from '@fortawesome/free-solid-svg-icons';
|
import { faConciergeBell } from '@fortawesome/free-solid-svg-icons';
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
import Field from '@/components/elements/Field';
|
import Field from '@/components/elements/Field';
|
||||||
import getServerDetails from '@/api/swr/admin/getServerDetails';
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { data: server } = getServerDetails();
|
|
||||||
const { isSubmitting } = useFormikContext();
|
const { isSubmitting } = useFormikContext();
|
||||||
|
|
||||||
if (!server) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminBox icon={faConciergeBell} title={'Feature Limits'} isLoading={isSubmitting}>
|
<AdminBox icon={faConciergeBell} title={'Feature Limits'} isLoading={isSubmitting}>
|
||||||
<div css={tw`grid grid-cols-1 xl:grid-cols-2 gap-4 lg:gap-6`}>
|
<div css={tw`grid grid-cols-1 xl:grid-cols-2 gap-4 lg:gap-6`}>
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import SelectField, { AsyncSelectField, Option } from '@/components/elements/SelectField';
|
||||||
|
import getAllocations from '@/api/admin/nodes/getAllocations';
|
||||||
|
import AdminBox from '@/components/admin/AdminBox';
|
||||||
|
import { faNetworkWired } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
import Label from '@/components/elements/Label';
|
||||||
|
import Select from '@/components/elements/Select';
|
||||||
|
import { useServerFromRoute } from '@/api/admin/server';
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const { isSubmitting } = useFormikContext();
|
||||||
|
const { data: server } = useServerFromRoute();
|
||||||
|
|
||||||
|
if (!server) return null;
|
||||||
|
|
||||||
|
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.getDisplayText() };
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminBox icon={faNetworkWired} title={'Networking'} isLoading={isSubmitting}>
|
||||||
|
<div css={tw`grid grid-cols-1 gap-4 lg:gap-6`}>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor={'allocationId'}>Primary Allocation</Label>
|
||||||
|
<Select id={'allocationId'} name={'allocationId'}>
|
||||||
|
{server.relationships.allocations?.map(a => (
|
||||||
|
<option key={a.id} value={a.id}>{a.getDisplayText()}</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<AsyncSelectField
|
||||||
|
id={'addAllocations'}
|
||||||
|
name={'addAllocations'}
|
||||||
|
label={'Add Allocations'}
|
||||||
|
loadOptions={loadOptions}
|
||||||
|
isMulti
|
||||||
|
/>
|
||||||
|
<SelectField
|
||||||
|
id={'removeAllocations'}
|
||||||
|
name={'removeAllocations'}
|
||||||
|
label={'Remove Allocations'}
|
||||||
|
options={server.relationships.allocations?.map(a => {
|
||||||
|
return { value: a.id.toString(), label: a.getDisplayText() };
|
||||||
|
}) || []}
|
||||||
|
isMulti
|
||||||
|
isSearchable
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</AdminBox>
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in a new issue