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 85881c2b5..b0a69ecc6 100644 --- a/plugins/lime-plugin-mesh-wide-config/src/components/Components.tsx +++ b/plugins/lime-plugin-mesh-wide-config/src/components/Components.tsx @@ -51,8 +51,8 @@ export const EditOrDelete = ({ onEdit, onDelete, }: { - onEdit: (e) => void; - onDelete: (e) => void; + onEdit?: (e) => void; + onDelete?: (e) => void; }) => { const runCb = (e, cb) => { e.stopPropagation(); @@ -60,14 +60,18 @@ export const EditOrDelete = ({ }; return (
- runCb(e, onEdit)} - /> - runCb(e, onDelete)} - /> + {!!onEdit && ( + runCb(e, onEdit)} + /> + )} + {!!onDelete && ( + 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 deleted file mode 100644 index 0862d8008..000000000 --- a/plugins/lime-plugin-mesh-wide-config/src/components/ConfigSection.tsx +++ /dev/null @@ -1,121 +0,0 @@ -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 { - 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"; - -export const ConfigSection = ({ dropdown }: { dropdown: IMeshWideSection }) => { - return ( - } - > - {Object.entries(dropdown.options).map(([key, value]) => ( - - ))} - - ); -}; - -export const SectionEditOrDelete = ({ name }) => { - 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 ( - <> - - - - - ); -}; - -export const AddNewSectionBtn = () => { - 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/FormEdit.tsx b/plugins/lime-plugin-mesh-wide-config/src/components/FormEdit.tsx new file mode 100644 index 000000000..37028ad24 --- /dev/null +++ b/plugins/lime-plugin-mesh-wide-config/src/components/FormEdit.tsx @@ -0,0 +1,185 @@ +import { Trans, t } from "@lingui/macro"; +import { useEffect, useState } from "preact/hooks"; +import { Controller, useFormContext } from "react-hook-form"; +import { v4 as uuidv4 } from "uuid"; + +import { useDisclosure } from "components/Modal/useDisclosure"; +import { Button, ButtonProps } from "components/buttons/button"; +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 { + AddNewSectionFormProps, + AddNewSectionModal, +} from "plugins/lime-plugin-mesh-wide-config/src/components/modals"; +import { IMeshWideConfig } from "plugins/lime-plugin-mesh-wide-config/src/meshConfigTypes"; + +export const EditableField = ({ + isList, + name, +}: { + isList: boolean; + name: string; +}) => { + const { control, setValue, watch, getValues } = useFormContext(); + + const value = watch(name); + // Hack to force re-render when the list changes + const [uniqueKeys, setUniqueKeys] = useState( + isList && value?.length ? value.map(() => uuidv4()) : [] + ); + + const syncKeysWithValues = () => { + // Ensure uniqueKeys matches the length of value array + setUniqueKeys((keys) => [ + ...keys, + ...Array(value.length - keys.length) + .fill(null) + .map(() => uuidv4()), + ]); + }; + + const removeListItem = (index) => { + const updatedValues = value.filter((_, i) => i !== index); + setValue(name, updatedValues); + setUniqueKeys((keys) => keys.filter((_, i) => i !== index)); + }; + + const addListItem = () => { + setValue(name, [...value, ""]); + setUniqueKeys((keys) => [...keys, uuidv4()]); + }; + + // Ensure the list has at least one item at the start + useEffect(() => { + if (isList && value.length === 0) { + setValue(name, [""]); + setUniqueKeys([uuidv4()]); // Reset keys for new list + } else if (isList) { + // Sync keys with values length on every render + syncKeysWithValues(); + } + }, [isList, value, name, setValue]); + + if (isList) { + return ( +
+ {uniqueKeys.map((item, index) => ( + { + return ( +
+ + removeListItem(index)} + /> +
+ ); + }} + /> + ))} + +
+ ); + } + + return ( + ( + Value} + className="w-100" + error={error?.message} + {...field} + /> + )} + /> + ); +}; + +export const AddNewConfigSection = ({ + sectionName, +}: { + sectionName?: string; +}) => { + const { watch, setValue } = useFormContext(); + + const { open, onOpen, onClose } = useDisclosure(); + const { showToast } = useToast(); + + const section = watch(sectionName); + + const onSuccess = (data: AddNewSectionFormProps) => { + if (!sectionName) { + setValue(data.name, {}); + } else { + let value: string | string[] = data.value; + if (data.isList) { + value = data.values; + } + setValue(sectionName, { + ...section, + [data.name]: value, + }); + } + onClose(); + showToast({ + text: Added section {data.name}, + }); + }; + + return ( + <> + + + + ); +}; + +export const AddElementButton = (props: ButtonProps) => { + return ( +
+ +
+ ); +}; diff --git a/plugins/lime-plugin-mesh-wide-config/src/components/FormFooter.tsx b/plugins/lime-plugin-mesh-wide-config/src/components/FormFooter.tsx new file mode 100644 index 000000000..81e00f2f0 --- /dev/null +++ b/plugins/lime-plugin-mesh-wide-config/src/components/FormFooter.tsx @@ -0,0 +1,63 @@ +import { Trans } from "@lingui/macro"; +import { useFormContext } from "react-hook-form"; + +import { IFullScreenModalProps } from "components/Modal/FullScreenModal"; +import { FooterStatus } from "components/status/footer"; +import { useToast } from "components/toast/toastProvider"; + +import { useSetCommunityConfig } from "plugins/lime-plugin-mesh-wide-config/src/meshConfigQueries"; +import { IMeshWideConfig } from "plugins/lime-plugin-mesh-wide-config/src/meshConfigTypes"; +import { jsonToConfig } from "plugins/lime-plugin-mesh-wide-config/src/utils/jsonParser"; + +export const FormFooter = ({ + onClose, + isDirty, +}: { isDirty: boolean } & Pick) => { + const { showToast } = useToast(); + const { handleSubmit } = useFormContext(); + const { mutate, isLoading } = useSetCommunityConfig({ + onError: () => { + showToast({ + text: Error updating the new configuration, + }); + }, + onSuccess: () => { + onClose(); + showToast({ + text: Starting mesh wide configuration change, + }); + }, + }); + const onSubmit = (data: IMeshWideConfig) => { + const newConfig = jsonToConfig(data); + mutate({ file_contents: newConfig }); + }; + + let message = No changes made; + if (isDirty) { + message = ( + <> + Changes made +
+ + Start mesh wide configuration update + + + ); + } + return ( + Start Lime Config update} + btnProps={{ + disabled: !isDirty || isLoading, + }} + onClick={() => { + handleSubmit(onSubmit)(); + }} + > +
{message}
+
+ ); +}; diff --git a/plugins/lime-plugin-mesh-wide-config/src/components/FormOption.tsx b/plugins/lime-plugin-mesh-wide-config/src/components/FormOption.tsx new file mode 100644 index 000000000..0c2c5c548 --- /dev/null +++ b/plugins/lime-plugin-mesh-wide-config/src/components/FormOption.tsx @@ -0,0 +1,120 @@ +import { Trans } from "@lingui/macro"; +import { useState } from "preact/hooks"; +import { FormProvider, useForm, useFormContext } from "react-hook-form"; + +import { useDisclosure } from "components/Modal/useDisclosure"; +import { Button } from "components/buttons/button"; +import Divider from "components/divider"; +import { useToast } from "components/toast/toastProvider"; + +import { EditOrDelete } from "plugins/lime-plugin-mesh-wide-config/src/components/Components"; +import { EditableField } from "plugins/lime-plugin-mesh-wide-config/src/components/FormEdit"; +import { DeletePropModal } from "plugins/lime-plugin-mesh-wide-config/src/components/modals"; +import { IMeshWideConfig } from "plugins/lime-plugin-mesh-wide-config/src/meshConfigTypes"; + +type OptionContainerProps = { + sectionName: string; + keyString: string; +}; + +export const OptionContainer = ({ + keyString, + sectionName, +}: OptionContainerProps) => { + const { watch, setValue } = useFormContext(); + const [isEditing, setIsEditing] = useState(false); + const { + open: isDeleteModalOpen, + onOpen: openDeleteModal, + onClose: onCloseDeleteModal, + } = useDisclosure(); + + const { showToast } = useToast(); + + const onDelete = async () => { + const newValues = { ...section }; + delete newValues[keyString]; + setValue(sectionName, newValues); + onCloseDeleteModal(); + showToast({ + text: Deleted {keyString}, + onAction: () => { + setValue(sectionName, section); + }, + }); + }; + + const name = `${sectionName}[${keyString}]`; + const value = watch(name); + const section = watch(sectionName); + + let _value = value; + const isList = Array.isArray(value); + if (isList) { + _value = value.join(", "); + } + + const fmethods = useForm({ + defaultValues: { [sectionName]: { [keyString]: value } }, + }); + + const onSubmit = (data: IMeshWideConfig) => { + const newSectionValues = { ...section, ...data[sectionName] }; + setValue(sectionName, newSectionValues); + setIsEditing(false); + }; + + return ( +
+
+ +
+
+
+
+ {isList && (List)} {keyString} +
+ setIsEditing((prev) => !prev)} + onDelete={openDeleteModal} + /> +
+ {!isEditing &&
{_value}
} + {isEditing && ( + +
+ +
+ + +
+ +
+ )} +
+ +
+ ); +}; diff --git a/plugins/lime-plugin-mesh-wide-config/src/components/FormSection.tsx b/plugins/lime-plugin-mesh-wide-config/src/components/FormSection.tsx new file mode 100644 index 000000000..63f32421c --- /dev/null +++ b/plugins/lime-plugin-mesh-wide-config/src/components/FormSection.tsx @@ -0,0 +1,80 @@ +import { Trans } from "@lingui/macro"; +import { useFormContext } from "react-hook-form"; + +import { useDisclosure } from "components/Modal/useDisclosure"; +import { Collapsible } from "components/collapsible"; +import { useToast } from "components/toast/toastProvider"; + +import { EditOrDelete } from "plugins/lime-plugin-mesh-wide-config/src/components/Components"; +import { AddNewConfigSection } from "plugins/lime-plugin-mesh-wide-config/src/components/FormEdit"; +import { OptionContainer } from "plugins/lime-plugin-mesh-wide-config/src/components/FormOption"; +import { + DeletePropModal, + EditPropModal, +} from "plugins/lime-plugin-mesh-wide-config/src/components/modals"; +import { IMeshWideSection } from "plugins/lime-plugin-mesh-wide-config/src/meshConfigTypes"; + +export const FormSection = ({ + title, + dropdown, +}: { + title: string; + dropdown: IMeshWideSection; +}) => { + return ( + } + > + {Object.entries(dropdown).map(([key, value]) => ( + + ))} + + + ); +}; + +export const SectionEditOrDelete = ({ name }) => { + const { + open: isDeleteModalOpen, + onOpen: openDeleteModal, + onClose: onCloseDeleteModal, + } = useDisclosure(); + const { showToast } = useToast(); + const { watch, setValue, reset } = useFormContext(); + + const onDelete = async () => { + const form = watch(); + const newForm = { ...form }; + delete newForm[name]; + reset(newForm); + onCloseDeleteModal(); + showToast({ + text: ( + + Deleted {name} - {new Date().toDateString()} + + ), + onAction: () => { + reset(form); + }, + }); + }; + + return ( + <> + + + + ); +}; diff --git a/plugins/lime-plugin-mesh-wide-config/src/components/MeshStatus.tsx b/plugins/lime-plugin-mesh-wide-config/src/components/MeshStatus.tsx deleted file mode 100644 index 361e2081a..000000000 --- a/plugins/lime-plugin-mesh-wide-config/src/components/MeshStatus.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Trans } from "@lingui/macro"; - -import { FooterStatus } from "components/status/footer"; -import { useToast } from "components/toast/toastProvider"; - -export const MeshStatus = () => { - const { showToast } = useToast(); - return ( - { - showToast({ - text: ( - <> - - Updating shared state{" "} - {new Date().toDateString()} - - - ), - }); - }} - > -
- 10 of 12 node are ready to update -
- - Last update: 30 second ago - -
-
- ); -}; diff --git a/plugins/lime-plugin-mesh-wide-config/src/components/OptionForm.tsx b/plugins/lime-plugin-mesh-wide-config/src/components/OptionForm.tsx deleted file mode 100644 index 5e805fb88..000000000 --- a/plugins/lime-plugin-mesh-wide-config/src/components/OptionForm.tsx +++ /dev/null @@ -1,135 +0,0 @@ -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 { - DeletePropModal, - EditPropModal, -} from "plugins/lime-plugin-mesh-wide-config/src/components/modals"; - -const EditOptionForm = ({ - keyString, - value, - onSubmit, -}: { - keyString: string; - value: string; - onSubmit?: (data) => void; -}) => { - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ - defaultValues: { key: keyString, value }, - }); - - const _onSubmit: SubmitHandler = (data) => { - onSubmit(data); - }; - - return ( -
- Key} - register={register} - /> - Value} - register={register} - /> - - - ); -}; - -export const OptionContainer = ({ - keyString, - value, -}: { - 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); - - return ( -
-
- -
-
- {!isEditing ? ( - <> -
-
{keyString}
- -
-
{value}
- - ) : ( - { - console.log("edited stuff", data); - toggleIsEditing(); - showToast({ - text: Edited {data.key}, - }); - }} - /> - )} -
- -
- ); -}; diff --git a/plugins/lime-plugin-mesh-wide-config/src/components/StepStates.tsx b/plugins/lime-plugin-mesh-wide-config/src/components/StepStates.tsx new file mode 100644 index 000000000..67bef8055 --- /dev/null +++ b/plugins/lime-plugin-mesh-wide-config/src/components/StepStates.tsx @@ -0,0 +1,108 @@ +import { Trans } from "@lingui/macro"; + +import { + MeshUpgradeErrorIcon, + MeshUpgradeSuccessIcon, + ParallelErrors, + StepState, +} from "components/mesh-wide-wizard/StepState"; + +import { + useParallelConfirmConfig, + useParallelReadyForApply, +} from "plugins/lime-plugin-mesh-wide-config/src/meshConfigQueries"; +import { useMeshConfig } from "plugins/lime-plugin-mesh-wide-config/src/providers/useMeshConfigProvider"; + +export const RestartScheduled = () => { + const { totalNodes } = useMeshConfig(); + const { errors, results } = useParallelReadyForApply(); + + return ( + Restart is scheduled!}> + <> + + {results?.length} of {totalNodes} will be upgraded + + {errors?.length > 0 && } + + + ); +}; + +export const RadyForApply = () => { + const { allNodesReadyForApply } = useMeshConfig(); + const title = ( +
+ Ready for apply new configuration +
+ ); + + return ( + + {!allNodesReadyForApply && ( +
+ + Some nodes have not the new configuration yet or are not + ready for apply it. +
+ Check network page for more information +
+
+ )} +
+ ); +}; + +export const Confirmed = () => { + const { errors } = useParallelConfirmConfig(); + let icon = ; + let title = Confirmed!; + let desc = New configuration confirmed successfully; + if (errors?.length > 0) { + icon = ; + title = Confirmed with some errors; + desc = New configuration was confirmed with some errors; + } + + return ( + + {desc} + {errors?.length > 0 && } + + ); +}; + +export const ConfirmationPending = () => { + const { errors } = useParallelConfirmConfig(); + const title = ( + + Configuration applied! +
+ Awaiting confirmation +
+ ); + + return ( + + <> + + Check if network is working properly and confirm the new + configuration +
+ If not confirmed, the new configuration will be rolled back + after a while +
+ {errors?.length > 0 && } + +
+ ); +}; + +export const DefaultState = () => { + return ( + Apply mesh wide new configuration!} + icon={} + /> + ); +}; 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 82ee1fd77..8cf7a31ba 100644 --- a/plugins/lime-plugin-mesh-wide-config/src/components/modals.tsx +++ b/plugins/lime-plugin-mesh-wide-config/src/components/modals.tsx @@ -1,14 +1,27 @@ -import { Trans } from "@lingui/macro"; -import { useForm } from "react-hook-form"; +import { Trans, t } from "@lingui/macro"; +import { Label } from "@tanstack/react-query-devtools/build/lib/Explorer"; +import { Controller, FormProvider, useForm } from "react-hook-form"; import { Modal, ModalProps } from "components/Modal/Modal"; import InputField from "components/inputs/InputField"; +import switchStyle from "components/switch"; + +import { EditableField } from "plugins/lime-plugin-mesh-wide-config/src/components/FormEdit"; +import { + useParallelConfirmConfig, + useParallelReadyForApply, +} from "plugins/lime-plugin-mesh-wide-config/src/meshConfigQueries"; +import { useMeshConfig } from "plugins/lime-plugin-mesh-wide-config/src/providers/useMeshConfigProvider"; +import { + IUseParallelQueriesModalProps, + ParallelQueriesModal, +} from "plugins/lime-plugin-mesh-wide-upgrade/src/components/modals"; export const DeletePropModal = ({ prop, ...rest }: { prop: string } & Pick) => ( - Delete property} {...rest}> + Delete property} cancelBtn {...rest}>
Are you sure you want to delete the {prop}{" "} @@ -39,37 +52,193 @@ export const EditPropModal = ({ export interface AddNewSectionFormProps { name: string; + value?: string; + values?: string[]; + isList?: boolean; } export const AddNewSectionModal = ({ onSuccess, + sectionName, ...rest -}: { onSuccess: (data: AddNewSectionFormProps) => void } & Pick< - ModalProps, - "isOpen" | "onClose" ->) => { +}: { + onSuccess: (data: AddNewSectionFormProps) => void; + sectionName?: string; +} & Pick) => { + const fmethods = useForm({ + defaultValues: { name: "", value: "", values: [], isList: false }, + }); + + const handleSuccess = (data: AddNewSectionFormProps) => { + onSuccess(data); // Call the parent onSuccess handler + reset(); // Reset the form after successful submission + }; + const { register, handleSubmit, formState: { errors }, - } = useForm({ - defaultValues: { name: "" }, - }); + watch, + reset, + control, + } = fmethods; + + let title = Add new section; + if (sectionName) { + title = Add new section for {sectionName}; + } + + const isList = watch("isList"); + return ( + +
+ Add} + cancelBtn + {...rest} + onSuccess={handleSubmit(handleSuccess)} + > + ( + Value} + className="w-100" + error={error?.message} + {...field} + /> + )} + /> + {sectionName && ( + <> +
+ + +
+ + + + )} +
+
+
+ ); +}; + +export const AbortModal = ({ + ...props +}: Pick) => { + const { abort } = useMeshConfig(); + const title = Abort current mesh wide configuration update?; + const content = ( + + This will the abort current configuration update process on all + nodes. Are you sure you want to proceed? + + ); + const btnTxt = Abort; return ( Add new section
} - successBtnText={Add} - {...rest} - onSuccess={handleSubmit(onSuccess)} + title={title} + deleteBtnText={btnTxt} + onDelete={() => { + abort(); + props.onClose(); + }} + {...props} > -
- Name} - register={register} - /> -
+ {content} ); }; + +export const ScheduleSafeRebootModal = ( + props: IUseParallelQueriesModalProps +) => { + const { callMutations: startScheduleMeshUpgrade } = + useParallelReadyForApply(); + + let title = All nodes are ready; + let content = ( + + Apply configuration on all of them with a scheduled safe reboot? + + ); + if (!props.isSuccess) { + title = Some nodes are not ready; + content = ( + + Are you sure you want to apply the configuration to the nodes + that are ready?
+ This will make some of them to reboot +
+ Check node list to see the network status +
+ ); + } + + return ( + { + startScheduleMeshUpgrade(); + props.onClose(); + }} + title={title} + {...props} + > + {content} + + ); +}; + +export const ConfirmModal = (props: IUseParallelQueriesModalProps) => { + const { callMutations } = useParallelConfirmConfig(); + let title = All nodes applied the configuration; + let content = ( + + Confirm configuration works properlly for al updated nodes + + ); + // this is not yet imlemented since we are not storing any information of mesh previous state + if (!props.isSuccess) { + title = Some nodes don't upgraded properly; + content = ( + + Are you sure you want to confirm the upgrade?
+ Check node list to see the network status +
+ ); + } + return ( + { + callMutations(); + props.onClose(); + }} + title={title} + {...props} + > + {content} + + ); +}; diff --git a/plugins/lime-plugin-mesh-wide-config/src/containers/EditConfiguration.tsx b/plugins/lime-plugin-mesh-wide-config/src/containers/EditConfiguration.tsx deleted file mode 100644 index d922c7233..000000000 --- a/plugins/lime-plugin-mesh-wide-config/src/containers/EditConfiguration.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Trans } from "@lingui/macro"; - -import { - FullScreenModal, - IFullScreenModalProps, -} from "components/Modal/FullScreenModal"; - -import { - AddNewSectionBtn, - ConfigSection, -} from "plugins/lime-plugin-mesh-wide-config/src/components/ConfigSection"; -import { MeshStatus } from "plugins/lime-plugin-mesh-wide-config/src/components/MeshStatus"; -import { useMeshWideConfig } from "plugins/lime-plugin-mesh-wide-config/src/meshConfigQueries"; - -const EditConfiguration = (props: Partial) => { - const { data: meshWideConfig, isLoading } = useMeshWideConfig({}); - - return ( - Mesh wide config} - isLoading={isLoading} - {...props} - > - {meshWideConfig && ( - <> -
- {meshWideConfig.map((dropdown, index) => ( - - ))} - -
- - - )} -
- ); -}; - -export default EditConfiguration; diff --git a/plugins/lime-plugin-mesh-wide-config/src/containers/LimeConfigEditForm.tsx b/plugins/lime-plugin-mesh-wide-config/src/containers/LimeConfigEditForm.tsx new file mode 100644 index 000000000..e905e77e1 --- /dev/null +++ b/plugins/lime-plugin-mesh-wide-config/src/containers/LimeConfigEditForm.tsx @@ -0,0 +1,87 @@ +import { Trans } from "@lingui/macro"; +import { useEffect, useRef, useState } from "preact/hooks"; +import { FormProvider, useForm } from "react-hook-form"; + +import { + FullScreenModal, + IFullScreenModalProps, +} from "components/Modal/FullScreenModal"; + +import { AddNewConfigSection } from "plugins/lime-plugin-mesh-wide-config/src/components/FormEdit"; +import { FormFooter } from "plugins/lime-plugin-mesh-wide-config/src/components/FormFooter"; +import { FormSection } from "plugins/lime-plugin-mesh-wide-config/src/components/FormSection"; +import { useCommunityConfig } from "plugins/lime-plugin-mesh-wide-config/src/meshConfigQueries"; +import { IMeshWideConfig } from "plugins/lime-plugin-mesh-wide-config/src/meshConfigTypes"; + +import { isEmpty } from "utils/utils"; + +const LimeConfigEditForm = (props: Partial) => { + const { data: meshWideConfig, isLoading } = useCommunityConfig({}); + + return ( + Mesh wide config} + isLoading={isLoading} + {...props} + > + {!!meshWideConfig && ( + + )} + + ); +}; + +const EditConfigForm = ({ + meshWideConfig, + onClose, +}: { + meshWideConfig: IMeshWideConfig; +} & Pick) => { + const [isDirty, setIsDirty] = useState(false); + const fMethods = useForm({ + defaultValues: meshWideConfig, + }); + const defaultValuesRef = useRef(meshWideConfig); + + const formData = fMethods.watch(); + + // compare values on each change + useEffect(() => { + setIsDirty( + JSON.stringify(formData) !== + JSON.stringify(defaultValuesRef.current) + ); + }, [formData]); + + return ( + +
+
+ {Object.entries(formData).map( + ([title, dropdown], index) => ( + + ) + )} + {!formData || + (isEmpty(formData) && ( + + Your Lime Community file seems empty! Please add + some configurations. + + ))} + +
+ + +
+ ); +}; + +export default LimeConfigEditForm; diff --git a/plugins/lime-plugin-mesh-wide-config/src/containers/NextStepFooter.tsx b/plugins/lime-plugin-mesh-wide-config/src/containers/NextStepFooter.tsx new file mode 100644 index 000000000..67b06b3be --- /dev/null +++ b/plugins/lime-plugin-mesh-wide-config/src/containers/NextStepFooter.tsx @@ -0,0 +1,161 @@ +import { Trans } from "@lingui/macro"; +import { useState } from "preact/hooks"; +import { useMemo } from "react"; + +import { useDisclosure } from "components/Modal/useDisclosure"; +import { StatusIcons } from "components/icons/status"; +import { FooterStatus } from "components/status/footer"; +import { IStatusAndButton } from "components/status/statusAndButton"; + +import { + AbortModal, + ConfirmModal, + ScheduleSafeRebootModal, +} from "plugins/lime-plugin-mesh-wide-config/src/components/modals"; +import LimeConfigEditForm from "plugins/lime-plugin-mesh-wide-config/src/containers/LimeConfigEditForm"; +import { useParallelReadyForApply } from "plugins/lime-plugin-mesh-wide-config/src/meshConfigQueries"; +import { ConfigUpdateState } from "plugins/lime-plugin-mesh-wide-config/src/meshConfigTypes"; +import { useMeshConfig } from "plugins/lime-plugin-mesh-wide-config/src/providers/useMeshConfigProvider"; + +function isShowAbortButtonState(value: string): value is ConfigUpdateState { + return [ + "READY_FOR_APPLY", + "RESTART_SCHEDULED", + "CONFIRMATION_PENDING", + "ERROR", + ].includes(value); +} + +const NextStepFooter = () => { + const { + open: showAbort, + onOpen: openAbort, + onClose: closeAbort, + } = useDisclosure(); + const { + open: showScheduleModal, + onOpen: openScheduleModal, + onClose: closeScheduleModal, + } = useDisclosure(); + + const { + open: showConfirmationModal, + onOpen: openConfirmationModal, + onClose: closeConfirmationModal, + } = useDisclosure(); + const [showEditConfig, setShowEditConfig] = useState(false); + + const { wizardState, allNodesReadyForApply } = useMeshConfig(); + const { errors: scheduleErrors } = useParallelReadyForApply(); + + const step: IStatusAndButton | null = useMemo(() => { + let step: IStatusAndButton | null = null; + if (showEditConfig) return null; + + switch (wizardState) { + case "ABORTED": + case "DEFAULT": + step = { + status: "success", + onClick: () => { + setShowEditConfig(true); + }, + btn: Change LiMe Config, + children: ( + + You can change shared network configuration + + ), + }; + break; + case "READY_FOR_APPLY": { + let status: StatusIcons = "success"; + let text = ( + + All nodes have the configuration ready to apply + + ); + if (!allNodesReadyForApply) { + status = "warning"; + text = ( + + Some nodes are not marked as ready to apply + + ); + } + step = { + status, + onClick: openScheduleModal, + btn: Apply new configuration, + children: text, + }; + break; + } + case "RESTART_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 configuration on all nodes, + btn: Confirm, + }; + break; + } + if (isShowAbortButtonState(wizardState)) { + const showAbort: Pick< + IStatusAndButton, + "btnCancel" | "onClickCancel" + > = { + btnCancel: Abort, + onClickCancel: openAbort, + }; + step = { ...step, ...showAbort }; + } + return step; + }, [allNodesReadyForApply, openAbort, showEditConfig, wizardState]); + + if (showEditConfig) { + return setShowEditConfig(false)} />; + } + + return ( + <> + {step && ( + + {step.children} + + )} + + + + + ); +}; + +export default NextStepFooter; diff --git a/plugins/lime-plugin-mesh-wide-config/src/containers/NodesListPage.tsx b/plugins/lime-plugin-mesh-wide-config/src/containers/NodesListPage.tsx new file mode 100644 index 000000000..c18bac2de --- /dev/null +++ b/plugins/lime-plugin-mesh-wide-config/src/containers/NodesListPage.tsx @@ -0,0 +1,172 @@ +import { Trans } from "@lingui/macro"; +import { ComponentChildren } from "preact"; + +import { StatusIcons } from "components/icons/status"; +import NodeInfoListItem, { + INodeInfoBodyItemProps, +} from "components/mesh-wide-wizard/NodeInfoListItem"; +import { NodesListWrapper } from "components/mesh-wide-wizard/NodesListWrapper"; + +import { useMeshWideConfigState } from "plugins/lime-plugin-mesh-wide-config/src/meshConfigQueries"; +import { meshConfigStateKey } from "plugins/lime-plugin-mesh-wide-config/src/meshConfigQueriesKeys"; +import { + ConfigUpdateState, + MeshWideNodeConfigInfo, + NodeMeshConfigInfo, +} from "plugins/lime-plugin-mesh-wide-config/src/meshConfigTypes"; +import { MainNodeStatusType } from "plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeTypes"; + +const NodeConfigItem = ({ + info, + name, +}: { + info: MeshWideNodeConfigInfo; + name: string; +}) => { + const status: StatusIcons = + info.transaction_state === "ERROR" || + info.transaction_state === "ABORTED" + ? "warning" + : "success"; + + // Description under node name + let descriptionMsg = InfoStatusMessageMap[info.transaction_state] ?? ( + Error retrieving the status + ); + + // Firt info when opening the accordion + const nodeStatusInfo: INodeInfoBodyItemProps = + detailedInfoStatusMessageMap(info)[info.transaction_state] ?? + detailedInfoStatusMessageMap()["DEFAULT"]; + + // Main node status message + const mainNodeStatusInfo = mainNodeStatusMessageMap[info.main_node]; + if (mainNodeStatusInfo) { + descriptionMsg = (Main Node) {descriptionMsg}; + } + + // Extra information from the state + const extraInfoItems: Array = [ + nodeStatusInfo, + ...(mainNodeStatusInfo ? [mainNodeStatusInfo] : []), + { + title: Ip, + description: {info.node_ip}, + }, + ]; + return ( + + ); +}; + +const NodesListPage = () => { + const { data, isLoading } = useMeshWideConfigState({}); + return ( + + ); +}; + +export const InfoStatusMessageMap: { + [status in ConfigUpdateState]: ComponentChildren; +} = { + DEFAULT: No update in progres, + READY_FOR_APPLY: Ready for apply, + RESTART_SCHEDULED: Restart scheduled, + CONFIRMATION_PENDING: Confirmation pending, + CONFIRMED: Confirmed, + ERROR: This node has an error, + ABORTED: This node aborted successfully, +}; + +type DetailedInfoStatusMessageMapType = { + [status in ConfigUpdateState]: INodeInfoBodyItemProps; +}; +export const detailedInfoStatusMessageMap = ( + nodeInfo?: NodeMeshConfigInfo +): DetailedInfoStatusMessageMapType => { + return { + DEFAULT: { + title: Everything is up to date!, + description: ( + Mesh configuration is on the last version + ), + }, + READY_FOR_APPLY: { + title: New configuration is on the node, + description: ( + This node is awaiting to apply the configuration + ), + }, + RESTART_SCHEDULED: { + title: The update is scheduled, + description: ( + + After a time the new configuration will be installed and the + node will reboot + + ), + }, + CONFIRMATION_PENDING: { + title: Awaiting confirmation, + description: ( + + The configuration seems to be updated successfully. Confirm + that the node is working properly or will be downgraded to + the previous version + + ), + }, + CONFIRMED: { + title: Configuration applied, + description: ( + + Congratulations, this node has the new configuration working + on it + + ), + }, + ERROR: { + title: This node has an error!, + description: nodeInfo.error, + }, + ABORTED: { + title: This node aborted, + description: ( + Start mesh wide configuration update again + ), + }, + }; +}; + +type MainNodeInfoStatusMessageMapType = { + [status in MainNodeStatusType]: INodeInfoBodyItemProps; +}; + +export const mainNodeStatusMessageMap: MainNodeInfoStatusMessageMapType = { + NO: null, + STARTING: { + title: Setting up main node, + description: ( + The become main node process is starting on this node + ), + }, + MAIN_NODE: { + title: This is a main node, + description: ( + Other nodes will download the firmware from it + ), + }, +}; + +export default NodesListPage; diff --git a/plugins/lime-plugin-mesh-wide-config/src/containers/StatusPage.tsx b/plugins/lime-plugin-mesh-wide-config/src/containers/StatusPage.tsx new file mode 100644 index 000000000..d56f84bc6 --- /dev/null +++ b/plugins/lime-plugin-mesh-wide-config/src/containers/StatusPage.tsx @@ -0,0 +1,69 @@ +import { Trans } from "@lingui/macro"; + +import { ErrorState } from "components/mesh-wide-wizard/ErrorState"; +import { LoadingPage } from "components/mesh-wide-wizard/LoadingPage"; + +import { + ConfirmationPending, + Confirmed, + DefaultState, + RadyForApply, + RestartScheduled, +} from "plugins/lime-plugin-mesh-wide-config/src/components/StepStates"; +import { useMeshConfig } from "plugins/lime-plugin-mesh-wide-config/src/providers/useMeshConfigProvider"; + +const StatusPage = () => { + const { wizardState, nodeInfo } = useMeshConfig(); + + switch (wizardState) { + case "ERROR": + return ; + case "READY_FOR_APPLY": + return ; + case "RESTART_SCHEDULED": + return ; + case "CONFIRMED": + return ; + case "ABORTING": + return ( + Aborting} + description={ + + Sending abort message to this node. The abort order + will be propagated to all nodes. + + } + /> + ); + case "CONFIRMATION_PENDING": + return ; + case "SENDING_START_SCHEDULE": + return ( + Scheduling apply configuration} + description={ + + Schedule apply configuration to all available nodes + + } + /> + ); + case "SENDING_CONFIRMATION": + return ( + Sending confirmation} + description={ + + Confirming new configuration to available nodes + + } + /> + ); + case "DEFAULT": + default: + return ; + } +}; + +export default StatusPage; diff --git a/plugins/lime-plugin-mesh-wide-config/src/meshConfigApi.ts b/plugins/lime-plugin-mesh-wide-config/src/meshConfigApi.ts deleted file mode 100644 index fc92ad20c..000000000 --- a/plugins/lime-plugin-mesh-wide-config/src/meshConfigApi.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { IMeshWideConfig } from "plugins/lime-plugin-mesh-wide-config/src/meshConfigTypes"; - -export const getMeshWideConfig = async () => meshWideConfig; - -const options = { - primary_interface: "eth0", - main_ipv4_address: "10.170.128.0/16/17", -}; - -const meshWideConfig: IMeshWideConfig = [ - { - name: "lime system", - options, - }, - { - name: "lime network", - options, - }, - { - name: "lime wifi", - options, - }, - { - name: "generic_uci_config prometheus", - options, - }, - { - name: "run_asset prometheus_enable", - options, - }, -]; diff --git a/plugins/lime-plugin-mesh-wide-config/src/meshConfigPage.tsx b/plugins/lime-plugin-mesh-wide-config/src/meshConfigPage.tsx index e77b07a37..34d1e15cf 100644 --- a/plugins/lime-plugin-mesh-wide-config/src/meshConfigPage.tsx +++ b/plugins/lime-plugin-mesh-wide-config/src/meshConfigPage.tsx @@ -1,19 +1,46 @@ -import { useState } from "preact/hooks"; import React from "react"; -import { Button } from "components/elements/button"; +import { AbortedNotification } from "components/mesh-wide-wizard/StepState"; +import WizardWrapper from "components/mesh-wide-wizard/WizardWrapper"; -import EditConfiguration from "plugins/lime-plugin-mesh-wide-config/src/containers/EditConfiguration"; +import NextStepFooter from "plugins/lime-plugin-mesh-wide-config/src/containers/NextStepFooter"; +import NodesListPage from "plugins/lime-plugin-mesh-wide-config/src/containers/NodesListPage"; +import StatusPage from "plugins/lime-plugin-mesh-wide-config/src/containers/StatusPage"; +import { + ConfigProvider, + useMeshConfig, +} from "plugins/lime-plugin-mesh-wide-config/src/providers/useMeshConfigProvider"; -const MeshConfigPage = () => { - // State to show modal - const [showEditConfig, setShowEditConfig] = useState(false); +const MeshConfig = () => { + const { + isLoading: meshConfigLoading, + meshInfo, + nodeInfo, + wizardState, + isError, + error, + } = useMeshConfig(); - if (showEditConfig) { - return setShowEditConfig(false)} />; - } + const isLoading = + meshConfigLoading || nodeInfo === undefined || meshInfo === undefined; - return ; + return ( + + ); }; +const MeshConfigPage = () => ( + + + +); + export default MeshConfigPage; diff --git a/plugins/lime-plugin-mesh-wide-config/src/meshConfigQueries.tsx b/plugins/lime-plugin-mesh-wide-config/src/meshConfigQueries.tsx index 396d49afd..f4afb82c9 100644 --- a/plugins/lime-plugin-mesh-wide-config/src/meshConfigQueries.tsx +++ b/plugins/lime-plugin-mesh-wide-config/src/meshConfigQueries.tsx @@ -1,14 +1,213 @@ -import { useQuery } from "@tanstack/react-query"; - -import { getMeshWideConfig } from "plugins/lime-plugin-mesh-wide-config/src/meshConfigApi"; -import { IMeshWideConfig } from "plugins/lime-plugin-mesh-wide-config/src/meshConfigTypes"; - -export function useMeshWideConfig(params) { - return useQuery( - ["lime-meshwide", "get_mesh_config"], - getMeshWideConfig, - { - ...params, - } +import { + UseMutationOptions, + UseQueryOptions, + useMutation, + useQuery, +} from "@tanstack/react-query"; + +import { + RemoteNodeCallError, + callToRemoteNode, + doSharedStateApiCall, +} from "components/shared-state/SharedStateApi"; +import { sharedStateQueries } from "components/shared-state/SharedStateQueriesKeys"; + +import { + MeshConfigQueryKeys, + meshConfigStateKey, +} from "plugins/lime-plugin-mesh-wide-config/src/meshConfigQueriesKeys"; +import { + GetCommunityConfigResponse, + IMeshWideConfig, + MeshWideConfigState, + NodeMeshConfigInfo, +} from "plugins/lime-plugin-mesh-wide-config/src/meshConfigTypes"; +import { parseConfigFile } from "plugins/lime-plugin-mesh-wide-config/src/utils/jsonParser"; +import { MeshWideRPCReturnTypes } from "plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeTypes"; +import { getNodeIpsByConfigCondition } from "plugins/lime-plugin-mesh-wide-upgrade/src/utils/api"; + +import { + IMutationFnVariables, + useMeshWideSyncCall, +} from "utils/meshWideSyncCall"; +import { + ApiServiceParamsType, + StandarizedApiError, + standarizedApiCall, +} from "utils/standarizedApi"; + +export const useCommunityConfig = ( + params?: Omit< + UseQueryOptions, + "queryFn" | "queryKey" + > +) => { + return useQuery({ + queryKey: MeshConfigQueryKeys.getCommunityConfig, + queryFn: () => + standarizedApiCall({ + args: MeshConfigQueryKeys.getCommunityConfig, + }), + select: (data) => parseConfigFile(data.file_contents), + ...params, + }); +}; + +interface SetCommunityConfigParams { + file_contents: string; +} + +export const useSetCommunityConfig = ( + params?: Omit< + UseMutationOptions< + MeshWideRPCReturnTypes, + StandarizedApiError, + SetCommunityConfigParams + >, + "mutationFn" | "mutationKey" + > +) => { + return useMutation< + MeshWideRPCReturnTypes, + StandarizedApiError, + SetCommunityConfigParams + >({ + mutationKey: MeshConfigQueryKeys.setCommunityConfig, + mutationFn: (args) => + standarizedApiCall({ + args: [...MeshConfigQueryKeys.setCommunityConfig, args], + }), + ...params, + }); +}; + +export const useMeshWideConfigState = ( + params?: Omit, "queryFn" | "queryKey"> +) => { + const queryKey = sharedStateQueries.getFromSharedState( + meshConfigStateKey + ) as ApiServiceParamsType; + return useQuery({ + queryKey, + queryFn: () => doSharedStateApiCall<"mesh_config">(queryKey), + ...params, + }); +}; + +export const useConfigNodeState = ( + params?: Omit, "queryFn" | "queryKey"> +) => { + return useQuery({ + queryKey: MeshConfigQueryKeys.getNodeStatus, + queryFn: () => + standarizedApiCall({ + args: MeshConfigQueryKeys.getNodeStatus, + }), + ...params, + }); +}; + +export const useParallelAbort = (opts?) => { + // State to store the errors + const { data: nodes } = useMeshWideConfigState(); + const ips = getNodeIpsByConfigCondition(nodes, (node) => + [ + "UPGRADE_SCHEDULED", + "CONFIRMATION_PENDING", + "ERROR", + "READY_FOR_APPLY", + "RESTART_SCHEDULED", + ].includes(node.transaction_state) ); + return useMeshWideSyncCall({ + mutationKey: MeshConfigQueryKeys.remoteAbort, + // mutationFn: remoteAbort, + mutationFn: ({ ip }) => + callToRemoteNode({ + ip, + apiCall: (customApi) => + standarizedApiCall({ + apiService: customApi, + args: MeshConfigQueryKeys.remoteAbort, + }), + }), + ips, + options: opts, + }); +}; + +// Parallel queries + +interface StartSafeRebootParams { + confirm_timeout: number; + start_delay: number; } +export type UseParallelReadyForApplyType = ReturnType< + typeof useParallelReadyForApply +>; +export const useParallelReadyForApply = ( + opts?: UseMutationOptions< + MeshWideRPCReturnTypes, + RemoteNodeCallError, + IMutationFnVariables + > +) => { + // State to store the errors + const { data: nodes } = useMeshWideConfigState({}); + const ips = getNodeIpsByConfigCondition( + nodes, + (node) => node.transaction_state === "READY_FOR_APPLY" + ); + return useMeshWideSyncCall({ + mutationKey: MeshConfigQueryKeys.startSafeReboot, + mutationFn: ({ ip, variables }) => + callToRemoteNode({ + ip, + apiCall: (customApi) => + standarizedApiCall({ + apiService: customApi, + args: [ + ...MeshConfigQueryKeys.startSafeReboot, + variables ?? { + confirm_timeout: 2001, + start_delay: 63, + }, + ], + }), + }), + ips, + options: opts, + }); +}; + +export type UseParallelConfirmConfig = ReturnType< + typeof useParallelConfirmConfig +>; +export const useParallelConfirmConfig = ( + opts?: UseMutationOptions< + MeshWideRPCReturnTypes, + RemoteNodeCallError, + IMutationFnVariables + > +) => { + // State to store the errors + const { data: nodes } = useMeshWideConfigState({}); + const ips = getNodeIpsByConfigCondition( + nodes, + (node) => node.transaction_state === "CONFIRMATION_PENDING" + ); + return useMeshWideSyncCall({ + mutationKey: MeshConfigQueryKeys.confirm, + mutationFn: ({ ip }) => + callToRemoteNode({ + ip, + apiCall: (customApi) => + standarizedApiCall({ + apiService: customApi, + args: [...MeshConfigQueryKeys.confirm], + }), + }), + ips, + options: opts, + }); +}; diff --git a/plugins/lime-plugin-mesh-wide-config/src/meshConfigQueriesKeys.tsx b/plugins/lime-plugin-mesh-wide-config/src/meshConfigQueriesKeys.tsx new file mode 100644 index 000000000..123cab948 --- /dev/null +++ b/plugins/lime-plugin-mesh-wide-config/src/meshConfigQueriesKeys.tsx @@ -0,0 +1,15 @@ +import { MeshConfigTypes } from "components/shared-state/SharedStateTypes"; + +export const MeshConfigQueryKeys: { + [key: string]: [string, string]; +} = { + getNodeStatus: ["lime-mesh-config", "get_node_status"], + getCommunityConfig: ["lime-mesh-config", "get_community_config"], + remoteConfirmUpgrade: ["lime-mesh-config", "start_config_transaction"], + startSafeReboot: ["lime-mesh-config", "start_safe_reboot"], + confirm: ["lime-mesh-config", "confirm"], + remoteAbort: ["lime-mesh-config", "abort"], + setCommunityConfig: ["lime-mesh-config", "start_config_transaction"], +}; + +export const meshConfigStateKey: keyof MeshConfigTypes = "mesh_config"; diff --git a/plugins/lime-plugin-mesh-wide-config/src/meshConfigTypes.tsx b/plugins/lime-plugin-mesh-wide-config/src/meshConfigTypes.tsx index d6e35770f..a8e24d546 100644 --- a/plugins/lime-plugin-mesh-wide-config/src/meshConfigTypes.tsx +++ b/plugins/lime-plugin-mesh-wide-config/src/meshConfigTypes.tsx @@ -1,6 +1,55 @@ +export type ConfigItemType = string | string[]; + export interface IMeshWideSection { - name: string; - options: { [key: string]: string }; + [key: string]: ConfigItemType; +} + +export type IMeshWideConfig = { + [section: string]: IMeshWideSection; +}; + +export type GetCommunityConfigResponse = { + file_contents: string; +}; + +export type MainNodeStatusType = "NO" | "MAIN_NODE"; + +export type ConfigUpdateState = + | "DEFAULT" // When no config has changed + | "READY_FOR_APPLY" //the config is set in the node and is ready to reboot + | "RESTART_SCHEDULED" // the node will reboot in xx seconds + | "CONFIRMATION_PENDING" // the node rebooted and the configuration is not confirmed + | "CONFIRMED" // the configuration has been set and the user was able to confirm the change + | "ERROR" + | "ABORTED"; + +export interface NodeMeshConfigInfo { + timestamp: string; + main_node: MainNodeStatusType; + error: string; + node_ip: string; + transaction_state: ConfigUpdateState; + current_config_hash: string; + safe_restart_remaining: number; + retry_count: number; + safe_restart_start_time_out: number; + safe_restart_start_mark: number; + board_name: string; + safe_restart_confirm_timeout: number; +} + +export type MeshWideNodeConfigInfo = { + bleachTTL: number; + author: string; +} & NodeMeshConfigInfo; + +export interface MeshWideConfigState { + [key: string]: MeshWideNodeConfigInfo; } -export type IMeshWideConfig = IMeshWideSection[]; +export type StepperWizardState = + | "ABORTING" + | "APPLYING" // the node is applying the configuration + | "SENDING_START_SCHEDULE" + | "SENDING_CONFIRMATION" + | ConfigUpdateState; diff --git a/plugins/lime-plugin-mesh-wide-config/src/providers/useMeshConfigProvider.tsx b/plugins/lime-plugin-mesh-wide-config/src/providers/useMeshConfigProvider.tsx new file mode 100644 index 000000000..c4f2f5ed7 --- /dev/null +++ b/plugins/lime-plugin-mesh-wide-config/src/providers/useMeshConfigProvider.tsx @@ -0,0 +1,192 @@ +import { ComponentChildren, createContext } from "preact"; +import { useEffect, useState } from "preact/hooks"; +import { useCallback, useContext, useMemo } from "react"; + +import { + UseParallelConfirmConfig, + UseParallelReadyForApplyType, + useConfigNodeState, + useMeshWideConfigState, + useParallelAbort, + useParallelConfirmConfig, + useParallelReadyForApply, +} from "plugins/lime-plugin-mesh-wide-config/src/meshConfigQueries"; +import { MeshConfigQueryKeys } from "plugins/lime-plugin-mesh-wide-config/src/meshConfigQueriesKeys"; +import { + NodeMeshConfigInfo, + StepperWizardState, +} from "plugins/lime-plugin-mesh-wide-config/src/meshConfigTypes"; + +import queryCache from "utils/queryCache"; + +const NODE_STATUS_REFETCH_INTERVAL = 5000; + +const getWizardState = ( + nodeInfo: NodeMeshConfigInfo | undefined, + isAborting: boolean, + scheduleSafeReboot: UseParallelReadyForApplyType | undefined, + confirmConfig: UseParallelConfirmConfig | undefined, + isNodeInfoError: boolean +): StepperWizardState => { + if (!nodeInfo) return; + if (isAborting) return "ABORTING"; + if (scheduleSafeReboot?.isLoading) { + return "SENDING_START_SCHEDULE"; + } + if ( + scheduleSafeReboot?.results?.length || + scheduleSafeReboot?.errors?.length + ) { + return "RESTART_SCHEDULED"; + } + if ( + nodeInfo.transaction_state === "CONFIRMATION_PENDING" || + nodeInfo.transaction_state === "CONFIRMED" + ) { + if (confirmConfig?.isLoading) { + return "SENDING_CONFIRMATION"; + } + if (confirmConfig?.errors?.length) { + return "CONFIRMATION_PENDING"; + } + } + // We suppose that if the upgrade is scheduled, and we lost the connection is because is upgrading + if (nodeInfo.transaction_state === "RESTART_SCHEDULED" && isNodeInfoError) { + return "APPLYING"; + } + return nodeInfo?.transaction_state ?? "DEFAULT"; +}; + +export const useMeshConfigProvider = () => { + // UseCallback to invalidate queries + const invalidateQueries = useCallback(() => { + return queryCache.invalidateQueries({ + queryKey: MeshConfigQueryKeys.getNodeStatus, + }); + }, []); + + const invalidateLogin = useCallback(() => { + queryCache.invalidateQueries({ + queryKey: ["session", "get"], + }); + }, []); + + const scheduleSafeReboot = useParallelReadyForApply(); + const confirmConfig = useParallelConfirmConfig(); + + const { + data: meshInfo, + isLoading: meshInfoLoading, + isError: isMeshInfoQueryError, + error: meshInfoQueryError, + } = useMeshWideConfigState({ + refetchInterval: NODE_STATUS_REFETCH_INTERVAL, + }); + const { + data: nodeInfo, + isLoading: nodeInfoLoading, + + isError: isNodeInfoError, + } = useConfigNodeState({ + refetchInterval: NODE_STATUS_REFETCH_INTERVAL, + }); + + // Inner state to control is aborting callback awaiting until query invalidation + const [isAborting, setIsAborting] = useState(false); + const { callMutations: abortMutation } = useParallelAbort(); + + const allNodesReadyForApply = useMemo(() => { + return Object.values(meshInfo || {}).every( + (node) => node.transaction_state === "READY_FOR_APPLY" + ); + }, [meshInfo]); + + const allNodesConfirmed = useMemo(() => { + return Object.values(meshInfo || {}).every( + (node) => node.transaction_state === "CONFIRMED" + ); + }, [meshInfo]); + + const isLoading = meshInfoLoading || nodeInfoLoading; + + const abort = useCallback(async () => { + setIsAborting(true); + abortMutation() + .then(() => { + return invalidateQueries(); + }) + .finally(() => { + setIsAborting(false); + }); + }, [abortMutation, invalidateQueries]); + + const wizardState: StepperWizardState = useMemo( + () => + getWizardState( + nodeInfo, + isAborting, + scheduleSafeReboot, + confirmConfig, + isNodeInfoError + ), + [nodeInfo, isAborting] + ); + + const totalNodes = meshInfo && Object.entries(meshInfo).length; + + let isError; + let error; + // If the state is upgrading, ignore the errors because is normal to lose the connection + if (wizardState !== "APPLYING") { + isError = isMeshInfoQueryError; + error = meshInfoQueryError; + } + + useEffect(() => { + if ( + meshInfoQueryError && + (meshInfoQueryError as any).code != null && + (meshInfoQueryError as any).code === -32002 // Auth failed error code + ) { + invalidateLogin(); + } + }, [invalidateLogin, meshInfoQueryError, wizardState]); + + return { + nodeInfo, + meshInfo, + isLoading, + allNodesReadyForApply, + allNodesConfirmed, + abort, + wizardState, + totalNodes, + isError, + error, + }; +}; + +export const ConfigContext = createContext< + ReturnType | undefined +>(undefined); + +export const ConfigProvider = ({ + children, +}: { + children: ComponentChildren; +}) => { + const config = useMeshConfigProvider(); + return ( + + {children} + + ); +}; + +export const useMeshConfig = () => { + const context = useContext(ConfigContext); + if (!context) { + throw new Error("useConfig must be used within an ConfigProvider"); + } + return context; +}; diff --git a/plugins/lime-plugin-mesh-wide-config/src/utils/jsonParser.ts b/plugins/lime-plugin-mesh-wide-config/src/utils/jsonParser.ts new file mode 100644 index 000000000..11b5369bf --- /dev/null +++ b/plugins/lime-plugin-mesh-wide-config/src/utils/jsonParser.ts @@ -0,0 +1,70 @@ +import { IMeshWideConfig } from "plugins/lime-plugin-mesh-wide-config/src/meshConfigTypes"; + +export const jsonToConfig = (json: IMeshWideConfig): string => { + let configFileContent = ""; + + for (const section in json) { + configFileContent += `config lime '${section}'\n`; + + for (const key in json[section]) { + const value = json[section][key]; + + if (Array.isArray(value)) { + // Handle lists + value.forEach((item) => { + configFileContent += `\tlist ${key} '${item}'\n`; + }); + } else { + // Handle single values (option) + configFileContent += `\toption ${key} '${value}'\n`; + } + } + configFileContent += "\n"; // Add a new line after each section + } + return configFileContent; +}; + +export const parseConfigFile = (data: string): IMeshWideConfig => { + // Replace escaped newlines with actual newlines + const lines = data.replace(/\\n/g, "\n").split("\n"); + const config: IMeshWideConfig = {}; + let configTitle = ""; + let sectionName = ""; + + lines.forEach((line) => { + line = line.trim(); + + // Ignore comments or empty lines + if (!line || line.startsWith("#")) { + return; + } + + if (line.startsWith("config")) { + // Extract the section name + const parts = line.split(" "); + configTitle = parts[1]; + sectionName = parts[2].replace(/'/g, ""); // remove single quotes + config[sectionName] = {}; + } else if (line.startsWith("option") || line.startsWith("list")) { + // Parse key-value pairs + const parts = line.split(" "); + const key = parts[1]; + const value = line + .split(/ (.+)/)[1] + .split(/ (.+)/)[1] + .replace(/'/g, ""); // strip quotes + + if (line.startsWith("list")) { + // Add value to list + if (!config[sectionName][key]) { + config[sectionName][key] = []; + } + (config[sectionName][key] as string[]).push(value); + } else { + // Assign value to option + config[sectionName][key] = value; + } + } + }); + return config; +}; 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 efa5464a9..a5ff57a2d 100644 --- a/plugins/lime-plugin-mesh-wide-upgrade/src/components/modals.tsx +++ b/plugins/lime-plugin-mesh-wide-upgrade/src/components/modals.tsx @@ -1,13 +1,7 @@ import { Trans } from "@lingui/macro"; -import { ComponentChildren, VNode } from "preact"; -import { useCallback } from "react"; +import { ComponentChildren } from "preact"; -import { - CallbackFn, - Modal, - ModalProps, - useModal, -} from "components/Modal/Modal"; +import { CallbackFn, Modal, ModalProps } from "components/Modal/Modal"; import { useMeshUpgrade } from "plugins/lime-plugin-mesh-wide-upgrade/src/hooks/meshWideUpgradeProvider"; import { @@ -15,7 +9,7 @@ import { useParallelScheduleUpgrade, } from "plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeQueries"; -type IUseParallelQueriesModalProps = { +export type IUseParallelQueriesModalProps = { isSuccess: boolean; } & Pick; diff --git a/plugins/lime-plugin-mesh-wide-upgrade/src/components/nodeUpgradeInfo.tsx b/plugins/lime-plugin-mesh-wide-upgrade/src/components/nodeUpgradeInfo.tsx index e574d7a38..b36f3f91e 100644 --- a/plugins/lime-plugin-mesh-wide-upgrade/src/components/nodeUpgradeInfo.tsx +++ b/plugins/lime-plugin-mesh-wide-upgrade/src/components/nodeUpgradeInfo.tsx @@ -1,10 +1,11 @@ import { Trans } from "@lingui/macro"; -import { ComponentChildren } from "preact"; -import { StatusIcon, StatusIcons } from "components/icons/status"; -import { ListItemCollapsible } from "components/list-material"; -import UpdateSharedStateBtn from "components/shared-state/UpdateSharedStateBtn"; +import { StatusIcons } from "components/icons/status"; +import NodeInfoListItem, { + INodeInfoBodyItemProps, +} from "components/mesh-wide-wizard/NodeInfoListItem"; +import { meshUpgradeSharedStateKey } from "plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeQueriesKeys"; import { MeshWideNodeUpgradeInfo } from "plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeTypes"; import { InfoStatusMessageMap, @@ -12,18 +13,6 @@ import { mainNodeStatusMessageMap, } from "plugins/lime-plugin-mesh-wide-upgrade/src/utils/upgradeStatusMessages"; -export interface INodeInfoBodyItemProps { - title: ComponentChildren; - description: ComponentChildren; -} - -const NodeInfoBodyItem = ({ title, description }: INodeInfoBodyItemProps) => ( -
-
{title}
-
{description}
-
-); - const NodeUpgradeInfoItem = ({ info, name, @@ -34,46 +23,45 @@ const NodeUpgradeInfoItem = ({ const status: StatusIcons = info.upgrade_state === "ERROR" ? "warning" : "success"; - const nodeStatusInfo = + const nodeStatusInfo: INodeInfoBodyItemProps = detailedInfoStatusMessageMap(info)[info.upgrade_state] ?? detailedInfoStatusMessageMap()["DEFAULT"]; - const mainNodeStatusInfo = mainNodeStatusMessageMap[info.main_node]; let descriptionMsg = InfoStatusMessageMap[info.upgrade_state] ?? ( Error retrieving the status, is this node outdated? ); + + const mainNodeStatusInfo = mainNodeStatusMessageMap[info.main_node]; if (mainNodeStatusInfo) { descriptionMsg = (Main Node) {descriptionMsg}; } + + const extraInfoItems: Array = [ + nodeStatusInfo, + ...(mainNodeStatusInfo ? [mainNodeStatusInfo] : []), + { + title: Board, + description: {info.board_name}, + }, + { + title: Firmware version, + description: {info.current_fw}, + }, + { + title: Ip, + description: {info.node_ip}, + }, + ]; + return ( - } - rightText={ - - } - > - - {mainNodeStatusInfo && } - Board} - description={{info.board_name}} - /> - Firmware version} - description={{info.current_fw}} - /> - Ip} - description={{info.node_ip}} - /> - + ); }; diff --git a/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/ConfirmationPending.tsx b/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/ConfirmationPending.tsx index 6e10cb934..d510c9cec 100644 --- a/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/ConfirmationPending.tsx +++ b/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/ConfirmationPending.tsx @@ -2,8 +2,9 @@ import { Trans } from "@lingui/macro"; import { ParallelErrors, - UpgradeState, -} from "plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/UpgradeState"; + StepState, +} from "components/mesh-wide-wizard/StepState"; + import { useParallelConfirmUpgrade } from "plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeQueries"; export const ConfirmationPending = () => { @@ -17,7 +18,7 @@ export const ConfirmationPending = () => { ); return ( - + <> Check if network is working properly and confirm the upgrade @@ -27,6 +28,6 @@ export const ConfirmationPending = () => { {errors?.length > 0 && } - + ); }; diff --git a/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/Confirmed.tsx b/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/Confirmed.tsx index a8e22df13..50a2d70f1 100644 --- a/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/Confirmed.tsx +++ b/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/Confirmed.tsx @@ -4,8 +4,9 @@ import { MeshUpgradeErrorIcon, MeshUpgradeSuccessIcon, ParallelErrors, - UpgradeState, -} from "plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/UpgradeState"; + StepState, +} from "components/mesh-wide-wizard/StepState"; + import { useParallelConfirmUpgrade } from "plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeQueries"; export const Confirmed = () => { @@ -21,9 +22,9 @@ export const Confirmed = () => { } return ( - + {desc} {errors?.length > 0 && } - + ); }; diff --git a/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/NewVersionAvailable.tsx b/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/NewVersionAvailable.tsx index 1fa267cc4..dc893bace 100644 --- a/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/NewVersionAvailable.tsx +++ b/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/NewVersionAvailable.tsx @@ -1,9 +1,9 @@ import { Trans } from "@lingui/macro"; import LineChart, { LineChartStep } from "components/PathChart"; +import { StepState } from "components/mesh-wide-wizard/StepState"; import { useNewVersion } from "plugins/lime-plugin-firmware/src/firmwareQueries"; -import { UpgradeState } from "plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/UpgradeState"; import { useBoardData } from "utils/queries"; @@ -73,10 +73,10 @@ export const NewVersionAvailable = ({ } return ( - +
-
+ ); }; diff --git a/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/NoNewVersion.tsx b/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/NoNewVersion.tsx index 4b1638b4c..ed6199b14 100644 --- a/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/NoNewVersion.tsx +++ b/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/NoNewVersion.tsx @@ -2,12 +2,12 @@ import { Trans } from "@lingui/macro"; import { MeshUpgradeSuccessIcon, - UpgradeState, -} from "plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/UpgradeState"; + StepState, +} from "components/mesh-wide-wizard/StepState"; export const NoNewVersionAvailable = () => { return ( - No new version available!} icon={} /> diff --git a/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/TransactionStarted.tsx b/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/TransactionStarted.tsx index 50d72f2fb..d9495ee90 100644 --- a/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/TransactionStarted.tsx +++ b/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/TransactionStarted.tsx @@ -1,6 +1,7 @@ import { Trans } from "@lingui/macro"; -import { UpgradeState } from "plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/UpgradeState"; +import { StepState } from "components/mesh-wide-wizard/StepState"; + import { useMeshUpgrade } from "plugins/lime-plugin-mesh-wide-upgrade/src/hooks/meshWideUpgradeProvider"; export const TransactionStarted = () => { @@ -12,7 +13,7 @@ export const TransactionStarted = () => { ); return ( - + {someNodeDownloading && (
@@ -22,6 +23,6 @@ export const TransactionStarted = () => {
)} -
+ ); }; diff --git a/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/UpgradeScheduled.tsx b/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/UpgradeScheduled.tsx index ed9d15efb..b76c8a622 100644 --- a/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/UpgradeScheduled.tsx +++ b/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/UpgradeScheduled.tsx @@ -2,8 +2,9 @@ import { Trans } from "@lingui/macro"; import { ParallelErrors, - UpgradeState, -} from "plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/UpgradeState"; + StepState, +} from "components/mesh-wide-wizard/StepState"; + import { useMeshUpgrade } from "plugins/lime-plugin-mesh-wide-upgrade/src/hooks/meshWideUpgradeProvider"; import { useParallelScheduleUpgrade } from "plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeQueries"; @@ -13,13 +14,13 @@ export const UpgradeScheduled = () => { const nodesToBeUpgraded = results?.length; return ( - Upgrade is scheduled!}> + Upgrade is scheduled!}> <> {nodesToBeUpgraded} of {totalNodes} will be upgraded {errors?.length > 0 && } - + ); }; diff --git a/plugins/lime-plugin-mesh-wide-upgrade/src/containers/meshWideUpgradeStatus.tsx b/plugins/lime-plugin-mesh-wide-upgrade/src/containers/meshWideUpgradeStatus.tsx index 8588d8d16..71fda0b31 100644 --- a/plugins/lime-plugin-mesh-wide-upgrade/src/containers/meshWideUpgradeStatus.tsx +++ b/plugins/lime-plugin-mesh-wide-upgrade/src/containers/meshWideUpgradeStatus.tsx @@ -1,17 +1,17 @@ import { Trans } from "@lingui/macro"; +import { ErrorState } from "components/mesh-wide-wizard/ErrorState"; +import { LoadingPage } from "components/mesh-wide-wizard/LoadingPage"; + import { ConfirmationPending } from "plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/ConfirmationPending"; import { Confirmed } from "plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/Confirmed"; -import { ErrorState } from "plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/ErrorState"; -import { LoadingPage } from "plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/LoadingPage"; import { NewVersionAvailable } from "plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/NewVersionAvailable"; import { NoNewVersionAvailable } from "plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/NoNewVersion"; import { TransactionStarted } from "plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/TransactionStarted"; import { UpgradeScheduled } from "plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/UpgradeScheduled"; import { useMeshUpgrade } from "plugins/lime-plugin-mesh-wide-upgrade/src/hooks/meshWideUpgradeProvider"; -import { CenterFlex } from "plugins/lime-plugin-mesh-wide-upgrade/src/utils/divs"; -const MeshWideUpgradeStatusState = () => { +export const MeshWideUpgradeStatus = () => { const { stepperState, meshWideError } = useMeshUpgrade(); switch (stepperState) { @@ -71,11 +71,3 @@ const MeshWideUpgradeStatusState = () => { return ; } }; - -export const MeshWideUpgradeStatus = () => { - return ( - - - - ); -}; diff --git a/plugins/lime-plugin-mesh-wide-upgrade/src/containers/nodesList.tsx b/plugins/lime-plugin-mesh-wide-upgrade/src/containers/nodesList.tsx index 4703c6857..b4345b9f2 100644 --- a/plugins/lime-plugin-mesh-wide-upgrade/src/containers/nodesList.tsx +++ b/plugins/lime-plugin-mesh-wide-upgrade/src/containers/nodesList.tsx @@ -1,47 +1,15 @@ -import { Trans } from "@lingui/macro"; - -import Loading from "components/loading"; +import { NodesListWrapper } from "components/mesh-wide-wizard/NodesListWrapper"; import NodeUpgradeInfoItem from "plugins/lime-plugin-mesh-wide-upgrade/src/components/nodeUpgradeInfo"; -import { UpgradeState } from "plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/UpgradeState"; import { useMeshUpgrade } from "plugins/lime-plugin-mesh-wide-upgrade/src/hooks/meshWideUpgradeProvider"; -import { CenterFlex } from "plugins/lime-plugin-mesh-wide-upgrade/src/utils/divs"; export const NodesList = () => { const { data, isLoading } = useMeshUpgrade(); - - if (isLoading) { - return ; - } - - if (!data || (data && Object.keys(data).length === 0)) { - return ( - - - No nodes present on the
- mesh wide upgrade state yet! - - } - /> -
- ); - } - return ( - <> - {data && - Object.entries(data).map(([key, nodeInfo], index) => { - return ( - - ); - })} - + ); }; diff --git a/plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeApi.tsx b/plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeApi.tsx index 24fa6921b..384e517d5 100644 --- a/plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeApi.tsx +++ b/plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeApi.tsx @@ -1,27 +1,23 @@ -import { - MeshUpgradeApiError, - callToRemoteNode, -} from "components/shared-state/SharedStateApi"; +import { callToRemoteNode } from "components/shared-state/SharedStateApi"; import { sharedStateQueries } from "components/shared-state/SharedStateQueriesKeys"; +import { meshUpgradeSharedStateKey } from "plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeQueriesKeys"; import { MeshWideRPCReturnTypes, MeshWideUpgradeInfo, NodeMeshUpgradeInfo, } from "plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeTypes"; +import { ApiServiceParamsType, standarizedApiCall } from "utils/standarizedApi"; import api, { UhttpdService } from "utils/uhttpd.service"; -// todo(kon): refactor this to use doSharedStateApiCall?? export const getMeshWideUpgradeInfo = async () => { - const query = sharedStateQueries.getFromSharedState("mesh_wide_upgrade"); - const res = await api.call(...query); - if (res.error) { - throw new Error( - `Error getting mesh wide upgrade info from shared state async, code error ${res.error}` - ); - } - return res.data as MeshWideUpgradeInfo; + const query = sharedStateQueries.getFromSharedState( + meshUpgradeSharedStateKey + ); + return standarizedApiCall({ + args: query as ApiServiceParamsType, + }); }; export const getMeshUpgradeNodeStatus = async () => { @@ -73,13 +69,10 @@ const meshUpgradeApiCall = async ( customApi?: UhttpdService ) => { const httpService = customApi || api; - const res = (await httpService.call( - "lime-mesh-upgrade", - method, - {} - )) as MeshWideRPCReturnTypes; - if (res.error) { - throw new MeshUpgradeApiError(res.error, res.code); - } - return res.code; + return ( + (await standarizedApiCall({ + apiService: httpService, + args: ["lime-mesh-upgrade", method, {}], + })) as MeshWideRPCReturnTypes + ).code; }; diff --git a/plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradePage.tsx b/plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradePage.tsx index 923008a35..d77767068 100644 --- a/plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradePage.tsx +++ b/plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradePage.tsx @@ -1,104 +1,56 @@ import { Trans } from "@lingui/macro"; -import { useState } from "preact/hooks"; -import { StatusIcon } from "components/icons/status"; -import Loading from "components/loading"; +import { AbortedNotification } from "components/mesh-wide-wizard/StepState"; +import WizardWrapper from "components/mesh-wide-wizard/WizardWrapper"; import Notification from "components/notifications/notification"; -import Tabs from "components/tabs"; import NextStepFooter from "plugins/lime-plugin-mesh-wide-upgrade/src/components/nextStepFooter"; -import { ErrorState } from "plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/ErrorState"; import { MeshWideUpgradeStatus } from "plugins/lime-plugin-mesh-wide-upgrade/src/containers/meshWideUpgradeStatus"; import { NodesList } from "plugins/lime-plugin-mesh-wide-upgrade/src/containers/nodesList"; import { MeshWideUpgradeProvider, useMeshUpgrade, } from "plugins/lime-plugin-mesh-wide-upgrade/src/hooks/meshWideUpgradeProvider"; -import { CenterFlex } from "plugins/lime-plugin-mesh-wide-upgrade/src/utils/divs"; + +const BannerNotification = () => { + const { thisNode } = useMeshUpgrade(); + return ( + <> + + + Upgrade all network nodes at once. This proces will take a + while and will require user interaction. + + + {thisNode.upgrade_state === "ABORTED" && } + + ); +}; const MeshWideUpgrade = () => { const { data: meshWideNodes, - isLoading, + isLoading: meshUpgradeLoading, thisNode, isError, error, } = useMeshUpgrade(); - const [showNodeList, setShowNodeList] = useState(0); - - if (isError) { - return ( - - - Errors found getting mesh info! - {error &&
{error.toString()}
} -
- } - /> - - ); - } - - if (isLoading || meshWideNodes === undefined || thisNode === undefined) { - return ( - - - - ); - } - const tabs = [ - { - key: 0, - repr: ( -
- Show state -
- ), - }, - { - key: 1, - repr: ( -
- Show nodes -
- ), - }, - ]; + const isLoading = + meshUpgradeLoading || + meshWideNodes === undefined || + thisNode === undefined; return ( -
- - - Upgrade all network nodes at once. This proces will take a - while and will require user interaction. - - - {thisNode.upgrade_state === "ABORTED" && ( -
- - This node aborted successfully -
- )} -
- -
- {showNodeList === 0 && } - {showNodeList === 1 && } -
-
- -
+ ); }; diff --git a/plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeQueriesKeys.tsx b/plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeQueriesKeys.tsx index 1e991e11e..22aaa6990 100644 --- a/plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeQueriesKeys.tsx +++ b/plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeQueriesKeys.tsx @@ -1,26 +1,26 @@ import { QueryKey } from "@tanstack/react-query"; import { sharedStateQueries } from "components/shared-state/SharedStateQueriesKeys"; +import { MeshUpgradeTypes } from "components/shared-state/SharedStateTypes"; -interface MeshUpgradeQueryKeysProps { - [key: string]: QueryKey; -} - -const MeshUpgradeQueryKeys: MeshUpgradeQueryKeysProps = { +const MeshUpgradeQueryKeys: { [key: string]: QueryKey } = { getMeshUpgradeNodeStatus: ["lime-mesh-upgrade", "get_node_status"], remoteScheduleUpgrade: ["lime-mesh-upgrade", "schedule_upgrade"], remoteConfirmUpgrade: ["lime-mesh-upgrade", "confirm_boot_partition"], remoteAbort: ["lime-mesh-upgrade", "abort"], }; +export const meshUpgradeSharedStateKey: keyof MeshUpgradeTypes = + "mesh_wide_upgrade"; + export const meshUpgradeQueryKeys = { getMeshWideUpgradeInfo: (): QueryKey => - sharedStateQueries.getFromSharedState("mesh_wide_upgrade"), + sharedStateQueries.getFromSharedState(meshUpgradeSharedStateKey), getMeshUpgradeNodeStatus: (): QueryKey => MeshUpgradeQueryKeys.getMeshUpgradeNodeStatus, remoteScheduleUpgrade: (): QueryKey => MeshUpgradeQueryKeys.remoteScheduleUpgrade, remoteConfirmUpgrade: (): QueryKey => MeshUpgradeQueryKeys.remoteConfirmUpgrade, - remoteAbort: (): QueryKey => MeshUpgradeQueryKeys.remoteConfirmUpgrade, + remoteAbort: (): QueryKey => MeshUpgradeQueryKeys.remoteAbort, }; diff --git a/plugins/lime-plugin-mesh-wide-upgrade/src/utils/api.ts b/plugins/lime-plugin-mesh-wide-upgrade/src/utils/api.ts index dfa1fc9ea..a0b90dbde 100644 --- a/plugins/lime-plugin-mesh-wide-upgrade/src/utils/api.ts +++ b/plugins/lime-plugin-mesh-wide-upgrade/src/utils/api.ts @@ -1,3 +1,7 @@ +import { + MeshWideConfigState, + NodeMeshConfigInfo, +} from "plugins/lime-plugin-mesh-wide-config/src/meshConfigTypes"; import { MeshWideUpgradeInfo, NodeMeshUpgradeInfo, @@ -24,3 +28,22 @@ export const getNodeIpsByCondition = ( ) .map((node) => node.node_ip as string); // 'as string' is safe here due to the filter condition }; + +// todo: merge with the upper and move it to util library +// Use TS to know the type you are going to return on the condition callback using only the nodes type +export const getNodeIpsByConfigCondition = ( + nodes: MeshWideConfigState, + // status: UpgradeStatusType + condition: (node: NodeMeshConfigInfo) => boolean +) => { + if (!nodes) return []; + return Object.values(nodes) + .filter( + (node) => + node.node_ip !== null && + node.node_ip !== undefined && + node.node_ip.trim() !== "" && + condition(node) + ) + .map((node) => node.node_ip as string); // 'as string' is safe here due to the filter condition +}; diff --git a/plugins/lime-plugin-mesh-wide-upgrade/src/utils/divs.tsx b/plugins/lime-plugin-mesh-wide-upgrade/src/utils/divs.tsx deleted file mode 100644 index 4b4e94b44..000000000 --- a/plugins/lime-plugin-mesh-wide-upgrade/src/utils/divs.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { VNode } from "preact"; - -export const CenterFlex = ({ children }: { children: VNode }) => { - return ( -
- {children} -
- ); -}; diff --git a/plugins/lime-plugin-mesh-wide-upgrade/src/utils/upgradeStatusMessages.tsx b/plugins/lime-plugin-mesh-wide-upgrade/src/utils/upgradeStatusMessages.tsx index 049f96ea9..8ff194a6c 100644 --- a/plugins/lime-plugin-mesh-wide-upgrade/src/utils/upgradeStatusMessages.tsx +++ b/plugins/lime-plugin-mesh-wide-upgrade/src/utils/upgradeStatusMessages.tsx @@ -1,7 +1,8 @@ import { Trans } from "@lingui/macro"; import { ComponentChildren } from "preact"; -import { INodeInfoBodyItemProps } from "plugins/lime-plugin-mesh-wide-upgrade/src/components/nodeUpgradeInfo"; +import { INodeInfoBodyItemProps } from "components/mesh-wide-wizard/NodeInfoListItem"; + import { MainNodeStatusType, MeshWideNodeUpgradeInfo, diff --git a/plugins/lime-plugin-rx/src/sections/alignment.tsx b/plugins/lime-plugin-rx/src/sections/alignment.tsx index ffaa09fe0..358bee69b 100644 --- a/plugins/lime-plugin-rx/src/sections/alignment.tsx +++ b/plugins/lime-plugin-rx/src/sections/alignment.tsx @@ -1,6 +1,6 @@ import { Trans } from "@lingui/macro"; -import { Button } from "components/elements/button"; +import { Button } from "components/buttons/button"; import { IconsClassName, diff --git a/plugins/lime-plugin-rx/src/sections/internetPath.tsx b/plugins/lime-plugin-rx/src/sections/internetPath.tsx index 559872961..6d51cf256 100644 --- a/plugins/lime-plugin-rx/src/sections/internetPath.tsx +++ b/plugins/lime-plugin-rx/src/sections/internetPath.tsx @@ -1,7 +1,7 @@ import { Trans } from "@lingui/macro"; import { useCallback } from "react"; -import { Button } from "components/elements/button"; +import { Button } from "components/buttons/button"; import { GlobeIcon } from "components/icons/globeIcon"; import Loading from "components/loading"; diff --git a/src/components/Modal/FullScreenModal.tsx b/src/components/Modal/FullScreenModal.tsx index 31a1a4ce5..2f73428ef 100644 --- a/src/components/Modal/FullScreenModal.tsx +++ b/src/components/Modal/FullScreenModal.tsx @@ -20,8 +20,8 @@ export const FullScreenModal = ({ onClose, }: IFullScreenModalProps) => { return ( -
-
+
+
) : ( -
+
{children}
)} diff --git a/src/components/buttons/button.tsx b/src/components/buttons/button.tsx index b92e8fdec..2b6598360 100644 --- a/src/components/buttons/button.tsx +++ b/src/components/buttons/button.tsx @@ -1,7 +1,7 @@ import { useState } from "preact/hooks"; import React, { useCallback } from "react"; -export interface ButtonProps { +export type ButtonProps = { onClick?: ((e) => void) | ((e) => Promise); children?: any; // type error with Trans component size?: "sm" | "md" | "lg"; @@ -9,7 +9,7 @@ export interface ButtonProps { href?: string; outline?: boolean; disabled?: boolean; -} +} & Omit, "size">; export const Button = ({ size = "md", @@ -72,10 +72,9 @@ export const Button = ({ const cls = `cursor-pointer font-semibold rounded-xl text-center place-content-center transition-all duration-300 justify-center border-0 ${sizeClasses} ${colorClasses}`; - // useCallback for button click const handleClick = useCallback( async (e) => { - if (innerIsLoading || disabled) return; + if (innerIsLoading || disabled || !onClick) return; setInnerIsLoading(true); try { await onClick(e); @@ -83,10 +82,10 @@ export const Button = ({ setInnerIsLoading(false); } }, - [disabled, innerIsLoading, onClick] + [innerIsLoading, disabled, onClick] ); - const Btn = () => ( + const btn = (
handleClick(e)} @@ -96,11 +95,10 @@ export const Button = ({ {children}
); - return href ? ( - - - - ) : ( - - ); + + if (href) { + return {btn}; + } + + return <>{btn}; }; diff --git a/src/components/elements/button.tsx b/src/components/elements/button.tsx deleted file mode 100644 index 1e1a871b7..000000000 --- a/src/components/elements/button.tsx +++ /dev/null @@ -1,57 +0,0 @@ -interface ButtonProps { - onClick?: () => void; - children?: any; // type error with Trans component - size?: "sm" | "md" | "lg"; - color?: "primary" | "secondary"; - href?: string; -} - -export const Button = ({ - size = "md", - color = "primary", - onClick, - children, - href, - ...props -}: ButtonProps) => { - let sizeClasses = "", - colorClasses = ""; - switch (size) { - case "sm": - sizeClasses = "py-2 px-4 text-sm"; - break; - case "md": - sizeClasses = "py-4 px-6 min-w-[theme('spacing[52]')]"; - break; - case "lg": - sizeClasses = "py-6 px-8"; - break; - } - - switch (color) { - case "primary": - colorClasses = "bg-button-primary"; - break; - case "secondary": - colorClasses = "bg-button-secondary"; - break; - } - - const cls = `cursor-pointer text-white font-semibold rounded-xl text-center place-content-center - justify-center border-0 ${sizeClasses} ${colorClasses}`; - - if (href) { - return ( - -
- {children} -
-
- ); - } - return ( -
- {children} -
- ); -}; diff --git a/src/components/inputs/InputField.tsx b/src/components/inputs/InputField.tsx index 2adc49556..ceb60e6e4 100644 --- a/src/components/inputs/InputField.tsx +++ b/src/components/inputs/InputField.tsx @@ -1,29 +1,32 @@ import { ComponentChild } from "preact"; +import { JSXInternal } from "preact/src/jsx"; import { FieldValues, Path } from "react-hook-form"; -import { UseFormRegister } from "react-hook-form/dist/types/form"; -import { RegisterOptions } from "react-hook-form/dist/types/validator"; const InputField = ({ id, label, - register, - options, + error, + ...inputProps }: { id: Path; - label: string | ComponentChild; - register?: UseFormRegister; - options?: RegisterOptions; -}) => { + label?: string | ComponentChild; + error?: string | ComponentChild; +} & Partial< + Omit, "label"> & { + defaultValue?: string; + } +>) => { return ( -
- +
+ {label && } + {error &&

{error}

}
); }; diff --git a/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/ErrorState.tsx b/src/components/mesh-wide-wizard/ErrorState.tsx similarity index 67% rename from plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/ErrorState.tsx rename to src/components/mesh-wide-wizard/ErrorState.tsx index bb8391712..16a810240 100644 --- a/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/ErrorState.tsx +++ b/src/components/mesh-wide-wizard/ErrorState.tsx @@ -3,16 +3,16 @@ import { VNode } from "preact"; import { MeshUpgradeErrorIcon, - UpgradeState, -} from "plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/UpgradeState"; + StepState, +} from "components/mesh-wide-wizard/StepState"; export const ErrorState = ({ msg }: { msg: string | VNode }) => { return ( - Error!} icon={} > {msg} - + ); }; diff --git a/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/LoadingPage.tsx b/src/components/mesh-wide-wizard/LoadingPage.tsx similarity index 55% rename from plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/LoadingPage.tsx rename to src/components/mesh-wide-wizard/LoadingPage.tsx index c17078fe4..9fc4c636a 100644 --- a/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/LoadingPage.tsx +++ b/src/components/mesh-wide-wizard/LoadingPage.tsx @@ -1,8 +1,7 @@ import { VNode } from "preact"; import Loading from "components/loading"; - -import { UpgradeState } from "plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/UpgradeState"; +import { StepState } from "components/mesh-wide-wizard/StepState"; export const LoadingPage = ({ title, @@ -12,8 +11,8 @@ export const LoadingPage = ({ description?: VNode; }) => { return ( - }> + }> {description} - + ); }; diff --git a/src/components/mesh-wide-wizard/NodeInfoListItem.tsx b/src/components/mesh-wide-wizard/NodeInfoListItem.tsx new file mode 100644 index 000000000..9b4d66c87 --- /dev/null +++ b/src/components/mesh-wide-wizard/NodeInfoListItem.tsx @@ -0,0 +1,67 @@ +import { ComponentChildren } from "preact"; + +import { StatusIcon, StatusIcons } from "components/icons/status"; +import { ListItemCollapsible } from "components/list-material"; +import UpdateSharedStateBtn from "components/shared-state/UpdateSharedStateBtn"; +import { ISyncWithNodeProps } from "components/shared-state/useSharedStateSync"; + +export interface INodeInfoBodyItemProps { + title: ComponentChildren; + description: ComponentChildren; +} + +const NodeInfoBodyItem = ({ title, description }: INodeInfoBodyItemProps) => ( +
+
{title}
+
{description}
+
+); + +export interface INodeUpgradeInfoItemProps { + extraInfoItems?: Array; + status?: StatusIcons; + name: string; + descriptionMsg?: ComponentChildren; + ip: string; + sharedStateUpdateTypes: Pick["types"]; +} + +const NodeInfoListItem = ({ + extraInfoItems, + status, + name, + descriptionMsg, + ip, + sharedStateUpdateTypes, +}: INodeUpgradeInfoItemProps) => { + const showUpdateBtn = sharedStateUpdateTypes.length && !!ip; + return ( + } + rightText={ + <> + {showUpdateBtn && ( + + )} + + } + > + {extraInfoItems?.map((item, index) => ( + + ))} + + ); +}; + +export default NodeInfoListItem; diff --git a/src/components/mesh-wide-wizard/NodesListWrapper.tsx b/src/components/mesh-wide-wizard/NodesListWrapper.tsx new file mode 100644 index 000000000..609d93d82 --- /dev/null +++ b/src/components/mesh-wide-wizard/NodesListWrapper.tsx @@ -0,0 +1,47 @@ +import { Trans } from "@lingui/macro"; +import { ComponentType } from "preact"; + +import Loading from "components/loading"; +import { StepState } from "components/mesh-wide-wizard/StepState"; +import { CenterFlex } from "components/mesh-wide-wizard/WizardWrapper"; + +type NodesListProps = { + data: Record | null; + isLoading: boolean; + NodeInfoComponent: ComponentType<{ name: string; info: T }>; +}; + +export const NodesListWrapper = ({ + data, + isLoading, + NodeInfoComponent, +}: NodesListProps) => { + if (isLoading) { + return ; + } + + if (!data || (data && Object.keys(data).length === 0)) { + return ( + + + No nodes present on the
+ mesh wide upgrade state yet! + + } + /> +
+ ); + } + + return ( + <> + {data && + Object.entries(data).map(([key, nodeInfo]) => ( + + ))} + + ); +}; diff --git a/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/UpgradeState.tsx b/src/components/mesh-wide-wizard/StepState.tsx similarity index 83% rename from plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/UpgradeState.tsx rename to src/components/mesh-wide-wizard/StepState.tsx index 7bbb83284..1bf3d6cc2 100644 --- a/plugins/lime-plugin-mesh-wide-upgrade/src/components/upgradeState/UpgradeState.tsx +++ b/src/components/mesh-wide-wizard/StepState.tsx @@ -2,7 +2,7 @@ import { Trans } from "@lingui/macro"; import { ComponentChildren } from "preact"; import { GlobeIcon } from "components/icons/globeIcon"; -import { Tick, Warning } from "components/icons/status"; +import { StatusIcon, Tick, Warning } from "components/icons/status"; import { RemoteNodeCallError } from "components/shared-state/SharedStateApi"; import { StatusMessage } from "components/status/statusMessage"; @@ -14,7 +14,7 @@ interface UpgradeStateProps { children?: ComponentChildren; } -export const UpgradeState = ({ +export const StepState = ({ icon = , title, children, @@ -58,3 +58,14 @@ export const MeshUpgradeSuccessIcon = () => { export const MeshUpgradeErrorIcon = () => { return ; }; + +export const AbortedNotification = () => ( +
+ + This node aborted successfully +
+); diff --git a/src/components/mesh-wide-wizard/WizardWrapper.tsx b/src/components/mesh-wide-wizard/WizardWrapper.tsx new file mode 100644 index 000000000..6276e89ab --- /dev/null +++ b/src/components/mesh-wide-wizard/WizardWrapper.tsx @@ -0,0 +1,103 @@ +import { Trans } from "@lingui/macro"; +import { ComponentType, VNode } from "preact"; +import { useState } from "preact/hooks"; + +import Loading from "components/loading"; +import { ErrorState } from "components/mesh-wide-wizard/ErrorState"; +import Tabs from "components/tabs"; + +interface WrapperProps { + isError?: boolean; + error?: unknown; + isLoading?: boolean; + banner?: ComponentType; + statusPage: ComponentType; + nodesList: ComponentType; + footer?: ComponentType; +} + +const WizardWrapper = ({ + isError, + error, + isLoading, + banner: Banner, + statusPage: StatusPage, + nodesList: NodesList, + footer: Footer, +}: WrapperProps) => { + const [showNodeList, setShowNodeList] = useState(0); + + if (isError) { + return ( + + + Errors found getting info! + {error &&
{error.toString()}
} +
+ } + /> + + ); + } + + if (isLoading) { + return ( + + + + ); + } + + const tabs = [ + { + key: 0, + repr: ( +
+ Show state +
+ ), + }, + { + key: 1, + repr: ( +
+ Show nodes +
+ ), + }, + ]; + + return ( +
+ {Banner && } +
+ +
+ {showNodeList === 0 && ( + + + + )} + {showNodeList === 1 && } +
+
+ {Footer &&
} +
+ ); +}; + +export const CenterFlex = ({ children }: { children: VNode }) => { + return ( +
+ {children} +
+ ); +}; + +export default WizardWrapper; diff --git a/src/components/shared-state/SharedStateApi.ts b/src/components/shared-state/SharedStateApi.ts index 0b6743a9d..3437d7e86 100644 --- a/src/components/shared-state/SharedStateApi.ts +++ b/src/components/shared-state/SharedStateApi.ts @@ -7,9 +7,8 @@ import { SharedStateReturnType, } from "components/shared-state/SharedStateTypes"; -import { MeshUpgradeApiErrorTypes } from "plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeTypes"; - import { login } from "utils/queries"; +import { StandarizedApiError } from "utils/standarizedApi"; import { UhttpdService, default as defaultApi } from "utils/uhttpd.service"; async function syncDataType({ @@ -92,7 +91,7 @@ export async function callToRemoteNode({ return await apiCall(customApi); } catch (error) { let additionalInfo = ""; - if (error instanceof MeshUpgradeApiError) { + if (error instanceof StandarizedApiError) { additionalInfo = `: ${error.message}`; } throw new RemoteNodeCallError( @@ -116,15 +115,3 @@ export class RemoteNodeCallError extends Error { Object.setPrototypeOf(this, RemoteNodeCallError.prototype); } } - -export class MeshUpgradeApiError extends Error { - message: string; - code: MeshUpgradeApiErrorTypes; - constructor(message: string, code: MeshUpgradeApiErrorTypes) { - super(message); // Pass the message to the Error constructor - this.name = "MeshUpgradeApiError"; // Set the name of the error - this.message = message; - this.code = code; - Object.setPrototypeOf(this, MeshUpgradeApiError.prototype); - } -} diff --git a/src/components/shared-state/SharedStateQueriesKeys.ts b/src/components/shared-state/SharedStateQueriesKeys.ts index 2c39e82cb..c284379cc 100644 --- a/src/components/shared-state/SharedStateQueriesKeys.ts +++ b/src/components/shared-state/SharedStateQueriesKeys.ts @@ -3,10 +3,24 @@ import { SharedStateDataTypeKeys, } from "components/shared-state/SharedStateTypes"; -const getFromSharedStateKey = ["shared-state-async", "get"]; -const insertIntoSharedStateKey = ["shared-state-async", "insert"]; -export const syncFromSharedStateKey = ["shared-state-async", "sync"]; -const publishAllFromSharedStateKey = ["shared-state-async", "publish_all"]; +import { ApiServiceParamsType } from "utils/standarizedApi"; + +const getFromSharedStateKey: ApiServiceParamsType = [ + "shared-state-async", + "get", +]; +const insertIntoSharedStateKey: ApiServiceParamsType = [ + "shared-state-async", + "insert", +]; +export const syncFromSharedStateKey: ApiServiceParamsType = [ + "shared-state-async", + "sync", +]; +const publishAllFromSharedStateKey: ApiServiceParamsType = [ + "shared-state-async", + "publish_all", +]; /** * Use this constant to get the query keys to be used as api call parameters. diff --git a/src/components/shared-state/SharedStateTypes.ts b/src/components/shared-state/SharedStateTypes.ts index 6f2496666..0395ca798 100644 --- a/src/components/shared-state/SharedStateTypes.ts +++ b/src/components/shared-state/SharedStateTypes.ts @@ -1,3 +1,4 @@ +import { MeshWideConfigState } from "plugins/lime-plugin-mesh-wide-config/src/meshConfigTypes"; import { MeshWideUpgradeInfo } from "plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeTypes"; import { IBabelLinks, @@ -6,6 +7,10 @@ import { IWifiLinks, } from "plugins/lime-plugin-mesh-wide/src/meshWideTypes"; +export type MeshConfigTypes = { + mesh_config: MeshWideConfigState; +}; + export type MeshUpgradeTypes = { mesh_wide_upgrade: MeshWideUpgradeInfo; }; @@ -25,7 +30,8 @@ export type MeshWideMapReferenceTypes = AppendRef; export type AllSharedStateTypes = MeshWideMapTypes & MeshUpgradeTypes & - MeshWideMapReferenceTypes; + MeshWideMapReferenceTypes & + MeshConfigTypes; export type SharedStateDataTypeKeys = keyof AllSharedStateTypes; diff --git a/src/components/status/statusAndButton.tsx b/src/components/status/statusAndButton.tsx index 632c3e87e..3967b7264 100644 --- a/src/components/status/statusAndButton.tsx +++ b/src/components/status/statusAndButton.tsx @@ -1,6 +1,6 @@ import { VNode } from "preact"; -import { Button } from "components/buttons/button"; +import { Button, ButtonProps } from "components/buttons/button"; import { IStatusMessage, StatusMessage } from "components/status/statusMessage"; export type IStatusAndButton = { @@ -8,6 +8,7 @@ export type IStatusAndButton = { btnCancel?: VNode | string; onClick?: () => void; onClickCancel?: () => void; + btnProps?: ButtonProps; } & IStatusMessage; export const StatusAndButton = ({ @@ -17,6 +18,7 @@ export const StatusAndButton = ({ btnCancel, onClick, onClickCancel, + btnProps, }: IStatusAndButton) => { const containerClasses = "flex flex-col items-center justify-center text-center bg-white py-5 gap-3"; @@ -26,11 +28,19 @@ export const StatusAndButton = ({ {children}
{btnCancel && ( - )} - {btn && } + {btn && ( + + )}
); diff --git a/src/utils/meshWideSyncCall.ts b/src/utils/meshWideSyncCall.ts index 3b3a84c41..49bcdcc8a 100644 --- a/src/utils/meshWideSyncCall.ts +++ b/src/utils/meshWideSyncCall.ts @@ -7,7 +7,7 @@ import { RemoteNodeCallError } from "components/shared-state/SharedStateApi"; import queryCache from "utils/queryCache"; import { useSharedData } from "utils/useSharedData"; -interface IMutationFnVariables { +export interface IMutationFnVariables { ip: string; variables?: TVariables; } diff --git a/src/utils/standarizedApi.ts b/src/utils/standarizedApi.ts new file mode 100644 index 000000000..7107990b4 --- /dev/null +++ b/src/utils/standarizedApi.ts @@ -0,0 +1,45 @@ +import { MeshUpgradeApiErrorTypes } from "plugins/lime-plugin-mesh-wide-upgrade/src/meshUpgradeTypes"; + +import api, { UhttpdService } from "utils/uhttpd.service"; + +export type ApiServiceParamsType = [string, string, object?]; + +// This a util function for standarizes API return calls +// During the development some methods are thought to have a specific return type: +// { +// data: T; +// error: number; +// } +// This is a wrapper function to do the calls to the methods that contains this return type +// Pass the array of arguments that api.call method needs. +export const standarizedApiCall = async ({ + apiService = api, + args, +}: { + apiService?: UhttpdService; + args: ApiServiceParamsType; +}) => { + if (args.length === 2) { + args.push({}); + } + const res = await apiService.call(...args); + if (res?.error && res?.error !== "0" && res?.error !== 0) { + throw new StandarizedApiError(res.error, res.code); + } + if (res.data) return res.data as T; + // Fallback to return the response if there is no data + return res as T; +}; + +export class StandarizedApiError extends Error { + message: string; + code: MeshUpgradeApiErrorTypes; + + constructor(message: string, code: MeshUpgradeApiErrorTypes) { + super(message); // Pass the message to the Error constructor + this.name = "MeshUpgradeApiError"; // Set the name of the error + this.message = message; + this.code = code; + Object.setPrototypeOf(this, StandarizedApiError.prototype); + } +}