feat: role-based UI access control
- Hide Admin sidebar section for non-ADMIN users - Add RequireAdmin route guard — /admin/* redirects to / for non-admin - Move App Config from admin section to main Config tab (per-app, visible when app selected). VIEWER sees read-only, OPERATOR+ can edit - Hide diagram node toolbar for VIEWER (onNodeAction conditional) - Add useIsAdmin/useCanControl helpers to centralize role checks - Remove App Config from admin sidebar tree Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router';
|
||||
import { useNavigate, useSearchParams, useParams } from 'react-router';
|
||||
import { Pencil, X } from 'lucide-react';
|
||||
import {
|
||||
DataTable, Badge, MonoText, DetailPanel, SectionHeader, Button, Toggle, Spinner, useToast,
|
||||
@@ -8,6 +8,7 @@ import type { Column } from '@cameleer/design-system';
|
||||
import { useAllApplicationConfigs, useApplicationConfig, useUpdateApplicationConfig, useProcessorRouteMapping } from '../../api/queries/commands';
|
||||
import type { ApplicationConfig, TapDefinition, ConfigUpdateResponse } from '../../api/queries/commands';
|
||||
import { useRouteCatalog } from '../../api/queries/catalog';
|
||||
import { useCanControl } from '../../auth/auth-store';
|
||||
import type { AppCatalogEntry, RouteSummary } from '../../api/types';
|
||||
import styles from './AppConfigPage.module.css';
|
||||
|
||||
@@ -72,6 +73,7 @@ function buildColumns(): Column<ConfigRow>[] {
|
||||
function AppConfigDetail({ appId, onClose }: { appId: string; onClose: () => void }) {
|
||||
const { toast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
const canEdit = useCanControl();
|
||||
const { data: config, isLoading } = useApplicationConfig(appId);
|
||||
const updateConfig = useUpdateApplicationConfig();
|
||||
const { data: catalog } = useRouteCatalog();
|
||||
@@ -241,9 +243,9 @@ function AppConfigDetail({ appId, onClose }: { appId: string; onClose: () => voi
|
||||
{updateConfig.isPending ? 'Saving\u2026' : 'Save'}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
) : canEdit ? (
|
||||
<button className={styles.editBtn} onClick={startEditing} title="Edit configuration"><Pencil size={14} /></button>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Settings */}
|
||||
@@ -319,17 +321,22 @@ function AppConfigDetail({ appId, onClose }: { appId: string; onClose: () => voi
|
||||
// ── Main Page ────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function AppConfigPage() {
|
||||
const { appId: routeAppId } = useParams<{ appId?: string }>();
|
||||
const { data: configs } = useAllApplicationConfigs();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [selectedApp, setSelectedApp] = useState<string | null>(null);
|
||||
const [selectedApp, setSelectedApp] = useState<string | null>(routeAppId ?? null);
|
||||
const columns = useMemo(buildColumns, []);
|
||||
|
||||
// Sync from route param when it changes (sidebar navigation)
|
||||
useEffect(() => {
|
||||
if (routeAppId) setSelectedApp(routeAppId);
|
||||
}, [routeAppId]);
|
||||
|
||||
// Auto-select app from query param (e.g., ?app=caller-app)
|
||||
useEffect(() => {
|
||||
const appParam = searchParams.get('app');
|
||||
if (appParam && !selectedApp) {
|
||||
setSelectedApp(appParam);
|
||||
// Clean up the query param
|
||||
searchParams.delete('app');
|
||||
searchParams.delete('processor');
|
||||
setSearchParams(searchParams, { replace: true });
|
||||
|
||||
@@ -5,7 +5,7 @@ import { StatusDot, MonoText, Badge, useGlobalFilters } from '@cameleer/design-s
|
||||
import { useCorrelationChain } from '../../api/queries/correlation';
|
||||
import { useAgents } from '../../api/queries/agents';
|
||||
import { useRouteCatalog } from '../../api/queries/catalog';
|
||||
import { useAuthStore } from '../../auth/auth-store';
|
||||
import { useCanControl } from '../../auth/auth-store';
|
||||
import type { ExecutionDetail } from '../../components/ExecutionDiagram/types';
|
||||
import { attributeBadgeColor } from '../../utils/attribute-color';
|
||||
import { RouteControlBar } from './RouteControlBar';
|
||||
@@ -79,8 +79,7 @@ export function ExchangeHeader({ detail, onCorrelatedSelect, onClearSelection }:
|
||||
};
|
||||
}, [agents, detail.instanceId]);
|
||||
|
||||
const roles = useAuthStore((s) => s.roles);
|
||||
const canControl = roles.some(r => r === 'OPERATOR' || r === 'ADMIN');
|
||||
const canControl = useCanControl();
|
||||
|
||||
return (
|
||||
<div className={styles.header}>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useRouteCatalog } from '../../api/queries/catalog';
|
||||
import { useAgents } from '../../api/queries/agents';
|
||||
import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands';
|
||||
import type { TapDefinition, ConfigUpdateResponse } from '../../api/queries/commands';
|
||||
import { useAuthStore } from '../../auth/auth-store';
|
||||
import { useCanControl } from '../../auth/auth-store';
|
||||
import { useTracingStore } from '../../stores/tracing-store';
|
||||
import type { NodeAction, NodeConfig } from '../../components/ProcessDiagram/types';
|
||||
import { TapConfigModal } from '../../components/TapConfigModal';
|
||||
@@ -154,8 +154,7 @@ function DiagramPanel({ appId, routeId, exchangeId, onCorrelatedSelect, onClearS
|
||||
|
||||
// Route state + capabilities for topology-only control bar
|
||||
const { data: agents } = useAgents(undefined, appId);
|
||||
const roles = useAuthStore((s) => s.roles);
|
||||
const canControl = roles.some(r => r === 'OPERATOR' || r === 'ADMIN');
|
||||
const canControl = useCanControl();
|
||||
const { hasRouteControl, hasReplay } = useMemo(() => {
|
||||
if (!agents) return { hasRouteControl: false, hasReplay: false };
|
||||
const agentList = agents as any[];
|
||||
@@ -332,7 +331,7 @@ function DiagramPanel({ appId, routeId, exchangeId, onCorrelatedSelect, onClearS
|
||||
executionDetail={detail}
|
||||
knownRouteIds={knownRouteIds}
|
||||
endpointRouteMap={endpointRouteMap}
|
||||
onNodeAction={handleNodeAction}
|
||||
onNodeAction={canControl ? handleNodeAction : undefined}
|
||||
nodeConfigs={nodeConfigs}
|
||||
/>
|
||||
{tapModal}
|
||||
@@ -359,7 +358,7 @@ function DiagramPanel({ appId, routeId, exchangeId, onCorrelatedSelect, onClearS
|
||||
diagramLayout={diagramQuery.data}
|
||||
knownRouteIds={knownRouteIds}
|
||||
endpointRouteMap={endpointRouteMap}
|
||||
onNodeAction={handleNodeAction}
|
||||
onNodeAction={canControl ? handleNodeAction : undefined}
|
||||
nodeConfigs={nodeConfigs}
|
||||
/>
|
||||
{tapModal}
|
||||
|
||||
Reference in New Issue
Block a user