feat(ui): translate nodes.ts view to Chinese
- Add import for i18n t() function - Translate device pairing section (pending/paired/approve/reject) - Translate exec node binding section - Translate exec approvals section (target, tabs) - Add comprehensive node-related translations to en-US and zh-TW locales
This commit is contained in:
parent
eccf005580
commit
edeb35dbcc
@ -407,34 +407,87 @@ export const enUS = {
|
||||
// Nodes page
|
||||
nodes: {
|
||||
title: "Nodes",
|
||||
desc: "Connected execution nodes and device pairings.",
|
||||
noNodes: "No nodes connected.",
|
||||
desc: "Paired devices and live links.",
|
||||
noNodes: "No nodes found.",
|
||||
devices: "Devices",
|
||||
devicesDesc: "Pairing requests + role tokens.",
|
||||
noDevices: "No paired devices.",
|
||||
approve: "Approve",
|
||||
reject: "Reject",
|
||||
revoke: "Revoke",
|
||||
rotate: "Rotate",
|
||||
pending: "Pending",
|
||||
paired: "Paired",
|
||||
approved: "Approved",
|
||||
offline: "offline",
|
||||
tokens: "Tokens",
|
||||
tokensNone: "Tokens: none",
|
||||
role: "role",
|
||||
requested: "requested",
|
||||
repair: "repair",
|
||||
active: "active",
|
||||
revoked: "revoked",
|
||||
scopes: "scopes",
|
||||
roles: "roles",
|
||||
|
||||
bindings: {
|
||||
title: "Exec Bindings",
|
||||
desc: "Bind agents to specific execution nodes.",
|
||||
default: "Default Node",
|
||||
title: "Exec node binding",
|
||||
desc: "Pin agents to a specific node when using",
|
||||
default: "Default binding",
|
||||
defaultDesc: "Used when agents do not override a node binding.",
|
||||
agent: "Agent",
|
||||
node: "Node",
|
||||
save: "Save Bindings",
|
||||
anyNode: "Any node",
|
||||
useDefault: "Use default",
|
||||
save: "Save",
|
||||
loadConfig: "Load config",
|
||||
loadConfigNote: "Load config to edit bindings.",
|
||||
noNodesAvailable: "No nodes with system.run available.",
|
||||
defaultAgent: "default agent",
|
||||
usesDefault: "uses default",
|
||||
override: "override",
|
||||
switchToForm: "Switch the Config tab to Form mode to edit bindings here.",
|
||||
},
|
||||
|
||||
approvals: {
|
||||
title: "Exec Approvals",
|
||||
desc: "Pre-approve commands for agent execution.",
|
||||
title: "Exec approvals",
|
||||
desc: "Allowlist and approval policy for",
|
||||
target: "Target",
|
||||
targetDesc: "Gateway edits local approvals; node edits the selected node.",
|
||||
host: "Host",
|
||||
gateway: "Gateway",
|
||||
selectAgent: "Select agent",
|
||||
addRule: "Add Rule",
|
||||
noRules: "No approval rules configured.",
|
||||
selectNode: "Select node",
|
||||
noNodesYet: "No nodes advertise exec approvals yet.",
|
||||
scope: "Scope",
|
||||
defaults: "Defaults",
|
||||
security: "Security",
|
||||
securityDesc: "Default security mode.",
|
||||
mode: "Mode",
|
||||
deny: "Deny",
|
||||
allowlist: "Allowlist",
|
||||
full: "Full",
|
||||
ask: "Ask",
|
||||
askDesc: "Default prompt policy.",
|
||||
off: "Off",
|
||||
onMiss: "On miss",
|
||||
always: "Always",
|
||||
askFallback: "Ask fallback",
|
||||
askFallbackDesc: "Applied when the UI prompt is unavailable.",
|
||||
fallback: "Fallback",
|
||||
autoAllowSkills: "Auto-allow skill CLIs",
|
||||
autoAllowSkillsDesc: "Allow skill executables listed by the Gateway.",
|
||||
usingDefault: "Using default",
|
||||
addPattern: "Add pattern",
|
||||
allowlistTitle: "Allowlist",
|
||||
allowlistDesc: "Case-insensitive glob patterns.",
|
||||
noAllowlist: "No allowlist entries yet.",
|
||||
pattern: "Pattern",
|
||||
newPattern: "New pattern",
|
||||
lastUsed: "Last used",
|
||||
never: "never",
|
||||
remove: "Remove",
|
||||
loadApprovals: "Load approvals",
|
||||
loadApprovalsNote: "Load exec approvals to edit allowlists.",
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@ -414,34 +414,87 @@ export const zhTW = {
|
||||
// 節點頁面
|
||||
nodes: {
|
||||
title: "節點",
|
||||
desc: "已連線的執行節點與裝置配對。",
|
||||
noNodes: "沒有已連線的節點。",
|
||||
desc: "已配對的裝置與即時連結。",
|
||||
noNodes: "找不到節點。",
|
||||
devices: "裝置",
|
||||
devicesDesc: "配對請求與角色權杖。",
|
||||
noDevices: "沒有已配對的裝置。",
|
||||
approve: "核准",
|
||||
reject: "拒絕",
|
||||
revoke: "撤銷",
|
||||
rotate: "輪換",
|
||||
pending: "待處理",
|
||||
paired: "已配對",
|
||||
approved: "已核准",
|
||||
offline: "離線",
|
||||
tokens: "權杖",
|
||||
tokensNone: "權杖:無",
|
||||
role: "角色",
|
||||
requested: "請求於",
|
||||
repair: "修復",
|
||||
active: "使用中",
|
||||
revoked: "已撤銷",
|
||||
scopes: "範圍",
|
||||
roles: "角色",
|
||||
|
||||
bindings: {
|
||||
title: "執行綁定",
|
||||
desc: "將代理綁定到特定執行節點。",
|
||||
default: "預設節點",
|
||||
title: "執行節點綁定",
|
||||
desc: "使用時將代理固定到特定節點",
|
||||
default: "預設綁定",
|
||||
defaultDesc: "當代理未覆寫節點綁定時使用。",
|
||||
agent: "代理",
|
||||
node: "節點",
|
||||
save: "儲存綁定",
|
||||
anyNode: "任意節點",
|
||||
useDefault: "使用預設",
|
||||
save: "儲存",
|
||||
loadConfig: "載入組態",
|
||||
loadConfigNote: "載入組態以編輯綁定。",
|
||||
noNodesAvailable: "沒有支援 system.run 的節點。",
|
||||
defaultAgent: "預設代理",
|
||||
usesDefault: "使用預設",
|
||||
override: "覆寫",
|
||||
switchToForm: "請將「組態」分頁切換為「表單」模式以在此編輯綁定。",
|
||||
},
|
||||
|
||||
approvals: {
|
||||
title: "執行核准",
|
||||
desc: "預先核准代理可執行的指令。",
|
||||
desc: "許可清單與核准政策,適用於",
|
||||
target: "目標",
|
||||
targetDesc: "閘道器編輯本地核准;節點編輯選定的節點。",
|
||||
host: "主機",
|
||||
gateway: "閘道器",
|
||||
selectAgent: "選擇代理",
|
||||
addRule: "新增規則",
|
||||
noRules: "尚未設定核准規則。",
|
||||
selectNode: "選擇節點",
|
||||
noNodesYet: "尚無節點公告執行核准。",
|
||||
scope: "範圍",
|
||||
defaults: "預設值",
|
||||
security: "安全性",
|
||||
securityDesc: "預設安全模式。",
|
||||
mode: "模式",
|
||||
deny: "拒絕",
|
||||
allowlist: "許可清單",
|
||||
full: "完整",
|
||||
ask: "詢問",
|
||||
askDesc: "預設提示政策。",
|
||||
off: "關閉",
|
||||
onMiss: "未命中時",
|
||||
always: "總是",
|
||||
askFallback: "詢問備援",
|
||||
askFallbackDesc: "當 UI 提示不可用時套用。",
|
||||
fallback: "備援",
|
||||
autoAllowSkills: "自動允許技能 CLI",
|
||||
autoAllowSkillsDesc: "允許閘道器列出的技能可執行檔。",
|
||||
usingDefault: "使用預設",
|
||||
addPattern: "新增模式",
|
||||
allowlistTitle: "許可清單",
|
||||
allowlistDesc: "不區分大小寫的 glob 模式。",
|
||||
noAllowlist: "尚無許可清單項目。",
|
||||
pattern: "模式",
|
||||
newPattern: "新模式",
|
||||
lastUsed: "上次使用",
|
||||
never: "從未",
|
||||
remove: "移除",
|
||||
loadApprovals: "載入核准",
|
||||
loadApprovalsNote: "載入執行核准以編輯許可清單。",
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { html, nothing } from "lit";
|
||||
|
||||
import { t } from "../../i18n";
|
||||
import { clampText, formatAgo, formatList } from "../format";
|
||||
import type {
|
||||
ExecApprovalsAllowlistEntry,
|
||||
@ -60,16 +61,16 @@ export function renderNodes(props: NodesProps) {
|
||||
<section class="card">
|
||||
<div class="row" style="justify-content: space-between;">
|
||||
<div>
|
||||
<div class="card-title">Nodes</div>
|
||||
<div class="card-sub">Paired devices and live links.</div>
|
||||
<div class="card-title">${t("nodes.title")}</div>
|
||||
<div class="card-sub">${t("nodes.desc")}</div>
|
||||
</div>
|
||||
<button class="btn" ?disabled=${props.loading} @click=${props.onRefresh}>
|
||||
${props.loading ? "Loading…" : "Refresh"}
|
||||
${props.loading ? t("common.loading") : t("common.refresh")}
|
||||
</button>
|
||||
</div>
|
||||
<div class="list" style="margin-top: 16px;">
|
||||
${props.nodes.length === 0
|
||||
? html`<div class="muted">No nodes found.</div>`
|
||||
? html`<div class="muted">${t("nodes.noNodes")}</div>`
|
||||
: props.nodes.map((n) => renderNode(n))}
|
||||
</div>
|
||||
</section>
|
||||
@ -84,11 +85,11 @@ function renderDevices(props: NodesProps) {
|
||||
<section class="card">
|
||||
<div class="row" style="justify-content: space-between;">
|
||||
<div>
|
||||
<div class="card-title">Devices</div>
|
||||
<div class="card-sub">Pairing requests + role tokens.</div>
|
||||
<div class="card-title">${t("nodes.devices")}</div>
|
||||
<div class="card-sub">${t("nodes.devicesDesc")}</div>
|
||||
</div>
|
||||
<button class="btn" ?disabled=${props.devicesLoading} @click=${props.onDevicesRefresh}>
|
||||
${props.devicesLoading ? "Loading…" : "Refresh"}
|
||||
${props.devicesLoading ? t("common.loading") : t("common.refresh")}
|
||||
</button>
|
||||
</div>
|
||||
${props.devicesError
|
||||
@ -97,18 +98,18 @@ function renderDevices(props: NodesProps) {
|
||||
<div class="list" style="margin-top: 16px;">
|
||||
${pending.length > 0
|
||||
? html`
|
||||
<div class="muted" style="margin-bottom: 8px;">Pending</div>
|
||||
<div class="muted" style="margin-bottom: 8px;">${t("nodes.pending")}</div>
|
||||
${pending.map((req) => renderPendingDevice(req, props))}
|
||||
`
|
||||
: nothing}
|
||||
${paired.length > 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 12px; margin-bottom: 8px;">Paired</div>
|
||||
<div class="muted" style="margin-top: 12px; margin-bottom: 8px;">${t("nodes.paired")}</div>
|
||||
${paired.map((device) => renderPairedDevice(device, props))}
|
||||
`
|
||||
: nothing}
|
||||
${pending.length === 0 && paired.length === 0
|
||||
? html`<div class="muted">No paired devices.</div>`
|
||||
? html`<div class="muted">${t("nodes.noDevices")}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
</section>
|
||||
@ -117,9 +118,9 @@ function renderDevices(props: NodesProps) {
|
||||
|
||||
function renderPendingDevice(req: PendingDevice, props: NodesProps) {
|
||||
const name = req.displayName?.trim() || req.deviceId;
|
||||
const age = typeof req.ts === "number" ? formatAgo(req.ts) : "n/a";
|
||||
const role = req.role?.trim() ? `role: ${req.role}` : "role: -";
|
||||
const repair = req.isRepair ? " · repair" : "";
|
||||
const age = typeof req.ts === "number" ? formatAgo(req.ts) : t("common.na");
|
||||
const role = req.role?.trim() ? `${t("nodes.role")}: ${req.role}` : `${t("nodes.role")}: -`;
|
||||
const repair = req.isRepair ? ` · ${t("nodes.repair")}` : "";
|
||||
const ip = req.remoteIp ? ` · ${req.remoteIp}` : "";
|
||||
return html`
|
||||
<div class="list-item">
|
||||
@ -127,16 +128,16 @@ function renderPendingDevice(req: PendingDevice, props: NodesProps) {
|
||||
<div class="list-title">${name}</div>
|
||||
<div class="list-sub">${req.deviceId}${ip}</div>
|
||||
<div class="muted" style="margin-top: 6px;">
|
||||
${role} · requested ${age}${repair}
|
||||
${role} · ${t("nodes.requested")} ${age}${repair}
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-meta">
|
||||
<div class="row" style="justify-content: flex-end; gap: 8px; flex-wrap: wrap;">
|
||||
<button class="btn btn--sm primary" @click=${() => props.onDeviceApprove(req.requestId)}>
|
||||
Approve
|
||||
${t("nodes.approve")}
|
||||
</button>
|
||||
<button class="btn btn--sm" @click=${() => props.onDeviceReject(req.requestId)}>
|
||||
Reject
|
||||
${t("nodes.reject")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -147,8 +148,8 @@ function renderPendingDevice(req: PendingDevice, props: NodesProps) {
|
||||
function renderPairedDevice(device: PairedDevice, props: NodesProps) {
|
||||
const name = device.displayName?.trim() || device.deviceId;
|
||||
const ip = device.remoteIp ? ` · ${device.remoteIp}` : "";
|
||||
const roles = `roles: ${formatList(device.roles)}`;
|
||||
const scopes = `scopes: ${formatList(device.scopes)}`;
|
||||
const roles = `${t("nodes.roles")}: ${formatList(device.roles)}`;
|
||||
const scopes = `${t("nodes.scopes")}: ${formatList(device.scopes)}`;
|
||||
const tokens = Array.isArray(device.tokens) ? device.tokens : [];
|
||||
return html`
|
||||
<div class="list-item">
|
||||
@ -157,9 +158,9 @@ function renderPairedDevice(device: PairedDevice, props: NodesProps) {
|
||||
<div class="list-sub">${device.deviceId}${ip}</div>
|
||||
<div class="muted" style="margin-top: 6px;">${roles} · ${scopes}</div>
|
||||
${tokens.length === 0
|
||||
? html`<div class="muted" style="margin-top: 6px;">Tokens: none</div>`
|
||||
? html`<div class="muted" style="margin-top: 6px;">${t("nodes.tokensNone")}</div>`
|
||||
: html`
|
||||
<div class="muted" style="margin-top: 10px;">Tokens</div>
|
||||
<div class="muted" style="margin-top: 10px;">${t("nodes.tokens")}</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 8px; margin-top: 6px;">
|
||||
${tokens.map((token) => renderTokenRow(device.deviceId, token, props))}
|
||||
</div>
|
||||
@ -170,8 +171,8 @@ function renderPairedDevice(device: PairedDevice, props: NodesProps) {
|
||||
}
|
||||
|
||||
function renderTokenRow(deviceId: string, token: DeviceTokenSummary, props: NodesProps) {
|
||||
const status = token.revokedAtMs ? "revoked" : "active";
|
||||
const scopes = `scopes: ${formatList(token.scopes)}`;
|
||||
const status = token.revokedAtMs ? t("nodes.revoked") : t("nodes.active");
|
||||
const scopes = `${t("nodes.scopes")}: ${formatList(token.scopes)}`;
|
||||
const when = formatAgo(token.rotatedAtMs ?? token.createdAtMs ?? token.lastUsedAtMs ?? null);
|
||||
return html`
|
||||
<div class="row" style="justify-content: space-between; gap: 8px;">
|
||||
@ -181,7 +182,7 @@ function renderTokenRow(deviceId: string, token: DeviceTokenSummary, props: Node
|
||||
class="btn btn--sm"
|
||||
@click=${() => props.onDeviceRotate(deviceId, token.role, token.scopes)}
|
||||
>
|
||||
Rotate
|
||||
${t("nodes.rotate")}
|
||||
</button>
|
||||
${token.revokedAtMs
|
||||
? nothing
|
||||
@ -190,7 +191,7 @@ function renderTokenRow(deviceId: string, token: DeviceTokenSummary, props: Node
|
||||
class="btn btn--sm danger"
|
||||
@click=${() => props.onDeviceRevoke(deviceId, token.role)}
|
||||
>
|
||||
Revoke
|
||||
${t("nodes.revoke")}
|
||||
</button>
|
||||
`}
|
||||
</div>
|
||||
@ -436,9 +437,9 @@ function renderBindings(state: BindingState) {
|
||||
<section class="card">
|
||||
<div class="row" style="justify-content: space-between; align-items: center;">
|
||||
<div>
|
||||
<div class="card-title">Exec node binding</div>
|
||||
<div class="card-title">${t("nodes.bindings.title")}</div>
|
||||
<div class="card-sub">
|
||||
Pin agents to a specific node when using <span class="mono">exec host=node</span>.
|
||||
${t("nodes.bindings.desc")} <span class="mono">exec host=node</span>.
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@ -446,33 +447,33 @@ function renderBindings(state: BindingState) {
|
||||
?disabled=${state.disabled || !state.configDirty}
|
||||
@click=${state.onSave}
|
||||
>
|
||||
${state.configSaving ? "Saving…" : "Save"}
|
||||
${state.configSaving ? t("common.saving") : t("nodes.bindings.save")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${state.formMode === "raw"
|
||||
? html`<div class="callout warn" style="margin-top: 12px;">
|
||||
Switch the Config tab to <strong>Form</strong> mode to edit bindings here.
|
||||
${t("nodes.bindings.switchToForm")}
|
||||
</div>`
|
||||
: nothing}
|
||||
|
||||
${!state.ready
|
||||
? html`<div class="row" style="margin-top: 12px; gap: 12px;">
|
||||
<div class="muted">Load config to edit bindings.</div>
|
||||
<div class="muted">${t("nodes.bindings.loadConfigNote")}</div>
|
||||
<button class="btn" ?disabled=${state.configLoading} @click=${state.onLoadConfig}>
|
||||
${state.configLoading ? "Loading…" : "Load config"}
|
||||
${state.configLoading ? t("common.loading") : t("nodes.bindings.loadConfig")}
|
||||
</button>
|
||||
</div>`
|
||||
: html`
|
||||
<div class="list" style="margin-top: 16px;">
|
||||
<div class="list-item">
|
||||
<div class="list-main">
|
||||
<div class="list-title">Default binding</div>
|
||||
<div class="list-sub">Used when agents do not override a node binding.</div>
|
||||
<div class="list-title">${t("nodes.bindings.default")}</div>
|
||||
<div class="list-sub">${t("nodes.bindings.defaultDesc")}</div>
|
||||
</div>
|
||||
<div class="list-meta">
|
||||
<label class="field">
|
||||
<span>Node</span>
|
||||
<span>${t("nodes.bindings.node")}</span>
|
||||
<select
|
||||
?disabled=${state.disabled || !supportsBinding}
|
||||
@change=${(event: Event) => {
|
||||
@ -481,7 +482,7 @@ function renderBindings(state: BindingState) {
|
||||
state.onBindDefault(value ? value : null);
|
||||
}}
|
||||
>
|
||||
<option value="" ?selected=${defaultValue === ""}>Any node</option>
|
||||
<option value="" ?selected=${defaultValue === ""}>${t("nodes.bindings.anyNode")}</option>
|
||||
${state.nodes.map(
|
||||
(node) =>
|
||||
html`<option
|
||||
@ -494,13 +495,13 @@ function renderBindings(state: BindingState) {
|
||||
</select>
|
||||
</label>
|
||||
${!supportsBinding
|
||||
? html`<div class="muted">No nodes with system.run available.</div>`
|
||||
? html`<div class="muted">${t("nodes.bindings.noNodesAvailable")}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${state.agents.length === 0
|
||||
? html`<div class="muted">No agents found.</div>`
|
||||
? html`<div class="muted">${t("nodes.noNodes")}</div>`
|
||||
: state.agents.map((agent) =>
|
||||
renderAgentBinding(agent, state),
|
||||
)}
|
||||
@ -517,9 +518,9 @@ function renderExecApprovals(state: ExecApprovalsState) {
|
||||
<section class="card">
|
||||
<div class="row" style="justify-content: space-between; align-items: center;">
|
||||
<div>
|
||||
<div class="card-title">Exec approvals</div>
|
||||
<div class="card-title">${t("nodes.approvals.title")}</div>
|
||||
<div class="card-sub">
|
||||
Allowlist and approval policy for <span class="mono">exec host=gateway/node</span>.
|
||||
${t("nodes.approvals.desc")} <span class="mono">exec host=gateway/node</span>.
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@ -527,7 +528,7 @@ function renderExecApprovals(state: ExecApprovalsState) {
|
||||
?disabled=${state.disabled || !state.dirty || !targetReady}
|
||||
@click=${state.onSave}
|
||||
>
|
||||
${state.saving ? "Saving…" : "Save"}
|
||||
${state.saving ? t("common.saving") : t("common.save")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -535,9 +536,9 @@ function renderExecApprovals(state: ExecApprovalsState) {
|
||||
|
||||
${!ready
|
||||
? html`<div class="row" style="margin-top: 12px; gap: 12px;">
|
||||
<div class="muted">Load exec approvals to edit allowlists.</div>
|
||||
<div class="muted">${t("nodes.approvals.loadApprovalsNote")}</div>
|
||||
<button class="btn" ?disabled=${state.loading || !targetReady} @click=${state.onLoad}>
|
||||
${state.loading ? "Loading…" : "Load approvals"}
|
||||
${state.loading ? t("common.loading") : t("nodes.approvals.loadApprovals")}
|
||||
</button>
|
||||
</div>`
|
||||
: html`
|
||||
@ -558,14 +559,14 @@ function renderExecApprovalsTarget(state: ExecApprovalsState) {
|
||||
<div class="list" style="margin-top: 12px;">
|
||||
<div class="list-item">
|
||||
<div class="list-main">
|
||||
<div class="list-title">Target</div>
|
||||
<div class="list-title">${t("nodes.approvals.target")}</div>
|
||||
<div class="list-sub">
|
||||
Gateway edits local approvals; node edits the selected node.
|
||||
${t("nodes.approvals.targetDesc")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-meta">
|
||||
<label class="field">
|
||||
<span>Host</span>
|
||||
<span>${t("nodes.approvals.host")}</span>
|
||||
<select
|
||||
?disabled=${state.disabled}
|
||||
@change=${(event: Event) => {
|
||||
@ -579,14 +580,14 @@ function renderExecApprovalsTarget(state: ExecApprovalsState) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="gateway" ?selected=${state.target === "gateway"}>Gateway</option>
|
||||
<option value="node" ?selected=${state.target === "node"}>Node</option>
|
||||
<option value="gateway" ?selected=${state.target === "gateway"}>${t("nodes.approvals.gateway")}</option>
|
||||
<option value="node" ?selected=${state.target === "node"}>${t("nodes.bindings.node")}</option>
|
||||
</select>
|
||||
</label>
|
||||
${state.target === "node"
|
||||
? html`
|
||||
<label class="field">
|
||||
<span>Node</span>
|
||||
<span>${t("nodes.bindings.node")}</span>
|
||||
<select
|
||||
?disabled=${state.disabled || !hasNodes}
|
||||
@change=${(event: Event) => {
|
||||
@ -595,7 +596,7 @@ function renderExecApprovalsTarget(state: ExecApprovalsState) {
|
||||
state.onSelectTarget("node", value ? value : null);
|
||||
}}
|
||||
>
|
||||
<option value="" ?selected=${nodeValue === ""}>Select node</option>
|
||||
<option value="" ?selected=${nodeValue === ""}>${t("nodes.approvals.selectNode")}</option>
|
||||
${state.targetNodes.map(
|
||||
(node) =>
|
||||
html`<option
|
||||
@ -612,7 +613,7 @@ function renderExecApprovalsTarget(state: ExecApprovalsState) {
|
||||
</div>
|
||||
</div>
|
||||
${state.target === "node" && !hasNodes
|
||||
? html`<div class="muted">No nodes advertise exec approvals yet.</div>`
|
||||
? html`<div class="muted">${t("nodes.approvals.noNodesYet")}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
@ -621,13 +622,13 @@ function renderExecApprovalsTarget(state: ExecApprovalsState) {
|
||||
function renderExecApprovalsTabs(state: ExecApprovalsState) {
|
||||
return html`
|
||||
<div class="row" style="margin-top: 12px; gap: 8px; flex-wrap: wrap;">
|
||||
<span class="label">Scope</span>
|
||||
<span class="label">${t("nodes.approvals.scope")}</span>
|
||||
<div class="row" style="gap: 8px; flex-wrap: wrap;">
|
||||
<button
|
||||
class="btn btn--sm ${state.selectedScope === EXEC_APPROVALS_DEFAULT_SCOPE ? "active" : ""}"
|
||||
@click=${() => state.onSelectScope(EXEC_APPROVALS_DEFAULT_SCOPE)}
|
||||
>
|
||||
Defaults
|
||||
${t("nodes.approvals.defaults")}
|
||||
</button>
|
||||
${state.agents.map((agent) => {
|
||||
const label = agent.name?.trim() ? `${agent.name} (${agent.id})` : agent.id;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user