WIP: Staged all changes

This commit is contained in:
2025-12-19 22:40:28 +03:00
parent 8f4b469c96
commit ce703322c2
64 changed files with 5985 additions and 833 deletions

40
frontend/src/App.svelte Normal file
View File

@@ -0,0 +1,40 @@
<script>
import Dashboard from './pages/Dashboard.svelte';
import { 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 Toast from './components/Toast.svelte';
async function handleFormSubmit(event) {
const params = event.detail;
const task = await api.createTask($selectedPlugin.id, params);
selectedTask.set(task);
selectedPlugin.set(null);
}
</script>
<Toast />
<main class="bg-gray-50 min-h-screen">
<header class="bg-white shadow-md p-4">
<h1 class="text-3xl font-bold text-gray-800">Superset Tools</h1>
</header>
<div class="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}
<Dashboard />
{/if}
</div>
</main>

3
frontend/src/app.css Normal file
View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,56 @@
<script>
import { createEventDispatcher } from 'svelte';
export let schema;
let formData = {};
const dispatch = createEventDispatcher();
function handleSubmit() {
dispatch('submit', formData);
}
// Initialize form data with default values from the schema
if (schema && schema.properties) {
for (const key in schema.properties) {
formData[key] = schema.properties[key].default || '';
}
}
</script>
<form on:submit|preventDefault={handleSubmit} class="space-y-4">
{#if schema && schema.properties}
{#each Object.entries(schema.properties) as [key, prop]}
<div class="flex flex-col">
<label for={key} class="mb-1 font-semibold text-gray-700">{prop.title || key}</label>
{#if prop.type === 'string'}
<input
type="text"
id={key}
bind:value={formData[key]}
placeholder={prop.description || ''}
class="p-2 border rounded-md"
/>
{:else if prop.type === 'number' || prop.type === 'integer'}
<input
type="number"
id={key}
bind:value={formData[key]}
placeholder={prop.description || ''}
class="p-2 border rounded-md"
/>
{:else if prop.type === 'boolean'}
<input
type="checkbox"
id={key}
bind:checked={formData[key]}
class="h-5 w-5"
/>
{/if}
</div>
{/each}
<button type="submit" class="w-full bg-green-500 text-white p-2 rounded-md hover:bg-green-600">
Run Task
</button>
{/if}
</form>

View File

@@ -0,0 +1,54 @@
<script>
import { onMount, onDestroy } from 'svelte';
import { selectedTask, taskLogs } from '../lib/stores.js';
let ws;
onMount(() => {
if ($selectedTask) {
taskLogs.set([]); // Clear previous logs
const wsUrl = `ws://localhost:8000/ws/logs/${$selectedTask.id}`;
ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log('WebSocket connection established');
};
ws.onmessage = (event) => {
const logEntry = JSON.parse(event.data);
taskLogs.update(logs => [...logs, logEntry]);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onclose = () => {
console.log('WebSocket connection closed');
};
}
});
onDestroy(() => {
if (ws) {
ws.close();
}
});
</script>
<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>
<div class="bg-gray-900 text-white font-mono text-sm p-4 rounded-md h-96 overflow-y-auto">
{#each $taskLogs as log}
<div>
<span class="text-gray-400">{new Date(log.timestamp).toLocaleTimeString()}</span>
<span class="{log.level === 'ERROR' ? 'text-red-500' : 'text-green-400'}">[{log.level}]</span>
<span>{log.message}</span>
</div>
{/each}
</div>
{:else}
<p>No task selected.</p>
{/if}
</div>

View File

@@ -0,0 +1,15 @@
<script>
import { toasts } from '../lib/toasts.js';
</script>
<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
{toast.type === 'info' && 'bg-blue-500'}
{toast.type === 'success' && 'bg-green-500'}
{toast.type === 'error' && 'bg-red-500'}
">
{toast.message}
</div>
{/each}
</div>

View File

@@ -0,0 +1,10 @@
<script>
let count = $state(0)
const increment = () => {
count += 1
}
</script>
<button onclick={increment}>
count is {count}
</button>

55
frontend/src/lib/api.js Normal file
View File

@@ -0,0 +1,55 @@
import { addToast } from './toasts.js';
const API_BASE_URL = 'http://localhost:8000';
/**
* Fetches data from the API.
* @param {string} endpoint The API endpoint to fetch data from.
* @returns {Promise<any>} The JSON response from the API.
*/
async function fetchApi(endpoint) {
try {
const response = await fetch(`${API_BASE_URL}${endpoint}`);
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(`Error fetching from ${endpoint}:`, error);
addToast(error.message, 'error');
throw error;
}
}
/**
* Posts data to the API.
* @param {string} endpoint The API endpoint to post data to.
* @param {object} body The data to post.
* @returns {Promise<any>} The JSON response from the API.
*/
async function postApi(endpoint, body) {
try {
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(`Error posting to ${endpoint}:`, error);
addToast(error.message, 'error');
throw error;
}
}
export const api = {
getPlugins: () => fetchApi('/plugins'),
getTasks: () => fetchApi('/tasks'),
getTask: (taskId) => fetchApi(`/tasks/${taskId}`),
createTask: (pluginId, params) => postApi('/tasks', { plugin_id: pluginId, params }),
};

View File

@@ -0,0 +1,40 @@
import { writable } from 'svelte/store';
import { api } from './api.js';
// Store for the list of available plugins
export const plugins = writable([]);
// Store for the list of tasks
export const tasks = writable([]);
// Store for the currently selected plugin
export const selectedPlugin = writable(null);
// Store for the currently selected task
export const selectedTask = writable(null);
// Store for the logs of the currently selected task
export const taskLogs = writable([]);
// Function to fetch plugins from the API
export async function fetchPlugins() {
try {
const data = await api.getPlugins();
console.log('Fetched plugins:', data); // Add console log
plugins.set(data);
} catch (error) {
console.error('Error fetching plugins:', error);
// Handle error appropriately in the UI
}
}
// Function to fetch tasks from the API
export async function fetchTasks() {
try {
const data = await api.getTasks();
tasks.set(data);
} catch (error) {
console.error('Error fetching tasks:', error);
// Handle error appropriately in the UI
}
}

View File

@@ -0,0 +1,13 @@
import { writable } from 'svelte/store';
export const toasts = writable([]);
export function addToast(message, type = 'info', duration = 3000) {
const id = Math.random().toString(36).substr(2, 9);
toasts.update(all => [...all, { id, message, type }]);
setTimeout(() => removeToast(id), duration);
}
function removeToast(id) {
toasts.update(all => all.filter(t => t.id !== id));
}

9
frontend/src/main.js Normal file
View File

@@ -0,0 +1,9 @@
import './app.css'
import App from './App.svelte'
const app = new App({
target: document.getElementById('app'),
props: {}
})
export default app

View File

@@ -0,0 +1,28 @@
<script>
import { onMount } from 'svelte';
import { plugins, fetchPlugins, selectedPlugin } from '../lib/stores.js';
onMount(async () => {
await fetchPlugins();
});
function selectPlugin(plugin) {
selectedPlugin.set(plugin);
}
</script>
<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">
{#each $plugins as plugin}
<div
class="border rounded-lg p-4 cursor-pointer hover:bg-gray-100"
on:click={() => 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>
</div>