hazo_auth 3.0.4 → 4.1.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/README.md +228 -8
- package/SETUP_CHECKLIST.md +370 -0
- package/dist/app/api/hazo_auth/me/route.d.ts +3 -0
- package/dist/app/api/hazo_auth/me/route.d.ts.map +1 -1
- package/dist/app/api/hazo_auth/me/route.js +9 -1
- package/dist/components/layouts/my_settings/components/profile_picture_library_tab.d.ts.map +1 -1
- package/dist/components/layouts/my_settings/components/profile_picture_library_tab.js +2 -2
- package/dist/components/layouts/profile_stamp_test/index.d.ts +10 -0
- package/dist/components/layouts/profile_stamp_test/index.d.ts.map +1 -0
- package/dist/components/layouts/profile_stamp_test/index.js +51 -0
- package/dist/components/layouts/rbac_test/index.d.ts +15 -0
- package/dist/components/layouts/rbac_test/index.d.ts.map +1 -0
- package/dist/components/layouts/rbac_test/index.js +378 -0
- package/dist/components/layouts/shared/components/password_field.js +1 -1
- package/dist/components/layouts/shared/components/profile_stamp.d.ts +58 -0
- package/dist/components/layouts/shared/components/profile_stamp.d.ts.map +1 -0
- package/dist/components/layouts/shared/components/profile_stamp.js +72 -0
- package/dist/components/layouts/shared/components/sidebar_layout_wrapper.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/sidebar_layout_wrapper.js +2 -2
- package/dist/components/layouts/shared/components/two_column_auth_layout.js +1 -1
- package/dist/components/layouts/shared/hooks/use_auth_status.d.ts +3 -0
- package/dist/components/layouts/shared/hooks/use_auth_status.d.ts.map +1 -1
- package/dist/components/layouts/shared/hooks/use_auth_status.js +4 -0
- package/dist/components/layouts/shared/index.d.ts +2 -0
- package/dist/components/layouts/shared/index.d.ts.map +1 -1
- package/dist/components/layouts/shared/index.js +1 -0
- package/dist/components/layouts/user_management/components/roles_matrix.d.ts +2 -3
- package/dist/components/layouts/user_management/components/roles_matrix.d.ts.map +1 -1
- package/dist/components/layouts/user_management/components/roles_matrix.js +133 -8
- package/dist/components/layouts/user_management/components/scope_hierarchy_tab.d.ts +12 -0
- package/dist/components/layouts/user_management/components/scope_hierarchy_tab.d.ts.map +1 -0
- package/dist/components/layouts/user_management/components/scope_hierarchy_tab.js +291 -0
- package/dist/components/layouts/user_management/components/scope_labels_tab.d.ts +13 -0
- package/dist/components/layouts/user_management/components/scope_labels_tab.d.ts.map +1 -0
- package/dist/components/layouts/user_management/components/scope_labels_tab.js +158 -0
- package/dist/components/layouts/user_management/components/user_scopes_tab.d.ts +11 -0
- package/dist/components/layouts/user_management/components/user_scopes_tab.d.ts.map +1 -0
- package/dist/components/layouts/user_management/components/user_scopes_tab.js +267 -0
- package/dist/components/layouts/user_management/index.d.ts +9 -2
- package/dist/components/layouts/user_management/index.d.ts.map +1 -1
- package/dist/components/layouts/user_management/index.js +22 -6
- package/dist/components/ui/hover-card.d.ts +7 -0
- package/dist/components/ui/hover-card.d.ts.map +1 -0
- package/dist/components/ui/hover-card.js +29 -0
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +1 -0
- package/dist/components/ui/select.d.ts +14 -0
- package/dist/components/ui/select.d.ts.map +1 -0
- package/dist/components/ui/select.js +59 -0
- package/dist/components/ui/tree-view.d.ts +108 -0
- package/dist/components/ui/tree-view.d.ts.map +1 -0
- package/dist/components/ui/tree-view.js +194 -0
- package/dist/lib/auth/auth_types.d.ts +45 -0
- package/dist/lib/auth/auth_types.d.ts.map +1 -1
- package/dist/lib/auth/auth_types.js +13 -0
- package/dist/lib/auth/hazo_get_auth.server.d.ts +4 -2
- package/dist/lib/auth/hazo_get_auth.server.d.ts.map +1 -1
- package/dist/lib/auth/hazo_get_auth.server.js +107 -3
- package/dist/lib/auth/scope_cache.d.ts +92 -0
- package/dist/lib/auth/scope_cache.d.ts.map +1 -0
- package/dist/lib/auth/scope_cache.js +171 -0
- package/dist/lib/scope_hierarchy_config.server.d.ts +39 -0
- package/dist/lib/scope_hierarchy_config.server.d.ts.map +1 -0
- package/dist/lib/scope_hierarchy_config.server.js +96 -0
- package/dist/lib/services/email_service.d.ts.map +1 -1
- package/dist/lib/services/email_service.js +7 -2
- package/dist/lib/services/profile_picture_service.d.ts +1 -7
- package/dist/lib/services/profile_picture_service.d.ts.map +1 -1
- package/dist/lib/services/profile_picture_service.js +77 -32
- package/dist/lib/services/registration_service.js +1 -1
- package/dist/lib/services/scope_labels_service.d.ts +48 -0
- package/dist/lib/services/scope_labels_service.d.ts.map +1 -0
- package/dist/lib/services/scope_labels_service.js +277 -0
- package/dist/lib/services/scope_service.d.ts +114 -0
- package/dist/lib/services/scope_service.d.ts.map +1 -0
- package/dist/lib/services/scope_service.js +582 -0
- package/dist/lib/services/user_scope_service.d.ts +74 -0
- package/dist/lib/services/user_scope_service.d.ts.map +1 -0
- package/dist/lib/services/user_scope_service.js +415 -0
- package/hazo_auth_config.example.ini +1 -1
- package/package.json +4 -1
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// file_description: Scope Hierarchy tab component for managing HRBAC scopes (L1-L7) using tree view
|
|
2
|
+
// section: client_directive
|
|
3
|
+
"use client";
|
|
4
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
5
|
+
// section: imports
|
|
6
|
+
import { useState, useEffect, useCallback, useMemo } from "react";
|
|
7
|
+
import { TreeView } from "../../../ui/tree-view";
|
|
8
|
+
import { Button } from "../../../ui/button";
|
|
9
|
+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../../../ui/dialog";
|
|
10
|
+
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "../../../ui/alert-dialog";
|
|
11
|
+
import { Input } from "../../../ui/input";
|
|
12
|
+
import { Label } from "../../../ui/label";
|
|
13
|
+
import { Loader2, Plus, Edit, Trash2, CircleCheck, CircleX, Building2, FolderTree, RefreshCw, } from "lucide-react";
|
|
14
|
+
import { toast } from "sonner";
|
|
15
|
+
import { useHazoAuthConfig } from "../../../../contexts/hazo_auth_provider";
|
|
16
|
+
const SCOPE_LEVEL_LABELS = {
|
|
17
|
+
hazo_scopes_l1: "Level 1",
|
|
18
|
+
hazo_scopes_l2: "Level 2",
|
|
19
|
+
hazo_scopes_l3: "Level 3",
|
|
20
|
+
hazo_scopes_l4: "Level 4",
|
|
21
|
+
hazo_scopes_l5: "Level 5",
|
|
22
|
+
hazo_scopes_l6: "Level 6",
|
|
23
|
+
hazo_scopes_l7: "Level 7",
|
|
24
|
+
};
|
|
25
|
+
// section: helpers
|
|
26
|
+
function getLevelNumber(level) {
|
|
27
|
+
return parseInt(level.replace("hazo_scopes_l", ""));
|
|
28
|
+
}
|
|
29
|
+
function getChildLevel(level) {
|
|
30
|
+
const num = getLevelNumber(level);
|
|
31
|
+
if (num >= 7)
|
|
32
|
+
return null;
|
|
33
|
+
return `hazo_scopes_l${num + 1}`;
|
|
34
|
+
}
|
|
35
|
+
// Convert ScopeTreeNode to TreeDataItem format
|
|
36
|
+
function convertToTreeData(nodes, onEdit, onDelete, onAddChild) {
|
|
37
|
+
return nodes.map((node) => {
|
|
38
|
+
const levelNum = getLevelNumber(node.level);
|
|
39
|
+
const hasChildren = node.children && node.children.length > 0;
|
|
40
|
+
const canHaveChildren = levelNum < 7;
|
|
41
|
+
const item = {
|
|
42
|
+
id: node.id,
|
|
43
|
+
name: `${node.name} (${node.seq})`,
|
|
44
|
+
icon: Building2,
|
|
45
|
+
scopeData: node,
|
|
46
|
+
actions: (_jsxs("div", { className: "flex items-center gap-1", children: [canHaveChildren && (_jsx(Button, { variant: "ghost", size: "sm", className: "h-6 w-6 p-0", onClick: (e) => {
|
|
47
|
+
e.stopPropagation();
|
|
48
|
+
onAddChild(node);
|
|
49
|
+
}, title: "Add child scope", children: _jsx(Plus, { className: "h-3 w-3" }) })), _jsx(Button, { variant: "ghost", size: "sm", className: "h-6 w-6 p-0", onClick: (e) => {
|
|
50
|
+
e.stopPropagation();
|
|
51
|
+
onEdit(node);
|
|
52
|
+
}, title: "Edit scope", children: _jsx(Edit, { className: "h-3 w-3" }) }), _jsx(Button, { variant: "ghost", size: "sm", className: "h-6 w-6 p-0 text-destructive hover:text-destructive", onClick: (e) => {
|
|
53
|
+
e.stopPropagation();
|
|
54
|
+
onDelete(node);
|
|
55
|
+
}, title: "Delete scope", children: _jsx(Trash2, { className: "h-3 w-3" }) })] })),
|
|
56
|
+
};
|
|
57
|
+
if (hasChildren) {
|
|
58
|
+
item.children = convertToTreeData(node.children, onEdit, onDelete, onAddChild);
|
|
59
|
+
}
|
|
60
|
+
return item;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// section: component
|
|
64
|
+
/**
|
|
65
|
+
* Scope Hierarchy tab component for managing HRBAC scopes
|
|
66
|
+
* Displays scopes in a tree view for intuitive hierarchy configuration
|
|
67
|
+
* @param props - Component props
|
|
68
|
+
* @returns Scope Hierarchy tab component
|
|
69
|
+
*/
|
|
70
|
+
export function ScopeHierarchyTab({ className, defaultOrg = "", }) {
|
|
71
|
+
const { apiBasePath } = useHazoAuthConfig();
|
|
72
|
+
// State
|
|
73
|
+
const [tree, setTree] = useState([]);
|
|
74
|
+
const [loading, setLoading] = useState(true);
|
|
75
|
+
const [actionLoading, setActionLoading] = useState(false);
|
|
76
|
+
const [org, setOrg] = useState(defaultOrg);
|
|
77
|
+
const [selectedItem, setSelectedItem] = useState();
|
|
78
|
+
// Dialog state
|
|
79
|
+
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
|
80
|
+
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
|
81
|
+
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
82
|
+
const [selectedScope, setSelectedScope] = useState(null);
|
|
83
|
+
const [addParentScope, setAddParentScope] = useState(null);
|
|
84
|
+
// Form state
|
|
85
|
+
const [newName, setNewName] = useState("");
|
|
86
|
+
const [newOrg, setNewOrg] = useState(defaultOrg);
|
|
87
|
+
const [editName, setEditName] = useState("");
|
|
88
|
+
// Load tree data
|
|
89
|
+
const loadTree = useCallback(async () => {
|
|
90
|
+
if (!org) {
|
|
91
|
+
setTree([]);
|
|
92
|
+
setLoading(false);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
setLoading(true);
|
|
96
|
+
try {
|
|
97
|
+
const params = new URLSearchParams({ action: "tree", org });
|
|
98
|
+
const response = await fetch(`${apiBasePath}/scope_management/scopes?${params}`);
|
|
99
|
+
const data = await response.json();
|
|
100
|
+
if (data.success) {
|
|
101
|
+
setTree(data.tree || []);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
toast.error(data.error || "Failed to load scope hierarchy");
|
|
105
|
+
setTree([]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
toast.error("Failed to load scope hierarchy");
|
|
110
|
+
setTree([]);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
setLoading(false);
|
|
114
|
+
}
|
|
115
|
+
}, [apiBasePath, org]);
|
|
116
|
+
// Load data when org changes
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
void loadTree();
|
|
119
|
+
}, [loadTree]);
|
|
120
|
+
// Handle add scope (root level)
|
|
121
|
+
const handleAddRootScope = () => {
|
|
122
|
+
setAddParentScope(null);
|
|
123
|
+
setNewOrg(org || defaultOrg);
|
|
124
|
+
setNewName("");
|
|
125
|
+
setAddDialogOpen(true);
|
|
126
|
+
};
|
|
127
|
+
// Handle add child scope
|
|
128
|
+
const handleAddChildScope = (parent) => {
|
|
129
|
+
setAddParentScope(parent);
|
|
130
|
+
setNewOrg(parent.org);
|
|
131
|
+
setNewName("");
|
|
132
|
+
setAddDialogOpen(true);
|
|
133
|
+
};
|
|
134
|
+
// Handle edit scope
|
|
135
|
+
const openEditDialog = (scope) => {
|
|
136
|
+
setSelectedScope(scope);
|
|
137
|
+
setEditName(scope.name);
|
|
138
|
+
setEditDialogOpen(true);
|
|
139
|
+
};
|
|
140
|
+
// Handle delete scope
|
|
141
|
+
const openDeleteDialog = (scope) => {
|
|
142
|
+
setSelectedScope(scope);
|
|
143
|
+
setDeleteDialogOpen(true);
|
|
144
|
+
};
|
|
145
|
+
// Create scope
|
|
146
|
+
const handleCreateScope = async () => {
|
|
147
|
+
if (!newName.trim()) {
|
|
148
|
+
toast.error("Name is required");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (!newOrg.trim()) {
|
|
152
|
+
toast.error("Organization is required");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
setActionLoading(true);
|
|
156
|
+
try {
|
|
157
|
+
const level = addParentScope
|
|
158
|
+
? getChildLevel(addParentScope.level)
|
|
159
|
+
: "hazo_scopes_l1";
|
|
160
|
+
if (!level) {
|
|
161
|
+
toast.error("Cannot add children to Level 7 scopes");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const body = {
|
|
165
|
+
level,
|
|
166
|
+
org: newOrg.trim(),
|
|
167
|
+
name: newName.trim(),
|
|
168
|
+
};
|
|
169
|
+
if (addParentScope) {
|
|
170
|
+
body.parent_scope_id = addParentScope.id;
|
|
171
|
+
}
|
|
172
|
+
const response = await fetch(`${apiBasePath}/scope_management/scopes`, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: { "Content-Type": "application/json" },
|
|
175
|
+
body: JSON.stringify(body),
|
|
176
|
+
});
|
|
177
|
+
const data = await response.json();
|
|
178
|
+
if (data.success) {
|
|
179
|
+
toast.success("Scope created successfully");
|
|
180
|
+
setAddDialogOpen(false);
|
|
181
|
+
setNewName("");
|
|
182
|
+
setAddParentScope(null);
|
|
183
|
+
await loadTree();
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
toast.error(data.error || "Failed to create scope");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
toast.error("Failed to create scope");
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
setActionLoading(false);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
// Update scope
|
|
197
|
+
const handleUpdateScope = async () => {
|
|
198
|
+
if (!selectedScope)
|
|
199
|
+
return;
|
|
200
|
+
if (!editName.trim()) {
|
|
201
|
+
toast.error("Name is required");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
setActionLoading(true);
|
|
205
|
+
try {
|
|
206
|
+
const body = {
|
|
207
|
+
level: selectedScope.level,
|
|
208
|
+
scope_id: selectedScope.id,
|
|
209
|
+
name: editName.trim(),
|
|
210
|
+
};
|
|
211
|
+
const response = await fetch(`${apiBasePath}/scope_management/scopes`, {
|
|
212
|
+
method: "PATCH",
|
|
213
|
+
headers: { "Content-Type": "application/json" },
|
|
214
|
+
body: JSON.stringify(body),
|
|
215
|
+
});
|
|
216
|
+
const data = await response.json();
|
|
217
|
+
if (data.success) {
|
|
218
|
+
toast.success("Scope updated successfully");
|
|
219
|
+
setEditDialogOpen(false);
|
|
220
|
+
setSelectedScope(null);
|
|
221
|
+
setEditName("");
|
|
222
|
+
await loadTree();
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
toast.error(data.error || "Failed to update scope");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
toast.error("Failed to update scope");
|
|
230
|
+
}
|
|
231
|
+
finally {
|
|
232
|
+
setActionLoading(false);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
// Delete scope
|
|
236
|
+
const handleDeleteScope = async () => {
|
|
237
|
+
if (!selectedScope)
|
|
238
|
+
return;
|
|
239
|
+
setActionLoading(true);
|
|
240
|
+
try {
|
|
241
|
+
const params = new URLSearchParams({
|
|
242
|
+
level: selectedScope.level,
|
|
243
|
+
scope_id: selectedScope.id,
|
|
244
|
+
});
|
|
245
|
+
const response = await fetch(`${apiBasePath}/scope_management/scopes?${params}`, {
|
|
246
|
+
method: "DELETE",
|
|
247
|
+
});
|
|
248
|
+
const data = await response.json();
|
|
249
|
+
if (data.success) {
|
|
250
|
+
toast.success("Scope deleted successfully");
|
|
251
|
+
setDeleteDialogOpen(false);
|
|
252
|
+
setSelectedScope(null);
|
|
253
|
+
await loadTree();
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
toast.error(data.error || "Failed to delete scope");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
toast.error("Failed to delete scope");
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
setActionLoading(false);
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
// Convert tree to TreeDataItem format
|
|
267
|
+
const treeData = useMemo(() => {
|
|
268
|
+
return convertToTreeData(tree, openEditDialog, openDeleteDialog, handleAddChildScope);
|
|
269
|
+
}, [tree]);
|
|
270
|
+
// Handle tree item selection
|
|
271
|
+
const handleSelectChange = (item) => {
|
|
272
|
+
setSelectedItem(item);
|
|
273
|
+
};
|
|
274
|
+
// Get level label for dialog
|
|
275
|
+
const getAddDialogLevelLabel = () => {
|
|
276
|
+
if (!addParentScope)
|
|
277
|
+
return "Level 1";
|
|
278
|
+
const childLevel = getChildLevel(addParentScope.level);
|
|
279
|
+
return childLevel ? SCOPE_LEVEL_LABELS[childLevel] : "Unknown";
|
|
280
|
+
};
|
|
281
|
+
return (_jsxs("div", { className: `cls_scope_hierarchy_tab flex flex-col gap-4 w-full ${className || ""}`, children: [_jsxs("div", { className: "cls_scope_hierarchy_header flex items-center justify-between gap-4 flex-wrap", children: [_jsxs("div", { className: "cls_scope_hierarchy_header_left flex items-center gap-4", children: [_jsxs("div", { className: "cls_scope_hierarchy_org_filter flex items-center gap-2", children: [_jsx(Label, { htmlFor: "scope_org", className: "text-sm font-medium", children: "Organization:" }), _jsx(Input, { id: "scope_org", value: org, onChange: (e) => setOrg(e.target.value), placeholder: "Enter organization", className: "w-[200px]" })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void loadTree(), disabled: loading || !org, children: [_jsx(RefreshCw, { className: `h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}` }), "Refresh"] })] }), _jsx("div", { className: "cls_scope_hierarchy_header_right", children: _jsxs(Button, { onClick: handleAddRootScope, variant: "default", size: "sm", disabled: !org, children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), "Add Root Scope"] }) })] }), loading ? (_jsx("div", { className: "cls_scope_hierarchy_loading flex items-center justify-center p-8", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-slate-400" }) })) : !org ? (_jsxs("div", { className: "cls_scope_hierarchy_empty flex flex-col items-center justify-center p-8 border rounded-lg border-dashed", children: [_jsx(Building2, { className: "h-12 w-12 text-muted-foreground mb-4" }), _jsx("p", { className: "text-muted-foreground text-center", children: "Enter an organization name to view scope hierarchy" })] })) : tree.length === 0 ? (_jsxs("div", { className: "cls_scope_hierarchy_empty flex flex-col items-center justify-center p-8 border rounded-lg border-dashed", children: [_jsx(FolderTree, { className: "h-12 w-12 text-muted-foreground mb-4" }), _jsxs("p", { className: "text-muted-foreground text-center mb-4", children: ["No scopes found for organization \"", org, "\""] }), _jsxs(Button, { onClick: handleAddRootScope, variant: "outline", size: "sm", children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), "Create First Scope"] })] })) : (_jsx("div", { className: "cls_scope_hierarchy_tree_container border rounded-lg overflow-auto w-full min-h-[300px]", children: _jsx(TreeView, { data: treeData, expandAll: true, defaultNodeIcon: Building2, defaultLeafIcon: Building2, onSelectChange: handleSelectChange, className: "w-full" }) })), (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.scopeData) && (_jsxs("div", { className: "cls_scope_hierarchy_selected_info p-4 border rounded-lg bg-muted/50", children: [_jsx("h4", { className: "font-medium mb-2", children: "Selected Scope" }), _jsxs("div", { className: "grid grid-cols-2 gap-2 text-sm", children: [_jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "Name:" }), " ", selectedItem.scopeData.name] }), _jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "Seq:" }), " ", selectedItem.scopeData.seq] }), _jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "Level:" }), " ", SCOPE_LEVEL_LABELS[selectedItem.scopeData.level]] }), _jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "Org:" }), " ", selectedItem.scopeData.org] })] })] })), _jsx(Dialog, { open: addDialogOpen, onOpenChange: setAddDialogOpen, children: _jsxs(DialogContent, { className: "cls_scope_hierarchy_add_dialog", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: addParentScope
|
|
282
|
+
? `Add Child Scope to "${addParentScope.name}"`
|
|
283
|
+
: "Add Root Scope" }), _jsxs(DialogDescription, { children: ["Create a new scope at ", getAddDialogLevelLabel(), ".", addParentScope &&
|
|
284
|
+
` This will be a child of "${addParentScope.name}".`] })] }), _jsxs("div", { className: "flex flex-col gap-4 py-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "new_scope_name", children: "Name *" }), _jsx(Input, { id: "new_scope_name", value: newName, onChange: (e) => setNewName(e.target.value), placeholder: "Enter scope name" })] }), !addParentScope && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "new_scope_org", children: "Organization *" }), _jsx(Input, { id: "new_scope_org", value: newOrg, onChange: (e) => setNewOrg(e.target.value), placeholder: "Enter organization" })] }))] }), _jsxs(DialogFooter, { children: [_jsx(Button, { onClick: handleCreateScope, disabled: actionLoading, variant: "default", children: actionLoading ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-4 w-4 mr-2 animate-spin" }), "Creating..."] })) : (_jsxs(_Fragment, { children: [_jsx(CircleCheck, { className: "h-4 w-4 mr-2" }), "Create"] })) }), _jsxs(Button, { onClick: () => setAddDialogOpen(false), variant: "outline", children: [_jsx(CircleX, { className: "h-4 w-4 mr-2" }), "Cancel"] })] })] }) }), _jsx(Dialog, { open: editDialogOpen, onOpenChange: setEditDialogOpen, children: _jsxs(DialogContent, { className: "cls_scope_hierarchy_edit_dialog", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Edit Scope" }), _jsxs(DialogDescription, { children: ["Update scope: ", selectedScope === null || selectedScope === void 0 ? void 0 : selectedScope.name, " (", selectedScope === null || selectedScope === void 0 ? void 0 : selectedScope.seq, ")"] })] }), _jsx("div", { className: "flex flex-col gap-4 py-4", children: _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "edit_scope_name", children: "Name *" }), _jsx(Input, { id: "edit_scope_name", value: editName, onChange: (e) => setEditName(e.target.value), placeholder: "Enter scope name" })] }) }), _jsxs(DialogFooter, { children: [_jsx(Button, { onClick: handleUpdateScope, disabled: actionLoading, variant: "default", children: actionLoading ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-4 w-4 mr-2 animate-spin" }), "Saving..."] })) : (_jsxs(_Fragment, { children: [_jsx(CircleCheck, { className: "h-4 w-4 mr-2" }), "Save"] })) }), _jsxs(Button, { onClick: () => {
|
|
285
|
+
setEditDialogOpen(false);
|
|
286
|
+
setSelectedScope(null);
|
|
287
|
+
}, variant: "outline", children: [_jsx(CircleX, { className: "h-4 w-4 mr-2" }), "Cancel"] })] })] }) }), _jsx(AlertDialog, { open: deleteDialogOpen, onOpenChange: setDeleteDialogOpen, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Delete Scope" }), _jsxs(AlertDialogDescription, { children: ["Are you sure you want to delete \"", selectedScope === null || selectedScope === void 0 ? void 0 : selectedScope.name, "\" (", selectedScope === null || selectedScope === void 0 ? void 0 : selectedScope.seq, ")? This action cannot be undone and will also delete all child scopes."] })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogAction, { onClick: handleDeleteScope, disabled: actionLoading, children: actionLoading ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-4 w-4 mr-2 animate-spin" }), "Deleting..."] })) : ("Delete") }), _jsx(AlertDialogCancel, { onClick: () => {
|
|
288
|
+
setDeleteDialogOpen(false);
|
|
289
|
+
setSelectedScope(null);
|
|
290
|
+
}, children: "Cancel" })] })] }) })] }));
|
|
291
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type ScopeLabelsTabProps = {
|
|
2
|
+
className?: string;
|
|
3
|
+
defaultOrg?: string;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Scope Labels tab component for configuring friendly names for scope levels
|
|
7
|
+
* Shows all 7 scope levels with their current labels from database
|
|
8
|
+
* Empty inputs for levels without labels - no placeholders
|
|
9
|
+
* @param props - Component props
|
|
10
|
+
* @returns Scope Labels tab component
|
|
11
|
+
*/
|
|
12
|
+
export declare function ScopeLabelsTab({ className, defaultOrg }: ScopeLabelsTabProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
//# sourceMappingURL=scope_labels_tab.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope_labels_tab.d.ts","sourceRoot":"","sources":["../../../../../src/components/layouts/user_management/components/scope_labels_tab.tsx"],"names":[],"mappings":"AAsBA,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAeF;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,EAAE,SAAS,EAAE,UAAe,EAAE,EAAE,mBAAmB,2CA4NjF"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// file_description: Scope Labels tab component for configuring friendly names for scope levels
|
|
2
|
+
// section: client_directive
|
|
3
|
+
"use client";
|
|
4
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
5
|
+
// section: imports
|
|
6
|
+
import { useState, useEffect, useCallback } from "react";
|
|
7
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../../../ui/table";
|
|
8
|
+
import { Button } from "../../../ui/button";
|
|
9
|
+
import { Input } from "../../../ui/input";
|
|
10
|
+
import { Label } from "../../../ui/label";
|
|
11
|
+
import { Loader2, Save } from "lucide-react";
|
|
12
|
+
import { toast } from "sonner";
|
|
13
|
+
import { useHazoAuthConfig } from "../../../../contexts/hazo_auth_provider";
|
|
14
|
+
const SCOPE_LEVELS = [
|
|
15
|
+
"hazo_scopes_l1",
|
|
16
|
+
"hazo_scopes_l2",
|
|
17
|
+
"hazo_scopes_l3",
|
|
18
|
+
"hazo_scopes_l4",
|
|
19
|
+
"hazo_scopes_l5",
|
|
20
|
+
"hazo_scopes_l6",
|
|
21
|
+
"hazo_scopes_l7",
|
|
22
|
+
];
|
|
23
|
+
// section: component
|
|
24
|
+
/**
|
|
25
|
+
* Scope Labels tab component for configuring friendly names for scope levels
|
|
26
|
+
* Shows all 7 scope levels with their current labels from database
|
|
27
|
+
* Empty inputs for levels without labels - no placeholders
|
|
28
|
+
* @param props - Component props
|
|
29
|
+
* @returns Scope Labels tab component
|
|
30
|
+
*/
|
|
31
|
+
export function ScopeLabelsTab({ className, defaultOrg = "" }) {
|
|
32
|
+
const { apiBasePath } = useHazoAuthConfig();
|
|
33
|
+
// State - simple record of scope_type to label string (empty string if not set)
|
|
34
|
+
const [labels, setLabels] = useState(() => {
|
|
35
|
+
const initial = {};
|
|
36
|
+
for (const level of SCOPE_LEVELS) {
|
|
37
|
+
initial[level] = "";
|
|
38
|
+
}
|
|
39
|
+
return initial;
|
|
40
|
+
});
|
|
41
|
+
const [originalLabels, setOriginalLabels] = useState(null);
|
|
42
|
+
const [loading, setLoading] = useState(true);
|
|
43
|
+
const [saving, setSaving] = useState(false);
|
|
44
|
+
const [org, setOrg] = useState(defaultOrg);
|
|
45
|
+
// Load labels from database (only real DB records, not synthetic defaults)
|
|
46
|
+
const loadLabels = useCallback(async () => {
|
|
47
|
+
if (!org.trim()) {
|
|
48
|
+
// Reset to empty if no org
|
|
49
|
+
const empty = {};
|
|
50
|
+
for (const level of SCOPE_LEVELS) {
|
|
51
|
+
empty[level] = "";
|
|
52
|
+
}
|
|
53
|
+
setLabels(empty);
|
|
54
|
+
setOriginalLabels(empty);
|
|
55
|
+
setLoading(false);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
setLoading(true);
|
|
59
|
+
try {
|
|
60
|
+
// Fetch WITHOUT defaults - only get actual DB records
|
|
61
|
+
const params = new URLSearchParams({ org: org.trim(), include_defaults: "false" });
|
|
62
|
+
const response = await fetch(`${apiBasePath}/scope_management/labels?${params}`);
|
|
63
|
+
const data = await response.json();
|
|
64
|
+
if (data.success) {
|
|
65
|
+
// Start with empty labels
|
|
66
|
+
const newLabels = {};
|
|
67
|
+
for (const level of SCOPE_LEVELS) {
|
|
68
|
+
newLabels[level] = "";
|
|
69
|
+
}
|
|
70
|
+
// Fill in labels from database
|
|
71
|
+
const dbLabels = data.labels || [];
|
|
72
|
+
for (const dbLabel of dbLabels) {
|
|
73
|
+
if (dbLabel.scope_type && dbLabel.label) {
|
|
74
|
+
newLabels[dbLabel.scope_type] = dbLabel.label;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
setLabels(newLabels);
|
|
78
|
+
setOriginalLabels(Object.assign({}, newLabels));
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
toast.error(data.error || "Failed to load labels");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (_a) {
|
|
85
|
+
toast.error("Failed to load labels");
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
setLoading(false);
|
|
89
|
+
}
|
|
90
|
+
}, [apiBasePath, org]);
|
|
91
|
+
// Load labels when org changes
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
void loadLabels();
|
|
94
|
+
}, [loadLabels]);
|
|
95
|
+
// Handle label change
|
|
96
|
+
const handleLabelChange = (level, value) => {
|
|
97
|
+
setLabels((prev) => (Object.assign(Object.assign({}, prev), { [level]: value })));
|
|
98
|
+
};
|
|
99
|
+
// Check if there are unsaved changes
|
|
100
|
+
const hasChanges = () => {
|
|
101
|
+
if (!originalLabels)
|
|
102
|
+
return false;
|
|
103
|
+
for (const level of SCOPE_LEVELS) {
|
|
104
|
+
if (labels[level] !== originalLabels[level]) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
};
|
|
110
|
+
// Save all labels - send all non-empty labels to be upserted
|
|
111
|
+
const handleSave = async () => {
|
|
112
|
+
if (!org.trim()) {
|
|
113
|
+
toast.error("Organization is required");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Collect all non-empty labels
|
|
117
|
+
const labelsToSave = [];
|
|
118
|
+
for (const level of SCOPE_LEVELS) {
|
|
119
|
+
const label = labels[level].trim();
|
|
120
|
+
if (label) {
|
|
121
|
+
labelsToSave.push({
|
|
122
|
+
scope_type: level,
|
|
123
|
+
label: label,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
setSaving(true);
|
|
128
|
+
try {
|
|
129
|
+
const response = await fetch(`${apiBasePath}/scope_management/labels`, {
|
|
130
|
+
method: "PUT",
|
|
131
|
+
headers: { "Content-Type": "application/json" },
|
|
132
|
+
body: JSON.stringify({
|
|
133
|
+
org: org.trim(),
|
|
134
|
+
labels: labelsToSave,
|
|
135
|
+
}),
|
|
136
|
+
});
|
|
137
|
+
const data = await response.json();
|
|
138
|
+
if (data.success) {
|
|
139
|
+
toast.success("Labels saved successfully");
|
|
140
|
+
// Reload to get fresh state
|
|
141
|
+
await loadLabels();
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
toast.error(data.error || "Failed to save labels");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (_a) {
|
|
148
|
+
toast.error("Failed to save labels");
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
setSaving(false);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
return (_jsxs("div", { className: `cls_scope_labels_tab flex flex-col gap-4 w-full ${className || ""}`, children: [_jsxs("div", { className: "cls_scope_labels_header flex items-center justify-between gap-4 flex-wrap", children: [_jsx("div", { className: "cls_scope_labels_header_left flex items-center gap-4", children: _jsxs("div", { className: "cls_scope_labels_org_input flex items-center gap-2", children: [_jsx(Label, { htmlFor: "labels_org", className: "text-sm font-medium", children: "Organization:" }), _jsx(Input, { id: "labels_org", value: org, onChange: (e) => setOrg(e.target.value), placeholder: "Enter organization name", className: "w-[200px]" })] }) }), _jsx("div", { className: "cls_scope_labels_header_right", children: _jsx(Button, { onClick: handleSave, disabled: saving || !hasChanges() || !org.trim(), variant: "default", size: "sm", children: saving ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-4 w-4 mr-2 animate-spin" }), "Saving..."] })) : (_jsxs(_Fragment, { children: [_jsx(Save, { className: "h-4 w-4 mr-2" }), "Save Changes"] })) }) })] }), loading ? (_jsx("div", { className: "cls_scope_labels_loading flex items-center justify-center p-8", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-slate-400" }) })) : (_jsx("div", { className: "cls_scope_labels_table_container border rounded-lg overflow-auto w-full", children: _jsxs(Table, { className: "cls_scope_labels_table w-full", children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-[180px]", children: "Scope Level" }), _jsx(TableHead, { children: "Label" })] }) }), _jsx(TableBody, { children: SCOPE_LEVELS.map((level) => {
|
|
155
|
+
const label = labels[level];
|
|
156
|
+
return (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-mono text-sm", children: level }), _jsx(TableCell, { children: _jsx(Input, { value: label, onChange: (e) => handleLabelChange(level, e.target.value), className: "max-w-[400px]", disabled: !org.trim() }) })] }, level));
|
|
157
|
+
}) })] }) })), !org.trim() && (_jsx("div", { className: "cls_scope_labels_info text-sm text-muted-foreground text-center p-4 bg-muted/50 rounded-lg", children: "Enter an organization name to customize scope labels." }))] }));
|
|
158
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type UserScopesTabProps = {
|
|
2
|
+
className?: string;
|
|
3
|
+
};
|
|
4
|
+
/**
|
|
5
|
+
* User Scopes tab component for assigning scopes to users
|
|
6
|
+
* Two-panel layout: Users list | Scope assignments
|
|
7
|
+
* @param props - Component props
|
|
8
|
+
* @returns User Scopes tab component
|
|
9
|
+
*/
|
|
10
|
+
export declare function UserScopesTab({ className }: UserScopesTabProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
//# sourceMappingURL=user_scopes_tab.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user_scopes_tab.d.ts","sourceRoot":"","sources":["../../../../../src/components/layouts/user_management/components/user_scopes_tab.tsx"],"names":[],"mappings":"AAoDA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AA6EF;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,EAAE,SAAS,EAAE,EAAE,kBAAkB,2CA8f9D"}
|