mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 23:40:24 +08:00
refactor: 机器相关配置迁移至系统配置、pgsql数据操作完善、新增context-path
This commit is contained in:
@@ -3,6 +3,10 @@ function getBaseApiUrl() {
|
||||
if (path == '/') {
|
||||
return window.location.host;
|
||||
}
|
||||
if (path.endsWith('/')) {
|
||||
// 去除最后一个/
|
||||
return window.location.host + path.replace(/\/$/, '');
|
||||
}
|
||||
return window.location.host + path;
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
size="small"
|
||||
@click="maximize(minimizeTerminal.terminalId)"
|
||||
>
|
||||
<el-tooltip effect="customized" :content="minimizeTerminal.desc" placement="top">
|
||||
<el-tooltip :content="minimizeTerminal.desc" placement="top">
|
||||
<span>
|
||||
{{ minimizeTerminal.title }}
|
||||
</span>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</el-icon>
|
||||
</template>
|
||||
<span v-for="(v, i) in tags" :key="i">
|
||||
<el-tooltip effect="customized" :content="v.remark" placement="top">
|
||||
<el-tooltip :content="v.remark" placement="top">
|
||||
<span class="color-success">{{ v.name }}</span>
|
||||
</el-tooltip>
|
||||
<span v-if="i != state.tags.length - 1" class="color-primary"> / </span>
|
||||
|
||||
@@ -24,6 +24,8 @@ export class TagTreeNode {
|
||||
*/
|
||||
params: any;
|
||||
|
||||
icon: any;
|
||||
|
||||
static TagPath = -1;
|
||||
|
||||
constructor(key: any, label: string, type?: NodeType) {
|
||||
@@ -42,6 +44,11 @@ export class TagTreeNode {
|
||||
return this;
|
||||
}
|
||||
|
||||
withIcon(icon: any) {
|
||||
this.icon = icon;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载子节点,使用节点类型的loadNodesFunc去加载子节点
|
||||
* @returns 子节点信息
|
||||
|
||||
@@ -57,34 +57,14 @@
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-popover placement="left" trigger="click" :width="300">
|
||||
<template #reference>
|
||||
<el-button type="primary" @click="selectDb(data.dbs)" link>库操作</el-button>
|
||||
</template>
|
||||
<el-input v-model="filterDb.param" @keyup="filterSchema" class="w-50 m-2" placeholder="搜索" size="small">
|
||||
<template #prefix>
|
||||
<el-icon class="el-input__icon">
|
||||
<search-icon />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<div
|
||||
class="el-tag--plain el-tag--success"
|
||||
v-for="db in filterDb.list"
|
||||
:key="db"
|
||||
style="border: 1px var(--color-success-light-3) solid; margin-top: 3px; border-radius: 5px; padding: 2px; position: relative"
|
||||
>
|
||||
<el-link type="success" plain size="small" :underline="false">{{ db }}</el-link>
|
||||
<el-link type="primary" plain size="small" :underline="false" @click="showTableInfo(data, db)" style="position: absolute; right: 4px"
|
||||
>操作
|
||||
</el-link>
|
||||
</div>
|
||||
</el-popover>
|
||||
<span v-if="actionBtns[perms.saveDb]">
|
||||
<el-button type="primary" @click="editDb(data)" link>编辑</el-button>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
</span>
|
||||
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-button type="primary" @click="onShowSqlExec(data)" link>SQL记录</el-button>
|
||||
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-dropdown @command="handleMoreActionCommand">
|
||||
<span class="el-dropdown-link-more">
|
||||
更多
|
||||
@@ -96,7 +76,7 @@
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item :command="{ type: 'detail', data }"> 详情 </el-dropdown-item>
|
||||
|
||||
<el-dropdown-item :command="{ type: 'edit', data }" v-if="actionBtns[perms.saveDb]"> 编辑 </el-dropdown-item>
|
||||
<!-- <el-dropdown-item :command="{ type: 'edit', data }" v-if="actionBtns[perms.saveDb]"> 编辑 </el-dropdown-item> -->
|
||||
|
||||
<el-dropdown-item :command="{ type: 'dumpDb', data }" v-if="data.type == 'mysql'"> 导出 </el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -105,10 +85,6 @@
|
||||
</template>
|
||||
</page-table>
|
||||
|
||||
<el-dialog width="80%" :title="`${db} 表信息`" :before-close="closeTableInfo" v-model="tableInfoDialog.visible">
|
||||
<db-table-list :db-id="dbId" :db="db" :db-type="state.row.type" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog width="620" :title="`${db} 数据库导出`" v-model="exportDialog.visible">
|
||||
<el-row justify="space-between">
|
||||
<el-col :span="9">
|
||||
@@ -190,7 +166,6 @@ import { dbApi } from './api';
|
||||
import config from '@/common/config';
|
||||
import { joinClientParams } from '@/common/request';
|
||||
import { isTrue } from '@/common/assert';
|
||||
import { Search as SearchIcon } from '@element-plus/icons-vue';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import TagInfo from '../component/TagInfo.vue';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
@@ -199,7 +174,6 @@ import { hasPerms } from '@/components/auth/auth';
|
||||
import DbSqlExecLog from './DbSqlExecLog.vue';
|
||||
|
||||
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
|
||||
const DbTableList = defineAsyncComponent(() => import('./table/DbTableList.vue'));
|
||||
|
||||
const perms = {
|
||||
base: 'db',
|
||||
@@ -254,13 +228,6 @@ const state = reactive({
|
||||
instanceId: 0,
|
||||
},
|
||||
},
|
||||
showDumpInfo: false,
|
||||
dumpInfo: {
|
||||
id: 0,
|
||||
db: '',
|
||||
type: 3,
|
||||
tables: [],
|
||||
},
|
||||
// sql执行记录弹框
|
||||
sqlExecLogDialog: {
|
||||
title: '',
|
||||
@@ -268,10 +235,6 @@ const state = reactive({
|
||||
dbs: [],
|
||||
dbId: 0,
|
||||
},
|
||||
chooseTableName: '',
|
||||
tableInfoDialog: {
|
||||
visible: false,
|
||||
},
|
||||
exportDialog: {
|
||||
visible: false,
|
||||
dbId: 0,
|
||||
@@ -293,8 +256,7 @@ const state = reactive({
|
||||
},
|
||||
});
|
||||
|
||||
const { dbId, db, tags, selectionData, query, datas, total, infoDialog, sqlExecLogDialog, tableInfoDialog, exportDialog, dbEditDialog, filterDb } =
|
||||
toRefs(state);
|
||||
const { db, tags, selectionData, query, datas, total, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog } = toRefs(state);
|
||||
|
||||
onMounted(async () => {
|
||||
if (Object.keys(actionBtns).length > 0) {
|
||||
@@ -449,36 +411,6 @@ const dumpDbs = () => {
|
||||
a.click();
|
||||
state.exportDialog.visible = false;
|
||||
};
|
||||
|
||||
const showTableInfo = async (row: any, db: string) => {
|
||||
state.dbId = row.id;
|
||||
state.row = row;
|
||||
state.db = db;
|
||||
state.tableInfoDialog.visible = true;
|
||||
};
|
||||
|
||||
const closeTableInfo = () => {
|
||||
state.showDumpInfo = false;
|
||||
state.tableInfoDialog.visible = false;
|
||||
};
|
||||
|
||||
// 点击查看时初始化数据
|
||||
const selectDb = (row: any) => {
|
||||
state.filterDb.param = '';
|
||||
state.filterDb.cache = row;
|
||||
state.filterDb.list = row;
|
||||
};
|
||||
|
||||
// 输入字符过滤schema
|
||||
const filterSchema = () => {
|
||||
if (state.filterDb.param) {
|
||||
state.filterDb.list = state.filterDb.cache.filter((a) => {
|
||||
return String(a).toLowerCase().indexOf(state.filterDb.param) > -1;
|
||||
});
|
||||
} else {
|
||||
state.filterDb.list = state.filterDb.cache;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.el-dropdown-link-more {
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRefs,watch, reactive, computed, onMounted, defineAsyncComponent } from 'vue';
|
||||
import { toRefs, watch, reactive, onMounted } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import { DbSqlExecTypeEnum } from './enums';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
@@ -103,13 +103,12 @@ onMounted(async () => {
|
||||
searchSqlExecLog();
|
||||
});
|
||||
|
||||
watch(props, async (newValue: any) => {
|
||||
watch(props, async () => {
|
||||
await searchSqlExecLog();
|
||||
});
|
||||
|
||||
|
||||
const searchSqlExecLog = async () => {
|
||||
state.query.dbId = props.dbId
|
||||
state.query.dbId = props.dbId;
|
||||
const res = await dbApi.getSqlExecs.request(state.query);
|
||||
state.data = res.list;
|
||||
state.total = res.total;
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="testConn" :loading="state.testConnBtnLoading" type="success">测试连接</el-button>
|
||||
<el-button @click="cancel()">取 消</el-button>
|
||||
<el-button type="primary" :loading="btnLoading" @click="btnOk">确 定</el-button>
|
||||
</div>
|
||||
@@ -153,6 +154,7 @@ const state = reactive({
|
||||
// 原用户名
|
||||
oldUserName: null,
|
||||
btnLoading: false,
|
||||
testConnBtnLoading: false,
|
||||
});
|
||||
|
||||
const { dialogVisible, tabActiveName, form, pwd, btnLoading } = toRefs(state);
|
||||
@@ -176,6 +178,32 @@ const getDbPwd = async () => {
|
||||
state.pwd = await dbApi.getInstancePwd.request({ id: state.form.id });
|
||||
};
|
||||
|
||||
const getReqForm = async () => {
|
||||
const reqForm = { ...state.form };
|
||||
reqForm.password = await RsaEncrypt(reqForm.password);
|
||||
if (!state.form.sshTunnelMachineId) {
|
||||
reqForm.sshTunnelMachineId = -1;
|
||||
}
|
||||
return reqForm;
|
||||
};
|
||||
|
||||
const testConn = async () => {
|
||||
dbForm.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
state.testConnBtnLoading = true;
|
||||
try {
|
||||
await dbApi.testConn.request(await getReqForm());
|
||||
ElMessage.success('连接成功');
|
||||
} finally {
|
||||
state.testConnBtnLoading = false;
|
||||
}
|
||||
} else {
|
||||
ElMessage.error('请正确填写信息');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const btnOk = async () => {
|
||||
if (!state.form.id) {
|
||||
notBlank(state.form.password, '新增操作,密码不可为空');
|
||||
@@ -185,12 +213,7 @@ const btnOk = async () => {
|
||||
|
||||
dbForm.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
const reqForm = { ...state.form };
|
||||
reqForm.password = await RsaEncrypt(reqForm.password);
|
||||
if (!state.form.sshTunnelMachineId) {
|
||||
reqForm.sshTunnelMachineId = -1;
|
||||
}
|
||||
dbApi.saveInstance.request(reqForm).then(() => {
|
||||
dbApi.saveInstance.request(await getReqForm()).then(() => {
|
||||
ElMessage.success('保存成功');
|
||||
emit('val-change', state.form);
|
||||
state.btnLoading = true;
|
||||
|
||||
@@ -87,7 +87,7 @@ const columns = ref([
|
||||
|
||||
// 该用户拥有的的操作列按钮权限
|
||||
const actionBtns = hasPerms([perms.saveInstance]);
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(65).fixedRight().alignCenter();
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(110).fixedRight().alignCenter();
|
||||
|
||||
const pageTableRef: any = ref(null);
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<tag-tree ref="tagTreeRef" :loadTags="loadTags" @current-contextmenu-click="onCurrentContextmenuClick" :height="state.tagTreeHeight">
|
||||
<template #prefix="{ data }">
|
||||
<span v-if="data.type.value == SqlExecNodeType.DbInst">
|
||||
<el-popover :show-after="500" placement="right-start" title="数据库实例信息" trigger="hover" :width="210">
|
||||
<el-popover :show-after="500" placement="right-start" title="数据库实例信息" trigger="hover" :width="250">
|
||||
<template #reference>
|
||||
<SvgIcon v-if="data.params.type === 'mysql'" name="iconfont icon-op-mysql" :size="18" />
|
||||
<SvgIcon v-if="data.params.type === 'postgres'" name="iconfont icon-op-postgres" :size="18" />
|
||||
@@ -42,32 +42,31 @@
|
||||
<SvgIcon name="InfoFilled" v-else />
|
||||
</template>
|
||||
<template #default>
|
||||
<el-form class="instances-pop-form" label-width="auto" :size="'small'">
|
||||
<el-form-item label="类型:">{{ data.params.type }}</el-form-item>
|
||||
<el-form-item label="host:">{{ `${data.params.host}:${data.params.port}` }}</el-form-item>
|
||||
<el-form-item label="user:">{{ data.params.username }}</el-form-item>
|
||||
<el-form-item label="名称:">{{ data.params.name }}</el-form-item>
|
||||
<el-form-item v-if="data.params.remark" label="备注:">{{ data.params.remark }}</el-form-item>
|
||||
</el-form>
|
||||
<el-descriptions :column="1" size="small">
|
||||
<el-descriptions-item label="名称">
|
||||
{{ data.params.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="host">
|
||||
{{ `${data.params.host}:${data.params.port}` }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="user">
|
||||
{{ data.params.username }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注">
|
||||
{{ data.params.remark }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-popover>
|
||||
</span>
|
||||
|
||||
<SvgIcon v-if="data.type.value == SqlExecNodeType.Db" name="Coin" color="#67c23a" />
|
||||
<SvgIcon v-if="data.icon" :name="data.icon.name" :color="data.icon.color" />
|
||||
</template>
|
||||
|
||||
<SvgIcon name="Calendar" v-if="data.type.value == SqlExecNodeType.TableMenu" color="#409eff" />
|
||||
|
||||
<el-tooltip
|
||||
:show-after="500"
|
||||
v-if="data.type.value == SqlExecNodeType.Table"
|
||||
effect="customized"
|
||||
:content="data.params.tableComment"
|
||||
placement="top-end"
|
||||
>
|
||||
<SvgIcon name="Calendar" color="#409eff" />
|
||||
<template #label="{ data }">
|
||||
<el-tooltip placement="left" :show-after="1000" v-if="data.type.value == SqlExecNodeType.Table" :content="data.params.tableComment">
|
||||
{{ data.label }}
|
||||
</el-tooltip>
|
||||
|
||||
<SvgIcon name="Files" v-if="data.type.value == SqlExecNodeType.SqlMenu || data.type.value == SqlExecNodeType.Sql" color="#f56c6c" />
|
||||
</template>
|
||||
|
||||
<template #suffix="{ data }">
|
||||
@@ -81,32 +80,61 @@
|
||||
|
||||
<el-col :span="20">
|
||||
<el-container id="data-exec" class="mt5 ml5">
|
||||
<el-tabs @tab-remove="onRemoveTab" @tab-change="onTabChange" style="width: 100%" v-model="state.activeName">
|
||||
<el-tab-pane closable v-for="dt in state.tabs.values()" :key="dt.key" :label="dt.key" :name="dt.key">
|
||||
<table-data
|
||||
v-if="dt.type === TabType.TableData"
|
||||
@gen-insert-sql="onGenerateInsertSql"
|
||||
:data="dt"
|
||||
:table-height="state.dataTabsTableHeight"
|
||||
></table-data>
|
||||
<el-tabs type="card" @tab-remove="onRemoveTab" @tab-change="onTabChange" style="width: 100%" v-model="state.activeName">
|
||||
<el-tab-pane closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
|
||||
<!-- <template #label>
|
||||
<el-popover :show-after="500" placement="right-start" title="数据库实例信息" trigger="hover" :width="250">
|
||||
<template #reference> {{ dt.label }} </template>
|
||||
<template #default>
|
||||
<el-descriptions :column="1" size="small">
|
||||
<el-descriptions-item label="名称">
|
||||
{{ dt.params.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="host">
|
||||
{{ `${dt.params.host}:${dt.params.port}` }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="user">
|
||||
{{ dt.params.username }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注">
|
||||
{{ dt.params.remark }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-popover>
|
||||
</template> -->
|
||||
|
||||
<query
|
||||
v-else
|
||||
<db-table-data-op
|
||||
v-if="dt.type === TabType.TableData"
|
||||
:db-id="dt.dbId"
|
||||
:db-name="dt.db"
|
||||
:table-name="dt.params.table"
|
||||
:table-height="state.dataTabsTableHeight"
|
||||
></db-table-data-op>
|
||||
|
||||
<db-sql-editor
|
||||
v-if="dt.type === TabType.Query"
|
||||
:db-id="dt.dbId"
|
||||
:db-name="dt.db"
|
||||
:sql-name="dt.params.sqlName"
|
||||
@save-sql-success="reloadSqls"
|
||||
@delete-sql-success="deleteSqlScript(dt)"
|
||||
:data="dt"
|
||||
:editor-height="state.editorHeight"
|
||||
>
|
||||
</query>
|
||||
</db-sql-editor>
|
||||
|
||||
<db-tables-op
|
||||
v-if="dt.type == TabType.TablesOp"
|
||||
:db-id="dt.params.id"
|
||||
:db="dt.params.db"
|
||||
:db-type="dt.params.type"
|
||||
:height="state.tablesOpHeight"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-container>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-dialog @close="state.genSqlDialog.visible = false" v-model="state.genSqlDialog.visible" title="SQL" width="1000px">
|
||||
<el-input v-model="state.genSqlDialog.sql" type="textarea" rows="20" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -120,8 +148,10 @@ import TagTree from '../component/TagTree.vue';
|
||||
import { dbApi } from './api';
|
||||
import { dispposeCompletionItemProvider } from '../../../components/monaco/completionItemProvider';
|
||||
|
||||
const Query = defineAsyncComponent(() => import('./component/tab/Query.vue'));
|
||||
const TableData = defineAsyncComponent(() => import('./component/tab/TableData.vue'));
|
||||
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
|
||||
const DbTableDataOp = defineAsyncComponent(() => import('./component/table/DbTableDataOp.vue'));
|
||||
const DbTablesOp = defineAsyncComponent(() => import('./component/table/DbTablesOp.vue'));
|
||||
|
||||
/**
|
||||
* 树节点类型
|
||||
*/
|
||||
@@ -132,16 +162,40 @@ class SqlExecNodeType {
|
||||
static SqlMenu = 4;
|
||||
static Table = 5;
|
||||
static Sql = 6;
|
||||
static PgSchemaMenu = 7;
|
||||
static PgSchema = 8;
|
||||
}
|
||||
|
||||
const DbIcon = {
|
||||
name: 'Coin',
|
||||
color: '#67c23a',
|
||||
};
|
||||
|
||||
// pgsql schema icon
|
||||
const SchemaIcon = {
|
||||
name: 'List',
|
||||
color: '#67c23a',
|
||||
};
|
||||
|
||||
const TableIcon = {
|
||||
name: 'Calendar',
|
||||
color: '#409eff',
|
||||
};
|
||||
|
||||
const SqlIcon = {
|
||||
name: 'Files',
|
||||
color: '#f56c6c',
|
||||
};
|
||||
|
||||
class ContextmenuClickId {
|
||||
static ReloadTable = 0;
|
||||
static TableOp = 1;
|
||||
}
|
||||
|
||||
// node节点点击时,触发改变db事件
|
||||
const changeDb = (nodeData: TagTreeNode) => {
|
||||
const nodeClickChangeDb = (nodeData: TagTreeNode) => {
|
||||
const params = nodeData.params;
|
||||
changeSchema({ id: params.id, name: params.name, type: params.type, tagPath: params.tagPath, databases: params.database }, params.db);
|
||||
changeDb({ id: params.id, name: params.name, type: params.type, tagPath: params.tagPath, databases: params.database }, params.db);
|
||||
};
|
||||
|
||||
// tagpath 节点类型
|
||||
@@ -161,54 +215,93 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst)
|
||||
const params = parentNode.params;
|
||||
const dbs = params.database.split(' ')?.sort();
|
||||
return dbs.map((x: any) => {
|
||||
return new TagTreeNode(`${parentNode.key}.${x}`, x, NodeTypeDb).withParams({
|
||||
tagPath: params.tagPath,
|
||||
id: params.id,
|
||||
name: params.name,
|
||||
type: params.type,
|
||||
dbs: dbs,
|
||||
db: x,
|
||||
});
|
||||
return new TagTreeNode(`${parentNode.key}.${x}`, x, NodeTypeDb)
|
||||
.withParams({
|
||||
tagPath: params.tagPath,
|
||||
id: params.id,
|
||||
name: params.name,
|
||||
type: params.type,
|
||||
dbs: dbs,
|
||||
db: x,
|
||||
})
|
||||
.withIcon(DbIcon);
|
||||
});
|
||||
})
|
||||
.withNodeClickFunc(changeDb);
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
|
||||
// 数据库节点
|
||||
const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
if (params.type == 'postgres') {
|
||||
return [new TagTreeNode(`${params.id}.${params.db}.schema-menu`, 'schema', NodeTypePostgresScheamMenu).withParams(params).withIcon(SchemaIcon)];
|
||||
}
|
||||
|
||||
return [
|
||||
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams(params),
|
||||
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeTypeSqlMenu).withParams(params),
|
||||
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams(params).withIcon(TableIcon),
|
||||
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeTypeSqlMenu).withParams(params).withIcon(SqlIcon),
|
||||
];
|
||||
})
|
||||
.withNodeClickFunc(changeDb);
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
|
||||
// 数据库表菜单节点
|
||||
const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
|
||||
.withContextMenuItems([{ contextMenuClickId: ContextmenuClickId.ReloadTable, txt: '刷新', icon: 'RefreshRight' }] as any)
|
||||
// postgres schema模式菜单
|
||||
const NodeTypePostgresScheamMenu = new NodeType(SqlExecNodeType.PgSchemaMenu)
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
const { id, db } = params;
|
||||
const schemaNames = await dbApi.pgSchemas.request({ id, db });
|
||||
return schemaNames.map((sn: any) => {
|
||||
// 将db变更为 db/schema;
|
||||
const nParams = { ...params };
|
||||
nParams.schema = sn;
|
||||
nParams.db = nParams.db + '/' + sn;
|
||||
return new TagTreeNode(`${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresScheam).withParams(nParams).withIcon(SchemaIcon);
|
||||
});
|
||||
})
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
|
||||
// postgres schema模式
|
||||
const NodeTypePostgresScheam = new NodeType(SqlExecNodeType.PgSchema)
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
return [
|
||||
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams(params).withIcon(TableIcon),
|
||||
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeTypeSqlMenu).withParams(params).withIcon(SqlIcon),
|
||||
];
|
||||
})
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
|
||||
// 数据库表菜单节点
|
||||
const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
|
||||
.withContextMenuItems([
|
||||
{ contextMenuClickId: ContextmenuClickId.ReloadTable, txt: '刷新', icon: 'RefreshRight' },
|
||||
{ contextMenuClickId: ContextmenuClickId.TableOp, txt: '表操作', icon: 'Setting' },
|
||||
] as any)
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
let { id, db } = params;
|
||||
// 获取当前库的所有表信息
|
||||
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
|
||||
state.reloadStatus = false;
|
||||
let dbTableSize = 0;
|
||||
const tablesNode = tables.map((x: any) => {
|
||||
dbTableSize += x.dataLength + x.indexLength;
|
||||
return new TagTreeNode(`${id}.${db}.${x.tableName}`, x.tableName, NodeTypeTable).withIsLeaf(true).withParams({
|
||||
id,
|
||||
db,
|
||||
tableName: x.tableName,
|
||||
tableComment: x.tableComment,
|
||||
size: formatByteSize(x.dataLength + x.indexLength, 1),
|
||||
});
|
||||
return new TagTreeNode(`${id}.${db}.${x.tableName}`, x.tableName, NodeTypeTable)
|
||||
.withIsLeaf(true)
|
||||
.withParams({
|
||||
id,
|
||||
db,
|
||||
tableName: x.tableName,
|
||||
tableComment: x.tableComment,
|
||||
size: formatByteSize(x.dataLength + x.indexLength, 1),
|
||||
})
|
||||
.withIcon(TableIcon);
|
||||
});
|
||||
// 设置父节点参数的表大小
|
||||
parentNode.params.dbTableSize = formatByteSize(dbTableSize);
|
||||
return tablesNode;
|
||||
})
|
||||
.withNodeClickFunc(changeDb);
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
|
||||
// 数据库sql模板菜单节点
|
||||
const NodeTypeSqlMenu = new NodeType(SqlExecNodeType.SqlMenu)
|
||||
@@ -220,15 +313,18 @@ const NodeTypeSqlMenu = new NodeType(SqlExecNodeType.SqlMenu)
|
||||
// 加载用户保存的sql脚本
|
||||
const sqls = await dbApi.getSqlNames.request({ id: id, db: db });
|
||||
return sqls.map((x: any) => {
|
||||
return new TagTreeNode(`${id}.${db}.${x.name}`, x.name, NodeTypeSql).withIsLeaf(true).withParams({
|
||||
id,
|
||||
db,
|
||||
dbs,
|
||||
sqlName: x.name,
|
||||
});
|
||||
return new TagTreeNode(`${id}.${db}.${x.name}`, x.name, NodeTypeSql)
|
||||
.withIsLeaf(true)
|
||||
.withParams({
|
||||
id,
|
||||
db,
|
||||
dbs,
|
||||
sqlName: x.name,
|
||||
})
|
||||
.withIcon(SqlIcon);
|
||||
});
|
||||
})
|
||||
.withNodeClickFunc(changeDb);
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
|
||||
// 表节点类型
|
||||
const NodeTypeTable = new NodeType(SqlExecNodeType.Table).withNodeClickFunc((nodeData: TagTreeNode) => {
|
||||
@@ -256,11 +352,8 @@ const state = reactive({
|
||||
tabs,
|
||||
dataTabsTableHeight: '600',
|
||||
editorHeight: '600',
|
||||
tablesOpHeight: '600',
|
||||
tagTreeHeight: window.innerHeight - 178 + 'px',
|
||||
genSqlDialog: {
|
||||
visible: false,
|
||||
sql: '',
|
||||
},
|
||||
});
|
||||
|
||||
const { nowDbInst } = toRefs(state);
|
||||
@@ -280,7 +373,8 @@ onBeforeUnmount(() => {
|
||||
*/
|
||||
const setHeight = () => {
|
||||
state.editorHeight = window.innerHeight - 518 + 'px';
|
||||
state.dataTabsTableHeight = window.innerHeight - 256 + 'px';
|
||||
state.dataTabsTableHeight = window.innerHeight - 262 + 'px';
|
||||
state.tablesOpHeight = window.innerHeight - 240 + 'px';
|
||||
state.tagTreeHeight = window.innerHeight - 165 + 'px';
|
||||
};
|
||||
|
||||
@@ -318,54 +412,62 @@ const onCurrentContextmenuClick = (clickData: any) => {
|
||||
const clickId = clickData.id;
|
||||
if (clickId == ContextmenuClickId.ReloadTable) {
|
||||
reloadTables(clickData.item.key);
|
||||
return;
|
||||
}
|
||||
if (clickId == ContextmenuClickId.TableOp) {
|
||||
const params = clickData.item.params;
|
||||
addTablesOpTab({ id: params.id, db: params.db, type: params.type, nodeKey: clickData.item.key });
|
||||
}
|
||||
};
|
||||
|
||||
// 选择数据库
|
||||
const changeSchema = (inst: any, schema: string) => {
|
||||
state.nowDbInst = DbInst.getOrNewInst(inst);
|
||||
state.db = schema;
|
||||
const changeDb = (db: any, dbName: string) => {
|
||||
state.nowDbInst = DbInst.getOrNewInst(db);
|
||||
state.db = dbName;
|
||||
};
|
||||
|
||||
// 加载选中的表数据,即新增表数据操作tab
|
||||
const loadTableData = async (inst: any, schema: string, tableName: string) => {
|
||||
changeSchema(inst, schema);
|
||||
const loadTableData = async (db: any, dbName: string, tableName: string) => {
|
||||
changeDb(db, dbName);
|
||||
if (tableName == '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const label = `${inst.id}:\`${schema}\`.${tableName}`;
|
||||
let tab = state.tabs.get(label);
|
||||
state.activeName = label;
|
||||
const key = `${db.id}:\`${dbName}\`.${tableName}`;
|
||||
let tab = state.tabs.get(key);
|
||||
state.activeName = key;
|
||||
// 如果存在该表tab,则直接返回
|
||||
if (tab) {
|
||||
return;
|
||||
}
|
||||
tab = new TabInfo();
|
||||
tab.key = label;
|
||||
tab.treeNodeKey = inst.nodeKey;
|
||||
tab.dbId = inst.id;
|
||||
tab.db = schema;
|
||||
tab.label = tableName;
|
||||
tab.key = key;
|
||||
tab.treeNodeKey = db.nodeKey;
|
||||
tab.dbId = db.id;
|
||||
tab.db = dbName;
|
||||
tab.type = TabType.TableData;
|
||||
tab.params = {
|
||||
table: tableName,
|
||||
};
|
||||
state.tabs.set(label, tab);
|
||||
state.tabs.set(key, tab);
|
||||
};
|
||||
|
||||
// 新建查询panel
|
||||
const addQueryTab = async (inst: any, db: string, sqlName: string = '') => {
|
||||
if (!db || !inst.id) {
|
||||
// 新建查询tab
|
||||
const addQueryTab = async (db: any, dbName: string, sqlName: string = '') => {
|
||||
if (!dbName || !db.id) {
|
||||
ElMessage.warning('请选择数据库实例及对应的schema');
|
||||
return;
|
||||
}
|
||||
changeSchema(inst, db);
|
||||
changeDb(db, dbName);
|
||||
|
||||
const dbId = inst.id;
|
||||
const dbId = db.id;
|
||||
let label;
|
||||
let key;
|
||||
// 存在sql模板名,则该模板名只允许一个tab
|
||||
if (sqlName) {
|
||||
label = `查询:${dbId}:${db}.${sqlName}`;
|
||||
label = `查询-${sqlName}`;
|
||||
key = `查询:${dbId}:${dbName}.${sqlName}`;
|
||||
} else {
|
||||
let count = 1;
|
||||
state.tabs.forEach((v) => {
|
||||
@@ -373,29 +475,66 @@ const addQueryTab = async (inst: any, db: string, sqlName: string = '') => {
|
||||
count++;
|
||||
}
|
||||
});
|
||||
label = `新查询${count}:${dbId}:${db}`;
|
||||
label = `新查询-${count}`;
|
||||
key = `新查询${count}:${dbId}:${dbName}`;
|
||||
}
|
||||
state.activeName = label;
|
||||
let tab = state.tabs.get(label);
|
||||
state.activeName = key;
|
||||
let tab = state.tabs.get(key);
|
||||
if (tab) {
|
||||
return;
|
||||
}
|
||||
tab = new TabInfo();
|
||||
tab.key = label;
|
||||
tab.treeNodeKey = inst.nodeKey;
|
||||
tab.key = key;
|
||||
tab.label = label;
|
||||
tab.treeNodeKey = db.nodeKey;
|
||||
tab.dbId = dbId;
|
||||
tab.db = db;
|
||||
tab.db = dbName;
|
||||
tab.type = TabType.Query;
|
||||
tab.params = {
|
||||
sqlName: sqlName,
|
||||
dbs: inst.dbs,
|
||||
dbs: db.dbs,
|
||||
};
|
||||
state.tabs.set(label, tab);
|
||||
state.tabs.set(key, tab);
|
||||
|
||||
// 注册当前sql编辑框提示词
|
||||
registerDbCompletionItemProvider('sql', tab.dbId, tab.db, tab.params.dbs);
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加数据操作tab
|
||||
* @param inst
|
||||
*/
|
||||
const addTablesOpTab = async (db: any) => {
|
||||
const dbName = db.db;
|
||||
if (!db || !db.id) {
|
||||
ElMessage.warning('请选择数据库实例及对应的schema');
|
||||
return;
|
||||
}
|
||||
changeDb(db, dbName);
|
||||
|
||||
const dbId = db.id;
|
||||
let key = `表操作:${dbId}:${dbName}.tablesOp`;
|
||||
state.activeName = key;
|
||||
|
||||
let tab = state.tabs.get(key);
|
||||
if (tab) {
|
||||
return;
|
||||
}
|
||||
tab = new TabInfo();
|
||||
tab.key = key;
|
||||
tab.label = `表操作-${dbName}`;
|
||||
tab.treeNodeKey = db.nodeKey;
|
||||
tab.dbId = dbId;
|
||||
tab.db = dbName;
|
||||
tab.type = TabType.TablesOp;
|
||||
tab.params = {
|
||||
id: db.id,
|
||||
db: dbName,
|
||||
type: db.type,
|
||||
};
|
||||
state.tabs.set(key, tab);
|
||||
};
|
||||
|
||||
const onRemoveTab = (targetName: string) => {
|
||||
let activeName = state.activeName;
|
||||
const tabNames = [...state.tabs.keys()];
|
||||
@@ -412,6 +551,7 @@ const onRemoveTab = (targetName: string) => {
|
||||
}
|
||||
state.tabs.delete(targetName);
|
||||
state.activeName = activeName;
|
||||
onTabChange();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -432,11 +572,6 @@ const onTabChange = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onGenerateInsertSql = async (sql: string) => {
|
||||
state.genSqlDialog.sql = sql;
|
||||
state.genSqlDialog.visible = true;
|
||||
};
|
||||
|
||||
const reloadSqls = (dbId: number, db: string) => {
|
||||
tagTreeRef.value.reloadNode(getSqlMenuNodeKey(dbId, db));
|
||||
};
|
||||
@@ -467,10 +602,10 @@ const reloadTables = (nodeKey: string) => {
|
||||
min-height: calc(100vh - 155px);
|
||||
|
||||
.el-tabs__header {
|
||||
margin: 0 0 5px;
|
||||
margin: 0 0 10px;
|
||||
|
||||
.el-tabs__item {
|
||||
padding: 0 5px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -478,11 +613,5 @@ const reloadTables = (nodeKey: string) => {
|
||||
.update_field_active {
|
||||
background-color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.instances-pop-form {
|
||||
.el-form-item {
|
||||
margin-bottom: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,6 +11,7 @@ export const dbApi = {
|
||||
tableIndex: Api.newGet('/dbs/{id}/t-index'),
|
||||
tableDdl: Api.newGet('/dbs/{id}/t-create-ddl'),
|
||||
columnMetadata: Api.newGet('/dbs/{id}/c-metadata'),
|
||||
pgSchemas: Api.newGet('/dbs/{id}/pg/schemas'),
|
||||
// 获取表即列提示
|
||||
hintTables: Api.newGet('/dbs/{id}/hint-tables'),
|
||||
sqlExec: Api.newPost('/dbs/{id}/exec-sql'),
|
||||
@@ -28,6 +29,7 @@ export const dbApi = {
|
||||
instances: Api.newGet('/instances'),
|
||||
getInstance: Api.newGet('/instances/{instanceId}'),
|
||||
getAllDatabase: Api.newGet('/instances/{instanceId}/databases'),
|
||||
testConn: Api.newPost('/instances/test-conn'),
|
||||
saveInstance: Api.newPost('/instances'),
|
||||
getInstancePwd: Api.newGet('/instances/{id}/pwd'),
|
||||
deleteInstance: Api.newDelete('/instances/{id}'),
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MonacoEditor ref="monacoEditorRef" class="mt5" v-model="state.sql" language="sql" :height="state.editorHeight" :id="'MonacoTextarea-' + ti.key" />
|
||||
<MonacoEditor ref="monacoEditorRef" class="mt5" v-model="state.sql" language="sql" :height="state.editorHeight" :id="'MonacoTextarea-' + getKey()" />
|
||||
|
||||
<div class="editor-move-resize" @mousedown="onDragSetHeight">
|
||||
<el-icon>
|
||||
@@ -69,10 +69,10 @@
|
||||
<el-link type="warning" :underline="false" @click="cancelUpdateFields"><span style="font-size: 12px">取消</span></el-link>
|
||||
</span>
|
||||
</el-row>
|
||||
<db-table
|
||||
<db-table-data
|
||||
ref="dbTableRef"
|
||||
:db-id="state.ti.dbId"
|
||||
:db="state.ti.db"
|
||||
:db-id="dbId"
|
||||
:db="dbName"
|
||||
:data="execRes.data"
|
||||
:table="state.table"
|
||||
:columns="execRes.tableColumn"
|
||||
@@ -81,7 +81,7 @@
|
||||
empty-text="tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改"
|
||||
@selection-change="onDataSelectionChange"
|
||||
@change-updated-field="changeUpdatedField"
|
||||
></db-table>
|
||||
></db-table-data>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -97,8 +97,8 @@ import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import { editor } from 'monaco-editor';
|
||||
|
||||
import DbTable from '../DbTable.vue';
|
||||
import { TabInfo } from '../../db';
|
||||
import DbTableData from '@/views/ops/db/component/table/DbTableData.vue';
|
||||
import { DbInst } from '../../db';
|
||||
import { exportCsv } from '@/common/utils/export';
|
||||
import { dateStrFormat } from '@/common/utils/date';
|
||||
import { dbApi } from '../../api';
|
||||
@@ -113,15 +113,18 @@ import syssocket from '@/common/syssocket';
|
||||
const emits = defineEmits(['saveSqlSuccess', 'deleteSqlSuccess']);
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: TabInfo,
|
||||
dbId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
dbName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
// sql脚本名,若有则去加载该sql内容
|
||||
// sqlName: {
|
||||
// type: String,
|
||||
// default: '',
|
||||
// },
|
||||
sqlName: {
|
||||
type: String,
|
||||
},
|
||||
editorHeight: {
|
||||
type: String,
|
||||
default: '600',
|
||||
@@ -136,9 +139,6 @@ let monacoEditor: editor.IStandaloneCodeEditor;
|
||||
|
||||
const state = reactive({
|
||||
token,
|
||||
ti: {} as TabInfo,
|
||||
dbs: [],
|
||||
dbId: null, // 当前选中操作的数据库实例
|
||||
table: '', // 当前单表操作sql的表信息
|
||||
sqlName: '',
|
||||
sql: '', // 当前编辑器的sql内容
|
||||
@@ -153,7 +153,7 @@ const state = reactive({
|
||||
hasUpdatedFileds: false,
|
||||
});
|
||||
|
||||
const { tableDataHeight, ti, execRes, table, sqlName, loading, hasUpdatedFileds } = toRefs(state);
|
||||
const { tableDataHeight, execRes, table, loading, hasUpdatedFileds } = toRefs(state);
|
||||
|
||||
watch(
|
||||
() => props.editorHeight,
|
||||
@@ -162,22 +162,22 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
const getNowDbInst = () => {
|
||||
return DbInst.getInst(props.dbId);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
console.log('in query mounted');
|
||||
state.ti = props.data;
|
||||
state.editorHeight = props.editorHeight;
|
||||
const params = state.ti.params;
|
||||
state.dbs = params && params.dbs;
|
||||
|
||||
if (params && params.sqlName) {
|
||||
state.sqlName = params.sqlName;
|
||||
const res = await dbApi.getSql.request({ id: state.ti.dbId, type: 1, name: state.sqlName, db: state.ti.db });
|
||||
if (props.sqlName) {
|
||||
const res = await dbApi.getSql.request({ id: props.dbId, type: 1, name: state.sqlName, db: props.dbName });
|
||||
state.sql = res.sql;
|
||||
}
|
||||
nextTick(() => {
|
||||
setTimeout(() => initMonacoEditor(), 50);
|
||||
});
|
||||
await state.ti.getNowDbInst().loadDbHints(state.ti.db);
|
||||
await getNowDbInst().loadDbHints(props.dbName);
|
||||
});
|
||||
|
||||
const initMonacoEditor = () => {
|
||||
@@ -186,7 +186,8 @@ const initMonacoEditor = () => {
|
||||
// 注册快捷键:ctrl + R 运行选中的sql
|
||||
monacoEditor.addAction({
|
||||
// An unique identifier of the contributed action.
|
||||
id: 'run-sql-action' + state.ti.key,
|
||||
// id: 'run-sql-action' + state.ti.key,
|
||||
id: 'run-sql-action' + getKey(),
|
||||
// A label of the action that will be presented to the user.
|
||||
label: '执行SQL',
|
||||
// A precondition for this action.
|
||||
@@ -213,7 +214,7 @@ const initMonacoEditor = () => {
|
||||
// 注册快捷键:ctrl + shift + f 格式化sql
|
||||
monacoEditor.addAction({
|
||||
// An unique identifier of the contributed action.
|
||||
id: 'format-sql-action' + state.ti.key,
|
||||
id: 'format-sql-action' + getKey(),
|
||||
// A label of the action that will be presented to the user.
|
||||
label: '格式化SQL',
|
||||
// A precondition for this action.
|
||||
@@ -245,7 +246,7 @@ const onDragSetHeight = () => {
|
||||
document.onmousemove = (e) => {
|
||||
e.preventDefault();
|
||||
//得到鼠标拖动的宽高距离:取绝对值
|
||||
state.editorHeight = `${document.getElementById('MonacoTextarea-' + state.ti.key)!.clientHeight + e.movementY}px`;
|
||||
state.editorHeight = `${document.getElementById('MonacoTextarea-' + getKey())!.clientHeight + e.movementY}px`;
|
||||
state.tableDataHeight -= e.movementY;
|
||||
};
|
||||
document.onmouseup = () => {
|
||||
@@ -253,6 +254,10 @@ const onDragSetHeight = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const getKey = () => {
|
||||
return props.dbId + ':' + props.dbName;
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行sql
|
||||
*/
|
||||
@@ -290,7 +295,7 @@ const onRunSql = async () => {
|
||||
try {
|
||||
state.loading = true;
|
||||
|
||||
const colAndData: any = await state.ti.getNowDbInst().runSql(state.ti.db, sql, execRemark);
|
||||
const colAndData: any = await getNowDbInst().runSql(props.dbName, sql, execRemark);
|
||||
if (!colAndData.res || colAndData.res.length === 0) {
|
||||
ElMessage.warning('未查询到结果集');
|
||||
}
|
||||
@@ -370,16 +375,17 @@ const saveSql = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
await dbApi.saveSql.request({ id: state.ti.dbId, db: state.ti.db, sql: sql, type: 1, name: sqlName });
|
||||
await dbApi.saveSql.request({ id: props.dbId, db: props.dbName, sql: sql, type: 1, name: sqlName });
|
||||
ElMessage.success('保存成功');
|
||||
// 保存sql脚本成功事件
|
||||
emits('saveSqlSuccess', state.ti.dbId, state.ti.db);
|
||||
emits('saveSqlSuccess', props.dbId, props.dbName);
|
||||
};
|
||||
|
||||
const deleteSql = async () => {
|
||||
const sqlName = state.sqlName;
|
||||
notBlank(sqlName, '该sql内容未保存');
|
||||
const { dbId, db } = state.ti;
|
||||
const dbId = props.dbId;
|
||||
const db = props.dbName;
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除【${sqlName}】该SQL内容?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
@@ -415,7 +421,7 @@ const formatSql = () => {
|
||||
* 提交事务,用于没有开启自动提交事务
|
||||
*/
|
||||
const onCommit = () => {
|
||||
state.ti.getNowDbInst().runSql(state.ti.db, 'COMMIT;');
|
||||
getNowDbInst().runSql(props.dbName, 'COMMIT;');
|
||||
ElMessage.success('COMMIT success');
|
||||
};
|
||||
|
||||
@@ -528,7 +534,7 @@ const execSqlFileSuccess = (res: any) => {
|
||||
|
||||
// 获取sql文件上传执行url
|
||||
const getUploadSqlFileUrl = () => {
|
||||
return `${config.baseApiUrl}/dbs/${state.ti.dbId}/exec-sql-file?db=${state.ti.db}&${joinClientParams()}`;
|
||||
return `${config.baseApiUrl}/dbs/${props.dbId}/exec-sql-file?db=${props.dbName}&${joinClientParams()}`;
|
||||
};
|
||||
|
||||
const onDataSelectionChange = (datas: []) => {
|
||||
@@ -546,8 +552,8 @@ const changeUpdatedField = (updatedFields: []) => {
|
||||
const onDeleteData = async () => {
|
||||
const deleteDatas = state.selectionDatas;
|
||||
isTrue(deleteDatas && deleteDatas.length > 0, '请先选择要删除的数据');
|
||||
const { db } = state.ti;
|
||||
const dbInst = state.ti.getNowDbInst();
|
||||
const db = props.dbName;
|
||||
const dbInst = getNowDbInst();
|
||||
const primaryKey = await dbInst.loadTableColumn(db, state.table);
|
||||
const primaryKeyColumnName = primaryKey.columnName;
|
||||
dbInst.promptExeSql(db, dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas), null, () => {
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, ref, nextTick, reactive } from 'vue';
|
||||
import { dbApi } from '../api';
|
||||
import { dbApi } from '@/views/ops/db/api';
|
||||
import { ElDialog, ElButton, ElInput, ElMessage, InputInstance } from 'element-plus';
|
||||
// import base style
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
@@ -30,7 +30,7 @@
|
||||
:sortable="sortable"
|
||||
>
|
||||
<template #header v-if="showColumnTip">
|
||||
<el-tooltip :show-after="500" raw-content placement="top" effect="customized">
|
||||
<el-tooltip :show-after="500" raw-content placement="top">
|
||||
<template #content> {{ getColumnTip(item) }} </template>
|
||||
{{ item.columnName }}
|
||||
</el-tooltip>
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, watch, reactive, toRefs } from 'vue';
|
||||
import { DbInst, UpdateFieldsMeta, FieldsMeta } from '../db';
|
||||
import { DbInst, UpdateFieldsMeta, FieldsMeta } from '@/views/ops/db/db';
|
||||
|
||||
const emits = defineEmits(['sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField']);
|
||||
|
||||
@@ -94,12 +94,12 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<db-table
|
||||
<db-table-data
|
||||
ref="dbTableRef"
|
||||
:db-id="state.ti.dbId"
|
||||
:db="state.ti.db"
|
||||
:db-id="dbId"
|
||||
:db="dbName"
|
||||
:data="datas"
|
||||
:table="state.table"
|
||||
:table="tableName"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:height="tableHeight"
|
||||
@@ -108,7 +108,7 @@
|
||||
@sort-change="(sort: any) => onTableSortChange(sort)"
|
||||
@selection-change="onDataSelectionChange"
|
||||
@change-updated-field="changeUpdatedField"
|
||||
></db-table>
|
||||
></db-table-data>
|
||||
|
||||
<el-row type="flex" class="mt5" justify="center">
|
||||
<el-pagination
|
||||
@@ -177,26 +177,37 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog @close="state.genSqlDialog.visible = false" v-model="state.genSqlDialog.visible" title="SQL" width="1000px">
|
||||
<el-input v-model="state.genSqlDialog.sql" type="textarea" rows="20" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, watch, reactive, toRefs, ref, Ref, onUnmounted } from 'vue';
|
||||
import { isTrue, notEmpty, notBlank } from '@/common/assert';
|
||||
import { isTrue, notEmpty } from '@/common/assert';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { DbInst, TabInfo } from '../../db';
|
||||
import { DbInst } from '@/views/ops/db/db';
|
||||
import { exportCsv } from '@/common/utils/export';
|
||||
import { dateStrFormat } from '@/common/utils/date';
|
||||
import DbTable from '../DbTable.vue';
|
||||
import DbTableData from './DbTableData.vue';
|
||||
|
||||
const emits = defineEmits(['genInsertSql']);
|
||||
const dataForm: any = ref(null);
|
||||
const conditionInputRef: any = ref();
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: TabInfo,
|
||||
dbId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
dbName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
tableName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
tableHeight: {
|
||||
@@ -208,8 +219,6 @@ const props = defineProps({
|
||||
const dbTableRef = ref(null) as Ref;
|
||||
|
||||
const state = reactive({
|
||||
ti: {} as TabInfo,
|
||||
table: '', // 当前的表名
|
||||
datas: [],
|
||||
sql: '', // 当前数据tab执行的sql
|
||||
orderBy: '',
|
||||
@@ -237,6 +246,10 @@ const state = reactive({
|
||||
placeholder: '',
|
||||
visible: false,
|
||||
},
|
||||
genSqlDialog: {
|
||||
visible: false,
|
||||
sql: '',
|
||||
},
|
||||
tableHeight: '600',
|
||||
hasUpdatedFileds: false,
|
||||
});
|
||||
@@ -250,14 +263,15 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
const getNowDbInst = () => {
|
||||
return DbInst.getInst(props.dbId);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
console.log('in table data mounted');
|
||||
state.ti = props.data;
|
||||
state.tableHeight = props.tableHeight;
|
||||
state.table = state.ti.params.table;
|
||||
notBlank(state.table, 'TableData组件params.table信息不能为空');
|
||||
|
||||
const columns = await state.ti.getNowDbInst().loadColumns(state.ti.db, state.table);
|
||||
const columns = await getNowDbInst().loadColumns(props.dbName, props.tableName);
|
||||
columns.forEach((x: any) => {
|
||||
x.show = true;
|
||||
});
|
||||
@@ -297,12 +311,13 @@ const pageChange = async () => {
|
||||
*/
|
||||
const selectData = async () => {
|
||||
state.loading = true;
|
||||
const dbInst = state.ti.getNowDbInst();
|
||||
const { db } = state.ti;
|
||||
const dbInst = getNowDbInst();
|
||||
const db = props.dbName;
|
||||
const table = props.tableName;
|
||||
try {
|
||||
const countRes = await dbInst.runSql(db, dbInst.getDefaultCountSql(state.table, state.condition));
|
||||
const countRes = await dbInst.runSql(db, dbInst.getDefaultCountSql(table, state.condition));
|
||||
state.count = countRes.res[0].count;
|
||||
let sql = dbInst.getDefaultSelectSql(state.table, state.condition, state.orderBy, state.pageNum, state.pageSize);
|
||||
let sql = dbInst.getDefaultSelectSql(table, state.condition, state.orderBy, state.pageNum, state.pageSize);
|
||||
state.sql = sql;
|
||||
if (state.count > 0) {
|
||||
const colAndData: any = await dbInst.runSql(db, sql);
|
||||
@@ -333,7 +348,7 @@ const exportData = () => {
|
||||
columnNames.push(column.columnName);
|
||||
}
|
||||
}
|
||||
exportCsv(`数据导出-${state.table}-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`, columnNames, dataList);
|
||||
exportCsv(`数据导出-${props.tableName}-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`, columnNames, dataList);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -376,7 +391,7 @@ const onCancelCondition = () => {
|
||||
* 提交事务,用于没有开启自动提交事务
|
||||
*/
|
||||
const onCommit = () => {
|
||||
state.ti.getNowDbInst().runSql(state.ti.db, 'COMMIT;');
|
||||
getNowDbInst().runSql(props.dbName, 'COMMIT;');
|
||||
ElMessage.success('COMMIT success');
|
||||
};
|
||||
|
||||
@@ -413,16 +428,17 @@ const changeUpdatedField = (updatedFields: []) => {
|
||||
const onDeleteData = async () => {
|
||||
const deleteDatas = state.selectionDatas;
|
||||
isTrue(deleteDatas && deleteDatas.length > 0, '请先选择要删除的数据');
|
||||
const { db } = state.ti;
|
||||
const dbInst = state.ti.getNowDbInst();
|
||||
dbInst.promptExeSql(db, dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas), null, () => {
|
||||
const db = props.dbName;
|
||||
const dbInst = getNowDbInst();
|
||||
dbInst.promptExeSql(db, dbInst.genDeleteByPrimaryKeysSql(db, props.tableName, deleteDatas), null, () => {
|
||||
onRefresh();
|
||||
});
|
||||
};
|
||||
|
||||
const onGenerateInsertSql = async () => {
|
||||
isTrue(state.selectionDatas && state.selectionDatas.length > 0, '请先选择数据');
|
||||
emits('genInsertSql', state.ti.getNowDbInst().genInsertSql(state.ti.db, state.table, state.selectionDatas));
|
||||
state.genSqlDialog.sql = getNowDbInst().genInsertSql(props.dbName, props.tableName, state.selectionDatas);
|
||||
state.genSqlDialog.visible = true;
|
||||
};
|
||||
|
||||
const submitUpdateFields = () => {
|
||||
@@ -434,7 +450,7 @@ const cancelUpdateFields = () => {
|
||||
};
|
||||
|
||||
const onShowAddDataDialog = async () => {
|
||||
state.addDataDialog.title = `添加'${state.table}'表数据`;
|
||||
state.addDataDialog.title = `添加'${props.tableName}'表数据`;
|
||||
state.addDataDialog.visible = true;
|
||||
};
|
||||
|
||||
@@ -447,7 +463,7 @@ const closeAddDataDialog = () => {
|
||||
const addRow = async () => {
|
||||
dataForm.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
const dbInst = state.ti.getNowDbInst();
|
||||
const dbInst = getNowDbInst();
|
||||
const data = state.addDataDialog.data;
|
||||
// key: 字段名,value: 字段名提示
|
||||
let obj: any = {};
|
||||
@@ -460,8 +476,8 @@ const addRow = async () => {
|
||||
}
|
||||
let columnNames = Object.keys(obj).join(',');
|
||||
let values = Object.values(obj).join(',');
|
||||
let sql = `INSERT INTO ${dbInst.wrapName(state.table)} (${columnNames}) VALUES (${values});`;
|
||||
dbInst.promptExeSql(state.ti.db, sql, null, () => {
|
||||
let sql = `INSERT INTO ${dbInst.wrapName(props.tableName)} (${columnNames}) VALUES (${values});`;
|
||||
dbInst.promptExeSql(props.dbName, sql, null, () => {
|
||||
closeAddDataDialog();
|
||||
onRefresh();
|
||||
});
|
||||
@@ -136,7 +136,7 @@
|
||||
import { watch, toRefs, reactive, ref } from 'vue';
|
||||
import { TYPE_LIST, CHARACTER_SET_NAME_LIST, COLLATION_SUFFIX_LIST } from './service';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import SqlExecBox from '../component/SqlExecBox';
|
||||
import SqlExecBox from '../sqleditor/SqlExecBox';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@@ -30,7 +30,7 @@
|
||||
<el-button type="primary" size="small" @click="openEditTable(false)">创建表</el-button>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" border stripe :data="filterTableInfos" size="small" height="65vh">
|
||||
<el-table v-loading="loading" border stripe :data="filterTableInfos" size="small" :height="height">
|
||||
<el-table-column property="tableName" label="表名" min-width="150" show-overflow-tooltip>
|
||||
<template #header>
|
||||
<el-input v-model="tableNameSearch" size="small" placeholder="表名: 输入可过滤" clearable />
|
||||
@@ -104,7 +104,7 @@
|
||||
<el-input disabled type="textarea" :autosize="{ minRows: 15, maxRows: 30 }" v-model="ddlDialog.ddl" size="small"> </el-input>
|
||||
</el-dialog>
|
||||
|
||||
<db-table-edit
|
||||
<db-table-op
|
||||
:title="tableCreateDialog.title"
|
||||
:active-name="tableCreateDialog.activeName"
|
||||
:dbId="dbId"
|
||||
@@ -113,7 +113,7 @@
|
||||
v-model:visible="tableCreateDialog.visible"
|
||||
@submit-sql="onSubmitSql"
|
||||
>
|
||||
</db-table-edit>
|
||||
</db-table-op>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -121,15 +121,19 @@
|
||||
import { toRefs, reactive, watch, computed, onMounted, defineAsyncComponent } from 'vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { formatByteSize } from '@/common/utils/format';
|
||||
import { dbApi } from '../api';
|
||||
import SqlExecBox from '../component/SqlExecBox';
|
||||
import { dbApi } from '@/views/ops/db/api';
|
||||
import SqlExecBox from '../sqleditor/SqlExecBox';
|
||||
import config from '@/common/config';
|
||||
import { joinClientParams } from '@/common/request';
|
||||
import { isTrue } from '@/common/assert';
|
||||
|
||||
const DbTableEdit = defineAsyncComponent(() => import('./DbTableEdit.vue'));
|
||||
const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue'));
|
||||
|
||||
const props = defineProps({
|
||||
height: {
|
||||
type: [String],
|
||||
default: '65vh',
|
||||
},
|
||||
dbId: {
|
||||
type: [Number],
|
||||
required: true,
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import { dbApi } from './api';
|
||||
import { getTextWidth } from '@/common/utils/string';
|
||||
import SqlExecBox from './component/SqlExecBox';
|
||||
import SqlExecBox from './component/sqleditor/SqlExecBox';
|
||||
|
||||
import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/mysql/mysql.js';
|
||||
import { language as addSqlLanguage } from './lang/mysql.js';
|
||||
@@ -39,11 +39,11 @@ export class DbInst {
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* schema -> db
|
||||
* dbName -> db
|
||||
*/
|
||||
dbs: Map<string, Db> = new Map();
|
||||
|
||||
/** 数据库schema,多个用空格隔开 */
|
||||
/** 数据库,多个用空格隔开 */
|
||||
databases: string;
|
||||
|
||||
/**
|
||||
@@ -281,9 +281,9 @@ export class DbInst {
|
||||
if (this.type == 'mysql') {
|
||||
return `\`${name}\``;
|
||||
}
|
||||
if (this.type == 'postgres') {
|
||||
return `"${name}"`;
|
||||
}
|
||||
// if (this.type == 'postgres') {
|
||||
// return `"${name}"`;
|
||||
// }
|
||||
return name;
|
||||
};
|
||||
|
||||
@@ -451,11 +451,18 @@ export enum TabType {
|
||||
* 查询框
|
||||
*/
|
||||
Query,
|
||||
|
||||
/**
|
||||
* 表操作
|
||||
*/
|
||||
TablesOp,
|
||||
}
|
||||
|
||||
export class TabInfo {
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* tab唯一key。与label、name都一致
|
||||
* tab唯一key。与name都一致
|
||||
*/
|
||||
key: string;
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
|
||||
<template #action="{ data }">
|
||||
<span v-auth="'machine:terminal'">
|
||||
<el-tooltip :show-after="500" effect="customized" content="按住ctrl则为新标签打开" placement="top">
|
||||
<el-tooltip :show-after="500" content="按住ctrl则为新标签打开" placement="top">
|
||||
<el-button :disabled="data.status == -1" type="primary" @click="showTerminal(data, $event)" link>终端</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
|
||||
@@ -5,15 +5,19 @@
|
||||
<tag-tree :loadTags="loadTags">
|
||||
<template #prefix="{ data }">
|
||||
<span v-if="data.type.value == MongoNodeType.Mongo">
|
||||
<el-popover :show-after="500" placement="right-start" title="mongo实例信息" trigger="hover" :width="210">
|
||||
<el-popover :show-after="500" placement="right-start" title="mongo实例信息" trigger="hover" :width="250">
|
||||
<template #reference>
|
||||
<SvgIcon name="iconfont icon-op-mongo" :size="18" />
|
||||
</template>
|
||||
<template #default>
|
||||
<el-form class="instances-pop-form" label-width="auto" :size="'small'">
|
||||
<el-form-item label="名称:">{{ data.params.name }}</el-form-item>
|
||||
<el-form-item label="链接:">{{ data.params.uri }}</el-form-item>
|
||||
</el-form>
|
||||
<el-descriptions :column="1" size="small">
|
||||
<el-descriptions-item label="名称">
|
||||
{{ data.params.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="链接">
|
||||
{{ data.params.uri }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-popover>
|
||||
</span>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="testConn" :loading="state.testConnBtnLoading" type="success">测试连接</el-button>
|
||||
<el-button @click="cancel()">取 消</el-button>
|
||||
<el-button type="primary" :loading="btnLoading" @click="btnOk">确 定</el-button>
|
||||
</div>
|
||||
@@ -99,6 +100,7 @@ const state = reactive({
|
||||
tagPath: null as any,
|
||||
},
|
||||
btnLoading: false,
|
||||
testConnBtnLoading: false,
|
||||
});
|
||||
|
||||
const { dialogVisible, tabActiveName, form, btnLoading } = toRefs(state);
|
||||
@@ -116,15 +118,35 @@ watch(props, async (newValue: any) => {
|
||||
}
|
||||
});
|
||||
|
||||
const getReqForm = () => {
|
||||
const reqForm = { ...state.form };
|
||||
if (!state.form.sshTunnelMachineId || state.form.sshTunnelMachineId <= 0) {
|
||||
reqForm.sshTunnelMachineId = -1;
|
||||
}
|
||||
return reqForm;
|
||||
};
|
||||
|
||||
const testConn = async () => {
|
||||
mongoForm.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
state.testConnBtnLoading = true;
|
||||
try {
|
||||
await mongoApi.testConn.request(getReqForm());
|
||||
ElMessage.success('连接成功');
|
||||
} finally {
|
||||
state.testConnBtnLoading = false;
|
||||
}
|
||||
} else {
|
||||
ElMessage.error('请正确填写信息');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const btnOk = async () => {
|
||||
mongoForm.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
const reqForm = { ...state.form };
|
||||
if (!state.form.sshTunnelMachineId || state.form.sshTunnelMachineId <= 0) {
|
||||
reqForm.sshTunnelMachineId = -1;
|
||||
}
|
||||
// reqForm.uri = await RsaEncrypt(reqForm.uri);
|
||||
mongoApi.saveMongo.request(reqForm).then(() => {
|
||||
mongoApi.saveMongo.request(getReqForm).then(() => {
|
||||
ElMessage.success('保存成功');
|
||||
emit('val-change', state.form);
|
||||
state.btnLoading = true;
|
||||
|
||||
@@ -3,6 +3,7 @@ import Api from '@/common/Api';
|
||||
export const mongoApi = {
|
||||
mongoList: Api.newGet('/mongos'),
|
||||
mongoTags: Api.newGet('/mongos/tags'),
|
||||
testConn: Api.newPost('/mongos/test-conn'),
|
||||
saveMongo: Api.newPost('/mongos'),
|
||||
deleteMongo: Api.newDelete('/mongos/{id}'),
|
||||
databases: Api.newGet('/mongos/{id}/databases'),
|
||||
|
||||
@@ -7,17 +7,25 @@
|
||||
<tag-tree :loadTags="loadTags">
|
||||
<template #prefix="{ data }">
|
||||
<span v-if="data.type.value == RedisNodeType.Redis">
|
||||
<el-popover :show-after="500" placement="right-start" title="redis实例信息" trigger="hover" :width="210">
|
||||
<el-popover :show-after="500" placement="right-start" title="redis实例信息" trigger="hover" :width="250">
|
||||
<template #reference>
|
||||
<SvgIcon name="iconfont icon-op-redis" :size="18" />
|
||||
</template>
|
||||
<template #default>
|
||||
<el-form class="instances-pop-form" label-width="auto" :size="'small'">
|
||||
<el-form-item label="名称:">{{ data.params.name }}</el-form-item>
|
||||
<el-form-item label="模式:">{{ data.params.mode }}</el-form-item>
|
||||
<el-form-item label="链接:">{{ data.params.host }}</el-form-item>
|
||||
<el-form-item label="备注:">{{ data.params.remark }}</el-form-item>
|
||||
</el-form>
|
||||
<el-descriptions :column="1" size="small">
|
||||
<el-descriptions-item label="名称">
|
||||
{{ data.params.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="模式">
|
||||
{{ data.params.mode }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="host">
|
||||
{{ data.params.host }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" label-align="right">
|
||||
{{ data.params.remark }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-popover>
|
||||
</span>
|
||||
@@ -615,12 +623,6 @@ const delKey = (key: string) => {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.instances-pop-form {
|
||||
.el-form-item {
|
||||
margin-bottom: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.key-list-vtree {
|
||||
height: calc(100vh - 250px);
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="testConn" :loading="state.testConnBtnLoading" type="success">测试连接</el-button>
|
||||
<el-button @click="cancel()">取 消</el-button>
|
||||
<el-button type="primary" :loading="btnLoading" @click="btnOk">确 定</el-button>
|
||||
</div>
|
||||
@@ -161,6 +162,7 @@ const state = reactive({
|
||||
dbList: [0],
|
||||
pwd: '',
|
||||
btnLoading: false,
|
||||
testConnBtnLoading: false,
|
||||
});
|
||||
|
||||
const { dialogVisible, tabActiveName, form, dbList, pwd, btnLoading } = toRefs(state);
|
||||
@@ -195,19 +197,40 @@ const getPwd = async () => {
|
||||
state.pwd = await redisApi.getRedisPwd.request({ id: state.form.id });
|
||||
};
|
||||
|
||||
const getReqForm = async () => {
|
||||
const reqForm = { ...state.form };
|
||||
if (reqForm.mode == 'sentinel' && reqForm.host.split('=').length != 2) {
|
||||
ElMessage.error('sentinel模式host需为: mastername=sentinelhost:sentinelport模式');
|
||||
return;
|
||||
}
|
||||
if (!state.form.sshTunnelMachineId || state.form.sshTunnelMachineId <= 0) {
|
||||
reqForm.sshTunnelMachineId = -1;
|
||||
}
|
||||
reqForm.password = await RsaEncrypt(reqForm.password);
|
||||
return reqForm;
|
||||
};
|
||||
|
||||
const testConn = async () => {
|
||||
redisForm.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
state.testConnBtnLoading = true;
|
||||
try {
|
||||
await redisApi.testConn.request(await getReqForm());
|
||||
ElMessage.success('连接成功');
|
||||
} finally {
|
||||
state.testConnBtnLoading = false;
|
||||
}
|
||||
} else {
|
||||
ElMessage.error('请正确填写信息');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const btnOk = async () => {
|
||||
redisForm.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
const reqForm = { ...state.form };
|
||||
if (reqForm.mode == 'sentinel' && reqForm.host.split('=').length != 2) {
|
||||
ElMessage.error('sentinel模式host需为: mastername=sentinelhost:sentinelport模式');
|
||||
return;
|
||||
}
|
||||
if (!state.form.sshTunnelMachineId || state.form.sshTunnelMachineId <= 0) {
|
||||
reqForm.sshTunnelMachineId = -1;
|
||||
}
|
||||
reqForm.password = await RsaEncrypt(reqForm.password);
|
||||
redisApi.saveRedis.request(reqForm).then(() => {
|
||||
redisApi.saveRedis.request(await getReqForm()).then(() => {
|
||||
ElMessage.success('保存成功');
|
||||
emit('val-change', state.form);
|
||||
state.btnLoading = true;
|
||||
|
||||
@@ -6,6 +6,7 @@ export const redisApi = {
|
||||
getRedisPwd: Api.newGet('/redis/{id}/pwd'),
|
||||
redisInfo: Api.newGet('/redis/{id}/info'),
|
||||
clusterInfo: Api.newGet('/redis/{id}/cluster-info'),
|
||||
testConn: Api.newPost('/redis/test-conn'),
|
||||
saveRedis: Api.newPost('/redis'),
|
||||
delRedis: Api.newDelete('/redis/{id}'),
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@ server:
|
||||
# debug release test
|
||||
model: release
|
||||
port: 18888
|
||||
# 上下文路径, 若设置了该值, 则请求地址为ip:port/context-path
|
||||
# context-path: /mayfly
|
||||
cors: true
|
||||
tls:
|
||||
enable: false
|
||||
key-file: ./default.key
|
||||
cert-file: ./default.pem
|
||||
# 机器终端操作回放文件存储路径
|
||||
machine-rec-path: ./rec
|
||||
jwt:
|
||||
# jwt key,不设置默认使用随机字符串
|
||||
key:
|
||||
|
||||
@@ -34,7 +34,7 @@ func InitRouter() *gin.Engine {
|
||||
})
|
||||
|
||||
// 设置静态资源
|
||||
setStatic(router)
|
||||
setStatic(serverConfig.ContextPath, router)
|
||||
|
||||
// 是否允许跨域
|
||||
if serverConfig.Cors {
|
||||
@@ -42,7 +42,7 @@ func InitRouter() *gin.Engine {
|
||||
}
|
||||
|
||||
// 设置路由组
|
||||
api := router.Group("/api")
|
||||
api := router.Group(serverConfig.ContextPath + "/api")
|
||||
{
|
||||
common_router.Init(api)
|
||||
|
||||
@@ -61,16 +61,17 @@ func InitRouter() *gin.Engine {
|
||||
return router
|
||||
}
|
||||
|
||||
func setStatic(router *gin.Engine) {
|
||||
func setStatic(contextPath string, router *gin.Engine) {
|
||||
// 使用embed打包静态资源至二进制文件中
|
||||
fsys, _ := fs.Sub(static.Static, "static")
|
||||
fileServer := http.FileServer(http.FS(fsys))
|
||||
handler := WrapStaticHandler(fileServer)
|
||||
router.GET("/", handler)
|
||||
router.GET("/favicon.ico", handler)
|
||||
router.GET("/config.js", handler)
|
||||
handler := WrapStaticHandler(http.StripPrefix(contextPath, fileServer))
|
||||
|
||||
router.GET(contextPath+"/", handler)
|
||||
router.GET(contextPath+"/favicon.ico", handler)
|
||||
router.GET(contextPath+"/config.js", handler)
|
||||
// 所有/assets/**开头的都是静态资源文件
|
||||
router.GET("/assets/*file", handler)
|
||||
router.GET(contextPath+"/assets/*file", handler)
|
||||
|
||||
// 设置静态资源
|
||||
if staticConfs := config.Conf.Server.Static; staticConfs != nil {
|
||||
|
||||
@@ -445,6 +445,14 @@ func (d *Db) GetCreateTableDdl(rc *req.Ctx) {
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (d *Db) GetPgsqlSchemas(rc *req.Ctx) {
|
||||
conn := d.getDbConn(rc.GinCtx)
|
||||
biz.IsTrue(conn.Info.Type == dbm.DbTypePostgres, "非postgres无法获取该schemas")
|
||||
res, err := d.getDbConn(rc.GinCtx).GetMeta().(*dbm.PgsqlMetadata).GetSchemas()
|
||||
biz.ErrIsNilAppendErr(err, "获取schemas失败: %s")
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func getDbId(g *gin.Context) uint64 {
|
||||
dbId, _ := strconv.Atoi(g.Param("dbId"))
|
||||
biz.IsTrue(dbId > 0, "dbId错误")
|
||||
|
||||
@@ -29,6 +29,18 @@ func (d *Instance) Instances(rc *req.Ctx) {
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (d *Instance) TestConn(rc *req.Ctx) {
|
||||
form := &form.InstanceForm{}
|
||||
instance := ginx.BindJsonAndCopyTo[*entity.DbInstance](rc.GinCtx, form, new(entity.DbInstance))
|
||||
|
||||
// 密码解密,并使用解密后的赋值
|
||||
originPwd, err := cryptox.DefaultRsaDecrypt(form.Password, true)
|
||||
biz.ErrIsNilAppendErr(err, "解密密码错误: %s")
|
||||
instance.Password = originPwd
|
||||
|
||||
biz.ErrIsNil(d.InstanceApp.TestConn(instance))
|
||||
}
|
||||
|
||||
// SaveInstance 保存数据库实例信息
|
||||
// @router /api/instances [post]
|
||||
func (d *Instance) SaveInstance(rc *req.Ctx) {
|
||||
|
||||
@@ -118,14 +118,24 @@ func (d *dbAppImpl) GetDbConn(dbId uint64, dbName string) (*dbm.DbConn, error) {
|
||||
if err != nil {
|
||||
return nil, errorx.NewBiz("数据库信息不存在")
|
||||
}
|
||||
if !strings.Contains(" "+db.Database+" ", " "+dbName+" ") {
|
||||
return nil, errorx.NewBiz("未配置数据库【%s】的操作权限", dbName)
|
||||
}
|
||||
|
||||
instance, err := d.dbInstanceApp.GetById(new(entity.DbInstance), db.InstanceId)
|
||||
if err != nil {
|
||||
return nil, errorx.NewBiz("数据库实例不存在")
|
||||
}
|
||||
|
||||
checkDb := dbName
|
||||
// 兼容pgsql db/schema模式
|
||||
if instance.Type == dbm.DbTypePostgres {
|
||||
ss := strings.Split(dbName, "/")
|
||||
if len(ss) > 1 {
|
||||
checkDb = ss[0]
|
||||
}
|
||||
}
|
||||
if !strings.Contains(" "+db.Database+" ", " "+checkDb+" ") {
|
||||
return nil, errorx.NewBiz("未配置数据库【%s】的操作权限", dbName)
|
||||
}
|
||||
|
||||
// 密码解密
|
||||
instance.PwdDecrypt()
|
||||
return toDbInfo(instance, dbId, dbName, db.TagPath), nil
|
||||
|
||||
@@ -17,6 +17,8 @@ type Instance interface {
|
||||
|
||||
Count(condition *entity.InstanceQuery) int64
|
||||
|
||||
TestConn(instanceEntity *entity.DbInstance) error
|
||||
|
||||
Save(ctx context.Context, instanceEntity *entity.DbInstance) error
|
||||
|
||||
// Delete 删除数据库信息
|
||||
@@ -45,19 +47,19 @@ func (app *instanceAppImpl) Count(condition *entity.InstanceQuery) int64 {
|
||||
return app.CountByCond(condition)
|
||||
}
|
||||
|
||||
func (app *instanceAppImpl) TestConn(instanceEntity *entity.DbInstance) error {
|
||||
dbConn, err := toDbInfo(instanceEntity, 0, "", "").Conn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbConn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *instanceAppImpl) Save(ctx context.Context, instanceEntity *entity.DbInstance) error {
|
||||
// 默认tcp连接
|
||||
instanceEntity.Network = instanceEntity.GetNetwork()
|
||||
|
||||
// 测试连接
|
||||
if instanceEntity.Password != "" {
|
||||
dbConn, err := toDbInfo(instanceEntity, 0, "", "").Conn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dbConn.Close()
|
||||
}
|
||||
|
||||
// 查找是否存在该库
|
||||
oldInstance := &entity.DbInstance{Host: instanceEntity.Host, Port: instanceEntity.Port, Username: instanceEntity.Username}
|
||||
if instanceEntity.SshTunnelMachineId > 0 {
|
||||
|
||||
@@ -55,7 +55,7 @@ func (dbType DbType) StmtSelectDbName() string {
|
||||
case DbTypeMysql:
|
||||
return "SELECT SCHEMA_NAME AS dbname FROM SCHEMATA"
|
||||
case DbTypePostgres:
|
||||
return "SELECT datname AS dbname FROM pg_database"
|
||||
return "SELECT datname AS dbname FROM pg_database WHERE datistemplate = false AND has_database_privilege(datname, 'CONNECT')"
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid database type: %s", dbType))
|
||||
}
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
--PGSQL_DB_SCHEMAS 库schemas
|
||||
select
|
||||
n.nspname as "schemaName"
|
||||
from
|
||||
pg_namespace n
|
||||
where
|
||||
has_schema_privilege(n.nspname, 'USAGE')
|
||||
and n.nspname not like 'pg_%'
|
||||
and n.nspname != 'information_schema'
|
||||
---------------------------------------
|
||||
--PGSQL_TABLE_INFO 表详细信息
|
||||
select
|
||||
c.relname as "tableName",
|
||||
@@ -10,12 +20,10 @@ from
|
||||
join pg_namespace n on
|
||||
c.relnamespace = n.oid
|
||||
join pg_stat_user_tables psut on
|
||||
psut.relid = c."oid"
|
||||
psut.relid = c.oid
|
||||
where
|
||||
n.nspname = (
|
||||
select
|
||||
current_schema ()
|
||||
)
|
||||
has_table_privilege(CAST(c.oid AS regclass), 'SELECT')
|
||||
and n.nspname = current_schema()
|
||||
and c.reltype > 0
|
||||
---------------------------------------
|
||||
--PGSQL_INDEX_INFO 表索引信息
|
||||
|
||||
@@ -29,13 +29,38 @@ func getPgsqlDB(d *DbInfo) (*sql.DB, error) {
|
||||
|
||||
db := d.Database
|
||||
var dbParam string
|
||||
exsitSchema := false
|
||||
if db != "" {
|
||||
dbParam = "dbname=" + db
|
||||
// postgres database可以使用db/schema表示,方便连接指定schema, 若不存在schema则使用默认schema
|
||||
ss := strings.Split(db, "/")
|
||||
if len(ss) > 1 {
|
||||
exsitSchema = true
|
||||
dbParam = fmt.Sprintf("dbname=%s search_path=%s", ss[0], ss[len(ss)-1])
|
||||
} else {
|
||||
dbParam = "dbname=" + db
|
||||
}
|
||||
}
|
||||
|
||||
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s %s sslmode=disable", d.Host, d.Port, d.Username, d.Password, dbParam)
|
||||
// 存在额外指定参数,则拼接该连接参数
|
||||
if d.Params != "" {
|
||||
// 存在指定的db,则需要将dbInstance配置中的parmas排除掉dbname和search_path
|
||||
if db != "" {
|
||||
paramArr := strings.Split(d.Params, "&")
|
||||
paramArr = collx.ArrayRemoveFunc(paramArr, func(param string) bool {
|
||||
if strings.HasPrefix(param, "dbname=") {
|
||||
return true
|
||||
}
|
||||
if exsitSchema && strings.HasPrefix(param, "search_path") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
d.Params = strings.Join(paramArr, " ")
|
||||
}
|
||||
dsn = fmt.Sprintf("%s %s", dsn, strings.Join(strings.Split(d.Params, "&"), " "))
|
||||
}
|
||||
|
||||
return sql.Open(driverName, dsn)
|
||||
}
|
||||
|
||||
@@ -67,6 +92,7 @@ func (pd *PqSqlDialer) DialTimeout(network, address string, timeout time.Duratio
|
||||
// ---------------------------------- pgsql元数据 -----------------------------------
|
||||
const (
|
||||
PGSQL_META_FILE = "metasql/pgsql_meta.sql"
|
||||
PGSQL_DB_SCHEMAS = "PGSQL_DB_SCHEMAS"
|
||||
PGSQL_TABLE_INFO_KEY = "PGSQL_TABLE_INFO"
|
||||
PGSQL_INDEX_INFO_KEY = "PGSQL_INDEX_INFO"
|
||||
PGSQL_COLUMN_MA_KEY = "PGSQL_COLUMN_MA"
|
||||
@@ -192,3 +218,17 @@ func (pm *PgsqlMetadata) GetTableRecord(tableName string, pageNum, pageSize int)
|
||||
func (pm *PgsqlMetadata) WalkTableRecord(tableName string, walk func(record map[string]any, columns []string)) error {
|
||||
return pm.dc.WalkTableRecord(fmt.Sprintf("SELECT * FROM %s", tableName), walk)
|
||||
}
|
||||
|
||||
// 获取pgsql当前连接的库可访问的schemaNames
|
||||
func (pm *PgsqlMetadata) GetSchemas() ([]string, error) {
|
||||
sql := GetLocalSql(PGSQL_META_FILE, PGSQL_DB_SCHEMAS)
|
||||
_, res, err := pm.dc.SelectData(sql)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
schemaNames := make([]string, 0)
|
||||
for _, re := range res {
|
||||
schemaNames = append(schemaNames, anyx.ConvString(re["schemaName"]))
|
||||
}
|
||||
return schemaNames, nil
|
||||
}
|
||||
|
||||
@@ -18,6 +18,11 @@ func newDbSqlExecRepo() repository.DbSqlExec {
|
||||
|
||||
// 分页获取
|
||||
func (d *dbSqlExecRepoImpl) GetPageList(condition *entity.DbSqlExecQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
qd := gormx.NewQuery(new(entity.DbSqlExec)).WithCondModel(condition).WithOrderBy(orderBy...)
|
||||
qd := gormx.NewQuery(new(entity.DbSqlExec)).
|
||||
Eq("db_id", condition.DbId).
|
||||
Eq("`table`", condition.Table).
|
||||
Eq("type", condition.Type).
|
||||
Eq("creator_id", condition.CreatorId).
|
||||
RLike("db", condition.Db).WithOrderBy(orderBy...)
|
||||
return gormx.PageQuery(qd, pageParam, toEntity)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ func InitDbRouter(router *gin.RouterGroup) {
|
||||
|
||||
req.NewGet(":dbId/t-create-ddl", d.GetCreateTableDdl),
|
||||
|
||||
req.NewGet(":dbId/pg/schemas", d.GetPgsqlSchemas),
|
||||
|
||||
req.NewPost(":dbId/exec-sql", d.ExecSql).Log(req.NewLog("db-执行Sql")),
|
||||
|
||||
req.NewPost(":dbId/exec-sql-file", d.ExecSqlFile).Log(req.NewLogSave("db-执行Sql文件")),
|
||||
|
||||
@@ -20,6 +20,8 @@ func InitInstanceRouter(router *gin.RouterGroup) {
|
||||
// 获取数据库列表
|
||||
req.NewGet("", d.Instances),
|
||||
|
||||
req.NewPost("/test-conn", d.TestConn),
|
||||
|
||||
req.NewPost("", d.SaveInstance).Log(req.NewLogSave("db-保存数据库实例信息")),
|
||||
|
||||
req.NewGet(":instanceId", d.GetInstance),
|
||||
|
||||
@@ -6,11 +6,11 @@ import (
|
||||
"mayfly-go/internal/machine/api/form"
|
||||
"mayfly-go/internal/machine/api/vo"
|
||||
"mayfly-go/internal/machine/application"
|
||||
"mayfly-go/internal/machine/config"
|
||||
"mayfly-go/internal/machine/domain/entity"
|
||||
"mayfly-go/internal/machine/mcm"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/config"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/ginx"
|
||||
"mayfly-go/pkg/model"
|
||||
@@ -189,7 +189,7 @@ func (m *Machine) WsSSH(g *gin.Context) {
|
||||
if cli.Info.EnableRecorder == 1 {
|
||||
now := time.Now()
|
||||
// 回放文件路径为: 基础配置路径/机器id/操作日期/操作者账号/操作时间.cast
|
||||
recPath := fmt.Sprintf("%s/%d/%s/%s", config.Conf.Server.GetMachineRecPath(), cli.Info.Id, now.Format("20060102"), rc.GetLoginAccount().Username)
|
||||
recPath := fmt.Sprintf("%s/%d/%s/%s", config.GetMachine().TerminalRecPath, cli.Info.Id, now.Format("20060102"), rc.GetLoginAccount().Username)
|
||||
os.MkdirAll(recPath, 0766)
|
||||
fileName := path.Join(recPath, fmt.Sprintf("%s.cast", now.Format("20060102_150405")))
|
||||
f, err := os.OpenFile(fileName, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0766)
|
||||
@@ -214,7 +214,7 @@ func (m *Machine) WsSSH(g *gin.Context) {
|
||||
func (m *Machine) MachineRecDirNames(rc *req.Ctx) {
|
||||
readPath := rc.GinCtx.Query("path")
|
||||
biz.NotEmpty(readPath, "path不能为空")
|
||||
path_ := path.Join(config.Conf.Server.GetMachineRecPath(), readPath)
|
||||
path_ := path.Join(config.GetMachine().TerminalRecPath, readPath)
|
||||
|
||||
// 如果是读取文件内容,则读取对应回放记录文件内容,否则读取文件夹名列表。小小偷懒一会不想再加个接口
|
||||
isFile := rc.GinCtx.Query("isFile")
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"mayfly-go/internal/machine/api/form"
|
||||
"mayfly-go/internal/machine/api/vo"
|
||||
"mayfly-go/internal/machine/application"
|
||||
"mayfly-go/internal/machine/config"
|
||||
"mayfly-go/internal/machine/domain/entity"
|
||||
"mayfly-go/internal/machine/mcm"
|
||||
msgapp "mayfly-go/internal/msg/application"
|
||||
@@ -175,8 +176,6 @@ func (m *MachineFile) WriteFileContent(rc *req.Ctx) {
|
||||
biz.ErrIsNilAppendErr(err, "打开文件失败: %s")
|
||||
}
|
||||
|
||||
const MaxUploadFileSize int64 = 1024 * 1024 * 1024
|
||||
|
||||
func (m *MachineFile) UploadFile(rc *req.Ctx) {
|
||||
g := rc.GinCtx
|
||||
fid := GetMachineFileId(g)
|
||||
@@ -184,7 +183,9 @@ func (m *MachineFile) UploadFile(rc *req.Ctx) {
|
||||
|
||||
fileheader, err := g.FormFile("file")
|
||||
biz.ErrIsNilAppendErr(err, "读取文件失败: %s")
|
||||
biz.IsTrue(fileheader.Size <= MaxUploadFileSize, "文件大小不能超过%d字节", MaxUploadFileSize)
|
||||
|
||||
maxUploadFileSize := config.GetMachine().UploadMaxFileSize
|
||||
biz.IsTrue(fileheader.Size <= maxUploadFileSize, "文件大小不能超过%d字节", maxUploadFileSize)
|
||||
|
||||
file, _ := fileheader.Open()
|
||||
defer file.Close()
|
||||
@@ -223,7 +224,9 @@ func (m *MachineFile) UploadFolder(rc *req.Ctx) {
|
||||
allFileSize := collx.ArrayReduce(fileheaders, 0, func(i int64, fh *multipart.FileHeader) int64 {
|
||||
return i + fh.Size
|
||||
})
|
||||
biz.IsTrue(allFileSize <= MaxUploadFileSize, "文件夹总大小不能超过%d字节", MaxUploadFileSize)
|
||||
|
||||
maxUploadFileSize := config.GetMachine().UploadMaxFileSize
|
||||
biz.IsTrue(allFileSize <= maxUploadFileSize, "文件夹总大小不能超过%d字节", maxUploadFileSize)
|
||||
|
||||
paths := mf.Value["paths"]
|
||||
|
||||
|
||||
43
server/internal/machine/config/config.go
Normal file
43
server/internal/machine/config/config.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
sysapp "mayfly-go/internal/sys/application"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/utils/bytex"
|
||||
)
|
||||
|
||||
const (
|
||||
ConfigKeyMachine string = "MachineConfig" // 机器相关配置
|
||||
)
|
||||
|
||||
type Machine struct {
|
||||
TerminalRecPath string // 终端操作记录存储位置
|
||||
UploadMaxFileSize int64 // 允许上传的最大文件size
|
||||
}
|
||||
|
||||
// 获取机器相关配置
|
||||
func GetMachine() *Machine {
|
||||
c := sysapp.GetConfigApp().GetConfig(ConfigKeyMachine)
|
||||
jm := c.GetJsonMap()
|
||||
|
||||
mc := new(Machine)
|
||||
|
||||
terminalRecPath := jm["terminalRecPath"]
|
||||
if terminalRecPath == "" {
|
||||
terminalRecPath = "./rec"
|
||||
}
|
||||
mc.TerminalRecPath = terminalRecPath
|
||||
|
||||
// 将1GB等字符串转为int64的byte
|
||||
uploadMaxFileSizeStr := jm["uploadMaxFileSize"]
|
||||
var uploadMaxFileSize int64 = 1 * bytex.GB
|
||||
if uploadMaxFileSizeStr != "" {
|
||||
var err error
|
||||
uploadMaxFileSize, err = bytex.ParseSize(uploadMaxFileSizeStr)
|
||||
if err != nil {
|
||||
logx.Errorf("解析机器配置的最大上传文件大小失败: uploadMaxFileSize=%s, 使用系统默认值1GB", uploadMaxFileSizeStr)
|
||||
}
|
||||
}
|
||||
mc.UploadMaxFileSize = uploadMaxFileSize
|
||||
return mc
|
||||
}
|
||||
@@ -46,6 +46,12 @@ func (m *Mongo) MongoTags(rc *req.Ctx) {
|
||||
rc.ResData = m.TagApp.ListTagByAccountIdAndResource(rc.GetLoginAccount().Id, new(entity.Mongo))
|
||||
}
|
||||
|
||||
func (m *Mongo) TestConn(rc *req.Ctx) {
|
||||
form := &form.Mongo{}
|
||||
mongo := ginx.BindJsonAndCopyTo[*entity.Mongo](rc.GinCtx, form, new(entity.Mongo))
|
||||
biz.ErrIsNilAppendErr(m.MongoApp.TestConn(mongo), "连接失败: %s")
|
||||
}
|
||||
|
||||
func (m *Mongo) Save(rc *req.Ctx) {
|
||||
form := &form.Mongo{}
|
||||
mongo := ginx.BindJsonAndCopyTo[*entity.Mongo](rc.GinCtx, form, new(entity.Mongo))
|
||||
|
||||
@@ -18,6 +18,8 @@ type Mongo interface {
|
||||
|
||||
Count(condition *entity.MongoQuery) int64
|
||||
|
||||
TestConn(entity *entity.Mongo) error
|
||||
|
||||
Save(ctx context.Context, entity *entity.Mongo) error
|
||||
|
||||
// 删除数据库信息
|
||||
@@ -52,6 +54,15 @@ func (d *mongoAppImpl) Delete(ctx context.Context, id uint64) error {
|
||||
return d.GetRepo().DeleteById(ctx, id)
|
||||
}
|
||||
|
||||
func (d *mongoAppImpl) TestConn(me *entity.Mongo) error {
|
||||
conn, err := me.ToMongoInfo().Conn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *mongoAppImpl) Save(ctx context.Context, m *entity.Mongo) error {
|
||||
if m.Id == 0 {
|
||||
return d.GetRepo().Insert(ctx, m)
|
||||
|
||||
@@ -26,7 +26,7 @@ type MongoInfo struct {
|
||||
}
|
||||
|
||||
func (mi *MongoInfo) Conn() (*MongoConn, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
|
||||
defer cancel()
|
||||
|
||||
mongoOptions := options.Client().ApplyURI(mi.Uri).
|
||||
@@ -40,7 +40,7 @@ func (mi *MongoInfo) Conn() (*MongoConn, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = client.Ping(context.TODO(), nil); err != nil {
|
||||
if err = client.Ping(ctx, nil); err != nil {
|
||||
client.Disconnect(ctx)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ func InitMongoRouter(router *gin.RouterGroup) {
|
||||
|
||||
req.NewGet("/tags", ma.MongoTags),
|
||||
|
||||
req.NewPost("/test-conn", ma.TestConn),
|
||||
|
||||
req.NewPost("", ma.Save).Log(req.NewLogSave("mongo-保存信息")),
|
||||
|
||||
req.NewDelete(":id", ma.DeleteMongo).Log(req.NewLogSave("mongo-删除信息")),
|
||||
|
||||
@@ -47,6 +47,18 @@ func (r *Redis) RedisTags(rc *req.Ctx) {
|
||||
rc.ResData = r.TagApp.ListTagByAccountIdAndResource(rc.GetLoginAccount().Id, new(entity.Redis))
|
||||
}
|
||||
|
||||
func (r *Redis) TestConn(rc *req.Ctx) {
|
||||
form := &form.Redis{}
|
||||
redis := ginx.BindJsonAndCopyTo[*entity.Redis](rc.GinCtx, form, new(entity.Redis))
|
||||
|
||||
// 密码解密,并使用解密后的赋值
|
||||
originPwd, err := cryptox.DefaultRsaDecrypt(redis.Password, true)
|
||||
biz.ErrIsNilAppendErr(err, "解密密码错误: %s")
|
||||
redis.Password = originPwd
|
||||
|
||||
biz.ErrIsNil(r.RedisApp.TestConn(redis))
|
||||
}
|
||||
|
||||
func (r *Redis) Save(rc *req.Ctx) {
|
||||
form := &form.Redis{}
|
||||
redis := ginx.BindJsonAndCopyTo[*entity.Redis](rc.GinCtx, form, new(entity.Redis))
|
||||
|
||||
@@ -20,6 +20,9 @@ type Redis interface {
|
||||
|
||||
Count(condition *entity.RedisQuery) int64
|
||||
|
||||
// 测试连接
|
||||
TestConn(re *entity.Redis) error
|
||||
|
||||
Save(ctx context.Context, re *entity.Redis) error
|
||||
|
||||
// 删除数据库信息
|
||||
@@ -29,9 +32,6 @@ type Redis interface {
|
||||
// id: 数据库实例id
|
||||
// db: 库号
|
||||
GetRedisConn(id uint64, db int) (*rdm.RedisConn, error)
|
||||
|
||||
// 测试连接
|
||||
TestConn(re *entity.Redis) error
|
||||
}
|
||||
|
||||
func newRedisApp(redisRepo repository.Redis) Redis {
|
||||
@@ -53,14 +53,21 @@ func (r *redisAppImpl) Count(condition *entity.RedisQuery) int64 {
|
||||
return r.GetRepo().Count(condition)
|
||||
}
|
||||
|
||||
func (r *redisAppImpl) Save(ctx context.Context, re *entity.Redis) error {
|
||||
// ’修改信息且密码不为空‘ or ‘新增’需要测试是否可连接
|
||||
if (re.Id != 0 && re.Password != "") || re.Id == 0 {
|
||||
if err := r.TestConn(re); err != nil {
|
||||
return errorx.NewBiz("Redis连接失败: %s", err.Error())
|
||||
}
|
||||
func (r *redisAppImpl) TestConn(re *entity.Redis) error {
|
||||
db := 0
|
||||
if re.Db != "" {
|
||||
db, _ = strconv.Atoi(strings.Split(re.Db, ",")[0])
|
||||
}
|
||||
|
||||
rc, err := re.ToRedisInfo(db).Conn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rc.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *redisAppImpl) Save(ctx context.Context, re *entity.Redis) error {
|
||||
// 查找是否存在该库
|
||||
oldRedis := &entity.Redis{Host: re.Host}
|
||||
if re.SshTunnelMachineId > 0 {
|
||||
@@ -118,17 +125,3 @@ func (r *redisAppImpl) GetRedisConn(id uint64, db int) (*rdm.RedisConn, error) {
|
||||
return re.ToRedisInfo(db), nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *redisAppImpl) TestConn(re *entity.Redis) error {
|
||||
db := 0
|
||||
if re.Db != "" {
|
||||
db, _ = strconv.Atoi(strings.Split(re.Db, ",")[0])
|
||||
}
|
||||
|
||||
rc, err := re.ToRedisInfo(db).Conn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rc.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ func InitRedisRouter(router *gin.RouterGroup) {
|
||||
|
||||
req.NewGet("/tags", rs.RedisTags),
|
||||
|
||||
req.NewPost("/test-conn", rs.TestConn),
|
||||
|
||||
req.NewPost("", rs.Save).Log(req.NewLogSave("redis-保存信息")),
|
||||
|
||||
req.NewGet(":id/pwd", rs.GetRedisPwd),
|
||||
|
||||
@@ -5,13 +5,13 @@ import (
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Port int `yaml:"port"`
|
||||
Model string `yaml:"model"`
|
||||
Cors bool `yaml:"cors"`
|
||||
Tls *Tls `yaml:"tls"`
|
||||
Static *[]*Static `yaml:"static"`
|
||||
StaticFile *[]*StaticFile `yaml:"static-file"`
|
||||
MachineRecPath string `yaml:"machine-rec-path"` // 机器终端操作回放文件存储路径
|
||||
Port int `yaml:"port"`
|
||||
Model string `yaml:"model"`
|
||||
ContextPath string `yaml:"context-path"` // 请求路径上下文
|
||||
Cors bool `yaml:"cors"`
|
||||
Tls *Tls `yaml:"tls"`
|
||||
Static *[]*Static `yaml:"static"`
|
||||
StaticFile *[]*StaticFile `yaml:"static-file"`
|
||||
}
|
||||
|
||||
func (s *Server) Default() {
|
||||
@@ -21,24 +21,12 @@ func (s *Server) Default() {
|
||||
if s.Port == 0 {
|
||||
s.Port = 8888
|
||||
}
|
||||
if s.MachineRecPath == "" {
|
||||
s.MachineRecPath = "./rec"
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) GetPort() string {
|
||||
return fmt.Sprintf(":%d", s.Port)
|
||||
}
|
||||
|
||||
// 获取终端回访记录存放基础路径, 如果配置文件未配置,则默认为./rec
|
||||
func (s *Server) GetMachineRecPath() string {
|
||||
path := s.MachineRecPath
|
||||
if path == "" {
|
||||
return "./rec"
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
type Static struct {
|
||||
RelativePath string `yaml:"relative-path"`
|
||||
Root string `yaml:"root"`
|
||||
|
||||
@@ -28,7 +28,7 @@ func runWebServer() {
|
||||
|
||||
server := config.Conf.Server
|
||||
port := server.GetPort()
|
||||
logx.Infof("Listening and serving HTTP on %s", port)
|
||||
logx.Infof("Listening and serving HTTP on %s", port+server.ContextPath)
|
||||
|
||||
var err error
|
||||
if server.Tls != nil && server.Tls.Enable {
|
||||
|
||||
43
server/pkg/utils/bytex/bytex.go
Normal file
43
server/pkg/utils/bytex/bytex.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package bytex
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
KB = 1024
|
||||
MB = KB * 1024
|
||||
GB = MB * 1024
|
||||
)
|
||||
|
||||
// 解析字符串byte size
|
||||
//
|
||||
// 1kb -> 1024
|
||||
// 1mb -> 1024 * 1024
|
||||
func ParseSize(sizeStr string) (int64, error) {
|
||||
sizeStr = strings.TrimSpace(sizeStr)
|
||||
unit := sizeStr[len(sizeStr)-2:]
|
||||
|
||||
valueStr := sizeStr[:len(sizeStr)-2]
|
||||
value, err := strconv.ParseInt(valueStr, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var bytes int64
|
||||
|
||||
switch strings.ToUpper(unit) {
|
||||
case "KB":
|
||||
bytes = value * KB
|
||||
case "MB":
|
||||
bytes = value * MB
|
||||
case "GB":
|
||||
bytes = value * GB
|
||||
default:
|
||||
return 0, fmt.Errorf("invalid size unit")
|
||||
}
|
||||
|
||||
return bytes, nil
|
||||
}
|
||||
11
server/pkg/utils/bytex/bytex_test.go
Normal file
11
server/pkg/utils/bytex/bytex_test.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package bytex
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseSize(t *testing.T) {
|
||||
res, _ := ParseSize("1MB")
|
||||
fmt.Println(res)
|
||||
}
|
||||
Binary file not shown.
@@ -469,6 +469,7 @@ INSERT INTO `t_sys_config` (name, `key`, params, value, remark, permission, crea
|
||||
INSERT INTO `t_sys_config` (name, `key`, params, value, remark, permission, create_time, creator_id, creator, update_time, modifier_id, modifier, is_deleted, delete_time) VALUES('是否启用水印', 'UseWatermark', '[{"name":"是否启用","model":"isUse","placeholder":"是否启用水印","options":"true,false"},{"name":"自定义信息","model":"content","placeholder":"额外添加的水印内容,可添加公司名称等"}]', '', '水印信息配置', 'all', '2022-08-25 23:36:35', 1, 'admin', '2022-08-26 10:02:52', 1, 'admin', 0, NULL);
|
||||
INSERT INTO `t_sys_config` (name, `key`, params, value, remark, create_time, creator_id, creator, update_time, modifier_id, modifier)VALUES ('数据库查询最大结果集', 'DbQueryMaxCount', '[]', '200', '允许sql查询的最大结果集数。注: 0=不限制', '2023-02-11 14:29:03', 1, 'admin', '2023-02-11 14:40:56', 1, 'admin');
|
||||
INSERT INTO `t_sys_config` (name, `key`, params, value, remark, create_time, creator_id, creator, update_time, modifier_id, modifier)VALUES ('数据库是否记录查询SQL', 'DbSaveQuerySQL', '[]', '0', '1: 记录、0:不记录', '2023-02-11 16:07:14', 1, 'admin', '2023-02-11 16:44:17', 1, 'admin');
|
||||
INSERT INTO `t_sys_config` (name, `key`, params, value, remark, permission, create_time, creator_id, creator, update_time, modifier_id, modifier, is_deleted, delete_time) VALUES('机器相关配置', 'MachineConfig', '[{"name":"终端回放存储路径","model":"terminalRecPath","placeholder":"终端回放存储路径"},{"name":"uploadMaxFileSize","model":"uploadMaxFileSize","placeholder":"允许上传的最大文件大小(1MB\\\\2GB等)"}]', '{"terminalRecPath":"./rec","uploadMaxFileSize":"1GB"}', '机器相关配置,如终端回放路径等', 'admin,', '2023-07-13 16:26:44', 1, 'admin', '2023-11-09 22:01:31', 1, 'admin', 0, NULL);
|
||||
COMMIT;
|
||||
|
||||
-- ----------------------------
|
||||
|
||||
2
server/resources/script/sql/v1.5.4.sql
Normal file
2
server/resources/script/sql/v1.5.4.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- 新增机器相关系统配置
|
||||
INSERT INTO `t_sys_config` (name, `key`, params, value, remark, permission, create_time, creator_id, creator, update_time, modifier_id, modifier, is_deleted, delete_time) VALUES('机器相关配置', 'MachineConfig', '[{"name":"终端回放存储路径","model":"terminalRecPath","placeholder":"终端回放存储路径"},{"name":"uploadMaxFileSize","model":"uploadMaxFileSize","placeholder":"允许上传的最大文件大小(1MB\\\\2GB等)"}]', '{"terminalRecPath":"./rec","uploadMaxFileSize":"1GB"}', '机器相关配置,如终端回放路径等', 'admin,', '2023-07-13 16:26:44', 1, 'admin', '2023-11-09 22:01:31', 1, 'admin', 0, NULL);
|
||||
Reference in New Issue
Block a user