Replaced 36 inline <svg> patterns with <Icon name="..."> from $lib/ui: - GitManager (9), Toast (5), migration/+page (12), datasets/[id] (3), TaskRunner (2), GitWorkspacePanel (2), dashboards/+page (1), dashboards/[id] (1), TaskHistory (1)
206 lines
9.9 KiB
Svelte
206 lines
9.9 KiB
Svelte
<!-- #region DatasetDetail [C:3] [TYPE Page] [SEMANTICS sveltekit, dataset, detail, columns, dashboard] -->
|
|
<!-- @ingroup Routes -->
|
|
<!-- @BRIEF Dataset detail view — renders DatasetDetailModel state. -->
|
|
<!-- @LAYER Page -->
|
|
<!-- @RELATION BINDS_TO -> [DatasetDetailModel] -->
|
|
<!-- @UX_STATE loading -> Skeleton loader shown. -->
|
|
<!-- @UX_STATE loaded -> Dataset details rendered with columns and linked dashboards. -->
|
|
<!-- @UX_STATE error -> Error banner with retry button. -->
|
|
<!-- @UX_FEEDBACK Clicking linked dashboard navigates to dashboard detail. -->
|
|
<!-- @UX_RECOVERY Refresh button reloads dataset details. -->
|
|
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import { page } from '$app/state';
|
|
import { t } from '$lib/i18n/index.svelte.js';
|
|
import { Button, Icon, Skeleton } from '$lib/ui';
|
|
import { DatasetDetailModel } from '$lib/models/DatasetDetailModel.svelte';
|
|
|
|
let datasetId = $derived(page.params.id);
|
|
let envId = $derived(page.url.searchParams.get('env_id') || '');
|
|
|
|
const model = new DatasetDetailModel();
|
|
$effect(() => { model.datasetId = datasetId; });
|
|
$effect(() => { model.envId = envId; });
|
|
|
|
const { getColumnTypeClass } = DatasetDetailModel;
|
|
|
|
onMount(async () => {
|
|
await model.loadDatasetDetail();
|
|
});
|
|
</script>
|
|
|
|
<div class="max-w-7xl mx-auto px-4 py-6">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between mb-6">
|
|
<div>
|
|
<Button variant="ghost" class="flex items-center gap-2 text-text-muted hover:text-text" onclick={() => model.goBack()}>
|
|
<Icon name="back" size={20} strokeWidth={2} />
|
|
{$t.common?.back}
|
|
</Button>
|
|
{#if model.dataset}
|
|
<h1 class="text-2xl font-bold text-text mt-4">{model.dataset.table_name}</h1>
|
|
<p class="text-sm text-text-muted mt-1">{model.dataset.schema} • {model.dataset.database}</p>
|
|
{:else if !model.isLoading}
|
|
<h1 class="text-2xl font-bold text-text mt-4">{$t.datasets?.detail_title}</h1>
|
|
{/if}
|
|
</div>
|
|
<Button variant="destructive" onclick={() => model.loadDatasetDetail()}>
|
|
{$t.common?.refresh}
|
|
</Button>
|
|
</div>
|
|
|
|
<!-- Error Banner -->
|
|
{#if model.error}
|
|
<div class="bg-destructive-light border border-destructive-ring text-destructive px-4 py-3 rounded mb-4 flex items-center justify-between">
|
|
<span>{model.error}</span>
|
|
<Button variant="destructive" onclick={() => model.loadDatasetDetail()}>
|
|
{$t.common?.retry}
|
|
</Button>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Loading Skeleton -->
|
|
{#if model.isLoading}
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<div class="bg-surface-card border border-border rounded-lg p-6">
|
|
<Skeleton variant="line" width="w-1/2" height="h-6" class="mb-4" />
|
|
{#each Array(5) as _}
|
|
<div class="flex justify-between py-2 border-b border-border last:border-0">
|
|
<Skeleton variant="line" width="w-20" />
|
|
<Skeleton variant="line" width="w-32" />
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
<div class="bg-surface-card border border-border rounded-lg p-6 lg:col-span-2">
|
|
<Skeleton variant="line" width="w-1/3" height="h-6" class="mb-4" />
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
{#each Array(4) as _}
|
|
<div class="p-3 border border-border rounded-lg">
|
|
<Skeleton variant="line" class="mb-2" />
|
|
<Skeleton variant="line" width="w-16" height="h-3" />
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{:else if model.dataset}
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<!-- Dataset Info Card -->
|
|
<div class="bg-surface-card border border-border rounded-lg p-6">
|
|
<h2 class="text-lg font-semibold text-text mb-4">{$t.datasets?.info}</h2>
|
|
<div class="flex justify-between py-2 border-b border-border">
|
|
<span class="text-sm text-text-muted">{$t.datasets?.table_name}</span>
|
|
<span class="text-sm font-medium text-text">{model.dataset.table_name}</span>
|
|
</div>
|
|
<div class="flex justify-between py-2 border-b border-border">
|
|
<span class="text-sm text-text-muted">{$t.datasets?.schema}</span>
|
|
<span class="text-sm font-medium text-text">{model.dataset.schema || "-"}</span>
|
|
</div>
|
|
<div class="flex justify-between py-2 border-b border-border">
|
|
<span class="text-sm text-text-muted">{$t.datasets?.database}</span>
|
|
<span class="text-sm font-medium text-text">{model.dataset.database}</span>
|
|
</div>
|
|
<div class="flex justify-between py-2 border-b border-border">
|
|
<span class="text-sm text-text-muted">{$t.datasets?.columns_count}</span>
|
|
<span class="text-sm font-medium text-text">{model.dataset.column_count}</span>
|
|
</div>
|
|
<div class="flex justify-between py-2 border-b border-border">
|
|
<span class="text-sm text-text-muted">{$t.datasets?.linked_dashboards}</span>
|
|
<span class="text-sm font-medium text-text">{model.linkedDashCount}</span>
|
|
</div>
|
|
{#if model.dataset.is_sqllab_view}
|
|
<div class="flex justify-between py-2 border-b border-border">
|
|
<span class="text-sm text-text-muted">{$t.datasets?.type}</span>
|
|
<span class="text-sm font-medium text-text">{$t.datasets?.sql_lab_view}</span>
|
|
</div>
|
|
{/if}
|
|
{#if model.dataset.created_on}
|
|
<div class="flex justify-between py-2 border-b border-border">
|
|
<span class="text-sm text-text-muted">{$t.datasets?.created}</span>
|
|
<span class="text-sm font-medium text-text">{new Date(model.dataset.created_on).toLocaleDateString()}</span>
|
|
</div>
|
|
{/if}
|
|
{#if model.dataset.changed_on}
|
|
<div class="flex justify-between py-2">
|
|
<span class="text-sm text-text-muted">{$t.datasets?.updated}</span>
|
|
<span class="text-sm font-medium text-text">{new Date(model.dataset.changed_on).toLocaleDateString()}</span>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Linked Dashboards Card -->
|
|
{#if model.dataset.linked_dashboards && model.dataset.linked_dashboards.length > 0}
|
|
<div class="bg-surface-card border border-border rounded-lg p-6">
|
|
<h2 class="text-lg font-semibold text-text mb-4">{$t.datasets?.linked_dashboards} ({model.linkedDashCount})</h2>
|
|
<div class="space-y-2">
|
|
{#each model.dataset.linked_dashboards as dashboard}
|
|
<Button type="button" variant="ghost" class="flex w-full items-center gap-3 p-3 border border-border rounded-lg hover:bg-surface-muted transition-colors text-left"
|
|
onclick={() => model.navigateToDashboard(dashboard)}>
|
|
<div class="w-8 h-8 bg-primary-light rounded-lg flex items-center justify-center text-primary">
|
|
<Icon name="dashboard" size={16} strokeWidth={2} />
|
|
</div>
|
|
<div class="flex-1">
|
|
<div class="font-medium text-text">{dashboard.title}</div>
|
|
<div class="text-xs text-text-muted">ID: {dashboard.id}{#if dashboard.slug} • {dashboard.slug}{/if}</div>
|
|
</div>
|
|
<Icon name="chevronRight" size={16} class="text-text-subtle" strokeWidth={2} />
|
|
</Button>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Columns Card -->
|
|
<div class="bg-surface-card border border-border rounded-lg p-6 lg:col-span-2">
|
|
<h2 class="text-lg font-semibold text-text mb-4">{$t.datasets?.columns} ({model.columnCount})</h2>
|
|
{#if model.dataset.columns && model.dataset.columns.length > 0}
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
{#each model.dataset.columns as column}
|
|
<div class="p-3 border border-border rounded-lg hover:border-primary-ring transition-colors">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<span class="font-medium text-text">{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-text-muted">
|
|
{#if column.is_dttm}
|
|
<span class="text-xs text-success">• {$t.datasets?.date_time}</span>
|
|
{/if}
|
|
{#if !column.is_active}
|
|
<span class="text-xs text-text-subtle">({$t.datasets?.inactive})</span>
|
|
{/if}
|
|
<span class="inline-flex items-center px-2 py-0.5 text-xs rounded-full {column.description ? 'bg-success-light text-success' : 'bg-surface-muted text-text-muted'}">
|
|
{column.description ? `✓ ${$t.datasets?.mapped}` : $t.datasets?.unmapped}
|
|
</span>
|
|
</div>
|
|
{#if column.description}
|
|
<p class="text-sm text-text-muted mt-2">{column.description}</p>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{:else}
|
|
<div class="py-8 text-center text-text-muted">{$t.datasets?.no_columns}</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- SQL Section -->
|
|
{#if model.dataset.sql}
|
|
<div class="bg-surface-card border border-border rounded-lg p-6 mt-6 lg:col-span-3">
|
|
<h2 class="text-lg font-semibold text-text mb-4">{$t.datasets?.sql_query}</h2>
|
|
<pre class="bg-terminal-bg text-terminal-bright p-4 rounded-lg overflow-x-auto text-sm font-mono">{model.dataset.sql}</pre>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{:else}
|
|
<div class="py-8 text-center text-text-muted">
|
|
<svg class="w-16 h-16 mx-auto mb-4 text-text-subtle" 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}</p>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
<!-- #endregion DatasetDetail -->
|