fix(superset): corrigir erro fetch failed no guest-token

- Adiciona AbortSignal.timeout(8s) em todas as chamadas fetch ao Superset
- Limpa cache de token automaticamente em caso de erro (clearTokenCache)
  para que próxima requisição force novo login ao invés de usar token stale
- Reduz TTL do cache de 50 para 45 min como margem de segurança
- Força iframe do SupersetDashboard a 100% width/height via classe CSS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jonas Pacheco 2026-03-22 21:18:44 -03:00
parent 4214a2561c
commit 4fd0e9d893
2 changed files with 34 additions and 3 deletions

View File

@ -82,6 +82,14 @@ export function SupersetDashboard({
},
});
// O SDK cria um <iframe> sem width/height explícitos — forçar 100%
const iframe = containerRef.current?.querySelector("iframe");
if (iframe) {
iframe.style.width = "100%";
iframe.style.height = "100%";
iframe.style.border = "none";
}
setStatus("ready");
} catch (err: any) {
console.error("[SupersetDashboard] Erro:", err.message, err);
@ -155,9 +163,16 @@ export function SupersetDashboard({
</a>
)}
<style>{`
.superset-embed-container iframe {
width: 100% !important;
height: 100% !important;
border: none !important;
}
`}</style>
<div
ref={containerRef}
className="w-full h-full rounded-xl overflow-hidden border border-[#c89b3c]/20"
className="superset-embed-container w-full h-full rounded-xl overflow-hidden border border-[#c89b3c]/20"
/>
</div>
);

View File

@ -6,12 +6,21 @@ const SUPERSET_URL = `http://${SUPERSET_HOST}:${SUPERSET_PORT}`;
const ADMIN_USER = process.env.SUPERSET_ADMIN_USER || "admin";
const ADMIN_PASS = process.env.SUPERSET_ADMIN_PASSWORD || "arcadia2026";
// Cache do service token (válido por 50 min)
// Cache do service token (válido por 45 min)
let cachedToken: string | null = null;
let cachedCsrf: string | null = null;
let cachedSession: string | null = null;
let tokenExpiry = 0;
const FETCH_TIMEOUT = 8000; // 8s — falha rápido em vez de ficar pendurado
function clearTokenCache() {
cachedToken = null;
cachedCsrf = null;
cachedSession = null;
tokenExpiry = 0;
}
async function getServiceToken(): Promise<{ token: string; csrf: string; session: string }> {
if (cachedToken && cachedCsrf && cachedSession && Date.now() < tokenExpiry) {
return { token: cachedToken, csrf: cachedCsrf, session: cachedSession };
@ -21,6 +30,7 @@ async function getServiceToken(): Promise<{ token: string; csrf: string; session
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: ADMIN_USER, password: ADMIN_PASS, provider: "db", refresh: true }),
signal: AbortSignal.timeout(FETCH_TIMEOUT),
});
if (!resp.ok) throw new Error(`Falha ao autenticar no Superset (${resp.status})`);
@ -29,6 +39,7 @@ async function getServiceToken(): Promise<{ token: string; csrf: string; session
const csrfResp = await fetch(`${SUPERSET_URL}/api/v1/security/csrf_token/`, {
headers: { "Authorization": `Bearer ${cachedToken}` },
signal: AbortSignal.timeout(FETCH_TIMEOUT),
});
if (!csrfResp.ok) throw new Error(`Falha ao obter CSRF token (${csrfResp.status})`);
const csrfData = await csrfResp.json();
@ -38,7 +49,7 @@ async function getServiceToken(): Promise<{ token: string; csrf: string; session
const setCookie = csrfResp.headers.get("set-cookie") || "";
cachedSession = setCookie.split(";")[0]; // ex: "session=xxx"
tokenExpiry = Date.now() + 50 * 60 * 1000;
tokenExpiry = Date.now() + 45 * 60 * 1000;
return { token: cachedToken!, csrf: cachedCsrf!, session: cachedSession };
}
@ -60,6 +71,7 @@ export function registerSupersetRoutes(app: Express): void {
// o admin via user_loader (necessário quando o papel Public tem can_read on Dashboard)
const dashResp = await fetch(`${SUPERSET_URL}/api/v1/dashboard/${dashboardId}`, {
headers: { "Authorization": `Bearer ${token}`, "Cookie": session },
signal: AbortSignal.timeout(FETCH_TIMEOUT),
});
if (!dashResp.ok) {
return res.status(404).json({ error: `Dashboard '${dashboardId}' não encontrado no Superset` });
@ -69,6 +81,7 @@ export function registerSupersetRoutes(app: Express): void {
const embeddedResp = await fetch(`${SUPERSET_URL}/api/v1/dashboard/${dbNumericId}/embedded`, {
headers: { "Authorization": `Bearer ${token}`, "Cookie": session },
signal: AbortSignal.timeout(FETCH_TIMEOUT),
});
if (!embeddedResp.ok) {
return res.status(404).json({ error: `Dashboard '${dashboardId}' não tem embedding habilitado` });
@ -97,6 +110,7 @@ export function registerSupersetRoutes(app: Express): void {
resources: [{ type: "dashboard", id: embeddedUuid }],
rls: user?.tenantId ? [{ clause: `tenant_id = ${user.tenantId}` }] : [],
}),
signal: AbortSignal.timeout(FETCH_TIMEOUT),
});
if (!guestResp.ok) {
@ -108,6 +122,7 @@ export function registerSupersetRoutes(app: Express): void {
res.json({ token: guestToken, embeddedId: embeddedUuid, supersetUrl: "/superset" });
} catch (err: any) {
console.error("[Superset] guest-token error:", err.message);
clearTokenCache(); // limpa cache para forçar novo login na próxima requisição
res.status(502).json({ error: err.message });
}
});
@ -128,6 +143,7 @@ export function registerSupersetRoutes(app: Express): void {
res.json(data.result || []);
} catch (err: any) {
console.error("[Superset] dashboards error:", err.message);
clearTokenCache();
res.status(502).json({ error: err.message, dashboards: [] });
}
});