456 lines
14 KiB
TypeScript
456 lines
14 KiB
TypeScript
"use client"
|
||
|
||
import { getProjectList } from "@/components/label/api/project"
|
||
import {
|
||
getAllSyncServerList,
|
||
getDataSyncTaskList,
|
||
newSyncTask,
|
||
updateSyncTaskStatus,
|
||
} from "@/components/label/api/sync"
|
||
import { Response } from "@/components/label/api/sync/typing"
|
||
import useAuth from "@/components/label/hooks/useAuth"
|
||
import {
|
||
ActionIcon,
|
||
Button,
|
||
Group,
|
||
Modal,
|
||
Paper,
|
||
Select,
|
||
Stack,
|
||
Text,
|
||
} from "@mantine/core"
|
||
import { modals } from "@mantine/modals"
|
||
import { notifications } from "@mantine/notifications"
|
||
import { IconPlayerPause, IconPlayerPlay, IconPlus, IconRefresh, IconSearch, IconTrash, IconX } from "@tabler/icons-react"
|
||
import { DataTable, DataTableColumn } from "mantine-datatable"
|
||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||
|
||
const StatusEnum = new Map<number, string>([
|
||
[1, "待同步"],
|
||
[2, "同步中"],
|
||
[3, "同步失败"],
|
||
[4, "同步成功"],
|
||
[5, "待删除"],
|
||
[6, "删除中"],
|
||
[7, "删除失败"],
|
||
[8, "删除完成"],
|
||
[9, "等待暂停"],
|
||
[10, "已暂停"],
|
||
[11, "等待关闭"],
|
||
[12, "已关闭"],
|
||
])
|
||
|
||
export default function TeamSyncTaskPage() {
|
||
const { isShow } = useAuth()
|
||
|
||
const [records, setRecords] = useState<Response.SyncTask[]>([])
|
||
const [loading, setLoading] = useState(false)
|
||
const [totalItems, setTotalItems] = useState(0)
|
||
const [page, setPage] = useState(1)
|
||
const [pageSize, setPageSize] = useState(20)
|
||
|
||
const [status, setStatus] = useState<string | null>(null)
|
||
const [projectId, setProjectId] = useState<string | null>(null)
|
||
const [serverId, setServerId] = useState<string | null>(null)
|
||
const [appliedStatus, setAppliedStatus] = useState<string | null>(null)
|
||
const [appliedProjectId, setAppliedProjectId] = useState<string | null>(null)
|
||
const [appliedServerId, setAppliedServerId] = useState<string | null>(null)
|
||
|
||
const [projectOpt, setProjectOpt] = useState<Array<{ label: string; value: string }>>([])
|
||
const [serverAllOpt, setServerAllOpt] = useState<Array<{ label: string; value: string }>>([])
|
||
const [serverOnlineOpt, setServerOnlineOpt] = useState<Array<{ label: string; value: string }>>(
|
||
[]
|
||
)
|
||
const [serverMap, setServerMap] = useState<Map<number, string>>(new Map())
|
||
|
||
const [open, setOpen] = useState(false)
|
||
const [newProjectId, setNewProjectId] = useState<string | null>(null)
|
||
const [newServerId, setNewServerId] = useState<string | null>(null)
|
||
const [creating, setCreating] = useState(false)
|
||
|
||
const loadOptions = useCallback(async () => {
|
||
const [projectRes, serverRes] = await Promise.all([
|
||
getProjectList({ page_number: 1, page_size: 10000 }),
|
||
getAllSyncServerList(),
|
||
])
|
||
|
||
const projects = projectRes?.project_list ?? []
|
||
setProjectOpt(projects.map((p) => ({ label: p.name, value: String(p.id) })))
|
||
|
||
const allServers = serverRes ?? []
|
||
setServerAllOpt(allServers.map((s) => ({ label: s.ip, value: String(s.id) })))
|
||
setServerOnlineOpt(
|
||
allServers
|
||
.filter((s) => s.is_online)
|
||
.map((s) => ({ label: s.ip, value: String(s.id) }))
|
||
)
|
||
setServerMap(new Map(allServers.map((s) => [s.id, s.ip])))
|
||
}, [])
|
||
|
||
const load = useCallback(async () => {
|
||
try {
|
||
setLoading(true)
|
||
const params: any = {
|
||
page_number: page,
|
||
page_size: pageSize,
|
||
}
|
||
if (appliedStatus) params.status = Number(appliedStatus)
|
||
if (appliedProjectId) params.project_id = Number(appliedProjectId)
|
||
if (appliedServerId) params.sync_server_id = Number(appliedServerId)
|
||
|
||
const res = await getDataSyncTaskList(params)
|
||
setRecords(res?.sync_tasks ?? [])
|
||
setTotalItems(res?.total_items ?? 0)
|
||
} catch (e) {
|
||
setRecords([])
|
||
setTotalItems(0)
|
||
notifications.show({
|
||
color: "red",
|
||
title: "加载失败",
|
||
message: e instanceof Error ? e.message : "请求失败",
|
||
})
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}, [appliedProjectId, appliedServerId, appliedStatus, page, pageSize])
|
||
|
||
useEffect(() => {
|
||
queueMicrotask(loadOptions)
|
||
}, [loadOptions])
|
||
|
||
useEffect(() => {
|
||
load()
|
||
}, [load])
|
||
|
||
const performStatusAction = async (id: number, statusTo: number, successText: string) => {
|
||
try {
|
||
await updateSyncTaskStatus(id, statusTo)
|
||
notifications.show({
|
||
color: "green",
|
||
title: "操作成功",
|
||
message: successText,
|
||
})
|
||
load()
|
||
} catch (e) {
|
||
notifications.show({
|
||
color: "red",
|
||
title: "操作失败",
|
||
message: e instanceof Error ? e.message : "请求失败",
|
||
})
|
||
}
|
||
}
|
||
|
||
const columns = useMemo(() => {
|
||
const cols: DataTableColumn<Response.SyncTask>[] = [
|
||
{ accessor: "id", title: "ID", width: 80 },
|
||
{ accessor: "uuid", title: "UUID", width: 320 },
|
||
{
|
||
accessor: "status",
|
||
title: "状态",
|
||
width: 120,
|
||
render: (record) => StatusEnum.get(record.status ?? 0) ?? "-",
|
||
},
|
||
{ accessor: "project_name", title: "项目名称", width: 260 },
|
||
{
|
||
accessor: "sync_server_id",
|
||
title: "同步服务IP",
|
||
width: 160,
|
||
render: (record) =>
|
||
record.sync_server_id ? serverMap.get(record.sync_server_id) ?? "-" : "-",
|
||
},
|
||
{ accessor: "location", title: "IP所在地", width: 140 },
|
||
{ accessor: "create_at", title: "创建时间", width: 180 },
|
||
{ accessor: "update_at", title: "更新时间", width: 180 },
|
||
{
|
||
accessor: "operation",
|
||
title: "操作",
|
||
width: 200,
|
||
textAlign: "center",
|
||
render: (record) => {
|
||
const couldRun = isShow("task_edit") && [10].includes(record.status ?? -1)
|
||
const couldStop = isShow("task_edit") && [1, 2].includes(record.status ?? -1)
|
||
const couldDelete =
|
||
isShow("task_edit") && [1, 2, 3, 4, 9, 10].includes(record.status ?? -1)
|
||
const couldClose =
|
||
isShow("task_edit") && ![11, 12].includes(record.status ?? -1)
|
||
|
||
return (
|
||
<Group justify="center" gap={4} wrap="nowrap">
|
||
{couldRun ? (
|
||
<ActionIcon
|
||
variant="subtle"
|
||
color="green"
|
||
onClick={() =>
|
||
performStatusAction(record.id as number, 1, "已恢复同步")
|
||
}>
|
||
<IconPlayerPlay size={16} />
|
||
</ActionIcon>
|
||
) : null}
|
||
{couldStop ? (
|
||
<ActionIcon
|
||
variant="subtle"
|
||
color="yellow"
|
||
onClick={() => {
|
||
modals.openConfirmModal({
|
||
title: "暂停任务",
|
||
centered: true,
|
||
children: (
|
||
<Text size="sm">是否确定暂停当前同步任务?</Text>
|
||
),
|
||
labels: { confirm: "确定", cancel: "取消" },
|
||
onConfirm: () =>
|
||
performStatusAction(record.id as number, 9, "已暂停任务"),
|
||
})
|
||
}}>
|
||
<IconPlayerPause size={16} />
|
||
</ActionIcon>
|
||
) : null}
|
||
{couldDelete ? (
|
||
<ActionIcon
|
||
variant="subtle"
|
||
color="red"
|
||
onClick={() => {
|
||
modals.openConfirmModal({
|
||
title: "删除任务",
|
||
centered: true,
|
||
children: (
|
||
<Text size="sm">是否确定删除当前同步任务?</Text>
|
||
),
|
||
labels: { confirm: "确定", cancel: "取消" },
|
||
confirmProps: { color: "red" },
|
||
onConfirm: () =>
|
||
performStatusAction(record.id as number, 5, "已提交删除"),
|
||
})
|
||
}}>
|
||
<IconTrash size={16} />
|
||
</ActionIcon>
|
||
) : null}
|
||
{couldClose ? (
|
||
<ActionIcon
|
||
variant="subtle"
|
||
color="gray"
|
||
onClick={() => {
|
||
modals.openConfirmModal({
|
||
title: "关闭任务",
|
||
centered: true,
|
||
children: (
|
||
<Text size="sm">是否确定关闭当前同步任务?</Text>
|
||
),
|
||
labels: { confirm: "确定", cancel: "取消" },
|
||
onConfirm: () =>
|
||
performStatusAction(record.id as number, 11, "已关闭任务"),
|
||
})
|
||
}}>
|
||
<IconX size={16} />
|
||
</ActionIcon>
|
||
) : null}
|
||
</Group>
|
||
)
|
||
},
|
||
},
|
||
]
|
||
return cols
|
||
}, [isShow, serverMap])
|
||
|
||
const createTask = async () => {
|
||
if (!newProjectId || !newServerId) {
|
||
notifications.show({
|
||
color: "yellow",
|
||
title: "参数不完整",
|
||
message: "请先选择项目和同步服务",
|
||
})
|
||
return
|
||
}
|
||
try {
|
||
setCreating(true)
|
||
await newSyncTask({
|
||
project_id: Number(newProjectId),
|
||
sync_server_id: Number(newServerId),
|
||
})
|
||
notifications.show({
|
||
color: "green",
|
||
title: "操作成功",
|
||
message: "已新增同步任务",
|
||
})
|
||
setOpen(false)
|
||
setNewProjectId(null)
|
||
setNewServerId(null)
|
||
load()
|
||
} catch (e) {
|
||
notifications.show({
|
||
color: "red",
|
||
title: "操作失败",
|
||
message: e instanceof Error ? e.message : "请求失败",
|
||
})
|
||
} finally {
|
||
setCreating(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Stack w="100%" h="calc(100vh - 56px)" p="md" gap="md">
|
||
<Paper
|
||
withBorder
|
||
p="md"
|
||
radius="md"
|
||
style={{
|
||
borderColor:
|
||
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
|
||
}}>
|
||
<Group justify="space-between" wrap="wrap">
|
||
<Group>
|
||
<Select
|
||
placeholder="任务状态"
|
||
size="xs"
|
||
radius="xs"
|
||
data={Array.from(StatusEnum.entries()).map(([value, label]) => ({
|
||
label,
|
||
value: String(value),
|
||
}))}
|
||
clearable
|
||
searchable
|
||
value={status}
|
||
onChange={setStatus}
|
||
/>
|
||
<Select
|
||
placeholder="项目名称"
|
||
size="xs"
|
||
radius="xs"
|
||
data={projectOpt}
|
||
clearable
|
||
searchable
|
||
value={projectId}
|
||
onChange={setProjectId}
|
||
/>
|
||
<Select
|
||
placeholder="同步服务IP"
|
||
size="xs"
|
||
radius="xs"
|
||
data={serverAllOpt}
|
||
clearable
|
||
searchable
|
||
value={serverId}
|
||
onChange={setServerId}
|
||
/>
|
||
<Button
|
||
size="xs"
|
||
radius="xs"
|
||
leftSection={<IconSearch size={16} />}
|
||
onClick={() => {
|
||
setAppliedStatus(status)
|
||
setAppliedProjectId(projectId)
|
||
setAppliedServerId(serverId)
|
||
setPage(1)
|
||
}}>
|
||
查询
|
||
</Button>
|
||
<Button
|
||
variant="default"
|
||
size="xs"
|
||
radius="xs"
|
||
leftSection={<IconRefresh size={16} />}
|
||
onClick={() => {
|
||
setStatus(null)
|
||
setProjectId(null)
|
||
setServerId(null)
|
||
setAppliedStatus(null)
|
||
setAppliedProjectId(null)
|
||
setAppliedServerId(null)
|
||
setPage(1)
|
||
setPageSize(20)
|
||
}}>
|
||
重置
|
||
</Button>
|
||
</Group>
|
||
{isShow("task_add") ? (
|
||
<Button
|
||
size="xs"
|
||
radius="xs"
|
||
leftSection={<IconPlus size={16} />}
|
||
onClick={() => {
|
||
setNewProjectId(null)
|
||
setNewServerId(null)
|
||
setOpen(true)
|
||
}}>
|
||
新增同步任务
|
||
</Button>
|
||
) : null}
|
||
</Group>
|
||
</Paper>
|
||
|
||
<Paper
|
||
withBorder
|
||
p="md"
|
||
radius="md"
|
||
style={{
|
||
borderColor:
|
||
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
|
||
flex: 1,
|
||
minHeight: 0,
|
||
display: "flex",
|
||
}}>
|
||
<DataTable<Response.SyncTask>
|
||
withTableBorder
|
||
withRowBorders
|
||
fetching={loading}
|
||
records={records}
|
||
columns={columns}
|
||
totalRecords={totalItems}
|
||
recordsPerPage={pageSize}
|
||
page={page}
|
||
onPageChange={setPage}
|
||
onRecordsPerPageChange={(v) => {
|
||
setPageSize(v)
|
||
setPage(1)
|
||
}}
|
||
recordsPerPageOptions={[10, 20, 50, 100]}
|
||
noRecordsText="暂无数据"
|
||
scrollAreaProps={{ type: "auto" }}
|
||
style={{ width: "100%" }}
|
||
/>
|
||
</Paper>
|
||
|
||
<Modal
|
||
opened={open}
|
||
onClose={() => setOpen(false)}
|
||
title="新增同步任务"
|
||
centered
|
||
size={520}
|
||
closeOnClickOutside={false}>
|
||
<Stack gap="sm">
|
||
<Select
|
||
label="项目名称"
|
||
size="xs"
|
||
radius="xs"
|
||
data={projectOpt}
|
||
searchable
|
||
value={newProjectId}
|
||
onChange={setNewProjectId}
|
||
/>
|
||
<Select
|
||
label="同步服务IP(仅在线)"
|
||
size="xs"
|
||
radius="xs"
|
||
data={serverOnlineOpt}
|
||
searchable
|
||
value={newServerId}
|
||
onChange={setNewServerId}
|
||
/>
|
||
<Group justify="flex-end">
|
||
<Button variant="default" size="xs" radius="xs" onClick={() => setOpen(false)}>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
size="xs"
|
||
radius="xs"
|
||
loading={creating}
|
||
onClick={createTask}>
|
||
确定
|
||
</Button>
|
||
</Group>
|
||
</Stack>
|
||
</Modal>
|
||
</Stack>
|
||
)
|
||
}
|