snowglobe-cli 0.2.2__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/PKG-INFO +1 -1
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/pyproject.toml +1 -1
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/core/cost_service.py +130 -1
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/tui/screens/cost.py +220 -25
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe_cli.egg-info/PKG-INFO +1 -1
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/LICENSE +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/README.md +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/setup.cfg +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/__init__.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/__main__.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/cli/__init__.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/cli/access.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/cli/app.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/cli/context.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/cli/cost.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/cli/debug.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/cli/diff.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/cli/optimizer.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/cli/prompts.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/cli/report.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/cli/shell.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/cli/shell_completer.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/collectors/access.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/collectors/query_history.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/collectors/query_profile.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/config/loader.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/core/access_service.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/core/optimizer.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/core/query_service.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/core/report_service.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/core/risk_service.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/engines/access/__init__.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/engines/access/explainer.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/engines/access/resolver.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/engines/ai/cortex_optimizer.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/engines/optimizer/query_optimizer.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/graphs/__init__.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/graphs/role_graph.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/graphs/user_graph.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/models/__init__.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/models/access.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/models/access_path.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/models/object_ref.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/models/object_type.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/models/optimizer.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/models/privilege.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/models/query.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/output/__init__.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/output/cli.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/queries/__init__.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/queries/query_history.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/snowflake/connection.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/state/db.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/state/state.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/templates/report.md.j2 +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/tests/access_tests.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/tui/__init__.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/tui/__main__.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/tui/app.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/tui/screens/__init__.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/tui/screens/access.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/tui/screens/home.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/tui/screens/refresh.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/tui/screens/reports.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/tui/screens/risk.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/tui/screens/tune.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/tui/styles.tcss +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/tui/widgets/__init__.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/tui/widgets/access_paths.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/tui/widgets/cache_badge.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/tui/widgets/header.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe/tui/widgets/nav.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe_cli.egg-info/SOURCES.txt +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe_cli.egg-info/dependency_links.txt +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe_cli.egg-info/entry_points.txt +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe_cli.egg-info/requires.txt +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/snowglobe_cli.egg-info/top_level.txt +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/tests/test_access.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/tests/test_optimizer_engine.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/tests/test_output.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/tests/test_query_validation.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/tests/test_state_db.py +0 -0
- {snowglobe_cli-0.2.2 → snowglobe_cli-0.2.4}/tests/test_tui_tune.py +0 -0
|
@@ -1171,7 +1171,7 @@ class CostService:
|
|
|
1171
1171
|
d = _sq(date)
|
|
1172
1172
|
svc_upper = service_type.upper()
|
|
1173
1173
|
|
|
1174
|
-
if
|
|
1174
|
+
if "WAREHOUSE" in svc_upper:
|
|
1175
1175
|
safe_wh = _sq(resource)
|
|
1176
1176
|
conn = self.context.connect()
|
|
1177
1177
|
try:
|
|
@@ -1335,3 +1335,132 @@ class CostService:
|
|
|
1335
1335
|
return df, "Query Attribution not enabled — showing slowest queries"
|
|
1336
1336
|
except Exception:
|
|
1337
1337
|
return pd.DataFrame(), "Could not load queries"
|
|
1338
|
+
|
|
1339
|
+
def get_user_warehouse_queries(
|
|
1340
|
+
self, user_name: str, warehouse_name: str, days: int = 7, limit: int = 10
|
|
1341
|
+
) -> tuple[pd.DataFrame, str | None]:
|
|
1342
|
+
"""Top queries for a user on a specific warehouse over the past N days."""
|
|
1343
|
+
safe_user = _sq(user_name)
|
|
1344
|
+
safe_wh = _sq(warehouse_name)
|
|
1345
|
+
conn = self.context.connect()
|
|
1346
|
+
try:
|
|
1347
|
+
with conn:
|
|
1348
|
+
rows = conn.query(f"""
|
|
1349
|
+
SELECT a.QUERY_ID,
|
|
1350
|
+
ROUND(a.CREDITS_ATTRIBUTED_COMPUTE, 4) AS CREDITS,
|
|
1351
|
+
q.QUERY_TYPE,
|
|
1352
|
+
LEFT(q.QUERY_TEXT, 80) AS QUERY_PREVIEW,
|
|
1353
|
+
ROUND(q.TOTAL_ELAPSED_TIME / 1000, 1) AS SECONDS,
|
|
1354
|
+
ROUND(q.BYTES_SCANNED / 1e9, 2) AS GB_SCANNED
|
|
1355
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_ATTRIBUTION_HISTORY a
|
|
1356
|
+
LEFT JOIN SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY q ON a.QUERY_ID = q.QUERY_ID
|
|
1357
|
+
WHERE a.START_TIME >= DATEADD('day', -{days}, CURRENT_TIMESTAMP())
|
|
1358
|
+
AND a.WAREHOUSE_NAME = '{safe_wh}'
|
|
1359
|
+
AND a.USER_NAME = '{safe_user}'
|
|
1360
|
+
ORDER BY a.CREDITS_ATTRIBUTED_COMPUTE DESC
|
|
1361
|
+
LIMIT {limit}
|
|
1362
|
+
""")
|
|
1363
|
+
if not rows:
|
|
1364
|
+
return pd.DataFrame(), None
|
|
1365
|
+
df = pd.DataFrame(rows)
|
|
1366
|
+
df["CREDITS"] = df["CREDITS"].astype(float)
|
|
1367
|
+
return df, None
|
|
1368
|
+
except Exception:
|
|
1369
|
+
pass
|
|
1370
|
+
|
|
1371
|
+
conn2 = self.context.connect()
|
|
1372
|
+
try:
|
|
1373
|
+
with conn2:
|
|
1374
|
+
rows = conn2.query(f"""
|
|
1375
|
+
SELECT QUERY_ID, 0.0 AS CREDITS, QUERY_TYPE,
|
|
1376
|
+
LEFT(QUERY_TEXT, 80) AS QUERY_PREVIEW,
|
|
1377
|
+
ROUND(TOTAL_ELAPSED_TIME / 1000, 1) AS SECONDS,
|
|
1378
|
+
ROUND(BYTES_SCANNED / 1e9, 2) AS GB_SCANNED
|
|
1379
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY
|
|
1380
|
+
WHERE START_TIME >= DATEADD('day', -{days}, CURRENT_TIMESTAMP())
|
|
1381
|
+
AND WAREHOUSE_NAME = '{safe_wh}'
|
|
1382
|
+
AND USER_NAME = '{safe_user}'
|
|
1383
|
+
AND EXECUTION_STATUS = 'SUCCESS'
|
|
1384
|
+
ORDER BY TOTAL_ELAPSED_TIME DESC
|
|
1385
|
+
LIMIT {limit}
|
|
1386
|
+
""")
|
|
1387
|
+
if not rows:
|
|
1388
|
+
return pd.DataFrame(), "Query Attribution not enabled — showing slowest queries"
|
|
1389
|
+
df = pd.DataFrame(rows)
|
|
1390
|
+
df["CREDITS"] = df["CREDITS"].astype(float)
|
|
1391
|
+
return df, "Query Attribution not enabled — showing slowest queries"
|
|
1392
|
+
except Exception:
|
|
1393
|
+
return pd.DataFrame(), "Could not load queries"
|
|
1394
|
+
|
|
1395
|
+
def get_ai_service_users(self, service_name: str, days: int = 30) -> tuple[pd.DataFrame, str | None]:
|
|
1396
|
+
"""Users of a specific AI/Cortex service over the past N days."""
|
|
1397
|
+
_empty = pd.DataFrame(columns=["USER_NAME", "CREDITS", "REQUESTS"])
|
|
1398
|
+
sql_map = {
|
|
1399
|
+
"Cortex Functions": f"""
|
|
1400
|
+
SELECT u.LOGIN_NAME AS USER_NAME,
|
|
1401
|
+
ROUND(SUM(f.credits), 4) AS CREDITS, COUNT(*) AS REQUESTS
|
|
1402
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_AI_FUNCTIONS_USAGE_HISTORY f
|
|
1403
|
+
INNER JOIN SNOWFLAKE.ACCOUNT_USAGE.USERS u ON u.USER_ID = f.USER_ID
|
|
1404
|
+
WHERE f.START_TIME >= DATEADD('day', -{days}, CURRENT_TIMESTAMP())
|
|
1405
|
+
AND f.credits > 0
|
|
1406
|
+
GROUP BY 1 ORDER BY 2 DESC LIMIT 50
|
|
1407
|
+
""",
|
|
1408
|
+
"Cortex Analyst": f"""
|
|
1409
|
+
SELECT USERNAME AS USER_NAME,
|
|
1410
|
+
ROUND(SUM(credits), 4) AS CREDITS, COUNT(*) AS REQUESTS
|
|
1411
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_ANALYST_USAGE_HISTORY
|
|
1412
|
+
WHERE START_TIME >= DATEADD('day', -{days}, CURRENT_TIMESTAMP())
|
|
1413
|
+
AND credits > 0
|
|
1414
|
+
GROUP BY 1 ORDER BY 2 DESC LIMIT 50
|
|
1415
|
+
""",
|
|
1416
|
+
"Cortex Agent": f"""
|
|
1417
|
+
SELECT USER_NAME,
|
|
1418
|
+
ROUND(SUM(token_credits), 4) AS CREDITS, COUNT(*) AS REQUESTS
|
|
1419
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_AGENT_USAGE_HISTORY
|
|
1420
|
+
WHERE START_TIME >= DATEADD('day', -{days}, CURRENT_TIMESTAMP())
|
|
1421
|
+
AND token_credits > 0
|
|
1422
|
+
GROUP BY 1 ORDER BY 2 DESC LIMIT 50
|
|
1423
|
+
""",
|
|
1424
|
+
"Cortex Code": f"""
|
|
1425
|
+
SELECT USER_NAME,
|
|
1426
|
+
ROUND(SUM(token_credits), 4) AS CREDITS, COUNT(*) AS REQUESTS
|
|
1427
|
+
FROM (
|
|
1428
|
+
SELECT USER_NAME, token_credits
|
|
1429
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_CODE_CLI_USAGE_HISTORY
|
|
1430
|
+
WHERE USAGE_TIME >= DATEADD('day', -{days}, CURRENT_TIMESTAMP())
|
|
1431
|
+
AND token_credits > 0
|
|
1432
|
+
UNION ALL
|
|
1433
|
+
SELECT USER_NAME, token_credits
|
|
1434
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_CODE_SNOWSIGHT_USAGE_HISTORY
|
|
1435
|
+
WHERE USAGE_TIME >= DATEADD('day', -{days}, CURRENT_TIMESTAMP())
|
|
1436
|
+
AND token_credits > 0
|
|
1437
|
+
UNION ALL
|
|
1438
|
+
SELECT USER_NAME, token_credits
|
|
1439
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_CODE_DESKTOP_USAGE_HISTORY
|
|
1440
|
+
WHERE USAGE_TIME >= DATEADD('day', -{days}, CURRENT_TIMESTAMP())
|
|
1441
|
+
AND token_credits > 0
|
|
1442
|
+
) GROUP BY 1 ORDER BY 2 DESC LIMIT 50
|
|
1443
|
+
""",
|
|
1444
|
+
"Snowflake Intelligence": f"""
|
|
1445
|
+
SELECT USER_NAME,
|
|
1446
|
+
ROUND(SUM(token_credits), 4) AS CREDITS, COUNT(*) AS REQUESTS
|
|
1447
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.SNOWFLAKE_INTELLIGENCE_USAGE_HISTORY
|
|
1448
|
+
WHERE START_TIME >= DATEADD('day', -{days}, CURRENT_TIMESTAMP())
|
|
1449
|
+
AND token_credits > 0
|
|
1450
|
+
GROUP BY 1 ORDER BY 2 DESC LIMIT 50
|
|
1451
|
+
""",
|
|
1452
|
+
}
|
|
1453
|
+
sql = sql_map.get(service_name)
|
|
1454
|
+
if not sql:
|
|
1455
|
+
return _empty, f"No user breakdown available for: {service_name}"
|
|
1456
|
+
conn = self.context.connect()
|
|
1457
|
+
try:
|
|
1458
|
+
with conn:
|
|
1459
|
+
rows = conn.query(sql)
|
|
1460
|
+
if not rows:
|
|
1461
|
+
return _empty, None
|
|
1462
|
+
df = pd.DataFrame(rows)
|
|
1463
|
+
df["CREDITS"] = df["CREDITS"].astype(float)
|
|
1464
|
+
return df, None
|
|
1465
|
+
except Exception:
|
|
1466
|
+
return _empty, "Could not load user breakdown"
|
|
@@ -40,16 +40,22 @@ class CostScreen(Vertical):
|
|
|
40
40
|
|
|
41
41
|
# Active view: "summary" | "trend" | "top_queries" | "warehouses" | "users"
|
|
42
42
|
# | "ai" | "ai_users" | "services" | "storage" | "replication" | "mv" | "budget"
|
|
43
|
-
# | "drill_service" | "drill_warehouse" | "drill_user"
|
|
43
|
+
# | "drill_service" | "drill_warehouse" | "drill_user" | "drill_user_queries"
|
|
44
|
+
# | "drill_ai_service"
|
|
44
45
|
# | "drill_day" | "drill_day_resource" | "drill_day_resource_users" | "drill_day_user_queries"
|
|
45
46
|
_current_view: str = "summary"
|
|
46
47
|
_drill_service: str | None = None
|
|
47
48
|
_drill_warehouse: str | None = None
|
|
48
49
|
_drill_user: str | None = None
|
|
50
|
+
_drill_user_warehouse: str | None = None # warehouse selected in drill_user
|
|
51
|
+
_drill_ai_service: str | None = None # AI service selected in ai view
|
|
49
52
|
_drill_day: str | None = None
|
|
50
53
|
_drill_day_service_type: str | None = None
|
|
51
54
|
_drill_day_resource: str | None = None
|
|
52
55
|
_drill_day_user: str | None = None
|
|
56
|
+
_drill_day_resource_kind: str = "other" # "warehouse" | "ai" | "other"
|
|
57
|
+
_drill_day_parent: str = "trend" # "trend" | "drill_service"
|
|
58
|
+
_drill_resource_users_parent: str = "drill_day_resource" # or "drill_warehouse"
|
|
53
59
|
# Becomes True after the user explicitly picks a view, so Select.Changed
|
|
54
60
|
# events fired during the initial mount don't auto-trigger a Snowflake fetch.
|
|
55
61
|
_user_initiated: bool = False
|
|
@@ -146,16 +152,37 @@ class CostScreen(Vertical):
|
|
|
146
152
|
elif self._current_view == "trend":
|
|
147
153
|
self._drill_day = key
|
|
148
154
|
self._fetch_day_drill(key)
|
|
155
|
+
elif self._current_view == "drill_service":
|
|
156
|
+
# Date row in service daily trend → full day drill (same chain as Trend)
|
|
157
|
+
self._drill_day_parent = "drill_service"
|
|
158
|
+
self._fetch_day_drill(key)
|
|
159
|
+
elif self._current_view == "drill_warehouse":
|
|
160
|
+
# Date row in warehouse daily trend → jump to Level 3 (users on that day)
|
|
161
|
+
self._drill_day = key
|
|
162
|
+
self._drill_day_service_type = "WAREHOUSE_METERING"
|
|
163
|
+
self._drill_day_resource_kind = "warehouse"
|
|
164
|
+
self._drill_resource_users_parent = "drill_warehouse"
|
|
165
|
+
self._fetch_day_resource_users(key, "WAREHOUSE_METERING", self._drill_warehouse)
|
|
166
|
+
elif self._current_view == "drill_user":
|
|
167
|
+
# Warehouse row in user detail → top queries for user+warehouse
|
|
168
|
+
self._fetch_user_queries(self._drill_user, key)
|
|
169
|
+
elif self._current_view == "ai":
|
|
170
|
+
self._fetch_ai_service_users(key)
|
|
171
|
+
elif self._current_view == "drill_user_queries":
|
|
172
|
+
try:
|
|
173
|
+
from textual.widgets import ContentSwitcher, Input
|
|
174
|
+
from snowglobe.tui.screens.tune import TuneScreen
|
|
175
|
+
self.app.query_one(ContentSwitcher).current = "tune"
|
|
176
|
+
tune = self.app.query_one(TuneScreen)
|
|
177
|
+
tune.query_one("#tu-query-id", Input).value = key
|
|
178
|
+
self.app.notify(f"Loaded {key[:24]}… into Tune. Press Analyse.", timeout=4)
|
|
179
|
+
except Exception:
|
|
180
|
+
self.app.notify(f"Query: {key[:24]}…", timeout=4)
|
|
149
181
|
elif self._current_view == "drill_day":
|
|
150
182
|
self._drill_day_service_type = key
|
|
151
183
|
self._fetch_day_resource_drill(self._drill_day, key)
|
|
152
184
|
elif self._current_view == "drill_day_resource":
|
|
153
|
-
|
|
154
|
-
can_drill = (
|
|
155
|
-
upper == _WAREHOUSE_SERVICE_TYPE
|
|
156
|
-
or any(kw in upper for kw in _AI_SERVICE_KEYWORDS)
|
|
157
|
-
)
|
|
158
|
-
if can_drill:
|
|
185
|
+
if self._drill_day_resource_kind in ("warehouse", "ai"):
|
|
159
186
|
self._fetch_day_resource_users(
|
|
160
187
|
self._drill_day, self._drill_day_service_type, key
|
|
161
188
|
)
|
|
@@ -164,7 +191,7 @@ class CostScreen(Vertical):
|
|
|
164
191
|
"No user-level detail available for this resource type.", timeout=3
|
|
165
192
|
)
|
|
166
193
|
elif self._current_view == "drill_day_resource_users":
|
|
167
|
-
if
|
|
194
|
+
if self._drill_day_resource_kind == "warehouse":
|
|
168
195
|
self._fetch_day_user_queries(self._drill_day, self._drill_day_resource, key)
|
|
169
196
|
else:
|
|
170
197
|
self.app.notify("No query detail available for AI services.", timeout=3)
|
|
@@ -221,6 +248,10 @@ class CostScreen(Vertical):
|
|
|
221
248
|
self._fetch_warehouse_drill(self._drill_warehouse)
|
|
222
249
|
elif self._current_view == "drill_user" and self._drill_user:
|
|
223
250
|
self._fetch_user_drill(self._drill_user)
|
|
251
|
+
elif self._current_view == "drill_user_queries" and self._drill_user and self._drill_user_warehouse:
|
|
252
|
+
self._fetch_user_queries(self._drill_user, self._drill_user_warehouse)
|
|
253
|
+
elif self._current_view == "drill_ai_service" and self._drill_ai_service:
|
|
254
|
+
self._fetch_ai_service_users(self._drill_ai_service)
|
|
224
255
|
elif self._current_view == "drill_day" and self._drill_day:
|
|
225
256
|
self._fetch_day_drill(self._drill_day)
|
|
226
257
|
elif self._current_view == "drill_day_resource" and self._drill_day and self._drill_day_service_type:
|
|
@@ -246,16 +277,30 @@ class CostScreen(Vertical):
|
|
|
246
277
|
elif self._current_view == "drill_user":
|
|
247
278
|
self._drill_user = None
|
|
248
279
|
self._fetch_users()
|
|
280
|
+
elif self._current_view == "drill_user_queries":
|
|
281
|
+
self._drill_user_warehouse = None
|
|
282
|
+
self._fetch_user_drill(self._drill_user)
|
|
283
|
+
elif self._current_view == "drill_ai_service":
|
|
284
|
+
self._drill_ai_service = None
|
|
285
|
+
self._fetch_ai()
|
|
249
286
|
elif self._current_view == "drill_day":
|
|
250
287
|
self._drill_day = None
|
|
251
288
|
self._drill_day_service_type = None
|
|
252
|
-
self.
|
|
289
|
+
if self._drill_day_parent == "drill_service":
|
|
290
|
+
self._drill_day_parent = "trend"
|
|
291
|
+
self._fetch_service_drill(self._drill_service)
|
|
292
|
+
else:
|
|
293
|
+
self._fetch_trend()
|
|
253
294
|
elif self._current_view == "drill_day_resource":
|
|
254
295
|
self._drill_day_service_type = None
|
|
255
296
|
self._fetch_day_drill(self._drill_day)
|
|
256
297
|
elif self._current_view == "drill_day_resource_users":
|
|
257
298
|
self._drill_day_resource = None
|
|
258
|
-
self.
|
|
299
|
+
if self._drill_resource_users_parent == "drill_warehouse":
|
|
300
|
+
self._drill_resource_users_parent = "drill_day_resource"
|
|
301
|
+
self._fetch_warehouse_drill(self._drill_warehouse)
|
|
302
|
+
else:
|
|
303
|
+
self._fetch_day_resource_drill(self._drill_day, self._drill_day_service_type)
|
|
259
304
|
elif self._current_view == "drill_day_user_queries":
|
|
260
305
|
self._drill_day_user = None
|
|
261
306
|
self._fetch_day_resource_users(
|
|
@@ -317,6 +362,7 @@ class CostScreen(Vertical):
|
|
|
317
362
|
|
|
318
363
|
def _fetch_trend(self, force: bool = False) -> None:
|
|
319
364
|
self._current_view = "trend"
|
|
365
|
+
self._reset_table()
|
|
320
366
|
self._set_status(f"Loading daily trend ({self._days()}d)…")
|
|
321
367
|
self._trend_worker(days=self._days(), force=force)
|
|
322
368
|
|
|
@@ -330,6 +376,8 @@ class CostScreen(Vertical):
|
|
|
330
376
|
self.app.call_from_thread(self._render_trend, df, days, cache_age)
|
|
331
377
|
|
|
332
378
|
def _render_trend(self, df: pd.DataFrame, days: int, cache_age: int | None) -> None:
|
|
379
|
+
if self._current_view != "trend":
|
|
380
|
+
return # stale callback
|
|
333
381
|
if df is None or df.empty:
|
|
334
382
|
self._set_status("No trend data found.")
|
|
335
383
|
self._clear_table()
|
|
@@ -417,6 +465,7 @@ class CostScreen(Vertical):
|
|
|
417
465
|
|
|
418
466
|
def _fetch_service_drill(self, service_type: str, force: bool = False) -> None:
|
|
419
467
|
self._current_view = "drill_service"
|
|
468
|
+
self._reset_table()
|
|
420
469
|
self._set_status(f"Loading daily trend for {service_type} ({self._days()}d)… Esc to return")
|
|
421
470
|
self._drill_worker(service_type=service_type, days=self._days())
|
|
422
471
|
|
|
@@ -430,15 +479,17 @@ class CostScreen(Vertical):
|
|
|
430
479
|
self.app.call_from_thread(self._render_service_drill, df, service_type, days)
|
|
431
480
|
|
|
432
481
|
def _render_service_drill(self, df: pd.DataFrame, service_type: str, days: int) -> None:
|
|
482
|
+
if self._current_view != "drill_service":
|
|
483
|
+
return
|
|
433
484
|
if df is None or df.empty:
|
|
434
485
|
self._set_status(f"No daily data for {service_type} ({days}d). Esc to return.")
|
|
435
486
|
self._clear_table()
|
|
436
487
|
return
|
|
437
488
|
total = float(df["CREDITS"].sum())
|
|
438
489
|
self._set_status(
|
|
439
|
-
f"{service_type} daily trend — {days}d · total {total:,.2f}
|
|
490
|
+
f"{service_type} daily trend — {days}d · total {total:,.2f}"
|
|
491
|
+
f" · click date to drill · Esc to return"
|
|
440
492
|
)
|
|
441
|
-
|
|
442
493
|
table = self.query_one(DataTable)
|
|
443
494
|
self._reset_table()
|
|
444
495
|
table.add_columns("DATE", "CREDITS", "TREND")
|
|
@@ -446,7 +497,8 @@ class CostScreen(Vertical):
|
|
|
446
497
|
for _, row in df.iterrows():
|
|
447
498
|
credits = float(row["CREDITS"])
|
|
448
499
|
ratio = credits / max_c if max_c > 0 else 0
|
|
449
|
-
table.add_row(str(row["DATE"]), f"{credits:,.2f}", _bar(ratio)
|
|
500
|
+
table.add_row(str(row["DATE"]), f"{credits:,.2f}", _bar(ratio),
|
|
501
|
+
key=str(row["DATE"]))
|
|
450
502
|
|
|
451
503
|
# --- View 4: Warehouses ------------------------------------------
|
|
452
504
|
|
|
@@ -489,6 +541,7 @@ class CostScreen(Vertical):
|
|
|
489
541
|
|
|
490
542
|
def _fetch_warehouse_drill(self, warehouse_name: str) -> None:
|
|
491
543
|
self._current_view = "drill_warehouse"
|
|
544
|
+
self._reset_table()
|
|
492
545
|
self._set_status(f"Loading daily trend for {warehouse_name} ({self._days()}d)… Esc to return")
|
|
493
546
|
self._warehouse_drill_worker(warehouse_name=warehouse_name, days=self._days())
|
|
494
547
|
|
|
@@ -502,13 +555,16 @@ class CostScreen(Vertical):
|
|
|
502
555
|
self.app.call_from_thread(self._render_warehouse_drill, df, warehouse_name, days)
|
|
503
556
|
|
|
504
557
|
def _render_warehouse_drill(self, df: pd.DataFrame, warehouse_name: str, days: int) -> None:
|
|
558
|
+
if self._current_view != "drill_warehouse":
|
|
559
|
+
return
|
|
505
560
|
if df is None or df.empty:
|
|
506
561
|
self._set_status(f"No daily data for {warehouse_name} ({days}d). Esc to return")
|
|
507
562
|
self._clear_table()
|
|
508
563
|
return
|
|
509
564
|
total = float(df["CREDITS"].sum())
|
|
510
565
|
self._set_status(
|
|
511
|
-
f"{warehouse_name} daily — {days}d · total {total:,.2f}
|
|
566
|
+
f"{warehouse_name} daily — {days}d · total {total:,.2f}"
|
|
567
|
+
f" · click date to see users · Esc to return"
|
|
512
568
|
)
|
|
513
569
|
table = self.query_one(DataTable)
|
|
514
570
|
self._reset_table()
|
|
@@ -517,7 +573,8 @@ class CostScreen(Vertical):
|
|
|
517
573
|
for _, row in df.iterrows():
|
|
518
574
|
credits = float(row["CREDITS"])
|
|
519
575
|
ratio = credits / max_c if max_c > 0 else 0
|
|
520
|
-
table.add_row(str(row["DATE"]), f"{credits:,.2f}", _bar(ratio)
|
|
576
|
+
table.add_row(str(row["DATE"]), f"{credits:,.2f}", _bar(ratio),
|
|
577
|
+
key=str(row["DATE"]))
|
|
521
578
|
|
|
522
579
|
# --- View 5: Users -----------------------------------------------
|
|
523
580
|
|
|
@@ -571,6 +628,7 @@ class CostScreen(Vertical):
|
|
|
571
628
|
|
|
572
629
|
def _fetch_user_drill(self, user_name: str) -> None:
|
|
573
630
|
self._current_view = "drill_user"
|
|
631
|
+
self._reset_table()
|
|
574
632
|
self._set_status(f"Loading warehouse detail for {user_name} ({min(self._days(),7)}d)… Esc to return")
|
|
575
633
|
self._user_drill_worker(user_name=user_name, days=min(self._days(), 7))
|
|
576
634
|
|
|
@@ -584,6 +642,8 @@ class CostScreen(Vertical):
|
|
|
584
642
|
self.app.call_from_thread(self._render_user_drill, df, user_name, days, note)
|
|
585
643
|
|
|
586
644
|
def _render_user_drill(self, df: pd.DataFrame, user_name: str, days: int, note: str | None) -> None:
|
|
645
|
+
if self._current_view != "drill_user":
|
|
646
|
+
return
|
|
587
647
|
if df is None or df.empty:
|
|
588
648
|
self._set_status(f"No attribution data for {user_name} ({days}d). Esc to return")
|
|
589
649
|
self._clear_table()
|
|
@@ -591,7 +651,8 @@ class CostScreen(Vertical):
|
|
|
591
651
|
total = float(df["CREDITS"].sum())
|
|
592
652
|
note_suffix = f" · ⚠ {note}" if note else ""
|
|
593
653
|
self._set_status(
|
|
594
|
-
f"{user_name} per-warehouse — {days}d · total {total:,.4f}{note_suffix}
|
|
654
|
+
f"{user_name} per-warehouse — {days}d · total {total:,.4f}{note_suffix}"
|
|
655
|
+
f" · click warehouse for top queries · Esc to return"
|
|
595
656
|
)
|
|
596
657
|
no_credits = note is not None
|
|
597
658
|
table = self.query_one(DataTable)
|
|
@@ -605,12 +666,117 @@ class CostScreen(Vertical):
|
|
|
605
666
|
credits_str,
|
|
606
667
|
str(int(row.get("QUERY_COUNT", 0))),
|
|
607
668
|
avg_str,
|
|
669
|
+
key=str(row.get("WAREHOUSE_NAME", "")),
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
# --- Drill: top queries for user+warehouse (from drill_user) -----
|
|
673
|
+
|
|
674
|
+
def _fetch_user_queries(self, user_name: str, warehouse_name: str) -> None:
|
|
675
|
+
self._current_view = "drill_user_queries"
|
|
676
|
+
self._drill_user_warehouse = warehouse_name
|
|
677
|
+
self._reset_table()
|
|
678
|
+
self._set_status(
|
|
679
|
+
f"Loading top queries for {user_name} on {warehouse_name}… Esc to return"
|
|
680
|
+
)
|
|
681
|
+
self._user_queries_worker(
|
|
682
|
+
user_name=user_name, warehouse_name=warehouse_name, days=min(self._days(), 7)
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
@work(thread=True, exclusive=True, group="cost")
|
|
686
|
+
def _user_queries_worker(self, *, user_name: str, warehouse_name: str, days: int) -> None:
|
|
687
|
+
try:
|
|
688
|
+
df, note = self.app.get_cost_service().get_user_warehouse_queries(
|
|
689
|
+
user_name, warehouse_name, days
|
|
690
|
+
)
|
|
691
|
+
except Exception as e:
|
|
692
|
+
self.app.call_from_thread(self._fetch_failed, e)
|
|
693
|
+
return
|
|
694
|
+
self.app.call_from_thread(self._render_user_queries, df, user_name, warehouse_name, note)
|
|
695
|
+
|
|
696
|
+
def _render_user_queries(
|
|
697
|
+
self, df: pd.DataFrame, user_name: str, warehouse_name: str, note: str | None
|
|
698
|
+
) -> None:
|
|
699
|
+
if self._current_view != "drill_user_queries":
|
|
700
|
+
return
|
|
701
|
+
self._reset_table()
|
|
702
|
+
table = self.query_one(DataTable)
|
|
703
|
+
note_suffix = f" · ⚠ {note}" if note else ""
|
|
704
|
+
if df is None or df.empty:
|
|
705
|
+
table.add_columns("(no queries found)")
|
|
706
|
+
self._set_status(
|
|
707
|
+
f"{user_name} on {warehouse_name} · no queries{note_suffix} · Esc to return"
|
|
708
|
+
)
|
|
709
|
+
return
|
|
710
|
+
table.add_columns("CREDITS", "TYPE", "SECONDS", "GB", "PREVIEW")
|
|
711
|
+
for _, row in df.iterrows():
|
|
712
|
+
table.add_row(
|
|
713
|
+
f"{float(row.get('CREDITS', 0)):,.4f}",
|
|
714
|
+
str(row.get("QUERY_TYPE", "")),
|
|
715
|
+
f"{float(row.get('SECONDS', 0)):.1f}",
|
|
716
|
+
f"{float(row.get('GB_SCANNED', 0)):.2f}",
|
|
717
|
+
str(row.get("QUERY_PREVIEW", ""))[:48],
|
|
718
|
+
key=str(row.get("QUERY_ID", "")),
|
|
608
719
|
)
|
|
720
|
+
self._set_status(
|
|
721
|
+
f"Top queries — {user_name} on {warehouse_name}{note_suffix}"
|
|
722
|
+
f" · ⏎ open in Tune · Esc to return"
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
# --- Drill: users of an AI service (from ai view) ----------------
|
|
726
|
+
|
|
727
|
+
def _fetch_ai_service_users(self, service_name: str) -> None:
|
|
728
|
+
self._current_view = "drill_ai_service"
|
|
729
|
+
self._drill_ai_service = service_name
|
|
730
|
+
self._reset_table()
|
|
731
|
+
self._set_status(
|
|
732
|
+
f"Loading users for {service_name} ({self._days()}d)… Esc to return"
|
|
733
|
+
)
|
|
734
|
+
self._ai_service_users_worker(service_name=service_name, days=self._days())
|
|
735
|
+
|
|
736
|
+
@work(thread=True, exclusive=True, group="cost")
|
|
737
|
+
def _ai_service_users_worker(self, *, service_name: str, days: int) -> None:
|
|
738
|
+
try:
|
|
739
|
+
df, note = self.app.get_cost_service().get_ai_service_users(service_name, days)
|
|
740
|
+
except Exception as e:
|
|
741
|
+
self.app.call_from_thread(self._fetch_failed, e)
|
|
742
|
+
return
|
|
743
|
+
self.app.call_from_thread(self._render_ai_service_users, df, service_name, days, note)
|
|
744
|
+
|
|
745
|
+
def _render_ai_service_users(
|
|
746
|
+
self, df: pd.DataFrame, service_name: str, days: int, note: str | None
|
|
747
|
+
) -> None:
|
|
748
|
+
if self._current_view != "drill_ai_service":
|
|
749
|
+
return
|
|
750
|
+
self._reset_table()
|
|
751
|
+
table = self.query_one(DataTable)
|
|
752
|
+
note_suffix = f" · ⚠ {note}" if note else ""
|
|
753
|
+
if df is None or df.empty:
|
|
754
|
+
table.add_columns("(no users found)")
|
|
755
|
+
self._set_status(f"{service_name} · no users{note_suffix} · Esc to return")
|
|
756
|
+
return
|
|
757
|
+
total = float(df["CREDITS"].sum())
|
|
758
|
+
max_c = float(df["CREDITS"].max()) if total > 0 else 1.0
|
|
759
|
+
table.add_columns("USER", "CREDITS", "REQUESTS", "% OF SERVICE", "BAR")
|
|
760
|
+
for _, row in df.iterrows():
|
|
761
|
+
credits = float(row["CREDITS"])
|
|
762
|
+
pct = (credits / total * 100) if total > 0 else 0
|
|
763
|
+
table.add_row(
|
|
764
|
+
str(row["USER_NAME"]),
|
|
765
|
+
f"{credits:,.4f}",
|
|
766
|
+
str(int(row.get("REQUESTS", 0))),
|
|
767
|
+
f"{pct:.1f}%",
|
|
768
|
+
_bar(credits / max_c),
|
|
769
|
+
)
|
|
770
|
+
self._set_status(
|
|
771
|
+
f"{service_name} users — {days}d · total {total:,.4f}{note_suffix} · Esc to return"
|
|
772
|
+
)
|
|
609
773
|
|
|
610
774
|
# --- Drill: day breakdown (Level 1 — service types) --------------
|
|
611
775
|
|
|
612
776
|
def _fetch_day_drill(self, date: str) -> None:
|
|
777
|
+
self._drill_day = date # always set — this may be called from home.py or back-nav
|
|
613
778
|
self._current_view = "drill_day"
|
|
779
|
+
self._reset_table() # clear immediately so stale rows can't be re-clicked while loading
|
|
614
780
|
self._set_status(f"Loading service breakdown for {date}… Esc to return")
|
|
615
781
|
self._day_drill_worker(date=date)
|
|
616
782
|
|
|
@@ -624,6 +790,8 @@ class CostScreen(Vertical):
|
|
|
624
790
|
self.app.call_from_thread(self._render_day_drill, df, date)
|
|
625
791
|
|
|
626
792
|
def _render_day_drill(self, df: pd.DataFrame, date: str) -> None:
|
|
793
|
+
if self._current_view != "drill_day":
|
|
794
|
+
return # stale callback — view advanced before this fired
|
|
627
795
|
self._reset_table()
|
|
628
796
|
table = self.query_one(DataTable)
|
|
629
797
|
if df is None or df.empty:
|
|
@@ -652,13 +820,28 @@ class CostScreen(Vertical):
|
|
|
652
820
|
def _fetch_day_resource_drill(self, date: str, service_type: str) -> None:
|
|
653
821
|
self._current_view = "drill_day_resource"
|
|
654
822
|
self._drill_day_service_type = service_type
|
|
655
|
-
self._set_status(f"Loading {service_type} breakdown for {date}… Esc to return")
|
|
656
823
|
upper = service_type.upper()
|
|
657
824
|
is_ai = any(kw in upper for kw in _AI_SERVICE_KEYWORDS)
|
|
658
|
-
|
|
825
|
+
is_warehouse = "WAREHOUSE" in upper
|
|
826
|
+
# Set kind synchronously — before async work — so navigation is correct
|
|
827
|
+
# even if the worker fails and the render never fires.
|
|
828
|
+
if is_ai:
|
|
829
|
+
self._drill_day_resource_kind = "ai"
|
|
830
|
+
elif is_warehouse:
|
|
831
|
+
self._drill_day_resource_kind = "warehouse"
|
|
832
|
+
else:
|
|
833
|
+
self._drill_day_resource_kind = "other"
|
|
834
|
+
# Clear table immediately so stale Level 1 rows can't be clicked while loading.
|
|
835
|
+
self._reset_table()
|
|
836
|
+
self._set_status(f"Loading {service_type} breakdown for {date}… Esc to return")
|
|
837
|
+
self._day_resource_drill_worker(
|
|
838
|
+
date=date, service_type=service_type, is_ai=is_ai, is_warehouse=is_warehouse
|
|
839
|
+
)
|
|
659
840
|
|
|
660
841
|
@work(thread=True, exclusive=True, group="cost")
|
|
661
|
-
def _day_resource_drill_worker(
|
|
842
|
+
def _day_resource_drill_worker(
|
|
843
|
+
self, *, date: str, service_type: str, is_ai: bool, is_warehouse: bool
|
|
844
|
+
) -> None:
|
|
662
845
|
try:
|
|
663
846
|
svc = self.app.get_cost_service()
|
|
664
847
|
if is_ai:
|
|
@@ -666,6 +849,11 @@ class CostScreen(Vertical):
|
|
|
666
849
|
label = "AI Service"
|
|
667
850
|
found = True
|
|
668
851
|
note = "some Cortex views unavailable" if any_missing else None
|
|
852
|
+
elif is_warehouse:
|
|
853
|
+
df = svc.get_day_warehouse_breakdown(date)
|
|
854
|
+
label = "Warehouse"
|
|
855
|
+
found = True
|
|
856
|
+
note = None
|
|
669
857
|
else:
|
|
670
858
|
df, label, found = svc.get_day_resource_breakdown(date, service_type)
|
|
671
859
|
note = None
|
|
@@ -678,6 +866,8 @@ class CostScreen(Vertical):
|
|
|
678
866
|
|
|
679
867
|
def _render_day_resource_drill(self, df: pd.DataFrame, date: str, service_type: str,
|
|
680
868
|
label: str, found: bool, note: str | None) -> None:
|
|
869
|
+
if self._current_view != "drill_day_resource":
|
|
870
|
+
return # stale callback
|
|
681
871
|
self._reset_table()
|
|
682
872
|
table = self.query_one(DataTable)
|
|
683
873
|
note_suffix = f" · ⚠ {note}" if note else ""
|
|
@@ -706,12 +896,11 @@ class CostScreen(Vertical):
|
|
|
706
896
|
cells.append(str(int(row.get("REQUEST_COUNT", 0))))
|
|
707
897
|
cells += [f"{pct:.1f}%", _bar(credits / max_c)]
|
|
708
898
|
table.add_row(*cells, key=str(row["RESOURCE_NAME"]))
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
899
|
+
user_hint = (
|
|
900
|
+
" · click to see users"
|
|
901
|
+
if self._drill_day_resource_kind in ("warehouse", "ai")
|
|
902
|
+
else ""
|
|
713
903
|
)
|
|
714
|
-
user_hint = " · click to see users" if can_drill_users else ""
|
|
715
904
|
self._set_status(
|
|
716
905
|
f"{service_type} — {date} · {total:,.4f} credits{note_suffix}{user_hint} · Esc to return"
|
|
717
906
|
)
|
|
@@ -721,6 +910,7 @@ class CostScreen(Vertical):
|
|
|
721
910
|
def _fetch_day_resource_users(self, date: str, service_type: str, resource: str) -> None:
|
|
722
911
|
self._current_view = "drill_day_resource_users"
|
|
723
912
|
self._drill_day_resource = resource
|
|
913
|
+
self._reset_table()
|
|
724
914
|
self._set_status(f"Loading users for {resource} on {date}… Esc to return")
|
|
725
915
|
self._day_resource_users_worker(date=date, service_type=service_type, resource=resource)
|
|
726
916
|
|
|
@@ -737,6 +927,8 @@ class CostScreen(Vertical):
|
|
|
737
927
|
|
|
738
928
|
def _render_day_resource_users(self, df: pd.DataFrame, date: str,
|
|
739
929
|
resource: str, note: str | None) -> None:
|
|
930
|
+
if self._current_view != "drill_day_resource_users":
|
|
931
|
+
return # stale callback
|
|
740
932
|
self._reset_table()
|
|
741
933
|
table = self.query_one(DataTable)
|
|
742
934
|
note_suffix = f" · ⚠ {note}" if note else ""
|
|
@@ -764,7 +956,7 @@ class CostScreen(Vertical):
|
|
|
764
956
|
)
|
|
765
957
|
query_hint = (
|
|
766
958
|
" · click user to see top queries"
|
|
767
|
-
if
|
|
959
|
+
if self._drill_day_resource_kind == "warehouse"
|
|
768
960
|
else ""
|
|
769
961
|
)
|
|
770
962
|
self._set_status(
|
|
@@ -776,6 +968,7 @@ class CostScreen(Vertical):
|
|
|
776
968
|
def _fetch_day_user_queries(self, date: str, warehouse: str, user: str) -> None:
|
|
777
969
|
self._current_view = "drill_day_user_queries"
|
|
778
970
|
self._drill_day_user = user
|
|
971
|
+
self._reset_table()
|
|
779
972
|
self._set_status(
|
|
780
973
|
f"Loading top queries for {user} on {warehouse} ({date})… Esc to return"
|
|
781
974
|
)
|
|
@@ -794,6 +987,8 @@ class CostScreen(Vertical):
|
|
|
794
987
|
|
|
795
988
|
def _render_day_user_queries(self, df: pd.DataFrame, date: str,
|
|
796
989
|
warehouse: str, user: str, note: str | None) -> None:
|
|
990
|
+
if self._current_view != "drill_day_user_queries":
|
|
991
|
+
return # stale callback
|
|
797
992
|
self._reset_table()
|
|
798
993
|
table = self.query_one(DataTable)
|
|
799
994
|
note_suffix = f" · ⚠ {note}" if note else ""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|