diff --git a/plugins/lime-plugin-mesh-wide-config/src/components/Components.tsx b/plugins/lime-plugin-mesh-wide-config/src/components/Components.tsx index 9c802d2b..85881c2b 100644 --- a/plugins/lime-plugin-mesh-wide-config/src/components/Components.tsx +++ b/plugins/lime-plugin-mesh-wide-config/src/components/Components.tsx @@ -53,9 +53,21 @@ export const EditOrDelete = ({ }: { onEdit: (e) => void; onDelete: (e) => void; -}) => ( -
- - -
-); +}) => { + const runCb = (e, cb) => { + e.stopPropagation(); + cb(); + }; + return ( +
+ runCb(e, onEdit)} + /> + runCb(e, onDelete)} + /> +
+ ); +}; diff --git a/plugins/lime-plugin-mesh-wide-config/src/components/ConfigSection.tsx b/plugins/lime-plugin-mesh-wide-config/src/components/ConfigSection.tsx index 0acd58bb..0862d800 100644 --- a/plugins/lime-plugin-mesh-wide-config/src/components/ConfigSection.tsx +++ b/plugins/lime-plugin-mesh-wide-config/src/components/ConfigSection.tsx @@ -1,17 +1,19 @@ import { Trans } from "@lingui/macro"; +import { useDisclosure } from "components/Modal/useDisclosure"; import { Button } from "components/buttons/button"; import { Collapsible } from "components/collapsible"; import { useToast } from "components/toast/toastProvider"; +import { EditOrDelete } from "plugins/lime-plugin-mesh-wide-config/src/components/Components"; import { OptionContainer } from "plugins/lime-plugin-mesh-wide-config/src/components/OptionForm"; import { - useAddNewSectionModal, - useDeletePropModal, - useEditPropModal, + AddNewSectionFormProps, + AddNewSectionModal, + DeletePropModal, + EditPropModal, } from "plugins/lime-plugin-mesh-wide-config/src/components/modals"; import { IMeshWideSection } from "plugins/lime-plugin-mesh-wide-config/src/meshConfigTypes"; -import { EditOrDelete } from "plugins/lime-plugin-mesh-wide/src/components/Components"; export const ConfigSection = ({ dropdown }: { dropdown: IMeshWideSection }) => { return ( @@ -28,77 +30,92 @@ export const ConfigSection = ({ dropdown }: { dropdown: IMeshWideSection }) => { }; export const SectionEditOrDelete = ({ name }) => { - const { toggleModal: toggleDeleteModal, actionModal: deletePropModal } = - useDeletePropModal(); - const { toggleModal: toggleEditModal, actionModal: editPropertyModal } = - useEditPropModal(); - + const { + open: isEditOpen, + onOpen: openEdit, + onClose: onCloseEdit, + } = useDisclosure(); + const { + open: isDeleteModalOpen, + onOpen: openDeleteModal, + onClose: onCloseDeleteModal, + } = useDisclosure(); const { showToast } = useToast(); + const onEdit = async () => { + console.log("edit stuff"); + onCloseEdit(); + showToast({ + text: ( + + Edited {name} - {new Date().toDateString()} + + ), + onAction: () => { + console.log("Undo action"); + }, + }); + }; + + const onDelete = async () => { + console.log("delete stuff"); + onCloseDeleteModal(); + showToast({ + text: ( + + Deleted {name} - {new Date().toDateString()} + + ), + onAction: () => { + console.log("Undo action"); + }, + }); + }; + return ( - { - e.stopPropagation(); - editPropertyModal(name, () => { - console.log("edit stuff"); - toggleEditModal(); - showToast({ - text: ( - - Edited {name} - {new Date().toDateString()} - - ), - onAction: () => { - console.log("Undo action"); - }, - }); - }); - }} - onDelete={(e) => { - e.stopPropagation(); - deletePropModal(name, () => { - console.log("delete stuff"); - toggleDeleteModal(); - showToast({ - text: ( - - Deleted {name} - {new Date().toDateString()} - - ), - onAction: () => { - console.log("Undo action"); - }, - }); - }); - }} - /> + <> + + + + ); }; export const AddNewSectionBtn = () => { - const { toggleModal: toggleNewSectionModal, actionModal: addSectionModal } = - useAddNewSectionModal(); - + const { open, onOpen, onClose } = useDisclosure(); const { showToast } = useToast(); + + const onSuccess = (data: AddNewSectionFormProps) => { + console.log(`Added`, data); + onClose(); + showToast({ + text: ( + + Added section {data.name} - {new Date().toDateString()} + + ), + }); + }; return ( - + <> + + + ); }; diff --git a/plugins/lime-plugin-mesh-wide-config/src/components/OptionForm.tsx b/plugins/lime-plugin-mesh-wide-config/src/components/OptionForm.tsx index df5dbb9f..5e805fb8 100644 --- a/plugins/lime-plugin-mesh-wide-config/src/components/OptionForm.tsx +++ b/plugins/lime-plugin-mesh-wide-config/src/components/OptionForm.tsx @@ -2,16 +2,17 @@ import { Trans } from "@lingui/macro"; import { useState } from "preact/hooks"; import { SubmitHandler, useForm } from "react-hook-form"; +import { useDisclosure } from "components/Modal/useDisclosure"; import { Button } from "components/buttons/button"; import Divider from "components/divider"; import InputField from "components/inputs/InputField"; import { useToast } from "components/toast/toastProvider"; +import { EditOrDelete } from "plugins/lime-plugin-mesh-wide-config/src/components/Components"; import { - useDeletePropModal, - useEditPropModal, + DeletePropModal, + EditPropModal, } from "plugins/lime-plugin-mesh-wide-config/src/components/modals"; -import { EditOrDelete } from "plugins/lime-plugin-mesh-wide/src/components/Components"; const EditOptionForm = ({ keyString, @@ -63,16 +64,31 @@ export const OptionContainer = ({ keyString: string; value: string; }) => { + const { + open: isDeleteModalOpen, + onOpen: openDeleteModal, + onClose: onCloseDeleteModal, + } = useDisclosure(); + const { showToast } = useToast(); + + const onDelete = async () => { + console.log("delete stuff"); + onCloseDeleteModal(); + showToast({ + text: ( + + Deleted {keyString} - {new Date().toDateString()} + + ), + onAction: () => { + console.log("Undo action"); + }, + }); + }; const [isEditing, setIsEditing] = useState(false); const toggleIsEditing = () => setIsEditing(!isEditing); - const { toggleModal: toggleDeleteModal, actionModal: deletePropModal } = - useDeletePropModal(); - const { toggleModal: toggleEditModal, actionModal: editPropertyModal } = - useEditPropModal(); - const { showToast } = useToast(); - return (
@@ -89,23 +105,7 @@ export const OptionContainer = ({
{keyString}
{ - e.stopPropagation(); - deletePropModal(keyString, () => { - console.log("delete stuff"); - toggleDeleteModal(); - showToast({ - text: ( - - Deleted {keyString} - - ), - onAction: () => { - console.log("Undo action"); - }, - }); - }); - }} + onDelete={openDeleteModal} />
{value}
@@ -115,18 +115,21 @@ export const OptionContainer = ({ keyString={keyString} value={value} onSubmit={(data) => { - editPropertyModal(keyString, () => { - console.log("edited stuff"); - toggleEditModal(); - toggleIsEditing(); - showToast({ - text: Edited {keyString}, - }); + console.log("edited stuff", data); + toggleIsEditing(); + showToast({ + text: Edited {data.key}, }); }} /> )}
+ ); }; diff --git a/plugins/lime-plugin-mesh-wide-config/src/components/modals.tsx b/plugins/lime-plugin-mesh-wide-config/src/components/modals.tsx index bd0ec925..82ee1fd7 100644 --- a/plugins/lime-plugin-mesh-wide-config/src/components/modals.tsx +++ b/plugins/lime-plugin-mesh-wide-config/src/components/modals.tsx @@ -1,87 +1,75 @@ import { Trans } from "@lingui/macro"; -import { ComponentChildren } from "preact"; -import { useCallback } from "preact/compat"; import { useForm } from "react-hook-form"; -import { ModalActions, useModal } from "components/Modal/Modal"; +import { Modal, ModalProps } from "components/Modal/Modal"; import InputField from "components/inputs/InputField"; -import { dataTypeNameMapping } from "plugins/lime-plugin-mesh-wide/src/lib/utils"; -import { MeshWideMapDataTypeKeys } from "plugins/lime-plugin-mesh-wide/src/meshWideTypes"; +export const DeletePropModal = ({ + prop, + ...rest +}: { prop: string } & Pick) => ( + Delete property} {...rest}> +
+ + Are you sure you want to delete the {prop}{" "} + property? + +
+
+); -const useActionModal = ( - title: ComponentChildren, - btnText: ComponentChildren, - actionName: ModalActions -) => { - const { toggleModal, setModalState } = useModal(); +export const EditPropModal = ({ + prop, + ...rest +}: { prop: string } & Pick) => ( + Edit property} + successBtnText={Edit} + {...rest} + cancelBtn + > +
+ + Are you sure you want to edit the {prop}{" "} + property? + +
+
+); - const actionModal = useCallback( - (prop: string, actionCb: () => void) => { - setModalState({ - content: ( -
- - Are you sure you want to {title} the{" "} - {prop} property? - -
- ), - title, - [`${actionName}Cb`]: actionCb, - [`${actionName}BtnText`]: btnText, - }); - toggleModal(); - }, - [actionName, btnText, setModalState, title, toggleModal] - ); - return { actionModal, toggleModal }; -}; - -export const useDeletePropModal = () => - useActionModal( - Delete property, - Delete, - "delete" - ); - -export const useEditPropModal = () => - useActionModal( - Edit property, - Edit, - "success" - ); - -export const useAddNewSectionModal = () => { - const { toggleModal, setModalState } = useModal(); +export interface AddNewSectionFormProps { + name: string; +} +export const AddNewSectionModal = ({ + onSuccess, + ...rest +}: { onSuccess: (data: AddNewSectionFormProps) => void } & Pick< + ModalProps, + "isOpen" | "onClose" +>) => { const { register, handleSubmit, formState: { errors }, - } = useForm({ + } = useForm({ defaultValues: { name: "" }, }); - const actionModal = useCallback( - (actionCb: (data) => void) => { - setModalState({ - content: ( -
- Name} - register={register} - /> -
- ), - title: Add new section, - successCb: handleSubmit(actionCb), - successBtnText: Add, - }); - toggleModal(); - }, - [handleSubmit, register, setModalState, toggleModal] + return ( + Add new section} + successBtnText={Add} + {...rest} + onSuccess={handleSubmit(onSuccess)} + > +
+ Name} + register={register} + /> +
+
); - return { actionModal, toggleModal }; }; diff --git a/plugins/lime-plugin-mesh-wide-upgrade/src/components/modals.tsx b/plugins/lime-plugin-mesh-wide-upgrade/src/components/modals.tsx index 3357844d..efa5464a 100644 --- a/plugins/lime-plugin-mesh-wide-upgrade/src/components/modals.tsx +++ b/plugins/lime-plugin-mesh-wide-upgrade/src/components/modals.tsx @@ -1,61 +1,65 @@ import { Trans } from "@lingui/macro"; -import { VNode } from "preact"; +import { ComponentChildren, VNode } from "preact"; import { useCallback } from "react"; -import { useModal } from "components/Modal/Modal"; +import { + CallbackFn, + Modal, + ModalProps, + useModal, +} from "components/Modal/Modal"; -interface IUseParallelQueriesModalProps { - useSuccessBtn?: boolean; - cb?: (e) => void; - title?: VNode; - content?: VNode; - btnTxt?: VNode; -} +import { useMeshUpgrade } from "plugins/lime-plugin-mesh-wide-upgrade/src/hooks/meshWideUpgradeProvider"; +import { + useParallelConfirmUpgrade, + useParallelScheduleUpgrade, +} from "plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeQueries"; -const useParallelQueriesModal = ({ - useSuccessBtn, - cb, - title, - content, - btnTxt = Schedule, -}: IUseParallelQueriesModalProps) => { - const { toggleModal, setModalState } = useModal(); - const runAndClose = useCallback(() => { - cb(null); - toggleModal(); - }, [cb, toggleModal]); +type IUseParallelQueriesModalProps = { + isSuccess: boolean; +} & Pick; - const showModal = useCallback(() => { - setModalState({ - content, - title, - successCb: useSuccessBtn ? runAndClose : undefined, - deleteCb: !useSuccessBtn ? runAndClose : undefined, - successBtnText: btnTxt, - deleteBtnText: btnTxt, - }); - toggleModal(); - }, [ - setModalState, - content, - title, - useSuccessBtn, - runAndClose, - btnTxt, - toggleModal, - ]); - return { showModal, toggleModal }; +export const ParallelQueriesModal = ({ + children, + isSuccess, + cb, + ...rest +}: { + cb: CallbackFn; + children: ComponentChildren; +} & IUseParallelQueriesModalProps & + Pick) => { + let props: Partial< + Pick< + ModalProps, + "onSuccess" | "onDelete" | "successBtnText" | "deleteBtnText" + > + > = { + onSuccess: cb, + successBtnText: Schedule, + }; + if (!isSuccess) { + props = { + onDelete: cb, + deleteBtnText: Schedule, + }; + } + return ( + + {children} + + ); }; -export const useScheduleUpgradeModal = ({ - useSuccessBtn, - cb, -}: IUseParallelQueriesModalProps) => { +export const ScheduleUpgradeModal = (props: IUseParallelQueriesModalProps) => { + const { callMutations: startScheduleMeshUpgrade } = + useParallelScheduleUpgrade(); + let title = All nodes are ready; let content = ( Schedule a firmware upgrade for all nodes on the network ); - if (!useSuccessBtn) { + if (!props.isSuccess) { title = Some nodes are not ready; content = ( @@ -65,23 +69,28 @@ export const useScheduleUpgradeModal = ({ ); } - return useParallelQueriesModal({ - useSuccessBtn, - cb, - title, - content, - }); + return ( + { + startScheduleMeshUpgrade(); + props.onClose(); + }} + title={title} + {...props} + > + {content} + + ); }; -export const useConfirmModal = ({ - useSuccessBtn, - cb, -}: IUseParallelQueriesModalProps) => { +export const ConfirmModal = (props: IUseParallelQueriesModalProps) => { + const { callMutations: confirmMeshUpgrade } = useParallelConfirmUpgrade(); + let title = All nodes are upgraded successfully; let content = ( Confirm mesh wide upgrade for all nodes on the network ); - if (!useSuccessBtn) { + if (!props.isSuccess) { title = Some nodes don't upgraded properly; content = ( @@ -90,15 +99,25 @@ export const useConfirmModal = ({ ); } - return useParallelQueriesModal({ - useSuccessBtn, - cb, - title, - content, - }); + return ( + { + confirmMeshUpgrade(); + props.onClose(); + }} + title={title} + {...props} + > + {content} + + ); }; -export const useAbortModal = ({ cb }: IUseParallelQueriesModalProps) => { +export const AbortModal = ({ + ...props +}: Omit) => { + const { abort } = useMeshUpgrade(); + const title = Abort current mesh wide upgrade?; const content = ( @@ -107,11 +126,17 @@ export const useAbortModal = ({ cb }: IUseParallelQueriesModalProps) => { ); const btnTxt = Abort; - return useParallelQueriesModal({ - useSuccessBtn: false, - cb, - title, - content, - btnTxt, - }); + return ( + { + abort(); + props.onClose(); + }} + {...props} + > + {content} + + ); }; diff --git a/plugins/lime-plugin-mesh-wide-upgrade/src/components/nextStepFooter.tsx b/plugins/lime-plugin-mesh-wide-upgrade/src/components/nextStepFooter.tsx index e2c82d4e..11f8304b 100644 --- a/plugins/lime-plugin-mesh-wide-upgrade/src/components/nextStepFooter.tsx +++ b/plugins/lime-plugin-mesh-wide-upgrade/src/components/nextStepFooter.tsx @@ -1,11 +1,203 @@ +import { Trans } from "@lingui/macro"; +import { useMemo } from "react"; + +import { useDisclosure } from "components/Modal/useDisclosure"; import { FooterStatus } from "components/status/footer"; +import { IStatusAndButton } from "components/status/statusAndButton"; + +import { + AbortModal, + ConfirmModal, + ScheduleUpgradeModal, +} from "plugins/lime-plugin-mesh-wide-upgrade/src/components/modals"; +import { useMeshUpgrade } from "plugins/lime-plugin-mesh-wide-upgrade/src/hooks/meshWideUpgradeProvider"; +import { useParallelScheduleUpgrade } from "plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeQueries"; +import { StepperState } from "plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeTypes"; + +export type ShowFooterStepperState = Extract< + StepperState, + | "UPDATE_AVAILABLE" + | "DOWNLOADED_MAIN" + | "TRANSACTION_STARTED" + | "UPGRADE_SCHEDULED" + | "CONFIRMATION_PENDING" + | "ERROR" +>; -import { useStep } from "plugins/lime-plugin-mesh-wide-upgrade/src/hooks/useStepper"; +export function isShowFooterStepperState( + value: string +): value is ShowFooterStepperState { + return [ + "UPDATE_AVAILABLE", + "DOWNLOADED_MAIN", + "TRANSACTION_STARTED", + "UPGRADE_SCHEDULED", + "CONFIRMATION_PENDING", + "ERROR", + ].includes(value); +} + +function isShowAbortButtonState( + value: string +): value is ShowFooterStepperState { + return [ + "TRANSACTION_STARTED", + "UPGRADE_SCHEDULED", + "CONFIRMATION_PENDING", + "ERROR", + ].includes(value); +} const NextStepFooter = () => { - const { step, showFooter } = useStep(); + const { + stepperState, + becomeMainNode, + startFwUpgradeTransaction, + allNodesReadyForUpgrade, + abort, + } = useMeshUpgrade(); + + const { errors: scheduleErrors } = useParallelScheduleUpgrade(); + + const { + open: showScheduleModal, + onOpen: openScheduleModal, + onClose: closeScheduleModal, + } = useDisclosure(); + + const { + open: showConfirmationModal, + onOpen: openConfirmationModal, + onClose: closeConfirmationModal, + } = useDisclosure(); + + const { + open: showAbort, + onOpen: openAbort, + onClose: closeAbort, + } = useDisclosure(); + + const showFooter = isShowFooterStepperState(stepperState); + + const step: IStatusAndButton | null = useMemo(() => { + if (!showFooter) return null; + + let step: IStatusAndButton; + switch (stepperState as ShowFooterStepperState) { + case "UPDATE_AVAILABLE": + step = { + status: "success", + onClick: () => becomeMainNode(), + children: ( + + Download remote firmware +
+ to start mesh upgrade +
+ ), + btn: Start mesh upgrade, + }; + break; + case "DOWNLOADED_MAIN": + step = { + status: "success", + onClick: startFwUpgradeTransaction, + children: Ready to start mesh wide upgrade, + btn: Start, + }; + break; + case "TRANSACTION_STARTED": + step = { + status: allNodesReadyForUpgrade ? "success" : "warning", + onClick: openScheduleModal, + children: allNodesReadyForUpgrade ? ( + Ready to start mesh wide upgrade + ) : ( + + Some nodes are not ready for upgrade
+ Check node details for more info +
+ ), + btn: Schedule upgrade, + }; + break; + case "UPGRADE_SCHEDULED": { + const data: Omit = { + onClick: openScheduleModal, + btn: Schedule again, + }; + if (scheduleErrors?.length) { + step = { + ...data, + status: "warning", + children: Some nodes have errors, + }; + } + step = { + ...data, + status: "success", + children: All nodes scheduled successful, + }; + break; + } + case "CONFIRMATION_PENDING": + step = { + status: "success", + onClick: openConfirmationModal, + children: Confirm upgrade on all nodes, + btn: Confirm, + }; + break; + case "ERROR": + default: + step = { + status: "warning", + children: Try last step again, + }; + } + if (isShowAbortButtonState(stepperState)) { + const showAbort: Pick< + IStatusAndButton, + "btnCancel" | "onClickCancel" + > = { + btnCancel: Abort, + onClickCancel: openAbort, + }; + step = { ...step, ...showAbort }; + } + return step; + }, [ + abort, + allNodesReadyForUpgrade, + becomeMainNode, + scheduleErrors?.length, + showConfirmationModal, + showFooter, + showScheduleModal, + startFwUpgradeTransaction, + stepperState, + ]); - return <>{showFooter && }; + return ( + <> + {showFooter && ( + <> + + + + + + )} + + ); }; export default NextStepFooter; diff --git a/plugins/lime-plugin-mesh-wide-upgrade/src/hooks/useStepper.tsx b/plugins/lime-plugin-mesh-wide-upgrade/src/hooks/useStepper.tsx index 65e37664..3ccb85ce 100644 --- a/plugins/lime-plugin-mesh-wide-upgrade/src/hooks/useStepper.tsx +++ b/plugins/lime-plugin-mesh-wide-upgrade/src/hooks/useStepper.tsx @@ -1,12 +1,13 @@ import { Trans } from "@lingui/macro"; import { useMemo } from "react"; +import { useDisclosure } from "components/Modal/useDisclosure"; import { IStatusAndButton } from "components/status/statusAndButton"; import { - useAbortModal, - useConfirmModal, - useScheduleUpgradeModal, + AbortModal, + ConfirmModal, + ScheduleUpgradeModal, } from "plugins/lime-plugin-mesh-wide-upgrade/src/components/modals"; import { useMeshUpgrade } from "plugins/lime-plugin-mesh-wide-upgrade/src/hooks/meshWideUpgradeProvider"; import { @@ -98,180 +99,3 @@ export const getStepperStatus = ( } return "INITIAL"; }; - -export type ShowFooterStepperState = Extract< - StepperState, - | "UPDATE_AVAILABLE" - | "DOWNLOADED_MAIN" - | "TRANSACTION_STARTED" - | "UPGRADE_SCHEDULED" - | "CONFIRMATION_PENDING" - | "ERROR" ->; - -export function isShowFooterStepperState( - value: string -): value is ShowFooterStepperState { - return [ - "UPDATE_AVAILABLE", - "DOWNLOADED_MAIN", - "TRANSACTION_STARTED", - "UPGRADE_SCHEDULED", - "CONFIRMATION_PENDING", - "ERROR", - ].includes(value); -} - -function isShowAbortButtonState( - value: string -): value is ShowFooterStepperState { - return [ - "TRANSACTION_STARTED", - "UPGRADE_SCHEDULED", - "CONFIRMATION_PENDING", - "ERROR", - ].includes(value); -} - -export const useStep = () => { - const { - stepperState, - becomeMainNode, - startFwUpgradeTransaction, - allNodesReadyForUpgrade, - abort, - } = useMeshUpgrade(); - - const { callMutations: startScheduleMeshUpgrade, errors: scheduleErrors } = - useParallelScheduleUpgrade(); - - const { callMutations: confirmMeshUpgrade } = useParallelConfirmUpgrade(); - - const { showModal: showScheduleModal } = useScheduleUpgradeModal({ - useSuccessBtn: allNodesReadyForUpgrade, - cb: () => { - return startScheduleMeshUpgrade(); - }, - }); - - const { showModal: showConfirmationModal } = useConfirmModal({ - // Ideally we have to implement some kind of state before run the upgrade to check if all nodes are up again. - useSuccessBtn: true, - cb: () => { - return confirmMeshUpgrade(); - }, - }); - - const { showModal: showAbortModal } = useAbortModal({ - cb: () => { - return abort(); - }, - }); - - const showFooter = isShowFooterStepperState(stepperState); - - const step: IStatusAndButton | null = useMemo(() => { - if (!showFooter) return null; - - let step: IStatusAndButton; - switch (stepperState as ShowFooterStepperState) { - case "UPDATE_AVAILABLE": - step = { - status: "success", - onClick: () => becomeMainNode(), - children: ( - - Download remote firmware -
- to start mesh upgrade -
- ), - btn: Start mesh upgrade, - }; - break; - case "DOWNLOADED_MAIN": - step = { - status: "success", - onClick: startFwUpgradeTransaction, - children: Ready to start mesh wide upgrade, - btn: Start, - }; - break; - case "TRANSACTION_STARTED": - step = { - status: allNodesReadyForUpgrade ? "success" : "warning", - onClick: () => { - showScheduleModal(); - }, - children: allNodesReadyForUpgrade ? ( - Ready to start mesh wide upgrade - ) : ( - - Some nodes are not ready for upgrade
- Check node details for more info -
- ), - btn: Schedule upgrade, - }; - break; - case "UPGRADE_SCHEDULED": { - const data: Omit = { - onClick: showScheduleModal, - btn: Schedule again, - }; - if (scheduleErrors?.length) { - step = { - ...data, - status: "warning", - children: Some nodes have errors, - }; - } - step = { - ...data, - status: "success", - children: All nodes scheduled successful, - }; - break; - } - case "CONFIRMATION_PENDING": - step = { - status: "success", - onClick: showConfirmationModal, - children: Confirm upgrade on all nodes, - btn: Confirm, - }; - break; - case "ERROR": - default: - step = { - status: "warning", - children: Try last step again, - }; - } - if (isShowAbortButtonState(stepperState)) { - const showAbort: Pick< - IStatusAndButton, - "btnCancel" | "onClickCancel" - > = { - btnCancel: Abort, - onClickCancel: async () => { - showAbortModal(); - }, - }; - step = { ...step, ...showAbort }; - } - return step; - }, [ - abort, - allNodesReadyForUpgrade, - becomeMainNode, - scheduleErrors?.length, - showConfirmationModal, - showFooter, - showScheduleModal, - startFwUpgradeTransaction, - stepperState, - ]); - - return { step, showFooter }; -}; diff --git a/plugins/lime-plugin-mesh-wide/src/components/Components.tsx b/plugins/lime-plugin-mesh-wide/src/components/Components.tsx index 9c802d2b..53b0ea7a 100644 --- a/plugins/lime-plugin-mesh-wide/src/components/Components.tsx +++ b/plugins/lime-plugin-mesh-wide/src/components/Components.tsx @@ -46,16 +46,3 @@ export const StatusMessage = ({ {children} ); - -export const EditOrDelete = ({ - onEdit, - onDelete, -}: { - onEdit: (e) => void; - onDelete: (e) => void; -}) => ( -
- - -
-); diff --git a/plugins/lime-plugin-mesh-wide/src/components/FeatureDetail/LinkDetail.tsx b/plugins/lime-plugin-mesh-wide/src/components/FeatureDetail/LinkDetail.tsx index 44e07459..c22a6a37 100644 --- a/plugins/lime-plugin-mesh-wide/src/components/FeatureDetail/LinkDetail.tsx +++ b/plugins/lime-plugin-mesh-wide/src/components/FeatureDetail/LinkDetail.tsx @@ -3,12 +3,13 @@ import { useMemo } from "preact/compat"; import { useState } from "preact/hooks"; import { useCallback } from "react"; +import { useDisclosure } from "components/Modal/useDisclosure"; import { Warning } from "components/icons/status"; import Tabs from "components/tabs"; import { useToast } from "components/toast/toastProvider"; import { StatusAndButton } from "plugins/lime-plugin-mesh-wide/src/components/Components"; -import { useSetLinkReferenceStateModal } from "plugins/lime-plugin-mesh-wide/src/components/modals"; +import { SetLinkReferenceStateModal } from "plugins/lime-plugin-mesh-wide/src/components/modals"; import { getQueryByLinkType, usePointToPointErrors, @@ -283,6 +284,8 @@ export const LinkReferenceStatus = ({ linkToShow, reference, }: LinkMapFeature) => { + const { open, onOpen, onClose } = useDisclosure(); + const isNewLink = !reference; const { errors } = usePointToPointErrors({ @@ -307,9 +310,6 @@ export const LinkReferenceStatus = ({ referenceError = true; } - // Modal to set ref state - const { closeModal, confirmModal, isModalOpen } = - useSetLinkReferenceStateModal(); const { showToast } = useToast(); // Get nodes to update @@ -355,9 +355,9 @@ export const LinkReferenceStatus = ({ text: Error setting new reference state!, }); } finally { - closeModal(); + onClose(); } - }, [callMutations, closeModal, showToast]); + }, [callMutations, onClose, showToast]); let btnText = ( @@ -394,20 +394,23 @@ export const LinkReferenceStatus = ({ errors?.hasErrors || isDown || isNewLink || referenceError; return ( - - confirmModal({ - dataType: linkToShow.type, - nodes: Object.values(nodesToUpdate), - isDown, - cb: setReferenceState, - }) - } - > - {errorMessage} - + <> + + {errorMessage} + + + ); }; diff --git a/plugins/lime-plugin-mesh-wide/src/components/FeatureDetail/NodeDetail.tsx b/plugins/lime-plugin-mesh-wide/src/components/FeatureDetail/NodeDetail.tsx index 3208f9b9..f8a26a66 100644 --- a/plugins/lime-plugin-mesh-wide/src/components/FeatureDetail/NodeDetail.tsx +++ b/plugins/lime-plugin-mesh-wide/src/components/FeatureDetail/NodeDetail.tsx @@ -1,6 +1,8 @@ import { Trans } from "@lingui/macro"; +import { DomEvent } from "leaflet"; import { useCallback } from "react"; +import { useDisclosure } from "components/Modal/useDisclosure"; import UpdateSharedStateBtn from "components/shared-state/UpdateSharedStateBtn"; import useSharedStateSync from "components/shared-state/useSharedStateSync"; import { useToast } from "components/toast/toastProvider"; @@ -11,7 +13,7 @@ import { Row, TitleAndText, } from "plugins/lime-plugin-mesh-wide/src/components/FeatureDetail/index"; -import { useSetNodeInfoReferenceStateModal } from "plugins/lime-plugin-mesh-wide/src/components/modals"; +import { SetNodeInfoReferenceStateModal } from "plugins/lime-plugin-mesh-wide/src/components/modals"; import { useSingleNodeErrors } from "plugins/lime-plugin-mesh-wide/src/hooks/useSingleNodeErrors"; import { getArrayDifference } from "plugins/lime-plugin-mesh-wide/src/lib/utils"; import { @@ -146,6 +148,7 @@ const NodeDetails = ({ actual, reference, name }: NodeMapFeature) => { }; export const NodeReferenceStatus = ({ actual, reference }: NodeMapFeature) => { + const { open, onOpen, onClose } = useDisclosure(); const { hasErrors: hasNodeErrors, isDown, @@ -162,8 +165,6 @@ export const NodeReferenceStatus = ({ actual, reference }: NodeMapFeature) => { const { data: meshWideNodesReference, isError: isReferenceError } = useMeshWideNodesReference({}); - const { toggleModal, confirmModal, isModalOpen } = - useSetNodeInfoReferenceStateModal(); const { showToast } = useToast(); const { syncNode } = useSharedStateSync({ @@ -190,17 +191,11 @@ export const NodeReferenceStatus = ({ actual, reference }: NodeMapFeature) => { }); }, onSettled: () => { - if (isModalOpen) toggleModal(); + if (open) onClose(); }, }, }); - const setReferenceState = useCallback(async () => { - confirmModal(hostname, isDown, async () => { - await mutateAsync(); - }); - }, [confirmModal, hostname, isDown, mutateAsync]); - let btnText = Set reference state for this node; if (isDown) { btnText = Delete this this node from reference state; @@ -230,13 +225,24 @@ export const NodeReferenceStatus = ({ actual, reference }: NodeMapFeature) => { const showSetReferenceButton = hasNodeErrors || isNewNode || referenceError; return ( - - {errorMessage} - + <> + + {errorMessage} + + { + await mutateAsync(); + }} + onClose={onClose} + isOpen={open} + /> + ); }; diff --git a/plugins/lime-plugin-mesh-wide/src/components/FeatureDetail/RebootNodeBtn.tsx b/plugins/lime-plugin-mesh-wide/src/components/FeatureDetail/RebootNodeBtn.tsx index 131be722..a8dec322 100644 --- a/plugins/lime-plugin-mesh-wide/src/components/FeatureDetail/RebootNodeBtn.tsx +++ b/plugins/lime-plugin-mesh-wide/src/components/FeatureDetail/RebootNodeBtn.tsx @@ -1,9 +1,9 @@ import { Trans } from "@lingui/macro"; import { useMutation } from "@tanstack/react-query"; -import { useEffect, useState } from "preact/hooks"; -import { useCallback } from "react"; +import { useCallback, useState } from "preact/hooks"; -import { useModal } from "components/Modal/Modal"; +import { Modal, ModalProps } from "components/Modal/Modal"; +import { useDisclosure } from "components/Modal/useDisclosure"; import { Button } from "components/buttons/button"; import { ErrorMsg } from "components/form"; import Loading from "components/loading"; @@ -39,14 +39,15 @@ const useRemoteReboot = (opts?) => { }); }; -const useRebootNodeModal = ({ node }: { node: INodeInfo }) => { - const modalKey = "rebootNodeModal"; - const { toggleModal, setModalState, isModalOpen, openModalKey } = - useModal(); +const RebootNodeModal = ({ + node, + isOpen, + onClose, +}: { node: INodeInfo } & Pick) => { const [password, setPassword] = useState(""); const { mutate, isLoading, error } = useRemoteReboot({ onSuccess: () => { - toggleModal(modalKey); + onClose(); }, }); @@ -58,73 +59,58 @@ const useRebootNodeModal = ({ node }: { node: INodeInfo }) => { mutate({ ip: node.ipv4, password }); }, [mutate, node.ipv4, password]); - const updateModalState = useCallback(() => { - setModalState({ - title: Reboot node {node.hostname}, - content: ( -
- - Are you sure you want to reboot this node? This action - will disconnect the node from the network for a few - minutes.
- Add shared password or let it empty if no password is - set. -
- {isLoading && } - {!isLoading && ( -
- - - {error && ( - - - Error performing reboot: {error} - - - )} -
- )} -
- ), - successCb: doLogin, - successBtnText: Reboot, - }); - }, [doLogin, error, isLoading, node.hostname, password, setModalState]); - - const rebootModal = useCallback(() => { - updateModalState(); - toggleModal(modalKey); - }, [toggleModal, updateModalState]); - - // Update modal state with mutation result - useEffect(() => { - if (isModalOpen && openModalKey === modalKey) { - updateModalState(); - } - }, [isLoading, error, isModalOpen, updateModalState, openModalKey]); - - return { rebootModal, toggleModal, isModalOpen }; + return ( + Reboot node {node.hostname}
} + successBtnText={Reboot} + > +
+ + Are you sure you want to reboot this node? This action will + disconnect the node from the network for a few minutes.{" "} +
+ Add shared password or let it empty if no password is set. +
+ {isLoading && } + {!isLoading && ( +
+ + + {error && ( + + Error performing reboot: {error} + + )} +
+ )} +
+ + ); }; const RemoteRebootBtn = ({ node }: { node: INodeInfo }) => { - const { rebootModal, isModalOpen } = useRebootNodeModal({ - node, - }); + const { open, onOpen, onClose } = useDisclosure(); return ( - + <> + + + ); }; diff --git a/plugins/lime-plugin-mesh-wide/src/components/modals.tsx b/plugins/lime-plugin-mesh-wide/src/components/modals.tsx index ab7ce6a0..6a443fb2 100644 --- a/plugins/lime-plugin-mesh-wide/src/components/modals.tsx +++ b/plugins/lime-plugin-mesh-wide/src/components/modals.tsx @@ -1,95 +1,83 @@ import { Trans } from "@lingui/macro"; -import { ComponentChildren } from "preact"; -import { useCallback } from "preact/compat"; -import { useForm } from "react-hook-form"; -import { ModalActions, useModal } from "components/Modal/Modal"; -import InputField from "components/inputs/InputField"; +import { Modal, ModalProps } from "components/Modal/Modal"; import { dataTypeNameMapping } from "plugins/lime-plugin-mesh-wide/src/lib/utils"; import { MeshWideMapDataTypeKeys } from "plugins/lime-plugin-mesh-wide/src/meshWideTypes"; -export const useSetNodeInfoReferenceStateModal = () => { - const { toggleModal, setModalState, isModalOpen } = useModal(); - - const confirmModal = useCallback( - (nodeName: string, isDown: boolean, cb: () => Promise) => { - let title = Set reference state for {nodeName}; - let content = Set the reference state for this node.; - if (isDown) { - title = ( - Remove {nodeName} from the reference state - ); - content = ( - - This node seems down, remove them from the reference - state? - - ); - } - setModalState({ - title, - content, - successCb: cb, - successBtnText: Continue, - }); - toggleModal(); - }, - [setModalState, toggleModal] +export const SetNodeInfoReferenceStateModal = ({ + nodeName, + isDown, + ...modalProps +}: { nodeName: string; isDown: boolean } & Pick< + ModalProps, + "onSuccess" | "isOpen" | "onClose" +>) => { + let title = Set reference state for {nodeName}; + let content = Set the reference state for this node.; + if (isDown) { + title = Remove {nodeName} from the reference state; + content = ( + + This node seems down, remove them from the reference state? + + ); + } + return ( + Continue
} + {...modalProps} + > + {content} + ); - return { confirmModal, toggleModal, isModalOpen }; }; -export const useSetLinkReferenceStateModal = () => { - const { toggleModal, setModalState, isModalOpen, closeModal } = useModal(); - - const confirmModal = ({ - dataType, - nodes, - isDown, - cb, - }: { - dataType: MeshWideMapDataTypeKeys; - nodes: string[]; - isDown: boolean; - cb: () => Promise; - }) => { - let title = ( +export const SetLinkReferenceStateModal = ({ + dataType, + nodes, + isDown, + ...modalProps +}: { + dataType: MeshWideMapDataTypeKeys; + nodes: string[]; + isDown: boolean; +} & Pick) => { + let title = ( + + Set reference state for this {dataTypeNameMapping(dataType)}? + + ); + let content = ( + This will set the reference state of this link: + ); + if (isDown) { + title = ( - Set reference state for this {dataTypeNameMapping(dataType)}? + Remove this {dataTypeNameMapping(dataType)} from the reference + state ); - let content = ( - This will set the reference state of this link: + content = ( + + This link seems down, remove them from the reference state? + ); - if (isDown) { - title = ( - - Remove this {dataTypeNameMapping(dataType)} from the - reference state - - ); - content = ( - - This link seems down, remove them from the reference state? - - ); - } - setModalState({ - title, - content: ( -
- {content} -
-
- {nodes.join(", ")} -
+ } + return ( + Continue} + {...modalProps} + > +
+ {content} +
+
+ {nodes.join(", ")}
- ), - successCb: cb, - successBtnText: Continue, - }); - toggleModal(); - }; - return { confirmModal, closeModal, isModalOpen }; +
+
+ ); }; diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index d7dd9826..d3a2f574 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -1,153 +1,128 @@ import { Trans } from "@lingui/macro"; -import { ComponentChildren, createContext } from "preact"; -import { useContext, useState } from "preact/hooks"; -import { useCallback } from "react"; +import { createContext } from "preact"; +import { createPortal } from "preact/compat"; +import { useCallback, useContext, useState } from "preact/hooks"; import { Button } from "components/buttons/button"; import Divider from "components/divider"; -interface ModalContextProps { - isModalOpen: boolean; - openModalKey?: string | null; - toggleModal: (key?: string) => void; - closeModal: (key?: string) => void; - setModalState: (state?: ModalState) => void; +export type ModalActionsProps = { + isOpen: boolean; + onClose: () => void; + onSuccess?: CallbackFn; + onDelete?: CallbackFn; +}; + +export type ModalPortalProps = { + children?; + title?; + cancelBtn?; + successBtnText?; + deleteBtnText?; +}; + +type ModalContextProps = { isLoading: boolean; -} - -type CallbackFn = () => void | Promise; - -interface ModalState { - content?: ComponentChildren; - title: ComponentChildren | string; - cancelBtn?: boolean; - successCb?: CallbackFn; - successBtnText?: ComponentChildren; - deleteCb?: CallbackFn; - deleteBtnText?: ComponentChildren; - isLoading?: boolean; -} - -const ModalContext = createContext({ - isModalOpen: false, - openModalKey: null, - toggleModal: () => {}, - closeModal: () => {}, - setModalState: () => {}, - isLoading: false, -}); +} & ModalProps; + +export type CallbackFn = () => void | Promise; + +const ModalContext = createContext(null); + +export type ModalProps = ModalActionsProps & ModalPortalProps; export const useModal = () => { const context = useContext(ModalContext); if (context === undefined) { - throw new Error("useModal must be used within a UseModalProvider"); + throw new Error("useModal must be used within a Modal component"); } return context; }; -export type ModalActions = "success" | "delete"; - -export const UseModalProvider = ({ children }) => { - const [isModalOpen, setModalOpen] = useState(false); - const [openModalKey, setOpenModalKey] = useState(null); +export const Modal = ({ + children, + title, + cancelBtn = false, + onSuccess, + successBtnText, + onDelete, + deleteBtnText, + isOpen, + onClose, +}: ModalProps) => { const [isLoading, setIsLoading] = useState(false); - const [modalState, setModalState] = useState({ - content: <>, - title: "", - cancelBtn: false, - successCb: () => {}, - successBtnText: Success, - deleteCb: () => {}, - deleteBtnText: Cancel, - }); - - const toggleModal = useCallback((key?: string) => { - setModalOpen((prevIsModalOpen) => !prevIsModalOpen); - setOpenModalKey(key ?? null); - }, []); - - const closeModal = useCallback((key?: string) => { - setModalOpen(false); - setOpenModalKey(null); - }, []); - const runCb = useCallback( async (cb: CallbackFn) => { if (isLoading) return; setIsLoading(true); await cb(); + // For some reason non async functions doesn't work properly. + // This is a trick to let them be called properly + await new Promise((resolve) => setTimeout(resolve, 0)); setIsLoading(false); }, [isLoading] ); - const successCb = - modalState.successCb != null ? () => runCb(modalState.successCb) : null; - const deleteCb = - modalState.deleteCb != null ? () => runCb(modalState.deleteCb) : null; + let successCb = null; + if (onSuccess) { + successCb = () => runCb(onSuccess); + } + + let deleteCb = null; + if (onDelete) { + deleteCb = () => runCb(onDelete); + } return ( - {children} - - {modalState.content} - + {isOpen && ( + + {children} + + )} ); }; -const Modal = ({ - isModalOpen, - toggleModal, +const ModalPortal = ({ children, title, - cancelBtn = true, - successCb, + cancelBtn = false, successBtnText = Success, - deleteCb, - deleteBtnText = Cancel, - isLoading, -}: { - isModalOpen: boolean; - toggleModal: () => void; - children?: ComponentChildren; -} & ModalState) => { + deleteBtnText = Delete, +}: ModalPortalProps) => { + const { isOpen, isLoading, onClose, onSuccess, onDelete } = useModal(); + const stopPropagation = (e) => { e.stopPropagation(); }; - return ( + + if (!isOpen) { + return null; + } + + return createPortal( <> -
+
{ - if (!isLoading) toggleModal(); + if (!isLoading) onClose(); }} >
- {/*
*/} - {successCb && ( + {onSuccess && ( )} - {deleteCb && ( + {onDelete && (
- + , + document.body ); }; diff --git a/src/components/Modal/useDisclosure.ts b/src/components/Modal/useDisclosure.ts new file mode 100644 index 00000000..d6fbc0a9 --- /dev/null +++ b/src/components/Modal/useDisclosure.ts @@ -0,0 +1,61 @@ +import { useCallback, useState } from "preact/hooks"; + +export interface UseDisclosureProps { + open?: boolean; + defaultOpen?: boolean; + id?: string; + + onClose?(): void; + + onOpen?(): void; +} + +/** + * `useDisclosure` is a custom hook used to help handle common open, close, or toggle scenarios. + * It can be used to control feedback component such as `Modal`, `AlertDialog`, `Drawer`, etc. + * + * @see Docs https://chakra-ui.com/docs/hooks/use-disclosure + */ +export function useDisclosure(props: UseDisclosureProps = {}) { + const { onClose: onCloseProp, onOpen: onOpenProp, open: openProp } = props; + + const handleOpen = useCallback(onOpenProp, []); + const handleClose = useCallback(onCloseProp, []); + + const [openState, setopen] = useState(props.defaultOpen || false); + + const open = openProp !== undefined ? openProp : openState; + + const isControlled = openProp !== undefined; + + const onClose = useCallback(() => { + if (!isControlled) { + setopen(false); + } + handleClose?.(); + }, [isControlled, handleClose]); + + const onOpen = useCallback(() => { + if (!isControlled) { + setopen(true); + } + handleOpen?.(); + }, [isControlled, handleOpen]); + + const onToggle = useCallback(() => { + if (open) { + onClose(); + } else { + onOpen(); + } + }, [open, onOpen, onClose]); + + return { + open, + onOpen, + onClose, + onToggle, + }; +} + +export type UseDisclosureReturn = ReturnType; diff --git a/src/components/app.tsx b/src/components/app.tsx index a45d4bbc..3ac12c60 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -6,7 +6,6 @@ import Router from "preact-router"; import { useEffect } from "preact/hooks"; import { Provider } from "react-redux"; -import { UseModalProvider } from "components/Modal/Modal"; import { ToastProvider } from "components/toast/toastProvider"; import { Menu } from "containers/Menu"; @@ -112,11 +111,9 @@ const AppDefault = () => { - - - - - + + +