feat: Services UI — nested nav, per-service pages, settings migration
Rename Store → Services: ServicesIndex.jsx shows built-in core services (Email, Calendar, Files) with Manage links, plus the existing add-on store below. New service sub-pages at /services/email|calendar|files serve both admin and peer roles. Admins see connection info, service status, users list, and an inline config form (port/data-dir). Peers see connection info and their personal credentials fetched from peerAPI. Navigation restructured: a Services parent item expands to show the three sub-pages via a collapsible sidebar group (ChevronDown toggle). Both admin and peer navigation include the Services group. Sidebar extracted NavItem/NavList components to eliminate the duplicate mobile/ desktop rendering. Settings.jsx drops EmailForm, CalendarForm, FilesForm and their SERVICE_DEFS entries. Port conflict detection and per-service validation logic extracted to utils/serviceConfig.js, shared by Settings and the new service pages. Service form flushers are registered without cleanup so the Apply banner saves dirty config even when the user navigates away from a service page before clicking Apply. Legacy routes /email, /calendar, /files, /store redirect to their new canonical paths. GET /api/config now includes installed_services so the nav can derive which add-ons are installed without a separate store fetch. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+38
-28
@@ -1,4 +1,4 @@
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Home,
|
||||
@@ -30,9 +30,6 @@ import Dashboard from './pages/Dashboard';
|
||||
import Peers from './pages/Peers';
|
||||
import NetworkServices from './pages/NetworkServices';
|
||||
import WireGuard from './pages/WireGuard';
|
||||
import Email from './pages/Email';
|
||||
import Calendar from './pages/Calendar';
|
||||
import Files from './pages/Files';
|
||||
import Routing from './pages/Routing';
|
||||
import Logs from './pages/Logs';
|
||||
import Settings from './pages/Settings';
|
||||
@@ -43,7 +40,10 @@ import Login from './pages/Login';
|
||||
import AccountSettings from './pages/AccountSettings';
|
||||
import PeerDashboard from './pages/PeerDashboard';
|
||||
import MyServices from './pages/MyServices';
|
||||
import Store from './pages/Store';
|
||||
import ServicesIndex from './pages/ServicesIndex';
|
||||
import EmailPage from './pages/services/EmailPage';
|
||||
import CalendarPage from './pages/services/CalendarPage';
|
||||
import FilesPage from './pages/services/FilesPage';
|
||||
import Connectivity from './pages/Connectivity';
|
||||
import Setup from './pages/Setup';
|
||||
import SetupGuard from './components/SetupGuard';
|
||||
@@ -230,29 +230,33 @@ 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 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: 'Email', href: '/email', icon: Mail },
|
||||
{ name: 'Calendar', href: '/calendar', icon: CalendarIcon },
|
||||
{ name: 'Files', href: '/files', icon: FolderOpen },
|
||||
{ name: 'Routing', href: '/routing', icon: Wifi },
|
||||
{ name: 'Vault', href: '/vault', icon: Key },
|
||||
{ name: 'Containers', href: '/containers', icon: Package2 },
|
||||
{ name: 'Store', href: '/store', icon: Package },
|
||||
{ name: 'Cell Network', href: '/cell-network', icon: Link2 },
|
||||
{ name: 'Connectivity', href: '/connectivity', icon: Network },
|
||||
{ name: 'Logs', href: '/logs', icon: Activity },
|
||||
{ name: 'Settings', href: '/settings', icon: SettingsIcon },
|
||||
{ name: 'Account', href: '/account', icon: User },
|
||||
{ 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: 'Routing', href: '/routing', icon: Wifi },
|
||||
{ name: 'Vault', href: '/vault', icon: Key },
|
||||
{ name: 'Containers', href: '/containers', icon: Package2 },
|
||||
{ name: 'Cell Network', href: '/cell-network', icon: Link2 },
|
||||
{ name: 'Connectivity', href: '/connectivity', icon: Network },
|
||||
{ name: 'Logs', href: '/logs', icon: Activity },
|
||||
{ name: 'Settings', href: '/settings', icon: SettingsIcon },
|
||||
{ name: 'Account', href: '/account', icon: User },
|
||||
];
|
||||
|
||||
const peerNavigation = [
|
||||
{ name: 'Dashboard', href: '/', icon: Home },
|
||||
{ name: 'My Services', href: '/my-services', icon: FolderOpen },
|
||||
{ name: 'Account', href: '/account', icon: User },
|
||||
{ name: 'Dashboard', href: '/', icon: Home },
|
||||
{ name: 'My Services', href: '/my-services', icon: Wifi },
|
||||
{ name: 'Services', href: '/services', icon: Package, children: coreServiceChildren },
|
||||
{ name: 'Account', href: '/account', icon: User },
|
||||
];
|
||||
|
||||
const { user } = useAuth();
|
||||
@@ -339,16 +343,22 @@ function AppCore() {
|
||||
<Route path="/" element={<PrivateRoute><RoleHome isOnline={isOnline} /></PrivateRoute>} />
|
||||
<Route path="/account" element={<PrivateRoute><AccountSettings /></PrivateRoute>} />
|
||||
<Route path="/my-services" element={<PrivateRoute requireRole="peer"><MyServices /></PrivateRoute>} />
|
||||
{/* Service pages — accessible to both admin and peer (role-conditional content inside) */}
|
||||
<Route path="/services" element={<PrivateRoute requireRole="admin"><ServicesIndex /></PrivateRoute>} />
|
||||
<Route path="/services/email" element={<PrivateRoute><EmailPage /></PrivateRoute>} />
|
||||
<Route path="/services/calendar" element={<PrivateRoute><CalendarPage /></PrivateRoute>} />
|
||||
<Route path="/services/files" element={<PrivateRoute><FilesPage /></PrivateRoute>} />
|
||||
{/* Legacy URL redirects */}
|
||||
<Route path="/email" element={<Navigate to="/services/email" replace />} />
|
||||
<Route path="/calendar" element={<Navigate to="/services/calendar" replace />} />
|
||||
<Route path="/files" element={<Navigate to="/services/files" replace />} />
|
||||
<Route path="/store" element={<Navigate to="/services" replace />} />
|
||||
<Route path="/peers" element={<PrivateRoute requireRole="admin"><Peers /></PrivateRoute>} />
|
||||
<Route path="/network" element={<PrivateRoute requireRole="admin"><NetworkServices /></PrivateRoute>} />
|
||||
<Route path="/wireguard" element={<PrivateRoute requireRole="admin"><WireGuard /></PrivateRoute>} />
|
||||
<Route path="/email" element={<PrivateRoute requireRole="admin"><Email /></PrivateRoute>} />
|
||||
<Route path="/calendar" element={<PrivateRoute requireRole="admin"><Calendar /></PrivateRoute>} />
|
||||
<Route path="/files" element={<PrivateRoute requireRole="admin"><Files /></PrivateRoute>} />
|
||||
<Route path="/routing" element={<PrivateRoute requireRole="admin"><Routing /></PrivateRoute>} />
|
||||
<Route path="/vault" element={<PrivateRoute requireRole="admin"><Vault /></PrivateRoute>} />
|
||||
<Route path="/containers" element={<PrivateRoute requireRole="admin"><ContainerDashboard /></PrivateRoute>} />
|
||||
<Route path="/store" element={<PrivateRoute requireRole="admin"><Store /></PrivateRoute>} />
|
||||
<Route path="/cell-network" element={<PrivateRoute requireRole="admin"><CellNetwork /></PrivateRoute>} />
|
||||
<Route path="/connectivity" element={<PrivateRoute requireRole="admin"><Connectivity /></PrivateRoute>} />
|
||||
<Route path="/logs" element={<PrivateRoute requireRole="admin"><Logs /></PrivateRoute>} />
|
||||
|
||||
Reference in New Issue
Block a user