provide option to download all collection data as json
fixed some typo
refactored desktop and web directories
fixed desktop URL parser
changed ValueKey to ObjectKey in QueryTable
MODIFY
packages/isar/lib/src/collection_schema.dart
packages/isar/lib/src/collection_schema.dart
@@ -124,7 +124,7 @@ class CollectionSchema<OBJ> {
if (propertyId != null) {
return propertyId;
} else {
- throw IsarError('Unknown propery "$propertyName"');
+ throw IsarError('Unknown property "$propertyName"');
}
}

MODIFY
packages/isar/lib/src/isar_connect.dart
packages/isar/lib/src/isar_connect.dart
@@ -12,6 +12,7 @@ abstract class _IsarConnect {
ConnectAction.watchInstance: _watchInstance,
ConnectAction.executeQuery: _executeQuery,
ConnectAction.removeQuery: _removeQuery,
+ ConnectAction.exportJson: _exportJson,
};
static bool _initialized = false;
@@ -161,6 +162,12 @@ abstract class _IsarConnect {
return true;
}
+ static Future<List<dynamic>> _exportJson(Map<String, dynamic> params) async {
+ final collection = Isar.getInstance(params['instance'] as String)!
+ .getCollectionByNameInternal(params['collection'] as String)!;
+ return collection.where().exportJson();
+ }
+
static Query<dynamic> _getQuery(Map<String, dynamic> params) {
final query = ConnectQuery.fromJson(params);
final collection = Isar.getInstance(query.instance)!

MODIFY
packages/isar/lib/src/isar_connect_api.dart
packages/isar/lib/src/isar_connect_api.dart
@@ -9,7 +9,8 @@ enum ConnectAction {
watchInstance('ext.isar.watchInstance'),
executeQuery('ext.isar.executeQuery'),
removeQuery('ext.isar.removeQuery'),
- exportQuery('ext.isar.exportQuery');
+ exportQuery('ext.isar.exportQuery'),
+ exportJson('ext.isar.exportJson');
const ConnectAction(this.method);

MODIFY
packages/isar_inspector/lib/collections.dart
packages/isar_inspector/lib/collections.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar_inspector/common.dart';
+import 'package:isar_inspector/desktop/download.dart'
+ if (dart.library.html) 'package:isar_inspector/web/download.dart';
import 'package:isar_inspector/state/collections_state.dart';
+import 'package:isar_inspector/state/instances_state.dart';
+import 'package:isar_inspector/state/isar_connect_state_notifier.dart';
class CollectionsList extends ConsumerWidget {
const CollectionsList({super.key});
@@ -12,11 +16,14 @@ class CollectionsList extends ConsumerWidget {
final collections = ref.watch(collectionsPod).valueOrNull ?? [];
final collectionInfo = ref.watch(collectionInfoPod);
final selectedCollection = ref.watch(selectedCollectionPod).valueOrNull;
+ final isarConnect = ref.watch(isarConnectPod.notifier);
+ final selectedInstance = ref.watch(selectedInstancePod).value!;
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
final collection = collections.elementAt(index);
final info = collectionInfo[collection.name];
+
return SizedBox(
height: 55,
child: IsarCard(
@@ -26,7 +33,7 @@ class CollectionsList extends ConsumerWidget {
ref.read(selectedCollectionNamePod.state).state = collection.name;
},
child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 25),
+ padding: const EdgeInsets.only(left: 25, right: 10),
child: Row(
children: [
Text(
@@ -37,7 +44,7 @@ class CollectionsList extends ConsumerWidget {
),
),
const Spacer(),
- if (info != null)
+ if (info != null) ...[
Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.center,
@@ -57,6 +64,29 @@ class CollectionsList extends ConsumerWidget {
),
],
),
+ const SizedBox(width: 5),
+ ],
+ GestureDetector(
+ onTap: () async {
+ await download(
+ await isarConnect.exportJson(
+ selectedInstance,
+ collection.name,
+ ),
+ '${selectedInstance}_${collection.name}.json',
+ );
+ },
+ child: Tooltip(
+ message: 'Download collection',
+ child: Icon(
+ Icons.download,
+ size: 30,
+ color: info != null && info.count == 0
+ ? Colors.grey
+ : null,
+ ),
+ ),
+ ),
],
),
),

RENAME
packages/isar_inspector/lib/app_io.dart
packages/isar_inspector/lib/desktop/app.dart
@@ -14,7 +14,7 @@ class App extends StatefulWidget {
class _AppState extends State<App> {
final _textController = TextEditingController();
- final urlRE = RegExp(r'^https:\/\/inspect\.isar\.dev\/#\/(\d+)\/(\w+)$');
+ final urlRE = RegExp(r'^https:\/\/inspect\.isar\.dev\/#\/(\d+)\/([\w-]+)$');
String? _port;
String? _secret;
String? _error;
@@ -77,10 +77,24 @@ class _AppState extends State<App> {
errorText: _error,
labelText: 'Enter URL',
),
+ onSubmitted: (_) => _connect(),
style: GoogleFonts.sourceCodePro(),
),
ElevatedButton(
- onPressed: () {
+ onPressed: _connect,
+ child: const Text('Connect'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ void _connect() {
final url = _textController.text.trim();
if (!urlRE.hasMatch(url)) {
setState(() {
@@ -96,16 +110,5 @@ class _AppState extends State<App> {
_secret = match.group(2);
_error = null;
});
- },
- child: const Text('Connect'),
- ),
- ],
- ),
- ),
- ),
- ),
- ),
- ),
- );
}
}

ADD
packages/isar_inspector/lib/desktop/download.dart
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:file_picker/file_picker.dart';
+import 'package:flutter/foundation.dart';
+
+Future<void> download(List<dynamic> data, String fileName) async {
+ if (data.isNotEmpty) {
+ // Calling file selectors without isolate blocks main isolate (windows)
+ await compute(_saveFile, {
+ 'fileName': fileName,
+ 'data': jsonEncode(data),
+ });
+ }
+}
+
+Future<void> _saveFile(Map<String, String> args) async {
+ final result = await FilePicker.platform.saveFile(
+ dialogTitle: 'Save file',
+ fileName: args['fileName'],
+ );
+
+ if (result != null) {
+ try {
+ final file = File(result);
+ await file.writeAsString(args['data']!);
+ } catch (_) {}
+ }
+}

MODIFY
packages/isar_inspector/lib/instance_selector.dart
packages/isar_inspector/lib/instance_selector.dart
@@ -67,7 +67,7 @@ class _InstanceSelectorState extends ConsumerState<InstanceSelector>
InstanceButton(
instance: instance,
onTap: () {
- selectInstane(ref, instance);
+ selectInstance(ref, instance);
_controller.reverse();
},
),

MODIFY
packages/isar_inspector/lib/main.dart
packages/isar_inspector/lib/main.dart
import 'package:flutter/material.dart';
-import 'package:isar_inspector/app_io.dart'
- if (dart.library.html) 'package:isar_inspector/app_web.dart';
+import 'package:isar_inspector/desktop/app.dart'
+ if (dart.library.html) 'package:isar_inspector/web/app.dart';
void main() async {
//ignore: prefer_const_constructors

MODIFY
packages/isar_inspector/lib/query_table.dart
packages/isar_inspector/lib/query_table.dart
@@ -41,7 +41,7 @@ class QueryTable extends ConsumerWidget {
return Stack(
children: [
TableBlock(
- key: Key(objects[index].hashCode.toString()),
+ key: ObjectKey(objects[index]),
collection: collection,
object: objects[index],
),
@@ -97,7 +97,7 @@ class QueryTable extends ConsumerWidget {
class TableBlock extends StatefulWidget {
const TableBlock({
- super.key,
+ required super.key,
required this.collection,
required this.object,
});

MODIFY
packages/isar_inspector/lib/state/instances_state.dart
packages/isar_inspector/lib/state/instances_state.dart
@@ -9,7 +9,7 @@ final instancesPod = FutureProvider((ref) {
final _selectedInstanceNamePod = StateProvider((ref) => 'isar');
-void selectInstane(WidgetRef ref, String instance) {
+void selectInstance(WidgetRef ref, String instance) {
ref.read(_selectedInstanceNamePod.state).state = instance;
}

MODIFY
packages/isar_inspector/lib/state/isar_connect_state_notifier.dart
packages/isar_inspector/lib/state/isar_connect_state_notifier.dart
@@ -43,6 +43,7 @@ class IsarConnectStateNotifier
}) : super(const AsyncValue.loading()) {
connect();
}
+
static const Duration kNormalTimeout = Duration(seconds: 4);
static const Duration kLongTimeout = Duration(seconds: 10);
@@ -163,6 +164,16 @@ class IsarConnectStateNotifier
);
}
+ Future<List<dynamic>> exportJson(String instance, String collection) async {
+ final data = await _call<List<dynamic>>(
+ ConnectAction.exportJson,
+ args: {'instance': instance, 'collection': collection},
+ timeout: kLongTimeout,
+ );
+
+ return data.cast();
+ }
+
Future<void> disconnect() async {
await state.value?.vmService.dispose();
}

RENAME
packages/isar_inspector/lib/app_web.dart
packages/isar_inspector/lib/web/app.dart
ADD
packages/isar_inspector/lib/web/download.dart
+import 'dart:convert';
+
+// ignore: avoid_web_libraries_in_flutter
+import 'dart:html';
+
+Future<void> download(List<dynamic> data, String fileName) async {
+ if (data.isEmpty) {
+ return;
+ }
+
+ try {
+ final base64 = base64Encode(utf8.encode(jsonEncode(data)));
+ final anchor =
+ AnchorElement(href: 'data:application/octet-stream;base64,$base64')
+ ..target = 'blank'
+ ..download = fileName;
+
+ document.body!.append(anchor);
+ anchor.click();
+ anchor.remove();
+ } catch (_) {}
+}

MODIFY
packages/isar_inspector/pubspec.yaml
packages/isar_inspector/pubspec.yaml
@@ -7,6 +7,7 @@ environment:
dependencies:
dartx: ^1.0.0
+ file_picker: ^4.6.1
flutter:
sdk: flutter
flutter_riverpod: ^2.0.0-dev.7
@@ -29,6 +30,8 @@ dev_dependencies:
very_good_analysis: ^3.0.1
dependency_overrides:
+ ffi: '>=2.0.0 <3.0.0'
+
isar:
path: ../isar

provide option to download all collection data as json

fixed some typo
refactored desktop and web directories
fixed desktop URL parser
changed ValueKey to ObjectKey in QueryTable