원격 터미널 브리지 POC 전에 Client HTTP lifecycle과 Edge run result surface 계약을 고정해야 하므로 관련 구현, 테스트, 로드맵 상태를 함께 정리한다.
215 lines
6.4 KiB
Dart
215 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/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;
|
|
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.mattermostHost;
|
|
if (host != null) {
|
|
_notificationSubscription = host.onNotification.listen(
|
|
_showMattermostNotification,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_notificationSubscription?.cancel();
|
|
_client?.close();
|
|
_controller.dispose();
|
|
_ownedStatusRepository?.close();
|
|
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),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|