Files
pigway-console-adapter-fram…/plugin/ui/index.js
T
2026-10-18 23:58:37 +08:00

104 lines
8.5 KiB
JavaScript

import { scopeAlertHealth, summaryStatus } from '/assets/js/overview-health.js';
import { state } from '/assets/js/state.js';
import { $, esc, pluginText as text } from '/assets/js/shared.js';
import { get } from '/assets/js/navigation.js';
function managementLink(host) {
try {
const url = new URL(host.connection?.management_url || host.connection?.url || host.url);
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) return '';
const label = text('打开 Frame Flow 管理', 'Open Frame Flow management');
return `<a class="docker-icon" style="margin-left: -7px" href="${esc(url.href)}" target="_blank" rel="noopener noreferrer" title="${label}" aria-label="${label}"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 4h6v6M20 4l-10 10M11 4H4v16h16v-7"/></svg></a>`;
} catch { return ''; }
}
const metricValue = metric => {
if (metric.unit === 'percent') return `${metric.value}%`;
if (metric.unit === 'count') return `${metric.value} ${text('个', 'items')}`;
const values = {
service_ready: [text('未就绪', 'Not ready'), text('就绪', 'Ready')],
device_online: [text('离线', 'Offline'), text('在线', 'Online')],
job_enabled: [text('停用', 'Disabled'), text('启用', 'Enabled')],
last_run_success: [text('失败或待确认', 'Failed or unconfirmed'), text('成功', 'Succeeded')],
};
return values[metric.key]?.[metric.value] ?? String(metric.value);
};
export function hostCard(entry) {
const snapshot = entry.snapshot || {};
const resources = snapshot.resources || [];
const current = entry.dataCurrent === true;
const label = item => item.label?.[state.LANG] || item.key || item.id;
const name = id => resources.find(r => r.id === id)?.name || id;
const alertRows = (snapshot.alerts || []).map(alert => {
const separator = alert.id.indexOf(':');
const kind = alert.id.split(':')[0];
const resource = separator < 0 ? '' : `${kind.startsWith('JOB_') ? 'job' : 'device'}:${alert.id.slice(separator + 1)}`;
return `<div class="row"><span>${esc(label(alert))}</span><span>${esc(resource ? name(resource) : '')}</span></div>`;
}).join('');
const metrics = snapshot.metrics || [];
const value = (resource, key) => metrics.find(m => m.resource_id === resource.id && m.key === key)?.value;
const card = (title, content, status = '') => `<div class="metric"><div style="display:flex;justify-content:space-between;align-items:baseline;gap:12px"><div class="muted">${esc(title)}</div>${status}</div><div class="value">${esc(content)}</div></div>`;
const status = (semantic, title) => `<span class="semantic-status" data-semantic="${semantic}">${esc(title)}</span>`;
const cards = resources.flatMap(resource => {
if (resource.type === 'job') {
const enabled = value(resource, 'job_enabled') === 1;
const result = value(resource, 'last_run_success');
const unknown = (snapshot.alerts || []).some(a => a.id === `JOB_UNKNOWN:${resource.id.slice(4)}`);
const kind = !current || unknown || result === undefined ? 'unknown' : result === 1 ? 'normal' : 'error';
const stateLabel = kind === 'normal' ? text('正常', 'Normal') : kind === 'error' ? text('失败', 'Failed') : text('待确认', 'Unconfirmed');
const title = /轮播/.test(resource.name || '') ? text('轮播任务', 'Slideshow task') : resource.name || text('任务', 'Job');
return [card(title, `${resource.name || text('任务', 'Job')} · ${enabled ? text('启用', 'Enabled') : text('停用', 'Disabled')}`, status(kind, stateLabel))];
}
return metrics.filter(m => m.resource_id === resource.id).map(metric => {
const title = label(metric);
return card(title, metricValue(metric));
});
}).join('');
const frames = resources.filter(resource => resource.type === 'device');
return `<article class="panel host-panel"><div class="host-head"><div><div class="host-title"><h2>${esc(entry.host.name || 'Frame Flow')}</h2>${managementLink(entry.host)}</div><div class="device-details">${frames.map(frame => `<span>${esc(frame.name || text('画框', 'Frame'))}</span>`).join('')}</div></div><span class="badge semantic-status" data-semantic="${current ? 'online' : 'unknown'}">${current ? text('在线', 'Online') : text('状态未确认', 'Unconfirmed')}</span></div>${!current ? `<p class="muted">${text('以下为上次采集数据', 'Showing the last collected data')}</p>` : ''}<div class="host-grid">${cards}</div>${(snapshot.alerts || []).length || !current || snapshot.alerts_complete !== true ? `<details class="group" data-overview-details="alerts"><summary class="smart-summary"><strong class="smart-summary-title">${text('已读取范围内的告警', 'Alerts within monitored scope')}</strong>${summaryStatus(scopeAlertHealth(snapshot.alerts, snapshot.alerts_complete, current), state.LANG)}</summary><div style="margin-top:12px">${alertRows || (current && snapshot.alerts_complete ? text('无告警', 'No alerts') : text('状态未确认', 'Unconfirmed'))}</div></details>` : ""}</article>`;
}
export function renderConfig() {
const host = state.fleetHosts.find(h => h.id === state.currentConfigHost);
const connection = host.connection || {};
$('mainSavebar').classList.add('hidden');
$('configMenu').innerHTML = `<button data-config-section="frame-flow" class="active">${text('Frame Flow 接入', 'Frame Flow connection')}</button>`;
$('configRows').innerHTML = `<section class="config-page active" data-config-page="frame-flow"><h2>${text('Frame Flow 接入', 'Frame Flow connection')}</h2><p class="hint">${text('配置保存在 Console;管理入口留空时,首页链接使用 Frame Flow 服务地址。API 令牌留空保留现有值。', 'Settings are stored in Console. Leave the management URL blank to use the Frame Flow service URL on the overview. A blank API token keeps the existing value.')}</p><div class="fields">${[
['url', text('Frame Flow 地址', 'Frame Flow URL'), connection.url || host.url || ''],
['token', text('API 令牌(留空保留)', 'API token (blank keeps existing)'), ''],
['management_url', text('管理入口地址(留空使用服务地址)', 'Management URL (blank uses service URL)'), connection.management_url || ''],
].map(([key, label, value]) => `<div class="field"><label>${label}<input data-frame-flow-connection="${key}" type="${key === 'token' ? 'password' : 'url'}" value="${esc(value)}" autocomplete="${key === 'token' ? 'new-password' : 'off'}"></label></div>`).join('')}</div><div class="action-row"><button id="saveFrameFlowConnection">${text('保存接入配置', 'Save connection')}</button><span id="frameFlowConnectionMessage" role="status"></span></div></section>`;
const page = $('configRows').querySelector('[data-config-page="frame-flow"]');
$('configMenu').onclick = event => {
if (!event.target.closest('[data-config-section="frame-flow"]')) return;
$('configMenu').querySelectorAll('button').forEach(b => b.classList.toggle('active', b.dataset.configSection === 'frame-flow'));
$('configRows').querySelectorAll('[data-config-page]').forEach(p => p.classList.toggle('active', p === page));
$('mainSavebar').classList.add('hidden');
};
$('saveFrameFlowConnection').onclick = async () => {
const button = $('saveFrameFlowConnection'), message = $('frameFlowConnectionMessage');
const values = Object.fromEntries([...page.querySelectorAll('[data-frame-flow-connection]')].map(input => [input.dataset.frameFlowConnection, input.value.trim()]));
for (const [key, value] of Object.entries(values)) {
if (key === 'token' || (key === 'management_url' && !value)) continue;
try {
const url = new URL(value);
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.pathname !== '/' || url.search || url.hash) throw Error();
} catch {
message.textContent = text('请使用不含路径或账号密码的 HTTP(S) 根地址', 'Use an HTTP(S) origin without paths or credentials');
message.className = 'error'; return;
}
}
button.disabled = true;
try {
const result = await get('/api/v1/hosts', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ operation: 'save', id: host.id, adapter_id: host.adapter_id, connection: values }) });
state.fleetHosts = result.hosts;
page.querySelector('[data-frame-flow-connection="token"]').value = '';
message.textContent = text('已保存', 'Saved'); message.className = 'success';
} catch (error) { message.textContent = error.message; message.className = 'error'; }
finally { button.disabled = false; }
};
}