Skip to content
This repository was archived by the owner on May 12, 2026. It is now read-only.

Commit fbf69fc

Browse files
committed
Users search
1 parent fef7f91 commit fbf69fc

1 file changed

Lines changed: 137 additions & 147 deletions

File tree

src/routes/(protected)/users/+page.svelte

Lines changed: 137 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
<script lang="ts">
22
import { Mail } from "@lucide/svelte";
3-
import { goto } from "$app/navigation";
3+
import { onMount } from "svelte";
44
import { page } from "$app/state";
55
import type { PageData } from "./$types";
66
77
let { data } = $props<{ data: PageData }>();
88
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") ?? "");
1111
let roles = $state<string[]>([]);
1212
let banks = $state<string[]>([]);
1313
@@ -39,7 +39,6 @@
3939
fetchBanks();
4040
});
4141
42-
let users = $derived(data.users || []);
4342
let hasApiAccess = $derived(data.hasApiAccess);
4443
let error = $derived(data.error);
4544
@@ -51,6 +50,8 @@
5150
let searchError = $state<string | null>(null);
5251
let isSearching = $state(false);
5352
let searchType = $state<"email" | "userid" | "username" | "">("");
53+
let sortBy = $state("");
54+
let sortDirection = $state<"asc" | "desc">("desc");
5455
let lastSearchCall = $state<{ proxyUrl: string; obpPath: string; status?: number; responseBody?: string } | null>(null);
5556
5657
// Fetch providers on mount
@@ -69,6 +70,22 @@
6970
fetchProviders();
7071
});
7172
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+
7289
// Detect search type based on input
7390
function detectSearchType(input: string): "email" | "userid" | "username" {
7491
// Check if it's an email
@@ -86,30 +103,38 @@
86103
}
87104
88105
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();
99107
isSearching = true;
100108
searchError = null;
101-
lastSearchedQuery = searchQuery.trim();
109+
lastSearchedQuery = query;
102110
103111
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);
112122
}
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);
113138
}
114139
const proxyUrl = `/proxy/obp/v6.0.0/users?${params.toString()}`;
115140
@@ -193,7 +218,19 @@
193218
}}
194219
class="flex gap-4 items-end flex-wrap"
195220
>
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;">
197234
<label for="provider-select" class="block text-sm font-medium mb-2"
198235
>Provider</label
199236
>
@@ -208,31 +245,92 @@
208245
{/each}
209246
</select>
210247
</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
214251
>
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}
220255
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>
222311
</div>
223312
<button
224313
type="submit"
225314
class="btn btn-primary"
226-
disabled={isSearching || !searchQuery.trim()}
315+
disabled={isSearching}
227316
>
228317
{isSearching ? "Searching..." : "Search"}
229318
</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>
230328
</form>
231329

232330
{#snippet technicalDetails()}
233331
{#if lastSearchCall}
234332
<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>
236334
<div class="search-call-info">
237335
<div class="search-call-field">
238336
<label for="last-search-proxy-url" class="search-call-label">Proxy URL</label>
@@ -282,62 +380,6 @@
282380
{/if}
283381
{/snippet}
284382

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-
341383
{#if searchResults.length > 0}
342384
<div class="mt-6">
343385
<div class="search-results-header mb-4">
@@ -392,71 +434,19 @@
392434
</div>
393435
<div class="alert alert-error">{searchError}</div>
394436
</div>
395-
{:else if lastSearchedQuery && !isSearching}
437+
{:else if lastSearchCall && !isSearching}
396438
<div class="mt-6">
397439
<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>
399443
{@render technicalDetails()}
400444
</div>
401445
</div>
402446
{/if}
403447
</div>
404448
</div>
405449

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>
460450
</div>
461451

462452
<style>

0 commit comments

Comments
 (0)