Added Agent Details including Task Details
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
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: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> {
|
||||
|
||||
late AgentDto mAgent;
|
||||
|
||||
bool mIsReloading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
mAgent = widget.agent;
|
||||
}
|
||||
|
||||
/// 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() async {
|
||||
|
||||
if (mIsReloading) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
mIsReloading = true;
|
||||
});
|
||||
|
||||
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) {
|
||||
_showMessage("This agent is no longer reported by the api.");
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
mIsReloading = false;
|
||||
});
|
||||
|
||||
_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",
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user