Cele chybove hlaseni u konektoru i skriptu
U odpovedi 401 nebo 400 je duvod napsany v tele odpovedi sluzby, ne v tom, ze prislo 401. Dosud se telo zkracovalo na 400 znaku a u overeni konektoru se zahazovalo cele - zbyla veta "Pristup zamitnut", podle ktere se neda hledat. - ScriptError nese `request` (metoda a cesta) a `detail` s celou odpovedi sluzby, zkracenou az na SCRIPT_ERROR_DETAIL_BYTES (vychozi 8 kB). Chyby jsou vzacne, takze objem neroste jako u logu uspesnych kroku - do detailu jde surove telo, ne prochazene pres JSON.stringify. U chyby chceme presne to, co sluzba poslala, vcetne HTML nebo prosteho textu - u chyby spojeni se pridava i `cause`, u neocekavane vyjimky zasobnik volani (mimo produkci, stejne jako u centralniho error handleru) - overeni konektoru vraci `detail`, `status` i `request` - cely detail jde i do logu serveru, at je to dohledatelne bez portalu - do chyby se dava jen cesta, ne cela adresa: v query muze byt tajemstvi - nova komponenta ErrorDetail: rozbaleni cele odpovedi a tlacitko Kopirovat vse Overeno: npm run typecheck prochazi na serveru i webu. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ad56c7f513
commit
3279dd7dac
@@ -94,6 +94,14 @@ export const config = {
|
||||
scriptTimeoutMs: positiveNumber(process.env.SCRIPT_TIMEOUT_MS, 15_000),
|
||||
/** Vetsi odpoved cizi sluzby se zahodi, misto aby snedla pamet procesu. */
|
||||
scriptMaxResponseBytes: positiveNumber(process.env.SCRIPT_MAX_RESPONSE_BYTES, 1_000_000),
|
||||
/**
|
||||
* Strop na text chybove odpovedi cizi sluzby.
|
||||
*
|
||||
* Zamerne velky. Odpoved na 401 nebo 400 obsahuje duvod a bez nej se chyba
|
||||
* nedá dohledat. Chyby jsou navic vzacne, takze objem neroste jako u logu
|
||||
* uspesnych kroku.
|
||||
*/
|
||||
errorDetailBytes: positiveNumber(process.env.SCRIPT_ERROR_DETAIL_BYTES, 8_000),
|
||||
/**
|
||||
* Strop na jednu strukturu (parametr typu object nebo list).
|
||||
* Radek s vystupem kroku je nejrychleji rostouci tabulka v systemu, takze
|
||||
|
||||
@@ -37,10 +37,11 @@ import {
|
||||
visibleServices,
|
||||
type Service,
|
||||
} from '../data/services.js';
|
||||
import { config } from '../config.js';
|
||||
import { resolveTarget, serviceBaseUrl } from '../scripts/connections.js';
|
||||
import { createHttp } from '../scripts/http.js';
|
||||
import { ScriptError } from '../scripts/types.js';
|
||||
import { createRedactor, describe } from '../scripts/util.js';
|
||||
import { createRedactor, describe, truncate } from '../scripts/util.js';
|
||||
|
||||
export const connectorsRouter = Router();
|
||||
|
||||
@@ -298,18 +299,36 @@ connectorsRouter.post('/:id/test', async (req, res) => {
|
||||
ok: true,
|
||||
checked,
|
||||
status: response.status,
|
||||
request: { method: 'GET', path },
|
||||
message: service.verifyPath
|
||||
? 'Napojení funguje, přístupové údaje jsou platné.'
|
||||
: 'Služba odpovídá. Přístupové údaje se tímhle neověřily, služba na to nemá čtecí volání.',
|
||||
});
|
||||
} catch (err) {
|
||||
const message =
|
||||
err instanceof ScriptError ? redact(err.message) : redact(describe(err, 200));
|
||||
// Cela odpoved sluzby, ne jen "HTTP 401". Duvod je napsany prave v ni.
|
||||
const isScriptError = err instanceof ScriptError;
|
||||
const message = isScriptError ? redact(err.message) : redact(describe(err, 400));
|
||||
const detail = isScriptError
|
||||
? err.detail
|
||||
? redact(err.detail)
|
||||
: undefined
|
||||
: redact(truncate(String(err instanceof Error ? err.stack ?? err.message : err), config.errorDetailBytes));
|
||||
|
||||
setConnectorStatus(connector.id, 'error', message, [tenantId]);
|
||||
console.warn(`[connectors] test ${connector.id} selhal: ${message}`);
|
||||
console.warn(
|
||||
`[connectors] test ${connector.id} selhal: ${message}` + (detail ? `
|
||||
${detail}` : ''),
|
||||
);
|
||||
|
||||
// Neuspesne overeni neni chyba API, je to vysledek. Proto 200.
|
||||
return res.json({ ok: false, checked, message });
|
||||
return res.json({
|
||||
ok: false,
|
||||
checked,
|
||||
message,
|
||||
...(isScriptError && err.status !== undefined ? { status: err.status } : {}),
|
||||
request: isScriptError && err.request ? err.request : { method: 'GET', path },
|
||||
...(detail ? { detail } : {}),
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
|
||||
+60
-21
@@ -10,8 +10,14 @@
|
||||
|
||||
import { config } from '../config.js';
|
||||
import type { ResolvedTarget } from './connections.js';
|
||||
import { ScriptError, type ScriptHttp, type ScriptHttpOptions, type ScriptHttpResponse } from './types.js';
|
||||
import { describe } from './util.js';
|
||||
import {
|
||||
ScriptError,
|
||||
type ScriptHttp,
|
||||
type ScriptHttpOptions,
|
||||
type ScriptHttpResponse,
|
||||
type ScriptRequestInfo,
|
||||
} from './types.js';
|
||||
import { truncate } from './util.js';
|
||||
|
||||
/** Kody, u kterych ma smysl opakovat. Zbytek je koncova chyba. */
|
||||
const retryableStatuses = new Set([408, 425, 429, 500, 502, 503, 504]);
|
||||
@@ -80,41 +86,69 @@ function buildUrl(target: ResolvedTarget, path: string, options?: ScriptHttpOpti
|
||||
return url;
|
||||
}
|
||||
|
||||
function statusError(status: number, url: URL, detail: string): ScriptError {
|
||||
const where = `${url.pathname} vrátilo HTTP ${status}`;
|
||||
/**
|
||||
* Chyba z HTTP kodu.
|
||||
*
|
||||
* `detail` je **cele telo odpovedi**, jen zredigovane a zkracene az na velkem
|
||||
* stropu. Prave tam cizi sluzba pise, co ji vadilo - "HTTP 400" samo o sobe
|
||||
* nikoho nikam nedovede.
|
||||
*/
|
||||
function statusError(
|
||||
status: number,
|
||||
request: ScriptRequestInfo,
|
||||
detail: string,
|
||||
): ScriptError {
|
||||
const where = `${request.method} ${request.path} vrátilo HTTP ${status}`;
|
||||
const options = { status, detail, request };
|
||||
|
||||
if (retryableStatuses.has(status)) {
|
||||
return new ScriptError('retryable', `Služba je momentálně nedostupná: ${where}.`, {
|
||||
status,
|
||||
detail,
|
||||
});
|
||||
return new ScriptError('retryable', `Služba je momentálně nedostupná: ${where}.`, options);
|
||||
}
|
||||
if (status === 401 || status === 403) {
|
||||
return new ScriptError('config', `Přístup zamítnut: ${where}. Zkontrolujte přístupové údaje.`, {
|
||||
status,
|
||||
detail,
|
||||
});
|
||||
return new ScriptError(
|
||||
'config',
|
||||
`Přístup zamítnut: ${where}. Zkontrolujte přístupové údaje.`,
|
||||
options,
|
||||
);
|
||||
}
|
||||
if (status === 404) {
|
||||
return new ScriptError('terminal', `Záznam nenalezen: ${where}.`, { status, detail });
|
||||
return new ScriptError('terminal', `Záznam nenalezen: ${where}.`, options);
|
||||
}
|
||||
return new ScriptError('terminal', `Volání selhalo: ${where}.`, { status, detail });
|
||||
return new ScriptError('terminal', `Volání selhalo: ${where}.`, options);
|
||||
}
|
||||
|
||||
function transportError(err: unknown, url: URL): ScriptError {
|
||||
function transportError(err: unknown, request: ScriptRequestInfo): ScriptError {
|
||||
if (err instanceof ScriptError) return err;
|
||||
|
||||
const code =
|
||||
err !== null && typeof err === 'object' && 'code' in err ? String((err as { code: unknown }).code) : '';
|
||||
const name = err instanceof Error ? err.name : '';
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
// Puvodni chyba vcetne pricin. `cause` nese u fetch to podstatne.
|
||||
const cause =
|
||||
err instanceof Error && err.cause instanceof Error ? `
|
||||
Příčina: ${err.cause.message}` : '';
|
||||
const detail = truncate(`${name}: ${message}${cause}`, config.errorDetailBytes);
|
||||
|
||||
if (name === 'AbortError' || name === 'TimeoutError') {
|
||||
return new ScriptError('timeout', `Volání ${url.pathname} nedoběhlo v limitu.`, { cause: err });
|
||||
return new ScriptError('timeout', `Volání ${request.path} nedoběhlo v limitu.`, {
|
||||
request,
|
||||
detail,
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
if (retryableCodes.has(code)) {
|
||||
return new ScriptError('retryable', `Nepodařilo se spojit se službou (${code}).`, { cause: err });
|
||||
return new ScriptError('retryable', `Nepodařilo se spojit se službou (${code}).`, {
|
||||
request,
|
||||
detail,
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
return new ScriptError('retryable', `Volání ${url.pathname} selhalo: ${message}`, { cause: err });
|
||||
return new ScriptError('retryable', `Volání ${request.path} selhalo: ${message}`, {
|
||||
request,
|
||||
detail,
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
|
||||
export interface CreateHttpOptions {
|
||||
@@ -137,6 +171,8 @@ export function createHttp(options: CreateHttpOptions): ScriptHttp {
|
||||
httpOptions?: ScriptHttpOptions,
|
||||
): Promise<ScriptHttpResponse<T>> {
|
||||
const url = buildUrl(target, path, httpOptions);
|
||||
// Do chyby jde jen cesta, ne cela adresa - v query muze byt tajemstvi.
|
||||
const request: ScriptRequestInfo = { method, path: url.pathname };
|
||||
const hasBody = body !== undefined && method !== 'GET' && method !== 'DELETE';
|
||||
const startedAt = Date.now();
|
||||
onCall();
|
||||
@@ -157,7 +193,7 @@ export function createHttp(options: CreateHttpOptions): ScriptHttp {
|
||||
body: hasBody ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
} catch (err) {
|
||||
throw transportError(err, url);
|
||||
throw transportError(err, request);
|
||||
}
|
||||
|
||||
const declaredSize = Number(response.headers.get('content-length') ?? 0);
|
||||
@@ -186,7 +222,8 @@ export function createHttp(options: CreateHttpOptions): ScriptHttp {
|
||||
} catch {
|
||||
throw new ScriptError('terminal', `Odpověď ${url.pathname} není platný JSON.`, {
|
||||
status: response.status,
|
||||
detail: redact(describe(raw, 300)),
|
||||
request,
|
||||
detail: redact(truncate(raw, config.errorDetailBytes)),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -195,7 +232,9 @@ export function createHttp(options: CreateHttpOptions): ScriptHttp {
|
||||
|
||||
const allowed = httpOptions?.allowStatus ?? [];
|
||||
if (!response.ok && !allowed.includes(response.status)) {
|
||||
throw statusError(response.status, url, redact(describe(parsed, 400)));
|
||||
// Zamerne surove telo, ne prochazene pres JSON.stringify - u chyby chceme
|
||||
// presne to, co sluzba poslala, vcetne pripadneho HTML nebo textu.
|
||||
throw statusError(response.status, request, redact(truncate(raw, config.errorDetailBytes)));
|
||||
}
|
||||
|
||||
return { status: response.status, body: parsed as T };
|
||||
|
||||
+34
-15
@@ -21,11 +21,11 @@ import {
|
||||
isRetryableKind,
|
||||
ScriptError,
|
||||
type ScriptContext,
|
||||
type ScriptErrorKind,
|
||||
type ScriptLogEntry,
|
||||
type ScriptRunError,
|
||||
type ScriptRunResult,
|
||||
} from './types.js';
|
||||
import { createRedactor, describe, scriptUtil } from './util.js';
|
||||
import { createRedactor, describe, scriptUtil, truncate } from './util.js';
|
||||
import { validateValues } from './values.js';
|
||||
|
||||
export interface RunScriptOptions {
|
||||
@@ -51,22 +51,36 @@ function defaultIdempotencyKey(scriptId: string, inputs: unknown): string {
|
||||
return `${scriptId}:${hash.slice(0, 24)}`;
|
||||
}
|
||||
|
||||
function toRunError(
|
||||
err: unknown,
|
||||
redact: (value: string) => string,
|
||||
): { kind: ScriptErrorKind; message: string; status?: number; detail?: string } {
|
||||
/**
|
||||
* Prevede vyjimku na popis chyby pro klienta.
|
||||
*
|
||||
* Vraci **cely** detail, tedy telo odpovedi sluzby, a u neocekavane vyjimky
|
||||
* i zasobnik volani. Bez toho uzivatel vidi jen "volani selhalo" a nema podle
|
||||
* ceho hledat. Zasobnik jen mimo produkci, stejne jako u error handleru.
|
||||
*/
|
||||
function toRunError(err: unknown, redact: (value: string) => string): ScriptRunError {
|
||||
if (err instanceof ScriptError) {
|
||||
return {
|
||||
kind: err.kind,
|
||||
message: redact(err.message),
|
||||
retryable: isRetryableKind(err.kind),
|
||||
...(err.status !== undefined ? { status: err.status } : {}),
|
||||
...(err.detail !== undefined ? { detail: redact(err.detail) } : {}),
|
||||
...(err.request !== undefined ? { request: err.request } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const stack = err instanceof Error ? err.stack : undefined;
|
||||
|
||||
// Neocekavana vyjimka ve skriptu. Opakovat ji nema smysl, kod se sam nespravi.
|
||||
return { kind: 'internal', message: redact(`Skript selhal: ${message}`) };
|
||||
return {
|
||||
kind: 'internal',
|
||||
message: redact(`Skript selhal: ${message}`),
|
||||
retryable: false,
|
||||
detail: redact(truncate(String(stack ?? message), config.errorDetailBytes)),
|
||||
...(config.isProduction || stack === undefined ? {} : { stack: redact(stack) }),
|
||||
};
|
||||
}
|
||||
|
||||
export async function runScript(
|
||||
@@ -165,10 +179,14 @@ export async function runScript(
|
||||
config: Object.freeze({ ...target.serviceConfig }),
|
||||
idempotencyKey,
|
||||
fail(message, detail) {
|
||||
throw new ScriptError('terminal', message, { detail: describe(detail) });
|
||||
throw new ScriptError('terminal', message, {
|
||||
detail: describe(detail, config.errorDetailBytes),
|
||||
});
|
||||
},
|
||||
retry(message, detail) {
|
||||
throw new ScriptError('retryable', message, { detail: describe(detail) });
|
||||
throw new ScriptError('retryable', message, {
|
||||
detail: describe(detail, config.errorDetailBytes),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -177,12 +195,13 @@ export async function runScript(
|
||||
returned = await script.run(validatedInputs.values, ctx);
|
||||
} catch (err) {
|
||||
const error = toRunError(err, redact);
|
||||
console.warn(`[scripts] ${scriptId} selhal (${error.kind}): ${error.message}`);
|
||||
return finish({
|
||||
ok: false,
|
||||
outputs: {},
|
||||
error: { ...error, retryable: isRetryableKind(error.kind) },
|
||||
});
|
||||
// Do logu serveru cely detail, at je to dohledatelne i bez portalu.
|
||||
console.warn(
|
||||
`[scripts] ${scriptId} selhal (${error.kind}): ${error.message}` +
|
||||
(error.detail ? `
|
||||
${error.detail}` : ''),
|
||||
);
|
||||
return finish({ ok: false, outputs: {}, error });
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
|
||||
+25
-2
@@ -158,23 +158,41 @@ export function isRetryableKind(kind: ScriptErrorKind): boolean {
|
||||
return retryableKinds.includes(kind);
|
||||
}
|
||||
|
||||
/** Ktere volani spadlo. Bez toho je chybova zprava jen pulka informace. */
|
||||
export interface ScriptRequestInfo {
|
||||
method: string;
|
||||
/** Cesta bez domeny. Cela adresa muze nest tajemstvi v query. */
|
||||
path: string;
|
||||
}
|
||||
|
||||
export class ScriptError extends Error {
|
||||
readonly kind: ScriptErrorKind;
|
||||
/** HTTP kod cizi sluzby, kdyz chyba prisla z volani. */
|
||||
readonly status?: number;
|
||||
/** Kratky detail k zobrazeni. Uz zredigovany, bez tajemstvi. */
|
||||
/**
|
||||
* Cela odpoved sluzby. Uz zredigovana, bez tajemstvi.
|
||||
* Zkracuje se az na `errorDetailBytes`, protoze prave tady je napsane,
|
||||
* co sluzbe vadilo.
|
||||
*/
|
||||
readonly detail?: string;
|
||||
readonly request?: ScriptRequestInfo;
|
||||
|
||||
constructor(
|
||||
kind: ScriptErrorKind,
|
||||
message: string,
|
||||
options: { status?: number; detail?: string; cause?: unknown } = {},
|
||||
options: {
|
||||
status?: number;
|
||||
detail?: string;
|
||||
request?: ScriptRequestInfo;
|
||||
cause?: unknown;
|
||||
} = {},
|
||||
) {
|
||||
super(message, options.cause !== undefined ? { cause: options.cause } : undefined);
|
||||
this.name = 'ScriptError';
|
||||
this.kind = kind;
|
||||
this.status = options.status;
|
||||
this.detail = options.detail;
|
||||
this.request = options.request;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +313,12 @@ export interface ScriptRunError {
|
||||
message: string;
|
||||
retryable: boolean;
|
||||
status?: number;
|
||||
/** Cela odpoved sluzby, uz zredigovana. Tady je napsane, co ji vadilo. */
|
||||
detail?: string;
|
||||
/** Ktere volani spadlo. */
|
||||
request?: ScriptRequestInfo;
|
||||
/** Zasobnik volani. Jen mimo produkci, stejne jako u error handleru. */
|
||||
stack?: string;
|
||||
/** Vyplnene jen u chyb ve vstupu nebo vystupu. */
|
||||
issues?: FieldIssue[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { Check, ChevronDown, ChevronRight, Copy } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { cn } from '@/lib/cn';
|
||||
|
||||
/**
|
||||
* Cele chybove hlaseni.
|
||||
*
|
||||
* Duvod, proc je to zvlast komponenta: zkracene hlaseni je k nicemu. U odpovedi
|
||||
* 400 nebo 401 je duvod napsany v tele odpovedi sluzby, ne v tom, ze prislo 401.
|
||||
* Proto se tady nic nezkraccuje, jde to rozbalit cele a zkopirovat.
|
||||
*
|
||||
* Server uz text zredigoval, takze pristupove udaje v nem nejsou.
|
||||
*/
|
||||
export function ErrorDetail({
|
||||
message,
|
||||
detail,
|
||||
status,
|
||||
request,
|
||||
retryable,
|
||||
kind,
|
||||
className,
|
||||
}: {
|
||||
message: string;
|
||||
/** Cela odpoved sluzby nebo zasobnik volani. */
|
||||
detail?: string;
|
||||
status?: number;
|
||||
request?: { method: string; path: string };
|
||||
retryable?: boolean;
|
||||
kind?: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
/** Co se zkopiruje: vsechno, co o chybe vime, at to jde poslat dal. */
|
||||
const fullText = [
|
||||
message,
|
||||
request ? `Volání: ${request.method} ${request.path}` : null,
|
||||
status !== undefined ? `HTTP: ${status}` : null,
|
||||
kind ? `Druh: ${kind}` : null,
|
||||
retryable !== undefined
|
||||
? `Opakování: ${retryable ? 'má smysl zkusit znovu' : 'opakování nepomůže'}`
|
||||
: null,
|
||||
detail ? `\nOdpověď služby:\n${detail}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
|
||||
async function copy() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(fullText);
|
||||
setCopied(true);
|
||||
window.setTimeout(() => setCopied(false), 2000);
|
||||
} catch (err) {
|
||||
// Bez pristupu ke schranke (starsi prohlizec, http) se nic nepredstira.
|
||||
console.warn('[error] schranka neni dostupna:', err);
|
||||
}
|
||||
}
|
||||
|
||||
const hasMore = Boolean(detail) || request !== undefined || status !== undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-lg border border-danger-500/40 bg-danger-500/8 p-3 text-sm',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<p className="text-danger-400">{message}</p>
|
||||
|
||||
<p className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-white/40">
|
||||
{request && (
|
||||
<span className="font-mono">
|
||||
{request.method} {request.path}
|
||||
</span>
|
||||
)}
|
||||
{status !== undefined && <span>HTTP {status}</span>}
|
||||
{kind && <span>{kind}</span>}
|
||||
{retryable !== undefined && (
|
||||
<span className={retryable ? 'text-warn-400' : undefined}>
|
||||
{retryable ? 'má smysl zkusit znovu' : 'opakování nepomůže'}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
|
||||
{hasMore && (
|
||||
<div className="mt-2 flex flex-wrap items-center gap-3">
|
||||
{detail && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((previous) => !previous)}
|
||||
className="flex items-center gap-1 text-xs text-white/50 transition-colors hover:text-white"
|
||||
>
|
||||
{open ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />}
|
||||
{open ? 'Skrýt odpověď služby' : 'Zobrazit celou odpověď služby'}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={copy}
|
||||
className="flex items-center gap-1 text-xs text-white/50 transition-colors hover:text-white"
|
||||
>
|
||||
{copied ? <Check className="size-3 text-ok-400" /> : <Copy className="size-3" />}
|
||||
{copied ? 'Zkopírováno' : 'Kopírovat vše'}
|
||||
</button>
|
||||
</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}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
Eye,
|
||||
EyeOff,
|
||||
@@ -13,6 +12,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { DataState } from '@/components/dashboard/DataState';
|
||||
import { ErrorDetail } from '@/components/dashboard/ErrorDetail';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Modal } from '@/components/ui/Modal';
|
||||
@@ -224,21 +224,17 @@ function ConnectorCard({
|
||||
)}
|
||||
|
||||
{connector.lastError && !test && (
|
||||
<p className="mt-2 line-clamp-3 text-xs text-danger-400">{connector.lastError}</p>
|
||||
<p className="mt-2 text-xs text-danger-400">
|
||||
{connector.lastError}
|
||||
<span className="mt-0.5 block text-white/35">
|
||||
Celé hlášení uvidíte po stisku Ověřit.
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{test && (
|
||||
<p
|
||||
className={cn(
|
||||
'mt-2 flex items-start gap-1.5 text-xs',
|
||||
test.ok ? 'text-ok-400' : 'text-danger-400',
|
||||
)}
|
||||
>
|
||||
{test.ok ? (
|
||||
<CheckCircle2 className="mt-0.5 size-3 shrink-0" />
|
||||
) : (
|
||||
<AlertTriangle className="mt-0.5 size-3 shrink-0" />
|
||||
)}
|
||||
{test?.ok && (
|
||||
<p className="mt-2 flex items-start gap-1.5 text-xs text-ok-400">
|
||||
<CheckCircle2 className="mt-0.5 size-3 shrink-0" />
|
||||
<span>
|
||||
{test.message}
|
||||
<span className="block text-white/35">Ověřeno: {test.checked}</span>
|
||||
@@ -246,6 +242,16 @@ function ConnectorCard({
|
||||
</p>
|
||||
)}
|
||||
|
||||
{test && !test.ok && (
|
||||
<ErrorDetail
|
||||
className="mt-2"
|
||||
message={test.message}
|
||||
detail={test.detail}
|
||||
status={test.status}
|
||||
request={test.request}
|
||||
/>
|
||||
)}
|
||||
|
||||
{error && <p className="mt-2 text-xs text-danger-400">{error}</p>}
|
||||
|
||||
<div className="mt-auto flex flex-wrap items-center gap-1.5 pt-4">
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useAuth } from '@/auth/AuthContext';
|
||||
import { DataState } from '@/components/dashboard/DataState';
|
||||
import { ErrorDetail } from '@/components/dashboard/ErrorDetail';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { apiFetch, ApiError } from '@/lib/api';
|
||||
@@ -507,25 +508,22 @@ function RunResult({ result }: { result: ScriptRunResult }) {
|
||||
</p>
|
||||
|
||||
{result.error && (
|
||||
<div className="rounded-lg border border-danger-500/40 bg-danger-500/8 p-3 text-sm">
|
||||
<p className="text-danger-400">{result.error.message}</p>
|
||||
<p className="mt-1 text-xs text-white/40">
|
||||
Druh: {result.error.kind}
|
||||
{result.error.status ? `, HTTP ${result.error.status}` : ''},{' '}
|
||||
{result.error.retryable ? 'má smysl zkusit znovu' : 'opakování nepomůže'}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<ErrorDetail
|
||||
message={result.error.message}
|
||||
detail={result.error.detail}
|
||||
status={result.error.status}
|
||||
request={result.error.request}
|
||||
retryable={result.error.retryable}
|
||||
kind={result.error.kind}
|
||||
/>
|
||||
{result.error.issues && result.error.issues.length > 0 && (
|
||||
<ul className="mt-2 ml-4 list-disc text-xs text-white/60">
|
||||
<ul className="ml-4 list-disc text-xs text-white/60">
|
||||
{result.error.issues.map((issue) => (
|
||||
<li key={`${issue.field}-${issue.message}`}>{issue.message}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{result.error.detail && (
|
||||
<pre className="mt-2 overflow-x-auto rounded bg-ink-950/60 p-2 text-xs text-white/50">
|
||||
{result.error.detail}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -298,6 +298,10 @@ export interface ConnectorTestResult {
|
||||
checked: string;
|
||||
status?: number;
|
||||
message: string;
|
||||
/** Ktere volani to bylo. Bez toho je zprava jen pulka informace. */
|
||||
request?: { method: string; path: string };
|
||||
/** Cela odpoved sluzby. Tady je napsane, co ji vadilo. */
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
export interface ServiceCatalog {
|
||||
@@ -526,7 +530,11 @@ export interface ScriptRunResult {
|
||||
message: string;
|
||||
retryable: boolean;
|
||||
status?: number;
|
||||
/** Cela odpoved sluzby, nebo zasobnik volani u chyby ve skriptu. */
|
||||
detail?: string;
|
||||
request?: { method: string; path: string };
|
||||
/** Jen mimo produkci. */
|
||||
stack?: string;
|
||||
issues?: Array<{ field: string; message: string }>;
|
||||
} | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user