ui(admin): add "working" React admin ui

This commit is contained in:
Matthew Penner 2022-12-15 19:06:14 -07:00
parent d1c7494933
commit 5402584508
No known key found for this signature in database
199 changed files with 13387 additions and 151 deletions

View file

@ -0,0 +1,12 @@
import http from '@/api/http';
import { Database, rawDataToDatabase } from '@/api/admin/databases/getDatabases';
export default (name: string, host: string, port: number, username: string, password: string, include: string[] = []): Promise<Database> => {
return new Promise((resolve, reject) => {
http.post('/api/application/databases', {
name, host, port, username, password,
}, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToDatabase(data)))
.catch(reject);
});
};

View file

@ -0,0 +1,9 @@
import http from '@/api/http';
export default (id: number): Promise<void> => {
return new Promise((resolve, reject) => {
http.delete(`/api/application/databases/${id}`)
.then(() => resolve())
.catch(reject);
});
};

View file

@ -0,0 +1,10 @@
import http from '@/api/http';
import { Database, rawDataToDatabase } from '@/api/admin/databases/getDatabases';
export default (id: number, include: string[] = []): Promise<Database> => {
return new Promise((resolve, reject) => {
http.get(`/api/application/databases/${id}`, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToDatabase(data)))
.catch(reject);
});
};

View file

@ -0,0 +1,64 @@
import http, { FractalResponseData, getPaginationSet, PaginatedResult } from '@/api/http';
import { useContext } from 'react';
import useSWR from 'swr';
import { createContext } from '@/api/admin';
export interface Database {
id: number;
name: string;
host: string;
port: number;
username: string;
maxDatabases: number;
createdAt: Date;
updatedAt: Date;
getAddress (): string;
}
export const rawDataToDatabase = ({ attributes }: FractalResponseData): Database => ({
id: attributes.id,
name: attributes.name,
host: attributes.host,
port: attributes.port,
username: attributes.username,
maxDatabases: attributes.max_databases,
createdAt: new Date(attributes.created_at),
updatedAt: new Date(attributes.updated_at),
getAddress: () => `${attributes.host}:${attributes.port}`,
});
export interface Filters {
id?: string;
name?: string;
host?: string;
}
export const Context = createContext<Filters>();
export default (include: string[] = []) => {
const { page, filters, sort, sortDirection } = useContext(Context);
const params = {};
if (filters !== null) {
Object.keys(filters).forEach(key => {
// @ts-ignore
params['filter[' + key + ']'] = filters[key];
});
}
if (sort !== null) {
// @ts-ignore
params.sort = (sortDirection ? '-' : '') + sort;
}
return useSWR<PaginatedResult<Database>>([ 'databases', page, filters, sort, sortDirection ], async () => {
const { data } = await http.get('/api/application/databases', { params: { include: include.join(','), page, ...params } });
return ({
items: (data.data || []).map(rawDataToDatabase),
pagination: getPaginationSet(data.meta.pagination),
});
});
};

View file

@ -0,0 +1,25 @@
import http from '@/api/http';
import { Database, rawDataToDatabase } from '@/api/admin/databases/getDatabases';
interface Filters {
name?: string;
host?: string;
}
export default (filters?: Filters): Promise<Database[]> => {
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/databases', { params })
.then(response => resolve(
(response.data.data || []).map(rawDataToDatabase)
))
.catch(reject);
});
};

View file

@ -0,0 +1,12 @@
import http from '@/api/http';
import { Database, rawDataToDatabase } from '@/api/admin/databases/getDatabases';
export default (id: number, name: string, host: string, port: number, username: string, password: string | undefined, include: string[] = []): Promise<Database> => {
return new Promise((resolve, reject) => {
http.patch(`/api/application/databases/${id}`, {
name, host, port, username, password,
}, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToDatabase(data)))
.catch(reject);
});
};

View file

@ -0,0 +1,104 @@
import type { AxiosError } from 'axios';
import { useParams } from 'react-router-dom';
import type { SWRResponse } from 'swr';
import useSWR from 'swr';
import type { Model, UUID, WithRelationships } from '@/api/admin/index';
import { withRelationships } from '@/api/admin/index';
import type { Nest } from '@/api/admin/nest';
import type { QueryBuilderParams } from '@/api/http';
import http, { withQueryBuilderParams } from '@/api/http';
import { Transformers } from '@definitions/admin';
export interface Egg extends Model {
id: number;
uuid: UUID;
nestId: number;
author: string;
name: string;
description: string | null;
features: string[] | null;
dockerImages: Record<string, 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;
}
/**
* A standard API response with the minimum viable details for the frontend
* to correctly render a egg.
*/
type LoadedEgg = WithRelationships<Egg, 'nest' | 'variables'>;
/**
* Gets a single egg from the database and returns it.
*/
export const getEgg = async (id: number | string): Promise<LoadedEgg> => {
const { data } = await http.get(`/api/application/eggs/${id}`, {
params: {
include: ['nest', 'variables'],
},
});
return withRelationships(Transformers.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(Transformers.toEgg);
};
export const exportEgg = async (eggId: number): Promise<Record<string, any>> => {
const { data } = await http.get(`/api/application/eggs/${eggId}/export`);
return data;
};
/**
* Returns an SWR instance by automatically loading in the server for the currently
* loaded route match in the admin area.
*/
export const useEggFromRoute = (): SWRResponse<LoadedEgg, AxiosError> => {
const params = useParams<'id'>();
return useSWR(`/api/application/eggs/${params.id}`, async () => getEgg(Number(params.id)), {
revalidateOnMount: false,
revalidateOnFocus: false,
});
};

View file

@ -0,0 +1,31 @@
import http from '@/api/http';
import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg';
type Egg2 = Omit<Omit<Partial<Egg>, 'configFiles'>, 'configStartup'> & { configFiles: string, configStartup: string };
export default (egg: Partial<Egg2>): Promise<Egg> => {
return new Promise((resolve, reject) => {
http.post(
'/api/application/eggs',
{
nest_id: egg.nestId,
name: egg.name,
description: egg.description,
features: egg.features,
docker_images: egg.dockerImages,
config_files: egg.configFiles,
config_startup: egg.configStartup,
config_stop: egg.configStop,
config_from: egg.configFrom,
startup: egg.startup,
script_container: egg.scriptContainer,
copy_script_from: egg.copyScriptFrom,
script_entry: egg.scriptEntry,
script_is_privileged: egg.scriptIsPrivileged,
script_install: egg.scriptInstall,
},
)
.then(({ data }) => resolve(rawDataToEgg(data)))
.catch(reject);
});
};

View file

@ -0,0 +1,22 @@
import http from '@/api/http';
import { EggVariable } from '@/api/admin/egg';
import { Transformers } from '@definitions/admin';
export type CreateEggVariable = Omit<EggVariable, 'id' | 'eggId' | 'createdAt' | 'updatedAt' | 'relationships'>;
export default async (eggId: number, variable: CreateEggVariable): Promise<EggVariable> => {
const { data } = await http.post(
`/api/application/eggs/${eggId}/variables`,
{
name: variable.name,
description: variable.description,
env_variable: variable.environmentVariable,
default_value: variable.defaultValue,
user_viewable: variable.isUserViewable,
user_editable: variable.isUserEditable,
rules: variable.rules,
},
);
return Transformers.toEggVariable(data);
};

View file

@ -0,0 +1,9 @@
import http from '@/api/http';
export default (id: number): Promise<void> => {
return new Promise((resolve, reject) => {
http.delete(`/api/application/eggs/${id}`)
.then(() => resolve())
.catch(reject);
});
};

View file

@ -0,0 +1,9 @@
import http from '@/api/http';
export default (eggId: number, variableId: number): Promise<void> => {
return new Promise((resolve, reject) => {
http.delete(`/api/application/eggs/${eggId}/variables/${variableId}`)
.then(() => resolve())
.catch(reject);
});
};

View file

@ -0,0 +1,108 @@
import { Nest } from '@/api/admin/nests/getNests';
import { rawDataToServer, Server } from '@/api/admin/servers/getServers';
import http, { FractalResponseData, FractalResponseList } from '@/api/http';
import useSWR from 'swr';
export interface EggVariable {
id: number;
eggId: number;
name: string;
description: string;
envVariable: string;
defaultValue: string;
userViewable: boolean;
userEditable: boolean;
rules: string;
createdAt: Date;
updatedAt: Date;
}
export const rawDataToEggVariable = ({ attributes }: FractalResponseData): EggVariable => ({
id: attributes.id,
eggId: attributes.egg_id,
name: attributes.name,
description: attributes.description,
envVariable: attributes.env_variable,
defaultValue: attributes.default_value,
userViewable: attributes.user_viewable,
userEditable: attributes.user_editable,
rules: attributes.rules,
createdAt: new Date(attributes.created_at),
updatedAt: new Date(attributes.updated_at),
});
export interface Egg {
id: number;
uuid: string;
nestId: number;
author: string;
name: string;
description: string | null;
features: string[] | null;
dockerImages: Record<string, 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;
relations: {
nest?: Nest;
servers?: Server[];
variables?: EggVariable[];
};
}
export const rawDataToEgg = ({ 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),
relations: {
nest: undefined,
servers: ((attributes.relationships?.servers as FractalResponseList | undefined)?.data || []).map(
rawDataToServer,
),
variables: ((attributes.relationships?.variables as FractalResponseList | undefined)?.data || []).map(
rawDataToEggVariable,
),
},
});
export const getEgg = async (id: number): Promise<Egg> => {
const { data } = await http.get(`/api/application/eggs/${id}`, { params: { include: ['variables'] } });
return rawDataToEgg(data);
};
export default (id: number) => {
return useSWR<Egg>(`egg:${id}`, async () => {
const { data } = await http.get(`/api/application/eggs/${id}`, { params: { include: ['variables'] } });
return rawDataToEgg(data);
});
};

View file

@ -0,0 +1,31 @@
import http from '@/api/http';
import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg';
type Egg2 = Omit<Omit<Partial<Egg>, 'configFiles'>, 'configStartup'> & { configFiles?: string, configStartup?: string };
export default (id: number, egg: Partial<Egg2>): Promise<Egg> => {
return new Promise((resolve, reject) => {
http.patch(
`/api/application/eggs/${id}`,
{
nest_id: egg.nestId,
name: egg.name,
description: egg.description,
features: egg.features,
docker_images: egg.dockerImages,
config_files: egg.configFiles,
config_startup: egg.configStartup,
config_stop: egg.configStop,
config_from: egg.configFrom,
startup: egg.startup,
script_container: egg.scriptContainer,
copy_script_from: egg.copyScriptFrom,
script_entry: egg.scriptEntry,
script_is_privileged: egg.scriptIsPrivileged,
script_install: egg.scriptInstall,
},
)
.then(({ data }) => resolve(rawDataToEgg(data)))
.catch(reject);
});
};

View file

@ -0,0 +1,21 @@
import http from '@/api/http';
import { EggVariable } from '@/api/admin/egg';
import { Transformers } from '@definitions/admin';
export default async (eggId: number, variables: Omit<EggVariable, 'eggId' | 'createdAt' | 'updatedAt'>[]): Promise<EggVariable[]> => {
const { data } = await http.patch(
`/api/application/eggs/${eggId}/variables`,
variables.map(variable => ({
id: variable.id,
name: variable.name,
description: variable.description,
env_variable: variable.environmentVariable,
default_value: variable.defaultValue,
user_viewable: variable.isUserViewable,
user_editable: variable.isUserEditable,
rules: variable.rules,
})),
);
return data.data.map(Transformers.toEggVariable);
};

View file

@ -0,0 +1,22 @@
import http from '@/api/http';
export interface VersionData {
panel: {
current: string;
latest: string;
}
wings: {
latest: string;
}
git: string | null;
}
export default (): Promise<VersionData> => {
return new Promise((resolve, reject) => {
http.get('/api/application/version')
.then(({ data }) => resolve(data))
.catch(reject);
});
};

View file

@ -0,0 +1,66 @@
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> {
page: number;
setPage: (page: ((p: number) => number) | number) => void;
filters: T | null;
setFilters: (filters: ((f: T | null) => T | null) | T | null) => void;
sort: string | null;
setSort: (sort: string | null) => void;
sortDirection: boolean;
setSortDirection: (direction: ((p: boolean) => boolean) | boolean) => void;
}
function create<T> () {
return createContext<ListContext<T>>({
page: 1,
setPage: () => 1,
filters: null,
setFilters: () => null,
sort: null,
setSort: () => null,
sortDirection: false,
setSortDirection: () => false,
});
}
export { create as createContext };

View 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[];
};
}

View file

@ -0,0 +1,12 @@
import http from '@/api/http';
import { Location, rawDataToLocation } from '@/api/admin/locations/getLocations';
export default (short: string, long: string | null, include: string[] = []): Promise<Location> => {
return new Promise((resolve, reject) => {
http.post('/api/application/locations', {
short, long,
}, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToLocation(data)))
.catch(reject);
});
};

View file

@ -0,0 +1,9 @@
import http from '@/api/http';
export default (id: number): Promise<void> => {
return new Promise((resolve, reject) => {
http.delete(`/api/application/locations/${id}`)
.then(() => resolve())
.catch(reject);
});
};

View file

@ -0,0 +1,10 @@
import http from '@/api/http';
import { Location, rawDataToLocation } from '@/api/admin/locations/getLocations';
export default (id: number, include: string[] = []): Promise<Location> => {
return new Promise((resolve, reject) => {
http.get(`/api/application/locations/${id}`, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToLocation(data)))
.catch(reject);
});
};

View file

@ -0,0 +1,54 @@
import http, { FractalResponseData, getPaginationSet, PaginatedResult } from '@/api/http';
import { useContext } from 'react';
import useSWR from 'swr';
import { createContext } from '@/api/admin';
export interface Location {
id: number;
short: string;
long: string;
createdAt: Date;
updatedAt: Date;
}
export const rawDataToLocation = ({ attributes }: FractalResponseData): Location => ({
id: attributes.id,
short: attributes.short,
long: attributes.long,
createdAt: new Date(attributes.created_at),
updatedAt: new Date(attributes.updated_at),
});
export interface Filters {
id?: string;
short?: string;
long?: string;
}
export const Context = createContext<Filters>();
export default (include: string[] = []) => {
const { page, filters, sort, sortDirection } = useContext(Context);
const params = {};
if (filters !== null) {
Object.keys(filters).forEach(key => {
// @ts-ignore
params['filter[' + key + ']'] = filters[key];
});
}
if (sort !== null) {
// @ts-ignore
params.sort = (sortDirection ? '-' : '') + sort;
}
return useSWR<PaginatedResult<Location>>([ 'locations', page, filters, sort, sortDirection ], async () => {
const { data } = await http.get('/api/application/locations', { params: { include: include.join(','), page, ...params } });
return ({
items: (data.data || []).map(rawDataToLocation),
pagination: getPaginationSet(data.meta.pagination),
});
});
};

View file

@ -0,0 +1,25 @@
import http from '@/api/http';
import { Location, rawDataToLocation } from '@/api/admin/locations/getLocations';
interface Filters {
short?: string;
long?: string;
}
export default (filters?: Filters): Promise<Location[]> => {
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/locations', { params })
.then(response => resolve(
(response.data.data || []).map(rawDataToLocation)
))
.catch(reject);
});
};

View file

@ -0,0 +1,12 @@
import http from '@/api/http';
import { Location, rawDataToLocation } from '@/api/admin/locations/getLocations';
export default (id: number, short: string, long: string | null, include: string[] = []): Promise<Location> => {
return new Promise((resolve, reject) => {
http.patch(`/api/application/locations/${id}`, {
short, long,
}, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToLocation(data)))
.catch(reject);
});
};

View file

@ -0,0 +1,12 @@
import http from '@/api/http';
import { Mount, rawDataToMount } from '@/api/admin/mounts/getMounts';
export default (name: string, description: string, source: string, target: string, readOnly: boolean, userMountable: boolean, include: string[] = []): Promise<Mount> => {
return new Promise((resolve, reject) => {
http.post('/api/application/mounts', {
name, description, source, target, read_only: readOnly, user_mountable: userMountable,
}, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToMount(data)))
.catch(reject);
});
};

View file

@ -0,0 +1,9 @@
import http from '@/api/http';
export default (id: number): Promise<void> => {
return new Promise((resolve, reject) => {
http.delete(`/api/application/mounts/${id}`)
.then(() => resolve())
.catch(reject);
});
};

View file

@ -0,0 +1,10 @@
import http from '@/api/http';
import { Mount, rawDataToMount } from '@/api/admin/mounts/getMounts';
export default (id: number, include: string[] = []): Promise<Mount> => {
return new Promise((resolve, reject) => {
http.get(`/api/application/mounts/${id}`, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToMount(data)))
.catch(reject);
});
};

View file

@ -0,0 +1,80 @@
import http, { FractalResponseData, FractalResponseList, getPaginationSet, PaginatedResult } from '@/api/http';
import { useContext } from 'react';
import useSWR from 'swr';
import { createContext } from '@/api/admin';
import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg';
import { Node, rawDataToNode } from '@/api/admin/nodes/getNodes';
import { Server, rawDataToServer } from '@/api/admin/servers/getServers';
export interface Mount {
id: number;
uuid: string;
name: string;
description?: string;
source: string;
target: string;
readOnly: boolean;
userMountable: boolean;
createdAt: Date;
updatedAt: Date;
relations: {
eggs: Egg[] | undefined;
nodes: Node[] | undefined;
servers: Server[] | undefined;
};
}
export const rawDataToMount = ({ attributes }: FractalResponseData): Mount => ({
id: attributes.id,
uuid: attributes.uuid,
name: attributes.name,
description: attributes.description,
source: attributes.source,
target: attributes.target,
readOnly: attributes.read_only,
userMountable: attributes.user_mountable,
createdAt: new Date(attributes.created_at),
updatedAt: new Date(attributes.updated_at),
relations: {
eggs: ((attributes.relationships?.eggs as FractalResponseList | undefined)?.data || []).map(rawDataToEgg),
nodes: ((attributes.relationships?.nodes as FractalResponseList | undefined)?.data || []).map(rawDataToNode),
servers: ((attributes.relationships?.servers as FractalResponseList | undefined)?.data || []).map(rawDataToServer),
},
});
export interface Filters {
id?: string;
name?: string;
source?: string;
target?: string;
}
export const Context = createContext<Filters>();
export default (include: string[] = []) => {
const { page, filters, sort, sortDirection } = useContext(Context);
const params = {};
if (filters !== null) {
Object.keys(filters).forEach(key => {
// @ts-ignore
params['filter[' + key + ']'] = filters[key];
});
}
if (sort !== null) {
// @ts-ignore
params.sort = (sortDirection ? '-' : '') + sort;
}
return useSWR<PaginatedResult<Mount>>([ 'mounts', page, filters, sort, sortDirection ], async () => {
const { data } = await http.get('/api/application/mounts', { params: { include: include.join(','), page, ...params } });
return ({
items: (data.data || []).map(rawDataToMount),
pagination: getPaginationSet(data.meta.pagination),
});
});
};

View file

@ -0,0 +1,12 @@
import http from '@/api/http';
import { Mount, rawDataToMount } from '@/api/admin/mounts/getMounts';
export default (id: number, name: string, description: string | null, source: string, target: string, readOnly: boolean, userMountable: boolean, include: string[] = []): Promise<Mount> => {
return new Promise((resolve, reject) => {
http.patch(`/api/application/mounts/${id}`, {
name, description, source, target, read_only: readOnly, user_mountable: userMountable,
}, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToMount(data)))
.catch(reject);
});
};

View 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 { Transformers } from '@definitions/admin';
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(Transformers.toNest);
};

View file

@ -0,0 +1,12 @@
import http from '@/api/http';
import { Nest, rawDataToNest } from '@/api/admin/nests/getNests';
export default (name: string, description: string | null, include: string[] = []): Promise<Nest> => {
return new Promise((resolve, reject) => {
http.post('/api/application/nests', {
name, description,
}, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToNest(data)))
.catch(reject);
});
};

View file

@ -0,0 +1,9 @@
import http from '@/api/http';
export default (id: number): Promise<void> => {
return new Promise((resolve, reject) => {
http.delete(`/api/application/nests/${id}`)
.then(() => resolve())
.catch(reject);
});
};

View file

@ -0,0 +1,38 @@
import http, { getPaginationSet, PaginatedResult } from '@/api/http';
import { useContext } from 'react';
import useSWR from 'swr';
import { createContext } from '@/api/admin';
import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg';
export interface Filters {
id?: string;
name?: string;
}
export const Context = createContext<Filters>();
export default (nestId: number, include: string[] = []) => {
const { page, filters, sort, sortDirection } = useContext(Context);
const params = {};
if (filters !== null) {
Object.keys(filters).forEach(key => {
// @ts-ignore
params['filter[' + key + ']'] = filters[key];
});
}
if (sort !== null) {
// @ts-ignore
params.sort = (sortDirection ? '-' : '') + sort;
}
return useSWR<PaginatedResult<Egg>>([ nestId, 'eggs', page, filters, sort, sortDirection ], async () => {
const { data } = await http.get(`/api/application/nests/${nestId}/eggs`, { params: { include: include.join(','), page, ...params } });
return ({
items: (data.data || []).map(rawDataToEgg),
pagination: getPaginationSet(data.meta.pagination),
});
});
};

View file

@ -0,0 +1,10 @@
import http from '@/api/http';
import { Nest, rawDataToNest } from '@/api/admin/nests/getNests';
export default (id: number, include: string[]): Promise<Nest> => {
return new Promise((resolve, reject) => {
http.get(`/api/application/nests/${id}`, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToNest(data)))
.catch(reject);
});
};

View file

@ -0,0 +1,66 @@
import http, { FractalResponseData, FractalResponseList, getPaginationSet, PaginatedResult } from '@/api/http';
import { useContext } from 'react';
import useSWR from 'swr';
import { createContext } from '@/api/admin';
import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg';
export interface Nest {
id: number;
uuid: string;
author: string;
name: string;
description?: string;
createdAt: Date;
updatedAt: Date;
relations: {
eggs: Egg[] | undefined;
},
}
export const rawDataToNest = ({ 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),
relations: {
eggs: ((attributes.relationships?.eggs as FractalResponseList | undefined)?.data || []).map(rawDataToEgg),
},
});
export interface Filters {
id?: string;
name?: string;
}
export const Context = createContext<Filters>();
export default (include: string[] = []) => {
const { page, filters, sort, sortDirection } = useContext(Context);
const params = {};
if (filters !== null) {
Object.keys(filters).forEach(key => {
// @ts-ignore
params['filter[' + key + ']'] = filters[key];
});
}
if (sort !== null) {
// @ts-ignore
params.sort = (sortDirection ? '-' : '') + sort;
}
return useSWR<PaginatedResult<Nest>>([ 'nests', page, filters, sort, sortDirection ], async () => {
const { data } = await http.get('/api/application/nests', { params: { include: include.join(','), page, ...params } });
return ({
items: (data.data || []).map(rawDataToNest),
pagination: getPaginationSet(data.meta.pagination),
});
});
};

View file

@ -0,0 +1,17 @@
import http from '@/api/http';
import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg';
export default (id: number, content: any, type = 'application/json', include: string[] = []): Promise<Egg> => {
return new Promise((resolve, reject) => {
http.post(`/api/application/nests/${id}/import`, content, {
headers: {
'Content-Type': type,
},
params: {
include: include.join(','),
},
})
.then(({ data }) => resolve(rawDataToEgg(data)))
.catch(reject);
});
};

View file

@ -0,0 +1,12 @@
import http from '@/api/http';
import { Nest, rawDataToNest } from '@/api/admin/nests/getNests';
export default (id: number, name: string, description: string | null, include: string[] = []): Promise<Nest> => {
return new Promise((resolve, reject) => {
http.patch(`/api/application/nests/${id}`, {
name, description,
}, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToNest(data)))
.catch(reject);
});
};

View file

@ -0,0 +1,84 @@
import { Model, UUID, WithRelationships, withRelationships } from '@/api/admin/index';
import { Location } from '@/api/admin/location';
import http, { QueryBuilderParams, withQueryBuilderParams } from '@/api/http';
import { Transformers } from '@definitions/admin';
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(Transformers.toNode(data.data), 'location');
};
export const searchNodes = async (params: QueryBuilderParams<'name'>): Promise<Node[]> => {
const { data } = await http.get('/api/application/nodes', {
params: withQueryBuilderParams(params),
});
return data.data.map(Transformers.toNode);
};
export const getAllocations = async (id: string | number, params?: QueryBuilderParams<'ip' | 'server_id'>): Promise<Allocation[]> => {
const { data } = await http.get(`/api/application/nodes/${id}/allocations`, {
params: withQueryBuilderParams(params),
});
return data.data.map(Transformers.toAllocation);
};

View file

@ -0,0 +1,16 @@
import http from '@/api/http';
import { Allocation, rawDataToAllocation } from '@/api/admin/nodes/getAllocations';
export interface Values {
ip: string;
ports: number[];
alias?: string;
}
export default (id: string | number, values: Values, include: string[] = []): Promise<Allocation[]> => {
return new Promise((resolve, reject) => {
http.post(`/api/application/nodes/${id}/allocations`, values, { params: { include: include.join(',') } })
.then(({ data }) => resolve((data || []).map(rawDataToAllocation)))
.catch(reject);
});
};

View file

@ -0,0 +1,9 @@
import http from '@/api/http';
export default (nodeId: number, allocationId: number): Promise<void> => {
return new Promise((resolve, reject) => {
http.delete(`/api/application/nodes/${nodeId}/allocations/${allocationId}`)
.then(() => resolve())
.catch(reject);
});
};

View file

@ -0,0 +1,39 @@
import { Allocation, rawDataToAllocation } from '@/api/admin/nodes/getAllocations';
import http, { getPaginationSet, PaginatedResult } from '@/api/http';
import { useContext } from 'react';
import useSWR from 'swr';
import { createContext } from '@/api/admin';
export interface Filters {
id?: string;
ip?: string;
port?: string;
}
export const Context = createContext<Filters>();
export default (id: number, include: string[] = []) => {
const { page, filters, sort, sortDirection } = useContext(Context);
const params = {};
if (filters !== null) {
Object.keys(filters).forEach(key => {
// @ts-ignore
params['filter[' + key + ']'] = filters[key];
});
}
if (sort !== null) {
// @ts-ignore
params.sort = (sortDirection ? '-' : '') + sort;
}
return useSWR<PaginatedResult<Allocation>>([ 'allocations', page, filters, sort, sortDirection ], async () => {
const { data } = await http.get(`/api/application/nodes/${id}/allocations`, { params: { include: include.join(','), page, ...params } });
return ({
items: (data.data || []).map(rawDataToAllocation),
pagination: getPaginationSet(data.meta.pagination),
});
});
};

View file

@ -0,0 +1,42 @@
import http from '@/api/http';
import { Node, rawDataToNode } from '@/api/admin/nodes/getNodes';
export interface Values {
name: string;
locationId: number;
databaseHostId: number | null;
fqdn: string;
scheme: string;
behindProxy: boolean;
public: boolean;
daemonBase: string;
memory: number;
memoryOverallocate: number;
disk: number;
diskOverallocate: number;
listenPortHTTP: number;
publicPortHTTP: number;
listenPortSFTP: number;
publicPortSFTP: number;
}
export default (values: Values, include: string[] = []): Promise<Node> => {
const data = {};
Object.keys(values).forEach((key) => {
const key2 = key
.replace('HTTP', 'Http')
.replace('SFTP', 'Sftp')
.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
// @ts-ignore
data[key2] = values[key];
});
return new Promise((resolve, reject) => {
http.post('/api/application/nodes', data, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToNode(data)))
.catch(reject);
});
};

View file

@ -0,0 +1,9 @@
import http from '@/api/http';
export default (id: number): Promise<void> => {
return new Promise((resolve, reject) => {
http.delete(`/api/application/nodes/${id}`)
.then(() => resolve())
.catch(reject);
});
};

View file

@ -0,0 +1,61 @@
import http, { FractalResponseData } from '@/api/http';
import { rawDataToServer, Server } from '@/api/admin/servers/getServers';
export interface Allocation {
id: number;
ip: string;
port: number;
alias: string | null;
serverId: number | null;
assigned: boolean;
relations: {
server?: Server;
}
getDisplayText (): string;
}
export const rawDataToAllocation = ({ attributes }: FractalResponseData): Allocation => ({
id: attributes.id,
ip: attributes.ip,
port: attributes.port,
alias: attributes.alias || null,
serverId: attributes.server_id,
assigned: attributes.assigned,
relations: {
server: attributes.relationships?.server?.object === 'server' ? rawDataToServer(attributes.relationships.server as FractalResponseData) : undefined,
},
// TODO: If IP is an IPv6, wrap IP in [].
getDisplayText (): string {
if (attributes.alias !== null) {
return `${attributes.ip}:${attributes.port} (${attributes.alias})`;
}
return `${attributes.ip}:${attributes.port}`;
},
});
export interface Filters {
ip?: string
/* eslint-disable camelcase */
server_id?: string;
/* eslint-enable camelcase */
}
export default (id: string | number, filters: Filters = {}, include: string[] = []): Promise<Allocation[]> => {
const params = {};
if (filters !== null) {
Object.keys(filters).forEach(key => {
// @ts-ignore
params['filter[' + key + ']'] = filters[key];
});
}
return new Promise((resolve, reject) => {
http.get(`/api/application/nodes/${id}/allocations`, { params: { include: include.join(','), ...params } })
.then(({ data }) => resolve((data.data || []).map(rawDataToAllocation)))
.catch(reject);
});
};

View file

@ -0,0 +1,10 @@
import http from '@/api/http';
import { Node, rawDataToNode } from '@/api/admin/nodes/getNodes';
export default (id: number, include: string[] = []): Promise<Node> => {
return new Promise((resolve, reject) => {
http.get(`/api/application/nodes/${id}`, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToNode(data)))
.catch(reject);
});
};

View file

@ -0,0 +1,9 @@
import http from '@/api/http';
export default (id: number): Promise<string> => {
return new Promise((resolve, reject) => {
http.get(`/api/application/nodes/${id}/configuration?format=yaml`)
.then(({ data }) => resolve(data))
.catch(reject);
});
};

View file

@ -0,0 +1,19 @@
import http from '@/api/http';
export interface NodeInformation {
version: string;
system: {
type: string;
arch: string;
release: string;
cpus: number;
};
}
export default (id: number): Promise<NodeInformation> => {
return new Promise((resolve, reject) => {
http.get(`/api/application/nodes/${id}/information`)
.then(({ data }) => resolve(data))
.catch(reject);
});
};

View file

@ -0,0 +1,107 @@
import http, { FractalResponseData, getPaginationSet, PaginatedResult } from '@/api/http';
import { useContext } from 'react';
import useSWR from 'swr';
import { createContext } from '@/api/admin';
import { Database, rawDataToDatabase } from '@/api/admin/databases/getDatabases';
import { Location, rawDataToLocation } from '@/api/admin/locations/getLocations';
export interface Node {
id: number;
uuid: string;
public: boolean;
name: string;
description: string | null;
locationId: number;
databaseHostId: number | null;
fqdn: string;
listenPortHTTP: number;
publicPortHTTP: number;
listenPortSFTP: number;
publicPortSFTP: number;
scheme: string;
behindProxy: boolean;
maintenanceMode: boolean;
memory: number;
memoryOverallocate: number;
disk: number;
diskOverallocate: number;
uploadSize: number;
daemonBase: string;
createdAt: Date;
updatedAt: Date;
relations: {
databaseHost: Database | undefined;
location: Location | undefined;
}
}
export const rawDataToNode = ({ attributes }: FractalResponseData): Node => ({
id: attributes.id,
uuid: attributes.uuid,
public: attributes.public,
name: attributes.name,
description: attributes.description,
locationId: attributes.location_id,
databaseHostId: attributes.database_host_id,
fqdn: attributes.fqdn,
listenPortHTTP: attributes.listen_port_http,
publicPortHTTP: attributes.public_port_http,
listenPortSFTP: attributes.listen_port_sftp,
publicPortSFTP: attributes.public_port_sftp,
scheme: attributes.scheme,
behindProxy: attributes.behind_proxy,
maintenanceMode: attributes.maintenance_mode,
memory: attributes.memory,
memoryOverallocate: attributes.memory_overallocate,
disk: attributes.disk,
diskOverallocate: attributes.disk_overallocate,
uploadSize: attributes.upload_size,
daemonBase: attributes.daemon_base,
createdAt: new Date(attributes.created_at),
updatedAt: new Date(attributes.updated_at),
relations: {
// eslint-disable-next-line camelcase
databaseHost: attributes.relationships?.database_host !== undefined && attributes.relationships?.database_host.object !== 'null_resource' ? rawDataToDatabase(attributes.relationships.database_host as FractalResponseData) : undefined,
location: attributes.relationships?.location !== undefined ? rawDataToLocation(attributes.relationships.location as FractalResponseData) : undefined,
},
});
export interface Filters {
id?: string;
uuid?: string;
name?: string;
image?: string;
/* eslint-disable camelcase */
external_id?: string;
/* eslint-enable camelcase */
}
export const Context = createContext<Filters>();
export default (include: string[] = []) => {
const { page, filters, sort, sortDirection } = useContext(Context);
const params = {};
if (filters !== null) {
Object.keys(filters).forEach(key => {
// @ts-ignore
params['filter[' + key + ']'] = filters[key];
});
}
if (sort !== null) {
// @ts-ignore
params.sort = (sortDirection ? '-' : '') + sort;
}
return useSWR<PaginatedResult<Node>>([ 'nodes', page, filters, sort, sortDirection ], async () => {
const { data } = await http.get('/api/application/nodes', { params: { include: include.join(','), page, ...params } });
return ({
items: (data.data || []).map(rawDataToNode),
pagination: getPaginationSet(data.meta.pagination),
});
});
};

View file

@ -0,0 +1,21 @@
import http from '@/api/http';
import { Node, rawDataToNode } from '@/api/admin/nodes/getNodes';
export default (id: number, node: Partial<Node>, include: string[] = []): Promise<Node> => {
const data = {};
Object.keys(node).forEach((key) => {
const key2 = key
.replace('HTTP', 'Http')
.replace('SFTP', 'Sftp')
.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
// @ts-ignore
data[key2] = node[key];
});
return new Promise((resolve, reject) => {
http.patch(`/api/application/nodes/${id}`, data, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToNode(data)))
.catch(reject);
});
};

View file

@ -0,0 +1,103 @@
import http, { getPaginationSet, PaginatedResult } from '@/api/http';
import { Transformers, UserRole } from '@definitions/admin';
import { useContext } from 'react';
import useSWR from 'swr';
import { createContext } from '@/api/admin/index';
export interface Filters {
id?: string;
name?: string;
}
export const Context = createContext<Filters>();
const createRole = (name: string, description: string | null, include: string[] = []): Promise<UserRole> => {
return new Promise((resolve, reject) => {
http.post('/api/application/roles', {
name, description,
}, { params: { include: include.join(',') } })
.then(({ data }) => resolve(Transformers.toUserRole(data)))
.catch(reject);
});
};
const deleteRole = (id: number): Promise<void> => {
return new Promise((resolve, reject) => {
http.delete(`/api/application/roles/${id}`)
.then(() => resolve())
.catch(reject);
});
};
const getRole = (id: number, include: string[] = []): Promise<UserRole> => {
return new Promise((resolve, reject) => {
http.get(`/api/application/roles/${id}`, { params: { include: include.join(',') } })
.then(({ data }) => resolve(Transformers.toUserRole(data)))
.catch(reject);
});
};
const searchRoles = (filters?: { name?: string }): Promise<UserRole[]> => {
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/roles', { params })
.then(response => resolve(
(response.data.data || []).map(Transformers.toUserRole)
))
.catch(reject);
});
};
const updateRole = (id: number, name: string, description: string | null, include: string[] = []): Promise<UserRole> => {
return new Promise((resolve, reject) => {
http.patch(`/api/application/roles/${id}`, {
name, description,
}, { params: { include: include.join(',') } })
.then(({ data }) => resolve(Transformers.toUserRole(data)))
.catch(reject);
});
};
const getRoles = (include: string[] = []) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { page, filters, sort, sortDirection } = useContext(Context);
const params = {};
if (filters !== null) {
Object.keys(filters).forEach(key => {
// @ts-ignore
params['filter[' + key + ']'] = filters[key];
});
}
if (sort !== null) {
// @ts-ignore
params.sort = (sortDirection ? '-' : '') + sort;
}
// eslint-disable-next-line react-hooks/rules-of-hooks
return useSWR<PaginatedResult<UserRole>>([ 'roles', page, filters, sort, sortDirection ], async () => {
const { data } = await http.get('/api/application/roles', { params: { include: include.join(','), page, ...params } });
return ({
items: (data.data || []).map(Transformers.toUserRole),
pagination: getPaginationSet(data.meta.pagination),
});
});
};
export {
createRole,
deleteRole,
getRole,
searchRoles,
updateRole,
getRoles,
};

View file

@ -0,0 +1,99 @@
import useSWR, { SWRResponse } from 'swr';
import { AxiosError } from 'axios';
import { useParams } from 'react-router-dom';
import http from '@/api/http';
import { Model, UUID, withRelationships, WithRelationships } from '@/api/admin/index';
import { Allocation, Node } from '@/api/admin/node';
import { Transformers, User } from '@definitions/admin';
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 | null;
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', 'variables'],
},
});
return withRelationships(Transformers.toServer(data), 'allocations', 'user', 'node', 'variables');
};
/**
* 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 = useParams<'id'>();
return useSWR(`/api/application/servers/${params.id}`, async () => getServer(Number(params.id)), {
revalidateOnMount: false,
revalidateOnFocus: false,
});
};

View file

@ -0,0 +1,80 @@
import http from '@/api/http';
import { Server, rawDataToServer } from '@/api/admin/servers/getServers';
export interface CreateServerRequest {
externalId: string;
name: string;
description: string | null;
ownerId: number;
nodeId: number;
limits: {
memory: number;
swap: number;
disk: number;
io: number;
cpu: number;
threads: string;
oomDisabled: boolean;
}
featureLimits: {
allocations: number;
backups: number;
databases: number;
};
allocation: {
default: number;
additional: number[];
};
startup: string;
environment: Record<string, any>;
eggId: number;
image: string;
skipScripts: boolean;
startOnCompletion: boolean;
}
export default (r: CreateServerRequest, include: string[] = []): Promise<Server> => {
return new Promise((resolve, reject) => {
http.post('/api/application/servers', {
externalId: r.externalId,
name: r.name,
description: r.description,
owner_id: r.ownerId,
node_id: r.nodeId,
limits: {
cpu: r.limits.cpu,
disk: r.limits.disk,
io: r.limits.io,
memory: r.limits.memory,
swap: r.limits.swap,
threads: r.limits.threads,
oom_killer: r.limits.oomDisabled,
},
feature_limits: {
allocations: r.featureLimits.allocations,
backups: r.featureLimits.backups,
databases: r.featureLimits.databases,
},
allocation: {
default: r.allocation.default,
additional: r.allocation.additional,
},
startup: r.startup,
environment: r.environment,
egg_id: r.eggId,
image: r.image,
skip_scripts: r.skipScripts,
start_on_completion: r.startOnCompletion,
}, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToServer(data)))
.catch(reject);
});
};

View file

@ -0,0 +1,9 @@
import http from '@/api/http';
export default (id: number): Promise<void> => {
return new Promise((resolve, reject) => {
http.delete(`/api/application/servers/${id}`)
.then(() => resolve())
.catch(reject);
});
};

View file

@ -0,0 +1,10 @@
import http from '@/api/http';
import { Server, rawDataToServer } from '@/api/admin/servers/getServers';
export default (id: number, include: string[]): Promise<Server> => {
return new Promise((resolve, reject) => {
http.get(`/api/application/servers/${id}`, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToServer(data)))
.catch(reject);
});
};

View file

@ -0,0 +1,177 @@
import { Allocation, rawDataToAllocation } from '@/api/admin/nodes/getAllocations';
import { useContext } from 'react';
import useSWR from 'swr';
import { createContext } from '@/api/admin';
import http, { FractalResponseData, FractalResponseList, getPaginationSet, PaginatedResult } from '@/api/http';
import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg';
import { Node, rawDataToNode } from '@/api/admin/nodes/getNodes';
import { Transformers, User } from '@definitions/admin';
export interface ServerVariable {
id: number;
eggId: number;
name: string;
description: string;
envVariable: string;
defaultValue: string;
userViewable: boolean;
userEditable: boolean;
rules: string;
required: boolean;
serverValue: string;
createdAt: Date;
updatedAt: Date;
}
export const rawDataToServerVariable = ({ attributes }: FractalResponseData): ServerVariable => ({
id: attributes.id,
eggId: attributes.egg_id,
name: attributes.name,
description: attributes.description,
envVariable: attributes.env_variable,
defaultValue: attributes.default_value,
userViewable: attributes.user_viewable,
userEditable: attributes.user_editable,
rules: attributes.rules,
required: attributes.required,
serverValue: attributes.server_value,
createdAt: new Date(attributes.created_at),
updatedAt: new Date(attributes.updated_at),
});
export interface Server {
id: number;
externalId: string | null
uuid: string;
identifier: string;
name: string;
description: string;
status: string;
limits: {
memory: number;
swap: number;
disk: number;
io: number;
cpu: number;
threads: string | null;
oomDisabled: boolean;
}
featureLimits: {
databases: number;
allocations: number;
backups: number;
}
ownerId: number;
nodeId: number;
allocationId: number;
nestId: number;
eggId: number;
container: {
startup: string;
image: string;
environment: Map<string, string>;
}
createdAt: Date;
updatedAt: Date;
relations: {
allocations?: Allocation[];
egg?: Egg;
node?: Node;
user?: User;
variables: ServerVariable[];
}
}
export const rawDataToServer = ({ attributes }: FractalResponseData): Server => ({
id: attributes.id,
externalId: attributes.external_id,
uuid: attributes.uuid,
identifier: attributes.identifier,
name: attributes.name,
description: attributes.description,
status: attributes.status,
limits: {
memory: attributes.limits.memory,
swap: attributes.limits.swap,
disk: attributes.limits.disk,
io: attributes.limits.io,
cpu: attributes.limits.cpu,
threads: attributes.limits.threads,
oomDisabled: attributes.limits.oom_disabled,
},
featureLimits: {
databases: attributes.feature_limits.databases,
allocations: attributes.feature_limits.allocations,
backups: attributes.feature_limits.backups,
},
ownerId: attributes.owner_id,
nodeId: attributes.node_id,
allocationId: attributes.allocation_id,
nestId: attributes.nest_id,
eggId: attributes.egg_id,
container: {
startup: attributes.container.startup,
image: attributes.container.image,
environment: attributes.container.environment,
},
createdAt: new Date(attributes.created_at),
updatedAt: new Date(attributes.updated_at),
relations: {
allocations: ((attributes.relationships?.allocations as FractalResponseList | undefined)?.data || []).map(rawDataToAllocation),
egg: attributes.relationships?.egg?.object === 'egg' ? rawDataToEgg(attributes.relationships.egg as FractalResponseData) : undefined,
node: attributes.relationships?.node?.object === 'node' ? rawDataToNode(attributes.relationships.node as FractalResponseData) : undefined,
user: attributes.relationships?.user?.object === 'user' ? Transformers.toUser(attributes.relationships.user as FractalResponseData) : undefined,
variables: ((attributes.relationships?.variables as FractalResponseList | undefined)?.data || []).map(rawDataToServerVariable),
},
}) as Server;
export interface Filters {
id?: string;
uuid?: string;
name?: string;
/* eslint-disable camelcase */
owner_id?: string;
node_id?: string;
external_id?: string;
/* eslint-enable camelcase */
}
export const Context = createContext<Filters>();
export default (include: string[] = []) => {
const { page, filters, sort, sortDirection } = useContext(Context);
const params = {};
if (filters !== null) {
Object.keys(filters).forEach(key => {
// @ts-ignore
params['filter[' + key + ']'] = filters[key];
});
}
if (sort !== null) {
// @ts-ignore
params.sort = (sortDirection ? '-' : '') + sort;
}
return useSWR<PaginatedResult<Server>>([ 'servers', page, filters, sort, sortDirection ], async () => {
const { data } = await http.get('/api/application/servers', { params: { include: include.join(','), page, ...params } });
return ({
items: (data.data || []).map(rawDataToServer),
pagination: getPaginationSet(data.meta.pagination),
});
});
};

View file

@ -0,0 +1,64 @@
import http from '@/api/http';
import { Server, rawDataToServer } from '@/api/admin/servers/getServers';
export interface Values {
externalId: string;
name: string;
ownerId: number;
limits: {
memory: number;
swap: number;
disk: number;
io: number;
cpu: number;
threads: string;
oomDisabled: boolean;
}
featureLimits: {
allocations: number;
backups: number;
databases: number;
}
allocationId: number;
addAllocations: number[];
removeAllocations: number[];
}
export default (id: number, server: Partial<Values>, include: string[] = []): Promise<Server> => {
return new Promise((resolve, reject) => {
http.patch(
`/api/application/servers/${id}`,
{
external_id: server.externalId,
name: server.name,
owner_id: server.ownerId,
limits: {
memory: server.limits?.memory,
swap: server.limits?.swap,
disk: server.limits?.disk,
io: server.limits?.io,
cpu: server.limits?.cpu,
threads: server.limits?.threads,
oom_killer: server.limits?.oomDisabled,
},
feature_limits: {
allocations: server.featureLimits?.allocations,
backups: server.featureLimits?.backups,
databases: server.featureLimits?.databases,
},
allocation_id: server.allocationId,
add_allocations: server.addAllocations,
remove_allocations: server.removeAllocations,
},
{ params: { include: include.join(',') } }
)
.then(({ data }) => resolve(rawDataToServer(data)))
.catch(reject);
});
};

View file

@ -0,0 +1,28 @@
import http from '@/api/http';
import { Server, rawDataToServer } from '@/api/admin/servers/getServers';
export interface Values {
startup: string;
environment: Record<string, any>;
eggId: number;
image: string;
skipScripts: boolean;
}
export default (id: number, values: Partial<Values>, include: string[] = []): Promise<Server> => {
return new Promise((resolve, reject) => {
http.patch(
`/api/application/servers/${id}/startup`,
{
startup: values.startup !== '' ? values.startup : null,
environment: values.environment,
egg_id: values.eggId,
image: values.image,
skip_scripts: values.skipScripts,
},
{ params: { include: include.join(',') } }
)
.then(({ data }) => resolve(rawDataToServer(data)))
.catch(reject);
});
};

View file

@ -0,0 +1,96 @@
import http, {
FractalPaginatedResponse,
PaginatedResult,
QueryBuilderParams,
getPaginationSet,
withQueryBuilderParams,
} from '@/api/http';
import { Transformers, User } from '@definitions/admin';
import useSWR, { SWRConfiguration, SWRResponse } from 'swr';
import { AxiosError } from 'axios';
export interface UpdateUserValues {
externalId: string;
username: string;
email: string;
password: string;
adminRoleId: number | null;
rootAdmin: boolean;
}
const filters = ['id', 'uuid', 'external_id', 'username', 'email'] as const;
type Filters = typeof filters[number];
const useGetUsers = (
params?: QueryBuilderParams<Filters>,
config?: SWRConfiguration,
): SWRResponse<PaginatedResult<User>, AxiosError> => {
return useSWR<PaginatedResult<User>>(
['/api/application/users', JSON.stringify(params)],
async () => {
const { data } = await http.get<FractalPaginatedResponse>('/api/application/users', {
params: withQueryBuilderParams(params),
});
return getPaginationSet(data, Transformers.toUser);
},
config || { revalidateOnMount: true, revalidateOnFocus: false },
);
};
const getUser = (id: number, include: string[] = []): Promise<User> => {
return new Promise((resolve, reject) => {
http.get(`/api/application/users/${id}`, { params: { include: include.join(',') } })
.then(({ data }) => resolve(Transformers.toUser(data)))
.catch(reject);
});
};
const searchUserAccounts = async (params: QueryBuilderParams<'username' | 'email'>): Promise<User[]> => {
const { data } = await http.get('/api/application/users', {
params: withQueryBuilderParams(params),
});
return data.data.map(Transformers.toUser);
};
const createUser = (values: UpdateUserValues, include: string[] = []): Promise<User> => {
const data = {};
Object.keys(values).forEach(k => {
// @ts-ignore
data[k.replace(/[A-Z]/g, l => `_${l.toLowerCase()}`)] = values[k];
});
return new Promise((resolve, reject) => {
http.post('/api/application/users', data, { params: { include: include.join(',') } })
.then(({ data }) => resolve(Transformers.toUser(data)))
.catch(reject);
});
};
const updateUser = (id: number, values: Partial<UpdateUserValues>, include: string[] = []): Promise<User> => {
const data = {};
Object.keys(values).forEach(k => {
// Don't set password if it is empty.
if (k === 'password' && values[k] === '') {
return;
}
// @ts-ignore
data[k.replace(/[A-Z]/g, l => `_${l.toLowerCase()}`)] = values[k];
});
return new Promise((resolve, reject) => {
http.patch(`/api/application/users/${id}`, data, { params: { include: include.join(',') } })
.then(({ data }) => resolve(Transformers.toUser(data)))
.catch(reject);
});
};
const deleteUser = (id: number): Promise<void> => {
return new Promise((resolve, reject) => {
http.delete(`/api/application/users/${id}`)
.then(() => resolve())
.catch(reject);
});
};
export { useGetUsers, getUser, searchUserAccounts, createUser, updateUser, deleteUser };

View file

@ -0,0 +1,2 @@
export * from './models.d';
export { default as Transformers } from './transformers';

View file

@ -0,0 +1,29 @@
import { ModelWithRelationships, UUID } from '@/api/definitions';
import { Server } from '@/api/admin/server';
interface User extends ModelWithRelationships {
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;
// TODO: just use an API call, this is probably a bad idea for performance.
servers?: Server[];
};
}
interface UserRole extends ModelWithRelationships {
id: number;
name: string;
description: string;
}

View 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 * as Models from '@definitions/admin/models';
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 default class Transformers {
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): Models.UserRole => ({
id: attributes.id,
name: attributes.name,
description: attributes.description,
relationships: {},
});
static toUser = ({ attributes }: FractalResponseData): Models.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),
},
});
}