feat: integrate SvelteKit for seamless navigation and improved data loading

This commit is contained in:
2025-12-20 22:41:23 +03:00
parent 58831c536a
commit 9b7b743319
106 changed files with 16217 additions and 123 deletions

View File

@@ -1,5 +1,5 @@
<!-- [DEF:App:Component] -->
<!--
[DEF:App:Component]
@SEMANTICS: main, entrypoint, layout, navigation
@PURPOSE: The root component of the frontend application. Manages navigation and layout.
@LAYER: UI
@@ -7,11 +7,11 @@
@RELATION: DEPENDS_ON -> frontend/src/pages/Settings.svelte
@RELATION: DEPENDS_ON -> frontend/src/lib/stores.js
@PROPS: None
@EVENTS: None
@INVARIANT: Navigation state must be persisted in the currentPage store.
-->
<script>
// [SECTION: IMPORTS]
import { get } from 'svelte/store';
import Dashboard from './pages/Dashboard.svelte';
import Settings from './pages/Settings.svelte';
import { selectedPlugin, selectedTask, currentPage } from './lib/stores.js';
@@ -19,31 +19,37 @@
import DynamicForm from './components/DynamicForm.svelte';
import { api } from './lib/api.js';
import Toast from './components/Toast.svelte';
// [/SECTION]
// [DEF:handleFormSubmit:Function]
// @PURPOSE: Handles form submission for task creation.
// @PARAM: event (CustomEvent) - The submit event from DynamicForm.
/**
* @purpose Handles form submission for task creation.
* @param {CustomEvent} event - The submit event from DynamicForm.
*/
async function handleFormSubmit(event) {
console.log("[App.handleFormSubmit][Action] Handling form submission for task creation.");
const params = event.detail;
try {
const task = await api.createTask($selectedPlugin.id, params);
const plugin = get(selectedPlugin);
const task = await api.createTask(plugin.id, params);
selectedTask.set(task);
selectedPlugin.set(null);
console.log(`[App.handleFormSubmit][Coherence:OK] Task created context={{'id': '${task.id}'}}`);
console.log(`[App.handleFormSubmit][Coherence:OK] Task created id=${task.id}`);
} catch (error) {
console.error(`[App.handleFormSubmit][Coherence:Failed] Task creation failed context={{'error': '${error}'}}`);
console.error(`[App.handleFormSubmit][Coherence:Failed] Task creation failed error=${error}`);
}
}
// [/DEF:handleFormSubmit]
// [DEF:navigate:Function]
// @PURPOSE: Changes the current page and resets state.
// @PARAM: page (string) - Target page name.
/**
* @purpose Changes the current page and resets state.
* @param {string} page - Target page name.
*/
function navigate(page) {
console.log(`[App.navigate][Action] Navigating to ${page}.`);
// Reset selection first
if (page !== $currentPage) {
if (page !== get(currentPage)) {
selectedPlugin.set(null);
selectedTask.set(null);
}
@@ -53,6 +59,7 @@
// [/DEF:navigate]
</script>
<!-- [SECTION: TEMPLATE] -->
<Toast />
<main class="bg-gray-50 min-h-screen">
@@ -101,4 +108,6 @@
{/if}
</div>
</main>
<!-- [/SECTION] -->
<!-- [/DEF:App] -->

12
frontend/src/app.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -1,5 +1,5 @@
<!-- [DEF:DynamicForm:Component] -->
<!--
[DEF:DynamicForm:Component]
@SEMANTICS: form, schema, dynamic, json-schema
@PURPOSE: Generates a form dynamically based on a JSON schema.
@LAYER: UI
@@ -11,7 +11,9 @@
- submit: Object - Dispatched when the form is submitted, containing the form data.
-->
<script>
// [SECTION: IMPORTS]
import { createEventDispatcher } from 'svelte';
// [/SECTION]
export let schema;
let formData = {};
@@ -19,7 +21,9 @@
const dispatch = createEventDispatcher();
// [DEF:handleSubmit:Function]
// @PURPOSE: Dispatches the submit event with the form data.
/**
* @purpose Dispatches the submit event with the form data.
*/
function handleSubmit() {
console.log("[DynamicForm][Action] Submitting form data.", { formData });
dispatch('submit', formData);
@@ -27,7 +31,9 @@
// [/DEF:handleSubmit]
// [DEF:initializeForm:Function]
// @PURPOSE: Initialize form data with default values from the schema.
/**
* @purpose Initialize form data with default values from the schema.
*/
function initializeForm() {
if (schema && schema.properties) {
for (const key in schema.properties) {
@@ -40,6 +46,7 @@
initializeForm();
</script>
<!-- [SECTION: TEMPLATE] -->
<form on:submit|preventDefault={handleSubmit} class="space-y-4">
{#if schema && schema.properties}
{#each Object.entries(schema.properties) as [key, prop]}
@@ -76,4 +83,6 @@
</button>
{/if}
</form>
<!-- [/DEF:DynamicForm] -->
<!-- [/SECTION] -->
<!-- [/DEF:DynamicForm] -->

View File

@@ -0,0 +1,3 @@
<footer class="bg-white border-t p-4 mt-8 text-center text-gray-500 text-sm">
&copy; 2025 Superset Tools. All rights reserved.
</footer>

View File

@@ -0,0 +1,26 @@
<script>
import { page } from '$app/stores';
</script>
<header class="bg-white shadow-md p-4 flex justify-between items-center">
<a
href="/"
class="text-3xl font-bold text-gray-800 focus:outline-none"
>
Superset Tools
</a>
<nav class="space-x-4">
<a
href="/"
class="text-gray-600 hover:text-blue-600 font-medium {$page.url.pathname === '/' ? 'text-blue-600 border-b-2 border-blue-600' : ''}"
>
Dashboard
</a>
<a
href="/settings"
class="text-gray-600 hover:text-blue-600 font-medium {$page.url.pathname === '/settings' ? 'text-blue-600 border-b-2 border-blue-600' : ''}"
>
Settings
</a>
</nav>
</header>

View File

@@ -1,5 +1,5 @@
<!-- [DEF:TaskRunner:Component] -->
<!--
[DEF:TaskRunner:Component]
@SEMANTICS: task, runner, logs, websocket
@PURPOSE: Connects to a WebSocket to display real-time logs for a running task.
@LAYER: UI
@@ -9,19 +9,25 @@
@EVENTS: None
-->
<script>
// [SECTION: IMPORTS]
import { onMount, onDestroy } from 'svelte';
import { get } from 'svelte/store';
import { selectedTask, taskLogs } from '../lib/stores.js';
// [/SECTION]
let ws;
// [DEF:onMount:Function]
// @PURPOSE: Initialize WebSocket connection for task logs.
/**
* @purpose Initialize WebSocket connection for task logs.
*/
onMount(() => {
if ($selectedTask) {
console.log(`[TaskRunner][Entry] Connecting to logs for task: ${$selectedTask.id}`);
const task = get(selectedTask);
if (task) {
console.log(`[TaskRunner][Entry] Connecting to logs for task: ${task.id}`);
taskLogs.set([]); // Clear previous logs
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/logs/${$selectedTask.id}`;
const wsUrl = `${protocol}//${window.location.host}/ws/logs/${task.id}`;
ws = new WebSocket(wsUrl);
ws.onopen = () => {
@@ -45,7 +51,9 @@
// [/DEF:onMount]
// [DEF:onDestroy:Function]
// @PURPOSE: Close WebSocket connection when the component is destroyed.
/**
* @purpose Close WebSocket connection when the component is destroyed.
*/
onDestroy(() => {
if (ws) {
console.log("[TaskRunner][Action] Closing WebSocket connection.");
@@ -55,6 +63,7 @@
// [/DEF:onDestroy]
</script>
<!-- [SECTION: TEMPLATE] -->
<div class="p-4 border rounded-lg bg-white shadow-md">
{#if $selectedTask}
<h2 class="text-xl font-semibold mb-2">Task: {$selectedTask.plugin_id}</h2>
@@ -71,4 +80,6 @@
<p>No task selected.</p>
{/if}
</div>
<!-- [/DEF:TaskRunner] -->
<!-- [/SECTION] -->
<!-- [/DEF:TaskRunner] -->

View File

@@ -1,5 +1,5 @@
<!-- [DEF:Toast:Component] -->
<!--
[DEF:Toast:Component]
@SEMANTICS: toast, notification, feedback, ui
@PURPOSE: Displays transient notifications (toasts) in the bottom-right corner.
@LAYER: UI
@@ -9,9 +9,12 @@
@EVENTS: None
-->
<script>
// [SECTION: IMPORTS]
import { toasts } from '../lib/toasts.js';
// [/SECTION]
</script>
<!-- [SECTION: TEMPLATE] -->
<div class="fixed bottom-0 right-0 p-4 space-y-2">
{#each $toasts as toast (toast.id)}
<div class="p-4 rounded-md shadow-lg text-white
@@ -23,4 +26,6 @@
</div>
{/each}
</div>
<!-- [/DEF:Toast] -->
<!-- [/SECTION] -->
<!-- [/DEF:Toast] -->

View File

@@ -5,7 +5,7 @@
import { addToast } from './toasts.js';
const API_BASE_URL = '';
const API_BASE_URL = '/api';
// [DEF:fetchApi:Function]
// @PURPOSE: Generic GET request wrapper.

View File

@@ -1,5 +1,5 @@
<!-- [DEF:Dashboard:Component] -->
<!--
[DEF:Dashboard:Component]
@SEMANTICS: dashboard, plugins, tools, list
@PURPOSE: Displays the list of available plugins and allows selecting one.
@LAYER: UI
@@ -9,11 +9,15 @@
@EVENTS: None
-->
<script>
// [SECTION: IMPORTS]
import { onMount } from 'svelte';
import { plugins, fetchPlugins, selectedPlugin } from '../lib/stores.js';
// [/SECTION]
// [DEF:onMount:Function]
// @PURPOSE: Fetch plugins when the component mounts.
/**
* @purpose Fetch plugins when the component mounts.
*/
onMount(async () => {
console.log("[Dashboard][Entry] Component mounted, fetching plugins.");
await fetchPlugins();
@@ -21,8 +25,10 @@
// [/DEF:onMount]
// [DEF:selectPlugin:Function]
// @PURPOSE: Selects a plugin to display its form.
// @PARAM: plugin (Object) - The plugin object to select.
/**
* @purpose Selects a plugin to display its form.
* @param {Object} plugin - The plugin object to select.
*/
function selectPlugin(plugin) {
console.log(`[Dashboard][Action] Selecting plugin: ${plugin.id}`);
selectedPlugin.set(plugin);
@@ -30,6 +36,7 @@
// [/DEF:selectPlugin]
</script>
<!-- [SECTION: TEMPLATE] -->
<div class="container mx-auto p-4">
<h1 class="text-2xl font-bold mb-4">Available Tools</h1>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
@@ -37,6 +44,9 @@
<div
class="border rounded-lg p-4 cursor-pointer hover:bg-gray-100"
on:click={() => selectPlugin(plugin)}
role="button"
tabindex="0"
on:keydown={(e) => e.key === 'Enter' && selectPlugin(plugin)}
>
<h2 class="text-xl font-semibold">{plugin.name}</h2>
<p class="text-gray-600">{plugin.description}</p>
@@ -45,4 +55,6 @@
{/each}
</div>
</div>
<!-- [/DEF:Dashboard] -->
<!-- [/SECTION] -->
<!-- [/DEF:Dashboard] -->

View File

@@ -1,21 +1,21 @@
<!-- [DEF:Settings:Component] -->
<!--
[DEF:Settings:Component]
@SEMANTICS: settings, ui, configuration
@PURPOSE: The main settings page for the application, allowing management of environments and global settings.
@LAYER: UI
@RELATION: CALLS -> api.js
@RELATION: USES -> stores.js
@PROPS:
None
@EVENTS:
None
@PROPS: None
@EVENTS: None
@INVARIANT: Settings changes must be saved to the backend.
-->
<script>
// [SECTION: IMPORTS]
import { onMount } from 'svelte';
import { getSettings, updateGlobalSettings, getEnvironments, addEnvironment, updateEnvironment, deleteEnvironment, testEnvironmentConnection } from '../lib/api';
import { addToast } from '../lib/toasts';
// [/SECTION]
let settings = {
environments: [],
@@ -36,26 +36,47 @@ None
let editingEnvId = null;
// [DEF:loadSettings:Function]
/**
* @purpose Loads settings from the backend.
*/
async function loadSettings() {
try {
console.log("[Settings.loadSettings][Action] Loading settings.");
const data = await getSettings();
settings = data;
console.log("[Settings.loadSettings][Coherence:OK] Settings loaded.");
} catch (error) {
console.error("[Settings.loadSettings][Coherence:Failed] Failed to load settings:", error);
addToast('Failed to load settings', 'error');
}
}
// [/DEF:loadSettings]
// [DEF:handleSaveGlobal:Function]
/**
* @purpose Saves global settings to the backend.
*/
async function handleSaveGlobal() {
try {
console.log("[Settings.handleSaveGlobal][Action] Saving global settings.");
await updateGlobalSettings(settings.settings);
addToast('Global settings saved', 'success');
console.log("[Settings.handleSaveGlobal][Coherence:OK] Global settings saved.");
} catch (error) {
console.error("[Settings.handleSaveGlobal][Coherence:Failed] Failed to save global settings:", error);
addToast('Failed to save global settings', 'error');
}
}
// [/DEF:handleSaveGlobal]
// [DEF:handleAddOrUpdateEnv:Function]
/**
* @purpose Adds or updates an environment.
*/
async function handleAddOrUpdateEnv() {
try {
console.log(`[Settings.handleAddOrUpdateEnv][Action] ${editingEnvId ? 'Updating' : 'Adding'} environment.`);
if (editingEnvId) {
await updateEnvironment(editingEnvId, newEnv);
addToast('Environment updated', 'success');
@@ -65,41 +86,73 @@ None
}
resetEnvForm();
await loadSettings();
console.log("[Settings.handleAddOrUpdateEnv][Coherence:OK] Environment saved.");
} catch (error) {
console.error("[Settings.handleAddOrUpdateEnv][Coherence:Failed] Failed to save environment:", error);
addToast('Failed to save environment', 'error');
}
}
// [/DEF:handleAddOrUpdateEnv]
// [DEF:handleDeleteEnv:Function]
/**
* @purpose Deletes an environment.
* @param {string} id - The ID of the environment to delete.
*/
async function handleDeleteEnv(id) {
if (confirm('Are you sure you want to delete this environment?')) {
try {
console.log(`[Settings.handleDeleteEnv][Action] Deleting environment: ${id}`);
await deleteEnvironment(id);
addToast('Environment deleted', 'success');
await loadSettings();
console.log("[Settings.handleDeleteEnv][Coherence:OK] Environment deleted.");
} catch (error) {
console.error("[Settings.handleDeleteEnv][Coherence:Failed] Failed to delete environment:", error);
addToast('Failed to delete environment', 'error');
}
}
}
// [/DEF:handleDeleteEnv]
// [DEF:handleTestEnv:Function]
/**
* @purpose Tests the connection to an environment.
* @param {string} id - The ID of the environment to test.
*/
async function handleTestEnv(id) {
try {
console.log(`[Settings.handleTestEnv][Action] Testing environment: ${id}`);
const result = await testEnvironmentConnection(id);
if (result.status === 'success') {
addToast('Connection successful', 'success');
console.log("[Settings.handleTestEnv][Coherence:OK] Connection successful.");
} else {
addToast(`Connection failed: ${result.message}`, 'error');
console.log("[Settings.handleTestEnv][Coherence:Failed] Connection failed.");
}
} catch (error) {
console.error("[Settings.handleTestEnv][Coherence:Failed] Error testing connection:", error);
addToast('Failed to test connection', 'error');
}
}
// [/DEF:handleTestEnv]
// [DEF:editEnv:Function]
/**
* @purpose Sets the form to edit an existing environment.
* @param {Object} env - The environment object to edit.
*/
function editEnv(env) {
newEnv = { ...env };
editingEnvId = env.id;
}
// [/DEF:editEnv]
// [DEF:resetEnvForm:Function]
/**
* @purpose Resets the environment form.
*/
function resetEnvForm() {
newEnv = {
id: '',
@@ -111,10 +164,12 @@ None
};
editingEnvId = null;
}
// [/DEF:resetEnvForm]
onMount(loadSettings);
</script>
<!-- [SECTION: TEMPLATE] -->
<div class="container mx-auto p-4">
<h1 class="text-2xl font-bold mb-6">Settings</h1>
@@ -211,4 +266,6 @@ None
</div>
</section>
</div>
<!-- [/SECTION] -->
<!-- [/DEF:Settings] -->

View File

@@ -0,0 +1,11 @@
<script>
import { page } from '$app/stores';
</script>
<div class="container mx-auto p-4 text-center mt-20">
<h1 class="text-6xl font-bold text-gray-800 mb-4">{$page.status}</h1>
<p class="text-2xl text-gray-600 mb-8">{$page.error?.message || 'Page not found'}</p>
<a href="/" class="bg-blue-500 text-white px-6 py-3 rounded-lg hover:bg-blue-600 transition-colors">
Back to Dashboard
</a>
</div>

View File

@@ -0,0 +1,17 @@
<script>
import Navbar from '../components/Navbar.svelte';
import Footer from '../components/Footer.svelte';
import Toast from '../components/Toast.svelte';
</script>
<Toast />
<main class="bg-gray-50 min-h-screen flex flex-col">
<Navbar />
<div class="p-4 flex-grow">
<slot />
</div>
<Footer />
</main>

View File

@@ -0,0 +1,2 @@
export const ssr = false;
export const prerender = false;

View File

@@ -0,0 +1,71 @@
<script>
import { plugins as pluginsStore, selectedPlugin, selectedTask } from '../lib/stores.js';
import TaskRunner from '../components/TaskRunner.svelte';
import DynamicForm from '../components/DynamicForm.svelte';
import { api } from '../lib/api.js';
import { get } from 'svelte/store';
/** @type {import('./$types').PageData} */
export let data;
// Sync store with loaded data if needed, or just use data.plugins directly
$: if (data.plugins) {
pluginsStore.set(data.plugins);
}
function selectPlugin(plugin) {
console.log(`[Dashboard][Action] Selecting plugin: ${plugin.id}`);
selectedPlugin.set(plugin);
}
async function handleFormSubmit(event) {
console.log("[App.handleFormSubmit][Action] Handling form submission for task creation.");
const params = event.detail;
try {
const plugin = get(selectedPlugin);
const task = await api.createTask(plugin.id, params);
selectedTask.set(task);
selectedPlugin.set(null);
console.log(`[App.handleFormSubmit][Coherence:OK] Task created id=${task.id}`);
} catch (error) {
console.error(`[App.handleFormSubmit][Coherence:Failed] Task creation failed error=${error}`);
}
}
</script>
<div class="container mx-auto p-4">
{#if $selectedTask}
<TaskRunner />
<button on:click={() => selectedTask.set(null)} class="mt-4 bg-blue-500 text-white p-2 rounded">
Back to Task List
</button>
{:else if $selectedPlugin}
<h2 class="text-2xl font-bold mb-4">{$selectedPlugin.name}</h2>
<DynamicForm schema={$selectedPlugin.schema} on:submit={handleFormSubmit} />
<button on:click={() => selectedPlugin.set(null)} class="mt-4 bg-gray-500 text-white p-2 rounded">
Back to Dashboard
</button>
{:else}
<h1 class="text-2xl font-bold mb-4">Available Tools</h1>
{#if data.error}
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
{data.error}
</div>
{/if}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{#each data.plugins as plugin}
<div
class="border rounded-lg p-4 cursor-pointer hover:bg-gray-100"
on:click={() => selectPlugin(plugin)}
role="button"
tabindex="0"
on:keydown={(e) => e.key === 'Enter' && selectPlugin(plugin)}
>
<h2 class="text-xl font-semibold">{plugin.name}</h2>
<p class="text-gray-600">{plugin.description}</p>
<span class="text-sm text-gray-400">v{plugin.version}</span>
</div>
{/each}
</div>
{/if}
</div>

View File

@@ -0,0 +1,17 @@
import { api } from '../lib/api';
/** @type {import('./$types').PageLoad} */
export async function load() {
try {
const plugins = await api.getPlugins();
return {
plugins
};
} catch (error) {
console.error('Failed to load plugins:', error);
return {
plugins: [],
error: 'Failed to load plugins'
};
}
}

View File

@@ -0,0 +1,209 @@
<script>
import { onMount } from 'svelte';
import { updateGlobalSettings, addEnvironment, updateEnvironment, deleteEnvironment, testEnvironmentConnection } from '../../lib/api';
import { addToast } from '../../lib/toasts';
/** @type {import('./$types').PageData} */
export let data;
let settings = data.settings;
$: settings = data.settings;
let newEnv = {
id: '',
name: '',
url: '',
username: '',
password: '',
is_default: false
};
let editingEnvId = null;
async function handleSaveGlobal() {
try {
console.log("[Settings.handleSaveGlobal][Action] Saving global settings.");
await updateGlobalSettings(settings.settings);
addToast('Global settings saved', 'success');
console.log("[Settings.handleSaveGlobal][Coherence:OK] Global settings saved.");
} catch (error) {
console.error("[Settings.handleSaveGlobal][Coherence:Failed] Failed to save global settings:", error);
addToast('Failed to save global settings', 'error');
}
}
async function handleAddOrUpdateEnv() {
try {
console.log(`[Settings.handleAddOrUpdateEnv][Action] ${editingEnvId ? 'Updating' : 'Adding'} environment.`);
if (editingEnvId) {
await updateEnvironment(editingEnvId, newEnv);
addToast('Environment updated', 'success');
} else {
await addEnvironment(newEnv);
addToast('Environment added', 'success');
}
resetEnvForm();
// In a real app, we might want to invalidate the load function here
// For now, we'll just manually update the local state or re-fetch
// But since we are using SvelteKit, we should ideally use invalidateAll()
location.reload(); // Simple way to refresh data for now
console.log("[Settings.handleAddOrUpdateEnv][Coherence:OK] Environment saved.");
} catch (error) {
console.error("[Settings.handleAddOrUpdateEnv][Coherence:Failed] Failed to save environment:", error);
addToast('Failed to save environment', 'error');
}
}
async function handleDeleteEnv(id) {
if (confirm('Are you sure you want to delete this environment?')) {
try {
console.log(`[Settings.handleDeleteEnv][Action] Deleting environment: ${id}`);
await deleteEnvironment(id);
addToast('Environment deleted', 'success');
location.reload();
console.log("[Settings.handleDeleteEnv][Coherence:OK] Environment deleted.");
} catch (error) {
console.error("[Settings.handleDeleteEnv][Coherence:Failed] Failed to delete environment:", error);
addToast('Failed to delete environment', 'error');
}
}
}
async function handleTestEnv(id) {
try {
console.log(`[Settings.handleTestEnv][Action] Testing environment: ${id}`);
const result = await testEnvironmentConnection(id);
if (result.status === 'success') {
addToast('Connection successful', 'success');
console.log("[Settings.handleTestEnv][Coherence:OK] Connection successful.");
} else {
addToast(`Connection failed: ${result.message}`, 'error');
console.log("[Settings.handleTestEnv][Coherence:Failed] Connection failed.");
}
} catch (error) {
console.error("[Settings.handleTestEnv][Coherence:Failed] Error testing connection:", error);
addToast('Failed to test connection', 'error');
}
}
function editEnv(env) {
newEnv = { ...env };
editingEnvId = env.id;
}
function resetEnvForm() {
newEnv = {
id: '',
name: '',
url: '',
username: '',
password: '',
is_default: false
};
editingEnvId = null;
}
</script>
<div class="container mx-auto p-4">
<h1 class="text-2xl font-bold mb-6">Settings</h1>
{#if data.error}
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
{data.error}
</div>
{/if}
<section class="mb-8 bg-white p-6 rounded shadow">
<h2 class="text-xl font-semibold mb-4">Global Settings</h2>
<div class="grid grid-cols-1 gap-4">
<div>
<label for="backup_path" class="block text-sm font-medium text-gray-700">Backup Storage Path</label>
<input type="text" id="backup_path" bind:value={settings.settings.backup_path} class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
</div>
<button on:click={handleSaveGlobal} class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 w-max">
Save Global Settings
</button>
</div>
</section>
<section class="mb-8 bg-white p-6 rounded shadow">
<h2 class="text-xl font-semibold mb-4">Superset Environments</h2>
{#if settings.environments.length === 0}
<div class="mb-4 p-4 bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700">
<p class="font-bold">Warning</p>
<p>No Superset environments configured. You must add at least one environment to perform backups or migrations.</p>
</div>
{/if}
<div class="mb-6 overflow-x-auto">
<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">Name</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">URL</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Username</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Default</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">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">{env.is_default ? 'Yes' : 'No'}</td>
<td class="px-6 py-4 whitespace-nowrap">
<button on:click={() => handleTestEnv(env.id)} class="text-green-600 hover:text-green-900 mr-4">Test</button>
<button on:click={() => editEnv(env)} class="text-indigo-600 hover:text-indigo-900 mr-4">Edit</button>
<button on:click={() => handleDeleteEnv(env.id)} class="text-red-600 hover:text-red-900">Delete</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<div class="bg-gray-50 p-4 rounded">
<h3 class="text-lg font-medium mb-4">{editingEnvId ? 'Edit' : 'Add'} Environment</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">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" />
</div>
<div>
<label for="env_name" class="block text-sm font-medium text-gray-700">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">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">Username</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">Password</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">
<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">Default Environment</label>
</div>
</div>
<div class="mt-4 flex gap-2">
<button on:click={handleAddOrUpdateEnv} class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
{editingEnvId ? 'Update' : 'Add'} Environment
</button>
{#if editingEnvId}
<button on:click={resetEnvForm} class="bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600">
Cancel
</button>
{/if}
</div>
</div>
</section>
</div>

View File

@@ -0,0 +1,23 @@
import { api } from '../../lib/api';
/** @type {import('./$types').PageLoad} */
export async function load() {
try {
const settings = await api.getSettings();
return {
settings
};
} catch (error) {
console.error('Failed to load settings:', error);
return {
settings: {
environments: [],
settings: {
backup_path: '',
default_environment_id: null
}
},
error: 'Failed to load settings'
};
}
}