Merge branch 'develop' into matthewpi/security-keys-backport

This commit is contained in:
Matthew Penner 2023-01-17 15:33:53 -07:00
commit f631ac1946
No known key found for this signature in database
1153 changed files with 25099 additions and 37002 deletions

View file

@ -1,30 +0,0 @@
import React from 'react';
import { Route } from 'react-router';
import { SwitchTransition } from 'react-transition-group';
import Fade from '@/components/elements/Fade';
import styled from 'styled-components/macro';
import tw from 'twin.macro';
const StyledSwitchTransition = styled(SwitchTransition)`
${tw`relative`};
& section {
${tw`absolute w-full top-0 left-0`};
}
`;
const TransitionRouter: React.FC = ({ children }) => {
return (
<Route
render={({ location }) => (
<StyledSwitchTransition>
<Fade timeout={150} key={location.pathname + location.search} in appear unmountOnExit>
<section>{children}</section>
</Fade>
</StyledSwitchTransition>
)}
/>
);
};
export default TransitionRouter;

View file

@ -1 +0,0 @@
module.exports = 'test-file-stub';

View file

@ -1,8 +1,10 @@
import useSWR, { ConfigInterface, responseInterface } from 'swr';
import { ActivityLog, Transformers } from '@definitions/user';
import { AxiosError } from 'axios';
import type { AxiosError } from 'axios';
import type { SWRConfiguration } from 'swr';
import useSWR from 'swr';
import http, { PaginatedResult, QueryBuilderParams, withQueryBuilderParams } from '@/api/http';
import { toPaginatedSet } from '@definitions/helpers';
import { ActivityLog, Transformers } from '@definitions/user';
import useFilteredObject from '@/plugins/useFilteredObject';
import { useUserSWRKey } from '@/plugins/useSWRKey';
@ -10,8 +12,8 @@ export type ActivityLogFilters = QueryBuilderParams<'ip' | 'event', 'timestamp'>
const useActivityLogs = (
filters?: ActivityLogFilters,
config?: ConfigInterface<PaginatedResult<ActivityLog>, AxiosError>
): responseInterface<PaginatedResult<ActivityLog>, AxiosError> => {
config?: SWRConfiguration<PaginatedResult<ActivityLog>, AxiosError>,
) => {
const key = useUserSWRKey(['account', 'activity', JSON.stringify(useFilteredObject(filters || {}))]);
return useSWR<PaginatedResult<ActivityLog>>(
@ -26,7 +28,7 @@ const useActivityLogs = (
return toPaginatedSet(data, Transformers.toActivityLog);
},
{ revalidateOnMount: false, ...(config || {}) }
{ revalidateOnMount: false, ...(config || {}) },
);
};

View file

@ -12,7 +12,7 @@ export default (description: string, allowedIps: string): Promise<ApiKey & { sec
...rawDataToApiKey(data.attributes),
// eslint-disable-next-line camelcase
secretToken: data.meta?.secret_token ?? '',
})
}),
)
.catch(reject);
});

View file

@ -1,10 +1,12 @@
import useSWR, { ConfigInterface } from 'swr';
import type { AxiosError } from 'axios';
import type { SWRConfiguration } from 'swr';
import useSWR from 'swr';
import http, { FractalResponseList } from '@/api/http';
import { SSHKey, Transformers } from '@definitions/user';
import { AxiosError } from 'axios';
import { useUserSWRKey } from '@/plugins/useSWRKey';
const useSSHKeys = (config?: ConfigInterface<SSHKey[], AxiosError>) => {
const useSSHKeys = (config?: SWRConfiguration<SSHKey[], AxiosError>) => {
const key = useUserSWRKey(['account', 'ssh-keys']);
return useSWR(
@ -16,7 +18,7 @@ const useSSHKeys = (config?: ConfigInterface<SSHKey[], AxiosError>) => {
return Transformers.toSSHKey(datum.attributes);
});
},
{ revalidateOnMount: false, ...(config || {}) }
{ revalidateOnMount: false, ...(config || {}) },
);
};

View file

@ -0,0 +1,27 @@
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,66 @@
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-expect-error todo
params['filter[' + key + ']'] = filters[key];
});
}
if (sort !== null) {
// @ts-expect-error todo
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,23 @@
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-expect-error todo
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,28 @@
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<string> => {
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,28 @@
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,19 @@
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,28 @@
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,24 @@
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,17 @@
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,56 @@
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-expect-error todo
params['filter[' + key + ']'] = filters[key];
});
}
if (sort !== null) {
// @ts-expect-error todo
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,23 @@
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-expect-error todo
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,17 @@
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,29 @@
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,84 @@
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-expect-error todo
params['filter[' + key + ']'] = filters[key];
});
}
if (sort !== null) {
// @ts-expect-error todo
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,30 @@
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,17 @@
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,40 @@
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-expect-error todo
params['filter[' + key + ']'] = filters[key];
});
}
if (sort !== null) {
// @ts-expect-error todo
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,68 @@
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-expect-error todo
params['filter[' + key + ']'] = filters[key];
});
}
if (sort !== null) {
// @ts-expect-error todo
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,17 @@
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,87 @@
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,41 @@
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-expect-error todo
params['filter[' + key + ']'] = filters[key];
});
}
if (sort !== null) {
// @ts-expect-error todo
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-expect-error todo
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,64 @@
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-expect-error todo
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,116 @@
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-expect-error todo
params['filter[' + key + ']'] = filters[key];
});
}
if (sort !== null) {
// @ts-expect-error todo
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-expect-error todo
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,111 @@
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-expect-error todo
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-expect-error todo
params['filter[' + key + ']'] = filters[key];
});
}
if (sort !== null) {
// @ts-expect-error todo
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;
oomKiller: 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;
ownerId: 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,84 @@
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;
oomKiller: 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.oomKiller,
},
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,193 @@
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;
oomKiller: 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,
oomKiller: attributes.limits.oom_killer,
},
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-expect-error todo
params['filter[' + key + ']'] = filters[key];
});
}
if (sort !== null) {
// @ts-expect-error todo
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;
oomKiller: 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?.oomKiller,
},
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,97 @@
import type { AxiosError } from 'axios';
import type { SWRConfiguration, SWRResponse } from 'swr';
import useSWR from 'swr';
import type { FractalPaginatedResponse, PaginatedResult, QueryBuilderParams } from '@/api/http';
import http, { getPaginationSet, withQueryBuilderParams } from '@/api/http';
import type { User } from '@definitions/admin';
import { Transformers } from '@definitions/admin';
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 {
items: (data.data || []).map(Transformers.toUser),
pagination: getPaginationSet(data.meta.pagination),
};
},
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-expect-error todo
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-expect-error todo
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

@ -20,9 +20,9 @@ export default ({ username, password, recaptchaData }: LoginData): Promise<Login
user: username,
password,
'g-recaptcha-response': recaptchaData,
})
}),
)
.then((response) => {
.then(response => {
if (!(response.data instanceof Object)) {
return reject(new Error('An error occurred while processing the login request.'));
}

View file

@ -8,11 +8,11 @@ export default (token: string, code: string, recoveryToken?: string): Promise<Lo
authentication_code: code,
recovery_token: recoveryToken && recoveryToken.length > 0 ? recoveryToken : undefined,
})
.then((response) =>
.then(response =>
resolve({
complete: response.data.data.complete,
intended: response.data.data.intended || undefined,
})
}),
)
.catch(reject);
});

View file

@ -19,11 +19,11 @@ export default (email: string, data: Data): Promise<PasswordResetResponse> => {
password: data.password,
password_confirmation: data.passwordConfirmation,
})
.then((response) =>
.then(response =>
resolve({
redirectTo: response.data.redirect_to,
sendToLogin: response.data.send_to_login,
})
}),
)
.catch(reject);
});

View file

@ -3,7 +3,7 @@ import http from '@/api/http';
export default (email: string, recaptchaData?: string): Promise<string> => {
return new Promise((resolve, reject) => {
http.post('/auth/password', { email, 'g-recaptcha-response': recaptchaData })
.then((response) => resolve(response.data.status || ''))
.then(response => resolve(response.data.status || ''))
.catch(reject);
});
};

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,228 @@
/* 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_killer, ...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,
ownerId: attributes.owner_id,
nodeId: attributes.node_id,
allocationId: attributes.allocation_id,
eggId: attributes.egg_id,
nestId: attributes.nest_id,
limits: { ...limits, oomKiller: oom_killer },
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),
},
});
}

View file

@ -15,17 +15,17 @@ function transform<T, M>(data: null | undefined, transformer: TransformerFunc<T>
function transform<T, M>(
data: FractalResponseData | null | undefined,
transformer: TransformerFunc<T>,
missing?: M
missing?: M,
): T | M;
function transform<T, M>(
data: FractalResponseList | FractalPaginatedResponse | null | undefined,
transformer: TransformerFunc<T>,
missing?: M
missing?: M,
): T[] | M;
function transform<T>(
data: FractalResponseData | FractalResponseList | FractalPaginatedResponse | null | undefined,
transformer: TransformerFunc<T>,
missing = undefined
missing = undefined,
) {
if (data === undefined || data === null) {
return missing;
@ -44,7 +44,7 @@ function transform<T>(
function toPaginatedSet<T extends TransformerFunc<Model>>(
response: FractalPaginatedResponse,
transformer: T
transformer: T,
): PaginatedResult<ReturnType<T>> {
return {
items: transform(response, transformer) as ReturnType<T>[],

View file

@ -19,7 +19,7 @@ export default ({ query, ...params }: QueryParams): Promise<PaginatedResult<Serv
resolve({
items: (data.data || []).map((datum: any) => rawDataToServerObject(datum)),
pagination: getPaginationSet(data.meta.pagination),
})
}),
)
.catch(reject);
});

View file

@ -11,7 +11,7 @@ const http: AxiosInstance = axios.create({
},
});
http.interceptors.request.use((req) => {
http.interceptors.request.use(req => {
if (!req.url?.endsWith('/resources')) {
store.getActions().progress.startContinuous();
}
@ -20,18 +20,18 @@ http.interceptors.request.use((req) => {
});
http.interceptors.response.use(
(resp) => {
resp => {
if (!resp.request?.url?.endsWith('/resources')) {
store.getActions().progress.setComplete();
}
return resp;
},
(error) => {
error => {
store.getActions().progress.setComplete();
throw error;
}
},
);
export default http;

View file

@ -1,21 +1,22 @@
import http from '@/api/http';
import { AxiosError } from 'axios';
import { History } from 'history';
import type { AxiosError } from 'axios';
import type { NavigateFunction } from 'react-router-dom';
export const setupInterceptors = (history: History) => {
import http from '@/api/http';
export const setupInterceptors = (navigate: NavigateFunction) => {
http.interceptors.response.use(
(resp) => resp,
resp => resp,
(error: AxiosError) => {
if (error.response?.status === 400) {
if (
(error.response?.data as Record<string, any>).errors?.[0].code === 'TwoFactorAuthRequiredException'
) {
if (!window.location.pathname.startsWith('/account')) {
history.replace('/account', { twoFactorRedirect: true });
navigate('/account', { state: { twoFactorRedirect: true } });
}
}
}
throw error;
}
},
);
};

View file

@ -1,8 +1,12 @@
import useSWR, { ConfigInterface, responseInterface } from 'swr';
import { ActivityLog, Transformers } from '@definitions/user';
import { AxiosError } from 'axios';
import http, { PaginatedResult, QueryBuilderParams, withQueryBuilderParams } from '@/api/http';
import type { AxiosError } from 'axios';
import type { SWRConfiguration } from 'swr';
import useSWR from 'swr';
import type { PaginatedResult, QueryBuilderParams } from '@/api/http';
import http, { withQueryBuilderParams } from '@/api/http';
import { toPaginatedSet } from '@definitions/helpers';
import type { ActivityLog } from '@definitions/user';
import { Transformers } from '@definitions/user';
import useFilteredObject from '@/plugins/useFilteredObject';
import { useServerSWRKey } from '@/plugins/useSWRKey';
import { ServerContext } from '@/state/server';
@ -11,9 +15,9 @@ export type ActivityLogFilters = QueryBuilderParams<'ip' | 'event', 'timestamp'>
const useActivityLogs = (
filters?: ActivityLogFilters,
config?: ConfigInterface<PaginatedResult<ActivityLog>, AxiosError>
): responseInterface<PaginatedResult<ActivityLog>, AxiosError> => {
const uuid = ServerContext.useStoreState((state) => state.server.data?.uuid);
config?: SWRConfiguration<PaginatedResult<ActivityLog>, AxiosError>,
) => {
const uuid = ServerContext.useStoreState(state => state.server.data?.uuid);
const key = useServerSWRKey(['activity', useFilteredObject(filters || {})]);
return useSWR<PaginatedResult<ActivityLog>>(
@ -28,7 +32,7 @@ const useActivityLogs = (
return toPaginatedSet(data, Transformers.toActivityLog);
},
{ revalidateOnMount: false, ...(config || {}) }
{ revalidateOnMount: false, ...(config || {}) },
);
};

View file

@ -11,9 +11,9 @@ export default (uuid: string, data: { connectionsFrom: string; databaseName: str
},
{
params: { include: 'password' },
}
},
)
.then((response) => resolve(rawDataToServerDatabase(response.data.attributes)))
.then(response => resolve(rawDataToServerDatabase(response.data.attributes)))
.catch(reject);
});
};

View file

@ -23,8 +23,8 @@ export default (uuid: string, includePassword = true): Promise<ServerDatabase[]>
http.get(`/api/client/servers/${uuid}/databases`, {
params: includePassword ? { include: 'password' } : undefined,
})
.then((response) =>
resolve((response.data.data || []).map((item: any) => rawDataToServerDatabase(item.attributes)))
.then(response =>
resolve((response.data.data || []).map((item: any) => rawDataToServerDatabase(item.attributes))),
)
.catch(reject);
});

View file

@ -4,7 +4,7 @@ import http from '@/api/http';
export default (uuid: string, database: string): Promise<ServerDatabase> => {
return new Promise((resolve, reject) => {
http.post(`/api/client/servers/${uuid}/databases/${database}/rotate-password`)
.then((response) => resolve(rawDataToServerDatabase(response.data.attributes)))
.then(response => resolve(rawDataToServerDatabase(response.data.attributes)))
.catch(reject);
});
};

View file

@ -10,7 +10,7 @@ export default async (uuid: string, directory: string, files: string[]): Promise
timeout: 60000,
timeoutErrorMessage:
'It looks like this archive is taking a long time to generate. It will appear once completed.',
}
},
);
return rawDataToFileObject(data);

View file

@ -8,6 +8,6 @@ export default async (uuid: string, directory: string, file: string): Promise<vo
timeout: 300000,
timeoutErrorMessage:
'It looks like this archive is taking a long time to be unarchived. Once completed the unarchived files will appear.',
}
},
);
};

View file

@ -4,7 +4,7 @@ export default (server: string, file: string): Promise<string> => {
return new Promise((resolve, reject) => {
http.get(`/api/client/servers/${server}/files/contents`, {
params: { file },
transformResponse: (res) => res,
transformResponse: res => res,
responseType: 'text',
})
.then(({ data }) => resolve(data))

View file

@ -17,6 +17,7 @@ export interface Server {
uuid: string;
name: string;
node: string;
isNodeUnderMaintenance: boolean;
status: ServerStatus;
sftpDetails: {
ip: string;
@ -24,7 +25,7 @@ export interface Server {
};
invocation: string;
dockerImage: string;
description: string;
description: string | null;
limits: {
memory: number;
swap: number;
@ -50,6 +51,7 @@ export const rawDataToServerObject = ({ attributes: data }: FractalResponseData)
uuid: data.uuid,
name: data.name,
node: data.node,
isNodeUnderMaintenance: data.is_node_under_maintenance,
status: data.status,
invocation: data.invocation,
dockerImage: data.docker_image,
@ -63,10 +65,10 @@ export const rawDataToServerObject = ({ attributes: data }: FractalResponseData)
featureLimits: { ...data.feature_limits },
isTransferring: data.is_transferring,
variables: ((data.relationships?.variables as FractalResponseList | undefined)?.data || []).map(
rawDataToServerEggVariable
rawDataToServerEggVariable,
),
allocations: ((data.relationships?.allocations as FractalResponseList | undefined)?.data || []).map(
rawDataToServerAllocation
rawDataToServerAllocation,
),
});
@ -78,7 +80,7 @@ export default (uuid: string): Promise<[Server, string[]]> => {
rawDataToServerObject(data),
// eslint-disable-next-line camelcase
data.meta?.is_server_owner ? ['*'] : data.meta?.user_permissions || [],
])
]),
)
.catch(reject);
});

View file

@ -26,7 +26,7 @@ export default (server: string): Promise<ServerStats> => {
networkRxInBytes: attributes.resources.network_rx_bytes,
networkTxInBytes: attributes.resources.network_tx_bytes,
uptime: attributes.resources.uptime,
})
}),
)
.catch(reject);
});

View file

@ -12,7 +12,7 @@ export default (server: string): Promise<Response> => {
resolve({
token: data.data.token,
socket: data.data.socket,
})
}),
)
.catch(reject);
});

View file

@ -16,7 +16,7 @@ export default async (uuid: string, schedule: number, task: number | undefined,
payload: data.payload,
continue_on_failure: data.continueOnFailure,
time_offset: data.timeOffset,
}
},
);
return rawDataToServerTask(response.attributes);

View file

@ -1,4 +1,10 @@
export type ServerStatus = 'installing' | 'install_failed' | 'suspended' | 'restoring_backup' | null;
export type ServerStatus =
| 'installing'
| 'install_failed'
| 'reinstall_failed'
| 'suspended'
| 'restoring_backup'
| null;
export interface ServerBackup {
uuid: string;

View file

@ -12,7 +12,7 @@ export default (uuid: string, params: Params, subuser?: Subuser): Promise<Subuse
http.post(`/api/client/servers/${uuid}/users${subuser ? `/${subuser.uuid}` : ''}`, {
...params,
})
.then((data) => resolve(rawDataToServerSubuser(data.data)))
.then(data => resolve(rawDataToServerSubuser(data.data)))
.catch(reject);
});
};

View file

@ -9,7 +9,7 @@ export const rawDataToServerSubuser = (data: FractalResponseData): Subuser => ({
twoFactorEnabled: data.attributes['2fa_enabled'],
createdAt: new Date(data.attributes.created_at),
permissions: data.attributes.permissions || [],
can: (permission) => (data.attributes.permissions || []).indexOf(permission) >= 0,
can: permission => (data.attributes.permissions || []).indexOf(permission) >= 0,
});
export default (uuid: string): Promise<Subuser[]> => {

View file

@ -1,11 +1,12 @@
import { ServerContext } from '@/state/server';
import useSWR from 'swr';
import http from '@/api/http';
import { rawDataToServerAllocation } from '@/api/transformers';
import { Allocation } from '@/api/server/getServer';
import { rawDataToServerAllocation } from '@/api/transformers';
import { ServerContext } from '@/state/server';
export default () => {
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
return useSWR<Allocation[]>(
['server:allocations', uuid],
@ -14,6 +15,6 @@ export default () => {
return (data.data || []).map(rawDataToServerAllocation);
},
{ revalidateOnFocus: false, revalidateOnMount: false }
{ revalidateOnFocus: false, revalidateOnMount: false },
);
};

View file

@ -1,9 +1,11 @@
import { createContext, useContext } from 'react';
import useSWR from 'swr';
import http, { getPaginationSet, PaginatedResult } from '@/api/http';
import { ServerBackup } from '@/api/server/types';
import type { PaginatedResult } from '@/api/http';
import http, { getPaginationSet } from '@/api/http';
import type { ServerBackup } from '@/api/server/types';
import { rawDataToServerBackup } from '@/api/transformers';
import { ServerContext } from '@/state/server';
import { createContext, useContext } from 'react';
interface ctx {
page: number;
@ -16,7 +18,7 @@ type BackupResponse = PaginatedResult<ServerBackup> & { backupCount: number };
export default () => {
const { page } = useContext(Context);
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
return useSWR<BackupResponse>(['server:backups', uuid, page], async () => {
const { data } = await http.get(`/api/client/servers/${uuid}/backups`, { params: { page } });

View file

@ -1,7 +1,10 @@
import useSWR, { ConfigInterface } from 'swr';
import type { AxiosError } from 'axios';
import type { SWRConfiguration } from 'swr';
import useSWR from 'swr';
import http, { FractalResponseList } from '@/api/http';
import type { ServerEggVariable } from '@/api/server/types';
import { rawDataToServerEggVariable } from '@/api/transformers';
import { ServerEggVariable } from '@/api/server/types';
interface Response {
invocation: string;
@ -9,7 +12,7 @@ interface Response {
dockerImages: Record<string, string>;
}
export default (uuid: string, initialData?: Response | null, config?: ConfigInterface<Response>) =>
export default (uuid: string, fallbackData?: Response, config?: SWRConfiguration<Response, AxiosError>) =>
useSWR(
[uuid, '/startup'],
async (): Promise<Response> => {
@ -23,5 +26,5 @@ export default (uuid: string, initialData?: Response | null, config?: ConfigInte
dockerImages: data.meta.docker_images || {},
};
},
{ initialData: initialData || undefined, errorRetryCount: 3, ...(config || {}) }
{ fallbackData, errorRetryCount: 3, ...(config ?? {}) },
);

View file

@ -49,7 +49,7 @@ export const rawDataToFileObject = (data: FractalResponseData): FileObject => ({
const matches = ['application/jar', 'application/octet-stream', 'inode/directory', /^image\//];
return matches.every((m) => !this.mimetype.match(m));
return matches.every(m => !this.mimetype.match(m));
},
});

View file

@ -1,5 +1,5 @@
import tw from 'twin.macro';
import { createGlobalStyle } from 'styled-components/macro';
import { createGlobalStyle } from 'styled-components';
export default createGlobalStyle`
body {

View file

@ -1,23 +1,22 @@
import React, { lazy } from 'react';
import { hot } from 'react-hot-loader/root';
import { Route, Router, Switch } from 'react-router-dom';
import { StoreProvider } from 'easy-peasy';
import { store } from '@/state';
import { SiteSettings } from '@/state/settings';
import { lazy } from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import '@/assets/tailwind.css';
import GlobalStylesheet from '@/assets/css/GlobalStylesheet';
import AuthenticatedRoute from '@/components/elements/AuthenticatedRoute';
import ProgressBar from '@/components/elements/ProgressBar';
import { NotFound } from '@/components/elements/ScreenBlock';
import tw from 'twin.macro';
import GlobalStylesheet from '@/assets/css/GlobalStylesheet';
import { history } from '@/components/history';
import { setupInterceptors } from '@/api/interceptors';
import AuthenticatedRoute from '@/components/elements/AuthenticatedRoute';
import { ServerContext } from '@/state/server';
import '@/assets/tailwind.css';
import Spinner from '@/components/elements/Spinner';
import { store } from '@/state';
import { ServerContext } from '@/state/server';
import { SiteSettings } from '@/state/settings';
import { AdminContext } from '@/state/admin';
const DashboardRouter = lazy(() => import(/* webpackChunkName: "dashboard" */ '@/routers/DashboardRouter'));
const ServerRouter = lazy(() => import(/* webpackChunkName: "server" */ '@/routers/ServerRouter'));
const AuthenticationRouter = lazy(() => import(/* webpackChunkName: "auth" */ '@/routers/AuthenticationRouter'));
const AdminRouter = lazy(() => import('@/routers/AdminRouter'));
const AuthenticationRouter = lazy(() => import('@/routers/AuthenticationRouter'));
const DashboardRouter = lazy(() => import('@/routers/DashboardRouter'));
const ServerRouter = lazy(() => import('@/routers/ServerRouter'));
interface ExtendedWindow extends Window {
SiteConfiguration?: SiteSettings;
@ -29,15 +28,17 @@ interface ExtendedWindow extends Window {
root_admin: boolean;
use_totp: boolean;
language: string;
avatar_url: string;
admin_role_name: string;
updated_at: string;
created_at: string;
/* eslint-enable camelcase */
};
}
setupInterceptors(history);
// setupInterceptors(history);
const App = () => {
function App() {
const { PterodactylUser, SiteConfiguration } = window as ExtendedWindow;
if (PterodactylUser && !store.getState().user.data) {
store.getActions().user.setUserData({
@ -46,6 +47,8 @@ const App = () => {
email: PterodactylUser.email,
language: PterodactylUser.language,
rootAdmin: PterodactylUser.root_admin,
avatarURL: PterodactylUser.avatar_url,
roleName: PterodactylUser.admin_role_name,
useTotp: PterodactylUser.use_totp,
createdAt: new Date(PterodactylUser.created_at),
updatedAt: new Date(PterodactylUser.updated_at),
@ -58,38 +61,66 @@ const App = () => {
return (
<>
{/* @ts-expect-error go away */}
<GlobalStylesheet />
<StoreProvider store={store}>
<ProgressBar />
<div css={tw`mx-auto w-auto`}>
<Router history={history}>
<Switch>
<Route path={'/auth'}>
<Spinner.Suspense>
<AuthenticationRouter />
</Spinner.Suspense>
</Route>
<AuthenticatedRoute path={'/server/:id'}>
<Spinner.Suspense>
<ServerContext.Provider>
<ServerRouter />
</ServerContext.Provider>
</Spinner.Suspense>
</AuthenticatedRoute>
<AuthenticatedRoute path={'/'}>
<Spinner.Suspense>
<DashboardRouter />
</Spinner.Suspense>
</AuthenticatedRoute>
<Route path={'*'}>
<NotFound />
</Route>
</Switch>
</Router>
<div className="mx-auto w-auto">
<BrowserRouter>
<Routes>
<Route
path="/auth/*"
element={
<Spinner.Suspense>
<AuthenticationRouter />
</Spinner.Suspense>
}
/>
<Route
path="/server/:id/*"
element={
<AuthenticatedRoute>
<Spinner.Suspense>
<ServerContext.Provider>
<ServerRouter />
</ServerContext.Provider>
</Spinner.Suspense>
</AuthenticatedRoute>
}
/>
<Route
path="/admin/*"
element={
<Spinner.Suspense>
<AdminContext.Provider>
<AdminRouter />
</AdminContext.Provider>
</Spinner.Suspense>
}
/>
<Route
path="/*"
element={
<AuthenticatedRoute>
<Spinner.Suspense>
<DashboardRouter />
</Spinner.Suspense>
</AuthenticatedRoute>
}
/>
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
</div>
</StoreProvider>
</>
);
};
}
export default hot(App);
export { App };

View file

@ -1,4 +1,3 @@
import React from 'react';
import BoringAvatar, { AvatarProps } from 'boring-avatars';
import { useStoreState } from '@/state/hooks';
@ -11,7 +10,7 @@ const _Avatar = ({ variant = 'beam', ...props }: AvatarProps) => (
);
const _UserAvatar = ({ variant = 'beam', ...props }: Omit<Props, 'name'>) => {
const uuid = useStoreState((state) => state.user.data?.uuid);
const uuid = useStoreState(state => state.user.data?.uuid);
return <BoringAvatar colors={palette} name={uuid || 'system'} variant={variant} {...props} />;
};

View file

@ -1,30 +1,28 @@
import React from 'react';
import MessageBox from '@/components/MessageBox';
import { useStoreState } from 'easy-peasy';
import tw from 'twin.macro';
import { Fragment } from 'react';
import MessageBox from '@/components/MessageBox';
type Props = Readonly<{
byKey?: string;
className?: string;
}>;
const FlashMessageRender = ({ byKey, className }: Props) => {
const flashes = useStoreState((state) =>
state.flashes.items.filter((flash) => (byKey ? flash.key === byKey : true))
);
function FlashMessageRender({ byKey, className }: Props) {
const flashes = useStoreState(state => state.flashes.items.filter(flash => (byKey ? flash.key === byKey : true)));
return flashes.length ? (
<div className={className}>
{flashes.map((flash, index) => (
<React.Fragment key={flash.id || flash.type + flash.message}>
{index > 0 && <div css={tw`mt-2`}></div>}
<Fragment key={flash.id || flash.type + flash.message}>
{index > 0 && <div className="mt-2" />}
<MessageBox type={flash.type} title={flash.title}>
{flash.message}
</MessageBox>
</React.Fragment>
</Fragment>
))}
</div>
) : null;
};
}
export default FlashMessageRender;

View file

@ -1,6 +1,5 @@
import * as React from 'react';
import tw, { TwStyle } from 'twin.macro';
import styled from 'styled-components/macro';
import styled from 'styled-components';
export type FlashMessageType = 'success' | 'info' | 'warning' | 'error';
@ -42,7 +41,7 @@ const getBackground = (type?: FlashMessageType): TwStyle | string => {
const Container = styled.div<{ $type?: FlashMessageType }>`
${tw`p-2 border items-center leading-normal rounded flex w-full text-sm text-white`};
${(props) => styling(props.$type)};
${props => styling(props.$type)};
`;
Container.displayName = 'MessageBox.Container';

View file

@ -1,13 +1,12 @@
import * as React from 'react';
import { useState } from 'react';
import { Link, NavLink } from 'react-router-dom';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCogs, faLayerGroup, faSignOutAlt } from '@fortawesome/free-solid-svg-icons';
import { faLayerGroup, faScrewdriverWrench, faSignOutAlt } from '@fortawesome/free-solid-svg-icons';
import { useStoreState } from 'easy-peasy';
import { ApplicationStore } from '@/state';
import SearchContainer from '@/components/dashboard/search/SearchContainer';
import tw, { theme } from 'twin.macro';
import styled from 'styled-components/macro';
import styled from 'styled-components';
import http from '@/api/http';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import Tooltip from '@/components/elements/tooltip/Tooltip';
@ -39,6 +38,7 @@ export default () => {
const onTriggerLogout = () => {
setIsLoggingOut(true);
http.post('/auth/logout').finally(() => {
// @ts-expect-error this is valid
window.location = '/';
@ -46,41 +46,44 @@ export default () => {
};
return (
<div className={'w-full bg-neutral-900 shadow-md overflow-x-auto'}>
<div className="w-full overflow-x-auto bg-neutral-900 shadow-md">
<SpinnerOverlay visible={isLoggingOut} />
<div className={'mx-auto w-full flex items-center h-[3.5rem] max-w-[1200px]'}>
<div id={'logo'} className={'flex-1'}>
<div className="mx-auto flex h-[3.5rem] w-full max-w-[1200px] items-center">
<div id="logo" className="flex-1">
<Link
to={'/'}
className={
'text-2xl font-header px-4 no-underline text-neutral-200 hover:text-neutral-100 transition-colors duration-150'
}
to="/"
className="px-4 font-header text-2xl text-neutral-200 no-underline transition-colors duration-150 hover:text-neutral-100"
>
{name}
</Link>
</div>
<RightNavigation className={'flex h-full items-center justify-center'}>
<RightNavigation className="flex h-full items-center justify-center">
<SearchContainer />
<Tooltip placement={'bottom'} content={'Dashboard'}>
<NavLink to={'/'} exact>
<Tooltip placement="bottom" content="Dashboard">
<NavLink to="/" end>
<FontAwesomeIcon icon={faLayerGroup} />
</NavLink>
</Tooltip>
{rootAdmin && (
<Tooltip placement={'bottom'} content={'Admin'}>
<a href={'/admin'} rel={'noreferrer'}>
<FontAwesomeIcon icon={faCogs} />
</a>
</Tooltip>
)}
<Tooltip placement={'bottom'} content={'Account Settings'}>
<NavLink to={'/account'}>
<span className={'flex items-center w-5 h-5'}>
<Tooltip placement="bottom" content="Account Settings">
<NavLink to="/account">
<span className="flex h-5 w-5 items-center">
<Avatar.User />
</span>
</NavLink>
</Tooltip>
<Tooltip placement={'bottom'} content={'Sign Out'}>
{rootAdmin && (
<Tooltip placement="bottom" content="Admin">
<a href="/admin" rel="noreferrer">
<FontAwesomeIcon icon={faScrewdriverWrench} />
</a>
</Tooltip>
)}
<Tooltip placement="bottom" content="Sign Out">
<button onClick={onTriggerLogout}>
<FontAwesomeIcon icon={faSignOutAlt} />
</button>

View file

@ -0,0 +1,36 @@
import type { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { ReactNode } from 'react';
import tw from 'twin.macro';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
interface Props {
icon?: IconProp;
isLoading?: boolean;
title: string | ReactNode;
className?: string;
noPadding?: boolean;
children: ReactNode;
button?: ReactNode;
}
const AdminBox = ({ icon, title, className, isLoading, children, button, noPadding }: Props) => (
<div css={tw`relative rounded shadow-md bg-neutral-700`} className={className}>
<SpinnerOverlay visible={isLoading || false} />
<div css={tw`flex flex-row bg-neutral-900 rounded-t px-4 xl:px-5 py-3 border-b border-black`}>
{typeof title === 'string' ? (
<p css={tw`text-sm uppercase`}>
{icon && <FontAwesomeIcon icon={icon} css={tw`mr-2 text-neutral-300`} />}
{title}
</p>
) : (
title
)}
{button}
</div>
<div css={[!noPadding && tw`px-4 xl:px-5 py-5`]}>{children}</div>
</div>
);
export default AdminBox;

View file

@ -0,0 +1,36 @@
import type { ChangeEvent } from 'react';
import tw, { styled } from 'twin.macro';
import Input from '@/components/elements/Input';
export const TableCheckbox = styled(Input)`
&& {
${tw`border-neutral-500 bg-transparent`};
&:not(:checked) {
${tw`hover:border-neutral-300`};
}
}
`;
export default ({
name,
checked,
onChange,
}: {
name: string;
checked: boolean;
onChange(e: ChangeEvent<HTMLInputElement>): void;
}) => {
return (
<div css={tw`flex items-center`}>
<TableCheckbox
type={'checkbox'}
name={'selectedItems'}
value={name}
checked={checked}
onChange={onChange}
/>
</div>
);
};

View file

@ -0,0 +1,42 @@
import type { ReactNode } from 'react';
import { useEffect } from 'react';
// import { CSSTransition } from 'react-transition-group';
import tw from 'twin.macro';
import FlashMessageRender from '@/components/FlashMessageRender';
const AdminContentBlock: React.FC<{
children: ReactNode;
title?: string;
showFlashKey?: string;
className?: string;
}> = ({ children, title, showFlashKey }) => {
useEffect(() => {
if (!title) {
return;
}
document.title = `Admin | ${title}`;
}, [title]);
return (
// <CSSTransition timeout={150} classNames={'fade'} appear in>
<>
{showFlashKey && <FlashMessageRender byKey={showFlashKey} css={tw`mb-4`} />}
{children}
{/* <p css={tw`text-center text-neutral-500 text-xs mt-4`}>
&copy; 2015 - 2021&nbsp;
<a
rel={'noopener nofollow noreferrer'}
href={'https://pterodactyl.io'}
target={'_blank'}
css={tw`no-underline text-neutral-500 hover:text-neutral-300`}
>
Pterodactyl Software
</a>
</p> */}
</>
// </CSSTransition>
);
};
export default AdminContentBlock;

Some files were not shown because too many files have changed in this diff Show more