mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
new: pve (#307)
This commit is contained in:
20
lib/core/extension/duration.dart
Normal file
20
lib/core/extension/duration.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
|
||||
extension DurationX on Duration {
|
||||
String get toStr {
|
||||
final days = inDays;
|
||||
if (days > 0) {
|
||||
return '$days ${l10n.day}';
|
||||
}
|
||||
final hours = inHours % 24;
|
||||
if (hours > 0) {
|
||||
return '$hours ${l10n.hour}';
|
||||
}
|
||||
final minutes = inMinutes % 60;
|
||||
if (minutes > 0) {
|
||||
return '$minutes ${l10n.minute}';
|
||||
}
|
||||
final seconds = inSeconds % 60;
|
||||
return '$seconds ${l10n.second}';
|
||||
}
|
||||
}
|
||||
@@ -33,3 +33,8 @@ extension BigIntX on BigInt {
|
||||
|
||||
String get kb2Str => (this * BigInt.from(1024)).bytes2Str;
|
||||
}
|
||||
|
||||
extension IntX on int {
|
||||
Duration secondsToDuration() => Duration(seconds: this);
|
||||
DateTime get tsToDateTime => DateTime.fromMillisecondsSinceEpoch(this * 1000);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:toolbox/core/extension/listx.dart';
|
||||
import 'package:toolbox/core/persistant_store.dart';
|
||||
|
||||
typedef Order<T> = List<T>;
|
||||
@@ -52,7 +53,7 @@ extension OrderX<T> on Order<T> {
|
||||
move(index, newIndex, property: property, onMove: onMove);
|
||||
}
|
||||
|
||||
/// order: ['d', 'b', 'e']\
|
||||
/// order: ['d', 'b', 'e']
|
||||
/// this: ['a', 'b', 'c', 'd']\
|
||||
/// result: ['d', 'b', 'a', 'c']\
|
||||
/// return: ['e']
|
||||
@@ -64,11 +65,11 @@ extension OrderX<T> on Order<T> {
|
||||
final missed = <T>[];
|
||||
final surplus = <String>[];
|
||||
for (final id in order.toSet()) {
|
||||
try {
|
||||
final item = firstWhere((e) => finder(e, id));
|
||||
newOrder.add(item);
|
||||
} catch (e) {
|
||||
final item = firstWhereOrNull((element) => finder(element, id));
|
||||
if (item == null) {
|
||||
surplus.add(id);
|
||||
} else {
|
||||
newOrder.add(item);
|
||||
}
|
||||
}
|
||||
for (final item in this) {
|
||||
|
||||
7
lib/core/extension/widget.dart
Normal file
7
lib/core/extension/widget.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension WidgetX on Widget {
|
||||
Widget get card {
|
||||
return Card(child: this);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import 'package:toolbox/view/page/iperf.dart';
|
||||
import 'package:toolbox/view/page/ping.dart';
|
||||
import 'package:toolbox/view/page/private_key/edit.dart';
|
||||
import 'package:toolbox/view/page/private_key/list.dart';
|
||||
import 'package:toolbox/view/page/pve.dart';
|
||||
import 'package:toolbox/view/page/server/detail.dart';
|
||||
import 'package:toolbox/view/page/setting/platform/android.dart';
|
||||
import 'package:toolbox/view/page/setting/platform/ios.dart';
|
||||
@@ -227,4 +228,8 @@ class AppRoute {
|
||||
static AppRoute serverFuncBtnsOrder({Key? key}) {
|
||||
return AppRoute(ServerFuncBtnsOrderPage(key: key), 'server_func_btns_seq');
|
||||
}
|
||||
|
||||
static AppRoute pve({Key? key, required ServerPrivateInfo spi}) {
|
||||
return AppRoute(PvePage(key: key, spi: spi), 'pve');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ enum ServerFuncBtn {
|
||||
snippet,
|
||||
@HiveField(6)
|
||||
iperf,
|
||||
@HiveField(7)
|
||||
pve,
|
||||
;
|
||||
|
||||
IconData get icon => switch (this) {
|
||||
@@ -30,6 +32,7 @@ enum ServerFuncBtn {
|
||||
process => Icons.list_alt_outlined,
|
||||
terminal => Icons.terminal,
|
||||
iperf => Icons.speed,
|
||||
pve => Icons.computer,
|
||||
};
|
||||
|
||||
String get toStr => switch (this) {
|
||||
@@ -40,6 +43,7 @@ enum ServerFuncBtn {
|
||||
process => l10n.process,
|
||||
terminal => l10n.terminal,
|
||||
iperf => 'iperf',
|
||||
pve => 'PVE',
|
||||
};
|
||||
|
||||
int toJson() => index;
|
||||
|
||||
@@ -27,6 +27,8 @@ class ServerFuncBtnAdapter extends TypeAdapter<ServerFuncBtn> {
|
||||
return ServerFuncBtn.snippet;
|
||||
case 6:
|
||||
return ServerFuncBtn.iperf;
|
||||
case 7:
|
||||
return ServerFuncBtn.pve;
|
||||
default:
|
||||
return ServerFuncBtn.terminal;
|
||||
}
|
||||
@@ -56,6 +58,9 @@ class ServerFuncBtnAdapter extends TypeAdapter<ServerFuncBtn> {
|
||||
case ServerFuncBtn.iperf:
|
||||
writer.writeByte(6);
|
||||
break;
|
||||
case ServerFuncBtn.pve:
|
||||
writer.writeByte(7);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
36
lib/data/model/server/custom.dart
Normal file
36
lib/data/model/server/custom.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
|
||||
part 'custom.g.dart';
|
||||
|
||||
@HiveType(typeId: 7)
|
||||
final class ServerCustom {
|
||||
@HiveField(0)
|
||||
final String? temperature;
|
||||
@HiveField(1)
|
||||
final String? pveAddr;
|
||||
|
||||
const ServerCustom({
|
||||
this.temperature,
|
||||
this.pveAddr,
|
||||
});
|
||||
|
||||
static ServerCustom fromJson(Map<String, dynamic> json) {
|
||||
final temperature = json["temperature"] as String?;
|
||||
final pveAddr = json["pveAddr"] as String?;
|
||||
return ServerCustom(
|
||||
temperature: temperature,
|
||||
pveAddr: pveAddr,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (temperature != null) {
|
||||
json["temperature"] = temperature;
|
||||
}
|
||||
if (pveAddr != null) {
|
||||
json["pveAddr"] = pveAddr;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
}
|
||||
44
lib/data/model/server/custom.g.dart
Normal file
44
lib/data/model/server/custom.g.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'custom.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class ServerCustomAdapter extends TypeAdapter<ServerCustom> {
|
||||
@override
|
||||
final int typeId = 7;
|
||||
|
||||
@override
|
||||
ServerCustom read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return ServerCustom(
|
||||
temperature: fields[0] as String?,
|
||||
pveAddr: fields[1] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, ServerCustom obj) {
|
||||
writer
|
||||
..writeByte(2)
|
||||
..writeByte(0)
|
||||
..write(obj.temperature)
|
||||
..writeByte(1)
|
||||
..write(obj.pveAddr);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ServerCustomAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
351
lib/data/model/server/pve.dart
Normal file
351
lib/data/model/server/pve.dart
Normal file
@@ -0,0 +1,351 @@
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/duration.dart';
|
||||
import 'package:toolbox/core/extension/numx.dart';
|
||||
|
||||
enum PveResType {
|
||||
lxc,
|
||||
qemu,
|
||||
node,
|
||||
storage,
|
||||
sdn,
|
||||
;
|
||||
|
||||
static PveResType fromString(String type) {
|
||||
switch (type) {
|
||||
case 'lxc':
|
||||
return PveResType.lxc;
|
||||
case 'qemu':
|
||||
return PveResType.qemu;
|
||||
case 'node':
|
||||
return PveResType.node;
|
||||
case 'storage':
|
||||
return PveResType.storage;
|
||||
case 'sdn':
|
||||
return PveResType.sdn;
|
||||
default:
|
||||
throw Exception('Unknown PveResType: $type');
|
||||
}
|
||||
}
|
||||
|
||||
String get toStr => switch (this) {
|
||||
PveResType.node => l10n.node,
|
||||
PveResType.qemu => 'QEMU',
|
||||
PveResType.lxc => 'LXC',
|
||||
PveResType.storage => l10n.storage,
|
||||
PveResType.sdn => 'SDN',
|
||||
};
|
||||
}
|
||||
|
||||
sealed class PveResIface {
|
||||
String get id;
|
||||
String get status;
|
||||
PveResType get type;
|
||||
|
||||
static PveResIface fromJson(Map<String, dynamic> json) {
|
||||
final type = PveResType.fromString(json['type']);
|
||||
switch (type) {
|
||||
case PveResType.lxc:
|
||||
return PveLxc.fromJson(json);
|
||||
case PveResType.qemu:
|
||||
return PveQemu.fromJson(json);
|
||||
case PveResType.node:
|
||||
return PveNode.fromJson(json);
|
||||
case PveResType.storage:
|
||||
return PveStorage.fromJson(json);
|
||||
case PveResType.sdn:
|
||||
return PveSdn.fromJson(json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class PveLxc extends PveResIface {
|
||||
@override
|
||||
final String id;
|
||||
@override
|
||||
final PveResType type;
|
||||
final int vmid;
|
||||
final String node;
|
||||
final String name;
|
||||
@override
|
||||
final String status;
|
||||
final int uptime;
|
||||
final int mem;
|
||||
final int maxmem;
|
||||
final double cpu;
|
||||
final int maxcpu;
|
||||
final int disk;
|
||||
final int maxdisk;
|
||||
final int diskread;
|
||||
final int diskwrite;
|
||||
final int netin;
|
||||
final int netout;
|
||||
|
||||
PveLxc({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.vmid,
|
||||
required this.node,
|
||||
required this.name,
|
||||
required this.status,
|
||||
required this.uptime,
|
||||
required this.mem,
|
||||
required this.maxmem,
|
||||
required this.cpu,
|
||||
required this.maxcpu,
|
||||
required this.disk,
|
||||
required this.maxdisk,
|
||||
required this.diskread,
|
||||
required this.diskwrite,
|
||||
required this.netin,
|
||||
required this.netout,
|
||||
});
|
||||
|
||||
static PveLxc fromJson(Map<String, dynamic> json) {
|
||||
return PveLxc(
|
||||
id: json['id'],
|
||||
type: PveResType.lxc,
|
||||
vmid: json['vmid'],
|
||||
node: json['node'],
|
||||
name: json['name'],
|
||||
status: json['status'],
|
||||
uptime: json['uptime'],
|
||||
mem: json['mem'],
|
||||
maxmem: json['maxmem'],
|
||||
cpu: (json['cpu'] as num).toDouble(),
|
||||
maxcpu: json['maxcpu'],
|
||||
disk: json['disk'],
|
||||
maxdisk: json['maxdisk'],
|
||||
diskread: json['diskread'],
|
||||
diskwrite: json['diskwrite'],
|
||||
netin: json['netin'],
|
||||
netout: json['netout'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class PveQemu extends PveResIface {
|
||||
@override
|
||||
final String id;
|
||||
@override
|
||||
final PveResType type;
|
||||
final int vmid;
|
||||
final String node;
|
||||
final String name;
|
||||
@override
|
||||
final String status;
|
||||
final int uptime;
|
||||
final int mem;
|
||||
final int maxmem;
|
||||
final double cpu;
|
||||
final int maxcpu;
|
||||
final int disk;
|
||||
final int maxdisk;
|
||||
final int diskread;
|
||||
final int diskwrite;
|
||||
final int netin;
|
||||
final int netout;
|
||||
|
||||
PveQemu({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.vmid,
|
||||
required this.node,
|
||||
required this.name,
|
||||
required this.status,
|
||||
required this.uptime,
|
||||
required this.mem,
|
||||
required this.maxmem,
|
||||
required this.cpu,
|
||||
required this.maxcpu,
|
||||
required this.disk,
|
||||
required this.maxdisk,
|
||||
required this.diskread,
|
||||
required this.diskwrite,
|
||||
required this.netin,
|
||||
required this.netout,
|
||||
});
|
||||
|
||||
static PveQemu fromJson(Map<String, dynamic> json) {
|
||||
return PveQemu(
|
||||
id: json['id'],
|
||||
type: PveResType.qemu,
|
||||
vmid: json['vmid'],
|
||||
node: json['node'],
|
||||
name: json['name'],
|
||||
status: json['status'],
|
||||
uptime: json['uptime'],
|
||||
mem: json['mem'],
|
||||
maxmem: json['maxmem'],
|
||||
cpu: (json['cpu'] as num).toDouble(),
|
||||
maxcpu: json['maxcpu'],
|
||||
disk: json['disk'],
|
||||
maxdisk: json['maxdisk'],
|
||||
diskread: json['diskread'],
|
||||
diskwrite: json['diskwrite'],
|
||||
netin: json['netin'],
|
||||
netout: json['netout'],
|
||||
);
|
||||
}
|
||||
|
||||
bool get isRunning => status == 'running';
|
||||
|
||||
String get topRight {
|
||||
if (!isRunning) {
|
||||
return uptime.secondsToDuration().toStr;
|
||||
}
|
||||
return l10n.stopped;
|
||||
}
|
||||
}
|
||||
|
||||
final class PveNode extends PveResIface {
|
||||
@override
|
||||
final String id;
|
||||
@override
|
||||
final PveResType type;
|
||||
final String node;
|
||||
@override
|
||||
final String status;
|
||||
final int uptime;
|
||||
final int mem;
|
||||
final int maxmem;
|
||||
final double cpu;
|
||||
final int maxcpu;
|
||||
|
||||
PveNode({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.node,
|
||||
required this.status,
|
||||
required this.uptime,
|
||||
required this.mem,
|
||||
required this.maxmem,
|
||||
required this.cpu,
|
||||
required this.maxcpu,
|
||||
});
|
||||
|
||||
static PveNode fromJson(Map<String, dynamic> json) {
|
||||
return PveNode(
|
||||
id: json['id'],
|
||||
type: PveResType.node,
|
||||
node: json['node'],
|
||||
status: json['status'],
|
||||
uptime: json['uptime'],
|
||||
mem: json['mem'],
|
||||
maxmem: json['maxmem'],
|
||||
cpu: (json['cpu'] as num).toDouble(),
|
||||
maxcpu: json['maxcpu'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class PveStorage extends PveResIface {
|
||||
@override
|
||||
final String id;
|
||||
@override
|
||||
final PveResType type;
|
||||
final String storage;
|
||||
final String node;
|
||||
@override
|
||||
final String status;
|
||||
final String plugintype;
|
||||
final String content;
|
||||
final int shared;
|
||||
final int disk;
|
||||
final int maxdisk;
|
||||
|
||||
PveStorage({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.storage,
|
||||
required this.node,
|
||||
required this.status,
|
||||
required this.plugintype,
|
||||
required this.content,
|
||||
required this.shared,
|
||||
required this.disk,
|
||||
required this.maxdisk,
|
||||
});
|
||||
|
||||
static PveStorage fromJson(Map<String, dynamic> json) {
|
||||
return PveStorage(
|
||||
id: json['id'],
|
||||
type: PveResType.storage,
|
||||
storage: json['storage'],
|
||||
node: json['node'],
|
||||
status: json['status'],
|
||||
plugintype: json['plugintype'],
|
||||
content: json['content'],
|
||||
shared: json['shared'],
|
||||
disk: json['disk'],
|
||||
maxdisk: json['maxdisk'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class PveSdn extends PveResIface {
|
||||
@override
|
||||
final String id;
|
||||
@override
|
||||
final PveResType type;
|
||||
final String sdn;
|
||||
final String node;
|
||||
@override
|
||||
final String status;
|
||||
|
||||
PveSdn({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.sdn,
|
||||
required this.node,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
static PveSdn fromJson(Map<String, dynamic> json) {
|
||||
return PveSdn(
|
||||
id: json['id'],
|
||||
type: PveResType.sdn,
|
||||
sdn: json['sdn'],
|
||||
node: json['node'],
|
||||
status: json['status'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class PveRes {
|
||||
final List<PveNode> nodes;
|
||||
final List<PveQemu> qemus;
|
||||
final List<PveLxc> lxcs;
|
||||
final List<PveStorage> storages;
|
||||
final List<PveSdn> sdns;
|
||||
|
||||
const PveRes({
|
||||
required this.nodes,
|
||||
required this.qemus,
|
||||
required this.lxcs,
|
||||
required this.storages,
|
||||
required this.sdns,
|
||||
});
|
||||
|
||||
int get length =>
|
||||
qemus.length + lxcs.length + nodes.length + storages.length + sdns.length;
|
||||
|
||||
PveResIface operator [](int index) {
|
||||
if (index < nodes.length) {
|
||||
return nodes[index];
|
||||
}
|
||||
index -= nodes.length;
|
||||
if (index < qemus.length) {
|
||||
return qemus[index];
|
||||
}
|
||||
index -= qemus.length;
|
||||
if (index < lxcs.length) {
|
||||
return lxcs[index];
|
||||
}
|
||||
index -= lxcs.length;
|
||||
if (index < storages.length) {
|
||||
return storages[index];
|
||||
}
|
||||
index -= storages.length;
|
||||
return sdns[index];
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:toolbox/data/model/server/custom.dart';
|
||||
import 'package:toolbox/data/model/server/server.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
|
||||
@@ -6,6 +7,7 @@ import '../app/error.dart';
|
||||
|
||||
part 'server_private_info.g.dart';
|
||||
|
||||
/// In former version, it's called `ServerPrivateInfo`.
|
||||
@HiveType(typeId: 3)
|
||||
class ServerPrivateInfo {
|
||||
@HiveField(0)
|
||||
@@ -33,6 +35,9 @@ class ServerPrivateInfo {
|
||||
@HiveField(9)
|
||||
final String? jumpId;
|
||||
|
||||
@HiveField(10)
|
||||
final ServerCustom? custom;
|
||||
|
||||
final String id;
|
||||
|
||||
const ServerPrivateInfo({
|
||||
@@ -46,6 +51,7 @@ class ServerPrivateInfo {
|
||||
this.alterUrl,
|
||||
this.autoConnect,
|
||||
this.jumpId,
|
||||
this.custom,
|
||||
}) : id = '$user@$ip:$port';
|
||||
|
||||
static ServerPrivateInfo fromJson(Map<String, dynamic> json) {
|
||||
@@ -59,6 +65,9 @@ class ServerPrivateInfo {
|
||||
final alterUrl = json["alterUrl"] as String?;
|
||||
final autoConnect = json["autoConnect"] as bool?;
|
||||
final jumpId = json["jumpId"] as String?;
|
||||
final custom = json["customCmd"] == null
|
||||
? null
|
||||
: ServerCustom.fromJson(json["custom"].cast<String, dynamic>());
|
||||
|
||||
return ServerPrivateInfo(
|
||||
name: name,
|
||||
@@ -71,6 +80,7 @@ class ServerPrivateInfo {
|
||||
alterUrl: alterUrl,
|
||||
autoConnect: autoConnect,
|
||||
jumpId: jumpId,
|
||||
custom: custom,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -80,12 +90,27 @@ class ServerPrivateInfo {
|
||||
data["ip"] = ip;
|
||||
data["port"] = port;
|
||||
data["user"] = user;
|
||||
data["authorization"] = pwd;
|
||||
data["pubKeyId"] = keyId;
|
||||
data["tags"] = tags;
|
||||
data["alterUrl"] = alterUrl;
|
||||
data["autoConnect"] = autoConnect;
|
||||
data["jumpId"] = jumpId;
|
||||
if (pwd != null) {
|
||||
data["authorization"] = pwd;
|
||||
}
|
||||
if (keyId != null) {
|
||||
data["pubKeyId"] = keyId;
|
||||
}
|
||||
if (tags != null) {
|
||||
data["tags"] = tags;
|
||||
}
|
||||
if (alterUrl != null) {
|
||||
data["alterUrl"] = alterUrl;
|
||||
}
|
||||
if (autoConnect != null) {
|
||||
data["autoConnect"] = autoConnect;
|
||||
}
|
||||
if (jumpId != null) {
|
||||
data["jumpId"] = jumpId;
|
||||
}
|
||||
if (custom != null) {
|
||||
data["custom"] = custom?.toJson();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,13 +27,14 @@ class ServerPrivateInfoAdapter extends TypeAdapter<ServerPrivateInfo> {
|
||||
alterUrl: fields[7] as String?,
|
||||
autoConnect: fields[8] as bool?,
|
||||
jumpId: fields[9] as String?,
|
||||
custom: fields[10] as ServerCustom?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, ServerPrivateInfo obj) {
|
||||
writer
|
||||
..writeByte(10)
|
||||
..writeByte(11)
|
||||
..writeByte(0)
|
||||
..write(obj.name)
|
||||
..writeByte(1)
|
||||
@@ -53,7 +54,9 @@ class ServerPrivateInfoAdapter extends TypeAdapter<ServerPrivateInfo> {
|
||||
..writeByte(8)
|
||||
..write(obj.autoConnect)
|
||||
..writeByte(9)
|
||||
..write(obj.jumpId);
|
||||
..write(obj.jumpId)
|
||||
..writeByte(10)
|
||||
..write(obj.custom);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
112
lib/data/provider/pve.dart
Normal file
112
lib/data/provider/pve.dart
Normal file
@@ -0,0 +1,112 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/data/model/server/pve.dart';
|
||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
|
||||
final class PveProvider extends ChangeNotifier {
|
||||
final ServerPrivateInfo spi;
|
||||
late final String addr;
|
||||
//late final SSHClient _client;
|
||||
|
||||
PveProvider({
|
||||
required this.spi,
|
||||
}) {
|
||||
// final client = _spi.server?.client;
|
||||
// if (client == null) {
|
||||
// throw Exception('Server client is null');
|
||||
// }
|
||||
// _client = client;
|
||||
final addr = spi.custom?.pveAddr;
|
||||
if (addr == null) {
|
||||
err.value = 'PVE address is null';
|
||||
return;
|
||||
}
|
||||
this.addr = addr;
|
||||
_init().then((_) => connected.complete());
|
||||
}
|
||||
|
||||
final err = ValueNotifier<String?>(null);
|
||||
final connected = Completer<void>();
|
||||
final session = Dio();
|
||||
|
||||
// int _localPort = 0;
|
||||
// String get addr => 'http://127.0.0.1:$_localPort';
|
||||
|
||||
Future<void> _init() async {
|
||||
//await _forward();
|
||||
await _login();
|
||||
}
|
||||
|
||||
// Future<void> _forward() async {
|
||||
// var retries = 0;
|
||||
// while (retries < 3) {
|
||||
// try {
|
||||
// _localPort = Random().nextInt(1000) + 37000;
|
||||
// print('Forwarding local port $_localPort');
|
||||
// final serverSocket = await ServerSocket.bind('localhost', _localPort);
|
||||
// final forward = await _client.forwardLocal('127.0.0.1', 8006);
|
||||
// serverSocket.listen((socket) {
|
||||
// forward.stream.cast<List<int>>().pipe(socket);
|
||||
// socket.pipe(forward.sink);
|
||||
// });
|
||||
// return;
|
||||
// } on SocketException {
|
||||
// retries++;
|
||||
// }
|
||||
// }
|
||||
// throw Exception('Failed to bind local port');
|
||||
// }
|
||||
|
||||
Future<void> _login() async {
|
||||
final resp = await session.post('$addr/api2/extjs/access/ticket', data: {
|
||||
'username': spi.user,
|
||||
'password': spi.pwd,
|
||||
'realm': 'pam',
|
||||
'new-format': '1'
|
||||
});
|
||||
final ticket = resp.data['data']['ticket'];
|
||||
session.options.headers['CSRFPreventionToken'] =
|
||||
resp.data['data']['CSRFPreventionToken'];
|
||||
session.options.headers['Cookie'] = 'PVEAuthCookie=$ticket';
|
||||
}
|
||||
|
||||
Future<PveRes> list() async {
|
||||
await connected.future;
|
||||
final resp = await session.get('$addr/api2/json/cluster/resources');
|
||||
final list = resp.data['data'] as List;
|
||||
final items = list.map((e) => PveResIface.fromJson(e)).toList();
|
||||
final qemus = <PveQemu>[];
|
||||
final lxcs = <PveLxc>[];
|
||||
final nodes = <PveNode>[];
|
||||
final storages = <PveStorage>[];
|
||||
final sdns = <PveSdn>[];
|
||||
for (final item in items) {
|
||||
switch (item.type) {
|
||||
case PveResType.lxc:
|
||||
lxcs.add(item as PveLxc);
|
||||
break;
|
||||
case PveResType.qemu:
|
||||
qemus.add(item as PveQemu);
|
||||
break;
|
||||
case PveResType.node:
|
||||
nodes.add(item as PveNode);
|
||||
break;
|
||||
case PveResType.storage:
|
||||
storages.add(item as PveStorage);
|
||||
break;
|
||||
case PveResType.sdn:
|
||||
sdns.add(item as PveSdn);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return PveRes(
|
||||
qemus: qemus,
|
||||
lxcs: lxcs,
|
||||
nodes: nodes,
|
||||
storages: storages,
|
||||
sdns: sdns,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
"addPrivateKey": "Private key hinzufügen",
|
||||
"addSystemPrivateKeyTip": "Derzeit haben Sie keinen privaten Schlüssel, fügen Sie den Schlüssel hinzu, der mit dem System geliefert wird (~/.ssh/id_rsa)?",
|
||||
"added2List": "Zur Aufgabenliste hinzugefügt",
|
||||
"addr": "Adresse",
|
||||
"all": "Alle",
|
||||
"alreadyLastDir": "Bereits im letzten Verzeichnis.",
|
||||
"alterUrl": "Url ändern",
|
||||
@@ -54,6 +55,7 @@
|
||||
"createFolder": "Ordner erstellen",
|
||||
"cursorType": "Cursor-Typ",
|
||||
"dark": "Dunkel",
|
||||
"day": "Tag",
|
||||
"debug": "Debug",
|
||||
"decode": "Decode",
|
||||
"decompress": "Dekomprimieren",
|
||||
@@ -109,6 +111,7 @@
|
||||
"highlight": "Code highlight",
|
||||
"homeWidgetUrlConfig": "Home-Widget-Link konfigurieren",
|
||||
"host": "Host",
|
||||
"hour": "Stunde",
|
||||
"httpFailedWithCode": "Anfrage fehlgeschlagen, Statuscode: {code}",
|
||||
"icloudSynced": "iCloud wird synchronisiert und einige Einstellungen erfordern möglicherweise einen Neustart der App, um wirksam zu werden.",
|
||||
"image": "Image",
|
||||
@@ -143,6 +146,7 @@
|
||||
"maxRetryCount": "Anzahl an Verbindungsversuchen",
|
||||
"maxRetryCountEqual0": "Unbegrenzte Verbindungsversuche zum Server",
|
||||
"min": "min",
|
||||
"minute": "Minute",
|
||||
"mission": "Mission",
|
||||
"more": "Mehr",
|
||||
"moveOutServerFuncBtnsHelp": "Ein: kann unter jeder Karte auf der Registerkarte \"Server\" angezeigt werden. Aus: kann oben auf der Seite \"Serverdetails\" angezeigt werden.",
|
||||
@@ -161,6 +165,7 @@
|
||||
"noServerAvailable": "Kein Server verfügbar.",
|
||||
"noTask": "Nicht fragen",
|
||||
"noUpdateAvailable": "Kein Update verfügbar",
|
||||
"node": "Knoten",
|
||||
"notSelected": "Nicht ausgewählt",
|
||||
"note": "Hinweis",
|
||||
"nullToken": "Null token",
|
||||
@@ -237,6 +242,7 @@
|
||||
"stats": "Statistik",
|
||||
"stop": "Stop",
|
||||
"stopped": "Ausgelaufen",
|
||||
"storage": "Speicher",
|
||||
"success": "Erfolgreich",
|
||||
"supportFmtArgs": "Die folgenden Formatierungsparameter werden unterstützt:",
|
||||
"suspend": "Suspend",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"addPrivateKey": "Add private key",
|
||||
"addSystemPrivateKeyTip": "Currently don't have any private key, do you add the one that comes with the system (~/.ssh/id_rsa)?",
|
||||
"added2List": "Added to task list",
|
||||
"addr": "Address",
|
||||
"all": "All",
|
||||
"alreadyLastDir": "Already in last directory.",
|
||||
"alterUrl": "Alter url",
|
||||
@@ -54,6 +55,7 @@
|
||||
"createFolder": "Create folder",
|
||||
"cursorType": "Cursor type",
|
||||
"dark": "Dark",
|
||||
"day": "Day",
|
||||
"debug": "Debug",
|
||||
"decode": "Decode",
|
||||
"decompress": "Decompress",
|
||||
@@ -109,6 +111,7 @@
|
||||
"highlight": "Code highlight",
|
||||
"homeWidgetUrlConfig": "Config home widget url",
|
||||
"host": "Host",
|
||||
"hour": "Hour",
|
||||
"httpFailedWithCode": "request failed, status code: {code}",
|
||||
"icloudSynced": "iCloud wird synchronisiert und einige Einstellungen erfordern möglicherweise einen Neustart der App, um wirksam zu werden.",
|
||||
"image": "Image",
|
||||
@@ -143,6 +146,7 @@
|
||||
"maxRetryCount": "Number of server reconnection",
|
||||
"maxRetryCountEqual0": "Will retry again and again.",
|
||||
"min": "min",
|
||||
"minute": "Minute",
|
||||
"mission": "Mission",
|
||||
"more": "More",
|
||||
"moveOutServerFuncBtnsHelp": "On: can be displayed below each card on the Server Tab page. Off: can be displayed at the top of the Server Details page.",
|
||||
@@ -161,6 +165,7 @@
|
||||
"noServerAvailable": "No server available.",
|
||||
"noTask": "No task",
|
||||
"noUpdateAvailable": "No update available",
|
||||
"node": "Node",
|
||||
"notSelected": "Not selected",
|
||||
"note": "Note",
|
||||
"nullToken": "Null token",
|
||||
@@ -237,6 +242,7 @@
|
||||
"stats": "Stats",
|
||||
"stop": "Stop",
|
||||
"stopped": "Stopped",
|
||||
"storage": "Storage",
|
||||
"success": "Success",
|
||||
"supportFmtArgs": "The following formatting parameters are supported:",
|
||||
"suspend": "Suspend",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"addPrivateKey": "Agregar una llave privada",
|
||||
"addSystemPrivateKeyTip": "Actualmente no hay ninguna llave privada, ¿quieres agregar la que viene por defecto en el sistema (~/.ssh/id_rsa)?",
|
||||
"added2List": "Añadido a la lista de tareas",
|
||||
"addr": "Dirección",
|
||||
"all": "Todos",
|
||||
"alreadyLastDir": "Ya estás en el directorio superior",
|
||||
"alterUrl": "URL alternativa",
|
||||
@@ -54,6 +55,7 @@
|
||||
"createFolder": "Crear carpeta",
|
||||
"cursorType": "Tipo de cursor",
|
||||
"dark": "Oscuro",
|
||||
"day": "Día",
|
||||
"debug": "Depurar",
|
||||
"decode": "Decodificar",
|
||||
"decompress": "Descomprimir",
|
||||
@@ -109,6 +111,7 @@
|
||||
"highlight": "Resaltar código",
|
||||
"homeWidgetUrlConfig": "Configuración de URL del widget de inicio",
|
||||
"host": "Anfitrión",
|
||||
"hour": "Hora",
|
||||
"httpFailedWithCode": "Fallo en la solicitud, código de estado: {code}",
|
||||
"icloudSynced": "iCloud sincronizado, algunos ajustes pueden requerir reiniciar para tomar efecto.",
|
||||
"image": "Imagen",
|
||||
@@ -143,6 +146,7 @@
|
||||
"maxRetryCount": "Número máximo de reintentos de conexión al servidor",
|
||||
"maxRetryCountEqual0": "Reintentará infinitamente",
|
||||
"min": "Mínimo",
|
||||
"minute": "Minuto",
|
||||
"mission": "Misión",
|
||||
"more": "Más",
|
||||
"moveOutServerFuncBtnsHelp": "Activado: se mostrará debajo de cada tarjeta en la página de servidores. Desactivado: se mostrará en la parte superior de los detalles del servidor.",
|
||||
@@ -161,6 +165,7 @@
|
||||
"noServerAvailable": "No hay servidores disponibles.",
|
||||
"noTask": "Sin tareas",
|
||||
"noUpdateAvailable": "No hay actualizaciones disponibles",
|
||||
"node": "Nodo",
|
||||
"notSelected": "No seleccionado",
|
||||
"note": "Nota",
|
||||
"nullToken": "Token nulo",
|
||||
@@ -237,6 +242,7 @@
|
||||
"stats": "Estadísticas",
|
||||
"stop": "Detener",
|
||||
"stopped": "Detenido",
|
||||
"storage": "Almacenamiento",
|
||||
"success": "Éxito",
|
||||
"supportFmtArgs": "Soporta los siguientes argumentos de formato:",
|
||||
"suspend": "Suspender",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"addPrivateKey": "Ajouter une clé privée",
|
||||
"addSystemPrivateKeyTip": "Actuellement, vous n'avez aucune clé privée. Voulez-vous ajouter celle qui accompagne le système (~/.ssh/id_rsa)?",
|
||||
"added2List": "Ajouté à la liste des tâches",
|
||||
"addr": "Adresse",
|
||||
"all": "Tous",
|
||||
"alreadyLastDir": "Déjà dans le dernier répertoire.",
|
||||
"alterUrl": "Modifier l'URL",
|
||||
@@ -54,6 +55,7 @@
|
||||
"createFolder": "Créer un dossier",
|
||||
"cursorType": "Type de curseur",
|
||||
"dark": "Sombre",
|
||||
"day": "Jour",
|
||||
"debug": "Déboguer",
|
||||
"decode": "Décoder",
|
||||
"decompress": "Décompresser",
|
||||
@@ -109,6 +111,7 @@
|
||||
"highlight": "Coloration syntaxique",
|
||||
"homeWidgetUrlConfig": "Configurer l'URL du widget d'accueil",
|
||||
"host": "Hôte",
|
||||
"hour": "Heure",
|
||||
"httpFailedWithCode": "requête échouée, code d'état : {code}",
|
||||
"icloudSynced": "iCloud est synchronisé et certaines options peuvent nécessiter un redémarrage de l'application pour être effectives.",
|
||||
"image": "Image",
|
||||
@@ -143,6 +146,7 @@
|
||||
"maxRetryCount": "Nombre de reconnexions du serveur",
|
||||
"maxRetryCountEqual0": "Réessayera encore et encore.",
|
||||
"min": "min",
|
||||
"minute": "Minute",
|
||||
"mission": "Mission",
|
||||
"more": "Plus",
|
||||
"moveOutServerFuncBtnsHelp": "Activé : peut être affiché sous chaque carte sur la page de l'onglet Serveur. Désactivé : peut être affiché en haut de la page Détails du serveur.",
|
||||
@@ -161,6 +165,7 @@
|
||||
"noServerAvailable": "Aucun serveur disponible.",
|
||||
"noTask": "Aucune tâche",
|
||||
"noUpdateAvailable": "Aucune mise à jour disponible",
|
||||
"node": "Noeud",
|
||||
"notSelected": "Non sélectionné",
|
||||
"note": "Note",
|
||||
"nullToken": "Jeton nul",
|
||||
@@ -237,6 +242,7 @@
|
||||
"stats": "Statistiques",
|
||||
"stop": "Arrêter",
|
||||
"stopped": "interrompue",
|
||||
"storage": "Stockage",
|
||||
"success": "Succès",
|
||||
"supportFmtArgs": "Les paramètres de formatage suivants sont pris en charge:",
|
||||
"suspend": "Suspendre",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"addPrivateKey": "Tambahkan kunci pribadi",
|
||||
"addSystemPrivateKeyTip": "Saat ini tidak memiliki kunci privat, apakah Anda menambahkan kunci yang disertakan dengan sistem (~/.ssh/id_rsa)?",
|
||||
"added2List": "Ditambahkan ke Daftar Tugas",
|
||||
"addr": "Alamat",
|
||||
"all": "Semua",
|
||||
"alreadyLastDir": "Sudah di direktori terakhir.",
|
||||
"alterUrl": "Alter url",
|
||||
@@ -54,6 +55,7 @@
|
||||
"createFolder": "Membuat folder",
|
||||
"cursorType": "Jenis kursor",
|
||||
"dark": "Gelap",
|
||||
"day": "Hari",
|
||||
"debug": "Debug",
|
||||
"decode": "Membaca sandi",
|
||||
"decompress": "Dekompresi",
|
||||
@@ -109,6 +111,7 @@
|
||||
"highlight": "Sorotan kode",
|
||||
"homeWidgetUrlConfig": "Konfigurasi URL Widget Rumah",
|
||||
"host": "Host",
|
||||
"hour": "Jam",
|
||||
"httpFailedWithCode": "Permintaan gagal, kode status: {code}",
|
||||
"icloudSynced": "iCloud disinkronkan dan beberapa pengaturan mungkin memerlukan pengaktifan ulang aplikasi agar dapat diterapkan.",
|
||||
"image": "Gambar",
|
||||
@@ -143,6 +146,7 @@
|
||||
"maxRetryCount": "Jumlah penyambungan kembali server",
|
||||
"maxRetryCountEqual0": "Akan mencoba lagi lagi dan lagi.",
|
||||
"min": "Min",
|
||||
"minute": "Menit",
|
||||
"mission": "Misi",
|
||||
"more": "Lebih Banyak",
|
||||
"moveOutServerFuncBtnsHelp": "Aktif: dapat ditampilkan di bawah setiap kartu pada halaman Tab Server. Nonaktif: dapat ditampilkan di bagian atas halaman Rincian Server.",
|
||||
@@ -161,6 +165,7 @@
|
||||
"noServerAvailable": "Tidak ada server yang tersedia.",
|
||||
"noTask": "Tidak bertanya",
|
||||
"noUpdateAvailable": "Tidak ada pembaruan yang tersedia",
|
||||
"node": "Node",
|
||||
"notSelected": "Tidak terpilih",
|
||||
"note": "Catatan",
|
||||
"nullToken": "Token NULL",
|
||||
@@ -237,6 +242,7 @@
|
||||
"stats": "Statistik",
|
||||
"stop": "Berhenti",
|
||||
"stopped": "dihentikan",
|
||||
"storage": "Penyimpanan",
|
||||
"success": "Kesuksesan",
|
||||
"supportFmtArgs": "Parameter pemformatan berikut ini didukung:",
|
||||
"suspend": "Suspend",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"addPrivateKey": "プライベートキーを追加",
|
||||
"addSystemPrivateKeyTip": "現在プライベートキーがありません。システムのデフォルト(~/.ssh/id_rsa)を追加しますか?",
|
||||
"added2List": "タスクリストに追加されました",
|
||||
"addr": "住所",
|
||||
"all": "すべて",
|
||||
"alreadyLastDir": "すでに最上位のディレクトリです",
|
||||
"alterUrl": "代替リンク",
|
||||
@@ -54,6 +55,7 @@
|
||||
"createFolder": "フォルダーを作成",
|
||||
"cursorType": "カーソルタイプ",
|
||||
"dark": "ダーク",
|
||||
"day": "日",
|
||||
"debug": "デバッグ",
|
||||
"decode": "デコード",
|
||||
"decompress": "解凍",
|
||||
@@ -109,6 +111,7 @@
|
||||
"highlight": "コードハイライト",
|
||||
"homeWidgetUrlConfig": "ホームウィジェットURL設定",
|
||||
"host": "ホスト",
|
||||
"hour": "時間",
|
||||
"httpFailedWithCode": "リクエスト失敗、ステータスコード: {code}",
|
||||
"icloudSynced": "iCloudが同期されました。一部の設定はアプリを再起動する必要があります。",
|
||||
"image": "イメージ",
|
||||
@@ -143,6 +146,7 @@
|
||||
"maxRetryCount": "サーバーの再接続試行回数",
|
||||
"maxRetryCountEqual0": "無限に再試行します",
|
||||
"min": "最小",
|
||||
"minute": "分",
|
||||
"mission": "ミッション",
|
||||
"more": "もっと",
|
||||
"moveOutServerFuncBtnsHelp": "有効にする:サーバータブの各カードの下に表示されます。無効にする:サーバーの詳細ページの上部に表示されます。",
|
||||
@@ -161,6 +165,7 @@
|
||||
"noServerAvailable": "使用可能なサーバーがありません。",
|
||||
"noTask": "タスクがありません",
|
||||
"noUpdateAvailable": "利用可能な更新はありません",
|
||||
"node": "ノード",
|
||||
"notSelected": "選択されていません",
|
||||
"note": "メモ",
|
||||
"nullToken": "トークンなし",
|
||||
@@ -237,6 +242,7 @@
|
||||
"stats": "統計",
|
||||
"stop": "停止",
|
||||
"stopped": "停止しました",
|
||||
"storage": "ストレージ",
|
||||
"success": "成功",
|
||||
"supportFmtArgs": "以下のフォーマット引数がサポートされています:",
|
||||
"suspend": "中断",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"addPrivateKey": "Adicionar uma chave privada",
|
||||
"addSystemPrivateKeyTip": "Atualmente, não há nenhuma chave privada. Gostaria de adicionar a chave do sistema (~/.ssh/id_rsa)?",
|
||||
"added2List": "Adicionado à lista de tarefas",
|
||||
"addr": "Endereço",
|
||||
"all": "Todos",
|
||||
"alreadyLastDir": "Já é o diretório mais alto",
|
||||
"alterUrl": "URL alternativa",
|
||||
@@ -54,6 +55,7 @@
|
||||
"createFolder": "Criar pasta",
|
||||
"cursorType": "Tipo de cursor",
|
||||
"dark": "Escuro",
|
||||
"day": "Dia",
|
||||
"debug": "Debugar",
|
||||
"decode": "Decodificar",
|
||||
"decompress": "Descomprimir",
|
||||
@@ -109,6 +111,7 @@
|
||||
"highlight": "Destaque de código",
|
||||
"homeWidgetUrlConfig": "Configuração de URL do widget da tela inicial",
|
||||
"host": "Host",
|
||||
"hour": "Hora",
|
||||
"httpFailedWithCode": "Falha na solicitação, código de status: {code}",
|
||||
"icloudSynced": "iCloud sincronizado, algumas configurações podem precisar de reinicialização do app para serem aplicadas.",
|
||||
"image": "Imagem",
|
||||
@@ -143,6 +146,7 @@
|
||||
"maxRetryCount": "Número de tentativas de reconexão com o servidor",
|
||||
"maxRetryCountEqual0": "Irá tentar indefinidamente",
|
||||
"min": "Mínimo",
|
||||
"minute": "Minuto",
|
||||
"mission": "Missão",
|
||||
"more": "Mais",
|
||||
"moveOutServerFuncBtnsHelp": "Ativado: Mostra abaixo de cada cartão na aba do servidor. Desativado: Mostra no topo da página de detalhes do servidor.",
|
||||
@@ -161,6 +165,7 @@
|
||||
"noServerAvailable": "Nenhum servidor disponível.",
|
||||
"noTask": "Sem tarefas",
|
||||
"noUpdateAvailable": "Sem atualizações disponíveis",
|
||||
"node": "Nó",
|
||||
"notSelected": "Não selecionado",
|
||||
"note": "Nota",
|
||||
"nullToken": "Token nulo",
|
||||
@@ -237,6 +242,7 @@
|
||||
"stats": "Estatísticas",
|
||||
"stop": "Parar",
|
||||
"stopped": "Parado",
|
||||
"storage": "Armazenamento",
|
||||
"success": "Sucesso",
|
||||
"supportFmtArgs": "Suporta os seguintes argumentos formatados:",
|
||||
"suspend": "Suspender",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"addPrivateKey": "добавить приватный ключ",
|
||||
"addSystemPrivateKeyTip": "В данный момент приватные ключи отсутствуют. Добавить системный приватный ключ (~/.ssh/id_rsa)?",
|
||||
"added2List": "добавлено в список задач",
|
||||
"addr": "Адрес",
|
||||
"all": "все",
|
||||
"alreadyLastDir": "Уже в корневом каталоге",
|
||||
"alterUrl": "альтернативная ссылка",
|
||||
@@ -54,6 +55,7 @@
|
||||
"createFolder": "создать папку",
|
||||
"cursorType": "Тип курсора",
|
||||
"dark": "темная",
|
||||
"day": "День",
|
||||
"debug": "отладка",
|
||||
"decode": "декодировать",
|
||||
"decompress": "разархивировать",
|
||||
@@ -109,6 +111,7 @@
|
||||
"highlight": "подсветка кода",
|
||||
"homeWidgetUrlConfig": "конфигурация URL виджета домашнего экрана",
|
||||
"host": "хост",
|
||||
"hour": "Час",
|
||||
"httpFailedWithCode": "Ошибка запроса, код: {code}",
|
||||
"icloudSynced": "Синхронизация с iCloud выполнена, некоторые настройки могут потребовать перезапуска приложения для вступления в силу.",
|
||||
"image": "образ",
|
||||
@@ -143,6 +146,7 @@
|
||||
"maxRetryCount": "максимальное количество попыток переподключения к серверу",
|
||||
"maxRetryCountEqual0": "будет бесконечно пытаться переподключиться",
|
||||
"min": "минимум",
|
||||
"minute": "Минута",
|
||||
"mission": "задача",
|
||||
"more": "больше",
|
||||
"moveOutServerFuncBtnsHelp": "Включено: кнопки функций сервера отображаются под каждой карточкой на вкладке сервера. Выключено: отображается в верхней части страницы деталей сервера.",
|
||||
@@ -161,6 +165,7 @@
|
||||
"noServerAvailable": "Нет доступных серверов.",
|
||||
"noTask": "нет задач",
|
||||
"noUpdateAvailable": "нет доступных обновлений",
|
||||
"node": "Узел",
|
||||
"notSelected": "не выбрано",
|
||||
"note": "заметка",
|
||||
"nullToken": "нет токена",
|
||||
@@ -237,6 +242,7 @@
|
||||
"stats": "статистика",
|
||||
"stop": "остановить",
|
||||
"stopped": "остановлено",
|
||||
"storage": "Хранение",
|
||||
"success": "успех",
|
||||
"supportFmtArgs": "Поддерживаются следующие форматы аргументов:",
|
||||
"suspend": "приостановить",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"addPrivateKey": "添加一个私钥",
|
||||
"addSystemPrivateKeyTip": "当前没有任何私钥,是否添加系统自带的(~/.ssh/id_rsa)?",
|
||||
"added2List": "已添加至任务列表",
|
||||
"addr": "地址",
|
||||
"all": "所有",
|
||||
"alreadyLastDir": "已经是最上层目录了",
|
||||
"alterUrl": "备选链接",
|
||||
@@ -54,6 +55,7 @@
|
||||
"createFolder": "创建文件夹",
|
||||
"cursorType": "光标类型",
|
||||
"dark": "暗",
|
||||
"day": "天",
|
||||
"debug": "调试",
|
||||
"decode": "解码",
|
||||
"decompress": "解压缩",
|
||||
@@ -109,6 +111,7 @@
|
||||
"highlight": "代码高亮",
|
||||
"homeWidgetUrlConfig": "桌面部件链接配置",
|
||||
"host": "主机",
|
||||
"hour": "小时",
|
||||
"httpFailedWithCode": "请求失败, 状态码: {code}",
|
||||
"icloudSynced": "iCloud已同步,某些设置可能需要重启才能生效。",
|
||||
"image": "镜像",
|
||||
@@ -143,6 +146,7 @@
|
||||
"maxRetryCount": "服务器尝试重连次数",
|
||||
"maxRetryCountEqual0": "会无限重试",
|
||||
"min": "最小",
|
||||
"minute": "分钟",
|
||||
"mission": "任务",
|
||||
"more": "更多",
|
||||
"moveOutServerFuncBtnsHelp": "开启:可以在服务器 Tab 页的每个卡片下方显示。关闭:在服务器详情页顶部显示。",
|
||||
@@ -161,6 +165,7 @@
|
||||
"noServerAvailable": "没有可用的服务器。",
|
||||
"noTask": "没有任务",
|
||||
"noUpdateAvailable": "没有可用更新",
|
||||
"node": "节点",
|
||||
"notSelected": "未选择",
|
||||
"note": "备注",
|
||||
"nullToken": "无Token",
|
||||
@@ -237,6 +242,7 @@
|
||||
"stats": "统计",
|
||||
"stop": "停止",
|
||||
"stopped": "已停止",
|
||||
"storage": "存储",
|
||||
"success": "成功",
|
||||
"supportFmtArgs": "支持以下格式化参数:",
|
||||
"suspend": "挂起",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"addPrivateKey": "新增一個私鑰",
|
||||
"addSystemPrivateKeyTip": "當前沒有任何私鑰,是否添加系統自帶的(~/.ssh/id_rsa)?",
|
||||
"added2List": "已添加至任務列表",
|
||||
"addr": "地址",
|
||||
"all": "所有",
|
||||
"alreadyLastDir": "已經是最上層目錄了",
|
||||
"alterUrl": "備選鏈接",
|
||||
@@ -54,6 +55,7 @@
|
||||
"createFolder": "創建文件夾",
|
||||
"cursorType": "光標類型",
|
||||
"dark": "暗",
|
||||
"day": "日",
|
||||
"debug": "調試",
|
||||
"decode": "解碼",
|
||||
"decompress": "解壓縮",
|
||||
@@ -109,6 +111,7 @@
|
||||
"highlight": "代碼高亮",
|
||||
"homeWidgetUrlConfig": "桌面部件鏈接配置",
|
||||
"host": "主機",
|
||||
"hour": "小時",
|
||||
"httpFailedWithCode": "請求失敗, 狀態碼: {code}",
|
||||
"icloudSynced": "iCloud已同步,某些設置可能需要重啟才能生效。",
|
||||
"image": "鏡像",
|
||||
@@ -143,6 +146,7 @@
|
||||
"maxRetryCount": "服務器嘗試重連次數",
|
||||
"maxRetryCountEqual0": "會無限重試",
|
||||
"min": "最小",
|
||||
"minute": "分鐘",
|
||||
"mission": "任務",
|
||||
"more": "更多",
|
||||
"moveOutServerFuncBtnsHelp": "開啟:可以在服務器 Tab 頁的每個卡片下方顯示。關閉:在服務器詳情頁頂部顯示。",
|
||||
@@ -161,6 +165,7 @@
|
||||
"noServerAvailable": "沒有可用的服務器。",
|
||||
"noTask": "沒有任務",
|
||||
"noUpdateAvailable": "沒有可用更新",
|
||||
"node": "節點",
|
||||
"notSelected": "未選擇",
|
||||
"note": "備註",
|
||||
"nullToken": "無Token",
|
||||
@@ -237,6 +242,7 @@
|
||||
"stats": "統計",
|
||||
"stop": "停止",
|
||||
"stopped": "已停止",
|
||||
"storage": "存儲",
|
||||
"success": "成功",
|
||||
"supportFmtArgs": "支援以下格式化參數:",
|
||||
"suspend": "挂起",
|
||||
|
||||
@@ -15,6 +15,7 @@ import 'package:toolbox/core/utils/platform/base.dart';
|
||||
import 'package:toolbox/core/utils/sync/webdav.dart';
|
||||
import 'package:toolbox/core/utils/ui.dart';
|
||||
import 'package:toolbox/data/model/app/menu/server_func.dart';
|
||||
import 'package:toolbox/data/model/server/custom.dart';
|
||||
import 'package:toolbox/data/res/logger.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
@@ -118,6 +119,7 @@ Future<void> _initDb() async {
|
||||
Hive.registerAdapter(VirtKeyAdapter()); // 4
|
||||
Hive.registerAdapter(NetViewTypeAdapter()); // 5
|
||||
Hive.registerAdapter(ServerFuncBtnAdapter()); // 6
|
||||
Hive.registerAdapter(ServerCustomAdapter()); // 7
|
||||
}
|
||||
|
||||
void _setupLogger() {
|
||||
|
||||
@@ -223,22 +223,22 @@ class BackupPage extends StatelessWidget {
|
||||
|
||||
Widget _buildBulkImportServers(BuildContext context) {
|
||||
return CardX(
|
||||
child: ListTile(
|
||||
title: Text(l10n.bulkImportServers),
|
||||
subtitle: MarkdownBody(
|
||||
data: l10n.bulkImportServersTip(Urls.appWiki),
|
||||
styleSheet: MarkdownStyleSheet(
|
||||
child: ListTile(
|
||||
title: Text(l10n.bulkImportServers),
|
||||
subtitle: MarkdownBody(
|
||||
data: l10n.bulkImportServersTip(Urls.appWiki),
|
||||
styleSheet: MarkdownStyleSheet(
|
||||
p: UIs.textGrey,
|
||||
a: TextStyle(
|
||||
color: primaryColor,
|
||||
)),
|
||||
onTapLink: (text, href, title) {
|
||||
if (href != null) openUrl(href);
|
||||
},
|
||||
a: TextStyle(color: primaryColor),
|
||||
),
|
||||
onTapLink: (text, href, title) {
|
||||
if (href != null) openUrl(href);
|
||||
},
|
||||
),
|
||||
leading: const Icon(Icons.import_export),
|
||||
onTap: () => _onBulkImportServers(context),
|
||||
),
|
||||
trailing: const Icon(Icons.import_export),
|
||||
onTap: () => _onBulkImportServers(context),
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onTapFileRestore(BuildContext context) async {
|
||||
|
||||
197
lib/view/page/pve.dart
Normal file
197
lib/view/page/pve.dart
Normal file
@@ -0,0 +1,197 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/numx.dart';
|
||||
import 'package:toolbox/core/extension/widget.dart';
|
||||
import 'package:toolbox/data/model/server/pve.dart';
|
||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
import 'package:toolbox/data/provider/pve.dart';
|
||||
import 'package:toolbox/data/res/ui.dart';
|
||||
import 'package:toolbox/view/widget/appbar.dart';
|
||||
import 'package:toolbox/view/widget/future_widget.dart';
|
||||
import 'package:toolbox/view/widget/percent_circle.dart';
|
||||
|
||||
final class PvePage extends StatefulWidget {
|
||||
final ServerPrivateInfo spi;
|
||||
|
||||
const PvePage({
|
||||
super.key,
|
||||
required this.spi,
|
||||
});
|
||||
|
||||
@override
|
||||
_PvePageState createState() => _PvePageState();
|
||||
}
|
||||
|
||||
const _kHorziPadding = 11.0;
|
||||
|
||||
final class _PvePageState extends State<PvePage> {
|
||||
late final pve = PveProvider(spi: widget.spi);
|
||||
late MediaQueryData _media;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_media = MediaQuery.of(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: const CustomAppBar(
|
||||
title: Text('PVE'),
|
||||
),
|
||||
body: _buildBody(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
if (pve.err.value != null) {
|
||||
return Center(
|
||||
child: Text('Failed to connect to PVE: ${pve.err.value}'),
|
||||
);
|
||||
}
|
||||
return FutureWidget(
|
||||
future: pve.list(),
|
||||
error: (e, _) {
|
||||
return Center(
|
||||
child: Text('Failed to list PVE: $e'),
|
||||
);
|
||||
},
|
||||
loading: UIs.centerLoading,
|
||||
success: (data) {
|
||||
if (data == null) {
|
||||
return const Center(
|
||||
child: Text('No PVE Resource found'),
|
||||
);
|
||||
}
|
||||
|
||||
PveResType? lastType;
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: _kHorziPadding,
|
||||
vertical: 7,
|
||||
),
|
||||
itemCount: data.length + 1,
|
||||
separatorBuilder: (context, index) {
|
||||
final type = switch (data[index]) {
|
||||
final PveNode _ => PveResType.node,
|
||||
final PveQemu _ => PveResType.qemu,
|
||||
final PveLxc _ => PveResType.lxc,
|
||||
final PveStorage _ => PveResType.storage,
|
||||
final PveSdn _ => PveResType.sdn,
|
||||
};
|
||||
if (type == lastType) {
|
||||
return UIs.placeholder;
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 7),
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
type.toStr,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) return UIs.placeholder;
|
||||
final item = data[index - 1];
|
||||
switch (item) {
|
||||
case final PveNode item:
|
||||
lastType = PveResType.node;
|
||||
return _buildNode(item);
|
||||
case final PveQemu item:
|
||||
lastType = PveResType.qemu;
|
||||
return _buildQemu(item);
|
||||
case final PveLxc item:
|
||||
lastType = PveResType.lxc;
|
||||
return _buildLxc(item);
|
||||
case final PveStorage item:
|
||||
lastType = PveResType.storage;
|
||||
return _buildStorage(item);
|
||||
case final PveSdn item:
|
||||
lastType = PveResType.sdn;
|
||||
return _buildSdn(item);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQemu(PveQemu item) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(item.name),
|
||||
trailing: Text(item.topRight),
|
||||
),
|
||||
if (item.isRunning) Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_wrap(PercentCircle(percent: (item.cpu / item.maxcpu) * 100), 4),
|
||||
_wrap(PercentCircle(percent: (item.mem / item.maxmem) * 100), 4),
|
||||
_wrap(PercentCircle(percent: (item.disk / item.maxdisk) * 100), 4),
|
||||
_wrap(
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
item.netin.bytes2Str,
|
||||
style: const TextStyle(fontSize: 10, color: Colors.grey),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
item.netout.bytes2Str,
|
||||
style: const TextStyle(fontSize: 10, color: Colors.grey),
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
],
|
||||
),
|
||||
4),
|
||||
],
|
||||
),
|
||||
if (item.isRunning) UIs.height13,
|
||||
],
|
||||
).card;
|
||||
}
|
||||
|
||||
Widget _buildLxc(PveLxc item) {
|
||||
return ListTile(
|
||||
title: Text(item.name),
|
||||
trailing: Text(item.status),
|
||||
).card;
|
||||
}
|
||||
|
||||
Widget _buildNode(PveNode item) {
|
||||
return ListTile(
|
||||
title: Text(item.node),
|
||||
trailing: Text(item.status),
|
||||
).card;
|
||||
}
|
||||
|
||||
Widget _buildStorage(PveStorage item) {
|
||||
return ListTile(
|
||||
title: Text(item.storage),
|
||||
trailing: Text(item.status),
|
||||
).card;
|
||||
}
|
||||
|
||||
Widget _buildSdn(PveSdn item) {
|
||||
return ListTile(
|
||||
title: Text(item.sdn),
|
||||
trailing: Text(item.status),
|
||||
).card;
|
||||
}
|
||||
|
||||
Widget _wrap(Widget child, int count) {
|
||||
return SizedBox(
|
||||
height: (_media.size.width - 2 * _kHorziPadding) / count,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,9 @@ import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/context/snackbar.dart';
|
||||
import 'package:toolbox/data/model/app/shell_func.dart';
|
||||
import 'package:toolbox/data/model/server/custom.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
import 'package:toolbox/view/widget/expand_tile.dart';
|
||||
|
||||
import '../../../core/route.dart';
|
||||
import '../../../data/model/server/private_key_info.dart';
|
||||
@@ -33,6 +35,8 @@ class _ServerEditPageState extends State<ServerEditPage> {
|
||||
final _portController = TextEditingController();
|
||||
final _usernameController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _pveAddrCtrl = TextEditingController();
|
||||
|
||||
final _nameFocus = FocusNode();
|
||||
final _ipFocus = FocusNode();
|
||||
final _alterUrlFocus = FocusNode();
|
||||
@@ -71,6 +75,7 @@ class _ServerEditPageState extends State<ServerEditPage> {
|
||||
_altUrlController.text = spi.alterUrl ?? '';
|
||||
_autoConnect.value = spi.autoConnect ?? true;
|
||||
_jumpServer.value = spi.jumpId;
|
||||
_pveAddrCtrl.text = spi.custom?.pveAddr ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,8 +226,6 @@ class _ServerEditPageState extends State<ServerEditPage> {
|
||||
allTags: [...Pros.server.tags.value],
|
||||
onRenameTag: Pros.server.renameTag,
|
||||
),
|
||||
_buildAuth(),
|
||||
//_buildJumpServer(),
|
||||
ListTile(
|
||||
title: Text(l10n.autoConnect),
|
||||
trailing: ListenableBuilder(
|
||||
@@ -235,6 +238,9 @@ class _ServerEditPageState extends State<ServerEditPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildAuth(),
|
||||
//_buildJumpServer(),
|
||||
_buildPVE(),
|
||||
];
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(17, 17, 17, 47),
|
||||
@@ -329,6 +335,17 @@ class _ServerEditPageState extends State<ServerEditPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPVE() {
|
||||
return ExpandTile(title: const Text('PVE'), children: [
|
||||
Input(
|
||||
controller: _pveAddrCtrl,
|
||||
type: TextInputType.url,
|
||||
label: l10n.addr,
|
||||
hint: 'https://example.com:8006',
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildFAB() {
|
||||
return FloatingActionButton(
|
||||
heroTag: 'server',
|
||||
@@ -428,6 +445,8 @@ class _ServerEditPageState extends State<ServerEditPage> {
|
||||
if (_portController.text.isEmpty) {
|
||||
_portController.text = '22';
|
||||
}
|
||||
final pveAddr = _pveAddrCtrl.text.isEmpty ? null : _pveAddrCtrl.text;
|
||||
final custom = pveAddr == null ? null : ServerCustom(pveAddr: pveAddr);
|
||||
|
||||
final spi = ServerPrivateInfo(
|
||||
name: _nameController.text.isEmpty
|
||||
@@ -444,6 +463,7 @@ class _ServerEditPageState extends State<ServerEditPage> {
|
||||
alterUrl: _altUrlController.text.isEmpty ? null : _altUrlController.text,
|
||||
autoConnect: _autoConnect.value,
|
||||
jumpId: _jumpServer.value,
|
||||
custom: custom,
|
||||
);
|
||||
|
||||
if (widget.spi == null) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:after_layout/after_layout.dart';
|
||||
import 'package:circle_chart/circle_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -16,13 +15,13 @@ import 'package:toolbox/data/model/server/sensors.dart';
|
||||
import 'package:toolbox/data/model/server/try_limiter.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
import 'package:toolbox/view/widget/percent_circle.dart';
|
||||
|
||||
import '../../../core/route.dart';
|
||||
import '../../../data/model/app/net_view.dart';
|
||||
import '../../../data/model/server/server.dart';
|
||||
import '../../../data/model/server/server_private_info.dart';
|
||||
import '../../../data/provider/server.dart';
|
||||
import '../../../data/res/color.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../widget/cardx.dart';
|
||||
import '../../widget/server_func_btns.dart';
|
||||
@@ -312,9 +311,11 @@ class _ServerPageState extends State<ServerPage>
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_wrapWithSizedbox(_buildPercentCircle(ss.cpu.usedPercent()), true),
|
||||
_wrapWithSizedbox(PercentCircle(percent: ss.cpu.usedPercent()), true),
|
||||
_wrapWithSizedbox(
|
||||
_buildPercentCircle(ss.mem.usedPercent * 100), true),
|
||||
PercentCircle(percent: ss.mem.usedPercent * 100),
|
||||
true,
|
||||
),
|
||||
_wrapWithSizedbox(_buildNet(ss, spi.id)),
|
||||
_wrapWithSizedbox(_buildDisk(ss, spi.id)),
|
||||
],
|
||||
@@ -509,29 +510,6 @@ class _ServerPageState extends State<ServerPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPercentCircle(double percent) {
|
||||
if (percent <= 0) percent = 0.01;
|
||||
if (percent >= 100) percent = 99.9;
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
CircleChart(
|
||||
progressColor: primaryColor,
|
||||
progressNumber: percent,
|
||||
maxNumber: 100,
|
||||
width: 57,
|
||||
height: 57,
|
||||
animationDuration: const Duration(milliseconds: 777),
|
||||
),
|
||||
Text(
|
||||
'${percent.toStringAsFixed(1)}%',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 12.7),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
|
||||
41
lib/view/widget/percent_circle.dart
Normal file
41
lib/view/widget/percent_circle.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'package:circle_chart/circle_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/data/res/color.dart';
|
||||
|
||||
final class PercentCircle extends StatelessWidget {
|
||||
final double percent;
|
||||
|
||||
const PercentCircle({
|
||||
super.key,
|
||||
required this.percent,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final percent = switch (this.percent) {
|
||||
0 => 0.01,
|
||||
100 => 99.9,
|
||||
// NaN
|
||||
final val when val.isNaN => 0.01,
|
||||
_ => this.percent,
|
||||
};
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
CircleChart(
|
||||
progressColor: primaryColor,
|
||||
progressNumber: percent,
|
||||
maxNumber: 100,
|
||||
width: 57,
|
||||
height: 57,
|
||||
animationDuration: const Duration(milliseconds: 777),
|
||||
),
|
||||
Text(
|
||||
'${percent.toStringAsFixed(1)}%',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 12.7),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -163,6 +163,12 @@ void _onTapMoreBtns(
|
||||
check: () => _checkClient(context, spi.id),
|
||||
);
|
||||
break;
|
||||
case ServerFuncBtn.pve:
|
||||
AppRoute.pve(spi: spi).checkGo(
|
||||
context: context,
|
||||
check: () => _checkClient(context, spi.id),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
136
test/pve_test.dart
Normal file
136
test/pve_test.dart
Normal file
@@ -0,0 +1,136 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:toolbox/data/model/server/pve.dart';
|
||||
|
||||
const _raw = '''
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"maxmem": 12884901888,
|
||||
"type": "lxc",
|
||||
"cpu": 0.0544631947461575,
|
||||
"netin": 65412250538,
|
||||
"template": 0,
|
||||
"diskread": 324033204224,
|
||||
"maxcpu": 8,
|
||||
"disk": 29767077888,
|
||||
"diskwrite": 707866570752,
|
||||
"node": "pve",
|
||||
"vmid": 100,
|
||||
"mem": 5389254656,
|
||||
"status": "running",
|
||||
"netout": 66898114418,
|
||||
"uptime": 1204757,
|
||||
"id": "lxc/100",
|
||||
"maxdisk": 134145380352,
|
||||
"name": "Jellyfin"
|
||||
},
|
||||
{
|
||||
"vmid": 101,
|
||||
"node": "pve",
|
||||
"uptime": 0,
|
||||
"netout": 0,
|
||||
"status": "stopped",
|
||||
"mem": 0,
|
||||
"id": "qemu/101",
|
||||
"name": "ubuntu",
|
||||
"maxdisk": 137438953472,
|
||||
"maxmem": 6442450944,
|
||||
"cpu": 0,
|
||||
"netin": 0,
|
||||
"type": "qemu",
|
||||
"disk": 0,
|
||||
"diskread": 0,
|
||||
"template": 0,
|
||||
"maxcpu": 8,
|
||||
"diskwrite": 0
|
||||
},
|
||||
{
|
||||
"maxcpu": 4,
|
||||
"template": 0,
|
||||
"diskread": 23287297536,
|
||||
"disk": 0,
|
||||
"diskwrite": 39555984896,
|
||||
"maxmem": 4294967296,
|
||||
"type": "qemu",
|
||||
"netin": 2190678599,
|
||||
"cpu": 0.0516426831961466,
|
||||
"id": "qemu/102",
|
||||
"maxdisk": 0,
|
||||
"name": "win",
|
||||
"node": "pve",
|
||||
"vmid": 102,
|
||||
"mem": 1791827968,
|
||||
"status": "running",
|
||||
"netout": 213292068,
|
||||
"uptime": 1013075
|
||||
},
|
||||
{
|
||||
"maxcpu": 12,
|
||||
"id": "node/pve",
|
||||
"disk": 358415503360,
|
||||
"maxdisk": 998011547648,
|
||||
"cgroup-mode": 2,
|
||||
"node": "pve",
|
||||
"maxmem": 29287632896,
|
||||
"type": "node",
|
||||
"status": "online",
|
||||
"mem": 11522887680,
|
||||
"cpu": 0.0451634094268353,
|
||||
"level": "",
|
||||
"uptime": 1204771
|
||||
},
|
||||
{
|
||||
"id": "storage/pve/DSM",
|
||||
"disk": 1250082226176,
|
||||
"maxdisk": 9909187887104,
|
||||
"storage": "DSM",
|
||||
"node": "pve",
|
||||
"status": "available",
|
||||
"type": "storage",
|
||||
"plugintype": "cifs",
|
||||
"content": "snippets,backup,images,rootdir,vztmpl,iso",
|
||||
"shared": 1
|
||||
},
|
||||
{
|
||||
"type": "storage",
|
||||
"status": "available",
|
||||
"plugintype": "dir",
|
||||
"content": "iso,vztmpl,images,rootdir,backup,snippets",
|
||||
"shared": 0,
|
||||
"node": "pve",
|
||||
"maxdisk": 1967847137280,
|
||||
"storage": "hard",
|
||||
"id": "storage/pve/hard",
|
||||
"disk": 620950544384
|
||||
},
|
||||
{
|
||||
"maxdisk": 998011547648,
|
||||
"storage": "local",
|
||||
"disk": 358415503360,
|
||||
"id": "storage/pve/local",
|
||||
"status": "available",
|
||||
"type": "storage",
|
||||
"plugintype": "dir",
|
||||
"content": "backup,snippets,rootdir,images,vztmpl,iso",
|
||||
"shared": 0,
|
||||
"node": "pve"
|
||||
},
|
||||
{
|
||||
"id": "sdn/pve/localnetwork",
|
||||
"node": "pve",
|
||||
"sdn": "localnetwork",
|
||||
"status": "ok",
|
||||
"type": "sdn"
|
||||
}
|
||||
]
|
||||
}''';
|
||||
|
||||
void main() {
|
||||
test('parse pve', () {
|
||||
final list = json.decode(_raw)['data'] as List;
|
||||
final pveItems = list.map((e) => PveResIface.fromJson(e)).toList();
|
||||
expect(pveItems.length, 8);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user