Add admin state store, add new role functionality
This commit is contained in:
parent
7369167e28
commit
dc0fdee030
|
@ -40,4 +40,9 @@ class AdminRole extends Model
|
||||||
'name' => 'required|string|max:64',
|
'name' => 'required|string|max:64',
|
||||||
'description' => 'nullable|string|max:255',
|
'description' => 'nullable|string|max:255',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $timestamps = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Role } from '@/api/admin/roles/getRoles';
|
||||||
|
import http from '@/api/http';
|
||||||
|
|
||||||
|
export default (name: string, description?: string): Promise<Role> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
http.post('/api/application/roles', {
|
||||||
|
name, description,
|
||||||
|
})
|
||||||
|
.then(({ data }) => resolve(data))
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
};
|
|
@ -1,9 +1,10 @@
|
||||||
import http from '@/api/http';
|
import http from '@/api/http';
|
||||||
|
|
||||||
export interface Role {
|
export interface Role {
|
||||||
id: number,
|
id: number;
|
||||||
name: string,
|
name: string;
|
||||||
description: string|null,
|
description: string | null;
|
||||||
|
sortId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (): Promise<Role[]> => {
|
export default (): Promise<Role[]> => {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
import Button from '@/components/elements/Button';
|
import Button from '@/components/elements/Button';
|
||||||
import Field from '@/components/elements/Field';
|
import Field from '@/components/elements/Field';
|
||||||
import Modal from '@/components/elements/Modal';
|
import Modal from '@/components/elements/Modal';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
import useFlash from '@/plugins/useFlash';
|
import useFlash from '@/plugins/useFlash';
|
||||||
import { Form, Formik, FormikHelpers } from 'formik';
|
import { Form, Formik, FormikHelpers } from 'formik';
|
||||||
import React, { useState } from 'react';
|
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
import { object } from 'yup';
|
import { object } from 'yup';
|
||||||
|
|
||||||
|
|
|
@ -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<Values>) => {
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<Formik
|
||||||
|
onSubmit={submit}
|
||||||
|
initialValues={{ name: '', description: '' }}
|
||||||
|
validationSchema={schema}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
({ isSubmitting, resetForm }) => (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
dismissable={!isSubmitting}
|
||||||
|
showSpinnerOverlay={isSubmitting}
|
||||||
|
onDismissed={() => {
|
||||||
|
resetForm();
|
||||||
|
setVisible(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FlashMessageRender byKey={'role:create'} css={tw`mb-6`}/>
|
||||||
|
<h2 css={tw`text-2xl mb-6`}>New Role</h2>
|
||||||
|
<Form css={tw`m-0`}>
|
||||||
|
<Field
|
||||||
|
type={'string'}
|
||||||
|
id={'name'}
|
||||||
|
name={'name'}
|
||||||
|
label={'Name'}
|
||||||
|
description={'A short name used to identify this role.'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div css={tw`mt-6`}>
|
||||||
|
<Field
|
||||||
|
type={'string'}
|
||||||
|
id={'description'}
|
||||||
|
name={'description'}
|
||||||
|
label={'Description'}
|
||||||
|
description={'A descriptive of this role.'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div css={tw`flex flex-wrap justify-end mt-6`}>
|
||||||
|
<Button
|
||||||
|
type={'button'}
|
||||||
|
isSecondary
|
||||||
|
css={tw`w-full sm:w-auto sm:mr-2`}
|
||||||
|
onClick={() => setVisible(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button css={tw`w-full mt-4 sm:w-auto sm:mt-0`} type={'submit'}>
|
||||||
|
Create Role
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Formik>
|
||||||
|
|
||||||
|
<Button type={'button'} size={'large'} css={tw`h-10 ml-auto px-4 py-0`} onClick={() => setVisible(true)}>
|
||||||
|
New Role
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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 { httpErrorToHuman } from '@/api/http';
|
||||||
|
import NewRoleButton from '@/components/admin/roles/NewRoleButton';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
import useFlash from '@/plugins/useFlash';
|
import useFlash from '@/plugins/useFlash';
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import Button from '@/components/elements/Button';
|
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
import AdminContentBlock from '@/components/admin/AdminContentBlock';
|
import AdminContentBlock from '@/components/admin/AdminContentBlock';
|
||||||
import Spinner from '@/components/elements/Spinner';
|
import Spinner from '@/components/elements/Spinner';
|
||||||
import getRoles, { Role } from '@/api/admin/roles/getRoles';
|
import getRoles from '@/api/admin/roles/getRoles';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { clearFlashes, addError } = useFlash();
|
const { addError, clearFlashes } = useFlash();
|
||||||
const [ loading, setLoading ] = useState<boolean>(true);
|
const [ loading, setLoading ] = useState(true);
|
||||||
const [ roles, setRoles ] = useState<Role[]>([]);
|
|
||||||
|
const roles = useDeepMemoize(AdminContext.useStoreState(state => state.roles.data));
|
||||||
|
const setRoles = AdminContext.useStoreActions(state => state.roles.setRoles);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setLoading(!roles.length);
|
||||||
clearFlashes('roles');
|
clearFlashes('roles');
|
||||||
|
|
||||||
getRoles()
|
getRoles()
|
||||||
.then(roles => setRoles(roles))
|
.then(roles => setRoles(roles))
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
addError({ message: httpErrorToHuman(error), key: 'roles' });
|
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
addError({ message: httpErrorToHuman(error), key: 'roles' });
|
||||||
})
|
})
|
||||||
.then(() => setLoading(false));
|
.then(() => setLoading(false));
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -33,9 +38,7 @@ export default () => {
|
||||||
<p css={tw`text-base text-neutral-400`}>Soon™</p>
|
<p css={tw`text-base text-neutral-400`}>Soon™</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button type={'button'} size={'large'} css={tw`h-10 ml-auto px-4 py-0`}>
|
<NewRoleButton />
|
||||||
New Role
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FlashMessageRender byKey={'roles'} css={tw`mb-4`}/>
|
<FlashMessageRender byKey={'roles'} css={tw`mb-4`}/>
|
||||||
|
|
|
@ -11,7 +11,6 @@ import Can from '@/components/elements/Can';
|
||||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||||
import getServerAllocations from '@/api/swr/getServerAllocations';
|
import getServerAllocations from '@/api/swr/getServerAllocations';
|
||||||
import isEqual from 'react-fast-compare';
|
import isEqual from 'react-fast-compare';
|
||||||
import { Allocation } from '@/api/server/getServer';
|
|
||||||
|
|
||||||
const NetworkContainer = () => {
|
const NetworkContainer = () => {
|
||||||
const [ loading, setLoading ] = useState(false);
|
const [ loading, setLoading ] = useState(false);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import RolesContainer from '@/components/admin/roles/RolesContainer';
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom';
|
import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom';
|
||||||
|
import RolesContainer from '@/components/admin/roles/RolesContainer';
|
||||||
import NotFound from '@/components/screens/NotFound';
|
import NotFound from '@/components/screens/NotFound';
|
||||||
import SettingsContainer from '@/components/admin/settings/SettingsContainer';
|
import SettingsContainer from '@/components/admin/settings/SettingsContainer';
|
||||||
import OverviewContainer from '@/components/admin/overview/OverviewContainer';
|
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 UsersContainer from '@/components/admin/users/UsersContainer';
|
||||||
import NestsContainer from '@/components/admin/nests/NestsContainer';
|
import NestsContainer from '@/components/admin/nests/NestsContainer';
|
||||||
import MountsContainer from '@/components/admin/mounts/MountsContainer';
|
import MountsContainer from '@/components/admin/mounts/MountsContainer';
|
||||||
|
import { AdminContext } from '@/state/admin';
|
||||||
|
|
||||||
const Sidebar = styled.div<{ collapsed?: boolean }>`
|
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`};
|
${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 name = useStoreState((state: ApplicationStore) => state.settings.data!.name);
|
||||||
const [ collapsed, setCollapsed ] = useState<boolean>();
|
const [ collapsed, setCollapsed ] = useState<boolean>();
|
||||||
|
|
||||||
|
@ -188,3 +189,9 @@ export default ({ location, match }: RouteComponentProps) => {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default (props: RouteComponentProps<any>) => (
|
||||||
|
<AdminContext.Provider>
|
||||||
|
<AdminRouter {...props}/>
|
||||||
|
</AdminContext.Provider>
|
||||||
|
);
|
||||||
|
|
|
@ -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<AdminStore>({
|
||||||
|
roles,
|
||||||
|
}, {
|
||||||
|
compose: composeWithDevTools({
|
||||||
|
name: 'AdminStore',
|
||||||
|
trace: true,
|
||||||
|
}),
|
||||||
|
});
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { action, Action } from 'easy-peasy';
|
||||||
|
import { Role } from '@/api/admin/roles/getRoles';
|
||||||
|
|
||||||
|
export interface AdminRoleStore {
|
||||||
|
data: Role[];
|
||||||
|
setRoles: Action<AdminRoleStore, Role[]>;
|
||||||
|
appendRole: Action<AdminRoleStore, Role>;
|
||||||
|
removeRole: Action<AdminRoleStore, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
Loading…
Reference in New Issue