fix(dashboards): stabilize grid layout and remove owners N+1 fallback

This commit is contained in:
2026-02-28 10:46:47 +03:00
parent 066747de59
commit 7e43830144
2 changed files with 104 additions and 75 deletions

View File

@@ -69,6 +69,7 @@
let sortColumn = "title";
let sortDirection = "asc";
let openFilterColumn = null;
let filterDropdownPosition = { left: 0, top: 0 };
let columnFilterSearch = {
title: "",
git_status: "",
@@ -482,8 +483,23 @@
* @PRE: column is valid filter key.
* @POST: openFilterColumn updated.
*/
function toggleFilterDropdown(column) {
openFilterColumn = openFilterColumn === column ? null : column;
function toggleFilterDropdown(column, event, panelWidth = 256) {
event?.stopPropagation();
if (openFilterColumn === column) {
openFilterColumn = null;
return;
}
const trigger = event?.currentTarget;
if (trigger?.getBoundingClientRect) {
const rect = trigger.getBoundingClientRect();
const viewportWidth = typeof window !== "undefined" ? window.innerWidth : 1920;
const safeLeft = Math.max(8, Math.min(rect.left, viewportWidth - panelWidth - 8));
filterDropdownPosition = {
left: safeLeft,
top: rect.bottom + 8,
};
}
openFilterColumn = column;
}
// [/DEF:DashboardHub.toggleFilterDropdown:Function]
@@ -1212,8 +1228,8 @@
{#if isLoading}
<div class="bg-white border border-slate-200 rounded-xl shadow-sm overflow-hidden">
<div
class="grid min-w-[1520px] gap-4 px-6 py-3 bg-slate-50 border-b border-slate-200 font-semibold text-[11px] uppercase tracking-wide text-slate-600"
style="grid-template-columns: 40px minmax(250px,2.3fr) minmax(118px,0.9fr) minmax(150px,1fr) minmax(150px,1fr) minmax(300px,2.1fr) minmax(280px,2fr);"
class="grid gap-4 px-6 py-3 bg-slate-50 border-b border-slate-200 font-semibold text-[11px] uppercase tracking-wide text-slate-600"
style="width: max(100%, 1520px); grid-template-columns: 40px minmax(250px,2.3fr) minmax(118px,0.9fr) minmax(150px,1fr) minmax(280px,2fr) minmax(150px,1fr) minmax(300px,2.1fr);"
>
<div class="h-4 rounded bg-gray-200 animate-pulse"></div>
<div class="h-4 rounded bg-gray-200 animate-pulse"></div>
@@ -1225,8 +1241,8 @@
</div>
{#each Array(5) as _}
<div
class="grid min-w-[1520px] gap-4 px-6 py-4 border-b border-slate-200 last:border-b-0 hover:bg-slate-50 transition-colors"
style="grid-template-columns: 40px minmax(250px,2.3fr) minmax(118px,0.9fr) minmax(150px,1fr) minmax(150px,1fr) minmax(300px,2.1fr) minmax(280px,2fr);"
class="grid gap-4 px-6 py-4 border-b border-slate-200 last:border-b-0 hover:bg-slate-50 transition-colors"
style="width: max(100%, 1520px); grid-template-columns: 40px minmax(250px,2.3fr) minmax(118px,0.9fr) minmax(150px,1fr) minmax(280px,2fr) minmax(150px,1fr) minmax(300px,2.1fr);"
>
<div class="h-4 rounded bg-gray-200 animate-pulse"></div>
<div class="h-4 rounded bg-gray-200 animate-pulse"></div>
@@ -1297,11 +1313,11 @@
<!-- Dashboard Grid -->
<div class="bg-white border border-slate-200 rounded-xl shadow-sm">
<div class="overflow-x-auto">
<div class="overflow-x-auto overflow-y-visible">
<!-- Grid Header -->
<div
class="grid min-w-[1520px] gap-4 px-6 py-3 bg-slate-50 border-b border-slate-200 font-semibold text-[11px] uppercase tracking-wide text-slate-600"
style="grid-template-columns: 40px minmax(250px,2.3fr) minmax(118px,0.9fr) minmax(150px,1fr) minmax(150px,1fr) minmax(300px,2.1fr) minmax(280px,2fr);"
class="grid gap-4 px-6 py-3 bg-slate-50 border-b border-slate-200 font-semibold text-[11px] uppercase tracking-wide text-slate-600"
style="width: max(100%, 1520px); grid-template-columns: 40px minmax(250px,2.3fr) minmax(118px,0.9fr) minmax(150px,1fr) minmax(280px,2fr) minmax(150px,1fr) minmax(300px,2.1fr);"
>
<div></div>
<div class="flex items-center gap-2">
@@ -1311,13 +1327,13 @@
<div class="relative column-filter">
<button
class="rounded border px-1.5 py-0.5 text-xs transition-colors {hasColumnFilter('title') ? 'border-blue-400 text-blue-700 bg-blue-50' : 'border-gray-300 text-gray-500 hover:text-gray-700'}"
on:click={() => toggleFilterDropdown("title")}
on:click={(event) => toggleFilterDropdown("title", event, 256)}
title={$t.dashboard?.column_filter}
>
</button>
{#if openFilterColumn === "title"}
<div class="absolute z-30 mt-2 w-64 rounded-lg border border-gray-200 bg-white shadow-lg p-3">
<div class="fixed z-50 w-64 rounded-lg border border-gray-200 bg-white shadow-lg p-3" style="left: {filterDropdownPosition.left}px; top: {filterDropdownPosition.top}px;">
<input
type="text"
class="w-full rounded border border-gray-300 px-2 py-1 text-xs"
@@ -1352,13 +1368,13 @@
<div class="relative column-filter">
<button
class="rounded border px-1.5 py-0.5 text-xs transition-colors {hasColumnFilter('git_status') ? 'border-blue-400 text-blue-700 bg-blue-50' : 'border-gray-300 text-gray-500 hover:text-gray-700'}"
on:click={() => toggleFilterDropdown("git_status")}
on:click={(event) => toggleFilterDropdown("git_status", event, 256)}
title={$t.dashboard?.column_filter}
>
</button>
{#if openFilterColumn === "git_status"}
<div class="absolute z-30 mt-2 w-64 rounded-lg border border-gray-200 bg-white shadow-lg p-3">
<div class="fixed z-50 w-64 rounded-lg border border-gray-200 bg-white shadow-lg p-3" style="left: {filterDropdownPosition.left}px; top: {filterDropdownPosition.top}px;">
<input
type="text"
class="w-full rounded border border-gray-300 px-2 py-1 text-xs"
@@ -1393,13 +1409,13 @@
<div class="relative column-filter">
<button
class="rounded border px-1.5 py-0.5 text-xs transition-colors {hasColumnFilter('llm_status') ? 'border-blue-400 text-blue-700 bg-blue-50' : 'border-gray-300 text-gray-500 hover:text-gray-700'}"
on:click={() => toggleFilterDropdown("llm_status")}
on:click={(event) => toggleFilterDropdown("llm_status", event, 256)}
title={$t.dashboard?.column_filter}
>
</button>
{#if openFilterColumn === "llm_status"}
<div class="absolute z-30 mt-2 w-64 rounded-lg border border-gray-200 bg-white shadow-lg p-3">
<div class="fixed z-50 w-64 rounded-lg border border-gray-200 bg-white shadow-lg p-3" style="left: {filterDropdownPosition.left}px; top: {filterDropdownPosition.top}px;">
<input
type="text"
class="w-full rounded border border-gray-300 px-2 py-1 text-xs"
@@ -1427,6 +1443,9 @@
{/if}
</div>
</div>
<div class="flex items-center gap-2">
{$t.dashboard?.actions}
</div>
<div class="flex items-center gap-2">
<button class="hover:text-gray-900 transition-colors" on:click={() => handleSort("changed_on")}>
{$t.dashboard?.changed_on} {getSortIndicator("changed_on")}
@@ -1434,13 +1453,13 @@
<div class="relative column-filter">
<button
class="rounded border px-1.5 py-0.5 text-xs transition-colors {hasColumnFilter('changed_on') ? 'border-blue-400 text-blue-700 bg-blue-50' : 'border-gray-300 text-gray-500 hover:text-gray-700'}"
on:click={() => toggleFilterDropdown("changed_on")}
on:click={(event) => toggleFilterDropdown("changed_on", event, 256)}
title={$t.dashboard?.column_filter}
>
</button>
{#if openFilterColumn === "changed_on"}
<div class="absolute z-30 mt-2 w-64 rounded-lg border border-gray-200 bg-white shadow-lg p-3">
<div class="fixed z-50 w-64 rounded-lg border border-gray-200 bg-white shadow-lg p-3" style="left: {filterDropdownPosition.left}px; top: {filterDropdownPosition.top}px;">
<input
type="text"
class="w-full rounded border border-gray-300 px-2 py-1 text-xs"
@@ -1475,13 +1494,13 @@
<div class="relative column-filter">
<button
class="rounded border px-1.5 py-0.5 text-xs transition-colors {hasColumnFilter('actor') ? 'border-blue-400 text-blue-700 bg-blue-50' : 'border-gray-300 text-gray-500 hover:text-gray-700'}"
on:click={() => toggleFilterDropdown("actor")}
on:click={(event) => toggleFilterDropdown("actor", event, 288)}
title={$t.dashboard?.column_filter}
>
</button>
{#if openFilterColumn === "actor"}
<div class="absolute z-30 mt-2 w-72 rounded-lg border border-gray-200 bg-white shadow-lg p-3">
<div class="fixed z-50 w-72 rounded-lg border border-gray-200 bg-white shadow-lg p-3" style="left: {filterDropdownPosition.left}px; top: {filterDropdownPosition.top}px;">
<input
type="text"
class="w-full rounded border border-gray-300 px-2 py-1 text-xs"
@@ -1509,16 +1528,13 @@
{/if}
</div>
</div>
<div class="flex items-center">
{$t.dashboard?.actions}
</div>
</div>
<!-- Grid Rows -->
{#each dashboards as dashboard}
<div
class="grid min-w-[1520px] gap-4 px-6 py-4 border-b border-slate-200 last:border-b-0 hover:bg-slate-50 transition-colors text-[13px]"
style="grid-template-columns: 40px minmax(250px,2.3fr) minmax(118px,0.9fr) minmax(150px,1fr) minmax(150px,1fr) minmax(300px,2.1fr) minmax(280px,2fr);"
class="grid gap-4 px-6 py-4 border-b border-slate-200 last:border-b-0 hover:bg-slate-50 transition-colors text-[13px]"
style="width: max(100%, 1520px); grid-template-columns: 40px minmax(250px,2.3fr) minmax(118px,0.9fr) minmax(150px,1fr) minmax(280px,2fr) minmax(150px,1fr) minmax(300px,2.1fr);"
>
<!-- Checkbox -->
<div>
@@ -1586,33 +1602,12 @@
{/if}
</div>
<!-- Changed On -->
<div class="text-xs text-slate-600">
{dashboard.changedOnLabel}
</div>
<!-- Owners -->
<div class="text-xs">
<div class="text-slate-500 text-[10px] uppercase tracking-wide">{$t.dashboard?.owners || "Owners"}</div>
{#if dashboard.owners?.length > 0}
<div class="mt-0.5 flex flex-wrap items-center gap-1">
{#each dashboard.owners as owner (owner)}
<span class="rounded bg-slate-100 px-2 py-0.5 text-xs text-slate-700 truncate max-w-[180px]" title={owner}>
{owner}
</span>
{/each}
</div>
{:else}
<div class="mt-0.5 text-gray-400">-</div>
{/if}
</div>
<!-- Actions -->
<div class="flex items-center">
<div class="flex items-center gap-1">
<div class="flex items-center gap-1.5">
{#if !dashboard.git?.hasRepo}
<button
class="p-2 rounded border border-emerald-200 bg-emerald-50 text-emerald-700 hover:bg-emerald-100 hover:border-emerald-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
class="inline-flex h-8 w-8 items-center justify-center rounded-md border border-slate-200 bg-white text-emerald-600 hover:bg-emerald-50 hover:border-emerald-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
on:click={() => handleGitInit(dashboard)}
disabled={isGitBusy(dashboard.id)}
title={$t.git?.init_repo || "Init Git repository"}
@@ -1630,7 +1625,7 @@
</button>
{:else}
<button
class="p-2 rounded border border-sky-200 bg-sky-50 text-sky-700 hover:bg-sky-100 hover:border-sky-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
class="inline-flex h-8 w-8 items-center justify-center rounded-md border border-slate-200 bg-white text-sky-600 hover:bg-sky-50 hover:border-sky-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
on:click={() => handleGitSync(dashboard)}
disabled={isGitBusy(dashboard.id)}
title={$t.git?.sync || "Sync from Superset"}
@@ -1648,7 +1643,7 @@
</svg>
</button>
<button
class="p-2 rounded border border-orange-200 bg-orange-50 text-orange-700 hover:bg-orange-100 hover:border-orange-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
class="inline-flex h-8 w-8 items-center justify-center rounded-md border border-slate-200 bg-white text-orange-600 hover:bg-orange-50 hover:border-orange-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
on:click={() => handleGitCommit(dashboard)}
disabled={isGitBusy(dashboard.id) ||
!dashboard.git?.hasChangesForCommit}
@@ -1668,7 +1663,7 @@
</svg>
</button>
<button
class="p-2 rounded border border-cyan-200 bg-cyan-50 text-cyan-700 hover:bg-cyan-100 hover:border-cyan-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
class="inline-flex h-8 w-8 items-center justify-center rounded-md border border-slate-200 bg-white text-cyan-600 hover:bg-cyan-50 hover:border-cyan-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
on:click={() => handleGitPull(dashboard)}
disabled={isGitBusy(dashboard.id)}
title={$t.git?.pull || "Pull"}
@@ -1687,7 +1682,7 @@
</svg>
</button>
<button
class="p-2 rounded border border-indigo-200 bg-indigo-50 text-indigo-700 hover:bg-indigo-100 hover:border-indigo-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
class="inline-flex h-8 w-8 items-center justify-center rounded-md border border-slate-200 bg-white text-indigo-600 hover:bg-indigo-50 hover:border-indigo-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
on:click={() => handleGitPush(dashboard)}
disabled={isGitBusy(dashboard.id)}
title={$t.git?.push || "Push"}
@@ -1707,7 +1702,7 @@
</button>
{/if}
<button
class="p-2 rounded border border-blue-200 bg-blue-50 text-blue-700 hover:bg-blue-100 hover:border-blue-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
class="inline-flex h-8 w-8 items-center justify-center rounded-md border border-slate-200 bg-white text-blue-600 hover:bg-blue-50 hover:border-blue-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
on:click={() => handleAction(dashboard, "migrate")}
title={$t.dashboard?.action_migrate}
>
@@ -1723,7 +1718,7 @@
</svg>
</button>
<button
class="p-2 rounded border border-emerald-200 bg-emerald-50 text-emerald-700 hover:bg-emerald-100 hover:border-emerald-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
class="inline-flex h-8 w-8 items-center justify-center rounded-md border border-slate-200 bg-white text-teal-600 hover:bg-teal-50 hover:border-teal-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
on:click={() => handleValidate(dashboard)}
disabled={validatingIds.has(dashboard.id)}
title={$t.dashboard?.action_validate}
@@ -1761,7 +1756,7 @@
{/if}
</button>
<button
class="p-2 rounded border border-amber-200 bg-amber-50 text-amber-700 hover:bg-amber-100 hover:border-amber-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
class="inline-flex h-8 w-8 items-center justify-center rounded-md border border-slate-200 bg-white text-amber-600 hover:bg-amber-50 hover:border-amber-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
on:click={() => handleAction(dashboard, "backup")}
title={$t.dashboard?.action_backup}
>
@@ -1780,6 +1775,27 @@
</button>
</div>
</div>
<!-- Changed On -->
<div class="text-xs text-slate-600">
{dashboard.changedOnLabel}
</div>
<!-- Owners -->
<div class="text-xs">
<div class="text-slate-500 text-[10px] uppercase tracking-wide">{$t.dashboard?.owners || "Owners"}</div>
{#if dashboard.owners?.length > 0}
<div class="mt-0.5 flex flex-wrap items-center gap-1">
{#each dashboard.owners as owner (owner)}
<span class="rounded bg-slate-100 px-2 py-0.5 text-xs text-slate-700 truncate max-w-[180px]" title={owner}>
{owner}
</span>
{/each}
</div>
{:else}
<div class="mt-0.5 text-gray-400">-</div>
{/if}
</div>
</div>
{/each}
</div>