mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 23:40:24 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2deb3109c2 | ||
|
|
a80221a950 | ||
|
|
10630847df | ||
|
|
f43851698e | ||
|
|
73884bb693 | ||
|
|
1b5bb1de8b | ||
|
|
4814793546 | ||
|
|
d85bbff270 | ||
|
|
bb1522f4dc | ||
|
|
a7632fbf58 |
@@ -22,7 +22,7 @@
|
||||
|
||||
### 介绍
|
||||
|
||||
web 版 **linux(终端[终端回放、命令过滤] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle sqlserver 达梦 高斯 sqlite)数据同步 数据迁移、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台**
|
||||
web 版 **linux(终端[终端回放、命令过滤] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle sqlserver 达梦 高斯 sqlite)数据操作 数据同步 数据迁移、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台**
|
||||
|
||||
### 开发语言与主要框架
|
||||
|
||||
|
||||
@@ -10,20 +10,20 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"asciinema-player": "^3.7.1",
|
||||
"@vueuse/core": "^10.11.0",
|
||||
"asciinema-player": "^3.8.0",
|
||||
"axios": "^1.6.2",
|
||||
"clipboard": "^2.0.11",
|
||||
"cropperjs": "^1.6.1",
|
||||
"dayjs": "^1.11.11",
|
||||
"echarts": "^5.5.0",
|
||||
"element-plus": "^2.7.3",
|
||||
"echarts": "^5.5.1",
|
||||
"element-plus": "^2.7.7",
|
||||
"js-base64": "^3.7.7",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
"monaco-editor": "^0.48.0",
|
||||
"monaco-sql-languages": "^0.11.0",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"monaco-sql-languages": "^0.12.2",
|
||||
"monaco-themes": "^0.4.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.1.7",
|
||||
@@ -34,8 +34,8 @@
|
||||
"sql-formatter": "^15.0.2",
|
||||
"trzsz": "^1.1.5",
|
||||
"uuid": "^9.0.1",
|
||||
"vue": "^3.4.27",
|
||||
"vue-router": "^4.3.2",
|
||||
"vue": "^3.4.32",
|
||||
"vue-router": "^4.4.0",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"xterm-addon-search": "^0.13.0",
|
||||
@@ -48,16 +48,16 @@
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
"@typescript-eslint/parser": "^6.7.4",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/compiler-sfc": "^3.4.27",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"@vue/compiler-sfc": "^3.4.32",
|
||||
"code-inspector-plugin": "^0.4.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-plugin-vue": "^9.25.0",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.77.1",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.11",
|
||||
"sass": "^1.77.8",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.3.4",
|
||||
"vue-eslint-parser": "^9.4.2"
|
||||
},
|
||||
"browserslist": [
|
||||
|
||||
@@ -15,7 +15,7 @@ const config = {
|
||||
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
||||
|
||||
// 系统版本
|
||||
version: 'v1.8.5',
|
||||
version: 'v1.8.8',
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -9,7 +9,19 @@ export function getValueByPath(obj: any, path: string) {
|
||||
const keys = path.split('.');
|
||||
let result = obj;
|
||||
for (let key of keys) {
|
||||
if (!result || typeof result !== 'object') {
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
// 如果是字符串,则尝试使用json解析
|
||||
if (typeof result == 'string') {
|
||||
try {
|
||||
result = JSON.parse(result);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
if (typeof result !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -23,7 +35,18 @@ export function getValueByPath(obj: any, path: string) {
|
||||
}
|
||||
|
||||
const index = parseInt(matchIndex[1]);
|
||||
result = Array.isArray(result[arrayKey]) ? result[arrayKey][index] : undefined;
|
||||
|
||||
let arrValue = result[arrayKey];
|
||||
if (typeof arrValue == 'string') {
|
||||
try {
|
||||
arrValue = JSON.parse(arrValue);
|
||||
} catch (e) {
|
||||
result = undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
result = Array.isArray(arrValue) ? arrValue[index] : undefined;
|
||||
} else {
|
||||
result = result[key];
|
||||
}
|
||||
|
||||
@@ -97,43 +97,6 @@ export function getTextWidth(str: string) {
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内容所需要占用的宽度
|
||||
*/
|
||||
export function getContentWidth(content: any): number {
|
||||
if (!content) {
|
||||
return 50;
|
||||
}
|
||||
// 以下分配的单位长度可根据实际需求进行调整
|
||||
let flexWidth = 0;
|
||||
for (const char of content) {
|
||||
if (flexWidth > 500) {
|
||||
break;
|
||||
}
|
||||
if ((char >= '0' && char <= '9') || (char >= 'a' && char <= 'z')) {
|
||||
// 小写字母、数字字符
|
||||
flexWidth += 9.3;
|
||||
continue;
|
||||
}
|
||||
if (char >= 'A' && char <= 'Z') {
|
||||
flexWidth += 9;
|
||||
continue;
|
||||
}
|
||||
if (char >= '\u4e00' && char <= '\u9fa5') {
|
||||
// 如果是中文字符,为字符分配16个单位宽度
|
||||
flexWidth += 20;
|
||||
} else {
|
||||
// 其他种类字符
|
||||
flexWidth += 8;
|
||||
}
|
||||
}
|
||||
// if (flexWidth > 450) {
|
||||
// // 设置最大宽度
|
||||
// flexWidth = 450;
|
||||
// }
|
||||
return flexWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns uuid
|
||||
@@ -179,3 +142,38 @@ export async function copyToClipboard(txt: string, selector: string = '#copyValu
|
||||
clipboard.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
export function fuzzyMatchField(keyword: string, fields: any[], ...valueExtractFuncs: Function[]) {
|
||||
keyword = keyword?.toLowerCase();
|
||||
return fields.filter((field) => {
|
||||
for (let valueExtractFunc of valueExtractFuncs) {
|
||||
const value = valueExtractFunc(field)?.toLowerCase();
|
||||
if (isPrefixSubsequence(keyword, value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配是否为前缀子序列 targetTemplate=username prefix=uname -> true,prefix=uname2 -> false
|
||||
* @param prefix 字符串前缀(不连续也可以,但不改变字符的相对顺序)
|
||||
* @param targetTemplate 目标模板
|
||||
* @returns 是否匹配
|
||||
*/
|
||||
export function isPrefixSubsequence(prefix: string, targetTemplate: string) {
|
||||
let i = 0; // 指向prefix的索引
|
||||
let j = 0; // 指向targetTemplate的索引
|
||||
|
||||
while (i < prefix.length && j < targetTemplate.length) {
|
||||
if (prefix[i] === targetTemplate[j]) {
|
||||
// 字符匹配,两个指针都向前移动
|
||||
i++;
|
||||
}
|
||||
j++; // 目标字符串指针始终向前移动
|
||||
}
|
||||
|
||||
// 如果prefix的所有字符都被找到,返回true
|
||||
return i === prefix.length;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ const state = reactive({
|
||||
search: null as any,
|
||||
weblinks: null as any,
|
||||
},
|
||||
status: TerminalStatus.NoConnected,
|
||||
status: -11,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
@@ -96,6 +96,7 @@ onBeforeUnmount(() => {
|
||||
});
|
||||
|
||||
function init() {
|
||||
state.status = TerminalStatus.NoConnected;
|
||||
if (term) {
|
||||
console.log('重新连接...');
|
||||
close();
|
||||
@@ -105,7 +106,7 @@ function init() {
|
||||
});
|
||||
}
|
||||
|
||||
function initTerm() {
|
||||
async function initTerm() {
|
||||
term = new Terminal({
|
||||
fontSize: themeConfig.value.terminalFontSize || 15,
|
||||
fontWeight: themeConfig.value.terminalFontWeight || 'normal',
|
||||
@@ -155,6 +156,7 @@ function initSocket() {
|
||||
state.status = TerminalStatus.Connected;
|
||||
|
||||
focus();
|
||||
fitTerminal();
|
||||
|
||||
// 如果有初始要执行的命令,则发送执行命令
|
||||
if (props.cmd) {
|
||||
@@ -209,7 +211,6 @@ function loadAddon() {
|
||||
// tell trzsz the terminal columns has been changed
|
||||
trzsz.setTerminalColumns(size.cols);
|
||||
});
|
||||
window.addEventListener('resize', () => state.addon.fit.fit());
|
||||
// enable drag files or directories to upload
|
||||
terminalRef.value.addEventListener('dragover', (event: Event) => event.preventDefault());
|
||||
terminalRef.value.addEventListener('drop', (event: any) => {
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import EnumValue from '@/common/Enum';
|
||||
|
||||
export enum TerminalStatus {
|
||||
Error = -1,
|
||||
NoConnected = 0,
|
||||
Connected = 1,
|
||||
Disconnected = 2,
|
||||
}
|
||||
|
||||
export const TerminalStatusEnum = {
|
||||
Error: EnumValue.of(TerminalStatus.Error, '连接出错').setExtra({ iconColor: 'var(--el-color-error)' }),
|
||||
NoConnected: EnumValue.of(TerminalStatus.NoConnected, '未连接').setExtra({ iconColor: 'var(--el-color-primary)' }),
|
||||
Connected: EnumValue.of(TerminalStatus.Connected, '连接成功').setExtra({ iconColor: 'var(--el-color-success)' }),
|
||||
Disconnected: EnumValue.of(TerminalStatus.Disconnected, '连接失败').setExtra({ iconColor: 'var(--el-color-error)' }),
|
||||
};
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item :span="2" label="名称">{{ db?.name }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="id">{{ db?.id }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="db.tags" /></el-descriptions-item>
|
||||
<el-descriptions-item :span="3" label="标签"><TagCodePath :path="db.codePaths" /></el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="1" label="名称">{{ db?.name }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="主机">{{ `${db?.host}:${db?.port}` }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="类型">
|
||||
<SvgIcon :name="getDbDialect(db?.type).getInfo().icon" :size="20" />{{ db?.type }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="用户名">{{ db?.username }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="数据库">{{ sqlExec.db }}</el-descriptions-item>
|
||||
<el-descriptions-item label="表">
|
||||
@@ -33,7 +30,9 @@ import { dbApi } from '@/views/ops/db/api';
|
||||
import { DbSqlExecTypeEnum } from '@/views/ops/db/enums';
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import { getDbDialect } from '@/views/ops/db/dialect';
|
||||
import ResourceTags from '@/views/ops/component/ResourceTags.vue';
|
||||
import { tagApi } from '@/views/ops/tag/api';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import TagCodePath from '@/views/ops/component/TagCodePath.vue';
|
||||
|
||||
const props = defineProps({
|
||||
// 业务key
|
||||
@@ -74,6 +73,10 @@ const getDbSqlExec = async (bizKey: string) => {
|
||||
state.sqlExec = res.list?.[0];
|
||||
const dbRes = await dbApi.dbs.request({ id: state.sqlExec.dbId });
|
||||
state.db = dbRes.list?.[0];
|
||||
|
||||
tagApi.listByQuery.request({ type: TagResourceTypeEnum.DbName.value, codes: state.db.code }).then((res) => {
|
||||
state.db.codePaths = res.map((item: any) => item.codePath);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item :span="1" label="名称">{{ redis?.name }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="id">{{ redis?.id }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="用户名">{{ redis?.username }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="3" label="标签"><TagCodePath :path="redis.codePaths" /></el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="redis.tags" /></el-descriptions-item>
|
||||
<el-descriptions-item :span="2" label="编号">{{ redis?.code }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="名称">{{ redis?.name }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="1" label="主机">{{ `${redis?.host}` }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="库">{{ state.db }}</el-descriptions-item>
|
||||
@@ -22,8 +21,10 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, watch, onMounted } from 'vue';
|
||||
import ResourceTags from '@/views/ops/component/ResourceTags.vue';
|
||||
import { redisApi } from '@/views/ops/redis/api';
|
||||
import TagCodePath from '@/views/ops/component/TagCodePath.vue';
|
||||
import { tagApi } from '@/views/ops/tag/api';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
const props = defineProps({
|
||||
// 业务表单
|
||||
@@ -75,6 +76,10 @@ const parseRunCmdForm = async (bizForm: string) => {
|
||||
return;
|
||||
}
|
||||
state.redis = res.list?.[0];
|
||||
|
||||
tagApi.listByQuery.request({ type: TagResourceTypeEnum.Redis.value, codes: state.redis.code }).then((res) => {
|
||||
state.redis.codePaths = res.map((item: any) => item.codePath);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
:default-expanded-keys="props.defaultExpandedKeys"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<span @dblclick="treeNodeDblclick(data)" :class="data.type.nodeDblclickFunc ? 'none-select' : ''">
|
||||
<span :id="node.key" @dblclick="treeNodeDblclick(data)" :class="data.type.nodeDblclickFunc ? 'none-select' : ''">
|
||||
<span v-if="data.type.value == TagTreeNode.TagPath">
|
||||
<tag-info :tag-path="data.label" />
|
||||
</span>
|
||||
@@ -48,11 +48,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, ref, watch, toRefs } from 'vue';
|
||||
import { onMounted, reactive, ref, watch, toRefs, nextTick } from 'vue';
|
||||
import { NodeType, TagTreeNode } from './tag';
|
||||
import TagInfo from './TagInfo.vue';
|
||||
import { Contextmenu } from '@/components/contextmenu';
|
||||
import { tagApi } from '../tag/api';
|
||||
import { isPrefixSubsequence } from '@/common/utils/string';
|
||||
|
||||
const props = defineProps({
|
||||
resourceType: {
|
||||
@@ -105,8 +106,7 @@ watch(filterText, (val) => {
|
||||
});
|
||||
|
||||
const filterNode = (value: string, data: any) => {
|
||||
if (!value) return true;
|
||||
return data.label.includes(value);
|
||||
return !value || isPrefixSubsequence(value, data.label);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -126,7 +126,7 @@ const loadTags = async () => {
|
||||
* @param { Object } node
|
||||
* @param { Object } resolve
|
||||
*/
|
||||
const loadNode = async (node: any, resolve: any) => {
|
||||
const loadNode = async (node: any, resolve: (data: any) => void, reject: () => void) => {
|
||||
if (typeof resolve !== 'function') {
|
||||
return;
|
||||
}
|
||||
@@ -141,6 +141,8 @@ const loadNode = async (node: any, resolve: any) => {
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
// 调用 reject 以保持节点状态,并允许远程加载继续。
|
||||
return reject();
|
||||
}
|
||||
return resolve(nodes);
|
||||
};
|
||||
@@ -207,6 +209,17 @@ const getNode = (nodeKey: any) => {
|
||||
|
||||
const setCurrentKey = (nodeKey: any) => {
|
||||
treeRef.value.setCurrentKey(nodeKey);
|
||||
|
||||
// 通过Id获取到对应的dom元素
|
||||
const node = document.getElementById(nodeKey);
|
||||
if (node) {
|
||||
setTimeout(() => {
|
||||
nextTick(() => {
|
||||
// 通过scrollIntoView方法将对应的dom元素定位到可见区域 【block: 'center'】这个属性是在垂直方向居中显示
|
||||
node.scrollIntoView({ block: 'center' });
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
v-bind="$attrs"
|
||||
ref="tagTreeRef"
|
||||
:data="state.tags"
|
||||
:default-expanded-keys="checkedTags"
|
||||
:default-expanded-keys="state.defaultExpandedKeys"
|
||||
:default-checked-keys="checkedTags"
|
||||
multiple
|
||||
:render-after-expand="true"
|
||||
@@ -50,6 +50,7 @@ import { ref, reactive, onMounted } from 'vue';
|
||||
import { tagApi } from '../tag/api';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import EnumValue from '@/common/Enum';
|
||||
import { isPrefixSubsequence } from '@/common/utils/string';
|
||||
|
||||
const props = defineProps({
|
||||
height: {
|
||||
@@ -74,10 +75,12 @@ const tagTreeRef: any = ref(null);
|
||||
const filterTag = ref('');
|
||||
|
||||
const state = reactive({
|
||||
defaultExpandedKeys: [] as any,
|
||||
tags: [],
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
state.defaultExpandedKeys = checkedTags.value;
|
||||
search();
|
||||
});
|
||||
|
||||
@@ -100,10 +103,7 @@ const search = async () => {
|
||||
};
|
||||
|
||||
const filterNode = (value: string, data: any) => {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
return data.codePath.toLowerCase().includes(value) || data.name.includes(value);
|
||||
return !value || isPrefixSubsequence(value, data.codePath) || isPrefixSubsequence(value, data.name);
|
||||
};
|
||||
|
||||
const onFilterValChanged = (val: string) => {
|
||||
|
||||
@@ -1,93 +1,112 @@
|
||||
<template>
|
||||
<div class="db-list">
|
||||
<page-table
|
||||
ref="pageTableRef"
|
||||
:page-api="dbApi.dbs"
|
||||
:before-query-fn="checkRouteTagPath"
|
||||
:search-items="searchItems"
|
||||
v-model:query-form="query"
|
||||
:columns="columns"
|
||||
lazy
|
||||
<el-drawer
|
||||
:title="title"
|
||||
v-model="dialogVisible"
|
||||
@open="search"
|
||||
:before-close="cancel"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="true"
|
||||
size="60%"
|
||||
>
|
||||
<template #instanceSelect>
|
||||
<el-select remote :remote-method="getInstances" v-model="query.instanceId" placeholder="输入并选择实例" filterable clearable>
|
||||
<el-option v-for="item in state.instances" :key="item.id" :label="`${item.name}`" :value="item.id">
|
||||
{{ item.name }}
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
{{ item.type }} / {{ item.host }}:{{ item.port }}
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
{{ item.username }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<template #type="{ data }">
|
||||
<el-tooltip :content="data.type" placement="top">
|
||||
<SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<template #host="{ data }">
|
||||
{{ `${data.host}:${data.port}` }}
|
||||
</template>
|
||||
|
||||
<template #database="{ data }">
|
||||
<el-popover placement="bottom" :width="200" trigger="click">
|
||||
<template #reference>
|
||||
<el-button @click="getDbNames(data)" type="primary" link>查看库</el-button>
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="cancel">
|
||||
<template #extra>
|
||||
<div class="mr20">
|
||||
<span>{{ $props.instance?.tags?.[0]?.codePath }}</span>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<SvgIcon :name="getDbDialect($props.instance?.type).getInfo()?.icon" :size="20" />
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<span>{{ $props.instance?.host }}:{{ $props.instance?.port }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="filterDbs" v-loading="state.loadingDbNames" size="small">
|
||||
<el-table-column prop="dbName" label="数据库">
|
||||
<template #header>
|
||||
<el-input v-model="state.dbNameSearch" size="small" placeholder="库名: 输入可过滤" clearable />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-popover>
|
||||
</DrawerHeader>
|
||||
</template>
|
||||
|
||||
<template #tagPath="{ data }">
|
||||
<ResourceTags :tags="data.tags" />
|
||||
</template>
|
||||
<page-table
|
||||
ref="pageTableRef"
|
||||
:page-api="dbApi.dbs"
|
||||
v-model:query-form="query"
|
||||
:columns="columns"
|
||||
lazy
|
||||
show-selection
|
||||
v-model:selection-data="state.selectionData"
|
||||
>
|
||||
<template #tableHeader>
|
||||
<el-button v-auth="perms.saveDb" type="primary" circle icon="Plus" @click="editDb(null)"> </el-button>
|
||||
<el-button v-auth="perms.delDb" :disabled="state.selectionData.length < 1" @click="deleteDb" type="danger" circle icon="delete"></el-button>
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button type="primary" @click="onShowSqlExec(data)" link>SQL记录</el-button>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<template #type="{ data }">
|
||||
<el-tooltip :content="data.type" placement="top">
|
||||
<SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<el-dropdown @command="handleMoreActionCommand">
|
||||
<span class="el-dropdown-link-more">
|
||||
更多
|
||||
<el-icon class="el-icon--right">
|
||||
<arrow-down />
|
||||
</el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item :command="{ type: 'detail', data }"> 详情 </el-dropdown-item>
|
||||
<el-dropdown-item :command="{ type: 'dumpDb', data }"> 导出 </el-dropdown-item>
|
||||
<el-dropdown-item :command="{ type: 'backupDb', data }" v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)">
|
||||
备份任务
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:command="{ type: 'backupHistory', data }"
|
||||
v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
|
||||
>
|
||||
备份历史
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:command="{ type: 'restoreDb', data }"
|
||||
v-if="actionBtns[perms.restoreDb] && supportAction('restoreDb', data.type)"
|
||||
>
|
||||
恢复任务
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</page-table>
|
||||
<template #database="{ data }">
|
||||
<el-popover placement="bottom" :width="200" trigger="click">
|
||||
<template #reference>
|
||||
<el-button @click="getDbNames(data)" type="primary" link>查看库</el-button>
|
||||
</template>
|
||||
<el-table :data="filterDbs" v-loading="state.loadingDbNames" size="small">
|
||||
<el-table-column prop="dbName" label="数据库">
|
||||
<template #header>
|
||||
<el-input v-model="state.dbNameSearch" size="small" placeholder="库名: 输入可过滤" clearable />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<el-dialog width="750px" :title="`${db} 数据库导出`" v-model="exportDialog.visible">
|
||||
<template #tagPath="{ data }">
|
||||
<ResourceTags :tags="data.tags" />
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button v-auth="perms.saveDb" @click="editDb(data)" type="primary" link>编辑</el-button>
|
||||
|
||||
<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">
|
||||
更多
|
||||
<el-icon class="el-icon--right">
|
||||
<arrow-down />
|
||||
</el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item :command="{ type: 'dumpDb', data }"> 导出 </el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:command="{ type: 'backupDb', data }"
|
||||
v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
|
||||
>
|
||||
备份任务
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:command="{ type: 'backupHistory', data }"
|
||||
v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
|
||||
>
|
||||
备份历史
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:command="{ type: 'restoreDb', data }"
|
||||
v-if="actionBtns[perms.restoreDb] && supportAction('restoreDb', data.type)"
|
||||
>
|
||||
恢复任务
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</page-table>
|
||||
</el-drawer>
|
||||
|
||||
<el-dialog width="750px" :title="`${exportDialog.db} 数据库导出`" v-model="exportDialog.visible">
|
||||
<el-row justify="space-between">
|
||||
<el-col :span="9">
|
||||
<el-form-item label="导出内容: ">
|
||||
@@ -168,54 +187,29 @@
|
||||
<db-restore-list :dbId="dbRestoreDialog.dbId" :dbNames="dbRestoreDialog.dbs" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-if="infoDialog.visible" v-model="infoDialog.visible" :before-close="onBeforeCloseInfoDialog">
|
||||
<el-descriptions title="详情" :column="3" border>
|
||||
<el-descriptions-item :span="2" label="名称">{{ infoDialog.data?.name }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="id">{{ infoDialog.data?.id }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="infoDialog.data.tags" /></el-descriptions-item>
|
||||
<el-descriptions-item :span="3" label="数据库实例名称">{{ infoDialog.instance?.name }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="主机">{{ infoDialog.instance?.host }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="端口">{{ infoDialog.instance?.port }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="授权凭证">{{ infoDialog.instance.authCertName }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="类型">
|
||||
<SvgIcon :name="getDbDialect(infoDialog.instance?.type).getInfo().icon" :size="20" />{{ infoDialog.instance?.type }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" label="数据库">{{ infoDialog.data?.database }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data?.remark }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="创建时间">{{ formatDate(infoDialog.data?.createTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="创建者">{{ infoDialog.data?.creator }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="更新时间">{{ formatDate(infoDialog.data?.updateTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data?.modifier }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
|
||||
<db-edit @val-change="search()" :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" v-model:db="dbEditDialog.data"></db-edit>
|
||||
<db-edit
|
||||
@confirm="confirmEditDb"
|
||||
@cancel="cancelEditDb"
|
||||
:title="dbEditDialog.title"
|
||||
v-model:visible="dbEditDialog.visible"
|
||||
:instance="props.instance"
|
||||
v-model:db="dbEditDialog.data"
|
||||
></db-edit>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
||||
import { computed, defineAsyncComponent, reactive, ref, Ref, toRefs } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import config from '@/common/config';
|
||||
import { joinClientParams } from '@/common/request';
|
||||
import { isTrue } from '@/common/assert';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import DbSqlExecLog from './DbSqlExecLog.vue';
|
||||
import { DbType } from './dialect';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { getDbDialect } from './dialect/index';
|
||||
import { getTagPathSearchItem } from '../component/tag';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import DbBackupList from './DbBackupList.vue';
|
||||
import DbBackupHistoryList from './DbBackupHistoryList.vue';
|
||||
import DbRestoreList from './DbRestoreList.vue';
|
||||
@@ -223,44 +217,47 @@ import ResourceTags from '../component/ResourceTags.vue';
|
||||
import { sleep } from '@/common/utils/loading';
|
||||
import { DbGetDbNamesMode } from './enums';
|
||||
import { DbInst } from './db';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
|
||||
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
|
||||
|
||||
const searchItems = [
|
||||
getTagPathSearchItem(TagResourceTypeEnum.DbName.value),
|
||||
SearchItem.slot('instanceId', '实例', 'instanceSelect'),
|
||||
SearchItem.input('code', '编号'),
|
||||
];
|
||||
const props = defineProps({
|
||||
instance: {
|
||||
type: [Object],
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const dialogVisible = defineModel<boolean>('visible');
|
||||
|
||||
const emit = defineEmits(['cancel']);
|
||||
|
||||
const columns = ref([
|
||||
TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),
|
||||
TableColumn.new('name', '名称'),
|
||||
TableColumn.new('type', '类型').isSlot().setAddWidth(-15).alignCenter(),
|
||||
TableColumn.new('instanceName', '实例名'),
|
||||
TableColumn.new('host', 'ip:port').isSlot().setAddWidth(40),
|
||||
TableColumn.new('authCertName', '授权凭证'),
|
||||
TableColumn.new('getDatabaseMode', '获库方式').typeTag(DbGetDbNamesMode),
|
||||
TableColumn.new('database', '库').isSlot().setMinWidth(80),
|
||||
TableColumn.new('remark', '备注'),
|
||||
TableColumn.new('code', '编号'),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(210).fixedRight().alignCenter(),
|
||||
]);
|
||||
|
||||
const perms = {
|
||||
base: 'db',
|
||||
saveDb: 'db:save',
|
||||
delDb: 'db:del',
|
||||
backupDb: 'db:backup',
|
||||
restoreDb: 'db:restore',
|
||||
};
|
||||
|
||||
// 该用户拥有的的操作列按钮权限
|
||||
// const actionBtns = hasPerms([perms.base, perms.saveDb]);
|
||||
const actionBtns = hasPerms(Object.values(perms));
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight().alignCenter();
|
||||
|
||||
const route = useRoute();
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
const state = reactive({
|
||||
row: {} as any,
|
||||
dbId: 0,
|
||||
db: '',
|
||||
loadingDbNames: false,
|
||||
currentDbNames: [],
|
||||
dbNameSearch: '',
|
||||
@@ -268,29 +265,20 @@ const state = reactive({
|
||||
/**
|
||||
* 选中的数据
|
||||
*/
|
||||
selectionData: [],
|
||||
selectionData: [] as any,
|
||||
/**
|
||||
* 查询条件
|
||||
*/
|
||||
query: {
|
||||
tagPath: '',
|
||||
instanceId: null,
|
||||
instanceId: 0,
|
||||
pageNum: 1,
|
||||
pageSize: 0,
|
||||
},
|
||||
infoDialog: {
|
||||
visible: false,
|
||||
data: null as any,
|
||||
instance: null as any,
|
||||
query: {
|
||||
instanceId: 0,
|
||||
},
|
||||
},
|
||||
// sql执行记录弹框
|
||||
sqlExecLogDialog: {
|
||||
title: '',
|
||||
visible: false,
|
||||
dbs: [],
|
||||
dbs: [] as any,
|
||||
dbId: 0,
|
||||
},
|
||||
// 数据库备份弹框
|
||||
@@ -321,6 +309,7 @@ const state = reactive({
|
||||
exportDialog: {
|
||||
visible: false,
|
||||
dbId: 0,
|
||||
db: '',
|
||||
type: 3,
|
||||
data: [] as any,
|
||||
value: [],
|
||||
@@ -339,14 +328,12 @@ const state = reactive({
|
||||
},
|
||||
});
|
||||
|
||||
const { db, query, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbBackupHistoryDialog, dbRestoreDialog } = toRefs(state);
|
||||
const { query, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbBackupHistoryDialog, dbRestoreDialog } = toRefs(state);
|
||||
|
||||
onMounted(async () => {
|
||||
if (Object.keys(actionBtns).length > 0) {
|
||||
columns.value.push(actionColumn);
|
||||
}
|
||||
search();
|
||||
});
|
||||
const search = async () => {
|
||||
state.query.instanceId = props.instance?.id;
|
||||
pageTableRef.value.search();
|
||||
};
|
||||
|
||||
const getDbNames = async (db: any) => {
|
||||
try {
|
||||
@@ -372,42 +359,46 @@ const filterDbs = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const checkRouteTagPath = (query: any) => {
|
||||
if (route.query.tagPath) {
|
||||
query.tagPath = route.query.tagPath as string;
|
||||
}
|
||||
return query;
|
||||
};
|
||||
|
||||
const search = async (tagPath: string = '') => {
|
||||
if (tagPath) {
|
||||
state.query.tagPath = tagPath;
|
||||
}
|
||||
pageTableRef.value.search();
|
||||
};
|
||||
|
||||
const showInfo = async (info: any) => {
|
||||
state.infoDialog.data = info;
|
||||
state.infoDialog.query.instanceId = info.instanceId;
|
||||
const res = await dbApi.getInstance.request(state.infoDialog.query);
|
||||
state.infoDialog.instance = res;
|
||||
state.infoDialog.visible = true;
|
||||
};
|
||||
|
||||
const onBeforeCloseInfoDialog = () => {
|
||||
state.infoDialog.visible = false;
|
||||
state.infoDialog.data = null;
|
||||
state.infoDialog.instance = null;
|
||||
};
|
||||
|
||||
const getInstances = async (instanceName = '') => {
|
||||
if (!instanceName) {
|
||||
state.instances = [];
|
||||
return;
|
||||
}
|
||||
const data = await dbApi.instances.request({ name: instanceName });
|
||||
const editDb = (data: any) => {
|
||||
if (data) {
|
||||
state.instances = data.list;
|
||||
state.dbEditDialog.data = { ...data };
|
||||
} else {
|
||||
state.dbEditDialog.data = {
|
||||
instanceId: props.instance.id,
|
||||
};
|
||||
}
|
||||
state.dbEditDialog.title = data ? '编辑数据库' : '新增数据库';
|
||||
state.dbEditDialog.visible = true;
|
||||
};
|
||||
|
||||
const confirmEditDb = async (db: any) => {
|
||||
db.instanceId = props.instance.id;
|
||||
await dbApi.saveDb.request(db);
|
||||
ElMessage.success('保存成功');
|
||||
search();
|
||||
cancelEditDb();
|
||||
};
|
||||
|
||||
const cancelEditDb = () => {
|
||||
state.dbEditDialog.visible = false;
|
||||
state.dbEditDialog.data = {};
|
||||
};
|
||||
|
||||
const deleteDb = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除【${state.selectionData.map((x: any) => x.name).join(', ')}】库?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
for (let db of state.selectionData) {
|
||||
await dbApi.deleteDb.request({ id: db.id });
|
||||
}
|
||||
ElMessage.success('删除成功');
|
||||
} catch (err) {
|
||||
//
|
||||
} finally {
|
||||
search();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -415,10 +406,6 @@ const handleMoreActionCommand = (commond: any) => {
|
||||
const data = commond.data;
|
||||
const type = commond.type;
|
||||
switch (type) {
|
||||
case 'detail': {
|
||||
showInfo(data);
|
||||
return;
|
||||
}
|
||||
case 'dumpDb': {
|
||||
onDumpDbs(data);
|
||||
return;
|
||||
@@ -441,7 +428,9 @@ const handleMoreActionCommand = (commond: any) => {
|
||||
const onShowSqlExec = async (row: any) => {
|
||||
state.sqlExecLogDialog.title = `${row.name}`;
|
||||
state.sqlExecLogDialog.dbId = row.id;
|
||||
state.sqlExecLogDialog.dbs = row.database.split(' ');
|
||||
DbInst.getDbNames(row).then((res) => {
|
||||
state.sqlExecLogDialog.dbs = res;
|
||||
});
|
||||
state.sqlExecLogDialog.visible = true;
|
||||
};
|
||||
|
||||
@@ -454,26 +443,32 @@ const onBeforeCloseSqlExecDialog = () => {
|
||||
const onShowDbBackupDialog = async (row: any) => {
|
||||
state.dbBackupDialog.title = `${row.name}`;
|
||||
state.dbBackupDialog.dbId = row.id;
|
||||
state.dbBackupDialog.dbs = row.database.split(' ');
|
||||
DbInst.getDbNames(row).then((res) => {
|
||||
state.sqlExecLogDialog.dbs = res;
|
||||
});
|
||||
state.dbBackupDialog.visible = true;
|
||||
};
|
||||
|
||||
const onShowDbBackupHistoryDialog = async (row: any) => {
|
||||
state.dbBackupHistoryDialog.title = `${row.name}`;
|
||||
state.dbBackupHistoryDialog.dbId = row.id;
|
||||
state.dbBackupHistoryDialog.dbs = row.database.split(' ');
|
||||
DbInst.getDbNames(row).then((res) => {
|
||||
state.sqlExecLogDialog.dbs = res;
|
||||
});
|
||||
state.dbBackupHistoryDialog.visible = true;
|
||||
};
|
||||
|
||||
const onShowDbRestoreDialog = async (row: any) => {
|
||||
state.dbRestoreDialog.title = `${row.name}`;
|
||||
state.dbRestoreDialog.dbId = row.id;
|
||||
state.dbRestoreDialog.dbs = row.database.split(' ');
|
||||
DbInst.getDbNames(row).then((res) => {
|
||||
state.sqlExecLogDialog.dbs = res;
|
||||
});
|
||||
state.dbRestoreDialog.visible = true;
|
||||
};
|
||||
|
||||
const onDumpDbs = async (row: any) => {
|
||||
const dbs = row.database.split(' ');
|
||||
const dbs = await DbInst.getDbNames(row);
|
||||
const data = [];
|
||||
for (let name of dbs) {
|
||||
data.push({
|
||||
@@ -481,6 +476,7 @@ const onDumpDbs = async (row: any) => {
|
||||
label: name,
|
||||
});
|
||||
}
|
||||
state.exportDialog.db = row.name;
|
||||
state.exportDialog.value = [];
|
||||
state.exportDialog.data = data;
|
||||
state.exportDialog.dbId = row.id;
|
||||
@@ -524,7 +520,10 @@ const supportAction = (action: string, dbType: string): boolean => {
|
||||
return actions.includes(action);
|
||||
};
|
||||
|
||||
defineExpose({ search });
|
||||
const cancel = () => {
|
||||
dialogVisible.value = false;
|
||||
emit('cancel');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.db-list {
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="50%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
</template>
|
||||
|
||||
<el-table :data="state.dbs" stripe>
|
||||
<el-table-column prop="name" label="名称" show-overflow-tooltip min-width="100"> </el-table-column>
|
||||
<el-table-column prop="authCertName" label="授权凭证" min-width="120" show-overflow-tooltip> </el-table-column>
|
||||
<el-table-column prop="getDatabaseMode" label="获库方式" min-width="80">
|
||||
<template #default="scope">
|
||||
<EnumTag :enums="DbGetDbNamesMode" :value="scope.row.getDatabaseMode" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="database" label="库" min-width="80">
|
||||
<template #default="scope">
|
||||
<el-popover placement="bottom" :width="200" trigger="click">
|
||||
<template #reference>
|
||||
<el-button @click="getDbNames(scope.row)" type="primary" link>查看库</el-button>
|
||||
</template>
|
||||
<el-table :data="filterDbs" size="small" v-loading="state.loadingDbNames">
|
||||
<el-table-column prop="dbName" label="数据库">
|
||||
<template #header>
|
||||
<el-input v-model="state.dbNameSearch" size="small" placeholder="库名: 输入可过滤" clearable />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="remark" label="备注" show-overflow-tooltip min-width="120"> </el-table-column>
|
||||
<el-table-column prop="code" label="编号" show-overflow-tooltip min-width="120"> </el-table-column>
|
||||
<el-table-column min-wdith="120px">
|
||||
<template #header>
|
||||
操作
|
||||
<el-button v-auth="perms.saveDb" type="primary" circle size="small" icon="Plus" @click="editDb(null)"> </el-button>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<el-button v-auth="perms.saveDb" @click="editDb(scope.row)" type="primary" icon="edit" link></el-button>
|
||||
<el-button class="ml1" v-auth="perms.delDb" type="danger" @click="deleteDb(scope.row)" icon="delete" link></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<db-edit
|
||||
@confirm="confirmEditDb"
|
||||
@cancel="cancelEditDb"
|
||||
:title="dbEditDialog.title"
|
||||
v-model:visible="dbEditDialog.visible"
|
||||
:instance="props.instance"
|
||||
v-model:db="dbEditDialog.data"
|
||||
></db-edit>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, toRefs, watchEffect } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
import DbEdit from './DbEdit.vue';
|
||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||
import { DbGetDbNamesMode } from './enums';
|
||||
import { DbInst } from './db';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
},
|
||||
instance: {
|
||||
type: [Object],
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const perms = {
|
||||
base: 'db',
|
||||
saveDb: 'db:save',
|
||||
delDb: 'db:del',
|
||||
};
|
||||
|
||||
//定义事件
|
||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
|
||||
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
dbs: [] as any,
|
||||
loadingDbNames: false,
|
||||
currentDbNames: [], // 当前数据库名
|
||||
dbNameSearch: '',
|
||||
dbEditDialog: {
|
||||
visible: false,
|
||||
data: null as any,
|
||||
title: '新增数据库',
|
||||
},
|
||||
});
|
||||
|
||||
const { dialogVisible, dbEditDialog } = toRefs(state);
|
||||
|
||||
watchEffect(() => {
|
||||
state.dialogVisible = props.visible;
|
||||
if (!state.dialogVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
getDbs();
|
||||
});
|
||||
|
||||
const getDbNames = async (db: any) => {
|
||||
try {
|
||||
state.loadingDbNames = true;
|
||||
state.currentDbNames = await DbInst.getDbNames(db);
|
||||
} finally {
|
||||
state.loadingDbNames = false;
|
||||
}
|
||||
};
|
||||
|
||||
const filterDbs = computed(() => {
|
||||
const dbNames = state.currentDbNames;
|
||||
if (!dbNames) {
|
||||
return [];
|
||||
}
|
||||
const dbNameObjs = dbNames.map((x) => {
|
||||
return {
|
||||
dbName: x,
|
||||
};
|
||||
});
|
||||
return dbNameObjs.filter((db: any) => {
|
||||
return db.dbName.includes(state.dbNameSearch);
|
||||
});
|
||||
});
|
||||
|
||||
const cancel = () => {
|
||||
emit('update:visible', false);
|
||||
emit('cancel');
|
||||
};
|
||||
|
||||
const getDbs = () => {
|
||||
dbApi.dbs.request({ pageSize: 200, instanceId: props.instance.id }).then((res: any) => {
|
||||
state.dbs = res.list || [];
|
||||
});
|
||||
};
|
||||
|
||||
const editDb = (data: any) => {
|
||||
if (data) {
|
||||
state.dbEditDialog.data = { ...data };
|
||||
} else {
|
||||
state.dbEditDialog.data = {
|
||||
instanceId: props.instance.id,
|
||||
};
|
||||
}
|
||||
state.dbEditDialog.title = data ? '编辑数据库' : '新增数据库';
|
||||
state.dbEditDialog.visible = true;
|
||||
};
|
||||
|
||||
const deleteDb = async (db: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除【${db.name}】库?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
await dbApi.deleteDb.request({ id: db.id });
|
||||
ElMessage.success('删除成功');
|
||||
getDbs();
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
const confirmEditDb = async (db: any) => {
|
||||
db.instanceId = props.instance.id;
|
||||
await dbApi.saveDb.request(db);
|
||||
ElMessage.success('保存成功');
|
||||
getDbs();
|
||||
cancelEditDb();
|
||||
};
|
||||
|
||||
const cancelEditDb = () => {
|
||||
state.dbEditDialog.visible = false;
|
||||
state.dbEditDialog.data = {};
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -35,7 +35,7 @@
|
||||
<template #action="{ data }">
|
||||
<el-button @click="showInfo(data)" link>详情</el-button>
|
||||
<el-button v-if="actionBtns[perms.saveInstance]" @click="editInstance(data)" type="primary" link>编辑</el-button>
|
||||
<el-button v-if="actionBtns[perms.saveDb]" @click="editDb(data)" type="primary" link>库配置</el-button>
|
||||
<el-button v-if="actionBtns[perms.saveDb]" @click="editDb(data)" type="primary" link>库管理</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
v-model:data="instanceEditDialog.data"
|
||||
></instance-edit>
|
||||
|
||||
<instance-db-conf :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" :instance="dbEditDialog.instance" />
|
||||
<DbList :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" :instance="dbEditDialog.instance" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -89,7 +89,7 @@ import { getTagPathSearchItem } from '../component/tag';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
const InstanceEdit = defineAsyncComponent(() => import('./InstanceEdit.vue'));
|
||||
const InstanceDbConf = defineAsyncComponent(() => import('./InstanceDbConf.vue'));
|
||||
const DbList = defineAsyncComponent(() => import('./DbList.vue'));
|
||||
|
||||
const props = defineProps({
|
||||
lazy: {
|
||||
@@ -215,7 +215,7 @@ const deleteInstance = async () => {
|
||||
|
||||
const editDb = (data: any) => {
|
||||
state.dbEditDialog.instance = data;
|
||||
state.dbEditDialog.title = `配置 "${data.name}" 数据库`;
|
||||
state.dbEditDialog.title = `管理 "${data.name}" 数据库`;
|
||||
state.dbEditDialog.visible = true;
|
||||
};
|
||||
|
||||
|
||||
@@ -58,16 +58,61 @@
|
||||
<el-row>
|
||||
<el-col :span="24" v-if="state.db">
|
||||
<el-descriptions :column="4" size="small" border>
|
||||
<el-descriptions-item label-align="right" label="操作"
|
||||
><el-button
|
||||
<el-descriptions-item label-align="right" label="操作">
|
||||
<el-button
|
||||
:disabled="!state.db || !nowDbInst.id"
|
||||
type="primary"
|
||||
icon="Search"
|
||||
@click="addQueryTab({ id: nowDbInst.id, dbs: nowDbInst.databases }, state.db)"
|
||||
size="small"
|
||||
>新建查询</el-button
|
||||
></el-descriptions-item
|
||||
>
|
||||
link
|
||||
@click="
|
||||
addQueryTab(
|
||||
{ id: nowDbInst.id, dbs: nowDbInst.databases, nodeKey: getSqlMenuNodeKey(nowDbInst.id, state.db) },
|
||||
state.db
|
||||
)
|
||||
"
|
||||
title="新建查询"
|
||||
>
|
||||
</el-button>
|
||||
|
||||
<template v-if="!dbConfig.locationTreeNode">
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-button @click="locationNowTreeNode(null)" title="定位至左侧树的指定位置" icon="Location" link></el-button>
|
||||
</template>
|
||||
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<!-- 数据库展示配置 -->
|
||||
<el-popover
|
||||
popper-style="max-height: 550px; overflow: auto; max-width: 450px"
|
||||
placement="bottom"
|
||||
width="auto"
|
||||
title="数据库展示配置"
|
||||
trigger="click"
|
||||
>
|
||||
<el-row>
|
||||
<el-checkbox
|
||||
v-model="dbConfig.showColumnComment"
|
||||
label="显示字段备注"
|
||||
:true-value="1"
|
||||
:false-value="0"
|
||||
size="small"
|
||||
/>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-checkbox
|
||||
v-model="dbConfig.locationTreeNode"
|
||||
label="自动定位树节点"
|
||||
:true-value="1"
|
||||
:false-value="0"
|
||||
size="small"
|
||||
/>
|
||||
</el-row>
|
||||
|
||||
<template #reference>
|
||||
<el-link type="primary" icon="setting" :underline="false"></el-link>
|
||||
</template>
|
||||
</el-popover>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label-align="right" label="tag">{{ nowDbInst.tagPath }}</el-descriptions-item>
|
||||
|
||||
@@ -103,7 +148,9 @@
|
||||
<el-tab-pane class="h100" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
|
||||
<template #label>
|
||||
<el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
|
||||
<template #reference> {{ dt.label }} </template>
|
||||
<template #reference>
|
||||
<span class="font12">{{ dt.label }}</span>
|
||||
</template>
|
||||
<template #default>
|
||||
<el-descriptions :column="1" size="small">
|
||||
<el-descriptions-item label="tagPath">
|
||||
@@ -130,6 +177,7 @@
|
||||
:db-name="dt.db"
|
||||
:table-name="dt.params.table"
|
||||
:table-height="state.dataTabsTableHeight"
|
||||
:ref="(el: any) => (dt.componentRef = el)"
|
||||
></db-table-data-op>
|
||||
|
||||
<db-sql-editor
|
||||
@@ -138,6 +186,7 @@
|
||||
:db-name="dt.db"
|
||||
:sql-name="dt.params.sqlName"
|
||||
@save-sql-success="reloadSqls"
|
||||
:ref="(el: any) => (dt.componentRef = el)"
|
||||
>
|
||||
</db-sql-editor>
|
||||
|
||||
@@ -173,7 +222,7 @@
|
||||
import { defineAsyncComponent, h, onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue';
|
||||
import { ElCheckbox, ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { formatByteSize } from '@/common/utils/format';
|
||||
import { DbInst, registerDbCompletionItemProvider, TabInfo, TabType } from './db';
|
||||
import { DbInst, DbThemeConfig, registerDbCompletionItemProvider, TabInfo, TabType } from './db';
|
||||
import { NodeType, TagTreeNode, getTagTypeCodeByPath } from '../component/tag';
|
||||
import TagTree from '../component/TagTree.vue';
|
||||
import { dbApi } from './api';
|
||||
@@ -184,7 +233,7 @@ import { getDbDialect, schemaDbTypes } from './dialect/index';
|
||||
import { sleep } from '@/common/utils/loading';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { Pane, Splitpanes } from 'splitpanes';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import { useEventListener, useStorage } from '@vueuse/core';
|
||||
import SqlExecBox from '@/views/ops/db/component/sqleditor/SqlExecBox';
|
||||
import { useAutoOpenResource } from '@/store/autoOpenResource';
|
||||
import { storeToRefs } from 'pinia';
|
||||
@@ -456,6 +505,8 @@ const state = reactive({
|
||||
|
||||
const { nowDbInst, tableCreateDialog } = toRefs(state);
|
||||
|
||||
const dbConfig = useStorage('dbConfig', DbThemeConfig);
|
||||
|
||||
const serverInfoReqParam = ref({
|
||||
instanceId: 0,
|
||||
});
|
||||
@@ -530,7 +581,7 @@ const loadTableData = async (db: any, dbName: string, tableName: string) => {
|
||||
}
|
||||
changeDb(db, dbName);
|
||||
|
||||
const key = `${db.id}:\`${dbName}\`.${tableName}`;
|
||||
const key = `tableData:${db.id}.${dbName}.${tableName}`;
|
||||
let tab = state.tabs.get(key);
|
||||
state.activeName = key;
|
||||
// 如果存在该表tab,则直接返回
|
||||
@@ -565,7 +616,7 @@ const addQueryTab = async (db: any, dbName: string, sqlName: string = '') => {
|
||||
// 存在sql模板名,则该模板名只允许一个tab
|
||||
if (sqlName) {
|
||||
label = `查询-${sqlName}`;
|
||||
key = `查询:${dbId}:${dbName}.${sqlName}`;
|
||||
key = `query:${dbId}.${dbName}.${sqlName}`;
|
||||
} else {
|
||||
let count = 1;
|
||||
state.tabs.forEach((v) => {
|
||||
@@ -574,7 +625,7 @@ const addQueryTab = async (db: any, dbName: string, sqlName: string = '') => {
|
||||
}
|
||||
});
|
||||
label = `新查询-${count}`;
|
||||
key = `新查询${count}:${dbId}:${dbName}`;
|
||||
key = `query:${count}.${dbId}.${dbName}`;
|
||||
}
|
||||
state.activeName = key;
|
||||
let tab = state.tabs.get(key);
|
||||
@@ -611,7 +662,7 @@ const addTablesOpTab = async (db: any) => {
|
||||
changeDb(db, dbName);
|
||||
|
||||
const dbId = db.id;
|
||||
let key = `表操作:${dbId}:${dbName}.tablesOp`;
|
||||
let key = `tablesOp:${dbId}.${dbName}`;
|
||||
state.activeName = key;
|
||||
|
||||
let tab = state.tabs.get(key);
|
||||
@@ -642,15 +693,22 @@ const onRemoveTab = (targetName: string) => {
|
||||
if (tabName !== targetName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
state.tabs.delete(targetName);
|
||||
if (activeName != targetName) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 如果删除的tab是当前激活的tab,则切换到前一个或后一个tab
|
||||
const nextTab = tabNames[i + 1] || tabNames[i - 1];
|
||||
if (nextTab) {
|
||||
activeName = nextTab;
|
||||
} else {
|
||||
activeName = '';
|
||||
}
|
||||
state.tabs.delete(targetName);
|
||||
state.activeName = activeName;
|
||||
onTabChange();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -670,6 +728,21 @@ const onTabChange = () => {
|
||||
registerDbCompletionItemProvider(nowTab.dbId, nowTab.db, nowTab.params.dbs, nowDbInst.value.type);
|
||||
}
|
||||
|
||||
// 激活当前tab(需要调用DbTableData组件的active,否则表头与数据会出现错位,暂不知为啥,先这样处理)
|
||||
nowTab?.componentRef?.active();
|
||||
|
||||
if (dbConfig.value.locationTreeNode) {
|
||||
locationNowTreeNode(nowTab);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 定位至当前树节点
|
||||
*/
|
||||
const locationNowTreeNode = (nowTab: any = null) => {
|
||||
if (!nowTab) {
|
||||
nowTab = state.tabs.get(state.activeName);
|
||||
}
|
||||
tagTreeRef.value.setCurrentKey(nowTab?.treeNodeKey);
|
||||
};
|
||||
|
||||
@@ -854,7 +927,7 @@ const getNowDbInfo = () => {
|
||||
margin: 0 0 5px;
|
||||
|
||||
.el-tabs__item {
|
||||
padding: 0 10px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,10 @@ export const dbApi = {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log(param.sql);
|
||||
}
|
||||
param.sql = Base64.encode(param.sql);
|
||||
// 非base64编码sql,则进行base64编码(refreshToken时,会重复调用该方法,故简单判断下)
|
||||
if (!Base64.isValid(param.sql)) {
|
||||
param.sql = Base64.encode(param.sql);
|
||||
}
|
||||
}
|
||||
return param;
|
||||
}),
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
<Pane :size="100 - state.editorSize">
|
||||
<div class="mt5 sql-exec-res h100">
|
||||
<el-tabs class="h100 w100" v-if="state.execResTabs.length > 0" @tab-remove="onRemoveTab" v-model="state.activeTab">
|
||||
<el-tabs class="h100 w100" v-if="state.execResTabs.length > 0" @tab-remove="onRemoveTab" @tab-change="active" v-model="state.activeTab">
|
||||
<el-tab-pane class="h100" closable v-for="dt in state.execResTabs" :label="dt.id" :name="dt.id" :key="dt.id">
|
||||
<template #label>
|
||||
<el-popover :show-after="1000" placement="top-start" title="执行信息" trigger="hover" :width="300">
|
||||
@@ -700,6 +700,19 @@ const initMonacoEditor = () => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const active = () => {
|
||||
const resTab = state.execResTabs[state.activeTab - 1];
|
||||
if (!resTab || !resTab.dbTableRef) {
|
||||
return;
|
||||
}
|
||||
|
||||
resTab.dbTableRef?.active();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
active,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
:clearable="false"
|
||||
type="Date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
:placeholder="`选择日期-${placeholder}`"
|
||||
/>
|
||||
|
||||
<el-date-picker
|
||||
@@ -41,7 +41,7 @@
|
||||
:clearable="false"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="选择日期时间"
|
||||
:placeholder="`选择日期时间-${placeholder}`"
|
||||
/>
|
||||
|
||||
<el-time-picker
|
||||
@@ -56,7 +56,7 @@
|
||||
v-model="itemValue"
|
||||
:clearable="false"
|
||||
value-format="HH:mm:ss"
|
||||
placeholder="选择时间"
|
||||
:placeholder="`选择时间-${placeholder}`"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -152,6 +152,10 @@ const getEditorLangByValue = (value: any) => {
|
||||
<style lang="scss">
|
||||
.string-input-container {
|
||||
position: relative;
|
||||
|
||||
.el-input__wrapper {
|
||||
padding: 1px 3px;
|
||||
}
|
||||
}
|
||||
.string-input-container-show-icon {
|
||||
.el-input__inner {
|
||||
@@ -174,6 +178,10 @@ const getEditorLangByValue = (value: any) => {
|
||||
.el-input__prefix {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-input__wrapper {
|
||||
padding: 1px 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-time-picker-popper {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
fixed
|
||||
class="table"
|
||||
:row-event-handlers="rowEventHandlers"
|
||||
@scroll="onTableScroll"
|
||||
>
|
||||
<template #header="{ columns }">
|
||||
<div v-for="(column, i) in columns" :key="i">
|
||||
@@ -59,9 +60,7 @@
|
||||
</div>
|
||||
|
||||
<div v-else class="header-column-title">
|
||||
<b class="el-text">
|
||||
{{ column.title }}
|
||||
</b>
|
||||
<b class="el-text"> {{ column.title }} </b>
|
||||
</div>
|
||||
|
||||
<!-- 字段列右部分内容 -->
|
||||
@@ -96,7 +95,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else :class="isUpdated(rowIndex, column.dataKey) ? 'update_field_active' : ''">
|
||||
<div v-else :class="isUpdated(rowIndex, column.dataKey) ? 'update_field_active ml2 mr2' : 'ml2 mr2'">
|
||||
<span v-if="rowData[column.dataKey!] === null" style="color: var(--el-color-info-light-5)"> NULL </span>
|
||||
|
||||
<span v-else :title="rowData[column.dataKey!]" class="el-text el-text--small is-truncated">
|
||||
@@ -121,7 +120,7 @@
|
||||
|
||||
<template #empty>
|
||||
<div style="text-align: center">
|
||||
<el-empty class="h100" :description="props.emptyText" :image-size="100" />
|
||||
<el-empty :description="props.emptyText" :image-size="100" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-v2>
|
||||
@@ -157,7 +156,7 @@
|
||||
import { onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue';
|
||||
import { ElInput, ElMessage } from 'element-plus';
|
||||
import { copyToClipboard } from '@/common/utils/string';
|
||||
import { DbInst } from '@/views/ops/db/db';
|
||||
import { DbInst, DbThemeConfig } from '@/views/ops/db/db';
|
||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { exportCsv, exportFile } from '@/common/utils/export';
|
||||
@@ -259,12 +258,10 @@ const cmDataDel = new ContextmenuItem('deleteData', '删除')
|
||||
return state.table == '';
|
||||
});
|
||||
|
||||
const cmDataEdit = new ContextmenuItem('editData', '编辑行')
|
||||
.withIcon('edit')
|
||||
.withOnClick(() => onEditRowData())
|
||||
.withHideFunc(() => {
|
||||
return state.table == '';
|
||||
});
|
||||
const cmFormView = new ContextmenuItem('formView', '表单视图').withIcon('Document').withOnClick(() => onEditRowData());
|
||||
// .withHideFunc(() => {
|
||||
// return state.table == '';
|
||||
// });
|
||||
|
||||
const cmDataGenInsertSql = new ContextmenuItem('genInsertSql', 'Insert SQL')
|
||||
.withIcon('tickets')
|
||||
@@ -364,7 +361,7 @@ const state = reactive({
|
||||
|
||||
const { tableHeight, datas } = toRefs(state);
|
||||
|
||||
const dbConfig = useStorage('dbConfig', { showColumnComment: false });
|
||||
const dbConfig = useStorage('dbConfig', DbThemeConfig);
|
||||
|
||||
/**
|
||||
* 行号字段列
|
||||
@@ -486,7 +483,7 @@ const setTableColumns = (columns: any) => {
|
||||
dataKey: columnName,
|
||||
width: DbInst.flexColumnWidth(columnName, state.datas),
|
||||
title: columnName,
|
||||
align: 'center',
|
||||
align: x.dataType == DataType.Number ? 'right' : 'left',
|
||||
headerClass: 'table-column',
|
||||
class: 'table-column',
|
||||
sortable: true,
|
||||
@@ -596,7 +593,7 @@ const dataContextmenuClick = (event: any, rowIndex: number, column: any, data: a
|
||||
const { clientX, clientY } = event;
|
||||
state.contextmenu.dropdown.x = clientX;
|
||||
state.contextmenu.dropdown.y = clientY;
|
||||
state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmDataEdit, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql];
|
||||
state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmFormView, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql];
|
||||
contextmenuRef.value.openContextmenu({ column, rowData: data });
|
||||
};
|
||||
|
||||
@@ -628,12 +625,12 @@ const onDeleteData = async () => {
|
||||
const onEditRowData = () => {
|
||||
const selectionDatas = Array.from(selectionRowsMap.values());
|
||||
if (selectionDatas.length > 1) {
|
||||
ElMessage.warning('只能编辑一行数据');
|
||||
ElMessage.warning('只能选择一行数据');
|
||||
return;
|
||||
}
|
||||
const data = selectionDatas[0];
|
||||
state.tableDataFormDialog.data = { ...data };
|
||||
state.tableDataFormDialog.title = `编辑表'${props.table}'数据`;
|
||||
state.tableDataFormDialog.title = state.table ? `'${props.table}'表单数据` : '表单视图';
|
||||
state.tableDataFormDialog.visible = true;
|
||||
};
|
||||
|
||||
@@ -649,7 +646,7 @@ const onGenerateJson = async () => {
|
||||
// 按列字段重新排序对象key
|
||||
const jsonObj = [];
|
||||
for (let selectionData of selectionDatas) {
|
||||
let obj = {};
|
||||
let obj: any = {};
|
||||
for (let column of state.columns) {
|
||||
if (column.show) {
|
||||
obj[column.title] = selectionData[column.dataKey];
|
||||
@@ -753,7 +750,7 @@ const submitUpdateFields = async () => {
|
||||
|
||||
for (let updateRow of cellUpdateMap.values()) {
|
||||
const rowData = { ...updateRow.rowData };
|
||||
let updateColumnValue = {};
|
||||
let updateColumnValue: any = {};
|
||||
|
||||
for (let k of updateRow.columnsMap.keys()) {
|
||||
const v = updateRow.columnsMap.get(k);
|
||||
@@ -841,11 +838,23 @@ const triggerRefresh = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const scrollLeftValue = ref(0);
|
||||
const onTableScroll = (param: any) => {
|
||||
scrollLeftValue.value = param.scrollLeft;
|
||||
};
|
||||
/**
|
||||
* 激活表格,恢复滚动位置,否则会造成表头与数据单元格错位(暂不知为啥,先这样解决)
|
||||
*/
|
||||
const active = () => {
|
||||
setTimeout(() => tableRef.value.scrollToLeft(scrollLeftValue.value));
|
||||
};
|
||||
|
||||
const getNowDbInst = () => {
|
||||
return DbInst.getInst(state.dbId);
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
active,
|
||||
submitUpdateFields,
|
||||
cancelUpdateFields,
|
||||
});
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
:key="column.columnName"
|
||||
class="w100 mb5"
|
||||
:prop="column.columnName"
|
||||
:required="!column.nullable && !column.isPrimaryKey && !column.isIdentity"
|
||||
:required="props.tableName != '' && !column.nullable && !column.isPrimaryKey && !column.isIdentity"
|
||||
>
|
||||
<template #label>
|
||||
<span class="pointer" :title="`${column.columnType} | ${column.columnComment}`">
|
||||
<span class="pointer" :title="column?.columnComment ? `${column.columnType} | ${column.columnComment}` : column.columnType">
|
||||
{{ column.columnName }}
|
||||
</span>
|
||||
</template>
|
||||
@@ -17,13 +17,13 @@
|
||||
<ColumnFormItem
|
||||
v-model="modelValue[`${column.columnName}`]"
|
||||
:data-type="dbInst.getDialect().getDataType(column.dataType)"
|
||||
:placeholder="`${column.columnType} ${column.columnComment}`"
|
||||
:placeholder="column?.columnComment ? `${column.columnType} | ${column.columnComment}` : column.columnType"
|
||||
:column-name="column.columnName"
|
||||
:disabled="column.isIdentity"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<template #footer v-if="props.tableName">
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="confirm">确定</el-button>
|
||||
@@ -99,7 +99,7 @@ const confirm = async () => {
|
||||
|
||||
let sql = '';
|
||||
if (oldValue) {
|
||||
const updateColumnValue = {};
|
||||
const updateColumnValue: any = {};
|
||||
Object.keys(oldValue).forEach((key) => {
|
||||
// 如果新旧值不相等,则为需要更新的字段
|
||||
if (oldValue[key] !== modelValue.value[key]) {
|
||||
|
||||
@@ -50,22 +50,6 @@
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<!-- 表数据展示配置 -->
|
||||
<el-popover
|
||||
popper-style="max-height: 550px; overflow: auto; max-width: 450px"
|
||||
placement="bottom"
|
||||
width="auto"
|
||||
title="展示配置"
|
||||
trigger="click"
|
||||
>
|
||||
<el-checkbox v-model="dbConfig.showColumnComment" label="显示字段备注" :true-value="true" :false-value="false" size="small" />
|
||||
<template #reference>
|
||||
<el-link type="primary" icon="setting" :underline="false"></el-link>
|
||||
</template>
|
||||
</el-popover>
|
||||
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip :show-after="500" v-if="hasUpdatedFileds" class="box-item" effect="dark" content="提交修改" placement="top">
|
||||
<el-link @click="submitUpdateFields()" type="success" :underline="false" class="font12">提交</el-link>
|
||||
</el-tooltip>
|
||||
@@ -258,8 +242,8 @@ import { DbInst } from '@/views/ops/db/db';
|
||||
import DbTableData from './DbTableData.vue';
|
||||
import { DbDialect } from '@/views/ops/db/dialect';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { useEventListener, useStorage } from '@vueuse/core';
|
||||
import { copyToClipboard } from '@/common/utils/string';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import { copyToClipboard, fuzzyMatchField } from '@/common/utils/string';
|
||||
import DbTableDataForm from './DbTableDataForm.vue';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -288,8 +272,6 @@ const condDialogInputRef: Ref = ref(null);
|
||||
|
||||
const defaultPageSize = DbInst.DefaultLimit;
|
||||
|
||||
const dbConfig = useStorage('dbConfig', { showColumnComment: false });
|
||||
|
||||
const state = reactive({
|
||||
datas: [],
|
||||
sql: '', // 当前数据tab执行的sql
|
||||
@@ -476,10 +458,7 @@ const getColumnTips = (queryString: string, callback: any) => {
|
||||
|
||||
let res = [];
|
||||
if (columnNameSearch) {
|
||||
columnNameSearch = columnNameSearch.toLowerCase();
|
||||
res = columns.filter((data: any) => {
|
||||
return data.columnName.toLowerCase().includes(columnNameSearch);
|
||||
});
|
||||
res = fuzzyMatchField(columnNameSearch, columns, (x: any) => x.columnName);
|
||||
}
|
||||
|
||||
completeCond = condition.value;
|
||||
@@ -534,10 +513,12 @@ const filterColumns = (searchKey: string) => {
|
||||
if (!searchKey) {
|
||||
return columns;
|
||||
}
|
||||
searchKey = searchKey.toLowerCase();
|
||||
return columns.filter((data: any) => {
|
||||
return data.columnName.toLowerCase().includes(searchKey) || data.columnComment.toLowerCase().includes(searchKey);
|
||||
});
|
||||
return fuzzyMatchField(
|
||||
searchKey,
|
||||
columns,
|
||||
(x: any) => x.columnName,
|
||||
(x: any) => x.columnComment
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -622,6 +603,10 @@ const onShowAddDataDialog = async () => {
|
||||
state.addDataDialog.title = `添加'${props.tableName}'表数据`;
|
||||
state.addDataDialog.visible = true;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
active: () => dbTableRef.value.active(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -131,6 +131,7 @@ import { compatibleMysql, editDbTypes, getDbDialect } from '../../dialect/index'
|
||||
import { DbInst } from '../../db';
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import { format as sqlFormatter } from 'sql-formatter';
|
||||
import { fuzzyMatchField } from '@/common/utils/string';
|
||||
|
||||
const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue'));
|
||||
|
||||
@@ -219,17 +220,11 @@ const filterTableInfos = computed(() => {
|
||||
if (!tableNameSearch && !tableCommentSearch) {
|
||||
return tables;
|
||||
}
|
||||
return tables.filter((data: any) => {
|
||||
let tnMatch = true;
|
||||
let tcMatch = true;
|
||||
if (tableNameSearch) {
|
||||
tnMatch = data.tableName.toLowerCase().includes(tableNameSearch.toLowerCase());
|
||||
}
|
||||
if (tableCommentSearch) {
|
||||
tcMatch = data.tableComment.includes(tableCommentSearch);
|
||||
}
|
||||
return tnMatch && tcMatch;
|
||||
});
|
||||
|
||||
if (tableNameSearch) {
|
||||
return fuzzyMatchField(tableNameSearch, tables, (table: any) => table.tableName);
|
||||
}
|
||||
return fuzzyMatchField(tableCommentSearch, tables, (table: any) => table.tableComment);
|
||||
});
|
||||
|
||||
const getTables = async () => {
|
||||
|
||||
@@ -450,8 +450,8 @@ export class DbInst {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取列名称的长度 加上排序图标长度、abc为字段类型简称占位符
|
||||
const columnWidth: number = getTextWidth(prop + 'abc') + 23;
|
||||
// 获取列名称的长度 加上排序图标长度、abc为字段类型简称占位符、排序图标等
|
||||
const columnWidth: number = getTextWidth(prop + 'abc') + 10;
|
||||
// prop为该列的字段名(传字符串);tableData为该表格的数据源(传变量);
|
||||
if (!tableData || !tableData.length || tableData.length === 0 || tableData === undefined) {
|
||||
return columnWidth;
|
||||
@@ -471,7 +471,7 @@ export class DbInst {
|
||||
maxWidthText = nowText;
|
||||
}
|
||||
}
|
||||
const contentWidth: number = getTextWidth(maxWidthText) + 15;
|
||||
const contentWidth: number = getTextWidth(maxWidthText) + 3;
|
||||
const flexWidth: number = contentWidth > columnWidth ? contentWidth : columnWidth;
|
||||
return flexWidth > 500 ? 500 : flexWidth;
|
||||
};
|
||||
@@ -601,6 +601,11 @@ export class TabInfo {
|
||||
*/
|
||||
params: any;
|
||||
|
||||
/**
|
||||
* 组件ref
|
||||
*/
|
||||
componentRef: any;
|
||||
|
||||
getNowDbInst() {
|
||||
return DbInst.getInst(this.dbId);
|
||||
}
|
||||
@@ -837,3 +842,18 @@ function getTableName4SqlCtx(sql: string, alias: string = '', defaultDb: string)
|
||||
return tables.length > 0 ? tables[0] : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库主题配置
|
||||
*/
|
||||
export const DbThemeConfig = {
|
||||
/**
|
||||
* 表数据表头是否显示备注
|
||||
*/
|
||||
showColumnComment: true,
|
||||
|
||||
/**
|
||||
* 是否自动定位至树节点
|
||||
*/
|
||||
locationTreeNode: true,
|
||||
};
|
||||
|
||||
@@ -69,11 +69,11 @@ export enum DataType {
|
||||
}
|
||||
|
||||
/** 列数据类型角标 */
|
||||
export const ColumnTypeSubscript = {
|
||||
export const ColumnTypeSubscript: any = {
|
||||
/** 字符串 */
|
||||
string: 'abc',
|
||||
string: 'ab',
|
||||
/** 数字 */
|
||||
number: '123',
|
||||
number: '12',
|
||||
/** 日期 */
|
||||
date: 'icon-clock',
|
||||
/** 时间 */
|
||||
|
||||
@@ -34,20 +34,15 @@
|
||||
|
||||
<Pane>
|
||||
<div class="machine-terminal-tabs card pd5">
|
||||
<el-tabs
|
||||
v-if="state.tabs.size > 0"
|
||||
type="card"
|
||||
@tab-remove="onRemoveTab"
|
||||
@tab-change="onTabChange"
|
||||
style="width: 100%"
|
||||
v-model="state.activeTermName"
|
||||
class="h100"
|
||||
>
|
||||
<el-tabs v-if="state.tabs.size > 0" type="card" @tab-remove="onRemoveTab" style="width: 100%" v-model="state.activeTermName" class="h100">
|
||||
<el-tab-pane class="h100" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
|
||||
<template #label>
|
||||
<el-popconfirm @confirm="handleReconnect(dt, true)" title="确认重新连接?">
|
||||
<template #reference>
|
||||
<el-icon class="mr5" :color="dt.status == 1 ? '#67c23a' : '#f56c6c'" :title="dt.status == 1 ? '' : '点击重连'"
|
||||
<el-icon
|
||||
class="mr5"
|
||||
:color="EnumValue.getEnumByValue(TerminalStatusEnum, dt.status)?.extra?.iconColor"
|
||||
:title="dt.status == TerminalStatusEnum.Connected.value ? '' : '点击重连'"
|
||||
><Connection />
|
||||
</el-icon>
|
||||
</template>
|
||||
@@ -62,7 +57,7 @@
|
||||
<el-descriptions :column="1" size="small">
|
||||
<el-descriptions-item label="机器名"> {{ dt.params?.name }} </el-descriptions-item>
|
||||
<el-descriptions-item label="host"> {{ dt.params?.ip }} : {{ dt.params?.port }} </el-descriptions-item>
|
||||
<el-descriptions-item label="username"> {{ dt.params?.username }} </el-descriptions-item>
|
||||
<el-descriptions-item label="username"> {{ dt.params?.selectAuthCert.username }} </el-descriptions-item>
|
||||
<el-descriptions-item label="remark"> {{ dt.params?.remark }} </el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
@@ -165,13 +160,14 @@ import TagTree from '../component/TagTree.vue';
|
||||
import { Pane, Splitpanes } from 'splitpanes';
|
||||
import { ContextmenuItem } from '@/components/contextmenu/index';
|
||||
import TerminalBody from '@/components/terminal/TerminalBody.vue';
|
||||
import { TerminalStatus } from '@/components/terminal/common';
|
||||
import { TerminalStatus, TerminalStatusEnum } from '@/components/terminal/common';
|
||||
import MachineRdp from '@/components/terminal-rdp/MachineRdp.vue';
|
||||
import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
|
||||
import ResourceTags from '../component/ResourceTags.vue';
|
||||
import { MachineProtocolEnum } from './enums';
|
||||
import { useAutoOpenResource } from '@/store/autoOpenResource';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import EnumValue from '@/common/Enum';
|
||||
|
||||
// 组件
|
||||
const ScriptManage = defineAsyncComponent(() => import('./ScriptManage.vue'));
|
||||
@@ -340,8 +336,13 @@ watch(
|
||||
watch(
|
||||
() => state.activeTermName,
|
||||
(newValue, oldValue) => {
|
||||
fitTerminal();
|
||||
|
||||
oldValue && terminalRefs[oldValue]?.blur && terminalRefs[oldValue]?.blur();
|
||||
terminalRefs[newValue]?.focus && terminalRefs[newValue]?.focus();
|
||||
|
||||
const nowTab = state.tabs.get(state.activeTermName);
|
||||
tagTreeRef.value.setCurrentKey(nowTab?.authCert);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -496,20 +497,27 @@ const onRemoveTab = (targetName: string) => {
|
||||
if (tabName !== targetName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
state.tabs.delete(targetName);
|
||||
let info = state.tabs.get(targetName);
|
||||
if (info) {
|
||||
terminalRefs[info.key]?.close();
|
||||
}
|
||||
|
||||
if (activeTermName != targetName) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 如果删除的tab是当前激活的tab,则切换到前一个或后一个tab
|
||||
const nextTab = tabNames[i + 1] || tabNames[i - 1];
|
||||
if (nextTab) {
|
||||
activeTermName = nextTab;
|
||||
} else {
|
||||
activeTermName = '';
|
||||
}
|
||||
let info = state.tabs.get(targetName);
|
||||
if (info) {
|
||||
terminalRefs[info.key]?.close();
|
||||
}
|
||||
|
||||
state.tabs.delete(targetName);
|
||||
state.activeTermName = activeTermName;
|
||||
onTabChange();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -535,21 +543,13 @@ const onResizeTagTree = () => {
|
||||
fitTerminal();
|
||||
};
|
||||
|
||||
const onTabChange = () => {
|
||||
fitTerminal();
|
||||
|
||||
const nowTab = state.tabs.get(state.activeTermName);
|
||||
tagTreeRef.value.setCurrentKey(nowTab?.authCert);
|
||||
};
|
||||
|
||||
const fitTerminal = () => {
|
||||
setTimeout(() => {
|
||||
let info = state.tabs.get(state.activeTermName);
|
||||
if (info) {
|
||||
terminalRefs[info.key]?.fitTerminal && terminalRefs[info.key]?.fitTerminal();
|
||||
terminalRefs[info.key]?.focus && terminalRefs[info.key]?.focus();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
|
||||
const handleReconnect = (tab: any, force = false) => {
|
||||
|
||||
@@ -12,6 +12,8 @@ export const machineApi = {
|
||||
process: Api.newGet('/machines/{id}/process'),
|
||||
// 终止进程
|
||||
killProcess: Api.newDelete('/machines/{id}/process'),
|
||||
users: Api.newGet('/machines/{id}/users'),
|
||||
groups: Api.newGet('/machines/{id}/groups'),
|
||||
testConn: Api.newPost('/machines/test-conn'),
|
||||
// 保存按钮
|
||||
saveMachine: Api.newPost('/machines'),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<el-dialog
|
||||
:title="title"
|
||||
v-model="dialogVisible"
|
||||
@open="search()"
|
||||
:close-on-click-modal="false"
|
||||
:before-close="cancel"
|
||||
:show-close="true"
|
||||
@@ -27,7 +28,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, ref, toRefs, reactive, Ref } from 'vue';
|
||||
import { ref, toRefs, reactive, Ref } from 'vue';
|
||||
import { cronJobApi } from '../api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
@@ -47,8 +48,6 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:visible', 'update:data', 'cancel']);
|
||||
|
||||
const searchItems = [SearchItem.input('machineCode', '机器编号'), SearchItem.select('status', '状态').withEnum(CronJobExecStatusEnum)];
|
||||
|
||||
const columns = ref([
|
||||
@@ -65,7 +64,7 @@ const state = reactive({
|
||||
tags: [] as any,
|
||||
params: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 8,
|
||||
cronJobId: 0,
|
||||
status: null,
|
||||
machineCode: '',
|
||||
@@ -78,24 +77,17 @@ const state = reactive({
|
||||
machines: [],
|
||||
});
|
||||
|
||||
const { dialogVisible, params } = toRefs(state);
|
||||
const { params } = toRefs(state);
|
||||
|
||||
watch(props, async (newValue: any) => {
|
||||
state.dialogVisible = newValue.visible;
|
||||
if (!newValue.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.params.cronJobId = props.data?.id;
|
||||
setTimeout(() => search(), 300);
|
||||
});
|
||||
const dialogVisible = defineModel<boolean>('visible');
|
||||
|
||||
const search = async () => {
|
||||
state.params.cronJobId = props.data?.id;
|
||||
pageTableRef.value.search();
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
emit('update:visible', false);
|
||||
dialogVisible.value = false;
|
||||
setTimeout(() => {
|
||||
initData();
|
||||
}, 500);
|
||||
|
||||
@@ -18,12 +18,12 @@
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="path" label="路径" min-width="150px" show-overflow-tooltip>
|
||||
<el-table-column prop="path" label="路径" min-width="180" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row.path" :disabled="scope.row.id != null" clearable> </el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-wdith="120px">
|
||||
<el-table-column label="操作" min-width="130">
|
||||
<template #default="scope">
|
||||
<el-button v-if="scope.row.id == null" @click="addFiles(scope.row)" type="success" icon="success-filled" plain></el-button>
|
||||
<el-button v-if="scope.row.id != null" @click="getConf(scope.row)" type="primary" icon="tickets" plain></el-button>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
>
|
||||
<el-table-column type="selection" width="30" />
|
||||
|
||||
<el-table-column prop="name" label="名称">
|
||||
<el-table-column prop="name" label="名称" min-width="380">
|
||||
<template #header>
|
||||
<div class="machine-file-table-header">
|
||||
<div>
|
||||
@@ -171,7 +171,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="size" label="大小" width="100" sortable>
|
||||
<el-table-column prop="size" label="大小" min-width="90" sortable>
|
||||
<template #default="scope">
|
||||
<span style="color: #67c23a; font-weight: bold" v-if="scope.row.type == '-'"> {{ formatByteSize(scope.row.size) }} </span>
|
||||
<span style="color: #67c23a; font-weight: bold" v-if="scope.row.type == 'd' && scope.row.dirSize"> {{ scope.row.dirSize }} </span>
|
||||
@@ -182,7 +182,11 @@
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="mode" label="属性" width="110"> </el-table-column>
|
||||
<el-table-column prop="modTime" label="修改时间" width="165" sortable> </el-table-column>
|
||||
<el-table-column v-if="$props.protocol == MachineProtocolEnum.Ssh.value" prop="username" label="用户" min-width="55" show-overflow-tooltip>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="$props.protocol == MachineProtocolEnum.Ssh.value" prop="groupname" label="组" min-width="55" show-overflow-tooltip>
|
||||
</el-table-column>
|
||||
<el-table-column prop="modTime" label="修改时间" width="160" sortable> </el-table-column>
|
||||
|
||||
<el-table-column width="100">
|
||||
<template #header>
|
||||
@@ -288,6 +292,8 @@ import MachineFileContent from './MachineFileContent.vue';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
import { convertToBytes, formatByteSize } from '@/common/utils/format';
|
||||
import { getMachineConfig } from '@/common/sysconfig';
|
||||
import { MachineProtocolEnum } from '../enums';
|
||||
import { fuzzyMatchField } from '@/common/utils/string';
|
||||
|
||||
const props = defineProps({
|
||||
machineId: { type: Number },
|
||||
@@ -303,6 +309,9 @@ const folderUploadRef: any = ref();
|
||||
|
||||
const folderType = 'd';
|
||||
|
||||
const userMap = new Map<number, any>();
|
||||
const groupMap = new Map<number, any>();
|
||||
|
||||
// 路径分隔符
|
||||
const pathSep = '/';
|
||||
|
||||
@@ -343,13 +352,27 @@ const { basePath, nowPath, loading, fileNameFilter, progressNum, uploadProgressS
|
||||
|
||||
onMounted(async () => {
|
||||
state.basePath = props.path;
|
||||
const machineId = props.machineId;
|
||||
|
||||
if (props.protocol == MachineProtocolEnum.Ssh.value) {
|
||||
machineApi.users.request({ id: machineId }).then((res: any) => {
|
||||
for (let user of res) {
|
||||
userMap.set(user.uid, user);
|
||||
}
|
||||
});
|
||||
|
||||
machineApi.groups.request({ id: machineId }).then((res: any) => {
|
||||
for (let group of res) {
|
||||
groupMap.set(group.gid, group);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setFiles(props.path);
|
||||
state.machineConfig = await getMachineConfig();
|
||||
});
|
||||
|
||||
const filterFiles = computed(() =>
|
||||
state.files.filter((data: any) => !state.fileNameFilter || data.name.toLowerCase().includes(state.fileNameFilter.toLowerCase()))
|
||||
);
|
||||
const filterFiles = computed(() => fuzzyMatchField(state.fileNameFilter, state.files, (file: any) => file.name));
|
||||
|
||||
const filePathNav = computed(() => {
|
||||
let basePath = state.basePath;
|
||||
@@ -517,6 +540,11 @@ const lsFile = async (path: string) => {
|
||||
path,
|
||||
});
|
||||
for (const file of res) {
|
||||
if (props.protocol == MachineProtocolEnum.Ssh.value) {
|
||||
file.username = userMap.get(file.uid)?.uname || file.uid;
|
||||
file.groupname = groupMap.get(file.gid)?.gname || file.gid;
|
||||
}
|
||||
|
||||
const type = file.type;
|
||||
if (type == folderType) {
|
||||
file.isFolder = true;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="codePaths" label="关联机器" min-width="250px" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<TagCodePath :path="scope.row.tags.map((tag: any) => tag.codePath)" />
|
||||
<TagCodePath :path="scope.row.tags?.map((tag: any) => tag.codePath)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" show-overflow-tooltip width="120px"> </el-table-column>
|
||||
@@ -173,7 +173,7 @@ const openFormDialog = (data: any) => {
|
||||
state.form = { ...DefaultForm };
|
||||
} else {
|
||||
state.form = _.cloneDeep(data);
|
||||
state.form.codePaths = data.tags.map((tag: any) => tag.codePath);
|
||||
state.form.codePaths = data.tags?.map((tag: any) => tag.codePath);
|
||||
}
|
||||
state.dialogVisible = true;
|
||||
};
|
||||
|
||||
@@ -156,6 +156,7 @@ import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||
import EnumValue from '@/common/Enum';
|
||||
import TagCodePath from '../component/TagCodePath.vue';
|
||||
import { isPrefixSubsequence } from '@/common/utils/string';
|
||||
|
||||
const MachineList = defineAsyncComponent(() => import('../machine/MachineList.vue'));
|
||||
const InstanceList = defineAsyncComponent(() => import('../db/InstanceList.vue'));
|
||||
@@ -371,8 +372,7 @@ const setNowTabData = () => {
|
||||
};
|
||||
|
||||
const filterNode = (value: string, data: Tree) => {
|
||||
if (!value) return true;
|
||||
return data.codePath.toLowerCase().includes(value) || data.name.includes(value);
|
||||
return !value || isPrefixSubsequence(value, data.codePath) || isPrefixSubsequence(value, data.name);
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
|
||||
@@ -100,31 +100,34 @@ const { dvisible, params, form } = toRefs(state);
|
||||
|
||||
const { isFetching: saveBtnLoading, execute: saveConfigExec } = configApi.save.useApi(form);
|
||||
|
||||
watchEffect(() => {
|
||||
state.dvisible = props.visible;
|
||||
if (!state.dvisible) {
|
||||
return;
|
||||
}
|
||||
watch(
|
||||
() => props.visible,
|
||||
() => {
|
||||
state.dvisible = props.visible;
|
||||
if (!state.dvisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.data) {
|
||||
state.form = { ...(props.data as any) };
|
||||
if (state.form.params) {
|
||||
state.params = JSON.parse(state.form.params);
|
||||
if (props.data) {
|
||||
state.form = { ...(props.data as any) };
|
||||
if (state.form.params) {
|
||||
state.params = JSON.parse(state.form.params);
|
||||
} else {
|
||||
state.params = [];
|
||||
}
|
||||
} else {
|
||||
state.form = { permission: 'all' } as any;
|
||||
state.params = [];
|
||||
}
|
||||
} else {
|
||||
state.form = { permission: 'all' } as any;
|
||||
state.params = [];
|
||||
}
|
||||
|
||||
if (state.form.permission != 'all') {
|
||||
const accounts = state.form.permission.split(',');
|
||||
state.permissionAccount = accounts.slice(0, accounts.length - 1);
|
||||
} else {
|
||||
state.permissionAccount = [];
|
||||
if (state.form.permission != 'all') {
|
||||
const accounts = state.form.permission.split(',');
|
||||
state.permissionAccount = accounts.slice(0, accounts.length - 1);
|
||||
} else {
|
||||
state.permissionAccount = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
const cancel = () => {
|
||||
// 更新父组件visible prop对应的值为false
|
||||
|
||||
@@ -123,6 +123,7 @@ import { formatDate } from '@/common/utils/format';
|
||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||
import { Splitpanes, Pane } from 'splitpanes';
|
||||
import { isPrefixSubsequence } from '@/common/utils/string';
|
||||
|
||||
const menuTypeValue = ResourceTypeEnum.Menu.value;
|
||||
const permissionTypeValue = ResourceTypeEnum.Permission.value;
|
||||
@@ -209,10 +210,7 @@ watch(filterResource, (val) => {
|
||||
});
|
||||
|
||||
const filterNode = (value: string, data: any) => {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
return data.name.includes(value);
|
||||
return !value || isPrefixSubsequence(value, data.name);
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
|
||||
@@ -3,9 +3,8 @@ module mayfly-go
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
gitee.com/chunanyong/dm v1.8.14
|
||||
gitee.com/chunanyong/dm v1.8.15
|
||||
gitee.com/liuzongyang/libpq v1.0.9
|
||||
github.com/buger/jsonparser v1.1.1
|
||||
github.com/emirpasic/gods v1.18.1
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
@@ -17,29 +16,30 @@ require (
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/kanzihuang/vitess/go/vt/sqlparser v0.0.0-20231018071450-ac8d9f0167e9
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230712084735-068dc2aee82d
|
||||
github.com/may-fly/cast v1.6.1
|
||||
github.com/microsoft/go-mssqldb v1.7.1
|
||||
github.com/microsoft/go-mssqldb v1.7.2
|
||||
github.com/mojocn/base64Captcha v1.3.6 // 验证码
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pkg/sftp v1.13.6
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/redis/go-redis/v9 v9.5.1
|
||||
github.com/redis/go-redis/v9 v9.5.3
|
||||
github.com/robfig/cron/v3 v3.0.1 // 定时任务
|
||||
github.com/sijms/go-ora/v2 v2.8.17
|
||||
github.com/sijms/go-ora/v2 v2.8.19
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.17.1
|
||||
github.com/veops/go-ansiterm v0.0.5
|
||||
go.mongodb.org/mongo-driver v1.15.0 // mongo
|
||||
golang.org/x/crypto v0.23.0 // ssh
|
||||
golang.org/x/oauth2 v0.20.0
|
||||
go.mongodb.org/mongo-driver v1.16.0 // mongo
|
||||
golang.org/x/crypto v0.25.0 // ssh
|
||||
golang.org/x/oauth2 v0.21.0
|
||||
golang.org/x/sync v0.7.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
// gorm
|
||||
gorm.io/driver/mysql v1.5.6
|
||||
gorm.io/gorm v1.25.10
|
||||
gorm.io/driver/mysql v1.5.7
|
||||
gorm.io/gorm v1.25.11
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -77,12 +77,14 @@ require (
|
||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/montanaflynn/stats v0.7.0 // indirect
|
||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.3 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
@@ -94,8 +96,8 @@ require (
|
||||
golang.org/x/exp v0.0.0-20230519143937-03e91628a987 // indirect
|
||||
golang.org/x/image v0.13.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230131230820-1c016267d619 // indirect
|
||||
google.golang.org/grpc v1.52.3 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
|
||||
@@ -57,10 +57,21 @@ func (d *Db) Dbs(rc *req.Ctx) {
|
||||
res, err := d.DbApp.GetPageList(queryCond, page, &dbvos)
|
||||
biz.ErrIsNil(err)
|
||||
|
||||
// 填充标签信息
|
||||
d.TagApp.FillTagInfo(tagentity.TagTypeDbName, collx.ArrayMap(dbvos, func(dbvo *vo.DbListVO) tagentity.ITagResource {
|
||||
return dbvo
|
||||
})...)
|
||||
instances, _ := d.InstanceApp.GetByIds(collx.ArrayMap(dbvos, func(i *vo.DbListVO) uint64 {
|
||||
return i.InstanceId
|
||||
}))
|
||||
instancesMap := collx.ArrayToMap(instances, func(i *entity.DbInstance) uint64 {
|
||||
return i.Id
|
||||
})
|
||||
for _, dbvo := range dbvos {
|
||||
di := instancesMap[dbvo.InstanceId]
|
||||
if di != nil {
|
||||
dbvo.InstanceCode = di.Code
|
||||
dbvo.InstanceType = di.Type
|
||||
dbvo.Host = di.Host
|
||||
dbvo.Port = di.Port
|
||||
}
|
||||
}
|
||||
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
@@ -2,26 +2,23 @@ package vo
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
tagentity "mayfly-go/internal/tag/domain/entity"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DbListVO struct {
|
||||
tagentity.ResourceTags
|
||||
|
||||
Id *int64 `json:"id"`
|
||||
Code string `json:"code"`
|
||||
Name *string `json:"name"`
|
||||
GetDatabaseMode entity.DbGetDatabaseMode `json:"getDatabaseMode"` // 获取数据库方式
|
||||
Database *string `json:"database"`
|
||||
Remark *string `json:"remark"`
|
||||
InstanceId uint64 `json:"instanceId"`
|
||||
AuthCertName string `json:"authCertName"`
|
||||
|
||||
InstanceId *int64 `json:"instanceId"`
|
||||
AuthCertName string `json:"authCertName"`
|
||||
InstanceName *string `json:"instanceName"`
|
||||
InstanceType *string `json:"type"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
InstanceCode string `json:"instanceCode" gorm:"-"`
|
||||
InstanceType string `json:"type" gorm:"-"`
|
||||
Host string `json:"host" gorm:"-"`
|
||||
Port int `json:"port" gorm:"-"`
|
||||
|
||||
CreateTime *time.Time `json:"createTime"`
|
||||
Creator *string `json:"creator"`
|
||||
@@ -30,7 +27,3 @@ type DbListVO struct {
|
||||
Modifier *string `json:"modifier"`
|
||||
ModifierId *int64 `json:"modifierId"`
|
||||
}
|
||||
|
||||
func (d DbListVO) GetCode() string {
|
||||
return d.Code
|
||||
}
|
||||
|
||||
@@ -297,7 +297,7 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
|
||||
|
||||
// 生成insert sql,数据在索引前,加速insert
|
||||
if reqParam.DumpData {
|
||||
writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- 表记录: %s \n-- ----------------------------\n", tableName))
|
||||
writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- 表数据: %s \n-- ----------------------------\n", tableName))
|
||||
|
||||
dumpHelper.BeforeInsert(writer, quoteTableName)
|
||||
// 获取列信息
|
||||
|
||||
@@ -2,7 +2,6 @@ package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"mayfly-go/internal/common/consts"
|
||||
"mayfly-go/internal/db/application/dto"
|
||||
"mayfly-go/internal/db/dbm"
|
||||
@@ -13,14 +12,11 @@ import (
|
||||
tagdto "mayfly-go/internal/tag/application/dto"
|
||||
tagentity "mayfly-go/internal/tag/domain/entity"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/structx"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Instance interface {
|
||||
@@ -71,17 +67,9 @@ func (app *instanceAppImpl) GetPageList(condition *entity.InstanceQuery, pagePar
|
||||
func (app *instanceAppImpl) TestConn(instanceEntity *entity.DbInstance, authCert *tagentity.ResourceAuthCert) error {
|
||||
instanceEntity.Network = instanceEntity.GetNetwork()
|
||||
|
||||
if authCert.Id != 0 {
|
||||
// 密文可能被清除,故需要重新获取
|
||||
authCert, _ = app.resourceAuthCertApp.GetAuthCert(authCert.Name)
|
||||
} else {
|
||||
if authCert.CiphertextType == tagentity.AuthCertCiphertextTypePublic {
|
||||
publicAuthCert, err := app.resourceAuthCertApp.GetAuthCert(authCert.Ciphertext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authCert = publicAuthCert
|
||||
}
|
||||
authCert, err := app.resourceAuthCertApp.GetRealAuthCert(authCert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dbConn, err := dbm.Conn(app.toDbInfoByAc(instanceEntity, authCert, ""))
|
||||
@@ -171,7 +159,7 @@ func (app *instanceAppImpl) SaveDbInstance(ctx context.Context, instance *dto.Sa
|
||||
}
|
||||
|
||||
func (app *instanceAppImpl) Delete(ctx context.Context, instanceId uint64) error {
|
||||
instance, err := app.GetById(instanceId, "name")
|
||||
instance, err := app.GetById(instanceId)
|
||||
if err != nil {
|
||||
return errorx.NewBiz("获取数据库实例错误,数据库实例ID为: %d", instance.Id)
|
||||
}
|
||||
@@ -180,26 +168,16 @@ func (app *instanceAppImpl) Delete(ctx context.Context, instanceId uint64) error
|
||||
DbInstanceId: instanceId,
|
||||
}
|
||||
err = app.restoreApp.restoreRepo.GetByCond(restore)
|
||||
switch {
|
||||
case err == nil:
|
||||
biz.ErrNotNil(err, "不能删除数据库实例【%s】,请先删除关联的数据库恢复任务。", instance.Name)
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
break
|
||||
default:
|
||||
biz.ErrIsNil(err, "删除数据库实例失败: %v", err)
|
||||
if err == nil {
|
||||
return errorx.NewBiz("不能删除数据库实例【%s】,请先删除关联的数据库恢复任务。", instance.Name)
|
||||
}
|
||||
|
||||
backup := &entity.DbBackup{
|
||||
DbInstanceId: instanceId,
|
||||
}
|
||||
err = app.backupApp.backupRepo.GetByCond(backup)
|
||||
switch {
|
||||
case err == nil:
|
||||
biz.ErrNotNil(err, "不能删除数据库实例【%s】,请先删除关联的数据库备份任务。", instance.Name)
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
break
|
||||
default:
|
||||
biz.ErrIsNil(err, "删除数据库实例失败: %v", err)
|
||||
if err == nil {
|
||||
return errorx.NewBiz("不能删除数据库实例【%s】,请先删除关联的数据库备份任务。", instance.Name)
|
||||
}
|
||||
|
||||
dbs, _ := app.dbApp.ListByCond(&entity.Db{
|
||||
|
||||
@@ -2,6 +2,7 @@ package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"mayfly-go/internal/db/dbm/dbi"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
@@ -87,16 +88,20 @@ func (app *dbTransferAppImpl) CreateLog(ctx context.Context, taskId uint64) (uin
|
||||
}
|
||||
|
||||
func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64, logId uint64) {
|
||||
defer app.logApp.Flush(logId, true)
|
||||
|
||||
task, err := app.GetById(taskId)
|
||||
if err != nil {
|
||||
logx.Errorf("创建DBMS-执行数据迁移日志失败:%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if app.IsRunning(taskId) {
|
||||
logx.Warnf("[%d]该任务正在运行中...", taskId)
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
defer app.logApp.Flush(logId, true)
|
||||
|
||||
// 修改状态与关联日志id
|
||||
task.LogId = logId
|
||||
task.RunningState = entity.DbTransferTaskRunStateRunning
|
||||
@@ -322,6 +327,8 @@ func (app *dbTransferAppImpl) transfer2Target(taskId uint64, targetConn *dbi.DbC
|
||||
columnNames = append(columnNames, targetMeta.QuoteIdentifier(col.ColumnName))
|
||||
}
|
||||
|
||||
dataHelper := targetMeta.GetDataHelper()
|
||||
|
||||
// 从目标库数据中取出源库字段对应的值
|
||||
values := make([][]any, 0)
|
||||
for _, record := range result {
|
||||
@@ -338,6 +345,14 @@ func (app *dbTransferAppImpl) transfer2Target(taskId uint64, targetConn *dbi.DbC
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if dataHelper.GetDataType(string(tc.DataType)) == dbi.DataTypeBlob {
|
||||
decodeBytes, err := hex.DecodeString(val.(string))
|
||||
if err == nil {
|
||||
val = decodeBytes
|
||||
}
|
||||
}
|
||||
|
||||
rawValue = append(rawValue, val)
|
||||
}
|
||||
values = append(values, rawValue)
|
||||
|
||||
@@ -214,6 +214,9 @@ func valueConvert(data []byte, colType *sql.ColumnType) any {
|
||||
if strings.Contains(colDatabaseTypeName, "bit") {
|
||||
return data[0]
|
||||
}
|
||||
if colDatabaseTypeName == "blob" {
|
||||
return fmt.Sprintf("%x", data)
|
||||
}
|
||||
|
||||
// 这里把[]byte数据转成string
|
||||
stringV := string(data)
|
||||
|
||||
@@ -28,17 +28,19 @@ type Dialect interface {
|
||||
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
|
||||
GetDbProgram() (DbProgram, error)
|
||||
|
||||
// 批量保存数据
|
||||
// BatchInsert 批量insert数据
|
||||
BatchInsert(tx *sql.Tx, tableName string, columns []string, values [][]any, duplicateStrategy int) (int64, error)
|
||||
|
||||
// 拷贝表
|
||||
// CopyTable 拷贝表
|
||||
CopyTable(copy *DbCopyTable) error
|
||||
|
||||
// CreateTable 创建表
|
||||
CreateTable(columns []Column, tableInfo Table, dropOldTable bool) (int, error)
|
||||
|
||||
// CreateIndex 创建索引
|
||||
CreateIndex(tableInfo Table, indexs []Index) error
|
||||
|
||||
// 有些数据库迁移完数据之后,需要更新表自增序列为当前表最大值
|
||||
// UpdateSequence 有些数据库迁移完数据之后,需要更新表自增序列为当前表最大值
|
||||
UpdateSequence(tableName string, columns []Column)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,37 +13,36 @@ import (
|
||||
type MetaData interface {
|
||||
BaseMetaData
|
||||
|
||||
// 获取数据库服务实例信息
|
||||
// GetDbServer 获取数据库服务实例信息
|
||||
GetDbServer() (*DbServer, error)
|
||||
|
||||
// 获取数据库名称列表
|
||||
// GetDbNames 获取数据库名称列表
|
||||
GetDbNames() ([]string, error)
|
||||
|
||||
// 获取表信息
|
||||
// GetTables 获取表信息
|
||||
GetTables(tableNames ...string) ([]Table, error)
|
||||
|
||||
// 获取指定表名的所有列元信息
|
||||
// GetColumns 获取指定表名的所有列元信息
|
||||
GetColumns(tableNames ...string) ([]Column, error)
|
||||
|
||||
// 根据数据库类型修复字段长度、精度等
|
||||
// FixColumn(column *Column)
|
||||
|
||||
// 获取表主键字段名,没有主键标识则默认第一个字段
|
||||
// GetPrimaryKey 获取表主键字段名,没有主键标识则默认第一个字段
|
||||
GetPrimaryKey(tableName string) (string, error)
|
||||
|
||||
// 获取表索引信息
|
||||
// GetTableIndex 获取表索引信息
|
||||
GetTableIndex(tableName string) ([]Index, error)
|
||||
|
||||
// 获取建表ddl
|
||||
// GetTableDDL 获取建表ddl
|
||||
GetTableDDL(tableName string, dropBeforeCreate bool) (string, error)
|
||||
|
||||
// GenerateTableDDL 生成建表ddl
|
||||
GenerateTableDDL(columns []Column, tableInfo Table, dropBeforeCreate bool) []string
|
||||
|
||||
// GenerateIndexDDL 生成索引ddl
|
||||
GenerateIndexDDL(indexs []Index, tableInfo Table) []string
|
||||
|
||||
GetSchemas() ([]string, error)
|
||||
|
||||
// 获取数据处理助手 用于解析格式化列数据等
|
||||
// GetDataHelper 获取数据处理助手 用于解析格式化列数据等
|
||||
GetDataHelper() DataHelper
|
||||
}
|
||||
|
||||
@@ -84,6 +83,13 @@ type Column struct {
|
||||
|
||||
// 拼接数据类型与长度等。如varchar(2000),decimal(20,2)
|
||||
func (c *Column) GetColumnType() string {
|
||||
// 哪些mysql数据类型不需要添加字段长度
|
||||
if collx.ArrayAnyMatches([]string{"int", "blob", "float", "double", "date", "year", "json"}, string(c.DataType)) {
|
||||
return string(c.DataType)
|
||||
}
|
||||
if c.DataType == "timestamp" {
|
||||
return "timestamp(6)"
|
||||
}
|
||||
if c.CharMaxLength > 0 {
|
||||
return fmt.Sprintf("%s(%d)", c.DataType, c.CharMaxLength)
|
||||
}
|
||||
@@ -124,6 +130,7 @@ const (
|
||||
CommonTypeVarbinary ColumnDataType = "varbinary"
|
||||
|
||||
CommonTypeInt ColumnDataType = "int"
|
||||
CommonTypeBit ColumnDataType = "bit"
|
||||
CommonTypeSmallint ColumnDataType = "smallint"
|
||||
CommonTypeTinyint ColumnDataType = "tinyint"
|
||||
CommonTypeNumber ColumnDataType = "number"
|
||||
@@ -146,6 +153,7 @@ const (
|
||||
DataTypeDate DataType = "date"
|
||||
DataTypeTime DataType = "time"
|
||||
DataTypeDateTime DataType = "datetime"
|
||||
DataTypeBlob DataType = "blob"
|
||||
)
|
||||
|
||||
// 列数据处理帮助方法
|
||||
|
||||
@@ -42,13 +42,13 @@ SELECT a.indexname AS "i
|
||||
indexdef AS "indexDef",
|
||||
c.attname AS "columnName",
|
||||
c.attnum AS "seqInIndex",
|
||||
case when a.indexname like '%_pkey' then 1 else 0 end AS "isPrimaryKey"
|
||||
case when a.indexname like '%%_pkey' then 1 else 0 end AS "isPrimaryKey"
|
||||
FROM pg_indexes a
|
||||
join pg_class b on a.indexname = b.relname
|
||||
join pg_attribute c on b.oid = c.attrelid
|
||||
WHERE a.schemaname = (select current_schema())
|
||||
AND a.tablename = '%s'
|
||||
AND a.indexname not like '%_pkey'
|
||||
AND a.indexname not like '%%_pkey'
|
||||
---------------------------------------
|
||||
--PGSQL_COLUMN_MA 表列信息
|
||||
SELECT a.table_name AS "tableName",
|
||||
|
||||
@@ -19,6 +19,8 @@ var (
|
||||
// 时间类型
|
||||
timeRegexp = regexp.MustCompile(`(?i)time`)
|
||||
|
||||
blobRegexp = regexp.MustCompile(`(?i)blob`)
|
||||
|
||||
// mysql数据类型 映射 公共数据类型
|
||||
commonColumnTypeMap = map[string]dbi.ColumnDataType{
|
||||
"bigint": dbi.CommonTypeBigint,
|
||||
@@ -37,6 +39,7 @@ var (
|
||||
"longtext": dbi.CommonTypeLongtext,
|
||||
"mediumblob": dbi.CommonTypeBlob,
|
||||
"mediumtext": dbi.CommonTypeText,
|
||||
"bit": dbi.CommonTypeBit,
|
||||
"set": dbi.CommonTypeVarchar,
|
||||
"smallint": dbi.CommonTypeSmallint,
|
||||
"text": dbi.CommonTypeText,
|
||||
@@ -60,6 +63,7 @@ var (
|
||||
dbi.CommonTypeMediumtext: "text",
|
||||
dbi.CommonTypeVarbinary: "varbinary",
|
||||
dbi.CommonTypeInt: "int",
|
||||
dbi.CommonTypeBit: "bit",
|
||||
dbi.CommonTypeSmallint: "smallint",
|
||||
dbi.CommonTypeTinyint: "tinyint",
|
||||
dbi.CommonTypeNumber: "decimal",
|
||||
@@ -92,6 +96,10 @@ func (dc *DataHelper) GetDataType(dbColumnType string) dbi.DataType {
|
||||
if timeRegexp.MatchString(dbColumnType) {
|
||||
return dbi.DataTypeTime
|
||||
}
|
||||
// blob类型
|
||||
if blobRegexp.MatchString(dbColumnType) {
|
||||
return dbi.DataTypeBlob
|
||||
}
|
||||
return dbi.DataTypeString
|
||||
}
|
||||
|
||||
@@ -157,6 +165,8 @@ func (dc *DataHelper) WrapValue(dbColumnValue any, dataType dbi.DataType) string
|
||||
case dbi.DataTypeDate, dbi.DataTypeDateTime, dbi.DataTypeTime:
|
||||
// mysql时间类型无需格式化
|
||||
return fmt.Sprintf("'%s'", dbColumnValue)
|
||||
case dbi.DataTypeBlob:
|
||||
return fmt.Sprintf("unhex('%s')", dbColumnValue)
|
||||
}
|
||||
return fmt.Sprintf("'%s'", dbColumnValue)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ type DbInstance struct {
|
||||
Network string `json:"network"`
|
||||
Extra *string `json:"extra"` // 连接需要的其他额外参数(json格式), 如oracle需要sid等
|
||||
Params *string `json:"params"` // 使用指针类型,可更新为零值(空字符串)
|
||||
Remark string `json:"remark"`
|
||||
Remark *string `json:"remark"`
|
||||
SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id
|
||||
}
|
||||
|
||||
|
||||
@@ -27,16 +27,10 @@ type DbTransferLogQuery struct {
|
||||
|
||||
// 数据库查询实体,不与数据库表字段一一对应
|
||||
type DbQuery struct {
|
||||
Id uint64 `form:"id"`
|
||||
Code string `json:"code" form:"code"`
|
||||
Name string `orm:"column(name)" json:"name"`
|
||||
Database string `orm:"column(database)" json:"database"`
|
||||
Remark string `json:"remark"`
|
||||
|
||||
Codes []string
|
||||
TagIds []uint64 `orm:"column(tag_id)"`
|
||||
TagPath string `form:"tagPath"`
|
||||
|
||||
Id uint64 `form:"id"`
|
||||
TagPath string `form:"tagPath"`
|
||||
Code string `json:"code" form:"code"`
|
||||
Codes []string
|
||||
InstanceId uint64 `form:"instanceId"`
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/gormx"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
@@ -18,16 +17,6 @@ func newDbRepo() repository.Db {
|
||||
|
||||
// 分页获取数据库信息列表
|
||||
func (d *dbRepoImpl) GetDbList(condition *entity.DbQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
qd := gormx.NewQueryWithTableName("t_db db").Joins("JOIN t_db_instance inst ON db.instance_id = inst.id").
|
||||
WithCond(model.NewCond().Columns("db.*, inst.name instance_name, inst.type instance_type, inst.host, inst.port ").
|
||||
Eq("db.instance_id", condition.InstanceId).
|
||||
Eq("db.id", condition.Id).
|
||||
Like("db.database", condition.Database).
|
||||
Eq("db.code", condition.Code).
|
||||
In("db.code", condition.Codes).
|
||||
Eq0("db."+model.DeletedColumn, model.ModelUndeleted).
|
||||
Eq0("inst."+model.DeletedColumn, model.ModelUndeleted),
|
||||
)
|
||||
|
||||
return gormx.PageQuery(qd, pageParam, toEntity)
|
||||
pd := model.NewCond().Eq("instance_id", condition.InstanceId).In("code", condition.Codes).Eq("id", condition.Id)
|
||||
return d.PageByCondToAny(pd, pageParam, toEntity)
|
||||
}
|
||||
|
||||
@@ -173,6 +173,22 @@ func (m *Machine) KillProcess(rc *req.Ctx) {
|
||||
biz.ErrIsNil(err, "终止进程失败: %s", res)
|
||||
}
|
||||
|
||||
func (m *Machine) GetUsers(rc *req.Ctx) {
|
||||
cli, err := m.MachineApp.GetCli(GetMachineId(rc))
|
||||
biz.ErrIsNilAppendErr(err, "获取客户端连接失败: %s")
|
||||
res, err := cli.GetUsers()
|
||||
biz.ErrIsNil(err)
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (m *Machine) GetGroups(rc *req.Ctx) {
|
||||
cli, err := m.MachineApp.GetCli(GetMachineId(rc))
|
||||
biz.ErrIsNilAppendErr(err, "获取客户端连接失败: %s")
|
||||
res, err := cli.GetGroups()
|
||||
biz.ErrIsNil(err)
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (m *Machine) WsSSH(g *gin.Context) {
|
||||
wsConn, err := ws.Upgrader.Upgrade(g.Writer, g.Request, nil)
|
||||
defer func() {
|
||||
@@ -261,7 +277,7 @@ func (m *Machine) WsGuacamole(g *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err = mi.IfUseSshTunnelChangeIpPort()
|
||||
err = mi.IfUseSshTunnelChangeIpPort(true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -28,9 +28,11 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/may-fly/cast"
|
||||
"github.com/pkg/sftp"
|
||||
)
|
||||
|
||||
type MachineFile struct {
|
||||
MachineApp application.Machine `inject:""`
|
||||
MachineFileApp application.MachineFile `inject:""`
|
||||
MsgApp msgapp.Msg `inject:""`
|
||||
}
|
||||
@@ -159,15 +161,21 @@ func (m *MachineFile) GetDirEntry(rc *req.Ctx) {
|
||||
path = readPath + name
|
||||
}
|
||||
|
||||
fisVO = append(fisVO, vo.MachineFileInfo{
|
||||
mfi := vo.MachineFileInfo{
|
||||
Name: fi.Name(),
|
||||
Size: fi.Size(),
|
||||
Path: path,
|
||||
Type: getFileType(fi.Mode()),
|
||||
Mode: fi.Mode().String(),
|
||||
ModTime: timex.DefaultFormat(fi.ModTime()),
|
||||
})
|
||||
}
|
||||
|
||||
if sftpFs, ok := fi.Sys().(*sftp.FileStat); ok {
|
||||
mfi.UID = sftpFs.UID
|
||||
mfi.GID = sftpFs.GID
|
||||
}
|
||||
|
||||
fisVO = append(fisVO, mfi)
|
||||
}
|
||||
sort.Sort(vo.MachineFileInfos(fisVO))
|
||||
rc.ResData = fisVO
|
||||
|
||||
@@ -78,6 +78,9 @@ type MachineFileInfo struct {
|
||||
Type string `json:"type"`
|
||||
Mode string `json:"mode"`
|
||||
ModTime string `json:"modTime"`
|
||||
|
||||
UID uint32 `json:"uid"`
|
||||
GID uint32 `json:"gid"`
|
||||
}
|
||||
|
||||
type MachineFileInfos []MachineFileInfo
|
||||
|
||||
@@ -159,17 +159,9 @@ func (m *machineAppImpl) SaveMachine(ctx context.Context, param *dto.SaveMachine
|
||||
func (m *machineAppImpl) TestConn(me *entity.Machine, authCert *tagentity.ResourceAuthCert) error {
|
||||
me.Id = 0
|
||||
|
||||
if authCert.Id != 0 {
|
||||
// 密文可能被清除,故需要重新获取
|
||||
authCert, _ = m.resourceAuthCertApp.GetAuthCert(authCert.Name)
|
||||
} else {
|
||||
if authCert.CiphertextType == tagentity.AuthCertCiphertextTypePublic {
|
||||
publicAuthCert, err := m.resourceAuthCertApp.GetAuthCert(authCert.Ciphertext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authCert = publicAuthCert
|
||||
}
|
||||
authCert, err := m.resourceAuthCertApp.GetRealAuthCert(authCert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mi, err := m.toMi(me, authCert)
|
||||
|
||||
@@ -222,8 +222,7 @@ func (m *machineFileAppImpl) MkDir(ctx context.Context, opParam *dto.MachineFile
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sftpCli.MkdirAll(path)
|
||||
return mi, err
|
||||
return mi, sftpCli.MkdirAll(path)
|
||||
}
|
||||
|
||||
func (m *machineFileAppImpl) CreateFile(ctx context.Context, opParam *dto.MachineFileOp) (*mcm.MachineInfo, error) {
|
||||
|
||||
@@ -23,7 +23,7 @@ func (m *machineRepoImpl) GetMachineList(condition *entity.MachineQuery, pagePar
|
||||
Like("ip", condition.Ip).
|
||||
Like("name", condition.Name).
|
||||
In("code", condition.Codes).
|
||||
Like("code", condition.Code).
|
||||
Eq("code", condition.Code).
|
||||
Eq("protocol", condition.Protocol)
|
||||
|
||||
return m.PageByCondToAny(qd, pageParam, toEntity)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"mayfly-go/pkg/logx"
|
||||
"strings"
|
||||
|
||||
"github.com/may-fly/cast"
|
||||
"github.com/pkg/sftp"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
@@ -66,30 +67,6 @@ func (c *Cli) Run(shell string) (string, error) {
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
// GetAllStats 获取机器的所有状态信息
|
||||
func (c *Cli) GetAllStats() *Stats {
|
||||
stats := new(Stats)
|
||||
res, err := c.Run(StatsShell)
|
||||
if err != nil {
|
||||
logx.Errorf("执行机器[id=%d, name=%s]运行状态信息脚本失败: %s", c.Info.Id, c.Info.Name, err.Error())
|
||||
return stats
|
||||
}
|
||||
|
||||
infos := strings.Split(res, "-----")
|
||||
if len(infos) < 8 {
|
||||
return stats
|
||||
}
|
||||
getUptime(infos[0], stats)
|
||||
getHostname(infos[1], stats)
|
||||
getLoad(infos[2], stats)
|
||||
getMemInfo(infos[3], stats)
|
||||
getFSInfo(infos[4], stats)
|
||||
getInterfaces(infos[5], stats)
|
||||
getInterfaceInfo(infos[6], stats)
|
||||
getCPU(infos[7], stats)
|
||||
return stats
|
||||
}
|
||||
|
||||
// Close 关闭client并从缓存中移除,如果使用隧道则也关闭
|
||||
func (c *Cli) Close() {
|
||||
m := c.Info
|
||||
@@ -115,3 +92,77 @@ func (c *Cli) Close() {
|
||||
CloseSshTunnelMachine(int(sshTunnelMachineId), m.GetTunnelId())
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllStats 获取机器的所有状态信息
|
||||
func (c *Cli) GetAllStats() *Stats {
|
||||
stats := new(Stats)
|
||||
res, err := c.Run(StatsShell)
|
||||
if err != nil {
|
||||
logx.Errorf("执行机器[id=%d, name=%s]运行状态信息脚本失败: %s", c.Info.Id, c.Info.Name, err.Error())
|
||||
return stats
|
||||
}
|
||||
|
||||
infos := strings.Split(res, "-----")
|
||||
if len(infos) < 8 {
|
||||
return stats
|
||||
}
|
||||
getUptime(infos[0], stats)
|
||||
getHostname(infos[1], stats)
|
||||
getLoad(infos[2], stats)
|
||||
getMemInfo(infos[3], stats)
|
||||
getFSInfo(infos[4], stats)
|
||||
getInterfaces(infos[5], stats)
|
||||
getInterfaceInfo(infos[6], stats)
|
||||
getCPU(infos[7], stats)
|
||||
return stats
|
||||
}
|
||||
|
||||
// GetUsers 读取/etc/passwd,获取系统所有用户信息
|
||||
func (c *Cli) GetUsers() ([]*UserInfo, error) {
|
||||
res, err := c.Run("cat /etc/passwd")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var users []*UserInfo
|
||||
userLines := strings.Split(res, "\n")
|
||||
for _, userLine := range userLines {
|
||||
if userLine == "" {
|
||||
continue
|
||||
}
|
||||
fields := strings.Split(userLine, ":")
|
||||
user := &UserInfo{
|
||||
Username: fields[0],
|
||||
UID: cast.ToUint32(fields[2]),
|
||||
GID: cast.ToUint32(fields[3]),
|
||||
HomeDir: fields[5],
|
||||
Shell: fields[6],
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// GetGroups 读取/etc/group,获取系统所有组信息
|
||||
func (c *Cli) GetGroups() ([]*GroupInfo, error) {
|
||||
res, err := c.Run("cat /etc/group")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var groups []*GroupInfo
|
||||
groupLines := strings.Split(res, "\n")
|
||||
for _, groupLine := range groupLines {
|
||||
if groupLine == "" {
|
||||
continue
|
||||
}
|
||||
fields := strings.Split(groupLine, ":")
|
||||
group := &GroupInfo{
|
||||
Groupname: fields[0],
|
||||
GID: cast.ToUint32(fields[2]),
|
||||
}
|
||||
groups = append(groups, group)
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
tagentity "mayfly-go/internal/tag/domain/entity"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/utils/netx"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
@@ -15,8 +16,8 @@ import (
|
||||
type MachineInfo struct {
|
||||
Key string `json:"key"` // 缓存key
|
||||
Id uint64 `json:"id"`
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Code string `json:"code"`
|
||||
Protocol int `json:"protocol"`
|
||||
|
||||
Ip string `json:"ip"` // IP地址
|
||||
@@ -35,12 +36,12 @@ type MachineInfo struct {
|
||||
CodePath []string `json:"codePath"`
|
||||
}
|
||||
|
||||
func (m *MachineInfo) UseSshTunnel() bool {
|
||||
return m.SshTunnelMachine != nil
|
||||
func (mi *MachineInfo) UseSshTunnel() bool {
|
||||
return mi.SshTunnelMachine != nil
|
||||
}
|
||||
|
||||
func (m *MachineInfo) GetTunnelId() string {
|
||||
return fmt.Sprintf("machine:%d", m.Id)
|
||||
func (mi *MachineInfo) GetTunnelId() string {
|
||||
return fmt.Sprintf("machine:%d", mi.Id)
|
||||
}
|
||||
|
||||
// 连接
|
||||
@@ -48,7 +49,7 @@ func (mi *MachineInfo) Conn() (*Cli, error) {
|
||||
logx.Infof("[%s]机器连接:%s:%d", mi.Name, mi.Ip, mi.Port)
|
||||
|
||||
// 如果使用了ssh隧道,则修改机器ip port为暴露的ip port
|
||||
err := mi.IfUseSshTunnelChangeIpPort()
|
||||
err := mi.IfUseSshTunnelChangeIpPort(false)
|
||||
if err != nil {
|
||||
return nil, errorx.NewBiz("ssh隧道连接失败: %s", err.Error())
|
||||
}
|
||||
@@ -66,33 +67,39 @@ func (mi *MachineInfo) Conn() (*Cli, error) {
|
||||
}
|
||||
|
||||
// 如果使用了ssh隧道,则修改机器ip port为暴露的ip port
|
||||
func (me *MachineInfo) IfUseSshTunnelChangeIpPort() error {
|
||||
if !me.UseSshTunnel() {
|
||||
func (mi *MachineInfo) IfUseSshTunnelChangeIpPort(out bool) error {
|
||||
if !mi.UseSshTunnel() {
|
||||
return nil
|
||||
}
|
||||
|
||||
originId := me.Id
|
||||
originId := mi.Id
|
||||
if originId == 0 {
|
||||
// 随机设置一个id,如果使用了隧道则用于临时保存隧道
|
||||
me.Id = uint64(time.Now().Nanosecond())
|
||||
mi.Id = uint64(time.Now().Nanosecond())
|
||||
}
|
||||
|
||||
sshTunnelMachine, err := GetSshTunnelMachine(int(me.SshTunnelMachine.Id), func(u uint64) (*MachineInfo, error) {
|
||||
return me.SshTunnelMachine, nil
|
||||
sshTunnelMachine, err := GetSshTunnelMachine(int(mi.SshTunnelMachine.Id), func(u uint64) (*MachineInfo, error) {
|
||||
return mi.SshTunnelMachine, nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
exposeIp, exposePort, err := sshTunnelMachine.OpenSshTunnel(me.GetTunnelId(), me.Ip, me.Port)
|
||||
exposeIp, exposePort, err := sshTunnelMachine.OpenSshTunnel(mi.GetTunnelId(), mi.Ip, mi.Port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 是否获取局域网的本地IP
|
||||
if out {
|
||||
exposeIp = netx.GetOutBoundIP()
|
||||
}
|
||||
|
||||
// 修改机器ip地址
|
||||
me.Ip = exposeIp
|
||||
me.Port = exposePort
|
||||
mi.Ip = exposeIp
|
||||
mi.Port = exposePort
|
||||
// 代理之后置空跳板机信息,防止重复跳
|
||||
me.TempSshMachineId = me.SshTunnelMachine.Id
|
||||
me.SshTunnelMachine = nil
|
||||
mi.TempSshMachineId = mi.SshTunnelMachine.Id
|
||||
mi.SshTunnelMachine = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ func (stm *SshTunnelMachine) OpenSshTunnel(id string, ip string, port int) (expo
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
localHost := "127.0.0.1"
|
||||
localHost := "0.0.0.0"
|
||||
localAddr := fmt.Sprintf("%s:%d", localHost, localPort)
|
||||
listener, err := net.Listen("tcp", localAddr)
|
||||
if err != nil {
|
||||
|
||||
@@ -264,6 +264,9 @@ func getInterfaceInfo(iInfo string, stats *Stats) (err error) {
|
||||
}
|
||||
|
||||
func getCPU(cpuInfo string, stats *Stats) (err error) {
|
||||
if !strings.Contains(cpuInfo, ":") {
|
||||
return
|
||||
}
|
||||
// %Cpu(s): 6.1 us, 3.0 sy, 0.0 ni, 90.9 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
value := strings.Split(cpuInfo, ":")[1]
|
||||
values := strings.Split(value, ",")
|
||||
@@ -287,3 +290,16 @@ func getCPU(cpuInfo string, stats *Stats) (err error) {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type UserInfo struct {
|
||||
UID uint32 `json:"uid"`
|
||||
Username string `json:"uname"`
|
||||
GID uint32 `json:"gid"`
|
||||
HomeDir string `json:"homeDir"` // 用户登录后的起始工作目录
|
||||
Shell string `json:"shell"` // 用户登录时使用的 shell 程序
|
||||
}
|
||||
|
||||
type GroupInfo struct {
|
||||
GID uint32 `json:"gid"`
|
||||
Groupname string `json:"gname"`
|
||||
}
|
||||
|
||||
@@ -98,18 +98,19 @@ func NewParserByteStream(width, height int) *ansiterm.ByteStream {
|
||||
|
||||
var (
|
||||
enterMarks = [][]byte{
|
||||
[]byte("\x1b[?1049h"),
|
||||
[]byte("\x1b[?1049h"), // 从备用屏幕缓冲区恢复屏幕内容
|
||||
[]byte("\x1b[?1048h"),
|
||||
[]byte("\x1b[?1047h"),
|
||||
[]byte("\x1b[?47h"),
|
||||
[]byte("\x1b[?25l"),
|
||||
[]byte("\x1b[?25l"), // 隐藏光标
|
||||
}
|
||||
|
||||
exitMarks = [][]byte{
|
||||
[]byte("\x1b[?1049l"),
|
||||
[]byte("\x1b[?1049l"), // 从备用屏幕缓冲区恢复屏幕内容
|
||||
[]byte("\x1b[?1048l"),
|
||||
[]byte("\x1b[?1047l"),
|
||||
[]byte("\x1b[?47l"),
|
||||
[]byte("\x1b[?25h"), // 显示光标
|
||||
}
|
||||
|
||||
screenMarks = [][]byte{
|
||||
|
||||
@@ -29,6 +29,10 @@ func InitMachineRouter(router *gin.RouterGroup) {
|
||||
|
||||
req.NewGet(":machineId/process", m.GetProcess),
|
||||
|
||||
req.NewGet(":machineId/users", m.GetUsers),
|
||||
|
||||
req.NewGet(":machineId/groups", m.GetGroups),
|
||||
|
||||
req.NewDelete(":machineId/process", m.KillProcess).Log(req.NewLogSave("终止进程")).RequiredPermissionCode("machine:killprocess"),
|
||||
|
||||
req.NewPost("", m.SaveMachine).Log(req.NewLogSave("保存机器信息")).RequiredPermission(saveMachineP),
|
||||
|
||||
@@ -76,18 +76,9 @@ func (r *redisAppImpl) TestConn(param *dto.SaveRedis) error {
|
||||
db = cast.ToInt(strings.Split(re.Db, ",")[0])
|
||||
}
|
||||
|
||||
authCert := param.AuthCert
|
||||
if authCert.Id != 0 {
|
||||
// 密文可能被清除,故需要重新获取
|
||||
authCert, _ = r.resourceAuthCertApp.GetAuthCert(authCert.Name)
|
||||
} else {
|
||||
if authCert.CiphertextType == tagentity.AuthCertCiphertextTypePublic {
|
||||
publicAuthCert, err := r.resourceAuthCertApp.GetAuthCert(authCert.Ciphertext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authCert = publicAuthCert
|
||||
}
|
||||
authCert, err := r.resourceAuthCertApp.GetRealAuthCert(param.AuthCert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rc, err := re.ToRedisInfo(db, authCert).Conn()
|
||||
|
||||
@@ -112,6 +112,7 @@ func (re *RedisInfo) connSentinel() (*RedisConn, error) {
|
||||
SentinelAddrs: strings.Split(masterNameAndHosts[1], ","),
|
||||
Username: re.Username,
|
||||
Password: re.Password, // no password set
|
||||
SentinelUsername: re.Username,
|
||||
SentinelPassword: re.Password, // 哨兵节点密码需与redis节点密码一致
|
||||
DB: re.Db, // use default DB
|
||||
DialTimeout: 8 * time.Second,
|
||||
|
||||
@@ -82,6 +82,11 @@ func (p *TagTree) ListByQuery(rc *req.Ctx) {
|
||||
cond.CodePaths = strings.Split(tagPaths, ",")
|
||||
}
|
||||
cond.Id = uint64(rc.QueryInt("id"))
|
||||
cond.Type = entity.TagType(rc.QueryInt("type"))
|
||||
codes := rc.Query("codes")
|
||||
if codes != "" {
|
||||
cond.Codes = strings.Split(codes, ",")
|
||||
}
|
||||
|
||||
var tagTrees []entity.TagTree
|
||||
p.TagTreeApp.ListByQuery(cond, &tagTrees)
|
||||
|
||||
@@ -26,7 +26,10 @@ type ResourceAuthCert interface {
|
||||
// GetAuthCert 根据授权凭证名称获取授权凭证
|
||||
GetAuthCert(authCertName string) (*entity.ResourceAuthCert, error)
|
||||
|
||||
// GetResourceAuthCert 获取资源授权凭证,默认获取特权账号,若没有则返回第一个
|
||||
//GetRealAuthCert 获取真实可连接鉴权的授权凭证,主要用于资源测试连接时
|
||||
GetRealAuthCert(authCert *entity.ResourceAuthCert) (*entity.ResourceAuthCert, error)
|
||||
|
||||
// GetResourceAuthCert 获取资源授权凭证,优先获取默认账号,若不存在默认账号则返回特权账号,都不存在则返回第一个
|
||||
GetResourceAuthCert(resourceType entity.TagType, resourceCode string) (*entity.ResourceAuthCert, error)
|
||||
|
||||
// FillAuthCertByAcs 根据授权凭证列表填充资源的授权凭证信息
|
||||
@@ -211,6 +214,25 @@ func (r *resourceAuthCertAppImpl) GetAuthCert(authCertName string) (*entity.Reso
|
||||
return r.decryptAuthCert(authCert)
|
||||
}
|
||||
|
||||
func (r *resourceAuthCertAppImpl) GetRealAuthCert(authCert *entity.ResourceAuthCert) (*entity.ResourceAuthCert, error) {
|
||||
// 如果使用的是公共授权凭证,则密文为凭证名称
|
||||
if authCert.CiphertextType == entity.AuthCertCiphertextTypePublic {
|
||||
return r.GetAuthCert(authCert.Ciphertext)
|
||||
}
|
||||
|
||||
if authCert.Id != 0 && authCert.Ciphertext == "" {
|
||||
// 密文可能被清除,故需要重新获取
|
||||
ac, err := r.GetAuthCert(authCert.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authCert.Ciphertext = ac.Ciphertext
|
||||
return authCert, nil
|
||||
}
|
||||
|
||||
return authCert, nil
|
||||
}
|
||||
|
||||
func (r *resourceAuthCertAppImpl) GetResourceAuthCert(resourceType entity.TagType, resourceCode string) (*entity.ResourceAuthCert, error) {
|
||||
resourceAuthCerts, err := r.ListByCond(&entity.ResourceAuthCert{
|
||||
ResourceType: int8(resourceType),
|
||||
@@ -224,6 +246,12 @@ func (r *resourceAuthCertAppImpl) GetResourceAuthCert(resourceType entity.TagTyp
|
||||
return nil, errorx.NewBiz("该资源不存在授权凭证账号")
|
||||
}
|
||||
|
||||
for _, resourceAuthCert := range resourceAuthCerts {
|
||||
if resourceAuthCert.Type == entity.AuthCertTypePrivateDefault {
|
||||
return r.decryptAuthCert(resourceAuthCert)
|
||||
}
|
||||
}
|
||||
|
||||
for _, resourceAuthCert := range resourceAuthCerts {
|
||||
if resourceAuthCert.Type == entity.AuthCertTypePrivileged {
|
||||
return r.decryptAuthCert(resourceAuthCert)
|
||||
|
||||
@@ -312,6 +312,7 @@ func (p *tagTreeAppImpl) DeleteTagByParam(ctx context.Context, param *dto.DelRes
|
||||
}
|
||||
|
||||
delTagType := param.ChildType
|
||||
var childrenTagIds []uint64
|
||||
for _, resourceTag := range resourceTags {
|
||||
// 获取所有关联的子标签
|
||||
childrenTag, _ := p.ListByCond(model.NewCond().RLike("code_path", resourceTag.CodePath).Eq("type", delTagType))
|
||||
@@ -319,14 +320,16 @@ func (p *tagTreeAppImpl) DeleteTagByParam(ctx context.Context, param *dto.DelRes
|
||||
continue
|
||||
}
|
||||
|
||||
childrenTagIds := collx.ArrayMap(childrenTag, func(item *entity.TagTree) uint64 {
|
||||
childrenTagIds = append(childrenTagIds, collx.ArrayMap(childrenTag, func(item *entity.TagTree) uint64 {
|
||||
return item.Id
|
||||
})
|
||||
// 删除code_path下的所有子标签
|
||||
return p.deleteByIds(ctx, childrenTagIds)
|
||||
})...)
|
||||
}
|
||||
|
||||
return nil
|
||||
if len(childrenTagIds) == 0 {
|
||||
return nil
|
||||
}
|
||||
// 删除code_path下的所有子标签
|
||||
return p.deleteByIds(ctx, collx.ArrayDeduplicate(childrenTagIds))
|
||||
}
|
||||
|
||||
func (p *tagTreeAppImpl) ListByQuery(condition *entity.TagTreeQuery, toEntity any) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import "fmt"
|
||||
|
||||
const (
|
||||
AppName = "mayfly-go"
|
||||
Version = "v1.8.5"
|
||||
Version = "v1.8.8"
|
||||
)
|
||||
|
||||
func GetAppInfo() string {
|
||||
|
||||
@@ -3,9 +3,8 @@ package jsonx
|
||||
import (
|
||||
"encoding/json"
|
||||
"mayfly-go/pkg/logx"
|
||||
"strings"
|
||||
|
||||
"github.com/buger/jsonparser"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// json字符串转map
|
||||
@@ -42,35 +41,35 @@ func ToStr(val any) string {
|
||||
//
|
||||
// @param fieldPath字段路径。如user.username等
|
||||
func GetStringByBytes(bytes []byte, fieldPath string) (string, error) {
|
||||
return jsonparser.GetString(bytes, strings.Split(fieldPath, ".")...)
|
||||
return gjson.GetBytes(bytes, fieldPath).String(), nil
|
||||
}
|
||||
|
||||
// 根据json字符串获取对应字段路径的string类型值
|
||||
//
|
||||
// @param fieldPath字段路径。如user.username等
|
||||
func GetString(jsonStr string, fieldPath string) (string, error) {
|
||||
return GetStringByBytes([]byte(jsonStr), fieldPath)
|
||||
return gjson.Get(jsonStr, fieldPath).String(), nil
|
||||
}
|
||||
|
||||
// 根据json字节数组获取对应字段路径的int类型值
|
||||
//
|
||||
// @param fieldPath字段路径。如user.age等
|
||||
func GetIntByBytes(bytes []byte, fieldPath string) (int64, error) {
|
||||
return jsonparser.GetInt(bytes, strings.Split(fieldPath, ".")...)
|
||||
return gjson.GetBytes(bytes, fieldPath).Int(), nil
|
||||
}
|
||||
|
||||
// 根据json字符串获取对应字段路径的int类型值
|
||||
//
|
||||
// @param fieldPath字段路径。如user.age等
|
||||
func GetInt(jsonStr string, fieldPath string) (int64, error) {
|
||||
return GetIntByBytes([]byte(jsonStr), fieldPath)
|
||||
return gjson.Get(jsonStr, fieldPath).Int(), nil
|
||||
}
|
||||
|
||||
// 根据json字节数组获取对应字段路径的bool类型值
|
||||
//
|
||||
// @param fieldPath字段路径。如user.isDeleted等
|
||||
func GetBoolByBytes(bytes []byte, fieldPath string) (bool, error) {
|
||||
return jsonparser.GetBoolean(bytes, strings.Split(fieldPath, ".")...)
|
||||
return gjson.GetBytes(bytes, fieldPath).Bool(), nil
|
||||
}
|
||||
|
||||
// 根据json字符串获取对应字段路径的bool类型值
|
||||
|
||||
@@ -3,8 +3,6 @@ package jsonx
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/buger/jsonparser"
|
||||
)
|
||||
|
||||
const jsonStr = `{
|
||||
@@ -36,7 +34,7 @@ func TestGetString(t *testing.T) {
|
||||
// val, err := GetString(jsonStr, "username1")
|
||||
|
||||
// 含有数组的
|
||||
val, err := GetString(jsonStr, "person.avatars.[0].url")
|
||||
val, err := GetString(jsonStr, "person.avatars.0.url")
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("error: ", err.Error())
|
||||
@@ -50,60 +48,3 @@ func TestGetInt(t *testing.T) {
|
||||
val2, _ := GetInt(jsonStr, "person.github.followers")
|
||||
fmt.Println(val, ",", val2)
|
||||
}
|
||||
|
||||
// 官方demo
|
||||
func TestJsonParser(t *testing.T) {
|
||||
data := []byte(jsonStr)
|
||||
// You can specify key path by providing arguments to Get function
|
||||
jsonparser.Get(data, "person", "name", "fullName")
|
||||
|
||||
// There is `GetInt` and `GetBoolean` helpers if you exactly know key data type
|
||||
jsonparser.GetInt(data, "person", "github", "followers")
|
||||
|
||||
// When you try to get object, it will return you []byte slice pointer to data containing it
|
||||
// In `company` it will be `{"name": "Acme"}`
|
||||
jsonparser.Get(data, "company")
|
||||
|
||||
// If the key doesn't exist it will throw an error
|
||||
var size int64
|
||||
if value, err := jsonparser.GetInt(data, "company", "size"); err == nil {
|
||||
size = value
|
||||
fmt.Println(size)
|
||||
}
|
||||
|
||||
// You can use `ArrayEach` helper to iterate items [item1, item2 .... itemN]
|
||||
jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
|
||||
fmt.Println(jsonparser.Get(value, "url"))
|
||||
}, "person", "avatars")
|
||||
|
||||
// Or use can access fields by index!
|
||||
jsonparser.GetString(data, "person", "avatars", "[0]", "url")
|
||||
|
||||
// You can use `ObjectEach` helper to iterate objects { "key1":object1, "key2":object2, .... "keyN":objectN }
|
||||
jsonparser.ObjectEach(data, func(key []byte, value []byte, dataType jsonparser.ValueType, offset int) error {
|
||||
fmt.Printf("Key: '%s'\n Value: '%s'\n Type: %s\n", string(key), string(value), dataType)
|
||||
return nil
|
||||
}, "person", "name")
|
||||
|
||||
// The most efficient way to extract multiple keys is `EachKey`
|
||||
|
||||
paths := [][]string{
|
||||
[]string{"person", "name", "fullName"},
|
||||
[]string{"person", "avatars", "[0]", "url"},
|
||||
[]string{"company", "url"},
|
||||
}
|
||||
|
||||
jsonparser.EachKey(data, func(idx int, value []byte, vt jsonparser.ValueType, err error) {
|
||||
switch idx {
|
||||
case 0: // []string{"person", "name", "fullName"}
|
||||
{
|
||||
}
|
||||
case 1: // []string{"person", "avatars", "[0]", "url"}
|
||||
{
|
||||
}
|
||||
case 2: // []string{"company", "url"},
|
||||
{
|
||||
}
|
||||
}
|
||||
}, paths...)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package netx
|
||||
import (
|
||||
"mayfly-go/pkg/logx"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
|
||||
)
|
||||
@@ -68,3 +69,13 @@ func Ip2Region(ip string) string {
|
||||
}
|
||||
return region
|
||||
}
|
||||
|
||||
func GetOutBoundIP() string {
|
||||
conn, err := net.Dial("udp", "8.8.8.8:53")
|
||||
if err != nil {
|
||||
return "0.0.0.0"
|
||||
}
|
||||
localAddr := conn.LocalAddr().(*net.UDPAddr)
|
||||
ip := strings.Split(localAddr.String(), ":")[0]
|
||||
return ip
|
||||
}
|
||||
|
||||
10
server/pkg/utils/netx/netx_test.go
Normal file
10
server/pkg/utils/netx/netx_test.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package netx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIp(t *testing.T) {
|
||||
fmt.Println(GetOutBoundIP())
|
||||
}
|
||||
Binary file not shown.
@@ -761,11 +761,10 @@ INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(45, 3, '12sSjal1/lskeiql1/Ljewisd3/', 2, 1, '脚本管理-保存脚本按钮', 'machine:script:save', 120000000, 'null', 1, 'admin', 1, 'admin', '2021-06-08 11:09:01', '2021-06-08 11:09:01', 0, NULL);
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(46, 3, '12sSjal1/lskeiql1/Ljeew43/', 2, 1, '脚本管理-删除按钮', 'machine:script:del', 130000000, 'null', 1, 'admin', 1, 'admin', '2021-06-08 11:09:27', '2021-06-08 11:09:27', 0, NULL);
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(47, 3, '12sSjal1/lskeiql1/ODewix43/', 2, 1, '脚本管理-执行按钮', 'machine:script:run', 140000000, 'null', 1, 'admin', 1, 'admin', '2021-06-08 11:09:50', '2021-06-08 11:09:50', 0, NULL);
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(49, 36, 'dbms23ax/xleaiec2/', 1, 1, '数据库管理', 'dbs', 20000000, '{"component":"ops/db/DbList","icon":"Coin","isKeepAlive":true,"routeName":"DbList"}', 1, 'admin', 1, 'admin', '2021-07-07 15:13:55', '2023-03-15 17:31:28', 0, NULL);
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(54, 49, 'dbms23ax/xleaiec2/leix3Axl/', 2, 1, '数据库保存', 'db:save', 10000000, 'null', 1, 'admin', 1, 'admin', '2021-07-08 17:30:36', '2021-07-08 17:31:05', 0, NULL);
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(55, 49, 'dbms23ax/xleaiec2/ygjL3sxA/', 2, 1, '数据库删除', 'db:del', 20000000, 'null', 1, 'admin', 1, 'admin', '2021-07-08 17:30:48', '2021-07-08 17:30:48', 0, NULL);
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(54, 135, 'dbms23ax/X0f4BxT0/leix3Axl/', 2, 1, '数据库保存', 'db:save', 10000000, 'null', 1, 'admin', 1, 'admin', '2021-07-08 17:30:36', '2021-07-08 17:31:05', 0, NULL);
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(55, 135, 'dbms23ax/X0f4BxT0/ygjL3sxA/', 2, 1, '数据库删除', 'db:del', 20000000, 'null', 1, 'admin', 1, 'admin', '2021-07-08 17:30:48', '2021-07-08 17:30:48', 0, NULL);
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(57, 3, '12sSjal1/lskeiql1/OJewex43/', 2, 1, '基本权限', 'machine', 10000000, 'null', 1, 'admin', 1, 'admin', '2021-07-09 10:48:02', '2021-07-09 10:48:02', 0, NULL);
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(58, 49, 'dbms23ax/xleaiec2/AceXe321/', 2, 1, '基本权限', 'db', 10000000, 'null', 1, 'admin', 1, 'admin', '2021-07-09 10:48:22', '2021-07-09 10:48:22', 0, NULL);
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(58, 135, 'dbms23ax/X0f4BxT0/AceXe321/', 2, 1, '数据库基本权限', 'db', 10000000, 'null', 1, 'admin', 1, 'admin', '2021-07-09 10:48:22', '2021-07-09 10:48:22', 0, NULL);
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(59, 38, 'dbms23ax/exaeca2x/ealcia23/', 2, 1, '基本权限', 'db:exec', 10000000, 'null', 1, 'admin', 1, 'admin', '2021-07-09 10:50:13', '2021-07-09 10:50:13', 0, NULL);
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(60, 0, 'RedisXq4/', 1, 1, 'Redis', '/redis', 50000001, '{"icon":"iconfont icon-redis","isKeepAlive":true,"routeName":"RDS"}', 1, 'admin', 1, 'admin', '2021-07-19 20:15:41', '2023-03-15 16:44:59', 0, NULL);
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(61, 60, 'RedisXq4/Exitx4al/', 1, 1, '数据操作', 'data-operation', 10000000, '{"component":"ops/redis/DataOperation","icon":"iconfont icon-redis","isKeepAlive":true,"routeName":"DataOperation"}', 1, 'admin', 1, 'admin', '2021-07-19 20:17:29', '2023-03-15 16:37:50', 0, NULL);
|
||||
@@ -814,8 +813,8 @@ INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(152, 150, 'Jra0n7De/zvAMo2vk/', 2, 1, '编辑', 'db:sync:save', 1703641320, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2023-12-27 09:42:00', '2023-12-27 09:42:12', 0, NULL);
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(151, 150, 'Jra0n7De/uAnHZxEV/', 2, 1, '基本权限', 'db:sync', 1703641202, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2023-12-27 09:40:02', '2023-12-27 09:40:02', 0, NULL);
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(150, 36, 'Jra0n7De/', 1, 1, '数据同步', 'sync', 1693040707, '{"component":"ops/db/SyncTaskList","icon":"Coin","isKeepAlive":true,"routeName":"SyncTaskList"}', 12, 'liuzongyang', 12, 'liuzongyang', '2023-12-22 09:51:34', '2023-12-27 10:16:57', 0, NULL);
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(160, 49, 'dbms23ax/xleaiec2/3NUXQFIO/', 2, 1, '数据库备份', 'db:backup', 1705973876, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:37:56', '2024-01-23 09:37:56', 0, NULL);
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(161, 49, 'dbms23ax/xleaiec2/ghErkTdb/', 2, 1, '数据库恢复', 'db:restore', 1705973909, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:38:29', '2024-01-23 09:38:29', 0, NULL);
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(160, 135, 'dbms23ax/X0f4BxT0/3NUXQFIO/', 2, 1, '数据库备份', 'db:backup', 1705973876, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:37:56', '2024-01-23 09:37:56', 0, NULL);
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(161, 135, 'dbms23ax/X0f4BxT0/ghErkTdb/', 2, 1, '数据库恢复', 'db:restore', 1705973909, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:38:29', '2024-01-23 09:38:29', 0, NULL);
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(1709208354, 1708911264, '6egfEVYr/fw0Hhvye/b4cNf3iq/', 2, 1, '删除流程', 'flow:procdef:del', 1709208354, 'null', 1, 'admin', 1, 'admin', '2024-02-29 20:05:54', '2024-02-29 20:05:54', 0, NULL);
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(1709208339, 1708911264, '6egfEVYr/fw0Hhvye/r9ZMTHqC/', 2, 1, '保存流程', 'flow:procdef:save', 1709208339, 'null', 1, 'admin', 1, 'admin', '2024-02-29 20:05:40', '2024-02-29 20:05:40', 0, NULL);
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(1709103180, 1708910975, '6egfEVYr/oNCIbynR/', 1, 1, '我的流程', 'procinsts', 1708911263, '{"component":"flow/ProcinstList","icon":"Tickets","isKeepAlive":true,"routeName":"ProcinstList"}', 1, 'admin', 1, 'admin', '2024-02-28 14:53:00', '2024-02-29 20:36:07', 0, NULL);
|
||||
|
||||
7
server/resources/script/sql/v1.8/v1.8.6.sql
Normal file
7
server/resources/script/sql/v1.8/v1.8.6.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
UPDATE t_sys_resource SET pid=135, ui_path='dbms23ax/X0f4BxT0/leix3Axl/', `type`=2, status=1, name='数据库保存', code='db:save', weight=1693041085, meta='null', creator_id=1, creator='admin', modifier_id=1, modifier='admin', create_time='2021-07-08 17:30:36', update_time='2024-05-17 21:50:01', is_deleted=0, delete_time=NULL WHERE id=54;
|
||||
UPDATE t_sys_resource SET pid=135, ui_path='dbms23ax/X0f4BxT0/ygjL3sxA/', `type`=2, status=1, name='数据库删除', code='db:del', weight=1693041086, meta='null', creator_id=1, creator='admin', modifier_id=1, modifier='admin', create_time='2021-07-08 17:30:48', update_time='2024-05-17 21:50:04', is_deleted=0, delete_time=NULL WHERE id=55;
|
||||
UPDATE t_sys_resource SET pid=135, ui_path='dbms23ax/X0f4BxT0/AceXe321/', `type`=2, status=1, name='数据库基本权限', code='db', weight=1693041085, meta='null', creator_id=1, creator='admin', modifier_id=1, modifier='admin', create_time='2021-07-09 10:48:22', update_time='2024-05-17 21:52:52', is_deleted=0, delete_time=NULL WHERE id=58;
|
||||
UPDATE t_sys_resource SET pid=135, ui_path='dbms23ax/X0f4BxT0/3NUXQFIO/', `type`=2, status=1, name='数据库备份', code='db:backup', weight=1693041087, meta='null', creator_id=1, creator='admin', modifier_id=1, modifier='admin', create_time='2024-01-23 09:37:56', update_time='2024-05-17 21:50:07', is_deleted=0, delete_time=NULL WHERE id=160;
|
||||
UPDATE t_sys_resource SET pid=135, ui_path='dbms23ax/X0f4BxT0/ghErkTdb/', `type`=2, status=1, name='数据库恢复', code='db:restore', weight=1693041088, meta='null', creator_id=1, creator='admin', modifier_id=1, modifier='admin', create_time='2024-01-23 09:38:29', update_time='2024-05-17 21:50:10', is_deleted=0, delete_time=NULL WHERE id=161;
|
||||
|
||||
DELETE FROM t_sys_resource WHERE id=49;
|
||||
Reference in New Issue
Block a user