Files
ScheduleTaskManager/web/src/pages/Dashboard.tsx
T
bobban 5269499520 Add manual scheduled task entry
Lets users log tasks the Linux agent can't discover on its own (e.g.
Docker-based backup jobs) directly in the UI. Tasks now carry an
origin ('agent' | 'manual') so the agent's report-sync logic only
ever creates/updates/stale-marks agent-sourced rows, leaving manual
entries untouched; the API rejects edits/deletes of agent-sourced
tasks to keep that boundary enforced server-side too.
2026-07-10 00:12:34 +02:00

138 lines
3.9 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState } from "react";
import { api, type ManualTaskInput, type ServerRecord, type TaskRecord } from "../api/client";
import FilterBar, { type Filters } from "../components/FilterBar";
import ServerGroup from "../components/ServerGroup";
import TaskForm from "../components/TaskForm";
export default function Dashboard() {
const [servers, setServers] = useState<ServerRecord[]>([]);
const [tasks, setTasks] = useState<TaskRecord[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [filters, setFilters] = useState<Filters>({
serverId: undefined,
scheduleType: undefined,
search: "",
includeStale: false,
});
const [formOpen, setFormOpen] = useState(false);
const [editingTask, setEditingTask] = useState<TaskRecord | null>(null);
const loadServers = useCallback(() => {
return api.servers
.list()
.then((res) => setServers(res.servers))
.catch((err) => setError(String(err)));
}, []);
const loadTasks = useCallback(() => {
return api.tasks
.list(filters)
.then((res) => {
setTasks(res.tasks);
setError(null);
})
.catch((err) => setError(String(err)));
}, [filters]);
useEffect(() => {
loadServers();
}, [loadServers]);
useEffect(() => {
setLoading(true);
const handle = setTimeout(() => {
loadTasks().finally(() => setLoading(false));
}, 200);
return () => clearTimeout(handle);
}, [loadTasks]);
const groups = useMemo(() => {
const byServer = new Map<number, { serverId: number; serverName: string; tasks: TaskRecord[] }>();
for (const task of tasks) {
const entry = byServer.get(task.serverId) ?? {
serverId: task.serverId,
serverName: task.serverName,
tasks: [],
};
entry.tasks.push(task);
byServer.set(task.serverId, entry);
}
return [...byServer.values()];
}, [tasks]);
function openAddForm() {
setEditingTask(null);
setFormOpen(true);
}
function openEditForm(task: TaskRecord) {
setEditingTask(task);
setFormOpen(true);
}
function closeForm() {
setFormOpen(false);
setEditingTask(null);
}
async function handleFormSubmit(data: ManualTaskInput) {
if (editingTask) {
await api.tasks.update(editingTask.id, data);
} else {
await api.tasks.create(data);
}
closeForm();
await loadTasks();
}
async function handleDelete(task: TaskRecord) {
if (!confirm(`Delete "${task.name}"?`)) return;
try {
await api.tasks.remove(task.id);
await loadTasks();
} catch (err) {
setError(String(err));
}
}
return (
<div className="dashboard">
<div className="dashboard-toolbar">
<FilterBar servers={servers} filters={filters} onChange={setFilters} />
<button className="btn btn-primary" onClick={openAddForm} disabled={servers.length === 0}>
Add task
</button>
</div>
{formOpen && (
<TaskForm
servers={servers}
task={editingTask}
defaultServerId={filters.serverId}
onSubmit={handleFormSubmit}
onCancel={closeForm}
/>
)}
{error && <div className="error-banner">{error}</div>}
{loading && <div className="loading-inline">Loading tasks</div>}
{!loading && groups.length === 0 && (
<div className="empty-state">
No scheduled tasks found yet. Install the agent on a server to auto-report cron/systemd tasks, or use
"Add task" above for things like Docker-based backup jobs.
</div>
)}
{groups.map((group) => (
<ServerGroup
key={group.serverId}
serverName={group.serverName}
tasks={group.tasks}
onEdit={openEditForm}
onDelete={handleDelete}
/>
))}
</div>
);
}