feat(team): add page
This commit is contained in:
455
app/management/team/sync_task/page.tsx
Normal file
455
app/management/team/sync_task/page.tsx
Normal file
@@ -0,0 +1,455 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user