feat: Phase 4 — dynamic nav + service visibility based on installed services
Unit Tests / test (push) Successful in 11m24s
Unit Tests / test (push) Successful in 11m24s
Email, calendar, and files no longer appear in the nav or as usable pages unless they are installed. The nav refreshes whenever a service is installed or removed via the new pic-services-changed CustomEvent. Changes: - routes/services.py: add GET /api/services/active endpoint - api.js: add servicesAPI.listActive() - App.jsx: replace hardcoded coreServiceChildren with dynamic state fetched from /api/services/active; SERVICE_META maps ids to nav entry shapes - ServiceNotInstalledBanner.jsx: new component — admin gets catalog link, peer gets "contact admin" message - EmailPage/CalendarPage/FilesPage: show banner when service not installed - ServicesIndex.jsx: remove CoreServiceCard + CORE_SERVICES "Built-in" section; rename Remove → Uninstall; dispatch pic-services-changed on install/uninstall success - MyServices.jsx: conditionally render service cards based on active list; placeholder card when absent; page-level notice when nothing is installed - tests/test_services_active_endpoint.py: 4 new endpoint tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+28
-8
@@ -20,7 +20,7 @@ import {
|
||||
AlertTriangle,
|
||||
User,
|
||||
} from 'lucide-react';
|
||||
import { healthAPI, cellAPI } from './services/api';
|
||||
import { healthAPI, cellAPI, servicesAPI } from './services/api';
|
||||
import { ConfigProvider } from './contexts/ConfigContext';
|
||||
import { DraftConfigProvider, useDraftConfig } from './contexts/DraftConfigContext';
|
||||
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
||||
@@ -48,6 +48,12 @@ import Connectivity from './pages/Connectivity';
|
||||
import Setup from './pages/Setup';
|
||||
import SetupGuard from './components/SetupGuard';
|
||||
|
||||
const SERVICE_META = {
|
||||
email: { name: 'Email', href: '/services/email', icon: Mail },
|
||||
calendar: { name: 'Calendar', href: '/services/calendar', icon: CalendarIcon },
|
||||
files: { name: 'File Storage', href: '/services/files', icon: FolderOpen },
|
||||
};
|
||||
|
||||
function PendingRestartBanner({ pending, onApply, onCancel }) {
|
||||
const [confirming, setConfirming] = useState(false);
|
||||
const [applying, setApplying] = useState(false);
|
||||
@@ -230,18 +236,32 @@ function AppCore() {
|
||||
window.dispatchEvent(new CustomEvent('pic-config-discarded'));
|
||||
}, []);
|
||||
|
||||
const coreServiceChildren = [
|
||||
{ name: 'Email', href: '/services/email', icon: Mail },
|
||||
{ name: 'Calendar', href: '/services/calendar', icon: CalendarIcon },
|
||||
{ name: 'Files', href: '/services/files', icon: FolderOpen },
|
||||
];
|
||||
const [activeServiceChildren, setActiveServiceChildren] = useState([]);
|
||||
|
||||
const fetchActiveServices = useCallback(async () => {
|
||||
try {
|
||||
const resp = await servicesAPI.listActive();
|
||||
const children = (resp.data || [])
|
||||
.filter(svc => SERVICE_META[svc.id])
|
||||
.map(svc => SERVICE_META[svc.id]);
|
||||
setActiveServiceChildren(children);
|
||||
} catch {
|
||||
// silent — empty nav children is safe
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchActiveServices();
|
||||
window.addEventListener('pic-services-changed', fetchActiveServices);
|
||||
return () => window.removeEventListener('pic-services-changed', fetchActiveServices);
|
||||
}, [fetchActiveServices]);
|
||||
|
||||
const adminNavigation = [
|
||||
{ name: 'Dashboard', href: '/', icon: Home },
|
||||
{ name: 'Peers', href: '/peers', icon: Users },
|
||||
{ name: 'Network Services', href: '/network', icon: Network },
|
||||
{ name: 'WireGuard', href: '/wireguard', icon: Shield },
|
||||
{ name: 'Services', href: '/services', icon: Package, children: coreServiceChildren },
|
||||
{ name: 'Services', href: '/services', icon: Package, children: activeServiceChildren },
|
||||
{ name: 'Routing', href: '/routing', icon: Wifi },
|
||||
{ name: 'Vault', href: '/vault', icon: Key },
|
||||
{ name: 'Containers', href: '/containers', icon: Package2 },
|
||||
@@ -255,7 +275,7 @@ function AppCore() {
|
||||
const peerNavigation = [
|
||||
{ name: 'Dashboard', href: '/', icon: Home },
|
||||
{ name: 'My Services', href: '/my-services', icon: Wifi },
|
||||
{ name: 'Services', href: '/services', icon: Package, children: coreServiceChildren },
|
||||
{ name: 'Services', href: '/services', icon: Package, children: activeServiceChildren },
|
||||
{ name: 'Account', href: '/account', icon: User },
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user