new: container stats (#272)

This commit is contained in:
lollipopkit
2024-02-20 15:00:13 +08:00
parent 483bf51c2f
commit 828752e354
19 changed files with 284 additions and 145 deletions

View File

@@ -1084,6 +1084,18 @@ abstract class S {
/// **'Different servers will have different logs, and the log is the path to the exit'** /// **'Different servers will have different logs, and the log is the path to the exit'**
String get openLastPathTip; String get openLastPathTip;
/// No description provided for @parseContainerStats.
///
/// In en, this message translates to:
/// **'Parse the container occupancy status'**
String get parseContainerStats;
/// No description provided for @parseContainerStatsTip.
///
/// In en, this message translates to:
/// **'Docker parsing the occupancy status is relatively slow.'**
String get parseContainerStatsTip;
/// No description provided for @paste. /// No description provided for @paste.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View File

@@ -518,6 +518,12 @@ class SDe extends S {
@override @override
String get openLastPathTip => 'Verschiedene Server haben unterschiedliche Einträge, und der Eintrag ist der Pfad zum Ausgang'; String get openLastPathTip => 'Verschiedene Server haben unterschiedliche Einträge, und der Eintrag ist der Pfad zum Ausgang';
@override
String get parseContainerStats => 'Den Status der Container-Belegung analysieren';
@override
String get parseContainerStatsTip => 'Das Analysieren des Belegungsstatus durch Docker ist relativ langsam';
@override @override
String get paste => 'Einfügen'; String get paste => 'Einfügen';

View File

@@ -518,6 +518,12 @@ class SEn extends S {
@override @override
String get openLastPathTip => 'Different servers will have different logs, and the log is the path to the exit'; String get openLastPathTip => 'Different servers will have different logs, and the log is the path to the exit';
@override
String get parseContainerStats => 'Parse the container occupancy status';
@override
String get parseContainerStatsTip => 'Docker parsing the occupancy status is relatively slow.';
@override @override
String get paste => 'Paste'; String get paste => 'Paste';

View File

@@ -518,6 +518,12 @@ class SFr extends S {
@override @override
String get openLastPathTip => 'Les serveurs différents auront des journaux différents, et le journal est le chemin de sortie'; String get openLastPathTip => 'Les serveurs différents auront des journaux différents, et le journal est le chemin de sortie';
@override
String get parseContainerStats => 'Analyser l\'état d\'occupation du conteneur';
@override
String get parseContainerStatsTip => 'L\'analyse de l\'état d\'occupation par Docker est relativement lente.';
@override @override
String get paste => 'Coller'; String get paste => 'Coller';

View File

@@ -518,6 +518,12 @@ class SId extends S {
@override @override
String get openLastPathTip => 'Server yang berbeda akan memiliki catatan yang berbeda, dan catatan tersebut adalah jalur menuju pintu keluar'; String get openLastPathTip => 'Server yang berbeda akan memiliki catatan yang berbeda, dan catatan tersebut adalah jalur menuju pintu keluar';
@override
String get parseContainerStats => 'Memecahkan status okupansi kontainer';
@override
String get parseContainerStatsTip => 'Parsing status okupansi oleh Docker agak lambat';
@override @override
String get paste => 'Tempel'; String get paste => 'Tempel';

View File

@@ -518,6 +518,12 @@ class SZh extends S {
@override @override
String get openLastPathTip => '不同的服务器会有不同的记录,且记录的是退出时的路径'; String get openLastPathTip => '不同的服务器会有不同的记录,且记录的是退出时的路径';
@override
String get parseContainerStats => '解析容器占用状态';
@override
String get parseContainerStatsTip => 'Docker解析占用状态较为缓慢';
@override @override
String get paste => '粘贴'; String get paste => '粘贴';
@@ -1390,6 +1396,12 @@ class SZhTw extends SZh {
@override @override
String get openLastPathTip => '不同的服務器會有不同的記錄,且記錄的是退出時的路徑'; String get openLastPathTip => '不同的服務器會有不同的記錄,且記錄的是退出時的路徑';
@override
String get parseContainerStats => '解析容器佔用狀態';
@override
String get parseContainerStatsTip => 'Docker解析佔用狀態較為緩慢';
@override @override
String get paste => '貼上'; String get paste => '貼上';

View File

@@ -40,7 +40,7 @@ enum ContainerErrType {
invalidVersion, invalidVersion,
cmdNoPrefix, cmdNoPrefix,
segmentsNotMatch, segmentsNotMatch,
parsePsItem, parsePs,
parseImages, parseImages,
parseStats, parseStats,
} }

View File

@@ -1,5 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:toolbox/core/extension/context/locale.dart';
import 'package:toolbox/core/extension/numx.dart';
import 'package:toolbox/data/model/container/type.dart'; import 'package:toolbox/data/model/container/type.dart';
abstract final class ContainerPs { abstract final class ContainerPs {
@@ -9,7 +11,14 @@ abstract final class ContainerPs {
String? get cmd; String? get cmd;
bool get running; bool get running;
String? cpu;
String? mem;
String? net;
String? disk;
factory ContainerPs.fromRawJson(String s, ContainerType typ) => typ.ps(s); factory ContainerPs.fromRawJson(String s, ContainerType typ) => typ.ps(s);
void parseStats(String s);
} }
final class PodmanPs implements ContainerPs { final class PodmanPs implements ContainerPs {
@@ -23,6 +32,11 @@ final class PodmanPs implements ContainerPs {
final List<String>? names; final List<String>? names;
final int? startedAt; final int? startedAt;
String? cpu;
String? mem;
String? net;
String? disk;
PodmanPs({ PodmanPs({
this.command, this.command,
this.created, this.created,
@@ -42,6 +56,23 @@ final class PodmanPs implements ContainerPs {
@override @override
bool get running => exited != true; bool get running => exited != true;
@override
void parseStats(String s) {
final stats = json.decode(s);
final cpuD = (stats['CPU'] as double? ?? 0).toStringAsFixed(1);
final cpuAvgD = (stats['AvgCPU'] as double? ?? 0).toStringAsFixed(1);
cpu = '$cpuD% / ${l10n.pingAvg} $cpuAvgD%';
final memLimit = (stats['MemLimit'] as int? ?? 0).bytes2Str;
final memUsage = (stats['MemUsage'] as int? ?? 0).bytes2Str;
mem = '$memUsage / $memLimit';
final netIn = (stats['NetInput'] as int? ?? 0).bytes2Str;
final netOut = (stats['NetOutput'] as int? ?? 0).bytes2Str;
net = '$netIn / ↑ $netOut';
final diskIn = (stats['BlockInput'] as int? ?? 0).bytes2Str;
final diskOut = (stats['BlockOutput'] as int? ?? 0).bytes2Str;
disk = '${l10n.read} $diskOut / ${l10n.write} $diskIn';
}
factory PodmanPs.fromRawJson(String str) => factory PodmanPs.fromRawJson(String str) =>
PodmanPs.fromJson(json.decode(str)); PodmanPs.fromJson(json.decode(str));
@@ -84,6 +115,11 @@ final class DockerPs implements ContainerPs {
final String? names; final String? names;
final String? state; final String? state;
String? cpu;
String? mem;
String? net;
String? disk;
DockerPs({ DockerPs({
this.command, this.command,
this.createdAt, this.createdAt,
@@ -102,6 +138,15 @@ final class DockerPs implements ContainerPs {
@override @override
bool get running => state == 'running'; bool get running => state == 'running';
@override
void parseStats(String s) {
final stats = json.decode(s);
cpu = stats['CPUPerc'];
mem = stats['MemUsage'];
net = stats['NetIO'];
disk = stats['BlockIO'];
}
factory DockerPs.fromRawJson(String str) => factory DockerPs.fromRawJson(String str) =>
DockerPs.fromJson(json.decode(str)); DockerPs.fromJson(json.decode(str));

View File

@@ -1,64 +0,0 @@
import 'dart:convert';
class Containerd {
final ContainerdClient client;
Containerd({
required this.client,
});
factory Containerd.fromRawJson(String str) =>
Containerd.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Containerd.fromJson(Map<String, dynamic> json) => Containerd(
client: ContainerdClient.fromJson(json["Client"]),
);
Map<String, dynamic> toJson() => {
"Client": client.toJson(),
};
}
class ContainerdClient {
final String apiVersion;
final String version;
final String goVersion;
final String gitCommit;
final String builtTime;
final String os;
ContainerdClient({
required this.apiVersion,
required this.version,
required this.goVersion,
required this.gitCommit,
required this.builtTime,
required this.os,
});
factory ContainerdClient.fromRawJson(String str) =>
ContainerdClient.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory ContainerdClient.fromJson(Map<String, dynamic> json) =>
ContainerdClient(
apiVersion: json["ApiVersion"],
version: json["Version"],
goVersion: json["GoVersion"],
gitCommit: json["GitCommit"],
builtTime: json["BuildTime"],
os: json["Os"],
);
Map<String, dynamic> toJson() => {
"ApiVersion": apiVersion,
"Version": version,
"GoVersion": goVersion,
"GitCommit": gitCommit,
"BuildTime": builtTime,
"Os": os,
};
}

View File

@@ -1,14 +1,15 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:dartssh2/dartssh2.dart'; import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:toolbox/core/extension/listx.dart';
import 'package:toolbox/core/extension/ssh_client.dart'; import 'package:toolbox/core/extension/ssh_client.dart';
import 'package:toolbox/data/model/app/shell_func.dart'; import 'package:toolbox/data/model/app/shell_func.dart';
import 'package:toolbox/data/model/container/image.dart'; import 'package:toolbox/data/model/container/image.dart';
import 'package:toolbox/data/model/container/ps.dart'; import 'package:toolbox/data/model/container/ps.dart';
import 'package:toolbox/data/model/app/error.dart'; import 'package:toolbox/data/model/app/error.dart';
import 'package:toolbox/data/model/container/type.dart'; import 'package:toolbox/data/model/container/type.dart';
import 'package:toolbox/data/model/container/version.dart';
import 'package:toolbox/data/res/logger.dart'; import 'package:toolbox/data/res/logger.dart';
import 'package:toolbox/data/res/store.dart'; import 'package:toolbox/data/res/store.dart';
import 'package:toolbox/core/extension/uint8list.dart'; import 'package:toolbox/core/extension/uint8list.dart';
@@ -74,14 +75,20 @@ class ContainerProvider extends ChangeNotifier {
final sudo = final sudo =
await _requiresSudo() && Stores.setting.containerTrySudo.fetch(); await _requiresSudo() && Stores.setting.containerTrySudo.fetch();
final includeStats = Stores.setting.containerParseStat.fetch();
await client?.execWithPwd( final code = await client?.execWithPwd(
_wrap(ContainerCmdType.execAll(type, sudo: sudo)), _wrap(ContainerCmdType.execAll(
type,
sudo: sudo,
includeStats: includeStats,
)),
context: context, context: context,
onStdout: (data, _) => raw = '$raw$data', onStdout: (data, _) => raw = '$raw$data',
); );
if (raw.contains(_dockerNotFound)) { /// Code 127 means command not found
if (code == 127 || raw.contains(_dockerNotFound)) {
error = ContainerErr(type: ContainerErrType.notInstalled); error = ContainerErr(type: ContainerErrType.notInstalled);
notifyListeners(); notifyListeners();
return; return;
@@ -99,12 +106,10 @@ class ContainerProvider extends ChangeNotifier {
return; return;
} }
// Parse docker version // Parse version
final verRaw = ContainerCmdType.version.find(segments); final verRaw = ContainerCmdType.version.find(segments);
debugPrint('version raw = $verRaw\n');
try { try {
final containerVersion = Containerd.fromRawJson(verRaw); version = json.decode(verRaw)['Client']['Version'];
version = containerVersion.client.version;
} catch (e, trace) { } catch (e, trace) {
error = ContainerErr( error = ContainerErr(
type: ContainerErrType.invalidVersion, type: ContainerErrType.invalidVersion,
@@ -115,14 +120,23 @@ class ContainerProvider extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
// Parse docker ps // Parse ps
final psRaw = ContainerCmdType.ps.find(segments); final psRaw = ContainerCmdType.ps.find(segments);
try {
final lines = psRaw.split('\n');
lines.removeWhere((element) => element.isEmpty);
items = lines.map((e) => ContainerPs.fromRawJson(e, type)).toList();
} catch (e, trace) {
error = ContainerErr(
type: ContainerErrType.parsePs,
message: '$e',
);
Loggers.parse.warning('Container ps failed', e, trace);
} finally {
notifyListeners();
}
final lines = psRaw.split('\n'); // Parse images
lines.removeWhere((element) => element.isEmpty);
items = lines.map((e) => ContainerPs.fromRawJson(e, type)).toList();
// Parse docker images
final imageRaw = ContainerCmdType.images.find(segments); final imageRaw = ContainerCmdType.images.find(segments);
try { try {
final imgLines = imageRaw.split('\n'); final imgLines = imageRaw.split('\n');
@@ -138,29 +152,31 @@ class ContainerProvider extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
// Parse docker stats // Parse stats
// final statsRaw = DockerCmdType.stats.find(segments); final statsRaw = ContainerCmdType.stats.find(segments);
// try { try {
// final statsLines = statsRaw.split('\n'); final statsLines = statsRaw.split('\n');
// statsLines.removeWhere((element) => element.isEmpty); statsLines.removeWhere((element) => element.isEmpty);
// if (statsLines.isNotEmpty) statsLines.removeAt(0); for (var item in items!) {
// for (var item in items!) { final id = item.id;
// final statsLine = statsLines.firstWhere( if (id == null) continue;
// (element) => element.contains(item.containerId), final statsLine = statsLines.firstWhereOrNull(
// orElse: () => '', /// Use 5 characters to match the container id, possibility of mismatch
// ); /// is very low.
// if (statsLine.isEmpty) continue; (element) => element.contains(id.substring(0, 5)),
// item.parseStats(statsLine); );
// } if (statsLine == null) continue;
// } catch (e, trace) { item.parseStats(statsLine);
// error = DockerErr( }
// type: DockerErrType.parseStats, } catch (e, trace) {
// message: '$e', error = ContainerErr(
// ); type: ContainerErrType.parseStats,
// _logger.warning('Parse docker stats: $statsRaw', e, trace); message: '$e',
// } finally { );
// notifyListeners(); Loggers.parse.warning('Parse docker stats: $statsRaw', e, trace);
// } } finally {
notifyListeners();
}
} }
Future<ContainerErr?> stop(String id) async => await run('stop $id'); Future<ContainerErr?> stop(String id) async => await run('stop $id');
@@ -223,21 +239,32 @@ const _jsonFmt = '--format "{{json .}}"';
enum ContainerCmdType { enum ContainerCmdType {
version, version,
ps, ps,
//stats, stats,
images, images,
; ;
String exec(ContainerType type, {bool sudo = false}) { String exec(
ContainerType type, {
bool sudo = false,
bool includeStats = false,
}) {
final prefix = sudo ? 'sudo -S ${type.name}' : type.name; final prefix = sudo ? 'sudo -S ${type.name}' : type.name;
return switch (this) { return switch (this) {
ContainerCmdType.version => '$prefix version $_jsonFmt', ContainerCmdType.version => '$prefix version $_jsonFmt',
ContainerCmdType.ps => '$prefix ps -a $_jsonFmt', ContainerCmdType.ps => '$prefix ps -a $_jsonFmt',
// DockerCmdType.stats => '$prefix stats --no-stream'; ContainerCmdType.stats =>
includeStats ? '$prefix stats --no-stream $_jsonFmt' : 'echo PASS',
ContainerCmdType.images => '$prefix image ls $_jsonFmt', ContainerCmdType.images => '$prefix image ls $_jsonFmt',
}; };
} }
static String execAll(ContainerType type, {bool sudo = false}) => values static String execAll(
.map((e) => e.exec(type, sudo: sudo)) ContainerType type, {
.join(' && echo $seperator && '); bool sudo = false,
bool includeStats = false,
}) {
return ContainerCmdType.values
.map((e) => e.exec(type, sudo: sudo))
.join(' && echo $seperator && ');
}
} }

View File

@@ -233,6 +233,9 @@ class SettingStore extends PersistentStore {
/// Keep previous server status when err occurs /// Keep previous server status when err occurs
late final keepStatusWhenErr = property('keepStatusWhenErr', false); late final keepStatusWhenErr = property('keepStatusWhenErr', false);
/// Parse container stat
late final containerParseStat = property('containerParseStat', true);
// Never show these settings for users // Never show these settings for users
// //
// ------BEGIN------ // ------BEGIN------

View File

@@ -164,6 +164,8 @@
"open": "Öffnen", "open": "Öffnen",
"openLastPath": "Öffnen Sie den letzten Pfad", "openLastPath": "Öffnen Sie den letzten Pfad",
"openLastPathTip": "Verschiedene Server haben unterschiedliche Einträge, und der Eintrag ist der Pfad zum Ausgang", "openLastPathTip": "Verschiedene Server haben unterschiedliche Einträge, und der Eintrag ist der Pfad zum Ausgang",
"parseContainerStats": "Den Status der Container-Belegung analysieren",
"parseContainerStatsTip": "Das Analysieren des Belegungsstatus durch Docker ist relativ langsam",
"paste": "Einfügen", "paste": "Einfügen",
"path": "Pfad", "path": "Pfad",
"percentOfSize": "{percent}% von {size}", "percentOfSize": "{percent}% von {size}",

View File

@@ -164,6 +164,8 @@
"open": "Open", "open": "Open",
"openLastPath": "Open the last path", "openLastPath": "Open the last path",
"openLastPathTip": "Different servers will have different logs, and the log is the path to the exit", "openLastPathTip": "Different servers will have different logs, and the log is the path to the exit",
"parseContainerStats": "Parse the container occupancy status",
"parseContainerStatsTip": "Docker parsing the occupancy status is relatively slow.",
"paste": "Paste", "paste": "Paste",
"path": "Path", "path": "Path",
"percentOfSize": "{percent}% of {size}", "percentOfSize": "{percent}% of {size}",

View File

@@ -164,6 +164,8 @@
"open": "Ouvrir", "open": "Ouvrir",
"openLastPath": "Ouvrir le dernier chemin", "openLastPath": "Ouvrir le dernier chemin",
"openLastPathTip": "Les serveurs différents auront des journaux différents, et le journal est le chemin de sortie", "openLastPathTip": "Les serveurs différents auront des journaux différents, et le journal est le chemin de sortie",
"parseContainerStats": "Analyser l'état d'occupation du conteneur",
"parseContainerStatsTip": "L'analyse de l'état d'occupation par Docker est relativement lente.",
"paste": "Coller", "paste": "Coller",
"path": "Chemin", "path": "Chemin",
"percentOfSize": "{percent}% de {size}", "percentOfSize": "{percent}% de {size}",

View File

@@ -164,6 +164,8 @@
"open": "Membuka", "open": "Membuka",
"openLastPath": "Buka jalur terakhir", "openLastPath": "Buka jalur terakhir",
"openLastPathTip": "Server yang berbeda akan memiliki catatan yang berbeda, dan catatan tersebut adalah jalur menuju pintu keluar", "openLastPathTip": "Server yang berbeda akan memiliki catatan yang berbeda, dan catatan tersebut adalah jalur menuju pintu keluar",
"parseContainerStats": "Memecahkan status okupansi kontainer",
"parseContainerStatsTip": "Parsing status okupansi oleh Docker agak lambat",
"paste": "Tempel", "paste": "Tempel",
"path": "Jalur", "path": "Jalur",
"percentOfSize": "{percent}% dari {size}", "percentOfSize": "{percent}% dari {size}",

View File

@@ -164,6 +164,8 @@
"open": "打开", "open": "打开",
"openLastPath": "打开上次的路径", "openLastPath": "打开上次的路径",
"openLastPathTip": "不同的服务器会有不同的记录,且记录的是退出时的路径", "openLastPathTip": "不同的服务器会有不同的记录,且记录的是退出时的路径",
"parseContainerStats": "解析容器占用状态",
"parseContainerStatsTip": "Docker解析占用状态较为缓慢",
"paste": "粘贴", "paste": "粘贴",
"path": "路径", "path": "路径",
"percentOfSize": "{size} 的 {percent}%", "percentOfSize": "{size} 的 {percent}%",

View File

@@ -164,6 +164,8 @@
"open": "打開", "open": "打開",
"openLastPath": "打開上次的路徑", "openLastPath": "打開上次的路徑",
"openLastPathTip": "不同的服務器會有不同的記錄,且記錄的是退出時的路徑", "openLastPathTip": "不同的服務器會有不同的記錄,且記錄的是退出時的路徑",
"parseContainerStats": "解析容器佔用狀態",
"parseContainerStatsTip": "Docker解析佔用狀態較為緩慢",
"paste": "貼上", "paste": "貼上",
"path": "路徑", "path": "路徑",
"percentOfSize": "{size} 的 {percent}%", "percentOfSize": "{size} 的 {percent}%",

View File

@@ -38,6 +38,7 @@ class _ContainerPageState extends State<ContainerPage> {
hostId: widget.spi.id, hostId: widget.spi.id,
context: context, context: context,
); );
late Size _size;
@override @override
void dispose() { void dispose() {
@@ -50,6 +51,12 @@ class _ContainerPageState extends State<ContainerPage> {
super.initState(); super.initState();
} }
@override
void didChangeDependencies() {
super.didChangeDependencies();
_size = MediaQuery.of(context).size;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProvider( return ChangeNotifierProvider(
@@ -113,22 +120,22 @@ class _ContainerPageState extends State<ContainerPage> {
return UIs.centerLoading; return UIs.centerLoading;
} }
final items = <Widget>[
_buildLoading(),
_buildVersion(),
_buildPs(),
_buildImage(),
_buildEditHost(),
_buildSwitchProvider(),
].map((e) => CardX(child: e)).toList();
return ListView( return ListView(
padding: const EdgeInsets.only(left: 13, right: 13, top: 13, bottom: 37), padding: const EdgeInsets.only(left: 13, right: 13, top: 13, bottom: 37),
children: items, children: <Widget>[
_buildLoading(),
_buildVersion(),
_buildPs(),
_buildImage(),
_buildEditHost(),
_buildSwitchProvider(),
],
); );
} }
Widget _buildImage() { Widget _buildImage() {
return ExpandTile( return CardX(
child: ExpandTile(
title: Text(l10n.imagesList), title: Text(l10n.imagesList),
subtitle: Text( subtitle: Text(
l10n.dockerImagesFmt(_container.images!.length), l10n.dockerImagesFmt(_container.images!.length),
@@ -136,7 +143,7 @@ class _ContainerPageState extends State<ContainerPage> {
), ),
initiallyExpanded: (_container.images?.length ?? 0) <= 3, initiallyExpanded: (_container.images?.length ?? 0) <= 3,
children: _container.images?.map(_buildImageItem).toList() ?? [], children: _container.images?.map(_buildImageItem).toList() ?? [],
); ));
} }
Widget _buildImageItem(ContainerImg e) { Widget _buildImageItem(ContainerImg e) {
@@ -169,7 +176,8 @@ class _ContainerPageState extends State<ContainerPage> {
} }
Widget _buildVersion() { Widget _buildVersion() {
return Padding( return CardX(
child: Padding(
padding: const EdgeInsets.all(17), padding: const EdgeInsets.all(17),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -178,31 +186,82 @@ class _ContainerPageState extends State<ContainerPage> {
Text(_container.version ?? l10n.unknown), Text(_container.version ?? l10n.unknown),
], ],
), ),
); ));
} }
Widget _buildPs() { Widget _buildPs() {
final items = _container.items; final items = _container.items;
if (items == null) return UIs.placeholder; if (items == null) return UIs.placeholder;
return ExpandTile( return Column(
title: Text(l10n.containerStatus),
subtitle: Text(
_buildPsCardSubtitle(items),
style: UIs.textGrey,
),
initiallyExpanded: items.length <= 7,
children: items.map(_buildPsItem).toList(), children: items.map(_buildPsItem).toList(),
); );
} }
Widget _buildPsItem(ContainerPs item) { Widget _buildPsItem(ContainerPs item) {
return ListTile( return CardX(
title: Text(item.name ?? l10n.unknown), child: Padding(
subtitle: Text( padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 11),
'${item.image ?? l10n.unknown} - ${item.running ? l10n.running : l10n.stopped}', child: Column(
style: UIs.text13Grey, 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} - ${item.running ? l10n.running : l10n.stopped}',
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),
],
)
],
), ),
trailing: _buildMoreBtn(item),
); );
} }
@@ -213,14 +272,14 @@ class _ContainerPageState extends State<ContainerPage> {
); );
} }
String _buildPsCardSubtitle(List<ContainerPs> running) { // String _buildPsCardSubtitle(List<ContainerPs> running) {
final runningCount = running.where((element) => element.running).length; // final runningCount = running.where((element) => element.running).length;
final stoped = running.length - runningCount; // final stoped = running.length - runningCount;
if (stoped == 0) { // if (stoped == 0) {
return l10n.dockerStatusRunningFmt(runningCount); // return l10n.dockerStatusRunningFmt(runningCount);
} // }
return l10n.dockerStatusRunningAndStoppedFmt(runningCount, stoped); // return l10n.dockerStatusRunningAndStoppedFmt(runningCount, stoped);
} // }
Widget _buildEditHost() { Widget _buildEditHost() {
final children = <Widget>[]; final children = <Widget>[];
@@ -241,9 +300,9 @@ class _ContainerPageState extends State<ContainerPage> {
child: Text(l10n.dockerEditHost), child: Text(l10n.dockerEditHost),
), ),
); );
return Column( return CardX(child: Column(
children: children, children: children,
); ));
} }
Widget _buildSwitchProvider() { Widget _buildSwitchProvider() {
@@ -263,7 +322,7 @@ class _ContainerPageState extends State<ContainerPage> {
child: Text(l10n.switchTo('Podman')), child: Text(l10n.switchTo('Podman')),
); );
} }
return child; return CardX(child: child);
} }
Future<void> _showAddFAB() async { Future<void> _showAddFAB() async {

View File

@@ -222,6 +222,7 @@ class _SettingPageState extends State<SettingPage> {
children: [ children: [
_buildUsePodman(), _buildUsePodman(),
_buildContainerTrySudo(), _buildContainerTrySudo(),
_buildContainerParseStat(),
].map((e) => CardX(child: e)).toList(), ].map((e) => CardX(child: e)).toList(),
); );
} }
@@ -1166,4 +1167,12 @@ class _SettingPageState extends State<SettingPage> {
trailing: StoreSwitch(prop: _setting.keepStatusWhenErr), trailing: StoreSwitch(prop: _setting.keepStatusWhenErr),
); );
} }
Widget _buildContainerParseStat() {
return ListTile(
title: Text(l10n.parseContainerStats),
subtitle: Text(l10n.parseContainerStatsTip, style: UIs.textGrey),
trailing: StoreSwitch(prop: _setting.containerParseStat),
);
}
} }