Cleanup more of the server screen typings
This commit is contained in:
parent
f6998018b4
commit
85c8f4884f
|
@ -1,10 +1,31 @@
|
||||||
import { Model, UUID } from '@/api/admin/index';
|
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 {
|
export interface Egg extends Model {
|
||||||
id: number;
|
id: number;
|
||||||
uuid: UUID;
|
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;
|
startup: string;
|
||||||
|
scriptContainer: string;
|
||||||
|
copyScriptFrom: number | null;
|
||||||
|
scriptEntry: string;
|
||||||
|
scriptIsPrivileged: boolean;
|
||||||
|
scriptInstall: string | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
relationships: {
|
relationships: {
|
||||||
|
nest?: Nest;
|
||||||
variables?: EggVariable[];
|
variables?: EggVariable[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -23,3 +44,27 @@ export interface EggVariable extends Model {
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: 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: {
|
||||||
|
includes: [ 'nest', 'variables' ],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return withRelationships(AdminTransformers.toEgg(data.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),
|
||||||
|
includes: [ 'variables' ],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.data.map(AdminTransformers.toEgg);
|
||||||
|
};
|
||||||
|
|
|
@ -16,6 +16,14 @@ export type WithRelationships<M extends Model, R extends string> = Omit<M, 'rela
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
* 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
|
* such that TypeScript understands the relationships on it. This is just to help
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -7,6 +7,7 @@ import { AdminTransformers } from '@/api/admin/transformers';
|
||||||
import { Allocation, Node } from '@/api/admin/node';
|
import { Allocation, Node } from '@/api/admin/node';
|
||||||
import { User } from '@/api/admin/user';
|
import { User } from '@/api/admin/user';
|
||||||
import { Egg, EggVariable } from '@/api/admin/egg';
|
import { Egg, EggVariable } from '@/api/admin/egg';
|
||||||
|
import { Nest } from '@/api/admin/nest';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines the limits for a server that exists on the Panel.
|
* Defines the limits for a server that exists on the Panel.
|
||||||
|
@ -41,6 +42,7 @@ export interface Server extends Model {
|
||||||
nodeId: number;
|
nodeId: number;
|
||||||
allocationId: number;
|
allocationId: number;
|
||||||
eggId: number;
|
eggId: number;
|
||||||
|
nestId: number;
|
||||||
limits: ServerLimits;
|
limits: ServerLimits;
|
||||||
featureLimits: {
|
featureLimits: {
|
||||||
databases: number;
|
databases: number;
|
||||||
|
@ -56,6 +58,7 @@ export interface Server extends Model {
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
relationships: {
|
relationships: {
|
||||||
allocations?: Allocation[];
|
allocations?: Allocation[];
|
||||||
|
nest?: Nest;
|
||||||
egg?: Egg;
|
egg?: Egg;
|
||||||
node?: Node;
|
node?: Node;
|
||||||
user?: User;
|
user?: User;
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { FractalResponseData, FractalResponseList } from '@/api/http';
|
||||||
import { User, UserRole } from '@/api/admin/user';
|
import { User, UserRole } from '@/api/admin/user';
|
||||||
import { Location } from '@/api/admin/location';
|
import { Location } from '@/api/admin/location';
|
||||||
import { Egg, EggVariable } from '@/api/admin/egg';
|
import { Egg, EggVariable } from '@/api/admin/egg';
|
||||||
|
import { Nest } from '@/api/admin/nest';
|
||||||
|
|
||||||
const isList = (data: FractalResponseList | FractalResponseData): data is FractalResponseList => data.object === 'list';
|
const isList = (data: FractalResponseList | FractalResponseData): data is FractalResponseList => data.object === 'list';
|
||||||
|
|
||||||
|
@ -24,7 +25,7 @@ function transform<T> (data: FractalResponseData | FractalResponseList | undefin
|
||||||
export class AdminTransformers {
|
export class AdminTransformers {
|
||||||
static toServer = ({ attributes }: FractalResponseData): Server => {
|
static toServer = ({ attributes }: FractalResponseData): Server => {
|
||||||
const { oom_disabled, ...limits } = attributes.limits;
|
const { oom_disabled, ...limits } = attributes.limits;
|
||||||
const { allocations, egg, node, user, variables } = attributes.relationships || {};
|
const { allocations, egg, nest, node, user, variables } = attributes.relationships || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: attributes.id,
|
id: attributes.id,
|
||||||
|
@ -38,6 +39,7 @@ export class AdminTransformers {
|
||||||
nodeId: attributes.node_id,
|
nodeId: attributes.node_id,
|
||||||
allocationId: attributes.allocation_id,
|
allocationId: attributes.allocation_id,
|
||||||
eggId: attributes.egg_id,
|
eggId: attributes.egg_id,
|
||||||
|
nestId: attributes.nest_id,
|
||||||
limits: { ...limits, oomDisabled: oom_disabled },
|
limits: { ...limits, oomDisabled: oom_disabled },
|
||||||
featureLimits: attributes.feature_limits,
|
featureLimits: attributes.feature_limits,
|
||||||
container: attributes.container,
|
container: attributes.container,
|
||||||
|
@ -45,6 +47,7 @@ export class AdminTransformers {
|
||||||
updatedAt: new Date(attributes.updated_at),
|
updatedAt: new Date(attributes.updated_at),
|
||||||
relationships: {
|
relationships: {
|
||||||
allocations: transform(allocations as FractalResponseList | undefined, this.toAllocation),
|
allocations: transform(allocations as FractalResponseList | undefined, this.toAllocation),
|
||||||
|
nest: transform(nest as FractalResponseData | undefined, this.toNest),
|
||||||
egg: transform(egg as FractalResponseData | undefined, this.toEgg),
|
egg: transform(egg as FractalResponseData | undefined, this.toEgg),
|
||||||
node: transform(node as FractalResponseData | undefined, this.toNode),
|
node: transform(node as FractalResponseData | undefined, this.toNode),
|
||||||
user: transform(user as FractalResponseData | undefined, this.toUser),
|
user: transform(user as FractalResponseData | undefined, this.toUser),
|
||||||
|
@ -132,7 +135,26 @@ export class AdminTransformers {
|
||||||
static toEgg = ({ attributes }: FractalResponseData): Egg => ({
|
static toEgg = ({ attributes }: FractalResponseData): Egg => ({
|
||||||
id: attributes.id,
|
id: attributes.id,
|
||||||
uuid: attributes.uuid,
|
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: {
|
relationships: {
|
||||||
|
nest: transform(attributes.relationships?.nest as FractalResponseData, this.toNest),
|
||||||
variables: transform(attributes.relationships?.variables as FractalResponseList, this.toEggVariable),
|
variables: transform(attributes.relationships?.variables as FractalResponseList, this.toEggVariable),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -174,4 +196,17 @@ export class AdminTransformers {
|
||||||
return !this.alias ? raw : `${this.alias} (${raw})`;
|
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),
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Model, UUID } from '@/api/admin/index';
|
import { Model, UUID } from '@/api/admin/index';
|
||||||
import { Server } from '@/api/admin/server';
|
import { Server } from '@/api/admin/server';
|
||||||
import http from '@/api/http';
|
import http, { QueryBuilderParams, withQueryBuilderParams } from '@/api/http';
|
||||||
import { AdminTransformers } from '@/api/admin/transformers';
|
import { AdminTransformers } from '@/api/admin/transformers';
|
||||||
|
|
||||||
export interface User extends Model {
|
export interface User extends Model {
|
||||||
|
@ -34,3 +34,11 @@ export const getUser = async (id: string | number): Promise<User> => {
|
||||||
|
|
||||||
return AdminTransformers.toUser(data.data);
|
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(','),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -21,13 +21,20 @@ export const SubNavigation = styled.div`
|
||||||
interface Props {
|
interface Props {
|
||||||
to: string;
|
to: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon: React.ComponentType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SubNavigationLink = ({ to, name, icon: IconComponent }: Props) => {
|
interface PropsWithIcon extends Props {
|
||||||
return (
|
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>
|
||||||
<IconComponent css={tw`w-6 h-6 mr-2`}/>{name}
|
{IconComponent ? <IconComponent/> : children}{name}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
|
@ -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;
|
||||||
const [ eggs, setEggs ] = useState<Egg[]>([]);
|
onEggSelect: (egg: Egg | null) => void;
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
export default ({ nestId, selectedEggId, onEggSelect }: Props) => {
|
||||||
const newVariables = newEgg?.relations.variables;
|
const [ , , { setValue, setTouched } ] = useField<Record<string, string | undefined>>('environment');
|
||||||
newVariables?.forEach(v => setFieldValue('environment.' + v.envVariable, ''));
|
const [ eggs, setEggs ] = useState<WithRelationships<Egg, 'variables'>[] | null>(null);
|
||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -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
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Egg, EggVariable } from '@/api/admin/egg';
|
import { Egg, EggVariable, getEgg } from '@/api/admin/egg';
|
||||||
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 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';
|
||||||
|
@ -16,6 +16,7 @@ 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 { 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();
|
||||||
|
@ -59,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>
|
||||||
);
|
);
|
||||||
|
@ -106,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();
|
||||||
|
|
||||||
|
@ -157,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,9 +172,9 @@ function ServerStartupForm ({ egg, setEgg, server }: { egg: Egg | null, setEgg:
|
||||||
}
|
}
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { data: server, mutate } = useServerFromRoute();
|
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<Egg | null>(null);
|
const [ egg, setEgg ] = useState<InferModel<typeof getEgg> | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!server) return;
|
if (!server) return;
|
||||||
|
@ -213,7 +206,7 @@ export default () => {
|
||||||
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,
|
||||||
|
@ -223,6 +216,7 @@ export default () => {
|
||||||
>
|
>
|
||||||
<ServerStartupForm
|
<ServerStartupForm
|
||||||
egg={egg}
|
egg={egg}
|
||||||
|
// @ts-ignore
|
||||||
setEgg={setEgg}
|
setEgg={setEgg}
|
||||||
server={server}
|
server={server}
|
||||||
/>
|
/>
|
||||||
|
|
Loading…
Reference in New Issue