1492 lines
56 KiB
Svelte
1492 lines
56 KiB
Svelte
<!-- [DEF:SettingsPage:Page] -->
|
||
<script>
|
||
/**
|
||
* @TIER: CRITICAL
|
||
* @PURPOSE: Consolidated Settings Page - All settings in one place with tabbed navigation
|
||
* @LAYER: UI
|
||
* @RELATION: BINDS_TO -> sidebarStore
|
||
* @INVARIANT: Always shows tabbed interface with all settings categories
|
||
*
|
||
* @UX_STATE: Loading -> Shows skeleton loader
|
||
* @UX_STATE: Loaded -> Shows tabbed settings interface
|
||
* @UX_STATE: Error -> Shows error banner with retry button
|
||
* @UX_FEEDBACK: Toast notifications on save success/failure
|
||
* @UX_RECOVERY: Refresh button reloads settings data
|
||
*/
|
||
|
||
import { onMount } from "svelte";
|
||
import { t } from "$lib/i18n";
|
||
import { api } from "$lib/api.js";
|
||
import { addToast } from "$lib/toasts";
|
||
import ProviderConfig from "../../components/llm/ProviderConfig.svelte";
|
||
|
||
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}',
|
||
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}',
|
||
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:",
|
||
};
|
||
const DEFAULT_LLM_PROVIDER_BINDINGS = {
|
||
dashboard_validation: "",
|
||
documentation: "",
|
||
git_commit: "",
|
||
};
|
||
|
||
// State
|
||
const SETTINGS_TABS = [
|
||
"environments",
|
||
"logging",
|
||
"connections",
|
||
"llm",
|
||
"migration",
|
||
"storage",
|
||
];
|
||
let activeTab = "environments";
|
||
let settings = null;
|
||
let isLoading = true;
|
||
let error = null;
|
||
|
||
// Environment editing state
|
||
let editingEnvId = null;
|
||
let isAddingEnv = false;
|
||
let newEnv = {
|
||
id: "",
|
||
name: "",
|
||
url: "",
|
||
username: "",
|
||
password: "",
|
||
is_default: false,
|
||
backup_schedule: {
|
||
enabled: false,
|
||
cron_expression: "0 0 * * *",
|
||
},
|
||
};
|
||
|
||
function normalizeTab(value) {
|
||
const normalized = String(value || "").trim().toLowerCase();
|
||
const aliases = {
|
||
environment: "environments",
|
||
env: "environments",
|
||
"migration-sync": "migration",
|
||
storages: "storage",
|
||
};
|
||
const resolved = aliases[normalized] || normalized;
|
||
return SETTINGS_TABS.includes(resolved) ? resolved : "environments";
|
||
}
|
||
|
||
function readTabFromUrl() {
|
||
if (typeof window === "undefined") return "environments";
|
||
const params = new URLSearchParams(window.location.search);
|
||
const fromQuery = params.get("tab");
|
||
if (fromQuery) return normalizeTab(fromQuery);
|
||
const fromHash = window.location.hash?.replace(/^#/, "");
|
||
if (fromHash) return normalizeTab(fromHash);
|
||
return "environments";
|
||
}
|
||
|
||
function writeTabToUrl(tab) {
|
||
if (typeof window === "undefined") return;
|
||
const url = new URL(window.location.href);
|
||
url.searchParams.set("tab", tab);
|
||
url.hash = tab;
|
||
window.history.replaceState({}, "", url.toString());
|
||
}
|
||
|
||
// Load settings on mount
|
||
onMount(async () => {
|
||
activeTab = readTabFromUrl();
|
||
const syncTabFromUrl = () => {
|
||
activeTab = readTabFromUrl();
|
||
};
|
||
window.addEventListener("popstate", syncTabFromUrl);
|
||
window.addEventListener("hashchange", syncTabFromUrl);
|
||
await loadSettings();
|
||
await loadMigrationSettings();
|
||
return () => {
|
||
window.removeEventListener("popstate", syncTabFromUrl);
|
||
window.removeEventListener("hashchange", syncTabFromUrl);
|
||
};
|
||
});
|
||
|
||
// Load consolidated settings from API
|
||
async function loadSettings() {
|
||
isLoading = true;
|
||
error = null;
|
||
try {
|
||
const response = await api.getConsolidatedSettings();
|
||
response.llm = normalizeLlmSettings(response.llm);
|
||
settings = response;
|
||
} catch (err) {
|
||
error = err.message || $t.settings?.load_failed ;
|
||
console.error("[SettingsPage][Coherence:Failed]", err);
|
||
} finally {
|
||
isLoading = false;
|
||
}
|
||
}
|
||
|
||
function normalizeLlmSettings(llm) {
|
||
const normalized = {
|
||
providers: [],
|
||
default_provider: "",
|
||
prompts: { ...DEFAULT_LLM_PROMPTS },
|
||
provider_bindings: { ...DEFAULT_LLM_PROVIDER_BINDINGS },
|
||
assistant_planner_provider: "",
|
||
assistant_planner_model: "",
|
||
...(llm || {}),
|
||
};
|
||
normalized.prompts = {
|
||
...DEFAULT_LLM_PROMPTS,
|
||
...(llm?.prompts || {}),
|
||
};
|
||
normalized.provider_bindings = {
|
||
...DEFAULT_LLM_PROVIDER_BINDINGS,
|
||
...(llm?.provider_bindings || {}),
|
||
};
|
||
normalized.assistant_planner_provider =
|
||
llm?.assistant_planner_provider || "";
|
||
normalized.assistant_planner_model = llm?.assistant_planner_model || "";
|
||
return normalized;
|
||
}
|
||
|
||
function isMultimodalModel(modelName) {
|
||
const token = (modelName || "").toLowerCase();
|
||
if (!token) return false;
|
||
return (
|
||
token.includes("gpt-4o") ||
|
||
token.includes("gpt-4.1") ||
|
||
token.includes("vision") ||
|
||
token.includes("vl") ||
|
||
token.includes("gemini") ||
|
||
token.includes("claude-3") ||
|
||
token.includes("claude-sonnet-4")
|
||
);
|
||
}
|
||
|
||
function getProviderById(providerId) {
|
||
if (!providerId) return null;
|
||
return (
|
||
(settings?.llm_providers || []).find((p) => p.id === providerId) || null
|
||
);
|
||
}
|
||
|
||
function isDashboardValidationBindingValid() {
|
||
const providerId = settings?.llm?.provider_bindings?.dashboard_validation;
|
||
if (!providerId) return true;
|
||
const provider = getProviderById(providerId);
|
||
return provider ? isMultimodalModel(provider.default_model) : true;
|
||
}
|
||
|
||
// Handle tab change
|
||
function handleTabChange(tab) {
|
||
const normalizedTab = normalizeTab(tab);
|
||
activeTab = normalizedTab;
|
||
writeTabToUrl(activeTab);
|
||
if (normalizedTab === "migration") {
|
||
loadMigrationSettings();
|
||
}
|
||
}
|
||
|
||
// Migration Settings State
|
||
let migrationCron = "0 2 * * *";
|
||
let displayMappings = [];
|
||
let mappingsTotal = 0;
|
||
let mappingsPage = 0;
|
||
let mappingsPageSize = 25;
|
||
let mappingsSearch = "";
|
||
let mappingsEnvFilter = "";
|
||
let mappingsTypeFilter = "";
|
||
let isSavingMigration = false;
|
||
let isLoadingMigration = false;
|
||
let isSyncing = false;
|
||
let searchTimeout = null;
|
||
|
||
$: mappingsTotalPages = Math.max(
|
||
1,
|
||
Math.ceil(mappingsTotal / mappingsPageSize),
|
||
);
|
||
|
||
async function loadMigrationSettings() {
|
||
isLoadingMigration = true;
|
||
try {
|
||
const settingsRes = await api.requestApi("/migration/settings");
|
||
migrationCron = settingsRes.cron;
|
||
await loadMappingsPage();
|
||
} catch (err) {
|
||
console.error("[SettingsPage][Migration] Failed to load:", err);
|
||
} finally {
|
||
isLoadingMigration = false;
|
||
}
|
||
}
|
||
|
||
async function loadMappingsPage() {
|
||
try {
|
||
const skip = mappingsPage * mappingsPageSize;
|
||
let url = `/migration/mappings-data?skip=${skip}&limit=${mappingsPageSize}`;
|
||
if (mappingsSearch)
|
||
url += `&search=${encodeURIComponent(mappingsSearch)}`;
|
||
if (mappingsEnvFilter)
|
||
url += `&env_id=${encodeURIComponent(mappingsEnvFilter)}`;
|
||
if (mappingsTypeFilter)
|
||
url += `&resource_type=${encodeURIComponent(mappingsTypeFilter)}`;
|
||
const res = await api.requestApi(url);
|
||
displayMappings = res.items || [];
|
||
mappingsTotal = res.total || 0;
|
||
} catch (err) {
|
||
console.error("[SettingsPage][Migration] Failed to load mappings:", err);
|
||
}
|
||
}
|
||
|
||
function onMappingsSearchInput(e) {
|
||
clearTimeout(searchTimeout);
|
||
searchTimeout = setTimeout(() => {
|
||
mappingsPage = 0;
|
||
loadMappingsPage();
|
||
}, 300);
|
||
}
|
||
|
||
function onMappingsFilterChange() {
|
||
mappingsPage = 0;
|
||
loadMappingsPage();
|
||
}
|
||
|
||
function goToMappingsPage(page) {
|
||
if (page < 0 || page >= mappingsTotalPages) return;
|
||
mappingsPage = page;
|
||
loadMappingsPage();
|
||
}
|
||
|
||
async function saveMigrationSettings() {
|
||
isSavingMigration = true;
|
||
try {
|
||
await api.putApi("/migration/settings", { cron: migrationCron });
|
||
addToast(
|
||
$t.settings?.save_success ,
|
||
"success",
|
||
);
|
||
} catch (err) {
|
||
addToast(
|
||
$t.settings?.save_failed ,
|
||
"error",
|
||
);
|
||
} finally {
|
||
isSavingMigration = false;
|
||
}
|
||
}
|
||
|
||
async function triggerSyncNow() {
|
||
isSyncing = true;
|
||
try {
|
||
const result = await api.postApi("/migration/sync-now", {});
|
||
addToast(
|
||
`Synced ${result.synced_count} environment(s)${result.failed_count > 0 ? `, ${result.failed_count} failed` : ""}`,
|
||
result.failed_count > 0 ? "warning" : "success",
|
||
);
|
||
await loadMigrationSettings();
|
||
} catch (err) {
|
||
console.error("[SettingsPage][Migration] Sync failed:", err);
|
||
addToast(err.message || $t.settings?.migration_sync_failed , "error");
|
||
} finally {
|
||
isSyncing = false;
|
||
}
|
||
}
|
||
|
||
// Handle global settings save (Logging, Storage)
|
||
async function handleSave() {
|
||
console.log("[SettingsPage][Action] Saving settings");
|
||
try {
|
||
settings.llm = normalizeLlmSettings(settings.llm);
|
||
// In a real app we might want to only send the changed section,
|
||
// but updateConsolidatedSettings expects full object or we can use specific endpoints.
|
||
// For now we use the consolidated update.
|
||
await api.updateConsolidatedSettings(settings);
|
||
addToast($t.settings?.save_success , "success");
|
||
} catch (err) {
|
||
console.error("[SettingsPage][Coherence:Failed]", err);
|
||
addToast($t.settings?.save_failed , "error");
|
||
}
|
||
}
|
||
|
||
// Handle environment actions
|
||
async function handleTestEnv(id) {
|
||
console.log(`[SettingsPage][Action] Test environment ${id}`);
|
||
addToast($t.settings?.testing_connection , "info");
|
||
try {
|
||
const result = await api.testEnvironmentConnection(id);
|
||
if (result.status === "success") {
|
||
addToast($t.settings?.connection_success , "success");
|
||
} else {
|
||
addToast(
|
||
($t.settings?.connection_failed ).replace(
|
||
"{error}",
|
||
result.message || $t.common?.unknown ,
|
||
),
|
||
"error",
|
||
);
|
||
}
|
||
} catch (err) {
|
||
console.error(
|
||
"[SettingsPage][Coherence:Failed] Error testing connection:",
|
||
err,
|
||
);
|
||
addToast($t.settings?.connection_test_failed , "error");
|
||
}
|
||
}
|
||
|
||
function editEnv(env) {
|
||
console.log(`[SettingsPage][Action] Edit environment ${env.id}`);
|
||
newEnv = JSON.parse(JSON.stringify(env)); // Deep copy
|
||
// Ensure backup_schedule exists
|
||
if (!newEnv.backup_schedule) {
|
||
newEnv.backup_schedule = { enabled: false, cron_expression: "0 0 * * *" };
|
||
}
|
||
editingEnvId = env.id;
|
||
isAddingEnv = false;
|
||
}
|
||
|
||
function resetEnvForm() {
|
||
newEnv = {
|
||
id: "",
|
||
name: "",
|
||
url: "",
|
||
username: "",
|
||
password: "",
|
||
is_default: false,
|
||
backup_schedule: {
|
||
enabled: false,
|
||
cron_expression: "0 0 * * *",
|
||
},
|
||
};
|
||
editingEnvId = null;
|
||
}
|
||
|
||
async function handleAddOrUpdateEnv() {
|
||
try {
|
||
console.log(
|
||
`[SettingsPage][Action] ${editingEnvId ? "Updating" : "Adding"} environment.`,
|
||
);
|
||
|
||
// Basic validation
|
||
if (!newEnv.id || !newEnv.name || !newEnv.url) {
|
||
addToast(
|
||
$t.settings?.env_required_fields,
|
||
"error",
|
||
);
|
||
return;
|
||
}
|
||
|
||
if (editingEnvId) {
|
||
await api.updateEnvironment(editingEnvId, newEnv);
|
||
addToast($t.settings?.env_updated , "success");
|
||
} else {
|
||
await api.addEnvironment(newEnv);
|
||
addToast($t.settings?.env_added , "success");
|
||
}
|
||
|
||
resetEnvForm();
|
||
editingEnvId = null;
|
||
isAddingEnv = false;
|
||
await loadSettings();
|
||
} catch (error) {
|
||
console.error(
|
||
"[SettingsPage][Coherence:Failed] Failed to save environment:",
|
||
error,
|
||
);
|
||
addToast(error.message || $t.settings?.env_save_failed , "error");
|
||
}
|
||
}
|
||
|
||
async function handleDeleteEnv(id) {
|
||
if (
|
||
confirm(
|
||
$t.settings?.env_delete_confirm,
|
||
)
|
||
) {
|
||
console.log(`[SettingsPage][Action] Delete environment ${id}`);
|
||
try {
|
||
await api.deleteEnvironment(id);
|
||
addToast($t.settings?.env_deleted , "success");
|
||
await loadSettings();
|
||
} catch (error) {
|
||
console.error(
|
||
"[SettingsPage][Coherence:Failed] Failed to delete environment:",
|
||
error,
|
||
);
|
||
addToast($t.settings?.env_delete_failed , "error");
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<div class="mx-auto w-full max-w-7xl space-y-6">
|
||
<!-- Header -->
|
||
<div class="flex items-center justify-between mb-6">
|
||
<h1 class="text-2xl font-bold text-gray-900">
|
||
{$t.settings?.title }
|
||
</h1>
|
||
<button
|
||
class="inline-flex items-center justify-center rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-hover"
|
||
on:click={loadSettings}
|
||
>
|
||
{$t.common?.refresh }
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Error Banner -->
|
||
{#if error}
|
||
<div
|
||
class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4 flex items-center justify-between"
|
||
>
|
||
<span>{error}</span>
|
||
<button
|
||
class="px-4 py-2 bg-destructive text-white rounded hover:bg-destructive-hover transition-colors"
|
||
on:click={loadSettings}
|
||
>
|
||
{$t.common?.retry }
|
||
</button>
|
||
</div>
|
||
{/if}
|
||
|
||
<!-- Loading State -->
|
||
{#if isLoading}
|
||
<div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
|
||
<div class="space-y-3">
|
||
<div class="h-8 animate-pulse rounded bg-slate-200"></div>
|
||
<div class="h-8 animate-pulse rounded bg-slate-200"></div>
|
||
<div class="h-8 animate-pulse rounded bg-slate-200"></div>
|
||
<div class="h-8 animate-pulse rounded bg-slate-200"></div>
|
||
<div class="h-8 animate-pulse rounded bg-slate-200"></div>
|
||
</div>
|
||
</div>
|
||
{:else if settings}
|
||
<!-- Tabs -->
|
||
<div class="border-b border-gray-200 mb-6">
|
||
<button
|
||
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none border-b-2"
|
||
class:text-blue-600={activeTab === "environments"}
|
||
class:border-blue-600={activeTab === "environments"}
|
||
class:text-gray-600={activeTab !== "environments"}
|
||
class:border-transparent={activeTab !== "environments"}
|
||
class:hover:text-gray-800={activeTab !== "environments"}
|
||
class:hover:border-gray-300={activeTab !== "environments"}
|
||
aria-current={activeTab === "environments" ? "page" : undefined}
|
||
on:click={() => handleTabChange("environments")}
|
||
>
|
||
{$t.settings?.environments }
|
||
</button>
|
||
<button
|
||
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none border-b-2"
|
||
class:text-blue-600={activeTab === "logging"}
|
||
class:border-blue-600={activeTab === "logging"}
|
||
class:text-gray-600={activeTab !== "logging"}
|
||
class:border-transparent={activeTab !== "logging"}
|
||
class:hover:text-gray-800={activeTab !== "logging"}
|
||
class:hover:border-gray-300={activeTab !== "logging"}
|
||
aria-current={activeTab === "logging" ? "page" : undefined}
|
||
on:click={() => handleTabChange("logging")}
|
||
>
|
||
{$t.settings?.logging }
|
||
</button>
|
||
<button
|
||
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none border-b-2"
|
||
class:text-blue-600={activeTab === "connections"}
|
||
class:border-blue-600={activeTab === "connections"}
|
||
class:text-gray-600={activeTab !== "connections"}
|
||
class:border-transparent={activeTab !== "connections"}
|
||
class:hover:text-gray-800={activeTab !== "connections"}
|
||
class:hover:border-gray-300={activeTab !== "connections"}
|
||
aria-current={activeTab === "connections" ? "page" : undefined}
|
||
on:click={() => handleTabChange("connections")}
|
||
>
|
||
{$t.settings?.connections }
|
||
</button>
|
||
<button
|
||
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none border-b-2"
|
||
class:text-blue-600={activeTab === "llm"}
|
||
class:border-blue-600={activeTab === "llm"}
|
||
class:text-gray-600={activeTab !== "llm"}
|
||
class:border-transparent={activeTab !== "llm"}
|
||
class:hover:text-gray-800={activeTab !== "llm"}
|
||
class:hover:border-gray-300={activeTab !== "llm"}
|
||
aria-current={activeTab === "llm" ? "page" : undefined}
|
||
on:click={() => handleTabChange("llm")}
|
||
>
|
||
{$t.settings?.llm }
|
||
</button>
|
||
<button
|
||
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none border-b-2"
|
||
class:text-blue-600={activeTab === "migration"}
|
||
class:border-blue-600={activeTab === "migration"}
|
||
class:text-gray-600={activeTab !== "migration"}
|
||
class:border-transparent={activeTab !== "migration"}
|
||
class:hover:text-gray-800={activeTab !== "migration"}
|
||
class:hover:border-gray-300={activeTab !== "migration"}
|
||
aria-current={activeTab === "migration" ? "page" : undefined}
|
||
on:click={() => handleTabChange("migration")}
|
||
>
|
||
{$t.settings?.migration_sync }
|
||
</button>
|
||
<button
|
||
class="px-4 py-2 text-sm font-medium transition-colors focus:outline-none border-b-2"
|
||
class:text-blue-600={activeTab === "storage"}
|
||
class:border-blue-600={activeTab === "storage"}
|
||
class:text-gray-600={activeTab !== "storage"}
|
||
class:border-transparent={activeTab !== "storage"}
|
||
class:hover:text-gray-800={activeTab !== "storage"}
|
||
class:hover:border-gray-300={activeTab !== "storage"}
|
||
aria-current={activeTab === "storage" ? "page" : undefined}
|
||
on:click={() => handleTabChange("storage")}
|
||
>
|
||
{$t.settings?.storage }
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Tab Content -->
|
||
<div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
|
||
{#if activeTab === "environments"}
|
||
<!-- Environments Tab -->
|
||
<div class="text-lg font-medium mb-4">
|
||
<h2 class="text-xl font-bold mb-4">
|
||
{$t.settings?.environments }
|
||
</h2>
|
||
<p class="text-gray-600 mb-6">
|
||
{$t.settings?.env_description}
|
||
</p>
|
||
|
||
{#if !editingEnvId && !isAddingEnv}
|
||
<div class="flex justify-end mb-6">
|
||
<button
|
||
class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700"
|
||
on:click={() => {
|
||
isAddingEnv = true;
|
||
resetEnvForm();
|
||
}}
|
||
>
|
||
{$t.settings?.env_add }
|
||
</button>
|
||
</div>
|
||
{/if}
|
||
|
||
{#if editingEnvId || isAddingEnv}
|
||
<!-- Add/Edit Environment Form -->
|
||
<div class="bg-gray-50 p-6 rounded-lg mb-6 border border-gray-200">
|
||
<h3 class="text-lg font-medium mb-4">
|
||
{editingEnvId
|
||
? $t.settings?.env_edit
|
||
: $t.settings?.env_add }
|
||
</h3>
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<label
|
||
for="env_id"
|
||
class="block text-sm font-medium text-gray-700"
|
||
>{$t.common?.id }</label
|
||
>
|
||
<input
|
||
type="text"
|
||
id="env_id"
|
||
bind:value={newEnv.id}
|
||
disabled={!!editingEnvId}
|
||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 disabled:bg-gray-100 disabled:text-gray-500"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label
|
||
for="env_name"
|
||
class="block text-sm font-medium text-gray-700"
|
||
>{$t.connections?.name }</label
|
||
>
|
||
<input
|
||
type="text"
|
||
id="env_name"
|
||
bind:value={newEnv.name}
|
||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label
|
||
for="env_url"
|
||
class="block text-sm font-medium text-gray-700">{$t.settings?.env_url}</label
|
||
>
|
||
<input
|
||
type="text"
|
||
id="env_url"
|
||
bind:value={newEnv.url}
|
||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label
|
||
for="env_user"
|
||
class="block text-sm font-medium text-gray-700"
|
||
>{$t.connections?.user }</label
|
||
>
|
||
<input
|
||
type="text"
|
||
id="env_user"
|
||
bind:value={newEnv.username}
|
||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label
|
||
for="env_pass"
|
||
class="block text-sm font-medium text-gray-700"
|
||
>{$t.connections?.pass }</label
|
||
>
|
||
<input
|
||
type="password"
|
||
id="env_pass"
|
||
bind:value={newEnv.password}
|
||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
|
||
/>
|
||
</div>
|
||
<div class="flex items-center mt-6">
|
||
<input
|
||
type="checkbox"
|
||
id="env_default"
|
||
bind:checked={newEnv.is_default}
|
||
class="h-4 w-4 text-blue-600 border-gray-300 rounded"
|
||
/>
|
||
<label
|
||
for="env_default"
|
||
class="ml-2 block text-sm text-gray-900"
|
||
>{$t.settings?.env_default }</label
|
||
>
|
||
</div>
|
||
</div>
|
||
|
||
<h3 class="text-lg font-medium mb-4 mt-6">
|
||
{$t.tasks?.backup_schedule }
|
||
</h3>
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div class="flex items-center">
|
||
<input
|
||
type="checkbox"
|
||
id="backup_enabled"
|
||
bind:checked={newEnv.backup_schedule.enabled}
|
||
class="h-4 w-4 text-blue-600 border-gray-300 rounded"
|
||
/>
|
||
<label
|
||
for="backup_enabled"
|
||
class="ml-2 block text-sm text-gray-900"
|
||
>{$t.settings?.enable_auto_backups }</label
|
||
>
|
||
</div>
|
||
<div>
|
||
<label
|
||
for="cron_expression"
|
||
class="block text-sm font-medium text-gray-700"
|
||
>{$t.tasks?.cron_label }</label
|
||
>
|
||
<input
|
||
type="text"
|
||
id="cron_expression"
|
||
bind:value={newEnv.backup_schedule.cron_expression}
|
||
placeholder={$t.settings?.cron_placeholder }
|
||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
|
||
/>
|
||
<p class="text-xs text-gray-500 mt-1">
|
||
{$t.tasks?.cron_hint }
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-6 flex gap-2 justify-end">
|
||
<button
|
||
on:click={() => {
|
||
isAddingEnv = false;
|
||
editingEnvId = null;
|
||
resetEnvForm();
|
||
}}
|
||
class="bg-gray-200 text-gray-700 px-4 py-2 rounded hover:bg-gray-300"
|
||
>
|
||
{$t.common?.cancel }
|
||
</button>
|
||
<button
|
||
on:click={handleAddOrUpdateEnv}
|
||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
||
>
|
||
{editingEnvId
|
||
? $t.settings?.env_update
|
||
: $t.settings?.env_add }
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|
||
{#if settings.environments && settings.environments.length > 0}
|
||
<div class="mt-6 overflow-x-auto border border-gray-200 rounded-lg">
|
||
<table class="min-w-full divide-y divide-gray-200">
|
||
<thead class="bg-gray-50">
|
||
<tr>
|
||
<th
|
||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||
>{$t.connections?.name }</th
|
||
>
|
||
<th
|
||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||
>{$t.settings?.env_url }</th
|
||
>
|
||
<th
|
||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||
>{$t.connections?.user }</th
|
||
>
|
||
<th
|
||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||
>{$t.settings?.default }</th
|
||
>
|
||
<th
|
||
class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||
>{$t.settings?.env_actions }</th
|
||
>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="bg-white divide-y divide-gray-200">
|
||
{#each settings.environments as env}
|
||
<tr>
|
||
<td class="px-6 py-4 whitespace-nowrap">{env.name}</td>
|
||
<td class="px-6 py-4 whitespace-nowrap">{env.url}</td>
|
||
<td class="px-6 py-4 whitespace-nowrap">{env.username}</td
|
||
>
|
||
<td class="px-6 py-4 whitespace-nowrap">
|
||
{#if env.is_default}
|
||
<span
|
||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"
|
||
>
|
||
{$t.common?.yes }
|
||
</span>
|
||
{:else}
|
||
<span class="text-gray-500">{$t.common?.no }</span>
|
||
{/if}
|
||
</td>
|
||
<td
|
||
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"
|
||
>
|
||
<button
|
||
class="text-green-600 hover:text-green-900 mr-4"
|
||
on:click={() => handleTestEnv(env.id)}
|
||
>
|
||
{$t.settings?.env_test }
|
||
</button>
|
||
<button
|
||
class="text-indigo-600 hover:text-indigo-900 mr-4"
|
||
on:click={() => editEnv(env)}
|
||
>
|
||
{$t.common.edit }
|
||
</button>
|
||
<button
|
||
class="text-red-600 hover:text-red-900"
|
||
on:click={() => handleDeleteEnv(env.id)}
|
||
>
|
||
{$t.settings?.env_delete }
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
{/each}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{:else if !isAddingEnv}
|
||
<div
|
||
class="mb-4 p-4 bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700"
|
||
>
|
||
<p class="font-bold">{$t.settings?.warning }</p>
|
||
<p>
|
||
No Superset environments configured. You must add at least one
|
||
environment to perform backups or migrations.
|
||
</p>
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
{:else if activeTab === "logging"}
|
||
<!-- Logging Tab -->
|
||
<div class="text-lg font-medium mb-4">
|
||
<h2 class="text-xl font-bold mb-4">
|
||
{$t.settings?.logging }
|
||
</h2>
|
||
<p class="text-gray-600 mb-6">
|
||
{$t.settings?.logging_description}
|
||
</p>
|
||
|
||
<div class="bg-gray-50 p-6 rounded-lg border border-gray-200">
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<label
|
||
for="log_level"
|
||
class="block text-sm font-medium text-gray-700"
|
||
>{$t.settings?.log_level }</label
|
||
>
|
||
<select
|
||
id="log_level"
|
||
bind:value={settings.logging.level}
|
||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
|
||
>
|
||
<option value="DEBUG">DEBUG</option>
|
||
<option value="INFO">INFO</option>
|
||
<option value="WARNING">WARNING</option>
|
||
<option value="ERROR">ERROR</option>
|
||
<option value="CRITICAL">CRITICAL</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label
|
||
for="task_log_level"
|
||
class="block text-sm font-medium text-gray-700"
|
||
>{$t.settings?.task_log_level }</label
|
||
>
|
||
<select
|
||
id="task_log_level"
|
||
bind:value={settings.logging.task_log_level}
|
||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
|
||
>
|
||
<option value="DEBUG">DEBUG</option>
|
||
<option value="INFO">INFO</option>
|
||
<option value="WARNING">WARNING</option>
|
||
<option value="ERROR">ERROR</option>
|
||
</select>
|
||
</div>
|
||
<div class="md:col-span-2">
|
||
<label class="flex items-center">
|
||
<input
|
||
type="checkbox"
|
||
id="enable_belief_state"
|
||
bind:checked={settings.logging.enable_belief_state}
|
||
class="h-4 w-4 text-blue-600 border-gray-300 rounded"
|
||
/>
|
||
<span class="ml-2 block text-sm text-gray-900"
|
||
>{$t.settings?.enable_belief_state }</span
|
||
>
|
||
</label>
|
||
<p class="text-xs text-gray-500 mt-1 ml-6">
|
||
{$t.settings?.belief_state_hint}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-6 flex justify-end">
|
||
<button
|
||
on:click={handleSave}
|
||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
||
>
|
||
{$t.settings?.save_logging }
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{:else if activeTab === "connections"}
|
||
<!-- Connections Tab -->
|
||
<div class="text-lg font-medium mb-4">
|
||
<h2 class="text-xl font-bold mb-4">
|
||
{$t.settings?.connections }
|
||
</h2>
|
||
<p class="text-gray-600 mb-6">
|
||
{$t.settings?.connections_description}
|
||
</p>
|
||
|
||
{#if settings.connections && settings.connections.length > 0}
|
||
<!-- Connections list would go here -->
|
||
<p class="text-gray-500 italic">
|
||
No additional connections configured. Superset database
|
||
connections are used by default.
|
||
</p>
|
||
{:else}
|
||
<div
|
||
class="text-center py-8 bg-gray-50 rounded-lg border border-dashed border-gray-300"
|
||
>
|
||
<p class="text-gray-500">
|
||
{$t.settings?.no_external_connections}
|
||
</p>
|
||
<button
|
||
class="mt-4 px-4 py-2 border border-blue-600 text-blue-600 rounded hover:bg-blue-50"
|
||
>
|
||
{$t.connections?.add_new }
|
||
</button>
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
{:else if activeTab === "llm"}
|
||
<!-- LLM Tab -->
|
||
<div class="text-lg font-medium mb-4">
|
||
<h2 class="text-xl font-bold mb-4">
|
||
{$t.settings?.llm }
|
||
</h2>
|
||
<p class="text-gray-600 mb-6">
|
||
{$t.settings?.llm_description}
|
||
</p>
|
||
|
||
<ProviderConfig
|
||
providers={settings.llm_providers || []}
|
||
onSave={loadSettings}
|
||
/>
|
||
|
||
<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}
|
||
</h3>
|
||
<p class="mt-1 text-sm text-gray-600">
|
||
{$t.settings?.llm_chatbot_settings_description}
|
||
</p>
|
||
|
||
<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"
|
||
>
|
||
{$t.settings?.llm_chatbot_provider }
|
||
</label>
|
||
<select
|
||
id="planner-provider"
|
||
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 }</option
|
||
>
|
||
{#each settings.llm_providers || [] as provider}
|
||
<option value={provider.id}>
|
||
{provider.name} ({provider.default_model})
|
||
</option>
|
||
{/each}
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label
|
||
for="planner-model"
|
||
class="block text-sm font-medium text-gray-700"
|
||
>
|
||
{$t.settings?.llm_chatbot_model }
|
||
</label>
|
||
<input
|
||
id="planner-model"
|
||
type="text"
|
||
bind:value={settings.llm.assistant_planner_model}
|
||
placeholder={$t.settings?.llm_chatbot_model_placeholder}
|
||
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<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}
|
||
</h3>
|
||
<p class="mt-1 text-sm text-gray-600">
|
||
{$t.settings?.llm_provider_bindings_description}
|
||
</p>
|
||
|
||
<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}
|
||
</label>
|
||
<select
|
||
id="binding-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 }</option
|
||
>
|
||
{#each settings.llm_providers || [] as provider}
|
||
<option value={provider.id}>
|
||
{provider.name} ({provider.default_model})
|
||
</option>
|
||
{/each}
|
||
</select>
|
||
{#if !isDashboardValidationBindingValid()}
|
||
<p class="mt-1 text-xs text-amber-700">
|
||
{$t.settings?.llm_multimodal_warning}
|
||
</p>
|
||
{/if}
|
||
</div>
|
||
|
||
<div>
|
||
<label
|
||
for="binding-documentation"
|
||
class="block text-sm font-medium text-gray-700"
|
||
>
|
||
{$t.settings?.llm_binding_documentation}
|
||
</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 }</option
|
||
>
|
||
{#each settings.llm_providers || [] as provider}
|
||
<option value={provider.id}>
|
||
{provider.name} ({provider.default_model})
|
||
</option>
|
||
{/each}
|
||
</select>
|
||
</div>
|
||
|
||
<div class="md:col-span-2">
|
||
<label
|
||
for="binding-git-commit"
|
||
class="block text-sm font-medium text-gray-700"
|
||
>
|
||
{$t.settings?.llm_binding_git_commit }
|
||
</label>
|
||
<select
|
||
id="binding-git-commit"
|
||
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 }</option
|
||
>
|
||
{#each settings.llm_providers || [] as provider}
|
||
<option value={provider.id}>
|
||
{provider.name} ({provider.default_model})
|
||
</option>
|
||
{/each}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-6 rounded-lg border border-gray-200 bg-gray-50 p-4">
|
||
<h3 class="text-base font-semibold text-gray-900">
|
||
{$t.settings?.llm_prompts_title }
|
||
</h3>
|
||
<p class="mt-1 text-sm text-gray-600">
|
||
{$t.settings?.llm_prompts_description}
|
||
</p>
|
||
|
||
<div class="mt-4 space-y-4">
|
||
<div>
|
||
<label
|
||
for="documentation-prompt"
|
||
class="block text-sm font-medium text-gray-700"
|
||
>
|
||
{$t.settings?.llm_prompt_documentation}
|
||
</label>
|
||
<textarea
|
||
id="documentation-prompt"
|
||
bind:value={settings.llm.prompts.documentation_prompt}
|
||
rows="8"
|
||
class="mt-1 block w-full rounded-md border border-gray-300 p-2 font-mono text-xs"
|
||
></textarea>
|
||
</div>
|
||
|
||
<div>
|
||
<label
|
||
for="dashboard-validation-prompt"
|
||
class="block text-sm font-medium text-gray-700"
|
||
>
|
||
{$t.settings?.llm_prompt_dashboard_validation}
|
||
</label>
|
||
<textarea
|
||
id="dashboard-validation-prompt"
|
||
bind:value={settings.llm.prompts.dashboard_validation_prompt}
|
||
rows="10"
|
||
class="mt-1 block w-full rounded-md border border-gray-300 p-2 font-mono text-xs"
|
||
></textarea>
|
||
</div>
|
||
|
||
<div>
|
||
<label
|
||
for="git-commit-prompt"
|
||
class="block text-sm font-medium text-gray-700"
|
||
>
|
||
{$t.settings?.llm_prompt_git_commit }
|
||
</label>
|
||
<textarea
|
||
id="git-commit-prompt"
|
||
bind:value={settings.llm.prompts.git_commit_prompt}
|
||
rows="8"
|
||
class="mt-1 block w-full rounded-md border border-gray-300 p-2 font-mono text-xs"
|
||
></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-4 flex justify-end">
|
||
<button
|
||
class="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
|
||
on:click={handleSave}
|
||
>
|
||
{$t.settings?.save_llm_prompts }
|
||
</button>
|
||
</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">
|
||
{$t.settings?.migration_sync_title}
|
||
</h2>
|
||
<p class="text-gray-600 mb-6">
|
||
{$t.settings?.migration_sync_description}
|
||
</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">
|
||
{$t.settings?.sync_schedule }
|
||
</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"
|
||
>{$t.tasks?.cron_label }</label
|
||
>
|
||
<input
|
||
type="text"
|
||
id="migration_cron"
|
||
bind:value={migrationCron}
|
||
placeholder={$t.settings?.migration_cron_placeholder }
|
||
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">
|
||
{$t.settings?.migration_cron_hint}
|
||
</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
|
||
? $t.settings?.saving
|
||
: $t.common?.save }
|
||
</button>
|
||
<button
|
||
on:click={triggerSyncNow}
|
||
disabled={isSyncing}
|
||
class="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 h-[42px] min-w-[150px] flex items-center justify-center gap-2 disabled:opacity-50"
|
||
>
|
||
{#if isSyncing}
|
||
<svg
|
||
class="w-4 h-4 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
|
||
>
|
||
{$t.settings?.syncing }
|
||
{:else}
|
||
<svg
|
||
class="w-4 h-4"
|
||
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
|
||
>
|
||
{$t.settings?.sync_now }
|
||
{/if}
|
||
</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
|
||
>{$t.settings?.synchronized_resources} <span
|
||
class="text-sm font-normal text-gray-500"
|
||
>({mappingsTotal})</span
|
||
></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
|
||
>
|
||
{$t.common?.refresh }
|
||
</button>
|
||
</h3>
|
||
|
||
<!-- Search and Filters -->
|
||
<div class="flex flex-wrap gap-3 mb-4">
|
||
<div class="flex-1 min-w-[200px]">
|
||
<input
|
||
type="text"
|
||
bind:value={mappingsSearch}
|
||
on:input={onMappingsSearchInput}
|
||
placeholder={$t.settings?.search_by_name_or_uuid}
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||
/>
|
||
</div>
|
||
<select
|
||
bind:value={mappingsEnvFilter}
|
||
on:change={onMappingsFilterChange}
|
||
class="px-3 py-2 border border-gray-300 rounded-md text-sm bg-white focus:ring-indigo-500 focus:border-indigo-500"
|
||
>
|
||
<option value="">
|
||
{$t.settings?.all_environments }
|
||
</option>
|
||
<option value="ss1">ss1</option>
|
||
<option value="ss2">ss2</option>
|
||
</select>
|
||
<select
|
||
bind:value={mappingsTypeFilter}
|
||
on:change={onMappingsFilterChange}
|
||
class="px-3 py-2 border border-gray-300 rounded-md text-sm bg-white focus:ring-indigo-500 focus:border-indigo-500"
|
||
>
|
||
<option value="">{$t.settings?.all_types }</option>
|
||
<option value="chart">{$t.settings?.type_chart }</option>
|
||
<option value="dataset">
|
||
{$t.nav?.datasets }
|
||
</option>
|
||
<option value="dashboard">
|
||
{$t.nav?.dashboards }
|
||
</option>
|
||
</select>
|
||
</div>
|
||
|
||
<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"
|
||
>{$t.settings?.resource_name }</th
|
||
>
|
||
<th
|
||
class="px-6 py-3 text-left font-medium text-gray-500 uppercase tracking-wider"
|
||
>{$t.settings?.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"
|
||
>{$t.settings?.target_id }</th
|
||
>
|
||
<th
|
||
class="px-6 py-3 text-left font-medium text-gray-500 uppercase tracking-wider"
|
||
>{$t.dashboard?.environment }</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"
|
||
>{$t.settings?.loading_mappings }</td
|
||
></tr
|
||
>
|
||
{:else if displayMappings.length === 0}
|
||
<tr
|
||
><td
|
||
colspan="5"
|
||
class="px-6 py-8 text-center text-gray-500"
|
||
>{mappingsSearch ||
|
||
mappingsEnvFilter ||
|
||
mappingsTypeFilter
|
||
? $t.settings?.no_matching_resources
|
||
: ($t.settings?.no_synchronized_resources)}</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 ||
|
||
$t.common?.not_available}</td
|
||
>
|
||
<td class="px-6 py-4 whitespace-nowrap"
|
||
><span
|
||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {mapping.resource_type ===
|
||
'chart'
|
||
? 'bg-blue-100 text-blue-800'
|
||
: mapping.resource_type === 'dataset'
|
||
? 'bg-green-100 text-green-800'
|
||
: 'bg-purple-100 text-purple-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>
|
||
|
||
<!-- Pagination Controls -->
|
||
{#if mappingsTotal > mappingsPageSize}
|
||
<div
|
||
class="flex items-center justify-between mt-4 text-sm text-gray-600"
|
||
>
|
||
<span
|
||
>{(
|
||
$t.dashboard?.showing
|
||
)
|
||
.replace(
|
||
"{start}",
|
||
String(mappingsPage * mappingsPageSize + 1),
|
||
)
|
||
.replace(
|
||
"{end}",
|
||
String(
|
||
Math.min(
|
||
(mappingsPage + 1) * mappingsPageSize,
|
||
mappingsTotal,
|
||
),
|
||
),
|
||
)
|
||
.replace("{total}", String(mappingsTotal))}</span
|
||
>
|
||
<div class="flex items-center gap-2">
|
||
<button
|
||
on:click={() => goToMappingsPage(0)}
|
||
disabled={mappingsPage === 0}
|
||
class="px-2 py-1 rounded border {mappingsPage === 0
|
||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||
: 'bg-white hover:bg-gray-50 text-gray-700'}">«</button
|
||
>
|
||
<button
|
||
on:click={() => goToMappingsPage(mappingsPage - 1)}
|
||
disabled={mappingsPage === 0}
|
||
class="px-2 py-1 rounded border {mappingsPage === 0
|
||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||
: 'bg-white hover:bg-gray-50 text-gray-700'}">‹</button
|
||
>
|
||
<span class="px-3 py-1 font-medium"
|
||
>{mappingsPage + 1} / {mappingsTotalPages}</span
|
||
>
|
||
<button
|
||
on:click={() => goToMappingsPage(mappingsPage + 1)}
|
||
disabled={mappingsPage >= mappingsTotalPages - 1}
|
||
class="px-2 py-1 rounded border {mappingsPage >=
|
||
mappingsTotalPages - 1
|
||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||
: 'bg-white hover:bg-gray-50 text-gray-700'}">›</button
|
||
>
|
||
<button
|
||
on:click={() => goToMappingsPage(mappingsTotalPages - 1)}
|
||
disabled={mappingsPage >= mappingsTotalPages - 1}
|
||
class="px-2 py-1 rounded border {mappingsPage >=
|
||
mappingsTotalPages - 1
|
||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||
: 'bg-white hover:bg-gray-50 text-gray-700'}">»</button
|
||
>
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
{:else if activeTab === "storage"}
|
||
<!-- Storage Tab -->
|
||
<div class="text-lg font-medium mb-4">
|
||
<h2 class="text-xl font-bold mb-4">
|
||
{$t.settings?.storage_title }
|
||
</h2>
|
||
<p class="text-gray-600 mb-6">
|
||
{$t.settings?.storage_description}
|
||
</p>
|
||
|
||
<div class="bg-gray-50 p-6 rounded-lg border border-gray-200">
|
||
<div class="grid grid-cols-1 gap-4">
|
||
<div>
|
||
<label
|
||
for="storage_path"
|
||
class="block text-sm font-medium text-gray-700"
|
||
>{$t.storage?.root }</label
|
||
>
|
||
<input
|
||
type="text"
|
||
id="storage_path"
|
||
bind:value={settings.storage.root_path}
|
||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label
|
||
for="backup_path"
|
||
class="block text-sm font-medium text-gray-700"
|
||
>{$t.storage?.backups }</label
|
||
>
|
||
<input
|
||
type="text"
|
||
id="backup_path"
|
||
bind:value={settings.storage.backup_path}
|
||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label
|
||
for="repo_path"
|
||
class="block text-sm font-medium text-gray-700"
|
||
>{$t.storage?.repositories }</label
|
||
>
|
||
<input
|
||
type="text"
|
||
id="repo_path"
|
||
bind:value={settings.storage.repo_path}
|
||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-6 flex justify-end">
|
||
<button
|
||
on:click={() => handleSave()}
|
||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
||
>
|
||
{$t.settings?.save_storage_config}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
|
||
<!-- [/DEF:SettingsPage:Page] -->
|