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.
138 lines
3.9 KiB
TypeScript
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>
|
|
);
|
|
}
|