Skip to content

Commit a9c1a2c

Browse files
First attempt at frontend
1 parent 91edf67 commit a9c1a2c

4 files changed

Lines changed: 111 additions & 1 deletion

File tree

admin-frontend/open_admin_app/lib/api/client_api.dart

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,13 @@ class ManagementRepositoryClientBloc implements Bloc {
6767
late GroupServiceApi groupServiceApi;
6868
late WebhookServiceApi webhookServiceApi;
6969
late TrackEventsServiceApi trackEventsServiceApi;
70+
late MaintenanceBannerServiceApi _maintenanceBannerApi;
7071
static late FHRouter router;
7172

73+
final _maintenanceSource = BehaviorSubject<MaintenanceInfo?>.seeded(null);
74+
75+
Stream<MaintenanceInfo?> get maintenanceStream => _maintenanceSource.stream;
76+
7277
// this reflects actual requests to change the route driven externally, so a user clicks on
7378
// something that should cause the page to change to this route.
7479
final _routerSource = BehaviorSubject<RouteChange?>();
@@ -152,6 +157,33 @@ class ManagementRepositoryClientBloc implements Bloc {
152157
// this is for fine grained route changes, like tab changes
153158
_routerSource.add(route);
154159
prefs.saveCurrentRoute(route.toJson());
160+
161+
// poll maintenance banner on every navigation for already-logged-in users
162+
if (personState.isLoggedIn) {
163+
_checkMaintenanceBanner();
164+
}
165+
}
166+
167+
/// Temporarily hides the maintenance banner for the current session until
168+
/// the next navigation event re-polls the backend.
169+
void dismissMaintenanceBanner() {
170+
_maintenanceSource.add(null);
171+
}
172+
173+
Future<void> _checkMaintenanceBanner() async {
174+
try {
175+
final response = await _maintenanceBannerApi.apiDelegate
176+
.getMaintenanceBanner();
177+
if (response.statusCode == 204) {
178+
_maintenanceSource.add(null);
179+
} else if (response.statusCode == 200) {
180+
final info = await _maintenanceBannerApi.apiDelegate
181+
.getMaintenanceBanner_decode(response.body!);
182+
_maintenanceSource.add(info.active ? info : null);
183+
}
184+
} catch (_) {
185+
// silently ignore — maintenance check should never break navigation
186+
}
155187
}
156188

157189
void _initializeRouteStreams() {
@@ -290,6 +322,7 @@ class ManagementRepositoryClientBloc implements Bloc {
290322
groupServiceApi = GroupServiceApi(client);
291323
webhookServiceApi = WebhookServiceApi(client);
292324
trackEventsServiceApi = TrackEventsServiceApi(client);
325+
_maintenanceBannerApi = MaintenanceBannerServiceApi(client);
293326
_errorSource.add(null);
294327
streamValley.apiClient = this;
295328

@@ -342,6 +375,11 @@ class ManagementRepositoryClientBloc implements Bloc {
342375

343376
identityProviders.identityInfo = setupResponse.providerInfo;
344377

378+
// Seed maintenance banner from initial load
379+
_maintenanceSource.add(setupResponse.maintenanceInfo?.active == true
380+
? setupResponse.maintenanceInfo
381+
: null);
382+
345383
// yes its initialised, we may not have logged in yet
346384
if (bearerToken != null) {
347385
setBearerToken(bearerToken);
@@ -576,6 +614,7 @@ class ManagementRepositoryClientBloc implements Bloc {
576614
_errorSource.close();
577615
_overlaySource.close();
578616
_snackbarSource.close();
617+
_maintenanceSource.close();
579618
personState.dispose();
580619
}
581620

admin-frontend/open_admin_app/lib/widgets/common/fh_scaffold.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'package:open_admin_app/widgets/stepper/stepper_container.dart';
1212

1313
import 'fh_drawer.dart';
1414
import 'fh_error.dart';
15+
import 'maintenance_banner_widget.dart';
1516

1617
class FHScaffoldWidget extends StatefulWidget {
1718
final Widget body;
@@ -115,6 +116,7 @@ class _InternalFHScaffoldWidgetWidgetState extends StatelessWidget {
115116
mainAxisAlignment: MainAxisAlignment.start,
116117
crossAxisAlignment: CrossAxisAlignment.start,
117118
children: [
119+
const MaintenanceBannerWidget(),
118120
_mainContent(context),
119121
]),
120122
);
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import 'package:bloc_provider/bloc_provider.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:mrapi/api.dart';
4+
import 'package:open_admin_app/api/client_api.dart';
5+
6+
/// Shows a dismissible warning banner at the top of every page when the
7+
/// system is in a maintenance window. Listens to [ManagementRepositoryClientBloc.maintenanceStream]
8+
/// and rebuilds automatically when the state changes.
9+
class MaintenanceBannerWidget extends StatelessWidget {
10+
const MaintenanceBannerWidget({super.key});
11+
12+
@override
13+
Widget build(BuildContext context) {
14+
final mrBloc = BlocProvider.of<ManagementRepositoryClientBloc>(context);
15+
return StreamBuilder<MaintenanceInfo?>(
16+
stream: mrBloc.maintenanceStream,
17+
builder: (context, snapshot) {
18+
final info = snapshot.data;
19+
if (info == null || !info.active) return const SizedBox.shrink();
20+
21+
final message = (info.message != null && info.message!.isNotEmpty)
22+
? info.message!
23+
: 'The system is undergoing maintenance. Some features may be unavailable.';
24+
25+
return Material(
26+
color: Colors.transparent,
27+
child: Container(
28+
width: double.infinity,
29+
color: Theme.of(context).colorScheme.errorContainer,
30+
padding:
31+
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
32+
child: Row(
33+
crossAxisAlignment: CrossAxisAlignment.center,
34+
children: [
35+
Icon(
36+
Icons.warning_amber_rounded,
37+
color: Theme.of(context).colorScheme.onErrorContainer,
38+
size: 20,
39+
),
40+
const SizedBox(width: 8),
41+
Expanded(
42+
child: Text(
43+
message,
44+
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
45+
color:
46+
Theme.of(context).colorScheme.onErrorContainer,
47+
),
48+
),
49+
),
50+
IconButton(
51+
icon: Icon(
52+
Icons.close,
53+
size: 18,
54+
color: Theme.of(context).colorScheme.onErrorContainer,
55+
),
56+
tooltip: 'Dismiss',
57+
padding: EdgeInsets.zero,
58+
constraints: const BoxConstraints(),
59+
onPressed: () => mrBloc.dismissMaintenanceBanner(),
60+
),
61+
],
62+
),
63+
),
64+
);
65+
},
66+
);
67+
}
68+
}

backend/mr-api/mr-api.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2764,7 +2764,8 @@ components:
27642764
maintenanceInfo:
27652765
description: "If present, the system is in or near a maintenance window"
27662766
nullable: true
2767-
$ref: "#/components/schemas/MaintenanceInfo"
2767+
allOf:
2768+
- $ref: "#/components/schemas/MaintenanceInfo"
27682769
IdentityProviderInfo:
27692770
type: object
27702771
required:

0 commit comments

Comments
 (0)