394 lines
14 KiB
Svelte
394 lines
14 KiB
Svelte
<!-- [DEF:DatasetDetail:Page] -->
|
|
<script>
|
|
/**
|
|
* @TIER: CRITICAL
|
|
* @PURPOSE: Dataset Detail View - Shows detailed dataset information with columns, SQL, and linked dashboards
|
|
* @LAYER: UI
|
|
* @RELATION: BINDS_TO -> sidebarStore
|
|
* @INVARIANT: Always shows dataset details when loaded
|
|
*
|
|
* @UX_STATE: Loading -> Shows skeleton loader
|
|
* @UX_STATE: Loaded -> Shows dataset details with columns and linked dashboards
|
|
* @UX_STATE: Error -> Shows error banner with retry button
|
|
* @UX_FEEDBACK: Clicking linked dashboard navigates to dashboard detail
|
|
* @UX_RECOVERY: Refresh button reloads dataset details
|
|
*/
|
|
|
|
import { onMount } from "svelte";
|
|
import { goto } from "$app/navigation";
|
|
import { page } from "$app/stores";
|
|
import { t } from "$lib/i18n";
|
|
import { api } from "$lib/api.js";
|
|
import { openDrawerForTask } from "$lib/stores/taskDrawer.js";
|
|
|
|
// Get dataset ID from URL params
|
|
$: datasetId = $page.params.id;
|
|
$: envId = $page.url.searchParams.get("env_id") || "";
|
|
|
|
// State
|
|
let dataset = null;
|
|
let isLoading = true;
|
|
let error = null;
|
|
|
|
// Load dataset details on mount
|
|
onMount(async () => {
|
|
await loadDatasetDetail();
|
|
});
|
|
|
|
// Load dataset details from API
|
|
async function loadDatasetDetail() {
|
|
if (!datasetId || !envId) {
|
|
error = $t.datasets?.missing_context || "Missing dataset ID or environment ID";
|
|
isLoading = false;
|
|
return;
|
|
}
|
|
|
|
isLoading = true;
|
|
error = null;
|
|
try {
|
|
const response = await api.getDatasetDetail(envId, datasetId);
|
|
dataset = response;
|
|
} catch (err) {
|
|
error = err.message || ($t.datasets?.load_detail_failed || "Failed to load dataset details");
|
|
console.error("[DatasetDetail][Coherence:Failed]", err);
|
|
} finally {
|
|
isLoading = false;
|
|
}
|
|
}
|
|
|
|
// Navigate to linked dashboard
|
|
function navigateToDashboard(dashboardId) {
|
|
goto(`/dashboards/${dashboardId}?env_id=${envId}`);
|
|
}
|
|
|
|
// Navigate back to dataset list
|
|
function goBack() {
|
|
goto(`/datasets?env_id=${envId}`);
|
|
}
|
|
|
|
// Get column type icon/color
|
|
function getColumnTypeClass(type) {
|
|
if (!type) return "text-gray-500";
|
|
const lowerType = type.toLowerCase();
|
|
if (
|
|
lowerType.includes("int") ||
|
|
lowerType.includes("float") ||
|
|
lowerType.includes("num")
|
|
) {
|
|
return "text-blue-600 bg-blue-50";
|
|
} else if (lowerType.includes("date") || lowerType.includes("time")) {
|
|
return "text-green-600 bg-green-50";
|
|
} else if (
|
|
lowerType.includes("str") ||
|
|
lowerType.includes("text") ||
|
|
lowerType.includes("char")
|
|
) {
|
|
return "text-purple-600 bg-purple-50";
|
|
} else if (lowerType.includes("bool")) {
|
|
return "text-orange-600 bg-orange-50";
|
|
}
|
|
return "text-gray-600 bg-gray-50";
|
|
}
|
|
|
|
// Get mapping progress percentage
|
|
function getMappingProgress(column) {
|
|
return column.description ? 100 : 0;
|
|
}
|
|
</script>
|
|
|
|
<div class="max-w-7xl mx-auto px-4 py-6">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between mb-6">
|
|
<div>
|
|
<button
|
|
class="flex items-center gap-2 text-gray-600 hover:text-gray-900 transition-colors"
|
|
on:click={goBack}
|
|
>
|
|
<svg
|
|
width="20"
|
|
height="20"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<path d="M19 12H5M12 19l-7-7 7-7" />
|
|
</svg>
|
|
{$t.common?.back || "Back to Datasets"}
|
|
</button>
|
|
{#if dataset}
|
|
<h1 class="text-2xl font-bold text-gray-900 mt-4">
|
|
{dataset.table_name}
|
|
</h1>
|
|
<p class="text-sm text-gray-500 mt-1">
|
|
{dataset.schema} • {dataset.database}
|
|
</p>
|
|
{:else if !isLoading}
|
|
<h1 class="text-2xl font-bold text-gray-900 mt-4">
|
|
{$t.datasets?.detail_title || "Dataset Details"}
|
|
</h1>
|
|
{/if}
|
|
</div>
|
|
<button
|
|
class="px-4 py-2 bg-destructive text-white rounded hover:bg-destructive-hover transition-colors"
|
|
on:click={loadDatasetDetail}
|
|
>
|
|
{$t.common?.refresh || "Refresh"}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Error Banner -->
|
|
{#if error}
|
|
<div
|
|
class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4 flex items-center justify-between"
|
|
>
|
|
<span>{error}</span>
|
|
<button
|
|
class="px-4 py-2 bg-destructive text-white rounded hover:bg-destructive-hover transition-colors"
|
|
on:click={loadDatasetDetail}
|
|
>
|
|
{$t.common?.retry || "Retry"}
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Loading State -->
|
|
{#if isLoading}
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<div class="bg-white border border-gray-200 rounded-lg p-6">
|
|
<div class="animate-pulse bg-gray-200 rounded h-6 w-1/2 mb-4"></div>
|
|
{#each Array(5) as _}
|
|
<div
|
|
class="flex justify-between py-2 border-b border-gray-100 last:border-0"
|
|
>
|
|
<div class="animate-pulse bg-gray-200 rounded h-4 w-20"></div>
|
|
<div class="animate-pulse bg-gray-200 rounded h-4 w-32"></div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
<div class="bg-white border border-gray-200 rounded-lg p-6 lg:col-span-2">
|
|
<div class="animate-pulse bg-gray-200 rounded h-6 w-1/3 mb-4"></div>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
{#each Array(4) as _}
|
|
<div class="p-3 border border-gray-200 rounded-lg">
|
|
<div
|
|
class="animate-pulse bg-gray-200 rounded h-4 w-full mb-2"
|
|
></div>
|
|
<div class="animate-pulse bg-gray-200 rounded h-3 w-16"></div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{:else if dataset}
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<!-- Dataset Info Card -->
|
|
<div class="bg-white border border-gray-200 rounded-lg p-6">
|
|
<h2 class="text-lg font-semibold text-gray-900 mb-4">
|
|
{$t.datasets?.info || "Dataset Information"}
|
|
</h2>
|
|
<div class="flex justify-between py-2 border-b border-gray-100">
|
|
<span class="text-sm text-gray-500"
|
|
>{$t.datasets?.table_name || "Table Name"}</span
|
|
>
|
|
<span class="text-sm font-medium text-gray-900"
|
|
>{dataset.table_name}</span
|
|
>
|
|
</div>
|
|
<div class="flex justify-between py-2 border-b border-gray-100">
|
|
<span class="text-sm text-gray-500"
|
|
>{$t.datasets?.schema || "Schema"}</span
|
|
>
|
|
<span class="text-sm font-medium text-gray-900"
|
|
>{dataset.schema || "-"}</span
|
|
>
|
|
</div>
|
|
<div class="flex justify-between py-2 border-b border-gray-100">
|
|
<span class="text-sm text-gray-500"
|
|
>{$t.datasets?.database || "Database"}</span
|
|
>
|
|
<span class="text-sm font-medium text-gray-900"
|
|
>{dataset.database}</span
|
|
>
|
|
</div>
|
|
<div class="flex justify-between py-2 border-b border-gray-100">
|
|
<span class="text-sm text-gray-500"
|
|
>{$t.datasets?.columns_count || "Columns"}</span
|
|
>
|
|
<span class="text-sm font-medium text-gray-900"
|
|
>{dataset.column_count}</span
|
|
>
|
|
</div>
|
|
<div class="flex justify-between py-2 border-b border-gray-100">
|
|
<span class="text-sm text-gray-500"
|
|
>{$t.datasets?.linked_dashboards || "Linked Dashboards"}</span
|
|
>
|
|
<span class="text-sm font-medium text-gray-900"
|
|
>{dataset.linked_dashboard_count}</span
|
|
>
|
|
</div>
|
|
{#if dataset.is_sqllab_view}
|
|
<div class="flex justify-between py-2 border-b border-gray-100">
|
|
<span class="text-sm text-gray-500"
|
|
>{$t.datasets?.type || "Type"}</span
|
|
>
|
|
<span class="text-sm font-medium text-gray-900">{$t.datasets?.sql_lab_view || "SQL Lab View"}</span>
|
|
</div>
|
|
{/if}
|
|
{#if dataset.created_on}
|
|
<div class="flex justify-between py-2 border-b border-gray-100">
|
|
<span class="text-sm text-gray-500"
|
|
>{$t.datasets?.created || "Created"}</span
|
|
>
|
|
<span class="text-sm font-medium text-gray-900"
|
|
>{new Date(dataset.created_on).toLocaleDateString()}</span
|
|
>
|
|
</div>
|
|
{/if}
|
|
{#if dataset.changed_on}
|
|
<div class="flex justify-between py-2">
|
|
<span class="text-sm text-gray-500"
|
|
>{$t.datasets?.updated || "Updated"}</span
|
|
>
|
|
<span class="text-sm font-medium text-gray-900"
|
|
>{new Date(dataset.changed_on).toLocaleDateString()}</span
|
|
>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Linked Dashboards Card -->
|
|
{#if dataset.linked_dashboards && dataset.linked_dashboards.length > 0}
|
|
<div class="bg-white border border-gray-200 rounded-lg p-6">
|
|
<h2 class="text-lg font-semibold text-gray-900 mb-4">
|
|
{$t.datasets?.linked_dashboards || "Linked Dashboards"} ({dataset.linked_dashboard_count})
|
|
</h2>
|
|
<div class="space-y-2">
|
|
{#each dataset.linked_dashboards as dashboard}
|
|
<div
|
|
class="flex items-center gap-3 p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors"
|
|
on:click={() => navigateToDashboard(dashboard.id)}
|
|
role="button"
|
|
tabindex="0"
|
|
>
|
|
<div
|
|
class="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center text-blue-600"
|
|
>
|
|
<svg
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
|
<line x1="3" y1="9" x2="21" y2="9" />
|
|
<line x1="9" y1="21" x2="9" y2="9" />
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1">
|
|
<div class="font-medium text-gray-900">{dashboard.title}</div>
|
|
<div class="text-xs text-gray-500">
|
|
ID: {dashboard.id}{#if dashboard.slug}
|
|
• {dashboard.slug}{/if}
|
|
</div>
|
|
</div>
|
|
<svg
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
class="text-gray-400"
|
|
>
|
|
<path d="M9 18l6-6-6-6" />
|
|
</svg>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Columns Card -->
|
|
<div class="bg-white border border-gray-200 rounded-lg p-6 lg:col-span-2">
|
|
<h2 class="text-lg font-semibold text-gray-900 mb-4">
|
|
{$t.datasets?.columns || "Columns"} ({dataset.column_count})
|
|
</h2>
|
|
{#if dataset.columns && dataset.columns.length > 0}
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
{#each dataset.columns as column}
|
|
<div
|
|
class="p-3 border border-gray-200 rounded-lg hover:border-blue-300 transition-colors"
|
|
>
|
|
<div class="flex items-center justify-between mb-2">
|
|
<span class="font-medium text-gray-900">{column.name}</span>
|
|
{#if column.type}
|
|
<span
|
|
class="text-xs px-2 py-1 rounded {getColumnTypeClass(
|
|
column.type,
|
|
)}">{column.type}</span
|
|
>
|
|
{/if}
|
|
</div>
|
|
<div class="flex items-center gap-2 text-xs text-gray-500">
|
|
{#if column.is_dttm}
|
|
<span class="text-xs text-green-600">📅 {$t.datasets?.date_time || "Date/Time"}</span>
|
|
{/if}
|
|
{#if !column.is_active}
|
|
<span class="text-xs text-gray-400">({$t.datasets?.inactive || "Inactive"})</span>
|
|
{/if}
|
|
<span
|
|
class="inline-flex items-center px-2 py-0.5 text-xs rounded-full {column.description
|
|
? 'bg-green-100 text-green-800'
|
|
: 'bg-gray-100 text-gray-600'}"
|
|
>
|
|
{column.description ? `✓ ${$t.datasets?.mapped || "Mapped"}` : ($t.datasets?.unmapped || "Unmapped")}
|
|
</span>
|
|
</div>
|
|
{#if column.description}
|
|
<p class="text-sm text-gray-600 mt-2">{column.description}</p>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{:else}
|
|
<div class="py-8 text-center text-gray-500">
|
|
{$t.datasets?.no_columns || "No columns found"}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- SQL Section (for SQL Lab views) -->
|
|
{#if dataset.sql}
|
|
<div
|
|
class="bg-white border border-gray-200 rounded-lg p-6 mt-6 lg:col-span-3"
|
|
>
|
|
<h2 class="text-lg font-semibold text-gray-900 mb-4">
|
|
{$t.datasets?.sql_query || "SQL Query"}
|
|
</h2>
|
|
<pre
|
|
class="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto text-sm font-mono">{dataset.sql}</pre>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{:else}
|
|
<div class="py-8 text-center text-gray-500">
|
|
<svg
|
|
class="w-16 h-16 mx-auto mb-4 text-gray-400"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<path d="M3 3h18v18H3V3zm16 16V5H5v14h14z" />
|
|
</svg>
|
|
<p>{$t.datasets?.not_found || "Dataset not found"}</p>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- [/DEF:DatasetDetail:Page] -->
|