fix(striker-ui): add (un)check all to checklist hook, apply to manage SSH key conflicts

main
Tsu-ba-me 2 years ago
parent 54197a2f2c
commit a1a2a043a8
  1. 104
      striker-ui/components/List.tsx
  2. 77
      striker-ui/components/StrikerConfig/ManageChangedSSHKeysForm.tsx
  3. 8
      striker-ui/components/StrikerConfig/ManageUsersForm.tsx
  4. 32
      striker-ui/hooks/useChecklist.tsx
  5. 2
      striker-ui/types/Checklist.d.ts
  6. 6
      striker-ui/types/List.d.ts
  7. 1
      striker-ui/types/ManageChangedSSHKeysForm.d.ts

@ -13,15 +13,7 @@ import {
SxProps,
Theme,
} from '@mui/material';
import {
FC,
ForwardedRef,
forwardRef,
useCallback,
useImperativeHandle,
useMemo,
useState,
} from 'react';
import { FC, forwardRef, useCallback, useMemo } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { BLUE, BORDER_RADIUS, GREY, RED } from '../lib/consts/DEFAULT_THEME';
@ -33,44 +25,39 @@ import IconButton from './IconButton';
import { BodyText } from './Text';
const List = forwardRef(
<T,>(
{
allowCheckAll: isAllowCheckAll = false,
allowEdit: isAllowEdit = false,
allowItemButton: isAllowItemButton = false,
disableDelete = false,
edit: isEdit = false,
flexBoxProps,
getListItemCheckboxProps,
header,
headerSpacing = '.3em',
initialCheckAll = false,
insertHeader: isInsertHeader = true,
listEmpty,
listItemIconMinWidth = '56px',
listItemKeyPrefix = uuidv4(),
listItemProps: { sx: listItemSx, ...restListItemProps } = {},
listItems,
listProps: { sx: listSx, ...restListProps } = {},
onAdd,
onDelete,
onEdit,
onAllCheckboxChange,
onItemCheckboxChange,
onItemClick,
renderListItem = (key) => <BodyText>{key}</BodyText>,
renderListItemCheckboxState,
scroll: isScroll = false,
// Input props that depend on other input props.
allowAddItem: isAllowAddItem = isAllowEdit,
allowCheckItem: isAllowCheckItem = isAllowEdit,
allowDelete: isAllowDelete = isAllowEdit,
allowEditItem: isAllowEditItem = isAllowEdit,
}: ListProps<T>,
ref: ForwardedRef<ListForwardedRefContent>,
) => {
const [isCheckAll, setIsCheckAll] = useState<boolean>(initialCheckAll);
<T,>({
allowCheckAll: isAllowCheckAll = false,
allowEdit: isAllowEdit = false,
allowItemButton: isAllowItemButton = false,
disableDelete = false,
edit: isEdit = false,
flexBoxProps,
getListCheckboxProps,
getListItemCheckboxProps,
header,
headerSpacing = '.3em',
insertHeader: isInsertHeader = true,
listEmpty,
listItemIconMinWidth = '56px',
listItemKeyPrefix = uuidv4(),
listItemProps: { sx: listItemSx, ...restListItemProps } = {},
listItems,
listProps: { sx: listSx, ...restListProps } = {},
onAdd,
onDelete,
onEdit,
onAllCheckboxChange,
onItemCheckboxChange,
onItemClick,
renderListItem = (key) => <BodyText>{key}</BodyText>,
renderListItemCheckboxState,
scroll: isScroll = false,
// Input props that depend on other input props.
allowAddItem: isAllowAddItem = isAllowEdit,
allowCheckItem: isAllowCheckItem = isAllowEdit,
allowDelete: isAllowDelete = isAllowEdit,
allowEditItem: isAllowEditItem = isAllowEdit,
}: ListProps<T>) => {
const checkAllMinWidth = useMemo(
() => `calc(${listItemIconMinWidth} - ${headerSpacing})`,
[headerSpacing, listItemIconMinWidth],
@ -122,14 +109,9 @@ const List = forwardRef(
element = isAllowCheckAll ? (
<MUIBox sx={{ minWidth: checkAllMinWidth }}>
<Checkbox
checked={isCheckAll}
edge="start"
onChange={(...args) => {
const [, isChecked] = args;
onAllCheckboxChange?.call(null, ...args);
setIsCheckAll(isChecked);
}}
onChange={onAllCheckboxChange}
{...getListCheckboxProps?.call(null)}
/>
</MUIBox>
) : (
@ -140,9 +122,9 @@ const List = forwardRef(
return element;
}, [
checkAllMinWidth,
getListCheckboxProps,
isAllowCheckAll,
isAllowCheckItem,
isCheckAll,
isEdit,
onAllCheckboxChange,
]);
@ -261,14 +243,6 @@ const List = forwardRef(
[isScroll],
);
useImperativeHandle(
ref,
() => ({
setCheckAll: (value) => setIsCheckAll(value),
}),
[],
);
return (
<FlexBox spacing={0} {...flexBoxProps}>
{headerElement}
@ -285,6 +259,4 @@ const List = forwardRef(
List.displayName = 'List';
export default List as <T>(
props: ListProps<T> & { ref?: ForwardedRef<ListForwardedRefContent> },
) => ReturnType<FC<ListProps<T>>>;
export default List as <T>(props: ListProps<T>) => ReturnType<FC<ListProps<T>>>;

@ -13,35 +13,30 @@ import MessageBox, { Message } from '../MessageBox';
import { ExpandablePanel } from '../Panels';
import periodicFetch from '../../lib/fetchers/periodicFetch';
import { BodyText } from '../Text';
import useProtect from '../../hooks/useProtect';
import useChecklist from '../../hooks/useChecklist';
import useProtectedState from '../../hooks/useProtectedState';
const ManageChangedSSHKeysForm: FC<ManageChangedSSHKeysFormProps> = ({
mitmExternalHref = 'https://en.wikipedia.org/wiki/Man-in-the-middle_attack',
refreshInterval = 60000,
}) => {
const { protect } = useProtect();
const confirmDialogRef = useRef<ConfirmDialogForwardedRefContent>({});
const listRef = useRef<ListForwardedRefContent>({});
const [apiMessage, setAPIMessage] = useProtectedState<Message | undefined>(
undefined,
protect,
);
const [changedSSHKeys, setChangedSSHKeys] = useProtectedState<ChangedSSHKeys>(
{},
protect,
);
const [confirmDialogProps, setConfirmDialogProps] =
useProtectedState<ConfirmDialogProps>(
{
actionProceedText: '',
content: '',
titleText: '',
},
protect,
);
useProtectedState<ConfirmDialogProps>({
actionProceedText: '',
content: '',
titleText: '',
});
const { checks, getCheck, hasAllChecks, hasChecks, setAllChecks, setCheck } =
useChecklist({ list: changedSSHKeys });
const apiMessageElement = useMemo(
() => apiMessage && <MessageBox {...apiMessage} />,
@ -140,40 +135,40 @@ const ManageChangedSSHKeysForm: FC<ManageChangedSSHKeysFormProps> = ({
allowCheckItem
allowDelete
allowEdit={false}
disableDelete={!hasChecks}
edit
getListCheckboxProps={() => ({
checked: hasAllChecks,
})}
listEmpty={
<BodyText align="center">No conflicting keys found.</BodyText>
}
listItems={changedSSHKeys}
onAllCheckboxChange={(event, isChecked) => {
Object.keys(changedSSHKeys).forEach((key) => {
changedSSHKeys[key].isChecked = isChecked;
});
setChangedSSHKeys((previous) => ({ ...previous }));
onAllCheckboxChange={(event, checked) => {
setAllChecks(checked);
}}
onDelete={() => {
let deleteCount = 0;
const deleteRequestBody = Object.entries(changedSSHKeys).reduce<{
const deleteRequestBody = checks.reduce<{
[hostUUID: string]: string[];
}>((previous, [stateUUID, { hostUUID, isChecked }]) => {
if (isChecked) {
if (!previous[hostUUID]) {
previous[hostUUID] = [];
}
}>((previous, stateUUID) => {
const checked = getCheck(stateUUID);
previous[hostUUID].push(stateUUID);
if (!checked) return previous;
deleteCount += 1;
const { hostUUID } = changedSSHKeys[stateUUID];
if (!previous[hostUUID]) {
previous[hostUUID] = [];
}
previous[hostUUID].push(stateUUID);
return previous;
}, {});
setConfirmDialogProps({
actionProceedText: 'Delete',
content: `Resolve ${deleteCount} SSH key conflicts. Please make sure the identity change(s) are expected to avoid MITM attacks.`,
content: `Resolve ${checks.length} SSH key conflicts. Please make sure the identity change(s) are expected to avoid MITM attacks.`,
onProceedAppend: () => {
api
.delete('/ssh-key/conflict', { data: deleteRequestBody })
@ -186,22 +181,13 @@ const ManageChangedSSHKeysForm: FC<ManageChangedSSHKeysFormProps> = ({
});
},
proceedColour: 'red',
titleText: `Delete ${deleteCount} conflicting SSH keys?`,
titleText: `Delete ${checks.length} conflicting SSH keys?`,
});
confirmDialogRef.current.setOpen?.call(null, true);
}}
onItemCheckboxChange={(key, event, isChecked) => {
changedSSHKeys[key].isChecked = isChecked;
listRef.current.setCheckAll?.call(
null,
Object.values(changedSSHKeys).every(
({ isChecked: isItemChecked }) => isItemChecked,
),
);
setChangedSSHKeys((previous) => ({ ...previous }));
onItemCheckboxChange={(key, event, checked) => {
setCheck(key, checked);
}}
renderListItem={(hostUUID, { hostName, ipAddress }) => (
<FlexBox
@ -214,10 +200,7 @@ const ManageChangedSSHKeysForm: FC<ManageChangedSSHKeysFormProps> = ({
<BodyText>{ipAddress}</BodyText>
</FlexBox>
)}
renderListItemCheckboxState={(key, { isChecked }) =>
isChecked === true
}
ref={listRef}
renderListItemCheckboxState={(key) => getCheck(key)}
/>
</FlexBox>
{apiMessageElement}

@ -15,12 +15,12 @@ import List from '../List';
import MessageBox, { Message } from '../MessageBox';
import MessageGroup, { MessageGroupForwardedRefContent } from '../MessageGroup';
import { ExpandablePanel } from '../Panels';
import periodicFetch from '../../lib/fetchers/periodicFetch';
import { BodyText } from '../Text';
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
@ -76,7 +76,7 @@ const ManageUsersForm: FC = () => {
const { isFormInvalid, isFormSubmitting, submitForm } = formUtils;
const { buildDeleteDialogProps, checks, getCheck, hasChecks, setCheck } =
useChecklist();
useChecklist({ list: users });
const { userName: udetailName, userUUID: udetailUuid } = useMemo<
Partial<UserOverviewMetadata>
@ -216,9 +216,7 @@ const ManageUsersForm: FC = () => {
confirmDialogRef.current.setOpen?.call(null, true);
}}
onEdit={() => setEditUsers((previous) => !previous)}
onItemCheckboxChange={(key, { target: { checked } }) =>
setCheck(key, checked)
}
onItemCheckboxChange={(key, event, checked) => setCheck(key, checked)}
onItemClick={(value) => {
if (editUsers) {
setUserDetail(value);

@ -4,19 +4,32 @@ import buildObjectStateSetterCallback from '../lib/buildObjectStateSetterCallbac
import FormSummary from '../components/FormSummary';
const useChecklist = (): {
const useChecklist = ({
list = {},
}: {
list?: Record<string, unknown>;
}): {
buildDeleteDialogProps: BuildDeleteDialogPropsFunction;
checklist: Checklist;
checks: ArrayChecklist;
getCheck: GetCheckFunction;
hasAllChecks: boolean;
hasChecks: boolean;
multipleItems: boolean;
setAllChecks: SetAllChecksFunction;
setCheck: SetCheckFunction;
} => {
const [checklist, setChecklist] = useState<Checklist>({});
const listKeys = useMemo(() => Object.keys(list), [list]);
const checks = useMemo(() => Object.keys(checklist), [checklist]);
const hasAllChecks = useMemo(
() => checks.length === listKeys.length,
[checks.length, listKeys.length],
);
const hasChecks = useMemo(() => checks.length > 0, [checks.length]);
const multipleItems = useMemo(() => listKeys.length > 1, [listKeys.length]);
const buildDeleteDialogProps = useCallback<BuildDeleteDialogPropsFunction>(
({
@ -40,6 +53,20 @@ const useChecklist = (): {
[checklist],
);
const setAllChecks = useCallback<SetAllChecksFunction>(
(checked) =>
setChecklist(
listKeys.reduce<Checklist>((previous, key) => {
if (checked) {
previous[key] = checked;
}
return previous;
}, {}),
),
[listKeys],
);
const setCheck = useCallback<SetCheckFunction>(
(key, checked) =>
setChecklist(buildObjectStateSetterCallback(key, checked || undefined)),
@ -51,7 +78,10 @@ const useChecklist = (): {
checklist,
checks,
getCheck,
hasAllChecks,
hasChecks,
multipleItems,
setAllChecks,
setCheck,
};
};

@ -10,4 +10,6 @@ type BuildDeleteDialogPropsFunction = (args: {
type GetCheckFunction = (key: string) => boolean;
type SetAllChecksFunction = (checked?: boolean) => void;
type SetCheckFunction = (key: string, checked?: boolean) => void;

@ -16,10 +16,10 @@ type ListOptionalProps<T extends unknown = unknown> = {
disableDelete?: boolean;
edit?: boolean;
flexBoxProps?: import('../components/FlexBox').FlexBoxProps;
getListCheckboxProps?: () => CheckboxProps;
getListItemCheckboxProps?: (key: string, value: T) => CheckboxProps;
header?: import('react').ReactNode;
headerSpacing?: number | string;
initialCheckAll?: boolean;
insertHeader?: boolean;
listEmpty?: import('react').ReactNode;
listItemIconMinWidth?: number | string;
@ -46,7 +46,3 @@ type ListOptionalProps<T extends unknown = unknown> = {
};
type ListProps<T extends unknown = unknown> = ListOptionalProps<T>;
type ListForwardedRefContent = {
setCheckAll?: (value: boolean) => void;
};

@ -3,7 +3,6 @@ type ChangedSSHKeys = {
hostName: string;
hostUUID: string;
ipAddress: string;
isChecked?: boolean;
};
};

Loading…
Cancel
Save