fix: 前端树形按钮变更为右击显示,后端修复协程panic导致进程退出

This commit is contained in:
meilin.huang
2021-12-02 10:35:48 +08:00
parent d762d346e0
commit fb0fc274b4
10 changed files with 230 additions and 166 deletions

View File

@@ -96,7 +96,6 @@
import { toRefs, reactive, computed, defineComponent, ref } from 'vue'; import { toRefs, reactive, computed, defineComponent, ref } from 'vue';
import { dbApi } from './api'; import { dbApi } from './api';
import 'codemirror/theme/ambiance.css';
import 'codemirror/addon/hint/show-hint.css'; import 'codemirror/addon/hint/show-hint.css';
// import base style // import base style
import 'codemirror/lib/codemirror.css'; import 'codemirror/lib/codemirror.css';
@@ -210,7 +209,13 @@ export default defineComponent({
// 没有选中的文本,则为全部文本 // 没有选中的文本,则为全部文本
let sql = getSql(); let sql = getSql();
notNull(sql, '内容不能为空'); notNull(sql, '内容不能为空');
runSqlStr(sql);
};
/**
* 执行sql str
*/
const runSqlStr = async (sql: string) => {
state.execRes.tableColumn = []; state.execRes.tableColumn = [];
state.execRes.data = []; state.execRes.data = [];
state.execRes.emptyResText = '查询中...'; state.execRes.emptyResText = '查询中...';
@@ -336,7 +341,7 @@ export default defineComponent({
// 赋值第一个表信息 // 赋值第一个表信息
if (state.tableMetadata.length > 0) { if (state.tableMetadata.length > 0) {
state.tableName = state.tableMetadata[0]['tableName']; state.tableName = state.tableMetadata[0]['tableName'];
changeTable(state.tableName); changeTable(state.tableName, false);
} }
}); });
@@ -367,14 +372,22 @@ export default defineComponent({
}; };
// 选择表事件 // 选择表事件
const changeTable = async (tableName: string) => { const changeTable = (tableName: string, execSelectSql: boolean = true) => {
if (tableName == '') { if (tableName == '') {
return; return;
} }
state.columnMetadata = await dbApi.columnMetadata.request({ dbApi.columnMetadata
id: state.dbId, .request({
tableName: tableName, id: state.dbId,
}); tableName: tableName,
})
.then((res) => {
state.columnMetadata = res;
});
if (execSelectSql) {
runSqlStr(`SELECT * FROM ${tableName} ORDER BY create_time DESC LIMIT 25`);
}
}; };
/** /**

View File

@@ -51,78 +51,89 @@
<el-dialog :title="tree.title" v-model="tree.visible" :close-on-click-modal="false" width="680px"> <el-dialog :title="tree.title" v-model="tree.visible" :close-on-click-modal="false" width="680px">
<div style="height: 45vh; overflow: auto"> <div style="height: 45vh; overflow: auto">
<el-tree v-if="tree.visible" ref="fileTree" :load="loadNode" :props="props" lazy node-key="id" :expand-on-click-node="false"> <el-tree v-if="tree.visible" ref="fileTree" :load="loadNode" :props="props" lazy node-key="id" :expand-on-click-node="true">
<template #default="{ node, data }"> <template #default="{ node, data }">
<span class="custom-tree-node"> <span class="custom-tree-node">
<span v-if="data.type == 'd' && !node.expanded"> <el-dropdown size="small" trigger="contextmenu">
<i class="el-icon-folder"></i> <span class="el-dropdown-link">
</span> <span v-if="data.type == 'd' && !node.expanded">
<span v-if="data.type == 'd' && node.expanded"> <i class="el-icon-folder"></i>
<i class="el-icon-folder-opened"></i> </span>
</span> <span v-if="data.type == 'd' && node.expanded">
<span v-if="data.type == '-'"> <i class="el-icon-folder-opened"></i>
<i class="el-icon-document"></i> </span>
</span> <span v-if="data.type == '-'">
<i class="el-icon-document"></i>
</span>
<span style="display: inline-block; width: 430px"> <span style="display: inline-block">
{{ node.label }} {{ node.label }}
<span style="color: #67c23a" v-if="data.type == '-'">&nbsp;&nbsp;[{{ formatFileSize(data.size) }}]</span> <span style="color: #67c23a" v-if="data.type == '-'">&nbsp;&nbsp;[{{ formatFileSize(data.size) }}]</span>
</span> </span>
</span>
<span> <template #dropdown>
<el-link <el-dropdown-menu>
@click.prevent="getFileContent(tree.folder.id, data.path)" <el-dropdown-item>
v-if="data.type == '-' && data.size < 1 * 1024 * 1024" <el-link
type="info" @click.prevent="getFileContent(tree.folder.id, data.path)"
icon="el-icon-view" v-if="data.type == '-' && data.size < 1 * 1024 * 1024"
:underline="false" type="info"
/> icon="el-icon-view"
:underline="false"
<el-upload >
:before-upload="beforeUpload" 查看
:on-success="uploadSuccess" </el-link>
:headers="{ token }" </el-dropdown-item>
:data="{ <el-dropdown-item v-if="data.type == 'd'">
fileId: tree.folder.id, <el-upload
path: data.path, :before-upload="beforeUpload"
machineId: machineId, :on-success="uploadSuccess"
}" :headers="{ token }"
:action="getUploadFile({ path: data.path })" :data="{
:show-file-list="false" fileId: tree.folder.id,
name="file" path: data.path,
multiple machineId: machineId,
:limit="100" }"
style="display: inline-block; margin-left: 2px" :action="getUploadFile({ path: data.path })"
> :show-file-list="false"
<el-link name="file"
v-auth="'machine:file:upload'" multiple
v-if="data.type == 'd'" :limit="100"
@click.prevent style="display: inline-block; margin-left: 2px"
icon="el-icon-upload" >
:underline="false" <el-link v-auth="'machine:file:upload'" @click.prevent icon="el-icon-upload" :underline="false">
/> 上传
</el-upload> </el-link>
</el-upload>
<el-link </el-dropdown-item>
v-auth="'machine:file:write'" <el-dropdown-item>
v-if="data.type == '-'" <el-link
@click.prevent="downloadFile(node, data)" v-auth="'machine:file:write'"
type="danger" v-if="data.type == '-'"
icon="el-icon-download" @click.prevent="downloadFile(node, data)"
:underline="false" type="primary"
style="margin-left: 2px" icon="el-icon-download"
/> :underline="false"
style="margin-left: 2px"
<el-link >下载</el-link
v-auth="'machine:file:rm'" >
v-if="!dontOperate(data)" </el-dropdown-item>
@click.prevent="deleteFile(node, data)" <el-dropdown-item>
type="danger" <el-link
icon="el-icon-delete" v-auth="'machine:file:rm'"
:underline="false" v-if="!dontOperate(data)"
style="margin-left: 2px" @click.prevent="deleteFile(node, data)"
/> type="danger"
</span> icon="el-icon-delete"
:underline="false"
style="margin-left: 2px"
>删除
</el-link>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</span> </span>
</template> </template>
</el-tree> </el-tree>
@@ -458,8 +469,8 @@ export default defineComponent({
}; };
const beforeUpload = (file: File) => { const beforeUpload = (file: File) => {
ElMessage.success(`'${file.name}' 上传中,请关注结果通知`) ElMessage.success(`'${file.name}' 上传中,请关注结果通知`);
} };
const dontOperate = (data: any) => { const dontOperate = (data: any) => {
const path = data.path; const path = data.path;

View File

@@ -48,19 +48,19 @@
</el-radio> </el-radio>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="name" label="名称" width></el-table-column> <el-table-column prop="name" label="名称" min-width="130"></el-table-column>
<el-table-column prop="ip" label="ip:port" min-width="160"> <el-table-column prop="ip" label="ip:port" min-width="130">
<template #default="scope"> <template #default="scope">
{{ `${scope.row.ip}:${scope.row.port}` }} {{ `${scope.row.ip}:${scope.row.port}` }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="ip" label="hasCli" min-width="60"> <el-table-column prop="ip" label="hasCli" width="70">
<template #default="scope"> <template #default="scope">
{{ `${scope.row.hasCli ? '是' : '否'}` }} {{ `${scope.row.hasCli ? '是' : '否'}` }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="username" label="用户名" :min-width="55"></el-table-column> <el-table-column prop="username" label="用户名" min-width="75"></el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="160"> <el-table-column prop="createTime" label="创建时间" width="160">
<template #default="scope"> <template #default="scope">
{{ $filters.dateFormat(scope.row.createTime) }} {{ $filters.dateFormat(scope.row.createTime) }}
</template> </template>
@@ -72,9 +72,8 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="modifier" label="修改者" :min-width="55"></el-table-column> --> <el-table-column prop="modifier" label="修改者" :min-width="55"></el-table-column> -->
<el-table-column label="操作" min-width="260"> <el-table-column label="操作" min-width="260" fixed="right">
<template #default="scope"> <template #default="scope">
<!-- <el-button type="primary" @click="monitor(scope.row.id)" icom="el-icon-tickets" size="mini" plain>监控</el-button> -->
<el-button type="success" @click="serviceManager(scope.row)" size="mini" plain>脚本</el-button> <el-button type="success" @click="serviceManager(scope.row)" size="mini" plain>脚本</el-button>
<el-button v-auth="'machine:terminal'" type="primary" @click="showTerminal(scope.row)" size="mini" plain>终端</el-button> <el-button v-auth="'machine:terminal'" type="primary" @click="showTerminal(scope.row)" size="mini" plain>终端</el-button>
<el-button :disabled="!scope.row.hasCli" type="danger" @click="closeCli(scope.row)" size="mini" plain>关闭连接</el-button> <el-button :disabled="!scope.row.hasCli" type="danger" @click="closeCli(scope.row)" size="mini" plain>关闭连接</el-button>

View File

@@ -4,6 +4,7 @@ export const machineApi = {
// 获取权限列表 // 获取权限列表
list: Api.create("/machines", 'get'), list: Api.create("/machines", 'get'),
info: Api.create("/machines/{id}/sysinfo", 'get'), info: Api.create("/machines/{id}/sysinfo", 'get'),
stats: Api.create("/machines/{id}/stats", 'get'),
closeCli: Api.create("/machines/{id}/close-cli", 'delete'), closeCli: Api.create("/machines/{id}/close-cli", 'delete'),
// 保存按钮 // 保存按钮
saveMachine: Api.create("/machines", 'post'), saveMachine: Api.create("/machines", 'post'),

View File

@@ -126,7 +126,7 @@
:data="showResourceDialog.resources" :data="showResourceDialog.resources"
node-key="id" node-key="id"
:props="showResourceDialog.defaultProps" :props="showResourceDialog.defaultProps"
:expand-on-click-node="false" :expand-on-click-node="true"
> >
<template #default="{ node, data }"> <template #default="{ node, data }">
<span class="custom-tree-node"> <span class="custom-tree-node">

View File

@@ -2,7 +2,7 @@
<div class="menu"> <div class="menu">
<div class="toolbar"> <div class="toolbar">
<div> <div>
<span style="font-size: 14px"> <i class="el-icon-info"></i>红色字体表示禁用状态 </span> <span style="font-size: 14px"> <i class="el-icon-info"></i>红色字体表示禁用状态,右击鼠标显示操作 </span>
</div> </div>
<el-button v-auth="'resource:add'" type="primary" icon="el-icon-plus" size="mini" @click="addResource(false)">添加</el-button> <el-button v-auth="'resource:add'" type="primary" icon="el-icon-plus" size="mini" @click="addResource(false)">添加</el-button>
</div> </div>
@@ -15,74 +15,93 @@
@node-expand="handleNodeExpand" @node-expand="handleNodeExpand"
@node-collapse="handleNodeCollapse" @node-collapse="handleNodeCollapse"
:default-expanded-keys="defaultExpandedKeys" :default-expanded-keys="defaultExpandedKeys"
:expand-on-click-node="false" :expand-on-click-node="true"
> >
<template #default="{ data }"> <template #default="{ data }">
<span class="custom-tree-node"> <span class="custom-tree-node">
<span style="font-size: 13px" v-if="data.type === enums.ResourceTypeEnum.MENU.value"> <el-dropdown size="mini" trigger="contextmenu">
<span style="color: #3c8dbc"></span> <span class="el-dropdown-link">
{{ data.name }} <span style="font-size: 13px" v-if="data.type === enums.ResourceTypeEnum.MENU.value">
<span style="color: #3c8dbc"></span> <span style="color: #3c8dbc"></span>
<el-tag v-if="data.children !== null" size="mini">{{ data.children.length }}</el-tag> {{ data.name }}
</span> <span style="color: #3c8dbc"></span>
<span style="font-size: 13px" v-if="data.type === enums.ResourceTypeEnum.PERMISSION.value"> <el-tag v-if="data.children !== null" size="mini">{{ data.children.length }}</el-tag>
<span style="color: #3c8dbc"></span> </span>
<span :style="data.status == 1 ? 'color: #67c23a;' : 'color: #f67c6c;'">{{ data.name }}</span> <span style="font-size: 13px" v-if="data.type === enums.ResourceTypeEnum.PERMISSION.value">
<span style="color: #3c8dbc"></span> <span style="color: #3c8dbc"></span>
</span> <span :style="data.status == 1 ? 'color: #67c23a;' : 'color: #f67c6c;'">{{ data.name }}</span>
<span style="color: #3c8dbc"></span>
<el-link @click.prevent="info(data)" style="margin-left: 25px" icon="el-icon-view" type="info" :underline="false" /> </span>
</span>
<el-link <template #dropdown>
v-auth="'resource:update'" <el-dropdown-menu>
@click.prevent="editResource(data)" <el-dropdown-item>
class="ml5" <el-link @click.prevent="info(data)" icon="el-icon-view" type="info" :underline="false">查看</el-link>
type="primary" </el-dropdown-item>
icon="el-icon-edit" <el-dropdown-item>
:underline="false" <el-link
/> v-auth="'resource:update'"
@click.prevent="editResource(data)"
<el-link type="primary"
v-auth="'resource:add'" icon="el-icon-edit"
@click.prevent="addResource(data)" :underline="false"
v-if="data.type === enums.ResourceTypeEnum.MENU.value" >
icon="el-icon-circle-plus-outline" 修改
:underline="false" </el-link>
type="success" </el-dropdown-item>
class="ml5" <el-dropdown-item>
/> <el-link
v-auth="'resource:add'"
<el-link @click.prevent="addResource(data)"
v-auth="'resource:changeStatus'" v-if="data.type === enums.ResourceTypeEnum.MENU.value"
@click.prevent="changeStatus(data, -1)" icon="el-icon-circle-plus-outline"
v-if="data.status === 1 && data.type === enums.ResourceTypeEnum.PERMISSION.value" :underline="false"
icon="el-icon-circle-close" type="success"
:underline="false" >
type="warning" 新增
class="ml5" </el-link></el-dropdown-item
/> >
<el-dropdown-item>
<el-link <el-link
v-auth="'resource:changeStatus'" v-auth="'resource:changeStatus'"
@click.prevent="changeStatus(data, 1)" @click.prevent="changeStatus(data, -1)"
v-if="data.status === -1 && data.type === enums.ResourceTypeEnum.PERMISSION.value" v-if="data.status === 1 && data.type === enums.ResourceTypeEnum.PERMISSION.value"
type="success" icon="el-icon-circle-close"
icon="el-icon-circle-check" :underline="false"
:underline="false" type="warning"
plain >
class="ml5" 禁用
/> </el-link>
</el-dropdown-item>
<el-link <el-dropdown-item>
v-auth="'resource:del'" <el-link
v-if="data.children == null && data.name !== '首页'" v-auth="'resource:changeStatus'"
@click.prevent="deleteMenu(data)" @click.prevent="changeStatus(data, 1)"
type="danger" v-if="data.status === -1 && data.type === enums.ResourceTypeEnum.PERMISSION.value"
icon="el-icon-remove-outline" type="success"
:underline="false" icon="el-icon-circle-check"
plain :underline="false"
class="ml5" plain
/> >
启用
</el-link>
</el-dropdown-item>
<el-dropdown-item>
<el-link
v-auth="'resource:del'"
v-if="data.children == null && data.name !== '首页'"
@click.prevent="deleteMenu(data)"
type="danger"
icon="el-icon-remove-outline"
:underline="false"
plain
>
删除
</el-link>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</span> </span>
</template> </template>
</el-tree> </el-tree>
@@ -201,7 +220,7 @@ export default defineComponent({
id: data.id, id: data.id,
}) })
.then((res) => { .then((res) => {
console.log(res) console.log(res);
ElMessage.success('删除成功!'); ElMessage.success('删除成功!');
search(); search();
}); });

View File

@@ -117,6 +117,15 @@ func (d *Db) ExecSqlFile(rc *ctx.ReqCtx) {
dbId := GetDbId(g) dbId := GetDbId(g)
go func() { go func() {
defer func() {
if err := recover(); err != nil {
switch t := err.(type) {
case *biz.BizError:
d.MsgApp.CreateAndSend(rc.LoginAccount, ws.ErrMsg("sql脚本执行失败", fmt.Sprintf("[%s]执行失败: [%s]", filename, t.Error())))
}
}
}()
db := d.DbApp.GetDbInstance(dbId) db := d.DbApp.GetDbInstance(dbId)
for _, sql := range sqls { for _, sql := range sqls {
sql = strings.Trim(sql, " ") sql = strings.Trim(sql, " ")

View File

@@ -8,10 +8,12 @@ import (
"mayfly-go/base/ctx" "mayfly-go/base/ctx"
"mayfly-go/base/ginx" "mayfly-go/base/ginx"
"mayfly-go/base/utils" "mayfly-go/base/utils"
"mayfly-go/base/ws"
"mayfly-go/server/devops/api/form" "mayfly-go/server/devops/api/form"
"mayfly-go/server/devops/api/vo" "mayfly-go/server/devops/api/vo"
"mayfly-go/server/devops/application" "mayfly-go/server/devops/application"
"mayfly-go/server/devops/domain/entity" "mayfly-go/server/devops/domain/entity"
sysApplication "mayfly-go/server/sys/application"
"strconv" "strconv"
"strings" "strings"
@@ -21,6 +23,7 @@ import (
type MachineFile struct { type MachineFile struct {
MachineFileApp application.MachineFile MachineFileApp application.MachineFile
MachineApp application.Machine MachineApp application.Machine
MsgApp sysApplication.Msg
} }
const ( const (
@@ -125,9 +128,24 @@ func (m *MachineFile) UploadFile(rc *ctx.ReqCtx) {
file, _ := fileheader.Open() file, _ := fileheader.Open()
bytes, _ := ioutil.ReadAll(file) bytes, _ := ioutil.ReadAll(file)
go m.MachineFileApp.UploadFile(rc.LoginAccount, fid, path, fileheader.Filename, bytes)
rc.ReqParam = fmt.Sprintf("path: %s", path) rc.ReqParam = fmt.Sprintf("path: %s", path)
la := rc.LoginAccount
go func() {
defer func() {
if err := recover(); err != nil {
switch t := err.(type) {
case *biz.BizError:
m.MsgApp.CreateAndSend(la, ws.ErrMsg("文件上传失败", fmt.Sprintf("执行文件上传失败:\n<-e errCode: %d, errMsg: %s", t.Code(), t.Error())))
}
}
}()
m.MachineFileApp.UploadFile(fid, path, fileheader.Filename, bytes)
// 保存消息并发送文件上传成功通知
machine := m.MachineApp.GetById(m.MachineFileApp.GetById(fid).MachineId)
m.MsgApp.CreateAndSend(la, ws.SuccessMsg("文件上传成功", fmt.Sprintf("[%s]文件已成功上传至 %s[%s:%s]", fileheader.Filename, machine.Name, machine.Ip, path)))
}()
} }
func (m *MachineFile) RemoveFile(rc *ctx.ReqCtx) { func (m *MachineFile) RemoveFile(rc *ctx.ReqCtx) {

View File

@@ -1,17 +1,14 @@
package application package application
import ( import (
"fmt"
"io" "io"
"io/fs" "io/fs"
"io/ioutil" "io/ioutil"
"mayfly-go/base/biz" "mayfly-go/base/biz"
"mayfly-go/base/model" "mayfly-go/base/model"
"mayfly-go/base/ws"
"mayfly-go/server/devops/domain/entity" "mayfly-go/server/devops/domain/entity"
"mayfly-go/server/devops/domain/repository" "mayfly-go/server/devops/domain/repository"
"mayfly-go/server/devops/infrastructure/persistence" "mayfly-go/server/devops/infrastructure/persistence"
sysApplication "mayfly-go/server/sys/application"
"os" "os"
"strings" "strings"
@@ -44,7 +41,7 @@ type MachineFile interface {
WriteFileContent(fileId uint64, path string, content []byte) WriteFileContent(fileId uint64, path string, content []byte)
// 文件上传 // 文件上传
UploadFile(la *model.LoginAccount, fileId uint64, path, filename string, content []byte) UploadFile(fileId uint64, path, filename string, content []byte)
// 移除文件 // 移除文件
RemoveFile(fileId uint64, path string) RemoveFile(fileId uint64, path string)
@@ -53,14 +50,12 @@ type MachineFile interface {
type machineFileAppImpl struct { type machineFileAppImpl struct {
machineFileRepo repository.MachineFile machineFileRepo repository.MachineFile
machineRepo repository.Machine machineRepo repository.Machine
msgApp sysApplication.Msg
} }
// 实现类单例 // 实现类单例
var MachineFileApp MachineFile = &machineFileAppImpl{ var MachineFileApp MachineFile = &machineFileAppImpl{
machineRepo: persistence.MachineDao, machineRepo: persistence.MachineDao,
machineFileRepo: persistence.MachineFileDao, machineFileRepo: persistence.MachineFileDao,
msgApp: sysApplication.MsgApp,
} }
// 分页获取机器脚本信息列表 // 分页获取机器脚本信息列表
@@ -138,7 +133,7 @@ func (m *machineFileAppImpl) WriteFileContent(fileId uint64, path string, conten
} }
// 上传文件 // 上传文件
func (m *machineFileAppImpl) UploadFile(la *model.LoginAccount, fileId uint64, path, filename string, content []byte) { func (m *machineFileAppImpl) UploadFile(fileId uint64, path, filename string, content []byte) {
path, machineId := m.checkAndReturnPathMid(fileId, path) path, machineId := m.checkAndReturnPathMid(fileId, path)
if !strings.HasSuffix(path, "/") { if !strings.HasSuffix(path, "/") {
path = path + "/" path = path + "/"
@@ -150,10 +145,6 @@ func (m *machineFileAppImpl) UploadFile(la *model.LoginAccount, fileId uint64, p
defer createfile.Close() defer createfile.Close()
createfile.Write(content) createfile.Write(content)
// 保存消息并发送文件上传成功通知
machine := m.machineRepo.GetById(machineId)
m.msgApp.CreateAndSend(la, ws.SuccessMsg("文件上传成功", fmt.Sprintf("[%s]文件已成功上传至 %s[%s:%s]", filename, machine.Name, machine.Ip, path)))
} }
// 删除文件 // 删除文件

View File

@@ -4,6 +4,7 @@ import (
"mayfly-go/base/ctx" "mayfly-go/base/ctx"
"mayfly-go/server/devops/api" "mayfly-go/server/devops/api"
"mayfly-go/server/devops/application" "mayfly-go/server/devops/application"
sysApplication "mayfly-go/server/sys/application"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -13,7 +14,9 @@ func InitMachineFileRouter(router *gin.RouterGroup) {
{ {
mf := &api.MachineFile{ mf := &api.MachineFile{
MachineFileApp: application.MachineFileApp, MachineFileApp: application.MachineFileApp,
MachineApp: application.MachineApp} MachineApp: application.MachineApp,
MsgApp: sysApplication.MsgApp,
}
// 获取指定机器文件列表 // 获取指定机器文件列表
machineFile.GET(":machineId/files", func(c *gin.Context) { machineFile.GET(":machineId/files", func(c *gin.Context) {