Add frontend
This commit is contained in:
81
frontend/src/routes/+layout.svelte
Normal file
81
frontend/src/routes/+layout.svelte
Normal file
@@ -0,0 +1,81 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import { onMount } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { checkHealth } from '$lib/api';
|
||||
let { children } = $props();
|
||||
|
||||
interface Notification {
|
||||
id: number;
|
||||
type: 'success' | 'error';
|
||||
message: string;
|
||||
}
|
||||
|
||||
let notifications: Notification[] = $state([]);
|
||||
let notificationId = 0;
|
||||
|
||||
let healthStatus = writable<'healthy' | 'unhealthy'>('unhealthy');
|
||||
|
||||
async function updateHealthStatus() {
|
||||
const status = await checkHealth();
|
||||
healthStatus.set(status);
|
||||
}
|
||||
|
||||
function showNotification(type: 'success' | 'error', message: string): void {
|
||||
const id = notificationId++;
|
||||
notifications = [...notifications, { id, type, message }];
|
||||
setTimeout(() => {
|
||||
notifications = notifications.filter((n) => n.id !== id);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.showNotification = showNotification;
|
||||
updateHealthStatus();
|
||||
setInterval(updateHealthStatus, 10000); // Check health every 10 seconds
|
||||
});
|
||||
</script>
|
||||
|
||||
<nav class="bg-gray-800 text-white shadow-md">
|
||||
<div class="container mx-auto flex justify-between items-center p-4">
|
||||
<a href="/" class="text-2xl font-bold hover:text-gray-400">Project Monitor</a>
|
||||
<div class="flex space-x-6">
|
||||
<a href="/" class="text-lg hover:text-gray-400">Home</a>
|
||||
<a href="/scripts" class="text-lg hover:text-gray-400">Scripts</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="relative">
|
||||
{@render children()}
|
||||
|
||||
<div class="fixed bottom-4 right-4 space-y-2">
|
||||
{#each notifications as notification (notification.id)}
|
||||
<div
|
||||
class="p-4 rounded shadow-lg text-white"
|
||||
class:bg-green-500={notification.type === 'success'}
|
||||
class:bg-red-500={notification.type === 'error'}
|
||||
>
|
||||
{notification.message}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fixed bottom-4 left-4 group">
|
||||
{#if $healthStatus === 'healthy'}
|
||||
<span class="text-green-500">✅</span>
|
||||
<div
|
||||
class="absolute bottom-full left-1/2 hidden group-hover:flex group-hover:w-max bg-green-500 text-white px-2 py-1 rounded shadow-lg"
|
||||
>
|
||||
Connected to backend
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-red-500">✘</span>
|
||||
<div
|
||||
class="absolute bottom-full left-1/2 hidden group-hover:flex group-hover:w-max bg-red-500 text-white px-2 py-1 rounded shadow-lg"
|
||||
>
|
||||
Not connected
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
12
frontend/src/routes/+page.svelte
Normal file
12
frontend/src/routes/+page.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<h1 class="text-4xl font-bold text-center mt-10 text-gray-800">Welcome to Project Monitor</h1>
|
||||
<p class="text-center mt-4 text-lg text-gray-600">
|
||||
Manage your scripts efficiently with our simple and intuitive interface.
|
||||
</p>
|
||||
<div class="flex justify-center mt-8">
|
||||
<a
|
||||
href="/scripts"
|
||||
class="px-6 py-3 bg-blue-500 text-white rounded-lg shadow-md hover:bg-blue-600"
|
||||
>
|
||||
Go to Scripts
|
||||
</a>
|
||||
</div>
|
||||
12
frontend/src/routes/scripts/+page.server.js
Normal file
12
frontend/src/routes/scripts/+page.server.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { fetchScripts } from '$lib/api';
|
||||
|
||||
/** @type {import('./$types').PageServerLoad} */
|
||||
export async function load() {
|
||||
try {
|
||||
const scripts = await fetchScripts();
|
||||
return { scripts };
|
||||
} catch (err) {
|
||||
throw error(500, 'Failed to fetch scripts - ' + err);
|
||||
}
|
||||
}
|
||||
68
frontend/src/routes/scripts/+page.svelte
Normal file
68
frontend/src/routes/scripts/+page.svelte
Normal file
@@ -0,0 +1,68 @@
|
||||
<script lang="ts">
|
||||
import { addScript } from '$lib/api';
|
||||
import type { Script } from '$lib/api';
|
||||
import CodeMirror from 'svelte-codemirror-editor';
|
||||
import { python } from '@codemirror/lang-python';
|
||||
|
||||
export let data: { scripts: Script[] };
|
||||
let scripts: Script[] = data.scripts;
|
||||
let newScript: Omit<Script, 'id' | 'created_at'> = { name: '', script_content: '' };
|
||||
|
||||
// Add a new script
|
||||
async function handleAddScript() {
|
||||
try {
|
||||
const addedScript = await addScript(newScript);
|
||||
scripts = [...scripts, addedScript];
|
||||
newScript = { name: '', script_content: '' };
|
||||
} catch (err) {
|
||||
window.showNotification('Failed to add script. ' + err);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="p-4">
|
||||
<h1 class="text-2xl font-bold mb-4">Scripts</h1>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{#each scripts as script (script.id)}
|
||||
<a
|
||||
href={`/scripts/${script.id}`}
|
||||
class="block p-4 border rounded shadow bg-white hover:bg-gray-100"
|
||||
>
|
||||
<h2 class="text-lg font-semibold text-gray-800">{script.name}</h2>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<h2 class="text-xl font-semibold mb-2">Add New Script</h2>
|
||||
<form
|
||||
on:submit|preventDefault={handleAddScript}
|
||||
class="space-y-4 p-4 border rounded shadow"
|
||||
on:submit={() => (newScript.script_content = editor.getValue())}
|
||||
>
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium">Name</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
bind:value={newScript.name}
|
||||
required
|
||||
class="mt-1 block w-full p-2 border rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="script_content" class="block text-sm font-medium">Content</label>
|
||||
<CodeMirror bind:value={newScript.script_content} lang={python()} />
|
||||
</div>
|
||||
<button type="submit" class="px-4 py-2 bg-green-500 text-white rounded"> Add Script </button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
22
frontend/src/routes/scripts/[id]/+page.server.ts
Normal file
22
frontend/src/routes/scripts/[id]/+page.server.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { fetchScriptById, fetchLogs } from '$lib/api';
|
||||
import type { Log } from '$lib/api';
|
||||
|
||||
export async function load({ params }) {
|
||||
const { id } = params;
|
||||
|
||||
try {
|
||||
const script = await fetchScriptById(parseInt(id));
|
||||
const logs: Log[] = (await fetchLogs(script.id)).sort(
|
||||
(a, b) => new Date(b.created_at!).getTime() - new Date(a.created_at!).getTime()
|
||||
);
|
||||
|
||||
if (!script) {
|
||||
throw error(404, 'Script not found');
|
||||
}
|
||||
|
||||
return { script, logs };
|
||||
} catch (err) {
|
||||
throw error(500, 'Failed to fetch script data - ' + err);
|
||||
}
|
||||
}
|
||||
154
frontend/src/routes/scripts/[id]/+page.svelte
Normal file
154
frontend/src/routes/scripts/[id]/+page.svelte
Normal file
@@ -0,0 +1,154 @@
|
||||
<script lang="ts">
|
||||
import { updateScript, deleteScript, addLog, deleteLog } from '$lib/api';
|
||||
import type { Script, Log } from '$lib/api';
|
||||
import CodeMirror from 'svelte-codemirror-editor';
|
||||
import { python } from '@codemirror/lang-python';
|
||||
|
||||
export let data: { script: Script; logs: Log[] };
|
||||
let script: Script = data.script;
|
||||
let logs: Log[] = data.logs;
|
||||
let updatedTitle: string = script.name || '';
|
||||
let updatedContent: string = script.script_content || '';
|
||||
let isEditMode: boolean = false;
|
||||
|
||||
let newLogMessage: string = '';
|
||||
// Notifications are now handled globally via the layout
|
||||
|
||||
async function handleUpdateScript() {
|
||||
if (script) {
|
||||
try {
|
||||
const updatedScript = await updateScript(script.id, {
|
||||
name: updatedTitle,
|
||||
script_content: updatedContent
|
||||
});
|
||||
script = updatedScript;
|
||||
window.showNotification('success', 'Script updated successfully!');
|
||||
isEditMode = false;
|
||||
} catch (err) {
|
||||
window.showNotification('error', 'Failed to update script. ' + err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddLog() {
|
||||
if (newLogMessage.trim()) {
|
||||
try {
|
||||
const newLog = await addLog(script.id, newLogMessage);
|
||||
logs = [newLog, ...logs];
|
||||
newLogMessage = '';
|
||||
window.showNotification('success', 'Log added successfully!');
|
||||
} catch (err) {
|
||||
window.showNotification('error', 'Failed to add log. ' + err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteLog(logId: number) {
|
||||
try {
|
||||
await deleteLog(script.id, logId);
|
||||
logs = logs.filter((log) => log.id !== logId);
|
||||
window.showNotification('success', 'Log deleted successfully!');
|
||||
} catch (err) {
|
||||
window.showNotification('error', 'Failed to delete log. ' + err);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the script
|
||||
async function handleDeleteScript() {
|
||||
if (script) {
|
||||
try {
|
||||
await deleteScript(script.id);
|
||||
window.location.href = '/scripts'; // Redirect to scripts list after deletion
|
||||
} catch (err) {
|
||||
window.showNotification('error', 'Failed to delete script. ' + err);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="p-4">
|
||||
<!-- Removed local notification container as notifications are now global -->
|
||||
|
||||
{#if script}
|
||||
{#if isEditMode}
|
||||
<input
|
||||
type="text"
|
||||
bind:value={updatedTitle}
|
||||
required
|
||||
class="text-2xl font-bold mb-4 w-full p-2 border rounded"
|
||||
/>
|
||||
<CodeMirror bind:value={updatedContent} lang={python()} />
|
||||
<button
|
||||
class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
on:click={handleUpdateScript}
|
||||
aria-label="Update the script title and content"
|
||||
>
|
||||
Update Script
|
||||
</button>
|
||||
<button
|
||||
class="mt-4 px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600"
|
||||
on:click={() => (isEditMode = false)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
{:else}
|
||||
<a href="/scripts" class="text-blue-500 hover:underline mb-4 inline-block"
|
||||
>← Return to Scripts</a
|
||||
>
|
||||
<h1 class="text-2xl font-bold mb-4">{script.name}</h1>
|
||||
<pre
|
||||
class="w-full p-2 border rounded font-mono text-sm bg-gray-100">{script.script_content}</pre>
|
||||
<button
|
||||
class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
on:click={() => (isEditMode = true)}
|
||||
>
|
||||
Edit Script
|
||||
</button>
|
||||
<button
|
||||
class="mt-4 px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
|
||||
on:click={handleDeleteScript}
|
||||
>
|
||||
Delete Script
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<section class="mt-8">
|
||||
<h2 class="text-xl font-bold mb-4">Logs</h2>
|
||||
<form on:submit|preventDefault={handleAddLog} class="mb-4">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={newLogMessage}
|
||||
placeholder="Enter new log message"
|
||||
class="w-full p-2 border rounded mb-2"
|
||||
required
|
||||
/>
|
||||
<button type="submit" class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600">
|
||||
Add Log
|
||||
</button>
|
||||
</form>
|
||||
<ul class="space-y-4">
|
||||
{#each logs as log (log.id)}
|
||||
<li class="p-4 border rounded bg-gray-50 flex justify-between items-center">
|
||||
<div>
|
||||
<p class="text-sm text-gray-700">{log.message}</p>
|
||||
<p class="text-xs text-gray-500">{log.created_at}</p>
|
||||
</div>
|
||||
<button
|
||||
class="px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600"
|
||||
on:click={() => handleDeleteLog(log.id)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user