@loom-framework/core 0.1.0-alpha.151 → 0.1.0-alpha.152
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/builtin-skills/app-skill/SKILL.md +15 -5
- package/builtin-skills/app-skill/references/auth.md +23 -19
- package/builtin-skills/app-skill/references/evolution.md +90 -0
- package/builtin-skills/app-skill/references/process-builder.md +140 -0
- package/builtin-skills/app-skill/references/process-metrics.md +93 -0
- package/builtin-skills/app-skill/references/process.md +222 -0
- package/builtin-skills/loom/SKILL.md +9 -7
- package/dist/backend/index.d.ts +4 -0
- package/dist/backend/index.d.ts.map +1 -1
- package/dist/backend/index.js +52 -2
- package/dist/backend/index.js.map +1 -1
- package/dist/backend/process/engine.d.ts +84 -0
- package/dist/backend/process/engine.d.ts.map +1 -0
- package/dist/backend/process/engine.js +511 -0
- package/dist/backend/process/engine.js.map +1 -0
- package/dist/backend/process/index.d.ts +7 -0
- package/dist/backend/process/index.d.ts.map +1 -0
- package/dist/backend/process/index.js +6 -0
- package/dist/backend/process/index.js.map +1 -0
- package/dist/backend/process/logger.d.ts +30 -0
- package/dist/backend/process/logger.d.ts.map +1 -0
- package/dist/backend/process/logger.js +132 -0
- package/dist/backend/process/logger.js.map +1 -0
- package/dist/backend/process/queue.d.ts +31 -0
- package/dist/backend/process/queue.d.ts.map +1 -0
- package/dist/backend/process/queue.js +80 -0
- package/dist/backend/process/queue.js.map +1 -0
- package/dist/backend/process/registry.d.ts +25 -0
- package/dist/backend/process/registry.d.ts.map +1 -0
- package/dist/backend/process/registry.js +98 -0
- package/dist/backend/process/registry.js.map +1 -0
- package/dist/backend/process/trigger.d.ts +29 -0
- package/dist/backend/process/trigger.d.ts.map +1 -0
- package/dist/backend/process/trigger.js +108 -0
- package/dist/backend/process/trigger.js.map +1 -0
- package/dist/backend/routes/auth-routes.d.ts +5 -0
- package/dist/backend/routes/auth-routes.d.ts.map +1 -1
- package/dist/backend/routes/auth-routes.js +221 -1
- package/dist/backend/routes/auth-routes.js.map +1 -1
- package/dist/backend/routes/index.d.ts +2 -0
- package/dist/backend/routes/index.d.ts.map +1 -1
- package/dist/backend/routes/index.js +1 -0
- package/dist/backend/routes/index.js.map +1 -1
- package/dist/backend/routes/process-routes.d.ts +15 -0
- package/dist/backend/routes/process-routes.d.ts.map +1 -0
- package/dist/backend/routes/process-routes.js +237 -0
- package/dist/backend/routes/process-routes.js.map +1 -0
- package/dist/cli/commands/auth.d.ts.map +1 -1
- package/dist/cli/commands/auth.js +30 -22
- package/dist/cli/commands/auth.js.map +1 -1
- package/dist/cli/commands/data.d.ts.map +1 -1
- package/dist/cli/commands/data.js +36 -47
- package/dist/cli/commands/data.js.map +1 -1
- package/dist/cli/commands/generate-system-settings.d.ts +3 -2
- package/dist/cli/commands/generate-system-settings.d.ts.map +1 -1
- package/dist/cli/commands/generate-system-settings.js +50 -7
- package/dist/cli/commands/generate-system-settings.js.map +1 -1
- package/dist/cli/commands/init.js +1 -1
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/process.d.ts +8 -0
- package/dist/cli/commands/process.d.ts.map +1 -0
- package/dist/cli/commands/process.js +444 -0
- package/dist/cli/commands/process.js.map +1 -0
- package/dist/cli/commands/role.d.ts +5 -2
- package/dist/cli/commands/role.d.ts.map +1 -1
- package/dist/cli/commands/role.js +145 -18
- package/dist/cli/commands/role.js.map +1 -1
- package/dist/cli/commands/user-cmd.d.ts.map +1 -1
- package/dist/cli/commands/user-cmd.js +41 -50
- package/dist/cli/commands/user-cmd.js.map +1 -1
- package/dist/cli/generators/capability-generator.d.ts.map +1 -1
- package/dist/cli/generators/capability-generator.js +121 -6
- package/dist/cli/generators/capability-generator.js.map +1 -1
- package/dist/cli/helpers/app-tsx-wiring.d.ts.map +1 -1
- package/dist/cli/helpers/app-tsx-wiring.js +21 -14
- package/dist/cli/helpers/app-tsx-wiring.js.map +1 -1
- package/dist/cli/helpers/auth-client.d.ts +19 -0
- package/dist/cli/helpers/auth-client.d.ts.map +1 -0
- package/dist/cli/helpers/auth-client.js +42 -0
- package/dist/cli/helpers/auth-client.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/templates/index.d.ts +1 -0
- package/dist/cli/templates/index.d.ts.map +1 -1
- package/dist/cli/templates/index.js +1 -0
- package/dist/cli/templates/index.js.map +1 -1
- package/dist/cli/templates/process-management-page.d.ts +8 -0
- package/dist/cli/templates/process-management-page.d.ts.map +1 -0
- package/dist/cli/templates/process-management-page.js +824 -0
- package/dist/cli/templates/process-management-page.js.map +1 -0
- package/dist/cli/templates/user-management-page.d.ts +2 -1
- package/dist/cli/templates/user-management-page.d.ts.map +1 -1
- package/dist/cli/templates/user-management-page.js +321 -62
- package/dist/cli/templates/user-management-page.js.map +1 -1
- package/dist/config.d.ts +43 -23
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -2
- package/dist/config.js.map +1 -1
- package/dist/types/auth.d.ts +0 -2
- package/dist/types/auth.d.ts.map +1 -1
- package/dist/types/config.d.ts +2 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/process.d.ts +106 -0
- package/dist/types/process.d.ts.map +1 -0
- package/dist/types/process.js +5 -0
- package/dist/types/process.js.map +1 -0
- package/package.json +3 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process-management-page.js","sourceRoot":"","sources":["../../../src/cli/templates/process-management-page.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,UAAU,6BAA6B;IAC3C,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8yBR,CAAC;AACF,CAAC"}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* User Management page template
|
|
3
3
|
*
|
|
4
4
|
* Generates UserManagement page as a local source file.
|
|
5
|
-
*
|
|
5
|
+
* Contains Users tab and Roles tab with full CRUD.
|
|
6
|
+
* Page imports useUsers/useRoles hooks from @loom-framework/frontend-antd
|
|
6
7
|
* and registers its own i18n keys via registerMessages().
|
|
7
8
|
*/
|
|
8
9
|
export declare function userManagementPageTemplate(): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user-management-page.d.ts","sourceRoot":"","sources":["../../../src/cli/templates/user-management-page.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"user-management-page.d.ts","sourceRoot":"","sources":["../../../src/cli/templates/user-management-page.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,wBAAgB,0BAA0B,IAAI,MAAM,CAqenD"}
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
* User Management page template
|
|
3
3
|
*
|
|
4
4
|
* Generates UserManagement page as a local source file.
|
|
5
|
-
*
|
|
5
|
+
* Contains Users tab and Roles tab with full CRUD.
|
|
6
|
+
* Page imports useUsers/useRoles hooks from @loom-framework/frontend-antd
|
|
6
7
|
* and registers its own i18n keys via registerMessages().
|
|
7
8
|
*/
|
|
8
9
|
export function userManagementPageTemplate() {
|
|
9
|
-
return `import React, { useState } from 'react';
|
|
10
|
-
import { Card, Table, Button, Modal, Form, Input, Select, Space, message, Popconfirm, Tag, Typography, theme, Breadcrumb, Flex } from 'antd';
|
|
11
|
-
import { PlusOutlined, EditOutlined, DeleteOutlined, UserOutlined, HomeOutlined } from '@ant-design/icons';
|
|
12
|
-
import { useLocale, useAppShell, useUsers, registerMessages } from '@loom-framework/frontend-antd';
|
|
10
|
+
return `import React, { useState, useCallback, useEffect } from 'react';
|
|
11
|
+
import { Card, Table, Button, Modal, Form, Input, Select, Space, message, Popconfirm, Tag, Typography, theme, Breadcrumb, Flex, Tabs, Descriptions } from 'antd';
|
|
12
|
+
import { PlusOutlined, EditOutlined, DeleteOutlined, UserOutlined, HomeOutlined, SafetyOutlined } from '@ant-design/icons';
|
|
13
|
+
import { useLocale, useAppShell, useUsers, useRoles, registerMessages } from '@loom-framework/frontend-antd';
|
|
13
14
|
|
|
14
15
|
registerMessages('zh-CN', {
|
|
15
16
|
'user.title': '用户管理',
|
|
@@ -32,6 +33,28 @@ registerMessages('zh-CN', {
|
|
|
32
33
|
'user.passwordRequired': '请输入密码',
|
|
33
34
|
'user.passwordMin': '密码至少8位',
|
|
34
35
|
'user.leaveBlankToKeep': '留空则保持不变',
|
|
36
|
+
'user.permissions': '权限',
|
|
37
|
+
'user.users': '用户',
|
|
38
|
+
'user.roles': '角色',
|
|
39
|
+
'user.addRole': '添加角色',
|
|
40
|
+
'user.editRole': '编辑角色',
|
|
41
|
+
'user.roleName': '角色名称',
|
|
42
|
+
'user.roleNameInvalid': '角色名必须以小写字母开头,只能包含小写字母、数字、连字符或下划线(2-32位)',
|
|
43
|
+
'user.roleExists': '角色名已存在',
|
|
44
|
+
'user.roleCreated': '角色已创建',
|
|
45
|
+
'user.roleCreateFailed': '创建角色失败',
|
|
46
|
+
'user.roleUpdated': '角色已更新',
|
|
47
|
+
'user.roleUpdateFailed': '更新角色失败',
|
|
48
|
+
'user.roleDeleted': '角色已删除',
|
|
49
|
+
'user.deleteRoleConfirm': '确认删除角色 "{name}"?',
|
|
50
|
+
'user.deleteRoleInUse': '角色 "{name}" 下有 {count} 位用户,无法删除',
|
|
51
|
+
'user.cannotDeleteLastAdmin': '无法删除最后一个管理员角色',
|
|
52
|
+
'user.model': '模型',
|
|
53
|
+
'user.level': '级别',
|
|
54
|
+
'user.wildcard': '全部模型',
|
|
55
|
+
'user.permissionLevel': '权限级别',
|
|
56
|
+
'user.defaultPermissions': '默认权限',
|
|
57
|
+
'user.namePlaceholder': '例如 editor',
|
|
35
58
|
});
|
|
36
59
|
|
|
37
60
|
registerMessages('en-US', {
|
|
@@ -55,36 +78,101 @@ registerMessages('en-US', {
|
|
|
55
78
|
'user.passwordRequired': 'Password is required',
|
|
56
79
|
'user.passwordMin': 'Password must be at least 8 characters',
|
|
57
80
|
'user.leaveBlankToKeep': 'Leave blank to keep current',
|
|
81
|
+
'user.permissions': 'Permissions',
|
|
82
|
+
'user.users': 'Users',
|
|
83
|
+
'user.roles': 'Roles',
|
|
84
|
+
'user.addRole': 'Add Role',
|
|
85
|
+
'user.editRole': 'Edit Role',
|
|
86
|
+
'user.roleName': 'Role Name',
|
|
87
|
+
'user.roleNameInvalid': 'Role name must start with a lowercase letter and contain only lowercase letters, digits, hyphens, or underscores (2-32 chars)',
|
|
88
|
+
'user.roleExists': 'Role name already exists',
|
|
89
|
+
'user.roleCreated': 'Role created',
|
|
90
|
+
'user.roleCreateFailed': 'Failed to create role',
|
|
91
|
+
'user.roleUpdated': 'Role updated',
|
|
92
|
+
'user.roleUpdateFailed': 'Failed to update role',
|
|
93
|
+
'user.roleDeleted': 'Role deleted',
|
|
94
|
+
'user.deleteRoleConfirm': 'Delete role "{name}"?',
|
|
95
|
+
'user.deleteRoleInUse': 'Role "{name}" has {count} user(s), cannot delete',
|
|
96
|
+
'user.cannotDeleteLastAdmin': 'Cannot delete the last admin role',
|
|
97
|
+
'user.model': 'Model',
|
|
98
|
+
'user.level': 'Level',
|
|
99
|
+
'user.wildcard': 'All Models',
|
|
100
|
+
'user.permissionLevel': 'Permission Level',
|
|
101
|
+
'user.defaultPermissions': 'Default Permissions',
|
|
102
|
+
'user.namePlaceholder': 'e.g. editor',
|
|
58
103
|
});
|
|
59
104
|
|
|
60
|
-
const
|
|
105
|
+
const LEVEL_COLORS: Record<string, string> = {
|
|
61
106
|
admin: 'red',
|
|
62
|
-
|
|
63
|
-
|
|
107
|
+
write: 'blue',
|
|
108
|
+
read: 'green',
|
|
109
|
+
none: 'default',
|
|
64
110
|
};
|
|
65
111
|
|
|
66
|
-
const
|
|
67
|
-
{ value: '
|
|
68
|
-
{ value: '
|
|
69
|
-
{ value: '
|
|
112
|
+
const LEVEL_OPTIONS = [
|
|
113
|
+
{ value: 'none', label: 'None' },
|
|
114
|
+
{ value: 'read', label: 'Read' },
|
|
115
|
+
{ value: 'write', label: 'Write' },
|
|
116
|
+
{ value: 'admin', label: 'Admin' },
|
|
70
117
|
];
|
|
71
118
|
|
|
72
119
|
export default function UserManagementPage(): React.ReactElement {
|
|
73
120
|
const { token } = theme.useToken();
|
|
74
121
|
const { t } = useLocale();
|
|
75
122
|
const { breadcrumbs, onNavClick } = useAppShell();
|
|
123
|
+
const [activeTab, setActiveTab] = useState('users');
|
|
124
|
+
const [addUserModalOpen, setAddUserModalOpen] = useState(false);
|
|
125
|
+
const [addRoleModalOpen, setAddRoleModalOpen] = useState(false);
|
|
126
|
+
|
|
127
|
+
const handleTabChange = (key: string) => {
|
|
128
|
+
setActiveTab(key);
|
|
129
|
+
setAddUserModalOpen(false);
|
|
130
|
+
setAddRoleModalOpen(false);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
|
|
135
|
+
<Flex justify="space-between" align="center" style={{ marginBottom: 12 }}>
|
|
136
|
+
<Breadcrumb items={[{ title: <HomeOutlined onClick={() => onNavClick?.('')} style={{ cursor: 'pointer' }} /> }, ...(breadcrumbs || []).map(b => ({ title: b.path ? <a onClick={() => onNavClick?.(b.path!)}>{b.title}</a> : b.title }))]} />
|
|
137
|
+
</Flex>
|
|
138
|
+
<Tabs
|
|
139
|
+
activeKey={activeTab}
|
|
140
|
+
onChange={handleTabChange}
|
|
141
|
+
destroyInactiveTabPane
|
|
142
|
+
animated={{ inkBar: true, tabPane: false }}
|
|
143
|
+
tabBarStyle={{ marginLeft: 12 }}
|
|
144
|
+
tabBarExtraContent={activeTab === 'users'
|
|
145
|
+
? <Button type="primary" icon={<PlusOutlined />} onClick={() => setAddUserModalOpen(true)}>{t('user.addUser') || 'Add User'}</Button>
|
|
146
|
+
: <Button type="primary" icon={<SafetyOutlined />} onClick={() => setAddRoleModalOpen(true)}>{t('user.addRole') || 'Add Role'}</Button>
|
|
147
|
+
}
|
|
148
|
+
items={[
|
|
149
|
+
{ key: 'users', label: t('user.users') || 'Users', children: <div style={{ animation: 'loom-page-enter 0.25s ease both' }}><UsersTab addUserModalOpen={addUserModalOpen} onAddUserModalClose={() => setAddUserModalOpen(false)} /></div> },
|
|
150
|
+
{ key: 'roles', label: t('user.roles') || 'Roles', children: <div style={{ animation: 'loom-page-enter 0.25s ease both' }}><RolesTab addRoleModalOpen={addRoleModalOpen} onAddRoleModalClose={() => setAddRoleModalOpen(false)} /></div> },
|
|
151
|
+
]}
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function UsersTab({ addUserModalOpen, onAddUserModalClose }: { addUserModalOpen: boolean; onAddUserModalClose: () => void }): React.ReactElement {
|
|
158
|
+
const { token } = theme.useToken();
|
|
159
|
+
const { t } = useLocale();
|
|
76
160
|
const { users, loading, addUser, deleteUser, updateUser } = useUsers();
|
|
77
|
-
const
|
|
161
|
+
const { rolesData } = useRoles();
|
|
78
162
|
const [editModalOpen, setEditModalOpen] = useState(false);
|
|
79
|
-
const [editingUser, setEditingUser] = useState<{
|
|
163
|
+
const [editingUser, setEditingUser] = useState<{ userId: string; username: string; role: string } | null>(null);
|
|
80
164
|
const [addForm] = Form.useForm();
|
|
81
165
|
const [editForm] = Form.useForm();
|
|
82
166
|
|
|
167
|
+
const roleOptions = rolesData
|
|
168
|
+
? rolesData.roles.map(r => ({ value: r.role, label: r.role }))
|
|
169
|
+
: [];
|
|
170
|
+
|
|
83
171
|
const handleAdd = async (values: { username: string; password: string; role: string }) => {
|
|
84
172
|
const result = await addUser(values.username, values.password, values.role);
|
|
85
173
|
if (result) {
|
|
86
174
|
message.success(t('user.userAdded') || 'User added');
|
|
87
|
-
|
|
175
|
+
onAddUserModalClose();
|
|
88
176
|
addForm.resetFields();
|
|
89
177
|
} else {
|
|
90
178
|
message.error(t('user.userAddFailed') || 'Failed to add user');
|
|
@@ -95,7 +183,7 @@ export default function UserManagementPage(): React.ReactElement {
|
|
|
95
183
|
if (!editingUser) return;
|
|
96
184
|
const updates: Partial<{ username: string; password: string; role: string }> = { role: values.role };
|
|
97
185
|
if (values.password) updates.password = values.password;
|
|
98
|
-
const result = await updateUser(editingUser.
|
|
186
|
+
const result = await updateUser(editingUser.userId, updates);
|
|
99
187
|
if (result) {
|
|
100
188
|
message.success(t('user.userUpdated') || 'User updated');
|
|
101
189
|
setEditModalOpen(false);
|
|
@@ -117,7 +205,7 @@ export default function UserManagementPage(): React.ReactElement {
|
|
|
117
205
|
dataIndex: 'role',
|
|
118
206
|
key: 'role',
|
|
119
207
|
width: 120,
|
|
120
|
-
render: (role: string) => <Tag color={
|
|
208
|
+
render: (role: string) => <Tag color={LEVEL_COLORS[role] || 'default'}>{role}</Tag>,
|
|
121
209
|
},
|
|
122
210
|
{
|
|
123
211
|
title: t('user.createdAt') || 'Created At',
|
|
@@ -130,7 +218,7 @@ export default function UserManagementPage(): React.ReactElement {
|
|
|
130
218
|
title: t('user.action') || 'Action',
|
|
131
219
|
key: 'action',
|
|
132
220
|
width: 120,
|
|
133
|
-
render: (_: unknown, record: {
|
|
221
|
+
render: (_: unknown, record: { userId: string; username: string; role: string }) => (
|
|
134
222
|
<Space size={0}>
|
|
135
223
|
<Button type="text" size="small" icon={<EditOutlined />} onClick={() => {
|
|
136
224
|
setEditingUser(record);
|
|
@@ -140,7 +228,7 @@ export default function UserManagementPage(): React.ReactElement {
|
|
|
140
228
|
<Popconfirm
|
|
141
229
|
title={t('user.deleteConfirm')?.replace('{name}', record.username) || \`Delete user "\${record.username}"?\`}
|
|
142
230
|
onConfirm={async () => {
|
|
143
|
-
const ok = await deleteUser(record.
|
|
231
|
+
const ok = await deleteUser(record.userId);
|
|
144
232
|
if (ok) message.success(t('user.userDeleted') || 'User deleted');
|
|
145
233
|
else message.error(t('user.userDeleteFailed') || 'Failed to delete user');
|
|
146
234
|
}}
|
|
@@ -153,59 +241,29 @@ export default function UserManagementPage(): React.ReactElement {
|
|
|
153
241
|
];
|
|
154
242
|
|
|
155
243
|
return (
|
|
156
|
-
|
|
157
|
-
<
|
|
158
|
-
<
|
|
159
|
-
</Flex>
|
|
160
|
-
<Card
|
|
161
|
-
title={
|
|
162
|
-
<Space>
|
|
163
|
-
<UserOutlined />
|
|
164
|
-
<span>{t('user.title') || 'User Management'}</span>
|
|
165
|
-
</Space>
|
|
166
|
-
}
|
|
167
|
-
extra={
|
|
168
|
-
<Button type="primary" size="small" icon={<PlusOutlined />} onClick={() => setAddModalOpen(true)}>
|
|
169
|
-
{t('user.addUser') || 'Add User'}
|
|
170
|
-
</Button>
|
|
171
|
-
}
|
|
172
|
-
>
|
|
173
|
-
<Table
|
|
174
|
-
dataSource={users}
|
|
175
|
-
columns={columns}
|
|
176
|
-
rowKey="id"
|
|
177
|
-
loading={loading}
|
|
178
|
-
pagination={false}
|
|
179
|
-
size="small"
|
|
180
|
-
/>
|
|
244
|
+
<>
|
|
245
|
+
<Card>
|
|
246
|
+
<Table dataSource={users} columns={columns} rowKey="userId" loading={loading} pagination={false} size="small" />
|
|
181
247
|
</Card>
|
|
182
248
|
|
|
183
249
|
<Modal
|
|
184
250
|
title={t('user.addUser') || 'Add User'}
|
|
185
|
-
open={
|
|
251
|
+
open={addUserModalOpen}
|
|
186
252
|
onOk={() => addForm.submit()}
|
|
187
|
-
onCancel={() => {
|
|
253
|
+
onCancel={() => { onAddUserModalClose(); addForm.resetFields(); }}
|
|
188
254
|
okText={t('common.save') || 'Save'}
|
|
189
255
|
cancelText={t('common.cancel') || 'Cancel'}
|
|
190
|
-
|
|
256
|
+
destroyOnHidden
|
|
191
257
|
>
|
|
192
258
|
<Form form={addForm} onFinish={handleAdd} layout="vertical" style={{ marginTop: token.marginMD }}>
|
|
193
|
-
<Form.Item
|
|
194
|
-
name="username"
|
|
195
|
-
label={t('user.username') || 'Username'}
|
|
196
|
-
rules={[{ required: true, message: t('user.usernameRequired') || 'Username is required' }]}
|
|
197
|
-
>
|
|
259
|
+
<Form.Item name="username" label={t('user.username') || 'Username'} rules={[{ required: true, message: t('user.usernameRequired') || 'Username is required' }]}>
|
|
198
260
|
<Input />
|
|
199
261
|
</Form.Item>
|
|
200
|
-
<Form.Item
|
|
201
|
-
name="password"
|
|
202
|
-
label={t('user.password') || 'Password'}
|
|
203
|
-
rules={[{ required: true, min: 8, message: t('user.passwordMin') || 'Password must be at least 8 characters' }]}
|
|
204
|
-
>
|
|
262
|
+
<Form.Item name="password" label={t('user.password') || 'Password'} rules={[{ required: true, min: 8, message: t('user.passwordMin') || 'Password must be at least 8 characters' }]}>
|
|
205
263
|
<Input.Password />
|
|
206
264
|
</Form.Item>
|
|
207
|
-
<Form.Item name="role" label={t('user.role') || 'Role'} initialValue=
|
|
208
|
-
<Select options={
|
|
265
|
+
<Form.Item name="role" label={t('user.role') || 'Role'} initialValue={roleOptions.length > 0 ? roleOptions[roleOptions.length - 1]?.value : undefined}>
|
|
266
|
+
<Select options={roleOptions} />
|
|
209
267
|
</Form.Item>
|
|
210
268
|
</Form>
|
|
211
269
|
</Modal>
|
|
@@ -217,18 +275,219 @@ export default function UserManagementPage(): React.ReactElement {
|
|
|
217
275
|
onCancel={() => { setEditModalOpen(false); setEditingUser(null); }}
|
|
218
276
|
okText={t('common.save') || 'Save'}
|
|
219
277
|
cancelText={t('common.cancel') || 'Cancel'}
|
|
220
|
-
|
|
278
|
+
destroyOnHidden
|
|
221
279
|
>
|
|
222
280
|
<Form form={editForm} onFinish={handleEdit} layout="vertical" style={{ marginTop: token.marginMD }}>
|
|
223
281
|
<Form.Item name="role" label={t('user.role') || 'Role'}>
|
|
224
|
-
<Select options={
|
|
282
|
+
<Select options={roleOptions} />
|
|
225
283
|
</Form.Item>
|
|
226
284
|
<Form.Item name="password" label={t('user.newPassword') || 'New Password'}>
|
|
227
285
|
<Input.Password placeholder={t('user.leaveBlankToKeep') || 'Leave blank to keep current'} />
|
|
228
286
|
</Form.Item>
|
|
229
287
|
</Form>
|
|
230
288
|
</Modal>
|
|
231
|
-
|
|
289
|
+
</>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function RolesTab({ addRoleModalOpen, onAddRoleModalClose }: { addRoleModalOpen: boolean; onAddRoleModalClose: () => void }): React.ReactElement {
|
|
294
|
+
const { token } = theme.useToken();
|
|
295
|
+
const { t } = useLocale();
|
|
296
|
+
const { users } = useUsers();
|
|
297
|
+
const { rolesData, loading, createRole, updateRole, deleteRole, refresh } = useRoles();
|
|
298
|
+
const [editModalOpen, setEditModalOpen] = useState(false);
|
|
299
|
+
const [editingRole, setEditingRole] = useState<{ role: string; permissions: Array<{ model: string; level: string }> } | null>(null);
|
|
300
|
+
const [roleForm] = Form.useForm();
|
|
301
|
+
const [permRows, setPermRows] = useState<Array<{ model: string; level: string }>>([]);
|
|
302
|
+
|
|
303
|
+
const modelOptions = rolesData
|
|
304
|
+
? [...rolesData.models.map(m => ({ value: m, label: m })), { value: '*', label: t('user.wildcard') || 'All Models' }]
|
|
305
|
+
: [];
|
|
306
|
+
|
|
307
|
+
const buildPermRows = useCallback((permissions: Array<{ model: string; level: string }>, models: string[]) => {
|
|
308
|
+
const rows: Array<{ model: string; level: string }> = [];
|
|
309
|
+
const covered = new Set(permissions.map(p => p.model));
|
|
310
|
+
for (const m of models) {
|
|
311
|
+
const existing = permissions.find(p => p.model === m);
|
|
312
|
+
rows.push({ model: m, level: existing?.level || 'none' });
|
|
313
|
+
}
|
|
314
|
+
if (!covered.has('*')) {
|
|
315
|
+
const wildcard = permissions.find(p => p.model === '*');
|
|
316
|
+
rows.push({ model: '*', level: wildcard?.level || 'none' });
|
|
317
|
+
} else {
|
|
318
|
+
rows.push({ model: '*', level: permissions.find(p => p.model === '*')?.level || 'none' });
|
|
319
|
+
}
|
|
320
|
+
return rows;
|
|
321
|
+
}, []);
|
|
322
|
+
|
|
323
|
+
const handleAddRole = async (values: { role: string }) => {
|
|
324
|
+
const permissions = permRows.filter(p => p.level !== 'none');
|
|
325
|
+
const result = await createRole(values.role, permissions);
|
|
326
|
+
if (result) {
|
|
327
|
+
message.success(t('user.roleCreated') || 'Role created');
|
|
328
|
+
onAddRoleModalClose();
|
|
329
|
+
roleForm.resetFields();
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const handleEditRole = async () => {
|
|
334
|
+
if (!editingRole) return;
|
|
335
|
+
const newName = roleForm.getFieldValue('role') as string;
|
|
336
|
+
const permissions = permRows.filter(p => p.level !== 'none');
|
|
337
|
+
const updates: { role?: string; permissions?: Array<{ model: string; level: string }> } = {};
|
|
338
|
+
if (newName !== editingRole.role) updates.role = newName;
|
|
339
|
+
updates.permissions = permissions;
|
|
340
|
+
const result = await updateRole(editingRole.role, updates);
|
|
341
|
+
if (result) {
|
|
342
|
+
message.success(t('user.roleUpdated') || 'Role updated');
|
|
343
|
+
setEditModalOpen(false);
|
|
344
|
+
setEditingRole(null);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const handleDelete = async (roleName: string) => {
|
|
349
|
+
const result = await deleteRole(roleName);
|
|
350
|
+
if (result.success) {
|
|
351
|
+
message.success(t('user.roleDeleted') || 'Role deleted');
|
|
352
|
+
} else if (result.affectedUsers && result.affectedUsers.length > 0) {
|
|
353
|
+
message.error(t('user.deleteRoleInUse')?.replace('{name}', roleName).replace('{count}', String(result.affectedUsers.length)) || \`Role "\${roleName}" has users, cannot delete\`);
|
|
354
|
+
} else {
|
|
355
|
+
message.error(result.error || t('user.cannotDeleteLastAdmin') || 'Cannot delete role');
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const openAddModal = () => {
|
|
360
|
+
roleForm.resetFields();
|
|
361
|
+
const initPerms = rolesData ? buildPermRows([], rolesData.models) : [];
|
|
362
|
+
setPermRows(initPerms.map(p => ({ ...p, level: 'read' })));
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
useEffect(() => {
|
|
366
|
+
if (addRoleModalOpen) openAddModal();
|
|
367
|
+
}, [addRoleModalOpen]);
|
|
368
|
+
|
|
369
|
+
const openEditModal = (roleInfo: { role: string; permissions: Array<{ model: string; level: string }> }) => {
|
|
370
|
+
setEditingRole(roleInfo);
|
|
371
|
+
roleForm.setFieldsValue({ role: roleInfo.role });
|
|
372
|
+
const rows = rolesData ? buildPermRows(roleInfo.permissions, rolesData.models) : [];
|
|
373
|
+
setPermRows(rows);
|
|
374
|
+
setEditModalOpen(true);
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const updatePermLevel = (model: string, level: string) => {
|
|
378
|
+
setPermRows(prev => prev.map(p => p.model === model ? { ...p, level } : p));
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const roleColumns = [
|
|
382
|
+
{
|
|
383
|
+
title: t('user.roleName') || 'Role Name',
|
|
384
|
+
dataIndex: 'role',
|
|
385
|
+
key: 'role',
|
|
386
|
+
width: 150,
|
|
387
|
+
render: (role: string) => <Tag color={LEVEL_COLORS[role] || 'default'} style={{ fontSize: token.fontSizeSM }}>{role}</Tag>,
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
title: t('user.permissions') || 'Permissions',
|
|
391
|
+
key: 'permissions',
|
|
392
|
+
render: (_: unknown, record: { role: string; permissions: Array<{ model: string; level: string }> }) => (
|
|
393
|
+
<Space size={4} wrap>
|
|
394
|
+
{record.permissions.map(p => (
|
|
395
|
+
<Tag key={\`\${p.model}-\${p.level}\`} color={LEVEL_COLORS[p.level] || 'default'}>
|
|
396
|
+
{p.model === '*' ? (t('user.wildcard') || 'All') : p.model}: {p.level}
|
|
397
|
+
</Tag>
|
|
398
|
+
))}
|
|
399
|
+
</Space>
|
|
400
|
+
),
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
title: t('user.users') || 'Users',
|
|
404
|
+
key: 'userCount',
|
|
405
|
+
width: 80,
|
|
406
|
+
render: (_: unknown, record: { role: string }) => users.filter(u => u.role === record.role).length,
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
title: t('user.action') || 'Action',
|
|
410
|
+
key: 'action',
|
|
411
|
+
width: 100,
|
|
412
|
+
render: (_: unknown, record: { role: string }) => (
|
|
413
|
+
<Space size={0}>
|
|
414
|
+
<Button type="text" size="small" icon={<EditOutlined />} onClick={() => openEditModal(record as { role: string; permissions: Array<{ model: string; level: string }> })} />
|
|
415
|
+
<Popconfirm
|
|
416
|
+
title={t('user.deleteRoleConfirm')?.replace('{name}', record.role) || \`Delete role "\${record.role}"?\`}
|
|
417
|
+
onConfirm={() => handleDelete(record.role)}
|
|
418
|
+
>
|
|
419
|
+
<Button type="text" size="small" danger icon={<DeleteOutlined />} />
|
|
420
|
+
</Popconfirm>
|
|
421
|
+
</Space>
|
|
422
|
+
),
|
|
423
|
+
},
|
|
424
|
+
];
|
|
425
|
+
|
|
426
|
+
const permColumns = [
|
|
427
|
+
{
|
|
428
|
+
title: t('user.model') || 'Model',
|
|
429
|
+
dataIndex: 'model',
|
|
430
|
+
key: 'model',
|
|
431
|
+
width: 200,
|
|
432
|
+
render: (model: string) => model === '*' ? <Tag>{t('user.wildcard') || 'All Models'}</Tag> : model,
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
title: t('user.level') || 'Level',
|
|
436
|
+
dataIndex: 'level',
|
|
437
|
+
key: 'level',
|
|
438
|
+
width: 150,
|
|
439
|
+
render: (level: string, record: { model: string }) => (
|
|
440
|
+
<Select value={level} onChange={v => updatePermLevel(record.model, v)} options={LEVEL_OPTIONS} style={{ width: 120 }} />
|
|
441
|
+
),
|
|
442
|
+
},
|
|
443
|
+
];
|
|
444
|
+
|
|
445
|
+
return (
|
|
446
|
+
<>
|
|
447
|
+
<Card>
|
|
448
|
+
<Table dataSource={rolesData?.roles || []} columns={roleColumns} rowKey="role" loading={loading} pagination={false} size="small" />
|
|
449
|
+
</Card>
|
|
450
|
+
|
|
451
|
+
<Modal
|
|
452
|
+
title={t('user.addRole') || 'Add Role'}
|
|
453
|
+
open={addRoleModalOpen}
|
|
454
|
+
onOk={() => roleForm.submit()}
|
|
455
|
+
onCancel={() => { onAddRoleModalClose(); roleForm.resetFields(); }}
|
|
456
|
+
okText={t('common.save') || 'Save'}
|
|
457
|
+
cancelText={t('common.cancel') || 'Cancel'}
|
|
458
|
+
destroyOnHidden
|
|
459
|
+
width={560}
|
|
460
|
+
>
|
|
461
|
+
<Form form={roleForm} onFinish={handleAddRole} layout="vertical" style={{ marginTop: token.marginMD }}>
|
|
462
|
+
<Form.Item name="role" label={t('user.roleName') || 'Role Name'} rules={[{ required: true, message: t('user.roleNameInvalid') || 'Invalid role name' }]}>
|
|
463
|
+
<Input placeholder={t('user.namePlaceholder') || 'e.g. editor'} />
|
|
464
|
+
</Form.Item>
|
|
465
|
+
<Form.Item label={t('user.permissionLevel') || 'Permission Level'}>
|
|
466
|
+
<Table dataSource={permRows} columns={permColumns} rowKey="model" pagination={false} size="small" />
|
|
467
|
+
</Form.Item>
|
|
468
|
+
</Form>
|
|
469
|
+
</Modal>
|
|
470
|
+
|
|
471
|
+
<Modal
|
|
472
|
+
title={t('user.editRole') || 'Edit Role'}
|
|
473
|
+
open={editModalOpen}
|
|
474
|
+
onOk={handleEditRole}
|
|
475
|
+
onCancel={() => { setEditModalOpen(false); setEditingRole(null); }}
|
|
476
|
+
okText={t('common.save') || 'Save'}
|
|
477
|
+
cancelText={t('common.cancel') || 'Cancel'}
|
|
478
|
+
destroyOnHidden
|
|
479
|
+
width={560}
|
|
480
|
+
>
|
|
481
|
+
<Form form={roleForm} layout="vertical" style={{ marginTop: token.marginMD }}>
|
|
482
|
+
<Form.Item name="role" label={t('user.roleName') || 'Role Name'} rules={[{ required: true }]}>
|
|
483
|
+
<Input />
|
|
484
|
+
</Form.Item>
|
|
485
|
+
<Form.Item label={t('user.permissionLevel') || 'Permission Level'}>
|
|
486
|
+
<Table dataSource={permRows} columns={permColumns} rowKey="model" pagination={false} size="small" />
|
|
487
|
+
</Form.Item>
|
|
488
|
+
</Form>
|
|
489
|
+
</Modal>
|
|
490
|
+
</>
|
|
232
491
|
);
|
|
233
492
|
}
|
|
234
493
|
`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user-management-page.js","sourceRoot":"","sources":["../../../src/cli/templates/user-management-page.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"user-management-page.js","sourceRoot":"","sources":["../../../src/cli/templates/user-management-page.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,UAAU,0BAA0B;IACxC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmeR,CAAC;AACF,CAAC"}
|