258 lines
6.9 KiB
Dart
258 lines
6.9 KiB
Dart
import 'package:ac_web/Api/AcApi.dart';
|
|
import 'package:ac_web/Api/Dtos/AgentDto.dart';
|
|
import 'package:ac_web/Api/Dtos/AgentTaskDto.dart';
|
|
import 'package:ac_web/Widgets/AgentStatusIndicator.dart';
|
|
import 'package:ac_web/Widgets/AgentTaskCard.dart';
|
|
import 'package:ac_web/Widgets/AgentTypeIcons.dart';
|
|
import 'package:ac_web/Widgets/AutoRefreshState.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
/// Page that shows every detail of a single agent, including all of its tasks
|
|
/// together with the status updates the agent reported for them.
|
|
class AgentDetails extends StatefulWidget {
|
|
const AgentDetails({super.key, required this.agent});
|
|
|
|
/// The agent as it was known when the page got opened.
|
|
final AgentDto agent;
|
|
|
|
@override
|
|
State<AgentDetails> createState() => _AgentDetailsState();
|
|
}
|
|
|
|
class _AgentDetailsState extends State<AgentDetails>
|
|
with AutoRefreshState<AgentDetails> {
|
|
|
|
late AgentDto mAgent;
|
|
|
|
bool mIsReloading = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
mAgent = widget.agent;
|
|
}
|
|
|
|
/// Reload triggered by the user through the app bar or by pulling the page
|
|
/// down. Reports what happened, even when nothing changed.
|
|
Future<void> ReloadAgent() => _reloadAgent(silent: false);
|
|
|
|
/// Automatic reload. It runs without the user asking for it, so it neither
|
|
/// disables the reload button nor pops up a message - the page simply keeps
|
|
/// showing the agent it already has.
|
|
@override
|
|
Future<void> RefreshData() => _reloadAgent(silent: true);
|
|
|
|
/// The api only offers the whole list of agents, so the agent this page
|
|
/// shows is looked up by its name again.
|
|
Future<void> _reloadAgent({required bool silent}) async {
|
|
|
|
if (mIsReloading) {
|
|
return;
|
|
}
|
|
|
|
mIsReloading = true;
|
|
|
|
if (!silent) {
|
|
setState(() {});
|
|
}
|
|
|
|
try {
|
|
final List<AgentDto> agents = await AcApi.instance.agents.getAgents();
|
|
|
|
final AgentDto? updated = agents
|
|
.where((agent) => agent.name == mAgent.name)
|
|
.firstOrNull;
|
|
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
mIsReloading = false;
|
|
if (updated != null) {
|
|
mAgent = updated;
|
|
}
|
|
});
|
|
|
|
if (updated == null && !silent) {
|
|
_showMessage("This agent is no longer reported by the api.");
|
|
}
|
|
} catch (error) {
|
|
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
mIsReloading = false;
|
|
});
|
|
|
|
if (!silent) {
|
|
_showMessage("Could not reload the agent: $error");
|
|
}
|
|
}
|
|
}
|
|
|
|
void _showMessage(String message) {
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(mAgent.name.isEmpty ? "Agent" : mAgent.name),
|
|
actions: [
|
|
IconButton(
|
|
onPressed: mIsReloading ? null : ReloadAgent,
|
|
icon: const Icon(Icons.refresh),
|
|
tooltip: "Reload",
|
|
),
|
|
],
|
|
),
|
|
body: RefreshIndicator(
|
|
onRefresh: ReloadAgent,
|
|
child: ListView(
|
|
padding: EdgeInsetsGeometry.all(16),
|
|
children: [
|
|
_buildHeader(context),
|
|
const SizedBox(height: 12),
|
|
_buildInfoRow(context, "Type", mAgent.type.displayName),
|
|
const SizedBox(height: 8),
|
|
_buildStatusRow(context),
|
|
const SizedBox(height: 8),
|
|
_buildInfoRow(context, "Last seen", _formatTimestamp(mAgent.lastSeen.toLocal())),
|
|
const SizedBox(height: 8),
|
|
_buildInfoRow(context, "Tasks", _taskCountLabel()),
|
|
const Divider(height: 32),
|
|
Text("Tasks", style: Theme.of(context).textTheme.titleLarge),
|
|
const SizedBox(height: 8),
|
|
..._buildTasks(context),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHeader(BuildContext context) {
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
spacing: 16,
|
|
children: [
|
|
CircleAvatar(
|
|
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
|
child: Icon(mAgent.type.icon, color: Theme.of(context).colorScheme.onSurface),
|
|
),
|
|
Expanded(
|
|
child: Text(
|
|
mAgent.name.isEmpty ? "Unnamed Agent" : mAgent.name,
|
|
style: Theme.of(context).textTheme.headlineSmall,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// Same layout as the other info rows, but with the colored status circle.
|
|
Widget _buildStatusRow(BuildContext context) {
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
spacing: 8,
|
|
children: [
|
|
SizedBox(
|
|
width: 96,
|
|
child: Text("Status", style: Theme.of(context).textTheme.labelLarge),
|
|
),
|
|
AgentStatusIndicator(status: mAgent.status, size: 14),
|
|
const SizedBox(width: 4),
|
|
Expanded(child: Text(mAgent.status.displayName)),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildInfoRow(BuildContext context, String label, String value) {
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
spacing: 8,
|
|
children: [
|
|
SizedBox(
|
|
width: 96,
|
|
child: Text(label, style: Theme.of(context).textTheme.labelLarge),
|
|
),
|
|
Expanded(child: SelectableText(value)),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// One card per task, newest first, with the newest one already expanded.
|
|
List<Widget> _buildTasks(BuildContext context) {
|
|
|
|
if (!mAgent.hasTasks) {
|
|
return [
|
|
Text(
|
|
"This agent is currently not working on any task.",
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
),
|
|
];
|
|
}
|
|
|
|
final List<AgentTaskDto> tasks = mAgent.tasks.toList()
|
|
..sort(_byStartTimeDescending);
|
|
|
|
return [
|
|
for (int i = 0; i < tasks.length; i++)
|
|
AgentTaskCard(task: tasks[i], initiallyExpanded: i == 0),
|
|
];
|
|
}
|
|
|
|
/// Newest task first. Tasks without a start time are kept at the end.
|
|
int _byStartTimeDescending(AgentTaskDto left, AgentTaskDto right) {
|
|
|
|
if (left.startTime == null && right.startTime == null) {
|
|
return 0;
|
|
}
|
|
if (left.startTime == null) {
|
|
return 1;
|
|
}
|
|
if (right.startTime == null) {
|
|
return -1;
|
|
}
|
|
return right.startTime!.compareTo(left.startTime!);
|
|
}
|
|
|
|
String _taskCountLabel() {
|
|
|
|
final int count = mAgent.tasks.length;
|
|
|
|
if (count == 0) {
|
|
return "No running tasks";
|
|
}
|
|
return count == 1 ? "1 running task" : "$count running tasks";
|
|
}
|
|
|
|
/// "15. August 2025, 14:32"
|
|
String _formatTimestamp(DateTime timestamp) {
|
|
|
|
final String hour = timestamp.hour.toString().padLeft(2, '0');
|
|
final String minute = timestamp.minute.toString().padLeft(2, '0');
|
|
|
|
return "${timestamp.day}. ${_monthNames[timestamp.month - 1]} ${timestamp.year}, $hour:$minute";
|
|
}
|
|
|
|
static const List<String> _monthNames = <String>[
|
|
"January",
|
|
"February",
|
|
"March",
|
|
"April",
|
|
"May",
|
|
"June",
|
|
"July",
|
|
"August",
|
|
"September",
|
|
"October",
|
|
"November",
|
|
"December",
|
|
];
|
|
}
|