Files
Agentic-OS/frontend/app/users/page.tsx
nearxos 6185b9b85a Initial commit: Agentic OS troubleshooting platform
Self-hosted, Docker-based agentic troubleshooting platform: FastAPI backend +
LangGraph agent, Next.js UI, tiered LLM routing (local Ollama -> Gemini ->
DeepSeek -> OpenRouter), MCP server manager, encrypted device credentials,
RBAC, audit log, project-memory + Obsidian integrations, and editable
troubleshooting decision rules tuned for the GeneseasX vessel stack.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 22:11:07 +03:00

230 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState } from "react";
import useSWR from "swr";
import AppShell from "@/components/AppShell";
import {
ConfirmDialog,
EmptyState,
FormError,
LoadError,
Modal,
PageHeader,
SkeletonRows,
StatusBadge,
} from "@/components/ui";
import { useToast } from "@/components/toast";
import { api, fetcher } from "@/lib/api";
type User = {
id: number;
email: string;
full_name: string | null;
role: string;
is_active: boolean;
};
const ROLES = ["readonly", "support", "admin"];
export default function UsersPage() {
const { data: users, error: usersError, isLoading, mutate } = useSWR<User[]>(
"/api/users",
fetcher
);
const toast = useToast();
const [open, setOpen] = useState(false);
const [form, setForm] = useState({ email: "", password: "", full_name: "", role: "readonly" });
const [error, setError] = useState<string | null>(null);
const [creating, setCreating] = useState(false);
const [deleteId, setDeleteId] = useState<number | null>(null);
const [deleting, setDeleting] = useState(false);
const create = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setCreating(true);
try {
await api("/api/users", { method: "POST", body: JSON.stringify(form) });
setOpen(false);
setForm({ email: "", password: "", full_name: "", role: "readonly" });
toast.success("User created");
mutate();
} catch (err: any) {
setError(err.message);
toast.error(err.message || "Could not create user");
} finally {
setCreating(false);
}
};
const update = async (id: number, patch: Partial<User>) => {
try {
await api(`/api/users/${id}`, { method: "PATCH", body: JSON.stringify(patch) });
toast.success("User updated");
mutate();
} catch (err: any) {
toast.error(err.message || "Update failed");
}
};
const remove = async () => {
if (deleteId == null) return;
setDeleting(true);
try {
await api(`/api/users/${deleteId}`, { method: "DELETE" });
toast.success("User deleted");
mutate();
} catch (err: any) {
toast.error(err.message || "Delete failed");
} finally {
setDeleting(false);
setDeleteId(null);
}
};
return (
<AppShell>
<PageHeader
title="Users"
subtitle="Manage operators and their roles"
action={
<button className="btn-primary" onClick={() => setOpen(true)}>
+ New user
</button>
}
/>
{isLoading && !users ? (
<SkeletonRows rows={4} />
) : usersError ? (
<LoadError message="Couldnt load users." onRetry={() => mutate()} />
) : (users || []).length === 0 ? (
<EmptyState title="No users yet" description="Create the first operator account." />
) : (
<div className="card overflow-x-auto p-0">
<table className="w-full min-w-[640px]">
<thead>
<tr>
<th className="th">Email</th>
<th className="th">Name</th>
<th className="th">Role</th>
<th className="th">Active</th>
<th className="th"></th>
</tr>
</thead>
<tbody>
{(users || []).map((u) => (
<tr key={u.id}>
<td className="td">{u.email}</td>
<td className="td">{u.full_name || "—"}</td>
<td className="td">
<select
aria-label={`Role for ${u.email}`}
className="input max-w-[140px]"
value={u.role}
onChange={(e) => update(u.id, { role: e.target.value as any })}
>
{ROLES.map((r) => (
<option key={r} value={r}>
{r}
</option>
))}
</select>
</td>
<td className="td">
<button
aria-label={`Toggle active for ${u.email}`}
aria-pressed={u.is_active}
onClick={() => update(u.id, { is_active: !u.is_active })}
>
<StatusBadge status={u.is_active ? "loaded" : "stopped"} />
</button>
</td>
<td className="td text-right">
<button className="btn-danger btn-sm" onClick={() => setDeleteId(u.id)}>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
<Modal open={open} onClose={() => setOpen(false)} title="Create user">
<form onSubmit={create} className="space-y-4">
<div>
<label htmlFor="user-email" className="label">
Email
</label>
<input
id="user-email"
className="input"
type="email"
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
required
/>
</div>
<div>
<label htmlFor="user-name" className="label">
Full name
</label>
<input
id="user-name"
className="input"
value={form.full_name}
onChange={(e) => setForm({ ...form, full_name: e.target.value })}
/>
</div>
<div>
<label htmlFor="user-password" className="label">
Password
</label>
<input
id="user-password"
className="input"
type="password"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
required
/>
</div>
<div>
<label htmlFor="user-role" className="label">
Role
</label>
<select
id="user-role"
className="input"
value={form.role}
onChange={(e) => setForm({ ...form, role: e.target.value })}
>
{ROLES.map((r) => (
<option key={r} value={r}>
{r}
</option>
))}
</select>
</div>
<FormError message={error} />
<button className="btn-primary w-full" disabled={creating}>
{creating ? "Creating…" : "Create"}
</button>
</form>
</Modal>
<ConfirmDialog
open={deleteId != null}
title="Delete user?"
message="This permanently removes the user account."
confirmLabel="Delete"
destructive
busy={deleting}
onConfirm={remove}
onCancel={() => setDeleteId(null)}
/>
</AppShell>
);
}