Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 163 additions & 0 deletions app/(admin)/admin/processing/events/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
"use client";

import { Filter, ScrollText } from "lucide-react";
import Link from "next/link";
import { useMemo, useState } from "react";

import PrettyDate from "@/components/General/PrettyDate";
import PrettyHeader from "@/components/General/PrettyHeader";
import { SmallUserElement } from "@/components/SmallUserElement";
import Spinner from "@/components/Spinner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { MultiSelect } from "@/components/ui/multi-select";
import { useScoreProcessingEvents } from "@/lib/hooks/api/score-processing/useScoreProcessingEvents";
import { ScoreProcessingEventType } from "@/lib/types/api";
import { isInstance } from "@/lib/utils/type.util";

const PAGE_SIZE = 25;

export default function Page() {
const [page, setPage] = useState(1);
const [showFilters, setShowFilters] = useState(false);
const [eventTypes, setEventTypes] = useState<ScoreProcessingEventType[]>([]);

const { data, error, isLoading } = useScoreProcessingEvents(
useMemo(() => ({ types: eventTypes, page, limit: PAGE_SIZE }), [eventTypes, page]),
{ refreshInterval: 0, revalidateOnFocus: false, keepPreviousData: true },
);

const events = data?.events ?? [];
const totalCount = data?.total_count ?? 0;
const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));

return (
<div className="flex w-full flex-col space-y-4">
<PrettyHeader text="Processing events" roundBottom icon={<ScrollText />} />

<div className="flex items-center justify-between gap-2">
<p className="text-sm text-muted-foreground">
{totalCount}
{" "}
event(s)
</p>
<Button type="button" variant="outline" className="relative" onClick={() => setShowFilters(!showFilters)}>
<Filter className="mr-2 size-4" />
Filters
{eventTypes.length > 0 && (
<div className="absolute -right-2 -top-2 flex size-5 items-center justify-center rounded-full bg-primary text-xs text-primary-foreground">
{eventTypes.length}
</div>
)}
</Button>
</div>

<div
className="overflow-hidden transition-all duration-500 ease-in-out"
style={{ maxHeight: showFilters ? "300px" : "0" }}
>
<Card>
<CardContent className="space-y-2 p-4">
<label className="text-sm font-medium">Event type</label>
<MultiSelect
options={Object.values(ScoreProcessingEventType).map(option => ({
value: option,
label: option,
}))}
defaultValue={eventTypes}
placeholder="All event types"
onValueChange={(values) => {
setEventTypes(values.filter(isScoreProcessingEventType));
setPage(1);
}}
/>
</CardContent>
</Card>
</div>

<div className="space-y-2">
{error
? (
<Card className="p-8">
<CardContent className="flex flex-col items-center justify-center gap-2 p-0 text-center text-muted-foreground">
<ScrollText className="size-12 opacity-50" />
<p className="font-medium text-foreground">Could not load processing events.</p>
<p className="text-sm">{error.message ?? "Refresh the page and try again."}</p>
</CardContent>
</Card>
)
: isLoading && events.length === 0
? (
<Card className="p-8">
<CardContent className="flex items-center justify-center p-0">
<Spinner />
</CardContent>
</Card>
)
: events.length > 0
? (
events.map(event => (
<Card key={event.id}>
<CardContent className="flex flex-col gap-2 p-4 md:flex-row md:items-center md:justify-between">
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<Badge variant="outline">{event.event_type}</Badge>
{event.executor
? (
<SmallUserElement avatarUrl={event.executor.avatar_url} username={event.executor.username} profileUrl={`/admin/users/${event.executor.user_id}/edit`} />
)
: <Badge className="bg-muted text-muted-foreground">Server</Badge>}
{event.score_id != null && (
<Link
href={`/admin/scores/${event.score_id}`}
className="text-sm text-muted-foreground hover:underline"
>
{`Score #${event.score_id}`}
</Link>
)}
{event.task_id != null && (
<span className="text-sm text-muted-foreground">
{`Task #${event.task_id}`}
</span>
)}
</div>
{event.json_data && (
<pre className="mt-2 overflow-x-auto whitespace-pre-wrap break-all rounded bg-muted/40 p-2 text-xs text-muted-foreground">
{event.json_data}
</pre>
)}
</div>
<PrettyDate className="text-sm text-muted-foreground" time={event.created_at} />
</CardContent>
</Card>
))
)
: (
<Card className="p-8">
<CardContent className="flex flex-col items-center justify-center p-0 text-muted-foreground">
<ScrollText className="mb-4 size-12 opacity-50" />
<p>No processing events found.</p>
</CardContent>
</Card>
)}
</div>

<div className="flex items-center justify-between">
<Button variant="outline" disabled={page <= 1} onClick={() => setPage(page - 1)}>
Previous
</Button>
<span className="text-sm text-muted-foreground">
{`Page ${page} of ${totalPages}`}
</span>
<Button variant="outline" disabled={page >= totalPages} onClick={() => setPage(page + 1)}>
Next
</Button>
</div>
</div>
);
}

function isScoreProcessingEventType(value: string): value is ScoreProcessingEventType {
return isInstance(value, ScoreProcessingEventType);
}
Loading
Loading