feat: grouped nav tree with badges, dashboard events section with filtered chart
Navigation: - Restructure flat nav into grouped tree: Notification (Trackers, Configs, Templates), Commands (same), Bots (Telegram), Settings (Common, Users) - Collapsible groups with expand/collapse state persisted in localStorage - Auto-expand group containing the active page - Counter badges on groups (sum of children) and individual items - New /api/status/counts endpoint for nav badge data - Mobile bottom nav uses flattened key pages Dashboard: - Rename "Recent Events" to "Events" - Move chart under Events section (after filters, before event list) - Filters (event type, provider, search) now affect both the event list AND the chart simultaneously - Add event_type, provider_id, search filter params to /api/status/chart
This commit is contained in:
@@ -34,8 +34,8 @@
|
||||
/** Calculate how many event rows fit in the remaining viewport space. */
|
||||
function calcPageSize(): number {
|
||||
if (typeof window === 'undefined') return 8;
|
||||
const EVENT_ROW_HEIGHT = 50; // px per event row (content + gap)
|
||||
const FIXED_OVERHEAD = 600; // header + stats + chart + events header + filters + paginator + padding
|
||||
const EVENT_ROW_HEIGHT = 50;
|
||||
const FIXED_OVERHEAD = 700; // slightly more for chart in Events section
|
||||
const available = window.innerHeight - FIXED_OVERHEAD;
|
||||
return Math.max(3, Math.floor(available / EVENT_ROW_HEIGHT));
|
||||
}
|
||||
@@ -53,13 +53,19 @@
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
|
||||
/** Build filter query string (shared by events list + chart). */
|
||||
function buildFilterParams(): URLSearchParams {
|
||||
const params = new URLSearchParams();
|
||||
if (filterEventType) params.set('event_type', filterEventType);
|
||||
if (filterProviderId) params.set('provider_id', filterProviderId);
|
||||
if (filterSearch) params.set('search', filterSearch);
|
||||
return params;
|
||||
}
|
||||
|
||||
async function loadEvents() {
|
||||
eventsLoading = true;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (filterEventType) params.set('event_type', filterEventType);
|
||||
if (filterProviderId) params.set('provider_id', filterProviderId);
|
||||
if (filterSearch) params.set('search', filterSearch);
|
||||
const params = buildFilterParams();
|
||||
params.set('sort', filterSort);
|
||||
params.set('limit', String(eventsLimit));
|
||||
params.set('offset', String(eventsOffset));
|
||||
@@ -72,9 +78,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadChart() {
|
||||
try {
|
||||
const params = buildFilterParams();
|
||||
const qs = params.toString();
|
||||
const chartRes = await api<any>(`/status/chart${qs ? '?' + qs : ''}`);
|
||||
chartDays = chartRes.days || [];
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
eventsOffset = 0;
|
||||
loadEvents();
|
||||
loadChart();
|
||||
}
|
||||
|
||||
function goToPage(page: number) {
|
||||
@@ -216,8 +232,7 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<EventChart days={chartDays} />
|
||||
|
||||
<!-- Events section -->
|
||||
<h3 class="text-base font-semibold mb-3 flex items-center gap-2">
|
||||
<MdiIcon name="mdiPulse" size={18} />
|
||||
{t('dashboard.recentEvents')}
|
||||
@@ -253,6 +268,9 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Chart (now inside Events section, affected by filters) -->
|
||||
<EventChart days={chartDays} />
|
||||
|
||||
{#if eventsLoading}
|
||||
<Card><p class="text-sm text-center py-4" style="color: var(--color-muted-foreground);">{t('dashboard.loadingEvents')}</p></Card>
|
||||
{:else if status.recent_events.length === 0}
|
||||
|
||||
Reference in New Issue
Block a user