refactor: use original dashboard UI primitives
This commit is contained in:
@@ -1,23 +1,14 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link, useParams } from "../../shared/router.jsx";
|
||||
import { useDataProvider, useList, useOne } from "../../shared/hooks/dataHooks.js";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
ConfirmDialog,
|
||||
DetailList,
|
||||
Field,
|
||||
Input,
|
||||
Modal,
|
||||
PageHeader,
|
||||
PageState,
|
||||
Select,
|
||||
StatusPill,
|
||||
Table,
|
||||
conflictMessage,
|
||||
} from "../../shared/ui/ui.jsx";
|
||||
import { Alert } from "../../components/ui/alert";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Card, CardContent } from "../../components/ui/card";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "../../components/ui/dialog";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../components/ui/select";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../components/ui/table";
|
||||
import { ConfirmDialog, DetailList, Field, PageHeader, PageState, StatusPill, conflictMessage } from "../../shared/ui/ui.jsx";
|
||||
import { useTitle } from "../../shared/hooks/hooks.js";
|
||||
|
||||
const createInitial = {
|
||||
@@ -63,31 +54,18 @@ function ExitActions({ exit, busy, onAction, onEdit, onDelete }) {
|
||||
const disabled = busy;
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
icon="ri-refresh-line"
|
||||
disabled={disabled}
|
||||
onClick={() => onAction(exit, "check")}
|
||||
>
|
||||
<Button size="sm" disabled={disabled} onClick={() => onAction(exit, "check")}>
|
||||
<i className="ri-refresh-line" aria-hidden="true" />
|
||||
检测
|
||||
</Button>
|
||||
{exit.health_status === "disabled" ? (
|
||||
<Button
|
||||
size="sm"
|
||||
icon="ri-checkbox-circle-line"
|
||||
disabled={disabled}
|
||||
onClick={() => onAction(exit, "enable")}
|
||||
>
|
||||
<Button size="sm" disabled={disabled} onClick={() => onAction(exit, "enable")}>
|
||||
<i className="ri-checkbox-circle-line" aria-hidden="true" />
|
||||
启用
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
icon="ri-forbid-line"
|
||||
disabled={disabled}
|
||||
onClick={() => onAction(exit, "disable")}
|
||||
>
|
||||
<Button size="sm" variant="destructive" disabled={disabled} onClick={() => onAction(exit, "disable")}>
|
||||
<i className="ri-forbid-line" aria-hidden="true" />
|
||||
停用
|
||||
</Button>
|
||||
)}
|
||||
@@ -160,31 +138,10 @@ function ExitCreateModal({
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
title={title}
|
||||
labelledBy="exit-create-title"
|
||||
size="lg"
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onClose} disabled={busy}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
form="exit-create-form"
|
||||
busy={busy}
|
||||
busyText="创建中…"
|
||||
disabled={!valid}
|
||||
>
|
||||
{submitLabel}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<form id="exit-create-form" onSubmit={submit} noValidate>
|
||||
<Dialog open={open} onOpenChange={(nextOpen) => { if (!nextOpen) onClose() }}>
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogHeader><DialogTitle>{title}</DialogTitle></DialogHeader>
|
||||
<form id="exit-create-form" onSubmit={submit} noValidate>
|
||||
{error ? (
|
||||
<Alert variant="destructive" className="mb-4">
|
||||
{conflictMessage(
|
||||
@@ -195,12 +152,12 @@ function ExitCreateModal({
|
||||
) : null}
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<Field id="exit-protocol" label="协议" required helper="代理协议">
|
||||
<Select
|
||||
id="exit-protocol"
|
||||
value={form.protocol}
|
||||
onChange={(event) => update("protocol", event.target.value)}
|
||||
options={protocolOptions}
|
||||
/>
|
||||
<Select value={form.protocol} onValueChange={(value) => update("protocol", value)}>
|
||||
<SelectTrigger id="exit-protocol"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{protocolOptions.map((option) => <SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field id="exit-host" label="主机" required helper="代理服务器地址">
|
||||
<Input
|
||||
@@ -248,7 +205,7 @@ function ExitCreateModal({
|
||||
maxLength={255}
|
||||
value={form.password}
|
||||
onChange={(event) => update("password", event.target.value)}
|
||||
invalid={!credentialsValid}
|
||||
aria-invalid={!credentialsValid || undefined}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
@@ -279,8 +236,15 @@ function ExitCreateModal({
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={onClose} disabled={busy}>取消</Button>
|
||||
<Button variant="default" type="submit" form="exit-create-form" disabled={!valid || busy}>
|
||||
{busy ? "保存中…" : submitLabel}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -293,95 +257,47 @@ function ExitTable({
|
||||
onEdit,
|
||||
onDelete,
|
||||
}) {
|
||||
const columns = [
|
||||
{
|
||||
header: "出口",
|
||||
width: "22%",
|
||||
render: (exit) => (
|
||||
<div className="min-w-0">
|
||||
<Link
|
||||
to={`/network-exits/${encodeURIComponent(exit.id)}`}
|
||||
className="anywhere font-semibold text-ink hover:text-primary"
|
||||
>
|
||||
{exit.id}
|
||||
</Link>
|
||||
<p className="anywhere text-xs text-muted">
|
||||
{exit.protocol}://{exit.host}:{exit.port}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "状态",
|
||||
width: "9%",
|
||||
render: (exit) => <ExitHealthPill exit={exit} />,
|
||||
},
|
||||
{
|
||||
header: "用户名",
|
||||
width: "10%",
|
||||
render: (exit) => (
|
||||
<span className="anywhere">{exit.username || "无"}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "密码",
|
||||
width: "10%",
|
||||
render: (exit) => (
|
||||
<span className="anywhere">{exit.password || "无"}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "出口IP",
|
||||
width: "13%",
|
||||
render: (exit) => (
|
||||
<span className="anywhere">
|
||||
{exit.observed_public_ip || "尚无观测"}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "最近检测",
|
||||
width: "15%",
|
||||
render: (exit) =>
|
||||
exit.last_checked_at
|
||||
? new Date(exit.last_checked_at).toLocaleString("zh-CN")
|
||||
: "未检测",
|
||||
},
|
||||
{
|
||||
header: "绑定账号",
|
||||
width: "13%",
|
||||
render: (exit) => {
|
||||
if (bindingsError) return <span className="text-muted">状态未知</span>;
|
||||
const accounts = accountsByExit.get(exit.id);
|
||||
return (
|
||||
<span className="anywhere">
|
||||
{accounts?.length ? accounts.join("、") : "未绑定"}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "操作",
|
||||
width: "8%",
|
||||
align: "right",
|
||||
render: (exit) => (
|
||||
<ExitActions
|
||||
exit={exit}
|
||||
busy={busy === exit.id}
|
||||
onAction={onAction}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Table
|
||||
columns={columns}
|
||||
rows={exits}
|
||||
rowKey={(exit) => exit.id}
|
||||
minWidth="1100px"
|
||||
/>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>出口</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>用户名</TableHead>
|
||||
<TableHead>密码</TableHead>
|
||||
<TableHead>出口IP</TableHead>
|
||||
<TableHead>最近检测</TableHead>
|
||||
<TableHead>绑定账号</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{exits.map((exit) => {
|
||||
const accounts = accountsByExit.get(exit.id);
|
||||
return (
|
||||
<TableRow key={exit.id}>
|
||||
<TableCell>
|
||||
<div className="min-w-0">
|
||||
<Link to={`/network-exits/${encodeURIComponent(exit.id)}`} className="anywhere font-semibold text-foreground hover:text-primary">{exit.id}</Link>
|
||||
<p className="anywhere text-xs text-muted-foreground">{exit.protocol}://{exit.host}:{exit.port}</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell><ExitHealthPill exit={exit} /></TableCell>
|
||||
<TableCell><span className="anywhere">{exit.username || "无"}</span></TableCell>
|
||||
<TableCell><span className="anywhere">{exit.password || "无"}</span></TableCell>
|
||||
<TableCell><span className="anywhere">{exit.observed_public_ip || "尚无观测"}</span></TableCell>
|
||||
<TableCell>{exit.last_checked_at ? new Date(exit.last_checked_at).toLocaleString("zh-CN") : "未检测"}</TableCell>
|
||||
<TableCell>
|
||||
{bindingsError ? <span className="text-muted-foreground">状态未知</span> : <span className="anywhere">{accounts?.length ? accounts.join("、") : "未绑定"}</span>}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ExitActions exit={exit} busy={busy === exit.id} onAction={onAction} onEdit={onEdit} onDelete={onDelete} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -425,7 +341,7 @@ export function NetworkExitList() {
|
||||
await dataProvider.create({ resource: "network-exits", variables: data });
|
||||
await query.refetch();
|
||||
setNotice({
|
||||
variant: "success",
|
||||
variant: "default",
|
||||
text: "网络出口已创建;检测通过后才可用于账号绑定。",
|
||||
});
|
||||
return true;
|
||||
@@ -449,7 +365,7 @@ export function NetworkExitList() {
|
||||
await query.refetch();
|
||||
setEditing(null);
|
||||
setNotice({
|
||||
variant: "success",
|
||||
variant: "default",
|
||||
text: "网络出口已更新,需重新检测后才能使用。",
|
||||
});
|
||||
return true;
|
||||
@@ -471,7 +387,7 @@ export function NetworkExitList() {
|
||||
await dataProvider.delete({ resource: "network-exits", id: exit.id });
|
||||
await query.refetch();
|
||||
setDeleting(null);
|
||||
setNotice({ variant: "success", text: "网络出口已删除。" });
|
||||
setNotice({ variant: "default", text: "网络出口已删除。" });
|
||||
} catch (reason) {
|
||||
setNotice({
|
||||
variant: "destructive",
|
||||
@@ -489,7 +405,7 @@ export function NetworkExitList() {
|
||||
await dataProvider.networkExitAction(exit.id, action);
|
||||
await Promise.all([query.refetch(), refetchBrowsers()]);
|
||||
setNotice({
|
||||
variant: "success",
|
||||
variant: "default",
|
||||
text:
|
||||
action === "check"
|
||||
? "出口检测完成。"
|
||||
@@ -517,16 +433,13 @@ export function NetworkExitList() {
|
||||
title="网络出口"
|
||||
description="维护固定出口、健康观测与账号绑定"
|
||||
>
|
||||
<Button
|
||||
variant="primary"
|
||||
icon="ri-add-line"
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
<Button variant="default" onClick={() => setCreateOpen(true)}>
|
||||
<i className="ri-add-line" aria-hidden="true" />
|
||||
创建网络出口
|
||||
</Button>
|
||||
</PageHeader>
|
||||
{notice ? (
|
||||
<Alert variant={notice.variant} className="mb-4">
|
||||
<Alert variant={notice.variant === "destructive" ? "destructive" : "default"} className="mb-4">
|
||||
{notice.text}
|
||||
</Alert>
|
||||
) : null}
|
||||
@@ -633,8 +546,11 @@ export function NetworkExitDetail() {
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Button icon="ri-arrow-left-line" as={Link} to="/network-exits">
|
||||
返回出口列表
|
||||
<Button asChild>
|
||||
<Link to="/network-exits">
|
||||
<i className="ri-arrow-left-line" aria-hidden="true" />
|
||||
返回出口列表
|
||||
</Link>
|
||||
</Button>
|
||||
</PageHeader>
|
||||
<Card>
|
||||
@@ -673,11 +589,8 @@ export function NetworkExitDetail() {
|
||||
]}
|
||||
/>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button
|
||||
as={Link}
|
||||
to={`/audit?network_exit_id=${encodeURIComponent(exit.id)}`}
|
||||
>
|
||||
查看审计
|
||||
<Button asChild>
|
||||
<Link to={`/audit?network_exit_id=${encodeURIComponent(exit.id)}`}>查看审计</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
Reference in New Issue
Block a user