From c7953b874d976654d1e1786a35fa07b94ba540a2 Mon Sep 17 00:00:00 2001 From: Uwe Trottmann <13865709+greenrobot-team@users.noreply.github.com> Date: Mon, 7 Feb 2022 11:51:37 +0100 Subject: [PATCH] Add Admin UI support. --- objectbox/CHANGELOG.md | 1 + objectbox/lib/objectbox.dart | 1 + objectbox/lib/src/admin.dart | 1 + objectbox/lib/src/native/admin.dart | 89 +++++++++++++++++++ .../lib/src/native/bindings/bindings.dart | 2 + objectbox/lib/src/web/admin.dart | 1 + objectbox/test/admin_test.dart | 42 +++++++++ 7 files changed, 137 insertions(+) create mode 100644 objectbox/lib/src/admin.dart create mode 100644 objectbox/lib/src/native/admin.dart create mode 100644 objectbox/lib/src/web/admin.dart create mode 100644 objectbox/test/admin_test.dart diff --git a/objectbox/CHANGELOG.md b/objectbox/CHANGELOG.md index 4b25243c7..410025e2f 100644 --- a/objectbox/CHANGELOG.md +++ b/objectbox/CHANGELOG.md @@ -1,6 +1,7 @@ ## latest * Add an option to change code-generator's `output_dir` in `pubspec.yaml`. #341 +* Support ObjectBox Admin for Android apps to browse the database, see our [docs](https://docs.objectbox.io/data-browser) to get started. #148 * Update: [objectbox-c 0.15.2](https://github.com/objectbox/objectbox-c/releases/tag/v0.15.0). * Update: [objectbox-android 3.1.2](https://github.com/objectbox/objectbox-java/releases/tag/V3.1.0). * Update: [objectbox-swift 1.7.0](https://github.com/objectbox/objectbox-swift/releases/tag/v1.7.0). diff --git a/objectbox/lib/objectbox.dart b/objectbox/lib/objectbox.dart index 336187dbb..0d06cac9c 100644 --- a/objectbox/lib/objectbox.dart +++ b/objectbox/lib/objectbox.dart @@ -6,6 +6,7 @@ library objectbox; export 'src/annotations.dart'; export 'src/box.dart' show Box, PutMode; +export 'src/admin.dart' show Admin; export 'src/common.dart'; export 'src/query.dart' show diff --git a/objectbox/lib/src/admin.dart b/objectbox/lib/src/admin.dart new file mode 100644 index 000000000..385037553 --- /dev/null +++ b/objectbox/lib/src/admin.dart @@ -0,0 +1 @@ +export 'native/admin.dart' if (dart.library.html) 'web/admin.dart'; diff --git a/objectbox/lib/src/native/admin.dart b/objectbox/lib/src/native/admin.dart new file mode 100644 index 000000000..bbdd7340b --- /dev/null +++ b/objectbox/lib/src/native/admin.dart @@ -0,0 +1,89 @@ +import 'dart:ffi'; + +import 'bindings/bindings.dart'; +import 'bindings/helpers.dart'; +import 'store.dart'; + +/// ObjectBox Admin allows you to explore the database in a regular web browser. +/// +/// ```dart +/// if (Admin.isAvailable()) { +/// // Keep a reference until no longer needed or manually closed. +/// admin = Admin(store); +/// } +/// ``` +/// +/// Admin runs directly on your device or on your development machine. +/// Behind the scenes this works by bundling a simple HTTP server into ObjectBox +/// when building your app. If triggered, it will then provide a basic web +/// interface to the data and schema. +/// +/// Note: ObjectBox Admin is currently supported for Android apps only. +/// [Additional configuration](https://docs.objectbox.io/data-browser) is +/// required. +class Admin { + late Pointer _cAdmin; + late final Pointer _cFinalizer; + + @pragma('vm:prefer-inline') + Pointer get _ptr => + isClosed() ? throw StateError('Admin already closed') : _cAdmin; + + /// Whether the loaded ObjectBox native library supports Admin. + static bool isAvailable() => C.has_feature(OBXFeature.Admin); + + /// Creates an ObjectBox Admin associated with the given store and options. + Admin(Store store, {String bindUri = 'http://127.0.0.1:8090'}) { + if (!isAvailable()) { + throw UnsupportedError( + 'Admin is not available in the loaded ObjectBox runtime library.'); + } + initializeDartAPI(); + + final opt = checkObxPtr(C.admin_opt()); + try { + checkObx(C.admin_opt_store(opt, InternalStoreAccess.ptr(store))); + checkObx(C.admin_opt_user_management(opt, false)); + withNativeString(bindUri, + (Pointer cStr) => checkObx(C.admin_opt_bind(opt, cStr))); + } catch (_) { + C.admin_opt_free(opt); + rethrow; + } + + _cAdmin = C.admin(opt); + + // Keep the finalizer so we can detach it when close() is called manually. + _cFinalizer = C.dartc_attach_finalizer( + this, native_admin_close, _cAdmin.cast(), 1024 * 1024); + if (_cFinalizer == nullptr) { + close(); + throwLatestNativeError(); + } + } + + /// Closes and cleans up all resources used by this Admin. + void close() { + if (!isClosed()) { + final errors = List.filled(2, 0); + if (_cFinalizer != nullptr) { + errors[0] = C.dartc_detach_finalizer(_cFinalizer, this); + } + errors[1] = C.admin_close(_cAdmin); + _cAdmin = nullptr; + errors.forEach(checkObx); + } + } + + /// Returns if the admin is already closed and can no longer be used. + bool isClosed() => _cAdmin.address == 0; + + /// Port the admin listens on. This is especially useful if the port was + /// assigned automatically (a "0" port was used in the [bindUri]). + late final int port = () { + final result = C.admin_port(_ptr); + reachabilityFence(this); + if (result == 0) throwLatestNativeError(); + return result; + }(); +} diff --git a/objectbox/lib/src/native/bindings/bindings.dart b/objectbox/lib/src/native/bindings/bindings.dart index 390d73060..30d2a4c69 100644 --- a/objectbox/lib/src/native/bindings/bindings.dart +++ b/objectbox/lib/src/native/bindings/bindings.dart @@ -122,6 +122,8 @@ late final native_query_close = _lib!.lookup>('obx_query_close'); late final native_query_prop_close = _lib!.lookup>('obx_query_prop_close'); +late final native_admin_close = + _lib!.lookup>('obx_admin_close'); /// Keeps `this` alive until this call, preventing finalizers to run. /// Necessary for objects with a finalizer attached because the optimizer may diff --git a/objectbox/lib/src/web/admin.dart b/objectbox/lib/src/web/admin.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/objectbox/lib/src/web/admin.dart @@ -0,0 +1 @@ + diff --git a/objectbox/test/admin_test.dart b/objectbox/test/admin_test.dart new file mode 100644 index 000000000..0e0af87ba --- /dev/null +++ b/objectbox/test/admin_test.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:objectbox/objectbox.dart'; +import 'package:test/test.dart'; + +import 'entity.dart'; +import 'test_env.dart'; + +void main() { + late TestEnv env; + + setUp(() { + env = TestEnv('admin'); + }); + + tearDown(() => env.closeAndDelete()); + + // Note: this test currently requires a C library with Sync server, + // so it is not run on public CI and must be run manually. + test('admin', () async { + env.box.put(TestEntity.filled()); + + final admin = Admin(env.store); + + // Check that it serves requests and has correct permissions configured. + final response = await HttpClient() + .get('127.0.0.1', admin.port, '/api/v2/auth-info') + .then((request) => request.close()); + expect(response.statusCode, 200); + expect(await response.transform(utf8.decoder).join(''), + '{"auth":false,"permissions":{"modelRead":true,"modelWrite":true,"objectsRead":true,"objectsWrite":true,"runtimeRead":true,"runtimeWrite":true}}'); + + expect(admin.isClosed(), isFalse); + admin.close(); + expect(admin.isClosed(), isTrue); + admin.close(); // does nothing + }, + skip: Admin.isAvailable() + ? null + : 'Admin is not available in the loaded library'); +}