feat: dashboard apps y mejora layout del dashboard Overview

Dashboard fn-registry Apps con 10 cards: KPIs por lenguaje, dominio,
framework, dependencias y catálogo completo. Cards del Overview
ampliadas a grid de 24 columnas con tamaños más legibles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 00:54:18 +01:00
parent 26ffba2c03
commit 95bba767bc
2 changed files with 247 additions and 15 deletions
@@ -0,0 +1,219 @@
"""Crea un dashboard en Metabase con metricas de la tabla apps del fn-registry."""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "python", "functions"))
from metabase.client import metabase_auth
from metabase import (
metabase_list_databases,
metabase_create_card,
metabase_create_dashboard,
metabase_update_dashboard,
metabase_list_dashboards,
)
# --- Config ---
METABASE_URL = "http://localhost:3000"
EMAIL = "admin@fnregistry.local"
PASSWORD = "FnRegistry2024!"
# --- Layout ---
# Grid de 24 unidades de ancho (estandar Metabase).
# Fila 0 (h=5): 4 scalars de 6 unidades → KPIs
# Fila 5 (h=8): 3 graficas de 8 unidades → distribucion general
# Fila 13 (h=9): 2 graficas de 12 unidades → uso de funciones + frameworks
# Fila 22 (h=8): tabla completa de apps → catalogo completo
CARDS = [
# ---- Fila 0: KPIs escalares (h=5) ----
{
"name": "Total de Apps",
"display": "scalar",
"sql": "SELECT COUNT(*) AS total FROM apps;",
"size_x": 6, "size_y": 5, "col": 0, "row": 0,
},
{
"name": "Apps Go",
"display": "scalar",
"sql": "SELECT COUNT(*) AS apps_go FROM apps WHERE lang = 'go';",
"size_x": 6, "size_y": 5, "col": 6, "row": 0,
},
{
"name": "Apps Python",
"display": "scalar",
"sql": "SELECT COUNT(*) AS apps_python FROM apps WHERE lang = 'py';",
"size_x": 6, "size_y": 5, "col": 12, "row": 0,
},
{
"name": "Dominios con Apps",
"display": "scalar",
"sql": "SELECT COUNT(DISTINCT domain) AS dominios FROM apps;",
"size_x": 6, "size_y": 5, "col": 18, "row": 0,
},
# ---- Fila 5: Distribucion general (h=8) ----
{
"name": "Apps por Lenguaje",
"display": "bar",
"sql": "SELECT lang, COUNT(*) AS cantidad FROM apps GROUP BY lang ORDER BY cantidad DESC;",
"size_x": 8, "size_y": 8, "col": 0, "row": 5,
},
{
"name": "Apps por Dominio",
"display": "pie",
"sql": "SELECT domain, COUNT(*) AS cantidad FROM apps GROUP BY domain ORDER BY cantidad DESC;",
"size_x": 8, "size_y": 8, "col": 8, "row": 5,
},
{
"name": "Apps con Framework",
"display": "bar",
"sql": """
SELECT
CASE WHEN framework = '' OR framework IS NULL THEN '(sin framework)' ELSE framework END AS framework,
COUNT(*) AS cantidad
FROM apps
GROUP BY framework
ORDER BY cantidad DESC;
""",
"size_x": 8, "size_y": 8, "col": 16, "row": 5,
},
# ---- Fila 13: Analisis de dependencias (h=9) ----
{
"name": "Apps con Mas Dependencias de Funciones",
"display": "row",
"sql": """
SELECT
name || ' (' || lang || ')' AS app,
(LENGTH(uses_functions) - LENGTH(REPLACE(uses_functions, ',', ''))
+ CASE WHEN uses_functions != '[]' AND uses_functions != '' THEN 1 ELSE 0 END) AS num_funciones_usadas
FROM apps
WHERE uses_functions != '[]' AND uses_functions != ''
ORDER BY num_funciones_usadas DESC
LIMIT 15;
""",
"size_x": 12, "size_y": 9, "col": 0, "row": 13,
},
{
"name": "Funciones del Registry Mas Usadas en Apps",
"display": "row",
"sql": """
WITH RECURSIVE split_uses(app_id, rest, val) AS (
SELECT id, uses_functions || ',', NULL
FROM apps
WHERE uses_functions != '[]' AND uses_functions != ''
UNION ALL
SELECT app_id,
SUBSTR(rest, INSTR(rest, ',') + 1),
TRIM(SUBSTR(rest, 1, INSTR(rest, ',') - 1), ' "[]')
FROM split_uses WHERE rest != ''
)
SELECT val AS funcion_usada, COUNT(*) AS veces_usada
FROM split_uses
WHERE val IS NOT NULL AND val != '' AND val != ']'
GROUP BY val
ORDER BY veces_usada DESC
LIMIT 15;
""",
"size_x": 12, "size_y": 9, "col": 12, "row": 13,
},
# ---- Fila 22: Catalogo completo (h=9) ----
{
"name": "Catalogo de Apps",
"display": "table",
"sql": """
SELECT
id,
name,
lang,
domain,
CASE WHEN framework = '' THEN '-' ELSE framework END AS framework,
description,
entry_point,
updated_at
FROM apps
ORDER BY domain, name;
""",
"size_x": 24, "size_y": 9, "col": 0, "row": 22,
},
]
def main():
print("Autenticando en Metabase...")
client = metabase_auth(METABASE_URL, EMAIL, PASSWORD)
# Encontrar la database registry.db
dbs = metabase_list_databases(client)
registry_db_id = None
for db in dbs:
if "registry" in db.get("name", "").lower() or (
db.get("engine") == "sqlite"
and "registry" in db.get("details", {}).get("db", "")
):
registry_db_id = db["id"]
print(f" Database encontrada: {db['name']} (id={db['id']})")
break
if not registry_db_id:
print("ERROR: No se encontro registry.db en Metabase.")
print("Databases disponibles:")
for db in dbs:
print(f" - {db['id']}: {db['name']} ({db['engine']})")
sys.exit(1)
# Verificar si ya existe un dashboard con este nombre
existing = metabase_list_dashboards(client)
for d in existing:
if d.get("name") == "fn-registry Apps":
print(f" Dashboard ya existe (id={d['id']}), recreando...")
from metabase import metabase_delete_dashboard
metabase_delete_dashboard(client, d["id"])
# Crear cards
print("Creando cards...")
created_cards = []
for i, card_def in enumerate(CARDS):
card = metabase_create_card(
client,
name=card_def["name"],
dataset_query={
"database": registry_db_id,
"type": "native",
"native": {"query": card_def["sql"]},
},
display=card_def["display"],
description=f"fn-registry apps: {card_def['name']}",
)
created_cards.append((card, card_def))
print(f" [{i+1}/{len(CARDS)}] {card_def['name']} (id={card['id']})")
# Crear dashboard
print("Creando dashboard...")
dashboard = metabase_create_dashboard(
client,
name="fn-registry Apps",
description="Dashboard de apps del registry: distribucion por lenguaje, dominio, dependencias y catalogo completo.",
)
dash_id = dashboard["id"]
print(f" Dashboard creado: id={dash_id}")
# Agregar cards al dashboard con posiciones
dashcards = []
for idx, (card, card_def) in enumerate(created_cards):
dashcards.append({
"id": -(idx + 1),
"card_id": card["id"],
"size_x": card_def["size_x"],
"size_y": card_def["size_y"],
"col": card_def["col"],
"row": card_def["row"],
})
metabase_update_dashboard(client, dash_id, dashcards=dashcards)
print(f"\nDashboard listo: {METABASE_URL}/dashboard/{dash_id}")
client.close()
if __name__ == "__main__":
main()
@@ -19,61 +19,72 @@ METABASE_URL = "http://localhost:3000"
EMAIL = "admin@fnregistry.local" EMAIL = "admin@fnregistry.local"
PASSWORD = "FnRegistry2024!" PASSWORD = "FnRegistry2024!"
# --- Layout ---
# Grid de 24 unidades de ancho (estandar Metabase).
# Fila 0 (h=5): 5 scalars de 4-5 unidades cada uno → fila de KPIs
# Fila 5 (h=8): 3 graficas de 8 unidades → distribucion general
# Fila 13 (h=9): 3 graficas de 8 unidades → analisis profundo
# Fila 22 (h=8): 2 tablas de 12 unidades → cobertura + x lenguaje
# Fila 30 (h=8): 2 tablas de 12 unidades → tipos + recientes
# --- SQL Queries --- # --- SQL Queries ---
CARDS = [ CARDS = [
# ---- Fila 0: KPIs escalares (h=5) ----
{ {
"name": "Total de Funciones", "name": "Total de Funciones",
"display": "scalar", "display": "scalar",
"sql": "SELECT COUNT(*) AS total FROM functions;", "sql": "SELECT COUNT(*) AS total FROM functions;",
"size_x": 4, "size_y": 3, "col": 0, "row": 0, "size_x": 5, "size_y": 5, "col": 0, "row": 0,
}, },
{ {
"name": "Funciones con Tests", "name": "Funciones con Tests",
"display": "scalar", "display": "scalar",
"sql": "SELECT COUNT(*) AS con_tests FROM functions WHERE tested = 1;", "sql": "SELECT COUNT(*) AS con_tests FROM functions WHERE tested = 1;",
"size_x": 4, "size_y": 3, "col": 4, "row": 0, "size_x": 5, "size_y": 5, "col": 5, "row": 0,
}, },
{ {
"name": "Funciones sin Tests", "name": "Funciones sin Tests",
"display": "scalar", "display": "scalar",
"sql": "SELECT COUNT(*) AS sin_tests FROM functions WHERE tested = 0;", "sql": "SELECT COUNT(*) AS sin_tests FROM functions WHERE tested = 0;",
"size_x": 4, "size_y": 3, "col": 8, "row": 0, "size_x": 4, "size_y": 5, "col": 10, "row": 0,
}, },
{ {
"name": "Total de Tipos", "name": "Total de Tipos",
"display": "scalar", "display": "scalar",
"sql": "SELECT COUNT(*) AS total FROM types;", "sql": "SELECT COUNT(*) AS total FROM types;",
"size_x": 3, "size_y": 3, "col": 12, "row": 0, "size_x": 5, "size_y": 5, "col": 14, "row": 0,
}, },
{ {
"name": "Proposals Pendientes", "name": "Proposals Pendientes",
"display": "scalar", "display": "scalar",
"sql": "SELECT COUNT(*) AS pendientes FROM proposals WHERE status = 'pending';", "sql": "SELECT COUNT(*) AS pendientes FROM proposals WHERE status = 'pending';",
"size_x": 3, "size_y": 3, "col": 15, "row": 0, "size_x": 5, "size_y": 5, "col": 19, "row": 0,
}, },
# ---- Fila 5: Distribucion general (h=8) ----
{ {
"name": "Funciones por Lenguaje", "name": "Funciones por Lenguaje",
"display": "bar", "display": "bar",
"sql": "SELECT lang, COUNT(*) AS cantidad FROM functions GROUP BY lang ORDER BY cantidad DESC;", "sql": "SELECT lang, COUNT(*) AS cantidad FROM functions GROUP BY lang ORDER BY cantidad DESC;",
"size_x": 6, "size_y": 5, "col": 0, "row": 3, "size_x": 8, "size_y": 8, "col": 0, "row": 5,
}, },
{ {
"name": "Funciones por Dominio", "name": "Funciones por Dominio",
"display": "pie", "display": "pie",
"sql": "SELECT domain, COUNT(*) AS cantidad FROM functions GROUP BY domain ORDER BY cantidad DESC;", "sql": "SELECT domain, COUNT(*) AS cantidad FROM functions GROUP BY domain ORDER BY cantidad DESC;",
"size_x": 6, "size_y": 5, "col": 6, "row": 3, "size_x": 8, "size_y": 8, "col": 8, "row": 5,
}, },
{ {
"name": "Funciones por Kind", "name": "Funciones por Kind",
"display": "bar", "display": "bar",
"sql": "SELECT kind, COUNT(*) AS cantidad FROM functions GROUP BY kind ORDER BY cantidad DESC;", "sql": "SELECT kind, COUNT(*) AS cantidad FROM functions GROUP BY kind ORDER BY cantidad DESC;",
"size_x": 6, "size_y": 5, "col": 12, "row": 3, "size_x": 8, "size_y": 8, "col": 16, "row": 5,
}, },
# ---- Fila 13: Analisis profundo (h=9) ----
{ {
"name": "Puras vs Impuras", "name": "Puras vs Impuras",
"display": "pie", "display": "pie",
"sql": "SELECT purity, COUNT(*) AS cantidad FROM functions GROUP BY purity ORDER BY cantidad DESC;", "sql": "SELECT purity, COUNT(*) AS cantidad FROM functions GROUP BY purity ORDER BY cantidad DESC;",
"size_x": 6, "size_y": 5, "col": 0, "row": 8, "size_x": 8, "size_y": 9, "col": 0, "row": 13,
}, },
{ {
"name": "Funciones Mas Usadas por Otras", "name": "Funciones Mas Usadas por Otras",
@@ -94,7 +105,7 @@ CARDS = [
ORDER BY veces_usada DESC ORDER BY veces_usada DESC
LIMIT 15; LIMIT 15;
""", """,
"size_x": 6, "size_y": 5, "col": 6, "row": 8, "size_x": 8, "size_y": 9, "col": 8, "row": 13,
}, },
{ {
"name": "Funciones Mas Complejas (mas dependencias)", "name": "Funciones Mas Complejas (mas dependencias)",
@@ -111,8 +122,9 @@ CARDS = [
ORDER BY num_dependencias DESC ORDER BY num_dependencias DESC
LIMIT 15; LIMIT 15;
""", """,
"size_x": 6, "size_y": 5, "col": 12, "row": 8, "size_x": 8, "size_y": 9, "col": 16, "row": 13,
}, },
# ---- Fila 22: Cobertura + cross-table (h=8) ----
{ {
"name": "Cobertura de Tests por Dominio", "name": "Cobertura de Tests por Dominio",
"display": "bar", "display": "bar",
@@ -125,7 +137,7 @@ CARDS = [
GROUP BY domain GROUP BY domain
ORDER BY domain; ORDER BY domain;
""", """,
"size_x": 9, "size_y": 5, "col": 0, "row": 13, "size_x": 12, "size_y": 8, "col": 0, "row": 22,
}, },
{ {
"name": "Funciones por Lenguaje y Dominio", "name": "Funciones por Lenguaje y Dominio",
@@ -142,8 +154,9 @@ CARDS = [
GROUP BY domain GROUP BY domain
ORDER BY total DESC; ORDER BY total DESC;
""", """,
"size_x": 9, "size_y": 5, "col": 9, "row": 13, "size_x": 12, "size_y": 8, "col": 12, "row": 22,
}, },
# ---- Fila 30: Tipos + recientes (h=8) ----
{ {
"name": "Tipos por Dominio y Algebraic", "name": "Tipos por Dominio y Algebraic",
"display": "table", "display": "table",
@@ -156,7 +169,7 @@ CARDS = [
GROUP BY domain, algebraic GROUP BY domain, algebraic
ORDER BY domain, cantidad DESC; ORDER BY domain, cantidad DESC;
""", """,
"size_x": 9, "size_y": 4, "col": 0, "row": 18, "size_x": 12, "size_y": 8, "col": 0, "row": 30,
}, },
{ {
"name": "Funciones Recientes (ultimas 20 indexadas)", "name": "Funciones Recientes (ultimas 20 indexadas)",
@@ -167,7 +180,7 @@ CARDS = [
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT 20; LIMIT 20;
""", """,
"size_x": 9, "size_y": 4, "col": 9, "row": 18, "size_x": 12, "size_y": 8, "col": 12, "row": 30,
}, },
] ]