foundry-mcp 0.8.22__py3-none-any.whl
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.
Potentially problematic release.
This version of foundry-mcp might be problematic. Click here for more details.
- foundry_mcp/__init__.py +13 -0
- foundry_mcp/cli/__init__.py +67 -0
- foundry_mcp/cli/__main__.py +9 -0
- foundry_mcp/cli/agent.py +96 -0
- foundry_mcp/cli/commands/__init__.py +37 -0
- foundry_mcp/cli/commands/cache.py +137 -0
- foundry_mcp/cli/commands/dashboard.py +148 -0
- foundry_mcp/cli/commands/dev.py +446 -0
- foundry_mcp/cli/commands/journal.py +377 -0
- foundry_mcp/cli/commands/lifecycle.py +274 -0
- foundry_mcp/cli/commands/modify.py +824 -0
- foundry_mcp/cli/commands/plan.py +640 -0
- foundry_mcp/cli/commands/pr.py +393 -0
- foundry_mcp/cli/commands/review.py +667 -0
- foundry_mcp/cli/commands/session.py +472 -0
- foundry_mcp/cli/commands/specs.py +686 -0
- foundry_mcp/cli/commands/tasks.py +807 -0
- foundry_mcp/cli/commands/testing.py +676 -0
- foundry_mcp/cli/commands/validate.py +982 -0
- foundry_mcp/cli/config.py +98 -0
- foundry_mcp/cli/context.py +298 -0
- foundry_mcp/cli/logging.py +212 -0
- foundry_mcp/cli/main.py +44 -0
- foundry_mcp/cli/output.py +122 -0
- foundry_mcp/cli/registry.py +110 -0
- foundry_mcp/cli/resilience.py +178 -0
- foundry_mcp/cli/transcript.py +217 -0
- foundry_mcp/config.py +1454 -0
- foundry_mcp/core/__init__.py +144 -0
- foundry_mcp/core/ai_consultation.py +1773 -0
- foundry_mcp/core/batch_operations.py +1202 -0
- foundry_mcp/core/cache.py +195 -0
- foundry_mcp/core/capabilities.py +446 -0
- foundry_mcp/core/concurrency.py +898 -0
- foundry_mcp/core/context.py +540 -0
- foundry_mcp/core/discovery.py +1603 -0
- foundry_mcp/core/error_collection.py +728 -0
- foundry_mcp/core/error_store.py +592 -0
- foundry_mcp/core/health.py +749 -0
- foundry_mcp/core/intake.py +933 -0
- foundry_mcp/core/journal.py +700 -0
- foundry_mcp/core/lifecycle.py +412 -0
- foundry_mcp/core/llm_config.py +1376 -0
- foundry_mcp/core/llm_patterns.py +510 -0
- foundry_mcp/core/llm_provider.py +1569 -0
- foundry_mcp/core/logging_config.py +374 -0
- foundry_mcp/core/metrics_persistence.py +584 -0
- foundry_mcp/core/metrics_registry.py +327 -0
- foundry_mcp/core/metrics_store.py +641 -0
- foundry_mcp/core/modifications.py +224 -0
- foundry_mcp/core/naming.py +146 -0
- foundry_mcp/core/observability.py +1216 -0
- foundry_mcp/core/otel.py +452 -0
- foundry_mcp/core/otel_stubs.py +264 -0
- foundry_mcp/core/pagination.py +255 -0
- foundry_mcp/core/progress.py +387 -0
- foundry_mcp/core/prometheus.py +564 -0
- foundry_mcp/core/prompts/__init__.py +464 -0
- foundry_mcp/core/prompts/fidelity_review.py +691 -0
- foundry_mcp/core/prompts/markdown_plan_review.py +515 -0
- foundry_mcp/core/prompts/plan_review.py +627 -0
- foundry_mcp/core/providers/__init__.py +237 -0
- foundry_mcp/core/providers/base.py +515 -0
- foundry_mcp/core/providers/claude.py +472 -0
- foundry_mcp/core/providers/codex.py +637 -0
- foundry_mcp/core/providers/cursor_agent.py +630 -0
- foundry_mcp/core/providers/detectors.py +515 -0
- foundry_mcp/core/providers/gemini.py +426 -0
- foundry_mcp/core/providers/opencode.py +718 -0
- foundry_mcp/core/providers/opencode_wrapper.js +308 -0
- foundry_mcp/core/providers/package-lock.json +24 -0
- foundry_mcp/core/providers/package.json +25 -0
- foundry_mcp/core/providers/registry.py +607 -0
- foundry_mcp/core/providers/test_provider.py +171 -0
- foundry_mcp/core/providers/validation.py +857 -0
- foundry_mcp/core/rate_limit.py +427 -0
- foundry_mcp/core/research/__init__.py +68 -0
- foundry_mcp/core/research/memory.py +528 -0
- foundry_mcp/core/research/models.py +1234 -0
- foundry_mcp/core/research/providers/__init__.py +40 -0
- foundry_mcp/core/research/providers/base.py +242 -0
- foundry_mcp/core/research/providers/google.py +507 -0
- foundry_mcp/core/research/providers/perplexity.py +442 -0
- foundry_mcp/core/research/providers/semantic_scholar.py +544 -0
- foundry_mcp/core/research/providers/tavily.py +383 -0
- foundry_mcp/core/research/workflows/__init__.py +25 -0
- foundry_mcp/core/research/workflows/base.py +298 -0
- foundry_mcp/core/research/workflows/chat.py +271 -0
- foundry_mcp/core/research/workflows/consensus.py +539 -0
- foundry_mcp/core/research/workflows/deep_research.py +4142 -0
- foundry_mcp/core/research/workflows/ideate.py +682 -0
- foundry_mcp/core/research/workflows/thinkdeep.py +405 -0
- foundry_mcp/core/resilience.py +600 -0
- foundry_mcp/core/responses.py +1624 -0
- foundry_mcp/core/review.py +366 -0
- foundry_mcp/core/security.py +438 -0
- foundry_mcp/core/spec.py +4119 -0
- foundry_mcp/core/task.py +2463 -0
- foundry_mcp/core/testing.py +839 -0
- foundry_mcp/core/validation.py +2357 -0
- foundry_mcp/dashboard/__init__.py +32 -0
- foundry_mcp/dashboard/app.py +119 -0
- foundry_mcp/dashboard/components/__init__.py +17 -0
- foundry_mcp/dashboard/components/cards.py +88 -0
- foundry_mcp/dashboard/components/charts.py +177 -0
- foundry_mcp/dashboard/components/filters.py +136 -0
- foundry_mcp/dashboard/components/tables.py +195 -0
- foundry_mcp/dashboard/data/__init__.py +11 -0
- foundry_mcp/dashboard/data/stores.py +433 -0
- foundry_mcp/dashboard/launcher.py +300 -0
- foundry_mcp/dashboard/views/__init__.py +12 -0
- foundry_mcp/dashboard/views/errors.py +217 -0
- foundry_mcp/dashboard/views/metrics.py +164 -0
- foundry_mcp/dashboard/views/overview.py +96 -0
- foundry_mcp/dashboard/views/providers.py +83 -0
- foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
- foundry_mcp/dashboard/views/tool_usage.py +139 -0
- foundry_mcp/prompts/__init__.py +9 -0
- foundry_mcp/prompts/workflows.py +525 -0
- foundry_mcp/resources/__init__.py +9 -0
- foundry_mcp/resources/specs.py +591 -0
- foundry_mcp/schemas/__init__.py +38 -0
- foundry_mcp/schemas/intake-schema.json +89 -0
- foundry_mcp/schemas/sdd-spec-schema.json +414 -0
- foundry_mcp/server.py +150 -0
- foundry_mcp/tools/__init__.py +10 -0
- foundry_mcp/tools/unified/__init__.py +92 -0
- foundry_mcp/tools/unified/authoring.py +3620 -0
- foundry_mcp/tools/unified/context_helpers.py +98 -0
- foundry_mcp/tools/unified/documentation_helpers.py +268 -0
- foundry_mcp/tools/unified/environment.py +1341 -0
- foundry_mcp/tools/unified/error.py +479 -0
- foundry_mcp/tools/unified/health.py +225 -0
- foundry_mcp/tools/unified/journal.py +841 -0
- foundry_mcp/tools/unified/lifecycle.py +640 -0
- foundry_mcp/tools/unified/metrics.py +777 -0
- foundry_mcp/tools/unified/plan.py +876 -0
- foundry_mcp/tools/unified/pr.py +294 -0
- foundry_mcp/tools/unified/provider.py +589 -0
- foundry_mcp/tools/unified/research.py +1283 -0
- foundry_mcp/tools/unified/review.py +1042 -0
- foundry_mcp/tools/unified/review_helpers.py +314 -0
- foundry_mcp/tools/unified/router.py +102 -0
- foundry_mcp/tools/unified/server.py +565 -0
- foundry_mcp/tools/unified/spec.py +1283 -0
- foundry_mcp/tools/unified/task.py +3846 -0
- foundry_mcp/tools/unified/test.py +431 -0
- foundry_mcp/tools/unified/verification.py +520 -0
- foundry_mcp-0.8.22.dist-info/METADATA +344 -0
- foundry_mcp-0.8.22.dist-info/RECORD +153 -0
- foundry_mcp-0.8.22.dist-info/WHEEL +4 -0
- foundry_mcp-0.8.22.dist-info/entry_points.txt +3 -0
- foundry_mcp-0.8.22.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Streamlit-based dashboard for foundry-mcp observability.
|
|
2
|
+
|
|
3
|
+
This module provides a web UI for viewing errors, metrics, provider status,
|
|
4
|
+
and SDD workflow progress.
|
|
5
|
+
|
|
6
|
+
Public API:
|
|
7
|
+
launch_dashboard: Start the Streamlit dashboard server
|
|
8
|
+
stop_dashboard: Stop the running dashboard server
|
|
9
|
+
get_dashboard_status: Check if dashboard is running
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
from foundry_mcp.dashboard import launch_dashboard, stop_dashboard
|
|
13
|
+
|
|
14
|
+
# Start dashboard
|
|
15
|
+
result = launch_dashboard(port=8501, open_browser=True)
|
|
16
|
+
print(f"Dashboard running at {result['url']}")
|
|
17
|
+
|
|
18
|
+
# Stop dashboard
|
|
19
|
+
stop_dashboard()
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from foundry_mcp.dashboard.launcher import (
|
|
23
|
+
get_dashboard_status,
|
|
24
|
+
launch_dashboard,
|
|
25
|
+
stop_dashboard,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"launch_dashboard",
|
|
30
|
+
"stop_dashboard",
|
|
31
|
+
"get_dashboard_status",
|
|
32
|
+
]
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Main Streamlit dashboard application.
|
|
2
|
+
|
|
3
|
+
This is the entry point for the Streamlit dashboard.
|
|
4
|
+
Run with: streamlit run src/foundry_mcp/dashboard/app.py
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import streamlit as st
|
|
8
|
+
|
|
9
|
+
# Page config must be first Streamlit command
|
|
10
|
+
st.set_page_config(
|
|
11
|
+
page_title="foundry-mcp Dashboard",
|
|
12
|
+
page_icon=":chart_with_upwards_trend:",
|
|
13
|
+
layout="wide",
|
|
14
|
+
initial_sidebar_state="expanded",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Import pages after config
|
|
18
|
+
from foundry_mcp.dashboard.views import overview, errors, metrics, tool_usage
|
|
19
|
+
|
|
20
|
+
# Custom dark theme CSS
|
|
21
|
+
st.markdown(
|
|
22
|
+
"""
|
|
23
|
+
<style>
|
|
24
|
+
/* Consistent dark theme styling */
|
|
25
|
+
.stMetric {
|
|
26
|
+
background-color: rgba(30, 30, 46, 0.8);
|
|
27
|
+
padding: 1rem;
|
|
28
|
+
border-radius: 0.5rem;
|
|
29
|
+
border: 1px solid rgba(100, 100, 150, 0.3);
|
|
30
|
+
}
|
|
31
|
+
.stMetric label {
|
|
32
|
+
color: #a0a0b0 !important;
|
|
33
|
+
}
|
|
34
|
+
.stMetric [data-testid="stMetricValue"] {
|
|
35
|
+
color: #e0e0f0 !important;
|
|
36
|
+
}
|
|
37
|
+
/* Card-like containers */
|
|
38
|
+
[data-testid="stVerticalBlock"] > [data-testid="stVerticalBlock"] {
|
|
39
|
+
background-color: rgba(30, 30, 46, 0.5);
|
|
40
|
+
border-radius: 0.5rem;
|
|
41
|
+
padding: 0.5rem;
|
|
42
|
+
}
|
|
43
|
+
</style>
|
|
44
|
+
""",
|
|
45
|
+
unsafe_allow_html=True,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Initialize session state
|
|
49
|
+
if "auto_refresh" not in st.session_state:
|
|
50
|
+
st.session_state.auto_refresh = False
|
|
51
|
+
if "refresh_interval" not in st.session_state:
|
|
52
|
+
st.session_state.refresh_interval = 5
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def render_sidebar():
|
|
56
|
+
"""Render navigation sidebar."""
|
|
57
|
+
with st.sidebar:
|
|
58
|
+
st.title(":cube: foundry-mcp")
|
|
59
|
+
st.caption("Observability Dashboard")
|
|
60
|
+
|
|
61
|
+
st.divider()
|
|
62
|
+
|
|
63
|
+
# Navigation
|
|
64
|
+
page = st.radio(
|
|
65
|
+
"Navigate",
|
|
66
|
+
options=["Overview", "Tool Usage", "Errors", "Metrics"],
|
|
67
|
+
label_visibility="collapsed",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
st.divider()
|
|
71
|
+
|
|
72
|
+
# Auto-refresh controls
|
|
73
|
+
st.subheader("Settings")
|
|
74
|
+
st.session_state.auto_refresh = st.checkbox(
|
|
75
|
+
"Auto-refresh",
|
|
76
|
+
value=st.session_state.auto_refresh,
|
|
77
|
+
)
|
|
78
|
+
if st.session_state.auto_refresh:
|
|
79
|
+
st.session_state.refresh_interval = st.slider(
|
|
80
|
+
"Interval (sec)",
|
|
81
|
+
min_value=5,
|
|
82
|
+
max_value=60,
|
|
83
|
+
value=st.session_state.refresh_interval,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Manual refresh button
|
|
87
|
+
if st.button("Refresh Now", use_container_width=True):
|
|
88
|
+
st.rerun()
|
|
89
|
+
|
|
90
|
+
return page
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def main():
|
|
94
|
+
"""Main dashboard entry point."""
|
|
95
|
+
# Render sidebar and get page selection
|
|
96
|
+
page = render_sidebar()
|
|
97
|
+
|
|
98
|
+
# Route to page
|
|
99
|
+
page_map = {
|
|
100
|
+
"Overview": overview.render,
|
|
101
|
+
"Tool Usage": tool_usage.render,
|
|
102
|
+
"Errors": errors.render,
|
|
103
|
+
"Metrics": metrics.render,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Render selected page
|
|
107
|
+
render_func = page_map.get(page, overview.render)
|
|
108
|
+
render_func()
|
|
109
|
+
|
|
110
|
+
# Auto-refresh logic
|
|
111
|
+
if st.session_state.auto_refresh:
|
|
112
|
+
import time
|
|
113
|
+
|
|
114
|
+
time.sleep(st.session_state.refresh_interval)
|
|
115
|
+
st.rerun()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
main()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Reusable dashboard UI components.
|
|
2
|
+
|
|
3
|
+
Components:
|
|
4
|
+
cards: KPI metric card helpers
|
|
5
|
+
charts: Plotly chart builders
|
|
6
|
+
filters: Time range and filter widgets
|
|
7
|
+
tables: Data table configurations
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from foundry_mcp.dashboard.components import cards, charts, filters, tables
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"cards",
|
|
14
|
+
"charts",
|
|
15
|
+
"filters",
|
|
16
|
+
"tables",
|
|
17
|
+
]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""KPI card components for dashboard."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import streamlit as st
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def metric_card(
|
|
9
|
+
label: str,
|
|
10
|
+
value: str | int | float,
|
|
11
|
+
delta: Optional[str] = None,
|
|
12
|
+
delta_color: str = "normal",
|
|
13
|
+
help_text: Optional[str] = None,
|
|
14
|
+
) -> None:
|
|
15
|
+
"""Render a metric card with optional delta indicator.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
label: Card title/label
|
|
19
|
+
value: Main value to display
|
|
20
|
+
delta: Optional change indicator (e.g., "+5%", "-3")
|
|
21
|
+
delta_color: Color for delta ("normal", "inverse", "off")
|
|
22
|
+
help_text: Optional tooltip help text
|
|
23
|
+
"""
|
|
24
|
+
st.metric(
|
|
25
|
+
label=label,
|
|
26
|
+
value=value,
|
|
27
|
+
delta=delta,
|
|
28
|
+
delta_color=delta_color,
|
|
29
|
+
help=help_text,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def status_badge(status: str, label: Optional[str] = None) -> None:
|
|
34
|
+
"""Render a status badge with color coding.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
status: Status value ("healthy", "unhealthy", "warning", "unknown")
|
|
38
|
+
label: Optional label to show before status
|
|
39
|
+
"""
|
|
40
|
+
colors = {
|
|
41
|
+
"healthy": ":green_circle:",
|
|
42
|
+
"unhealthy": ":red_circle:",
|
|
43
|
+
"warning": ":yellow_circle:",
|
|
44
|
+
"unknown": ":white_circle:",
|
|
45
|
+
"available": ":green_circle:",
|
|
46
|
+
"unavailable": ":red_circle:",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
emoji = colors.get(status.lower(), ":white_circle:")
|
|
50
|
+
text = label if label else status.title()
|
|
51
|
+
st.markdown(f"{emoji} **{text}**")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def info_card(title: str, content: str, icon: Optional[str] = None) -> None:
|
|
55
|
+
"""Render an info card with title and content.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
title: Card title
|
|
59
|
+
content: Card content (markdown supported)
|
|
60
|
+
icon: Optional emoji icon
|
|
61
|
+
"""
|
|
62
|
+
with st.container(border=True):
|
|
63
|
+
header = f"{icon} {title}" if icon else title
|
|
64
|
+
st.subheader(header)
|
|
65
|
+
st.markdown(content)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def kpi_row(
|
|
69
|
+
metrics: list[dict],
|
|
70
|
+
columns: int = 6,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Render a row of KPI metric cards.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
metrics: List of metric dicts with keys: label, value, delta, help
|
|
76
|
+
columns: Number of columns (default 6)
|
|
77
|
+
"""
|
|
78
|
+
cols = st.columns(columns)
|
|
79
|
+
|
|
80
|
+
for i, m in enumerate(metrics):
|
|
81
|
+
with cols[i % columns]:
|
|
82
|
+
metric_card(
|
|
83
|
+
label=m.get("label", ""),
|
|
84
|
+
value=m.get("value", 0),
|
|
85
|
+
delta=m.get("delta"),
|
|
86
|
+
delta_color=m.get("delta_color", "normal"),
|
|
87
|
+
help_text=m.get("help"),
|
|
88
|
+
)
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Plotly chart builders for dashboard."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import streamlit as st
|
|
6
|
+
|
|
7
|
+
# Try importing plotly - it's an optional dependency
|
|
8
|
+
try:
|
|
9
|
+
import plotly.express as px
|
|
10
|
+
import plotly.graph_objects as go
|
|
11
|
+
|
|
12
|
+
PLOTLY_AVAILABLE = True
|
|
13
|
+
except ImportError:
|
|
14
|
+
PLOTLY_AVAILABLE = False
|
|
15
|
+
px = None
|
|
16
|
+
go = None
|
|
17
|
+
|
|
18
|
+
# Try importing pandas
|
|
19
|
+
try:
|
|
20
|
+
import pandas as pd
|
|
21
|
+
|
|
22
|
+
PANDAS_AVAILABLE = True
|
|
23
|
+
except ImportError:
|
|
24
|
+
PANDAS_AVAILABLE = False
|
|
25
|
+
pd = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _check_deps():
|
|
29
|
+
"""Check if required dependencies are available."""
|
|
30
|
+
if not PLOTLY_AVAILABLE:
|
|
31
|
+
st.warning("Plotly not installed. Install with: pip install plotly")
|
|
32
|
+
return False
|
|
33
|
+
if not PANDAS_AVAILABLE:
|
|
34
|
+
st.warning("Pandas not installed. Install with: pip install pandas")
|
|
35
|
+
return False
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def bar_chart(
|
|
40
|
+
df: "pd.DataFrame",
|
|
41
|
+
x: str,
|
|
42
|
+
y: str,
|
|
43
|
+
title: Optional[str] = None,
|
|
44
|
+
color: Optional[str] = None,
|
|
45
|
+
orientation: str = "v",
|
|
46
|
+
height: int = 400,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Render an interactive bar chart.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
df: DataFrame with data
|
|
52
|
+
x: Column name for x-axis (or values if horizontal)
|
|
53
|
+
y: Column name for y-axis (or categories if horizontal)
|
|
54
|
+
title: Optional chart title
|
|
55
|
+
color: Optional column for color grouping
|
|
56
|
+
orientation: "v" for vertical, "h" for horizontal
|
|
57
|
+
height: Chart height in pixels
|
|
58
|
+
"""
|
|
59
|
+
if not _check_deps():
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
if df is None or df.empty:
|
|
63
|
+
st.info("No data to display")
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
fig = px.bar(
|
|
67
|
+
df,
|
|
68
|
+
x=x,
|
|
69
|
+
y=y,
|
|
70
|
+
title=title,
|
|
71
|
+
color=color,
|
|
72
|
+
orientation=orientation,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
fig.update_layout(
|
|
76
|
+
template="plotly_dark",
|
|
77
|
+
height=height,
|
|
78
|
+
margin=dict(l=20, r=20, t=40, b=20),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
st.plotly_chart(fig, use_container_width=True)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def pie_chart(
|
|
85
|
+
df: "pd.DataFrame",
|
|
86
|
+
values: str,
|
|
87
|
+
names: str,
|
|
88
|
+
title: Optional[str] = None,
|
|
89
|
+
hole: float = 0.4,
|
|
90
|
+
height: int = 400,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Render an interactive pie/donut chart.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
df: DataFrame with data
|
|
96
|
+
values: Column name for values
|
|
97
|
+
names: Column name for category names
|
|
98
|
+
title: Optional chart title
|
|
99
|
+
hole: Hole size for donut (0 for pie, 0.4 for donut)
|
|
100
|
+
height: Chart height in pixels
|
|
101
|
+
"""
|
|
102
|
+
if not _check_deps():
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
if df is None or df.empty:
|
|
106
|
+
st.info("No data to display")
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
# Custom colors for status charts
|
|
110
|
+
color_map = {
|
|
111
|
+
"completed": "#10b981",
|
|
112
|
+
"in_progress": "#3b82f6",
|
|
113
|
+
"pending": "#9ca3af",
|
|
114
|
+
"blocked": "#ef4444",
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fig = px.pie(
|
|
118
|
+
df,
|
|
119
|
+
values=values,
|
|
120
|
+
names=names,
|
|
121
|
+
title=title,
|
|
122
|
+
hole=hole,
|
|
123
|
+
color=names,
|
|
124
|
+
color_discrete_map=color_map,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
fig.update_layout(
|
|
128
|
+
template="plotly_dark",
|
|
129
|
+
height=height,
|
|
130
|
+
margin=dict(l=20, r=20, t=40, b=20),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
st.plotly_chart(fig, use_container_width=True)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def treemap_chart(
|
|
137
|
+
df: "pd.DataFrame",
|
|
138
|
+
path: list[str],
|
|
139
|
+
values: str,
|
|
140
|
+
title: Optional[str] = None,
|
|
141
|
+
height: int = 400,
|
|
142
|
+
) -> None:
|
|
143
|
+
"""Render an interactive treemap chart.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
df: DataFrame with data
|
|
147
|
+
path: List of column names for hierarchy path
|
|
148
|
+
values: Column name for values
|
|
149
|
+
title: Optional chart title
|
|
150
|
+
height: Chart height in pixels
|
|
151
|
+
"""
|
|
152
|
+
if not _check_deps():
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
if df is None or df.empty:
|
|
156
|
+
st.info("No data to display")
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
fig = px.treemap(
|
|
160
|
+
df,
|
|
161
|
+
path=path,
|
|
162
|
+
values=values,
|
|
163
|
+
title=title,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
fig.update_layout(
|
|
167
|
+
template="plotly_dark",
|
|
168
|
+
height=height,
|
|
169
|
+
margin=dict(l=20, r=20, t=40, b=20),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
st.plotly_chart(fig, use_container_width=True)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def empty_chart(message: str = "No data available") -> None:
|
|
176
|
+
"""Display a placeholder for empty charts."""
|
|
177
|
+
st.info(message)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Filter widget components for dashboard."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import streamlit as st
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def time_range_filter(
|
|
10
|
+
key: str = "time_range",
|
|
11
|
+
default: str = "24h",
|
|
12
|
+
) -> int:
|
|
13
|
+
"""Render a time range selector.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
key: Unique key for the widget
|
|
17
|
+
default: Default selection
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Number of hours for the selected range
|
|
21
|
+
"""
|
|
22
|
+
options = {
|
|
23
|
+
"1 hour": 1,
|
|
24
|
+
"6 hours": 6,
|
|
25
|
+
"24 hours": 24,
|
|
26
|
+
"7 days": 168,
|
|
27
|
+
"30 days": 720,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Find default index
|
|
31
|
+
default_hours = {"1h": 1, "6h": 6, "24h": 24, "7d": 168, "30d": 720}.get(default, 24)
|
|
32
|
+
default_label = next((k for k, v in options.items() if v == default_hours), "24 hours")
|
|
33
|
+
default_idx = list(options.keys()).index(default_label)
|
|
34
|
+
|
|
35
|
+
selected = st.selectbox(
|
|
36
|
+
"Time Range",
|
|
37
|
+
options=list(options.keys()),
|
|
38
|
+
index=default_idx,
|
|
39
|
+
key=key,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return options[selected]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def date_range_filter(
|
|
46
|
+
key: str = "date_range",
|
|
47
|
+
default_days: int = 7,
|
|
48
|
+
) -> tuple[datetime, datetime]:
|
|
49
|
+
"""Render a date range picker.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
key: Unique key for the widget
|
|
53
|
+
default_days: Default number of days back
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Tuple of (start_date, end_date)
|
|
57
|
+
"""
|
|
58
|
+
end_date = datetime.now()
|
|
59
|
+
start_date = end_date - timedelta(days=default_days)
|
|
60
|
+
|
|
61
|
+
dates = st.date_input(
|
|
62
|
+
"Date Range",
|
|
63
|
+
value=(start_date.date(), end_date.date()),
|
|
64
|
+
key=key,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if isinstance(dates, tuple) and len(dates) == 2:
|
|
68
|
+
return (
|
|
69
|
+
datetime.combine(dates[0], datetime.min.time()),
|
|
70
|
+
datetime.combine(dates[1], datetime.max.time()),
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
# Single date selected
|
|
74
|
+
return (
|
|
75
|
+
datetime.combine(dates, datetime.min.time()),
|
|
76
|
+
datetime.combine(dates, datetime.max.time()),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def multi_select_filter(
|
|
81
|
+
label: str,
|
|
82
|
+
options: list[str],
|
|
83
|
+
key: str,
|
|
84
|
+
default: Optional[list[str]] = None,
|
|
85
|
+
) -> list[str]:
|
|
86
|
+
"""Render a multi-select filter.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
label: Filter label
|
|
90
|
+
options: List of options to choose from
|
|
91
|
+
key: Unique key for the widget
|
|
92
|
+
default: Default selected values
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
List of selected values
|
|
96
|
+
"""
|
|
97
|
+
return st.multiselect(
|
|
98
|
+
label,
|
|
99
|
+
options=options,
|
|
100
|
+
default=default,
|
|
101
|
+
key=key,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def text_filter(
|
|
106
|
+
label: str,
|
|
107
|
+
key: str,
|
|
108
|
+
placeholder: Optional[str] = None,
|
|
109
|
+
) -> str:
|
|
110
|
+
"""Render a text input filter.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
label: Filter label
|
|
114
|
+
key: Unique key for the widget
|
|
115
|
+
placeholder: Optional placeholder text
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Input text value
|
|
119
|
+
"""
|
|
120
|
+
return st.text_input(
|
|
121
|
+
label,
|
|
122
|
+
key=key,
|
|
123
|
+
placeholder=placeholder or f"Filter by {label.lower()}...",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def filter_row(num_cols: int = 4):
|
|
128
|
+
"""Create a row of filter columns.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
num_cols: Number of columns
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Tuple of column objects
|
|
135
|
+
"""
|
|
136
|
+
return st.columns(num_cols)
|