"""Crea un dashboard en Metabase con metricas 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): 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": 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": 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": 5, "col": 10, "row": 0, }, { "name": "Total de Tipos", "display": "scalar", "sql": "SELECT COUNT(*) AS total FROM types;", "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": 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": 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": 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": 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": 8, "size_y": 9, "col": 0, "row": 13, }, { "name": "Funciones Mas Usadas por Otras", "display": "row", "sql": """ WITH RECURSIVE split_uses(fn_id, rest, val) AS ( SELECT id, uses_functions || ',', NULL FROM functions WHERE uses_functions != '[]' AND uses_functions != '' UNION ALL SELECT fn_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": 8, "size_y": 9, "col": 8, "row": 13, }, { "name": "Funciones Mas Complejas (mas dependencias)", "display": "row", "sql": """ SELECT name || ' (' || lang || ')' AS funcion, (LENGTH(uses_functions) - LENGTH(REPLACE(uses_functions, ',', '')) + CASE WHEN uses_functions != '[]' AND uses_functions != '' THEN 1 ELSE 0 END) AS num_dependencias, (LENGTH(uses_types) - LENGTH(REPLACE(uses_types, ',', '')) + CASE WHEN uses_types != '[]' AND uses_types != '' THEN 1 ELSE 0 END) AS num_tipos FROM functions WHERE uses_functions != '[]' AND uses_functions != '' ORDER BY num_dependencias DESC LIMIT 15; """, "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", "sql": """ SELECT domain, SUM(CASE WHEN tested = 1 THEN 1 ELSE 0 END) AS con_tests, SUM(CASE WHEN tested = 0 THEN 1 ELSE 0 END) AS sin_tests FROM functions GROUP BY domain ORDER BY domain; """, "size_x": 12, "size_y": 8, "col": 0, "row": 22, }, { "name": "Funciones por Lenguaje y Dominio", "display": "table", "sql": """ SELECT domain, SUM(CASE WHEN lang = 'go' THEN 1 ELSE 0 END) AS go, SUM(CASE WHEN lang = 'py' THEN 1 ELSE 0 END) AS python, SUM(CASE WHEN lang = 'bash' THEN 1 ELSE 0 END) AS bash, SUM(CASE WHEN lang = 'ts' THEN 1 ELSE 0 END) AS typescript, COUNT(*) AS total FROM functions GROUP BY domain ORDER BY total DESC; """, "size_x": 12, "size_y": 8, "col": 12, "row": 22, }, # ---- Fila 30: Tipos + recientes (h=8) ---- { "name": "Tipos por Dominio y Algebraic", "display": "table", "sql": """ SELECT domain, algebraic, COUNT(*) AS cantidad FROM types GROUP BY domain, algebraic ORDER BY domain, cantidad DESC; """, "size_x": 12, "size_y": 8, "col": 0, "row": 30, }, { "name": "Funciones Recientes (ultimas 20 indexadas)", "display": "table", "sql": """ SELECT name, lang, domain, kind, purity, tested FROM functions ORDER BY created_at DESC LIMIT 20; """, "size_x": 12, "size_y": 8, "col": 12, "row": 30, }, ] 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 Overview": 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: {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 Overview", description="Dashboard de metricas del registry: funciones, tipos, tests, dependencias y complejidad.", ) 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()