Skip to content

Commit 0cedbab

Browse files
Merge pull request #1263 from QueryaHub/issue/1150-toolbar-rest
feat(sql-workspace): elapsed time in the results toolbar, icon-only toolbar on a narrow window (#1150)
2 parents 4eac948 + 1053917 commit 0cedbab

6 files changed

Lines changed: 102 additions & 11 deletions

File tree

‎lib/features/workspace/generic_sql_workspace.dart‎

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,7 @@ class GenericSqlWorkspaceState extends material.State<GenericSqlWorkspace> {
661661
session.rows = [];
662662
session.affectedRows = null;
663663
session.statusLine = null;
664+
session.elapsed = null;
664665
session.planRoot = null;
665666
session.resultGridPrimaryKeys = const [];
666667
session.resultGridColumnDataTypes = null;
@@ -756,6 +757,7 @@ class GenericSqlWorkspaceState extends material.State<GenericSqlWorkspace> {
756757
session.stagingBuffer = null;
757758
session.statusLine = scriptStatus ??
758759
_resultStatus(result, cols, outRows, null);
760+
session.elapsed = multi ? sw.elapsed : result.elapsed ?? sw.elapsed;
759761
session.running = false;
760762
});
761763
// Anything that is not a read may change table shapes: forget the cache.
@@ -903,6 +905,7 @@ class GenericSqlWorkspaceState extends material.State<GenericSqlWorkspace> {
903905
session.rows = [];
904906
session.affectedRows = null;
905907
session.statusLine = null;
908+
session.elapsed = null;
906909
session.planRoot = null;
907910
session.resultGridPrimaryKeys = const [];
908911
session.resultGridColumnDataTypes = null;
@@ -1322,6 +1325,7 @@ class GenericSqlWorkspaceState extends material.State<GenericSqlWorkspace> {
13221325
isLoading: session.running,
13231326
affectedRows: session.affectedRows,
13241327
statusLine: session.statusLine,
1328+
elapsed: session.elapsed,
13251329
stagingBuffer: session.stagingBuffer,
13261330
columnDataTypes: session.resultGridColumnDataTypes,
13271331
onApplyChanges: widget.isReadOnly ||
@@ -1349,7 +1353,12 @@ class GenericSqlWorkspaceState extends material.State<GenericSqlWorkspace> {
13491353
) {
13501354
final accent = context.workbench.accent;
13511355
final historyEnabled = widget.connectionRow.id != null && !session.running;
1352-
return material.SingleChildScrollView(
1356+
return material.LayoutBuilder(
1357+
builder: (context, constraints) {
1358+
// Below the breakpoint the labels hide and the icons stay (the
1359+
// tooltips name them), so the toolbar stays on one row.
1360+
final compact = constraints.maxWidth < _toolbarCompactWidth;
1361+
return material.SingleChildScrollView(
13531362
scrollDirection: material.Axis.horizontal,
13541363
child: material.Row(
13551364
children: [
@@ -1361,6 +1370,7 @@ class GenericSqlWorkspaceState extends material.State<GenericSqlWorkspace> {
13611370
key: const material.ValueKey('run_script'),
13621371
label: 'Execute (F5)',
13631372
icon: material.Icons.play_arrow_rounded,
1373+
compact: compact,
13641374
loading: session.running,
13651375
onPressed: () => execute(session),
13661376
),
@@ -1395,6 +1405,7 @@ class GenericSqlWorkspaceState extends material.State<GenericSqlWorkspace> {
13951405
key: const material.ValueKey('explain_query'),
13961406
label: 'Explain',
13971407
icon: material.Icons.account_tree_outlined,
1408+
compact: compact,
13981409
tooltip: 'Show the query plan',
13991410
onPressed: session.running ? null : () => explain(session),
14001411
),
@@ -1405,6 +1416,7 @@ class GenericSqlWorkspaceState extends material.State<GenericSqlWorkspace> {
14051416
key: const material.ValueKey('cancel_query'),
14061417
label: 'Cancel',
14071418
icon: material.Icons.stop_rounded,
1419+
compact: compact,
14081420
isDestructive: true,
14091421
tooltip: 'Interrupt the running query',
14101422
onPressed: () => cancelRunning(session),
@@ -1480,10 +1492,12 @@ class GenericSqlWorkspaceState extends material.State<GenericSqlWorkspace> {
14801492
material.Icon(material.Icons.tune_rounded,
14811493
size: 16, color: accent),
14821494
const material.SizedBox(width: 6),
1483-
Text(_autocommit
1484-
? 'Session · auto-commit'
1485-
: 'Session · manual'),
1486-
const material.SizedBox(width: 4),
1495+
if (!compact) ...[
1496+
Text(_autocommit
1497+
? 'Session · auto-commit'
1498+
: 'Session · manual'),
1499+
const material.SizedBox(width: 4),
1500+
],
14871501
material.Icon(material.Icons.expand_more_rounded,
14881502
size: 16, color: accent),
14891503
],
@@ -1499,8 +1513,13 @@ class GenericSqlWorkspaceState extends material.State<GenericSqlWorkspace> {
14991513
],
15001514
),
15011515
);
1516+
},
1517+
);
15021518
}
15031519

1520+
/// Toolbar width below which button labels hide and only icons stay.
1521+
static const double _toolbarCompactWidth = 900;
1522+
15041523
/// Transaction state as a badge: an open transaction is a warning, because
15051524
/// work can be lost, and looks different from no transaction.
15061525
material.Widget _txBadge() {

‎lib/features/workspace/results_tab.dart‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class ResultsTab extends material.StatefulWidget {
5151
this.isLoading = false,
5252
this.affectedRows,
5353
this.statusLine,
54+
this.elapsed,
5455
this.showExportToolbar = true,
5556
this.stagingBuffer,
5657
this.onApplyChanges,
@@ -65,6 +66,9 @@ class ResultsTab extends material.StatefulWidget {
6566
final bool isLoading;
6667
final int? affectedRows;
6768
final String? statusLine;
69+
70+
/// Time the query took, shown as a badge next to the row count.
71+
final Duration? elapsed;
6872
final bool showExportToolbar;
6973
final DataGridStagingBuffer? stagingBuffer;
7074
final material.VoidCallback? onApplyChanges;
@@ -417,6 +421,14 @@ class _ResultsTabState extends material.State<ResultsTab> {
417421
? 'Rows affected: ${widget.affectedRows}'
418422
: '${filteredRows.length}${_filterText.isNotEmpty ? ' of ${effectiveRows.length}' : ''} rows'),
419423
).small().semiBold(),
424+
if (widget.elapsed != null) ...[
425+
const Gap(8),
426+
QueryaBadge(
427+
key: const material.ValueKey('result_elapsed'),
428+
label: formatResultElapsed(widget.elapsed!),
429+
isMonospace: true,
430+
),
431+
],
420432

421433
const Gap(16),
422434

@@ -664,3 +676,12 @@ Future<void> _showSaveFileErrorDialog(material.BuildContext context) {
664676
),
665677
);
666678
}
679+
680+
/// `14 ms`, `1.2 s`, `2 min 5 s`: what the elapsed badge shows.
681+
String formatResultElapsed(Duration d) {
682+
if (d.inMilliseconds < 1000) return '${d.inMilliseconds} ms';
683+
if (d.inSeconds < 60) {
684+
return '${(d.inMilliseconds / 1000).toStringAsFixed(1)} s';
685+
}
686+
return '${d.inMinutes} min ${d.inSeconds % 60} s';
687+
}

‎lib/features/workspace/sql_query_tab_session.dart‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ class SqlQueryTabSession {
4444
int? affectedRows;
4545
String? statusLine;
4646

47+
/// How long the last run took (the whole script for a multi-statement run).
48+
Duration? elapsed;
49+
4750
/// The EXPLAIN plan as a tree, and whether the result area shows it (or text).
4851
PlanNode? planRoot;
4952
bool planAsTree = true;

‎lib/shared/widgets/querya_action_button.dart‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class QueryaActionButton extends material.StatelessWidget {
2020
this.isDestructive = false,
2121
this.tooltip,
2222
this.size = ButtonSize.normal,
23+
this.compact = false,
2324
});
2425

2526
final String label;
@@ -38,6 +39,10 @@ class QueryaActionButton extends material.StatelessWidget {
3839
final String? tooltip;
3940
final ButtonSize size;
4041

42+
/// Hides the label and keeps the icon (narrow toolbars). The label becomes
43+
/// the tooltip when none is set. Without an [icon], [compact] has no effect.
44+
final bool compact;
45+
4146
@override
4247
material.Widget build(material.BuildContext context) {
4348
final workbench = context.workbench;
@@ -51,18 +56,24 @@ class QueryaActionButton extends material.StatelessWidget {
5156
leading = material.Icon(icon, size: 18, color: tone);
5257
}
5358

59+
final iconOnly = compact && leading != null;
5460
material.Widget button = OutlineButton(
5561
size: size,
5662
onPressed: enabled ? onPressed : null,
5763
leading: leading,
58-
child: isDestructive
59-
? Text(label, style: material.TextStyle(color: tone))
60-
: Text(label),
64+
child: iconOnly
65+
? const material.SizedBox.shrink()
66+
: isDestructive
67+
? Text(label, style: material.TextStyle(color: tone))
68+
: Text(label),
6169
);
6270

63-
if (tooltip != null && tooltip!.isNotEmpty) {
71+
final hint = (tooltip != null && tooltip!.isNotEmpty)
72+
? tooltip!
73+
: (iconOnly ? label : null);
74+
if (hint != null) {
6475
button = material.Tooltip(
65-
message: tooltip!,
76+
message: hint,
6677
waitDuration: kQueryaTooltipWait,
6778
child: button,
6879
);

‎test/features/main_screen/workspace_homes_and_preferences_test.dart‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ void main() {
8484
await tester.pump();
8585
await tester.pump(const Duration(milliseconds: 200));
8686

87-
expect(find.textContaining('Execute'), findsWidgets);
87+
// The label hides on a narrow window; the button keeps its key.
88+
expect(find.byKey(const material.ValueKey('run_script')), findsOneWidget);
8889
});
8990
});
9091

‎test/features/workspace/generic_sql_workspace_shortcuts_test.dart‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'package:querya_desktop/core/database/table_mutation_engine.dart';
1313
import 'package:querya_desktop/core/storage/local_db.dart';
1414
import 'package:querya_desktop/features/settings/preferences_shortcuts_section.dart';
1515
import 'package:querya_desktop/features/workspace/generic_sql_workspace.dart';
16+
import 'package:querya_desktop/features/workspace/results_tab.dart' show formatResultElapsed;
1617
import 'package:querya_desktop/features/workspace/sql_execution_delegate.dart';
1718
import 'package:shadcn_flutter/shadcn_flutter.dart' show OutlineButton;
1819

@@ -366,6 +367,41 @@ void main() {
366367
expect(tester.getCenter(session).dy, closeTo(tester.getCenter(run).dy, 1));
367368
});
368369

370+
testWidgets('below the breakpoint the labels hide and the icons stay',
371+
timeout: _timeout, (tester) async {
372+
await pumpWorkspace(tester, FakeSqlExecutionDelegate());
373+
expect(find.text('Execute (F5)'), findsOneWidget);
374+
expect(find.text('Explain'), findsOneWidget);
375+
376+
await tester.binding.setSurfaceSize(const material.Size(800, 700));
377+
await tester.pumpAndSettle();
378+
379+
expect(find.text('Execute (F5)'), findsNothing);
380+
expect(find.text('Explain'), findsNothing);
381+
expect(find.byKey(const material.ValueKey('run_script')), findsOneWidget);
382+
expect(find.byKey(const material.ValueKey('explain_query')), findsOneWidget);
383+
expect(find.byTooltip('Show the query plan'), findsOneWidget);
384+
expect(find.byTooltip('Execute (F5)'), findsOneWidget);
385+
});
386+
387+
testWidgets('the results toolbar shows how long the query took',
388+
timeout: _timeout, (tester) async {
389+
final delegate = FakeSqlExecutionDelegate();
390+
await pumpWorkspace(tester, delegate, initialSql: 'SELECT n FROM t');
391+
expect(find.byKey(const material.ValueKey('result_elapsed')), findsNothing);
392+
393+
await tester.tap(find.widgetWithText(OutlineButton, 'Execute (F5)'));
394+
await settle(tester);
395+
396+
expect(find.byKey(const material.ValueKey('result_elapsed')), findsOneWidget);
397+
});
398+
399+
test('the elapsed badge reads ms, s and min', () {
400+
expect(formatResultElapsed(const Duration(milliseconds: 14)), '14 ms');
401+
expect(formatResultElapsed(const Duration(milliseconds: 1234)), '1.2 s');
402+
expect(formatResultElapsed(const Duration(seconds: 125)), '2 min 5 s');
403+
});
404+
369405
testWidgets('no Query caption and no Data Output bar above the results',
370406
timeout: _timeout, (tester) async {
371407
await pumpWorkspace(tester, FakeSqlExecutionDelegate());

0 commit comments

Comments
 (0)