Skip to content

Commit

Permalink
Admin UI - Add a prompt/snackbar when feature value or its strategy c…
Browse files Browse the repository at this point in the history
…hanges (#1136)

* add a snackbar/prompt when feature value changes


Co-authored-by: Irina Southwell <“irinasouthwell@gmail.com”>
  • Loading branch information
IrinaSouth and Irina Southwell authored Mar 29, 2024
1 parent 532f7a0 commit c3e9855
Show file tree
Hide file tree
Showing 10 changed files with 261 additions and 225 deletions.
29 changes: 15 additions & 14 deletions admin-frontend/open_admin_app/lib/theme/theme_data.dart
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import 'package:flutter/material.dart';

final ThemeData myTheme = ThemeData(
colorScheme: flexSchemeLight,
useMaterial3: true,
brightness: Brightness.light,
dataTableTheme: const DataTableThemeData(
headingTextStyle: TextStyle(
fontSize: 14, fontWeight: FontWeight.w800, color: Colors.black87)),
);
colorScheme: flexSchemeLight,
useMaterial3: true,
brightness: Brightness.light,
dataTableTheme: const DataTableThemeData(
headingTextStyle: TextStyle(
fontSize: 14, fontWeight: FontWeight.w800, color: Colors.black87)),
snackBarTheme: const SnackBarThemeData(backgroundColor: Colors.orange));

final ThemeData darkTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: flexSchemeDark,
dataTableTheme: const DataTableThemeData(
headingTextStyle: TextStyle(
fontSize: 14, fontWeight: FontWeight.w800, color: Colors.white)),
);
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: flexSchemeDark,
dataTableTheme: const DataTableThemeData(
headingTextStyle: TextStyle(
fontSize: 14, fontWeight: FontWeight.w800, color: Colors.white)),
snackBarTheme: SnackBarThemeData(
backgroundColor: Colors.orange.shade800.withOpacity(0.7)));

// Light and dark ColorSchemes made by FlexColorScheme v7.0.1.
// These ColorScheme objects require Flutter 3.7 or later.
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@


import 'package:mrapi/api.dart';
import 'package:open_admin_app/widgets/features/editing_feature_value_block.dart';
import 'package:open_admin_app/widgets/strategyeditor/editing_rollout_strategy.dart';
Expand All @@ -16,16 +14,20 @@ class FeatureValueStrategyProvider extends StrategyEditorProvider {
}

@override
Future<RolloutStrategyValidationResponse?> validateStrategy(EditingRolloutStrategy rs) {
Future<RolloutStrategyValidationResponse?> validateStrategy(
EditingRolloutStrategy rs) {
// convert the "editing rollout strategy" we have been editing back to a normal strategy
// but with a null value
var strategy = rs.toRolloutStrategy(null)!;

// create a list of strategies, taking all the existing ones except for one where the id of
// the one we were editing matches the one in the list (i.e. replace the one in the list with
// this one)
final customStrategies = [strategy, ...
fvStrategyBloc.featureValue.rolloutStrategies!.where((rs) => rs.id != strategy.id)];
final customStrategies = [
strategy,
...fvStrategyBloc.featureValue.rolloutStrategies!
.where((rs) => rs.id != strategy.id)
];

// now go and do an evaluation
return fvStrategyBloc.perApplicationFeaturesBloc.validationCheck(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class _EditBooleanValueDropDownWidgetState
widget.strBloc.updateFeatureValueDefault(replacementBoolean);
} else {
widget.rolloutStrategy!.value = replacementBoolean;
// widget.strBloc.updateStrategy();
widget.strBloc.updateStrategyValue();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ class _EditJsonValueContainerState extends State<EditJsonValueContainer> {
: json.encode(json.decode(tec.text.trim())).toString();
if (widget.rolloutStrategy != null) {
widget.rolloutStrategy!.value = replacementValue;
widget.strBloc.updateStrategy();
widget.strBloc.updateStrategyValue();
} else {
widget.strBloc.updateFeatureValueDefault(replacementValue);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class _EditNumberValueContainerState extends State<EditNumberValueContainer> {

@override
Widget build(BuildContext context) {
final debouncer = Debouncer(milliseconds: 1000);

return SizedBox(
width: 200,
height: 36,
Expand Down Expand Up @@ -71,9 +73,13 @@ class _EditNumberValueContainerState extends State<EditNumberValueContainer> {
validateNumber(tec.text) != null ? 'Not a valid number' : null,
),
onChanged: (value) {
final replacementValue =
value.trim().isEmpty ? null : double.parse(tec.text.trim());
_updateFeatureValue(replacementValue);
debouncer.run(
() {
final replacementValue =
value.trim().isEmpty ? null : double.parse(tec.text.trim());
_updateFeatureValue(replacementValue);
},
);
},
inputFormatters: [
DecimalTextInputFormatter(
Expand All @@ -87,7 +93,7 @@ class _EditNumberValueContainerState extends State<EditNumberValueContainer> {
widget.strBloc.updateFeatureValueDefault(replacementValue);
} else {
widget.rolloutStrategy!.value = replacementValue;
//widget.strBloc.updateStrategy();
widget.strBloc.updateStrategyValue();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:mrapi/api.dart';
import 'package:open_admin_app/utils/utils.dart';
import 'package:open_admin_app/widgets/features/editing_feature_value_block.dart';

class EditStringValueContainer extends StatefulWidget {
Expand Down Expand Up @@ -40,6 +41,8 @@ class _EditStringValueContainerState extends State<EditStringValueContainer> {

@override
Widget build(BuildContext context) {
final debouncer = Debouncer(milliseconds: 1000);

return SizedBox(
width: 200,
height: 36,
Expand Down Expand Up @@ -68,13 +71,17 @@ class _EditStringValueContainerState extends State<EditStringValueContainer> {
: 'not set',
hintStyle: Theme.of(context).textTheme.bodySmall),
onChanged: (value) {
final replacementValue = value.isEmpty ? null : tec.text.trim();
if (widget.rolloutStrategy != null) {
widget.rolloutStrategy!.value = replacementValue;
//widget.strBloc.updateStrategy();
} else {
widget.strBloc.updateFeatureValueDefault(replacementValue);
}
debouncer.run(
() {
final replacementValue = value.isEmpty ? null : tec.text.trim();
if (widget.rolloutStrategy != null) {
widget.rolloutStrategy!.value = replacementValue;
widget.strBloc.updateStrategyValue();
} else {
widget.strBloc.updateFeatureValueDefault(replacementValue);
}
},
);
},
));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:bloc_provider/bloc_provider.dart';
import 'package:mrapi/api.dart';
import 'package:open_admin_app/fhos_logger.dart';
Expand All @@ -20,14 +22,18 @@ class EditingFeatureValueBloc implements Bloc {
// actually ORIGINAL feature value (before changes)
late FeatureValue currentFeatureValue;

late final BehaviorSubject<List<RolloutStrategy>> _strategySource;
final _rolloutStrategyAttributeList =
BehaviorSubject<List<RolloutStrategyAttribute>>();
Stream<List<RolloutStrategyAttribute>> get attributes =>
_rolloutStrategyAttributeList.stream;
late StreamSubscription<FeatureValue> _featureValueStreamSubscription;

late final BehaviorSubject<List<RolloutStrategy>> _strategySource;
Stream<List<RolloutStrategy>> get strategies => _strategySource.stream;

final _isFeatureValueUpdatedSource = BehaviorSubject<bool>.seeded(false);
BehaviorSubject<bool> get isFeatureValueUpdatedStream =>
_isFeatureValueUpdatedSource;

final _currentFv = BehaviorSubject<FeatureValue>();
get currentFv => _currentFv.stream;

EditingFeatureValueBloc(
this.applicationId,
this.feature,
Expand All @@ -44,13 +50,14 @@ class EditingFeatureValueBloc implements Bloc {
[...currentFeatureValue.rolloutStrategies ?? []]);
environmentId = environmentFeatureValue.environmentId;
addFeatureValueToStream(featureValue);
_featureValueStreamSubscription = _currentFv.listen(featureValueHasChanged);
}

/*
* This takes the result of the adding of a new strategy and converts it back to a RolloutStrategy
*/
void addStrategy(EditingRolloutStrategy rs) {
List<RolloutStrategy> strategies = _strategySource.value;
var strategies = _strategySource.value;

final index = strategies.indexWhere((s) => s.id == rs.id);
if (index == -1) {
Expand All @@ -65,18 +72,24 @@ class EditingFeatureValueBloc implements Bloc {
strategies[index] = rs.toRolloutStrategy(strategies[index].value)!;
}

updateFeatureValueStrategies(strategies);
currentFeatureValue.rolloutStrategies = strategies;
addFeatureValueToStream(currentFeatureValue);
_strategySource.add(strategies);
}

void updateStrategy() {
void updateStrategyValue() {
final strategies = _strategySource.value;
_strategySource.add(strategies);
currentFeatureValue.rolloutStrategies = strategies;
addFeatureValueToStream(currentFeatureValue);
}

void updateStrategyAndFeatureValue() {
final strategies = _strategySource.value;
_strategySource.add(strategies);
updateFeatureValueStrategies(strategies);
void featureValueHasChanged(FeatureValue updatedFeatureValue) {
if (featureValue != updatedFeatureValue) {
_isFeatureValueUpdatedSource.add(true);
} else {
_isFeatureValueUpdatedSource.add(false);
}
}

void removeStrategy(RolloutStrategy rs) {
Expand All @@ -85,7 +98,7 @@ class EditingFeatureValueBloc implements Bloc {
fhosLogger.fine(
"removing strategy ${rs.id} from list ${strategies.map((e) => e.id)}");
strategies.removeWhere((e) => e.id == rs.id);
_strategySource.add(strategies);
updateStrategyValue();
}

updateFeatureValueLockedStatus(bool locked) {
Expand All @@ -98,12 +111,6 @@ class EditingFeatureValueBloc implements Bloc {
addFeatureValueToStream(currentFeatureValue);
}

void updateFeatureValueStrategies(List<RolloutStrategy> strategies) {
currentFeatureValue.rolloutStrategies = strategies;
addFeatureValueToStream(currentFeatureValue);
_strategySource.add(strategies);
}

void updateFeatureValueDefault(replacementValue) {
switch (feature.valueType) {
case FeatureValueType.BOOLEAN:
Expand All @@ -122,10 +129,6 @@ class EditingFeatureValueBloc implements Bloc {
addFeatureValueToStream(currentFeatureValue);
}

final _currentFv = BehaviorSubject<FeatureValue>();

get currentFv => _currentFv.stream;

PerApplicationFeaturesBloc get perApplicationFeaturesBloc =>
_featureStatusBloc;

Expand All @@ -135,11 +138,13 @@ class EditingFeatureValueBloc implements Bloc {

@override
void dispose() {
_featureValueStreamSubscription.cancel();
_currentFv.close();
_isFeatureValueUpdatedSource.close();
_strategySource.close();
}

saveFeatureValueUpdates() async {
currentFeatureValue.rolloutStrategies = _strategySource.value;
await _featureServiceApi.updateAllFeatureValuesByApplicationForKey(
applicationId, feature.key, [currentFeatureValue]);
await _featureStatusBloc.updateApplicationFeatureValuesStream();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import 'package:open_admin_app/widgets/strategyeditor/individual_strategy_bloc.d
import 'package:open_admin_app/widgets/strategyeditor/rollout_strategies_widget.dart';
import 'package:open_admin_app/widgets/strategyeditor/strategy_utils.dart';


class StrategyEditingWidget extends StatefulWidget {
final bool editable;
final StrategyEditorBloc bloc;
Expand Down Expand Up @@ -45,8 +44,7 @@ class _StrategyEditingWidgetState extends State<StrategyEditingWidget> {
_strategyName.text = widget.bloc.rolloutStrategy.name ?? '';

if (widget.bloc.rolloutStrategy.percentage != null) {
_strategyPercentage.text =
widget.bloc.rolloutStrategy.percentageText;
_strategyPercentage.text = widget.bloc.rolloutStrategy.percentageText;
}
}

Expand Down Expand Up @@ -103,8 +101,7 @@ class _StrategyEditingWidgetState extends State<StrategyEditingWidget> {
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Column(children: [
if ((widget.bloc.rolloutStrategy.percentage !=
null) ||
if ((widget.bloc.rolloutStrategy.percentage != null) ||
showPercentageField)
Row(
children: [
Expand Down Expand Up @@ -199,7 +196,9 @@ class _StrategyEditingWidgetState extends State<StrategyEditingWidget> {
),
if (widget.editable)
FHFlatButton(
title: widget.bloc.rolloutStrategy.saved ? 'Update' : 'Add',
title: widget.bloc.rolloutStrategy.saved
? 'Update'
: 'Add',
onPressed: () => _validationAction()),
],
),
Expand All @@ -223,20 +222,23 @@ class _StrategyEditingWidgetState extends State<StrategyEditingWidget> {
/// this window.
Future<void> _processUpdate() async {
final updatedStrategy = widget.bloc.rolloutStrategy.copy(
name: _strategyName.text,
attributes: widget.bloc.currentAttributes)
name: _strategyName.text, attributes: widget.bloc.currentAttributes)
..percentageFromText = _strategyPercentage.text;

await checkForViolationsAndPop(updatedStrategy, () async => await widget.bloc.strategyEditorProvider.updateStrategy(updatedStrategy));
await checkForViolationsAndPop(updatedStrategy, () async {
await widget.bloc.strategyEditorProvider.updateStrategy(updatedStrategy);
});
}

Future<void> checkForViolationsAndPop(EditingRolloutStrategy updatedStrategy, AsyncCallback onSuccess) async {
Future<void> checkForViolationsAndPop(
EditingRolloutStrategy updatedStrategy, AsyncCallback onSuccess) async {
final localValidationCheck = updatedStrategy.violations();

if (localValidationCheck.isNotEmpty) {
widget.bloc.updateLocalViolations(localValidationCheck);
} else {
final validationCheck = await widget.bloc.strategyEditorProvider.validateStrategy(updatedStrategy);
final validationCheck = await widget.bloc.strategyEditorProvider
.validateStrategy(updatedStrategy);

if (validationCheck != null) {
if (isValidationOk(validationCheck)) {
Expand Down
13 changes: 8 additions & 5 deletions docs/modules/ROOT/pages/users.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ NOTE: FeatureHub doesn't send emails to recover passwords or any registration or

==== When there is only one Super Admin

When there is only a single Super Admin, and they have forgotten their password, the only way to reset it is to go to the database. To do this, in the database, find the id of the superuser in the `fh_person` table, and reset the `password` field to `1000:caffda0b26e265a0977718a548d784e6:1123a076c3925d0d77f2c902115e8732de25ae22394f74faaa52c8d9d9a829b8021299afd4a1793e47936445bb0ceff0f17f329716342db19f4e428dd5859dc1`. You can then login with the password `featurehub`.
When there is only a single Super Admin, and they have forgotten their password, the only way to reset it is to go to the database. To do this, in the database, find the id of the superuser in the `fh_person` table, and reset the `password` field to `1000:caffda0b26e265a0977718a548d784e6:1123a076c3925d0d77f2c902115e8732de25ae22394f74faaa52c8d9d9a829b8021299afd4a1793e47936445bb0ceff0f17f329716342db19f4e428dd5859dc1`.

You can then login using the password `featurehub`.

== User groups

Expand Down Expand Up @@ -83,7 +85,7 @@ image::fh-group-permissions.png[Group permissions, 1500]

=== Administrator groups

There are two types of administrator groups that are available by default, *Organization Super Admin* and *Portfolio Admin*.
There are two types of administrator groups that are available by default, *Portfolio Admin* and *Organization Super Admin*.

==== Portfolio Administrators
Portfolio Administrators permissions:
Expand All @@ -97,16 +99,17 @@ Portfolio Administrators permissions:
** Manage groups access to applications
** Add and delete user from a group

NOTE: Every Portfolio automatically gets a group called "Administrators", simply adding people to this group
NOTE: Every Portfolio automatically gets a group called "Administrators" on creation, simply adding people to this group
will make them administrators for this portfolio.

==== Organization Super Admin
Organization Super Admin permissions:

Inherits all permissions "Portfolio Admin" has, plus:
Inherits all permissions of "Portfolio Admin", plus all the following permissions:

** Create and manage users of the system
** Create and manage user groups
** Create and manage portfolios
** Create and manage Admin service accounts

In other words, organization super admin has got all privileges, hence it is recommended to have at least 2 super admins, in case one of them leaves the organization.
TIP: In other words, organization super admin has got all privileges, hence it is recommended to have at least 2 super admins, in case one of them leaves the organization.

0 comments on commit c3e9855

Please sign in to comment.