diff --git a/assets/translations/ru.json b/assets/translations/ru.json index a8d3f0d5..6ceb70a4 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -482,7 +482,8 @@ "upgradeServer": "Обновить сервер", "rebootServer": "Перезагрузить сервер", "create_ssh_key": "Создать SSH ключ для {}", - "delete_ssh_key": "Удалить SSH ключ для {}" + "delete_ssh_key": "Удалить SSH ключ для {}", + "server_jobs": "Задачи на сервере" }, "validations": { "required": "Обязательное поле.", diff --git a/lib/logic/api_maps/graphql_maps/server_api/jobs_api.dart b/lib/logic/api_maps/graphql_maps/server_api/jobs_api.dart index 28f362a9..8d731e3e 100644 --- a/lib/logic/api_maps/graphql_maps/server_api/jobs_api.dart +++ b/lib/logic/api_maps/graphql_maps/server_api/jobs_api.dart @@ -22,14 +22,24 @@ mixin JobsApi on ApiMap { return jobsList; } - Future removeApiJob(final String uid) async { + Future removeApiJob(final String uid) async { try { final GraphQLClient client = await getClient(); final variables = Variables$Mutation$RemoveJob(jobId: uid); final mutation = Options$Mutation$RemoveJob(variables: variables); - await client.mutate$RemoveJob(mutation); + final response = await client.mutate$RemoveJob(mutation); + return GenericMutationResult( + success: response.parsedData?.removeJob.success ?? false, + code: response.parsedData?.removeJob.code ?? 0, + message: response.parsedData?.removeJob.message, + ); } catch (e) { print(e); + return GenericMutationResult( + success: false, + code: 0, + message: e.toString(), + ); } } } diff --git a/lib/logic/cubit/server_jobs/server_jobs_cubit.dart b/lib/logic/cubit/server_jobs/server_jobs_cubit.dart index 46f04e5b..43b2b2d5 100644 --- a/lib/logic/cubit/server_jobs/server_jobs_cubit.dart +++ b/lib/logic/cubit/server_jobs/server_jobs_cubit.dart @@ -14,7 +14,7 @@ class ServerJobsCubit ServerJobsCubit(final ServerInstallationCubit serverInstallationCubit) : super( serverInstallationCubit, - const ServerJobsState(), + ServerJobsState(), ); Timer? timer; @@ -23,7 +23,7 @@ class ServerJobsCubit @override void clear() async { emit( - const ServerJobsState(), + ServerJobsState(), ); if (timer != null && timer!.isActive) { timer!.cancel(); @@ -40,7 +40,7 @@ class ServerJobsCubit serverJobList: jobs, ), ); - timer = Timer(const Duration(seconds: 10), () => reload(useTimer: true)); + timer = Timer(const Duration(seconds: 5), () => reload(useTimer: true)); } } @@ -72,6 +72,44 @@ class ServerJobsCubit return job; } + /// Get the job object and change its isHidden to true. + /// Emit the new state. + /// Call the api to remove the job. + /// If the api call fails, change the isHidden to false and emit the new state. + /// If the api call succeeds, remove the job from the list and emit the new state. + Future removeServerJob(final String uid) async { + final ServerJob? job = getServerJobByUid(uid); + if (job == null) { + return; + } + + job.isHidden = true; + emit( + ServerJobsState( + serverJobList: state.serverJobList, + ), + ); + + final result = await api.removeApiJob(uid); + if (!result.success) { + job.isHidden = false; + emit( + ServerJobsState( + serverJobList: state.serverJobList, + ), + ); + getIt().showSnackBar(result.message!); + return; + } + + state.serverJobList.remove(job); + emit( + ServerJobsState( + serverJobList: state.serverJobList, + ), + ); + } + Future reload({final bool useTimer = false}) async { final List jobs = await api.getServerJobs(); emit( @@ -80,7 +118,7 @@ class ServerJobsCubit ), ); if (useTimer) { - timer = Timer(const Duration(seconds: 10), () => reload(useTimer: true)); + timer = Timer(const Duration(seconds: 5), () => reload(useTimer: true)); } } } diff --git a/lib/logic/cubit/server_jobs/server_jobs_state.dart b/lib/logic/cubit/server_jobs/server_jobs_state.dart index bf7d0adb..25d8deab 100644 --- a/lib/logic/cubit/server_jobs/server_jobs_state.dart +++ b/lib/logic/cubit/server_jobs/server_jobs_state.dart @@ -1,22 +1,28 @@ part of 'server_jobs_cubit.dart'; class ServerJobsState extends ServerInstallationDependendState { - const ServerJobsState({ - this.serverJobList = const [], + ServerJobsState({ + final serverJobList = const [], this.migrationJobUid, - }); - final List serverJobList; + }) { + _serverJobList = serverJobList; + } + + late final List _serverJobList; final String? migrationJobUid; + List get serverJobList => + _serverJobList.where((final ServerJob job) => !job.isHidden).toList(); + @override - List get props => [migrationJobUid, ...serverJobList]; + List get props => [migrationJobUid, ..._serverJobList]; ServerJobsState copyWith({ final List? serverJobList, final String? migrationJobUid, }) => ServerJobsState( - serverJobList: serverJobList ?? this.serverJobList, + serverJobList: serverJobList ?? _serverJobList, migrationJobUid: migrationJobUid ?? this.migrationJobUid, ); } diff --git a/lib/logic/models/json/server_job.dart b/lib/logic/models/json/server_job.dart index 156ebc25..67bbfca4 100644 --- a/lib/logic/models/json/server_job.dart +++ b/lib/logic/models/json/server_job.dart @@ -47,6 +47,7 @@ class ServerJob { final String? result; final String? statusText; final DateTime? finishedAt; + bool isHidden = false; } enum JobStatusEnum { diff --git a/lib/ui/components/jobs_content/jobs_content.dart b/lib/ui/components/jobs_content/jobs_content.dart index f7063230..58fffd74 100644 --- a/lib/ui/components/jobs_content/jobs_content.dart +++ b/lib/ui/components/jobs_content/jobs_content.dart @@ -6,6 +6,7 @@ import 'package:selfprivacy/config/get_it_config.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/cubit/server_jobs/server_jobs_cubit.dart'; +import 'package:selfprivacy/logic/models/json/server_job.dart'; import 'package:selfprivacy/ui/components/action_button/action_button.dart'; import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; @@ -18,118 +19,139 @@ class JobsContent extends StatelessWidget { const JobsContent({final super.key}); @override - Widget build(final BuildContext context) => BlocBuilder( - builder: (final context, final state) { - late List widgets; - final ServerInstallationState installationState = - context.read().state; - if (state is JobsStateEmpty) { - widgets = [ - const SizedBox(height: 80), - Center(child: BrandText.body1('jobs.empty'.tr())), - ]; + Widget build(final BuildContext context) { + final List serverJobs = + context.watch().state.serverJobList; - if (installationState is ServerInstallationFinished) { - widgets = [ - ...widgets, - const SizedBox(height: 80), - BrandButton.rised( - onPressed: () => context.read().upgradeServer(), - text: 'jobs.upgradeServer'.tr(), - ), - const SizedBox(height: 10), - BrandButton.text( - onPressed: () { - final NavigationService nav = getIt(); - nav.showPopUpDialog( - BrandAlert( - title: 'jobs.rebootServer'.tr(), - contentText: 'modals.3'.tr(), - actions: [ - ActionButton( - text: 'basis.cancel'.tr(), - ), - ActionButton( - onPressed: () => - {context.read().rebootServer()}, - text: 'modals.9'.tr(), - ) - ], - ), - ); - }, - title: 'jobs.rebootServer'.tr(), - ), - ]; - } - } else if (state is JobsStateLoading) { + return BlocBuilder( + builder: (final context, final state) { + late List widgets; + final ServerInstallationState installationState = + context.read().state; + if (state is JobsStateEmpty) { + widgets = [ + const SizedBox(height: 80), + Center(child: BrandText.body1('jobs.empty'.tr())), + ]; + + if (installationState is ServerInstallationFinished) { widgets = [ + ...widgets, const SizedBox(height: 80), - BrandLoader.horizontal(), - ]; - } else if (state is JobsStateWithJobs) { - widgets = [ - ...state.clientJobList - .map( - (final j) => Row( - children: [ - Expanded( - child: BrandCards.small( - child: Text(j.title), - ), + BrandButton.rised( + onPressed: () => context.read().upgradeServer(), + text: 'jobs.upgradeServer'.tr(), + ), + const SizedBox(height: 10), + BrandButton.text( + onPressed: () { + final NavigationService nav = getIt(); + nav.showPopUpDialog( + BrandAlert( + title: 'jobs.rebootServer'.tr(), + contentText: 'modals.3'.tr(), + actions: [ + ActionButton( + text: 'basis.cancel'.tr(), ), - const SizedBox(width: 10), - ElevatedButton( - style: ElevatedButton.styleFrom( - primary: - Theme.of(context).colorScheme.errorContainer, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), + ActionButton( onPressed: () => - context.read().removeJob(j.id), - child: Text( - 'basis.remove'.tr(), - style: TextStyle( - color: Theme.of(context) - .colorScheme - .onErrorContainer, - ), - ), - ), + {context.read().rebootServer()}, + text: 'modals.9'.tr(), + ) ], ), - ) - .toList(), - const SizedBox(height: 20), - BrandButton.rised( - onPressed: () => context.read().applyAll(), - text: 'jobs.start'.tr(), + ); + }, + title: 'jobs.rebootServer'.tr(), ), ]; } - return ListView( - padding: paddingH15V0, - children: [ - const SizedBox(height: 15), - Center( - child: BrandText.h2( - 'jobs.title'.tr(), - ), - ), - const SizedBox(height: 20), - ...widgets, - const SizedBox(height: 8), - const Divider(), - const SizedBox(height: 8), - ...context.read().state.serverJobList.map( - (final job) => ServerJobCard( - serverJob: job, - ), + } else if (state is JobsStateLoading) { + widgets = [ + const SizedBox(height: 80), + BrandLoader.horizontal(), + ]; + } else if (state is JobsStateWithJobs) { + widgets = [ + ...state.clientJobList + .map( + (final j) => Row( + children: [ + Expanded( + child: BrandCards.small( + child: Text(j.title), + ), + ), + const SizedBox(width: 10), + ElevatedButton( + style: ElevatedButton.styleFrom( + primary: Theme.of(context).colorScheme.errorContainer, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + onPressed: () => + context.read().removeJob(j.id), + child: Text( + 'basis.remove'.tr(), + style: TextStyle( + color: + Theme.of(context).colorScheme.onErrorContainer, + ), + ), + ), + ], ), - ], - ); - }, - ); + ) + .toList(), + const SizedBox(height: 20), + BrandButton.rised( + onPressed: () => context.read().applyAll(), + text: 'jobs.start'.tr(), + ), + ]; + } + return ListView( + padding: paddingH15V0, + children: [ + const SizedBox(height: 15), + Center( + child: BrandText.h2( + 'jobs.title'.tr(), + ), + ), + const SizedBox(height: 20), + ...widgets, + const SizedBox(height: 8), + const Divider(height: 0), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'jobs.server_jobs'.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + ), + ...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, + ), + onDismissed: (final direction) { + context.read().removeServerJob(job.uid); + }, + ), + ), + const SizedBox(height: 24), + ], + ); + }, + ); + } }