mirror of
https://github.com/hansjone/netx.git
synced 2026-10-19 13:10:46 +08:00
Improve UME and managed NE list UI layout and styling.
Reorganize the UME page with collapsible CLI, sync status, and AI alert panels; embed CLI connect config in the main view; move task cards under token status; add tags column to managed NE list; and fix hop/proxy form alignment plus consistent section spacing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
39aad258e6
commit
775989cad4
6 changed files with 391 additions and 193 deletions
|
|
@ -16,6 +16,7 @@ import { queryKeys } from "../constants/queryKeys";
|
||||||
import { useI18n } from "../i18n";
|
import { useI18n } from "../i18n";
|
||||||
import { useToast } from "../hooks/useToast";
|
import { useToast } from "../hooks/useToast";
|
||||||
import type { CliConnectProfileItem, UmeCliOverrideItem } from "../types";
|
import type { CliConnectProfileItem, UmeCliOverrideItem } from "../types";
|
||||||
|
import { defaultHopTemplate, isAutoHopTemplate, patchHopVendorChange } from "../utils/hopProxy";
|
||||||
import { formatSystemTime } from "../utils/time";
|
import { formatSystemTime } from "../utils/time";
|
||||||
|
|
||||||
function connectPillLevel(status: string): "up" | "down" | "unknown" | "warn" {
|
function connectPillLevel(status: string): "up" | "down" | "unknown" | "warn" {
|
||||||
|
|
@ -36,9 +37,17 @@ type ProfileForm = {
|
||||||
device_type_default: string;
|
device_type_default: string;
|
||||||
vendor_default: string;
|
vendor_default: string;
|
||||||
is_default: boolean;
|
is_default: boolean;
|
||||||
|
hop_enabled: boolean;
|
||||||
hop: HopProxyFieldsState;
|
hop: HopProxyFieldsState;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function applyHopTemplate(hop: HopProxyFieldsState, protocol: string, vrf: string, force = false): Partial<HopProxyFieldsState> {
|
||||||
|
if (!force && !isAutoHopTemplate(hop.hop_command_template, hop.hop_vendor, hop.hop_protocol, hop.hop_vrf)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return { hop_command_template: defaultHopTemplate(hop.hop_vendor, protocol, vrf) };
|
||||||
|
}
|
||||||
|
|
||||||
const emptyForm = (): ProfileForm => ({
|
const emptyForm = (): ProfileForm => ({
|
||||||
id: "",
|
id: "",
|
||||||
name: "default",
|
name: "default",
|
||||||
|
|
@ -49,7 +58,8 @@ const emptyForm = (): ProfileForm => ({
|
||||||
device_type_default: "zte_zxros",
|
device_type_default: "zte_zxros",
|
||||||
vendor_default: "ZTE",
|
vendor_default: "ZTE",
|
||||||
is_default: true,
|
is_default: true,
|
||||||
hop: { ...emptyHopProxyFields(), hop_vendor: "bastion", hop_port: 22 },
|
hop_enabled: false,
|
||||||
|
hop: emptyHopProxyFields(),
|
||||||
});
|
});
|
||||||
|
|
||||||
function profileToForm(row: CliConnectProfileItem): ProfileForm {
|
function profileToForm(row: CliConnectProfileItem): ProfileForm {
|
||||||
|
|
@ -63,6 +73,7 @@ function profileToForm(row: CliConnectProfileItem): ProfileForm {
|
||||||
device_type_default: row.device_type_default,
|
device_type_default: row.device_type_default,
|
||||||
vendor_default: row.vendor_default,
|
vendor_default: row.vendor_default,
|
||||||
is_default: row.is_default,
|
is_default: row.is_default,
|
||||||
|
hop_enabled: row.hop_enabled,
|
||||||
hop: {
|
hop: {
|
||||||
hop_vendor: (row.hop_vendor || "bastion") as HopProxyFieldsState["hop_vendor"],
|
hop_vendor: (row.hop_vendor || "bastion") as HopProxyFieldsState["hop_vendor"],
|
||||||
hop_host: row.hop_host ?? "",
|
hop_host: row.hop_host ?? "",
|
||||||
|
|
@ -77,7 +88,7 @@ function profileToForm(row: CliConnectProfileItem): ProfileForm {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UmeCliConnectPanel() {
|
export function UmeCliConnectPanel({ enabled = true, embedded = false }: { enabled?: boolean; embedded?: boolean }) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { showOk, showError } = useToast();
|
const { showOk, showError } = useToast();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
@ -91,9 +102,9 @@ export function UmeCliConnectPanel() {
|
||||||
setTestPending(false);
|
setTestPending(false);
|
||||||
}, [testNeId]);
|
}, [testNeId]);
|
||||||
|
|
||||||
const metaQuery = useQuery({ queryKey: queryKeys.cliMeta, queryFn: fetchCliMeta });
|
const metaQuery = useQuery({ queryKey: queryKeys.cliMeta, queryFn: fetchCliMeta, enabled });
|
||||||
const neMetaQuery = useQuery({ queryKey: queryKeys.managedNeMeta, queryFn: fetchManagedNeMeta });
|
const neMetaQuery = useQuery({ queryKey: queryKeys.managedNeMeta, queryFn: fetchManagedNeMeta, enabled });
|
||||||
const profilesQuery = useQuery({ queryKey: queryKeys.cliProfiles, queryFn: fetchCliProfiles });
|
const profilesQuery = useQuery({ queryKey: queryKeys.cliProfiles, queryFn: fetchCliProfiles, enabled });
|
||||||
const overrideQuery = useQuery({
|
const overrideQuery = useQuery({
|
||||||
queryKey: queryKeys.umeCliOverride(testNeId),
|
queryKey: queryKeys.umeCliOverride(testNeId),
|
||||||
queryFn: () => fetchUmeCliOverride(testNeId),
|
queryFn: () => fetchUmeCliOverride(testNeId),
|
||||||
|
|
@ -118,6 +129,11 @@ export function UmeCliConnectPanel() {
|
||||||
|
|
||||||
const saveMutation = useMutation({
|
const saveMutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
|
if (form.hop_enabled) {
|
||||||
|
if (!form.hop.hop_host.trim()) throw new Error(t("managedNe.hop.hostRequired"));
|
||||||
|
if (!form.hop.hop_username.trim()) throw new Error(t("managedNe.hop.userRequired"));
|
||||||
|
if (!form.id && !form.hop.hop_password) throw new Error(t("managedNe.hop.passwordRequired"));
|
||||||
|
}
|
||||||
const body = {
|
const body = {
|
||||||
name: form.name.trim(),
|
name: form.name.trim(),
|
||||||
username: form.username.trim(),
|
username: form.username.trim(),
|
||||||
|
|
@ -127,14 +143,13 @@ export function UmeCliConnectPanel() {
|
||||||
device_type_default: form.device_type_default,
|
device_type_default: form.device_type_default,
|
||||||
vendor_default: form.vendor_default,
|
vendor_default: form.vendor_default,
|
||||||
is_default: form.is_default,
|
is_default: form.is_default,
|
||||||
hop_enabled:
|
hop_enabled: form.hop_enabled,
|
||||||
form.hop.hop_vendor !== "linux" ? Boolean((form.hop.hop_host ?? "").trim()) : true,
|
|
||||||
hop_vendor: form.hop.hop_vendor,
|
hop_vendor: form.hop.hop_vendor,
|
||||||
hop_host: form.hop.hop_host,
|
hop_host: form.hop.hop_host,
|
||||||
hop_port: form.hop.hop_port,
|
hop_port: form.hop.hop_port,
|
||||||
hop_protocol: form.hop.hop_protocol,
|
hop_protocol: form.hop.hop_protocol,
|
||||||
hop_username: form.hop.hop_username,
|
hop_username: form.hop.hop_username,
|
||||||
hop_password: form.hop.hop_password || undefined,
|
hop_password: form.hop_enabled ? form.hop.hop_password || undefined : undefined,
|
||||||
hop_command_template: form.hop.hop_command_template,
|
hop_command_template: form.hop.hop_command_template,
|
||||||
hop_vrf: form.hop.hop_vrf,
|
hop_vrf: form.hop.hop_vrf,
|
||||||
hop_target_auth_mode: form.hop.hop_target_auth_mode,
|
hop_target_auth_mode: form.hop.hop_target_auth_mode,
|
||||||
|
|
@ -185,16 +200,18 @@ export function UmeCliConnectPanel() {
|
||||||
const vendors = neMetaQuery.data?.vendors ?? [];
|
const vendors = neMetaQuery.data?.vendors ?? [];
|
||||||
const cliReady = Boolean(metaQuery.data?.cli_profile_ready);
|
const cliReady = Boolean(metaQuery.data?.cli_profile_ready);
|
||||||
|
|
||||||
return (
|
const body = (
|
||||||
<section className="panel">
|
<div className={embedded ? "cli-connect-panel" : undefined}>
|
||||||
<div className="panel__toolbar">
|
{!embedded ? (
|
||||||
<h2>{t("ume.cli.title")}</h2>
|
<div className="panel__toolbar">
|
||||||
<HelpHint text={t("ume.cli.hint")} />
|
<h2>{t("ume.cli.title")}</h2>
|
||||||
</div>
|
<HelpHint text={t("ume.cli.hint")} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<p className="form-field-hint" style={{ marginBottom: 12 }}>
|
<p className="form-field-hint" style={{ marginBottom: 12 }}>
|
||||||
{cliReady ? t("ume.cli.statusReady") : t("ume.cli.statusNotReady")}
|
{cliReady ? t("ume.cli.statusReady") : t("ume.cli.statusNotReady")}
|
||||||
</p>
|
</p>
|
||||||
<div className="form-grid" style={{ maxWidth: 960 }}>
|
<div className="form-grid">
|
||||||
<label>
|
<label>
|
||||||
<span className="form-label">{t("ume.cli.profileName")}</span>
|
<span className="form-label">{t("ume.cli.profileName")}</span>
|
||||||
<input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
|
<input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
|
||||||
|
|
@ -241,13 +258,43 @@ export function UmeCliConnectPanel() {
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<HopProxyFields
|
<div className="form-fieldset" style={{ marginTop: 16 }}>
|
||||||
value={form.hop}
|
<div className="form-fieldset__title">{t("managedNe.hop.sectionTitle")}</div>
|
||||||
onChange={(patch) => setForm((prev) => ({ ...prev, hop: { ...prev.hop, ...patch } }))}
|
<label className="form-check">
|
||||||
hopPasswordRequired={!form.id}
|
<input
|
||||||
hopPasswordOptional={Boolean(form.id)}
|
type="checkbox"
|
||||||
/>
|
checked={form.hop_enabled}
|
||||||
<div className="filter-inline" style={{ marginTop: 16 }}>
|
onChange={(e) => {
|
||||||
|
const hop_enabled = e.target.checked;
|
||||||
|
setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
hop_enabled,
|
||||||
|
...(hop_enabled
|
||||||
|
? {
|
||||||
|
hop: {
|
||||||
|
...prev.hop,
|
||||||
|
...patchHopVendorChange(prev.hop.hop_vendor, prev.hop),
|
||||||
|
...applyHopTemplate(prev.hop, prev.hop.hop_protocol, prev.hop.hop_vrf, true),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="form-check__text">{t("managedNe.hop.enable")}</span>
|
||||||
|
</label>
|
||||||
|
{form.hop_enabled ? (
|
||||||
|
<div className="hop-proxy-fields">
|
||||||
|
<HopProxyFields
|
||||||
|
value={form.hop}
|
||||||
|
onChange={(patch) => setForm((prev) => ({ ...prev, hop: { ...prev.hop, ...patch } }))}
|
||||||
|
hopPasswordRequired={!form.id}
|
||||||
|
hopPasswordOptional={Boolean(form.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="actions-row actions-row--inline" style={{ marginTop: 16 }}>
|
||||||
<button type="button" onClick={() => saveMutation.mutate()} disabled={saveMutation.isPending}>
|
<button type="button" onClick={() => saveMutation.mutate()} disabled={saveMutation.isPending}>
|
||||||
{saveMutation.isPending ? t("managedNe.form.saving") : t("managedNe.form.save")}
|
{saveMutation.isPending ? t("managedNe.form.saving") : t("managedNe.form.save")}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -279,14 +326,13 @@ export function UmeCliConnectPanel() {
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="panel" style={{ marginTop: 20 }}>
|
<div className={embedded ? undefined : "panel"} style={{ marginTop: 20 }}>
|
||||||
<h3>{t("ume.cli.connectTestTitle")}</h3>
|
<h3 className={embedded ? "card__section-title" : undefined}>{t("ume.cli.connectTestTitle")}</h3>
|
||||||
<div className="filter-inline">
|
<div className="cli-connect-test-row">
|
||||||
<input
|
<input
|
||||||
value={sampleUmeNeId}
|
value={sampleUmeNeId}
|
||||||
onChange={(e) => setSampleUmeNeId(e.target.value)}
|
onChange={(e) => setSampleUmeNeId(e.target.value)}
|
||||||
placeholder={t("ume.cli.sampleNePh")}
|
placeholder={t("ume.cli.sampleNePh")}
|
||||||
style={{ minWidth: 320 }}
|
|
||||||
/>
|
/>
|
||||||
<button type="button" onClick={() => connectTestMutation.mutate()} disabled={connectTestMutation.isPending}>
|
<button type="button" onClick={() => connectTestMutation.mutate()} disabled={connectTestMutation.isPending}>
|
||||||
{connectTestMutation.isPending || overrideTesting
|
{connectTestMutation.isPending || overrideTesting
|
||||||
|
|
@ -335,7 +381,7 @@ export function UmeCliConnectPanel() {
|
||||||
{(profilesQuery.data?.items || []).length > 1 ? (
|
{(profilesQuery.data?.items || []).length > 1 ? (
|
||||||
<div style={{ marginTop: 16 }}>
|
<div style={{ marginTop: 16 }}>
|
||||||
<span className="form-label">{t("ume.cli.existingProfiles")}</span>
|
<span className="form-label">{t("ume.cli.existingProfiles")}</span>
|
||||||
<div className="filter-inline">
|
<div className="actions-row actions-row--inline">
|
||||||
{(profilesQuery.data?.items || []).map((p) => (
|
{(profilesQuery.data?.items || []).map((p) => (
|
||||||
<button key={p.id} type="button" className="link-btn" onClick={() => setForm(profileToForm(p))}>
|
<button key={p.id} type="button" className="link-btn" onClick={() => setForm(profileToForm(p))}>
|
||||||
{p.name}
|
{p.name}
|
||||||
|
|
@ -345,6 +391,9 @@ export function UmeCliConnectPanel() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</section>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (embedded) return body;
|
||||||
|
return <section className="panel">{body}</section>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -337,6 +337,8 @@ const en = {
|
||||||
},
|
},
|
||||||
keyAlert: {
|
keyAlert: {
|
||||||
title: "AI alarm monitor (OClaw WhatsApp)",
|
title: "AI alarm monitor (OClaw WhatsApp)",
|
||||||
|
showPanel: "Expand",
|
||||||
|
hidePanel: "Collapse",
|
||||||
help: "Exact match by notificationId, or substring match on alarm description (case-insensitive; nativeProbableCause, etc.). Optionally restrict to UME inventory ne_type values (multi-select; empty = all devices). Pushed via OClaw WebSocket. Shares NETX_OCLAW_ANALYZE_TOKEN.",
|
help: "Exact match by notificationId, or substring match on alarm description (case-insensitive; nativeProbableCause, etc.). Optionally restrict to UME inventory ne_type values (multi-select; empty = all devices). Pushed via OClaw WebSocket. Shares NETX_OCLAW_ANALYZE_TOKEN.",
|
||||||
ws: "OClaw WSS",
|
ws: "OClaw WSS",
|
||||||
wsConnected: "connected",
|
wsConnected: "connected",
|
||||||
|
|
@ -455,6 +457,8 @@ const en = {
|
||||||
},
|
},
|
||||||
syncStatus: {
|
syncStatus: {
|
||||||
title: "Sync history",
|
title: "Sync history",
|
||||||
|
showPanel: "Expand",
|
||||||
|
hidePanel: "Collapse",
|
||||||
empty: "No sync records",
|
empty: "No sync records",
|
||||||
},
|
},
|
||||||
ne: {
|
ne: {
|
||||||
|
|
@ -473,6 +477,8 @@ const en = {
|
||||||
},
|
},
|
||||||
cli: {
|
cli: {
|
||||||
title: "UME CLI connection profile",
|
title: "UME CLI connection profile",
|
||||||
|
showPanel: "Expand",
|
||||||
|
hidePanel: "Collapse",
|
||||||
hint: "Unified SSH credentials and hop/bastion for UME inventory NEs. OClaw MCP can exec show commands via ume_ne_id without per-device managed_ne entries.",
|
hint: "Unified SSH credentials and hop/bastion for UME inventory NEs. OClaw MCP can exec show commands via ume_ne_id without per-device managed_ne entries.",
|
||||||
statusReady: "CLI profile is ready for UME NE command execution.",
|
statusReady: "CLI profile is ready for UME NE command execution.",
|
||||||
statusNotReady: "Set target username and hop/bastion settings (if enabled).",
|
statusNotReady: "Set target username and hop/bastion settings (if enabled).",
|
||||||
|
|
|
||||||
|
|
@ -335,6 +335,8 @@ const zh = {
|
||||||
},
|
},
|
||||||
keyAlert: {
|
keyAlert: {
|
||||||
title: "AI 告警监控(OClaw WhatsApp)",
|
title: "AI 告警监控(OClaw WhatsApp)",
|
||||||
|
showPanel: "展开",
|
||||||
|
hidePanel: "收起",
|
||||||
help: "按 notificationId 精确匹配,或按告警描述关键字模糊匹配(不区分大小写,匹配 nativeProbableCause 等字段);可选限定 UME Inventory 中的网元类型 (ne_type),不选则匹配全部设备。经 OClaw WebSocket 推送到 WhatsApp 群。鉴权与 NETX_OCLAW_ANALYZE_TOKEN 共用。",
|
help: "按 notificationId 精确匹配,或按告警描述关键字模糊匹配(不区分大小写,匹配 nativeProbableCause 等字段);可选限定 UME Inventory 中的网元类型 (ne_type),不选则匹配全部设备。经 OClaw WebSocket 推送到 WhatsApp 群。鉴权与 NETX_OCLAW_ANALYZE_TOKEN 共用。",
|
||||||
ws: "OClaw WSS",
|
ws: "OClaw WSS",
|
||||||
wsConnected: "已连接",
|
wsConnected: "已连接",
|
||||||
|
|
@ -453,6 +455,8 @@ const zh = {
|
||||||
},
|
},
|
||||||
syncStatus: {
|
syncStatus: {
|
||||||
title: "同步状态",
|
title: "同步状态",
|
||||||
|
showPanel: "展开",
|
||||||
|
hidePanel: "收起",
|
||||||
empty: "暂无同步记录",
|
empty: "暂无同步记录",
|
||||||
},
|
},
|
||||||
ne: {
|
ne: {
|
||||||
|
|
@ -471,6 +475,8 @@ const zh = {
|
||||||
},
|
},
|
||||||
cli: {
|
cli: {
|
||||||
title: "UME CLI 连接配置",
|
title: "UME CLI 连接配置",
|
||||||
|
showPanel: "展开",
|
||||||
|
hidePanel: "收起",
|
||||||
hint: "为 UME 清单网元配置统一 SSH 凭据与跳板/堡垒机;OClaw MCP 可通过 ume_ne_id 直接执行 show 命令,无需逐台录入 managed_ne。",
|
hint: "为 UME 清单网元配置统一 SSH 凭据与跳板/堡垒机;OClaw MCP 可通过 ume_ne_id 直接执行 show 命令,无需逐台录入 managed_ne。",
|
||||||
statusReady: "CLI 配置已就绪,可对 UME 网元执行命令。",
|
statusReady: "CLI 配置已就绪,可对 UME 网元执行命令。",
|
||||||
statusNotReady: "请填写目标用户名,并配置跳板/堡垒机(若启用)。",
|
statusNotReady: "请填写目标用户名,并配置跳板/堡垒机(若启用)。",
|
||||||
|
|
|
||||||
|
|
@ -355,13 +355,26 @@ button {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card input,
|
.card__section-title {
|
||||||
|
margin: 14px 0 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card input:not([type="checkbox"]):not([type="radio"]),
|
||||||
.card select,
|
.card select,
|
||||||
.card textarea {
|
.card textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card input[type="checkbox"],
|
||||||
|
.card input[type="radio"] {
|
||||||
|
width: auto;
|
||||||
|
margin-bottom: 0;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.pager input,
|
.pager input,
|
||||||
.pager select {
|
.pager select {
|
||||||
width: auto;
|
width: auto;
|
||||||
|
|
@ -902,7 +915,11 @@ pre {
|
||||||
.page-stack {
|
.page-stack {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-stack > .cards {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel--warn {
|
.panel--warn {
|
||||||
|
|
@ -920,7 +937,8 @@ pre {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel__toolbar h2 {
|
.panel__toolbar h2,
|
||||||
|
.panel__toolbar h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1008,6 +1026,104 @@ pre {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cli-connect-panel {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-connect-panel .form-grid,
|
||||||
|
.cli-connect-panel .form-fieldset {
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-connect-panel .form-grid label:not(.form-check) {
|
||||||
|
align-items: stretch;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-connect-panel .form-label {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-connect-panel .form-grid input,
|
||||||
|
.cli-connect-panel .form-grid select,
|
||||||
|
.cli-connect-panel .form-fieldset input,
|
||||||
|
.cli-connect-panel .form-fieldset select {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-connect-panel .form-fieldset {
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
min-inline-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-connect-panel .hop-proxy-fields,
|
||||||
|
.cli-connect-panel .hop-proxy-fields .form-grid {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-connect-panel .hop-proxy-fields .form-grid label {
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-connect-panel .hop-proxy-fields .form-grid select,
|
||||||
|
.cli-connect-panel .hop-proxy-fields .form-grid input {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-align: left;
|
||||||
|
text-align-last: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-connect-panel .hop-proxy-fields .form-field-hint {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-connect-panel .form-fieldset legend {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
padding: 0 6px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-connect-panel .form-check {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .cli-connect-panel input,
|
||||||
|
.card .cli-connect-panel select,
|
||||||
|
.card .cli-connect-panel textarea {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-connect-test-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-connect-test-row input {
|
||||||
|
flex: 1 1 320px;
|
||||||
|
min-width: 240px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.form-grid {
|
.form-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
|
@ -1036,6 +1152,14 @@ pre {
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
.form-grid label {
|
.form-grid label {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -1060,13 +1184,21 @@ pre {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-fieldset legend {
|
.form-fieldset legend,
|
||||||
|
.form-fieldset__title {
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-fieldset__title {
|
||||||
|
display: block;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
padding: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
.form-check {
|
.form-check {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
@ -1075,6 +1207,21 @@ pre {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check__text {
|
||||||
|
white-space: nowrap;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* form-grid labels are column layout; checkbox rows must stay horizontal */
|
||||||
|
.form-grid label.form-check {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-tag {
|
.table-tag {
|
||||||
|
|
|
||||||
|
|
@ -601,6 +601,7 @@ export function NePage() {
|
||||||
<th>{t("managedNe.col.name")}</th>
|
<th>{t("managedNe.col.name")}</th>
|
||||||
<th>{t("managedNe.col.vendor")}</th>
|
<th>{t("managedNe.col.vendor")}</th>
|
||||||
<th>{t("managedNe.col.deviceType")}</th>
|
<th>{t("managedNe.col.deviceType")}</th>
|
||||||
|
<th>{t("managedNe.col.tags")}</th>
|
||||||
<th>{t("managedNe.col.ip")}</th>
|
<th>{t("managedNe.col.ip")}</th>
|
||||||
<th>{t("managedNe.col.user")}</th>
|
<th>{t("managedNe.col.user")}</th>
|
||||||
<th>{t("managedNe.col.connect")}</th>
|
<th>{t("managedNe.col.connect")}</th>
|
||||||
|
|
@ -621,6 +622,15 @@ export function NePage() {
|
||||||
<td>{row.name || row.ip_address}</td>
|
<td>{row.name || row.ip_address}</td>
|
||||||
<td>{row.vendor}</td>
|
<td>{row.vendor}</td>
|
||||||
<td>{row.device_type}</td>
|
<td>{row.device_type}</td>
|
||||||
|
<td>
|
||||||
|
{row.tags
|
||||||
|
? row.tags.split(/\s+/).map((tag) => (
|
||||||
|
<span key={tag} className="table-tag">
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
: t("common.empty")}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{row.ip_address}:{row.port}/{row.protocol}
|
{row.ip_address}:{row.port}/{row.protocol}
|
||||||
{row.hop_enabled ? (
|
{row.hop_enabled ? (
|
||||||
|
|
@ -808,7 +818,7 @@ export function NePage() {
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{t("managedNe.hop.enable")}
|
<span className="form-check__text">{t("managedNe.hop.enable")}</span>
|
||||||
</label>
|
</label>
|
||||||
{form.hop_enabled ? (
|
{form.hop_enabled ? (
|
||||||
<HopProxyFields
|
<HopProxyFields
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,8 @@ export function UmePage() {
|
||||||
const [nePageSize, setNePageSize] = useState(50);
|
const [nePageSize, setNePageSize] = useState(50);
|
||||||
const [expandedNeId, setExpandedNeId] = useState("");
|
const [expandedNeId, setExpandedNeId] = useState("");
|
||||||
const [nePanelOpen, setNePanelOpen] = useState(false);
|
const [nePanelOpen, setNePanelOpen] = useState(false);
|
||||||
|
const [syncStatusPanelOpen, setSyncStatusPanelOpen] = useState(false);
|
||||||
|
const [keyAlertPanelOpen, setKeyAlertPanelOpen] = useState(false);
|
||||||
const [curSeverity, setCurSeverity] = useState("");
|
const [curSeverity, setCurSeverity] = useState("");
|
||||||
const [curCleared, setCurCleared] = useState("");
|
const [curCleared, setCurCleared] = useState("");
|
||||||
const [curHostName, setCurHostName] = useState("");
|
const [curHostName, setCurHostName] = useState("");
|
||||||
|
|
@ -69,17 +71,7 @@ export function UmePage() {
|
||||||
const [keyAlertNeTypes, setKeyAlertNeTypes] = useState<string[]>([]);
|
const [keyAlertNeTypes, setKeyAlertNeTypes] = useState<string[]>([]);
|
||||||
const [keyAlertEditRule, setKeyAlertEditRule] = useState<UmeKeyAlertRuleItem | null>(null);
|
const [keyAlertEditRule, setKeyAlertEditRule] = useState<UmeKeyAlertRuleItem | null>(null);
|
||||||
const [keyAlertEditNeTypes, setKeyAlertEditNeTypes] = useState<string[]>([]);
|
const [keyAlertEditNeTypes, setKeyAlertEditNeTypes] = useState<string[]>([]);
|
||||||
const [umeViewTab, setUmeViewTab] = useState<"main" | "cli">("main");
|
const [cliPanelOpen, setCliPanelOpen] = useState(false);
|
||||||
|
|
||||||
const syncMutation = useMutation({
|
|
||||||
mutationFn: async (domains: string[]) => apiPost<{ ok: boolean; jobs: unknown[] }>("/v1/ume/sync", { domains }),
|
|
||||||
onSuccess: async () => {
|
|
||||||
await queryClient.invalidateQueries({ queryKey: queryKeys.umeSyncStatusAll });
|
|
||||||
await queryClient.invalidateQueries({ queryKey: queryKeys.umeNEAll });
|
|
||||||
await queryClient.invalidateQueries({ queryKey: queryKeys.umeCurrentAlarmsAll });
|
|
||||||
await queryClient.invalidateQueries({ queryKey: queryKeys.umeInventoryNeTypes });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const syncStatusQuery = useQuery({
|
const syncStatusQuery = useQuery({
|
||||||
queryKey: queryKeys.umeSyncStatus(syncPage, syncPageSize),
|
queryKey: queryKeys.umeSyncStatus(syncPage, syncPageSize),
|
||||||
|
|
@ -138,6 +130,7 @@ export function UmePage() {
|
||||||
}),
|
}),
|
||||||
staleTime: 3000,
|
staleTime: 3000,
|
||||||
refetchInterval: 5000,
|
refetchInterval: 5000,
|
||||||
|
enabled: keyAlertPanelOpen,
|
||||||
});
|
});
|
||||||
const keyAlertNeTypesQuery = useQuery({
|
const keyAlertNeTypesQuery = useQuery({
|
||||||
queryKey: queryKeys.umeInventoryNeTypes,
|
queryKey: queryKeys.umeInventoryNeTypes,
|
||||||
|
|
@ -261,7 +254,7 @@ export function UmePage() {
|
||||||
queryKey: queryKeys.cliTargets(neKeyword, nePage, nePageSize),
|
queryKey: queryKeys.cliTargets(neKeyword, nePage, nePageSize),
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
fetchCliTargets({ source: "ume", keyword: neKeyword, page: nePage, pageSize: nePageSize }),
|
fetchCliTargets({ source: "ume", keyword: neKeyword, page: nePage, pageSize: nePageSize }),
|
||||||
enabled: nePanelOpen && umeViewTab === "main",
|
enabled: nePanelOpen,
|
||||||
staleTime: 5000,
|
staleTime: 5000,
|
||||||
});
|
});
|
||||||
const cliStatusByNeId = useMemo(() => {
|
const cliStatusByNeId = useMemo(() => {
|
||||||
|
|
@ -459,30 +452,7 @@ export function UmePage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className="panel" style={{ marginBottom: 12 }}>
|
<div className="page-stack">
|
||||||
<div className="filter-inline">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={umeViewTab === "main" ? "link-btn" : "link-btn"}
|
|
||||||
style={{ fontWeight: umeViewTab === "main" ? 700 : 400 }}
|
|
||||||
onClick={() => setUmeViewTab("main")}
|
|
||||||
>
|
|
||||||
{t("ume.tabs.main")}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="link-btn"
|
|
||||||
style={{ fontWeight: umeViewTab === "cli" ? 700 : 400 }}
|
|
||||||
onClick={() => setUmeViewTab("cli")}
|
|
||||||
>
|
|
||||||
{t("ume.tabs.cli")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{umeViewTab === "cli" ? (
|
|
||||||
<UmeCliConnectPanel />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<section className="cards">
|
<section className="cards">
|
||||||
<article className="card card--full">
|
<article className="card card--full">
|
||||||
<h3>{t("ume.token.title")}</h3>
|
<h3>{t("ume.token.title")}</h3>
|
||||||
|
|
@ -562,6 +532,105 @@ export function UmePage() {
|
||||||
)}
|
)}
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
<article className="card card--full">
|
||||||
|
<h3>{t("ume.tasks.currentTitle")}</h3>
|
||||||
|
<div className="actions-row actions-row--inline">
|
||||||
|
<span className={`conn-pill conn-pill--${runningTasks.length > 0 ? "unknown" : "up"}`}>
|
||||||
|
{t("ume.tasks.running")}: {runningTasks.length}
|
||||||
|
</span>
|
||||||
|
<button onClick={() => queryClient.invalidateQueries({ queryKey: queryKeys.umeSyncStatusAll })} disabled={syncStatusQuery.isFetching}>
|
||||||
|
{t("common.refresh")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>domain</th>
|
||||||
|
<th>trigger_mode</th>
|
||||||
|
<th>status</th>
|
||||||
|
<th>started_at</th>
|
||||||
|
<th>error</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{runningTasks.map((x) => (
|
||||||
|
<tr key={`running-${x.id}`}>
|
||||||
|
<td>{x.id}</td>
|
||||||
|
<td>{x.domain}</td>
|
||||||
|
<td>{x.trigger_mode}</td>
|
||||||
|
<td>{x.status}</td>
|
||||||
|
<td>{formatSystemTime(x.started_at)}</td>
|
||||||
|
<td>{x.error_message || t("common.empty")}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{!syncStatusQuery.isLoading && runningTasks.length === 0 && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={6}>{t("ume.tasks.noRunning")}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3 className="card__section-title">{t("ume.tasks.runtimeTitle")}</h3>
|
||||||
|
{runtimeTaskError ? (
|
||||||
|
<div className="pill pill--high" style={{ marginBottom: 8 }}>
|
||||||
|
{t("ume.tasks.runtimeOpFailed")}: {runtimeTaskError}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>task</th>
|
||||||
|
<th title={t("ume.tasks.intervalTitle")}>interval</th>
|
||||||
|
<th>status</th>
|
||||||
|
<th title={t("ume.tasks.lastRunTitle")}>last_run_at</th>
|
||||||
|
<th>{t("ume.tasks.lastErrorCol")}</th>
|
||||||
|
<th>actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{runtimeTasks.map((x) => (
|
||||||
|
<tr key={`runtime-${x.task}`}>
|
||||||
|
<td title={x.task}>{runtimeTaskLabel(x.task)}</td>
|
||||||
|
<td title={typeof x.interval_s === "number" ? `${x.interval_s}s` : undefined}>
|
||||||
|
{runtimeIntervalLabel(x.interval_label)}
|
||||||
|
</td>
|
||||||
|
<td>{x.status}</td>
|
||||||
|
<td>{x.last_run_at ? formatSystemTime(x.last_run_at) : t("common.empty")}</td>
|
||||||
|
<td>{runtimeLastError(x.last_error, t) || t("common.empty")}</td>
|
||||||
|
<td>
|
||||||
|
{Boolean(x.paused) ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="link-btn"
|
||||||
|
disabled={runtimeTaskMutation.isPending}
|
||||||
|
onClick={() => runtimeTaskMutation.mutate({ task: x.task, action: "resume" })}
|
||||||
|
>
|
||||||
|
resume
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="link-btn"
|
||||||
|
disabled={runtimeTaskMutation.isPending}
|
||||||
|
onClick={() => runtimeTaskMutation.mutate({ task: x.task, action: "pause" })}
|
||||||
|
>
|
||||||
|
pause
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{!syncStatusQuery.isLoading && runtimeTasks.length === 0 && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={6}>{t("ume.tasks.noRuntime")}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</article>
|
||||||
|
|
||||||
<article className="card card--full">
|
<article className="card card--full">
|
||||||
<h3 className="card-title-with-hint">
|
<h3 className="card-title-with-hint">
|
||||||
{t("ume.subscription.title")}
|
{t("ume.subscription.title")}
|
||||||
|
|
@ -711,10 +780,30 @@ export function UmePage() {
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article className="card card--full">
|
<article className="card card--full">
|
||||||
<h3 className="card-title-with-hint">
|
<div className="panel__toolbar" style={{ marginBottom: cliPanelOpen ? 12 : 0 }}>
|
||||||
{t("ume.keyAlert.title")}
|
<h3 className="card-title-with-hint">
|
||||||
<HelpHint text={t("ume.keyAlert.help")} ariaLabel={t("common.help")} />
|
{t("ume.cli.title")}
|
||||||
</h3>
|
<HelpHint text={t("ume.cli.hint")} ariaLabel={t("common.help")} />
|
||||||
|
</h3>
|
||||||
|
<button type="button" className="link-btn" onClick={() => setCliPanelOpen((x) => !x)}>
|
||||||
|
{cliPanelOpen ? t("ume.cli.hidePanel") : t("ume.cli.showPanel")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{cliPanelOpen ? <UmeCliConnectPanel embedded enabled /> : null}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="card card--full">
|
||||||
|
<div className="panel__toolbar" style={{ marginBottom: keyAlertPanelOpen ? 12 : 0 }}>
|
||||||
|
<h3 className="card-title-with-hint">
|
||||||
|
{t("ume.keyAlert.title")}
|
||||||
|
<HelpHint text={t("ume.keyAlert.help")} ariaLabel={t("common.help")} />
|
||||||
|
</h3>
|
||||||
|
<button type="button" className="link-btn" onClick={() => setKeyAlertPanelOpen((x) => !x)}>
|
||||||
|
{keyAlertPanelOpen ? t("ume.keyAlert.hidePanel") : t("ume.keyAlert.showPanel")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{keyAlertPanelOpen ? (
|
||||||
|
<>
|
||||||
<div className="actions-row actions-row--inline">
|
<div className="actions-row actions-row--inline">
|
||||||
<span className={`conn-pill conn-pill--${oclawWsPill}`}>
|
<span className={`conn-pill conn-pill--${oclawWsPill}`}>
|
||||||
{t("ume.keyAlert.ws")}:{" "}
|
{t("ume.keyAlert.ws")}:{" "}
|
||||||
|
|
@ -1059,130 +1148,20 @@ export function UmePage() {
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</>
|
||||||
|
) : null}
|
||||||
<article className="card card--full">
|
|
||||||
<h3>{t("ume.sync.title")}</h3>
|
|
||||||
<div className="actions-row actions-row--inline">
|
|
||||||
<button onClick={() => syncMutation.mutate(["inventory"])} disabled={syncMutation.isPending}>
|
|
||||||
{t("ume.sync.inventory")}
|
|
||||||
</button>
|
|
||||||
<button onClick={() => syncMutation.mutate(["alarms_current"])} disabled={syncMutation.isPending}>
|
|
||||||
{t("ume.sync.alarmsCurrent")}
|
|
||||||
</button>
|
|
||||||
<button onClick={() => syncMutation.mutate(["inventory", "alarms_current"])} disabled={syncMutation.isPending}>
|
|
||||||
{t("ume.sync.full")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{syncMutation.error && (
|
|
||||||
<div className="pill pill--high">
|
|
||||||
{t("ume.sync.failed")}: {String(syncMutation.error)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="panel">
|
<section className="panel">
|
||||||
<h2>{t("ume.tasks.currentTitle")}</h2>
|
<div className="panel__toolbar">
|
||||||
<div className="actions-row actions-row--inline">
|
<h2>{t("ume.syncStatus.title")}</h2>
|
||||||
<span className={`conn-pill conn-pill--${runningTasks.length > 0 ? "unknown" : "up"}`}>
|
<button type="button" className="link-btn" onClick={() => setSyncStatusPanelOpen((x) => !x)}>
|
||||||
{t("ume.tasks.running")}: {runningTasks.length}
|
{syncStatusPanelOpen ? t("ume.syncStatus.hidePanel") : t("ume.syncStatus.showPanel")}
|
||||||
</span>
|
|
||||||
<button onClick={() => queryClient.invalidateQueries({ queryKey: queryKeys.umeSyncStatusAll })} disabled={syncStatusQuery.isFetching}>
|
|
||||||
{t("common.refresh")}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<table>
|
{syncStatusPanelOpen ? (
|
||||||
<thead>
|
<>
|
||||||
<tr>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>domain</th>
|
|
||||||
<th>trigger_mode</th>
|
|
||||||
<th>status</th>
|
|
||||||
<th>started_at</th>
|
|
||||||
<th>error</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{runningTasks.map((x) => (
|
|
||||||
<tr key={`running-${x.id}`}>
|
|
||||||
<td>{x.id}</td>
|
|
||||||
<td>{x.domain}</td>
|
|
||||||
<td>{x.trigger_mode}</td>
|
|
||||||
<td>{x.status}</td>
|
|
||||||
<td>{formatSystemTime(x.started_at)}</td>
|
|
||||||
<td>{x.error_message || t("common.empty")}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
{!syncStatusQuery.isLoading && runningTasks.length === 0 && (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={6}>{t("ume.tasks.noRunning")}</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<h3 style={{ marginTop: 12 }}>{t("ume.tasks.runtimeTitle")}</h3>
|
|
||||||
{runtimeTaskError ? (
|
|
||||||
<div className="pill pill--high" style={{ marginBottom: 8 }}>
|
|
||||||
{t("ume.tasks.runtimeOpFailed")}: {runtimeTaskError}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>task</th>
|
|
||||||
<th title={t("ume.tasks.intervalTitle")}>interval</th>
|
|
||||||
<th>status</th>
|
|
||||||
<th title={t("ume.tasks.lastRunTitle")}>last_run_at</th>
|
|
||||||
<th>{t("ume.tasks.lastErrorCol")}</th>
|
|
||||||
<th>actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{runtimeTasks.map((x) => (
|
|
||||||
<tr key={`runtime-${x.task}`}>
|
|
||||||
<td title={x.task}>{runtimeTaskLabel(x.task)}</td>
|
|
||||||
<td title={typeof x.interval_s === "number" ? `${x.interval_s}s` : undefined}>
|
|
||||||
{runtimeIntervalLabel(x.interval_label)}
|
|
||||||
</td>
|
|
||||||
<td>{x.status}</td>
|
|
||||||
<td>{x.last_run_at ? formatSystemTime(x.last_run_at) : t("common.empty")}</td>
|
|
||||||
<td>{runtimeLastError(x.last_error, t) || t("common.empty")}</td>
|
|
||||||
<td>
|
|
||||||
{Boolean(x.paused) ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="link-btn"
|
|
||||||
disabled={runtimeTaskMutation.isPending}
|
|
||||||
onClick={() => runtimeTaskMutation.mutate({ task: x.task, action: "resume" })}
|
|
||||||
>
|
|
||||||
resume
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="link-btn"
|
|
||||||
disabled={runtimeTaskMutation.isPending}
|
|
||||||
onClick={() => runtimeTaskMutation.mutate({ task: x.task, action: "pause" })}
|
|
||||||
>
|
|
||||||
pause
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
{!syncStatusQuery.isLoading && runtimeTasks.length === 0 && (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={6}>{t("ume.tasks.noRuntime")}</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="panel">
|
|
||||||
<h2>{t("ume.syncStatus.title")}</h2>
|
|
||||||
<div className="actions-row actions-row--inline">
|
<div className="actions-row actions-row--inline">
|
||||||
<button onClick={() => queryClient.invalidateQueries({ queryKey: queryKeys.umeSyncStatusAll })} disabled={syncStatusQuery.isFetching}>
|
<button onClick={() => queryClient.invalidateQueries({ queryKey: queryKeys.umeSyncStatusAll })} disabled={syncStatusQuery.isFetching}>
|
||||||
{t("common.refresh")}
|
{t("common.refresh")}
|
||||||
|
|
@ -1249,6 +1228,8 @@ export function UmePage() {
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="panel">
|
<section className="panel">
|
||||||
|
|
@ -1476,6 +1457,7 @@ export function UmePage() {
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
{keyAlertEditRule ? (
|
{keyAlertEditRule ? (
|
||||||
<div className="modal-backdrop" role="presentation" onClick={() => setKeyAlertEditRule(null)}>
|
<div className="modal-backdrop" role="presentation" onClick={() => setKeyAlertEditRule(null)}>
|
||||||
|
|
@ -1554,7 +1536,5 @@ export function UmePage() {
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue