UI cảnh báo cho máy không phải linux
This commit is contained in:
@@ -141,6 +141,34 @@ function compareAgentVersions(currentVersion, latestVersion) {
|
||||
return AGENT_VERSION_COLLATOR.compare(current, latest);
|
||||
}
|
||||
|
||||
const CLIENT_OS_LABELS = {
|
||||
android: 'Android',
|
||||
linux: 'Linux',
|
||||
macos: 'macOS',
|
||||
unknown: 'unknown OS',
|
||||
windows: 'Windows'
|
||||
};
|
||||
|
||||
function detectClientOs() {
|
||||
if (typeof navigator === 'undefined') return 'unknown';
|
||||
|
||||
const ua = String(navigator.userAgent || '').toLowerCase();
|
||||
const platform = [
|
||||
navigator.userAgentData?.platform,
|
||||
navigator.platform
|
||||
].filter(Boolean).join(' ').toLowerCase();
|
||||
|
||||
if (ua.includes('android') || platform.includes('android')) return 'android';
|
||||
if (platform.includes('linux') || ua.includes('linux')) return 'linux';
|
||||
if (platform.includes('win') || ua.includes('windows')) return 'windows';
|
||||
if (platform.includes('mac') || ua.includes('mac os')) return 'macos';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function getClientOsLabel(os) {
|
||||
return CLIENT_OS_LABELS[os] || CLIENT_OS_LABELS.unknown;
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [settings, setSettings] = useState(readSettings);
|
||||
const [draftSettings, setDraftSettings] = useState(settings);
|
||||
@@ -166,6 +194,9 @@ function App() {
|
||||
const agentBaseUrl = settings.agentBaseUrl;
|
||||
const installCommand = `curl -fsSL ${joinUrl(packageBaseUrl, '/install-agent.sh')} | sudo bash`;
|
||||
const agentCommand = latestAgentPackage?.installCommand || installCommand;
|
||||
const clientOs = useMemo(() => detectClientOs(), []);
|
||||
const isClientLinux = clientOs === 'linux';
|
||||
const clientOsLabel = getClientOsLabel(clientOs);
|
||||
|
||||
const installedByAppId = useMemo(() => {
|
||||
return new Map(installedApps.map((app) => [app.appId, app]));
|
||||
@@ -251,6 +282,17 @@ function App() {
|
||||
}, [packageBaseUrl]);
|
||||
|
||||
const refreshAgent = useCallback(async () => {
|
||||
if (!isClientLinux) {
|
||||
setAgentHealth(null);
|
||||
setSystemInfo(null);
|
||||
setInstalledApps([]);
|
||||
setAgentStatus({
|
||||
state: 'warning',
|
||||
message: `Current client is ${clientOsLabel}. Web Client only supports Linux.`
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
setAgentStatus({ state: 'loading', message: 'Đang kiểm tra Agent local' });
|
||||
try {
|
||||
const health = await fetchAgentHealth(agentBaseUrl);
|
||||
@@ -271,7 +313,7 @@ function App() {
|
||||
setAgentStatus({ state: 'danger', message: getErrorMessage(error) });
|
||||
return false;
|
||||
}
|
||||
}, [agentBaseUrl]);
|
||||
}, [agentBaseUrl, clientOsLabel, isClientLinux]);
|
||||
|
||||
const refreshAll = useCallback(async () => {
|
||||
await Promise.all([refreshPackage(), refreshAgent()]);
|
||||
@@ -385,6 +427,11 @@ function App() {
|
||||
}, []);
|
||||
|
||||
const runAppAction = useCallback(async (action, app) => {
|
||||
if (!isClientLinux) {
|
||||
notify('warning', 'Web Client chỉ hỗ trợ cài đặt trên máy Linux.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!agentHealth) {
|
||||
notify('warning', 'Agent local đang offline. Cài Agent rồi bấm Retry.');
|
||||
return;
|
||||
@@ -425,7 +472,7 @@ function App() {
|
||||
} finally {
|
||||
setBusyAction('');
|
||||
}
|
||||
}, [agentBaseUrl, agentHealth, notify, packageBaseUrl, startPreflightFailedTask, startTask]);
|
||||
}, [agentBaseUrl, agentHealth, isClientLinux, notify, packageBaseUrl, startPreflightFailedTask, startTask]);
|
||||
|
||||
const openEndpointDialog = useCallback(() => {
|
||||
setDraftSettings(settings);
|
||||
@@ -450,6 +497,11 @@ function App() {
|
||||
}, [draftSettings, notify]);
|
||||
|
||||
const copyInstallCommand = useCallback(async () => {
|
||||
if (!isClientLinux) {
|
||||
notify('warning', 'Lệnh cài Agent chỉ hiển thị trên máy Linux.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!navigator.clipboard?.writeText) {
|
||||
if (copyTextFallback(agentCommand)) {
|
||||
notify('success', agentNeedsUpdate ? 'Da copy lenh update Agent' : 'Da copy lenh cai Agent');
|
||||
@@ -467,7 +519,7 @@ function App() {
|
||||
} catch {
|
||||
notify('warning', 'Không thể copy tự động trong browser này');
|
||||
}
|
||||
}, [agentCommand, agentNeedsUpdate, notify]);
|
||||
}, [agentCommand, agentNeedsUpdate, isClientLinux, notify]);
|
||||
|
||||
useEffect(() => {
|
||||
refreshAll();
|
||||
@@ -551,9 +603,9 @@ function App() {
|
||||
<div className="nav-label">Runtime</div>
|
||||
<StatusBox
|
||||
icon={agentHealth ? PlugZap : WifiOff}
|
||||
title={agentNeedsUpdate ? 'Agent update available' : (agentHealth ? 'Agent online' : 'Agent offline')}
|
||||
detail={agentHealth ? `${agentHealth.hostname || 'localhost'} · ${agentHealth.agentVersion || '-'}${agentNeedsUpdate ? ` -> ${latestAgentPackage.version}` : ''}` : '127.0.0.1:5010'}
|
||||
tone={agentNeedsUpdate ? 'warning' : (agentHealth ? 'success' : 'danger')}
|
||||
title={!isClientLinux ? 'Linux client required' : (agentNeedsUpdate ? 'Agent update available' : (agentHealth ? 'Agent online' : 'Agent offline'))}
|
||||
detail={!isClientLinux ? `${clientOsLabel} detected` : (agentHealth ? `${agentHealth.hostname || 'localhost'} · ${agentHealth.agentVersion || '-'}${agentNeedsUpdate ? ` -> ${latestAgentPackage.version}` : ''}` : '127.0.0.1:5010')}
|
||||
tone={!isClientLinux ? 'warning' : (agentNeedsUpdate ? 'warning' : (agentHealth ? 'success' : 'danger'))}
|
||||
/>
|
||||
<StatusBox
|
||||
icon={Server}
|
||||
@@ -584,7 +636,7 @@ function App() {
|
||||
<header className="topbar">
|
||||
<div className="topbar-title">
|
||||
<span>robot.installer</span>
|
||||
<strong>{agentHealth ? 'Ready for install' : 'Waiting for Agent'}</strong>
|
||||
<strong>{!isClientLinux ? 'Linux client required' : (agentHealth ? 'Ready for install' : 'Waiting for Agent')}</strong>
|
||||
</div>
|
||||
<div className="topbar-actions">
|
||||
<a className="icon-button" href={joinUrl(packageBaseUrl, '/api/apps')} target="_blank" rel="noreferrer" title="Open package API">
|
||||
@@ -603,16 +655,18 @@ function App() {
|
||||
<h1>Application catalog</h1>
|
||||
<p>Released apps từ package server và trạng thái cài đặt trên máy local.</p>
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
{isClientLinux && (
|
||||
<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>
|
||||
<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">
|
||||
@@ -622,7 +676,11 @@ function App() {
|
||||
<MetricCard label="Components" value={stats.components} note={selectedApp?.appCode || selectedApp?.appId || 'selected app'} />
|
||||
</div>
|
||||
|
||||
{!agentHealth && (
|
||||
{!isClientLinux && (
|
||||
<ClientOsNotice osLabel={clientOsLabel} />
|
||||
)}
|
||||
|
||||
{isClientLinux && !agentHealth && (
|
||||
<div className="offline-banner">
|
||||
<AlertCircle size={19} aria-hidden="true" />
|
||||
<div>
|
||||
@@ -636,7 +694,7 @@ function App() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{agentNeedsUpdate && (
|
||||
{isClientLinux && agentNeedsUpdate && (
|
||||
<div className="offline-banner agent-update-banner">
|
||||
<AlertCircle size={19} aria-hidden="true" />
|
||||
<div>
|
||||
@@ -733,7 +791,7 @@ function App() {
|
||||
<td>{app.packageCount || 0}</td>
|
||||
<td className="action-col">
|
||||
<div className="action-group">
|
||||
{!app.installed && (
|
||||
{isClientLinux && !app.installed && (
|
||||
<button
|
||||
className="btn btn-primary compact"
|
||||
type="button"
|
||||
@@ -747,7 +805,7 @@ function App() {
|
||||
Install
|
||||
</button>
|
||||
)}
|
||||
{app.installed && app.canUpdate && (
|
||||
{isClientLinux && app.installed && app.canUpdate && (
|
||||
<button
|
||||
className="btn btn-warning compact"
|
||||
type="button"
|
||||
@@ -761,7 +819,7 @@ function App() {
|
||||
Update
|
||||
</button>
|
||||
)}
|
||||
{app.installed && openUrl && (
|
||||
{isClientLinux && app.installed && openUrl && (
|
||||
<a
|
||||
className="btn btn-secondary compact"
|
||||
href={openUrl}
|
||||
@@ -774,7 +832,7 @@ function App() {
|
||||
Open App
|
||||
</a>
|
||||
)}
|
||||
{app.installed && (
|
||||
{isClientLinux && app.installed && (
|
||||
<button
|
||||
className="icon-button danger"
|
||||
type="button"
|
||||
@@ -810,6 +868,7 @@ function App() {
|
||||
latestAgentPackage={latestAgentPackage}
|
||||
needsUpdate={agentNeedsUpdate}
|
||||
onCopyUpdate={copyInstallCommand}
|
||||
showAgentActions={isClientLinux}
|
||||
/>
|
||||
{activeTask && (
|
||||
<TaskPanel
|
||||
@@ -931,6 +990,20 @@ function MetricCard({ label, value, note, tone }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ClientOsNotice({ osLabel }) {
|
||||
return (
|
||||
<div className="offline-banner client-os-banner">
|
||||
<AlertCircle size={19} aria-hidden="true" />
|
||||
<div>
|
||||
<strong>Web Client chỉ hỗ trợ máy Linux</strong>
|
||||
<p>
|
||||
Máy hiện tại được nhận diện là {osLabel}. Lệnh cài Agent và các thao tác cài đặt đã được ẩn.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LocalStatus({ app, task }) {
|
||||
if (isTaskActive(task)) {
|
||||
return (
|
||||
@@ -1053,7 +1126,7 @@ function TaskPanel({ task, onClear, onRefresh }) {
|
||||
);
|
||||
}
|
||||
|
||||
function AgentPanel({ health, systemInfo, status, latestAgentPackage, needsUpdate, onCopyUpdate }) {
|
||||
function AgentPanel({ health, systemInfo, status, latestAgentPackage, needsUpdate, onCopyUpdate, showAgentActions }) {
|
||||
return (
|
||||
<section className="panel">
|
||||
<div className="panel-header">
|
||||
@@ -1094,7 +1167,7 @@ function AgentPanel({ health, systemInfo, status, latestAgentPackage, needsUpdat
|
||||
<span><Cpu size={14} aria-hidden="true" /> {systemInfo?.kernel || 'kernel -'}</span>
|
||||
<span><HardDrive size={14} aria-hidden="true" /> {systemInfo?.diskFree || 'disk -'}</span>
|
||||
</div>
|
||||
{needsUpdate && (
|
||||
{needsUpdate && showAgentActions && (
|
||||
<div className="agent-update-action">
|
||||
<button className="btn btn-warning" type="button" onClick={onCopyUpdate}>
|
||||
<Clipboard size={15} aria-hidden="true" />
|
||||
|
||||
@@ -545,12 +545,25 @@ code {
|
||||
border-color: rgba(181, 71, 8, 0.35);
|
||||
}
|
||||
|
||||
.client-os-banner {
|
||||
background: #fffbeb;
|
||||
border-color: rgba(181, 71, 8, 0.35);
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.offline-banner strong {
|
||||
color: #111827;
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.client-os-banner p {
|
||||
color: var(--on-surface-variant);
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.workbench-grid {
|
||||
display: grid;
|
||||
flex: 1;
|
||||
|
||||
Reference in New Issue
Block a user