Merge branch 'dane/type-cleanup' into v2
This commit is contained in:
commit
a3572006cb
30 changed files with 959 additions and 565 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":
|
||||||
|
|
70
resources/scripts/api/admin/egg.ts
Normal file
70
resources/scripts/api/admin/egg.ts
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import { Model, UUID, WithRelationships, withRelationships } from '@/api/admin/index';
|
||||||
|
import { Nest } from '@/api/admin/nest';
|
||||||
|
import http, { QueryBuilderParams, withQueryBuilderParams } from '@/api/http';
|
||||||
|
import { AdminTransformers } from '@/api/admin/transformers';
|
||||||
|
|
||||||
|
export interface Egg extends Model {
|
||||||
|
id: number;
|
||||||
|
uuid: UUID;
|
||||||
|
nestId: number;
|
||||||
|
author: string;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
features: string[] | null;
|
||||||
|
dockerImages: string[];
|
||||||
|
configFiles: Record<string, any> | null;
|
||||||
|
configStartup: Record<string, any> | null;
|
||||||
|
configStop: string | null;
|
||||||
|
configFrom: number | null;
|
||||||
|
startup: string;
|
||||||
|
scriptContainer: string;
|
||||||
|
copyScriptFrom: number | null;
|
||||||
|
scriptEntry: string;
|
||||||
|
scriptIsPrivileged: boolean;
|
||||||
|
scriptInstall: string | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
relationships: {
|
||||||
|
nest?: Nest;
|
||||||
|
variables?: EggVariable[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EggVariable extends Model {
|
||||||
|
id: number;
|
||||||
|
eggId: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
environmentVariable: string;
|
||||||
|
defaultValue: string;
|
||||||
|
isUserViewable: boolean;
|
||||||
|
isUserEditable: boolean;
|
||||||
|
isRequired: boolean;
|
||||||
|
rules: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a single egg from the database and returns it.
|
||||||
|
*/
|
||||||
|
export const getEgg = async (id: number | string): Promise<WithRelationships<Egg, 'nest' | 'variables'>> => {
|
||||||
|
const { data } = await http.get(`/api/application/eggs/${id}`, {
|
||||||
|
params: {
|
||||||
|
include: [ 'nest', 'variables' ],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return withRelationships(AdminTransformers.toEgg(data), 'nest', 'variables');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const searchEggs = async (nestId: number, params: QueryBuilderParams<'name'>): Promise<WithRelationships<Egg, 'variables'>[]> => {
|
||||||
|
const { data } = await http.get(`/api/application/nests/${nestId}/eggs`, {
|
||||||
|
params: {
|
||||||
|
...withQueryBuilderParams(params),
|
||||||
|
include: [ 'variables' ],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.data.map(AdminTransformers.toEgg);
|
||||||
|
};
|
|
@ -1,5 +1,38 @@
|
||||||
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 type that allows you to infer the type of an object by giving
|
||||||
|
* it the specific API request function with a return type. For example:
|
||||||
|
*
|
||||||
|
* type EggT = InferModel<typeof getEgg>;
|
||||||
|
*/
|
||||||
|
export type InferModel<T extends (...args: any) => any> = ReturnType<T> extends Promise<infer U> ? U : T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
13
resources/scripts/api/admin/location.ts
Normal file
13
resources/scripts/api/admin/location.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { Model } from '@/api/admin/index';
|
||||||
|
import { Node } from '@/api/admin/node';
|
||||||
|
|
||||||
|
export interface Location extends Model {
|
||||||
|
id: number;
|
||||||
|
short: string;
|
||||||
|
long: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
relationships: {
|
||||||
|
nodes?: Node[];
|
||||||
|
};
|
||||||
|
}
|
25
resources/scripts/api/admin/nest.ts
Normal file
25
resources/scripts/api/admin/nest.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { Model, UUID } from '@/api/admin/index';
|
||||||
|
import { Egg } from '@/api/admin/egg';
|
||||||
|
import http, { QueryBuilderParams, withQueryBuilderParams } from '@/api/http';
|
||||||
|
import { AdminTransformers } from '@/api/admin/transformers';
|
||||||
|
|
||||||
|
export interface Nest extends Model {
|
||||||
|
id: number;
|
||||||
|
uuid: UUID;
|
||||||
|
author: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
relationships: {
|
||||||
|
eggs?: Egg[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const searchNests = async (params: QueryBuilderParams<'name'>): Promise<Nest[]> => {
|
||||||
|
const { data } = await http.get('/api/application/nests', {
|
||||||
|
params: withQueryBuilderParams(params),
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.data.map(AdminTransformers.toNest);
|
||||||
|
};
|
|
@ -1,24 +0,0 @@
|
||||||
import http from '@/api/http';
|
|
||||||
import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg';
|
|
||||||
|
|
||||||
interface Filters {
|
|
||||||
name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default (nestId: number, filters?: Filters, include: string[] = []): Promise<Egg[]> => {
|
|
||||||
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/nests/${nestId}/eggs`, { params: { include: include.join(','), ...params } })
|
|
||||||
.then(response => resolve(
|
|
||||||
(response.data.data || []).map(rawDataToEgg)
|
|
||||||
))
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -1,24 +0,0 @@
|
||||||
import http from '@/api/http';
|
|
||||||
import { Nest, rawDataToNest } from '@/api/admin/nests/getNests';
|
|
||||||
|
|
||||||
interface Filters {
|
|
||||||
name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default (filters?: Filters): Promise<Nest[]> => {
|
|
||||||
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/nests', { params: { ...params } })
|
|
||||||
.then(response => resolve(
|
|
||||||
(response.data.data || []).map(rawDataToNest)
|
|
||||||
))
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
|
68
resources/scripts/api/admin/node.ts
Normal file
68
resources/scripts/api/admin/node.ts
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import { Model, UUID, WithRelationships, withRelationships } from '@/api/admin/index';
|
||||||
|
import { Location } from '@/api/admin/location';
|
||||||
|
import http from '@/api/http';
|
||||||
|
import { AdminTransformers } from '@/api/admin/transformers';
|
||||||
|
import { Server } from '@/api/admin/server';
|
||||||
|
|
||||||
|
interface NodePorts {
|
||||||
|
http: {
|
||||||
|
listen: number;
|
||||||
|
public: number;
|
||||||
|
};
|
||||||
|
sftp: {
|
||||||
|
listen: number;
|
||||||
|
public: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Allocation extends Model {
|
||||||
|
id: number;
|
||||||
|
ip: string;
|
||||||
|
port: number;
|
||||||
|
alias: string | null;
|
||||||
|
isAssigned: boolean;
|
||||||
|
relationships: {
|
||||||
|
node?: Node;
|
||||||
|
server?: Server | null;
|
||||||
|
};
|
||||||
|
getDisplayText(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Node extends Model {
|
||||||
|
id: number;
|
||||||
|
uuid: UUID;
|
||||||
|
isPublic: boolean;
|
||||||
|
locationId: number;
|
||||||
|
databaseHostId: number;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
fqdn: string;
|
||||||
|
ports: NodePorts;
|
||||||
|
scheme: 'http' | 'https';
|
||||||
|
isBehindProxy: boolean;
|
||||||
|
isMaintenanceMode: boolean;
|
||||||
|
memory: number;
|
||||||
|
memoryOverallocate: number;
|
||||||
|
disk: number;
|
||||||
|
diskOverallocate: number;
|
||||||
|
uploadSize: number;
|
||||||
|
daemonBase: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
relationships: {
|
||||||
|
location?: Location;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a single node and returns it.
|
||||||
|
*/
|
||||||
|
export const getNode = async (id: string | number): Promise<WithRelationships<Node, 'location'>> => {
|
||||||
|
const { data } = await http.get(`/api/application/nodes/${id}`, {
|
||||||
|
params: {
|
||||||
|
include: [ 'location' ],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return withRelationships(AdminTransformers.toNode(data.data), 'location');
|
||||||
|
};
|
100
resources/scripts/api/admin/server.ts
Normal file
100
resources/scripts/api/admin/server.ts
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
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';
|
||||||
|
import { Allocation, Node } from '@/api/admin/node';
|
||||||
|
import { User } from '@/api/admin/user';
|
||||||
|
import { Egg, EggVariable } from '@/api/admin/egg';
|
||||||
|
import { Nest } from '@/api/admin/nest';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerVariable extends EggVariable {
|
||||||
|
serverValue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
nestId: 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[];
|
||||||
|
nest?: Nest;
|
||||||
|
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: {
|
||||||
|
include: [ '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,
|
||||||
|
|
212
resources/scripts/api/admin/transformers.ts
Normal file
212
resources/scripts/api/admin/transformers.ts
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
import { Allocation, Node } from '@/api/admin/node';
|
||||||
|
import { Server, ServerVariable } from '@/api/admin/server';
|
||||||
|
import { FractalResponseData, FractalResponseList } from '@/api/http';
|
||||||
|
import { User, UserRole } from '@/api/admin/user';
|
||||||
|
import { Location } from '@/api/admin/location';
|
||||||
|
import { Egg, EggVariable } from '@/api/admin/egg';
|
||||||
|
import { Nest } from '@/api/admin/nest';
|
||||||
|
|
||||||
|
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, nest, 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,
|
||||||
|
nestId: attributes.nest_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, this.toAllocation),
|
||||||
|
nest: transform(nest as FractalResponseData | undefined, this.toNest),
|
||||||
|
egg: transform(egg as FractalResponseData | undefined, this.toEgg),
|
||||||
|
node: transform(node as FractalResponseData | undefined, this.toNode),
|
||||||
|
user: transform(user as FractalResponseData | undefined, this.toUser),
|
||||||
|
variables: transform(variables as FractalResponseList | undefined, this.toServerEggVariable),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
static toNode = ({ attributes }: FractalResponseData): Node => {
|
||||||
|
return {
|
||||||
|
id: attributes.id,
|
||||||
|
uuid: attributes.uuid,
|
||||||
|
isPublic: attributes.public,
|
||||||
|
locationId: attributes.location_id,
|
||||||
|
databaseHostId: attributes.database_host_id,
|
||||||
|
name: attributes.name,
|
||||||
|
description: attributes.description,
|
||||||
|
fqdn: attributes.fqdn,
|
||||||
|
ports: {
|
||||||
|
http: {
|
||||||
|
public: attributes.publicPortHttp,
|
||||||
|
listen: attributes.listenPortHttp,
|
||||||
|
},
|
||||||
|
sftp: {
|
||||||
|
public: attributes.publicPortSftp,
|
||||||
|
listen: attributes.listenPortSftp,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scheme: attributes.scheme,
|
||||||
|
isBehindProxy: attributes.behindProxy,
|
||||||
|
isMaintenanceMode: attributes.maintenance_mode,
|
||||||
|
memory: attributes.memory,
|
||||||
|
memoryOverallocate: attributes.memory_overallocate,
|
||||||
|
disk: attributes.disk,
|
||||||
|
diskOverallocate: attributes.disk_overallocate,
|
||||||
|
uploadSize: attributes.upload_size,
|
||||||
|
daemonBase: attributes.daemonBase,
|
||||||
|
createdAt: new Date(attributes.created_at),
|
||||||
|
updatedAt: new Date(attributes.updated_at),
|
||||||
|
relationships: {
|
||||||
|
location: transform(attributes.relationships?.location as FractalResponseData, this.toLocation),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
static toUserRole = ({ attributes }: FractalResponseData): UserRole => ({
|
||||||
|
id: attributes.id,
|
||||||
|
name: attributes.name,
|
||||||
|
description: attributes.description,
|
||||||
|
relationships: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
static toUser = ({ attributes }: FractalResponseData): User => {
|
||||||
|
return {
|
||||||
|
id: attributes.id,
|
||||||
|
uuid: attributes.uuid,
|
||||||
|
externalId: attributes.external_id,
|
||||||
|
username: attributes.username,
|
||||||
|
email: attributes.email,
|
||||||
|
language: attributes.language,
|
||||||
|
adminRoleId: attributes.adminRoleId || null,
|
||||||
|
roleName: attributes.role_name,
|
||||||
|
isRootAdmin: attributes.root_admin,
|
||||||
|
isUsingTwoFactor: attributes['2fa'] || false,
|
||||||
|
avatarUrl: attributes.avatar_url,
|
||||||
|
createdAt: new Date(attributes.created_at),
|
||||||
|
updatedAt: new Date(attributes.updated_at),
|
||||||
|
relationships: {
|
||||||
|
role: transform(attributes.relationships?.role as FractalResponseData, this.toUserRole) || null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
static toLocation = ({ attributes }: FractalResponseData): Location => ({
|
||||||
|
id: attributes.id,
|
||||||
|
short: attributes.short,
|
||||||
|
long: attributes.long,
|
||||||
|
createdAt: new Date(attributes.created_at),
|
||||||
|
updatedAt: new Date(attributes.updated_at),
|
||||||
|
relationships: {
|
||||||
|
nodes: transform(attributes.relationships?.node as FractalResponseList, this.toNode),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
static toEgg = ({ attributes }: FractalResponseData): Egg => ({
|
||||||
|
id: attributes.id,
|
||||||
|
uuid: attributes.uuid,
|
||||||
|
nestId: attributes.nest_id,
|
||||||
|
author: attributes.author,
|
||||||
|
name: attributes.name,
|
||||||
|
description: attributes.description,
|
||||||
|
features: attributes.features,
|
||||||
|
dockerImages: attributes.docker_images,
|
||||||
|
configFiles: attributes.config?.files,
|
||||||
|
configStartup: attributes.config?.startup,
|
||||||
|
configStop: attributes.config?.stop,
|
||||||
|
configFrom: attributes.config?.extends,
|
||||||
|
startup: attributes.startup,
|
||||||
|
copyScriptFrom: attributes.copy_script_from,
|
||||||
|
scriptContainer: attributes.script?.container,
|
||||||
|
scriptEntry: attributes.script?.entry,
|
||||||
|
scriptIsPrivileged: attributes.script?.privileged,
|
||||||
|
scriptInstall: attributes.script?.install,
|
||||||
|
createdAt: new Date(attributes.created_at),
|
||||||
|
updatedAt: new Date(attributes.updated_at),
|
||||||
|
relationships: {
|
||||||
|
nest: transform(attributes.relationships?.nest as FractalResponseData, this.toNest),
|
||||||
|
variables: transform(attributes.relationships?.variables as FractalResponseList, this.toEggVariable),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
static toEggVariable = ({ attributes }: FractalResponseData): EggVariable => ({
|
||||||
|
id: attributes.id,
|
||||||
|
eggId: attributes.egg_id,
|
||||||
|
name: attributes.name,
|
||||||
|
description: attributes.description,
|
||||||
|
environmentVariable: attributes.env_variable,
|
||||||
|
defaultValue: attributes.default_value,
|
||||||
|
isUserViewable: attributes.user_viewable,
|
||||||
|
isUserEditable: attributes.user_editable,
|
||||||
|
isRequired: attributes.required,
|
||||||
|
rules: attributes.rules,
|
||||||
|
createdAt: new Date(attributes.created_at),
|
||||||
|
updatedAt: new Date(attributes.updated_at),
|
||||||
|
relationships: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
static toServerEggVariable = (data: FractalResponseData): ServerVariable => ({
|
||||||
|
...this.toEggVariable(data),
|
||||||
|
serverValue: data.attributes.server_value,
|
||||||
|
});
|
||||||
|
|
||||||
|
static toAllocation = ({ attributes }: FractalResponseData): Allocation => ({
|
||||||
|
id: attributes.id,
|
||||||
|
ip: attributes.ip,
|
||||||
|
port: attributes.port,
|
||||||
|
alias: attributes.alias || null,
|
||||||
|
isAssigned: attributes.assigned,
|
||||||
|
relationships: {
|
||||||
|
node: transform(attributes.relationships?.node as FractalResponseData, this.toNode),
|
||||||
|
server: transform(attributes.relationships?.server as FractalResponseData, this.toServer),
|
||||||
|
},
|
||||||
|
getDisplayText (): string {
|
||||||
|
const raw = `${this.ip}:${this.port}`;
|
||||||
|
|
||||||
|
return !this.alias ? raw : `${this.alias} (${raw})`;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
static toNest = ({ attributes }: FractalResponseData): Nest => ({
|
||||||
|
id: attributes.id,
|
||||||
|
uuid: attributes.uuid,
|
||||||
|
author: attributes.author,
|
||||||
|
name: attributes.name,
|
||||||
|
description: attributes.description,
|
||||||
|
createdAt: new Date(attributes.created_at),
|
||||||
|
updatedAt: new Date(attributes.updated_at),
|
||||||
|
relationships: {
|
||||||
|
eggs: transform(attributes.relationships?.eggs as FractalResponseList, this.toEgg),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
44
resources/scripts/api/admin/user.ts
Normal file
44
resources/scripts/api/admin/user.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { Model, UUID } from '@/api/admin/index';
|
||||||
|
import { Server } from '@/api/admin/server';
|
||||||
|
import http, { QueryBuilderParams, withQueryBuilderParams } from '@/api/http';
|
||||||
|
import { AdminTransformers } from '@/api/admin/transformers';
|
||||||
|
|
||||||
|
export interface User extends Model {
|
||||||
|
id: number;
|
||||||
|
uuid: UUID;
|
||||||
|
externalId: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
language: string;
|
||||||
|
adminRoleId: number | null;
|
||||||
|
roleName: string;
|
||||||
|
isRootAdmin: boolean;
|
||||||
|
isUsingTwoFactor: boolean;
|
||||||
|
avatarUrl: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
relationships: {
|
||||||
|
role: UserRole | null;
|
||||||
|
servers?: Server[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserRole extends Model {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getUser = async (id: string | number): Promise<User> => {
|
||||||
|
const { data } = await http.get(`/api/application/users/${id}`);
|
||||||
|
|
||||||
|
return AdminTransformers.toUser(data.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const searchUserAccounts = async (params: QueryBuilderParams<'username' | 'email'>): Promise<User[]> => {
|
||||||
|
const { data } = await http.get('/api/application/users', {
|
||||||
|
params: withQueryBuilderParams(params),
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.data.map(AdminTransformers.toUser);
|
||||||
|
};
|
|
@ -1,25 +0,0 @@
|
||||||
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);
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -111,3 +111,43 @@ export function getPaginationSet (data: any): PaginationDataSet {
|
||||||
totalPages: data.total_pages,
|
totalPages: data.total_pages,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type QueryBuilderFilterValue = string | number | boolean | null;
|
||||||
|
|
||||||
|
export interface QueryBuilderParams<FilterKeys extends string = string, SortKeys extends string = string> {
|
||||||
|
filters?: {
|
||||||
|
[K in FilterKeys]?: QueryBuilderFilterValue | Readonly<QueryBuilderFilterValue[]>;
|
||||||
|
};
|
||||||
|
sorts?: {
|
||||||
|
[K in SortKeys]?: -1 | 0 | 1 | 'asc' | 'desc' | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function that parses a data object provided and builds query parameters
|
||||||
|
* for the Laravel Query Builder package automatically. This will apply sorts and
|
||||||
|
* filters deterministically based on the provided values.
|
||||||
|
*/
|
||||||
|
export const withQueryBuilderParams = (data?: QueryBuilderParams): Record<string, unknown> => {
|
||||||
|
if (!data) return {};
|
||||||
|
|
||||||
|
const filters = Object.keys(data.filters || {}).reduce((obj, key) => {
|
||||||
|
const value = data.filters?.[key];
|
||||||
|
|
||||||
|
return !value || value === '' ? obj : { ...obj, [`filter[${key}]`]: value };
|
||||||
|
}, {} as NonNullable<QueryBuilderParams['filters']>);
|
||||||
|
|
||||||
|
const sorts = Object.keys(data.sorts || {}).reduce((arr, key) => {
|
||||||
|
const value = data.sorts?.[key];
|
||||||
|
if (!value || ![ 'asc', 'desc', 1, -1 ].includes(value)) {
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [ ...arr, (value === -1 || value === 'desc' ? '-' : '') + key ];
|
||||||
|
}, [] as string[]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...filters,
|
||||||
|
sorts: !sorts.length ? undefined : sorts.join(','),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -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 });
|
|
||||||
};
|
|
|
@ -5,35 +5,36 @@ import tw, { styled } from 'twin.macro';
|
||||||
export const SubNavigation = styled.div`
|
export const SubNavigation = styled.div`
|
||||||
${tw`flex flex-row items-center flex-shrink-0 h-12 mb-4 border-b border-neutral-700`};
|
${tw`flex flex-row items-center flex-shrink-0 h-12 mb-4 border-b border-neutral-700`};
|
||||||
|
|
||||||
& > div {
|
|
||||||
${tw`flex flex-col justify-center flex-shrink-0 h-full`};
|
|
||||||
|
|
||||||
& > a {
|
& > a {
|
||||||
${tw`flex flex-row items-center h-full px-4 border-t text-neutral-300`};
|
${tw`flex flex-row items-center h-full px-4 border-b text-neutral-300 text-base whitespace-nowrap border-transparent`};
|
||||||
border-top-color: transparent !important;
|
|
||||||
|
|
||||||
& > svg {
|
& > svg {
|
||||||
${tw`w-6 h-6 mr-2`};
|
${tw`w-6 h-6 mr-2`};
|
||||||
}
|
}
|
||||||
|
|
||||||
& > span {
|
|
||||||
${tw`text-base whitespace-nowrap`};
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active, &.active {
|
&:active, &.active {
|
||||||
${tw`border-b text-primary-300 border-primary-300`};
|
${tw`text-primary-300 border-primary-300`};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const SubNavigationLink = ({ to, name, children }: { to: string, name: string, children: React.ReactNode }) => {
|
interface Props {
|
||||||
return (
|
to: string;
|
||||||
<div>
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PropsWithIcon extends Props {
|
||||||
|
icon: React.ComponentType;
|
||||||
|
children?: never;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PropsWithoutIcon extends Props {
|
||||||
|
icon?: never;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SubNavigationLink = ({ to, name, icon: IconComponent, children }: PropsWithIcon | PropsWithoutIcon) => (
|
||||||
<NavLink to={to} exact>
|
<NavLink to={to} exact>
|
||||||
{children}
|
{IconComponent ? <IconComponent/> : children}{name}
|
||||||
<span>{name}</span>
|
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,70 +1,60 @@
|
||||||
import Label from '@/components/elements/Label';
|
import Label from '@/components/elements/Label';
|
||||||
import Select from '@/components/elements/Select';
|
import Select from '@/components/elements/Select';
|
||||||
import { useFormikContext } from 'formik';
|
import { useField } from 'formik';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Egg } from '@/api/admin/eggs/getEgg';
|
import { Egg, searchEggs } from '@/api/admin/egg';
|
||||||
import searchEggs from '@/api/admin/nests/searchEggs';
|
import { WithRelationships } from '@/api/admin';
|
||||||
|
|
||||||
export default ({ nestId, egg, setEgg }: { nestId: number | null; egg: Egg | null, setEgg: (value: Egg | null) => void }) => {
|
interface Props {
|
||||||
const { setFieldValue } = useFormikContext();
|
nestId?: number;
|
||||||
|
selectedEggId?: number;
|
||||||
|
onEggSelect: (egg: Egg | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
const [ eggs, setEggs ] = useState<Egg[]>([]);
|
export default ({ nestId, selectedEggId, onEggSelect }: Props) => {
|
||||||
|
const [ , , { setValue, setTouched } ] = useField<Record<string, string | undefined>>('environment');
|
||||||
/**
|
const [ eggs, setEggs ] = useState<WithRelationships<Egg, 'variables'>[] | null>(null);
|
||||||
* So you may be asking yourself, "what cluster-fuck of code is this?"
|
|
||||||
*
|
|
||||||
* Well, this code makes sure that when the egg changes, that the environment
|
|
||||||
* object has empty string values instead of undefined so React doesn't think
|
|
||||||
* the variable fields are uncontrolled.
|
|
||||||
*/
|
|
||||||
const setEgg2 = (newEgg: Egg | null) => {
|
|
||||||
if (newEgg === null) {
|
|
||||||
setEgg(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset all variables to be empty, don't inherit the previous values.
|
|
||||||
const newVariables = newEgg?.relations.variables;
|
|
||||||
newVariables?.forEach(v => setFieldValue('environment.' + v.envVariable, ''));
|
|
||||||
const variables = egg?.relations.variables?.filter(v => newVariables?.find(v2 => v2.envVariable === v.envVariable) === undefined);
|
|
||||||
|
|
||||||
setEgg(newEgg);
|
|
||||||
|
|
||||||
// Clear any variables that don't exist on the new egg.
|
|
||||||
variables?.forEach(v => setFieldValue('environment.' + v.envVariable, undefined));
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (nestId === null) {
|
if (!nestId) return setEggs(null);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
searchEggs(nestId, {}, [ 'variables' ])
|
searchEggs(nestId, {}).then(eggs => {
|
||||||
.then(eggs => {
|
|
||||||
setEggs(eggs);
|
setEggs(eggs);
|
||||||
if (eggs.length < 1) {
|
onEggSelect(eggs[0] || null);
|
||||||
setEgg2(null);
|
}).catch(error => console.error(error));
|
||||||
return;
|
|
||||||
}
|
|
||||||
setEgg2(eggs[0]);
|
|
||||||
})
|
|
||||||
.catch(error => console.error(error));
|
|
||||||
}, [ nestId ]);
|
}, [ nestId ]);
|
||||||
|
|
||||||
|
const onSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
if (!eggs) return;
|
||||||
|
|
||||||
|
const match = eggs.find(egg => String(egg.id) === e.currentTarget.value);
|
||||||
|
if (!match) return onEggSelect(null);
|
||||||
|
|
||||||
|
// Ensure that only new egg variables are present in the record storing all
|
||||||
|
// of the possible variables. This ensures the fields are controlled, rather
|
||||||
|
// than uncontrolled when a user begins typing in them.
|
||||||
|
setValue(match.relationships.variables.reduce((obj, value) => ({
|
||||||
|
...obj,
|
||||||
|
[value.environmentVariable]: undefined,
|
||||||
|
}), {}));
|
||||||
|
setTouched(true);
|
||||||
|
|
||||||
|
onEggSelect(match);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Label>Egg</Label>
|
<Label>Egg</Label>
|
||||||
<Select
|
<Select id={'eggId'} name={'eggId'} defaultValue={selectedEggId} onChange={onSelectChange}>
|
||||||
defaultValue={egg?.id || undefined}
|
{!eggs ?
|
||||||
id={'eggId'}
|
<option disabled>Loading...</option>
|
||||||
name={'eggId'}
|
:
|
||||||
onChange={e => setEgg2(eggs.find(egg => egg.id.toString() === e.currentTarget.value) || null)}
|
eggs.map(v => (
|
||||||
>
|
|
||||||
{eggs.map(v => (
|
|
||||||
<option key={v.id} value={v.id.toString()}>
|
<option key={v.id} value={v.id.toString()}>
|
||||||
{v.name}
|
{v.name}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))
|
||||||
|
}
|
||||||
</Select>
|
</Select>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
import Label from '@/components/elements/Label';
|
|
||||||
import Select from '@/components/elements/Select';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { Nest } from '@/api/admin/nests/getNests';
|
|
||||||
import searchNests from '@/api/admin/nests/searchNests';
|
|
||||||
|
|
||||||
export default ({ nestId, setNestId }: { nestId: number | null; setNestId: (value: number | null) => void }) => {
|
|
||||||
const [ nests, setNests ] = useState<Nest[] | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
searchNests({})
|
|
||||||
.then(nests => setNests(nests))
|
|
||||||
.catch(error => console.error(error));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Label>Nest</Label>
|
|
||||||
<Select value={nestId || undefined} onChange={e => setNestId(Number(e.currentTarget.value))}>
|
|
||||||
{nests?.map(v => (
|
|
||||||
<option key={v.id} value={v.id.toString()}>
|
|
||||||
{v.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
36
resources/scripts/components/admin/servers/NestSelector.tsx
Normal file
36
resources/scripts/components/admin/servers/NestSelector.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import Label from '@/components/elements/Label';
|
||||||
|
import Select from '@/components/elements/Select';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Nest, searchNests } from '@/api/admin/nest';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
selectedNestId?: number;
|
||||||
|
onNestSelect: (nest: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ({ selectedNestId, onNestSelect }: Props) => {
|
||||||
|
const [ nests, setNests ] = useState<Nest[] | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
searchNests({})
|
||||||
|
.then(setNests)
|
||||||
|
.catch(error => console.error(error));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Label>Nest</Label>
|
||||||
|
<Select value={selectedNestId} onChange={e => onNestSelect(Number(e.currentTarget.value))}>
|
||||||
|
{!nests ?
|
||||||
|
<option disabled>Loading...</option>
|
||||||
|
:
|
||||||
|
nests?.map(v => (
|
||||||
|
<option key={v.uuid} value={v.id.toString()}>
|
||||||
|
{v.name}
|
||||||
|
</option>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,24 +1,18 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useFormikContext } from 'formik';
|
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';
|
import SearchableSelect, { Option } from '@/components/elements/SearchableSelect';
|
||||||
|
import { User, searchUserAccounts } from '@/api/admin/user';
|
||||||
|
|
||||||
export default ({ selected }: { selected: User | null }) => {
|
export default ({ selected }: { selected: User }) => {
|
||||||
const context = useFormikContext();
|
const context = useFormikContext();
|
||||||
|
|
||||||
const [ user, setUser ] = useState<User | null>(selected);
|
const [ user, setUser ] = useState<User | null>(selected);
|
||||||
const [ users, setUsers ] = useState<User[] | null>(null);
|
const [ users, setUsers ] = useState<User[] | null>(null);
|
||||||
|
|
||||||
const onSearch = (query: string): Promise<void> => {
|
const onSearch = async (query: string) => {
|
||||||
return new Promise((resolve, reject) => {
|
setUsers(
|
||||||
searchUsers({ username: query, email: query })
|
await searchUserAccounts({ filters: { username: query, email: query } })
|
||||||
.then((users) => {
|
);
|
||||||
setUsers(users);
|
|
||||||
return resolve();
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelect = (user: User | null) => {
|
const onSelect = (user: User | null) => {
|
||||||
|
@ -26,9 +20,7 @@ export default ({ selected }: { selected: User | null }) => {
|
||||||
context.setFieldValue('ownerId', user?.id || null);
|
context.setFieldValue('ownerId', user?.id || null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSelectedText = (user: User | null): string => {
|
const getSelectedText = (user: User | null): string => user?.email || '';
|
||||||
return user?.email || '';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SearchableSelect
|
<SearchableSelect
|
||||||
|
|
|
@ -5,27 +5,29 @@ import tw from 'twin.macro';
|
||||||
import Button from '@/components/elements/Button';
|
import Button from '@/components/elements/Button';
|
||||||
import ConfirmationModal from '@/components/elements/ConfirmationModal';
|
import ConfirmationModal from '@/components/elements/ConfirmationModal';
|
||||||
import deleteServer from '@/api/admin/servers/deleteServer';
|
import deleteServer from '@/api/admin/servers/deleteServer';
|
||||||
|
import { TrashIcon } from '@heroicons/react/outline';
|
||||||
|
import { useServerFromRoute } from '@/api/admin/server';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
interface Props {
|
export default () => {
|
||||||
serverId: number;
|
const history = useHistory();
|
||||||
onDeleted: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ({ serverId, onDeleted }: Props) => {
|
|
||||||
const [ visible, setVisible ] = useState(false);
|
const [ visible, setVisible ] = useState(false);
|
||||||
const [ loading, setLoading ] = useState(false);
|
const [ loading, setLoading ] = useState(false);
|
||||||
|
const { data: server } = useServerFromRoute();
|
||||||
|
|
||||||
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
const {
|
||||||
|
clearFlashes,
|
||||||
|
clearAndAddHttpError,
|
||||||
|
} = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||||
|
|
||||||
const onDelete = () => {
|
const onDelete = () => {
|
||||||
|
if (!server) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
clearFlashes('server');
|
clearFlashes('server');
|
||||||
|
|
||||||
deleteServer(serverId)
|
deleteServer(server.id)
|
||||||
.then(() => {
|
.then(() => history.push('/admin/servers'))
|
||||||
setLoading(false);
|
|
||||||
onDeleted();
|
|
||||||
})
|
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
clearAndAddHttpError({ key: 'server', error });
|
clearAndAddHttpError({ key: 'server', error });
|
||||||
|
@ -35,6 +37,8 @@ export default ({ serverId, onDeleted }: Props) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!server) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
|
@ -47,11 +51,14 @@ export default ({ serverId, onDeleted }: Props) => {
|
||||||
>
|
>
|
||||||
Are you sure you want to delete this server?
|
Are you sure you want to delete this server?
|
||||||
</ConfirmationModal>
|
</ConfirmationModal>
|
||||||
|
<Button
|
||||||
<Button type={'button'} size={'xsmall'} color={'red'} onClick={() => setVisible(true)}>
|
type={'button'}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" css={tw`h-5 w-5`}>
|
size={'small'}
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
color={'red'}
|
||||||
</svg>
|
onClick={() => setVisible(true)}
|
||||||
|
css={tw`flex items-center justify-center`}
|
||||||
|
>
|
||||||
|
<TrashIcon css={tw`w-5 h-5 mr-2`}/> Delete Server
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,17 +1,13 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import AdminBox from '@/components/admin/AdminBox';
|
import AdminBox from '@/components/admin/AdminBox';
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
import { Context } from '@/components/admin/servers/ServerRouter';
|
|
||||||
import Button from '@/components/elements/Button';
|
import Button from '@/components/elements/Button';
|
||||||
|
import { useServerFromRoute } from '@/api/admin/server';
|
||||||
|
|
||||||
const ServerManageContainer = () => {
|
export default () => {
|
||||||
const server = Context.useStoreState(state => state.server);
|
const { data: server } = useServerFromRoute();
|
||||||
|
|
||||||
if (server === undefined) {
|
if (!server) return null;
|
||||||
return (
|
|
||||||
<></>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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`grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-x-2 gap-y-2`}>
|
||||||
|
@ -52,17 +48,3 @@ const ServerManageContainer = () => {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const server = Context.useStoreState(state => state.server);
|
|
||||||
|
|
||||||
if (server === undefined) {
|
|
||||||
return (
|
|
||||||
<></>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ServerManageContainer/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -4,48 +4,29 @@ import React, { useEffect } from 'react';
|
||||||
import { useLocation } from 'react-router';
|
import { useLocation } from 'react-router';
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
import { Route, Switch, useRouteMatch } from 'react-router-dom';
|
import { Route, Switch, useRouteMatch } from 'react-router-dom';
|
||||||
import { action, Action, createContextStore } from 'easy-peasy';
|
|
||||||
import { Server } from '@/api/admin/servers/getServers';
|
|
||||||
import AdminContentBlock from '@/components/admin/AdminContentBlock';
|
import AdminContentBlock from '@/components/admin/AdminContentBlock';
|
||||||
import Spinner from '@/components/elements/Spinner';
|
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';
|
||||||
|
import { AdjustmentsIcon, CogIcon, DatabaseIcon, FolderIcon, ShieldExclamationIcon } from '@heroicons/react/outline';
|
||||||
|
|
||||||
export const ServerIncludes = [ 'allocations', 'user', 'variables' ];
|
export default () => {
|
||||||
|
|
||||||
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 location = useLocation();
|
||||||
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();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!error) {
|
if (!error) clearFlashes('server');
|
||||||
clearFlashes('server');
|
if (error) clearAndAddHttpError({ error, key: 'server' });
|
||||||
} else {
|
|
||||||
clearAndAddHttpError({ error, key: 'server' });
|
|
||||||
}
|
|
||||||
}, [ error ]);
|
}, [ error ]);
|
||||||
|
|
||||||
if (!server || (error && isValidating)) {
|
if (!server || (error && isValidating)) {
|
||||||
|
@ -67,60 +48,18 @@ const ServerRouter = () => {
|
||||||
</div>
|
</div>
|
||||||
<FlashMessageRender byKey={'server'} css={tw`mb-4`}/>
|
<FlashMessageRender byKey={'server'} css={tw`mb-4`}/>
|
||||||
<SubNavigation>
|
<SubNavigation>
|
||||||
<SubNavigationLink to={`${match.url}`} name={'Settings'}>
|
<SubNavigationLink to={`${match.url}`} name={'Settings'} icon={CogIcon}/>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
<SubNavigationLink to={`${match.url}/startup`} name={'Startup'} icon={AdjustmentsIcon}/>
|
||||||
<path
|
<SubNavigationLink to={`${match.url}/databases`} name={'Databases'} icon={DatabaseIcon}/>
|
||||||
clipRule="evenodd"
|
<SubNavigationLink to={`${match.url}/mounts`} name={'Mounts'} icon={FolderIcon}/>
|
||||||
fillRule="evenodd"
|
<SubNavigationLink to={`${match.url}/manage`} name={'Manage'} icon={ShieldExclamationIcon}/>
|
||||||
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>
|
</SubNavigation>
|
||||||
<Switch location={location}>
|
<Switch location={location}>
|
||||||
<Route path={`${match.path}`} exact>
|
<Route path={`${match.path}`} exact>
|
||||||
<ServerSettingsContainer server={server}/>
|
<ServerSettingsContainer/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`${match.path}/startup`} exact>
|
<Route path={`${match.path}/startup`} exact>
|
||||||
<ServerStartupContainer server={server}/>
|
<ServerStartupContainer/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`${match.path}/manage`} exact>
|
<Route path={`${match.path}/manage`} exact>
|
||||||
<ServerManageContainer/>
|
<ServerManageContainer/>
|
||||||
|
@ -129,11 +68,3 @@ const ServerRouter = () => {
|
||||||
</AdminContentBlock>
|
</AdminContentBlock>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default () => {
|
|
||||||
return (
|
|
||||||
<Context.Provider>
|
|
||||||
<ServerRouter/>
|
|
||||||
</Context.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,168 +1,22 @@
|
||||||
import getAllocations from '@/api/admin/nodes/getAllocations';
|
import { useServerFromRoute } from '@/api/admin/server';
|
||||||
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, faConciergeBell, faNetworkWired } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import AdminBox from '@/components/admin/AdminBox';
|
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
import { object } from 'yup';
|
import { object } from 'yup';
|
||||||
import updateServer, { Values } from '@/api/admin/servers/updateServer';
|
import updateServer, { Values } from '@/api/admin/servers/updateServer';
|
||||||
import Field from '@/components/elements/Field';
|
import { Form, Formik, FormikHelpers } from 'formik';
|
||||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
import { useStoreActions } from 'easy-peasy';
|
||||||
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
|
||||||
import { Context, ServerIncludes } from '@/components/admin/servers/ServerRouter';
|
|
||||||
import { ApplicationStore } from '@/state';
|
|
||||||
import { Actions, useStoreActions } from 'easy-peasy';
|
|
||||||
import Button from '@/components/elements/Button';
|
import Button from '@/components/elements/Button';
|
||||||
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';
|
||||||
|
import ServerResourceBox from '@/components/admin/servers/settings/ServerResourceBox';
|
||||||
|
|
||||||
export function ServerAllocationsContainer ({ server }: { server: Server }) {
|
export default () => {
|
||||||
const { isSubmitting } = useFormikContext();
|
const { data: server, mutate } = useServerFromRoute();
|
||||||
|
const { clearFlashes, clearAndAddHttpError } = useStoreActions(actions => actions.flashes);
|
||||||
|
|
||||||
const loadOptions = async (inputValue: string, callback: (options: Option[]) => void) => {
|
if (!server) return null;
|
||||||
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 () {
|
|
||||||
const { isSubmitting } = useFormikContext();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AdminBox icon={faBalanceScale} title={'Resources'} css={tw`relative w-full`}>
|
|
||||||
<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-col md:mr-4 md:mb-0`}>
|
|
||||||
<Field
|
|
||||||
id={'limits.cpu'}
|
|
||||||
name={'limits.cpu'}
|
|
||||||
label={'CPU Limit'}
|
|
||||||
type={'text'}
|
|
||||||
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 css={tw`mb-6 md:w-full md:flex md:flex-col md:ml-4 md:mb-0`}>
|
|
||||||
<Field
|
|
||||||
id={'limits.threads'}
|
|
||||||
name={'limits.threads'}
|
|
||||||
label={'CPU Pinning'}
|
|
||||||
type={'text'}
|
|
||||||
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 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={'limits.memory'}
|
|
||||||
name={'limits.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={'limits.swap'}
|
|
||||||
name={'limits.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={'limits.disk'}
|
|
||||||
name={'limits.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={'limits.io'}
|
|
||||||
name={'limits.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-2 md:w-full md:flex md:flex-row`}>
|
|
||||||
<div css={tw`bg-neutral-800 border border-neutral-900 shadow-inner p-4 rounded`}>
|
|
||||||
<FormikSwitch
|
|
||||||
name={'limits.oomDisabled'}
|
|
||||||
label={'Out of Memory Killer'}
|
|
||||||
description={'Enabling the Out of Memory Killer may cause server processes to exit unexpectedly.'}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AdminBox>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ServerSettingsContainer2 ({ server }: { server: Server }) {
|
|
||||||
const history = useHistory();
|
|
||||||
|
|
||||||
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
|
||||||
|
|
||||||
const setServer = Context.useStoreActions(actions => actions.setServer);
|
|
||||||
|
|
||||||
const submit = (values: Values, { setSubmitting, setFieldValue }: FormikHelpers<Values>) => {
|
const submit = (values: Values, { setSubmitting, setFieldValue }: FormikHelpers<Values>) => {
|
||||||
clearFlashes('server');
|
clearFlashes('server');
|
||||||
|
@ -171,9 +25,9 @@ export default function ServerSettingsContainer2 ({ server }: { server: Server }
|
||||||
// OOM Killer is enabled, rather than when disabled.
|
// OOM Killer is enabled, rather than when disabled.
|
||||||
values.limits.oomDisabled = !values.limits.oomDisabled;
|
values.limits.oomDisabled = !values.limits.oomDisabled;
|
||||||
|
|
||||||
updateServer(server.id, values, ServerIncludes)
|
updateServer(server.id, values)
|
||||||
.then(s => {
|
.then(s => {
|
||||||
setServer({ ...server, ...s });
|
// setServer({ ...server, ...s });
|
||||||
|
|
||||||
// TODO: Figure out how to properly clear react-selects for allocations.
|
// TODO: Figure out how to properly clear react-selects for allocations.
|
||||||
setFieldValue('addAllocations', []);
|
setFieldValue('addAllocations', []);
|
||||||
|
@ -192,8 +46,7 @@ export default function ServerSettingsContainer2 ({ server }: { server: Server }
|
||||||
initialValues={{
|
initialValues={{
|
||||||
externalId: server.externalId || '',
|
externalId: server.externalId || '',
|
||||||
name: server.name,
|
name: server.name,
|
||||||
ownerId: server.ownerId,
|
ownerId: server.userId,
|
||||||
|
|
||||||
limits: {
|
limits: {
|
||||||
memory: server.limits.memory,
|
memory: server.limits.memory,
|
||||||
swap: server.limits.swap,
|
swap: server.limits.swap,
|
||||||
|
@ -205,19 +58,16 @@ export default function ServerSettingsContainer2 ({ server }: { server: Server }
|
||||||
// OOM Killer is enabled, rather than when disabled.
|
// OOM Killer is enabled, rather than when disabled.
|
||||||
oomDisabled: !server.limits.oomDisabled,
|
oomDisabled: !server.limits.oomDisabled,
|
||||||
},
|
},
|
||||||
|
|
||||||
featureLimits: {
|
featureLimits: {
|
||||||
allocations: server.featureLimits.allocations,
|
allocations: server.featureLimits.allocations,
|
||||||
backups: server.featureLimits.backups,
|
backups: server.featureLimits.backups,
|
||||||
databases: server.featureLimits.databases,
|
databases: server.featureLimits.databases,
|
||||||
},
|
},
|
||||||
|
|
||||||
allocationId: server.allocationId,
|
allocationId: server.allocationId,
|
||||||
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,19 +75,19 @@ 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/>
|
<ServerResourceBox/>
|
||||||
|
<div css={tw`bg-neutral-700 rounded shadow-md px-4 xl:px-5 py-5 mt-6`}>
|
||||||
<div css={tw`bg-neutral-700 rounded shadow-md py-2 px-6 mt-6`}>
|
|
||||||
<div css={tw`flex flex-row`}>
|
<div css={tw`flex flex-row`}>
|
||||||
<ServerDeleteButton
|
<ServerDeleteButton/>
|
||||||
serverId={server?.id}
|
<Button
|
||||||
onDeleted={() => history.push('/admin/servers')}
|
type="submit"
|
||||||
/>
|
size="small"
|
||||||
<Button type="submit" size="small" css={tw`ml-auto`} disabled={isSubmitting || !isValid}>
|
css={tw`ml-auto`}
|
||||||
|
disabled={isSubmitting || !isValid}
|
||||||
|
>
|
||||||
Save Changes
|
Save Changes
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -248,4 +98,4 @@ export default function ServerSettingsContainer2 ({ server }: { server: Server }
|
||||||
)}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { getEgg, Egg, EggVariable } from '@/api/admin/eggs/getEgg';
|
import { Egg, EggVariable, getEgg } from '@/api/admin/egg';
|
||||||
import { Server } from '@/api/admin/servers/getServers';
|
|
||||||
import updateServerStartup, { Values } from '@/api/admin/servers/updateServerStartup';
|
import updateServerStartup, { Values } from '@/api/admin/servers/updateServerStartup';
|
||||||
import EggSelect from '@/components/admin/servers/EggSelect';
|
import EggSelect from '@/components/admin/servers/EggSelect';
|
||||||
import NestSelect from '@/components/admin/servers/NestSelect';
|
import NestSelector from '@/components/admin/servers/NestSelector';
|
||||||
import { Context, ServerIncludes } from '@/components/admin/servers/ServerRouter';
|
|
||||||
import FormikSwitch from '@/components/elements/FormikSwitch';
|
import FormikSwitch from '@/components/elements/FormikSwitch';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import Button from '@/components/elements/Button';
|
import Button from '@/components/elements/Button';
|
||||||
|
@ -17,6 +15,8 @@ import { ApplicationStore } from '@/state';
|
||||||
import { Actions, useStoreActions } from 'easy-peasy';
|
import { Actions, useStoreActions } from 'easy-peasy';
|
||||||
import Label from '@/components/elements/Label';
|
import Label from '@/components/elements/Label';
|
||||||
import { object } from 'yup';
|
import { object } from 'yup';
|
||||||
|
import { Server, useServerFromRoute } from '@/api/admin/server';
|
||||||
|
import { InferModel } from '@/api/admin';
|
||||||
|
|
||||||
function ServerStartupLineContainer ({ egg, server }: { egg: Egg | null; server: Server }) {
|
function ServerStartupLineContainer ({ egg, server }: { egg: Egg | null; server: Server }) {
|
||||||
const { isSubmitting, setFieldValue } = useFormikContext();
|
const { isSubmitting, setFieldValue } = useFormikContext();
|
||||||
|
@ -60,26 +60,18 @@ function ServerStartupLineContainer ({ egg, server }: { egg: Egg | null; server:
|
||||||
function ServerServiceContainer ({ egg, setEgg, server }: { egg: Egg | null, setEgg: (value: Egg | null) => void, server: Server }) {
|
function ServerServiceContainer ({ egg, setEgg, server }: { egg: Egg | null, setEgg: (value: Egg | null) => void, server: Server }) {
|
||||||
const { isSubmitting } = useFormikContext();
|
const { isSubmitting } = useFormikContext();
|
||||||
|
|
||||||
const [ nestId, setNestId ] = useState<number | null>(server.nestId);
|
const [ nestId, setNestId ] = useState(server.nestId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminBox title={'Service Configuration'} css={tw`relative w-full`}>
|
<AdminBox title={'Service Configuration'} isLoading={isSubmitting}>
|
||||||
<SpinnerOverlay visible={isSubmitting}/>
|
|
||||||
|
|
||||||
<div css={tw`mb-6`}>
|
<div css={tw`mb-6`}>
|
||||||
<NestSelect nestId={nestId} setNestId={setNestId}/>
|
<NestSelector selectedNestId={nestId} onNestSelect={setNestId}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div css={tw`mb-6`}>
|
<div css={tw`mb-6`}>
|
||||||
<EggSelect nestId={nestId} egg={egg} setEgg={setEgg}/>
|
<EggSelect nestId={nestId} selectedEggId={egg?.id} onEggSelect={setEgg}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div css={tw`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={'skipScript'} label={'Skip Egg Install Script'} description={'Soon™'}/>
|
||||||
name={'skipScript'}
|
|
||||||
label={'Skip Egg Install Script'}
|
|
||||||
description={'SoonTM'}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</AdminBox>
|
</AdminBox>
|
||||||
);
|
);
|
||||||
|
@ -107,7 +99,7 @@ function ServerImageContainer () {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ServerVariableContainer ({ variable, defaultValue }: { variable: EggVariable, defaultValue: string }) {
|
function ServerVariableContainer ({ variable, defaultValue }: { variable: EggVariable, defaultValue: string }) {
|
||||||
const key = 'environment.' + variable.envVariable;
|
const key = 'environment.' + variable.environmentVariable;
|
||||||
|
|
||||||
const { isSubmitting, setFieldValue } = useFormikContext();
|
const { isSubmitting, setFieldValue } = useFormikContext();
|
||||||
|
|
||||||
|
@ -158,11 +150,11 @@ function ServerStartupForm ({ egg, setEgg, server }: { egg: Egg | null, setEgg:
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div css={tw`grid grid-cols-1 md:grid-cols-2 gap-y-6 gap-x-8`}>
|
<div css={tw`grid grid-cols-1 md:grid-cols-2 gap-y-6 gap-x-8`}>
|
||||||
{egg?.relations.variables?.map((v, i) => (
|
{egg?.relationships.variables?.map((v, i) => (
|
||||||
<ServerVariableContainer
|
<ServerVariableContainer
|
||||||
key={i}
|
key={i}
|
||||||
variable={v}
|
variable={v}
|
||||||
defaultValue={server.relations?.variables.find(v2 => v.eggId === v2.eggId && v.envVariable === v2.envVariable)?.serverValue || v.defaultValue}
|
defaultValue={server.relationships?.variables?.find(v2 => v.eggId === v2.eggId && v.environmentVariable === v2.environmentVariable)?.serverValue || v.defaultValue}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -179,26 +171,28 @@ function ServerStartupForm ({ egg, setEgg, server }: { egg: Egg | null, setEgg:
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ServerStartupContainer ({ server }: { server: Server }) {
|
export default () => {
|
||||||
|
const { data: server } = useServerFromRoute();
|
||||||
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||||
|
const [ egg, setEgg ] = useState<InferModel<typeof getEgg> | null>(null);
|
||||||
const [ egg, setEgg ] = useState<Egg | null>(null);
|
|
||||||
|
|
||||||
const setServer = Context.useStoreActions(actions => actions.setServer);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!server) return;
|
||||||
|
|
||||||
getEgg(server.eggId)
|
getEgg(server.eggId)
|
||||||
.then(egg => setEgg(egg))
|
.then(egg => setEgg(egg))
|
||||||
.catch(error => console.error(error));
|
.catch(error => console.error(error));
|
||||||
}, []);
|
}, [ server?.eggId ]);
|
||||||
|
|
||||||
|
if (!server) return null;
|
||||||
|
|
||||||
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||||
clearFlashes('server');
|
clearFlashes('server');
|
||||||
|
|
||||||
updateServerStartup(server.id, values, ServerIncludes)
|
updateServerStartup(server.id, values)
|
||||||
.then(s => {
|
// .then(s => {
|
||||||
setServer({ ...server, ...s });
|
// mutate(data => { ...data, ...s });
|
||||||
})
|
// })
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
clearAndAddHttpError({ key: 'server', error });
|
clearAndAddHttpError({ key: 'server', error });
|
||||||
|
@ -212,7 +206,7 @@ export default function ServerStartupContainer ({ server }: { server: Server })
|
||||||
initialValues={{
|
initialValues={{
|
||||||
startup: server.container.startup,
|
startup: server.container.startup,
|
||||||
// Don't ask.
|
// Don't ask.
|
||||||
environment: Object.fromEntries(egg?.relations.variables?.map(v => [ v.envVariable, '' ]) || []),
|
environment: Object.fromEntries(egg?.relationships.variables.map(v => [ v.environmentVariable, '' ]) || []),
|
||||||
image: server.container.image,
|
image: server.container.image,
|
||||||
eggId: server.eggId,
|
eggId: server.eggId,
|
||||||
skipScripts: false,
|
skipScripts: false,
|
||||||
|
@ -222,9 +216,10 @@ export default function ServerStartupContainer ({ server }: { server: Server })
|
||||||
>
|
>
|
||||||
<ServerStartupForm
|
<ServerStartupForm
|
||||||
egg={egg}
|
egg={egg}
|
||||||
|
// @ts-ignore
|
||||||
setEgg={setEgg}
|
setEgg={setEgg}
|
||||||
server={server}
|
server={server}
|
||||||
/>
|
/>
|
||||||
</Formik>
|
</Formik>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import AdminBox from '@/components/admin/AdminBox';
|
||||||
|
import { faBalanceScale } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
import Field from '@/components/elements/Field';
|
||||||
|
import FormikSwitch from '@/components/elements/FormikSwitch';
|
||||||
|
import React from 'react';
|
||||||
|
import { useServerFromRoute } from '@/api/admin/server';
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const { isSubmitting } = useFormikContext();
|
||||||
|
const { data: server } = useServerFromRoute();
|
||||||
|
|
||||||
|
if (!server) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminBox icon={faBalanceScale} title={'Resources'} isLoading={isSubmitting}>
|
||||||
|
<div css={tw`grid grid-cols-1 xl:grid-cols-2 gap-4 lg:gap-6`}>
|
||||||
|
<Field
|
||||||
|
id={'limits.cpu'}
|
||||||
|
name={'limits.cpu'}
|
||||||
|
label={'CPU Limit'}
|
||||||
|
type={'text'}
|
||||||
|
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.'}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
id={'limits.threads'}
|
||||||
|
name={'limits.threads'}
|
||||||
|
label={'CPU Pinning'}
|
||||||
|
type={'text'}
|
||||||
|
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.'}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
id={'limits.memory'}
|
||||||
|
name={'limits.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.'}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
id={'limits.swap'}
|
||||||
|
name={'limits.swap'}
|
||||||
|
label={'Swap Limit'}
|
||||||
|
type={'number'}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
id={'limits.disk'}
|
||||||
|
name={'limits.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.'}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
id={'limits.io'}
|
||||||
|
name={'limits.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 css={tw`xl:col-span-2 bg-neutral-800 border border-neutral-900 shadow-inner p-4 rounded`}>
|
||||||
|
<FormikSwitch
|
||||||
|
name={'limits.oomDisabled'}
|
||||||
|
label={'Out of Memory Killer'}
|
||||||
|
description={'Enabling the Out of Memory Killer may cause server processes to exit unexpectedly.'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AdminBox>
|
||||||
|
);
|
||||||
|
};
|
|
@ -9,13 +9,9 @@ interface Props {
|
||||||
isSecondary?: boolean;
|
isSecondary?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ButtonStyle = styled.button<Omit<Props, 'isLoading'>>`
|
const ButtonStyle = styled.button<Props>`
|
||||||
${tw`relative inline-block rounded p-2 tracking-wide text-sm transition-all duration-150 border`};
|
${tw`relative inline-block rounded p-2 tracking-wide text-sm transition-all duration-150 border`};
|
||||||
|
|
||||||
& > span {
|
|
||||||
${tw`select-none`};
|
|
||||||
}
|
|
||||||
|
|
||||||
${props => ((!props.isSecondary && !props.color) || props.color === 'primary') && css<Props>`
|
${props => ((!props.isSecondary && !props.color) || props.color === 'primary') && css<Props>`
|
||||||
${props => !props.isSecondary && tw`bg-primary-500 border-primary-600 border text-primary-50`};
|
${props => !props.isSecondary && tw`bg-primary-500 border-primary-600 border text-primary-50`};
|
||||||
|
|
||||||
|
@ -77,21 +73,23 @@ const ButtonStyle = styled.button<Omit<Props, 'isLoading'>>`
|
||||||
}
|
}
|
||||||
`};
|
`};
|
||||||
|
|
||||||
&:disabled { opacity: 0.55; cursor: default }
|
${props => props.isLoading && tw`text-transparent`};
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
${tw`opacity-75 cursor-not-allowed`};
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type ComponentProps = Omit<JSX.IntrinsicElements['button'], 'ref' | keyof Props> & Props;
|
type ComponentProps = Omit<JSX.IntrinsicElements['button'], 'ref' | keyof Props> & Props;
|
||||||
|
|
||||||
const Button: React.FC<ComponentProps> = ({ children, isLoading, ...props }) => (
|
const Button: React.FC<ComponentProps> = ({ children, isLoading, disabled, ...props }) => (
|
||||||
<ButtonStyle {...props}>
|
<ButtonStyle {...props} isLoading={isLoading} disabled={isLoading || disabled}>
|
||||||
{isLoading &&
|
{isLoading &&
|
||||||
<div css={tw`flex absolute justify-center items-center w-full h-full left-0 top-0`}>
|
<div css={tw`flex absolute justify-center items-center w-full h-full left-0 top-0`}>
|
||||||
<Spinner size={'small'}/>
|
<Spinner size={'small'}/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<span css={isLoading ? tw`text-transparent` : undefined}>
|
|
||||||
{children}
|
{children}
|
||||||
</span>
|
|
||||||
</ButtonStyle>
|
</ButtonStyle>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue