From 8f1a5bf0ab718f99de1a1606460244ce1212fff5 Mon Sep 17 00:00:00 2001 From: Matthew Penner Date: Tue, 5 Jan 2021 09:17:44 -0700 Subject: [PATCH] Re-enable debugbar, add table to ServersContainer.tsx --- .../Application/ApplicationApiController.php | 1 + .../Api/Application/BaseTransformer.php | 33 ++++ .../Api/Application/ServerTransformer.php | 20 +- config/debugbar.php | 2 +- .../scripts/api/admin/nests/eggs/getEggs.ts | 27 ++- resources/scripts/api/admin/nests/getNests.ts | 13 +- resources/scripts/api/admin/nodes/getNodes.ts | 69 +++++++ resources/scripts/api/admin/roles/getRoles.ts | 11 +- .../scripts/api/admin/servers/getServers.ts | 63 +++++++ resources/scripts/api/admin/users/getUsers.ts | 19 +- resources/scripts/api/transformers.ts | 62 +------ .../admin/servers/ServersContainer.tsx | 174 +++++++++++++++++- .../components/admin/users/UsersContainer.tsx | 4 +- resources/scripts/state/admin/index.ts | 3 + resources/scripts/state/admin/servers.ts | 27 +++ 15 files changed, 446 insertions(+), 82 deletions(-) create mode 100644 resources/scripts/api/admin/nodes/getNodes.ts create mode 100644 resources/scripts/api/admin/servers/getServers.ts create mode 100644 resources/scripts/state/admin/servers.ts diff --git a/app/Http/Controllers/Api/Application/ApplicationApiController.php b/app/Http/Controllers/Api/Application/ApplicationApiController.php index 70d2bcfe2..941be0620 100644 --- a/app/Http/Controllers/Api/Application/ApplicationApiController.php +++ b/app/Http/Controllers/Api/Application/ApplicationApiController.php @@ -67,6 +67,7 @@ abstract class ApplicationApiController extends Controller { /** @var \Pterodactyl\Transformers\Api\Application\BaseTransformer $transformer */ $transformer = Container::getInstance()->make($abstract); + $transformer->setRootAdmin($this->request->user()->root_admin); $transformer->setKey($this->request->attributes->get('api_key')); Assert::isInstanceOf($transformer, BaseTransformer::class); diff --git a/app/Transformers/Api/Application/BaseTransformer.php b/app/Transformers/Api/Application/BaseTransformer.php index 3bdd0ad91..90618fb09 100644 --- a/app/Transformers/Api/Application/BaseTransformer.php +++ b/app/Transformers/Api/Application/BaseTransformer.php @@ -22,6 +22,11 @@ abstract class BaseTransformer extends TransformerAbstract */ private $key; + /** + * @var bool + */ + private $rootAdmin; + /** * Return the resource name for the JSONAPI output. * @@ -64,6 +69,30 @@ abstract class BaseTransformer extends TransformerAbstract return $this->key; } + /** + * ? + * + * @param bool $rootAdmin + * + * @return $this + */ + public function setRootAdmin(bool $rootAdmin) + { + $this->rootAdmin = $rootAdmin; + + return $this; + } + + /** + * ? + * + * @return bool + */ + public function isRootAdmin(): bool + { + return $this->rootAdmin; + } + /** * Determine if the API key loaded onto the transformer has permission * to access a different resource. This is used when including other @@ -75,6 +104,10 @@ abstract class BaseTransformer extends TransformerAbstract */ protected function authorize(string $resource): bool { + if ($this->isRootAdmin()) { + return true; + } + return AdminAcl::check($this->getKey(), $resource, AdminAcl::READ); } diff --git a/app/Transformers/Api/Application/ServerTransformer.php b/app/Transformers/Api/Application/ServerTransformer.php index e58152bb0..53e3483da 100644 --- a/app/Transformers/Api/Application/ServerTransformer.php +++ b/app/Transformers/Api/Application/ServerTransformer.php @@ -67,7 +67,17 @@ class ServerTransformer extends BaseTransformer 'identifier' => $model->uuidShort, 'name' => $model->name, 'description' => $model->description, - 'suspended' => (bool) $model->suspended, + + 'is_suspended' => $model->suspended, + 'is_installing' => $model->installed !== 1, + 'is_transferring' => ! is_null($model->transfer), + + 'user' => $model->owner_id, + 'node' => $model->node_id, + 'allocation' => $model->allocation_id, + 'nest' => $model->nest_id, + 'egg' => $model->egg_id, + 'limits' => [ 'memory' => $model->memory, 'swap' => $model->swap, @@ -76,22 +86,20 @@ class ServerTransformer extends BaseTransformer 'cpu' => $model->cpu, 'threads' => $model->threads, ], + 'feature_limits' => [ 'databases' => $model->database_limit, 'allocations' => $model->allocation_limit, 'backups' => $model->backup_limit, ], - 'user' => $model->owner_id, - 'node' => $model->node_id, - 'allocation' => $model->allocation_id, - 'nest' => $model->nest_id, - 'egg' => $model->egg_id, + 'container' => [ 'startup_command' => $model->startup, 'image' => $model->image, 'installed' => (int) $model->installed === 1, 'environment' => $this->environmentService->handle($model), ], + $model->getUpdatedAtColumn() => $this->formatTimestamp($model->updated_at), $model->getCreatedAtColumn() => $this->formatTimestamp($model->created_at), ]; diff --git a/config/debugbar.php b/config/debugbar.php index 1367301cc..27d285e41 100644 --- a/config/debugbar.php +++ b/config/debugbar.php @@ -11,7 +11,7 @@ return [ | */ - 'enabled' => false, + 'enabled' => true, /* |-------------------------------------------------------------------------- diff --git a/resources/scripts/api/admin/nests/eggs/getEggs.ts b/resources/scripts/api/admin/nests/eggs/getEggs.ts index 6509e1ac0..5edd7b19f 100644 --- a/resources/scripts/api/admin/nests/eggs/getEggs.ts +++ b/resources/scripts/api/admin/nests/eggs/getEggs.ts @@ -1,5 +1,4 @@ -import http from '@/api/http'; -import { rawDataToEgg } from '@/api/transformers'; +import http, { FractalResponseData } from '@/api/http'; export interface Egg { id: number; @@ -25,6 +24,30 @@ export interface Egg { updatedAt: Date; } +export const rawDataToEgg = ({ attributes }: FractalResponseData): Egg => ({ + id: attributes.id, + uuid: attributes.uuid, + nest_id: attributes.nest_id, + author: attributes.author, + name: attributes.name, + description: attributes.description, + features: attributes.features, + dockerImages: attributes.docker_images, + configFiles: attributes.config_files, + configStartup: attributes.config_startup, + configLogs: attributes.config_logs, + configStop: attributes.config_stop, + configFrom: attributes.config_from, + startup: attributes.startup, + scriptContainer: attributes.script_container, + copyScriptFrom: attributes.copy_script_from, + scriptEntry: attributes.script_entry, + scriptIsPrivileged: attributes.script_is_privileged, + scriptInstall: attributes.script_install, + createdAt: new Date(attributes.created_at), + updatedAt: new Date(attributes.updated_at), +}); + export default (id: number): Promise => { return new Promise((resolve, reject) => { http.get(`/api/application/nests/${id}`) diff --git a/resources/scripts/api/admin/nests/getNests.ts b/resources/scripts/api/admin/nests/getNests.ts index 4ba1bb259..da1ef41b3 100644 --- a/resources/scripts/api/admin/nests/getNests.ts +++ b/resources/scripts/api/admin/nests/getNests.ts @@ -1,5 +1,4 @@ -import http, { getPaginationSet, PaginatedResult } from '@/api/http'; -import { rawDataToNest } from '@/api/transformers'; +import http, { FractalResponseData, getPaginationSet, PaginatedResult } from '@/api/http'; import { createContext, useContext } from 'react'; import useSWR from 'swr'; @@ -13,6 +12,16 @@ export interface Nest { updatedAt: Date; } +export const rawDataToNest = ({ attributes }: FractalResponseData): Nest => ({ + id: attributes.id, + uuid: attributes.uuid, + author: attributes.author, + name: attributes.name, + description: attributes.description, + createdAt: new Date(attributes.created_at), + updatedAt: new Date(attributes.updated_at), +}); + interface ctx { page: number; setPage: (value: number | ((s: number) => number)) => void; diff --git a/resources/scripts/api/admin/nodes/getNodes.ts b/resources/scripts/api/admin/nodes/getNodes.ts new file mode 100644 index 000000000..45a020f0d --- /dev/null +++ b/resources/scripts/api/admin/nodes/getNodes.ts @@ -0,0 +1,69 @@ +import http, { FractalResponseData, getPaginationSet, PaginatedResult } from '@/api/http'; +import { createContext, useContext } from 'react'; +import useSWR from 'swr'; + +export interface Node { + id: number; + uuid: string; + public: boolean; + name: string; + description: string | null; + locationId: number; + fqdn: string; + scheme: string; + behindProxy: boolean; + maintenanceMode: boolean; + memory: number; + memoryOverallocate: number; + disk: number; + diskOverallocate: number; + uploadSize: number; + daemonListen: number; + daemonSftp: number; + daemonBase: string; + createdAt: Date; + updatedAt: Date; +} + +export const rawDataToNode = ({ attributes }: FractalResponseData): Node => ({ + id: attributes.id, + uuid: attributes.uuid, + public: attributes.public, + name: attributes.name, + description: attributes.description, + locationId: attributes.location_id, + fqdn: attributes.fqdn, + scheme: attributes.scheme, + behindProxy: attributes.behind_proxy, + maintenanceMode: attributes.maintenance_mode, + memory: attributes.memory, + memoryOverallocate: attributes.memory_overallocate, + disk: attributes.disk, + diskOverallocate: attributes.disk_overallocate, + uploadSize: attributes.upload_size, + daemonListen: attributes.daemon_listen, + daemonSftp: attributes.daemon_sftp, + daemonBase: attributes.daemon_base, + createdAt: new Date(attributes.created_at), + updatedAt: new Date(attributes.updated_at), +}); + +interface ctx { + page: number; + setPage: (value: number | ((s: number) => number)) => void; +} + +export const Context = createContext({ page: 1, setPage: () => 1 }); + +export default () => { + const { page } = useContext(Context); + + return useSWR>([ 'nodes', page ], async () => { + const { data } = await http.get('/api/application/nodes', { params: { page } }); + + return ({ + items: (data.data || []).map(rawDataToNode), + pagination: getPaginationSet(data.meta.pagination), + }); + }); +}; diff --git a/resources/scripts/api/admin/roles/getRoles.ts b/resources/scripts/api/admin/roles/getRoles.ts index 0a6213a08..b928e04c6 100644 --- a/resources/scripts/api/admin/roles/getRoles.ts +++ b/resources/scripts/api/admin/roles/getRoles.ts @@ -1,5 +1,4 @@ -import http from '@/api/http'; -import { rawDataToAdminRole } from '@/api/transformers'; +import http, { FractalResponseData } from '@/api/http'; export interface Role { id: number; @@ -7,10 +6,16 @@ export interface Role { description: string | null; } +export const rawDataToRole = ({ attributes }: FractalResponseData): Role => ({ + id: attributes.id, + name: attributes.name, + description: attributes.description, +}); + export default (): Promise => { return new Promise((resolve, reject) => { http.get('/api/application/roles') - .then(({ data }) => resolve((data.data || []).map(rawDataToAdminRole))) + .then(({ data }) => resolve((data.data || []).map(rawDataToRole))) .catch(reject); }); }; diff --git a/resources/scripts/api/admin/servers/getServers.ts b/resources/scripts/api/admin/servers/getServers.ts new file mode 100644 index 000000000..daa6f2d8f --- /dev/null +++ b/resources/scripts/api/admin/servers/getServers.ts @@ -0,0 +1,63 @@ +import http, { FractalResponseData, getPaginationSet, PaginatedResult } from '@/api/http'; +import { createContext, useContext } from 'react'; +import useSWR from 'swr'; +import { Node, rawDataToNode } from '@/api/admin/nodes/getNodes'; +import { User, rawDataToUser } from '@/api/admin/users/getUsers'; + +export interface Server { + id: number; + externalId: string; + uuid: string; + identifier: string; + name: string; + description: string; + isSuspended: boolean; + isInstalling: boolean; + isTransferring: boolean; + createdAt: Date; + updatedAt: Date; + + relations: { + node: Node | undefined; + user: User | undefined; + }; +} + +const rawDataToServerObject = ({ attributes }: FractalResponseData): Server => ({ + id: attributes.id, + externalId: attributes.external_id, + uuid: attributes.uuid, + identifier: attributes.identifier, + name: attributes.name, + description: attributes.description, + isSuspended: attributes.is_suspended, + isInstalling: attributes.is_installing, + isTransferring: attributes.is_transferring, + createdAt: new Date(attributes.created_at), + updatedAt: new Date(attributes.updated_at), + + relations: { + node: attributes.relationships?.node !== undefined ? rawDataToNode(attributes.relationships.node as FractalResponseData) : undefined, + user: attributes.relationships?.user !== undefined ? rawDataToUser(attributes.relationships.user as FractalResponseData) : undefined, + }, +}); + +interface ctx { + page: number; + setPage: (value: number | ((s: number) => number)) => void; +} + +export const Context = createContext({ page: 1, setPage: () => 1 }); + +export default () => { + const { page } = useContext(Context); + + return useSWR>([ 'servers', page ], async () => { + const { data } = await http.get('/api/application/servers', { params: { include: 'node,user', page } }); + + return ({ + items: (data.data || []).map(rawDataToServerObject), + pagination: getPaginationSet(data.meta.pagination), + }); + }); +}; diff --git a/resources/scripts/api/admin/users/getUsers.ts b/resources/scripts/api/admin/users/getUsers.ts index 226c426dd..5b5138cde 100644 --- a/resources/scripts/api/admin/users/getUsers.ts +++ b/resources/scripts/api/admin/users/getUsers.ts @@ -1,5 +1,4 @@ -import http, { getPaginationSet, PaginatedResult } from '@/api/http'; -import { rawDataToUser } from '@/api/transformers'; +import http, { FractalResponseData, getPaginationSet, PaginatedResult } from '@/api/http'; import { createContext, useContext } from 'react'; import useSWR from 'swr'; @@ -19,6 +18,22 @@ export interface User { updatedAt: Date; } +export const rawDataToUser = ({ attributes }: FractalResponseData): User => ({ + id: attributes.id, + externalId: attributes.external_id, + uuid: attributes.uuid, + username: attributes.username, + email: attributes.email, + firstName: attributes.first_name, + lastName: attributes.last_name, + language: attributes.language, + rootAdmin: attributes.root_admin, + tfa: attributes['2fa'], + roleName: attributes.role_name, + createdAt: new Date(attributes.created_at), + updatedAt: new Date(attributes.updated_at), +}); + interface ctx { page: number; setPage: (value: number | ((s: number) => number)) => void; diff --git a/resources/scripts/api/transformers.ts b/resources/scripts/api/transformers.ts index 11e1bfb10..73c6c217f 100644 --- a/resources/scripts/api/transformers.ts +++ b/resources/scripts/api/transformers.ts @@ -1,9 +1,5 @@ -import { Egg } from '@/api/admin/nests/eggs/getEggs'; -import { Nest } from '@/api/admin/nests/getNests'; -import { Role } from '@/api/admin/roles/getRoles'; -import { User } from '@/api/admin/users/getUsers'; -import { Allocation } from '@/api/server/getServer'; import { FractalResponseData } from '@/api/http'; +import { Allocation } from '@/api/server/getServer'; import { FileObject } from '@/api/server/files/loadDirectory'; import { ServerBackup, ServerEggVariable } from '@/api/server/types'; @@ -78,59 +74,3 @@ export const rawDataToServerEggVariable = ({ attributes }: FractalResponseData): isEditable: attributes.is_editable, rules: attributes.rules.split('|'), }); - -export const rawDataToAdminRole = ({ attributes }: FractalResponseData): Role => ({ - id: attributes.id, - name: attributes.name, - description: attributes.description, -}); - -export const rawDataToNest = ({ attributes }: FractalResponseData): Nest => ({ - id: attributes.id, - uuid: attributes.uuid, - author: attributes.author, - name: attributes.name, - description: attributes.description, - createdAt: new Date(attributes.created_at), - updatedAt: new Date(attributes.updated_at), -}); - -export const rawDataToEgg = ({ attributes }: FractalResponseData): Egg => ({ - id: attributes.id, - uuid: attributes.uuid, - nest_id: attributes.nest_id, - author: attributes.author, - name: attributes.name, - description: attributes.description, - features: attributes.features, - dockerImages: attributes.docker_images, - configFiles: attributes.config_files, - configStartup: attributes.config_startup, - configLogs: attributes.config_logs, - configStop: attributes.config_stop, - configFrom: attributes.config_from, - startup: attributes.startup, - scriptContainer: attributes.script_container, - copyScriptFrom: attributes.copy_script_from, - scriptEntry: attributes.script_entry, - scriptIsPrivileged: attributes.script_is_privileged, - scriptInstall: attributes.script_install, - createdAt: new Date(attributes.created_at), - updatedAt: new Date(attributes.updated_at), -}); - -export const rawDataToUser = ({ attributes }: FractalResponseData): User => ({ - id: attributes.id, - externalId: attributes.external_id, - uuid: attributes.uuid, - username: attributes.username, - email: attributes.email, - firstName: attributes.first_name, - lastName: attributes.last_name, - language: attributes.language, - rootAdmin: attributes.root_admin, - tfa: attributes['2fa'], - roleName: attributes.role_name, - createdAt: new Date(attributes.created_at), - updatedAt: new Date(attributes.updated_at), -}); diff --git a/resources/scripts/components/admin/servers/ServersContainer.tsx b/resources/scripts/components/admin/servers/ServersContainer.tsx index 350387279..1cd967dba 100644 --- a/resources/scripts/components/admin/servers/ServersContainer.tsx +++ b/resources/scripts/components/admin/servers/ServersContainer.tsx @@ -1,12 +1,68 @@ +import React, { useContext, useEffect, useState } from 'react'; +import getServers, { Context as ServersContext } from '@/api/admin/servers/getServers'; +import AdminCheckbox from '@/components/admin/AdminCheckbox'; +import AdminTable, { ContentWrapper, Loading, NoItems, Pagination, TableBody, TableHead, TableHeader } from '@/components/admin/AdminTable'; import Button from '@/components/elements/Button'; -import React from 'react'; +import CopyOnClick from '@/components/elements/CopyOnClick'; +import FlashMessageRender from '@/components/FlashMessageRender'; +import useFlash from '@/plugins/useFlash'; +import { AdminContext } from '@/state/admin'; +import { NavLink, useRouteMatch } from 'react-router-dom'; import tw from 'twin.macro'; import AdminContentBlock from '@/components/admin/AdminContentBlock'; -export default () => { +const RowCheckbox = ({ id }: { id: number }) => { + const isChecked = AdminContext.useStoreState(state => state.servers.selectedServers.indexOf(id) >= 0); + const appendSelectedServer = AdminContext.useStoreActions(actions => actions.servers.appendSelectedServer); + const removeSelectedServer = AdminContext.useStoreActions(actions => actions.servers.removeSelectedServer); + + return ( + ) => { + if (e.currentTarget.checked) { + appendSelectedServer(id); + } else { + removeSelectedServer(id); + } + }} + /> + ); +}; + +const UsersContainer = () => { + const match = useRouteMatch(); + + const { page, setPage } = useContext(ServersContext); + const { clearFlashes, clearAndAddHttpError } = useFlash(); + const { data: servers, error, isValidating } = getServers(); + + useEffect(() => { + if (!error) { + clearFlashes('servers'); + return; + } + + clearAndAddHttpError({ error, key: 'servers' }); + }, [ error ]); + + const length = servers?.items?.length || 0; + + const setSelectedServers = AdminContext.useStoreActions(actions => actions.servers.setSelectedServers); + const selectedServerLength = AdminContext.useStoreState(state => state.servers.selectedServers.length); + + const onSelectAllClick = (e: React.ChangeEvent) => { + setSelectedServers(e.currentTarget.checked ? (servers?.items?.map(server => server.id) || []) : []); + }; + + useEffect(() => { + setSelectedServers([]); + }, [ page ]); + return ( -
+

Servers

All servers available on the system.

@@ -16,6 +72,118 @@ export default () => { New Server
+ + + + + { servers === undefined || (error && isValidating) ? + + : + length < 1 ? + + : + + +
+ + + + + + + + + + + { + servers.items.map(server => ( + + + + + + + + {/* TODO: Have permission check for displaying user information. */} + + + {/* TODO: Have permission check for displaying node information. */} + + + + + )) + } + +
+ + + + {server.identifier} + + + + {server.name} + + + + +
+ {server.relations.user?.firstName} {server.relations.user?.lastName} +
+ +
+ {server.relations.user?.email} +
+
+
+ +
+ {server.relations.node?.name} +
+ +
+ {server.relations.node?.fqdn}:{server.relations.node?.daemonListen} +
+
+
+ { server.isInstalling ? + + Installing + + : + server.isTransferring ? + + Transferring + + : server.isSuspended ? + + Suspended + + : + + Active + + } +
+
+
+
+ } +
); }; + +export default () => { + const [ page, setPage ] = useState(1); + + return ( + + + + ); +}; diff --git a/resources/scripts/components/admin/users/UsersContainer.tsx b/resources/scripts/components/admin/users/UsersContainer.tsx index 51c2d6b86..2204bcb69 100644 --- a/resources/scripts/components/admin/users/UsersContainer.tsx +++ b/resources/scripts/components/admin/users/UsersContainer.tsx @@ -1,6 +1,6 @@ +import React, { useContext, useEffect, useState } from 'react'; import AdminCheckbox from '@/components/admin/AdminCheckbox'; import CopyOnClick from '@/components/elements/CopyOnClick'; -import React, { useContext, useEffect, useState } from 'react'; import getUsers, { Context as UsersContext } from '@/api/admin/users/getUsers'; import AdminTable, { ContentWrapper, Loading, NoItems, Pagination, TableBody, TableHead, TableHeader } from '@/components/admin/AdminTable'; import Button from '@/components/elements/Button'; @@ -11,7 +11,7 @@ import { NavLink, useRouteMatch } from 'react-router-dom'; import tw from 'twin.macro'; import AdminContentBlock from '@/components/admin/AdminContentBlock'; -const RowCheckbox = ({ id }: { id: number}) => { +const RowCheckbox = ({ id }: { id: number }) => { const isChecked = AdminContext.useStoreState(state => state.users.selectedUsers.indexOf(id) >= 0); const appendSelectedUser = AdminContext.useStoreActions(actions => actions.users.appendSelectedUser); const removeSelectedUser = AdminContext.useStoreActions(actions => actions.users.removeSelectedUser); diff --git a/resources/scripts/state/admin/index.ts b/resources/scripts/state/admin/index.ts index 1a80d3715..0702987b7 100644 --- a/resources/scripts/state/admin/index.ts +++ b/resources/scripts/state/admin/index.ts @@ -3,17 +3,20 @@ import { composeWithDevTools } from 'redux-devtools-extension'; import nests, { AdminNestStore } from '@/state/admin/nests'; import roles, { AdminRoleStore } from '@/state/admin/roles'; +import servers, { AdminServerStore } from '@/state/admin/servers'; import users, { AdminUserStore } from '@/state/admin/users'; interface AdminStore { nests: AdminNestStore; roles: AdminRoleStore; + servers: AdminServerStore; users: AdminUserStore; } export const AdminContext = createContextStore({ nests, roles, + servers, users, }, { compose: composeWithDevTools({ diff --git a/resources/scripts/state/admin/servers.ts b/resources/scripts/state/admin/servers.ts new file mode 100644 index 000000000..8a2c0b3e3 --- /dev/null +++ b/resources/scripts/state/admin/servers.ts @@ -0,0 +1,27 @@ +import { action, Action } from 'easy-peasy'; + +export interface AdminServerStore { + selectedServers: number[]; + + setSelectedServers: Action; + appendSelectedServer: Action; + removeSelectedServer: Action; +} + +const roles: AdminServerStore = { + selectedServers: [], + + setSelectedServers: action((state, payload) => { + state.selectedServers = payload; + }), + + appendSelectedServer: action((state, payload) => { + state.selectedServers = state.selectedServers.filter(id => id !== payload).concat(payload); + }), + + removeSelectedServer: action((state, payload) => { + state.selectedServers = state.selectedServers.filter(id => id !== payload); + }), +}; + +export default roles;