diff --git a/lib/core/database/postgres_connection.dart b/lib/core/database/postgres_connection.dart index b324ed04..ca32f916 100644 --- a/lib/core/database/postgres_connection.dart +++ b/lib/core/database/postgres_connection.dart @@ -743,16 +743,23 @@ class PostgresConnection with SshTunnelHost { stats['version'] = ver.first[0] as String; final settings = await _queued((c) => c.execute( - "SELECT name, setting FROM pg_settings " - "WHERE name IN ('max_connections','shared_buffers','work_mem'," + // lower(name): the setting is called "TimeZone", which a plain list of + // lower-case names never matched (the Timezone row stayed empty). + "SELECT lower(name), setting, unit FROM pg_settings " + "WHERE lower(name) IN ('max_connections','shared_buffers','work_mem'," "'effective_cache_size','server_version','data_directory'," "'listen_addresses','port','server_encoding','timezone')", )); final settingsMap = {}; + final settingUnits = {}; for (final row in settings) { - settingsMap[row[0] as String] = row[1] as String; + final name = row[0] as String; + settingsMap[name] = row[1] as String; + final unit = row[2]; + if (unit is String && unit.isNotEmpty) settingUnits[name] = unit; } stats['settings'] = settingsMap; + stats['setting_units'] = settingUnits; final activity = await _queued((c) => c.execute( "SELECT count(*) AS total, " diff --git a/lib/features/postgresql/pg_setting_format.dart b/lib/features/postgresql/pg_setting_format.dart new file mode 100644 index 00000000..9ea6b8e3 --- /dev/null +++ b/lib/features/postgresql/pg_setting_format.dart @@ -0,0 +1,29 @@ +/// A `pg_settings` value with its [unit] spelled out (#1822 run-through): the +/// raw `16384` of `shared_buffers` means 16384 blocks of 8 kB, so the card +/// shows `128 MB`. Values with an unknown or no unit come back as they are. +String formatPgSetting(String setting, String? unit) { + final n = int.tryParse(setting); + if (n == null || unit == null || unit.isEmpty) return setting; + final bytes = switch (unit) { + 'B' => n, + 'kB' => n * 1024, + '8kB' => n * 8 * 1024, + 'MB' => n * 1024 * 1024, + 'GB' => n * 1024 * 1024 * 1024, + _ => null, + }; + if (bytes == null) return '$setting $unit'; + const steps = [ + (1024 * 1024 * 1024 * 1024, 'TB'), + (1024 * 1024 * 1024, 'GB'), + (1024 * 1024, 'MB'), + (1024, 'kB'), + ]; + for (final (size, label) in steps) { + if (bytes >= size) { + final v = bytes / size; + return '${v == v.roundToDouble() ? v.round() : v.toStringAsFixed(1)} $label'; + } + } + return '$bytes B'; +} diff --git a/lib/features/postgresql/postgres_stats_view.dart b/lib/features/postgresql/postgres_stats_view.dart index af7b47c2..d2917160 100644 --- a/lib/features/postgresql/postgres_stats_view.dart +++ b/lib/features/postgresql/postgres_stats_view.dart @@ -1,3 +1,4 @@ +import 'package:querya_desktop/features/postgresql/pg_setting_format.dart'; import 'package:flutter/widgets.dart'; import 'dart:math' as math; @@ -510,6 +511,10 @@ class _PostgresStatsViewState extends material.State material.Widget _serverSettingsCard( material.BuildContext context, Map stats) { final settings = stats['settings'] as Map? ?? {}; + final units = stats['setting_units'] as Map? ?? {}; + String setting(String name) => settings[name] == null + ? '—' + : formatPgSetting(settings[name]!, units[name]); return _card( context, 'Server Settings', @@ -517,10 +522,9 @@ class _PostgresStatsViewState extends material.State mainAxisSize: material.MainAxisSize.min, crossAxisAlignment: material.CrossAxisAlignment.stretch, children: [ - _row(context, 'Shared buffers', settings['shared_buffers'] ?? '—'), - _row(context, 'Work mem', settings['work_mem'] ?? '—'), - _row(context, 'Effective cache', - settings['effective_cache_size'] ?? '—'), + _row(context, 'Shared buffers', setting('shared_buffers')), + _row(context, 'Work mem', setting('work_mem')), + _row(context, 'Effective cache', setting('effective_cache_size')), _row(context, 'Encoding', settings['server_encoding'] ?? '—'), _row(context, 'Timezone', settings['timezone'] ?? '—'), ], diff --git a/lib/features/postgresql/postgres_table_toolbar.dart b/lib/features/postgresql/postgres_table_toolbar.dart index 0bba3291..f22aa778 100644 --- a/lib/features/postgresql/postgres_table_toolbar.dart +++ b/lib/features/postgresql/postgres_table_toolbar.dart @@ -89,18 +89,16 @@ class PostgresTableToolbar extends material.StatelessWidget { ), material.Expanded( flex: 2, + // Wraps in a narrow pane: a sideways scroll hid Prev / Next / + // Refresh and the mouse wheel does not scroll sideways. 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: [ + builder: (context, constraints) => material.Wrap( + alignment: material.WrapAlignment.end, + runAlignment: material.WrapAlignment.end, + crossAxisAlignment: material.WrapCrossAlignment.center, + spacing: 6, + runSpacing: 6, + children: [ material.Container( padding: const material.EdgeInsets.symmetric( horizontal: 8, @@ -118,18 +116,24 @@ class PostgresTableToolbar extends material.StatelessWidget { ), ), ), - const Gap(6), - if (editAction != null) ...[ - editAction!, - const Gap(4), - ], + if (editAction != null) + // Never wider than the pane: a long "N pending + // changes" row shrinks instead of overflowing. + material.ConstrainedBox( + constraints: material.BoxConstraints( + maxWidth: constraints.maxWidth), + child: material.FittedBox( + fit: material.BoxFit.scaleDown, + alignment: material.Alignment.centerRight, + child: editAction, + ), + ), QueryaButton.secondary( label: 'SQL', icon: material.Icons.code_rounded, onPressed: onOpenSql, size: QueryaControlSize.sm, ), - const Gap(4), QueryaButton.secondary( label: 'Privileges', icon: material.Icons.admin_panel_settings_rounded, @@ -137,7 +141,6 @@ class PostgresTableToolbar extends material.StatelessWidget { size: QueryaControlSize.sm, ), if (isMaterializedView && !customSqlActive) ...[ - const Gap(4), QueryaButton.secondary( label: 'Refresh MV', icon: material.Icons.sync_rounded, @@ -146,7 +149,6 @@ class PostgresTableToolbar extends material.StatelessWidget { ), ], if (customSqlActive) ...[ - const Gap(4), QueryaButton.secondary( label: 'Table', icon: material.Icons.table_chart_rounded, @@ -154,32 +156,26 @@ class PostgresTableToolbar extends material.StatelessWidget { size: QueryaControlSize.sm, ), ], - const Gap(4), QueryaButton.secondary( label: 'Prev', icon: material.Icons.chevron_left_rounded, onPressed: canGoPrevious ? onGoPrevious : null, size: QueryaControlSize.sm, ), - const Gap(4), QueryaButton.secondary( label: 'Next', icon: material.Icons.chevron_right_rounded, onPressed: canGoNext ? onGoNext : null, size: QueryaControlSize.sm, ), - const Gap(8), QueryaButton.secondary( label: 'Refresh', icon: material.Icons.refresh_rounded, onPressed: loading ? null : onRefresh, size: QueryaControlSize.sm, ), - ], - ), - ), - ); - }, + ], + ), ), ), ], diff --git a/test/features/postgresql/pg_setting_format_test.dart b/test/features/postgresql/pg_setting_format_test.dart new file mode 100644 index 00000000..dac77a5b --- /dev/null +++ b/test/features/postgresql/pg_setting_format_test.dart @@ -0,0 +1,22 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/features/postgresql/pg_setting_format.dart'; + +void main() { + test('block and kilobyte settings are shown as sizes', () { + expect(formatPgSetting('16384', '8kB'), '128 MB'); + expect(formatPgSetting('524288', '8kB'), '4 GB'); + expect(formatPgSetting('4096', 'kB'), '4 MB'); + expect(formatPgSetting('64', 'kB'), '64 kB'); + expect(formatPgSetting('1536', 'kB'), '1.5 MB'); + }); + + test('no unit or a non-number stays as it is', () { + expect(formatPgSetting('100', null), '100'); + expect(formatPgSetting('100', ''), '100'); + expect(formatPgSetting('Etc/UTC', null), 'Etc/UTC'); + }); + + test('a unit that is not memory is kept next to the value', () { + expect(formatPgSetting('5000', 'ms'), '5000 ms'); + }); +} diff --git a/test/features/postgresql/postgres_table_toolbar_layout_test.dart b/test/features/postgresql/postgres_table_toolbar_layout_test.dart index e9550aa9..69d43ae3 100644 --- a/test/features/postgresql/postgres_table_toolbar_layout_test.dart +++ b/test/features/postgresql/postgres_table_toolbar_layout_test.dart @@ -129,5 +129,30 @@ void main() { ); }); }); + + testWidgets('every action stays on screen in a narrow pane (#1822 run)', + (tester) async { + await tester.binding.setSurfaceSize(const material.Size(700, 300)); + addTearDown(() => tester.binding.setSurfaceSize(null)); + await tester.pumpWidget( + MaterialApp( + theme: queryaDarkMaterialTheme, + darkTheme: queryaDarkMaterialTheme, + themeMode: material.ThemeMode.dark, + home: material.Scaffold( + body: material.Align( + alignment: material.Alignment.topLeft, + child: _toolbar(title: 'shop.orders'), + ), + ), + ), + ); + await tester.pump(); + + for (final label in ['SQL', 'Privileges', 'Prev', 'Next', 'Refresh']) { + final rect = tester.getRect(find.text(label)); + expect(rect.right, lessThanOrEqualTo(700), reason: label); + } + }); }); }