mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 23:40:24 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57361d8241 | ||
|
|
b347bd7ef5 | ||
|
|
070c8ac0da | ||
|
|
e221c2f42e | ||
|
|
c7bab3a71b | ||
|
|
82c17a51a2 |
@@ -18,7 +18,7 @@ WORKDIR /mayfly
|
||||
# Copy the go source for building server
|
||||
COPY server .
|
||||
|
||||
RUN go mod download
|
||||
RUN go mod tidy && go mod download
|
||||
|
||||
COPY --from=fe-builder /mayfly/dist /mayfly/static/static
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
"countup.js": "^2.7.0",
|
||||
"cropperjs": "^1.5.11",
|
||||
"echarts": "^5.4.3",
|
||||
"element-plus": "^2.4.2",
|
||||
"jsencrypt": "^3.3.1",
|
||||
"element-plus": "^2.4.3",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
"monaco-editor": "^0.44.0",
|
||||
@@ -25,13 +25,12 @@
|
||||
"monaco-themes": "^0.4.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.1.7",
|
||||
"qrcode.vue": "^3.4.0",
|
||||
"qrcode.vue": "^3.4.1",
|
||||
"screenfull": "^6.0.2",
|
||||
"sortablejs": "^1.15.0",
|
||||
"sql-formatter": "^14.0.0",
|
||||
"uuid": "^9.0.1",
|
||||
"vue": "^3.3.9",
|
||||
"vue-clipboard3": "^1.0.1",
|
||||
"vue": "^3.3.10",
|
||||
"vue-router": "^4.2.5",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
@@ -53,7 +52,7 @@
|
||||
"prettier": "^3.0.3",
|
||||
"sass": "^1.69.0",
|
||||
"typescript": "^5.3.2",
|
||||
"vite": "^5.0.2",
|
||||
"vite": "^5.0.5",
|
||||
"vue-eslint-parser": "^9.3.1"
|
||||
},
|
||||
"browserslist": [
|
||||
|
||||
9
mayfly_go_web/src/common/commonEnum.ts
Normal file
9
mayfly_go_web/src/common/commonEnum.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import EnumValue from './Enum';
|
||||
|
||||
// 标签关联的资源类型
|
||||
export const TagResourceTypeEnum = {
|
||||
Machine: EnumValue.of(1, '机器'),
|
||||
Db: EnumValue.of(2, '数据库'),
|
||||
Redis: EnumValue.of(3, 'redis'),
|
||||
Mongo: EnumValue.of(4, 'mongo'),
|
||||
};
|
||||
@@ -15,7 +15,7 @@ const config = {
|
||||
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
||||
|
||||
// 系统版本
|
||||
version: 'v1.5.4',
|
||||
version: 'v1.6.0',
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -40,3 +40,7 @@ export const NextLoading = {
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
@@ -26,14 +26,14 @@
|
||||
>
|
||||
<!-- 这里只获取指定个数的筛选条件 -->
|
||||
<el-input
|
||||
v-model="queryForm[item.prop]"
|
||||
v-model="queryForm_[item.prop]"
|
||||
:placeholder="'输入' + item.label + '关键字'"
|
||||
clearable
|
||||
v-if="item.type == 'text'"
|
||||
></el-input>
|
||||
|
||||
<el-select-v2
|
||||
v-model="queryForm[item.prop]"
|
||||
v-model="queryForm_[item.prop]"
|
||||
:options="item.options"
|
||||
clearable
|
||||
:placeholder="'选择' + item.label + '关键字'"
|
||||
@@ -41,7 +41,7 @@
|
||||
/>
|
||||
|
||||
<el-date-picker
|
||||
v-model="queryForm[item.prop]"
|
||||
v-model="queryForm_[item.prop]"
|
||||
clearable
|
||||
type="datetimerange"
|
||||
format="YYYY-MM-DD hh:mm:ss"
|
||||
@@ -185,8 +185,10 @@
|
||||
import { toRefs, watch, reactive, onMounted } from 'vue';
|
||||
import { TableColumn, TableQuery } from './index';
|
||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
const emit = defineEmits(['update:queryForm', 'update:pageNum', 'update:pageSize', 'update:selectionData', 'pageChange'])
|
||||
const emit = defineEmits(['update:queryForm', 'update:pageNum', 'update:pageSize', 'update:selectionData', 'pageChange']);
|
||||
|
||||
const props = defineProps({
|
||||
size: {
|
||||
@@ -195,7 +197,7 @@ const props = defineProps({
|
||||
},
|
||||
inputWidth: {
|
||||
type: [Number, String],
|
||||
default: 0,
|
||||
default: '200px',
|
||||
},
|
||||
// 是否显示选择列
|
||||
showSelection: {
|
||||
@@ -204,7 +206,7 @@ const props = defineProps({
|
||||
},
|
||||
// 当前选择的数据
|
||||
selectionData: {
|
||||
type: Array<any>
|
||||
type: Array<any>,
|
||||
},
|
||||
// 列信息
|
||||
columns: {
|
||||
@@ -236,16 +238,18 @@ const props = defineProps({
|
||||
type: Array<TableQuery>,
|
||||
default: function () {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
},
|
||||
// 绑定的查询表单
|
||||
queryForm: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
|
||||
const state = reactive({
|
||||
pageSizes: [] as any, // 可选每页显示的数据量
|
||||
@@ -253,140 +257,153 @@ const state = reactive({
|
||||
pageNum: 1,
|
||||
isOpenMoreQuery: false,
|
||||
defaultQueryCount: 2, // 默认显示的查询参数个数,展开后每行显示查询条件个数为该值加1。第一行用最后一列来占用按钮
|
||||
queryForm: {} as any,
|
||||
queryForm_: {} as any,
|
||||
loadingData: false,
|
||||
// 输入框宽度
|
||||
inputWidth: "200px" as any,
|
||||
inputWidth_: '200px' as any,
|
||||
formatVal: '', // 格式化后的值
|
||||
tableMaxHeight: window.innerHeight - 240 + 'px',
|
||||
})
|
||||
});
|
||||
|
||||
const {
|
||||
pageSizes,
|
||||
isOpenMoreQuery,
|
||||
defaultQueryCount,
|
||||
queryForm,
|
||||
loadingData,
|
||||
inputWidth,
|
||||
formatVal,
|
||||
tableMaxHeight,
|
||||
} = toRefs(state)
|
||||
const { pageSizes, isOpenMoreQuery, defaultQueryCount, queryForm_, inputWidth_, formatVal, loadingData, tableMaxHeight } = toRefs(state);
|
||||
|
||||
watch(() => props.queryForm, (newValue: any) => {
|
||||
state.queryForm = newValue;
|
||||
})
|
||||
|
||||
watch(() => props.pageNum, (newValue: any) => {
|
||||
state.pageNum = newValue;
|
||||
})
|
||||
|
||||
watch(() => props.pageSize, (newValue: any) => {
|
||||
state.pageSize = newValue;
|
||||
})
|
||||
|
||||
watch(() => props.data, (newValue: any) => {
|
||||
if (newValue && newValue.length > 0) {
|
||||
props.columns.forEach(item => {
|
||||
if (item.autoWidth && item.show) {
|
||||
item.autoCalculateMinWidth(props.data);
|
||||
}
|
||||
})
|
||||
watch(
|
||||
() => props.queryForm,
|
||||
(newValue: any) => {
|
||||
state.queryForm_ = newValue;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.pageNum,
|
||||
(newValue: any) => {
|
||||
state.pageNum = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.pageSize,
|
||||
(newValue: any) => {
|
||||
state.pageSize = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(newValue: any) => {
|
||||
if (newValue && newValue.length > 0) {
|
||||
props.columns.forEach((item) => {
|
||||
if (item.autoWidth && item.show) {
|
||||
item.autoCalculateMinWidth(props.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
const pageSize = props.pageSize;
|
||||
let pageSize = props.pageSize;
|
||||
|
||||
// 如果pageSize设为0,则使用系统全局配置的pageSize
|
||||
if (!pageSize) {
|
||||
pageSize = themeConfig.value.defaultListPageSize;
|
||||
// 可能storage已经存在配置json,则可能没值,需要清storage重试
|
||||
if (!pageSize) {
|
||||
pageSize = 10;
|
||||
}
|
||||
emit('update:pageSize', pageSize);
|
||||
}
|
||||
|
||||
state.pageNum = props.pageNum;
|
||||
state.pageSize = pageSize;
|
||||
state.queryForm = props.queryForm;
|
||||
state.queryForm_ = props.queryForm;
|
||||
state.pageSizes = [pageSize, pageSize * 2, pageSize * 3, pageSize * 4, pageSize * 5];
|
||||
|
||||
// 如果没传输入框宽度,则根据组件size设置默认宽度
|
||||
if (!props.inputWidth) {
|
||||
state.inputWidth = props.size == 'small' ? '150px' : '200px';
|
||||
state.inputWidth_ = props.size == 'small' ? '150px' : '200px';
|
||||
} else {
|
||||
state.inputWidth = props.inputWidth;
|
||||
state.inputWidth_ = props.inputWidth;
|
||||
}
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
calcuTableHeight();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
const calcuTableHeight = () => {
|
||||
state.tableMaxHeight = window.innerHeight - 240 + 'px';
|
||||
}
|
||||
};
|
||||
|
||||
const formatText = (data: any)=> {
|
||||
const formatText = (data: any) => {
|
||||
state.formatVal = '';
|
||||
try {
|
||||
state.formatVal = JSON.stringify(JSON.parse(data), null, 4);
|
||||
} catch (e) {
|
||||
} catch (e) {
|
||||
state.formatVal = data;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getRowQueryItem = (row: number) => {
|
||||
// 第一行需要加个查询等按钮列
|
||||
if (row === 1) {
|
||||
const res = props.query.slice(row - 1, defaultQueryCount.value);
|
||||
// 查询等按钮列
|
||||
res.push(TableQuery.slot("", "", "queryBtns"));
|
||||
return res
|
||||
res.push(TableQuery.slot('', '', 'queryBtns'));
|
||||
return res;
|
||||
}
|
||||
const columnCount = defaultQueryCount.value + 1;
|
||||
return props.query.slice((row - 1) * columnCount - 1, row * columnCount - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectionChange = (val: any) => {
|
||||
emit('update:selectionData', val);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePageChange = () => {
|
||||
emit('update:pageNum', state.pageNum);
|
||||
execQuery();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSizeChange = () => {
|
||||
changePageNum(1);
|
||||
emit('update:pageSize', state.pageSize);
|
||||
execQuery();
|
||||
}
|
||||
};
|
||||
|
||||
const queryData = () => {
|
||||
changePageNum(1);
|
||||
execQuery();
|
||||
}
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
// 将查询参数绑定的值置空,并重新粗发查询接口
|
||||
for (let qi of props.query) {
|
||||
state.queryForm[qi.prop] = null;
|
||||
state.queryForm_[qi.prop] = null;
|
||||
}
|
||||
|
||||
changePageNum(1);
|
||||
emit('update:queryForm', state.queryForm);
|
||||
emit('update:queryForm', state.queryForm_);
|
||||
execQuery();
|
||||
}
|
||||
};
|
||||
|
||||
const changePageNum = (pageNum: number) => {
|
||||
state.pageNum = pageNum;
|
||||
emit('update:pageNum', state.pageNum);
|
||||
}
|
||||
};
|
||||
|
||||
const execQuery = () => {
|
||||
emit('pageChange');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 是否正在加载数据
|
||||
*/
|
||||
const loading = (loading: boolean) => {
|
||||
state.loadingData = loading;
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ loading })
|
||||
defineExpose({ loading });
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.page-table {
|
||||
@@ -420,15 +437,15 @@ defineExpose({ loading })
|
||||
}
|
||||
|
||||
.el-select-v2 {
|
||||
width: v-bind(inputWidth);
|
||||
width: v-bind(inputWidth_);
|
||||
}
|
||||
|
||||
.el-input {
|
||||
width: v-bind(inputWidth);
|
||||
width: v-bind(inputWidth_);
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width: v-bind(inputWidth);
|
||||
width: v-bind(inputWidth_);
|
||||
}
|
||||
|
||||
.el-date-editor {
|
||||
|
||||
@@ -61,6 +61,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 全局设置 -->
|
||||
<el-divider content-position="left">全局设置</el-divider>
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt15">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">分页size</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||
<el-input-number
|
||||
v-model="themeConfig.defaultListPageSize"
|
||||
controls-position="right"
|
||||
:min="10"
|
||||
:max="50"
|
||||
@change="setLocalThemeConfig"
|
||||
size="small"
|
||||
style="width: 90px"
|
||||
>
|
||||
</el-input-number>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 全局主题 -->
|
||||
<el-divider content-position="left">全局主题</el-divider>
|
||||
<div class="layout-breadcrumb-seting-bar-flex">
|
||||
|
||||
@@ -135,6 +135,10 @@ export const useThemeConfig = defineStore('themeConfig', {
|
||||
globalI18n: 'zh-cn',
|
||||
// 默认全局组件大小,可选值"<|large|default|small>",默认 ''
|
||||
globalComponentSize: '',
|
||||
|
||||
/** 全局设置 */
|
||||
// 默认列表页的分页大小
|
||||
defaultListPageSize: 15,
|
||||
},
|
||||
}),
|
||||
actions: {
|
||||
|
||||
2
mayfly_go_web/src/types/pinia.d.ts
vendored
2
mayfly_go_web/src/types/pinia.d.ts
vendored
@@ -57,6 +57,8 @@ declare interface ThemeConfigState {
|
||||
terminalFontSize: number;
|
||||
terminalFontWeight: string | any;
|
||||
editorTheme: string;
|
||||
|
||||
defaultListPageSize: number;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ import { nextTick, onMounted, ref, toRefs, reactive, computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { initRouter } from '@/router/index';
|
||||
import { saveToken, saveUser, setSession } from '@/common/utils/storage';
|
||||
import { saveToken, saveUser } from '@/common/utils/storage';
|
||||
import { formatAxis } from '@/common/utils/format';
|
||||
import openApi from '@/common/openApi';
|
||||
import { RsaEncrypt } from '@/common/rsa';
|
||||
|
||||
45
mayfly_go_web/src/views/ops/component/ResourceTag.vue
Normal file
45
mayfly_go_web/src/views/ops/component/ResourceTag.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div style="display: inline-flex; justify-content: center; align-items: center; cursor: pointer; vertical-align: middle">
|
||||
<el-popover :show-after="500" @show="getTags" placement="top-start" width="230" trigger="hover">
|
||||
<template #reference>
|
||||
<div>
|
||||
<!-- <el-button type="primary" link size="small">标签</el-button> -->
|
||||
<SvgIcon name="view" :size="16" color="var(--el-color-primary)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-tag effect="plain" v-for="tag in tags" :key="tag" class="ml5" type="success" size="small">{{ tag.tagPath }}</el-tag>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, toRefs } from 'vue';
|
||||
import { tagApi } from '../tag/api';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
const props = defineProps({
|
||||
resourceCode: {
|
||||
type: [String],
|
||||
required: true,
|
||||
},
|
||||
resourceType: {
|
||||
type: [Number],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
tags: [] as any,
|
||||
});
|
||||
|
||||
const { tags } = toRefs(state);
|
||||
|
||||
const getTags = async () => {
|
||||
state.tags = await tagApi.getTagResources.request({
|
||||
resourceCode: props.resourceCode,
|
||||
resourceType: props.resourceType,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -43,17 +43,22 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, ref, watch, toRefs, onUnmounted } from 'vue';
|
||||
import { TagTreeNode } from './tag';
|
||||
import { NodeType, TagTreeNode } from './tag';
|
||||
import TagInfo from './TagInfo.vue';
|
||||
import { Contextmenu } from '@/components/contextmenu';
|
||||
import { useViewport } from '@/common/use';
|
||||
import { tagApi } from '../tag/api';
|
||||
|
||||
const props = defineProps({
|
||||
load: {
|
||||
type: Function,
|
||||
required: false,
|
||||
resourceType: {
|
||||
type: [Number],
|
||||
required: true,
|
||||
},
|
||||
loadTags: {
|
||||
tagPathNodeType: {
|
||||
type: [NodeType],
|
||||
required: true,
|
||||
},
|
||||
load: {
|
||||
type: Function,
|
||||
required: false,
|
||||
},
|
||||
@@ -109,6 +114,18 @@ const filterNode = (value: string, data: any) => {
|
||||
return data.label.includes(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载标签树节点
|
||||
*/
|
||||
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
|
||||
@@ -120,8 +137,8 @@ const loadNode = async (node: any, resolve: any) => {
|
||||
}
|
||||
let nodes = [];
|
||||
try {
|
||||
if (node.level == 0 && props.loadTags) {
|
||||
nodes = await props.loadTags(node);
|
||||
if (node.level == 0) {
|
||||
nodes = await loadTags();
|
||||
} else if (props.load) {
|
||||
nodes = await props.load(node);
|
||||
} else {
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
<div>
|
||||
<el-tree-select
|
||||
v-bind="$attrs"
|
||||
@check="changeTag"
|
||||
v-model="selectTags"
|
||||
@change="changeTag"
|
||||
style="width: 100%"
|
||||
:data="tags"
|
||||
placeholder="请选择关联标签"
|
||||
:render-after-expand="true"
|
||||
:default-expanded-keys="[selectTags]"
|
||||
show-checkbox
|
||||
check-strictly
|
||||
node-key="id"
|
||||
:props="{
|
||||
value: 'id',
|
||||
@@ -33,35 +33,46 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useAttrs, toRefs, reactive, onMounted } from 'vue';
|
||||
import { toRefs, reactive, onMounted } from 'vue';
|
||||
import { tagApi } from '../tag/api';
|
||||
|
||||
const attrs = useAttrs();
|
||||
//定义事件
|
||||
const emit = defineEmits(['changeTag', 'update:tagPath']);
|
||||
const emit = defineEmits(['update:modelValue', 'changeTag', 'input']);
|
||||
|
||||
const props = defineProps({
|
||||
resourceCode: {
|
||||
type: [String],
|
||||
required: true,
|
||||
},
|
||||
resourceType: {
|
||||
type: [Number],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
tags: [],
|
||||
// 单选则为id,多选为id数组
|
||||
selectTags: null as any,
|
||||
selectTags: [],
|
||||
});
|
||||
|
||||
const { tags, selectTags } = toRefs(state);
|
||||
|
||||
onMounted(async () => {
|
||||
if (attrs.modelValue) {
|
||||
state.selectTags = attrs.modelValue;
|
||||
if (props.resourceCode) {
|
||||
const resourceTags = await tagApi.getTagResources.request({
|
||||
resourceCode: props.resourceCode,
|
||||
resourceType: props.resourceType,
|
||||
});
|
||||
state.selectTags = resourceTags.map((x: any) => x.tagId);
|
||||
changeTag();
|
||||
}
|
||||
|
||||
state.tags = await tagApi.getTagTrees.request(null);
|
||||
});
|
||||
|
||||
const changeTag = (tag: any, checkInfo: any) => {
|
||||
if (checkInfo.checkedNodes.length > 0) {
|
||||
emit('update:tagPath', tag.codePath);
|
||||
emit('changeTag', tag);
|
||||
} else {
|
||||
emit('update:tagPath', null);
|
||||
}
|
||||
const changeTag = () => {
|
||||
emit('changeTag', state.selectTags);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -10,8 +10,19 @@
|
||||
width="38%"
|
||||
>
|
||||
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
|
||||
<el-form-item prop="tagId" label="标签" required>
|
||||
<tag-select v-model="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
|
||||
<el-form-item ref="tagSelectRef" prop="tagId" label="标签" required>
|
||||
<tag-tree-select
|
||||
@change-tag="
|
||||
(tagIds) => {
|
||||
form.tagId = tagIds;
|
||||
tagSelectRef.validate();
|
||||
}
|
||||
"
|
||||
multiple
|
||||
:resource-code="form.code"
|
||||
:resource-type="TagResourceTypeEnum.Db.value"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="instanceId" label="数据库实例" required>
|
||||
@@ -77,7 +88,8 @@
|
||||
import { toRefs, reactive, watch, ref } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import TagSelect from '../component/TagSelect.vue';
|
||||
import TagTreeSelect from '../component/TagTreeSelect.vue';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@@ -128,6 +140,7 @@ const rules = {
|
||||
};
|
||||
|
||||
const dbForm: any = ref(null);
|
||||
const tagSelectRef: any = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
@@ -135,9 +148,9 @@ const state = reactive({
|
||||
databaseList: [] as any,
|
||||
form: {
|
||||
id: null,
|
||||
tagId: null as any,
|
||||
tagPath: null as any,
|
||||
tagId: [],
|
||||
name: null,
|
||||
code: '',
|
||||
database: '',
|
||||
remark: '',
|
||||
instanceId: null as any,
|
||||
@@ -148,13 +161,14 @@ const state = reactive({
|
||||
|
||||
const { dialogVisible, allDatabases, databaseList, form, btnLoading } = toRefs(state);
|
||||
|
||||
watch(props, (newValue: any) => {
|
||||
watch(props, async (newValue: any) => {
|
||||
state.dialogVisible = newValue.visible;
|
||||
if (!state.dialogVisible) {
|
||||
return;
|
||||
}
|
||||
if (newValue.db) {
|
||||
state.form = { ...newValue.db };
|
||||
|
||||
// 将数据库名使用空格切割,获取所有数据库列表
|
||||
state.databaseList = newValue.db.database.split(' ');
|
||||
} else {
|
||||
|
||||
@@ -46,10 +46,7 @@
|
||||
</template>
|
||||
|
||||
<template #tagPath="{ data }">
|
||||
<tag-info :tag-path="data.tagPath" />
|
||||
<span class="ml5">
|
||||
{{ data.tagPath }}
|
||||
</span>
|
||||
<resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Db.value" />
|
||||
</template>
|
||||
|
||||
<template #host="{ data }">
|
||||
@@ -167,12 +164,15 @@ import config from '@/common/config';
|
||||
import { joinClientParams } from '@/common/request';
|
||||
import { isTrue } from '@/common/assert';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import TagInfo from '../component/TagInfo.vue';
|
||||
import ResourceTag from '../component/ResourceTag.vue';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn, TableQuery } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import DbSqlExecLog from './DbSqlExecLog.vue';
|
||||
import { DbType } from './dialect';
|
||||
import { tagApi } from '../tag/api';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
|
||||
|
||||
@@ -185,12 +185,12 @@ const perms = {
|
||||
const queryConfig = [TableQuery.slot('tagPath', '标签', 'tagPathSelect'), TableQuery.slot('instanceId', '实例', 'instanceSelect')];
|
||||
|
||||
const columns = ref([
|
||||
TableColumn.new('tagPath', '标签路径').isSlot().setAddWidth(20),
|
||||
TableColumn.new('instanceName', '实例名'),
|
||||
TableColumn.new('type', '类型'),
|
||||
TableColumn.new('host', 'ip:port').isSlot().setAddWidth(40),
|
||||
TableColumn.new('username', 'username'),
|
||||
TableColumn.new('name', '名称'),
|
||||
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(10).alignCenter(),
|
||||
TableColumn.new('remark', '备注'),
|
||||
]);
|
||||
|
||||
@@ -198,6 +198,7 @@ const columns = ref([
|
||||
const actionBtns = hasPerms([perms.base, perms.saveDb]);
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter();
|
||||
|
||||
const route = useRoute();
|
||||
const pageTableRef: any = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
@@ -214,10 +215,10 @@ const state = reactive({
|
||||
* 查询条件
|
||||
*/
|
||||
query: {
|
||||
tagPath: null,
|
||||
tagPath: '',
|
||||
instanceId: null,
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 0,
|
||||
},
|
||||
datas: [],
|
||||
total: 0,
|
||||
@@ -269,6 +270,10 @@ onMounted(async () => {
|
||||
const search = async () => {
|
||||
try {
|
||||
pageTableRef.value.loading(true);
|
||||
|
||||
if (route.query.tagPath) {
|
||||
state.query.tagPath = route.query.tagPath as string;
|
||||
}
|
||||
let res: any = await dbApi.dbs.request(state.query);
|
||||
// 切割数据库
|
||||
res.list?.forEach((e: any) => {
|
||||
@@ -297,7 +302,7 @@ const onBeforeCloseInfoDialog = () => {
|
||||
};
|
||||
|
||||
const getTags = async () => {
|
||||
state.tags = await dbApi.dbTags.request(null);
|
||||
state.tags = await tagApi.getResourceTagPaths.request({ resourceType: TagResourceTypeEnum.Db.value });
|
||||
};
|
||||
|
||||
const getInstances = async (instanceName = '') => {
|
||||
|
||||
@@ -105,7 +105,7 @@ const state = reactive({
|
||||
query: {
|
||||
name: null,
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 0,
|
||||
},
|
||||
datas: [],
|
||||
total: 0,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="db-sql-exec">
|
||||
<el-row>
|
||||
<el-col :span="5">
|
||||
<tag-tree ref="tagTreeRef" :loadTags="loadTags">
|
||||
<tag-tree :resource-type="TagResourceTypeEnum.Db.value" :tag-path-node-type="NodeTypeTagPath" ref="tagTreeRef">
|
||||
<template #prefix="{ data }">
|
||||
<span v-if="data.type.value == SqlExecNodeType.DbInst">
|
||||
<el-popover :show-after="500" placement="right-start" title="数据库实例信息" trigger="hover" :width="250">
|
||||
@@ -160,6 +160,8 @@ import { dispposeCompletionItemProvider } from '@/components/monaco/completionIt
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { ContextmenuItem } from '@/components/contextmenu';
|
||||
import { getDbDialect } from './dialect/index';
|
||||
import { sleep } from '@/common/utils/loading';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
|
||||
const DbTableDataOp = defineAsyncComponent(() => import('./component/table/DbTableDataOp.vue'));
|
||||
@@ -213,11 +215,16 @@ const nodeClickChangeDb = (nodeData: TagTreeNode) => {
|
||||
|
||||
// tagpath 节点类型
|
||||
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const dbInfos = instMap.get(parentNode.key);
|
||||
const dbInfoRes = await dbApi.dbs.request({ tagPath: parentNode.key });
|
||||
const dbInfos = dbInfoRes.list;
|
||||
if (!dbInfos) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 防止过快加载会出现一闪而过,对眼睛不好
|
||||
await sleep(100);
|
||||
return dbInfos?.map((x: any) => {
|
||||
x.tagPath = parentNode.key;
|
||||
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x);
|
||||
});
|
||||
});
|
||||
@@ -396,35 +403,6 @@ const setHeight = () => {
|
||||
state.tablesOpHeight = window.innerHeight - 220 + 'px';
|
||||
};
|
||||
|
||||
/**
|
||||
* instmap; tagPaht -> info[]
|
||||
*/
|
||||
const instMap: Map<string, any[]> = new Map();
|
||||
|
||||
const getInsts = async () => {
|
||||
const res = await dbApi.dbs.request({ pageNum: 1, pageSize: 1000 });
|
||||
if (!res.total) return;
|
||||
for (const db of res.list) {
|
||||
const tagPath = db.tagPath;
|
||||
let dbInsts = instMap.get(tagPath) || [];
|
||||
dbInsts.push(db);
|
||||
instMap.set(tagPath, dbInsts?.sort());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载标签树节点
|
||||
*/
|
||||
const loadTags = async () => {
|
||||
await getInsts();
|
||||
const tagPaths = instMap.keys();
|
||||
const tagNodes = [];
|
||||
for (let tagPath of tagPaths) {
|
||||
tagNodes.push(new TagTreeNode(tagPath, tagPath, NodeTypeTagPath));
|
||||
}
|
||||
return tagNodes;
|
||||
};
|
||||
|
||||
// 选择数据库,改变当前正在操作的数据库信息
|
||||
const changeDb = (db: any, dbName: string) => {
|
||||
state.nowDbInst = DbInst.getOrNewInst(db);
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
:ref="(el: any) => el?.focus()"
|
||||
@blur="onExitEditMode(rowData, column, rowIndex)"
|
||||
class="w100"
|
||||
input-style="text-align: center; height: 27px;"
|
||||
input-style="text-align: center; height: 26px;"
|
||||
size="small"
|
||||
v-model="rowData[column.dataKey!]"
|
||||
></el-input>
|
||||
@@ -106,6 +106,11 @@
|
||||
</el-auto-resizer>
|
||||
|
||||
<el-dialog @close="state.genTxtDialog.visible = false" v-model="state.genTxtDialog.visible" :title="state.genTxtDialog.title" width="1000px">
|
||||
<template #header>
|
||||
<div class="mr15" style="display: flex; justify-content: flex-end">
|
||||
<el-button id="copyValue" @click="copyGenTxt(state.genTxtDialog.txt)" icon="CopyDocument" type="success" size="small">一键复制</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-input v-model="state.genTxtDialog.txt" type="textarea" rows="20" />
|
||||
</el-dialog>
|
||||
|
||||
@@ -530,6 +535,11 @@ const onGenerateJson = async () => {
|
||||
state.genTxtDialog.visible = true;
|
||||
};
|
||||
|
||||
const copyGenTxt = async (txt: string) => {
|
||||
await copyToClipboard(txt);
|
||||
state.genTxtDialog.visible = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 导出当前页数据
|
||||
*/
|
||||
|
||||
@@ -311,8 +311,6 @@ const handlerWindowClick = () => {
|
||||
};
|
||||
|
||||
const onRefresh = async () => {
|
||||
// 查询条件置空
|
||||
state.condition = '';
|
||||
state.pageNum = 1;
|
||||
await selectData();
|
||||
};
|
||||
|
||||
@@ -4,8 +4,19 @@
|
||||
<el-form :model="form" ref="machineForm" :rules="rules" label-width="auto">
|
||||
<el-tabs v-model="tabActiveName">
|
||||
<el-tab-pane label="基础信息" name="basic">
|
||||
<el-form-item prop="tagId" label="标签">
|
||||
<tag-select v-model="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
|
||||
<el-form-item ref="tagSelectRef" prop="tagId" label="标签">
|
||||
<tag-tree-select
|
||||
multiple
|
||||
@change-tag="
|
||||
(tagIds) => {
|
||||
form.tagId = tagIds;
|
||||
tagSelectRef.validate();
|
||||
}
|
||||
"
|
||||
:resource-code="form.code"
|
||||
:resource-type="TagResourceTypeEnum.Machine.value"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="name" label="名称" required>
|
||||
<el-input v-model.trim="form.name" placeholder="请输入机器别名" auto-complete="off"></el-input>
|
||||
@@ -71,9 +82,10 @@
|
||||
import { toRefs, reactive, watch, ref } from 'vue';
|
||||
import { machineApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import TagSelect from '../component/TagSelect.vue';
|
||||
import TagTreeSelect from '../component/TagTreeSelect.vue';
|
||||
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
|
||||
import AuthCertSelect from './authcert/AuthCertSelect.vue';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@@ -95,7 +107,7 @@ const rules = {
|
||||
{
|
||||
required: true,
|
||||
message: '请选择标签',
|
||||
trigger: ['blur', 'change'],
|
||||
trigger: ['change'],
|
||||
},
|
||||
],
|
||||
name: [
|
||||
@@ -126,17 +138,11 @@ const rules = {
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入授权密码',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const machineForm: any = ref(null);
|
||||
const authCertSelectRef: any = ref(null);
|
||||
const tagSelectRef: any = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
@@ -146,14 +152,14 @@ const state = reactive({
|
||||
authType: 1,
|
||||
form: {
|
||||
id: null,
|
||||
code: '',
|
||||
ip: null,
|
||||
port: 22,
|
||||
name: null,
|
||||
authCertId: null as any,
|
||||
username: '',
|
||||
password: '',
|
||||
tagId: null as any,
|
||||
tagPath: null as any,
|
||||
tagId: [],
|
||||
remark: '',
|
||||
sshTunnelMachineId: null as any,
|
||||
enableRecorder: -1,
|
||||
@@ -173,6 +179,7 @@ watch(props, async (newValue: any) => {
|
||||
state.tabActiveName = 'basic';
|
||||
if (newValue.machine) {
|
||||
state.form = { ...newValue.machine };
|
||||
|
||||
// 如果凭证类型为公共的,则表示使用授权凭证认证
|
||||
const authCertId = (state.form as any).authCertId;
|
||||
if (authCertId > 0) {
|
||||
@@ -181,7 +188,7 @@ watch(props, async (newValue: any) => {
|
||||
state.authType = 1;
|
||||
}
|
||||
} else {
|
||||
state.form = { port: 22 } as any;
|
||||
state.form = { port: 22, tagId: [] } as any;
|
||||
state.authType = 1;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -24,13 +24,6 @@
|
||||
<el-button v-auth="perms.delMachine" :disabled="selectionData.length < 1" @click="deleteMachine()" type="danger" icon="delete">删除</el-button>
|
||||
</template>
|
||||
|
||||
<template #tagPath="{ data }">
|
||||
<tag-info :tag-path="data.tagPath" />
|
||||
<span class="ml5">
|
||||
{{ data.tagPath }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #ipPort="{ data }">
|
||||
<el-link :disabled="data.status == -1" @click="showMachineStats(data)" type="primary" :underline="false">
|
||||
{{ `${data.ip}:${data.port}` }}
|
||||
@@ -82,6 +75,10 @@
|
||||
></el-switch>
|
||||
</template>
|
||||
|
||||
<template #tagPath="{ data }">
|
||||
<resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Machine.value" />
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<span v-auth="'machine:terminal'">
|
||||
<el-tooltip :show-after="500" content="按住ctrl则为新标签打开" placement="top">
|
||||
@@ -190,15 +187,17 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRefs, reactive, onMounted, defineAsyncComponent } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { machineApi, getMachineTerminalSocketUrl } from './api';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import TagInfo from '../component/TagInfo.vue';
|
||||
import ResourceTag from '../component/ResourceTag.vue';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn, TableQuery } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { formatByteSize } from '@/common/utils/format';
|
||||
import { tagApi } from '../tag/api';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
// 组件
|
||||
const TerminalDialog = defineAsyncComponent(() => import('@/components/terminal/TerminalDialog.vue'));
|
||||
@@ -210,6 +209,7 @@ const MachineRec = defineAsyncComponent(() => import('./MachineRec.vue'));
|
||||
const ProcessList = defineAsyncComponent(() => import('./ProcessList.vue'));
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const pageTableRef: any = ref(null);
|
||||
const terminalDialogRef: any = ref(null);
|
||||
|
||||
@@ -224,13 +224,13 @@ const perms = {
|
||||
const queryConfig = [TableQuery.slot('tagPath', '标签', 'tagPathSelect'), TableQuery.text('ip', 'IP'), TableQuery.text('name', '名称')];
|
||||
|
||||
const columns = ref([
|
||||
TableColumn.new('tagPath', '标签路径').isSlot().setAddWidth(20),
|
||||
TableColumn.new('name', '名称'),
|
||||
TableColumn.new('ipPort', 'ip:port').isSlot().setAddWidth(50),
|
||||
TableColumn.new('stat', '运行状态').isSlot().setAddWidth(50),
|
||||
TableColumn.new('fs', '磁盘(挂载点=>可用/总)').isSlot().setAddWidth(20),
|
||||
TableColumn.new('username', '用户名'),
|
||||
TableColumn.new('status', '状态').isSlot().setMinWidth(85),
|
||||
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(10).alignCenter(),
|
||||
TableColumn.new('remark', '备注'),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(238).fixedRight().alignCenter(),
|
||||
]);
|
||||
@@ -242,10 +242,10 @@ const state = reactive({
|
||||
tags: [] as any,
|
||||
params: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 0,
|
||||
ip: null,
|
||||
name: null,
|
||||
tagPath: null,
|
||||
tagPath: '',
|
||||
},
|
||||
// 列表数据
|
||||
data: {
|
||||
@@ -360,7 +360,7 @@ const closeCli = async (row: any) => {
|
||||
};
|
||||
|
||||
const getTags = async () => {
|
||||
state.tags = await machineApi.tagList.request(null);
|
||||
state.tags = await tagApi.getResourceTagPaths.request({ resourceType: TagResourceTypeEnum.Machine.value });
|
||||
};
|
||||
|
||||
const openFormDialog = async (machine: any) => {
|
||||
@@ -434,6 +434,9 @@ const showFileManage = (selectionData: any) => {
|
||||
const search = async () => {
|
||||
try {
|
||||
pageTableRef.value.loading(true);
|
||||
if (route.query.tagPath) {
|
||||
state.params.tagPath = route.query.tagPath as string;
|
||||
}
|
||||
const res = await machineApi.list.request(state.params);
|
||||
state.data = res;
|
||||
} finally {
|
||||
|
||||
@@ -7,31 +7,44 @@
|
||||
:before-close="handleClose"
|
||||
:close-on-click-modal="false"
|
||||
:destroy-on-close="true"
|
||||
width="800"
|
||||
>
|
||||
<page-table
|
||||
height="100%"
|
||||
v-model:query-form="query"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:total="total"
|
||||
v-model:page-size="query.pageSize"
|
||||
v-model:page-num="query.pageNum"
|
||||
@pageChange="getTermOps()"
|
||||
>
|
||||
<template #action="{ data }">
|
||||
<el-button @click="playRec(data)" loading-icon="loading" :loading="data.playRecLoding" type="primary" link>回放</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
:title="title"
|
||||
v-model="playerDialogVisible"
|
||||
:before-close="handleClosePlayer"
|
||||
:close-on-click-modal="false"
|
||||
:destroy-on-close="true"
|
||||
width="70%"
|
||||
>
|
||||
<div class="toolbar">
|
||||
<el-select @change="getUsers" v-model="operateDate" placeholder="操作日期" filterable>
|
||||
<el-option v-for="item in operateDates" :key="item" :label="item" :value="item"> </el-option>
|
||||
</el-select>
|
||||
<el-select class="ml10" @change="getRecs" filterable v-model="user" placeholder="请选择操作人">
|
||||
<el-option v-for="item in users" :key="item" :label="item" :value="item"> </el-option>
|
||||
</el-select>
|
||||
<el-select class="ml10" @change="playRec" filterable v-model="rec" placeholder="请选择操作记录">
|
||||
<el-option v-for="item in recs" :key="item" :label="item" :value="item"> </el-option>
|
||||
</el-select>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
快捷键-> space[空格键]: 暂停/播放
|
||||
</div>
|
||||
<div ref="playerRef" id="rc-player"></div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, watch, ref, reactive } from 'vue';
|
||||
import { toRefs, watch, ref, reactive, nextTick } from 'vue';
|
||||
import { machineApi } from './api';
|
||||
import * as AsciinemaPlayer from 'asciinema-player';
|
||||
import 'asciinema-player/dist/bundle/asciinema-player.css';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean },
|
||||
@@ -41,67 +54,75 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['update:visible', 'cancel', 'update:machineId']);
|
||||
|
||||
const columns = [
|
||||
TableColumn.new('creator', '操作者').setMinWidth(120),
|
||||
TableColumn.new('createTime', '开始时间').isTime().setMinWidth(150),
|
||||
TableColumn.new('endTime', '结束时间').isTime().setMinWidth(150),
|
||||
TableColumn.new('recordFilePath', '文件路径').setMinWidth(200),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(60).fixedRight().alignCenter(),
|
||||
];
|
||||
|
||||
const playerRef = ref(null);
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
title: '',
|
||||
machineId: 0,
|
||||
operateDates: [],
|
||||
users: [],
|
||||
recs: [],
|
||||
operateDate: '',
|
||||
user: '',
|
||||
rec: '',
|
||||
data: [],
|
||||
total: 0,
|
||||
query: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
|
||||
playerDialogVisible: false,
|
||||
});
|
||||
|
||||
const { dialogVisible, title, operateDates, operateDate, users, recs, user, rec } = toRefs(state);
|
||||
const { dialogVisible, query, data, total, playerDialogVisible } = toRefs(state);
|
||||
|
||||
watch(props, async (newValue: any) => {
|
||||
const visible = newValue.visible;
|
||||
if (visible) {
|
||||
state.machineId = newValue.machineId;
|
||||
state.title = newValue.title;
|
||||
await getOperateDate();
|
||||
await getTermOps();
|
||||
}
|
||||
state.dialogVisible = visible;
|
||||
});
|
||||
|
||||
const getOperateDate = async () => {
|
||||
const res = await machineApi.recDirNames.request({ path: state.machineId });
|
||||
state.operateDates = res as any;
|
||||
};
|
||||
|
||||
const getUsers = async (operateDate: string) => {
|
||||
state.users = [];
|
||||
state.user = '';
|
||||
state.recs = [];
|
||||
state.rec = '';
|
||||
const res = await machineApi.recDirNames.request({ path: `${state.machineId}/${operateDate}` });
|
||||
state.users = res as any;
|
||||
};
|
||||
|
||||
const getRecs = async (user: string) => {
|
||||
state.recs = [];
|
||||
state.rec = '';
|
||||
const res = await machineApi.recDirNames.request({ path: `${state.machineId}/${state.operateDate}/${user}` });
|
||||
state.recs = res as any;
|
||||
const getTermOps = async () => {
|
||||
const res = await machineApi.termOpRecs.request({ id: state.machineId, ...state.query });
|
||||
state.data = res.list;
|
||||
state.total = res.total;
|
||||
};
|
||||
|
||||
let player: any = null;
|
||||
|
||||
const playRec = async (rec: string) => {
|
||||
if (player) {
|
||||
player.dispose();
|
||||
const playRec = async (rec: any) => {
|
||||
try {
|
||||
if (player) {
|
||||
player.dispose();
|
||||
}
|
||||
rec.playRecLoding = true;
|
||||
const content = await machineApi.termOpRec.request({
|
||||
recId: rec.id,
|
||||
id: rec.machineId,
|
||||
});
|
||||
|
||||
state.playerDialogVisible = true;
|
||||
nextTick(() => {
|
||||
player = AsciinemaPlayer.create(`data:text/plain;base64,${content}`, playerRef.value, {
|
||||
autoPlay: true,
|
||||
speed: 1.0,
|
||||
idleTimeLimit: 2,
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
rec.playRecLoding = false;
|
||||
}
|
||||
const content = await machineApi.recDirNames.request({
|
||||
isFile: '1',
|
||||
path: `${state.machineId}/${state.operateDate}/${state.user}/${rec}`,
|
||||
});
|
||||
player = AsciinemaPlayer.create(`data:text/plain;base64,${content}`, playerRef.value, {
|
||||
autoPlay: true,
|
||||
speed: 1.0,
|
||||
idleTimeLimit: 2,
|
||||
});
|
||||
};
|
||||
|
||||
const handleClosePlayer = () => {
|
||||
state.playerDialogVisible = false;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -111,12 +132,8 @@ const handleClose = () => {
|
||||
emit('update:visible', false);
|
||||
emit('update:machineId', null);
|
||||
emit('cancel');
|
||||
state.operateDates = [];
|
||||
state.users = [];
|
||||
state.recs = [];
|
||||
state.operateDate = '';
|
||||
state.user = '';
|
||||
state.rec = '';
|
||||
state.data = [];
|
||||
state.total = 0;
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@@ -124,5 +141,15 @@ const handleClose = () => {
|
||||
.el-overlay .el-overlay-dialog .el-dialog .el-dialog__body {
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
#rc-player {
|
||||
.ap-terminal {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.ap-player {
|
||||
height: 550px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -43,7 +43,10 @@ export const machineApi = {
|
||||
// 删除配置的文件or目录
|
||||
delConf: Api.newDelete('/machines/{machineId}/files/{id}'),
|
||||
terminal: Api.newGet('/api/machines/{id}/terminal'),
|
||||
recDirNames: Api.newGet('/machines/rec/names'),
|
||||
// 机器终端操作记录列表
|
||||
termOpRecs: Api.newGet('/machines/{id}/term-recs'),
|
||||
// 机器终端操作记录详情
|
||||
termOpRec: Api.newGet('/machines/{id}/term-recs/{recId}'),
|
||||
};
|
||||
|
||||
export const authCertApi = {
|
||||
@@ -59,6 +62,7 @@ export const cronJobApi = {
|
||||
relateCronJobIds: Api.newGet('/machine-cronjobs/cronjob-ids'),
|
||||
save: Api.newPost('/machine-cronjobs'),
|
||||
delete: Api.newDelete('/machine-cronjobs/{id}'),
|
||||
run: Api.newPost('/machine-cronjobs/run/{key}'),
|
||||
execList: Api.newGet('/machine-cronjobs/execs'),
|
||||
};
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ import { AuthMethodEnum } from '../enums';
|
||||
const state = reactive({
|
||||
query: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 0,
|
||||
name: null,
|
||||
},
|
||||
queryConfig: [TableQuery.text('name', '凭证名称')],
|
||||
@@ -105,7 +105,9 @@ const deleteAc = async (data: any) => {
|
||||
await authCertApi.delete.request({ id: data.map((x: any) => x.id).join(',') });
|
||||
ElMessage.success('删除成功');
|
||||
search();
|
||||
} catch (err) {}
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
ref="pageTableRef"
|
||||
:query="queryConfig"
|
||||
v-model:query-form="params"
|
||||
:data="data.list"
|
||||
:data="state.data.list"
|
||||
:columns="columns"
|
||||
:total="data.total"
|
||||
:total="state.data.total"
|
||||
v-model:page-size="params.pageSize"
|
||||
v-model:page-num="params.pageNum"
|
||||
@pageChange="search()"
|
||||
@@ -88,7 +88,7 @@ const state = reactive({
|
||||
|
||||
const machineMap: Map<number, any> = new Map();
|
||||
|
||||
const { dialogVisible, params, data } = toRefs(state);
|
||||
const { dialogVisible, params } = toRefs(state);
|
||||
|
||||
watch(props, async (newValue: any) => {
|
||||
state.dialogVisible = newValue.visible;
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button :disabled="data.status == CronJobStatusEnum.Disable.value" v-auth="perms.saveCronJob" type="primary" @click="runCronJob(data)" link
|
||||
>执行</el-button
|
||||
>
|
||||
<el-button v-auth="perms.saveCronJob" type="primary" @click="openFormDialog(data)" link>编辑</el-button>
|
||||
<el-button type="primary" @click="showExec(data)" link>执行记录</el-button>
|
||||
</template>
|
||||
@@ -69,7 +72,7 @@ const columns = ref([
|
||||
const state = reactive({
|
||||
params: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 0,
|
||||
ip: null,
|
||||
name: null,
|
||||
},
|
||||
@@ -111,6 +114,11 @@ const openFormDialog = async (data: any) => {
|
||||
state.cronJobEdit.visible = true;
|
||||
};
|
||||
|
||||
const runCronJob = async (data: any) => {
|
||||
await cronJobApi.run.request({ key: data.key });
|
||||
ElMessage.success('执行成功');
|
||||
};
|
||||
|
||||
const deleteCronJob = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除【${state.selectionData.map((x: any) => x.name).join(', ')}】计划任务信息? 该操作将同时删除执行记录`, '提示', {
|
||||
@@ -121,7 +129,9 @@ const deleteCronJob = async () => {
|
||||
await cronJobApi.delete.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
ElMessage.success('操作成功');
|
||||
search();
|
||||
} catch (err) {}
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<el-row>
|
||||
<el-col :span="5">
|
||||
<tag-tree :loadTags="loadTags">
|
||||
<tag-tree :resource-type="TagResourceTypeEnum.Mongo.value" :tag-path-node-type="NodeTypeTagPath">
|
||||
<template #prefix="{ data }">
|
||||
<span v-if="data.type.value == MongoNodeType.Mongo">
|
||||
<el-popover :show-after="500" placement="right-start" title="mongo实例信息" trigger="hover" :width="250">
|
||||
@@ -172,6 +172,8 @@ import { isTrue, notBlank } from '@/common/assert';
|
||||
import { TagTreeNode, NodeType } from '../component/tag';
|
||||
import TagTree from '../component/TagTree.vue';
|
||||
import { formatByteSize } from '@/common/utils/format';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { sleep } from '@/common/utils/loading';
|
||||
|
||||
const MonacoEditor = defineAsyncComponent(() => import('@/components/monaco/MonacoEditor.vue'));
|
||||
|
||||
@@ -192,12 +194,15 @@ class MongoNodeType {
|
||||
|
||||
// tagpath 节点类型
|
||||
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
// 点击标签 -> 显示mongo信息列表
|
||||
const mongoInfos = instMap.get(parentNode.key);
|
||||
if (!mongoInfos) {
|
||||
const res = await mongoApi.mongoList.request({ tagPath: parentNode.key });
|
||||
if (!res.total) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const mongoInfos = res.list;
|
||||
await sleep(100);
|
||||
return mongoInfos?.map((x: any) => {
|
||||
x.tagPath = parentNode.key;
|
||||
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeMongo).withParams(x);
|
||||
});
|
||||
});
|
||||
@@ -278,35 +283,6 @@ const nowColl = computed(() => {
|
||||
return getNowDataTab();
|
||||
});
|
||||
|
||||
/**
|
||||
* instmap; tagPaht -> mongo info[]
|
||||
*/
|
||||
const instMap: Map<string, any[]> = new Map();
|
||||
|
||||
const getInsts = async () => {
|
||||
const res = await mongoApi.mongoList.request({ pageNum: 1, pageSize: 1000 });
|
||||
if (!res.total) return;
|
||||
for (const mongoInfo of res.list) {
|
||||
const tagPath = mongoInfo.tagPath;
|
||||
let mongoInsts = instMap.get(tagPath) || [];
|
||||
mongoInsts.push(mongoInfo);
|
||||
instMap.set(tagPath, mongoInsts);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载标签树树节点
|
||||
*/
|
||||
const loadTags = async () => {
|
||||
await getInsts();
|
||||
const tagPaths = instMap.keys();
|
||||
const tagNodes = [];
|
||||
for (let tagPath of tagPaths) {
|
||||
tagNodes.push(new TagTreeNode(tagPath, tagPath, NodeTypeTagPath));
|
||||
}
|
||||
return tagNodes;
|
||||
};
|
||||
|
||||
const changeCollection = async (id: any, schema: string, collection: string) => {
|
||||
const label = `${id}:\`${schema}\`.${collection}`;
|
||||
let dataTab = state.dataTabs[label];
|
||||
|
||||
@@ -4,8 +4,19 @@
|
||||
<el-form :model="form" ref="mongoForm" :rules="rules" label-width="85px">
|
||||
<el-tabs v-model="tabActiveName">
|
||||
<el-tab-pane label="基础信息" name="basic">
|
||||
<el-form-item prop="tagId" label="标签" required>
|
||||
<tag-select v-model="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
|
||||
<el-form-item ref="tagSelectRef" prop="tagId" label="标签" required>
|
||||
<tag-tree-select
|
||||
@change-tag="
|
||||
(tagIds) => {
|
||||
form.tagId = tagIds;
|
||||
tagSelectRef.validate();
|
||||
}
|
||||
"
|
||||
multiple
|
||||
:resource-code="form.code"
|
||||
:resource-type="TagResourceTypeEnum.Mongo.value"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="name" label="名称" required>
|
||||
@@ -45,8 +56,9 @@
|
||||
import { toRefs, reactive, watch, ref } from 'vue';
|
||||
import { mongoApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import TagSelect from '../component/TagSelect.vue';
|
||||
import TagTreeSelect from '../component/TagTreeSelect.vue';
|
||||
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@@ -88,16 +100,18 @@ const rules = {
|
||||
};
|
||||
|
||||
const mongoForm: any = ref(null);
|
||||
const tagSelectRef: any = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
tabActiveName: 'basic',
|
||||
form: {
|
||||
id: null,
|
||||
code: '',
|
||||
name: null,
|
||||
uri: null,
|
||||
sshTunnelMachineId: null as any,
|
||||
tagId: null as any,
|
||||
tagPath: null as any,
|
||||
tagId: [],
|
||||
},
|
||||
btnLoading: false,
|
||||
testConnBtnLoading: false,
|
||||
@@ -146,7 +160,7 @@ const testConn = async () => {
|
||||
const btnOk = async () => {
|
||||
mongoForm.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
mongoApi.saveMongo.request(getReqForm).then(() => {
|
||||
mongoApi.saveMongo.request(getReqForm()).then(() => {
|
||||
ElMessage.success('保存成功');
|
||||
emit('val-change', state.form);
|
||||
state.btnLoading = true;
|
||||
|
||||
@@ -25,10 +25,7 @@
|
||||
</template>
|
||||
|
||||
<template #tagPath="{ data }">
|
||||
<tag-info :tag-path="data.tagPath" />
|
||||
<span class="ml5">
|
||||
{{ data.tagPath }}
|
||||
</span>
|
||||
<resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Mongo.value" />
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
@@ -57,21 +54,25 @@
|
||||
import { mongoApi } from './api';
|
||||
import { defineAsyncComponent, ref, toRefs, reactive, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import TagInfo from '../component/TagInfo.vue';
|
||||
import ResourceTag from '../component/ResourceTag.vue';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn, TableQuery } from '@/components/pagetable';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { tagApi } from '../tag/api';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const MongoEdit = defineAsyncComponent(() => import('./MongoEdit.vue'));
|
||||
const MongoDbs = defineAsyncComponent(() => import('./MongoDbs.vue'));
|
||||
const MongoRunCommand = defineAsyncComponent(() => import('./MongoRunCommand.vue'));
|
||||
|
||||
const pageTableRef: any = ref(null);
|
||||
const route = useRoute();
|
||||
|
||||
const queryConfig = [TableQuery.slot('tagPath', '标签', 'tagPathSelect')];
|
||||
const columns = ref([
|
||||
TableColumn.new('tagPath', '标签路径').isSlot().setAddWidth(20),
|
||||
TableColumn.new('name', '名称'),
|
||||
TableColumn.new('uri', '连接uri'),
|
||||
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(20).alignCenter(),
|
||||
TableColumn.new('createTime', '创建时间').isTime(),
|
||||
TableColumn.new('creator', '创建人'),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(170).fixedRight().alignCenter(),
|
||||
@@ -88,8 +89,8 @@ const state = reactive({
|
||||
selectionData: [],
|
||||
query: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
tagPath: null,
|
||||
pageSize: 0,
|
||||
tagPath: '',
|
||||
},
|
||||
mongoEditDialog: {
|
||||
visible: false,
|
||||
@@ -134,6 +135,11 @@ const deleteMongo = async () => {
|
||||
const search = async () => {
|
||||
try {
|
||||
pageTableRef.value.loading(true);
|
||||
|
||||
if (route.query.tagPath) {
|
||||
state.query.tagPath = route.query.tagPath as string;
|
||||
}
|
||||
|
||||
const res = await mongoApi.mongoList.request(state.query);
|
||||
state.list = res.list;
|
||||
state.total = res.total;
|
||||
@@ -143,7 +149,7 @@ const search = async () => {
|
||||
};
|
||||
|
||||
const getTags = async () => {
|
||||
state.tags = await mongoApi.mongoTags.request(null);
|
||||
state.tags = await tagApi.getResourceTagPaths.request({ resourceType: TagResourceTypeEnum.Mongo.value });
|
||||
};
|
||||
|
||||
const editMongo = async (data: any) => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<el-col :span="5">
|
||||
<el-row type="flex" justify="space-between">
|
||||
<el-col :span="24" class="flex-auto">
|
||||
<tag-tree :loadTags="loadTags">
|
||||
<tag-tree :resource-type="TagResourceTypeEnum.Redis.value" :tag-path-node-type="NodeTypeTagPath">
|
||||
<template #prefix="{ data }">
|
||||
<span v-if="data.type.value == RedisNodeType.Redis">
|
||||
<el-popover :show-after="500" placement="right-start" title="redis实例信息" trigger="hover" :width="250">
|
||||
@@ -181,6 +181,8 @@ import { TagTreeNode, NodeType } from '../component/tag';
|
||||
import TagTree from '../component/TagTree.vue';
|
||||
import { keysToTree, sortByTreeNodes, keysToList } from './utils';
|
||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||
import { sleep } from '../../../common/utils/loading';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
const KeyDetail = defineAsyncComponent(() => import('./KeyDetail.vue'));
|
||||
|
||||
@@ -212,11 +214,15 @@ class RedisNodeType {
|
||||
|
||||
// tagpath 节点类型
|
||||
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const redisInfos = instMap.get(parentNode.key);
|
||||
if (!redisInfos) {
|
||||
const res = await redisApi.redisList.request({ tagPath: parentNode.key });
|
||||
if (!res.total) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const redisInfos = res.list;
|
||||
await sleep(100);
|
||||
return redisInfos.map((x: any) => {
|
||||
x.tagPath = parentNode.key;
|
||||
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeRedis).withParams(x);
|
||||
});
|
||||
});
|
||||
@@ -321,34 +327,34 @@ const setHeight = () => {
|
||||
state.keyTreeHeight = window.innerHeight - 174 + 'px';
|
||||
};
|
||||
|
||||
/**
|
||||
* instmap; tagPaht -> redis info[]
|
||||
*/
|
||||
const instMap: Map<string, any[]> = new Map();
|
||||
// /**
|
||||
// * instmap; tagPaht -> redis info[]
|
||||
// */
|
||||
// const instMap: Map<string, any[]> = new Map();
|
||||
|
||||
const getInsts = async () => {
|
||||
const res = await redisApi.redisList.request({ pageNum: 1, pageSize: 1000 });
|
||||
if (!res.total) return;
|
||||
for (const redisInfo of res.list) {
|
||||
const tagPath = redisInfo.tagPath;
|
||||
let redisInsts = instMap.get(tagPath) || [];
|
||||
redisInsts.push(redisInfo);
|
||||
instMap.set(tagPath, redisInsts);
|
||||
}
|
||||
};
|
||||
// const getInsts = async () => {
|
||||
// const res = await redisApi.redisList.request({ pageNum: 1, pageSize: 1000 });
|
||||
// if (!res.total) return;
|
||||
// for (const redisInfo of res.list) {
|
||||
// const tagPath = redisInfo.tagPath;
|
||||
// let redisInsts = instMap.get(tagPath) || [];
|
||||
// redisInsts.push(redisInfo);
|
||||
// instMap.set(tagPath, redisInsts);
|
||||
// }
|
||||
// };
|
||||
|
||||
/**
|
||||
* 加载标签树节点
|
||||
*/
|
||||
const loadTags = async () => {
|
||||
await getInsts();
|
||||
const tagPaths = instMap.keys();
|
||||
const tagNodes = [];
|
||||
for (let tagPath of tagPaths) {
|
||||
tagNodes.push(new TagTreeNode(tagPath, tagPath, NodeTypeTagPath));
|
||||
}
|
||||
return tagNodes;
|
||||
};
|
||||
// /**
|
||||
// * 加载标签树节点
|
||||
// */
|
||||
// const loadTags = async () => {
|
||||
// await getInsts();
|
||||
// const tagPaths = instMap.keys();
|
||||
// const tagNodes = [];
|
||||
// for (let tagPath of tagPaths) {
|
||||
// tagNodes.push(new TagTreeNode(tagPath, tagPath, NodeTypeTagPath));
|
||||
// }
|
||||
// return tagNodes;
|
||||
// };
|
||||
|
||||
const scan = async (appendKey = false) => {
|
||||
isTrue(state.scanParam.id != null, '请先选择redis');
|
||||
|
||||
@@ -4,8 +4,19 @@
|
||||
<el-form :model="form" ref="redisForm" :rules="rules" label-width="auto">
|
||||
<el-tabs v-model="tabActiveName">
|
||||
<el-tab-pane label="基础信息" name="basic">
|
||||
<el-form-item prop="tagId" label="标签" required>
|
||||
<tag-select v-model="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
|
||||
<el-form-item ref="tagSelectRef" prop="tagId" label="标签" required>
|
||||
<tag-tree-select
|
||||
@change-tag="
|
||||
(tagIds) => {
|
||||
form.tagId = tagIds;
|
||||
tagSelectRef.validate();
|
||||
}
|
||||
"
|
||||
multiple
|
||||
:resource-code="form.code"
|
||||
:resource-type="TagResourceTypeEnum.Redis.value"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="name" label="名称" required>
|
||||
<el-input v-model.trim="form.name" placeholder="请输入redis名称" auto-complete="off"></el-input>
|
||||
@@ -87,8 +98,9 @@ import { toRefs, reactive, watch, ref } from 'vue';
|
||||
import { redisApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { RsaEncrypt } from '@/common/rsa';
|
||||
import TagSelect from '../component/TagSelect.vue';
|
||||
import TagTreeSelect from '../component/TagTreeSelect.vue';
|
||||
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@@ -143,13 +155,15 @@ const rules = {
|
||||
};
|
||||
|
||||
const redisForm: any = ref(null);
|
||||
const tagSelectRef: any = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
tabActiveName: 'basic',
|
||||
form: {
|
||||
id: null,
|
||||
tagId: null as any,
|
||||
tagPath: null as any,
|
||||
code: '',
|
||||
tagId: [],
|
||||
name: null,
|
||||
mode: 'standalone',
|
||||
host: '',
|
||||
|
||||
@@ -25,10 +25,7 @@
|
||||
</template>
|
||||
|
||||
<template #tagPath="{ data }">
|
||||
<tag-info :tag-path="data.tagPath" />
|
||||
<span class="ml5">
|
||||
{{ data.tagPath }}
|
||||
</span>
|
||||
<resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Redis.value" />
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
@@ -167,18 +164,22 @@ import { ref, toRefs, reactive, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import RedisEdit from './RedisEdit.vue';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import TagInfo from '../component/TagInfo.vue';
|
||||
import ResourceTag from '../component/ResourceTag.vue';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn, TableQuery } from '@/components/pagetable';
|
||||
import { tagApi } from '../tag/api';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const pageTableRef: any = ref(null);
|
||||
const route = useRoute();
|
||||
|
||||
const queryConfig = [TableQuery.slot('tagPath', '标签', 'tagPathSelect')];
|
||||
const columns = ref([
|
||||
TableColumn.new('tagPath', '标签路径').isSlot().setAddWidth(20),
|
||||
TableColumn.new('name', '名称'),
|
||||
TableColumn.new('host', 'host:port'),
|
||||
TableColumn.new('mode', 'mode'),
|
||||
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(10).alignCenter(),
|
||||
TableColumn.new('remark', '备注'),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(200).fixedRight().alignCenter(),
|
||||
]);
|
||||
@@ -189,9 +190,9 @@ const state = reactive({
|
||||
total: 0,
|
||||
selectionData: [],
|
||||
query: {
|
||||
tagPath: null,
|
||||
tagPath: '',
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 0,
|
||||
},
|
||||
detailDialog: {
|
||||
visible: false,
|
||||
@@ -269,6 +270,11 @@ const onShowClusterInfo = async (redis: any) => {
|
||||
const search = async () => {
|
||||
try {
|
||||
pageTableRef.value.loading(true);
|
||||
|
||||
if (route.query.tagPath) {
|
||||
state.query.tagPath = route.query.tagPath as string;
|
||||
}
|
||||
|
||||
const res = await redisApi.redisList.request(state.query);
|
||||
state.redisTable = res.list;
|
||||
state.total = res.total;
|
||||
@@ -278,7 +284,7 @@ const search = async () => {
|
||||
};
|
||||
|
||||
const getTags = async () => {
|
||||
state.tags = await redisApi.redisTags.request(null);
|
||||
state.tags = await tagApi.getResourceTagPaths.request({ resourceType: TagResourceTypeEnum.Redis.value });
|
||||
};
|
||||
|
||||
const editRedis = async (data: any) => {
|
||||
|
||||
@@ -79,6 +79,24 @@
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog :title="`[ ${resourceDialog.tagPath} ] 关联的资源`" v-model="resourceDialog.visible" width="500px">
|
||||
<el-table max-height="300" :data="resourceDialog.data">
|
||||
<el-table-column property="resourceType" label="资源类型" min-width="50" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
{{ EnumValue.getLabelByValue(TagResourceTypeEnum, scope.row.resourceType) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column property="count" label="数量" min-width="50" show-overflow-tooltip> </el-table-column>
|
||||
|
||||
<el-table-column label="操作" min-width="50" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<el-button @click="showResources(scope.row.resourceType, resourceDialog.tagPath)" link type="success">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
|
||||
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -89,6 +107,9 @@ import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { tagApi } from './api';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu/index';
|
||||
import { TagResourceTypeEnum } from '../../../common/commonEnum';
|
||||
import EnumValue from '@/common/Enum';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
interface Tree {
|
||||
id: number;
|
||||
@@ -97,6 +118,8 @@ interface Tree {
|
||||
children?: Tree[];
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const tagForm: any = ref(null);
|
||||
const tagTreeRef: any = ref(null);
|
||||
const filterTag = ref('');
|
||||
@@ -123,6 +146,14 @@ const contextmenuDel = new ContextmenuItem('delete', '删除')
|
||||
})
|
||||
.withOnClick((data: any) => deleteTag(data));
|
||||
|
||||
const contextmenuShowRelateResource = new ContextmenuItem('showRelateResources', '查看关联资源')
|
||||
.withIcon('view')
|
||||
.withHideFunc((data: any) => {
|
||||
// 存在子标签,则不允许查看关联资源
|
||||
return data.children;
|
||||
})
|
||||
.withOnClick((data: any) => showRelateResource(data));
|
||||
|
||||
const state = reactive({
|
||||
data: [],
|
||||
saveTabDialog: {
|
||||
@@ -136,6 +167,12 @@ const state = reactive({
|
||||
// 资源类型选择是否选
|
||||
data: null as any,
|
||||
},
|
||||
resourceDialog: {
|
||||
title: '',
|
||||
visible: false,
|
||||
tagPath: '',
|
||||
data: null as any,
|
||||
},
|
||||
// 展开的节点
|
||||
defaultExpandedKeys: [] as any,
|
||||
contextmenu: {
|
||||
@@ -143,11 +180,11 @@ const state = reactive({
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
items: [contextmenuInfo, contextmenuEdit, contextmenuAdd, contextmenuDel],
|
||||
items: [contextmenuInfo, contextmenuEdit, contextmenuAdd, contextmenuDel, contextmenuShowRelateResource],
|
||||
},
|
||||
});
|
||||
|
||||
const { data, saveTabDialog, infoDialog, defaultExpandedKeys } = toRefs(state);
|
||||
const { data, saveTabDialog, infoDialog, resourceDialog, defaultExpandedKeys } = toRefs(state);
|
||||
|
||||
const props = {
|
||||
label: 'name',
|
||||
@@ -205,7 +242,7 @@ const info = async (data: any) => {
|
||||
const showSaveTagDialog = (data: any) => {
|
||||
if (data) {
|
||||
state.saveTabDialog.form.pid = data.id;
|
||||
state.saveTabDialog.title = `新增 [${data.codePath}] 子标签信息`;
|
||||
state.saveTabDialog.title = `新增[ ${data.codePath} ]子标签信息`;
|
||||
} else {
|
||||
state.saveTabDialog.title = '新增根标签信息';
|
||||
}
|
||||
@@ -221,6 +258,49 @@ const showEditTagDialog = (data: any) => {
|
||||
state.saveTabDialog.visible = true;
|
||||
};
|
||||
|
||||
const showRelateResource = async (data: any) => {
|
||||
const resourceMap = new Map();
|
||||
state.resourceDialog.tagPath = data.codePath;
|
||||
const tagResources = await tagApi.getTagResources.request({ tagId: data.id });
|
||||
for (let tagResource of tagResources) {
|
||||
const resourceType = tagResource.resourceType;
|
||||
const exist = resourceMap.get(resourceType);
|
||||
if (exist) {
|
||||
exist.count = exist.count + 1;
|
||||
} else {
|
||||
resourceMap.set(resourceType, { resourceType, count: 1, tagPath: tagResource.tagPath });
|
||||
}
|
||||
}
|
||||
state.resourceDialog.data = Array.from(resourceMap.values());
|
||||
state.resourceDialog.visible = true;
|
||||
};
|
||||
|
||||
const showResources = (resourceType: any, tagPath: string) => {
|
||||
state.resourceDialog.visible = false;
|
||||
setTimeout(() => {
|
||||
let toPath = '';
|
||||
if (resourceType == TagResourceTypeEnum.Machine.value) {
|
||||
toPath = '/machine/machines';
|
||||
}
|
||||
if (resourceType == TagResourceTypeEnum.Db.value) {
|
||||
toPath = '/dbms/dbs';
|
||||
}
|
||||
if (resourceType == TagResourceTypeEnum.Redis.value) {
|
||||
toPath = '/redis/manage';
|
||||
}
|
||||
if (resourceType == TagResourceTypeEnum.Mongo.value) {
|
||||
toPath = '/mongo/mongo-manage';
|
||||
}
|
||||
|
||||
router.push({
|
||||
path: toPath,
|
||||
query: {
|
||||
tagPath,
|
||||
},
|
||||
});
|
||||
}, 350);
|
||||
};
|
||||
|
||||
const saveTag = async () => {
|
||||
tagForm.value.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
|
||||
@@ -157,7 +157,7 @@ const state = reactive({
|
||||
},
|
||||
query: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 0,
|
||||
name: null,
|
||||
},
|
||||
queryConfig: [TableQuery.text('name', '团队名称')],
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import Api from '@/common/Api';
|
||||
|
||||
export const tagApi = {
|
||||
getAccountTags: Api.newGet('/tag-trees/account-has'),
|
||||
listByQuery: Api.newGet('/tag-trees/query'),
|
||||
getTagTrees: Api.newGet('/tag-trees'),
|
||||
saveTagTree: Api.newPost('/tag-trees'),
|
||||
delTagTree: Api.newDelete('/tag-trees/{id}'),
|
||||
|
||||
getResourceTagPaths: Api.newGet('/tag-trees/resources/{resourceType}/tag-paths'),
|
||||
getTagResources: Api.newGet('/tag-trees/resources'),
|
||||
|
||||
getTeams: Api.newGet('/teams'),
|
||||
saveTeam: Api.newPost('/teams'),
|
||||
delTeam: Api.newDelete('/teams/{id}'),
|
||||
|
||||
@@ -129,7 +129,7 @@ const state = reactive({
|
||||
query: {
|
||||
username: '',
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 0,
|
||||
},
|
||||
datas: [],
|
||||
total: 0,
|
||||
@@ -252,7 +252,9 @@ const deleteAccount = async () => {
|
||||
await accountApi.del.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
ElMessage.success('删除成功');
|
||||
search();
|
||||
} catch (err) {}
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -99,7 +99,7 @@ const paramsFormRef: any = ref(null);
|
||||
const state = reactive({
|
||||
query: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 0,
|
||||
name: null,
|
||||
},
|
||||
total: 0,
|
||||
|
||||
@@ -78,7 +78,7 @@ const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(26
|
||||
const state = reactive({
|
||||
query: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 0,
|
||||
name: null,
|
||||
},
|
||||
total: 0,
|
||||
|
||||
@@ -43,7 +43,7 @@ const state = reactive({
|
||||
creatorId: null,
|
||||
description: null,
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 0,
|
||||
},
|
||||
queryConfig: [
|
||||
TableQuery.slot('creatorId', '操作人', 'selectAccount'),
|
||||
|
||||
@@ -42,9 +42,9 @@ const viteConfig: UserConfig = {
|
||||
chunkSizeWarningLimit: 1500,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: `assets/[hash].[name].js`,
|
||||
chunkFileNames: `assets/[hash].[name].js`,
|
||||
assetFileNames: `assets/[name].[hash].[ext]`,
|
||||
entryFileNames: `assets/[name]-[hash].js`,
|
||||
chunkFileNames: `assets/[name]-[hash].js`,
|
||||
assetFileNames: `assets/[name]-[hash].[ext]`,
|
||||
compact: true,
|
||||
manualChunks: {
|
||||
vue: ['vue', 'vue-router', 'pinia'],
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.21.8.tgz#642af7d0333eab9c0ad70b14ac5e76dbde7bfdf8"
|
||||
integrity sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==
|
||||
|
||||
"@babel/parser@^7.23.3":
|
||||
version "7.23.4"
|
||||
resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.23.4.tgz#409fbe690c333bb70187e2de4021e1e47a026661"
|
||||
integrity sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==
|
||||
"@babel/parser@^7.23.5":
|
||||
version "7.23.5"
|
||||
resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.23.5.tgz#37dee97c4752af148e1d38c34b856b2507660563"
|
||||
integrity sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==
|
||||
|
||||
"@babel/runtime@^7.21.0":
|
||||
version "7.21.5"
|
||||
@@ -24,16 +24,16 @@
|
||||
resolved "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.4.1.tgz"
|
||||
integrity sha512-ej5oVy6lykXsvieQtqZxCOaLT+xD4+QNarq78cIYISHmZXshCvROLudpQN3lfL8G0NL7plMSSK+zlyvCaIJ4Iw==
|
||||
|
||||
"@element-plus/icons-vue@^2.0.6":
|
||||
version "2.0.9"
|
||||
resolved "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.0.9.tgz"
|
||||
integrity sha512-okdrwiVeKBmW41Hkl0eMrXDjzJwhQMuKiBOu17rOszqM+LS/yBYpNQNV5Jvoh06Wc+89fMmb/uhzf8NZuDuUaQ==
|
||||
|
||||
"@element-plus/icons-vue@^2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.1.0.tgz#7ad90d08a8c0d5fd3af31c4f73264ca89614397a"
|
||||
integrity sha512-PSBn3elNoanENc1vnCfh+3WA9fimRC7n+fWkf3rE5jvv+aBohNHABC/KAR5KWPecxWxDTVT1ERpRbOMRcOV/vA==
|
||||
|
||||
"@element-plus/icons-vue@^2.3.1":
|
||||
version "2.3.1"
|
||||
resolved "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz#1f635ad5fdd5c85ed936481525570e82b5a8307a"
|
||||
integrity sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==
|
||||
|
||||
"@esbuild/android-arm64@0.19.8":
|
||||
version "0.19.8"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.19.8.tgz#fb7130103835b6d43ea499c3f30cfb2b2ed58456"
|
||||
@@ -440,6 +440,16 @@
|
||||
resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.4.0.tgz#8ae96573236cdb12de6850a6d929b5537ec85390"
|
||||
integrity sha512-xdguqb+VUwiRpSg+nsc2HtbAUSGak25DXYvpQQi4RVU1Xq1uworyoH/md9Rfd8zMmPR/pSghr309QNcftUVseg==
|
||||
|
||||
"@vue/compiler-core@3.3.10":
|
||||
version "3.3.10"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.3.10.tgz#9ca4123a1458df43db641aaa8b7d1e636aa22545"
|
||||
integrity sha512-doe0hODR1+i1menPkRzJ5MNR6G+9uiZHIknK3Zn5OcIztu6GGw7u0XUzf3AgB8h/dfsZC9eouzoLo3c3+N/cVA==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.23.5"
|
||||
"@vue/shared" "3.3.10"
|
||||
estree-walker "^2.0.2"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
"@vue/compiler-core@3.3.4":
|
||||
version "3.3.4"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.3.4.tgz#7fbf591c1c19e1acd28ffd284526e98b4f581128"
|
||||
@@ -450,15 +460,13 @@
|
||||
estree-walker "^2.0.2"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
"@vue/compiler-core@3.3.9":
|
||||
version "3.3.9"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.3.9.tgz#df1fc7947dcef5c2e12d257eae540057707f47d1"
|
||||
integrity sha512-+/Lf68Vr/nFBA6ol4xOtJrW+BQWv3QWKfRwGSm70jtXwfhZNF4R/eRgyVJYoxFRhdCTk/F6g99BP0ffPgZihfQ==
|
||||
"@vue/compiler-dom@3.3.10":
|
||||
version "3.3.10"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.3.10.tgz#183811252be6aff4ac923f783124bb1590301907"
|
||||
integrity sha512-NCrqF5fm10GXZIK0GrEAauBqdy+F2LZRt3yNHzrYjpYBuRssQbuPLtSnSNjyR9luHKkWSH8we5LMB3g+4z2HvA==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.23.3"
|
||||
"@vue/shared" "3.3.9"
|
||||
estree-walker "^2.0.2"
|
||||
source-map-js "^1.0.2"
|
||||
"@vue/compiler-core" "3.3.10"
|
||||
"@vue/shared" "3.3.10"
|
||||
|
||||
"@vue/compiler-dom@3.3.4":
|
||||
version "3.3.4"
|
||||
@@ -468,28 +476,20 @@
|
||||
"@vue/compiler-core" "3.3.4"
|
||||
"@vue/shared" "3.3.4"
|
||||
|
||||
"@vue/compiler-dom@3.3.9":
|
||||
version "3.3.9"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.3.9.tgz#67315ea4193d9d18c7a710889b8f90f7aa3914d2"
|
||||
integrity sha512-nfWubTtLXuT4iBeDSZ5J3m218MjOy42Vp2pmKVuBKo2/BLcrFUX8nCSr/bKRFiJ32R8qbdnnnBgRn9AdU5v0Sg==
|
||||
"@vue/compiler-sfc@3.3.10":
|
||||
version "3.3.10"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.3.10.tgz#8eb97d42f276089ec58fd0565ef3a813bceeaa87"
|
||||
integrity sha512-xpcTe7Rw7QefOTRFFTlcfzozccvjM40dT45JtrE3onGm/jBLZ0JhpKu3jkV7rbDFLeeagR/5RlJ2Y9SvyS0lAg==
|
||||
dependencies:
|
||||
"@vue/compiler-core" "3.3.9"
|
||||
"@vue/shared" "3.3.9"
|
||||
|
||||
"@vue/compiler-sfc@3.3.9":
|
||||
version "3.3.9"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.3.9.tgz#5900906baba1a90389200d81753ad0f7ceb98a83"
|
||||
integrity sha512-wy0CNc8z4ihoDzjASCOCsQuzW0A/HP27+0MDSSICMjVIFzk/rFViezkR3dzH+miS2NDEz8ywMdbjO5ylhOLI2A==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.23.3"
|
||||
"@vue/compiler-core" "3.3.9"
|
||||
"@vue/compiler-dom" "3.3.9"
|
||||
"@vue/compiler-ssr" "3.3.9"
|
||||
"@vue/reactivity-transform" "3.3.9"
|
||||
"@vue/shared" "3.3.9"
|
||||
"@babel/parser" "^7.23.5"
|
||||
"@vue/compiler-core" "3.3.10"
|
||||
"@vue/compiler-dom" "3.3.10"
|
||||
"@vue/compiler-ssr" "3.3.10"
|
||||
"@vue/reactivity-transform" "3.3.10"
|
||||
"@vue/shared" "3.3.10"
|
||||
estree-walker "^2.0.2"
|
||||
magic-string "^0.30.5"
|
||||
postcss "^8.4.31"
|
||||
postcss "^8.4.32"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
"@vue/compiler-sfc@^3.3.4":
|
||||
@@ -508,6 +508,14 @@
|
||||
postcss "^8.1.10"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
"@vue/compiler-ssr@3.3.10":
|
||||
version "3.3.10"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.3.10.tgz#5a1b14a358cb3960a4edbce0ade90548e452fcaa"
|
||||
integrity sha512-12iM4jA4GEbskwXMmPcskK5wImc2ohKm408+o9iox3tfN9qua8xL0THIZtoe9OJHnXP4eOWZpgCAAThEveNlqQ==
|
||||
dependencies:
|
||||
"@vue/compiler-dom" "3.3.10"
|
||||
"@vue/shared" "3.3.10"
|
||||
|
||||
"@vue/compiler-ssr@3.3.4":
|
||||
version "3.3.4"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz#9d1379abffa4f2b0cd844174ceec4a9721138777"
|
||||
@@ -516,19 +524,22 @@
|
||||
"@vue/compiler-dom" "3.3.4"
|
||||
"@vue/shared" "3.3.4"
|
||||
|
||||
"@vue/compiler-ssr@3.3.9":
|
||||
version "3.3.9"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.3.9.tgz#3b3dbfa5368165fa4ff74c060503b4087ec1beed"
|
||||
integrity sha512-NO5oobAw78R0G4SODY5A502MGnDNiDjf6qvhn7zD7TJGc8XDeIEw4fg6JU705jZ/YhuokBKz0A5a/FL/XZU73g==
|
||||
dependencies:
|
||||
"@vue/compiler-dom" "3.3.9"
|
||||
"@vue/shared" "3.3.9"
|
||||
|
||||
"@vue/devtools-api@^6.5.0":
|
||||
version "6.5.0"
|
||||
resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.5.0.tgz#98b99425edee70b4c992692628fa1ea2c1e57d07"
|
||||
integrity sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==
|
||||
|
||||
"@vue/reactivity-transform@3.3.10":
|
||||
version "3.3.10"
|
||||
resolved "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.3.10.tgz#b045776cc954bb57883fd305db7a200d42993768"
|
||||
integrity sha512-0xBdk+CKHWT+Gev8oZ63Tc0qFfj935YZx+UAynlutnrDZ4diFCVFMWixn65HzjE3S1iJppWOo6Tt1OzASH7VEg==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.23.5"
|
||||
"@vue/compiler-core" "3.3.10"
|
||||
"@vue/shared" "3.3.10"
|
||||
estree-walker "^2.0.2"
|
||||
magic-string "^0.30.5"
|
||||
|
||||
"@vue/reactivity-transform@3.3.4":
|
||||
version "3.3.4"
|
||||
resolved "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz#52908476e34d6a65c6c21cd2722d41ed8ae51929"
|
||||
@@ -540,59 +551,48 @@
|
||||
estree-walker "^2.0.2"
|
||||
magic-string "^0.30.0"
|
||||
|
||||
"@vue/reactivity-transform@3.3.9":
|
||||
version "3.3.9"
|
||||
resolved "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.3.9.tgz#5d894dd9a42a422a2db309babb385f9a2529b52f"
|
||||
integrity sha512-HnUFm7Ry6dFa4Lp63DAxTixUp8opMtQr6RxQCpDI1vlh12rkGIeYqMvJtK+IKyEfEOa2I9oCkD1mmsPdaGpdVg==
|
||||
"@vue/reactivity@3.3.10":
|
||||
version "3.3.10"
|
||||
resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.3.10.tgz#78fe3da319276d9e6d0f072037532928c472a287"
|
||||
integrity sha512-H5Z7rOY/JLO+e5a6/FEXaQ1TMuOvY4LDVgT+/+HKubEAgs9qeeZ+NhADSeEtrNQeiKLDuzeKc8v0CUFpB6Pqgw==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.23.3"
|
||||
"@vue/compiler-core" "3.3.9"
|
||||
"@vue/shared" "3.3.9"
|
||||
estree-walker "^2.0.2"
|
||||
magic-string "^0.30.5"
|
||||
"@vue/shared" "3.3.10"
|
||||
|
||||
"@vue/reactivity@3.3.9":
|
||||
version "3.3.9"
|
||||
resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.3.9.tgz#e28e8071bd74edcdd9c87b667ad00e8fbd8d6920"
|
||||
integrity sha512-VmpIqlNp+aYDg2X0xQhJqHx9YguOmz2UxuUJDckBdQCNkipJvfk9yA75woLWElCa0Jtyec3lAAt49GO0izsphw==
|
||||
"@vue/runtime-core@3.3.10":
|
||||
version "3.3.10"
|
||||
resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.3.10.tgz#d7b78c5c0500b856cf9447ef81d4a1b1438fd5bb"
|
||||
integrity sha512-DZ0v31oTN4YHX9JEU5VW1LoIVgFovWgIVb30bWn9DG9a7oA415idcwsRNNajqTx8HQJyOaWfRKoyuP2P2TYIag==
|
||||
dependencies:
|
||||
"@vue/shared" "3.3.9"
|
||||
"@vue/reactivity" "3.3.10"
|
||||
"@vue/shared" "3.3.10"
|
||||
|
||||
"@vue/runtime-core@3.3.9":
|
||||
version "3.3.9"
|
||||
resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.3.9.tgz#c835b77f7dc7ae5f251e93f277b54963ea1b5c31"
|
||||
integrity sha512-xxaG9KvPm3GTRuM4ZyU8Tc+pMVzcu6eeoSRQJ9IE7NmCcClW6z4B3Ij6L4EDl80sxe/arTtQ6YmgiO4UZqRc+w==
|
||||
"@vue/runtime-dom@3.3.10":
|
||||
version "3.3.10"
|
||||
resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.3.10.tgz#130dfffb8fee8051671aaf80c5104d2020544950"
|
||||
integrity sha512-c/jKb3ny05KJcYk0j1m7Wbhrxq7mZYr06GhKykDMNRRR9S+/dGT8KpHuNQjv3/8U4JshfkAk6TpecPD3B21Ijw==
|
||||
dependencies:
|
||||
"@vue/reactivity" "3.3.9"
|
||||
"@vue/shared" "3.3.9"
|
||||
|
||||
"@vue/runtime-dom@3.3.9":
|
||||
version "3.3.9"
|
||||
resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.3.9.tgz#68081d981695a229d72f431fed0b0cdd9161ce53"
|
||||
integrity sha512-e7LIfcxYSWbV6BK1wQv9qJyxprC75EvSqF/kQKe6bdZEDNValzeRXEVgiX7AHI6hZ59HA4h7WT5CGvm69vzJTQ==
|
||||
dependencies:
|
||||
"@vue/runtime-core" "3.3.9"
|
||||
"@vue/shared" "3.3.9"
|
||||
"@vue/runtime-core" "3.3.10"
|
||||
"@vue/shared" "3.3.10"
|
||||
csstype "^3.1.2"
|
||||
|
||||
"@vue/server-renderer@3.3.9":
|
||||
version "3.3.9"
|
||||
resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.3.9.tgz#ffb41bc9c7afafcc608d0c500e9d6b0af7d68fad"
|
||||
integrity sha512-w0zT/s5l3Oa3ZjtLW88eO4uV6AQFqU8X5GOgzq7SkQQu6vVr+8tfm+OI2kDBplS/W/XgCBuFXiPw6T5EdwXP0A==
|
||||
"@vue/server-renderer@3.3.10":
|
||||
version "3.3.10"
|
||||
resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.3.10.tgz#f23d151f0e5021ebdc730052d9934c9178486742"
|
||||
integrity sha512-0i6ww3sBV3SKlF3YTjSVqKQ74xialMbjVYGy7cOTi7Imd8ediE7t72SK3qnvhrTAhOvlQhq6Bk6nFPdXxe0sAg==
|
||||
dependencies:
|
||||
"@vue/compiler-ssr" "3.3.9"
|
||||
"@vue/shared" "3.3.9"
|
||||
"@vue/compiler-ssr" "3.3.10"
|
||||
"@vue/shared" "3.3.10"
|
||||
|
||||
"@vue/shared@3.3.10":
|
||||
version "3.3.10"
|
||||
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.3.10.tgz#1583a8d85a957d8b819078c465d2a11db7914b2f"
|
||||
integrity sha512-2y3Y2J1a3RhFa0WisHvACJR2ncvWiVHcP8t0Inxo+NKz+8RKO4ZV8eZgCxRgQoA6ITfV12L4E6POOL9HOU5nqw==
|
||||
|
||||
"@vue/shared@3.3.4":
|
||||
version "3.3.4"
|
||||
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.3.4.tgz#06e83c5027f464eef861c329be81454bc8b70780"
|
||||
integrity sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==
|
||||
|
||||
"@vue/shared@3.3.9":
|
||||
version "3.3.9"
|
||||
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.3.9.tgz#df740d26d338faf03e09ca662a8031acf66051db"
|
||||
integrity sha512-ZE0VTIR0LmYgeyhurPTpy4KzKsuDyQbMSdM49eKkMnT5X4VfFBLysMzjIZhLEFQYjjOVVfbvUDHckwjDFiO2eA==
|
||||
|
||||
"@vueuse/core@^9.1.0":
|
||||
version "9.2.0"
|
||||
resolved "https://registry.npmmirror.com/@vueuse/core/-/core-9.2.0.tgz"
|
||||
@@ -764,15 +764,6 @@ clipboard@^2.0.11:
|
||||
select "^1.1.2"
|
||||
tiny-emitter "^2.0.0"
|
||||
|
||||
clipboard@^2.0.6:
|
||||
version "2.0.10"
|
||||
resolved "https://registry.npmmirror.com/clipboard/-/clipboard-2.0.10.tgz"
|
||||
integrity sha512-cz3m2YVwFz95qSEbCDi2fzLN/epEN9zXBvfgAoGkvGOJZATMl9gtTDVOtBYkx2ODUJl2kvmud7n32sV2BpYR4g==
|
||||
dependencies:
|
||||
good-listener "^1.2.2"
|
||||
select "^1.1.2"
|
||||
tiny-emitter "^2.0.0"
|
||||
|
||||
color-convert@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.nlark.com/color-convert/download/color-convert-2.0.1.tgz"
|
||||
@@ -910,13 +901,13 @@ echarts@^5.4.3:
|
||||
tslib "2.3.0"
|
||||
zrender "5.4.4"
|
||||
|
||||
element-plus@^2.4.2:
|
||||
version "2.4.2"
|
||||
resolved "https://registry.npmmirror.com/element-plus/-/element-plus-2.4.2.tgz#2a24632e0904ccd7bbbd64c269704f6b9969833c"
|
||||
integrity sha512-E/HwXX7JF1LPvQSjs0fZ8WblIoc0quoXsRXQZiL7QDq7xJdNGSUaXtdk7xiEv7axPmLfEFtxE5du9fFspDrmJw==
|
||||
element-plus@^2.4.3:
|
||||
version "2.4.3"
|
||||
resolved "https://registry.npmmirror.com/element-plus/-/element-plus-2.4.3.tgz#ff21d0207d71752eb6a47a46609bc667f222841f"
|
||||
integrity sha512-b3q26j+lM4SBqiyzw8HybybGnP2pk4MWgrnzzzYW5qKQUgV6EG1Zg7nMCfgCVccI8tNvZoTiUHb2mFaiB9qT8w==
|
||||
dependencies:
|
||||
"@ctrl/tinycolor" "^3.4.1"
|
||||
"@element-plus/icons-vue" "^2.0.6"
|
||||
"@element-plus/icons-vue" "^2.3.1"
|
||||
"@floating-ui/dom" "^1.0.1"
|
||||
"@popperjs/core" "npm:@sxzz/popperjs-es@^2.11.7"
|
||||
"@types/lodash" "^4.14.182"
|
||||
@@ -1373,7 +1364,7 @@ js-yaml@^4.1.0:
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
|
||||
jsencrypt@^3.3.1:
|
||||
jsencrypt@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.npmmirror.com/jsencrypt/-/jsencrypt-3.3.2.tgz#b0f1a2278810c7ba1cb8957af11195354622df7c"
|
||||
integrity sha512-arQR1R1ESGdAxY7ZheWr12wCaF2yF47v5qpB76TtV64H1pyGudk9Hvw8Y9tb/FiTIaaTRUyaSnm5T/Y53Ghm/A==
|
||||
@@ -1527,10 +1518,10 @@ nanoid@^3.1.30:
|
||||
resolved "https://registry.npmmirror.com/nanoid/download/nanoid-3.1.30.tgz"
|
||||
integrity sha1-Y/k8xUjSoRPcXfvGO/oJ4rm2Q2I=
|
||||
|
||||
nanoid@^3.3.6:
|
||||
version "3.3.6"
|
||||
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
|
||||
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
|
||||
nanoid@^3.3.7:
|
||||
version "3.3.7"
|
||||
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
|
||||
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
@@ -1669,12 +1660,12 @@ postcss@^8.1.10:
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.1"
|
||||
|
||||
postcss@^8.4.31:
|
||||
version "8.4.31"
|
||||
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
|
||||
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
|
||||
postcss@^8.4.32:
|
||||
version "8.4.32"
|
||||
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.32.tgz#1dac6ac51ab19adb21b8b34fd2d93a86440ef6c9"
|
||||
integrity sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==
|
||||
dependencies:
|
||||
nanoid "^3.3.6"
|
||||
nanoid "^3.3.7"
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
@@ -1698,10 +1689,10 @@ punycode@^2.1.0:
|
||||
resolved "https://registry.nlark.com/punycode/download/punycode-2.1.1.tgz"
|
||||
integrity sha1-tYsBCsQMIsVldhbI0sLALHv0eew=
|
||||
|
||||
qrcode.vue@^3.4.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.npmmirror.com/qrcode.vue/-/qrcode.vue-3.4.0.tgz#4513ff1a4734cb7184086c2fd439f0d462c6d281"
|
||||
integrity sha512-4XeImbv10Fin16Fl2DArCMhGyAdvIg2jb7vDT+hZiIAMg/6H6mz9nUZr/dR8jBcun5VzNzkiwKhiqOGbloinwA==
|
||||
qrcode.vue@^3.4.1:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.npmmirror.com/qrcode.vue/-/qrcode.vue-3.4.1.tgz#dd8141da9c4ea07ee56b111cd13eadf123af822a"
|
||||
integrity sha512-wq/zHsifH4FJ1GXQi8/wNxD1KfQkckIpjK1KPTc/qwYU5/Bkd4me0w4xZSg6EXk6xLBkVDE0zxVagewv5EMAVA==
|
||||
|
||||
queue-microtask@^1.2.2:
|
||||
version "1.2.3"
|
||||
@@ -1947,24 +1938,17 @@ uuid@^9.0.1:
|
||||
resolved "https://registry.npmmirror.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
|
||||
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
|
||||
|
||||
vite@^5.0.2:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.npmmirror.com/vite/-/vite-5.0.2.tgz#3c94627dace83b9bf04b64eaf618038e30fb95c0"
|
||||
integrity sha512-6CCq1CAJCNM1ya2ZZA7+jS2KgnhbzvxakmlIjN24cF/PXhRMzpM/z8QgsVJA/Dm5fWUWnVEsmtBoMhmerPxT0g==
|
||||
vite@^5.0.5:
|
||||
version "5.0.5"
|
||||
resolved "https://registry.npmmirror.com/vite/-/vite-5.0.5.tgz#3eebe3698e3b32cea36350f58879258fec858a3c"
|
||||
integrity sha512-OekeWqR9Ls56f3zd4CaxzbbS11gqYkEiBtnWFFgYR2WV8oPJRRKq0mpskYy/XaoCL3L7VINDhqqOMNDiYdGvGg==
|
||||
dependencies:
|
||||
esbuild "^0.19.3"
|
||||
postcss "^8.4.31"
|
||||
postcss "^8.4.32"
|
||||
rollup "^4.2.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
|
||||
vue-clipboard3@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmmirror.com/vue-clipboard3/-/vue-clipboard3-1.0.1.tgz"
|
||||
integrity sha512-iJ2vrizowfA73W3pcxMAKhYSvfekJrQ3FhbveVe9esS1Vfu+xW3Fgc0UKE8N4Q6DyRtcAoNlef8txmD8tK8dIg==
|
||||
dependencies:
|
||||
clipboard "^2.0.6"
|
||||
|
||||
vue-demi@*:
|
||||
version "0.13.11"
|
||||
resolved "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.13.11.tgz"
|
||||
@@ -1995,16 +1979,16 @@ vue-router@^4.2.5:
|
||||
dependencies:
|
||||
"@vue/devtools-api" "^6.5.0"
|
||||
|
||||
vue@^3.3.9:
|
||||
version "3.3.9"
|
||||
resolved "https://registry.npmmirror.com/vue/-/vue-3.3.9.tgz#219a2ec68e8d4d0b0180460af0f5b9299b3f3f1f"
|
||||
integrity sha512-sy5sLCTR8m6tvUk1/ijri3Yqzgpdsmxgj6n6yl7GXXCXqVbmW2RCXe9atE4cEI6Iv7L89v5f35fZRRr5dChP9w==
|
||||
vue@^3.3.10:
|
||||
version "3.3.10"
|
||||
resolved "https://registry.npmmirror.com/vue/-/vue-3.3.10.tgz#6e19c1982ee655a14babe1610288b90005f02ab1"
|
||||
integrity sha512-zg6SIXZdTBwiqCw/1p+m04VyHjLfwtjwz8N57sPaBhEex31ND0RYECVOC1YrRwMRmxFf5T1dabl6SGUbMKKuVw==
|
||||
dependencies:
|
||||
"@vue/compiler-dom" "3.3.9"
|
||||
"@vue/compiler-sfc" "3.3.9"
|
||||
"@vue/runtime-dom" "3.3.9"
|
||||
"@vue/server-renderer" "3.3.9"
|
||||
"@vue/shared" "3.3.9"
|
||||
"@vue/compiler-dom" "3.3.10"
|
||||
"@vue/compiler-sfc" "3.3.10"
|
||||
"@vue/runtime-dom" "3.3.10"
|
||||
"@vue/server-renderer" "3.3.10"
|
||||
"@vue/shared" "3.3.10"
|
||||
|
||||
which@^2.0.1:
|
||||
version "2.0.2"
|
||||
|
||||
@@ -3,6 +3,7 @@ module mayfly-go
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
gitee.com/liuzongyang/libpq v1.0.9
|
||||
github.com/buger/jsonparser v1.1.1
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/glebarez/sqlite v1.10.0
|
||||
@@ -15,7 +16,6 @@ require (
|
||||
github.com/golang-jwt/jwt/v5 v5.1.0
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/kanzihuang/vitess/go/vt/sqlparser v0.0.0-20231018071450-ac8d9f0167e9
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230712084735-068dc2aee82d
|
||||
github.com/mojocn/base64Captcha v1.3.5 // 验证码
|
||||
github.com/pkg/errors v0.9.1
|
||||
@@ -25,7 +25,7 @@ require (
|
||||
github.com/robfig/cron/v3 v3.0.1 // 定时任务
|
||||
github.com/stretchr/testify v1.8.4
|
||||
go.mongodb.org/mongo-driver v1.12.1 // mongo
|
||||
golang.org/x/crypto v0.15.0 // ssh
|
||||
golang.org/x/crypto v0.16.0 // ssh
|
||||
golang.org/x/oauth2 v0.14.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
// gorm
|
||||
@@ -68,6 +68,7 @@ require (
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
@@ -79,7 +80,7 @@ require (
|
||||
golang.org/x/image v0.0.0-20220302094943-723b81ca9867 // indirect
|
||||
golang.org/x/net v0.18.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.14.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230131230820-1c016267d619 // indirect
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/common/consts"
|
||||
dbapp "mayfly-go/internal/db/application"
|
||||
dbentity "mayfly-go/internal/db/domain/entity"
|
||||
machineapp "mayfly-go/internal/machine/application"
|
||||
machineentity "mayfly-go/internal/machine/domain/entity"
|
||||
mongoapp "mayfly-go/internal/mongo/application"
|
||||
mongoentity "mayfly-go/internal/mongo/domain/entity"
|
||||
redisapp "mayfly-go/internal/redis/application"
|
||||
redisentity "mayfly-go/internal/redis/domain/entity"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
"mayfly-go/pkg/req"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
@@ -24,19 +21,12 @@ type Index struct {
|
||||
|
||||
func (i *Index) Count(rc *req.Ctx) {
|
||||
accountId := rc.GetLoginAccount().Id
|
||||
tagIds := i.TagApp.ListTagIdByAccountId(accountId)
|
||||
|
||||
var mongoNum int64
|
||||
var redisNum int64
|
||||
var dbNum int64
|
||||
var machienNum int64
|
||||
mongoNum := len(i.TagApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeMongo, ""))
|
||||
machienNum := len(i.TagApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeMachine, ""))
|
||||
dbNum := len(i.TagApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeDb, ""))
|
||||
redisNum := len(i.TagApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeRedis, ""))
|
||||
|
||||
if len(tagIds) > 0 {
|
||||
mongoNum = i.MongoApp.Count(&mongoentity.MongoQuery{TagIds: tagIds})
|
||||
machienNum = i.MachineApp.Count(&machineentity.MachineQuery{TagIds: tagIds})
|
||||
dbNum = i.DbApp.Count(&dbentity.DbQuery{TagIds: tagIds})
|
||||
redisNum = i.RedisApp.Count(&redisentity.RedisQuery{TagIds: tagIds})
|
||||
}
|
||||
rc.ResData = collx.M{
|
||||
"mongoNum": mongoNum,
|
||||
"machineNum": machienNum,
|
||||
|
||||
@@ -10,9 +10,14 @@ const (
|
||||
RedisConnExpireTime = 30 * time.Minute
|
||||
MongoConnExpireTime = 30 * time.Minute
|
||||
|
||||
/**** 开发测试使用 ****/
|
||||
// MachineConnExpireTime = 4 * time.Minute
|
||||
// DbConnExpireTime = 2 * time.Minute
|
||||
// RedisConnExpireTime = 2 * time.Minute
|
||||
// MongoConnExpireTime = 2 * time.Minute
|
||||
/**** 开发测试使用 ****/
|
||||
// MachineConnExpireTime = 4 * time.Minute
|
||||
// DbConnExpireTime = 2 * time.Minute
|
||||
// RedisConnExpireTime = 2 * time.Minute
|
||||
// MongoConnExpireTime = 2 * time.Minute
|
||||
|
||||
TagResourceTypeMachine = 1
|
||||
TagResourceTypeDb = 2
|
||||
TagResourceTypeRedis = 3
|
||||
TagResourceTypeMongo = 4
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mayfly-go/internal/common/consts"
|
||||
"mayfly-go/internal/db/api/form"
|
||||
"mayfly-go/internal/db/api/vo"
|
||||
"mayfly-go/internal/db/application"
|
||||
@@ -41,29 +42,25 @@ func (d *Db) Dbs(rc *req.Ctx) {
|
||||
queryCond, page := ginx.BindQueryAndPage[*entity.DbQuery](rc.GinCtx, new(entity.DbQuery))
|
||||
|
||||
// 不存在可访问标签id,即没有可操作数据
|
||||
tagIds := d.TagApp.ListTagIdByAccountId(rc.GetLoginAccount().Id)
|
||||
if len(tagIds) == 0 {
|
||||
codes := d.TagApp.GetAccountResourceCodes(rc.GetLoginAccount().Id, consts.TagResourceTypeDb, queryCond.TagPath)
|
||||
if len(codes) == 0 {
|
||||
rc.ResData = model.EmptyPageResult[any]()
|
||||
return
|
||||
}
|
||||
queryCond.Codes = codes
|
||||
|
||||
queryCond.TagIds = tagIds
|
||||
res, err := d.DbApp.GetPageList(queryCond, page, new([]vo.DbListVO))
|
||||
biz.ErrIsNil(err)
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (d *Db) DbTags(rc *req.Ctx) {
|
||||
rc.ResData = d.TagApp.ListTagByAccountIdAndResource(rc.GetLoginAccount().Id, new(entity.Db))
|
||||
}
|
||||
|
||||
func (d *Db) Save(rc *req.Ctx) {
|
||||
form := &form.DbForm{}
|
||||
db := ginx.BindJsonAndCopyTo[*entity.Db](rc.GinCtx, form, new(entity.Db))
|
||||
|
||||
rc.ReqParam = form
|
||||
|
||||
biz.ErrIsNil(d.DbApp.Save(rc.MetaCtx, db))
|
||||
biz.ErrIsNil(d.DbApp.Save(rc.MetaCtx, db, form.TagId...))
|
||||
}
|
||||
|
||||
func (d *Db) DeleteDb(rc *req.Ctx) {
|
||||
@@ -90,7 +87,7 @@ func (d *Db) ExecSql(rc *req.Ctx) {
|
||||
dbId := getDbId(g)
|
||||
dbConn, err := d.DbApp.GetDbConn(dbId, form.Db)
|
||||
biz.ErrIsNil(err)
|
||||
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.GetLoginAccount().Id, dbConn.Info.TagPath), "%s")
|
||||
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.GetLoginAccount().Id, dbConn.Info.TagPath...), "%s")
|
||||
|
||||
rc.ReqParam = fmt.Sprintf("%s\n-> %s", dbConn.Info.GetLogDesc(), form.Sql)
|
||||
biz.NotEmpty(form.Sql, "sql不能为空")
|
||||
@@ -161,7 +158,7 @@ func (d *Db) ExecSqlFile(rc *req.Ctx) {
|
||||
|
||||
dbConn, err := d.DbApp.GetDbConn(dbId, dbName)
|
||||
biz.ErrIsNil(err)
|
||||
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.GetLoginAccount().Id, dbConn.Info.TagPath), "%s")
|
||||
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.GetLoginAccount().Id, dbConn.Info.TagPath...), "%s")
|
||||
rc.ReqParam = fmt.Sprintf("filename: %s -> %s", filename, dbConn.Info.GetLogDesc())
|
||||
|
||||
defer func() {
|
||||
@@ -230,7 +227,7 @@ func (d *Db) ExecSqlFile(rc *req.Ctx) {
|
||||
}
|
||||
dbConn, err = d.DbApp.GetDbConn(dbId, stmtUse.DBName.String())
|
||||
biz.ErrIsNil(err)
|
||||
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(laId, dbConn.Info.TagPath), "%s")
|
||||
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(laId, dbConn.Info.TagPath...), "%s")
|
||||
execReq.DbConn = dbConn
|
||||
}
|
||||
// 需要记录执行记录
|
||||
@@ -270,7 +267,7 @@ func (d *Db) DumpSql(rc *req.Ctx) {
|
||||
la := rc.GetLoginAccount()
|
||||
db, err := d.DbApp.GetById(new(entity.Db), dbId)
|
||||
biz.ErrIsNil(err, "该数据库不存在")
|
||||
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(la.Id, db.TagPath), "%s")
|
||||
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(la.Id, d.TagApp.ListTagPathByResource(consts.TagResourceTypeDb, db.Code)...), "%s")
|
||||
|
||||
now := time.Now()
|
||||
filename := fmt.Sprintf("%s.%s.sql%s", db.Name, now.Format("20060102150405"), extName)
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package form
|
||||
|
||||
type DbForm struct {
|
||||
Id uint64 `json:"id"`
|
||||
Name string `binding:"required" json:"name"`
|
||||
Database string `json:"database"`
|
||||
Remark string `json:"remark"`
|
||||
TagId uint64 `binding:"required" json:"tagId"`
|
||||
TagPath string `binding:"required" json:"tagPath"`
|
||||
InstanceId uint64 `binding:"required" json:"instanceId"`
|
||||
Id uint64 `json:"id"`
|
||||
Name string `binding:"required" json:"name"`
|
||||
Database string `json:"database"`
|
||||
Remark string `json:"remark"`
|
||||
TagId []uint64 `binding:"required" json:"tagId"`
|
||||
InstanceId uint64 `binding:"required" json:"instanceId"`
|
||||
}
|
||||
|
||||
type DbSqlSaveForm struct {
|
||||
|
||||
@@ -4,11 +4,10 @@ import "time"
|
||||
|
||||
type DbListVO struct {
|
||||
Id *int64 `json:"id"`
|
||||
Code string `json:"code"`
|
||||
Name *string `json:"name"`
|
||||
Database *string `json:"database"`
|
||||
Remark *string `json:"remark"`
|
||||
TagId *int64 `json:"tagId"`
|
||||
TagPath *string `json:"tagPath"`
|
||||
|
||||
InstanceId *int64 `json:"instanceId"`
|
||||
InstanceName *string `json:"instanceName"`
|
||||
|
||||
@@ -2,11 +2,12 @@ package application
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/db/infrastructure/persistence"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
)
|
||||
|
||||
var (
|
||||
instanceApp Instance = newInstanceApp(persistence.GetInstanceRepo())
|
||||
dbApp Db = newDbApp(persistence.GetDbRepo(), persistence.GetDbSqlRepo(), instanceApp)
|
||||
dbApp Db = newDbApp(persistence.GetDbRepo(), persistence.GetDbSqlRepo(), instanceApp, tagapp.GetTagTreeApp())
|
||||
dbSqlExecApp DbSqlExec = newDbSqlExecApp(persistence.GetDbSqlExecRepo())
|
||||
dbSqlApp DbSql = newDbSqlApp(persistence.GetDbSqlRepo())
|
||||
)
|
||||
|
||||
@@ -2,13 +2,16 @@ package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayfly-go/internal/common/consts"
|
||||
"mayfly-go/internal/db/dbm"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
"mayfly-go/pkg/utils/structx"
|
||||
"strings"
|
||||
)
|
||||
@@ -21,7 +24,7 @@ type Db interface {
|
||||
|
||||
Count(condition *entity.DbQuery) int64
|
||||
|
||||
Save(ctx context.Context, entity *entity.Db) error
|
||||
Save(ctx context.Context, entity *entity.Db, tagIds ...uint64) error
|
||||
|
||||
// 删除数据库信息
|
||||
Delete(ctx context.Context, id uint64) error
|
||||
@@ -32,10 +35,11 @@ type Db interface {
|
||||
GetDbConn(dbId uint64, dbName string) (*dbm.DbConn, error)
|
||||
}
|
||||
|
||||
func newDbApp(dbRepo repository.Db, dbSqlRepo repository.DbSql, dbInstanceApp Instance) Db {
|
||||
func newDbApp(dbRepo repository.Db, dbSqlRepo repository.DbSql, dbInstanceApp Instance, tagApp tagapp.TagTree) Db {
|
||||
app := &dbAppImpl{
|
||||
dbSqlRepo: dbSqlRepo,
|
||||
dbInstanceApp: dbInstanceApp,
|
||||
tagApp: tagApp,
|
||||
}
|
||||
app.Repo = dbRepo
|
||||
return app
|
||||
@@ -46,6 +50,7 @@ type dbAppImpl struct {
|
||||
|
||||
dbSqlRepo repository.DbSql
|
||||
dbInstanceApp Instance
|
||||
tagApp tagapp.TagTree
|
||||
}
|
||||
|
||||
// 分页获取数据库信息列表
|
||||
@@ -57,7 +62,7 @@ func (d *dbAppImpl) Count(condition *entity.DbQuery) int64 {
|
||||
return d.GetRepo().Count(condition)
|
||||
}
|
||||
|
||||
func (d *dbAppImpl) Save(ctx context.Context, dbEntity *entity.Db) error {
|
||||
func (d *dbAppImpl) Save(ctx context.Context, dbEntity *entity.Db, tagIds ...uint64) error {
|
||||
// 查找是否存在
|
||||
oldDb := &entity.Db{Name: dbEntity.Name, InstanceId: dbEntity.InstanceId}
|
||||
err := d.GetBy(oldDb)
|
||||
@@ -66,7 +71,15 @@ func (d *dbAppImpl) Save(ctx context.Context, dbEntity *entity.Db) error {
|
||||
if err == nil {
|
||||
return errorx.NewBiz("该实例下数据库名已存在")
|
||||
}
|
||||
return d.Insert(ctx, dbEntity)
|
||||
|
||||
resouceCode := stringx.Rand(16)
|
||||
dbEntity.Code = resouceCode
|
||||
|
||||
return d.Tx(ctx, func(ctx context.Context) error {
|
||||
return d.Insert(ctx, dbEntity)
|
||||
}, func(ctx context.Context) error {
|
||||
return d.tagApp.RelateResource(ctx, resouceCode, consts.TagResourceTypeDb, tagIds)
|
||||
})
|
||||
}
|
||||
|
||||
// 如果存在该库,则校验修改的库是否为该库
|
||||
@@ -94,7 +107,11 @@ func (d *dbAppImpl) Save(ctx context.Context, dbEntity *entity.Db) error {
|
||||
d.dbSqlRepo.DeleteByCond(ctx, &entity.DbSql{DbId: dbId, Db: v})
|
||||
}
|
||||
|
||||
return d.UpdateById(ctx, dbEntity)
|
||||
return d.Tx(ctx, func(ctx context.Context) error {
|
||||
return d.UpdateById(ctx, dbEntity)
|
||||
}, func(ctx context.Context) error {
|
||||
return d.tagApp.RelateResource(ctx, oldDb.Code, consts.TagResourceTypeDb, tagIds)
|
||||
})
|
||||
}
|
||||
|
||||
func (d *dbAppImpl) Delete(ctx context.Context, id uint64) error {
|
||||
@@ -138,11 +155,11 @@ func (d *dbAppImpl) GetDbConn(dbId uint64, dbName string) (*dbm.DbConn, error) {
|
||||
|
||||
// 密码解密
|
||||
instance.PwdDecrypt()
|
||||
return toDbInfo(instance, dbId, dbName, db.TagPath), nil
|
||||
return toDbInfo(instance, dbId, dbName, d.tagApp.ListTagPathByResource(consts.TagResourceTypeDb, db.Code)...), nil
|
||||
})
|
||||
}
|
||||
|
||||
func toDbInfo(instance *entity.DbInstance, dbId uint64, database string, tagPath string) *dbm.DbInfo {
|
||||
func toDbInfo(instance *entity.DbInstance, dbId uint64, database string, tagPath ...string) *dbm.DbInfo {
|
||||
di := new(dbm.DbInfo)
|
||||
di.Id = dbId
|
||||
di.Database = database
|
||||
|
||||
@@ -48,6 +48,7 @@ func (app *instanceAppImpl) Count(condition *entity.InstanceQuery) int64 {
|
||||
}
|
||||
|
||||
func (app *instanceAppImpl) TestConn(instanceEntity *entity.DbInstance) error {
|
||||
instanceEntity.Network = instanceEntity.GetNetwork()
|
||||
dbConn, err := toDbInfo(instanceEntity, 0, "", "").Conn()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
pq "gitee.com/liuzongyang/libpq"
|
||||
"github.com/kanzihuang/vitess/go/vt/sqlparser"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
type DbType string
|
||||
|
||||
@@ -8,11 +8,12 @@ import (
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/utils/anyx"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/netx"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
pq "gitee.com/liuzongyang/libpq"
|
||||
)
|
||||
|
||||
func getPgsqlDB(d *DbInfo) (*sql.DB, error) {
|
||||
@@ -41,7 +42,7 @@ func getPgsqlDB(d *DbInfo) (*sql.DB, error) {
|
||||
}
|
||||
}
|
||||
|
||||
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s %s sslmode=disable", d.Host, d.Port, d.Username, d.Password, dbParam)
|
||||
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s %s sslmode=disable connect_timeout=8", d.Host, d.Port, d.Username, d.Password, dbParam)
|
||||
// 存在额外指定参数,则拼接该连接参数
|
||||
if d.Params != "" {
|
||||
// 存在指定的db,则需要将dbInstance配置中的parmas排除掉dbname和search_path
|
||||
@@ -79,7 +80,8 @@ func (pd *PqSqlDialer) Dial(network, address string) (net.Conn, error) {
|
||||
return nil, err
|
||||
}
|
||||
if sshConn, err := sshTunnel.GetDialConn("tcp", address); err == nil {
|
||||
return sshConn, nil
|
||||
// 将ssh conn包装,否则会返回错误: ssh: tcpChan: deadline not supported
|
||||
return &netx.WrapSshConn{Conn: sshConn}, nil
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ type DbInfo struct {
|
||||
Params string
|
||||
Database string
|
||||
|
||||
TagPath string
|
||||
TagPath []string
|
||||
SshTunnelMachineId int
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,9 @@ import (
|
||||
type Db struct {
|
||||
model.Model
|
||||
|
||||
Code string `orm:"column(code)" json:"code"`
|
||||
Name string `orm:"column(name)" json:"name"`
|
||||
Database string `orm:"column(database)" json:"database"`
|
||||
Remark string `json:"remark"`
|
||||
TagId uint64
|
||||
TagPath string
|
||||
InstanceId uint64
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ type DbQuery struct {
|
||||
Database string `orm:"column(database)" json:"database"`
|
||||
Remark string `json:"remark"`
|
||||
|
||||
Codes []string
|
||||
TagIds []uint64 `orm:"column(tag_id)"`
|
||||
TagPath string `form:"tagPath"`
|
||||
|
||||
|
||||
@@ -23,11 +23,9 @@ func (d *dbRepoImpl) GetDbList(condition *entity.DbQuery, pageParam *model.PageP
|
||||
Joins("JOIN t_db_instance inst ON db.instance_id = inst.id").
|
||||
Eq("db.instance_id", condition.InstanceId).
|
||||
Like("db.database", condition.Database).
|
||||
In("db.tag_id", condition.TagIds).
|
||||
RLike("db.tag_path", condition.TagPath).
|
||||
In("db.code", condition.Codes).
|
||||
Eq0("db."+model.DeletedColumn, model.ModelUndeleted).
|
||||
Eq0("inst."+model.DeletedColumn, model.ModelUndeleted).
|
||||
OrderByAsc("db.tag_path")
|
||||
Eq0("inst."+model.DeletedColumn, model.ModelUndeleted)
|
||||
|
||||
return gormx.PageQuery(qd, pageParam, toEntity)
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@ func InitDbRouter(router *gin.RouterGroup) {
|
||||
// 获取数据库列表
|
||||
req.NewGet("", d.Dbs),
|
||||
|
||||
req.NewGet("/tags", d.DbTags),
|
||||
|
||||
req.NewPost("", d.Save).Log(req.NewLogSave("db-保存数据库信息")),
|
||||
|
||||
req.NewDelete(":dbId", d.DeleteDb).Log(req.NewLogSave("db-删除数据库信息")),
|
||||
|
||||
@@ -7,11 +7,10 @@ type MachineForm struct {
|
||||
Port int `json:"port" binding:"required"` // 端口号
|
||||
|
||||
// 资产授权凭证信息列表
|
||||
AuthCertId int `json:"authCertId"`
|
||||
TagId uint64 `json:"tagId" binding:"required"`
|
||||
TagPath string `json:"tagPath" binding:"required"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
AuthCertId int `json:"authCertId"`
|
||||
TagId []uint64 `json:"tagId" binding:"required"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
|
||||
Remark string `json:"remark"`
|
||||
SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id
|
||||
|
||||
@@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"mayfly-go/internal/common/consts"
|
||||
"mayfly-go/internal/machine/api/form"
|
||||
"mayfly-go/internal/machine/api/vo"
|
||||
"mayfly-go/internal/machine/application"
|
||||
@@ -17,34 +18,32 @@ import (
|
||||
"mayfly-go/pkg/req"
|
||||
"mayfly-go/pkg/utils/anyx"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
"mayfly-go/pkg/ws"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type Machine struct {
|
||||
MachineApp application.Machine
|
||||
TagApp tagapp.TagTree
|
||||
MachineApp application.Machine
|
||||
MachineTermOpApp application.MachineTermOp
|
||||
TagApp tagapp.TagTree
|
||||
}
|
||||
|
||||
func (m *Machine) Machines(rc *req.Ctx) {
|
||||
condition, pageParam := ginx.BindQueryAndPage(rc.GinCtx, new(entity.MachineQuery))
|
||||
|
||||
// 不存在可访问标签id,即没有可操作数据
|
||||
tagIds := m.TagApp.ListTagIdByAccountId(rc.GetLoginAccount().Id)
|
||||
if len(tagIds) == 0 {
|
||||
codes := m.TagApp.GetAccountResourceCodes(rc.GetLoginAccount().Id, consts.TagResourceTypeMachine, condition.TagPath)
|
||||
if len(codes) == 0 {
|
||||
rc.ResData = model.EmptyPageResult[any]()
|
||||
return
|
||||
}
|
||||
condition.TagIds = tagIds
|
||||
condition.Codes = codes
|
||||
|
||||
res, err := m.MachineApp.GetMachineList(condition, pageParam, new([]*vo.MachineVO))
|
||||
biz.ErrIsNil(err)
|
||||
@@ -67,10 +66,6 @@ func (m *Machine) Machines(rc *req.Ctx) {
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (m *Machine) MachineTags(rc *req.Ctx) {
|
||||
rc.ResData = m.TagApp.ListTagByAccountIdAndResource(rc.GetLoginAccount().Id, new(entity.Machine))
|
||||
}
|
||||
|
||||
func (m *Machine) MachineStats(rc *req.Ctx) {
|
||||
cli, err := m.MachineApp.GetCli(GetMachineId(rc.GinCtx))
|
||||
biz.ErrIsNilAppendErr(err, "获取客户端连接失败: %s")
|
||||
@@ -85,7 +80,7 @@ func (m *Machine) SaveMachine(rc *req.Ctx) {
|
||||
machineForm.Password = "******"
|
||||
rc.ReqParam = machineForm
|
||||
|
||||
biz.ErrIsNil(m.MachineApp.Save(rc.MetaCtx, me))
|
||||
biz.ErrIsNil(m.MachineApp.Save(rc.MetaCtx, me, machineForm.TagId...))
|
||||
}
|
||||
|
||||
func (m *Machine) TestConn(rc *req.Ctx) {
|
||||
@@ -140,7 +135,7 @@ func (m *Machine) GetProcess(rc *req.Ctx) {
|
||||
|
||||
cli, err := m.MachineApp.GetCli(GetMachineId(rc.GinCtx))
|
||||
biz.ErrIsNilAppendErr(err, "获取客户端连接失败: %s")
|
||||
biz.ErrIsNilAppendErr(m.TagApp.CanAccess(rc.GetLoginAccount().Id, cli.Info.TagPath), "%s")
|
||||
biz.ErrIsNilAppendErr(m.TagApp.CanAccess(rc.GetLoginAccount().Id, cli.Info.TagPath...), "%s")
|
||||
|
||||
res, err := cli.Run(cmd)
|
||||
biz.ErrIsNilAppendErr(err, "获取进程信息失败: %s")
|
||||
@@ -154,7 +149,7 @@ func (m *Machine) KillProcess(rc *req.Ctx) {
|
||||
|
||||
cli, err := m.MachineApp.GetCli(GetMachineId(rc.GinCtx))
|
||||
biz.ErrIsNilAppendErr(err, "获取客户端连接失败: %s")
|
||||
biz.ErrIsNilAppendErr(m.TagApp.CanAccess(rc.GetLoginAccount().Id, cli.Info.TagPath), "%s")
|
||||
biz.ErrIsNilAppendErr(m.TagApp.CanAccess(rc.GetLoginAccount().Id, cli.Info.TagPath...), "%s")
|
||||
|
||||
res, err := cli.Run("sudo kill -9 " + pid)
|
||||
biz.ErrIsNil(err, "终止进程失败: %s", res)
|
||||
@@ -180,59 +175,35 @@ func (m *Machine) WsSSH(g *gin.Context) {
|
||||
|
||||
cli, err := m.MachineApp.GetCli(GetMachineId(g))
|
||||
biz.ErrIsNilAppendErr(err, "获取客户端连接失败: %s")
|
||||
biz.ErrIsNilAppendErr(m.TagApp.CanAccess(rc.GetLoginAccount().Id, cli.Info.TagPath), "%s")
|
||||
biz.ErrIsNilAppendErr(m.TagApp.CanAccess(rc.GetLoginAccount().Id, cli.Info.TagPath...), "%s")
|
||||
|
||||
cols := ginx.QueryInt(g, "cols", 80)
|
||||
rows := ginx.QueryInt(g, "rows", 40)
|
||||
|
||||
var recorder *mcm.Recorder
|
||||
if cli.Info.EnableRecorder == 1 {
|
||||
now := time.Now()
|
||||
// 回放文件路径为: 基础配置路径/机器id/操作日期/操作者账号/操作时间.cast
|
||||
recPath := fmt.Sprintf("%s/%d/%s/%s", config.GetMachine().TerminalRecPath, cli.Info.Id, now.Format("20060102"), rc.GetLoginAccount().Username)
|
||||
os.MkdirAll(recPath, 0766)
|
||||
fileName := path.Join(recPath, fmt.Sprintf("%s.cast", now.Format("20060102_150405")))
|
||||
f, err := os.OpenFile(fileName, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0766)
|
||||
biz.ErrIsNilAppendErr(err, "创建终端回放记录文件失败: %s")
|
||||
defer f.Close()
|
||||
recorder = mcm.NewRecorder(f)
|
||||
}
|
||||
|
||||
mts, err := mcm.NewTerminalSession(stringx.Rand(16), wsConn, cli, rows, cols, recorder)
|
||||
biz.ErrIsNilAppendErr(err, "\033[1;31m连接失败: %s\033[0m")
|
||||
|
||||
// 记录系统操作日志
|
||||
rc.WithLog(req.NewLogSave("机器-终端操作"))
|
||||
rc.ReqParam = cli.Info
|
||||
req.LogHandler(rc)
|
||||
|
||||
mts.Start()
|
||||
defer mts.Stop()
|
||||
err = m.MachineTermOpApp.TermConn(rc.MetaCtx, cli, wsConn, rows, cols)
|
||||
biz.ErrIsNilAppendErr(err, "\033[1;31m连接失败: %s\033[0m")
|
||||
}
|
||||
|
||||
// 获取机器终端回放记录的相应文件夹名或文件内容
|
||||
func (m *Machine) MachineRecDirNames(rc *req.Ctx) {
|
||||
readPath := rc.GinCtx.Query("path")
|
||||
biz.NotEmpty(readPath, "path不能为空")
|
||||
path_ := path.Join(config.GetMachine().TerminalRecPath, readPath)
|
||||
func (m *Machine) MachineTermOpRecords(rc *req.Ctx) {
|
||||
mid := GetMachineId(rc.GinCtx)
|
||||
res, err := m.MachineTermOpApp.GetPageList(&entity.MachineTermOp{MachineId: mid}, ginx.GetPageParam(rc.GinCtx), new([]entity.MachineTermOp))
|
||||
biz.ErrIsNil(err)
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
// 如果是读取文件内容,则读取对应回放记录文件内容,否则读取文件夹名列表。小小偷懒一会不想再加个接口
|
||||
isFile := rc.GinCtx.Query("isFile")
|
||||
if isFile == "1" {
|
||||
bytes, err := os.ReadFile(path_)
|
||||
biz.ErrIsNilAppendErr(err, "还未有相应终端操作记录: %s")
|
||||
rc.ResData = base64.StdEncoding.EncodeToString(bytes)
|
||||
return
|
||||
}
|
||||
func (m *Machine) MachineTermOpRecord(rc *req.Ctx) {
|
||||
recId, _ := strconv.Atoi(rc.GinCtx.Param("recId"))
|
||||
termOp, err := m.MachineTermOpApp.GetById(new(entity.MachineTermOp), uint64(recId))
|
||||
biz.ErrIsNil(err)
|
||||
|
||||
files, err := os.ReadDir(path_)
|
||||
biz.ErrIsNilAppendErr(err, "还未有相应终端操作记录: %s")
|
||||
var names []string
|
||||
for _, f := range files {
|
||||
names = append(names, f.Name())
|
||||
}
|
||||
sort.Sort(sort.Reverse(sort.StringSlice(names)))
|
||||
rc.ResData = names
|
||||
bytes, err := os.ReadFile(path.Join(config.GetMachine().TerminalRecPath, termOp.RecordFilePath))
|
||||
biz.ErrIsNilAppendErr(err, "读取终端操作记录失败: %s")
|
||||
rc.ResData = base64.StdEncoding.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
func GetMachineId(g *gin.Context) uint64 {
|
||||
|
||||
@@ -63,6 +63,12 @@ func (m *MachineCronJob) GetRelateCronJobIds(rc *req.Ctx) {
|
||||
rc.ResData = m.MachineCronJobApp.GetRelateMachineIds(uint64(ginx.QueryInt(rc.GinCtx, "machineId", -1)))
|
||||
}
|
||||
|
||||
func (m *MachineCronJob) RunCronJob(rc *req.Ctx) {
|
||||
cronJobKey := ginx.PathParam(rc.GinCtx, "key")
|
||||
biz.NotEmpty(cronJobKey, "cronJob key不能为空")
|
||||
m.MachineCronJobApp.RunCronJob(cronJobKey)
|
||||
}
|
||||
|
||||
func (m *MachineCronJob) CronJobExecs(rc *req.Ctx) {
|
||||
cond, pageParam := ginx.BindQueryAndPage[*entity.MachineCronJobExec](rc.GinCtx, new(entity.MachineCronJobExec))
|
||||
res, err := m.MachineCronJobApp.GetExecPageList(cond, pageParam, new([]entity.MachineCronJobExec))
|
||||
|
||||
@@ -69,7 +69,7 @@ func (m *MachineScript) RunMachineScript(rc *req.Ctx) {
|
||||
}
|
||||
cli, err := m.MachineApp.GetCli(machineId)
|
||||
biz.ErrIsNilAppendErr(err, "获取客户端连接失败: %s")
|
||||
biz.ErrIsNilAppendErr(m.TagApp.CanAccess(rc.GetLoginAccount().Id, cli.Info.TagPath), "%s")
|
||||
biz.ErrIsNilAppendErr(m.TagApp.CanAccess(rc.GetLoginAccount().Id, cli.Info.TagPath...), "%s")
|
||||
|
||||
res, err := cli.Run(script)
|
||||
// 记录请求参数
|
||||
|
||||
@@ -13,6 +13,7 @@ type AuthCertBaseVO struct {
|
||||
|
||||
type MachineVO struct {
|
||||
Id uint64 `json:"id"`
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Ip string `json:"ip"`
|
||||
Port int `json:"port"`
|
||||
@@ -28,8 +29,8 @@ type MachineVO struct {
|
||||
ModifierId *int64 `json:"modifierId"`
|
||||
Remark *string `json:"remark"`
|
||||
EnableRecorder int8 `json:"enableRecorder"`
|
||||
TagId uint64 `json:"tagId"`
|
||||
TagPath string `json:"tagPath"`
|
||||
// TagId uint64 `json:"tagId"`
|
||||
// TagPath string `json:"tagPath"`
|
||||
|
||||
HasCli bool `json:"hasCli" gorm:"-"`
|
||||
Stat map[string]any `json:"stat" gorm:"-"`
|
||||
|
||||
@@ -2,12 +2,14 @@ package application
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/machine/infrastructure/persistence"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
)
|
||||
|
||||
var (
|
||||
machineApp Machine = newMachineApp(
|
||||
persistence.GetMachineRepo(),
|
||||
GetAuthCertApp(),
|
||||
tagapp.GetTagTreeApp(),
|
||||
)
|
||||
|
||||
machineFileApp MachineFile = newMachineFileApp(
|
||||
@@ -28,6 +30,8 @@ var (
|
||||
persistence.GetMachineCronJobExecRepo(),
|
||||
GetMachineApp(),
|
||||
)
|
||||
|
||||
machineTermOpApp MachineTermOp = newMachineTermOpApp(persistence.GetMachineTermOpRepo())
|
||||
)
|
||||
|
||||
func GetMachineApp() Machine {
|
||||
@@ -49,3 +53,7 @@ func GetAuthCertApp() AuthCert {
|
||||
func GetMachineCronJobApp() MachineCronJob {
|
||||
return machineCropJobApp
|
||||
}
|
||||
|
||||
func GetMachineTermOpApp() MachineTermOp {
|
||||
return machineTermOpApp
|
||||
}
|
||||
|
||||
@@ -3,17 +3,20 @@ package application
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"mayfly-go/internal/common/consts"
|
||||
"mayfly-go/internal/machine/api/vo"
|
||||
"mayfly-go/internal/machine/domain/entity"
|
||||
"mayfly-go/internal/machine/domain/repository"
|
||||
"mayfly-go/internal/machine/infrastructure/cache"
|
||||
"mayfly-go/internal/machine/mcm"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/gormx"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/scheduler"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -22,7 +25,7 @@ import (
|
||||
type Machine interface {
|
||||
base.App[*entity.Machine]
|
||||
|
||||
Save(ctx context.Context, m *entity.Machine) error
|
||||
Save(ctx context.Context, m *entity.Machine, tagIds ...uint64) error
|
||||
|
||||
// 测试机器连接
|
||||
TestConn(me *entity.Machine) error
|
||||
@@ -30,8 +33,6 @@ type Machine interface {
|
||||
// 调整机器状态
|
||||
ChangeStatus(ctx context.Context, id uint64, status int8) error
|
||||
|
||||
Count(condition *entity.MachineQuery) int64
|
||||
|
||||
Delete(ctx context.Context, id uint64) error
|
||||
|
||||
// 分页获取机器信息列表
|
||||
@@ -50,9 +51,10 @@ type Machine interface {
|
||||
GetMachineStats(machineId uint64) (*mcm.Stats, error)
|
||||
}
|
||||
|
||||
func newMachineApp(machineRepo repository.Machine, authCertApp AuthCert) Machine {
|
||||
func newMachineApp(machineRepo repository.Machine, authCertApp AuthCert, tagApp tagapp.TagTree) Machine {
|
||||
app := &machineAppImpl{
|
||||
authCertApp: authCertApp,
|
||||
tagApp: tagApp,
|
||||
}
|
||||
app.Repo = machineRepo
|
||||
return app
|
||||
@@ -62,6 +64,8 @@ type machineAppImpl struct {
|
||||
base.AppImpl[*entity.Machine, repository.Machine]
|
||||
|
||||
authCertApp AuthCert
|
||||
|
||||
tagApp tagapp.TagTree
|
||||
}
|
||||
|
||||
// 分页获取机器信息列表
|
||||
@@ -69,11 +73,7 @@ func (m *machineAppImpl) GetMachineList(condition *entity.MachineQuery, pagePara
|
||||
return m.GetRepo().GetMachineList(condition, pageParam, toEntity, orderBy...)
|
||||
}
|
||||
|
||||
func (m *machineAppImpl) Count(condition *entity.MachineQuery) int64 {
|
||||
return m.GetRepo().Count(condition)
|
||||
}
|
||||
|
||||
func (m *machineAppImpl) Save(ctx context.Context, me *entity.Machine) error {
|
||||
func (m *machineAppImpl) Save(ctx context.Context, me *entity.Machine, tagIds ...uint64) error {
|
||||
oldMachine := &entity.Machine{Ip: me.Ip, Port: me.Port, Username: me.Username}
|
||||
if me.SshTunnelMachineId > 0 {
|
||||
oldMachine.SshTunnelMachineId = me.SshTunnelMachineId
|
||||
@@ -85,9 +85,16 @@ func (m *machineAppImpl) Save(ctx context.Context, me *entity.Machine) error {
|
||||
if err == nil {
|
||||
return errorx.NewBiz("该机器信息已存在")
|
||||
}
|
||||
resouceCode := stringx.Rand(16)
|
||||
me.Code = resouceCode
|
||||
// 新增机器,默认启用状态
|
||||
me.Status = entity.MachineStatusEnable
|
||||
return m.Insert(ctx, me)
|
||||
|
||||
return m.Tx(ctx, func(ctx context.Context) error {
|
||||
return m.Insert(ctx, me)
|
||||
}, func(ctx context.Context) error {
|
||||
return m.tagApp.RelateResource(ctx, resouceCode, consts.TagResourceTypeMachine, tagIds)
|
||||
})
|
||||
}
|
||||
|
||||
// 如果存在该库,则校验修改的库是否为该库
|
||||
@@ -97,7 +104,11 @@ func (m *machineAppImpl) Save(ctx context.Context, me *entity.Machine) error {
|
||||
|
||||
// 关闭连接
|
||||
mcm.DeleteCli(me.Id)
|
||||
return m.UpdateById(ctx, me)
|
||||
return m.Tx(ctx, func(ctx context.Context) error {
|
||||
return m.UpdateById(ctx, me)
|
||||
}, func(ctx context.Context) error {
|
||||
return m.tagApp.RelateResource(ctx, oldMachine.Code, consts.TagResourceTypeMachine, tagIds)
|
||||
})
|
||||
}
|
||||
|
||||
func (m *machineAppImpl) TestConn(me *entity.Machine) error {
|
||||
@@ -214,7 +225,7 @@ func (m *machineAppImpl) toMachineInfo(me *entity.Machine) (*mcm.MachineInfo, er
|
||||
mi.Ip = me.Ip
|
||||
mi.Port = me.Port
|
||||
mi.Username = me.Username
|
||||
mi.TagPath = me.TagPath
|
||||
mi.TagPath = m.tagApp.ListTagPathByResource(consts.TagResourceTypeMachine, me.Code)
|
||||
mi.EnableRecorder = me.EnableRecorder
|
||||
|
||||
if me.UseAuthCert() {
|
||||
|
||||
@@ -44,6 +44,10 @@ type MachineCronJob interface {
|
||||
|
||||
// 初始化计划任务
|
||||
InitCronJob()
|
||||
|
||||
// 执行cron job
|
||||
// @param key cron job key
|
||||
RunCronJob(key string)
|
||||
}
|
||||
|
||||
type machineCropJobAppImpl struct {
|
||||
@@ -183,30 +187,7 @@ func (m *machineCropJobAppImpl) InitCronJob() {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *machineCropJobAppImpl) addCronJob(mcj *entity.MachineCronJob) {
|
||||
var key string
|
||||
isDisable := mcj.Status == entity.MachineCronJobStatusDisable
|
||||
if mcj.Id == 0 {
|
||||
key = stringx.Rand(16)
|
||||
mcj.Key = key
|
||||
if isDisable {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
key = mcj.Key
|
||||
}
|
||||
|
||||
if isDisable {
|
||||
scheduler.RemoveByKey(key)
|
||||
return
|
||||
}
|
||||
|
||||
scheduler.AddFunByKey(key, mcj.Cron, func() {
|
||||
go m.runCronJob(key)
|
||||
})
|
||||
}
|
||||
|
||||
func (m *machineCropJobAppImpl) runCronJob(key string) {
|
||||
func (m *machineCropJobAppImpl) RunCronJob(key string) {
|
||||
// 简单使用redis分布式锁防止多实例同一时刻重复执行
|
||||
if lock := rediscli.NewLock(key, 30*time.Second); lock != nil {
|
||||
if !lock.Lock() {
|
||||
@@ -229,6 +210,28 @@ func (m *machineCropJobAppImpl) runCronJob(key string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *machineCropJobAppImpl) addCronJob(mcj *entity.MachineCronJob) {
|
||||
var key string
|
||||
isDisable := mcj.Status == entity.MachineCronJobStatusDisable
|
||||
if mcj.Id == 0 {
|
||||
key = stringx.Rand(16)
|
||||
mcj.Key = key
|
||||
if isDisable {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
key = mcj.Key
|
||||
}
|
||||
|
||||
if isDisable {
|
||||
scheduler.RemoveByKey(key)
|
||||
return
|
||||
}
|
||||
|
||||
scheduler.AddFunByKey(key, mcj.Cron, func() {
|
||||
go m.RunCronJob(key)
|
||||
})
|
||||
}
|
||||
func (m *machineCropJobAppImpl) runCronJob0(mid uint64, cronJob *entity.MachineCronJob) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
|
||||
93
server/internal/machine/application/machine_term_op.go
Normal file
93
server/internal/machine/application/machine_term_op.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"mayfly-go/internal/machine/config"
|
||||
"mayfly-go/internal/machine/domain/entity"
|
||||
"mayfly-go/internal/machine/domain/repository"
|
||||
"mayfly-go/internal/machine/mcm"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/contextx"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type MachineTermOp interface {
|
||||
base.App[*entity.MachineTermOp]
|
||||
|
||||
// 终端连接操作
|
||||
TermConn(ctx context.Context, cli *mcm.Cli, wsConn *websocket.Conn, rows, cols int) error
|
||||
|
||||
GetPageList(condition *entity.MachineTermOp, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
}
|
||||
|
||||
func newMachineTermOpApp(machineTermOpRepo repository.MachineTermOp) MachineTermOp {
|
||||
return &machineTermOpAppImpl{
|
||||
base.AppImpl[*entity.MachineTermOp, repository.MachineTermOp]{Repo: machineTermOpRepo},
|
||||
}
|
||||
}
|
||||
|
||||
type machineTermOpAppImpl struct {
|
||||
base.AppImpl[*entity.MachineTermOp, repository.MachineTermOp]
|
||||
}
|
||||
|
||||
func (a *machineTermOpAppImpl) TermConn(ctx context.Context, cli *mcm.Cli, wsConn *websocket.Conn, rows, cols int) error {
|
||||
var recorder *mcm.Recorder
|
||||
var termOpRecord *entity.MachineTermOp
|
||||
|
||||
// 开启终端操作记录
|
||||
if cli.Info.EnableRecorder == 1 {
|
||||
now := time.Now()
|
||||
la := contextx.GetLoginAccount(ctx)
|
||||
|
||||
termOpRecord = new(entity.MachineTermOp)
|
||||
|
||||
termOpRecord.CreateTime = &now
|
||||
termOpRecord.Creator = la.Username
|
||||
termOpRecord.CreatorId = la.Id
|
||||
|
||||
termOpRecord.MachineId = cli.Info.Id
|
||||
termOpRecord.Username = cli.Info.Username
|
||||
|
||||
// 回放文件路径为: 基础配置路径/操作日期(202301)/day/hour/randstr.cast
|
||||
recRelPath := path.Join(now.Format("200601"), fmt.Sprintf("%d", now.Day()), fmt.Sprintf("%d", now.Hour()))
|
||||
// 文件绝对路径
|
||||
recAbsPath := path.Join(config.GetMachine().TerminalRecPath, recRelPath)
|
||||
os.MkdirAll(recAbsPath, 0766)
|
||||
filename := fmt.Sprintf("%s.cast", stringx.RandByChars(18, stringx.LowerChars))
|
||||
f, err := os.OpenFile(path.Join(recAbsPath, filename), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0766)
|
||||
if err != nil {
|
||||
return errorx.NewBiz("创建终端回放记录文件失败: %s", err.Error())
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
termOpRecord.RecordFilePath = path.Join(recRelPath, filename)
|
||||
recorder = mcm.NewRecorder(f)
|
||||
}
|
||||
|
||||
mts, err := mcm.NewTerminalSession(stringx.Rand(16), wsConn, cli, rows, cols, recorder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mts.Start()
|
||||
defer mts.Stop()
|
||||
|
||||
if termOpRecord != nil {
|
||||
now := time.Now()
|
||||
termOpRecord.EndTime = &now
|
||||
return a.Insert(ctx, termOpRecord)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *machineTermOpAppImpl) GetPageList(condition *entity.MachineTermOp, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return a.GetRepo().GetPageList(condition, pageParam, toEntity)
|
||||
}
|
||||
@@ -8,14 +8,13 @@ import (
|
||||
type Machine struct {
|
||||
model.Model
|
||||
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Ip string `json:"ip"` // IP地址
|
||||
Port int `json:"port"` // 端口号
|
||||
Username string `json:"username"` // 用户名
|
||||
Password string `json:"password"` // 密码
|
||||
AuthCertId int `json:"authCertId"` // 授权凭证id
|
||||
TagId uint64
|
||||
TagPath string
|
||||
Ip string `json:"ip"` // IP地址
|
||||
Port int `json:"port"` // 端口号
|
||||
Username string `json:"username"` // 用户名
|
||||
Password string `json:"password"` // 密码
|
||||
AuthCertId int `json:"authCertId"` // 授权凭证id
|
||||
Status int8 `json:"status"` // 状态 1:启用;2:停用
|
||||
Remark string `json:"remark"` // 备注
|
||||
SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id
|
||||
|
||||
19
server/internal/machine/domain/entity/machine_term_op.go
Normal file
19
server/internal/machine/domain/entity/machine_term_op.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"mayfly-go/pkg/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MachineTermOp struct {
|
||||
model.DeletedModel
|
||||
|
||||
MachineId uint64 `json:"machineId"`
|
||||
Username string `json:"username"`
|
||||
RecordFilePath string `json:"recordFilePath"` // 回放文件路径
|
||||
|
||||
CreateTime *time.Time `json:"createTime"`
|
||||
CreatorId uint64 `json:"creatorId"`
|
||||
Creator string `json:"creator"`
|
||||
EndTime *time.Time `json:"endTime"`
|
||||
}
|
||||
@@ -6,7 +6,8 @@ type MachineQuery struct {
|
||||
Status int8 `json:"status" form:"status"`
|
||||
Ip string `json:"ip" form:"ip"` // IP地址
|
||||
TagPath string `json:"tagPath" form:"tagPath"`
|
||||
TagIds []uint64
|
||||
|
||||
Codes []string
|
||||
}
|
||||
|
||||
type AuthCertQuery struct {
|
||||
|
||||
@@ -12,6 +12,4 @@ type Machine interface {
|
||||
|
||||
// 分页获取机器信息列表
|
||||
GetMachineList(condition *entity.MachineQuery, pageParam *model.PageParam, toEntity *[]*vo.MachineVO, orderBy ...string) (*model.PageResult[*[]*vo.MachineVO], error)
|
||||
|
||||
Count(condition *entity.MachineQuery) int64
|
||||
}
|
||||
|
||||
14
server/internal/machine/domain/repository/machine_term_op.go
Normal file
14
server/internal/machine/domain/repository/machine_term_op.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/machine/domain/entity"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
type MachineTermOp interface {
|
||||
base.Repo[*entity.MachineTermOp]
|
||||
|
||||
// 分页获取机器终端执行记录列表
|
||||
GetPageList(condition *entity.MachineTermOp, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
}
|
||||
@@ -26,9 +26,7 @@ func (m *machineRepoImpl) GetMachineList(condition *entity.MachineQuery, pagePar
|
||||
Eq("status", condition.Status).
|
||||
Like("ip", condition.Ip).
|
||||
Like("name", condition.Name).
|
||||
In("tag_id", condition.TagIds).
|
||||
RLike("tag_path", condition.TagPath).
|
||||
OrderByAsc("tag_path")
|
||||
In("code", condition.Codes)
|
||||
|
||||
if condition.Ids != "" {
|
||||
// ,分割id转为id数组
|
||||
@@ -40,12 +38,3 @@ func (m *machineRepoImpl) GetMachineList(condition *entity.MachineQuery, pagePar
|
||||
|
||||
return gormx.PageQuery(qd, pageParam, toEntity)
|
||||
}
|
||||
|
||||
func (m *machineRepoImpl) Count(condition *entity.MachineQuery) int64 {
|
||||
where := make(map[string]any)
|
||||
if len(condition.TagIds) > 0 {
|
||||
where["tag_id"] = condition.TagIds
|
||||
}
|
||||
|
||||
return m.CountByCond(where)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/machine/domain/entity"
|
||||
"mayfly-go/internal/machine/domain/repository"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/gormx"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
type machineTermOpRepoImpl struct {
|
||||
base.RepoImpl[*entity.MachineTermOp]
|
||||
}
|
||||
|
||||
func newMachineTermOpRepoImpl() repository.MachineTermOp {
|
||||
return &machineTermOpRepoImpl{base.RepoImpl[*entity.MachineTermOp]{M: new(entity.MachineTermOp)}}
|
||||
}
|
||||
|
||||
func (m *machineTermOpRepoImpl) GetPageList(condition *entity.MachineTermOp, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
qd := gormx.NewQuery(condition).WithCondModel(condition).WithOrderBy(orderBy...)
|
||||
return gormx.PageQuery(qd, pageParam, toEntity)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ var (
|
||||
machineCropJobRepo repository.MachineCronJob = newMachineCronJobRepo()
|
||||
machineCropJobExecRepo repository.MachineCronJobExec = newMachineCronJobExecRepo()
|
||||
machineCronJobRelateRepo repository.MachineCronJobRelate = newMachineCropJobRelateRepo()
|
||||
machineTermOpRepo repository.MachineTermOp = newMachineTermOpRepoImpl()
|
||||
)
|
||||
|
||||
func GetMachineRepo() repository.Machine {
|
||||
@@ -39,3 +40,7 @@ func GetMachineCronJobExecRepo() repository.MachineCronJobExec {
|
||||
func GetMachineCronJobRelateRepo() repository.MachineCronJobRelate {
|
||||
return machineCronJobRelateRepo
|
||||
}
|
||||
|
||||
func GetMachineTermOpRepo() repository.MachineTermOp {
|
||||
return machineTermOpRepo
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ type MachineInfo struct {
|
||||
|
||||
SshTunnelMachine *MachineInfo `json:"-"` // ssh隧道机器
|
||||
EnableRecorder int8 `json:"-"` // 是否启用终端回放记录
|
||||
TagPath string `json:"tagPath"`
|
||||
TagPath []string `json:"tagPath"`
|
||||
}
|
||||
|
||||
func (m *MachineInfo) UseSshTunnel() bool {
|
||||
|
||||
@@ -140,6 +140,7 @@ type WsMsg struct {
|
||||
Rows int `json:"rows"`
|
||||
}
|
||||
|
||||
// 接收客户端ws发送过来的消息,并写入终端会话中。
|
||||
func (ts *TerminalSession) receiveWsMsg() {
|
||||
wsConn := ts.wsConn
|
||||
for {
|
||||
|
||||
@@ -11,8 +11,9 @@ import (
|
||||
|
||||
func InitMachineRouter(router *gin.RouterGroup) {
|
||||
m := &api.Machine{
|
||||
MachineApp: application.GetMachineApp(),
|
||||
TagApp: tagapp.GetTagTreeApp(),
|
||||
MachineApp: application.GetMachineApp(),
|
||||
MachineTermOpApp: application.GetMachineTermOpApp(),
|
||||
TagApp: tagapp.GetTagTreeApp(),
|
||||
}
|
||||
|
||||
machines := router.Group("machines")
|
||||
@@ -22,8 +23,6 @@ func InitMachineRouter(router *gin.RouterGroup) {
|
||||
reqs := [...]*req.Conf{
|
||||
req.NewGet("", m.Machines),
|
||||
|
||||
req.NewGet("/tags", m.MachineTags),
|
||||
|
||||
req.NewGet(":machineId/stats", m.MachineStats),
|
||||
|
||||
req.NewGet(":machineId/process", m.GetProcess),
|
||||
@@ -40,8 +39,11 @@ func InitMachineRouter(router *gin.RouterGroup) {
|
||||
|
||||
req.NewDelete(":machineId/close-cli", m.CloseCli).Log(req.NewLogSave("关闭机器客户端")).RequiredPermissionCode("machine:close-cli"),
|
||||
|
||||
// 获取机器终端回放记录的相应文件夹名或文件名,目前具有保存机器信息的权限标识才有权限查看终端回放
|
||||
req.NewGet("rec/names", m.MachineRecDirNames).RequiredPermission(saveMachineP),
|
||||
// 获取机器终端回放记录列表,目前具有保存机器信息的权限标识才有权限查看终端回放
|
||||
req.NewGet(":machineId/term-recs", m.MachineTermOpRecords).RequiredPermission(saveMachineP),
|
||||
|
||||
// 获取机器终端回放记录
|
||||
req.NewGet(":machineId/term-recs/:recId", m.MachineTermOpRecord).RequiredPermission(saveMachineP),
|
||||
}
|
||||
|
||||
req.BatchSetGroup(machines, reqs[:])
|
||||
|
||||
@@ -26,6 +26,8 @@ func InitMachineCronJobRouter(router *gin.RouterGroup) {
|
||||
|
||||
req.NewDelete(":ids", cj.Delete).Log(req.NewLogSave("删除机器计划任务")),
|
||||
|
||||
req.NewPost("/run/:key", cj.RunCronJob).Log(req.NewLogSave("手动执行计划任务")),
|
||||
|
||||
req.NewGet("/execs", cj.CronJobExecs),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package form
|
||||
|
||||
type Mongo struct {
|
||||
Id uint64 `json:"id"`
|
||||
Uri string `binding:"required" json:"uri"`
|
||||
SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id
|
||||
Name string `binding:"required" json:"name"`
|
||||
TagId uint64 `binding:"required" json:"tagId"`
|
||||
TagPath string `binding:"required" json:"tagPath"`
|
||||
Id uint64 `json:"id"`
|
||||
Uri string `binding:"required" json:"uri"`
|
||||
SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id
|
||||
Name string `binding:"required" json:"name"`
|
||||
TagId []uint64 `binding:"required" json:"tagId"`
|
||||
}
|
||||
|
||||
type MongoCommand struct {
|
||||
|
||||
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayfly-go/internal/common/consts"
|
||||
"mayfly-go/internal/mongo/api/form"
|
||||
"mayfly-go/internal/mongo/application"
|
||||
"mayfly-go/internal/mongo/domain/entity"
|
||||
@@ -30,22 +31,18 @@ func (m *Mongo) Mongos(rc *req.Ctx) {
|
||||
queryCond, page := ginx.BindQueryAndPage[*entity.MongoQuery](rc.GinCtx, new(entity.MongoQuery))
|
||||
|
||||
// 不存在可访问标签id,即没有可操作数据
|
||||
tagIds := m.TagApp.ListTagIdByAccountId(rc.GetLoginAccount().Id)
|
||||
if len(tagIds) == 0 {
|
||||
codes := m.TagApp.GetAccountResourceCodes(rc.GetLoginAccount().Id, consts.TagResourceTypeMongo, queryCond.TagPath)
|
||||
if len(codes) == 0 {
|
||||
rc.ResData = model.EmptyPageResult[any]()
|
||||
return
|
||||
}
|
||||
queryCond.TagIds = tagIds
|
||||
queryCond.Codes = codes
|
||||
|
||||
res, err := m.MongoApp.GetPageList(queryCond, page, new([]entity.Mongo))
|
||||
biz.ErrIsNil(err)
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (m *Mongo) MongoTags(rc *req.Ctx) {
|
||||
rc.ResData = m.TagApp.ListTagByAccountIdAndResource(rc.GetLoginAccount().Id, new(entity.Mongo))
|
||||
}
|
||||
|
||||
func (m *Mongo) TestConn(rc *req.Ctx) {
|
||||
form := &form.Mongo{}
|
||||
mongo := ginx.BindJsonAndCopyTo[*entity.Mongo](rc.GinCtx, form, new(entity.Mongo))
|
||||
@@ -63,7 +60,7 @@ func (m *Mongo) Save(rc *req.Ctx) {
|
||||
}(form.Uri)
|
||||
rc.ReqParam = form
|
||||
|
||||
biz.ErrIsNil(m.MongoApp.Save(rc.MetaCtx, mongo))
|
||||
biz.ErrIsNil(m.MongoApp.Save(rc.MetaCtx, mongo, form.TagId...))
|
||||
}
|
||||
|
||||
func (m *Mongo) DeleteMongo(rc *req.Ctx) {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package application
|
||||
|
||||
import "mayfly-go/internal/mongo/infrastructure/persistence"
|
||||
import (
|
||||
"mayfly-go/internal/mongo/infrastructure/persistence"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
)
|
||||
|
||||
var (
|
||||
mongoApp Mongo = newMongoAppImpl(persistence.GetMongoRepo())
|
||||
mongoApp Mongo = newMongoAppImpl(persistence.GetMongoRepo(), tagapp.GetTagTreeApp())
|
||||
)
|
||||
|
||||
func GetMongoApp() Mongo {
|
||||
|
||||
@@ -2,12 +2,15 @@ package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayfly-go/internal/common/consts"
|
||||
"mayfly-go/internal/mongo/domain/entity"
|
||||
"mayfly-go/internal/mongo/domain/repository"
|
||||
"mayfly-go/internal/mongo/mgm"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
)
|
||||
|
||||
type Mongo interface {
|
||||
@@ -16,11 +19,9 @@ type Mongo interface {
|
||||
// 分页获取机器脚本信息列表
|
||||
GetPageList(condition *entity.MongoQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
|
||||
Count(condition *entity.MongoQuery) int64
|
||||
|
||||
TestConn(entity *entity.Mongo) error
|
||||
|
||||
Save(ctx context.Context, entity *entity.Mongo) error
|
||||
Save(ctx context.Context, entity *entity.Mongo, tagIds ...uint64) error
|
||||
|
||||
// 删除数据库信息
|
||||
Delete(ctx context.Context, id uint64) error
|
||||
@@ -30,14 +31,18 @@ type Mongo interface {
|
||||
GetMongoConn(id uint64) (*mgm.MongoConn, error)
|
||||
}
|
||||
|
||||
func newMongoAppImpl(mongoRepo repository.Mongo) Mongo {
|
||||
return &mongoAppImpl{
|
||||
base.AppImpl[*entity.Mongo, repository.Mongo]{Repo: mongoRepo},
|
||||
func newMongoAppImpl(mongoRepo repository.Mongo, tagApp tagapp.TagTree) Mongo {
|
||||
app := &mongoAppImpl{
|
||||
tagApp: tagApp,
|
||||
}
|
||||
app.Repo = mongoRepo
|
||||
return app
|
||||
}
|
||||
|
||||
type mongoAppImpl struct {
|
||||
base.AppImpl[*entity.Mongo, repository.Mongo]
|
||||
|
||||
tagApp tagapp.TagTree
|
||||
}
|
||||
|
||||
// 分页获取数据库信息列表
|
||||
@@ -45,10 +50,6 @@ func (d *mongoAppImpl) GetPageList(condition *entity.MongoQuery, pageParam *mode
|
||||
return d.GetRepo().GetList(condition, pageParam, toEntity, orderBy...)
|
||||
}
|
||||
|
||||
func (d *mongoAppImpl) Count(condition *entity.MongoQuery) int64 {
|
||||
return d.GetRepo().Count(condition)
|
||||
}
|
||||
|
||||
func (d *mongoAppImpl) Delete(ctx context.Context, id uint64) error {
|
||||
mgm.CloseConn(id)
|
||||
return d.GetRepo().DeleteById(ctx, id)
|
||||
@@ -63,22 +64,45 @@ func (d *mongoAppImpl) TestConn(me *entity.Mongo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *mongoAppImpl) Save(ctx context.Context, m *entity.Mongo) error {
|
||||
func (d *mongoAppImpl) Save(ctx context.Context, m *entity.Mongo, tagIds ...uint64) error {
|
||||
oldMongo := &entity.Mongo{Name: m.Name}
|
||||
err := d.GetBy(oldMongo)
|
||||
|
||||
if m.Id == 0 {
|
||||
return d.GetRepo().Insert(ctx, m)
|
||||
if err == nil {
|
||||
return errorx.NewBiz("该名称已存在")
|
||||
}
|
||||
|
||||
resouceCode := stringx.Rand(16)
|
||||
m.Code = resouceCode
|
||||
|
||||
return d.Tx(ctx, func(ctx context.Context) error {
|
||||
return d.Insert(ctx, m)
|
||||
}, func(ctx context.Context) error {
|
||||
return d.tagApp.RelateResource(ctx, resouceCode, consts.TagResourceTypeMongo, tagIds)
|
||||
})
|
||||
}
|
||||
|
||||
// 如果存在该库,则校验修改的库是否为该库
|
||||
if err == nil && oldMongo.Id != m.Id {
|
||||
return errorx.NewBiz("该名称已存在")
|
||||
}
|
||||
|
||||
// 先关闭连接
|
||||
mgm.CloseConn(m.Id)
|
||||
return d.GetRepo().UpdateById(ctx, m)
|
||||
return d.Tx(ctx, func(ctx context.Context) error {
|
||||
return d.UpdateById(ctx, m)
|
||||
}, func(ctx context.Context) error {
|
||||
return d.tagApp.RelateResource(ctx, oldMongo.Code, consts.TagResourceTypeMongo, tagIds)
|
||||
})
|
||||
}
|
||||
|
||||
func (d *mongoAppImpl) GetMongoConn(id uint64) (*mgm.MongoConn, error) {
|
||||
return mgm.GetMongoConn(id, func() (*mgm.MongoInfo, error) {
|
||||
mongo, err := d.GetById(new(entity.Mongo), id)
|
||||
me, err := d.GetById(new(entity.Mongo), id)
|
||||
if err != nil {
|
||||
return nil, errorx.NewBiz("mongo信息不存在")
|
||||
}
|
||||
return mongo.ToMongoInfo(), nil
|
||||
return me.ToMongoInfo(d.tagApp.ListTagPathByResource(consts.TagResourceTypeMongo, me.Code)...), nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,16 +9,16 @@ import (
|
||||
type Mongo struct {
|
||||
model.Model
|
||||
|
||||
Code string `orm:"column(code)" json:"code"`
|
||||
Name string `orm:"column(name)" json:"name"`
|
||||
Uri string `orm:"column(uri)" json:"uri"`
|
||||
SshTunnelMachineId int `orm:"column(ssh_tunnel_machine_id)" json:"sshTunnelMachineId"` // ssh隧道机器id
|
||||
TagId uint64 `json:"tagId"`
|
||||
TagPath string `json:"tagPath"`
|
||||
}
|
||||
|
||||
// 转换为mongoInfo进行连接
|
||||
func (me *Mongo) ToMongoInfo() *mgm.MongoInfo {
|
||||
func (me *Mongo) ToMongoInfo(tagPath ...string) *mgm.MongoInfo {
|
||||
mongoInfo := new(mgm.MongoInfo)
|
||||
structx.Copy(mongoInfo, me)
|
||||
mongoInfo.TagPath = tagPath
|
||||
return mongoInfo
|
||||
}
|
||||
|
||||
@@ -10,5 +10,5 @@ type MongoQuery struct {
|
||||
SshTunnelMachineId uint64 // ssh隧道机器id
|
||||
TagPath string `json:"tagPath" form:"tagPath"`
|
||||
|
||||
TagIds []uint64
|
||||
Codes []string
|
||||
}
|
||||
|
||||
@@ -11,6 +11,4 @@ type Mongo interface {
|
||||
|
||||
// 分页获取列表
|
||||
GetList(condition *entity.MongoQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
|
||||
Count(condition *entity.MongoQuery) int64
|
||||
}
|
||||
|
||||
@@ -20,16 +20,6 @@ func newMongoRepo() repository.Mongo {
|
||||
func (d *mongoRepoImpl) GetList(condition *entity.MongoQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
qd := gormx.NewQuery(new(entity.Mongo)).
|
||||
Like("name", condition.Name).
|
||||
In("tag_id", condition.TagIds).
|
||||
RLike("tag_path", condition.TagPath).
|
||||
OrderByAsc("tag_path")
|
||||
In("code", condition.Codes)
|
||||
return gormx.PageQuery(qd, pageParam, toEntity)
|
||||
}
|
||||
|
||||
func (d *mongoRepoImpl) Count(condition *entity.MongoQuery) int64 {
|
||||
where := make(map[string]any)
|
||||
if len(condition.TagIds) > 0 {
|
||||
where["tag_id"] = condition.TagIds
|
||||
}
|
||||
return gormx.CountByCond(new(entity.Mongo), where)
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ type MongoInfo struct {
|
||||
|
||||
Uri string `json:"-"`
|
||||
|
||||
TagPath string `json:"tagPath"`
|
||||
SshTunnelMachineId int `json:"-"` // ssh隧道机器id
|
||||
TagPath []string `json:"tagPath"`
|
||||
SshTunnelMachineId int `json:"-"` // ssh隧道机器id
|
||||
}
|
||||
|
||||
func (mi *MongoInfo) Conn() (*MongoConn, error) {
|
||||
|
||||
@@ -23,8 +23,6 @@ func InitMongoRouter(router *gin.RouterGroup) {
|
||||
// 获取所有mongo列表
|
||||
req.NewGet("", ma.Mongos),
|
||||
|
||||
req.NewGet("/tags", ma.MongoTags),
|
||||
|
||||
req.NewPost("/test-conn", ma.TestConn),
|
||||
|
||||
req.NewPost("", ma.Save).Log(req.NewLogSave("mongo-保存信息")),
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
package form
|
||||
|
||||
type Redis struct {
|
||||
Id uint64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host" binding:"required"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Mode string `json:"mode"`
|
||||
Db string `json:"db"`
|
||||
SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id
|
||||
TagId uint64 `binding:"required" json:"tagId"`
|
||||
TagPath string `binding:"required" json:"tagPath"`
|
||||
Remark string `json:"remark"`
|
||||
Id uint64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host" binding:"required"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Mode string `json:"mode"`
|
||||
Db string `json:"db"`
|
||||
SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id
|
||||
TagId []uint64 `binding:"required" json:"tagId"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
type Rename struct {
|
||||
|
||||
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayfly-go/internal/common/consts"
|
||||
"mayfly-go/internal/redis/api/form"
|
||||
"mayfly-go/internal/redis/api/vo"
|
||||
"mayfly-go/internal/redis/application"
|
||||
@@ -31,22 +32,18 @@ func (r *Redis) RedisList(rc *req.Ctx) {
|
||||
queryCond, page := ginx.BindQueryAndPage[*entity.RedisQuery](rc.GinCtx, new(entity.RedisQuery))
|
||||
|
||||
// 不存在可访问标签id,即没有可操作数据
|
||||
tagIds := r.TagApp.ListTagIdByAccountId(rc.GetLoginAccount().Id)
|
||||
if len(tagIds) == 0 {
|
||||
codes := r.TagApp.GetAccountResourceCodes(rc.GetLoginAccount().Id, consts.TagResourceTypeRedis, queryCond.TagPath)
|
||||
if len(codes) == 0 {
|
||||
rc.ResData = model.EmptyPageResult[any]()
|
||||
return
|
||||
}
|
||||
queryCond.TagIds = tagIds
|
||||
queryCond.Codes = codes
|
||||
|
||||
res, err := r.RedisApp.GetPageList(queryCond, page, new([]vo.Redis))
|
||||
biz.ErrIsNil(err)
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (r *Redis) RedisTags(rc *req.Ctx) {
|
||||
rc.ResData = r.TagApp.ListTagByAccountIdAndResource(rc.GetLoginAccount().Id, new(entity.Redis))
|
||||
}
|
||||
|
||||
func (r *Redis) TestConn(rc *req.Ctx) {
|
||||
form := &form.Redis{}
|
||||
redis := ginx.BindJsonAndCopyTo[*entity.Redis](rc.GinCtx, form, new(entity.Redis))
|
||||
@@ -72,7 +69,7 @@ func (r *Redis) Save(rc *req.Ctx) {
|
||||
form.Password = "****"
|
||||
rc.ReqParam = form
|
||||
|
||||
biz.ErrIsNil(r.RedisApp.Save(rc.MetaCtx, redis))
|
||||
biz.ErrIsNil(r.RedisApp.Save(rc.MetaCtx, redis, form.TagId...))
|
||||
}
|
||||
|
||||
// 获取redis实例密码,由于数据库是加密存储,故提供该接口展示原文密码
|
||||
@@ -229,7 +226,7 @@ func (r *Redis) checkKeyAndGetRedisConn(rc *req.Ctx) (*rdm.RedisConn, string) {
|
||||
func (r *Redis) getRedisConn(rc *req.Ctx) *rdm.RedisConn {
|
||||
ri, err := r.RedisApp.GetRedisConn(getIdAndDbNum(rc.GinCtx))
|
||||
biz.ErrIsNil(err)
|
||||
biz.ErrIsNilAppendErr(r.TagApp.CanAccess(rc.GetLoginAccount().Id, ri.Info.TagPath), "%s")
|
||||
biz.ErrIsNilAppendErr(r.TagApp.CanAccess(rc.GetLoginAccount().Id, ri.Info.TagPath...), "%s")
|
||||
return ri
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,13 @@ import "time"
|
||||
|
||||
type Redis struct {
|
||||
Id *int64 `json:"id"`
|
||||
Code *string `json:"code"`
|
||||
Name *string `json:"name"`
|
||||
Host *string `json:"host"`
|
||||
Db string `json:"db"`
|
||||
Mode *string `json:"mode"`
|
||||
SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id
|
||||
Remark *string `json:"remark"`
|
||||
TagId *uint64 `json:"tagId"`
|
||||
TagPath *string `json:"tagPath"`
|
||||
CreateTime *time.Time `json:"createTime"`
|
||||
Creator *string `json:"creator"`
|
||||
CreatorId *int64 `json:"creatorId"`
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package application
|
||||
|
||||
import "mayfly-go/internal/redis/infrastructure/persistence"
|
||||
import (
|
||||
"mayfly-go/internal/redis/infrastructure/persistence"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
)
|
||||
|
||||
var (
|
||||
redisApp Redis = newRedisApp(persistence.GetRedisRepo())
|
||||
redisApp Redis = newRedisApp(persistence.GetRedisRepo(), tagapp.GetTagTreeApp())
|
||||
)
|
||||
|
||||
func GetRedisApp() Redis {
|
||||
|
||||
@@ -2,12 +2,15 @@ package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayfly-go/internal/common/consts"
|
||||
"mayfly-go/internal/redis/domain/entity"
|
||||
"mayfly-go/internal/redis/domain/repository"
|
||||
"mayfly-go/internal/redis/rdm"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@@ -18,12 +21,10 @@ type Redis interface {
|
||||
// 分页获取机器脚本信息列表
|
||||
GetPageList(condition *entity.RedisQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
|
||||
Count(condition *entity.RedisQuery) int64
|
||||
|
||||
// 测试连接
|
||||
TestConn(re *entity.Redis) error
|
||||
|
||||
Save(ctx context.Context, re *entity.Redis) error
|
||||
Save(ctx context.Context, re *entity.Redis, tagIds ...uint64) error
|
||||
|
||||
// 删除数据库信息
|
||||
Delete(ctx context.Context, id uint64) error
|
||||
@@ -34,14 +35,18 @@ type Redis interface {
|
||||
GetRedisConn(id uint64, db int) (*rdm.RedisConn, error)
|
||||
}
|
||||
|
||||
func newRedisApp(redisRepo repository.Redis) Redis {
|
||||
return &redisAppImpl{
|
||||
base.AppImpl[*entity.Redis, repository.Redis]{Repo: redisRepo},
|
||||
func newRedisApp(redisRepo repository.Redis, tagApp tagapp.TagTree) Redis {
|
||||
app := &redisAppImpl{
|
||||
tagApp: tagApp,
|
||||
}
|
||||
app.Repo = redisRepo
|
||||
return app
|
||||
}
|
||||
|
||||
type redisAppImpl struct {
|
||||
base.AppImpl[*entity.Redis, repository.Redis]
|
||||
|
||||
tagApp tagapp.TagTree
|
||||
}
|
||||
|
||||
// 分页获取redis列表
|
||||
@@ -49,10 +54,6 @@ func (r *redisAppImpl) GetPageList(condition *entity.RedisQuery, pageParam *mode
|
||||
return r.GetRepo().GetRedisList(condition, pageParam, toEntity, orderBy...)
|
||||
}
|
||||
|
||||
func (r *redisAppImpl) Count(condition *entity.RedisQuery) int64 {
|
||||
return r.GetRepo().Count(condition)
|
||||
}
|
||||
|
||||
func (r *redisAppImpl) TestConn(re *entity.Redis) error {
|
||||
db := 0
|
||||
if re.Db != "" {
|
||||
@@ -67,7 +68,7 @@ func (r *redisAppImpl) TestConn(re *entity.Redis) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *redisAppImpl) Save(ctx context.Context, re *entity.Redis) error {
|
||||
func (r *redisAppImpl) Save(ctx context.Context, re *entity.Redis, tagIds ...uint64) error {
|
||||
// 查找是否存在该库
|
||||
oldRedis := &entity.Redis{Host: re.Host}
|
||||
if re.SshTunnelMachineId > 0 {
|
||||
@@ -80,7 +81,15 @@ func (r *redisAppImpl) Save(ctx context.Context, re *entity.Redis) error {
|
||||
return errorx.NewBiz("该实例已存在")
|
||||
}
|
||||
re.PwdEncrypt()
|
||||
return r.Insert(ctx, re)
|
||||
|
||||
resouceCode := stringx.Rand(16)
|
||||
re.Code = resouceCode
|
||||
|
||||
return r.Tx(ctx, func(ctx context.Context) error {
|
||||
return r.Insert(ctx, re)
|
||||
}, func(ctx context.Context) error {
|
||||
return r.tagApp.RelateResource(ctx, resouceCode, consts.TagResourceTypeRedis, tagIds)
|
||||
})
|
||||
}
|
||||
|
||||
// 如果存在该库,则校验修改的库是否为该库
|
||||
@@ -94,8 +103,13 @@ func (r *redisAppImpl) Save(ctx context.Context, re *entity.Redis) error {
|
||||
rdm.CloseConn(re.Id, db)
|
||||
}
|
||||
}
|
||||
|
||||
re.PwdEncrypt()
|
||||
return r.UpdateById(ctx, re)
|
||||
return r.Tx(ctx, func(ctx context.Context) error {
|
||||
return r.UpdateById(ctx, re)
|
||||
}, func(ctx context.Context) error {
|
||||
return r.tagApp.RelateResource(ctx, oldRedis.Code, consts.TagResourceTypeRedis, tagIds)
|
||||
})
|
||||
}
|
||||
|
||||
// 删除Redis信息
|
||||
@@ -122,6 +136,6 @@ func (r *redisAppImpl) GetRedisConn(id uint64, db int) (*rdm.RedisConn, error) {
|
||||
}
|
||||
re.PwdDecrypt()
|
||||
|
||||
return re.ToRedisInfo(db), nil
|
||||
return re.ToRedisInfo(db, r.tagApp.ListTagPathByResource(consts.TagResourceTypeRedis, re.Code)...), nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,6 +13,6 @@ type RedisQuery struct {
|
||||
SshTunnelMachineId int `orm:"column(ssh_tunnel_machine_id)" json:"sshTunnelMachineId"` // ssh隧道机器id
|
||||
Remark string
|
||||
|
||||
TagIds []uint64
|
||||
Codes []string
|
||||
TagPath string `form:"tagPath"`
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
type Redis struct {
|
||||
model.Model
|
||||
|
||||
Code string `orm:"column(code)" json:"code"`
|
||||
Name string `orm:"column(name)" json:"name"`
|
||||
Host string `orm:"column(host)" json:"host"`
|
||||
Mode string `json:"mode"`
|
||||
@@ -18,8 +19,6 @@ type Redis struct {
|
||||
Db string `orm:"column(database)" json:"db"`
|
||||
SshTunnelMachineId int `orm:"column(ssh_tunnel_machine_id)" json:"sshTunnelMachineId"` // ssh隧道机器id
|
||||
Remark string
|
||||
TagId uint64
|
||||
TagPath string
|
||||
}
|
||||
|
||||
func (r *Redis) PwdEncrypt() {
|
||||
@@ -33,9 +32,10 @@ func (r *Redis) PwdDecrypt() {
|
||||
}
|
||||
|
||||
// 转换为redisInfo进行连接
|
||||
func (re *Redis) ToRedisInfo(db int) *rdm.RedisInfo {
|
||||
func (re *Redis) ToRedisInfo(db int, tagPath ...string) *rdm.RedisInfo {
|
||||
redisInfo := new(rdm.RedisInfo)
|
||||
structx.Copy(redisInfo, re)
|
||||
redisInfo.Db = db
|
||||
redisInfo.TagPath = tagPath
|
||||
return redisInfo
|
||||
}
|
||||
|
||||
@@ -11,6 +11,4 @@ type Redis interface {
|
||||
|
||||
// 分页获取机器信息列表
|
||||
GetRedisList(condition *entity.RedisQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
|
||||
Count(condition *entity.RedisQuery) int64
|
||||
}
|
||||
|
||||
@@ -20,17 +20,6 @@ func newRedisRepo() repository.Redis {
|
||||
func (r *redisRepoImpl) GetRedisList(condition *entity.RedisQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
qd := gormx.NewQuery(new(entity.Redis)).
|
||||
Like("host", condition.Host).
|
||||
In("tag_id", condition.TagIds).
|
||||
RLike("tag_path", condition.TagPath).
|
||||
OrderByAsc("tag_path")
|
||||
In("code", condition.Codes)
|
||||
return gormx.PageQuery(qd, pageParam, toEntity)
|
||||
}
|
||||
|
||||
func (r *redisRepoImpl) Count(condition *entity.RedisQuery) int64 {
|
||||
where := make(map[string]any)
|
||||
if len(condition.TagIds) > 0 {
|
||||
where["tag_id"] = condition.TagIds
|
||||
}
|
||||
|
||||
return gormx.CountByCond(new(entity.Redis), where)
|
||||
}
|
||||
|
||||
@@ -31,9 +31,9 @@ type RedisInfo struct {
|
||||
Username string `json:"-"`
|
||||
Password string `json:"-"`
|
||||
|
||||
Name string `json:"-"`
|
||||
TagPath string `json:"tagPath"`
|
||||
SshTunnelMachineId int `json:"-"`
|
||||
Name string `json:"-"`
|
||||
TagPath []string `json:"tagPath"`
|
||||
SshTunnelMachineId int `json:"-"`
|
||||
}
|
||||
|
||||
func (r *RedisInfo) Conn() (*RedisConn, error) {
|
||||
|
||||
@@ -26,8 +26,6 @@ func InitRedisRouter(router *gin.RouterGroup) {
|
||||
// 获取redis list
|
||||
req.NewGet("", rs.RedisList),
|
||||
|
||||
req.NewGet("/tags", rs.RedisTags),
|
||||
|
||||
req.NewPost("/test-conn", rs.TestConn),
|
||||
|
||||
req.NewPost("", rs.Save).Log(req.NewLogSave("redis-保存信息")),
|
||||
|
||||
@@ -43,9 +43,10 @@ func (m *syslogAppImpl) SaveFromReq(req *req.Ctx) {
|
||||
syslog.CreateTime = time.Now()
|
||||
syslog.Creator = lg.Username
|
||||
syslog.CreatorId = lg.Id
|
||||
syslog.Description = req.GetLogInfo().Description
|
||||
|
||||
if req.GetLogInfo().LogResp {
|
||||
logInfo := req.GetLogInfo()
|
||||
syslog.Description = logInfo.Description
|
||||
if logInfo.LogResp {
|
||||
respB, _ := json.Marshal(req.ResData)
|
||||
syslog.Resp = string(respB)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user