refactor: Rework ClientJobs cubit so it doesn't depend on other cubits

Also implemented tracking of the jobs and rebuild status
This commit is contained in:
Inex Code 2024-02-20 19:33:24 +03:00
parent fdb40fccd7
commit 16094a3257
17 changed files with 2313 additions and 192 deletions

View file

@ -36,7 +36,8 @@
"continue": "Continue",
"alert": "Alert",
"copied_to_clipboard": "Copied to clipboard!",
"please_connect": "Please connect your server, domain and DNS provider to dive in!"
"please_connect": "Please connect your server, domain and DNS provider to dive in!",
"network_error": "Network error"
},
"more_page": {
"configuration_wizard": "Setup wizard",
@ -394,7 +395,8 @@
"could_not_add_ssh_key": "Couldn't add SSH key",
"username_rule": "Username must contain only lowercase latin letters, digits and underscores, should not start with a digit",
"email_login": "Email login",
"no_ssh_notice": "Only email and SSH accounts are created for this user. Single Sign On for all services is coming soon."
"no_ssh_notice": "Only email and SSH accounts are created for this user. Single Sign On for all services is coming soon.",
"user_already_exists": "User with such username already exists"
},
"initializing": {
"server_provider_description": "A place where your data and SelfPrivacy services will reside:",
@ -594,6 +596,7 @@
"service_turn_off": "Turn off",
"service_turn_on": "Turn on",
"job_added": "Job added",
"job_postponed": "Job added, but you will be able to launch it after current jobs are finished",
"run_jobs": "Run jobs",
"reboot_success": "Server is rebooting",
"reboot_failed": "Couldn't reboot the server. Check the app logs.",
@ -606,7 +609,11 @@
"delete_ssh_key": "Delete SSH key for {}",
"server_jobs": "Jobs on the server",
"reset_user_password": "Reset password of user",
"generic_error": "Couldn't connect to the server!"
"generic_error": "Couldn't connect to the server!",
"rebuild_system": "Rebuild system",
"start_server_upgrade": "Start the server upgrade",
"change_auto_upgrade_settings": "Change auto-upgrade settings",
"change_server_timezone": "Change server timezone"
},
"validations": {
"required": "Required",

View file

@ -104,9 +104,7 @@ class BlocAndProviderConfigState extends State<BlocAndProviderConfig> {
),
BlocProvider(create: (final _) => volumesBloc),
BlocProvider(
create: (final _) => JobsCubit(
servicesBloc: servicesBloc,
),
create: (final _) => JobsCubit(),
),
],
child: widget.child,

View file

@ -52,6 +52,14 @@ mutation RunSystemRebuild {
}
}
mutation RunSystemRebuildFallback {
system {
runSystemRebuild {
...basicMutationReturnFields
}
}
}
mutation RunSystemRollback {
system {
runSystemRollback {
@ -71,6 +79,14 @@ mutation RunSystemUpgrade {
}
}
mutation RunSystemUpgradeFallback {
system {
runSystemUpgrade {
...basicMutationReturnFields
}
}
}
mutation PullRepositoryChanges {
system {
pullRepositoryChanges {

File diff suppressed because it is too large Load diff

View file

@ -29,7 +29,11 @@ mixin ServerActionsApi on GraphQLApiMap {
print(response.exception.toString());
}
if (response.parsedData!.system.rebootSystem.success) {
time = DateTime.now().toUtc();
return GenericResult(
data: time,
success: true,
message: response.parsedData!.system.rebootSystem.message,
);
}
} catch (e) {
print(e);
@ -50,23 +54,94 @@ mixin ServerActionsApi on GraphQLApiMap {
}
}
Future<bool> upgrade() async {
Future<GenericResult<ServerJob?>> upgrade() async {
try {
final GraphQLClient client = await getClient();
return _commonBoolRequest(
() async => client.mutate$RunSystemUpgrade(),
);
final result = await client.mutate$RunSystemUpgrade();
if (result.hasException) {
final fallbackResult = await client.mutate$RunSystemUpgradeFallback();
if (fallbackResult.parsedData!.system.runSystemUpgrade.success) {
return GenericResult(
success: true,
data: null,
message: fallbackResult.parsedData!.system.runSystemUpgrade.message,
);
} else {
return GenericResult(
success: false,
message: fallbackResult.parsedData!.system.runSystemUpgrade.message,
data: null,
);
}
} else if (result.parsedData!.system.runSystemUpgrade.success &&
result.parsedData!.system.runSystemUpgrade.job != null) {
return GenericResult(
success: true,
data: ServerJob.fromGraphQL(
result.parsedData!.system.runSystemUpgrade.job!,
),
message: result.parsedData!.system.runSystemUpgrade.message,
);
} else {
return GenericResult(
success: false,
message: result.parsedData!.system.runSystemUpgrade.message,
data: null,
);
}
} catch (e) {
return false;
return GenericResult(
success: false,
message: e.toString(),
data: null,
);
}
}
Future<void> apply() async {
Future<GenericResult<ServerJob?>> apply() async {
try {
final GraphQLClient client = await getClient();
await client.mutate$RunSystemRebuild();
final result = await client.mutate$RunSystemRebuild();
if (result.hasException) {
final fallbackResult = await client.mutate$RunSystemRebuildFallback();
if (fallbackResult.parsedData!.system.runSystemRebuild.success) {
return GenericResult(
success: true,
data: null,
message: fallbackResult.parsedData!.system.runSystemRebuild.message,
);
} else {
return GenericResult(
success: false,
message: fallbackResult.parsedData!.system.runSystemRebuild.message,
data: null,
);
}
} else {
if (result.parsedData!.system.runSystemRebuild.success &&
result.parsedData!.system.runSystemRebuild.job != null) {
return GenericResult(
success: true,
data: ServerJob.fromGraphQL(
result.parsedData!.system.runSystemRebuild.job!,
),
message: result.parsedData!.system.runSystemRebuild.message,
);
} else {
return GenericResult(
success: false,
message: result.parsedData!.system.runSystemRebuild.message,
data: null,
);
}
}
} catch (e) {
print(e);
return GenericResult(
success: false,
message: e.toString(),
data: null,
);
}
}
}

View file

@ -24,8 +24,6 @@ class DevicesBloc extends Bloc<DevicesEvent, DevicesState> {
final apiConnectionRepository = getIt<ApiConnectionRepository>();
_apiDataSubscription = apiConnectionRepository.dataStream.listen(
(final ApiData apiData) {
print('============');
print(apiData.devices.data);
add(
DevicesListChanged(apiData.devices.data),
);
@ -42,7 +40,6 @@ class DevicesBloc extends Bloc<DevicesEvent, DevicesState> {
if (state is DevicesDeleting) {
return;
}
print(event.devices);
if (event.devices == null) {
emit(DevicesError());
return;
@ -103,7 +100,6 @@ class DevicesBloc extends Bloc<DevicesEvent, DevicesState> {
@override
void onChange(final Change<DevicesState> change) {
super.onChange(change);
print(change);
}
@override

View file

@ -1,33 +1,55 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/bloc/services/services_bloc.dart';
import 'package:selfprivacy/logic/models/job.dart';
import 'package:selfprivacy/logic/models/json/server_job.dart';
export 'package:provider/provider.dart';
part 'client_jobs_state.dart';
class JobsCubit extends Cubit<JobsState> {
JobsCubit({
required this.servicesBloc,
}) : super(JobsStateEmpty());
JobsCubit() : super(JobsStateEmpty()) {
final apiConnectionRepository = getIt<ApiConnectionRepository>();
_apiDataSubscription = apiConnectionRepository.dataStream.listen(
(final ApiData apiData) {
if (apiData.serverJobs.data != null &&
apiData.serverJobs.data!.isNotEmpty) {
_handleServerJobs(apiData.serverJobs.data!);
}
},
);
}
StreamSubscription? _apiDataSubscription;
final ServerApi api = ServerApi();
final ServicesBloc servicesBloc;
void addJob(final ClientJob job) {
final jobs = currentJobList;
if (job.canAddTo(jobs)) {
_updateJobsState([
...jobs,
...[job],
]);
void _handleServerJobs(final List<ServerJob> jobs) {
if (state is! JobsStateLoading) {
return;
}
if (state.rebuildJobUid == null) {
return;
}
// Find a job with the uid of the rebuild job
final ServerJob? rebuildJob = jobs.firstWhereOrNull(
(final job) => job.uid == state.rebuildJobUid,
);
if (rebuildJob == null ||
rebuildJob.status == JobStatusEnum.error ||
rebuildJob.status == JobStatusEnum.finished) {
emit((state as JobsStateLoading).finished());
}
}
void addJob(final ClientJob job) async {
emit(state.addJob(job));
}
void removeJob(final String id) {
@ -35,61 +57,145 @@ class JobsCubit extends Cubit<JobsState> {
emit(newState);
}
List<ClientJob> get currentJobList {
final List<ClientJob> jobs = <ClientJob>[];
if (state is JobsStateWithJobs) {
jobs.addAll((state as JobsStateWithJobs).clientJobList);
}
return jobs;
}
void _updateJobsState(final List<ClientJob> newJobs) {
getIt<NavigationService>().showSnackBar('jobs.job_added'.tr());
emit(JobsStateWithJobs(newJobs));
}
Future<void> rebootServer() async {
emit(JobsStateLoading());
final rebootResult = await api.reboot();
if (rebootResult.success && rebootResult.data != null) {
getIt<NavigationService>().showSnackBar('jobs.reboot_success'.tr());
} else {
getIt<NavigationService>().showSnackBar('jobs.reboot_failed'.tr());
if (state is JobsStateEmpty) {
emit(
JobsStateLoading(
[RebootServerJob(status: JobStatusEnum.running)],
null,
const [],
),
);
final rebootResult = await api.reboot();
if (rebootResult.success && rebootResult.data != null) {
emit(
JobsStateFinished(
[
RebootServerJob(
status: JobStatusEnum.finished,
message: rebootResult.message,
),
],
null,
const [],
),
);
} else {
emit(
JobsStateFinished(
[RebootServerJob(status: JobStatusEnum.error)],
null,
const [],
),
);
}
}
emit(JobsStateEmpty());
}
Future<void> upgradeServer() async {
emit(JobsStateLoading());
final bool isPullSuccessful = await api.pullConfigurationUpdate();
final bool isSuccessful = await api.upgrade();
if (isSuccessful) {
if (!isPullSuccessful) {
getIt<NavigationService>().showSnackBar('jobs.config_pull_failed'.tr());
if (state is JobsStateEmpty) {
emit(
JobsStateLoading(
[UpgradeServerJob(status: JobStatusEnum.running)],
null,
const [],
),
);
final result = await getIt<ApiConnectionRepository>().api.upgrade();
if (result.success && result.data != null) {
emit(
JobsStateLoading(
[UpgradeServerJob(status: JobStatusEnum.finished)],
result.data!.uid,
const [],
),
);
} else {
getIt<NavigationService>().showSnackBar('jobs.upgrade_success'.tr());
emit(
JobsStateFinished(
[UpgradeServerJob(status: JobStatusEnum.error)],
null,
const [],
),
);
}
} else {
getIt<NavigationService>().showSnackBar('jobs.upgrade_failed'.tr());
}
emit(JobsStateEmpty());
}
Future<void> applyAll() async {
if (state is JobsStateWithJobs) {
final List<ClientJob> jobs = (state as JobsStateWithJobs).clientJobList;
emit(JobsStateLoading());
emit(JobsStateLoading(jobs, null, const []));
await Future<void>.delayed(Duration.zero);
final rebuildRequired = jobs.any((final job) => job.requiresRebuild);
for (final ClientJob job in jobs) {
job.execute(this);
emit(
(state as JobsStateLoading)
.updateJobStatus(job.id, JobStatusEnum.running),
);
final (result, message) = await job.execute(this);
if (result) {
emit(
(state as JobsStateLoading).updateJobStatus(
job.id,
JobStatusEnum.finished,
message: message,
),
);
} else {
emit(
(state as JobsStateLoading)
.updateJobStatus(job.id, JobStatusEnum.error, message: message),
);
}
}
await api.pullConfigurationUpdate();
await api.apply();
servicesBloc.add(const ServicesReload());
emit(JobsStateEmpty());
if (!rebuildRequired) {
emit((state as JobsStateLoading).finished());
return;
}
final rebuildResult = await getIt<ApiConnectionRepository>().api.apply();
if (rebuildResult.success) {
if (rebuildResult.data != null) {
emit(
(state as JobsStateLoading)
.copyWith(rebuildJobUid: rebuildResult.data!.uid),
);
} else {
emit((state as JobsStateLoading).finished());
}
} else {
emit((state as JobsStateLoading).finished());
}
}
}
Future<void> acknowledgeFinished() async {
if (state is! JobsStateFinished) {
return;
}
final rebuildJobUid = state.rebuildJobUid;
if ((state as JobsStateFinished).postponedJobs.isNotEmpty) {
emit(JobsStateWithJobs((state as JobsStateFinished).postponedJobs));
} else {
emit(JobsStateEmpty());
}
if (rebuildJobUid != null) {
await getIt<ApiConnectionRepository>().removeServerJob(rebuildJobUid);
}
}
@override
void onChange(final Change<JobsState> change) {
super.onChange(change);
}
@override
Future<void> close() {
_apiDataSubscription?.cancel();
return super.close();
}
}

View file

@ -1,17 +1,32 @@
part of 'client_jobs_cubit.dart';
abstract class JobsState extends Equatable {
sealed class JobsState extends Equatable {
String? get rebuildJobUid => null;
JobsState addJob(final ClientJob job);
@override
List<Object?> get props => [];
}
class JobsStateLoading extends JobsState {}
class JobsStateEmpty extends JobsState {
@override
JobsStateWithJobs addJob(final ClientJob job) {
getIt<NavigationService>().showSnackBar('jobs.job_added'.tr());
return JobsStateWithJobs([job]);
}
class JobsStateEmpty extends JobsState {}
@override
List<Object?> get props => [];
}
class JobsStateWithJobs extends JobsState {
JobsStateWithJobs(this.clientJobList);
final List<ClientJob> clientJobList;
bool get rebuildRequired =>
clientJobList.any((final job) => job.requiresRebuild);
JobsState removeById(final String id) {
final List<ClientJob> newJobsList =
clientJobList.where((final element) => element.id != id).toList();
@ -22,5 +37,118 @@ class JobsStateWithJobs extends JobsState {
}
@override
List<Object?> get props => clientJobList;
List<Object?> get props => [clientJobList];
@override
JobsState addJob(final ClientJob job) {
if (job is ReplaceableJob) {
final List<ClientJob> newJobsList = clientJobList
.where((final element) => element.runtimeType != job.runtimeType)
.toList();
newJobsList.add(job);
getIt<NavigationService>().showSnackBar('jobs.job_added'.tr());
return JobsStateWithJobs(newJobsList);
}
if (job.canAddTo(clientJobList)) {
final List<ClientJob> newJobsList = [...clientJobList, job];
getIt<NavigationService>().showSnackBar('jobs.job_added'.tr());
return JobsStateWithJobs(newJobsList);
}
return this;
}
}
class JobsStateLoading extends JobsState {
JobsStateLoading(this.clientJobList, this.rebuildJobUid, this.postponedJobs);
final List<ClientJob> clientJobList;
@override
final String? rebuildJobUid;
bool get rebuildRequired =>
clientJobList.any((final job) => job.requiresRebuild);
final List<ClientJob> postponedJobs;
JobsStateLoading updateJobStatus(
final String id,
final JobStatusEnum status, {
final String? message,
}) {
final List<ClientJob> newJobsList = clientJobList.map((final job) {
if (job.id == id) {
return job.copyWithNewStatus(status: status, message: message);
}
return job;
}).toList();
return JobsStateLoading(newJobsList, rebuildJobUid, postponedJobs);
}
JobsStateLoading copyWith({
final List<ClientJob>? clientJobList,
final String? rebuildJobUid,
final List<ClientJob>? postponedJobs,
}) =>
JobsStateLoading(
clientJobList ?? this.clientJobList,
rebuildJobUid ?? this.rebuildJobUid,
postponedJobs ?? this.postponedJobs,
);
JobsStateFinished finished() =>
JobsStateFinished(clientJobList, rebuildJobUid, postponedJobs);
@override
List<Object?> get props => [clientJobList, rebuildJobUid, postponedJobs];
@override
JobsState addJob(final ClientJob job) {
// Do the same, but add jobs to the postponed list
if (job is ReplaceableJob) {
final List<ClientJob> newPostponedJobs = postponedJobs
.where((final element) => element.runtimeType != job.runtimeType)
.toList();
newPostponedJobs.add(job);
getIt<NavigationService>().showSnackBar('jobs.job_postponed'.tr());
return JobsStateLoading(clientJobList, rebuildJobUid, newPostponedJobs);
}
if (job.canAddTo(postponedJobs)) {
final List<ClientJob> newPostponedJobs = [...postponedJobs, job];
getIt<NavigationService>().showSnackBar('jobs.job_postponed'.tr());
return JobsStateLoading(clientJobList, rebuildJobUid, newPostponedJobs);
}
return this;
}
}
class JobsStateFinished extends JobsState {
JobsStateFinished(this.clientJobList, this.rebuildJobUid, this.postponedJobs);
final List<ClientJob> clientJobList;
@override
final String? rebuildJobUid;
bool get rebuildRequired =>
clientJobList.any((final job) => job.requiresRebuild);
final List<ClientJob> postponedJobs;
@override
List<Object?> get props => [clientJobList, rebuildJobUid, postponedJobs];
@override
JobsState addJob(final ClientJob job) {
if (job is ReplaceableJob) {
final List<ClientJob> newPostponedJobs = postponedJobs
.where((final element) => element.runtimeType != job.runtimeType)
.toList();
newPostponedJobs.add(job);
getIt<NavigationService>().showSnackBar('jobs.job_added'.tr());
return JobsStateWithJobs(newPostponedJobs);
}
if (job.canAddTo(postponedJobs)) {
final List<ClientJob> newPostponedJobs = [...postponedJobs, job];
getIt<NavigationService>().showSnackBar('jobs.job_added'.tr());
return JobsStateWithJobs(newPostponedJobs);
}
return this;
}
}

View file

@ -40,20 +40,6 @@ class ServerDetailsRepository {
return data;
}
Future<void> setAutoUpgradeSettings(
final AutoUpgradeSettings settings,
) async {
await server.setAutoUpgradeSettings(settings);
}
Future<void> setTimezone(
final String timezone,
) async {
if (timezone.isNotEmpty) {
await server.setTimezone(timezone);
}
}
}
class ServerDetailsRepositoryDto {

View file

@ -70,45 +70,41 @@ class ApiConnectionRepository {
);
}
Future<void> createUser(final User user) async {
Future<(bool, String)> createUser(final User user) async {
final List<User>? loadedUsers = _apiData.users.data;
if (loadedUsers == null) {
return;
return (false, 'basis.network_error'.tr());
}
// If user exists on server, do nothing
if (loadedUsers
.any((final User u) => u.login == user.login && u.isFoundOnServer)) {
return;
return (false, 'users.user_already_exists'.tr());
}
final String? password = user.password;
if (password == null) {
getIt<NavigationService>()
.showSnackBar('users.could_not_create_user'.tr());
return;
return (false, 'users.could_not_create_user'.tr());
}
// If API returned error, do nothing
final GenericResult<User?> result =
await api.createUser(user.login, password);
if (result.data == null) {
getIt<NavigationService>()
.showSnackBar(result.message ?? 'users.could_not_create_user'.tr());
return;
return (false, result.message ?? 'users.could_not_create_user'.tr());
}
_apiData.users.data?.add(result.data!);
_apiData.users.invalidate();
return (true, result.message ?? 'basis.done'.tr());
}
Future<void> deleteUser(final User user) async {
Future<(bool, String)> deleteUser(final User user) async {
final List<User>? loadedUsers = _apiData.users.data;
if (loadedUsers == null) {
return;
return (false, 'basis.network_error'.tr());
}
// If user is primary or root, don't delete
if (user.type != UserType.normal) {
getIt<NavigationService>()
.showSnackBar('users.could_not_delete_user'.tr());
return;
return (false, 'users.could_not_delete_user'.tr());
}
final GenericResult result = await api.deleteUser(user.login);
if (result.success && result.data) {
@ -117,19 +113,18 @@ class ApiConnectionRepository {
}
if (!result.success || !result.data) {
getIt<NavigationService>()
.showSnackBar(result.message ?? 'jobs.generic_error'.tr());
return (false, result.message ?? 'jobs.generic_error'.tr());
}
return (true, result.message ?? 'basis.done'.tr());
}
Future<void> changeUserPassword(
Future<(bool, String)> changeUserPassword(
final User user,
final String newPassword,
) async {
if (user.type == UserType.root) {
getIt<NavigationService>()
.showSnackBar('users.could_not_change_password'.tr());
return;
return (false, 'users.could_not_change_password'.tr());
}
final GenericResult<User?> result = await api.updateUser(
user.login,
@ -139,13 +134,21 @@ class ApiConnectionRepository {
getIt<NavigationService>().showSnackBar(
result.message ?? 'users.could_not_change_password'.tr(),
);
return (
false,
result.message ?? 'users.could_not_change_password'.tr(),
);
}
return (true, result.message ?? 'basis.done'.tr());
}
Future<void> addSshKey(final User user, final String publicKey) async {
Future<(bool, String)> addSshKey(
final User user,
final String publicKey,
) async {
final List<User>? loadedUsers = _apiData.users.data;
if (loadedUsers == null) {
return;
return (false, 'basis.network_error'.tr());
}
final GenericResult<User?> result =
await api.addSshKey(user.login, publicKey);
@ -156,15 +159,19 @@ class ApiConnectionRepository {
loadedUsers[index] = updatedUser;
_apiData.users.invalidate();
} else {
getIt<NavigationService>()
.showSnackBar(result.message ?? 'users.could_not_add_ssh_key'.tr());
return (false, result.message ?? 'users.could_not_add_ssh_key'.tr());
}
return (true, result.message ?? 'basis.done'.tr());
}
Future<void> deleteSshKey(final User user, final String publicKey) async {
Future<(bool, String)> deleteSshKey(
final User user,
final String publicKey,
) async {
final List<User>? loadedUsers = _apiData.users.data;
if (loadedUsers == null) {
return;
return (false, 'basis.network_error'.tr());
}
final GenericResult<User?> result =
await api.removeSshKey(user.login, publicKey);
@ -175,9 +182,9 @@ class ApiConnectionRepository {
loadedUsers[index] = updatedUser;
_apiData.users.invalidate();
} else {
getIt<NavigationService>()
.showSnackBar(result.message ?? 'jobs.generic_error'.tr());
return (false, result.message ?? 'jobs.generic_error'.tr());
}
return (true, result.message ?? 'basis.done'.tr());
}
void dispose() {
@ -345,11 +352,8 @@ class ApiDataElement<T> {
final Function callback,
) async {
if (VersionConstraint.parse(requiredApiVersion).allows(version)) {
print('Fetching data for $runtimeType');
if (isExpired) {
print('Data is expired');
final newData = await fetchData();
print(newData);
if (T is List) {
if (Object.hashAll(newData as Iterable<Object?>) !=
Object.hashAll(_data as Iterable<Object?>)) {

View file

@ -3,7 +3,9 @@ import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart';
import 'package:selfprivacy/logic/models/hive/user.dart';
import 'package:selfprivacy/logic/models/json/server_job.dart';
import 'package:selfprivacy/logic/models/service.dart';
import 'package:selfprivacy/utils/password_generator.dart';
@ -12,18 +14,31 @@ abstract class ClientJob extends Equatable {
ClientJob({
required this.title,
final String? id,
this.requiresRebuild = true,
this.status = JobStatusEnum.created,
this.message,
}) : id = id ?? StringGenerators.simpleId();
final String title;
final String id;
final bool requiresRebuild;
final JobStatusEnum status;
final String? message;
bool canAddTo(final List<ClientJob> jobs) => true;
void execute(final JobsCubit cubit);
Future<(bool, String)> execute(final JobsCubit cubit);
@override
List<Object> get props => [id, title];
List<Object> get props => [id, title, status];
ClientJob copyWithNewStatus({
required final JobStatusEnum status,
final String? message,
});
}
@Deprecated('Jobs bloc should handle it itself')
class RebuildServerJob extends ClientJob {
RebuildServerJob({
required super.title,
@ -35,47 +50,138 @@ class RebuildServerJob extends ClientJob {
!jobs.any((final job) => job is RebuildServerJob);
@override
void execute(final JobsCubit cubit) async {
await cubit.upgradeServer();
Future<(bool, String)> execute(final JobsCubit cubit) async =>
(false, 'unimplemented');
@override
RebuildServerJob copyWithNewStatus({
required final JobStatusEnum status,
final String? message,
}) {
throw UnimplementedError();
}
}
class UpgradeServerJob extends ClientJob {
UpgradeServerJob({
super.status,
super.message,
super.id,
}) : super(title: 'jobs.start_server_upgrade'.tr());
@override
bool canAddTo(final List<ClientJob> jobs) =>
!jobs.any((final job) => job is UpgradeServerJob);
@override
Future<(bool, String)> execute(final JobsCubit cubit) async =>
(false, 'unimplemented');
@override
UpgradeServerJob copyWithNewStatus({
required final JobStatusEnum status,
final String? message,
}) =>
UpgradeServerJob(
status: status,
message: message,
id: id,
);
}
class RebootServerJob extends ClientJob {
RebootServerJob({
super.status,
super.message,
super.id,
}) : super(title: 'jobs.reboot_server'.tr(), requiresRebuild: false);
@override
bool canAddTo(final List<ClientJob> jobs) =>
!jobs.any((final job) => job is RebootServerJob);
@override
Future<(bool, String)> execute(final JobsCubit cubit) async =>
(false, 'unimplemented');
@override
RebootServerJob copyWithNewStatus({
required final JobStatusEnum status,
final String? message,
}) =>
RebootServerJob(
status: status,
message: message,
id: id,
);
}
class CreateUserJob extends ClientJob {
CreateUserJob({
required this.user,
super.status,
super.message,
super.id,
}) : super(title: '${"jobs.create_user".tr()} ${user.login}');
final User user;
@override
void execute(final JobsCubit cubit) async {
await getIt<ApiConnectionRepository>().createUser(user);
}
Future<(bool, String)> execute(final JobsCubit cubit) async =>
getIt<ApiConnectionRepository>().createUser(user);
@override
List<Object> get props => [id, title, user];
List<Object> get props => [...super.props, user];
@override
CreateUserJob copyWithNewStatus({
required final JobStatusEnum status,
final String? message,
}) =>
CreateUserJob(
user: user,
status: status,
message: message,
id: id,
);
}
class ResetUserPasswordJob extends ClientJob {
ResetUserPasswordJob({
required this.user,
super.status,
super.message,
super.id,
}) : super(title: '${"jobs.reset_user_password".tr()} ${user.login}');
final User user;
@override
void execute(final JobsCubit cubit) async {
await getIt<ApiConnectionRepository>()
.changeUserPassword(user, user.password!);
}
Future<(bool, String)> execute(final JobsCubit cubit) async =>
getIt<ApiConnectionRepository>().changeUserPassword(user, user.password!);
@override
List<Object> get props => [id, title, user];
List<Object> get props => [...super.props, user];
@override
ResetUserPasswordJob copyWithNewStatus({
required final JobStatusEnum status,
final String? message,
}) =>
ResetUserPasswordJob(
user: user,
status: status,
message: message,
id: id,
);
}
class DeleteUserJob extends ClientJob {
DeleteUserJob({
required this.user,
super.status,
super.message,
super.id,
}) : super(title: '${"jobs.delete_user".tr()} ${user.login}');
final User user;
@ -86,18 +192,32 @@ class DeleteUserJob extends ClientJob {
);
@override
void execute(final JobsCubit cubit) async {
await getIt<ApiConnectionRepository>().deleteUser(user);
}
Future<(bool, String)> execute(final JobsCubit cubit) async =>
getIt<ApiConnectionRepository>().deleteUser(user);
@override
List<Object> get props => [id, title, user];
List<Object> get props => [...super.props, user];
@override
DeleteUserJob copyWithNewStatus({
required final JobStatusEnum status,
final String? message,
}) =>
DeleteUserJob(
user: user,
status: status,
message: message,
id: id,
);
}
class ServiceToggleJob extends ClientJob {
ServiceToggleJob({
required this.service,
required this.needToTurnOn,
super.status,
super.message,
super.id,
}) : super(
title:
'${needToTurnOn ? "jobs.service_turn_on".tr() : "jobs.service_turn_off".tr()} ${service.displayName}',
@ -112,36 +232,68 @@ class ServiceToggleJob extends ClientJob {
);
@override
void execute(final JobsCubit cubit) async {
Future<(bool, String)> execute(final JobsCubit cubit) async {
await cubit.api.switchService(service.id, needToTurnOn);
return (true, 'Check not implemented');
}
@override
List<Object> get props => [...super.props, service];
@override
ServiceToggleJob copyWithNewStatus({
required final JobStatusEnum status,
final String? message,
}) =>
ServiceToggleJob(
service: service,
needToTurnOn: needToTurnOn,
status: status,
message: message,
id: id,
);
}
class CreateSSHKeyJob extends ClientJob {
CreateSSHKeyJob({
required this.user,
required this.publicKey,
super.status,
super.message,
super.id,
}) : super(title: 'jobs.create_ssh_key'.tr(args: [user.login]));
final User user;
final String publicKey;
@override
void execute(final JobsCubit cubit) async {
await getIt<ApiConnectionRepository>().addSshKey(user, publicKey);
}
Future<(bool, String)> execute(final JobsCubit cubit) async =>
getIt<ApiConnectionRepository>().addSshKey(user, publicKey);
@override
List<Object> get props => [id, title, user, publicKey];
List<Object> get props => [...super.props, user, publicKey];
@override
CreateSSHKeyJob copyWithNewStatus({
required final JobStatusEnum status,
final String? message,
}) =>
CreateSSHKeyJob(
user: user,
publicKey: publicKey,
status: status,
message: message,
id: id,
);
}
class DeleteSSHKeyJob extends ClientJob {
DeleteSSHKeyJob({
required this.user,
required this.publicKey,
super.status,
super.message,
super.id,
}) : super(title: 'jobs.delete_ssh_key'.tr(args: [user.login]));
final User user;
@ -156,10 +308,114 @@ class DeleteSSHKeyJob extends ClientJob {
);
@override
void execute(final JobsCubit cubit) async {
await getIt<ApiConnectionRepository>().deleteSshKey(user, publicKey);
Future<(bool, String)> execute(final JobsCubit cubit) async =>
getIt<ApiConnectionRepository>().deleteSshKey(user, publicKey);
@override
List<Object> get props => [...super.props, user, publicKey];
@override
DeleteSSHKeyJob copyWithNewStatus({
required final JobStatusEnum status,
final String? message,
}) =>
DeleteSSHKeyJob(
user: user,
publicKey: publicKey,
status: status,
message: message,
id: id,
);
}
abstract class ReplaceableJob extends ClientJob {
ReplaceableJob({
required super.title,
super.id,
super.status,
super.message,
});
bool shouldRemoveInsteadOfAdd(final List<ClientJob> jobs) => false;
}
class ChangeAutoUpgradeSettingsJob extends ReplaceableJob {
ChangeAutoUpgradeSettingsJob({
required this.enable,
required this.allowReboot,
super.status,
super.message,
super.id,
}) : super(title: 'jobs.change_auto_upgrade_settings'.tr());
final bool enable;
final bool allowReboot;
@override
Future<(bool, String)> execute(final JobsCubit cubit) async {
await cubit.api.setAutoUpgradeSettings(
AutoUpgradeSettings(enable: enable, allowReboot: allowReboot),
);
return (true, 'Check not implemented');
}
@override
List<Object> get props => [id, title, user, publicKey];
bool shouldRemoveInsteadOfAdd(final List<ClientJob> jobs) {
// TODO: Finish this
throw UnimplementedError();
}
@override
List<Object> get props => [...super.props, enable, allowReboot];
@override
ChangeAutoUpgradeSettingsJob copyWithNewStatus({
required final JobStatusEnum status,
final String? message,
}) =>
ChangeAutoUpgradeSettingsJob(
enable: enable,
allowReboot: allowReboot,
status: status,
message: message,
id: id,
);
}
class ChangeServerTimezoneJob extends ReplaceableJob {
ChangeServerTimezoneJob({
required this.timezone,
super.status,
super.message,
super.id,
}) : super(title: 'jobs.change_server_timezone'.tr());
final String timezone;
@override
Future<(bool, String)> execute(final JobsCubit cubit) async {
await getIt<ApiConnectionRepository>().api.setTimezone(timezone);
return (true, 'Check not implemented');
}
@override
bool shouldRemoveInsteadOfAdd(final List<ClientJob> jobs) {
// TODO: Finish this
throw UnimplementedError();
}
@override
List<Object> get props => [...super.props, timezone];
@override
ChangeServerTimezoneJob copyWithNewStatus({
required final JobStatusEnum status,
final String? message,
}) =>
ChangeServerTimezoneJob(
timezone: timezone,
status: status,
message: message,
id: id,
);
}

View file

@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -6,7 +7,6 @@ import 'package:selfprivacy/logic/bloc/server_jobs/server_jobs_bloc.dart';
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/models/json/server_job.dart';
import 'package:selfprivacy/ui/components/brand_loader/brand_loader.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/components/jobs_content/server_job_card.dart';
import 'package:selfprivacy/ui/helpers/modals.dart';
@ -19,6 +19,32 @@ class JobsContent extends StatelessWidget {
final ScrollController controller;
IconData _getIcon(final JobStatusEnum status) {
switch (status) {
case JobStatusEnum.created:
return Icons.query_builder_outlined;
case JobStatusEnum.running:
return Icons.pending_outlined;
case JobStatusEnum.finished:
return Icons.check_circle_outline;
case JobStatusEnum.error:
return Icons.error_outline;
}
}
Color _getColor(final JobStatusEnum status, final BuildContext context) {
switch (status) {
case JobStatusEnum.created:
return Theme.of(context).colorScheme.secondary;
case JobStatusEnum.running:
return Theme.of(context).colorScheme.tertiary;
case JobStatusEnum.finished:
return Theme.of(context).colorScheme.primary;
case JobStatusEnum.error:
return Theme.of(context).colorScheme.error;
}
}
@override
Widget build(final BuildContext context) {
final List<ServerJob> serverJobs =
@ -68,8 +94,274 @@ class JobsContent extends StatelessWidget {
}
} else if (state is JobsStateLoading) {
widgets = [
const SizedBox(height: 80),
BrandLoader.horizontal(),
...state.clientJobList.map(
(final j) => Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
_getIcon(j.status),
color: _getColor(j.status, context),
),
),
Expanded(
child: Card(
color: Theme.of(context).colorScheme.surfaceVariant,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 10,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
j.title,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
if (j.message != null)
Text(
j.message!,
style: Theme.of(context).textTheme.labelSmall,
),
],
),
),
),
),
],
),
),
if (state.rebuildRequired)
Builder(
builder: (final context) {
final rebuildJob = serverJobs.firstWhereOrNull(
(final job) => job.uid == state.rebuildJobUid,
);
return Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
_getIcon(rebuildJob?.status ?? JobStatusEnum.created),
color: _getColor(
rebuildJob?.status ?? JobStatusEnum.created,
context,
),
),
),
Expanded(
child: Card(
color: Theme.of(context).colorScheme.surfaceVariant,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 10,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
rebuildJob?.name ??
'jobs.rebuild_system'.tr(),
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
if (rebuildJob?.description != null)
Text(
rebuildJob!.description,
style:
Theme.of(context).textTheme.labelSmall,
),
const SizedBox(height: 8),
LinearProgressIndicator(
value: rebuildJob?.progress == null
? 0.0
: ((rebuildJob!.progress ?? 0) < 1)
? null
: rebuildJob.progress! / 100.0,
color: _getColor(
rebuildJob?.status ?? JobStatusEnum.created,
context,
),
backgroundColor: Theme.of(context)
.colorScheme
.surfaceVariant,
minHeight: 7.0,
borderRadius: BorderRadius.circular(7.0),
),
const SizedBox(height: 8),
if (rebuildJob?.error != null ||
rebuildJob?.result != null ||
rebuildJob?.statusText != null)
Text(
rebuildJob?.error ??
rebuildJob?.result ??
rebuildJob?.statusText ??
'',
style:
Theme.of(context).textTheme.labelSmall,
),
],
),
),
),
),
],
);
},
),
];
} else if (state is JobsStateFinished) {
widgets = [
...state.clientJobList.map(
(final j) => Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
_getIcon(j.status),
color: _getColor(j.status, context),
),
),
Expanded(
child: Card(
color: Theme.of(context).colorScheme.surfaceVariant,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 10,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
j.title,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
if (j.message != null)
Text(
j.message!,
style: Theme.of(context).textTheme.labelSmall,
),
],
),
),
),
),
],
),
),
if (state.rebuildRequired)
Builder(
builder: (final context) {
final rebuildJob = serverJobs.firstWhereOrNull(
(final job) => job.uid == state.rebuildJobUid,
);
if (rebuildJob == null) {
return const SizedBox();
}
return Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
_getIcon(rebuildJob.status),
color: _getColor(
rebuildJob.status,
context,
),
),
),
Expanded(
child: Card(
color: Theme.of(context).colorScheme.surfaceVariant,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 10,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
rebuildJob.name,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
Text(
rebuildJob.description,
style: Theme.of(context).textTheme.labelSmall,
),
const SizedBox(height: 8),
LinearProgressIndicator(
value: rebuildJob.progress == null
? 0.0
: ((rebuildJob.progress ?? 0) < 1)
? null
: rebuildJob.progress! / 100.0,
color: _getColor(
rebuildJob.status,
context,
),
backgroundColor: Theme.of(context)
.colorScheme
.surfaceVariant,
minHeight: 7.0,
borderRadius: BorderRadius.circular(7.0),
),
const SizedBox(height: 8),
if (rebuildJob.error != null ||
rebuildJob.result != null ||
rebuildJob.statusText != null)
Text(
rebuildJob.error ??
rebuildJob.result ??
rebuildJob.statusText ??
'',
style:
Theme.of(context).textTheme.labelSmall,
),
],
),
),
),
),
],
);
},
),
const SizedBox(height: 16),
BrandButton.rised(
onPressed: () => context.read<JobsCubit>().acknowledgeFinished(),
text: 'basis.done'.tr(),
),
];
} else if (state is JobsStateWithJobs) {
widgets = [
@ -84,19 +376,31 @@ class JobsContent extends StatelessWidget {
horizontal: 15,
vertical: 10,
),
child: Text(
j.title,
style:
Theme.of(context).textTheme.labelLarge?.copyWith(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
j.title,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
if (j.message != null)
Text(
j.message!,
style: Theme.of(context).textTheme.labelSmall,
),
],
),
),
),
),
const SizedBox(width: 10),
const SizedBox(width: 8),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
@ -116,7 +420,7 @@ class JobsContent extends StatelessWidget {
],
),
),
const SizedBox(height: 20),
const SizedBox(height: 16),
BrandButton.rised(
onPressed: () => context.read<JobsCubit>().applyAll(),
text: 'jobs.start'.tr(),
@ -161,23 +465,25 @@ class JobsContent extends StatelessWidget {
],
),
),
...serverJobs.map(
(final job) => Dismissible(
key: ValueKey(job.uid),
direction: job.status == JobStatusEnum.finished ||
job.status == JobStatusEnum.error
? DismissDirection.horizontal
: DismissDirection.none,
child: ServerJobCard(
serverJob: job,
...serverJobs
.whereNot((final job) => job.uid == state.rebuildJobUid)
.map(
(final job) => Dismissible(
key: ValueKey(job.uid),
direction: job.status == JobStatusEnum.finished ||
job.status == JobStatusEnum.error
? DismissDirection.horizontal
: DismissDirection.none,
child: ServerJobCard(
serverJob: job,
),
onDismissed: (final direction) {
context.read<ServerJobsBloc>().add(
RemoveServerJob(job.uid),
);
},
),
),
onDismissed: (final direction) {
context.read<ServerJobsBloc>().add(
RemoveServerJob(job.uid),
);
},
),
),
const SizedBox(height: 24),
],
);

View file

@ -8,7 +8,6 @@ import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/metrics/metrics_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart';
import 'package:selfprivacy/logic/models/job.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/components/brand_loader/brand_loader.dart';

View file

@ -30,24 +30,32 @@ class _ServerSettingsState extends State<_ServerSettings> {
value: allowAutoUpgrade ?? false,
onChanged: (final switched) {
context.read<JobsCubit>().addJob(
RebuildServerJob(title: 'jobs.upgrade_server'.tr()),
);
context
.read<ServerDetailsCubit>()
.repository
.setAutoUpgradeSettings(
AutoUpgradeSettings(
enable: switched,
ChangeAutoUpgradeSettingsJob(
allowReboot: rebootAfterUpgrade ?? false,
enable: switched,
),
);
setState(() {
allowAutoUpgrade = switched;
});
},
title: Text('server.allow_autoupgrade'.tr()),
title: Text(
'server.allow_autoupgrade'.tr(),
style: TextStyle(
fontStyle: allowAutoUpgrade !=
serverDetailsState.autoUpgradeSettings.enable
? FontStyle.italic
: FontStyle.normal,
),
),
subtitle: Text(
'server.allow_autoupgrade_hint'.tr(),
style: TextStyle(
fontStyle: allowAutoUpgrade !=
serverDetailsState.autoUpgradeSettings.enable
? FontStyle.italic
: FontStyle.normal,
),
),
activeColor: Theme.of(context).colorScheme.primary,
),
@ -55,24 +63,32 @@ class _ServerSettingsState extends State<_ServerSettings> {
value: rebootAfterUpgrade ?? false,
onChanged: (final switched) {
context.read<JobsCubit>().addJob(
RebuildServerJob(title: 'jobs.upgrade_server'.tr()),
);
context
.read<ServerDetailsCubit>()
.repository
.setAutoUpgradeSettings(
AutoUpgradeSettings(
enable: allowAutoUpgrade ?? false,
ChangeAutoUpgradeSettingsJob(
allowReboot: switched,
enable: allowAutoUpgrade ?? false,
),
);
setState(() {
rebootAfterUpgrade = switched;
});
},
title: Text('server.reboot_after_upgrade'.tr()),
title: Text(
'server.reboot_after_upgrade'.tr(),
style: TextStyle(
fontStyle: rebootAfterUpgrade !=
serverDetailsState.autoUpgradeSettings.allowReboot
? FontStyle.italic
: FontStyle.normal,
),
),
subtitle: Text(
'server.reboot_after_upgrade_hint'.tr(),
style: TextStyle(
fontStyle: rebootAfterUpgrade !=
serverDetailsState.autoUpgradeSettings.allowReboot
? FontStyle.italic
: FontStyle.normal,
),
),
activeColor: Theme.of(context).colorScheme.primary,
),
@ -82,9 +98,6 @@ class _ServerSettingsState extends State<_ServerSettings> {
serverDetailsState.serverTimezone.toString(),
),
onTap: () {
context.read<JobsCubit>().addJob(
RebuildServerJob(title: 'jobs.upgrade_server'.tr()),
);
Navigator.of(context).push(
materialRoute(
const SelectTimezone(),

View file

@ -140,8 +140,10 @@ class _SelectTimezoneState extends State<SelectTimezone> {
'GMT ${duration.toTimezoneOffsetFormat()} ${area.isNotEmpty ? '($area)' : ''}',
),
onTap: () {
context.read<ServerDetailsCubit>().repository.setTimezone(
location.name,
context.read<JobsCubit>().addJob(
ChangeServerTimezoneJob(
timezone: location.name,
),
);
Navigator.of(context).pop();
},

View file

@ -202,7 +202,7 @@ packages:
source: hosted
version: "4.8.0"
collection:
dependency: transitive
dependency: "direct main"
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a

View file

@ -13,6 +13,7 @@ dependencies:
auto_size_text: ^3.0.0
bloc_concurrency: ^0.2.3
crypt: ^4.3.1
collection: ^1.18.0
cubit_form: ^2.0.1
device_info_plus: ^9.1.1
dio: ^5.4.0