"feat: Added debug server and related components for improved development experience"
This commit is contained in:
141
frontend/src/app/my/jobs/columns.tsx
Normal file
141
frontend/src/app/my/jobs/columns.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
"use client";
|
||||
|
||||
import type { InferResultType } from "@/utils/drizzle";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { BadgeCheckIcon, EyeIcon, HourglassIcon, MoreHorizontal, OctagonXIcon, ShareIcon } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import type { printers } from "@/server/db/schema";
|
||||
import type { InferSelectModel } from "drizzle-orm";
|
||||
import Link from "next/link";
|
||||
|
||||
export const columns: ColumnDef<
|
||||
InferResultType<
|
||||
"printJobs",
|
||||
{
|
||||
printer: true;
|
||||
}
|
||||
>
|
||||
>[] = [
|
||||
{
|
||||
accessorKey: "printer",
|
||||
header: "Drucker",
|
||||
cell: ({ row }) => {
|
||||
const printer: InferSelectModel<typeof printers> = row.getValue("printer");
|
||||
return printer.name;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "startAt",
|
||||
header: "Startzeitpunkt",
|
||||
cell: ({ row }) => {
|
||||
const startAt = new Date(row.original.startAt);
|
||||
|
||||
return `${startAt.toLocaleDateString("de-DE", {
|
||||
dateStyle: "medium",
|
||||
})} ${startAt.toLocaleTimeString("de-DE")}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "durationInMinutes",
|
||||
header: "Dauer (Minuten)",
|
||||
},
|
||||
{
|
||||
accessorKey: "comments",
|
||||
header: "Anmerkungen",
|
||||
cell: ({ row }) => {
|
||||
const comments = row.original.comments;
|
||||
|
||||
if (comments) {
|
||||
return <span className="text-sm">{comments.slice(0, 50)}</span>;
|
||||
}
|
||||
|
||||
return <span className="text-muted-foreground text-sm">Keine Anmerkungen</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: "Status",
|
||||
cell: ({ row }) => {
|
||||
const aborted = row.original.aborted;
|
||||
|
||||
if (aborted) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<OctagonXIcon className="w-4 h-4 text-red-500" /> <span className="text-red-600">Abgebrochen</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const startAt = new Date(row.original.startAt).getTime();
|
||||
const endAt = startAt + row.original.durationInMinutes * 60 * 1000;
|
||||
|
||||
if (Date.now() < endAt) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<HourglassIcon className="w-4 h-4 text-yellow-500" />
|
||||
<span className="text-yellow-600">Läuft...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<BadgeCheckIcon className="w-4 h-4 text-green-500" />
|
||||
<span className="text-green-600">Abgeschlossen</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const job = row.original;
|
||||
const { toast } = useToast();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Menu öffnen</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Aktionen</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
const baseUrl = new URL(window.location.href);
|
||||
baseUrl.pathname = `/job/${job.id}`;
|
||||
navigator.clipboard.writeText(baseUrl.toString());
|
||||
toast({
|
||||
description: "URL zum Druckauftrag in die Zwischenablage kopiert.",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ShareIcon className="w-4 h-4" />
|
||||
<span>Teilen</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<Link href={`/job/${job.id}`} className="flex items-center gap-2">
|
||||
<EyeIcon className="w-4 h-4" />
|
||||
<span>Details anzeigen</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
73
frontend/src/app/my/jobs/data-table.tsx
Normal file
73
frontend/src/app/my/jobs/data-table.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
type ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
}
|
||||
|
||||
export function JobsTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
Keine Ergebnisse gefunden
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex items-center justify-end space-x-2 py-4 select-none">
|
||||
<Button variant="outline" size="sm" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
|
||||
Vorherige Seite
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
|
||||
Nächste Seite
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
47
frontend/src/app/my/profile/page.tsx
Normal file
47
frontend/src/app/my/profile/page.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { validateRequest } from "@/server/auth";
|
||||
import { UserRole, translateUserRole } from "@/server/auth/permissions";
|
||||
import type { Metadata } from "next";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Dein Profil",
|
||||
};
|
||||
|
||||
export default async function ProfilePage() {
|
||||
const { user } = await validateRequest();
|
||||
|
||||
if (!user) {
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
const badgeVariant = {
|
||||
[UserRole.ADMIN]: "destructive" as const,
|
||||
[UserRole.USER]: "default" as const,
|
||||
[UserRole.GUEST]: "secondary" as const,
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row justify-between items-center">
|
||||
<div>
|
||||
<CardTitle>{user?.displayName}</CardTitle>
|
||||
<CardDescription>
|
||||
{user?.username} — {user?.email}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge variant={badgeVariant[user?.role]}>{translateUserRole(user?.role)}</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>
|
||||
Deine Daten wurden vom <abbr>GitHub Enterprise Server</abbr> importiert und können hier nur angezeigt werden.
|
||||
</p>
|
||||
<p>
|
||||
Solltest Du Änderungen oder eine Löschung deiner Daten von unserem Dienst beantragen wollen, so wende dich
|
||||
bitte an einen Administrator.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user