Skip to content

Commit

Permalink
Admin UI: Overview of all Clients (#619)
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

* feat: add clients overview dialogs

* chore: update types

* feat: add clients overview

* chore: replace placeholder width clients overview widget

* feat: add client filters

* chore: add barrel exports

* 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: remove unnecessary barrel export

* chore: remove unnecessary widget

* 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: change title to label

* chore: remove unnecessary if statement

* 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

* chore: add MultiSelectFilter widget

* fix: widget parameter error

* fix: build error

* fix: center text

* fix: remove custom onTap to restore selection rows

* fix: use fixed labels

* fix: update button style

* chore: naming

* chore: move optional to utils

* fix: make createdAt a dateTime

* refactor: simplify everything

* fix: remove text size

* fix: use TextField for dates

* fix: avoid mem leak

* chore: 8er padding

* refactor: styling and codestyle in change client secret

* fix: request focus on open dialog

* chore: show close button in snack bars

* refactor: direct feedback in list after creating client

* fix: use pop scope to not cancel during delete / save

* chore: naming

* refactor: use titles, show loading

* fix: error handing

* chore: wording & buggy behaviour

* refactor: align clients and identities

* chore: PR comments

* chore: move texts

* chore: inline code

* refactor: use buttons inside the field

* fix: date filters lastDate

* chore: add Backbone to title

* feat: properly format dates

* refactor: local imports

---------

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 May 3, 2024
1 parent 0a90d2a commit aed77da
Show file tree
Hide file tree
Showing 28 changed files with 1,039 additions and 152 deletions.
12 changes: 9 additions & 3 deletions AdminUi/apps/admin_ui/lib/core/widgets/app_title.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import 'package:flutter_svg/flutter_svg.dart';
import '../constants.dart';

class AppTitle extends StatelessWidget {
const AppTitle({super.key});
final EdgeInsetsGeometry? padding;

const AppTitle({this.padding, super.key});

@override
Widget build(BuildContext context) {
const textStyle = TextStyle(fontSize: 25);

return Row(
final row = Row(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset('assets/logo.svg', width: 30, height: 30),
Expand All @@ -19,11 +21,15 @@ class AppTitle extends StatelessWidget {
TextSpan(
children: [
TextSpan(text: 'enmeshed', style: textStyle.copyWith(fontWeight: FontWeight.bold)),
const TextSpan(text: ' Admin UI', style: textStyle),
const TextSpan(text: ' Backbone Admin UI', style: textStyle),
],
),
),
],
);

if (padding == null) return row;

return Padding(padding: padding!, child: row);
}
}
62 changes: 31 additions & 31 deletions AdminUi/apps/admin_ui/lib/core/widgets/filters/date_filter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,30 @@ class DateFilter extends StatefulWidget {
}

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

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

_controller = TextEditingController();
}

@override
void dispose() {
_controller.dispose();

super.dispose();
}

@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${widget.label}:',
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text('${widget.label}:', style: const TextStyle(fontWeight: FontWeight.bold)),
Gaps.h8,
Row(
children: [
Expand All @@ -41,31 +53,16 @@ class _DateFilterState extends State<DateFilter> {
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),
),
],
],
SizedBox(
width: 160,
child: TextField(
onTap: _selectNewDate,
readOnly: true,
controller: _controller,
decoration: InputDecoration(
border: const OutlineInputBorder(),
suffixIcon:
_selectedDate == null ? const Icon(Icons.calendar_today) : IconButton(onPressed: _clearDate, icon: const Icon(Icons.clear)),
),
),
),
Expand All @@ -77,6 +74,7 @@ class _DateFilterState extends State<DateFilter> {

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

Expand All @@ -85,12 +83,14 @@ class _DateFilterState extends State<DateFilter> {
context: context,
initialDate: _selectedDate ?? DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime.now().add(const Duration(days: 1)),
lastDate: DateTime.now(),
locale: Localizations.localeOf(context),
);

if (picked == null) return;
if (picked == null || !mounted) return;

setState(() => _selectedDate = picked);
_controller.text = DateFormat.yMd(Localizations.localeOf(context).languageCode).format(picked);
widget.onFilterSelected(_operator, picked);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@ class InputField extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$label:',
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text('$label:', style: const TextStyle(fontWeight: FontWeight.bold)),
Gaps.h8,
SizedBox(
width: 180,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,7 @@ class MultiSelectFilter extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$label:',
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text('$label:', style: const TextStyle(fontWeight: FontWeight.bold)),
Gaps.h8,
SizedBox(
width: 250,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,7 @@ class _NumberFilterState extends State<NumberFilter> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${widget.label}:',
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text('${widget.label}:', style: const TextStyle(fontWeight: FontWeight.bold)),
Gaps.h8,
Row(
children: [
Expand All @@ -53,7 +50,6 @@ class _NumberFilterState extends State<NumberFilter> {
widget.onNumberSelected(_operator, enteredValue);
},
decoration: const InputDecoration(border: OutlineInputBorder()),
style: const TextStyle(fontSize: 12),
inputFormatters: <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly],
keyboardType: TextInputType.number,
),
Expand Down
189 changes: 189 additions & 0 deletions AdminUi/apps/admin_ui/lib/home/clients_overview/clients_filter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
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:get_it/get_it.dart';
import 'package:multi_dropdown/multiselect_dropdown.dart';

import '/core/core.dart';

class ClientsFilter {
final String? clientId;
final String? displayName;
final List<String>? tiers;
final (FilterOperator, DateTime)? createdAt;
final (FilterOperator, int)? numberOfIdentities;

const ClientsFilter({
this.clientId,
this.displayName,
this.tiers,
this.createdAt,
this.numberOfIdentities,
});

static const empty = ClientsFilter();

ClientsFilter copyWith({
Optional<String>? clientId,
Optional<String>? displayName,
Optional<List<String>>? tiers,
Optional<(FilterOperator, DateTime)>? createdAt,
Optional<(FilterOperator, int)>? numberOfIdentities,
}) {
return ClientsFilter(
clientId: clientId != null ? clientId.value : this.clientId,
displayName: displayName != null ? displayName.value : this.displayName,
tiers: tiers != null ? tiers.value : this.tiers,
createdAt: createdAt != null ? createdAt.value : this.createdAt,
numberOfIdentities: numberOfIdentities != null ? numberOfIdentities.value : this.numberOfIdentities,
);
}

List<Clients> apply(List<Clients> clients) => clients.where(matches).toList();

bool matches(Clients client) {
if (clientId != null && !client.clientId.contains(clientId!)) return false;
if (displayName != null && !client.displayName.contains(displayName!)) return false;
if (tiers != null && !tiers!.contains(client.defaultTier.id)) return false;
if (createdAt != null && !_applyDateFilter(client.createdAt, createdAt!.$2, createdAt!.$1)) return false;
if (numberOfIdentities != null && !_applyNumberFilter(client.numberOfIdentities ?? 0, numberOfIdentities!.$2, numberOfIdentities!.$1)) {
return false;
}

return true;
}

bool _applyDateFilter(DateTime clientDate, DateTime filterDate, FilterOperator filterOperator) {
final clientDateAtMidnight = DateTime(clientDate.year, clientDate.month, clientDate.day);
final filterDateAtMidnight = DateTime(filterDate.year, filterDate.month, filterDate.day);

return switch (filterOperator) {
FilterOperator.equal => clientDateAtMidnight.isAtSameMomentAs(filterDateAtMidnight),
FilterOperator.lessThan => clientDateAtMidnight.isBefore(filterDateAtMidnight),
FilterOperator.greaterThan => clientDateAtMidnight.isAfter(filterDateAtMidnight),
FilterOperator.lessThanOrEqual =>
clientDateAtMidnight.isBefore(filterDateAtMidnight) || clientDateAtMidnight.isAtSameMomentAs(filterDateAtMidnight),
FilterOperator.greaterThanOrEqual =>
clientDateAtMidnight.isAfter(filterDateAtMidnight) || clientDateAtMidnight.isAtSameMomentAs(filterDateAtMidnight),
FilterOperator.notEqual => !clientDateAtMidnight.isAtSameMomentAs(filterDateAtMidnight),
};
}

bool _applyNumberFilter(int clientNumber, int filterNumber, FilterOperator filterOperator) {
return switch (filterOperator) {
FilterOperator.equal => clientNumber == filterNumber,
FilterOperator.lessThan => clientNumber < filterNumber,
FilterOperator.greaterThan => clientNumber > filterNumber,
FilterOperator.lessThanOrEqual => clientNumber <= filterNumber,
FilterOperator.greaterThanOrEqual => clientNumber >= filterNumber,
FilterOperator.notEqual => clientNumber != filterNumber,
};
}
}

class ClientsFilterRow extends StatefulWidget {
final void Function(ClientsFilter filter) onFilterChanged;

const ClientsFilterRow({
required this.onFilterChanged,
super.key,
});

@override
State<ClientsFilterRow> createState() => _ClientsFilterRowState();
}

class _ClientsFilterRowState extends State<ClientsFilterRow> {
late MultiSelectController<String> _tierController;

ClientsFilter filter = ClientsFilter.empty;

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

_tierController = MultiSelectController();
_loadTiers();
}

@override
void dispose() {
_tierController.dispose();

super.dispose();
}

@override
Widget build(BuildContext context) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Padding(
padding: const EdgeInsets.all(8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
InputField(
label: 'Client ID',
onEnteredText: (String enteredText) {
filter = filter.copyWith(clientId: enteredText.isEmpty ? const Optional.absent() : Optional(enteredText));

widget.onFilterChanged(filter);
},
),
Gaps.w16,
InputField(
label: 'Display Name',
onEnteredText: (String enteredText) {
filter = filter.copyWith(displayName: enteredText.isEmpty ? const Optional.absent() : Optional(enteredText));

widget.onFilterChanged(filter);
},
),
Gaps.w16,
MultiSelectFilter(
label: 'Default Tier',
searchLabel: 'Search Tiers',
controller: _tierController,
onOptionSelected: (List<ValueItem<dynamic>> selectedOptions) {
filter = filter.copyWith(
tiers: selectedOptions.isEmpty ? const Optional.absent() : Optional(selectedOptions.map((item) => item.value as String).toList()),
);

widget.onFilterChanged(filter);
},
),
Gaps.w16,
NumberFilter(
label: 'Number of Identitites',
onNumberSelected: (FilterOperator operator, String enteredValue) {
filter = filter.copyWith(
numberOfIdentities: enteredValue.isEmpty ? const Optional.absent() : Optional((operator, int.parse(enteredValue))),
);

widget.onFilterChanged(filter);
},
),
Gaps.w16,
DateFilter(
label: 'Created At',
onFilterSelected: (FilterOperator operator, DateTime? selectedDate) {
filter = filter.copyWith(
createdAt: selectedDate == null ? const Optional.absent() : Optional((operator, selectedDate)),
);

widget.onFilterChanged(filter);
},
),
],
),
),
);
}

Future<void> _loadTiers() async {
final response = await GetIt.I.get<AdminApiClient>().tiers.getTiers();
final defaultTiers = response.data.where((element) => element.canBeUsedAsDefaultForClient == true).toList();
final tierItems = defaultTiers.map((tier) => ValueItem(label: tier.name, value: tier.id)).toList();
_tierController.setOptions(tierItems);
}
}
Loading

0 comments on commit aed77da

Please sign in to comment.