ready for test

This commit is contained in:
2026-02-25 13:35:09 +03:00
parent 21e969a769
commit 33433c3173
11 changed files with 994 additions and 351 deletions

View File

@@ -10,20 +10,23 @@
<script lang="ts">
// [SECTION: IMPORTS]
import { onMount } from 'svelte';
import EnvSelector from '../../components/EnvSelector.svelte';
import DashboardGrid from '../../components/DashboardGrid.svelte';
import MappingTable from '../../components/MappingTable.svelte';
import TaskRunner from '../../components/TaskRunner.svelte';
import TaskHistory from '../../components/TaskHistory.svelte';
import TaskLogViewer from '../../components/TaskLogViewer.svelte';
import PasswordPrompt from '../../components/PasswordPrompt.svelte';
import { api } from '../../lib/api.js';
import { selectedTask } from '../../lib/stores.js';
import { resumeTask } from '../../services/taskService.js';
import type { DashboardMetadata, DashboardSelection } from '../../types/dashboard';
import { t } from '$lib/i18n';
import { Button, Card, PageHeader } from '$lib/ui';
import { onMount } from "svelte";
import EnvSelector from "../../components/EnvSelector.svelte";
import DashboardGrid from "../../components/DashboardGrid.svelte";
import MappingTable from "../../components/MappingTable.svelte";
import TaskRunner from "../../components/TaskRunner.svelte";
import TaskHistory from "../../components/TaskHistory.svelte";
import TaskLogViewer from "../../components/TaskLogViewer.svelte";
import PasswordPrompt from "../../components/PasswordPrompt.svelte";
import { api } from "../../lib/api.js";
import { selectedTask } from "../../lib/stores.js";
import { resumeTask } from "../../services/taskService.js";
import type {
DashboardMetadata,
DashboardSelection,
} from "../../types/dashboard";
import { t } from "$lib/i18n";
import { Button, Card, PageHeader } from "$lib/ui";
// [/SECTION]
// [SECTION: STATE]
@@ -31,6 +34,7 @@
let sourceEnvId = "";
let targetEnvId = "";
let replaceDb = false;
let fixCrossFilters = true;
let loading = true;
let error = "";
let dashboards: DashboardMetadata[] = [];
@@ -40,12 +44,12 @@
let mappings: any[] = [];
let suggestions: any[] = [];
let fetchingDbs = false;
// UI State for Modals
let showLogViewer = false;
let logViewerTaskId: string | null = null;
let logViewerTaskStatus: string | null = null;
let showPasswordPrompt = false;
let passwordPromptDatabases: string[] = [];
let passwordPromptErrorMessage = "";
@@ -101,13 +105,18 @@
if (!sourceEnvId || !targetEnvId) return;
fetchingDbs = true;
error = "";
try {
const [src, tgt, maps, sugs] = await Promise.all([
api.requestApi(`/environments/${sourceEnvId}/databases`),
api.requestApi(`/environments/${targetEnvId}/databases`),
api.requestApi(`/mappings?source_env_id=${sourceEnvId}&target_env_id=${targetEnvId}`),
api.postApi(`/mappings/suggest`, { source_env_id: sourceEnvId, target_env_id: targetEnvId })
api.requestApi(
`/mappings?source_env_id=${sourceEnvId}&target_env_id=${targetEnvId}`,
),
api.postApi(`/mappings/suggest`, {
source_env_id: sourceEnvId,
target_env_id: targetEnvId,
}),
]);
sourceDatabases = src;
@@ -130,22 +139,25 @@
*/
async function handleMappingUpdate(event: CustomEvent) {
const { sourceUuid, targetUuid } = event.detail;
const sDb = sourceDatabases.find(d => d.uuid === sourceUuid);
const tDb = targetDatabases.find(d => d.uuid === targetUuid);
const sDb = sourceDatabases.find((d) => d.uuid === sourceUuid);
const tDb = targetDatabases.find((d) => d.uuid === targetUuid);
if (!sDb || !tDb) return;
try {
const savedMapping = await api.postApi('/mappings', {
const savedMapping = await api.postApi("/mappings", {
source_env_id: sourceEnvId,
target_env_id: targetEnvId,
source_db_uuid: sourceUuid,
target_db_uuid: targetUuid,
source_db_name: sDb.database_name,
target_db_name: tDb.database_name
target_db_name: tDb.database_name,
});
mappings = [...mappings.filter(m => m.source_db_uuid !== sourceUuid), savedMapping];
mappings = [
...mappings.filter((m) => m.source_db_uuid !== sourceUuid),
savedMapping,
];
} catch (e) {
error = e.message;
}
@@ -157,10 +169,10 @@
// @PRE: event.detail contains task object.
// @POST: logViewer state updated and showLogViewer set to true.
function handleViewLogs(event: CustomEvent) {
const task = event.detail;
logViewerTaskId = task.id;
logViewerTaskStatus = task.status;
showLogViewer = true;
const task = event.detail;
logViewerTaskId = task.id;
logViewerTaskStatus = task.status;
showLogViewer = true;
}
// [/DEF:handleViewLogs:Function]
@@ -172,19 +184,23 @@
// For now, we rely on the WebSocket or manual check.
// Ideally, TaskHistory or TaskRunner emits an event when input is needed.
// Or we watch selectedTask.
$: if ($selectedTask && $selectedTask.status === 'AWAITING_INPUT' && $selectedTask.input_request) {
const req = $selectedTask.input_request;
if (req.type === 'database_password') {
passwordPromptDatabases = req.databases || [];
passwordPromptErrorMessage = req.error_message || "";
showPasswordPrompt = true;
}
} else if (!$selectedTask || $selectedTask.status !== 'AWAITING_INPUT') {
// Close prompt if task is no longer waiting (e.g. resumed)
// But only if we are viewing this task.
// showPasswordPrompt = false;
// Actually, don't auto-close, let the user or success handler close it.
$: if (
$selectedTask &&
$selectedTask.status === "AWAITING_INPUT" &&
$selectedTask.input_request
) {
const req = $selectedTask.input_request;
if (req.type === "database_password") {
passwordPromptDatabases = req.databases || [];
passwordPromptErrorMessage = req.error_message || "";
showPasswordPrompt = true;
}
} else if (!$selectedTask || $selectedTask.status !== "AWAITING_INPUT") {
// Close prompt if task is no longer waiting (e.g. resumed)
// But only if we are viewing this task.
// showPasswordPrompt = false;
// Actually, don't auto-close, let the user or success handler close it.
}
// [/DEF:handlePasswordPrompt:Function]
@@ -193,18 +209,19 @@
// @PRE: event.detail contains passwords.
// @POST: resumeTask is called and showPasswordPrompt is hidden on success.
async function handleResumeMigration(event: CustomEvent) {
if (!$selectedTask) return;
const { passwords } = event.detail;
try {
await resumeTask($selectedTask.id, passwords);
showPasswordPrompt = false;
// Task status update will be handled by store/websocket
} catch (e) {
console.error("Failed to resume task:", e);
passwordPromptErrorMessage = e.message || ($t.migration?.resume_failed || "Failed to resume task");
// Keep prompt open
}
if (!$selectedTask) return;
const { passwords } = event.detail;
try {
await resumeTask($selectedTask.id, passwords);
showPasswordPrompt = false;
// Task status update will be handled by store/websocket
} catch (e) {
console.error("Failed to resume task:", e);
passwordPromptErrorMessage =
e.message || $t.migration?.resume_failed || "Failed to resume task";
// Keep prompt open
}
}
// [/DEF:handleResumeMigration:Function]
@@ -216,15 +233,21 @@
*/
async function startMigration() {
if (!sourceEnvId || !targetEnvId) {
error = $t.migration?.select_both_envs || "Please select both source and target environments.";
error =
$t.migration?.select_both_envs ||
"Please select both source and target environments.";
return;
}
if (sourceEnvId === targetEnvId) {
error = $t.migration?.different_envs || "Source and target environments must be different.";
error =
$t.migration?.different_envs ||
"Source and target environments must be different.";
return;
}
if (selectedDashboardIds.length === 0) {
error = $t.migration?.select_dashboards || "Please select at least one dashboard to migrate.";
error =
$t.migration?.select_dashboards ||
"Please select at least one dashboard to migrate.";
return;
}
@@ -234,28 +257,37 @@
selected_ids: selectedDashboardIds,
source_env_id: sourceEnvId,
target_env_id: targetEnvId,
replace_db_config: replaceDb
replace_db_config: replaceDb,
fix_cross_filters: fixCrossFilters,
};
console.log(`[MigrationDashboard][Action] Starting migration with selection:`, selection);
const result = await api.postApi('/migration/execute', selection);
console.log(`[MigrationDashboard][Action] Migration started: ${result.task_id} - ${result.message}`);
console.log(
`[MigrationDashboard][Action] Starting migration with selection:`,
selection,
);
const result = await api.postApi("/migration/execute", selection);
console.log(
`[MigrationDashboard][Action] Migration started: ${result.task_id} - ${result.message}`,
);
// Wait a brief moment for the backend to ensure the task is retrievable
await new Promise(r => setTimeout(r, 500));
await new Promise((r) => setTimeout(r, 500));
// Fetch full task details and switch to TaskRunner view
try {
const task = await api.getTask(result.task_id);
selectedTask.set(task);
} catch (fetchErr) {
// Fallback: create a temporary task object to switch view immediately
console.warn($t.migration?.task_placeholder_warn || "Could not fetch task details immediately, using placeholder.");
console.warn(
$t.migration?.task_placeholder_warn ||
"Could not fetch task details immediately, using placeholder.",
);
selectedTask.set({
id: result.task_id,
plugin_id: 'superset-migration',
status: 'RUNNING',
logs: [],
params: {}
id: result.task_id,
plugin_id: "superset-migration",
status: "RUNNING",
logs: [],
params: {},
});
}
} catch (e) {
@@ -269,7 +301,7 @@
<!-- [SECTION: TEMPLATE] -->
<div class="max-w-4xl mx-auto p-6">
<PageHeader title={$t.nav.migration} />
<TaskHistory on:viewLogs={handleViewLogs} />
{#if $selectedTask}
@@ -285,7 +317,9 @@
{#if loading}
<p>{$t.migration?.loading_envs || "Loading environments..."}</p>
{:else if error}
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
<div
class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4"
>
{error}
</div>
{/if}
@@ -305,8 +339,10 @@
<!-- [DEF:DashboardSelectionSection:Component] -->
<div class="mb-8">
<h2 class="text-lg font-medium mb-4">{$t.migration?.select_dashboards_title || "Select Dashboards"}</h2>
<h2 class="text-lg font-medium mb-4">
{$t.migration?.select_dashboards_title || "Select Dashboards"}
</h2>
{#if sourceEnvId}
<DashboardGrid
{dashboards}
@@ -314,30 +350,54 @@
environmentId={sourceEnvId}
/>
{:else}
<p class="text-gray-500 italic">{$t.dashboard?.select_source || "Select a source environment to view dashboards."}</p>
<p class="text-gray-500 italic">
{$t.dashboard?.select_source ||
"Select a source environment to view dashboards."}
</p>
{/if}
</div>
<!-- [/DEF:DashboardSelectionSection:Component] -->
<div class="mb-4">
<div class="flex items-center mb-2">
<input
id="fix-cross-filters"
type="checkbox"
bind:checked={fixCrossFilters}
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
/>
<label for="fix-cross-filters" class="ml-2 block text-sm text-gray-900">
{$t.migration?.fix_cross_filters ||
"Fix Cross-Filters (Auto-repair broken links during migration)"}
</label>
</div>
<div class="flex items-center mb-4">
<input
id="replace-db"
type="checkbox"
bind:checked={replaceDb}
on:change={() => { if (replaceDb && sourceDatabases.length === 0) fetchDatabases(); }}
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
/>
<label for="replace-db" class="ml-2 block text-sm text-gray-900">
{$t.migration?.replace_db || "Replace Database (Apply Mappings)"}
</label>
<div class="flex items-center">
<input
id="replace-db"
type="checkbox"
bind:checked={replaceDb}
on:change={() => {
if (replaceDb && sourceDatabases.length === 0) fetchDatabases();
}}
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
/>
<label for="replace-db" class="ml-2 block text-sm text-gray-900">
{$t.migration?.replace_db || "Replace Database (Apply Mappings)"}
</label>
</div>
</div>
{#if replaceDb}
<div class="mb-8 p-4 border rounded-md bg-gray-50">
<h3 class="text-md font-medium mb-4">{$t.migration?.database_mappings || "Database Mappings"}</h3>
<h3 class="text-md font-medium mb-4">
{$t.migration?.database_mappings || "Database Mappings"}
</h3>
{#if fetchingDbs}
<p>{$t.migration?.loading_dbs || "Loading databases and suggestions..."}</p>
<p>
{$t.migration?.loading_dbs ||
"Loading databases and suggestions..."}
</p>
{:else if sourceDatabases.length > 0}
<MappingTable
{sourceDatabases}
@@ -359,7 +419,10 @@
<Button
on:click={startMigration}
disabled={!sourceEnvId || !targetEnvId || sourceEnvId === targetEnvId || selectedDashboardIds.length === 0}
disabled={!sourceEnvId ||
!targetEnvId ||
sourceEnvId === targetEnvId ||
selectedDashboardIds.length === 0}
>
{$t.migration?.start || "Start Migration"}
</Button>
@@ -368,21 +431,20 @@
<!-- Modals -->
<TaskLogViewer
bind:show={showLogViewer}
taskId={logViewerTaskId}
taskStatus={logViewerTaskStatus}
on:close={() => showLogViewer = false}
bind:show={showLogViewer}
taskId={logViewerTaskId}
taskStatus={logViewerTaskStatus}
on:close={() => (showLogViewer = false)}
/>
<PasswordPrompt
bind:show={showPasswordPrompt}
databases={passwordPromptDatabases}
errorMessage={passwordPromptErrorMessage}
on:resume={handleResumeMigration}
on:cancel={() => showPasswordPrompt = false}
bind:show={showPasswordPrompt}
databases={passwordPromptDatabases}
errorMessage={passwordPromptErrorMessage}
on:resume={handleResumeMigration}
on:cancel={() => (showPasswordPrompt = false)}
/>
<!-- [/SECTION] -->
<!-- [/DEF:MigrationDashboard:Component] -->

View File

@@ -22,9 +22,9 @@
const DEFAULT_LLM_PROMPTS = {
dashboard_validation_prompt:
"Analyze the attached dashboard screenshot and the following execution logs for health and visual issues.\\n\\nLogs:\\n{logs}\\n\\nProvide the analysis in JSON format with the following structure:\\n{\\n \\\"status\\\": \\\"PASS\\\" | \\\"WARN\\\" | \\\"FAIL\\\",\\n \\\"summary\\\": \\\"Short summary of findings\\\",\\n \\\"issues\\\": [\\n {\\n \\\"severity\\\": \\\"WARN\\\" | \\\"FAIL\\\",\\n \\\"message\\\": \\\"Description of the issue\\\",\\n \\\"location\\\": \\\"Optional location info (e.g. chart name)\\\"\\n }\\n ]\\n}",
'Analyze the attached dashboard screenshot and the following execution logs for health and visual issues.\\n\\nLogs:\\n{logs}\\n\\nProvide the analysis in JSON format with the following structure:\\n{\\n \\"status\\": \\"PASS\\" | \\"WARN\\" | \\"FAIL\\",\\n \\"summary\\": \\"Short summary of findings\\",\\n \\"issues\\": [\\n {\\n \\"severity\\": \\"WARN\\" | \\"FAIL\\",\\n \\"message\\": \\"Description of the issue\\",\\n \\"location\\": \\"Optional location info (e.g. chart name)\\"\\n }\\n ]\\n}',
documentation_prompt:
"Generate professional documentation for the following dataset and its columns.\\nDataset: {dataset_name}\\nColumns: {columns_json}\\n\\nProvide the documentation in JSON format:\\n{\\n \\\"dataset_description\\\": \\\"General description of the dataset\\\",\\n \\\"column_descriptions\\\": [\\n {\\n \\\"name\\\": \\\"column_name\\\",\\n \\\"description\\\": \\\"Generated description\\\"\\n }\\n ]\\n}",
'Generate professional documentation for the following dataset and its columns.\\nDataset: {dataset_name}\\nColumns: {columns_json}\\n\\nProvide the documentation in JSON format:\\n{\\n \\"dataset_description\\": \\"General description of the dataset\\",\\n \\"column_descriptions\\": [\\n {\\n \\"name\\": \\"column_name\\",\\n \\"description\\": \\"Generated description\\"\\n }\\n ]\\n}',
git_commit_prompt:
"Generate a concise and professional git commit message based on the following diff and recent history.\\nUse Conventional Commits format (e.g., feat: ..., fix: ..., docs: ...).\\n\\nRecent History:\\n{history}\\n\\nDiff:\\n{diff}\\n\\nCommit Message:",
};
@@ -59,6 +59,7 @@
// Load settings on mount
onMount(async () => {
await loadSettings();
await loadMigrationSettings();
});
// Load consolidated settings from API
@@ -95,7 +96,8 @@
...DEFAULT_LLM_PROVIDER_BINDINGS,
...(llm?.provider_bindings || {}),
};
normalized.assistant_planner_provider = llm?.assistant_planner_provider || "";
normalized.assistant_planner_provider =
llm?.assistant_planner_provider || "";
normalized.assistant_planner_model = llm?.assistant_planner_model || "";
return normalized;
}
@@ -116,7 +118,9 @@
function getProviderById(providerId) {
if (!providerId) return null;
return (settings?.llm_providers || []).find((p) => p.id === providerId) || null;
return (
(settings?.llm_providers || []).find((p) => p.id === providerId) || null
);
}
function isDashboardValidationBindingValid() {
@@ -129,6 +133,9 @@
// Handle tab change
function handleTabChange(tab) {
activeTab = tab;
if (tab === "migration") {
loadMigrationSettings();
}
}
// Get tab class
@@ -138,6 +145,44 @@
: "text-gray-600 hover:text-gray-800 border-transparent hover:border-gray-300";
}
// Migration Settings State
let migrationCron = "0 2 * * *";
let displayMappings = [];
let isSavingMigration = false;
let isLoadingMigration = false;
async function loadMigrationSettings() {
isLoadingMigration = true;
try {
const settingsRes = await api.requestApi("/migration/settings");
migrationCron = settingsRes.cron;
const mappingsRes = await api.requestApi("/migration/mappings-data");
displayMappings = mappingsRes;
} catch (err) {
console.error("[SettingsPage][Migration] Failed to load:", err);
} finally {
isLoadingMigration = false;
}
}
async function saveMigrationSettings() {
isSavingMigration = true;
try {
await api.putApi("/migration/settings", { cron: migrationCron });
addToast(
$t.settings?.save_success || "Migration settings saved",
"success",
);
} catch (err) {
addToast(
$t.settings?.save_failed || "Failed to save migration settings",
"error",
);
} finally {
isSavingMigration = false;
}
}
// Handle global settings save (Logging, Storage)
async function handleSave() {
console.log("[SettingsPage][Action] Saving settings");
@@ -327,6 +372,14 @@
>
{$t.settings?.llm || "LLM"}
</button>
<button
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none {getTabClass(
'migration',
)}"
on:click={() => handleTabChange("migration")}
>
Migration Sync
</button>
<button
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none {getTabClass(
'storage',
@@ -712,7 +765,8 @@
<div class="mt-6 rounded-lg border border-gray-200 bg-white p-4">
<h3 class="text-base font-semibold text-gray-900">
{$t.settings?.llm_chatbot_settings_title || "Chatbot Planner Settings"}
{$t.settings?.llm_chatbot_settings_title ||
"Chatbot Planner Settings"}
</h3>
<p class="mt-1 text-sm text-gray-600">
{$t.settings?.llm_chatbot_settings_description ||
@@ -721,7 +775,10 @@
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label for="planner-provider" class="block text-sm font-medium text-gray-700">
<label
for="planner-provider"
class="block text-sm font-medium text-gray-700"
>
{$t.settings?.llm_chatbot_provider || "Chatbot Provider"}
</label>
<select
@@ -729,7 +786,9 @@
bind:value={settings.llm.assistant_planner_provider}
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
>
<option value="">{$t.dashboard?.use_default || "Use Default"}</option>
<option value=""
>{$t.dashboard?.use_default || "Use Default"}</option
>
{#each settings.llm_providers || [] as provider}
<option value={provider.id}>
{provider.name} ({provider.default_model})
@@ -739,14 +798,18 @@
</div>
<div>
<label for="planner-model" class="block text-sm font-medium text-gray-700">
<label
for="planner-model"
class="block text-sm font-medium text-gray-700"
>
{$t.settings?.llm_chatbot_model || "Chatbot Model Override"}
</label>
<input
id="planner-model"
type="text"
bind:value={settings.llm.assistant_planner_model}
placeholder={$t.settings?.llm_chatbot_model_placeholder || "Optional, e.g. gpt-4.1-mini"}
placeholder={$t.settings?.llm_chatbot_model_placeholder ||
"Optional, e.g. gpt-4.1-mini"}
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
/>
</div>
@@ -755,7 +818,8 @@
<div class="mt-6 rounded-lg border border-gray-200 bg-white p-4">
<h3 class="text-base font-semibold text-gray-900">
{$t.settings?.llm_provider_bindings_title || "Provider Bindings by Task"}
{$t.settings?.llm_provider_bindings_title ||
"Provider Bindings by Task"}
</h3>
<p class="mt-1 text-sm text-gray-600">
{$t.settings?.llm_provider_bindings_description ||
@@ -764,15 +828,23 @@
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label for="binding-dashboard-validation" class="block text-sm font-medium text-gray-700">
{$t.settings?.llm_binding_dashboard_validation || "Dashboard Validation Provider"}
<label
for="binding-dashboard-validation"
class="block text-sm font-medium text-gray-700"
>
{$t.settings?.llm_binding_dashboard_validation ||
"Dashboard Validation Provider"}
</label>
<select
id="binding-dashboard-validation"
bind:value={settings.llm.provider_bindings.dashboard_validation}
bind:value={
settings.llm.provider_bindings.dashboard_validation
}
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
>
<option value="">{$t.dashboard?.use_default || "Use Default"}</option>
<option value=""
>{$t.dashboard?.use_default || "Use Default"}</option
>
{#each settings.llm_providers || [] as provider}
<option value={provider.id}>
{provider.name} ({provider.default_model})
@@ -788,15 +860,21 @@
</div>
<div>
<label for="binding-documentation" class="block text-sm font-medium text-gray-700">
{$t.settings?.llm_binding_documentation || "Documentation Provider"}
<label
for="binding-documentation"
class="block text-sm font-medium text-gray-700"
>
{$t.settings?.llm_binding_documentation ||
"Documentation Provider"}
</label>
<select
id="binding-documentation"
bind:value={settings.llm.provider_bindings.documentation}
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
>
<option value="">{$t.dashboard?.use_default || "Use Default"}</option>
<option value=""
>{$t.dashboard?.use_default || "Use Default"}</option
>
{#each settings.llm_providers || [] as provider}
<option value={provider.id}>
{provider.name} ({provider.default_model})
@@ -806,7 +884,10 @@
</div>
<div class="md:col-span-2">
<label for="binding-git-commit" class="block text-sm font-medium text-gray-700">
<label
for="binding-git-commit"
class="block text-sm font-medium text-gray-700"
>
{$t.settings?.llm_binding_git_commit || "Git Commit Provider"}
</label>
<select
@@ -814,7 +895,9 @@
bind:value={settings.llm.provider_bindings.git_commit}
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
>
<option value="">{$t.dashboard?.use_default || "Use Default"}</option>
<option value=""
>{$t.dashboard?.use_default || "Use Default"}</option
>
{#each settings.llm_providers || [] as provider}
<option value={provider.id}>
{provider.name} ({provider.default_model})
@@ -840,7 +923,8 @@
for="documentation-prompt"
class="block text-sm font-medium text-gray-700"
>
{$t.settings?.llm_prompt_documentation || "Documentation Prompt"}
{$t.settings?.llm_prompt_documentation ||
"Documentation Prompt"}
</label>
<textarea
id="documentation-prompt"
@@ -892,6 +976,150 @@
</div>
</div>
</div>
{:else if activeTab === "migration"}
<!-- Migration Sync Tab -->
<div class="text-lg font-medium mb-4">
<h2 class="text-xl font-bold mb-4">
Cross-Environment ID Synchronization
</h2>
<p class="text-gray-600 mb-6">
Configure the background synchronization schedule and view the
currently mapped Dashboard, Chart, and Dataset IDs.
</p>
<!-- Cron Configuration -->
<div class="bg-gray-50 p-6 rounded-lg border border-gray-200 mb-6">
<h3 class="text-lg font-medium mb-4">Sync Schedule (Cron)</h3>
<div class="flex items-end gap-4">
<div class="flex-grow">
<label
for="migration_cron"
class="block text-sm font-medium text-gray-700"
>Cron Expression</label
>
<input
type="text"
id="migration_cron"
bind:value={migrationCron}
placeholder="0 2 * * *"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 font-mono text-sm"
/>
<p class="text-xs text-gray-500 mt-1">
Example: 0 2 * * * (daily at 2 AM UTC)
</p>
</div>
<button
on:click={saveMigrationSettings}
disabled={isSavingMigration}
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 h-[42px] min-w-[100px] flex items-center justify-center disabled:opacity-50"
>
{isSavingMigration ? "Saving..." : "Save"}
</button>
</div>
</div>
<!-- Mappings Table -->
<div class="bg-gray-50 p-6 rounded-lg border border-gray-200">
<h3
class="text-lg font-medium mb-4 flex items-center justify-between"
>
<span>Synchronized Resources</span>
<button
on:click={loadMigrationSettings}
class="text-sm text-indigo-600 hover:text-indigo-800 flex items-center gap-1"
disabled={isLoadingMigration}
>
<svg
class="w-4 h-4 {isLoadingMigration ? 'animate-spin' : ''}"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
></path></svg
>
Refresh
</button>
</h3>
<div class="overflow-x-auto border border-gray-200 rounded-lg">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="bg-gray-100">
<tr>
<th
class="px-6 py-3 text-left font-medium text-gray-500 uppercase tracking-wider"
>Resource Name</th
>
<th
class="px-6 py-3 text-left font-medium text-gray-500 uppercase tracking-wider"
>Type</th
>
<th
class="px-6 py-3 text-left font-medium text-gray-500 uppercase tracking-wider"
>UUID</th
>
<th
class="px-6 py-3 text-left font-medium text-gray-500 uppercase tracking-wider"
>Target ID</th
>
<th
class="px-6 py-3 text-left font-medium text-gray-500 uppercase tracking-wider"
>Env</th
>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{#if isLoadingMigration && displayMappings.length === 0}
<tr
><td
colspan="5"
class="px-6 py-8 text-center text-gray-500"
>Loading mappings...</td
></tr
>
{:else if displayMappings.length === 0}
<tr
><td
colspan="5"
class="px-6 py-8 text-center text-gray-500"
>No synchronized resources found.</td
></tr
>
{:else}
{#each displayMappings as mapping}
<tr class="hover:bg-gray-50">
<td
class="px-6 py-4 whitespace-nowrap font-medium text-gray-900"
>{mapping.resource_name || "N/A"}</td
>
<td class="px-6 py-4 whitespace-nowrap"
><span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
>{mapping.resource_type}</span
></td
>
<td
class="px-6 py-4 whitespace-nowrap font-mono text-xs text-gray-500"
>{mapping.uuid}</td
>
<td
class="px-6 py-4 whitespace-nowrap font-mono text-xs font-bold text-gray-700"
>{mapping.remote_id}</td
>
<td class="px-6 py-4 whitespace-nowrap text-gray-500"
>{mapping.environment_id}</td
>
</tr>
{/each}
{/if}
</tbody>
</table>
</div>
</div>
</div>
{:else if activeTab === "storage"}
<!-- Storage Tab -->
<div class="text-lg font-medium mb-4">