diff --git a/app/Models/AdminRole.php b/app/Models/AdminRole.php index 76b22478c..001810cee 100644 --- a/app/Models/AdminRole.php +++ b/app/Models/AdminRole.php @@ -40,4 +40,9 @@ class AdminRole extends Model 'name' => 'required|string|max:64', 'description' => 'nullable|string|max:255', ]; + + /** + * @var bool + */ + public $timestamps = false; } diff --git a/resources/scripts/api/admin/roles/createRole.ts b/resources/scripts/api/admin/roles/createRole.ts new file mode 100644 index 000000000..e62c9bf52 --- /dev/null +++ b/resources/scripts/api/admin/roles/createRole.ts @@ -0,0 +1,12 @@ +import { Role } from '@/api/admin/roles/getRoles'; +import http from '@/api/http'; + +export default (name: string, description?: string): Promise => { + return new Promise((resolve, reject) => { + http.post('/api/application/roles', { + name, description, + }) + .then(({ data }) => resolve(data)) + .catch(reject); + }); +}; diff --git a/resources/scripts/api/admin/roles/getRoles.ts b/resources/scripts/api/admin/roles/getRoles.ts index 4a660927c..63def3c1d 100644 --- a/resources/scripts/api/admin/roles/getRoles.ts +++ b/resources/scripts/api/admin/roles/getRoles.ts @@ -1,9 +1,10 @@ import http from '@/api/http'; export interface Role { - id: number, - name: string, - description: string|null, + id: number; + name: string; + description: string | null; + sortId: number; } export default (): Promise => { diff --git a/resources/scripts/components/admin/api/NewApiKeyButton.tsx b/resources/scripts/components/admin/api/NewApiKeyButton.tsx index fce62f7c4..0e8d4ad3a 100644 --- a/resources/scripts/components/admin/api/NewApiKeyButton.tsx +++ b/resources/scripts/components/admin/api/NewApiKeyButton.tsx @@ -1,10 +1,10 @@ +import React, { useState } from 'react'; 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 React, { useState } from 'react'; import tw from 'twin.macro'; import { object } from 'yup'; diff --git a/resources/scripts/components/admin/roles/NewRoleButton.tsx b/resources/scripts/components/admin/roles/NewRoleButton.tsx new file mode 100644 index 000000000..eb920c7d1 --- /dev/null +++ b/resources/scripts/components/admin/roles/NewRoleButton.tsx @@ -0,0 +1,111 @@ +import createRole from '@/api/admin/roles/createRole'; +import { httpErrorToHuman } from '@/api/http'; +import { AdminContext } from '@/state/admin'; +import React, { useState } from 'react'; +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 role name must be provided.') + .max(32, 'Role name must not exceed 32 characters.'), + description: string() + .max(255, 'Role description must not exceed 255 characters.'), +}); + +export default () => { + const [ visible, setVisible ] = useState(false); + const { addError, clearFlashes } = useFlash(); + + const appendRole = AdminContext.useStoreActions(actions => actions.roles.appendRole); + + const submit = ({ name, description }: Values, { setSubmitting }: FormikHelpers) => { + clearFlashes('role:create'); + setSubmitting(true); + + createRole(name, description) + .then(role => { + appendRole(role); + setVisible(false); + }) + .catch(error => { + addError({ key: 'role:create', message: httpErrorToHuman(error) }); + setSubmitting(false); + }); + }; + + return ( + <> + + { + ({ isSubmitting, resetForm }) => ( + { + resetForm(); + setVisible(false); + }} + > + +

New Role

+
+ + +
+ +
+ +
+ + +
+ +
+ ) + } +
+ + + + ); +}; diff --git a/resources/scripts/components/admin/roles/RolesContainer.tsx b/resources/scripts/components/admin/roles/RolesContainer.tsx index 5ddf1fd28..5a2f4ab6b 100644 --- a/resources/scripts/components/admin/roles/RolesContainer.tsx +++ b/resources/scripts/components/admin/roles/RolesContainer.tsx @@ -1,26 +1,31 @@ +import React, { useEffect, useState } from 'react'; +import { useDeepMemoize } from '@/plugins/useDeepMemoize'; +import { AdminContext } from '@/state/admin'; import { httpErrorToHuman } from '@/api/http'; +import NewRoleButton from '@/components/admin/roles/NewRoleButton'; import FlashMessageRender from '@/components/FlashMessageRender'; import useFlash from '@/plugins/useFlash'; -import React, { useEffect, useState } from 'react'; -import Button from '@/components/elements/Button'; import tw from 'twin.macro'; import AdminContentBlock from '@/components/admin/AdminContentBlock'; import Spinner from '@/components/elements/Spinner'; -import getRoles, { Role } from '@/api/admin/roles/getRoles'; +import getRoles from '@/api/admin/roles/getRoles'; export default () => { - const { clearFlashes, addError } = useFlash(); - const [ loading, setLoading ] = useState(true); - const [ roles, setRoles ] = useState([]); + const { addError, clearFlashes } = useFlash(); + const [ loading, setLoading ] = useState(true); + + const roles = useDeepMemoize(AdminContext.useStoreState(state => state.roles.data)); + const setRoles = AdminContext.useStoreActions(state => state.roles.setRoles); useEffect(() => { + setLoading(!roles.length); clearFlashes('roles'); getRoles() .then(roles => setRoles(roles)) .catch(error => { - addError({ message: httpErrorToHuman(error), key: 'roles' }); console.error(error); + addError({ message: httpErrorToHuman(error), key: 'roles' }); }) .then(() => setLoading(false)); }, []); @@ -33,9 +38,7 @@ export default () => {

Soon™

- + diff --git a/resources/scripts/components/server/network/NetworkContainer.tsx b/resources/scripts/components/server/network/NetworkContainer.tsx index 236707219..fdc66aa6d 100644 --- a/resources/scripts/components/server/network/NetworkContainer.tsx +++ b/resources/scripts/components/server/network/NetworkContainer.tsx @@ -11,7 +11,6 @@ import Can from '@/components/elements/Can'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import getServerAllocations from '@/api/swr/getServerAllocations'; import isEqual from 'react-fast-compare'; -import { Allocation } from '@/api/server/getServer'; const NetworkContainer = () => { const [ loading, setLoading ] = useState(false); diff --git a/resources/scripts/routers/AdminRouter.tsx b/resources/scripts/routers/AdminRouter.tsx index 593d7ef1f..0319e855d 100644 --- a/resources/scripts/routers/AdminRouter.tsx +++ b/resources/scripts/routers/AdminRouter.tsx @@ -1,6 +1,6 @@ -import RolesContainer from '@/components/admin/roles/RolesContainer'; import React, { useState } from 'react'; import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom'; +import RolesContainer from '@/components/admin/roles/RolesContainer'; import NotFound from '@/components/screens/NotFound'; import SettingsContainer from '@/components/admin/settings/SettingsContainer'; import OverviewContainer from '@/components/admin/overview/OverviewContainer'; @@ -16,6 +16,7 @@ import ServersContainer from '@/components/admin/servers/ServersContainer'; import UsersContainer from '@/components/admin/users/UsersContainer'; import NestsContainer from '@/components/admin/nests/NestsContainer'; import MountsContainer from '@/components/admin/mounts/MountsContainer'; +import { AdminContext } from '@/state/admin'; const Sidebar = styled.div<{ collapsed?: boolean }>` ${tw`h-screen flex flex-col items-center flex-shrink-0 bg-neutral-900 overflow-x-hidden transition-all duration-250 ease-linear`}; @@ -74,7 +75,7 @@ const Sidebar = styled.div<{ collapsed?: boolean }>` } `; -export default ({ location, match }: RouteComponentProps) => { +const AdminRouter = ({ location, match }: RouteComponentProps) => { const name = useStoreState((state: ApplicationStore) => state.settings.data!.name); const [ collapsed, setCollapsed ] = useState(); @@ -188,3 +189,9 @@ export default ({ location, match }: RouteComponentProps) => { ); }; + +export default (props: RouteComponentProps) => ( + + + +); diff --git a/resources/scripts/state/admin/index.ts b/resources/scripts/state/admin/index.ts new file mode 100644 index 000000000..e6f0a0e40 --- /dev/null +++ b/resources/scripts/state/admin/index.ts @@ -0,0 +1,16 @@ +import { createContextStore } from 'easy-peasy'; +import { composeWithDevTools } from 'redux-devtools-extension'; +import roles, { AdminRoleStore } from '@/state/admin/roles'; + +interface AdminStore { + roles: AdminRoleStore; +} + +export const AdminContext = createContextStore({ + roles, +}, { + compose: composeWithDevTools({ + name: 'AdminStore', + trace: true, + }), +}); diff --git a/resources/scripts/state/admin/roles.ts b/resources/scripts/state/admin/roles.ts new file mode 100644 index 000000000..f67aa7f97 --- /dev/null +++ b/resources/scripts/state/admin/roles.ts @@ -0,0 +1,31 @@ +import { action, Action } from 'easy-peasy'; +import { Role } from '@/api/admin/roles/getRoles'; + +export interface AdminRoleStore { + data: Role[]; + setRoles: Action; + appendRole: Action; + removeRole: Action; +} + +const roles: AdminRoleStore = { + data: [], + + setRoles: action((state, payload) => { + state.data = payload; + }), + + 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); + } else { + state.data = [ ...state.data, payload ]; + } + }), + + removeRole: action((state, payload) => { + state.data = [ ...state.data.filter(role => role.id !== payload) ]; + }), +}; + +export default roles;