mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
支持添加删除服务器信息,以在服务器状态页显示CPU、内存等
This commit is contained in:
3
lib/core/extension/stringx.dart
Normal file
3
lib/core/extension/stringx.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
extension StringX on String {
|
||||
int get i => int.parse(this);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/persistant_store.dart';
|
||||
import 'package:toolbox/view/widget/card_dialog.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
void unawaited(Future<void> future) {}
|
||||
@@ -24,19 +26,44 @@ void showSnackBarWithAction(
|
||||
|
||||
Future<bool> openUrl(String url) async {
|
||||
print('openUrl $url');
|
||||
|
||||
if (!await canLaunch(url)) {
|
||||
print('canLaunch false');
|
||||
return false;
|
||||
}
|
||||
|
||||
final ok = await launch(url, forceSafariVC: false);
|
||||
|
||||
if (ok == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
print('launch $url failed');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<T?>? showRoundDialog<T>(
|
||||
BuildContext context, String title, Widget child, List<Widget> actions,
|
||||
{EdgeInsets? padding}) {
|
||||
return showDialog<T>(
|
||||
context: context,
|
||||
builder: (ctx) {
|
||||
return CardDialog(
|
||||
title: Text(title),
|
||||
content: child,
|
||||
actions: actions,
|
||||
padding: padding,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget buildSwitch(BuildContext context, StoreProperty<bool> prop,
|
||||
{Function(bool)? func}) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: prop.listenable(),
|
||||
builder: (context, bool value, widget) {
|
||||
return Switch(
|
||||
value: value,
|
||||
onChanged: (value) {
|
||||
if (func != null) func(value);
|
||||
prop.put(value);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
46
lib/data/model/disk_info.dart
Normal file
46
lib/data/model/disk_info.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
class DiskInfo {
|
||||
/*
|
||||
{
|
||||
"mountPath": "",
|
||||
"mountLocation": "",
|
||||
"usedPercent": 0,
|
||||
"used": "",=
|
||||
"size": "",
|
||||
"avail": ""
|
||||
}
|
||||
*/
|
||||
|
||||
String? mountPath;
|
||||
String? mountLocation;
|
||||
double? usedPercent;
|
||||
String? used;
|
||||
String? size;
|
||||
String? avail;
|
||||
|
||||
DiskInfo({
|
||||
this.mountPath,
|
||||
this.mountLocation,
|
||||
this.usedPercent,
|
||||
this.used,
|
||||
this.size,
|
||||
this.avail,
|
||||
});
|
||||
DiskInfo.fromJson(Map<String, dynamic> json) {
|
||||
mountPath = json["mountPath"]?.toString();
|
||||
mountLocation = json["mountLocation"]?.toString();
|
||||
usedPercent = double.parse(json["usedPercent"]);
|
||||
used = json["used"]?.toString();
|
||||
size = json["size"]?.toString();
|
||||
avail = json["avail"]?.toString();
|
||||
}
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data["mountPath"] = mountPath;
|
||||
data["mountLocation"] = mountLocation;
|
||||
data["usedPercent"] = usedPercent;
|
||||
data["used"] = used;
|
||||
data["size"] = size;
|
||||
data["avail"] = avail;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
57
lib/data/model/server_private_info.dart
Normal file
57
lib/data/model/server_private_info.dart
Normal file
@@ -0,0 +1,57 @@
|
||||
import 'dart:convert';
|
||||
|
||||
///
|
||||
/// Code generated by jsonToDartModel https://ashamp.github.io/jsonToDartModel/
|
||||
///
|
||||
class ServerPrivateInfo {
|
||||
/*
|
||||
{
|
||||
"ip": "",
|
||||
"port": 1,
|
||||
"user": "",
|
||||
"authorization": ""
|
||||
}
|
||||
*/
|
||||
|
||||
String? name;
|
||||
String? ip;
|
||||
int? port;
|
||||
String? user;
|
||||
Object? authorization;
|
||||
|
||||
ServerPrivateInfo({
|
||||
this.name,
|
||||
this.ip,
|
||||
this.port,
|
||||
this.user,
|
||||
this.authorization,
|
||||
});
|
||||
ServerPrivateInfo.fromJson(Map<String, dynamic> json) {
|
||||
name = json["name"]?.toString();
|
||||
ip = json["ip"]?.toString();
|
||||
port = json["port"]?.toInt();
|
||||
user = json["user"]?.toString();
|
||||
authorization = json["authorization"];
|
||||
}
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data["name"] = name;
|
||||
data["ip"] = ip;
|
||||
data["port"] = port;
|
||||
data["user"] = user;
|
||||
data["authorization"] = authorization;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
List<ServerPrivateInfo>? getServerInfoList(dynamic data) {
|
||||
List<ServerPrivateInfo> ss = [];
|
||||
if (data is String) {
|
||||
data = json.decode(data);
|
||||
}
|
||||
for (var t in data) {
|
||||
ss.add(ServerPrivateInfo.fromJson(t));
|
||||
}
|
||||
|
||||
return ss;
|
||||
}
|
||||
90
lib/data/model/server_status.dart
Normal file
90
lib/data/model/server_status.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
import 'package:toolbox/data/model/disk_info.dart';
|
||||
import 'package:toolbox/data/model/tcp_status.dart';
|
||||
|
||||
///
|
||||
/// Code generated by jsonToDartModel https://ashamp.github.io/jsonToDartModel/
|
||||
///
|
||||
|
||||
class ServerStatus {
|
||||
/*
|
||||
{
|
||||
"cpuPercent": 0,
|
||||
"memList": [
|
||||
1
|
||||
],
|
||||
"sysVer": "",
|
||||
"uptime": "",
|
||||
"disk": [
|
||||
{
|
||||
"mountPath": "",
|
||||
"mountLocation": "",
|
||||
"usedPercent": 0,
|
||||
"used": "",
|
||||
"size": "",
|
||||
"avail": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
double? cpuPercent;
|
||||
List<int?>? memList;
|
||||
String? sysVer;
|
||||
String? uptime;
|
||||
List<DiskInfo?>? disk;
|
||||
TcpStatus? tcp;
|
||||
|
||||
ServerStatus(
|
||||
{this.cpuPercent,
|
||||
this.memList,
|
||||
this.sysVer,
|
||||
this.uptime,
|
||||
this.disk,
|
||||
this.tcp});
|
||||
ServerStatus.fromJson(Map<String, dynamic> json) {
|
||||
cpuPercent = double.parse(json["cpuPercent"]);
|
||||
if (json["memList"] != null) {
|
||||
final v = json["memList"];
|
||||
final arr0 = <int>[];
|
||||
v.forEach((v) {
|
||||
arr0.add(v.toInt());
|
||||
});
|
||||
memList = arr0;
|
||||
}
|
||||
sysVer = json["sysVer"]?.toString();
|
||||
uptime = json["uptime"]?.toString();
|
||||
if (json["disk"] != null) {
|
||||
final v = json["disk"];
|
||||
final arr0 = <DiskInfo>[];
|
||||
v.forEach((v) {
|
||||
arr0.add(DiskInfo.fromJson(v));
|
||||
});
|
||||
disk = arr0;
|
||||
}
|
||||
tcp = TcpStatus.fromJson(json['tcp']);
|
||||
}
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data["cpuPercent"] = cpuPercent;
|
||||
if (memList != null) {
|
||||
final v = memList;
|
||||
final arr0 = [];
|
||||
for (var v in v!) {
|
||||
arr0.add(v);
|
||||
}
|
||||
data["memList"] = arr0;
|
||||
}
|
||||
data["sysVer"] = sysVer;
|
||||
data["uptime"] = uptime;
|
||||
if (disk != null) {
|
||||
final v = disk;
|
||||
final arr0 = [];
|
||||
for (var v in v!) {
|
||||
arr0.add(v!.toJson());
|
||||
}
|
||||
data["disk"] = arr0;
|
||||
}
|
||||
data['tcp'] = tcp;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
39
lib/data/model/tcp_status.dart
Normal file
39
lib/data/model/tcp_status.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
///
|
||||
/// Code generated by jsonToDartModel https://ashamp.github.io/jsonToDartModel/
|
||||
///
|
||||
class TcpStatus {
|
||||
/*
|
||||
{
|
||||
"maxConn": 0,
|
||||
"active": 1,
|
||||
"passive": 2,
|
||||
"fail": 3
|
||||
}
|
||||
*/
|
||||
|
||||
int? maxConn;
|
||||
int? active;
|
||||
int? passive;
|
||||
int? fail;
|
||||
|
||||
TcpStatus({
|
||||
this.maxConn,
|
||||
this.active,
|
||||
this.passive,
|
||||
this.fail,
|
||||
});
|
||||
TcpStatus.fromJson(Map<String, dynamic> json) {
|
||||
maxConn = json["maxConn"]?.toInt();
|
||||
active = json["active"]?.toInt();
|
||||
passive = json["passive"]?.toInt();
|
||||
fail = json["fail"]?.toInt();
|
||||
}
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = Map<String, dynamic>();
|
||||
data["maxConn"] = maxConn;
|
||||
data["active"] = active;
|
||||
data["passive"] = passive;
|
||||
data["fail"] = fail;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
29
lib/data/provider/server.dart
Normal file
29
lib/data/provider/server.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'package:toolbox/core/provider_base.dart';
|
||||
import 'package:toolbox/data/model/server_private_info.dart';
|
||||
import 'package:toolbox/data/store/server.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
|
||||
class ServerProvider extends BusyProvider {
|
||||
late List<ServerPrivateInfo> _servers;
|
||||
|
||||
List<ServerPrivateInfo> get servers => _servers;
|
||||
|
||||
Future<void> loadData() async {
|
||||
setBusyState(true);
|
||||
_servers = locator<ServerStore>().fetch();
|
||||
setBusyState(false);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void addServer(ServerPrivateInfo info) {
|
||||
_servers.add(info);
|
||||
locator<ServerStore>().put(info);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void delServer(ServerPrivateInfo info) {
|
||||
_servers.remove(info);
|
||||
locator<ServerStore>().delete(info);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -1 +1,27 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:toolbox/core/persistant_store.dart';
|
||||
import 'package:toolbox/data/model/server_private_info.dart';
|
||||
|
||||
class ServerStore extends PersistentStore {
|
||||
void put(ServerPrivateInfo info) {
|
||||
final ss = fetch();
|
||||
if (!have(info)) ss.add(info);
|
||||
box.put('servers', json.encode(ss));
|
||||
}
|
||||
|
||||
List<ServerPrivateInfo> fetch() {
|
||||
return getServerInfoList(
|
||||
json.decode(box.get('servers', defaultValue: '[]')!))!;
|
||||
}
|
||||
|
||||
void delete(ServerPrivateInfo s) {
|
||||
final ss = fetch();
|
||||
ss.removeWhere((e) => e.ip == s.ip && e.port == s.port && e.user == e.user);
|
||||
box.put('servers', json.encode(ss));
|
||||
}
|
||||
|
||||
bool have(ServerPrivateInfo s) => fetch()
|
||||
.where((e) => e.ip == s.ip && e.port == s.port && e.user == e.user)
|
||||
.isNotEmpty;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:toolbox/data/provider/app.dart';
|
||||
import 'package:toolbox/data/provider/debug.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/data/service/app.dart';
|
||||
import 'package:toolbox/data/store/server.dart';
|
||||
import 'package:toolbox/data/store/setting.dart';
|
||||
|
||||
GetIt locator = GetIt.instance;
|
||||
@@ -13,12 +15,17 @@ void setupLocatorForServices() {
|
||||
void setupLocatorForProviders() {
|
||||
locator.registerSingleton(AppProvider());
|
||||
locator.registerSingleton(DebugProvider());
|
||||
locator.registerSingleton(ServerProvider());
|
||||
}
|
||||
|
||||
Future<void> setupLocatorForStores() async {
|
||||
final setting = SettingStore();
|
||||
await setting.init(boxName: 'setting');
|
||||
locator.registerSingleton(setting);
|
||||
|
||||
final server = ServerStore();
|
||||
await server.init(boxName: 'server');
|
||||
locator.registerSingleton(server);
|
||||
}
|
||||
|
||||
Future<void> setupLocator() async {
|
||||
|
||||
@@ -7,11 +7,13 @@ import 'package:toolbox/app.dart';
|
||||
import 'package:toolbox/core/analysis.dart';
|
||||
import 'package:toolbox/data/provider/app.dart';
|
||||
import 'package:toolbox/data/provider/debug.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
|
||||
Future<void> initApp() async {
|
||||
await Hive.initFlutter();
|
||||
await setupLocator();
|
||||
locator<ServerProvider>().loadData();
|
||||
}
|
||||
|
||||
void runInZone(dynamic Function() body) {
|
||||
@@ -51,6 +53,7 @@ Future<void> main() async {
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (_) => locator<AppProvider>()),
|
||||
ChangeNotifierProvider(create: (_) => locator<DebugProvider>()),
|
||||
ChangeNotifierProvider(create: (_) => locator<ServerProvider>()),
|
||||
],
|
||||
child: const MyApp(),
|
||||
),
|
||||
|
||||
@@ -10,8 +10,6 @@ class DebugPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _DebugPageState extends State<DebugPage> {
|
||||
DebugProvider get debug => Provider.of<DebugProvider>(context);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -33,11 +31,12 @@ class _DebugPageState extends State<DebugPage> {
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: debug.widgets,
|
||||
),
|
||||
child: Consumer<DebugProvider>(builder: (_, debug, __) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: debug.widgets);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import 'package:after_layout/after_layout.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:toolbox/core/route.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/data/res/build_data.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/page/convert.dart';
|
||||
import 'package:toolbox/view/page/debug.dart';
|
||||
import 'package:toolbox/view/page/server.dart';
|
||||
@@ -14,8 +18,11 @@ class MyHomePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage>
|
||||
with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin {
|
||||
final List<String> _tabs = ['服务器', '编/解码', '1', '2', '3'];
|
||||
with
|
||||
AutomaticKeepAliveClientMixin,
|
||||
SingleTickerProviderStateMixin,
|
||||
AfterLayoutMixin {
|
||||
final List<String> _tabs = ['服务器', '编/解码', '1', '2'];
|
||||
late final TabController _tabController;
|
||||
|
||||
@override
|
||||
@@ -45,7 +52,6 @@ class _MyHomePageState extends State<MyHomePage>
|
||||
ConvertPage(),
|
||||
ConvertPage(),
|
||||
ConvertPage(),
|
||||
ConvertPage()
|
||||
]),
|
||||
);
|
||||
}
|
||||
@@ -93,4 +99,10 @@ class _MyHomePageState extends State<MyHomePage>
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
Future<void> afterFirstLayout(BuildContext context) async {
|
||||
await GetIt.I.allReady();
|
||||
await locator<ServerProvider>().loadData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import 'package:after_layout/after_layout.dart';
|
||||
import 'package:charts_flutter/flutter.dart' as chart;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:ssh2/ssh2.dart';
|
||||
import 'package:toolbox/core/extension/stringx.dart';
|
||||
import 'package:toolbox/core/utils.dart';
|
||||
import 'package:toolbox/data/model/disk_info.dart';
|
||||
import 'package:toolbox/data/model/server_private_info.dart';
|
||||
import 'package:toolbox/data/model/server_status.dart';
|
||||
import 'package:toolbox/data/model/tcp_status.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/widget/circle_pie.dart';
|
||||
|
||||
class ServerPage extends StatefulWidget {
|
||||
@@ -14,13 +24,29 @@ class ServerPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ServerPageState extends State<ServerPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
with AutomaticKeepAliveClientMixin, AfterLayoutMixin {
|
||||
late MediaQueryData _media;
|
||||
late ThemeData _theme;
|
||||
bool useKey = false;
|
||||
|
||||
final nameController = TextEditingController();
|
||||
final ipController = TextEditingController();
|
||||
final portController = TextEditingController();
|
||||
final usernameController = TextEditingController();
|
||||
final passwordController = TextEditingController();
|
||||
final keyController = TextEditingController();
|
||||
final ipFocusNode = FocusNode();
|
||||
final portFocusNode = FocusNode();
|
||||
final usernameFocusNode = FocusNode();
|
||||
final passwordFocusNode = FocusNode();
|
||||
|
||||
late ServerProvider serverProvider;
|
||||
final cachedServerStatus = <ServerStatus?>[];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
serverProvider = locator<ServerProvider>();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -38,123 +64,359 @@ class _ServerPageState extends State<ServerPage>
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7),
|
||||
child: AnimationLimiter(
|
||||
child: Column(
|
||||
children: AnimationConfiguration.toStaggeredList(
|
||||
duration: const Duration(milliseconds: 377),
|
||||
childAnimationBuilder: (widget) => SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: widget,
|
||||
child: Consumer<ServerProvider>(builder: (_, pro, __) {
|
||||
return Column(
|
||||
children: AnimationConfiguration.toStaggeredList(
|
||||
duration: const Duration(milliseconds: 377),
|
||||
childAnimationBuilder: (widget) => SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: widget,
|
||||
),
|
||||
),
|
||||
),
|
||||
children: [const SizedBox(height: 13), ..._buildServerCards()],
|
||||
))),
|
||||
children: [
|
||||
const SizedBox(height: 13),
|
||||
...pro.servers
|
||||
.map((e) => _buildEachServerCard(e, pro.servers.indexOf(e)))
|
||||
],
|
||||
));
|
||||
})),
|
||||
),
|
||||
onTap: () => FocusScope.of(context).requestFocus(FocusNode()),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
showSnackBar(context, const Text(''));
|
||||
showRoundDialog(context, '新建服务器连接', _buildTextInputField(context), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('关闭')),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
final authorization = keyController.text.isEmpty
|
||||
? passwordController.text
|
||||
: {
|
||||
"privateKey": keyController.text,
|
||||
"passphrase": passwordController.text
|
||||
};
|
||||
serverProvider.addServer(ServerPrivateInfo(
|
||||
name: nameController.text,
|
||||
ip: ipController.text,
|
||||
port: int.parse(portController.text),
|
||||
user: usernameController.text,
|
||||
authorization: authorization));
|
||||
nameController.clear();
|
||||
ipController.clear();
|
||||
portController.clear();
|
||||
usernameController.clear();
|
||||
passwordController.clear();
|
||||
keyController.clear();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('连接'))
|
||||
]);
|
||||
},
|
||||
tooltip: 'add a server',
|
||||
heroTag: 'server page fab',
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<String>>? _getData() async {
|
||||
final client = SSHClient(
|
||||
host: '',
|
||||
port: 0,
|
||||
username: '',
|
||||
passwordOrKey: '',
|
||||
InputDecoration _buildDecoration(String label, {TextStyle? textStyle}) {
|
||||
return InputDecoration(labelText: label, labelStyle: textStyle);
|
||||
}
|
||||
|
||||
Widget _buildTextInputField(BuildContext ctx) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: nameController,
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: _buildDecoration('名称'),
|
||||
onSubmitted: (_) =>
|
||||
FocusScope.of(context).requestFocus(ipFocusNode),
|
||||
),
|
||||
TextField(
|
||||
controller: ipController,
|
||||
focusNode: ipFocusNode,
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: _buildDecoration('IP'),
|
||||
onSubmitted: (_) =>
|
||||
FocusScope.of(context).requestFocus(usernameFocusNode),
|
||||
),
|
||||
TextField(
|
||||
controller: portController,
|
||||
focusNode: portFocusNode,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: _buildDecoration('Port'),
|
||||
onSubmitted: (_) =>
|
||||
FocusScope.of(context).requestFocus(usernameFocusNode),
|
||||
),
|
||||
TextField(
|
||||
controller: usernameController,
|
||||
focusNode: usernameFocusNode,
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: _buildDecoration('用户名'),
|
||||
onSubmitted: (_) =>
|
||||
FocusScope.of(context).requestFocus(passwordFocusNode),
|
||||
),
|
||||
TextField(
|
||||
controller: keyController,
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: _buildDecoration('密钥(可选)'),
|
||||
onSubmitted: (_) => {},
|
||||
),
|
||||
TextField(
|
||||
controller: passwordController,
|
||||
focusNode: passwordFocusNode,
|
||||
obscureText: true,
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: _buildDecoration('密码'),
|
||||
onSubmitted: (_) => {},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<ServerStatus>? _getData(ServerPrivateInfo info) async {
|
||||
final client = SSHClient(
|
||||
host: info.ip!,
|
||||
port: info.port!,
|
||||
username: info.user!,
|
||||
passwordOrKey: info.authorization,
|
||||
);
|
||||
|
||||
await client.connect();
|
||||
final cpu = await client.execute(
|
||||
"top -bn1 | grep load | awk '{printf \"%.2f\", \$(NF-2)}'") ??
|
||||
'failed';
|
||||
final mem = await client
|
||||
.execute("free -m | awk 'NR==2{printf \"%s/%sMB\", \$3,\$2}'") ??
|
||||
'failed';
|
||||
return [cpu.trim(), mem.trim()];
|
||||
'0';
|
||||
final mem = await client.execute('free -m') ?? '';
|
||||
final sysVer = await client.execute('cat /etc/issue.net') ?? 'Unkown';
|
||||
final upTime = await client.execute('uptime') ?? 'Failed';
|
||||
final disk = await client.execute('df -h') ?? 'Failed';
|
||||
final tcp = await client.execute('cat /proc/net/snmp') ?? 'Failed';
|
||||
|
||||
return ServerStatus(
|
||||
cpuPercent: double.parse(cpu.trim()),
|
||||
memList: _getMem(mem),
|
||||
sysVer: sysVer.trim(),
|
||||
disk: _getDisk(disk),
|
||||
uptime: _getUpTime(upTime),
|
||||
tcp: _getTcp(tcp));
|
||||
}
|
||||
|
||||
Widget _buildEachServerCard() {
|
||||
return FutureBuilder<List<String>>(
|
||||
future: _getData(),
|
||||
builder: (BuildContext context, AsyncSnapshot<List<String>> snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.hasError) {
|
||||
return Text("Error: ${snapshot.error}");
|
||||
} else {
|
||||
return _buildEachCardContent(snapshot);
|
||||
}
|
||||
} else {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
String _getUpTime(String raw) {
|
||||
return raw.split('up ')[1].split(', ')[0];
|
||||
}
|
||||
|
||||
TcpStatus _getTcp(String raw) {
|
||||
final lines = raw.split('\n');
|
||||
int idx = 0;
|
||||
for (var item in lines) {
|
||||
if (item.contains('Tcp:')) {
|
||||
idx++;
|
||||
}
|
||||
if (idx == 2) {
|
||||
final vals = item.split(RegExp(r'\s{1,}'));
|
||||
return TcpStatus(
|
||||
maxConn: vals[5].i,
|
||||
active: vals[6].i,
|
||||
passive: vals[7].i,
|
||||
fail: vals[8].i);
|
||||
}
|
||||
}
|
||||
return TcpStatus(maxConn: 0, active: 0, passive: 0, fail: 0);
|
||||
}
|
||||
|
||||
List<DiskInfo> _getDisk(String disk) {
|
||||
final list = <DiskInfo>[];
|
||||
final items = disk.split('\n');
|
||||
for (var item in items) {
|
||||
if (items.indexOf(item) == 0 || item.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final vals = item.split(RegExp(r'\s{1,}'));
|
||||
list.add(DiskInfo(
|
||||
mountPath: vals[1],
|
||||
mountLocation: vals[5],
|
||||
usedPercent: double.parse(vals[4].replaceFirst('%', '')),
|
||||
used: vals[2],
|
||||
size: vals[1],
|
||||
avail: vals[3]));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
List<int> _getMem(String mem) {
|
||||
for (var item in mem.split('\n')) {
|
||||
if (item.contains('Mem:')) {
|
||||
return RegExp(r'[1-9][0-9]*')
|
||||
.allMatches(item)
|
||||
.map((e) => int.parse(item.substring(e.start, e.end)))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
Widget _buildEachServerCard(ServerPrivateInfo e, int index) {
|
||||
return FutureBuilder<ServerStatus>(
|
||||
future: _getData(e),
|
||||
builder: (BuildContext context, AsyncSnapshot<ServerStatus> snapshot) {
|
||||
return GestureDetector(
|
||||
child: _buildEachCardContent(snapshot, e.name ?? '', index),
|
||||
onLongPress: () =>
|
||||
showRoundDialog(context, '是否删除', const Text('删除后无法恢复'), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('否')),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
serverProvider.delServer(e);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('是'))
|
||||
]),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEachCardContent(AsyncSnapshot snapshot) {
|
||||
final cpuPercent = double.parse(snapshot.data![0]) * 100;
|
||||
final memSplit = snapshot.data![1].replaceFirst('MB', '').split('/');
|
||||
final memPercent = int.parse(memSplit[0]) / int.parse(memSplit[1]) * 100;
|
||||
final cpuData = [
|
||||
IndexPercent(0, cpuPercent.toInt()),
|
||||
];
|
||||
final memData = [
|
||||
IndexPercent(0, memPercent.toInt()),
|
||||
];
|
||||
return Card(
|
||||
child: Padding(padding:const EdgeInsets.all(13) , child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
Widget _buildEachCardContent(
|
||||
AsyncSnapshot<ServerStatus> snapshot, String serverName, int index) {
|
||||
Widget child;
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
if (cachedServerStatus.length > index && cachedServerStatus.elementAt(index) != null) {
|
||||
child = _buildRealServerCard(cachedServerStatus.elementAt(index)!, serverName);
|
||||
} else {
|
||||
child = _buildRealServerCard(
|
||||
ServerStatus(
|
||||
cpuPercent: 0,
|
||||
memList: [100, 0],
|
||||
disk: [
|
||||
DiskInfo(
|
||||
mountLocation: '',
|
||||
mountPath: '',
|
||||
used: '',
|
||||
size: '',
|
||||
avail: '',
|
||||
usedPercent: 0)
|
||||
],
|
||||
sysVer: '',
|
||||
uptime: '',
|
||||
tcp: TcpStatus(maxConn: 0, active: 0, passive: 0, fail: 0)),
|
||||
serverName);
|
||||
}
|
||||
} else if (snapshot.hasError) {
|
||||
child = Column(
|
||||
children: [
|
||||
Text(' Jilin', style: TextStyle(fontWeight: FontWeight.bold),),
|
||||
const SizedBox(height: 7,),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildPercentCircle(cpuPercent, 'CPU', [
|
||||
chart.Series<IndexPercent, int>(
|
||||
id: 'CPU',
|
||||
domainFn: (IndexPercent cpu, _) => cpu.id,
|
||||
measureFn: (IndexPercent cpu, _) => cpu.percent,
|
||||
data: cpuData,
|
||||
Text(
|
||||
serverName,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Center(
|
||||
child: Text("Error: ${snapshot.error}"),
|
||||
)
|
||||
]),
|
||||
_buildPercentCircle(memPercent, 'MEM', [
|
||||
chart.Series<IndexPercent, int>(
|
||||
id: 'MEM',
|
||||
domainFn: (IndexPercent sales, _) => sales.id,
|
||||
measureFn: (IndexPercent sales, _) => sales.percent,
|
||||
data: memData,
|
||||
)
|
||||
])
|
||||
],
|
||||
)
|
||||
],
|
||||
),),
|
||||
);
|
||||
} else {
|
||||
if (cachedServerStatus.length <= index) {
|
||||
cachedServerStatus.add(snapshot.data!);
|
||||
} else {
|
||||
cachedServerStatus[index] = snapshot.data!;
|
||||
}
|
||||
child = _buildRealServerCard(snapshot.data!, serverName);
|
||||
}
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(13),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPercentCircle(
|
||||
double percent, String title, List<chart.Series<IndexPercent, int>> series) {
|
||||
Widget _buildRealServerCard(ServerStatus ss, String serverName) {
|
||||
final cpuData = [
|
||||
IndexPercent(0, ss.cpuPercent!.toInt()),
|
||||
IndexPercent(1, 100 - ss.cpuPercent!.toInt()),
|
||||
];
|
||||
final memData = <IndexPercent>[];
|
||||
for (var e in ss.memList!) {
|
||||
memData.add(IndexPercent(ss.memList!.indexOf(e), e!.toInt()));
|
||||
}
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
serverName,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(ss.uptime!,
|
||||
style: TextStyle(
|
||||
color: _theme.textTheme.bodyText1!.color!.withAlpha(100)))
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 13,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildPercentCircle(ss.cpuPercent!, 'CPU', [
|
||||
chart.Series<IndexPercent, int>(
|
||||
id: 'CPU',
|
||||
domainFn: (IndexPercent cpu, _) => cpu.id,
|
||||
measureFn: (IndexPercent cpu, _) => cpu.percent,
|
||||
data: cpuData,
|
||||
)
|
||||
]),
|
||||
_buildPercentCircle(
|
||||
ss.memList![1]! / ss.memList![0]! * 100, 'Mem', [
|
||||
chart.Series<IndexPercent, int>(
|
||||
id: 'Mem',
|
||||
domainFn: (IndexPercent sales, _) => sales.id,
|
||||
measureFn: (IndexPercent sales, _) => sales.percent,
|
||||
data: memData,
|
||||
)
|
||||
]),
|
||||
_buildIOData('Net', ss.tcp!.maxConn!.toString(), '0kb/s'),
|
||||
_buildIOData('Disk', '0kb/s', '0kb/s')
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIOData(String title, String up, String down) {
|
||||
return SizedBox(
|
||||
width: _media.size.width * 0.2,
|
||||
height: _media.size.height * 0.1,
|
||||
child: Stack(
|
||||
children: [
|
||||
DonutPieChart.withRandomData(),
|
||||
Positioned(
|
||||
child: Text(
|
||||
'${percent.toStringAsFixed(1)}%',
|
||||
textAlign: TextAlign.center,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'↓$up',
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
Text(
|
||||
'↑$down',
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
],
|
||||
),
|
||||
top: _media.size.height * 0.012,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0
|
||||
),
|
||||
Positioned(
|
||||
child: Text(title, textAlign: TextAlign.center),
|
||||
@@ -166,10 +428,39 @@ class _ServerPageState extends State<ServerPage>
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildServerCards() {
|
||||
return [_buildEachServerCard()];
|
||||
Widget _buildPercentCircle(double percent, String title,
|
||||
List<chart.Series<IndexPercent, int>> series) {
|
||||
return SizedBox(
|
||||
width: _media.size.width * 0.2,
|
||||
height: _media.size.height * 0.1,
|
||||
child: Stack(
|
||||
children: [
|
||||
DonutPieChart(series),
|
||||
Positioned(
|
||||
child: Text(
|
||||
'${percent.toStringAsFixed(1)}%',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: _media.size.height * 0.03,
|
||||
),
|
||||
Positioned(
|
||||
child: Text(title, textAlign: TextAlign.center),
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
Future<void> afterFirstLayout(BuildContext context) async {
|
||||
await GetIt.I.allReady();
|
||||
await locator<ServerProvider>().loadData();
|
||||
}
|
||||
}
|
||||
|
||||
25
lib/view/widget/card_dialog.dart
Normal file
25
lib/view/widget/card_dialog.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CardDialog extends StatelessWidget {
|
||||
const CardDialog(
|
||||
{Key? key, this.title, this.content, this.actions, this.padding})
|
||||
: super(key: key);
|
||||
|
||||
final Widget? content;
|
||||
final List<Widget>? actions;
|
||||
final Widget? title;
|
||||
final EdgeInsets? padding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
contentPadding: padding ?? const EdgeInsets.fromLTRB(24, 17, 24, 7),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(20.0)),
|
||||
),
|
||||
title: title,
|
||||
content: content,
|
||||
actions: actions,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:math';
|
||||
// EXCLUDE_FROM_GALLERY_DOCS_END
|
||||
import 'package:charts_flutter/flutter.dart' as charts;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -7,59 +5,28 @@ class DonutPieChart extends StatelessWidget {
|
||||
final List<charts.Series<dynamic, num>> seriesList;
|
||||
final bool animate;
|
||||
|
||||
const DonutPieChart(this.seriesList, {Key? key, this.animate = true}) : super(key: key);
|
||||
|
||||
factory DonutPieChart.withRandomData() {
|
||||
return DonutPieChart(_createRandomData());
|
||||
}
|
||||
|
||||
/// Create random data.
|
||||
static List<charts.Series<IndexPercent, int>> _createRandomData() {
|
||||
final random = Random();
|
||||
|
||||
final data = [
|
||||
IndexPercent(0, random.nextInt(100)),
|
||||
IndexPercent(1, random.nextInt(100)),
|
||||
IndexPercent(2, random.nextInt(100)),
|
||||
IndexPercent(3, random.nextInt(100)),
|
||||
];
|
||||
|
||||
return [
|
||||
charts.Series<IndexPercent, int>(
|
||||
id: 'Sales',
|
||||
domainFn: (IndexPercent sales, _) => sales.id,
|
||||
measureFn: (IndexPercent sales, _) => sales.percent,
|
||||
data: data,
|
||||
)
|
||||
];
|
||||
}
|
||||
// EXCLUDE_FROM_GALLERY_DOCS_END
|
||||
const DonutPieChart(this.seriesList, {Key? key, this.animate = false})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return charts.PieChart(seriesList,
|
||||
animate: animate,
|
||||
layoutConfig: charts.LayoutConfig(
|
||||
leftMarginSpec: charts.MarginSpec.fixedPixel(1),
|
||||
topMarginSpec: charts.MarginSpec.fixedPixel(1),
|
||||
rightMarginSpec: charts.MarginSpec.fixedPixel(1),
|
||||
bottomMarginSpec: charts.MarginSpec.fixedPixel(17)
|
||||
),
|
||||
// Configure the width of the pie slices to 60px. The remaining space in
|
||||
// the chart will be left as a hole in the center.
|
||||
leftMarginSpec: charts.MarginSpec.fixedPixel(1),
|
||||
topMarginSpec: charts.MarginSpec.fixedPixel(1),
|
||||
rightMarginSpec: charts.MarginSpec.fixedPixel(1),
|
||||
bottomMarginSpec: charts.MarginSpec.fixedPixel(17)),
|
||||
defaultRenderer: charts.ArcRendererConfig<num>(
|
||||
arcWidth: 6,
|
||||
minHoleWidthForCenterContent: 60,
|
||||
arcRatio: 0.2,
|
||||
)
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Sample linear data type.
|
||||
class IndexPercent {
|
||||
final int id;
|
||||
final int percent;
|
||||
|
||||
IndexPercent(this.id, this.percent);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user