272 lines
8.3 KiB
TypeScript
272 lines
8.3 KiB
TypeScript
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 { 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 getInitialTab = () => {
|
|
if (typeof window !== 'undefined') {
|
|
const hash = window.location.hash.replace('#', '');
|
|
if (hash) return hash;
|
|
const params = new URLSearchParams(window.location.search);
|
|
const tab = params.get('tab');
|
|
if (tab) return tab;
|
|
}
|
|
return 'overview';
|
|
};
|
|
|
|
const [currentTab, setCurrentTab] = useState(getInitialTab);
|
|
const [showRebuildModal, setShowRebuildModal] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const onHashChange = () => {
|
|
const hash = window.location.hash.replace('#', '');
|
|
if (hash) setCurrentTab(hash);
|
|
};
|
|
window.addEventListener('hashchange', onHashChange);
|
|
return () => window.removeEventListener('hashchange', onHashChange);
|
|
}, []);
|
|
|
|
const selectTab = (tab: string) => {
|
|
setCurrentTab(tab);
|
|
if (typeof window !== 'undefined') {
|
|
window.location.hash = tab;
|
|
}
|
|
};
|
|
|
|
// 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 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-cyan-950/20 via-slate-950 to-slate-950 text-slate-100 font-sans">
|
|
<Sidebar currentTab={currentTab === 'analytics' ? 'overview' : currentTab} onSelectTab={selectTab} />
|
|
|
|
<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' || currentTab === 'analytics') && (
|
|
<OverviewView
|
|
status={status}
|
|
pools={pools}
|
|
apps={installedApps}
|
|
onNavigate={selectTab}
|
|
/>
|
|
)}
|
|
|
|
{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 === '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)}
|
|
onComplete={loadData}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|