Add app source and tests
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
10
lib/Peach/see/server/detail/ConnectServerDetail.dart
Normal file
10
lib/Peach/see/server/detail/ConnectServerDetail.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
import '../../../about/internal/application/DeploymentRecord.dart';
|
||||
import '../../../about/internal/application/ServerProfile.dart';
|
||||
|
||||
abstract class ConnectServerDetail {
|
||||
/// The stored copy of this server, re-read after any change.
|
||||
void onServerLoaded(ServerProfile server, List<DeploymentRecord> history);
|
||||
|
||||
/// The server (and its stored password) was removed.
|
||||
void onServerDeleted();
|
||||
}
|
||||
20
lib/Peach/see/server/detail/ServerDetail.dart
Normal file
20
lib/Peach/see/server/detail/ServerDetail.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import '../../../about/internal/application/DeploymentRecord.dart';
|
||||
import '../../../about/internal/application/ServerProfile.dart';
|
||||
import 'ServerDetailState.dart';
|
||||
|
||||
/// A single server: its connection, its tools, and the services on it.
|
||||
class ServerDetail extends StatefulWidget {
|
||||
final ServerProfile server;
|
||||
final List<DeploymentRecord> history;
|
||||
|
||||
const ServerDetail({
|
||||
super.key,
|
||||
required this.server,
|
||||
this.history = const [],
|
||||
});
|
||||
|
||||
@override
|
||||
State<ServerDetail> createState() => ServerDetailState();
|
||||
}
|
||||
591
lib/Peach/see/server/detail/ServerDetailState.dart
Normal file
591
lib/Peach/see/server/detail/ServerDetailState.dart
Normal file
@@ -0,0 +1,591 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
import '../../../about/internal/application/DeploymentRecord.dart';
|
||||
import '../../../about/internal/application/NavigatorType.dart';
|
||||
import '../../../about/internal/application/NotificationType.dart';
|
||||
import '../../../about/internal/application/ServerProfile.dart';
|
||||
import '../../../about/internal/application/ServiceConfig.dart';
|
||||
import '../../../about/internal/application/TextType.dart';
|
||||
import '../../../configs/Navigator.dart';
|
||||
import '../../../designs/text/Text.dart';
|
||||
import '../../../utils/Colors.dart';
|
||||
import '../../configuration/Configuration.dart';
|
||||
import '../../deploy/batch/BatchDeploy.dart';
|
||||
import '../../deploy/single/Deploy.dart';
|
||||
import '../../docker/manager/DockerManager.dart';
|
||||
import '../../files/Files.dart';
|
||||
import '../../health/Health.dart';
|
||||
import '../../kafka/Kafka.dart';
|
||||
import '../../logs/Logs.dart';
|
||||
import '../../redis/Redis.dart';
|
||||
import '../../service/ServiceEditor.dart';
|
||||
import '../../systemd/Systemd.dart';
|
||||
import '../editor/ServerEditor.dart';
|
||||
import 'ConnectServerDetail.dart';
|
||||
import 'ServerDetail.dart';
|
||||
import 'ViewServerDetail.dart';
|
||||
|
||||
class ServerDetailState extends State<ServerDetail>
|
||||
implements ConnectServerDetail {
|
||||
ViewServerDetail? _model;
|
||||
|
||||
late ServerProfile _server;
|
||||
List<DeploymentRecord> _history = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_server = widget.server.copy();
|
||||
_history = widget.history;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<ViewServerDetail>.reactive(
|
||||
viewModelBuilder: () => ViewServerDetail(context, this),
|
||||
onViewModelReady: _onReady,
|
||||
builder: (context, viewModel, child) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: text(_server.name, 20, TextType.Bold, color: context.jp.text),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: 'Edit server',
|
||||
onPressed: _onEditServer,
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Delete server',
|
||||
onPressed: _onDeleteServer,
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: _onAddService,
|
||||
icon: const Icon(Icons.add),
|
||||
label: text('Add service', 14, TextType.Bold, color: colorWhite),
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 96),
|
||||
children: [
|
||||
_connectionCard(),
|
||||
const SizedBox(height: 18),
|
||||
_toolsSection(),
|
||||
const SizedBox(height: 20),
|
||||
_sectionHeader(),
|
||||
const SizedBox(height: 10),
|
||||
if (_server.services.isEmpty)
|
||||
_noServices()
|
||||
else
|
||||
LayoutBuilder(
|
||||
builder: (context, c) {
|
||||
const gap = 12.0;
|
||||
final cols = c.maxWidth >= 760 ? 2 : 1;
|
||||
final w = (c.maxWidth - gap * (cols - 1)) / cols;
|
||||
return Wrap(
|
||||
spacing: gap,
|
||||
runSpacing: gap,
|
||||
children: [
|
||||
for (final svc in _server.services)
|
||||
SizedBox(width: w, child: _serviceCard(svc)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onReady(ViewServerDetail model) {
|
||||
_model = model;
|
||||
// onViewModelReady fires mid-build; a synchronous setState from the
|
||||
// first load would dirty this State while it is still being built.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _initiate();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _initiate() async {
|
||||
_model?.loadServer(_server.id);
|
||||
}
|
||||
|
||||
// ── named handlers: server ─────────────────────────────────────────────────
|
||||
|
||||
Future<void> _onEditServer() async {
|
||||
final edited = await PeachNavigation().navigateToPageWithData(
|
||||
ServerEditor(server: _server),
|
||||
context,
|
||||
);
|
||||
if (edited is! ServerProfile) return;
|
||||
// Preserve the services list on the edited connection.
|
||||
edited.services = _server.services;
|
||||
_model?.saveServer(edited);
|
||||
}
|
||||
|
||||
void _onDeleteServer() {
|
||||
_model?.showApplicationNotification(
|
||||
NotificationType.warning,
|
||||
'Delete "${_server.name}"?',
|
||||
'Removes the server, its services and its stored password.',
|
||||
true,
|
||||
true,
|
||||
null,
|
||||
action: 'Delete server',
|
||||
positiveAction: _onConfirmDeleteServer,
|
||||
);
|
||||
}
|
||||
|
||||
void _onConfirmDeleteServer() {
|
||||
Navigator.pop(context);
|
||||
_model?.deleteServer(_server);
|
||||
}
|
||||
|
||||
// ── named handlers: services ───────────────────────────────────────────────
|
||||
|
||||
Future<void> _onAddService() async {
|
||||
final created = await PeachNavigation().navigateToPageWithData(
|
||||
const ServiceEditor(),
|
||||
context,
|
||||
);
|
||||
if (created is! ServiceConfig) return;
|
||||
_model?.addService(_server, created);
|
||||
}
|
||||
|
||||
Future<void> _onEditService(ServiceConfig svc) async {
|
||||
final edited = await PeachNavigation().navigateToPageWithData(
|
||||
ServiceEditor(service: svc),
|
||||
context,
|
||||
);
|
||||
if (edited is! ServiceConfig) return;
|
||||
_model?.updateService(_server, edited);
|
||||
}
|
||||
|
||||
void _onDeleteService(ServiceConfig svc) =>
|
||||
_model?.removeService(_server, svc);
|
||||
|
||||
Future<void> _onDeploy(ServiceConfig svc) async {
|
||||
await PeachNavigation().navigateToPageWithData(
|
||||
Deploy(server: _server, service: svc),
|
||||
context,
|
||||
);
|
||||
_model?.loadServer(_server.id);
|
||||
}
|
||||
|
||||
void _onViewLogs(ServiceConfig svc) {
|
||||
PeachNavigation().navigateToPage(
|
||||
NavigatorType.justOpen,
|
||||
Logs(server: _server, service: svc),
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onRegisterUnit(ServiceConfig? svc) async {
|
||||
await PeachNavigation().navigateToPageWithData(
|
||||
Systemd(server: _server, service: svc),
|
||||
context,
|
||||
);
|
||||
_model?.loadServer(_server.id);
|
||||
}
|
||||
|
||||
// ── named handlers: tools ──────────────────────────────────────────────────
|
||||
|
||||
Future<void> _onOpenBatch() async {
|
||||
await PeachNavigation().navigateToPageWithData(
|
||||
BatchDeploy(server: _server),
|
||||
context,
|
||||
);
|
||||
_model?.loadServer(_server.id);
|
||||
}
|
||||
|
||||
void _onOpenConfiguration() => _open(Configuration(server: _server));
|
||||
|
||||
void _onOpenHealth() => _open(Health(servers: [_server]));
|
||||
|
||||
void _onOpenFiles() => _open(Files(server: _server));
|
||||
|
||||
void _onOpenDocker() => _open(DockerManager(server: _server));
|
||||
|
||||
void _onOpenRedis() => _open(Redis(server: _server));
|
||||
|
||||
void _onOpenKafka() => _open(Kafka(server: _server));
|
||||
|
||||
void _onNewUnit() => _onRegisterUnit(null);
|
||||
|
||||
void _open(Widget destination) => PeachNavigation().navigateToPage(
|
||||
NavigatorType.justOpen,
|
||||
destination,
|
||||
context,
|
||||
);
|
||||
|
||||
// ── ConnectServerDetail ────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
void onServerLoaded(ServerProfile server, List<DeploymentRecord> history) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_server = server;
|
||||
_history = history;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onServerDeleted() {
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
|
||||
// ── screen-only widgets ────────────────────────────────────────────────────
|
||||
|
||||
Widget _connectionCard() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: PeachTheme.panel(context, radius: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_line(
|
||||
Icons.dns,
|
||||
'${_server.username}@${_server.host}:${_server.port}',
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_line(
|
||||
_server.useSudo ? Icons.shield : Icons.shield_outlined,
|
||||
_server.useSudo ? 'sudo enabled' : 'no sudo',
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_line(Icons.lock, 'password stored encrypted'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _line(IconData icon, String label) => Row(
|
||||
children: [
|
||||
Icon(icon, size: 15, color: PeachTheme.seed.withValues(alpha: 0.9)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: text(label, 13, TextType.Regular, color: context.jp.textDim),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _toolsSection() {
|
||||
return LayoutBuilder(
|
||||
builder: (context, c) {
|
||||
const gap = 12.0;
|
||||
final cols = c.maxWidth >= 520 ? 4 : 2;
|
||||
final w = (c.maxWidth - gap * (cols - 1)) / cols;
|
||||
return Wrap(
|
||||
spacing: gap,
|
||||
runSpacing: gap,
|
||||
children: [
|
||||
_tool(
|
||||
w,
|
||||
Icons.rocket_launch,
|
||||
'Build & deploy all',
|
||||
'Batch build + deploy',
|
||||
PeachTheme.seed,
|
||||
_onOpenBatch,
|
||||
),
|
||||
_tool(
|
||||
w,
|
||||
Icons.fact_check_outlined,
|
||||
'Configuration',
|
||||
'Environment & setup',
|
||||
PeachTheme.warn,
|
||||
_onOpenConfiguration,
|
||||
),
|
||||
_tool(
|
||||
w,
|
||||
Icons.monitor_heart,
|
||||
'Health',
|
||||
'Service status',
|
||||
PeachTheme.danger,
|
||||
_onOpenHealth,
|
||||
),
|
||||
_tool(
|
||||
w,
|
||||
Icons.folder_open,
|
||||
'Files',
|
||||
'Browse / transfer',
|
||||
PeachTheme.accent,
|
||||
_onOpenFiles,
|
||||
),
|
||||
_tool(
|
||||
w,
|
||||
Icons.inventory_2,
|
||||
'Docker',
|
||||
'Containers & images',
|
||||
PeachTheme.accent,
|
||||
_onOpenDocker,
|
||||
),
|
||||
_tool(
|
||||
w,
|
||||
Icons.data_object,
|
||||
'Redis',
|
||||
'Browse keys / values',
|
||||
PeachTheme.warn,
|
||||
_onOpenRedis,
|
||||
),
|
||||
_tool(
|
||||
w,
|
||||
Icons.hub,
|
||||
'Kafka',
|
||||
'Topics · groups · lag',
|
||||
PeachTheme.violet,
|
||||
_onOpenKafka,
|
||||
),
|
||||
_tool(
|
||||
w,
|
||||
Icons.miscellaneous_services,
|
||||
'New unit',
|
||||
'systemd wizard',
|
||||
PeachTheme.violet,
|
||||
_onNewUnit,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tool(
|
||||
double w,
|
||||
IconData icon,
|
||||
String title,
|
||||
String sub,
|
||||
Color color,
|
||||
VoidCallback onTap,
|
||||
) {
|
||||
final jp = context.jp;
|
||||
return SizedBox(
|
||||
width: w,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: PeachTheme.panel(context, radius: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.14),
|
||||
borderRadius: BorderRadius.circular(11),
|
||||
border: Border.all(color: color.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Icon(icon, color: color, size: 20),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
text(
|
||||
title,
|
||||
14,
|
||||
TextType.Bold,
|
||||
color: jp.text,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 1),
|
||||
text(
|
||||
sub,
|
||||
11.5,
|
||||
TextType.Regular,
|
||||
color: jp.textFaint,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _sectionHeader() => Row(
|
||||
children: [
|
||||
text(
|
||||
'SERVICES',
|
||||
12,
|
||||
TextType.Bold,
|
||||
color: context.jp.textDim,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
text(
|
||||
'${_server.services.length}',
|
||||
12,
|
||||
TextType.Bold,
|
||||
color: PeachTheme.seed,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _serviceCard(ServiceConfig svc) {
|
||||
final jp = context.jp;
|
||||
final count = _history.where((h) => h.serviceId == svc.id).length;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: PeachTheme.panel(context, radius: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 38,
|
||||
height: 38,
|
||||
decoration: BoxDecoration(
|
||||
color: (svc.isWeb ? PeachTheme.accent : PeachTheme.seed)
|
||||
.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(9),
|
||||
),
|
||||
child: Icon(
|
||||
svc.isWeb
|
||||
? Icons.language
|
||||
: (svc.isGeneric
|
||||
? Icons.terminal
|
||||
: Icons.settings_suggest),
|
||||
color: svc.isWeb ? PeachTheme.accent : PeachTheme.seed,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
text(
|
||||
svc.name,
|
||||
15,
|
||||
TextType.Regular,
|
||||
color: jp.text,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
text(
|
||||
svc.isWeb
|
||||
? svc.remoteRoot
|
||||
: svc.remotePath(
|
||||
svc.remoteJarName.isEmpty
|
||||
? '<jar>'
|
||||
: svc.remoteJarName,
|
||||
),
|
||||
12,
|
||||
TextType.Regular,
|
||||
color: jp.textDim,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!svc.isWeb)
|
||||
IconButton(
|
||||
onPressed: () => _onRegisterUnit(svc),
|
||||
icon: const Icon(
|
||||
Icons.settings_applications_outlined,
|
||||
size: 20,
|
||||
),
|
||||
tooltip: 'Register systemd unit',
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => _onEditService(svc),
|
||||
icon: const Icon(Icons.edit_outlined, size: 20),
|
||||
tooltip: 'Edit',
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => _onDeleteService(svc),
|
||||
icon: const Icon(Icons.delete_outline, size: 20),
|
||||
tooltip: 'Delete',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: [
|
||||
if (svc.isWeb)
|
||||
_tag('◆ ${svc.framework.label}')
|
||||
else if (svc.serviceName.isNotEmpty)
|
||||
_tag('unit: ${svc.serviceName}'),
|
||||
if (svc.canBuild) _tag('⚙ builds from source'),
|
||||
if (count > 0) _tag('$count deploys'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _onViewLogs(svc),
|
||||
icon: const Icon(Icons.article_outlined, size: 18),
|
||||
label: text('Logs', 13, TextType.Bold, color: jp.textDim),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: jp.textDim,
|
||||
side: BorderSide(color: jp.border),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
FilledButton.icon(
|
||||
onPressed: () => _onDeploy(svc),
|
||||
icon: const Icon(Icons.rocket_launch, size: 18),
|
||||
label: text('Deploy', 13, TextType.Bold, color: colorWhite),
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tag(String label) => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: context.jp.surface2,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: text(label, 11, TextType.Regular, color: context.jp.textDim),
|
||||
);
|
||||
|
||||
Widget _noServices() => Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 30),
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.settings_suggest,
|
||||
size: 40,
|
||||
color: PeachTheme.seed.withValues(alpha: 0.5),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
text('No services yet', 14, TextType.Bold, color: context.jp.text),
|
||||
const SizedBox(height: 4),
|
||||
text(
|
||||
'Add a service to deploy a jar to this server.',
|
||||
13,
|
||||
TextType.Regular,
|
||||
color: context.jp.textDim,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
87
lib/Peach/see/server/detail/ViewServerDetail.dart
Normal file
87
lib/Peach/see/server/detail/ViewServerDetail.dart
Normal file
@@ -0,0 +1,87 @@
|
||||
import '../../../about/internal/application/ServerProfile.dart';
|
||||
import '../../../about/internal/application/ServiceConfig.dart';
|
||||
import '../../parent/ParentViewModel.dart';
|
||||
import 'ConnectServerDetail.dart';
|
||||
|
||||
class ViewServerDetail extends ParentViewModel {
|
||||
ConnectServerDetail connection;
|
||||
|
||||
ViewServerDetail(super.context, this.connection);
|
||||
|
||||
/// Re-reads this server from storage along with the deployment history, so
|
||||
/// the screen always reflects what was actually persisted.
|
||||
void loadServer(String serverId) async {
|
||||
try {
|
||||
final servers = await getDataManager().getServers();
|
||||
final history = await getDataManager().getDeployments();
|
||||
final stored = servers.where((s) => s.id == serverId).firstOrNull;
|
||||
if (stored == null) {
|
||||
connection.onServerDeleted();
|
||||
return;
|
||||
}
|
||||
connection.onServerLoaded(stored, history);
|
||||
} catch (error) {
|
||||
handleError(error, () => loadServer(serverId), () {}, "Retry");
|
||||
}
|
||||
}
|
||||
|
||||
/// Persists [server] — used after editing the connection, or after adding,
|
||||
/// editing or removing one of its services.
|
||||
void saveServer(ServerProfile server) async {
|
||||
try {
|
||||
final servers = await getDataManager().getServers();
|
||||
final index = servers.indexWhere((s) => s.id == server.id);
|
||||
if (index >= 0) {
|
||||
servers[index] = server;
|
||||
} else {
|
||||
servers.add(server);
|
||||
}
|
||||
await getDataManager().setServers(servers);
|
||||
loadServer(server.id);
|
||||
} catch (error) {
|
||||
handleError(error, () => saveServer(server), () {}, "Retry");
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds [service] to [server] and persists.
|
||||
void addService(ServerProfile server, ServiceConfig service) {
|
||||
server.services.add(service);
|
||||
saveServer(server);
|
||||
}
|
||||
|
||||
/// Replaces the stored copy of [service] on [server].
|
||||
void updateService(ServerProfile server, ServiceConfig service) {
|
||||
final index = server.services.indexWhere((s) => s.id == service.id);
|
||||
if (index >= 0) {
|
||||
server.services[index] = service;
|
||||
} else {
|
||||
server.services.add(service);
|
||||
}
|
||||
saveServer(server);
|
||||
}
|
||||
|
||||
void removeService(ServerProfile server, ServiceConfig service) {
|
||||
server.services.removeWhere((s) => s.id == service.id);
|
||||
saveServer(server);
|
||||
}
|
||||
|
||||
/// Saves the order services are deployed in during a batch run.
|
||||
void saveDeployOrder(ServerProfile server, List<String> ids) {
|
||||
server.deployOrder = ids;
|
||||
saveServer(server);
|
||||
}
|
||||
|
||||
void deleteServer(ServerProfile server) async {
|
||||
showLoading("Removing ${server.name} …");
|
||||
try {
|
||||
final servers = await getDataManager().getServers();
|
||||
servers.removeWhere((s) => s.id == server.id);
|
||||
await getDataManager().setServers(servers);
|
||||
await getDataManager().deleteServerSecret(server.id);
|
||||
closeLoading();
|
||||
connection.onServerDeleted();
|
||||
} catch (error) {
|
||||
handleError(error, () => deleteServer(server), () {}, "Retry");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user