Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"dashboard_notifications_alt": "Notifications",
"dashboard_loading_view": "Loading dashboard view...",
"dashboard_loading_devices": "Loading devices…",
"dashboard_search_placeholder": "Search by location, device name or EUI…",
"error_bad_request_title": "Bad Request",
"error_bad_request_description": "The server could not understand your request. Please check the URL and try again.",
"error_unauthorized_title": "Unauthorized",
Expand Down
1 change: 1 addition & 0 deletions messages/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"dashboard_notifications_alt": "通知",
"dashboard_loading_view": "ダッシュボードを読み込み中...",
"dashboard_loading_devices": "デバイスを読み込み中…",
"dashboard_search_placeholder": "ロケーション、デバイス名、EUIで検索…",
"error_bad_request_title": "不正なリクエスト",
"error_bad_request_description": "サーバーがリクエストを理解できませんでした。URL を確認して再度お試しください。",
"error_unauthorized_title": "認証が必要です",
Expand Down
15 changes: 13 additions & 2 deletions src/lib/components/dashboard/DashboardTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -75,22 +75,33 @@
group: filters.group || undefined,
locationGroup: filters.locationGroup || undefined,
location: filters.location || undefined,
name: query.search?.trim() || filters.name || undefined
name: filters.name || undefined
},
{ signal: query.signal }
);
return { rows: page.rows, total: page.total };
}

// Mapped into CwDataTable's `Record<string, string[]>` filter shape so the
// table re-runs `loadData` whenever the dashboard filters (incl. the search
// box) change. The actual values are read from `filters` inside `loadData`.
const tableFilters = $derived({
group: filters.group ? [filters.group] : [],
locationGroup: filters.locationGroup ? [filters.locationGroup] : [],
location: filters.location ? [filters.location] : [],
name: filters.name ? [filters.name] : []
});
</script>

<CwDataTable
{columns}
{loadData}
filters={tableFilters}
rowKey="dev_eui"
searchable={false}
fillParent
pageSize={25}
pageSizeOptions={[25, 50, 100]}
searchable
onRowClick={(row) => {
if (row.location?.location_id != null) {
goto(resolve(`/locations/${row.location.location_id}/devices/${row.dev_eui}`));
Expand Down
55 changes: 45 additions & 10 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { onMount } from 'svelte';
import Icon from '$lib/components/Icon.svelte';
import { AppPage } from '$lib/components/layout';
import { CwButton } from '@cropwatchdevelopment/cwui';
import { CwButton, CwSearchInput } from '@cropwatchdevelopment/cwui';
import DashboardCards from '$lib/components/dashboard/DashboardCards.svelte';
import DashboardTable from '$lib/components/dashboard/DashboardTable.svelte';
import { getAppContext } from '$lib/appContext.svelte';
Expand All @@ -17,17 +17,30 @@

const VIEW_STORAGE_KEY = 'cropwatch.dashboard.view';
const MOBILE_QUERY = '(max-width: 767px)';
const SEARCH_DEBOUNCE_MS = 300;

const app = getAppContext();

let view = $state<DashboardView>('table');
let viewReady = $state(!browser);

// Free-text search box. `searchName` updates on every keystroke; `debouncedName`
// trails it so the views re-fetch once the user pauses, not on every key.
let searchName = $state(page.url.searchParams.get('name') ?? '');
let debouncedName = $state(page.url.searchParams.get('name') ?? '');
$effect(() => {
const next = searchName;
const timer = setTimeout(() => {
debouncedName = next;
}, SEARCH_DEBOUNCE_MS);
return () => clearTimeout(timer);
});

const filters = $derived({
group: page.url.searchParams.get('group') ?? '',
locationGroup: page.url.searchParams.get('locationGroup') ?? '',
location: page.url.searchParams.get('location') ?? '',
name: page.url.searchParams.get('name') ?? ''
name: debouncedName.trim()
});

function setView(next: DashboardView) {
Expand Down Expand Up @@ -55,12 +68,22 @@
if (!token || sidebarDataLoaded) return;
sidebarDataLoaded = true;
const api = new ApiService({ authToken: token });
api.getLocationGroups().then((groups) => {
app.locationGroups = groups;
}).catch(() => { /* sidebar tolerates an empty list */ });
api.getLocations().then((locations) => {
app.locations = locations;
}).catch(() => { /* sidebar tolerates an empty list */ });
api
.getLocationGroups()
.then((groups) => {
app.locationGroups = groups;
})
.catch(() => {
/* sidebar tolerates an empty list */
});
api
.getLocations()
.then((locations) => {
app.locations = locations;
})
.catch(() => {
/* sidebar tolerates an empty list */
});
});
</script>

Expand All @@ -71,8 +94,20 @@
<AppPage width="full" class="dashboard-page">
<div class="--cw-bg-base flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
<header class="flex-none">
<div class="mb-2 flex w-full flex-col gap-4">
<div class="flex w-full flex-row gap-2 sm:w-auto sm:flex-row sm:flex-wrap sm:items-center sm:justify-end">
<div class="mb-2 flex w-full flex-row gap-4">
<div
class="hidden md:flex w-full flex-row gap-2 sm:w-auto sm:flex-row sm:flex-wrap sm:items-center sm:justify-start"
>
<CwSearchInput
bind:value={searchName}
placeholder={m.dashboard_search_placeholder()}
class="w-full min-w-0"
/>
</div>
<span class="flex-1"></span>
<div
class="flex w-full flex-row gap-2 sm:w-auto sm:flex-row sm:flex-wrap sm:items-center sm:justify-end"
>
<CwButton
class="w-full md:w-auto"
size="sm"
Expand Down
Loading