From 95bba767bc7324812a2eeb869a39dbef99c42e6a Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sun, 29 Mar 2026 00:54:18 +0100 Subject: [PATCH] feat: dashboard apps y mejora layout del dashboard Overview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../create_apps_dashboard.py | 219 ++++++++++++++++++ .../create_registry_dashboard.py | 43 ++-- 2 files changed, 247 insertions(+), 15 deletions(-) create mode 100644 apps/metabase_registry/create_apps_dashboard.py diff --git a/apps/metabase_registry/create_apps_dashboard.py b/apps/metabase_registry/create_apps_dashboard.py new file mode 100644 index 00000000..f9d0055d --- /dev/null +++ b/apps/metabase_registry/create_apps_dashboard.py @@ -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() diff --git a/apps/metabase_registry/create_registry_dashboard.py b/apps/metabase_registry/create_registry_dashboard.py index 5cf556d5..fc2f6154 100644 --- a/apps/metabase_registry/create_registry_dashboard.py +++ b/apps/metabase_registry/create_registry_dashboard.py @@ -19,61 +19,72 @@ METABASE_URL = "http://localhost:3000" EMAIL = "admin@fnregistry.local" 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 --- CARDS = [ + # ---- Fila 0: KPIs escalares (h=5) ---- { "name": "Total de Funciones", "display": "scalar", "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", "display": "scalar", "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", "display": "scalar", "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", "display": "scalar", "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", "display": "scalar", "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", "display": "bar", "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", "display": "pie", "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", "display": "bar", "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", "display": "pie", "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", @@ -94,7 +105,7 @@ CARDS = [ ORDER BY veces_usada DESC 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)", @@ -111,8 +122,9 @@ CARDS = [ ORDER BY num_dependencias DESC 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", "display": "bar", @@ -125,7 +137,7 @@ CARDS = [ GROUP 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", @@ -142,8 +154,9 @@ CARDS = [ GROUP BY domain 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", "display": "table", @@ -156,7 +169,7 @@ CARDS = [ GROUP BY domain, algebraic 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)", @@ -167,7 +180,7 @@ CARDS = [ ORDER BY created_at DESC LIMIT 20; """, - "size_x": 9, "size_y": 4, "col": 9, "row": 18, + "size_x": 12, "size_y": 8, "col": 12, "row": 30, }, ]