re-org tenant settings

This commit is contained in:
Oliver Kieselbach
2026-03-23 04:53:17 +01:00
parent 2a43a72907
commit 4a2b0e8ad9
41 changed files with 1972 additions and 1140 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,30 @@
"use client";
import { useTenantConfig } from "./TenantConfigContext";
export function TenantNotifications() {
const { error, successMessage } = useTenantConfig();
if (!error && !successMessage) return null;
return (
<div className="space-y-2 mb-6">
{successMessage && (
<div className="bg-green-50 border border-green-200 rounded-lg p-4 flex items-center space-x-3">
<svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="text-green-800 font-medium">{successMessage}</span>
</div>
)}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center space-x-3">
<svg className="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="text-red-800">{error}</span>
</div>
)}
</div>
);
}
@@ -0,0 +1,46 @@
"use client";
import { usePageSections } from "../../../hooks/usePageSections";
import { PageSectionItem } from "../../../contexts/SidebarContext";
import { ACCESS_NAV_SECTIONS } from "./accessNavSections";
import { UsersIcon, KeyIcon } from "../../../lib/sidebarIcons";
import { useTenantConfig } from "../TenantConfigContext";
const SECTION_ICONS: Record<string, React.ReactNode> = {
"admin-management": <UsersIcon />,
"bootstrap-sessions": <KeyIcon />,
};
export function AccessSidebar({ children }: { children: React.ReactNode }) {
const { config, user } = useTenantConfig();
// Filter: hide bootstrap-sessions if not enabled; show admin-management only for admins
const visibleSections = ACCESS_NAV_SECTIONS.filter((s) => {
if (s.id === "bootstrap-sessions" && !config?.bootstrapTokenEnabled) return false;
if (s.id === "admin-management" && !user?.isTenantAdmin && !user?.isGalacticAdmin) return false;
return true;
});
const accessItems: PageSectionItem[] = visibleSections.map((s) => ({
id: s.id,
label: s.label,
icon: SECTION_ICONS[s.id],
href: `/settings/access/${s.id}`,
}));
usePageSections(accessItems, "Access", "route");
return (
<>
<header className="bg-white shadow">
<div className="py-6 px-4 sm:px-6 lg:px-8">
<h1 className="text-2xl font-normal text-gray-900">Tenant Configuration</h1>
<p className="text-sm text-gray-500 mt-1">Admin management and bootstrap sessions</p>
</div>
</header>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
{children}
</div>
</>
);
}
@@ -0,0 +1,20 @@
"use client";
import { useParams } from "next/navigation";
import { notFound } from "next/navigation";
import { type AccessSectionId } from "../accessNavSections";
import { SectionAdminManagement } from "../sections/SectionAdminManagement";
import { SectionBootstrapSessions } from "../sections/SectionBootstrapSessions";
const SECTION_COMPONENTS: Record<AccessSectionId, React.ComponentType> = {
"admin-management": SectionAdminManagement,
"bootstrap-sessions": SectionBootstrapSessions,
};
export default function AccessSectionPage() {
const params = useParams();
const section = params.section as string;
const SectionContent = SECTION_COMPONENTS[section as AccessSectionId];
if (!SectionContent) notFound();
return <SectionContent />;
}
@@ -0,0 +1,6 @@
export const ACCESS_NAV_SECTIONS = [
{ id: "admin-management", label: "Admin Management", description: "Manage tenant admins and operators" },
{ id: "bootstrap-sessions", label: "Bootstrap Sessions", description: "Create and manage bootstrap tokens" },
] as const;
export type AccessSectionId = (typeof ACCESS_NAV_SECTIONS)[number]["id"];
@@ -0,0 +1,5 @@
import { AccessSidebar } from "./AccessSidebar";
export default function AccessLayout({ children }: { children: React.ReactNode }) {
return <AccessSidebar>{children}</AccessSidebar>;
}
@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";
export default function AccessPage() {
redirect("/settings/access/admin-management");
}
@@ -0,0 +1,45 @@
"use client";
import { useTenantConfig } from "../../TenantConfigContext";
import { TenantNotifications } from "../../TenantNotifications";
import AdminManagementSection from "../../components/AdminManagementSection";
export function SectionAdminManagement() {
const {
admins, loadingAdmins,
newAdminEmail, setNewAdminEmail,
newMemberRole, setNewMemberRole,
addingAdmin, removingAdmin, togglingAdmin,
adminSearchQuery, setAdminSearchQuery,
currentAdminPage, setCurrentAdminPage,
user,
handleAddAdmin, handleRemoveAdmin,
handleToggleTenantAdmin, handleUpdatePermissions,
} = useTenantConfig();
return (
<>
<TenantNotifications />
<AdminManagementSection
admins={admins}
loadingAdmins={loadingAdmins}
newAdminEmail={newAdminEmail}
setNewAdminEmail={setNewAdminEmail}
newMemberRole={newMemberRole}
setNewMemberRole={setNewMemberRole}
addingAdmin={addingAdmin}
removingAdmin={removingAdmin}
togglingAdmin={togglingAdmin}
adminSearchQuery={adminSearchQuery}
setAdminSearchQuery={setAdminSearchQuery}
currentAdminPage={currentAdminPage}
setCurrentAdminPage={setCurrentAdminPage}
user={user}
onAddAdmin={handleAddAdmin}
onRemoveAdmin={handleRemoveAdmin}
onToggleAdmin={handleToggleTenantAdmin}
onUpdatePermissions={handleUpdatePermissions}
/>
</>
);
}
@@ -0,0 +1,34 @@
"use client";
import { useTenantConfig } from "../../TenantConfigContext";
import { TenantNotifications } from "../../TenantNotifications";
import BootstrapSessionsSection from "../../components/BootstrapSessionsSection";
export function SectionBootstrapSessions() {
const {
config,
bootstrapSessions, bootstrapLoading,
fetchBootstrapSessions, revokeBootstrapSession, createBootstrapSession,
} = useTenantConfig();
if (!config?.bootstrapTokenEnabled) {
return (
<div className="bg-white rounded-lg shadow p-8 text-center">
<p className="text-gray-500">Bootstrap Sessions are not available for this tenant.</p>
</div>
);
}
return (
<>
<TenantNotifications />
<BootstrapSessionsSection
sessions={bootstrapSessions}
loading={bootstrapLoading}
onRefresh={fetchBootstrapSessions}
onRevoke={revokeBootstrapSession}
onCreate={createBootstrapSession}
/>
</>
);
}
@@ -0,0 +1,45 @@
"use client";
import { usePageSections } from "../../../hooks/usePageSections";
import { PageSectionItem } from "../../../contexts/SidebarContext";
import { AGENT_NAV_SECTIONS } from "./agentNavSections";
import { GearIcon, MagnifyingGlassIcon, LockClosedIcon } from "../../../lib/sidebarIcons";
import { useTenantConfig } from "../TenantConfigContext";
const SECTION_ICONS: Record<string, React.ReactNode> = {
"settings": <GearIcon />,
"analyzers": <MagnifyingGlassIcon />,
"unrestricted-mode": <LockClosedIcon />,
};
export function AgentSidebar({ children }: { children: React.ReactNode }) {
const { config } = useTenantConfig();
// Filter out unrestricted-mode if not enabled
const visibleSections = AGENT_NAV_SECTIONS.filter(
(s) => s.id !== "unrestricted-mode" || config?.unrestrictedModeEnabled
);
const agentItems: PageSectionItem[] = visibleSections.map((s) => ({
id: s.id,
label: s.label,
icon: SECTION_ICONS[s.id],
href: `/settings/agent/${s.id}`,
}));
usePageSections(agentItems, "Agent", "route");
return (
<>
<header className="bg-white shadow">
<div className="py-6 px-4 sm:px-6 lg:px-8">
<h1 className="text-2xl font-normal text-gray-900">Tenant Configuration</h1>
<p className="text-sm text-gray-500 mt-1">Agent collector and behavior configuration</p>
</div>
</header>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
{children}
</div>
</>
);
}
@@ -0,0 +1,22 @@
"use client";
import { useParams } from "next/navigation";
import { notFound } from "next/navigation";
import { type AgentSectionId } from "../agentNavSections";
import { SectionAgentSettings } from "../sections/SectionAgentSettings";
import { SectionAgentAnalyzers } from "../sections/SectionAgentAnalyzers";
import { SectionUnrestrictedMode } from "../sections/SectionUnrestrictedMode";
const SECTION_COMPONENTS: Record<AgentSectionId, React.ComponentType> = {
"settings": SectionAgentSettings,
"analyzers": SectionAgentAnalyzers,
"unrestricted-mode": SectionUnrestrictedMode,
};
export default function AgentSectionPage() {
const params = useParams();
const section = params.section as string;
const SectionContent = SECTION_COMPONENTS[section as AgentSectionId];
if (!SectionContent) notFound();
return <SectionContent />;
}
@@ -0,0 +1,7 @@
export const AGENT_NAV_SECTIONS = [
{ id: "settings", label: "Agent Settings", description: "Agent collector and behavior configuration" },
{ id: "analyzers", label: "Agent Analyzers", description: "Local admin and software inventory analyzers" },
{ id: "unrestricted-mode", label: "Unrestricted Mode", description: "Unrestricted mode configuration" },
] as const;
export type AgentSectionId = (typeof AGENT_NAV_SECTIONS)[number]["id"];
@@ -0,0 +1,5 @@
import { AgentSidebar } from "./AgentSidebar";
export default function AgentLayout({ children }: { children: React.ReactNode }) {
return <AgentSidebar>{children}</AgentSidebar>;
}
@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";
export default function AgentPage() {
redirect("/settings/agent/settings");
}
@@ -0,0 +1,35 @@
"use client";
import { useTenantConfig } from "../../TenantConfigContext";
import { TenantNotifications } from "../../TenantNotifications";
import AgentAnalyzersSection from "../../components/AgentAnalyzersSection";
export function SectionAgentAnalyzers() {
const {
enableLocalAdminAnalyzer, setEnableLocalAdminAnalyzer,
localAdminAllowedAccounts, setLocalAdminAllowedAccounts,
newAllowedAccount, setNewAllowedAccount,
enableSoftwareInventoryAnalyzer, setEnableSoftwareInventoryAnalyzer,
handleSaveAgentAnalyzers, handleResetAgentAnalyzers,
savingSection,
} = useTenantConfig();
return (
<>
<TenantNotifications />
<AgentAnalyzersSection
enableLocalAdminAnalyzer={enableLocalAdminAnalyzer}
setEnableLocalAdminAnalyzer={setEnableLocalAdminAnalyzer}
localAdminAllowedAccounts={localAdminAllowedAccounts}
setLocalAdminAllowedAccounts={setLocalAdminAllowedAccounts}
newAllowedAccount={newAllowedAccount}
setNewAllowedAccount={setNewAllowedAccount}
enableSoftwareInventoryAnalyzer={enableSoftwareInventoryAnalyzer}
setEnableSoftwareInventoryAnalyzer={setEnableSoftwareInventoryAnalyzer}
onSave={handleSaveAgentAnalyzers}
onReset={handleResetAgentAnalyzers}
saving={savingSection === "agentAnalyzers"}
/>
</>
);
}
@@ -0,0 +1,68 @@
"use client";
import { useTenantConfig } from "../../TenantConfigContext";
import { TenantNotifications } from "../../TenantNotifications";
import AgentSettingsSection from "../../components/AgentSettingsSection";
export function SectionAgentSettings() {
const {
enablePerformanceCollector, setEnablePerformanceCollector,
performanceCollectorInterval, setPerformanceCollectorInterval,
helloWaitTimeoutSeconds, setHelloWaitTimeoutSeconds,
selfDestructOnComplete, setSelfDestructOnComplete,
keepLogFile, setKeepLogFile,
rebootOnComplete, setRebootOnComplete,
rebootDelaySeconds, setRebootDelaySeconds,
enableGeoLocation, setEnableGeoLocation,
enableImeMatchLog, setEnableImeMatchLog,
logLevel, setLogLevel,
showScriptOutput, setShowScriptOutput,
showEnrollmentSummary, setShowEnrollmentSummary,
enrollmentSummaryTimeoutSeconds, setEnrollmentSummaryTimeoutSeconds,
enrollmentSummaryBrandingImageUrl, setEnrollmentSummaryBrandingImageUrl,
enrollmentSummaryLaunchRetrySeconds, setEnrollmentSummaryLaunchRetrySeconds,
handleSaveAgentSettings, handleResetAgentSettings,
savingSection,
} = useTenantConfig();
return (
<>
<TenantNotifications />
<AgentSettingsSection
enablePerformanceCollector={enablePerformanceCollector}
setEnablePerformanceCollector={setEnablePerformanceCollector}
performanceCollectorInterval={performanceCollectorInterval}
setPerformanceCollectorInterval={setPerformanceCollectorInterval}
helloWaitTimeoutSeconds={helloWaitTimeoutSeconds}
setHelloWaitTimeoutSeconds={setHelloWaitTimeoutSeconds}
selfDestructOnComplete={selfDestructOnComplete}
setSelfDestructOnComplete={setSelfDestructOnComplete}
keepLogFile={keepLogFile}
setKeepLogFile={setKeepLogFile}
rebootOnComplete={rebootOnComplete}
setRebootOnComplete={setRebootOnComplete}
rebootDelaySeconds={rebootDelaySeconds}
setRebootDelaySeconds={setRebootDelaySeconds}
enableGeoLocation={enableGeoLocation}
setEnableGeoLocation={setEnableGeoLocation}
enableImeMatchLog={enableImeMatchLog}
setEnableImeMatchLog={setEnableImeMatchLog}
logLevel={logLevel}
setLogLevel={setLogLevel}
showScriptOutput={showScriptOutput}
setShowScriptOutput={setShowScriptOutput}
showEnrollmentSummary={showEnrollmentSummary}
setShowEnrollmentSummary={setShowEnrollmentSummary}
enrollmentSummaryTimeoutSeconds={enrollmentSummaryTimeoutSeconds}
setEnrollmentSummaryTimeoutSeconds={setEnrollmentSummaryTimeoutSeconds}
enrollmentSummaryBrandingImageUrl={enrollmentSummaryBrandingImageUrl}
setEnrollmentSummaryBrandingImageUrl={setEnrollmentSummaryBrandingImageUrl}
enrollmentSummaryLaunchRetrySeconds={enrollmentSummaryLaunchRetrySeconds}
setEnrollmentSummaryLaunchRetrySeconds={setEnrollmentSummaryLaunchRetrySeconds}
onSave={handleSaveAgentSettings}
onReset={handleResetAgentSettings}
saving={savingSection === "agentSettings"}
/>
</>
);
}
@@ -0,0 +1,34 @@
"use client";
import { useTenantConfig } from "../../TenantConfigContext";
import { TenantNotifications } from "../../TenantNotifications";
import UnrestrictedModeSection from "../../components/UnrestrictedModeSection";
export function SectionUnrestrictedMode() {
const {
config,
unrestrictedMode, setUnrestrictedMode,
handleSaveUnrestrictedMode,
savingSection,
} = useTenantConfig();
if (!config?.unrestrictedModeEnabled) {
return (
<div className="bg-white rounded-lg shadow p-8 text-center">
<p className="text-gray-500">Unrestricted Mode is not available for this tenant.</p>
</div>
);
}
return (
<>
<TenantNotifications />
<UnrestrictedModeSection
unrestrictedMode={unrestrictedMode}
setUnrestrictedMode={setUnrestrictedMode}
onSave={handleSaveUnrestrictedMode}
saving={savingSection === "unrestrictedMode"}
/>
</>
);
}
@@ -0,0 +1,36 @@
"use client";
import { usePageSections } from "../../../hooks/usePageSections";
import { PageSectionItem } from "../../../contexts/SidebarContext";
import { INTEGRATIONS_NAV_SECTIONS } from "./integrationsNavSections";
import { BellIcon, CloudArrowUpIcon } from "../../../lib/sidebarIcons";
const SECTION_ICONS: Record<string, React.ReactNode> = {
"notifications": <BellIcon />,
"diagnostics": <CloudArrowUpIcon />,
};
const integrationsItems: PageSectionItem[] = INTEGRATIONS_NAV_SECTIONS.map((s) => ({
id: s.id,
label: s.label,
icon: SECTION_ICONS[s.id],
href: `/settings/integrations/${s.id}`,
}));
export function IntegrationsSidebar({ children }: { children: React.ReactNode }) {
usePageSections(integrationsItems, "Integrations", "route");
return (
<>
<header className="bg-white shadow">
<div className="py-6 px-4 sm:px-6 lg:px-8">
<h1 className="text-2xl font-normal text-gray-900">Tenant Configuration</h1>
<p className="text-sm text-gray-500 mt-1">Webhook notifications and diagnostics upload</p>
</div>
</header>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
{children}
</div>
</>
);
}
@@ -0,0 +1,20 @@
"use client";
import { useParams } from "next/navigation";
import { notFound } from "next/navigation";
import { type IntegrationsSectionId } from "../integrationsNavSections";
import { SectionNotifications } from "../sections/SectionNotifications";
import { SectionDiagnostics } from "../sections/SectionDiagnostics";
const SECTION_COMPONENTS: Record<IntegrationsSectionId, React.ComponentType> = {
"notifications": SectionNotifications,
"diagnostics": SectionDiagnostics,
};
export default function IntegrationsSectionPage() {
const params = useParams();
const section = params.section as string;
const SectionContent = SECTION_COMPONENTS[section as IntegrationsSectionId];
if (!SectionContent) notFound();
return <SectionContent />;
}
@@ -0,0 +1,6 @@
export const INTEGRATIONS_NAV_SECTIONS = [
{ id: "notifications", label: "Notifications", description: "Webhook notification configuration" },
{ id: "diagnostics", label: "Diagnostics", description: "Diagnostics upload and log path configuration" },
] as const;
export type IntegrationsSectionId = (typeof INTEGRATIONS_NAV_SECTIONS)[number]["id"];
@@ -0,0 +1,5 @@
import { IntegrationsSidebar } from "./IntegrationsSidebar";
export default function IntegrationsLayout({ children }: { children: React.ReactNode }) {
return <IntegrationsSidebar>{children}</IntegrationsSidebar>;
}
@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";
export default function IntegrationsPage() {
redirect("/settings/integrations/notifications");
}
@@ -0,0 +1,42 @@
"use client";
import { useTenantConfig } from "../../TenantConfigContext";
import { TenantNotifications } from "../../TenantNotifications";
import DiagnosticsSection from "../../components/DiagnosticsSection";
export function SectionDiagnostics() {
const {
diagnosticsBlobSasUrl, setDiagnosticsBlobSasUrl,
diagnosticsUploadMode, setDiagnosticsUploadMode,
tenantDiagPaths, setTenantDiagPaths,
globalDiagPaths,
newDiagPath, setNewDiagPath,
newDiagDesc, setNewDiagDesc,
unrestrictedMode,
handleSaveDiagnostics, handleResetDiagnostics,
savingSection,
} = useTenantConfig();
return (
<>
<TenantNotifications />
<DiagnosticsSection
diagnosticsBlobSasUrl={diagnosticsBlobSasUrl}
setDiagnosticsBlobSasUrl={setDiagnosticsBlobSasUrl}
diagnosticsUploadMode={diagnosticsUploadMode}
setDiagnosticsUploadMode={setDiagnosticsUploadMode}
tenantDiagPaths={tenantDiagPaths}
setTenantDiagPaths={setTenantDiagPaths}
globalDiagPaths={globalDiagPaths}
newDiagPath={newDiagPath}
setNewDiagPath={setNewDiagPath}
newDiagDesc={newDiagDesc}
setNewDiagDesc={setNewDiagDesc}
unrestrictedMode={unrestrictedMode}
onSave={handleSaveDiagnostics}
onReset={handleResetDiagnostics}
saving={savingSection === "diagnostics"}
/>
</>
);
}
@@ -0,0 +1,39 @@
"use client";
import { useTenantConfig } from "../../TenantConfigContext";
import { TenantNotifications } from "../../TenantNotifications";
import NotificationsSection from "../../components/NotificationsSection";
export function SectionNotifications() {
const {
webhookProviderType, setWebhookProviderType,
webhookUrl, setWebhookUrl,
webhookNotifyOnSuccess, setWebhookNotifyOnSuccess,
webhookNotifyOnFailure, setWebhookNotifyOnFailure,
handleTestWebhook, testingWebhook, testWebhookResult,
handleSaveNotifications, handleResetNotifications,
savingSection,
} = useTenantConfig();
return (
<>
<TenantNotifications />
<NotificationsSection
webhookProviderType={webhookProviderType}
setWebhookProviderType={setWebhookProviderType}
webhookUrl={webhookUrl}
setWebhookUrl={setWebhookUrl}
webhookNotifyOnSuccess={webhookNotifyOnSuccess}
setWebhookNotifyOnSuccess={setWebhookNotifyOnSuccess}
webhookNotifyOnFailure={webhookNotifyOnFailure}
setWebhookNotifyOnFailure={setWebhookNotifyOnFailure}
onTestWebhook={handleTestWebhook}
testingWebhook={testingWebhook}
testWebhookResult={testWebhookResult}
onSave={handleSaveNotifications}
onReset={handleResetNotifications}
saving={savingSection === "notifications"}
/>
</>
);
}
@@ -0,0 +1,42 @@
"use client";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { ProtectedRoute } from "../../components/ProtectedRoute";
import { useAuth } from "../../contexts/AuthContext";
import { TenantConfigProvider } from "./TenantConfigContext";
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
const { user } = useAuth();
const router = useRouter();
useEffect(() => {
if (!user) return;
// Regular users (not admin, not operator) → redirect to progress portal
if (!user.isTenantAdmin && !user.isGalacticAdmin && user.role !== "Operator") {
router.replace("/progress");
return;
}
// Operator without bootstrap permission → no settings access
if (user.role === "Operator" && !user.isTenantAdmin && !user.isGalacticAdmin && !user.canManageBootstrapTokens) {
router.replace("/dashboard");
}
}, [user, router]);
// Don't render until we know user is allowed
if (!user) return null;
if (!user.isTenantAdmin && !user.isGalacticAdmin && user.role !== "Operator") return null;
if (user.role === "Operator" && !user.isTenantAdmin && !user.isGalacticAdmin && !user.canManageBootstrapTokens) return null;
return (
<ProtectedRoute>
<TenantConfigProvider>
<div className="min-h-screen bg-gray-50">
{children}
</div>
</TenantConfigProvider>
</ProtectedRoute>
);
}
@@ -0,0 +1,36 @@
"use client";
import { usePageSections } from "../../../hooks/usePageSections";
import { PageSectionItem } from "../../../contexts/SidebarContext";
import { MANAGEMENT_NAV_SECTIONS } from "./managementNavSections";
import { CircleStackIcon, ArrowRightOnRectangleIcon } from "../../../lib/sidebarIcons";
const SECTION_ICONS: Record<string, React.ReactNode> = {
"data": <CircleStackIcon />,
"offboarding": <ArrowRightOnRectangleIcon />,
};
const managementItems: PageSectionItem[] = MANAGEMENT_NAV_SECTIONS.map((s) => ({
id: s.id,
label: s.label,
icon: SECTION_ICONS[s.id],
href: `/settings/management/${s.id}`,
}));
export function ManagementSidebar({ children }: { children: React.ReactNode }) {
usePageSections(managementItems, "Management", "route");
return (
<>
<header className="bg-white shadow">
<div className="py-6 px-4 sm:px-6 lg:px-8">
<h1 className="text-2xl font-normal text-gray-900">Tenant Configuration</h1>
<p className="text-sm text-gray-500 mt-1">Data retention and tenant offboarding</p>
</div>
</header>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
{children}
</div>
</>
);
}
@@ -0,0 +1,20 @@
"use client";
import { useParams } from "next/navigation";
import { notFound } from "next/navigation";
import { type ManagementSectionId } from "../managementNavSections";
import { SectionDataManagement } from "../sections/SectionDataManagement";
import { SectionOffboarding } from "../sections/SectionOffboarding";
const SECTION_COMPONENTS: Record<ManagementSectionId, React.ComponentType> = {
"data": SectionDataManagement,
"offboarding": SectionOffboarding,
};
export default function ManagementSectionPage() {
const params = useParams();
const section = params.section as string;
const SectionContent = SECTION_COMPONENTS[section as ManagementSectionId];
if (!SectionContent) notFound();
return <SectionContent />;
}
@@ -0,0 +1,5 @@
import { ManagementSidebar } from "./ManagementSidebar";
export default function ManagementLayout({ children }: { children: React.ReactNode }) {
return <ManagementSidebar>{children}</ManagementSidebar>;
}
@@ -0,0 +1,6 @@
export const MANAGEMENT_NAV_SECTIONS = [
{ id: "data", label: "Data Management", description: "Data retention and session timeout configuration" },
{ id: "offboarding", label: "Offboarding", description: "Tenant offboarding and data removal" },
] as const;
export type ManagementSectionId = (typeof MANAGEMENT_NAV_SECTIONS)[number]["id"];
@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";
export default function ManagementPage() {
redirect("/settings/management/data");
}
@@ -0,0 +1,31 @@
"use client";
import { useTenantConfig } from "../../TenantConfigContext";
import { TenantNotifications } from "../../TenantNotifications";
import DataManagementSection from "../../components/DataManagementSection";
export function SectionDataManagement() {
const {
dataRetentionDays, setDataRetentionDays,
sessionTimeoutHours, setSessionTimeoutHours,
user,
handleSaveDataManagement, handleResetDataManagement,
savingSection,
} = useTenantConfig();
return (
<>
<TenantNotifications />
<DataManagementSection
dataRetentionDays={dataRetentionDays}
setDataRetentionDays={setDataRetentionDays}
sessionTimeoutHours={sessionTimeoutHours}
setSessionTimeoutHours={setSessionTimeoutHours}
isGalacticAdmin={user?.isGalacticAdmin}
onSave={handleSaveDataManagement}
onReset={handleResetDataManagement}
saving={savingSection === "dataManagement"}
/>
</>
);
}
@@ -0,0 +1,30 @@
"use client";
import { useTenantConfig } from "../../TenantConfigContext";
import { TenantNotifications } from "../../TenantNotifications";
import OffboardingSection from "../../components/OffboardingSection";
export function SectionOffboarding() {
const {
showOffboardDialog, setShowOffboardDialog,
offboardConfirmText, setOffboardConfirmText,
offboarding, offboardError, setOffboardError,
handleOffboard,
} = useTenantConfig();
return (
<>
<TenantNotifications />
<OffboardingSection
showOffboardDialog={showOffboardDialog}
setShowOffboardDialog={setShowOffboardDialog}
offboardConfirmText={offboardConfirmText}
setOffboardConfirmText={setOffboardConfirmText}
offboarding={offboarding}
offboardError={offboardError}
setOffboardError={setOffboardError}
onOffboard={handleOffboard}
/>
</>
);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,36 @@
"use client";
import { usePageSections } from "../../../hooks/usePageSections";
import { PageSectionItem } from "../../../contexts/SidebarContext";
import { VALIDATION_NAV_SECTIONS } from "./validationNavSections";
import { ShieldCheckIcon, CpuChipIcon } from "../../../lib/sidebarIcons";
const SECTION_ICONS: Record<string, React.ReactNode> = {
"autopilot": <ShieldCheckIcon />,
"hardware-whitelist": <CpuChipIcon />,
};
const validationItems: PageSectionItem[] = VALIDATION_NAV_SECTIONS.map((s) => ({
id: s.id,
label: s.label,
icon: SECTION_ICONS[s.id],
href: `/settings/validation/${s.id}`,
}));
export function ValidationSidebar({ children }: { children: React.ReactNode }) {
usePageSections(validationItems, "Validation", "route");
return (
<>
<header className="bg-white shadow">
<div className="py-6 px-4 sm:px-6 lg:px-8">
<h1 className="text-2xl font-normal text-gray-900">Tenant Configuration</h1>
<p className="text-sm text-gray-500 mt-1">Autopilot validation and hardware whitelist</p>
</div>
</header>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
{children}
</div>
</>
);
}
@@ -0,0 +1,20 @@
"use client";
import { useParams } from "next/navigation";
import { notFound } from "next/navigation";
import { VALIDATION_NAV_SECTIONS, type ValidationSectionId } from "../validationNavSections";
import { SectionAutopilotValidation } from "../sections/SectionAutopilotValidation";
import { SectionHardwareWhitelist } from "../sections/SectionHardwareWhitelist";
const SECTION_COMPONENTS: Record<ValidationSectionId, React.ComponentType> = {
"autopilot": SectionAutopilotValidation,
"hardware-whitelist": SectionHardwareWhitelist,
};
export default function ValidationSectionPage() {
const params = useParams();
const section = params.section as string;
const SectionContent = SECTION_COMPONENTS[section as ValidationSectionId];
if (!SectionContent) notFound();
return <SectionContent />;
}
@@ -0,0 +1,5 @@
import { ValidationSidebar } from "./ValidationSidebar";
export default function ValidationLayout({ children }: { children: React.ReactNode }) {
return <ValidationSidebar>{children}</ValidationSidebar>;
}
@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";
export default function ValidationPage() {
redirect("/settings/validation/autopilot");
}
@@ -0,0 +1,29 @@
"use client";
import { useTenantConfig } from "../../TenantConfigContext";
import { TenantNotifications } from "../../TenantNotifications";
import AutopilotValidationSection from "../../components/AutopilotValidationSection";
export function SectionAutopilotValidation() {
const {
validateAutopilotDevice, setValidateAutopilotDevice,
validateCorporateIdentifier, setValidateCorporateIdentifier,
autopilotConsentInProgress, savingSection,
beginDeviceValidationConsentFlow,
} = useTenantConfig();
return (
<>
<TenantNotifications />
<AutopilotValidationSection
validateAutopilotDevice={validateAutopilotDevice}
setValidateAutopilotDevice={setValidateAutopilotDevice}
validateCorporateIdentifier={validateCorporateIdentifier}
setValidateCorporateIdentifier={setValidateCorporateIdentifier}
autopilotConsentInProgress={autopilotConsentInProgress}
saving={savingSection === "autopilotValidation"}
onBeginConsent={beginDeviceValidationConsentFlow}
/>
</>
);
}
@@ -0,0 +1,29 @@
"use client";
import { useTenantConfig } from "../../TenantConfigContext";
import { TenantNotifications } from "../../TenantNotifications";
import HardwareWhitelistSection from "../../components/HardwareWhitelistSection";
export function SectionHardwareWhitelist() {
const {
manufacturerWhitelist, setManufacturerWhitelist,
modelWhitelist, setModelWhitelist,
handleSaveHardwareWhitelist, handleResetHardwareWhitelist,
savingSection,
} = useTenantConfig();
return (
<>
<TenantNotifications />
<HardwareWhitelistSection
manufacturerWhitelist={manufacturerWhitelist}
setManufacturerWhitelist={setManufacturerWhitelist}
modelWhitelist={modelWhitelist}
setModelWhitelist={setModelWhitelist}
onSave={handleSaveHardwareWhitelist}
onReset={handleResetHardwareWhitelist}
saving={savingSection === "hardwareWhitelist"}
/>
</>
);
}
@@ -0,0 +1,6 @@
export const VALIDATION_NAV_SECTIONS = [
{ id: "autopilot", label: "Autopilot Validation", description: "Autopilot device and corporate identifier validation" },
{ id: "hardware-whitelist", label: "Hardware Whitelist", description: "Manufacturer and model whitelist configuration" },
] as const;
export type ValidationSectionId = (typeof VALIDATION_NAV_SECTIONS)[number]["id"];
File diff suppressed because one or more lines are too long