Files
flutter_server_box/lib/view/page/container.dart
lollipopkit🏳️‍⚧️ 22c43c7124 Lollipopkit/issue382 (#383)
2024-06-10 21:34:56 +08:00

607 lines
16 KiB
Dart

import 'dart:async';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:toolbox/core/extension/context/locale.dart';
import 'package:toolbox/core/route.dart';
import 'package:toolbox/data/model/app/menu/base.dart';
import 'package:toolbox/data/model/app/menu/container.dart';
import 'package:toolbox/data/model/container/image.dart';
import 'package:toolbox/data/model/container/type.dart';
import 'package:toolbox/data/res/store.dart';
import '../../data/model/container/ps.dart';
import '../../data/model/server/server_private_info.dart';
import '../../data/provider/container.dart';
import '../widget/two_line_text.dart';
class ContainerPage extends StatefulWidget {
final ServerPrivateInfo spi;
const ContainerPage({required this.spi, super.key});
@override
State<ContainerPage> createState() => _ContainerPageState();
}
class _ContainerPageState extends State<ContainerPage> {
final _textController = TextEditingController();
late final _container = ContainerProvider(
client: widget.spi.server?.client,
userName: widget.spi.user,
hostId: widget.spi.id,
context: context,
);
late Size _size;
@override
void dispose() {
super.dispose();
_textController.dispose();
}
@override
void initState() {
super.initState();
_initAutoRefresh();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_size = MediaQuery.of(context).size;
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => _container,
builder: (_, __) => Consumer<ContainerProvider>(
builder: (_, ___, __) {
return Scaffold(
appBar: CustomAppBar(
centerTitle: true,
title: TwoLineText(up: l10n.container, down: widget.spi.name),
actions: [
IconButton(
onPressed: () =>
context.showLoadingDialog(fn: () => _container.refresh()),
icon: const Icon(Icons.refresh),
)
],
),
body: _buildMain(),
floatingActionButton: _container.error == null ? _buildFAB() : null,
);
},
),
);
}
Widget _buildFAB() {
return FloatingActionButton(
onPressed: () async => await _showAddFAB(),
child: const Icon(Icons.add),
);
}
Widget _buildMain() {
if (_container.error != null && _container.items == null) {
return SizedBox.expand(
child: Column(
children: [
const Spacer(),
const Icon(
Icons.error,
size: 37,
),
UIs.height13,
Padding(
padding: const EdgeInsets.symmetric(horizontal: 23),
child: Text(_container.error?.toString() ?? l10n.unknownError),
),
const Spacer(),
_buildEditHost(),
_buildSwitchProvider(),
UIs.height13,
],
),
);
}
if (_container.items == null || _container.images == null) {
return UIs.centerLoading;
}
return ListView(
padding: const EdgeInsets.only(left: 13, right: 13, top: 13, bottom: 37),
children: <Widget>[
_buildLoading(),
_buildVersion(),
_buildPs(),
_buildImage(),
_buildEditHost(),
_buildSwitchProvider(),
],
);
}
Widget _buildImage() {
return CardX(
child: ExpandTile(
title: Text(l10n.imagesList),
subtitle: Text(
l10n.dockerImagesFmt(_container.images!.length),
style: UIs.textGrey,
),
initiallyExpanded: (_container.images?.length ?? 0) <= 3,
children: _container.images?.map(_buildImageItem).toList() ?? [],
));
}
Widget _buildImageItem(ContainerImg e) {
return ListTile(
title: Text(e.repository ?? l10n.unknown),
subtitle: Text('${e.tag} - ${e.sizeMB}', style: UIs.textGrey),
trailing: IconButton(
padding: EdgeInsets.zero,
alignment: Alignment.centerRight,
icon: const Icon(Icons.delete),
onPressed: () => _showImageRmDialog(e),
),
);
}
Widget _buildLoading() {
if (_container.runLog == null) return UIs.placeholder;
return Padding(
padding: const EdgeInsets.all(17),
child: Column(
children: [
const Center(
child: CircularProgressIndicator(),
),
UIs.height13,
Text(_container.runLog ?? '...'),
],
),
);
}
Widget _buildVersion() {
return CardX(
child: Padding(
padding: const EdgeInsets.all(17),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(_container.type.name.upperFirst),
Text(_container.version ?? l10n.unknown),
],
),
));
}
Widget _buildPs() {
final items = _container.items;
if (items == null) return UIs.placeholder;
return Column(
children: items.map(_buildPsItem).toList(),
);
}
Widget _buildPsItem(ContainerPs item) {
return CardX(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 11),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(item.name ?? l10n.unknown, style: UIs.text15),
const SizedBox(height: 3),
_buildMoreBtn(item),
],
),
Text(
'${item.image ?? l10n.unknown} - ${switch (item) {
final PodmanPs ps => ps.running ? l10n.running : l10n.stopped,
final DockerPs ps => ps.state,
}}',
style: UIs.text13Grey,
),
_buildPsItemStats(item),
],
),
),
);
}
Widget _buildPsItemStats(ContainerPs item) {
if (item.cpu == null || item.mem == null) return UIs.placeholder;
return Column(
children: [
UIs.height13,
Row(
children: [
_buildPsItemStatsItem('CPU', item.cpu, Icons.memory),
UIs.width13,
_buildPsItemStatsItem('Net', item.net, Icons.network_cell),
],
),
Row(
children: [
_buildPsItemStatsItem(
'Mem', item.mem, Icons.settings_input_component),
UIs.width13,
_buildPsItemStatsItem('Disk', item.disk, Icons.storage),
],
),
],
);
}
Widget _buildPsItemStatsItem(String title, String? value, IconData icon) {
return SizedBox(
width: _size.width / 2 - 41,
child: Column(
children: [
Row(
children: [
Icon(icon, size: 12, color: Colors.grey),
UIs.width7,
Text(value ?? l10n.unknown, style: UIs.text11Grey),
],
)
],
),
);
}
Widget _buildMoreBtn(ContainerPs dItem) {
return PopupMenu(
items: ContainerMenu.items(dItem.running)
.map((e) => PopMenu.build(e, e.icon, e.toStr))
.toList(),
onSelected: (item) => _onTapMoreBtn(item, dItem),
);
}
// String _buildPsCardSubtitle(List<ContainerPs> running) {
// final runningCount = running.where((element) => element.running).length;
// final stoped = running.length - runningCount;
// if (stoped == 0) {
// return l10n.dockerStatusRunningFmt(runningCount);
// }
// return l10n.dockerStatusRunningAndStoppedFmt(runningCount, stoped);
// }
Widget _buildEditHost() {
final children = <Widget>[];
final emptyImgs = _container.images?.isEmpty ?? false;
final emptyPs = _container.items?.isEmpty ?? false;
if (emptyPs && emptyImgs) {
children.add(Padding(
padding: const EdgeInsets.fromLTRB(17, 17, 17, 0),
child: Text(
l10n.dockerEmptyRunningItems,
textAlign: TextAlign.center,
),
));
}
children.add(
TextButton(
onPressed: _showEditHostDialog,
child: Text(l10n.dockerEditHost),
),
);
return CardX(
child: Column(
children: children,
));
}
Widget _buildSwitchProvider() {
late final Widget child;
if (_container.type == ContainerType.podman) {
child = TextButton(
onPressed: () {
_container.setType(ContainerType.docker);
},
child: Text(l10n.switchTo('Docker')),
);
} else {
child = TextButton(
onPressed: () {
_container.setType(ContainerType.podman);
},
child: Text(l10n.switchTo('Podman')),
);
}
return CardX(child: child);
}
Future<void> _showAddFAB() async {
final imageCtrl = TextEditingController();
final nameCtrl = TextEditingController();
final argsCtrl = TextEditingController();
await context.showRoundDialog(
title: l10n.newContainer,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Input(
autoFocus: true,
type: TextInputType.text,
label: l10n.image,
hint: 'xxx:1.1',
controller: imageCtrl,
),
Input(
type: TextInputType.text,
controller: nameCtrl,
label: l10n.containerName,
hint: 'xxx',
),
Input(
type: TextInputType.text,
controller: argsCtrl,
label: l10n.extraArgs,
hint: '-p 2222:22 -v ~/.xxx/:/xxx',
),
],
),
actions: [
TextButton(
onPressed: () => context.pop(),
child: Text(l10n.cancel),
),
TextButton(
onPressed: () async {
context.pop();
await _showAddCmdPreview(
_buildAddCmd(
imageCtrl.text.trim(),
nameCtrl.text.trim(),
argsCtrl.text.trim(),
),
);
},
child: Text(l10n.ok),
)
],
);
}
Future<void> _showAddCmdPreview(String cmd) async {
await context.showRoundDialog(
title: l10n.preview,
child: Text(cmd),
actions: [
TextButton(
onPressed: () => context.pop(),
child: Text(l10n.cancel),
),
TextButton(
onPressed: () async {
context.pop();
final result = await context.showLoadingDialog(
fn: () => _container.run(cmd),
);
if (result != null) {
context.showSnackBar(result.message ?? l10n.unknownError);
}
},
child: Text(l10n.run),
)
],
);
}
String _buildAddCmd(String image, String name, String args) {
var suffix = '';
if (args.isEmpty) {
suffix = image;
} else {
suffix = '$args $image';
}
if (name.isEmpty) {
return 'run -itd $suffix';
}
return 'run -itd --name $name $suffix';
}
Future<void> _showEditHostDialog() async {
final id = widget.spi.id;
final host = Stores.container.fetch(id);
final ctrl = TextEditingController(text: host);
await context.showRoundDialog(
title: l10n.dockerEditHost,
child: Input(
maxLines: 2,
controller: ctrl,
onSubmitted: _onSaveDockerHost,
hint: 'unix:///run/user/1000/docker.sock',
),
actions: [
TextButton(
onPressed: () => _onSaveDockerHost(ctrl.text),
child: Text(l10n.ok),
),
],
);
}
void _onSaveDockerHost(String val) {
context.pop();
Stores.container.put(widget.spi.id, val.trim());
_container.refresh();
}
void _showImageRmDialog(ContainerImg e) {
context.showRoundDialog(
title: l10n.attention,
child: Text(l10n.askContinue('${l10n.delete} Image(${e.repository})')),
actions: [
TextButton(
onPressed: () => context.pop(),
child: Text(l10n.cancel),
),
TextButton(
onPressed: () async {
context.pop();
final result = await _container.run('rmi ${e.id} -f');
if (result != null) {
context.showSnackBar(result.message ?? l10n.unknownError);
}
},
child: Text(l10n.ok, style: UIs.textRed),
),
],
);
}
void _onTapMoreBtn(ContainerMenu item, ContainerPs dItem) async {
final id = dItem.id;
if (id == null) {
context.showSnackBar('Id is null');
return;
}
switch (item) {
case ContainerMenu.rm:
var force = false;
context.showRoundDialog(
title: l10n.attention,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(l10n.askContinue(
'${l10n.delete} Container(${dItem.name})',
)),
UIs.height13,
Row(
children: [
StatefulBuilder(builder: (_, setState) {
return Checkbox(
value: force,
onChanged: (val) => setState(
() => force = val ?? false,
),
);
}),
Text(l10n.force),
],
)
],
),
actions: [
TextButton(
onPressed: () async {
context.pop();
final result = await context.showLoadingDialog(
fn: () => _container.delete(id, force),
);
if (result != null) {
context.showRoundDialog(
title: l10n.error,
child: Text(result.message ?? l10n.unknownError),
);
}
},
child: Text(l10n.ok),
)
],
);
break;
case ContainerMenu.start:
final result = await context.showLoadingDialog(
fn: () => _container.start(id),
);
if (result != null) {
context.showRoundDialog(
title: l10n.error,
child: Text(result.message ?? l10n.unknownError),
);
}
break;
case ContainerMenu.stop:
final result = await context.showLoadingDialog(
fn: () => _container.stop(id),
);
if (result != null) {
context.showRoundDialog(
title: l10n.error,
child: Text(result.message ?? l10n.unknownError),
);
}
break;
case ContainerMenu.restart:
final result = await context.showLoadingDialog(
fn: () => _container.restart(id),
);
if (result != null) {
context.showRoundDialog(
title: l10n.error,
child: Text(result.message ?? l10n.unknownError),
);
}
break;
case ContainerMenu.logs:
AppRoutes.ssh(
spi: widget.spi,
initCmd: '${switch (_container.type) {
ContainerType.podman => 'podman',
ContainerType.docker => 'docker',
}} logs -f --tail 100 ${dItem.id}',
).go(context);
break;
case ContainerMenu.terminal:
AppRoutes.ssh(
spi: widget.spi,
initCmd: '${switch (_container.type) {
ContainerType.podman => 'podman',
ContainerType.docker => 'docker',
}} exec -it ${dItem.id} sh',
).go(context);
break;
// case DockerMenuType.stats:
// showRoundDialog(
// context: context,
// title: Text(l10n.stats),
// child: Text(
// 'CPU: ${dItem.cpu}\n'
// 'Mem: ${dItem.mem}\n'
// 'Net: ${dItem.net}\n'
// 'Block: ${dItem.disk}',
// ),
// actions: [
// TextButton(
// onPressed: () => context.pop(),
// child: Text(l10n.ok),
// ),
// ],
// );
// break;
}
}
void _initAutoRefresh() {
if (Stores.setting.contaienrAutoRefresh.fetch()) {
Timer.periodic(
Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch()),
(timer) {
if (mounted) {
_container.refresh(isAuto: true);
} else {
timer.cancel();
}
},
);
}
}
}