This commit is contained in:
2026-02-19 17:43:45 +03:00
parent c2a4c8062a
commit c8029ed309
28 changed files with 3369 additions and 1297 deletions

View File

@@ -363,183 +363,19 @@
}
</script>
<style>
.container {
@apply max-w-7xl mx-auto px-4 py-6;
}
.header {
@apply flex items-center justify-between mb-6;
}
.title {
@apply text-2xl font-bold text-gray-900;
}
.env-selector {
@apply flex items-center space-x-4;
}
.env-dropdown {
@apply px-4 py-2 border border-gray-300 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-blue-500;
}
.refresh-btn {
@apply px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors;
}
.search-input {
@apply px-4 py-2 border border-gray-300 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-blue-500;
}
.error-banner {
@apply bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4 flex items-center justify-between;
}
.retry-btn {
@apply px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition-colors;
}
.toolbar {
@apply flex items-center justify-between mb-4 gap-4;
}
.selection-buttons {
@apply flex items-center gap-2;
}
.dataset-grid {
@apply bg-white border border-gray-200 rounded-lg overflow-hidden;
}
.grid-header {
@apply grid grid-cols-12 gap-4 px-6 py-3 bg-gray-50 border-b border-gray-200 font-semibold text-sm text-gray-700;
}
.grid-row {
@apply grid grid-cols-12 gap-4 px-6 py-4 border-b border-gray-200 hover:bg-gray-50 transition-colors;
}
.grid-row:last-child {
@apply border-b-0;
}
.col-checkbox {
@apply col-span-1;
}
.col-table-name {
@apply col-span-3 font-medium text-gray-900;
}
.col-schema {
@apply col-span-2;
}
.col-mapping {
@apply col-span-2;
}
.col-task {
@apply col-span-3;
}
.col-actions {
@apply col-span-1;
}
.mapping-progress {
@apply w-24 h-2 rounded-full overflow-hidden;
}
.mapping-bar {
@apply h-full transition-all duration-300;
}
.task-status {
@apply inline-flex items-center space-x-2 cursor-pointer hover:text-blue-600 transition-colors;
}
.action-btn {
@apply px-3 py-1 text-sm border border-gray-300 rounded hover:bg-gray-100 transition-colors;
}
.action-btn.primary {
@apply bg-blue-600 text-white border-blue-600 hover:bg-blue-700;
}
.empty-state {
@apply py-12 text-center text-gray-500;
}
.skeleton {
@apply animate-pulse bg-gray-200 rounded;
}
.floating-panel {
@apply fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 shadow-lg p-4 transition-transform transform translate-y-full;
}
.floating-panel.visible {
@apply transform translate-y-0;
}
.modal-overlay {
@apply fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50;
}
.modal {
@apply bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[80vh] overflow-y-auto;
}
.modal-header {
@apply px-6 py-4 border-b border-gray-200 flex items-center justify-between relative;
}
.close-modal-btn {
@apply absolute top-4 right-4 p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-full transition-all;
}
.modal-body {
@apply px-6 py-4;
}
.modal-footer {
@apply px-6 py-4 border-t border-gray-200 flex justify-end gap-3;
}
.pagination {
@apply flex items-center justify-between px-4 py-3 bg-gray-50 border-t border-gray-200;
}
.pagination-info {
@apply text-sm text-gray-600;
}
.pagination-controls {
@apply flex items-center gap-2;
}
.page-btn {
@apply px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed;
}
.page-btn.active {
@apply bg-blue-600 text-white border-blue-600;
}
</style>
<div class="container">
<div class="max-w-[80rem] mx-auto px-4 pt-6 pb-6">
<!-- Header -->
<div class="header">
<h1 class="title">{$t.nav?.datasets || 'Datasets'}</h1>
<div class="env-selector">
<select class="env-dropdown" bind:value={selectedEnv} on:change={handleEnvChange}>
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-gray-900">{$t.nav?.datasets || 'Datasets'}</h1>
<div class="flex items-center gap-4">
<select class="px-2 py-1 border border-gray-300 rounded bg-white focus:outline-none focus:ring-2 focus:ring-blue-500" bind:value={selectedEnv} on:change={handleEnvChange}>
{#each environments as env}
<option value={env.id}>{env.name}</option>
{/each}
</select>
<button class="refresh-btn" on:click={loadDatasets}>
<button class="px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700" on:click={loadDatasets}>
{$t.common?.refresh || 'Refresh'}
</button>
</div>
@@ -547,9 +383,9 @@
<!-- Error Banner -->
{#if error}
<div class="error-banner">
<div class="bg-red-100 border border-red-400 text-red-700 px-3 py-2 rounded mb-4 flex items-center justify-between">
<span>{error}</span>
<button class="retry-btn" on:click={loadDatasets}>
<button class="px-2 py-1 bg-red-600 text-white rounded hover:bg-red-700" on:click={loadDatasets}>
{$t.common?.retry || 'Retry'}
</button>
</div>
@@ -557,29 +393,29 @@
<!-- Loading State -->
{#if isLoading}
<div class="dataset-grid">
<div class="grid-header">
<div class="col-checkbox skeleton h-4"></div>
<div class="col-table-name skeleton h-4"></div>
<div class="col-schema skeleton h-4"></div>
<div class="col-mapping skeleton h-4"></div>
<div class="col-task skeleton h-4"></div>
<div class="col-actions skeleton h-4"></div>
<div class="bg-white border border-gray-200 rounded overflow-hidden">
<div class="grid grid-cols-12 gap-4 px-6 py-3 bg-gray-50 border-b border-gray-200 text-sm font-semibold text-gray-500">
<div class="col-span-1 animate-pulse bg-gray-200 rounded h-4"></div>
<div class="col-span-3 animate-pulse bg-gray-200 rounded h-4"></div>
<div class="col-span-2 animate-pulse bg-gray-200 rounded h-4"></div>
<div class="col-span-2 animate-pulse bg-gray-200 rounded h-4"></div>
<div class="col-span-3 animate-pulse bg-gray-200 rounded h-4"></div>
<div class="col-span-1 animate-pulse bg-gray-200 rounded h-4"></div>
</div>
{#each Array(5) as _}
<div class="grid-row">
<div class="col-checkbox skeleton h-4"></div>
<div class="col-table-name skeleton h-4"></div>
<div class="col-schema skeleton h-4"></div>
<div class="col-mapping skeleton h-4"></div>
<div class="col-task skeleton h-4"></div>
<div class="col-actions skeleton h-4"></div>
<div class="grid grid-cols-12 gap-4 px-6 py-4 border-b border-gray-200">
<div class="col-span-1 animate-pulse bg-gray-200 rounded h-4"></div>
<div class="col-span-3 animate-pulse bg-gray-200 rounded h-4"></div>
<div class="col-span-2 animate-pulse bg-gray-200 rounded h-4"></div>
<div class="col-span-2 animate-pulse bg-gray-200 rounded h-4"></div>
<div class="col-span-3 animate-pulse bg-gray-200 rounded h-4"></div>
<div class="col-span-1 animate-pulse bg-gray-200 rounded h-4"></div>
</div>
{/each}
</div>
{:else if datasets.length === 0}
<!-- Empty State -->
<div class="empty-state">
<div class="py-12 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>
@@ -587,17 +423,17 @@
</div>
{:else}
<!-- Toolbar -->
<div class="toolbar">
<div class="selection-buttons">
<div class="flex items-center justify-between mb-4 gap-4">
<div class="flex items-center gap-2">
<button
class="action-btn"
class="px-2 py-1 text-sm border border-gray-300 rounded hover:bg-gray-100"
on:click={handleSelectAll}
disabled={total === 0}
>
{isAllSelected ? 'Deselect All' : 'Select All'}
</button>
<button
class="action-btn"
class="px-2 py-1 text-sm border border-gray-300 rounded hover:bg-gray-100"
on:click={handleSelectVisible}
disabled={datasets.length === 0}
>
@@ -612,7 +448,7 @@
<div>
<input
type="text"
class="search-input"
class="px-2 py-1 border border-gray-300 rounded bg-white focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Search datasets..."
on:input={handleSearch}
value={searchQuery}
@@ -621,22 +457,22 @@
</div>
<!-- Dataset Grid -->
<div class="dataset-grid">
<div class="bg-white border border-gray-200 rounded overflow-hidden">
<!-- Grid Header -->
<div class="grid-header">
<div class="col-checkbox"></div>
<div class="col-table-name">{$t.datasets?.table_name || 'Table Name'}</div>
<div class="col-schema">{$t.datasets?.schema || 'Schema'}</div>
<div class="col-mapping">{$t.datasets?.mapped_fields || 'Mapped Fields'}</div>
<div class="col-task">{$t.datasets?.last_task || 'Last Task'}</div>
<div class="col-actions">{$t.datasets?.actions || 'Actions'}</div>
<div class="grid grid-cols-12 gap-4 px-6 py-3 bg-gray-50 border-b border-gray-200 text-sm font-semibold text-gray-500">
<div class="col-span-1"></div>
<div class="col-span-3 font-medium text-gray-700">{$t.datasets?.table_name || 'Table Name'}</div>
<div class="col-span-2">{$t.datasets?.schema || 'Schema'}</div>
<div class="col-span-2">{$t.datasets?.mapped_fields || 'Mapped Fields'}</div>
<div class="col-span-3">{$t.datasets?.last_task || 'Last Task'}</div>
<div class="col-span-1">{$t.datasets?.actions || 'Actions'}</div>
</div>
<!-- Grid Rows -->
{#each datasets as dataset}
<div class="grid-row">
<div class="grid grid-cols-12 gap-4 px-6 py-4 border-b border-gray-200 hover:bg-gray-50 last:border-b-0">
<!-- Checkbox -->
<div class="col-checkbox">
<div class="col-span-1">
<input
type="checkbox"
checked={selectedIds.has(dataset.id)}
@@ -645,7 +481,7 @@
</div>
<!-- Table Name -->
<div class="col-table-name">
<div class="col-span-3 font-medium text-gray-900">
<a
href={`/datasets/${dataset.id}?env_id=${selectedEnv}`}
class="text-blue-600 hover:text-blue-800 hover:underline"
@@ -655,15 +491,15 @@
</div>
<!-- Schema -->
<div class="col-schema">
<div class="col-span-2">
{dataset.schema}
</div>
<!-- Mapping Progress -->
<div class="col-mapping">
<div class="col-span-2">
{#if dataset.mappedFields}
<div class="mapping-progress" title="{$t.datasets?.mapped_of_total || 'Mapped of total'}: {dataset.mappedFields.mapped} / {dataset.mappedFields.total}">
<div class="mapping-bar {getMappingProgressClass(dataset.mappedFields.mapped, dataset.mappedFields.total)}" style="width: {dataset.mappedFields.mapped / dataset.mappedFields.total * 100}%"></div>
<div class="w-24 h-2 rounded-full overflow-hidden" title="{$t.datasets?.mapped_of_total || 'Mapped of total'}: {dataset.mappedFields.mapped} / {dataset.mappedFields.total}">
<div class="h-full transition-all {getMappingProgressClass(dataset.mappedFields.mapped, dataset.mappedFields.total)}" style="width: {dataset.mappedFields.mapped / dataset.mappedFields.total * 100}%"></div>
</div>
{:else}
<span class="text-gray-400">-</span>
@@ -671,10 +507,10 @@
</div>
<!-- Last Task -->
<div class="col-task">
<div class="col-span-3">
{#if dataset.lastTask}
<div
class="task-status"
class="inline-flex items-center gap-2 cursor-pointer hover:text-blue-600"
on:click={() => handleTaskStatusClick(dataset)}
role="button"
tabindex="0"
@@ -699,10 +535,10 @@
</div>
<!-- Actions -->
<div class="col-actions">
<div class="col-span-1">
{#if dataset.actions.includes('map_columns')}
<button
class="action-btn primary"
class="px-1 py-0.5 text-sm bg-blue-600 text-white rounded hover:bg-blue-700"
on:click={() => handleAction(dataset, 'map_columns')}
aria-label={$t.datasets?.action_map_columns || 'Map Columns'}
>
@@ -716,20 +552,20 @@
<!-- Pagination -->
{#if totalPages > 1}
<div class="pagination">
<div class="pagination-info">
<div class="flex items-center justify-between px-3 py-3 bg-gray-50 border-t border-gray-200">
<div class="text-sm text-gray-500">
Showing {((currentPage - 1) * pageSize) + 1}-{Math.min(currentPage * pageSize, total)} of {total}
</div>
<div class="pagination-controls">
<div class="flex items-center gap-2">
<button
class="page-btn"
class="px-2 py-1 text-sm border border-gray-300 rounded hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
on:click={() => handlePageChange(1)}
disabled={currentPage === 1}
>
First
</button>
<button
class="page-btn"
class="px-2 py-1 text-sm border border-gray-300 rounded hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
on:click={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
>
@@ -737,21 +573,21 @@
</button>
{#each Array.from({length: totalPages}, (_, i) => i + 1) as pageNum}
<button
class="page-btn {pageNum === currentPage ? 'active' : ''}"
class="px-2 py-1 text-sm border rounded {pageNum === currentPage ? 'bg-blue-600 text-white border-blue-600' : 'border-gray-300 hover:bg-gray-100'}"
on:click={() => handlePageChange(pageNum)}
>
{pageNum}
</button>
{/each}
<button
class="page-btn"
class="px-2 py-1 text-sm border border-gray-300 rounded hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
on:click={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
>
Next
</button>
<button
class="page-btn"
class="px-2 py-1 text-sm border border-gray-300 rounded hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
on:click={() => handlePageChange(totalPages)}
disabled={currentPage === totalPages}
>
@@ -760,7 +596,7 @@
</div>
<div>
<select
class="env-dropdown"
class="px-2 py-1 border border-gray-300 rounded bg-white focus:outline-none focus:ring-2 focus:ring-blue-500"
value={pageSize}
on:change={handlePageSizeChange}
>
@@ -776,7 +612,7 @@
<!-- Floating Bulk Action Panel -->
{#if selectedIds.size > 0}
<div class="floating-panel visible">
<div class="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 shadow-lg p-4 translate-y-0">
<div class="flex items-center justify-between max-w-7xl mx-auto">
<div class="flex items-center gap-4">
<span class="font-medium">
@@ -785,19 +621,19 @@
</div>
<div class="flex gap-3">
<button
class="action-btn primary"
class="px-3 py-1.5 bg-blue-600 text-white rounded hover:bg-blue-700"
on:click={() => showMapColumnsModal = true}
>
Map Columns
</button>
<button
class="action-btn primary"
class="px-3 py-1.5 bg-blue-600 text-white rounded hover:bg-blue-700"
on:click={() => showGenerateDocsModal = true}
>
Generate Docs
</button>
<button
class="action-btn"
class="px-3 py-1.5 border border-gray-300 rounded hover:bg-gray-100"
on:click={() => selectedIds.clear()}
>
Cancel
@@ -810,23 +646,23 @@
<!-- Map Columns Modal -->
{#if showMapColumnsModal}
<div class="modal-overlay" on:click={() => showMapColumnsModal = false}>
<div class="modal" on:click|stopPropagation>
<div class="modal-header">
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50" on:click={() => showMapColumnsModal = false}>
<div class="bg-white rounded-lg shadow-2xl max-w-xl w-full m-4 max-h-[80vh] overflow-y-auto" on:click|stopPropagation>
<div class="px-4 py-3 border-b border-gray-200 flex items-center justify-between relative">
<h2 class="text-xl font-bold">Bulk Column Mapping</h2>
<button on:click={() => showMapColumnsModal = false} class="close-modal-btn" aria-label="Close modal">
<button on:click={() => showMapColumnsModal = false} class="absolute top-4 right-4 p-1 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-full" aria-label="Close modal">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-body">
<div class="p-4">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-2">Source Type</label>
<select
class="env-dropdown w-full"
class="w-full px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
bind:value={mapSourceType}
>
<option value="postgresql">PostgreSQL Comments</option>
@@ -838,7 +674,7 @@
<label class="block text-sm font-medium mb-2">Connection ID</label>
<input
type="text"
class="search-input w-full"
class="w-full px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter connection ID..."
bind:value={mapConnectionId}
/>
@@ -869,11 +705,11 @@
</div>
</div>
</div>
<div class="modal-footer">
<button class="action-btn" on:click={() => showMapColumnsModal = false}>Cancel</button>
<div class="px-4 py-3 border-t border-gray-200 flex justify-end gap-3">
<button class="px-3 py-1.5 border border-gray-300 rounded hover:bg-gray-100" on:click={() => showMapColumnsModal = false}>Cancel</button>
<button
type="button"
class="action-btn primary"
class="px-3 py-1.5 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
on:click|preventDefault={handleBulkMapColumns}
disabled={selectedIds.size === 0 || (mapSourceType === 'postgresql' && !mapConnectionId) || (mapSourceType === 'xlsx' && (!mapFileData || mapFileData.length === 0))}
>
@@ -886,23 +722,23 @@
<!-- Generate Docs Modal -->
{#if showGenerateDocsModal}
<div class="modal-overlay" on:click={() => showGenerateDocsModal = false}>
<div class="modal" on:click|stopPropagation>
<div class="modal-header">
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50" on:click={() => showGenerateDocsModal = false}>
<div class="bg-white rounded-lg shadow-2xl max-w-xl w-full m-4 max-h-[80vh] overflow-y-auto" on:click|stopPropagation>
<div class="px-4 py-3 border-b border-gray-200 flex items-center justify-between relative">
<h2 class="text-xl font-bold">Bulk Documentation Generation</h2>
<button on:click={() => showGenerateDocsModal = false} class="close-modal-btn" aria-label="Close modal">
<button on:click={() => showGenerateDocsModal = false} class="absolute top-4 right-4 p-1 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-full" aria-label="Close modal">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-body">
<div class="p-4">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-2">LLM Provider</label>
<select
class="env-dropdown w-full"
class="w-full px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
bind:value={llmProvider}
>
<option value="">Select LLM provider...</option>
@@ -925,10 +761,10 @@
</div>
</div>
</div>
<div class="modal-footer">
<button class="action-btn" on:click={() => showGenerateDocsModal = false}>Cancel</button>
<div class="px-4 py-3 border-t border-gray-200 flex justify-end gap-3">
<button class="px-3 py-1.5 border border-gray-300 rounded hover:bg-gray-100" on:click={() => showGenerateDocsModal = false}>Cancel</button>
<button
class="action-btn primary"
class="px-3 py-1.5 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
on:click={handleBulkGenerateDocs}
disabled={!llmProvider || selectedIds.size === 0}
>
@@ -940,4 +776,5 @@
{/if}
</div>
<!-- [/DEF:DatasetHub:Page] -->