chore: release v0.3.3
Build and Push / build (release) Successful in 3m6s

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:
Jeroen Schweitzer
2026-01-03 16:12:18 +01:00
co-authored by Claude Opus 4.5
parent 0d15d3dbb5
commit bdced8739c
26 changed files with 499 additions and 136 deletions
@@ -35,6 +35,9 @@ class DataGridController<T> extends StateNotifier<DataGridState<T>> {
/// Debounce timer for search.
Timer? _searchDebounce;
/// All items before local filtering (for local search).
List<T> _allItems = [];
@override
void dispose() {
_searchDebounce?.cancel();
@@ -57,16 +60,21 @@ class DataGridController<T> extends StateNotifier<DataGridState<T>> {
final (offset, limit) = _getPaginationParams();
final result = await source.fetch(
searchQuery: state.searchQuery.isEmpty ? null : state.searchQuery,
sortField: sortField,
sortDescending: state.sortDescending,
offset: offset,
limit: limit,
);
// Store all items for local filtering
_allItems = result.items;
// Apply local filter if search query exists
final filteredItems = _applyLocalFilter(_allItems);
state = state.copyWith(
items: result.items,
totalCount: result.totalCount,
items: filteredItems,
totalCount: filteredItems.length,
hasMore: result.hasMore,
isLoading: false,
isInitialLoad: false,
@@ -82,6 +90,26 @@ class DataGridController<T> extends StateNotifier<DataGridState<T>> {
}
}
/// Applies local filtering based on search query.
List<T> _applyLocalFilter(List<T> items) {
if (state.searchQuery.isEmpty) return items;
final query = state.searchQuery.toLowerCase();
return items.where((item) {
// Check all searchable columns
for (final column in config.columns) {
if (column.searchable) {
final value = column.valueBuilder(item);
if (value.toLowerCase().contains(query)) {
return true;
}
}
}
return false;
}).toList();
}
/// Gets pagination parameters based on data mode.
(int?, int?) _getPaginationParams() {
return switch (config.dataMode) {
@@ -97,13 +125,13 @@ class DataGridController<T> extends StateNotifier<DataGridState<T>> {
/// Refreshes the grid data.
Future<void> refresh() => _load(refresh: true);
/// Sets the search query with debouncing.
/// Sets the search query with debouncing (filters locally).
void search(String query) {
_searchDebounce?.cancel();
_searchDebounce = Timer(const Duration(milliseconds: 300), () {
_searchDebounce = Timer(const Duration(milliseconds: 150), () {
if (state.searchQuery != query) {
state = state.copyWith(searchQuery: query, currentPage: 0);
_load(refresh: true);
_applyFilterAndUpdateState();
}
});
}
@@ -113,10 +141,19 @@ class DataGridController<T> extends StateNotifier<DataGridState<T>> {
_searchDebounce?.cancel();
if (state.searchQuery.isNotEmpty) {
state = state.copyWith(searchQuery: '', currentPage: 0);
_load(refresh: true);
_applyFilterAndUpdateState();
}
}
/// Applies local filter and updates state with filtered items.
void _applyFilterAndUpdateState() {
final filteredItems = _applyLocalFilter(_allItems);
state = state.copyWith(
items: filteredItems,
totalCount: filteredItems.length,
);
}
/// Sorts by the given column index.
void sortBy(int columnIndex) {
final column = config.columns[columnIndex];