From 11fc88c8494abfd1b1fe4fe47308d6ef935c0e2a Mon Sep 17 00:00:00 2001 From: Matthew Penner Date: Mon, 2 Aug 2021 09:54:13 -0600 Subject: [PATCH] ui(admin): implement user and node servers tab --- .../Application/Servers/ServerController.php | 2 +- .../scripts/api/admin/servers/getServers.ts | 7 +- .../scripts/components/admin/AdminTable.tsx | 4 +- .../components/admin/nodes/NodeRouter.tsx | 3 +- .../components/admin/nodes/NodeServers.tsx | 13 ++ .../admin/servers/ServersContainer.tsx | 186 +---------------- .../components/admin/servers/ServersTable.tsx | 189 ++++++++++++++++++ .../components/admin/users/UserRouter.tsx | 3 +- .../components/admin/users/UserServers.tsx | 13 ++ 9 files changed, 234 insertions(+), 186 deletions(-) create mode 100644 resources/scripts/components/admin/nodes/NodeServers.tsx create mode 100644 resources/scripts/components/admin/servers/ServersTable.tsx create mode 100644 resources/scripts/components/admin/users/UserServers.tsx diff --git a/app/Http/Controllers/Api/Application/Servers/ServerController.php b/app/Http/Controllers/Api/Application/Servers/ServerController.php index 21af4620..da537c9e 100644 --- a/app/Http/Controllers/Api/Application/Servers/ServerController.php +++ b/app/Http/Controllers/Api/Application/Servers/ServerController.php @@ -47,7 +47,7 @@ class ServerController extends ApplicationApiController } $servers = QueryBuilder::for(Server::query()) - ->allowedFilters(['uuid', 'uuidShort', 'name', 'image', 'external_id']) + ->allowedFilters(['id', 'uuid', 'uuidShort', 'name', 'owner_id', 'node_id', 'external_id']) ->allowedSorts(['id', 'uuid', 'uuidShort', 'name', 'owner_id', 'node_id', 'status']) ->paginate($perPage); diff --git a/resources/scripts/api/admin/servers/getServers.ts b/resources/scripts/api/admin/servers/getServers.ts index 4ba207c7..a3c0d955 100644 --- a/resources/scripts/api/admin/servers/getServers.ts +++ b/resources/scripts/api/admin/servers/getServers.ts @@ -1,8 +1,8 @@ -import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg'; -import http, { FractalResponseData, getPaginationSet, PaginatedResult } from '@/api/http'; import { useContext } from 'react'; import useSWR from 'swr'; import { createContext } from '@/api/admin'; +import http, { FractalResponseData, getPaginationSet, PaginatedResult } from '@/api/http'; +import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg'; import { Node, rawDataToNode } from '@/api/admin/nodes/getNodes'; import { User, rawDataToUser } from '@/api/admin/users/getUsers'; @@ -104,8 +104,9 @@ export interface Filters { id?: string; uuid?: string; name?: string; - image?: string; /* eslint-disable camelcase */ + owner_id?: string; + node_id?: string; external_id?: string; /* eslint-enable camelcase */ } diff --git a/resources/scripts/components/admin/AdminTable.tsx b/resources/scripts/components/admin/AdminTable.tsx index 1c820595..232221e6 100644 --- a/resources/scripts/components/admin/AdminTable.tsx +++ b/resources/scripts/components/admin/AdminTable.tsx @@ -8,9 +8,9 @@ import tw, { styled } from 'twin.macro'; import { PaginatedResult, PaginationDataSet } from '@/api/http'; import { ListContext as TableHooks } from '@/api/admin'; -export function useTableHooks (): TableHooks { +export function useTableHooks (initialState?: T | (() => T)): TableHooks { const [ page, setPage ] = useState(1); - const [ filters, setFilters ] = useState(null); + const [ filters, setFilters ] = useState(initialState || null); const [ sort, setSortState ] = useState(null); const [ sortDirection, setSortDirection ] = useState(false); diff --git a/resources/scripts/components/admin/nodes/NodeRouter.tsx b/resources/scripts/components/admin/nodes/NodeRouter.tsx index 0ab65c77..a8b6b471 100644 --- a/resources/scripts/components/admin/nodes/NodeRouter.tsx +++ b/resources/scripts/components/admin/nodes/NodeRouter.tsx @@ -14,6 +14,7 @@ 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'; +import NodeServers from '@/components/admin/nodes/NodeServers'; interface ctx { node: Node | undefined; @@ -123,7 +124,7 @@ const NodeRouter = () => { -

Servers

+
diff --git a/resources/scripts/components/admin/nodes/NodeServers.tsx b/resources/scripts/components/admin/nodes/NodeServers.tsx new file mode 100644 index 00000000..29bea70f --- /dev/null +++ b/resources/scripts/components/admin/nodes/NodeServers.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import ServersTable from '@/components/admin/servers/ServersTable'; +import { Context } from '@/components/admin/nodes/NodeRouter'; + +function NodeServers () { + const node = Context.useStoreState(state => state.node); + + return ( + + ); +} + +export default NodeServers; diff --git a/resources/scripts/components/admin/servers/ServersContainer.tsx b/resources/scripts/components/admin/servers/ServersContainer.tsx index 0ff59428..78f52933 100644 --- a/resources/scripts/components/admin/servers/ServersContainer.tsx +++ b/resources/scripts/components/admin/servers/ServersContainer.tsx @@ -1,76 +1,14 @@ -import React, { useContext, useEffect } from 'react'; -import getServers, { Context as ServersContext, Filters } from '@/api/admin/servers/getServers'; -import AdminCheckbox from '@/components/admin/AdminCheckbox'; -import AdminTable, { TableBody, TableHead, TableHeader, Pagination, Loading, NoItems, ContentWrapper, useTableHooks } from '@/components/admin/AdminTable'; -import Button from '@/components/elements/Button'; -import CopyOnClick from '@/components/elements/CopyOnClick'; -import FlashMessageRender from '@/components/FlashMessageRender'; -import useFlash from '@/plugins/useFlash'; -import { AdminContext } from '@/state/admin'; +import React from 'react'; import { NavLink, useRouteMatch } from 'react-router-dom'; import tw from 'twin.macro'; +import FlashMessageRender from '@/components/FlashMessageRender'; import AdminContentBlock from '@/components/admin/AdminContentBlock'; +import ServersTable from '@/components/admin/servers/ServersTable'; +import Button from '@/components/elements/Button'; -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 ServersContainer = () => { +function ServersContainer () { const match = useRouteMatch(); - const { page, setPage, setFilters, sort, setSort, sortDirection } = useContext(ServersContext); - const { clearFlashes, clearAndAddHttpError } = useFlash(); - const { data: servers, error, isValidating } = getServers([ 'node', 'user' ]); - - 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) || []) : []); - }; - - const onSearch = (query: string): Promise => { - return new Promise((resolve) => { - if (query.length < 2) { - setFilters(null); - } else { - setFilters({ name: query }); - } - return resolve(); - }); - }; - - useEffect(() => { - setSelectedServers([]); - }, [ page ]); - - useEffect(() => { - if (!error) { - clearFlashes('servers'); - return; - } - - clearAndAddHttpError({ key: 'servers', error }); - }, [ error ]); - return (
@@ -90,117 +28,9 @@ const ServersContainer = () => { - - - -
- - - setSort('uuidShort')}/> - setSort('name')}/> - setSort('owner_id')}/> - setSort('node_id')}/> - setSort('status')}/> - - - - { servers !== undefined && !error && !isValidating && length > 0 && - servers.items.map(server => ( - - - - - - - - {/* TODO: Have permission check for displaying user information. */} - - - {/* TODO: Have permission check for displaying node information. */} - - - - - )) - } - -
- - - - {server.identifier} - - - - {server.name} - - - -
- Silly User -
- -
- {server.relations.user?.email} -
-
-
- -
- {server.relations.node?.name} -
- -
- {server.relations.node?.fqdn} -
-
-
- {server.status === 'installing' ? - - Installing - - : - server.status === 'transferring' ? - - Transferring - - : server.status === 'suspended' ? - - Suspended - - : - - Active - - } -
- - { servers === undefined || (error && isValidating) ? - - : - length < 1 ? - - : - null - } -
-
-
-
+ ); -}; +} -export default () => { - const hooks = useTableHooks(); - - return ( - - - - ); -}; +export default ServersContainer; diff --git a/resources/scripts/components/admin/servers/ServersTable.tsx b/resources/scripts/components/admin/servers/ServersTable.tsx new file mode 100644 index 00000000..da395c22 --- /dev/null +++ b/resources/scripts/components/admin/servers/ServersTable.tsx @@ -0,0 +1,189 @@ +import React, { useContext, useEffect } from 'react'; +import { NavLink, useRouteMatch } from 'react-router-dom'; +import tw from 'twin.macro'; +import getServers, { Context as ServersContext, Filters } from '@/api/admin/servers/getServers'; +import AdminCheckbox from '@/components/admin/AdminCheckbox'; +import AdminTable, { ContentWrapper, Loading, NoItems, Pagination, TableBody, TableHead, TableHeader, useTableHooks } from '@/components/admin/AdminTable'; +import CopyOnClick from '@/components/elements/CopyOnClick'; +import { AdminContext } from '@/state/admin'; +import useFlash from '@/plugins/useFlash'; + +function 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); + } + }} + /> + ); +} + +interface Props { + filters?: Filters; +} + +function ServersTable ({ filters }: Props) { + const match = useRouteMatch(); + + const { clearFlashes, clearAndAddHttpError } = useFlash(); + + const { page, setPage, setFilters, sort, setSort, sortDirection } = useContext(ServersContext); + const { data: servers, error, isValidating } = getServers([ 'node', 'user' ]); + + 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) || []) : []); + }; + + const onSearch = (query: string): Promise => { + return new Promise((resolve) => { + if (query.length < 2) { + setFilters(filters || null); + } else { + setFilters({ ...filters, name: query }); + } + return resolve(); + }); + }; + + useEffect(() => { + setSelectedServers([]); + }, [ page ]); + + useEffect(() => { + if (!error) { + clearFlashes('servers'); + return; + } + + clearAndAddHttpError({ key: 'servers', error }); + }, [ error ]); + + return ( + + + +
+ + + setSort('uuidShort')}/> + setSort('name')}/> + setSort('owner_id')}/> + setSort('node_id')}/> + setSort('status')}/> + + + + { servers !== undefined && !error && !isValidating && length > 0 && + servers.items.map(server => ( + + + + + + + + {/* TODO: Have permission check for displaying user information. */} + + + {/* TODO: Have permission check for displaying node information. */} + + + + + )) + } + +
+ + + + {server.identifier} + + + + {server.name} + + + +
+ Silly User +
+ +
+ {server.relations.user?.email} +
+
+
+ +
+ {server.relations.node?.name} +
+ +
+ {server.relations.node?.fqdn} +
+
+
+ {server.status === 'installing' ? + + Installing + + : + server.status === 'transferring' ? + + Transferring + + : server.status === 'suspended' ? + + Suspended + + : + + Active + + } +
+ + { servers === undefined || (error && isValidating) ? + + : + length < 1 ? + + : + null + } +
+
+
+
+ ); +} + +export default ({ filters }: Props) => { + const hooks = useTableHooks(filters); + + return ( + + + + ); +}; diff --git a/resources/scripts/components/admin/users/UserRouter.tsx b/resources/scripts/components/admin/users/UserRouter.tsx index 00c5d857..af04b56e 100644 --- a/resources/scripts/components/admin/users/UserRouter.tsx +++ b/resources/scripts/components/admin/users/UserRouter.tsx @@ -10,6 +10,7 @@ import Spinner from '@/components/elements/Spinner'; import FlashMessageRender from '@/components/FlashMessageRender'; import { ApplicationStore } from '@/state'; import { SubNavigation, SubNavigationLink } from '@/components/admin/SubNavigation'; +import UserServers from '@/components/admin/users/UserServers'; interface ctx { user: User | undefined; @@ -89,7 +90,7 @@ const UserRouter = () => { -

Servers

+
diff --git a/resources/scripts/components/admin/users/UserServers.tsx b/resources/scripts/components/admin/users/UserServers.tsx new file mode 100644 index 00000000..6bf8afab --- /dev/null +++ b/resources/scripts/components/admin/users/UserServers.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import ServersTable from '@/components/admin/servers/ServersTable'; +import { Context } from '@/components/admin/users/UserRouter'; + +function UserServers () { + const user = Context.useStoreState(state => state.user); + + return ( + + ); +} + +export default UserServers;