fix(striker-ui): complete user management in striker config

main
Tsu-ba-me 2 years ago
parent 46f299d878
commit 87c8240bf7
  1. 197
      striker-ui/components/StrikerConfig/CommonUserInputGroup.tsx
  2. 271
      striker-ui/components/StrikerConfig/ManageUsersForm.tsx
  3. 5
      striker-ui/types/APIUser.d.ts
  4. 11
      striker-ui/types/CommonUserInputGroup.d.ts

@ -0,0 +1,197 @@
import { ReactElement, useMemo, useRef, useState } from 'react';
import INPUT_TYPES from '../../lib/consts/INPUT_TYPES';
import Grid from '../Grid';
import InputWithRef, { InputForwardedRefContent } from '../InputWithRef';
import OutlinedInputWithLabel from '../OutlinedInputWithLabel';
import {
buildPeacefulStringTestBatch,
testNotBlank,
} from '../../lib/test_input';
const INPUT_ID_USER_CONFIRM_PASSWORD = 'common-user-input-confirm-password';
const INPUT_ID_USER_NAME = 'common-user-input-name';
const INPUT_ID_USER_PASSWORD = 'common-user-input-password';
const INPUT_LABEL_USER_CONFIRM_PASSWORD = 'Confirm password';
const INPUT_LABEL_USER_NAME = 'Username';
const INPUT_LABEL_USER_PASSWORD = 'Password';
const CommonUserInputGroup = <
M extends {
[K in
| typeof INPUT_ID_USER_CONFIRM_PASSWORD
| typeof INPUT_ID_USER_NAME
| typeof INPUT_ID_USER_PASSWORD]: string;
},
>({
formUtils: {
buildFinishInputTestBatchFunction,
buildInputFirstRenderFunction,
setMessage,
setValidity,
},
previous: { name: previousName } = {},
readOnlyUserName,
requirePassword = false,
showPasswordField,
}: CommonUserInputGroupProps<M>): ReactElement => {
const userPasswordInputRef = useRef<InputForwardedRefContent<'string'>>({});
const userConfirmPasswordInputRef = useRef<
InputForwardedRefContent<'string'>
>({});
const [requireConfirmPassword, setRequireConfirmPassword] =
useState<boolean>(requirePassword);
const userPasswordInputGroup = useMemo(
() =>
showPasswordField
? {
'common-user-input-cell-password': {
children: (
<InputWithRef
input={
<OutlinedInputWithLabel
id={INPUT_ID_USER_PASSWORD}
label={INPUT_LABEL_USER_PASSWORD}
type={INPUT_TYPES.password}
/>
}
inputTestBatch={buildPeacefulStringTestBatch(
INPUT_LABEL_USER_PASSWORD,
() => {
setMessage(INPUT_ID_USER_PASSWORD);
},
{
onFinishBatch: buildFinishInputTestBatchFunction(
INPUT_ID_USER_PASSWORD,
),
},
(message) => {
setMessage(INPUT_ID_USER_PASSWORD, { children: message });
},
)}
onBlurAppend={({ target: { value } }) => {
setRequireConfirmPassword(value.length > 0);
setValidity(
INPUT_ID_USER_CONFIRM_PASSWORD,
value ===
userConfirmPasswordInputRef.current.getValue?.call(
null,
),
);
}}
onFirstRender={buildInputFirstRenderFunction(
INPUT_ID_USER_PASSWORD,
)}
ref={userPasswordInputRef}
required={requirePassword}
/>
),
},
'common-user-input-cell-confirm-password': {
children: (
<InputWithRef
input={
<OutlinedInputWithLabel
id={INPUT_ID_USER_CONFIRM_PASSWORD}
inputProps={{ readOnly: !requireConfirmPassword }}
label={INPUT_LABEL_USER_CONFIRM_PASSWORD}
type={INPUT_TYPES.password}
/>
}
inputTestBatch={{
defaults: {
onSuccess: () => {
setMessage(INPUT_ID_USER_CONFIRM_PASSWORD);
},
},
onFinishBatch: buildFinishInputTestBatchFunction(
INPUT_ID_USER_CONFIRM_PASSWORD,
),
tests: [
{ test: testNotBlank },
{
onFailure: () => {
setMessage(INPUT_ID_USER_CONFIRM_PASSWORD, {
children: 'The passwords do not match.',
});
},
test: ({ value }) =>
value ===
userPasswordInputRef.current.getValue?.call(null),
},
],
}}
onFirstRender={buildInputFirstRenderFunction(
INPUT_ID_USER_CONFIRM_PASSWORD,
)}
ref={userConfirmPasswordInputRef}
required={requireConfirmPassword}
/>
),
},
}
: undefined,
[
buildFinishInputTestBatchFunction,
buildInputFirstRenderFunction,
requireConfirmPassword,
requirePassword,
setMessage,
setValidity,
showPasswordField,
],
);
return (
<Grid
columns={{ xs: 1, sm: 2, md: 3 }}
layout={{
'common-user-input-cell-name': {
children: (
<InputWithRef
input={
<OutlinedInputWithLabel
id={INPUT_ID_USER_NAME}
inputProps={{ readOnly: readOnlyUserName }}
label={INPUT_LABEL_USER_NAME}
value={previousName}
/>
}
inputTestBatch={buildPeacefulStringTestBatch(
INPUT_LABEL_USER_NAME,
() => {
setMessage(INPUT_ID_USER_NAME);
},
{
onFinishBatch:
buildFinishInputTestBatchFunction(INPUT_ID_USER_NAME),
},
(message) => {
setMessage(INPUT_ID_USER_NAME, { children: message });
},
)}
onFirstRender={buildInputFirstRenderFunction(INPUT_ID_USER_NAME)}
required
/>
),
md: 1,
sm: 2,
},
...userPasswordInputGroup,
}}
spacing="1em"
/>
);
};
export {
INPUT_ID_USER_CONFIRM_PASSWORD,
INPUT_ID_USER_NAME,
INPUT_ID_USER_PASSWORD,
};
export default CommonUserInputGroup;

@ -1,51 +1,256 @@
import { FC, useEffect } from 'react';
import { FC, useMemo, useRef, useState } from 'react';
import api from '../../lib/api';
import API_BASE_URL from '../../lib/consts/API_BASE_URL';
import CommonUserInputGroup, {
INPUT_ID_USER_CONFIRM_PASSWORD,
INPUT_ID_USER_NAME,
INPUT_ID_USER_PASSWORD,
} from './CommonUserInputGroup';
import ConfirmDialog from '../ConfirmDialog';
import FormDialog from '../FormDialog';
import FormSummary from '../FormSummary';
import handleAPIError from '../../lib/handleAPIError';
import List from '../List';
import MessageBox, { Message } from '../MessageBox';
import MessageGroup, { MessageGroupForwardedRefContent } from '../MessageGroup';
import { ExpandablePanel } from '../Panels';
import { BodyText } from '../Text';
import useProtect from '../../hooks/useProtect';
import useChecklist from '../../hooks/useChecklist';
import useConfirmDialogProps from '../../hooks/useConfirmDialogProps';
import useFormUtils from '../../hooks/useFormUtils';
import useProtectedState from '../../hooks/useProtectedState';
import periodicFetch from '../../lib/fetchers/periodicFetch';
const getFormEntries = (
...[{ target }]: DivFormEventHandlerParameters
): CreateUserRequestBody => {
const { elements } = target as HTMLFormElement;
const { value: userName } = elements.namedItem(
INPUT_ID_USER_NAME,
) as HTMLInputElement;
const inputUserPassword = elements.namedItem(INPUT_ID_USER_PASSWORD);
let password = '';
if (inputUserPassword) {
({ value: password } = inputUserPassword as HTMLInputElement);
}
return { password, userName };
};
const ManageUsersForm: FC = () => {
const { protect } = useProtect();
const addUserFormDialogRef = useRef<ConfirmDialogForwardedRefContent>({});
const confirmDialogRef = useRef<ConfirmDialogForwardedRefContent>({});
const editUserFormDialogRef = useRef<ConfirmDialogForwardedRefContent>({});
const messageGroupRef = useRef<MessageGroupForwardedRefContent>({});
const [confirmDialogProps, setConfirmDialogProps] = useConfirmDialogProps();
const [editUsers, setEditUsers] = useState<boolean>(false);
const [listMessage, setListMessage] = useProtectedState<Message>({
children: `No users found.`,
});
const [userDetail, setUserDetail] = useProtectedState<
UserOverviewMetadata | undefined
>(undefined);
const { data: users, isLoading: loadingUsers } =
periodicFetch<UserOverviewMetadataList>(`${API_BASE_URL}/user`, {
onError: (error) => {
setListMessage(handleAPIError(error));
},
});
const formUtils = useFormUtils(
[
INPUT_ID_USER_CONFIRM_PASSWORD,
INPUT_ID_USER_NAME,
INPUT_ID_USER_PASSWORD,
],
messageGroupRef,
);
const { isFormInvalid, isFormSubmitting, submitForm } = formUtils;
const { buildDeleteDialogProps, checks, getCheck, hasChecks, setCheck } =
useChecklist();
const { userName: udetailName, userUUID: udetailUuid } = useMemo<
Partial<UserOverviewMetadata>
>(() => userDetail ?? {}, [userDetail]);
const [listMessage, setListMessage] = useProtectedState<Message>(
{ children: `No users found.` },
protect,
const addUserFormDialogProps = useMemo<ConfirmDialogProps>(
() => ({
actionProceedText: 'Add',
content: (
<CommonUserInputGroup
formUtils={formUtils}
requirePassword
showPasswordField
/>
),
onSubmitAppend: (...args) => {
const body = getFormEntries(...args);
setConfirmDialogProps({
actionProceedText: 'Add',
content: <FormSummary entries={body} hasPassword />,
onProceedAppend: () => {
submitForm({
body,
getErrorMsg: (parentMsg) => <>Add user failed. {parentMsg}</>,
method: 'post',
successMsg: `Created user ${body.userName}.`,
url: '/user',
});
},
titleText: `Add the following new user?`,
});
confirmDialogRef.current.setOpen?.call(null, true);
},
titleText: 'Add a web interface user',
}),
[formUtils, setConfirmDialogProps, submitForm],
);
const [users, setUsers] = useProtectedState<
UserOverviewMetadataList | undefined
>(undefined, protect);
useEffect(() => {
if (!users) {
api
.get<UserOverviewMetadataList>('/user')
.then(({ data }) => {
setUsers(data);
})
.catch((error) => {
// Initialize to prevent infinite fetch.
setUsers({});
setListMessage(handleAPIError(error));
const editUserFormDialogProps = useMemo<ConfirmDialogProps>(
() => ({
actionProceedText: 'Edit',
content: (
<CommonUserInputGroup
formUtils={formUtils}
previous={{ name: udetailName }}
readOnlyUserName={udetailName === 'admin'}
showPasswordField
/>
),
onSubmitAppend: (...args) => {
const body = getFormEntries(...args);
setConfirmDialogProps({
actionProceedText: 'Update',
content: <FormSummary entries={body} hasPassword />,
onProceedAppend: () => {
submitForm({
body,
getErrorMsg: (parentMsg) => <>Update user failed. {parentMsg}</>,
method: 'put',
successMsg: `Updated user ${udetailName}`,
url: `/user/${udetailUuid}`,
});
},
titleText: `Update user ${udetailName} with the following?`,
});
}
}, [setListMessage, setUsers, users]);
confirmDialogRef.current.setOpen?.call(null, true);
},
titleText: `Edit user ${udetailName}`,
}),
[formUtils, setConfirmDialogProps, submitForm, udetailName, udetailUuid],
);
const messageArea = useMemo(
() => (
<MessageGroup
count={1}
defaultMessageType="warning"
ref={messageGroupRef}
/>
),
[],
);
const allowModOthers = useMemo<boolean>(
() => users?.current?.userName === 'admin',
[users],
);
return (
<ExpandablePanel header="Manage users" loading={!users}>
<List
allowEdit={false}
listEmpty={<MessageBox {...listMessage} />}
listItems={users}
renderListItem={(userUUID, { userName }) => (
<BodyText>{userName}</BodyText>
)}
<>
<ExpandablePanel header="Manage users" loading={loadingUsers}>
<List
allowAddItem={allowModOthers}
allowDelete={allowModOthers}
allowEdit
allowItemButton={editUsers}
disableDelete={!hasChecks}
edit={editUsers}
getListItemCheckboxProps={(key, { userName }) => ({
disabled: userName === 'admin',
})}
header
listEmpty={<MessageBox {...listMessage} />}
listItems={users}
onAdd={() => {
addUserFormDialogRef.current.setOpen?.call(null, true);
}}
onDelete={() => {
setConfirmDialogProps(
buildDeleteDialogProps({
confirmDialogProps: {
onProceedAppend: () => {
submitForm({
body: { uuids: checks },
getErrorMsg: (parentMsg) => (
<>Delete user(s) failed. {parentMsg}</>
),
method: 'delete',
url: '/user',
});
},
},
formSummaryProps: {
renderEntry: (key) => (
<BodyText>{users?.[key].userName}</BodyText>
),
},
getConfirmDialogTitle: (length) =>
`Delete the following ${length} users?`,
}),
);
confirmDialogRef.current.setOpen?.call(null, true);
}}
onEdit={() => setEditUsers((previous) => !previous)}
onItemCheckboxChange={(key, { target: { checked } }) =>
setCheck(key, checked)
}
onItemClick={(value) => {
if (editUsers) {
setUserDetail(value);
editUserFormDialogRef.current.setOpen?.call(null, true);
}
}}
renderListItemCheckboxState={(key) => getCheck(key)}
renderListItem={(userUUID, { userName }) => (
<BodyText>{userName}</BodyText>
)}
/>
</ExpandablePanel>
<FormDialog
{...addUserFormDialogProps}
disableProceed={isFormInvalid}
loadingAction={isFormSubmitting}
preActionArea={messageArea}
ref={addUserFormDialogRef}
/>
<FormDialog
{...editUserFormDialogProps}
disableProceed={isFormInvalid}
loadingAction={isFormSubmitting}
preActionArea={messageArea}
ref={editUserFormDialogRef}
/>
<ConfirmDialog
closeOnProceed
{...confirmDialogProps}
ref={confirmDialogRef}
/>
</ExpandablePanel>
</>
);
};

@ -6,3 +6,8 @@ type UserOverviewMetadata = {
type UserOverviewMetadataList = {
[userUUID: string]: UserOverviewMetadata;
};
type CreateUserRequestBody = {
userName: string;
password: string;
};

@ -0,0 +1,11 @@
type CommonUserInputGroupOptionalProps = {
previous?: { name?: string; password?: string };
readOnlyUserName?: boolean;
requirePassword?: boolean;
showPasswordField?: boolean;
};
type CommonUserInputGroupProps<M extends MapToInputTestID> =
CommonUserInputGroupOptionalProps & {
formUtils: FormUtils<M>;
};
Loading…
Cancel
Save