From 00d0f49ededc054bce69a7508b9f5ac00b24c7bd Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 9 Oct 2021 12:02:32 -0700 Subject: [PATCH 1/5] Cleanup typing for server and expose more useful endpoint and transformer logic --- .eslintrc.yml | 4 + resources/scripts/api/admin/index.ts | 25 +++++ resources/scripts/api/admin/server.ts | 95 +++++++++++++++++++ .../scripts/api/admin/servers/getServers.ts | 2 +- resources/scripts/api/admin/transformers.ts | 56 +++++++++++ .../scripts/api/swr/admin/getServerDetails.ts | 19 ---- .../components/admin/servers/ServerRouter.tsx | 4 +- .../admin/servers/ServerSettingsContainer.tsx | 75 +++------------ .../servers/settings/BaseSettingsBox.tsx | 6 +- .../servers/settings/FeatureLimitsBox.tsx | 4 - .../admin/servers/settings/NetworkingBox.tsx | 57 +++++++++++ 11 files changed, 257 insertions(+), 90 deletions(-) create mode 100644 resources/scripts/api/admin/server.ts create mode 100644 resources/scripts/api/admin/transformers.ts delete mode 100644 resources/scripts/api/swr/admin/getServerDetails.ts create mode 100644 resources/scripts/components/admin/servers/settings/NetworkingBox.tsx diff --git a/.eslintrc.yml b/.eslintrc.yml index a4630dcb8..1c3adcb14 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -43,6 +43,10 @@ rules: array-bracket-spacing: - warn - always + "@typescript-eslint/no-unused-vars": + - warn + - argsIgnorePattern: '^_' + varsIgnorePattern: '^_' # Remove errors for not having newlines between operands of ternary expressions https://eslint.org/docs/rules/multiline-ternary multiline-ternary: 0 "react-hooks/rules-of-hooks": diff --git a/resources/scripts/api/admin/index.ts b/resources/scripts/api/admin/index.ts index 54161f2b0..6d57288c5 100644 --- a/resources/scripts/api/admin/index.ts +++ b/resources/scripts/api/admin/index.ts @@ -1,5 +1,30 @@ import { createContext } from 'react'; +export interface Model { + relationships: Record; +} + +export type UUID = string; + +/** + * Marks the provided relationships keys as present in the given model + * rather than being optional to improve typing responses. + */ +export type WithRelationships = Omit & { + relationships: Omit & { + [K in R]: NonNullable; + } +} + +/** + * Helper function that just returns the model you pass in, but types the model + * such that TypeScript understands the relationships on it. This is just to help + * reduce the amount of duplicated type casting all over the codebase. + */ +export const withRelationships = (model: M, ..._keys: R[]) => { + return model as unknown as WithRelationships; +}; + export interface ListContext { page: number; setPage: (page: ((p: number) => number) | number) => void; diff --git a/resources/scripts/api/admin/server.ts b/resources/scripts/api/admin/server.ts new file mode 100644 index 000000000..fd45aa0bb --- /dev/null +++ b/resources/scripts/api/admin/server.ts @@ -0,0 +1,95 @@ +import { Allocation } from '@/api/admin/nodes/getAllocations'; +import { Egg } from '@/api/admin/eggs/getEgg'; +import { User } from '@/api/admin/users/getUsers'; +import { Node } from '@/api/admin/nodes/getNodes'; +import { rawDataToServer, ServerVariable } from '@/api/admin/servers/getServers'; +import useSWR, { SWRResponse } from 'swr'; +import { AxiosError } from 'axios'; +import { useRouteMatch } from 'react-router-dom'; +import http from '@/api/http'; +import { Model, UUID, withRelationships, WithRelationships } from '@/api/admin/index'; +import { AdminTransformers } from '@/api/admin/transformers'; + +/** + * Defines the limits for a server that exists on the Panel. + */ +interface ServerLimits { + memory: number; + swap: number; + disk: number; + io: number; + cpu: number; + threads: string | null; + oomDisabled: boolean; +} + +/** + * Defines a single server instance that is returned from the Panel's admin + * API endpoints. + */ +export interface Server extends Model { + id: number; + uuid: UUID; + externalId: string | null; + identifier: string; + name: string; + description: string; + status: string; + userId: number; + nodeId: number; + allocationId: number; + eggId: number; + limits: ServerLimits; + featureLimits: { + databases: number; + allocations: number; + backups: number; + }; + container: { + startup: string; + image: string; + environment: Record; + }; + createdAt: Date; + updatedAt: Date; + relationships: { + allocations?: Allocation[]; + egg?: Egg; + node?: Node; + user?: User; + variables?: ServerVariable[]; + }; +} + +/** + * A standard API response with the minimum viable details for the frontend + * to correctly render a server. + */ +type LoadedServer = WithRelationships; + +/** + * Fetches a server from the API and ensures that the allocations, user, and + * node data is loaded. + */ +export const getServer = async (id: number | string): Promise => { + const { data } = await http.get(`/api/application/servers/${id}`, { + params: { + includes: [ 'allocations', 'user', 'node' ], + }, + }); + + return withRelationships(AdminTransformers.toServer(data), 'allocations', 'user', 'node'); +}; + +/** + * Returns an SWR instance by automatically loading in the server for the currently + * loaded route match in the admin area. + */ +export const useServerFromRoute = (): SWRResponse => { + const { params } = useRouteMatch<{ id: string }>(); + + return useSWR(`/api/application/servers/${params.id}`, async () => getServer(params.id), { + revalidateOnMount: false, + revalidateOnFocus: false, + }); +}; diff --git a/resources/scripts/api/admin/servers/getServers.ts b/resources/scripts/api/admin/servers/getServers.ts index 8f40a30f2..e34e0a566 100644 --- a/resources/scripts/api/admin/servers/getServers.ts +++ b/resources/scripts/api/admin/servers/getServers.ts @@ -23,7 +23,7 @@ export interface ServerVariable { updatedAt: Date; } -const rawDataToServerVariable = ({ attributes }: FractalResponseData): ServerVariable => ({ +export const rawDataToServerVariable = ({ attributes }: FractalResponseData): ServerVariable => ({ id: attributes.id, eggId: attributes.egg_id, name: attributes.name, diff --git a/resources/scripts/api/admin/transformers.ts b/resources/scripts/api/admin/transformers.ts new file mode 100644 index 000000000..d3d21a1ab --- /dev/null +++ b/resources/scripts/api/admin/transformers.ts @@ -0,0 +1,56 @@ +/* eslint-disable camelcase */ +import { Server } from '@/api/admin/server'; +import { FractalResponseData, FractalResponseList } from '@/api/http'; +import { rawDataToAllocation } from '@/api/admin/nodes/getAllocations'; +import { rawDataToEgg } from '@/api/admin/eggs/getEgg'; +import { rawDataToNode } from '@/api/admin/nodes/getNodes'; +import { rawDataToUser } from '@/api/admin/users/getUsers'; +import { rawDataToServerVariable } from '@/api/admin/servers/getServers'; + +const isList = (data: FractalResponseList | FractalResponseData): data is FractalResponseList => data.object === 'list'; + +function transform (data: undefined, transformer: (callback: FractalResponseData) => T, missing?: M): undefined; +function transform (data: FractalResponseData | undefined, transformer: (callback: FractalResponseData) => T, missing?: M): T | M | undefined; +function transform (data: FractalResponseList | undefined, transformer: (callback: FractalResponseData) => T, missing?: M): T[] | undefined; +function transform (data: FractalResponseData | FractalResponseList | undefined, transformer: (callback: FractalResponseData) => T, missing = undefined) { + if (data === undefined) return undefined; + + if (isList(data)) { + return data.data.map(transformer); + } + + return !data ? missing : transformer(data); +} + +export class AdminTransformers { + static toServer = ({ attributes }: FractalResponseData): Server => { + const { oom_disabled, ...limits } = attributes.limits; + const { allocations, egg, node, user, variables } = attributes.relationships || {}; + + return { + id: attributes.id, + uuid: attributes.uuid, + externalId: attributes.external_id, + identifier: attributes.identifier, + name: attributes.name, + description: attributes.description, + status: attributes.status, + userId: attributes.owner_id, + nodeId: attributes.node_id, + allocationId: attributes.allocation_id, + eggId: attributes.egg_id, + limits: { ...limits, oomDisabled: oom_disabled }, + featureLimits: attributes.feature_limits, + container: attributes.container, + createdAt: new Date(attributes.created_at), + updatedAt: new Date(attributes.updated_at), + relationships: { + allocations: transform(allocations as FractalResponseList | undefined, rawDataToAllocation), + egg: transform(egg as FractalResponseData | undefined, rawDataToEgg), + node: transform(node as FractalResponseData | undefined, rawDataToNode), + user: transform(user as FractalResponseData | undefined, rawDataToUser), + variables: transform(variables as FractalResponseList | undefined, rawDataToServerVariable), + }, + }; + }; +} diff --git a/resources/scripts/api/swr/admin/getServerDetails.ts b/resources/scripts/api/swr/admin/getServerDetails.ts deleted file mode 100644 index ce88110f3..000000000 --- a/resources/scripts/api/swr/admin/getServerDetails.ts +++ /dev/null @@ -1,19 +0,0 @@ -import useSWR, { SWRResponse } from 'swr'; -import http from '@/api/http'; -import { rawDataToServer, Server } from '@/api/admin/servers/getServers'; -import { useRouteMatch } from 'react-router-dom'; -import { AxiosError } from 'axios'; - -export default (): SWRResponse => { - const { params } = useRouteMatch<{ id: string }>(); - - return useSWR(`/api/application/servers/${params.id}`, async (key) => { - const { data } = await http.get(key, { - params: { - includes: [ 'allocations', 'user', 'variables' ], - }, - }); - - return rawDataToServer(data); - }, { revalidateOnMount: false, revalidateOnFocus: false }); -}; diff --git a/resources/scripts/components/admin/servers/ServerRouter.tsx b/resources/scripts/components/admin/servers/ServerRouter.tsx index 0733f1536..2e08bc4eb 100644 --- a/resources/scripts/components/admin/servers/ServerRouter.tsx +++ b/resources/scripts/components/admin/servers/ServerRouter.tsx @@ -11,8 +11,8 @@ import Spinner from '@/components/elements/Spinner'; import FlashMessageRender from '@/components/FlashMessageRender'; import { SubNavigation, SubNavigationLink } from '@/components/admin/SubNavigation'; import ServerSettingsContainer from '@/components/admin/servers/ServerSettingsContainer'; -import getServerDetails from '@/api/swr/admin/getServerDetails'; import useFlash from '@/plugins/useFlash'; +import { useServerFromRoute } from '@/api/admin/server'; export const ServerIncludes = [ 'allocations', 'user', 'variables' ]; @@ -34,7 +34,7 @@ const ServerRouter = () => { const match = useRouteMatch<{ id?: string }>(); const { clearFlashes, clearAndAddHttpError } = useFlash(); - const { data: server, error, isValidating, mutate } = getServerDetails(); + const { data: server, error, isValidating, mutate } = useServerFromRoute(); useEffect(() => { mutate(); diff --git a/resources/scripts/components/admin/servers/ServerSettingsContainer.tsx b/resources/scripts/components/admin/servers/ServerSettingsContainer.tsx index b3a25ee40..38f59693a 100644 --- a/resources/scripts/components/admin/servers/ServerSettingsContainer.tsx +++ b/resources/scripts/components/admin/servers/ServerSettingsContainer.tsx @@ -1,10 +1,6 @@ -import getAllocations from '@/api/admin/nodes/getAllocations'; import { Server } from '@/api/admin/servers/getServers'; import ServerDeleteButton from '@/components/admin/servers/ServerDeleteButton'; -import Label from '@/components/elements/Label'; -import Select from '@/components/elements/Select'; -import SelectField, { AsyncSelectField, Option } from '@/components/elements/SelectField'; -import { faBalanceScale, faConciergeBell, faNetworkWired } from '@fortawesome/free-solid-svg-icons'; +import { faBalanceScale } from '@fortawesome/free-solid-svg-icons'; import React from 'react'; import AdminBox from '@/components/admin/AdminBox'; import { useHistory } from 'react-router-dom'; @@ -21,56 +17,7 @@ import Button from '@/components/elements/Button'; import FormikSwitch from '@/components/elements/FormikSwitch'; import BaseSettingsBox from '@/components/admin/servers/settings/BaseSettingsBox'; import FeatureLimitsBox from '@/components/admin/servers/settings/FeatureLimitsBox'; - -export function ServerAllocationsContainer ({ server }: { server: Server }) { - const { isSubmitting } = useFormikContext(); - - const loadOptions = async (inputValue: string, callback: (options: Option[]) => void) => { - const allocations = await getAllocations(server.nodeId, { ip: inputValue, server_id: '0' }); - callback(allocations.map(a => { - return { value: a.id.toString(), label: a.getDisplayText() }; - })); - }; - - return ( - - - -
- - -
- - - - { - return { value: a.id.toString(), label: a.getDisplayText() }; - }) || []} - isMulti - isSearchable - css={tw`mb-2`} - /> -
- ); -} +import NetworkingBox from '@/components/admin/servers/settings/NetworkingBox'; export function ServerResourceContainer () { const { isSubmitting } = useFormikContext(); @@ -160,7 +107,10 @@ export function ServerResourceContainer () { export default function ServerSettingsContainer2 ({ server }: { server: Server }) { const history = useHistory(); - const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions) => actions.flashes); + const { + clearFlashes, + clearAndAddHttpError, + } = useStoreActions((actions: Actions) => actions.flashes); const setServer = Context.useStoreActions(actions => actions.setServer); @@ -216,8 +166,7 @@ export default function ServerSettingsContainer2 ({ server }: { server: Server } addAllocations: [] as number[], removeAllocations: [] as number[], }} - validationSchema={object().shape({ - })} + validationSchema={object().shape({})} > {({ isSubmitting, isValid }) => (
@@ -225,9 +174,8 @@ export default function ServerSettingsContainer2 ({ server }: { server: Server }
- +
-
@@ -237,7 +185,12 @@ export default function ServerSettingsContainer2 ({ server }: { server: Server } serverId={server?.id} onDeleted={() => history.push('/admin/servers')} /> -
diff --git a/resources/scripts/components/admin/servers/settings/BaseSettingsBox.tsx b/resources/scripts/components/admin/servers/settings/BaseSettingsBox.tsx index edca72b63..898bd62da 100644 --- a/resources/scripts/components/admin/servers/settings/BaseSettingsBox.tsx +++ b/resources/scripts/components/admin/servers/settings/BaseSettingsBox.tsx @@ -5,10 +5,10 @@ import AdminBox from '@/components/admin/AdminBox'; import { faCogs } from '@fortawesome/free-solid-svg-icons'; import Field from '@/components/elements/Field'; import OwnerSelect from '@/components/admin/servers/OwnerSelect'; -import getServerDetails from '@/api/swr/admin/getServerDetails'; +import { useServerFromRoute } from '@/api/admin/server'; export default () => { - const { data: server } = getServerDetails(); + const { data: server } = useServerFromRoute(); const { isSubmitting } = useFormikContext(); if (!server) return null; @@ -18,7 +18,7 @@ export default () => {
- +
); diff --git a/resources/scripts/components/admin/servers/settings/FeatureLimitsBox.tsx b/resources/scripts/components/admin/servers/settings/FeatureLimitsBox.tsx index d6597dd13..ea312fb81 100644 --- a/resources/scripts/components/admin/servers/settings/FeatureLimitsBox.tsx +++ b/resources/scripts/components/admin/servers/settings/FeatureLimitsBox.tsx @@ -4,14 +4,10 @@ import AdminBox from '@/components/admin/AdminBox'; import { faConciergeBell } from '@fortawesome/free-solid-svg-icons'; import tw from 'twin.macro'; import Field from '@/components/elements/Field'; -import getServerDetails from '@/api/swr/admin/getServerDetails'; export default () => { - const { data: server } = getServerDetails(); const { isSubmitting } = useFormikContext(); - if (!server) return null; - return (
diff --git a/resources/scripts/components/admin/servers/settings/NetworkingBox.tsx b/resources/scripts/components/admin/servers/settings/NetworkingBox.tsx new file mode 100644 index 000000000..864d8fa9b --- /dev/null +++ b/resources/scripts/components/admin/servers/settings/NetworkingBox.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { useFormikContext } from 'formik'; +import SelectField, { AsyncSelectField, Option } from '@/components/elements/SelectField'; +import getAllocations from '@/api/admin/nodes/getAllocations'; +import AdminBox from '@/components/admin/AdminBox'; +import { faNetworkWired } from '@fortawesome/free-solid-svg-icons'; +import tw from 'twin.macro'; +import Label from '@/components/elements/Label'; +import Select from '@/components/elements/Select'; +import { useServerFromRoute } from '@/api/admin/server'; + +export default () => { + const { isSubmitting } = useFormikContext(); + const { data: server } = useServerFromRoute(); + + if (!server) return null; + + const loadOptions = async (inputValue: string, callback: (options: Option[]) => void) => { + const allocations = await getAllocations(server.nodeId, { ip: inputValue, server_id: '0' }); + + callback(allocations.map(a => { + return { value: a.id.toString(), label: a.getDisplayText() }; + })); + }; + + return ( + +
+
+ + +
+ + { + return { value: a.id.toString(), label: a.getDisplayText() }; + }) || []} + isMulti + isSearchable + /> +
+
+ ); +}; From e3aca937b574423075b231dddf758784d79b688a Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 10 Oct 2021 11:32:07 -0700 Subject: [PATCH 2/5] Add more type cleanup and have a completed server type --- resources/scripts/api/admin/egg.ts | 24 ++++ resources/scripts/api/admin/location.ts | 13 ++ resources/scripts/api/admin/node.ts | 67 ++++++++++ resources/scripts/api/admin/server.ts | 12 +- resources/scripts/api/admin/transformers.ts | 138 ++++++++++++++++++-- resources/scripts/api/admin/user.ts | 36 +++++ 6 files changed, 274 insertions(+), 16 deletions(-) create mode 100644 resources/scripts/api/admin/egg.ts create mode 100644 resources/scripts/api/admin/location.ts create mode 100644 resources/scripts/api/admin/node.ts create mode 100644 resources/scripts/api/admin/user.ts diff --git a/resources/scripts/api/admin/egg.ts b/resources/scripts/api/admin/egg.ts new file mode 100644 index 000000000..3449eb2c7 --- /dev/null +++ b/resources/scripts/api/admin/egg.ts @@ -0,0 +1,24 @@ +import { Model, UUID } from '@/api/admin/index'; + +export interface Egg extends Model { + id: string; + uuid: UUID; + relationships: { + variables?: EggVariable[]; + }; +} + +export interface EggVariable extends Model { + id: number; + eggId: number; + name: string; + description: string; + environmentVariable: string; + defaultValue: string; + isUserViewable: boolean; + isUserEditable: boolean; + isRequired: boolean; + rules: string; + createdAt: Date; + updatedAt: Date; +} diff --git a/resources/scripts/api/admin/location.ts b/resources/scripts/api/admin/location.ts new file mode 100644 index 000000000..82ff394f8 --- /dev/null +++ b/resources/scripts/api/admin/location.ts @@ -0,0 +1,13 @@ +import { Model } from '@/api/admin/index'; +import { Node } from '@/api/admin/node'; + +export interface Location extends Model { + id: number; + short: string; + long: string; + createdAt: Date; + updatedAt: Date; + relationships: { + nodes?: Node[]; + }; +} diff --git a/resources/scripts/api/admin/node.ts b/resources/scripts/api/admin/node.ts new file mode 100644 index 000000000..9bc333541 --- /dev/null +++ b/resources/scripts/api/admin/node.ts @@ -0,0 +1,67 @@ +import { Model, UUID, WithRelationships, withRelationships } from '@/api/admin/index'; +import { Location } from '@/api/admin/location'; +import http from '@/api/http'; +import { AdminTransformers } from '@/api/admin/transformers'; +import { Server } from '@/api/admin/server'; + +interface NodePorts { + http: { + listen: number; + public: number; + }; + sftp: { + listen: number; + public: number; + }; +} + +export interface Allocation extends Model { + id: number; + ip: string; + port: number; + alias: string | null; + isAssigned: boolean; + relationships: { + node?: Node; + server?: Server | null; + }; +} + +export interface Node extends Model { + id: number; + uuid: UUID; + isPublic: boolean; + locationId: number; + databaseHostId: number; + name: string; + description: string | null; + fqdn: string; + ports: NodePorts; + scheme: 'http' | 'https'; + isBehindProxy: boolean; + isMaintenanceMode: boolean; + memory: number; + memoryOverallocate: number; + disk: number; + diskOverallocate: number; + uploadSize: number; + daemonBase: string; + createdAt: Date; + updatedAt: Date; + relationships: { + location?: Location; + }; +} + +/** + * Gets a single node and returns it. + */ +export const getNode = async (id: string | number): Promise> => { + const { data } = await http.get(`/api/application/nodes/${id}`, { + params: { + includes: [ 'location' ], + }, + }); + + return withRelationships(AdminTransformers.toNode(data.data), 'location'); +}; diff --git a/resources/scripts/api/admin/server.ts b/resources/scripts/api/admin/server.ts index fd45aa0bb..b65f50636 100644 --- a/resources/scripts/api/admin/server.ts +++ b/resources/scripts/api/admin/server.ts @@ -1,14 +1,12 @@ -import { Allocation } from '@/api/admin/nodes/getAllocations'; -import { Egg } from '@/api/admin/eggs/getEgg'; -import { User } from '@/api/admin/users/getUsers'; -import { Node } from '@/api/admin/nodes/getNodes'; -import { rawDataToServer, ServerVariable } from '@/api/admin/servers/getServers'; import useSWR, { SWRResponse } from 'swr'; import { AxiosError } from 'axios'; import { useRouteMatch } from 'react-router-dom'; import http from '@/api/http'; import { Model, UUID, withRelationships, WithRelationships } from '@/api/admin/index'; import { AdminTransformers } from '@/api/admin/transformers'; +import { Allocation, Node } from '@/api/admin/node'; +import { User } from '@/api/admin/user'; +import { Egg, EggVariable } from '@/api/admin/egg'; /** * Defines the limits for a server that exists on the Panel. @@ -23,6 +21,10 @@ interface ServerLimits { oomDisabled: boolean; } +export interface ServerVariable extends EggVariable { + serverValue: string; +} + /** * Defines a single server instance that is returned from the Panel's admin * API endpoints. diff --git a/resources/scripts/api/admin/transformers.ts b/resources/scripts/api/admin/transformers.ts index d3d21a1ab..795a0f441 100644 --- a/resources/scripts/api/admin/transformers.ts +++ b/resources/scripts/api/admin/transformers.ts @@ -1,11 +1,10 @@ /* eslint-disable camelcase */ -import { Server } from '@/api/admin/server'; +import { Allocation, Node } from '@/api/admin/node'; +import { Server, ServerVariable } from '@/api/admin/server'; import { FractalResponseData, FractalResponseList } from '@/api/http'; -import { rawDataToAllocation } from '@/api/admin/nodes/getAllocations'; -import { rawDataToEgg } from '@/api/admin/eggs/getEgg'; -import { rawDataToNode } from '@/api/admin/nodes/getNodes'; -import { rawDataToUser } from '@/api/admin/users/getUsers'; -import { rawDataToServerVariable } from '@/api/admin/servers/getServers'; +import { User, UserRole } from '@/api/admin/user'; +import { Location } from '@/api/admin/location'; +import { Egg, EggVariable } from '@/api/admin/egg'; const isList = (data: FractalResponseList | FractalResponseData): data is FractalResponseList => data.object === 'list'; @@ -45,12 +44,129 @@ export class AdminTransformers { createdAt: new Date(attributes.created_at), updatedAt: new Date(attributes.updated_at), relationships: { - allocations: transform(allocations as FractalResponseList | undefined, rawDataToAllocation), - egg: transform(egg as FractalResponseData | undefined, rawDataToEgg), - node: transform(node as FractalResponseData | undefined, rawDataToNode), - user: transform(user as FractalResponseData | undefined, rawDataToUser), - variables: transform(variables as FractalResponseList | undefined, rawDataToServerVariable), + allocations: transform(allocations as FractalResponseList | undefined, this.toAllocation), + egg: transform(egg as FractalResponseData | undefined, this.toEgg), + node: transform(node as FractalResponseData | undefined, this.toNode), + user: transform(user as FractalResponseData | undefined, this.toUser), + variables: transform(variables as FractalResponseList | undefined, this.toServerEggVariable), }, }; }; + + static toNode = ({ attributes }: FractalResponseData): Node => { + return { + id: attributes.id, + uuid: attributes.uuid, + isPublic: attributes.public, + locationId: attributes.location_id, + databaseHostId: attributes.database_host_id, + name: attributes.name, + description: attributes.description, + fqdn: attributes.fqdn, + ports: { + http: { + public: attributes.publicPortHttp, + listen: attributes.listenPortHttp, + }, + sftp: { + public: attributes.publicPortSftp, + listen: attributes.listenPortSftp, + }, + }, + scheme: attributes.scheme, + isBehindProxy: attributes.behindProxy, + isMaintenanceMode: attributes.maintenance_mode, + memory: attributes.memory, + memoryOverallocate: attributes.memory_overallocate, + disk: attributes.disk, + diskOverallocate: attributes.disk_overallocate, + uploadSize: attributes.upload_size, + daemonBase: attributes.daemonBase, + createdAt: new Date(attributes.created_at), + updatedAt: new Date(attributes.updated_at), + relationships: { + location: transform(attributes.relationships!.location as FractalResponseData, this.toLocation), + }, + }; + }; + + static toUserRole = ({ attributes }: FractalResponseData): UserRole => ({ + id: attributes.id, + name: attributes.name, + description: attributes.description, + relationships: {}, + }); + + static toUser = ({ attributes }: FractalResponseData): User => { + return { + id: attributes.id, + uuid: attributes.uuid, + externalId: attributes.external_id, + username: attributes.username, + email: attributes.email, + language: attributes.language, + adminRoleId: attributes.adminRoleId || null, + roleName: attributes.role_name, + isRootAdmin: attributes.root_admin, + isUsingTwoFactor: attributes['2fa'] || false, + avatarUrl: attributes.avatar_url, + createdAt: new Date(attributes.created_at), + updatedAt: new Date(attributes.updated_at), + relationships: { + role: transform(attributes.relationships?.role as FractalResponseData, this.toUserRole) || null, + }, + }; + }; + + static toLocation = ({ attributes }: FractalResponseData): Location => ({ + id: attributes.id, + short: attributes.short, + long: attributes.long, + createdAt: new Date(attributes.created_at), + updatedAt: new Date(attributes.updated_at), + relationships: { + nodes: transform(attributes.relationships?.node as FractalResponseList, this.toNode), + }, + }); + + static toEgg = ({ attributes }: FractalResponseData): Egg => ({ + id: attributes.id, + uuid: attributes.uuid, + relationships: { + variables: transform(attributes.relationships?.variables as FractalResponseList, this.toEggVariable), + }, + }); + + static toEggVariable = ({ attributes }: FractalResponseData): EggVariable => ({ + id: attributes.id, + eggId: attributes.egg_id, + name: attributes.name, + description: attributes.description, + environmentVariable: attributes.env_variable, + defaultValue: attributes.default_value, + isUserViewable: attributes.user_viewable, + isUserEditable: attributes.user_editable, + isRequired: attributes.required, + rules: attributes.rules, + createdAt: new Date(attributes.created_at), + updatedAt: new Date(attributes.updated_at), + relationships: {}, + }); + + static toServerEggVariable = (data: FractalResponseData): ServerVariable => ({ + ...this.toEggVariable(data), + serverValue: data.attributes.server_value, + }); + + static toAllocation = ({ attributes }: FractalResponseData): Allocation => ({ + id: attributes.id, + ip: attributes.ip, + port: attributes.port, + alias: attributes.alias || null, + isAssigned: attributes.assigned, + relationships: { + node: transform(attributes.relationships?.node as FractalResponseData, this.toNode), + server: transform(attributes.relationships?.server as FractalResponseData, this.toServer), + }, + }); } diff --git a/resources/scripts/api/admin/user.ts b/resources/scripts/api/admin/user.ts new file mode 100644 index 000000000..7d67e908b --- /dev/null +++ b/resources/scripts/api/admin/user.ts @@ -0,0 +1,36 @@ +import { Model, UUID } from '@/api/admin/index'; +import { Server } from '@/api/admin/server'; +import http from '@/api/http'; +import { AdminTransformers } from '@/api/admin/transformers'; + +export interface User extends Model { + id: number; + uuid: UUID; + externalId: string; + username: string; + email: string; + language: string; + adminRoleId: number | null; + roleName: string; + isRootAdmin: boolean; + isUsingTwoFactor: boolean; + avatarUrl: string; + createdAt: Date; + updatedAt: Date; + relationships: { + role: UserRole | null; + servers?: Server[]; + }; +} + +export interface UserRole extends Model { + id: string; + name: string; + description: string; +} + +export const getUser = async (id: string | number): Promise => { + const { data } = await http.get(`/api/application/users/${id}`); + + return AdminTransformers.toUser(data.data); +}; From f6998018b4c5a670385e20f5870974c5f3471fae Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 10 Oct 2021 12:03:28 -0700 Subject: [PATCH 3/5] Cleanup more of the server UI logic --- resources/scripts/api/admin/egg.ts | 3 +- resources/scripts/api/admin/node.ts | 1 + resources/scripts/api/admin/transformers.ts | 5 + .../components/admin/SubNavigation.tsx | 46 +++--- .../admin/servers/ServerDeleteButton.tsx | 41 +++--- .../admin/servers/ServerManageContainer.tsx | 26 +--- .../components/admin/servers/ServerRouter.tsx | 91 ++---------- .../admin/servers/ServerSettingsContainer.tsx | 133 ++---------------- .../admin/servers/ServerStartupContainer.tsx | 27 ++-- .../servers/settings/ServerResourceBox.tsx | 70 +++++++++ .../scripts/components/elements/Button.tsx | 22 ++- 11 files changed, 176 insertions(+), 289 deletions(-) create mode 100644 resources/scripts/components/admin/servers/settings/ServerResourceBox.tsx diff --git a/resources/scripts/api/admin/egg.ts b/resources/scripts/api/admin/egg.ts index 3449eb2c7..0f43814d0 100644 --- a/resources/scripts/api/admin/egg.ts +++ b/resources/scripts/api/admin/egg.ts @@ -1,8 +1,9 @@ import { Model, UUID } from '@/api/admin/index'; export interface Egg extends Model { - id: string; + id: number; uuid: UUID; + startup: string; relationships: { variables?: EggVariable[]; }; diff --git a/resources/scripts/api/admin/node.ts b/resources/scripts/api/admin/node.ts index 9bc333541..9ae6678a3 100644 --- a/resources/scripts/api/admin/node.ts +++ b/resources/scripts/api/admin/node.ts @@ -25,6 +25,7 @@ export interface Allocation extends Model { node?: Node; server?: Server | null; }; + getDisplayText(): string; } export interface Node extends Model { diff --git a/resources/scripts/api/admin/transformers.ts b/resources/scripts/api/admin/transformers.ts index 795a0f441..a279d1c3a 100644 --- a/resources/scripts/api/admin/transformers.ts +++ b/resources/scripts/api/admin/transformers.ts @@ -168,5 +168,10 @@ export class AdminTransformers { node: transform(attributes.relationships?.node as FractalResponseData, this.toNode), server: transform(attributes.relationships?.server as FractalResponseData, this.toServer), }, + getDisplayText (): string { + const raw = `${this.ip}:${this.port}`; + + return !this.alias ? raw : `${this.alias} (${raw})`; + }, }); } diff --git a/resources/scripts/components/admin/SubNavigation.tsx b/resources/scripts/components/admin/SubNavigation.tsx index e0dfe7091..857ed3093 100644 --- a/resources/scripts/components/admin/SubNavigation.tsx +++ b/resources/scripts/components/admin/SubNavigation.tsx @@ -3,37 +3,31 @@ import { NavLink } from 'react-router-dom'; import tw, { styled } from 'twin.macro'; export const SubNavigation = styled.div` - ${tw`flex flex-row items-center flex-shrink-0 h-12 mb-4 border-b border-neutral-700`}; + ${tw`flex flex-row items-center flex-shrink-0 h-12 mb-4 border-b border-neutral-700`}; - & > div { - ${tw`flex flex-col justify-center flex-shrink-0 h-full`}; + & > a { + ${tw`flex flex-row items-center h-full px-4 border-b text-neutral-300 text-base whitespace-nowrap border-transparent`}; - & > a { - ${tw`flex flex-row items-center h-full px-4 border-t text-neutral-300`}; - border-top-color: transparent !important; - - & > svg { - ${tw`w-6 h-6 mr-2`}; - } - - & > span { - ${tw`text-base whitespace-nowrap`}; - } - - &:active, &.active { - ${tw`border-b text-primary-300 border-primary-300`}; - } - } + & > svg { + ${tw`w-6 h-6 mr-2`}; } + + &:active, &.active { + ${tw`text-primary-300 border-primary-300`}; + } + } `; -export const SubNavigationLink = ({ to, name, children }: { to: string, name: string, children: React.ReactNode }) => { +interface Props { + to: string; + name: string; + icon: React.ComponentType; +} + +export const SubNavigationLink = ({ to, name, icon: IconComponent }: Props) => { return ( -
- - {children} - {name} - -
+ + {name} + ); }; diff --git a/resources/scripts/components/admin/servers/ServerDeleteButton.tsx b/resources/scripts/components/admin/servers/ServerDeleteButton.tsx index 4cbd3295e..1d2d0db35 100644 --- a/resources/scripts/components/admin/servers/ServerDeleteButton.tsx +++ b/resources/scripts/components/admin/servers/ServerDeleteButton.tsx @@ -5,27 +5,29 @@ import tw from 'twin.macro'; import Button from '@/components/elements/Button'; import ConfirmationModal from '@/components/elements/ConfirmationModal'; import deleteServer from '@/api/admin/servers/deleteServer'; +import { TrashIcon } from '@heroicons/react/outline'; +import { useServerFromRoute } from '@/api/admin/server'; +import { useHistory } from 'react-router-dom'; -interface Props { - serverId: number; - onDeleted: () => void; -} - -export default ({ serverId, onDeleted }: Props) => { +export default () => { + const history = useHistory(); const [ visible, setVisible ] = useState(false); const [ loading, setLoading ] = useState(false); + const { data: server } = useServerFromRoute(); - const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions) => actions.flashes); + const { + clearFlashes, + clearAndAddHttpError, + } = useStoreActions((actions: Actions) => actions.flashes); const onDelete = () => { + if (!server) return; + setLoading(true); clearFlashes('server'); - deleteServer(serverId) - .then(() => { - setLoading(false); - onDeleted(); - }) + deleteServer(server.id) + .then(() => history.push('/admin/servers')) .catch(error => { console.error(error); clearAndAddHttpError({ key: 'server', error }); @@ -35,6 +37,8 @@ export default ({ serverId, onDeleted }: Props) => { }); }; + if (!server) return null; + return ( <> { > Are you sure you want to delete this server? - - ); diff --git a/resources/scripts/components/admin/servers/ServerManageContainer.tsx b/resources/scripts/components/admin/servers/ServerManageContainer.tsx index 56c82b289..fba2f6d49 100644 --- a/resources/scripts/components/admin/servers/ServerManageContainer.tsx +++ b/resources/scripts/components/admin/servers/ServerManageContainer.tsx @@ -1,17 +1,13 @@ import React from 'react'; import AdminBox from '@/components/admin/AdminBox'; import tw from 'twin.macro'; -import { Context } from '@/components/admin/servers/ServerRouter'; import Button from '@/components/elements/Button'; +import { useServerFromRoute } from '@/api/admin/server'; -const ServerManageContainer = () => { - const server = Context.useStoreState(state => state.server); +export default () => { + const { data: server } = useServerFromRoute(); - if (server === undefined) { - return ( - <> - ); - } + if (!server) return null; return (
@@ -52,17 +48,3 @@ const ServerManageContainer = () => {
); }; - -export default () => { - const server = Context.useStoreState(state => state.server); - - if (server === undefined) { - return ( - <> - ); - } - - return ( - - ); -}; diff --git a/resources/scripts/components/admin/servers/ServerRouter.tsx b/resources/scripts/components/admin/servers/ServerRouter.tsx index 2e08bc4eb..9988b28c0 100644 --- a/resources/scripts/components/admin/servers/ServerRouter.tsx +++ b/resources/scripts/components/admin/servers/ServerRouter.tsx @@ -4,8 +4,6 @@ import React, { useEffect } from 'react'; import { useLocation } from 'react-router'; import tw from 'twin.macro'; import { Route, Switch, useRouteMatch } from 'react-router-dom'; -import { action, Action, createContextStore } from 'easy-peasy'; -import { Server } from '@/api/admin/servers/getServers'; import AdminContentBlock from '@/components/admin/AdminContentBlock'; import Spinner from '@/components/elements/Spinner'; import FlashMessageRender from '@/components/FlashMessageRender'; @@ -13,23 +11,9 @@ import { SubNavigation, SubNavigationLink } from '@/components/admin/SubNavigati import ServerSettingsContainer from '@/components/admin/servers/ServerSettingsContainer'; import useFlash from '@/plugins/useFlash'; import { useServerFromRoute } from '@/api/admin/server'; +import { AdjustmentsIcon, CogIcon, DatabaseIcon, FolderIcon, ShieldExclamationIcon } from '@heroicons/react/outline'; -export const ServerIncludes = [ 'allocations', 'user', 'variables' ]; - -interface ctx { - server: Server | undefined; - setServer: Action; -} - -export const Context = createContextStore({ - server: undefined, - - setServer: action((state, payload) => { - state.server = payload; - }), -}); - -const ServerRouter = () => { +export default () => { const location = useLocation(); const match = useRouteMatch<{ id?: string }>(); @@ -41,11 +25,8 @@ const ServerRouter = () => { }, []); useEffect(() => { - if (!error) { - clearFlashes('server'); - } else { - clearAndAddHttpError({ error, key: 'server' }); - } + if (!error) clearFlashes('server'); + if (error) clearAndAddHttpError({ error, key: 'server' }); }, [ error ]); if (!server || (error && isValidating)) { @@ -67,60 +48,18 @@ const ServerRouter = () => {
- - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - + - + @@ -129,11 +68,3 @@ const ServerRouter = () => { ); }; - -export default () => { - return ( - - - - ); -}; diff --git a/resources/scripts/components/admin/servers/ServerSettingsContainer.tsx b/resources/scripts/components/admin/servers/ServerSettingsContainer.tsx index 38f59693a..ff9e93423 100644 --- a/resources/scripts/components/admin/servers/ServerSettingsContainer.tsx +++ b/resources/scripts/components/admin/servers/ServerSettingsContainer.tsx @@ -1,118 +1,22 @@ -import { Server } from '@/api/admin/servers/getServers'; +import { useServerFromRoute } from '@/api/admin/server'; import ServerDeleteButton from '@/components/admin/servers/ServerDeleteButton'; -import { faBalanceScale } from '@fortawesome/free-solid-svg-icons'; import React from 'react'; -import AdminBox from '@/components/admin/AdminBox'; -import { useHistory } from 'react-router-dom'; import tw from 'twin.macro'; import { object } from 'yup'; import updateServer, { Values } from '@/api/admin/servers/updateServer'; -import Field from '@/components/elements/Field'; -import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; -import { Form, Formik, FormikHelpers, useFormikContext } from 'formik'; -import { Context, ServerIncludes } from '@/components/admin/servers/ServerRouter'; -import { ApplicationStore } from '@/state'; -import { Actions, useStoreActions } from 'easy-peasy'; +import { Form, Formik, FormikHelpers } from 'formik'; +import { useStoreActions } from 'easy-peasy'; import Button from '@/components/elements/Button'; -import FormikSwitch from '@/components/elements/FormikSwitch'; import BaseSettingsBox from '@/components/admin/servers/settings/BaseSettingsBox'; import FeatureLimitsBox from '@/components/admin/servers/settings/FeatureLimitsBox'; import NetworkingBox from '@/components/admin/servers/settings/NetworkingBox'; +import ServerResourceBox from '@/components/admin/servers/settings/ServerResourceBox'; -export function ServerResourceContainer () { - const { isSubmitting } = useFormikContext(); +export default () => { + const { data: server, mutate } = useServerFromRoute(); + const { clearFlashes, clearAndAddHttpError } = useStoreActions(actions => actions.flashes); - return ( - - - -
-
- -
- -
- -
-
- -
-
- -
- -
- -
-
- -
-
- -
- -
- -
-
- -
-
- -
-
-
- ); -} - -export default function ServerSettingsContainer2 ({ server }: { server: Server }) { - const history = useHistory(); - - const { - clearFlashes, - clearAndAddHttpError, - } = useStoreActions((actions: Actions) => actions.flashes); - - const setServer = Context.useStoreActions(actions => actions.setServer); + if (!server) return null; const submit = (values: Values, { setSubmitting, setFieldValue }: FormikHelpers) => { clearFlashes('server'); @@ -121,9 +25,9 @@ export default function ServerSettingsContainer2 ({ server }: { server: Server } // OOM Killer is enabled, rather than when disabled. values.limits.oomDisabled = !values.limits.oomDisabled; - updateServer(server.id, values, ServerIncludes) + updateServer(server.id, values) .then(s => { - setServer({ ...server, ...s }); + // setServer({ ...server, ...s }); // TODO: Figure out how to properly clear react-selects for allocations. setFieldValue('addAllocations', []); @@ -142,8 +46,7 @@ export default function ServerSettingsContainer2 ({ server }: { server: Server } initialValues={{ externalId: server.externalId || '', name: server.name, - ownerId: server.ownerId, - + ownerId: server.userId, limits: { memory: server.limits.memory, swap: server.limits.swap, @@ -155,13 +58,11 @@ export default function ServerSettingsContainer2 ({ server }: { server: Server } // OOM Killer is enabled, rather than when disabled. oomDisabled: !server.limits.oomDisabled, }, - featureLimits: { allocations: server.featureLimits.allocations, backups: server.featureLimits.backups, databases: server.featureLimits.databases, }, - allocationId: server.allocationId, addAllocations: [] as number[], removeAllocations: [] as number[], @@ -177,14 +78,10 @@ export default function ServerSettingsContainer2 ({ server }: { server: Server }
- - -
+ +
- history.push('/admin/servers')} - /> +
- {egg?.relations.variables?.map((v, i) => ( + {egg?.relationships.variables?.map((v, i) => ( v.eggId === v2.eggId && v.envVariable === v2.envVariable)?.serverValue || v.defaultValue} + defaultValue={server.relationships?.variables?.find(v2 => v.eggId === v2.eggId && v.environmentVariable === v2.environmentVariable)?.serverValue || v.defaultValue} /> ))}
@@ -179,9 +172,9 @@ function ServerStartupForm ({ egg, setEgg, server }: { egg: Egg | null, setEgg: } export default () => { - const { data: server, mutate } = useServerFromRoute(); + const { data: server } = useServerFromRoute(); const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions) => actions.flashes); - const [ egg, setEgg ] = useState(null); + const [ egg, setEgg ] = useState | null>(null); useEffect(() => { if (!server) return; @@ -213,7 +206,7 @@ export default () => { initialValues={{ startup: server.container.startup, // Don't ask. - environment: Object.fromEntries(egg?.relations.variables?.map(v => [ v.envVariable, '' ]) || []), + environment: Object.fromEntries(egg?.relationships.variables.map(v => [ v.environmentVariable, '' ]) || []), image: server.container.image, eggId: server.eggId, skipScripts: false, @@ -223,6 +216,7 @@ export default () => { > From 8486c914ae8136971147b60c26d4625c4b260c51 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 10 Oct 2021 13:21:21 -0700 Subject: [PATCH 5/5] More fixup for egg handling --- resources/scripts/api/admin/egg.ts | 6 +++--- resources/scripts/api/admin/node.ts | 2 +- resources/scripts/api/admin/server.ts | 2 +- resources/scripts/api/admin/transformers.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/resources/scripts/api/admin/egg.ts b/resources/scripts/api/admin/egg.ts index 740468942..fb7e52ed8 100644 --- a/resources/scripts/api/admin/egg.ts +++ b/resources/scripts/api/admin/egg.ts @@ -51,18 +51,18 @@ export interface EggVariable extends Model { export const getEgg = async (id: number | string): Promise> => { const { data } = await http.get(`/api/application/eggs/${id}`, { params: { - includes: [ 'nest', 'variables' ], + include: [ 'nest', 'variables' ], }, }); - return withRelationships(AdminTransformers.toEgg(data.data), 'nest', 'variables'); + return withRelationships(AdminTransformers.toEgg(data), 'nest', 'variables'); }; export const searchEggs = async (nestId: number, params: QueryBuilderParams<'name'>): Promise[]> => { const { data } = await http.get(`/api/application/nests/${nestId}/eggs`, { params: { ...withQueryBuilderParams(params), - includes: [ 'variables' ], + include: [ 'variables' ], }, }); diff --git a/resources/scripts/api/admin/node.ts b/resources/scripts/api/admin/node.ts index 9ae6678a3..3320e746a 100644 --- a/resources/scripts/api/admin/node.ts +++ b/resources/scripts/api/admin/node.ts @@ -60,7 +60,7 @@ export interface Node extends Model { export const getNode = async (id: string | number): Promise> => { const { data } = await http.get(`/api/application/nodes/${id}`, { params: { - includes: [ 'location' ], + include: [ 'location' ], }, }); diff --git a/resources/scripts/api/admin/server.ts b/resources/scripts/api/admin/server.ts index 4a0112c44..368175543 100644 --- a/resources/scripts/api/admin/server.ts +++ b/resources/scripts/api/admin/server.ts @@ -79,7 +79,7 @@ type LoadedServer = WithRelationships; export const getServer = async (id: number | string): Promise => { const { data } = await http.get(`/api/application/servers/${id}`, { params: { - includes: [ 'allocations', 'user', 'node' ], + include: [ 'allocations', 'user', 'node' ], }, }); diff --git a/resources/scripts/api/admin/transformers.ts b/resources/scripts/api/admin/transformers.ts index 6447bb04c..b306a41c8 100644 --- a/resources/scripts/api/admin/transformers.ts +++ b/resources/scripts/api/admin/transformers.ts @@ -88,7 +88,7 @@ export class AdminTransformers { createdAt: new Date(attributes.created_at), updatedAt: new Date(attributes.updated_at), relationships: { - location: transform(attributes.relationships!.location as FractalResponseData, this.toLocation), + location: transform(attributes.relationships?.location as FractalResponseData, this.toLocation), }, }; };