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
@@ -197,6 +197,139 @@ class ContainerRepositoryImpl implements ContainerRepository {
|
||||
}
|
||||
```
|
||||
|
||||
## Data Model Patterns
|
||||
|
||||
Models handle conversion between API JSON and domain entities. The pattern depends on whether the feature is **read-only** or **CRUD**.
|
||||
|
||||
### Read-Only Models
|
||||
|
||||
For data fetched from external systems (Docker, NPM, Portainer) where Flutter doesn't create/update records:
|
||||
|
||||
```dart
|
||||
@freezed
|
||||
sealed class ContainerModel with _$ContainerModel {
|
||||
const factory ContainerModel({
|
||||
required String id,
|
||||
required String name,
|
||||
required String status,
|
||||
}) = _ContainerModel;
|
||||
|
||||
const ContainerModel._();
|
||||
|
||||
factory ContainerModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$ContainerModelFromJson(json);
|
||||
|
||||
/// Converts API response to domain entity.
|
||||
Container toEntity() => Container(
|
||||
id: id,
|
||||
name: name,
|
||||
status: ContainerStatus.values.byName(status),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Only `toEntity()` is needed** - no `fromEntity()` or `toJson()` required.
|
||||
|
||||
### CRUD Models (Bidirectional)
|
||||
|
||||
For data that Flutter creates, updates, and deletes:
|
||||
|
||||
```dart
|
||||
@freezed
|
||||
sealed class QuickLinkModel with _$QuickLinkModel {
|
||||
const factory QuickLinkModel({
|
||||
@Default(0) int id,
|
||||
required String title,
|
||||
required String url,
|
||||
String? icon,
|
||||
String? category,
|
||||
@Default(0) int position,
|
||||
@JsonKey(name: 'is_visible') @Default(true) bool isVisible,
|
||||
}) = _QuickLinkModel;
|
||||
|
||||
const QuickLinkModel._();
|
||||
|
||||
factory QuickLinkModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$QuickLinkModelFromJson(json);
|
||||
|
||||
/// Converts API response to domain entity.
|
||||
QuickLink toEntity() => QuickLink(
|
||||
id: id.toString(),
|
||||
name: title,
|
||||
url: url,
|
||||
iconName: icon ?? 'link',
|
||||
category: category,
|
||||
sortOrder: position,
|
||||
isActive: isVisible,
|
||||
);
|
||||
|
||||
/// Creates model from domain entity for API requests.
|
||||
factory QuickLinkModel.fromEntity(QuickLink entity) => QuickLinkModel(
|
||||
id: int.tryParse(entity.id) ?? 0,
|
||||
title: entity.name,
|
||||
url: entity.url,
|
||||
icon: entity.iconName,
|
||||
category: entity.category,
|
||||
position: entity.sortOrder,
|
||||
isVisible: entity.isActive,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Critical rules for CRUD models:**
|
||||
|
||||
1. **Always use generated `toJson()`** - Never write custom JSON methods that selectively include fields. The generated `toJson()` from freezed/json_serializable always includes ALL fields, which is the correct behavior.
|
||||
|
||||
2. **Never create custom `toCreateJson()` or similar** - This leads to bugs where fields are silently dropped.
|
||||
|
||||
3. **Use `fromEntity()` factory** - Maps domain entity fields to API field names.
|
||||
|
||||
### Form Update Pattern
|
||||
|
||||
When updating existing entities in forms, **always use `copyWith()`** to preserve existing data:
|
||||
|
||||
```dart
|
||||
// ✅ Correct - preserves all existing fields
|
||||
final updatedLink = existingLink.copyWith(
|
||||
name: _nameController.text.trim(),
|
||||
url: _urlController.text.trim(),
|
||||
category: category.isEmpty ? null : category,
|
||||
);
|
||||
await actions.update(updatedLink);
|
||||
|
||||
// ❌ Wrong - loses existing data not in form
|
||||
final newLink = QuickLink(
|
||||
id: existingLink.id,
|
||||
name: _nameController.text.trim(),
|
||||
url: _urlController.text.trim(),
|
||||
// Missing: sortOrder, other fields...
|
||||
);
|
||||
```
|
||||
|
||||
### Datasource Usage
|
||||
|
||||
```dart
|
||||
// Create - convert entity to model, use toJson()
|
||||
Future<QuickLink> createQuickLink(QuickLink link) async {
|
||||
final model = QuickLinkModel.fromEntity(link);
|
||||
final response = await _dio.post<Map<String, dynamic>>(
|
||||
_basePath,
|
||||
data: model.toJson(), // Always use generated toJson()
|
||||
);
|
||||
return QuickLinkModel.fromJson(response.data!).toEntity();
|
||||
}
|
||||
|
||||
// Update - same pattern
|
||||
Future<QuickLink> updateQuickLink(QuickLink link) async {
|
||||
final model = QuickLinkModel.fromEntity(link);
|
||||
final response = await _dio.put<Map<String, dynamic>>(
|
||||
'$_basePath/${link.id}',
|
||||
data: model.toJson(), // Always use generated toJson()
|
||||
);
|
||||
return QuickLinkModel.fromJson(response.data!).toEntity();
|
||||
}
|
||||
```
|
||||
|
||||
### Presentation Layer (Flutter + Riverpod)
|
||||
|
||||
The presentation layer contains UI code and state management.
|
||||
|
||||
Reference in New Issue
Block a user