Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e1d1dd457 | ||
|
|
34fdc77818 | ||
|
|
c374ecbffb |
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [1.1.12] - 2026-01-04
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Control Room navigation reorganized:
|
||||||
|
- New "Stack" section with Containers and Proxy Hosts
|
||||||
|
- New "Data Management" section with PostgreSQL, Redis, Qdrant, Neo4j placeholders
|
||||||
|
- Removed: Networks, Volumes, Images (Portainer) and Redirections, Streams, Certificates (NPM)
|
||||||
|
|
||||||
|
## [1.1.11] - 2026-01-04
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- API client providers now use `keepAlive: true` to prevent Ref invalidation
|
||||||
|
- Fixes "DioException [unknown]: null" error on /security/users and other API pages
|
||||||
|
- AuthInterceptor's stored Ref was becoming invalid when provider auto-disposed
|
||||||
|
|
||||||
## [1.1.10] - 2026-01-04
|
## [1.1.10] - 2026-01-04
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import 'api_client_native.dart' if (dart.library.html) 'api_client_web.dart'
|
|||||||
part 'api_client.g.dart';
|
part 'api_client.g.dart';
|
||||||
|
|
||||||
/// Provides the Dio instance for Core API.
|
/// Provides the Dio instance for Core API.
|
||||||
@riverpod
|
///
|
||||||
|
/// Uses keepAlive to prevent auto-dispose - the AuthInterceptor stores
|
||||||
|
/// a Ref that must remain valid for the lifetime of API requests.
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
Dio coreApiClient(Ref ref) {
|
Dio coreApiClient(Ref ref) {
|
||||||
final options = BaseOptions(
|
final options = BaseOptions(
|
||||||
baseUrl: AppConfig.coreApiUrl,
|
baseUrl: AppConfig.coreApiUrl,
|
||||||
@@ -33,7 +36,10 @@ Dio coreApiClient(Ref ref) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Provides the Dio instance for Tatlock API.
|
/// Provides the Dio instance for Tatlock API.
|
||||||
@riverpod
|
///
|
||||||
|
/// Uses keepAlive to prevent auto-dispose - the AuthInterceptor stores
|
||||||
|
/// a Ref that must remain valid for the lifetime of API requests.
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
Dio tatlockApiClient(Ref ref) {
|
Dio tatlockApiClient(Ref ref) {
|
||||||
final options = BaseOptions(
|
final options = BaseOptions(
|
||||||
baseUrl: AppConfig.tatlockApiUrl,
|
baseUrl: AppConfig.tatlockApiUrl,
|
||||||
|
|||||||
@@ -69,44 +69,11 @@ class _SectionContent extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
switch (nav) {
|
return switch (nav) {
|
||||||
case ControlRoomNav.containers:
|
ControlRoomNav.containers => const _ContainersSection(),
|
||||||
return const _ContainersSection();
|
ControlRoomNav.proxyHosts => const ProxyHostsPage(),
|
||||||
case ControlRoomNav.proxyHosts:
|
_ => _PlaceholderSection(nav: nav),
|
||||||
return const ProxyHostsPage();
|
};
|
||||||
default:
|
|
||||||
return _PlaceholderSection(nav: nav);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Containers section with optional stack filter.
|
|
||||||
class _ContainersSection extends ConsumerWidget {
|
|
||||||
const _ContainersSection();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final selectedStack = ref.watch(selectedStackProvider);
|
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
|
||||||
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
// Stacks filter panel
|
|
||||||
const _StacksFilterPanel(),
|
|
||||||
// Divider
|
|
||||||
VerticalDivider(
|
|
||||||
width: 1,
|
|
||||||
thickness: 1,
|
|
||||||
color: colorScheme.outlineVariant,
|
|
||||||
),
|
|
||||||
// Main content - containers list or stack detail
|
|
||||||
Expanded(
|
|
||||||
child: selectedStack == null
|
|
||||||
? const ContainersListPage()
|
|
||||||
: StackDetailPage(stackId: selectedStack),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,6 +117,36 @@ class _PlaceholderSection extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Containers section with optional stack filter.
|
||||||
|
class _ContainersSection extends ConsumerWidget {
|
||||||
|
const _ContainersSection();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final selectedStack = ref.watch(selectedStackProvider);
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
// Stacks filter panel
|
||||||
|
const _StacksFilterPanel(),
|
||||||
|
// Divider
|
||||||
|
VerticalDivider(
|
||||||
|
width: 1,
|
||||||
|
thickness: 1,
|
||||||
|
color: colorScheme.outlineVariant,
|
||||||
|
),
|
||||||
|
// Main content - containers list or stack detail
|
||||||
|
Expanded(
|
||||||
|
child: selectedStack == null
|
||||||
|
? const ContainersListPage()
|
||||||
|
: StackDetailPage(stackId: selectedStack),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Stacks filter panel for Containers section (includes "All Containers" option).
|
/// Stacks filter panel for Containers section (includes "All Containers" option).
|
||||||
class _StacksFilterPanel extends ConsumerWidget {
|
class _StacksFilterPanel extends ConsumerWidget {
|
||||||
const _StacksFilterPanel();
|
const _StacksFilterPanel();
|
||||||
|
|||||||
@@ -7,30 +7,26 @@ import 'package:tatlock_ui/shared/layouts/widgets/nav_panel.dart';
|
|||||||
/// Route paths for Control Room.
|
/// Route paths for Control Room.
|
||||||
abstract class ControlRoomRoutes {
|
abstract class ControlRoomRoutes {
|
||||||
static const base = '/control-room';
|
static const base = '/control-room';
|
||||||
// Portainer
|
// Stack
|
||||||
static const containers = '/control-room/containers';
|
static const containers = '/control-room/containers';
|
||||||
static const networks = '/control-room/networks';
|
|
||||||
static const volumes = '/control-room/volumes';
|
|
||||||
static const images = '/control-room/images';
|
|
||||||
// NPM
|
|
||||||
static const proxyHosts = '/control-room/proxy-hosts';
|
static const proxyHosts = '/control-room/proxy-hosts';
|
||||||
static const redirections = '/control-room/redirections';
|
// Data Management
|
||||||
static const streams = '/control-room/streams';
|
static const postgres = '/control-room/postgres';
|
||||||
static const certificates = '/control-room/certificates';
|
static const redis = '/control-room/redis';
|
||||||
|
static const qdrant = '/control-room/qdrant';
|
||||||
|
static const neo4j = '/control-room/neo4j';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Control Room navigation items with section grouping.
|
/// Control Room navigation items with section grouping.
|
||||||
enum ControlRoomNav {
|
enum ControlRoomNav {
|
||||||
// Portainer section
|
// Stack section - Docker containers and reverse proxy
|
||||||
containers('containers', 'Containers', Icons.dns, 'Portainer'),
|
containers('containers', 'Containers', Icons.dns, 'Stack'),
|
||||||
networks('networks', 'Networks', Icons.hub, 'Portainer'),
|
proxyHosts('proxy-hosts', 'Proxy Hosts', Icons.public, 'Stack'),
|
||||||
volumes('volumes', 'Volumes', Icons.storage, 'Portainer'),
|
// Data Management section - Database browsers
|
||||||
images('images', 'Images', Icons.photo_library, 'Portainer'),
|
postgres('postgres', 'PostgreSQL', Icons.table_chart, 'Data Management'),
|
||||||
// NPM section
|
redis('redis', 'Redis', Icons.memory, 'Data Management'),
|
||||||
proxyHosts('proxy-hosts', 'Proxy Hosts', Icons.public, 'NPM'),
|
qdrant('qdrant', 'Qdrant', Icons.scatter_plot, 'Data Management'),
|
||||||
redirections('redirections', 'Redirections', Icons.alt_route, 'NPM'),
|
neo4j('neo4j', 'Neo4j', Icons.hub, 'Data Management');
|
||||||
streams('streams', 'Streams', Icons.stream, 'NPM'),
|
|
||||||
certificates('certificates', 'SSL Certificates', Icons.verified_user, 'NPM');
|
|
||||||
|
|
||||||
const ControlRoomNav(this.id, this.label, this.icon, this.section);
|
const ControlRoomNav(this.id, this.label, this.icon, this.section);
|
||||||
|
|
||||||
|
|||||||
+1
-4
@@ -14,10 +14,7 @@ void main() async {
|
|||||||
// Use path-based URLs on web (no-op on mobile/desktop)
|
// Use path-based URLs on web (no-op on mobile/desktop)
|
||||||
configureUrlStrategy();
|
configureUrlStrategy();
|
||||||
|
|
||||||
developer.log(
|
debugPrint('🪣 ${AppVersion.name} v${AppVersion.fullVersion}');
|
||||||
'${AppVersion.name} v${AppVersion.fullVersion}',
|
|
||||||
name: 'tatlock_ui',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Initialize auth before starting the app.
|
// Initialize auth before starting the app.
|
||||||
// This handles OIDC callback and silent auth on web.
|
// This handles OIDC callback and silent auth on web.
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.1.10+1
|
version: 1.1.12+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.10.4
|
sdk: ^3.10.4
|
||||||
|
|||||||
Reference in New Issue
Block a user