Skip to content

Commit

Permalink
Admin UI: Overview of all identities (#593)
Browse files Browse the repository at this point in the history
* feat: add data table 2

* Admin UI: Login page (#584)

* chore: adapt the header appearance

* chore: add new design for login  box

* chore: adapt login position in padding widget  via screen size

* chore: logic for displaying the error message for attempted login

* chore: remove error message

* chore: add error message

* chore: change the button colors and add a text button

* chore: remove Visibility widget

* chore: add text style for text button

* chore: change todo comment according to the flutter style

* refactor: extract the app title to be a separate widget

* refactor: extract text field to be reusable

* chore: use new app title widget

* chore: use custom text field

* chore: make the text field fixed height

* chore: rename variables and make them private

* chore: update imports

* chore: use extracted app title widget

* chore: remove unnecessary widgets and center the card

* chore: add custom colors and move login button to the bottom

* chore: remove unused import

* refactor: rename folder and files appropriately

* refactor: extract sized box into a separate custom widget

* refactor: extract elevated button into a separate custom widget

* chore: make the variable private

* chore: local variable should not start with underscore

* chore: add DI for baseUrl

* fix: untangle coding

* fix: imports

* fix: make CustomColors easier accessible

* fix: make prettier

* feat: add gaps

* fix: rename file

* refactor: remove CustomX, make whole logic with one variable

* fix: bool logic

* ci: add jkoenig134 as codeowner

* chore: remove accidentally committet code

* fix: undo change

* refactor: simplify login screen

* refactor: simplify app title

* refactor: PR comments

* chore: inline svg picture

* chore: make multi line params last

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Julian König <[email protected]>
Co-authored-by: Timo Notheisen <[email protected]>

* feat: add multiselect

* feat: add data table 2 and multi dropdown

* fix: remove dependencies from wrong pubspec

* fix: remove filter when there are no filters applied

* chore: add builders

* chore: display the IdentityOverview

* fix: change the widget

* feat: add identity overview

* feat: add components

* feat: add shared filter fields

* chore: tier overview (wip)

* chore: identity details (wip)

* chore: pagination (wip)

* chore: update rowCount

* fix: pagination

* fix: pagination

* chore: remove comment and print

* chore: remove comments

* fix: revert created files

* chore: use switch expression

* chore: use gaps 16 and enlarge the textfield to 120

* chore: remove unused controller

* chore: use async paginated data table 2

* chore: rename variables and add this.*

* chore: change the named parameter

* chore: change the color depending of a theme mode

* chore: rename a label

* chore: remove comment

* chore: move files into a new folder

* chore: remove components.dart and change the import

* chore: move the file in the new folder and implement async data table source

* chore: add trailing commas

* refactor: change code structure / remove empty widget

* chore: add logger

* fix: data loading

* chore: make selected date nullable and remove isDateSelected

* chore: make variables private and remove late

* fix: overhaul sorting and filtering

* fix: add empty hint

* fix: remove dynamic

* chore: add set state

* chore: remove onDateSelected from setState

* chore: pageNumber should start from 0

* chore: trigger order by from odata request

* chore: remove required

* chore: make constructor inline

* chore: remove unnecessary setState

* chore: rename the method

* chore: add onOptionRemoved and make the dropdown wider

* fix: revert remove the onOptionRemoved

* fix: remove comments

* fix: use map instead of List<_Operator>

* fix: remove unused scrollController

* feat: add intl dependency

* chore: add optional and remove unnecessary variables and method

* chore: use absent method

* fix: change substring to date format

* fix: use lastLoginAt

* fix: add a statement to check whether the lastLoginAt is null or not

* refactor: simplify

* chore: remove unused field

* chore: remove the unused from method

* fix: make method inline

* fix: change to named parameter

* fix: make field private

* fix: undo the change

* fix: make method inline

* fix: use record

* fix: rename variables

* fix: use setter

* fix: resolve ! for newValue

* fix: move callback outside of setState

* fix: rename a method

* fix: maximum date that can be picked is "tomorrow"

* fix: make an early return

* fix: move callback outside of setState and make setState inline

* fix: trigger onFilterSelected when operator changes

* chore: make an early return

* fix: use DateFormat

* fix: replace variable with null

* fix: return picked instead of _selectedDate

* chore: make early return, rename variable and make statement inline

* fix: remove setState and rename variable

* fix: swap places of helper method with main method

* fix: revert make method more readable

* fix: move the setter's position

* fix: remove setState

* refactor: cleanup / make more readable

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Julian König <[email protected]>
Co-authored-by: Timo Notheisen <[email protected]>
  • Loading branch information
4 people authored Apr 26, 2024
1 parent ff91a68 commit d41de55
Show file tree
Hide file tree
Showing 20 changed files with 705 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -1 +1 @@
* @tnotheis @jkoenig134
* @tnotheis @jkoenig134
96 changes: 96 additions & 0 deletions AdminUi/apps/admin_ui/lib/core/widgets/filters/date_filter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import 'package:admin_api_sdk/admin_api_sdk.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';

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

class DateFilter extends StatefulWidget {
final void Function(FilterOperator operator, DateTime? selectedDate) onFilterSelected;
final String label;

const DateFilter({required this.onFilterSelected, required this.label, super.key});

@override
State<DateFilter> createState() => _DateFilterState();
}

class _DateFilterState extends State<DateFilter> {
FilterOperator _operator = FilterOperator.equal;
DateTime? _selectedDate;

@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${widget.label}:',
style: const TextStyle(fontWeight: FontWeight.bold),
),
Gaps.h8,
Row(
children: [
DropdownButton<FilterOperator>(
value: _operator,
onChanged: (selectedOperator) {
if (selectedOperator == null) return;
setState(() => _operator = selectedOperator);
widget.onFilterSelected(selectedOperator, _selectedDate);
},
items: FilterOperator.values.toDropdownMenuItems(),
),
Gaps.w8,
InkWell(
onTap: _selectNewDate,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_selectedDate != null ? DateFormat('yyyy-MM-dd').format(_selectedDate!) : 'Select date',
style: const TextStyle(fontSize: 14),
),
Gaps.w8,
const Icon(Icons.calendar_today),
if (_selectedDate != null) ...[
Gaps.w8,
GestureDetector(
onTap: _clearDate,
child: const Icon(Icons.clear, size: 20),
),
],
],
),
),
),
],
),
],
);
}

void _clearDate() {
setState(() => _selectedDate = null);
widget.onFilterSelected(_operator, null);
}

Future<void> _selectNewDate() async {
final picked = await showDatePicker(
context: context,
initialDate: _selectedDate ?? DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime.now().add(const Duration(days: 1)),
);

if (picked == null) return;

setState(() => _selectedDate = picked);
widget.onFilterSelected(_operator, picked);
}
}
4 changes: 4 additions & 0 deletions AdminUi/apps/admin_ui/lib/core/widgets/filters/filters.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export 'date_filter.dart';
export 'input_filter.dart';
export 'multi_select.dart';
export 'number_filter.dart';
31 changes: 31 additions & 0 deletions AdminUi/apps/admin_ui/lib/core/widgets/filters/input_filter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import 'package:flutter/material.dart';

import '/core/core.dart';

class InputField extends StatelessWidget {
final void Function(String enteredText) onEnteredText;
final String label;

const InputField({required this.onEnteredText, required this.label, super.key});

@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$label:',
style: const TextStyle(fontWeight: FontWeight.bold),
),
Gaps.h8,
SizedBox(
width: 180,
child: TextField(
onChanged: onEnteredText,
decoration: const InputDecoration(border: OutlineInputBorder()),
),
),
],
);
}
}
51 changes: 51 additions & 0 deletions AdminUi/apps/admin_ui/lib/core/widgets/filters/multi_select.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:multi_dropdown/multiselect_dropdown.dart';

import '/core/core.dart';

class MultiSelectFilter extends StatelessWidget {
final String label;
final String searchLabel;
final MultiSelectController<String> controller;
final void Function(List<ValueItem<String>> selectedOptions) onOptionSelected;

const MultiSelectFilter({
required this.label,
required this.searchLabel,
required this.controller,
required this.onOptionSelected,
super.key,
});

@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$label:',
style: const TextStyle(fontWeight: FontWeight.bold),
),
Gaps.h8,
SizedBox(
width: 250,
child: MultiSelectDropDown(
hint: '',
searchLabel: searchLabel,
searchEnabled: true,
controller: controller,
options: controller.options,
fieldBackgroundColor: Theme.of(context).colorScheme.background,
searchBackgroundColor: Theme.of(context).colorScheme.background,
dropdownBackgroundColor: Theme.of(context).colorScheme.background,
selectedOptionBackgroundColor: Theme.of(context).colorScheme.background,
selectedOptionTextColor: Theme.of(context).colorScheme.onBackground,
optionsBackgroundColor: Theme.of(context).colorScheme.background,
optionTextStyle: TextStyle(color: Theme.of(context).colorScheme.onBackground),
onOptionSelected: onOptionSelected,
),
),
],
);
}
}
66 changes: 66 additions & 0 deletions AdminUi/apps/admin_ui/lib/core/widgets/filters/number_filter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import 'package:admin_api_sdk/admin_api_sdk.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

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

class NumberFilter extends StatefulWidget {
final void Function(FilterOperator operator, String enteredValue) onNumberSelected;
final String label;

const NumberFilter({
required this.onNumberSelected,
required this.label,
super.key,
});

@override
State<NumberFilter> createState() => _NumberFilterState();
}

class _NumberFilterState extends State<NumberFilter> {
late FilterOperator _operator = FilterOperator.equal;
late String _value = '';

@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${widget.label}:',
style: const TextStyle(fontWeight: FontWeight.bold),
),
Gaps.h8,
Row(
children: [
DropdownButton<FilterOperator>(
value: _operator,
onChanged: (selectedOperator) {
if (selectedOperator == null) return;
setState(() => _operator = selectedOperator);
widget.onNumberSelected(selectedOperator, _value);
},
items: FilterOperator.values.toDropdownMenuItems(),
),
Gaps.w16,
SizedBox(
width: 120,
child: TextField(
onChanged: (enteredValue) {
_value = enteredValue;
widget.onNumberSelected(_operator, enteredValue);
},
decoration: const InputDecoration(border: OutlineInputBorder()),
style: const TextStyle(fontSize: 12),
inputFormatters: <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly],
keyboardType: TextInputType.number,
),
),
],
),
],
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import 'package:admin_api_sdk/admin_api_sdk.dart';
import 'package:flutter/material.dart';

extension ToFilterOperatorDropdownMenuItem on List<FilterOperator> {
List<DropdownMenuItem<FilterOperator>> toDropdownMenuItems() => map(
(operator) => DropdownMenuItem<FilterOperator>(
value: operator,
child: Text(operator.userFriendlyOperator),
),
).toList();
}
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
@@ -1 +1,2 @@
export 'app_title.dart';
export 'filters/filters.dart';
9 changes: 1 addition & 8 deletions AdminUi/apps/admin_ui/lib/home/home.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import 'package:flutter/material.dart';

class Identities extends StatelessWidget {
const Identities({super.key});

@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
export 'identities_overview/identities_overview.dart';

class Tiers extends StatelessWidget {
const Tiers({super.key});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import 'package:admin_api_sdk/admin_api_sdk.dart';
import 'package:data_table_2/data_table_2.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:intl/intl.dart';
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) {
_filter = newFilter;
notifyListeners();
}
}

void sort({required int sortColumnIndex, required bool sortColumnAscending}) {
_sortingSettings = (sortColumnIndex: sortColumnIndex, sortAscending: sortColumnAscending);
notifyListeners();
}

@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,
pageSize: count,
filter: _filter,
orderBy: orderBy,
);
_pagination = response.pagination;

final rows = response.data.indexed
.map(
(identity) => DataRow.byIndex(
index: pageNumber * count + identity.$1,
cells: [
DataCell(Text(identity.$2.address)),
DataCell(Text(identity.$2.tier.name)),
DataCell(Text(identity.$2.createdWithClient)),
DataCell(Text(identity.$2.numberOfDevices.toString())),
DataCell(Text(DateFormat('yyyy-MM-dd').format(identity.$2.createdAt))),
DataCell(Text(identity.$2.lastLoginAt != null ? DateFormat('yyyy-MM-dd').format(identity.$2.lastLoginAt!) : '')),
DataCell(Text(identity.$2.datawalletVersion?.toString() ?? '')),
DataCell(Text(identity.$2.identityVersion.toString())),
],
),
)
.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');
}
}

String _getODataOrderBy() {
final columnName = _getFieldNameByIndex(_sortingSettings.sortColumnIndex);
final sortingDirection = _sortingSettings.sortAscending ? 'asc' : 'desc';
return '$columnName $sortingDirection';
}

String _getFieldNameByIndex(int index) => switch (index) {
0 => 'address',
2 => 'createdWithClient',
3 => 'numberOfDevices',
4 => 'createdAt',
5 => 'lastLoginAt',
6 => 'datawalletVersion',
7 => 'identityVersion',
_ => throw Exception('Invalid column index')
};
}
Loading

0 comments on commit d41de55

Please sign in to comment.