Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Application strategies part1 #1186

Draft
wants to merge 44 commits into
base: main
Choose a base branch
from
Draft
Changes from 1 commit
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
bf5172c
Start of the application strategies. Add, edit and delete strategy.
IrinaSouth Oct 2, 2024
57be98f
Fix creating vs. updating strategy
IrinaSouth Oct 3, 2024
96019c8
Merge branch 'main' into feature/application-strategies-part1
IrinaSouth Oct 3, 2024
35737ba
fix async create
IrinaSouth Oct 3, 2024
8051e1d
return error if strategy already exists
IrinaSouth Oct 3, 2024
dca0e4f
tidy up old code
IrinaSouth Oct 4, 2024
0690f3e
ability to add application strategy from the feature value editing sc…
IrinaSouth Oct 22, 2024
dd6eef1
refactor feature value display on the main dashboard and add shared s…
IrinaSouth Oct 25, 2024
17e0659
make strategy editable, fix page reload, ability to remove strategy, …
IrinaSouth Oct 31, 2024
bb7f428
add backend saving support
rvowles Nov 4, 2024
6ea6161
update strategy functionality
IrinaSouth Nov 5, 2024
72e1cfe
changes to ensure we can test properly
rvowles Nov 5, 2024
f1b2122
updates
IrinaSouth Nov 5, 2024
1bedf9e
server related problems
rvowles Nov 6, 2024
fe342da
update issues
rvowles Nov 7, 2024
f0c6de0
ordering of shared strategies
rvowles Nov 7, 2024
923a484
prevent duplicate strategies being passed and sort out deleting
rvowles Nov 8, 2024
d56dc60
delete check and lock check
rvowles Nov 8, 2024
ce594db
fix behaviour when no apps present
IrinaSouth Nov 9, 2024
45c1eb4
support usage API feature
rvowles Nov 9, 2024
5f93906
updated messaging api to deliver feature diffs
rvowles Nov 9, 2024
4cb4537
add application prefix
rvowles Nov 9, 2024
60d979b
display usage in envs and features
IrinaSouth Nov 16, 2024
2a5bee6
more tests but still borked
rvowles Nov 18, 2024
606f943
add slack handlebars for application strategy
IrinaSouth Nov 19, 2024
c01f7ec
unbust build
rvowles Nov 21, 2024
01ed3d3
deal with app strategies being the _only_ thing that changed
rvowles Nov 24, 2024
7811b9f
enable reordering of app strategies
IrinaSouth Nov 27, 2024
bdda85b
add app strategy permissions
IrinaSouth Nov 29, 2024
568b88c
make table non-paginated
IrinaSouth Dec 3, 2024
87825f0
add new properties to app strategy
IrinaSouth Dec 3, 2024
ce25eaa
dawg, updated list application strategies
rvowles Dec 3, 2024
f5d4b30
update
IrinaSouth Dec 4, 2024
e5fa5f8
added pagination for application strategies
rvowles Dec 6, 2024
a8ebd68
include new permission
IrinaSouth Dec 7, 2024
9696113
lots of changes around publishing when changes happen for a app strategy
rvowles Dec 7, 2024
ca27eac
include new permission part 2
IrinaSouth Dec 7, 2024
5c8afbd
fixes around publishing behaviour + testing
rvowles Dec 9, 2024
c8efc0d
update app strategy role permissions for API
rvowles Dec 9, 2024
8062be8
set max on listing app strategies
IrinaSouth Dec 16, 2024
8204ca9
permissions fix to allow superusers and
rvowles Jan 13, 2025
d4ef295
extra tests
rvowles Jan 21, 2025
974ff4b
add search function for shared strategy and add appid into the route
IrinaSouth Jan 25, 2025
1e8ae3e
minor update to include app name on edit screen
IrinaSouth Jan 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
update
IrinaSouth committed Dec 4, 2024
commit f5d4b300ff977eb3b4d4accb2781c960c5910b06
36 changes: 26 additions & 10 deletions admin-frontend/open_admin_app/lib/common/person_state.dart
Original file line number Diff line number Diff line change
@@ -40,32 +40,47 @@ class PersonState {
_personSource.add(p);
}

final _featureCreateRoles = [ApplicationRoleType.EDIT, ApplicationRoleType.EDIT_AND_DELETE, ApplicationRoleType.FEATURE_CREATE];
final _featureEditDeleteRoles = [ApplicationRoleType.EDIT, ApplicationRoleType.EDIT_AND_DELETE];
final _featureCreateRoles = [
ApplicationRoleType.FEATURE_EDIT,
ApplicationRoleType.FEATURE_EDIT_AND_DELETE,
ApplicationRoleType.FEATURE_CREATE
];
final _featureEditDeleteRoles = [
ApplicationRoleType.FEATURE_EDIT,
ApplicationRoleType.FEATURE_EDIT_AND_DELETE
];

bool personCanEditFeaturesForApplication(String? appId) {
return _personHasApplicationRoleInApp(appId, _featureEditDeleteRoles );
return _personHasApplicationRoleInApp(appId, _featureEditDeleteRoles);
}

bool personCanCreateFeaturesForApplication(String? appId) {
return _personHasApplicationRoleInApp(appId, _featureCreateRoles );
return _personHasApplicationRoleInApp(appId, _featureCreateRoles);
}

// if we add roles that are NOT feature related, this will need to change to exclude them
bool personCanAnythingFeaturesForApplication(String? appId) {
return _isUserIsSuperAdmin ||
person.groups.any((gp) => gp.applicationRoles.any((ar) => ar.applicationId == appId && ar.roles.isNotEmpty) == true);
person.groups.any((gp) =>
gp.applicationRoles.any(
(ar) => ar.applicationId == appId && ar.roles.isNotEmpty) ==
true);
}

bool _personHasApplicationRoleInApp(String? appId, List<ApplicationRoleType> roles) {
bool _personHasApplicationRoleInApp(
String? appId, List<ApplicationRoleType> roles) {
if (appId == null) {
return _isUserIsSuperAdmin;
}

return _isUserIsSuperAdmin ||
person.groups.any((gp) => gp.applicationRoles.any((ar) => ar.applicationId == appId &&
( gp.admin == true || ar.roles.any((roleForAppInGroup) => roles.contains(roleForAppInGroup)) )
) == true);
person.groups.any((gp) =>
gp.applicationRoles.any((ar) =>
ar.applicationId == appId &&
(gp.admin == true ||
ar.roles.any((roleForAppInGroup) =>
roles.contains(roleForAppInGroup)))) ==
true);
}

bool userHasPortfolioPermission(String? pid) {
@@ -80,7 +95,8 @@ class PersonState {
if (appId == null) return false;

return person.groups.any((g) =>
g.applicationRoles.any((appRole) => appRole.applicationId == appId) == true);
g.applicationRoles.any((appRole) => appRole.applicationId == appId) ==
true);
}

// if they are admin in a group where there is no portfolio id, they are super-admin
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ import 'package:advanced_datatable/datatable.dart';
import 'package:bloc_provider/bloc_provider.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:mrapi/api.dart';
import 'package:open_admin_app/api/client_api.dart';
import 'package:open_admin_app/common/stream_valley.dart';
@@ -109,7 +110,9 @@ class ApplicationStrategyListState extends State<ApplicationStrategyList> {
columns: [
DataColumn(
label: const Text('Name'), onSort: setSort),
const DataColumn(label: Text("Date added")),
const DataColumn(label: Text("Date created (UTC)")),
const DataColumn(label: Text("Date updated (UTC)")),
const DataColumn(label: Text("Created by")),
const DataColumn(
label: Text('Used in'),
),
@@ -160,7 +163,7 @@ class ApplicationStrategyListState extends State<ApplicationStrategyList> {
}

class ApplicationStrategyDataTableSource
extends AdvancedDataTableSource<ApplicationRolloutStrategy> {
extends AdvancedDataTableSource<ListApplicationRolloutStrategyItem> {
String lastSearchTerm = '';
final ApplicationStrategyBloc bloc;
final BuildContext context;
@@ -183,14 +186,14 @@ class ApplicationStrategyDataTableSource
}

@override
Future<RemoteDataSourceDetails<ApplicationRolloutStrategy>> getNextPage(
NextPageRequest pageRequest) async {
Future<RemoteDataSourceDetails<ListApplicationRolloutStrategyItem>>
getNextPage(NextPageRequest pageRequest) async {
final data = await bloc.getStrategiesData(
lastSearchTerm.isNotEmpty ? lastSearchTerm : null,
(pageRequest.sortAscending ?? true) == true
? SortOrder.ASC
: SortOrder.DESC);
List<ApplicationRolloutStrategy> rs = data.items;
List<ListApplicationRolloutStrategyItem> rs = data.items;
return RemoteDataSourceDetails(
data.max,
rs,
@@ -205,21 +208,27 @@ class ApplicationStrategyDataTableSource
index: index,
cells: [
DataCell(Text(
strategy.name,
strategy.strategy.name,
)),
DataCell(Text(
"",
DateFormat('yyyy-MM-dd HH:mm:ss').format(strategy.whenCreated),
)),
DataCell(Text(
'environments: ${strategy.usage!.length}, feature values: ${strategy.usage!.map((e) => e.featuresCount).sum}')),
DateFormat('yyyy-MM-dd HH:mm:ss').format(strategy.whenUpdated),
)),
DataCell(
Text(strategy.updatedBy.email),
),
DataCell(Text(
'environments: ${strategy.strategy.usage!.length}, feature values: ${strategy.strategy.usage!.map((e) => e.featuresCount).sum}')),
DataCell(Row(children: <Widget>[
FHIconButton(
icon: const Icon(Icons.edit),
onPressed: () => {
ManagementRepositoryClientBloc.router.navigateTo(
context, '/edit-application-strategy',
params: {
'id': [strategy.id]
'id': [strategy.strategy.id]
})
}),
// const SizedBox(
@@ -229,16 +238,16 @@ class ApplicationStrategyDataTableSource
icon: const Icon(Icons.delete),
onPressed: () => bloc.mrClient.addOverlay((BuildContext context) {
return FHDeleteThingWarningWidget(
thing: "Application strategy '${strategy.name}'",
thing: "Application strategy '${strategy.strategy.name}'",
content:
'This application strategy will be deleted and unassigned from all the flags. \n\nThis cannot be undone!',
bloc: bloc.mrClient,
deleteSelected: () async {
try {
await bloc.deleteStrategy(strategy.id);
await bloc.deleteStrategy(strategy.strategy.id);
setNextView(); // triggers reload from server with latest settings and rebuilds state
bloc.mrClient.addSnackbar(Text(
"Application strategy '${strategy.name}' deleted!"));
"Application strategy '${strategy.strategy.name}' deleted!"));
return true;
} catch (e, s) {
await bloc.mrClient.dialogError(e, s);
@@ -253,7 +262,7 @@ class ApplicationStrategyDataTableSource
onSelectChanged: (newValue) {
ManagementRepositoryClientBloc.router
.navigateTo(context, '/edit-application-strategy', params: {
'id': [strategy.id]
'id': [strategy.strategy.id]
});
});
}
Original file line number Diff line number Diff line change
@@ -173,9 +173,12 @@ class _AdminFeatureRole {

final _adminFeatureRoles = [
_AdminFeatureRole('none', 'No feature permissions', []),
_AdminFeatureRole('creator', 'Create features', [ApplicationRoleType.FEATURE_CREATE]),
_AdminFeatureRole('editor', 'Create / Edit / Delete features',
[ApplicationRoleType.FEATURE_CREATE, ApplicationRoleType.EDIT_AND_DELETE])
_AdminFeatureRole(
'creator', 'Create features', [ApplicationRoleType.FEATURE_CREATE]),
_AdminFeatureRole('editor', 'Create / Edit / Delete features', [
ApplicationRoleType.FEATURE_CREATE,
ApplicationRoleType.FEATURE_EDIT_AND_DELETE
])
];

final _noFeaturePermissionRole = _adminFeatureRoles[0];
@@ -187,7 +190,7 @@ _AdminFeatureRole _discoverAdminRoleType(
.firstWhereOrNull((element) => element.applicationId == applicationId)
?.roles ??
[];
if (roles.length == 1 && roles.contains(ApplicationRoleType.EDIT)) {
if (roles.length == 1 && roles.contains(ApplicationRoleType.FEATURE_EDIT)) {
return _editorFeaturePermissionRole;
}
return _adminFeatureRoles
@@ -416,8 +419,8 @@ class _GroupPermissionDetailState extends State<_GroupPermissionDetailWidget> {
(item) => item.applicationId == aid && item.groupId == group.id);

if (agr == null ||
!(agr.roles.contains(ApplicationRoleType.EDIT) ||
agr.roles.contains(ApplicationRoleType.EDIT_AND_DELETE))) {
!(agr.roles.contains(ApplicationRoleType.FEATURE_EDIT) ||
agr.roles.contains(ApplicationRoleType.FEATURE_EDIT_AND_DELETE))) {
return false;
}
return true;
@@ -429,7 +432,7 @@ class _GroupPermissionDetailState extends State<_GroupPermissionDetailWidget> {
applicationId: aid,
groupId: group.id,
roles: [
ApplicationRoleType.EDIT_AND_DELETE,
ApplicationRoleType.FEATURE_EDIT_AND_DELETE,
ApplicationRoleType.FEATURE_CREATE
]);
group.applicationRoles.add(agr);
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import 'package:mrapi/api.dart';
import 'package:open_admin_app/widgets/features/editing_feature_value_block.dart';

class ApplicationStrategiesDropDown extends StatefulWidget {
final List<ApplicationRolloutStrategy> strategies;
final List<ListApplicationRolloutStrategyItem> strategies;
final EditingFeatureValueBloc bloc;

const ApplicationStrategiesDropDown(
@@ -42,11 +42,11 @@ class _ApplicationStrategiesDropDownState
isDense: true,
items: widget.strategies.isNotEmpty
? widget.strategies
.map((ApplicationRolloutStrategy strategy) {
.map((ListApplicationRolloutStrategyItem strategy) {
return DropdownMenuItem<String>(
value: strategy.id,
value: strategy.strategy.id,
child: Text(
strategy.name,
strategy.strategy.name,
style: Theme.of(context).textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
));
Original file line number Diff line number Diff line change
@@ -334,7 +334,8 @@ class _EditFeatureValueWidgetState extends State<EditFeatureValueWidget> {
child: const Text(
"Show available strategies")),
StreamBuilder<
List<ApplicationRolloutStrategy>>(
List<
ListApplicationRolloutStrategyItem>>(
stream: widget.bloc
.availableApplicationStrategies,
builder: (context, snapshot) {
Original file line number Diff line number Diff line change
@@ -35,10 +35,11 @@ class EditingFeatureValueBloc implements Bloc {
_applicationStrategySource
.stream; // should it be ApplicationRolloutStrategy type?

late final BehaviorSubject<List<ApplicationRolloutStrategy>>
late final BehaviorSubject<List<ListApplicationRolloutStrategyItem>>
_availableApplicationStrategiesSource;
Stream<List<ApplicationRolloutStrategy>> get availableApplicationStrategies =>
_availableApplicationStrategiesSource.stream;
Stream<List<ListApplicationRolloutStrategyItem>>
get availableApplicationStrategies =>
_availableApplicationStrategiesSource.stream;

final _isFeatureValueUpdatedSource = BehaviorSubject<bool>.seeded(false);
BehaviorSubject<bool> get isFeatureValueUpdatedStream =>
@@ -72,7 +73,7 @@ class EditingFeatureValueBloc implements Bloc {
BehaviorSubject<List<RolloutStrategyInstance>>.seeded(
[...currentFeatureValue.rolloutStrategyInstances ?? []]);
_availableApplicationStrategiesSource =
BehaviorSubject<List<ApplicationRolloutStrategy>>.seeded([]);
BehaviorSubject<List<ListApplicationRolloutStrategyItem>>.seeded([]);
environmentId = environmentFeatureValue.environmentId;
addFeatureValueToStream(featureValue);
_featureValueStreamSubscription = _currentFv.listen(featureValueHasChanged);
@@ -222,15 +223,15 @@ class EditingFeatureValueBloc implements Bloc {
if (_selectedStrategyIdToAdd != null) {
final strategyList = _availableApplicationStrategiesSource.value;

ApplicationRolloutStrategy ars = strategyList
.firstWhere((strategy) => strategy.id == _selectedStrategyIdToAdd!);
ListApplicationRolloutStrategyItem ars = strategyList.firstWhere(
(strategy) => strategy.strategy.id == _selectedStrategyIdToAdd!);
var currentApplicationStrategies = _applicationStrategySource.value;
if (!currentApplicationStrategies.any(
(strategy) => strategy.strategyId == _selectedStrategyIdToAdd!)) {
var rolloutStrategy = RolloutStrategyInstance(
value: feature.valueType == FeatureValueType.BOOLEAN ? false : null,
name: ars.name,
strategyId: ars.id);
name: ars.strategy.name,
strategyId: ars.strategy.id);
currentApplicationStrategies.add(rolloutStrategy);
_applicationStrategySource.add(currentApplicationStrategies);
updateApplicationStrategyValue();
4 changes: 0 additions & 4 deletions backend/mr-api/application-strategies.yaml
Original file line number Diff line number Diff line change
@@ -279,10 +279,6 @@ components:
type: string
format: date-time
nullable: true
lastUpdatedByEmail:
type: string
lastUpdatedByName:
type: string
ApplicationRolloutStrategy:
allOf:
- $ref: "#/components/schemas/BaseStrategy"