feat(ui): implement modern NaxOS Web Dashboard with Perses analytics and ZFS migration wizard
CI & Build NaxOS Web Dashboard / build (push) Failing after 1m29s

This commit is contained in:
Lukas Holzner
2026-09-03 23:59:06 +02:00
parent 155beec162
commit eaad4719eb
27 changed files with 5580 additions and 2 deletions
+246
View File
@@ -0,0 +1,246 @@
import React, { useState, useEffect } from 'react';
import { Sidebar } from './components/Sidebar.js';
import { Topbar } from './components/Topbar.js';
import { RebuildModal } from './components/RebuildModal.js';
import { OverviewView } from './views/OverviewView.js';
import { StorageView } from './views/StorageView.js';
import { SharesView } from './views/SharesView.js';
import { AppsView } from './views/AppsView.js';
import { AnalyticsView } from './views/AnalyticsView.js';
import { GitOpsView } from './views/GitOpsView.js';
import { LogsView } from './views/LogsView.js';
import { WizardView } from './views/WizardView.js';
import { api } from './api/client.js';
import type {
SystemStatus,
ZfsPool,
ZfsDataset,
ZfsSnapshot,
UnimportedPool,
SmbShare,
NfsExport,
AppCatalogItem,
InstalledApp,
GitOpsStatus,
GitCommit,
LogEntry,
AppRuntime,
} from './types/index.js';
export const App: React.FC = () => {
const [currentTab, setCurrentTab] = useState('overview');
const [showRebuildModal, setShowRebuildModal] = useState(false);
// Application state
const [status, setStatus] = useState<SystemStatus | null>(null);
const [pools, setPools] = useState<ZfsPool[]>([]);
const [datasets, setDatasets] = useState<ZfsDataset[]>([]);
const [snapshots, setSnapshots] = useState<ZfsSnapshot[]>([]);
const [unimportedPools, setUnimportedPools] = useState<UnimportedPool[]>([]);
const [smbShares, setSmbShares] = useState<Record<string, SmbShare>>({});
const [nfsExports, setNfsExports] = useState<NfsExport[]>([]);
const [catalog, setCatalog] = useState<AppCatalogItem[]>([]);
const [installedApps, setInstalledApps] = useState<InstalledApp[]>([]);
const [gitops, setGitops] = useState<GitOpsStatus | null>(null);
const [commits, setCommits] = useState<GitCommit[]>([]);
const [logs, setLogs] = useState<LogEntry[]>([]);
const [selectedLogUnit, setSelectedLogUnit] = useState('all');
const loadData = async () => {
try {
const [
statusRes,
poolsRes,
datasetsRes,
snapshotsRes,
unimportedRes,
sharesRes,
exportsRes,
catalogRes,
appsRes,
gitopsRes,
commitsRes,
logsRes,
] = await Promise.all([
api.getStatus(),
api.getPools(),
api.getDatasets(),
api.getSnapshots(),
api.getUnimportedPools(),
api.getSmbShares(),
api.getNfsExports(),
api.getAppCatalog(),
api.getInstalledApps(),
api.getGitOpsStatus(),
api.getGitCommits(),
api.getLogs(),
]);
if (statusRes) setStatus(statusRes.status);
if (poolsRes) setPools(poolsRes.pools);
if (datasetsRes) setDatasets(datasetsRes.datasets);
if (snapshotsRes) setSnapshots(snapshotsRes.snapshots);
if (unimportedRes) setUnimportedPools(unimportedRes.pools);
if (sharesRes) setSmbShares(sharesRes.shares);
if (exportsRes) setNfsExports(exportsRes.exports);
if (catalogRes) setCatalog(catalogRes.catalog);
if (appsRes) setInstalledApps(appsRes.apps);
if (gitopsRes) setGitops(gitopsRes.status);
if (commitsRes) setCommits(commitsRes.commits);
if (logsRes) setLogs(logsRes.logs);
} catch (err) {
console.error('Error loading NaxOS state:', err);
}
};
useEffect(() => {
loadData();
const interval = setInterval(loadData, 8000);
return () => clearInterval(interval);
}, []);
// Handlers
const handleImportPool = async (poolName: string) => {
await api.importPool({ poolName, force: true, noMount: true });
await loadData();
};
const handleScrubPool = async (poolName: string) => {
await api.scrubPool(poolName, 'start');
await loadData();
};
const handleCreateSnapshot = async (dataset: string, tag: string) => {
await api.createSnapshot(dataset, tag);
await loadData();
};
const handleRollbackSnapshot = async (snapshotName: string) => {
await api.rollbackSnapshot(snapshotName);
await loadData();
};
const handleSaveSmbShare = async (share: SmbShare) => {
await api.saveSmbShare(share);
await loadData();
};
const handleDeleteSmbShare = async (name: string) => {
await api.deleteSmbShare(name);
await loadData();
};
const handleInstallApp = async (appId: string, runtime: AppRuntime) => {
await api.installApp({ appId, runtime });
await loadData();
};
const handleSetAppStatus = async (appId: string, appStatus: 'running' | 'stopped') => {
await api.setAppStatus(appId, appStatus);
await loadData();
};
const handleUninstallApp = async (appId: string) => {
await api.uninstallApp(appId);
await loadData();
};
const handleRollbackGitOps = async (commitSha: string) => {
await api.rollbackCommit(commitSha);
await loadData();
};
const handleReboot = async () => {
if (confirm('Are you sure you want to reboot the NaxOS storage appliance?')) {
await api.reboot();
alert('Reboot signal sent. The appliance will restart momentarily.');
}
};
return (
<div className="flex h-screen w-screen overflow-hidden bg-slate-950 text-slate-100 font-sans">
<Sidebar currentTab={currentTab} onSelectTab={setCurrentTab} />
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
<Topbar
status={status}
gitops={gitops}
onTriggerRebuild={() => setShowRebuildModal(true)}
onReboot={handleReboot}
/>
<main className="flex-1 overflow-y-auto p-8">
{currentTab === 'overview' && (
<OverviewView
status={status}
pools={pools}
apps={installedApps}
onNavigate={setCurrentTab}
/>
)}
{currentTab === 'storage' && (
<StorageView
pools={pools}
datasets={datasets}
snapshots={snapshots}
unimportedPools={unimportedPools}
onRefresh={loadData}
onImportPool={handleImportPool}
onScrubPool={handleScrubPool}
onCreateSnapshot={handleCreateSnapshot}
onRollbackSnapshot={handleRollbackSnapshot}
/>
)}
{currentTab === 'shares' && (
<SharesView
smbShares={smbShares}
nfsExports={nfsExports}
onSaveSmbShare={handleSaveSmbShare}
onDeleteSmbShare={handleDeleteSmbShare}
/>
)}
{currentTab === 'apps' && (
<AppsView
catalog={catalog}
installedApps={installedApps}
onInstallApp={handleInstallApp}
onSetStatus={handleSetAppStatus}
onUninstall={handleUninstallApp}
/>
)}
{currentTab === 'analytics' && <AnalyticsView />}
{currentTab === 'gitops' && (
<GitOpsView
status={gitops}
commits={commits}
onRollback={handleRollbackGitOps}
onTriggerRebuild={() => setShowRebuildModal(true)}
/>
)}
{currentTab === 'logs' && (
<LogsView
logs={logs}
selectedUnit={selectedLogUnit}
onSelectUnit={setSelectedLogUnit}
/>
)}
{currentTab === 'wizard' && <WizardView />}
</main>
</div>
<RebuildModal
isOpen={showRebuildModal}
onClose={() => setShowRebuildModal(false)}
/>
</div>
);
};