Skip to content

Commit

Permalink
AdminUI: Manage quotas on Identity details page (#676)
Browse files Browse the repository at this point in the history
* feat: manage identity quotas

* chore: remove buttons and trigger update on tier change

* refactor: extract add quota dialog into a core modal

* chore: add a tooltip and grey out the disabled rows

* chore: wrap a column widget in a card widget

* fix: revert allow empty response

* refactor: proper codeshare

* refactor: undo other stuff

* fix. undo strange translation

* fix: add assert to make sure _addQuota is not mis-called

* refactor: undo even more stuff

* chore: make one function call the dialog

* chore: remove late keyword

* chore: use bool getter

* chore: move identity quota into a separate file

* refactor: extract quota buttons into separate widget

* refactor: endpoint call

* refactor: rename the folder

* fix: change imports

* fix: move identity quota list to correct folder

* fix: imports

* fix: import

* fix: move quota button group out of the core

* fix: import

* fix: move quotas button group

* fix: revert changes

* refactor: rename a file

* fix: import

* chore: simplify stuff

* refactor: this defenitely was no "button"

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Julian König <[email protected]>
  • Loading branch information
3 people authored May 28, 2024
1 parent e6747e6 commit 7c96ffd
Show file tree
Hide file tree
Showing 10 changed files with 326 additions and 95 deletions.
Original file line number Diff line number Diff line change
@@ -1,52 +1,73 @@
import 'dart:async';

import 'package:admin_api_sdk/admin_api_sdk.dart';
import 'package:admin_api_types/admin_api_types.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get_it/get_it.dart';

import '/core/core.dart';
import '../constants.dart';
import '../extensions.dart';

Future<void> showAddQuotaDialog({
required BuildContext context,
required VoidCallback onQuotaAdded,
String? tierId,
String? identityAddress,
}) async {
assert(tierId != null || identityAddress != null, 'Either tierId or address must be provided');
assert(tierId == null || identityAddress == null, 'Only one of tierId or address can be provided');

final metrics = await GetIt.I.get<AdminApiClient>().quotas.getMetrics();

if (!context.mounted) return;

Future<void> showAddQuotaDialog({required BuildContext context, required String tierId, required VoidCallback onQuotaAdded}) async {
await showDialog<void>(
context: context,
builder: (BuildContext context) => _AddQuotaDialog(
tierId: tierId,
availableMetrics: metrics.data,
addQuota: ({required String metricKey, required int max, required String period}) {
if (tierId != null) {
return GetIt.I.get<AdminApiClient>().quotas.createTierQuota(tierId: tierId, metricKey: metricKey, max: max, period: period);
}

return GetIt.I.get<AdminApiClient>().quotas.createIdentityQuota(address: identityAddress!, metricKey: metricKey, max: max, period: period);
},
onQuotaAdded: onQuotaAdded,
),
);
}

class _AddQuotaDialog extends StatefulWidget {
final String tierId;
final List<Metric> availableMetrics;
final Future<ApiResponse<dynamic>> Function({required String metricKey, required int max, required String period}) addQuota;
final VoidCallback onQuotaAdded;

const _AddQuotaDialog({required this.tierId, required this.onQuotaAdded});
const _AddQuotaDialog({
required this.availableMetrics,
required this.addQuota,
required this.onQuotaAdded,
});

@override
State<_AddQuotaDialog> createState() => _AddQuotaDialogState();
}

class _AddQuotaDialogState extends State<_AddQuotaDialog> {
final _maxAmountController = TextEditingController();
List<Metric> _availableMetrics = [];

bool _saving = false;
String? _errorMessage;

String? _selectedMetric;
int? _maxAmount;
String? _selectedPeriod;

bool get _isValid => _selectedMetric != null && _maxAmount != null && _selectedPeriod != null;

@override
void initState() {
super.initState();

_maxAmountController.addListener(() => setState(() => _maxAmount = int.tryParse(_maxAmountController.text)));

_loadMetrics();
}

@override
Expand All @@ -68,7 +89,8 @@ class _AddQuotaDialogState extends State<_AddQuotaDialog> {
mainAxisSize: MainAxisSize.min,
children: [
DropdownButtonFormField(
items: _availableMetrics.map((metric) => DropdownMenuItem(value: metric.key, child: Text(metric.displayName))).toList(),
value: _selectedMetric,
items: widget.availableMetrics.map((metric) => DropdownMenuItem(value: metric.key, child: Text(metric.displayName))).toList(),
onChanged: _saving ? null : (String? selected) => setState(() => _selectedMetric = selected),
decoration: const InputDecoration(
border: OutlineInputBorder(),
Expand All @@ -89,6 +111,7 @@ class _AddQuotaDialogState extends State<_AddQuotaDialog> {
),
Gaps.h24,
DropdownButtonFormField(
value: _selectedPeriod,
items: const [
DropdownMenuItem(value: 'Hour', child: Text('Hour')),
DropdownMenuItem(value: 'Day', child: Text('Day')),
Expand Down Expand Up @@ -130,12 +153,13 @@ class _AddQuotaDialogState extends State<_AddQuotaDialog> {
Future<void> _addQuota() async {
setState(() => _saving = true);

final response = await GetIt.I.get<AdminApiClient>().quotas.createTierQuota(
tierId: widget.tierId,
metricKey: _selectedMetric!,
max: _maxAmount!,
period: _selectedPeriod!,
);
assert(_selectedMetric != null && _maxAmount != null && _selectedPeriod != null, 'Invalid state');

final response = await widget.addQuota(
metricKey: _selectedMetric!,
max: _maxAmount!,
period: _selectedPeriod!,
);

if (response.hasError) {
setState(() {
Expand All @@ -149,9 +173,4 @@ class _AddQuotaDialogState extends State<_AddQuotaDialog> {
if (mounted) Navigator.of(context, rootNavigator: true).pop();
widget.onQuotaAdded();
}

Future<void> _loadMetrics() async {
final metrics = await GetIt.I.get<AdminApiClient>().quotas.getMetrics();
setState(() => _availableMetrics = metrics.data);
}
}
1 change: 1 addition & 0 deletions AdminUi/apps/admin_ui/lib/core/modals/modals.dart
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export 'add_quota_dialog.dart';
export 'confirmation_dialog.dart';
export 'settings_dialog.dart';
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import 'package:logger/logger.dart';
class IdentityDataTableSource extends AsyncDataTableSource {
Pagination? _pagination;
var _sortingSettings = (sortColumnIndex: 0, sortAscending: true);

IdentityOverviewFilter? _filter;
set filter(IdentityOverviewFilter? newFilter) {
if (_filter != newFilter) {
Expand All @@ -30,18 +29,14 @@ class IdentityDataTableSource extends AsyncDataTableSource {

@override
bool get isRowCountApproximate => false;

@override
int get rowCount => _pagination?.totalRecords ?? 0;

@override
int get selectedRowCount => 0;

@override
Future<AsyncRowsResponse> getRows(int startIndex, int count) async {
final pageNumber = startIndex ~/ count;
final orderBy = _getODataOrderBy();

try {
final response = await GetIt.I.get<AdminApiClient>().identities.getIdentities(
pageNumber: pageNumber,
Expand Down Expand Up @@ -83,11 +78,9 @@ class IdentityDataTableSource extends AsyncDataTableSource {
),
)
.toList();

return AsyncRowsResponse(response.pagination.totalRecords, rows);
} catch (e) {
GetIt.I.get<Logger>().e('Failed to load data: $e');

throw Exception('Failed to load data: $e');
}
}
Expand Down
104 changes: 104 additions & 0 deletions AdminUi/apps/admin_ui/lib/core/widgets/quotas_button_group.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import 'package:admin_api_sdk/admin_api_sdk.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';

import '../constants.dart';
import '../modals/modals.dart';

class QuotasButtonGroup extends StatefulWidget {
final List<String> selectedQuotas;
final VoidCallback onQuotasChanged;
final String? identityAddress;
final String? tierId;

const QuotasButtonGroup({
required this.selectedQuotas,
required this.onQuotasChanged,
this.identityAddress,
this.tierId,
super.key,
}) : assert(identityAddress != null || tierId != null, 'Either identityAddress or tierId must be provided'),
assert(identityAddress == null || tierId == null, 'Only one of identityAddress or tierId can be provided');

@override
State<QuotasButtonGroup> createState() => _QuotasButtonGroupState();
}

class _QuotasButtonGroupState extends State<QuotasButtonGroup> {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
icon: Icon(
Icons.delete,
color: widget.selectedQuotas.isNotEmpty ? Theme.of(context).colorScheme.onError : null,
),
style: ButtonStyle(
backgroundColor: WidgetStateProperty.resolveWith((states) {
return widget.selectedQuotas.isNotEmpty ? Theme.of(context).colorScheme.error : null;
}),
),
onPressed: widget.selectedQuotas.isNotEmpty ? _removeSelectedQuotas : null,
),
Gaps.w8,
IconButton.filled(
icon: const Icon(Icons.add),
onPressed: () => showAddQuotaDialog(
context: context,
identityAddress: widget.identityAddress,
tierId: widget.tierId,
onQuotaAdded: widget.onQuotasChanged,
),
),
],
),
);
}

Future<void> _removeSelectedQuotas() async {
final confirmed = await showConfirmationDialog(
context: context,
title: 'Remove Quotas',
message:
'Are you sure you want to remove the selected quotas from ${widget.identityAddress != null ? 'the identity "${widget.identityAddress}"' : 'the tier "${widget.tierId}"'}?',
);

if (!confirmed) return;

for (final quota in widget.selectedQuotas) {
final result = await _deleteQuota(quota);
if (result.hasError && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('An error occurred while deleting the quota(s). Please try again.'),
showCloseIcon: true,
),
);

return;
}
}

widget.onQuotasChanged();
widget.selectedQuotas.clear();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Selected quota(s) have been removed.'),
showCloseIcon: true,
),
);
}
}

Future<ApiResponse<void>> _deleteQuota(String quota) {
final client = GetIt.I.get<AdminApiClient>();

if (widget.identityAddress != null) return client.quotas.deleteIdentityQuota(address: widget.identityAddress!, individualQuotaId: quota);
return client.quotas.deleteTierQuota(tierId: widget.tierId!, tierQuotaDefinitionId: quota);
}
}
1 change: 1 addition & 0 deletions AdminUi/apps/admin_ui/lib/core/widgets/widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export 'app_title.dart';
export 'copy_to_clipboard_button.dart';
export 'filters/filters.dart';
export 'identities_data_table/identities_data_table.dart';
export 'quotas_button_group.dart';
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:get_it/get_it.dart';
import 'package:intl/intl.dart';

import '/core/core.dart';
import 'identity_quotas_table/identity_quotas_table.dart';
import 'modals/change_tier.dart';

class IdentityDetails extends StatefulWidget {
Expand Down Expand Up @@ -66,6 +67,8 @@ class _IdentityDetailsState extends State<IdentityDetails> {
availableTiers: _tiers!,
updateTierOfIdentity: _reloadIdentity,
),
Gaps.h16,
IdentityQuotaList(identityDetails, _reloadIdentity),
],
),
),
Expand Down
Loading

0 comments on commit 7c96ffd

Please sign in to comment.