@loom-framework/core 0.1.0-alpha.151 → 0.1.0-alpha.153

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.
Files changed (110) hide show
  1. package/builtin-skills/app-skill/SKILL.md +15 -5
  2. package/builtin-skills/app-skill/references/auth.md +23 -19
  3. package/builtin-skills/app-skill/references/evolution.md +90 -0
  4. package/builtin-skills/app-skill/references/process-builder.md +140 -0
  5. package/builtin-skills/app-skill/references/process-metrics.md +93 -0
  6. package/builtin-skills/app-skill/references/process.md +222 -0
  7. package/builtin-skills/loom/SKILL.md +9 -7
  8. package/dist/backend/index.d.ts +4 -0
  9. package/dist/backend/index.d.ts.map +1 -1
  10. package/dist/backend/index.js +52 -2
  11. package/dist/backend/index.js.map +1 -1
  12. package/dist/backend/process/engine.d.ts +84 -0
  13. package/dist/backend/process/engine.d.ts.map +1 -0
  14. package/dist/backend/process/engine.js +511 -0
  15. package/dist/backend/process/engine.js.map +1 -0
  16. package/dist/backend/process/index.d.ts +7 -0
  17. package/dist/backend/process/index.d.ts.map +1 -0
  18. package/dist/backend/process/index.js +6 -0
  19. package/dist/backend/process/index.js.map +1 -0
  20. package/dist/backend/process/logger.d.ts +30 -0
  21. package/dist/backend/process/logger.d.ts.map +1 -0
  22. package/dist/backend/process/logger.js +132 -0
  23. package/dist/backend/process/logger.js.map +1 -0
  24. package/dist/backend/process/queue.d.ts +31 -0
  25. package/dist/backend/process/queue.d.ts.map +1 -0
  26. package/dist/backend/process/queue.js +80 -0
  27. package/dist/backend/process/queue.js.map +1 -0
  28. package/dist/backend/process/registry.d.ts +25 -0
  29. package/dist/backend/process/registry.d.ts.map +1 -0
  30. package/dist/backend/process/registry.js +98 -0
  31. package/dist/backend/process/registry.js.map +1 -0
  32. package/dist/backend/process/trigger.d.ts +29 -0
  33. package/dist/backend/process/trigger.d.ts.map +1 -0
  34. package/dist/backend/process/trigger.js +108 -0
  35. package/dist/backend/process/trigger.js.map +1 -0
  36. package/dist/backend/routes/auth-routes.d.ts +5 -0
  37. package/dist/backend/routes/auth-routes.d.ts.map +1 -1
  38. package/dist/backend/routes/auth-routes.js +221 -1
  39. package/dist/backend/routes/auth-routes.js.map +1 -1
  40. package/dist/backend/routes/index.d.ts +2 -0
  41. package/dist/backend/routes/index.d.ts.map +1 -1
  42. package/dist/backend/routes/index.js +1 -0
  43. package/dist/backend/routes/index.js.map +1 -1
  44. package/dist/backend/routes/process-routes.d.ts +15 -0
  45. package/dist/backend/routes/process-routes.d.ts.map +1 -0
  46. package/dist/backend/routes/process-routes.js +237 -0
  47. package/dist/backend/routes/process-routes.js.map +1 -0
  48. package/dist/cli/commands/auth.d.ts.map +1 -1
  49. package/dist/cli/commands/auth.js +30 -22
  50. package/dist/cli/commands/auth.js.map +1 -1
  51. package/dist/cli/commands/data.d.ts.map +1 -1
  52. package/dist/cli/commands/data.js +36 -47
  53. package/dist/cli/commands/data.js.map +1 -1
  54. package/dist/cli/commands/generate-system-settings.d.ts +3 -2
  55. package/dist/cli/commands/generate-system-settings.d.ts.map +1 -1
  56. package/dist/cli/commands/generate-system-settings.js +50 -7
  57. package/dist/cli/commands/generate-system-settings.js.map +1 -1
  58. package/dist/cli/commands/init.js +2 -2
  59. package/dist/cli/commands/init.js.map +1 -1
  60. package/dist/cli/commands/process.d.ts +8 -0
  61. package/dist/cli/commands/process.d.ts.map +1 -0
  62. package/dist/cli/commands/process.js +444 -0
  63. package/dist/cli/commands/process.js.map +1 -0
  64. package/dist/cli/commands/role.d.ts +5 -2
  65. package/dist/cli/commands/role.d.ts.map +1 -1
  66. package/dist/cli/commands/role.js +145 -18
  67. package/dist/cli/commands/role.js.map +1 -1
  68. package/dist/cli/commands/user-cmd.d.ts.map +1 -1
  69. package/dist/cli/commands/user-cmd.js +41 -50
  70. package/dist/cli/commands/user-cmd.js.map +1 -1
  71. package/dist/cli/generators/capability-generator.d.ts.map +1 -1
  72. package/dist/cli/generators/capability-generator.js +121 -6
  73. package/dist/cli/generators/capability-generator.js.map +1 -1
  74. package/dist/cli/helpers/app-tsx-wiring.d.ts.map +1 -1
  75. package/dist/cli/helpers/app-tsx-wiring.js +21 -14
  76. package/dist/cli/helpers/app-tsx-wiring.js.map +1 -1
  77. package/dist/cli/helpers/auth-client.d.ts +19 -0
  78. package/dist/cli/helpers/auth-client.d.ts.map +1 -0
  79. package/dist/cli/helpers/auth-client.js +42 -0
  80. package/dist/cli/helpers/auth-client.js.map +1 -0
  81. package/dist/cli/index.d.ts.map +1 -1
  82. package/dist/cli/index.js +2 -0
  83. package/dist/cli/index.js.map +1 -1
  84. package/dist/cli/templates/index.d.ts +1 -0
  85. package/dist/cli/templates/index.d.ts.map +1 -1
  86. package/dist/cli/templates/index.js +1 -0
  87. package/dist/cli/templates/index.js.map +1 -1
  88. package/dist/cli/templates/process-management-page.d.ts +8 -0
  89. package/dist/cli/templates/process-management-page.d.ts.map +1 -0
  90. package/dist/cli/templates/process-management-page.js +824 -0
  91. package/dist/cli/templates/process-management-page.js.map +1 -0
  92. package/dist/cli/templates/user-management-page.d.ts +2 -1
  93. package/dist/cli/templates/user-management-page.d.ts.map +1 -1
  94. package/dist/cli/templates/user-management-page.js +321 -62
  95. package/dist/cli/templates/user-management-page.js.map +1 -1
  96. package/dist/config.d.ts +43 -23
  97. package/dist/config.d.ts.map +1 -1
  98. package/dist/config.js +8 -2
  99. package/dist/config.js.map +1 -1
  100. package/dist/types/auth.d.ts +0 -2
  101. package/dist/types/auth.d.ts.map +1 -1
  102. package/dist/types/config.d.ts +2 -0
  103. package/dist/types/config.d.ts.map +1 -1
  104. package/dist/types/index.d.ts +1 -0
  105. package/dist/types/index.d.ts.map +1 -1
  106. package/dist/types/process.d.ts +106 -0
  107. package/dist/types/process.d.ts.map +1 -0
  108. package/dist/types/process.js +5 -0
  109. package/dist/types/process.js.map +1 -0
  110. 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
- * Page imports useUsers hook and sub-components from @loom-framework/frontend-antd
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;;;;;;GAMG;AAEH,wBAAgB,0BAA0B,IAAI,MAAM,CAmOnD"}
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
- * Page imports useUsers hook and sub-components from @loom-framework/frontend-antd
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 ROLE_COLORS: Record<string, string> = {
105
+ const LEVEL_COLORS: Record<string, string> = {
61
106
  admin: 'red',
62
- editor: 'blue',
63
- viewer: 'green',
107
+ write: 'blue',
108
+ read: 'green',
109
+ none: 'default',
64
110
  };
65
111
 
66
- const ROLE_OPTIONS = [
67
- { value: 'admin', label: 'admin' },
68
- { value: 'editor', label: 'editor' },
69
- { value: 'viewer', label: 'viewer' },
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 [addModalOpen, setAddModalOpen] = useState(false);
161
+ const { rolesData } = useRoles();
78
162
  const [editModalOpen, setEditModalOpen] = useState(false);
79
- const [editingUser, setEditingUser] = useState<{ id: string; username: string; role: string } | null>(null);
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
- setAddModalOpen(false);
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.id, updates);
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={ROLE_COLORS[role] || 'default'}>{role}</Tag>,
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: { id: string; username: string; role: string }) => (
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.id);
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
- <div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
157
- <Flex justify="space-between" align="center" style={{ marginBottom: 12 }}>
158
- <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 }))]} />
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={addModalOpen}
251
+ open={addUserModalOpen}
186
252
  onOk={() => addForm.submit()}
187
- onCancel={() => { setAddModalOpen(false); addForm.resetFields(); }}
253
+ onCancel={() => { onAddUserModalClose(); addForm.resetFields(); }}
188
254
  okText={t('common.save') || 'Save'}
189
255
  cancelText={t('common.cancel') || 'Cancel'}
190
- destroyOnClose
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="viewer">
208
- <Select options={ROLE_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
- destroyOnClose
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={ROLE_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
- </div>
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;;;;;;GAMG;AAEH,MAAM,UAAU,0BAA0B;IACxC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiOR,CAAC;AACF,CAAC"}
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"}