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);
|
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() {
|
function App() {
|
||||||
const [settings, setSettings] = useState(readSettings);
|
const [settings, setSettings] = useState(readSettings);
|
||||||
const [draftSettings, setDraftSettings] = useState(settings);
|
const [draftSettings, setDraftSettings] = useState(settings);
|
||||||
@@ -166,6 +194,9 @@ function App() {
|
|||||||
const agentBaseUrl = settings.agentBaseUrl;
|
const agentBaseUrl = settings.agentBaseUrl;
|
||||||
const installCommand = `curl -fsSL ${joinUrl(packageBaseUrl, '/install-agent.sh')} | sudo bash`;
|
const installCommand = `curl -fsSL ${joinUrl(packageBaseUrl, '/install-agent.sh')} | sudo bash`;
|
||||||
const agentCommand = latestAgentPackage?.installCommand || installCommand;
|
const agentCommand = latestAgentPackage?.installCommand || installCommand;
|
||||||
|
const clientOs = useMemo(() => detectClientOs(), []);
|
||||||
|
const isClientLinux = clientOs === 'linux';
|
||||||
|
const clientOsLabel = getClientOsLabel(clientOs);
|
||||||
|
|
||||||
const installedByAppId = useMemo(() => {
|
const installedByAppId = useMemo(() => {
|
||||||
return new Map(installedApps.map((app) => [app.appId, app]));
|
return new Map(installedApps.map((app) => [app.appId, app]));
|
||||||
@@ -251,6 +282,17 @@ function App() {
|
|||||||
}, [packageBaseUrl]);
|
}, [packageBaseUrl]);
|
||||||
|
|
||||||
const refreshAgent = useCallback(async () => {
|
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' });
|
setAgentStatus({ state: 'loading', message: 'Đang kiểm tra Agent local' });
|
||||||
try {
|
try {
|
||||||
const health = await fetchAgentHealth(agentBaseUrl);
|
const health = await fetchAgentHealth(agentBaseUrl);
|
||||||
@@ -271,7 +313,7 @@ function App() {
|
|||||||
setAgentStatus({ state: 'danger', message: getErrorMessage(error) });
|
setAgentStatus({ state: 'danger', message: getErrorMessage(error) });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, [agentBaseUrl]);
|
}, [agentBaseUrl, clientOsLabel, isClientLinux]);
|
||||||
|
|
||||||
const refreshAll = useCallback(async () => {
|
const refreshAll = useCallback(async () => {
|
||||||
await Promise.all([refreshPackage(), refreshAgent()]);
|
await Promise.all([refreshPackage(), refreshAgent()]);
|
||||||
@@ -385,6 +427,11 @@ function App() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const runAppAction = useCallback(async (action, 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) {
|
if (!agentHealth) {
|
||||||
notify('warning', 'Agent local đang offline. Cài Agent rồi bấm Retry.');
|
notify('warning', 'Agent local đang offline. Cài Agent rồi bấm Retry.');
|
||||||
return;
|
return;
|
||||||
@@ -425,7 +472,7 @@ function App() {
|
|||||||
} finally {
|
} finally {
|
||||||
setBusyAction('');
|
setBusyAction('');
|
||||||
}
|
}
|
||||||
}, [agentBaseUrl, agentHealth, notify, packageBaseUrl, startPreflightFailedTask, startTask]);
|
}, [agentBaseUrl, agentHealth, isClientLinux, notify, packageBaseUrl, startPreflightFailedTask, startTask]);
|
||||||
|
|
||||||
const openEndpointDialog = useCallback(() => {
|
const openEndpointDialog = useCallback(() => {
|
||||||
setDraftSettings(settings);
|
setDraftSettings(settings);
|
||||||
@@ -450,6 +497,11 @@ function App() {
|
|||||||
}, [draftSettings, notify]);
|
}, [draftSettings, notify]);
|
||||||
|
|
||||||
const copyInstallCommand = useCallback(async () => {
|
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 (!navigator.clipboard?.writeText) {
|
||||||
if (copyTextFallback(agentCommand)) {
|
if (copyTextFallback(agentCommand)) {
|
||||||
notify('success', agentNeedsUpdate ? 'Da copy lenh update Agent' : 'Da copy lenh cai Agent');
|
notify('success', agentNeedsUpdate ? 'Da copy lenh update Agent' : 'Da copy lenh cai Agent');
|
||||||
@@ -467,7 +519,7 @@ function App() {
|
|||||||
} catch {
|
} catch {
|
||||||
notify('warning', 'Không thể copy tự động trong browser này');
|
notify('warning', 'Không thể copy tự động trong browser này');
|
||||||
}
|
}
|
||||||
}, [agentCommand, agentNeedsUpdate, notify]);
|
}, [agentCommand, agentNeedsUpdate, isClientLinux, notify]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshAll();
|
refreshAll();
|
||||||
@@ -551,9 +603,9 @@ function App() {
|
|||||||
<div className="nav-label">Runtime</div>
|
<div className="nav-label">Runtime</div>
|
||||||
<StatusBox
|
<StatusBox
|
||||||
icon={agentHealth ? PlugZap : WifiOff}
|
icon={agentHealth ? PlugZap : WifiOff}
|
||||||
title={agentNeedsUpdate ? 'Agent update available' : (agentHealth ? 'Agent online' : 'Agent offline')}
|
title={!isClientLinux ? 'Linux client required' : (agentNeedsUpdate ? 'Agent update available' : (agentHealth ? 'Agent online' : 'Agent offline'))}
|
||||||
detail={agentHealth ? `${agentHealth.hostname || 'localhost'} · ${agentHealth.agentVersion || '-'}${agentNeedsUpdate ? ` -> ${latestAgentPackage.version}` : ''}` : '127.0.0.1:5010'}
|
detail={!isClientLinux ? `${clientOsLabel} detected` : (agentHealth ? `${agentHealth.hostname || 'localhost'} · ${agentHealth.agentVersion || '-'}${agentNeedsUpdate ? ` -> ${latestAgentPackage.version}` : ''}` : '127.0.0.1:5010')}
|
||||||
tone={agentNeedsUpdate ? 'warning' : (agentHealth ? 'success' : 'danger')}
|
tone={!isClientLinux ? 'warning' : (agentNeedsUpdate ? 'warning' : (agentHealth ? 'success' : 'danger'))}
|
||||||
/>
|
/>
|
||||||
<StatusBox
|
<StatusBox
|
||||||
icon={Server}
|
icon={Server}
|
||||||
@@ -584,7 +636,7 @@ function App() {
|
|||||||
<header className="topbar">
|
<header className="topbar">
|
||||||
<div className="topbar-title">
|
<div className="topbar-title">
|
||||||
<span>robot.installer</span>
|
<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>
|
||||||
<div className="topbar-actions">
|
<div className="topbar-actions">
|
||||||
<a className="icon-button" href={joinUrl(packageBaseUrl, '/api/apps')} target="_blank" rel="noreferrer" title="Open package API">
|
<a className="icon-button" href={joinUrl(packageBaseUrl, '/api/apps')} target="_blank" rel="noreferrer" title="Open package API">
|
||||||
@@ -603,6 +655,7 @@ function App() {
|
|||||||
<h1>Application catalog</h1>
|
<h1>Application catalog</h1>
|
||||||
<p>Released apps từ package server và trạng thái cài đặt trên máy local.</p>
|
<p>Released apps từ package server và trạng thái cài đặt trên máy local.</p>
|
||||||
</div>
|
</div>
|
||||||
|
{isClientLinux && (
|
||||||
<div className="page-actions">
|
<div className="page-actions">
|
||||||
<button className="btn btn-secondary" type="button" onClick={copyInstallCommand}>
|
<button className="btn btn-secondary" type="button" onClick={copyInstallCommand}>
|
||||||
<Clipboard size={15} aria-hidden="true" />
|
<Clipboard size={15} aria-hidden="true" />
|
||||||
@@ -613,6 +666,7 @@ function App() {
|
|||||||
{agentNeedsUpdate ? 'update-agent.sh' : 'install-agent.sh'}
|
{agentNeedsUpdate ? 'update-agent.sh' : 'install-agent.sh'}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="dashboard-stats">
|
<div className="dashboard-stats">
|
||||||
@@ -622,7 +676,11 @@ function App() {
|
|||||||
<MetricCard label="Components" value={stats.components} note={selectedApp?.appCode || selectedApp?.appId || 'selected app'} />
|
<MetricCard label="Components" value={stats.components} note={selectedApp?.appCode || selectedApp?.appId || 'selected app'} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!agentHealth && (
|
{!isClientLinux && (
|
||||||
|
<ClientOsNotice osLabel={clientOsLabel} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isClientLinux && !agentHealth && (
|
||||||
<div className="offline-banner">
|
<div className="offline-banner">
|
||||||
<AlertCircle size={19} aria-hidden="true" />
|
<AlertCircle size={19} aria-hidden="true" />
|
||||||
<div>
|
<div>
|
||||||
@@ -636,7 +694,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{agentNeedsUpdate && (
|
{isClientLinux && agentNeedsUpdate && (
|
||||||
<div className="offline-banner agent-update-banner">
|
<div className="offline-banner agent-update-banner">
|
||||||
<AlertCircle size={19} aria-hidden="true" />
|
<AlertCircle size={19} aria-hidden="true" />
|
||||||
<div>
|
<div>
|
||||||
@@ -733,7 +791,7 @@ function App() {
|
|||||||
<td>{app.packageCount || 0}</td>
|
<td>{app.packageCount || 0}</td>
|
||||||
<td className="action-col">
|
<td className="action-col">
|
||||||
<div className="action-group">
|
<div className="action-group">
|
||||||
{!app.installed && (
|
{isClientLinux && !app.installed && (
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary compact"
|
className="btn btn-primary compact"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -747,7 +805,7 @@ function App() {
|
|||||||
Install
|
Install
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{app.installed && app.canUpdate && (
|
{isClientLinux && app.installed && app.canUpdate && (
|
||||||
<button
|
<button
|
||||||
className="btn btn-warning compact"
|
className="btn btn-warning compact"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -761,7 +819,7 @@ function App() {
|
|||||||
Update
|
Update
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{app.installed && openUrl && (
|
{isClientLinux && app.installed && openUrl && (
|
||||||
<a
|
<a
|
||||||
className="btn btn-secondary compact"
|
className="btn btn-secondary compact"
|
||||||
href={openUrl}
|
href={openUrl}
|
||||||
@@ -774,7 +832,7 @@ function App() {
|
|||||||
Open App
|
Open App
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{app.installed && (
|
{isClientLinux && app.installed && (
|
||||||
<button
|
<button
|
||||||
className="icon-button danger"
|
className="icon-button danger"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -810,6 +868,7 @@ function App() {
|
|||||||
latestAgentPackage={latestAgentPackage}
|
latestAgentPackage={latestAgentPackage}
|
||||||
needsUpdate={agentNeedsUpdate}
|
needsUpdate={agentNeedsUpdate}
|
||||||
onCopyUpdate={copyInstallCommand}
|
onCopyUpdate={copyInstallCommand}
|
||||||
|
showAgentActions={isClientLinux}
|
||||||
/>
|
/>
|
||||||
{activeTask && (
|
{activeTask && (
|
||||||
<TaskPanel
|
<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 }) {
|
function LocalStatus({ app, task }) {
|
||||||
if (isTaskActive(task)) {
|
if (isTaskActive(task)) {
|
||||||
return (
|
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 (
|
return (
|
||||||
<section className="panel">
|
<section className="panel">
|
||||||
<div className="panel-header">
|
<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><Cpu size={14} aria-hidden="true" /> {systemInfo?.kernel || 'kernel -'}</span>
|
||||||
<span><HardDrive size={14} aria-hidden="true" /> {systemInfo?.diskFree || 'disk -'}</span>
|
<span><HardDrive size={14} aria-hidden="true" /> {systemInfo?.diskFree || 'disk -'}</span>
|
||||||
</div>
|
</div>
|
||||||
{needsUpdate && (
|
{needsUpdate && showAgentActions && (
|
||||||
<div className="agent-update-action">
|
<div className="agent-update-action">
|
||||||
<button className="btn btn-warning" type="button" onClick={onCopyUpdate}>
|
<button className="btn btn-warning" type="button" onClick={onCopyUpdate}>
|
||||||
<Clipboard size={15} aria-hidden="true" />
|
<Clipboard size={15} aria-hidden="true" />
|
||||||
|
|||||||
@@ -545,12 +545,25 @@ code {
|
|||||||
border-color: rgba(181, 71, 8, 0.35);
|
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 {
|
.offline-banner strong {
|
||||||
color: #111827;
|
color: #111827;
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.client-os-banner p {
|
||||||
|
color: var(--on-surface-variant);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.45;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.workbench-grid {
|
.workbench-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user