diff --git a/src/lang/en/manage.json b/src/lang/en/manage.json index 8a3535c6..4d524f4c 100644 --- a/src/lang/en/manage.json +++ b/src/lang/en/manage.json @@ -16,6 +16,7 @@ "upload": "Upload", "copy": "Copy", "decompress": "Decompress", + "syncer": "Syncer", "backup-restore": "Backup & Restore", "home": "Home", "indexes": "Indexes", diff --git a/src/lang/en/tasks.json b/src/lang/en/tasks.json index 6aaf78f2..c0f81efd 100644 --- a/src/lang/en/tasks.json +++ b/src/lang/en/tasks.json @@ -5,6 +5,26 @@ "copy": "Copy file from a storage to another storage", "decompress": "Download and decompress an archive file", "decompress_upload": "Upload extracted file into target storage", + "syncer": { + "run": "Run", + "task_info": "TaskInfo", + "task_name": "TaskName", + "src_path": "SrcPath", + "dst_path": "DstPath", + "delete_path": "DeletePath", + "task_type": "TaskType", + "lazy_cache": "LazyCache", + "state": "State", + "status": "Status", + "all": "All", + "copy": "Copy", + "move": "Move", + "delete": "Delete", + "copy_and_delete": "CopyAndDelete", + "move_and_delete": "MoveAndDelete", + "two_way_sync": "TwoWaySync", + "child_task_info": "ChildTaskInfo" + }, "done": "Completed", "undone": "Running", "clear_succeeded": "Clear Succeeded", diff --git a/src/pages/manage/routes.tsx b/src/pages/manage/routes.tsx index 03d6429c..612fabc9 100644 --- a/src/pages/manage/routes.tsx +++ b/src/pages/manage/routes.tsx @@ -39,6 +39,14 @@ const hide_routes: Route[] = [ to: "/messenger", component: lazy(() => import("./messenger/Messenger")), }, + { + to: "/syncer/add", + component: lazy(() => import("./syncer/AddOrEdit")), + }, + { + to: "/syncer/edit/:id", + component: lazy(() => import("./syncer/AddOrEdit")), + }, ] const Placeholder = (props: { title: string; to: string }) => { diff --git a/src/pages/manage/sidemenu_items.tsx b/src/pages/manage/sidemenu_items.tsx index b4b73091..ea05e13a 100644 --- a/src/pages/manage/sidemenu_items.tsx +++ b/src/pages/manage/sidemenu_items.tsx @@ -23,7 +23,7 @@ import { IoCopy, IoHome, IoMagnetOutline } from "solid-icons/io" import { Component, lazy } from "solid-js" import { Group, UserRole } from "~/types" import { FaSolidBook, FaSolidDatabase } from "solid-icons/fa" -import { TbArchive } from "solid-icons/tb" +import { TbArchive, TbRepeat } from "solid-icons/tb" export type SideMenuItem = SideMenuItemProps & { component?: Component @@ -155,6 +155,13 @@ export const side_menu_items: SideMenuItem[] = [ }, ], }, + { + title: "manage.sidemenu.syncer", + icon: TbRepeat, + to: "/@manage/tasks/syncer", + role: UserRole.GENERAL, + component: lazy(() => import("~/pages/manage/syncer/Syncer")), + }, { title: "manage.sidemenu.users", icon: BsPersonCircle, diff --git a/src/pages/manage/syncer/AddOrEdit.tsx b/src/pages/manage/syncer/AddOrEdit.tsx new file mode 100644 index 00000000..75c96250 --- /dev/null +++ b/src/pages/manage/syncer/AddOrEdit.tsx @@ -0,0 +1,166 @@ +import { + Button, + FormControl, + FormLabel, + Heading, + Input, + Select, + SelectContent, + SelectIcon, + SelectListbox, + SelectOption, + SelectOptionIndicator, + SelectOptionText, + SelectPlaceholder, + SelectTrigger, + SelectValue, + Switch as HopeSwitch, + VStack, +} from "@hope-ui/solid" +import { MaybeLoading, FolderChooseInput } from "~/components" +import { useFetch, useRouter, useT } from "~/hooks" +import { handleResp, notify, r } from "~/utils" +import { PEmptyResp, PResp } from "~/types" +import { createStore } from "solid-js/store" +import { SyncerTaskArgs, SyncerTaskInfo } from "~/types/syncer_task" +import { For } from "solid-js" + +const AddOrEdit = () => { + const t = useT() + const { params, back } = useRouter() + const { id } = params + const [syncerTask, setSyncerTask] = createStore({ + id: 0, + task_name: "", + src_path: "", + dst_path: "", + task_type: "", + lazy_cache: true, + }) + const [syncerTaskArgLoading, loadSyncerTaskArg] = useFetch( + (): PResp => r.get(`/syncer/get?id=${id}`), + ) + + const initEdit = async () => { + const resp = await loadSyncerTaskArg() + // @ts-ignore + handleResp(resp, setSyncerTask) + } + + const [okLoading, ok] = useFetch((): PEmptyResp => { + return r.post(`/syncer/${id ? "update" : "create"}`, syncerTask) + }) + console.log(syncerTask) + console.log(syncerTask.id) + if (id) { + initEdit() + } + + return ( + + + {t(`global.${id ? "edit" : "add"}`)} + + + {t(`tasks.syncer.task_name`)} + + setSyncerTask("task_name", e.currentTarget.value)} + /> + + + + + {t(`tasks.syncer.src_path`)} + + setSyncerTask("src_path", path)} + onlyFolder + /> + + + + + {t(`tasks.syncer.dst_path`)} + + setSyncerTask("dst_path", path)} + onlyFolder + /> + + + + + {t(`tasks.syncer.task_type`)} + + + + + + {t(`tasks.syncer.lazy_cache`)} + + + setSyncerTask("lazy_cache", e.currentTarget.checked) + } + /> + + + + + ) +} + +export default AddOrEdit diff --git a/src/pages/manage/syncer/Syncer.tsx b/src/pages/manage/syncer/Syncer.tsx new file mode 100644 index 00000000..806bbe93 --- /dev/null +++ b/src/pages/manage/syncer/Syncer.tsx @@ -0,0 +1,240 @@ +import { + Box, + Button, + HStack, + Table, + Tbody, + Td, + Th, + Thead, + Tr, + VStack, +} from "@hope-ui/solid" +import { createSignal, For, onCleanup, Show } from "solid-js" +import { + useFetch, + useListFetch, + useManageTitle, + useRouter, + useT, +} from "~/hooks" +import { handleResp, notify, r } from "~/utils" +import { PPageResp, PEmptyResp, PResp } from "~/types" +import { SyncerTaskArgs, SyncerTaskInfo } from "~/types/syncer_task" +import { DeletePopover } from "~/pages/manage/common/DeletePopover" +import { CompletedStates, TaskState } from "~/pages/manage/tasks/Task" +import SyncerChildTaskInfo from "~/pages/manage/syncer/TaskInfo" + +const Syncer = () => { + const t = useT() + useManageTitle("manage.sidemenu.syncer") + const { to } = useRouter() + + const [showInfoDrawer, setShowInfoDrawer] = createSignal(false) + const [currentTaskId, setCurrentTaskId] = createSignal(null) + + const [listing, getSyncerTasks] = useFetch( + (): PPageResp => r.get("/syncer/list"), + ) + const [deleting, deleteSyncerTask] = useListFetch( + (id: number): PEmptyResp => r.post(`/syncer/delete?id=${id}`), + ) + const [running, runSyncerTask] = useListFetch( + (id: number): PEmptyResp => r.post(`/syncer/run?id=${id}`), + ) + + const [canceling, cancelSyncerTask] = useListFetch( + (id: number): PEmptyResp => r.post(`/syncer/cancel?id=${id}`), + ) + const [, SyncerTaskInfoList] = useFetch( + (): PResp => r.get(`/syncer/taskInfo`), + ) + + const [syncerTaskArgs, setSyncerTaskArgs] = createSignal([]) + const refresh = async () => { + const resp = await getSyncerTasks() + handleResp(resp, (data) => setSyncerTaskArgs(data.content)) + } + + const [syncerTaskInfoList, setSyncerTaskInfoList] = createSignal< + SyncerTaskInfo[] + >([]) + const GetSyncerTaskInfoList = async () => { + const resp = await SyncerTaskInfoList() + handleResp(resp, (data) => { + if (data === null) { + setSyncerTaskInfoList([]) + } else { + setSyncerTaskInfoList(data) + } + }) + } + + refresh() + GetSyncerTaskInfoList() + const interval = setInterval(GetSyncerTaskInfoList, 2000) + onCleanup(() => clearInterval(interval)) + return ( + <> + + + + + + + + + + + {(title) => } + + + + + + + {(syncerTask) => ( + + + + + + + + + )} + + +
{t(`tasks.syncer.${title}`)}{t("global.operations")}
{syncerTask.task_name}{syncerTask.src_path}{syncerTask.dst_path}{t(`tasks.syncer.${syncerTask.task_type}`)} + {" "} + Number(task.id) === syncerTask.id, + )?.state ?? 0 + } + /> + + + + + + + + { + return ( + Number(task.id) === syncerTask.id && + !CompletedStates.includes(task.state) + ) + }) + } + > + + + + { + return ( + Number(task.id) === syncerTask.id && + !CompletedStates.includes(task.state) + ) + }) + } + > + { + const resp = await deleteSyncerTask(syncerTask.id) + handleResp(resp, () => { + notify.success(t("global.delete_success")) + refresh() + }) + }} + /> + + +
+
+
+ Number(t.id) === currentTaskId()) + ?.ChildTaskInfos ?? [] + } + onClose={() => setShowInfoDrawer(false)} + /> + + ) +} + +export default Syncer diff --git a/src/pages/manage/syncer/TaskInfo.tsx b/src/pages/manage/syncer/TaskInfo.tsx new file mode 100644 index 00000000..a4082c04 --- /dev/null +++ b/src/pages/manage/syncer/TaskInfo.tsx @@ -0,0 +1,135 @@ +import { + Box, + Button, + Drawer, + DrawerBody, + DrawerContent, + DrawerHeader, + DrawerOverlay, + HStack, + Progress, + ProgressIndicator, + Text, + useColorModeValue, + VStack, +} from "@hope-ui/solid" +import { createSignal, For } from "solid-js" + +import { useT } from "~/hooks" + +import { ChildTaskInfo } from "~/types/syncer_task" +import { TaskState } from "~/pages/manage/tasks/Task" +import { getMainColor } from "~/store" +import { getPath } from "~/pages/manage/tasks/helper" + +interface TaskInfoProps { + opened: boolean + taskId: number | null + childTasks: ChildTaskInfo[] | [] + onClose: () => void +} + +const SyncerChildTaskInfo = (props: TaskInfoProps) => { + const t = useT() + + const taskTypes = () => { + const set = new Set() + props.childTasks.forEach((task) => set.add(task.task_type)) + return ["all", ...Array.from(set)] + } + + const [selectedType, setSelectedType] = createSignal("all") + const filteredTasks = () => { + if (selectedType() === "all") return props.childTasks + return props.childTasks.filter((task) => task.task_type === selectedType()) + } + + return ( + + + + + {t("tasks.syncer.child_task_info")} - ID: {props.taskId} + + + + + + {(type) => ( + + )} + + + + + {(child) => ( + + + {child.task_type === "delete" ? ( + + + {t("tasks.syncer.delete_path")}:{" "} + {" "} + {child.delete_path} + + ) : ( + <> + + + {t("tasks.syncer.src_path")}: + {" "} + {getPath("", child.src_path)} + + + + {t("tasks.syncer.dst_path")}: + {" "} + {getPath("", child.dst_path)} + + + )} + + + + + + + + + + )} + + + + + + ) +} + +export default SyncerChildTaskInfo diff --git a/src/pages/manage/tasks/Task.tsx b/src/pages/manage/tasks/Task.tsx index 7948431b..4e1867e4 100644 --- a/src/pages/manage/tasks/Task.tsx +++ b/src/pages/manage/tasks/Task.tsx @@ -35,7 +35,13 @@ enum TaskStateEnum { BeforeRetry, } -const StateMap: Record< +export const CompletedStates = [ + TaskStateEnum.Succeeded, + TaskStateEnum.Canceled, + TaskStateEnum.Failed, +] + +export const StateMap: Record< string, | "primary" | "accent" @@ -46,6 +52,7 @@ const StateMap: Record< | "danger" | undefined > = { + [TaskStateEnum.Pending]: "info", [TaskStateEnum.Failed]: "danger", [TaskStateEnum.Succeeded]: "success", [TaskStateEnum.Canceled]: "neutral", diff --git a/src/types/syncer_task.ts b/src/types/syncer_task.ts new file mode 100644 index 00000000..aae1a802 --- /dev/null +++ b/src/types/syncer_task.ts @@ -0,0 +1,25 @@ +export interface SyncerTaskArgs { + id: number + task_name: string + src_path: string + dst_path: string + task_type: string + lazy_cache: boolean +} + +export interface SyncerTaskInfo { + id: string + state: number + task_name: string + ChildTaskInfos: ChildTaskInfo[] +} + +export interface ChildTaskInfo { + task_id: string + task_type: "copy" | "move" | string + src_path: string + dst_path: string + delete_path: string + State: number + Progress: number +}