Dialog portalem do body, hlavicky odpovedi u chyby

Dialog se vykresloval uvnitr karty konektoru misto pres obrazovku. Samo
position: fixed nestaci: rodic s backdrop-filter (nase .glass, tedy skoro
kazdy panel a karta) je pro fixed potomka containing block. Modal proto jde
portalem do document.body. Tykalo se to vsech dialogu, videt to bylo az
u Logu, ktere jsou v male karte.

K chybe se zapisuji vybrane hlavicky odpovedi: server, via, content-type,
www-authenticate, retry-after, x-request-id, date. Rikaji, kdo odpoved vydal.
Server: Kestrel je sama aplikace, Via: 1.1 Caddy proxy pred ni. U 403 od proxy
byva telo prazdne a bez hlavicek by nezbylo vubec nic. Allowlist, ne vsechno:
Set-Cookie a podobne do zaznamu nepatri.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
JiriUhlir
2026-08-25 07:15:22 +02:00
co-authored by Claude Opus 5
parent 5a124a53d8
commit ab88979627
11 changed files with 143 additions and 9 deletions
+25 -3
View File
@@ -16,6 +16,7 @@ export function ErrorDetail({
detail,
status,
request,
responseHeaders,
retryable,
kind,
defaultOpen = false,
@@ -27,6 +28,11 @@ export function ErrorDetail({
status?: number;
/** `url` je cela adresa vcetne serveru. Bez ni nejde poznat, kam to slo. */
request?: { method: string; path: string; url?: string };
/**
* Vybrane hlavicky odpovedi. Rikaji, kdo odpoved vydal - aplikace, nebo
* proxy pred ni. U kodu bez tela je to jedina stopa, ktera zbyde.
*/
responseHeaders?: Record<string, string> | null;
retryable?: boolean;
kind?: string;
/**
@@ -39,6 +45,10 @@ export function ErrorDetail({
const [open, setOpen] = useState(defaultOpen);
const [copied, setCopied] = useState(false);
const headerLines = Object.entries(responseHeaders ?? {}).map(
([name, value]) => `${name}: ${value}`,
);
/** Co se zkopiruje: vsechno, co o chybe vime, at to jde poslat dal. */
const fullText = [
message,
@@ -48,9 +58,12 @@ export function ErrorDetail({
retryable !== undefined
? `Opakování: ${retryable ? 'má smysl zkusit znovu' : 'opakování nepomůže'}`
: null,
// Hlavicky rikaji, kdo odpoved vydal. Do schranky patri s tim ostatnim,
// protoze prave tohle se posila dal, kdyz to ma nekdo resit.
...(headerLines.length > 0 ? ['', 'Hlavičky odpovědi:', ...headerLines] : []),
detail ? `\nOdpověď služby:\n${detail}` : null,
]
.filter(Boolean)
.filter((line) => line !== null)
.join('\n');
async function copy() {
@@ -64,7 +77,8 @@ export function ErrorDetail({
}
}
const hasMore = Boolean(detail) || request !== undefined || status !== undefined;
const hasMore =
Boolean(detail) || headerLines.length > 0 || request !== undefined || status !== undefined;
return (
<div
@@ -92,7 +106,7 @@ export function ErrorDetail({
{hasMore && (
<div className="mt-2 flex flex-wrap items-center gap-3">
{detail && (
{(detail || headerLines.length > 0) && (
<button
type="button"
onClick={() => setOpen((previous) => !previous)}
@@ -113,6 +127,14 @@ export function ErrorDetail({
</div>
)}
{open && headerLines.length > 0 && (
<div className="mt-2 max-h-40 overflow-auto rounded bg-ink-950/70 p-2.5 font-mono text-xs break-words text-white/50">
{headerLines.map((line) => (
<div key={line}>{line}</div>
))}
</div>
)}
{open && detail && (
<pre className="mt-2 max-h-80 overflow-auto rounded bg-ink-950/70 p-2.5 font-mono text-xs whitespace-pre-wrap break-words text-white/70">
{detail}
+10 -2
View File
@@ -1,11 +1,18 @@
import { X } from 'lucide-react';
import { useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import type { ReactNode } from 'react';
import { cn } from '@/lib/cn';
/**
* Dialog nad obsahem. Zavira se Esc, klikem na pozadi i krizkem.
* Pri otevreni zamkne scroll stranky a preda focus dovnitr.
*
* **Vykresluje se portalem do `document.body`, ne na miste volani.** Samo
* `position: fixed` nestaci: rodic s `backdrop-filter` (nase `.glass`, tedy
* skoro kazdy panel a karta) je pro fixed potomka containing block a dialog
* se pak vejde do te karty misto pres celou obrazovku. Bez portalu je tedy
* dialog v karte konektoru maly a nepouzitelny.
*/
export function Modal({
open,
@@ -50,7 +57,7 @@ export function Modal({
if (!open) return null;
return (
return createPortal(
<div className="fixed inset-0 z-[60] flex items-start justify-center overflow-y-auto p-4 sm:items-center sm:p-6">
<div
className="fixed inset-0 bg-ink-950/80 backdrop-blur-sm"
@@ -85,6 +92,7 @@ export function Modal({
{children}
</div>
</div>
</div>,
document.body,
);
}
+3
View File
@@ -318,6 +318,7 @@ function ConnectorCard({
detail={test.detail}
status={test.status}
request={test.request}
responseHeaders={test.responseHeaders}
defaultOpen
/>
)}
@@ -399,6 +400,7 @@ function ConnectorLogs({
onClose={onClose}
title={`Logy: ${connector.name}`}
description="Posledních pět ověření. U neúspěchu je vidět celá odpověď služby."
className="max-w-4xl"
>
{targetUrl && (
<p className="border-b border-ink-600/60 px-5 py-3 font-mono text-xs break-all text-white/40">
@@ -442,6 +444,7 @@ function ConnectorLogs({
detail={check.detail ?? undefined}
status={check.status ?? undefined}
request={check.request ?? undefined}
responseHeaders={check.responseHeaders}
defaultOpen
/>
)}
+4
View File
@@ -395,6 +395,8 @@ export interface ConnectorCheck {
detail: string | null;
/** Cela adresa vcetne serveru, bez query. */
request: { method: string; path: string; url: string } | null;
/** Vybrane hlavicky odpovedi. Rikaji, kdo odpoved vydal. */
responseHeaders: Record<string, string> | null;
}
export interface ConnectorTestResult {
@@ -409,6 +411,8 @@ export interface ConnectorTestResult {
detail?: string;
/** Zaklad adresy, na kterou konektor miri. Vraci se i kdyz overeni projde. */
baseUrl?: string;
/** Vybrane hlavicky odpovedi. Rikaji, kdo odpoved vydal. */
responseHeaders?: Record<string, string>;
}
export interface ServiceCatalog {