增加consul web管理
This commit is contained in:
parent
c523e7e89c
commit
1590ac30c0
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
vf=0.1.3
|
||||
vb=0.2.0
|
||||
vf=0.3.0
|
||||
vb=0.3.0
|
||||
docker login --username=starsliao@163.com registry.cn-shenzhen.aliyuncs.com
|
||||
|
||||
docker tag nginx-consul:latest registry.cn-shenzhen.aliyuncs.com/starsl/nginx-consul:latest
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import os
|
||||
from itsdangerous import TimedJSONWebSignatureSerializer
|
||||
|
||||
consul_token = os.environ.get('consul_token','a94d1ecb-81d3-ea0a-4dc8-5e6701e528c5')
|
||||
consul_url = os.environ.get('consul_url','http://10.5.148.67:8500/v1')
|
||||
admin_passwd = os.environ.get('admin_passwd','cass.007')
|
||||
consul_token = os.environ.get('consul_token','635abc53-c18c-f780-58a9-f04feb28fef1')
|
||||
consul_url = os.environ.get('consul_url','http://10.0.0.26:8500/v1')
|
||||
admin_passwd = os.environ.get('admin_passwd','123456')
|
||||
secret_key = os.environ.get('secret_key',consul_token)
|
||||
s = TimedJSONWebSignatureSerializer(secret_key)
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
#!/usr/bin/env python3
|
||||
from flask import Flask
|
||||
from views import login, blackbox
|
||||
from views import login, blackbox, consul
|
||||
|
||||
app = Flask(__name__)
|
||||
app.register_blueprint(login.blueprint)
|
||||
app.register_blueprint(blackbox.blueprint)
|
||||
app.register_blueprint(consul.blueprint)
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=2026)
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
import requests,json
|
||||
import sys
|
||||
sys.path.append("..")
|
||||
from config import consul_token,consul_url
|
||||
|
||||
headers = {'X-Consul-Token': consul_token}
|
||||
|
||||
def get_hosts():
|
||||
url = f'{consul_url}/agent/host'
|
||||
response = requests.get(url, headers=headers)
|
||||
if response.status_code == 200:
|
||||
info = response.json()
|
||||
pmem = round(info["Memory"]["usedPercent"])
|
||||
pdisk = round(info["Disk"]["usedPercent"])
|
||||
host = {'hostname':info["Host"]["hostname"],'uptime':f'{round(info["Host"]["uptime"]/3600/24)}天',
|
||||
'os':f'{info["Host"]["platform"]} {info["Host"]["platformVersion"]}','kernel':info["Host"]["kernelVersion"]}
|
||||
cpu = {'cores':f'{len(info["CPU"])}核','vendorId':info["CPU"][0]["vendorId"],'modelName':info["CPU"][0]["modelName"]}
|
||||
memory = {'total':f'{round(info["Memory"]["total"]/1024**3)}GB','available':f'{round(info["Memory"]["available"]/1024**3)}GB',
|
||||
'used':f'{round(info["Memory"]["used"]/1024**3)}GB','usedPercent':f'{pmem}%'}
|
||||
disk = {'path':info["Disk"]["path"],'fstype':info["Disk"]["fstype"],'total':f'{round(info["Disk"]["total"]/1024**3)}GB',
|
||||
'free':f'{round(info["Disk"]["free"]/1024**3)}GB','used':f'{round(info["Disk"]["used"]/1024**3)}GB','usedPercent':f'{pdisk}%'}
|
||||
return {'code': 20000,'host':host,'cpu':cpu,'memory':memory,'disk':disk, 'pmem':pmem, 'pdisk':pdisk}
|
||||
else:
|
||||
return {'code': 50000, 'data': f'{response.status_code}:{response.text}'}
|
||||
def get_services():
|
||||
url = f'{consul_url}/internal/ui/services'
|
||||
response = requests.get(url, headers=headers)
|
||||
if response.status_code == 200:
|
||||
info = response.json()
|
||||
services_list = [{'Name':i['Name'],'Datacenter':i['Datacenter'],'InstanceCount':i['InstanceCount'],'ChecksCritical':i['ChecksCritical'],'ChecksPassing':i['ChecksPassing'],'Tags':i['Tags'],'Nodes':list(set(i['Nodes']))} for i in info if i['Name'] != 'consul']
|
||||
return {'code': 20000,'services':services_list}
|
||||
else:
|
||||
return {'code': 50000, 'data': f'{response.status_code}:{response.text}'}
|
||||
def get_services_nameonly():
|
||||
url = f'{consul_url}/catalog/services'
|
||||
response = requests.get(url, headers=headers)
|
||||
if response.status_code == 200:
|
||||
info = response.json()
|
||||
info.pop('consul')
|
||||
return {'code': 20000,'services_name':list(info.keys())}
|
||||
else:
|
||||
return {'code': 50000, 'data': f'{response.status_code}:{response.text}'}
|
||||
def get_instances(service_name):
|
||||
url = f'{consul_url}/health/service/{service_name}'
|
||||
response = requests.get(url, headers=headers)
|
||||
if response.status_code == 200:
|
||||
info = response.json()
|
||||
instances_list = []
|
||||
for i in info:
|
||||
instance_dict = {}
|
||||
instance_dict['ID'] = i['Service']['ID']
|
||||
instance_dict['name'] = i['Service']['Service']
|
||||
instance_dict['tags'] = '无' if i['Service']['Tags'] == [] else i['Service']['Tags']
|
||||
instance_dict['address'] = i['Service'].get('Address')
|
||||
instance_dict['port'] = i['Service'].get('Port')
|
||||
if i['Service']['Meta'] == {} or i['Service']['Meta'] is None:
|
||||
instance_dict['meta'] = '无'
|
||||
else:
|
||||
instance_dict['meta'] = [i['Service']['Meta']]
|
||||
instance_dict['meta_label'] = [{'prop': x, 'label': x} for x in i['Service']['Meta'].keys()]
|
||||
if len(i['Checks']) ==2:
|
||||
instance_dict['status'] = i['Checks'][1]['Status']
|
||||
instance_dict['output'] = i['Checks'][1]['Output']
|
||||
else:
|
||||
instance_dict['status'] = '无'
|
||||
instance_dict['output'] = '无'
|
||||
instances_list.append(instance_dict)
|
||||
return {'code': 20000,'instances':instances_list}
|
||||
else:
|
||||
return {'code': 50000, 'data': f'{response.status_code}:{response.text}'}
|
||||
|
||||
def del_instance(service_id):
|
||||
reg = requests.put(f'{consul_url}/agent/service/deregister/{service_id}', headers=headers)
|
||||
if reg.status_code == 200:
|
||||
return {"code": 20000, "data": f"【{service_id}】删除成功!"}
|
||||
else:
|
||||
return {"code": 50000, "data": f"{reg.status_code}【{service_id}】{reg.text}"}
|
||||
|
||||
def add_instance(instance_dict):
|
||||
isMeta = instance_dict['metaInfo']['isMeta']
|
||||
isCheck = instance_dict['checkInfo']['isCheck']
|
||||
address = instance_dict['address']
|
||||
port = None if instance_dict['port'] == '' else int(instance_dict['port'])
|
||||
instance_dict['port'] = port
|
||||
if isMeta:
|
||||
try:
|
||||
metaJson = json.loads(instance_dict['metaInfo']['metaJson'])
|
||||
instance_dict['meta'] = metaJson
|
||||
except:
|
||||
return {"code": 50000, "data": "Meta必须JSON字符串格式!"}
|
||||
if isCheck:
|
||||
ctype = instance_dict['checkInfo']['ctype']
|
||||
interval = instance_dict['checkInfo']['interval']
|
||||
timeout = instance_dict['checkInfo']['timeout']
|
||||
if instance_dict['checkInfo']['isAddress'] == 'true':
|
||||
if port is not None and address != '' and ctype != 'HTTP':
|
||||
checkaddress = f'{address}:{port}'
|
||||
elif port is not None and address != '' and ctype == 'HTTP':
|
||||
checkaddress = f'http://{address}:{port}'
|
||||
else:
|
||||
return {"code": 50000, "data": "健康检查地址使用与实例IP端口一致时,地址/端口不可为空!"}
|
||||
else:
|
||||
checkaddress = instance_dict['checkInfo']['caddress']
|
||||
if checkaddress == '':
|
||||
return {"code": 50000, "data": "自定义健康检查,地址信息不可为空!"}
|
||||
|
||||
check = {ctype: checkaddress,"Interval": interval,"Timeout": timeout}
|
||||
instance_dict['check'] = check
|
||||
|
||||
del instance_dict['metaInfo']
|
||||
del instance_dict['checkInfo']
|
||||
print(instance_dict)
|
||||
|
||||
reg = requests.put(f'{consul_url}/agent/service/register', headers=headers, data=json.dumps(instance_dict))
|
||||
sid = instance_dict['ID']
|
||||
if reg.status_code == 200:
|
||||
return {"code": 20000, "data": f"【{sid}】增加成功!"}
|
||||
else:
|
||||
return {"code": 50000, "data": f"{reg.status_code}【{sid}】{reg.text}"}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
from flask import Blueprint
|
||||
from flask_restful import reqparse, Resource, Api
|
||||
import sys
|
||||
sys.path.append("..")
|
||||
from units import token_auth,consul_manager
|
||||
|
||||
blueprint = Blueprint('consul',__name__)
|
||||
api = Api(blueprint)
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument('service_name',type=str)
|
||||
parser.add_argument('sid',type=str)
|
||||
parser.add_argument('instance_dict',type=dict)
|
||||
|
||||
class ConsulApi(Resource):
|
||||
decorators = [token_auth.auth.login_required]
|
||||
def get(self, stype):
|
||||
if stype == 'services':
|
||||
return consul_manager.get_services()
|
||||
elif stype == 'services_name':
|
||||
return consul_manager.get_services_nameonly()
|
||||
elif stype == 'instances':
|
||||
args = parser.parse_args()
|
||||
return consul_manager.get_instances(args['service_name'])
|
||||
elif stype == 'hosts':
|
||||
return consul_manager.get_hosts()
|
||||
|
||||
def post(self, stype):
|
||||
if stype == 'sid':
|
||||
args = parser.parse_args()
|
||||
return consul_manager.add_instance(args['instance_dict'])
|
||||
|
||||
def put(self, stype):
|
||||
if stype == 'sid':
|
||||
args = parser.parse_args()
|
||||
resp_del = consul_manager.del_instance(args['sid'])
|
||||
resp_add = consul_manager.add_instance(args['instance_dict'])
|
||||
if resp_del["code"] == 20000 and resp_add["code"] == 20000:
|
||||
return {"code": 20000, "data": f"更新成功!"}
|
||||
else:
|
||||
return {"code": 50000, "data": f"更新失败!"}
|
||||
|
||||
def delete(self, stype):
|
||||
if stype == 'sid':
|
||||
args = parser.parse_args()
|
||||
return consul_manager.del_instance(args['sid'])
|
||||
|
||||
api.add_resource(ConsulApi, '/api/consul/<stype>')
|
|
@ -14,6 +14,8 @@ module.exports = {
|
|||
// add your custom rules here
|
||||
//it is base on https://github.com/vuejs/eslint-config-vue
|
||||
rules: {
|
||||
"no-irregular-whitespace": "off",
|
||||
"vue/html-quotes": "off",
|
||||
"vue/max-attributes-per-line": [2, {
|
||||
"singleline": 10,
|
||||
"multiline": {
|
||||
|
@ -96,7 +98,6 @@ module.exports = {
|
|||
'no-implied-eval': 2,
|
||||
'no-inner-declarations': [2, 'functions'],
|
||||
'no-invalid-regexp': 2,
|
||||
'no-irregular-whitespace': 2,
|
||||
'no-iterator': 2,
|
||||
'no-label-var': 2,
|
||||
'no-labels': [2, {
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import request from '@/utils/request-ops'
|
||||
|
||||
export function getHosts() {
|
||||
return request({
|
||||
url: '/api/consul/hosts',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getServices() {
|
||||
return request({
|
||||
url: '/api/consul/services',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getServicesName() {
|
||||
return request({
|
||||
url: '/api/consul/services_name',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getInstances(service_name) {
|
||||
return request({
|
||||
url: '/api/consul/instances',
|
||||
method: 'get',
|
||||
params: { service_name }
|
||||
})
|
||||
}
|
||||
|
||||
export function delSid(sid) {
|
||||
return request({
|
||||
url: '/api/consul/sid',
|
||||
method: 'delete',
|
||||
params: { sid }
|
||||
})
|
||||
}
|
||||
|
||||
export function addSid(instance_dict) {
|
||||
return request({
|
||||
url: '/api/consul/sid',
|
||||
method: 'post',
|
||||
data: { instance_dict }
|
||||
})
|
||||
}
|
||||
|
||||
export function updateSid(sid, instance_dict) {
|
||||
return request({
|
||||
url: '/api/consul/sid',
|
||||
method: 'put',
|
||||
data: { sid, instance_dict }
|
||||
})
|
||||
}
|
|
@ -46,7 +46,7 @@ export const constantRoutes = [
|
|||
{
|
||||
path: '/',
|
||||
component: Layout,
|
||||
redirect: '/blackbox/index',
|
||||
redirect: '/dashboard',
|
||||
children: [{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
|
@ -55,16 +55,66 @@ export const constantRoutes = [
|
|||
}]
|
||||
},
|
||||
|
||||
{
|
||||
path: '/consul',
|
||||
component: Layout,
|
||||
redirect: '/consul/services',
|
||||
name: 'Consul 管理',
|
||||
meta: { title: 'Consul 管理', icon: 'example' },
|
||||
children: [
|
||||
{
|
||||
path: 'hosts',
|
||||
name: 'Hosts',
|
||||
component: () => import('@/views/consul/hosts'),
|
||||
meta: { title: 'Hosts', icon: 'el-icon-school' }
|
||||
},
|
||||
{
|
||||
path: 'services',
|
||||
name: 'Services',
|
||||
component: () => import('@/views/consul/services'),
|
||||
meta: { title: 'Services', icon: 'el-icon-news' }
|
||||
},
|
||||
{
|
||||
path: 'instances',
|
||||
name: 'Instances',
|
||||
component: () => import('@/views/consul/instances'),
|
||||
meta: { title: 'Instances', icon: 'el-icon-connection' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/blackbox',
|
||||
component: Layout,
|
||||
children: [{
|
||||
path: 'index',
|
||||
name: '站点监控',
|
||||
name: 'Blackbox 站点监控',
|
||||
component: () => import('@/views/blackbox/index'),
|
||||
meta: { title: '站点监控', icon: 'tree' }
|
||||
meta: { title: 'Blackbox 站点监控', icon: 'tree' }
|
||||
}]
|
||||
},
|
||||
{
|
||||
path: '友情链接',
|
||||
component: Layout,
|
||||
meta: { title: '友情链接', icon: 'link' },
|
||||
children: [
|
||||
{
|
||||
path: 'https://starsl.cn',
|
||||
meta: { title: 'StarsL.cn', icon: 'el-icon-s-custom' }
|
||||
},
|
||||
{
|
||||
path: 'https://github.com/starsliao?tab=repositories',
|
||||
meta: { title: '我的Github', icon: 'el-icon-star-off' }
|
||||
},
|
||||
{
|
||||
path: 'https://grafana.com/orgs/starsliao/dashboards',
|
||||
meta: { title: '我的Grafana', icon: 'el-icon-odometer' }
|
||||
},
|
||||
{
|
||||
path: 'https://starsl.cn/static/img/qr.png',
|
||||
meta: { title: '我的公众号', icon: 'el-icon-chat-dot-round' }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// 404 page must be placed at the end !!!
|
||||
{ path: '*', redirect: '/404', hidden: true }
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
module.exports = {
|
||||
|
||||
title: 'Blackbox Manager',
|
||||
title: 'Consul Manager',
|
||||
|
||||
/**
|
||||
* @type {boolean} true | false
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
<template>
|
||||
<div class="app-container">
|
||||
<el-row :gutter="30" type="flex" class="row-bg" justify="center">
|
||||
<el-col :span="8">
|
||||
<el-card shadow="always">
|
||||
<div slot="header" class="clearfix">
|
||||
<span><el-button type="primary" icon="el-icon-s-platform" size="medium" circle /> 主机</span>
|
||||
</div>
|
||||
<el-descriptions direction="vertical" :column="2" border>
|
||||
<el-descriptions-item v-for="( value, label ) in host" :key="label" :label="label">{{ value }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card shadow="always">
|
||||
<div slot="header" class="clearfix">
|
||||
<span><el-button type="success" icon="el-icon-cpu" size="medium" circle /> CPU</span>
|
||||
</div>
|
||||
<el-descriptions direction="vertical" :column="2" border>
|
||||
<el-descriptions-item v-for="( value, label ) in cpu" :key="label" :label="label">{{ value }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<br><br>
|
||||
<el-row :gutter="30" type="flex" class="row-bg" justify="center">
|
||||
<el-col :span="8">
|
||||
<el-card shadow="always">
|
||||
<div slot="header" class="clearfix">
|
||||
<span><el-button type="warning" icon="el-icon-set-up" size="medium" circle /> 内存</span>
|
||||
</div>
|
||||
<el-descriptions direction="vertical" :column="2" border>
|
||||
<el-descriptions-item v-for="( value, label ) in memory" :key="label" :label="label">{{ value }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-progress :text-inside="true" :percentage="pmem" :stroke-width="24" :color="customColorMethod" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card shadow="always">
|
||||
<div slot="header" class="clearfix">
|
||||
<span><el-button type="info" icon="el-icon-coin" size="medium" circle /> 磁盘</span>
|
||||
</div>
|
||||
<el-descriptions direction="vertical" :column="3" border>
|
||||
<el-descriptions-item v-for="( value, label ) in disk" :key="label" :label="label">{{ value }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-progress :text-inside="true" :percentage="pdisk" :stroke-width="24" :color="customColorMethod" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getHosts } from '@/api/consul'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
listLoading: true,
|
||||
host: {},
|
||||
cpu: {},
|
||||
memory: {},
|
||||
disk: {},
|
||||
pmem: 0,
|
||||
pdisk: 0
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchData()
|
||||
},
|
||||
methods: {
|
||||
customColorMethod(percentage) {
|
||||
if (percentage < 40) {
|
||||
return '#67C23A'
|
||||
} else if (percentage < 80) {
|
||||
return '#E6A23C'
|
||||
} else {
|
||||
return '#F56C6C'
|
||||
}
|
||||
},
|
||||
fetchData() {
|
||||
this.listLoading = true
|
||||
getHosts().then(response => {
|
||||
this.host = response.host
|
||||
this.cpu = response.cpu
|
||||
this.memory = response.memory
|
||||
this.disk = response.disk
|
||||
this.pmem = response.pmem
|
||||
this.pdisk = response.pdisk
|
||||
this.listLoading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.clearfix {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.item {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.clearfix:before,
|
||||
.clearfix:after {
|
||||
display: table;
|
||||
content: "";
|
||||
}
|
||||
.clearfix:after {
|
||||
clear: both
|
||||
}
|
||||
|
||||
.box-card {
|
||||
width: 480px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,506 @@
|
|||
<template>
|
||||
<div class="app-container">
|
||||
<el-select v-model="services_name" placeholder="请选择 Services" filterable collapse-tags style="width: 250px" class="filter-item" @change="fetchData(services_name)">
|
||||
<el-option v-for="item in services_name_list" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
<el-tooltip class="item" effect="light" content="刷新当前Services" placement="top">
|
||||
<el-button class="filter-item" style="margin-left: 10px;" type="primary" icon="el-icon-refresh" circle @click="fetchData(services_name)" />
|
||||
</el-tooltip>
|
||||
<el-button class="filter-item" type="primary" icon="el-icon-edit" @click="handleCreate">
|
||||
新增
|
||||
</el-button>
|
||||
<el-button class="filter-item" type="danger" icon="el-icon-delete" @click="handleDelAll">
|
||||
批量删除
|
||||
</el-button>
|
||||
|
||||
<el-table ref="expandstable" v-loading="listLoading" :data="instances" border fit highlight-current-row style="width: 100%;" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" align="center" width="30" />
|
||||
<el-table-column label="ID" width="50px" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.$index+1 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="id" label="实例ID" sortable align="center" show-overflow-tooltip>
|
||||
<template slot-scope="{row}">
|
||||
<span>{{ row.ID }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="address" label="地址" sortable width="150px" align="center" show-overflow-tooltip>
|
||||
<template slot-scope="{row}">
|
||||
<span>{{ row.address }} </span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="port" label="端口" width="80px" sortable align="center">
|
||||
<template slot-scope="{row}">
|
||||
<span>{{ row.port }} </span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="tags" label="Tags" sortable align="center" width="200">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag v-for="atag in row.tags" :key="atag" size="mini">{{ atag }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="meta" label="Meta" align="center" width="80px">
|
||||
<template slot-scope="{row}">
|
||||
<span v-if="row.meta === '无'">{{ row.meta }}</span>
|
||||
<el-link v-else type="primary" @click="expandsHandle(row)">展开</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="status" label="状态" width="80px" sortable align="center">
|
||||
<template slot-scope="{row}">
|
||||
<span>{{ row.status }} </span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="output" label="检查明细" sortable align="center" show-overflow-tooltip>
|
||||
<template slot-scope="{row}">
|
||||
<span style="font-size: 12px">{{ row.output }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width">
|
||||
<template slot-scope="{row}">
|
||||
<el-button type="primary" size="mini" @click="handleUpdate(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button size="mini" type="danger" @click="handleDelete(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column type="expand" width="1">
|
||||
<template slot-scope="{row}">
|
||||
<el-table style="width: 100%" :data="row.meta" row-class-name="success-row" fit border>
|
||||
<el-table-column v-for="{ prop, label } in row.meta_label" :key="prop" :prop="prop" :label="label" />
|
||||
</el-table>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog :title="textMap[dialogStatus]" :visible.sync="dialogFormVisible" width="45%">
|
||||
<el-form ref="dataForm" :rules="rules" :model="newService" label-position="right" label-width="100px" style="width: 500px; margin-left: 50px;">
|
||||
<el-form-item label="所属服务组" prop="name">
|
||||
<el-autocomplete v-model="newService.name" :fetch-suggestions="Sugg_name" placeholder="优先选择" clearable style="width: 360px" class="filter-item" />
|
||||
</el-form-item>
|
||||
<div v-if="dialogStatus==='update'">
|
||||
<el-form-item label="服务实例ID" prop="ID">
|
||||
<el-input v-model="newService.ID" placeholder="请输入" clearable style="width: 360px" :disabled="true" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-form-item label="服务实例ID" prop="ID">
|
||||
<el-input v-model="newService.ID" placeholder="请输入" clearable style="width: 360px" class="filter-item" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="地址" prop="address">
|
||||
<el-input v-model="newService.address" placeholder="请输入" clearable style="width: 360px" class="filter-item" />
|
||||
</el-form-item>
|
||||
<el-form-item label="端口" prop="port">
|
||||
<el-input v-model="newService.port" placeholder="请输入" clearable style="width: 360px" class="filter-item" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Tags" prop="tags">
|
||||
<el-tag v-for="tag in newService.tags" :key="tag" closable :disable-transitions="false" @close="handleClose(tag)">{{ tag }}</el-tag>
|
||||
<el-input v-if="inputVisible" ref="saveTagInput" v-model="inputValue" class="input-new-tag" size="small" @keyup.enter.native="handleInputConfirm" @blur="handleInputConfirm" />
|
||||
<el-button v-else class="button-new-tag" size="small" type="primary" icon="el-icon-circle-plus-outline" @click="showInput">新增</el-button>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="配置Meta" prop="isMeta">
|
||||
<el-switch v-model="newService.metaInfo.isMeta" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="newService.metaInfo.isMeta" prop="newmeta">
|
||||
<span slot="label">
|
||||
<span class="span-box">
|
||||
<span>Meta</span>
|
||||
<el-tooltip style="diaplay:inline" effect="dark" content='Meta必须是JSON字符串格式,例如:{ "aaa":"bbb", "ccc": "ddd" }' placement="top">
|
||||
<i class="el-icon-info" />
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</span>
|
||||
<el-input v-model="newService.metaInfo.metaJson" :autosize="{ minRows: 2, maxRows: 4}" type="textarea" placeholder='{ "aaa": "bbb", "ccc": "ddd" }' clearable style="width: 360px" class="filter-item" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="coption !== '' && dialogStatus==='update'" label="健康检查操作" prop="coption">
|
||||
<el-radio-group v-model="coption" @change="modcheck">
|
||||
<el-radio label="false">不修改</el-radio>
|
||||
<el-radio label="delete">删除健康检查</el-radio>
|
||||
<el-radio label="modf">修改健康检查</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form :inline="true" class="demo-form-inline" label-position="right" label-width="100px">
|
||||
<el-form-item v-if="coption === '' || coption === 'modf'" label="健康检查" prop="isCheck">
|
||||
<el-switch v-model="newService.checkInfo.isCheck" active-text=" " />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="newService.checkInfo.isCheck" label="检查类型" prop="ctype">
|
||||
<el-select v-model="newService.checkInfo.ctype" placeholder="请选择" style="width: 120px">
|
||||
<el-option label="TCP" value="TCP" />
|
||||
<el-option label="HTTP" value="HTTP" />
|
||||
<el-option label="GRPC" value="GRPC" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-form-item v-if="newService.checkInfo.isCheck" label="检查地址" prop="isAddress">
|
||||
<el-radio-group v-model="newService.checkInfo.isAddress">
|
||||
<el-radio label="true">与实例IP端口一致</el-radio>
|
||||
<el-radio label="false">自定义</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="newService.checkInfo.isCheck && newService.checkInfo.isAddress === 'false'" prop="caddress">
|
||||
<span slot="label">
|
||||
<span class="span-box">
|
||||
<span>地址信息</span>
|
||||
<el-tooltip style="diaplay:inline" effect="dark" content="检查类型为HTTP时,地址必须以http开头。" placement="top">
|
||||
<i class="el-icon-info" />
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</span>
|
||||
<el-input v-model="newService.checkInfo.caddress" placeholder="请输入" clearable style="width: 360px" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form v-if="newService.checkInfo.isCheck" :inline="true" class="demo-form-inline" label-position="right" label-width="100px">
|
||||
<el-form-item label="检查间隔" prop="interval">
|
||||
<el-input v-model="newService.checkInfo.interval" placeholder="请输入" clearable style="width: 120px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="检查超时" prop="timeout">
|
||||
<el-input v-model="newService.checkInfo.timeout" placeholder="请输入" clearable style="width: 120px" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-form>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button v-if="dialogStatus==='create'" type="primary" @click="createAndNew">
|
||||
确认并新增
|
||||
</el-button>
|
||||
<el-button @click="dialogFormVisible = false">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button type="primary" @click="dialogStatus==='create'?createData():updateData()">
|
||||
确认
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getInstances, getServicesName, delSid, addSid } from '@/api/consul'
|
||||
export default {
|
||||
data() {
|
||||
const validateInput = (rule, value, callback) => {
|
||||
if (!this.checkSpecialKey(value)) {
|
||||
callback(new Error('不能含有空格或 [ ]`~!#$^&*=|"{}\':;/?'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
return {
|
||||
instances: [],
|
||||
services_name: '',
|
||||
services_name_list: [],
|
||||
multipleSelection: [],
|
||||
newService: {
|
||||
ID: '',
|
||||
name: '',
|
||||
address: '',
|
||||
port: '',
|
||||
tags: [],
|
||||
metaInfo: {
|
||||
isMeta: false,
|
||||
metaJson: ''
|
||||
},
|
||||
checkInfo: {
|
||||
isCheck: false,
|
||||
ctype: 'TCP',
|
||||
isAddress: 'true',
|
||||
caddress: '',
|
||||
interval: '15s',
|
||||
timeout: '5s'
|
||||
}
|
||||
},
|
||||
coption: '',
|
||||
dialogFormVisible: false,
|
||||
dialogStatus: '',
|
||||
textMap: {
|
||||
update: '更新',
|
||||
create: '创建'
|
||||
},
|
||||
value_name: [],
|
||||
inputVisible: false,
|
||||
inputValue: '',
|
||||
rules: {
|
||||
name: [{ required: true, message: '此为必填项', trigger: 'change' },
|
||||
{ validator: validateInput, trigger: ['blur', 'change'] }],
|
||||
ID: [{ required: true, message: '此为必填项', trigger: 'change' },
|
||||
{ validator: validateInput, trigger: ['blur', 'change'] }]
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchServicesName()
|
||||
if (this.$route.query.service_name) {
|
||||
this.fetchData(this.$route.query.service_name)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.$route.query.service_name) {
|
||||
this.services_name = this.$route.query.service_name
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
expandsHandle(row) {
|
||||
this.$refs.expandstable.toggleRowExpansion(row)
|
||||
},
|
||||
handleSelectionChange(val) {
|
||||
this.multipleSelection = val
|
||||
},
|
||||
Sugg_name(queryString, cb) {
|
||||
var xname = this.xname
|
||||
var results = queryString ? xname.filter(this.createFilter(queryString)) : xname
|
||||
cb(results)
|
||||
},
|
||||
load_name() {
|
||||
for (const x in this.services_name_list) {
|
||||
this.value_name.push({ 'value': this.services_name_list[x] })
|
||||
}
|
||||
return this.value_name
|
||||
},
|
||||
createFilter(queryString) {
|
||||
return (restaurant) => {
|
||||
return (restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0)
|
||||
}
|
||||
},
|
||||
handleClose(tag) {
|
||||
this.newService.tags.splice(this.newService.tags.indexOf(tag), 1)
|
||||
},
|
||||
|
||||
showInput() {
|
||||
this.inputVisible = true
|
||||
this.$nextTick(_ => {
|
||||
this.$refs.saveTagInput.$refs.input.focus()
|
||||
})
|
||||
},
|
||||
|
||||
handleInputConfirm() {
|
||||
const inputValue = this.inputValue
|
||||
if (inputValue) {
|
||||
this.newService.tags.push(inputValue)
|
||||
}
|
||||
this.inputVisible = false
|
||||
this.inputValue = ''
|
||||
},
|
||||
checkSpecialKey(str) {
|
||||
const specialKey = '[]`~!#$^&*=|{}\'":;/? '
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
if (specialKey.indexOf(str.substr(i, 1)) !== -1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
modcheck(label) {
|
||||
if (label === 'modf') {
|
||||
this.newService.checkInfo.isCheck = true
|
||||
} else {
|
||||
this.newService.checkInfo.isCheck = false
|
||||
}
|
||||
},
|
||||
handleCreate() {
|
||||
this.coption = ''
|
||||
this.newService = { ID: '', name: '', address: '', port: '', tags: [], metaInfo: { isMeta: false, metaJson: '' }, checkInfo: { isCheck: false, ctype: 'TCP', isAddress: 'true', caddress: '', interval: '15s', timeout: '5s' }}
|
||||
this.dialogStatus = 'create'
|
||||
this.dialogFormVisible = true
|
||||
this.newService.name = this.services_name
|
||||
this.$nextTick(() => {
|
||||
this.$refs['dataForm'].clearValidate()
|
||||
})
|
||||
},
|
||||
createData() {
|
||||
this.$refs['dataForm'].validate((valid) => {
|
||||
if (valid) {
|
||||
this.newService.tags = [...new Set(this.newService.tags)]
|
||||
addSid(this.newService).then(response => {
|
||||
this.fetchServicesName()
|
||||
this.services_name = this.newService.name
|
||||
this.fetchData(this.newService.name)
|
||||
this.dialogFormVisible = false
|
||||
this.$message({
|
||||
message: response.data,
|
||||
type: 'success'
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
createAndNew() {
|
||||
this.$refs['dataForm'].validate((valid) => {
|
||||
if (valid) {
|
||||
this.newService.tags = [...new Set(this.newService.tags)]
|
||||
addSid(this.newService).then(response => {
|
||||
this.fetchServicesName()
|
||||
this.services_name = this.newService.name
|
||||
this.fetchData(this.newService.name)
|
||||
this.$message({
|
||||
message: response.data,
|
||||
type: 'success'
|
||||
})
|
||||
this.dialogStatus = 'create'
|
||||
this.newService.ID = ''
|
||||
this.newService.address = ''
|
||||
this.newService.port = ''
|
||||
this.$nextTick(() => {
|
||||
this.$refs['dataForm'].clearValidate()
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
fetchData(sname) {
|
||||
this.listLoading = true
|
||||
getInstances(sname).then(response => {
|
||||
this.instances = response.instances
|
||||
this.listLoading = false
|
||||
})
|
||||
},
|
||||
fetchServicesName() {
|
||||
this.listLoading = true
|
||||
getServicesName().then(response => {
|
||||
this.services_name_list = response.services_name
|
||||
this.listLoading = false
|
||||
this.xname = this.load_name()
|
||||
})
|
||||
},
|
||||
handleUpdate(row) {
|
||||
this.coption = ''
|
||||
this.newService.checkInfo.isCheck = false
|
||||
this.newService.ID = row.ID
|
||||
this.newService.name = row.name
|
||||
this.newService.address = row.address
|
||||
this.newService.port = row.port
|
||||
if (row.tags === '无') {
|
||||
this.newService.tags = []
|
||||
} else {
|
||||
this.newService.tags = row.tags
|
||||
}
|
||||
if (row.meta === '无') {
|
||||
this.newService.metaInfo.isMeta = false
|
||||
} else {
|
||||
this.newService.metaInfo.isMeta = true
|
||||
this.newService.metaInfo.metaJson = JSON.stringify(row.meta[0])
|
||||
}
|
||||
if (row.status === '无') {
|
||||
this.newService.checkInfo.isCheck = false
|
||||
} else {
|
||||
this.coption = 'false'
|
||||
}
|
||||
|
||||
this.dialogStatus = 'update'
|
||||
this.dialogFormVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs['dataForm'].clearValidate()
|
||||
})
|
||||
},
|
||||
updateData() {
|
||||
this.$refs['dataForm'].validate((valid) => {
|
||||
if (valid) {
|
||||
if (this.coption === 'delete') {
|
||||
delSid(this.newService.ID).then(response => {
|
||||
addSid(this.newService).then(response => {
|
||||
this.fetchServicesName()
|
||||
this.services_name = this.newService.name
|
||||
this.fetchData(this.newService.name)
|
||||
this.dialogFormVisible = false
|
||||
this.$message({
|
||||
message: response.data,
|
||||
type: 'success'
|
||||
})
|
||||
})
|
||||
})
|
||||
} else {
|
||||
addSid(this.newService).then(response => {
|
||||
this.fetchServicesName()
|
||||
this.services_name = this.newService.name
|
||||
this.fetchData(this.newService.name)
|
||||
this.dialogFormVisible = false
|
||||
this.$message({
|
||||
message: response.data,
|
||||
type: 'success'
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
handleDelete(row) {
|
||||
this.$confirm('此操作将删除【' + row.name + '】:\n' + row.ID + ',是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
delSid(row.ID).then(response => {
|
||||
this.$message({
|
||||
message: response.data,
|
||||
type: 'success'
|
||||
})
|
||||
this.$delete(this.instances, this.instances.indexOf(row))
|
||||
})
|
||||
}).catch(() => {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: '已取消删除'
|
||||
})
|
||||
})
|
||||
},
|
||||
handleDelAll() {
|
||||
this.$confirm('此操作将批量删除选中行,是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
for (let i = 0; i < this.multipleSelection.length; i++) {
|
||||
delSid(this.multipleSelection[i].ID).then(response => {
|
||||
this.$message({
|
||||
message: response.data,
|
||||
type: 'success'
|
||||
})
|
||||
})
|
||||
this.$delete(this.instances, this.instances.indexOf(this.multipleSelection[i]))
|
||||
}
|
||||
}).catch(() => {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: '已取消删除'
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.el-table__expand-column .cell {
|
||||
display: none;
|
||||
}
|
||||
.el-table .success-row {
|
||||
background: oldlace;
|
||||
}
|
||||
.el-tag + .el-tag {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.button-new-tag {
|
||||
margin-left: 10px;
|
||||
height: 32px;
|
||||
line-height: 30px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.input-new-tag {
|
||||
width: 90px;
|
||||
margin-left: 10px;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,91 @@
|
|||
<template>
|
||||
<div class="app-container">
|
||||
<el-table
|
||||
v-loading="listLoading"
|
||||
:data="services"
|
||||
border
|
||||
fit
|
||||
highlight-current-row
|
||||
style="width: 100%;"
|
||||
>
|
||||
<el-table-column label="ID" width="73px" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.$index+1 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="Name" label="服务名" sortable align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-link type="primary" @click="handleInstances(row.Name)">{{ row.Name }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="Nodes" label="节点" sortable align="center" width="200">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag v-for="atag in row.Nodes" :key="atag" size="mini" effect="dark">{{ atag }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="Datacenter" label="数据中心" sortable align="center" width="120">
|
||||
<template slot-scope="{row}">
|
||||
<span>{{ row.Datacenter }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="Tags" label="Tags" sortable align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag v-for="atag in row.Tags" :key="atag" size="mini">{{ atag }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="InstanceCount" label="实例数" sortable align="center" width="100">
|
||||
<template slot-scope="{row}">
|
||||
<span>{{ row.InstanceCount }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="ChecksPassing" label="健康实例" sortable align="center" width="120">
|
||||
<template slot-scope="{row}">
|
||||
<span>{{ row.ChecksPassing - 1 }} </span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="ChecksCritical" label="实例状态" sortable align="center" width="120">
|
||||
<template slot-scope="{row}">
|
||||
<el-tooltip v-if="row.ChecksCritical != 0" class="item" effect="dark" content="健康检查失败的实例数" placement="top">
|
||||
<el-button size="mini" type="danger" icon="el-icon-close" circle>{{ row.ChecksCritical }}</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-else-if="row.ChecksPassing == 1" class="item" effect="dark" content="所有实例都没有配置健康检查" placement="top">
|
||||
<el-button size="mini" type="info" icon="el-icon-minus" circle />
|
||||
</el-tooltip>
|
||||
<el-tooltip v-else class="item" effect="dark" content="已配置的健康检查都通过" placement="top">
|
||||
<el-button size="mini" type="success" icon="el-icon-check" circle />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getServices } from '@/api/consul'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
services: []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchData()
|
||||
},
|
||||
methods: {
|
||||
fetchData() {
|
||||
this.listLoading = true
|
||||
getServices().then(response => {
|
||||
this.services = response.services
|
||||
this.listLoading = false
|
||||
})
|
||||
},
|
||||
handleInstances(sname) {
|
||||
this.$router.push({
|
||||
path: '/consul/instances',
|
||||
query: { service_name: sname }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,6 +1,51 @@
|
|||
<template>
|
||||
<div class="dashboard-container">
|
||||
<div class="dashboard-text">StarsL.cn</div>
|
||||
<el-badge :value="1" class="mark">
|
||||
<el-link :underline="false" type="primary" icon="el-icon-star-on" href="https://github.com/starsliao/ConsulManager" target="_blank" class="dashboard-text">StarsL.cn</el-link>
|
||||
</el-badge>
|
||||
<el-timeline>
|
||||
<el-timeline-item timestamp="2022/2/10" placement="top">
|
||||
<el-card>
|
||||
<h4>v0.3.0</h4>
|
||||
<p>更名Consul Manager</p>
|
||||
<p>增加Consul Web管理功能</p>
|
||||
<p>增加Consul服务器的状态查看</p>
|
||||
<p>支持Consul Services的增删改查</p>
|
||||
<p>支持批量删除Service功能</p>
|
||||
<p>优化了对Tags、Meta、健康检查的配置管理</p>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
<el-timeline-item timestamp="2022/1/29" placement="top">
|
||||
<el-card>
|
||||
<h4>v0.2.0</h4>
|
||||
<p>后端使用Flask Blueprint重构,版本号v0.2.0</p>
|
||||
<p>前端规范URI路径,匹配后端,版本号v0.1.3</p>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
<el-timeline-item timestamp="2022/1/27" placement="top">
|
||||
<el-card>
|
||||
<h4>v0.1.2</h4>
|
||||
<p>所有字段增加了排序功能</p>
|
||||
<p>新增筛选功能:可以根据名称或实例来进行关键字筛选</p>
|
||||
<p>新增清空查询条件按钮</p>
|
||||
<p>简化了web界面新增操作: </p>
|
||||
<p> 选择选项查询后,点击新增或自动填写好选择的选项</p>
|
||||
<p> 增加确认并新增按钮,可以自动填上之前填写的前4个字段</p>
|
||||
<p>新增批量删除功能</p>
|
||||
<p>新增分页功能</p>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
<el-timeline-item timestamp="2022/1/7" placement="top">
|
||||
<el-card>
|
||||
<h4>v0.1.0</h4>
|
||||
<p>基于Prometheus + Blackbox_Exporter实现站点与接口监控。</p>
|
||||
<p>基于Consul实现Prometheus监控目标的自动发现。</p>
|
||||
<p>Blackbox Manager:基于Flask + Vue实现的Web管理平台维护监控目标。</p>
|
||||
<p>实现了一个脚本可批量导入监控目标到Consul。</p>
|
||||
<p>更新了一个Blackbox Exporter的Grafana展示看板。</p>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left">
|
||||
|
||||
<div class="title-container">
|
||||
<h3 style="font-size:35px" class="title">Blackbox Manager</h3>
|
||||
<h3 style="font-size:35px" class="title">Consul Manager</h3>
|
||||
</div>
|
||||
|
||||
<el-form-item prop="username">
|
||||
|
@ -45,7 +45,7 @@
|
|||
|
||||
</el-form>
|
||||
<div align="center" class="title-container">
|
||||
<span style="font-size:10px" class="title">v0.1.3</span>
|
||||
<span style="font-size:10px" class="title">v0.3.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
Loading…
Reference in New Issue