rbac-manager 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +29 -0
- package/README.md +317 -2
- package/package.json +45 -4
- package/scripts/postbuild.js +17 -0
- package/src/config/rbac.config.ts +206 -0
- package/src/constants/actions.constants.ts +21 -0
- package/src/constants/apiKeys.constants.ts +25 -0
- package/src/constants/auth.constants.ts +39 -0
- package/src/constants/endpoints.constants.ts +66 -0
- package/src/constants/i18n.constants.ts +82 -0
- package/src/constants/index.ts +70 -0
- package/src/constants/masters.constants.ts +68 -0
- package/src/constants/permissions/index.ts +44 -0
- package/src/constants/permissions/masterPermissions.constants.ts +44 -0
- package/src/constants/permissions/rolePermissions.constants.ts +18 -0
- package/src/constants/permissions.constants.ts +34 -0
- package/src/constants/roles.constants.ts +34 -0
- package/src/core/adapters/authAdapter.ts +89 -0
- package/src/core/adapters/index.ts +17 -0
- package/src/core/adapters/mastersAdapter.ts +132 -0
- package/src/core/adapters/permissionsAdapter.ts +90 -0
- package/src/core/adapters/rolesAdapter.ts +115 -0
- package/src/core/api/httpClient.ts +296 -0
- package/src/core/context/PermissionContext.tsx +232 -0
- package/src/core/context/RBACConfigContext.tsx +52 -0
- package/src/core/guards/Can.tsx +98 -0
- package/src/core/guards/RouteGuard.tsx +42 -0
- package/src/core/hooks/useMaster.ts +99 -0
- package/src/core/hooks/usePermissions.ts +114 -0
- package/src/core/hooks/useRoles.ts +120 -0
- package/src/core/types/adapter.types.ts +54 -0
- package/src/core/types/auth.types.ts +44 -0
- package/src/core/types/index.ts +7 -0
- package/src/core/types/masters.types.ts +63 -0
- package/src/core/types/pagination.types.ts +37 -0
- package/src/core/types/rbac.types.ts +69 -0
- package/src/core/types/router.types.ts +43 -0
- package/src/core/types/style.types.ts +53 -0
- package/src/core/utils/permissionResolver.ts +44 -0
- package/src/index.ts +44 -0
- package/src/modules/masters/DepartmentMasterView.tsx +126 -0
- package/src/modules/masters/DesignationMasterView.tsx +97 -0
- package/src/modules/masters/EmployeeMasterView.tsx +107 -0
- package/src/modules/masters/EntityMasterView.tsx +102 -0
- package/src/modules/masters/ModuleMasterView.tsx +104 -0
- package/src/modules/masters/SubModuleMasterView.tsx +106 -0
- package/src/modules/masters/components/MasterDataTable.tsx +684 -0
- package/src/modules/masters/index.ts +7 -0
- package/src/modules/roles-and-permissions/components/PermissionMatrix.tsx +289 -0
- package/src/modules/roles-and-permissions/components/PermissionRegistry.tsx +108 -0
- package/src/modules/roles-and-permissions/components/RoleListTable.tsx +568 -0
- package/src/modules/roles-and-permissions/components/RoleModal.tsx +213 -0
- package/src/modules/roles-and-permissions/index.ts +4 -0
- package/src/shared/components/Badge.tsx +24 -0
- package/src/shared/components/Button.tsx +75 -0
- package/src/shared/components/DynamicSidebar.tsx +181 -0
- package/src/shared/components/Input.tsx +48 -0
- package/src/shared/components/Modal.tsx +70 -0
- package/src/shared/components/Table.tsx +345 -0
- package/src/styles/standalone.css +51 -0
- package/src/theme/ThemeContext.tsx +74 -0
- package/src/theme/defaultTheme.ts +81 -0
- package/src/theme/index.ts +3 -0
- package/src/theme/theme.types.ts +106 -0
- package/tsconfig.cjs.json +10 -0
- package/tsconfig.esm.json +10 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
|
2
|
+
import { usePermissions } from '../../../core/hooks/usePermissions';
|
|
3
|
+
import { useRoles } from '../../../core/hooks/useRoles';
|
|
4
|
+
import { StandardRole, RolePermissionMatrixRow } from '../../../core/types';
|
|
5
|
+
import { Button } from '../../../shared/components/Button';
|
|
6
|
+
import { Badge } from '../../../shared/components/Badge';
|
|
7
|
+
import { useRBACConfig } from '../../../core/context/RBACConfigContext';
|
|
8
|
+
import { RBAC_CONSTANTS } from '../../../constants';
|
|
9
|
+
|
|
10
|
+
export interface PermissionMatrixProps {
|
|
11
|
+
role: StandardRole;
|
|
12
|
+
onSuccess?: () => void;
|
|
13
|
+
onCancel?: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const PermissionMatrix: React.FC<PermissionMatrixProps> = ({ role, onSuccess, onCancel }) => {
|
|
17
|
+
const { config } = useRBACConfig();
|
|
18
|
+
const i18n = RBAC_CONSTANTS.i18n;
|
|
19
|
+
const { actions, registryModules, fetchActions, fetchRegistry, buildMatrixRows, isLoading: isRegistryLoading } =
|
|
20
|
+
usePermissions();
|
|
21
|
+
const { fetchRolePermissions, assignPermissions } = useRoles();
|
|
22
|
+
|
|
23
|
+
const [assignedIds, setAssignedIds] = useState<Set<string | number>>(new Set());
|
|
24
|
+
const [initialLoading, setInitialLoading] = useState<boolean>(true);
|
|
25
|
+
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
|
26
|
+
const [saveSuccess, setSaveSuccess] = useState<boolean>(false);
|
|
27
|
+
|
|
28
|
+
// Load actions, registry tree, and current role permissions
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
let isMounted = true;
|
|
31
|
+
async function loadData() {
|
|
32
|
+
setInitialLoading(true);
|
|
33
|
+
try {
|
|
34
|
+
const [, , currentPerms] = await Promise.all([
|
|
35
|
+
fetchActions(),
|
|
36
|
+
fetchRegistry({ limit: 100 }),
|
|
37
|
+
fetchRolePermissions(role.id),
|
|
38
|
+
]);
|
|
39
|
+
if (isMounted) {
|
|
40
|
+
setAssignedIds(new Set(currentPerms));
|
|
41
|
+
}
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error('Failed to load permission matrix data', err);
|
|
44
|
+
} finally {
|
|
45
|
+
if (isMounted) setInitialLoading(false);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
loadData();
|
|
49
|
+
return () => {
|
|
50
|
+
isMounted = false;
|
|
51
|
+
};
|
|
52
|
+
}, [fetchActions, fetchRegistry, fetchRolePermissions, role.id]);
|
|
53
|
+
|
|
54
|
+
// Compute matrix rows
|
|
55
|
+
const matrixRows = useMemo<RolePermissionMatrixRow[]>(() => {
|
|
56
|
+
return buildMatrixRows(registryModules, assignedIds, actions);
|
|
57
|
+
}, [buildMatrixRows, registryModules, assignedIds, actions]);
|
|
58
|
+
|
|
59
|
+
// Toggle single action on a module
|
|
60
|
+
const toggleAction = useCallback((permissionActionId: string | number) => {
|
|
61
|
+
if (!permissionActionId) return;
|
|
62
|
+
setAssignedIds((prev) => {
|
|
63
|
+
const next = new Set(prev);
|
|
64
|
+
if (next.has(permissionActionId)) {
|
|
65
|
+
next.delete(permissionActionId);
|
|
66
|
+
} else {
|
|
67
|
+
next.add(permissionActionId);
|
|
68
|
+
}
|
|
69
|
+
return next;
|
|
70
|
+
});
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
// Toggle entire row (all actions on one module)
|
|
74
|
+
const toggleRow = useCallback((row: RolePermissionMatrixRow) => {
|
|
75
|
+
const validActionIds = row.actions.map((a: { permissionActionId: string | number }) => a.permissionActionId).filter(Boolean);
|
|
76
|
+
if (validActionIds.length === 0) return;
|
|
77
|
+
|
|
78
|
+
setAssignedIds((prev) => {
|
|
79
|
+
const next = new Set(prev);
|
|
80
|
+
const allRowSelected = validActionIds.every((id: string | number) => next.has(id));
|
|
81
|
+
|
|
82
|
+
if (allRowSelected) {
|
|
83
|
+
validActionIds.forEach((id: string | number) => next.delete(id));
|
|
84
|
+
} else {
|
|
85
|
+
validActionIds.forEach((id: string | number) => next.add(id));
|
|
86
|
+
}
|
|
87
|
+
return next;
|
|
88
|
+
});
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
// Toggle entire column (e.g. all 'view' actions across all modules)
|
|
92
|
+
const toggleColumn = useCallback(
|
|
93
|
+
(actionKey: string) => {
|
|
94
|
+
const columnActionIds: (string | number)[] = [];
|
|
95
|
+
matrixRows.forEach((row) => {
|
|
96
|
+
const cell = row.actions.find((a: { actionKey: string }) => a.actionKey === actionKey);
|
|
97
|
+
if (cell && cell.permissionActionId) {
|
|
98
|
+
columnActionIds.push(cell.permissionActionId);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (columnActionIds.length === 0) return;
|
|
103
|
+
|
|
104
|
+
setAssignedIds((prev) => {
|
|
105
|
+
const next = new Set(prev);
|
|
106
|
+
const allColSelected = columnActionIds.every((id) => next.has(id));
|
|
107
|
+
|
|
108
|
+
if (allColSelected) {
|
|
109
|
+
columnActionIds.forEach((id) => next.delete(id));
|
|
110
|
+
} else {
|
|
111
|
+
columnActionIds.forEach((id) => next.add(id));
|
|
112
|
+
}
|
|
113
|
+
return next;
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
[matrixRows]
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// Select All / Deselect All
|
|
120
|
+
const toggleAll = useCallback(() => {
|
|
121
|
+
const allValidIds: (string | number)[] = [];
|
|
122
|
+
matrixRows.forEach((row) => {
|
|
123
|
+
row.actions.forEach((a: { permissionActionId: string | number }) => {
|
|
124
|
+
if (a.permissionActionId) allValidIds.push(a.permissionActionId);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
setAssignedIds((prev) => {
|
|
129
|
+
const allSelected = allValidIds.length > 0 && allValidIds.every((id) => prev.has(id));
|
|
130
|
+
return allSelected ? new Set() : new Set(allValidIds);
|
|
131
|
+
});
|
|
132
|
+
}, [matrixRows]);
|
|
133
|
+
|
|
134
|
+
const handleSave = async () => {
|
|
135
|
+
setIsSubmitting(true);
|
|
136
|
+
setSaveSuccess(false);
|
|
137
|
+
try {
|
|
138
|
+
await assignPermissions(role.id, Array.from(assignedIds));
|
|
139
|
+
setSaveSuccess(true);
|
|
140
|
+
if (config.onNotification) {
|
|
141
|
+
config.onNotification({
|
|
142
|
+
type: 'success',
|
|
143
|
+
title: i18n.success || 'Success',
|
|
144
|
+
message: `Permissions updated successfully for role "${role.name}"`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (onSuccess) onSuccess();
|
|
148
|
+
} catch (err: any) {
|
|
149
|
+
if (config.onNotification) {
|
|
150
|
+
config.onNotification({
|
|
151
|
+
type: 'error',
|
|
152
|
+
title: i18n.error || 'Error',
|
|
153
|
+
message: err.message || 'Failed to update permissions',
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
} finally {
|
|
157
|
+
setIsSubmitting(false);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
if (initialLoading || isRegistryLoading) {
|
|
162
|
+
return (
|
|
163
|
+
<div className="flex flex-col items-center justify-center py-16">
|
|
164
|
+
<div className="h-8 w-8 animate-spin rounded-full border-4 border-[#28518F] border-t-transparent mb-3"></div>
|
|
165
|
+
<p className="text-sm text-gray-500">Loading permission matrix...</p>
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<div className="flex flex-col space-y-4">
|
|
172
|
+
{/* Header Info */}
|
|
173
|
+
<div className="flex items-center justify-between pb-3 border-b border-gray-200">
|
|
174
|
+
<div className="flex items-center gap-3">
|
|
175
|
+
<span className="text-sm font-semibold text-gray-900">Role: {role.name}</span>
|
|
176
|
+
<Badge variant="info">{role.key}</Badge>
|
|
177
|
+
<span className="text-xs text-gray-500">({assignedIds.size} permissions assigned)</span>
|
|
178
|
+
</div>
|
|
179
|
+
<div className="flex items-center gap-2">
|
|
180
|
+
<Button variant="outline" size="sm" onClick={toggleAll}>
|
|
181
|
+
Toggle All
|
|
182
|
+
</Button>
|
|
183
|
+
<Button variant="primary" size="sm" isLoading={isSubmitting} onClick={handleSave}>
|
|
184
|
+
Save Changes
|
|
185
|
+
</Button>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
{saveSuccess && (
|
|
190
|
+
<div className="p-3 bg-emerald-50 border border-emerald-200 rounded-lg text-xs text-emerald-700">
|
|
191
|
+
✓ Permissions successfully updated for <strong>{role.name}</strong>.
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
|
|
195
|
+
{/* Checkbox Matrix Table */}
|
|
196
|
+
<div className="overflow-x-auto rounded-xl border border-gray-200 shadow-sm">
|
|
197
|
+
<table className="min-w-full divide-y divide-gray-200 text-sm">
|
|
198
|
+
<thead className="bg-[#FAFAFC] text-gray-700 font-semibold">
|
|
199
|
+
<tr>
|
|
200
|
+
<th className="px-4 py-3 text-left w-1/3">Module / Sub-Module</th>
|
|
201
|
+
{actions.map((act) => (
|
|
202
|
+
<th key={act.id} className="px-3 py-3 text-center">
|
|
203
|
+
<div className="flex flex-col items-center gap-1">
|
|
204
|
+
<span>{act.name}</span>
|
|
205
|
+
<button
|
|
206
|
+
type="button"
|
|
207
|
+
onClick={() => toggleColumn(act.key)}
|
|
208
|
+
className="text-[10px] text-blue-600 hover:underline font-normal"
|
|
209
|
+
>
|
|
210
|
+
toggle
|
|
211
|
+
</button>
|
|
212
|
+
</div>
|
|
213
|
+
</th>
|
|
214
|
+
))}
|
|
215
|
+
<th className="px-3 py-3 text-center">Row</th>
|
|
216
|
+
</tr>
|
|
217
|
+
</thead>
|
|
218
|
+
<tbody className="divide-y divide-gray-200 bg-white">
|
|
219
|
+
{matrixRows.map((row) => {
|
|
220
|
+
const rowActionIds = row.actions.map((a: { permissionActionId: string | number }) => a.permissionActionId).filter(Boolean);
|
|
221
|
+
const isRowFullySelected =
|
|
222
|
+
rowActionIds.length > 0 && rowActionIds.every((id: string | number) => assignedIds.has(id));
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<tr key={`${row.moduleId}-${row.slug}`} className="hover:bg-gray-50/75 transition-colors">
|
|
226
|
+
{/* Module Name with Indentation based on level */}
|
|
227
|
+
<td className="px-4 py-3 text-gray-800">
|
|
228
|
+
<div
|
|
229
|
+
style={{ paddingLeft: `${row.level * 20}px` }}
|
|
230
|
+
className="flex items-center gap-2 font-medium"
|
|
231
|
+
>
|
|
232
|
+
{row.level > 0 && <span className="text-gray-300">↳</span>}
|
|
233
|
+
<span>{row.moduleName}</span>
|
|
234
|
+
<span className="text-xs text-gray-400 font-mono">({row.slug})</span>
|
|
235
|
+
</div>
|
|
236
|
+
</td>
|
|
237
|
+
|
|
238
|
+
{/* Actions Checkboxes */}
|
|
239
|
+
{actions.map((act) => {
|
|
240
|
+
const cell = row.actions.find((a: { actionKey: string }) => a.actionKey === act.key);
|
|
241
|
+
const isRegistered = Boolean(cell?.permissionActionId);
|
|
242
|
+
const isChecked = cell && cell.permissionActionId ? assignedIds.has(cell.permissionActionId) : false;
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<td key={act.id} className="px-3 py-3 text-center">
|
|
246
|
+
{isRegistered ? (
|
|
247
|
+
<input
|
|
248
|
+
type="checkbox"
|
|
249
|
+
checked={isChecked}
|
|
250
|
+
onChange={() => cell && toggleAction(cell.permissionActionId)}
|
|
251
|
+
className="h-4 w-4 rounded border-gray-300 text-[#28518F] focus:ring-[#28518F] cursor-pointer"
|
|
252
|
+
/>
|
|
253
|
+
) : (
|
|
254
|
+
<span className="text-gray-300 text-xs">—</span>
|
|
255
|
+
)}
|
|
256
|
+
</td>
|
|
257
|
+
);
|
|
258
|
+
})}
|
|
259
|
+
|
|
260
|
+
{/* Row Toggle */}
|
|
261
|
+
<td className="px-3 py-3 text-center">
|
|
262
|
+
<input
|
|
263
|
+
type="checkbox"
|
|
264
|
+
checked={isRowFullySelected}
|
|
265
|
+
onChange={() => toggleRow(row)}
|
|
266
|
+
className="h-4 w-4 rounded border-gray-300 text-[#28518F] focus:ring-[#28518F] cursor-pointer"
|
|
267
|
+
/>
|
|
268
|
+
</td>
|
|
269
|
+
</tr>
|
|
270
|
+
);
|
|
271
|
+
})}
|
|
272
|
+
</tbody>
|
|
273
|
+
</table>
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
{/* Footer Actions */}
|
|
277
|
+
<div className="flex justify-end gap-2 pt-2">
|
|
278
|
+
{onCancel && (
|
|
279
|
+
<Button variant="outline" onClick={onCancel}>
|
|
280
|
+
{i18n.cancel || 'Cancel'}
|
|
281
|
+
</Button>
|
|
282
|
+
)}
|
|
283
|
+
<Button variant="primary" isLoading={isSubmitting} onClick={handleSave}>
|
|
284
|
+
{i18n.saveChanges || 'Save Permissions'}
|
|
285
|
+
</Button>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
);
|
|
289
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { usePermissions } from '../../../core/hooks/usePermissions';
|
|
3
|
+
import { Table, Column } from '../../../shared/components/Table';
|
|
4
|
+
import { Badge } from '../../../shared/components/Badge';
|
|
5
|
+
import { StandardModuleWithActions, StandardPermissionActionMapping } from '../../../core/types';
|
|
6
|
+
|
|
7
|
+
export const PermissionRegistry: React.FC = () => {
|
|
8
|
+
const { registryModules, isLoading, fetchRegistry, fetchActions } = usePermissions();
|
|
9
|
+
const [search, setSearch] = useState<string>('');
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
fetchActions();
|
|
13
|
+
fetchRegistry({ page: 1, limit: 50 });
|
|
14
|
+
}, [fetchActions, fetchRegistry]);
|
|
15
|
+
|
|
16
|
+
// Flatten modules tree for table view with level indicator
|
|
17
|
+
const flattenModules = (modules: StandardModuleWithActions[], level = 0): Array<StandardModuleWithActions & { level: number }> => {
|
|
18
|
+
const list: Array<StandardModuleWithActions & { level: number }> = [];
|
|
19
|
+
modules.forEach((m) => {
|
|
20
|
+
list.push({ ...m, level });
|
|
21
|
+
if (m.children && m.children.length > 0) {
|
|
22
|
+
list.push(...flattenModules(m.children, level + 1));
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
return list;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const flatData = flattenModules(registryModules).filter((m) =>
|
|
29
|
+
search ? m.name.toLowerCase().includes(search.toLowerCase()) || m.slug.toLowerCase().includes(search.toLowerCase()) : true
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const columns: Column<StandardModuleWithActions & { level: number }>[] = [
|
|
33
|
+
{
|
|
34
|
+
key: 'name',
|
|
35
|
+
header: 'Module Name',
|
|
36
|
+
render: (item) => (
|
|
37
|
+
<div style={{ paddingLeft: `${item.level * 20}px` }} className="flex items-center gap-2">
|
|
38
|
+
{item.level > 0 && <span className="text-gray-300">↳</span>}
|
|
39
|
+
<div>
|
|
40
|
+
<span className="font-semibold text-gray-900">{item.name}</span>
|
|
41
|
+
<div className="text-xs text-gray-400 font-mono">{item.slug}</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: 'routePath',
|
|
48
|
+
header: 'Frontend Route Path',
|
|
49
|
+
render: (item) => (
|
|
50
|
+
<span className="text-xs font-mono text-gray-600 bg-gray-50 px-2 py-1 rounded border border-gray-200">
|
|
51
|
+
{item.routePath || '—'}
|
|
52
|
+
</span>
|
|
53
|
+
),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
key: 'actions',
|
|
57
|
+
header: 'Registered Actions',
|
|
58
|
+
render: (item) => (
|
|
59
|
+
<div className="flex flex-wrap gap-1.5">
|
|
60
|
+
{item.actions.length > 0 ? (
|
|
61
|
+
item.actions.map((act: StandardPermissionActionMapping) => (
|
|
62
|
+
<Badge key={act.permissionActionId} variant="info">
|
|
63
|
+
{act.actionDetails?.name || act.permissionKey}
|
|
64
|
+
</Badge>
|
|
65
|
+
))
|
|
66
|
+
) : (
|
|
67
|
+
<span className="text-xs text-gray-400">No actions registered</span>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
),
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
key: 'status',
|
|
74
|
+
header: 'Status',
|
|
75
|
+
align: 'center',
|
|
76
|
+
render: (item) => (
|
|
77
|
+
<Badge variant={item.isActive ? 'success' : 'neutral'}>
|
|
78
|
+
{item.isActive ? 'Active' : 'Inactive'}
|
|
79
|
+
</Badge>
|
|
80
|
+
),
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="space-y-4">
|
|
86
|
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
87
|
+
<div>
|
|
88
|
+
<h2 className="text-xl font-bold text-gray-900">Permission Registry</h2>
|
|
89
|
+
<p className="text-xs text-gray-500 mt-1">
|
|
90
|
+
System modules, sub-modules, routes, and active permission actions.
|
|
91
|
+
</p>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div className="flex items-center gap-3">
|
|
96
|
+
<input
|
|
97
|
+
type="text"
|
|
98
|
+
placeholder="Filter modules or slugs..."
|
|
99
|
+
value={search}
|
|
100
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
101
|
+
className="w-full sm:w-72 px-3.5 py-2 border border-gray-300 rounded-lg text-sm bg-white focus:outline-none focus:ring-2 focus:ring-[#28518F]"
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<Table columns={columns} data={flatData} isLoading={isLoading} />
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
};
|