ui(admin): make all tables searchable and sortable
This commit is contained in:
parent
8f8d66584d
commit
c0e9f1adee
27 changed files with 968 additions and 229 deletions
|
@ -38,12 +38,13 @@ class EggController extends ApplicationApiController
|
|||
*/
|
||||
public function index(GetEggsRequest $request, Nest $nest): array
|
||||
{
|
||||
$perPage = $request->query('per_page', 0);
|
||||
$perPage = $request->query('per_page', 10);
|
||||
if ($perPage > 100) {
|
||||
throw new QueryValueOutOfRangeHttpException('per_page', 1, 100);
|
||||
}
|
||||
|
||||
$eggs = QueryBuilder::for(Egg::query())
|
||||
->where('nest_id', '=', $nest->id)
|
||||
->allowedFilters(['id', 'name', 'author'])
|
||||
->allowedSorts(['id', 'name', 'author']);
|
||||
if ($perPage > 0) {
|
||||
|
|
|
@ -9,7 +9,6 @@ use Spatie\QueryBuilder\QueryBuilder;
|
|||
use Pterodactyl\Services\Locations\LocationUpdateService;
|
||||
use Pterodactyl\Services\Locations\LocationCreationService;
|
||||
use Pterodactyl\Services\Locations\LocationDeletionService;
|
||||
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
|
||||
use Pterodactyl\Transformers\Api\Application\LocationTransformer;
|
||||
use Pterodactyl\Exceptions\Http\QueryValueOutOfRangeHttpException;
|
||||
use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
|
||||
|
@ -24,7 +23,6 @@ class LocationController extends ApplicationApiController
|
|||
private LocationCreationService $creationService;
|
||||
private LocationDeletionService $deletionService;
|
||||
private LocationUpdateService $updateService;
|
||||
private LocationRepositoryInterface $repository;
|
||||
|
||||
/**
|
||||
* LocationController constructor.
|
||||
|
@ -32,15 +30,13 @@ class LocationController extends ApplicationApiController
|
|||
public function __construct(
|
||||
LocationCreationService $creationService,
|
||||
LocationDeletionService $deletionService,
|
||||
LocationUpdateService $updateService,
|
||||
LocationRepositoryInterface $repository
|
||||
LocationUpdateService $updateService
|
||||
) {
|
||||
parent::__construct();
|
||||
|
||||
$this->creationService = $creationService;
|
||||
$this->deletionService = $deletionService;
|
||||
$this->updateService = $updateService;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -57,7 +53,7 @@ class LocationController extends ApplicationApiController
|
|||
|
||||
$locations = QueryBuilder::for(Location::query())
|
||||
->allowedFilters(['short', 'long'])
|
||||
->allowedSorts(['id'])
|
||||
->allowedSorts(['id', 'short', 'long'])
|
||||
->paginate($perPage);
|
||||
|
||||
return $this->fractal->collection($locations)
|
||||
|
|
|
@ -40,8 +40,8 @@ class MountController extends ApplicationApiController
|
|||
}
|
||||
|
||||
$mounts = QueryBuilder::for(Mount::query())
|
||||
->allowedFilters(['name', 'host'])
|
||||
->allowedSorts(['id', 'name', 'host'])
|
||||
->allowedFilters(['id', 'name', 'source', 'target'])
|
||||
->allowedSorts(['id', 'name', 'source', 'target'])
|
||||
->paginate($perPage);
|
||||
|
||||
return $this->fractal->collection($mounts)
|
||||
|
|
|
@ -50,7 +50,7 @@ class NestController extends ApplicationApiController
|
|||
*/
|
||||
public function index(GetNestsRequest $request): array
|
||||
{
|
||||
$perPage = $request->query('per_page', 0);
|
||||
$perPage = $request->query('per_page', 10);
|
||||
if ($perPage > 100) {
|
||||
throw new QueryValueOutOfRangeHttpException('per_page', 1, 100);
|
||||
}
|
||||
|
|
|
@ -3,9 +3,13 @@
|
|||
namespace Pterodactyl\Http\Controllers\Api\Application\Roles;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Pterodactyl\Models\Location;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Models\AdminRole;
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
use Pterodactyl\Transformers\Api\Application\LocationTransformer;
|
||||
use Pterodactyl\Transformers\Api\Application\AdminRoleTransformer;
|
||||
use Pterodactyl\Exceptions\Http\QueryValueOutOfRangeHttpException;
|
||||
use Pterodactyl\Http\Requests\Api\Application\Roles\GetRoleRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Application\Roles\GetRolesRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Application\Roles\StoreRoleRequest;
|
||||
|
@ -30,7 +34,17 @@ class RoleController extends ApplicationApiController
|
|||
*/
|
||||
public function index(GetRolesRequest $request): array
|
||||
{
|
||||
return $this->fractal->collection(AdminRole::all())
|
||||
$perPage = $request->query('per_page', 10);
|
||||
if ($perPage < 1 || $perPage > 100) {
|
||||
throw new QueryValueOutOfRangeHttpException('per_page', 1, 100);
|
||||
}
|
||||
|
||||
$roles = QueryBuilder::for(AdminRole::query())
|
||||
->allowedFilters(['id', 'name'])
|
||||
->allowedSorts(['id', 'name'])
|
||||
->paginate($perPage);
|
||||
|
||||
return $this->fractal->collection($roles)
|
||||
->transformWith($this->getTransformer(AdminRoleTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ use Pterodactyl\Http\Requests\Api\Application\Users\DeleteUserRequest;
|
|||
use Pterodactyl\Http\Requests\Api\Application\Users\UpdateUserRequest;
|
||||
use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
|
||||
|
||||
class UserController extends ApplicationApiController
|
||||
class UserController extends ApplicationApiController
|
||||
{
|
||||
private UserRepositoryInterface $repository;
|
||||
private UserCreationService $creationService;
|
||||
|
@ -58,8 +58,8 @@ class UserController extends ApplicationApiController
|
|||
}
|
||||
|
||||
$users = QueryBuilder::for(User::query())
|
||||
->allowedFilters(['email', 'uuid', 'username', 'external_id'])
|
||||
->allowedSorts(['id', 'uuid'])
|
||||
->allowedFilters(['id', 'uuid', 'username', 'email', 'first_name', 'last_name', 'external_id'])
|
||||
->allowedSorts(['id', 'uuid', 'username', 'email', 'admin_role_id'])
|
||||
->paginate($perPage);
|
||||
|
||||
return $this->fractal->collection($users)
|
||||
|
|
|
@ -28,18 +28,58 @@ export const rawDataToDatabase = ({ attributes }: FractalResponseData): Database
|
|||
getAddress: () => `${attributes.host}:${attributes.port}`,
|
||||
});
|
||||
|
||||
export interface Filters {
|
||||
id?: string;
|
||||
name?: string;
|
||||
host?: string;
|
||||
}
|
||||
|
||||
interface ctx {
|
||||
page: number;
|
||||
setPage: (value: number | ((s: number) => number)) => void;
|
||||
|
||||
filters: Filters | null;
|
||||
setFilters: (filters: Filters | null) => void;
|
||||
|
||||
sort: string | null;
|
||||
setSort: (sort: string | null) => void;
|
||||
|
||||
sortDirection: boolean;
|
||||
setSortDirection: (direction: boolean) => void;
|
||||
}
|
||||
|
||||
export const Context = createContext<ctx>({ page: 1, setPage: () => 1 });
|
||||
export const Context = createContext<ctx>({
|
||||
page: 1,
|
||||
setPage: () => 1,
|
||||
|
||||
filters: null,
|
||||
setFilters: () => null,
|
||||
|
||||
sort: null,
|
||||
setSort: () => null,
|
||||
|
||||
sortDirection: false,
|
||||
setSortDirection: () => false,
|
||||
});
|
||||
|
||||
export default (include: string[] = []) => {
|
||||
const { page } = useContext(Context);
|
||||
const { page, filters, sort, sortDirection } = useContext(Context);
|
||||
|
||||
return useSWR<PaginatedResult<Database>>([ 'databases', page ], async () => {
|
||||
const { data } = await http.get('/api/application/databases', { params: { include: include.join(','), page } });
|
||||
const params = {};
|
||||
if (filters !== null) {
|
||||
Object.keys(filters).forEach(key => {
|
||||
// @ts-ignore
|
||||
params['filter[' + key + ']'] = filters[key];
|
||||
});
|
||||
}
|
||||
|
||||
if (sort !== null) {
|
||||
// @ts-ignore
|
||||
params.sort = (sortDirection ? '-' : '') + sort;
|
||||
}
|
||||
|
||||
return useSWR<PaginatedResult<Database>>([ 'databases', page, filters, sort, sortDirection ], async () => {
|
||||
const { data } = await http.get('/api/application/databases', { params: { include: include.join(','), page, ...params } });
|
||||
|
||||
return ({
|
||||
items: (data.data || []).map(rawDataToDatabase),
|
||||
|
|
|
@ -18,18 +18,58 @@ export const rawDataToLocation = ({ attributes }: FractalResponseData): Location
|
|||
updatedAt: new Date(attributes.updated_at),
|
||||
});
|
||||
|
||||
export interface Filters {
|
||||
id?: string;
|
||||
short?: string;
|
||||
long?: string;
|
||||
}
|
||||
|
||||
interface ctx {
|
||||
page: number;
|
||||
setPage: (value: number | ((s: number) => number)) => void;
|
||||
|
||||
filters: Filters | null;
|
||||
setFilters: (filters: Filters | null) => void;
|
||||
|
||||
sort: string | null;
|
||||
setSort: (sort: string | null) => void;
|
||||
|
||||
sortDirection: boolean;
|
||||
setSortDirection: (direction: boolean) => void;
|
||||
}
|
||||
|
||||
export const Context = createContext<ctx>({ page: 1, setPage: () => 1 });
|
||||
export const Context = createContext<ctx>({
|
||||
page: 1,
|
||||
setPage: () => 1,
|
||||
|
||||
filters: null,
|
||||
setFilters: () => null,
|
||||
|
||||
sort: null,
|
||||
setSort: () => null,
|
||||
|
||||
sortDirection: false,
|
||||
setSortDirection: () => false,
|
||||
});
|
||||
|
||||
export default (include: string[] = []) => {
|
||||
const { page } = useContext(Context);
|
||||
const { page, filters, sort, sortDirection } = useContext(Context);
|
||||
|
||||
return useSWR<PaginatedResult<Location>>([ 'locations', page ], async () => {
|
||||
const { data } = await http.get('/api/application/locations', { params: { include: include.join(','), page } });
|
||||
const params = {};
|
||||
if (filters !== null) {
|
||||
Object.keys(filters).forEach(key => {
|
||||
// @ts-ignore
|
||||
params['filter[' + key + ']'] = filters[key];
|
||||
});
|
||||
}
|
||||
|
||||
if (sort !== null) {
|
||||
// @ts-ignore
|
||||
params.sort = (sortDirection ? '-' : '') + sort;
|
||||
}
|
||||
|
||||
return useSWR<PaginatedResult<Location>>([ 'locations', page, filters, sort, sortDirection ], async () => {
|
||||
const { data } = await http.get('/api/application/locations', { params: { include: include.join(','), page, ...params } });
|
||||
|
||||
return ({
|
||||
items: (data.data || []).map(rawDataToLocation),
|
||||
|
|
|
@ -43,18 +43,59 @@ export const rawDataToMount = ({ attributes }: FractalResponseData): Mount => ({
|
|||
},
|
||||
});
|
||||
|
||||
export interface Filters {
|
||||
id?: string;
|
||||
name?: string;
|
||||
source?: string;
|
||||
target?: string;
|
||||
}
|
||||
|
||||
interface ctx {
|
||||
page: number;
|
||||
setPage: (value: number | ((s: number) => number)) => void;
|
||||
|
||||
filters: Filters | null;
|
||||
setFilters: (filters: Filters | null) => void;
|
||||
|
||||
sort: string | null;
|
||||
setSort: (sort: string | null) => void;
|
||||
|
||||
sortDirection: boolean;
|
||||
setSortDirection: (direction: boolean) => void;
|
||||
}
|
||||
|
||||
export const Context = createContext<ctx>({ page: 1, setPage: () => 1 });
|
||||
export const Context = createContext<ctx>({
|
||||
page: 1,
|
||||
setPage: () => 1,
|
||||
|
||||
filters: null,
|
||||
setFilters: () => null,
|
||||
|
||||
sort: null,
|
||||
setSort: () => null,
|
||||
|
||||
sortDirection: false,
|
||||
setSortDirection: () => false,
|
||||
});
|
||||
|
||||
export default (include: string[] = []) => {
|
||||
const { page } = useContext(Context);
|
||||
const { page, filters, sort, sortDirection } = useContext(Context);
|
||||
|
||||
return useSWR<PaginatedResult<Mount>>([ 'mounts', page ], async () => {
|
||||
const { data } = await http.get('/api/application/mounts', { params: { include: include.join(','), page } });
|
||||
const params = {};
|
||||
if (filters !== null) {
|
||||
Object.keys(filters).forEach(key => {
|
||||
// @ts-ignore
|
||||
params['filter[' + key + ']'] = filters[key];
|
||||
});
|
||||
}
|
||||
|
||||
if (sort !== null) {
|
||||
// @ts-ignore
|
||||
params.sort = (sortDirection ? '-' : '') + sort;
|
||||
}
|
||||
|
||||
return useSWR<PaginatedResult<Mount>>([ 'mounts', page, filters, sort, sortDirection ], async () => {
|
||||
const { data } = await http.get('/api/application/mounts', { params: { include: include.join(','), page, ...params } });
|
||||
|
||||
return ({
|
||||
items: (data.data || []).map(rawDataToMount),
|
||||
|
|
|
@ -1,10 +1,63 @@
|
|||
import http from '@/api/http';
|
||||
import http, { getPaginationSet, PaginatedResult } from '@/api/http';
|
||||
import { createContext, useContext } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg';
|
||||
|
||||
export default (nestId: number): Promise<Egg[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(`/api/application/nests/${nestId}/eggs`)
|
||||
.then(({ data }) => resolve((data.data || []).map(rawDataToEgg)))
|
||||
.catch(reject);
|
||||
export interface Filters {
|
||||
id?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface ctx {
|
||||
page: number;
|
||||
setPage: (value: number | ((s: number) => number)) => void;
|
||||
|
||||
filters: Filters | null;
|
||||
setFilters: (filters: Filters | null) => void;
|
||||
|
||||
sort: string | null;
|
||||
setSort: (sort: string | null) => void;
|
||||
|
||||
sortDirection: boolean;
|
||||
setSortDirection: (direction: boolean) => void;
|
||||
}
|
||||
|
||||
export const Context = createContext<ctx>({
|
||||
page: 1,
|
||||
setPage: () => 1,
|
||||
|
||||
filters: null,
|
||||
setFilters: () => null,
|
||||
|
||||
sort: null,
|
||||
setSort: () => null,
|
||||
|
||||
sortDirection: false,
|
||||
setSortDirection: () => false,
|
||||
});
|
||||
|
||||
export default (nestId: number, include: string[] = []) => {
|
||||
const { page, filters, sort, sortDirection } = useContext(Context);
|
||||
|
||||
const params = {};
|
||||
if (filters !== null) {
|
||||
Object.keys(filters).forEach(key => {
|
||||
// @ts-ignore
|
||||
params['filter[' + key + ']'] = filters[key];
|
||||
});
|
||||
}
|
||||
|
||||
if (sort !== null) {
|
||||
// @ts-ignore
|
||||
params.sort = (sortDirection ? '-' : '') + sort;
|
||||
}
|
||||
|
||||
return useSWR<PaginatedResult<Egg>>([ nestId, 'eggs', page, filters, sort, sortDirection ], async () => {
|
||||
const { data } = await http.get(`/api/application/nests/${nestId}/eggs`, { params: { include: include.join(','), page, ...params } });
|
||||
|
||||
return ({
|
||||
items: (data.data || []).map(rawDataToEgg),
|
||||
pagination: getPaginationSet(data.meta.pagination),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -31,24 +31,57 @@ export const rawDataToNest = ({ attributes }: FractalResponseData): Nest => ({
|
|||
},
|
||||
});
|
||||
|
||||
export interface Filters {
|
||||
id?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface ctx {
|
||||
page: number;
|
||||
setPage: (value: number | ((s: number) => number)) => void;
|
||||
|
||||
filters: Filters | null;
|
||||
setFilters: (filters: Filters | null) => void;
|
||||
|
||||
sort: string | null;
|
||||
setSort: (sort: string | null) => void;
|
||||
|
||||
sortDirection: boolean;
|
||||
setSortDirection: (direction: boolean) => void;
|
||||
}
|
||||
|
||||
export const Context = createContext<ctx>({ page: 1, setPage: () => 1 });
|
||||
export const Context = createContext<ctx>({
|
||||
page: 1,
|
||||
setPage: () => 1,
|
||||
|
||||
filters: null,
|
||||
setFilters: () => null,
|
||||
|
||||
sort: null,
|
||||
setSort: () => null,
|
||||
|
||||
sortDirection: false,
|
||||
setSortDirection: () => false,
|
||||
});
|
||||
|
||||
export default (include: string[] = []) => {
|
||||
const { page } = useContext(Context);
|
||||
const { page, filters, sort, sortDirection } = useContext(Context);
|
||||
|
||||
return useSWR<PaginatedResult<Nest>>([ 'nests', page ], async () => {
|
||||
const { data } = await http.get('/api/application/nests', {
|
||||
params: {
|
||||
include: include.join(','),
|
||||
per_page: 10,
|
||||
page,
|
||||
},
|
||||
const params = {};
|
||||
if (filters !== null) {
|
||||
Object.keys(filters).forEach(key => {
|
||||
// @ts-ignore
|
||||
params['filter[' + key + ']'] = filters[key];
|
||||
});
|
||||
}
|
||||
|
||||
if (sort !== null) {
|
||||
// @ts-ignore
|
||||
params.sort = (sortDirection ? '-' : '') + sort;
|
||||
}
|
||||
|
||||
return useSWR<PaginatedResult<Nest>>([ 'nests', page, filters, sort, sortDirection ], async () => {
|
||||
const { data } = await http.get('/api/application/nests', { params: { include: include.join(','), page, ...params } });
|
||||
|
||||
return ({
|
||||
items: (data.data || []).map(rawDataToNest),
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import http from '@/api/http';
|
||||
import { rawDataToServerAllocation } from '@/api/transformers';
|
||||
import http, { FractalResponseData } from '@/api/http';
|
||||
|
||||
export interface Allocation {
|
||||
id: number;
|
||||
|
@ -7,17 +6,22 @@ export interface Allocation {
|
|||
alias: string | null;
|
||||
port: number;
|
||||
notes: string | null;
|
||||
isDefault: boolean;
|
||||
assigned: boolean;
|
||||
}
|
||||
|
||||
export default (uuid: string): Promise<[ Allocation, string[] ]> => {
|
||||
export const rawDataToAllocation = (data: FractalResponseData): Allocation => ({
|
||||
id: data.attributes.id,
|
||||
ip: data.attributes.ip,
|
||||
alias: data.attributes.ip_alias,
|
||||
port: data.attributes.port,
|
||||
notes: data.attributes.notes,
|
||||
assigned: data.attributes.assigned,
|
||||
});
|
||||
|
||||
export default (uuid: string): Promise<Allocation[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(`/api/application/allocations/${uuid}`)
|
||||
.then(({ data }) => resolve([
|
||||
rawDataToServerAllocation(data),
|
||||
// eslint-disable-next-line camelcase
|
||||
data.meta?.is_allocation_owner ? [ '*' ] : (data.meta?.user_permissions || []),
|
||||
]))
|
||||
http.get(`/api/application/nodes/${uuid}/allocations`)
|
||||
.then(({ data }) => resolve((data.data || []).map(rawDataToAllocation)))
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -68,6 +68,7 @@ export const rawDataToNode = ({ attributes }: FractalResponseData): Node => ({
|
|||
});
|
||||
|
||||
export interface Filters {
|
||||
id?: string;
|
||||
uuid?: string;
|
||||
name?: string;
|
||||
image?: string;
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import http, { FractalResponseData } from '@/api/http';
|
||||
import http, { FractalResponseData, getPaginationSet, PaginatedResult } from '@/api/http';
|
||||
import { createContext, useContext } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
export interface Role {
|
||||
id: number;
|
||||
|
@ -12,10 +14,61 @@ export const rawDataToRole = ({ attributes }: FractalResponseData): Role => ({
|
|||
description: attributes.description,
|
||||
});
|
||||
|
||||
export default (include: string[] = []): Promise<Role[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get('/api/application/roles', { params: { include: include.join(',') } })
|
||||
.then(({ data }) => resolve((data.data || []).map(rawDataToRole)))
|
||||
.catch(reject);
|
||||
export interface Filters {
|
||||
id?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface ctx {
|
||||
page: number;
|
||||
setPage: (value: number | ((s: number) => number)) => void;
|
||||
|
||||
filters: Filters | null;
|
||||
setFilters: (filters: Filters | null) => void;
|
||||
|
||||
sort: string | null;
|
||||
setSort: (sort: string | null) => void;
|
||||
|
||||
sortDirection: boolean;
|
||||
setSortDirection: (direction: boolean) => void;
|
||||
}
|
||||
|
||||
export const Context = createContext<ctx>({
|
||||
page: 1,
|
||||
setPage: () => 1,
|
||||
|
||||
filters: null,
|
||||
setFilters: () => null,
|
||||
|
||||
sort: null,
|
||||
setSort: () => null,
|
||||
|
||||
sortDirection: false,
|
||||
setSortDirection: () => false,
|
||||
});
|
||||
|
||||
export default (include: string[] = []) => {
|
||||
const { page, filters, sort, sortDirection } = useContext(Context);
|
||||
|
||||
const params = {};
|
||||
if (filters !== null) {
|
||||
Object.keys(filters).forEach(key => {
|
||||
// @ts-ignore
|
||||
params['filter[' + key + ']'] = filters[key];
|
||||
});
|
||||
}
|
||||
|
||||
if (sort !== null) {
|
||||
// @ts-ignore
|
||||
params.sort = (sortDirection ? '-' : '') + sort;
|
||||
}
|
||||
|
||||
return useSWR<PaginatedResult<Role>>([ 'roles', page, filters, sort, sortDirection ], async () => {
|
||||
const { data } = await http.get('/api/application/roles', { params: { include: include.join(','), page, ...params } });
|
||||
|
||||
return ({
|
||||
items: (data.data || []).map(rawDataToRole),
|
||||
pagination: getPaginationSet(data.meta.pagination),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -100,6 +100,7 @@ export const rawDataToServer = ({ attributes }: FractalResponseData): Server =>
|
|||
});
|
||||
|
||||
export interface Filters {
|
||||
id?: string;
|
||||
uuid?: string;
|
||||
name?: string;
|
||||
image?: string;
|
||||
|
|
|
@ -36,18 +36,61 @@ export const rawDataToUser = ({ attributes }: FractalResponseData): User => ({
|
|||
updatedAt: new Date(attributes.updated_at),
|
||||
});
|
||||
|
||||
export interface Filters {
|
||||
id?: string;
|
||||
uuid?: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
}
|
||||
|
||||
interface ctx {
|
||||
page: number;
|
||||
setPage: (value: number | ((s: number) => number)) => void;
|
||||
|
||||
filters: Filters | null;
|
||||
setFilters: (filters: Filters | null) => void;
|
||||
|
||||
sort: string | null;
|
||||
setSort: (sort: string | null) => void;
|
||||
|
||||
sortDirection: boolean;
|
||||
setSortDirection: (direction: boolean) => void;
|
||||
}
|
||||
|
||||
export const Context = createContext<ctx>({ page: 1, setPage: () => 1 });
|
||||
export const Context = createContext<ctx>({
|
||||
page: 1,
|
||||
setPage: () => 1,
|
||||
|
||||
filters: null,
|
||||
setFilters: () => null,
|
||||
|
||||
sort: null,
|
||||
setSort: () => null,
|
||||
|
||||
sortDirection: false,
|
||||
setSortDirection: () => false,
|
||||
});
|
||||
|
||||
export default (include: string[] = []) => {
|
||||
const { page } = useContext(Context);
|
||||
const { page, filters, sort, sortDirection } = useContext(Context);
|
||||
|
||||
return useSWR<PaginatedResult<User>>([ 'users', page ], async () => {
|
||||
const { data } = await http.get('/api/application/users', { params: { include: include.join(','), page } });
|
||||
const params = {};
|
||||
if (filters !== null) {
|
||||
Object.keys(filters).forEach(key => {
|
||||
// @ts-ignore
|
||||
params['filter[' + key + ']'] = filters[key];
|
||||
});
|
||||
}
|
||||
|
||||
if (sort !== null) {
|
||||
// @ts-ignore
|
||||
params.sort = (sortDirection ? '-' : '') + sort;
|
||||
}
|
||||
|
||||
return useSWR<PaginatedResult<User>>([ 'users', page, filters, sort, sortDirection ], async () => {
|
||||
const { data } = await http.get('/api/application/users', { params: { include: include.join(','), page, ...params } });
|
||||
|
||||
return ({
|
||||
items: (data.data || []).map(rawDataToUser),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import getDatabases, { Context as DatabasesContext } from '@/api/admin/databases/getDatabases';
|
||||
import getDatabases, { Context as DatabasesContext, Filters } from '@/api/admin/databases/getDatabases';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
import { AdminContext } from '@/state/admin';
|
||||
|
@ -34,7 +34,7 @@ const RowCheckbox = ({ id }: { id: number}) => {
|
|||
const DatabasesContainer = () => {
|
||||
const match = useRouteMatch();
|
||||
|
||||
const { page, setPage } = useContext(DatabasesContext);
|
||||
const { page, setPage, setFilters, sort, setSort, sortDirection } = useContext(DatabasesContext);
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
const { data: databases, error, isValidating } = getDatabases();
|
||||
|
||||
|
@ -56,6 +56,17 @@ const DatabasesContainer = () => {
|
|||
setSelectedDatabases(e.currentTarget.checked ? (databases?.items?.map(database => database.id) || []) : []);
|
||||
};
|
||||
|
||||
const onSearch = (query: string): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
if (query.length < 2) {
|
||||
setFilters(null);
|
||||
} else {
|
||||
setFilters({ id: query, name: query, host: query });
|
||||
}
|
||||
return resolve();
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedDatabases([]);
|
||||
}, [ page ]);
|
||||
|
@ -89,15 +100,16 @@ const DatabasesContainer = () => {
|
|||
<ContentWrapper
|
||||
checked={selectedDatabasesLength === (length === 0 ? -1 : length)}
|
||||
onSelectAllClick={onSelectAllClick}
|
||||
onSearch={onSearch}
|
||||
>
|
||||
<Pagination data={databases} onPageSelect={setPage}>
|
||||
<div css={tw`overflow-x-auto`}>
|
||||
<table css={tw`w-full table-auto`}>
|
||||
<TableHead>
|
||||
<TableHeader name={'ID'}/>
|
||||
<TableHeader name={'Name'}/>
|
||||
<TableHeader name={'ID'} direction={sort === 'id' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('id')}/>
|
||||
<TableHeader name={'Name'} direction={sort === 'name' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('name')}/>
|
||||
<TableHeader name={'Address'}/>
|
||||
<TableHeader name={'Username'}/>
|
||||
<TableHeader name={'Username'} direction={sort === 'username' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('username')}/>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
|
@ -143,9 +155,21 @@ const DatabasesContainer = () => {
|
|||
|
||||
export default () => {
|
||||
const [ page, setPage ] = useState<number>(1);
|
||||
const [ filters, setFilters ] = useState<Filters | null>(null);
|
||||
const [ sort, setSortState ] = useState<string | null>(null);
|
||||
const [ sortDirection, setSortDirection ] = useState<boolean>(false);
|
||||
|
||||
const setSort = (newSort: string | null) => {
|
||||
if (sort === newSort) {
|
||||
setSortDirection(!sortDirection);
|
||||
} else {
|
||||
setSortState(newSort);
|
||||
setSortDirection(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DatabasesContext.Provider value={{ page, setPage }}>
|
||||
<DatabasesContext.Provider value={{ page, setPage, filters, setFilters, sort, setSort, sortDirection, setSortDirection }}>
|
||||
<DatabasesContainer/>
|
||||
</DatabasesContext.Provider>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import getLocations, { Context as LocationsContext } from '@/api/admin/locations/getLocations';
|
||||
import getLocations, { Context as LocationsContext, Filters } from '@/api/admin/locations/getLocations';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
import { AdminContext } from '@/state/admin';
|
||||
|
@ -34,7 +34,7 @@ const RowCheckbox = ({ id }: { id: number}) => {
|
|||
const LocationsContainer = () => {
|
||||
const match = useRouteMatch();
|
||||
|
||||
const { page, setPage } = useContext(LocationsContext);
|
||||
const { page, setPage, setFilters, sort, setSort, sortDirection } = useContext(LocationsContext);
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
const { data: locations, error, isValidating } = getLocations();
|
||||
|
||||
|
@ -56,6 +56,17 @@ const LocationsContainer = () => {
|
|||
setSelectedLocations(e.currentTarget.checked ? (locations?.items?.map(location => location.id) || []) : []);
|
||||
};
|
||||
|
||||
const onSearch = (query: string): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
if (query.length < 2) {
|
||||
setFilters(null);
|
||||
} else {
|
||||
setFilters({ short: query, long: query });
|
||||
}
|
||||
return resolve();
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedLocations([]);
|
||||
}, [ page ]);
|
||||
|
@ -85,14 +96,15 @@ const LocationsContainer = () => {
|
|||
<ContentWrapper
|
||||
checked={selectedLocationsLength === (length === 0 ? -1 : length)}
|
||||
onSelectAllClick={onSelectAllClick}
|
||||
onSearch={onSearch}
|
||||
>
|
||||
<Pagination data={locations} onPageSelect={setPage}>
|
||||
<div css={tw`overflow-x-auto`}>
|
||||
<table css={tw`w-full table-auto`}>
|
||||
<TableHead>
|
||||
<TableHeader name={'ID'}/>
|
||||
<TableHeader name={'Short Name'}/>
|
||||
<TableHeader name={'Long Name'}/>
|
||||
<TableHeader name={'ID'} direction={sort === 'id' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('id')}/>
|
||||
<TableHeader name={'Short Name'} direction={sort === 'short' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('short')}/>
|
||||
<TableHeader name={'Long Name'} direction={sort === 'long' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('long')}/>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
|
@ -132,9 +144,21 @@ const LocationsContainer = () => {
|
|||
|
||||
export default () => {
|
||||
const [ page, setPage ] = useState<number>(1);
|
||||
const [ filters, setFilters ] = useState<Filters | null>(null);
|
||||
const [ sort, setSortState ] = useState<string | null>(null);
|
||||
const [ sortDirection, setSortDirection ] = useState<boolean>(false);
|
||||
|
||||
const setSort = (newSort: string | null) => {
|
||||
if (sort === newSort) {
|
||||
setSortDirection(!sortDirection);
|
||||
} else {
|
||||
setSortState(newSort);
|
||||
setSortDirection(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<LocationsContext.Provider value={{ page, setPage }}>
|
||||
<LocationsContext.Provider value={{ page, setPage, filters, setFilters, sort, setSort, sortDirection, setSortDirection }}>
|
||||
<LocationsContainer/>
|
||||
</LocationsContext.Provider>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import getMounts, { Context as MountsContext } from '@/api/admin/mounts/getMounts';
|
||||
import getMounts, { Context as MountsContext, Filters } from '@/api/admin/mounts/getMounts';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
import { AdminContext } from '@/state/admin';
|
||||
|
@ -34,7 +34,7 @@ const RowCheckbox = ({ id }: { id: number}) => {
|
|||
const MountsContainer = () => {
|
||||
const match = useRouteMatch();
|
||||
|
||||
const { page, setPage } = useContext(MountsContext);
|
||||
const { page, setPage, setFilters, sort, setSort, sortDirection } = useContext(MountsContext);
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
const { data: mounts, error, isValidating } = getMounts();
|
||||
|
||||
|
@ -56,6 +56,17 @@ const MountsContainer = () => {
|
|||
setSelectedMounts(e.currentTarget.checked ? (mounts?.items?.map(mount => mount.id) || []) : []);
|
||||
};
|
||||
|
||||
const onSearch = (query: string): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
if (query.length < 2) {
|
||||
setFilters(null);
|
||||
} else {
|
||||
setFilters({ id: query });
|
||||
}
|
||||
return resolve();
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedMounts([]);
|
||||
}, [ page ]);
|
||||
|
@ -87,15 +98,16 @@ const MountsContainer = () => {
|
|||
<ContentWrapper
|
||||
checked={selectedMountsLength === (length === 0 ? -1 : length)}
|
||||
onSelectAllClick={onSelectAllClick}
|
||||
onSearch={onSearch}
|
||||
>
|
||||
<Pagination data={mounts} onPageSelect={setPage}>
|
||||
<div css={tw`overflow-x-auto`}>
|
||||
<table css={tw`w-full table-auto`}>
|
||||
<TableHead>
|
||||
<TableHeader name={'ID'}/>
|
||||
<TableHeader name={'Name'}/>
|
||||
<TableHeader name={'Source Path'}/>
|
||||
<TableHeader name={'Target Path'}/>
|
||||
<TableHeader name={'ID'} direction={sort === 'id' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('id')}/>
|
||||
<TableHeader name={'Name'} direction={sort === 'name' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('name')}/>
|
||||
<TableHeader name={'Source Path'} direction={sort === 'source' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('source')}/>
|
||||
<TableHeader name={'Target Path'} direction={sort === 'target' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('target')}/>
|
||||
<th css={tw`px-6 py-2`}/>
|
||||
<th css={tw`px-6 py-2`}/>
|
||||
</TableHead>
|
||||
|
@ -171,9 +183,21 @@ const MountsContainer = () => {
|
|||
|
||||
export default () => {
|
||||
const [ page, setPage ] = useState<number>(1);
|
||||
const [ filters, setFilters ] = useState<Filters | null>(null);
|
||||
const [ sort, setSortState ] = useState<string | null>(null);
|
||||
const [ sortDirection, setSortDirection ] = useState<boolean>(false);
|
||||
|
||||
const setSort = (newSort: string | null) => {
|
||||
if (sort === newSort) {
|
||||
setSortDirection(!sortDirection);
|
||||
} else {
|
||||
setSortState(newSort);
|
||||
setSortDirection(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<MountsContext.Provider value={{ page, setPage }}>
|
||||
<MountsContext.Provider value={{ page, setPage, filters, setFilters, sort, setSort, sortDirection, setSortDirection }}>
|
||||
<MountsContainer/>
|
||||
</MountsContext.Provider>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import { NavLink, useRouteMatch } from 'react-router-dom';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import tw from 'twin.macro';
|
||||
import AdminContentBlock from '@/components/admin/AdminContentBlock';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
|
@ -16,12 +16,11 @@ import { ApplicationStore } from '@/state';
|
|||
import { action, Action, Actions, createContextStore, useStoreActions } from 'easy-peasy';
|
||||
import { Form, Formik, FormikHelpers } from 'formik';
|
||||
import AdminBox from '@/components/admin/AdminBox';
|
||||
import AdminCheckbox from '@/components/admin/AdminCheckbox';
|
||||
import AdminTable, { ContentWrapper, NoItems, TableBody, TableHead, TableHeader, TableRow } from '@/components/admin/AdminTable';
|
||||
import CopyOnClick from '@/components/elements/CopyOnClick';
|
||||
import Input from '@/components/elements/Input';
|
||||
import Label from '@/components/elements/Label';
|
||||
import NestDeleteButton from '@/components/admin/nests/NestDeleteButton';
|
||||
import NestEggTable from '@/components/admin/nests/NestEggTable';
|
||||
|
||||
interface ctx {
|
||||
nest: Nest | undefined;
|
||||
|
@ -198,28 +197,8 @@ const ViewDetailsContainer = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const RowCheckbox = ({ id }: { id: number }) => {
|
||||
const isChecked = Context.useStoreState(state => state.selectedEggs.indexOf(id) >= 0);
|
||||
const appendSelectedEggs = Context.useStoreActions(actions => actions.appendSelectedEggs);
|
||||
const removeSelectedEggs = Context.useStoreActions(actions => actions.removeSelectedEggs);
|
||||
|
||||
return (
|
||||
<AdminCheckbox
|
||||
name={id.toString()}
|
||||
checked={isChecked}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.currentTarget.checked) {
|
||||
appendSelectedEggs(id);
|
||||
} else {
|
||||
removeSelectedEggs(id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const NestEditContainer = () => {
|
||||
const match = useRouteMatch<{ nestId?: string }>();
|
||||
const match = useRouteMatch<{ nestId: string }>();
|
||||
|
||||
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
const [ loading, setLoading ] = useState(true);
|
||||
|
@ -227,13 +206,10 @@ const NestEditContainer = () => {
|
|||
const nest = Context.useStoreState(state => state.nest);
|
||||
const setNest = Context.useStoreActions(actions => actions.setNest);
|
||||
|
||||
const setSelectedEggs = Context.useStoreActions(actions => actions.setSelectedEggs);
|
||||
const selectedEggsLength = Context.useStoreState(state => state.selectedEggs.length);
|
||||
|
||||
useEffect(() => {
|
||||
clearFlashes('nest');
|
||||
|
||||
getNest(Number(match.params?.nestId), [ 'eggs' ])
|
||||
getNest(Number(match.params.nestId), [ 'eggs' ])
|
||||
.then(nest => setNest(nest))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
|
@ -254,12 +230,6 @@ const NestEditContainer = () => {
|
|||
);
|
||||
}
|
||||
|
||||
const length = nest.relations.eggs?.length || 0;
|
||||
|
||||
const onSelectAllClick = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSelectedEggs(e.currentTarget.checked ? (nest.relations.eggs?.map(egg => egg.id) || []) : []);
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminContentBlock title={'Nests - ' + nest.name}>
|
||||
<div css={tw`w-full flex flex-row items-center mb-8`}>
|
||||
|
@ -289,52 +259,7 @@ const NestEditContainer = () => {
|
|||
<ViewDetailsContainer/>
|
||||
</div>
|
||||
|
||||
<AdminTable>
|
||||
{ length < 1 ?
|
||||
<NoItems/>
|
||||
:
|
||||
<ContentWrapper
|
||||
checked={selectedEggsLength === (length === 0 ? -1 : length)}
|
||||
onSelectAllClick={onSelectAllClick}
|
||||
>
|
||||
<div css={tw`overflow-x-auto`}>
|
||||
<table css={tw`w-full table-auto`}>
|
||||
<TableHead>
|
||||
<TableHeader name={'ID'}/>
|
||||
<TableHeader name={'Name'}/>
|
||||
<TableHeader name={'Description'}/>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{
|
||||
nest.relations.eggs?.map(egg => (
|
||||
<TableRow key={egg.id}>
|
||||
<td css={tw`pl-6`}>
|
||||
<RowCheckbox id={egg.id}/>
|
||||
</td>
|
||||
|
||||
<td css={tw`px-6 text-sm text-neutral-200 text-left whitespace-nowrap`}>
|
||||
<CopyOnClick text={egg.id.toString()}>
|
||||
<code css={tw`font-mono bg-neutral-900 rounded py-1 px-2`}>{egg.id}</code>
|
||||
</CopyOnClick>
|
||||
</td>
|
||||
|
||||
<td css={tw`px-6 text-sm text-neutral-200 text-left whitespace-nowrap`}>
|
||||
<NavLink to={`${match.url}/eggs/${egg.id}`} css={tw`text-primary-400 hover:text-primary-300`}>
|
||||
{egg.name}
|
||||
</NavLink>
|
||||
</td>
|
||||
|
||||
<td css={tw`px-6 text-sm text-neutral-200 text-left whitespace-nowrap`}>{egg.description}</td>
|
||||
</TableRow>
|
||||
))
|
||||
}
|
||||
</TableBody>
|
||||
</table>
|
||||
</div>
|
||||
</ContentWrapper>
|
||||
}
|
||||
</AdminTable>
|
||||
<NestEggTable/>
|
||||
</AdminContentBlock>
|
||||
);
|
||||
};
|
||||
|
|
147
resources/scripts/components/admin/nests/NestEggTable.tsx
Normal file
147
resources/scripts/components/admin/nests/NestEggTable.tsx
Normal file
|
@ -0,0 +1,147 @@
|
|||
import CopyOnClick from '@/components/elements/CopyOnClick';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import getEggs, { Context as EggsContext, Filters } from '@/api/admin/nests/getEggs';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
import { NavLink, useRouteMatch } from 'react-router-dom';
|
||||
import tw from 'twin.macro';
|
||||
import AdminCheckbox from '@/components/admin/AdminCheckbox';
|
||||
import AdminTable, { TableBody, TableHead, TableHeader, TableRow, Pagination, Loading, NoItems, ContentWrapper } from '@/components/admin/AdminTable';
|
||||
import { Context } from '@/components/admin/nests/NestEditContainer';
|
||||
|
||||
const RowCheckbox = ({ id }: { id: number}) => {
|
||||
const isChecked = Context.useStoreState(state => state.selectedEggs.indexOf(id) >= 0);
|
||||
const appendSelectedEggs = Context.useStoreActions(actions => actions.appendSelectedEggs);
|
||||
const removeSelectedEggs = Context.useStoreActions(actions => actions.removeSelectedEggs);
|
||||
|
||||
return (
|
||||
<AdminCheckbox
|
||||
name={id.toString()}
|
||||
checked={isChecked}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.currentTarget.checked) {
|
||||
appendSelectedEggs(id);
|
||||
} else {
|
||||
removeSelectedEggs(id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const EggsTable = () => {
|
||||
const match = useRouteMatch<{ nestId: string }>();
|
||||
|
||||
const { page, setPage, setFilters, sort, setSort, sortDirection } = useContext(EggsContext);
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
const { data: eggs, error, isValidating } = getEggs(Number(match.params.nestId));
|
||||
|
||||
useEffect(() => {
|
||||
if (!error) {
|
||||
clearFlashes('nests');
|
||||
return;
|
||||
}
|
||||
|
||||
clearAndAddHttpError({ key: 'nests', error });
|
||||
}, [ error ]);
|
||||
|
||||
const length = eggs?.items?.length || 0;
|
||||
|
||||
const setSelectedEggs = Context.useStoreActions(actions => actions.setSelectedEggs);
|
||||
const selectedEggsLength = Context.useStoreState(state => state.selectedEggs.length);
|
||||
|
||||
const onSelectAllClick = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSelectedEggs(e.currentTarget.checked ? (eggs?.items?.map(nest => nest.id) || []) : []);
|
||||
};
|
||||
|
||||
const onSearch = (query: string): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
if (query.length < 2) {
|
||||
setFilters(null);
|
||||
} else {
|
||||
setFilters({ name: query });
|
||||
}
|
||||
return resolve();
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedEggs([]);
|
||||
}, [ page ]);
|
||||
|
||||
return (
|
||||
<AdminTable>
|
||||
{ eggs === undefined || (error && isValidating) ?
|
||||
<Loading/>
|
||||
:
|
||||
length < 1 ?
|
||||
<NoItems/>
|
||||
:
|
||||
<ContentWrapper
|
||||
checked={selectedEggsLength === (length === 0 ? -1 : length)}
|
||||
onSelectAllClick={onSelectAllClick}
|
||||
onSearch={onSearch}
|
||||
>
|
||||
<Pagination data={eggs} onPageSelect={setPage}>
|
||||
<div css={tw`overflow-x-auto`}>
|
||||
<table css={tw`w-full table-auto`}>
|
||||
<TableHead>
|
||||
<TableHeader name={'ID'} direction={sort === 'id' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('id')}/>
|
||||
<TableHeader name={'Name'} direction={sort === 'name' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('name')}/>
|
||||
<TableHeader name={'Description'}/>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{
|
||||
eggs.items.map(egg => (
|
||||
<TableRow key={egg.id}>
|
||||
<td css={tw`pl-6`}>
|
||||
<RowCheckbox id={egg.id}/>
|
||||
</td>
|
||||
|
||||
<td css={tw`px-6 text-sm text-neutral-200 text-left whitespace-nowrap`}>
|
||||
<CopyOnClick text={egg.id.toString()}>
|
||||
<code css={tw`font-mono bg-neutral-900 rounded py-1 px-2`}>{egg.id}</code>
|
||||
</CopyOnClick>
|
||||
</td>
|
||||
|
||||
<td css={tw`px-6 text-sm text-neutral-200 text-left whitespace-nowrap`}>
|
||||
<NavLink to={`${match.url}/eggs/${egg.id}`} css={tw`text-primary-400 hover:text-primary-300`}>
|
||||
{egg.name}
|
||||
</NavLink>
|
||||
</td>
|
||||
|
||||
<td css={tw`px-6 text-sm text-neutral-200 text-left whitespace-nowrap`}>{egg.description}</td>
|
||||
</TableRow>
|
||||
))
|
||||
}
|
||||
</TableBody>
|
||||
</table>
|
||||
</div>
|
||||
</Pagination>
|
||||
</ContentWrapper>
|
||||
}
|
||||
</AdminTable>
|
||||
);
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const [ page, setPage ] = useState<number>(1);
|
||||
const [ filters, setFilters ] = useState<Filters | null>(null);
|
||||
const [ sort, setSortState ] = useState<string | null>(null);
|
||||
const [ sortDirection, setSortDirection ] = useState<boolean>(false);
|
||||
|
||||
const setSort = (newSort: string | null) => {
|
||||
if (sort === newSort) {
|
||||
setSortDirection(!sortDirection);
|
||||
} else {
|
||||
setSortState(newSort);
|
||||
setSortDirection(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<EggsContext.Provider value={{ page, setPage, filters, setFilters, sort, setSort, sortDirection, setSortDirection }}>
|
||||
<EggsTable/>
|
||||
</EggsContext.Provider>
|
||||
);
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import CopyOnClick from '@/components/elements/CopyOnClick';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import getNests, { Context as NestsContext } from '@/api/admin/nests/getNests';
|
||||
import getNests, { Context as NestsContext, Filters } from '@/api/admin/nests/getNests';
|
||||
import NewNestButton from '@/components/admin/nests/NewNestButton';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
|
@ -34,7 +34,7 @@ const RowCheckbox = ({ id }: { id: number}) => {
|
|||
const NestsContainer = () => {
|
||||
const match = useRouteMatch();
|
||||
|
||||
const { page, setPage } = useContext(NestsContext);
|
||||
const { page, setPage, setFilters, sort, setSort, sortDirection } = useContext(NestsContext);
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
const { data: nests, error, isValidating } = getNests();
|
||||
|
||||
|
@ -56,6 +56,17 @@ const NestsContainer = () => {
|
|||
setSelectedNests(e.currentTarget.checked ? (nests?.items?.map(nest => nest.id) || []) : []);
|
||||
};
|
||||
|
||||
const onSearch = (query: string): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
if (query.length < 2) {
|
||||
setFilters(null);
|
||||
} else {
|
||||
setFilters({ id: query });
|
||||
}
|
||||
return resolve();
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedNests([]);
|
||||
}, [ page ]);
|
||||
|
@ -85,13 +96,14 @@ const NestsContainer = () => {
|
|||
<ContentWrapper
|
||||
checked={selectedNestsLength === (length === 0 ? -1 : length)}
|
||||
onSelectAllClick={onSelectAllClick}
|
||||
onSearch={onSearch}
|
||||
>
|
||||
<Pagination data={nests} onPageSelect={setPage}>
|
||||
<div css={tw`overflow-x-auto`}>
|
||||
<table css={tw`w-full table-auto`}>
|
||||
<TableHead>
|
||||
<TableHeader name={'ID'}/>
|
||||
<TableHeader name={'Name'}/>
|
||||
<TableHeader name={'ID'} direction={sort === 'id' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('id')}/>
|
||||
<TableHeader name={'Name'} direction={sort === 'name' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('name')}/>
|
||||
<TableHeader name={'Description'}/>
|
||||
</TableHead>
|
||||
|
||||
|
@ -132,9 +144,21 @@ const NestsContainer = () => {
|
|||
|
||||
export default () => {
|
||||
const [ page, setPage ] = useState<number>(1);
|
||||
const [ filters, setFilters ] = useState<Filters | null>(null);
|
||||
const [ sort, setSortState ] = useState<string | null>(null);
|
||||
const [ sortDirection, setSortDirection ] = useState<boolean>(false);
|
||||
|
||||
const setSort = (newSort: string | null) => {
|
||||
if (sort === newSort) {
|
||||
setSortDirection(!sortDirection);
|
||||
} else {
|
||||
setSortState(newSort);
|
||||
setSortDirection(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<NestsContext.Provider value={{ page, setPage }}>
|
||||
<NestsContext.Provider value={{ page, setPage, filters, setFilters, sort, setSort, sortDirection, setSortDirection }}>
|
||||
<NestsContainer/>
|
||||
</NestsContext.Provider>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
import Label from '@/components/elements/Label';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import AdminBox from '@/components/admin/AdminBox';
|
||||
import Creatable from 'react-select/creatable';
|
||||
import { ActionMeta, GroupTypeBase, InputActionMeta, ValueType } from 'react-select/src/types';
|
||||
import { SelectStyle } from '@/components/elements/Select2';
|
||||
import tw from 'twin.macro';
|
||||
import getAllocations from '@/api/admin/nodes/getAllocations';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const distinct = (value: any, index: any, self: any) => {
|
||||
return self.indexOf(value) === index;
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const match = useRouteMatch<{ id: string }>();
|
||||
|
||||
const [ ips, setIPs ] = useState<Option[]>([]);
|
||||
const [ ports, setPorts ] = useState<Option[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
getAllocations(match.params.id)
|
||||
.then(allocations => {
|
||||
setIPs(allocations.map(a => a.ip).filter(distinct).map(ip => {
|
||||
return { value: ip, label: ip };
|
||||
}));
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onChange = (value: ValueType<Option, any>, action: ActionMeta<any>) => {
|
||||
console.log({
|
||||
event: 'onChange',
|
||||
value,
|
||||
action,
|
||||
});
|
||||
};
|
||||
|
||||
const onInputChange = (newValue: string, actionMeta: InputActionMeta) => {
|
||||
console.log({
|
||||
event: 'onInputChange',
|
||||
newValue,
|
||||
actionMeta,
|
||||
});
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const isValidNewOption1 = (inputValue: string, selectValue: ValueType<Option, any>, selectOptions: ReadonlyArray<Option | GroupTypeBase<Option>>): boolean => {
|
||||
return inputValue.match(/^([0-9a-f.:/]+)$/) !== null;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const isValidNewOption2 = (inputValue: string, selectValue: ValueType<Option, any>, selectOptions: ReadonlyArray<Option | GroupTypeBase<Option>>): boolean => {
|
||||
return inputValue.match(/^([0-9-]+)$/) !== null;
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminBox title={'Allocations'}>
|
||||
<div css={tw`mb-6`}>
|
||||
<Label>IPs and CIDRs</Label>
|
||||
<Creatable
|
||||
options={ips}
|
||||
styles={SelectStyle}
|
||||
onChange={onChange}
|
||||
onInputChange={onInputChange}
|
||||
isValidNewOption={isValidNewOption1}
|
||||
isMulti
|
||||
isSearchable
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div css={tw`mb-6`}>
|
||||
<Label>Ports</Label>
|
||||
<Creatable
|
||||
options={ports}
|
||||
styles={SelectStyle}
|
||||
// onChange={onChange}
|
||||
// onInputChange={onInputChange}
|
||||
isValidNewOption={isValidNewOption2}
|
||||
isMulti
|
||||
isSearchable
|
||||
/>
|
||||
</div>
|
||||
</AdminBox>
|
||||
);
|
||||
};
|
|
@ -13,6 +13,7 @@ import { SubNavigation, SubNavigationLink } from '@/components/admin/SubNavigati
|
|||
import NodeAboutContainer from '@/components/admin/nodes/NodeAboutContainer';
|
||||
import NodeSettingsContainer from '@/components/admin/nodes/NodeSettingsContainer';
|
||||
import NodeConfigurationContainer from '@/components/admin/nodes/NodeConfigurationContainer';
|
||||
import NodeAllocationContainer from '@/components/admin/nodes/NodeAllocationContainer';
|
||||
|
||||
interface ctx {
|
||||
node: Node | undefined;
|
||||
|
@ -118,7 +119,7 @@ const NodeRouter = () => {
|
|||
</Route>
|
||||
|
||||
<Route path={`${match.path}/allocations`} exact>
|
||||
<p>Allocations</p>
|
||||
<NodeAllocationContainer/>
|
||||
</Route>
|
||||
|
||||
<Route path={`${match.path}/servers`} exact>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useDeepMemoize } from '@/plugins/useDeepMemoize';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import getRoles, { Context as RolesContext, Filters } from '@/api/admin/roles/getRoles';
|
||||
import { AdminContext } from '@/state/admin';
|
||||
import NewRoleButton from '@/components/admin/roles/NewRoleButton';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
|
@ -7,9 +7,8 @@ import useFlash from '@/plugins/useFlash';
|
|||
import { NavLink, useRouteMatch } from 'react-router-dom';
|
||||
import tw from 'twin.macro';
|
||||
import AdminContentBlock from '@/components/admin/AdminContentBlock';
|
||||
import getRoles from '@/api/admin/roles/getRoles';
|
||||
import AdminCheckbox from '@/components/admin/AdminCheckbox';
|
||||
import AdminTable, { ContentWrapper, Loading, NoItems, TableBody, TableHead, TableHeader, TableRow } from '@/components/admin/AdminTable';
|
||||
import AdminTable, { TableBody, TableHead, TableHeader, TableRow, Pagination, Loading, NoItems, ContentWrapper } from '@/components/admin/AdminTable';
|
||||
import CopyOnClick from '@/components/elements/CopyOnClick';
|
||||
|
||||
const RowCheckbox = ({ id }: { id: number }) => {
|
||||
|
@ -32,35 +31,46 @@ const RowCheckbox = ({ id }: { id: number }) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const RolesContainer = () => {
|
||||
const match = useRouteMatch();
|
||||
|
||||
const { page, setPage, setFilters, sort, setSort, sortDirection } = useContext(RolesContext);
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
const [ loading, setLoading ] = useState(true);
|
||||
const { data: roles, error, isValidating } = getRoles();
|
||||
|
||||
const roles = useDeepMemoize(AdminContext.useStoreState(state => state.roles.data));
|
||||
const setRoles = AdminContext.useStoreActions(state => state.roles.setRoles);
|
||||
useEffect(() => {
|
||||
if (!error) {
|
||||
clearFlashes('roles');
|
||||
return;
|
||||
}
|
||||
|
||||
clearAndAddHttpError({ key: 'roles', error });
|
||||
}, [ error ]);
|
||||
|
||||
const length = roles?.items?.length || 0;
|
||||
|
||||
const setSelectedRoles = AdminContext.useStoreActions(actions => actions.roles.setSelectedRoles);
|
||||
const selectedRolesLength = AdminContext.useStoreState(state => state.roles.selectedRoles.length);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(!roles.length);
|
||||
clearFlashes('roles');
|
||||
|
||||
getRoles()
|
||||
.then(roles => setRoles(roles))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
clearAndAddHttpError({ key: 'roles', error });
|
||||
})
|
||||
.then(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const onSelectAllClick = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSelectedRoles(e.currentTarget.checked ? (roles.map(role => role.id) || []) : []);
|
||||
setSelectedRoles(e.currentTarget.checked ? (roles?.items?.map(role => role.id) || []) : []);
|
||||
};
|
||||
|
||||
const onSearch = (query: string): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
if (query.length < 2) {
|
||||
setFilters(null);
|
||||
} else {
|
||||
setFilters({ name: query });
|
||||
}
|
||||
return resolve();
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedRoles([]);
|
||||
}, [ page ]);
|
||||
|
||||
return (
|
||||
<AdminContentBlock title={'Roles'}>
|
||||
<div css={tw`w-full flex flex-row items-center mb-8`}>
|
||||
|
@ -77,54 +87,79 @@ export default () => {
|
|||
<FlashMessageRender byKey={'roles'} css={tw`mb-4`}/>
|
||||
|
||||
<AdminTable>
|
||||
{ loading ?
|
||||
{ roles === undefined || (error && isValidating) ?
|
||||
<Loading/>
|
||||
:
|
||||
roles.length < 1 ?
|
||||
length < 1 ?
|
||||
<NoItems/>
|
||||
:
|
||||
<ContentWrapper
|
||||
checked={selectedRolesLength === (roles.length === 0 ? -1 : roles.length)}
|
||||
checked={selectedRolesLength === (length === 0 ? -1 : length)}
|
||||
onSelectAllClick={onSelectAllClick}
|
||||
onSearch={onSearch}
|
||||
>
|
||||
<div css={tw`overflow-x-auto`}>
|
||||
<table css={tw`w-full table-auto`}>
|
||||
<TableHead>
|
||||
<TableHeader name={'ID'}/>
|
||||
<TableHeader name={'Name'}/>
|
||||
<TableHeader name={'Description'}/>
|
||||
</TableHead>
|
||||
<Pagination data={roles} onPageSelect={setPage}>
|
||||
<div css={tw`overflow-x-auto`}>
|
||||
<table css={tw`w-full table-auto`}>
|
||||
<TableHead>
|
||||
<TableHeader name={'ID'} direction={sort === 'id' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('id')}/>
|
||||
<TableHeader name={'Name'} direction={sort === 'name' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('name')}/>
|
||||
<TableHeader name={'Description'}/>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{
|
||||
roles.map(role => (
|
||||
<TableRow key={role.id} css={role.id === roles[roles.length - 1].id ? tw`rounded-b-lg` : undefined}>
|
||||
<td css={tw`pl-6`}>
|
||||
<RowCheckbox id={role.id}/>
|
||||
</td>
|
||||
<TableBody>
|
||||
{
|
||||
roles.items.map(role => (
|
||||
<TableRow key={role.id}>
|
||||
<td css={tw`pl-6`}>
|
||||
<RowCheckbox id={role.id}/>
|
||||
</td>
|
||||
|
||||
<td css={tw`px-6 text-sm text-neutral-200 text-left whitespace-nowrap`}>
|
||||
<CopyOnClick text={role.id.toString()}>
|
||||
<code css={tw`font-mono bg-neutral-900 rounded py-1 px-2`}>{role.id}</code>
|
||||
</CopyOnClick>
|
||||
</td>
|
||||
<td css={tw`px-6 text-sm text-neutral-200 text-left whitespace-nowrap`}>
|
||||
<CopyOnClick text={role.id.toString()}>
|
||||
<code css={tw`font-mono bg-neutral-900 rounded py-1 px-2`}>{role.id}</code>
|
||||
</CopyOnClick>
|
||||
</td>
|
||||
|
||||
<td css={tw`px-6 text-sm text-neutral-200 text-left whitespace-nowrap`}>
|
||||
<NavLink to={`${match.url}/${role.id}`} css={tw`text-primary-400 hover:text-primary-300`}>
|
||||
{role.name}
|
||||
</NavLink>
|
||||
</td>
|
||||
<td css={tw`px-6 text-sm text-neutral-200 text-left whitespace-nowrap`}>
|
||||
<NavLink to={`${match.url}/${role.id}`} css={tw`text-primary-400 hover:text-primary-300`}>
|
||||
{role.name}
|
||||
</NavLink>
|
||||
</td>
|
||||
|
||||
<td css={tw`px-6 text-sm text-neutral-200 text-left whitespace-nowrap`}>{role.description}</td>
|
||||
</TableRow>
|
||||
))
|
||||
}
|
||||
</TableBody>
|
||||
</table>
|
||||
</div>
|
||||
<td css={tw`px-6 text-sm text-neutral-200 text-left whitespace-nowrap`}>{role.description}</td>
|
||||
</TableRow>
|
||||
))
|
||||
}
|
||||
</TableBody>
|
||||
</table>
|
||||
</div>
|
||||
</Pagination>
|
||||
</ContentWrapper>
|
||||
}
|
||||
</AdminTable>
|
||||
</AdminContentBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const [ page, setPage ] = useState<number>(1);
|
||||
const [ filters, setFilters ] = useState<Filters | null>(null);
|
||||
const [ sort, setSortState ] = useState<string | null>(null);
|
||||
const [ sortDirection, setSortDirection ] = useState<boolean>(false);
|
||||
|
||||
const setSort = (newSort: string | null) => {
|
||||
if (sort === newSort) {
|
||||
setSortDirection(!sortDirection);
|
||||
} else {
|
||||
setSortState(newSort);
|
||||
setSortDirection(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<RolesContext.Provider value={{ page, setPage, filters, setFilters, sort, setSort, sortDirection, setSortDirection }}>
|
||||
<RolesContainer/>
|
||||
</RolesContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import AdminCheckbox from '@/components/admin/AdminCheckbox';
|
||||
import CopyOnClick from '@/components/elements/CopyOnClick';
|
||||
import getUsers, { Context as UsersContext } from '@/api/admin/users/getUsers';
|
||||
import getUsers, { Context as UsersContext, Filters } from '@/api/admin/users/getUsers';
|
||||
import AdminTable, { ContentWrapper, Loading, NoItems, Pagination, TableBody, TableHead, TableHeader } from '@/components/admin/AdminTable';
|
||||
import Button from '@/components/elements/Button';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
|
@ -34,7 +34,7 @@ const RowCheckbox = ({ id }: { id: number }) => {
|
|||
const UsersContainer = () => {
|
||||
const match = useRouteMatch();
|
||||
|
||||
const { page, setPage } = useContext(UsersContext);
|
||||
const { page, setPage, setFilters, sort, setSort, sortDirection } = useContext(UsersContext);
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
const { data: users, error, isValidating } = getUsers();
|
||||
|
||||
|
@ -56,6 +56,17 @@ const UsersContainer = () => {
|
|||
setSelectedUsers(e.currentTarget.checked ? (users?.items?.map(user => user.id) || []) : []);
|
||||
};
|
||||
|
||||
const onSearch = (query: string): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
if (query.length < 2) {
|
||||
setFilters(null);
|
||||
} else {
|
||||
setFilters({ username: query });
|
||||
}
|
||||
return resolve();
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedUsers([]);
|
||||
}, [ page ]);
|
||||
|
@ -89,16 +100,17 @@ const UsersContainer = () => {
|
|||
<ContentWrapper
|
||||
checked={selectedUserLength === (length === 0 ? -1 : length)}
|
||||
onSelectAllClick={onSelectAllClick}
|
||||
onSearch={onSearch}
|
||||
>
|
||||
<Pagination data={users} onPageSelect={setPage}>
|
||||
<div css={tw`overflow-x-auto`}>
|
||||
<table css={tw`w-full table-auto`}>
|
||||
<TableHead>
|
||||
<TableHeader name={'ID'}/>
|
||||
<TableHeader name={'Name'}/>
|
||||
<TableHeader name={'Username'}/>
|
||||
<TableHeader name={'ID'} direction={sort === 'id' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('id')}/>
|
||||
<TableHeader name={'Name'} direction={sort === 'email' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('email')}/>
|
||||
<TableHeader name={'Username'} direction={sort === 'username' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('username')}/>
|
||||
<TableHeader name={'Status'}/>
|
||||
<TableHeader name={'Role'}/>
|
||||
<TableHeader name={'Role'} direction={sort === 'admin_role_id' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('admin_role_id')}/>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
|
@ -160,9 +172,21 @@ const UsersContainer = () => {
|
|||
|
||||
export default () => {
|
||||
const [ page, setPage ] = useState<number>(1);
|
||||
const [ filters, setFilters ] = useState<Filters | null>(null);
|
||||
const [ sort, setSortState ] = useState<string | null>(null);
|
||||
const [ sortDirection, setSortDirection ] = useState<boolean>(false);
|
||||
|
||||
const setSort = (newSort: string | null) => {
|
||||
if (sort === newSort) {
|
||||
setSortDirection(!sortDirection);
|
||||
} else {
|
||||
setSortState(newSort);
|
||||
setSortDirection(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<UsersContext.Provider value={{ page, setPage }}>
|
||||
<UsersContext.Provider value={{ page, setPage, filters, setFilters, sort, setSort, sortDirection, setSortDirection }}>
|
||||
<UsersContainer/>
|
||||
</UsersContext.Provider>
|
||||
);
|
||||
|
|
101
resources/scripts/components/elements/Select2.ts
Normal file
101
resources/scripts/components/elements/Select2.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
import { CSSObject } from '@emotion/serialize';
|
||||
import { ContainerProps, ControlProps, InputProps, MenuProps, MultiValueProps, OptionProps, PlaceholderProps, SingleValueProps, StylesConfig, ValueContainerProps } from 'react-select';
|
||||
import { theme } from 'twin.macro';
|
||||
|
||||
type T = any;
|
||||
|
||||
export const SelectStyle: StylesConfig<T, any, any> = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
container: (base: CSSObject, props: ContainerProps<T, any, any>): CSSObject => {
|
||||
return {
|
||||
...base,
|
||||
};
|
||||
},
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
control: (base: CSSObject, props: ControlProps<T, any, any>): CSSObject => {
|
||||
return {
|
||||
...base,
|
||||
height: '2.75rem',
|
||||
/* paddingTop: '0.75rem',
|
||||
paddingBottom: '0.75rem',
|
||||
paddingLeft: '4rem',
|
||||
paddingRight: '4rem', */
|
||||
background: theme`colors.neutral.600`,
|
||||
borderColor: theme`colors.neutral.500`,
|
||||
borderWidth: '2px',
|
||||
color: theme`colors.neutral.200`,
|
||||
};
|
||||
},
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
input: (base: CSSObject, props: InputProps): CSSObject => {
|
||||
return {
|
||||
...base,
|
||||
color: theme`colors.neutral.200`,
|
||||
fontSize: '0.875rem',
|
||||
};
|
||||
},
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
menu: (base: CSSObject, props: MenuProps<T, any, any>): CSSObject => {
|
||||
return {
|
||||
...base,
|
||||
background: theme`colors.neutral.900`,
|
||||
color: theme`colors.neutral.200`,
|
||||
};
|
||||
},
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
multiValue: (base: CSSObject, props: MultiValueProps<T, any>): CSSObject => {
|
||||
return {
|
||||
...base,
|
||||
background: theme`colors.neutral.900`,
|
||||
color: theme`colors.neutral.200`,
|
||||
};
|
||||
},
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
multiValueLabel: (base: CSSObject, props: MultiValueProps<T, any>): CSSObject => {
|
||||
return {
|
||||
...base,
|
||||
color: theme`colors.neutral.200`,
|
||||
};
|
||||
},
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
option: (base: CSSObject, props: OptionProps<T, any, any>): CSSObject => {
|
||||
return {
|
||||
...base,
|
||||
background: theme`colors.neutral.900`,
|
||||
':hover': {
|
||||
background: theme`colors.neutral.700`,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
placeholder: (base: CSSObject, props: PlaceholderProps<T, any, any>): CSSObject => {
|
||||
return {
|
||||
...base,
|
||||
color: theme`colors.neutral.300`,
|
||||
fontSize: '0.875rem',
|
||||
};
|
||||
},
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
singleValue: (base: CSSObject, props: SingleValueProps<T, any>): CSSObject => {
|
||||
return {
|
||||
...base,
|
||||
color: '#00000',
|
||||
};
|
||||
},
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
valueContainer: (base: CSSObject, props: ValueContainerProps<T, any>): CSSObject => {
|
||||
return {
|
||||
...base,
|
||||
};
|
||||
},
|
||||
};
|
Loading…
Reference in a new issue