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. 44
      striker-ui/components/List.tsx
  2. 67
      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, SxProps,
Theme, Theme,
} from '@mui/material'; } from '@mui/material';
import { import { FC, forwardRef, useCallback, useMemo } from 'react';
FC,
ForwardedRef,
forwardRef,
useCallback,
useImperativeHandle,
useMemo,
useState,
} from 'react';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { BLUE, BORDER_RADIUS, GREY, RED } from '../lib/consts/DEFAULT_THEME'; import { BLUE, BORDER_RADIUS, GREY, RED } from '../lib/consts/DEFAULT_THEME';
@ -33,18 +25,17 @@ import IconButton from './IconButton';
import { BodyText } from './Text'; import { BodyText } from './Text';
const List = forwardRef( const List = forwardRef(
<T,>( <T,>({
{
allowCheckAll: isAllowCheckAll = false, allowCheckAll: isAllowCheckAll = false,
allowEdit: isAllowEdit = false, allowEdit: isAllowEdit = false,
allowItemButton: isAllowItemButton = false, allowItemButton: isAllowItemButton = false,
disableDelete = false, disableDelete = false,
edit: isEdit = false, edit: isEdit = false,
flexBoxProps, flexBoxProps,
getListCheckboxProps,
getListItemCheckboxProps, getListItemCheckboxProps,
header, header,
headerSpacing = '.3em', headerSpacing = '.3em',
initialCheckAll = false,
insertHeader: isInsertHeader = true, insertHeader: isInsertHeader = true,
listEmpty, listEmpty,
listItemIconMinWidth = '56px', listItemIconMinWidth = '56px',
@ -66,11 +57,7 @@ const List = forwardRef(
allowCheckItem: isAllowCheckItem = isAllowEdit, allowCheckItem: isAllowCheckItem = isAllowEdit,
allowDelete: isAllowDelete = isAllowEdit, allowDelete: isAllowDelete = isAllowEdit,
allowEditItem: isAllowEditItem = isAllowEdit, allowEditItem: isAllowEditItem = isAllowEdit,
}: ListProps<T>, }: ListProps<T>) => {
ref: ForwardedRef<ListForwardedRefContent>,
) => {
const [isCheckAll, setIsCheckAll] = useState<boolean>(initialCheckAll);
const checkAllMinWidth = useMemo( const checkAllMinWidth = useMemo(
() => `calc(${listItemIconMinWidth} - ${headerSpacing})`, () => `calc(${listItemIconMinWidth} - ${headerSpacing})`,
[headerSpacing, listItemIconMinWidth], [headerSpacing, listItemIconMinWidth],
@ -122,14 +109,9 @@ const List = forwardRef(
element = isAllowCheckAll ? ( element = isAllowCheckAll ? (
<MUIBox sx={{ minWidth: checkAllMinWidth }}> <MUIBox sx={{ minWidth: checkAllMinWidth }}>
<Checkbox <Checkbox
checked={isCheckAll}
edge="start" edge="start"
onChange={(...args) => { onChange={onAllCheckboxChange}
const [, isChecked] = args; {...getListCheckboxProps?.call(null)}
onAllCheckboxChange?.call(null, ...args);
setIsCheckAll(isChecked);
}}
/> />
</MUIBox> </MUIBox>
) : ( ) : (
@ -140,9 +122,9 @@ const List = forwardRef(
return element; return element;
}, [ }, [
checkAllMinWidth, checkAllMinWidth,
getListCheckboxProps,
isAllowCheckAll, isAllowCheckAll,
isAllowCheckItem, isAllowCheckItem,
isCheckAll,
isEdit, isEdit,
onAllCheckboxChange, onAllCheckboxChange,
]); ]);
@ -261,14 +243,6 @@ const List = forwardRef(
[isScroll], [isScroll],
); );
useImperativeHandle(
ref,
() => ({
setCheckAll: (value) => setIsCheckAll(value),
}),
[],
);
return ( return (
<FlexBox spacing={0} {...flexBoxProps}> <FlexBox spacing={0} {...flexBoxProps}>
{headerElement} {headerElement}
@ -285,6 +259,4 @@ const List = forwardRef(
List.displayName = 'List'; List.displayName = 'List';
export default List as <T>( export default List as <T>(props: ListProps<T>) => ReturnType<FC<ListProps<T>>>;
props: ListProps<T> & { ref?: ForwardedRef<ListForwardedRefContent> },
) => ReturnType<FC<ListProps<T>>>;

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

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

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

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

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

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

Loading…
Cancel
Save