chore: run prettier
This commit is contained in:
parent
9cdbbc3a00
commit
155d7bb876
76 changed files with 788 additions and 550 deletions
|
@ -1,11 +1,26 @@
|
||||||
import http from '@/api/http';
|
import http from '@/api/http';
|
||||||
import { Database, rawDataToDatabase } from '@/api/admin/databases/getDatabases';
|
import { Database, rawDataToDatabase } from '@/api/admin/databases/getDatabases';
|
||||||
|
|
||||||
export default (name: string, host: string, port: number, username: string, password: string, include: string[] = []): Promise<Database> => {
|
export default (
|
||||||
|
name: string,
|
||||||
|
host: string,
|
||||||
|
port: number,
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
include: string[] = [],
|
||||||
|
): Promise<Database> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.post('/api/application/databases', {
|
http.post(
|
||||||
name, host, port, username, password,
|
'/api/application/databases',
|
||||||
}, { params: { include: include.join(',') } })
|
{
|
||||||
|
name,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
{ params: { include: include.join(',') } },
|
||||||
|
)
|
||||||
.then(({ data }) => resolve(rawDataToDatabase(data)))
|
.then(({ data }) => resolve(rawDataToDatabase(data)))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,7 +13,7 @@ export interface Database {
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|
||||||
getAddress (): string;
|
getAddress(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rawDataToDatabase = ({ attributes }: FractalResponseData): Database => ({
|
export const rawDataToDatabase = ({ attributes }: FractalResponseData): Database => ({
|
||||||
|
@ -53,12 +53,14 @@ export default (include: string[] = []) => {
|
||||||
params.sort = (sortDirection ? '-' : '') + sort;
|
params.sort = (sortDirection ? '-' : '') + sort;
|
||||||
}
|
}
|
||||||
|
|
||||||
return useSWR<PaginatedResult<Database>>([ 'databases', page, filters, sort, sortDirection ], async () => {
|
return useSWR<PaginatedResult<Database>>(['databases', page, filters, sort, sortDirection], async () => {
|
||||||
const { data } = await http.get('/api/application/databases', { params: { include: include.join(','), page, ...params } });
|
const { data } = await http.get('/api/application/databases', {
|
||||||
|
params: { include: include.join(','), page, ...params },
|
||||||
|
});
|
||||||
|
|
||||||
return ({
|
return {
|
||||||
items: (data.data || []).map(rawDataToDatabase),
|
items: (data.data || []).map(rawDataToDatabase),
|
||||||
pagination: getPaginationSet(data.meta.pagination),
|
pagination: getPaginationSet(data.meta.pagination),
|
||||||
});
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,9 +17,7 @@ export default (filters?: Filters): Promise<Database[]> => {
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.get('/api/application/databases', { params })
|
http.get('/api/application/databases', { params })
|
||||||
.then(response => resolve(
|
.then(response => resolve((response.data.data || []).map(rawDataToDatabase)))
|
||||||
(response.data.data || []).map(rawDataToDatabase)
|
|
||||||
))
|
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,27 @@
|
||||||
import http from '@/api/http';
|
import http from '@/api/http';
|
||||||
import { Database, rawDataToDatabase } from '@/api/admin/databases/getDatabases';
|
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> => {
|
export default (
|
||||||
|
id: number,
|
||||||
|
name: string,
|
||||||
|
host: string,
|
||||||
|
port: number,
|
||||||
|
username: string,
|
||||||
|
password: string | undefined,
|
||||||
|
include: string[] = [],
|
||||||
|
): Promise<Database> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.patch(`/api/application/databases/${id}`, {
|
http.patch(
|
||||||
name, host, port, username, password,
|
`/api/application/databases/${id}`,
|
||||||
}, { params: { include: include.join(',') } })
|
{
|
||||||
|
name,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
{ params: { include: include.join(',') } },
|
||||||
|
)
|
||||||
.then(({ data }) => resolve(rawDataToDatabase(data)))
|
.then(({ data }) => resolve(rawDataToDatabase(data)))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,30 +1,27 @@
|
||||||
import http from '@/api/http';
|
import http from '@/api/http';
|
||||||
import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg';
|
import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg';
|
||||||
|
|
||||||
type Egg2 = Omit<Omit<Partial<Egg>, 'configFiles'>, 'configStartup'> & { configFiles: string, configStartup: string };
|
type Egg2 = Omit<Omit<Partial<Egg>, 'configFiles'>, 'configStartup'> & { configFiles: string; configStartup: string };
|
||||||
|
|
||||||
export default (egg: Partial<Egg2>): Promise<Egg> => {
|
export default (egg: Partial<Egg2>): Promise<Egg> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.post(
|
http.post('/api/application/eggs', {
|
||||||
'/api/application/eggs',
|
nest_id: egg.nestId,
|
||||||
{
|
name: egg.name,
|
||||||
nest_id: egg.nestId,
|
description: egg.description,
|
||||||
name: egg.name,
|
features: egg.features,
|
||||||
description: egg.description,
|
docker_images: egg.dockerImages,
|
||||||
features: egg.features,
|
config_files: egg.configFiles,
|
||||||
docker_images: egg.dockerImages,
|
config_startup: egg.configStartup,
|
||||||
config_files: egg.configFiles,
|
config_stop: egg.configStop,
|
||||||
config_startup: egg.configStartup,
|
config_from: egg.configFrom,
|
||||||
config_stop: egg.configStop,
|
startup: egg.startup,
|
||||||
config_from: egg.configFrom,
|
script_container: egg.scriptContainer,
|
||||||
startup: egg.startup,
|
copy_script_from: egg.copyScriptFrom,
|
||||||
script_container: egg.scriptContainer,
|
script_entry: egg.scriptEntry,
|
||||||
copy_script_from: egg.copyScriptFrom,
|
script_is_privileged: egg.scriptIsPrivileged,
|
||||||
script_entry: egg.scriptEntry,
|
script_install: egg.scriptInstall,
|
||||||
script_is_privileged: egg.scriptIsPrivileged,
|
})
|
||||||
script_install: egg.scriptInstall,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.then(({ data }) => resolve(rawDataToEgg(data)))
|
.then(({ data }) => resolve(rawDataToEgg(data)))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,18 +5,15 @@ import { Transformers } from '@definitions/admin';
|
||||||
export type CreateEggVariable = Omit<EggVariable, 'id' | 'eggId' | 'createdAt' | 'updatedAt' | 'relationships'>;
|
export type CreateEggVariable = Omit<EggVariable, 'id' | 'eggId' | 'createdAt' | 'updatedAt' | 'relationships'>;
|
||||||
|
|
||||||
export default async (eggId: number, variable: CreateEggVariable): Promise<EggVariable> => {
|
export default async (eggId: number, variable: CreateEggVariable): Promise<EggVariable> => {
|
||||||
const { data } = await http.post(
|
const { data } = await http.post(`/api/application/eggs/${eggId}/variables`, {
|
||||||
`/api/application/eggs/${eggId}/variables`,
|
name: variable.name,
|
||||||
{
|
description: variable.description,
|
||||||
name: variable.name,
|
env_variable: variable.environmentVariable,
|
||||||
description: variable.description,
|
default_value: variable.defaultValue,
|
||||||
env_variable: variable.environmentVariable,
|
user_viewable: variable.isUserViewable,
|
||||||
default_value: variable.defaultValue,
|
user_editable: variable.isUserEditable,
|
||||||
user_viewable: variable.isUserViewable,
|
rules: variable.rules,
|
||||||
user_editable: variable.isUserEditable,
|
});
|
||||||
rules: variable.rules,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return Transformers.toEggVariable(data);
|
return Transformers.toEggVariable(data);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,30 +1,27 @@
|
||||||
import http from '@/api/http';
|
import http from '@/api/http';
|
||||||
import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg';
|
import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg';
|
||||||
|
|
||||||
type Egg2 = Omit<Omit<Partial<Egg>, 'configFiles'>, 'configStartup'> & { configFiles?: string, configStartup?: string };
|
type Egg2 = Omit<Omit<Partial<Egg>, 'configFiles'>, 'configStartup'> & { configFiles?: string; configStartup?: string };
|
||||||
|
|
||||||
export default (id: number, egg: Partial<Egg2>): Promise<Egg> => {
|
export default (id: number, egg: Partial<Egg2>): Promise<Egg> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.patch(
|
http.patch(`/api/application/eggs/${id}`, {
|
||||||
`/api/application/eggs/${id}`,
|
nest_id: egg.nestId,
|
||||||
{
|
name: egg.name,
|
||||||
nest_id: egg.nestId,
|
description: egg.description,
|
||||||
name: egg.name,
|
features: egg.features,
|
||||||
description: egg.description,
|
docker_images: egg.dockerImages,
|
||||||
features: egg.features,
|
config_files: egg.configFiles,
|
||||||
docker_images: egg.dockerImages,
|
config_startup: egg.configStartup,
|
||||||
config_files: egg.configFiles,
|
config_stop: egg.configStop,
|
||||||
config_startup: egg.configStartup,
|
config_from: egg.configFrom,
|
||||||
config_stop: egg.configStop,
|
startup: egg.startup,
|
||||||
config_from: egg.configFrom,
|
script_container: egg.scriptContainer,
|
||||||
startup: egg.startup,
|
copy_script_from: egg.copyScriptFrom,
|
||||||
script_container: egg.scriptContainer,
|
script_entry: egg.scriptEntry,
|
||||||
copy_script_from: egg.copyScriptFrom,
|
script_is_privileged: egg.scriptIsPrivileged,
|
||||||
script_entry: egg.scriptEntry,
|
script_install: egg.scriptInstall,
|
||||||
script_is_privileged: egg.scriptIsPrivileged,
|
})
|
||||||
script_install: egg.scriptInstall,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.then(({ data }) => resolve(rawDataToEgg(data)))
|
.then(({ data }) => resolve(rawDataToEgg(data)))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,7 +2,10 @@ import http from '@/api/http';
|
||||||
import { EggVariable } from '@/api/admin/egg';
|
import { EggVariable } from '@/api/admin/egg';
|
||||||
import { Transformers } from '@definitions/admin';
|
import { Transformers } from '@definitions/admin';
|
||||||
|
|
||||||
export default async (eggId: number, variables: Omit<EggVariable, 'eggId' | 'createdAt' | 'updatedAt'>[]): Promise<EggVariable[]> => {
|
export default async (
|
||||||
|
eggId: number,
|
||||||
|
variables: Omit<EggVariable, 'eggId' | 'createdAt' | 'updatedAt'>[],
|
||||||
|
): Promise<EggVariable[]> => {
|
||||||
const { data } = await http.patch(
|
const { data } = await http.patch(
|
||||||
`/api/application/eggs/${eggId}/variables`,
|
`/api/application/eggs/${eggId}/variables`,
|
||||||
variables.map(variable => ({
|
variables.map(variable => ({
|
||||||
|
|
|
@ -4,11 +4,11 @@ export interface VersionData {
|
||||||
panel: {
|
panel: {
|
||||||
current: string;
|
current: string;
|
||||||
latest: string;
|
latest: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
wings: {
|
wings: {
|
||||||
latest: string;
|
latest: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
git: string | null;
|
git: string | null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,8 +13,8 @@ export type UUID = string;
|
||||||
export type WithRelationships<M extends Model, R extends string> = Omit<M, 'relationships'> & {
|
export type WithRelationships<M extends Model, R extends string> = Omit<M, 'relationships'> & {
|
||||||
relationships: Omit<M['relationships'], keyof R> & {
|
relationships: Omit<M['relationships'], keyof R> & {
|
||||||
[K in R]: NonNullable<M['relationships'][K]>;
|
[K in R]: NonNullable<M['relationships'][K]>;
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper type that allows you to infer the type of an object by giving
|
* Helper type that allows you to infer the type of an object by giving
|
||||||
|
@ -29,7 +29,7 @@ export type InferModel<T extends (...args: any) => any> = ReturnType<T> extends
|
||||||
* such that TypeScript understands the relationships on it. This is just to help
|
* such that TypeScript understands the relationships on it. This is just to help
|
||||||
* reduce the amount of duplicated type casting all over the codebase.
|
* reduce the amount of duplicated type casting all over the codebase.
|
||||||
*/
|
*/
|
||||||
export const withRelationships = <M extends Model, R extends string> (model: M, ..._keys: R[]) => {
|
export const withRelationships = <M extends Model, R extends string>(model: M, ..._keys: R[]) => {
|
||||||
return model as unknown as WithRelationships<M, R>;
|
return model as unknown as WithRelationships<M, R>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ export interface ListContext<T> {
|
||||||
setSortDirection: (direction: ((p: boolean) => boolean) | boolean) => void;
|
setSortDirection: (direction: ((p: boolean) => boolean) | boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function create<T> () {
|
function create<T>() {
|
||||||
return createContext<ListContext<T>>({
|
return createContext<ListContext<T>>({
|
||||||
page: 1,
|
page: 1,
|
||||||
setPage: () => 1,
|
setPage: () => 1,
|
||||||
|
|
|
@ -3,9 +3,14 @@ import { Location, rawDataToLocation } from '@/api/admin/locations/getLocations'
|
||||||
|
|
||||||
export default (short: string, long: string | null, include: string[] = []): Promise<Location> => {
|
export default (short: string, long: string | null, include: string[] = []): Promise<Location> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.post('/api/application/locations', {
|
http.post(
|
||||||
short, long,
|
'/api/application/locations',
|
||||||
}, { params: { include: include.join(',') } })
|
{
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
},
|
||||||
|
{ params: { include: include.join(',') } },
|
||||||
|
)
|
||||||
.then(({ data }) => resolve(rawDataToLocation(data)))
|
.then(({ data }) => resolve(rawDataToLocation(data)))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
|
|
|
@ -43,12 +43,14 @@ export default (include: string[] = []) => {
|
||||||
params.sort = (sortDirection ? '-' : '') + sort;
|
params.sort = (sortDirection ? '-' : '') + sort;
|
||||||
}
|
}
|
||||||
|
|
||||||
return useSWR<PaginatedResult<Location>>([ 'locations', page, filters, sort, sortDirection ], async () => {
|
return useSWR<PaginatedResult<Location>>(['locations', page, filters, sort, sortDirection], async () => {
|
||||||
const { data } = await http.get('/api/application/locations', { params: { include: include.join(','), page, ...params } });
|
const { data } = await http.get('/api/application/locations', {
|
||||||
|
params: { include: include.join(','), page, ...params },
|
||||||
|
});
|
||||||
|
|
||||||
return ({
|
return {
|
||||||
items: (data.data || []).map(rawDataToLocation),
|
items: (data.data || []).map(rawDataToLocation),
|
||||||
pagination: getPaginationSet(data.meta.pagination),
|
pagination: getPaginationSet(data.meta.pagination),
|
||||||
});
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,9 +17,7 @@ export default (filters?: Filters): Promise<Location[]> => {
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.get('/api/application/locations', { params })
|
http.get('/api/application/locations', { params })
|
||||||
.then(response => resolve(
|
.then(response => resolve((response.data.data || []).map(rawDataToLocation)))
|
||||||
(response.data.data || []).map(rawDataToLocation)
|
|
||||||
))
|
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,9 +3,14 @@ import { Location, rawDataToLocation } from '@/api/admin/locations/getLocations'
|
||||||
|
|
||||||
export default (id: number, short: string, long: string | null, include: string[] = []): Promise<Location> => {
|
export default (id: number, short: string, long: string | null, include: string[] = []): Promise<Location> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.patch(`/api/application/locations/${id}`, {
|
http.patch(
|
||||||
short, long,
|
`/api/application/locations/${id}`,
|
||||||
}, { params: { include: include.join(',') } })
|
{
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
},
|
||||||
|
{ params: { include: include.join(',') } },
|
||||||
|
)
|
||||||
.then(({ data }) => resolve(rawDataToLocation(data)))
|
.then(({ data }) => resolve(rawDataToLocation(data)))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,11 +1,28 @@
|
||||||
import http from '@/api/http';
|
import http from '@/api/http';
|
||||||
import { Mount, rawDataToMount } from '@/api/admin/mounts/getMounts';
|
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> => {
|
export default (
|
||||||
|
name: string,
|
||||||
|
description: string,
|
||||||
|
source: string,
|
||||||
|
target: string,
|
||||||
|
readOnly: boolean,
|
||||||
|
userMountable: boolean,
|
||||||
|
include: string[] = [],
|
||||||
|
): Promise<Mount> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.post('/api/application/mounts', {
|
http.post(
|
||||||
name, description, source, target, read_only: readOnly, user_mountable: userMountable,
|
'/api/application/mounts',
|
||||||
}, { params: { include: include.join(',') } })
|
{
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
read_only: readOnly,
|
||||||
|
user_mountable: userMountable,
|
||||||
|
},
|
||||||
|
{ params: { include: include.join(',') } },
|
||||||
|
)
|
||||||
.then(({ data }) => resolve(rawDataToMount(data)))
|
.then(({ data }) => resolve(rawDataToMount(data)))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
|
|
|
@ -40,7 +40,9 @@ export const rawDataToMount = ({ attributes }: FractalResponseData): Mount => ({
|
||||||
relations: {
|
relations: {
|
||||||
eggs: ((attributes.relationships?.eggs as FractalResponseList | undefined)?.data || []).map(rawDataToEgg),
|
eggs: ((attributes.relationships?.eggs as FractalResponseList | undefined)?.data || []).map(rawDataToEgg),
|
||||||
nodes: ((attributes.relationships?.nodes as FractalResponseList | undefined)?.data || []).map(rawDataToNode),
|
nodes: ((attributes.relationships?.nodes as FractalResponseList | undefined)?.data || []).map(rawDataToNode),
|
||||||
servers: ((attributes.relationships?.servers as FractalResponseList | undefined)?.data || []).map(rawDataToServer),
|
servers: ((attributes.relationships?.servers as FractalResponseList | undefined)?.data || []).map(
|
||||||
|
rawDataToServer,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -69,12 +71,14 @@ export default (include: string[] = []) => {
|
||||||
params.sort = (sortDirection ? '-' : '') + sort;
|
params.sort = (sortDirection ? '-' : '') + sort;
|
||||||
}
|
}
|
||||||
|
|
||||||
return useSWR<PaginatedResult<Mount>>([ 'mounts', page, filters, sort, sortDirection ], async () => {
|
return useSWR<PaginatedResult<Mount>>(['mounts', page, filters, sort, sortDirection], async () => {
|
||||||
const { data } = await http.get('/api/application/mounts', { params: { include: include.join(','), page, ...params } });
|
const { data } = await http.get('/api/application/mounts', {
|
||||||
|
params: { include: include.join(','), page, ...params },
|
||||||
|
});
|
||||||
|
|
||||||
return ({
|
return {
|
||||||
items: (data.data || []).map(rawDataToMount),
|
items: (data.data || []).map(rawDataToMount),
|
||||||
pagination: getPaginationSet(data.meta.pagination),
|
pagination: getPaginationSet(data.meta.pagination),
|
||||||
});
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,29 @@
|
||||||
import http from '@/api/http';
|
import http from '@/api/http';
|
||||||
import { Mount, rawDataToMount } from '@/api/admin/mounts/getMounts';
|
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> => {
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.patch(`/api/application/mounts/${id}`, {
|
http.patch(
|
||||||
name, description, source, target, read_only: readOnly, user_mountable: userMountable,
|
`/api/application/mounts/${id}`,
|
||||||
}, { params: { include: include.join(',') } })
|
{
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
read_only: readOnly,
|
||||||
|
user_mountable: userMountable,
|
||||||
|
},
|
||||||
|
{ params: { include: include.join(',') } },
|
||||||
|
)
|
||||||
.then(({ data }) => resolve(rawDataToMount(data)))
|
.then(({ data }) => resolve(rawDataToMount(data)))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,9 +3,14 @@ import { Nest, rawDataToNest } from '@/api/admin/nests/getNests';
|
||||||
|
|
||||||
export default (name: string, description: string | null, include: string[] = []): Promise<Nest> => {
|
export default (name: string, description: string | null, include: string[] = []): Promise<Nest> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.post('/api/application/nests', {
|
http.post(
|
||||||
name, description,
|
'/api/application/nests',
|
||||||
}, { params: { include: include.join(',') } })
|
{
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
},
|
||||||
|
{ params: { include: include.join(',') } },
|
||||||
|
)
|
||||||
.then(({ data }) => resolve(rawDataToNest(data)))
|
.then(({ data }) => resolve(rawDataToNest(data)))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
|
|
|
@ -27,12 +27,14 @@ export default (nestId: number, include: string[] = []) => {
|
||||||
params.sort = (sortDirection ? '-' : '') + sort;
|
params.sort = (sortDirection ? '-' : '') + sort;
|
||||||
}
|
}
|
||||||
|
|
||||||
return useSWR<PaginatedResult<Egg>>([ nestId, 'eggs', page, filters, sort, sortDirection ], async () => {
|
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 } });
|
const { data } = await http.get(`/api/application/nests/${nestId}/eggs`, {
|
||||||
|
params: { include: include.join(','), page, ...params },
|
||||||
|
});
|
||||||
|
|
||||||
return ({
|
return {
|
||||||
items: (data.data || []).map(rawDataToEgg),
|
items: (data.data || []).map(rawDataToEgg),
|
||||||
pagination: getPaginationSet(data.meta.pagination),
|
pagination: getPaginationSet(data.meta.pagination),
|
||||||
});
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,7 +15,7 @@ export interface Nest {
|
||||||
|
|
||||||
relations: {
|
relations: {
|
||||||
eggs: Egg[] | undefined;
|
eggs: Egg[] | undefined;
|
||||||
},
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rawDataToNest = ({ attributes }: FractalResponseData): Nest => ({
|
export const rawDataToNest = ({ attributes }: FractalResponseData): Nest => ({
|
||||||
|
@ -55,12 +55,14 @@ export default (include: string[] = []) => {
|
||||||
params.sort = (sortDirection ? '-' : '') + sort;
|
params.sort = (sortDirection ? '-' : '') + sort;
|
||||||
}
|
}
|
||||||
|
|
||||||
return useSWR<PaginatedResult<Nest>>([ 'nests', page, filters, sort, sortDirection ], async () => {
|
return useSWR<PaginatedResult<Nest>>(['nests', page, filters, sort, sortDirection], async () => {
|
||||||
const { data } = await http.get('/api/application/nests', { params: { include: include.join(','), page, ...params } });
|
const { data } = await http.get('/api/application/nests', {
|
||||||
|
params: { include: include.join(','), page, ...params },
|
||||||
|
});
|
||||||
|
|
||||||
return ({
|
return {
|
||||||
items: (data.data || []).map(rawDataToNest),
|
items: (data.data || []).map(rawDataToNest),
|
||||||
pagination: getPaginationSet(data.meta.pagination),
|
pagination: getPaginationSet(data.meta.pagination),
|
||||||
});
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,9 +3,14 @@ import { Nest, rawDataToNest } from '@/api/admin/nests/getNests';
|
||||||
|
|
||||||
export default (id: number, name: string, description: string | null, include: string[] = []): Promise<Nest> => {
|
export default (id: number, name: string, description: string | null, include: string[] = []): Promise<Nest> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.patch(`/api/application/nests/${id}`, {
|
http.patch(
|
||||||
name, description,
|
`/api/application/nests/${id}`,
|
||||||
}, { params: { include: include.join(',') } })
|
{
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
},
|
||||||
|
{ params: { include: include.join(',') } },
|
||||||
|
)
|
||||||
.then(({ data }) => resolve(rawDataToNest(data)))
|
.then(({ data }) => resolve(rawDataToNest(data)))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
|
|
|
@ -60,7 +60,7 @@ export interface Node extends Model {
|
||||||
export const getNode = async (id: string | number): Promise<WithRelationships<Node, 'location'>> => {
|
export const getNode = async (id: string | number): Promise<WithRelationships<Node, 'location'>> => {
|
||||||
const { data } = await http.get(`/api/application/nodes/${id}`, {
|
const { data } = await http.get(`/api/application/nodes/${id}`, {
|
||||||
params: {
|
params: {
|
||||||
include: [ 'location' ],
|
include: ['location'],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -75,7 +75,10 @@ export const searchNodes = async (params: QueryBuilderParams<'name'>): Promise<N
|
||||||
return data.data.map(Transformers.toNode);
|
return data.data.map(Transformers.toNode);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAllocations = async (id: string | number, params?: QueryBuilderParams<'ip' | 'server_id'>): Promise<Allocation[]> => {
|
export const getAllocations = async (
|
||||||
|
id: string | number,
|
||||||
|
params?: QueryBuilderParams<'ip' | 'server_id'>,
|
||||||
|
): Promise<Allocation[]> => {
|
||||||
const { data } = await http.get(`/api/application/nodes/${id}/allocations`, {
|
const { data } = await http.get(`/api/application/nodes/${id}/allocations`, {
|
||||||
params: withQueryBuilderParams(params),
|
params: withQueryBuilderParams(params),
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,12 +28,14 @@ export default (id: number, include: string[] = []) => {
|
||||||
params.sort = (sortDirection ? '-' : '') + sort;
|
params.sort = (sortDirection ? '-' : '') + sort;
|
||||||
}
|
}
|
||||||
|
|
||||||
return useSWR<PaginatedResult<Allocation>>([ 'allocations', page, filters, sort, sortDirection ], async () => {
|
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 } });
|
const { data } = await http.get(`/api/application/nodes/${id}/allocations`, {
|
||||||
|
params: { include: include.join(','), page, ...params },
|
||||||
|
});
|
||||||
|
|
||||||
return ({
|
return {
|
||||||
items: (data.data || []).map(rawDataToAllocation),
|
items: (data.data || []).map(rawDataToAllocation),
|
||||||
pagination: getPaginationSet(data.meta.pagination),
|
pagination: getPaginationSet(data.meta.pagination),
|
||||||
});
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -25,7 +25,7 @@ export interface Values {
|
||||||
export default (values: Values, include: string[] = []): Promise<Node> => {
|
export default (values: Values, include: string[] = []): Promise<Node> => {
|
||||||
const data = {};
|
const data = {};
|
||||||
|
|
||||||
Object.keys(values).forEach((key) => {
|
Object.keys(values).forEach(key => {
|
||||||
const key2 = key
|
const key2 = key
|
||||||
.replace('HTTP', 'Http')
|
.replace('HTTP', 'Http')
|
||||||
.replace('SFTP', 'Sftp')
|
.replace('SFTP', 'Sftp')
|
||||||
|
|
|
@ -11,9 +11,9 @@ export interface Allocation {
|
||||||
|
|
||||||
relations: {
|
relations: {
|
||||||
server?: Server;
|
server?: Server;
|
||||||
}
|
};
|
||||||
|
|
||||||
getDisplayText (): string;
|
getDisplayText(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rawDataToAllocation = ({ attributes }: FractalResponseData): Allocation => ({
|
export const rawDataToAllocation = ({ attributes }: FractalResponseData): Allocation => ({
|
||||||
|
@ -25,11 +25,14 @@ export const rawDataToAllocation = ({ attributes }: FractalResponseData): Alloca
|
||||||
assigned: attributes.assigned,
|
assigned: attributes.assigned,
|
||||||
|
|
||||||
relations: {
|
relations: {
|
||||||
server: attributes.relationships?.server?.object === 'server' ? rawDataToServer(attributes.relationships.server as FractalResponseData) : undefined,
|
server:
|
||||||
|
attributes.relationships?.server?.object === 'server'
|
||||||
|
? rawDataToServer(attributes.relationships.server as FractalResponseData)
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO: If IP is an IPv6, wrap IP in [].
|
// TODO: If IP is an IPv6, wrap IP in [].
|
||||||
getDisplayText (): string {
|
getDisplayText(): string {
|
||||||
if (attributes.alias !== null) {
|
if (attributes.alias !== null) {
|
||||||
return `${attributes.ip}:${attributes.port} (${attributes.alias})`;
|
return `${attributes.ip}:${attributes.port} (${attributes.alias})`;
|
||||||
}
|
}
|
||||||
|
@ -38,7 +41,7 @@ export const rawDataToAllocation = ({ attributes }: FractalResponseData): Alloca
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface Filters {
|
export interface Filters {
|
||||||
ip?: string
|
ip?: string;
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
server_id?: string;
|
server_id?: string;
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
|
|
|
@ -33,7 +33,7 @@ export interface Node {
|
||||||
relations: {
|
relations: {
|
||||||
databaseHost: Database | undefined;
|
databaseHost: Database | undefined;
|
||||||
location: Location | undefined;
|
location: Location | undefined;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rawDataToNode = ({ attributes }: FractalResponseData): Node => ({
|
export const rawDataToNode = ({ attributes }: FractalResponseData): Node => ({
|
||||||
|
@ -63,8 +63,15 @@ export const rawDataToNode = ({ attributes }: FractalResponseData): Node => ({
|
||||||
|
|
||||||
relations: {
|
relations: {
|
||||||
// eslint-disable-next-line camelcase
|
// 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,
|
databaseHost:
|
||||||
location: attributes.relationships?.location !== undefined ? rawDataToLocation(attributes.relationships.location as FractalResponseData) : undefined,
|
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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -96,12 +103,14 @@ export default (include: string[] = []) => {
|
||||||
params.sort = (sortDirection ? '-' : '') + sort;
|
params.sort = (sortDirection ? '-' : '') + sort;
|
||||||
}
|
}
|
||||||
|
|
||||||
return useSWR<PaginatedResult<Node>>([ 'nodes', page, filters, sort, sortDirection ], async () => {
|
return useSWR<PaginatedResult<Node>>(['nodes', page, filters, sort, sortDirection], async () => {
|
||||||
const { data } = await http.get('/api/application/nodes', { params: { include: include.join(','), page, ...params } });
|
const { data } = await http.get('/api/application/nodes', {
|
||||||
|
params: { include: include.join(','), page, ...params },
|
||||||
|
});
|
||||||
|
|
||||||
return ({
|
return {
|
||||||
items: (data.data || []).map(rawDataToNode),
|
items: (data.data || []).map(rawDataToNode),
|
||||||
pagination: getPaginationSet(data.meta.pagination),
|
pagination: getPaginationSet(data.meta.pagination),
|
||||||
});
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Node, rawDataToNode } from '@/api/admin/nodes/getNodes';
|
||||||
export default (id: number, node: Partial<Node>, include: string[] = []): Promise<Node> => {
|
export default (id: number, node: Partial<Node>, include: string[] = []): Promise<Node> => {
|
||||||
const data = {};
|
const data = {};
|
||||||
|
|
||||||
Object.keys(node).forEach((key) => {
|
Object.keys(node).forEach(key => {
|
||||||
const key2 = key
|
const key2 = key
|
||||||
.replace('HTTP', 'Http')
|
.replace('HTTP', 'Http')
|
||||||
.replace('SFTP', 'Sftp')
|
.replace('SFTP', 'Sftp')
|
||||||
|
|
|
@ -13,9 +13,14 @@ export const Context = createContext<Filters>();
|
||||||
|
|
||||||
const createRole = (name: string, description: string | null, include: string[] = []): Promise<UserRole> => {
|
const createRole = (name: string, description: string | null, include: string[] = []): Promise<UserRole> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.post('/api/application/roles', {
|
http.post(
|
||||||
name, description,
|
'/api/application/roles',
|
||||||
}, { params: { include: include.join(',') } })
|
{
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
},
|
||||||
|
{ params: { include: include.join(',') } },
|
||||||
|
)
|
||||||
.then(({ data }) => resolve(Transformers.toUserRole(data)))
|
.then(({ data }) => resolve(Transformers.toUserRole(data)))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
|
@ -48,18 +53,26 @@ const searchRoles = (filters?: { name?: string }): Promise<UserRole[]> => {
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.get('/api/application/roles', { params })
|
http.get('/api/application/roles', { params })
|
||||||
.then(response => resolve(
|
.then(response => resolve((response.data.data || []).map(Transformers.toUserRole)))
|
||||||
(response.data.data || []).map(Transformers.toUserRole)
|
|
||||||
))
|
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateRole = (id: number, name: string, description: string | null, include: string[] = []): Promise<UserRole> => {
|
const updateRole = (
|
||||||
|
id: number,
|
||||||
|
name: string,
|
||||||
|
description: string | null,
|
||||||
|
include: string[] = [],
|
||||||
|
): Promise<UserRole> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.patch(`/api/application/roles/${id}`, {
|
http.patch(
|
||||||
name, description,
|
`/api/application/roles/${id}`,
|
||||||
}, { params: { include: include.join(',') } })
|
{
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
},
|
||||||
|
{ params: { include: include.join(',') } },
|
||||||
|
)
|
||||||
.then(({ data }) => resolve(Transformers.toUserRole(data)))
|
.then(({ data }) => resolve(Transformers.toUserRole(data)))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
|
@ -83,21 +96,16 @@ const getRoles = (include: string[] = []) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
return useSWR<PaginatedResult<UserRole>>([ 'roles', page, filters, sort, sortDirection ], async () => {
|
return useSWR<PaginatedResult<UserRole>>(['roles', page, filters, sort, sortDirection], async () => {
|
||||||
const { data } = await http.get('/api/application/roles', { params: { include: include.join(','), page, ...params } });
|
const { data } = await http.get('/api/application/roles', {
|
||||||
|
params: { include: include.join(','), page, ...params },
|
||||||
|
});
|
||||||
|
|
||||||
return ({
|
return {
|
||||||
items: (data.data || []).map(Transformers.toUserRole),
|
items: (data.data || []).map(Transformers.toUserRole),
|
||||||
pagination: getPaginationSet(data.meta.pagination),
|
pagination: getPaginationSet(data.meta.pagination),
|
||||||
});
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export { createRole, deleteRole, getRole, searchRoles, updateRole, getRoles };
|
||||||
createRole,
|
|
||||||
deleteRole,
|
|
||||||
getRole,
|
|
||||||
searchRoles,
|
|
||||||
updateRole,
|
|
||||||
getRoles,
|
|
||||||
};
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ export interface CreateServerRequest {
|
||||||
cpu: number;
|
cpu: number;
|
||||||
threads: string;
|
threads: string;
|
||||||
oomDisabled: boolean;
|
oomDisabled: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
featureLimits: {
|
featureLimits: {
|
||||||
allocations: number;
|
allocations: number;
|
||||||
|
@ -39,41 +39,45 @@ export interface CreateServerRequest {
|
||||||
|
|
||||||
export default (r: CreateServerRequest, include: string[] = []): Promise<Server> => {
|
export default (r: CreateServerRequest, include: string[] = []): Promise<Server> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.post('/api/application/servers', {
|
http.post(
|
||||||
externalId: r.externalId,
|
'/api/application/servers',
|
||||||
name: r.name,
|
{
|
||||||
description: r.description,
|
externalId: r.externalId,
|
||||||
owner_id: r.ownerId,
|
name: r.name,
|
||||||
node_id: r.nodeId,
|
description: r.description,
|
||||||
|
owner_id: r.ownerId,
|
||||||
|
node_id: r.nodeId,
|
||||||
|
|
||||||
limits: {
|
limits: {
|
||||||
cpu: r.limits.cpu,
|
cpu: r.limits.cpu,
|
||||||
disk: r.limits.disk,
|
disk: r.limits.disk,
|
||||||
io: r.limits.io,
|
io: r.limits.io,
|
||||||
memory: r.limits.memory,
|
memory: r.limits.memory,
|
||||||
swap: r.limits.swap,
|
swap: r.limits.swap,
|
||||||
threads: r.limits.threads,
|
threads: r.limits.threads,
|
||||||
oom_killer: r.limits.oomDisabled,
|
oom_killer: r.limits.oomDisabled,
|
||||||
|
},
|
||||||
|
|
||||||
|
feature_limits: {
|
||||||
|
allocations: r.featureLimits.allocations,
|
||||||
|
backups: r.featureLimits.backups,
|
||||||
|
databases: r.featureLimits.databases,
|
||||||
|
},
|
||||||
|
|
||||||
|
allocation: {
|
||||||
|
default: r.allocation.default,
|
||||||
|
additional: r.allocation.additional,
|
||||||
|
},
|
||||||
|
|
||||||
|
startup: r.startup,
|
||||||
|
environment: r.environment,
|
||||||
|
egg_id: r.eggId,
|
||||||
|
image: r.image,
|
||||||
|
skip_scripts: r.skipScripts,
|
||||||
|
start_on_completion: r.startOnCompletion,
|
||||||
},
|
},
|
||||||
|
{ params: { include: include.join(',') } },
|
||||||
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)))
|
.then(({ data }) => resolve(rawDataToServer(data)))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
|
|
|
@ -41,7 +41,7 @@ export const rawDataToServerVariable = ({ attributes }: FractalResponseData): Se
|
||||||
|
|
||||||
export interface Server {
|
export interface Server {
|
||||||
id: number;
|
id: number;
|
||||||
externalId: string | null
|
externalId: string | null;
|
||||||
uuid: string;
|
uuid: string;
|
||||||
identifier: string;
|
identifier: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -56,13 +56,13 @@ export interface Server {
|
||||||
cpu: number;
|
cpu: number;
|
||||||
threads: string | null;
|
threads: string | null;
|
||||||
oomDisabled: boolean;
|
oomDisabled: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
featureLimits: {
|
featureLimits: {
|
||||||
databases: number;
|
databases: number;
|
||||||
allocations: number;
|
allocations: number;
|
||||||
backups: number;
|
backups: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
ownerId: number;
|
ownerId: number;
|
||||||
nodeId: number;
|
nodeId: number;
|
||||||
|
@ -74,7 +74,7 @@ export interface Server {
|
||||||
startup: string;
|
startup: string;
|
||||||
image: string;
|
image: string;
|
||||||
environment: Map<string, string>;
|
environment: Map<string, string>;
|
||||||
}
|
};
|
||||||
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
@ -85,57 +85,71 @@ export interface Server {
|
||||||
node?: Node;
|
node?: Node;
|
||||||
user?: User;
|
user?: User;
|
||||||
variables: ServerVariable[];
|
variables: ServerVariable[];
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rawDataToServer = ({ attributes }: FractalResponseData): Server => ({
|
export const rawDataToServer = ({ attributes }: FractalResponseData): Server =>
|
||||||
id: attributes.id,
|
({
|
||||||
externalId: attributes.external_id,
|
id: attributes.id,
|
||||||
uuid: attributes.uuid,
|
externalId: attributes.external_id,
|
||||||
identifier: attributes.identifier,
|
uuid: attributes.uuid,
|
||||||
name: attributes.name,
|
identifier: attributes.identifier,
|
||||||
description: attributes.description,
|
name: attributes.name,
|
||||||
status: attributes.status,
|
description: attributes.description,
|
||||||
|
status: attributes.status,
|
||||||
|
|
||||||
limits: {
|
limits: {
|
||||||
memory: attributes.limits.memory,
|
memory: attributes.limits.memory,
|
||||||
swap: attributes.limits.swap,
|
swap: attributes.limits.swap,
|
||||||
disk: attributes.limits.disk,
|
disk: attributes.limits.disk,
|
||||||
io: attributes.limits.io,
|
io: attributes.limits.io,
|
||||||
cpu: attributes.limits.cpu,
|
cpu: attributes.limits.cpu,
|
||||||
threads: attributes.limits.threads,
|
threads: attributes.limits.threads,
|
||||||
oomDisabled: attributes.limits.oom_disabled,
|
oomDisabled: attributes.limits.oom_disabled,
|
||||||
},
|
},
|
||||||
|
|
||||||
featureLimits: {
|
featureLimits: {
|
||||||
databases: attributes.feature_limits.databases,
|
databases: attributes.feature_limits.databases,
|
||||||
allocations: attributes.feature_limits.allocations,
|
allocations: attributes.feature_limits.allocations,
|
||||||
backups: attributes.feature_limits.backups,
|
backups: attributes.feature_limits.backups,
|
||||||
},
|
},
|
||||||
|
|
||||||
ownerId: attributes.owner_id,
|
ownerId: attributes.owner_id,
|
||||||
nodeId: attributes.node_id,
|
nodeId: attributes.node_id,
|
||||||
allocationId: attributes.allocation_id,
|
allocationId: attributes.allocation_id,
|
||||||
nestId: attributes.nest_id,
|
nestId: attributes.nest_id,
|
||||||
eggId: attributes.egg_id,
|
eggId: attributes.egg_id,
|
||||||
|
|
||||||
container: {
|
container: {
|
||||||
startup: attributes.container.startup,
|
startup: attributes.container.startup,
|
||||||
image: attributes.container.image,
|
image: attributes.container.image,
|
||||||
environment: attributes.container.environment,
|
environment: attributes.container.environment,
|
||||||
},
|
},
|
||||||
|
|
||||||
createdAt: new Date(attributes.created_at),
|
createdAt: new Date(attributes.created_at),
|
||||||
updatedAt: new Date(attributes.updated_at),
|
updatedAt: new Date(attributes.updated_at),
|
||||||
|
|
||||||
relations: {
|
relations: {
|
||||||
allocations: ((attributes.relationships?.allocations as FractalResponseList | undefined)?.data || []).map(rawDataToAllocation),
|
allocations: ((attributes.relationships?.allocations as FractalResponseList | undefined)?.data || []).map(
|
||||||
egg: attributes.relationships?.egg?.object === 'egg' ? rawDataToEgg(attributes.relationships.egg as FractalResponseData) : undefined,
|
rawDataToAllocation,
|
||||||
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,
|
egg:
|
||||||
variables: ((attributes.relationships?.variables as FractalResponseList | undefined)?.data || []).map(rawDataToServerVariable),
|
attributes.relationships?.egg?.object === 'egg'
|
||||||
},
|
? rawDataToEgg(attributes.relationships.egg as FractalResponseData)
|
||||||
}) as Server;
|
: 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 {
|
export interface Filters {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
@ -166,12 +180,14 @@ export default (include: string[] = []) => {
|
||||||
params.sort = (sortDirection ? '-' : '') + sort;
|
params.sort = (sortDirection ? '-' : '') + sort;
|
||||||
}
|
}
|
||||||
|
|
||||||
return useSWR<PaginatedResult<Server>>([ 'servers', page, filters, sort, sortDirection ], async () => {
|
return useSWR<PaginatedResult<Server>>(['servers', page, filters, sort, sortDirection], async () => {
|
||||||
const { data } = await http.get('/api/application/servers', { params: { include: include.join(','), page, ...params } });
|
const { data } = await http.get('/api/application/servers', {
|
||||||
|
params: { include: include.join(','), page, ...params },
|
||||||
|
});
|
||||||
|
|
||||||
return ({
|
return {
|
||||||
items: (data.data || []).map(rawDataToServer),
|
items: (data.data || []).map(rawDataToServer),
|
||||||
pagination: getPaginationSet(data.meta.pagination),
|
pagination: getPaginationSet(data.meta.pagination),
|
||||||
});
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,13 +14,13 @@ export interface Values {
|
||||||
cpu: number;
|
cpu: number;
|
||||||
threads: string;
|
threads: string;
|
||||||
oomDisabled: boolean;
|
oomDisabled: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
featureLimits: {
|
featureLimits: {
|
||||||
allocations: number;
|
allocations: number;
|
||||||
backups: number;
|
backups: number;
|
||||||
databases: number;
|
databases: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
allocationId: number;
|
allocationId: number;
|
||||||
addAllocations: number[];
|
addAllocations: number[];
|
||||||
|
@ -56,7 +56,7 @@ export default (id: number, server: Partial<Values>, include: string[] = []): Pr
|
||||||
add_allocations: server.addAllocations,
|
add_allocations: server.addAllocations,
|
||||||
remove_allocations: server.removeAllocations,
|
remove_allocations: server.removeAllocations,
|
||||||
},
|
},
|
||||||
{ params: { include: include.join(',') } }
|
{ params: { include: include.join(',') } },
|
||||||
)
|
)
|
||||||
.then(({ data }) => resolve(rawDataToServer(data)))
|
.then(({ data }) => resolve(rawDataToServer(data)))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
|
|
|
@ -20,7 +20,7 @@ export default (id: number, values: Partial<Values>, include: string[] = []): Pr
|
||||||
image: values.image,
|
image: values.image,
|
||||||
skip_scripts: values.skipScripts,
|
skip_scripts: values.skipScripts,
|
||||||
},
|
},
|
||||||
{ params: { include: include.join(',') } }
|
{ params: { include: include.join(',') } },
|
||||||
)
|
)
|
||||||
.then(({ data }) => resolve(rawDataToServer(data)))
|
.then(({ data }) => resolve(rawDataToServer(data)))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
|
|
|
@ -46,13 +46,13 @@ export default () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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} />
|
<SpinnerOverlay visible={isLoggingOut} />
|
||||||
<div className="mx-auto w-full flex items-center h-[3.5rem] max-w-[1200px]">
|
<div className="mx-auto flex h-[3.5rem] w-full max-w-[1200px] items-center">
|
||||||
<div id="logo" className="flex-1">
|
<div id="logo" className="flex-1">
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
className="text-2xl font-header px-4 no-underline text-neutral-200 hover:text-neutral-100 transition-colors duration-150"
|
className="px-4 font-header text-2xl text-neutral-200 no-underline transition-colors duration-150 hover:text-neutral-100"
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -69,7 +69,7 @@ export default () => {
|
||||||
|
|
||||||
<Tooltip placement="bottom" content="Account Settings">
|
<Tooltip placement="bottom" content="Account Settings">
|
||||||
<NavLink to="/account">
|
<NavLink to="/account">
|
||||||
<span className="flex items-center w-5 h-5">
|
<span className="flex h-5 w-5 items-center">
|
||||||
<Avatar.User />
|
<Avatar.User />
|
||||||
</span>
|
</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
|
@ -197,8 +197,6 @@ export default function EggSettingsContainer() {
|
||||||
dockerImages[alias] = image;
|
dockerImages[alias] = image;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(dockerImages);
|
|
||||||
|
|
||||||
updateEgg(egg.id, {
|
updateEgg(egg.id, {
|
||||||
...values,
|
...values,
|
||||||
dockerImages,
|
dockerImages,
|
||||||
|
|
|
@ -16,7 +16,6 @@ export default ({ selected }: { selected?: User }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelect = (user: User | null) => {
|
const onSelect = (user: User | null) => {
|
||||||
console.log(user);
|
|
||||||
setUser(user);
|
setUser(user);
|
||||||
setFieldValue('ownerId', user?.id || null);
|
setFieldValue('ownerId', user?.id || null);
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,8 +27,6 @@ export default () => {
|
||||||
// OOM Killer is enabled, rather than when disabled.
|
// OOM Killer is enabled, rather than when disabled.
|
||||||
values.limits.oomDisabled = !values.limits.oomDisabled;
|
values.limits.oomDisabled = !values.limits.oomDisabled;
|
||||||
|
|
||||||
console.log(values);
|
|
||||||
|
|
||||||
updateServer(server.id, values)
|
updateServer(server.id, values)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// setServer({ ...server, ...s });
|
// setServer({ ...server, ...s });
|
||||||
|
|
|
@ -33,7 +33,6 @@ function ServerStartupLineContainer({ egg, server }: { egg: Egg | null; server:
|
||||||
}
|
}
|
||||||
|
|
||||||
if (server.eggId === egg.id) {
|
if (server.eggId === egg.id) {
|
||||||
console.log(server.container);
|
|
||||||
setFieldValue('image', server.container.image);
|
setFieldValue('image', server.container.image);
|
||||||
setFieldValue('startup', server.container.startup || '');
|
setFieldValue('startup', server.container.startup || '');
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -30,14 +30,14 @@ const UserTableRow = ({ user, selected, onRowChange }: Props) => {
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td className={'whitespace-nowrap'}>
|
<td className={'whitespace-nowrap'}>
|
||||||
<div className={'flex justify-end items-center w-8'}>
|
<div className={'flex w-8 items-center justify-end'}>
|
||||||
<Checkbox checked={selected} onChange={e => onRowChange(user, e.currentTarget.checked)} />
|
<Checkbox checked={selected} onChange={e => onRowChange(user, e.currentTarget.checked)} />
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className={'pl-6 py-4 whitespace-nowrap'}>
|
<td className={'whitespace-nowrap py-4 pl-6'}>
|
||||||
<div className={'flex items-center'}>
|
<div className={'flex items-center'}>
|
||||||
<div className={'w-10 h-10'}>
|
<div className={'h-10 w-10'}>
|
||||||
<img src={user.avatarUrl} className={'w-10 h-10 rounded-full'} alt={'User avatar'} />
|
<img src={user.avatarUrl} className={'h-10 w-10 rounded-full'} alt={'User avatar'} />
|
||||||
</div>
|
</div>
|
||||||
<div className={'ml-4'}>
|
<div className={'ml-4'}>
|
||||||
<p className={'font-medium'}>{user.email}</p>
|
<p className={'font-medium'}>{user.email}</p>
|
||||||
|
@ -45,18 +45,18 @@ const UserTableRow = ({ user, selected, onRowChange }: Props) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className={'pl-2 py-4 whitespace-nowrap'}>
|
<td className={'whitespace-nowrap py-4 pl-2'}>
|
||||||
{user.isUsingTwoFactor && (
|
{user.isUsingTwoFactor && (
|
||||||
<span
|
<span
|
||||||
className={
|
className={
|
||||||
'bg-green-100 uppercase text-green-700 font-semibold text-xs px-2 py-0.5 rounded'
|
'rounded bg-green-100 px-2 py-0.5 text-xs font-semibold uppercase text-green-700'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
2-FA Enabled
|
2-FA Enabled
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className={'px-6 py-4 whitespace-nowrap'}>
|
<td className={'whitespace-nowrap px-6 py-4'}>
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<Dropdown.Button className={'px-2'}>
|
<Dropdown.Button className={'px-2'}>
|
||||||
<DotsVerticalIcon />
|
<DotsVerticalIcon />
|
||||||
|
|
|
@ -42,9 +42,9 @@ const UsersContainer = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={'flex justify-end mb-4'}>
|
<div className={'mb-4 flex justify-end'}>
|
||||||
<Button className={'shadow focus:ring-offset-2 focus:ring-offset-neutral-800'}>
|
<Button className={'shadow focus:ring-offset-2 focus:ring-offset-neutral-800'}>
|
||||||
Add User <PlusIcon className={'ml-2 w-5 h-5'} />
|
Add User <PlusIcon className={'ml-2 h-5 w-5'} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className={'relative flex items-center rounded-t bg-neutral-700 px-4 py-2'}>
|
<div className={'relative flex items-center rounded-t bg-neutral-700 px-4 py-2'}>
|
||||||
|
@ -68,7 +68,7 @@ const UsersContainer = () => {
|
||||||
<Transition.Fade as={Fragment} show={selected.length > 0} duration={'duration-75'}>
|
<Transition.Fade as={Fragment} show={selected.length > 0} duration={'duration-75'}>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'absolute rounded-t bg-neutral-700 w-full h-full top-0 left-0 flex items-center justify-end space-x-4 px-4'
|
'absolute top-0 left-0 flex h-full w-full items-center justify-end space-x-4 rounded-t bg-neutral-700 px-4'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className={'flex-1'}>
|
<div className={'flex-1'}>
|
||||||
|
@ -79,13 +79,13 @@ const UsersContainer = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button.Text shape={Shape.IconSquare}>
|
<Button.Text shape={Shape.IconSquare}>
|
||||||
<SupportIcon className={'w-4 h-4'} />
|
<SupportIcon className={'h-4 w-4'} />
|
||||||
</Button.Text>
|
</Button.Text>
|
||||||
<Button.Text shape={Shape.IconSquare}>
|
<Button.Text shape={Shape.IconSquare}>
|
||||||
<LockOpenIcon className={'w-4 h-4'} />
|
<LockOpenIcon className={'h-4 w-4'} />
|
||||||
</Button.Text>
|
</Button.Text>
|
||||||
<Button.Text shape={Shape.IconSquare}>
|
<Button.Text shape={Shape.IconSquare}>
|
||||||
<TrashIcon className={'w-4 h-4'} />
|
<TrashIcon className={'h-4 w-4'} />
|
||||||
</Button.Text>
|
</Button.Text>
|
||||||
</div>
|
</div>
|
||||||
</Transition.Fade>
|
</Transition.Fade>
|
||||||
|
@ -94,7 +94,7 @@ const UsersContainer = () => {
|
||||||
<thead className={'bg-neutral-900'}>
|
<thead className={'bg-neutral-900'}>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope={'col'} className={'w-8'} />
|
<th scope={'col'} className={'w-8'} />
|
||||||
<th scope={'col'} className={'text-left px-6 py-2 w-full'}>
|
<th scope={'col'} className={'w-full px-6 py-2 text-left'}>
|
||||||
Email
|
Email
|
||||||
</th>
|
</th>
|
||||||
<th scope={'col'} />
|
<th scope={'col'} />
|
||||||
|
|
|
@ -34,13 +34,13 @@ export default () => {
|
||||||
<PageContentBlock title={'Account Activity Log'}>
|
<PageContentBlock title={'Account Activity Log'}>
|
||||||
<FlashMessageRender byKey={'account'} />
|
<FlashMessageRender byKey={'account'} />
|
||||||
{(filters.filters?.event || filters.filters?.ip) && (
|
{(filters.filters?.event || filters.filters?.ip) && (
|
||||||
<div className={'flex justify-end mb-2'}>
|
<div className={'mb-2 flex justify-end'}>
|
||||||
<Link
|
<Link
|
||||||
to={'#'}
|
to={'#'}
|
||||||
className={classNames(btnStyles.button, btnStyles.text, 'w-full sm:w-auto')}
|
className={classNames(btnStyles.button, btnStyles.text, 'w-full sm:w-auto')}
|
||||||
onClick={() => setFilters(value => ({ ...value, filters: {} }))}
|
onClick={() => setFilters(value => ({ ...value, filters: {} }))}
|
||||||
>
|
>
|
||||||
Clear Filters <XCircleIcon className={'w-4 h-4 ml-2'} />
|
Clear Filters <XCircleIcon className={'ml-2 h-4 w-4'} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -28,7 +28,7 @@ export default ({ tokens, open, onClose }: RecoveryTokenDialogProps) => {
|
||||||
>
|
>
|
||||||
<Dialog.Icon position={'container'} type={'success'} />
|
<Dialog.Icon position={'container'} type={'success'} />
|
||||||
<CopyOnClick text={tokens.join('\n')} showInNotification={false}>
|
<CopyOnClick text={tokens.join('\n')} showInNotification={false}>
|
||||||
<pre className={'bg-slate-800 rounded p-2 mt-6'}>
|
<pre className={'mt-6 rounded bg-slate-800 p-2'}>
|
||||||
{grouped.map(value => (
|
{grouped.map(value => (
|
||||||
<span key={value.join('_')} className={'block'}>
|
<span key={value.join('_')} className={'block'}>
|
||||||
{value[0]}
|
{value[0]}
|
||||||
|
|
|
@ -62,7 +62,7 @@ const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
|
||||||
return (
|
return (
|
||||||
<form id={'enable-totp-form'} onSubmit={submit}>
|
<form id={'enable-totp-form'} onSubmit={submit}>
|
||||||
<FlashMessageRender byKey={'account:two-step'} className={'mt-4'} />
|
<FlashMessageRender byKey={'account:two-step'} className={'mt-4'} />
|
||||||
<div className={'flex items-center justify-center w-56 h-56 p-2 bg-slate-50 shadow mx-auto mt-6'}>
|
<div className={'mx-auto mt-6 flex h-56 w-56 items-center justify-center bg-slate-50 p-2 shadow'}>
|
||||||
{!token ? (
|
{!token ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
|
@ -70,7 +70,7 @@ const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<CopyOnClick text={token?.secret}>
|
<CopyOnClick text={token?.secret}>
|
||||||
<p className={'font-mono text-sm text-slate-100 text-center mt-2'}>
|
<p className={'mt-2 text-center font-mono text-sm text-slate-100'}>
|
||||||
{token?.secret.match(/.{1,4}/g)!.join(' ') || 'Loading...'}
|
{token?.secret.match(/.{1,4}/g)!.join(' ') || 'Loading...'}
|
||||||
</p>
|
</p>
|
||||||
</CopyOnClick>
|
</CopyOnClick>
|
||||||
|
@ -90,7 +90,7 @@ const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
|
||||||
autoComplete={'one-time-code'}
|
autoComplete={'one-time-code'}
|
||||||
pattern={'\\d{6}'}
|
pattern={'\\d{6}'}
|
||||||
/>
|
/>
|
||||||
<label htmlFor={'totp-password'} className={'block mt-3'}>
|
<label htmlFor={'totp-password'} className={'mt-3 block'}>
|
||||||
Account Password
|
Account Password
|
||||||
</label>
|
</label>
|
||||||
<Input.Text
|
<Input.Text
|
||||||
|
|
|
@ -9,7 +9,7 @@ interface CodeProps {
|
||||||
|
|
||||||
export default ({ dark, className, children }: CodeProps) => (
|
export default ({ dark, className, children }: CodeProps) => (
|
||||||
<code
|
<code
|
||||||
className={classNames('font-mono text-sm px-2 py-1 inline-block rounded', className, {
|
className={classNames('inline-block rounded px-2 py-1 font-mono text-sm', className, {
|
||||||
'bg-neutral-700': !dark,
|
'bg-neutral-700': !dark,
|
||||||
'bg-neutral-900 text-slate-100': dark,
|
'bg-neutral-900 text-slate-100': dark,
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -50,8 +50,8 @@ const CopyOnClick = ({ text, showInNotification = true, children }: CopyOnClickP
|
||||||
{copied && (
|
{copied && (
|
||||||
<Portal>
|
<Portal>
|
||||||
<FadeTransition show duration="duration-250" key={copied ? 'visible' : 'invisible'}>
|
<FadeTransition show duration="duration-250" key={copied ? 'visible' : 'invisible'}>
|
||||||
<div className="fixed z-50 bottom-0 right-0 m-4">
|
<div className="fixed bottom-0 right-0 z-50 m-4">
|
||||||
<div className="rounded-md py-3 px-4 text-slate-200 bg-neutral-600/95 shadow">
|
<div className="rounded-md bg-neutral-600/95 py-3 px-4 text-slate-200 shadow">
|
||||||
<p>
|
<p>
|
||||||
{showInNotification
|
{showInNotification
|
||||||
? `Copied "${String(text)}" to clipboard.`
|
? `Copied "${String(text)}" to clipboard.`
|
||||||
|
|
|
@ -26,7 +26,15 @@ interface IdObj {
|
||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Option = <T extends IdObj>({ selectId, id, item, active, isHighlighted, onClick, children }: OptionProps<T>) => {
|
export const Option = <T extends IdObj>({
|
||||||
|
selectId,
|
||||||
|
id,
|
||||||
|
item,
|
||||||
|
active,
|
||||||
|
isHighlighted,
|
||||||
|
onClick,
|
||||||
|
children,
|
||||||
|
}: OptionProps<T>) => {
|
||||||
if (isHighlighted === undefined) {
|
if (isHighlighted === undefined) {
|
||||||
isHighlighted = false;
|
isHighlighted = false;
|
||||||
}
|
}
|
||||||
|
@ -39,16 +47,32 @@ export const Option = <T extends IdObj>({ selectId, id, item, active, isHighligh
|
||||||
|
|
||||||
if (active) {
|
if (active) {
|
||||||
return (
|
return (
|
||||||
<li id={selectId + '-select-item-' + id} role="option" css={[ tw`relative py-2 pl-3 cursor-pointer select-none text-neutral-200 pr-9 hover:bg-neutral-700`, isHighlighted ? tw`bg-neutral-700` : null ]} onClick={onClick(item)}>
|
<li
|
||||||
|
id={selectId + '-select-item-' + id}
|
||||||
|
role="option"
|
||||||
|
css={[
|
||||||
|
tw`relative py-2 pl-3 cursor-pointer select-none text-neutral-200 pr-9 hover:bg-neutral-700`,
|
||||||
|
isHighlighted ? tw`bg-neutral-700` : null,
|
||||||
|
]}
|
||||||
|
onClick={onClick(item)}
|
||||||
|
>
|
||||||
<div css={tw`flex items-center`}>
|
<div css={tw`flex items-center`}>
|
||||||
<span css={tw`block font-medium truncate`}>
|
<span css={tw`block font-medium truncate`}>{children}</span>
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span css={tw`absolute inset-y-0 right-0 flex items-center pr-4`}>
|
<span css={tw`absolute inset-y-0 right-0 flex items-center pr-4`}>
|
||||||
<svg css={tw`w-5 h-5 text-primary-400`} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
<svg
|
||||||
<path clipRule="evenodd" fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"/>
|
css={tw`w-5 h-5 text-primary-400`}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
clipRule="evenodd"
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
@ -56,11 +80,17 @@ export const Option = <T extends IdObj>({ selectId, id, item, active, isHighligh
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li id={selectId + 'select-item-' + id} role="option" css={[ tw`relative py-2 pl-3 cursor-pointer select-none text-neutral-200 pr-9 hover:bg-neutral-700`, isHighlighted ? tw`bg-neutral-700` : null ]} onClick={onClick(item)}>
|
<li
|
||||||
|
id={selectId + 'select-item-' + id}
|
||||||
|
role="option"
|
||||||
|
css={[
|
||||||
|
tw`relative py-2 pl-3 cursor-pointer select-none text-neutral-200 pr-9 hover:bg-neutral-700`,
|
||||||
|
isHighlighted ? tw`bg-neutral-700` : null,
|
||||||
|
]}
|
||||||
|
onClick={onClick(item)}
|
||||||
|
>
|
||||||
<div css={tw`flex items-center`}>
|
<div css={tw`flex items-center`}>
|
||||||
<span css={tw`block font-normal truncate`}>
|
<span css={tw`block font-normal truncate`}>{children}</span>
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
@ -88,13 +118,27 @@ interface SearchableSelectProps<T> {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SearchableSelect = <T extends IdObj>({ id, name, label, placeholder, selected, setSelected, items, setItems, onSearch, onSelect, getSelectedText, children, className }: SearchableSelectProps<T>) => {
|
export const SearchableSelect = <T extends IdObj>({
|
||||||
const [ loading, setLoading ] = useState(false);
|
id,
|
||||||
const [ expanded, setExpanded ] = useState(false);
|
name,
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
selected,
|
||||||
|
setSelected,
|
||||||
|
items,
|
||||||
|
setItems,
|
||||||
|
onSearch,
|
||||||
|
onSelect,
|
||||||
|
getSelectedText,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: SearchableSelectProps<T>) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
const [ inputText, setInputText ] = useState('');
|
const [inputText, setInputText] = useState('');
|
||||||
|
|
||||||
const [ highlighted, setHighlighted ] = useState<number | null>(null);
|
const [highlighted, setHighlighted] = useState<number | null>(null);
|
||||||
|
|
||||||
const searchInput = createRef<HTMLInputElement>();
|
const searchInput = createRef<HTMLInputElement>();
|
||||||
const itemsList = createRef<HTMLDivElement>();
|
const itemsList = createRef<HTMLDivElement>();
|
||||||
|
@ -220,7 +264,7 @@ export const SearchableSelect = <T extends IdObj>({ id, name, label, placeholder
|
||||||
window.removeEventListener('mousedown', clickHandler);
|
window.removeEventListener('mousedown', clickHandler);
|
||||||
window.removeEventListener('contextmenu', contextmenuHandler);
|
window.removeEventListener('contextmenu', contextmenuHandler);
|
||||||
};
|
};
|
||||||
}, [ expanded ]);
|
}, [expanded]);
|
||||||
|
|
||||||
const onClick = (item: T) => () => {
|
const onClick = (item: T) => () => {
|
||||||
onSelect(item);
|
onSelect(item);
|
||||||
|
@ -235,13 +279,15 @@ export const SearchableSelect = <T extends IdObj>({ id, name, label, placeholder
|
||||||
}
|
}
|
||||||
|
|
||||||
setInputText(getSelectedText(selected) || '');
|
setInputText(getSelectedText(selected) || '');
|
||||||
}, [ selected ]);
|
}, [selected]);
|
||||||
|
|
||||||
// This shit is really stupid but works, so is it really stupid?
|
// This shit is really stupid but works, so is it really stupid?
|
||||||
const c = React.Children.map(children, child => React.cloneElement(child as ReactElement, {
|
const c = React.Children.map(children, child =>
|
||||||
isHighlighted: ((child as ReactElement).props as OptionProps<T>).id === highlighted,
|
React.cloneElement(child as ReactElement, {
|
||||||
onClick: onClick.bind(child),
|
isHighlighted: ((child as ReactElement).props as OptionProps<T>).id === highlighted,
|
||||||
}));
|
onClick: onClick.bind(child),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
|
@ -269,33 +315,57 @@ export const SearchableSelect = <T extends IdObj>({ id, name, label, placeholder
|
||||||
/>
|
/>
|
||||||
</InputSpinner>
|
</InputSpinner>
|
||||||
|
|
||||||
<div css={[ tw`absolute inset-y-0 right-0 flex items-center pr-2 ml-3`, !expanded && tw`pointer-events-none` ]}>
|
<div
|
||||||
{inputText !== '' && expanded &&
|
css={[
|
||||||
<svg css={tw`w-5 h-5 text-neutral-400 cursor-pointer`} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
|
tw`absolute inset-y-0 right-0 flex items-center pr-2 ml-3`,
|
||||||
onMouseDown={e => {
|
!expanded && tw`pointer-events-none`,
|
||||||
e.preventDefault();
|
]}
|
||||||
setInputText('');
|
>
|
||||||
}}
|
{inputText !== '' && expanded && (
|
||||||
|
<svg
|
||||||
|
css={tw`w-5 h-5 text-neutral-400 cursor-pointer`}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
onMouseDown={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
setInputText('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
clipRule="evenodd"
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
<svg
|
||||||
|
css={tw`w-5 h-5 text-neutral-400 pointer-events-none`}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<path clipRule="evenodd" fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"/>
|
<path
|
||||||
</svg>
|
clipRule="evenodd"
|
||||||
}
|
fillRule="evenodd"
|
||||||
<svg css={tw`w-5 h-5 text-neutral-400 pointer-events-none`} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"
|
||||||
<path clipRule="evenodd" fillRule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dropdown ref={itemsList} expanded={expanded}>
|
<Dropdown ref={itemsList} expanded={expanded}>
|
||||||
{items === null || items.length < 1 ?
|
{items === null || items.length < 1 ? (
|
||||||
items === null || inputText.length < 2 ?
|
items === null || inputText.length < 2 ? (
|
||||||
<div css={tw`flex flex-row items-center h-10 px-3`}>
|
<div css={tw`flex flex-row items-center h-10 px-3`}>
|
||||||
<p css={tw`text-sm`}>Please type 2 or more characters.</p>
|
<p css={tw`text-sm`}>Please type 2 or more characters.</p>
|
||||||
</div>
|
</div>
|
||||||
:
|
) : (
|
||||||
<div css={tw`flex flex-row items-center h-10 px-3`}>
|
<div css={tw`flex flex-row items-center h-10 px-3`}>
|
||||||
<p css={tw`text-sm`}>No results found.</p>
|
<p css={tw`text-sm`}>No results found.</p>
|
||||||
</div>
|
</div>
|
||||||
:
|
)
|
||||||
|
) : (
|
||||||
<ul
|
<ul
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
role={id + '-select'}
|
role={id + '-select'}
|
||||||
|
@ -305,7 +375,7 @@ export const SearchableSelect = <T extends IdObj>({ id, name, label, placeholder
|
||||||
>
|
>
|
||||||
{c}
|
{c}
|
||||||
</ul>
|
</ul>
|
||||||
}
|
)}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,22 @@
|
||||||
import { CSSObject } from '@emotion/serialize';
|
import { CSSObject } from '@emotion/serialize';
|
||||||
import { Field as FormikField, FieldProps } from 'formik';
|
import { Field as FormikField, FieldProps } from 'formik';
|
||||||
import React, { forwardRef } from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
import Select, { ContainerProps, ControlProps, GroupProps, IndicatorContainerProps, IndicatorProps, InputProps, MenuListComponentProps, MenuProps, MultiValueProps, OptionProps, PlaceholderProps, SingleValueProps, StylesConfig, ValueContainerProps } from 'react-select';
|
import Select, {
|
||||||
|
ContainerProps,
|
||||||
|
ControlProps,
|
||||||
|
GroupProps,
|
||||||
|
IndicatorContainerProps,
|
||||||
|
IndicatorProps,
|
||||||
|
InputProps,
|
||||||
|
MenuListComponentProps,
|
||||||
|
MenuProps,
|
||||||
|
MultiValueProps,
|
||||||
|
OptionProps,
|
||||||
|
PlaceholderProps,
|
||||||
|
SingleValueProps,
|
||||||
|
StylesConfig,
|
||||||
|
ValueContainerProps,
|
||||||
|
} from 'react-select';
|
||||||
import Async from 'react-select/async';
|
import Async from 'react-select/async';
|
||||||
import Creatable from 'react-select/creatable';
|
import Creatable from 'react-select/creatable';
|
||||||
import tw, { theme } from 'twin.macro';
|
import tw, { theme } from 'twin.macro';
|
||||||
|
@ -42,10 +57,9 @@ export const SelectStyle: StylesConfig<T, any, any> = {
|
||||||
borderWidth: '2px',
|
borderWidth: '2px',
|
||||||
color: theme`colors.neutral.200`,
|
color: theme`colors.neutral.200`,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
boxShadow: props.isFocused ?
|
boxShadow: props.isFocused
|
||||||
'rgb(255, 255, 255) 0px 0px 0px 0px, rgba(36, 135, 235, 0.5) 0px 0px 0px 2px, rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px'
|
? 'rgb(255, 255, 255) 0px 0px 0px 0px, rgba(36, 135, 235, 0.5) 0px 0px 0px 2px, rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px'
|
||||||
:
|
: undefined,
|
||||||
undefined,
|
|
||||||
|
|
||||||
':hover': {
|
':hover': {
|
||||||
borderColor: !props.isFocused ? theme`colors.neutral.400` : theme`colors.primary.300`,
|
borderColor: !props.isFocused ? theme`colors.neutral.400` : theme`colors.primary.300`,
|
||||||
|
@ -228,67 +242,74 @@ interface SelectFieldProps {
|
||||||
isSearchable?: boolean;
|
isSearchable?: boolean;
|
||||||
|
|
||||||
isCreatable?: boolean;
|
isCreatable?: boolean;
|
||||||
isValidNewOption?: ((
|
isValidNewOption?:
|
||||||
inputValue: string,
|
| ((inputValue: string, value: ValueType<any, boolean>, options: ReadonlyArray<any>) => boolean)
|
||||||
value: ValueType<any, boolean>,
|
| undefined;
|
||||||
options: ReadonlyArray<any>,
|
|
||||||
) => boolean) | undefined;
|
|
||||||
|
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SelectField = forwardRef<HTMLElement, SelectFieldProps>(
|
const SelectField = forwardRef<HTMLElement, SelectFieldProps>(function Select2(
|
||||||
function Select2 ({ id, name, label, description, validate, className, isMulti, isCreatable, ...props }, ref) {
|
{ id, name, label, description, validate, className, isMulti, isCreatable, ...props },
|
||||||
const { options } = props;
|
ref,
|
||||||
|
) {
|
||||||
|
const { options } = props;
|
||||||
|
|
||||||
const onChange = (options: Option | Option[], name: string, setFieldValue: (field: string, value: any, shouldValidate?: boolean) => void) => {
|
const onChange = (
|
||||||
if (isMulti) {
|
options: Option | Option[],
|
||||||
setFieldValue(name, (options as Option[]).map(o => o.value));
|
name: string,
|
||||||
return;
|
setFieldValue: (field: string, value: any, shouldValidate?: boolean) => void,
|
||||||
}
|
) => {
|
||||||
|
if (isMulti) {
|
||||||
|
setFieldValue(
|
||||||
|
name,
|
||||||
|
(options as Option[]).map(o => o.value),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setFieldValue(name, (options as Option).value);
|
setFieldValue(name, (options as Option).value);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormikField innerRef={ref} name={name} validate={validate}>
|
<FormikField innerRef={ref} name={name} validate={validate}>
|
||||||
{({ field, form: { errors, touched, setFieldValue } }: FieldProps) => (
|
{({ field, form: { errors, touched, setFieldValue } }: FieldProps) => (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{label && <Label htmlFor={id}>{label}</Label>}
|
{label && <Label htmlFor={id}>{label}</Label>}
|
||||||
{isCreatable ?
|
{isCreatable ? (
|
||||||
<Creatable
|
<Creatable
|
||||||
{...field}
|
{...field}
|
||||||
{...props}
|
{...props}
|
||||||
styles={SelectStyle}
|
styles={SelectStyle}
|
||||||
options={options}
|
options={options}
|
||||||
value={(options ? options.find(o => o.value === field.value) : '') as any}
|
value={(options ? options.find(o => o.value === field.value) : '') as any}
|
||||||
onChange={o => onChange(o, name, setFieldValue)}
|
onChange={o => onChange(o, name, setFieldValue)}
|
||||||
isMulti={isMulti}
|
isMulti={isMulti}
|
||||||
/>
|
/>
|
||||||
:
|
) : (
|
||||||
<Select
|
<Select
|
||||||
{...field}
|
{...field}
|
||||||
{...props}
|
{...props}
|
||||||
styles={SelectStyle}
|
styles={SelectStyle}
|
||||||
options={options}
|
options={options}
|
||||||
value={(options ? options.find(o => o.value === field.value) : '') as any}
|
value={(options ? options.find(o => o.value === field.value) : '') as any}
|
||||||
onChange={o => onChange(o, name, setFieldValue)}
|
onChange={o => onChange(o, name, setFieldValue)}
|
||||||
isMulti={isMulti}
|
isMulti={isMulti}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
{touched[field.name] && errors[field.name] ?
|
{touched[field.name] && errors[field.name] ? (
|
||||||
<p css={tw`text-red-200 text-xs mt-1`}>
|
<p css={tw`text-red-200 text-xs mt-1`}>
|
||||||
{(errors[field.name] as string).charAt(0).toUpperCase() + (errors[field.name] as string).slice(1)}
|
{(errors[field.name] as string).charAt(0).toUpperCase() +
|
||||||
</p>
|
(errors[field.name] as string).slice(1)}
|
||||||
:
|
</p>
|
||||||
description ? <p css={tw`text-neutral-400 text-xs mt-1`}>{description}</p> : null
|
) : description ? (
|
||||||
}
|
<p css={tw`text-neutral-400 text-xs mt-1`}>{description}</p>
|
||||||
</div>
|
) : null}
|
||||||
)}
|
</div>
|
||||||
</FormikField>
|
)}
|
||||||
);
|
</FormikField>
|
||||||
}
|
);
|
||||||
);
|
});
|
||||||
|
|
||||||
interface AsyncSelectFieldProps {
|
interface AsyncSelectFieldProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
@ -305,43 +326,52 @@ interface AsyncSelectFieldProps {
|
||||||
loadOptions(inputValue: string, callback: (options: Array<Option>) => void): void;
|
loadOptions(inputValue: string, callback: (options: Array<Option>) => void): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AsyncSelectField = forwardRef<HTMLElement, AsyncSelectFieldProps>(
|
const AsyncSelectField = forwardRef<HTMLElement, AsyncSelectFieldProps>(function AsyncSelect2(
|
||||||
function AsyncSelect2 ({ id, name, label, description, validate, className, isMulti, ...props }, ref) {
|
{ id, name, label, description, validate, className, isMulti, ...props },
|
||||||
const onChange = (options: Option | Option[], name: string, setFieldValue: (field: string, value: any, shouldValidate?: boolean) => void) => {
|
ref,
|
||||||
if (isMulti) {
|
) {
|
||||||
setFieldValue(name, (options as Option[]).map(o => Number(o.value)));
|
const onChange = (
|
||||||
return;
|
options: Option | Option[],
|
||||||
}
|
name: string,
|
||||||
|
setFieldValue: (field: string, value: any, shouldValidate?: boolean) => void,
|
||||||
|
) => {
|
||||||
|
if (isMulti) {
|
||||||
|
setFieldValue(
|
||||||
|
name,
|
||||||
|
(options as Option[]).map(o => Number(o.value)),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setFieldValue(name, Number((options as Option).value));
|
setFieldValue(name, Number((options as Option).value));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormikField innerRef={ref} name={name} validate={validate}>
|
<FormikField innerRef={ref} name={name} validate={validate}>
|
||||||
{({ field, form: { errors, touched, setFieldValue } }: FieldProps) => (
|
{({ field, form: { errors, touched, setFieldValue } }: FieldProps) => (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{label && <Label htmlFor={id}>{label}</Label>}
|
{label && <Label htmlFor={id}>{label}</Label>}
|
||||||
<Async
|
<Async
|
||||||
{...props}
|
{...props}
|
||||||
id={id}
|
id={id}
|
||||||
name={name}
|
name={name}
|
||||||
styles={SelectStyle}
|
styles={SelectStyle}
|
||||||
onChange={o => onChange(o, name, setFieldValue)}
|
onChange={o => onChange(o, name, setFieldValue)}
|
||||||
isMulti={isMulti}
|
isMulti={isMulti}
|
||||||
/>
|
/>
|
||||||
{touched[field.name] && errors[field.name] ?
|
{touched[field.name] && errors[field.name] ? (
|
||||||
<p css={tw`text-red-200 text-xs mt-1`}>
|
<p css={tw`text-red-200 text-xs mt-1`}>
|
||||||
{(errors[field.name] as string).charAt(0).toUpperCase() + (errors[field.name] as string).slice(1)}
|
{(errors[field.name] as string).charAt(0).toUpperCase() +
|
||||||
</p>
|
(errors[field.name] as string).slice(1)}
|
||||||
:
|
</p>
|
||||||
description ? <p css={tw`text-neutral-400 text-xs mt-1`}>{description}</p> : null
|
) : description ? (
|
||||||
}
|
<p css={tw`text-neutral-400 text-xs mt-1`}>{description}</p>
|
||||||
</div>
|
) : null}
|
||||||
)}
|
</div>
|
||||||
</FormikField>
|
)}
|
||||||
);
|
</FormikField>
|
||||||
}
|
);
|
||||||
);
|
});
|
||||||
|
|
||||||
export default SelectField;
|
export default SelectField;
|
||||||
export { AsyncSelectField };
|
export { AsyncSelectField };
|
||||||
|
|
|
@ -44,13 +44,13 @@ export default ({ activity, children }: Props) => {
|
||||||
const properties = wrapProperties(activity.properties);
|
const properties = wrapProperties(activity.properties);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'grid grid-cols-10 py-4 border-b-2 border-slate-800 last:rounded-b last:border-0 group'}>
|
<div className={'group grid grid-cols-10 border-b-2 border-slate-800 py-4 last:rounded-b last:border-0'}>
|
||||||
<div className={'hidden sm:flex sm:col-span-1 items-center justify-center select-none'}>
|
<div className={'hidden select-none items-center justify-center sm:col-span-1 sm:flex'}>
|
||||||
<div className={'flex items-center w-10 h-10 rounded-full bg-slate-600 overflow-hidden'}>
|
<div className={'flex h-10 w-10 items-center overflow-hidden rounded-full bg-slate-600'}>
|
||||||
<Avatar name={actor?.uuid || 'system'} />
|
<Avatar name={actor?.uuid || 'system'} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={'col-span-10 sm:col-span-9 flex'}>
|
<div className={'col-span-10 flex sm:col-span-9'}>
|
||||||
<div className={'flex-1 px-4 sm:px-0'}>
|
<div className={'flex-1 px-4 sm:px-0'}>
|
||||||
<div className={'flex items-center text-slate-50'}>
|
<div className={'flex items-center text-slate-50'}>
|
||||||
<Tooltip placement={'top'} content={actor?.email || 'System User'}>
|
<Tooltip placement={'top'} content={actor?.email || 'System User'}>
|
||||||
|
@ -59,7 +59,7 @@ export default ({ activity, children }: Props) => {
|
||||||
<span className={'text-slate-400'}> — </span>
|
<span className={'text-slate-400'}> — </span>
|
||||||
<Link
|
<Link
|
||||||
to={`#${pathTo({ event: activity.event })}`}
|
to={`#${pathTo({ event: activity.event })}`}
|
||||||
className={'transition-colors duration-75 active:text-cyan-400 hover:text-cyan-400'}
|
className={'transition-colors duration-75 hover:text-cyan-400 active:text-cyan-400'}
|
||||||
>
|
>
|
||||||
{activity.event}
|
{activity.event}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -11,7 +11,7 @@ export default ({ meta }: { meta: Record<string, unknown> }) => {
|
||||||
<Dialog open={open} onClose={() => setOpen(false)} hideCloseIcon title={'Metadata'}>
|
<Dialog open={open} onClose={() => setOpen(false)} hideCloseIcon title={'Metadata'}>
|
||||||
<pre
|
<pre
|
||||||
className={
|
className={
|
||||||
'bg-slate-900 rounded p-2 font-mono text-sm leading-relaxed overflow-x-scroll whitespace-pre-wrap'
|
'overflow-x-scroll whitespace-pre-wrap rounded bg-slate-900 p-2 font-mono text-sm leading-relaxed'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{JSON.stringify(meta, null, 2)}
|
{JSON.stringify(meta, null, 2)}
|
||||||
|
@ -23,11 +23,11 @@ export default ({ meta }: { meta: Record<string, unknown> }) => {
|
||||||
<button
|
<button
|
||||||
aria-describedby={'View additional event metadata'}
|
aria-describedby={'View additional event metadata'}
|
||||||
className={
|
className={
|
||||||
'p-2 transition-colors duration-100 text-slate-400 group-hover:text-slate-300 group-hover:hover:text-slate-50'
|
'p-2 text-slate-400 transition-colors duration-100 group-hover:text-slate-300 group-hover:hover:text-slate-50'
|
||||||
}
|
}
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
>
|
>
|
||||||
<ClipboardListIcon className={'w-5 h-5'} />
|
<ClipboardListIcon className={'h-5 w-5'} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -12,7 +12,7 @@ export default ({ type, className, children }: AlertProps) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'flex items-center border-l-8 text-slate-50 rounded-md shadow px-4 py-3',
|
'flex items-center rounded-md border-l-8 px-4 py-3 text-slate-50 shadow',
|
||||||
{
|
{
|
||||||
['border-red-500 bg-red-500/25']: type === 'danger',
|
['border-red-500 bg-red-500/25']: type === 'danger',
|
||||||
['border-yellow-500 bg-yellow-500/25']: type === 'warning',
|
['border-yellow-500 bg-yellow-500/25']: type === 'warning',
|
||||||
|
@ -21,9 +21,9 @@ export default ({ type, className, children }: AlertProps) => {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{type === 'danger' ? (
|
{type === 'danger' ? (
|
||||||
<ShieldExclamationIcon className={'w-6 h-6 text-red-400 mr-2'} />
|
<ShieldExclamationIcon className={'mr-2 h-6 w-6 text-red-400'} />
|
||||||
) : (
|
) : (
|
||||||
<ExclamationIcon className={'w-6 h-6 text-yellow-500 mr-2'} />
|
<ExclamationIcon className={'mr-2 h-6 w-6 text-yellow-500'} />
|
||||||
)}
|
)}
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -73,8 +73,8 @@ export default ({
|
||||||
open={open}
|
open={open}
|
||||||
onClose={onDialogClose}
|
onClose={onDialogClose}
|
||||||
>
|
>
|
||||||
<div className={'fixed inset-0 bg-slate-900/50 z-40'} />
|
<div className={'fixed inset-0 z-40 bg-slate-900/50'} />
|
||||||
<div className={'fixed inset-0 overflow-y-auto z-50'}>
|
<div className={'fixed inset-0 z-50 overflow-y-auto'}>
|
||||||
<div
|
<div
|
||||||
ref={container}
|
ref={container}
|
||||||
className={styles.container}
|
className={styles.container}
|
||||||
|
@ -89,9 +89,9 @@ export default ({
|
||||||
variants={variants}
|
variants={variants}
|
||||||
className={styles.panel}
|
className={styles.panel}
|
||||||
>
|
>
|
||||||
<div className={'flex p-6 pb-0 overflow-y-auto'}>
|
<div className={'flex overflow-y-auto p-6 pb-0'}>
|
||||||
{iconPosition === 'container' && icon}
|
{iconPosition === 'container' && icon}
|
||||||
<div className={'flex-1 max-h-[70vh] min-w-0'}>
|
<div className={'max-h-[70vh] min-w-0 flex-1'}>
|
||||||
<div className={'flex items-center'}>
|
<div className={'flex items-center'}>
|
||||||
{iconPosition !== 'container' && icon}
|
{iconPosition !== 'container' && icon}
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -8,7 +8,7 @@ export default ({ children }: { children: React.ReactNode }) => {
|
||||||
|
|
||||||
useDeepCompareEffect(() => {
|
useDeepCompareEffect(() => {
|
||||||
setFooter(
|
setFooter(
|
||||||
<div className={'px-6 py-3 bg-slate-700 flex items-center justify-end space-x-3 rounded-b'}>
|
<div className={'flex items-center justify-end space-x-3 rounded-b bg-slate-700 px-6 py-3'}>
|
||||||
{children}
|
{children}
|
||||||
</div>,
|
</div>,
|
||||||
);
|
);
|
||||||
|
|
|
@ -18,7 +18,7 @@ export default ({ type, position, className }: DialogIconProps) => {
|
||||||
|
|
||||||
setIcon(
|
setIcon(
|
||||||
<div className={classNames(styles.dialog_icon, styles[type], className)}>
|
<div className={classNames(styles.dialog_icon, styles[type], className)}>
|
||||||
<Icon className={'w-6 h-6'} />
|
<Icon className={'h-6 w-6'} />
|
||||||
</div>,
|
</div>,
|
||||||
);
|
);
|
||||||
}, [type, className]);
|
}, [type, className]);
|
||||||
|
|
|
@ -12,7 +12,7 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const DropdownGap = ({ invisible }: { invisible?: boolean }) => (
|
const DropdownGap = ({ invisible }: { invisible?: boolean }) => (
|
||||||
<div className={classNames('border m-2', { 'border-neutral-700': !invisible, 'border-transparent': invisible })} />
|
<div className={classNames('m-2 border', { 'border-neutral-700': !invisible, 'border-transparent': invisible })} />
|
||||||
);
|
);
|
||||||
|
|
||||||
type TypedChild = (React.ReactChild | React.ReactFragment | React.ReactPortal) & {
|
type TypedChild = (React.ReactChild | React.ReactFragment | React.ReactPortal) & {
|
||||||
|
|
|
@ -37,7 +37,7 @@ const PaginationFooter = ({ pagination, className, onPageSelect }: Props) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('flex items-center justify-between my-2', className)}>
|
<div className={classNames('my-2 flex items-center justify-between', className)}>
|
||||||
<p className={'text-sm text-neutral-500'}>
|
<p className={'text-sm text-neutral-500'}>
|
||||||
Showing
|
Showing
|
||||||
<span className={'font-semibold text-neutral-400'}>
|
<span className={'font-semibold text-neutral-400'}>
|
||||||
|
@ -50,7 +50,7 @@ const PaginationFooter = ({ pagination, className, onPageSelect }: Props) => {
|
||||||
{pagination.totalPages > 1 && (
|
{pagination.totalPages > 1 && (
|
||||||
<div className={'flex space-x-1'}>
|
<div className={'flex space-x-1'}>
|
||||||
<Button.Text {...buttonProps(1)} disabled={pages.previous.length !== 2}>
|
<Button.Text {...buttonProps(1)} disabled={pages.previous.length !== 2}>
|
||||||
<ChevronDoubleLeftIcon className={'w-3 h-3'} />
|
<ChevronDoubleLeftIcon className={'h-3 w-3'} />
|
||||||
</Button.Text>
|
</Button.Text>
|
||||||
{pages.previous.reverse().map(value => (
|
{pages.previous.reverse().map(value => (
|
||||||
<Button.Text key={`previous-${value}`} {...buttonProps(value)}>
|
<Button.Text key={`previous-${value}`} {...buttonProps(value)}>
|
||||||
|
@ -66,7 +66,7 @@ const PaginationFooter = ({ pagination, className, onPageSelect }: Props) => {
|
||||||
</Button.Text>
|
</Button.Text>
|
||||||
))}
|
))}
|
||||||
<Button.Text {...buttonProps(total)} disabled={pages.next.length !== 2}>
|
<Button.Text {...buttonProps(total)} disabled={pages.next.length !== 2}>
|
||||||
<ChevronDoubleRightIcon className={'w-3 h-3'} />
|
<ChevronDoubleRightIcon className={'h-3 w-3'} />
|
||||||
</Button.Text>
|
</Button.Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -3,14 +3,18 @@ import { PaginationDataSet } from '@/api/http';
|
||||||
|
|
||||||
const TFootPaginated = ({ pagination, span }: { span: number; pagination: PaginationDataSet }) => {
|
const TFootPaginated = ({ pagination, span }: { span: number; pagination: PaginationDataSet }) => {
|
||||||
const start = (pagination.currentPage - 1) * pagination.perPage;
|
const start = (pagination.currentPage - 1) * pagination.perPage;
|
||||||
const end = ((pagination.currentPage - 1) * pagination.perPage) + pagination.count;
|
const end = (pagination.currentPage - 1) * pagination.perPage + pagination.count;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr className={'bg-neutral-800'}>
|
<tr className={'bg-neutral-800'}>
|
||||||
<td scope={'col'} colSpan={span} className={'px-4 py-2'}>
|
<td scope={'col'} colSpan={span} className={'px-4 py-2'}>
|
||||||
<p className={'text-sm text-neutral-500'}>
|
<p className={'text-sm text-neutral-500'}>
|
||||||
Showing <span className={'font-semibold text-neutral-400'}>{Math.max(start, Math.min(pagination.total, 1))}</span> to
|
Showing{' '}
|
||||||
|
<span className={'font-semibold text-neutral-400'}>
|
||||||
|
{Math.max(start, Math.min(pagination.total, 1))}
|
||||||
|
</span>{' '}
|
||||||
|
to
|
||||||
<span className={'font-semibold text-neutral-400'}>{end}</span> of
|
<span className={'font-semibold text-neutral-400'}>{end}</span> of
|
||||||
<span className={'font-semibold text-neutral-400'}>{pagination.total}</span> results.
|
<span className={'font-semibold text-neutral-400'}>{pagination.total}</span> results.
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -108,7 +108,7 @@ export default ({ children, ...props }: Props) => {
|
||||||
ay || 0,
|
ay || 0,
|
||||||
)}px) rotate(45deg)`,
|
)}px) rotate(45deg)`,
|
||||||
}}
|
}}
|
||||||
className={classNames('absolute bg-slate-900 w-3 h-3', side)}
|
className={classNames('absolute h-3 w-3 bg-slate-900', side)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
@ -35,20 +35,20 @@ export default () => {
|
||||||
<ServerContentBlock title={'Activity Log'}>
|
<ServerContentBlock title={'Activity Log'}>
|
||||||
<FlashMessageRender byKey={'server:activity'} />
|
<FlashMessageRender byKey={'server:activity'} />
|
||||||
{(filters.filters?.event || filters.filters?.ip) && (
|
{(filters.filters?.event || filters.filters?.ip) && (
|
||||||
<div className={'flex justify-end mb-2'}>
|
<div className={'mb-2 flex justify-end'}>
|
||||||
<Link
|
<Link
|
||||||
to={'#'}
|
to={'#'}
|
||||||
className={classNames(btnStyles.button, btnStyles.text, 'w-full sm:w-auto')}
|
className={classNames(btnStyles.button, btnStyles.text, 'w-full sm:w-auto')}
|
||||||
onClick={() => setFilters(value => ({ ...value, filters: {} }))}
|
onClick={() => setFilters(value => ({ ...value, filters: {} }))}
|
||||||
>
|
>
|
||||||
Clear Filters <XCircleIcon className={'w-4 h-4 ml-2'} />
|
Clear Filters <XCircleIcon className={'ml-2 h-4 w-4'} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!data && isValidating ? (
|
{!data && isValidating ? (
|
||||||
<Spinner centered />
|
<Spinner centered />
|
||||||
) : !data?.items.length ? (
|
) : !data?.items.length ? (
|
||||||
<p className={'text-sm text-center text-slate-400'}>No activity logs available for this server.</p>
|
<p className={'text-center text-sm text-slate-400'}>No activity logs available for this server.</p>
|
||||||
) : (
|
) : (
|
||||||
<div className={'bg-slate-700'}>
|
<div className={'bg-slate-700'}>
|
||||||
{data?.items.map(activity => (
|
{data?.items.map(activity => (
|
||||||
|
|
|
@ -12,7 +12,7 @@ export default ({ title, legend, children }: ChartBlockProps) => (
|
||||||
<div className={classNames(styles.chart_container, 'group')}>
|
<div className={classNames(styles.chart_container, 'group')}>
|
||||||
<div className={'flex items-center justify-between px-4 py-2'}>
|
<div className={'flex items-center justify-between px-4 py-2'}>
|
||||||
<h3 className={'font-header transition-colors duration-100 group-hover:text-slate-50'}>{title}</h3>
|
<h3 className={'font-header transition-colors duration-100 group-hover:text-slate-50'}>{title}</h3>
|
||||||
{legend && <p className={'text-sm flex items-center'}>{legend}</p>}
|
{legend && <p className={'flex items-center text-sm'}>{legend}</p>}
|
||||||
</div>
|
</div>
|
||||||
<div className={'z-10 ml-2'}>{children}</div>
|
<div className={'z-10 ml-2'}>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -232,11 +232,11 @@ export default () => {
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'text-slate-100 peer-focus:text-slate-50 peer-focus:animate-pulse',
|
'text-slate-100 peer-focus:animate-pulse peer-focus:text-slate-50',
|
||||||
styles.command_icon,
|
styles.command_icon,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ChevronDoubleRightIcon className={'w-4 h-4'} />
|
<ChevronDoubleRightIcon className={'h-4 w-4'} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -33,26 +33,26 @@ function ServerConsoleContainer() {
|
||||||
: 'This server is currently being transferred to another node and all actions are unavailable.'}
|
: 'This server is currently being transferred to another node and all actions are unavailable.'}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
<div className={'grid grid-cols-4 gap-4 mb-4'}>
|
<div className={'mb-4 grid grid-cols-4 gap-4'}>
|
||||||
<div className={'hidden sm:block sm:col-span-2 lg:col-span-3 pr-4'}>
|
<div className={'hidden pr-4 sm:col-span-2 sm:block lg:col-span-3'}>
|
||||||
<h1 className={'font-header text-2xl text-slate-50 leading-relaxed line-clamp-1'}>{name}</h1>
|
<h1 className={'font-header text-2xl leading-relaxed text-slate-50 line-clamp-1'}>{name}</h1>
|
||||||
<p className={'text-sm line-clamp-2'}>{description}</p>
|
<p className={'text-sm line-clamp-2'}>{description}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={'col-span-4 sm:col-span-2 lg:col-span-1 self-end'}>
|
<div className={'col-span-4 self-end sm:col-span-2 lg:col-span-1'}>
|
||||||
<Can action={['control.start', 'control.stop', 'control.restart']} matchAny>
|
<Can action={['control.start', 'control.stop', 'control.restart']} matchAny>
|
||||||
<PowerButtons className={'flex sm:justify-end space-x-2'} />
|
<PowerButtons className={'flex space-x-2 sm:justify-end'} />
|
||||||
</Can>
|
</Can>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={'grid grid-cols-4 gap-2 sm:gap-4 mb-4'}>
|
<div className={'mb-4 grid grid-cols-4 gap-2 sm:gap-4'}>
|
||||||
<div className={'flex col-span-4 lg:col-span-3'}>
|
<div className={'col-span-4 flex lg:col-span-3'}>
|
||||||
<Spinner.Suspense>
|
<Spinner.Suspense>
|
||||||
<Console />
|
<Console />
|
||||||
</Spinner.Suspense>
|
</Spinner.Suspense>
|
||||||
</div>
|
</div>
|
||||||
<ServerDetailsBlock className={'col-span-4 lg:col-span-1 order-last lg:order-none'} />
|
<ServerDetailsBlock className={'order-last col-span-4 lg:order-none lg:col-span-1'} />
|
||||||
</div>
|
</div>
|
||||||
<div className={'grid grid-cols-1 md:grid-cols-3 gap-2 sm:gap-4'}>
|
<div className={'grid grid-cols-1 gap-2 sm:gap-4 md:grid-cols-3'}>
|
||||||
<Spinner.Suspense>
|
<Spinner.Suspense>
|
||||||
<StatGraphs />
|
<StatGraphs />
|
||||||
</Spinner.Suspense>
|
</Spinner.Suspense>
|
||||||
|
|
|
@ -38,7 +38,7 @@ function Limit({ limit, children }: { limit: string | null; children: ReactNode
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{children}
|
{children}
|
||||||
<span className={'ml-1 text-slate-300 text-[70%] select-none'}>/ {limit || <>∞</>}</span>
|
<span className={'ml-1 select-none text-[70%] text-slate-300'}>/ {limit || <>∞</>}</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,11 +33,11 @@ function StatBlock({ title, copyOnClick, icon, color, className, children }: Sta
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={'flex flex-col justify-center overflow-hidden w-full'}>
|
<div className={'flex w-full flex-col justify-center overflow-hidden'}>
|
||||||
<p className={'font-header leading-tight text-xs md:text-sm text-slate-200'}>{title}</p>
|
<p className={'font-header text-xs leading-tight text-slate-200 md:text-sm'}>{title}</p>
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={'h-[1.75rem] w-full font-semibold text-slate-50 truncate'}
|
className={'h-[1.75rem] w-full truncate font-semibold text-slate-50'}
|
||||||
style={{ fontSize }}
|
style={{ fontSize }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -79,10 +79,10 @@ export default () => {
|
||||||
legend={
|
legend={
|
||||||
<>
|
<>
|
||||||
<Tooltip arrow content={'Inbound'}>
|
<Tooltip arrow content={'Inbound'}>
|
||||||
<CloudDownloadIcon className={'mr-2 w-4 h-4 text-yellow-400'} />
|
<CloudDownloadIcon className={'mr-2 h-4 w-4 text-yellow-400'} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip arrow content={'Outbound'}>
|
<Tooltip arrow content={'Outbound'}>
|
||||||
<CloudUploadIcon className={'w-4 h-4 text-cyan-400'} />
|
<CloudUploadIcon className={'h-4 w-4 text-cyan-400'} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,7 @@ export default () => {
|
||||||
return (
|
return (
|
||||||
<ServerContentBlock title={'File Manager'} showFlashKey={'files'}>
|
<ServerContentBlock title={'File Manager'} showFlashKey={'files'}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<div className={'flex flex-wrap-reverse md:flex-nowrap mb-4'}>
|
<div className={'mb-4 flex flex-wrap-reverse md:flex-nowrap'}>
|
||||||
<FileManagerBreadcrumbs
|
<FileManagerBreadcrumbs
|
||||||
renderLeft={
|
renderLeft={
|
||||||
<FileActionCheckbox
|
<FileActionCheckbox
|
||||||
|
|
|
@ -25,7 +25,7 @@ const Spinner = ({ progress, className }: { progress: number; className?: string
|
||||||
{...svgProps}
|
{...svgProps}
|
||||||
stroke={'white'}
|
stroke={'white'}
|
||||||
strokeDasharray={28 * Math.PI}
|
strokeDasharray={28 * Math.PI}
|
||||||
className={'rotate-[-90deg] origin-[50%_50%] transition-[stroke-dashoffset] duration-300'}
|
className={'origin-[50%_50%] rotate-[-90deg] transition-[stroke-dashoffset] duration-300'}
|
||||||
style={{ strokeDashoffset: ((100 - progress) / 100) * 28 * Math.PI }}
|
style={{ strokeDashoffset: ((100 - progress) / 100) * 28 * Math.PI }}
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -40,20 +40,20 @@ const FileUploadList = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'space-y-2 mt-6'}>
|
<div className={'mt-6 space-y-2'}>
|
||||||
{uploads.map(([name, file]) => (
|
{uploads.map(([name, file]) => (
|
||||||
<div key={name} className={'flex items-center space-x-3 bg-slate-700 p-3 rounded'}>
|
<div key={name} className={'flex items-center space-x-3 rounded bg-slate-700 p-3'}>
|
||||||
<Tooltip content={`${Math.floor((file.loaded / file.total) * 100)}%`} placement={'left'}>
|
<Tooltip content={`${Math.floor((file.loaded / file.total) * 100)}%`} placement={'left'}>
|
||||||
<div className={'flex-shrink-0'}>
|
<div className={'flex-shrink-0'}>
|
||||||
<Spinner progress={(file.loaded / file.total) * 100} className={'w-6 h-6'} />
|
<Spinner progress={(file.loaded / file.total) * 100} className={'h-6 w-6'} />
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Code className={'flex-1 truncate'}>{name}</Code>
|
<Code className={'flex-1 truncate'}>{name}</Code>
|
||||||
<button
|
<button
|
||||||
onClick={cancelFileUpload.bind(this, name)}
|
onClick={cancelFileUpload.bind(this, name)}
|
||||||
className={'text-slate-500 hover:text-slate-200 transition-colors duration-75'}
|
className={'text-slate-500 transition-colors duration-75 hover:text-slate-200'}
|
||||||
>
|
>
|
||||||
<XIcon className={'w-5 h-5'} />
|
<XIcon className={'h-5 w-5'} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
@ -92,11 +92,11 @@ export default () => {
|
||||||
{count > 0 && (
|
{count > 0 && (
|
||||||
<Tooltip content={`${count} files are uploading, click to view`}>
|
<Tooltip content={`${count} files are uploading, click to view`}>
|
||||||
<button
|
<button
|
||||||
className={'flex items-center justify-center w-10 h-10'}
|
className={'flex h-10 w-10 items-center justify-center'}
|
||||||
onClick={() => (open.value = true)}
|
onClick={() => (open.value = true)}
|
||||||
>
|
>
|
||||||
<Spinner progress={(progress.uploaded / progress.total) * 100} className={'w-8 h-8'} />
|
<Spinner progress={(progress.uploaded / progress.total) * 100} className={'h-8 w-8'} />
|
||||||
<CloudUploadIcon className={'h-3 absolute mx-auto animate-pulse'} />
|
<CloudUploadIcon className={'absolute mx-auto h-3 animate-pulse'} />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -62,7 +62,7 @@ const MassActionsBar = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="pointer-events-none fixed bottom-0 z-20 left-0 right-0 flex justify-center">
|
<div className="pointer-events-none fixed bottom-0 left-0 right-0 z-20 flex justify-center">
|
||||||
<SpinnerOverlay visible={loading} size={'large'} fixed>
|
<SpinnerOverlay visible={loading} size={'large'} fixed>
|
||||||
{loadingMessage}
|
{loadingMessage}
|
||||||
</SpinnerOverlay>
|
</SpinnerOverlay>
|
||||||
|
@ -93,9 +93,9 @@ const MassActionsBar = () => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Portal>
|
<Portal>
|
||||||
<div className="fixed bottom-0 mb-6 flex justify-center w-full z-50">
|
<div className="fixed bottom-0 z-50 mb-6 flex w-full justify-center">
|
||||||
<FadeTransition duration="duration-75" show={selectedFiles.length > 0} appear unmount>
|
<FadeTransition duration="duration-75" show={selectedFiles.length > 0} appear unmount>
|
||||||
<div className="flex items-center space-x-4 pointer-events-auto rounded p-4 bg-black/50">
|
<div className="pointer-events-auto flex items-center space-x-4 rounded bg-black/50 p-4">
|
||||||
<Button onClick={() => setShowMove(true)}>Move</Button>
|
<Button onClick={() => setShowMove(true)}>Move</Button>
|
||||||
<Button onClick={onClickCompress}>Archive</Button>
|
<Button onClick={onClickCompress}>Archive</Button>
|
||||||
<Button.Danger variant={Button.Variants.Secondary} onClick={() => setShowConfirm(true)}>
|
<Button.Danger variant={Button.Variants.Secondary} onClick={() => setShowConfirm(true)}>
|
||||||
|
|
|
@ -120,14 +120,14 @@ export default ({ className }: WithClassname) => {
|
||||||
onFileSubmission(e.dataTransfer.files);
|
onFileSubmission(e.dataTransfer.files);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={'w-full flex items-center justify-center pointer-events-none'}>
|
<div className={'pointer-events-none flex w-full items-center justify-center'}>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'flex items-center space-x-4 bg-black w-full ring-4 ring-blue-200 ring-opacity-60 rounded p-6 mx-10 max-w-sm'
|
'mx-10 flex w-full max-w-sm items-center space-x-4 rounded bg-black p-6 ring-4 ring-blue-200 ring-opacity-60'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<CloudUploadIcon className={'w-10 h-10 flex-shrink-0'} />
|
<CloudUploadIcon className={'h-10 w-10 flex-shrink-0'} />
|
||||||
<p className={'font-header flex-1 text-lg text-neutral-100 text-center'}>
|
<p className={'flex-1 text-center font-header text-lg text-neutral-100'}>
|
||||||
Drag and drop files to upload.
|
Drag and drop files to upload.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -60,8 +60,8 @@ const AllocationRow = ({ allocation }: Props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GreyRowBox $hoverable={false} className={'flex-wrap md:flex-nowrap mt-2'}>
|
<GreyRowBox $hoverable={false} className={'mt-2 flex-wrap md:flex-nowrap'}>
|
||||||
<div className={'flex items-center w-full md:w-auto'}>
|
<div className={'flex w-full items-center md:w-auto'}>
|
||||||
<div className={'pl-4 pr-6 text-neutral-400'}>
|
<div className={'pl-4 pr-6 text-neutral-400'}>
|
||||||
<FontAwesomeIcon icon={faNetworkWired} />
|
<FontAwesomeIcon icon={faNetworkWired} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -79,24 +79,24 @@ const AllocationRow = ({ allocation }: Props) => {
|
||||||
)}
|
)}
|
||||||
<Label>{allocation.alias ? 'Hostname' : 'IP Address'}</Label>
|
<Label>{allocation.alias ? 'Hostname' : 'IP Address'}</Label>
|
||||||
</div>
|
</div>
|
||||||
<div className={'w-16 md:w-24 overflow-hidden'}>
|
<div className={'w-16 overflow-hidden md:w-24'}>
|
||||||
<Code dark>{allocation.port}</Code>
|
<Code dark>{allocation.port}</Code>
|
||||||
<Label>Port</Label>
|
<Label>Port</Label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={'mt-4 w-full md:mt-0 md:flex-1 md:w-auto'}>
|
<div className={'mt-4 w-full md:mt-0 md:w-auto md:flex-1'}>
|
||||||
<InputSpinner visible={loading}>
|
<InputSpinner visible={loading}>
|
||||||
<Textarea
|
<Textarea
|
||||||
className={'bg-neutral-800 hover:border-neutral-600 border-transparent'}
|
className={'border-transparent bg-neutral-800 hover:border-neutral-600'}
|
||||||
placeholder={'Notes'}
|
placeholder={'Notes'}
|
||||||
defaultValue={allocation.notes || undefined}
|
defaultValue={allocation.notes || undefined}
|
||||||
onChange={e => setAllocationNotes(e.currentTarget.value)}
|
onChange={e => setAllocationNotes(e.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
</InputSpinner>
|
</InputSpinner>
|
||||||
</div>
|
</div>
|
||||||
<div className={'flex justify-end space-x-4 mt-4 w-full md:mt-0 md:w-48'}>
|
<div className={'mt-4 flex w-full justify-end space-x-4 md:mt-0 md:w-48'}>
|
||||||
{allocation.isDefault ? (
|
{allocation.isDefault ? (
|
||||||
<Button size={Button.Sizes.Small} className={'!text-slate-50 !bg-blue-600'} disabled>
|
<Button size={Button.Sizes.Small} className={'!bg-blue-600 !text-slate-50'} disabled>
|
||||||
Primary
|
Primary
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -50,7 +50,7 @@ export default ({ scheduleId, onDeleted }: Props) => {
|
||||||
</Dialog.Confirm>
|
</Dialog.Confirm>
|
||||||
<Button.Danger
|
<Button.Danger
|
||||||
variant={Button.Variants.Secondary}
|
variant={Button.Variants.Secondary}
|
||||||
className={'flex-1 sm:flex-none mr-4 border-transparent'}
|
className={'mr-4 flex-1 border-transparent sm:flex-none'}
|
||||||
onClick={() => setVisible(true)}
|
onClick={() => setVisible(true)}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
|
|
|
@ -8,25 +8,25 @@ interface Props {
|
||||||
|
|
||||||
const ScheduleCronRow = ({ cron, className }: Props) => (
|
const ScheduleCronRow = ({ cron, className }: Props) => (
|
||||||
<div className={classNames('flex', className)}>
|
<div className={classNames('flex', className)}>
|
||||||
<div className={'w-1/5 sm:w-auto text-center'}>
|
<div className={'w-1/5 text-center sm:w-auto'}>
|
||||||
<p className={'font-medium'}>{cron.minute}</p>
|
<p className={'font-medium'}>{cron.minute}</p>
|
||||||
<p className={'text-2xs text-neutral-500 uppercase'}>Minute</p>
|
<p className={'text-2xs uppercase text-neutral-500'}>Minute</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={'w-1/5 sm:w-auto text-center ml-4'}>
|
<div className={'ml-4 w-1/5 text-center sm:w-auto'}>
|
||||||
<p className={'font-medium'}>{cron.hour}</p>
|
<p className={'font-medium'}>{cron.hour}</p>
|
||||||
<p className={'text-2xs text-neutral-500 uppercase'}>Hour</p>
|
<p className={'text-2xs uppercase text-neutral-500'}>Hour</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={'w-1/5 sm:w-auto text-center ml-4'}>
|
<div className={'ml-4 w-1/5 text-center sm:w-auto'}>
|
||||||
<p className={'font-medium'}>{cron.dayOfMonth}</p>
|
<p className={'font-medium'}>{cron.dayOfMonth}</p>
|
||||||
<p className={'text-2xs text-neutral-500 uppercase'}>Day (Month)</p>
|
<p className={'text-2xs uppercase text-neutral-500'}>Day (Month)</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={'w-1/5 sm:w-auto text-center ml-4'}>
|
<div className={'ml-4 w-1/5 text-center sm:w-auto'}>
|
||||||
<p className={'font-medium'}>{cron.month}</p>
|
<p className={'font-medium'}>{cron.month}</p>
|
||||||
<p className={'text-2xs text-neutral-500 uppercase'}>Month</p>
|
<p className={'text-2xs uppercase text-neutral-500'}>Month</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={'w-1/5 sm:w-auto text-center ml-4'}>
|
<div className={'ml-4 w-1/5 text-center sm:w-auto'}>
|
||||||
<p className={'font-medium'}>{cron.dayOfWeek}</p>
|
<p className={'font-medium'}>{cron.dayOfWeek}</p>
|
||||||
<p className={'text-2xs text-neutral-500 uppercase'}>Day (Week)</p>
|
<p className={'text-2xs uppercase text-neutral-500'}>Day (Week)</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -118,7 +118,7 @@ export default () => {
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`flex sm:block mt-3 sm:mt-0`}>
|
<div css={tw`flex sm:block mt-3 sm:mt-0`}>
|
||||||
<Can action={'schedule.update'}>
|
<Can action={'schedule.update'}>
|
||||||
<Button.Text className={'flex-1 mr-4'} onClick={toggleEditModal}>
|
<Button.Text className={'mr-4 flex-1'} onClick={toggleEditModal}>
|
||||||
Edit
|
Edit
|
||||||
</Button.Text>
|
</Button.Text>
|
||||||
<NewTaskButton schedule={schedule} />
|
<NewTaskButton schedule={schedule} />
|
||||||
|
|
|
@ -1,79 +1,86 @@
|
||||||
import extractSearchFilters from '@/helpers/extractSearchFilters';
|
import extractSearchFilters from '@/helpers/extractSearchFilters';
|
||||||
|
|
||||||
type TestCase = [ string, 0 | Record<string, string[]> ];
|
type TestCase = [string, 0 | Record<string, string[]>];
|
||||||
|
|
||||||
describe('@/helpers/extractSearchFilters.ts', function () {
|
describe('@/helpers/extractSearchFilters.ts', function () {
|
||||||
const cases: TestCase[] = [
|
const cases: TestCase[] = [
|
||||||
[ '', {} ],
|
['', {}],
|
||||||
[ 'hello world', {} ],
|
['hello world', {}],
|
||||||
[ 'bar:xyz foo:abc', { bar: [ 'xyz' ], foo: [ 'abc' ] } ],
|
['bar:xyz foo:abc', { bar: ['xyz'], foo: ['abc'] }],
|
||||||
[ 'hello foo:abc', { foo: [ 'abc' ] } ],
|
['hello foo:abc', { foo: ['abc'] }],
|
||||||
[ 'hello foo:abc world another bar:xyz hodor', { foo: [ 'abc' ], bar: [ 'xyz' ] } ],
|
['hello foo:abc world another bar:xyz hodor', { foo: ['abc'], bar: ['xyz'] }],
|
||||||
[ 'foo:1 foo:2 foo: 3 foo:4', { foo: [ '1', '2', '4' ] } ],
|
['foo:1 foo:2 foo: 3 foo:4', { foo: ['1', '2', '4'] }],
|
||||||
[ ' foo:123 foo:bar:123 foo: foo:string', { foo: [ '123', 'bar:123', 'string' ] } ],
|
[' foo:123 foo:bar:123 foo: foo:string', { foo: ['123', 'bar:123', 'string'] }],
|
||||||
[ 'foo:1 bar:2 baz:3', { foo: [ '1' ], bar: [ '2' ] } ],
|
['foo:1 bar:2 baz:3', { foo: ['1'], bar: ['2'] }],
|
||||||
[ 'hello "world this" is quoted', {} ],
|
['hello "world this" is quoted', {}],
|
||||||
[ 'hello "world foo:123 is" quoted', {} ],
|
['hello "world foo:123 is" quoted', {}],
|
||||||
[ 'hello foo:"this is quoted" bar:"this \\"is deeply\\" quoted" world foo:another', {
|
[
|
||||||
foo: [ 'this is quoted', 'another' ],
|
'hello foo:"this is quoted" bar:"this \\"is deeply\\" quoted" world foo:another',
|
||||||
bar: [ 'this "is deeply" quoted' ],
|
{
|
||||||
} ],
|
foo: ['this is quoted', 'another'],
|
||||||
|
bar: ['this "is deeply" quoted'],
|
||||||
|
},
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
it.each(cases)('should return expected filters: [%s]', function (input, output) {
|
it.each(cases)('should return expected filters: [%s]', function (input, output) {
|
||||||
expect(extractSearchFilters(input, [ 'foo', 'bar' ])).toStrictEqual({
|
expect(extractSearchFilters(input, ['foo', 'bar'])).toStrictEqual({
|
||||||
filters: output,
|
filters: output,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow modification of the default parameter', function () {
|
it('should allow modification of the default parameter', function () {
|
||||||
expect(extractSearchFilters('hello world', [ 'foo' ], { defaultFilter: 'default_param', returnUnmatched: true })).toStrictEqual({
|
expect(
|
||||||
|
extractSearchFilters('hello world', ['foo'], { defaultFilter: 'default_param', returnUnmatched: true }),
|
||||||
|
).toStrictEqual({
|
||||||
filters: {
|
filters: {
|
||||||
default_param: [ 'hello world' ],
|
default_param: ['hello world'],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(extractSearchFilters('foo:123 bar', [ 'foo' ], { defaultFilter: 'default_param' })).toStrictEqual({
|
expect(extractSearchFilters('foo:123 bar', ['foo'], { defaultFilter: 'default_param' })).toStrictEqual({
|
||||||
filters: {
|
filters: {
|
||||||
foo: [ '123' ],
|
foo: ['123'],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[ '', {} ],
|
['', {}],
|
||||||
[ 'hello world', { '*': [ 'hello world' ] } ],
|
['hello world', { '*': ['hello world'] }],
|
||||||
[ 'hello world foo:123 bar:456', { foo: [ '123' ], bar: [ '456' ], '*': [ 'hello world' ] } ],
|
['hello world foo:123 bar:456', { foo: ['123'], bar: ['456'], '*': ['hello world'] }],
|
||||||
[ 'hello world foo:123 another string', { foo: [ '123' ], '*': [ 'hello world another string' ] } ],
|
['hello world foo:123 another string', { foo: ['123'], '*': ['hello world another string'] }],
|
||||||
])('should return unmatched parameters: %s', function (input, output) {
|
])('should return unmatched parameters: %s', function (input, output) {
|
||||||
expect(extractSearchFilters(input, [ 'foo', 'bar' ], { returnUnmatched: true })).toStrictEqual({
|
expect(extractSearchFilters(input, ['foo', 'bar'], { returnUnmatched: true })).toStrictEqual({
|
||||||
filters: output,
|
filters: output,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[ '', {} ],
|
['', {}],
|
||||||
[ 'hello world', { '*': [ 'hello', 'world' ] } ],
|
['hello world', { '*': ['hello', 'world'] }],
|
||||||
[ 'hello world foo:123 bar:456', { foo: [ '123' ], bar: [ '456' ], '*': [ 'hello', 'world' ] } ],
|
['hello world foo:123 bar:456', { foo: ['123'], bar: ['456'], '*': ['hello', 'world'] }],
|
||||||
[ 'hello world foo:123 another string', { foo: [ '123' ], '*': [ 'hello', 'world', 'another', 'string' ] } ],
|
['hello world foo:123 another string', { foo: ['123'], '*': ['hello', 'world', 'another', 'string'] }],
|
||||||
])('should split unmatched parameters: %s', function (input, output) {
|
])('should split unmatched parameters: %s', function (input, output) {
|
||||||
expect(extractSearchFilters(input, [ 'foo', 'bar' ], {
|
expect(
|
||||||
returnUnmatched: true,
|
extractSearchFilters(input, ['foo', 'bar'], {
|
||||||
splitUnmatched: true,
|
returnUnmatched: true,
|
||||||
})).toStrictEqual({
|
splitUnmatched: true,
|
||||||
|
}),
|
||||||
|
).toStrictEqual({
|
||||||
filters: output,
|
filters: output,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([ true, false ])('should return the unsplit value (splitting: %s)', function (split) {
|
it.each([true, false])('should return the unsplit value (splitting: %s)', function (split) {
|
||||||
const extracted = extractSearchFilters('hello foo:123 bar:123 world', [ 'foo' ], {
|
const extracted = extractSearchFilters('hello foo:123 bar:123 world', ['foo'], {
|
||||||
returnUnmatched: true,
|
returnUnmatched: true,
|
||||||
splitUnmatched: split,
|
splitUnmatched: split,
|
||||||
});
|
});
|
||||||
expect(extracted).toStrictEqual({
|
expect(extracted).toStrictEqual({
|
||||||
filters: {
|
filters: {
|
||||||
foo: [ '123' ],
|
foo: ['123'],
|
||||||
'*': split ? [ 'hello', 'bar:123', 'world' ] : [ 'hello bar:123 world' ],
|
'*': split ? ['hello', 'bar:123', 'world'] : ['hello bar:123 world'],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,13 +7,13 @@ interface Options<D extends string = string> {
|
||||||
returnUnmatched?: boolean;
|
returnUnmatched?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractSearchFilters = <T extends string, D extends string = '*'> (
|
const extractSearchFilters = <T extends string, D extends string = '*'>(
|
||||||
str: string,
|
str: string,
|
||||||
params: Readonly<T[]>,
|
params: Readonly<T[]>,
|
||||||
options?: Options<D>,
|
options?: Options<D>,
|
||||||
): QueryBuilderParams<T> | QueryBuilderParams<D> | QueryBuilderParams<T & D> => {
|
): QueryBuilderParams<T> | QueryBuilderParams<D> | QueryBuilderParams<T & D> => {
|
||||||
const opts: Required<Options<D>> = {
|
const opts: Required<Options<D>> = {
|
||||||
defaultFilter: options?.defaultFilter || '*' as D,
|
defaultFilter: options?.defaultFilter || ('*' as D),
|
||||||
splitUnmatched: options?.splitUnmatched || false,
|
splitUnmatched: options?.splitUnmatched || false,
|
||||||
returnUnmatched: options?.returnUnmatched || false,
|
returnUnmatched: options?.returnUnmatched || false,
|
||||||
};
|
};
|
||||||
|
@ -30,12 +30,12 @@ const extractSearchFilters = <T extends string, D extends string = '*'> (
|
||||||
} else if (!params.includes(filter)) {
|
} else if (!params.includes(filter)) {
|
||||||
unmatched.push(segment);
|
unmatched.push(segment);
|
||||||
} else {
|
} else {
|
||||||
filters.set(filter, [ ...(filters.get(filter) || []), value ]);
|
filters.set(filter, [...(filters.get(filter) || []), value]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.returnUnmatched && str.trim().length > 0) {
|
if (opts.returnUnmatched && str.trim().length > 0) {
|
||||||
filters.set(opts.defaultFilter as any, opts.splitUnmatched ? unmatched : [ unmatched.join(' ') ]);
|
filters.set(opts.defaultFilter as any, opts.splitUnmatched ? unmatched : [unmatched.join(' ')]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filters.size === 0) {
|
if (filters.size === 0) {
|
||||||
|
|
|
@ -2,14 +2,14 @@ import splitStringWhitespace from '@/helpers/splitStringWhitespace';
|
||||||
|
|
||||||
describe('@/helpers/splitStringWhitespace.ts', function () {
|
describe('@/helpers/splitStringWhitespace.ts', function () {
|
||||||
it.each([
|
it.each([
|
||||||
[ '', [] ],
|
['', []],
|
||||||
[ 'hello world', [ 'hello', 'world' ] ],
|
['hello world', ['hello', 'world']],
|
||||||
[ ' hello world ', [ 'hello', 'world' ] ],
|
[' hello world ', ['hello', 'world']],
|
||||||
[ 'hello123 world 123 $$ s ', [ 'hello123', 'world', '123', '$$', 's' ] ],
|
['hello123 world 123 $$ s ', ['hello123', 'world', '123', '$$', 's']],
|
||||||
[ 'hello world! how are you?', [ 'hello', 'world!', 'how', 'are', 'you?' ] ],
|
['hello world! how are you?', ['hello', 'world!', 'how', 'are', 'you?']],
|
||||||
[ 'hello "foo bar baz" world', [ 'hello', 'foo bar baz', 'world' ] ],
|
['hello "foo bar baz" world', ['hello', 'foo bar baz', 'world']],
|
||||||
[ 'hello "foo \\"bar bar \\" baz" world', [ 'hello', 'foo "bar bar " baz', 'world' ] ],
|
['hello "foo \\"bar bar \\" baz" world', ['hello', 'foo "bar bar " baz', 'world']],
|
||||||
[ 'hello "foo "bar baz" baz" world', [ 'hello', 'foo bar', 'baz baz', 'world' ] ],
|
['hello "foo "bar baz" baz" world', ['hello', 'foo bar', 'baz baz', 'world']],
|
||||||
])('should handle string: %s', function (input, output) {
|
])('should handle string: %s', function (input, output) {
|
||||||
expect(splitStringWhitespace(input)).toStrictEqual(output);
|
expect(splitStringWhitespace(input)).toStrictEqual(output);
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,9 +7,9 @@
|
||||||
*/
|
*/
|
||||||
export default (str: string): string[] => {
|
export default (str: string): string[] => {
|
||||||
let quoted = false;
|
let quoted = false;
|
||||||
const parts = [ '' ] as string[];
|
const parts = [''] as string[];
|
||||||
|
|
||||||
for (const char of (str.trim().match(/\\?.|^$/g) || [])) {
|
for (const char of str.trim().match(/\\?.|^$/g) || []) {
|
||||||
if (char === '"') {
|
if (char === '"') {
|
||||||
quoted = !quoted;
|
quoted = !quoted;
|
||||||
} else if (!quoted && char === ' ') {
|
} else if (!quoted && char === ' ') {
|
||||||
|
|
|
@ -3,10 +3,10 @@ import { debounce } from 'debounce';
|
||||||
|
|
||||||
type DebounceFn<V> = ((value: V) => void) & { clear: () => void };
|
type DebounceFn<V> = ((value: V) => void) & { clear: () => void };
|
||||||
|
|
||||||
export default <S> (initial: S, interval?: number, immediate?: boolean): [ S, DebounceFn<S> ] => {
|
export default <S>(initial: S, interval?: number, immediate?: boolean): [S, DebounceFn<S>] => {
|
||||||
const [ state, setState ] = useState<S>(initial);
|
const [state, setState] = useState<S>(initial);
|
||||||
|
|
||||||
const debouncedSetState = debounce((v: S) => setState(v), interval, immediate);
|
const debouncedSetState = debounce((v: S) => setState(v), interval, immediate);
|
||||||
|
|
||||||
return [ state, debouncedSetState ];
|
return [state, debouncedSetState];
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue