- 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)
303 lines
14 KiB
Svelte
303 lines
14 KiB
Svelte
<!-- #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 -->
|