Allow removing completed and failed server jobs

Inex Code 2022-09-18 23:12:09 +03:00
parent 19aab4b57f
commit e330878e6d
6 changed files with 194 additions and 116 deletions

View File

@ -482,7 +482,8 @@
"upgradeServer": "Обновить сервер", "upgradeServer": "Обновить сервер",
"rebootServer": "Перезагрузить сервер", "rebootServer": "Перезагрузить сервер",
"create_ssh_key": "Создать SSH ключ для {}", "create_ssh_key": "Создать SSH ключ для {}",
"delete_ssh_key": "Удалить SSH ключ для {}" "delete_ssh_key": "Удалить SSH ключ для {}",
"server_jobs": "Задачи на сервере"
}, },
"validations": { "validations": {
"required": "Обязательное поле.", "required": "Обязательное поле.",

View File

@ -22,14 +22,24 @@ mixin JobsApi on ApiMap {
return jobsList; return jobsList;
} }
Future<void> removeApiJob(final String uid) async { Future<GenericMutationResult> removeApiJob(final String uid) async {
try { try {
final GraphQLClient client = await getClient(); final GraphQLClient client = await getClient();
final variables = Variables$Mutation$RemoveJob(jobId: uid); final variables = Variables$Mutation$RemoveJob(jobId: uid);
final mutation = Options$Mutation$RemoveJob(variables: variables); 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) { } catch (e) {
print(e); print(e);
return GenericMutationResult(
success: false,
code: 0,
message: e.toString(),
);
} }
} }
} }

View File

@ -14,7 +14,7 @@ class ServerJobsCubit
ServerJobsCubit(final ServerInstallationCubit serverInstallationCubit) ServerJobsCubit(final ServerInstallationCubit serverInstallationCubit)
: super( : super(
serverInstallationCubit, serverInstallationCubit,
const ServerJobsState(), ServerJobsState(),
); );
Timer? timer; Timer? timer;
@ -23,7 +23,7 @@ class ServerJobsCubit
@override @override
void clear() async { void clear() async {
emit( emit(
const ServerJobsState(), ServerJobsState(),
); );
if (timer != null && timer!.isActive) { if (timer != null && timer!.isActive) {
timer!.cancel(); timer!.cancel();
@ -40,7 +40,7 @@ class ServerJobsCubit
serverJobList: jobs, 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; 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<void> 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<NavigationService>().showSnackBar(result.message!);
return;
}
state.serverJobList.remove(job);
emit(
ServerJobsState(
serverJobList: state.serverJobList,
),
);
}
Future<void> reload({final bool useTimer = false}) async { Future<void> reload({final bool useTimer = false}) async {
final List<ServerJob> jobs = await api.getServerJobs(); final List<ServerJob> jobs = await api.getServerJobs();
emit( emit(
@ -80,7 +118,7 @@ class ServerJobsCubit
), ),
); );
if (useTimer) { if (useTimer) {
timer = Timer(const Duration(seconds: 10), () => reload(useTimer: true)); timer = Timer(const Duration(seconds: 5), () => reload(useTimer: true));
} }
} }
} }

View File

@ -1,22 +1,28 @@
part of 'server_jobs_cubit.dart'; part of 'server_jobs_cubit.dart';
class ServerJobsState extends ServerInstallationDependendState { class ServerJobsState extends ServerInstallationDependendState {
const ServerJobsState({ ServerJobsState({
this.serverJobList = const [], final serverJobList = const <ServerJob>[],
this.migrationJobUid, this.migrationJobUid,
}); }) {
final List<ServerJob> serverJobList; _serverJobList = serverJobList;
}
late final List<ServerJob> _serverJobList;
final String? migrationJobUid; final String? migrationJobUid;
List<ServerJob> get serverJobList =>
_serverJobList.where((final ServerJob job) => !job.isHidden).toList();
@override @override
List<Object?> get props => [migrationJobUid, ...serverJobList]; List<Object?> get props => [migrationJobUid, ..._serverJobList];
ServerJobsState copyWith({ ServerJobsState copyWith({
final List<ServerJob>? serverJobList, final List<ServerJob>? serverJobList,
final String? migrationJobUid, final String? migrationJobUid,
}) => }) =>
ServerJobsState( ServerJobsState(
serverJobList: serverJobList ?? this.serverJobList, serverJobList: serverJobList ?? _serverJobList,
migrationJobUid: migrationJobUid ?? this.migrationJobUid, migrationJobUid: migrationJobUid ?? this.migrationJobUid,
); );
} }

View File

@ -47,6 +47,7 @@ class ServerJob {
final String? result; final String? result;
final String? statusText; final String? statusText;
final DateTime? finishedAt; final DateTime? finishedAt;
bool isHidden = false;
} }
enum JobStatusEnum { enum JobStatusEnum {

View File

@ -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/client_jobs/client_jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_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/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/action_button/action_button.dart';
import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart';
import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; import 'package:selfprivacy/ui/components/brand_button/brand_button.dart';
@ -18,118 +19,139 @@ class JobsContent extends StatelessWidget {
const JobsContent({final super.key}); const JobsContent({final super.key});
@override @override
Widget build(final BuildContext context) => BlocBuilder<JobsCubit, JobsState>( Widget build(final BuildContext context) {
builder: (final context, final state) { final List<ServerJob> serverJobs =
late List<Widget> widgets; context.watch<ServerJobsCubit>().state.serverJobList;
final ServerInstallationState installationState =
context.read<ServerInstallationCubit>().state;
if (state is JobsStateEmpty) {
widgets = [
const SizedBox(height: 80),
Center(child: BrandText.body1('jobs.empty'.tr())),
];
if (installationState is ServerInstallationFinished) { return BlocBuilder<JobsCubit, JobsState>(
widgets = [ builder: (final context, final state) {
...widgets, late List<Widget> widgets;
const SizedBox(height: 80), final ServerInstallationState installationState =
BrandButton.rised( context.read<ServerInstallationCubit>().state;
onPressed: () => context.read<JobsCubit>().upgradeServer(), if (state is JobsStateEmpty) {
text: 'jobs.upgradeServer'.tr(), widgets = [
), const SizedBox(height: 80),
const SizedBox(height: 10), Center(child: BrandText.body1('jobs.empty'.tr())),
BrandButton.text( ];
onPressed: () {
final NavigationService nav = getIt<NavigationService>(); if (installationState is ServerInstallationFinished) {
nav.showPopUpDialog(
BrandAlert(
title: 'jobs.rebootServer'.tr(),
contentText: 'modals.3'.tr(),
actions: [
ActionButton(
text: 'basis.cancel'.tr(),
),
ActionButton(
onPressed: () =>
{context.read<JobsCubit>().rebootServer()},
text: 'modals.9'.tr(),
)
],
),
);
},
title: 'jobs.rebootServer'.tr(),
),
];
}
} else if (state is JobsStateLoading) {
widgets = [ widgets = [
...widgets,
const SizedBox(height: 80), const SizedBox(height: 80),
BrandLoader.horizontal(), BrandButton.rised(
]; onPressed: () => context.read<JobsCubit>().upgradeServer(),
} else if (state is JobsStateWithJobs) { text: 'jobs.upgradeServer'.tr(),
widgets = [ ),
...state.clientJobList const SizedBox(height: 10),
.map( BrandButton.text(
(final j) => Row( onPressed: () {
children: [ final NavigationService nav = getIt<NavigationService>();
Expanded( nav.showPopUpDialog(
child: BrandCards.small( BrandAlert(
child: Text(j.title), title: 'jobs.rebootServer'.tr(),
), contentText: 'modals.3'.tr(),
actions: [
ActionButton(
text: 'basis.cancel'.tr(),
), ),
const SizedBox(width: 10), ActionButton(
ElevatedButton(
style: ElevatedButton.styleFrom(
primary:
Theme.of(context).colorScheme.errorContainer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
onPressed: () => onPressed: () =>
context.read<JobsCubit>().removeJob(j.id), {context.read<JobsCubit>().rebootServer()},
child: Text( text: 'modals.9'.tr(),
'basis.remove'.tr(), )
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onErrorContainer,
),
),
),
], ],
), ),
) );
.toList(), },
const SizedBox(height: 20), title: 'jobs.rebootServer'.tr(),
BrandButton.rised(
onPressed: () => context.read<JobsCubit>().applyAll(),
text: 'jobs.start'.tr(),
), ),
]; ];
} }
return ListView( } else if (state is JobsStateLoading) {
padding: paddingH15V0, widgets = [
children: [ const SizedBox(height: 80),
const SizedBox(height: 15), BrandLoader.horizontal(),
Center( ];
child: BrandText.h2( } else if (state is JobsStateWithJobs) {
'jobs.title'.tr(), widgets = [
), ...state.clientJobList
), .map(
const SizedBox(height: 20), (final j) => Row(
...widgets, children: [
const SizedBox(height: 8), Expanded(
const Divider(), child: BrandCards.small(
const SizedBox(height: 8), child: Text(j.title),
...context.read<ServerJobsCubit>().state.serverJobList.map( ),
(final job) => ServerJobCard( ),
serverJob: job, const SizedBox(width: 10),
), ElevatedButton(
style: ElevatedButton.styleFrom(
primary: Theme.of(context).colorScheme.errorContainer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
onPressed: () =>
context.read<JobsCubit>().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<JobsCubit>().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<ServerJobsCubit>().removeServerJob(job.uid);
},
),
),
const SizedBox(height: 24),
],
);
},
);
}
} }