fix UI client
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user