diff --git a/lib/features/main_screen/main_screen.dart b/lib/features/main_screen/main_screen.dart index 4918b351..b6862d4e 100644 --- a/lib/features/main_screen/main_screen.dart +++ b/lib/features/main_screen/main_screen.dart @@ -1562,7 +1562,11 @@ class _MainContentSplitState extends State<_MainContentSplit> lastSelectedExtensionObject: ws.lastSelectedExtensionObject, lastSelectedMongoDb: ws.lastSelectedMongoDb, + lastSelectedMongoCollection: + ws.lastSelectedMongoCollection, lastSelectedRedisDb: ws.lastSelectedRedisDb, + selectedRedisKey: ws.selectedRedisKey, + lastSelectedRedisKey: ws.lastSelectedRedisKey, onNavigateHome: () { widget.workspace.value = widget.workspace.value.unselectActiveObject(); diff --git a/lib/features/main_screen/main_screen_workspace_state.dart b/lib/features/main_screen/main_screen_workspace_state.dart index a85b4333..3e8ad845 100644 --- a/lib/features/main_screen/main_screen_workspace_state.dart +++ b/lib/features/main_screen/main_screen_workspace_state.dart @@ -27,12 +27,15 @@ class MainScreenWorkspaceState { this.lastSelectedExtensionObject, this.lastSelectedMongoDb, this.lastSelectedMongoCollection, + this.selectedRedisKey, this.lastSelectedRedisDb, + this.lastSelectedRedisKey, this.isReadOnly = false, }); final ConnectionRow? activeConnection; final int? activeRedisDb; + final String? selectedRedisKey; final String? activeMongoDB; final String? selectedMongoCollection; final ({ @@ -104,6 +107,9 @@ class MainScreenWorkspaceState { /// Last Redis database index opened for 1-click return from server stats. final int? lastSelectedRedisDb; + /// Last Redis key opened for 1-click return from server stats. + final String? lastSelectedRedisKey; + final bool isReadOnly; static const empty = MainScreenWorkspaceState(); @@ -112,6 +118,7 @@ class MainScreenWorkspaceState { return MainScreenWorkspaceState( activeConnection: activeConnection, activeRedisDb: activeRedisDb, + selectedRedisKey: selectedRedisKey, activeMongoDB: activeMongoDB, selectedMongoCollection: selectedMongoCollection, selectedPostgresObject: selectedPostgresObject, @@ -130,6 +137,7 @@ class MainScreenWorkspaceState { lastSelectedMongoDb: lastSelectedMongoDb, lastSelectedMongoCollection: lastSelectedMongoCollection, lastSelectedRedisDb: lastSelectedRedisDb, + lastSelectedRedisKey: lastSelectedRedisKey, isReadOnly: !isReadOnly, ); } @@ -139,6 +147,7 @@ class MainScreenWorkspaceState { return MainScreenWorkspaceState( activeConnection: connection, activeRedisDb: null, + selectedRedisKey: null, activeMongoDB: null, selectedMongoCollection: null, selectedPostgresObject: null, @@ -165,6 +174,8 @@ class MainScreenWorkspaceState { same ? (selectedMongoCollection ?? lastSelectedMongoCollection) : null, lastSelectedRedisDb: same ? (activeRedisDb ?? lastSelectedRedisDb) : null, + lastSelectedRedisKey: + same ? (selectedRedisKey ?? lastSelectedRedisKey) : null, isReadOnly: false, ); } @@ -175,6 +186,7 @@ class MainScreenWorkspaceState { return MainScreenWorkspaceState( activeConnection: activeConnection, activeRedisDb: null, + selectedRedisKey: null, activeMongoDB: null, selectedMongoCollection: null, selectedPostgresObject: null, @@ -198,6 +210,7 @@ class MainScreenWorkspaceState { lastSelectedMongoCollection: selectedMongoCollection ?? lastSelectedMongoCollection, lastSelectedRedisDb: activeRedisDb ?? lastSelectedRedisDb, + lastSelectedRedisKey: selectedRedisKey ?? lastSelectedRedisKey, isReadOnly: isReadOnly, ); } @@ -256,7 +269,11 @@ class MainScreenWorkspaceState { break; case 'redis': if (lastSelectedRedisDb != null) { - return selectRedisDb(conn, lastSelectedRedisDb!); + return selectRedisDb( + conn, + lastSelectedRedisDb!, + key: lastSelectedRedisKey, + ); } break; default: @@ -408,10 +425,15 @@ class MainScreenWorkspaceState { ); } - MainScreenWorkspaceState selectRedisDb(ConnectionRow connection, int db) { + MainScreenWorkspaceState selectRedisDb( + ConnectionRow connection, + int db, { + String? key, + }) { return MainScreenWorkspaceState( activeConnection: connection, activeRedisDb: db, + selectedRedisKey: key, activeMongoDB: null, selectedPostgresObject: null, postgresSqlTabRequestToken: postgresSqlTabRequestToken, @@ -428,6 +450,8 @@ class MainScreenWorkspaceState { lastSelectedMongoDb: null, lastSelectedMongoCollection: null, lastSelectedRedisDb: db, + lastSelectedRedisKey: + key ?? (db == lastSelectedRedisDb ? lastSelectedRedisKey : null), isReadOnly: isReadOnly, ); } @@ -607,6 +631,7 @@ class MainScreenWorkspaceState { return other is MainScreenWorkspaceState && activeConnection?.id == other.activeConnection?.id && activeRedisDb == other.activeRedisDb && + selectedRedisKey == other.selectedRedisKey && activeMongoDB == other.activeMongoDB && selectedMongoCollection == other.selectedMongoCollection && _pgEquals(selectedPostgresObject, other.selectedPostgresObject) && @@ -627,6 +652,7 @@ class MainScreenWorkspaceState { lastSelectedMongoDb == other.lastSelectedMongoDb && lastSelectedMongoCollection == other.lastSelectedMongoCollection && lastSelectedRedisDb == other.lastSelectedRedisDb && + lastSelectedRedisKey == other.lastSelectedRedisKey && isReadOnly == other.isReadOnly; } @@ -634,6 +660,7 @@ class MainScreenWorkspaceState { int get hashCode => Object.hashAll([ activeConnection?.id, activeRedisDb, + selectedRedisKey, activeMongoDB, selectedMongoCollection, selectedPostgresObject == null @@ -705,6 +732,7 @@ class MainScreenWorkspaceState { lastSelectedMongoDb, lastSelectedMongoCollection, lastSelectedRedisDb, + lastSelectedRedisKey, isReadOnly, ]); } diff --git a/lib/features/main_screen/workspace_panel.dart b/lib/features/main_screen/workspace_panel.dart index 0eeb2696..54a75aa5 100644 --- a/lib/features/main_screen/workspace_panel.dart +++ b/lib/features/main_screen/workspace_panel.dart @@ -59,7 +59,10 @@ class WorkspacePanel extends StatefulWidget { this.lastSelectedSqliteObject, this.lastSelectedExtensionObject, this.lastSelectedMongoDb, + this.lastSelectedMongoCollection, this.lastSelectedRedisDb, + this.selectedRedisKey, + this.lastSelectedRedisKey, this.onNavigateHome, this.onRestoreLastSelectedObject, this.isReadOnly = false, @@ -164,9 +167,18 @@ class WorkspacePanel extends StatefulWidget { /// Last MongoDB database for 1-click return from stats. final String? lastSelectedMongoDb; + /// Last MongoDB collection for 1-click return from stats. + final String? lastSelectedMongoCollection; + /// Last Redis database index for 1-click return from stats. final int? lastSelectedRedisDb; + /// Currently selected Redis key, if any. + final String? selectedRedisKey; + + /// Last Redis key for 1-click return from stats. + final String? lastSelectedRedisKey; + /// Collection to open in [MongoExplorerView] after Quick Switcher jump. final String? initialMongoCollection; @@ -320,6 +332,7 @@ class _WorkspacePanelState extends State { key: ValueKey('mongo_stats_${activeConn.id}'), connectionRow: activeConn, lastSelectedMongoDb: widget.lastSelectedMongoDb, + lastSelectedMongoCollection: widget.lastSelectedMongoCollection, onRestoreLastSelectedObject: widget.onRestoreLastSelectedObject, ), object: mongoDb == null @@ -341,6 +354,7 @@ class _WorkspacePanelState extends State { key: ValueKey('redis_stats_${activeConn.id}'), connectionRow: activeConn, lastSelectedRedisDb: widget.lastSelectedRedisDb, + lastSelectedRedisKey: widget.lastSelectedRedisKey, onRestoreLastSelectedObject: widget.onRestoreLastSelectedObject, ), object: redisDb == null @@ -349,6 +363,7 @@ class _WorkspacePanelState extends State { key: ValueKey('redis_${activeConn.id}_db_$redisDb'), connectionRow: activeConn, database: redisDb, + initialKey: widget.selectedRedisKey, isReadOnly: widget.isReadOnly, ), ); diff --git a/lib/features/mongodb/mongo_explorer_view.dart b/lib/features/mongodb/mongo_explorer_view.dart index c13ffc53..6685cd40 100644 --- a/lib/features/mongodb/mongo_explorer_view.dart +++ b/lib/features/mongodb/mongo_explorer_view.dart @@ -64,6 +64,7 @@ class _MongoExplorerViewState extends material.State { // Navigation state String? _selectedDatabase; String? _selectedCollection; + String? _lastSelectedCollection; Map? _selectedDocument; @override @@ -75,6 +76,7 @@ class _MongoExplorerViewState extends material.State { } if (widget.initialCollection != null) { _selectedCollection = widget.initialCollection; + _lastSelectedCollection = widget.initialCollection; } } @@ -159,6 +161,7 @@ class _MongoExplorerViewState extends material.State { void _navigateToCollection(String collName) { setState(() { _selectedCollection = collName; + _lastSelectedCollection = collName; _selectedDocument = null; }); } @@ -310,6 +313,24 @@ class _MongoExplorerViewState extends material.State { key: ValueKey('stats_${widget.connectionRow.id}'), connectionRow: widget.connectionRow, connection: conn, + lastSelectedMongoDb: _selectedDatabase, + lastSelectedMongoCollection: + _selectedCollection ?? _lastSelectedCollection, + onRestoreLastSelectedObject: + (_selectedCollection ?? _lastSelectedCollection) != null + ? () { + setState(() { + _showStats = false; + if (_selectedCollection == null && + _lastSelectedCollection != null) { + _navigateToCollection( + _lastSelectedCollection!); + } + }); + } + : (_selectedDatabase != null + ? () => setState(() => _showStats = false) + : null), onBack: () => setState(() => _showStats = false), ), ], diff --git a/lib/features/mongodb/mongo_stats_view.dart b/lib/features/mongodb/mongo_stats_view.dart index a93790f3..23d4af9c 100644 --- a/lib/features/mongodb/mongo_stats_view.dart +++ b/lib/features/mongodb/mongo_stats_view.dart @@ -21,6 +21,7 @@ class MongoStatsView extends material.StatefulWidget { this.connection, this.onBack, this.lastSelectedMongoDb, + this.lastSelectedMongoCollection, this.onRestoreLastSelectedObject, }); @@ -35,6 +36,9 @@ class MongoStatsView extends material.StatefulWidget { /// Remembers the last visited database for 1-click return from stats. final String? lastSelectedMongoDb; + + /// Remembers the last visited collection for 1-click return from stats. + final String? lastSelectedMongoCollection; final material.VoidCallback? onRestoreLastSelectedObject; @override @@ -208,6 +212,26 @@ class _MongoStatsViewState extends material.State { final cs = Theme.of(context).colorScheme; + return material.Container( + color: cs.background, + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + material.Padding( + padding: const material.EdgeInsets.fromLTRB(24, 20, 24, 16), + child: _header(context), + ), + const Divider(height: 1), + material.Expanded( + child: _buildBody(context, cs), + ), + ], + ), + ); + } + + material.Widget _buildBody( + material.BuildContext context, shadcn.ColorScheme cs) { if (_loading) { return const material.Center( child: QueryaSpinner( @@ -273,25 +297,21 @@ class _MongoStatsViewState extends material.State { ); } - return material.Container( - color: cs.background, - child: material.RefreshIndicator( - onRefresh: _refreshNow, - child: material.LayoutBuilder( - builder: (context, constraints) { - final contentWidth = math.max(0.0, constraints.maxWidth - 48); - return material.SingleChildScrollView( - physics: const material.AlwaysScrollableScrollPhysics(), - padding: const material.EdgeInsets.all(24), - child: material.SizedBox( - width: contentWidth, - child: material.Column( - mainAxisSize: material.MainAxisSize.min, - crossAxisAlignment: material.CrossAxisAlignment.stretch, - children: [ - _header(context), - const Gap(24), - _summaryChips(context, status), + return material.RefreshIndicator( + onRefresh: _refreshNow, + child: material.LayoutBuilder( + builder: (context, constraints) { + final contentWidth = math.max(0.0, constraints.maxWidth - 48); + return material.SingleChildScrollView( + physics: const material.AlwaysScrollableScrollPhysics(), + padding: const material.EdgeInsets.all(24), + child: material.SizedBox( + width: contentWidth, + child: material.Column( + mainAxisSize: material.MainAxisSize.min, + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + _summaryChips(context, status), const Gap(24), _gridRow( _memoryCard(context, status), @@ -345,8 +365,7 @@ class _MongoStatsViewState extends material.State { ); }, ), - ), - ); + ); } material.Widget _header(material.BuildContext context) { @@ -394,7 +413,17 @@ class _MongoStatsViewState extends material.State { alignment: material.WrapAlignment.end, crossAxisAlignment: material.WrapCrossAlignment.center, children: [ - if (widget.lastSelectedMongoDb != null && + if (widget.lastSelectedMongoCollection != null && + widget.onRestoreLastSelectedObject != null) + OutlineButton( + onPressed: widget.onRestoreLastSelectedObject, + leading: const material.Icon( + material.Icons.table_chart_outlined, + size: 18, + ), + child: Text('Return to ${widget.lastSelectedMongoCollection}'), + ) + else if (widget.lastSelectedMongoDb != null && widget.onRestoreLastSelectedObject != null) OutlineButton( onPressed: widget.onRestoreLastSelectedObject, diff --git a/lib/features/redis/redis_explorer_view.dart b/lib/features/redis/redis_explorer_view.dart index c8e14c86..1a685c71 100644 --- a/lib/features/redis/redis_explorer_view.dart +++ b/lib/features/redis/redis_explorer_view.dart @@ -34,11 +34,13 @@ class RedisExplorerView extends material.StatefulWidget { super.key, required this.connectionRow, required this.database, + this.initialKey, this.isReadOnly = false, }); final ConnectionRow connectionRow; final int database; + final String? initialKey; final bool isReadOnly; @override @@ -58,6 +60,8 @@ class _RedisExplorerViewState extends material.State { // Navigation state RedisBulkValue? _selectedKey; String? _selectedKeyType; + RedisBulkValue? _lastSelectedKey; + String? _lastSelectedKeyType; // Key used to reach _RedisKeyEditorState.canNavigateAway() from the explorer. RedisKeyEditorController? _keyEditorController; @@ -65,6 +69,13 @@ class _RedisExplorerViewState extends material.State { @override void initState() { super.initState(); + if (widget.initialKey != null) { + final key = RedisBulkValue.utf8(widget.initialKey!); + _selectedKey = key; + _lastSelectedKey = key; + _selectedKeyType = 'string'; + _keyEditorController = RedisKeyEditorController(); + } _connect(); } @@ -75,8 +86,15 @@ class _RedisExplorerViewState extends material.State { oldWidget.database != widget.database) { _disconnectCurrent(); _connect(); - } else if (oldWidget.isReadOnly != widget.isReadOnly) { - unawaited(_connection?.applyClientReadOnly(widget.isReadOnly)); + } else { + if (oldWidget.isReadOnly != widget.isReadOnly) { + unawaited(_connection?.applyClientReadOnly(widget.isReadOnly)); + } + if (widget.initialKey != null && + widget.initialKey != _selectedKey?.label) { + final key = RedisBulkValue.utf8(widget.initialKey!); + _navigateToKey(key, _selectedKeyType ?? 'string'); + } } } @@ -104,8 +122,10 @@ class _RedisExplorerViewState extends material.State { setState(() { _connecting = true; _error = null; - _selectedKey = null; - _selectedKeyType = null; + if (widget.initialKey == null) { + _selectedKey = null; + _selectedKeyType = null; + } _showStats = false; }); try { @@ -141,6 +161,8 @@ class _RedisExplorerViewState extends material.State { setState(() { _selectedKey = key; _selectedKeyType = type; + _lastSelectedKey = key; + _lastSelectedKeyType = type; _keyEditorController = RedisKeyEditorController(); }); } @@ -161,6 +183,7 @@ class _RedisExplorerViewState extends material.State { void _onKeyRenamed(RedisBulkValue newKey) { setState(() { _selectedKey = newKey; + _lastSelectedKey = newKey; _keyEditorController = RedisKeyEditorController(); _refreshEpoch++; }); @@ -296,6 +319,25 @@ class _RedisExplorerViewState extends material.State { key: ValueKey('stats_${widget.connectionRow.id}'), connectionRow: widget.connectionRow, connection: conn, + lastSelectedRedisDb: widget.database, + lastSelectedRedisKey: + (_selectedKey ?? _lastSelectedKey)?.label, + onRestoreLastSelectedObject: + (_selectedKey ?? _lastSelectedKey) != null + ? () { + setState(() { + _showStats = false; + _showCli = false; + if (_selectedKey == null && + _lastSelectedKey != null) { + _selectedKey = _lastSelectedKey; + _selectedKeyType = _lastSelectedKeyType; + _keyEditorController = + RedisKeyEditorController(); + } + }); + } + : () => setState(() => _showStats = false), onBack: () => setState(() => _showStats = false), onCli: () => setState(() { _showStats = false; diff --git a/lib/features/redis/redis_view.dart b/lib/features/redis/redis_view.dart index 81a086cf..d1099191 100644 --- a/lib/features/redis/redis_view.dart +++ b/lib/features/redis/redis_view.dart @@ -45,6 +45,7 @@ class RedisView extends material.StatefulWidget { this.onBack, this.onCli, this.lastSelectedRedisDb, + this.lastSelectedRedisKey, this.onRestoreLastSelectedObject, }); @@ -62,6 +63,9 @@ class RedisView extends material.StatefulWidget { /// Remembers the last visited database index for 1-click return from stats. final int? lastSelectedRedisDb; + + /// Remembers the last visited key for 1-click return from stats. + final String? lastSelectedRedisKey; final material.VoidCallback? onRestoreLastSelectedObject; @override @@ -217,6 +221,26 @@ class _RedisViewState extends material.State { final cs = Theme.of(context).colorScheme; + return material.Container( + color: cs.background, + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + material.Padding( + padding: const material.EdgeInsets.fromLTRB(24, 20, 24, 16), + child: _header(context), + ), + const Divider(height: 1), + material.Expanded( + child: _buildBody(context, cs), + ), + ], + ), + ); + } + + material.Widget _buildBody( + material.BuildContext context, shadcn.ColorScheme cs) { if (_loading) { return const material.Center( child: QueryaSpinner( @@ -228,28 +252,30 @@ class _RedisViewState extends material.State { final err = _error; if (err != null) { - return material.Center( - child: material.Padding( - padding: const material.EdgeInsets.all(32), - child: material.Column( - mainAxisSize: material.MainAxisSize.min, - children: [ - material.Icon(material.Icons.error_outline_rounded, - size: 48, color: cs.destructive), - const Gap(16), - const Text('Connection Error').large().semiBold(), - const Gap(8), - material.SelectableText(err, - style: material.TextStyle( - color: cs.mutedForeground, fontSize: 13)), - const Gap(24), - OutlineButton( - onPressed: () => unawaited(_load(reconnect: true)), - leading: const material.Icon(material.Icons.refresh_rounded, - size: 18), - child: const Text('Retry'), - ), - ], + return material.SingleChildScrollView( + child: material.Center( + child: material.Padding( + padding: const material.EdgeInsets.all(32), + child: material.Column( + mainAxisSize: material.MainAxisSize.min, + children: [ + material.Icon(material.Icons.error_outline_rounded, + size: 48, color: cs.destructive), + const Gap(16), + const Text('Connection Error').large().semiBold(), + const Gap(8), + material.SelectableText(err, + style: material.TextStyle( + color: cs.mutedForeground, fontSize: 13)), + const Gap(24), + OutlineButton( + onPressed: () => unawaited(_load(reconnect: true)), + leading: const material.Icon(material.Icons.refresh_rounded, + size: 18), + child: const Text('Retry'), + ), + ], + ), ), ), ); @@ -257,8 +283,9 @@ class _RedisViewState extends material.State { final info = _info; if (info == null) { - return material.Center( - child: material.Padding( + return material.SingleChildScrollView( + child: material.Center( + child: material.Padding( padding: const material.EdgeInsets.all(32), child: material.Column( mainAxisSize: material.MainAxisSize.min, @@ -279,29 +306,26 @@ class _RedisViewState extends material.State { ], ), ), - ); + ), + ); } - return material.Container( - color: cs.background, - child: material.RefreshIndicator( - onRefresh: _fetch, - child: material.LayoutBuilder( - builder: (context, constraints) { - final contentWidth = math.max(0.0, constraints.maxWidth - 48); - final isNarrow = contentWidth < 560; - return material.SingleChildScrollView( - physics: const material.AlwaysScrollableScrollPhysics(), - padding: const material.EdgeInsets.all(24), - child: material.SizedBox( - width: contentWidth, - child: material.Column( - mainAxisSize: material.MainAxisSize.min, - crossAxisAlignment: material.CrossAxisAlignment.stretch, - children: [ - _header(context), - const Gap(24), - _summaryChips(context, info), + return material.RefreshIndicator( + onRefresh: _fetch, + child: material.LayoutBuilder( + builder: (context, constraints) { + final contentWidth = math.max(0.0, constraints.maxWidth - 48); + final isNarrow = contentWidth < 560; + return material.SingleChildScrollView( + physics: const material.AlwaysScrollableScrollPhysics(), + padding: const material.EdgeInsets.all(24), + child: material.SizedBox( + width: contentWidth, + child: material.Column( + mainAxisSize: material.MainAxisSize.min, + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + _summaryChips(context, info), const Gap(24), _gridRow( _memoryCard(context, info), @@ -378,72 +402,90 @@ class _RedisViewState extends material.State { ); }, ), - ), -); + ); } material.Widget _header(material.BuildContext context) { final cs = shadcn.Theme.of(context).colorScheme; - return material.Row( + return material.Column( + crossAxisAlignment: material.CrossAxisAlignment.stretch, children: [ - material.Container( - padding: const material.EdgeInsets.all(10), - decoration: material.BoxDecoration( - color: cs.primary.withValues(alpha: 0.12), - borderRadius: material.BorderRadius.circular(12), - ), - child: material.Icon(material.Icons.memory_rounded, - size: 28, color: cs.primary), - ), - const Gap(16), - material.Expanded( - child: material.Column( - crossAxisAlignment: material.CrossAxisAlignment.start, - mainAxisSize: material.MainAxisSize.min, - children: [ - Text(widget.connectionRow.name).large().semiBold(), - const Gap(4), - Text('${widget.connectionRow.host ?? 'localhost'}:${widget.connectionRow.port ?? 6379}') - .muted() - .small(), - ], - ), + material.Row( + crossAxisAlignment: material.CrossAxisAlignment.start, + children: [ + material.Container( + padding: const material.EdgeInsets.all(10), + decoration: material.BoxDecoration( + color: cs.primary.withValues(alpha: 0.12), + borderRadius: material.BorderRadius.circular(12), + ), + child: material.Icon(material.Icons.memory_rounded, + size: 28, color: cs.primary), + ), + const Gap(16), + material.Expanded( + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.start, + mainAxisSize: material.MainAxisSize.min, + children: [ + Text(widget.connectionRow.name).large().semiBold(), + const Gap(4), + Text('${widget.connectionRow.host ?? 'localhost'}:${widget.connectionRow.port ?? 6379}') + .muted() + .small(), + ], + ), + ), + ], ), - if (widget.lastSelectedRedisDb != null && - widget.onRestoreLastSelectedObject != null) ...[ - OutlineButton( - onPressed: widget.onRestoreLastSelectedObject, - leading: const material.Icon( - material.Icons.memory_outlined, - size: 18, + const Gap(12), + material.Wrap( + spacing: 8, + runSpacing: 8, + alignment: material.WrapAlignment.end, + crossAxisAlignment: material.WrapCrossAlignment.center, + children: [ + if (widget.lastSelectedRedisKey != null && + widget.onRestoreLastSelectedObject != null) + OutlineButton( + onPressed: widget.onRestoreLastSelectedObject, + leading: const material.Icon( + material.Icons.vpn_key_outlined, + size: 18, + ), + child: Text('Return to ${widget.lastSelectedRedisKey}'), + ) + else if (widget.lastSelectedRedisDb != null && + widget.onRestoreLastSelectedObject != null) + OutlineButton( + onPressed: widget.onRestoreLastSelectedObject, + leading: const material.Icon( + material.Icons.memory_outlined, + size: 18, + ), + child: Text('Return to db${widget.lastSelectedRedisDb}'), + ), + if (widget.onBack != null) + OutlineButton( + onPressed: widget.onBack, + leading: const material.Icon(material.Icons.grid_view_rounded, + size: 18), + child: const Text('Explorer'), + ), + if (widget.onCli != null) + OutlineButton( + onPressed: widget.onCli, + leading: const material.Icon(material.Icons.terminal_rounded, + size: 18), + child: const Text('CLI Console'), + ), + OutlineButton( + onPressed: () => unawaited(_refresh()), + leading: + const material.Icon(material.Icons.refresh_rounded, size: 18), + child: const Text('Refresh'), ), - child: Text('Return to db${widget.lastSelectedRedisDb}'), - ), - const Gap(8), - ], - if (widget.onBack != null) ...[ - OutlineButton( - onPressed: widget.onBack, - leading: - const material.Icon(material.Icons.grid_view_rounded, size: 18), - child: const Text('Explorer'), - ), - const Gap(8), - ], - if (widget.onCli != null) ...[ - OutlineButton( - onPressed: widget.onCli, - leading: - const material.Icon(material.Icons.terminal_rounded, size: 18), - child: const Text('CLI Console'), - ), - const Gap(8), - ], - OutlineButton( - onPressed: () => unawaited(_refresh()), - leading: - const material.Icon(material.Icons.refresh_rounded, size: 18), - child: const Text('Refresh'), + ], ), ], ); diff --git a/test/features/main_screen/main_screen_workspace_state_test.dart b/test/features/main_screen/main_screen_workspace_state_test.dart index 9a509823..4c07f1a4 100644 --- a/test/features/main_screen/main_screen_workspace_state_test.dart +++ b/test/features/main_screen/main_screen_workspace_state_test.dart @@ -283,21 +283,27 @@ void main() { // 4. MongoDB final mongoState = MainScreenWorkspaceState.empty - .selectMongoDb(mongoConn, 'analytics') + .selectMongoCollection(mongoConn, 'analytics', 'events') .unselectActiveObject(); expect(mongoState.activeMongoDB, isNull); + expect(mongoState.selectedMongoCollection, isNull); expect(mongoState.lastSelectedMongoDb, 'analytics'); + expect(mongoState.lastSelectedMongoCollection, 'events'); final restoredMongo = mongoState.restoreLastSelectedObject(); expect(restoredMongo.activeMongoDB, 'analytics'); + expect(restoredMongo.selectedMongoCollection, 'events'); // 5. Redis final redisState = MainScreenWorkspaceState.empty - .selectRedisDb(redisConn, 7) + .selectRedisDb(redisConn, 7, key: 'token:abc') .selectConnection(redisConn); expect(redisState.activeRedisDb, isNull); + expect(redisState.selectedRedisKey, isNull); expect(redisState.lastSelectedRedisDb, 7); + expect(redisState.lastSelectedRedisKey, 'token:abc'); final restoredRedis = redisState.restoreLastSelectedObject(); expect(restoredRedis.activeRedisDb, 7); + expect(restoredRedis.selectedRedisKey, 'token:abc'); }); test('restoreLastSelectedObject ignores cached references from other drivers', () { diff --git a/test/features/nosql/nosql_return_to_object_test.dart b/test/features/nosql/nosql_return_to_object_test.dart new file mode 100644 index 00000000..5b2b4de2 --- /dev/null +++ b/test/features/nosql/nosql_return_to_object_test.dart @@ -0,0 +1,169 @@ +import 'package:flutter/material.dart' as material; +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/core/storage/local_db.dart'; +import 'package:querya_desktop/features/main_screen/main_screen_workspace_state.dart'; +import 'package:querya_desktop/features/mongodb/mongo_stats_view.dart'; +import 'package:querya_desktop/features/redis/redis_view.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart' as shadcn; + +void main() { + final createdAt = DateTime.utc(2025).toIso8601String(); + final mongoConn = ConnectionRow( + type: 'mongodb', + name: 'prod_mongo', + host: '127.0.0.1', + port: 27017, + createdAt: createdAt, + id: 101, + ); + final redisConn = ConnectionRow( + type: 'redis', + name: 'prod_redis', + host: '127.0.0.1', + port: 6379, + createdAt: createdAt, + id: 102, + ); + + group('MainScreenWorkspaceState NoSQL 1-click restore', () { + test('restores MongoDB database and collection', () { + final state = MainScreenWorkspaceState.empty + .selectMongoCollection(mongoConn, 'ecommerce', 'orders') + .unselectActiveObject(); + + expect(state.activeMongoDB, isNull); + expect(state.selectedMongoCollection, isNull); + expect(state.lastSelectedMongoDb, 'ecommerce'); + expect(state.lastSelectedMongoCollection, 'orders'); + + final restored = state.restoreLastSelectedObject(); + expect(restored.activeMongoDB, 'ecommerce'); + expect(restored.selectedMongoCollection, 'orders'); + }); + + test('restores Redis database and key', () { + final state = MainScreenWorkspaceState.empty + .selectRedisDb(redisConn, 3, key: 'user:session:99') + .unselectActiveObject(); + + expect(state.activeRedisDb, isNull); + expect(state.selectedRedisKey, isNull); + expect(state.lastSelectedRedisDb, 3); + expect(state.lastSelectedRedisKey, 'user:session:99'); + + final restored = state.restoreLastSelectedObject(); + expect(restored.activeRedisDb, 3); + expect(restored.selectedRedisKey, 'user:session:99'); + }); + }); + + group('MongoStatsView Return to Collection / Database button', () { + testWidgets('shows Return to collection when collection is set', + (tester) async { + var restored = false; + await tester.pumpWidget( + shadcn.ShadcnApp( + home: material.Scaffold( + body: MongoStatsView( + connectionRow: mongoConn, + lastSelectedMongoDb: 'ecommerce', + lastSelectedMongoCollection: 'orders', + onRestoreLastSelectedObject: () => restored = true, + ), + ), + ), + ); + + final buttonFinder = find.widgetWithText( + shadcn.OutlineButton, + 'Return to orders', + ); + expect(buttonFinder, findsOneWidget); + + await tester.tap(buttonFinder); + await tester.pump(); + expect(restored, isTrue); + }); + + testWidgets('shows Return to database when only database is set', + (tester) async { + var restored = false; + await tester.pumpWidget( + shadcn.ShadcnApp( + home: material.Scaffold( + body: MongoStatsView( + connectionRow: mongoConn, + lastSelectedMongoDb: 'ecommerce', + lastSelectedMongoCollection: null, + onRestoreLastSelectedObject: () => restored = true, + ), + ), + ), + ); + + final buttonFinder = find.widgetWithText( + shadcn.OutlineButton, + 'Return to ecommerce', + ); + expect(buttonFinder, findsOneWidget); + + await tester.tap(buttonFinder); + await tester.pump(); + expect(restored, isTrue); + }); + }); + + group('RedisView Return to Key / Database button', () { + testWidgets('shows Return to key when key is set', (tester) async { + var restored = false; + await tester.pumpWidget( + shadcn.ShadcnApp( + home: material.Scaffold( + body: RedisView( + connectionRow: redisConn, + lastSelectedRedisDb: 0, + lastSelectedRedisKey: 'cache:config:app', + onRestoreLastSelectedObject: () => restored = true, + ), + ), + ), + ); + + final buttonFinder = find.widgetWithText( + shadcn.OutlineButton, + 'Return to cache:config:app', + ); + expect(buttonFinder, findsOneWidget); + + await tester.tap(buttonFinder); + await tester.pump(); + expect(restored, isTrue); + }); + + testWidgets('shows Return to db when only db is set', (tester) async { + var restored = false; + await tester.pumpWidget( + shadcn.ShadcnApp( + home: material.Scaffold( + body: RedisView( + connectionRow: redisConn, + lastSelectedRedisDb: 5, + lastSelectedRedisKey: null, + onRestoreLastSelectedObject: () => restored = true, + ), + ), + ), + ); + + final buttonFinder = find.widgetWithText( + shadcn.OutlineButton, + 'Return to db5', + ); + expect(buttonFinder, findsOneWidget); + + await tester.tap(buttonFinder); + await tester.pump(); + expect(restored, isTrue); + }); + }); +}