From 6c85be72fa366075341bc2ae3f860d0449cdc8ef Mon Sep 17 00:00:00 2001 From: Matthew Penner Date: Thu, 31 Dec 2020 17:27:16 -0700 Subject: [PATCH] Add nest endpoints and pages --- .../scripts/api/admin/nests/createNest.ts | 12 ++ .../scripts/api/admin/nests/eggs/getEggs.ts | 34 +++ resources/scripts/api/admin/nests/getNests.ts | 20 ++ resources/scripts/api/transformers.ts | 36 ++++ .../components/admin/AdminCheckbox.tsx | 24 +++ .../components/admin/nests/NestsContainer.tsx | 204 +++++++++++++++++- .../components/admin/nests/NewNestButton.tsx | 111 ++++++++++ .../components/admin/roles/NewRoleButton.tsx | 2 +- .../components/admin/roles/RolesContainer.tsx | 2 +- resources/scripts/state/admin/index.ts | 3 + resources/scripts/state/admin/nests.ts | 31 +++ resources/scripts/state/admin/roles.ts | 4 +- 12 files changed, 473 insertions(+), 10 deletions(-) create mode 100644 resources/scripts/api/admin/nests/createNest.ts create mode 100644 resources/scripts/api/admin/nests/eggs/getEggs.ts create mode 100644 resources/scripts/api/admin/nests/getNests.ts create mode 100644 resources/scripts/components/admin/AdminCheckbox.tsx create mode 100644 resources/scripts/components/admin/nests/NewNestButton.tsx create mode 100644 resources/scripts/state/admin/nests.ts diff --git a/resources/scripts/api/admin/nests/createNest.ts b/resources/scripts/api/admin/nests/createNest.ts new file mode 100644 index 000000000..49004a88d --- /dev/null +++ b/resources/scripts/api/admin/nests/createNest.ts @@ -0,0 +1,12 @@ +import http from '@/api/http'; +import { Nest } from '@/api/admin/nests/getNests'; + +export default (name: string, description?: string): Promise => { + return new Promise((resolve, reject) => { + http.post('/api/application/nests', { + name, description, + }) + .then(({ data }) => resolve(data.attributes)) + .catch(reject); + }); +}; diff --git a/resources/scripts/api/admin/nests/eggs/getEggs.ts b/resources/scripts/api/admin/nests/eggs/getEggs.ts new file mode 100644 index 000000000..6509e1ac0 --- /dev/null +++ b/resources/scripts/api/admin/nests/eggs/getEggs.ts @@ -0,0 +1,34 @@ +import http from '@/api/http'; +import { rawDataToEgg } from '@/api/transformers'; + +export interface Egg { + id: number; + uuid: string; + nest_id: number; + author: string; + name: string; + description: string | null; + features: string[] | null; + dockerImages: string[]; + configFiles: string | null; + configStartup: string | null; + configLogs: string | null; + configStop: string | null; + configFrom: number | null; + startup: string; + scriptContainer: string; + copyScriptFrom: number | null; + scriptEntry: string; + scriptIsPrivileged: boolean; + scriptInstall: string | null; + createdAt: Date; + updatedAt: Date; +} + +export default (id: number): Promise => { + return new Promise((resolve, reject) => { + http.get(`/api/application/nests/${id}`) + .then(({ data }) => resolve((data.data || []).map(rawDataToEgg))) + .catch(reject); + }); +}; diff --git a/resources/scripts/api/admin/nests/getNests.ts b/resources/scripts/api/admin/nests/getNests.ts new file mode 100644 index 000000000..643cdd383 --- /dev/null +++ b/resources/scripts/api/admin/nests/getNests.ts @@ -0,0 +1,20 @@ +import http from '@/api/http'; +import { rawDataToNest } from '@/api/transformers'; + +export interface Nest { + id: number; + uuid: string; + author: string; + name: string; + description: string | null; + createdAt: Date; + updatedAt: Date; +} + +export default (): Promise => { + return new Promise((resolve, reject) => { + http.get('/api/application/nests') + .then(({ data }) => resolve((data.data || []).map(rawDataToNest))) + .catch(reject); + }); +}; diff --git a/resources/scripts/api/transformers.ts b/resources/scripts/api/transformers.ts index 94d4f711b..614bebce1 100644 --- a/resources/scripts/api/transformers.ts +++ b/resources/scripts/api/transformers.ts @@ -1,3 +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 { Allocation } from '@/api/server/getServer'; import { FractalResponseData } from '@/api/http'; @@ -81,3 +83,37 @@ export const rawDataToAdminRole = ({ attributes }: FractalResponseData): Role => 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), +}); diff --git a/resources/scripts/components/admin/AdminCheckbox.tsx b/resources/scripts/components/admin/AdminCheckbox.tsx new file mode 100644 index 000000000..6d1ed5b5d --- /dev/null +++ b/resources/scripts/components/admin/AdminCheckbox.tsx @@ -0,0 +1,24 @@ +import Input from '@/components/elements/Input'; +import React from 'react'; +import styled from 'styled-components/macro'; +import tw from 'twin.macro'; + +const Checkbox = styled(Input)` + && { + ${tw`border-neutral-500 bg-transparent`}; + + &:not(:checked) { + ${tw`hover:border-neutral-300`}; + } + } +`; + +export default ({ name }: { name: string }) => { + return ( + + ); +}; diff --git a/resources/scripts/components/admin/nests/NestsContainer.tsx b/resources/scripts/components/admin/nests/NestsContainer.tsx index 4938c5e86..352e4bc9d 100644 --- a/resources/scripts/components/admin/nests/NestsContainer.tsx +++ b/resources/scripts/components/admin/nests/NestsContainer.tsx @@ -1,20 +1,212 @@ -import Button from '@/components/elements/Button'; -import React from 'react'; +import AdminCheckbox from '@/components/admin/AdminCheckbox'; +import React, { useEffect, useState } from 'react'; +import getNests from '@/api/admin/nests/getNests'; +import { httpErrorToHuman } from '@/api/http'; +import NewNestButton from '@/components/admin/nests/NewNestButton'; +import Spinner from '@/components/elements/Spinner'; +import FlashMessageRender from '@/components/FlashMessageRender'; +import { useDeepMemoize } from '@/plugins/useDeepMemoize'; +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 match = useRouteMatch(); + + const { addError, clearFlashes } = useFlash(); + const [ loading, setLoading ] = useState(true); + + const nests = useDeepMemoize(AdminContext.useStoreState(state => state.nests.data)); + const setNests = AdminContext.useStoreActions(state => state.nests.setNests); + + useEffect(() => { + setLoading(!nests.length); + clearFlashes('nests'); + + getNests() + .then(nests => setNests(nests)) + .catch(error => { + console.error(error); + addError({ message: httpErrorToHuman(error), key: 'nests' }); + }) + .then(() => setLoading(false)); + }, []); + return ( -
+

Nests

All nests currently available on this system.

- + +
+ + + +
+
+ { loading ? +
+ +
+ : + nests.length < 1 ? +
+
+ {'No +
+ +

No items could be found, it's almost like they are hiding.

+
+ : + <> +
+
+ + + + + +
+ +
+ + + + + + + +
+
+ +
+ + + + + + + + + + {/* + + + + { + nests.map(nest => ( + + + + + + + + )) + } + +
+ + + + ID + +
+ + + + +
+
+
+ + Name + +
+ + + + +
+
+
+ + Description + +
+ + + + +
+
+
*/} +
+ + {nest.id} + + {nest.name} + + {nest.description}
+
+ +
+

+ Showing 1 to 10 of 97 results +

+ +
+ +
+
+ + } +
); diff --git a/resources/scripts/components/admin/nests/NewNestButton.tsx b/resources/scripts/components/admin/nests/NewNestButton.tsx new file mode 100644 index 000000000..c585001ea --- /dev/null +++ b/resources/scripts/components/admin/nests/NewNestButton.tsx @@ -0,0 +1,111 @@ +import React, { useState } from 'react'; +import createNest from '@/api/admin/nests/createNest'; +import { httpErrorToHuman } from '@/api/http'; +import { AdminContext } from '@/state/admin'; +import Button from '@/components/elements/Button'; +import Field from '@/components/elements/Field'; +import Modal from '@/components/elements/Modal'; +import FlashMessageRender from '@/components/FlashMessageRender'; +import useFlash from '@/plugins/useFlash'; +import { Form, Formik, FormikHelpers } from 'formik'; +import tw from 'twin.macro'; +import { object, string } from 'yup'; + +interface Values { + name: string, + description: string, +} + +const schema = object().shape({ + name: string() + .required('A nest name must be provided.') + .max(32, 'Nest name must not exceed 32 characters.'), + description: string() + .max(255, 'Nest description must not exceed 255 characters.'), +}); + +export default () => { + const [ visible, setVisible ] = useState(false); + const { addError, clearFlashes } = useFlash(); + + const appendNest = AdminContext.useStoreActions(actions => actions.nests.appendNest); + + const submit = ({ name, description }: Values, { setSubmitting }: FormikHelpers) => { + clearFlashes('nest:create'); + setSubmitting(true); + + createNest(name, description) + .then(nest => { + appendNest(nest); + setVisible(false); + }) + .catch(error => { + addError({ key: 'nest:create', message: httpErrorToHuman(error) }); + setSubmitting(false); + }); + }; + + return ( + <> + + { + ({ isSubmitting, resetForm }) => ( + { + resetForm(); + setVisible(false); + }} + > + +

New Nest

+
+ + +
+ +
+ +
+ + +
+ +
+ ) + } +
+ + + + ); +}; diff --git a/resources/scripts/components/admin/roles/NewRoleButton.tsx b/resources/scripts/components/admin/roles/NewRoleButton.tsx index b659e7549..2e2119fa0 100644 --- a/resources/scripts/components/admin/roles/NewRoleButton.tsx +++ b/resources/scripts/components/admin/roles/NewRoleButton.tsx @@ -64,7 +64,7 @@ export default () => { }} > -

New Role

+

New Role

{

Soon™

- + diff --git a/resources/scripts/state/admin/index.ts b/resources/scripts/state/admin/index.ts index e6f0a0e40..1483b6b12 100644 --- a/resources/scripts/state/admin/index.ts +++ b/resources/scripts/state/admin/index.ts @@ -1,12 +1,15 @@ import { createContextStore } from 'easy-peasy'; import { composeWithDevTools } from 'redux-devtools-extension'; +import nests, { AdminNestStore } from '@/state/admin/nests'; import roles, { AdminRoleStore } from '@/state/admin/roles'; interface AdminStore { + nests: AdminNestStore roles: AdminRoleStore; } export const AdminContext = createContextStore({ + nests, roles, }, { compose: composeWithDevTools({ diff --git a/resources/scripts/state/admin/nests.ts b/resources/scripts/state/admin/nests.ts new file mode 100644 index 000000000..bf1d8d2cf --- /dev/null +++ b/resources/scripts/state/admin/nests.ts @@ -0,0 +1,31 @@ +import { action, Action } from 'easy-peasy'; +import { Nest } from '@/api/admin/nests/getNests'; + +export interface AdminNestStore { + data: Nest[]; + setNests: Action; + appendNest: Action; + removeNest: Action; +} + +const nests: AdminNestStore = { + data: [], + + setNests: action((state, payload) => { + state.data = payload; + }), + + appendNest: action((state, payload) => { + if (state.data.find(nest => nest.id === payload.id)) { + state.data = state.data.map(nest => nest.id === payload.id ? payload : nest); + } else { + state.data = [ ...state.data, payload ]; + } + }), + + removeNest: action((state, payload) => { + state.data = [ ...state.data.filter(nest => nest.id !== payload) ]; + }), +}; + +export default nests; diff --git a/resources/scripts/state/admin/roles.ts b/resources/scripts/state/admin/roles.ts index f67aa7f97..bf6bf418c 100644 --- a/resources/scripts/state/admin/roles.ts +++ b/resources/scripts/state/admin/roles.ts @@ -16,8 +16,8 @@ const roles: AdminRoleStore = { }), appendRole: action((state, payload) => { - if (state.data.find(database => database.id === payload.id)) { - state.data = state.data.map(database => database.id === payload.id ? payload : database); + if (state.data.find(role => role.id === payload.id)) { + state.data = state.data.map(role => role.id === payload.id ? payload : role); } else { state.data = [ ...state.data, payload ]; }