95bba767bc
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>
220 lines
7.4 KiB
Python
220 lines
7.4 KiB
Python
"""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()
|