184 lines
5.2 KiB
TypeScript
184 lines
5.2 KiB
TypeScript
import { userCache } from "@/cache";
|
|
import { client } from "@/client";
|
|
import { Avatar, AvatarImage } from "@/components/ui/avatar";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
import { useBreakpoint } from "@/hooks/useBreakpoint";
|
|
import { useCache } from "@/hooks/useCache";
|
|
import { useLoading } from "@/hooks/useLoading";
|
|
import { DEBOUNCE } from "@/snippets";
|
|
import { UserId } from "common";
|
|
import { Role } from "common/the_api";
|
|
import { Array, Effect, HashSet, Match, Option, pipe } from "effect";
|
|
import { Loader2 } from "lucide-react";
|
|
import { useRef, useState } from "react";
|
|
|
|
export function Admin() {
|
|
|
|
const [displayName, setDisplayName] = useState("");
|
|
const [role, setRole] = useState<Role | "">("");
|
|
|
|
const debounce = useRef(Effect.void);
|
|
const breakpoint = useBreakpoint();
|
|
const columns = breakpoint ? 4 : 1;
|
|
|
|
const { isLoading, error, data: userIds } = useLoading(Effect.gen(function* () {
|
|
yield* debounce.current;
|
|
const data = yield* client.queryUsers({
|
|
displayName: displayName !== "" ? Option.some(displayName) : Option.none(),
|
|
role: role !== "" ? Option.some(role) : Option.none(),
|
|
offset: 0,
|
|
limit: 100,
|
|
});
|
|
return data;
|
|
}), [displayName, role]);
|
|
|
|
return (
|
|
<div className="p-4 overflow-y-auto flex flex-col gap-4">
|
|
<div className="flex flex-row flex-wrap items-baseline gap-4">
|
|
<Input
|
|
className="w-full md:w-[32ch]"
|
|
type="text"
|
|
placeholder="Nazwa wyświetlana"
|
|
value={displayName}
|
|
onChange={(e) => {
|
|
setDisplayName(e.target.value);
|
|
debounce.current = DEBOUNCE;
|
|
}}
|
|
/>
|
|
<Select
|
|
value={role}
|
|
onValueChange={(role) => setRole(role === "None" ? "" : role as Role)}
|
|
>
|
|
<SelectTrigger className="w-full md:w-[24ch]">
|
|
<SelectValue placeholder="Rola" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectGroup>
|
|
<SelectLabel>Role</SelectLabel>
|
|
<SelectItem value="None"><em>Dowolna</em></SelectItem>
|
|
<SelectItem value={Role.Viewer}>Przeglądanie</SelectItem>
|
|
<SelectItem value={Role.Editor}>Edytor</SelectItem>
|
|
<SelectItem value={Role.Admin}>Administrator</SelectItem>
|
|
</SelectGroup>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<Table>
|
|
{breakpoint && (
|
|
<TableHeader className="bg-white sticky top-0">
|
|
<TableRow>
|
|
<TableHead className="w-10" />
|
|
<TableHead>Nazwa wyświetlana</TableHead>
|
|
<TableHead>ID</TableHead>
|
|
<TableHead>Role</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
)}
|
|
<TableBody>
|
|
{isLoading ? (
|
|
<TableRow>
|
|
<TableCell colSpan={columns} >
|
|
<div className="flex items-center justify-center gap-2">
|
|
<Loader2 className="animate-spin" />
|
|
Ładowanie…
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
) : error !== null ? (
|
|
<TableRow>
|
|
<TableCell colSpan={columns} >
|
|
<div className="flex items-center justify-center gap-2">
|
|
Wystąpił błąd: {Match.value(error).pipe(
|
|
Match.tag("FetchError", () => "Nie można połączyć się z serwerem"),
|
|
Match.tag("Unauthenticated", () => "Zaloguj się, aby kontynuować"),
|
|
Match.exhaustive,
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
userIds.map((userId) => <UserRow key={userId} userId={userId} />)
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
namespace UserRow {
|
|
export interface Props {
|
|
readonly userId: UserId;
|
|
}
|
|
}
|
|
|
|
function UserRow(props: UserRow.Props) {
|
|
|
|
const { isLoading, error, data: user } = useCache(userCache, props.userId);
|
|
|
|
const breakpoint = useBreakpoint();
|
|
const columns = breakpoint ? 4 : 1;
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<TableRow>
|
|
<TableCell colSpan={columns}>Ładowanie…</TableCell>
|
|
</TableRow>
|
|
);
|
|
}
|
|
|
|
if (error !== null) {
|
|
return (
|
|
<TableRow>
|
|
<TableCell colSpan={columns}>
|
|
Wystąpił błąd: {Match.value(error).pipe(
|
|
Match.tag("FetchError", () => "Nie można połączyć się z serwerem"),
|
|
Match.tag("NotFound", () => "Użytkownik nie istnieje"),
|
|
Match.tag("Unauthenticated", () => "Zaloguj się, aby kontynuować"),
|
|
Match.exhaustive,
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
}
|
|
|
|
const rolesText = pipe(
|
|
user.roles,
|
|
HashSet.values,
|
|
Array.join(", "),
|
|
);
|
|
|
|
return (
|
|
<TableRow>
|
|
{breakpoint ? (<>
|
|
<TableCell>
|
|
<Avatar className="shadow">
|
|
<AvatarImage src={Option.getOrUndefined(user.avatarUrl)} />
|
|
</Avatar>
|
|
</TableCell>
|
|
<TableCell>
|
|
{Option.getOrUndefined(user.displayName)}
|
|
</TableCell>
|
|
<TableCell className="font-mono">
|
|
{user.userId}
|
|
</TableCell>
|
|
<TableCell>
|
|
{rolesText}
|
|
</TableCell>
|
|
</>) : (
|
|
<TableCell className="flex flex-col">
|
|
<div className="flex flex-row items-center gap-2 mb-2">
|
|
<Avatar className="shadow">
|
|
<AvatarImage src={Option.getOrUndefined(user.avatarUrl)} />
|
|
</Avatar>
|
|
<div>{Option.getOrUndefined(user.displayName)}</div>
|
|
</div>
|
|
<div className="text-xs font-mono">{user.userId}</div>
|
|
<div className="text-xs">{rolesText}</div>
|
|
</TableCell>
|
|
)}
|
|
</TableRow>
|
|
);
|
|
}
|