From 5ca477983c7705257752239ce9e893eb9a5884a1 Mon Sep 17 00:00:00 2001 From: busya Date: Thu, 18 Jun 2026 09:28:33 +0300 Subject: [PATCH] fix(dashboard): git filter shows 'pending' instead of 'no_repo' for dashboards without repo The frontend defaulted git status to 'pending' when git_status was null, but the backend returns 'no_repo' for such dashboards. This mismatch caused the git filter to show 'pending' as an option that matched nothing, making all dashboards disappear with no way to recover. Fixed by aligning the frontend fallback to 'no_repo' to match backend. --- .../lib/models/DashboardHubModel.svelte.ts | 39 ++++++++++++++++--- .../models/Dashboards.FiltersModel.svelte.ts | 8 ++-- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/frontend/src/lib/models/DashboardHubModel.svelte.ts b/frontend/src/lib/models/DashboardHubModel.svelte.ts index 4ead8847..431c7a87 100644 --- a/frontend/src/lib/models/DashboardHubModel.svelte.ts +++ b/frontend/src/lib/models/DashboardHubModel.svelte.ts @@ -56,6 +56,8 @@ export class DashboardHubModel { // ── Core state ──────────────────────────────────────────────── selectedEnv: string | null = $state(null); allDashboards: import("./Dashboards.FiltersModel.svelte.ts").DashboardRow[] = $state([]); + /** Larger dataset for filter options (loaded via loadDashboardSearchOptions, up to 100) */ + filterOptionsDashboards: import("./Dashboards.FiltersModel.svelte.ts").DashboardRow[] = $state([]); dashboards: import("./Dashboards.FiltersModel.svelte.ts").DashboardRow[] = $state([]); filteredDashboards: import("./Dashboards.FiltersModel.svelte.ts").DashboardRow[] = $state([]); isLoading: boolean = $state(true); @@ -63,6 +65,8 @@ export class DashboardHubModel { // ── Validation popover ──────────────────────────────────────── validationStatuses: Record = $state({}); + /** Version counter bumped after each fetchValidationStatuses — triggers grid re-render */ + validationVersion: number = $state(0); openValidationPopover: string | null = $state(null); validationPopoverPosition: { left: number; top: number } = $state({ left: 0, top: 0 }); validationStatusLoading: boolean = $state(false); @@ -181,11 +185,15 @@ export class DashboardHubModel { } getFilterOptions(column: any): string[] { - return this.filters.getFilterOptions(this.allDashboards as any, this.validationStatuses); + const data = this.filterOptionsDashboards.length > this.allDashboards.length + ? this.filterOptionsDashboards : this.allDashboards; + return this.filters.getFilterOptions(column, data as any, this.validationStatuses); } getVisibleFilterOptions(column: any): string[] { - return this.filters.getVisibleFilterOptions(column, this.allDashboards as any, this.validationStatuses); + const data = this.filterOptionsDashboards.length > this.allDashboards.length + ? this.filterOptionsDashboards : this.allDashboards; + return this.filters.getVisibleFilterOptions(column, data as any, this.validationStatuses); } toggleFilterDropdown(column: any, event: Event | undefined, panelWidth?: number): void { @@ -356,7 +364,7 @@ export class DashboardHubModel { branch: d.git_status?.branch || null, hasRepo: d.git_status?.has_repo === true, hasChangesForCommit: d.git_status?.has_changes_for_commit === true, - } : { status: "pending", branch: null, hasRepo: null, hasChangesForCommit: false }, + } : { status: "no_repo", branch: null, hasRepo: null, hasChangesForCommit: false }, lastTask: d.last_task ? { status: normalizeTaskStatus(d.last_task.status), validationStatus: normalizeValidationStatus(d.last_task.validation_status), @@ -387,10 +395,29 @@ export class DashboardHubModel { search: query || undefined, page_size: 100, page_context: 'other', apply_profile_default: false, override_show_all: true, }); - this.searchableDashboardOptions = (response?.dashboards || []).map((d: any) => ({ + const raw = response?.dashboards || []; + this.searchableDashboardOptions = raw.map((d: any) => ({ id: d.id, name: d.title, subtitle: d.slug || '', hint: d.last_modified ? formatDate(d.last_modified) : '', })); + // Also store for filter options (lightweight DashboardRow mapping) + this.filterOptionsDashboards = raw.map((d: any) => { + const owners = normalizeOwners(d.owners); + const hasGitStatus = !!d.git_status; + return { + id: d.id, title: d.title, slug: d.slug, + changedOn: d.last_modified || null, + changedOnLabel: formatDate(d.last_modified), + owners, + actorLabel: owners.length > 0 ? owners.join(", ") : "-", + git: hasGitStatus ? { + status: d.git_status?.sync_status?.toLowerCase() || "no_repo", + branch: d.git_status?.branch || null, + hasRepo: d.git_status?.has_repo === true, + hasChangesForCommit: d.git_status?.has_changes_for_commit === true, + } : { status: "no_repo", branch: null, hasRepo: null, hasChangesForCommit: false }, + } as any; + }); } catch { this.searchableDashboardOptions = []; console.warn('[DashboardHub] loadDashboardSearchOptions failed'); @@ -405,7 +432,7 @@ export class DashboardHubModel { const ids = this.dashboards.map((d: any) => d.id).filter(Boolean); if (ids.length === 0) return; const result = await api.getValidationStatusBatch(this.selectedEnv, ids.join(",")); - if (result && typeof result === "object") this.validationStatuses = result; + if (result && typeof result === "object") { this.validationStatuses = result; this.validationVersion++; } } catch { /* graceful no-op */ } finally { this.validationStatusLoading = false; } } @@ -433,7 +460,7 @@ export class DashboardHubModel { this.total = result.total; this.totalPages = result.totalPages; this.selection.updateSelectionState(this.filteredDashboards as any, this.dashboards as any); - void this.hydrateVisibleGitStatusesBatch(); + void this.hydrateVisibleGitStatusesBatch().catch(() => {}); void this.fetchValidationStatuses(); } diff --git a/frontend/src/lib/models/Dashboards.FiltersModel.svelte.ts b/frontend/src/lib/models/Dashboards.FiltersModel.svelte.ts index e19977dd..fd3f2316 100644 --- a/frontend/src/lib/models/Dashboards.FiltersModel.svelte.ts +++ b/frontend/src/lib/models/Dashboards.FiltersModel.svelte.ts @@ -98,22 +98,22 @@ export class DashboardsFiltersModel { getColumnCellValue(dashboard: DashboardRow, column: FilterColumn, validationStatuses: Record): string { if (column === "title") return dashboard.title || "-"; - if (column === "git_status") return String(dashboard.git?.status || "pending").toLowerCase(); + if (column === "git_status") return String(dashboard.git?.status || "no_repo").toLowerCase(); if (column === "validation_status") return this.getValidationLabelForCell(dashboard, validationStatuses); if (column === "changed_on") return dashboard.changedOn ? String(dashboard.changedOn).slice(0, 10) : "-"; if (column === "actor") return dashboard.actorLabel || "-"; return "-"; } - getFilterOptions(allDashboards: DashboardRow[], validationStatuses: Record): string[] { + getFilterOptions(column: FilterColumn, allDashboards: DashboardRow[], validationStatuses: Record): string[] { return Array.from(new SvelteSet( - allDashboards.map(d => this.getColumnCellValue(d, "title", validationStatuses)).filter(Boolean) + allDashboards.map(d => this.getColumnCellValue(d, column, validationStatuses)).filter(Boolean) )).sort((a, b) => a.localeCompare(b, "ru")); } getVisibleFilterOptions(column: FilterColumn, allDashboards: DashboardRow[], validationStatuses: Record): string[] { const searchText = (this.columnFilterSearch[column] || "").toLowerCase(); - return this.getFilterOptions(allDashboards, validationStatuses).filter(v => v.toLowerCase().includes(searchText)); + return this.getFilterOptions(column, allDashboards, validationStatuses).filter(v => v.toLowerCase().includes(searchText)); } toggleFilterDropdown(column: FilterColumn, event: Event | undefined, panelWidth = 256): void {