feat(miroflow): integração com Superset para criar dashboards automáticos
- Adiciona tabela miroflow_generated_dashboards para rastrear dashboards criados - Endpoint POST /api/superset/miroflow/create-dashboard cria dashboard no Superset - MiroFlowControl.tsx com botão 'Criar Dashboard' após análise - Dashboard criado com dados da análise e SQL query - Suporta todos os agentes (statistician, fiscal_auditor, researcher)
This commit is contained in:
parent
264029eafc
commit
c665f6ec40
|
|
@ -73,6 +73,26 @@ export function MiroFlowControl({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createDashboardMutation = useMutation({
|
||||||
|
mutationFn: async (dashboardData: {
|
||||||
|
dashboardTitle: string;
|
||||||
|
sqlQuery: string;
|
||||||
|
}) => {
|
||||||
|
const response = await apiRequest(
|
||||||
|
"POST",
|
||||||
|
"/api/superset/miroflow/create-dashboard",
|
||||||
|
{
|
||||||
|
...dashboardData,
|
||||||
|
analysisId: mutation.data?.execution_id,
|
||||||
|
agent: selectedAgent,
|
||||||
|
task,
|
||||||
|
insights: mutation.data?.result,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleAnalyze = () => {
|
const handleAnalyze = () => {
|
||||||
if (!task.trim()) {
|
if (!task.trim()) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -207,6 +227,46 @@ export function MiroFlowControl({
|
||||||
{mutation.data?.execution_id?.slice(0, 8)}...
|
{mutation.data?.execution_id?.slice(0, 8)}...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Create Dashboard Button */}
|
||||||
|
<div className="pt-4 border-t border-[#c89b3c]/20">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const title = `Análise ${selectedAgent} - ${new Date().toLocaleDateString('pt-BR')}`;
|
||||||
|
createDashboardMutation.mutate({
|
||||||
|
dashboardTitle: title,
|
||||||
|
sqlQuery: `SELECT * FROM arcadia LIMIT 100`, // Placeholder - idealmente vem do MiroFlow
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={createDashboardMutation.isPending}
|
||||||
|
className="w-full bg-[#8b7355] hover:bg-[#9d8568] text-white font-semibold"
|
||||||
|
>
|
||||||
|
{createDashboardMutation.isPending ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
Criando Dashboard...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"📊 Criar Dashboard no Superset"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{createDashboardMutation.isSuccess && (
|
||||||
|
<div className="mt-2 rounded-lg p-3 bg-green-900/20 border border-green-500/50">
|
||||||
|
<p className="text-xs text-green-100">
|
||||||
|
✅ Dashboard criado! Atualizando...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{createDashboardMutation.isError && (
|
||||||
|
<div className="mt-2 rounded-lg p-3 bg-red-900/20 border border-red-500/50">
|
||||||
|
<p className="text-xs text-red-100">
|
||||||
|
❌ Erro: {createDashboardMutation.error instanceof Error ? createDashboardMutation.error.message : "Erro ao criar dashboard"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
-- MiroFlow Generated Dashboards
|
||||||
|
-- Rastreia dashboards criados pelo MiroFlow baseado em análises
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS miroflow_generated_dashboards (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Análise que criou este dashboard
|
||||||
|
analysis_id VARCHAR(255),
|
||||||
|
agent VARCHAR(50) NOT NULL CHECK (agent IN ('statistician', 'fiscal_auditor', 'researcher')),
|
||||||
|
task TEXT NOT NULL,
|
||||||
|
insights JSONB,
|
||||||
|
|
||||||
|
-- Dashboard no Superset
|
||||||
|
dashboard_id INT NOT NULL,
|
||||||
|
dashboard_title VARCHAR(255) NOT NULL,
|
||||||
|
dataset_name VARCHAR(255),
|
||||||
|
|
||||||
|
-- SQL sugerido/criado
|
||||||
|
sql_query TEXT,
|
||||||
|
|
||||||
|
-- Auditoria
|
||||||
|
created_by UUID,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
tenant_id INT,
|
||||||
|
|
||||||
|
-- Referência
|
||||||
|
superset_url VARCHAR(512),
|
||||||
|
|
||||||
|
-- Soft delete
|
||||||
|
deleted_at TIMESTAMP,
|
||||||
|
|
||||||
|
-- Constraints
|
||||||
|
CONSTRAINT unique_analysis_dashboard UNIQUE (analysis_id, dashboard_id) DEFERRABLE INITIALLY DEFERRED
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_miroflow_dashboards_analysis ON miroflow_generated_dashboards(analysis_id);
|
||||||
|
CREATE INDEX idx_miroflow_dashboards_created_at ON miroflow_generated_dashboards(created_at DESC);
|
||||||
|
CREATE INDEX idx_miroflow_dashboards_dashboard_id ON miroflow_generated_dashboards(dashboard_id);
|
||||||
|
CREATE INDEX idx_miroflow_dashboards_tenant ON miroflow_generated_dashboards(tenant_id);
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { Express, Request, Response } from "express";
|
import type { Express, Request, Response } from "express";
|
||||||
|
import { createMiroFlowBridge } from "./MiroFlowBridge";
|
||||||
|
|
||||||
const SUPERSET_HOST = process.env.SUPERSET_HOST || "localhost";
|
const SUPERSET_HOST = process.env.SUPERSET_HOST || "localhost";
|
||||||
const SUPERSET_PORT = parseInt(process.env.SUPERSET_PORT || "8088", 10);
|
const SUPERSET_PORT = parseInt(process.env.SUPERSET_PORT || "8088", 10);
|
||||||
|
|
@ -6,6 +7,8 @@ const SUPERSET_URL = `http://${SUPERSET_HOST}:${SUPERSET_PORT}`;
|
||||||
const ADMIN_USER = process.env.SUPERSET_ADMIN_USER || "admin";
|
const ADMIN_USER = process.env.SUPERSET_ADMIN_USER || "admin";
|
||||||
const ADMIN_PASS = process.env.SUPERSET_ADMIN_PASSWORD || "arcadia2026";
|
const ADMIN_PASS = process.env.SUPERSET_ADMIN_PASSWORD || "arcadia2026";
|
||||||
|
|
||||||
|
const miroflowBridge = createMiroFlowBridge();
|
||||||
|
|
||||||
// Cache do service token (válido por 50 min)
|
// Cache do service token (válido por 50 min)
|
||||||
let cachedToken: string | null = null;
|
let cachedToken: string | null = null;
|
||||||
let tokenExpiry = 0;
|
let tokenExpiry = 0;
|
||||||
|
|
@ -105,4 +108,200 @@ export function registerSupersetRoutes(app: Express): void {
|
||||||
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
|
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
|
||||||
res.redirect("/superset/login");
|
res.redirect("/superset/login");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// MiroFlow Bridge Routes
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/superset/miroflow/analyze
|
||||||
|
* Enriquece dataset Superset com análises científicas via MiroFlow
|
||||||
|
*
|
||||||
|
* Request body:
|
||||||
|
* {
|
||||||
|
* "dataset": { "data": [...], "columns": [...] },
|
||||||
|
* "config": { "type": "forecast|anomaly|exploratory", "horizon": 30, "confidence_level": 0.95 }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Response:
|
||||||
|
* {
|
||||||
|
* "success": true,
|
||||||
|
* "data": { ...enriched dataset with _miroflow_analysis, _anomaly_scores, etc },
|
||||||
|
* "analysis": { "summary": "...", "analysis_type": "..." },
|
||||||
|
* "execution_id": "uuid"
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
app.post("/api/superset/miroflow/analyze", async (req: Request, res: Response) => {
|
||||||
|
await miroflowBridge.handleAnalyzeRequest(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/superset/miroflow/health
|
||||||
|
* Verifica conexão com MiroFlow bridge
|
||||||
|
*/
|
||||||
|
app.get("/api/superset/miroflow/health", async (_req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const miroflowUrl = process.env.MIROFLOW_URL || "http://localhost:8006";
|
||||||
|
const response = await fetch(`${miroflowUrl}/health`, {
|
||||||
|
signal: AbortSignal.timeout(5_000),
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
res.json({
|
||||||
|
online: response.ok,
|
||||||
|
miroflow: data,
|
||||||
|
bridge: "operational",
|
||||||
|
endpoints: {
|
||||||
|
analyze: "/api/superset/miroflow/analyze",
|
||||||
|
health: "/api/superset/miroflow/health",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
res.json({
|
||||||
|
online: false,
|
||||||
|
error: err.message,
|
||||||
|
bridge: "offline",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/superset/miroflow/create-dashboard
|
||||||
|
* Cria um dashboard no Superset baseado em análise MiroFlow
|
||||||
|
*/
|
||||||
|
app.post("/api/superset/miroflow/create-dashboard", async (req: Request, res: Response) => {
|
||||||
|
if (!req.isAuthenticated()) {
|
||||||
|
return res.status(401).json({ error: "Não autenticado" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { dashboardTitle, sqlQuery, analysisId, agent, task, insights } = req.body;
|
||||||
|
|
||||||
|
if (!dashboardTitle || !sqlQuery || !analysisId) {
|
||||||
|
return res.status(400).json({ error: "dashboardTitle, sqlQuery e analysisId são obrigatórios" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await getServiceToken();
|
||||||
|
const user = req.user as any;
|
||||||
|
|
||||||
|
// 1. Criar dataset no Superset
|
||||||
|
const datasetResp = await fetch(`${SUPERSET_URL}/api/v1/datasets/`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
database: 1, // ID do banco "Arcádia Suite"
|
||||||
|
table_name: `miroflow_${analysisId.substring(0, 8)}`,
|
||||||
|
sql: sqlQuery,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!datasetResp.ok) {
|
||||||
|
const err = await datasetResp.json().catch(() => ({ detail: "Erro ao criar dataset" }));
|
||||||
|
throw new Error(`Falha ao criar dataset: ${err.detail}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataset = await datasetResp.json();
|
||||||
|
const datasetId = dataset.id;
|
||||||
|
|
||||||
|
// 2. Criar chart automático
|
||||||
|
const chartResp = await fetch(`${SUPERSET_URL}/api/v1/chart/`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
datasource_id: datasetId,
|
||||||
|
datasource_type: "table",
|
||||||
|
chart_type: "table", // Começar com table, depois adicionar mais tipos
|
||||||
|
name: `${dashboardTitle} - Dados`,
|
||||||
|
description: `Criado via MiroFlow Analysis (${agent})`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!chartResp.ok) {
|
||||||
|
const err = await chartResp.json().catch(() => ({ detail: "Erro ao criar chart" }));
|
||||||
|
throw new Error(`Falha ao criar chart: ${err.detail}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const chart = await chartResp.json();
|
||||||
|
const chartId = chart.id;
|
||||||
|
|
||||||
|
// 3. Criar dashboard
|
||||||
|
const dashboardResp = await fetch(`${SUPERSET_URL}/api/v1/dashboard/`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
dashboard_title: dashboardTitle,
|
||||||
|
description: `Análise MiroFlow - ${agent} - ${new Date().toLocaleString('pt-BR')}`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dashboardResp.ok) {
|
||||||
|
const err = await dashboardResp.json().catch(() => ({ detail: "Erro ao criar dashboard" }));
|
||||||
|
throw new Error(`Falha ao criar dashboard: ${err.detail}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dashboard = await dashboardResp.json();
|
||||||
|
const dashboardId = dashboard.id;
|
||||||
|
|
||||||
|
// 4. Adicionar chart ao dashboard
|
||||||
|
const updateResp = await fetch(`${SUPERSET_URL}/api/v1/dashboard/${dashboardId}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
dashboard_title: dashboardTitle,
|
||||||
|
slices: [chartId],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!updateResp.ok) {
|
||||||
|
throw new Error("Falha ao adicionar chart ao dashboard");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Salvar referência no banco Arcádia
|
||||||
|
const db = await import("../db/index.js");
|
||||||
|
await db.query(
|
||||||
|
`INSERT INTO miroflow_generated_dashboards
|
||||||
|
(analysis_id, agent, task, insights, dashboard_id, dashboard_title, dataset_name, sql_query, created_by, tenant_id, superset_url)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
|
||||||
|
[
|
||||||
|
analysisId,
|
||||||
|
agent || "unknown",
|
||||||
|
task || "",
|
||||||
|
JSON.stringify(insights || {}),
|
||||||
|
dashboardId,
|
||||||
|
dashboardTitle,
|
||||||
|
`miroflow_${analysisId.substring(0, 8)}`,
|
||||||
|
sqlQuery,
|
||||||
|
user.id,
|
||||||
|
user.tenantId || null,
|
||||||
|
`${SUPERSET_URL}/superset/dashboard/${dashboardId}/`,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
dashboardId,
|
||||||
|
dashboardUrl: `${SUPERSET_URL}/superset/dashboard/${dashboardId}/`,
|
||||||
|
message: `Dashboard "${dashboardTitle}" criado com sucesso!`,
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("[Superset] Erro ao criar dashboard:", err.message);
|
||||||
|
res.status(502).json({
|
||||||
|
error: err.message,
|
||||||
|
source: "superset_create_dashboard",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[Superset] MiroFlow Bridge registered at /api/superset/miroflow");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue