feat(datasets): add tooltips, text hints and icon-only action buttons
StatsBar — title-подсказки на все кнопки-фильтры. DatasetList — action-кнопки заменены на SVG-иконки с tooltip (title="map_columns_tip" / "generate_docs_tip"), title на select/deselect и пагинацию. +page.svelte — title на refresh, bulk-панель, обе модалки (маппинг колонок и генерация docs): source_type_tip, database_tip, sql_query_tip, file_input_tip, llm_provider_tip и текстовые хинты. Добавлены id на form-элементы для a11y. i18n: 20 новых tooltip-ключей в ru/en.
This commit is contained in:
@@ -56,5 +56,29 @@
|
||||
"save_description": "Save description",
|
||||
"add_description": "Add description",
|
||||
"columns": "Columns",
|
||||
"detail_empty": "No data"
|
||||
"detail_empty": "No data",
|
||||
"filter_all_tip": "Show all datasets in the environment",
|
||||
"filter_unmapped_tip": "Datasets without mapped columns",
|
||||
"filter_mapped_tip": "Datasets with mapped columns",
|
||||
"filter_linked_tip": "Datasets linked to Superset dashboards",
|
||||
"refresh_tip": "Refresh the dataset list",
|
||||
"select_all_tip": "Select all datasets on this page",
|
||||
"deselect_all_tip": "Deselect all datasets",
|
||||
"map_columns_tip": "Run column mapping for this dataset",
|
||||
"generate_docs_tip": "Generate column descriptions via LLM",
|
||||
"pagination_first_tip": "First page",
|
||||
"pagination_prev_tip": "Previous page",
|
||||
"pagination_next_tip": "Next page",
|
||||
"pagination_last_tip": "Last page",
|
||||
"bulk_map_tip": "Bulk column mapping for selected datasets",
|
||||
"bulk_docs_tip": "Bulk documentation generation for selected datasets",
|
||||
"clear_selection_tip": "Deselect all datasets",
|
||||
"source_type_tip": "Select data source: SQL Lab (Superset DB) or XLSX file",
|
||||
"database_tip": "Superset database to look up column mappings",
|
||||
"sql_query_tip": "Custom SQL query for mapping. Leave empty for automatic information_schema lookup",
|
||||
"file_input_tip": "XLSX file with column mapping data",
|
||||
"start_mapping_tip": "Start mapping task for selected datasets",
|
||||
"cancel_tip": "Cancel and close",
|
||||
"llm_provider_tip": "Select LLM provider for documentation generation",
|
||||
"start_docs_tip": "Start documentation generation for selected datasets"
|
||||
}
|
||||
|
||||
@@ -56,5 +56,29 @@
|
||||
"save_description": "Сохранить описание",
|
||||
"add_description": "Добавить описание",
|
||||
"columns": "Колонки",
|
||||
"detail_empty": "Нет данных"
|
||||
"detail_empty": "Нет данных",
|
||||
"filter_all_tip": "Показать все датасеты в окружении",
|
||||
"filter_unmapped_tip": "Датасеты без размеченных колонок",
|
||||
"filter_mapped_tip": "Датасеты с размеченными колонками",
|
||||
"filter_linked_tip": "Датасеты, связанные с дашбордами Superset",
|
||||
"refresh_tip": "Обновить список датасетов",
|
||||
"select_all_tip": "Выбрать все датасеты на странице",
|
||||
"deselect_all_tip": "Снять выбор со всех датасетов",
|
||||
"map_columns_tip": "Запустить маппинг колонок для этого датасета",
|
||||
"generate_docs_tip": "Сгенерировать описание колонок через LLM",
|
||||
"pagination_first_tip": "Первая страница",
|
||||
"pagination_prev_tip": "Предыдущая страница",
|
||||
"pagination_next_tip": "Следующая страница",
|
||||
"pagination_last_tip": "Последняя страница",
|
||||
"bulk_map_tip": "Массовый маппинг колонок для выбранных датасетов",
|
||||
"bulk_docs_tip": "Массовая генерация документации для выбранных датасетов",
|
||||
"clear_selection_tip": "Снять выбор со всех датасетов",
|
||||
"source_type_tip": "Выберите источник данных: SQL Lab (база Superset) или XLSX-файл",
|
||||
"database_tip": "База данных Superset для поиска соответствий колонок",
|
||||
"sql_query_tip": "Произвольный SQL-запрос для маппинга. Оставьте пустым для автоматического поиска по information_schema",
|
||||
"file_input_tip": "XLSX-файл с данными маппинга колонок",
|
||||
"start_mapping_tip": "Запустить задачу маппинга для выбранных датасетов",
|
||||
"cancel_tip": "Отменить и закрыть",
|
||||
"llm_provider_tip": "Выберите LLM-провайдера для генерации документации",
|
||||
"start_docs_tip": "Запустить генерацию документации для выбранных датасетов"
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@
|
||||
<span class="px-2 py-1 text-xs rounded border border-gray-300 bg-gray-50 text-gray-600">
|
||||
{$t.datasets?.all}: {stats?.total ?? 0}
|
||||
</span>
|
||||
<button class="px-3 py-1 text-sm bg-primary text-white rounded hover:bg-primary-hover" onclick={() => loadDatasets()}>
|
||||
<button class="px-3 py-1 text-sm bg-primary text-white rounded hover:bg-primary-hover" title={$t.datasets?.refresh_tip} onclick={() => loadDatasets()}>
|
||||
{$t.common?.refresh ?? '⟳'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -331,13 +331,13 @@
|
||||
<div class="max-w-screen-2xl mx-auto flex items-center justify-between">
|
||||
<span class="font-medium text-sm">✓ {selectedIds.size} {$t.datasets?.selected}</span>
|
||||
<div class="flex gap-2">
|
||||
<button class="px-3 py-1.5 text-sm bg-primary text-white rounded hover:bg-primary-hover" onclick={() => { showMapColumnsModal = true; mapSourceType = "sqllab"; mapDatabaseId = null; mapSqlQuery = ""; mapFileData = null; if (selectedEnv) api.getEnvironmentDatabases(selectedEnv).then(dbs => mapDatabases = dbs ?? []).catch(() => {}); }}>
|
||||
<button class="px-3 py-1.5 text-sm bg-primary text-white rounded hover:bg-primary-hover" title={$t.datasets?.bulk_map_tip} onclick={() => { showMapColumnsModal = true; mapSourceType = "sqllab"; mapDatabaseId = null; mapSqlQuery = ""; mapFileData = null; if (selectedEnv) api.getEnvironmentDatabases(selectedEnv).then(dbs => mapDatabases = dbs ?? []).catch(() => {}); }}>
|
||||
🗺 {$t.datasets?.action_map_columns}
|
||||
</button>
|
||||
<button class="px-3 py-1.5 text-sm bg-primary text-white rounded hover:bg-primary-hover" onclick={() => { showGenerateDocsModal = true; }}>
|
||||
<button class="px-3 py-1.5 text-sm bg-primary text-white rounded hover:bg-primary-hover" title={$t.datasets?.bulk_docs_tip} onclick={() => { showGenerateDocsModal = true; }}>
|
||||
📄 {$t.datasets?.generate_docs}
|
||||
</button>
|
||||
<button class="px-3 py-1.5 text-sm border border-gray-300 rounded hover:bg-gray-100" onclick={() => selectedIds = new SvelteSet()}>
|
||||
<button class="px-3 py-1.5 text-sm border border-gray-300 rounded hover:bg-gray-100" title={$t.datasets?.clear_selection_tip} onclick={() => selectedIds = new SvelteSet()}>
|
||||
✕ {$t.common?.cancel ?? 'Clear'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -351,34 +351,54 @@
|
||||
<div class="bg-white rounded-lg shadow-xl p-6 max-w-md w-full" style="max-height: 90vh; overflow-y: auto;" onclick={(e) => e.stopPropagation()}>
|
||||
<h3 class="text-lg font-semibold mb-4">{$t.datasets?.bulk_map_columns}</h3>
|
||||
<p class="text-sm text-gray-600 mb-3">{$t.datasets?.selected_count?.replace('{count}', String(selectedIds.size))}</p>
|
||||
<select class="w-full border rounded px-2 py-1.5 mb-3" bind:value={mapSourceType} onchange={() => { if (mapSourceType === 'sqllab' && mapDatabases.length === 0 && selectedEnv) { api.getEnvironmentDatabases(selectedEnv).then(dbs => mapDatabases = dbs ?? []).catch(() => {}); } }}>
|
||||
<select
|
||||
class="w-full border rounded px-2 py-1.5 mb-3"
|
||||
title={$t.datasets?.source_type_tip}
|
||||
bind:value={mapSourceType}
|
||||
onchange={() => { if (mapSourceType === 'sqllab' && mapDatabases.length === 0 && selectedEnv) { api.getEnvironmentDatabases(selectedEnv).then(dbs => mapDatabases = dbs ?? []).catch(() => {}); } }}
|
||||
>
|
||||
<option value="sqllab">{$t.datasets?.source_sqllab ?? 'Superset SQL Lab'}</option>
|
||||
<option value="xlsx">{$t.datasets?.source_xlsx}</option>
|
||||
</select>
|
||||
{#if mapSourceType === "sqllab"}
|
||||
<label class="text-sm font-medium mb-1 block">{$t.datasets?.source_database ?? 'База данных'}</label>
|
||||
<select class="w-full border rounded px-2 py-1.5 mb-3" bind:value={mapDatabaseId}>
|
||||
<label class="text-sm font-medium mb-1 block" for="map-database-select">{$t.datasets?.source_database ?? 'База данных'}</label>
|
||||
<select
|
||||
id="map-database-select"
|
||||
class="w-full border rounded px-2 py-1.5 mb-3"
|
||||
title={$t.datasets?.database_tip}
|
||||
bind:value={mapDatabaseId}
|
||||
>
|
||||
<option value="">-- {$t.datasets?.select_database ?? 'выберите базу данных'} --</option>
|
||||
{#each mapDatabases as db}
|
||||
{#each mapDatabases as db (db.id)}
|
||||
<option value={db.id}>{db.database_name || db.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<label class="text-sm font-medium mb-1 block">
|
||||
<label class="text-sm font-medium mb-1 block" for="map-sql-query">
|
||||
{$t.datasets?.sql_query ?? 'SQL-запрос'}
|
||||
<span class="text-xs text-gray-400 font-normal ml-1">({$t.datasets?.sql_query_hint ?? 'оставьте пустым для information_schema'})</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="map-sql-query"
|
||||
class="w-full border rounded px-2 py-1.5 mb-4 font-mono text-xs"
|
||||
rows="4"
|
||||
title={$t.datasets?.sql_query_tip}
|
||||
placeholder="SELECT column_name, description AS verbose_name FROM my_mappings WHERE table_name = 'my_table';"
|
||||
bind:value={mapSqlQuery}
|
||||
></textarea>
|
||||
{:else}
|
||||
<input type="file" accept=".xlsx" class="w-full mb-4" onchange={(e) => mapFileData = e.target.files} />
|
||||
<label class="text-sm font-medium mb-1 block" for="map-file-input">{$t.datasets?.xlsx_file ?? 'XLSX-файл'}</label>
|
||||
<input
|
||||
id="map-file-input"
|
||||
type="file"
|
||||
accept=".xlsx"
|
||||
class="w-full mb-4"
|
||||
title={$t.datasets?.file_input_tip}
|
||||
onchange={(e) => mapFileData = e.target.files}
|
||||
/>
|
||||
{/if}
|
||||
<div class="flex justify-end gap-2">
|
||||
<button class="px-3 py-1.5 border rounded hover:bg-gray-100" onclick={() => showMapColumnsModal = false}>{$t.common?.cancel}</button>
|
||||
<button class="px-3 py-1.5 bg-primary text-white rounded hover:bg-primary-hover" onclick={handleBulkMapColumns}>{$t.datasets?.start_mapping}</button>
|
||||
<button class="px-3 py-1.5 border rounded hover:bg-gray-100" title={$t.datasets?.cancel_tip} onclick={() => showMapColumnsModal = false}>{$t.common?.cancel}</button>
|
||||
<button class="px-3 py-1.5 bg-primary text-white rounded hover:bg-primary-hover" title={$t.datasets?.start_mapping_tip} onclick={handleBulkMapColumns}>{$t.datasets?.start_mapping}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -390,15 +410,15 @@
|
||||
<div class="bg-white rounded-lg shadow-xl p-6 max-w-md w-full" onclick={(e) => e.stopPropagation()}>
|
||||
<h3 class="text-lg font-semibold mb-4">{$t.datasets?.bulk_docs_generation}</h3>
|
||||
<p class="text-sm text-gray-600 mb-3">{$t.datasets?.selected_count?.replace('{count}', String(selectedIds.size))}</p>
|
||||
<select class="w-full border rounded px-2 py-1.5 mb-4" bind:value={llmProvider}>
|
||||
<select class="w-full border rounded px-2 py-1.5 mb-4" title={$t.datasets?.llm_provider_tip} bind:value={llmProvider}>
|
||||
<option value="">{$t.datasets?.select_llm_provider_option}</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="anthropic">Anthropic</option>
|
||||
<option value="local">Local LLM</option>
|
||||
</select>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button class="px-3 py-1.5 border rounded hover:bg-gray-100" onclick={() => showGenerateDocsModal = false}>{$t.common?.cancel}</button>
|
||||
<button class="px-3 py-1.5 bg-primary text-white rounded hover:bg-primary-hover" onclick={handleBulkGenerateDocs}>{$t.datasets?.generate_docs}</button>
|
||||
<button class="px-3 py-1.5 border rounded hover:bg-gray-100" title={$t.datasets?.cancel_tip} onclick={() => showGenerateDocsModal = false}>{$t.common?.cancel}</button>
|
||||
<button class="px-3 py-1.5 bg-primary text-white rounded hover:bg-primary-hover" title={$t.datasets?.start_docs_tip} onclick={handleBulkGenerateDocs}>{$t.datasets?.generate_docs}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -65,10 +65,12 @@
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="px-2 py-1 text-xs border border-gray-300 rounded hover:bg-gray-50"
|
||||
title={$t.datasets?.select_all_tip}
|
||||
onclick={() => { selectedIds = new Set(datasets.map(d => d.id)); oncheckbox?.({}, 'all'); }}
|
||||
>{$t.datasets?.select_all}</button>
|
||||
<button
|
||||
class="px-2 py-1 text-xs border border-gray-300 rounded hover:bg-gray-50"
|
||||
title={$t.datasets?.deselect_all_tip}
|
||||
onclick={() => { selectedIds = new Set(); oncheckbox?.({}, 'clear-all'); }}
|
||||
>{$t.datasets?.deselect_all}</button>
|
||||
</div>
|
||||
@@ -134,16 +136,35 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Quick actions -->
|
||||
<!-- Quick actions (icon-only) -->
|
||||
<div class="flex gap-1 shrink-0 items-start">
|
||||
<button
|
||||
class="px-2 py-0.5 text-xs border border-gray-300 text-gray-500 rounded hover:bg-gray-50 hover:text-gray-700 transition-colors"
|
||||
class="p-1 border border-gray-300 text-gray-400 rounded hover:bg-gray-50 hover:text-gray-600 transition-colors"
|
||||
title={$t.datasets?.map_columns_tip}
|
||||
onclick={(e) => { e.stopPropagation(); onaction?.(dataset, 'map_columns'); }}
|
||||
>{$t.datasets?.action_map_columns}</button>
|
||||
>
|
||||
<!-- columns/grid icon -->
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="7" height="7" rx="1"/>
|
||||
<rect x="14" y="3" width="7" height="7" rx="1"/>
|
||||
<rect x="3" y="14" width="7" height="7" rx="1"/>
|
||||
<rect x="14" y="14" width="7" height="7" rx="1"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="px-2 py-0.5 text-xs border border-gray-300 text-gray-500 rounded hover:bg-gray-50 hover:text-gray-700 transition-colors"
|
||||
class="p-1 border border-gray-300 text-gray-400 rounded hover:bg-gray-50 hover:text-gray-600 transition-colors"
|
||||
title={$t.datasets?.generate_docs_tip}
|
||||
onclick={(e) => { e.stopPropagation(); onaction?.(dataset, 'generate_docs'); }}
|
||||
>{$t.datasets?.generate_docs}</button>
|
||||
>
|
||||
<!-- file/text icon -->
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
<line x1="16" y1="13" x2="8" y2="13"/>
|
||||
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||
<polyline points="10 9 9 9 8 9"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -157,11 +178,11 @@
|
||||
{($t.datasets?.showing ?? 'Showing {start}-{end} of {total}').replace('{start}', String((page - 1) * pageSize + 1)).replace('{end}', String(Math.min(page * pageSize, total))).replace('{total}', String(total))}
|
||||
</span>
|
||||
<div class="flex gap-1">
|
||||
<button class="px-2 py-1 text-xs border rounded {page <= 1 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-50'}" onclick={() => onpagechange?.(1)} disabled={page <= 1}>«</button>
|
||||
<button class="px-2 py-1 text-xs border rounded {page <= 1 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-50'}" onclick={() => onpagechange?.(page - 1)} disabled={page <= 1}>‹</button>
|
||||
<button class="px-2 py-1 text-xs border rounded {page <= 1 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-50'}" title={$t.datasets?.pagination_first_tip} onclick={() => onpagechange?.(1)} disabled={page <= 1}>«</button>
|
||||
<button class="px-2 py-1 text-xs border rounded {page <= 1 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-50'}" title={$t.datasets?.pagination_prev_tip} onclick={() => onpagechange?.(page - 1)} disabled={page <= 1}>‹</button>
|
||||
<span class="px-2 py-1 text-xs">{page} / {totalPages}</span>
|
||||
<button class="px-2 py-1 text-xs border rounded {page >= totalPages ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-50'}" onclick={() => onpagechange?.(page + 1)} disabled={page >= totalPages}>›</button>
|
||||
<button class="px-2 py-1 text-xs border rounded {page >= totalPages ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-50'}" onclick={() => onpagechange?.(totalPages)} disabled={page >= totalPages}>»</button>
|
||||
<button class="px-2 py-1 text-xs border rounded {page >= totalPages ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-50'}" title={$t.datasets?.pagination_next_tip} onclick={() => onpagechange?.(page + 1)} disabled={page >= totalPages}>›</button>
|
||||
<button class="px-2 py-1 text-xs border rounded {page >= totalPages ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-50'}" title={$t.datasets?.pagination_last_tip} onclick={() => onpagechange?.(totalPages)} disabled={page >= totalPages}>»</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
class="inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm transition-all {activeFilter === tile.key ? 'bg-primary text-white shadow-sm' : 'bg-white border border-gray-300 text-gray-600 hover:border-gray-400 hover:text-gray-800'}"
|
||||
onclick={() => handleTileClick(tile.key)}
|
||||
aria-pressed={activeFilter === tile.key}
|
||||
title={$t.datasets?.[tile.key === 'all' ? 'filter_all_tip' : tile.key === 'unmapped' ? 'filter_unmapped_tip' : tile.key === 'mapped' ? 'filter_mapped_tip' : 'filter_linked_tip']}
|
||||
>
|
||||
<span>{$t.datasets?.[tile.label] ?? tile.label}</span>
|
||||
<span class="font-semibold tabular-nums">{tile.getCount(stats)}</span>
|
||||
|
||||
@@ -50,10 +50,11 @@ describe('DatasetList Component', () => {
|
||||
expect(src).toContain('selectedIds');
|
||||
});
|
||||
|
||||
it('has quick-action buttons (map, docs)', () => {
|
||||
it('has quick-action icon buttons (map, docs)', () => {
|
||||
const src = fs.readFileSync(COMPONENT_PATH, 'utf-8');
|
||||
expect(src).toContain('action_map_columns');
|
||||
expect(src).toContain('generate_docs');
|
||||
expect(src).toContain('map_columns_tip');
|
||||
expect(src).toContain('generate_docs_tip');
|
||||
expect(src).toContain('<svg'); // icon-based, not text
|
||||
});
|
||||
|
||||
it('shows skeleton when loading', () => {
|
||||
|
||||
Reference in New Issue
Block a user