generated from Dicken/dickendock
Fix: visible-Flag für Scan-Müll, editierbare Hostname/Port/HTTPS, Sidebar fixiert, Suche nur bei Eingabe
This commit is contained in:
@@ -31,6 +31,7 @@ export function HomePage() {
|
||||
const { data: services, isLoading, isError } = useServices();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const queryClient = useQueryClient();
|
||||
const isSearching = query.trim().length > 0;
|
||||
|
||||
const toggleFavorite = useMutation({
|
||||
mutationFn: toggleServiceFavorite,
|
||||
@@ -39,9 +40,16 @@ export function HomePage() {
|
||||
},
|
||||
});
|
||||
|
||||
// Ausgeblendete Dienste (z. B. Fehlerseiten/nicht erreichbare Scan-Treffer,
|
||||
// siehe Admin -> Dienste) tauchen in der Suche nicht auf.
|
||||
const visibleServices = useMemo(
|
||||
() => (services ?? []).filter((s) => s.visible),
|
||||
[services]
|
||||
);
|
||||
|
||||
const results = useMemo(
|
||||
() => rankServices(services ?? [], query),
|
||||
[services, query]
|
||||
() => rankServices(visibleServices, query),
|
||||
[visibleServices, query]
|
||||
);
|
||||
|
||||
// Auswahl zurücksetzen, sobald sich die Trefferliste ändert
|
||||
@@ -61,6 +69,7 @@ export function HomePage() {
|
||||
}
|
||||
|
||||
if (document.activeElement !== inputRef.current) return;
|
||||
if (query.trim().length === 0) return; // keine sichtbare Liste, nichts zu steuern
|
||||
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
@@ -80,7 +89,7 @@ export function HomePage() {
|
||||
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => window.removeEventListener("keydown", onKeyDown);
|
||||
}, [results, selectedIndex]);
|
||||
}, [results, selectedIndex, query]);
|
||||
|
||||
const isOnline = !healthError && health?.status === "ok";
|
||||
|
||||
@@ -124,7 +133,7 @@ export function HomePage() {
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
{isLoading ? (
|
||||
{!isSearching ? null : isLoading ? (
|
||||
<p className="mt-4 text-center text-sm text-black/40 dark:text-white/40">
|
||||
Lade Dienste …
|
||||
</p>
|
||||
@@ -140,7 +149,7 @@ export function HomePage() {
|
||||
onOpen={openService}
|
||||
onToggleFavorite={(service) => toggleFavorite.mutate(service)}
|
||||
emptyLabel={
|
||||
(services?.length ?? 0) === 0
|
||||
(visibleServices?.length ?? 0) === 0
|
||||
? "Noch keine Dienste angelegt. Füge welche im Adminbereich hinzu."
|
||||
: "Keine Treffer für deine Suche."
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export function AdminLayout() {
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-neutral-50 dark:bg-neutral-950 sm:flex">
|
||||
<div className="min-h-screen bg-neutral-50 dark:bg-neutral-950">
|
||||
{/* Mobile-Kopfleiste mit Hamburger-Button, nur unterhalb des sm-Breakpoints sichtbar */}
|
||||
<div
|
||||
className="flex items-center gap-3 border-b border-black/10 bg-white/80 px-4 py-3
|
||||
@@ -51,11 +51,16 @@ export function AdminLayout() {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/*
|
||||
Immer "fixed" (nicht nur mobil) – sonst scrollt die Sidebar bei langen
|
||||
Seiteninhalten mit dem restlichen Dokument mit und "Zurück zur Suche"
|
||||
verschwindet erst nach dem Runterscrollen aus dem Blickfeld.
|
||||
*/}
|
||||
<aside
|
||||
className={`fixed inset-y-0 left-0 z-40 flex w-64 flex-col border-r border-black/10
|
||||
bg-white transition-transform duration-200 ease-out dark:border-white/10
|
||||
dark:bg-neutral-950 sm:static sm:z-auto sm:w-56 sm:shrink-0 sm:translate-x-0
|
||||
sm:bg-white/70 sm:transition-none dark:sm:bg-white/5 ${
|
||||
dark:bg-neutral-950 sm:z-auto sm:w-56 sm:translate-x-0 sm:bg-white/70
|
||||
sm:transition-none dark:sm:bg-white/5 ${
|
||||
mobileOpen ? "translate-x-0" : "-translate-x-full"
|
||||
}`}
|
||||
>
|
||||
@@ -92,7 +97,7 @@ export function AdminLayout() {
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="border-t border-black/10 p-3 dark:border-white/10">
|
||||
<div className="shrink-0 border-t border-black/10 p-3 dark:border-white/10">
|
||||
<Link
|
||||
to="/"
|
||||
className="flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium
|
||||
@@ -105,7 +110,8 @@ export function AdminLayout() {
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="flex-1 overflow-x-hidden overflow-y-auto p-4 sm:p-8">
|
||||
{/* sm:ml-56 gleicht die jetzt fixierte Sidebar aus (statt Flexbox-Nebeneinander) */}
|
||||
<main className="overflow-x-hidden p-4 sm:ml-56 sm:p-8">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,11 @@ interface ServicePatch {
|
||||
alias?: string[];
|
||||
order?: number;
|
||||
favorite?: boolean;
|
||||
visible?: boolean;
|
||||
hostname?: string;
|
||||
port?: number;
|
||||
https?: boolean;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
async function patchService(id: string, patch: ServicePatch): Promise<Service> {
|
||||
@@ -38,6 +43,12 @@ function EditForm({ service, onDone }: { service: Service; onDone: () => void })
|
||||
const [category, setCategory] = useState(service.category ?? "");
|
||||
const [alias, setAlias] = useState(service.alias.join(", "));
|
||||
const [order, setOrder] = useState(String(service.order));
|
||||
const [hostname, setHostname] = useState(service.hostname);
|
||||
const [port, setPort] = useState(String(service.port));
|
||||
const [https, setHttps] = useState(service.https);
|
||||
|
||||
const portNumber = Number(port) || service.port;
|
||||
const previewUrl = `${https ? "https" : "http"}://${hostname || service.hostname}:${portNumber}`;
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
@@ -49,6 +60,10 @@ function EditForm({ service, onDone }: { service: Service; onDone: () => void })
|
||||
.map((a) => a.trim())
|
||||
.filter(Boolean),
|
||||
order: Number(order) || 0,
|
||||
hostname: hostname.trim(),
|
||||
port: portNumber,
|
||||
https,
|
||||
url: previewUrl,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["services"] });
|
||||
@@ -99,12 +114,55 @@ function EditForm({ service, onDone }: { service: Service; onDone: () => void })
|
||||
dark:border-white/10 dark:bg-white/10 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<Button size="sm" variant="primary" onClick={() => mutation.mutate()} disabled={mutation.isPending}>
|
||||
Speichern
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={onDone}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
|
||||
<div className="w-full border-t border-black/10 pt-3 dark:border-white/10" />
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-black/50 dark:text-white/50">
|
||||
Hostname / IP
|
||||
</label>
|
||||
<input
|
||||
value={hostname}
|
||||
onChange={(e) => setHostname(e.target.value)}
|
||||
className="rounded-lg border border-black/10 bg-white px-2 py-1 text-sm
|
||||
dark:border-white/10 dark:bg-white/10 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-black/50 dark:text-white/50">Port</label>
|
||||
<input
|
||||
value={port}
|
||||
onChange={(e) => setPort(e.target.value)}
|
||||
type="number"
|
||||
min={1}
|
||||
max={65535}
|
||||
className="w-24 rounded-lg border border-black/10 bg-white px-2 py-1 text-sm
|
||||
dark:border-white/10 dark:bg-white/10 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 flex items-center gap-1.5 text-xs text-black/50 dark:text-white/50">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={https}
|
||||
onChange={(e) => setHttps(e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
HTTPS
|
||||
</label>
|
||||
</div>
|
||||
<div className="text-xs text-black/40 dark:text-white/40">
|
||||
Öffnet: <span className="font-mono">{previewUrl}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full gap-2 pt-1">
|
||||
<Button size="sm" variant="primary" onClick={() => mutation.mutate()} disabled={mutation.isPending}>
|
||||
Speichern
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={onDone}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -120,6 +178,11 @@ function ServiceRow({ service }: { service: Service }) {
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["services"] }),
|
||||
});
|
||||
|
||||
const visibleMutation = useMutation({
|
||||
mutationFn: () => patchService(service.id, { visible: !service.visible }),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["services"] }),
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: () => deleteServiceRequest(service.id),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["services"] }),
|
||||
@@ -130,19 +193,42 @@ function ServiceRow({ service }: { service: Service }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className="border-b border-black/5 last:border-0 dark:border-white/5">
|
||||
<tr
|
||||
className={`border-b border-black/5 last:border-0 dark:border-white/5 ${
|
||||
service.visible ? "" : "opacity-50"
|
||||
}`}
|
||||
>
|
||||
<td className="px-4 py-3">
|
||||
<button
|
||||
onClick={() => favoriteMutation.mutate()}
|
||||
aria-label={service.favorite ? "Favorit entfernen" : "Als Favorit markieren"}
|
||||
className={`text-lg ${service.favorite ? "text-amber-500" : "text-black/15 hover:text-amber-400 dark:text-white/15"}`}
|
||||
>
|
||||
★
|
||||
</button>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<button
|
||||
onClick={() => favoriteMutation.mutate()}
|
||||
aria-label={service.favorite ? "Favorit entfernen" : "Als Favorit markieren"}
|
||||
className={`text-lg ${service.favorite ? "text-amber-500" : "text-black/15 hover:text-amber-400 dark:text-white/15"}`}
|
||||
>
|
||||
★
|
||||
</button>
|
||||
<button
|
||||
onClick={() => visibleMutation.mutate()}
|
||||
aria-label={service.visible ? "In der Suche ausblenden" : "In der Suche einblenden"}
|
||||
title={service.visible ? "In der Suche sichtbar" : "In der Suche ausgeblendet"}
|
||||
className={`text-base ${service.visible ? "text-black/40 dark:text-white/40" : "text-black/60 dark:text-white/60"}`}
|
||||
>
|
||||
{service.visible ? "👁️" : "🙈"}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="font-medium text-black dark:text-white">{service.displayName}</div>
|
||||
<div className="text-xs text-black/40 dark:text-white/40">{service.hostname}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-black dark:text-white">{service.displayName}</span>
|
||||
{!service.visible ? (
|
||||
<span className="rounded-full bg-black/10 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-black/50 dark:bg-white/10 dark:text-white/50">
|
||||
ausgeblendet
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="text-xs text-black/40 dark:text-white/40">
|
||||
{service.hostname}:{service.port}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-black/60 dark:text-white/60">{service.category ?? "–"}</td>
|
||||
<td className="px-4 py-3 text-black/60 dark:text-white/60">
|
||||
@@ -155,7 +241,7 @@ function ServiceRow({ service }: { service: Service }) {
|
||||
rel="noopener noreferrer"
|
||||
className="text-black/60 underline decoration-black/20 hover:text-black dark:text-white/60 dark:decoration-white/20 dark:hover:text-white"
|
||||
>
|
||||
öffnen
|
||||
öffnen{service.https ? " (https)" : ""}
|
||||
</a>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
@@ -174,12 +260,17 @@ function ServiceRow({ service }: { service: Service }) {
|
||||
|
||||
export function ServicesPage() {
|
||||
const { data: services, isLoading, isError } = useServices();
|
||||
const hiddenCount = services?.filter((s) => !s.visible).length ?? 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AdminPageHeader
|
||||
title="Dienste"
|
||||
description="Name, Kategorie, Alias und Reihenfolge bleiben bei erneuten Scans erhalten."
|
||||
description={
|
||||
hiddenCount > 0
|
||||
? `Name, Kategorie, Alias, IP/Port/Protokoll und Reihenfolge bleiben bei erneuten Scans erhalten. ${hiddenCount} Dienst(e) sind aktuell in der Suche ausgeblendet (🙈).`
|
||||
: "Name, Kategorie, Alias, IP/Port/Protokoll und Reihenfolge bleiben bei erneuten Scans erhalten."
|
||||
}
|
||||
/>
|
||||
|
||||
{isLoading ? (
|
||||
|
||||
Reference in New Issue
Block a user