Skip to content

Commit eec3854

Browse files
p-gentiliCopilot
andauthored
[New] add CI dashboard page (#48)
* [New] replace first page with a CI dashboard * Restore comic page * tests * moar tests * fix: use snap real home when available * Update lib/services/github_provider.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update lib/screens/dashboard_screen.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update lib/services/config_service.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Revert "fix: use snap real home when available" This reverts commit da34e0e. * fix use snap_user_common when snap'd * fix test * missing coverage --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent dfc4db6 commit eec3854

20 files changed

+2037
-57
lines changed

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,61 @@ On snap-ready systems, you can install it on the command-line with:
2828
sudo snap install standup-timer
2929
```
3030

31+
## CI/CD Dashboard
32+
33+
Stand-Up Timer can display the status of CI/CD workflows during the meeting.
34+
Configure it by creating `~/.config/standup-timer/workflows.yaml` or in
35+
`~/snap/standup-timer/current/.config/standup-timer/workflows.yaml` if
36+
running from snap.
37+
38+
### GitHub Actions
39+
40+
```yaml
41+
github_token: ghp_xxxx # optional default token for all GitHub entries
42+
43+
workflows:
44+
- label: "My workflow" # display name (optional)
45+
provider: github
46+
owner: canonical # GitHub organisation or user
47+
repo: my-repo
48+
workflow: ci.yaml # workflow filename
49+
token: ghp_xxxx # per-entry token (overrides github_token)
50+
```
51+
52+
The `token` is only required for private repositories. It can be set once as
53+
`github_token` at the top level and will apply to all GitHub entries that do
54+
not specify their own `token`.
55+
56+
### Jenkins
57+
58+
```yaml
59+
workflows:
60+
- label: "My Jenkins job" # display name (optional)
61+
provider: jenkins
62+
url: https://jenkins.example.com/job/my-job
63+
username: admin # optional
64+
token: abc123 # Jenkins API token (optional)
65+
```
66+
67+
### Mixed example
68+
69+
```yaml
70+
github_token: ghp_xxxx
71+
72+
workflows:
73+
- label: "Checkbox Daily Builds"
74+
provider: github
75+
owner: canonical
76+
repo: checkbox
77+
workflow: checkbox-daily-native-builds.yaml
78+
79+
- label: "Release pipeline"
80+
provider: jenkins
81+
url: https://jenkins.example.com/job/release
82+
username: admin
83+
token: abc123
84+
```
85+
3186
## Community and Support
3287

3388
You can report any issues, bugs, or feature requests on the project's

lib/comic.dart

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import 'dart:math';
33
import 'package:flutter/material.dart';
44
import 'package:flutter/services.dart';
55
import 'package:http/http.dart' as http;
6-
import 'widgets/timer_controls.dart';
76

87
class Comic {
98
final String title;
@@ -55,19 +54,11 @@ Future<Comic> fetchComic() async {
5554
}
5655

5756
class ComicScreen extends StatefulWidget {
58-
final bool showTimerControls;
59-
final bool isRunning;
60-
final bool isDisabled;
61-
final VoidCallback? onToggleTimer;
62-
final VoidCallback? onResetTimer;
57+
final VoidCallback? onNext;
6358

6459
const ComicScreen({
6560
super.key,
66-
this.showTimerControls = false,
67-
this.isRunning = false,
68-
this.isDisabled = false,
69-
this.onToggleTimer,
70-
this.onResetTimer,
61+
this.onNext,
7162
});
7263

7364
@override
@@ -200,7 +191,7 @@ class _ComicScreenState extends State<ComicScreen> {
200191
Expanded(
201192
child: Container(
202193
width: constraints.maxWidth,
203-
height: constraints.maxHeight - (widget.showTimerControls ? 180 : 120), // Reserve space for controls if needed
194+
height: constraints.maxHeight - (widget.onNext != null ? 180 : 120),
204195
padding: const EdgeInsets.symmetric(horizontal: 16),
205196
child: GestureDetector(
206197
onTap: () => _showImageModal(context, comic),
@@ -210,7 +201,7 @@ class _ComicScreenState extends State<ComicScreen> {
210201
comic.img,
211202
fit: BoxFit.contain,
212203
width: constraints.maxWidth - 32,
213-
height: constraints.maxHeight - (widget.showTimerControls ? 180 : 120),
204+
height: constraints.maxHeight - (widget.onNext != null ? 180 : 120),
214205
loadingBuilder: (context, child, progress) {
215206
if (progress == null) return child;
216207
return const Center(child: CircularProgressIndicator());
@@ -232,14 +223,16 @@ class _ComicScreenState extends State<ComicScreen> {
232223
textAlign: TextAlign.center,
233224
),
234225
),
235-
if (widget.showTimerControls) ...[
226+
if (widget.onNext != null) ...[
236227
Padding(
237228
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
238-
child: TimerControls(
239-
isRunning: widget.isRunning,
240-
isDisabled: widget.isDisabled,
241-
onToggleTimer: widget.onToggleTimer ?? () {},
242-
onResetTimer: widget.onResetTimer ?? () {},
229+
child: Align(
230+
alignment: Alignment.centerRight,
231+
child: OutlinedButton.icon(
232+
onPressed: widget.onNext,
233+
icon: const Icon(Icons.dashboard_outlined, size: 16),
234+
label: const Text('View CI Dashboard'),
235+
),
243236
),
244237
),
245238
],
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import 'dart:async';
2+
import 'package:flutter_riverpod/flutter_riverpod.dart';
3+
import '../services/ci_provider.dart';
4+
import '../services/config_service.dart';
5+
6+
class WorkflowsState {
7+
final List<CiRun> runs;
8+
final bool isLoading;
9+
final String? configError;
10+
final DateTime? lastFetched;
11+
12+
const WorkflowsState({
13+
this.runs = const [],
14+
this.isLoading = false,
15+
this.configError,
16+
this.lastFetched,
17+
});
18+
19+
WorkflowsState copyWith({
20+
List<CiRun>? runs,
21+
bool? isLoading,
22+
String? configError,
23+
DateTime? lastFetched,
24+
}) {
25+
return WorkflowsState(
26+
runs: runs ?? this.runs,
27+
isLoading: isLoading ?? this.isLoading,
28+
configError: configError ?? this.configError,
29+
lastFetched: lastFetched ?? this.lastFetched,
30+
);
31+
}
32+
}
33+
34+
class WorkflowsNotifier extends Notifier<WorkflowsState> {
35+
Timer? _refreshTimer;
36+
List<CiProvider>? _providers;
37+
38+
@override
39+
WorkflowsState build() {
40+
ref.onDispose(() => _refreshTimer?.cancel());
41+
// Schedule _init after build() returns so that `state` is initialized
42+
// before refresh() tries to read it.
43+
Future.microtask(_init);
44+
return const WorkflowsState(isLoading: true);
45+
}
46+
47+
void _init() {
48+
try {
49+
_providers = ConfigService.loadProviders();
50+
} catch (e) {
51+
state = WorkflowsState(configError: 'Failed to parse config: $e');
52+
return;
53+
}
54+
55+
if (_providers == null) {
56+
state = const WorkflowsState(
57+
configError: 'Create workflows.yaml to monitor CI workflows.',
58+
);
59+
return;
60+
}
61+
62+
if (_providers!.isEmpty) {
63+
state = const WorkflowsState(
64+
configError: 'No workflows listed in workflows.yaml.',
65+
);
66+
return;
67+
}
68+
69+
refresh();
70+
_refreshTimer =
71+
Timer.periodic(const Duration(seconds: 60), (_) => refresh());
72+
}
73+
74+
Future<void> refresh() async {
75+
if (_providers == null) return;
76+
state = state.copyWith(isLoading: true);
77+
78+
final runs = await Future.wait(
79+
_providers!.map((p) => p.fetchLatestRun()),
80+
);
81+
82+
state = state.copyWith(
83+
runs: runs,
84+
isLoading: false,
85+
lastFetched: DateTime.now(),
86+
);
87+
}
88+
}
89+
90+
final workflowsProvider =
91+
NotifierProvider<WorkflowsNotifier, WorkflowsState>(WorkflowsNotifier.new);

0 commit comments

Comments
 (0)