Fix users and roles apis

This commit is contained in:
Dane Everitt 2022-02-27 15:26:26 -05:00
parent 034b4ad3b0
commit 37ce7b08b7
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
28 changed files with 217 additions and 498 deletions

View file

@ -0,0 +1,103 @@
import http, { getPaginationSet, PaginatedResult } from '@/api/http';
import { Transformers, UserRole } from '@definitions/admin';
import { useContext } from 'react';
import useSWR from 'swr';
import { createContext } from '@/api/admin/index';
export interface Filters {
id?: string;
name?: string;
}
export const Context = createContext<Filters>();
const createRole = (name: string, description: string | null, include: string[] = []): Promise<UserRole> => {
return new Promise((resolve, reject) => {
http.post('/api/application/roles', {
name, description,
}, { params: { include: include.join(',') } })
.then(({ data }) => resolve(Transformers.toUserRole(data)))
.catch(reject);
});
};
const deleteRole = (id: number): Promise<void> => {
return new Promise((resolve, reject) => {
http.delete(`/api/application/roles/${id}`)
.then(() => resolve())
.catch(reject);
});
};
const getRole = (id: number, include: string[] = []): Promise<UserRole> => {
return new Promise((resolve, reject) => {
http.get(`/api/application/roles/${id}`, { params: { include: include.join(',') } })
.then(({ data }) => resolve(Transformers.toUserRole(data)))
.catch(reject);
});
};
const searchRoles = (filters?: { name?: string }): Promise<UserRole[]> => {
const params = {};
if (filters !== undefined) {
Object.keys(filters).forEach(key => {
// @ts-ignore
params['filter[' + key + ']'] = filters[key];
});
}
return new Promise((resolve, reject) => {
http.get('/api/application/roles', { params })
.then(response => resolve(
(response.data.data || []).map(Transformers.toUserRole)
))
.catch(reject);
});
};
const updateRole = (id: number, name: string, description: string | null, include: string[] = []): Promise<UserRole> => {
return new Promise((resolve, reject) => {
http.patch(`/api/application/roles/${id}`, {
name, description,
}, { params: { include: include.join(',') } })
.then(({ data }) => resolve(Transformers.toUserRole(data)))
.catch(reject);
});
};
const getRoles = (include: string[] = []) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { page, filters, sort, sortDirection } = useContext(Context);
const params = {};
if (filters !== null) {
Object.keys(filters).forEach(key => {
// @ts-ignore
params['filter[' + key + ']'] = filters[key];
});
}
if (sort !== null) {
// @ts-ignore
params.sort = (sortDirection ? '-' : '') + sort;
}
// eslint-disable-next-line react-hooks/rules-of-hooks
return useSWR<PaginatedResult<UserRole>>([ 'roles', page, filters, sort, sortDirection ], async () => {
const { data } = await http.get('/api/application/roles', { params: { include: include.join(','), page, ...params } });
return ({
items: (data.data || []).map(Transformers.toUserRole),
pagination: getPaginationSet(data.meta.pagination),
});
});
};
export {
createRole,
deleteRole,
getRole,
searchRoles,
updateRole,
getRoles,
};

View file

@ -1,12 +0,0 @@
import http from '@/api/http';
import { Role, rawDataToRole } from '@/api/admin/roles/getRoles';
export default (name: string, description: string | null, include: string[] = []): Promise<Role> => {
return new Promise((resolve, reject) => {
http.post('/api/application/roles', {
name, description,
}, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToRole(data)))
.catch(reject);
});
};

View file

@ -1,9 +0,0 @@
import http from '@/api/http';
export default (id: number): Promise<void> => {
return new Promise((resolve, reject) => {
http.delete(`/api/application/roles/${id}`)
.then(() => resolve())
.catch(reject);
});
};

View file

@ -1,10 +0,0 @@
import http from '@/api/http';
import { Role, rawDataToRole } from '@/api/admin/roles/getRoles';
export default (id: number, include: string[] = []): Promise<Role> => {
return new Promise((resolve, reject) => {
http.get(`/api/application/roles/${id}`, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToRole(data)))
.catch(reject);
});
};

View file

@ -1,49 +0,0 @@
import http, { FractalResponseData, getPaginationSet, PaginatedResult } from '@/api/http';
import { useContext } from 'react';
import useSWR from 'swr';
import { createContext } from '@/api/admin';
export interface Role {
id: number;
name: string;
description?: string;
}
export const rawDataToRole = ({ attributes }: FractalResponseData): Role => ({
id: attributes.id,
name: attributes.name,
description: attributes.description,
});
export interface Filters {
id?: string;
name?: string;
}
export const Context = createContext<Filters>();
export default (include: string[] = []) => {
const { page, filters, sort, sortDirection } = useContext(Context);
const params = {};
if (filters !== null) {
Object.keys(filters).forEach(key => {
// @ts-ignore
params['filter[' + key + ']'] = filters[key];
});
}
if (sort !== null) {
// @ts-ignore
params.sort = (sortDirection ? '-' : '') + sort;
}
return useSWR<PaginatedResult<Role>>([ 'roles', page, filters, sort, sortDirection ], async () => {
const { data } = await http.get('/api/application/roles', { params: { include: include.join(','), page, ...params } });
return ({
items: (data.data || []).map(rawDataToRole),
pagination: getPaginationSet(data.meta.pagination),
});
});
};

View file

@ -1,24 +0,0 @@
import http from '@/api/http';
import { Role, rawDataToRole } from '@/api/admin/roles/getRoles';
interface Filters {
name?: string;
}
export default (filters?: Filters): Promise<Role[]> => {
const params = {};
if (filters !== undefined) {
Object.keys(filters).forEach(key => {
// @ts-ignore
params['filter[' + key + ']'] = filters[key];
});
}
return new Promise((resolve, reject) => {
http.get('/api/application/roles', { params })
.then(response => resolve(
(response.data.data || []).map(rawDataToRole)
))
.catch(reject);
});
};

View file

@ -1,12 +0,0 @@
import http from '@/api/http';
import { Role, rawDataToRole } from '@/api/admin/roles/getRoles';
export default (id: number, name: string, description: string | null, include: string[] = []): Promise<Role> => {
return new Promise((resolve, reject) => {
http.patch(`/api/application/roles/${id}`, {
name, description,
}, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToRole(data)))
.catch(reject);
});
};

View file

@ -5,7 +5,7 @@ import { createContext } from '@/api/admin';
import http, { FractalResponseData, FractalResponseList, getPaginationSet, PaginatedResult } from '@/api/http'; import http, { FractalResponseData, FractalResponseList, getPaginationSet, PaginatedResult } from '@/api/http';
import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg'; import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg';
import { Node, rawDataToNode } from '@/api/admin/nodes/getNodes'; import { Node, rawDataToNode } from '@/api/admin/nodes/getNodes';
import { User, rawDataToUser } from '@/api/admin/users/getUsers'; import { Transformers, User } from '@definitions/admin';
export interface ServerVariable { export interface ServerVariable {
id: number; id: number;
@ -132,7 +132,7 @@ export const rawDataToServer = ({ attributes }: FractalResponseData): Server =>
allocations: ((attributes.relationships?.allocations as FractalResponseList | undefined)?.data || []).map(rawDataToAllocation), allocations: ((attributes.relationships?.allocations as FractalResponseList | undefined)?.data || []).map(rawDataToAllocation),
egg: attributes.relationships?.egg?.object === 'egg' ? rawDataToEgg(attributes.relationships.egg as FractalResponseData) : undefined, egg: attributes.relationships?.egg?.object === 'egg' ? rawDataToEgg(attributes.relationships.egg as FractalResponseData) : undefined,
node: attributes.relationships?.node?.object === 'node' ? rawDataToNode(attributes.relationships.node as FractalResponseData) : undefined, node: attributes.relationships?.node?.object === 'node' ? rawDataToNode(attributes.relationships.node as FractalResponseData) : undefined,
user: attributes.relationships?.user?.object === 'user' ? rawDataToUser(attributes.relationships.user as FractalResponseData) : undefined, user: attributes.relationships?.user?.object === 'user' ? Transformers.toUser(attributes.relationships.user as FractalResponseData) : undefined,
variables: ((attributes.relationships?.variables as FractalResponseList | undefined)?.data || []).map(rawDataToServerVariable), variables: ((attributes.relationships?.variables as FractalResponseList | undefined)?.data || []).map(rawDataToServerVariable),
}, },
}) as Server; }) as Server;

View file

@ -1,16 +0,0 @@
import http, { QueryBuilderParams, withQueryBuilderParams } from '@/api/http';
import { Transformers, User } from '@definitions/admin';
export const getUser = async (id: string | number): Promise<User> => {
const { data } = await http.get(`/api/application/users/${id}`);
return Transformers.toUser(data.data);
};
export const searchUserAccounts = async (params: QueryBuilderParams<'username' | 'email'>): Promise<User[]> => {
const { data } = await http.get('/api/application/users', {
params: withQueryBuilderParams(params),
});
return data.data.map(Transformers.toUser);
};

View file

@ -0,0 +1,74 @@
import http, { QueryBuilderParams, withQueryBuilderParams } from '@/api/http';
import { Transformers, User } from '@definitions/admin';
export interface UpdateUserValues {
externalId: string;
username: string;
email: string;
password: string;
adminRoleId: number | null;
rootAdmin: boolean;
}
const getUser = (id: number, include: string[] = []): Promise<User> => {
return new Promise((resolve, reject) => {
http.get(`/api/application/users/${id}`, { params: { include: include.join(',') } })
.then(({ data }) => resolve(Transformers.toUser(data)))
.catch(reject);
});
};
const searchUserAccounts = async (params: QueryBuilderParams<'username' | 'email'>): Promise<User[]> => {
const { data } = await http.get('/api/application/users', {
params: withQueryBuilderParams(params),
});
return data.data.map(Transformers.toUser);
};
const createUser = (values: UpdateUserValues, include: string[] = []): Promise<User> => {
const data = {};
Object.keys(values).forEach(k => {
// @ts-ignore
data[k.replace(/[A-Z]/g, l => `_${l.toLowerCase()}`)] = values[k];
});
return new Promise((resolve, reject) => {
http.post('/api/application/users', data, { params: { include: include.join(',') } })
.then(({ data }) => resolve(Transformers.toUser(data)))
.catch(reject);
});
};
const updateUser = (id: number, values: Partial<UpdateUserValues>, include: string[] = []): Promise<User> => {
const data = {};
Object.keys(values).forEach(k => {
// Don't set password if it is empty.
if (k === 'password' && values[k] === '') {
return;
}
// @ts-ignore
data[k.replace(/[A-Z]/g, l => `_${l.toLowerCase()}`)] = values[k];
});
return new Promise((resolve, reject) => {
http.patch(`/api/application/users/${id}`, data, { params: { include: include.join(',') } })
.then(({ data }) => resolve(Transformers.toUser(data)))
.catch(reject);
});
};
const deleteUser = (id: number): Promise<void> => {
return new Promise((resolve, reject) => {
http.delete(`/api/application/users/${id}`)
.then(() => resolve())
.catch(reject);
});
};
export {
getUser,
searchUserAccounts,
createUser,
updateUser,
deleteUser,
};

View file

@ -1,18 +0,0 @@
import http from '@/api/http';
import { User, rawDataToUser } from '@/api/admin/users/getUsers';
import { Values } from '@/api/admin/users/updateUser';
export type { Values };
export default (values: Values, include: string[] = []): Promise<User> => {
const data = {};
Object.keys(values).forEach(k => {
// @ts-ignore
data[k.replace(/[A-Z]/g, l => `_${l.toLowerCase()}`)] = values[k];
});
return new Promise((resolve, reject) => {
http.post('/api/application/users', data, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToUser(data)))
.catch(reject);
});
};

View file

@ -1,9 +0,0 @@
import http from '@/api/http';
export default (id: number): Promise<void> => {
return new Promise((resolve, reject) => {
http.delete(`/api/application/users/${id}`)
.then(() => resolve())
.catch(reject);
});
};

View file

@ -1,10 +0,0 @@
import http from '@/api/http';
import { User, rawDataToUser } from '@/api/admin/users/getUsers';
export default (id: number, include: string[] = []): Promise<User> => {
return new Promise((resolve, reject) => {
http.get(`/api/application/users/${id}`, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToUser(data)))
.catch(reject);
});
};

View file

@ -1,81 +0,0 @@
import http, { FractalResponseData, getPaginationSet, PaginatedResult } from '@/api/http';
import { useContext } from 'react';
import useSWR from 'swr';
import { createContext } from '@/api/admin';
import { rawDataToDatabase } from '@/api/admin/databases/getDatabases';
import { Role } from '@/api/admin/roles/getRoles';
export interface User {
id: number;
externalId: string;
uuid: string;
username: string;
email: string;
language: string;
adminRoleId: number | null;
rootAdmin: boolean;
tfa: boolean;
avatarURL: string;
roleName: string | null;
createdAt: Date;
updatedAt: Date;
relationships: {
role: Role | undefined;
};
}
export const rawDataToUser = ({ attributes }: FractalResponseData): User => ({
id: attributes.id,
externalId: attributes.external_id,
uuid: attributes.uuid,
username: attributes.username,
email: attributes.email,
language: attributes.language,
adminRoleId: attributes.admin_role_id,
rootAdmin: attributes.root_admin,
tfa: attributes['2fa'],
avatarURL: attributes.avatar_url,
roleName: attributes.role_name,
createdAt: new Date(attributes.created_at),
updatedAt: new Date(attributes.updated_at),
relationships: {
role: attributes.relationships?.role !== undefined && attributes.relationships?.role.object !== 'null_resource' ? rawDataToDatabase(attributes.relationships.role as FractalResponseData) : undefined,
},
});
export interface Filters {
id?: string;
uuid?: string;
username?: string;
email?: string;
}
export const Context = createContext<Filters>();
export default (include: string[] = []) => {
const { page, filters, sort, sortDirection } = useContext(Context);
const params = {};
if (filters !== null) {
Object.keys(filters).forEach(key => {
// @ts-ignore
params['filter[' + key + ']'] = filters[key];
});
}
if (sort !== null) {
// @ts-ignore
params.sort = (sortDirection ? '-' : '') + sort;
}
return useSWR<PaginatedResult<User>>([ 'users', page, filters, sort, sortDirection ], async () => {
const { data } = await http.get('/api/application/users', { params: { include: include.join(','), page, ...params } });
return ({
items: (data.data || []).map(rawDataToUser),
pagination: getPaginationSet(data.meta.pagination),
});
});
};

View file

@ -1,28 +0,0 @@
import http from '@/api/http';
import { User, rawDataToUser } from '@/api/admin/users/getUsers';
export interface Values {
externalId: string;
username: string;
email: string;
password: string;
adminRoleId: number | null;
rootAdmin: boolean;
}
export default (id: number, values: Partial<Values>, include: string[] = []): Promise<User> => {
const data = {};
Object.keys(values).forEach(k => {
// Don't set password if it is empty.
if (k === 'password' && values[k] === '') {
return;
}
// @ts-ignore
data[k.replace(/[A-Z]/g, l => `_${l.toLowerCase()}`)] = values[k];
});
return new Promise((resolve, reject) => {
http.patch(`/api/application/users/${id}`, data, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToUser(data)))
.catch(reject);
});
};

View file

@ -23,7 +23,7 @@ interface User extends ModelWithRelationships {
} }
interface UserRole extends ModelWithRelationships { interface UserRole extends ModelWithRelationships {
id: string; id: number;
name: string; name: string;
description: string; description: string;
} }

View file

@ -2,8 +2,7 @@ import { Form, Formik, FormikHelpers } from 'formik';
import React, { useState } from 'react'; import React, { useState } from 'react';
import tw from 'twin.macro'; import tw from 'twin.macro';
import { object, string } from 'yup'; import { object, string } from 'yup';
import createRole from '@/api/admin/roles/createRole'; import { getRoles, createRole } from '@/api/admin/roles';
import getRoles from '@/api/admin/roles/getRoles';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
import Button from '@/components/elements/Button'; import Button from '@/components/elements/Button';
import Field from '@/components/elements/Field'; import Field from '@/components/elements/Field';

View file

@ -1,7 +1,7 @@
import { Actions, useStoreActions } from 'easy-peasy'; import { Actions, useStoreActions } from 'easy-peasy';
import React, { useState } from 'react'; import React, { useState } from 'react';
import tw from 'twin.macro'; import tw from 'twin.macro';
import deleteRole from '@/api/admin/roles/deleteRole'; import { deleteRole } from '@/api/admin/roles';
import Button from '@/components/elements/Button'; import Button from '@/components/elements/Button';
import ConfirmationModal from '@/components/elements/ConfirmationModal'; import ConfirmationModal from '@/components/elements/ConfirmationModal';
import { ApplicationStore } from '@/state'; import { ApplicationStore } from '@/state';

View file

@ -5,9 +5,7 @@ import { useHistory } from 'react-router';
import { useRouteMatch } from 'react-router-dom'; import { useRouteMatch } from 'react-router-dom';
import tw from 'twin.macro'; import tw from 'twin.macro';
import { object, string } from 'yup'; import { object, string } from 'yup';
import { Role } from '@/api/admin/roles/getRoles'; import { getRole, updateRole } from '@/api/admin/roles';
import getRole from '@/api/admin/roles/getRole';
import updateRole from '@/api/admin/roles/updateRole';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
import AdminBox from '@/components/admin/AdminBox'; import AdminBox from '@/components/admin/AdminBox';
import AdminContentBlock from '@/components/admin/AdminContentBlock'; import AdminContentBlock from '@/components/admin/AdminContentBlock';
@ -17,10 +15,11 @@ import Field from '@/components/elements/Field';
import Spinner from '@/components/elements/Spinner'; import Spinner from '@/components/elements/Spinner';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import { ApplicationStore } from '@/state'; import { ApplicationStore } from '@/state';
import { UserRole } from '@definitions/admin';
interface ctx { interface ctx {
role: Role | undefined; role: UserRole | undefined;
setRole: Action<ctx, Role | undefined>; setRole: Action<ctx, UserRole | undefined>;
} }
export const Context = createContextStore<ctx>({ export const Context = createContextStore<ctx>({
@ -39,7 +38,10 @@ interface Values {
const EditInformationContainer = () => { const EditInformationContainer = () => {
const history = useHistory(); const history = useHistory();
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes); const {
clearFlashes,
clearAndAddHttpError,
} = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const role = Context.useStoreState(state => state.role); const role = Context.useStoreState(state => state.role);
const setRole = Context.useStoreActions(actions => actions.setRole); const setRole = Context.useStoreActions(actions => actions.setRole);
@ -125,7 +127,10 @@ const EditInformationContainer = () => {
const RoleEditContainer = () => { const RoleEditContainer = () => {
const match = useRouteMatch<{ id?: string }>(); const match = useRouteMatch<{ id?: string }>();
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes); const {
clearFlashes,
clearAndAddHttpError,
} = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const [ loading, setLoading ] = useState(true); const [ loading, setLoading ] = useState(true);
const role = Context.useStoreState(state => state.role); const role = Context.useStoreState(state => state.role);

View file

@ -1,5 +1,5 @@
import React, { useContext, useEffect } from 'react'; import React, { useContext, useEffect } from 'react';
import getRoles, { Context as RolesContext, Filters } from '@/api/admin/roles/getRoles'; import { getRoles, Context as RolesContext, Filters } from '@/api/admin/roles';
import { AdminContext } from '@/state/admin'; import { AdminContext } from '@/state/admin';
import NewRoleButton from '@/components/admin/roles/NewRoleButton'; import NewRoleButton from '@/components/admin/roles/NewRoleButton';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';

View file

@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import SearchableSelect, { Option } from '@/components/elements/SearchableSelect'; import SearchableSelect, { Option } from '@/components/elements/SearchableSelect';
import { User } from '@definitions/admin'; import { User } from '@definitions/admin';
import { searchUserAccounts } from '@/api/admin/user'; import { searchUserAccounts } from '@/api/admin/users';
export default ({ selected }: { selected?: User }) => { export default ({ selected }: { selected?: User }) => {
const { setFieldValue } = useFormikContext(); const { setFieldValue } = useFormikContext();

View file

@ -7,14 +7,14 @@ import { useHistory } from 'react-router-dom';
import { Actions, useStoreActions } from 'easy-peasy'; import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationStore } from '@/state'; import { ApplicationStore } from '@/state';
import { FormikHelpers } from 'formik'; import { FormikHelpers } from 'formik';
import createUser, { Values } from '@/api/admin/users/createUser'; import { createUser, UpdateUserValues } from '@/api/admin/users';
export default () => { export default () => {
const history = useHistory(); const history = useHistory();
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes); const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => { const submit = (values: UpdateUserValues, { setSubmitting }: FormikHelpers<UpdateUserValues>) => {
clearFlashes('user:create'); clearFlashes('user:create');
createUser(values) createUser(values)

View file

@ -1,14 +1,14 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import { Role } from '@/api/admin/roles/getRoles'; import { searchRoles } from '@/api/admin/roles';
import searchRoles from '@/api/admin/roles/searchRoles';
import SearchableSelect, { Option } from '@/components/elements/SearchableSelect'; import SearchableSelect, { Option } from '@/components/elements/SearchableSelect';
import { UserRole } from '@definitions/admin';
export default ({ selected }: { selected: Role | null }) => { export default ({ selected }: { selected: UserRole | null }) => {
const context = useFormikContext(); const context = useFormikContext();
const [ role, setRole ] = useState<Role | null>(selected); const [ role, setRole ] = useState<UserRole | null>(selected);
const [ roles, setRoles ] = useState<Role[] | null>(null); const [ roles, setRoles ] = useState<UserRole[] | null>(null);
const onSearch = (query: string): Promise<void> => { const onSearch = (query: string): Promise<void> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -21,12 +21,12 @@ export default ({ selected }: { selected: Role | null }) => {
}); });
}; };
const onSelect = (role: Role | null) => { const onSelect = (role: UserRole | null) => {
setRole(role); setRole(role);
context.setFieldValue('adminRoleId', role?.id || null); context.setFieldValue('adminRoleId', role?.id || null);
}; };
const getSelectedText = (role: Role | null): string | undefined => { const getSelectedText = (role: UserRole | null): string | undefined => {
return role?.name; return role?.name;
}; };

View file

@ -1,4 +1,4 @@
import updateUser, { Values } from '@/api/admin/users/updateUser'; import { updateUser, UpdateUserValues } from '@/api/admin/users';
import UserDeleteButton from '@/components/admin/users/UserDeleteButton'; import UserDeleteButton from '@/components/admin/users/UserDeleteButton';
import UserForm from '@/components/admin/users/UserForm'; import UserForm from '@/components/admin/users/UserForm';
import { Context } from '@/components/admin/users/UserRouter'; import { Context } from '@/components/admin/users/UserRouter';
@ -23,7 +23,7 @@ const UserAboutContainer = () => {
); );
} }
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => { const submit = (values: UpdateUserValues, { setSubmitting }: FormikHelpers<UpdateUserValues>) => {
clearFlashes('user'); clearFlashes('user');
updateUser(user.id, values) updateUser(user.id, values)
@ -44,7 +44,7 @@ const UserAboutContainer = () => {
email: user.email, email: user.email,
adminRoleId: user.adminRoleId, adminRoleId: user.adminRoleId,
password: '', password: '',
rootAdmin: user.rootAdmin, rootAdmin: user.isRootAdmin,
}} }}
onSubmit={submit} onSubmit={submit}
uuid={user.uuid} uuid={user.uuid}

View file

@ -4,7 +4,7 @@ import { ApplicationStore } from '@/state';
import tw from 'twin.macro'; import tw from 'twin.macro';
import Button from '@/components/elements/Button'; import Button from '@/components/elements/Button';
import ConfirmationModal from '@/components/elements/ConfirmationModal'; import ConfirmationModal from '@/components/elements/ConfirmationModal';
import deleteUser from '@/api/admin/users/deleteUser'; import { deleteUser } from '@/api/admin/users';
interface Props { interface Props {
userId: number; userId: number;

View file

@ -5,13 +5,12 @@ import Label from '@/components/elements/Label';
import React from 'react'; import React from 'react';
import tw from 'twin.macro'; import tw from 'twin.macro';
import { action, Action, createContextStore } from 'easy-peasy'; import { action, Action, createContextStore } from 'easy-peasy';
import { User } from '@/api/admin/users/getUsers'; import { User, UserRole } from '@definitions/admin';
import AdminBox from '@/components/admin/AdminBox'; import AdminBox from '@/components/admin/AdminBox';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import { Form, Formik, FormikHelpers } from 'formik'; import { Form, Formik, FormikHelpers } from 'formik';
import { bool, object, string } from 'yup'; import { bool, object, string } from 'yup';
import { Role } from '@/api/admin/roles/getRoles'; import { UpdateUserValues } from '@/api/admin/users';
import { Values } from '@/api/admin/users/updateUser';
import Button from '@/components/elements/Button'; import Button from '@/components/elements/Button';
import Field, { FieldRow } from '@/components/elements/Field'; import Field, { FieldRow } from '@/components/elements/Field';
import RoleSelect from '@/components/admin/users/RoleSelect'; import RoleSelect from '@/components/admin/users/RoleSelect';
@ -31,17 +30,17 @@ export const Context = createContextStore<ctx>({
export interface Params { export interface Params {
title: string; title: string;
initialValues?: Values; initialValues?: UpdateUserValues;
children?: React.ReactNode; children?: React.ReactNode;
onSubmit: (values: Values, helpers: FormikHelpers<Values>) => void; onSubmit: (values: UpdateUserValues, helpers: FormikHelpers<UpdateUserValues>) => void;
uuid?: string; uuid?: string;
role: Role | null; role: UserRole | null;
} }
export default function UserForm ({ title, initialValues, children, onSubmit, uuid, role }: Params) { export default function UserForm ({ title, initialValues, children, onSubmit, uuid, role }: Params) {
const submit = (values: Values, helpers: FormikHelpers<Values>) => { const submit = (values: UpdateUserValues, helpers: FormikHelpers<UpdateUserValues>) => {
onSubmit(values, helpers); onSubmit(values, helpers);
}; };

View file

@ -4,14 +4,14 @@ import { useLocation } from 'react-router';
import tw from 'twin.macro'; import tw from 'twin.macro';
import { Route, Switch, useRouteMatch } from 'react-router-dom'; import { Route, Switch, useRouteMatch } from 'react-router-dom';
import { action, Action, Actions, createContextStore, useStoreActions } from 'easy-peasy'; import { action, Action, Actions, createContextStore, useStoreActions } from 'easy-peasy';
import { User } from '@/api/admin/users/getUsers'; import { getUser } from '@/api/admin/users';
import getUser from '@/api/admin/users/getUser';
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 FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
import { ApplicationStore } from '@/state'; import { ApplicationStore } from '@/state';
import { SubNavigation, SubNavigationLink } from '@/components/admin/SubNavigation'; import { SubNavigation, SubNavigationLink } from '@/components/admin/SubNavigation';
import UserServers from '@/components/admin/users/UserServers'; import UserServers from '@/components/admin/users/UserServers';
import { User } from '@definitions/admin';
interface ctx { interface ctx {
user: User | undefined; user: User | undefined;

View file

@ -1,183 +0,0 @@
import React, { useContext, useEffect } from 'react';
import AdminCheckbox from '@/components/admin/AdminCheckbox';
import CopyOnClick from '@/components/elements/CopyOnClick';
import getUsers, { Context as UsersContext, Filters } from '@/api/admin/users/getUsers';
import AdminTable, { TableBody, TableHead, TableHeader, Pagination, Loading, NoItems, ContentWrapper, useTableHooks } from '@/components/admin/AdminTable';
import Button from '@/components/elements/Button';
import FlashMessageRender from '@/components/FlashMessageRender';
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';
const RowCheckbox = ({ id }: { id: number }) => {
const isChecked = AdminContext.useStoreState(state => state.users.selectedUsers.indexOf(id) >= 0);
const appendSelectedUser = AdminContext.useStoreActions(actions => actions.users.appendSelectedUser);
const removeSelectedUser = AdminContext.useStoreActions(actions => actions.users.removeSelectedUser);
return (
<AdminCheckbox
name={id.toString()}
checked={isChecked}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.currentTarget.checked) {
appendSelectedUser(id);
} else {
removeSelectedUser(id);
}
}}
/>
);
};
const UsersContainer = () => {
const match = useRouteMatch();
const { page, setPage, setFilters, sort, setSort, sortDirection } = useContext(UsersContext);
const { clearFlashes, clearAndAddHttpError } = useFlash();
const { data: users, error, isValidating } = getUsers();
useEffect(() => {
if (!error) {
clearFlashes('users');
return;
}
clearAndAddHttpError({ key: 'users', error });
}, [ error ]);
const length = users?.items?.length || 0;
const setSelectedUsers = AdminContext.useStoreActions(actions => actions.users.setSelectedUsers);
const selectedUserLength = AdminContext.useStoreState(state => state.users.selectedUsers.length);
const onSelectAllClick = (e: React.ChangeEvent<HTMLInputElement>) => {
setSelectedUsers(e.currentTarget.checked ? (users?.items?.map(user => user.id) || []) : []);
};
const onSearch = (query: string): Promise<void> => {
return new Promise((resolve) => {
if (query.length < 2) {
setFilters(null);
} else {
setFilters({ username: query });
}
return resolve();
});
};
useEffect(() => {
setSelectedUsers([]);
}, [ page ]);
return (
<AdminContentBlock title={'Users'}>
<div css={tw`w-full flex flex-row items-center mb-8`}>
<div css={tw`flex flex-col flex-shrink`} style={{ minWidth: '0' }}>
<h2 css={tw`text-2xl text-neutral-50 font-header font-medium`}>Users</h2>
<p css={tw`text-base text-neutral-400 whitespace-nowrap overflow-ellipsis overflow-hidden`}>All registered users on the system.</p>
</div>
<div css={tw`flex ml-auto pl-4`}>
<NavLink to={`${match.url}/new`}>
<Button type={'button'} size={'large'} css={tw`h-10 px-4 py-0 whitespace-nowrap`}>
New User
</Button>
</NavLink>
</div>
</div>
<FlashMessageRender byKey={'users'} css={tw`mb-4`}/>
<AdminTable>
<ContentWrapper
checked={selectedUserLength === (length === 0 ? -1 : length)}
onSelectAllClick={onSelectAllClick}
onSearch={onSearch}
>
<Pagination data={users} onPageSelect={setPage}>
<div css={tw`overflow-x-auto`}>
<table css={tw`w-full table-auto`}>
<TableHead>
<TableHeader name={'ID'} direction={sort === 'id' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('id')}/>
<TableHeader name={'Name'} direction={sort === 'email' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('email')}/>
<TableHeader name={'Username'} direction={sort === 'username' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('username')}/>
<TableHeader name={'Status'}/>
<TableHeader name={'Role'} direction={sort === 'admin_role_id' ? (sortDirection ? 1 : 2) : null} onClick={() => setSort('admin_role_id')}/>
</TableHead>
<TableBody>
{ users !== undefined && !error && !isValidating && length > 0 &&
users.items.map(user => (
<tr key={user.id} css={tw`h-14 hover:bg-neutral-600`}>
<td css={tw`pl-6`}>
<RowCheckbox id={user.id}/>
</td>
<td css={tw`px-6 text-sm text-neutral-200 text-left whitespace-nowrap`}>
<CopyOnClick text={user.id.toString()}>
<code css={tw`font-mono bg-neutral-900 rounded py-1 px-2`}>{user.id}</code>
</CopyOnClick>
</td>
<td css={tw`px-6 whitespace-nowrap`}>
<NavLink to={`${match.url}/${user.id}`}>
<div css={tw`flex items-center`}>
<div css={tw`flex-shrink-0 h-10 w-10`}>
<img css={tw`h-10 w-10 rounded-full`} alt="" src={user.avatarURL + '?s=40'}/>
</div>
<div css={tw`ml-4`}>
<div css={tw`text-sm text-neutral-200`}>
{user.email}
</div>
<div css={tw`text-sm text-neutral-400`}>
{user.uuid.split('-')[0]}
</div>
</div>
</div>
</NavLink>
</td>
<td css={tw`px-6 text-sm text-neutral-200 text-left whitespace-nowrap`}>{user.username}</td>
<td css={tw`px-6 whitespace-nowrap`}>
<span css={tw`px-2 inline-flex text-xs leading-5 font-medium rounded-full bg-green-100 text-green-800`}>
Active
</span>
</td>
<td css={tw`px-6 text-sm text-neutral-200 text-left whitespace-nowrap`}>{user.roleName || 'None'}</td>
</tr>
))
}
</TableBody>
</table>
{ users === undefined || (error && isValidating) ?
<Loading/>
:
length < 1 ?
<NoItems/>
:
null
}
</div>
</Pagination>
</ContentWrapper>
</AdminTable>
</AdminContentBlock>
);
};
export default () => {
const hooks = useTableHooks<Filters>();
return (
<UsersContext.Provider value={hooks}>
<UsersContainer/>
</UsersContext.Provider>
);
};