Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions lib/features/connections/connection_creation_flow.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'package:flutter/foundation.dart' show debugPrint, visibleForTesting;
import 'package:flutter/foundation.dart' show visibleForTesting;
import 'package:flutter/material.dart' as material;
import 'package:querya_desktop/core/extensions/extension_driver_catalog.dart';
import 'package:querya_desktop/core/extensions/models/extension_contributions.dart';
Expand All @@ -10,6 +10,8 @@ import 'package:querya_desktop/features/mongodb/mongodb_connection_form.dart';
import 'package:querya_desktop/features/mysql/mysql_connection_form.dart';
import 'package:querya_desktop/features/postgresql/postgresql_connection_form.dart';
import 'package:querya_desktop/features/redis/redis_connection_form.dart';
import 'package:querya_desktop/core/util/querya_log.dart';
import 'package:querya_desktop/shared/report_error.dart';
import 'package:querya_desktop/shared/widgets/app_toast.dart';

export 'package:querya_desktop/features/connections/connection_edit_secrets.dart';
Expand All @@ -26,14 +28,12 @@ Future<bool> saveNewConnection(
await LocalDb.instance.addConnection(row);
return true;
} catch (e, st) {
debugPrint('saveNewConnection: "${row.name}" failed: '
'${e.runtimeType}: $e\n$st');
const action = 'The connection was not saved';
if (context.mounted) {
showAppToast(
context: context,
message: 'The connection was not saved. $e',
variant: AppToastVariant.error,
);
reportError(context, e,
stack: st, where: 'saveNewConnection', action: action);
} else {
QueryaLog.instance.error('saveNewConnection', action, error: e, stack: st);
}
return false;
}
Expand Down
10 changes: 6 additions & 4 deletions lib/features/connections/connections_panel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import 'package:querya_desktop/features/connections/connection_creation_flow.dar
import 'package:querya_desktop/features/connections/connection_databases_folder.dart';
import 'package:querya_desktop/features/connections/driver_icon.dart';
import 'package:querya_desktop/features/connections/querya_connection_tree_row.dart';
import 'package:querya_desktop/shared/report_error.dart';
import 'package:querya_desktop/shared/widgets/widgets.dart';
import 'package:querya_desktop/core/database/redis_service.dart';
import 'package:querya_desktop/app/app_shutdown.dart';
Expand Down Expand Up @@ -721,10 +722,11 @@ class ConnectionsPanelState extends State<ConnectionsPanel> {
} on SecretsStoreUnavailableException catch (e) {
// Saving would write the secrets it could not read as empty ones.
if (mounted) {
showAppToast(
context: context,
variant: AppToastVariant.error,
message: 'The connection was not saved. ${e.toString()}',
reportError(
context,
e,
where: 'ConnectionsPanel._editConnection',
action: 'The connection was not saved',
);
}
return;
Expand Down
6 changes: 4 additions & 2 deletions lib/features/redis/redis_connection_form.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'package:querya_desktop/shared/report_error.dart';
import 'package:querya_desktop/core/database/database_error_mapper.dart';
import 'package:flutter/widgets.dart';
import 'dart:async';

Expand Down Expand Up @@ -220,8 +222,8 @@ class _RedisConnectionFormContentState
if (mounted) {
_showTestResult(result.ok ? 'success' : (result.error ?? 'failed'));
}
} catch (e) {
if (mounted) _showTestResult('error: $e');
} catch (e, st) {
if (mounted) _showTestResult(failureMessage('error', e, where: 'RedisConnectionForm', stack: st, driver: DatabaseDriver.redis));
}
}

Expand Down
58 changes: 30 additions & 28 deletions lib/features/redis/redis_key_editor.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'package:querya_desktop/shared/report_error.dart';
import 'package:querya_desktop/core/database/database_error_mapper.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart' as material;
import 'package:querya_desktop/core/theme/querya_ui_colors.dart';
Expand Down Expand Up @@ -315,11 +317,11 @@ class _RedisKeyEditorState extends material.State<RedisKeyEditor> {
}
if (!mounted) return;
setState(() => _loadingMore = false);
} catch (e) {
} catch (e, st) {
if (!mounted) return;
setState(() {
_loadingMore = false;
_error = 'Load failed: $e';
_error = failureMessage('Load failed', e, where: 'RedisKeyEditor', stack: st, driver: DatabaseDriver.redis);
});
}
}
Expand All @@ -336,9 +338,9 @@ class _RedisKeyEditorState extends material.State<RedisKeyEditor> {
if (!mounted) return;
setState(() => _success = 'Value saved');
_clearSuccessAfterDelay();
} catch (e) {
} catch (e, st) {
if (!mounted) return;
setState(() => _error = 'Save failed: $e');
setState(() => _error = failureMessage('Save failed', e, where: 'RedisKeyEditor', stack: st, driver: DatabaseDriver.redis));
}
}

Expand Down Expand Up @@ -377,9 +379,9 @@ class _RedisKeyEditorState extends material.State<RedisKeyEditor> {
});
_clearSuccessAfterDelay();
widget.onKeyRenamed?.call(newBulk);
} catch (e) {
} catch (e, st) {
if (mounted) {
setState(() => _error = 'Rename failed: $e');
setState(() => _error = failureMessage('Rename failed', e, where: 'RedisKeyEditor', stack: st, driver: DatabaseDriver.redis));
}
}
}
Expand All @@ -397,9 +399,9 @@ class _RedisKeyEditorState extends material.State<RedisKeyEditor> {
try {
await widget.connection.del(_cmdKey);
widget.onKeyDeleted?.call();
} catch (e) {
} catch (e, st) {
if (!mounted) return;
setState(() => _error = 'Delete failed: $e');
setState(() => _error = failureMessage('Delete failed', e, where: 'RedisKeyEditor', stack: st, driver: DatabaseDriver.redis));
}
}

Expand All @@ -417,9 +419,9 @@ class _RedisKeyEditorState extends material.State<RedisKeyEditor> {
_success = seconds > 0 ? 'TTL set to $seconds seconds' : 'TTL removed';
});
_clearSuccessAfterDelay();
} catch (e) {
} catch (e, st) {
if (!mounted) return;
setState(() => _error = 'TTL failed: $e');
setState(() => _error = failureMessage('TTL failed', e, where: 'RedisKeyEditor', stack: st, driver: DatabaseDriver.redis));
}
}

Expand All @@ -429,9 +431,9 @@ class _RedisKeyEditorState extends material.State<RedisKeyEditor> {
try {
await widget.connection.hset(_cmdKey, field, value);
await _load();
} catch (e) {
} catch (e, st) {
if (!mounted) return;
setState(() => _error = 'HSET failed: $e');
setState(() => _error = failureMessage('HSET failed', e, where: 'RedisKeyEditor', stack: st, driver: DatabaseDriver.redis));
}
}

Expand All @@ -448,9 +450,9 @@ class _RedisKeyEditorState extends material.State<RedisKeyEditor> {
try {
await widget.connection.hdel(_cmdKey, field);
await _load();
} catch (e) {
} catch (e, st) {
if (!mounted) return;
setState(() => _error = 'HDEL failed: $e');
setState(() => _error = failureMessage('HDEL failed', e, where: 'RedisKeyEditor', stack: st, driver: DatabaseDriver.redis));
}
}

Expand All @@ -460,9 +462,9 @@ class _RedisKeyEditorState extends material.State<RedisKeyEditor> {
try {
await widget.connection.rpush(_cmdKey, value);
await _load();
} catch (e) {
} catch (e, st) {
if (!mounted) return;
setState(() => _error = 'RPUSH failed: $e');
setState(() => _error = failureMessage('RPUSH failed', e, where: 'RedisKeyEditor', stack: st, driver: DatabaseDriver.redis));
}
}

Expand All @@ -485,9 +487,9 @@ class _RedisKeyEditorState extends material.State<RedisKeyEditor> {
}
await widget.connection.lset(_cmdKey, index, newValue);
await _load();
} catch (e) {
} catch (e, st) {
if (!mounted) return;
setState(() => _error = 'LSET failed: $e');
setState(() => _error = failureMessage('LSET failed', e, where: 'RedisKeyEditor', stack: st, driver: DatabaseDriver.redis));
}
}

Expand Down Expand Up @@ -522,9 +524,9 @@ class _RedisKeyEditorState extends material.State<RedisKeyEditor> {
rethrow;
}
await _load();
} catch (e) {
} catch (e, st) {
if (!mounted) return;
setState(() => _error = 'LREM failed: $e');
setState(() => _error = failureMessage('LREM failed', e, where: 'RedisKeyEditor', stack: st, driver: DatabaseDriver.redis));
}
}

Expand All @@ -541,9 +543,9 @@ class _RedisKeyEditorState extends material.State<RedisKeyEditor> {
try {
await widget.connection.sadd(_cmdKey, member);
await _load();
} catch (e) {
} catch (e, st) {
if (!mounted) return;
setState(() => _error = 'SADD failed: $e');
setState(() => _error = failureMessage('SADD failed', e, where: 'RedisKeyEditor', stack: st, driver: DatabaseDriver.redis));
}
}

Expand All @@ -560,9 +562,9 @@ class _RedisKeyEditorState extends material.State<RedisKeyEditor> {
try {
await widget.connection.srem(_cmdKey, member);
await _load();
} catch (e) {
} catch (e, st) {
if (!mounted) return;
setState(() => _error = 'SREM failed: $e');
setState(() => _error = failureMessage('SREM failed', e, where: 'RedisKeyEditor', stack: st, driver: DatabaseDriver.redis));
}
}

Expand All @@ -572,9 +574,9 @@ class _RedisKeyEditorState extends material.State<RedisKeyEditor> {
try {
await widget.connection.zadd(_cmdKey, score, member);
await _load();
} catch (e) {
} catch (e, st) {
if (!mounted) return;
setState(() => _error = 'ZADD failed: $e');
setState(() => _error = failureMessage('ZADD failed', e, where: 'RedisKeyEditor', stack: st, driver: DatabaseDriver.redis));
}
}

Expand All @@ -591,9 +593,9 @@ class _RedisKeyEditorState extends material.State<RedisKeyEditor> {
try {
await widget.connection.zrem(_cmdKey, member);
await _load();
} catch (e) {
} catch (e, st) {
if (!mounted) return;
setState(() => _error = 'ZREM failed: $e');
setState(() => _error = failureMessage('ZREM failed', e, where: 'RedisKeyEditor', stack: st, driver: DatabaseDriver.redis));
}
}

Expand Down
14 changes: 8 additions & 6 deletions lib/features/redis/redis_keys_view.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'package:querya_desktop/shared/report_error.dart';
import 'package:querya_desktop/core/database/database_error_mapper.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart' as material;
import 'package:querya_desktop/core/theme/querya_ui_colors.dart';
Expand Down Expand Up @@ -130,13 +132,13 @@ class _RedisKeysViewState extends material.State<RedisKeysView> {
ttl: metas[i].ttl,
),
];
} catch (e) {
} catch (e, st) {
// Still show keys with unknown type/TTL; surface the failure non-blocking.
infos = [
for (final name in accumulatedKeys)
RedisKeyInfo(name: name, type: 'unknown', ttl: -1),
];
typeTtlError = 'Failed to load key types/TTLs: $e';
typeTtlError = failureMessage('Failed to load key types/TTLs', e, where: 'RedisKeysView', stack: st, driver: DatabaseDriver.redis);
}
} else {
infos = [];
Expand Down Expand Up @@ -193,8 +195,8 @@ class _RedisKeysViewState extends material.State<RedisKeysView> {
_keys.removeWhere((k) => k.name == keyInfo.name);
_dbSize = (_dbSize - 1).clamp(0, _dbSize);
});
} catch (e) {
if (mounted) setState(() => _error = 'Delete failed: $e');
} catch (e, st) {
if (mounted) setState(() => _error = failureMessage('Delete failed', e, where: 'RedisKeysView', stack: st, driver: DatabaseDriver.redis));
}
}

Expand Down Expand Up @@ -222,11 +224,11 @@ class _RedisKeysViewState extends material.State<RedisKeysView> {
_keys.removeWhere((k) => toRemove.contains(k.name));
_dbSize = (_dbSize - count).clamp(0, _dbSize);
});
} catch (e) {
} catch (e, st) {
if (!mounted) return;
// Some chunks may have been removed: show what is really left.
await _load();
if (mounted) setState(() => _error = 'Delete folder failed: $e');
if (mounted) setState(() => _error = failureMessage('Delete folder failed', e, where: 'RedisKeysView', stack: st, driver: DatabaseDriver.redis));
}
}

Expand Down
52 changes: 52 additions & 0 deletions lib/shared/report_error.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import 'package:flutter/widgets.dart';
import 'package:querya_desktop/core/database/database_error_mapper.dart';
import 'package:querya_desktop/core/mcp/mcp_redaction.dart';
import 'package:querya_desktop/core/util/querya_log.dart';
import 'package:querya_desktop/shared/widgets/app_toast.dart';

/// The text a user reads for [error]: the mapped message with its hint for a
/// driver error, the error's own text otherwise, without credentials.
String describeError(Object error, {DatabaseDriver? driver}) =>
McpRedaction.redact(describeDatabaseError(error, driver: driver));

/// The one way to tell the user that an action failed: writes the failure to
/// the log (with its stack) and shows an error toast, `<action>. <reason>`.
///
/// Use it for a failed user action. A lazy load that fails inside a tree uses
/// `TreeLoadError`, and a query uses `QueryErrorView`; they should still log
/// through [QueryaLog].
///
/// [action] is a short sentence without the final full stop (`'The connection
/// was not saved'`). [where] names the place for the log (`'saveNewConnection'`).
/// Safe to call when [context] has been unmounted: the log is still written and
/// the toast is skipped.
void reportError(
BuildContext context,
Object error, {
required String where,
required String action,
StackTrace? stack,
DatabaseDriver? driver,
}) {
QueryaLog.instance.error(where, action, error: error, stack: stack);
if (!context.mounted) return;
showAppToast(
context: context,
variant: AppToastVariant.error,
message: '$action. ${describeError(error, driver: driver)}',
);
}

/// Logs [error] and returns `<action>: <reason>` for an inline error line (a
/// view's own error text), where a toast is not the surface. [action] is a
/// short phrase (`'Delete failed'`); [where] names the place for the log.
String failureMessage(
String action,
Object error, {
required String where,
StackTrace? stack,
DatabaseDriver? driver,
}) {
QueryaLog.instance.error(where, action, error: error, stack: stack);
return '$action: ${describeError(error, driver: driver)}';
}
Loading
Loading