mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 15:30:25 +08:00
Compare commits
9 Commits
v1.10.2
...
4836a770c4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4836a770c4 | ||
|
|
e6c89fad1b | ||
|
|
dba19b1e66 | ||
|
|
4e30bdb7cc | ||
|
|
4ac57cd140 | ||
|
|
c4d52ce47a | ||
|
|
54d0688571 | ||
|
|
66d5fd6ca4 | ||
|
|
25195b6360 |
32
README.md
32
README.md
@@ -51,42 +51,36 @@ http://go.mayfly.run
|
|||||||
|
|
||||||
#### 首页
|
#### 首页
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### 机器操作
|
#### 资源管理
|
||||||
|
|
||||||
##### 状态查看
|

|
||||||
|
|
||||||

|
#### 资源操作
|
||||||
|
|
||||||
##### ssh 终端
|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
##### 文件操作
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### 数据库操作
|
|
||||||
|
|
||||||
##### sql 编辑器
|

|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
##### 在线增删改查数据
|

|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
#### Redis 操作
|

|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
#### Mongo 操作
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
#### 工单流程审批
|
#### 工单流程审批
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
31
README_EN.md
31
README_EN.md
@@ -46,40 +46,35 @@ account/password:test/test123.
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### Machine Operation
|
#### Resource Manage
|
||||||
|
|
||||||
##### Status
|

|
||||||
|
|
||||||

|
#### Resource Operation
|
||||||
|
|
||||||
##### SSH Terminal
|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
##### File Operation
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### Database Operation
|
|
||||||
|
|
||||||
##### SQL Editor
|

|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
##### Add, delete, update and check data online
|

|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
#### Redis Operation
|

|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
#### Mongo Operation
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
#### Work order process approval
|
#### Work order process approval
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -11,40 +11,40 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"@logicflow/core": "^2.1.1",
|
"@logicflow/core": "^2.1.3",
|
||||||
"@logicflow/extension": "^2.1.2",
|
"@logicflow/extension": "^2.1.5",
|
||||||
"@vueuse/core": "^13.8.0",
|
"@vueuse/core": "^13.9.0",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/addon-search": "^0.15.0",
|
"@xterm/addon-search": "^0.15.0",
|
||||||
"@xterm/addon-web-links": "^0.11.0",
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"asciinema-player": "^3.10.0",
|
"asciinema-player": "^3.11.1",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dayjs": "^1.11.18",
|
"dayjs": "^1.11.18",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"element-plus": "^2.11.1",
|
"element-plus": "^2.11.4",
|
||||||
"js-base64": "^3.7.7",
|
"js-base64": "^3.7.8",
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.5.4",
|
||||||
"monaco-editor": "^0.52.2",
|
"monaco-editor": "^0.54.0",
|
||||||
"monaco-sql-languages": "^0.15.1",
|
"monaco-sql-languages": "^0.15.1",
|
||||||
"monaco-themes": "^0.4.6",
|
"monaco-themes": "^0.4.7",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"qrcode.vue": "^3.6.0",
|
"qrcode.vue": "^3.6.0",
|
||||||
"screenfull": "^6.0.2",
|
"screenfull": "^6.0.2",
|
||||||
"sortablejs": "^1.15.6",
|
"sortablejs": "^1.15.6",
|
||||||
"sql-formatter": "^15.6.5",
|
"sql-formatter": "^15.6.8",
|
||||||
"trzsz": "^1.1.5",
|
"trzsz": "^1.1.5",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^13.0.0",
|
||||||
"vue": "^v3.6.0-alpha.2",
|
"vue": "^v3.5.22",
|
||||||
"vue-i18n": "^11.1.11",
|
"vue-i18n": "^11.1.12",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.6.3",
|
||||||
"vuedraggable": "^4.1.0"
|
"vuedraggable": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.12",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/node": "^22.13.14",
|
"@types/node": "^22.13.14",
|
||||||
"@types/nprogress": "^0.2.0",
|
"@types/nprogress": "^0.2.0",
|
||||||
@@ -56,11 +56,11 @@
|
|||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"code-inspector-plugin": "^1.0.4",
|
"code-inspector-plugin": "^1.0.4",
|
||||||
"eslint": "^9.29.0",
|
"eslint": "^9.29.0",
|
||||||
"eslint-plugin-vue": "^10.4.0",
|
"eslint-plugin-vue": "^10.5.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.6.1",
|
"prettier": "^3.6.1",
|
||||||
"sass": "^1.90.0",
|
"sass": "^1.93.2",
|
||||||
"tailwindcss": "^4.1.12",
|
"tailwindcss": "^4.1.14",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"vite": "npm:rolldown-vite@latest",
|
"vite": "npm:rolldown-vite@latest",
|
||||||
"vite-plugin-progress": "0.0.7",
|
"vite-plugin-progress": "0.0.7",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const ResourceTypeEnum = {
|
|||||||
Mongo: EnumValue.of(4, 'mongo').setExtra({ icon: 'icon mongo/mongo', iconColor: 'var(--el-color-success)' }).tagTypeDanger(),
|
Mongo: EnumValue.of(4, 'mongo').setExtra({ icon: 'icon mongo/mongo', iconColor: 'var(--el-color-success)' }).tagTypeDanger(),
|
||||||
AuthCert: EnumValue.of(5, 'ac.ac').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
|
AuthCert: EnumValue.of(5, 'ac.ac').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
|
||||||
Es: EnumValue.of(6, 'tag.es').setExtra({ icon: 'icon es/es-color', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
|
Es: EnumValue.of(6, 'tag.es').setExtra({ icon: 'icon es/es-color', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
|
||||||
|
Container: EnumValue.of(7, 'tag.container').setExtra({ icon: 'icon docker/docker', iconColor: 'var(--el-color-primary)' }),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 标签关联的资源类型
|
// 标签关联的资源类型
|
||||||
@@ -35,6 +36,7 @@ export const TagResourceTypeEnum = {
|
|||||||
Redis: ResourceTypeEnum.Redis,
|
Redis: ResourceTypeEnum.Redis,
|
||||||
Mongo: ResourceTypeEnum.Mongo,
|
Mongo: ResourceTypeEnum.Mongo,
|
||||||
AuthCert: ResourceTypeEnum.AuthCert,
|
AuthCert: ResourceTypeEnum.AuthCert,
|
||||||
|
Container: ResourceTypeEnum.Container,
|
||||||
|
|
||||||
Db: EnumValue.of(22, '数据库').setExtra({ icon: 'icon db/db' }),
|
Db: EnumValue.of(22, '数据库').setExtra({ icon: 'icon db/db' }),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const config = {
|
|||||||
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
||||||
|
|
||||||
// 系统版本
|
// 系统版本
|
||||||
version: 'v1.10.2',
|
version: 'v1.10.4',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -12,7 +12,13 @@ import { ElMessage } from 'element-plus';
|
|||||||
export function templateResolve(template: string, param: any) {
|
export function templateResolve(template: string, param: any) {
|
||||||
return template.replace(/\{\w+\}/g, (word) => {
|
return template.replace(/\{\w+\}/g, (word) => {
|
||||||
const key = word.substring(1, word.length - 1);
|
const key = word.substring(1, word.length - 1);
|
||||||
const value = param[key];
|
let value;
|
||||||
|
// 兼容FormData类型的参数
|
||||||
|
if (param instanceof FormData) {
|
||||||
|
value = param.get(key);
|
||||||
|
} else {
|
||||||
|
value = param[key];
|
||||||
|
}
|
||||||
if (value != null || value != undefined) {
|
if (value != null || value != undefined) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const props = defineProps({
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
type: [Object, String, Number, null],
|
type: [Object, String, Number, null, Boolean],
|
||||||
required: true,
|
required: true,
|
||||||
default: () => null,
|
default: () => null,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-form-item v-bind="$attrs">
|
<el-form-item v-bind="$attrs">
|
||||||
<template #label>
|
<template #label>
|
||||||
{{ props.label }}
|
<div class="flex items-center">
|
||||||
|
{{ props.label }}
|
||||||
|
|
||||||
<el-tooltip :placement="props.placement">
|
<el-tooltip :placement="props.placement">
|
||||||
<template #content>
|
<template #content>
|
||||||
<span v-html="props.tooltip"></span>
|
<span v-html="props.tooltip"></span>
|
||||||
</template>
|
</template>
|
||||||
<SvgIcon name="QuestionFilled" />
|
<SvgIcon name="QuestionFilled" class="ml-1" />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 遍历父组件传入的 solts 透传给子组件 -->
|
<!-- 遍历父组件传入的 solts 透传给子组件 -->
|
||||||
@@ -24,11 +26,11 @@ import { useSlots } from 'vue';
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
require: true,
|
required: true,
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
type: String,
|
type: String,
|
||||||
require: true,
|
required: true,
|
||||||
},
|
},
|
||||||
placement: {
|
placement: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ const defaultOptions = {
|
|||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
lineNumbers: 'on',
|
lineNumbers: 'on',
|
||||||
lineNumbersMinChars: 3,
|
lineNumbersMinChars: 3,
|
||||||
|
fixedOverflowWidgets: true, // 使弹出层不被容器限制
|
||||||
} as editor.IStandaloneEditorConstructionOptions;
|
} as editor.IStandaloneEditorConstructionOptions;
|
||||||
|
|
||||||
const monacoTextareaRef: Ref<any> = useTemplateRef('monacoTextareaRef');
|
const monacoTextareaRef: Ref<any> = useTemplateRef('monacoTextareaRef');
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="h-full">
|
||||||
<monaco-editor
|
<monaco-editor
|
||||||
ref="editorRef"
|
ref="editorRef"
|
||||||
:height="props.height"
|
:height="props.height"
|
||||||
@@ -22,7 +22,7 @@ import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
height: {
|
height: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'calc(100vh - 200px)',
|
default: '100%',
|
||||||
},
|
},
|
||||||
wsUrl: {
|
wsUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -45,14 +45,20 @@ watch(data, (value) => {
|
|||||||
// eslint-disable-next-line no-control-regex
|
// eslint-disable-next-line no-control-regex
|
||||||
modelValue.value = modelValue.value + value.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
|
modelValue.value = modelValue.value + value.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
editorRef.value?.revealLastLine();
|
revealLastLine();
|
||||||
}, 200);
|
}, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
const reload = (wsUrl: string) => {
|
const reload = (wsUrl: string) => {
|
||||||
modelValue.value = '';
|
modelValue.value = '';
|
||||||
editorRef.value?.revealLastLine();
|
|
||||||
websocketUrl.value = wsUrl;
|
websocketUrl.value = wsUrl;
|
||||||
|
revealLastLine();
|
||||||
|
};
|
||||||
|
|
||||||
|
const revealLastLine = () => {
|
||||||
|
const editor = editorRef.value.getEditor();
|
||||||
|
const lineCount = editor?.getModel().getLineCount();
|
||||||
|
editor.revealLine(lineCount);
|
||||||
};
|
};
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export default {
|
|||||||
resultSet: 'Result Set',
|
resultSet: 'Result Set',
|
||||||
tableDataEmptyTextTips:
|
tableDataEmptyTextTips:
|
||||||
'tips: Single table query at the beginning of select * or click the default query data of the table name, double-click the data online modification',
|
'tips: Single table query at the beginning of select * or click the default query data of the table name, double-click the data online modification',
|
||||||
noSelctRunSqlMsg: 'Select the sql you want to execute',
|
noSelectRunSqlMsg: 'Select the sql you want to execute or move the cursor near the sql you want to execute',
|
||||||
enterExecRemarkTips: 'Please enter remark',
|
enterExecRemarkTips: 'Please enter remark',
|
||||||
execRemarkPlaceholder: 'Enter the remark to execute the sql',
|
execRemarkPlaceholder: 'Enter the remark to execute the sql',
|
||||||
currentSqlTabIsRunning: 'The current result set tab is being executed, please use the new TAB to execute',
|
currentSqlTabIsRunning: 'The current result set tab is being executed, please use the new TAB to execute',
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
export default {
|
export default {
|
||||||
docker: {
|
docker: {
|
||||||
|
containerConf: 'Container Config',
|
||||||
|
addr: 'Address',
|
||||||
|
addrTips: 'eg: unix:///var/run/docker.sock 、tcp://192.168.1.1',
|
||||||
|
|
||||||
container: 'Container',
|
container: 'Container',
|
||||||
containerName: 'Container Name',
|
containerName: 'Container Name',
|
||||||
running: 'Running',
|
running: 'Running',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
es: {
|
es: {
|
||||||
keywordPlaceholder: 'host / name / code',
|
keywordPlaceholder: 'host / name / code',
|
||||||
|
protocol: 'Protocol',
|
||||||
port: 'Port',
|
port: 'Port',
|
||||||
size: 'size',
|
size: 'size',
|
||||||
docs: 'docs',
|
docs: 'docs',
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ export default {
|
|||||||
personalCenter: 'Personal Center',
|
personalCenter: 'Personal Center',
|
||||||
myResource: 'Resource',
|
myResource: 'Resource',
|
||||||
|
|
||||||
tag: 'Tag',
|
tag: 'Resource',
|
||||||
tagTree: 'Tag Tree',
|
tagTree: 'Resource Tree',
|
||||||
tagSave: 'Save Tag',
|
tagSave: 'Save Tag',
|
||||||
tagDelete: 'Delete Tag',
|
tagDelete: 'Delete Tag',
|
||||||
authorization: 'Authorization',
|
authorization: 'Authorization',
|
||||||
@@ -92,6 +92,8 @@ export default {
|
|||||||
mongoManage: 'Mongo Manage',
|
mongoManage: 'Mongo Manage',
|
||||||
mongoManageBase: 'Base Permission',
|
mongoManageBase: 'Base Permission',
|
||||||
|
|
||||||
|
containerManageBase: 'Container Manage - Base Permission',
|
||||||
|
|
||||||
flow: 'Flow',
|
flow: 'Flow',
|
||||||
myTask: 'My Task',
|
myTask: 'My Task',
|
||||||
myFlow: 'My Flow',
|
myFlow: 'My Flow',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export default {
|
|||||||
tagTips1: '1. Used to group assets',
|
tagTips1: '1. Used to group assets',
|
||||||
tagTips2: '2. Can be allocated in team management for resource isolation',
|
tagTips2: '2. Can be allocated in team management for resource isolation',
|
||||||
tagTips3: '3. Team members who own a parent tag have access to resources that manipulate their own or child tag associations',
|
tagTips3: '3. Team members who own a parent tag have access to resources that manipulate their own or child tag associations',
|
||||||
|
tagTips4: '4. Right-click nodes to edit or add child tags',
|
||||||
machine: 'Machine',
|
machine: 'Machine',
|
||||||
db: 'Db',
|
db: 'Db',
|
||||||
code: 'Code',
|
code: 'Code',
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export default {
|
|||||||
times: '耗时',
|
times: '耗时',
|
||||||
resultSet: '结果集',
|
resultSet: '结果集',
|
||||||
tableDataEmptyTextTips: 'tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改',
|
tableDataEmptyTextTips: 'tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改',
|
||||||
noSelctRunSqlMsg: '请选中需要执行的sql',
|
noSelectRunSqlMsg: '请选中需要执行的sql或将光标移动到要执行sql附近',
|
||||||
enterExecRemarkTips: '请输入备注',
|
enterExecRemarkTips: '请输入备注',
|
||||||
execRemarkPlaceholder: '输入执行该sql的备注信息',
|
execRemarkPlaceholder: '输入执行该sql的备注信息',
|
||||||
currentSqlTabIsRunning: '当前结果集tab正在执行, 请使用新标签执行',
|
currentSqlTabIsRunning: '当前结果集tab正在执行, 请使用新标签执行',
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
export default {
|
export default {
|
||||||
docker: {
|
docker: {
|
||||||
|
containerConf: '容器配置',
|
||||||
|
addr: '地址',
|
||||||
|
addrTips: '如:unix:///var/run/docker.sock 、tcp://192.168.1.1',
|
||||||
|
|
||||||
container: '容器',
|
container: '容器',
|
||||||
containerName: '容器名',
|
containerName: '容器名',
|
||||||
running: '运行中',
|
running: '运行中',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
es: {
|
es: {
|
||||||
keywordPlaceholder: 'host / 名称 / 编号',
|
keywordPlaceholder: 'host / 名称 / 编号',
|
||||||
|
protocol: '协议',
|
||||||
port: '端口',
|
port: '端口',
|
||||||
size: '存储大小',
|
size: '存储大小',
|
||||||
docs: '文档数',
|
docs: '文档数',
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ export default {
|
|||||||
personalCenter: '个人中心',
|
personalCenter: '个人中心',
|
||||||
myResource: '我的资源',
|
myResource: '我的资源',
|
||||||
|
|
||||||
tag: '标签',
|
tag: '资源',
|
||||||
tagTree: '标签树',
|
tagTree: '资源树',
|
||||||
tagSave: '保存标签',
|
tagSave: '保存标签',
|
||||||
tagDelete: '删除标签',
|
tagDelete: '删除标签',
|
||||||
authorization: '授权凭证',
|
authorization: '授权凭证',
|
||||||
@@ -92,6 +92,8 @@ export default {
|
|||||||
mongoManage: 'Mongo管理',
|
mongoManage: 'Mongo管理',
|
||||||
mongoManageBase: 'Mongo-管理-基本权限',
|
mongoManageBase: 'Mongo-管理-基本权限',
|
||||||
|
|
||||||
|
containerManageBase: '容器-管理-基本权限',
|
||||||
|
|
||||||
flow: '工单流程',
|
flow: '工单流程',
|
||||||
myTask: '我的任务',
|
myTask: '我的任务',
|
||||||
myFlow: '我的流程',
|
myFlow: '我的流程',
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ export default {
|
|||||||
tagTips1: '1. 用于将资产进行归类',
|
tagTips1: '1. 用于将资产进行归类',
|
||||||
tagTips2: '2. 可在团队管理中进行分配,用于资源隔离',
|
tagTips2: '2. 可在团队管理中进行分配,用于资源隔离',
|
||||||
tagTips3: '3. 拥有父标签的团队成员可访问操作其自身或子标签关联的资源',
|
tagTips3: '3. 拥有父标签的团队成员可访问操作其自身或子标签关联的资源',
|
||||||
|
tagTips4: '4. 右击节点可进行编辑或添加子标签操作',
|
||||||
machine: '机器',
|
machine: '机器',
|
||||||
db: '数据库',
|
db: '数据库',
|
||||||
es: 'ES',
|
es: 'ES',
|
||||||
|
container: '容器',
|
||||||
code: '编号',
|
code: '编号',
|
||||||
createSubTag: '创建子标签',
|
createSubTag: '创建子标签',
|
||||||
createSubTagTitle: '创建【{codePath}】的子标签',
|
createSubTagTitle: '创建【{codePath}】的子标签',
|
||||||
@@ -20,6 +22,7 @@ export default {
|
|||||||
redisDataOp: 'Redis操作',
|
redisDataOp: 'Redis操作',
|
||||||
esDataOp: 'ES操作',
|
esDataOp: 'ES操作',
|
||||||
mongoDataOp: 'Mongo操作',
|
mongoDataOp: 'Mongo操作',
|
||||||
|
containerOp: '容器操作',
|
||||||
allResource: '所有资源',
|
allResource: '所有资源',
|
||||||
},
|
},
|
||||||
team: {
|
team: {
|
||||||
|
|||||||
@@ -654,8 +654,9 @@ const onCopyConfigClick = (target: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const checkClientWidth = () => {
|
const checkClientWidth = () => {
|
||||||
const oldLayout = getLocal('oldLayout');
|
let oldLayout = getLocal('oldLayout');
|
||||||
if (!oldLayout) {
|
if (!oldLayout) {
|
||||||
|
oldLayout = themeConfig.value.layout;
|
||||||
setLocal('oldLayout', themeConfig.value.layout);
|
setLocal('oldLayout', themeConfig.value.layout);
|
||||||
}
|
}
|
||||||
if (width.value < 1000) {
|
if (width.value < 1000) {
|
||||||
|
|||||||
@@ -138,9 +138,8 @@ onBeforeRouteUpdate((to) => {
|
|||||||
.horizontal-menu :deep(.el-sub-menu__title) {
|
.horizontal-menu :deep(.el-sub-menu__title) {
|
||||||
margin: 0 5px !important;
|
margin: 0 5px !important;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
max-width: 150px;
|
width: fit-content;
|
||||||
min-width: 120px; // 统一最小宽度
|
|
||||||
text-align: center; // 使文字居中对齐
|
text-align: center; // 使文字居中对齐
|
||||||
padding: 0 8px !important; // 统一内边距
|
padding: 0 16px !important; // 统一内边距
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-form :model="bizForm" ref="formRef" :rules="rules" label-width="auto">
|
<el-form :model="bizForm" ref="formRef" :rules="rules" label-width="auto">
|
||||||
<el-form-item prop="id" label="DB" required>
|
<el-form-item prop="id" label="DB" required>
|
||||||
<TagTreeResourceSelect
|
<ResourceSelect
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
v-model="selectRedis"
|
v-model="selectRedis"
|
||||||
@change="changeRedis"
|
@change="changeRedis"
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
:tag-path-node-type="NodeTypeTagPath"
|
:tag-path-node-type="NodeTypeTagPath"
|
||||||
:placeholder="$t('flow.selectRedisPlaceholder')"
|
:placeholder="$t('flow.selectRedisPlaceholder')"
|
||||||
>
|
>
|
||||||
</TagTreeResourceSelect>
|
</ResourceSelect>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="cmd" label="CMD" required>
|
<el-form-item prop="cmd" label="CMD" required>
|
||||||
@@ -21,12 +21,13 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
import TagTreeResourceSelect from '@/views/ops/component/TagTreeResourceSelect.vue';
|
import ResourceSelect from '@/views/ops/resource/ResourceSelect.vue';
|
||||||
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
|
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
|
||||||
import { redisApi } from '@/views/ops/redis/api';
|
import { redisApi } from '@/views/ops/redis/api';
|
||||||
import { sleep } from '@/common/utils/loading';
|
import { sleep } from '@/common/utils/loading';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { Rules } from '@/common/rule';
|
import { Rules } from '@/common/rule';
|
||||||
|
import { RedisIcon } from '@/views/ops/redis/resource';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -52,7 +53,7 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asyn
|
|||||||
await sleep(100);
|
await sleep(100);
|
||||||
return redisInfos.map((x: any) => {
|
return redisInfos.map((x: any) => {
|
||||||
x.tagPath = parentNode.key;
|
x.tagPath = parentNode.key;
|
||||||
return new TagTreeNode(`${x.code}`, x.name, NodeTypeRedis).withParams(x);
|
return new TagTreeNode(`${x.code}`, x.name, NodeTypeRedis).withParams(x).withIcon(RedisIcon);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,15 +62,18 @@ const NodeTypeRedis = new NodeType(1).withLoadNodesFunc(async (parentNode: TagTr
|
|||||||
const redisInfo = parentNode.params;
|
const redisInfo = parentNode.params;
|
||||||
|
|
||||||
let dbs: TagTreeNode[] = redisInfo.db.split(',').map((x: string) => {
|
let dbs: TagTreeNode[] = redisInfo.db.split(',').map((x: string) => {
|
||||||
return new TagTreeNode(x, `db${x}`, 2 as any).withIsLeaf(true).withParams({
|
return new TagTreeNode(x, `db${x}`, 2 as any)
|
||||||
id: redisInfo.id,
|
.withIsLeaf(true)
|
||||||
db: x,
|
.withParams({
|
||||||
name: `db${x}`,
|
id: redisInfo.id,
|
||||||
keys: 0,
|
db: x,
|
||||||
tagPath: redisInfo.tagPath,
|
name: `db${x}`,
|
||||||
redisName: redisInfo.name,
|
keys: 0,
|
||||||
code: redisInfo.code,
|
tagPath: redisInfo.tagPath,
|
||||||
});
|
redisName: redisInfo.name,
|
||||||
|
code: redisInfo.code,
|
||||||
|
})
|
||||||
|
.withIcon({ name: 'Coin', color: '#67c23a' });
|
||||||
});
|
});
|
||||||
|
|
||||||
if (redisInfo.mode == 'cluster') {
|
if (redisInfo.mode == 'cluster') {
|
||||||
|
|||||||
@@ -37,10 +37,9 @@
|
|||||||
:label="$t(TagResourceTypeEnum.Machine.label)"
|
:label="$t(TagResourceTypeEnum.Machine.label)"
|
||||||
:value="TagResourceTypeEnum.Machine.value"
|
:value="TagResourceTypeEnum.Machine.value"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<el-option
|
<el-option
|
||||||
:key="TagResourceTypeEnum.DbInstance.value"
|
:key="TagResourceTypeEnum.DbInstance.value"
|
||||||
:label="TagResourceTypeEnum.DbInstance.label"
|
:label="$t(TagResourceTypeEnum.DbInstance.label)"
|
||||||
:value="TagResourceTypeEnum.DbInstance.value"
|
:value="TagResourceTypeEnum.DbInstance.value"
|
||||||
/>
|
/>
|
||||||
<el-option
|
<el-option
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-splitter @resize="handleResize">
|
|
||||||
<el-splitter-panel :size="leftPaneSize + '%'" max="40%">
|
|
||||||
<slot name="left"></slot>
|
|
||||||
</el-splitter-panel>
|
|
||||||
|
|
||||||
<el-splitter-panel>
|
|
||||||
<slot name="right"></slot>
|
|
||||||
</el-splitter-panel>
|
|
||||||
</el-splitter>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { useWindowSize } from '@vueuse/core';
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
const emit = defineEmits(['resize']);
|
|
||||||
|
|
||||||
const { width } = useWindowSize();
|
|
||||||
|
|
||||||
const leftPaneSize = computed(() => (width.value >= 1600 ? 20 : 24));
|
|
||||||
|
|
||||||
// 处理 resize 事件
|
|
||||||
const handleResize = (event: any) => {
|
|
||||||
emit('resize', event);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss"></style>
|
|
||||||
@@ -1,276 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-card class="h-full flex tag-tree-card" body-class="!p-0 flex flex-col w-full">
|
|
||||||
<div class="tag-tree-header">
|
|
||||||
<el-input v-model="filterText" :placeholder="$t('tag.tagFilterPlaceholder')" clearable size="small" class="tag-tree-search w-full">
|
|
||||||
<template #prefix>
|
|
||||||
<SvgIcon class="tag-tree-search-icon" name="search" />
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
</div>
|
|
||||||
<el-scrollbar>
|
|
||||||
<el-tree
|
|
||||||
class="min-w-full inline-block"
|
|
||||||
ref="treeRef"
|
|
||||||
:highlight-current="true"
|
|
||||||
:indent="10"
|
|
||||||
:load="loadNode"
|
|
||||||
:props="treeProps"
|
|
||||||
lazy
|
|
||||||
node-key="key"
|
|
||||||
:expand-on-click-node="false"
|
|
||||||
:filter-node-method="filterNode"
|
|
||||||
@node-click="treeNodeClick"
|
|
||||||
@node-expand="treeNodeClick"
|
|
||||||
@node-contextmenu="nodeContextmenu"
|
|
||||||
:default-expanded-keys="props.defaultExpandedKeys"
|
|
||||||
>
|
|
||||||
<template #default="{ node, data }">
|
|
||||||
<div
|
|
||||||
:id="node.key"
|
|
||||||
class="w-full node-container flex items-center cursor-pointer select-none"
|
|
||||||
:class="data.type.nodeDblclickFunc ? 'select-none' : ''"
|
|
||||||
>
|
|
||||||
<span v-if="data.type.value == TagTreeNode.TagPath">
|
|
||||||
<tag-info :tag-path="data.label" />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<slot v-else :node="node" :data="data" name="prefix"></slot>
|
|
||||||
|
|
||||||
<span class="ml-1" :title="data.labelRemark">
|
|
||||||
<slot name="label" :data="data" v-if="!data.disabled"> {{ $t(data.label) }}</slot>
|
|
||||||
<!-- 禁用状态 -->
|
|
||||||
<slot name="disabledLabel" :data="data" v-else>
|
|
||||||
<el-link type="danger" disabled underline="never">
|
|
||||||
{{ `${$t(data.label)}` }}
|
|
||||||
</el-link>
|
|
||||||
</slot>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="ml-auto pr-1.5 text-[10px] text-gray-400">
|
|
||||||
<slot :node="node" :data="data" name="suffix"></slot>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-tree>
|
|
||||||
|
|
||||||
<contextmenu :dropdown="state.dropdown" :items="state.contextmenuItems" ref="contextmenuRef" @currentContextmenuClick="onCurrentContextmenuClick" />
|
|
||||||
</el-scrollbar>
|
|
||||||
</el-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { nextTick, onMounted, reactive, ref, toRefs, watch } 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';
|
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
resourceType: {
|
|
||||||
type: [Number, String],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
defaultExpandedKeys: {
|
|
||||||
type: [Array],
|
|
||||||
},
|
|
||||||
tagPathNodeType: {
|
|
||||||
type: [NodeType],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
load: {
|
|
||||||
type: Function,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
loadContextmenuItems: {
|
|
||||||
type: Function,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const treeProps = {
|
|
||||||
label: 'name',
|
|
||||||
children: 'zones',
|
|
||||||
isLeaf: 'isLeaf',
|
|
||||||
};
|
|
||||||
|
|
||||||
const emit = defineEmits(['nodeClick', 'currentContextmenuClick']);
|
|
||||||
const treeRef: any = ref(null);
|
|
||||||
const contextmenuRef = ref();
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
height: 600 as any,
|
|
||||||
filterText: '',
|
|
||||||
dropdown: {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
contextmenuItems: [],
|
|
||||||
opend: {},
|
|
||||||
});
|
|
||||||
const { filterText } = toRefs(state);
|
|
||||||
|
|
||||||
onMounted(async () => {});
|
|
||||||
|
|
||||||
watch(filterText, (val) => {
|
|
||||||
treeRef.value?.filter(val);
|
|
||||||
});
|
|
||||||
|
|
||||||
const filterNode = (value: string, data: any) => {
|
|
||||||
return !value || isPrefixSubsequence(value, data.label);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载标签树节点
|
|
||||||
*/
|
|
||||||
const loadTags = async () => {
|
|
||||||
const tags = await tagApi.getResourceTagPaths.request({ resourceType: props.resourceType });
|
|
||||||
const tagNodes = [];
|
|
||||||
for (let tagPath of tags) {
|
|
||||||
tagNodes.push(new TagTreeNode(tagPath, tagPath, props.tagPathNodeType));
|
|
||||||
}
|
|
||||||
return tagNodes;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载树节点
|
|
||||||
* @param { Object } node
|
|
||||||
* @param { Object } resolve
|
|
||||||
*/
|
|
||||||
const loadNode = async (node: any, resolve: (data: any) => void, reject: () => void) => {
|
|
||||||
if (typeof resolve !== 'function') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let nodes = [];
|
|
||||||
try {
|
|
||||||
if (node.level == 0) {
|
|
||||||
nodes = await loadTags();
|
|
||||||
} else if (props.load) {
|
|
||||||
nodes = await props.load(node);
|
|
||||||
} else {
|
|
||||||
nodes = await node.data.loadChildren();
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
// 调用 reject 以保持节点状态,并允许远程加载继续。
|
|
||||||
return reject();
|
|
||||||
}
|
|
||||||
return resolve(nodes);
|
|
||||||
};
|
|
||||||
|
|
||||||
let lastNodeClickTime = 0;
|
|
||||||
|
|
||||||
const treeNodeClick = async (data: any, node: any) => {
|
|
||||||
const currentClickNodeTime = Date.now();
|
|
||||||
if (currentClickNodeTime - lastNodeClickTime < 300) {
|
|
||||||
treeNodeDblclick(data, node);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lastNodeClickTime = currentClickNodeTime;
|
|
||||||
|
|
||||||
if (!data.disabled && !data.type.nodeDblclickFunc && data.type.nodeClickFunc) {
|
|
||||||
emit('nodeClick', data);
|
|
||||||
await data.type.nodeClickFunc(data);
|
|
||||||
}
|
|
||||||
// 关闭可能存在的右击菜单
|
|
||||||
contextmenuRef.value.closeContextmenu();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 树节点双击事件
|
|
||||||
const treeNodeDblclick = (data: any, node: any) => {
|
|
||||||
if (node.expanded) {
|
|
||||||
node.collapse();
|
|
||||||
} else {
|
|
||||||
node.expand();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.disabled && data.type.nodeDblclickFunc) {
|
|
||||||
data.type.nodeDblclickFunc(data);
|
|
||||||
}
|
|
||||||
// 关闭可能存在的右击菜单
|
|
||||||
contextmenuRef.value.closeContextmenu();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 树节点右击事件
|
|
||||||
const nodeContextmenu = (event: any, data: any) => {
|
|
||||||
if (data.disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载当前节点是否需要显示右击菜单
|
|
||||||
let items = data.type.contextMenuItems;
|
|
||||||
if (!items || items.length == 0) {
|
|
||||||
if (props.loadContextmenuItems) {
|
|
||||||
items = props.loadContextmenuItems(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!items) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state.contextmenuItems = items;
|
|
||||||
const { clientX, clientY } = event;
|
|
||||||
state.dropdown.x = clientX;
|
|
||||||
state.dropdown.y = clientY;
|
|
||||||
contextmenuRef.value.openContextmenu(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCurrentContextmenuClick = (clickData: any) => {
|
|
||||||
emit('currentContextmenuClick', clickData);
|
|
||||||
};
|
|
||||||
|
|
||||||
const reloadNode = (nodeKey: any) => {
|
|
||||||
let node = getNode(nodeKey);
|
|
||||||
node.loaded = false;
|
|
||||||
node.expand();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getNode = (nodeKey: any) => {
|
|
||||||
let node = treeRef.value.getNode(nodeKey);
|
|
||||||
if (!node) {
|
|
||||||
throw new Error('未找到节点: ' + nodeKey);
|
|
||||||
}
|
|
||||||
return node;
|
|
||||||
};
|
|
||||||
|
|
||||||
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({
|
|
||||||
reloadNode,
|
|
||||||
getNode,
|
|
||||||
setCurrentKey,
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.tag-tree-card {
|
|
||||||
:deep(.el-card__body) {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-tree-header {
|
|
||||||
padding: 4px 6px;
|
|
||||||
border-bottom: 1px solid var(--el-border-color-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-tree-search {
|
|
||||||
:deep(.el-input__wrapper) {
|
|
||||||
border-radius: 14px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -5,38 +5,36 @@
|
|||||||
<DrawerHeader :header="title" :back="cancel" />
|
<DrawerHeader :header="title" :back="cancel" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
|
<el-form :model="form" ref="dbForm" :rules="rules" label-position="top" label-width="auto">
|
||||||
<el-divider content-position="left">{{ $t('common.basic') }}</el-divider>
|
<el-divider content-position="left">{{ $t('common.basic') }}</el-divider>
|
||||||
|
|
||||||
<el-form-item prop="taskName" :label="$t('db.taskName')" required>
|
<el-form-item prop="taskName" :label="$t('db.taskName')" required>
|
||||||
<el-input v-model.trim="form.taskName" auto-complete="off" />
|
<el-input v-model.trim="form.taskName" auto-complete="off" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-row class="!w-full">
|
||||||
<el-row class="!w-full">
|
<el-col :span="12">
|
||||||
<el-col :span="12">
|
<el-form-item prop="status" :label="$t('common.status')" label-position="left">
|
||||||
<el-form-item prop="status" :label="$t('common.status')">
|
<el-switch
|
||||||
<el-switch
|
v-model="form.status"
|
||||||
v-model="form.status"
|
inline-prompt
|
||||||
inline-prompt
|
:active-text="$t('common.enable')"
|
||||||
:active-text="$t('common.enable')"
|
:inactive-text="$t('common.disable')"
|
||||||
:inactive-text="$t('common.disable')"
|
:active-value="1"
|
||||||
:active-value="1"
|
:inactive-value="-1"
|
||||||
:inactive-value="-1"
|
/>
|
||||||
/>
|
</el-form-item>
|
||||||
</el-form-item>
|
</el-col>
|
||||||
</el-col>
|
|
||||||
|
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item prop="cronAble" :label="$t('db.cronAble')" required>
|
<el-form-item prop="cronAble" :label="$t('db.cronAble')" required label-position="left">
|
||||||
<el-radio-group v-model="form.cronAble">
|
<el-radio-group v-model="form.cronAble">
|
||||||
<el-radio :label="$t('common.yes')" :value="1" />
|
<el-radio :label="$t('common.yes')" :value="1" />
|
||||||
<el-radio :label="$t('common.no')" :value="-1" />
|
<el-radio :label="$t('common.no')" :value="-1" />
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item prop="cron" label="cron" :required="form.cronAble == 1">
|
<el-form-item prop="cron" label="cron" :required="form.cronAble == 1">
|
||||||
<CrontabInput v-model="form.cron" />
|
<CrontabInput v-model="form.cron" />
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
<DrawerHeader :header="title" :back="cancel" />
|
<DrawerHeader :header="title" :back="cancel" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
|
<el-form :model="form" ref="dbForm" :rules="rules" label-position="top" label-width="auto">
|
||||||
<el-tabs v-model="tabActiveName">
|
<el-tabs v-model="tabActiveName">
|
||||||
<el-tab-pane :label="$t('common.basic')" :name="basicTab">
|
<el-tab-pane :label="$t('common.basic')" :name="basicTab">
|
||||||
<el-row>
|
<el-row :gutter="10">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item prop="taskName" :label="$t('db.taskName')" required>
|
<el-form-item prop="taskName" :label="$t('db.taskName')" required>
|
||||||
<el-input v-model.trim="form.taskName" auto-complete="off" />
|
<el-input v-model.trim="form.taskName" auto-complete="off" />
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-form-item prop="status" :label="$t('common.status')" label-width="60" required>
|
<el-form-item prop="status" :label="$t('common.status')" label-position="left" label-width="60" required>
|
||||||
<el-switch
|
<el-switch
|
||||||
v-model="form.status"
|
v-model="form.status"
|
||||||
inline-prompt
|
inline-prompt
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
<monaco-editor height="200px" class="task-sql" language="sql" v-model="form.dataSql" />
|
<monaco-editor height="200px" class="task-sql" language="sql" v-model="form.dataSql" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-row>
|
<el-row :gutter="10">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item prop="targetTableName" :label="$t('db.targetDbTable')" required>
|
<el-form-item prop="targetTableName" :label="$t('db.targetDbTable')" required>
|
||||||
<el-select v-model="form.targetTableName" filterable>
|
<el-select v-model="form.targetTableName" filterable>
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row>
|
<el-row :gutter="10">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTooltip :label="$t('db.updateField')" prop="updField" :tooltip="$t('db.updateFieldTips')">
|
<FormItemTooltip :label="$t('db.updateField')" prop="updField" :tooltip="$t('db.updateFieldTips')">
|
||||||
<el-input v-model.trim="form.updField" :placeholder="$t('db.updateFiledPlaceholder')" auto-complete="off" />
|
<el-input v-model.trim="form.updField" :placeholder="$t('db.updateFiledPlaceholder')" auto-complete="off" />
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row>
|
<el-row :gutter="10">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTooltip :label="$t('db.fieldValueSrc')" prop="updFieldSrc" :tooltip="$t('db.fieldValueSrcTips')">
|
<FormItemTooltip :label="$t('db.fieldValueSrc')" prop="updFieldSrc" :tooltip="$t('db.fieldValueSrcTips')">
|
||||||
<el-input v-model.trim="form.updFieldSrc" :placeholder="$t('db.fieldValueSrcPlaceholder')" auto-complete="off" />
|
<el-input v-model.trim="form.updFieldSrc" :placeholder="$t('db.fieldValueSrcPlaceholder')" auto-complete="off" />
|
||||||
@@ -105,17 +105,32 @@
|
|||||||
|
|
||||||
<el-tab-pane :label="$t('db.fieldMap')" :name="fieldTab" :disabled="!baseFieldCompleted">
|
<el-tab-pane :label="$t('db.fieldMap')" :name="fieldTab" :disabled="!baseFieldCompleted">
|
||||||
<el-form-item prop="fieldMap" :label="$t('db.fieldMap')" required>
|
<el-form-item prop="fieldMap" :label="$t('db.fieldMap')" required>
|
||||||
<el-table :data="form.fieldMap" :max-height="fieldMapTableHeight" size="small">
|
<el-table :data="form.fieldMap" :max-height="fieldMapTableHeight">
|
||||||
<el-table-column prop="src" :label="$t('db.srcField')" :width="200" />
|
<el-table-column prop="src" :label="$t('db.srcField')" :width="200"></el-table-column>
|
||||||
<el-table-column prop="target" :label="$t('db.targetField')">
|
<el-table-column prop="target" :label="$t('db.targetField')">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-select v-model="scope.row.target" allow-create filterable>
|
<el-select v-model="scope.row.target" allow-create filterable>
|
||||||
|
<template #label="{ label, value }">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<el-text tag="b">{{ value }}</el-text>
|
||||||
|
<el-text size="small">{{ label }}</el-text>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in state.targetColumnList"
|
v-for="item in state.targetColumnList"
|
||||||
:key="item.columnName"
|
:key="item.columnName"
|
||||||
:label="item.columnName + ` ${item.columnType}` + (item.columnComment && ' - ' + item.columnComment)"
|
:label="`${item.columnType}${item.columnComment && ' - ' + item.columnComment}`"
|
||||||
:value="item.columnName"
|
:value="item.columnName"
|
||||||
/>
|
>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
{{ item.columnName }}
|
||||||
|
|
||||||
|
<el-text size="small">
|
||||||
|
{{ item.columnType }}{{ item.columnComment && ' - ' + item.columnComment }}
|
||||||
|
</el-text>
|
||||||
|
</div>
|
||||||
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -305,7 +320,7 @@ watch(dialogVisible, async (newValue: boolean) => {
|
|||||||
state.tabActiveName = 'basic';
|
state.tabActiveName = 'basic';
|
||||||
const propsData = props.data as any;
|
const propsData = props.data as any;
|
||||||
if (!propsData?.id) {
|
if (!propsData?.id) {
|
||||||
let d = {} as FormData;
|
let d = { taskCron: '' } as FormData;
|
||||||
Object.assign(d, basicFormData);
|
Object.assign(d, basicFormData);
|
||||||
state.form = d;
|
state.form = d;
|
||||||
return;
|
return;
|
||||||
@@ -401,6 +416,7 @@ const refreshPreviewInsertSql = () => {
|
|||||||
const onSelectSrcDb = async (params: any) => {
|
const onSelectSrcDb = async (params: any) => {
|
||||||
// 初始化数据源
|
// 初始化数据源
|
||||||
params.databases = params.dbs; // 数据源里需要这个值
|
params.databases = params.dbs; // 数据源里需要这个值
|
||||||
|
console.log(params.dbs);
|
||||||
state.srcDbInst = await DbInst.getOrNewInst(params);
|
state.srcDbInst = await DbInst.getOrNewInst(params);
|
||||||
registerDbCompletionItemProvider(params.id, params.db, params.dbs, params.type);
|
registerDbCompletionItemProvider(params.id, params.db, params.dbs, params.type);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,31 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<TagTreeResourceSelect
|
<ResourceSelect v-bind="$attrs" v-model="selectNode" @change="changeNode" :resource-type="TagResourceTypePath.Db" :tag-path-node-type="NodeTypeDbInst">
|
||||||
v-bind="$attrs"
|
|
||||||
v-model="selectNode"
|
|
||||||
@change="changeNode"
|
|
||||||
:resource-type="TagResourceTypePath.Db"
|
|
||||||
:tag-path-node-type="NodeTypeTagPath"
|
|
||||||
>
|
|
||||||
<template #iconPrefix>
|
<template #iconPrefix>
|
||||||
<SvgIcon v-if="dbType && getDbDialect(dbType)" :name="getDbDialect(dbType).getInfo().icon" :size="18" />
|
<SvgIcon v-if="dbType && getDbDialect(dbType)" :name="getDbDialect(dbType).getInfo().icon" :size="18" />
|
||||||
</template>
|
</template>
|
||||||
<template #prefix="{ data }">
|
</ResourceSelect>
|
||||||
<SvgIcon v-if="data.type.value == SqlExecNodeType.DbInst" :name="getDbDialect(data.params.type).getInfo().icon" :size="18" />
|
|
||||||
<SvgIcon v-if="data.icon" :name="data.icon.name" :color="data.icon.color" />
|
|
||||||
</template>
|
|
||||||
</TagTreeResourceSelect>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
import { TagResourceTypeEnum, TagResourceTypePath } from '@/common/commonEnum';
|
import { TagResourceTypeEnum, TagResourceTypePath } from '@/common/commonEnum';
|
||||||
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
|
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
|
||||||
import { dbApi } from '@/views/ops/db/api';
|
import { dbApi } from '@/views/ops/db/api';
|
||||||
import { sleep } from '@/common/utils/loading';
|
import { sleep } from '@/common/utils/loading';
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
import { getDbDialect, schemaDbTypes } from '@/views/ops/db/dialect';
|
||||||
import { getDbDialect, noSchemaTypes } from '@/views/ops/db/dialect';
|
import ResourceSelect from '@/views/ops/resource/ResourceSelect.vue';
|
||||||
import TagTreeResourceSelect from '../../component/TagTreeResourceSelect.vue';
|
import NodeDbInst from '@/views/ops/db/resource/NodeDbInst.vue';
|
||||||
import { computed } from 'vue';
|
import NodeDb from '@/views/ops/db/resource/NodeDb.vue';
|
||||||
import { DbInst } from '../db';
|
import { DbIcon, SchemaIcon } from '@/views/ops/db/resource';
|
||||||
|
import { DbInst } from '@/views/ops/db/db';
|
||||||
|
|
||||||
const dbId = defineModel<number>('dbId');
|
const dbId = defineModel<number>('dbId');
|
||||||
const instName = defineModel<string>('instName');
|
const instName = defineModel<string>('instName');
|
||||||
@@ -35,20 +27,6 @@ const dbType = defineModel<string>('dbType');
|
|||||||
|
|
||||||
const emits = defineEmits(['selectDb']);
|
const emits = defineEmits(['selectDb']);
|
||||||
|
|
||||||
/**
|
|
||||||
* 树节点类型
|
|
||||||
*/
|
|
||||||
class SqlExecNodeType {
|
|
||||||
static DbInst = 1;
|
|
||||||
static Db = 2;
|
|
||||||
static TableMenu = 3;
|
|
||||||
static SqlMenu = 4;
|
|
||||||
static Table = 5;
|
|
||||||
static Sql = 6;
|
|
||||||
static PgSchemaMenu = 7;
|
|
||||||
static PgSchema = 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectNode = computed({
|
const selectNode = computed({
|
||||||
get: () => {
|
get: () => {
|
||||||
return dbName.value ? `${tagPath.value} > ${instName.value} > ${dbName.value}` : '';
|
return dbName.value ? `${tagPath.value} > ${instName.value} > ${dbName.value}` : '';
|
||||||
@@ -58,90 +36,94 @@ const selectNode = computed({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const DbIcon = {
|
const NodeTypeDbInst = new NodeType(TagResourceTypeEnum.DbInstance.value).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
name: 'Coin',
|
const tagPath = parentNode.key;
|
||||||
color: '#67c23a',
|
|
||||||
};
|
|
||||||
|
|
||||||
// pgsql schema icon
|
const dbInstancesRes = await dbApi.instances.request({ tagPath, pageSize: 100 });
|
||||||
const SchemaIcon = {
|
const dbInstances = dbInstancesRes.list;
|
||||||
name: 'List',
|
if (!dbInstances) {
|
||||||
color: '#67c23a',
|
|
||||||
};
|
|
||||||
|
|
||||||
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
|
||||||
const dbInfoRes = await dbApi.dbs.request({ tagPath: parentNode.key });
|
|
||||||
const dbInfos = dbInfoRes.list;
|
|
||||||
if (!dbInfos) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 防止过快加载会出现一闪而过,对眼睛不好
|
// 防止过快加载会出现一闪而过,对眼睛不好
|
||||||
await sleep(100);
|
await sleep(100);
|
||||||
return dbInfos?.map((x: any) => {
|
return dbInstances?.map((x: any) => {
|
||||||
x.tagPath = parentNode.key;
|
x.tagPath = tagPath;
|
||||||
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x);
|
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeDbConf).withParams(x).withNodeComponent(NodeDbInst);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/** mysql类型的数据库,没有schema层 */
|
const NodeTypeDbConf = new NodeType(TagResourceTypeEnum.Db.value).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
const noSchemaType = (type: string) => {
|
const params = parentNode.params;
|
||||||
return noSchemaTypes.includes(type);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 数据库实例节点类型
|
const tagPath = params.tagPath;
|
||||||
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
const authCerts = {} as any;
|
||||||
|
for (let authCert of params.authCerts) {
|
||||||
|
authCerts[authCert.name] = authCert;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbInfoRes = await dbApi.dbs.request({
|
||||||
|
tagPath: `${tagPath}${TagResourceTypeEnum.DbInstance.value}|${params.code}`,
|
||||||
|
});
|
||||||
|
const dbInfos = dbInfoRes.list;
|
||||||
|
if (!dbInfos) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbInfos?.map((x: any) => {
|
||||||
|
x.tagPath = tagPath;
|
||||||
|
x.username = authCerts[x.authCertName]?.username;
|
||||||
|
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeDbs).withParams(x).withIcon(DbIcon).withNodeComponent(NodeDb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 数据库列表名类型
|
||||||
|
const NodeTypeDbs = new NodeType(222).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
const params = parentNode.params;
|
const params = parentNode.params;
|
||||||
const dbs = (await DbInst.getDbNames(params))?.sort();
|
const dbs = (await DbInst.getDbNames(params))?.sort();
|
||||||
let fn: NodeType;
|
const hasSchema = schemaDbTypes.includes(params.type);
|
||||||
if (noSchemaType(params.type)) {
|
const nodeType = hasSchema ? NodeTypeDbSchema : NodeTypeNoSchemaDb;
|
||||||
fn = MysqlNodeTypes;
|
|
||||||
} else {
|
|
||||||
fn = PgNodeTypes;
|
|
||||||
}
|
|
||||||
return dbs.map((x: any) => {
|
return dbs.map((x: any) => {
|
||||||
let tagTreeNode = new TagTreeNode(`${parentNode.key}.${x}`, `${x}`, fn)
|
return TagTreeNode.new(parentNode, `${parentNode.key}.${x}`, x, nodeType)
|
||||||
.withParams({
|
.withParams({
|
||||||
tagPath: params.tagPath,
|
tagPath: params.tagPath,
|
||||||
id: params.id,
|
id: params.id,
|
||||||
code: params.code,
|
|
||||||
instanceId: params.instanceId,
|
|
||||||
name: params.name,
|
name: params.name,
|
||||||
type: params.type,
|
type: params.type,
|
||||||
host: `${params.host}:${params.port}`,
|
host: `${params.host}:${params.port}`,
|
||||||
dbs: dbs,
|
dbs: dbs,
|
||||||
db: x,
|
db: x,
|
||||||
|
code: params.code,
|
||||||
})
|
})
|
||||||
.withIcon(DbIcon);
|
.withIcon(DbIcon)
|
||||||
if (noSchemaType(params.type)) {
|
.withIsLeaf(!hasSchema);
|
||||||
tagTreeNode.isLeaf = true;
|
|
||||||
}
|
|
||||||
return tagTreeNode;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 数据库节点
|
// 数据库节点
|
||||||
const PgNodeTypes = new NodeType(SqlExecNodeType.Db).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
const NodeTypeDbSchema = new NodeType(2).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
// pg类数据库会多一层schema
|
|
||||||
const params = parentNode.params;
|
const params = parentNode.params;
|
||||||
|
params.parentKey = parentNode.key;
|
||||||
const { id, db } = params;
|
const { id, db } = params;
|
||||||
const schemaNames = await dbApi.pgSchemas.request({ id, db });
|
const schemaNames = await dbApi.pgSchemas.request({ id, db });
|
||||||
|
const dbs = schemaNames.map((x: any) => `${db}/${x}`);
|
||||||
return schemaNames.map((sn: any) => {
|
return schemaNames.map((sn: any) => {
|
||||||
// 将db变更为 db/schema;
|
// 将db变更为 db/schema;
|
||||||
const nParams = { ...params };
|
const nParams = { ...params };
|
||||||
nParams.schema = sn;
|
nParams.schema = sn;
|
||||||
nParams.db = nParams.db + '/' + sn;
|
nParams.db = nParams.db + '/' + sn;
|
||||||
nParams.dbs = schemaNames;
|
nParams.dbs = dbs;
|
||||||
let tagTreeNode = new TagTreeNode(`${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresSchema).withParams(nParams).withIcon(SchemaIcon);
|
return TagTreeNode.new(parentNode, `${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresSchema)
|
||||||
tagTreeNode.isLeaf = true;
|
.withParams(nParams)
|
||||||
return tagTreeNode;
|
.withIcon(SchemaIcon)
|
||||||
|
.withIsLeaf(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const MysqlNodeTypes = new NodeType(SqlExecNodeType.Db);
|
|
||||||
|
|
||||||
// postgres schema模式
|
// postgres schema模式
|
||||||
const NodeTypePostgresSchema = new NodeType(SqlExecNodeType.PgSchema);
|
const NodeTypePostgresSchema = new NodeType(99);
|
||||||
|
const NodeTypeNoSchemaDb = new NodeType(99);
|
||||||
|
|
||||||
const changeNode = (nodeData: TagTreeNode) => {
|
const changeNode = (nodeData: TagTreeNode) => {
|
||||||
const params = nodeData.params;
|
const params = nodeData.params;
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-row>
|
<el-row>
|
||||||
<span v-if="dt.hasUpdatedFileds" class="mt-1">
|
<span v-if="dt.hasUpdatedFields" class="mt-1">
|
||||||
<span>
|
<span>
|
||||||
<el-link type="success" underline="never" @click="submitUpdateFields(dt)"
|
<el-link type="success" underline="never" @click="submitUpdateFields(dt)"
|
||||||
><span style="font-size: 12px">{{ $t('common.submit') }}</span></el-link
|
><span style="font-size: 12px">{{ $t('common.submit') }}</span></el-link
|
||||||
@@ -110,6 +110,7 @@
|
|||||||
:data="dt.data"
|
:data="dt.data"
|
||||||
:table="dt.table"
|
:table="dt.table"
|
||||||
:columns="dt.tableColumn"
|
:columns="dt.tableColumn"
|
||||||
|
:column-more-actions="['fixed']"
|
||||||
:loading="dt.loading"
|
:loading="dt.loading"
|
||||||
:abort-fn="dt.abortFn"
|
:abort-fn="dt.abortFn"
|
||||||
:height="tableDataHeight"
|
:height="tableDataHeight"
|
||||||
@@ -199,7 +200,7 @@ class ExecResTab {
|
|||||||
/**
|
/**
|
||||||
* 是否有更新字段
|
* 是否有更新字段
|
||||||
*/
|
*/
|
||||||
hasUpdatedFileds: boolean;
|
hasUpdatedFields: boolean;
|
||||||
|
|
||||||
errorMsg: string;
|
errorMsg: string;
|
||||||
|
|
||||||
@@ -305,13 +306,8 @@ const getKey = () => {
|
|||||||
* 执行sql
|
* 执行sql
|
||||||
*/
|
*/
|
||||||
const onRunSql = async (newTab = false) => {
|
const onRunSql = async (newTab = false) => {
|
||||||
// 没有选中的文本,则为全部文本
|
const sqls = getSql();
|
||||||
let sql = getSql() as string;
|
notBlank(sqls, t('db.noSelectRunSqlMsg'));
|
||||||
notBlank(sql && sql.trim(), t('db.noSelctRunSqlTips'));
|
|
||||||
// 去除字符串前的空格、换行等
|
|
||||||
sql = sql.replace(/(^\s*)/g, '');
|
|
||||||
|
|
||||||
const sqls = splitSql(sql);
|
|
||||||
|
|
||||||
if (sqls.length == 1) {
|
if (sqls.length == 1) {
|
||||||
const oneSql = sqls[0];
|
const oneSql = sqls[0];
|
||||||
@@ -522,11 +518,56 @@ const runSql = async (sql: string, remark = '', newTab = false) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function splitSql(sql: string, delimiter: string = ';') {
|
/**
|
||||||
|
* 获取sql,如果有鼠标选中,则返回选中内容,否则返回当前光标附近的sql
|
||||||
|
*/
|
||||||
|
const getSql = (): string[] => {
|
||||||
|
// 编辑器还没初始化
|
||||||
|
if (!monacoEditor?.getModel()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let sql = '' as string | undefined;
|
||||||
|
// 选择选中的sql
|
||||||
|
let selection = monacoEditor.getSelection();
|
||||||
|
if (selection) {
|
||||||
|
sql = monacoEditor.getModel()?.getValueInRange(selection);
|
||||||
|
sql = sql?.replace(/(^\s*)/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有选中的内容且不为空,直接返回
|
||||||
|
if (sql && sql.trim()) {
|
||||||
|
return splitSqlStatements(sql).map((x) => x.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有选中任何内容时,自动选择当前光标所在的SQL语句行
|
||||||
|
const currentPosition = monacoEditor.getPosition();
|
||||||
|
if (currentPosition) {
|
||||||
|
const model = monacoEditor.getModel();
|
||||||
|
if (model) {
|
||||||
|
const fullSql = model.getValue();
|
||||||
|
const sqlStatement = getCurrentStatement(fullSql, currentPosition, model);
|
||||||
|
if (sqlStatement) {
|
||||||
|
return [sqlStatement];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用SQL解析器,用于提取SQL语句及其位置信息
|
||||||
|
* @param sql 完整的SQL文本
|
||||||
|
* @param delimiter SQL语句分隔符,默认为分号
|
||||||
|
* @param withPosition 是否需要返回位置信息
|
||||||
|
*/
|
||||||
|
function splitSqlStatements(sql: string, delimiter: string = ';') {
|
||||||
let state = 'normal';
|
let state = 'normal';
|
||||||
let buffer = '';
|
let buffer = '';
|
||||||
let result = [];
|
let result = [];
|
||||||
let inString = null; // 用于记录当前字符串的引号类型(' 或 ")
|
let inString = null; // 用于记录当前字符串的引号类型(' 或 ")
|
||||||
|
let startPos = 0;
|
||||||
|
|
||||||
for (let i = 0; i < sql.length; i++) {
|
for (let i = 0; i < sql.length; i++) {
|
||||||
const char = sql[i];
|
const char = sql[i];
|
||||||
@@ -535,9 +576,11 @@ function splitSql(sql: string, delimiter: string = ';') {
|
|||||||
if (state === 'normal') {
|
if (state === 'normal') {
|
||||||
if (char === '-' && nextChar === '-') {
|
if (char === '-' && nextChar === '-') {
|
||||||
state = 'singleLineComment';
|
state = 'singleLineComment';
|
||||||
|
// buffer += char + nextChar;
|
||||||
i++; // 跳过下一个字符
|
i++; // 跳过下一个字符
|
||||||
} else if (char === '/' && nextChar === '*') {
|
} else if (char === '/' && nextChar === '*') {
|
||||||
state = 'multiLineComment';
|
state = 'multiLineComment';
|
||||||
|
// buffer += char + nextChar;
|
||||||
i++; // 跳过下一个字符
|
i++; // 跳过下一个字符
|
||||||
} else if (char === "'" || char === '"') {
|
} else if (char === "'" || char === '"') {
|
||||||
state = 'string';
|
state = 'string';
|
||||||
@@ -545,9 +588,14 @@ function splitSql(sql: string, delimiter: string = ';') {
|
|||||||
buffer += char;
|
buffer += char;
|
||||||
} else if (char === delimiter) {
|
} else if (char === delimiter) {
|
||||||
if (buffer.trim()) {
|
if (buffer.trim()) {
|
||||||
result.push(buffer.trim());
|
result.push({
|
||||||
|
text: buffer.trim(),
|
||||||
|
start: startPos,
|
||||||
|
end: i,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
buffer = '';
|
buffer = '';
|
||||||
|
startPos = i + 1;
|
||||||
} else {
|
} else {
|
||||||
buffer += char;
|
buffer += char;
|
||||||
}
|
}
|
||||||
@@ -562,45 +610,70 @@ function splitSql(sql: string, delimiter: string = ';') {
|
|||||||
inString = null;
|
inString = null;
|
||||||
}
|
}
|
||||||
} else if (state === 'singleLineComment') {
|
} else if (state === 'singleLineComment') {
|
||||||
|
// buffer += char;
|
||||||
if (char === '\n') {
|
if (char === '\n') {
|
||||||
state = 'normal';
|
state = 'normal';
|
||||||
}
|
}
|
||||||
} else if (state === 'multiLineComment') {
|
} else if (state === 'multiLineComment') {
|
||||||
|
// buffer += char;
|
||||||
if (char === '*' && nextChar === '/') {
|
if (char === '*' && nextChar === '/') {
|
||||||
|
buffer += nextChar;
|
||||||
state = 'normal';
|
state = 'normal';
|
||||||
i++; // 跳过下一个字符
|
i++; // 跳过下一个字符
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理最后一个语句(没有以分号结尾的情况)
|
||||||
if (buffer.trim()) {
|
if (buffer.trim()) {
|
||||||
result.push(buffer.trim());
|
result.push({
|
||||||
|
text: buffer.trim(),
|
||||||
|
start: startPos,
|
||||||
|
end: sql.length,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取sql,如果有鼠标选中,则返回选中内容,否则返回输入框内所有内容
|
* 获取光标所在的SQL语句
|
||||||
|
* @param fullSql 完整的SQL文本
|
||||||
|
* @param position 光标位置
|
||||||
|
* @param model Monaco编辑器模型
|
||||||
*/
|
*/
|
||||||
const getSql = () => {
|
function getCurrentStatement(fullSql: string, position: monaco.Position, model: monaco.editor.ITextModel): string | null {
|
||||||
let res = '' as string | undefined;
|
// 使用通用SQL解析器来分割SQL语句,并记录每个语句的位置
|
||||||
// 编辑器还没初始化
|
const statements: { text: string; start: number; end: number }[] = splitSqlStatements(fullSql);
|
||||||
if (!monacoEditor?.getModel()) {
|
|
||||||
return res;
|
// 根据光标位置找到对应的SQL语句
|
||||||
}
|
if (position) {
|
||||||
// 选择选中的sql
|
const offset = model.getOffsetAt(position);
|
||||||
let selection = monacoEditor.getSelection();
|
|
||||||
if (selection) {
|
// 遍历所有语句,找到光标所在的语句
|
||||||
res = monacoEditor.getModel()?.getValueInRange(selection);
|
for (let i = 0; i < statements.length; i++) {
|
||||||
|
const stmt = statements[i];
|
||||||
|
// 光标在语句范围内(包括末尾分号)
|
||||||
|
if (offset >= stmt.start && offset <= stmt.end) {
|
||||||
|
return stmt.text;
|
||||||
|
}
|
||||||
|
// 光标在语句分号后一个位置
|
||||||
|
if (offset === stmt.end + 1) {
|
||||||
|
return stmt.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果光标处没有SQL,则执行光标前的最后一个SQL
|
||||||
|
for (let i = statements.length - 1; i >= 0; i--) {
|
||||||
|
const stmt = statements[i];
|
||||||
|
if (offset > stmt.end) {
|
||||||
|
return stmt.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 整个编辑器的sql
|
return null;
|
||||||
if (!res) {
|
}
|
||||||
return monacoEditor.getModel()?.getValue();
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveSql = async () => {
|
const saveSql = async () => {
|
||||||
const sql = monacoEditor.getModel()?.getValue();
|
const sql = monacoEditor.getModel()?.getValue();
|
||||||
@@ -710,7 +783,7 @@ const getUploadSqlFileUrl = () => {
|
|||||||
|
|
||||||
const changeUpdatedField = (updatedFields: any, dt: ExecResTab) => {
|
const changeUpdatedField = (updatedFields: any, dt: ExecResTab) => {
|
||||||
// 如果存在要更新字段,则显示提交和取消按钮
|
// 如果存在要更新字段,则显示提交和取消按钮
|
||||||
dt.hasUpdatedFileds = updatedFields && updatedFields.size > 0;
|
dt.hasUpdatedFields = updatedFields && updatedFields.size > 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -97,19 +97,19 @@
|
|||||||
</span>
|
</span>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
<el-dropdown-item command="sort-asc">
|
<el-dropdown-item v-if="showColumnActionSort" command="sort-asc">
|
||||||
<SvgIcon name="top" class="mr-1" />
|
<SvgIcon name="top" class="mr-1" />
|
||||||
{{ $t('db.asc') }}
|
{{ $t('db.asc') }}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item command="sort-desc">
|
<el-dropdown-item v-if="showColumnActionSort" command="sort-desc">
|
||||||
<SvgIcon name="bottom" class="mr-1" />
|
<SvgIcon name="bottom" class="mr-1" />
|
||||||
{{ $t('db.desc') }}
|
{{ $t('db.desc') }}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item v-if="!column.fixed" command="fix">
|
<el-dropdown-item v-if="showColumnActionFixed && !column.fixed" command="fix">
|
||||||
<SvgIcon name="Paperclip" class="mr-1" />
|
<SvgIcon name="Paperclip" class="mr-1" />
|
||||||
{{ $t('db.fixed') }}
|
{{ $t('db.fixed') }}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item v-else command="unfix">
|
<el-dropdown-item v-if="showColumnActionFixed && column.fixed" command="unfix">
|
||||||
<SvgIcon name="Minus" class="mr-1" />
|
<SvgIcon name="Minus" class="mr-1" />
|
||||||
{{ $t('db.cancelFiexd') }}
|
{{ $t('db.cancelFiexd') }}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
@@ -201,7 +201,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onBeforeUnmount, onMounted, reactive, ref, toRefs, watch, Ref } from 'vue';
|
import { onBeforeUnmount, onMounted, reactive, ref, toRefs, watch, Ref, computed } from 'vue';
|
||||||
import { ElInput, ElMessage } from 'element-plus';
|
import { ElInput, ElMessage } from 'element-plus';
|
||||||
import { copyToClipboard } from '@/common/utils/string';
|
import { copyToClipboard } from '@/common/utils/string';
|
||||||
import { DbInst, DbThemeConfig } from '@/views/ops/db/db';
|
import { DbInst, DbThemeConfig } from '@/views/ops/db/db';
|
||||||
@@ -238,6 +238,10 @@ const props = defineProps({
|
|||||||
columns: {
|
columns: {
|
||||||
type: Array<any>,
|
type: Array<any>,
|
||||||
},
|
},
|
||||||
|
columnMoreActions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => ['sort', 'fixed'],
|
||||||
|
},
|
||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
@@ -452,6 +456,16 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 显示列排序
|
||||||
|
const showColumnActionSort = computed(() => {
|
||||||
|
return props.columnMoreActions.includes('sort');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示列固定
|
||||||
|
const showColumnActionFixed = computed(() => {
|
||||||
|
return props.columnMoreActions.includes('fixed');
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
console.log('in DbTable mounted');
|
console.log('in DbTable mounted');
|
||||||
state.tableHeight = props.height;
|
state.tableHeight = props.height;
|
||||||
|
|||||||
13
frontend/src/views/ops/db/resource/NodeDb.vue
Normal file
13
frontend/src/views/ops/db/resource/NodeDb.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<BaseTreeNode v-bind="$attrs">
|
||||||
|
<template #suffix="{ data }">
|
||||||
|
<span v-if="data.params.username">{{ ` ${data.params.username}` }}</span>
|
||||||
|
</template>
|
||||||
|
</BaseTreeNode>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BaseTreeNode from '@/views/ops/resource/BaseTreeNode.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
@@ -16,9 +16,9 @@
|
|||||||
<el-descriptions-item label="version">
|
<el-descriptions-item label="version">
|
||||||
<span v-loading="loadingServerInfo"> {{ `${dbServerInfo?.version}` }}</span>
|
<span v-loading="loadingServerInfo"> {{ `${dbServerInfo?.version}` }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item :label="$t('db.acName')">
|
<!-- <el-descriptions-item :label="$t('db.acName')">
|
||||||
{{ data.params.authCertName }}
|
{{ data.params.authCertName }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item> -->
|
||||||
<el-descriptions-item :label="$t('common.remark')">
|
<el-descriptions-item :label="$t('common.remark')">
|
||||||
{{ data.params.remark }}
|
{{ data.params.remark }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@@ -45,7 +45,7 @@ const showDbInfo = async (db: any) => {
|
|||||||
if (dbServerInfo.value) {
|
if (dbServerInfo.value) {
|
||||||
dbServerInfo.value.version = '';
|
dbServerInfo.value.version = '';
|
||||||
}
|
}
|
||||||
serverInfoReqParam.value.instanceId = db.instanceId;
|
serverInfoReqParam.value.instanceId = db.id;
|
||||||
await getDbServerInfo();
|
await getDbServerInfo();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ContextmenuItem } from '@/components/contextmenu';
|
import { ContextmenuItem } from '@/components/contextmenu';
|
||||||
|
|
||||||
import { NodeType, TagTreeNode, ResourceConfig } from '../../component/tag';
|
import { NodeType, TagTreeNode, ResourceConfig } from '../../component/tag';
|
||||||
import { ResourceTypeEnum } from '@/common/commonEnum';
|
import { ResourceTypeEnum, TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
import { defineAsyncComponent } from 'vue';
|
import { defineAsyncComponent } from 'vue';
|
||||||
import { dbApi } from '../api';
|
import { dbApi } from '../api';
|
||||||
import { sleep } from '@/common/utils/loading';
|
import { sleep } from '@/common/utils/loading';
|
||||||
@@ -13,20 +13,21 @@ import { formatByteSize } from '@/common/utils/format';
|
|||||||
const DbInstList = defineAsyncComponent(() => import('../InstanceList.vue'));
|
const DbInstList = defineAsyncComponent(() => import('../InstanceList.vue'));
|
||||||
const DbDataOp = defineAsyncComponent(() => import('./DbDataOp.vue'));
|
const DbDataOp = defineAsyncComponent(() => import('./DbDataOp.vue'));
|
||||||
const NodeDbInst = defineAsyncComponent(() => import('./NodeDbInst.vue'));
|
const NodeDbInst = defineAsyncComponent(() => import('./NodeDbInst.vue'));
|
||||||
|
const NodeDb = defineAsyncComponent(() => import('./NodeDb.vue'));
|
||||||
const NodeDbTable = defineAsyncComponent(() => import('./NodeDbTable.vue'));
|
const NodeDbTable = defineAsyncComponent(() => import('./NodeDbTable.vue'));
|
||||||
|
|
||||||
const DbIcon = {
|
export const DbIcon = {
|
||||||
name: ResourceTypeEnum.Db.extra.icon,
|
name: ResourceTypeEnum.Db.extra.icon,
|
||||||
color: ResourceTypeEnum.Db.extra.iconColor,
|
color: ResourceTypeEnum.Db.extra.iconColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
// pgsql schema icon
|
// pgsql schema icon
|
||||||
const SchemaIcon = {
|
export const SchemaIcon = {
|
||||||
name: 'List',
|
name: 'List',
|
||||||
color: '#67c23a',
|
color: '#67c23a',
|
||||||
};
|
};
|
||||||
|
|
||||||
const TableIcon = {
|
export const TableIcon = {
|
||||||
name: 'icon db/table',
|
name: 'icon db/table',
|
||||||
color: '#409eff',
|
color: '#409eff',
|
||||||
};
|
};
|
||||||
@@ -65,34 +66,58 @@ const ContextmenuItemRefresh = new ContextmenuItem('refresh', 'common.refresh')
|
|||||||
.withIcon('RefreshRight')
|
.withIcon('RefreshRight')
|
||||||
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).reloadNode(node.key));
|
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).reloadNode(node.key));
|
||||||
|
|
||||||
// tagpath 节点类型
|
// 数据库实例节点类型
|
||||||
const NodeTypeDbTag = new NodeType(TagTreeNode.TagPath)
|
const NodeTypeDbInst = new NodeType(TagResourceTypeEnum.DbInstance.value).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
parentNode.ctx?.addResourceComponent(DbDataOpComp);
|
||||||
parentNode.ctx?.addResourceComponent(DbDataOpComp);
|
const tagPath = parentNode.params.tagPath;
|
||||||
|
|
||||||
const tagPath = parentNode.params.tagPath;
|
const dbInstancesRes = await dbApi.instances.request({ tagPath, pageSize: 100 });
|
||||||
const dbInfoRes = await dbApi.dbs.request({ tagPath });
|
const dbInstances = dbInstancesRes.list;
|
||||||
|
if (!dbInstances) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防止过快加载会出现一闪而过,对眼睛不好
|
||||||
|
await sleep(100);
|
||||||
|
return dbInstances?.map((x: any) => {
|
||||||
|
x.tagPath = tagPath;
|
||||||
|
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeDbConf).withParams(x).withNodeComponent(NodeDbInst);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 数据库配置节点类型
|
||||||
|
const NodeTypeDbConf = new NodeType(TagResourceTypeEnum.Db.value)
|
||||||
|
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
const params = parentNode.params;
|
||||||
|
|
||||||
|
const tagPath = params.tagPath;
|
||||||
|
const authCerts = {} as any;
|
||||||
|
for (let authCert of params.authCerts) {
|
||||||
|
authCerts[authCert.name] = authCert;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbInfoRes = await dbApi.dbs.request({
|
||||||
|
tagPath: `${tagPath}${TagResourceTypeEnum.DbInstance.value}|${params.code}`,
|
||||||
|
});
|
||||||
const dbInfos = dbInfoRes.list;
|
const dbInfos = dbInfoRes.list;
|
||||||
if (!dbInfos) {
|
if (!dbInfos) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 防止过快加载会出现一闪而过,对眼睛不好
|
|
||||||
await sleep(100);
|
|
||||||
return dbInfos?.map((x: any) => {
|
return dbInfos?.map((x: any) => {
|
||||||
x.tagPath = tagPath;
|
x.tagPath = tagPath;
|
||||||
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeDbInst).withParams(x).withNodeComponent(NodeDbInst);
|
x.username = authCerts[x.authCertName]?.username;
|
||||||
|
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeDbs).withParams(x).withIcon(DbIcon).withNodeComponent(NodeDb);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.withContextMenuItems([ContextmenuItemRefresh]);
|
.withContextMenuItems([ContextmenuItemRefresh]);
|
||||||
|
|
||||||
// 数据库实例节点类型
|
// 数据库列表名类型
|
||||||
const NodeTypeDbInst = new NodeType(1).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
const NodeTypeDbs = new NodeType(222).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
const params = parentNode.params;
|
const params = parentNode.params;
|
||||||
const dbs = (await DbInst.getDbNames(params))?.sort();
|
const dbs = (await DbInst.getDbNames(params))?.sort();
|
||||||
// 查询数据库版本信息
|
// 查询数据库版本信息
|
||||||
const version = await dbApi.getCompatibleDbVersion.request({ id: params.id, db: dbs[0] });
|
const version = await dbApi.getCompatibleDbVersion.request({ id: params.id, db: dbs[0] });
|
||||||
|
|
||||||
return dbs.map((x: any) => {
|
return dbs.map((x: any) => {
|
||||||
return TagTreeNode.new(parentNode, `${parentNode.key}.${x}`, x, NodeTypeDb)
|
return TagTreeNode.new(parentNode, `${parentNode.key}.${x}`, x, NodeTypeDb)
|
||||||
.withParams({
|
.withParams({
|
||||||
@@ -282,7 +307,7 @@ const getSqlMenuNodeKey = (dbId: number, db: string) => {
|
|||||||
export default {
|
export default {
|
||||||
order: 2,
|
order: 2,
|
||||||
resourceType: ResourceTypeEnum.Db.value,
|
resourceType: ResourceTypeEnum.Db.value,
|
||||||
rootNodeType: NodeTypeDbTag,
|
rootNodeType: NodeTypeDbInst,
|
||||||
manager: {
|
manager: {
|
||||||
componentConf: {
|
componentConf: {
|
||||||
component: DbInstList,
|
component: DbInstList,
|
||||||
|
|||||||
166
frontend/src/views/ops/docker/ContainerConfList.vue
Normal file
166
frontend/src/views/ops/docker/ContainerConfList.vue
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-full">
|
||||||
|
<page-table
|
||||||
|
ref="pageTableRef"
|
||||||
|
:page-api="dockerApi.page"
|
||||||
|
:before-query-fn="checkRouteTagPath"
|
||||||
|
:searchItems="searchItems"
|
||||||
|
v-model:query-form="query"
|
||||||
|
:show-selection="true"
|
||||||
|
v-model:selection-data="selectionData"
|
||||||
|
:columns="columns"
|
||||||
|
lazy
|
||||||
|
>
|
||||||
|
<template #tableHeader>
|
||||||
|
<el-button type="primary" icon="plus" @click="editContainerConf(false)" plain>{{ $t('common.create') }}</el-button>
|
||||||
|
<el-button type="danger" icon="delete" :disabled="selectionData.length < 1" @click="deleteConf" plain>{{ $t('common.delete') }}</el-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #tagPath="{ data }">
|
||||||
|
<resource-tags :tags="data.tags" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #action="{ data }">
|
||||||
|
<el-button @click="showDetail(data)" link>{{ $t('common.detail') }}</el-button>
|
||||||
|
<el-button type="primary" link @click="editContainerConf(data)">{{ $t('common.edit') }}</el-button>
|
||||||
|
</template>
|
||||||
|
</page-table>
|
||||||
|
|
||||||
|
<el-dialog v-if="detailDialog.visible" v-model="detailDialog.visible">
|
||||||
|
<el-descriptions :title="$t('common.detail')" :column="3" border>
|
||||||
|
<el-descriptions-item :span="1.5" label="id">{{ detailDialog.data.id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1.5" :label="$t('common.name')">{{ detailDialog.data.name }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="3" :label="$t('tag.relateTag')"><ResourceTags :tags="detailDialog.data.tags" /></el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="3" :label="$t('docker.addr')">{{ detailDialog.data.addr }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="3" :label="$t('common.remark')">{{ detailDialog.data.remark }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="2" :label="$t('common.createTime')">{{ formatDate(detailDialog.data.createTime) }} </el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" :label="$t('common.creator')">{{ detailDialog.data.creator }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="2" :label="$t('common.updateTime')">{{ formatDate(detailDialog.data.updateTime) }} </el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" :label="$t('common.modifier')">{{ detailDialog.data.modifier }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<ContainerConfEdit
|
||||||
|
@val-change="search()"
|
||||||
|
:title="containerConfEditDialog.title"
|
||||||
|
v-model:visible="containerConfEditDialog.visible"
|
||||||
|
v-model:container="containerConfEditDialog.data"
|
||||||
|
></ContainerConfEdit>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { dockerApi } from './api';
|
||||||
|
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
||||||
|
import { formatDate } from '@/common/utils/format';
|
||||||
|
import ResourceTags from '../component/ResourceTags.vue';
|
||||||
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
|
import { TableColumn } from '@/components/pagetable';
|
||||||
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { getTagPathSearchItem } from '../component/tag';
|
||||||
|
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||||
|
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
const ContainerConfEdit = defineAsyncComponent(() => import('./CotainerConfEdit.vue'));
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
lazy: {
|
||||||
|
type: [Boolean],
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const pageTableRef: Ref<any> = ref(null);
|
||||||
|
|
||||||
|
const searchItems = [
|
||||||
|
SearchItem.input('keyword', 'common.keyword').withPlaceholder('redis.keywordPlaceholder'),
|
||||||
|
getTagPathSearchItem(TagResourceTypeEnum.Container.value),
|
||||||
|
];
|
||||||
|
|
||||||
|
const columns = ref([
|
||||||
|
TableColumn.new('tags[0].tagPath', 'tag.relateTag').isSlot('tagPath').setAddWidth(20),
|
||||||
|
TableColumn.new('name', 'common.name'),
|
||||||
|
TableColumn.new('addr', 'docker.addr'),
|
||||||
|
TableColumn.new('remark', 'common.remark'),
|
||||||
|
TableColumn.new('code', 'common.code'),
|
||||||
|
TableColumn.new('action', 'common.operation').isSlot().setMinWidth(200).fixedRight().alignCenter(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
selectionData: [],
|
||||||
|
query: {
|
||||||
|
tagPath: '',
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 0,
|
||||||
|
},
|
||||||
|
detailDialog: {
|
||||||
|
visible: false,
|
||||||
|
data: null as any,
|
||||||
|
},
|
||||||
|
containerConfEditDialog: {
|
||||||
|
visible: false,
|
||||||
|
data: null as any,
|
||||||
|
title: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { selectionData, query, detailDialog, containerConfEditDialog } = toRefs(state);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!props.lazy) {
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const checkRouteTagPath = (query: any) => {
|
||||||
|
if (route.query.tagPath) {
|
||||||
|
query.tagPath = route.query.tagPath as string;
|
||||||
|
}
|
||||||
|
return query;
|
||||||
|
};
|
||||||
|
|
||||||
|
const showDetail = (detail: any) => {
|
||||||
|
state.detailDialog.data = detail;
|
||||||
|
state.detailDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteConf = async () => {
|
||||||
|
try {
|
||||||
|
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.name).join('、'));
|
||||||
|
await dockerApi.delConf.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
|
||||||
|
useI18nDeleteSuccessMsg();
|
||||||
|
search();
|
||||||
|
} catch (err) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const search = async (tagPath: string = '') => {
|
||||||
|
if (tagPath) {
|
||||||
|
state.query.tagPath = tagPath;
|
||||||
|
}
|
||||||
|
pageTableRef.value.search();
|
||||||
|
};
|
||||||
|
|
||||||
|
const editContainerConf = async (data: any) => {
|
||||||
|
if (!data) {
|
||||||
|
state.containerConfEditDialog.data = null;
|
||||||
|
state.containerConfEditDialog.title = useI18nCreateTitle('docker.containerConf');
|
||||||
|
} else {
|
||||||
|
state.containerConfEditDialog.data = data;
|
||||||
|
state.containerConfEditDialog.title = useI18nEditTitle('docker.containerConf');
|
||||||
|
}
|
||||||
|
state.containerConfEditDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ search });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
115
frontend/src/views/ops/docker/CotainerConfEdit.vue
Normal file
115
frontend/src/views/ops/docker/CotainerConfEdit.vue
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-drawer :title="title" v-model="dialogVisible" :before-close="onCancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||||
|
<template #header>
|
||||||
|
<DrawerHeader :header="title" :back="onCancel" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form :model="form" ref="formRef" :rules="rules" label-width="auto">
|
||||||
|
<el-form-item prop="tagCodePaths" :label="$t('tag.relateTag')" required>
|
||||||
|
<tag-tree-select multiple v-model="form.tagCodePaths" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="name" :label="$t('common.name')" required>
|
||||||
|
<el-input v-model.trim="form.name" auto-complete="off"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="addr" :label="$t('docker.addr')" required>
|
||||||
|
<el-input v-model.trim="form.addr" :placeholder="$t('docker.addrTips')" auto-complete="off" type="textarea"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="remark" :label="$t('common.remark')">
|
||||||
|
<el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<!-- <el-button @click="onTestConn" :loading="testConnBtnLoading" type="success">{{ $t('ac.testConn') }}</el-button> -->
|
||||||
|
<el-button @click="onCancel()">{{ $t('common.cancel') }}</el-button>
|
||||||
|
<el-button type="primary" :loading="saveBtnLoading" @click="onConfirm">{{ $t('common.confirm') }}</el-button>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { toRefs, reactive, watch, useTemplateRef } from 'vue';
|
||||||
|
import { dockerApi } from './api';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import TagTreeSelect from '../component/TagTreeSelect.vue';
|
||||||
|
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||||
|
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { Rules } from '@/common/rule';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
container: {
|
||||||
|
type: [Boolean, Object],
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const dialogVisible = defineModel<boolean>('visible', { default: false });
|
||||||
|
|
||||||
|
const emit = defineEmits(['val-change', 'cancel']);
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
tagCodePaths: [Rules.requiredSelect('tag.relateTag')],
|
||||||
|
name: [Rules.requiredInput('common.name')],
|
||||||
|
addr: [Rules.requiredInput('addr')],
|
||||||
|
};
|
||||||
|
|
||||||
|
const formRef: any = useTemplateRef('formRef');
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
form: {
|
||||||
|
id: null,
|
||||||
|
code: '',
|
||||||
|
tagCodePaths: [],
|
||||||
|
name: null,
|
||||||
|
addr: '',
|
||||||
|
remark: '',
|
||||||
|
},
|
||||||
|
dbList: [0],
|
||||||
|
pwd: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { form } = toRefs(state);
|
||||||
|
|
||||||
|
const { isFetching: saveBtnLoading, execute: saveConfExec } = dockerApi.saveConf.useApi(form);
|
||||||
|
|
||||||
|
watch(dialogVisible, () => {
|
||||||
|
if (!dialogVisible.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container: any = props.container;
|
||||||
|
if (container) {
|
||||||
|
state.form = { ...container };
|
||||||
|
state.form.tagCodePaths = container.tags.map((t: any) => t.codePath);
|
||||||
|
} else {
|
||||||
|
state.form = {} as any;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onTestConn = async () => {
|
||||||
|
await useI18nFormValidate(formRef);
|
||||||
|
// await testConnExec();
|
||||||
|
ElMessage.success(t('ac.connSuccess'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onConfirm = async () => {
|
||||||
|
await useI18nFormValidate(formRef);
|
||||||
|
await saveConfExec();
|
||||||
|
useI18nSaveSuccessMsg();
|
||||||
|
emit('val-change', state.form);
|
||||||
|
onCancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
dialogVisible.value = false;
|
||||||
|
emit('cancel');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
@@ -3,25 +3,29 @@ import config from '@/common/config';
|
|||||||
import { joinClientParams } from '@/common/request';
|
import { joinClientParams } from '@/common/request';
|
||||||
|
|
||||||
export const dockerApi = {
|
export const dockerApi = {
|
||||||
info: Api.newGet('/docker/info'),
|
page: Api.newGet('/docker/container-conf/page'),
|
||||||
|
saveConf: Api.newPost('/docker/container-conf/save'),
|
||||||
|
delConf: Api.newDelete('/docker/container-conf/del/{id}'),
|
||||||
|
|
||||||
containers: Api.newGet('/docker/containers'),
|
info: Api.newGet('/docker/{id}/info'),
|
||||||
containersStats: Api.newGet('/docker/containers/stats'),
|
|
||||||
containerStop: Api.newPost('/docker/containers/stop'),
|
|
||||||
containerRemove: Api.newPost('/docker/containers/remove'),
|
|
||||||
containerRestart: Api.newPost('/docker/containers/restart'),
|
|
||||||
containerCreate: Api.newPost('/docker/containers/create'),
|
|
||||||
|
|
||||||
images: Api.newGet('/docker/images'),
|
containers: Api.newGet('/docker/{id}/containers'),
|
||||||
imageRemove: Api.newPost('/docker/images/remove'),
|
containersStats: Api.newGet('/docker/{id}/containers/stats'),
|
||||||
imageSave: Api.newPost('/docker/images/save'),
|
containerStop: Api.newPost('/docker/{id}/containers/stop'),
|
||||||
imageUpload: Api.newPost('/docker/images/load'),
|
containerRemove: Api.newPost('/docker/{id}/containers/remove'),
|
||||||
|
containerRestart: Api.newPost('/docker/{id}/containers/restart'),
|
||||||
|
containerCreate: Api.newPost('/docker/{id}/containers/create'),
|
||||||
|
|
||||||
|
images: Api.newGet('/docker/{id}/images'),
|
||||||
|
imageRemove: Api.newPost('/docker/{id}/images/remove'),
|
||||||
|
imageSave: Api.newPost('/docker/{id}/images/save'),
|
||||||
|
imageUpload: Api.newPost('/docker/{id}/images/load'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getDockerExecSocketUrl(host: any, containerId: string) {
|
export function getDockerExecSocketUrl(id: number, containerId: string) {
|
||||||
return `/docker/containers/exec?host=${host}&containerId=${containerId}`;
|
return `/docker/${id}/containers/exec?id=${id}&containerId=${containerId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getContainerLogSocketUrl(host: any, containerId: string) {
|
export function getContainerLogSocketUrl(id: number, containerId: string) {
|
||||||
return `${config.baseWsUrl}/docker/containers/logs?${joinClientParams()}&host=${host}&containerId=${containerId}`;
|
return `${config.baseWsUrl}/docker/${id}/containers/logs?${joinClientParams()}&id=${id}&containerId=${containerId}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<template #label>
|
<template #label>
|
||||||
{{ $t('docker.image') }}
|
{{ $t('docker.image') }}
|
||||||
<el-tooltip :content="$t('docker.imageTips')" placement="top">
|
<el-tooltip :content="$t('docker.imageTips')" placement="top">
|
||||||
<SvgIcon class="mb10" name="question-filled" />
|
<SvgIcon class="mb-1" name="question-filled" />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -34,9 +34,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="cmdStr" :label="$t('Command')">
|
<el-form-item prop="cmdStr" :label="$t('Command')">
|
||||||
<el-select v-model="form.cmdStr" filterable allow-create>
|
<el-input v-model="form.cmdStr" />
|
||||||
<el-option v-for="item in defaultCommands" :key="item" :label="item" :value="item"></el-option>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item :label="$t('docker.port')">
|
<el-form-item :label="$t('docker.port')">
|
||||||
@@ -283,8 +281,8 @@ const rules = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
host: {
|
id: {
|
||||||
type: String,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -311,8 +309,6 @@ const defaultForm = {
|
|||||||
envsStr: '',
|
envsStr: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultCommands = ["start.sh jupyter notebook --NotebookApp.token=''"];
|
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
dockerInfo: {} as any,
|
dockerInfo: {} as any,
|
||||||
images: [] as any,
|
images: [] as any,
|
||||||
@@ -349,10 +345,10 @@ const runtimeSelect = computed(() => {
|
|||||||
const init = async () => {
|
const init = async () => {
|
||||||
state.form = deepClone(defaultForm);
|
state.form = deepClone(defaultForm);
|
||||||
state.submitForm = {};
|
state.submitForm = {};
|
||||||
dockerApi.info.request({ host: props.host }).then((res) => {
|
dockerApi.info.request({ id: props.id }).then((res) => {
|
||||||
state.dockerInfo = res;
|
state.dockerInfo = res;
|
||||||
});
|
});
|
||||||
state.images = await dockerApi.images.request({ host: props.host });
|
state.images = await dockerApi.images.request({ id: props.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePortsAdd = () => {
|
const handlePortsAdd = () => {
|
||||||
@@ -398,7 +394,7 @@ const btnOk = async () => {
|
|||||||
await useI18nFormValidate(formRef);
|
await useI18nFormValidate(formRef);
|
||||||
|
|
||||||
state.submitForm = { ...state.form };
|
state.submitForm = { ...state.form };
|
||||||
state.submitForm.host = props.host;
|
state.submitForm.id = props.id;
|
||||||
|
|
||||||
if (state.submitForm.exposedPorts) {
|
if (state.submitForm.exposedPorts) {
|
||||||
state.submitForm.exposedPorts = state.form.exposedPorts.map((item: any) => {
|
state.submitForm.exposedPorts = state.form.exposedPorts.map((item: any) => {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
<el-table-column prop="name" :label="$t('docker.name')" :min-width="120" show-overflow-tooltip> </el-table-column>
|
<el-table-column prop="name" :label="$t('docker.name')" :min-width="120" show-overflow-tooltip> </el-table-column>
|
||||||
<el-table-column prop="imageName" :label="$t('docker.image')" :min-width="150" show-overflow-tooltip> </el-table-column>
|
<el-table-column prop="imageName" :label="$t('docker.image')" :min-width="150" show-overflow-tooltip> </el-table-column>
|
||||||
|
|
||||||
<el-table-column prop="state" :label="$t('common.status')" :min-width="80">
|
<el-table-column prop="state" :label="$t('common.status')" :min-width="110">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-dropdown @command="handleCommand">
|
<el-dropdown @command="handleCommand">
|
||||||
<el-button :type="EnumValue.getEnumByValue(ContainerStateEnum, row.state)?.tag.type" round plain size="small">
|
<el-button :type="EnumValue.getEnumByValue(ContainerStateEnum, row.state)?.tag.type" round plain size="small">
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column v-loading="true" prop="stats" :label="$t('docker.stats')" :min-width="90">
|
<el-table-column v-loading="true" prop="stats" :label="$t('docker.stats')" :min-width="130">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<SvgIcon v-if="getLoadingState(row.containerId)" class="is-loading" name="loading" color="var(--el-color-primary)" />
|
<SvgIcon v-if="getLoadingState(row.containerId)" class="is-loading" name="loading" color="var(--el-color-primary)" />
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
|
|
||||||
<el-table-column prop="status" label="运行时长" :min-width="120"> </el-table-column>
|
<el-table-column prop="status" label="运行时长" :min-width="120"> </el-table-column>
|
||||||
|
|
||||||
<el-table-column :label="$t('common.operation')" :min-width="140">
|
<el-table-column :label="$t('common.operation')" :min-width="180">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-button @click="openTerminal(row)" :disabled="row.state != ContainerStateEnum.Running.value" type="primary" link plain> SSH </el-button>
|
<el-button @click="openTerminal(row)" :disabled="row.state != ContainerStateEnum.Running.value" type="primary" link plain> SSH </el-button>
|
||||||
@@ -157,16 +157,16 @@
|
|||||||
draggable
|
draggable
|
||||||
append-to-body
|
append-to-body
|
||||||
>
|
>
|
||||||
<TerminalBody ref="terminal" :socket-url="getDockerExecSocketUrl(params.host, terminalDialog.containerId)" />
|
<TerminalBody ref="terminal" :socket-url="getDockerExecSocketUrl(props.id, terminalDialog.containerId)" />
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<ContainerLog v-model:visible="logDialog.visible" :host="params.host" :container-id="logDialog.containerId" />
|
<ContainerLog v-model:visible="logDialog.visible" :id="props.id" :container-id="logDialog.containerId" :title="logDialog.title" />
|
||||||
|
|
||||||
<ContainerCreate v-model:visible="containerCreateDialog.visible" :host="params.host" @success="getContainers" />
|
<ContainerCreate v-model:visible="containerCreateDialog.visible" :id="props.id" @success="getContainers" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, defineAsyncComponent, onMounted, reactive, toRefs } from 'vue';
|
import { computed, defineAsyncComponent, onMounted, reactive, toRefs, watch } from 'vue';
|
||||||
import { dockerApi, getDockerExecSocketUrl } from '../api';
|
import { dockerApi, getDockerExecSocketUrl } from '../api';
|
||||||
import { formatByteSize, formatDate } from '@/common/utils/format';
|
import { formatByteSize, formatDate } from '@/common/utils/format';
|
||||||
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
|
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
|
||||||
@@ -182,15 +182,15 @@ const ContainerLog = defineAsyncComponent(() => import('./ContainerLog.vue'));
|
|||||||
const ContainerCreate = defineAsyncComponent(() => import('./ContainerCreate.vue'));
|
const ContainerCreate = defineAsyncComponent(() => import('./ContainerCreate.vue'));
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
host: {
|
id: {
|
||||||
type: String,
|
type: Number,
|
||||||
default: '',
|
default: 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
params: {
|
params: {
|
||||||
host: props.host,
|
id: props.id,
|
||||||
name: '',
|
name: '',
|
||||||
state: null,
|
state: null,
|
||||||
},
|
},
|
||||||
@@ -222,6 +222,13 @@ onMounted(() => {
|
|||||||
getContainers();
|
getContainers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.id,
|
||||||
|
() => {
|
||||||
|
getContainers();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const filterTableDatas = computed(() => {
|
const filterTableDatas = computed(() => {
|
||||||
let tables: any = state.containers;
|
let tables: any = state.containers;
|
||||||
const nameSearch = state.params.name;
|
const nameSearch = state.params.name;
|
||||||
@@ -241,6 +248,10 @@ const filterTableDatas = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const getContainers = async () => {
|
const getContainers = async () => {
|
||||||
|
if (!props.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.params.id = props.id;
|
||||||
state.loadingContainers = true;
|
state.loadingContainers = true;
|
||||||
try {
|
try {
|
||||||
state.containers = await dockerApi.containers.request(state.params);
|
state.containers = await dockerApi.containers.request(state.params);
|
||||||
@@ -281,21 +292,21 @@ const setContainersStats = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const containerRestart = async (param: any) => {
|
const containerRestart = async (param: any) => {
|
||||||
await dockerApi.containerRestart.request({ host: state.params.host, containerId: param.containerId });
|
await dockerApi.containerRestart.request({ id: props.id, containerId: param.containerId });
|
||||||
useI18nOperateSuccessMsg();
|
useI18nOperateSuccessMsg();
|
||||||
getContainers();
|
getContainers();
|
||||||
};
|
};
|
||||||
|
|
||||||
const containerStop = async (param: any) => {
|
const containerStop = async (param: any) => {
|
||||||
await useI18nConfirm('docker.stopContainerConfirm', { name: param.name });
|
await useI18nConfirm('docker.stopContainerConfirm', { name: param.name });
|
||||||
await dockerApi.containerStop.request({ host: state.params.host, containerId: param.containerId });
|
await dockerApi.containerStop.request({ id: props.id, containerId: param.containerId });
|
||||||
useI18nOperateSuccessMsg();
|
useI18nOperateSuccessMsg();
|
||||||
getContainers();
|
getContainers();
|
||||||
};
|
};
|
||||||
|
|
||||||
const containerRemove = async (param: any) => {
|
const containerRemove = async (param: any) => {
|
||||||
await useI18nConfirm('docker.removeContainerConfirm', { name: param.name });
|
await useI18nConfirm('docker.removeContainerConfirm', { name: param.name });
|
||||||
await dockerApi.containerRemove.request({ host: state.params.host, containerId: param.containerId });
|
await dockerApi.containerRemove.request({ id: props.id, containerId: param.containerId });
|
||||||
useI18nDeleteSuccessMsg();
|
useI18nDeleteSuccessMsg();
|
||||||
getContainers();
|
getContainers();
|
||||||
};
|
};
|
||||||
@@ -316,11 +327,6 @@ const openLog = (row: any) => {
|
|||||||
state.logDialog.visible = true;
|
state.logDialog.visible = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const openUrl = (row: any) => {
|
|
||||||
const port = row.ports[0];
|
|
||||||
window.open('http://' + props.host.split('//')[1].split(':')[0] + ':' + port.split('->')[0]?.split(':')[1] + '/lab');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCommand = async (commond: any) => {
|
const handleCommand = async (commond: any) => {
|
||||||
const row = commond.row;
|
const row = commond.row;
|
||||||
const type = commond.type;
|
const type = commond.type;
|
||||||
|
|||||||
@@ -2,32 +2,34 @@
|
|||||||
<div>
|
<div>
|
||||||
<el-drawer title="logs" v-model="visible" @close="close" :destroy-on-close="true" :close-on-click-modal="true" size="60%">
|
<el-drawer title="logs" v-model="visible" @close="close" :destroy-on-close="true" :close-on-click-modal="true" size="60%">
|
||||||
<template #header>
|
<template #header>
|
||||||
<DrawerHeader :header="props.host" :back="() => (visible = false)">
|
<DrawerHeader :header="`${props.title}`" :back="() => (visible = false)">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<div class="mr20"></div>
|
<div class="mr20"></div>
|
||||||
</template>
|
</template>
|
||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-row :gutter="10" class="mb20">
|
<div class="flex flex-col flex-1">
|
||||||
<el-col :span="6">
|
<el-row :gutter="10" class="mb-2">
|
||||||
<el-select @change="searchLog" v-model.number="state.tail">
|
<el-col :span="6">
|
||||||
<template #prefix>{{ $t('docker.lines') }}</template>
|
<el-select @change="searchLog" v-model.number="state.tail">
|
||||||
<el-option :value="100" :label="100" />
|
<template #prefix>{{ $t('docker.lines') }}</template>
|
||||||
<el-option :value="200" :label="200" />
|
<el-option :value="100" :label="100" />
|
||||||
<el-option :value="500" :label="500" />
|
<el-option :value="200" :label="200" />
|
||||||
<el-option :value="1000" :label="1000" />
|
<el-option :value="500" :label="500" />
|
||||||
</el-select>
|
<el-option :value="1000" :label="1000" />
|
||||||
</el-col>
|
</el-select>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-checkbox @change="searchLog" border v-model="state.isWatch">
|
<el-checkbox @change="searchLog" border v-model="state.isWatch">
|
||||||
{{ $t('docker.follow') }}
|
{{ $t('docker.follow') }}
|
||||||
</el-checkbox>
|
</el-checkbox>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<RealLogViewer ref="realLogViewerRef" :ws-url="wsUrl" height="calc(100vh - 200px)" />
|
<RealLogViewer ref="realLogViewerRef" :ws-url="wsUrl" height="calc(100vh - 200px)" />
|
||||||
|
</div>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -39,7 +41,11 @@ import { getContainerLogSocketUrl } from '../api';
|
|||||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
host: {
|
id: {
|
||||||
|
type: Number,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
@@ -60,7 +66,7 @@ const state = reactive({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const wsUrl = computed(
|
const wsUrl = computed(
|
||||||
() => `${getContainerLogSocketUrl(props.host, props.containerId)}&tail=${state.tail}&follow=${state.isWatch ? '1' : '0'}&since=${state.since}`
|
() => `${getContainerLogSocketUrl(props.id, props.containerId)}&tail=${state.tail}&follow=${state.isWatch ? '1' : '0'}&since=${state.since}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const searchLog = () => {
|
const searchLog = () => {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column prop="size" :label="$t('docker.size')" :min-width="50">
|
<el-table-column prop="size" :label="$t('docker.size')" :min-width="60">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ formatByteSize(row.size) }}
|
{{ formatByteSize(row.size) }}
|
||||||
</template>
|
</template>
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
draggable
|
draggable
|
||||||
append-to-body
|
append-to-body
|
||||||
>
|
>
|
||||||
<TerminalBody ref="terminal" :socket-url="getDockerExecSocketUrl(params.host, terminalDialog.containerId)" height="560px" />
|
<TerminalBody ref="terminal" :socket-url="getDockerExecSocketUrl(props.id, terminalDialog.containerId)" height="560px" />
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -99,15 +99,15 @@ import { ElMessage } from 'element-plus';
|
|||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
host: {
|
id: {
|
||||||
type: String,
|
type: Number,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
params: {
|
params: {
|
||||||
host: props.host,
|
id: 0,
|
||||||
name: '',
|
name: '',
|
||||||
state: null,
|
state: null,
|
||||||
},
|
},
|
||||||
@@ -147,6 +147,10 @@ const filterTableDatas = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const getImages = async () => {
|
const getImages = async () => {
|
||||||
|
if (!props.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.params.id = props.id;
|
||||||
state.loadingImages = true;
|
state.loadingImages = true;
|
||||||
try {
|
try {
|
||||||
state.images = await dockerApi.images.request(state.params);
|
state.images = await dockerApi.images.request(state.params);
|
||||||
@@ -157,7 +161,7 @@ const getImages = async () => {
|
|||||||
|
|
||||||
const exportImage = async (row: any) => {
|
const exportImage = async (row: any) => {
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.setAttribute('href', `${config.baseApiUrl}/docker/images/save?host=${state.params.host}&tag=${row.tags[0]}&${joinClientParams()}`);
|
a.setAttribute('href', `${config.baseApiUrl}/docker/${props.id}/images/save?id=${props.id}&tag=${row.tags[0]}&${joinClientParams()}`);
|
||||||
a.setAttribute('target', '_blank');
|
a.setAttribute('target', '_blank');
|
||||||
a.click();
|
a.click();
|
||||||
};
|
};
|
||||||
@@ -166,7 +170,7 @@ const uploadImage = (content: any) => {
|
|||||||
const params = new FormData();
|
const params = new FormData();
|
||||||
// const path = state.nowPath;
|
// const path = state.nowPath;
|
||||||
params.append('file', content.file);
|
params.append('file', content.file);
|
||||||
params.append('host', state.params.host);
|
params.append('id', props.id + '');
|
||||||
params.append('token', token);
|
params.append('token', token);
|
||||||
dockerApi.imageUpload
|
dockerApi.imageUpload
|
||||||
.xhrReq(params, {
|
.xhrReq(params, {
|
||||||
@@ -193,7 +197,7 @@ const uploadSuccess = (res: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const imageRemove = async (row: any) => {
|
const imageRemove = async (row: any) => {
|
||||||
await dockerApi.imageRemove.request({ host: state.params.host, imageId: row.id });
|
await dockerApi.imageRemove.request({ id: props.id, imageId: row.id });
|
||||||
getImages();
|
getImages();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
47
frontend/src/views/ops/docker/resource/ContainerOp.vue
Normal file
47
frontend/src/views/ops/docker/resource/ContainerOp.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card h-full">
|
||||||
|
<el-tabs v-model="activeName" @tab-change="handleTabChange">
|
||||||
|
<el-tab-pane :label="$t('docker.container')" :name="containerTab">
|
||||||
|
<ContainerList :id="containerConfId" />
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane :label="$t('docker.image')" :name="imageTab">
|
||||||
|
<ImageList v-if="activeName == imageTab" :id="containerConfId" />
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ContainerOpComp } from '@/views/ops/docker/resource';
|
||||||
|
import { toRefs, reactive, onMounted, defineAsyncComponent, ref, getCurrentInstance } from 'vue';
|
||||||
|
|
||||||
|
const ContainerList = defineAsyncComponent(() => import('../container/ContainerList.vue'));
|
||||||
|
const ImageList = defineAsyncComponent(() => import('../image/ImageList.vue'));
|
||||||
|
|
||||||
|
const emits = defineEmits(['init']);
|
||||||
|
|
||||||
|
const containerTab = 'containerTab';
|
||||||
|
const imageTab = 'imageTab';
|
||||||
|
|
||||||
|
const containerConfId = ref<number>(0);
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
activeName: containerTab,
|
||||||
|
cmdConfs: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { activeName } = toRefs(state);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
emits('init', { name: ContainerOpComp.name, ref: getCurrentInstance()?.exposed });
|
||||||
|
state.activeName = containerTab;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleTabChange = (tabName: any) => {};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
init: function (id: number) {
|
||||||
|
containerConfId.value = id;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
46
frontend/src/views/ops/docker/resource/index.ts
Normal file
46
frontend/src/views/ops/docker/resource/index.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { ResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
import { NodeType, TagTreeNode, ResourceComponentConfig, ResourceConfig } from '@/views/ops/component/tag';
|
||||||
|
import { dockerApi } from '@/views/ops/docker/api';
|
||||||
|
|
||||||
|
const ContainerConfList = defineAsyncComponent(() => import('../ContainerConfList.vue'));
|
||||||
|
const ContainerOp = defineAsyncComponent(() => import('./ContainerOp.vue'));
|
||||||
|
|
||||||
|
const Icon = {
|
||||||
|
name: ResourceTypeEnum.Container.extra.icon,
|
||||||
|
color: ResourceTypeEnum.Container.extra.iconColor,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ContainerOpComp: ResourceComponentConfig = {
|
||||||
|
name: 'tag.containerOp',
|
||||||
|
component: ContainerOp,
|
||||||
|
icon: Icon,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NodeTypeContainerTag = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (node: TagTreeNode) => {
|
||||||
|
// 加载标签树下的容器列表
|
||||||
|
const res = await dockerApi.page.request({ tagPath: node.params.tagPath });
|
||||||
|
// 把list 根据name字段排序
|
||||||
|
return res?.list
|
||||||
|
.sort((a: any, b: any) => a.name.localeCompare(b.name))
|
||||||
|
.map((x: any) => TagTreeNode.new(node, x.code, x.name, NodeTypeContainer).withIsLeaf(true).withParams(x).withIcon(Icon));
|
||||||
|
});
|
||||||
|
|
||||||
|
const NodeTypeContainer = new NodeType(11).withNodeClickFunc(async (node: TagTreeNode) => {
|
||||||
|
(await node.ctx?.addResourceComponent(ContainerOpComp)).init(node.params.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
order: 1.5,
|
||||||
|
resourceType: ResourceTypeEnum.Container.value,
|
||||||
|
rootNodeType: NodeTypeContainerTag,
|
||||||
|
manager: {
|
||||||
|
componentConf: {
|
||||||
|
component: ContainerConfList,
|
||||||
|
icon: Icon,
|
||||||
|
name: 'tag.container',
|
||||||
|
},
|
||||||
|
permCode: 'container',
|
||||||
|
countKey: 'container',
|
||||||
|
},
|
||||||
|
} as ResourceConfig;
|
||||||
@@ -19,6 +19,13 @@
|
|||||||
<el-form-item prop="version" :label="t('common.version')">
|
<el-form-item prop="version" :label="t('common.version')">
|
||||||
<el-input v-model.trim="form.version" auto-complete="off" disabled></el-input>
|
<el-input v-model.trim="form.version" auto-complete="off" disabled></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<!-- 增加协议下拉框 http和https,默认http-->
|
||||||
|
<el-form-item prop="protocol" :label="t('es.protocol')">
|
||||||
|
<el-select v-model="form.protocol" placeholder="http">
|
||||||
|
<el-option label="http" value="http"></el-option>
|
||||||
|
<el-option label="https" value="https"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="host" label="Host" required>
|
<el-form-item prop="host" label="Host" required>
|
||||||
<el-col :span="18">
|
<el-col :span="18">
|
||||||
@@ -105,6 +112,7 @@ const DefaultForm = {
|
|||||||
id: null,
|
id: null,
|
||||||
code: '',
|
code: '',
|
||||||
name: null,
|
name: null,
|
||||||
|
protocol: 'http',
|
||||||
host: '',
|
host: '',
|
||||||
version: '',
|
version: '',
|
||||||
port: 9200,
|
port: 9200,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@
|
|||||||
:destroy-on-close="true"
|
:destroy-on-close="true"
|
||||||
:show-close="true"
|
:show-close="true"
|
||||||
:before-close="handleClose"
|
:before-close="handleClose"
|
||||||
width="55%"
|
width="60%"
|
||||||
>
|
>
|
||||||
<page-table
|
<page-table
|
||||||
ref="pageTableRef"
|
ref="pageTableRef"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<el-row class="mb-2 ml-4">
|
<el-row class="mb-2 ml-4">
|
||||||
<el-breadcrumb separator-icon="ArrowRight">
|
<el-breadcrumb separator-icon="ArrowRight">
|
||||||
<el-breadcrumb-item v-for="path in filePathNav" :key="path">
|
<el-breadcrumb-item v-for="path in filePathNav" :key="path">
|
||||||
<el-link @click="setFiles(path.path)" style="font-weight: bold">{{ path.name }}</el-link>
|
<el-link @click="setFiles(path.path)" class="!cursor-pointer !font-bold">{{ path.name }}</el-link>
|
||||||
</el-breadcrumb-item>
|
</el-breadcrumb-item>
|
||||||
</el-breadcrumb>
|
</el-breadcrumb>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
</el-popover>
|
</el-popover>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div :ref="(el: any) => setTerminalWrapperRef(el, dt.key)" class="terminal-wrapper flex-1 h-[calc(100vh-155px)]">
|
<div class="terminal-wrapper flex-1 h-[calc(100vh-155px)]">
|
||||||
<TerminalBody
|
<TerminalBody
|
||||||
v-if="dt.params.protocol == MachineProtocolEnum.Ssh.value"
|
v-if="dt.params.protocol == MachineProtocolEnum.Ssh.value"
|
||||||
:mount-init="false"
|
:mount-init="false"
|
||||||
@@ -161,11 +161,6 @@ const actionBtns = hasPerms([perms.updateMachine, perms.closeCli]);
|
|||||||
|
|
||||||
const emits = defineEmits(['init']);
|
const emits = defineEmits(['init']);
|
||||||
|
|
||||||
class MachineNodeType {
|
|
||||||
static Machine = 1;
|
|
||||||
static AuthCert = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
const resourceOpCtx: ResourceOpCtx | undefined = inject(ResourceOpCtxKey);
|
const resourceOpCtx: ResourceOpCtx | undefined = inject(ResourceOpCtxKey);
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
@@ -294,10 +289,11 @@ const openTerminal = (machine: any, ex?: boolean) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
state.tabs.set(key, tab);
|
state.tabs.set(key, tab);
|
||||||
state.activeTermName = key;
|
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
handleReconnect(tab);
|
handleReconnect(tab);
|
||||||
|
state.activeTermName = key;
|
||||||
|
setTimeout(() => fitTerminal(), 300);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -398,17 +394,6 @@ const setTerminalRef = (el: any, key: any) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const terminalWrapperRefs: any = {};
|
|
||||||
const setTerminalWrapperRef = (el: any, key: any) => {
|
|
||||||
if (key) {
|
|
||||||
terminalWrapperRefs[key] = el;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onResizeTagTree = () => {
|
|
||||||
fitTerminal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const fitTerminal = () => {
|
const fitTerminal = () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
let info = state.tabs.get(state.activeTermName);
|
let info = state.tabs.get(state.activeTermName);
|
||||||
@@ -419,9 +404,7 @@ const fitTerminal = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleReconnect = (tab: any, force = false) => {
|
const handleReconnect = (tab: any, force = false) => {
|
||||||
let width = terminalWrapperRefs[tab.key]?.offsetWidth;
|
terminalRefs[tab.key]?.init();
|
||||||
let height = terminalWrapperRefs[tab.key]?.offsetHeight;
|
|
||||||
terminalRefs[tab.key]?.init(width, height, force);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
|||||||
@@ -111,8 +111,6 @@
|
|||||||
</el-splitter-panel>
|
</el-splitter-panel>
|
||||||
</el-splitter>
|
</el-splitter>
|
||||||
|
|
||||||
<div style="text-align: center; margin-top: 10px"></div>
|
|
||||||
|
|
||||||
<el-dialog :title="$t('redis.addKey')" v-model="newKeyDialog.visible" width="500px" :destroy-on-close="true" :close-on-click-modal="false">
|
<el-dialog :title="$t('redis.addKey')" v-model="newKeyDialog.visible" width="500px" :destroy-on-close="true" :close-on-click-modal="false">
|
||||||
<el-form ref="keyForm" label-width="auto" :rules="keyFormRules" :model="newKeyDialog.keyInfo">
|
<el-form ref="keyForm" label-width="auto" :rules="keyFormRules" :model="newKeyDialog.keyInfo">
|
||||||
<el-form-item prop="key" label="Key" required>
|
<el-form-item prop="key" label="Key" required>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ResourceTypeEnum, TagResourceTypeEnum } from '@/common/commonEnum';
|
|||||||
import { redisApi } from '../api';
|
import { redisApi } from '../api';
|
||||||
import { sleep } from '@/common/utils/loading';
|
import { sleep } from '@/common/utils/loading';
|
||||||
|
|
||||||
const RedisIcon = {
|
export const RedisIcon = {
|
||||||
name: ResourceTypeEnum.Redis.extra.icon,
|
name: ResourceTypeEnum.Redis.extra.icon,
|
||||||
color: ResourceTypeEnum.Redis.extra.iconColor,
|
color: ResourceTypeEnum.Redis.extra.iconColor,
|
||||||
};
|
};
|
||||||
@@ -43,7 +43,7 @@ const NodeTypeRedis = new NodeType(2).withLoadNodesFunc(async (parentNode: TagTr
|
|||||||
const redisInfo = parentNode.params;
|
const redisInfo = parentNode.params;
|
||||||
|
|
||||||
let dbs: TagTreeNode[] = redisInfo.db.split(',').map((x: string) => {
|
let dbs: TagTreeNode[] = redisInfo.db.split(',').map((x: string) => {
|
||||||
return TagTreeNode.new(parentNode, x, `db${x}`, NodeTypeDb)
|
return TagTreeNode.new(parentNode, `${parentNode.key}.${x}`, `db${x}`, NodeTypeDb)
|
||||||
.withIsLeaf(true)
|
.withIsLeaf(true)
|
||||||
.withParams({
|
.withParams({
|
||||||
id: redisInfo.id,
|
id: redisInfo.id,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div
|
<div
|
||||||
:id="props.node.key"
|
:id="props.node.key"
|
||||||
class="w-full node-container flex items-center cursor-pointer select-none"
|
class="w-full node-container flex items-center cursor-pointer select-none"
|
||||||
:class="props.data.type.nodeDblclickFunc ? 'select-none' : ''"
|
:class="props.data.type?.nodeDblclickFunc ? 'select-none' : ''"
|
||||||
@mouseenter="showActions = true"
|
@mouseenter="showActions = true"
|
||||||
@mouseleave="showActions = false"
|
@mouseleave="showActions = false"
|
||||||
>
|
>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span v-else class="ml-auto pr-1.5 text-[10px] text-gray-400">
|
<span v-else class="ml-auto pr-2 text-[10px] text-gray-400">
|
||||||
<slot :node="node" :data="data" name="suffix"></slot>
|
<slot :node="node" :data="data" name="suffix"></slot>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,7 +55,7 @@ import { ContextmenuItem } from '@/components/contextmenu';
|
|||||||
import { ResourceOpCtx, TagTreeNode } from '@/views/ops/component/tag';
|
import { ResourceOpCtx, TagTreeNode } from '@/views/ops/component/tag';
|
||||||
import { ResourceOpCtxKey } from '@/views/ops/resource/resource';
|
import { ResourceOpCtxKey } from '@/views/ops/resource/resource';
|
||||||
|
|
||||||
const resourceOpCtx: ResourceOpCtx | undefined = inject(ResourceOpCtxKey);
|
const resourceOpCtx: ResourceOpCtx | undefined = inject(ResourceOpCtxKey, undefined);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
node: {
|
node: {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full">
|
<div class="h-full">
|
||||||
<ResourceOpPanel @resize="onResizeOpPanel">
|
<el-splitter @resize="onResizeOpPanel">
|
||||||
<template #left>
|
<el-splitter-panel size="24%" max="40%">
|
||||||
<el-card class="h-full flex tag-tree-card" body-class="!p-0 flex flex-col w-full">
|
<el-card class="h-full flex tag-tree-card" body-class="!p-0 flex flex-col w-full">
|
||||||
<div class="tag-tree-header flex flex-row justify-between items-center">
|
<div class="tag-tree-header flex flex-row justify-between items-center">
|
||||||
<el-input v-model="filterText" :placeholder="$t('tag.tagFilterPlaceholder')" clearable size="small" class="tag-tree-search w-full">
|
<el-input v-model="filterText" :placeholder="$t('tag.tagFilterPlaceholder')" clearable size="small" class="tag-tree-search w-full">
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<el-dropdown-item
|
<el-dropdown-item
|
||||||
:command="{ name }"
|
:command="{ name }"
|
||||||
v-for="(compConf, name) in resourceComponents"
|
v-for="(compConf, name) in resourceComponents"
|
||||||
:disabled="name == activeResourceComp"
|
:disabled="name == activeResourceCompName"
|
||||||
>
|
>
|
||||||
<SvgIcon v-if="compConf.icon" :name="compConf.icon.name" :color="compConf.icon.color" />
|
<SvgIcon v-if="compConf.icon" :name="compConf.icon.name" :color="compConf.icon.color" />
|
||||||
<div class="ml-1">{{ $t(name) }}</div>
|
<div class="ml-1">{{ $t(name) }}</div>
|
||||||
@@ -44,6 +44,7 @@
|
|||||||
:filter-node-method="filterNode"
|
:filter-node-method="filterNode"
|
||||||
@node-click="treeNodeClick"
|
@node-click="treeNodeClick"
|
||||||
@node-expand="treeNodeClick"
|
@node-expand="treeNodeClick"
|
||||||
|
@node-contextmenu="onNodeContextmenu"
|
||||||
:default-expanded-keys="state.defaultExpandedKeys"
|
:default-expanded-keys="state.defaultExpandedKeys"
|
||||||
>
|
>
|
||||||
<template #default="{ node, data }">
|
<template #default="{ node, data }">
|
||||||
@@ -53,24 +54,27 @@
|
|||||||
</el-tree>
|
</el-tree>
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
</el-card>
|
</el-card>
|
||||||
</template>
|
</el-splitter-panel>
|
||||||
|
|
||||||
<template #right>
|
<el-splitter-panel>
|
||||||
<el-card class="h-full" body-class=" h-full !p-1 flex flex-col flex-1">
|
<el-card class="h-full" body-class=" h-full !p-1 flex flex-col flex-1">
|
||||||
<transition name="slide-x" mode="out-in">
|
<transition name="slide-x" mode="out-in">
|
||||||
<keep-alive>
|
<keep-alive>
|
||||||
<component :is="resourceComponents[activeResourceComp]?.component" :key="activeResourceComp" @init="initResourceComp" />
|
<component :is="resourceComponents[activeResourceCompName]?.component" :key="activeResourceCompName" @init="initResourceComp" />
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</transition>
|
</transition>
|
||||||
</el-card>
|
</el-card>
|
||||||
</template>
|
</el-splitter-panel>
|
||||||
</ResourceOpPanel>
|
</el-splitter>
|
||||||
|
|
||||||
|
<Contextmenu :dropdown="state.dropdown" :items="state.contextmenuItems" ref="contextmenuRef" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { markRaw, nextTick, provide, reactive, ref, toRefs, useTemplateRef, watch } from 'vue';
|
import { markRaw, nextTick, provide, reactive, ref, toRefs, useTemplateRef, watch } from 'vue';
|
||||||
|
|
||||||
|
import { Contextmenu } from '@/components/contextmenu';
|
||||||
import { isPrefixSubsequence } from '@/common/utils/string';
|
import { isPrefixSubsequence } from '@/common/utils/string';
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
@@ -78,7 +82,6 @@ import EnumValue from '@/common/Enum';
|
|||||||
import { getResourceNodeType, getResourceTypes, ResourceOpCtxKey } from './resource';
|
import { getResourceNodeType, getResourceTypes, ResourceOpCtxKey } from './resource';
|
||||||
import BaseTreeNode from './BaseTreeNode.vue';
|
import BaseTreeNode from './BaseTreeNode.vue';
|
||||||
import { tagApi } from '@/views/ops/tag/api';
|
import { tagApi } from '@/views/ops/tag/api';
|
||||||
import ResourceOpPanel from '@/views/ops/component/ResourceOpPanel.vue';
|
|
||||||
import { TagTreeNode, ResourceComponentConfig, ResourceOpCtx } from '@/views/ops/component/tag';
|
import { TagTreeNode, ResourceComponentConfig, ResourceOpCtx } from '@/views/ops/component/tag';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useAutoOpenResource } from '@/store/autoOpenResource';
|
import { useAutoOpenResource } from '@/store/autoOpenResource';
|
||||||
@@ -109,11 +112,16 @@ const { t } = useI18n();
|
|||||||
const emit = defineEmits(['nodeClick', 'currentContextmenuClick']);
|
const emit = defineEmits(['nodeClick', 'currentContextmenuClick']);
|
||||||
|
|
||||||
const treeRef: any = useTemplateRef('treeRef');
|
const treeRef: any = useTemplateRef('treeRef');
|
||||||
|
const contextmenuRef: any = useTemplateRef('contextmenuRef');
|
||||||
|
|
||||||
// 存储所有注册的资源组件引用
|
// 存储所有注册的资源组件引用,key -> 组件名称
|
||||||
const resourceComponents = ref<Record<string, ResourceComponentConfig>>({});
|
const resourceComponents = ref<Record<string, ResourceComponentConfig>>({});
|
||||||
// 当前激活的资源组件
|
|
||||||
const activeResourceComp = ref<string>('');
|
// 存储当前组件对应的最后操作的节点key,用户切换资源操作组件时,定位到相应的树节点
|
||||||
|
const resourceComponentsNodeKey = ref<Record<string, string>>({});
|
||||||
|
|
||||||
|
// 当前激活(正在操作)的资源组件
|
||||||
|
const activeResourceCompName = ref<string>('');
|
||||||
|
|
||||||
const resourceComponentRefs = ref<Record<string, any>>({});
|
const resourceComponentRefs = ref<Record<string, any>>({});
|
||||||
|
|
||||||
@@ -131,6 +139,11 @@ const setResourceComponentRefs = async (name: string, ref: any) => {
|
|||||||
const state = reactive({
|
const state = reactive({
|
||||||
defaultExpandedKeys: [] as string[],
|
defaultExpandedKeys: [] as string[],
|
||||||
filterText: '',
|
filterText: '',
|
||||||
|
contextmenuItems: [],
|
||||||
|
dropdown: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { filterText } = toRefs(state);
|
const { filterText } = toRefs(state);
|
||||||
@@ -213,21 +226,30 @@ const loadNode = async (node: any, resolve: (data: any) => void, reject: () => v
|
|||||||
let lastNodeClickTime = 0;
|
let lastNodeClickTime = 0;
|
||||||
|
|
||||||
const treeNodeClick = async (data: any, node: any) => {
|
const treeNodeClick = async (data: any, node: any) => {
|
||||||
const currentClickNodeTime = Date.now();
|
// 关闭可能存在的右击菜单
|
||||||
if (currentClickNodeTime - lastNodeClickTime < 300) {
|
contextmenuRef.value?.closeContextmenu();
|
||||||
treeNodeDblclick(data, node);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lastNodeClickTime = currentClickNodeTime;
|
|
||||||
|
|
||||||
if (!data.disabled && !data.type.nodeDblclickFunc && data.type.nodeClickFunc) {
|
const currentClickNodeTime = Date.now();
|
||||||
emit('nodeClick', data);
|
// 双击节点
|
||||||
await data.type.nodeClickFunc(data);
|
if (currentClickNodeTime - lastNodeClickTime < 300) {
|
||||||
|
await treeNodeDblclick(data, node);
|
||||||
|
} else {
|
||||||
|
lastNodeClickTime = currentClickNodeTime;
|
||||||
|
if (!data.disabled && !data.type.nodeDblclickFunc && data.type.nodeClickFunc) {
|
||||||
|
emit('nodeClick', data);
|
||||||
|
await data.type.nodeClickFunc(data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (activeResourceCompName.value) {
|
||||||
|
resourceComponentsNodeKey.value[activeResourceCompName.value] = data.key;
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 树节点双击事件
|
// 树节点双击事件
|
||||||
const treeNodeDblclick = (data: any, node: any) => {
|
const treeNodeDblclick = async (data: any, node: any) => {
|
||||||
if (node.expanded) {
|
if (node.expanded) {
|
||||||
node.collapse();
|
node.collapse();
|
||||||
} else {
|
} else {
|
||||||
@@ -235,10 +257,33 @@ const treeNodeDblclick = (data: any, node: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!data.disabled && data.type.nodeDblclickFunc) {
|
if (!data.disabled && data.type.nodeDblclickFunc) {
|
||||||
data.type.nodeDblclickFunc(data);
|
await data.type.nodeDblclickFunc(data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 树节点右击事件
|
||||||
|
const onNodeContextmenu = (event: any, data: any) => {
|
||||||
|
if (data.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载当前节点是否需要显示右击菜单
|
||||||
|
let items = data.type.contextMenuItems;
|
||||||
|
if (!items || items.length == 0) {
|
||||||
|
if (props.loadContextmenuItems) {
|
||||||
|
items = props.loadContextmenuItems(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!items) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.contextmenuItems = items;
|
||||||
|
const { clientX, clientY } = event;
|
||||||
|
state.dropdown.x = clientX;
|
||||||
|
state.dropdown.y = clientY;
|
||||||
|
contextmenuRef.value.openContextmenu(data);
|
||||||
|
};
|
||||||
|
|
||||||
// 初始化资源组件ref
|
// 初始化资源组件ref
|
||||||
const initResourceComp = (val: any) => {
|
const initResourceComp = (val: any) => {
|
||||||
if (!val.ref || resourceComponentRefs.value[val.name]) {
|
if (!val.ref || resourceComponentRefs.value[val.name]) {
|
||||||
@@ -248,7 +293,6 @@ const initResourceComp = (val: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const addResourceComponent = async (componentConf: ResourceComponentConfig) => {
|
const addResourceComponent = async (componentConf: ResourceComponentConfig) => {
|
||||||
console.log(componentConf);
|
|
||||||
const compName = componentConf.name;
|
const compName = componentConf.name;
|
||||||
|
|
||||||
if (!resourceComponents.value[compName]) {
|
if (!resourceComponents.value[compName]) {
|
||||||
@@ -259,7 +303,7 @@ const addResourceComponent = async (componentConf: ResourceComponentConfig) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
activeResourceComp.value = compName;
|
activeResourceCompName.value = compName;
|
||||||
|
|
||||||
// 使用一个 Promise 来确保组件引用已经被设置
|
// 使用一个 Promise 来确保组件引用已经被设置
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -279,7 +323,11 @@ const addResourceComponent = async (componentConf: ResourceComponentConfig) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const changeResourceOp = (data: any) => {
|
const changeResourceOp = (data: any) => {
|
||||||
activeResourceComp.value = data.name;
|
const compName = data.name;
|
||||||
|
activeResourceCompName.value = compName;
|
||||||
|
if (resourceComponentsNodeKey.value[compName]) {
|
||||||
|
setCurrentKey(resourceComponentsNodeKey.value[compName]);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const reloadNode = (nodeKey: any) => {
|
const reloadNode = (nodeKey: any) => {
|
||||||
|
|||||||
@@ -18,28 +18,18 @@
|
|||||||
<slot name="iconPrefix" :node="node" :data="data" />
|
<slot name="iconPrefix" :node="node" :data="data" />
|
||||||
</template>
|
</template>
|
||||||
<template #default="{ node, data }">
|
<template #default="{ node, data }">
|
||||||
<span>
|
<component v-if="data.nodeComponent" :is="data.nodeComponent" :node="node" :data="data" />
|
||||||
<span v-if="data.type.value == TagTreeNode.TagPath">
|
<BaseTreeNode v-else :node="node" :data="data" />
|
||||||
<tag-info :tag-path="data.label" />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<slot v-else :node="node" :data="data" name="prefix"></slot>
|
|
||||||
|
|
||||||
<span class="ml-0.5" :title="data.labelRemark">
|
|
||||||
<slot name="label" :data="data"> {{ data.label }}</slot>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<slot :node="node" :data="data" name="suffix"></slot>
|
|
||||||
</span>
|
|
||||||
</template>
|
</template>
|
||||||
</el-tree-select>
|
</el-tree-select>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, reactive, ref, toRefs, watch } from 'vue';
|
import { onMounted, reactive, ref, toRefs, watch } from 'vue';
|
||||||
import { NodeType, TagTreeNode } from './tag';
|
|
||||||
import TagInfo from './TagInfo.vue';
|
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
|
||||||
import { tagApi } from '../tag/api';
|
import { tagApi } from '@/views/ops/tag/api';
|
||||||
|
import BaseTreeNode from '@/views/ops/resource/BaseTreeNode.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
resourceType: {
|
resourceType: {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="tag-tree-list card !p-2 h-full flex">
|
<div class="tag-tree-list card !p-2 h-full flex">
|
||||||
<el-splitter>
|
<el-splitter>
|
||||||
<el-splitter-panel size="25%" max="35%" class="flex flex-col flex-1">
|
<el-splitter-panel size="24%" max="35%" class="flex flex-col flex-1">
|
||||||
<div class="card !p-1 !mr-1 flex flex-row items-center justify-between overflow-hidden">
|
<div class="card !p-1 !mr-1 flex flex-row items-center justify-between overflow-hidden">
|
||||||
<el-input v-model="filterTag" clearable :placeholder="$t('tag.nameFilterPlaceholder')" class="mr-2" />
|
<el-input v-model="filterTag" clearable :placeholder="$t('tag.nameFilterPlaceholder')" class="mr-2" />
|
||||||
<el-button
|
<el-button
|
||||||
@@ -16,8 +16,11 @@
|
|||||||
<template #content>
|
<template #content>
|
||||||
{{ $t('tag.tagTips1') }}
|
{{ $t('tag.tagTips1') }}
|
||||||
<br />
|
<br />
|
||||||
{{ $t('tag.tagTips2') }} <br />
|
{{ $t('tag.tagTips2') }}
|
||||||
|
<br />
|
||||||
{{ $t('tag.tagTips3') }}
|
{{ $t('tag.tagTips3') }}
|
||||||
|
<br />
|
||||||
|
{{ $t('tag.tagTips4') }}
|
||||||
</template>
|
</template>
|
||||||
<SvgIcon class="ml-1" name="question-filled" />
|
<SvgIcon class="ml-1" name="question-filled" />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
@@ -37,10 +40,6 @@
|
|||||||
@node-contextmenu="onNodeContextmenu"
|
@node-contextmenu="onNodeContextmenu"
|
||||||
@node-click="onTreeNodeClick"
|
@node-click="onTreeNodeClick"
|
||||||
:default-expanded-keys="defaultExpandedKeys"
|
:default-expanded-keys="defaultExpandedKeys"
|
||||||
draggable
|
|
||||||
:allow-drop="allowDrop"
|
|
||||||
:allow-drag="allowDrag"
|
|
||||||
@node-drop="onNodeDrop"
|
|
||||||
:expand-on-click-node="false"
|
:expand-on-click-node="false"
|
||||||
:filter-node-method="filterNode"
|
:filter-node-method="filterNode"
|
||||||
>
|
>
|
||||||
@@ -279,7 +278,7 @@ watch(filterTag, (val) => {
|
|||||||
watch(
|
watch(
|
||||||
() => state.currentTag,
|
() => state.currentTag,
|
||||||
(val: any) => {
|
(val: any) => {
|
||||||
if (val.type == TagResourceTypeEnum.Tag.value) {
|
if (val?.type == TagResourceTypeEnum.Tag.value) {
|
||||||
tagApi.countTagResource.request({ tagPath: val.codePath }).then((res: any) => {
|
tagApi.countTagResource.request({ tagPath: val.codePath }).then((res: any) => {
|
||||||
state.resourceCount = res;
|
state.resourceCount = res;
|
||||||
});
|
});
|
||||||
@@ -289,82 +288,6 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const allowDrop = (draggingNode: any, dropNode: any, type: any) => {
|
|
||||||
// 不允许同层级移动
|
|
||||||
if (type != 'inner') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dropNodeData = dropNode.data;
|
|
||||||
const draggingNodeData = draggingNode.data;
|
|
||||||
const dropTagType = dropNodeData.type;
|
|
||||||
const draggingTagType = draggingNodeData.type;
|
|
||||||
|
|
||||||
// 目标节点只允许为标签类型
|
|
||||||
if (dropTagType != TagResourceTypeEnum.Tag.value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 目标节点下没有子节点
|
|
||||||
if (!dropNodeData.children) {
|
|
||||||
// 都为标签类型允许移动
|
|
||||||
if (dropTagType == draggingTagType && dropTagType == TagResourceTypeEnum.Tag.value) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 目标节点为标签,允许移动
|
|
||||||
if (dropTagType == TagResourceTypeEnum.Tag.value) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let child of dropNodeData.children) {
|
|
||||||
// 当前移动节点若在目标节点下有相同code,则不允许移动
|
|
||||||
if (draggingNodeData.code == child.code) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const childType = child.type;
|
|
||||||
// 移动节点非标签类型时(资源标签),并且子节点存在标签类型,则不允许移动,因为资源只允许放在叶子标签类型下
|
|
||||||
if (draggingTagType != TagResourceTypeEnum.Tag.value && childType == TagResourceTypeEnum.Tag.value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移动节点为标签类型时(资源标签),并且子节点存在资源类型,则不允许移动
|
|
||||||
if (draggingTagType == TagResourceTypeEnum.Tag.value && childType != TagResourceTypeEnum.Tag.value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const allowDrag = (node: any) => {
|
|
||||||
const tagType = node.data.type;
|
|
||||||
return (
|
|
||||||
tagType == TagResourceTypeEnum.Tag.value ||
|
|
||||||
tagType == TagResourceTypeEnum.DbInstance.value ||
|
|
||||||
tagType == TagResourceTypeEnum.Redis.value ||
|
|
||||||
tagType == TagResourceTypeEnum.Machine.value ||
|
|
||||||
tagType == TagResourceTypeEnum.Mongo.value
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onNodeDrop = async (draggingNode: any, dropNode: any) => {
|
|
||||||
const draggingData = draggingNode.data;
|
|
||||||
const dropData = dropNode.data;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await tagApi.movingTag.request({
|
|
||||||
fromPath: draggingData.codePath,
|
|
||||||
toPath: dropData.codePath,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
search();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onTabChange = () => {
|
const onTabChange = () => {
|
||||||
setNowTabData();
|
setNowTabData();
|
||||||
};
|
};
|
||||||
@@ -502,7 +425,7 @@ const removeDeafultExpandId = (id: any) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss" scoped>
|
||||||
.tag-tree-list {
|
.tag-tree-list {
|
||||||
.tag-tree-data {
|
.tag-tree-data {
|
||||||
// .el-tree-node__content {
|
// .el-tree-node__content {
|
||||||
|
|||||||
@@ -6,42 +6,42 @@ require (
|
|||||||
gitee.com/chunanyong/dm v1.8.20
|
gitee.com/chunanyong/dm v1.8.20
|
||||||
gitee.com/liuzongyang/libpq v1.10.11
|
gitee.com/liuzongyang/libpq v1.10.11
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.1
|
github.com/antlr4-go/antlr/v4 v4.13.1
|
||||||
github.com/docker/docker v28.3.3+incompatible
|
github.com/docker/docker v28.5.0+incompatible
|
||||||
github.com/docker/go-connections v0.6.0
|
github.com/docker/go-connections v0.6.0
|
||||||
github.com/gin-gonic/gin v1.10.1
|
github.com/gin-gonic/gin v1.11.0
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
github.com/go-gormigrate/gormigrate/v2 v2.1.4
|
github.com/go-gormigrate/gormigrate/v2 v2.1.5
|
||||||
github.com/go-ldap/ldap/v3 v3.4.11
|
github.com/go-ldap/ldap/v3 v3.4.12
|
||||||
github.com/go-playground/locales v0.14.1
|
github.com/go-playground/locales v0.14.1
|
||||||
github.com/go-playground/universal-translator v0.18.1
|
github.com/go-playground/universal-translator v0.18.1
|
||||||
github.com/go-playground/validator/v10 v10.27.0
|
github.com/go-playground/validator/v10 v10.28.0
|
||||||
github.com/go-sql-driver/mysql v1.9.3
|
github.com/go-sql-driver/mysql v1.9.3
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20250630080345-f9402614f6ba
|
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20250930013652-2d71241a3bb9
|
||||||
github.com/microsoft/go-mssqldb v1.9.2
|
github.com/microsoft/go-mssqldb v1.9.3
|
||||||
github.com/mojocn/base64Captcha v1.3.8 // 验证码
|
github.com/mojocn/base64Captcha v1.3.8 // 验证码
|
||||||
github.com/opencontainers/image-spec v1.1.1
|
github.com/opencontainers/image-spec v1.1.1
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/pkg/sftp v1.13.9
|
github.com/pkg/sftp v1.13.9
|
||||||
github.com/pquerna/otp v1.5.0
|
github.com/pquerna/otp v1.5.0
|
||||||
github.com/redis/go-redis/v9 v9.12.1
|
github.com/redis/go-redis/v9 v9.14.0
|
||||||
github.com/robfig/cron/v3 v3.0.1 // 定时任务
|
github.com/robfig/cron/v3 v3.0.1 // 定时任务
|
||||||
github.com/sijms/go-ora/v2 v2.9.0
|
github.com/sijms/go-ora/v2 v2.9.0
|
||||||
github.com/spf13/cast v1.9.2
|
github.com/spf13/cast v1.10.0
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/tidwall/gjson v1.18.0
|
github.com/tidwall/gjson v1.18.0
|
||||||
github.com/veops/go-ansiterm v0.0.5
|
github.com/veops/go-ansiterm v0.0.5
|
||||||
go.mongodb.org/mongo-driver/v2 v2.2.2 // mongo
|
go.mongodb.org/mongo-driver/v2 v2.3.0 // mongo
|
||||||
golang.org/x/crypto v0.41.0 // ssh
|
golang.org/x/crypto v0.43.0 // ssh
|
||||||
golang.org/x/oauth2 v0.30.0
|
golang.org/x/oauth2 v0.32.0
|
||||||
golang.org/x/sync v0.16.0
|
golang.org/x/sync v0.17.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
// gorm
|
// gorm
|
||||||
gorm.io/driver/mysql v1.6.0
|
gorm.io/driver/mysql v1.6.0
|
||||||
gorm.io/gorm v1.30.2
|
gorm.io/gorm v1.31.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -49,10 +49,12 @@ require (
|
|||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/boombuler/barcode v1.1.0 // indirect
|
github.com/boombuler/barcode v1.1.0 // indirect
|
||||||
github.com/bytedance/sonic v1.14.0 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
|
github.com/bytedance/sonic v1.14.1 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/containerd/errdefs v1.0.0 // indirect
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
@@ -62,14 +64,14 @@ require (
|
|||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
@@ -82,7 +84,7 @@ require (
|
|||||||
github.com/kr/fs v0.1.0 // indirect
|
github.com/kr/fs v0.1.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||||
github.com/moby/term v0.5.2 // indirect
|
github.com/moby/term v0.5.2 // indirect
|
||||||
@@ -93,9 +95,10 @@ require (
|
|||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.55.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/tidwall/match v1.2.0 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
@@ -104,24 +107,26 @@ require (
|
|||||||
github.com/xdg-go/scram v1.1.2 // indirect
|
github.com/xdg-go/scram v1.1.2 // indirect
|
||||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
|
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
go.uber.org/mock v0.6.0 // indirect
|
||||||
golang.org/x/arch v0.19.0 // indirect
|
golang.org/x/arch v0.21.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect
|
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect
|
||||||
golang.org/x/image v0.29.0 // indirect
|
golang.org/x/image v0.31.0 // indirect
|
||||||
golang.org/x/net v0.42.0 // indirect
|
golang.org/x/mod v0.28.0 // indirect
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
golang.org/x/net v0.45.0 // indirect
|
||||||
golang.org/x/text v0.28.0 // indirect
|
golang.org/x/sys v0.37.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
golang.org/x/text v0.30.0 // indirect
|
||||||
modernc.org/libc v1.66.4 // indirect
|
golang.org/x/tools v0.37.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
|
modernc.org/libc v1.66.10 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
modernc.org/sqlite v1.38.1 // indirect
|
modernc.org/sqlite v1.39.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace google.golang.org/genproto => google.golang.org/genproto v0.0.0-20250603155806-513f23925822
|
replace google.golang.org/genproto => google.golang.org/genproto v0.0.0-20250603155806-513f23925822
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ func (d *dbAppImpl) GetDbConnByInstanceId(ctx context.Context, instanceId uint64
|
|||||||
return nil, errorx.NewBiz("failed to get database list")
|
return nil, errorx.NewBiz("failed to get database list")
|
||||||
}
|
}
|
||||||
if len(dbs) == 0 {
|
if len(dbs) == 0 {
|
||||||
return nil, errorx.NewBiz("DB instance [%d] database is not configured, please configure it first", instanceId)
|
return nil, errorx.NewBizf("DB instance [%d] database is not configured, please configure it first", instanceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用该实例关联的已配置数据库中的第一个库进行连接并返回
|
// 使用该实例关联的已配置数据库中的第一个库进行连接并返回
|
||||||
@@ -308,7 +308,7 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
|
|||||||
}
|
}
|
||||||
if len(tbs) <= 0 {
|
if len(tbs) <= 0 {
|
||||||
log(fmt.Sprintf("failed to get table [%s] information: No table information was retrieved", tableName))
|
log(fmt.Sprintf("failed to get table [%s] information: No table information was retrieved", tableName))
|
||||||
return errorx.NewBiz("Failed to get table information: %s", tableName)
|
return errorx.NewBizf("Failed to get table information: %s", tableName)
|
||||||
}
|
}
|
||||||
|
|
||||||
tableInfo := tbs[0]
|
tableInfo := tbs[0]
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ func (app *dataSyncAppImpl) Delete(ctx context.Context, id uint64) error {
|
|||||||
|
|
||||||
func (app *dataSyncAppImpl) Run(ctx context.Context, id uint64) error {
|
func (app *dataSyncAppImpl) Run(ctx context.Context, id uint64) error {
|
||||||
if app.IsRunning(id) {
|
if app.IsRunning(id) {
|
||||||
logx.Warn("[%d] the db sync task is running...", id)
|
logx.Warnf("[%d] the db sync task is running...", id)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ func (app *dataSyncAppImpl) Run(ctx context.Context, id uint64) error {
|
|||||||
}
|
}
|
||||||
updateStateTask.Id = id
|
updateStateTask.Id = id
|
||||||
if err := app.UpdateById(ctx, updateStateTask); err != nil {
|
if err := app.UpdateById(ctx, updateStateTask); err != nil {
|
||||||
return errorx.NewBiz("failed to update task running state: %s", err.Error())
|
return errorx.NewBizf("failed to update task running state: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标记该任务运行中
|
// 标记该任务运行中
|
||||||
@@ -136,7 +136,7 @@ func (app *dataSyncAppImpl) Run(ctx context.Context, id uint64) error {
|
|||||||
logx.ErrorfContext(ctx, "data source connection unavailable: %s", err.Error())
|
logx.ErrorfContext(ctx, "data source connection unavailable: %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
srcConn, err := app.dbApp.GetDbConn(ctx, uint64(task.SrcDbId), task.SrcDbName)
|
srcConn, err := app.dbApp.GetDbConn(context.Background(), uint64(task.SrcDbId), task.SrcDbName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logx.ErrorfContext(ctx, "failed to connect to the source database: %s", err.Error())
|
logx.ErrorfContext(ctx, "failed to connect to the source database: %s", err.Error())
|
||||||
return
|
return
|
||||||
@@ -184,20 +184,20 @@ func (app *dataSyncAppImpl) doDataSync(ctx context.Context, sql string, task *en
|
|||||||
srcConn, err := app.dbApp.GetDbConn(ctx, uint64(task.SrcDbId), task.SrcDbName)
|
srcConn, err := app.dbApp.GetDbConn(ctx, uint64(task.SrcDbId), task.SrcDbName)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.NewBiz("failed to connect to the source database: %s", err.Error())
|
return errorx.NewBizf("failed to connect to the source database: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取目标数据库连接
|
// 获取目标数据库连接
|
||||||
targetConn, err := app.dbApp.GetDbConn(ctx, uint64(task.TargetDbId), task.TargetDbName)
|
targetConn, err := app.dbApp.GetDbConn(ctx, uint64(task.TargetDbId), task.TargetDbName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.NewBiz("failed to connect to the target database: %s", err.Error())
|
return errorx.NewBizf("failed to connect to the target database: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// task.FieldMap为json数组字符串 [{"src":"id","target":"id"}],转为map
|
// task.FieldMap为json数组字符串 [{"src":"id","target":"id"}],转为map
|
||||||
var fieldMap []map[string]string
|
var fieldMap []map[string]string
|
||||||
err = json.Unmarshal([]byte(task.FieldMap), &fieldMap)
|
err = json.Unmarshal([]byte(task.FieldMap), &fieldMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.NewBiz("there was an error parsing the field map json: %s", err.Error())
|
return errorx.NewBizf("there was an error parsing the field map json: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录本次同步数据总数
|
// 记录本次同步数据总数
|
||||||
@@ -213,7 +213,7 @@ func (app *dataSyncAppImpl) doDataSync(ctx context.Context, sql string, task *en
|
|||||||
|
|
||||||
targetTableColumns, err := targetConn.GetMetadata().GetColumns(task.TargetTableName)
|
targetTableColumns, err := targetConn.GetMetadata().GetColumns(task.TargetTableName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.NewBiz("failed to get target table columns: %s", err.Error())
|
return errorx.NewBizf("failed to get target table columns: %s", err.Error())
|
||||||
}
|
}
|
||||||
targetColumnName2Column := collx.ArrayToMap(targetTableColumns, func(column dbi.Column) string {
|
targetColumnName2Column := collx.ArrayToMap(targetTableColumns, func(column dbi.Column) string {
|
||||||
return column.ColumnName
|
return column.ColumnName
|
||||||
@@ -300,7 +300,7 @@ func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap [
|
|||||||
// 开启本批次执行事务
|
// 开启本批次执行事务
|
||||||
targetDbTx, err := targetDbConn.Begin()
|
targetDbTx, err := targetDbConn.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.NewBiz("failed to start the target database transaction: %s", err.Error())
|
return errorx.NewBizf("failed to start the target database transaction: %s", err.Error())
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
@@ -320,7 +320,7 @@ func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap [
|
|||||||
// 如果是mssql,暂不手动提交事务,否则报错 mssql: The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
|
// 如果是mssql,暂不手动提交事务,否则报错 mssql: The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
|
||||||
if err := targetDbTx.Commit(); err != nil {
|
if err := targetDbTx.Commit(); err != nil {
|
||||||
if targetDbConn.Info.Type != dbi.ToDbType("mssql") {
|
if targetDbConn.Info.Type != dbi.ToDbType("mssql") {
|
||||||
return errorx.NewBiz("data synchronization - The target database transaction failed to commit: %s", err.Error())
|
return errorx.NewBizf("data synchronization - The target database transaction failed to commit: %s", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,9 +370,11 @@ func (app *dataSyncAppImpl) saveLog(log *entity.DataSyncLog) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *dataSyncAppImpl) InitCronJob() {
|
func (app *dataSyncAppImpl) InitCronJob() {
|
||||||
|
ctx := contextx.NewTraceId()
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
logx.ErrorTrace("the data synchronization task failed to initialize", err)
|
logx.ErrorTraceContext(ctx, "the data synchronization task failed to initialize", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -380,10 +382,11 @@ func (app *dataSyncAppImpl) InitCronJob() {
|
|||||||
_ = app.UpdateByCond(context.TODO(), &entity.DataSyncTask{RunningState: entity.DataSyncTaskRunStateReady}, &entity.DataSyncTask{RunningState: entity.DataSyncTaskRunStateRunning})
|
_ = app.UpdateByCond(context.TODO(), &entity.DataSyncTask{RunningState: entity.DataSyncTaskRunStateReady}, &entity.DataSyncTask{RunningState: entity.DataSyncTaskRunStateRunning})
|
||||||
|
|
||||||
if err := app.CursorByCond(&entity.DataSyncTaskQuery{Status: entity.DataSyncTaskStatusEnable}, func(dst *entity.DataSyncTask) error {
|
if err := app.CursorByCond(&entity.DataSyncTaskQuery{Status: entity.DataSyncTaskStatusEnable}, func(dst *entity.DataSyncTask) error {
|
||||||
app.addCronJob(contextx.NewTraceId(), dst)
|
app.MarkStop(dst.Id)
|
||||||
|
app.addCronJob(ctx, dst)
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
logx.ErrorTrace("the db data sync task failed to initialize: %v", err)
|
logx.ErrorTraceContext(ctx, "the db data sync task failed to initialize: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,13 +417,13 @@ func (app *dataSyncAppImpl) addCronJob(ctx context.Context, taskEntity *entity.D
|
|||||||
// 根据状态添加新的任务
|
// 根据状态添加新的任务
|
||||||
if taskEntity.Status == entity.DataSyncTaskStatusEnable {
|
if taskEntity.Status == entity.DataSyncTaskStatusEnable {
|
||||||
taskId := taskEntity.Id
|
taskId := taskEntity.Id
|
||||||
logx.Infof("start add the data sync task job: %s, cron[%s]", taskEntity.TaskName, taskEntity.TaskCron)
|
logx.InfofContext(ctx, "start add the data sync task job: %s, cron[%s]", taskEntity.TaskName, taskEntity.TaskCron)
|
||||||
if err := scheduler.AddFunByKey(key, taskEntity.TaskCron, func() {
|
if err := scheduler.AddFunByKey(key, taskEntity.TaskCron, func() {
|
||||||
if err := app.Run(context.Background(), taskId); err != nil {
|
if err := app.Run(context.Background(), taskId); err != nil {
|
||||||
logx.Errorf("the data sync task failed to execute at a scheduled time: %s", err.Error())
|
logx.ErrorfContext(ctx, "the data sync task failed to execute at a scheduled time: %s", err.Error())
|
||||||
}
|
}
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
logx.ErrorTrace("add db data sync job failed", err)
|
logx.ErrorTraceContext(ctx, "add db data sync job failed", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ func (d *dbSqlExecAppImpl) FlowBizHandle(ctx context.Context, bizHandleParam *fl
|
|||||||
|
|
||||||
execSqlBizForm, err := jsonx.To[*FlowDbExecSqlBizForm](procinst.BizForm)
|
execSqlBizForm, err := jsonx.To[*FlowDbExecSqlBizForm](procinst.BizForm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errorx.NewBiz("failed to parse the business form information: %s", err.Error())
|
return nil, errorx.NewBizf("failed to parse the business form information: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
dbConn, err := d.dbApp.GetDbConn(ctx, execSqlBizForm.DbId, execSqlBizForm.DbName)
|
dbConn, err := d.dbApp.GetDbConn(ctx, execSqlBizForm.DbId, execSqlBizForm.DbName)
|
||||||
@@ -471,7 +471,7 @@ func (d *dbSqlExecAppImpl) doUpdate(ctx context.Context, sqlExecParam *sqlExecPa
|
|||||||
nowRec++
|
nowRec++
|
||||||
res = append(res, row)
|
res = append(res, row)
|
||||||
if nowRec == maxRec {
|
if nowRec == maxRec {
|
||||||
return errorx.NewBiz("update SQL - the maximum number of updated queries is exceeded: %d", maxRec)
|
return errorx.NewBizf("update SQL - the maximum number of updated queries is exceeded: %d", maxRec)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -122,12 +122,12 @@ func (app *dbTransferAppImpl) InitCronJob() {
|
|||||||
|
|
||||||
func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64) (uint64, error) {
|
func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64) (uint64, error) {
|
||||||
if app.IsRunning(taskId) {
|
if app.IsRunning(taskId) {
|
||||||
return 0, errorx.NewBiz("the db transfer task [%d] is running, please do not repeat the operation", taskId)
|
return 0, errorx.NewBizf("the db transfer task [%d] is running, please do not repeat the operation", taskId)
|
||||||
}
|
}
|
||||||
|
|
||||||
task, err := app.GetById(taskId)
|
task, err := app.GetById(taskId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, errorx.NewBiz("db transfer task [%d] not found", taskId)
|
return 0, errorx.NewBizf("db transfer task [%d] not found", taskId)
|
||||||
}
|
}
|
||||||
|
|
||||||
logId, _ := app.CreateLog(ctx, taskId)
|
logId, _ := app.CreateLog(ctx, taskId)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mayfly-go/pkg/utils/collx"
|
"mayfly-go/pkg/utils/collx"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -111,6 +112,12 @@ func ClearNumScale(column *Column) {
|
|||||||
column.CharMaxLength = 0
|
column.CharMaxLength = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ClearNumPrecision(column *Column) {
|
||||||
|
column.NumScale = 0
|
||||||
|
column.NumPrecision = 0
|
||||||
|
column.CharMaxLength = 0
|
||||||
|
}
|
||||||
|
|
||||||
// DataType 数据类型, 对应于go类型,如int int64等。可自定义其他类型
|
// DataType 数据类型, 对应于go类型,如int int64等。可自定义其他类型
|
||||||
type DataType struct {
|
type DataType struct {
|
||||||
Name string // 类型名
|
Name string // 类型名
|
||||||
@@ -173,7 +180,13 @@ func SQLValueString(val any) string {
|
|||||||
return fmt.Sprintf("%v", val)
|
return fmt.Sprintf("%v", val)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("'%s'", strings.ReplaceAll(strings.ReplaceAll(strVal, "'", "''"), `\`, `\\`))
|
// 使用 strconv.Quote 来处理所有特殊字符
|
||||||
|
quoted := strconv.Quote(strVal)
|
||||||
|
// 去掉 strconv.Quote 添加的外层引号,因为会在最后添加 SQL 的单引号
|
||||||
|
quoted = quoted[1 : len(quoted)-1]
|
||||||
|
// 处理 SQL 中的单引号
|
||||||
|
quoted = strings.ReplaceAll(quoted, "'", "''")
|
||||||
|
return fmt.Sprintf("'%s'", quoted)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -41,6 +41,15 @@ func (d *DbConn) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DbConn) Ping() error {
|
func (d *DbConn) Ping() error {
|
||||||
|
// 首先检查d是否为nil
|
||||||
|
if d == nil {
|
||||||
|
return fmt.Errorf("d is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 然后检查d.db是否为nil,这是避免空指针异常的关键
|
||||||
|
if d.db == nil {
|
||||||
|
return fmt.Errorf("db is nil")
|
||||||
|
}
|
||||||
return d.db.Ping()
|
return d.db.Ping()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,13 +70,13 @@ func (di *DbInfo) Conn(ctx context.Context, meta Meta) (*DbConn, error) {
|
|||||||
conn, err := meta.GetSqlDb(ctx, di)
|
conn, err := meta.GetSqlDb(ctx, di)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logx.Errorf("db connection failed: %s:%d/%s, err:%s", di.Host, di.Port, database, err.Error())
|
logx.Errorf("db connection failed: %s:%d/%s, err:%s", di.Host, di.Port, database, err.Error())
|
||||||
return nil, errorx.NewBiz("db connection failed: %s", err.Error())
|
return nil, errorx.NewBizf("db connection failed: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
err = conn.Ping()
|
err = conn.Ping()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logx.Errorf("db ping failed: %s:%d/%s, err:%s", di.Host, di.Port, database, err.Error())
|
logx.Errorf("db ping failed: %s:%d/%s, err:%s", di.Host, di.Port, database, err.Error())
|
||||||
return nil, errorx.NewBiz("db connection failed: %s", err.Error())
|
return nil, errorx.NewBizf("db connection failed: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
dbc := &DbConn{Id: GetDbConnId(di.Id, database), Info: di}
|
dbc := &DbConn{Id: GetDbConnId(di.Id, database), Info: di}
|
||||||
|
|||||||
@@ -24,17 +24,16 @@ type Meta struct {
|
|||||||
func (dm *Meta) GetSqlDb(ctx context.Context, d *dbi.DbInfo) (*sql.DB, error) {
|
func (dm *Meta) GetSqlDb(ctx context.Context, d *dbi.DbInfo) (*sql.DB, error) {
|
||||||
driverName := "dm"
|
driverName := "dm"
|
||||||
db := d.Database
|
db := d.Database
|
||||||
var dbParam string
|
dbParam := "?escapeProcess=true"
|
||||||
if db != "" {
|
if db != "" {
|
||||||
// dm database可以使用db/schema表示,方便连接指定schema, 若不存在schema则使用默认schema
|
// dm database可以使用db/schema表示,方便连接指定schema, 若不存在schema则使用默认schema
|
||||||
ss := strings.Split(db, "/")
|
ss := strings.Split(db, "/")
|
||||||
if len(ss) > 1 {
|
if len(ss) > 1 {
|
||||||
dbParam = fmt.Sprintf("%s?schema=\"%s\"&escapeProcess=true", ss[0], ss[len(ss)-1])
|
dbParam = fmt.Sprintf("%s&schema=\"%s\"", dbParam, ss[len(ss)-1])
|
||||||
} else {
|
|
||||||
dbParam = db + "?escapeProcess=true"
|
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
dbParam = "?escapeProcess=true"
|
if d.Params != "" {
|
||||||
|
dbParam += "&" + d.Params
|
||||||
}
|
}
|
||||||
|
|
||||||
err := d.IfUseSshTunnelChangeIpPort(ctx)
|
err := d.IfUseSshTunnelChangeIpPort(ctx)
|
||||||
@@ -42,7 +41,7 @@ func (dm *Meta) GetSqlDb(ctx context.Context, d *dbi.DbInfo) (*sql.DB, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
dsn := fmt.Sprintf("dm://%s:%s@%s:%d/%s", d.Username, url.PathEscape(d.Password), d.Host, d.Port, dbParam)
|
dsn := fmt.Sprintf("dm://%s:%s@%s:%d%s", d.Username, url.PathEscape(d.Password), d.Host, d.Port, dbParam)
|
||||||
return sql.Open(driverName, dsn)
|
return sql.Open(driverName, dsn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ func (dd *DMMetadata) GetPrimaryKey(tablename string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if len(columns) == 0 {
|
if len(columns) == 0 {
|
||||||
return "", errorx.NewBiz("[%s] 表不存在", tablename)
|
return "", errorx.NewBizf("[%s] 表不存在", tablename)
|
||||||
}
|
}
|
||||||
for _, v := range columns {
|
for _, v := range columns {
|
||||||
if v.IsPrimaryKey {
|
if v.IsPrimaryKey {
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ func (md *MssqlMetadata) GetPrimaryKey(tablename string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if len(columns) == 0 {
|
if len(columns) == 0 {
|
||||||
return "", errorx.NewBiz("[%s] 表不存在", tablename)
|
return "", errorx.NewBizf("[%s] 表不存在", tablename)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range columns {
|
for _, v := range columns {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ var (
|
|||||||
UnsignedMediumint = dbi.NewDbDataType("unsigned mediumint", dbi.DTInt64).WithCT(dbi.CTUnsignedInt4).WithFixColumn(dbi.ClearNumScale)
|
UnsignedMediumint = dbi.NewDbDataType("unsigned mediumint", dbi.DTInt64).WithCT(dbi.CTUnsignedInt4).WithFixColumn(dbi.ClearNumScale)
|
||||||
|
|
||||||
Decimal = dbi.NewDbDataType("decimal", dbi.DTDecimal).WithCT(dbi.CTDecimal)
|
Decimal = dbi.NewDbDataType("decimal", dbi.DTDecimal).WithCT(dbi.CTDecimal)
|
||||||
Double = dbi.NewDbDataType("double", dbi.DTNumeric).WithCT(dbi.CTNumeric)
|
Double = dbi.NewDbDataType("double", dbi.DTNumeric).WithCT(dbi.CTNumeric).WithFixColumn(dbi.ClearNumPrecision)
|
||||||
Float = dbi.NewDbDataType("float", dbi.DTNumeric).WithCT(dbi.CTNumeric)
|
Float = dbi.NewDbDataType("float", dbi.DTNumeric).WithCT(dbi.CTNumeric)
|
||||||
|
|
||||||
Varchar = dbi.NewDbDataType("varchar", dbi.DTString).WithCT(dbi.CTVarchar)
|
Varchar = dbi.NewDbDataType("varchar", dbi.DTString).WithCT(dbi.CTVarchar)
|
||||||
@@ -40,9 +40,9 @@ var (
|
|||||||
Enum = dbi.NewDbDataType("enum", dbi.DTString).WithCT(dbi.CTEnum)
|
Enum = dbi.NewDbDataType("enum", dbi.DTString).WithCT(dbi.CTEnum)
|
||||||
Set = dbi.NewDbDataType("set", dbi.DTString).WithCT(dbi.CTVarchar)
|
Set = dbi.NewDbDataType("set", dbi.DTString).WithCT(dbi.CTVarchar)
|
||||||
|
|
||||||
Blob = dbi.NewDbDataType("blob", dbi.DTBytes).WithCT(dbi.CTBlob)
|
Blob = dbi.NewDbDataType("blob", dbi.DTBytes).WithCT(dbi.CTBlob).WithFixColumn(dbi.ClearNumScale)
|
||||||
Mediumblob = dbi.NewDbDataType("mediumblob", dbi.DTBytes).WithCT(dbi.CTMediumblob)
|
Mediumblob = dbi.NewDbDataType("mediumblob", dbi.DTBytes).WithCT(dbi.CTMediumblob).WithFixColumn(dbi.ClearNumScale)
|
||||||
Longblob = dbi.NewDbDataType("longblob", dbi.DTBytes).WithCT(dbi.CTLongblob)
|
Longblob = dbi.NewDbDataType("longblob", dbi.DTBytes).WithCT(dbi.CTLongblob).WithFixColumn(dbi.ClearNumScale)
|
||||||
Binary = dbi.NewDbDataType("binary", dbi.DTBytes).WithCT(dbi.CTBinary)
|
Binary = dbi.NewDbDataType("binary", dbi.DTBytes).WithCT(dbi.CTBinary)
|
||||||
Varbinary = dbi.NewDbDataType("varbinary", dbi.DTBytes).WithCT(dbi.CTVarbinary)
|
Varbinary = dbi.NewDbDataType("varbinary", dbi.DTBytes).WithCT(dbi.CTVarbinary)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ func (md *MysqlMetadata) GetPrimaryKey(tablename string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if len(columns) == 0 {
|
if len(columns) == 0 {
|
||||||
return "", errorx.NewBiz("[%s] 表不存在", tablename)
|
return "", errorx.NewBizf("[%s] 表不存在", tablename)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range columns {
|
for _, v := range columns {
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ func (od *OracleMetadata) GetPrimaryKey(tablename string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if len(columns) == 0 {
|
if len(columns) == 0 {
|
||||||
return "", errorx.NewBiz("[%s] 表不存在", tablename)
|
return "", errorx.NewBizf("[%s] 表不存在", tablename)
|
||||||
}
|
}
|
||||||
for _, v := range columns {
|
for _, v := range columns {
|
||||||
if v.IsPrimaryKey {
|
if v.IsPrimaryKey {
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ func (pd *PgsqlMetadata) GetPrimaryKey(tablename string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if len(columns) == 0 {
|
if len(columns) == 0 {
|
||||||
return "", errorx.NewBiz("[%s] 表不存在", tablename)
|
return "", errorx.NewBizf("[%s] 表不存在", tablename)
|
||||||
}
|
}
|
||||||
for _, v := range columns {
|
for _, v := range columns {
|
||||||
if v.IsPrimaryKey {
|
if v.IsPrimaryKey {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package api
|
|||||||
import "mayfly-go/pkg/ioc"
|
import "mayfly-go/pkg/ioc"
|
||||||
|
|
||||||
func InitIoc() {
|
func InitIoc() {
|
||||||
|
ioc.Register(new(ContainerConf))
|
||||||
ioc.Register(new(Docker))
|
ioc.Register(new(Docker))
|
||||||
ioc.Register(new(Container))
|
ioc.Register(new(Container))
|
||||||
ioc.Register(new(Image))
|
ioc.Register(new(Image))
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"mayfly-go/internal/docker/api/form"
|
"mayfly-go/internal/docker/api/form"
|
||||||
"mayfly-go/internal/docker/api/vo"
|
"mayfly-go/internal/docker/api/vo"
|
||||||
"mayfly-go/internal/docker/dkm"
|
|
||||||
"mayfly-go/internal/docker/imsg"
|
"mayfly-go/internal/docker/imsg"
|
||||||
"mayfly-go/pkg/biz"
|
"mayfly-go/pkg/biz"
|
||||||
"mayfly-go/pkg/errorx"
|
"mayfly-go/pkg/errorx"
|
||||||
@@ -52,12 +51,11 @@ func (d *Container) ReqConfs() *req.Confs {
|
|||||||
req.NewGet("/logs", d.ContainerLogs).NoRes(),
|
req.NewGet("/logs", d.ContainerLogs).NoRes(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return req.NewConfs("docker/containers", reqs[:]...)
|
return req.NewConfs("docker/:id/containers", reqs[:]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Container) GetContainers(rc *req.Ctx) {
|
func (d *Container) GetContainers(rc *req.Ctx) {
|
||||||
cli, err := dkm.GetCli(rc.Query("host"))
|
cli := GetCli(rc)
|
||||||
biz.ErrIsNil(err)
|
|
||||||
cs, err := cli.ContainerList()
|
cs, err := cli.ContainerList()
|
||||||
biz.ErrIsNil(err)
|
biz.ErrIsNil(err)
|
||||||
|
|
||||||
@@ -89,8 +87,7 @@ func (d *Container) GetContainers(rc *req.Ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Container) GetContainersStats(rc *req.Ctx) {
|
func (d *Container) GetContainersStats(rc *req.Ctx) {
|
||||||
cli, err := dkm.GetCli(rc.Query("host"))
|
cli := GetCli(rc)
|
||||||
biz.ErrIsNil(err)
|
|
||||||
cs, err := cli.ContainerList()
|
cs, err := cli.ContainerList()
|
||||||
biz.ErrIsNil(err)
|
biz.ErrIsNil(err)
|
||||||
|
|
||||||
@@ -141,8 +138,7 @@ func (d *Container) ContainerCreate(rc *req.Ctx) {
|
|||||||
|
|
||||||
rc.ReqParam = containerCreate
|
rc.ReqParam = containerCreate
|
||||||
|
|
||||||
cli, err := dkm.GetCli(containerCreate.Host)
|
cli := GetCli(rc)
|
||||||
biz.ErrIsNil(err)
|
|
||||||
|
|
||||||
config, hostConfig, networkConfig, err := loadConfigInfo(true, containerCreate, nil)
|
config, hostConfig, networkConfig, err := loadConfigInfo(true, containerCreate, nil)
|
||||||
biz.ErrIsNil(err)
|
biz.ErrIsNil(err)
|
||||||
@@ -152,14 +148,14 @@ func (d *Container) ContainerCreate(rc *req.Ctx) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = cli.DockerClient.ContainerRemove(ctx, containerCreate.Name, container.RemoveOptions{RemoveVolumes: true, Force: true})
|
_ = cli.DockerClient.ContainerRemove(ctx, containerCreate.Name, container.RemoveOptions{RemoveVolumes: true, Force: true})
|
||||||
panic(errorx.NewBiz("create container failed, err: %v", err))
|
panic(errorx.NewBizf("create container failed, err: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
logx.Infof("create container %s successful! now check if the container is started and delete the container information if it is not.", containerCreate.Name)
|
logx.Infof("create container %s successful! now check if the container is started and delete the container information if it is not.", containerCreate.Name)
|
||||||
|
|
||||||
if err := cli.DockerClient.ContainerStart(ctx, con.ID, container.StartOptions{}); err != nil {
|
if err := cli.DockerClient.ContainerStart(ctx, con.ID, container.StartOptions{}); err != nil {
|
||||||
_ = cli.DockerClient.ContainerRemove(ctx, containerCreate.Name, container.RemoveOptions{RemoveVolumes: true, Force: true})
|
_ = cli.DockerClient.ContainerRemove(ctx, containerCreate.Name, container.RemoveOptions{RemoveVolumes: true, Force: true})
|
||||||
panic(errorx.NewBiz("create successful but start failed, err: %v", err))
|
panic(errorx.NewBizf("create successful but start failed, err: %v", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,36 +163,30 @@ func (d *Container) ContainerStop(rc *req.Ctx) {
|
|||||||
containerOp := &form.ContainerOp{}
|
containerOp := &form.ContainerOp{}
|
||||||
biz.ErrIsNil(rc.BindJSON(containerOp))
|
biz.ErrIsNil(rc.BindJSON(containerOp))
|
||||||
|
|
||||||
rc.ReqParam = collx.Kvs("host", containerOp.Host, "containerId", containerOp.ContainerId)
|
cli := GetCli(rc)
|
||||||
|
rc.ReqParam = collx.Kvs("addr", cli.Server.Addr, "containerId", containerOp.ContainerId)
|
||||||
|
|
||||||
cli, err := dkm.GetCli(containerOp.Host)
|
biz.ErrIsNil(cli.ContainerStop(containerOp.ContainerId))
|
||||||
biz.ErrIsNil(err)
|
|
||||||
err = cli.ContainerStop(containerOp.ContainerId)
|
|
||||||
biz.ErrIsNil(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Container) ContainerRemove(rc *req.Ctx) {
|
func (d *Container) ContainerRemove(rc *req.Ctx) {
|
||||||
containerOp := &form.ContainerOp{}
|
containerOp := &form.ContainerOp{}
|
||||||
biz.ErrIsNil(rc.BindJSON(containerOp))
|
biz.ErrIsNil(rc.BindJSON(containerOp))
|
||||||
|
|
||||||
rc.ReqParam = collx.Kvs("host", containerOp.Host, "containerId", containerOp.ContainerId)
|
cli := GetCli(rc)
|
||||||
|
rc.ReqParam = collx.Kvs("addr", cli.Server.Addr, "containerId", containerOp.ContainerId)
|
||||||
|
|
||||||
cli, err := dkm.GetCli(containerOp.Host)
|
biz.ErrIsNil(cli.ContainerRemove(containerOp.ContainerId))
|
||||||
biz.ErrIsNil(err)
|
|
||||||
err = cli.ContainerRemove(containerOp.ContainerId)
|
|
||||||
biz.ErrIsNil(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Container) ContainerRestart(rc *req.Ctx) {
|
func (d *Container) ContainerRestart(rc *req.Ctx) {
|
||||||
containerOp := &form.ContainerOp{}
|
containerOp := &form.ContainerOp{}
|
||||||
biz.ErrIsNil(rc.BindJSON(containerOp))
|
biz.ErrIsNil(rc.BindJSON(containerOp))
|
||||||
|
|
||||||
rc.ReqParam = collx.Kvs("host", containerOp.Host, "containerId", containerOp.ContainerId)
|
cli := GetCli(rc)
|
||||||
|
rc.ReqParam = collx.Kvs("addr", cli.Server.Addr, "containerId", containerOp.ContainerId)
|
||||||
|
|
||||||
cli, err := dkm.GetCli(containerOp.Host)
|
biz.ErrIsNil(cli.ContainerRestart(containerOp.ContainerId))
|
||||||
biz.ErrIsNil(err)
|
|
||||||
err = cli.ContainerRestart(containerOp.ContainerId)
|
|
||||||
biz.ErrIsNil(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Container) ContainerLogs(rc *req.Ctx) {
|
func (d *Container) ContainerLogs(rc *req.Ctx) {
|
||||||
@@ -211,9 +201,7 @@ func (d *Container) ContainerLogs(rc *req.Ctx) {
|
|||||||
}()
|
}()
|
||||||
biz.ErrIsNilAppendErr(err, "Upgrade websocket fail: %s")
|
biz.ErrIsNilAppendErr(err, "Upgrade websocket fail: %s")
|
||||||
|
|
||||||
cli, err := dkm.GetCli(rc.Query("host"))
|
cli := GetCli(rc)
|
||||||
biz.ErrIsNil(err)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(rc.MetaCtx)
|
ctx, cancel := context.WithCancel(rc.MetaCtx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -288,9 +276,7 @@ func (d *Container) ContainerExecAttach(rc *req.Ctx) {
|
|||||||
biz.ErrIsNilAppendErr(err, "Upgrade websocket fail: %s")
|
biz.ErrIsNilAppendErr(err, "Upgrade websocket fail: %s")
|
||||||
wsConn.WriteMessage(websocket.TextMessage, []byte("Connecting to container..."))
|
wsConn.WriteMessage(websocket.TextMessage, []byte("Connecting to container..."))
|
||||||
|
|
||||||
cli, err := dkm.GetCli(rc.Query("host"))
|
cli := GetCli(rc)
|
||||||
biz.ErrIsNil(err)
|
|
||||||
|
|
||||||
cols := rc.QueryIntDefault("cols", 80)
|
cols := rc.QueryIntDefault("cols", 80)
|
||||||
rows := rc.QueryIntDefault("rows", 32)
|
rows := rc.QueryIntDefault("rows", 32)
|
||||||
|
|
||||||
@@ -311,9 +297,7 @@ func (d *Container) ContainerProxy(rc *req.Ctx) {
|
|||||||
containerID := pathParts[2]
|
containerID := pathParts[2]
|
||||||
remainingPath := strings.Join(pathParts[3:], "/")
|
remainingPath := strings.Join(pathParts[3:], "/")
|
||||||
|
|
||||||
cli, err := dkm.GetCli(rc.Query("host"))
|
cli := GetCli(rc)
|
||||||
biz.ErrIsNil(err)
|
|
||||||
|
|
||||||
ctx := rc.MetaCtx
|
ctx := rc.MetaCtx
|
||||||
containerJSON, err := cli.DockerClient.ContainerInspect(ctx, containerID)
|
containerJSON, err := cli.DockerClient.ContainerInspect(ctx, containerID)
|
||||||
biz.ErrIsNil(err)
|
biz.ErrIsNil(err)
|
||||||
|
|||||||
96
server/internal/docker/api/container_conf.go
Normal file
96
server/internal/docker/api/container_conf.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mayfly-go/internal/docker/api/form"
|
||||||
|
"mayfly-go/internal/docker/api/vo"
|
||||||
|
"mayfly-go/internal/docker/application"
|
||||||
|
"mayfly-go/internal/docker/application/dto"
|
||||||
|
"mayfly-go/internal/docker/dkm"
|
||||||
|
"mayfly-go/internal/docker/domain/entity"
|
||||||
|
"mayfly-go/internal/pkg/consts"
|
||||||
|
tagapp "mayfly-go/internal/tag/application"
|
||||||
|
tagentity "mayfly-go/internal/tag/domain/entity"
|
||||||
|
"mayfly-go/pkg/biz"
|
||||||
|
"mayfly-go/pkg/model"
|
||||||
|
"mayfly-go/pkg/req"
|
||||||
|
"mayfly-go/pkg/utils/collx"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ContainerConf struct {
|
||||||
|
containerApp application.Container `inject:"T"`
|
||||||
|
tagTreeApp tagapp.TagTree `inject:"T"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *ContainerConf) ReqConfs() *req.Confs {
|
||||||
|
reqs := [...]*req.Conf{
|
||||||
|
req.NewGet("/page", cc.GetContainerPage),
|
||||||
|
req.NewPost("/save", cc.Save),
|
||||||
|
req.NewDelete("/del/:ids", cc.Delete),
|
||||||
|
}
|
||||||
|
|
||||||
|
return req.NewConfs("docker/container-conf", reqs[:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *ContainerConf) GetContainerPage(rc *req.Ctx) {
|
||||||
|
condition := req.BindQuery[*entity.ContainerQuery](rc)
|
||||||
|
|
||||||
|
tags := cc.tagTreeApp.GetAccountTags(rc.GetLoginAccount().Id, &tagentity.TagTreeQuery{
|
||||||
|
TypePaths: collx.AsArray(tagentity.NewTypePaths(tagentity.TagTypeContainer)),
|
||||||
|
CodePathLikes: collx.AsArray(condition.TagPath),
|
||||||
|
})
|
||||||
|
// 不存在,即没有可操作数据
|
||||||
|
if len(tags) == 0 {
|
||||||
|
rc.ResData = model.NewEmptyPageResult[any]()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tagCodePaths := tags.GetCodePaths()
|
||||||
|
containerCodes := tagentity.GetCodesByCodePaths(tagentity.TagTypeContainer, tagCodePaths...)
|
||||||
|
condition.Codes = collx.ArrayDeduplicate(containerCodes)
|
||||||
|
|
||||||
|
res, err := cc.containerApp.GetContainerPage(condition)
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
if res.Total == 0 {
|
||||||
|
rc.ResData = res
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resVo := model.PageResultConv[*entity.Container, *vo.ContainerConf](res)
|
||||||
|
containerVos := resVo.List
|
||||||
|
cc.tagTreeApp.FillTagInfo(tagentity.TagType(consts.ResourceTypeContainer), collx.ArrayMap(containerVos, func(cvo *vo.ContainerConf) tagentity.ITagResource {
|
||||||
|
return cvo
|
||||||
|
})...)
|
||||||
|
|
||||||
|
rc.ResData = resVo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ContainerConf) Save(rc *req.Ctx) {
|
||||||
|
machineForm, container := req.BindJsonAndCopyTo[*form.ContainerSave, *entity.Container](rc)
|
||||||
|
rc.ReqParam = machineForm
|
||||||
|
|
||||||
|
biz.ErrIsNil(c.containerApp.SaveContainer(rc.MetaCtx, &dto.SaveContainer{
|
||||||
|
Container: container,
|
||||||
|
TagCodePaths: machineForm.TagCodePaths,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ContainerConf) Delete(rc *req.Ctx) {
|
||||||
|
idsStr := rc.PathParam("ids")
|
||||||
|
rc.ReqParam = idsStr
|
||||||
|
ids := strings.Split(idsStr, ",")
|
||||||
|
|
||||||
|
for _, v := range ids {
|
||||||
|
biz.ErrIsNil(c.containerApp.DeleteContainer(rc.MetaCtx, cast.ToUint64(v)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCli(rc *req.Ctx) *dkm.Client {
|
||||||
|
id := rc.PathParamInt("id")
|
||||||
|
biz.IsTrue(id > 0, "id error")
|
||||||
|
cli, err := application.GetContainerApp().GetContainerCli(rc.MetaCtx, uint64(id))
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
return cli
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"mayfly-go/internal/docker/dkm"
|
|
||||||
"mayfly-go/pkg/biz"
|
"mayfly-go/pkg/biz"
|
||||||
"mayfly-go/pkg/req"
|
"mayfly-go/pkg/req"
|
||||||
)
|
)
|
||||||
@@ -14,13 +13,11 @@ func (d *Docker) ReqConfs() *req.Confs {
|
|||||||
req.NewGet("/info", d.GetDockerInfo),
|
req.NewGet("/info", d.GetDockerInfo),
|
||||||
}
|
}
|
||||||
|
|
||||||
return req.NewConfs("docker", reqs[:]...)
|
return req.NewConfs("docker/:id", reqs[:]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Docker) GetDockerInfo(rc *req.Ctx) {
|
func (d *Docker) GetDockerInfo(rc *req.Ctx) {
|
||||||
host := rc.Query("host")
|
cli := GetCli(rc)
|
||||||
cli, err := dkm.GetCli(host)
|
|
||||||
biz.ErrIsNil(err)
|
|
||||||
info, err := cli.DockerClient.Info(rc.MetaCtx)
|
info, err := cli.DockerClient.Info(rc.MetaCtx)
|
||||||
biz.ErrIsNil(err)
|
biz.ErrIsNil(err)
|
||||||
rc.ResData = info
|
rc.ResData = info
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
package form
|
package form
|
||||||
|
|
||||||
|
import "mayfly-go/pkg/model"
|
||||||
|
|
||||||
|
type ContainerSave struct {
|
||||||
|
model.ExtraData
|
||||||
|
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
Addr string `json:"addr" binding:"required"`
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
TagCodePaths []string `json:"tagCodePaths" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
type ContainerOp struct {
|
type ContainerOp struct {
|
||||||
Host string `json:"host"`
|
|
||||||
ContainerId string `json:"containerId" binding:"required"`
|
ContainerId string `json:"containerId" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContainerCreate struct {
|
type ContainerCreate struct {
|
||||||
Host string `json:"host" binding:"required"`
|
|
||||||
ContainerID string `json:"containerId"`
|
ContainerID string `json:"containerId"`
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Image string `json:"image" validate:"required"`
|
Image string `json:"image" validate:"required"`
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package form
|
package form
|
||||||
|
|
||||||
type ImageOp struct {
|
type ImageOp struct {
|
||||||
Host string `json:"host" binding:"required"`
|
|
||||||
ImageId string `json:"imageId" binding:"required"`
|
ImageId string `json:"imageId" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"mayfly-go/internal/docker/api/form"
|
"mayfly-go/internal/docker/api/form"
|
||||||
"mayfly-go/internal/docker/api/vo"
|
"mayfly-go/internal/docker/api/vo"
|
||||||
"mayfly-go/internal/docker/dkm"
|
|
||||||
"mayfly-go/internal/docker/imsg"
|
"mayfly-go/internal/docker/imsg"
|
||||||
"mayfly-go/pkg/biz"
|
"mayfly-go/pkg/biz"
|
||||||
"mayfly-go/pkg/req"
|
"mayfly-go/pkg/req"
|
||||||
@@ -32,12 +31,11 @@ func (d *Image) ReqConfs() *req.Confs {
|
|||||||
req.NewPost("/load", d.ImageLoad).Log(req.NewLogSaveI(imsg.LogDockerImageLoad)),
|
req.NewPost("/load", d.ImageLoad).Log(req.NewLogSaveI(imsg.LogDockerImageLoad)),
|
||||||
}
|
}
|
||||||
|
|
||||||
return req.NewConfs("docker/images", reqs[:]...)
|
return req.NewConfs("docker/:id/images", reqs[:]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Image) GetImages(rc *req.Ctx) {
|
func (d *Image) GetImages(rc *req.Ctx) {
|
||||||
cli, err := dkm.GetCli(rc.Query("host"))
|
cli := GetCli(rc)
|
||||||
biz.ErrIsNil(err)
|
|
||||||
is, err := cli.ImageList()
|
is, err := cli.ImageList()
|
||||||
biz.ErrIsNil(err)
|
biz.ErrIsNil(err)
|
||||||
|
|
||||||
@@ -62,18 +60,12 @@ func (d *Image) ImageRemove(rc *req.Ctx) {
|
|||||||
imageOp := &form.ImageOp{}
|
imageOp := &form.ImageOp{}
|
||||||
biz.ErrIsNil(rc.BindJSON(imageOp))
|
biz.ErrIsNil(rc.BindJSON(imageOp))
|
||||||
|
|
||||||
rc.ReqParam = collx.Kvs("host", imageOp.Host, "imageId", imageOp.ImageId)
|
rc.ReqParam = collx.Kvs("imageId", imageOp.ImageId)
|
||||||
cli, err := dkm.GetCli(imageOp.Host)
|
cli := GetCli(rc)
|
||||||
biz.ErrIsNil(err)
|
biz.ErrIsNil(cli.ImageRemove(imageOp.ImageId))
|
||||||
err = cli.ImageRemove(imageOp.ImageId)
|
|
||||||
biz.ErrIsNil(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Image) ImageLoad(rc *req.Ctx) {
|
func (d *Image) ImageLoad(rc *req.Ctx) {
|
||||||
host := rc.PostForm("host")
|
|
||||||
biz.NotEmpty(host, "host cannot be empty")
|
|
||||||
rc.ReqParam = host
|
|
||||||
|
|
||||||
fileheader, err := rc.FormFile("file")
|
fileheader, err := rc.FormFile("file")
|
||||||
biz.ErrIsNilAppendErr(err, "read form file error: %s")
|
biz.ErrIsNilAppendErr(err, "read form file error: %s")
|
||||||
|
|
||||||
@@ -81,8 +73,9 @@ func (d *Image) ImageLoad(rc *req.Ctx) {
|
|||||||
biz.ErrIsNil(err)
|
biz.ErrIsNil(err)
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
cli, err := dkm.GetCli(host)
|
cli := GetCli(rc)
|
||||||
biz.ErrIsNil(err)
|
rc.ReqParam = cli.Server
|
||||||
|
|
||||||
resp, err := cli.DockerClient.ImageLoad(rc.MetaCtx, file)
|
resp, err := cli.DockerClient.ImageLoad(rc.MetaCtx, file)
|
||||||
biz.ErrIsNil(err)
|
biz.ErrIsNil(err)
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
@@ -94,14 +87,10 @@ func (d *Image) ImageLoad(rc *req.Ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Image) ImageExport(rc *req.Ctx) {
|
func (d *Image) ImageExport(rc *req.Ctx) {
|
||||||
host := rc.Query("host")
|
|
||||||
biz.NotEmpty(host, "host cannot be empty")
|
|
||||||
tag := rc.Query("tag")
|
tag := rc.Query("tag")
|
||||||
biz.NotEmpty(tag, "tag cannot be empty")
|
biz.NotEmpty(tag, "tag cannot be empty")
|
||||||
|
|
||||||
cli, err := dkm.GetCli(host)
|
cli := GetCli(rc)
|
||||||
biz.ErrIsNil(err)
|
|
||||||
|
|
||||||
reader, err := cli.DockerClient.ImageSave(rc.MetaCtx, []string{tag}, client.ImageSaveWithPlatforms())
|
reader, err := cli.DockerClient.ImageSave(rc.MetaCtx, []string{tag}, client.ImageSaveWithPlatforms())
|
||||||
biz.ErrIsNil(err)
|
biz.ErrIsNil(err)
|
||||||
defer reader.Close()
|
defer reader.Close()
|
||||||
|
|||||||
21
server/internal/docker/api/vo/container.go
Normal file
21
server/internal/docker/api/vo/container.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package vo
|
||||||
|
|
||||||
|
import (
|
||||||
|
tagentity "mayfly-go/internal/tag/domain/entity"
|
||||||
|
"mayfly-go/pkg/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ContainerConf struct {
|
||||||
|
model.Model
|
||||||
|
model.ExtraData
|
||||||
|
tagentity.ResourceTags // 标签信息
|
||||||
|
|
||||||
|
Addr string `json:"addr"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ContainerConf) GetCode() string {
|
||||||
|
return c.Code
|
||||||
|
}
|
||||||
13
server/internal/docker/application/application.go
Normal file
13
server/internal/docker/application/application.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mayfly-go/pkg/ioc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitIoc() {
|
||||||
|
ioc.Register(new(containerAppImpl), ioc.WithComponentName("ContainerApp"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetContainerApp() Container {
|
||||||
|
return ioc.Get[Container]("ContainerApp")
|
||||||
|
}
|
||||||
144
server/internal/docker/application/container.go
Normal file
144
server/internal/docker/application/container.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"mayfly-go/internal/docker/application/dto"
|
||||||
|
"mayfly-go/internal/docker/dkm"
|
||||||
|
"mayfly-go/internal/docker/domain/entity"
|
||||||
|
"mayfly-go/internal/docker/domain/repository"
|
||||||
|
"mayfly-go/internal/docker/imsg"
|
||||||
|
tagapp "mayfly-go/internal/tag/application"
|
||||||
|
tagdto "mayfly-go/internal/tag/application/dto"
|
||||||
|
tagentity "mayfly-go/internal/tag/domain/entity"
|
||||||
|
"mayfly-go/pkg/base"
|
||||||
|
"mayfly-go/pkg/errorx"
|
||||||
|
"mayfly-go/pkg/model"
|
||||||
|
"mayfly-go/pkg/utils/stringx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Container interface {
|
||||||
|
base.App[*entity.Container]
|
||||||
|
|
||||||
|
GetContainerPage(condition *entity.ContainerQuery, orderBy ...string) (*model.PageResult[*entity.Container], error)
|
||||||
|
|
||||||
|
// SaveContainer 保存容器配置信息
|
||||||
|
SaveContainer(context.Context, *dto.SaveContainer) error
|
||||||
|
|
||||||
|
// DeleteContainer 删除容器配置信息
|
||||||
|
DeleteContainer(context.Context, uint64) error
|
||||||
|
|
||||||
|
// GetContaienrCli 获取容器客户端
|
||||||
|
GetContainerCli(context.Context, uint64) (*dkm.Client, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type containerAppImpl struct {
|
||||||
|
base.AppImpl[*entity.Container, repository.Container]
|
||||||
|
|
||||||
|
tagApp tagapp.TagTree `inject:"T"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ (Container) = (*containerAppImpl)(nil)
|
||||||
|
|
||||||
|
func (c *containerAppImpl) GetContainerPage(condition *entity.ContainerQuery, orderBy ...string) (*model.PageResult[*entity.Container], error) {
|
||||||
|
return c.Repo.GetContainerPage(condition, orderBy...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *containerAppImpl) SaveContainer(ctx context.Context, saveContainer *dto.SaveContainer) error {
|
||||||
|
container := saveContainer.Container
|
||||||
|
tagCodePaths := saveContainer.TagCodePaths
|
||||||
|
resourceType := tagentity.TagTypeContainer
|
||||||
|
|
||||||
|
oldContainer := &entity.Container{
|
||||||
|
Addr: container.Addr,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.GetByCond(oldContainer)
|
||||||
|
|
||||||
|
if container.Id == 0 {
|
||||||
|
if err == nil {
|
||||||
|
return errorx.NewBizI(ctx, imsg.ErrContainerConfExist)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成随机编号
|
||||||
|
container.Code = stringx.Rand(10)
|
||||||
|
|
||||||
|
return c.Tx(ctx, func(ctx context.Context) error {
|
||||||
|
if err := c.Insert(ctx, container); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.tagApp.SaveResourceTag(ctx, &tagdto.SaveResourceTag{
|
||||||
|
ResourceTag: &tagdto.ResourceTag{
|
||||||
|
Code: container.Code,
|
||||||
|
Name: container.Name,
|
||||||
|
Type: resourceType,
|
||||||
|
},
|
||||||
|
ParentTagCodePaths: tagCodePaths,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil && container.Id != oldContainer.Id {
|
||||||
|
return errorx.NewBizI(ctx, imsg.ErrContainerConfExist)
|
||||||
|
}
|
||||||
|
if oldContainer.Code == "" {
|
||||||
|
oldContainer, _ = c.GetById(container.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
dkm.CloseCli(oldContainer.Id)
|
||||||
|
return c.Tx(ctx, func(ctx context.Context) error {
|
||||||
|
if err := c.UpdateById(ctx, container); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldContainer.Name != container.Name {
|
||||||
|
if err := c.tagApp.UpdateTagName(ctx, tagentity.TagTypeMachine, oldContainer.Code, container.Name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.tagApp.SaveResourceTag(ctx, &tagdto.SaveResourceTag{
|
||||||
|
ResourceTag: &tagdto.ResourceTag{
|
||||||
|
Code: oldContainer.Code,
|
||||||
|
Name: container.Name,
|
||||||
|
Type: resourceType,
|
||||||
|
},
|
||||||
|
ParentTagCodePaths: tagCodePaths,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *containerAppImpl) DeleteContainer(ctx context.Context, id uint64) error {
|
||||||
|
container, err := c.GetById(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dkm.CloseCli(id)
|
||||||
|
return c.Tx(ctx, func(ctx context.Context) error {
|
||||||
|
if err := c.DeleteById(ctx, id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.tagApp.SaveResourceTag(ctx, &tagdto.SaveResourceTag{
|
||||||
|
ResourceTag: &tagdto.ResourceTag{
|
||||||
|
Code: container.Code,
|
||||||
|
Type: tagentity.TagTypeContainer,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *containerAppImpl) GetContainerCli(ctx context.Context, id uint64) (*dkm.Client, error) {
|
||||||
|
return dkm.GetCli(id, func(u uint64) (*dkm.ContainerServer, error) {
|
||||||
|
containerConf, err := c.GetById(u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &dkm.ContainerServer{
|
||||||
|
Id: id,
|
||||||
|
Addr: containerConf.Addr,
|
||||||
|
}, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
8
server/internal/docker/application/dto/container.go
Normal file
8
server/internal/docker/application/dto/container.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import "mayfly-go/internal/docker/domain/entity"
|
||||||
|
|
||||||
|
type SaveContainer struct {
|
||||||
|
Container *entity.Container
|
||||||
|
TagCodePaths []string
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package dkm
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mayfly-go/internal/machine/mcm"
|
"mayfly-go/internal/machine/mcm"
|
||||||
"mayfly-go/pkg/logx"
|
"mayfly-go/pkg/logx"
|
||||||
@@ -26,13 +27,13 @@ const (
|
|||||||
DefaultServer = "unix:///var/run/docker.sock"
|
DefaultServer = "unix:///var/run/docker.sock"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DockerServer struct {
|
type ContainerServer struct {
|
||||||
Host string
|
Id uint64
|
||||||
|
Addr string
|
||||||
Client *client.Client
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
|
Server *ContainerServer
|
||||||
DockerClient *client.Client
|
DockerClient *client.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,12 +47,13 @@ func (c *Client) Ping() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetCli get docker cli
|
// GetCli get docker cli
|
||||||
func GetCli(host string) (*Client, error) {
|
func GetCli(id uint64, getContainer func(uint64) (*ContainerServer, error)) (*Client, error) {
|
||||||
if host == "" {
|
pool, err := poolGroup.GetCachePool(fmt.Sprintf("%d", id), func() (*Client, error) {
|
||||||
host = DefaultServer
|
containerServer, err := getContainer(id)
|
||||||
}
|
if err != nil {
|
||||||
pool, err := poolGroup.GetCachePool(host, func() (*Client, error) {
|
return nil, err
|
||||||
return NewClient(&DockerServer{Host: host})
|
}
|
||||||
|
return NewClient(containerServer)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -59,18 +61,23 @@ func GetCli(host string) (*Client, error) {
|
|||||||
return pool.Get(context.Background())
|
return pool.Get(context.Background())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CloseCli(id uint64) error {
|
||||||
|
return poolGroup.Close(fmt.Sprintf("%d", id))
|
||||||
|
}
|
||||||
|
|
||||||
// NewClient new docker client
|
// NewClient new docker client
|
||||||
func NewClient(server *DockerServer) (*Client, error) {
|
func NewClient(server *ContainerServer) (*Client, error) {
|
||||||
if server.Host == "" {
|
if server.Addr == "" {
|
||||||
server.Host = DefaultServer
|
server.Addr = DefaultServer
|
||||||
}
|
}
|
||||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithHost(server.Host), client.WithAPIVersionNegotiation())
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithHost(server.Addr), client.WithAPIVersionNegotiation())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Client{
|
return &Client{
|
||||||
DockerClient: cli,
|
DockerClient: cli,
|
||||||
|
Server: server,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
14
server/internal/docker/domain/entity/contrainer.go
Normal file
14
server/internal/docker/domain/entity/contrainer.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
import "mayfly-go/pkg/model"
|
||||||
|
|
||||||
|
// 容器配置
|
||||||
|
type Container struct {
|
||||||
|
model.Model
|
||||||
|
model.ExtraData
|
||||||
|
|
||||||
|
Code string `json:"code" gorm:"size:32;comment:code"` // code
|
||||||
|
Name string `json:"name" gorm:"size:32"` // 名称
|
||||||
|
Addr string `json:"addr" gorm:"size:64;not null;comment:地址"` // 地址
|
||||||
|
Remark string `json:"remark" gorm:"comment:备注"` // 备注
|
||||||
|
}
|
||||||
16
server/internal/docker/domain/entity/query.go
Normal file
16
server/internal/docker/domain/entity/query.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
import "mayfly-go/pkg/model"
|
||||||
|
|
||||||
|
type ContainerQuery struct {
|
||||||
|
model.PageParam
|
||||||
|
|
||||||
|
Id uint64 `json:"id" form:"id"`
|
||||||
|
Code string `json:"code" form:"code"`
|
||||||
|
Name string `json:"name" form:"name"`
|
||||||
|
Addr string `json:"addr" form:"addr"`
|
||||||
|
TagPath string `json:"tagPath" form:"tagPath"`
|
||||||
|
|
||||||
|
Keyword string `json:"keyword" form:"keyword"`
|
||||||
|
Codes []string
|
||||||
|
}
|
||||||
14
server/internal/docker/domain/repository/container.go
Normal file
14
server/internal/docker/domain/repository/container.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mayfly-go/internal/docker/domain/entity"
|
||||||
|
"mayfly-go/pkg/base"
|
||||||
|
"mayfly-go/pkg/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Container interface {
|
||||||
|
base.Repo[*entity.Container]
|
||||||
|
|
||||||
|
// 分页获取容器配置列表
|
||||||
|
GetContainerPage(condition *entity.ContainerQuery, orderBy ...string) (*model.PageResult[*entity.Container], error)
|
||||||
|
}
|
||||||
@@ -5,4 +5,11 @@ import "mayfly-go/pkg/i18n"
|
|||||||
var En = map[i18n.MsgId]string{
|
var En = map[i18n.MsgId]string{
|
||||||
LogDockerContainerStop: "Container - Stop",
|
LogDockerContainerStop: "Container - Stop",
|
||||||
LogDockerContainerRestart: "Container - Restart",
|
LogDockerContainerRestart: "Container - Restart",
|
||||||
|
LogDockerContainerRemove: "Container - Remove",
|
||||||
|
LogDockerContainerCreate: "Container - Create",
|
||||||
|
|
||||||
|
LogDockerImageRemove: "Image - Remove",
|
||||||
|
LogDockerImageLoad: "Image - Load",
|
||||||
|
|
||||||
|
ErrContainerConfExist: "Container conf already exists",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,4 +18,6 @@ const (
|
|||||||
|
|
||||||
LogDockerImageRemove
|
LogDockerImageRemove
|
||||||
LogDockerImageLoad
|
LogDockerImageLoad
|
||||||
|
|
||||||
|
ErrContainerConfExist
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,4 +10,6 @@ var Zh_CN = map[i18n.MsgId]string{
|
|||||||
|
|
||||||
LogDockerImageRemove: "镜像-删除",
|
LogDockerImageRemove: "镜像-删除",
|
||||||
LogDockerImageLoad: "镜像-导入",
|
LogDockerImageLoad: "镜像-导入",
|
||||||
|
|
||||||
|
ErrContainerConfExist: "容器配置已存在",
|
||||||
}
|
}
|
||||||
|
|||||||
33
server/internal/docker/infra/persistence/container.go
Normal file
33
server/internal/docker/infra/persistence/container.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package persistence
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mayfly-go/internal/docker/domain/entity"
|
||||||
|
"mayfly-go/internal/docker/domain/repository"
|
||||||
|
"mayfly-go/pkg/base"
|
||||||
|
"mayfly-go/pkg/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type containerRepoImpl struct {
|
||||||
|
base.RepoImpl[*entity.Container]
|
||||||
|
}
|
||||||
|
|
||||||
|
func newContainerRepo() repository.Container {
|
||||||
|
return &containerRepoImpl{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *containerRepoImpl) GetContainerPage(condition *entity.ContainerQuery, orderBy ...string) (*model.PageResult[*entity.Container], error) {
|
||||||
|
qd := model.NewCond().
|
||||||
|
Eq("id", condition.Id).
|
||||||
|
Like("addr", condition.Addr).
|
||||||
|
Like("name", condition.Name).
|
||||||
|
In("code", condition.Codes).
|
||||||
|
Eq("code", condition.Code)
|
||||||
|
|
||||||
|
keyword := condition.Keyword
|
||||||
|
if keyword != "" {
|
||||||
|
keyword = "%" + keyword + "%"
|
||||||
|
qd.And("addr like ? or name like ? or code like ?", keyword, keyword, keyword)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.PageByCond(qd, condition.PageParam)
|
||||||
|
}
|
||||||
9
server/internal/docker/infra/persistence/persistence.go
Normal file
9
server/internal/docker/infra/persistence/persistence.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package persistence
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mayfly-go/pkg/ioc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitIoc() {
|
||||||
|
ioc.Register(newContainerRepo(), ioc.WithComponentName("ContainerRepo"))
|
||||||
|
}
|
||||||
@@ -1,7 +1,16 @@
|
|||||||
package init
|
package init
|
||||||
|
|
||||||
import "mayfly-go/internal/docker/api"
|
import (
|
||||||
|
"mayfly-go/initialize"
|
||||||
|
"mayfly-go/internal/docker/api"
|
||||||
|
"mayfly-go/internal/docker/application"
|
||||||
|
"mayfly-go/internal/docker/infra/persistence"
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
api.InitIoc()
|
initialize.AddInitIocFunc(func() {
|
||||||
|
persistence.InitIoc()
|
||||||
|
application.InitIoc()
|
||||||
|
api.InitIoc()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
type InstanceForm struct {
|
type InstanceForm struct {
|
||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
|
Protocol string `binding:"required" json:"protocol"`
|
||||||
Name string `binding:"required" json:"name"`
|
Name string `binding:"required" json:"name"`
|
||||||
Host string `binding:"required" json:"host"`
|
Host string `binding:"required" json:"host"`
|
||||||
Port int `binding:"required" json:"port"`
|
Port int `binding:"required" json:"port"`
|
||||||
|
|||||||
@@ -9,13 +9,14 @@ type InstanceListVO struct {
|
|||||||
tagentity.AuthCerts // 授权凭证信息
|
tagentity.AuthCerts // 授权凭证信息
|
||||||
tagentity.ResourceTags
|
tagentity.ResourceTags
|
||||||
|
|
||||||
Id *int64 `json:"id"`
|
Id *int64 `json:"id"`
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
Name *string `json:"name"`
|
Name *string `json:"name"`
|
||||||
Host *string `json:"host"`
|
Protocol *string `json:"protocol"`
|
||||||
Port *int `json:"port"`
|
Host *string `json:"host"`
|
||||||
Version *string `json:"version"`
|
Port *int `json:"port"`
|
||||||
Remark *string `json:"remark"`
|
Version *string `json:"version"`
|
||||||
|
Remark *string `json:"remark"`
|
||||||
|
|
||||||
CreateTime *time.Time `json:"createTime"`
|
CreateTime *time.Time `json:"createTime"`
|
||||||
Creator *string `json:"creator"`
|
Creator *string `json:"creator"`
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type EsInstance struct {
|
|||||||
|
|
||||||
Code string `json:"code" gorm:"size:32;not null;"`
|
Code string `json:"code" gorm:"size:32;not null;"`
|
||||||
Name string `json:"name" gorm:"size:32;not null;"`
|
Name string `json:"name" gorm:"size:32;not null;"`
|
||||||
|
Protocol string `json:"protocol" gorm:"size:10;not null;"`
|
||||||
Host string `json:"host" gorm:"size:255;not null;"`
|
Host string `json:"host" gorm:"size:255;not null;"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
Network string `json:"network" gorm:"size:20;"`
|
Network string `json:"network" gorm:"size:20;"`
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package esi
|
package esi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mayfly-go/internal/machine/mcm"
|
"mayfly-go/internal/machine/mcm"
|
||||||
"mayfly-go/pkg/logx"
|
"mayfly-go/pkg/logx"
|
||||||
@@ -27,6 +28,15 @@ func (d *EsConn) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *EsConn) Ping() error {
|
func (d *EsConn) Ping() error {
|
||||||
|
// 首先检查d是否为nil
|
||||||
|
if d == nil {
|
||||||
|
return fmt.Errorf("es connection is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 然后检查d.Info是否为nil,这是避免空指针异常的关键
|
||||||
|
if d.Info == nil {
|
||||||
|
return fmt.Errorf("es Info is nil")
|
||||||
|
}
|
||||||
_, err := d.Info.Ping()
|
_, err := d.Info.Ping()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -43,6 +53,16 @@ func (d *EsConn) StartProxy() error {
|
|||||||
d.proxy = httputil.NewSingleHostReverseProxy(targetURL)
|
d.proxy = httputil.NewSingleHostReverseProxy(targetURL)
|
||||||
// 设置 proxy buffer pool
|
// 设置 proxy buffer pool
|
||||||
d.proxy.BufferPool = NewBufferPool()
|
d.proxy.BufferPool = NewBufferPool()
|
||||||
|
|
||||||
|
// Configure TLS to skip certificate verification for non-compliant certificates
|
||||||
|
if targetURL.Scheme == "https" {
|
||||||
|
d.proxy.Transport = &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type EsInfo struct {
|
|||||||
InstanceId uint64 // 实例id
|
InstanceId uint64 // 实例id
|
||||||
Name string
|
Name string
|
||||||
|
|
||||||
|
Protocol string // 协议,默认http
|
||||||
Host string
|
Host string
|
||||||
Port int
|
Port int
|
||||||
Network string
|
Network string
|
||||||
@@ -58,14 +59,14 @@ func (di *EsInfo) Conn(ctx context.Context) (*EsConn, map[string]any, error) {
|
|||||||
err := di.IfUseSshTunnelChangeIpPort(ctx)
|
err := di.IfUseSshTunnelChangeIpPort(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logx.Errorf("es ssh failed: %s, err:%s", di.baseUrl, err.Error())
|
logx.Errorf("es ssh failed: %s, err:%s", di.baseUrl, err.Error())
|
||||||
return nil, nil, errorx.NewBiz("es ssh failed: %s", err.Error())
|
return nil, nil, errorx.NewBizf("es ssh failed: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试获取es版本信息,调用接口:get /
|
// 尝试获取es版本信息,调用接口:get /
|
||||||
res, err := di.Ping()
|
res, err := di.Ping()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logx.Errorf("es ping failed: %s, err:%s", di.baseUrl, err.Error())
|
logx.Errorf("es ping failed: %s, err:%s", di.baseUrl, err.Error())
|
||||||
return nil, nil, errorx.NewBiz("es ping failed: %s", err.Error())
|
return nil, nil, errorx.NewBizf("es ping failed: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
esc := &EsConn{Id: di.InstanceId, Info: di}
|
esc := &EsConn{Id: di.InstanceId, Info: di}
|
||||||
@@ -90,7 +91,14 @@ func (di *EsInfo) Ping() (map[string]any, error) {
|
|||||||
|
|
||||||
// ExecApi 执行api
|
// ExecApi 执行api
|
||||||
func (di *EsInfo) ExecApi(method, path string, data any, timeoutSecond ...int) (map[string]any, error) {
|
func (di *EsInfo) ExecApi(method, path string, data any, timeoutSecond ...int) (map[string]any, error) {
|
||||||
request := httpx.NewReq(di.baseUrl + path)
|
var request *httpx.Req
|
||||||
|
// Use insecure TLS client for HTTPS connections to handle non-compliant certificates
|
||||||
|
if di.Protocol == "https" {
|
||||||
|
request = httpx.NewReqWithInsecureTLS(di.baseUrl + path)
|
||||||
|
} else {
|
||||||
|
request = httpx.NewReq(di.baseUrl + path)
|
||||||
|
}
|
||||||
|
|
||||||
if di.authorization != "" {
|
if di.authorization != "" {
|
||||||
request.Header("Authorization", di.authorization)
|
request.Header("Authorization", di.authorization)
|
||||||
}
|
}
|
||||||
@@ -111,12 +119,17 @@ func (di *EsInfo) ExecApi(method, path string, data any, timeoutSecond ...int) (
|
|||||||
return request.PutObj(data).BodyToMap()
|
return request.PutObj(data).BodyToMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errorx.NewBiz("不支持的请求方法: %s", method)
|
return nil, errorx.NewBizf("不支持的请求方法: %s", method)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果使用了ssh隧道,将其host port改变其本地映射host port
|
// 如果使用了ssh隧道,将其host port改变其本地映射host port
|
||||||
func (di *EsInfo) IfUseSshTunnelChangeIpPort(ctx context.Context) error {
|
func (di *EsInfo) IfUseSshTunnelChangeIpPort(ctx context.Context) error {
|
||||||
|
// 设置默认协议
|
||||||
|
if di.Protocol == "" {
|
||||||
|
di.Protocol = "http"
|
||||||
|
}
|
||||||
|
|
||||||
// 开启ssh隧道
|
// 开启ssh隧道
|
||||||
if di.SshTunnelMachineId > 0 {
|
if di.SshTunnelMachineId > 0 {
|
||||||
stm, err := GetSshTunnel(ctx, di.SshTunnelMachineId)
|
stm, err := GetSshTunnel(ctx, di.SshTunnelMachineId)
|
||||||
@@ -130,9 +143,9 @@ func (di *EsInfo) IfUseSshTunnelChangeIpPort(ctx context.Context) error {
|
|||||||
di.Host = exposedIp
|
di.Host = exposedIp
|
||||||
di.Port = exposedPort
|
di.Port = exposedPort
|
||||||
di.useSshTunnel = true
|
di.useSshTunnel = true
|
||||||
di.baseUrl = fmt.Sprintf("http://%s:%d", exposedIp, exposedPort)
|
di.baseUrl = fmt.Sprintf("%s://%s:%d", di.Protocol, exposedIp, exposedPort)
|
||||||
} else {
|
} else {
|
||||||
di.baseUrl = fmt.Sprintf("http://%s:%d", di.Host, di.Port)
|
di.baseUrl = fmt.Sprintf("%s://%s:%d", di.Protocol, di.Host, di.Port)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -312,10 +312,10 @@ func (m *machineAppImpl) ToMachineInfoById(machineId uint64) (*mcm.MachineInfo,
|
|||||||
func (m *machineAppImpl) getMachineAndAuthCert(machineId uint64) (*entity.Machine, *tagentity.ResourceAuthCert, error) {
|
func (m *machineAppImpl) getMachineAndAuthCert(machineId uint64) (*entity.Machine, *tagentity.ResourceAuthCert, error) {
|
||||||
me, err := m.GetById(machineId)
|
me, err := m.GetById(machineId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, errorx.NewBiz("[%d] machine not found", machineId)
|
return nil, nil, errorx.NewBizf("[%d] machine not found", machineId)
|
||||||
}
|
}
|
||||||
if me.Status != entity.MachineStatusEnable && me.Protocol == 1 {
|
if me.Status != entity.MachineStatusEnable && me.Protocol == 1 {
|
||||||
return nil, nil, errorx.NewBiz("[%s] machine has been disable", me.Code)
|
return nil, nil, errorx.NewBizf("[%s] machine has been disable", me.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
authCert, err := m.resourceAuthCertApp.GetResourceAuthCert(tagentity.TagTypeMachine, me.Code)
|
authCert, err := m.resourceAuthCertApp.GetResourceAuthCert(tagentity.TagTypeMachine, me.Code)
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ func (m *machineFileAppImpl) GetDirSize(ctx context.Context, opParam *dto.Machin
|
|||||||
//du: cannot access ‘/proc/19087/fdinfo/3’: No such file or directory\n
|
//du: cannot access ‘/proc/19087/fdinfo/3’: No such file or directory\n
|
||||||
//18G /\n
|
//18G /\n
|
||||||
if res == "" {
|
if res == "" {
|
||||||
return "", errorx.NewBiz("failed to get directory size: %s", err.Error())
|
return "", errorx.NewBizf("failed to get directory size: %s", err.Error())
|
||||||
}
|
}
|
||||||
strs := strings.Split(res, "\n")
|
strs := strings.Split(res, "\n")
|
||||||
res = strs[len(strs)-2]
|
res = strs[len(strs)-2]
|
||||||
@@ -247,7 +247,7 @@ func (m *machineFileAppImpl) CreateFile(ctx context.Context, opParam *dto.Machin
|
|||||||
}
|
}
|
||||||
file, err := sftpCli.Create(path)
|
file, err := sftpCli.Create(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errorx.NewBiz("failed to create file: %s", err.Error())
|
return nil, errorx.NewBizf("failed to create file: %s", err.Error())
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
return mi, err
|
return mi, err
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func (m *machineTermOpAppImpl) TermConn(ctx context.Context, cli *mcm.Cli, wsCon
|
|||||||
|
|
||||||
fileKey, wc, saveFileFunc, err := m.fileApp.NewWriter(ctx, "", fmt.Sprintf("mto_%d_%s.cast", termOpRecord.MachineId, timex.TimeNo()))
|
fileKey, wc, saveFileFunc, err := m.fileApp.NewWriter(ctx, "", fmt.Sprintf("mto_%d_%s.cast", termOpRecord.MachineId, timex.TimeNo()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.NewBiz("failed to create a terminal playback log file: %v", err)
|
return errorx.NewBizf("failed to create a terminal playback log file: %v", err)
|
||||||
}
|
}
|
||||||
defer saveFileFunc(&err)
|
defer saveFileFunc(&err)
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func (c *Cli) GetSftpCli() (*sftp.Client, error) {
|
|||||||
if sftpclient == nil {
|
if sftpclient == nil {
|
||||||
sc, serr := sftp.NewClient(c.sshClient)
|
sc, serr := sftp.NewClient(c.sshClient)
|
||||||
if serr != nil {
|
if serr != nil {
|
||||||
return nil, errorx.NewBiz("failed to obtain the sftp client: %s", serr.Error())
|
return nil, errorx.NewBizf("failed to obtain the sftp client: %s", serr.Error())
|
||||||
}
|
}
|
||||||
sftpclient = sc
|
sftpclient = sc
|
||||||
c.sftpClient = sftpclient
|
c.sftpClient = sftpclient
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ func (mi *MachineInfo) Conn(ctx context.Context) (*Cli, error) {
|
|||||||
// 如果使用了ssh隧道,则修改机器ip port为暴露的ip port
|
// 如果使用了ssh隧道,则修改机器ip port为暴露的ip port
|
||||||
err := mi.IfUseSshTunnelChangeIpPort(ctx, false)
|
err := mi.IfUseSshTunnelChangeIpPort(ctx, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errorx.NewBiz("ssh tunnel connection failed: %s", err.Error())
|
return nil, errorx.NewBizf("ssh tunnel connection failed: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
cli := &Cli{Info: mi}
|
cli := &Cli{Info: mi}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user