gitflow-analytics 1.3.11__py3-none-any.whl → 3.3.0__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.
- gitflow_analytics/_version.py +1 -1
- gitflow_analytics/classification/batch_classifier.py +156 -4
- gitflow_analytics/cli.py +803 -135
- gitflow_analytics/config/loader.py +39 -1
- gitflow_analytics/config/schema.py +1 -0
- gitflow_analytics/core/cache.py +20 -0
- gitflow_analytics/core/data_fetcher.py +1051 -117
- gitflow_analytics/core/git_auth.py +169 -0
- gitflow_analytics/core/git_timeout_wrapper.py +347 -0
- gitflow_analytics/core/metrics_storage.py +12 -3
- gitflow_analytics/core/progress.py +219 -18
- gitflow_analytics/core/subprocess_git.py +145 -0
- gitflow_analytics/extractors/ml_tickets.py +3 -2
- gitflow_analytics/extractors/tickets.py +93 -8
- gitflow_analytics/integrations/jira_integration.py +1 -1
- gitflow_analytics/integrations/orchestrator.py +47 -29
- gitflow_analytics/metrics/branch_health.py +3 -2
- gitflow_analytics/models/database.py +72 -1
- gitflow_analytics/pm_framework/adapters/jira_adapter.py +12 -5
- gitflow_analytics/pm_framework/orchestrator.py +8 -3
- gitflow_analytics/qualitative/classifiers/llm/openai_client.py +24 -4
- gitflow_analytics/qualitative/classifiers/llm_commit_classifier.py +3 -1
- gitflow_analytics/qualitative/core/llm_fallback.py +34 -2
- gitflow_analytics/reports/narrative_writer.py +118 -74
- gitflow_analytics/security/__init__.py +11 -0
- gitflow_analytics/security/config.py +189 -0
- gitflow_analytics/security/extractors/__init__.py +7 -0
- gitflow_analytics/security/extractors/dependency_checker.py +379 -0
- gitflow_analytics/security/extractors/secret_detector.py +197 -0
- gitflow_analytics/security/extractors/vulnerability_scanner.py +333 -0
- gitflow_analytics/security/llm_analyzer.py +347 -0
- gitflow_analytics/security/reports/__init__.py +5 -0
- gitflow_analytics/security/reports/security_report.py +358 -0
- gitflow_analytics/security/security_analyzer.py +414 -0
- gitflow_analytics/tui/app.py +3 -1
- gitflow_analytics/tui/progress_adapter.py +313 -0
- gitflow_analytics/tui/screens/analysis_progress_screen.py +407 -46
- gitflow_analytics/tui/screens/results_screen.py +219 -206
- gitflow_analytics/ui/__init__.py +21 -0
- gitflow_analytics/ui/progress_display.py +1477 -0
- gitflow_analytics/verify_activity.py +697 -0
- {gitflow_analytics-1.3.11.dist-info → gitflow_analytics-3.3.0.dist-info}/METADATA +2 -1
- {gitflow_analytics-1.3.11.dist-info → gitflow_analytics-3.3.0.dist-info}/RECORD +47 -31
- gitflow_analytics/cli_rich.py +0 -503
- {gitflow_analytics-1.3.11.dist-info → gitflow_analytics-3.3.0.dist-info}/WHEEL +0 -0
- {gitflow_analytics-1.3.11.dist-info → gitflow_analytics-3.3.0.dist-info}/entry_points.txt +0 -0
- {gitflow_analytics-1.3.11.dist-info → gitflow_analytics-3.3.0.dist-info}/licenses/LICENSE +0 -0
- {gitflow_analytics-1.3.11.dist-info → gitflow_analytics-3.3.0.dist-info}/top_level.txt +0 -0
|
@@ -9,7 +9,17 @@ from rich.table import Table
|
|
|
9
9
|
from textual.binding import Binding
|
|
10
10
|
from textual.containers import Container, Horizontal, ScrollableContainer, Vertical
|
|
11
11
|
from textual.screen import Screen
|
|
12
|
-
from textual.widgets import
|
|
12
|
+
from textual.widgets import (
|
|
13
|
+
Button,
|
|
14
|
+
Footer,
|
|
15
|
+
Header,
|
|
16
|
+
Label,
|
|
17
|
+
RadioButton,
|
|
18
|
+
Rule,
|
|
19
|
+
Static,
|
|
20
|
+
TabbedContent,
|
|
21
|
+
TabPane,
|
|
22
|
+
)
|
|
13
23
|
|
|
14
24
|
from gitflow_analytics.config import Config
|
|
15
25
|
|
|
@@ -100,10 +110,11 @@ class ResultsScreen(Screen):
|
|
|
100
110
|
allowing users to quickly understand the overall scope and key metrics
|
|
101
111
|
without diving into detailed data tables.
|
|
102
112
|
"""
|
|
103
|
-
|
|
113
|
+
# Build all widgets first, then add to container
|
|
114
|
+
widgets = []
|
|
104
115
|
|
|
105
116
|
# Key metrics section
|
|
106
|
-
|
|
117
|
+
widgets.append(Label("Analysis Summary", classes="section-title"))
|
|
107
118
|
|
|
108
119
|
# Create summary table
|
|
109
120
|
summary_table = Table(show_header=False, show_edge=False, pad_edge=False)
|
|
@@ -151,16 +162,13 @@ class ResultsScreen(Screen):
|
|
|
151
162
|
"Ticket Coverage", f"{ticket_coverage:.1f}%", "Commits with ticket references"
|
|
152
163
|
)
|
|
153
164
|
|
|
154
|
-
from rich.console import Console
|
|
155
165
|
from rich.panel import Panel
|
|
156
166
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
container.mount(Static(Panel(summary_table, title="Key Metrics", border_style="blue")))
|
|
167
|
+
widgets.append(Static(Panel(summary_table, title="Key Metrics", border_style="blue")))
|
|
160
168
|
|
|
161
169
|
# Top contributors section
|
|
162
|
-
|
|
163
|
-
|
|
170
|
+
widgets.append(Rule())
|
|
171
|
+
widgets.append(Label("Top Contributors", classes="section-title"))
|
|
164
172
|
|
|
165
173
|
if self.developers:
|
|
166
174
|
top_devs = sorted(
|
|
@@ -185,275 +193,280 @@ class ResultsScreen(Screen):
|
|
|
185
193
|
f"{avg_points:.1f}",
|
|
186
194
|
)
|
|
187
195
|
|
|
188
|
-
|
|
196
|
+
widgets.append(
|
|
189
197
|
Static(Panel(contrib_table, title="Developer Activity", border_style="green"))
|
|
190
198
|
)
|
|
191
199
|
|
|
192
200
|
# Qualitative insights summary (if available)
|
|
193
201
|
if self._has_qualitative_data():
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
202
|
+
widgets.append(Rule())
|
|
203
|
+
widgets.append(Label("Qualitative Analysis Summary", classes="section-title"))
|
|
204
|
+
widgets.append(Static(self._create_qualitative_summary()))
|
|
197
205
|
|
|
198
|
-
|
|
206
|
+
# Create container with all widgets
|
|
207
|
+
return ScrollableContainer(*widgets)
|
|
199
208
|
|
|
200
209
|
def _create_developers_panel(self) -> Container:
|
|
201
210
|
"""Create interactive developers data panel."""
|
|
202
|
-
|
|
211
|
+
# Create enhanced data table
|
|
212
|
+
developers_table = EnhancedDataTable(data=self.developers, id="developers-table")
|
|
203
213
|
|
|
204
|
-
container
|
|
205
|
-
|
|
214
|
+
# Create container with all widgets
|
|
215
|
+
return Container(
|
|
216
|
+
Label("Developer Statistics", classes="section-title"),
|
|
206
217
|
Static(
|
|
207
218
|
f"Showing {len(self.developers)} unique developers. Click column headers to sort.",
|
|
208
219
|
classes="help-text",
|
|
209
|
-
)
|
|
220
|
+
),
|
|
221
|
+
developers_table,
|
|
222
|
+
Horizontal(
|
|
223
|
+
Button("Export Developers", id="export-developers"),
|
|
224
|
+
Button("Show Identity Details", id="show-identities"),
|
|
225
|
+
classes="action-bar",
|
|
226
|
+
),
|
|
210
227
|
)
|
|
211
228
|
|
|
212
|
-
# Create enhanced data table
|
|
213
|
-
developers_table = EnhancedDataTable(data=self.developers, id="developers-table")
|
|
214
|
-
|
|
215
|
-
container.mount(developers_table)
|
|
216
|
-
|
|
217
|
-
# Action buttons
|
|
218
|
-
with container.mount(Horizontal(classes="action-bar")):
|
|
219
|
-
yield Button("Export Developers", id="export-developers")
|
|
220
|
-
yield Button("Show Identity Details", id="show-identities")
|
|
221
|
-
|
|
222
|
-
return container
|
|
223
|
-
|
|
224
229
|
def _create_commits_panel(self) -> Container:
|
|
225
230
|
"""Create interactive commits data panel."""
|
|
226
|
-
|
|
231
|
+
# Build widgets list
|
|
232
|
+
widgets = []
|
|
227
233
|
|
|
228
|
-
|
|
229
|
-
|
|
234
|
+
widgets.append(Label("Commit Analysis", classes="section-title"))
|
|
235
|
+
widgets.append(
|
|
230
236
|
Static(
|
|
231
|
-
f"Showing {len(self.commits)} commits.
|
|
237
|
+
f"Showing {len(self.commits)} commits. Click column headers to sort.",
|
|
232
238
|
classes="help-text",
|
|
233
239
|
)
|
|
234
240
|
)
|
|
235
241
|
|
|
236
|
-
#
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
242
|
+
# Create commits table with meaningful columns
|
|
243
|
+
commits_table = Table(show_header=True, header_style="bold magenta", expand=True)
|
|
244
|
+
commits_table.add_column("Date", width=12)
|
|
245
|
+
commits_table.add_column("Developer", width=20)
|
|
246
|
+
commits_table.add_column("Message", width=50)
|
|
247
|
+
commits_table.add_column("Points", width=8, justify="right")
|
|
248
|
+
commits_table.add_column("Tickets", width=15)
|
|
249
|
+
commits_table.add_column("Files", width=8, justify="right")
|
|
250
|
+
|
|
251
|
+
# Add rows (limit to recent 100 for performance)
|
|
252
|
+
for commit in sorted(self.commits, key=lambda c: c.get("timestamp", ""), reverse=True)[
|
|
253
|
+
:100
|
|
254
|
+
]:
|
|
255
|
+
timestamp = commit.get("timestamp", "")
|
|
256
|
+
date_str = timestamp.strftime("%Y-%m-%d") if timestamp else "Unknown"
|
|
257
|
+
|
|
258
|
+
developer = commit.get("author_name", "Unknown")[:18]
|
|
259
|
+
message = commit.get("message", "")[:48]
|
|
260
|
+
story_points = commit.get("story_points", 0) or 0
|
|
261
|
+
tickets = ", ".join(commit.get("ticket_references", []))[:13]
|
|
262
|
+
files = commit.get("files_changed", 0)
|
|
263
|
+
|
|
264
|
+
commits_table.add_row(
|
|
265
|
+
date_str,
|
|
266
|
+
developer,
|
|
267
|
+
message,
|
|
268
|
+
str(story_points) if story_points > 0 else "",
|
|
269
|
+
tickets,
|
|
270
|
+
str(files) if files > 0 else "",
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Create enhanced data table
|
|
274
|
+
commits_table_widget = EnhancedDataTable(data=self.commits[:100], id="commits-table")
|
|
275
|
+
|
|
276
|
+
widgets.append(commits_table_widget)
|
|
264
277
|
|
|
265
278
|
# Action buttons
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
279
|
+
widgets.append(
|
|
280
|
+
Horizontal(
|
|
281
|
+
Button("Export Commits", id="export-commits"),
|
|
282
|
+
Button("Show Untracked", id="show-untracked"),
|
|
283
|
+
classes="action-bar",
|
|
284
|
+
)
|
|
285
|
+
)
|
|
270
286
|
|
|
271
|
-
return
|
|
287
|
+
return Container(*widgets)
|
|
272
288
|
|
|
273
289
|
def _create_prs_panel(self) -> Container:
|
|
274
|
-
"""Create pull requests
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
290
|
+
"""Create pull requests data panel."""
|
|
291
|
+
widgets = []
|
|
292
|
+
|
|
293
|
+
widgets.append(Label("Pull Request Analysis", classes="section-title"))
|
|
294
|
+
widgets.append(Static(f"Showing {len(self.prs)} pull requests.", classes="help-text"))
|
|
295
|
+
|
|
296
|
+
# Create PR table
|
|
297
|
+
pr_table = Table(show_header=True, header_style="bold magenta", expand=True)
|
|
298
|
+
pr_table.add_column("PR #", width=8)
|
|
299
|
+
pr_table.add_column("Title", width=40)
|
|
300
|
+
pr_table.add_column("Author", width=20)
|
|
301
|
+
pr_table.add_column("Status", width=10)
|
|
302
|
+
pr_table.add_column("Created", width=12)
|
|
303
|
+
pr_table.add_column("Commits", width=8, justify="right")
|
|
304
|
+
|
|
305
|
+
# Add rows
|
|
306
|
+
for pr in self.prs[:100]: # Limit for performance
|
|
307
|
+
pr_number = pr.get("number", "")
|
|
308
|
+
title = pr.get("title", "")[:38]
|
|
309
|
+
author = pr.get("author", "Unknown")[:18]
|
|
310
|
+
status = pr.get("state", "unknown")
|
|
311
|
+
created = pr.get("created_at", "")
|
|
312
|
+
if created:
|
|
313
|
+
created_str = (
|
|
314
|
+
created.strftime("%Y-%m-%d")
|
|
315
|
+
if hasattr(created, "strftime")
|
|
316
|
+
else str(created)[:10]
|
|
317
|
+
)
|
|
318
|
+
else:
|
|
319
|
+
created_str = "Unknown"
|
|
320
|
+
commit_count = pr.get("commit_count", 0)
|
|
321
|
+
|
|
322
|
+
pr_table.add_row(
|
|
323
|
+
str(pr_number),
|
|
324
|
+
title,
|
|
325
|
+
author,
|
|
326
|
+
status,
|
|
327
|
+
created_str,
|
|
328
|
+
str(commit_count) if commit_count > 0 else "",
|
|
282
329
|
)
|
|
283
|
-
)
|
|
284
330
|
|
|
285
|
-
#
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
"title": (
|
|
290
|
-
pr.get("title", "")[:60] + "..."
|
|
291
|
-
if len(pr.get("title", "")) > 60
|
|
292
|
-
else pr.get("title", "")
|
|
293
|
-
),
|
|
294
|
-
"author": pr.get("author", ""),
|
|
295
|
-
"state": pr.get("state", ""),
|
|
296
|
-
"created_date": (
|
|
297
|
-
pr.get("created_at", "").strftime("%Y-%m-%d") if pr.get("created_at") else ""
|
|
298
|
-
),
|
|
299
|
-
"merged_date": (
|
|
300
|
-
pr.get("merged_at", "").strftime("%Y-%m-%d") if pr.get("merged_at") else ""
|
|
301
|
-
),
|
|
302
|
-
"commits": pr.get("commits_count", 0),
|
|
303
|
-
"changed_files": pr.get("changed_files", 0),
|
|
304
|
-
"additions": pr.get("additions", 0),
|
|
305
|
-
"deletions": pr.get("deletions", 0),
|
|
306
|
-
}
|
|
307
|
-
prs_data.append(pr_row)
|
|
308
|
-
|
|
309
|
-
prs_table = EnhancedDataTable(data=prs_data, id="prs-table")
|
|
310
|
-
|
|
311
|
-
container.mount(prs_table)
|
|
331
|
+
# Create enhanced data table
|
|
332
|
+
prs_table = EnhancedDataTable(data=self.prs, id="prs-table")
|
|
333
|
+
|
|
334
|
+
widgets.append(prs_table)
|
|
312
335
|
|
|
313
336
|
# Action buttons
|
|
314
|
-
|
|
315
|
-
yield Button("Export PRs", id="export-prs")
|
|
316
|
-
yield Button("Show PR Metrics", id="show-pr-metrics")
|
|
337
|
+
widgets.append(Horizontal(Button("Export PRs", id="export-prs"), classes="action-bar"))
|
|
317
338
|
|
|
318
|
-
return
|
|
339
|
+
return Container(*widgets)
|
|
319
340
|
|
|
320
341
|
def _create_qualitative_panel(self) -> ScrollableContainer:
|
|
321
|
-
"""Create qualitative
|
|
322
|
-
|
|
342
|
+
"""Create qualitative analysis panel with enhanced insights."""
|
|
343
|
+
widgets = []
|
|
323
344
|
|
|
324
|
-
|
|
345
|
+
widgets.append(Label("Qualitative Analysis Results", classes="section-title"))
|
|
325
346
|
|
|
326
|
-
if
|
|
327
|
-
|
|
328
|
-
|
|
347
|
+
if self._has_qualitative_data():
|
|
348
|
+
widgets.append(Static("AI-powered insights from commit analysis", classes="help-text"))
|
|
349
|
+
widgets.append(
|
|
350
|
+
Static(
|
|
351
|
+
"Note: Qualitative analysis is based on commit messages and may not reflect "
|
|
352
|
+
"actual implementation details.",
|
|
353
|
+
classes="warning-text",
|
|
354
|
+
)
|
|
329
355
|
)
|
|
330
|
-
|
|
331
|
-
|
|
356
|
+
else:
|
|
357
|
+
widgets.append(
|
|
358
|
+
Static(
|
|
359
|
+
"No qualitative analysis data available. Enable OpenAI integration to generate insights.",
|
|
360
|
+
classes="warning-text",
|
|
361
|
+
)
|
|
332
362
|
)
|
|
333
|
-
return
|
|
334
|
-
|
|
335
|
-
# Analyze qualitative data distributions
|
|
336
|
-
change_types = {}
|
|
337
|
-
risk_levels = {}
|
|
338
|
-
domains = {}
|
|
339
|
-
confidence_scores = []
|
|
340
|
-
|
|
341
|
-
for commit in self.commits:
|
|
342
|
-
if "change_type" in commit:
|
|
343
|
-
change_type = commit.get("change_type", "unknown")
|
|
344
|
-
change_types[change_type] = change_types.get(change_type, 0) + 1
|
|
345
|
-
|
|
346
|
-
risk_level = commit.get("risk_level", "unknown")
|
|
347
|
-
risk_levels[risk_level] = risk_levels.get(risk_level, 0) + 1
|
|
348
|
-
|
|
349
|
-
domain = commit.get("business_domain", "unknown")
|
|
350
|
-
domains[domain] = domains.get(domain, 0) + 1
|
|
363
|
+
return ScrollableContainer(*widgets)
|
|
351
364
|
|
|
352
|
-
|
|
353
|
-
|
|
365
|
+
# Get qualitative data
|
|
366
|
+
qual_data = self._get_qualitative_data()
|
|
367
|
+
summary_data = qual_data.get("summary", {})
|
|
354
368
|
|
|
355
|
-
# Change
|
|
356
|
-
|
|
369
|
+
# Change Type Distribution
|
|
370
|
+
widgets.append(Label("Change Type Distribution", classes="subsection-title"))
|
|
357
371
|
|
|
358
372
|
change_table = Table(show_header=True, header_style="bold cyan")
|
|
359
|
-
change_table.add_column("
|
|
360
|
-
change_table.add_column("Count", justify="right"
|
|
361
|
-
change_table.add_column("Percentage", justify="right"
|
|
373
|
+
change_table.add_column("Type", width=20)
|
|
374
|
+
change_table.add_column("Count", width=10, justify="right")
|
|
375
|
+
change_table.add_column("Percentage", width=12, justify="right")
|
|
376
|
+
|
|
377
|
+
change_types = summary_data.get("change_types", {})
|
|
378
|
+
total_changes = sum(change_types.values())
|
|
362
379
|
|
|
363
|
-
total_commits = len(self.commits)
|
|
364
380
|
for change_type, count in sorted(change_types.items(), key=lambda x: x[1], reverse=True):
|
|
365
|
-
|
|
366
|
-
change_table.add_row(change_type.title(),
|
|
381
|
+
percentage = (count / total_changes * 100) if total_changes > 0 else 0
|
|
382
|
+
change_table.add_row(change_type.title(), str(count), f"{percentage:.1f}%")
|
|
367
383
|
|
|
368
384
|
from rich.panel import Panel
|
|
369
385
|
|
|
370
|
-
|
|
386
|
+
widgets.append(Static(Panel(change_table, title="Change Types", border_style="cyan")))
|
|
371
387
|
|
|
372
|
-
# Risk
|
|
373
|
-
|
|
374
|
-
|
|
388
|
+
# Risk Level Distribution
|
|
389
|
+
widgets.append(Rule())
|
|
390
|
+
widgets.append(Label("Risk Level Distribution", classes="subsection-title"))
|
|
375
391
|
|
|
376
392
|
risk_table = Table(show_header=True, header_style="bold red")
|
|
377
393
|
risk_table.add_column("Risk Level", width=20)
|
|
378
|
-
risk_table.add_column("Count", justify="right"
|
|
379
|
-
risk_table.add_column("Percentage", justify="right"
|
|
394
|
+
risk_table.add_column("Count", width=10, justify="right")
|
|
395
|
+
risk_table.add_column("Percentage", width=12, justify="right")
|
|
380
396
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
397
|
+
risk_levels = summary_data.get("risk_levels", {})
|
|
398
|
+
for level, count in risk_levels.items():
|
|
399
|
+
percentage = (count / total_changes * 100) if total_changes > 0 else 0
|
|
400
|
+
risk_table.add_row(level.title(), str(count), f"{percentage:.1f}%")
|
|
384
401
|
|
|
385
|
-
|
|
402
|
+
widgets.append(Static(Panel(risk_table, title="Risk Levels", border_style="red")))
|
|
386
403
|
|
|
387
|
-
# Business
|
|
388
|
-
|
|
389
|
-
|
|
404
|
+
# Business Domain Activity
|
|
405
|
+
widgets.append(Rule())
|
|
406
|
+
widgets.append(Label("Business Domain Activity", classes="subsection-title"))
|
|
390
407
|
|
|
391
408
|
domain_table = Table(show_header=True, header_style="bold green")
|
|
392
|
-
domain_table.add_column("
|
|
393
|
-
domain_table.add_column("
|
|
394
|
-
domain_table.add_column("
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
409
|
+
domain_table.add_column("Domain", width=20)
|
|
410
|
+
domain_table.add_column("Commits", width=10, justify="right")
|
|
411
|
+
domain_table.add_column("Focus", width=12, justify="right")
|
|
412
|
+
|
|
413
|
+
domains = summary_data.get("business_domains", {})
|
|
414
|
+
for domain, count in sorted(domains.items(), key=lambda x: x[1], reverse=True)[:10]:
|
|
415
|
+
percentage = (count / total_changes * 100) if total_changes > 0 else 0
|
|
416
|
+
domain_table.add_row(domain.title(), str(count), f"{percentage:.1f}%")
|
|
417
|
+
|
|
418
|
+
widgets.append(Static(Panel(domain_table, title="Business Domains", border_style="green")))
|
|
419
|
+
|
|
420
|
+
# Analysis Confidence
|
|
421
|
+
if "confidence_scores" in summary_data:
|
|
422
|
+
widgets.append(Rule())
|
|
423
|
+
widgets.append(Label("Analysis Confidence", classes="subsection-title"))
|
|
424
|
+
|
|
425
|
+
conf_scores = summary_data["confidence_scores"]
|
|
426
|
+
conf_table = Table(show_header=False, show_edge=False)
|
|
427
|
+
conf_table.add_column("Metric", width=25)
|
|
428
|
+
conf_table.add_column("Score", width=15)
|
|
429
|
+
|
|
430
|
+
conf_table.add_row("Average Confidence", f"{conf_scores.get('average', 0):.1f}%")
|
|
431
|
+
conf_table.add_row(
|
|
432
|
+
"High Confidence", f"{conf_scores.get('high_confidence_pct', 0):.1f}%"
|
|
433
|
+
)
|
|
434
|
+
conf_table.add_row("Low Confidence", f"{conf_scores.get('low_confidence_pct', 0):.1f}%")
|
|
415
435
|
|
|
416
|
-
|
|
417
|
-
Static(Panel(
|
|
436
|
+
widgets.append(
|
|
437
|
+
Static(Panel(conf_table, title="ML Model Confidence", border_style="yellow"))
|
|
418
438
|
)
|
|
419
439
|
|
|
420
|
-
return
|
|
440
|
+
return ScrollableContainer(*widgets)
|
|
421
441
|
|
|
422
442
|
def _create_export_panel(self) -> Container:
|
|
423
443
|
"""Create export options panel."""
|
|
424
|
-
|
|
444
|
+
widgets = []
|
|
425
445
|
|
|
426
|
-
|
|
427
|
-
|
|
446
|
+
widgets.append(Label("Export Analysis Results", classes="section-title"))
|
|
447
|
+
widgets.append(
|
|
428
448
|
Static(
|
|
429
|
-
"
|
|
449
|
+
"Select export format and click export to save results.",
|
|
430
450
|
classes="help-text",
|
|
431
451
|
)
|
|
432
452
|
)
|
|
433
453
|
|
|
434
454
|
# Export options
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
)
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
if self._has_qualitative_data():
|
|
446
|
-
yield Button("🧠 Export Qualitative Insights (CSV)", id="export-qualitative-csv")
|
|
447
|
-
|
|
448
|
-
yield Rule()
|
|
449
|
-
yield Button("📊 Export Complete Dataset (JSON)", id="export-json")
|
|
450
|
-
yield Button("📋 Generate Markdown Report", id="export-markdown")
|
|
455
|
+
export_options = Vertical(
|
|
456
|
+
RadioButton("CSV Reports (Multiple Files)", id="export-csv", value=True),
|
|
457
|
+
RadioButton("JSON (Complete Data)", id="export-json"),
|
|
458
|
+
RadioButton("Markdown Report", id="export-markdown"),
|
|
459
|
+
Rule(),
|
|
460
|
+
Button("Export Now", variant="primary", id="export-now"),
|
|
461
|
+
Label("", id="export-path-label"),
|
|
462
|
+
id="export-options",
|
|
463
|
+
)
|
|
451
464
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
465
|
+
widgets.append(export_options)
|
|
466
|
+
widgets.append(Rule())
|
|
467
|
+
widgets.append(Static("", id="export-status"))
|
|
455
468
|
|
|
456
|
-
return
|
|
469
|
+
return Container(*widgets)
|
|
457
470
|
|
|
458
471
|
def _has_qualitative_data(self) -> bool:
|
|
459
472
|
"""Check if qualitative analysis data is available."""
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""UI components for GitFlow Analytics."""
|
|
2
|
+
|
|
3
|
+
from .progress_display import (
|
|
4
|
+
RICH_AVAILABLE,
|
|
5
|
+
ProgressStatistics,
|
|
6
|
+
RepositoryInfo,
|
|
7
|
+
RepositoryStatus,
|
|
8
|
+
RichProgressDisplay,
|
|
9
|
+
SimpleProgressDisplay,
|
|
10
|
+
create_progress_display,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"create_progress_display",
|
|
15
|
+
"RichProgressDisplay",
|
|
16
|
+
"SimpleProgressDisplay",
|
|
17
|
+
"RepositoryInfo",
|
|
18
|
+
"RepositoryStatus",
|
|
19
|
+
"ProgressStatistics",
|
|
20
|
+
"RICH_AVAILABLE",
|
|
21
|
+
]
|