Files
ss-tools/frontend/src/lib/components/translate/TargetSchemaHint.svelte
busya 5e4b03a662 fix: resolve all 221 eslint errors across frontend
- svelte/no-unused-svelte-ignore (7→0): removed stale a11y ignore comments
- svelte/no-unnecessary-state-wrap (5→0): removed () around SvelteSet
- svelte/prefer-writable-derived (3→0): suppressed with eslint-disable comments
- svelte/no-at-html-tags (2→0): added eslint-disable-next-line comments
- no-self-assign (1→0): replaced self-assign with map-based array update
- no-unsafe-optional-chaining (21→0): added ?? fallback defaults
- no-unused-vars (181→0): removed dead imports, vars, catch bindings
- parse error in health.svelte.ts: fixed malformed import statement
- Removed deprecated .eslintignore file (config moved to eslint.config.js)
- Updated test assertion that checked for removed dead code

Remaining warnings: 190 require-each-key + 67 navigation-without-resolve
(both intentionally set to warn level in eslint.config.js)
2026-06-03 15:51:31 +03:00

303 lines
14 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- #region TargetSchemaHint [C:3] [TYPE Component] [SEMANTICS translate, schema, validation, hint, button] -->
<!-- @BRIEF Подсказка о колонках целевой таблицы + кнопка "Проверить схему".
Пользователь сам нажимает кнопку — никакого авто-шума.
Показывает diff ожидаемых vs актуальных колонок. -->
<!-- @LAYER UI -->
<!-- @RELATION DEPENDS_ON -> [EXT:frontend:checkTargetTableSchema] -->
<!-- @RELATION BINDS_TO -> [EXT:frontend:api] -->
<!-- @UX_STATE Idle -> Кнопка активна. Ждём нажатия. -->
<!-- @UX_STATE Checking -> Спиннер, кнопка disabled. -->
<!-- @UX_STATE Complete -> Показан результат: все ок / отсутствуют колонки / таблица не найдена. -->
<!-- @UX_STATE Error -> Ошибка соединения. Кнопка активна для повтора. -->
<!-- @UX_FEEDBACK Цветовой индикатор: зелёный (все ок), жёлтый (не все колонки), красный (таблица не найдена/ошибка). -->
<!-- @UX_REACTIVITY Props -> $props() для полей конфигурации. -->
<script lang="ts">
import { onDestroy } from 'svelte';
import { checkTargetTableSchema } from '$lib/api/translate.js';
import { _ } from '$lib/i18n/index.svelte.js';
let {
environmentId = '',
targetDatabaseId = '',
targetSchema = '',
targetTable = '',
targetKeyCols = [],
targetColumn = null,
translationColumn = null,
targetLanguageColumn = null,
targetSourceColumn = null,
targetSourceLanguageColumn = null,
} = $props();
/** @type {'idle'|'checking'|'complete'|'error'} */
let state = $state('idle');
let result = $state(null);
let abortController = $state(null);
let renderKey = $state(0);
/** CSS-класс body в зависимости от состояния */
let bodyClass = $derived.by(() => {
if (state === 'complete' && result?.all_present) return 'px-3 py-2.5 bg-success-light';
if (state === 'complete' && !result?.all_present && result?.table_exists) return 'px-3 py-2.5 bg-warning-light';
if (state === 'error' || (state === 'complete' && !result?.table_exists)) return 'px-3 py-2.5 bg-destructive-light';
return 'px-3 py-2.5';
});
/** Колонки с типами для отображения — $state, наполняется в handleCheck() */
let actualColumns = $state([]);
/** Есть ли минимальные данные для проверки? */
let canCheck = $derived(
!!environmentId
&& !!targetDatabaseId
&& !!targetTable
);
/** Текст кнопки */
let buttonLabel = $derived.by(() => {
if (state === 'checking') return _('translate.target_schema.checking') || 'Checking…';
if (state === 'complete') return _('translate.target_schema.check_again') || 'Check Again';
return _('translate.target_schema.check_button') || 'Check Target Schema';
});
/**
* Выводит тип колонки по соглашению именования,
* когда реальный тип из БД недоступен.
*/
function inferType(colName) {
const n = colName.toLowerCase();
// Служебные флаги и boolean
if (n === 'is_original' || n.startsWith('is_') || n.startsWith('has_') || n.endsWith('_flag')) return 'INT';
// Внешние ключи
if (n.endsWith('_id') || n.endsWith('_key')) return 'INT';
// Код языка
if (n.endsWith('_lang') || n === 'lang_code' || n.endsWith('_language')) return 'STRING';
// Даты
if (n.endsWith('_date') || n.endsWith('_time') || n.endsWith('_at')) return 'DATETIME';
// Всё остальное — текст
return 'STRING';
}
/** Основная проверка */
async function handleCheck() {
if (!canCheck || state === 'checking') return;
// Abort предыдущего запроса если был
if (abortController) abortController.abort();
abortController = new AbortController();
const signal = abortController.signal;
state = 'checking';
result = null;
try {
const res = await checkTargetTableSchema({
environment_id: environmentId,
target_database_id: targetDatabaseId,
target_schema: targetSchema || 'public',
target_table: targetTable,
target_key_cols: targetKeyCols || [],
target_column: targetColumn || null,
translation_column: translationColumn || null,
target_language_column: targetLanguageColumn || null,
target_source_column: targetSourceColumn || null,
target_source_language_column: targetSourceLanguageColumn || null,
});
// Если запрос был отменён — не обновляем state
if (signal.aborted) return;
result = res;
// Build merged column list with types from actual_columns
const ac = res?.actual_columns || [];
const actualMap = {};
for (const c of ac) actualMap[c.name] = c;
actualColumns = (res?.expected_columns || []).map(col => {
const match = actualMap[col.name];
return {
name: col.name,
data_type: match ? match.data_type : inferType(col.name),
is_missing: (res?.missing_columns || []).some(m => m.name === col.name),
};
});
state = 'complete';
renderKey++;
} catch {
if (signal.aborted) return;
state = 'error';
result = null;
}
}
onDestroy(() => {
if (abortController) abortController.abort();
});
</script>
<div class="target-schema-hint mt-3 rounded-lg border text-sm transition-colors"
class:border-success={state === 'complete' && result?.all_present}
class:border-warning={state === 'complete' && !result?.all_present && result?.table_exists}
class:border-destructive-ring={state === 'error' || (state === 'complete' && !result?.table_exists)}
class:border-border={state === 'idle' || state === 'checking'}
role="region"
aria-label={_('translate.target_schema.region_label') || 'Target Schema Check'}
>
<!-- Header: label + кнопка -->
<div class="flex items-center justify-between px-3 py-2 border-b border-border"
class:bg-success-light={state === 'complete' && result?.all_present}
class:bg-warning-light={state === 'complete' && !result?.all_present && result?.table_exists}
class:bg-destructive-light={state === 'error' || (state === 'complete' && !result?.table_exists)}
class:bg-surface-muted={state === 'idle' || state === 'checking'}
>
<div class="flex items-center gap-2">
<!-- Иконка состояния -->
{#if state === 'checking'}
<svg class="w-4 h-4 text-text-subtle animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
</svg>
{:else if state === 'complete' && result?.all_present}
<svg class="w-4 h-4 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
</svg>
{:else if state === 'complete' && !result?.table_exists}
<svg class="w-4 h-4 text-destructive" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/>
</svg>
{:else if state === 'complete' && !result?.all_present}
<svg class="w-4 h-4 text-warning" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
{:else}
<svg class="w-4 h-4 text-text-subtle" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
{/if}
<span class="font-medium text-text text-xs uppercase tracking-wide">
{_('translate.target_schema.title') || 'Target Schema'}
</span>
</div>
<!-- Кнопка -->
<button
onclick={handleCheck}
disabled={!canCheck || state === 'checking'}
class="text-xs font-medium px-3 py-1.5 rounded-md border transition-colors focus-visible:ring-2 focus-visible:ring-primary-ring focus-visible:outline-none"
class:bg-surface-card={state !== 'checking'}
class:text-text={state !== 'checking'}
class:border-border-strong={state !== 'checking'}
class:hover:bg-surface-muted={state !== 'checking'}
class:bg-surface-muted={state === 'checking' || !canCheck}
class:text-text-subtle={state === 'checking' || !canCheck}
class:border-border={state === 'checking' || !canCheck}
class:cursor-not-allowed={!canCheck || state === 'checking'}
aria-busy={state === 'checking'}
>
{buttonLabel}
</button>
</div>
<!-- Body: результат проверки -->
<div class={bodyClass}>
{#if state === 'idle'}
<p class="text-text-muted">
{#if !targetTable}
{_('translate.target_schema.enter_table') || 'Enter a target table name and click "Check Target Schema".'}
{:else if !targetDatabaseId}
{_('translate.target_schema.select_database') || 'Select a target database and click "Check Target Schema".'}
{:else}
{_('translate.target_schema.press_button') || 'Click "Check Target Schema" to verify the target table columns.'}
{/if}
</p>
{:else if state === 'checking'}
<p class="text-text-muted">
{_('translate.target_schema.checking') || 'Checking target table schema…'}
</p>
{:else if state === 'complete' && result}
{#key renderKey}
{#if !result.table_exists}
<div class="flex items-center gap-2 text-destructive">
<svg class="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/>
</svg>
<p class="font-medium">{_('translate.target_schema.table_not_found') || 'Target table not found in the database.'}</p>
</div>
{:else if result.all_present}
<div class="flex items-center gap-2 text-success">
<svg class="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
</svg>
<p class="font-medium">{_('translate.target_schema.all_present') || 'Target table has all required columns.'}</p>
</div>
{:else}
<div class="flex items-center gap-2 text-warning mb-2">
<svg class="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<p class="font-medium">{_('translate.target_schema.missing_columns_title') || 'Target table is missing some required columns.'}</p>
</div>
{/if}
<!-- Список колонок с типами -->
{#if actualColumns.length > 0}
<div class="mt-1.5">
<p class="text-xs font-medium text-text-muted mb-1.5">
{_('translate.target_schema.expected_columns') || 'Required columns for translation inserts:'}
</p>
<div class="flex flex-wrap gap-1.5">
{#each actualColumns as col}
<span
class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-mono border"
class:bg-success-light={!col.is_missing}
class:text-success={!col.is_missing}
class:border-success={!col.is_missing}
class:bg-destructive-light={col.is_missing}
class:text-destructive={col.is_missing}
class:border-destructive-ring={col.is_missing}
>
{#if col.is_missing}
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
{:else}
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
</svg>
{/if}
<span>{col.name}</span>
{#if col.data_type}<span class="text-[10px] text-success/70">({col.data_type})</span>{/if}
</span>
{/each}
</div>
</div>
{/if}
{/key}
<!-- Сопроводительный текст если есть пропущенные -->
{#if result.table_exists && !result.all_present}
<div class="mt-2 text-xs text-warning">
<p>{_('translate.target_schema.add_columns') || 'Add these missing columns to the target table, or configure custom column names in "Target Column Mapping" above.'}</p>
</div>
{/if}
<!-- Ошибка от Superset -->
{#if result.error}
<p class="mt-1.5 text-xs text-destructive font-mono">{result.error}</p>
{/if}
{:else if state === 'error'}
<div class="flex items-center gap-2 text-destructive">
<svg class="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/>
</svg>
<p class="font-medium">
{_('translate.target_schema.check_error') || 'Could not verify target table schema. Make sure the table exists and is accessible.'}
</p>
</div>
{/if}
</div>
</div>
<!-- #endregion TargetSchemaHint -->