iop/apps/client/lib/client_home_page.dart

217 lines
6.4 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/nexo/nexo_notification_host_integration.dart';
class ClientHomePage extends StatefulWidget {
final ClientWireClient? testClient;
final NexoNotificationHostIntegration? notificationHost;
final ControlPlaneStatusRepository? statusRepository;
const ClientHomePage({
super.key,
this.testClient,
this.notificationHost,
this.statusRepository,
});
@override
State<ClientHomePage> createState() => _ClientHomePageState();
}
class _ClientHomePageState extends State<ClientHomePage> {
String _wireStatus = 'Disconnected';
ClientWireClient? _client;
StreamSubscription? _notificationSubscription;
HttpControlPlaneStatusRepository? _ownedStatusRepository;
late final ControlPlaneStatusController _controller;
@override
void initState() {
super.initState();
final ownedStatusRepository = widget.statusRepository == null
? HttpControlPlaneStatusRepository(
baseUrl: ClientConfig.controlPlaneHttpUrl,
)
: null;
_ownedStatusRepository = ownedStatusRepository;
final statusRepo = widget.statusRepository ?? ownedStatusRepository!;
_controller = ControlPlaneStatusController(statusRepository: statusRepo);
// Auto-connect on start
WidgetsBinding.instance.addPostFrameCallback((_) {
applyFullscreenMode();
_connectAndFetch();
});
final host = widget.notificationHost;
if (host != null) {
_notificationSubscription = host.onNotification.listen(
_showNexoNotification,
);
}
}
@override
void dispose() {
_notificationSubscription?.cancel();
_client?.close();
_controller.dispose();
_ownedStatusRepository?.close();
super.dispose();
}
void _showNexoNotification(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),
);
},
);
}
}