Support Docker start/stop/remove

This commit is contained in:
Junyuan Feng
2022-03-08 17:40:32 +08:00
parent 34e6b99297
commit 241002c3ea
8 changed files with 190 additions and 56 deletions

View File

@@ -56,7 +56,6 @@ Support, but not tested|Windows/Linux
- [ ] SFTP - [ ] SFTP
- [ ] Snippet market - [ ] Snippet market
- [ ] Docker manager - [ ] Docker manager
- [ ] SSH terminal
## Build ## Build
Please use `make.dart` to build. Please use `make.dart` to build.

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:toolbox/data/res/color.dart';
class MenuItem { class MenuItem {
final String text; final String text;
@@ -8,9 +9,21 @@ class MenuItem {
required this.text, required this.text,
required this.icon, required this.icon,
}); });
Widget get build => Row(
children: [
Icon(icon, color: primaryColor),
const SizedBox(
width: 10,
),
Text(
text,
),
],
);
} }
class MenuItems { class ServerTabMenuItems {
static const List<MenuItem> firstItems = [sftp, snippet, apt, docker]; static const List<MenuItem> firstItems = [sftp, snippet, apt, docker];
static const List<MenuItem> secondItems = [edit]; static const List<MenuItem> secondItems = [edit];
@@ -19,18 +32,10 @@ class MenuItems {
static const apt = MenuItem(text: 'Apt', icon: Icons.system_security_update); static const apt = MenuItem(text: 'Apt', icon: Icons.system_security_update);
static const docker = MenuItem(text: 'Docker', icon: Icons.view_agenda); static const docker = MenuItem(text: 'Docker', icon: Icons.view_agenda);
static const edit = MenuItem(text: 'Edit', icon: Icons.edit); static const edit = MenuItem(text: 'Edit', icon: Icons.edit);
}
static Widget buildItem(MenuItem item) {
return Row( class DockerMenuItems {
children: [ static const rm = MenuItem(text: 'Remove', icon: Icons.delete);
Icon(item.icon), static const start = MenuItem(text: 'Start', icon: Icons.play_arrow);
const SizedBox( static const stop = MenuItem(text: 'Stop', icon: Icons.stop);
width: 10,
),
Text(
item.text,
),
],
);
}
} }

View File

@@ -11,17 +11,21 @@ class DockerPsItem {
this.status, this.ports, this.name); this.status, this.ports, this.name);
DockerPsItem.fromRawString(String rawString) { DockerPsItem.fromRawString(String rawString) {
final List<String> parts = rawString.split(' '); List<String> parts = rawString.split(' ');
parts.removeWhere((element) => element.isEmpty);
parts = parts.map((e) => e.trim()).toList();
containerId = parts[0]; containerId = parts[0];
image = parts[1]; image = parts[1];
command = parts[2]; command = parts[2].trim();
created = parts[3]; created = parts[3];
status = parts[4]; status = parts[4];
ports = parts[5]; if (running) {
if (running && parts.length == 9) { ports = parts[5];
name = parts[8];
} else {
name = parts[6]; name = parts[6];
} else {
ports = '';
name = parts[5];
} }
} }

View File

@@ -50,14 +50,42 @@ class DockerProvider extends BusyProvider {
notifyListeners(); notifyListeners();
} }
Future<void> stop(String id) async { Future<bool> stop(String id) async {
setBusyState();
if (client == null) { if (client == null) {
error = 'no client'; error = 'no client';
notifyListeners(); setBusyState(false);
return; return false;
} }
final result = await client!.run('docker stop $id').string; final result = await client!.run('docker stop $id').string;
await refresh(); await refresh();
notifyListeners(); setBusyState(false);
return result.contains(id);
}
Future<bool> start(String id) async {
setBusyState();
if (client == null) {
error = 'no client';
setBusyState(false);
return false;
}
final result = await client!.run('docker start $id').string;
await refresh();
setBusyState(false);
return result.contains(id);
}
Future<bool> delete(String id) async {
setBusyState();
if (client == null) {
error = 'no client';
setBusyState(false);
return false;
}
final result = await client!.run('docker rm $id').string;
await refresh();
setBusyState(false);
return result.contains(id);
} }
} }

View File

@@ -8,6 +8,7 @@ import 'package:toolbox/data/provider/apt.dart';
import 'package:toolbox/data/provider/server.dart'; import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/locator.dart'; import 'package:toolbox/locator.dart';
import 'package:toolbox/view/widget/round_rect_card.dart'; import 'package:toolbox/view/widget/round_rect_card.dart';
import 'package:toolbox/view/widget/two_line_text.dart';
class AptManagePage extends StatefulWidget { class AptManagePage extends StatefulWidget {
const AptManagePage(this.spi, {Key? key}) : super(key: key); const AptManagePage(this.spi, {Key? key}) : super(key: key);
@@ -53,7 +54,7 @@ class _AptManagePageState extends State<AptManagePage>
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Apt'), title: TwoLineText(up: 'Apt', down: widget.spi.ip),
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.refresh), icon: const Icon(Icons.refresh),

View File

@@ -1,13 +1,17 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:toolbox/core/utils.dart'; import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/app/menu_item.dart';
import 'package:toolbox/data/model/docker/ps.dart'; import 'package:toolbox/data/model/docker/ps.dart';
import 'package:toolbox/data/model/server/server_private_info.dart'; import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/provider/docker.dart'; import 'package:toolbox/data/provider/docker.dart';
import 'package:toolbox/data/provider/server.dart'; import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/locator.dart'; import 'package:toolbox/locator.dart';
import 'package:toolbox/view/widget/center_loading.dart'; import 'package:toolbox/view/widget/center_loading.dart';
import 'package:toolbox/view/widget/two_line_text.dart';
import 'package:toolbox/view/widget/round_rect_card.dart'; import 'package:toolbox/view/widget/round_rect_card.dart';
import 'package:toolbox/view/widget/url_text.dart';
class DockerManagePage extends StatefulWidget { class DockerManagePage extends StatefulWidget {
final ServerPrivateInfo spi; final ServerPrivateInfo spi;
@@ -20,6 +24,7 @@ class DockerManagePage extends StatefulWidget {
class _DockerManagePageState extends State<DockerManagePage> { class _DockerManagePageState extends State<DockerManagePage> {
final _docker = locator<DockerProvider>(); final _docker = locator<DockerProvider>();
final greyTextStyle = const TextStyle(color: Colors.grey); final greyTextStyle = const TextStyle(color: Colors.grey);
late MediaQueryData _media;
@override @override
void dispose() { void dispose() {
@@ -27,6 +32,12 @@ class _DockerManagePageState extends State<DockerManagePage> {
_docker.clear(); _docker.clear();
} }
@override
void didChangeDependencies() {
super.didChangeDependencies();
_media = MediaQuery.of(context);
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -46,7 +57,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Docker'), title: TwoLineText(up: 'Docker', down: widget.spi.ip),
), ),
body: _buildMain(), body: _buildMain(),
); );
@@ -57,8 +68,15 @@ class _DockerManagePageState extends State<DockerManagePage> {
final running = docker.running; final running = docker.running;
if (docker.error != null && running == null) { if (docker.error != null && running == null) {
return Center( return Center(
child: Text(docker.error!), child: Column(
); children: [
SizedBox(
height: _media.size.height * 0.43,
),
Text(docker.error!),
_buildSolution(docker.error!)
],
));
} }
if (running == null) { if (running == null) {
_docker.refresh(); _docker.refresh();
@@ -66,23 +84,40 @@ class _DockerManagePageState extends State<DockerManagePage> {
} }
return ListView( return ListView(
padding: const EdgeInsets.all(7), padding: const EdgeInsets.all(7),
children: children: [
[_buildVersion(docker.edition ?? 'Unknown', docker.version ?? 'Unknown'), _buildPsItems(running)].map((e) => RoundRectCard(e)).toList(), _buildVersion(
docker.edition ?? 'Unknown', docker.version ?? 'Unknown'),
_buildPsItems(running, docker)
].map((e) => RoundRectCard(e)).toList(),
); );
}); });
} }
Widget _buildVersion(String edition, String version) { Widget _buildSolution(String err) {
return Padding(padding: EdgeInsets.all(13), child: Row( switch (err) {
mainAxisAlignment: MainAxisAlignment.spaceBetween, case 'docker not found':
children: [ return const UrlText(
Text(edition), text: 'Please https://docs.docker.com/engine/install docker first.',
Text(version) replace: 'install',
], );
),); case 'no client':
return const Text('Plz wait for the connection to be established.');
default:
return const Text('Unknown error');
}
} }
Widget _buildPsItems(List<DockerPsItem> running) { Widget _buildVersion(String edition, String version) {
return Padding(
padding: const EdgeInsets.all(13),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [Text(edition), Text(version)],
),
);
}
Widget _buildPsItems(List<DockerPsItem> running, DockerProvider docker) {
return ExpansionTile( return ExpansionTile(
title: const Text('Container Status'), title: const Text('Container Status'),
subtitle: Text(_buildSubtitle(running), style: greyTextStyle), subtitle: Text(_buildSubtitle(running), style: greyTextStyle),
@@ -90,14 +125,60 @@ class _DockerManagePageState extends State<DockerManagePage> {
return ListTile( return ListTile(
title: Text(item.image), title: Text(item.image),
subtitle: Text(item.status), subtitle: Text(item.status),
trailing: IconButton( trailing: docker.isBusy ? const CircularProgressIndicator() : _buildMoreBtn(item.running, item.containerId),
onPressed: () {},
icon: Icon(item.running ? Icons.stop : Icons.play_arrow)),
); );
}).toList(), }).toList(),
); );
} }
Widget _buildMoreBtn(bool running, String containerId) {
final item = running ? DockerMenuItems.stop : DockerMenuItems.start;
return DropdownButtonHideUnderline(
child: DropdownButton2(
customButton: const Padding(
padding: EdgeInsets.only(left: 7),
child: Icon(
Icons.more_vert,
size: 17,
),
),
customItemsHeight: 8,
items: [
DropdownMenuItem<MenuItem>(
value: item,
child: item.build,
),
DropdownMenuItem<MenuItem>(
value: DockerMenuItems.rm,
child: DockerMenuItems.rm.build,
),
],
onChanged: (value) {
final item = value as MenuItem;
switch (item) {
case DockerMenuItems.rm:
_docker.delete(containerId);
break;
case DockerMenuItems.start:
_docker.start(containerId);
break;
case DockerMenuItems.stop:
_docker.stop(containerId);
break;
}
},
itemHeight: 37,
itemPadding: const EdgeInsets.only(left: 17, right: 17),
dropdownWidth: 160,
dropdownDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(7),
),
dropdownElevation: 8,
offset: const Offset(0, 8),
),
);
}
String _buildSubtitle(List<DockerPsItem> running) { String _buildSubtitle(List<DockerPsItem> running) {
final runningCount = running.where((element) => element.running).length; final runningCount = running.where((element) => element.running).length;
final stoped = running.length - runningCount; final stoped = running.length - runningCount;

View File

@@ -7,7 +7,6 @@ import 'package:marquee/marquee.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:toolbox/core/route.dart'; import 'package:toolbox/core/route.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/app/menu_item.dart'; import 'package:toolbox/data/model/app/menu_item.dart';
import 'package:toolbox/data/model/server/server.dart'; import 'package:toolbox/data/model/server/server.dart';
import 'package:toolbox/data/model/server/server_connection_state.dart'; import 'package:toolbox/data/model/server/server_connection_state.dart';
@@ -222,30 +221,30 @@ class _ServerPageState extends State<ServerPage>
size: 17, size: 17,
), ),
), ),
customItemsIndexes: [MenuItems.firstItems.length], customItemsIndexes: [ServerTabMenuItems.firstItems.length],
customItemsHeight: 8, customItemsHeight: 8,
items: [ items: [
...MenuItems.firstItems.map( ...ServerTabMenuItems.firstItems.map(
(item) => DropdownMenuItem<MenuItem>( (item) => DropdownMenuItem<MenuItem>(
value: item, value: item,
child: MenuItems.buildItem(item), child: item.build,
), ),
), ),
const DropdownMenuItem<Divider>(enabled: false, child: Divider()), const DropdownMenuItem<Divider>(enabled: false, child: Divider()),
...MenuItems.secondItems.map( ...ServerTabMenuItems.secondItems.map(
(item) => DropdownMenuItem<MenuItem>( (item) => DropdownMenuItem<MenuItem>(
value: item, value: item,
child: MenuItems.buildItem(item), child: item.build,
), ),
), ),
], ],
onChanged: (value) { onChanged: (value) {
final item = value as MenuItem; final item = value as MenuItem;
switch (item) { switch (item) {
case MenuItems.apt: case ServerTabMenuItems.apt:
AppRoute(AptManagePage(spi), 'apt manage page').go(context); AppRoute(AptManagePage(spi), 'apt manage page').go(context);
break; break;
case MenuItems.sftp: case ServerTabMenuItems.sftp:
AppRoute( AppRoute(
SFTPPage( SFTPPage(
spi: spi, spi: spi,
@@ -253,7 +252,7 @@ class _ServerPageState extends State<ServerPage>
'SFTP') 'SFTP')
.go(context); .go(context);
break; break;
case MenuItems.snippet: case ServerTabMenuItems.snippet:
AppRoute( AppRoute(
SnippetListPage( SnippetListPage(
spi: spi, spi: spi,
@@ -261,7 +260,7 @@ class _ServerPageState extends State<ServerPage>
'snippet list') 'snippet list')
.go(context); .go(context);
break; break;
case MenuItems.edit: case ServerTabMenuItems.edit:
AppRoute( AppRoute(
ServerEditPage( ServerEditPage(
spi: spi, spi: spi,
@@ -269,7 +268,7 @@ class _ServerPageState extends State<ServerPage>
'Edit server info page') 'Edit server info page')
.go(context); .go(context);
break; break;
case MenuItems.docker: case ServerTabMenuItems.docker:
AppRoute(DockerManagePage(spi), 'Docker manage page').go(context); AppRoute(DockerManagePage(spi), 'Docker manage page').go(context);
break; break;
} }

View File

@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
class TwoLineText extends StatelessWidget {
const TwoLineText({Key? key, required this.up, required this.down}) : super(key: key);
final String up;
final String down;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(up, style: const TextStyle(fontSize: 15),),
Text(down, style: const TextStyle(fontSize: 11),)
],
);
}
}