- Move fleet polling logic to control orchestrator - Split client state management into separate controller/repository - Add client bootstrap and home page components - Update HTTP fleet handlers and views - Add unit tests for client bootstrap and status controller - Archive completed subtask documents
199 lines
6 KiB
Dart
199 lines
6 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:iop_console/iop_console.dart';
|
|
|
|
import 'client_config.dart';
|
|
import 'control_plane_status_client.dart';
|
|
import 'control_plane_status_controller.dart';
|
|
import 'control_plane_status_widgets.dart';
|
|
import 'iop_wire/client_wire_client.dart';
|
|
import 'main.dart';
|
|
import 'src/integrations/mattermost/mattermost_push_host_integration.dart';
|
|
|
|
class ClientHomePage extends StatefulWidget {
|
|
final ClientWireClient? testClient;
|
|
final MattermostPushHostIntegration? mattermostHost;
|
|
final ControlPlaneStatusRepository? statusRepository;
|
|
|
|
const ClientHomePage({
|
|
super.key,
|
|
this.testClient,
|
|
this.mattermostHost,
|
|
this.statusRepository,
|
|
});
|
|
|
|
@override
|
|
State<ClientHomePage> createState() => _ClientHomePageState();
|
|
}
|
|
|
|
class _ClientHomePageState extends State<ClientHomePage> {
|
|
String _wireStatus = 'Disconnected';
|
|
ClientWireClient? _client;
|
|
StreamSubscription? _notificationSubscription;
|
|
|
|
late final ControlPlaneStatusController _controller;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
final statusRepo = widget.statusRepository ??
|
|
HttpControlPlaneStatusRepository(
|
|
baseUrl: ClientConfig.controlPlaneHttpUrl,
|
|
);
|
|
_controller = ControlPlaneStatusController(statusRepository: statusRepo);
|
|
|
|
// Auto-connect on start
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
applyFullscreenMode();
|
|
_connectAndFetch();
|
|
});
|
|
|
|
final host = widget.mattermostHost;
|
|
if (host != null) {
|
|
_notificationSubscription = host.onNotification.listen(
|
|
_showMattermostNotification,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_notificationSubscription?.cancel();
|
|
_client?.close();
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _showMattermostNotification(Map<String, dynamic> data) {
|
|
if (data['type'] != 'message' || !mounted) return;
|
|
|
|
final message = data['message'] as String? ?? '';
|
|
final channel = data['channel_name'] as String? ?? '';
|
|
final sender = data['sender_name'] as String? ?? '';
|
|
final content = sender.isNotEmpty
|
|
? '[$channel] $sender: $message'
|
|
: message;
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(content, maxLines: 2, overflow: TextOverflow.ellipsis),
|
|
duration: const Duration(seconds: 4),
|
|
behavior: SnackBarBehavior.floating,
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _connectAndFetch() async {
|
|
await Future.wait([
|
|
_connectWire(),
|
|
_controller.fetchEdges(),
|
|
]);
|
|
}
|
|
|
|
Future<void> _connectWire() async {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_wireStatus = 'Connecting';
|
|
});
|
|
|
|
try {
|
|
ClientWireClient client;
|
|
if (widget.testClient != null) {
|
|
client = widget.testClient!;
|
|
} else {
|
|
client = await ClientWireClient.connectToUrl(
|
|
ClientConfig.controlPlaneWireUrl,
|
|
);
|
|
}
|
|
_client = client;
|
|
|
|
// Hello handshake
|
|
final response = await client.hello(
|
|
clientId: 'client-ui',
|
|
clientVersion: '1.0.0',
|
|
);
|
|
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_wireStatus = response.ready ? 'Connected' : 'Error';
|
|
});
|
|
|
|
client.addDisconnectListener((_) {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_wireStatus = 'Disconnected';
|
|
_client = null;
|
|
});
|
|
});
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_wireStatus = 'Error';
|
|
_client = null;
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ListenableBuilder(
|
|
listenable: _controller,
|
|
builder: (context, _) {
|
|
final config = IopConsoleConfig(
|
|
controlPlaneHttpUrl: ClientConfig.controlPlaneHttpUrl,
|
|
controlPlaneWireUrl: ClientConfig.controlPlaneWireUrl,
|
|
);
|
|
|
|
final currentEdgeId = _controller.selectedEdgeId;
|
|
|
|
return IopConsoleShell(
|
|
config: config,
|
|
capabilities: iopDefaultCapabilityPack,
|
|
overview: IopConsoleOverview(
|
|
config: config,
|
|
statusText: _wireStatus,
|
|
onRefresh: _connectAndFetch,
|
|
),
|
|
edges: EdgesPanel(
|
|
edges: _controller.edges,
|
|
isLoading: _controller.loadingEdges,
|
|
error: _controller.edgesError,
|
|
selectedEdgeId: currentEdgeId,
|
|
onSelectEdge: _controller.selectEdge,
|
|
onRefresh: _controller.fetchEdges,
|
|
),
|
|
nodes: NodesPanel(
|
|
edges: _controller.edges,
|
|
selectedEdgeId: currentEdgeId,
|
|
selectedEdgeStatus: currentEdgeId != null ? _controller.edgeStatuses[currentEdgeId] : null,
|
|
isLoading: currentEdgeId != null ? (_controller.loadingStatuses[currentEdgeId] ?? false) : false,
|
|
error: currentEdgeId != null ? _controller.statusErrors[currentEdgeId] : null,
|
|
onSelectEdge: _controller.selectEdge,
|
|
onRefresh: () {
|
|
if (currentEdgeId != null) _controller.fetchEdgeStatus(currentEdgeId);
|
|
},
|
|
),
|
|
runtime: RuntimePanel(
|
|
edges: _controller.edges,
|
|
selectedEdgeId: currentEdgeId,
|
|
onSelectEdge: _controller.selectEdge,
|
|
statusRepository: _controller.statusRepository,
|
|
),
|
|
executionLogs: ExecutionLogsPanel(
|
|
edges: _controller.edges,
|
|
selectedEdgeId: currentEdgeId,
|
|
events: currentEdgeId != null ? (_controller.edgeEvents[currentEdgeId] ?? []) : [],
|
|
isLoading: currentEdgeId != null ? (_controller.loadingEvents[currentEdgeId] ?? false) : false,
|
|
error: currentEdgeId != null ? _controller.eventErrors[currentEdgeId] : null,
|
|
onSelectEdge: _controller.selectEdge,
|
|
onRefresh: () {
|
|
if (currentEdgeId != null) _controller.fetchEdgeEvents(currentEdgeId);
|
|
},
|
|
),
|
|
agent: const IopAgentPanel(capabilities: iopDefaultCapabilityPack),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|