From 3971c4499d58d422e6cecabb0ee0def265d43bd9 Mon Sep 17 00:00:00 2001 From: Matthew Penner Date: Mon, 15 Feb 2021 18:48:10 -0700 Subject: [PATCH] admin(ui): fix up SearchableSelect.tsx --- .../Api/Application/NodeTransformer.php | 63 ++++++++++++++- resources/scripts/api/admin/nodes/getNodes.ts | 5 ++ .../components/admin/nodes/DatabaseSelect.tsx | 46 +++-------- .../components/admin/nodes/LocationSelect.tsx | 10 +-- .../admin/nodes/NodeEditContainer.tsx | 2 +- .../admin/nodes/NodeSettingsContainer.tsx | 9 ++- .../components/elements/SearchableSelect.tsx | 80 ++++++++++++++++--- 7 files changed, 157 insertions(+), 58 deletions(-) diff --git a/app/Transformers/Api/Application/NodeTransformer.php b/app/Transformers/Api/Application/NodeTransformer.php index f6f2a2cd1..d7de33ce6 100644 --- a/app/Transformers/Api/Application/NodeTransformer.php +++ b/app/Transformers/Api/Application/NodeTransformer.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Transformers\Api\Application; use Pterodactyl\Models\Node; +use League\Fractal\Resource\NullResource; use Pterodactyl\Services\Acl\Api\AdminAcl; class NodeTransformer extends BaseTransformer @@ -12,7 +13,7 @@ class NodeTransformer extends BaseTransformer * * @var array */ - protected $availableIncludes = ['allocations', 'location', 'servers']; + protected $availableIncludes = ['allocations', 'database_host', 'location', 'mounts', 'servers']; /** * Return the resource name for the JSONAPI output. @@ -44,10 +45,11 @@ class NodeTransformer extends BaseTransformer } /** - * Return the nodes associated with this location. + * Return the allocations associated with this node. * * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource * + * @throws \Illuminate\Contracts\Container\BindingResolutionException * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeAllocations(Node $node) @@ -66,10 +68,39 @@ class NodeTransformer extends BaseTransformer } /** - * Return the nodes associated with this location. + * Return the database host associated with this node. * * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource * + * @throws \Illuminate\Contracts\Container\BindingResolutionException + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException + */ + public function includeDatabaseHost(Node $node) + { + if (!$this->authorize(AdminAcl::RESOURCE_DATABASE_HOSTS)) { + return $this->null(); + } + + $node->loadMissing('databaseHost'); + + $databaseHost = $node->getRelation('databaseHost'); + if (is_null($databaseHost)) { + return new NullResource(); + } + + return $this->item( + $node->getRelation('databaseHost'), + $this->makeTransformer(DatabaseHostTransformer::class), + 'databaseHost' + ); + } + + /** + * Return the location associated with this node. + * + * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + * + * @throws \Illuminate\Contracts\Container\BindingResolutionException * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeLocation(Node $node) @@ -88,10 +119,34 @@ class NodeTransformer extends BaseTransformer } /** - * Return the nodes associated with this location. + * Return the mounts associated with this node. * * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource * + * @throws \Illuminate\Contracts\Container\BindingResolutionException + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException + */ + public function includeMounts(Node $node) + { + if (!$this->authorize(AdminAcl::RESOURCE_MOUNTS)) { + return $this->null(); + } + + $node->loadMissing('mounts'); + + return $this->collection( + $node->getRelation('mounts'), + $this->makeTransformer(MountTransformer::class), + 'mount' + ); + } + + /** + * Return the servers associated with this node. + * + * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource + * + * @throws \Illuminate\Contracts\Container\BindingResolutionException * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeServers(Node $node) diff --git a/resources/scripts/api/admin/nodes/getNodes.ts b/resources/scripts/api/admin/nodes/getNodes.ts index d70224673..99a2fd6ec 100644 --- a/resources/scripts/api/admin/nodes/getNodes.ts +++ b/resources/scripts/api/admin/nodes/getNodes.ts @@ -1,6 +1,7 @@ import http, { FractalResponseData, getPaginationSet, PaginatedResult } from '@/api/http'; import { createContext, useContext } from 'react'; import useSWR from 'swr'; +import { Database, rawDataToDatabase } from '@/api/admin/databases/getDatabases'; import { Location, rawDataToLocation } from '@/api/admin/locations/getLocations'; export interface Node { @@ -10,6 +11,7 @@ export interface Node { name: string; description: string | null; locationId: number; + databaseHostId: number | null; fqdn: string; listenPortHTTP: number; publicPortHTTP: number; @@ -28,6 +30,7 @@ export interface Node { updatedAt: Date; relations: { + databaseHost: Database | undefined; location: Location | undefined; }; } @@ -39,6 +42,7 @@ export const rawDataToNode = ({ attributes }: FractalResponseData): Node => ({ name: attributes.name, description: attributes.description, locationId: attributes.location_id, + databaseHostId: attributes.database_host_id, fqdn: attributes.fqdn, listenPortHTTP: attributes.listen_port_http, publicPortHTTP: attributes.public_port_http, @@ -57,6 +61,7 @@ export const rawDataToNode = ({ attributes }: FractalResponseData): Node => ({ updatedAt: new Date(attributes.updated_at), relations: { + databaseHost: attributes.relationships?.databaseHost !== undefined ? rawDataToDatabase(attributes.relationships.databaseHost as FractalResponseData) : undefined, location: attributes.relationships?.location !== undefined ? rawDataToLocation(attributes.relationships.location as FractalResponseData) : undefined, }, }); diff --git a/resources/scripts/components/admin/nodes/DatabaseSelect.tsx b/resources/scripts/components/admin/nodes/DatabaseSelect.tsx index 30f7212de..fca37ad87 100644 --- a/resources/scripts/components/admin/nodes/DatabaseSelect.tsx +++ b/resources/scripts/components/admin/nodes/DatabaseSelect.tsx @@ -1,11 +1,10 @@ import React, { useState } from 'react'; -import SearchableSelect from '@/components/elements/SearchableSelect'; +import SearchableSelect, { Option } from '@/components/elements/SearchableSelect'; import searchDatabases from '@/api/admin/databases/searchDatabases'; import { Database } from '@/api/admin/databases/getDatabases'; -import tw from 'twin.macro'; -export default () => { - const [ database, setDatabase ] = useState(null); +export default ({ selected }: { selected?: Database | null }) => { + const [ database, setDatabase ] = useState(selected || null); const [ databases, setDatabases ] = useState([]); const onSearch = (query: string): Promise => { @@ -21,47 +20,26 @@ export default () => { setDatabase(database); }; + const getSelectedText = (database: Database | null): string => { + return database?.name || ''; + }; + return ( {databases.map(d => ( - d.id === database?.id ? -
  • { - e.stopPropagation(); - // selectItem(d); - }} - > -
    - - {d.name} - -
    - - - - -
  • - : -
  • { - e.stopPropagation(); - // selectItem(d); - }} - > -
    - - {d.name} - -
    -
  • + ))}
    ); diff --git a/resources/scripts/components/admin/nodes/LocationSelect.tsx b/resources/scripts/components/admin/nodes/LocationSelect.tsx index c2835016f..8a17abc0c 100644 --- a/resources/scripts/components/admin/nodes/LocationSelect.tsx +++ b/resources/scripts/components/admin/nodes/LocationSelect.tsx @@ -13,10 +13,10 @@ const Dropdown = styled.div<{ expanded: boolean }>` ${props => !props.expanded && tw`hidden`}; `; -export default ({ defaultLocation }: { defaultLocation: Location }) => { +export default ({ defaultLocation }: { defaultLocation: Location | null }) => { const [ loading, setLoading ] = useState(false); const [ expanded, setExpanded ] = useState(false); - const [ location, setLocation ] = useState(defaultLocation); + const [ location, setLocation ] = useState(defaultLocation); const [ locations, setLocations ] = useState([]); const [ inputText, setInputText ] = useState(''); @@ -48,7 +48,7 @@ export default ({ defaultLocation }: { defaultLocation: Location }) => { }; useEffect(() => { - setInputText(location.short); + setInputText(location?.short || ''); setExpanded(false); }, [ location ]); @@ -58,7 +58,7 @@ export default ({ defaultLocation }: { defaultLocation: Location }) => { return; } - setInputText(location.short); + setInputText(location?.short || ''); setExpanded(false); }; @@ -100,7 +100,7 @@ export default ({ defaultLocation }: { defaultLocation: Location }) => { :
      {locations.map(l => ( - l.id === location.id ? + l.id === location?.id ?
    • { e.stopPropagation(); selectLocation(l); diff --git a/resources/scripts/components/admin/nodes/NodeEditContainer.tsx b/resources/scripts/components/admin/nodes/NodeEditContainer.tsx index 581b25820..b2a2857a6 100644 --- a/resources/scripts/components/admin/nodes/NodeEditContainer.tsx +++ b/resources/scripts/components/admin/nodes/NodeEditContainer.tsx @@ -48,7 +48,7 @@ const NodeEditContainer = () => { useEffect(() => { clearFlashes('node'); - getNode(Number(match.params?.id)) + getNode(Number(match.params?.id), [ 'database_host', 'location' ]) .then(node => setNode(node)) .catch(error => { console.error(error); diff --git a/resources/scripts/components/admin/nodes/NodeSettingsContainer.tsx b/resources/scripts/components/admin/nodes/NodeSettingsContainer.tsx index 3b26ffa7b..90c8cf87b 100644 --- a/resources/scripts/components/admin/nodes/NodeSettingsContainer.tsx +++ b/resources/scripts/components/admin/nodes/NodeSettingsContainer.tsx @@ -1,3 +1,4 @@ +import DatabaseSelect from '@/components/admin/nodes/DatabaseSelect'; import React from 'react'; import AdminBox from '@/components/admin/AdminBox'; import tw from 'twin.macro'; @@ -17,6 +18,7 @@ interface Values { name: string; description: string; locationId: number; + databaseHostId: number | null; fqdn: string; listenPortHTTP: number; publicPortHTTP: number; @@ -57,6 +59,7 @@ export default () => { name: node.name, description: node.description || '', locationId: node.locationId, + databaseHostId: node.databaseHostId, fqdn: node.fqdn, listenPortHTTP: node.listenPortHTTP, publicPortHTTP: node.publicPortHTTP, @@ -95,7 +98,11 @@ export default () => {
      - + +
      + +
      +
      diff --git a/resources/scripts/components/elements/SearchableSelect.tsx b/resources/scripts/components/elements/SearchableSelect.tsx index 5cbc87414..915fabda2 100644 --- a/resources/scripts/components/elements/SearchableSelect.tsx +++ b/resources/scripts/components/elements/SearchableSelect.tsx @@ -1,31 +1,35 @@ -import React, { useEffect, useState } from 'react'; +import React, { ReactElement, useEffect, useState } from 'react'; +import { debounce } from 'debounce'; import styled from 'styled-components/macro'; import tw from 'twin.macro'; import Input from '@/components/elements/Input'; import Label from '@/components/elements/Label'; import InputSpinner from '@/components/elements/InputSpinner'; -import { debounce } from 'debounce'; const Dropdown = styled.div<{ expanded: boolean }>` ${tw`absolute mt-1 w-full rounded-md bg-neutral-900 shadow-lg z-10`}; ${props => !props.expanded && tw`hidden`}; `; -interface Props { +interface SearchableSelectProps { id: string; name: string; nullable: boolean; + selected: T | null; + items: T[]; setItems: (items: T[]) => void; onSearch: (query: string) => Promise; onSelect: (item: T) => void; + getSelectedText: (item: T | null) => string; + children: React.ReactNode; } -function SearchableSelect ({ id, name, items, setItems, onSearch, children }: Props) { +function SearchableSelect ({ id, name, selected, items, setItems, onSearch, onSelect, getSelectedText, children }: SearchableSelectProps) { const [ loading, setLoading ] = useState(false); const [ expanded, setExpanded ] = useState(false); @@ -51,15 +55,10 @@ function SearchableSelect ({ id, name, items, setItems, onSearch, children }: onSearch(query).then(() => setLoading(false)); }, 250); - /* const selectItem = (item: any) => { - onSelect(item); - }; */ - useEffect(() => { - // setInputText(location.short); + setInputText(getSelectedText(selected) || ''); setExpanded(false); - }, [ ]); - // }, [ location ]); + }, [ selected ]); useEffect(() => { const handler = (e: KeyboardEvent) => { @@ -67,7 +66,7 @@ function SearchableSelect ({ id, name, items, setItems, onSearch, children }: return; } - // setInputText(location.short); + setInputText(getSelectedText(selected) || ''); setExpanded(false); }; @@ -77,6 +76,16 @@ function SearchableSelect ({ id, name, items, setItems, onSearch, children }: }; }, [ expanded ]); + const onClick = (item: T) => (e: React.MouseEvent) => { + e.preventDefault(); + onSelect(item); + }; + + // This shit is really stupid but works, so is it really stupid? + const c = React.Children.map(children, child => React.cloneElement(child as ReactElement, { + onClick: onClick.bind(child), + })); + return (
      @@ -108,7 +117,7 @@ function SearchableSelect ({ id, name, items, setItems, onSearch, children }:
      :
        - {children} + {c}
      } @@ -117,4 +126,49 @@ function SearchableSelect ({ id, name, items, setItems, onSearch, children }: ); } +interface OptionProps { + id: string | number; + item: T; + active: boolean; + + onClick?: (item: T) => (e: React.MouseEvent) => void; + + children: React.ReactNode; +} + +export function Option ({ id, item, active, onClick, children }: OptionProps) { + if (onClick === undefined) { + // eslint-disable-next-line @typescript-eslint/no-empty-function + onClick = () => () => {}; + } + + if (active) { + return ( +
    • +
      + + {children} + +
      + + + + +
    • + ); + } + + return ( +
    • +
      + + {children} + +
      +
    • + ); +} + export default SearchableSelect;