snowglobe-cli 0.2.3__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.3 → snowglobe_cli-0.2.4}/PKG-INFO +1 -1
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/pyproject.toml +1 -1
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/core/cost_service.py +130 -1
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/tui/screens/cost.py +215 -24
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe_cli.egg-info/PKG-INFO +1 -1
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/LICENSE +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/README.md +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/setup.cfg +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/__init__.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/__main__.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/cli/__init__.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/cli/access.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/cli/app.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/cli/context.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/cli/cost.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/cli/debug.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/cli/diff.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/cli/optimizer.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/cli/prompts.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/cli/report.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/cli/shell.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/cli/shell_completer.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/collectors/access.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/collectors/query_history.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/collectors/query_profile.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/config/loader.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/core/access_service.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/core/optimizer.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/core/query_service.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/core/report_service.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/core/risk_service.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/engines/access/__init__.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/engines/access/explainer.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/engines/access/resolver.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/engines/ai/cortex_optimizer.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/engines/optimizer/query_optimizer.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/graphs/__init__.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/graphs/role_graph.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/graphs/user_graph.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/models/__init__.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/models/access.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/models/access_path.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/models/object_ref.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/models/object_type.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/models/optimizer.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/models/privilege.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/models/query.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/output/__init__.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/output/cli.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/queries/__init__.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/queries/query_history.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/snowflake/connection.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/state/db.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/state/state.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/templates/report.md.j2 +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/tests/access_tests.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/tui/__init__.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/tui/__main__.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/tui/app.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/tui/screens/__init__.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/tui/screens/access.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/tui/screens/home.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/tui/screens/refresh.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/tui/screens/reports.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/tui/screens/risk.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/tui/screens/tune.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/tui/styles.tcss +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/tui/widgets/__init__.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/tui/widgets/access_paths.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/tui/widgets/cache_badge.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/tui/widgets/header.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe/tui/widgets/nav.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe_cli.egg-info/SOURCES.txt +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe_cli.egg-info/dependency_links.txt +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe_cli.egg-info/entry_points.txt +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe_cli.egg-info/requires.txt +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/snowglobe_cli.egg-info/top_level.txt +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/tests/test_access.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/tests/test_optimizer_engine.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/tests/test_output.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/tests/test_query_validation.py +0 -0
- {snowglobe_cli-0.2.3 → snowglobe_cli-0.2.4}/tests/test_state_db.py +0 -0
- {snowglobe_cli-0.2.3 → 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,17 +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
|
|
53
|
-
_drill_day_resource_kind: str = "other"
|
|
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"
|
|
54
59
|
# Becomes True after the user explicitly picks a view, so Select.Changed
|
|
55
60
|
# events fired during the initial mount don't auto-trigger a Snowflake fetch.
|
|
56
61
|
_user_initiated: bool = False
|
|
@@ -147,6 +152,32 @@ class CostScreen(Vertical):
|
|
|
147
152
|
elif self._current_view == "trend":
|
|
148
153
|
self._drill_day = key
|
|
149
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)
|
|
150
181
|
elif self._current_view == "drill_day":
|
|
151
182
|
self._drill_day_service_type = key
|
|
152
183
|
self._fetch_day_resource_drill(self._drill_day, key)
|
|
@@ -217,6 +248,10 @@ class CostScreen(Vertical):
|
|
|
217
248
|
self._fetch_warehouse_drill(self._drill_warehouse)
|
|
218
249
|
elif self._current_view == "drill_user" and self._drill_user:
|
|
219
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)
|
|
220
255
|
elif self._current_view == "drill_day" and self._drill_day:
|
|
221
256
|
self._fetch_day_drill(self._drill_day)
|
|
222
257
|
elif self._current_view == "drill_day_resource" and self._drill_day and self._drill_day_service_type:
|
|
@@ -242,16 +277,30 @@ class CostScreen(Vertical):
|
|
|
242
277
|
elif self._current_view == "drill_user":
|
|
243
278
|
self._drill_user = None
|
|
244
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()
|
|
245
286
|
elif self._current_view == "drill_day":
|
|
246
287
|
self._drill_day = None
|
|
247
288
|
self._drill_day_service_type = None
|
|
248
|
-
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()
|
|
249
294
|
elif self._current_view == "drill_day_resource":
|
|
250
295
|
self._drill_day_service_type = None
|
|
251
296
|
self._fetch_day_drill(self._drill_day)
|
|
252
297
|
elif self._current_view == "drill_day_resource_users":
|
|
253
298
|
self._drill_day_resource = None
|
|
254
|
-
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)
|
|
255
304
|
elif self._current_view == "drill_day_user_queries":
|
|
256
305
|
self._drill_day_user = None
|
|
257
306
|
self._fetch_day_resource_users(
|
|
@@ -313,6 +362,7 @@ class CostScreen(Vertical):
|
|
|
313
362
|
|
|
314
363
|
def _fetch_trend(self, force: bool = False) -> None:
|
|
315
364
|
self._current_view = "trend"
|
|
365
|
+
self._reset_table()
|
|
316
366
|
self._set_status(f"Loading daily trend ({self._days()}d)…")
|
|
317
367
|
self._trend_worker(days=self._days(), force=force)
|
|
318
368
|
|
|
@@ -326,6 +376,8 @@ class CostScreen(Vertical):
|
|
|
326
376
|
self.app.call_from_thread(self._render_trend, df, days, cache_age)
|
|
327
377
|
|
|
328
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
|
|
329
381
|
if df is None or df.empty:
|
|
330
382
|
self._set_status("No trend data found.")
|
|
331
383
|
self._clear_table()
|
|
@@ -413,6 +465,7 @@ class CostScreen(Vertical):
|
|
|
413
465
|
|
|
414
466
|
def _fetch_service_drill(self, service_type: str, force: bool = False) -> None:
|
|
415
467
|
self._current_view = "drill_service"
|
|
468
|
+
self._reset_table()
|
|
416
469
|
self._set_status(f"Loading daily trend for {service_type} ({self._days()}d)… Esc to return")
|
|
417
470
|
self._drill_worker(service_type=service_type, days=self._days())
|
|
418
471
|
|
|
@@ -426,15 +479,17 @@ class CostScreen(Vertical):
|
|
|
426
479
|
self.app.call_from_thread(self._render_service_drill, df, service_type, days)
|
|
427
480
|
|
|
428
481
|
def _render_service_drill(self, df: pd.DataFrame, service_type: str, days: int) -> None:
|
|
482
|
+
if self._current_view != "drill_service":
|
|
483
|
+
return
|
|
429
484
|
if df is None or df.empty:
|
|
430
485
|
self._set_status(f"No daily data for {service_type} ({days}d). Esc to return.")
|
|
431
486
|
self._clear_table()
|
|
432
487
|
return
|
|
433
488
|
total = float(df["CREDITS"].sum())
|
|
434
489
|
self._set_status(
|
|
435
|
-
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"
|
|
436
492
|
)
|
|
437
|
-
|
|
438
493
|
table = self.query_one(DataTable)
|
|
439
494
|
self._reset_table()
|
|
440
495
|
table.add_columns("DATE", "CREDITS", "TREND")
|
|
@@ -442,7 +497,8 @@ class CostScreen(Vertical):
|
|
|
442
497
|
for _, row in df.iterrows():
|
|
443
498
|
credits = float(row["CREDITS"])
|
|
444
499
|
ratio = credits / max_c if max_c > 0 else 0
|
|
445
|
-
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"]))
|
|
446
502
|
|
|
447
503
|
# --- View 4: Warehouses ------------------------------------------
|
|
448
504
|
|
|
@@ -485,6 +541,7 @@ class CostScreen(Vertical):
|
|
|
485
541
|
|
|
486
542
|
def _fetch_warehouse_drill(self, warehouse_name: str) -> None:
|
|
487
543
|
self._current_view = "drill_warehouse"
|
|
544
|
+
self._reset_table()
|
|
488
545
|
self._set_status(f"Loading daily trend for {warehouse_name} ({self._days()}d)… Esc to return")
|
|
489
546
|
self._warehouse_drill_worker(warehouse_name=warehouse_name, days=self._days())
|
|
490
547
|
|
|
@@ -498,13 +555,16 @@ class CostScreen(Vertical):
|
|
|
498
555
|
self.app.call_from_thread(self._render_warehouse_drill, df, warehouse_name, days)
|
|
499
556
|
|
|
500
557
|
def _render_warehouse_drill(self, df: pd.DataFrame, warehouse_name: str, days: int) -> None:
|
|
558
|
+
if self._current_view != "drill_warehouse":
|
|
559
|
+
return
|
|
501
560
|
if df is None or df.empty:
|
|
502
561
|
self._set_status(f"No daily data for {warehouse_name} ({days}d). Esc to return")
|
|
503
562
|
self._clear_table()
|
|
504
563
|
return
|
|
505
564
|
total = float(df["CREDITS"].sum())
|
|
506
565
|
self._set_status(
|
|
507
|
-
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"
|
|
508
568
|
)
|
|
509
569
|
table = self.query_one(DataTable)
|
|
510
570
|
self._reset_table()
|
|
@@ -513,7 +573,8 @@ class CostScreen(Vertical):
|
|
|
513
573
|
for _, row in df.iterrows():
|
|
514
574
|
credits = float(row["CREDITS"])
|
|
515
575
|
ratio = credits / max_c if max_c > 0 else 0
|
|
516
|
-
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"]))
|
|
517
578
|
|
|
518
579
|
# --- View 5: Users -----------------------------------------------
|
|
519
580
|
|
|
@@ -567,6 +628,7 @@ class CostScreen(Vertical):
|
|
|
567
628
|
|
|
568
629
|
def _fetch_user_drill(self, user_name: str) -> None:
|
|
569
630
|
self._current_view = "drill_user"
|
|
631
|
+
self._reset_table()
|
|
570
632
|
self._set_status(f"Loading warehouse detail for {user_name} ({min(self._days(),7)}d)… Esc to return")
|
|
571
633
|
self._user_drill_worker(user_name=user_name, days=min(self._days(), 7))
|
|
572
634
|
|
|
@@ -580,6 +642,8 @@ class CostScreen(Vertical):
|
|
|
580
642
|
self.app.call_from_thread(self._render_user_drill, df, user_name, days, note)
|
|
581
643
|
|
|
582
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
|
|
583
647
|
if df is None or df.empty:
|
|
584
648
|
self._set_status(f"No attribution data for {user_name} ({days}d). Esc to return")
|
|
585
649
|
self._clear_table()
|
|
@@ -587,7 +651,8 @@ class CostScreen(Vertical):
|
|
|
587
651
|
total = float(df["CREDITS"].sum())
|
|
588
652
|
note_suffix = f" · ⚠ {note}" if note else ""
|
|
589
653
|
self._set_status(
|
|
590
|
-
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"
|
|
591
656
|
)
|
|
592
657
|
no_credits = note is not None
|
|
593
658
|
table = self.query_one(DataTable)
|
|
@@ -601,12 +666,117 @@ class CostScreen(Vertical):
|
|
|
601
666
|
credits_str,
|
|
602
667
|
str(int(row.get("QUERY_COUNT", 0))),
|
|
603
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", "")),
|
|
604
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
|
+
)
|
|
605
773
|
|
|
606
774
|
# --- Drill: day breakdown (Level 1 — service types) --------------
|
|
607
775
|
|
|
608
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
|
|
609
778
|
self._current_view = "drill_day"
|
|
779
|
+
self._reset_table() # clear immediately so stale rows can't be re-clicked while loading
|
|
610
780
|
self._set_status(f"Loading service breakdown for {date}… Esc to return")
|
|
611
781
|
self._day_drill_worker(date=date)
|
|
612
782
|
|
|
@@ -620,6 +790,8 @@ class CostScreen(Vertical):
|
|
|
620
790
|
self.app.call_from_thread(self._render_day_drill, df, date)
|
|
621
791
|
|
|
622
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
|
|
623
795
|
self._reset_table()
|
|
624
796
|
table = self.query_one(DataTable)
|
|
625
797
|
if df is None or df.empty:
|
|
@@ -648,13 +820,28 @@ class CostScreen(Vertical):
|
|
|
648
820
|
def _fetch_day_resource_drill(self, date: str, service_type: str) -> None:
|
|
649
821
|
self._current_view = "drill_day_resource"
|
|
650
822
|
self._drill_day_service_type = service_type
|
|
651
|
-
self._set_status(f"Loading {service_type} breakdown for {date}… Esc to return")
|
|
652
823
|
upper = service_type.upper()
|
|
653
824
|
is_ai = any(kw in upper for kw in _AI_SERVICE_KEYWORDS)
|
|
654
|
-
|
|
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
|
+
)
|
|
655
840
|
|
|
656
841
|
@work(thread=True, exclusive=True, group="cost")
|
|
657
|
-
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:
|
|
658
845
|
try:
|
|
659
846
|
svc = self.app.get_cost_service()
|
|
660
847
|
if is_ai:
|
|
@@ -662,6 +849,11 @@ class CostScreen(Vertical):
|
|
|
662
849
|
label = "AI Service"
|
|
663
850
|
found = True
|
|
664
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
|
|
665
857
|
else:
|
|
666
858
|
df, label, found = svc.get_day_resource_breakdown(date, service_type)
|
|
667
859
|
note = None
|
|
@@ -669,20 +861,13 @@ class CostScreen(Vertical):
|
|
|
669
861
|
self.app.call_from_thread(self._fetch_failed, e)
|
|
670
862
|
return
|
|
671
863
|
self.app.call_from_thread(
|
|
672
|
-
self._render_day_resource_drill, df, date, service_type, label, found, note
|
|
864
|
+
self._render_day_resource_drill, df, date, service_type, label, found, note
|
|
673
865
|
)
|
|
674
866
|
|
|
675
867
|
def _render_day_resource_drill(self, df: pd.DataFrame, date: str, service_type: str,
|
|
676
|
-
label: str, found: bool, note: str | None
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
if is_ai:
|
|
680
|
-
self._drill_day_resource_kind = "ai"
|
|
681
|
-
elif label == "Warehouse":
|
|
682
|
-
self._drill_day_resource_kind = "warehouse"
|
|
683
|
-
else:
|
|
684
|
-
self._drill_day_resource_kind = "other"
|
|
685
|
-
|
|
868
|
+
label: str, found: bool, note: str | None) -> None:
|
|
869
|
+
if self._current_view != "drill_day_resource":
|
|
870
|
+
return # stale callback
|
|
686
871
|
self._reset_table()
|
|
687
872
|
table = self.query_one(DataTable)
|
|
688
873
|
note_suffix = f" · ⚠ {note}" if note else ""
|
|
@@ -725,6 +910,7 @@ class CostScreen(Vertical):
|
|
|
725
910
|
def _fetch_day_resource_users(self, date: str, service_type: str, resource: str) -> None:
|
|
726
911
|
self._current_view = "drill_day_resource_users"
|
|
727
912
|
self._drill_day_resource = resource
|
|
913
|
+
self._reset_table()
|
|
728
914
|
self._set_status(f"Loading users for {resource} on {date}… Esc to return")
|
|
729
915
|
self._day_resource_users_worker(date=date, service_type=service_type, resource=resource)
|
|
730
916
|
|
|
@@ -741,6 +927,8 @@ class CostScreen(Vertical):
|
|
|
741
927
|
|
|
742
928
|
def _render_day_resource_users(self, df: pd.DataFrame, date: str,
|
|
743
929
|
resource: str, note: str | None) -> None:
|
|
930
|
+
if self._current_view != "drill_day_resource_users":
|
|
931
|
+
return # stale callback
|
|
744
932
|
self._reset_table()
|
|
745
933
|
table = self.query_one(DataTable)
|
|
746
934
|
note_suffix = f" · ⚠ {note}" if note else ""
|
|
@@ -780,6 +968,7 @@ class CostScreen(Vertical):
|
|
|
780
968
|
def _fetch_day_user_queries(self, date: str, warehouse: str, user: str) -> None:
|
|
781
969
|
self._current_view = "drill_day_user_queries"
|
|
782
970
|
self._drill_day_user = user
|
|
971
|
+
self._reset_table()
|
|
783
972
|
self._set_status(
|
|
784
973
|
f"Loading top queries for {user} on {warehouse} ({date})… Esc to return"
|
|
785
974
|
)
|
|
@@ -798,6 +987,8 @@ class CostScreen(Vertical):
|
|
|
798
987
|
|
|
799
988
|
def _render_day_user_queries(self, df: pd.DataFrame, date: str,
|
|
800
989
|
warehouse: str, user: str, note: str | None) -> None:
|
|
990
|
+
if self._current_view != "drill_day_user_queries":
|
|
991
|
+
return # stale callback
|
|
801
992
|
self._reset_table()
|
|
802
993
|
table = self.query_one(DataTable)
|
|
803
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
|