fix UI client

This commit is contained in:
2026-06-08 09:27:55 +07:00
parent 08b94337ad
commit f9bec78c82
4 changed files with 313 additions and 149 deletions

View File

@@ -233,6 +233,7 @@ function App() {
const [busyAction, setBusyAction] = useState('');
const [activeTask, setActiveTask] = useState(null);
const [endpointDialogOpen, setEndpointDialogOpen] = useState(false);
const [agentDialogOpen, setAgentDialogOpen] = useState(false);
const packageBaseUrl = settings.packageBaseUrl;
const agentBaseUrl = settings.agentBaseUrl;
@@ -300,15 +301,6 @@ function App() {
);
}, [agentHealth?.agentVersion, latestAgentPackage?.version]);
const stats = useMemo(() => {
return {
available: apps.length,
installed: installedApps.length,
updates: mergedApps.filter((app) => app.canUpdate).length,
components: selectedManifest?.components?.length || selectedDetail?.packages?.length || 0
};
}, [apps.length, installedApps.length, mergedApps, selectedDetail, selectedManifest]);
const notify = useCallback((type, message) => {
setToast({ id: Date.now(), type, message });
}, []);
@@ -628,17 +620,18 @@ function App() {
}, [toast]);
useEffect(() => {
if (!endpointDialogOpen) return undefined;
if (!endpointDialogOpen && !agentDialogOpen) return undefined;
function onKeyDown(event) {
if (event.key === 'Escape') {
closeEndpointDialog();
if (endpointDialogOpen) closeEndpointDialog();
if (agentDialogOpen) setAgentDialogOpen(false);
}
}
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, [closeEndpointDialog, endpointDialogOpen]);
}, [agentDialogOpen, closeEndpointDialog, endpointDialogOpen]);
const agentStatusTitle = !canUseAgentEndpoint
? (isClientWindows ? 'Remote endpoint needed' : 'Linux client required')
@@ -706,6 +699,12 @@ function App() {
<strong>{topbarAgentState}</strong>
</div>
<div className="topbar-actions">
<AgentStatusButton
detail={agentStatusDetail}
onClick={() => setAgentDialogOpen(true)}
title={agentStatusTitle}
tone={agentStatusTone}
/>
<a className="icon-button" href={joinUrl(packageBaseUrl, '/api/apps')} target="_blank" rel="noreferrer" title="Open package API">
<ExternalLink size={17} aria-hidden="true" />
</a>
@@ -717,34 +716,6 @@ function App() {
</header>
<section className="page">
<div className="page-header">
<div>
<h1>Application catalog</h1>
<p>Released apps from package server and install state on the selected Agent endpoint.</p>
</div>
{canShowAgentCommand && (
<div className="page-actions">
<button className="btn btn-secondary" type="button" onClick={copyInstallCommand}>
<Clipboard size={15} aria-hidden="true" />
{agentNeedsUpdate ? 'Copy Agent update' : 'Copy Agent command'}
</button>
{isClientLinux && isLocalAgentEndpoint && (
<a className="btn btn-primary" href={joinUrl(packageBaseUrl, '/install-agent.sh')} target="_blank" rel="noreferrer">
<Download size={15} aria-hidden="true" />
{agentNeedsUpdate ? 'update-agent.sh' : 'install-agent.sh'}
</a>
)}
</div>
)}
</div>
<div className="dashboard-stats">
<MetricCard label="Released apps" value={stats.available} note={packageStatus.state === 'loading' ? 'loading' : 'from API'} />
<MetricCard label="Installed target" value={stats.installed} note={agentHealth ? 'Agent SQLite' : 'Agent offline'} />
<MetricCard label="Updates" value={stats.updates} note="version diff" tone={stats.updates ? 'warning' : 'success'} />
<MetricCard label="Components" value={stats.components} note={selectedApp?.appCode || selectedApp?.appId || 'selected app'} />
</div>
{!canUseAgentEndpoint && (
<ClientOsNotice
agentCommand={agentCommand}
@@ -937,17 +908,6 @@ function App() {
</section>
<aside className="side-stack">
<AgentPanel
health={agentHealth}
systemInfo={systemInfo}
status={agentStatus}
title={agentTargetLabel}
endpoint={agentBaseUrl}
latestAgentPackage={latestAgentPackage}
needsUpdate={agentNeedsUpdate}
onCopyUpdate={copyInstallCommand}
showAgentActions={canShowAgentCommand}
/>
{activeTask && (
<TaskPanel
task={activeTask}
@@ -968,6 +928,20 @@ function App() {
</main>
{toast && <Toast toast={toast} />}
{agentDialogOpen && (
<AgentDialog
endpoint={agentBaseUrl}
health={agentHealth}
latestAgentPackage={latestAgentPackage}
needsUpdate={agentNeedsUpdate}
onClose={() => setAgentDialogOpen(false)}
onCopyUpdate={copyInstallCommand}
showAgentActions={canShowAgentCommand}
status={agentStatus}
systemInfo={systemInfo}
title={agentTargetLabel}
/>
)}
{endpointDialogOpen && (
<EndpointDialog
draftSettings={draftSettings}
@@ -980,6 +954,43 @@ function App() {
);
}
function AgentStatusButton({ title, detail, tone, onClick }) {
const Icon = tone === 'success'
? CheckCircle2
: tone === 'danger'
? XCircle
: tone === 'warning'
? AlertCircle
: Loader2;
return (
<button className={`agent-status-button tone-${tone || 'info'}`} type="button" onClick={onClick}>
<span className="agent-status-icon">
<Icon className={tone === 'info' ? 'spin' : ''} size={16} aria-hidden="true" />
</span>
<span className="agent-status-copy">
<strong>{title}</strong>
<small>{detail}</small>
</span>
</button>
);
}
function AgentDialog(props) {
return (
<div className="dialog-backdrop">
<section
aria-labelledby="agent-dialog-title"
aria-modal="true"
className="dialog-panel agent-dialog-panel"
role="dialog"
>
<AgentPanel {...props} />
</section>
</div>
);
}
function EndpointDialog({ draftSettings, onApply, onCancel, onChange }) {
return (
<div className="dialog-backdrop">
@@ -1056,18 +1067,6 @@ function StatusBox({ icon: Icon, title, detail, tone }) {
);
}
function MetricCard({ label, value, note, tone }) {
return (
<article className={`metric-card ${tone ? `tone-${tone}` : ''}`}>
<span>{label}</span>
<div>
<strong>{value}</strong>
<small>{note}</small>
</div>
</article>
);
}
function ClientOsNotice({ agentCommand, canShowCommand, isWindows, onCopyCommand, osLabel }) {
return (
<div className={`offline-banner client-os-banner ${canShowCommand ? 'command-visible' : ''}`}>
@@ -1213,17 +1212,49 @@ function TaskPanel({ task, onClear, onRefresh }) {
);
}
function AgentPanel({ health, systemInfo, status, title, endpoint, latestAgentPackage, needsUpdate, onCopyUpdate, showAgentActions }) {
function AgentPanel({ health, systemInfo, status, title, endpoint, latestAgentPackage, needsUpdate, onClose, onCopyUpdate, showAgentActions }) {
const statusTone = needsUpdate
? 'warning'
: health
? 'success'
: status.state === 'loading'
? 'info'
: status.state === 'warning'
? 'warning'
: 'danger';
const statusLabel = needsUpdate
? 'Update available'
: health
? 'Online'
: status.state === 'loading'
? 'Checking'
: status.state === 'warning'
? 'Warning'
: 'Offline';
return (
<section className="panel">
<section className="agent-detail">
<div className="panel-header">
<div>
<h2>{title || 'Agent'}</h2>
<h2 id="agent-dialog-title">{title || 'Agent'}</h2>
<p>{status.message || endpoint || '127.0.0.1:5010'}</p>
</div>
{needsUpdate ? <AlertCircle className="panel-state warning" size={20} aria-hidden="true" /> : health ? <CheckCircle2 className="panel-state success" size={20} aria-hidden="true" /> : <XCircle className="panel-state danger" size={20} aria-hidden="true" />}
<div className="panel-actions">
{needsUpdate ? <AlertCircle className="panel-state warning" size={20} aria-hidden="true" /> : health ? <CheckCircle2 className="panel-state success" size={20} aria-hidden="true" /> : <XCircle className="panel-state danger" size={20} aria-hidden="true" />}
<button className="icon-button subtle" type="button" title="Close" onClick={onClose}>
<X size={16} aria-hidden="true" />
</button>
</div>
</div>
<dl className="detail-list compact-list">
<div>
<dt>Status</dt>
<dd><span className={`badge badge-${statusTone}`}>{statusLabel}</span></dd>
</div>
<div>
<dt>Endpoint</dt>
<dd>{endpoint || '127.0.0.1:5010'}</dd>
</div>
<div>
<dt>Version</dt>
<dd>
@@ -1254,13 +1285,13 @@ function AgentPanel({ health, systemInfo, status, title, endpoint, latestAgentPa
<span><Cpu size={14} aria-hidden="true" /> {systemInfo?.kernel || 'kernel -'}</span>
<span><HardDrive size={14} aria-hidden="true" /> {systemInfo?.diskFree || 'disk -'}</span>
</div>
{needsUpdate && showAgentActions && (
{showAgentActions && (
<div className="agent-update-action">
<button className="btn btn-warning" type="button" onClick={onCopyUpdate}>
<button className={`btn ${needsUpdate ? 'btn-warning' : 'btn-secondary'}`} type="button" onClick={onCopyUpdate}>
<Clipboard size={15} aria-hidden="true" />
Copy update command
{needsUpdate ? 'Copy update command' : 'Copy Agent command'}
</button>
{latestAgentPackage?.downloadUrl && (
{needsUpdate && latestAgentPackage?.downloadUrl && (
<a className="btn btn-secondary" href={latestAgentPackage.downloadUrl} target="_blank" rel="noreferrer">
<Download size={15} aria-hidden="true" />
Latest .deb
@@ -1311,7 +1342,7 @@ function AppDetailPanel({ app, detail, manifest, status, packageBaseUrl }) {
{status.state === 'danger' && (
<div className="table-empty compact-empty danger-text">{status.message}</div>
)}
{(components.length ? components : packages).slice(0, 5).map((item) => (
{(components.length ? components : packages).map((item) => (
<div className="component-item" key={item.componentId || item.id || item.packageId}>
<div>
<strong>{item.componentId || item.code || item.name}</strong>