feat: 新增数据库表信息查看及其他优化

This commit is contained in:
meilin.huang
2021-08-18 17:57:33 +08:00
parent 5f1b74aba1
commit 00f053573e
57 changed files with 421 additions and 523 deletions

View File

@@ -52,15 +52,15 @@ type TimedCache struct {
type timedcache struct { type timedcache struct {
defaultExpiration time.Duration defaultExpiration time.Duration
updateAccessTime bool // 是否更新最后访问时间 updateAccessTime bool // 是否更新最后访问时间
items map[string]*Item items map[interface{}]*Item
mu sync.RWMutex mu sync.RWMutex
onEvicted func(string, interface{}) // 移除时回调函数 onEvicted func(interface{}, interface{}) // 移除时回调函数
janitor *janitor janitor *janitor
} }
// Add an item to the cache only if an item doesn't already exist for the given // Add an item to the cache only if an item doesn't already exist for the given
// key, or if the existing item has expired. Returns an error otherwise. // key, or if the existing item has expired. Returns an error otherwise.
func (c *timedcache) Add(k string, x interface{}, d time.Duration) error { func (c *timedcache) Add(k interface{}, x interface{}, d time.Duration) error {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
_, found := c.get(k) _, found := c.get(k)
@@ -71,13 +71,13 @@ func (c *timedcache) Add(k string, x interface{}, d time.Duration) error {
return nil return nil
} }
func (c *timedcache) Put(k string, x interface{}) { func (c *timedcache) Put(k interface{}, x interface{}) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
c.set(k, x, c.defaultExpiration) c.set(k, x, c.defaultExpiration)
} }
func (c *timedcache) AddIfAbsent(k string, x interface{}) { func (c *timedcache) AddIfAbsent(k interface{}, x interface{}) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
_, found := c.get(k) _, found := c.get(k)
@@ -87,7 +87,7 @@ func (c *timedcache) AddIfAbsent(k string, x interface{}) {
c.set(k, x, c.defaultExpiration) c.set(k, x, c.defaultExpiration)
} }
func (c *timedcache) ComputeIfAbsent(k string, getValueFunc func(string) (interface{}, error)) (interface{}, error) { func (c *timedcache) ComputeIfAbsent(k interface{}, getValueFunc func(interface{}) (interface{}, error)) (interface{}, error) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
value, found := c.get(k) value, found := c.get(k)
@@ -103,7 +103,7 @@ func (c *timedcache) ComputeIfAbsent(k string, getValueFunc func(string) (interf
return value, nil return value, nil
} }
func (c *timedcache) set(k string, x interface{}, d time.Duration) { func (c *timedcache) set(k interface{}, x interface{}, d time.Duration) {
var e int64 var e int64
if d == DefaultExpiration { if d == DefaultExpiration {
d = c.defaultExpiration d = c.defaultExpiration
@@ -120,13 +120,13 @@ func (c *timedcache) set(k string, x interface{}, d time.Duration) {
// Get an item from the cache. Returns the item or nil, and a bool indicating // Get an item from the cache. Returns the item or nil, and a bool indicating
// whether the key was found. // whether the key was found.
func (c *timedcache) Get(k string) (interface{}, bool) { func (c *timedcache) Get(k interface{}) (interface{}, bool) {
c.mu.RLock() c.mu.RLock()
defer c.mu.RUnlock() defer c.mu.RUnlock()
return c.get(k) return c.get(k)
} }
func (c *timedcache) get(k string) (interface{}, bool) { func (c *timedcache) get(k interface{}) (interface{}, bool) {
item, found := c.items[k] item, found := c.items[k]
if !found { if !found {
return nil, false return nil, false
@@ -145,7 +145,7 @@ func (c *timedcache) get(k string) (interface{}, bool) {
// item's value is not an integer, if it was not found, or if it is not // item's value is not an integer, if it was not found, or if it is not
// possible to increment it by n. To retrieve the incremented value, use one // possible to increment it by n. To retrieve the incremented value, use one
// of the specialized methods, e.g. IncrementInt64. // of the specialized methods, e.g. IncrementInt64.
func (c *timedcache) Increment(k string, n int64) error { func (c *timedcache) Increment(k interface{}, n int64) error {
c.mu.Lock() c.mu.Lock()
v, found := c.items[k] v, found := c.items[k]
if !found || v.Expired() { if !found || v.Expired() {
@@ -198,10 +198,10 @@ func (c *timedcache) Count() int {
} }
// Copies all unexpired items in the cache into a new map and returns it. // Copies all unexpired items in the cache into a new map and returns it.
func (c *timedcache) Items() map[string]*Item { func (c *timedcache) Items() map[interface{}]*Item {
c.mu.RLock() c.mu.RLock()
defer c.mu.RUnlock() defer c.mu.RUnlock()
m := make(map[string]*Item, len(c.items)) m := make(map[interface{}]*Item, len(c.items))
now := time.Now().UnixNano() now := time.Now().UnixNano()
for k, v := range c.items { for k, v := range c.items {
// "Inlining" of Expired // "Inlining" of Expired
@@ -216,7 +216,7 @@ func (c *timedcache) Items() map[string]*Item {
} }
// 删除指定key的数据 // 删除指定key的数据
func (c *timedcache) Delete(k string) { func (c *timedcache) Delete(k interface{}) {
c.mu.Lock() c.mu.Lock()
v, evicted := c.delete(k) v, evicted := c.delete(k)
c.mu.Unlock() c.mu.Unlock()
@@ -225,7 +225,7 @@ func (c *timedcache) Delete(k string) {
} }
} }
func (c *timedcache) delete(k string) (interface{}, bool) { func (c *timedcache) delete(k interface{}) (interface{}, bool) {
// 如果有移除回调函数,则返回值及是否有删除回调函数用于进行回调处理 // 如果有移除回调函数,则返回值及是否有删除回调函数用于进行回调处理
if c.onEvicted != nil { if c.onEvicted != nil {
if v, found := c.items[k]; found { if v, found := c.items[k]; found {
@@ -238,7 +238,7 @@ func (c *timedcache) delete(k string) (interface{}, bool) {
} }
type keyAndValue struct { type keyAndValue struct {
key string key interface{}
value interface{} value interface{}
} }
@@ -265,7 +265,7 @@ func (c *timedcache) DeleteExpired() {
// 清空所有缓存 // 清空所有缓存
func (c *timedcache) Clear() { func (c *timedcache) Clear() {
c.mu.Lock() c.mu.Lock()
c.items = map[string]*Item{} c.items = map[interface{}]*Item{}
c.mu.Unlock() c.mu.Unlock()
} }
@@ -378,7 +378,7 @@ func runJanitor(c *timedcache, ci time.Duration) {
go j.Run(c) go j.Run(c)
} }
func newCache(de time.Duration, m map[string]*Item) *timedcache { func newCache(de time.Duration, m map[interface{}]*Item) *timedcache {
if de == 0 { if de == 0 {
de = -1 de = -1
} }
@@ -389,7 +389,7 @@ func newCache(de time.Duration, m map[string]*Item) *timedcache {
return c return c
} }
func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]*Item) *TimedCache { func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[interface{}]*Item) *TimedCache {
c := newCache(de, m) c := newCache(de, m)
// This trick ensures that the janitor goroutine (which--granted it // This trick ensures that the janitor goroutine (which--granted it
// was enabled--is running DeleteExpired on c forever) does not keep // was enabled--is running DeleteExpired on c forever) does not keep
@@ -410,12 +410,12 @@ func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]*Item)
// manually. If the cleanup interval is less than one, expired items are not // manually. If the cleanup interval is less than one, expired items are not
// deleted from the cache before calling c.DeleteExpired(). // deleted from the cache before calling c.DeleteExpired().
func NewTimedCache(defaultExpiration, cleanupInterval time.Duration) *TimedCache { func NewTimedCache(defaultExpiration, cleanupInterval time.Duration) *TimedCache {
items := make(map[string]*Item) items := make(map[interface{}]*Item)
return newCacheWithJanitor(defaultExpiration, cleanupInterval, items) return newCacheWithJanitor(defaultExpiration, cleanupInterval, items)
} }
// 调用删除函数时,会回调该剔除函数 // 调用删除函数时,会回调该剔除函数
func (c *TimedCache) OnEvicted(f func(string, interface{})) *TimedCache { func (c *TimedCache) OnEvicted(f func(interface{}, interface{})) *TimedCache {
c.mu.Lock() c.mu.Lock()
c.onEvicted = f c.onEvicted = f
c.mu.Unlock() c.mu.Unlock()

View File

@@ -131,11 +131,9 @@ func (r *RequestWrapper) PostMulipart(files []MultipartFile, reqParams map[strin
_, err = io.Copy(part, reader) _, err = io.Copy(part, reader)
} }
// 如果有其他参数则写入body // 如果有其他参数则写入body
if reqParams != nil { for k, v := range reqParams {
for k, v := range reqParams { if err := writer.WriteField(k, v); err != nil {
if err := writer.WriteField(k, v); err != nil { return createRequestError(err)
return createRequestError(err)
}
} }
} }
if err := writer.Close(); err != nil { if err := writer.Close(); err != nil {
@@ -219,7 +217,7 @@ func request(rw *RequestWrapper) *ResponseWrapper {
} }
func setRequestHeader(req *http.Request, header map[string]string) { func setRequestHeader(req *http.Request, header map[string]string) {
req.Header.Set("User-Agent", "golang/mayflyjob") req.Header.Set("User-Agent", "golang/mayfly")
for k, v := range header { for k, v := range header {
req.Header.Set(k, v) req.Header.Set(k, v)
} }

View File

@@ -145,10 +145,12 @@ func GetByConditionTo(conditionModel interface{}, toModel interface{}) error {
// 获取分页结果 // 获取分页结果
func GetPage(pageParam *PageParam, conditionModel interface{}, toModels interface{}, orderBy ...string) *PageResult { func GetPage(pageParam *PageParam, conditionModel interface{}, toModels interface{}, orderBy ...string) *PageResult {
var count int64 var count int64
global.Db.Model(conditionModel).Where(conditionModel).Count(&count) err := global.Db.Model(conditionModel).Where(conditionModel).Count(&count).Error
biz.ErrIsNilAppendErr(err, " 查询错误:%s")
if count == 0 { if count == 0 {
return &PageResult{Total: 0, List: []string{}} return &PageResult{Total: 0, List: []string{}}
} }
page := pageParam.PageNum page := pageParam.PageNum
pageSize := pageParam.PageSize pageSize := pageParam.PageSize
var orderByStr string var orderByStr string
@@ -157,7 +159,7 @@ func GetPage(pageParam *PageParam, conditionModel interface{}, toModels interfac
} else { } else {
orderByStr = strings.Join(orderBy, ",") orderByStr = strings.Join(orderBy, ",")
} }
err := global.Db.Model(conditionModel).Where(conditionModel).Order(orderByStr).Limit(pageSize).Offset((page - 1) * pageSize).Find(toModels).Error err = global.Db.Model(conditionModel).Where(conditionModel).Order(orderByStr).Limit(pageSize).Offset((page - 1) * pageSize).Find(toModels).Error
biz.ErrIsNil(err, "查询失败") biz.ErrIsNil(err, "查询失败")
return &PageResult{Total: count, List: toModels} return &PageResult{Total: count, List: toModels}
} }

View File

@@ -22,8 +22,8 @@
"sortablejs": "^1.13.0", "sortablejs": "^1.13.0",
"sql-formatter": "^2.3.3", "sql-formatter": "^2.3.3",
"vue": "^3.0.5", "vue": "^3.0.5",
"vue-class-component": "^8.0.0-0",
"vue-router": "^4.0.2", "vue-router": "^4.0.2",
"vue3-json-editor": "^1.1.3",
"vuex": "^4.0.0-rc.2", "vuex": "^4.0.0-rc.2",
"xterm": "^4.9.0", "xterm": "^4.9.0",
"xterm-addon-fit": "^0.4.0" "xterm-addon-fit": "^0.4.0"

View File

@@ -1,3 +1,4 @@
window.globalConfig = { window.globalConfig = {
"BaseApiUrl": "http://localhost:8888/api" "BaseApiUrl": "/api",
"BaseWsUrl": "ws://localhost:8888"
} }

View File

@@ -1,5 +1,6 @@
const config = { const config = {
baseApiUrl: (window as any).globalConfig.BaseApiUrl baseApiUrl: `${(window as any).globalConfig.BaseApiUrl}/api`,
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl}/api`
} }
export default config export default config

View File

@@ -0,0 +1,21 @@
/**
* 格式化字节单位
* @param size byte size
* @returns
*/
export function formatByteSize(size: any) {
const value = Number(size);
if (size && !isNaN(value)) {
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'BB'];
let index = 0;
let k = value;
if (value >= 1024) {
while (k > 1024) {
k = k / 1024;
index++;
}
}
return `${k.toFixed(2)}${units[index]}`;
}
return '-';
}

View File

@@ -32,7 +32,7 @@
>编辑</el-button >编辑</el-button
> >
<el-button <el-button
v-auth="permissions.delDb" v-auth="permissions.delDb"
:disabled="chooseId == null" :disabled="chooseId == null"
@click="deleteDb(chooseId)" @click="deleteDb(chooseId)"
type="danger" type="danger"
@@ -69,6 +69,12 @@
{{ $filters.dateFormat(scope.row.createTime) }} {{ $filters.dateFormat(scope.row.createTime) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="更多信息" min-width="100">
<template #default="scope">
<el-link @click.prevent="tableInfo(scope.row)" type="success">表信息</el-link>
</template>
</el-table-column>
</el-table> </el-table>
<el-pagination <el-pagination
@current-change="handlePageChange" @current-change="handlePageChange"
@@ -80,26 +86,99 @@
:page-size="query.pageSize" :page-size="query.pageSize"
/> />
<db-edit @val-change="valChange" :projects="projects" :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" v-model:db="dbEditDialog.data"></db-edit> <el-dialog
width="75%"
:title="`${chooseData ? chooseData.database : ''} 表信息`"
:before-close="closeTableInfo"
v-model="tableInfoDialog.visible"
>
<el-table border :data="tableInfoDialog.infos" size="small">
<el-table-column property="tableName" label="表名" min-width="150" show-overflow-tooltip></el-table-column>
<el-table-column property="tableComment" label="备注" min-width="150" show-overflow-tooltip></el-table-column>
<el-table-column
prop="tableRows"
label="Rows"
min-width="70"
sortable
:sort-method="(a, b) => parseInt(a.tableRows) - parseInt(b.tableRows)"
></el-table-column>
<el-table-column
property="dataLength"
label="数据大小"
sortable
:sort-method="(a, b) => parseInt(a.dataLength) - parseInt(b.dataLength)"
>
<template #default="scope">
{{ formatByteSize(scope.row.dataLength) }}
</template>
</el-table-column>
<el-table-column
property="indexLength"
label="索引大小"
sortable
:sort-method="(a, b) => parseInt(a.indexLength) - parseInt(b.indexLength)"
>
<template #default="scope">
{{ formatByteSize(scope.row.indexLength) }}
</template>
</el-table-column>
<el-table-column property="createTime" label="创建时间" min-width="150"> </el-table-column>
<el-table-column label="更多信息" min-width="100">
<template #default="scope">
<el-link @click.prevent="showColumns(scope.row)" type="primary">字段</el-link>
<el-link class="ml5" @click.prevent="showTableIndex(scope.row)" type="success">索引</el-link>
<el-link class="ml5" @click.prevent="showCreateDdl(scope.row)" type="info">SQL</el-link>
</template>
</el-table-column>
</el-table>
</el-dialog>
<el-dialog width="40%" :title="`${chooseTableName} 字段信息`" v-model="columnDialog.visible">
<el-table border :data="columnDialog.columns" size="mini">
<el-table-column prop="columnName" label="名称" show-overflow-tooltip> </el-table-column>
<el-table-column prop="columnComment" label="备注" show-overflow-tooltip> </el-table-column>
<el-table-column width="120" prop="columnType" label="类型" show-overflow-tooltip> </el-table-column>
</el-table>
</el-dialog>
<el-dialog width="40%" :title="`${chooseTableName} 索引信息`" v-model="indexDialog.visible">
<el-table border :data="indexDialog.indexs" size="mini">
<el-table-column prop="indexName" label="索引名" show-overflow-tooltip> </el-table-column>
<el-table-column prop="columnName" label="列名" show-overflow-tooltip> </el-table-column>
<el-table-column prop="seqInIndex" label="列序列号" show-overflow-tooltip> </el-table-column>
<el-table-column prop="indexType" label="类型"> </el-table-column>
</el-table>
</el-dialog>
<el-dialog width="55%" :title="`${chooseTableName} Create-DDL`" v-model="ddlDialog.visible">
<el-input disabled type="textarea" :autosize="{ minRows: 15, maxRows: 30 }" v-model="ddlDialog.ddl"> </el-input>
</el-dialog>
<db-edit
@val-change="valChange"
:projects="projects"
:title="dbEditDialog.title"
v-model:visible="dbEditDialog.visible"
v-model:db="dbEditDialog.data"
></db-edit>
</div> </div>
</template> </template>
<script lang='ts'> <script lang='ts'>
import { toRefs, reactive, onMounted, defineComponent } from 'vue'; import { toRefs, reactive, onMounted, defineComponent } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import ProjectEnvSelect from '../component/ProjectEnvSelect.vue'; import { formatByteSize } from '@/common/utils/format';
import DbEdit from './DbEdit.vue'; import DbEdit from './DbEdit.vue';
import { dbApi } from './api'; import { dbApi } from './api';
import { projectApi } from '../project/api.ts'; import { projectApi } from '../project/api.ts';
export default defineComponent({ export default defineComponent({
name: 'DbList', name: 'DbList',
components: { components: {
ProjectEnvSelect,
DbEdit, DbEdit,
}, },
setup() { setup() {
const state = reactive({ const state = reactive({
permissions: { permissions: {
saveDb: 'db:save', saveDb: 'db:save',
delDb: 'db:del', delDb: 'db:del',
}, },
@@ -118,6 +197,24 @@ export default defineComponent({
}, },
datas: [], datas: [],
total: 0, total: 0,
chooseTableName: '',
tableInfoDialog: {
visible: false,
infos: [],
},
columnDialog: {
visible: false,
columns: [],
},
indexDialog: {
visible: false,
indexs: [],
},
ddlDialog: {
visible: false,
ddl: '',
},
dbEditDialog: { dbEditDialog: {
visible: false, visible: false,
data: null, data: null,
@@ -179,6 +276,47 @@ export default defineComponent({
} catch (err) {} } catch (err) {}
}; };
const tableInfo = async (row: any) => {
state.tableInfoDialog.infos = await dbApi.tableInfos.request({ id: row.id });
state.tableInfoDialog.visible = true;
};
const closeTableInfo = () => {
state.tableInfoDialog.visible = false;
state.tableInfoDialog.infos = [];
};
const showColumns = async (row: any) => {
state.chooseTableName = row.tableName;
state.columnDialog.columns = await dbApi.columnMetadata.request({
id: state.chooseId,
tableName: row.tableName,
});
state.columnDialog.visible = true;
};
const showTableIndex = async (row: any) => {
state.chooseTableName = row.tableName;
state.indexDialog.indexs = await dbApi.tableIndex.request({
id: state.chooseId,
tableName: row.tableName,
});
state.indexDialog.visible = true;
};
const showCreateDdl = async (row: any) => {
state.chooseTableName = row.tableName;
const res = await dbApi.tableDdl.request({
id: state.chooseId,
tableName: row.tableName,
});
state.ddlDialog.ddl = res[0]['Create Table'];
console.log(state.ddlDialog);
state.ddlDialog.visible = true;
};
return { return {
...toRefs(state), ...toRefs(state),
// enums, // enums,
@@ -188,6 +326,12 @@ export default defineComponent({
editDb, editDb,
valChange, valChange,
deleteDb, deleteDb,
tableInfo,
closeTableInfo,
showColumns,
showTableIndex,
showCreateDdl,
formatByteSize,
}; };
}, },
}); });

View File

@@ -5,6 +5,9 @@ export const dbApi = {
dbs: Api.create("/dbs", 'get'), dbs: Api.create("/dbs", 'get'),
saveDb: Api.create("/dbs", 'post'), saveDb: Api.create("/dbs", 'post'),
deleteDb: Api.create("/dbs/{id}", 'delete'), deleteDb: Api.create("/dbs/{id}", 'delete'),
tableInfos: Api.create("/dbs/{id}/t-infos", 'get'),
tableIndex: Api.create("/dbs/{id}/t-index", 'get'),
tableDdl: Api.create("/dbs/{id}/t-create-ddl", 'get'),
tableMetadata: Api.create("/dbs/{id}/t-metadata", 'get'), tableMetadata: Api.create("/dbs/{id}/t-metadata", 'get'),
columnMetadata: Api.create("/dbs/{id}/c-metadata", 'get'), columnMetadata: Api.create("/dbs/{id}/c-metadata", 'get'),
// 获取表即列提示 // 获取表即列提示

View File

@@ -98,10 +98,8 @@
import { toRefs, reactive, onMounted, defineComponent } from 'vue'; import { toRefs, reactive, onMounted, defineComponent } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { DynamicFormDialog } from '@/components/dynamic-form';
// import Monitor from './Monitor.vue'; // import Monitor from './Monitor.vue';
import { machineApi } from './api'; import { machineApi } from './api';
import SshTerminal from './SshTerminal.vue';
import ServiceManage from './ServiceManage.vue'; import ServiceManage from './ServiceManage.vue';
import FileManage from './FileManage.vue'; import FileManage from './FileManage.vue';
import MachineEdit from './MachineEdit.vue'; import MachineEdit from './MachineEdit.vue';
@@ -109,10 +107,8 @@ import MachineEdit from './MachineEdit.vue';
export default defineComponent({ export default defineComponent({
name: 'MachineList', name: 'MachineList',
components: { components: {
SshTerminal,
ServiceManage, ServiceManage,
FileManage, FileManage,
DynamicFormDialog,
MachineEdit, MachineEdit,
}, },
setup() { setup() {

View File

@@ -7,6 +7,7 @@ import 'xterm/css/xterm.css';
import { Terminal } from 'xterm'; import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit'; import { FitAddon } from 'xterm-addon-fit';
import { getSession } from '@/common/utils/storage.ts'; import { getSession } from '@/common/utils/storage.ts';
import config from '@/common/config'
export default { export default {
name: 'Xterm', name: 'Xterm',
@@ -82,7 +83,7 @@ export default {
} }
}, },
initSocket() { initSocket() {
this.socket = new WebSocket(`ws://localhost:8888/api/machines/${this.machineId}/terminal?token=${getSession('token')}`); this.socket = new WebSocket(`${config.baseWsUrl}/machines/${this.machineId}/terminal?token=${getSession('token')}`);
// 监听socket连接 // 监听socket连接
this.socket.onopen = this.open; this.socket.onopen = this.open;
// 监听socket错误信息 // 监听socket错误信息

View File

@@ -17,7 +17,15 @@
<el-button @click="showEnv(chooseData)" :disabled="chooseId == null" type="info" icon="el-icon-setting" size="mini">环境管理</el-button> <el-button @click="showEnv(chooseData)" :disabled="chooseId == null" type="info" icon="el-icon-setting" size="mini">环境管理</el-button>
<el-button v-auth="'role:del'" :disabled="chooseId == null" type="danger" icon="el-icon-delete" size="mini">删除</el-button> <el-button
v-auth="permissions.delProject"
@click="delProject"
:disabled="chooseId == null"
type="danger"
icon="el-icon-delete"
size="mini"
>删除</el-button
>
<div style="float: right"> <div style="float: right">
<el-input <el-input
@@ -69,7 +77,7 @@
<el-dialog width="400px" title="项目编辑" :before-close="cancelAddProject" v-model="addProjectDialog.visible"> <el-dialog width="400px" title="项目编辑" :before-close="cancelAddProject" v-model="addProjectDialog.visible">
<el-form :model="addProjectDialog.form" size="small" label-width="70px"> <el-form :model="addProjectDialog.form" size="small" label-width="70px">
<el-form-item label="项目名:" required> <el-form-item label="项目名:" required>
<el-input :disabled="addProjectDialog.form.id" v-model="addProjectDialog.form.name" auto-complete="off"></el-input> <el-input :disabled="addProjectDialog.form.id ? true : false" v-model="addProjectDialog.form.name" auto-complete="off"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="描述:"> <el-form-item label="描述:">
<el-input v-model="addProjectDialog.form.remark" auto-complete="off"></el-input> <el-input v-model="addProjectDialog.form.remark" auto-complete="off"></el-input>
@@ -160,7 +168,14 @@
<el-dialog width="400px" title="添加成员" :before-close="cancelAddMember" v-model="showMemDialog.addVisible"> <el-dialog width="400px" title="添加成员" :before-close="cancelAddMember" v-model="showMemDialog.addVisible">
<el-form :model="showMemDialog.memForm" size="small" label-width="70px"> <el-form :model="showMemDialog.memForm" size="small" label-width="70px">
<el-form-item label="账号:"> <el-form-item label="账号:">
<el-select style="width: 100%" remote :remote-method="getAccount" v-model="showMemDialog.memForm.accountId" filterable placeholder="请选择"> <el-select
style="width: 100%"
remote
:remote-method="getAccount"
v-model="showMemDialog.memForm.accountId"
filterable
placeholder="请选择"
>
<el-option v-for="item in showMemDialog.accounts" :key="item.id" :label="item.username" :value="item.id"> </el-option> <el-option v-for="item in showMemDialog.accounts" :key="item.id" :label="item.username" :value="item.id"> </el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -187,7 +202,6 @@ import { projectApi } from './api';
import { accountApi } from '../../system/api'; import { accountApi } from '../../system/api';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { notEmpty, notNull } from '@/common/assert'; import { notEmpty, notNull } from '@/common/assert';
import { auth } from '../../../common/utils/authFunction';
export default defineComponent({ export default defineComponent({
name: 'ProjectList', name: 'ProjectList',
components: {}, components: {},
@@ -195,6 +209,7 @@ export default defineComponent({
const state = reactive({ const state = reactive({
permissions: { permissions: {
saveProject: 'project:save', saveProject: 'project:save',
delProject: 'project:del',
saveMember: 'project:member:add', saveMember: 'project:member:add',
delMember: 'project:member:del', delMember: 'project:member:del',
saveEnv: 'project:env:add', saveEnv: 'project:env:add',
@@ -262,7 +277,7 @@ export default defineComponent({
const showAddProjectDialog = (data: any) => { const showAddProjectDialog = (data: any) => {
if (data) { if (data) {
state.addProjectDialog.form = data; state.addProjectDialog.form = { ...data };
} else { } else {
state.addProjectDialog.form = {} as any; state.addProjectDialog.form = {} as any;
} }
@@ -285,6 +300,21 @@ export default defineComponent({
cancelAddProject(); cancelAddProject();
}; };
const delProject = async () => {
try {
await ElMessageBox.confirm(`确定删除该项目?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await projectApi.delProject.request({ id: state.chooseId });
ElMessage.success('删除成功');
state.chooseData = null;
state.chooseId = null;
search();
} catch (err) {}
};
const choose = (item: any) => { const choose = (item: any) => {
if (!item) { if (!item) {
return; return;
@@ -380,11 +410,6 @@ export default defineComponent({
state.showEnvDialog.addVisible = false; state.showEnvDialog.addVisible = false;
}; };
const roleEditChange = (data: any) => {
ElMessage.success('修改成功!');
search();
};
return { return {
...toRefs(state), ...toRefs(state),
search, search,
@@ -392,6 +417,7 @@ export default defineComponent({
choose, choose,
showAddProjectDialog, showAddProjectDialog,
addProject, addProject,
delProject,
cancelAddProject, cancelAddProject,
showMembers, showMembers,
setMemebers, setMemebers,

View File

@@ -5,6 +5,7 @@ export const projectApi = {
accountProjects: Api.create("/accounts/projects", 'get'), accountProjects: Api.create("/accounts/projects", 'get'),
projects: Api.create("/projects", 'get'), projects: Api.create("/projects", 'get'),
saveProject: Api.create("/projects", 'post'), saveProject: Api.create("/projects", 'post'),
delProject: Api.create("/projects", 'delete'),
// 获取项目下的环境信息 // 获取项目下的环境信息
projectEnvs: Api.create("/projects/{projectId}/envs", 'get'), projectEnvs: Api.create("/projects/{projectId}/envs", 'get'),
saveProjectEnv: Api.create("/projects/{projectId}/envs", 'post'), saveProjectEnv: Api.create("/projects/{projectId}/envs", 'post'),

View File

@@ -195,174 +195,6 @@ export default defineComponent({
}; };
}, },
}); });
// @Component({
// name: 'RedisList',
// components: {
// Info,
// DynamicFormDialog
// }
// })
// export default class RedisList extends Vue {
// validatePort = (rule: any, value: any, callback: any) => {
// if (value > 65535 || value < 1) {
// callback(new Error('端口号错误'))
// }
// callback()
// }
// redisTable = []
// permission = redisPermission
// keyPermission = redisKeyPermission
// currentId = null
// currentData: any = null
// params = {
// host: null,
// clusterId: null
// }
// redisInfo = {
// url: ''
// }
// clusters = [
// {
// id: 0,
// name: '单机'
// }
// ]
// infoDialog = {
// title: '',
// visible: false,
// info: {
// Server: {},
// Keyspace: {},
// Clients: {},
// CPU: {},
// Memory: {}
// }
// }
// formDialog = {
// visible: false,
// title: '',
// formInfo: {
// createApi: redisApi.save,
// updateApi: redisApi.update,
// formRows: [
// [
// {
// type: 'input',
// label: '主机:',
// name: 'host',
// placeholder: '请输入节点ip',
// rules: [
// {
// required: true,
// message: '请输入节点ip',
// trigger: ['blur', 'change']
// }
// ]
// }
// ],
// [
// {
// type: 'input',
// label: '端口号:',
// name: 'port',
// placeholder: '请输入节点端口号',
// inputType: 'number',
// rules: [
// {
// required: true,
// message: '请输入节点端口号',
// trigger: ['blur', 'change']
// }
// ]
// }
// ],
// [
// {
// type: 'input',
// label: '密码:',
// name: 'pwd',
// placeholder: '请输入节点密码',
// inputType: 'password'
// }
// ],
// [
// {
// type: 'input',
// label: '描述:',
// name: 'description',
// placeholder: '请输入节点描述',
// inputType: 'textarea'
// }
// ]
// ]
// },
// formData: { port: 6379 }
// }
// mounted() {
// this.search()
// }
// choose(item: any) {
// if (!item) {
// return
// }
// this.currentId = item.id
// this.currentData = item
// }
// // connect() {
// // Req.post('/open/redis/connect', this.form, res => {
// // this.redisInfo = res
// // })
// // }
// async deleteNode() {
// await redisApi.del.request({ id: this.currentId })
// this.$message.success('删除成功')
// this.search()
// }
// manage(row: any) {
// this.$router.push(`/redis_operation/${row.clusterId}/${row.id}`)
// }
// info(redis: any) {
// redisApi.info.request({ id: redis.id }).then(res => {
// this.infoDialog.info = res
// this.infoDialog.title = `'${redis.host}' info`
// this.infoDialog.visible = true
// })
// }
// search() {
// redisApi.list.request(this.params).then(res => {
// this.redisTable = res
// })
// }
// openFormDialog(redis: any) {
// let dialogTitle
// if (redis) {
// this.formDialog.formData = this.currentData
// dialogTitle = '编辑redis节点'
// } else {
// this.formDialog.formData = { port: 6379 }
// dialogTitle = '添加redis节点'
// }
// this.formDialog.title = dialogTitle
// this.formDialog.visible = true
// }
// submitSuccess() {
// this.currentId = null
// this.currentData = null
// this.search()
// }
// }
</script> </script>
<style> <style>

View File

@@ -1,9 +1,12 @@
<template> <template>
<el-dialog :title="keyValue.key" v-model="visible" :before-close="cancel" :show-close="false" width="750px"> <el-dialog :title="keyValue.key" v-model="visible" :before-close="cancel" :show-close="false" width="800px">
<el-form> <el-form>
<el-form-item> <el-form-item>
<el-input v-model="keyValue.value" type="textarea" :autosize="{ minRows: 10, maxRows: 20 }" autocomplete="off"></el-input> <!-- <el-input v-model="keyValue.value" type="textarea" :autosize="{ minRows: 10, maxRows: 20 }" autocomplete="off"></el-input> -->
</el-form-item> </el-form-item>
<vue3-json-editor v-model="keyValue.jsonValue" @json-change="valueChange" :show-btns="false" :expandedOnStart="true" />
</el-form> </el-form>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
@@ -18,8 +21,13 @@ import { defineComponent, reactive, watch, toRefs } from 'vue';
import { redisApi } from './api'; import { redisApi } from './api';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { isTrue } from '@/common/assert'; import { isTrue } from '@/common/assert';
import { Vue3JsonEditor } from 'vue3-json-editor';
export default defineComponent({ export default defineComponent({
name: 'ValueDialog', name: 'ValueDialog',
components: {
Vue3JsonEditor,
},
props: { props: {
visible: { visible: {
type: Boolean, type: Boolean,
@@ -52,23 +60,30 @@ export default defineComponent({
() => props.keyValue, () => props.keyValue,
(val) => { (val) => {
state.keyValue = val; state.keyValue = val;
if (state.keyValue.type != 'string') { if (typeof val.value == 'string') {
state.keyValue.value = JSON.stringify(val.value, undefined, 2) state.keyValue.jsonValue = JSON.parse(val.value)
} else {
state.keyValue.jsonValue = val.value;
} }
// state.keyValue.value = JSON.stringify(val.value, undefined, 2)
} }
); );
const saveValue = async () => { const saveValue = async () => {
isTrue(state.keyValue.type == 'string', "暂不支持除string外其他类型修改") isTrue(state.keyValue.type == 'string', '暂不支持除string外其他类型修改');
await redisApi.saveStringValue.request(state.keyValue); await redisApi.saveStringValue.request(state.keyValue);
ElMessage.success('保存成功'); ElMessage.success('保存成功');
cancel(); cancel();
}; };
const valueChange = (val: any) => {
state.keyValue.value = JSON.stringify(val);
};
return { return {
...toRefs(state), ...toRefs(state),
saveValue, saveValue,
valueChange,
cancel, cancel,
}; };
}, },

View File

@@ -91,7 +91,6 @@
<script lang="ts"> <script lang="ts">
import { toRefs, reactive, onMounted, defineComponent } from 'vue'; import { toRefs, reactive, onMounted, defineComponent } from 'vue';
import RoleEdit from './RoleEdit.vue'; import RoleEdit from './RoleEdit.vue';
import { rolePermission } from '../permissions';
import ResourceEdit from './ResourceEdit.vue'; import ResourceEdit from './ResourceEdit.vue';
import ShowResource from './ShowResource.vue'; import ShowResource from './ShowResource.vue';
import { roleApi, resourceApi } from '../api'; import { roleApi, resourceApi } from '../api';
@@ -157,7 +156,7 @@ export default defineComponent({
state.chooseData = item; state.chooseData = item;
}; };
const roleEditChange = (data: any) => { const roleEditChange = () => {
ElMessage.success('修改成功!'); ElMessage.success('修改成功!');
search(); search();
}; };

View File

@@ -1,29 +0,0 @@
app:
name: mock-server
version: 1.0.0
server:
# debug release test
model: release
port: 8888
cors: true
# 静态资源
static:
- relative-path: /static
root: ./static/static
# 静态文件
static-file:
- relative-path: /
filepath: ./static/index.html
- relative-path: /favicon.ico
filepath: ./static/favicon.ico
redis:
host: 127.0.0.1
port: 6379
mysql:
host: localhost:3306
username: root
password: 111049
db-name: mayfly-job
config: charset=utf8&loc=Local&parseTime=true

View File

@@ -1,23 +0,0 @@
package form
type MockData struct {
Method string `json:"method" binding:"required"`
Enable uint `json:"enable"`
Description string `valid:"Required" json:"description"`
Data string `valid:"Required" json:"data"`
EffectiveUser []string `json:"effectiveUser"`
}
type Machine struct {
Name string `json:"name"`
Ip string `json:"ip"` // IP地址
Username string `json:"username"` // 用户名
Password string `json:"-"`
Port int `json:"port"` // 端口号
}
type MachineService struct {
Name string `json:"name"`
Ip string `json:"ip"` // IP地址
Service string `json:"service"` // 服务命令
}

View File

@@ -1,79 +0,0 @@
package controllers
import (
"encoding/json"
"mayfly-go/base/biz"
"mayfly-go/base/ctx"
"mayfly-go/base/ginx"
"mayfly-go/base/rediscli"
"mayfly-go/base/utils"
"mayfly-go/mock-server/controllers/form"
)
const key = "ccbscf:mock:data"
// @router /api/mock-datas/:method [get]
func GetMockData(rc *ctx.ReqCtx) {
g := rc.GinCtx
method := g.Param("method")
params := utils.MapBuilder("method", method).ToMap()
// 调用该mock数据的用户若该数据指定了生效用户则需要校验是否可访问
username := g.Query("username")
if username != "" {
params["username"] = username
}
// 记录日志使用
rc.ReqParam = params
mockData := &form.MockData{}
// 从redis中获取key为 ccbscf:mock:datafield为method的hash值
json.Unmarshal([]byte(rediscli.HGet(key, method)), mockData)
// 数据不存在或者状态为禁用
biz.IsTrue(mockData.Enable == 1, "无该mock数据")
eu := mockData.EffectiveUser
// 如果设置的生效用户为空,则表示所有用户都生效
if len(eu) == 0 {
rc.ResData = mockData.Data
return
}
biz.IsTrue(utils.StrLen(username) != 0, "该用户无法访问该mock数据")
// 判断该用户是否在该数据指定的生效用户中
for _, e := range eu {
if username == e {
rc.ResData = mockData.Data
return
}
}
panic(biz.NewBizErr("该用户无法访问该mock数据"))
}
// @router /api/mock-datas [put]
func UpdateMockData(rc *ctx.ReqCtx) {
mockData := &form.MockData{}
ginx.BindJsonAndValid(rc.GinCtx, mockData)
rc.ReqParam = mockData.Method
val, _ := json.Marshal(mockData)
rediscli.HSet(key, mockData.Method, val)
}
// @router /api/mock-datas [post]
func CreateMockData(rc *ctx.ReqCtx) {
mockData := &form.MockData{}
ginx.BindJsonAndValid(rc.GinCtx, mockData)
biz.IsTrue(!rediscli.HExist(key, mockData.Method), "该方法已存在")
val, _ := json.Marshal(mockData)
rediscli.HSet(key, mockData.Method, val)
}
// @router /api/mock-datas [get]
func GetAllData(rc *ctx.ReqCtx) {
rc.ResData = rediscli.HGetAll(key)
}
// @router /api/mock-datas/:method [delete]
func DeleteMockData(rc *ctx.ReqCtx) {
method := rc.GinCtx.Param("method")
rc.ReqParam = method
rediscli.HDel(key, method)
}

View File

@@ -1,42 +0,0 @@
package initialize
import (
"mayfly-go/base/config"
"mayfly-go/base/middleware"
"mayfly-go/mock-server/routers"
"github.com/gin-gonic/gin"
)
func InitRouter() *gin.Engine {
// server配置
serverConfig := config.Conf.Server
gin.SetMode(serverConfig.Model)
var router = gin.New()
// 设置静态资源
if staticConfs := serverConfig.Static; staticConfs != nil {
for _, scs := range *staticConfs {
router.Static(scs.RelativePath, scs.Root)
}
}
// 设置静态文件
if staticFileConfs := serverConfig.StaticFile; staticFileConfs != nil {
for _, sfs := range *staticFileConfs {
router.StaticFile(sfs.RelativePath, sfs.Filepath)
}
}
// 是否允许跨域
if serverConfig.Cors {
router.Use(middleware.Cors())
}
// 设置路由组
api := router.Group("/api")
{
routers.InitMockRouter(api) // 注册mock路由
}
return router
}

View File

@@ -1 +0,0 @@
{"/Users/hml/Desktop/project/go/mayfly-go/mock-server/controllers":1615026881770981752}

View File

@@ -1,12 +0,0 @@
package main
import (
"mayfly-go/base/rediscli"
"mayfly-go/base/starter"
"mayfly-go/mock-server/initialize"
)
func main() {
rediscli.SetCli(starter.ConnRedis())
starter.RunWebServer(initialize.InitRouter())
}

View File

@@ -1,38 +0,0 @@
package routers
import (
"mayfly-go/base/ctx"
"mayfly-go/mock-server/controllers"
"github.com/gin-gonic/gin"
)
func InitMockRouter(router *gin.RouterGroup) {
mock := router.Group("mock-datas")
{
// 获取mock数据
mock.GET(":method", func(c *gin.Context) {
rc := ctx.NewReqCtxWithGin(c).WithNeedToken(false).WithLog(ctx.NewLogInfo("获取mock数据"))
rc.Handle(controllers.GetMockData)
})
mock.GET("", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).WithNeedToken(false).Handle(controllers.GetAllData)
})
mock.POST("", func(c *gin.Context) {
rc := ctx.NewReqCtxWithGin(c).WithNeedToken(false).WithLog(ctx.NewLogInfo("保存新增mock数据"))
rc.Handle(controllers.CreateMockData)
})
mock.PUT("", func(c *gin.Context) {
rc := ctx.NewReqCtxWithGin(c).WithNeedToken(false).WithLog(ctx.NewLogInfo("修改mock数据"))
rc.Handle(controllers.UpdateMockData)
})
mock.DELETE(":method", func(c *gin.Context) {
rc := ctx.NewReqCtxWithGin(c).WithNeedToken(false).WithLog(ctx.NewLogInfo("删除mock数据"))
rc.Handle(controllers.DeleteMockData)
})
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1 +0,0 @@
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>mayfly-go-front</title><link href="/static/css/chunk-0bdde738.53f73f21.css" rel="prefetch"><link href="/static/css/chunk-1ccf71b8.f03c28a3.css" rel="prefetch"><link href="/static/css/chunk-4852ffd2.676f6792.css" rel="prefetch"><link href="/static/css/chunk-76193938.2d81c5bb.css" rel="prefetch"><link href="/static/css/chunk-a034c660.1463bb24.css" rel="prefetch"><link href="/static/js/chunk-0bdde738.d5de5f8f.js" rel="prefetch"><link href="/static/js/chunk-1ccf71b8.93b9c9d3.js" rel="prefetch"><link href="/static/js/chunk-4852ffd2.ccfafec1.js" rel="prefetch"><link href="/static/js/chunk-6e9f0a70.0aa40cfd.js" rel="prefetch"><link href="/static/js/chunk-76193938.803db4d0.js" rel="prefetch"><link href="/static/js/chunk-a034c660.9487808f.js" rel="prefetch"><link href="/static/css/app.b4088619.css" rel="preload" as="style"><link href="/static/css/chunk-vendors.16da611a.css" rel="preload" as="style"><link href="/static/js/app.0e96251b.js" rel="preload" as="script"><link href="/static/js/chunk-vendors.1a569244.js" rel="preload" as="script"><link href="/static/css/chunk-vendors.16da611a.css" rel="stylesheet"><link href="/static/css/app.b4088619.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but mayfly-go-front doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/static/js/chunk-vendors.1a569244.js"></script><script src="/static/js/app.0e96251b.js"></script></body></html>

View File

@@ -1 +0,0 @@
#app{background-color:#222d32}.main{display:flex}.main .el-menu:not(.el-menu--collapse){width:230px}.main .app{width:100%;background-color:#ecf0f5}.main .aside{position:fixed;margin-top:50px;z-index:10;background-color:#222d32;transition:all .3s ease-in-out}.main .aside .menu{overflow-y:auto;height:100vh}.main .app-body{margin-left:230px;transition:margin-left .3s ease-in-out}.main .main-container{margin-top:88px;padding:2px;min-height:calc(100vh - 88px)}.header{width:100%;position:fixed;display:flex;z-index:10}.header,.header .logo{height:50px;background-color:#303643}.header .logo{width:230px;text-align:center;line-height:50px;color:#fff;transition:all .3s ease-in-out}.header .logo .min{display:none}.header .right{position:absolute;right:0}.header .header-btn{overflow:hidden;height:50px;display:inline-block;text-align:center;line-height:50px;cursor:pointer;padding:0 14px;color:#fff}.header .header-btn .el-badge__content{top:14px;right:7px;text-align:center;font-size:9px;padding:0 3px;background-color:#00a65a;color:#fff;border:none;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.header .header-btn:hover{background-color:#222d32}.menu{border-right:none;-moz-user-select:-moz-none;-moz-user-select:none;-o-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.el-menu--vertical{min-width:190px}.setting-category{padding:10px 0;border-bottom:1px solid #eee}#mainContainer iframe{border:none;outline:none;width:100%;height:100%;position:absolute;background-color:#ecf0f5}.el-menu-item,.el-submenu__title{font-weight:500}#nav-bar{margin-top:50px;height:38px;width:100%;z-index:8;background:#fff;box-shadow:0 1px 3px 0 rgba(0,0,0,.12),0 0 3px 0 rgba(0,0,0,.04);position:fixed;top:0}*{padding:0;margin:0;outline:none;box-sizing:border-box}body{font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,,Arial,sans-serif}a{color:#3c8dbc;text-decoration:none}::-webkit-scrollbar{width:4px;height:8px;background-color:#f5f5f5}::-webkit-scrollbar-thumb,::-webkit-scrollbar-track{-webkit-box-shadow:inset 0 0 6px rgba(0,0,0,.3);background-color:#f5f5f5}.el-menu .fa{vertical-align:middle;margin-right:5px;width:24px;text-align:center}.el-menu .fa:not(.is-children){font-size:14px}.gray-mode{filter:grayscale(100%)}.fade-enter-active,.fade-leave-active{transition:opacity .2s ease-in-out}.fade-enter,.fade-leave-to{opacity:0}.none-select{moz-user-select:-moz-none;-moz-user-select:none;-o-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.toolbar{width:100%;padding:8px;background-color:#fff;overflow:hidden;line-height:32px;border:1px solid #e6ebf5}.fl{float:left}

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.cm-s-panda-syntax{background:#292a2b;color:#e6e6e6;line-height:1.5;font-family:Operator Mono,Source Code Pro,Menlo,Monaco,Consolas,Courier New,monospace}.cm-s-panda-syntax .CodeMirror-cursor{border-color:#ff2c6d}.cm-s-panda-syntax .CodeMirror-activeline-background{background:rgba(99,123,156,.1)}.cm-s-panda-syntax .CodeMirror-selected{background:#fff}.cm-s-panda-syntax .cm-comment{font-style:italic;color:#676b79}.cm-s-panda-syntax .cm-operator{color:#f3f3f3}.cm-s-panda-syntax .cm-string{color:#19f9d8}.cm-s-panda-syntax .cm-string-2{color:#ffb86c}.cm-s-panda-syntax .cm-tag{color:#ff2c6d}.cm-s-panda-syntax .cm-meta{color:#b084eb}.cm-s-panda-syntax .cm-number{color:#ffb86c}.cm-s-panda-syntax .cm-atom{color:#ff2c6d}.cm-s-panda-syntax .cm-keyword{color:#ff75b5}.cm-s-panda-syntax .cm-variable{color:#ffb86c}.cm-s-panda-syntax .cm-type,.cm-s-panda-syntax .cm-variable-2,.cm-s-panda-syntax .cm-variable-3{color:#ff9ac1}.cm-s-panda-syntax .cm-def{color:#e6e6e6}.cm-s-panda-syntax .cm-property{color:#f3f3f3}.cm-s-panda-syntax .cm-attribute,.cm-s-panda-syntax .cm-unit{color:#ffb86c}.cm-s-panda-syntax .CodeMirror-matchingbracket{border-bottom:1px dotted #19f9d8;padding-bottom:2px;color:#e6e6e6}.cm-s-panda-syntax .CodeMirror-gutters{background:#292a2b;border-right-color:hsla(0,0%,100%,.1)}.cm-s-panda-syntax .CodeMirror-linenumber{color:#e6e6e6;opacity:.6}.CodeMirror-lint-markers{width:16px}.CodeMirror-lint-tooltip{background-color:#ffd;border:1px solid #000;border-radius:4px 4px 4px 4px;color:#000;font-family:monospace;font-size:10pt;overflow:hidden;padding:2px 5px;position:fixed;white-space:pre;white-space:pre-wrap;z-index:100;max-width:600px;opacity:0;transition:opacity .4s;-moz-transition:opacity .4s;-webkit-transition:opacity .4s;-o-transition:opacity .4s;-ms-transition:opacity .4s}.CodeMirror-lint-mark{background-position:0 100%;background-repeat:repeat-x}.CodeMirror-lint-mark-warning{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=")}.CodeMirror-lint-mark-error{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==")}.CodeMirror-lint-marker{background-position:50%;background-repeat:no-repeat;cursor:pointer;display:inline-block;height:16px;width:16px;vertical-align:middle;position:relative}.CodeMirror-lint-message{padding-left:18px;background-position:0 0;background-repeat:no-repeat}.CodeMirror-lint-marker-warning,.CodeMirror-lint-message-warning{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=")}.CodeMirror-lint-marker-error,.CodeMirror-lint-message-error{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=")}.CodeMirror-lint-marker-multiple{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC");background-repeat:no-repeat;background-position:100% 100%;width:100%;height:100%}#jsonedit .CodeMirror{overflow-y:scroll!important;height:400px!important}.el-dialog__body{padding:2px 2px}

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.login{display:flex;justify-content:center;align-items:center;position:absolute;height:100%;width:100%;background-color:#e4e5e6}.login .login-form{width:375px;height:435px;padding:30px;background-color:#fff;text-align:left;border-radius:4px;position:relative;margin-left:0;margin-right:0;zoom:1;display:block}.login .login-form .login-header{text-align:center;font-size:16px;font-weight:700;margin-bottom:20px}

View File

@@ -1 +0,0 @@
.active-plate-main{width:100%;height:130px}.active-plate-main .active-list{display:flex;list-style:none;padding-top:15px}.active-plate-main .active-list .item{position:relative;flex:1;text-align:center}.active-plate-main .active-list .item .num{font-size:42px;font-weight:700;font-family:sans-serif}.active-plate-main .active-list .item .desc{font-size:16px}.active-plate-main .active-list .item:after{position:absolute;top:18px;right:0;content:"";display:block;width:1px;height:56px;background:#e7eef0}.active-plate-main .active-list .item:last-of-type:after{background:none}.card-main{border-radius:8px;background:#fff;margin-bottom:20px;padding-bottom:10px}.title{color:#060606;font-size:16px;padding:20px 32px}.title span{padding-left:17px;font-size:12px;color:#dededf}.pie-main{padding:28px}.bar-main,.gauge-main,.pie-main{width:100%;height:360px;background:#fff}.bar-main{padding:28px}.funnel-main{height:295px}.funnel-main,.line-main{width:100%;padding:28px;background:#fff}.base-chart,.line-main{height:360px}.base-chart{width:100%;padding:28px;background:#fff}.count-style{font-size:50px}.xterm{font-feature-settings:"liga" 0;position:relative;-moz-user-select:none;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm{cursor:text}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility,.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:.5}.xterm-underline{text-decoration:underline}.el-dialog__body{padding:2px 2px}

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-76193938"],{"9d64":function(e,t,n){e.exports=n.p+"static/img/logo.e92f231a.png"},a248:function(e,t,n){"use strict";n("e16b")},e16b:function(e,t,n){},ede4:function(e,t,n){"use strict";n.r(t);var r=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"login"},[n("div",{staticClass:"login-form"},[e._m(0),n("el-input",{staticStyle:{"margin-bottom":"18px"},attrs:{placeholder:"请输入用户名","suffix-icon":"fa fa-user"},model:{value:e.loginForm.username,callback:function(t){e.$set(e.loginForm,"username",t)},expression:"loginForm.username"}}),n("el-input",{staticStyle:{"margin-bottom":"18px"},attrs:{placeholder:"请输入密码","suffix-icon":"fa fa-keyboard-o",type:"password",autocomplete:"new-password"},model:{value:e.loginForm.password,callback:function(t){e.$set(e.loginForm,"password",t)},expression:"loginForm.password"}}),n("el-button",{staticStyle:{width:"100%","margin-bottom":"18px"},attrs:{type:"primary",loading:e.loginLoading},nativeOn:{click:function(t){return e.login(t)}}},[e._v("登录")]),n("div",[n("el-checkbox",{model:{value:e.remember,callback:function(t){e.remember=t},expression:"remember"}},[e._v("记住密码")])],1)],1)])},a=[function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("div",{staticClass:"login-header"},[r("img",{attrs:{src:n("9d64"),width:"150",height:"120",alt:""}})])}],o=n("60b5"),i=n("303e"),s=n("acf6"),u=n("e378"),c=n("a8e5"),l=(n("6a61"),n("21c9")),m=n("d789"),g={login:function(e){return m["a"].request("POST","/accounts/login",e,null)},captcha:function(){return m["a"].request("GET","/open/captcha",null,null)},logout:function(e){return m["a"].request("POST","/sys/accounts/logout/{token}",e,null)}},p=n("e4a1"),d=n("79cb"),f=function(e){Object(u["a"])(n,e);var t=Object(c["a"])(n);function n(){var e;return Object(i["a"])(this,n),e=t.apply(this,arguments),e.loginForm={username:"",password:"",uuid:""},e.remember=!1,e.loginLoading=!1,e}return Object(s["a"])(n,[{key:"mounted",value:function(){var e,t=this.getRemember();null!=t&&(e=JSON.parse(t)),e?(this.remember=!0,this.loginForm.username=e.username,this.loginForm.password=e.password):this.remember=!1}},{key:"getCaptcha",value:function(){var e=Object(o["a"])(regeneratorRuntime.mark((function e(){var t;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.next=2,g.captcha();case 2:t=e.sent,this.loginForm.uuid=t.uuid;case 4:case"end":return e.stop()}}),e,this)})));function t(){return e.apply(this,arguments)}return t}()},{key:"login",value:function(){var e=Object(o["a"])(regeneratorRuntime.mark((function e(){var t,n=this;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return this.loginLoading=!0,e.prev=1,e.next=4,g.login(this.loginForm);case 4:t=e.sent,this.remember?localStorage.setItem("remember",JSON.stringify(this.loginForm)):localStorage.removeItem("remember"),setTimeout((function(){d["a"].saveToken(t.token),n.$notify({title:"登录成功",message:"很高兴你使用Mayfly Admin别忘了给个Star哦。",type:"success"}),n.loginLoading=!1;var e=n.$route.query.redirect;e?n.$router.push(e):n.$router.push({path:"/"})}),500),e.next=12;break;case 9:e.prev=9,e.t0=e["catch"](1),this.loginLoading=!1;case 12:case"end":return e.stop()}}),e,this,[[1,9]])})));function t(){return e.apply(this,arguments)}return t}()},{key:"getRemember",value:function(){return localStorage.getItem("remember")}}]),n}(p["c"]);f=Object(l["a"])([Object(p["a"])({name:"Login"})],f);var h=f,b=h,v=(n("a248"),n("5d22")),w=Object(v["a"])(b,r,a,!1,null,null,null);t["default"]=w.exports}}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -48,6 +48,22 @@ func (d *Db) DeleteDb(rc *ctx.ReqCtx) {
d.DbApp.Delete(uint64(ginx.PathParamInt(rc.GinCtx, "id"))) d.DbApp.Delete(uint64(ginx.PathParamInt(rc.GinCtx, "id")))
} }
func (d *Db) TableInfos(rc *ctx.ReqCtx) {
rc.ResData = d.DbApp.GetDbInstance(GetDbId(rc.GinCtx)).GetTableInfos()
}
func (d *Db) TableIndex(rc *ctx.ReqCtx) {
tn := rc.GinCtx.Query("tableName")
biz.NotEmpty(tn, "tableName不能为空")
rc.ResData = d.DbApp.GetDbInstance(GetDbId(rc.GinCtx)).GetTableIndex(tn)
}
func (d *Db) GetCreateTableDdl(rc *ctx.ReqCtx) {
tn := rc.GinCtx.Query("tableName")
biz.NotEmpty(tn, "tableName不能为空")
rc.ResData = d.DbApp.GetDbInstance(GetDbId(rc.GinCtx)).GetCreateTableDdl(tn)
}
// @router /api/db/:dbId/exec-sql [get] // @router /api/db/:dbId/exec-sql [get]
func (d *Db) ExecSql(rc *ctx.ReqCtx) { func (d *Db) ExecSql(rc *ctx.ReqCtx) {
g := rc.GinCtx g := rc.GinCtx
@@ -84,7 +100,6 @@ func (d *Db) ExecSql(rc *ctx.ReqCtx) {
rc.ResData = colAndRes rc.ResData = colAndRes
} }
} }
// @router /api/db/:dbId/t-metadata [get] // @router /api/db/:dbId/t-metadata [get]

View File

@@ -49,6 +49,10 @@ func (p *Project) SaveProject(rc *ctx.ReqCtx) {
p.ProjectApp.SaveProject(project) p.ProjectApp.SaveProject(project)
} }
func (p *Project) DelProject(rc *ctx.ReqCtx) {
p.ProjectApp.DelProject(uint64(ginx.QueryInt(rc.GinCtx, "id", 0)))
}
// 获取项目下的环境信息 // 获取项目下的环境信息
func (p *Project) GetProjectEnvs(rc *ctx.ReqCtx) { func (p *Project) GetProjectEnvs(rc *ctx.ReqCtx) {
projectEnvs := &[]entity.ProjectEnv{} projectEnvs := &[]entity.ProjectEnv{}

View File

@@ -5,6 +5,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"mayfly-go/base/biz" "mayfly-go/base/biz"
"mayfly-go/base/cache"
"mayfly-go/base/global"
"mayfly-go/base/model" "mayfly-go/base/model"
"mayfly-go/server/devops/domain/entity" "mayfly-go/server/devops/domain/entity"
"mayfly-go/server/devops/domain/repository" "mayfly-go/server/devops/domain/repository"
@@ -90,17 +92,24 @@ func (d *dbAppImpl) Delete(id uint64) {
d.dbSqlRepo.DeleteBy(&entity.DbSql{DbId: id}) d.dbSqlRepo.DeleteBy(&entity.DbSql{DbId: id})
} }
var mutex sync.Mutex
func (da *dbAppImpl) GetDbInstance(id uint64) *DbInstance { func (da *dbAppImpl) GetDbInstance(id uint64) *DbInstance {
mutex.Lock()
defer mutex.Unlock()
// Id不为0则为需要缓存 // Id不为0则为需要缓存
needCache := id != 0 needCache := id != 0
if needCache { if needCache {
load, ok := dbCache.Load(id) load, ok := dbCache.Get(id)
if ok { if ok {
return load.(*DbInstance) return load.(*DbInstance)
} }
} }
d := da.GetById(id) d := da.GetById(id)
biz.NotNil(d, "数据库信息不存在") biz.NotNil(d, "数据库信息不存在")
global.Log.Infof("连接db: %s:%d/%s", d.Host, d.Port, d.Database)
DB, err := sql.Open(d.Type, getDsn(d)) DB, err := sql.Open(d.Type, getDsn(d))
biz.ErrIsNil(err, fmt.Sprintf("Open %s failed, err:%v\n", d.Type, err)) biz.ErrIsNil(err, fmt.Sprintf("Open %s failed, err:%v\n", d.Type, err))
perr := DB.Ping() perr := DB.Ping()
@@ -117,17 +126,23 @@ func (da *dbAppImpl) GetDbInstance(id uint64) *DbInstance {
dbi := &DbInstance{Id: id, Type: d.Type, db: DB} dbi := &DbInstance{Id: id, Type: d.Type, db: DB}
if needCache { if needCache {
dbCache.LoadOrStore(d.Id, dbi) dbCache.Put(id, dbi)
} }
return dbi return dbi
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
var dbCache sync.Map // 客户端连接缓存30分钟内没有访问则会被关闭
var dbCache = cache.NewTimedCache(30*time.Minute, 5*time.Second).
WithUpdateAccessTime(true).
OnEvicted(func(key interface{}, value interface{}) {
global.Log.Info(fmt.Sprintf("删除db连接缓存 id: %d", key))
value.(*DbInstance).Close()
})
func GetDbInstanceByCache(id uint64) *DbInstance { func GetDbInstanceByCache(id uint64) *DbInstance {
if load, ok := dbCache.Load(id); ok { if load, ok := dbCache.Get(fmt.Sprint(id)); ok {
return load.(*DbInstance) return load.(*DbInstance)
} }
return nil return nil
@@ -153,13 +168,17 @@ type DbInstance struct {
// 依次返回 列名数组结果map错误 // 依次返回 列名数组结果map错误
func (d *DbInstance) SelectData(sql string) ([]string, []map[string]string, error) { func (d *DbInstance) SelectData(sql string) ([]string, []map[string]string, error) {
sql = strings.Trim(sql, " ") sql = strings.Trim(sql, " ")
if !strings.HasPrefix(sql, "SELECT") && !strings.HasPrefix(sql, "select") { isSelect := strings.HasPrefix(sql, "SELECT") || strings.HasPrefix(sql, "select")
isShow := strings.HasPrefix(sql, "show")
if !isSelect && !isShow {
return nil, nil, errors.New("该sql非查询语句") return nil, nil, errors.New("该sql非查询语句")
} }
// 没加limit则默认限制50条 // 没加limit则默认限制50条
if !strings.Contains(sql, "limit") && !strings.Contains(sql, "LIMIT") { if isSelect && !strings.Contains(sql, "limit") && !strings.Contains(sql, "LIMIT") {
sql = sql + " LIMIT 50" sql = sql + " LIMIT 50"
} }
rows, err := d.db.Query(sql) rows, err := d.db.Query(sql)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -224,10 +243,9 @@ func (d *DbInstance) Exec(sql string) (int64, error) {
return res.RowsAffected() return res.RowsAffected()
} }
// 关闭连接,并从缓存中移除 // 关闭连接
func (d *DbInstance) Close() { func (d *DbInstance) Close() {
d.db.Close() d.db.Close()
dbCache.Delete(d.Id)
} }
// 获取dataSourceName // 获取dataSourceName
@@ -241,6 +259,7 @@ func getDsn(d *entity.Db) string {
func CloseDb(id uint64) { func CloseDb(id uint64) {
if di := GetDbInstanceByCache(id); di != nil { if di := GetDbInstanceByCache(id); di != nil {
di.Close() di.Close()
dbCache.Delete(id)
} }
} }
@@ -252,10 +271,22 @@ const (
create_time createTime from information_schema.tables create_time createTime from information_schema.tables
WHERE table_schema = (SELECT database())` WHERE table_schema = (SELECT database())`
// mysql 表信息
MYSQL_TABLE_INFO = `SELECT table_name tableName, table_comment tableComment, table_rows tableRows,
data_length dataLength, index_length indexLength, create_time createTime
FROM information_schema.tables
WHERE table_schema = (SELECT database())`
// mysql 索引信息
MYSQL_INDEX_INFO = `SELECT index_name indexName, column_name columnName, index_type indexType,
SEQ_IN_INDEX seqInIndex, INDEX_COMMENT indexComment
FROM information_schema.STATISTICS
WHERE table_schema = (SELECT database()) AND table_name = '%s'`
// mysql 列信息元数据 // mysql 列信息元数据
MYSQL_COLOUMN_MA = `SELECT table_name tableName, column_name columnName, column_type columnType, MYSQL_COLOUMN_MA = `SELECT table_name tableName, column_name columnName, column_type columnType,
column_comment columnComment, column_key columnKey, extra from information_schema.columns column_comment columnComment, column_key columnKey, extra from information_schema.columns
WHERE table_name in (%s) AND table_schema = (SELECT database()) ORDER BY ordinal_position limit 15000` WHERE table_name in (%s) AND table_schema = (SELECT database()) ORDER BY ordinal_position limit 18000`
) )
func (d *DbInstance) GetTableMetedatas() []map[string]string { func (d *DbInstance) GetTableMetedatas() []map[string]string {
@@ -283,3 +314,30 @@ func (d *DbInstance) GetColumnMetadatas(tableNames ...string) []map[string]strin
biz.ErrIsNilAppendErr(err, "获取数据库列信息失败: %s") biz.ErrIsNilAppendErr(err, "获取数据库列信息失败: %s")
return res return res
} }
func (d *DbInstance) GetTableInfos() []map[string]string {
var sql string
if d.Type == "mysql" {
sql = MYSQL_TABLE_INFO
}
_, res, _ := d.SelectData(sql)
return res
}
func (d *DbInstance) GetTableIndex(tableName string) []map[string]string {
var sql string
if d.Type == "mysql" {
sql = fmt.Sprintf(MYSQL_INDEX_INFO, tableName)
}
_, res, _ := d.SelectData(sql)
return res
}
func (d *DbInstance) GetCreateTableDdl(tableName string) []map[string]string {
var sql string
if d.Type == "mysql" {
sql = fmt.Sprintf("show create table %s ", tableName)
}
_, res, _ := d.SelectData(sql)
return res
}

View File

@@ -16,6 +16,8 @@ type Project interface {
SaveProject(project *entity.Project) SaveProject(project *entity.Project)
DelProject(id uint64)
// 根据项目id获取所有该项目下的环境信息列表 // 根据项目id获取所有该项目下的环境信息列表
ListEnvByProjectId(projectId uint64, listPtr interface{}) ListEnvByProjectId(projectId uint64, listPtr interface{})
@@ -64,6 +66,12 @@ func (p *projectAppImpl) SaveProject(project *entity.Project) {
} }
} }
func (p *projectAppImpl) DelProject(id uint64) {
p.projectRepo.Delete(id)
p.projectEnvRepo.DeleteEnvs(id)
p.projectMemberRepo.DeleteMems(id)
}
// 根据项目id获取所有该项目下的环境信息列表 // 根据项目id获取所有该项目下的环境信息列表
func (p *projectAppImpl) ListEnvByProjectId(projectId uint64, listPtr interface{}) { func (p *projectAppImpl) ListEnvByProjectId(projectId uint64, listPtr interface{}) {
p.projectEnvRepo.ListEnv(&entity.ProjectEnv{ProjectId: projectId}, listPtr) p.projectEnvRepo.ListEnv(&entity.ProjectEnv{ProjectId: projectId}, listPtr)

View File

@@ -1,12 +1,15 @@
package application package application
import ( import (
"fmt"
"mayfly-go/base/biz" "mayfly-go/base/biz"
"mayfly-go/base/cache"
"mayfly-go/base/global"
"mayfly-go/base/model" "mayfly-go/base/model"
"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"
"sync" "time"
"github.com/go-redis/redis" "github.com/go-redis/redis"
) )
@@ -85,7 +88,7 @@ func (r *redisAppImpl) GetRedisInstance(id uint64) *RedisInstance {
// Id不为0则为需要缓存 // Id不为0则为需要缓存
needCache := id != 0 needCache := id != 0
if needCache { if needCache {
load, ok := redisCache.Load(id) load, ok := redisCache.Get(id)
if ok { if ok {
return load.(*RedisInstance) return load.(*RedisInstance)
} }
@@ -93,6 +96,8 @@ func (r *redisAppImpl) GetRedisInstance(id uint64) *RedisInstance {
// 缓存不存在则回调获取redis信息 // 缓存不存在则回调获取redis信息
re := r.GetById(id) re := r.GetById(id)
biz.NotNil(re, "redis信息不存在") biz.NotNil(re, "redis信息不存在")
global.Log.Infof("连接redis: %s", re.Host)
rcli := redis.NewClient(&redis.Options{ rcli := redis.NewClient(&redis.Options{
Addr: re.Host, Addr: re.Host,
Password: re.Password, // no password set Password: re.Password, // no password set
@@ -104,14 +109,20 @@ func (r *redisAppImpl) GetRedisInstance(id uint64) *RedisInstance {
ri := &RedisInstance{Id: id, Cli: rcli} ri := &RedisInstance{Id: id, Cli: rcli}
if needCache { if needCache {
redisCache.LoadOrStore(re.Id, ri) redisCache.Put(re.Id, ri)
} }
return ri return ri
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
var redisCache sync.Map // redis客户端连接缓存30分钟内没有访问则会被关闭
var redisCache = cache.NewTimedCache(30*time.Minute, 5*time.Second).
WithUpdateAccessTime(true).
OnEvicted(func(key interface{}, value interface{}) {
global.Log.Info(fmt.Sprintf("删除redis连接缓存 id: %d", key))
value.(*RedisInstance).Cli.Close()
})
// redis实例 // redis实例
type RedisInstance struct { type RedisInstance struct {
@@ -121,7 +132,7 @@ type RedisInstance struct {
// 关闭redis连接 // 关闭redis连接
func CloseRedis(id uint64) { func CloseRedis(id uint64) {
if load, ok := redisCache.Load(id); ok { if load, ok := redisCache.Get(id); ok {
load.(*RedisInstance).Cli.Close() load.(*RedisInstance).Cli.Close()
redisCache.Delete(id) redisCache.Delete(id)
} }

View File

@@ -13,4 +13,6 @@ type Project interface {
Save(p *entity.Project) Save(p *entity.Project)
Update(project *entity.Project) Update(project *entity.Project)
Delete(id uint64)
} }

View File

@@ -7,4 +7,6 @@ type ProjectEnv interface {
ListEnv(condition *entity.ProjectEnv, toEntity interface{}, orderBy ...string) ListEnv(condition *entity.ProjectEnv, toEntity interface{}, orderBy ...string)
Save(entity *entity.ProjectEnv) Save(entity *entity.ProjectEnv)
DeleteEnvs(projectId uint64)
} }

View File

@@ -16,4 +16,6 @@ type ProjectMemeber interface {
// 根据成员id和项目id删除关联关系 // 根据成员id和项目id删除关联关系
DeleteByPidMid(projectId, accountId uint64) DeleteByPidMid(projectId, accountId uint64)
DeleteMems(projectId uint64)
} }

View File

@@ -28,14 +28,14 @@ type Cli struct {
// 机器客户端连接缓存30分钟内没有访问则会被关闭 // 机器客户端连接缓存30分钟内没有访问则会被关闭
var cliCache = cache.NewTimedCache(30*time.Minute, 5*time.Second). var cliCache = cache.NewTimedCache(30*time.Minute, 5*time.Second).
WithUpdateAccessTime(true). WithUpdateAccessTime(true).
OnEvicted(func(key string, value interface{}) { OnEvicted(func(key interface{}, value interface{}) {
global.Log.Info(fmt.Sprintf("删除机器连接缓存 id: %s", key)) global.Log.Info(fmt.Sprintf("删除机器连接缓存 id: %d", key))
value.(*Cli).Close() value.(*Cli).Close()
}) })
// 从缓存中获取客户端信息,不存在则回调获取机器信息函数,并新建 // 从缓存中获取客户端信息,不存在则回调获取机器信息函数,并新建
func GetCli(machineId uint64, getMachine func(uint64) *entity.Machine) (*Cli, error) { func GetCli(machineId uint64, getMachine func(uint64) *entity.Machine) (*Cli, error) {
cli, err := cliCache.ComputeIfAbsent(fmt.Sprint(machineId), func(key string) (interface{}, error) { cli, err := cliCache.ComputeIfAbsent(machineId, func(key interface{}) (interface{}, error) {
c, err := newClient(getMachine(machineId)) c, err := newClient(getMachine(machineId))
if err != nil { if err != nil {
return nil, err return nil, err
@@ -210,7 +210,7 @@ func (c *Cli) RunTerminal(shell string, stdout, stderr io.Writer) error {
// 关闭指定机器的连接 // 关闭指定机器的连接
func Close(id uint64) { func Close(id uint64) {
if cli, ok := cliCache.Get(fmt.Sprint(id)); ok { if cli, ok := cliCache.Get(id); ok {
cli.(*Cli).Close() cli.(*Cli).Close()
} }
} }

View File

@@ -18,3 +18,7 @@ func (p *projectEnvRepo) ListEnv(condition *entity.ProjectEnv, toEntity interfac
func (p *projectEnvRepo) Save(entity *entity.ProjectEnv) { func (p *projectEnvRepo) Save(entity *entity.ProjectEnv) {
biz.ErrIsNilAppendErr(model.Insert(entity), "保存环境失败:%s") biz.ErrIsNilAppendErr(model.Insert(entity), "保存环境失败:%s")
} }
func (p *projectEnvRepo) DeleteEnvs(projectId uint64) {
model.DeleteByCondition(&entity.ProjectEnv{ProjectId: projectId})
}

View File

@@ -26,3 +26,7 @@ func (p *projectMemeberRepo) GetPageList(condition *entity.ProjectMember, pagePa
func (p *projectMemeberRepo) DeleteByPidMid(projectId, accountId uint64) { func (p *projectMemeberRepo) DeleteByPidMid(projectId, accountId uint64) {
model.DeleteByCondition(&entity.ProjectMember{ProjectId: projectId, AccountId: accountId}) model.DeleteByCondition(&entity.ProjectMember{ProjectId: projectId, AccountId: accountId})
} }
func (p *projectMemeberRepo) DeleteMems(projectId uint64) {
model.DeleteByCondition(&entity.ProjectMember{ProjectId: projectId})
}

View File

@@ -26,3 +26,7 @@ func (p *projectRepo) Save(project *entity.Project) {
func (p *projectRepo) Update(project *entity.Project) { func (p *projectRepo) Update(project *entity.Project) {
biz.ErrIsNil(model.UpdateById(project), "更新项目信息") biz.ErrIsNil(model.UpdateById(project), "更新项目信息")
} }
func (p *projectRepo) Delete(id uint64) {
model.DeleteById(new(entity.Project), id)
}

View File

@@ -32,6 +32,18 @@ func InitDbRouter(router *gin.RouterGroup) {
Handle(d.DeleteDb) Handle(d.DeleteDb)
}) })
db.GET(":dbId/t-infos", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).Handle(d.TableInfos)
})
db.GET(":dbId/t-index", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).Handle(d.TableIndex)
})
db.GET(":dbId/t-create-ddl", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).WithNeedToken(false).Handle(d.GetCreateTableDdl)
})
// db.GET(":dbId/exec-sql", controllers.SelectData) // db.GET(":dbId/exec-sql", controllers.SelectData)
db.GET(":dbId/exec-sql", func(g *gin.Context) { db.GET(":dbId/exec-sql", func(g *gin.Context) {
rc := ctx.NewReqCtxWithGin(g).WithLog(ctx.NewLogInfo("执行Sql语句")) rc := ctx.NewReqCtxWithGin(g).WithLog(ctx.NewLogInfo("执行Sql语句"))

View File

@@ -34,6 +34,15 @@ func InitProjectRouter(router *gin.RouterGroup) {
Handle(m.SaveProject) Handle(m.SaveProject)
}) })
delProjectLog := ctx.NewLogInfo("删除项目信息")
delPP := ctx.NewPermission("project:del")
// 删除项目
project.DELETE("", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).WithLog(delProjectLog).
WithRequiredPermission(delPP).
Handle(m.DelProject)
})
// 获取项目下的环境信息列表 // 获取项目下的环境信息列表
project.GET("/:projectId/envs", func(c *gin.Context) { project.GET("/:projectId/envs", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).Handle(m.GetProjectEnvs) ctx.NewReqCtxWithGin(c).Handle(m.GetProjectEnvs)