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.
This commit is contained in:
+84
-16
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { api, type ServerRecord, type TaskRecord } from "../api/client";
|
||||
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[]>([]);
|
||||
@@ -14,28 +15,37 @@ export default function Dashboard() {
|
||||
search: "",
|
||||
includeStale: false,
|
||||
});
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
const [editingTask, setEditingTask] = useState<TaskRecord | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
api.servers
|
||||
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(() => {
|
||||
api.tasks
|
||||
.list(filters)
|
||||
.then((res) => {
|
||||
setTasks(res.tasks);
|
||||
setError(null);
|
||||
})
|
||||
.catch((err) => setError(String(err)))
|
||||
.finally(() => setLoading(false));
|
||||
loadTasks().finally(() => setLoading(false));
|
||||
}, 200);
|
||||
return () => clearTimeout(handle);
|
||||
}, [filters]);
|
||||
}, [loadTasks]);
|
||||
|
||||
const groups = useMemo(() => {
|
||||
const byServer = new Map<number, { serverId: number; serverName: string; tasks: TaskRecord[] }>();
|
||||
@@ -51,18 +61,76 @@ export default function Dashboard() {
|
||||
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">
|
||||
<FilterBar servers={servers} filters={filters} onChange={setFilters} />
|
||||
<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. Add a server under "Servers" and install the agent to start reporting.
|
||||
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} />
|
||||
<ServerGroup
|
||||
key={group.serverId}
|
||||
serverName={group.serverName}
|
||||
tasks={group.tasks}
|
||||
onEdit={openEditForm}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user