@makolabs/ripple 3.0.9 → 3.0.11
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/dist/drawer/Drawer.svelte +33 -12
- package/dist/drawer/drawer-types.d.ts +13 -5
- package/dist/drawer/drawer.d.ts +45 -0
- package/dist/drawer/drawer.js +14 -5
- package/dist/funcs/mock-user-management.d.ts +40 -0
- package/dist/funcs/mock-user-management.js +85 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -1
- package/dist/modal/ConfirmDialog.svelte +6 -9
- package/dist/modal/Modal.svelte +18 -14
- package/dist/modal/modal-types.d.ts +9 -5
- package/dist/modal/modal.d.ts +42 -0
- package/dist/modal/modal.js +18 -8
- package/dist/user-management/ApiKeyField.svelte +165 -0
- package/dist/user-management/ApiKeyField.svelte.d.ts +32 -0
- package/dist/user-management/RoleCard.svelte +73 -0
- package/dist/user-management/RoleCard.svelte.d.ts +16 -0
- package/dist/user-management/UserApproveModal.svelte +115 -0
- package/dist/user-management/UserApproveModal.svelte.d.ts +4 -0
- package/dist/user-management/UserIdentityCard.svelte +53 -0
- package/dist/user-management/UserIdentityCard.svelte.d.ts +11 -0
- package/dist/user-management/UserManagement.svelte +191 -20
- package/dist/user-management/UserModal.svelte +203 -439
- package/dist/user-management/UserTable.svelte +48 -55
- package/dist/user-management/UserViewModal.svelte +87 -221
- package/dist/user-management/UserViewModal.svelte.d.ts +1 -1
- package/dist/user-management/user-management-types.d.ts +52 -3
- package/package.json +1 -1
- package/dist/modal/ModalFooter.svelte +0 -35
- package/dist/modal/ModalFooter.svelte.d.ts +0 -11
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import {
|
|
3
|
+
Button,
|
|
4
|
+
Color,
|
|
3
5
|
Table,
|
|
4
6
|
type TableColumn,
|
|
5
7
|
type User,
|
|
@@ -21,6 +23,8 @@
|
|
|
21
23
|
onview,
|
|
22
24
|
onedit,
|
|
23
25
|
ondelete,
|
|
26
|
+
onapprove,
|
|
27
|
+
onreject,
|
|
24
28
|
class: className = ''
|
|
25
29
|
}: UserTableProps = $props();
|
|
26
30
|
|
|
@@ -82,45 +86,6 @@
|
|
|
82
86
|
];
|
|
83
87
|
</script>
|
|
84
88
|
|
|
85
|
-
{#snippet EyeIcon()}
|
|
86
|
-
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
87
|
-
<path
|
|
88
|
-
stroke-linecap="round"
|
|
89
|
-
stroke-linejoin="round"
|
|
90
|
-
stroke-width="2"
|
|
91
|
-
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
92
|
-
></path>
|
|
93
|
-
<path
|
|
94
|
-
stroke-linecap="round"
|
|
95
|
-
stroke-linejoin="round"
|
|
96
|
-
stroke-width="2"
|
|
97
|
-
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
|
98
|
-
></path>
|
|
99
|
-
</svg>
|
|
100
|
-
{/snippet}
|
|
101
|
-
|
|
102
|
-
{#snippet EditIcon()}
|
|
103
|
-
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
104
|
-
<path
|
|
105
|
-
stroke-linecap="round"
|
|
106
|
-
stroke-linejoin="round"
|
|
107
|
-
stroke-width="2"
|
|
108
|
-
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
|
109
|
-
></path>
|
|
110
|
-
</svg>
|
|
111
|
-
{/snippet}
|
|
112
|
-
|
|
113
|
-
{#snippet TrashIcon()}
|
|
114
|
-
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
115
|
-
<path
|
|
116
|
-
stroke-linecap="round"
|
|
117
|
-
stroke-linejoin="round"
|
|
118
|
-
stroke-width="2"
|
|
119
|
-
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
120
|
-
></path>
|
|
121
|
-
</svg>
|
|
122
|
-
{/snippet}
|
|
123
|
-
|
|
124
89
|
{#snippet UserCell(user: User)}
|
|
125
90
|
<div class="flex items-center">
|
|
126
91
|
<div class="h-8 w-8 shrink-0">
|
|
@@ -173,36 +138,64 @@
|
|
|
173
138
|
{/snippet}
|
|
174
139
|
|
|
175
140
|
{#snippet ActionsCell(user: User)}
|
|
176
|
-
<div class="flex items-center justify-end
|
|
141
|
+
<div class="flex items-center justify-end gap-2">
|
|
177
142
|
{#if onview}
|
|
178
|
-
<
|
|
143
|
+
<Button
|
|
144
|
+
size="sm"
|
|
145
|
+
variant="outline"
|
|
179
146
|
onclick={() => onview(user)}
|
|
180
|
-
class="inline-flex rounded p-1 text-blue-600 transition-colors hover:bg-blue-50 hover:text-blue-900"
|
|
181
|
-
title="View User"
|
|
182
147
|
aria-label="View User"
|
|
148
|
+
testId="view-user-{user.id}"
|
|
183
149
|
>
|
|
184
|
-
|
|
185
|
-
</
|
|
150
|
+
View
|
|
151
|
+
</Button>
|
|
186
152
|
{/if}
|
|
187
153
|
{#if onedit}
|
|
188
|
-
<
|
|
154
|
+
<Button
|
|
155
|
+
size="sm"
|
|
156
|
+
variant="outline"
|
|
157
|
+
color={Color.PRIMARY}
|
|
189
158
|
onclick={() => onedit(user)}
|
|
190
|
-
class="inline-flex rounded p-1 text-green-600 transition-colors hover:bg-green-50 hover:text-green-900"
|
|
191
|
-
title="Edit User"
|
|
192
159
|
aria-label="Edit User"
|
|
160
|
+
testId="edit-user-{user.id}"
|
|
193
161
|
>
|
|
194
|
-
|
|
195
|
-
</
|
|
162
|
+
Edit
|
|
163
|
+
</Button>
|
|
196
164
|
{/if}
|
|
197
165
|
{#if ondelete}
|
|
198
|
-
<
|
|
166
|
+
<Button
|
|
167
|
+
size="sm"
|
|
168
|
+
variant="outline"
|
|
169
|
+
color={Color.DANGER}
|
|
199
170
|
onclick={() => ondelete(user.id)}
|
|
200
|
-
class="text-danger-600 hover:bg-danger-50 hover:text-danger-900 inline-flex rounded p-1 transition-colors"
|
|
201
|
-
title="Delete User"
|
|
202
171
|
aria-label="Delete User"
|
|
172
|
+
testId="delete-user-{user.id}"
|
|
173
|
+
>
|
|
174
|
+
Delete
|
|
175
|
+
</Button>
|
|
176
|
+
{/if}
|
|
177
|
+
{#if onapprove}
|
|
178
|
+
<Button
|
|
179
|
+
size="sm"
|
|
180
|
+
color={Color.SUCCESS}
|
|
181
|
+
onclick={() => onapprove(user)}
|
|
182
|
+
aria-label="Approve User"
|
|
183
|
+
testId="approve-user-{user.id}"
|
|
184
|
+
>
|
|
185
|
+
Approve
|
|
186
|
+
</Button>
|
|
187
|
+
{/if}
|
|
188
|
+
{#if onreject}
|
|
189
|
+
<Button
|
|
190
|
+
size="sm"
|
|
191
|
+
variant="outline"
|
|
192
|
+
color={Color.DANGER}
|
|
193
|
+
onclick={() => onreject(user)}
|
|
194
|
+
aria-label="Reject User"
|
|
195
|
+
testId="reject-user-{user.id}"
|
|
203
196
|
>
|
|
204
|
-
|
|
205
|
-
</
|
|
197
|
+
Reject
|
|
198
|
+
</Button>
|
|
206
199
|
{/if}
|
|
207
200
|
</div>
|
|
208
201
|
{/snippet}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { Modal, Button,
|
|
2
|
+
import { Modal, Button, Color, cn } from '../index.js';
|
|
3
|
+
import RoleCard from './RoleCard.svelte';
|
|
4
|
+
import ApiKeyField from './ApiKeyField.svelte';
|
|
5
|
+
import UserIdentityCard from './UserIdentityCard.svelte';
|
|
6
|
+
import type { Role, UserViewModalProps } from '../index.js';
|
|
3
7
|
import { SvelteDate } from 'svelte/reactivity';
|
|
4
8
|
|
|
5
9
|
let {
|
|
@@ -12,25 +16,17 @@
|
|
|
12
16
|
class: className
|
|
13
17
|
}: UserViewModalProps = $props();
|
|
14
18
|
|
|
15
|
-
// Local state
|
|
16
|
-
let showApiKey = $state(false);
|
|
17
19
|
let regeneratingApiKey = $state(false);
|
|
18
20
|
let apiKeyError = $state<string | null>(null);
|
|
19
21
|
|
|
20
22
|
function handleClose() {
|
|
21
23
|
open = false;
|
|
22
|
-
|
|
24
|
+
apiKeyError = null;
|
|
23
25
|
if (onclose) onclose();
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
function handleEdit() {
|
|
27
|
-
if (onedit && user)
|
|
28
|
-
onedit(user);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function getModalTitle() {
|
|
33
|
-
return getUserDisplayName(user);
|
|
29
|
+
if (onedit && user) onedit(user);
|
|
34
30
|
}
|
|
35
31
|
|
|
36
32
|
function formatDate(timestamp?: number) {
|
|
@@ -42,13 +38,8 @@
|
|
|
42
38
|
});
|
|
43
39
|
}
|
|
44
40
|
|
|
45
|
-
function toggleApiKeyVisibility() {
|
|
46
|
-
showApiKey = !showApiKey;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
41
|
async function handleRegenerateApiKey() {
|
|
50
42
|
if (!user?.id || !onregenerateapikey) return;
|
|
51
|
-
|
|
52
43
|
try {
|
|
53
44
|
regeneratingApiKey = true;
|
|
54
45
|
apiKeyError = null;
|
|
@@ -61,7 +52,6 @@
|
|
|
61
52
|
}
|
|
62
53
|
}
|
|
63
54
|
|
|
64
|
-
// Get API key from user's private_metadata
|
|
65
55
|
const apiKey = $derived(
|
|
66
56
|
user?.private_metadata &&
|
|
67
57
|
typeof user.private_metadata === 'object' &&
|
|
@@ -70,233 +60,109 @@
|
|
|
70
60
|
: ''
|
|
71
61
|
);
|
|
72
62
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// Find the current role object
|
|
77
|
-
const currentRole = $derived(
|
|
78
|
-
user?.role && roles.length > 0 ? roles.find((r) => r.value === user.role) : null
|
|
63
|
+
const currentRole = $derived<Role | null>(
|
|
64
|
+
user?.role && roles.length > 0 ? (roles.find((r) => r.value === user.role) ?? null) : null
|
|
79
65
|
);
|
|
80
66
|
</script>
|
|
81
67
|
|
|
68
|
+
{#snippet ReadOnlyField(label: string, value: string | undefined)}
|
|
69
|
+
<div>
|
|
70
|
+
<p class="text-default-700 mb-1 text-sm font-medium">{label}</p>
|
|
71
|
+
<div
|
|
72
|
+
class="border-default-300 bg-default-50 text-default-900 rounded-lg border px-3 py-2 text-sm"
|
|
73
|
+
>
|
|
74
|
+
{value || '-'}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
{/snippet}
|
|
78
|
+
|
|
82
79
|
<Modal
|
|
83
80
|
{open}
|
|
84
81
|
onclose={handleClose}
|
|
85
|
-
title=
|
|
86
|
-
|
|
82
|
+
title="User details"
|
|
83
|
+
footerAlign="end"
|
|
84
|
+
contentClass="max-w-3xl"
|
|
87
85
|
bodyClass="flex flex-col"
|
|
88
86
|
class={cn(className)}
|
|
89
87
|
>
|
|
90
|
-
<div class="flex gap-6">
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<div>
|
|
99
|
-
<p class="text-default-700 mb-1 text-sm font-medium">First Name</p>
|
|
100
|
-
<div
|
|
101
|
-
class="border-default-300 bg-default-50 text-default-900 rounded-lg border px-3 py-2 text-sm"
|
|
102
|
-
>
|
|
103
|
-
{user?.first_name || '-'}
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
|
|
107
|
-
<!-- Last Name -->
|
|
108
|
-
<div>
|
|
109
|
-
<p class="text-default-700 mb-1 text-sm font-medium">Last Name</p>
|
|
110
|
-
<div
|
|
111
|
-
class="border-default-300 bg-default-50 text-default-900 rounded-lg border px-3 py-2 text-sm"
|
|
112
|
-
>
|
|
113
|
-
{user?.last_name || '-'}
|
|
114
|
-
</div>
|
|
115
|
-
</div>
|
|
116
|
-
|
|
117
|
-
<!-- Email Address -->
|
|
118
|
-
<div>
|
|
119
|
-
<p class="text-default-700 mb-1 text-sm font-medium">Email Address</p>
|
|
120
|
-
<div
|
|
121
|
-
class="border-default-300 bg-default-50 text-default-900 rounded-lg border px-3 py-2 text-sm"
|
|
122
|
-
>
|
|
123
|
-
{user?.email_addresses?.[0]?.email_address || '-'}
|
|
124
|
-
</div>
|
|
125
|
-
</div>
|
|
126
|
-
|
|
127
|
-
<!-- Mako API Key -->
|
|
128
|
-
{#if apiKey || onregenerateapikey}
|
|
129
|
-
<div>
|
|
130
|
-
<div class="mb-2 flex items-center justify-between">
|
|
131
|
-
<p class="text-default-700 text-sm font-medium">Mako API Key</p>
|
|
132
|
-
{#if onregenerateapikey}
|
|
133
|
-
<button
|
|
134
|
-
type="button"
|
|
135
|
-
onclick={handleRegenerateApiKey}
|
|
136
|
-
disabled={regeneratingApiKey}
|
|
137
|
-
class="disabled:text-default-400 inline-flex items-center gap-1 text-sm text-blue-600 transition-colors hover:text-blue-700 hover:underline disabled:cursor-not-allowed"
|
|
138
|
-
aria-label="Regenerate API key"
|
|
139
|
-
>
|
|
140
|
-
<svg
|
|
141
|
-
class="h-4 w-4 {regeneratingApiKey ? 'animate-spin' : ''}"
|
|
142
|
-
fill="none"
|
|
143
|
-
stroke="currentColor"
|
|
144
|
-
viewBox="0 0 24 24"
|
|
145
|
-
>
|
|
146
|
-
<path
|
|
147
|
-
stroke-linecap="round"
|
|
148
|
-
stroke-linejoin="round"
|
|
149
|
-
stroke-width="2"
|
|
150
|
-
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
151
|
-
></path>
|
|
152
|
-
</svg>
|
|
153
|
-
{apiKey ? 'Regenerate' : 'Generate'}
|
|
154
|
-
</button>
|
|
155
|
-
{/if}
|
|
156
|
-
</div>
|
|
157
|
-
<div class="relative flex-1">
|
|
158
|
-
<div
|
|
159
|
-
class="scrollbar-hide border-default-300 bg-default-50 text-default-900 w-full overflow-x-auto overflow-y-hidden rounded-lg border px-3 py-2 pr-12 font-mono text-sm"
|
|
160
|
-
>
|
|
161
|
-
<div class="whitespace-nowrap">
|
|
162
|
-
{showApiKey ? apiKey || 'No API key' : apiKey ? maskedApiKey : 'No API key'}
|
|
163
|
-
</div>
|
|
164
|
-
</div>
|
|
165
|
-
{#if apiKey}
|
|
166
|
-
<button
|
|
167
|
-
type="button"
|
|
168
|
-
onclick={toggleApiKeyVisibility}
|
|
169
|
-
class="bg-default-50 text-default-500 hover:text-default-700 absolute top-1/2 right-3 -translate-y-1/2 cursor-pointer"
|
|
170
|
-
aria-label={showApiKey ? 'Hide API key' : 'Show API key'}
|
|
171
|
-
>
|
|
172
|
-
{#if showApiKey}
|
|
173
|
-
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
174
|
-
<path
|
|
175
|
-
stroke-linecap="round"
|
|
176
|
-
stroke-linejoin="round"
|
|
177
|
-
stroke-width="2"
|
|
178
|
-
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.29 3.29m0 0A9.966 9.966 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
|
|
179
|
-
></path>
|
|
180
|
-
</svg>
|
|
181
|
-
{:else}
|
|
182
|
-
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
183
|
-
<path
|
|
184
|
-
stroke-linecap="round"
|
|
185
|
-
stroke-linejoin="round"
|
|
186
|
-
stroke-width="2"
|
|
187
|
-
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
188
|
-
></path>
|
|
189
|
-
<path
|
|
190
|
-
stroke-linecap="round"
|
|
191
|
-
stroke-linejoin="round"
|
|
192
|
-
stroke-width="2"
|
|
193
|
-
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
|
194
|
-
></path>
|
|
195
|
-
</svg>
|
|
196
|
-
{/if}
|
|
197
|
-
</button>
|
|
198
|
-
{/if}
|
|
199
|
-
</div>
|
|
200
|
-
{#if apiKeyError}
|
|
201
|
-
<p class="text-danger-500 mt-1 text-xs">{apiKeyError}</p>
|
|
202
|
-
{:else}
|
|
203
|
-
<p class="text-default-500 mt-1 text-xs">
|
|
204
|
-
API keys are system-managed and cannot be manually edited
|
|
205
|
-
</p>
|
|
206
|
-
{/if}
|
|
207
|
-
</div>
|
|
208
|
-
{/if}
|
|
209
|
-
</div>
|
|
210
|
-
|
|
211
|
-
<!-- Right Column: Role & Permissions -->
|
|
212
|
-
<div class="min-w-0 flex-1 space-y-4">
|
|
213
|
-
<div class="border-default-200 border-b pb-3">
|
|
214
|
-
<h3 class="text-default-900 text-lg font-semibold">Role & Permissions</h3>
|
|
88
|
+
<div class="flex flex-col gap-6">
|
|
89
|
+
<UserIdentityCard {user} />
|
|
90
|
+
|
|
91
|
+
<!-- Profile -->
|
|
92
|
+
<div class="space-y-4">
|
|
93
|
+
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
94
|
+
{@render ReadOnlyField('First name', user?.first_name)}
|
|
95
|
+
{@render ReadOnlyField('Last name', user?.last_name)}
|
|
215
96
|
</div>
|
|
97
|
+
{@render ReadOnlyField('Email address', user?.email_addresses?.[0]?.email_address)}
|
|
216
98
|
|
|
217
|
-
|
|
218
|
-
{#if user?.role}
|
|
219
|
-
<div>
|
|
220
|
-
<p class="text-default-700 mb-3 text-sm font-medium">User Role</p>
|
|
221
|
-
<div class="rounded-lg border-2 border-blue-500 bg-blue-50 p-3">
|
|
222
|
-
<div class="flex items-center justify-between gap-2">
|
|
223
|
-
<div class="min-w-0 flex-1">
|
|
224
|
-
<h4 class="text-default-900 text-sm font-semibold">
|
|
225
|
-
{currentRole?.label || user.role}
|
|
226
|
-
</h4>
|
|
227
|
-
{#if currentRole?.description}
|
|
228
|
-
<p class="text-default-600 mt-1 line-clamp-2 text-xs">
|
|
229
|
-
{currentRole.description}
|
|
230
|
-
</p>
|
|
231
|
-
{/if}
|
|
232
|
-
</div>
|
|
233
|
-
<div
|
|
234
|
-
class="flex h-5 w-5 shrink-0 items-center justify-center rounded-full border-2 border-blue-500 bg-blue-500"
|
|
235
|
-
>
|
|
236
|
-
<div class="h-2 w-2 rounded-full bg-white"></div>
|
|
237
|
-
</div>
|
|
238
|
-
</div>
|
|
239
|
-
</div>
|
|
240
|
-
</div>
|
|
241
|
-
{/if}
|
|
242
|
-
|
|
243
|
-
<!-- Permissions -->
|
|
244
|
-
{#if user?.permissions && user.permissions.length > 0}
|
|
245
|
-
<div>
|
|
246
|
-
<h4 class="text-default-700 mb-2 text-sm font-medium">
|
|
247
|
-
Permissions ({user.permissions.length})
|
|
248
|
-
</h4>
|
|
249
|
-
<div class="bg-default-50 max-h-60 overflow-y-auto rounded-lg p-3">
|
|
250
|
-
<div class="space-y-2">
|
|
251
|
-
{#each user.permissions as permission, index (`${permission}-${index}`)}
|
|
252
|
-
<div class="flex items-start gap-2 text-xs">
|
|
253
|
-
<div class="mt-1 h-1 w-1 shrink-0 rounded-full bg-blue-500"></div>
|
|
254
|
-
<div class="text-default-700 font-mono">{permission}</div>
|
|
255
|
-
</div>
|
|
256
|
-
{/each}
|
|
257
|
-
</div>
|
|
258
|
-
</div>
|
|
259
|
-
</div>
|
|
260
|
-
{/if}
|
|
261
|
-
|
|
262
|
-
<!-- Additional Info -->
|
|
263
|
-
<div class="space-y-2">
|
|
99
|
+
<div class="flex flex-wrap gap-x-6 gap-y-2 pt-1 text-xs">
|
|
264
100
|
{#if user?.created_at}
|
|
265
101
|
<div>
|
|
266
|
-
<
|
|
267
|
-
<
|
|
102
|
+
<span class="text-default-500">Created · </span>
|
|
103
|
+
<span class="text-default-900 font-medium">{formatDate(user.created_at)}</span>
|
|
268
104
|
</div>
|
|
269
105
|
{/if}
|
|
270
106
|
{#if user?.last_sign_in_at}
|
|
271
107
|
<div>
|
|
272
|
-
<
|
|
273
|
-
<
|
|
108
|
+
<span class="text-default-500">Last sign in · </span>
|
|
109
|
+
<span class="text-default-900 font-medium">{formatDate(user.last_sign_in_at)}</span>
|
|
274
110
|
</div>
|
|
275
111
|
{/if}
|
|
276
112
|
</div>
|
|
277
113
|
</div>
|
|
278
|
-
</div>
|
|
279
114
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
115
|
+
<!-- Role + permissions -->
|
|
116
|
+
{#if user?.role}
|
|
117
|
+
<div class="space-y-3">
|
|
118
|
+
<span class="text-default-700 mb-2 block text-sm font-medium">Role</span>
|
|
119
|
+
<RoleCard
|
|
120
|
+
role={currentRole ?? { value: user.role, label: user.role, permissions: [] }}
|
|
121
|
+
selected
|
|
122
|
+
interactive={false}
|
|
123
|
+
/>
|
|
124
|
+
|
|
125
|
+
{#if user?.permissions && user.permissions.length > 0}
|
|
126
|
+
<div>
|
|
127
|
+
<span class="text-default-500 mb-1.5 block text-xs font-medium">
|
|
128
|
+
Granted permissions ({user.permissions.length})
|
|
129
|
+
</span>
|
|
130
|
+
<div class="flex flex-wrap gap-1.5">
|
|
131
|
+
{#each user.permissions as permission, index (`${permission}-${index}`)}
|
|
132
|
+
<span
|
|
133
|
+
class="bg-default-100 text-default-700 rounded-md px-2 py-0.5 font-mono text-xs"
|
|
134
|
+
>
|
|
135
|
+
{permission}
|
|
136
|
+
</span>
|
|
137
|
+
{/each}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
298
140
|
{/if}
|
|
299
141
|
</div>
|
|
300
|
-
|
|
142
|
+
{/if}
|
|
143
|
+
|
|
144
|
+
<!-- API key -->
|
|
145
|
+
{#if apiKey || onregenerateapikey}
|
|
146
|
+
<ApiKeyField
|
|
147
|
+
value={apiKey}
|
|
148
|
+
label="Mako API Key"
|
|
149
|
+
error={apiKeyError}
|
|
150
|
+
helperText="API keys are system-managed and cannot be manually edited"
|
|
151
|
+
regenerate={onregenerateapikey
|
|
152
|
+
? {
|
|
153
|
+
label: apiKey ? 'Regenerate' : 'Generate',
|
|
154
|
+
loading: regeneratingApiKey,
|
|
155
|
+
onclick: handleRegenerateApiKey
|
|
156
|
+
}
|
|
157
|
+
: undefined}
|
|
158
|
+
/>
|
|
159
|
+
{/if}
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
{#snippet footer()}
|
|
163
|
+
<Button variant="outline" onclick={handleClose}>Close</Button>
|
|
164
|
+
{#if onedit}
|
|
165
|
+
<Button color={Color.PRIMARY} onclick={handleEdit}>Edit user</Button>
|
|
166
|
+
{/if}
|
|
301
167
|
{/snippet}
|
|
302
168
|
</Modal>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { UserViewModalProps } from '../index.js';
|
|
2
2
|
declare const UserViewModal: import("svelte").Component<UserViewModalProps, {}, "open" | "user">;
|
|
3
3
|
type UserViewModal = ReturnType<typeof UserViewModal>;
|
|
4
4
|
export default UserViewModal;
|
|
@@ -83,11 +83,15 @@ export interface UserTableProps {
|
|
|
83
83
|
direction: 'asc' | 'desc' | null;
|
|
84
84
|
}) => void;
|
|
85
85
|
/** Fires when the user clicks the View action on a row. */
|
|
86
|
-
onview
|
|
86
|
+
onview?: (user: User) => void;
|
|
87
87
|
/** Fires when the user clicks the Edit action on a row. */
|
|
88
|
-
onedit
|
|
88
|
+
onedit?: (user: User) => void;
|
|
89
89
|
/** Fires when the user confirms a Delete action on a row. */
|
|
90
|
-
ondelete
|
|
90
|
+
ondelete?: (userId: string) => void;
|
|
91
|
+
/** Fires when the user clicks the Approve action on a row (pending users). */
|
|
92
|
+
onapprove?: (user: User) => void;
|
|
93
|
+
/** Fires when the user clicks the Reject action on a row (pending users). */
|
|
94
|
+
onreject?: (user: User) => void;
|
|
91
95
|
class?: ClassValue;
|
|
92
96
|
testId?: string;
|
|
93
97
|
}
|
|
@@ -201,6 +205,51 @@ export interface UserManagementAdapter {
|
|
|
201
205
|
error?: string;
|
|
202
206
|
token?: string;
|
|
203
207
|
}>;
|
|
208
|
+
/**
|
|
209
|
+
* Optional. List users that are registered in your identity provider but
|
|
210
|
+
* haven't been onboarded yet (no org assignment, no API key). When this
|
|
211
|
+
* method is present, `<UserManagement>` shows a "Pending" tab with an
|
|
212
|
+
* approval workflow. When absent, the tab is hidden entirely.
|
|
213
|
+
*/
|
|
214
|
+
getPendingUsers?: (options: GetUsersOptions) => PromiseLike<GetUsersResult>;
|
|
215
|
+
/**
|
|
216
|
+
* Optional. Approve a pending user — assigns them to the org with the
|
|
217
|
+
* selected role and generates their first API key. Returns the new key
|
|
218
|
+
* for one-time reveal in the UI.
|
|
219
|
+
*/
|
|
220
|
+
approveUser?: (input: {
|
|
221
|
+
userId: string;
|
|
222
|
+
role: string;
|
|
223
|
+
}) => PromiseLike<{
|
|
224
|
+
apiKey: string;
|
|
225
|
+
}>;
|
|
226
|
+
/**
|
|
227
|
+
* Optional. Reject a pending user — permanently deletes them from the
|
|
228
|
+
* identity provider. Pair with a confirmation dialog at the call site.
|
|
229
|
+
*/
|
|
230
|
+
rejectUser?: (userId: string) => PromiseLike<void>;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Props for `<UserApproveModal>` — confirms approval of a pending user with
|
|
234
|
+
* a role pick + on-success API key reveal.
|
|
235
|
+
*/
|
|
236
|
+
export interface UserApproveModalProps {
|
|
237
|
+
open?: boolean;
|
|
238
|
+
user: User | null;
|
|
239
|
+
roles?: Role[];
|
|
240
|
+
/**
|
|
241
|
+
* Approval callback. Receives the userId and chosen role; should resolve
|
|
242
|
+
* with the newly generated API key for one-time display.
|
|
243
|
+
*/
|
|
244
|
+
onapprove: (input: {
|
|
245
|
+
userId: string;
|
|
246
|
+
role: string;
|
|
247
|
+
}) => Promise<{
|
|
248
|
+
apiKey: string;
|
|
249
|
+
}>;
|
|
250
|
+
onclose: () => void;
|
|
251
|
+
class?: ClassValue;
|
|
252
|
+
testId?: string;
|
|
204
253
|
}
|
|
205
254
|
/**
|
|
206
255
|
* Props for `<UserManagement>` — the all-in-one user dashboard
|
package/package.json
CHANGED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { cn } from '../helper/cls.js';
|
|
3
|
-
import { buildTestId } from '../helper/testid.js';
|
|
4
|
-
import type { Snippet } from 'svelte';
|
|
5
|
-
import type { ClassValue } from 'tailwind-variants';
|
|
6
|
-
|
|
7
|
-
type Props = {
|
|
8
|
-
class?: ClassValue;
|
|
9
|
-
align?: 'start' | 'center' | 'end' | 'between';
|
|
10
|
-
children: Snippet;
|
|
11
|
-
testId?: string;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
let { class: className = '', align = 'end', children, testId }: Props = $props();
|
|
15
|
-
|
|
16
|
-
const alignClass = $derived(
|
|
17
|
-
{
|
|
18
|
-
start: 'justify-start',
|
|
19
|
-
center: 'justify-center',
|
|
20
|
-
end: 'justify-end',
|
|
21
|
-
between: 'justify-between'
|
|
22
|
-
}[align]
|
|
23
|
-
);
|
|
24
|
-
</script>
|
|
25
|
-
|
|
26
|
-
<div
|
|
27
|
-
class={cn(
|
|
28
|
-
'border-default-200 flex flex-wrap items-center gap-3 border-t px-6 py-4',
|
|
29
|
-
alignClass,
|
|
30
|
-
className
|
|
31
|
-
)}
|
|
32
|
-
data-testid={buildTestId('modal-footer', undefined, testId)}
|
|
33
|
-
>
|
|
34
|
-
{@render children()}
|
|
35
|
-
</div>
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { Snippet } from 'svelte';
|
|
2
|
-
import type { ClassValue } from 'tailwind-variants';
|
|
3
|
-
type Props = {
|
|
4
|
-
class?: ClassValue;
|
|
5
|
-
align?: 'start' | 'center' | 'end' | 'between';
|
|
6
|
-
children: Snippet;
|
|
7
|
-
testId?: string;
|
|
8
|
-
};
|
|
9
|
-
declare const ModalFooter: import("svelte").Component<Props, {}, "">;
|
|
10
|
-
type ModalFooter = ReturnType<typeof ModalFooter>;
|
|
11
|
-
export default ModalFooter;
|