Features: - Health check endpoint for Portainer monitoring - Local search filtering in DataGrid - Container status badges reflect health (green/orange/blue) Improvements: - Standardized 56px header heights across panels - Container grid parses Docker API format correctly - Search bar styling improvements - Status badges have consistent width Fixes: - Quick links persistence (link type, form refresh) - Iframe switching closes existing content first - ContainerState type conflict resolved Branding: - Updated favicon and icons with Tatlock bucket logo 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
0d15d3dbb5
commit
bdced8739c
@@ -7,6 +7,9 @@ import 'package:tatlock_ui/features/control_room/containers/presentation/widgets
|
||||
import 'package:tatlock_ui/shared/components/data_grid/adapters/core_api_source.dart';
|
||||
import 'package:tatlock_ui/shared/components/data_grid/data_grid_exports.dart';
|
||||
|
||||
/// Health status of a container.
|
||||
enum ContainerHealth { healthy, unhealthy, starting, none }
|
||||
|
||||
/// Container data class for DataGrid display.
|
||||
class ContainerData {
|
||||
ContainerData({
|
||||
@@ -17,25 +20,45 @@ class ContainerData {
|
||||
required this.state,
|
||||
required this.status,
|
||||
required this.ports,
|
||||
required this.health,
|
||||
});
|
||||
|
||||
factory ContainerData.fromJson(Map<String, dynamic> json) {
|
||||
final ports = (json['ports'] as List<dynamic>?)
|
||||
// Docker API uses capitalized keys
|
||||
final ports = (json['Ports'] as List<dynamic>?)
|
||||
?.map((p) => ContainerPort.fromJson(p as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
final fullId = json['Id'] as String? ?? '';
|
||||
final names = json['Names'] as List<dynamic>? ?? [];
|
||||
final name = names.isNotEmpty
|
||||
? (names.first as String).replaceFirst('/', '')
|
||||
: 'Unknown';
|
||||
|
||||
final status = json['Status'] as String? ?? 'Unknown';
|
||||
|
||||
return ContainerData(
|
||||
id: json['id'] as String,
|
||||
fullId: json['full_id'] as String? ?? json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
image: json['image'] as String,
|
||||
state: json['state'] as String,
|
||||
status: json['status'] as String,
|
||||
id: fullId.length > 12 ? fullId.substring(0, 12) : fullId,
|
||||
fullId: fullId,
|
||||
name: name,
|
||||
image: json['Image'] as String? ?? 'Unknown',
|
||||
state: json['State'] as String? ?? 'unknown',
|
||||
status: status,
|
||||
ports: ports,
|
||||
health: _parseHealth(status),
|
||||
);
|
||||
}
|
||||
|
||||
/// Parse health status from Docker status string (e.g., "Up 2 hours (healthy)")
|
||||
static ContainerHealth _parseHealth(String status) {
|
||||
final lower = status.toLowerCase();
|
||||
if (lower.contains('(healthy)')) return ContainerHealth.healthy;
|
||||
if (lower.contains('(unhealthy)')) return ContainerHealth.unhealthy;
|
||||
if (lower.contains('(health: starting)')) return ContainerHealth.starting;
|
||||
return ContainerHealth.none;
|
||||
}
|
||||
|
||||
final String id;
|
||||
final String fullId;
|
||||
final String name;
|
||||
@@ -43,6 +66,16 @@ class ContainerData {
|
||||
final String state;
|
||||
final String status;
|
||||
final List<ContainerPort> ports;
|
||||
final ContainerHealth health;
|
||||
|
||||
/// Status string with health info stripped (just shows uptime).
|
||||
String get displayStatus {
|
||||
return status
|
||||
.replaceAll(RegExp(r'\s*\(healthy\)', caseSensitive: false), '')
|
||||
.replaceAll(RegExp(r'\s*\(unhealthy\)', caseSensitive: false), '')
|
||||
.replaceAll(RegExp(r'\s*\(health: starting\)', caseSensitive: false), '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
bool get canStart => state == 'exited' || state == 'created';
|
||||
bool get canStop => state == 'running';
|
||||
@@ -53,9 +86,10 @@ class ContainerPort {
|
||||
ContainerPort({required this.privatePort, this.publicPort, this.type = 'tcp'});
|
||||
|
||||
factory ContainerPort.fromJson(Map<String, dynamic> json) => ContainerPort(
|
||||
privatePort: json['private_port'] as int? ?? json['PrivatePort'] as int? ?? 0,
|
||||
publicPort: json['public_port'] as int? ?? json['PublicPort'] as int?,
|
||||
type: json['type'] as String? ?? json['Type'] as String? ?? 'tcp',
|
||||
// Docker API uses PrivatePort/PublicPort
|
||||
privatePort: json['PrivatePort'] as int? ?? 0,
|
||||
publicPort: json['PublicPort'] as int?,
|
||||
type: json['Type'] as String? ?? 'tcp',
|
||||
);
|
||||
|
||||
final int privatePort;
|
||||
@@ -99,7 +133,7 @@ class _ContainersListPageState extends ConsumerState<ContainersListPage> {
|
||||
columns: [
|
||||
DataGridColumn<ContainerData>(
|
||||
header: 'Container',
|
||||
valueBuilder: (c) => c.name,
|
||||
valueBuilder: (c) => '${c.name} ${c.image}',
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
width: const DataGridColumnWidth.flex(2),
|
||||
@@ -113,8 +147,8 @@ class _ContainersListPageState extends ConsumerState<ContainersListPage> {
|
||||
),
|
||||
DataGridColumn<ContainerData>(
|
||||
header: 'Status',
|
||||
valueBuilder: (c) => c.status,
|
||||
width: const DataGridColumnWidth.fixed(140),
|
||||
valueBuilder: (c) => c.displayStatus,
|
||||
width: const DataGridColumnWidth.fixed(160),
|
||||
alignment: DataGridColumnAlignment.end,
|
||||
),
|
||||
],
|
||||
@@ -242,7 +276,7 @@ class _ContainerCell extends StatelessWidget {
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
ContainerStatusBadge.fromString(container.state),
|
||||
ContainerStatusBadge.fromString(container.state, health: container.health),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
|
||||
Reference in New Issue
Block a user