Files
ss-tools/frontend/src/routes/datasets/[id]/+page.svelte
busya 0350fd6e19 refactor(frontend): migrate inline SVGs to Icon component (10 files, 36 icons)
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)
2026-06-17 15:13:08 +03:00

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} &bull; {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} &bull; {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">&bull; {$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 -->