|
1 | 1 | <script lang="ts"> |
2 | 2 | import { Mail } from "@lucide/svelte"; |
3 | | - import { goto } from "$app/navigation"; |
| 3 | + import { onMount } from "svelte"; |
4 | 4 | import { page } from "$app/state"; |
5 | 5 | import type { PageData } from "./$types"; |
6 | 6 |
|
7 | 7 | let { data } = $props<{ data: PageData }>(); |
8 | 8 |
|
9 | | - let roleFilter = $state(page.url.searchParams.get("role_name") || ""); |
10 | | - let bankIdFilter = $state(page.url.searchParams.get("bank_id") || ""); |
| 9 | + let roleFilter = $state(page.url.searchParams.get("role_name") ?? ""); |
| 10 | + let bankIdFilter = $state(page.url.searchParams.get("bank_id") ?? ""); |
11 | 11 | let roles = $state<string[]>([]); |
12 | 12 | let banks = $state<string[]>([]); |
13 | 13 |
|
|
39 | 39 | fetchBanks(); |
40 | 40 | }); |
41 | 41 |
|
42 | | - let users = $derived(data.users || []); |
43 | 42 | let hasApiAccess = $derived(data.hasApiAccess); |
44 | 43 | let error = $derived(data.error); |
45 | 44 |
|
|
51 | 50 | let searchError = $state<string | null>(null); |
52 | 51 | let isSearching = $state(false); |
53 | 52 | let searchType = $state<"email" | "userid" | "username" | "">(""); |
| 53 | + let sortBy = $state(""); |
| 54 | + let sortDirection = $state<"asc" | "desc">("desc"); |
54 | 55 | let lastSearchCall = $state<{ proxyUrl: string; obpPath: string; status?: number; responseBody?: string } | null>(null); |
55 | 56 |
|
56 | 57 | // Fetch providers on mount |
|
69 | 70 | fetchProviders(); |
70 | 71 | }); |
71 | 72 |
|
| 73 | + // Auto-run search on mount (URL filters if present, else empty search) |
| 74 | + onMount(() => { |
| 75 | + handleSearch(); |
| 76 | + }); |
| 77 | +
|
| 78 | + function handleClear() { |
| 79 | + searchQuery = ""; |
| 80 | + selectedProvider = ""; |
| 81 | + roleFilter = ""; |
| 82 | + bankIdFilter = ""; |
| 83 | + sortBy = ""; |
| 84 | + sortDirection = "desc"; |
| 85 | + history.replaceState(null, "", "/users"); |
| 86 | + handleSearch(); |
| 87 | + } |
| 88 | +
|
72 | 89 | // Detect search type based on input |
73 | 90 | function detectSearchType(input: string): "email" | "userid" | "username" { |
74 | 91 | // Check if it's an email |
|
86 | 103 | } |
87 | 104 |
|
88 | 105 | async function handleSearch() { |
89 | | - if (!searchQuery.trim()) { |
90 | | - searchResults = []; |
91 | | - searchError = null; |
92 | | - searchType = ""; |
93 | | - lastSearchedQuery = ""; |
94 | | - return; |
95 | | - } |
96 | | -
|
97 | | - const type = detectSearchType(searchQuery.trim()); |
98 | | - searchType = type; |
| 106 | + const query = searchQuery.trim(); |
99 | 107 | isSearching = true; |
100 | 108 | searchError = null; |
101 | | - lastSearchedQuery = searchQuery.trim(); |
| 109 | + lastSearchedQuery = query; |
102 | 110 |
|
103 | 111 | const params = new URLSearchParams(); |
104 | | - if (type === "email") { |
105 | | - params.set("email", searchQuery); |
106 | | - } else if (type === "userid") { |
107 | | - params.set("user_id", searchQuery); |
108 | | - } else { |
109 | | - params.set("username", searchQuery); |
110 | | - if (selectedProvider) { |
111 | | - params.set("provider", selectedProvider); |
| 112 | + params.set("limit", "100"); |
| 113 | + if (query) { |
| 114 | + const type = detectSearchType(query); |
| 115 | + searchType = type; |
| 116 | + if (type === "email") { |
| 117 | + params.set("email", query); |
| 118 | + } else if (type === "userid") { |
| 119 | + params.set("user_id", query); |
| 120 | + } else { |
| 121 | + params.set("username", query); |
112 | 122 | } |
| 123 | + } else { |
| 124 | + searchType = ""; |
| 125 | + } |
| 126 | + if (selectedProvider) { |
| 127 | + params.set("provider", selectedProvider); |
| 128 | + } |
| 129 | + if (roleFilter) { |
| 130 | + params.set("role_name", roleFilter); |
| 131 | + } |
| 132 | + if (bankIdFilter) { |
| 133 | + params.set("bank_id", bankIdFilter); |
| 134 | + } |
| 135 | + if (sortBy) { |
| 136 | + params.set("sort_by", sortBy); |
| 137 | + params.set("sort_direction", sortDirection); |
113 | 138 | } |
114 | 139 | const proxyUrl = `/proxy/obp/v6.0.0/users?${params.toString()}`; |
115 | 140 |
|
|
193 | 218 | }} |
194 | 219 | class="flex gap-4 items-end flex-wrap" |
195 | 220 | > |
196 | | - <div style="flex: 0 0 300px;"> |
| 221 | + <div class="flex-1" style="min-width: 240px;"> |
| 222 | + <label for="search-input" class="block text-sm font-medium mb-2" |
| 223 | + >Search</label |
| 224 | + > |
| 225 | + <input |
| 226 | + type="text" |
| 227 | + id="search-input" |
| 228 | + bind:value={searchQuery} |
| 229 | + placeholder="Enter email, user ID (UUID), or username" |
| 230 | + class="form-input w-full" |
| 231 | + /> |
| 232 | + </div> |
| 233 | + <div style="flex: 0 0 220px;"> |
197 | 234 | <label for="provider-select" class="block text-sm font-medium mb-2" |
198 | 235 | >Provider</label |
199 | 236 | > |
|
208 | 245 | {/each} |
209 | 246 | </select> |
210 | 247 | </div> |
211 | | - <div class="flex-1"> |
212 | | - <label for="search-input" class="block text-sm font-medium mb-2" |
213 | | - >Search</label |
| 248 | + <div style="flex: 0 0 220px;"> |
| 249 | + <label for="role-input" class="block text-sm font-medium mb-2" |
| 250 | + >Role</label |
214 | 251 | > |
215 | | - <input |
216 | | - type="text" |
217 | | - id="search-input" |
218 | | - bind:value={searchQuery} |
219 | | - placeholder="Enter email, user ID (UUID), or username" |
| 252 | + <select |
| 253 | + id="role-input" |
| 254 | + bind:value={roleFilter} |
220 | 255 | class="form-input w-full" |
221 | | - /> |
| 256 | + > |
| 257 | + <option value="">All roles</option> |
| 258 | + {#each roles as role} |
| 259 | + <option value={role}>{role}</option> |
| 260 | + {/each} |
| 261 | + </select> |
| 262 | + </div> |
| 263 | + <div style="flex: 0 0 220px;"> |
| 264 | + <label for="bank-id-input" class="block text-sm font-medium mb-2" |
| 265 | + >Bank ID</label |
| 266 | + > |
| 267 | + <select |
| 268 | + id="bank-id-input" |
| 269 | + bind:value={bankIdFilter} |
| 270 | + class="form-input w-full" |
| 271 | + > |
| 272 | + <option value="">All banks</option> |
| 273 | + {#each banks as bank} |
| 274 | + <option value={bank}>{bank}</option> |
| 275 | + {/each} |
| 276 | + </select> |
| 277 | + </div> |
| 278 | + <div style="flex: 0 0 180px;"> |
| 279 | + <label for="sort-by-input" class="block text-sm font-medium mb-2" |
| 280 | + >Sort by</label |
| 281 | + > |
| 282 | + <select |
| 283 | + id="sort-by-input" |
| 284 | + bind:value={sortBy} |
| 285 | + data-testid="sort-by-input" |
| 286 | + class="form-input w-full" |
| 287 | + > |
| 288 | + <option value="">Default</option> |
| 289 | + <option value="created_date">Created date</option> |
| 290 | + <option value="updated_date">Updated date</option> |
| 291 | + <option value="username">Username</option> |
| 292 | + <option value="email">Email</option> |
| 293 | + <option value="user_id">User ID</option> |
| 294 | + <option value="provider">Provider</option> |
| 295 | + </select> |
| 296 | + </div> |
| 297 | + <div style="flex: 0 0 140px;"> |
| 298 | + <label for="sort-direction-input" class="block text-sm font-medium mb-2" |
| 299 | + >Direction</label |
| 300 | + > |
| 301 | + <select |
| 302 | + id="sort-direction-input" |
| 303 | + bind:value={sortDirection} |
| 304 | + data-testid="sort-direction-input" |
| 305 | + class="form-input w-full" |
| 306 | + disabled={!sortBy} |
| 307 | + > |
| 308 | + <option value="desc">Descending</option> |
| 309 | + <option value="asc">Ascending</option> |
| 310 | + </select> |
222 | 311 | </div> |
223 | 312 | <button |
224 | 313 | type="submit" |
225 | 314 | class="btn btn-primary" |
226 | | - disabled={isSearching || !searchQuery.trim()} |
| 315 | + disabled={isSearching} |
227 | 316 | > |
228 | 317 | {isSearching ? "Searching..." : "Search"} |
229 | 318 | </button> |
| 319 | + <button |
| 320 | + type="button" |
| 321 | + class="btn btn-secondary" |
| 322 | + onclick={handleClear} |
| 323 | + disabled={isSearching} |
| 324 | + data-testid="clear-search" |
| 325 | + > |
| 326 | + Clear |
| 327 | + </button> |
230 | 328 | </form> |
231 | 329 |
|
232 | 330 | {#snippet technicalDetails()} |
233 | 331 | {#if lastSearchCall} |
234 | 332 | <details class="search-call-details" data-testid="last-search-call"> |
235 | | - <summary class="search-call-summary" data-testid="last-search-call-toggle">Query</summary> |
| 333 | + <summary class="search-call-summary" data-testid="last-search-call-toggle">Debug</summary> |
236 | 334 | <div class="search-call-info"> |
237 | 335 | <div class="search-call-field"> |
238 | 336 | <label for="last-search-proxy-url" class="search-call-label">Proxy URL</label> |
|
282 | 380 | {/if} |
283 | 381 | {/snippet} |
284 | 382 |
|
285 | | - <form |
286 | | - onsubmit={(e) => { |
287 | | - e.preventDefault(); |
288 | | - const params = new URLSearchParams(); |
289 | | - if (roleFilter.trim()) { |
290 | | - params.set("role_name", roleFilter.trim()); |
291 | | - } |
292 | | - if (bankIdFilter.trim()) { |
293 | | - params.set("bank_id", bankIdFilter.trim()); |
294 | | - } |
295 | | - const qs = params.toString(); |
296 | | - goto(qs ? `?${qs}` : "/users", { invalidateAll: true }); |
297 | | - }} |
298 | | - class="flex gap-4 items-end mt-4" |
299 | | - > |
300 | | - <div class="flex-1"> |
301 | | - <label for="role-input" class="block text-sm font-medium mb-2" |
302 | | - >Role</label |
303 | | - > |
304 | | - <select |
305 | | - id="role-input" |
306 | | - bind:value={roleFilter} |
307 | | - class="form-input w-full" |
308 | | - > |
309 | | - <option value="">All roles</option> |
310 | | - {#each roles as role} |
311 | | - <option value={role}>{role}</option> |
312 | | - {/each} |
313 | | - </select> |
314 | | - </div> |
315 | | - <div style="flex: 0 0 300px;"> |
316 | | - <label for="bank-id-input" class="block text-sm font-medium mb-2" |
317 | | - >Bank ID</label |
318 | | - > |
319 | | - <select |
320 | | - id="bank-id-input" |
321 | | - bind:value={bankIdFilter} |
322 | | - class="form-input w-full" |
323 | | - > |
324 | | - <option value="">All banks</option> |
325 | | - {#each banks as bank} |
326 | | - <option value={bank}>{bank}</option> |
327 | | - {/each} |
328 | | - </select> |
329 | | - </div> |
330 | | - <button |
331 | | - type="submit" |
332 | | - class="btn btn-primary" |
333 | | - > |
334 | | - Filter |
335 | | - </button> |
336 | | - {#if page.url.searchParams.has("role_name") || page.url.searchParams.has("bank_id")} |
337 | | - <a href="/users" class="btn btn-secondary">Clear</a> |
338 | | - {/if} |
339 | | - </form> |
340 | | - |
341 | 383 | {#if searchResults.length > 0} |
342 | 384 | <div class="mt-6"> |
343 | 385 | <div class="search-results-header mb-4"> |
|
392 | 434 | </div> |
393 | 435 | <div class="alert alert-error">{searchError}</div> |
394 | 436 | </div> |
395 | | - {:else if lastSearchedQuery && !isSearching} |
| 437 | + {:else if lastSearchCall && !isSearching} |
396 | 438 | <div class="mt-6"> |
397 | 439 | <div class="search-results-header mb-3"> |
398 | | - <span class="text-gray-500">No users found matching "{lastSearchedQuery}"</span> |
| 440 | + <span class="text-gray-500"> |
| 441 | + No users found{lastSearchedQuery ? ` matching "${lastSearchedQuery}"` : ""} |
| 442 | + </span> |
399 | 443 | {@render technicalDetails()} |
400 | 444 | </div> |
401 | 445 | </div> |
402 | 446 | {/if} |
403 | 447 | </div> |
404 | 448 | </div> |
405 | 449 |
|
406 | | - <!-- Users List Panel --> |
407 | | - <div class="panel"> |
408 | | - <div class="panel-header"> |
409 | | - <h2 class="panel-title">{page.url.searchParams.has("role_name") || page.url.searchParams.has("bank_id") ? `Users${page.url.searchParams.has("role_name") ? ` with Role: ${page.url.searchParams.get("role_name")}` : ""}${page.url.searchParams.has("bank_id") ? ` at Bank: ${page.url.searchParams.get("bank_id")}` : ""}` : "Recent Users"}</h2> |
410 | | - <div class="panel-subtitle">{page.url.searchParams.has("role_name") || page.url.searchParams.has("bank_id") ? "Filtered results" : "Most recently created users"} (up to 100)</div> |
411 | | - </div> |
412 | | - <div class="panel-content"> |
413 | | - {#if users && users.length > 0} |
414 | | - <div class="table-wrapper"> |
415 | | - <table class="users-table"> |
416 | | - <thead> |
417 | | - <tr> |
418 | | - <th>Username</th> |
419 | | - <th>Email</th> |
420 | | - <th>User ID</th> |
421 | | - <th>Provider</th> |
422 | | - <th>Actions</th> |
423 | | - </tr> |
424 | | - </thead> |
425 | | - <tbody> |
426 | | - {#each users as user} |
427 | | - <tr> |
428 | | - <td class="font-medium">{user.username || "N/A"}</td> |
429 | | - <td>{user.email || "N/A"}</td> |
430 | | - <td class="font-mono text-sm">{user.user_id || "N/A"}</td> |
431 | | - <td>{user.provider || "N/A"}</td> |
432 | | - <td> |
433 | | - {#if user.user_id} |
434 | | - <a |
435 | | - href="/users/{encodeURIComponent(user.user_id)}" |
436 | | - class="text-blue-600 hover:text-blue-800 underline" |
437 | | - > |
438 | | - Details |
439 | | - </a> |
440 | | - {:else} |
441 | | - <span class="text-gray-400">N/A</span> |
442 | | - {/if} |
443 | | - </td> |
444 | | - </tr> |
445 | | - {/each} |
446 | | - </tbody> |
447 | | - </table> |
448 | | - </div> |
449 | | - {:else if hasApiAccess} |
450 | | - <div class="empty-state"> |
451 | | - <p>No users found</p> |
452 | | - </div> |
453 | | - {:else} |
454 | | - <div class="empty-state"> |
455 | | - <p>Unable to load users. Please check your API access.</p> |
456 | | - </div> |
457 | | - {/if} |
458 | | - </div> |
459 | | - </div> |
460 | 450 | </div> |
461 | 451 |
|
462 | 452 | <style> |
|
0 commit comments