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.
Files changed (48) hide show
  1. gitflow_analytics/_version.py +1 -1
  2. gitflow_analytics/classification/batch_classifier.py +156 -4
  3. gitflow_analytics/cli.py +803 -135
  4. gitflow_analytics/config/loader.py +39 -1
  5. gitflow_analytics/config/schema.py +1 -0
  6. gitflow_analytics/core/cache.py +20 -0
  7. gitflow_analytics/core/data_fetcher.py +1051 -117
  8. gitflow_analytics/core/git_auth.py +169 -0
  9. gitflow_analytics/core/git_timeout_wrapper.py +347 -0
  10. gitflow_analytics/core/metrics_storage.py +12 -3
  11. gitflow_analytics/core/progress.py +219 -18
  12. gitflow_analytics/core/subprocess_git.py +145 -0
  13. gitflow_analytics/extractors/ml_tickets.py +3 -2
  14. gitflow_analytics/extractors/tickets.py +93 -8
  15. gitflow_analytics/integrations/jira_integration.py +1 -1
  16. gitflow_analytics/integrations/orchestrator.py +47 -29
  17. gitflow_analytics/metrics/branch_health.py +3 -2
  18. gitflow_analytics/models/database.py +72 -1
  19. gitflow_analytics/pm_framework/adapters/jira_adapter.py +12 -5
  20. gitflow_analytics/pm_framework/orchestrator.py +8 -3
  21. gitflow_analytics/qualitative/classifiers/llm/openai_client.py +24 -4
  22. gitflow_analytics/qualitative/classifiers/llm_commit_classifier.py +3 -1
  23. gitflow_analytics/qualitative/core/llm_fallback.py +34 -2
  24. gitflow_analytics/reports/narrative_writer.py +118 -74
  25. gitflow_analytics/security/__init__.py +11 -0
  26. gitflow_analytics/security/config.py +189 -0
  27. gitflow_analytics/security/extractors/__init__.py +7 -0
  28. gitflow_analytics/security/extractors/dependency_checker.py +379 -0
  29. gitflow_analytics/security/extractors/secret_detector.py +197 -0
  30. gitflow_analytics/security/extractors/vulnerability_scanner.py +333 -0
  31. gitflow_analytics/security/llm_analyzer.py +347 -0
  32. gitflow_analytics/security/reports/__init__.py +5 -0
  33. gitflow_analytics/security/reports/security_report.py +358 -0
  34. gitflow_analytics/security/security_analyzer.py +414 -0
  35. gitflow_analytics/tui/app.py +3 -1
  36. gitflow_analytics/tui/progress_adapter.py +313 -0
  37. gitflow_analytics/tui/screens/analysis_progress_screen.py +407 -46
  38. gitflow_analytics/tui/screens/results_screen.py +219 -206
  39. gitflow_analytics/ui/__init__.py +21 -0
  40. gitflow_analytics/ui/progress_display.py +1477 -0
  41. gitflow_analytics/verify_activity.py +697 -0
  42. {gitflow_analytics-1.3.11.dist-info → gitflow_analytics-3.3.0.dist-info}/METADATA +2 -1
  43. {gitflow_analytics-1.3.11.dist-info → gitflow_analytics-3.3.0.dist-info}/RECORD +47 -31
  44. gitflow_analytics/cli_rich.py +0 -503
  45. {gitflow_analytics-1.3.11.dist-info → gitflow_analytics-3.3.0.dist-info}/WHEEL +0 -0
  46. {gitflow_analytics-1.3.11.dist-info → gitflow_analytics-3.3.0.dist-info}/entry_points.txt +0 -0
  47. {gitflow_analytics-1.3.11.dist-info → gitflow_analytics-3.3.0.dist-info}/licenses/LICENSE +0 -0
  48. {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 Button, Footer, Header, Label, Rule, Static, TabbedContent, TabPane
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
- container = ScrollableContainer()
113
+ # Build all widgets first, then add to container
114
+ widgets = []
104
115
 
105
116
  # Key metrics section
106
- container.mount(Label("Analysis Summary", classes="section-title"))
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
- Console()
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
- container.mount(Rule())
163
- container.mount(Label("Top Contributors", classes="section-title"))
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
- container.mount(
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
- container.mount(Rule())
195
- container.mount(Label("Qualitative Analysis Summary", classes="section-title"))
196
- container.mount(Static(self._create_qualitative_summary()))
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
- return container
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
- container = Container()
211
+ # Create enhanced data table
212
+ developers_table = EnhancedDataTable(data=self.developers, id="developers-table")
203
213
 
204
- container.mount(Label("Developer Statistics", classes="section-title"))
205
- container.mount(
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
- container = Container()
231
+ # Build widgets list
232
+ widgets = []
227
233
 
228
- container.mount(Label("Commit Analysis", classes="section-title"))
229
- container.mount(
234
+ widgets.append(Label("Commit Analysis", classes="section-title"))
235
+ widgets.append(
230
236
  Static(
231
- f"Showing {len(self.commits)} commits. Use filters to explore specific data.",
237
+ f"Showing {len(self.commits)} commits. Click column headers to sort.",
232
238
  classes="help-text",
233
239
  )
234
240
  )
235
241
 
236
- # Prepare commits data for table display
237
- commits_data = []
238
- for commit in self.commits[:1000]: # Limit to 1000 for performance
239
- commit_row = {
240
- "date": (
241
- commit.get("timestamp", "").strftime("%Y-%m-%d")
242
- if commit.get("timestamp")
243
- else ""
244
- ),
245
- "author": commit.get("author_name", ""),
246
- "message": (
247
- commit.get("message", "")[:80] + "..."
248
- if len(commit.get("message", "")) > 80
249
- else commit.get("message", "")
250
- ),
251
- "files_changed": commit.get("files_changed_count", 0),
252
- "insertions": commit.get("insertions", 0),
253
- "deletions": commit.get("deletions", 0),
254
- "story_points": commit.get("story_points", 0),
255
- "project_key": commit.get("project_key", ""),
256
- "change_type": commit.get("change_type", "unknown"),
257
- "risk_level": commit.get("risk_level", "unknown"),
258
- }
259
- commits_data.append(commit_row)
260
-
261
- commits_table = EnhancedDataTable(data=commits_data, id="commits-table")
262
-
263
- container.mount(commits_table)
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
- with container.mount(Horizontal(classes="action-bar")):
267
- yield Button("Export Commits", id="export-commits")
268
- yield Button("Filter by Author", id="filter-author")
269
- yield Button("Filter by Project", id="filter-project")
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 container
287
+ return Container(*widgets)
272
288
 
273
289
  def _create_prs_panel(self) -> Container:
274
- """Create pull requests analysis panel."""
275
- container = Container()
276
-
277
- container.mount(Label("Pull Request Analysis", classes="section-title"))
278
- container.mount(
279
- Static(
280
- f"Showing {len(self.prs)} pull requests with metrics and timing data.",
281
- classes="help-text",
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
- # Prepare PR data for table
286
- prs_data = []
287
- for pr in self.prs:
288
- pr_row = {
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
- with container.mount(Horizontal(classes="action-bar")):
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 container
339
+ return Container(*widgets)
319
340
 
320
341
  def _create_qualitative_panel(self) -> ScrollableContainer:
321
- """Create qualitative insights panel."""
322
- container = ScrollableContainer()
342
+ """Create qualitative analysis panel with enhanced insights."""
343
+ widgets = []
323
344
 
324
- container.mount(Label("Qualitative Analysis Results", classes="section-title"))
345
+ widgets.append(Label("Qualitative Analysis Results", classes="section-title"))
325
346
 
326
- if not self._has_qualitative_data():
327
- container.mount(
328
- Static("No qualitative analysis data available.", classes="info-message")
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
- container.mount(
331
- Static("Run analysis with qualitative processing enabled to see insights here.")
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 container
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
- if "confidence_score" in commit:
353
- confidence_scores.append(commit["confidence_score"])
365
+ # Get qualitative data
366
+ qual_data = self._get_qualitative_data()
367
+ summary_data = qual_data.get("summary", {})
354
368
 
355
- # Change types distribution
356
- container.mount(Label("Change Type Distribution", classes="subsection-title"))
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("Change Type", width=20)
360
- change_table.add_column("Count", justify="right", width=10)
361
- change_table.add_column("Percentage", justify="right", width=12)
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
- pct = (count / total_commits) * 100
366
- change_table.add_row(change_type.title(), f"{count:,}", f"{pct:.1f}%")
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
- container.mount(Static(Panel(change_table, title="Change Types", border_style="cyan")))
386
+ widgets.append(Static(Panel(change_table, title="Change Types", border_style="cyan")))
371
387
 
372
- # Risk levels distribution
373
- container.mount(Rule())
374
- container.mount(Label("Risk Level Distribution", classes="subsection-title"))
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", width=10)
379
- risk_table.add_column("Percentage", justify="right", width=12)
394
+ risk_table.add_column("Count", width=10, justify="right")
395
+ risk_table.add_column("Percentage", width=12, justify="right")
380
396
 
381
- for risk_level, count in sorted(risk_levels.items(), key=lambda x: x[1], reverse=True):
382
- pct = (count / total_commits) * 100
383
- risk_table.add_row(risk_level.title(), f"{count:,}", f"{pct:.1f}%")
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
- container.mount(Static(Panel(risk_table, title="Risk Levels", border_style="red")))
402
+ widgets.append(Static(Panel(risk_table, title="Risk Levels", border_style="red")))
386
403
 
387
- # Business domains
388
- container.mount(Rule())
389
- container.mount(Label("Business Domain Activity", classes="subsection-title"))
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("Business Domain", width=25)
393
- domain_table.add_column("Count", justify="right", width=10)
394
- domain_table.add_column("Percentage", justify="right", width=12)
395
-
396
- for domain, count in sorted(domains.items(), key=lambda x: x[1], reverse=True):
397
- pct = (count / total_commits) * 100
398
- domain_table.add_row(domain.title(), f"{count:,}", f"{pct:.1f}%")
399
-
400
- container.mount(Static(Panel(domain_table, title="Business Domains", border_style="green")))
401
-
402
- # Confidence score statistics
403
- if confidence_scores:
404
- container.mount(Rule())
405
- container.mount(Label("Analysis Confidence", classes="subsection-title"))
406
-
407
- avg_confidence = sum(confidence_scores) / len(confidence_scores)
408
- min_confidence = min(confidence_scores)
409
- max_confidence = max(confidence_scores)
410
-
411
- confidence_text = f"""Average Confidence: {avg_confidence:.2f}
412
- Minimum Confidence: {min_confidence:.2f}
413
- Maximum Confidence: {max_confidence:.2f}
414
- Total Analyzed: {len(confidence_scores):,} commits"""
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
- container.mount(
417
- Static(Panel(confidence_text, title="Confidence Statistics", border_style="yellow"))
436
+ widgets.append(
437
+ Static(Panel(conf_table, title="ML Model Confidence", border_style="yellow"))
418
438
  )
419
439
 
420
- return container
440
+ return ScrollableContainer(*widgets)
421
441
 
422
442
  def _create_export_panel(self) -> Container:
423
443
  """Create export options panel."""
424
- container = Container()
444
+ widgets = []
425
445
 
426
- container.mount(Label("Export Analysis Results", classes="section-title"))
427
- container.mount(
446
+ widgets.append(Label("Export Analysis Results", classes="section-title"))
447
+ widgets.append(
428
448
  Static(
429
- "Export your analysis results in various formats for further analysis or reporting.",
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
- with container.mount(Vertical(id="export-options")):
436
- yield Button(
437
- "📄 Export Summary Report (CSV)", variant="primary", id="export-summary-csv"
438
- )
439
- yield Button("👥 Export Developer Statistics (CSV)", id="export-developers-csv")
440
- yield Button("📝 Export Commit Details (CSV)", id="export-commits-csv")
441
-
442
- if self.prs:
443
- yield Button("🔀 Export Pull Requests (CSV)", id="export-prs-csv")
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
- # Export status
453
- container.mount(Rule())
454
- container.mount(Static("", id="export-status"))
465
+ widgets.append(export_options)
466
+ widgets.append(Rule())
467
+ widgets.append(Static("", id="export-status"))
455
468
 
456
- return container
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
+ ]