From bfa426af9e9a07343533087d3a19fccea08c32e2 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Mon, 27 Jul 2026 21:27:01 +0300 Subject: [PATCH] fix(ui): tree errors, empty table state, scrollable toolbars Show PG/MySQL sidebar load errors with Retry; empty browse shows empty-state + Retry; MySQL/SQLite toolbars scroll horizontally when narrow. Closes #449 Closes #450 Closes #451 --- .../connections/connections_panel_mysql.dart | 37 ++++- .../connections_panel_pg_tree.dart | 74 +++++++++- lib/features/mysql/mysql_table_view.dart | 139 ++++++++++++------ .../postgresql/postgres_table_view.dart | 28 +++- lib/features/sqlite/sqlite_table_view.dart | 110 ++++++++++---- 5 files changed, 308 insertions(+), 80 deletions(-) diff --git a/lib/features/connections/connections_panel_mysql.dart b/lib/features/connections/connections_panel_mysql.dart index fb88a466..3d801c6f 100644 --- a/lib/features/connections/connections_panel_mysql.dart +++ b/lib/features/connections/connections_panel_mysql.dart @@ -369,6 +369,7 @@ class _MysqlDatabaseNode extends StatefulWidget { class _MysqlDatabaseNodeState extends State<_MysqlDatabaseNode> { bool _expanded = false; bool _loading = false; + String? _error; List _tables = []; List _views = []; List _procedures = []; @@ -388,7 +389,10 @@ class _MysqlDatabaseNodeState extends State<_MysqlDatabaseNode> { Future _loadTables() async { if (!mounted) return; - setState(() => _loading = true); + setState(() { + _loading = true; + _error = null; + }); MysqlLease? lease; try { final c = widget.connection; @@ -412,10 +416,14 @@ class _MysqlDatabaseNodeState extends State<_MysqlDatabaseNode> { _procedures = procs; _functions = funcs; _loading = false; + _error = null; }); } catch (e) { if (!mounted) return; - setState(() => _loading = false); + setState(() { + _error = e.toString(); + _loading = false; + }); } finally { lease?.release(); } @@ -480,6 +488,31 @@ class _MysqlDatabaseNodeState extends State<_MysqlDatabaseNode> { const Text('Loading...').muted().xSmall(), ], ), + ) + else if (_error != null) + material.Padding( + padding: const material.EdgeInsets.only( + left: 24, + top: 4, + bottom: 8, + ), + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.start, + children: [ + material.SelectableText( + _error!, + style: material.TextStyle( + fontSize: 11, + color: theme.colorScheme.destructive, + ), + ), + const material.SizedBox(height: 6), + GhostButton( + onPressed: _loadTables, + child: const Text('Retry'), + ), + ], + ), ), if (_tables.isNotEmpty || _views.isNotEmpty || diff --git a/lib/features/connections/connections_panel_pg_tree.dart b/lib/features/connections/connections_panel_pg_tree.dart index aee015ff..868cf1be 100644 --- a/lib/features/connections/connections_panel_pg_tree.dart +++ b/lib/features/connections/connections_panel_pg_tree.dart @@ -285,6 +285,7 @@ class _PgDatabaseNode extends StatefulWidget { class _PgDatabaseNodeState extends State<_PgDatabaseNode> { bool _expanded = false; bool _loading = false; + String? _error; List _schemas = []; void _toggle() { @@ -296,7 +297,10 @@ class _PgDatabaseNodeState extends State<_PgDatabaseNode> { Future _loadSchemas() async { if (!mounted) return; - setState(() => _loading = true); + setState(() { + _loading = true; + _error = null; + }); PgLease? lease; try { final c = widget.connection; @@ -310,10 +314,14 @@ class _PgDatabaseNodeState extends State<_PgDatabaseNode> { setState(() { _schemas = schemas; _loading = false; + _error = null; }); } catch (e) { if (!mounted) return; - setState(() => _loading = false); + setState(() { + _error = e.toString(); + _loading = false; + }); } finally { lease?.release(); } @@ -395,6 +403,31 @@ class _PgDatabaseNodeState extends State<_PgDatabaseNode> { const Text('Loading...').muted().xSmall(), ], ), + ) + else if (_error != null) + material.Padding( + padding: const material.EdgeInsets.only( + left: 24, + top: 4, + bottom: 8, + ), + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.start, + children: [ + material.SelectableText( + _error!, + style: material.TextStyle( + fontSize: 11, + color: theme.colorScheme.destructive, + ), + ), + const material.SizedBox(height: 6), + GhostButton( + onPressed: _loadSchemas, + child: const Text('Retry'), + ), + ], + ), ), if (_schemas.isNotEmpty) _PgSchemasNode( @@ -597,6 +630,7 @@ class _PgSchemaNode extends StatefulWidget { class _PgSchemaNodeState extends State<_PgSchemaNode> { bool _expanded = false; bool _loading = false; + String? _error; List _tables = []; List _views = []; List _matviews = []; @@ -613,7 +647,10 @@ class _PgSchemaNodeState extends State<_PgSchemaNode> { Future _loadObjects() async { if (!mounted) return; - setState(() => _loading = true); + setState(() { + _loading = true; + _error = null; + }); PgLease? lease; try { final c = widget.connection; @@ -642,10 +679,14 @@ class _PgSchemaNodeState extends State<_PgSchemaNode> { _sequences = sequences; _loading = false; _loaded = true; + _error = null; }); } catch (e) { if (!mounted) return; - setState(() => _loading = false); + setState(() { + _error = e.toString(); + _loading = false; + }); } finally { lease?.release(); } @@ -706,6 +747,31 @@ class _PgSchemaNodeState extends State<_PgSchemaNode> { const Text('Loading...').muted().xSmall(), ], ), + ) + else if (_error != null) + material.Padding( + padding: const material.EdgeInsets.only( + left: 24, + top: 4, + bottom: 8, + ), + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.start, + children: [ + material.SelectableText( + _error!, + style: material.TextStyle( + fontSize: 11, + color: theme.colorScheme.destructive, + ), + ), + const material.SizedBox(height: 6), + GhostButton( + onPressed: _loadObjects, + child: const Text('Retry'), + ), + ], + ), ), if (_loaded) ...[ _PgObjectGroup( diff --git a/lib/features/mysql/mysql_table_view.dart b/lib/features/mysql/mysql_table_view.dart index d365c2fc..5a1f2571 100644 --- a/lib/features/mysql/mysql_table_view.dart +++ b/lib/features/mysql/mysql_table_view.dart @@ -405,7 +405,33 @@ class _MysqlTableViewState extends material.State { } if (_columnNames.isEmpty) { - return material.Container(color: cs.background); + return material.Container( + color: cs.background, + child: material.Center( + child: material.Padding( + padding: const material.EdgeInsets.all(32), + child: material.Column( + mainAxisSize: material.MainAxisSize.min, + children: [ + const Text('No columns returned').muted().small(), + const Gap(24), + OutlineButton( + onPressed: () { + if (_customSqlActive) { + unawaited(_fetchCustom()); + } else { + unawaited(_fetch(refreshCount: true)); + } + }, + leading: const material.Icon(material.Icons.refresh_rounded, + size: 18), + child: const Text('Retry'), + ), + ], + ), + ), + ), + ); } const double rowHeight = 36; @@ -444,6 +470,7 @@ class _MysqlTableViewState extends material.State { child: material.Text( title, overflow: material.TextOverflow.ellipsis, + maxLines: 1, style: material.TextStyle( fontSize: 13, fontWeight: material.FontWeight.w600, @@ -451,49 +478,75 @@ class _MysqlTableViewState extends material.State { ), ), ), - material.Text( - _paginationLabel(), - style: material.TextStyle( - fontSize: 11, - color: cs.mutedForeground, - ), - ), - const Gap(8), - OutlineButton( - onPressed: _loading - ? null - : () { - if (_customSqlActive) { - unawaited(_fetchCustom()); - } else { - unawaited(_fetch(refreshCount: true)); - } - }, - child: const Text('Refresh'), - ), - const Gap(6), - OutlineButton( - onPressed: _openSqlEditor, - child: const Text('SQL'), - ), - if (_customSqlActive) ...[ - const Gap(6), - OutlineButton( - onPressed: _exitCustomMode, - child: const Text('Browse'), + material.Expanded( + flex: 2, + child: material.LayoutBuilder( + builder: (context, constraints) { + return material.SingleChildScrollView( + scrollDirection: material.Axis.horizontal, + child: material.ConstrainedBox( + constraints: material.BoxConstraints( + minWidth: constraints.maxWidth, + ), + child: material.Row( + mainAxisAlignment: material.MainAxisAlignment.end, + mainAxisSize: material.MainAxisSize.min, + children: [ + material.Text( + _paginationLabel(), + style: material.TextStyle( + fontSize: 11, + color: cs.mutedForeground, + ), + ), + const Gap(8), + OutlineButton( + onPressed: _loading + ? null + : () { + if (_customSqlActive) { + unawaited(_fetchCustom()); + } else { + unawaited(_fetch(refreshCount: true)); + } + }, + child: const Text('Refresh'), + ), + const Gap(6), + OutlineButton( + onPressed: _openSqlEditor, + child: const Text('SQL'), + ), + if (_customSqlActive) ...[ + const Gap(6), + OutlineButton( + onPressed: _exitCustomMode, + child: const Text('Browse'), + ), + ], + const Gap(6), + GhostButton( + onPressed: (!_canGoPrevious || _loading) + ? null + : _goToPreviousPage, + child: const Icon( + material.Icons.chevron_left_rounded, + size: 20), + ), + GhostButton( + onPressed: (!_canGoNext || _loading) + ? null + : _goToNextPage, + child: const Icon( + material.Icons.chevron_right_rounded, + size: 20), + ), + ], + ), + ), + ); + }, ), - ], - const Gap(6), - GhostButton( - onPressed: - (!_canGoPrevious || _loading) ? null : _goToPreviousPage, - child: - const Icon(material.Icons.chevron_left_rounded, size: 20), - ), - GhostButton( - onPressed: (!_canGoNext || _loading) ? null : _goToNextPage, - child: const Icon(material.Icons.chevron_right_rounded, - size: 20), ), ], ), diff --git a/lib/features/postgresql/postgres_table_view.dart b/lib/features/postgresql/postgres_table_view.dart index 05e6ea26..1733cf14 100644 --- a/lib/features/postgresql/postgres_table_view.dart +++ b/lib/features/postgresql/postgres_table_view.dart @@ -447,7 +447,33 @@ class _PostgresTableViewState extends material.State { } if (_columnNames.isEmpty) { - return material.Container(color: cs.background); + return material.Container( + color: cs.background, + child: material.Center( + child: material.Padding( + padding: const material.EdgeInsets.all(32), + child: material.Column( + mainAxisSize: material.MainAxisSize.min, + children: [ + const Text('No columns returned').muted().small(), + const Gap(24), + OutlineButton( + onPressed: () { + if (_customSqlActive) { + _fetchCustom(); + } else { + _fetch(refreshCount: true); + } + }, + leading: const material.Icon(material.Icons.refresh_rounded, + size: 18), + child: const Text('Retry'), + ), + ], + ), + ), + ), + ); } const double rowHeight = 36; diff --git a/lib/features/sqlite/sqlite_table_view.dart b/lib/features/sqlite/sqlite_table_view.dart index fd3f4f7d..483d1ab6 100644 --- a/lib/features/sqlite/sqlite_table_view.dart +++ b/lib/features/sqlite/sqlite_table_view.dart @@ -392,7 +392,27 @@ class _SqliteTableViewState extends material.State { } if (_columnNames.isEmpty) { - return material.Container(color: cs.background); + return material.Container( + color: cs.background, + child: material.Center( + child: material.Padding( + padding: const material.EdgeInsets.all(32), + child: material.Column( + mainAxisSize: material.MainAxisSize.min, + children: [ + const Text('No columns returned').muted().small(), + const Gap(24), + OutlineButton( + onPressed: _connectAndLoad, + leading: const material.Icon(material.Icons.refresh_rounded, + size: 18), + child: const Text('Retry'), + ), + ], + ), + ), + ), + ); } const double rowHeight = 36; @@ -430,6 +450,7 @@ class _SqliteTableViewState extends material.State { child: material.Text( title, overflow: material.TextOverflow.ellipsis, + maxLines: 1, style: material.TextStyle( fontSize: 13, fontWeight: material.FontWeight.w600, @@ -437,37 +458,66 @@ class _SqliteTableViewState extends material.State { ), ), ), - material.Text( - _paginationLabel(), - style: material.TextStyle( - fontSize: 11, - color: cs.mutedForeground, + material.Expanded( + flex: 2, + child: material.LayoutBuilder( + builder: (context, constraints) { + return material.SingleChildScrollView( + scrollDirection: material.Axis.horizontal, + child: material.ConstrainedBox( + constraints: material.BoxConstraints( + minWidth: constraints.maxWidth, + ), + child: material.Row( + mainAxisAlignment: material.MainAxisAlignment.end, + mainAxisSize: material.MainAxisSize.min, + children: [ + material.Text( + _paginationLabel(), + style: material.TextStyle( + fontSize: 11, + color: cs.mutedForeground, + ), + ), + const Gap(8), + OutlineButton( + size: ButtonSize.small, + onPressed: _loading + ? null + : () => unawaited(_showDdlDialog()), + child: const Text('DDL'), + ), + const Gap(6), + OutlineButton( + onPressed: _loading + ? null + : () => unawaited(_fetch()), + child: const Text('Refresh'), + ), + const Gap(6), + GhostButton( + onPressed: (!_canGoPrevious || _loading) + ? null + : _goToPreviousPage, + child: const Icon( + material.Icons.chevron_left_rounded, + size: 20), + ), + GhostButton( + onPressed: (!_canGoNext || _loading) + ? null + : _goToNextPage, + child: const Icon( + material.Icons.chevron_right_rounded, + size: 20), + ), + ], + ), + ), + ); + }, ), ), - const Gap(8), - OutlineButton( - size: ButtonSize.small, - onPressed: - _loading ? null : () => unawaited(_showDdlDialog()), - child: const Text('DDL'), - ), - const Gap(6), - OutlineButton( - onPressed: _loading ? null : () => unawaited(_fetch()), - child: const Text('Refresh'), - ), - const Gap(6), - GhostButton( - onPressed: - (!_canGoPrevious || _loading) ? null : _goToPreviousPage, - child: - const Icon(material.Icons.chevron_left_rounded, size: 20), - ), - GhostButton( - onPressed: (!_canGoNext || _loading) ? null : _goToNextPage, - child: const Icon(material.Icons.chevron_right_rounded, - size: 20), - ), ], ), ),