From 661f5d604d448930c723259d12ba1b70ee716e88 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Mon, 8 Jan 2024 10:50:38 -0800 Subject: [PATCH] Use `package:http_image_provider` in all `Client` implementation examples (#1089) --- .github/workflows/cronet.yml | 2 +- pkgs/cronet_http/CHANGELOG.md | 4 ++ pkgs/cronet_http/README.md | 2 +- pkgs/cronet_http/example/lib/book.dart | 4 +- pkgs/cronet_http/example/lib/main.dart | 35 ++++++++---- pkgs/cronet_http/example/pubspec.yaml | 3 +- pkgs/cronet_http/pubspec.yaml | 2 +- pkgs/cupertino_http/CHANGELOG.md | 4 ++ pkgs/cupertino_http/README.md | 57 ++++++------------- pkgs/cupertino_http/example/lib/book.dart | 4 +- pkgs/cupertino_http/example/lib/main.dart | 31 ++++++---- pkgs/cupertino_http/example/pubspec.yaml | 3 +- pkgs/cupertino_http/pubspec.yaml | 2 +- pkgs/flutter_http_example/README.md | 8 +-- pkgs/flutter_http_example/lib/book.dart | 4 +- .../lib/http_client_factory.dart | 16 +++++- pkgs/flutter_http_example/lib/main.dart | 31 +++------- pkgs/flutter_http_example/pubspec.yaml | 6 +- .../test/widget_test.dart | 20 +++++-- pkgs/http/README.md | 3 +- 20 files changed, 126 insertions(+), 115 deletions(-) diff --git a/.github/workflows/cronet.yml b/.github/workflows/cronet.yml index 0976a756ff..8e114a0055 100644 --- a/.github/workflows/cronet.yml +++ b/.github/workflows/cronet.yml @@ -39,7 +39,7 @@ jobs: - name: Make cronet_http_embedded copy if: ${{ matrix.package == 'cronet_http_embedded' }} run: | - cp -r pkgs/cronet_http pkgs/cronet_http_embedded + mv pkgs/cronet_http pkgs/cronet_http_embedded cd pkgs/cronet_http_embedded flutter pub get && dart tool/prepare_for_embedded.dart - id: install diff --git a/pkgs/cronet_http/CHANGELOG.md b/pkgs/cronet_http/CHANGELOG.md index cf1fcbc208..6cc73ab31d 100644 --- a/pkgs/cronet_http/CHANGELOG.md +++ b/pkgs/cronet_http/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.1-wip + +* Use `package:http_image_provider` in the example application. + ## 1.0.0 * No functional changes. diff --git a/pkgs/cronet_http/README.md b/pkgs/cronet_http/README.md index 49acf3390d..47ebd8bac1 100644 --- a/pkgs/cronet_http/README.md +++ b/pkgs/cronet_http/README.md @@ -37,7 +37,7 @@ import 'package:http/http.dart'; import 'package:http/io_client.dart'; void main() async { - late Client httpClient; + final Client httpClient; if (Platform.isAndroid) { final engine = CronetEngine.build( cacheMode: CacheMode.memory, diff --git a/pkgs/cronet_http/example/lib/book.dart b/pkgs/cronet_http/example/lib/book.dart index 61a663b4d3..584ae9a361 100644 --- a/pkgs/cronet_http/example/lib/book.dart +++ b/pkgs/cronet_http/example/lib/book.dart @@ -5,7 +5,7 @@ class Book { String title; String description; - String imageUrl; + Uri imageUrl; Book(this.title, this.description, this.imageUrl); @@ -21,7 +21,7 @@ class Book { 'description': final String description, 'imageLinks': {'smallThumbnail': final String thumbnail} }) { - books.add(Book(title, description, thumbnail)); + books.add(Book(title, description, Uri.parse(thumbnail))); } } } diff --git a/pkgs/cronet_http/example/lib/main.dart b/pkgs/cronet_http/example/lib/main.dart index 61f11a7b01..6e1bad22c4 100644 --- a/pkgs/cronet_http/example/lib/main.dart +++ b/pkgs/cronet_http/example/lib/main.dart @@ -5,21 +5,31 @@ import 'dart:convert'; import 'dart:io'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:cronet_http/cronet_http.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart'; +import 'package:http/io_client.dart'; +import 'package:http_image_provider/http_image_provider.dart'; +import 'package:provider/provider.dart'; import 'book.dart'; void main() { - var clientFactory = Client.new; // Constructs the default client. + final Client httpClient; if (Platform.isAndroid) { final engine = CronetEngine.build( - cacheMode: CacheMode.memory, userAgent: 'Book Agent'); - clientFactory = () => CronetClient.fromCronetEngine(engine); + cacheMode: CacheMode.memory, + cacheMaxSize: 2 * 1024 * 1024, + userAgent: 'Book Agent'); + httpClient = CronetClient.fromCronetEngine(engine); + } else { + httpClient = IOClient(HttpClient()..userAgent = 'Book Agent'); } - runWithClient(() => runApp(const BookSearchApp()), clientFactory); + + runApp(Provider( + create: (_) => httpClient, + child: const BookSearchApp(), + dispose: (_, client) => client.close())); } class BookSearchApp extends StatelessWidget { @@ -44,20 +54,22 @@ class HomePage extends StatefulWidget { class _HomePageState extends State { List? _books; String? _lastQuery; + late Client _client; @override void initState() { super.initState(); + _client = context.read(); } // Get the list of books matching `query`. // The `get` call will automatically use the `client` configurated in `main`. Future> _findMatchingBooks(String query) async { - final response = await get( + final response = await _client.get( Uri.https( 'www.googleapis.com', '/books/v1/volumes', - {'q': query, 'maxResults': '40', 'printType': 'books'}, + {'q': query, 'maxResults': '20', 'printType': 'books'}, ), ); @@ -129,11 +141,10 @@ class _BookListState extends State { itemBuilder: (context, index) => Card( key: ValueKey(widget.books[index].title), child: ListTile( - leading: CachedNetworkImage( - placeholder: (context, url) => - const CircularProgressIndicator(), - imageUrl: - widget.books[index].imageUrl.replaceFirst('http', 'https')), + leading: Image( + image: HttpImage( + widget.books[index].imageUrl.replace(scheme: 'https'), + client: context.read())), title: Text(widget.books[index].title), subtitle: Text(widget.books[index].description), ), diff --git a/pkgs/cronet_http/example/pubspec.yaml b/pkgs/cronet_http/example/pubspec.yaml index 5bb80276a6..904b1e5ade 100644 --- a/pkgs/cronet_http/example/pubspec.yaml +++ b/pkgs/cronet_http/example/pubspec.yaml @@ -7,13 +7,14 @@ environment: sdk: ^3.0.0 dependencies: - cached_network_image: ^3.2.3 cronet_http: path: ../ cupertino_icons: ^1.0.2 flutter: sdk: flutter http: ^1.0.0 + http_image_provider: ^0.0.2 + provider: ^6.1.1 dev_dependencies: dart_flutter_team_lints: ^2.0.0 diff --git a/pkgs/cronet_http/pubspec.yaml b/pkgs/cronet_http/pubspec.yaml index 5f4229ebf8..c9e0e4d0a0 100644 --- a/pkgs/cronet_http/pubspec.yaml +++ b/pkgs/cronet_http/pubspec.yaml @@ -1,5 +1,5 @@ name: cronet_http -version: 1.0.0 +version: 1.0.1-wip description: >- An Android Flutter plugin that provides access to the Cronet HTTP client. repository: https://github.com/dart-lang/http/tree/master/pkgs/cronet_http diff --git a/pkgs/cupertino_http/CHANGELOG.md b/pkgs/cupertino_http/CHANGELOG.md index d768f7a5c7..395bb8b3b7 100644 --- a/pkgs/cupertino_http/CHANGELOG.md +++ b/pkgs/cupertino_http/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.2.1-wip + +* Use `package:http_image_provider` in the example application. + ## 1.2.0 * Add support for setting additional http headers in diff --git a/pkgs/cupertino_http/README.md b/pkgs/cupertino_http/README.md index 83514dcc40..77c1d79244 100644 --- a/pkgs/cupertino_http/README.md +++ b/pkgs/cupertino_http/README.md @@ -24,49 +24,25 @@ This approach allows the same HTTP code to be used on all platforms, while still allowing platform-specific setup. ```dart -late Client client; -if (Platform.isIOS) { - final config = URLSessionConfiguration.ephemeralSessionConfiguration() - ..allowsCellularAccess = false - ..allowsConstrainedNetworkAccess = false - ..allowsExpensiveNetworkAccess = false; - client = CupertinoClient.fromSessionConfiguration(config); -} else { - client = IOClient(); // Uses an HTTP client based on dart:io -} - -final response = await client.get(Uri.https( - 'www.googleapis.com', - '/books/v1/volumes', - {'q': 'HTTP', 'maxResults': '40', 'printType': 'books'})); -``` - -[package:http runWithClient][] can be used to configure the -[package:http Client][] for the entire application. - -```dart -void main() { - late Client client; - if (Platform.isIOS) { - client = CupertinoClient.defaultSessionConfiguration(); +import 'package:cupertino_http/cupertino_http.dart'; +import 'package:http/http.dart'; +import 'package:http/io_client.dart'; + +void main() async { + final Client httpClient; + if (Platform.isIOS || Platform.isMacOS) { + final config = URLSessionConfiguration.ephemeralSessionConfiguration() + ..cache = URLCache.withCapacity(memoryCapacity: 2 * 1024 * 1024) + ..httpAdditionalHeaders = {'User-Agent': 'Book Agent'}; + httpClient = CupertinoClient.fromSessionConfiguration(config); } else { - client = IOClient(); + httpClient = IOClient(HttpClient()..userAgent = 'Book Agent'); } - runWithClient(() => runApp(const MyApp()), () => client); -} - -... - -class MainPageState extends State { - void someMethod() { - // Will use the Client configured in main. - final response = await get(Uri.https( - 'www.googleapis.com', - '/books/v1/volumes', - {'q': 'HTTP', 'maxResults': '40', 'printType': 'books'})); - } - ... + final response = await client.get(Uri.https( + 'www.googleapis.com', + '/books/v1/volumes', + {'q': 'HTTP', 'maxResults': '40', 'printType': 'books'})); } ``` @@ -88,6 +64,5 @@ task.resume(); ``` [package:http Client]: https://pub.dev/documentation/http/latest/http/Client-class.html -[package:http runWithClient]: https://pub.dev/documentation/http/latest/http/runWithClient.html [Foundation URL Loading System]: https://developer.apple.com/documentation/foundation/url_loading_system [dart:io HttpClient]: https://api.dart.dev/stable/dart-io/HttpClient-class.html diff --git a/pkgs/cupertino_http/example/lib/book.dart b/pkgs/cupertino_http/example/lib/book.dart index f2a27fc460..b47ca9e67e 100644 --- a/pkgs/cupertino_http/example/lib/book.dart +++ b/pkgs/cupertino_http/example/lib/book.dart @@ -5,7 +5,7 @@ class Book { String title; String description; - String imageUrl; + Uri imageUrl; Book(this.title, this.description, this.imageUrl); @@ -21,7 +21,7 @@ class Book { 'description': final String description, 'imageLinks': {'smallThumbnail': final String thumbnail} }) { - books.add(Book(title, description, thumbnail)); + books.add(Book(title, description, Uri.parse(thumbnail))); } } } diff --git a/pkgs/cupertino_http/example/lib/main.dart b/pkgs/cupertino_http/example/lib/main.dart index 32c2b08980..092300a64c 100644 --- a/pkgs/cupertino_http/example/lib/main.dart +++ b/pkgs/cupertino_http/example/lib/main.dart @@ -5,22 +5,30 @@ import 'dart:convert'; import 'dart:io'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:cupertino_http/cupertino_http.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart'; +import 'package:http/io_client.dart'; +import 'package:http_image_provider/http_image_provider.dart'; +import 'package:provider/provider.dart'; import 'book.dart'; void main() { - var clientFactory = Client.new; // The default Client. + final Client httpClient; if (Platform.isIOS || Platform.isMacOS) { final config = URLSessionConfiguration.ephemeralSessionConfiguration() ..cache = URLCache.withCapacity(memoryCapacity: 2 * 1024 * 1024) ..httpAdditionalHeaders = {'User-Agent': 'Book Agent'}; - clientFactory = () => CupertinoClient.fromSessionConfiguration(config); + httpClient = CupertinoClient.fromSessionConfiguration(config); + } else { + httpClient = IOClient(HttpClient()..userAgent = 'Book Agent'); } - runWithClient(() => runApp(const BookSearchApp()), clientFactory); + + runApp(Provider( + create: (_) => httpClient, + child: const BookSearchApp(), + dispose: (_, client) => client.close())); } class BookSearchApp extends StatelessWidget { @@ -45,20 +53,22 @@ class HomePage extends StatefulWidget { class _HomePageState extends State { List? _books; String? _lastQuery; + late Client _client; @override void initState() { super.initState(); + _client = context.read(); } // Get the list of books matching `query`. // The `get` call will automatically use the `client` configurated in `main`. Future> _findMatchingBooks(String query) async { - final response = await get( + final response = await _client.get( Uri.https( 'www.googleapis.com', '/books/v1/volumes', - {'q': query, 'maxResults': '40', 'printType': 'books'}, + {'q': query, 'maxResults': '20', 'printType': 'books'}, ), ); @@ -130,11 +140,10 @@ class _BookListState extends State { itemBuilder: (context, index) => Card( key: ValueKey(widget.books[index].title), child: ListTile( - leading: CachedNetworkImage( - placeholder: (context, url) => - const CircularProgressIndicator(), - imageUrl: - widget.books[index].imageUrl.replaceFirst('http', 'https')), + leading: Image( + image: HttpImage( + widget.books[index].imageUrl.replace(scheme: 'https'), + client: context.read())), title: Text(widget.books[index].title), subtitle: Text(widget.books[index].description), ), diff --git a/pkgs/cupertino_http/example/pubspec.yaml b/pkgs/cupertino_http/example/pubspec.yaml index bae184b71c..08048579ae 100644 --- a/pkgs/cupertino_http/example/pubspec.yaml +++ b/pkgs/cupertino_http/example/pubspec.yaml @@ -10,13 +10,14 @@ environment: flutter: '>=3.10.0' dependencies: - cached_network_image: ^3.2.3 cupertino_http: path: ../ cupertino_icons: ^1.0.2 flutter: sdk: flutter http: ^1.0.0 + http_image_provider: ^0.0.2 + provider: ^6.1.1 dev_dependencies: convert: ^3.1.1 diff --git a/pkgs/cupertino_http/pubspec.yaml b/pkgs/cupertino_http/pubspec.yaml index 0a97bdd25d..e9a2bbfdb2 100644 --- a/pkgs/cupertino_http/pubspec.yaml +++ b/pkgs/cupertino_http/pubspec.yaml @@ -1,5 +1,5 @@ name: cupertino_http -version: 1.2.0 +version: 1.2.1-wip description: >- A macOS/iOS Flutter plugin that provides access to the Foundation URL Loading System. diff --git a/pkgs/flutter_http_example/README.md b/pkgs/flutter_http_example/README.md index 2c6cdfd025..1a9cf9dd8a 100644 --- a/pkgs/flutter_http_example/README.md +++ b/pkgs/flutter_http_example/README.md @@ -9,8 +9,7 @@ A Flutter sample app that illustrates how to configure and use including: * configuration for multiple platforms. - * using `runWithClient` and `package:provider` to pass `Client`s through - an application. + * using `package:provider` to pass `Client`s through an application. * writing tests using `MockClient`. ## The important bits @@ -34,9 +33,8 @@ This library demonstrates how to: * import `http_client_factory.dart` or `http_client_factory_web.dart`, depending on whether we are targeting the web browser or not. -* share a `package:http` `Client` by using `runWithClient` and - `package:provider`. -* call `package:http` functions. +* share a `package:http` `Client` by using `package:provider`. +* call `package:http` `Client` methods. ### `widget_test.dart` diff --git a/pkgs/flutter_http_example/lib/book.dart b/pkgs/flutter_http_example/lib/book.dart index f2a27fc460..b47ca9e67e 100644 --- a/pkgs/flutter_http_example/lib/book.dart +++ b/pkgs/flutter_http_example/lib/book.dart @@ -5,7 +5,7 @@ class Book { String title; String description; - String imageUrl; + Uri imageUrl; Book(this.title, this.description, this.imageUrl); @@ -21,7 +21,7 @@ class Book { 'description': final String description, 'imageLinks': {'smallThumbnail': final String thumbnail} }) { - books.add(Book(title, description, thumbnail)); + books.add(Book(title, description, Uri.parse(thumbnail))); } } } diff --git a/pkgs/flutter_http_example/lib/http_client_factory.dart b/pkgs/flutter_http_example/lib/http_client_factory.dart index cb36597c23..6e6ddc040b 100644 --- a/pkgs/flutter_http_example/lib/http_client_factory.dart +++ b/pkgs/flutter_http_example/lib/http_client_factory.dart @@ -7,13 +7,23 @@ import 'dart:io'; import 'package:cronet_http/cronet_http.dart'; import 'package:cupertino_http/cupertino_http.dart'; import 'package:http/http.dart'; +import 'package:http/io_client.dart'; + +const _maxCacheSize = 2 * 1024 * 1024; Client httpClient() { if (Platform.isAndroid) { - return CronetClient.defaultCronetEngine(); + final engine = CronetEngine.build( + cacheMode: CacheMode.memory, + cacheMaxSize: _maxCacheSize, + userAgent: 'Book Agent'); + return CronetClient.fromCronetEngine(engine); } if (Platform.isIOS || Platform.isMacOS) { - return CupertinoClient.defaultSessionConfiguration(); + final config = URLSessionConfiguration.ephemeralSessionConfiguration() + ..cache = URLCache.withCapacity(memoryCapacity: _maxCacheSize) + ..httpAdditionalHeaders = {'User-Agent': 'Book Agent'}; + return CupertinoClient.fromSessionConfiguration(config); } - return Client(); // Return the default client. + return IOClient(HttpClient()..userAgent = 'Book Agent'); } diff --git a/pkgs/flutter_http_example/lib/main.dart b/pkgs/flutter_http_example/lib/main.dart index 406b9e6b71..548c3dee0a 100644 --- a/pkgs/flutter_http_example/lib/main.dart +++ b/pkgs/flutter_http_example/lib/main.dart @@ -4,9 +4,9 @@ import 'dart:convert'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart'; +import 'package:http_image_provider/http_image_provider.dart'; import 'package:provider/provider.dart'; import 'book.dart'; @@ -14,22 +14,10 @@ import 'http_client_factory.dart' if (dart.library.js_interop) 'http_client_factory_web.dart' as http_factory; void main() { - // `runWithClient` is used to control which `package:http` `Client` is used - // when the `Client` constructor is called. This method allows you to choose - // the `Client` even when the package that you are using does not offer - // explicit parameterization. - // - // However, `runWithClient` does not work with Flutter tests. See - // https://github.com/flutter/flutter/issues/96939. - // - // Use `package:provider` and `runWithClient` together so that tests and - // unparameterized `Client` usages both work. - runWithClient( - () => runApp(Provider( - create: (_) => http_factory.httpClient(), - child: const BookSearchApp(), - dispose: (_, client) => client.close())), - http_factory.httpClient); + runApp(Provider( + create: (_) => http_factory.httpClient(), + child: const BookSearchApp(), + dispose: (_, client) => client.close())); } class BookSearchApp extends StatelessWidget { @@ -141,11 +129,10 @@ class _BookListState extends State { itemBuilder: (context, index) => Card( key: ValueKey(widget.books[index].title), child: ListTile( - leading: CachedNetworkImage( - placeholder: (context, url) => - const CircularProgressIndicator(), - imageUrl: - widget.books[index].imageUrl.replaceFirst('http', 'https')), + leading: Image( + image: HttpImage( + widget.books[index].imageUrl.replace(scheme: 'https'), + client: context.read())), title: Text(widget.books[index].title), subtitle: Text(widget.books[index].description), ), diff --git a/pkgs/flutter_http_example/pubspec.yaml b/pkgs/flutter_http_example/pubspec.yaml index 0331490003..7d0f0892ce 100644 --- a/pkgs/flutter_http_example/pubspec.yaml +++ b/pkgs/flutter_http_example/pubspec.yaml @@ -9,14 +9,14 @@ environment: flutter: '>=3.10.0' dependencies: - cached_network_image: ^3.2.3 - cronet_http: ^0.4.1 - cupertino_http: ^1.1.0 + cronet_http: ^1.0.0 + cupertino_http: ^1.2.0 cupertino_icons: ^1.0.2 fetch_client: ^1.0.2 flutter: sdk: flutter http: ^1.0.0 + http_image_provider: ^0.0.2 provider: ^6.0.5 dev_dependencies: diff --git a/pkgs/flutter_http_example/test/widget_test.dart b/pkgs/flutter_http_example/test/widget_test.dart index 9e0af25b97..f890a1bb11 100644 --- a/pkgs/flutter_http_example/test/widget_test.dart +++ b/pkgs/flutter_http_example/test/widget_test.dart @@ -2,6 +2,8 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:flutter_http_example/main.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -17,7 +19,7 @@ const _singleBookResponse = ''' "title": "Flutter Cookbook", "description": "Write, test, and publish your web, desktop...", "imageLinks": { - "smallThumbnail": "http://books.google.com/books/content?id=gcnAEAAAQBAJ&printsec=frontcover&img=1&zoom=5&edge=curl&source=gbs_api" + "smallThumbnail": "http://thumbnailurl/" } } } @@ -25,6 +27,11 @@ const _singleBookResponse = ''' } '''; +final _dummyPngImage = base64Decode( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmM' + 'IQAAAABJRU5ErkJggg==', +); + void main() { Widget app(Client client) => Provider( create: (_) => client, @@ -42,11 +49,14 @@ void main() { testWidgets('test search with one result', (WidgetTester tester) async { final mockClient = MockClient((request) async { - if (request.url.path != '/books/v1/volumes' && - request.url.queryParameters['q'] != 'Flutter') { - return Response('', 404); + if (request.url.path == '/books/v1/volumes' && + request.url.queryParameters['q'] == 'Flutter') { + return Response(_singleBookResponse, 200); + } else if (request.url == Uri.https('thumbnailurl', '/')) { + return Response.bytes(_dummyPngImage, 200, + headers: const {'Content-Type': 'image/png'}); } - return Response(_singleBookResponse, 200); + return Response('', 404); }); await tester.pumpWidget(app(mockClient)); diff --git a/pkgs/http/README.md b/pkgs/http/README.md index 642c238035..85ec2911df 100644 --- a/pkgs/http/README.md +++ b/pkgs/http/README.md @@ -272,7 +272,7 @@ $ dart compile exe --define=no_default_http_client=true ... > [!TIP] > [The Flutter HTTP example application][flutterhttpexample] demonstrates > how to make the configured [`Client`][client] available using -> [`package:provider`][provider] and [`runWithClient`](runwithclient). +> [`package:provider`][provider] and [`package:http_image_provider`][http_image_provider]. [browserclient]: https://pub.dev/documentation/http/latest/browser_client/BrowserClient-class.html [client]: https://pub.dev/documentation/http/latest/http/Client-class.html @@ -284,6 +284,7 @@ $ dart compile exe --define=no_default_http_client=true ... [fetch]: https://pub.dev/packages/fetch_client [fetchclient]: https://pub.dev/documentation/fetch_client/latest/fetch_client/FetchClient-class.html [flutterhttpexample]: https://github.com/dart-lang/http/tree/master/pkgs/flutter_http_example +[http_image_provider]: https://pub.dev/documentation/http_image_provider [ioclient]: https://pub.dev/documentation/http/latest/io_client/IOClient-class.html [isolate]: https://dart.dev/language/concurrency#how-isolates-work [flutterstatemanagement]: https://docs.flutter.dev/data-and-backend/state-mgmt/options