Files
var-monorepo/apps/dispatch/app/_components/navbar/AdminPanel.tsx

430 lines
14 KiB
TypeScript

"use client";
import { PublicUser } from "@repo/db";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { cn } from "_helpers/cn";
import { getConnectedAircraftsAPI, kickAircraftAPI } from "_querys/aircrafts";
import { getConnectedDispatcherAPI, kickDispatcherAPI } from "_querys/dispatcher";
import { getLivekitRooms, kickLivekitParticipant } from "_querys/livekit";
import { editUserAPI } from "_querys/user";
import { ParticipantInfo } from "livekit-server-sdk";
import {
Eye,
LockKeyhole,
Plane,
RedoDot,
Shield,
Speaker,
User,
UserCheck,
Workflow,
} from "lucide-react";
import { ReactNode, useRef, useState } from "react";
import toast from "react-hot-toast";
const PenaltyDropdown = ({
onClick,
btnClassName,
showDatePicker,
btnTip,
Icon,
}: {
onClick: (data: { reason: string; until: Date | null }) => void;
showDatePicker?: boolean;
btnClassName?: string;
btnTip?: string;
Icon: ReactNode;
}) => {
const [reason, setReason] = useState("");
const [until, setUntil] = useState<string>("default");
return (
<details className="dropdown dropdown-left dropdown-center">
<summary className={cn("btn btn-xs btn-square btn-soft", btnClassName)}>{Icon}</summary>
<div className="dropdown-content flex gap-3 bg-base-100 rounded-box z-1 p-2 mr-3 shadow-sm">
<input
value={reason}
onChange={(e) => setReason(e.target.value)}
type="text"
className="input min-w-[250px]"
placeholder="Begründung"
/>
{showDatePicker && (
<select
className="select min-w-[150px] select-bordered"
value={until}
onChange={(e) => setUntil(e.target.value)}
>
<option value="default" disabled>
Unbegrenzt
</option>
<option value="1h">1 Stunde</option>
<option value="6h">6 Stunden</option>
<option value="12h">12 Stunden</option>
<option value="24h">24 Stunden</option>
<option value="72h">72 Stunden</option>
<option value="1w">1 Woche</option>
<option value="2w">2 Wochen</option>
<option value="1m">1 Monat</option>
<option value="3m">3 Monate</option>
<option value="6m">6 Monate</option>
<option value="1y">1 Jahr</option>
</select>
)}
<button
className={cn(
"btn btn-square btn-soft tooltip tooltip-bottom tooltip-warning",
btnClassName,
)}
data-tip={btnTip}
onClick={() => {
let untilDate: Date | null = null;
if (until !== "default") {
const now = new Date();
switch (until) {
case "1h":
untilDate = new Date(now.getTime() + 1 * 60 * 60 * 1000);
break;
case "6h":
untilDate = new Date(now.getTime() + 6 * 60 * 60 * 1000);
break;
case "12h":
untilDate = new Date(now.getTime() + 12 * 60 * 60 * 1000);
break;
case "24h":
untilDate = new Date(now.getTime() + 24 * 60 * 60 * 1000);
break;
case "72h":
untilDate = new Date(now.getTime() + 72 * 60 * 60 * 1000);
break;
case "1w":
untilDate = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
break;
case "2w":
untilDate = new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000);
break;
case "1m":
untilDate = new Date(now.setMonth(now.getMonth() + 1));
break;
case "3m":
untilDate = new Date(now.setMonth(now.getMonth() + 3));
break;
case "6m":
untilDate = new Date(now.setMonth(now.getMonth() + 6));
break;
case "1y":
untilDate = new Date(now.setFullYear(now.getFullYear() + 1));
break;
default:
untilDate = null;
}
}
onClick({ reason, until: untilDate });
}}
>
{Icon}
</button>
</div>
</details>
);
};
export default function AdminPanel() {
const queryClient = useQueryClient();
const { data: pilots } = useQuery({
queryKey: ["pilots"],
queryFn: () => getConnectedAircraftsAPI(),
refetchInterval: 10000,
});
const { data: dispatcher } = useQuery({
queryKey: ["dispatcher"],
queryFn: () => getConnectedDispatcherAPI(),
refetchInterval: 10000,
});
const { data: livekitRooms } = useQuery({
queryKey: ["livekit-rooms"],
queryFn: () => getLivekitRooms(),
refetchInterval: 10000,
});
const kickLivekitParticipantMutation = useMutation({
mutationFn: kickLivekitParticipant,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["livekit-rooms"] });
},
});
const kickPilotMutation = useMutation({
mutationFn: kickAircraftAPI,
onSuccess: () => {
toast.success("Pilot wurde erfolgreich gekickt");
queryClient.invalidateQueries({
queryKey: ["aircrafts"],
});
queryClient.invalidateQueries({
queryKey: ["connected-audio-users"],
});
},
});
const kickDispatchMutation = useMutation({
mutationFn: kickDispatcherAPI,
onSuccess: () => {
toast.success("Disponent wurde erfolgreich gekickt");
queryClient.invalidateQueries({
queryKey: ["dispatcher"],
});
queryClient.invalidateQueries({
queryKey: ["connected-audio-users"],
});
},
});
const participants: { participant: ParticipantInfo; room: string }[] = [];
if (livekitRooms) {
livekitRooms?.forEach((room) => {
room.participants.forEach((participant) => {
participants.push({
participant,
room: room.room.name,
});
});
});
}
const livekitUserNotConnected = participants.filter((p) => {
const pilot = pilots?.find(
(d) => (d.publicUser as unknown as PublicUser).publicId === p.participant.identity,
);
const fDispatcher = dispatcher?.find(
(d) => (d.publicUser as unknown as PublicUser).publicId === p.participant.identity,
);
return !pilot && !fDispatcher;
});
const modalRef = useRef<HTMLDialogElement>(null);
return (
<div>
<button
className="btn btn-soft btn-primary btn-sm flex items-center gap-2"
onSubmit={() => false}
onClick={() => {
modalRef.current?.showModal();
}}
>
<Shield size={18} /> Admin Panel
</button>
<dialog ref={modalRef} className="modal min-w-[500px]">
<div className="modal-box w-11/12 max-w-7xl">
<form method="dialog">
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
</form>
<h3 className="font-bold text-lg flex items-center gap-2">
<Shield size={22} /> Admin Panel
</h3>
<div className="flex gap-2 mt-4 w-full">
<div className="card bg-base-300 shadow-md w-full h-96 overflow-y-auto">
<div className="card-body">
<div className="card-title flex items-center gap-2">
<UserCheck size={20} /> Verbundene Clients
</div>
<table className="table w-full">
<thead>
<tr>
<th>VAR #</th>
<th>Name</th>
<th>Station</th>
<th>Voice</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{pilots?.map((p) => {
const publicUser = p.publicUser as unknown as PublicUser;
const livekitParticipant = participants.find(
(p) => p.participant.identity === publicUser.publicId,
);
return (
<tr key={p.id}>
<td className="flex items-center gap-2">
<Plane /> {publicUser.publicId}
</td>
<td>{publicUser.fullName}</td>
<td>{p.Station.bosCallsign}</td>
<td>
{!livekitParticipant ? (
<span className="text-error">Nicht verbunden</span>
) : (
<span className="text-success">{livekitParticipant.room}</span>
)}
</td>
<td className="flex gap-2">
<PenaltyDropdown
btnClassName="btn-warning"
btnTip="Kick"
Icon={<RedoDot size={15} />}
onClick={({ reason }) =>
kickPilotMutation.mutate({ id: p.id, reason })
}
/>
<PenaltyDropdown
btnClassName="btn-error tooltip-error"
btnTip="Kick + Berechtigungen entfernen"
showDatePicker
Icon={<LockKeyhole size={15} />}
onClick={({ reason, until }) =>
kickPilotMutation.mutate({ id: p.id, reason, bann: true, until })
}
/>
<a
href={`${process.env.NEXT_PUBLIC_HUB_URL}/admin/user/${p.userId}`}
target="_blank"
rel="noopener noreferrer"
>
<button
className="btn btn-xs btn-square btn-info btn-soft tooltip tooltip-bottom tooltip-info"
data-tip="Profil"
>
<User size={15} />
</button>
</a>
</td>
</tr>
);
})}
{dispatcher?.map((d) => {
const publicUser = d.publicUser as unknown as PublicUser;
const livekitParticipant = participants.find(
(p) => p.participant.identity === publicUser.publicId,
);
return (
<tr key={d.id}>
<td className="flex items-center gap-2">
<Workflow /> {publicUser.publicId}
</td>
<td>{publicUser.fullName}</td>
<td>{d.zone}</td>
<td>
{!livekitParticipant ? (
<span className="text-error">Nicht verbunden</span>
) : (
<span className="text-success">{livekitParticipant.room}</span>
)}
</td>
<td className="flex gap-2">
<PenaltyDropdown
btnClassName="btn-warning"
btnTip="Kick"
Icon={<RedoDot size={15} />}
onClick={({ reason }) =>
kickDispatchMutation.mutate({ id: d.id, reason })
}
/>
<PenaltyDropdown
btnClassName="btn-error tooltip-error"
btnTip="Kick + Berechtigungen entfernen"
showDatePicker
Icon={<LockKeyhole size={15} />}
onClick={({ reason, until }) =>
kickDispatchMutation.mutate({ id: d.id, reason, bann: true, until })
}
/>
<a
href={`${process.env.NEXT_PUBLIC_HUB_URL}/admin/user/${d.userId}`}
target="_blank"
rel="noopener noreferrer"
>
<button
className="btn btn-xs btn-square btn-info btn-soft tooltip tooltip-bottom tooltip-info"
data-tip="Profil"
>
<User size={15} />
</button>
</a>
</td>
</tr>
);
})}
{livekitUserNotConnected.map((p) => {
const publicUser = JSON.parse(
p.participant.attributes.publicUser || "{}",
) as PublicUser;
return (
<tr key={p.participant.identity}>
<td className="flex items-center gap-2">
<Speaker /> {p.participant.identity}
</td>
<td>{publicUser?.fullName}</td>
<td>
<span className="text-error">Nicht verbunden</span>
</td>
<td>
<span className="text-success">{p.room}</span>
</td>
<td className="flex gap-2">
<button
className="btn btn-xs btn-square btn-warning btn-soft tooltip tooltip-bottom tooltip-warning"
data-tip="Kick"
onClick={() =>
kickLivekitParticipantMutation.mutate({
roomName: p.room,
identity: p.participant.identity,
})
}
>
<RedoDot size={15} />
</button>
<button
className="btn btn-xs btn-square btn-error btn-soft tooltip tooltip-bottom tooltip-error"
data-tip="Kick + Berechtigungen entfernen"
>
<LockKeyhole size={15} />
</button>
<a
href={`${process.env.NEXT_PUBLIC_HUB_URL}/admin/user/${p.participant.attributes.userId}`}
target="_blank"
rel="noopener noreferrer"
>
<button
className="btn btn-xs btn-square btn-info btn-soft tooltip tooltip-bottom tooltip-info"
data-tip="Profil"
>
<User size={15} />
</button>
</a>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
{/* <div className="card bg-base-300 shadow-md w-full mt-4 max-h-48 overflow-y-auto">
<div className="card-body">
<div className="card-title flex items-center gap-2">
<ShieldAlert size={20} /> Allgemeine Befehle
</div>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center justify-left gap-2">
<button className="btn btn-soft btn-error mt-2">Kick Everybody</button>
<button className="btn btn-soft btn-warning mt-2">
Wartungsmodus einschalten
</button>
<button className="btn btn-soft btn-info mt-2">Delete All Missions</button>
</div>
<button className="btn btn-outline btn-info mt-2 flex items-center gap-2">
<Eye size={22} />
Als Observer verbinden
</button>
</div>
</div>
</div> */}
</div>
<form method="dialog" className="modal-backdrop">
<button>close</button>
</form>
</dialog>
</div>
);
}