From 85c93158fbd5f0e781c263d1d7b4357f7b86752a Mon Sep 17 00:00:00 2001 From: Fringg Date: Wed, 25 Feb 2026 04:20:37 +0300 Subject: [PATCH] refactor: replace role/policy modals with separate pages - Create AdminRoleEdit.tsx for /admin/roles/create and /admin/roles/:id/edit - Create AdminPolicyEdit.tsx for /admin/policies/create and /admin/policies/:id/edit - Remove create/edit modals from AdminRoles.tsx and AdminPolicies.tsx - Add new lazy routes in App.tsx with proper permission gates - Keep delete confirmation as small inline modal (appropriate for destructive action) --- src/App.tsx | 42 ++ src/pages/AdminPolicies.tsx | 707 +-------------------------------- src/pages/AdminPolicyEdit.tsx | 716 ++++++++++++++++++++++++++++++++++ src/pages/AdminRoleEdit.tsx | 536 +++++++++++++++++++++++++ src/pages/AdminRoles.tsx | 563 +------------------------- 5 files changed, 1317 insertions(+), 1247 deletions(-) create mode 100644 src/pages/AdminPolicyEdit.tsx create mode 100644 src/pages/AdminRoleEdit.tsx diff --git a/src/App.tsx b/src/App.tsx index fb404cb..a9214ff 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -91,8 +91,10 @@ const AdminPinnedMessageCreate = lazy(() => import('./pages/AdminPinnedMessageCr const AdminChannelSubscriptions = lazy(() => import('./pages/AdminChannelSubscriptions')); const AdminEmailTemplatePreview = lazy(() => import('./pages/AdminEmailTemplatePreview')); const AdminRoles = lazy(() => import('./pages/AdminRoles')); +const AdminRoleEdit = lazy(() => import('./pages/AdminRoleEdit')); const AdminRoleAssign = lazy(() => import('./pages/AdminRoleAssign')); const AdminPolicies = lazy(() => import('./pages/AdminPolicies')); +const AdminPolicyEdit = lazy(() => import('./pages/AdminPolicyEdit')); const AdminAuditLog = lazy(() => import('./pages/AdminAuditLog')); function ProtectedRoute({ children }: { children: React.ReactNode }) { @@ -892,6 +894,26 @@ function App() { } /> + + + + + + } + /> + + + + + + } + /> } /> + + + + + + } + /> + + + + + + } + /> ( ); -const XMarkIcon = () => ( - - - -); - const ShieldIcon = () => ( ( ); -// === Types === +// === Helpers === interface PolicyConditions { - time_range?: { - start: string; - end: string; - }; + time_range?: { start: string; end: string }; ip_whitelist?: string[]; rate_limit?: number; } -interface PolicyFormData { - name: string; - description: string; - effect: 'allow' | 'deny'; - resource: string; - actions: string[]; - role_id: number | null; - priority: number; - conditions: PolicyConditions; - conditionsEnabled: { - time_range: boolean; - ip_whitelist: boolean; - rate_limit: boolean; - }; -} - -const INITIAL_FORM: PolicyFormData = { - name: '', - description: '', - effect: 'allow', - resource: '', - actions: [], - role_id: null, - priority: 0, - conditions: { - time_range: { start: '09:00', end: '18:00' }, - ip_whitelist: [], - rate_limit: 100, - }, - conditionsEnabled: { - time_range: false, - ip_whitelist: false, - rate_limit: false, - }, -}; - -// === Helpers === - function parseConditions(raw: Record): PolicyConditions { const result: PolicyConditions = {}; @@ -168,25 +115,6 @@ function parseConditions(raw: Record): PolicyConditions { return result; } -function buildConditionsPayload( - conditions: PolicyConditions, - enabled: PolicyFormData['conditionsEnabled'], -): Record { - const result: Record = {}; - - if (enabled.time_range && conditions.time_range) { - result.time_range = conditions.time_range; - } - if (enabled.ip_whitelist && conditions.ip_whitelist && conditions.ip_whitelist.length > 0) { - result.ip_whitelist = conditions.ip_whitelist; - } - if (enabled.rate_limit && conditions.rate_limit !== undefined) { - result.rate_limit = conditions.rate_limit; - } - - return result; -} - // === Sub-components === interface EffectBadgeProps { @@ -211,109 +139,6 @@ function EffectBadge({ effect, className }: EffectBadgeProps) { ); } -interface IpTagInputProps { - values: string[]; - onChange: (values: string[]) => void; -} - -function IpTagInput({ values, onChange }: IpTagInputProps) { - const { t } = useTranslation(); - const [inputValue, setInputValue] = useState(''); - - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === 'Enter' || e.key === ',') { - e.preventDefault(); - const trimmed = inputValue.trim().replace(/,+$/, ''); - if (trimmed && !values.includes(trimmed)) { - onChange([...values, trimmed]); - } - setInputValue(''); - } else if (e.key === 'Backspace' && !inputValue && values.length > 0) { - onChange(values.slice(0, -1)); - } - }, - [inputValue, values, onChange], - ); - - const removeIp = useCallback( - (ip: string) => { - onChange(values.filter((v) => v !== ip)); - }, - [values, onChange], - ); - - return ( -
- {values.map((ip) => ( - - {ip} - - - ))} - setInputValue(e.target.value)} - onKeyDown={handleKeyDown} - className="min-w-[120px] flex-1 bg-transparent text-sm text-dark-100 placeholder-dark-500 outline-none" - placeholder={values.length === 0 ? t('admin.policies.conditions.ipPlaceholder') : ''} - /> -
- ); -} - -interface ConditionToggleProps { - label: string; - enabled: boolean; - onToggle: () => void; - children: React.ReactNode; -} - -function ConditionToggle({ label, enabled, onToggle, children }: ConditionToggleProps) { - return ( -
- - {enabled &&
{children}
} -
- ); -} - // === Main Page === export default function AdminPolicies() { @@ -322,10 +147,6 @@ export default function AdminPolicies() { const queryClient = useQueryClient(); const { capabilities } = usePlatform(); - // Modal state - const [modalOpen, setModalOpen] = useState(false); - const [editingPolicy, setEditingPolicy] = useState(null); - const [formData, setFormData] = useState(INITIAL_FORM); const [deleteConfirm, setDeleteConfirm] = useState(null); const [formError, setFormError] = useState(null); @@ -344,35 +165,7 @@ export default function AdminPolicies() { queryFn: rbacApi.getRoles, }); - const { data: permissionRegistry } = useQuery({ - queryKey: ['admin-permission-registry'], - queryFn: rbacApi.getPermissionRegistry, - }); - // Mutations - const createMutation = useMutation({ - mutationFn: (payload: CreatePolicyPayload) => rbacApi.createPolicy(payload), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['admin-policies'] }); - closeModal(); - }, - onError: () => { - setFormError(t('admin.policies.errors.createFailed')); - }, - }); - - const updateMutation = useMutation({ - mutationFn: ({ id, payload }: { id: number; payload: UpdatePolicyPayload }) => - rbacApi.updatePolicy(id, payload), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['admin-policies'] }); - closeModal(); - }, - onError: () => { - setFormError(t('admin.policies.errors.updateFailed')); - }, - }); - const deleteMutation = useMutation({ mutationFn: rbacApi.deletePolicy, onSuccess: () => { @@ -396,138 +189,11 @@ export default function AdminPolicies() { return map; }, [roles]); - const resourceSections = useMemo(() => { - if (!permissionRegistry) return []; - return permissionRegistry; - }, [permissionRegistry]); - - const selectedResourceActions = useMemo(() => { - if (!formData.resource || !permissionRegistry) return []; - const section = permissionRegistry.find((s) => s.section === formData.resource); - return section?.actions ?? []; - }, [formData.resource, permissionRegistry]); - const sortedPolicies = useMemo(() => { if (!policies) return []; return [...policies].sort((a, b) => b.priority - a.priority); }, [policies]); - // Handlers - const closeModal = useCallback(() => { - setModalOpen(false); - setEditingPolicy(null); - setFormData(INITIAL_FORM); - setFormError(null); - }, []); - - const openCreateModal = useCallback(() => { - setEditingPolicy(null); - setFormData(INITIAL_FORM); - setFormError(null); - setModalOpen(true); - }, []); - - const openEditModal = useCallback((policy: AccessPolicy) => { - const parsed = parseConditions(policy.conditions); - - const actions = Array.isArray(policy.actions) ? policy.actions : []; - - setEditingPolicy(policy); - setFormData({ - name: policy.name, - description: policy.description ?? '', - effect: policy.effect, - resource: policy.resource, - actions, - role_id: policy.role_id ?? null, - priority: policy.priority, - conditions: { - time_range: parsed.time_range ?? { start: '09:00', end: '18:00' }, - ip_whitelist: parsed.ip_whitelist ?? [], - rate_limit: parsed.rate_limit ?? 100, - }, - conditionsEnabled: { - time_range: !!parsed.time_range, - ip_whitelist: !!parsed.ip_whitelist && parsed.ip_whitelist.length > 0, - rate_limit: parsed.rate_limit !== undefined, - }, - }); - setFormError(null); - setModalOpen(true); - }, []); - - const handleToggleAction = useCallback((action: string) => { - setFormData((prev) => { - const has = prev.actions.includes(action); - return { - ...prev, - actions: has ? prev.actions.filter((a) => a !== action) : [...prev.actions, action], - }; - }); - }, []); - - const handleResourceChange = useCallback((resource: string) => { - setFormData((prev) => ({ - ...prev, - resource, - actions: [], - })); - }, []); - - const handleSubmit = useCallback( - (e: React.FormEvent) => { - e.preventDefault(); - setFormError(null); - - if (!formData.name.trim()) { - setFormError(t('admin.policies.errors.nameRequired')); - return; - } - if (!formData.resource) { - setFormError(t('admin.policies.errors.resourceRequired')); - return; - } - if (formData.actions.length === 0) { - setFormError(t('admin.policies.errors.actionsRequired')); - return; - } - - const conditionsPayload = buildConditionsPayload( - formData.conditions, - formData.conditionsEnabled, - ); - - if (editingPolicy) { - const payload: UpdatePolicyPayload = { - name: formData.name.trim(), - description: formData.description.trim() || null, - effect: formData.effect, - resource: formData.resource, - actions: formData.actions, - conditions: conditionsPayload, - priority: formData.priority, - role_id: formData.role_id, - }; - updateMutation.mutate({ id: editingPolicy.id, payload }); - } else { - const payload: CreatePolicyPayload = { - name: formData.name.trim(), - description: formData.description.trim() || null, - effect: formData.effect, - resource: formData.resource, - actions: formData.actions, - conditions: conditionsPayload, - priority: formData.priority, - role_id: formData.role_id, - }; - createMutation.mutate(payload); - } - }, - [formData, editingPolicy, createMutation, updateMutation, t], - ); - - const isSaving = createMutation.isPending || updateMutation.isPending; - // Condition icons renderer const renderConditionIcons = useCallback( (conditions: Record) => { @@ -608,7 +274,7 @@ export default function AdminPolicies() { - - -
- {/* Name */} -
- - setFormData((prev) => ({ ...prev, name: e.target.value }))} - className="w-full rounded-lg border border-dark-600 bg-dark-900 px-3 py-2 text-dark-100 placeholder-dark-500 outline-none transition-colors focus:border-accent-500" - placeholder={t('admin.policies.form.namePlaceholder')} - autoFocus - /> -
- - {/* Description */} -
- -