Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
7ec678ae7d
|
@@ -27,6 +27,7 @@
|
||||
import Event from "@components/moderator/pages/event/Event.svelte";
|
||||
import Pages from "@components/moderator/pages/pages/Pages.svelte";
|
||||
import Generator from "@components/moderator/pages/generators/Generator.svelte";
|
||||
import AuditLog from "@components/moderator/pages/logs/AuditLog.svelte";
|
||||
import { Tooltip } from "bits-ui";
|
||||
|
||||
const routes: RouteDefinition = {
|
||||
@@ -36,6 +37,7 @@
|
||||
"/event/:id": Event,
|
||||
"/event/:id/generate": Generator,
|
||||
"/pages": Pages,
|
||||
"/logs": AuditLog,
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -27,4 +27,5 @@
|
||||
<a href="#/players" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/players"}> Players </a>
|
||||
<a href="#/pages" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/pages"}> Pages </a>
|
||||
<a href="#/schematics" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/schematics"}> Schematics </a>
|
||||
<a href="#/logs" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/logs"}> Logs </a>
|
||||
</nav>
|
||||
|
||||
235
src/components/moderator/pages/logs/AuditLog.svelte
Normal file
235
src/components/moderator/pages/logs/AuditLog.svelte
Normal file
@@ -0,0 +1,235 @@
|
||||
<script lang="ts">
|
||||
import { createSvelteTable, FlexRender } from "@components/ui/data-table";
|
||||
import { columns } from "./columns";
|
||||
import { getCoreRowModel, getPaginationRowModel, type PaginationState } from "@tanstack/table-core";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@components/ui/table";
|
||||
import { auditLog } from "@components/repo/auditlog";
|
||||
import { now, ZonedDateTime } from "@internationalized/date";
|
||||
import { AuditLogEntrySchema, type AuditLogEntry } from "@components/types/auditlog";
|
||||
import { Button } from "@components/ui/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
|
||||
import { Input } from "@components/ui/input";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
|
||||
import { players } from "@components/stores/stores";
|
||||
import { Check } from "lucide-svelte";
|
||||
import { cn } from "@components/utils";
|
||||
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
|
||||
|
||||
let debounceTimer: NodeJS.Timeout;
|
||||
const debounce = <T,>(value: T, func: (value: T) => void) => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => {
|
||||
func(value);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
let actionText = $state("");
|
||||
let serverText = $state("");
|
||||
let fullText = $state("");
|
||||
let actors = $state<string[]>([]);
|
||||
let actionTypes = $state<string[]>([]);
|
||||
let timeGreater = $state<ZonedDateTime>(now("Europe/Berlin").subtract({ months: 1 }));
|
||||
let timeLess = $state<ZonedDateTime>(now("Europe/Berlin"));
|
||||
let serverOwner = $state<string[]>([]);
|
||||
let velocity = $state(false);
|
||||
let sorting = $state("DESC");
|
||||
|
||||
let pagination = $state<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 25,
|
||||
});
|
||||
|
||||
let data = $state<AuditLogEntry[]>([]);
|
||||
let rows = $state(0);
|
||||
|
||||
$effect(() => {
|
||||
$auditLog
|
||||
.get(
|
||||
actionText || undefined,
|
||||
serverText || undefined,
|
||||
fullText || undefined,
|
||||
actors.length > 0 ? actors : undefined,
|
||||
actionTypes.length > 0 ? actionTypes : undefined,
|
||||
timeGreater ? timeGreater.toDate().getTime() : undefined,
|
||||
timeLess ? timeLess.toDate().getTime() : undefined,
|
||||
serverOwner.length > 0 ? serverOwner : undefined,
|
||||
velocity,
|
||||
pagination.pageIndex,
|
||||
pagination.pageSize,
|
||||
sorting || undefined
|
||||
)
|
||||
.then((res) => {
|
||||
data = res.entries;
|
||||
rows = res.rows;
|
||||
});
|
||||
});
|
||||
|
||||
const table = createSvelteTable({
|
||||
get data() {
|
||||
return data;
|
||||
},
|
||||
columns,
|
||||
state: {
|
||||
get pagination() {
|
||||
return pagination;
|
||||
},
|
||||
},
|
||||
onPaginationChange: (updater) => {
|
||||
if (typeof updater === "function") {
|
||||
pagination = updater(pagination);
|
||||
} else {
|
||||
pagination = updater;
|
||||
}
|
||||
},
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
manualPagination: true,
|
||||
get rowCount() {
|
||||
return rows;
|
||||
},
|
||||
});
|
||||
|
||||
let playerSearch = $state("");
|
||||
let ownerSearch = $state("");
|
||||
</script>
|
||||
|
||||
<div class="p-4">
|
||||
<div class="rounded border mb-4 p-2 flex lg:flex-row flex-col">
|
||||
<Input
|
||||
class="w-48 mr-2"
|
||||
placeholder="Suchen..."
|
||||
value={fullText}
|
||||
onchange={(e) =>
|
||||
debounce(e.currentTarget.value, (v) => {
|
||||
fullText = v;
|
||||
})}
|
||||
oninput={(e) =>
|
||||
debounce(e.currentTarget.value, (v) => {
|
||||
fullText = v;
|
||||
})}
|
||||
/>
|
||||
<Select type="multiple" value={actionTypes} onValueChange={(e) => (actionTypes = e)}>
|
||||
<SelectTrigger class="w-48 mr-2" placeholder="Aktionstypen auswählen...">Aktionstypen ({actionTypes.length})</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each ["JOIN", "LEAVE", "COMMAND", "SENSITIVE_COMMAND", "CHAT", "GUI_OPEN", "GUI_CLOSE", "GUI_CLICK"] as option}
|
||||
<SelectItem value={option}>{option}</SelectItem>
|
||||
{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Button variant="outline" class="mr-2 {actors && 'text-muted-foreground'}">Spieler Filter ({actors.length})</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="p-0">
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput bind:value={playerSearch} placeholder="Search players..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No Players found :(</CommandEmpty>
|
||||
<CommandGroup heading="Players">
|
||||
{#each $players.filter((v) => v.name.toLowerCase().includes(playerSearch.toLowerCase())).filter((v, i) => i < 50) as player (player.uuid)}
|
||||
<CommandItem
|
||||
value={player.name}
|
||||
onSelect={() => (actors = actors.includes(player.uuid) ? actors.filter((v) => v !== player.uuid) : [...actors, player.uuid])}
|
||||
keywords={[player.uuid]}
|
||||
>
|
||||
<Check class={cn("mr-2 size-4", !actors.includes(player.uuid) && "text-transparent")} />
|
||||
{player.name}
|
||||
</CommandItem>
|
||||
{/each}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Button variant="outline" class="mr-2 {serverOwner && 'text-muted-foreground'}">Server Owner ({serverOwner.length})</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="p-0">
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput bind:value={ownerSearch} placeholder="Search players..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No Players found :(</CommandEmpty>
|
||||
<CommandGroup heading="Players">
|
||||
{#each $players.filter((v) => v.name.toLowerCase().includes(ownerSearch.toLowerCase())).filter((v, i) => i < 50) as player (player.uuid)}
|
||||
<CommandItem
|
||||
value={player.name}
|
||||
onSelect={() => (serverOwner = serverOwner.includes(player.uuid) ? serverOwner.filter((v) => v !== player.uuid) : [...serverOwner, player.uuid])}
|
||||
keywords={[player.uuid]}
|
||||
>
|
||||
<Check class={cn("mr-2 size-4", !serverOwner.includes(player.uuid) && "text-transparent")} />
|
||||
{player.name}
|
||||
</CommandItem>
|
||||
{/each}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div class="mr-2">
|
||||
<DateTimePicker bind:value={timeGreater} />
|
||||
</div>
|
||||
<div class="mr-2">
|
||||
<DateTimePicker bind:value={timeLess} />
|
||||
</div>
|
||||
<Select type="single" value={sorting} onValueChange={(e) => (sorting = e)}>
|
||||
<SelectTrigger class="w-48 mr-2">{sorting === "ASC" ? "Aufsteigend" : "Absteigend"}</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ASC">Aufsteigend</SelectItem>
|
||||
<SelectItem value="DESC">Absteigend</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="rounded border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<TableRow>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
<TableHead colspan={header.colSpan}>
|
||||
{#if !header.isPlaceholder}
|
||||
<FlexRender content={header.column.columnDef.header} context={header.getContext()} />
|
||||
{/if}
|
||||
</TableHead>
|
||||
{/each}
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each table.getRowModel().rows as row (row.id)}
|
||||
<TableRow>
|
||||
{#each row.getVisibleCells() as cell (cell.id)}
|
||||
<TableCell class="p-2 align-top">
|
||||
<FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
|
||||
</TableCell>
|
||||
{/each}
|
||||
</TableRow>
|
||||
{:else}
|
||||
<TableRow>
|
||||
<TableCell colspan={columns.length} class="h-24 text-center">Keine Einträge gefunden.</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end space-x-2 py-4">
|
||||
<div>
|
||||
<Select type="single" value={pagination.pageSize.toString()} onValueChange={(e) => (pagination = { pageSize: +e, pageIndex: 0 })}>
|
||||
<SelectTrigger>{pagination.pageSize}</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="5">5</SelectItem>
|
||||
<SelectItem value="10">10</SelectItem>
|
||||
<SelectItem value="25">25</SelectItem>
|
||||
<SelectItem value="50">50</SelectItem>
|
||||
<SelectItem value="100">100</SelectItem>
|
||||
<SelectItem value="200">200</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onclick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>Previous</Button>
|
||||
<Button variant="outline" size="sm" onclick={() => table.nextPage()} disabled={!table.getCanNextPage()}>Next</Button>
|
||||
</div>
|
||||
</div>
|
||||
35
src/components/moderator/pages/logs/columns.ts
Normal file
35
src/components/moderator/pages/logs/columns.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { AuditLogEntry } from "@components/types/auditlog";
|
||||
import type { ColumnDef } from "@tanstack/table-core";
|
||||
|
||||
export const columns: ColumnDef<AuditLogEntry>[] = [
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "ID",
|
||||
},
|
||||
{
|
||||
accessorKey: "time",
|
||||
header: "Time",
|
||||
cell: (info) => new Date(info.getValue<number>()).toLocaleString(),
|
||||
},
|
||||
{
|
||||
accessorKey: "server",
|
||||
header: "Server",
|
||||
},
|
||||
{
|
||||
accessorKey: "serverOwner",
|
||||
header: "Server Owner",
|
||||
cell: (info) => info.getValue<string | null>() || "N/A",
|
||||
},
|
||||
{
|
||||
accessorKey: "actor",
|
||||
header: "Spieler",
|
||||
},
|
||||
{
|
||||
accessorKey: "actionType",
|
||||
header: "Action Type",
|
||||
},
|
||||
{
|
||||
accessorKey: "actionText",
|
||||
header: "Action Text",
|
||||
},
|
||||
];
|
||||
40
src/components/repo/auditlog.ts
Normal file
40
src/components/repo/auditlog.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { derived } from "svelte/store";
|
||||
import { fetchWithToken, tokenStore } from "./repo";
|
||||
import { PagedAutidLogSchema } from "@components/types/auditlog";
|
||||
|
||||
export class AuditLogRepo {
|
||||
async get(
|
||||
actionText: string | undefined,
|
||||
serverText: string | undefined,
|
||||
fullText: string | undefined,
|
||||
actor: string[] | undefined,
|
||||
actionType: string[] | undefined,
|
||||
timeFrom: number | undefined,
|
||||
timeTo: number | undefined,
|
||||
serverOwner: string[] | undefined,
|
||||
velocity: boolean | undefined,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
sorting: string | undefined
|
||||
) {
|
||||
const params = new URLSearchParams();
|
||||
if (actionText) params.append("actionText", actionText);
|
||||
if (serverText) params.append("serverText", serverText);
|
||||
if (fullText) params.append("fullText", fullText);
|
||||
if (actor) actor.forEach((a) => params.append("actor", a.toString()));
|
||||
if (actionType) actionType.forEach((a) => params.append("actionType", a));
|
||||
if (timeFrom) params.append("timeGreater", timeFrom.toString());
|
||||
if (timeTo) params.append("timeLess", timeTo.toString());
|
||||
if (serverOwner) serverOwner.forEach((s) => params.append("serverOwner", s.toString()));
|
||||
if (velocity !== undefined) params.append("velocity", velocity.toString());
|
||||
params.append("page", page.toString());
|
||||
params.append("limit", pageSize.toString());
|
||||
if (sorting) params.append("sorting", sorting);
|
||||
|
||||
return await fetchWithToken("", `/auditlog?${params.toString()}`)
|
||||
.then((value) => value.json())
|
||||
.then((data) => PagedAutidLogSchema.parse(data));
|
||||
}
|
||||
}
|
||||
|
||||
export const auditLog = derived(tokenStore, ($token) => new AuditLogRepo());
|
||||
19
src/components/types/auditlog.ts
Normal file
19
src/components/types/auditlog.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const AuditLogEntrySchema = z.object({
|
||||
id: z.number(),
|
||||
time: z.number(),
|
||||
server: z.string(),
|
||||
serverOwner: z.string().nullable(),
|
||||
actor: z.string(),
|
||||
actionType: z.enum(["JOIN", "LEAVE", "COMMAND", "SENSITIVE_COMMAND", "CHAT", "GUI_OPEN", "GUI_CLOSE", "GUI_CLICK"]),
|
||||
actionText: z.string(),
|
||||
});
|
||||
|
||||
export const PagedAutidLogSchema = z.object({
|
||||
entries: z.array(AuditLogEntrySchema),
|
||||
rows: z.number(),
|
||||
});
|
||||
|
||||
export type AuditLogEntry = z.infer<typeof AuditLogEntrySchema>;
|
||||
export type PagedAuditLog = z.infer<typeof PagedAutidLogSchema>;
|
||||
Reference in New Issue
Block a user