Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,18 @@ class ProtectedEnvironmentsRepository {
String name,
) => client.protectedEnvironments.getForApprovalGroupEdit(projectId, name);

Future<ProtectedEnvironment> editApprovalGroupInheritance(
int projectId,
String name, {
required int ruleId,
required int groupInheritanceType,
}) => client.protectedEnvironments.editApprovalGroupInheritance(
projectId,
name,
ruleId: ruleId,
groupInheritanceType: groupInheritanceType,
);

Future<ProtectedEnvironment> addApprovalGroup(
int projectId,
String name, {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:gitlab_api/gitlab_api.dart';
import 'package:gitlab_models/gitlab_models.dart';

import 'protected_environments_controller.dart';

class ProtectedEnvironmentApprovalInheritanceController
extends FamilyAsyncNotifier<void, ProtectedEnvironmentRef> {
bool _disposed = false;

@override
void build(ProtectedEnvironmentRef arg) {
ref.watch(protectedEnvironmentsRepositoryProvider.future);
_disposed = false;
ref.onDispose(() => _disposed = true);
}

Future<ProtectedEnvironment> inspect() async {
final session = ref.read(protectedEnvironmentsRepositoryProvider.future);
bool isCurrent() =>
!_disposed &&
identical(
session,
ref.read(protectedEnvironmentsRepositoryProvider.future),
);
final repository = await session;
if (!isCurrent()) {
throw const GitLabConflictException('Protection session changed');
}
if (repository == null) throw StateError('No authenticated account');
await repository.ensureUniqueName(arg.projectId, arg.name);
if (!isCurrent()) {
throw const GitLabConflictException('Protection session changed');
}
final detail = await repository.getForApprovalGroupEdit(
arg.projectId,
arg.name,
);
if (!isCurrent()) {
throw const GitLabConflictException('Protection session changed');
}
return detail;
}

Future<void> edit({
required ProtectedEnvironment expected,
required int ruleId,
required int groupInheritanceType,
}) async {
if (state.isLoading) {
throw StateError('Approval membership update already pending');
}
if (expected.name != arg.name ||
arg.name.trim().isEmpty ||
arg.name != arg.name.trim() ||
ruleId <= 0 ||
!{0, 1}.contains(groupInheritanceType)) {
throw ArgumentError(
'Exact environment rule, rule ID and supported membership required',
);
}
final session = ref.read(protectedEnvironmentsRepositoryProvider.future);
bool isCurrent() =>
!_disposed &&
identical(
session,
ref.read(protectedEnvironmentsRepositoryProvider.future),
);
void invalidate() {
ref.invalidate(protectedEnvironmentsControllerProvider(arg.projectId));
ref.invalidate(protectedEnvironmentDetailProvider(arg));
}

var attempted = false;
state = const AsyncLoading();
try {
final current = await inspect();
if (!isCurrent() || current != expected) {
throw const GitLabConflictException('Protection rule changed');
}
final matches = current.approvalRules
.where((rule) => rule.id == ruleId)
.toList();
if (matches.length != 1 ||
!editableApprovalGroupMembership(matches.single) ||
matches.single.groupInheritanceType == groupInheritanceType) {
throw const GitLabConflictException(
'Approval membership is unavailable or unchanged',
);
}
final intended = expected.copyWith(
approvalRules: [
for (final rule in expected.approvalRules)
rule.id == ruleId
? rule.copyWith(groupInheritanceType: groupInheritanceType)
: rule,
],
);
final repository = await session;
if (!isCurrent()) {
throw const GitLabConflictException('Protection session changed');
}
if (repository == null) throw StateError('No authenticated account');
attempted = true;
final updated = await repository.editApprovalGroupInheritance(
arg.projectId,
arg.name,
ruleId: ruleId,
groupInheritanceType: groupInheritanceType,
);
if (!isCurrent()) {
throw const GitLabConflictException('Protection session changed');
}
if (updated != intended) {
throw const GitLabServerException(
'Unconfirmed environment approval membership',
);
}
final confirmed = await repository.getForApprovalGroupEdit(
arg.projectId,
arg.name,
);
if (!isCurrent()) {
throw const GitLabConflictException('Protection session changed');
}
if (confirmed != intended) {
throw const GitLabServerException(
'Unconfirmed environment approval membership',
);
}
state = const AsyncData(null);
invalidate();
} catch (error, stackTrace) {
if (isCurrent()) {
state = AsyncError(error, stackTrace);
if (attempted) invalidate();
}
rethrow;
}
}
}

bool editableApprovalGroupMembership(ProtectedEnvironmentAccess rule) =>
rule.id != null &&
rule.id! > 0 &&
rule.groupId != null &&
rule.groupId! > 0 &&
rule.userId == null &&
rule.accessLevel == null &&
rule.requiredApprovals != null &&
rule.requiredApprovals! > 0 &&
{0, 1}.contains(rule.groupInheritanceType);

final protectedEnvironmentApprovalInheritanceControllerProvider =
AsyncNotifierProvider.family<
ProtectedEnvironmentApprovalInheritanceController,
void,
ProtectedEnvironmentRef
>(ProtectedEnvironmentApprovalInheritanceController.new);
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import 'package:go_router/go_router.dart';

import '../../../app/router.dart';
import '../../../l10n/app_localizations.dart';
import 'controllers/protected_environment_approval_inheritance_controller.dart';
import 'controllers/protected_environments_controller.dart';
import 'widgets/protected_environment_approval_count_dialog.dart';
import 'widgets/protected_environment_approval_group_dialog.dart';
import 'widgets/protected_environment_approval_inheritance_dialog.dart';
import 'widgets/protected_environment_deploy_role_dialog.dart';
import 'widgets/protected_environment_edit_role_dialog.dart';
import 'widgets/protected_environment_remove_approval_dialog.dart';
Expand Down Expand Up @@ -235,6 +237,51 @@ class ProtectedEnvironmentDetailScreen extends ConsumerWidget {
);
final permissions = Column(
children: [
if (projectKey != null &&
data.approvalRules.any(editableApprovalGroupMembership))
Padding(
padding: const EdgeInsets.only(bottom: LabFoxSpacing.sm),
child: OutlinedButton.icon(
key: const ValueKey(
'protected-environment-approval-inheritance-open',
),
icon: const Icon(Icons.account_tree_outlined),
label: Text(
l10n.protectedEnvironmentApprovalInheritanceTitle,
),
onPressed: () async {
final session = ref.read(
protectedEnvironmentsRepositoryProvider.future,
);
final edited = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (_) =>
ProtectedEnvironmentApprovalInheritanceDialog(
target: projectKey,
rule: data,
),
);
if (edited == true &&
context.mounted &&
identical(
session,
ref.read(
protectedEnvironmentsRepositoryProvider
.future,
),
)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
l10n.protectedEnvironmentApprovalInheritanceSuccess,
),
),
);
}
},
),
),
if (projectKey != null)
Padding(
padding: const EdgeInsets.only(bottom: LabFoxSpacing.sm),
Expand Down
Loading
Loading