mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 00:50:46 +08:00
Allow biz_state tasks from all CLI targets with searchable NE picker.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
0a967b4172
commit
4811a25ac3
4 changed files with 229 additions and 51 deletions
|
|
@ -105,25 +105,31 @@ def _ne_meta(db: Session, *, source: str, ne_id: str) -> dict[str, str]:
|
||||||
"vendor": str(getattr(row, "vendor", "") or ""),
|
"vendor": str(getattr(row, "vendor", "") or ""),
|
||||||
"device_type": str(getattr(row, "device_type", "") or ""),
|
"device_type": str(getattr(row, "device_type", "") or ""),
|
||||||
}
|
}
|
||||||
# ume: best-effort from inventory if present
|
if src == "ume":
|
||||||
try:
|
|
||||||
from ..models import UmeInventoryNE
|
from ..models import UmeInventoryNE
|
||||||
|
|
||||||
row = db.get(UmeInventoryNE, nid)
|
row = db.get(UmeInventoryNE, nid)
|
||||||
if row:
|
if not row:
|
||||||
return {
|
raise HTTPException(status_code=404, detail="ume_ne_not_found")
|
||||||
"ne_name": str(getattr(row, "name", "") or ""),
|
return {
|
||||||
"ne_ip": str(getattr(row, "ip", "") or ""),
|
"ne_name": str(
|
||||||
"vendor": str(getattr(row, "vendor", "") or ""),
|
getattr(row, "user_label", "")
|
||||||
"device_type": str(getattr(row, "device_type", "") or ""),
|
or getattr(row, "ne_name", "")
|
||||||
}
|
or getattr(row, "host_name", "")
|
||||||
except Exception:
|
or getattr(row, "ip_address", "")
|
||||||
pass
|
or nid
|
||||||
return {"ne_name": "", "ne_ip": "", "vendor": "", "device_type": ""}
|
),
|
||||||
|
"ne_ip": str(getattr(row, "ip_address", "") or getattr(row, "ip", "") or ""),
|
||||||
|
"vendor": str(getattr(row, "vendor", "") or ""),
|
||||||
|
"device_type": str(getattr(row, "device_type", "") or getattr(row, "ne_type", "") or ""),
|
||||||
|
}
|
||||||
|
raise HTTPException(status_code=400, detail="invalid_source")
|
||||||
|
|
||||||
|
|
||||||
def create_task(db: Session, body: dict[str, Any]) -> dict[str, Any]:
|
def create_task(db: Session, body: dict[str, Any]) -> dict[str, Any]:
|
||||||
source = str(body.get("source") or "managed").strip().lower() or "managed"
|
source = str(body.get("source") or "managed").strip().lower() or "managed"
|
||||||
|
if source not in ("managed", "ume"):
|
||||||
|
raise HTTPException(status_code=400, detail="invalid_source")
|
||||||
ne_id = str(body.get("ne_id") or "").strip()
|
ne_id = str(body.get("ne_id") or "").strip()
|
||||||
if not ne_id:
|
if not ne_id:
|
||||||
raise HTTPException(status_code=400, detail="ne_id_required")
|
raise HTTPException(status_code=400, detail="ne_id_required")
|
||||||
|
|
|
||||||
|
|
@ -169,11 +169,18 @@ const en = {
|
||||||
bizState: {
|
bizState: {
|
||||||
title: "Business state",
|
title: "Business state",
|
||||||
create: "Create task",
|
create: "Create task",
|
||||||
createHint: "Pick a managed NE (LLDP enabled by default)",
|
createHint: "Pick any valid NE (managed / UME); search supported",
|
||||||
created: "Task created",
|
created: "Task created",
|
||||||
createFailed: "Create failed",
|
createFailed: "Create failed",
|
||||||
empty: "No business-state tasks yet.",
|
empty: "No business-state tasks yet.",
|
||||||
pickNe: "Select managed NE…",
|
pickNe: "Select NE…",
|
||||||
|
neKeywordPh: "Name / IP / ID",
|
||||||
|
allSource: "All sources",
|
||||||
|
selectedNe: "Selected",
|
||||||
|
neEmpty: "No matching NEs.",
|
||||||
|
colSource: "Source",
|
||||||
|
colVendor: "Vendor",
|
||||||
|
colConnect: "Connect",
|
||||||
start: "Start schedule",
|
start: "Start schedule",
|
||||||
pause: "Pause",
|
pause: "Pause",
|
||||||
collectNow: "Collect now",
|
collectNow: "Collect now",
|
||||||
|
|
|
||||||
|
|
@ -169,11 +169,18 @@ const zh = {
|
||||||
bizState: {
|
bizState: {
|
||||||
title: "业务状态监控",
|
title: "业务状态监控",
|
||||||
create: "创建任务",
|
create: "创建任务",
|
||||||
createHint: "选择托管网元后创建(默认启用 LLDP)",
|
createHint: "从托管 / UME 等有效网元中选择(支持查找)",
|
||||||
created: "已创建任务",
|
created: "已创建任务",
|
||||||
createFailed: "创建失败",
|
createFailed: "创建失败",
|
||||||
empty: "暂无业务监控任务。",
|
empty: "暂无业务监控任务。",
|
||||||
pickNe: "选择托管网元…",
|
pickNe: "选择网元…",
|
||||||
|
neKeywordPh: "名称 / IP / ID",
|
||||||
|
allSource: "全部来源",
|
||||||
|
selectedNe: "已选",
|
||||||
|
neEmpty: "无匹配网元。",
|
||||||
|
colSource: "来源",
|
||||||
|
colVendor: "厂商",
|
||||||
|
colConnect: "连通",
|
||||||
start: "启动周期",
|
start: "启动周期",
|
||||||
pause: "暂停",
|
pause: "暂停",
|
||||||
collectNow: "立即采集",
|
collectNow: "立即采集",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Button, Input, Modal } from "@heroui/react";
|
import { Button, Input, Modal } from "@heroui/react";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { ListPager } from "../../components/ListPager";
|
||||||
import { AppModalShell } from "../../components/ui/AppModalShell";
|
import { AppModalShell } from "../../components/ui/AppModalShell";
|
||||||
import { FieldSelect } from "../../components/ui/FieldSelect";
|
import { FieldSelect } from "../../components/ui/FieldSelect";
|
||||||
import { useDebouncedValue } from "../../hooks/useDebouncedValue";
|
import { useDebouncedValue } from "../../hooks/useDebouncedValue";
|
||||||
|
|
@ -22,11 +23,13 @@ import {
|
||||||
formatErr,
|
formatErr,
|
||||||
} from "../../services/api";
|
} from "../../services/api";
|
||||||
import type { CliTargetItem } from "../../types";
|
import type { CliTargetItem } from "../../types";
|
||||||
|
import { pageCount } from "../../utils/display";
|
||||||
import { formatSystemTime } from "../../utils/time";
|
import { formatSystemTime } from "../../utils/time";
|
||||||
import { jobChipColor, NmStatusChip } from "./nmChips";
|
import { jobChipColor, NmStatusChip, sourceChipColor } from "./nmChips";
|
||||||
|
|
||||||
type TaskRow = {
|
type TaskRow = {
|
||||||
id: string;
|
id: string;
|
||||||
|
source?: string;
|
||||||
ne_name: string;
|
ne_name: string;
|
||||||
ne_ip: string;
|
ne_ip: string;
|
||||||
vendor: string;
|
vendor: string;
|
||||||
|
|
@ -66,6 +69,9 @@ type SheetDef = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type TaskTab = "profiles" | "batches";
|
type TaskTab = "profiles" | "batches";
|
||||||
|
type NeSourceFilter = "all" | "managed" | "ume";
|
||||||
|
|
||||||
|
const NE_PAGE_SIZE = 10;
|
||||||
|
|
||||||
function fmtTime(v?: string | null) {
|
function fmtTime(v?: string | null) {
|
||||||
if (!v) return "—";
|
if (!v) return "—";
|
||||||
|
|
@ -78,6 +84,10 @@ function cellText(v: unknown): string {
|
||||||
return String(v);
|
return String(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function neSourceOf(row: CliTargetItem): "managed" | "ume" {
|
||||||
|
return row.source === "ume" ? "ume" : "managed";
|
||||||
|
}
|
||||||
|
|
||||||
function buildBatchSheets(batch: any, t: (k: string) => string): SheetDef[] {
|
function buildBatchSheets(batch: any, t: (k: string) => string): SheetDef[] {
|
||||||
const sheets: SheetDef[] = [];
|
const sheets: SheetDef[] = [];
|
||||||
const cmds = (batch?.commands || []) as any[];
|
const cmds = (batch?.commands || []) as any[];
|
||||||
|
|
@ -152,12 +162,21 @@ export function BizStatePage() {
|
||||||
const { showOk, showError } = useToast();
|
const { showOk, showError } = useToast();
|
||||||
|
|
||||||
const [tasks, setTasks] = useState<TaskRow[]>([]);
|
const [tasks, setTasks] = useState<TaskRow[]>([]);
|
||||||
const [nes, setNes] = useState<CliTargetItem[]>([]);
|
|
||||||
const [neId, setNeId] = useState("");
|
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [listKeyword, setListKeyword] = useState("");
|
const [listKeyword, setListKeyword] = useState("");
|
||||||
const debouncedListKw = useDebouncedValue(listKeyword, 250);
|
const debouncedListKw = useDebouncedValue(listKeyword, 250);
|
||||||
|
|
||||||
|
// create-task modal (all valid CLI targets)
|
||||||
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
|
const [neKeyword, setNeKeyword] = useState("");
|
||||||
|
const debouncedNeKw = useDebouncedValue(neKeyword, 300);
|
||||||
|
const [neSource, setNeSource] = useState<NeSourceFilter>("all");
|
||||||
|
const [nePage, setNePage] = useState(1);
|
||||||
|
const [neTotal, setNeTotal] = useState(0);
|
||||||
|
const [neItems, setNeItems] = useState<CliTargetItem[]>([]);
|
||||||
|
const [neLoading, setNeLoading] = useState(false);
|
||||||
|
const [selectedNe, setSelectedNe] = useState<CliTargetItem | null>(null);
|
||||||
|
|
||||||
// task modal
|
// task modal
|
||||||
const [taskId, setTaskId] = useState("");
|
const [taskId, setTaskId] = useState("");
|
||||||
const [detail, setDetail] = useState<any>(null);
|
const [detail, setDetail] = useState<any>(null);
|
||||||
|
|
@ -187,13 +206,6 @@ export function BizStatePage() {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
await refreshTasks();
|
await refreshTasks();
|
||||||
const neRes = await fetchCliTargets({
|
|
||||||
source: "managed",
|
|
||||||
keyword: "",
|
|
||||||
page: 1,
|
|
||||||
pageSize: 200,
|
|
||||||
});
|
|
||||||
setNes(neRes.items || []);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError(formatErr(e));
|
showError(formatErr(e));
|
||||||
}
|
}
|
||||||
|
|
@ -201,11 +213,40 @@ export function BizStatePage() {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- mount once
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- mount once
|
||||||
}, [refreshTasks]);
|
}, [refreshTasks]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!createOpen) return;
|
||||||
|
let cancelled = false;
|
||||||
|
setNeLoading(true);
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetchCliTargets({
|
||||||
|
source: neSource,
|
||||||
|
keyword: debouncedNeKw,
|
||||||
|
page: nePage,
|
||||||
|
pageSize: NE_PAGE_SIZE,
|
||||||
|
});
|
||||||
|
if (cancelled) return;
|
||||||
|
setNeItems(res.items || []);
|
||||||
|
setNeTotal(Number(res.total || 0));
|
||||||
|
} catch (e) {
|
||||||
|
if (!cancelled) showError(formatErr(e));
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) setNeLoading(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [createOpen, neSource, debouncedNeKw, nePage]);
|
||||||
|
|
||||||
|
const nePages = pageCount(neTotal, NE_PAGE_SIZE);
|
||||||
|
|
||||||
const filteredTasks = useMemo(() => {
|
const filteredTasks = useMemo(() => {
|
||||||
const kw = debouncedListKw.trim().toLowerCase();
|
const kw = debouncedListKw.trim().toLowerCase();
|
||||||
if (!kw) return tasks;
|
if (!kw) return tasks;
|
||||||
return tasks.filter((row) => {
|
return tasks.filter((row) => {
|
||||||
const blob = `${row.ne_name} ${row.ne_ip} ${row.vendor} ${row.status} ${row.last_error}`.toLowerCase();
|
const blob =
|
||||||
|
`${row.ne_name} ${row.ne_ip} ${row.vendor} ${row.status} ${row.source || ""} ${row.last_error}`.toLowerCase();
|
||||||
return blob.includes(kw);
|
return blob.includes(kw);
|
||||||
});
|
});
|
||||||
}, [tasks, debouncedListKw]);
|
}, [tasks, debouncedListKw]);
|
||||||
|
|
@ -235,6 +276,19 @@ export function BizStatePage() {
|
||||||
);
|
);
|
||||||
}, [activeSheet, debouncedSheetKw, sheetColumn]);
|
}, [activeSheet, debouncedSheetKw, sheetColumn]);
|
||||||
|
|
||||||
|
const openCreate = () => {
|
||||||
|
setCreateOpen(true);
|
||||||
|
setSelectedNe(null);
|
||||||
|
setNeKeyword("");
|
||||||
|
setNeSource("all");
|
||||||
|
setNePage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeCreate = () => {
|
||||||
|
setCreateOpen(false);
|
||||||
|
setSelectedNe(null);
|
||||||
|
};
|
||||||
|
|
||||||
const loadTask = async (id: string) => {
|
const loadTask = async (id: string) => {
|
||||||
const task = await bizStateGetTask(id);
|
const task = await bizStateGetTask(id);
|
||||||
setDetail(task);
|
setDetail(task);
|
||||||
|
|
@ -269,21 +323,20 @@ export function BizStatePage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const createTask = async () => {
|
const createTask = async () => {
|
||||||
if (!neId) return;
|
if (!selectedNe) return;
|
||||||
const ne = nes.find((n) => n.id === neId);
|
const source = neSourceOf(selectedNe);
|
||||||
if (!ne) return;
|
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
const task = await bizStateCreateTask({
|
const task = await bizStateCreateTask({
|
||||||
source: "managed",
|
source,
|
||||||
ne_id: ne.id,
|
ne_id: selectedNe.id,
|
||||||
ne_name: ne.name,
|
ne_name: selectedNe.name,
|
||||||
ne_ip: ne.ip_address,
|
ne_ip: selectedNe.ip_address,
|
||||||
vendor: ne.vendor,
|
vendor: selectedNe.vendor || "",
|
||||||
device_type: ne.device_type,
|
device_type: selectedNe.device_type || "",
|
||||||
});
|
});
|
||||||
showOk(t("bizState.created"));
|
showOk(t("bizState.created"));
|
||||||
setNeId("");
|
closeCreate();
|
||||||
await refreshTasks();
|
await refreshTasks();
|
||||||
await openTask(String(task.id), "profiles");
|
await openTask(String(task.id), "profiles");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -458,19 +511,7 @@ export function BizStatePage() {
|
||||||
<div className="panel__toolbar">
|
<div className="panel__toolbar">
|
||||||
<h2>{t("bizState.title")}</h2>
|
<h2>{t("bizState.title")}</h2>
|
||||||
<div className="btn-row">
|
<div className="btn-row">
|
||||||
<FieldSelect
|
<Button size="sm" variant="primary" onPress={openCreate}>
|
||||||
value={neId}
|
|
||||||
onChange={(e) => setNeId(e.target.value)}
|
|
||||||
aria-label={t("bizState.pickNe")}
|
|
||||||
>
|
|
||||||
<option value="">{t("bizState.pickNe")}</option>
|
|
||||||
{nes.map((n) => (
|
|
||||||
<option key={n.id} value={n.id}>
|
|
||||||
{n.name || n.ip_address} ({n.vendor || "-"})
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</FieldSelect>
|
|
||||||
<Button size="sm" variant="primary" isDisabled={busy || !neId} onPress={() => void createTask()}>
|
|
||||||
{t("bizState.create")}
|
{t("bizState.create")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -501,6 +542,7 @@ export function BizStatePage() {
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{t("bizState.colNe")}</th>
|
<th>{t("bizState.colNe")}</th>
|
||||||
|
<th>{t("bizState.colSource")}</th>
|
||||||
<th>{t("bizState.colStatus")}</th>
|
<th>{t("bizState.colStatus")}</th>
|
||||||
<th>{t("bizState.colLast")}</th>
|
<th>{t("bizState.colLast")}</th>
|
||||||
<th>{t("bizState.colActions")}</th>
|
<th>{t("bizState.colActions")}</th>
|
||||||
|
|
@ -516,6 +558,11 @@ export function BizStatePage() {
|
||||||
</div>
|
</div>
|
||||||
{row.last_error ? <div className="form-error">{row.last_error}</div> : null}
|
{row.last_error ? <div className="form-error">{row.last_error}</div> : null}
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<NmStatusChip color={sourceChipColor(row.source)}>
|
||||||
|
{row.source || "managed"}
|
||||||
|
</NmStatusChip>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div className="pt-list-actions" style={{ flexWrap: "wrap", gap: 4 }}>
|
<div className="pt-list-actions" style={{ flexWrap: "wrap", gap: 4 }}>
|
||||||
<NmStatusChip color={jobChipColor(row.status)}>{row.status}</NmStatusChip>
|
<NmStatusChip color={jobChipColor(row.status)}>{row.status}</NmStatusChip>
|
||||||
|
|
@ -588,7 +635,7 @@ export function BizStatePage() {
|
||||||
))}
|
))}
|
||||||
{!filteredTasks.length ? (
|
{!filteredTasks.length ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={4}>
|
<td colSpan={5}>
|
||||||
<div className="pt-list-empty">{t("bizState.empty")}</div>
|
<div className="pt-list-empty">{t("bizState.empty")}</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -598,6 +645,117 @@ export function BizStatePage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Create task: pick any CLI target with search */}
|
||||||
|
<AppModalShell open={createOpen} onClose={closeCreate} size="lg">
|
||||||
|
<Modal.Header>
|
||||||
|
<Modal.Heading>{t("bizState.create")}</Modal.Heading>
|
||||||
|
<Modal.CloseTrigger />
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body className="flex flex-col gap-3">
|
||||||
|
<p className="muted">{t("bizState.createHint")}</p>
|
||||||
|
<div className="filter-inline">
|
||||||
|
<Input
|
||||||
|
value={neKeyword}
|
||||||
|
placeholder={t("bizState.neKeywordPh")}
|
||||||
|
onChange={(e) => {
|
||||||
|
setNeKeyword(e.target.value);
|
||||||
|
setNePage(1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FieldSelect
|
||||||
|
value={neSource}
|
||||||
|
onChange={(e) => {
|
||||||
|
setNeSource(e.target.value as NeSourceFilter);
|
||||||
|
setNePage(1);
|
||||||
|
}}
|
||||||
|
aria-label={t("bizState.colSource")}
|
||||||
|
>
|
||||||
|
<option value="all">{t("bizState.allSource")}</option>
|
||||||
|
<option value="managed">managed</option>
|
||||||
|
<option value="ume">ume</option>
|
||||||
|
</FieldSelect>
|
||||||
|
{selectedNe ? (
|
||||||
|
<span className="muted">
|
||||||
|
{t("bizState.selectedNe")}: {selectedNe.name} ({selectedNe.ip_address}) · {neSourceOf(selectedNe)}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="pt-list-table-wrap">
|
||||||
|
<table className="data-table pt-list-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th />
|
||||||
|
<th>{t("bizState.colSource")}</th>
|
||||||
|
<th>{t("bizState.colNe")}</th>
|
||||||
|
<th>IP</th>
|
||||||
|
<th>{t("bizState.colVendor")}</th>
|
||||||
|
<th>{t("bizState.colConnect")}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{neItems.map((row) => {
|
||||||
|
const checked =
|
||||||
|
selectedNe?.id === row.id && neSourceOf(selectedNe) === neSourceOf(row);
|
||||||
|
return (
|
||||||
|
<tr key={`${row.source}:${row.id}`}>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="bs-ne"
|
||||||
|
checked={checked}
|
||||||
|
onChange={() => setSelectedNe(row)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<NmStatusChip color={sourceChipColor(row.source)}>{row.source}</NmStatusChip>
|
||||||
|
</td>
|
||||||
|
<td>{row.name || "—"}</td>
|
||||||
|
<td>{row.ip_address || "—"}</td>
|
||||||
|
<td>{row.vendor || "—"}</td>
|
||||||
|
<td>
|
||||||
|
<NmStatusChip color={jobChipColor(row.connect_status)}>
|
||||||
|
{row.connect_status || "—"}
|
||||||
|
</NmStatusChip>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{!neItems.length ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={6}>
|
||||||
|
<div className="pt-list-empty">
|
||||||
|
{neLoading ? t("common.refreshing") : t("bizState.neEmpty")}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : null}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<ListPager
|
||||||
|
page={nePage}
|
||||||
|
pages={nePages}
|
||||||
|
total={neTotal}
|
||||||
|
pageSize={NE_PAGE_SIZE}
|
||||||
|
onPageChange={setNePage}
|
||||||
|
disabled={neLoading}
|
||||||
|
/>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="primary"
|
||||||
|
isDisabled={busy || !selectedNe}
|
||||||
|
onPress={() => void createTask()}
|
||||||
|
>
|
||||||
|
{t("bizState.create")}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="ghost" onPress={closeCreate}>
|
||||||
|
{t("bizState.cancel")}
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</AppModalShell>
|
||||||
|
|
||||||
{/* Task detail modal */}
|
{/* Task detail modal */}
|
||||||
<AppModalShell open={Boolean(taskId)} onClose={closeTask} size="lg" className="app-heroui-modal--xl">
|
<AppModalShell open={Boolean(taskId)} onClose={closeTask} size="lg" className="app-heroui-modal--xl">
|
||||||
<Modal.Header>
|
<Modal.Header>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue