import { dSizeStr } from 'format-data-size'; import { FC, useCallback, useMemo, useRef, useState } from 'react'; import API_BASE_URL from '../../lib/consts/API_BASE_URL'; import { UPLOAD_FILE_TYPES } from '../../lib/consts/UPLOAD_FILE_TYPES'; import AddFileForm from './AddFileForm'; import api from '../../lib/api'; import ConfirmDialog from '../ConfirmDialog'; import { DialogWithHeader } from '../Dialog'; import Divider from '../Divider'; import EditFileForm from './EditFileForm'; import FlexBox from '../FlexBox'; import handleAPIError from '../../lib/handleAPIError'; import List from '../List'; import MessageGroup, { MessageGroupForwardedRefContent } from '../MessageGroup'; import { Panel, PanelHeader } from '../Panels'; import periodicFetch from '../../lib/fetchers/periodicFetch'; import Spinner from '../Spinner'; import { BodyText, HeaderText, MonoText } from '../Text'; import useChecklist from '../../hooks/useChecklist'; import useConfirmDialogProps from '../../hooks/useConfirmDialogProps'; import useFetch from '../../hooks/useFetch'; import useProtectedState from '../../hooks/useProtectedState'; const toAnvilOverviewHostList = ( data: APIAnvilOverviewArray[number]['hosts'], ) => data.reduce( (previous, { hostName: name, hostType: type, hostUUID: uuid }) => { previous[uuid] = { name, type, uuid }; return previous; }, {}, ); const toAnvilOverviewList = (data: APIAnvilOverviewArray) => data.reduce( ( previous, { anvilDescription: description, anvilName: name, anvilUUID: uuid, hosts, }, ) => { previous[uuid] = { description, hosts: toAnvilOverviewHostList(hosts), name, uuid, }; return previous; }, {}, ); const toFileOverviewList = (rows: string[][]) => rows.reduce((previous, row) => { const [uuid, name, size, type, checksum] = row; previous[uuid] = { checksum, name, size, type: type as FileType, uuid, }; return previous; }, {}); const toFileDetail = (rows: string[][]) => { const { 0: first } = rows; if (!first) return undefined; const [uuid, name, size, type, checksum] = first; return rows.reduce( (previous, row) => { const { 5: locationUuid, 6: locationActive, 7: anvilUuid, 8: anvilName, 9: anvilDescription, 10: hostUuid, 11: hostName, 12: hostType, } = row; if (!previous.anvils[anvilUuid]) { previous.anvils[anvilUuid] = { description: anvilDescription, locationUuids: [], name: anvilName, uuid: anvilUuid, }; } if (!previous.hosts[hostUuid]) { previous.hosts[hostUuid] = { locationUuids: [], name: hostName, type: hostType, uuid: hostUuid, }; } if (hostType === 'dr') { previous.hosts[hostUuid].locationUuids.push(locationUuid); } else { previous.anvils[anvilUuid].locationUuids.push(locationUuid); } const active = Number(locationActive) === 1; previous.locations[locationUuid] = { anvilUuid, active, hostUuid, uuid: locationUuid, }; return previous; }, { anvils: {}, checksum, hosts: {}, locations: {}, name, size, type: type as FileType, uuid, }, ); }; const ManageFilePanel: FC = () => { const addFormDialogRef = useRef(null); const confirmDialogRef = useRef({}); const editFormDialogRef = useRef(null); const messageGroupRef = useRef({}); const [confirmDialogProps, setConfirmDialogProps] = useConfirmDialogProps(); const [edit, setEdit] = useState(false); const [file, setFile] = useProtectedState( undefined, ); const [loadingFile, setLoadingFile] = useProtectedState(false); const { data: rows, isLoading: loadingFiles } = periodicFetch( `${API_BASE_URL}/file`, ); const files = useMemo( () => (rows ? toFileOverviewList(rows) : undefined), [rows], ); const { buildDeleteDialogProps, checks, getCheck, hasAllChecks, hasChecks, multipleItems, setAllChecks, setCheck, } = useChecklist({ list: files, }); const setApiMessage = useCallback( (message: Message) => messageGroupRef.current.setMessage?.call(null, 'api', message), [], ); const getFileDetail = useCallback( (fileUuid: string) => { setLoadingFile(true); api .get(`file/${fileUuid}`) .then(({ data }) => { setFile(toFileDetail(data)); }) .catch((error) => { const emsg = handleAPIError(error); emsg.children = <>Failed to get file detail. {emsg.children}; setApiMessage(emsg); }) .finally(() => { setLoadingFile(false); }); }, [setApiMessage, setFile, setLoadingFile], ); const { data: rawAnvils, loading: loadingAnvils } = useFetch('/anvil', { onError: (error) => { setApiMessage({ children: <>Failed to get node list. {error}, type: 'warning', }); }, }); const anvils = useMemo( () => rawAnvils && toAnvilOverviewList(rawAnvils), [rawAnvils], ); const { data: drHosts, loading: loadingDrHosts } = useFetch('/host?types=dr', { onError: (error) => { setApiMessage({ children: <>Failed to get DR host list. {error}, type: 'warning', }); }, }); const list = useMemo( () => ( ({ checked: hasAllChecks, onChange: (event, checked) => { setAllChecks(checked); }, })} getListItemCheckboxProps={(uuid) => ({ checked: getCheck(uuid), onChange: (event, checked) => { setCheck(uuid, checked); }, })} header listEmpty="No file(s) found." listItems={files} onAdd={() => { addFormDialogRef.current?.setOpen(true); }} onDelete={() => { setConfirmDialogProps( buildDeleteDialogProps({ onProceedAppend: () => { checks.forEach((fileUuid) => api.delete(`/file/${fileUuid}`)); }, getConfirmDialogTitle: (count) => `Delete the following ${count} file(s)?`, renderEntry: ({ key }) => ( {files?.[key].name} ), }), ); confirmDialogRef.current.setOpen?.call(null, true); }} onEdit={() => { setEdit((previous) => !previous); }} onItemClick={(value, uuid) => { editFormDialogRef.current?.setOpen(true); getFileDetail(uuid); }} renderListItem={(uuid, { checksum, name, size, type }) => ( {name} {UPLOAD_FILE_TYPES.get(type)?.[1]} {dSizeStr(size, { toUnit: 'ibyte' })} {checksum} )} /> ), [ buildDeleteDialogProps, checks, edit, files, getCheck, getFileDetail, hasAllChecks, hasChecks, multipleItems, setAllChecks, setCheck, setConfirmDialogProps, ], ); const panelContent = useMemo( () => (loadingFiles ? : list), [loadingFiles, list], ); const messageArea = useMemo( () => ( ), [], ); const loadingAddForm = useMemo( () => loadingFiles || loadingAnvils || loadingDrHosts, [loadingAnvils, loadingDrHosts, loadingFiles], ); const loadingEditForm = useMemo( () => loadingFiles || loadingAnvils || loadingDrHosts || loadingFile, [loadingAnvils, loadingDrHosts, loadingFile, loadingFiles], ); const addForm = useMemo( () => anvils && drHosts && , [anvils, drHosts], ); const editForm = useMemo( () => anvils && drHosts && file && ( ), [anvils, drHosts, file], ); return ( <> Files {messageArea} {panelContent} {addForm} {editForm} ); }; export default ManageFilePanel;