fix(dashboards): stabilize grid layout and remove owners N+1 fallback
This commit is contained in:
@@ -14,7 +14,7 @@ import json
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple, Union, cast
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union, cast
|
||||
from requests import Response
|
||||
from datetime import datetime
|
||||
from .logger import logger as app_logger, belief_scope
|
||||
@@ -87,7 +87,17 @@ class SupersetClient:
|
||||
app_logger.info("[get_dashboards][Enter] Fetching dashboards.")
|
||||
validated_query = self._validate_query_params(query or {})
|
||||
if 'columns' not in validated_query:
|
||||
validated_query['columns'] = ["slug", "id", "changed_on_utc", "dashboard_title", "published"]
|
||||
validated_query['columns'] = [
|
||||
"slug",
|
||||
"id",
|
||||
"changed_on_utc",
|
||||
"dashboard_title",
|
||||
"published",
|
||||
"created_by",
|
||||
"changed_by",
|
||||
"changed_by_name",
|
||||
"owners",
|
||||
]
|
||||
|
||||
paginated_data = self._fetch_all_pages(
|
||||
endpoint="/dashboard/",
|
||||
@@ -105,53 +115,56 @@ class SupersetClient:
|
||||
# @RETURN: List[Dict]
|
||||
def get_dashboards_summary(self) -> List[Dict]:
|
||||
with belief_scope("SupersetClient.get_dashboards_summary"):
|
||||
query = {
|
||||
"columns": [
|
||||
"id",
|
||||
"dashboard_title",
|
||||
"changed_on_utc",
|
||||
"published",
|
||||
"created_by",
|
||||
"created_by_name",
|
||||
"changed_by",
|
||||
"changed_by_name",
|
||||
"owners",
|
||||
]
|
||||
}
|
||||
# Rely on list endpoint default projection to stay compatible
|
||||
# across Superset versions and preserve owners in one request.
|
||||
query: Dict[str, Any] = {}
|
||||
_, dashboards = self.get_dashboards(query=query)
|
||||
|
||||
# Map fields to DashboardMetadata schema
|
||||
result = []
|
||||
for dash in dashboards:
|
||||
owners = self._extract_owner_labels(dash.get("owners"))
|
||||
# No per-dashboard detail requests here: keep list endpoint O(1).
|
||||
if not owners:
|
||||
owners = self._extract_owner_labels(
|
||||
[dash.get("created_by"), dash.get("changed_by")],
|
||||
)
|
||||
|
||||
result.append({
|
||||
"id": dash.get("id"),
|
||||
"title": dash.get("dashboard_title"),
|
||||
"last_modified": dash.get("changed_on_utc"),
|
||||
"status": "published" if dash.get("published") else "draft",
|
||||
"created_by": self._extract_user_display(
|
||||
dash.get("created_by_name"),
|
||||
None,
|
||||
dash.get("created_by"),
|
||||
),
|
||||
"modified_by": self._extract_user_display(
|
||||
dash.get("changed_by_name"),
|
||||
dash.get("changed_by"),
|
||||
),
|
||||
"owners": self._extract_owner_labels(dash.get("owners")),
|
||||
"owners": owners,
|
||||
})
|
||||
return result
|
||||
# [/DEF:get_dashboards_summary:Function]
|
||||
|
||||
# [DEF:_extract_owner_labels:Function]
|
||||
# @PURPOSE: Normalize dashboard owners payload to stable display labels.
|
||||
# @PRE: owners payload can be None, list of dicts or list of strings.
|
||||
# @PRE: owners payload can be scalar, object or list.
|
||||
# @POST: Returns deduplicated non-empty owner labels preserving order.
|
||||
# @RETURN: List[str]
|
||||
def _extract_owner_labels(self, owners_payload: Optional[List[Union[Dict, str]]]) -> List[str]:
|
||||
if not isinstance(owners_payload, list):
|
||||
def _extract_owner_labels(self, owners_payload: Any) -> List[str]:
|
||||
if owners_payload is None:
|
||||
return []
|
||||
|
||||
owners_list: List[Any]
|
||||
if isinstance(owners_payload, list):
|
||||
owners_list = owners_payload
|
||||
else:
|
||||
owners_list = [owners_payload]
|
||||
|
||||
normalized: List[str] = []
|
||||
for owner in owners_payload:
|
||||
for owner in owners_list:
|
||||
label: Optional[str] = None
|
||||
if isinstance(owner, dict):
|
||||
label = self._extract_user_display(None, owner)
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user