ready for test
This commit is contained in:
@@ -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] -->
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user