一大波更新
parent
ae5ad33a75
commit
7ae24131c8
|
@ -2,7 +2,7 @@
|
||||||
# @author yutent<yutent.io@gmail.com>
|
# @author yutent<yutent.io@gmail.com>
|
||||||
# @date 2024/01/15 15:38:34
|
# @date 2024/01/15 15:38:34
|
||||||
|
|
||||||
import subprocess, json
|
import subprocess, json, re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
def toISOTime(time_str):
|
def toISOTime(time_str):
|
||||||
|
@ -59,10 +59,14 @@ class Docker:
|
||||||
|
|
||||||
out = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE)
|
out = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE)
|
||||||
|
|
||||||
out = out.stdout.decode().strip().split("\n")
|
out = json.loads(out.stdout.decode().strip())
|
||||||
out = [json.loads(it) for it in out]
|
|
||||||
|
return [{
|
||||||
|
"name": it['Name'],
|
||||||
|
'config': it['ConfigFiles'],
|
||||||
|
'status': it['Status'],
|
||||||
|
} for it in out]
|
||||||
|
|
||||||
return out
|
|
||||||
|
|
||||||
def networks(self):
|
def networks(self):
|
||||||
cmd = 'docker network ls --format=json'
|
cmd = 'docker network ls --format=json'
|
||||||
|
@ -72,9 +76,20 @@ class Docker:
|
||||||
out = out.stdout.decode().strip().split("\n")
|
out = out.stdout.decode().strip().split("\n")
|
||||||
out = [json.loads(it) for it in out]
|
out = [json.loads(it) for it in out]
|
||||||
|
|
||||||
return out
|
# return out
|
||||||
|
return [{
|
||||||
|
"id": it['ID'],
|
||||||
|
"name": it['Name'],
|
||||||
|
"driver": it['Driver'],
|
||||||
|
'scope': it['Scope'],
|
||||||
|
'ipv6': it['IPv6'],
|
||||||
|
'labels': it['Labels'],
|
||||||
|
'internal': it['Internal'],
|
||||||
|
'created': toISOTime(re.sub(r'\.\d+', '', it['CreatedAt'])),
|
||||||
|
} for it in out]
|
||||||
|
|
||||||
def volumns(self):
|
|
||||||
|
def volumes(self):
|
||||||
cmd = 'docker volume ls --format=json'
|
cmd = 'docker volume ls --format=json'
|
||||||
|
|
||||||
out = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE)
|
out = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE)
|
||||||
|
@ -82,13 +97,75 @@ class Docker:
|
||||||
out = out.stdout.decode().strip().split("\n")
|
out = out.stdout.decode().strip().split("\n")
|
||||||
out = [json.loads(it) for it in out]
|
out = [json.loads(it) for it in out]
|
||||||
|
|
||||||
return out
|
return [{
|
||||||
|
"name": it['Name'],
|
||||||
|
"driver": it['Driver'],
|
||||||
|
'scope': it['Scope'],
|
||||||
|
'availability': it['Availability'],
|
||||||
|
'mountpoint': it['Mountpoint'],
|
||||||
|
'size': it['Size'],
|
||||||
|
'status': it['Status'],
|
||||||
|
} for it in out]
|
||||||
|
|
||||||
|
|
||||||
|
def stop_service(self, config = ''):
|
||||||
|
cmd = 'docker compose -f ' + config + ' stop'
|
||||||
|
out = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE,stderr=subprocess.PIPE)
|
||||||
|
|
||||||
|
return out.stderr.decode().strip()
|
||||||
|
|
||||||
|
|
||||||
|
def prune_volumes(self):
|
||||||
|
cmd = 'docker volume prune -a -f'
|
||||||
|
out = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE,stderr=subprocess.PIPE)
|
||||||
|
|
||||||
|
return out.stderr.decode().strip()
|
||||||
|
|
||||||
|
|
||||||
|
def rm_volume(self, name = ''):
|
||||||
|
cmd = 'docker volume rm ' + name
|
||||||
|
out = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE,stderr=subprocess.PIPE)
|
||||||
|
|
||||||
|
return out.stderr.decode().strip()
|
||||||
|
|
||||||
|
|
||||||
|
def prune_networks(self):
|
||||||
|
cmd = 'docker network prune -f'
|
||||||
|
out = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE,stderr=subprocess.PIPE)
|
||||||
|
|
||||||
|
return out.stderr.decode().strip()
|
||||||
|
|
||||||
|
|
||||||
|
def rm_network(self, id = ''):
|
||||||
|
cmd = 'docker network rm ' + id
|
||||||
|
out = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE,stderr=subprocess.PIPE)
|
||||||
|
|
||||||
|
return out.stderr.decode().strip()
|
||||||
|
|
||||||
|
|
||||||
|
def start(self, id = ''):
|
||||||
|
cmd = 'docker start ' + id
|
||||||
|
out = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE,stderr=subprocess.PIPE)
|
||||||
|
print('stdout', out.stdout.decode())
|
||||||
|
print('stderr', out.stderr.decode())
|
||||||
|
return out.stderr.decode().strip()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def stop(self, id = ''):
|
||||||
|
cmd = 'docker stop ' + id
|
||||||
|
out = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE,stderr=subprocess.PIPE)
|
||||||
|
print('stdout', out.stdout.decode())
|
||||||
|
print('stderr', out.stderr.decode())
|
||||||
|
return out.stderr.decode().strip()
|
||||||
|
|
||||||
|
|
||||||
def rm(self, id = ''):
|
def rm(self, id = ''):
|
||||||
cmd = 'docker rm ' + id
|
cmd = 'docker rm ' + id
|
||||||
out = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE,stderr=subprocess.PIPE)
|
out = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE,stderr=subprocess.PIPE)
|
||||||
return out.stderr.decode().strip()
|
return out.stderr.decode().strip()
|
||||||
|
|
||||||
|
|
||||||
def rmi(self, id = ''):
|
def rmi(self, id = ''):
|
||||||
cmd = 'docker rmi ' + id
|
cmd = 'docker rmi ' + id
|
||||||
out = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE,stderr=subprocess.PIPE)
|
out = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE,stderr=subprocess.PIPE)
|
||||||
|
|
|
@ -11,6 +11,8 @@ from webengine.gtk3 import WebEngine, create_setting, create_hmr_server, create_
|
||||||
from _window import Window
|
from _window import Window
|
||||||
from _docker import Docker
|
from _docker import Docker
|
||||||
|
|
||||||
|
# from utils import run_async
|
||||||
|
|
||||||
|
|
||||||
APP_ID = 'fun.wkit.dooke'
|
APP_ID = 'fun.wkit.dooke'
|
||||||
__dir__ = os.path.dirname(os.path.realpath(__file__))
|
__dir__ = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
@ -48,7 +50,7 @@ class Application(Gtk.Application):
|
||||||
def bridge_handler(self, ev, params):
|
def bridge_handler(self, ev, params):
|
||||||
_error = None
|
_error = None
|
||||||
output = None
|
output = None
|
||||||
print(ev, params)
|
# print(ev, params)
|
||||||
match ev:
|
match ev:
|
||||||
case 'docker':
|
case 'docker':
|
||||||
action = params['action']
|
action = params['action']
|
||||||
|
@ -63,12 +65,29 @@ class Application(Gtk.Application):
|
||||||
|
|
||||||
case 'services':
|
case 'services':
|
||||||
output = client.services()
|
output = client.services()
|
||||||
|
case 'service-stop':
|
||||||
|
_error = client.stop_service(params.get('config'))
|
||||||
|
|
||||||
case 'volumns':
|
|
||||||
output = client.volumns()
|
case 'volumes':
|
||||||
|
output = client.volumes()
|
||||||
|
case 'volume-prune':
|
||||||
|
_error = client.prune_volumes()
|
||||||
|
case 'volume-rm':
|
||||||
|
_error = client.rm_volume(params.get('name'))
|
||||||
|
|
||||||
case 'networks':
|
case 'networks':
|
||||||
output = client.networks()
|
output = client.networks()
|
||||||
|
case 'network-prune':
|
||||||
|
_error = client.prune_networks()
|
||||||
|
case 'network-rm':
|
||||||
|
_error = client.rm_network(params.get('id'))
|
||||||
|
|
||||||
|
case 'start':
|
||||||
|
_error = client.start(params.get('id'))
|
||||||
|
|
||||||
|
case 'stop':
|
||||||
|
_error = client.stop(params.get('id'))
|
||||||
|
|
||||||
case 'rm':
|
case 'rm':
|
||||||
_error = client.rm(params.get('id'))
|
_error = client.rm(params.get('id'))
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
|
||||||
|
import gi, threading
|
||||||
|
from gi.repository import GObject
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 定义一个异步修饰器, 用于在子线程中运行一些会阻塞主线程的任务
|
||||||
|
def run_async(func):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
thread = threading.Thread(target=func, args=args, kwargs=kwargs)
|
||||||
|
thread.daemon = True
|
||||||
|
thread.start()
|
||||||
|
return thread
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
# 类型js的settimeout的修饰器
|
||||||
|
def set_timeout(timeout = 0.5):
|
||||||
|
def decorator(callback):
|
||||||
|
def wrapper(*args):
|
||||||
|
t = threading.Timer(timeout, callback, args=args)
|
||||||
|
t.start()
|
||||||
|
return t
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
# 定义一个修饰器, 用于将当前方法转到主线程中运行 (子线程中调用方法时)
|
||||||
|
def idle(func):
|
||||||
|
def wrapper(*args):
|
||||||
|
GObject.idle_add(func, *args)
|
||||||
|
return wrapper
|
||||||
|
|
|
@ -45,6 +45,7 @@ createApp({
|
||||||
|
|
||||||
router-view {
|
router-view {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
width: calc(100vw - 144px);
|
||||||
height: 100;
|
height: 100;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
|
@ -9,6 +9,8 @@ import { createRouter, createWebHistory } from 'wkitd'
|
||||||
import './views/home.js'
|
import './views/home.js'
|
||||||
import './views/images.js'
|
import './views/images.js'
|
||||||
import './views/volumes.js'
|
import './views/volumes.js'
|
||||||
|
import './views/networks.js'
|
||||||
|
import './views/services.js'
|
||||||
|
|
||||||
export default createRouter({
|
export default createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
|
@ -24,6 +26,14 @@ export default createRouter({
|
||||||
{
|
{
|
||||||
path: '/volumes',
|
path: '/volumes',
|
||||||
name: 'wc-volumes'
|
name: 'wc-volumes'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/networks',
|
||||||
|
name: 'wc-networks'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/services',
|
||||||
|
name: 'wc-services'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
@ -34,5 +34,10 @@ export default createStore({
|
||||||
name: '网络管理',
|
name: '网络管理',
|
||||||
icon: 'share'
|
icon: 'share'
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
containers: [],
|
||||||
|
images: [],
|
||||||
|
volumes: [],
|
||||||
|
services: [],
|
||||||
|
networks: []
|
||||||
})
|
})
|
||||||
|
|
|
@ -17,8 +17,8 @@ export const docker = {
|
||||||
return native.handler('docker', { action: 'images' })
|
return native.handler('docker', { action: 'images' })
|
||||||
},
|
},
|
||||||
|
|
||||||
volumns() {
|
volumes() {
|
||||||
return native.handler('docker', { action: 'volumns' })
|
return native.handler('docker', { action: 'volumes' })
|
||||||
},
|
},
|
||||||
|
|
||||||
networks() {
|
networks() {
|
||||||
|
@ -35,5 +35,37 @@ export const docker = {
|
||||||
|
|
||||||
rmi(id) {
|
rmi(id) {
|
||||||
return native.handler('docker', { action: 'rmi', id })
|
return native.handler('docker', { action: 'rmi', id })
|
||||||
|
},
|
||||||
|
|
||||||
|
start(id) {
|
||||||
|
return native.handler('docker', { action: 'start', id })
|
||||||
|
},
|
||||||
|
|
||||||
|
stop(id) {
|
||||||
|
return native.handler('docker', { action: 'stop', id })
|
||||||
|
},
|
||||||
|
|
||||||
|
service: {
|
||||||
|
stop(config) {
|
||||||
|
return native.handler('docker', { action: 'service-stop', config })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
volume: {
|
||||||
|
prune() {
|
||||||
|
return native.handler('docker', { action: 'volume-prune' })
|
||||||
|
},
|
||||||
|
rm(name) {
|
||||||
|
return native.handler('docker', { action: 'volume-rm', name })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
network: {
|
||||||
|
prune() {
|
||||||
|
return native.handler('docker', { action: 'network-prune' })
|
||||||
|
},
|
||||||
|
rm(id) {
|
||||||
|
return native.handler('docker', { action: 'network-rm', id })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,14 @@ import { html, css, Component, classMap, nextTick, outsideClick } from 'wkit'
|
||||||
|
|
||||||
import { docker } from '../utils/index.js'
|
import { docker } from '../utils/index.js'
|
||||||
|
|
||||||
|
const STATE_RUNNING = '🟢'
|
||||||
|
const STATE_CREATED = '⚪'
|
||||||
|
const STATE_STOPPED = '🔴'
|
||||||
|
const ACTION_STOP = '⏹'
|
||||||
|
const ACTION_START = '⏸'
|
||||||
|
|
||||||
class Home extends Component {
|
class Home extends Component {
|
||||||
static props = {
|
static props = {}
|
||||||
containers: []
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = [
|
static styles = [
|
||||||
css`
|
css`
|
||||||
|
@ -142,16 +146,20 @@ class Home extends Component {
|
||||||
|
|
||||||
list.forEach(it => {
|
list.forEach(it => {
|
||||||
it.image = it.image.split(':')
|
it.image = it.image.split(':')
|
||||||
it.cmd = it.cmd.replace(/^"|"$/, '')
|
it.cmd = it.cmd.replace(/^"|"$/g, '')
|
||||||
it.port = it.port
|
it.port = it.port
|
||||||
.replace('0.0.0.0:', '')
|
.replace('0.0.0.0:', '')
|
||||||
.replace(':::', '')
|
.replace(':::', '')
|
||||||
.replace(/\/tcp/g, '')
|
.replace(/\/tcp/g, '')
|
||||||
it.state =
|
it.state =
|
||||||
it.state === 'running' ? '🟢' : it.state === 'created' ? '⚪' : '🔴'
|
it.state === 'running'
|
||||||
|
? STATE_RUNNING
|
||||||
|
: it.state === 'created'
|
||||||
|
? STATE_CREATED
|
||||||
|
: STATE_STOPPED
|
||||||
})
|
})
|
||||||
|
|
||||||
this.containers = list
|
this.$store.containers = list
|
||||||
}
|
}
|
||||||
|
|
||||||
async #filter(ev) {
|
async #filter(ev) {
|
||||||
|
@ -170,13 +178,23 @@ class Home extends Component {
|
||||||
this.$requestUpdate()
|
this.$requestUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async #toggleStat(item) {
|
||||||
|
//
|
||||||
|
if (item.state === STATE_RUNNING) {
|
||||||
|
await docker.stop(item.id)
|
||||||
|
} else {
|
||||||
|
await docker.start(item.id)
|
||||||
|
}
|
||||||
|
this.#fetch()
|
||||||
|
}
|
||||||
|
|
||||||
async #remove(item) {
|
async #remove(item) {
|
||||||
await docker.rm(item.id)
|
await docker.rm(item.id)
|
||||||
this.#fetch()
|
this.#fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let list = this.containers
|
let list = this.$store.containers
|
||||||
let txt = this.#input
|
let txt = this.#input
|
||||||
|
|
||||||
if (txt) {
|
if (txt) {
|
||||||
|
@ -232,9 +250,20 @@ class Home extends Component {
|
||||||
<wc-tooltip title=${it.status}>${it.state}</wc-tooltip>
|
<wc-tooltip title=${it.status}>${it.state}</wc-tooltip>
|
||||||
</section>
|
</section>
|
||||||
<section class="field flex gap-12 center">
|
<section class="field flex gap-12 center">
|
||||||
<span class="action ${it.state === '🟢' ? 'red' : 'green'}"
|
<wc-popconfirm
|
||||||
>${it.state === '🟢' ? '⏹' : '⏸'}</span
|
title=${`是否要${
|
||||||
|
it.state === STATE_RUNNING ? '停止' : '启动'
|
||||||
|
}此容器?`}
|
||||||
|
@confirm=${ev => this.#toggleStat(it)}
|
||||||
>
|
>
|
||||||
|
<span
|
||||||
|
class="action ${it.state === STATE_RUNNING
|
||||||
|
? 'red'
|
||||||
|
: 'green'}"
|
||||||
|
>
|
||||||
|
${it.state === STATE_RUNNING ? ACTION_STOP : ACTION_START}
|
||||||
|
</span>
|
||||||
|
</wc-popconfirm>
|
||||||
<wc-popconfirm
|
<wc-popconfirm
|
||||||
title="是否要删除此容器?"
|
title="是否要删除此容器?"
|
||||||
@confirm=${ev => this.#remove(it)}
|
@confirm=${ev => this.#remove(it)}
|
||||||
|
|
|
@ -8,9 +8,7 @@ import { html, css, Component, classMap, nextTick, outsideClick } from 'wkit'
|
||||||
import { docker } from '../utils/index.js'
|
import { docker } from '../utils/index.js'
|
||||||
|
|
||||||
class Images extends Component {
|
class Images extends Component {
|
||||||
static props = {
|
static props = {}
|
||||||
images: []
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = [
|
static styles = [
|
||||||
css`
|
css`
|
||||||
|
@ -96,6 +94,16 @@ class Images extends Component {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
color: var(--color-blue-1);
|
color: var(--color-blue-1);
|
||||||
}
|
}
|
||||||
|
.action {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.red {
|
||||||
|
color: var(--color-red-1);
|
||||||
|
}
|
||||||
|
&.green {
|
||||||
|
color: var(--color-teal-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.thead {
|
.thead {
|
||||||
|
@ -117,7 +125,7 @@ class Images extends Component {
|
||||||
|
|
||||||
async #fetch() {
|
async #fetch() {
|
||||||
let list = await docker.images()
|
let list = await docker.images()
|
||||||
this.images = list
|
this.$store.images = list
|
||||||
}
|
}
|
||||||
|
|
||||||
#filter(ev) {
|
#filter(ev) {
|
||||||
|
@ -135,7 +143,7 @@ class Images extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let list = this.images
|
let list = this.$store.images
|
||||||
let txt = this.#input
|
let txt = this.#input
|
||||||
if (txt) {
|
if (txt) {
|
||||||
list = list.filter(it => it.name.includes(txt))
|
list = list.filter(it => it.name.includes(txt))
|
||||||
|
|
|
@ -0,0 +1,202 @@
|
||||||
|
/**
|
||||||
|
* {}
|
||||||
|
* @author yutent<yutent.io@gmail.com>
|
||||||
|
* @date 2023/08/08 18:19:17
|
||||||
|
*/
|
||||||
|
import { html, css, Component, classMap, nextTick, outsideClick } from 'wkit'
|
||||||
|
|
||||||
|
import { docker } from '../utils/index.js'
|
||||||
|
|
||||||
|
class Networks extends Component {
|
||||||
|
static props = {}
|
||||||
|
|
||||||
|
static styles = [
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: var(--color-dark-1);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noselect {
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
|
||||||
|
css`
|
||||||
|
.main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.main .toolbar {
|
||||||
|
height: 48px;
|
||||||
|
border-bottom: 1px solid var(--color-plain-3);
|
||||||
|
}
|
||||||
|
.main .list {
|
||||||
|
overflow: auto;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
.item,
|
||||||
|
.thead {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
padding: 0 12px;
|
||||||
|
line-height: 1.25;
|
||||||
|
border-radius: 24px;
|
||||||
|
|
||||||
|
--wc-icon-size: 16px;
|
||||||
|
}
|
||||||
|
.field {
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
&.flex-2 {
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.name {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-ell {
|
||||||
|
overflow: hidden;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
color: var(--color-blue-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.red {
|
||||||
|
color: var(--color-red-1);
|
||||||
|
}
|
||||||
|
&.green {
|
||||||
|
color: var(--color-teal-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.thead {
|
||||||
|
color: var(--color-dark-2);
|
||||||
|
}
|
||||||
|
.item {
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-plain-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
]
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
this.#fetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
async #fetch() {
|
||||||
|
let list = await docker.networks()
|
||||||
|
this.$store.networks = list
|
||||||
|
}
|
||||||
|
|
||||||
|
async #remove(item) {
|
||||||
|
try {
|
||||||
|
await docker.network.rm(item.id)
|
||||||
|
this.#fetch()
|
||||||
|
} catch (err) {
|
||||||
|
layer.toast(err, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #clearUnUse(ev) {
|
||||||
|
try {
|
||||||
|
await layer.confirm('是否要清除本地未使用的网络?')
|
||||||
|
await docker.network.prune()
|
||||||
|
layer.toast('清除成功', 'success')
|
||||||
|
this.#fetch()
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let list = this.$store.networks
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<main class="main noselect">
|
||||||
|
<wc-space class="toolbar">
|
||||||
|
<wc-button
|
||||||
|
size="small"
|
||||||
|
type="warning"
|
||||||
|
icon="trash"
|
||||||
|
:disabled=${list.length < 1}
|
||||||
|
@click=${this.#clearUnUse}
|
||||||
|
>
|
||||||
|
清除未使用网络
|
||||||
|
</wc-button>
|
||||||
|
</wc-space>
|
||||||
|
|
||||||
|
<ul class="list">
|
||||||
|
<li class="thead">
|
||||||
|
<span class="field flex-2">网络/ID</span>
|
||||||
|
<span class="field">driver</span>
|
||||||
|
<span class="field center">IPv6</span>
|
||||||
|
<span class="field center">创建时间</span>
|
||||||
|
<span class="field center">操作</span>
|
||||||
|
</li>
|
||||||
|
${list.map(
|
||||||
|
it => html`
|
||||||
|
<li class="item">
|
||||||
|
<section class="field flex-2 name">
|
||||||
|
<span class="text-ell">${it.name}</span>
|
||||||
|
<code>${it.id}</code>
|
||||||
|
</section>
|
||||||
|
<section class="field">${it.driver}</section>
|
||||||
|
<section class="field center">${it.ipv6}</section>
|
||||||
|
<section class="field center">
|
||||||
|
<wc-time stamp=${new Date(it.created).getTime()}></wc-time>
|
||||||
|
</section>
|
||||||
|
<section class="field center">
|
||||||
|
<wc-popconfirm
|
||||||
|
title="是否要删除此网络?"
|
||||||
|
@confirm=${ev => this.#remove(it)}
|
||||||
|
>
|
||||||
|
<span class="action">🗑</span>
|
||||||
|
</wc-popconfirm>
|
||||||
|
</section>
|
||||||
|
</li>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</main>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Networks.reg('networks')
|
|
@ -0,0 +1,204 @@
|
||||||
|
/**
|
||||||
|
* {}
|
||||||
|
* @author yutent<yutent.io@gmail.com>
|
||||||
|
* @date 2023/08/08 18:19:17
|
||||||
|
*/
|
||||||
|
import { html, css, Component, classMap, nextTick, outsideClick } from 'wkit'
|
||||||
|
|
||||||
|
import { docker } from '../utils/index.js'
|
||||||
|
|
||||||
|
class Services extends Component {
|
||||||
|
static props = {}
|
||||||
|
|
||||||
|
static styles = [
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: var(--color-dark-1);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noselect {
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
|
||||||
|
css`
|
||||||
|
.main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.main .toolbar {
|
||||||
|
height: 48px;
|
||||||
|
border-bottom: 1px solid var(--color-plain-3);
|
||||||
|
}
|
||||||
|
.main .list {
|
||||||
|
overflow: auto;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
.item,
|
||||||
|
.thead {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
padding: 0 12px;
|
||||||
|
line-height: 1.25;
|
||||||
|
border-radius: 24px;
|
||||||
|
|
||||||
|
--wc-icon-size: 16px;
|
||||||
|
}
|
||||||
|
.field {
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
&.flex-2 {
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.name {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-ell {
|
||||||
|
overflow: hidden;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
color: var(--color-blue-1);
|
||||||
|
}
|
||||||
|
.action {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.red {
|
||||||
|
color: var(--color-red-1);
|
||||||
|
}
|
||||||
|
&.green {
|
||||||
|
color: var(--color-teal-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.thead {
|
||||||
|
color: var(--color-dark-2);
|
||||||
|
}
|
||||||
|
.item {
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-plain-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
]
|
||||||
|
|
||||||
|
#input = ''
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
this.#fetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
async #fetch() {
|
||||||
|
let list = await docker.services()
|
||||||
|
this.$store.services = list
|
||||||
|
}
|
||||||
|
|
||||||
|
#filter(ev) {
|
||||||
|
let txt = ev.currentTarget.value.trim()
|
||||||
|
if (this.#input === txt) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.#input = txt
|
||||||
|
this.$requestUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
async #stop(item) {
|
||||||
|
let { services } = this.$store
|
||||||
|
try {
|
||||||
|
await docker.service.stop(item.config)
|
||||||
|
} catch (err) {
|
||||||
|
if (err.includes('Stopped')) {
|
||||||
|
layer.toast('服务已停止', 'success')
|
||||||
|
this.#fetch()
|
||||||
|
} else {
|
||||||
|
layer.toast(err, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let list = this.$store.services
|
||||||
|
let txt = this.#input
|
||||||
|
if (txt) {
|
||||||
|
list = list.filter(it => it.name.includes(txt))
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<main class="main noselect">
|
||||||
|
<wc-space class="toolbar">
|
||||||
|
<wc-input
|
||||||
|
size="small"
|
||||||
|
round
|
||||||
|
placeholder="搜索服务"
|
||||||
|
@input=${this.#filter}
|
||||||
|
></wc-input>
|
||||||
|
</wc-space>
|
||||||
|
|
||||||
|
<ul class="list">
|
||||||
|
<li class="thead">
|
||||||
|
<span class="field flex-2">服务</span>
|
||||||
|
<span class="field flex-2">配置文件</span>
|
||||||
|
<span class="field center">状态</span>
|
||||||
|
<span class="field center">操作</span>
|
||||||
|
</li>
|
||||||
|
${list.map(
|
||||||
|
it => html`
|
||||||
|
<li class="item">
|
||||||
|
<section class="field flex-2">
|
||||||
|
<span class="text-ell">${it.name}</span>
|
||||||
|
</section>
|
||||||
|
<section class="field flex-2">${it.config}</section>
|
||||||
|
<section class="field center">${it.status}</section>
|
||||||
|
<section class="field center">
|
||||||
|
<wc-popconfirm
|
||||||
|
title="是否要停止此服务?"
|
||||||
|
@confirm=${ev => this.#stop(it)}
|
||||||
|
>
|
||||||
|
<span class="action red">⏹</span>
|
||||||
|
</wc-popconfirm>
|
||||||
|
</section>
|
||||||
|
</li>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</main>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Services.reg('services')
|
|
@ -0,0 +1,211 @@
|
||||||
|
/**
|
||||||
|
* {}
|
||||||
|
* @author yutent<yutent.io@gmail.com>
|
||||||
|
* @date 2023/08/08 18:19:17
|
||||||
|
*/
|
||||||
|
import { html, css, Component, classMap, nextTick, outsideClick } from 'wkit'
|
||||||
|
|
||||||
|
import { docker } from '../utils/index.js'
|
||||||
|
|
||||||
|
class Volumes extends Component {
|
||||||
|
static props = {}
|
||||||
|
|
||||||
|
static styles = [
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: var(--color-dark-1);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noselect {
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
|
||||||
|
css`
|
||||||
|
.main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main .toolbar {
|
||||||
|
height: 48px;
|
||||||
|
border-bottom: 1px solid var(--color-plain-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main .list {
|
||||||
|
overflow: auto;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
.item,
|
||||||
|
.thead {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
padding: 0 12px;
|
||||||
|
line-height: 1.25;
|
||||||
|
border-radius: 24px;
|
||||||
|
|
||||||
|
--wc-icon-size: 16px;
|
||||||
|
}
|
||||||
|
.field {
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
&.flex-2 {
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.name {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-ell {
|
||||||
|
overflow: hidden;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
color: var(--color-blue-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.red {
|
||||||
|
color: var(--color-red-1);
|
||||||
|
}
|
||||||
|
&.green {
|
||||||
|
color: var(--color-teal-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.thead {
|
||||||
|
color: var(--color-dark-2);
|
||||||
|
}
|
||||||
|
.item {
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-plain-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
]
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
this.#fetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
async #fetch() {
|
||||||
|
let list = await docker.volumes()
|
||||||
|
this.$store.volumes = list
|
||||||
|
}
|
||||||
|
|
||||||
|
async #remove(item) {
|
||||||
|
try {
|
||||||
|
await docker.volume.rm(item.name)
|
||||||
|
this.#fetch()
|
||||||
|
} catch (err) {
|
||||||
|
layer.toast(err, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #clearUnUse(ev) {
|
||||||
|
try {
|
||||||
|
await layer.confirm(
|
||||||
|
'是否要清除本地未使用的磁盘? <br>(此操作仅是清除docker的挂载, 不会删除本地文件)'
|
||||||
|
)
|
||||||
|
await docker.volume.prune()
|
||||||
|
layer.toast('清除成功', 'success')
|
||||||
|
this.#fetch()
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let list = this.$store.volumes
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<main class="main noselect">
|
||||||
|
<wc-space class="toolbar">
|
||||||
|
<wc-button
|
||||||
|
size="small"
|
||||||
|
type="warning"
|
||||||
|
icon="trash"
|
||||||
|
:disabled=${list.length < 1}
|
||||||
|
@click=${this.#clearUnUse}
|
||||||
|
>
|
||||||
|
清除未使用磁盘
|
||||||
|
</wc-button>
|
||||||
|
</wc-space>
|
||||||
|
|
||||||
|
<ul class="list">
|
||||||
|
<li class="thead">
|
||||||
|
<span class="field">name</span>
|
||||||
|
<span class="field center">Scope</span>
|
||||||
|
<span class="field center">挂载点</span>
|
||||||
|
<span class="field center">大小</span>
|
||||||
|
<span class="field center">状态</span>
|
||||||
|
<span class="field center">操作</span>
|
||||||
|
</li>
|
||||||
|
${list.map(
|
||||||
|
it => html`
|
||||||
|
<li class="item">
|
||||||
|
<section class="field name">
|
||||||
|
<wc-tooltip title=${it.name}>
|
||||||
|
<span class="text-ell">${it.name}</span>
|
||||||
|
</wc-tooltip>
|
||||||
|
</section>
|
||||||
|
<section class="field center">${it.scope}</section>
|
||||||
|
<section class="field center">
|
||||||
|
<wc-tooltip title=${it.mountpoint}>
|
||||||
|
<span class="text-ell">${it.mountpoint}</span>
|
||||||
|
</wc-tooltip>
|
||||||
|
</section>
|
||||||
|
<section class="field center">${it.size}</section>
|
||||||
|
<section class="field center">${it.status}</section>
|
||||||
|
<section class="field center">
|
||||||
|
<wc-popconfirm
|
||||||
|
title="是否要删除此镜像?"
|
||||||
|
@confirm=${ev => this.#remove(it)}
|
||||||
|
>
|
||||||
|
<span class="action">🗑</span>
|
||||||
|
</wc-popconfirm>
|
||||||
|
</section>
|
||||||
|
</li>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</main>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Volumes.reg('volumes')
|
Loading…
Reference in New Issue