mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 23:40:24 +08:00
@@ -11,7 +11,7 @@ module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
sourceType: 'module',
|
||||
},
|
||||
extends: ['plugin:vue/vue3-essential', 'plugin:vue/essential', 'eslint:recommended'],
|
||||
extends: ['plugin:vue/essential', 'eslint:recommended'],
|
||||
plugins: ['vue', '@typescript-eslint'],
|
||||
overrides: [
|
||||
{
|
||||
@@ -35,9 +35,8 @@ module.exports = {
|
||||
'@typescript-eslint/ban-types': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-redeclare': 'error',
|
||||
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [2],
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'vue/custom-event-name-casing': 'off',
|
||||
'vue/attributes-order': 'off',
|
||||
'vue/one-component-per-file': 'off',
|
||||
@@ -53,6 +52,7 @@ module.exports = {
|
||||
'vue/no-arrow-functions-in-watch': 'off',
|
||||
'vue/no-template-key': 'off',
|
||||
'vue/no-v-html': 'off',
|
||||
'vue/no-unused-vars': 'off',
|
||||
'vue/comment-directive': 'off',
|
||||
'vue/no-parsing-error': 'off',
|
||||
'vue/no-deprecated-v-on-native-modifier': 'off',
|
||||
@@ -67,7 +67,7 @@ module.exports = {
|
||||
'generator-star-spacing': 'off',
|
||||
'no-unreachable': 'off',
|
||||
'no-multiple-template-root': 'off',
|
||||
'no-unused-vars': 'error',
|
||||
'no-unused-vars': 'off',
|
||||
'no-v-model-argument': 'off',
|
||||
'no-case-declarations': 'off',
|
||||
// 'no-console': 'error',
|
||||
|
||||
1
frontend/src/assets/icon/es/es-color.svg
Normal file
1
frontend/src/assets/icon/es/es-color.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M96.426667 649.173333H712.96a137.173333 137.173333 0 0 0 0-274.346666H96.426667c-12.8 43.52-19.626667 89.514667-19.626667 137.173333s6.826667 93.696 19.626667 137.173333z" fill="#07A5DE" p-id="6101"></path><path d="M563.2 25.6A486.4 486.4 0 0 0 125.354667 299.946667H837.546667c52.096 0 97.450667-29.013333 120.661333-71.808A485.76 485.76 0 0 0 563.2 25.6z" fill="#EFBF19" p-id="6102"></path><path d="M942.421333 816.64a137.258667 137.258667 0 0 0-129.749333-92.586667H125.312A486.4 486.4 0 0 0 563.2 998.4c153.344 0 290.090667-70.954667 379.221333-181.76z" fill="#3EBEB1" p-id="6103"></path><path d="M506.197333 649.173333c12.8-43.52 19.626667-89.514667 19.626667-137.173333s-6.826667-93.696-19.626667-137.173333H96.469333c-12.8 43.52-19.626667 89.514667-19.626666 137.173333s6.826667 93.696 19.626666 137.173333h409.728z" fill="#231F20" p-id="6104"></path><path d="M477.269333 724.053333H125.354667a488.533333 488.533333 0 0 0 175.957333 197.888 488.533333 488.533333 0 0 0 175.957333-197.930666z" fill="#019B8F" p-id="6105"></path><path d="M301.312 102.058667a488.533333 488.533333 0 0 1 175.957333 197.930666H125.354667a488.533333 488.533333 0 0 1 175.957333-197.930666z" fill="#D8A22A" p-id="6106"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
frontend/src/assets/icon/es/es.svg
Normal file
1
frontend/src/assets/icon/es/es.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M465.664 679.168c105.301333 0.597333 172.970667 1.066667 202.922667 1.450667 20.48 0.256 36.181333 0.426667 47.274666 0.469333h3.84c45.824 0 84.096 8.533333 114.901334 25.258667 31.189333 16.938667 54.826667 42.368 70.826666 76.245333l1.152 2.517333a24.106667 24.106667 0 0 1-1.621333 3.413334c-46.336 67.84-101.034667 116.565333-164.096 146.346666-63.146667 29.824-134.613333 40.704-214.485333 32.469334-159.232-16.384-283.477333-106.24-372.352-269.994667a5.973333 5.973333 0 0 1 3.584-8.618667c13.653333-3.968 27.733333-6.528 41.941333-7.594666 91.306667-1.365333 170.538667-1.877333 238.165333-1.962667h27.946667z m44.885333 63.829333l-0.64 1.152c-3.754667 6.485333-9.386667 15.36-16.128 25.6l-2.645333 3.925334-1.578667 2.346666c-21.205333 31.445333-51.072 72.234667-70.784 94.464 64.853333 34.304 133.162667 45.44 227.157334 27.52 95.146667-18.090667 145.450667-52.565333 175.829333-114.090666-5.034667-10.581333-14.592-19.285333-31.488-27.733334-12.8-6.4-32.426667-11.050667-58.752-14.250666l-221.013333 1.066666z m-257.578666-5.546666l1.237333 1.536c21.504 26.112 67.712 72.277333 96.896 95.786666 15.146667-14.08 29.098667-29.397333 41.642667-45.824 13.952-18.261333 24.149333-32.64 35.370666-52.821333l-175.146666 1.322667z m471.296-360.874667c38.229333 5.077333 67.626667 18.944 88.448 41.301333 20.736 22.229333 33.024 52.992 36.565333 92.373334 3.626667 39.722667-5.76 71.808-27.733333 96.426666-20.906667 23.381333-53.461333 40.106667-97.877334 49.706667l-2.645333 0.597333-2.816 0.554667H144.725333a8.021333 8.021333 0 0 1-7.893333-6.485333 1545.173333 1545.173333 0 0 1-0.298667-1.578667c-12.373333-62.378667-18.517333-106.666667-18.517333-132.906667 0-38.570667 5.888-81.962667 17.706667-130.261333l1.066666-4.394667a7.082667 7.082667 0 0 1 6.826667-5.333333h580.650667zM197.546667 442.88l-0.853334 2.688c-7.509333 24.064-12.544 44.330667-12.117333 70.954667 0 30.293333 5.418667 54.272 13.653333 81.664h283.050667l0.341333-2.218667 0.469334-3.2c3.541333-24.448 4.010667-47.701333 4.010666-76.544 0-30.805333-1.066667-51.541333-6.4-75.264l-282.154666 1.92z m493.397333-3.029333l-131.797333 1.024 0.512 2.474666c4.48 22.357333 6.741333 43.861333 6.741333 73.216 0 30.421333-2.432 53.76-7.552 79.189334l134.826667-0.170667 1.962666-0.213333c28.16-2.901333 49.194667-7.210667 62.421334-23.04 11.52-13.866667 17.152-32.469333 17.152-55.765334 0-24.746667-6.272-42.624-19.456-54.826666-13.653333-12.714667-34.474667-19.2-61.994667-21.674667l-2.816-0.213333z m49.877333-342.784c63.104 29.824 117.845333 78.592 164.181334 146.346666l1.536 2.304a23.466667 23.466667 0 0 1-1.066667 3.669334c-16 33.92-39.594667 59.306667-70.784 76.245333-30.805333 16.768-69.12 25.258667-114.986667 25.258667-11.178667 0-28.16 0.213333-50.944 0.469333-45.098667 0.597333-112.597333 1.408-202.965333 1.493333h-14.122667c-70.613333 0-154.453333-0.512-251.733333-1.962666a207.061333 207.061333 0 0 1-42.24-7.594667 5.973333 5.973333 0 0 1-3.626667-8.618667C242.986667 170.922667 367.146667 81.066667 526.378667 64.682667c79.829333-8.277333 151.296 2.56 214.4 32.426666z m-102.101333 28.501333c-85.205333-15.36-143.957333-4.010667-213.717333 27.221333 11.648 13.312 26.410667 33.621333 40.874666 55.04l1.578667 2.389334 2.56 3.754666 3.498667 5.376 2.346666 3.584 1.237334 1.877334c18.688 28.757333 35.157333 56.746667 41.728 70.613333h213.674666l2.474667-0.298667 2.56-0.341333c21.290667-2.986667 38.144-10.794667 55.978667-19.754667 17.408-8.576 30.122667-18.304 40.106666-29.866666-49.493333-63.018667-108.586667-104.106667-194.901333-119.594667zM367.744 186.453333c-12.458667 10.069333-34.304 29.44-56.192 50.048l-1.706667 1.621334-3.328 3.157333-3.498666 3.328-1.877334 1.792-2.048 2.005333c-17.322667 16.64-33.578667 33.109333-44.501333 45.909334l179.797333-1.536-1.109333-1.877334a3067.264 3067.264 0 0 1-12.672-21.418666l-11.776-20.053334-2.474667-4.053333-2.56-4.266667-1.152-2.005333-1.237333-2.005333c-12.458667-20.693333-24.917333-40.405333-33.706667-50.645334z" fill="#2c2c2c" p-id="5739"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -1,5 +1,5 @@
|
||||
import request from './request';
|
||||
import { useApiFetch } from '@/hooks/useRequest';
|
||||
import { RequestOptions, useApiFetch } from '@/hooks/useRequest';
|
||||
|
||||
/**
|
||||
* 可用于各模块定义各自api请求
|
||||
@@ -49,7 +49,7 @@ class Api {
|
||||
* @param reqOptions 其他可选值
|
||||
* @returns
|
||||
*/
|
||||
useApi<T>(params: any = null, reqOptions: RequestInit = {}) {
|
||||
useApi<T>(params: any = null, reqOptions?: RequestOptions) {
|
||||
return useApiFetch<T>(this, params, reqOptions);
|
||||
}
|
||||
|
||||
@@ -59,8 +59,8 @@ class Api {
|
||||
*/
|
||||
async request(param: any = null, options: any = {}): Promise<any> {
|
||||
const { execute, data } = this.useApi(param, options);
|
||||
await execute();
|
||||
return data.value;
|
||||
const res = await execute();
|
||||
return data.value || res;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,8 @@ export const ResourceTypeEnum = {
|
||||
Db: EnumValue.of(2, '数据库实例').setExtra({ icon: 'Coin', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
|
||||
Redis: EnumValue.of(3, 'redis').setExtra({ icon: 'icon redis/redis', iconColor: 'var(--el-color-danger)' }).tagTypeInfo(),
|
||||
Mongo: EnumValue.of(4, 'mongo').setExtra({ icon: 'icon mongo/mongo', iconColor: 'var(--el-color-success)' }).tagTypeDanger(),
|
||||
AuthCert: EnumValue.of(5, '授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
|
||||
Es: EnumValue.of(6, 'ES实例').setExtra({ icon: 'Coin', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
|
||||
};
|
||||
|
||||
// 标签关联的资源类型
|
||||
@@ -24,9 +26,10 @@ export const TagResourceTypeEnum = {
|
||||
|
||||
Machine: ResourceTypeEnum.Machine,
|
||||
DbInstance: ResourceTypeEnum.Db,
|
||||
EsInstance: ResourceTypeEnum.Es,
|
||||
Redis: ResourceTypeEnum.Redis,
|
||||
Mongo: ResourceTypeEnum.Mongo,
|
||||
AuthCert: EnumValue.of(5, '授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
|
||||
AuthCert: ResourceTypeEnum.AuthCert,
|
||||
|
||||
Db: EnumValue.of(22, '数据库').setExtra({ icon: 'Coin' }),
|
||||
};
|
||||
@@ -37,4 +40,5 @@ export const TagResourceTypePath = {
|
||||
|
||||
DbInstanceAuthCert: `${TagResourceTypeEnum.DbInstance.value}/${TagResourceTypeEnum.AuthCert.value}`,
|
||||
Db: `${TagResourceTypeEnum.DbInstance.value}/${TagResourceTypeEnum.AuthCert.value}/${TagResourceTypeEnum.Db.value}`,
|
||||
Es: `${TagResourceTypeEnum.EsInstance.value}/${TagResourceTypeEnum.AuthCert.value}`,
|
||||
};
|
||||
|
||||
@@ -30,6 +30,18 @@ export function formatByteSize(size: number, fixed = 2) {
|
||||
return parseFloat((size / Math.pow(base, exponent)).toFixed(fixed)) + units[exponent];
|
||||
}
|
||||
|
||||
export function formatDocSize(size: number, fixed = 2) {
|
||||
if (size === 0) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
const units = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
|
||||
const base = 1000;
|
||||
const exponent = Math.floor(Math.log(size) / Math.log(base));
|
||||
|
||||
return parseFloat((size / Math.pow(base, exponent)).toFixed(fixed)) + units[exponent];
|
||||
}
|
||||
|
||||
/**
|
||||
* 容量转为对应的字节大小,如 1KB转为 1024
|
||||
* @param sizeString 1kb 1gb等
|
||||
@@ -86,8 +98,8 @@ export function formatTime(time: number, unit: string = 's') {
|
||||
let result = '';
|
||||
|
||||
const timeUnits = Object.entries(units).map(([unit, duration]) => {
|
||||
const value = Math.floor(seconds / duration);
|
||||
seconds %= duration;
|
||||
const value = Math.floor(seconds / (duration as any));
|
||||
seconds %= duration as any;
|
||||
return { value, unit };
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
:style="`top: ${state.dropdown.y + 5}px;left: ${state.dropdown.x}px;`"
|
||||
:key="Math.random()"
|
||||
v-show="state.isShow && !allHide"
|
||||
@contextmenu="headerContextmenuClick"
|
||||
>
|
||||
<ul class="el-dropdown-menu">
|
||||
<template v-for="(v, k) in state.dropdownList">
|
||||
@@ -125,6 +126,10 @@ const onCurrentContextmenuClick = (ci: ContextmenuItem) => {
|
||||
emit('currentContextmenuClick', { id: ci.clickId, item: state.item });
|
||||
};
|
||||
|
||||
const headerContextmenuClick = (event: any, data: any) => {
|
||||
event.preventDefault(); // 阻止默认的右击菜单行为
|
||||
};
|
||||
|
||||
// 打开右键菜单:判断是否固定,固定则不显示关闭按钮
|
||||
const openContextmenu = (item: any) => {
|
||||
state.item = item;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<div class="monaco-editor-custom relative h-full" style="border: 1px solid var(--el-border-color-light, #ebeef5)">
|
||||
<div class="monaco-editor-custom relative h-full">
|
||||
<div class="monaco-editor-content" ref="monacoTextareaRef" :style="{ height: height }"></div>
|
||||
<el-select v-if="canChangeMode" class="code-mode-select" v-model="languageMode" @change="changeLanguage" filterable>
|
||||
<el-option v-for="mode in languageArr" :key="mode.value" :label="mode.label" :value="mode.value"> </el-option>
|
||||
<el-option v-for="mode in languageArr" :key="mode.value" :label="mode.label" :value="mode.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, toRefs, reactive, onMounted, onBeforeUnmount, useTemplateRef, Ref } from 'vue';
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import * as monaco from 'monaco-editor';
|
||||
// 相关语言
|
||||
import 'monaco-editor/esm/vs/basic-languages/shell/shell.contribution.js';
|
||||
import 'monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution.js';
|
||||
@@ -31,7 +31,6 @@ import 'monaco-editor/esm/vs/editor/contrib/format//browser/formatActions.js';
|
||||
// 提示
|
||||
import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestController.js';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestInlineCompletions.js';
|
||||
|
||||
import { editor, languages } from 'monaco-editor';
|
||||
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker';
|
||||
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
|
||||
@@ -134,6 +133,7 @@ const defaultOptions = {
|
||||
theme: 'SolarizedLight',
|
||||
automaticLayout: true, //自适应宽高布局
|
||||
foldingStrategy: 'indentation', //代码可分小段折叠
|
||||
folding: true,
|
||||
roundedSelection: false, // 禁用选择文本背景的圆角
|
||||
matchBrackets: 'near',
|
||||
linkedEditing: true,
|
||||
@@ -149,7 +149,13 @@ const defaultOptions = {
|
||||
minimap: {
|
||||
enabled: false, // 不要小地图
|
||||
},
|
||||
};
|
||||
renderLineHighlight: 'all',
|
||||
selectOnLineNumbers: false,
|
||||
readOnly: false,
|
||||
scrollBeyondLastLine: false,
|
||||
lineNumbers: 'on',
|
||||
lineNumbersMinChars: 3,
|
||||
} as editor.IStandaloneEditorConstructionOptions;
|
||||
|
||||
const monacoTextareaRef: Ref<any> = useTemplateRef('monacoTextareaRef');
|
||||
|
||||
@@ -221,7 +227,8 @@ const initMonacoEditorIns = () => {
|
||||
monaco.editor.defineTheme('SolarizedLight', SolarizedLight);
|
||||
defaultOptions.language = state.languageMode;
|
||||
defaultOptions.theme = themeConfig.value.editorTheme;
|
||||
monacoEditorIns = monaco.editor.create(monacoTextareaRef.value, Object.assign(defaultOptions, props.options as any));
|
||||
let options = Object.assign(defaultOptions, props.options as any);
|
||||
monacoEditorIns = monaco.editor.create(monacoTextareaRef.value, options);
|
||||
|
||||
// 监听内容改变,双向绑定
|
||||
monacoEditorIns.onDidChangeModelContent(() => {
|
||||
@@ -317,5 +324,8 @@ defineExpose({ getEditor, format, focus });
|
||||
top: 10px;
|
||||
max-width: 130px;
|
||||
}
|
||||
|
||||
border: 1px solid var(--el-border-color-light, #ebeef5);
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -38,14 +38,19 @@ const useCustomFetch = createFetch({
|
||||
return { options };
|
||||
},
|
||||
async afterFetch(ctx) {
|
||||
const result: Result = await ctx.response.json();
|
||||
ctx.data = result;
|
||||
ctx.data = await ctx.response.json();
|
||||
return ctx;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function useApiFetch<T>(api: Api, params: any = null, reqOptions: RequestInit = {}) {
|
||||
interface EsReq {
|
||||
esProxyReq: boolean;
|
||||
}
|
||||
|
||||
export interface RequestOptions extends RequestInit, EsReq {}
|
||||
|
||||
export function useApiFetch<T>(api: Api, params: any = null, reqOptions?: RequestOptions) {
|
||||
const uaf = useCustomFetch<T>(api.url, {
|
||||
async beforeFetch({ url, options }) {
|
||||
options.method = api.method;
|
||||
@@ -90,13 +95,20 @@ export function useApiFetch<T>(api: Api, params: any = null, reqOptions: Request
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
onFetchError: (ctx) => {
|
||||
if (reqOptions?.esProxyReq) {
|
||||
uaf.data = { value: JSON.parse(ctx.data) };
|
||||
return Promise.resolve(uaf.data);
|
||||
}
|
||||
return ctx;
|
||||
},
|
||||
}) as any;
|
||||
|
||||
// 统一处理后的返回结果,如果直接使用uaf.data,则数据会出现由{code: x, data: {}} -> data 的变化导致某些结果绑定报错
|
||||
const data = ref<T | null>(null);
|
||||
return {
|
||||
execute: async function () {
|
||||
await execCustomFetch(uaf);
|
||||
await execCustomFetch(uaf, reqOptions);
|
||||
data.value = uaf.data.value;
|
||||
},
|
||||
isFetching: uaf.isFetching,
|
||||
@@ -108,37 +120,44 @@ export function useApiFetch<T>(api: Api, params: any = null, reqOptions: Request
|
||||
let refreshingToken = false;
|
||||
let queue: any[] = [];
|
||||
|
||||
async function execCustomFetch(uaf: UseFetchReturn<any>) {
|
||||
async function execCustomFetch(uaf: UseFetchReturn<any>, reqOptions?: RequestOptions) {
|
||||
try {
|
||||
await uaf.execute(true);
|
||||
} catch (e: any) {
|
||||
const rejectPromise = Promise.reject(e);
|
||||
if (!reqOptions?.esProxyReq) {
|
||||
const rejectPromise = Promise.reject(e);
|
||||
|
||||
if (e?.name == 'AbortError') {
|
||||
console.log('请求已取消');
|
||||
if (e?.name == 'AbortError') {
|
||||
console.log('请求已取消');
|
||||
return rejectPromise;
|
||||
}
|
||||
|
||||
const respStatus = uaf.response.value?.status;
|
||||
if (respStatus == 404) {
|
||||
ElMessage.error('url not found');
|
||||
return rejectPromise;
|
||||
}
|
||||
if (respStatus == 500) {
|
||||
ElMessage.error('server error');
|
||||
return rejectPromise;
|
||||
}
|
||||
|
||||
console.error(e);
|
||||
ElMessage.error('network error');
|
||||
return rejectPromise;
|
||||
}
|
||||
|
||||
const respStatus = uaf.response.value?.status;
|
||||
if (respStatus == 404) {
|
||||
ElMessage.error('url not found');
|
||||
return rejectPromise;
|
||||
}
|
||||
if (respStatus == 500) {
|
||||
ElMessage.error('server error');
|
||||
return rejectPromise;
|
||||
}
|
||||
|
||||
console.error(e);
|
||||
ElMessage.error('network error');
|
||||
return rejectPromise;
|
||||
}
|
||||
|
||||
const result: Result = uaf.data.value as any;
|
||||
const result: Result & { error: any; status: number } = uaf.data.value as any;
|
||||
if (!result) {
|
||||
ElMessage.error('network request failed');
|
||||
return Promise.reject(result);
|
||||
}
|
||||
// es代理请求
|
||||
if (reqOptions?.esProxyReq) {
|
||||
uaf.data.value = result;
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
|
||||
const resultCode = result.code;
|
||||
|
||||
@@ -154,7 +173,7 @@ async function execCustomFetch(uaf: UseFetchReturn<any>) {
|
||||
// 请求加入队列等待, 防止并发多次请求refreshToken
|
||||
return new Promise((resolve) => {
|
||||
queue.push(() => {
|
||||
resolve(execCustomFetch(uaf));
|
||||
resolve(execCustomFetch(uaf, reqOptions));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -178,13 +197,13 @@ async function execCustomFetch(uaf: UseFetchReturn<any>) {
|
||||
queue = [];
|
||||
}
|
||||
|
||||
await execCustomFetch(uaf);
|
||||
await execCustomFetch(uaf, reqOptions);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果提示没有权限,则跳转至无权限页面
|
||||
if (resultCode === ResultEnum.NO_PERMISSION) {
|
||||
router.push({
|
||||
await router.push({
|
||||
path: URL_401,
|
||||
});
|
||||
return Promise.reject(result);
|
||||
|
||||
@@ -7,12 +7,16 @@ export default {
|
||||
detail: 'Details',
|
||||
add: 'Add',
|
||||
save: 'Save',
|
||||
close: 'Close',
|
||||
download: 'Download',
|
||||
upload: 'Upload',
|
||||
remove: 'Remove',
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
submit: 'Submit',
|
||||
operation: 'Operations',
|
||||
name: 'Name',
|
||||
version: 'Version',
|
||||
code: 'Code',
|
||||
remark: 'Remark',
|
||||
status: 'Status',
|
||||
@@ -48,9 +52,11 @@ export default {
|
||||
previousStep: 'Previous Step',
|
||||
nextStep: 'Next Step',
|
||||
copy: 'Copy',
|
||||
copyCell: 'Copy Cell',
|
||||
search: 'Search',
|
||||
pleaseInput: 'Please enter {label}',
|
||||
pleaseSelect: 'Please select {label}',
|
||||
pleaseSelectOne: 'Please select Only One Data',
|
||||
formValidationError: 'Please check the form',
|
||||
createTitle: 'Create {name}',
|
||||
editTitle: 'Edit {name}',
|
||||
@@ -61,6 +67,8 @@ export default {
|
||||
deleteSuccess: 'delete successfully',
|
||||
operateSuccess: 'operate successfully',
|
||||
fieldNotEmpty: '{field} cannot be empty',
|
||||
selectAll: 'Select all',
|
||||
MultiPlaceholder: 'Multiple are separated by commas',
|
||||
},
|
||||
layout: {
|
||||
user: {
|
||||
|
||||
@@ -219,4 +219,11 @@ export default {
|
||||
running: 'Running',
|
||||
waitRun: 'Wait Run',
|
||||
},
|
||||
es: {
|
||||
keywordPlaceholder: 'host / name / code',
|
||||
port: 'Port',
|
||||
acName: 'Credential',
|
||||
dbInst: 'Es Instance',
|
||||
connSuccess: 'be connected successfully',
|
||||
},
|
||||
};
|
||||
|
||||
122
frontend/src/i18n/en/es.ts
Normal file
122
frontend/src/i18n/en/es.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
export default {
|
||||
es: {
|
||||
keywordPlaceholder: 'host / name / code',
|
||||
port: 'Port',
|
||||
size: 'size',
|
||||
docs: 'docs',
|
||||
health: 'health',
|
||||
aliases: 'Aliases',
|
||||
addAlias: 'Add Alias',
|
||||
specifyIdAdd: 'Specify the ID added, if id exists, then update',
|
||||
addIndex: 'Add Index',
|
||||
editIndex: 'Edit Index',
|
||||
status: 'status',
|
||||
acName: 'Credential',
|
||||
emptyTable: 'data not fund',
|
||||
connSuccess: 'be connected successfully',
|
||||
shouldTestConn: 'please test connection first',
|
||||
instance: 'ES Instance',
|
||||
instanceSave: 'Save Instance',
|
||||
instanceDel: 'Delete Instance',
|
||||
operation: 'Data Operation',
|
||||
dataSave: 'Data Save',
|
||||
dataDel: 'Data Del',
|
||||
indexName: 'Index Name',
|
||||
requireIndexName: 'Index Name Is Required',
|
||||
indexDetail: 'Index Detail',
|
||||
indexMapping: 'Mappings',
|
||||
indexStats: 'Stats',
|
||||
opViewColumns: 'Option View Columns',
|
||||
opIndex: 'Index Management',
|
||||
opSearch: 'Search',
|
||||
searchParamsPreview: 'Search Params Preview',
|
||||
opBasicSearch: 'Basic Search',
|
||||
opSeniorSearch: 'Senior Search',
|
||||
sampleMappings: 'Sample Mappings',
|
||||
copyMappings: 'Copy Mappings',
|
||||
readonlyMsg: 'The content is readOnly',
|
||||
opDashboard: 'Dashboard',
|
||||
opSettings: 'Settings',
|
||||
templates: 'Templates',
|
||||
availableSettingFields: 'Available Setting Fields',
|
||||
Reindex: 'Reindex',
|
||||
ReindexTargetIdx: 'Target Index',
|
||||
ReindexIsSync: 'Sync Able',
|
||||
ReindexDescription:
|
||||
"If a field in Mapping has been defined, you can't modify the type of the field, and you can't change the number of shards, you can use the Reindex API to solve this problem.",
|
||||
ReindexSyncDescription: 'If the amount of index data is large, we recommend that you enable asynchronous data to avoid request timeouts.',
|
||||
ReindexToOtherInst: 'To other Instance',
|
||||
ReindexSyncTask: 'Sync Task',
|
||||
makeSearchParam: 'Make Search Params',
|
||||
filterColumn: 'Filter Columns',
|
||||
searchParams: 'Search',
|
||||
searchParamsDesc: 'If no field is selected or no condition value is set, it will not take effect',
|
||||
standardSearch: 'Standard Search',
|
||||
AggregationSearch: 'Aggregation Search',
|
||||
SqlSearch: 'Sql Search',
|
||||
searchError: 'Search Error',
|
||||
execError: 'Exec Error',
|
||||
docJsonError: 'Document JSON Format Error',
|
||||
sortParams: 'Sort',
|
||||
otherParams: 'Other',
|
||||
previewParams: 'Preview',
|
||||
closeIndexConfirm: 'This operation will close index [{name}]. Do you want to continue?',
|
||||
openIndexConfirm: 'This operation will open index [{name}]. Do you want to continue?',
|
||||
clearCacheConfirm: 'This operation will clear index [{name}] cache. Do you want to continue?',
|
||||
page: {
|
||||
home: 'First Page',
|
||||
prev: 'Prev Page',
|
||||
next: 'Next Page',
|
||||
total: 'Total Count',
|
||||
changeSize: 'Change Page Size',
|
||||
},
|
||||
temp: {
|
||||
addTemp: 'Add template',
|
||||
view: 'Template Detail',
|
||||
name: 'name',
|
||||
priority: 'priority',
|
||||
index_patterns: 'patterns',
|
||||
content: 'content',
|
||||
showHide: 'show system templates',
|
||||
description: 'description',
|
||||
filter: 'filter name / description',
|
||||
versionAlert: 'Versions prior to 7.8 are not supported',
|
||||
note: `1、When creating a new index, if the index name matches the wildcard of the index template, the index template's settings (_setting, _mapping, etc.) are used。
|
||||
2、Templates take effect only when an index is created, and modifying a template does not affect existing indexes。
|
||||
3、You can specify the value of "priority", which was "order" before version 7.8, and if the new index name matches multiple templates, the one with the lowest priority will be used first.`,
|
||||
},
|
||||
dashboard: {
|
||||
instInfo: 'Instance Info',
|
||||
clusterHealth: 'Cluster Health',
|
||||
nodes: 'Nodes Info',
|
||||
sysMem: 'System Mem',
|
||||
jvmMem: 'JVM Mem',
|
||||
fileSystem: 'File System',
|
||||
analyze: 'Analyze',
|
||||
idxName: 'Index Name',
|
||||
field: 'Field',
|
||||
text: 'Text',
|
||||
startAnalyze: 'Start Analyze',
|
||||
},
|
||||
contextmenu: {
|
||||
index: {
|
||||
addIndex: 'Add Index',
|
||||
showSys: 'Show System Index',
|
||||
copyName: 'Copy Name',
|
||||
refresh: 'Refresh Index',
|
||||
flush: 'Flush Index',
|
||||
clearCache: 'Clear Index Cache',
|
||||
addAlias: 'Add Alias',
|
||||
Close: 'Close',
|
||||
Open: 'Open',
|
||||
Delete: 'Delete Index',
|
||||
edit: 'Edit Index',
|
||||
DeleteSelectLine: 'Copy Selected Line Json',
|
||||
BaseSearch: 'Base Search',
|
||||
SeniorSearch: 'Senior Search',
|
||||
copyLineJson: 'Copy Line Json',
|
||||
copySelectLineJson: 'Copy Selected Line Json',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -7,12 +7,16 @@ export default {
|
||||
detail: '详情',
|
||||
add: '添加',
|
||||
save: '保存',
|
||||
close: '关闭',
|
||||
download: '下载',
|
||||
upload: '上传',
|
||||
remove: '移除',
|
||||
confirm: '确定',
|
||||
cancel: '取消',
|
||||
submit: '提交',
|
||||
operation: '操作',
|
||||
name: '名称',
|
||||
version: '版本',
|
||||
code: '编号',
|
||||
remark: '备注',
|
||||
status: '状态',
|
||||
@@ -48,9 +52,11 @@ export default {
|
||||
previousStep: '上一步',
|
||||
nextStep: '下一步',
|
||||
copy: '复制',
|
||||
copyCell: '复制单元格',
|
||||
search: '搜索',
|
||||
pleaseInput: '请输入{label}',
|
||||
pleaseSelect: '请选择{label}',
|
||||
pleaseSelectOne: '请选择一条数据',
|
||||
formValidationError: '信息填写有误,请检查',
|
||||
createTitle: '创建{name}',
|
||||
editTitle: '编辑{name}',
|
||||
@@ -61,6 +67,8 @@ export default {
|
||||
deleteSuccess: '删除成功',
|
||||
operateSuccess: '操作成功',
|
||||
fieldNotEmpty: '{field}不能为空',
|
||||
selectAll: '全选',
|
||||
MultiPlaceholder: '多个用逗号隔开',
|
||||
},
|
||||
layout: {
|
||||
user: {
|
||||
|
||||
121
frontend/src/i18n/zh-cn/es.ts
Normal file
121
frontend/src/i18n/zh-cn/es.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
export default {
|
||||
es: {
|
||||
keywordPlaceholder: 'host / 名称 / 编号',
|
||||
port: '端口',
|
||||
size: '存储大小',
|
||||
docs: '文档数',
|
||||
health: '健康',
|
||||
aliases: '别名',
|
||||
addAlias: '添加别名',
|
||||
specifyIdAdd: '可指定_id添加,如果_id已存在,则修改',
|
||||
addIndex: '添加索引',
|
||||
editIndex: '编辑索引',
|
||||
status: '状态',
|
||||
acName: '授权凭证',
|
||||
emptyTable: '无数据',
|
||||
connSuccess: '连接成功',
|
||||
shouldTestConn: '请先测试连接可用性',
|
||||
instance: 'ES实例',
|
||||
instanceSave: '实例保存',
|
||||
instanceDel: '实例删除',
|
||||
operation: '数据操作',
|
||||
dataSave: '数据保存',
|
||||
dataDel: '数据删除',
|
||||
indexName: '索引名',
|
||||
requireIndexName: '请填写索引名',
|
||||
indexDetail: '索引详情',
|
||||
indexMapping: '映射',
|
||||
indexStats: '统计信息',
|
||||
opViewColumns: '设置显示字段',
|
||||
opIndex: '索引管理',
|
||||
opSearch: '搜索',
|
||||
searchParamsPreview: '搜索条件预览',
|
||||
opBasicSearch: '基础搜索',
|
||||
opSeniorSearch: '高级搜索',
|
||||
sampleMappings: 'Mapping示例',
|
||||
copyMappings: '拷贝Mapping',
|
||||
readonlyMsg: '该内容不可修改',
|
||||
opDashboard: '仪表盘',
|
||||
opSettings: '设置',
|
||||
templates: '模板管理',
|
||||
availableSettingFields: '支持修改的字段',
|
||||
Reindex: '索引迁移',
|
||||
ReindexTargetIdx: '目标索引',
|
||||
ReindexIsSync: '是否异步',
|
||||
ReindexDescription: '如果 Mapping 中字段已经定义就不能修改其字段的类型等属性了,同时也不能改变分片的数量, 可以使用 Reindex API 来解决这个问题。',
|
||||
ReindexSyncDescription: '如果索引数据量较大,建议开启异步,以免造成请求超时。',
|
||||
ReindexToOtherInst: '迁移到其他实例',
|
||||
ReindexSyncTask: '异步任务',
|
||||
makeSearchParam: '组装搜索条件',
|
||||
filterColumn: '过滤列名',
|
||||
searchParams: '查询',
|
||||
searchParamsDesc: '未选择字段,或未设置条件值,则不生效',
|
||||
standardSearch: '标准查询',
|
||||
AggregationSearch: '聚合查询',
|
||||
SqlSearch: 'Sql查询',
|
||||
searchError: '查询错误',
|
||||
execError: '执行错误',
|
||||
docJsonError: '文档JSON格式错误',
|
||||
sortParams: '排序',
|
||||
otherParams: '其他',
|
||||
previewParams: '预览',
|
||||
closeIndexConfirm: '将会关闭索引:[{name}]。 确认继续吗?',
|
||||
openIndexConfirm: '将会打开索引:[{name}]。 确认继续吗?',
|
||||
clearCacheConfirm: '将会清除索引:[{name}]缓存。 确认继续吗?',
|
||||
page: {
|
||||
home: '首页',
|
||||
prev: '上一页',
|
||||
next: '下一页',
|
||||
total: '点击切换总条数',
|
||||
changeSize: '修改每页条数',
|
||||
},
|
||||
temp: {
|
||||
addTemp: '添加模板',
|
||||
view: '模板详情',
|
||||
name: '模板名',
|
||||
priority: '优先级',
|
||||
index_patterns: '匹配模式',
|
||||
content: '模板内容',
|
||||
showHide: '显示隐藏模板',
|
||||
description: '描述信息',
|
||||
filter: '模糊过滤名字和描述',
|
||||
versionAlert: '暂不支持 7.8 以前的版本',
|
||||
note: `1、在新建索引时,如果索引名与索引模板的通配符匹配,那么就使用索引模板的设置(_setting、_mapping等)。
|
||||
2、模板仅在索引创建时才会生效,而且修改模板不会影响现有的索引。
|
||||
3、可以指定"priority"的数值,7.8版本前是"order",如果新建的索引名匹配到了多个模板,则优先使用priority最小的那个。`,
|
||||
},
|
||||
dashboard: {
|
||||
instInfo: '实例信息',
|
||||
clusterHealth: '集群健康',
|
||||
nodes: '节点信息',
|
||||
sysMem: '系统内存',
|
||||
jvmMem: 'JVM内存',
|
||||
fileSystem: '文件系统',
|
||||
analyze: '字段分析',
|
||||
idxName: '索引名',
|
||||
field: '字段名',
|
||||
text: '文本',
|
||||
startAnalyze: '开始分析',
|
||||
},
|
||||
contextmenu: {
|
||||
index: {
|
||||
addIndex: '添加索引',
|
||||
showSys: '显示系统索引',
|
||||
copyName: '复制名字',
|
||||
refresh: '刷新索引',
|
||||
flush: 'flush索引',
|
||||
clearCache: '清除索引缓存',
|
||||
addAlias: '添加别名',
|
||||
Close: '关闭索引',
|
||||
Open: '打开索引',
|
||||
Delete: '删除索引',
|
||||
edit: '编辑索引',
|
||||
DeleteSelectLine: '删除选中行',
|
||||
BaseSearch: '基本搜索',
|
||||
SeniorSearch: '高级搜索',
|
||||
copyLineJson: '复制整行JSON',
|
||||
copySelectLineJson: '复制选中行JSON',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -9,6 +9,7 @@ export default {
|
||||
tagTips3: '3. 拥有父标签的团队成员可访问操作其自身或子标签关联的资源',
|
||||
machine: '机器',
|
||||
db: '数据库',
|
||||
es: 'ES',
|
||||
code: '编号',
|
||||
createSubTag: '创建子标签',
|
||||
createSubTagTitle: '创建【{codePath}】的子标签',
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
}
|
||||
"
|
||||
>
|
||||
<SvgIcon name="icon layout/tag-view-active" class="layout-navbars-tagsview-ul-li-iconfont !text-[14px]" v-if="isActive(v)" />
|
||||
<SvgIcon :name="v.icon" class="layout-navbars-tagsview-ul-li-iconfont" v-if="!isActive(v) && themeConfig.isTagsviewIcon" />
|
||||
<SvgIcon :name="v.icon" class="layout-navbars-tagsview-ul-li-iconfont" v-if="themeConfig.isTagsviewIcon" />
|
||||
<span>{{ $t(v.title) }}</span>
|
||||
<template v-if="isActive(v)">
|
||||
<SvgIcon
|
||||
|
||||
@@ -112,7 +112,7 @@ export const useThemeConfig = defineStore('themeConfig', {
|
||||
/* 布局切换
|
||||
------------------------------- */
|
||||
// 默认布局,可选 1、默认 defaults 2、经典 classic 3、横向 transverse 4、分栏 columns
|
||||
layout: 'classic',
|
||||
layout: 'transverse',
|
||||
|
||||
terminalTheme: 'light',
|
||||
// ssh终端字体颜色
|
||||
@@ -161,7 +161,7 @@ export const useThemeConfig = defineStore('themeConfig', {
|
||||
} else {
|
||||
getServerConf().then((res) => {
|
||||
this.themeConfig.globalI18n = res.i18n;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// 根据后台系统配置初始化
|
||||
|
||||
@@ -218,6 +218,9 @@ $menuHeight: 46px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dialog 对话框
|
||||
------------------------------- */
|
||||
|
||||
/* Card 卡片
|
||||
------------------------------- */
|
||||
.el-card__header {
|
||||
|
||||
@@ -45,7 +45,6 @@ import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import ProcdefEdit from './ProcdefEdit.vue';
|
||||
import ProcdefTasks from './components/ProcdefTasks.vue';
|
||||
import { ProcdefStatus } from './enums';
|
||||
import TagCodePath from '../ops/component/TagCodePath.vue';
|
||||
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
||||
|
||||
@@ -19,8 +19,6 @@ const emit = defineEmits(['resize']);
|
||||
|
||||
const { width } = useWindowSize();
|
||||
|
||||
console.log(width);
|
||||
|
||||
const leftPaneSize = computed(() => (width.value >= 1600 ? 20 : 24));
|
||||
|
||||
// 处理 resize 事件
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
@clear="clear"
|
||||
placeholder="SSH tunnel machine"
|
||||
clearable
|
||||
filterable
|
||||
>
|
||||
<el-option v-for="item in sshTunnelMachineList" :key="item.id" :label="`${item.ip}:${item.port} [${item.name}]`" :value="item.id"> </el-option>
|
||||
</el-select>
|
||||
|
||||
1201
frontend/src/views/ops/es/EsOperation.vue
Normal file
1201
frontend/src/views/ops/es/EsOperation.vue
Normal file
File diff suppressed because it is too large
Load Diff
195
frontend/src/views/ops/es/InstanceEdit.vue
Normal file
195
frontend/src/views/ops/es/InstanceEdit.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
</template>
|
||||
|
||||
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
|
||||
<el-divider content-position="left">{{ t('common.basic') }}</el-divider>
|
||||
|
||||
<el-form-item ref="tagSelectRef" prop="tagCodePaths" :label="t('tag.relateTag')">
|
||||
<tag-tree-select
|
||||
multiple
|
||||
@change-tag="
|
||||
(paths: any) => {
|
||||
form.tagCodePaths = paths;
|
||||
tagSelectRef.validate();
|
||||
}
|
||||
"
|
||||
:select-tags="form.tagCodePaths"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="name" :label="t('common.name')" required>
|
||||
<el-input v-model.trim="form.name" auto-complete="off"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="version" :label="t('common.version')">
|
||||
<el-input v-model.trim="form.version" auto-complete="off" disabled></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="host" label="Host" required>
|
||||
<el-col :span="18">
|
||||
<el-input v-model.trim="form.host" auto-complete="off"></el-input>
|
||||
</el-col>
|
||||
<el-col style="text-align: center" :span="1">:</el-col>
|
||||
<el-col :span="5">
|
||||
<el-input type="number" v-model.number="form.port" :placeholder="t('es.port')"></el-input>
|
||||
</el-col>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="remark" :label="t('common.remark')">
|
||||
<el-input v-model="form.remark" auto-complete="off" type="textarea"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">{{ t('common.account') }}</el-divider>
|
||||
<div>
|
||||
<ResourceAuthCertTableEdit
|
||||
v-model="form.authCerts"
|
||||
:resource-code="form.code"
|
||||
:resource-type="TagResourceTypeEnum.EsInstance.value"
|
||||
:test-conn-btn-loading="testConnBtnLoading"
|
||||
@test-conn="testConn"
|
||||
:disable-ciphertext-type="[AuthCertCiphertextTypeEnum.PrivateKey.value]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-divider content-position="left">{{ t('common.other') }}</el-divider>
|
||||
|
||||
<el-form-item prop="sshTunnelMachineId" :label="t('machine.sshTunnel')">
|
||||
<ssh-tunnel-select v-model="form.sshTunnelMachineId" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="testConn(null)" type="success" v-if="form.authCerts?.length <= 0">{{ t('ac.testConn') }}</el-button>
|
||||
<el-button @click="cancel()">{{ t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">{{ t('common.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, toRefs, watchEffect } from 'vue';
|
||||
import { esApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import ResourceAuthCertTableEdit from '../component/ResourceAuthCertTableEdit.vue';
|
||||
import { AuthCertCiphertextTypeEnum } from '../tag/enums';
|
||||
import TagTreeSelect from '../component/TagTreeSelect.vue';
|
||||
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Rules } from '@/common/rule';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
},
|
||||
data: {
|
||||
type: [Boolean, Object],
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
//定义事件
|
||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
|
||||
|
||||
const rules = {
|
||||
tagCodePaths: [Rules.requiredSelect('tag.relateTag')],
|
||||
name: [Rules.requiredInput('common.name')],
|
||||
type: [Rules.requiredSelect('common.type')],
|
||||
host: [Rules.requiredInput('Host:Port')],
|
||||
};
|
||||
|
||||
const dbForm: any = ref(null);
|
||||
const tagSelectRef: any = ref(null);
|
||||
|
||||
const DefaultForm = {
|
||||
id: null,
|
||||
code: '',
|
||||
name: null,
|
||||
host: '',
|
||||
version: '',
|
||||
port: 9200,
|
||||
remark: '',
|
||||
sshTunnelMachineId: null as any,
|
||||
authCerts: [],
|
||||
tagCodePaths: [],
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
form: DefaultForm,
|
||||
submitForm: {} as any,
|
||||
});
|
||||
|
||||
const { dialogVisible, form, submitForm } = toRefs(state);
|
||||
|
||||
const { isFetching: saveBtnLoading, execute: saveInstanceExec, data: saveInstanceRes } = esApi.saveInstance.useApi(submitForm);
|
||||
const { isFetching: testConnBtnLoading, execute: testConnExec, data: testConnRes } = esApi.testConn.useApi<any>(submitForm);
|
||||
|
||||
watchEffect(() => {
|
||||
state.dialogVisible = props.visible;
|
||||
if (!state.dialogVisible) {
|
||||
return;
|
||||
}
|
||||
const dbInst: any = props.data;
|
||||
if (dbInst) {
|
||||
state.form = { ...dbInst };
|
||||
state.form.tagCodePaths = dbInst.tags.map((t: any) => t.codePath) || [];
|
||||
} else {
|
||||
state.form = { ...DefaultForm };
|
||||
state.form.authCerts = [];
|
||||
}
|
||||
});
|
||||
|
||||
const getReqForm = async () => {
|
||||
const reqForm: any = { ...state.form };
|
||||
reqForm.selectAuthCert = null;
|
||||
reqForm.tags = null;
|
||||
if (!state.form.sshTunnelMachineId) {
|
||||
reqForm.sshTunnelMachineId = -1;
|
||||
}
|
||||
return reqForm;
|
||||
};
|
||||
|
||||
const testConn = async (authCert: any) => {
|
||||
await useI18nFormValidate(dbForm);
|
||||
state.submitForm = await getReqForm();
|
||||
if (authCert) {
|
||||
state.submitForm.authCerts = [authCert];
|
||||
}
|
||||
await testConnExec();
|
||||
state.form.version = testConnRes.value.version.number;
|
||||
ElMessage.success(t('es.connSuccess'));
|
||||
};
|
||||
|
||||
const btnOk = async () => {
|
||||
if (!state.form.version) {
|
||||
ElMessage.warning(t('es.shouldTestConn'));
|
||||
return;
|
||||
}
|
||||
|
||||
await useI18nFormValidate(dbForm);
|
||||
state.submitForm = await getReqForm();
|
||||
await saveInstanceExec();
|
||||
useI18nSaveSuccessMsg();
|
||||
state.form.id = saveInstanceRes as any;
|
||||
emit('val-change', state.form);
|
||||
cancel();
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
emit('update:visible', false);
|
||||
emit('cancel');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
195
frontend/src/views/ops/es/InstanceList.vue
Normal file
195
frontend/src/views/ops/es/InstanceList.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div class="es-list">
|
||||
<page-table
|
||||
ref="pageTableRef"
|
||||
:page-api="esApi.instances"
|
||||
:data-handler-fn="handleData"
|
||||
:searchItems="searchItems"
|
||||
v-model:query-form="query"
|
||||
:show-selection="true"
|
||||
v-model:selection-data="state.selectionData"
|
||||
:columns="columns"
|
||||
lazy
|
||||
>
|
||||
<template #tableHeader>
|
||||
<el-button v-auth="perms.saveInstance" type="primary" icon="plus" @click="editInstance(false)">{{ $t('common.create') }}</el-button>
|
||||
<el-button v-auth="perms.delInstance" :disabled="selectionData.length < 1" @click="deleteInstance()" type="danger" icon="delete">
|
||||
{{ $t('common.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<template #tagPath="{ data }">
|
||||
<ResourceTags :tags="data.tags" />
|
||||
</template>
|
||||
|
||||
<template #authCert="{ data }">
|
||||
<ResourceAuthCert v-model:select-auth-cert="data.selectAuthCert" :auth-certs="data.authCerts" />
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button @click="showInfo(data)" link>{{ $t('common.detail') }}</el-button>
|
||||
<el-button v-if="actionBtns[perms.saveInstance]" @click="editInstance(data)" type="primary" link>{{ $t('common.edit') }}</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
|
||||
<el-dialog v-model="infoDialog.visible" :title="$t('common.detail')">
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item :span="2" :label="$t('common.name')">{{ infoDialog.data.name }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="ID">{{ infoDialog.data.id }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="2" label="Host">{{ infoDialog.data.host }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('es.port')">{{ infoDialog.data.port }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" :label="$t('common.remark')">{{ infoDialog.data.remark }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" :label="$t('machine.sshTunnel')">
|
||||
{{ infoDialog.data.sshTunnelMachineId > 0 ? $t('common.yes') : $t('common.no') }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" :label="$t('common.createTime')">{{ formatDate(infoDialog.data.createTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('common.creator')">{{ infoDialog.data.creator }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" :label="$t('common.updateTime')">{{ formatDate(infoDialog.data.updateTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('common.modifier')">{{ infoDialog.data.modifier }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
|
||||
<instance-edit
|
||||
@val-change="search()"
|
||||
:title="instanceEditDialog.title"
|
||||
v-model:visible="instanceEditDialog.visible"
|
||||
v-model:data="instanceEditDialog.data"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
||||
import { esApi } from './api';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import ResourceAuthCert from '../component/ResourceAuthCert.vue';
|
||||
import ResourceTags from '../component/ResourceTags.vue';
|
||||
import { getTagPathSearchItem } from '../component/tag';
|
||||
import { TagResourceTypePath } from '@/common/commonEnum';
|
||||
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle } from '@/hooks/useI18n';
|
||||
|
||||
const InstanceEdit = defineAsyncComponent(() => import('./InstanceEdit.vue'));
|
||||
|
||||
const props = defineProps({
|
||||
lazy: {
|
||||
type: [Boolean],
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const perms = {
|
||||
saveInstance: 'es:instance:save',
|
||||
delInstance: 'es:instance:del',
|
||||
};
|
||||
|
||||
const searchItems = [SearchItem.input('keyword', 'common.keyword').withPlaceholder('es.keywordPlaceholder'), getTagPathSearchItem(TagResourceTypePath.Es)];
|
||||
|
||||
const columns = ref([
|
||||
TableColumn.new('tags[0].tagPath', 'tag.relateTag').isSlot('tagPath').setAddWidth(20),
|
||||
TableColumn.new('name', 'common.name'),
|
||||
TableColumn.new('host', 'host:port').setFormatFunc((data: any) => `${data.host}:${data.port}`),
|
||||
TableColumn.new('authCerts[0].username', 'es.acName').isSlot('authCert').setAddWidth(10),
|
||||
TableColumn.new('remark', 'common.remark'),
|
||||
TableColumn.new('code', 'common.code'),
|
||||
]);
|
||||
|
||||
// 该用户拥有的的操作列按钮权限
|
||||
const actionBtns: any = hasPerms(Object.values(perms));
|
||||
const actionColumn = TableColumn.new('action', 'common.operation').isSlot().setMinWidth(180).fixedRight().noShowOverflowTooltip().alignCenter();
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
row: {},
|
||||
dbId: 0,
|
||||
db: '',
|
||||
/**
|
||||
* 选中的数据
|
||||
*/
|
||||
selectionData: [],
|
||||
/**
|
||||
* 查询条件
|
||||
*/
|
||||
query: {
|
||||
name: null,
|
||||
tagPath: '',
|
||||
pageNum: 1,
|
||||
pageSize: 0,
|
||||
},
|
||||
infoDialog: {
|
||||
visible: false,
|
||||
data: null as any,
|
||||
},
|
||||
instanceEditDialog: {
|
||||
visible: false,
|
||||
data: null as any,
|
||||
title: '',
|
||||
},
|
||||
});
|
||||
|
||||
const { selectionData, query, infoDialog, instanceEditDialog } = toRefs(state);
|
||||
|
||||
onMounted(async () => {
|
||||
if (Object.keys(actionBtns).length > 0) {
|
||||
columns.value.push(actionColumn);
|
||||
}
|
||||
if (!props.lazy) {
|
||||
search();
|
||||
}
|
||||
});
|
||||
|
||||
const search = (tagPath: string = '') => {
|
||||
if (tagPath) {
|
||||
state.query.tagPath = tagPath;
|
||||
}
|
||||
pageTableRef.value.search();
|
||||
};
|
||||
|
||||
const handleData = (res: any) => {
|
||||
const dataList = res.list;
|
||||
// 赋值授权凭证
|
||||
for (let x of dataList) {
|
||||
if (x.authCerts && x.authCerts.length > 0) {
|
||||
x.selectAuthCert = x.authCerts[0];
|
||||
}
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
const showInfo = (info: any) => {
|
||||
state.infoDialog.data = info;
|
||||
state.infoDialog.visible = true;
|
||||
};
|
||||
|
||||
const editInstance = async (data: any) => {
|
||||
if (!data) {
|
||||
state.instanceEditDialog.data = null;
|
||||
state.instanceEditDialog.title = useI18nCreateTitle('es.instance');
|
||||
} else {
|
||||
state.instanceEditDialog.data = data;
|
||||
state.instanceEditDialog.title = useI18nEditTitle('es.instance');
|
||||
}
|
||||
state.instanceEditDialog.visible = true;
|
||||
};
|
||||
|
||||
const deleteInstance = async () => {
|
||||
try {
|
||||
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.name).join('、'));
|
||||
await esApi.deleteInstance.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
useI18nDeleteSuccessMsg();
|
||||
search();
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ search });
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
55
frontend/src/views/ops/es/api.ts
Normal file
55
frontend/src/views/ops/es/api.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import Api from '@/common/Api';
|
||||
import MonacoEditorBox from '@/components/monaco/MonacoEditorBox';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export const esApi = {
|
||||
instances: Api.newGet('/es/instance'),
|
||||
deleteInstance: Api.newDelete('/es/instance/{id}'),
|
||||
saveInstance: Api.newPost('/es/instance'),
|
||||
testConn: Api.newPost('/es/instance/test-conn'),
|
||||
|
||||
// proxyGet: Api.newGet('/es/instance/proxy/{id}/{path}'),
|
||||
// proxyPost: Api.newPost('/es/instance/proxy/{id}/{path}'),
|
||||
// proxyPut: Api.newPut('/es/instance/proxy/{id}/{path}'),
|
||||
// proxyDelete: Api.newDelete('/es/instance/proxy/{id}/{path}'),
|
||||
|
||||
proxyReq: async function (method: string, id: any, path: string, param?: any) {
|
||||
if (path.startsWith('/')) {
|
||||
path = path.substring(1);
|
||||
}
|
||||
let res = {} as any;
|
||||
const t = i18n.global.t;
|
||||
switch (method) {
|
||||
case 'get':
|
||||
res = await Api.newGet(`/es/instance/proxy/${id}/${path}`).request(param, { esProxyReq: true });
|
||||
break;
|
||||
case 'post':
|
||||
res = await Api.newPost(`/es/instance/proxy/${id}/${path}`).request(param, { esProxyReq: true });
|
||||
break;
|
||||
case 'put':
|
||||
res = await Api.newPut(`/es/instance/proxy/${id}/${path}`).request(param, { esProxyReq: true });
|
||||
break;
|
||||
case 'delete':
|
||||
res = await Api.newDelete(`/es/instance/proxy/${id}/${path}`).request(param, { esProxyReq: true });
|
||||
break;
|
||||
}
|
||||
let error = res.error || (res.failures && res.failures.length > 0 && res.failures[0]) || res.msg;
|
||||
if (error) {
|
||||
return await esApi.alertError(error, t('es.execError'));
|
||||
}
|
||||
return res;
|
||||
},
|
||||
|
||||
alertError: async (errData: any, title: string) => {
|
||||
MonacoEditorBox({
|
||||
content: JSON.stringify(errData, null, 2),
|
||||
title,
|
||||
language: 'json',
|
||||
width: '600px',
|
||||
canChangeLang: false,
|
||||
options: { wordWrap: 'on', tabSize: 2, readOnly: true }, // 自动换行
|
||||
});
|
||||
|
||||
return await Promise.reject(errData);
|
||||
},
|
||||
};
|
||||
164
frontend/src/views/ops/es/component/EsAddIndex.vue
Normal file
164
frontend/src/views/ops/es/component/EsAddIndex.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<!-- es 编辑索引 -->
|
||||
|
||||
<template>
|
||||
<el-drawer
|
||||
:title="t('es.addIndex')"
|
||||
v-model="visible"
|
||||
size="50%"
|
||||
:destroy-on-close="false"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
class="es-edit-index h-full"
|
||||
>
|
||||
<el-auto-resizer>
|
||||
<template #default="{ height, width }">
|
||||
<el-form :model="formData" ref="formRef">
|
||||
<el-form-item :label="t('es.indexName')" required prop="idxName">
|
||||
<el-input v-model.trim="formData.idxName" maxlength="200" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-space>
|
||||
<el-form-item>
|
||||
<el-select v-model="formData.copyIdxName">
|
||||
<el-option v-for="idx in idxNames" :key="idx" :value="idx" :label="idx" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="onCopyMappings" link type="primary">{{ t('es.copyMappings') }}</el-button>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button @click="onSampleMappings" link type="warning">{{ t('es.sampleMappings') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-space>
|
||||
<el-form-item required prop="mappings" label="mappings" label-position="top">
|
||||
<monaco-editor v-model="formData.mappings" language="json" :height="height - 130 + 'px'" width="100%" :options="{ tabSize: 2 }" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
</el-auto-resizer>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">{{ t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="confirm" :loading="loading">{{ t('common.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ref, watch } from 'vue';
|
||||
import { esApi } from '@/views/ops/es/api';
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const defaultSettings = {
|
||||
number_of_shards: 5,
|
||||
number_of_replicas: 1,
|
||||
blocks: {
|
||||
read_only: 'false',
|
||||
},
|
||||
max_result_window: '1000000',
|
||||
refresh_interval: '30s',
|
||||
};
|
||||
|
||||
const emptyMappings = {
|
||||
mappings: {
|
||||
properties: {},
|
||||
},
|
||||
settings: defaultSettings,
|
||||
};
|
||||
|
||||
// 点击加载mapping示例
|
||||
const sampleMappings = {
|
||||
mappings: {
|
||||
properties: {
|
||||
title: {
|
||||
type: 'text',
|
||||
analyzer: 'ik_max_word',
|
||||
search_analyzer: 'ik_smart',
|
||||
fields: {
|
||||
standard: {
|
||||
type: 'text',
|
||||
analyzer: 'standard',
|
||||
},
|
||||
keyword: {
|
||||
type: 'keyword',
|
||||
ignore_above: 250,
|
||||
},
|
||||
},
|
||||
},
|
||||
mediaName: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
keyword: {
|
||||
type: 'keyword',
|
||||
ignore_above: 256,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
settings: defaultSettings,
|
||||
};
|
||||
|
||||
const formData = ref({
|
||||
idxName: '',
|
||||
copyIdxName: '',
|
||||
mappings: '',
|
||||
});
|
||||
|
||||
interface Props {
|
||||
instId: any;
|
||||
idxNames: string[];
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
const loading = ref(false);
|
||||
const formRef = ref();
|
||||
|
||||
const visible = defineModel<boolean>('visible');
|
||||
|
||||
watch(visible, async (x: any) => {
|
||||
if (x) {
|
||||
formData.value.idxName = '';
|
||||
formData.value.copyIdxName = '';
|
||||
formData.value.mappings = JSON.stringify(emptyMappings, null, 2);
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const confirm = async () => {
|
||||
await formRef.value.validate();
|
||||
loading.value = true;
|
||||
if (!formData.value.idxName) {
|
||||
ElMessage.warning(t('es.requireIndexName'));
|
||||
return;
|
||||
}
|
||||
await esApi.proxyReq('put', props.instId, `/${formData.value.idxName}`, JSON.parse(formData.value.mappings));
|
||||
ElMessage.success(t('common.saveSuccess'));
|
||||
emit('success');
|
||||
loading.value = false;
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const onSampleMappings = () => {
|
||||
formData.value.mappings = JSON.stringify(sampleMappings, null, 2);
|
||||
};
|
||||
const onCopyMappings = async () => {
|
||||
let mp = await esApi.proxyReq('get', props.instId, `/${formData.value.copyIdxName}/_mappings`);
|
||||
let properties = mp[formData.value.copyIdxName].mappings.properties;
|
||||
formData.value.mappings = JSON.stringify(
|
||||
{
|
||||
mappings: {
|
||||
properties,
|
||||
},
|
||||
settings: defaultSettings,
|
||||
},
|
||||
null,
|
||||
2
|
||||
);
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss"></style>
|
||||
385
frontend/src/views/ops/es/component/EsDashboard.vue
Normal file
385
frontend/src/views/ops/es/component/EsDashboard.vue
Normal file
@@ -0,0 +1,385 @@
|
||||
<template>
|
||||
<el-tabs v-model="state.tabName" type="card">
|
||||
<el-tab-pane name="nodesStats" v-loading="state.nodesStatsLoading" style="height: calc(100vh - 200px); overflow-y: auto">
|
||||
<template #label>
|
||||
{{ t('es.dashboard.nodes') }}
|
||||
<el-button v-if="state.tabName === 'nodesStats'" icon="refresh" @click="fetchNodesStats" link type="primary" />
|
||||
</template>
|
||||
<el-descriptions class="nodes-num" column="3" border>
|
||||
<el-descriptions-item label="total">
|
||||
{{ state.nodesStats._nodes?.total }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="successful">
|
||||
{{ state.nodesStats._nodes?.successful }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="failed">
|
||||
{{ state.nodesStats._nodes?.failed }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-tabs>
|
||||
<el-tab-pane :label="node.name" v-for="node in state.nodesStats.nodes" :key="node.key">
|
||||
<el-card class="mt-1">
|
||||
<el-form label-width="100">
|
||||
<el-form-item label="ID">
|
||||
<el-tag size="small" type="primary">{{ node.key }}</el-tag>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="IP">
|
||||
<el-tag size="small" type="primary">{{ node.ip }}</el-tag>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="TIME">
|
||||
<el-tag size="small" type="primary">{{ dayjs(node.timestamp).format('YYYY-MM-DD HH:mm:ss') }}</el-tag>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Roles">
|
||||
<el-space wrap>
|
||||
<el-tag v-for="r in node.roles" :key="r" type="success">{{ r }}</el-tag>
|
||||
</el-space>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Docs">
|
||||
<el-space>
|
||||
<el-tag type="warning">count: {{ node.indices.docs.count }}</el-tag>
|
||||
<el-tag type="info">deleted: {{ node.indices.docs.deleted }}</el-tag>
|
||||
<el-tag type="primary">{{ formatByteSize(node.indices.store.size_in_bytes) }}</el-tag>
|
||||
</el-space>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('es.dashboard.sysMem')">
|
||||
{{ formatByteSize(node.os.mem.used_in_bytes) }} / {{ formatByteSize(node.os.mem.total_in_bytes) }}
|
||||
<el-progress
|
||||
striped
|
||||
striped-flow
|
||||
duration="50"
|
||||
style="width: 100%"
|
||||
:percentage="node.os.mem.used_percent"
|
||||
:color="getPercentColor(node.os.mem.used_percent)"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('es.dashboard.jvmMem')">
|
||||
{{ formatByteSize(node.jvm.mem.heap_used_in_bytes) }} / {{ formatByteSize(node.jvm.mem.heap_max_in_bytes) }}
|
||||
<el-progress
|
||||
striped
|
||||
striped-flow
|
||||
duration="50"
|
||||
style="width: 100%"
|
||||
:percentage="node.jvm.mem.heap_used_percent"
|
||||
:color="getPercentColor(node.jvm.mem.heap_used_percent)"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="CPU">
|
||||
<el-progress
|
||||
striped
|
||||
striped-flow
|
||||
duration="50"
|
||||
style="width: 100%"
|
||||
:percentage="node.os.cpu.percent"
|
||||
:color="getPercentColor(node.os.cpu.percent)"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('es.dashboard.fileSystem')">
|
||||
{{ formatByteSize(node.fs.total.total_in_bytes - node.fs.total.free_in_bytes) }} /
|
||||
{{ formatByteSize(node.fs.total.total_in_bytes) }}
|
||||
<el-progress
|
||||
striped
|
||||
striped-flow
|
||||
duration="50"
|
||||
style="width: 100%"
|
||||
:percentage="
|
||||
Math.round(((node.fs.total.total_in_bytes - node.fs.total.free_in_bytes) * 100) / node.fs.total.total_in_bytes)
|
||||
"
|
||||
:color="
|
||||
getPercentColor(((node.fs.total.total_in_bytes - node.fs.total.free_in_bytes) * 100) / node.fs.total.total_in_bytes)
|
||||
"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane
|
||||
name="instInfo"
|
||||
v-loading="state.instInfoLoading"
|
||||
:label="t('es.dashboard.instInfo')"
|
||||
style="height: calc(100vh - 200px); overflow-y: auto"
|
||||
>
|
||||
<el-card shadow="hover">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label-align="left" align="right" :label="item.name" v-for="item in state.instInfo" :key="item.name">
|
||||
{{ item.value }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
name="clusterHealth"
|
||||
v-loading="state.clusterHealthLoading"
|
||||
:label="t('es.dashboard.clusterHealth')"
|
||||
style="height: calc(100vh - 200px); overflow-y: auto"
|
||||
>
|
||||
<el-card shadow="always">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label-align="left" align="right" :label="item.name" v-for="item in state.clusterHealth" :key="item.name">
|
||||
{{ item.value }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane
|
||||
name="analyze"
|
||||
v-loading="state.clusterStateLoading"
|
||||
:label="t('es.dashboard.analyze')"
|
||||
style="height: calc(100vh - 200px); overflow-y: auto"
|
||||
>
|
||||
<el-card class="h-full">
|
||||
<el-form :model="state.analyze" ref="analyzeFormRef" label-position="right" label-width="100">
|
||||
<el-form-item :label="t('es.dashboard.idxName')" required prop="idxName">
|
||||
<el-select v-model="state.analyze.idxName" filterable clearable @change="onSelectIdxField">
|
||||
<el-option v-for="idx in state.idxFields" :key="idx.name" :value="idx.name" :label="idx.name" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('es.dashboard.field')" required prop="field">
|
||||
<el-select v-model="state.analyze.field" filterable clearable>
|
||||
<el-option v-for="field in state.analyze.fields" :key="field" :value="field" :label="field" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('es.dashboard.text')" required prop="text">
|
||||
<el-input type="textarea" rows="5" v-model="state.analyze.text" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button @click="onAnalyze" :loading="state.analyze.loading">{{ t('es.dashboard.startAnalyze') }}</el-button>
|
||||
<el-table :data="state.analyze.tokens" style="height: calc(100vh - 500px)" stripe size="small" :v-loading="true">
|
||||
<el-table-column label="token" prop="token" />
|
||||
<el-table-column label="position" prop="position" />
|
||||
<el-table-column label="start_offset" prop="start_offset" />
|
||||
<el-table-column label="end_offset" prop="end_offset" />
|
||||
<el-table-column label="type" prop="type" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { nextTick, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { esApi } from '@/views/ops/es/api';
|
||||
import { formatByteSize } from '@/common/utils/format';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
interface Props {
|
||||
instId: any;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const analyzeFormRef = ref();
|
||||
|
||||
const state = reactive({
|
||||
tabName: 'nodesStats',
|
||||
instInfo: [] as any[],
|
||||
clusterHealth: [] as any[],
|
||||
nodesStats: { _nodes: {} as any, nodes: [] as any[] } as any,
|
||||
idxFields: [] as any[],
|
||||
nodesStatsLoading: false,
|
||||
instInfoLoading: false,
|
||||
clusterHealthLoading: false,
|
||||
clusterStateLoading: false,
|
||||
analyze: {
|
||||
loading: false,
|
||||
idxName: '',
|
||||
fields: [],
|
||||
field: '',
|
||||
text: '',
|
||||
tokens: [],
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick(async () => {
|
||||
await fetchNodesStats();
|
||||
});
|
||||
});
|
||||
|
||||
watch(
|
||||
() => state.tabName,
|
||||
async (val) => {
|
||||
switch (val) {
|
||||
case 'instInfo':
|
||||
return await fetchInstInfo();
|
||||
case 'clusterHealth':
|
||||
return await fetchClusterHealth();
|
||||
case 'nodesStats':
|
||||
return await fetchNodesStats();
|
||||
case 'analyze':
|
||||
await fetchClusterState();
|
||||
return;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const fetchInstInfo = async () => {
|
||||
state.instInfoLoading = true;
|
||||
state.instInfo = [];
|
||||
let res = await esApi.proxyReq('get', props.instId, '/');
|
||||
let fo = flattenObject(res);
|
||||
for (const it in fo) {
|
||||
state.instInfo.push({
|
||||
name: it,
|
||||
value: fo[it],
|
||||
});
|
||||
}
|
||||
state.instInfoLoading = false;
|
||||
|
||||
// key 排序
|
||||
state.instInfo = state.instInfo.sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
function flattenObject(obj: Record<string, any>, parentKey = '', result: Record<string, any> = {}): Record<string, any> {
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
const newKey = parentKey ? `${parentKey}.${key}` : key;
|
||||
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
|
||||
flattenObject(obj[key], newKey, result);
|
||||
} else {
|
||||
result[newKey] = obj[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const fetchClusterHealth = async () => {
|
||||
state.clusterHealthLoading = true;
|
||||
state.clusterHealth = [];
|
||||
let res = await esApi.proxyReq('get', props.instId, '/_cluster/health');
|
||||
let fo = flattenObject(res);
|
||||
for (const it in fo) {
|
||||
state.clusterHealth.push({
|
||||
name: it,
|
||||
value: fo[it],
|
||||
});
|
||||
}
|
||||
state.clusterHealthLoading = false;
|
||||
|
||||
// key 排序
|
||||
state.clusterHealth = state.clusterHealth.sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
const fetchNodesStats = async () => {
|
||||
state.nodesStatsLoading = true;
|
||||
let res = await esApi.proxyReq('get', props.instId, '/_nodes/stats/os,jvm,indices,transport,fs');
|
||||
state.nodesStats._nodes = res._nodes;
|
||||
let nodes = [] as any[];
|
||||
for (let k in res.nodes) {
|
||||
let node = res.nodes[k];
|
||||
node.key = k;
|
||||
nodes.push(node);
|
||||
}
|
||||
state.nodesStats.nodes = nodes.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// 以node名排序
|
||||
state.nodesStatsLoading = false;
|
||||
// id
|
||||
// ip
|
||||
// name
|
||||
// roles
|
||||
// 系统内存 饼图 os.mem.total_in_bytes os.mem.used_in_bytes os.mem.used_percent
|
||||
// 系统cpu使用率 饼图 os.cpu.percent
|
||||
// jvm内存 饼图 jvm.mem.heap_max_in_bytes jvm.mem.heap_used_in_bytes jvm.mem.heap_used_percent
|
||||
// 存储空间占用信息 饼图 fs.total.total_in_bytes fs.total.free_in_bytes
|
||||
// 索引文档数 indices.docs.count
|
||||
// 索引占用 indices.store.size_in_bytes
|
||||
// 总分片数量 indices.shard_stats.total_count
|
||||
// 网络流量 transport.rx_size_in_bytes transport.tx_size_in_bytes
|
||||
};
|
||||
|
||||
const fetchClusterState = async () => {
|
||||
state.clusterStateLoading = true;
|
||||
const res = await esApi.proxyReq('get', props.instId, '/_cluster/state');
|
||||
|
||||
const idxFields = [];
|
||||
|
||||
for (let k in res.metadata.indices) {
|
||||
// 过滤系统索引
|
||||
if (k.indexOf('.') >= 0) {
|
||||
continue;
|
||||
}
|
||||
let properties = res.metadata.indices[k]?.mappings?._doc?.properties || {};
|
||||
let fields = [];
|
||||
for (let k in properties) {
|
||||
let f = properties[k];
|
||||
// long字段类型不支持分析
|
||||
if (f.type === 'long' || f.type === 'date') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 添加字段
|
||||
fields.push(k);
|
||||
|
||||
// 如果有子字段,则添加子字段
|
||||
if (f.fields) {
|
||||
for (let fk in f.fields) {
|
||||
fields.push(`${k}.${fk}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
idxFields.push({
|
||||
name: k,
|
||||
fields: fields.sort(),
|
||||
});
|
||||
}
|
||||
|
||||
// 索引字段信息
|
||||
state.idxFields = idxFields.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
state.clusterStateLoading = false;
|
||||
};
|
||||
|
||||
const getPercentColor = (percent: number) => {
|
||||
if (percent < 60) {
|
||||
return '#67c23a';
|
||||
} else if (percent < 80) {
|
||||
return '#e6a23c';
|
||||
} else {
|
||||
return '#f56c6c';
|
||||
}
|
||||
};
|
||||
|
||||
const onSelectIdxField = () => {
|
||||
state.analyze.fields = state.idxFields.find((item: any) => item.name === state.analyze.idxName)?.fields || [];
|
||||
state.analyze.field = '';
|
||||
};
|
||||
|
||||
const onAnalyze = async () => {
|
||||
await analyzeFormRef.value.validate();
|
||||
state.analyze.loading = true;
|
||||
|
||||
setTimeout(() => {
|
||||
state.analyze.loading = false;
|
||||
}, 2000);
|
||||
|
||||
let res = await esApi.proxyReq('post', props.instId, `/${state.analyze.idxName}/_analyze`, {
|
||||
field: state.analyze.field,
|
||||
text: state.analyze.text,
|
||||
});
|
||||
state.analyze.tokens = res.tokens;
|
||||
state.analyze.loading = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.nodes-num {
|
||||
font-size: 20px;
|
||||
}
|
||||
</style>
|
||||
138
frontend/src/views/ops/es/component/EsEditRow.vue
Normal file
138
frontend/src/views/ops/es/component/EsEditRow.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:title="`${model.isAdd ? t('common.add') : t('common.edit')} ${model.idxName}`"
|
||||
v-model="visible"
|
||||
:destroy-on-close="false"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
size="50%"
|
||||
>
|
||||
<el-auto-resizer>
|
||||
<template #default="{ height, width }">
|
||||
<el-form>
|
||||
<el-form-item label="_id">
|
||||
<el-input v-model.trim="_id" :disabled="model._id != ''" :placeholder="t('es.specifyIdAdd')" />
|
||||
</el-form-item>
|
||||
<monaco-editor v-model="model.doc" language="json" :height="height - 40 + 'px'" :options="{ wordWrap: 'on', tabSize: 2 }" />
|
||||
</el-form>
|
||||
</template>
|
||||
</el-auto-resizer>
|
||||
<template #footer>
|
||||
<el-button size="small" @click="visible = false">{{ t('common.cancel') }}</el-button>
|
||||
<el-button size="small" v-auth="perms.saveData" @click="onSaveDoc" :loading="loading" type="primary">{{ t('common.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineAsyncComponent, ref, watch } from 'vue';
|
||||
import { esApi } from '@/views/ops/es/api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const MonacoEditor = defineAsyncComponent(() => import('@/components/monaco/MonacoEditor.vue'));
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const perms = {
|
||||
saveData: 'es:data:save',
|
||||
};
|
||||
|
||||
const visible = defineModel<boolean>('visible');
|
||||
const loading = ref(false);
|
||||
const _id = ref('');
|
||||
|
||||
interface Params {
|
||||
isAdd: boolean;
|
||||
instId: string;
|
||||
doc: string;
|
||||
idxName: string;
|
||||
_id: string;
|
||||
}
|
||||
const model = defineModel<Params>({ required: true });
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const getZeroValueByProperties = async () => {
|
||||
// 根据mapping字段赋值
|
||||
let mp = await esApi.proxyReq('get', model.value.instId, `/${model.value.idxName}/_mappings`);
|
||||
let properties = mp[model.value.idxName].mappings.properties;
|
||||
let data = {} as any;
|
||||
|
||||
for (let key in properties) {
|
||||
let item = properties[key];
|
||||
switch (item.type) {
|
||||
case 'object':
|
||||
case 'nested':
|
||||
case 'flattened':
|
||||
data[key] = {};
|
||||
break;
|
||||
case 'long':
|
||||
case 'short':
|
||||
case 'byte':
|
||||
case 'double':
|
||||
case 'float':
|
||||
case 'half_float':
|
||||
case 'scaled_float':
|
||||
data[key] = 0;
|
||||
break;
|
||||
case 'boolean':
|
||||
data[key] = false;
|
||||
break;
|
||||
default:
|
||||
data[key] = '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
watch(visible, async (newValue) => {
|
||||
if (!newValue) {
|
||||
model.value._id = '';
|
||||
model.value.doc = '';
|
||||
_id.value = '';
|
||||
loading.value = false;
|
||||
} else {
|
||||
if (model.value._id) {
|
||||
_id.value = model.value._id;
|
||||
}
|
||||
if (!model.value.doc) {
|
||||
model.value.doc = JSON.stringify(await getZeroValueByProperties(), null, 2);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const onSaveDoc = async () => {
|
||||
loading.value = true;
|
||||
let doc = model.value.doc;
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(doc);
|
||||
} catch (error) {
|
||||
ElMessage.error(t('es.docJsonError'));
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果数据中带有_id,则删除_id
|
||||
if (data._id) {
|
||||
delete data._id;
|
||||
}
|
||||
// 2 秒后关闭loading,避免接口报错后不关闭loading
|
||||
setTimeout(async () => {
|
||||
loading.value = false;
|
||||
}, 2000);
|
||||
|
||||
await esApi.proxyReq('post', model.value.instId, `/${model.value.idxName}/_doc/${_id.value}`, data);
|
||||
|
||||
ElMessage.success(t('common.saveSuccess'));
|
||||
|
||||
setTimeout(() => {
|
||||
visible.value = false;
|
||||
emit('success');
|
||||
}, 500);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
213
frontend/src/views/ops/es/component/EsIndexDetail.vue
Normal file
213
frontend/src/views/ops/es/component/EsIndexDetail.vue
Normal file
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:title="t('es.indexDetail') + ' - ' + state.idxName"
|
||||
v-model="visible"
|
||||
size="50%"
|
||||
:destroy-on-close="false"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
class="es-index-detail h-full"
|
||||
@close="onClose"
|
||||
>
|
||||
<el-auto-resizer>
|
||||
<template #default="{ height, width }">
|
||||
<el-tabs v-model="activeName">
|
||||
<el-tab-pane name="settings">
|
||||
<template #label>
|
||||
<el-tooltip>
|
||||
<template #content> {{ t('es.availableSettingFields') }}: {{ allowedKeys }}</template>
|
||||
<el-space>{{ t('es.opSettings') }}<SvgIcon name="QuestionFilled" /></el-space>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<monaco-editor v-model="state.settings" language="json" :height="height - 40 + 'px'" :options="{ tabSize: 2 }" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('es.indexMapping')" name="mappings">
|
||||
<monaco-editor v-model="state.mappings" language="json" :height="height - 40 + 'px'" :options="state.editorOptions" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('es.indexStats')" name="stats">
|
||||
<monaco-editor v-model="state.stats" language="json" :height="height - 40 + 'px'" :options="state.editorOptions" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('es.aliases')" name="aliases">
|
||||
<el-button type="primary" @click="onAddAlias" icon="plus" size="small">{{ t('es.addAlias') }}</el-button>
|
||||
|
||||
<div :style="{ paddingTop: '20px' }">
|
||||
<el-space direction="vertical" alignment="start">
|
||||
<el-tag v-for="tag in state.aliases" :key="tag" closable type="primary" @close="onRemoveAlias(tag)">
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
</el-space>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</el-auto-resizer>
|
||||
|
||||
<template #footer>
|
||||
<el-button size="small" @click="visible = false">{{ t('common.close') }}</el-button>
|
||||
<el-button size="small" @click="onOk" type="primary" v-if="activeName == 'settings'" :loading="state.loading">{{ t('common.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
|
||||
<el-dialog v-model="dialogFormVisible" :title="t('es.addAlias')" width="400">
|
||||
<el-form :model="state.aliasesForm">
|
||||
<el-form-item :label="t('es.aliases')">
|
||||
<el-input v-model="state.aliasesForm.name" autocomplete="off" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button size="small" @click="dialogFormVisible = false">{{ t('common.cancel') }}</el-button>
|
||||
<el-button size="small" @click="onSubmitAddAlias" :loading="aliasLoading" type="primary">{{ t('common.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineAsyncComponent, reactive, ref, watch } from 'vue';
|
||||
import { esApi } from '@/views/ops/es/api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { useI18nDeleteConfirm } from '@/hooks/useI18n';
|
||||
const MonacoEditor = defineAsyncComponent(() => import('@/components/monaco/MonacoEditor.vue'));
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const visible = ref(false);
|
||||
const aliasLoading = ref(false);
|
||||
const activeName = ref('settings');
|
||||
|
||||
const defaultData = {
|
||||
instId: 0,
|
||||
loading: false,
|
||||
aliases: [] as string[],
|
||||
idxName: '',
|
||||
health: {},
|
||||
mappings: '',
|
||||
stats: '',
|
||||
settings: '',
|
||||
editorOptions: { tabSize: 2, readOnly: true, readOnlyMessage: { value: t('es.readonlyMsg') } },
|
||||
aliasesForm: { name: '' },
|
||||
};
|
||||
|
||||
let state = reactive(defaultData);
|
||||
|
||||
const dialogFormVisible = ref(false);
|
||||
|
||||
let allowedKeys = ['number_of_replicas', 'refresh_interval', 'blocks.read_only', 'blocks.read', 'blocks.write', 'max_result_window', 'blocks'];
|
||||
|
||||
const onOk = async () => {
|
||||
if (activeName.value === 'settings') {
|
||||
/**
|
||||
* 常见可修改设置:
|
||||
* 设置项 描述
|
||||
* number_of_replicas 副本分片数,可随时修改
|
||||
* refresh_interval 刷新间隔,控制索引频率
|
||||
* blocks.read_only 设置索引为只读或可写
|
||||
* blocks.read / blocks.write 控制是否允许读/写操作
|
||||
* max_result_window 控制最大返回结果数量,默认为10000
|
||||
*/
|
||||
let settings = JSON.parse(state.settings).index;
|
||||
// 只允许传可设置的字段
|
||||
for (let key in settings) {
|
||||
if (allowedKeys.indexOf(key) == -1) {
|
||||
delete settings[key];
|
||||
}
|
||||
}
|
||||
|
||||
await esApi.proxyReq('put', state.instId, `/${state.idxName}/_settings`, { index: settings });
|
||||
ElMessage.success(t('common.saveSuccess'));
|
||||
}
|
||||
};
|
||||
|
||||
watch(activeName, async (val) => {
|
||||
state.mappings = '';
|
||||
state.stats = '';
|
||||
state.aliases = [];
|
||||
// 如果没有值就请求接口获取值
|
||||
if (val === 'mappings') {
|
||||
await refreshMappings();
|
||||
} else if (val === 'stats') {
|
||||
await refreshStats();
|
||||
} else if (val === 'aliases') {
|
||||
await refreshAlias();
|
||||
}
|
||||
});
|
||||
|
||||
const refreshMappings = async () => {
|
||||
let res = await esApi.proxyReq('get', state.instId, `/${state.idxName}/_mappings`);
|
||||
state.mappings = JSON.stringify(res[state.idxName].mappings, null, 2);
|
||||
};
|
||||
|
||||
const refreshStats = async () => {
|
||||
let stats = await esApi.proxyReq('get', state.instId, `/${state.idxName}/_stats`);
|
||||
state.stats = JSON.stringify(stats.indices[state.idxName], null, 2);
|
||||
};
|
||||
|
||||
const refreshAlias = async () => {
|
||||
let aliases = await esApi.proxyReq('get', state.instId, `/${state.idxName}/_alias`);
|
||||
state.aliases = Object.keys(aliases[state.idxName].aliases);
|
||||
};
|
||||
|
||||
const refreshSettings = async () => {
|
||||
let res = await esApi.proxyReq('get', state.instId, `/${state.idxName}/_settings`);
|
||||
let st = res[state.idxName].settings;
|
||||
|
||||
state.settings = JSON.stringify(st, null, 2);
|
||||
};
|
||||
|
||||
const initBasic = async () => {
|
||||
state.health = await esApi.proxyReq('get', state.instId, `/_cluster/health/${state.idxName}`);
|
||||
await refreshSettings();
|
||||
};
|
||||
|
||||
const onAddAlias = async () => {
|
||||
dialogFormVisible.value = true;
|
||||
state.aliasesForm.name = '';
|
||||
aliasLoading.value = false;
|
||||
};
|
||||
const onRemoveAlias = async (name: string) => {
|
||||
await useI18nDeleteConfirm(`${t('es.aliases')}: ${name}`);
|
||||
|
||||
await esApi.proxyReq('delete', state.instId, `/${state.idxName}/_alias/${name}`);
|
||||
ElMessage.success(t('common.deleteSuccess'));
|
||||
await refreshAlias();
|
||||
};
|
||||
|
||||
const onSubmitAddAlias = async () => {
|
||||
aliasLoading.value = true;
|
||||
await esApi.proxyReq('put', state.instId, `/${state.idxName}/_alias/${state.aliasesForm.name}`);
|
||||
ElMessage.success(t('common.saveSuccess'));
|
||||
await refreshAlias();
|
||||
dialogFormVisible.value = false;
|
||||
aliasLoading.value = false;
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
state = reactive(defaultData);
|
||||
};
|
||||
|
||||
const open = (data: any) => {
|
||||
visible.value = true;
|
||||
activeName.value = 'settings';
|
||||
state = reactive(defaultData);
|
||||
state.instId = data.instId;
|
||||
state.idxName = data.idxName;
|
||||
initBasic();
|
||||
};
|
||||
const close = () => {
|
||||
visible.value = false;
|
||||
onClose();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
close,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.es-index-detail {
|
||||
.el-drawer__body {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
284
frontend/src/views/ops/es/component/EsIndexTemplate.vue
Normal file
284
frontend/src/views/ops/es/component/EsIndexTemplate.vue
Normal file
@@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
size="50%"
|
||||
:destroy-on-close="false"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
class="es-op-temp h-full"
|
||||
v-model="visible"
|
||||
:title="t('es.templates')"
|
||||
>
|
||||
<el-auto-resizer>
|
||||
<template #default="{ height, width }">
|
||||
<el-space class="mb-3">
|
||||
<el-input :placeholder="t('es.temp.filter')" v-model.trim="state.filterTableName" @input="onFilterTemplates" />
|
||||
<el-button type="primary" @click="onAddTemplate" icon="plus">{{ t('common.add') }}</el-button>
|
||||
<SvgIcon name="refresh" @click="fetchTemplates" :size="20" />
|
||||
<el-dropdown :hide-on-click="false">
|
||||
<SvgIcon name="setting" :size="20" />
|
||||
<el-button link icon="setting" />
|
||||
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item>
|
||||
<el-checkbox v-model="state.showHideTemps" @change="onSwitchShowHide">{{ t('es.temp.showHide') }}</el-checkbox>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-tooltip>
|
||||
<template #content>
|
||||
<pre>{{ t('es.temp.note') }}</pre>
|
||||
</template>
|
||||
<SvgIcon name="Warning" :size="20" />
|
||||
</el-tooltip>
|
||||
<el-text type="warning" size="small">{{ t('es.temp.versionAlert') }}</el-text>
|
||||
</el-space>
|
||||
|
||||
<el-table :data="state.templates" :max-height="height - 40" stripe size="small">
|
||||
<el-table-column prop="name" :label="t('es.temp.name')" />
|
||||
<el-table-column prop="index_patterns" :label="t('es.temp.index_patterns')" />
|
||||
<el-table-column prop="description" :label="t('es.temp.description')" />
|
||||
<el-table-column :label="t('common.operation')" width="100px" align="center">
|
||||
<template #default="scope">
|
||||
<el-space>
|
||||
<el-button link type="primary" size="small" @click="onViewTemplate(scope.row)">{{ t('common.detail') }}</el-button>
|
||||
<el-button link type="danger" size="small" @click="onDelTemplate(scope.row.name)">{{ t('common.delete') }}</el-button>
|
||||
</el-space>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
</el-auto-resizer>
|
||||
</el-drawer>
|
||||
|
||||
<el-drawer
|
||||
size="50%"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
class="es-add-temp h-full"
|
||||
v-model="state.addVisible"
|
||||
:title="state.formReadonly ? t('es.temp.view') : t('es.temp.addTemp')"
|
||||
>
|
||||
<el-auto-resizer>
|
||||
<template #default="{ height, width }">
|
||||
<el-form :model="form" ref="formRef" label-position="right" label-width="80">
|
||||
<el-form-item :label="t('es.temp.name')" required prop="name">
|
||||
<el-input v-model.trim="form.name" :disabled="state.formReadonly" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('es.temp.priority')" required prop="priority">
|
||||
<el-input-number v-model="form.priority" :disabled="state.formReadonly" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('es.temp.index_patterns')" prop="index_patterns">
|
||||
<el-select allow-create filterable multiple clearable v-model="form.index_patterns" :disabled="state.formReadonly"></el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('es.temp.description')" required prop="description">
|
||||
<el-input v-model.trim="form.description" :disabled="state.formReadonly" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('es.temp.content')" required prop="template">
|
||||
<monaco-editor
|
||||
v-model="form.template"
|
||||
language="json"
|
||||
:height="height - 200 + 'px'"
|
||||
:options="{ tabSize: 2, readOnly: state.formReadonly }"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
</el-auto-resizer>
|
||||
<template #footer>
|
||||
<el-button @click="state.addVisible = false">{{ t('common.close') }}</el-button>
|
||||
<el-button v-if="!state.formReadonly" type="primary" @click="doAddTemplate">{{ t('common.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { esApi } from '@/views/ops/es/api';
|
||||
import { nextTick, reactive, ref, unref, watch } from 'vue';
|
||||
import { useI18nConfirm, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const visible = defineModel<boolean>();
|
||||
|
||||
interface Props {
|
||||
instId: any;
|
||||
version: string;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const formRef = ref();
|
||||
|
||||
const state = reactive({
|
||||
originTemplates: [] as any,
|
||||
templates: [] as any,
|
||||
showHideTemps: false,
|
||||
filterTableName: '',
|
||||
addVisible: false,
|
||||
formReadonly: false,
|
||||
form: {
|
||||
name: '',
|
||||
priority: 100,
|
||||
index_patterns: [],
|
||||
template: '',
|
||||
description: '',
|
||||
},
|
||||
|
||||
// es 版本是否小于 7.8.0
|
||||
// 7.8之前的版本模板接口为_template,优先级字段为order,
|
||||
// 7.8之后的版本模板接口为_index_template,优先级字段为priority
|
||||
v: {
|
||||
oldVersion: false,
|
||||
api: '_index_template',
|
||||
priority: 'priority',
|
||||
},
|
||||
});
|
||||
|
||||
const { form } = unref(state);
|
||||
|
||||
const getDefaultTemplate = () => {
|
||||
return {
|
||||
settings: {
|
||||
number_of_shards: 5,
|
||||
number_of_replicas: 1,
|
||||
blocks: {
|
||||
read_only: 'false',
|
||||
},
|
||||
max_result_window: '1000000',
|
||||
refresh_interval: '30s',
|
||||
},
|
||||
mappings: { properties: {} },
|
||||
};
|
||||
};
|
||||
|
||||
watch(visible, async (x: any) => {
|
||||
if (x) {
|
||||
// 初始化状态
|
||||
state.filterTableName = '';
|
||||
state.originTemplates = [];
|
||||
state.templates = [];
|
||||
state.showHideTemps = false;
|
||||
|
||||
state.v.oldVersion = isVersionBefore7_8_0(props.version);
|
||||
if (state.v.oldVersion) {
|
||||
state.v.api = '_template';
|
||||
state.v.priority = 'order';
|
||||
} else {
|
||||
state.v.api = '_index_template';
|
||||
state.v.priority = 'priority';
|
||||
}
|
||||
|
||||
await nextTick(fetchTemplates);
|
||||
}
|
||||
});
|
||||
|
||||
const isVersionBefore7_8_0 = (version: string) => {
|
||||
if (!version) {
|
||||
return false;
|
||||
}
|
||||
const v1 = version.split('.').map(Number);
|
||||
const v2 = [7, 8, 0]; // 比较目标版本
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (v1[i] < v2[i]) return true;
|
||||
if (v1[i] > v2[i]) return false;
|
||||
}
|
||||
return false; // 等于 7.8.0 时返回 false
|
||||
};
|
||||
|
||||
const fetchTemplates = async () => {
|
||||
const data = await esApi.proxyReq('get', props.instId, `/${state.v.api}`);
|
||||
state.originTemplates = data.index_templates
|
||||
.map((a: any) => {
|
||||
return {
|
||||
name: a.name,
|
||||
priority: a.index_template.priority || 'NULL',
|
||||
index_patterns: JSON.stringify(a.index_template.index_patterns || '[]'),
|
||||
template: JSON.stringify(a.index_template.template || {}, null, 2),
|
||||
description: a.index_template._meta?.description || '',
|
||||
};
|
||||
})
|
||||
.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
||||
|
||||
onSwitchShowHide();
|
||||
};
|
||||
|
||||
const onSwitchShowHide = () => {
|
||||
if (state.showHideTemps) {
|
||||
state.templates = state.originTemplates;
|
||||
} else {
|
||||
state.templates = state.originTemplates.filter((item: any) => item.name.indexOf('.') < 0);
|
||||
}
|
||||
};
|
||||
const onFilterTemplates = () => {
|
||||
onSwitchShowHide();
|
||||
|
||||
let regx = createPattern(state.filterTableName);
|
||||
state.templates = state.templates.filter((item: any) => regx.test(item.name) || regx.test(item.description));
|
||||
};
|
||||
|
||||
function createPattern(str: string): RegExp {
|
||||
const escaped = str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // 转义特殊字符
|
||||
const pattern = [...escaped].join('.*');
|
||||
return new RegExp(`.*${pattern}.*`);
|
||||
}
|
||||
|
||||
const onViewTemplate = async (data: any) => {
|
||||
state.addVisible = true;
|
||||
state.formReadonly = true;
|
||||
|
||||
state.form.name = data.name;
|
||||
state.form.priority = data.priority;
|
||||
state.form.index_patterns = JSON.parse(data.index_patterns);
|
||||
state.form.template = data.template;
|
||||
state.form.description = data.description;
|
||||
};
|
||||
|
||||
const onAddTemplate = () => {
|
||||
state.addVisible = true;
|
||||
state.formReadonly = false;
|
||||
|
||||
state.form.name = '';
|
||||
state.form.priority = 100;
|
||||
state.form.index_patterns = [];
|
||||
state.form.template = JSON.stringify(getDefaultTemplate(), null, 2);
|
||||
state.form.description = '';
|
||||
};
|
||||
|
||||
const doAddTemplate = async () => {
|
||||
await formRef.value.validate();
|
||||
let data = {
|
||||
index_patterns: state.form.index_patterns,
|
||||
[state.v.priority]: state.form.priority,
|
||||
template: JSON.parse(state.form.template),
|
||||
_meta: {
|
||||
description: state.form.description,
|
||||
},
|
||||
};
|
||||
await esApi.proxyReq('put', props.instId, `/${state.v.api}/${state.form.name}`, data);
|
||||
useI18nOperateSuccessMsg();
|
||||
|
||||
setTimeout(async () => {
|
||||
state.addVisible = false;
|
||||
await fetchTemplates();
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const onDelTemplate = async (name: any) => {
|
||||
await useI18nDeleteConfirm(name);
|
||||
await useI18nConfirm('es.deleteTemplateConfirm', { name: name });
|
||||
await esApi.proxyReq('delete', props.instId, `/${state.v.api}/${name}`);
|
||||
useI18nDeleteSuccessMsg();
|
||||
|
||||
setTimeout(async () => {
|
||||
await fetchTemplates();
|
||||
}, 500);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
85
frontend/src/views/ops/es/component/EsReindex.vue
Normal file
85
frontend/src/views/ops/es/component/EsReindex.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:title="t('es.Reindex')"
|
||||
v-model="visible"
|
||||
size="40%"
|
||||
:destroy-on-close="false"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
class="es-reindex h-full"
|
||||
>
|
||||
<el-tabs v-model="tabActiveName">
|
||||
<el-tab-pane name="basic" label="basic">
|
||||
<el-form :model="formData" ref="formRef">
|
||||
<el-form-item :label="t('es.ReindexTargetIdx')" required prop="targetIdxName">
|
||||
<el-select clearable filterable v-model="formData.targetIdxName" :style="{ width: '100%' }">
|
||||
<el-option v-for="idx in idxNames" :key="idx" :value="idx" :label="idx" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('es.ReindexIsSync')">
|
||||
<el-space>
|
||||
<el-switch v-model="formData.sync" />
|
||||
<el-text type="info" size="small">{{ t('es.ReindexSyncDescription') }}</el-text>
|
||||
</el-space>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-text type="info" size="small">{{ t('es.ReindexDescription') }}</el-text>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="otherInst" :label="t('es.ReindexToOtherInst')"> developing... </el-tab-pane>
|
||||
<el-tab-pane name="task" :label="t('es.ReindexSyncTask')"> developing... </el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">{{ t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="confirm">{{ t('common.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ref } from 'vue';
|
||||
import { esApi } from '@/views/ops/es/api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const { t } = useI18n();
|
||||
const visible = defineModel<boolean>('visible');
|
||||
|
||||
const formRef = ref();
|
||||
|
||||
interface Props {
|
||||
instId: any;
|
||||
idxName: string;
|
||||
idxNames: string[];
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
const tabActiveName = ref('basic');
|
||||
|
||||
const formData = ref({
|
||||
targetIdxName: '',
|
||||
sync: false,
|
||||
});
|
||||
|
||||
const confirm = async () => {
|
||||
if (tabActiveName.value === 'basic') {
|
||||
await doBasicReindex();
|
||||
}
|
||||
};
|
||||
|
||||
const doBasicReindex = async () => {
|
||||
await formRef.value.validate();
|
||||
let wfc = '';
|
||||
if (!formData.value.sync) {
|
||||
wfc = '?wait_for_completion=false';
|
||||
}
|
||||
let data = { source: { index: props.idxName }, dest: { index: formData.value.targetIdxName } };
|
||||
|
||||
let res = await esApi.proxyReq('POST', props.instId, `/_reindex${wfc}`, data);
|
||||
// FIXME 如果是异步,返回异步任务id,添加到任务列表中,可以在任务列表中查看状态
|
||||
ElMessage.success(t('common.operateSuccess'));
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
621
frontend/src/views/ops/es/component/EsSearch.vue
Normal file
621
frontend/src/views/ops/es/component/EsSearch.vue
Normal file
@@ -0,0 +1,621 @@
|
||||
<template>
|
||||
<el-dialog :title="t('es.makeSearchParam')" v-model="visible" :width="state.searchBoxWidth" class="es-search-form-inline">
|
||||
<el-tabs v-model="state.activeName">
|
||||
<el-tab-pane :label="t('es.standardSearch')" name="standard">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<el-space>
|
||||
<span>{{ t('es.searchParams') }}</span>
|
||||
<el-text type="info" size="small">{{ t('es.searchParamsDesc') }}</el-text>
|
||||
</el-space>
|
||||
</template>
|
||||
<el-button v-if="state.queryParams.length == 0" size="small" @click="onAddParam" type="primary" icon="plus">{{
|
||||
t('common.add')
|
||||
}}</el-button>
|
||||
<div v-for="item in state.queryParams" :key="item.uuid">
|
||||
<el-form :inline="true" :model="item">
|
||||
<el-form-item>
|
||||
<el-switch v-model="item.enable" active-text="on" inactive-text="off" inline-prompt />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-select v-model="item.type">
|
||||
<el-option v-for="p in paramTypes" :key="p" :label="p" :value="p" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-select filterable v-model="item.field" class="field-select">
|
||||
<el-option v-for="f in fields" :key="f" :label="f" :value="f" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-select filterable v-model="item.matchType">
|
||||
<el-option v-for="d in matchTypes" :key="d" :label="d" :value="d" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="item.matchType !== 'range'">
|
||||
<el-input
|
||||
v-model.trim="item.value"
|
||||
:placeholder="item.matchType === 'terms' || item.type === 'should' ? t('common.MultiPlaceholder') : ''"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button link @click="onAddParam" type="primary" icon="plus" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button link @click="onCopyParam(item)" type="primary" icon="CopyDocument" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button link @click="onDelParam(item.uuid)" type="danger" icon="delete" />
|
||||
</el-form-item>
|
||||
<div v-if="item.matchType === 'range'">
|
||||
<el-form-item>
|
||||
<el-select v-model="item.gtType" class="es-range-select">
|
||||
<el-option value="gt">gt ></el-option>
|
||||
<el-option value="gte">gte >=</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input class="es-range-input" v-model.trim="item.gtValue" placeholder="> or >=" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-select v-model="item.ltType" class="es-range-select">
|
||||
<el-option value="lt">lt <</el-option>
|
||||
<el-option value="lte">lte <=</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input class="es-range-input" v-model.trim="item.ltValue" placeholder="< or <=" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card :header="t('es.sortParams')">
|
||||
<el-button v-if="state.sortParams.length == 0" size="small" @click="onAddSort" type="primary" icon="plus">{{ t('common.add') }}</el-button>
|
||||
|
||||
<div v-for="item in state.sortParams" :key="item.uuid">
|
||||
<el-form :inline="true" :model="item">
|
||||
<el-form-item>
|
||||
<el-switch v-model="item.enable" active-text="on" inactive-text="off" inline-prompt></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-select filterable v-model="item.field" class="field-select">
|
||||
<el-option v-for="f in fields" :key="f" :label="f" :value="f" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-select filterable v-model="item.order">
|
||||
<el-option v-for="t in orderTypes" :key="t" :label="t" :value="t" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button link @click="onAddSort" type="primary" icon="plus" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button link @click="onDelSort(item.uuid)" type="danger" icon="delete" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card :header="t('es.otherParams')">
|
||||
<el-form label-width="200px" label-position="left">
|
||||
<el-form-item label="track_total_hits">
|
||||
<el-checkbox v-model="state.track_total_hits" />
|
||||
</el-form-item>
|
||||
<el-form-item label="minimum_should_match">
|
||||
<el-input-number size="small" v-model="state.minimum_should_match" :min="1" :max="10" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('es.AggregationSearch')" name="aggs"> developing... </el-tab-pane>
|
||||
<el-tab-pane :label="t('es.SqlSearch')" name="sql"> developing... </el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<template #footer>
|
||||
<div>
|
||||
<el-button size="small" @click="onPreviewParam" icon="view">{{ t('es.previewParams') }}</el-button>
|
||||
<el-button size="small" @click="onClearParam" icon="refresh">{{ t('common.reset') }}</el-button>
|
||||
<!-- <el-button size="small" @click="onSaveParam" type="primary" icon="check">{{ t('common.save') }}</el-button>-->
|
||||
|
||||
<el-button size="small" @click="visible = false" icon="close">{{ t('common.close') }}</el-button>
|
||||
<el-button size="small" @click="onSearch" type="primary" icon="search">{{ t('common.search') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import { randomUuid } from '@/common/utils/string';
|
||||
import MonacoEditorBox from '@/components/monaco/MonacoEditorBox';
|
||||
import { esApi } from '@/views/ops/es/api';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
/**
|
||||
*
|
||||
* 类型 是否参与评分 必须满足 说明
|
||||
* must ✅ 是 ✅ 是 所有条件都必须满足,类似 SQL 的 AND
|
||||
* should ✅ 是(默认) ❌ 否 至少满足一个,类似 SQL 的 OR,可通过 minimum_should_match 控制
|
||||
* must_not ❌ 否 ✅ 是 所有条件都不满足,不参与评分
|
||||
*
|
||||
*
|
||||
* 匹配方式 适用类型 特点 示例
|
||||
* match text 对文本字段进行分词后匹配。 "match": { "content": "elasticsearch search" } 匹配包含 "elasticsearch" 或 "search" 的文档。
|
||||
* match_phrase text 短语匹配,要求关键词按顺序连续出现。 "match_phrase": { "content": "今天天气不错" } 只有当该短语作为一个整体出现在内容中时才匹配。
|
||||
* term keyword、boolean、number 等不分词字段 对字段进行精确匹配,不进行分词。 "term": { "status": "published" 匹配 status 字段等于 "published" 的文档。
|
||||
* terms 用于匹配多个值中的任意一个(类似 SQL 中的 IN) "terms": { "category": ["tech", "science", "ai"] }
|
||||
* exists 判断某个字段是否存在。
|
||||
* wildcard 支持通配符匹配(* 匹配任意字符序列,? 匹配单个字符)。
|
||||
* range 用于匹配数值或日期范围 "range": { "age": { "gte": 18, "lte": 30 } }
|
||||
*
|
||||
* 使用建议
|
||||
* 对于需要全文检索的字段,使用 text 类型 + match。
|
||||
* 对于精确匹配(如 ID、状态码、标签等),使用 keyword 类型 + term。
|
||||
* 如果要提高性能,可以将不关心相关度的条件放在 bool.filter 中。
|
||||
* 尽量避免在大数据集中频繁使用 wildcard,它会显著影响性能。
|
||||
*
|
||||
* 查询示例
|
||||
* {
|
||||
* "query": {
|
||||
* "bool": {
|
||||
* "must": [
|
||||
* { "match": { "title": "搜索测试" }}
|
||||
* ],
|
||||
* "should": [
|
||||
* { "term": { "category": "技术" }},
|
||||
* { "match_phrase": { "content": "性能优化" }}
|
||||
* ],
|
||||
* "must_not": [
|
||||
* { "term": { "status": "草稿" }}
|
||||
* ],
|
||||
* "minimum_should_match": 1
|
||||
* }
|
||||
* },
|
||||
* "sort": { "etlTime": { "order": "desc" } },
|
||||
* "aggs": {}
|
||||
* "from": 0,
|
||||
* "size": 25
|
||||
* }
|
||||
*
|
||||
* 聚合查询(Aggregations)
|
||||
* 是一种强大的数据分析功能,用于对数据进行分类、统计、分析和分组。它类似于 SQL 中的 GROUP BY 和 COUNT()、SUM() 等操作,但更加强大灵活。
|
||||
* 一、聚合的基本结构
|
||||
*
|
||||
* {
|
||||
* "size": 0,
|
||||
* "aggs": {
|
||||
* "自定义聚合名称": {
|
||||
* "聚合类型": {
|
||||
* // 聚合参数
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* - "size": 0:表示不返回具体的文档内容,只返回聚合结果。
|
||||
* - "aggs":是聚合的入口,你可以在这里定义多个聚合项。
|
||||
*
|
||||
* 二、常见的聚合类型
|
||||
* 1. 指标聚合(Metric Aggregations)
|
||||
* 用于计算字段的统计指标
|
||||
*
|
||||
* 聚合类型 描述
|
||||
* avg 计算某个字段的平均值
|
||||
* sum 计算字段总和
|
||||
* min / max 获取最小值或最大值
|
||||
* value_count 统计非空值的数量
|
||||
* cardinality 去重统计(类似 SQL 的 COUNT(DISTINCT))
|
||||
*
|
||||
* 示例:
|
||||
*
|
||||
* "aggs": {
|
||||
* "avg_salary": { "avg": { "field": "salary" } },
|
||||
* "unique_users": { "cardinality": { "field": "user_id.keyword" } }
|
||||
* }
|
||||
*
|
||||
* 2. 桶聚合(Bucket Aggregations)
|
||||
* 用于将文档分组(类似 SQL 的 GROUP BY),每个桶是一个分组。
|
||||
* (1) terms:按字段值分组统计
|
||||
* "aggs": {
|
||||
* "group_by_status": {
|
||||
* "terms": { "field": "status.keyword" }
|
||||
* }
|
||||
* }
|
||||
* 按 status 字段的不同值进行分组,并统计每组数量。
|
||||
* (2) date_histogram:按时间间隔分组
|
||||
* "aggs": {
|
||||
* "articles_over_time": {
|
||||
* "date_histogram": {
|
||||
* "field": "publish_date",
|
||||
* "calendar_interval": "day"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* 按天统计文章发布数量。
|
||||
*
|
||||
* (3) range / date_range:按数值/日期范围分组
|
||||
*
|
||||
* "aggs": {
|
||||
* "age_distribution": {
|
||||
* "range": {
|
||||
* "field": "age",
|
||||
* "ranges": [
|
||||
* { "from": 0, "to": 18 },
|
||||
* { "from": 18, "to": 35 },
|
||||
* { "from": 35, "to": 60 }
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* 按年龄段区间统计人数。
|
||||
* (4) histogram:按固定数值步长分组
|
||||
* "aggs": {
|
||||
* "price_distribution": {
|
||||
* "histogram": {
|
||||
* "field": "price",
|
||||
* "interval": 100
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* 每 100 元为一个价格区间进行分组统计。
|
||||
*
|
||||
* 3. 嵌套聚合(组合使用)
|
||||
* 你可以在一个聚合中嵌套其他聚合,实现多维分析。
|
||||
* 示例:先按状态分组,再按平均工资排序
|
||||
* "aggs": {
|
||||
* "group_by_status": {
|
||||
* "terms": {
|
||||
* "field": "status.keyword",
|
||||
* "order": { "avg_salary": "desc" }
|
||||
* },
|
||||
* "aggs": {
|
||||
* "avg_salary": { "avg": { "field": "salary" } }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
* es 数据类型
|
||||
*
|
||||
* 一、基础数据类型
|
||||
* 类型 描述
|
||||
* text 用于全文本搜索,会被分析器分词处理。适用于长文本内容。
|
||||
* keyword 不分词,作为完整字符串存储和匹配,适用于精确查询、聚合、排序等。
|
||||
* long 64位整数。
|
||||
* integer 32位整数。
|
||||
* short 16位整数。
|
||||
* byte 8位整数。
|
||||
* double 双精度浮点数。
|
||||
* float 单精度浮点数。
|
||||
* half_float 半精度浮点数(占用更少空间)。
|
||||
* scaled_float 以长整型形式存储浮点数(如:1.99 存为 199,缩放因子为 100)。
|
||||
* date 日期类型,可接受格式化字符串或时间戳。
|
||||
* boolean 布尔值,true 或 false。
|
||||
* binary Base64 编码的二进制数据(不存储,仅用于传输)。
|
||||
*
|
||||
* 二、复杂数据类型
|
||||
* 类型 描述
|
||||
* object 默认嵌套 JSON 对象类型,适合嵌套结构但不支持嵌套查询。
|
||||
* nested 特殊的 object 类型,支持嵌套查询(需使用 nested query)。
|
||||
* flattened 将整个对象视为单个字段,适用于动态结构但只支持精确匹配。
|
||||
* join 用于父子文档关系(Parent-Child),实现文档间逻辑关联。
|
||||
* percolator 用于预注册查询,然后对新文档进行匹配测试。
|
||||
*
|
||||
* 三、地理空间数据类型
|
||||
* 类型 描述
|
||||
* geo_point 表示经纬度坐标,可用于距离查询、地理围栏等。
|
||||
* geo_shape 支持复杂的地理形状(如多边形、线段等),用于高级地理查询。
|
||||
*
|
||||
* 四、特殊用途数据类型
|
||||
* 类型 描述
|
||||
* ip 用于 IPv4/IPv6 地址,支持范围查询。
|
||||
* token_count 统计某个 text 字段分词后的词项数量。
|
||||
* murmur3 自动计算字段的哈希值(需显式开启)。
|
||||
* attachment 用于解析 Base64 编码的文件(如 PDF、Word 等)。
|
||||
* search_as_you_type 优化自动补全搜索体验的字段类型。
|
||||
* rank_feature / rank_features 用于基于机器学习的相关性评分优化。
|
||||
*
|
||||
* 五、数组类型
|
||||
* 在 ES 中,没有单独的数组类型。任何字段都可以包含多个值,只要它们的类型一致。
|
||||
*
|
||||
* 六、字段映射示例
|
||||
*
|
||||
* {
|
||||
* "mappings": {
|
||||
* "properties": {
|
||||
* "title": { "type": "text" },
|
||||
* "status": { "type": "keyword" },
|
||||
* "views": { "type": "integer" },
|
||||
* "created_at": { "type": "date" },
|
||||
* "location": { "type": "geo_point" },
|
||||
* "tags": { "type": "keyword" },
|
||||
* "user": {
|
||||
* "type": "nested",
|
||||
* "properties": {
|
||||
* "name": { "type": "text" },
|
||||
* "email": { "type": "keyword" }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
*/
|
||||
|
||||
const defaultSearch = {
|
||||
sort: {} as any, // etlTime: { order: 'desc' }
|
||||
query: { bool: { must: [], should: [], must_not: [] } },
|
||||
aggs: {},
|
||||
} as any;
|
||||
|
||||
interface Props {
|
||||
instId: string;
|
||||
idxName: string;
|
||||
}
|
||||
|
||||
const fields = ref<string[]>();
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits(['search']);
|
||||
|
||||
const visible = defineModel<boolean>('visible');
|
||||
watch(visible, async (v) => {
|
||||
if (v) {
|
||||
// 通过mapping获取所有字段信息
|
||||
if (fields.value?.length) {
|
||||
return;
|
||||
}
|
||||
let mp = await esApi.proxyReq('get', props.instId, `/${props.idxName}/_mappings`);
|
||||
let properties = mp[props.idxName].mappings.properties;
|
||||
let data = ['_id'];
|
||||
for (let key in properties) {
|
||||
data.push(key);
|
||||
let item = properties[key];
|
||||
if (item.fields) {
|
||||
for (let f in item.fields) {
|
||||
data.push(`${key}.${f}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
fields.value = data;
|
||||
}
|
||||
});
|
||||
|
||||
const paramTypes = ['must', 'should', 'must_not'] as const;
|
||||
const matchTypes = ['match', 'match_phrase', 'term', 'terms', 'exists', 'wildcard', 'range'] as const;
|
||||
const orderTypes = ['asc', 'desc'] as const;
|
||||
const gtTypes = ['gt', 'gte'] as const;
|
||||
const ltTypes = ['lt', 'lte'] as const;
|
||||
|
||||
type searchParam = {
|
||||
uuid: string; // 唯一id
|
||||
enable: boolean; // 是否启用,启用后才应用到搜索
|
||||
type: (typeof paramTypes)[number];
|
||||
field: string;
|
||||
matchType: (typeof matchTypes)[number];
|
||||
value: any;
|
||||
gtType: (typeof gtTypes)[number];
|
||||
gtValue: string;
|
||||
ltType: (typeof ltTypes)[number];
|
||||
ltValue: string;
|
||||
};
|
||||
|
||||
type sortParam = {
|
||||
uuid: string; // 唯一id
|
||||
enable: boolean; // 是否启用,启用后才应用到搜索
|
||||
field: string;
|
||||
order: (typeof orderTypes)[number];
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
searchBoxWidth: '720px',
|
||||
queryParams: [] as searchParam[],
|
||||
sortParams: [] as sortParam[],
|
||||
search: defaultSearch,
|
||||
minimum_should_match: 1,
|
||||
track_total_hits: false,
|
||||
|
||||
activeName: 'standard',
|
||||
});
|
||||
|
||||
const onAddParam = () => {
|
||||
state.queryParams.push({
|
||||
uuid: randomUuid(),
|
||||
enable: true,
|
||||
type: 'must',
|
||||
field: '',
|
||||
matchType: 'term',
|
||||
value: '',
|
||||
gtType: 'gt',
|
||||
gtValue: '',
|
||||
ltType: 'lt',
|
||||
ltValue: '',
|
||||
});
|
||||
};
|
||||
|
||||
const onCopyParam = (item: searchParam) => {
|
||||
let newItem = JSON.parse(JSON.stringify(item));
|
||||
newItem.uuid = randomUuid();
|
||||
state.queryParams.push(newItem);
|
||||
};
|
||||
|
||||
const onDelParam = (uuid: string) => {
|
||||
state.queryParams = state.queryParams.filter((item) => item.uuid !== uuid);
|
||||
};
|
||||
const onAddSort = () => {
|
||||
state.sortParams.push({
|
||||
uuid: randomUuid(),
|
||||
enable: true,
|
||||
field: '',
|
||||
order: 'asc',
|
||||
});
|
||||
};
|
||||
|
||||
const onDelSort = (uuid: string) => {
|
||||
state.sortParams = state.sortParams.filter((item) => item.uuid !== uuid);
|
||||
};
|
||||
|
||||
const onClearParam = () => {
|
||||
// 清空查询条件
|
||||
state.queryParams = [];
|
||||
state.sortParams = [];
|
||||
};
|
||||
const onSaveParam = () => {
|
||||
// 保存查询条件
|
||||
};
|
||||
|
||||
const onPreviewParam = () => {
|
||||
parseParams();
|
||||
MonacoEditorBox({
|
||||
content: JSON.stringify(state.search, null, 2),
|
||||
title: t('es.searchParamsPreview'),
|
||||
language: 'json',
|
||||
width: state.searchBoxWidth,
|
||||
canChangeLang: false,
|
||||
options: { wordWrap: 'on', tabSize: 2, readOnly: true }, // 自动换行
|
||||
});
|
||||
};
|
||||
|
||||
const parseParams = () => {
|
||||
// 组装查询条件并emit search事件
|
||||
let must = [] as any;
|
||||
let should = [] as any;
|
||||
let must_not = [] as any;
|
||||
let sort = {} as any;
|
||||
|
||||
for (let item of state.queryParams) {
|
||||
if (!item.enable || !item.field || (!item.value.trim() && !item.gtValue.trim() && !item.ltValue.trim())) {
|
||||
continue;
|
||||
}
|
||||
// wildcard 自动添加通配符
|
||||
if (item.matchType === 'wildcard' && !item.value.includes('*') && !item.value.includes('?')) {
|
||||
item.value = `*${item.value}*`;
|
||||
}
|
||||
|
||||
let value = item.value;
|
||||
if (item.matchType === 'terms') {
|
||||
value = item.value.split(',').map((item: string) => item.trim());
|
||||
}
|
||||
|
||||
let match = {
|
||||
[item.matchType]: {
|
||||
[item.field]: value,
|
||||
},
|
||||
} as any;
|
||||
|
||||
// 处理range
|
||||
if (item.matchType == 'range') {
|
||||
let gtType = item.gtType;
|
||||
let ltType = item.ltType;
|
||||
let gtValue = item.gtValue.trim();
|
||||
let ltValue = item.ltValue.trim();
|
||||
if (!gtValue && !ltValue) {
|
||||
continue;
|
||||
}
|
||||
let range = {} as any;
|
||||
if (gtValue) {
|
||||
range[gtType] = gtValue;
|
||||
}
|
||||
if (ltValue) {
|
||||
range[ltType] = ltValue;
|
||||
}
|
||||
|
||||
match = {
|
||||
range: {
|
||||
[item.field]: range,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
switch (item.type) {
|
||||
case 'must':
|
||||
must.push(match);
|
||||
break;
|
||||
case 'should':
|
||||
should.push(match);
|
||||
break;
|
||||
case 'must_not':
|
||||
must_not.push(match);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
state.search.query = { bool: { must, should, must_not } };
|
||||
|
||||
// 排序
|
||||
state.sortParams.forEach((item) => {
|
||||
if (item.enable && item.field) {
|
||||
sort[item.field] = { order: item.order };
|
||||
}
|
||||
});
|
||||
state.search.sort = sort;
|
||||
|
||||
// track_total_hits
|
||||
if (state.track_total_hits) {
|
||||
state.search['track_total_hits'] = true;
|
||||
} else {
|
||||
delete state.search['track_total_hits'];
|
||||
}
|
||||
|
||||
// minimum_should_match 需要结合should使用,默认为1,表示至少一个should条件满足
|
||||
if (should.length > 0) {
|
||||
state.search['minimum_should_match'] = Math.max(1, state.minimum_should_match);
|
||||
} else {
|
||||
delete state.search['minimum_should_match'];
|
||||
}
|
||||
};
|
||||
|
||||
const onSearch = () => {
|
||||
parseParams();
|
||||
emit('search', state.search);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.es-search-form-inline {
|
||||
* {
|
||||
border-radius: 3px;
|
||||
}
|
||||
.el-card {
|
||||
margin-bottom: 10px;
|
||||
.el-card__header {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-input {
|
||||
--el-input-width: 150px;
|
||||
}
|
||||
.es-range-input {
|
||||
--el-input-width: 240px;
|
||||
}
|
||||
.el-select {
|
||||
--el-select-width: 100px;
|
||||
font-size: var(--font-size);
|
||||
}
|
||||
.es-range-select {
|
||||
--el-select-width: 70px;
|
||||
}
|
||||
.field-select {
|
||||
--el-select-width: 150px;
|
||||
}
|
||||
.el-form {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.el-form-item {
|
||||
margin-right: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.el-input-number {
|
||||
width: 80px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -6,66 +6,51 @@
|
||||
</template>
|
||||
|
||||
<el-form :model="form" ref="redisForm" :rules="rules" label-width="auto">
|
||||
<el-tabs v-model="tabActiveName">
|
||||
<el-tab-pane :label="$t('common.basic')" name="basic">
|
||||
<el-form-item ref="tagSelectRef" prop="tagCodePaths" :label="$t('tag.relateTag')" required>
|
||||
<tag-tree-select
|
||||
@change-tag="
|
||||
(tagCodePaths) => {
|
||||
form.tagCodePaths = tagCodePaths;
|
||||
tagSelectRef.validate();
|
||||
}
|
||||
"
|
||||
multiple
|
||||
:select-tags="form.tagCodePaths"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="name" :label="$t('common.name')" required>
|
||||
<el-input v-model.trim="form.name" auto-complete="off"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="mode" label="mode" required>
|
||||
<el-select v-model="form.mode">
|
||||
<el-option label="standalone" value="standalone"> </el-option>
|
||||
<el-option label="cluster" value="cluster"> </el-option>
|
||||
<el-option label="sentinel" value="sentinel"> </el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="host" label="host" required>
|
||||
<el-input v-model.trim="form.host" :placeholder="$t('redis.hostTips')" auto-complete="off" type="textarea"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="username" :label="$t('common.username')">
|
||||
<el-input v-model.trim="form.username"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password" :label="$t('common.password')">
|
||||
<el-input type="password" show-password v-model.trim="form.password" autocomplete="new-password"> </el-input>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.mode == 'sentinel'" prop="redisNodePassword" :label="$t('redis.nodePassword')">
|
||||
<el-input type="password" show-password v-model.trim="form.redisNodePassword" autocomplete="new-password"> </el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="db" label="DB" required>
|
||||
<el-select
|
||||
@change="changeDb"
|
||||
:disabled="form.mode == 'cluster'"
|
||||
v-model="dbList"
|
||||
multiple
|
||||
allow-create
|
||||
filterable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="db in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]" :key="db" :label="db" :value="db" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="remark" :label="$t('common.remark')">
|
||||
<el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="$t('common.other')" name="other">
|
||||
<el-form-item prop="sshTunnelMachineId" :label="$t('machine.sshTunnel')">
|
||||
<ssh-tunnel-select v-model="form.sshTunnelMachineId" />
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-form-item ref="tagSelectRef" prop="tagCodePaths" :label="$t('tag.relateTag')" required>
|
||||
<tag-tree-select
|
||||
@change-tag="
|
||||
(tagCodePaths) => {
|
||||
form.tagCodePaths = tagCodePaths;
|
||||
tagSelectRef.validate();
|
||||
}
|
||||
"
|
||||
multiple
|
||||
:select-tags="form.tagCodePaths"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="name" :label="$t('common.name')" required>
|
||||
<el-input v-model.trim="form.name" auto-complete="off"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="mode" label="mode" required>
|
||||
<el-select v-model="form.mode">
|
||||
<el-option label="standalone" value="standalone"> </el-option>
|
||||
<el-option label="cluster" value="cluster"> </el-option>
|
||||
<el-option label="sentinel" value="sentinel"> </el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="host" label="host" required>
|
||||
<el-input v-model.trim="form.host" :placeholder="$t('redis.hostTips')" auto-complete="off" type="textarea"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="username" :label="$t('common.username')">
|
||||
<el-input v-model.trim="form.username"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password" :label="$t('common.password')">
|
||||
<el-input type="password" show-password v-model.trim="form.password" autocomplete="new-password"> </el-input>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.mode == 'sentinel'" prop="redisNodePassword" :label="$t('redis.nodePassword')">
|
||||
<el-input type="password" show-password v-model.trim="form.redisNodePassword" autocomplete="new-password"> </el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="db" label="DB" required>
|
||||
<el-select @change="changeDb" :disabled="form.mode == 'cluster'" v-model="dbList" multiple allow-create filterable style="width: 100%">
|
||||
<el-option v-for="db in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]" :key="db" :label="db" :value="db" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="remark" :label="$t('common.remark')">
|
||||
<el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="sshTunnelMachineId" :label="$t('machine.sshTunnel')">
|
||||
<ssh-tunnel-select v-model="form.sshTunnelMachineId" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { h, render } from 'vue';
|
||||
import CmdExecDialog from './CmdExecDialog.vue';
|
||||
|
||||
export type CmdExecProps = {
|
||||
id: number;
|
||||
db: number | string;
|
||||
cmd: any[];
|
||||
flowProcdef?: any;
|
||||
visible?: boolean;
|
||||
runSuccessFn?: Function;
|
||||
cancelFn?: Function;
|
||||
};
|
||||
|
||||
const showCmdExecBox = (props: CmdExecProps): void => {
|
||||
const propsCancelFn = props.cancelFn;
|
||||
props.cancelFn = () => {
|
||||
propsCancelFn && propsCancelFn();
|
||||
setTimeout(() => {
|
||||
// 销毁组件
|
||||
render(null, document.body);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const vnode = h(CmdExecDialog, { ...props, visible: true });
|
||||
render(vnode, document.body);
|
||||
};
|
||||
|
||||
export default showCmdExecBox;
|
||||
@@ -1,95 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dialog title="待执行cmd" v-model="dialogVisible" :show-close="false" width="600px" @close="cancel">
|
||||
<el-input type="textarea" disabled v-model="state.cmdStr" class="mt-1" :rows="5" />
|
||||
<el-input @keyup.enter="runCmd" ref="remarkInputRef" v-model="remark" placeholder="请输入执行备注" class="mt-1" />
|
||||
|
||||
<div v-if="props.flowProcdef">
|
||||
<el-divider content-position="left">审批节点</el-divider>
|
||||
<procdef-tasks :procdef="props.flowProcdef" />
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
<el-button @click="runCmd" type="primary" :loading="btnLoading">执 行</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, ref, reactive, onMounted } from 'vue';
|
||||
import { ElDialog, ElButton, ElInput, ElMessage, InputInstance, ElDivider } from 'element-plus';
|
||||
|
||||
import ProcdefTasks from '@/views/flow/components/ProcdefTasks.vue';
|
||||
import { redisApi } from '../api';
|
||||
import { CmdExecProps } from './CmdExecBox';
|
||||
|
||||
const props = withDefaults(defineProps<CmdExecProps>(), {});
|
||||
|
||||
const remarkInputRef = ref<InputInstance>();
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
flowProcdef: null as any,
|
||||
cmdStr: '',
|
||||
remark: '',
|
||||
btnLoading: false,
|
||||
});
|
||||
|
||||
const { dialogVisible, remark, btnLoading } = toRefs(state);
|
||||
|
||||
onMounted(() => {
|
||||
show(props);
|
||||
});
|
||||
|
||||
const show = (props: CmdExecProps) => {
|
||||
const cmdArr = props.cmd.map((item: any, index: number) => {
|
||||
if (index === 0) {
|
||||
return item; // 第一个元素直接返回原值
|
||||
}
|
||||
if (typeof item === 'string') {
|
||||
return `'${item}'`; // 字符串加单引号
|
||||
}
|
||||
return item; // 其他类型直接返回
|
||||
});
|
||||
state.cmdStr = cmdArr.join(' ');
|
||||
|
||||
state.dialogVisible = props.visible || true;
|
||||
setTimeout(() => {
|
||||
remarkInputRef.value?.focus();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行cmd
|
||||
*/
|
||||
const runCmd = async () => {
|
||||
if (!state.remark) {
|
||||
ElMessage.error('请输入执行备注信息');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
state.btnLoading = true;
|
||||
await redisApi.runCmd.request({
|
||||
id: props.id,
|
||||
db: props.db,
|
||||
cmd: props.cmd,
|
||||
remark: state.remark,
|
||||
});
|
||||
props.runSuccessFn && props.runSuccessFn();
|
||||
ElMessage.success('工单提交成功');
|
||||
} finally {
|
||||
state.btnLoading = false;
|
||||
cancel();
|
||||
}
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
state.dialogVisible = false;
|
||||
props.cancelFn && props.cancelFn();
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -109,6 +109,14 @@
|
||||
|
||||
<el-tab-pane
|
||||
class="h-full"
|
||||
:disabled="currentTag.type != TagResourceTypeEnum.Tag.value"
|
||||
:label="`${$t('tag.es')} (${resourceCount.es || 0})`"
|
||||
:name="EsTag"
|
||||
>
|
||||
<EsInstanceList lazy ref="esInstanceListRef" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane
|
||||
:disabled="currentTag.type != TagResourceTypeEnum.Tag.value"
|
||||
:label="`Redis (${resourceCount.redis || 0})`"
|
||||
:name="RedisTag"
|
||||
@@ -178,6 +186,7 @@ import { Rules } from '@/common/rule';
|
||||
|
||||
const MachineList = defineAsyncComponent(() => import('../machine/MachineList.vue'));
|
||||
const InstanceList = defineAsyncComponent(() => import('../db/InstanceList.vue'));
|
||||
const EsInstanceList = defineAsyncComponent(() => import('../es/InstanceList.vue'));
|
||||
const RedisList = defineAsyncComponent(() => import('../redis/RedisList.vue'));
|
||||
const MongoList = defineAsyncComponent(() => import('../mongo/MongoList.vue'));
|
||||
|
||||
@@ -196,12 +205,14 @@ const filterTag = ref('');
|
||||
const contextmenuRef = ref();
|
||||
const machineListRef: Ref<any> = ref(null);
|
||||
const dbInstanceListRef: Ref<any> = ref(null);
|
||||
const esInstanceListRef: Ref<any> = ref(null);
|
||||
const redisListRef: Ref<any> = ref(null);
|
||||
const mongoListRef: Ref<any> = ref(null);
|
||||
|
||||
const TagDetail = 'tagDetail';
|
||||
const MachineTag = 'machineTag';
|
||||
const DbTag = 'dbTag';
|
||||
const EsTag = 'EsTag';
|
||||
const RedisTag = 'redisTag';
|
||||
const MongoTag = 'mongoTag';
|
||||
|
||||
@@ -380,6 +391,9 @@ const setNowTabData = () => {
|
||||
case DbTag:
|
||||
dbInstanceListRef.value.search(tagPath);
|
||||
break;
|
||||
case EsTag:
|
||||
esInstanceListRef.value.search(tagPath);
|
||||
break;
|
||||
case RedisTag:
|
||||
redisListRef.value.search(tagPath);
|
||||
break;
|
||||
|
||||
3
server/.gitignore
vendored
3
server/.gitignore
vendored
@@ -9,3 +9,6 @@ mayfly_rsa.pub
|
||||
/db/mysql/
|
||||
# mariadb 程序目录
|
||||
/db/mariadb/
|
||||
|
||||
*.sqlite
|
||||
file
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"mayfly-go/internal/db/application"
|
||||
"mayfly-go/internal/db/application/dto"
|
||||
"mayfly-go/internal/db/config"
|
||||
"mayfly-go/internal/db/dbm"
|
||||
"mayfly-go/internal/db/dbm/dbi"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/imsg"
|
||||
@@ -142,6 +143,8 @@ func (d *Db) ExecSql(rc *req.Ctx) {
|
||||
dbId := getDbId(rc)
|
||||
dbConn, err := d.dbApp.GetDbConn(dbId, form.Db)
|
||||
biz.ErrIsNil(err)
|
||||
defer dbm.PutDbConn(dbConn)
|
||||
|
||||
biz.ErrIsNilAppendErr(d.tagApp.CanAccess(rc.GetLoginAccount().Id, dbConn.Info.CodePath...), "%s")
|
||||
|
||||
global.EventBus.Publish(rc.MetaCtx, event.EventTopicResourceOp, dbConn.Info.CodePath[0])
|
||||
@@ -193,6 +196,7 @@ func (d *Db) ExecSqlFile(rc *req.Ctx) {
|
||||
|
||||
dbConn, err := d.dbApp.GetDbConn(dbId, dbName)
|
||||
biz.ErrIsNil(err)
|
||||
defer dbm.PutDbConn(dbConn)
|
||||
biz.ErrIsNilAppendErr(d.tagApp.CanAccess(rc.GetLoginAccount().Id, dbConn.Info.CodePath...), "%s")
|
||||
rc.ReqParam = fmt.Sprintf("filename: %s -> %s", filename, dbConn.Info.GetLogDesc())
|
||||
|
||||
@@ -226,6 +230,8 @@ func (d *Db) DumpSql(rc *req.Ctx) {
|
||||
la := rc.GetLoginAccount()
|
||||
dbConn, err := d.dbApp.GetDbConn(dbId, dbName)
|
||||
biz.ErrIsNil(err)
|
||||
defer dbm.PutDbConn(dbConn)
|
||||
|
||||
biz.ErrIsNilAppendErr(d.tagApp.CanAccess(la.Id, dbConn.Info.CodePath...), "%s")
|
||||
|
||||
now := time.Now()
|
||||
@@ -354,6 +360,7 @@ func (d *Db) CopyTable(rc *req.Ctx) {
|
||||
|
||||
conn, err := d.dbApp.GetDbConn(form.Id, form.Db)
|
||||
biz.ErrIsNilAppendErr(err, "copy table error: %s")
|
||||
defer dbm.PutDbConn(conn)
|
||||
|
||||
err = conn.GetDialect().CopyTable(copy)
|
||||
if err != nil {
|
||||
@@ -377,5 +384,6 @@ func getDbName(rc *req.Ctx) string {
|
||||
func (d *Db) getDbConn(rc *req.Ctx) *dbi.DbConn {
|
||||
dc, err := d.dbApp.GetDbConn(getDbId(rc), getDbName(rc))
|
||||
biz.ErrIsNil(err)
|
||||
defer dbm.PutDbConn(dc)
|
||||
return dc
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"mayfly-go/internal/db/api/vo"
|
||||
"mayfly-go/internal/db/application"
|
||||
"mayfly-go/internal/db/application/dto"
|
||||
"mayfly-go/internal/db/dbm"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/imsg"
|
||||
fileapp "mayfly-go/internal/file/application"
|
||||
@@ -151,6 +152,8 @@ func (d *DbTransferTask) FileRun(rc *req.Ctx) {
|
||||
|
||||
targetDbConn, err := d.dbApp.GetDbConn(fm.TargetDbId, fm.TargetDbName)
|
||||
biz.ErrIsNilAppendErr(err, "failed to connect to the target database: %s")
|
||||
defer dbm.PutDbConn(targetDbConn)
|
||||
|
||||
biz.ErrIsNilAppendErr(d.tagApp.CanAccess(rc.GetLoginAccount().Id, targetDbConn.Info.CodePath...), "%s")
|
||||
|
||||
filename, reader, err := d.fileApp.GetReader(context.TODO(), tFile.FileKey)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mayfly-go/internal/db/dbm"
|
||||
"mayfly-go/internal/db/dbm/dbi"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
@@ -150,6 +151,7 @@ func (app *dataSyncAppImpl) RunCronJob(ctx context.Context, id uint64) error {
|
||||
return
|
||||
}
|
||||
srcConn, err := app.dbApp.GetDbConn(uint64(task.SrcDbId), task.SrcDbName)
|
||||
defer dbm.PutDbConn(srcConn)
|
||||
if err != nil {
|
||||
logx.ErrorfContext(ctx, "failed to connect to the source database: %s", err.Error())
|
||||
return
|
||||
@@ -204,12 +206,15 @@ func (app *dataSyncAppImpl) doDataSync(ctx context.Context, sql string, task *en
|
||||
|
||||
// 获取源数据库连接
|
||||
srcConn, err := app.dbApp.GetDbConn(uint64(task.SrcDbId), task.SrcDbName)
|
||||
defer dbm.PutDbConn(srcConn)
|
||||
|
||||
if err != nil {
|
||||
return syncLog, errorx.NewBiz("failed to connect to the source database: %s", err.Error())
|
||||
}
|
||||
|
||||
// 获取目标数据库连接
|
||||
targetConn, err := app.dbApp.GetDbConn(uint64(task.TargetDbId), task.TargetDbName)
|
||||
defer dbm.PutDbConn(targetConn)
|
||||
if err != nil {
|
||||
return syncLog, errorx.NewBiz("failed to connect to the target database: %s", err.Error())
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"mayfly-go/internal/db/application/dto"
|
||||
"mayfly-go/internal/db/config"
|
||||
"mayfly-go/internal/db/dbm"
|
||||
"mayfly-go/internal/db/dbm/dbi"
|
||||
"mayfly-go/internal/db/dbm/sqlparser"
|
||||
"mayfly-go/internal/db/dbm/sqlparser/sqlstmt"
|
||||
@@ -283,6 +284,7 @@ func (d *dbSqlExecAppImpl) FlowBizHandle(ctx context.Context, bizHandleParam *fl
|
||||
}
|
||||
|
||||
dbConn, err := d.dbApp.GetDbConn(execSqlBizForm.DbId, execSqlBizForm.DbName)
|
||||
defer dbm.PutDbConn(dbConn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mayfly-go/internal/db/application/dto"
|
||||
"mayfly-go/internal/db/dbm"
|
||||
"mayfly-go/internal/db/dbm/dbi"
|
||||
"mayfly-go/internal/db/dbm/sqlparser"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
@@ -209,6 +210,7 @@ func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64, logId uint
|
||||
// 获取源库连接、目标库连接,判断连接可用性,否则记录日志:xx连接不可用
|
||||
// 获取源库表信息
|
||||
srcConn, err := app.dbApp.GetDbConn(uint64(task.SrcDbId), task.SrcDbName)
|
||||
defer dbm.PutDbConn(srcConn)
|
||||
if err != nil {
|
||||
app.EndTransfer(ctx, logId, taskId, "failed to obtain source db connection", err, nil)
|
||||
return
|
||||
@@ -247,6 +249,7 @@ func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64, logId uint
|
||||
func (app *dbTransferAppImpl) transfer2Db(ctx context.Context, taskId uint64, logId uint64, task *entity.DbTransferTask, srcConn *dbi.DbConn, start time.Time, tables []dbi.Table) {
|
||||
// 获取目标库表信息
|
||||
targetConn, err := app.dbApp.GetDbConn(uint64(task.TargetDbId), task.TargetDbName)
|
||||
defer dbm.PutDbConn(targetConn)
|
||||
if err != nil {
|
||||
app.EndTransfer(ctx, logId, taskId, "failed to get target db connection", err, nil)
|
||||
return
|
||||
|
||||
@@ -3,9 +3,9 @@ package dbi
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"mayfly-go/internal/machine/mcm"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/logx"
|
||||
)
|
||||
@@ -173,14 +173,18 @@ func (d *DbConn) Close() {
|
||||
if err := d.db.Close(); err != nil {
|
||||
logx.Errorf("关闭数据库实例[%s]连接失败: %s", d.Id, err.Error())
|
||||
}
|
||||
// 如果是使用了自己实现的ssh隧道转发,则需要手动将其关闭
|
||||
if d.Info.useSshTunnel {
|
||||
mcm.CloseSshTunnelMachine(d.Info.SshTunnelMachineId, fmt.Sprintf("db:%d", d.Info.Id))
|
||||
}
|
||||
// TODO 关闭实例隧道会影响其他正在使用的连接,所以暂时不关闭
|
||||
//if d.Info.useSshTunnel {
|
||||
// mcm.CloseSshTunnelMachine(d.Info.SshTunnelMachineId, fmt.Sprintf("db:%d", d.Info.Id))
|
||||
//}
|
||||
d.db = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DbConn) Ping() error {
|
||||
return d.db.Ping()
|
||||
}
|
||||
|
||||
// 游标方式遍历查询rows, walkFn error不为nil, 则跳出遍历
|
||||
func (d *DbConn) walkQueryRows(ctx context.Context, selectSql string, walkFn WalkQueryRowsFunc, args ...any) ([]*QueryColumn, error) {
|
||||
cancelCtx, cancelFunc := context.WithCancel(ctx)
|
||||
@@ -238,10 +242,10 @@ func (d *DbConn) walkQueryRows(ctx context.Context, selectSql string, walkFn Wal
|
||||
|
||||
// 包装sql执行相关错误
|
||||
func wrapSqlError(err error) error {
|
||||
if err == context.Canceled {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return errorx.NewBiz("execution cancel")
|
||||
}
|
||||
if err == context.DeadlineExceeded {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return errorx.NewBiz("execution timeout")
|
||||
}
|
||||
return err
|
||||
|
||||
@@ -9,69 +9,83 @@ import (
|
||||
_ "mayfly-go/internal/db/dbm/oracle"
|
||||
_ "mayfly-go/internal/db/dbm/postgres"
|
||||
_ "mayfly-go/internal/db/dbm/sqlite"
|
||||
"mayfly-go/internal/machine/mcm"
|
||||
"mayfly-go/internal/pkg/consts"
|
||||
"mayfly-go/pkg/cache"
|
||||
"mayfly-go/pkg/logx"
|
||||
"sync"
|
||||
"mayfly-go/pkg/pool"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 客户端连接缓存,指定时间内没有访问则会被关闭, key为数据库连接id
|
||||
var connCache = cache.NewTimedCache(consts.DbConnExpireTime, 5*time.Second).
|
||||
WithUpdateAccessTime(true).
|
||||
OnEvicted(func(key any, value any) {
|
||||
logx.Info(fmt.Sprintf("delete db conn cache, id = %s", key))
|
||||
value.(*dbi.DbConn).Close()
|
||||
})
|
||||
var connPool = make(map[string]pool.Pool)
|
||||
var instPool = make(map[uint64]pool.Pool)
|
||||
|
||||
func init() {
|
||||
mcm.AddCheckSshTunnelMachineUseFunc(func(machineId int) bool {
|
||||
// 遍历所有db连接实例,若存在db实例使用该ssh隧道机器,则返回true,表示还在使用中...
|
||||
items := connCache.Items()
|
||||
for _, v := range items {
|
||||
if v.Value.(*dbi.DbConn).Info.SshTunnelMachineId == machineId {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
var mutex sync.Mutex
|
||||
// PutDbConn 释放连接
|
||||
func PutDbConn(c *dbi.DbConn) {
|
||||
if nil == c {
|
||||
return
|
||||
}
|
||||
connId := dbi.GetDbConnId(c.Info.Id, c.Info.Database)
|
||||
if p, ok := connPool[connId]; ok {
|
||||
p.Put(c)
|
||||
}
|
||||
}
|
||||
|
||||
// 从缓存中获取数据库连接信息,若缓存中不存在则会使用回调函数获取dbInfo进行连接并缓存
|
||||
func GetDbConn(dbId uint64, database string, getDbInfo func() (*dbi.DbInfo, error)) (*dbi.DbConn, error) {
|
||||
func getPool(dbId uint64, database string, getDbInfo func() (*dbi.DbInfo, error)) (pool.Pool, error) {
|
||||
connId := dbi.GetDbConnId(dbId, database)
|
||||
|
||||
// connId不为空,则为需要缓存
|
||||
needCache := connId != ""
|
||||
if needCache {
|
||||
load, ok := connCache.Get(connId)
|
||||
if ok {
|
||||
return load.(*dbi.DbConn), nil
|
||||
// 获取连接池,如果没有,则创建一个
|
||||
if p, ok := connPool[connId]; !ok {
|
||||
var err error
|
||||
p, err = pool.NewChannelPool(&pool.Config{
|
||||
InitialCap: 1, //资源池初始连接数
|
||||
MaxCap: 10, //最大空闲连接数
|
||||
MaxIdle: 10, //最大并发连接数
|
||||
IdleTimeout: 10 * time.Minute, // 连接最大空闲时间,过期则失效
|
||||
Factory: func() (interface{}, error) {
|
||||
// 若缓存中不存在,则从回调函数中获取DbInfo
|
||||
dbInfo, err := getDbInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 连接数据库
|
||||
return Conn(dbInfo)
|
||||
},
|
||||
Close: func(v interface{}) error {
|
||||
v.(*dbi.DbConn).Close()
|
||||
return nil
|
||||
},
|
||||
Ping: func(v interface{}) error {
|
||||
return v.(*dbi.DbConn).Ping()
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
connPool[connId] = p
|
||||
instPool[dbId] = p
|
||||
return p, nil
|
||||
} else {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
// GetDbConn 从连接池中获取连接信息,记的用完连接后必须调用 PutDbConn 还回池
|
||||
func GetDbConn(dbId uint64, database string, getDbInfo func() (*dbi.DbInfo, error)) (*dbi.DbConn, error) {
|
||||
|
||||
// 若缓存中不存在,则从回调函数中获取DbInfo
|
||||
dbInfo, err := getDbInfo()
|
||||
p, err := getPool(dbId, database, getDbInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 连接数据库
|
||||
dbConn, err := Conn(dbInfo)
|
||||
// 从连接池中获取一个可用的连接
|
||||
c, err := p.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ec := c.(*dbi.DbConn)
|
||||
return ec, nil
|
||||
|
||||
if needCache {
|
||||
connCache.Put(connId, dbConn)
|
||||
}
|
||||
return dbConn, nil
|
||||
}
|
||||
|
||||
// 使用指定dbInfo信息进行连接
|
||||
@@ -81,16 +95,19 @@ func Conn(di *dbi.DbInfo) (*dbi.DbConn, error) {
|
||||
|
||||
// 根据实例id获取连接
|
||||
func GetDbConnByInstanceId(instanceId uint64) *dbi.DbConn {
|
||||
for _, connItem := range connCache.Items() {
|
||||
conn := connItem.Value.(*dbi.DbConn)
|
||||
if conn.Info.InstanceId == instanceId {
|
||||
return conn
|
||||
if p, ok := instPool[instanceId]; ok {
|
||||
c, err := p.Get()
|
||||
if err != nil {
|
||||
logx.Error(fmt.Sprintf("实例id[%d]连接获取失败:%s", instanceId, err))
|
||||
return nil
|
||||
}
|
||||
return c.(*dbi.DbConn)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 删除db缓存并关闭该数据库所有连接
|
||||
func CloseDb(dbId uint64, db string) {
|
||||
connCache.Delete(dbi.GetDbConnId(dbId, db))
|
||||
delete(connPool, dbi.GetDbConnId(dbId, db))
|
||||
delete(instPool, dbId)
|
||||
}
|
||||
|
||||
7
server/internal/es/api/api.go
Normal file
7
server/internal/es/api/api.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package api
|
||||
|
||||
import "mayfly-go/pkg/ioc"
|
||||
|
||||
func InitIoc() {
|
||||
ioc.Register(new(Instance))
|
||||
}
|
||||
170
server/internal/es/api/es_instance.go
Normal file
170
server/internal/es/api/es_instance.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/may-fly/cast"
|
||||
"mayfly-go/internal/es/api/form"
|
||||
"mayfly-go/internal/es/api/vo"
|
||||
"mayfly-go/internal/es/application"
|
||||
"mayfly-go/internal/es/application/dto"
|
||||
"mayfly-go/internal/es/domain/entity"
|
||||
"mayfly-go/internal/es/esm/esi"
|
||||
"mayfly-go/internal/es/imsg"
|
||||
"mayfly-go/internal/pkg/consts"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
tagentity "mayfly-go/internal/tag/domain/entity"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/req"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Instance struct {
|
||||
inst application.Instance `inject:"T"`
|
||||
tagApp tagapp.TagTree `inject:"T"`
|
||||
resourceAuthCertApp tagapp.ResourceAuthCert `inject:"T"`
|
||||
}
|
||||
|
||||
func (d *Instance) ReqConfs() *req.Confs {
|
||||
reqs := [...]*req.Conf{
|
||||
|
||||
// /es/instance 获取实例列表
|
||||
req.NewGet("", d.Instances),
|
||||
|
||||
// /es/instance/test-conn 测试连接
|
||||
req.NewPost("/test-conn", d.TestConn),
|
||||
|
||||
// /es/instance 添加实例
|
||||
req.NewPost("", d.SaveInstance).Log(req.NewLogSaveI(imsg.LogEsInstSave)),
|
||||
|
||||
// /es/instance/:id 删除实例
|
||||
req.NewDelete(":instanceId", d.DeleteInstance).Log(req.NewLogSaveI(imsg.LogEsInstDelete)),
|
||||
|
||||
// /es/instance/proxy 反向代理es接口请求
|
||||
req.NewAny("/proxy/:instanceId/*path", d.Proxy),
|
||||
}
|
||||
|
||||
return req.NewConfs("/es/instance", reqs[:]...)
|
||||
}
|
||||
|
||||
func (d *Instance) Instances(rc *req.Ctx) {
|
||||
queryCond := req.BindQuery(rc, new(entity.InstanceQuery))
|
||||
|
||||
// 只查询实例,兼容没有录入密码的实例
|
||||
instTags := d.tagApp.GetAccountTags(rc.GetLoginAccount().Id, &tagentity.TagTreeQuery{
|
||||
TypePaths: collx.AsArray(tagentity.NewTypePaths(tagentity.TagTypeEsInstance)),
|
||||
CodePathLikes: collx.AsArray(queryCond.TagPath),
|
||||
})
|
||||
|
||||
// 不存在可操作的数据库,即没有可操作数据
|
||||
if len(instTags) == 0 {
|
||||
rc.ResData = model.NewEmptyPageResult[any]()
|
||||
return
|
||||
}
|
||||
dbInstCodes := tagentity.GetCodesByCodePaths(tagentity.TagTypeEsInstance, instTags.GetCodePaths()...)
|
||||
queryCond.Codes = dbInstCodes
|
||||
|
||||
res, err := d.inst.GetPageList(queryCond)
|
||||
biz.ErrIsNil(err)
|
||||
resVo := model.PageResultConv[*entity.EsInstance, *vo.InstanceListVO](res)
|
||||
instvos := resVo.List
|
||||
|
||||
// 只查询标签
|
||||
certTags := d.tagApp.GetAccountTags(rc.GetLoginAccount().Id, &tagentity.TagTreeQuery{
|
||||
TypePaths: collx.AsArray(tagentity.NewTypePaths(tagentity.TagTypeEsInstance, tagentity.TagTypeAuthCert)),
|
||||
CodePathLikes: collx.AsArray(queryCond.TagPath),
|
||||
})
|
||||
|
||||
// 填充授权凭证信息
|
||||
d.resourceAuthCertApp.FillAuthCertByAcNames(tagentity.GetCodesByCodePaths(tagentity.TagTypeAuthCert, certTags.GetCodePaths()...), collx.ArrayMap(instvos, func(vos *vo.InstanceListVO) tagentity.IAuthCert {
|
||||
return vos
|
||||
})...)
|
||||
|
||||
// 填充标签信息
|
||||
d.tagApp.FillTagInfo(tagentity.TagType(consts.ResourceTypeEsInstance), collx.ArrayMap(instvos, func(insvo *vo.InstanceListVO) tagentity.ITagResource {
|
||||
return insvo
|
||||
})...)
|
||||
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (d *Instance) TestConn(rc *req.Ctx) {
|
||||
fm := &form.InstanceForm{}
|
||||
instance := req.BindJsonAndCopyTo[*entity.EsInstance](rc, fm, new(entity.EsInstance))
|
||||
|
||||
var ac *tagentity.ResourceAuthCert
|
||||
if len(fm.AuthCerts) > 0 {
|
||||
ac = fm.AuthCerts[0]
|
||||
}
|
||||
|
||||
res, err := d.inst.TestConn(instance, ac)
|
||||
biz.ErrIsNil(err)
|
||||
rc.ResData = res
|
||||
}
|
||||
func (d *Instance) SaveInstance(rc *req.Ctx) {
|
||||
fm := &form.InstanceForm{}
|
||||
instance := req.BindJsonAndCopyTo[*entity.EsInstance](rc, fm, new(entity.EsInstance))
|
||||
|
||||
rc.ReqParam = fm
|
||||
id, err := d.inst.SaveInst(rc.MetaCtx, &dto.SaveEsInstance{
|
||||
EsInstance: instance,
|
||||
AuthCerts: fm.AuthCerts,
|
||||
TagCodePaths: fm.TagCodePaths,
|
||||
})
|
||||
biz.ErrIsNil(err)
|
||||
rc.ResData = id
|
||||
}
|
||||
func (d *Instance) DeleteInstance(rc *req.Ctx) {
|
||||
idsStr := rc.PathParam("instanceId")
|
||||
rc.ReqParam = idsStr
|
||||
ids := strings.Split(idsStr, ",")
|
||||
|
||||
for _, v := range ids {
|
||||
biz.ErrIsNilAppendErr(d.inst.Delete(rc.MetaCtx, cast.ToUint64(v)), "delete db instance failed: %s")
|
||||
}
|
||||
}
|
||||
func (d *Instance) Proxy(rc *req.Ctx) {
|
||||
path := rc.PathParam("path")
|
||||
instanceId := getInstanceId(rc)
|
||||
// 去掉request中的 id 和 path参数,否则es会报错
|
||||
|
||||
r := rc.GetRequest()
|
||||
_ = RemoveQueryParam(r, "id", "path")
|
||||
|
||||
err := d.inst.DoConn(instanceId, func(conn *esi.EsConn) error {
|
||||
conn.Proxy(rc.GetWriter(), r, path)
|
||||
return nil
|
||||
})
|
||||
|
||||
biz.ErrIsNil(err)
|
||||
}
|
||||
|
||||
func RemoveQueryParam(req *http.Request, paramNames ...string) error {
|
||||
parsedURL, err := url.ParseRequestURI(req.RequestURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Get the query parameters
|
||||
queryParams, err := url.ParseQuery(parsedURL.RawQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Remove the specified query parameter
|
||||
for i := range paramNames {
|
||||
delete(queryParams, paramNames[i])
|
||||
}
|
||||
// Reconstruct the query string
|
||||
parsedURL.RawQuery = queryParams.Encode()
|
||||
// Update the request URL
|
||||
req.URL = parsedURL
|
||||
req.RequestURI = parsedURL.String()
|
||||
return nil
|
||||
}
|
||||
|
||||
func getInstanceId(rc *req.Ctx) uint64 {
|
||||
instanceId := rc.PathParamInt("instanceId")
|
||||
biz.IsTrue(instanceId > 0, "instanceId error")
|
||||
return uint64(instanceId)
|
||||
}
|
||||
18
server/internal/es/api/form/instance.go
Normal file
18
server/internal/es/api/form/instance.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package form
|
||||
|
||||
import (
|
||||
tagentity "mayfly-go/internal/tag/domain/entity"
|
||||
)
|
||||
|
||||
type InstanceForm struct {
|
||||
Id uint64 `json:"id"`
|
||||
Name string `binding:"required" json:"name"`
|
||||
Host string `binding:"required" json:"host"`
|
||||
Port int `binding:"required" json:"port"`
|
||||
Version string `json:"version"`
|
||||
Remark string `json:"remark"`
|
||||
SshTunnelMachineId int `json:"sshTunnelMachineId"`
|
||||
|
||||
AuthCerts []*tagentity.ResourceAuthCert `json:"authCerts"` // 资产授权凭证信息列表
|
||||
TagCodePaths []string `binding:"required" json:"tagCodePaths"`
|
||||
}
|
||||
31
server/internal/es/api/vo/instance.go
Normal file
31
server/internal/es/api/vo/instance.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package vo
|
||||
|
||||
import (
|
||||
tagentity "mayfly-go/internal/tag/domain/entity"
|
||||
"time"
|
||||
)
|
||||
|
||||
type InstanceListVO struct {
|
||||
tagentity.AuthCerts // 授权凭证信息
|
||||
tagentity.ResourceTags
|
||||
|
||||
Id *int64 `json:"id"`
|
||||
Code string `json:"code"`
|
||||
Name *string `json:"name"`
|
||||
Host *string `json:"host"`
|
||||
Port *int `json:"port"`
|
||||
Version *string `json:"version"`
|
||||
CreateTime *time.Time `json:"createTime"`
|
||||
Creator *string `json:"creator"`
|
||||
CreatorId *int64 `json:"creatorId"`
|
||||
|
||||
UpdateTime *time.Time `json:"updateTime"`
|
||||
Modifier *string `json:"modifier"`
|
||||
ModifierId *int64 `json:"modifierId"`
|
||||
|
||||
SshTunnelMachineId int `json:"sshTunnelMachineId"`
|
||||
}
|
||||
|
||||
func (i *InstanceListVO) GetCode() string {
|
||||
return i.Code
|
||||
}
|
||||
15
server/internal/es/application/application.go
Normal file
15
server/internal/es/application/application.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"mayfly-go/pkg/ioc"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func InitIoc() {
|
||||
ioc.Register(new(instanceAppImpl), ioc.WithComponentName("EsInstanceApp"))
|
||||
}
|
||||
|
||||
func Init() {
|
||||
sync.OnceFunc(func() {
|
||||
})()
|
||||
}
|
||||
12
server/internal/es/application/dto/dto.go
Normal file
12
server/internal/es/application/dto/dto.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/es/domain/entity"
|
||||
tagentity "mayfly-go/internal/tag/domain/entity"
|
||||
)
|
||||
|
||||
type SaveEsInstance struct {
|
||||
EsInstance *entity.EsInstance
|
||||
AuthCerts []*tagentity.ResourceAuthCert
|
||||
TagCodePaths []string
|
||||
}
|
||||
284
server/internal/es/application/es_instance.go
Normal file
284
server/internal/es/application/es_instance.go
Normal file
@@ -0,0 +1,284 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"mayfly-go/internal/es/application/dto"
|
||||
"mayfly-go/internal/es/domain/entity"
|
||||
"mayfly-go/internal/es/domain/repository"
|
||||
"mayfly-go/internal/es/esm/esi"
|
||||
"mayfly-go/internal/es/imsg"
|
||||
"mayfly-go/internal/pkg/consts"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
tagdto "mayfly-go/internal/tag/application/dto"
|
||||
tagentity "mayfly-go/internal/tag/domain/entity"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/pool"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
"mayfly-go/pkg/utils/structx"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Instance interface {
|
||||
base.App[*entity.EsInstance]
|
||||
// GetPageList 分页获取数据库实例
|
||||
GetPageList(condition *entity.InstanceQuery, orderBy ...string) (*model.PageResult[*entity.EsInstance], error)
|
||||
|
||||
// DoConn 获取连接并执行函数
|
||||
DoConn(instanceId uint64, fn func(*esi.EsConn) error) error
|
||||
|
||||
TestConn(instance *entity.EsInstance, ac *tagentity.ResourceAuthCert) (map[string]any, error)
|
||||
|
||||
SaveInst(ctx context.Context, d *dto.SaveEsInstance) (uint64, error)
|
||||
|
||||
Delete(ctx context.Context, instanceId uint64) error
|
||||
}
|
||||
|
||||
var _ Instance = &instanceAppImpl{}
|
||||
|
||||
var connPool = make(map[uint64]pool.Pool)
|
||||
|
||||
type instanceAppImpl struct {
|
||||
base.AppImpl[*entity.EsInstance, repository.EsInstance]
|
||||
|
||||
tagApp tagapp.TagTree `inject:"T"`
|
||||
resourceAuthCertApp tagapp.ResourceAuthCert `inject:"T"`
|
||||
}
|
||||
|
||||
// GetPageList 分页获取数据库实例
|
||||
func (app *instanceAppImpl) GetPageList(condition *entity.InstanceQuery, orderBy ...string) (*model.PageResult[*entity.EsInstance], error) {
|
||||
return app.GetRepo().GetInstanceList(condition, orderBy...)
|
||||
}
|
||||
|
||||
func (app *instanceAppImpl) DoConn(instanceId uint64, fn func(*esi.EsConn) error) error {
|
||||
// 通过实例id获取实例连接信息
|
||||
p, err := app.getPool(instanceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 从连接池中获取一个可用的连接
|
||||
c, err := p.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ec := c.(*esi.EsConn)
|
||||
|
||||
// 用完后放回连接池
|
||||
defer p.Put(c)
|
||||
|
||||
return fn(ec)
|
||||
}
|
||||
|
||||
func (app *instanceAppImpl) getPool(instanceId uint64) (pool.Pool, error) {
|
||||
// 获取连接池,如果没有,则创建一个
|
||||
if p, ok := connPool[instanceId]; !ok {
|
||||
var err error
|
||||
p, err = pool.NewChannelPool(&pool.Config{
|
||||
InitialCap: 1, //资源池初始连接数
|
||||
MaxCap: 10, //最大空闲连接数
|
||||
MaxIdle: 10, //最大并发连接数
|
||||
IdleTimeout: 10 * time.Minute, // 连接最大空闲时间,过期则失效
|
||||
Factory: func() (interface{}, error) {
|
||||
return app.createConn(instanceId)
|
||||
},
|
||||
Close: func(v interface{}) error {
|
||||
return v.(*esi.EsConn).Close()
|
||||
},
|
||||
Ping: func(v interface{}) error {
|
||||
return v.(*esi.EsConn).Ping()
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
connPool[instanceId] = p
|
||||
return p, nil
|
||||
} else {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
func (app *instanceAppImpl) createConn(instanceId uint64) (*esi.EsConn, error) {
|
||||
// 缓存不存在,则重新连接
|
||||
instance, err := app.GetById(instanceId)
|
||||
if err != nil {
|
||||
return nil, errorx.NewBiz("es instance not found")
|
||||
}
|
||||
|
||||
ei, err := app.ToEsInfo(instance, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ei.CodePath = app.tagApp.ListTagPathByTypeAndCode(int8(tagentity.TagTypeEsInstance), instance.Code)
|
||||
|
||||
conn, _, err := ei.Conn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 缓存连接信息
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (app *instanceAppImpl) ToEsInfo(instance *entity.EsInstance, ac *tagentity.ResourceAuthCert) (*esi.EsInfo, error) {
|
||||
ei := new(esi.EsInfo)
|
||||
ei.InstanceId = instance.Id
|
||||
structx.Copy(ei, instance)
|
||||
ei.OriginUrl = fmt.Sprintf("http://%s:%d", instance.Host, instance.Port)
|
||||
|
||||
if ac != nil {
|
||||
if ac.Ciphertext == "" && ac.Name != "" {
|
||||
ac1, err := app.resourceAuthCertApp.GetAuthCert(ac.Name)
|
||||
if err == nil {
|
||||
ac = ac1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if instance.Code != "" {
|
||||
ac2, err := app.resourceAuthCertApp.GetResourceAuthCert(tagentity.TagTypeEsInstance, instance.Code)
|
||||
if err == nil {
|
||||
ac = ac2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ac != nil && ac.Ciphertext != "" {
|
||||
ei.Username = ac.Username
|
||||
ei.Password = ac.Ciphertext
|
||||
}
|
||||
|
||||
return ei, nil
|
||||
}
|
||||
|
||||
func (app *instanceAppImpl) TestConn(instance *entity.EsInstance, ac *tagentity.ResourceAuthCert) (map[string]any, error) {
|
||||
instance.Network = instance.GetNetwork()
|
||||
|
||||
ei, err := app.ToEsInfo(instance, ac)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, res, err := ei.Conn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
|
||||
}
|
||||
|
||||
func (app *instanceAppImpl) SaveInst(ctx context.Context, instance *dto.SaveEsInstance) (uint64, error) {
|
||||
instanceEntity := instance.EsInstance
|
||||
// 默认tcp连接
|
||||
instanceEntity.Network = instanceEntity.GetNetwork()
|
||||
resourceType := consts.ResourceTypeEsInstance
|
||||
authCerts := instance.AuthCerts
|
||||
tagCodePaths := instance.TagCodePaths
|
||||
|
||||
// 查找是否存在该库
|
||||
oldInstance := &entity.EsInstance{
|
||||
Host: instanceEntity.Host,
|
||||
Port: instanceEntity.Port,
|
||||
SshTunnelMachineId: instanceEntity.SshTunnelMachineId,
|
||||
}
|
||||
|
||||
err := app.GetByCond(oldInstance)
|
||||
if instanceEntity.Id == 0 {
|
||||
if err == nil {
|
||||
return 0, errorx.NewBizI(ctx, imsg.ErrEsInstExist)
|
||||
}
|
||||
instanceEntity.Code = stringx.Rand(10)
|
||||
|
||||
return instanceEntity.Id, app.Tx(ctx, func(ctx context.Context) error {
|
||||
return app.Insert(ctx, instanceEntity)
|
||||
}, func(ctx context.Context) error {
|
||||
return app.resourceAuthCertApp.RelateAuthCert(ctx, &tagdto.RelateAuthCert{
|
||||
ResourceCode: instanceEntity.Code,
|
||||
ResourceType: tagentity.TagType(resourceType),
|
||||
AuthCerts: authCerts,
|
||||
})
|
||||
}, func(ctx context.Context) error {
|
||||
return app.tagApp.SaveResourceTag(ctx, &tagdto.SaveResourceTag{
|
||||
ResourceTag: app.genEsInstanceResourceTag(instanceEntity, authCerts),
|
||||
ParentTagCodePaths: tagCodePaths,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 如果存在该库,则校验修改的库是否为该库
|
||||
if err == nil {
|
||||
if oldInstance.Id != instanceEntity.Id {
|
||||
return 0, errorx.NewBizI(ctx, imsg.ErrEsInstExist)
|
||||
}
|
||||
} else {
|
||||
// 根据host等未查到旧数据,则需要根据id重新获取,因为后续需要使用到code
|
||||
oldInstance, err = app.GetById(instanceEntity.Id)
|
||||
if err != nil {
|
||||
return 0, errorx.NewBiz("db instance not found")
|
||||
}
|
||||
}
|
||||
|
||||
return oldInstance.Id, app.Tx(ctx, func(ctx context.Context) error {
|
||||
return app.UpdateById(ctx, instanceEntity)
|
||||
}, func(ctx context.Context) error {
|
||||
return app.resourceAuthCertApp.RelateAuthCert(ctx, &tagdto.RelateAuthCert{
|
||||
ResourceCode: oldInstance.Code,
|
||||
ResourceType: tagentity.TagType(resourceType),
|
||||
AuthCerts: authCerts,
|
||||
})
|
||||
}, func(ctx context.Context) error {
|
||||
if instanceEntity.Name != oldInstance.Name {
|
||||
if err := app.tagApp.UpdateTagName(ctx, tagentity.TagTypeDbInstance, oldInstance.Code, instanceEntity.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return app.tagApp.SaveResourceTag(ctx, &tagdto.SaveResourceTag{
|
||||
ResourceTag: app.genEsInstanceResourceTag(oldInstance, authCerts),
|
||||
ParentTagCodePaths: tagCodePaths,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (app *instanceAppImpl) genEsInstanceResourceTag(ei *entity.EsInstance, authCerts []*tagentity.ResourceAuthCert) *tagdto.ResourceTag {
|
||||
|
||||
// 授权证书对应的tag
|
||||
authCertTags := collx.ArrayMap[*tagentity.ResourceAuthCert, *tagdto.ResourceTag](authCerts, func(val *tagentity.ResourceAuthCert) *tagdto.ResourceTag {
|
||||
return &tagdto.ResourceTag{
|
||||
Code: val.Name,
|
||||
Name: val.Username,
|
||||
Type: tagentity.TagTypeAuthCert,
|
||||
}
|
||||
})
|
||||
|
||||
// es实例
|
||||
return &tagdto.ResourceTag{
|
||||
Code: ei.Code,
|
||||
Name: ei.Name,
|
||||
Type: tagentity.TagTypeEsInstance,
|
||||
Children: authCertTags,
|
||||
}
|
||||
}
|
||||
|
||||
func (app *instanceAppImpl) Delete(ctx context.Context, instanceId uint64) error {
|
||||
instance, err := app.GetById(instanceId)
|
||||
if err != nil {
|
||||
return errorx.NewBiz("db instnace not found")
|
||||
}
|
||||
|
||||
return app.Tx(ctx, func(ctx context.Context) error {
|
||||
// 删除该实例
|
||||
return app.DeleteById(ctx, instanceId)
|
||||
}, func(ctx context.Context) error {
|
||||
// 删除该实例关联的授权凭证信息
|
||||
return app.resourceAuthCertApp.RelateAuthCert(ctx, &tagdto.RelateAuthCert{
|
||||
ResourceCode: instance.Code,
|
||||
ResourceType: tagentity.TagType(consts.ResourceTypeEsInstance),
|
||||
})
|
||||
}, func(ctx context.Context) error {
|
||||
// 删除该实例关联的tag信息
|
||||
return app.tagApp.DeleteTagByParam(ctx, &tagdto.DelResourceTag{
|
||||
ResourceCode: instance.Code,
|
||||
ResourceType: tagentity.TagType(consts.ResourceTypeEsInstance),
|
||||
})
|
||||
})
|
||||
}
|
||||
36
server/internal/es/domain/entity/es_instance.go
Normal file
36
server/internal/es/domain/entity/es_instance.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
type EsInstance struct {
|
||||
model.Model
|
||||
|
||||
Code string `json:"code" gorm:"size:32;not null;"`
|
||||
Name string `json:"name" gorm:"size:32;not null;"`
|
||||
Host string `json:"host" gorm:"size:255;not null;"`
|
||||
Port int `json:"port"`
|
||||
Network string `json:"network" gorm:"size:20;"`
|
||||
Version string `json:"version" gorm:"size:50;"`
|
||||
AuthCertName string `json:"authCertName" gorm:"size:255;"`
|
||||
SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id
|
||||
}
|
||||
|
||||
func (d *EsInstance) TableName() string {
|
||||
return "t_es_instance"
|
||||
}
|
||||
|
||||
// 获取es连接网络, 若没有使用ssh隧道,则直接返回。否则返回拼接的网络需要注册至指定dial
|
||||
func (d *EsInstance) GetNetwork() string {
|
||||
network := d.Network
|
||||
if d.SshTunnelMachineId <= 0 {
|
||||
if network == "" {
|
||||
return "tcp"
|
||||
} else {
|
||||
return network
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("es+ssh:%d", d.SshTunnelMachineId)
|
||||
}
|
||||
16
server/internal/es/domain/entity/query.go
Normal file
16
server/internal/es/domain/entity/query.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package entity
|
||||
|
||||
import "mayfly-go/pkg/model"
|
||||
|
||||
// InstanceQuery 数据库实例查询
|
||||
type InstanceQuery struct {
|
||||
model.PageParam
|
||||
|
||||
Id uint64 `json:"id" form:"id"`
|
||||
Name string `json:"name" form:"name"`
|
||||
Code string `json:"code" form:"code"`
|
||||
Host string `json:"host" form:"host"`
|
||||
TagPath string `json:"tagPath" form:"tagPath"`
|
||||
Keyword string `json:"keyword" form:"keyword"`
|
||||
Codes []string
|
||||
}
|
||||
14
server/internal/es/domain/repository/es_instance.go
Normal file
14
server/internal/es/domain/repository/es_instance.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/es/domain/entity"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
type EsInstance interface {
|
||||
base.Repo[*entity.EsInstance]
|
||||
|
||||
// 分页获取数据库实例信息列表
|
||||
GetInstanceList(condition *entity.InstanceQuery, orderBy ...string) (*model.PageResult[*entity.EsInstance], error)
|
||||
}
|
||||
29
server/internal/es/esm/esi/buffer_pool.go
Normal file
29
server/internal/es/esm/esi/buffer_pool.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package esi
|
||||
|
||||
import (
|
||||
"net/http/httputil"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type BufferPool struct {
|
||||
pool *sync.Pool
|
||||
}
|
||||
|
||||
// 需要实现 httputil.BufferPool
|
||||
var _ httputil.BufferPool = (*BufferPool)(nil)
|
||||
|
||||
func NewBufferPool() *BufferPool {
|
||||
return &BufferPool{&sync.Pool{
|
||||
New: func() interface{} {
|
||||
return make([]byte, 32*1024)
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func (b *BufferPool) Get() []byte {
|
||||
return b.pool.Get().([]byte)
|
||||
}
|
||||
|
||||
func (b *BufferPool) Put(buf []byte) {
|
||||
b.pool.Put(buf)
|
||||
}
|
||||
55
server/internal/es/esm/esi/conn.go
Normal file
55
server/internal/es/esm/esi/conn.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package esi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mayfly-go/internal/machine/mcm"
|
||||
"mayfly-go/pkg/logx"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type EsConn struct {
|
||||
Id uint64
|
||||
Info *EsInfo
|
||||
|
||||
proxy *httputil.ReverseProxy
|
||||
}
|
||||
|
||||
// StartProxy 开始代理
|
||||
func (d *EsConn) StartProxy() error {
|
||||
// 目标 URL
|
||||
targetURL, err := url.Parse(d.Info.baseUrl)
|
||||
if err != nil {
|
||||
logx.Errorf("Error parsing URL: %v", err)
|
||||
return err
|
||||
}
|
||||
// 创建反向代理
|
||||
d.proxy = httputil.NewSingleHostReverseProxy(targetURL)
|
||||
// 设置 proxy buffer pool
|
||||
d.proxy.BufferPool = NewBufferPool()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *EsConn) Proxy(w http.ResponseWriter, r *http.Request, path string) {
|
||||
r.URL.Path = path
|
||||
if d.Info.authorization != "" {
|
||||
r.Header.Set("Authorization", d.Info.authorization)
|
||||
}
|
||||
r.Header.Set("connection", "keep-alive")
|
||||
r.Header.Set("Accept", "application/json")
|
||||
d.proxy.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (d *EsConn) Close() error {
|
||||
// 如果是使用了ssh隧道转发,则需要手动将其关闭
|
||||
if d.Info.useSshTunnel {
|
||||
mcm.CloseSshTunnelMachine(uint64(d.Info.SshTunnelMachineId), fmt.Sprintf("es:%d", d.Id))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *EsConn) Ping() error {
|
||||
_, err := d.Info.Ping()
|
||||
return err
|
||||
}
|
||||
142
server/internal/es/esm/esi/es_info.go
Normal file
142
server/internal/es/esm/esi/es_info.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package esi
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
machineapp "mayfly-go/internal/machine/application"
|
||||
"mayfly-go/internal/machine/mcm"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/httpx"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/utils/structx"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type EsVersion string
|
||||
|
||||
type EsInfo struct {
|
||||
model.ExtraData // 连接需要的其他额外参数(json字符串),如oracle数据库需要指定sid等
|
||||
|
||||
InstanceId uint64 // 实例id
|
||||
Name string
|
||||
|
||||
Host string
|
||||
Port int
|
||||
Network string
|
||||
Username string
|
||||
Password string
|
||||
|
||||
Version EsVersion // 数据库版本信息,用于语法兼容
|
||||
DefaultVersion bool // 经过查询数据库版本信息后,是否仍然使用默认版本
|
||||
|
||||
CodePath []string
|
||||
SshTunnelMachineId int
|
||||
useSshTunnel bool // 是否使用系统自己实现的ssh隧道连接,而非库自带的
|
||||
|
||||
OriginUrl string // 原始url
|
||||
baseUrl string // 发起http请求的基本url
|
||||
authorization string // 发起http请求携带的认证信息
|
||||
}
|
||||
|
||||
// 获取记录日志的描述
|
||||
func (di *EsInfo) GetLogDesc() string {
|
||||
return fmt.Sprintf("ES[id=%d, tag=%s, name=%s, ip=%s:%d]", di.InstanceId, di.CodePath, di.Name, di.Host, di.Port)
|
||||
}
|
||||
|
||||
// 连接数据库
|
||||
func (di *EsInfo) Conn() (*EsConn, map[string]any, error) {
|
||||
// 使用basic加密用户名和密码
|
||||
if di.Username != "" && di.Password != "" {
|
||||
encodeString := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", di.Username, di.Password)))
|
||||
di.authorization = fmt.Sprintf("Basic %s", encodeString)
|
||||
}
|
||||
|
||||
// 使用ssh隧道
|
||||
err := di.IfUseSshTunnelChangeIpPort()
|
||||
if err != nil {
|
||||
logx.Errorf("es ssh failed: %s, err:%s", di.baseUrl, err.Error())
|
||||
return nil, nil, errorx.NewBiz("es ssh failed: %s", err.Error())
|
||||
}
|
||||
|
||||
// 尝试获取es版本信息,调用接口:get /
|
||||
res, err := di.Ping()
|
||||
if err != nil {
|
||||
logx.Errorf("es ping failed: %s, err:%s", di.baseUrl, err.Error())
|
||||
return nil, nil, errorx.NewBiz("es ping failed: %s", err.Error())
|
||||
}
|
||||
|
||||
esc := &EsConn{Id: di.InstanceId, Info: di}
|
||||
err = esc.StartProxy()
|
||||
if err != nil {
|
||||
logx.Errorf("es porxy failed: %s, err:%s", di.baseUrl, err.Error())
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if di.OriginUrl != di.baseUrl {
|
||||
logx.Infof("es porxy success: %s => %s", di.baseUrl, di.OriginUrl)
|
||||
} else {
|
||||
logx.Infof("es porxy success: %s", di.baseUrl)
|
||||
}
|
||||
|
||||
return esc, res, nil
|
||||
}
|
||||
|
||||
func (di *EsInfo) Ping() (map[string]any, error) {
|
||||
return di.ExecApi("get", "", nil)
|
||||
}
|
||||
|
||||
// ExecApi 执行api
|
||||
func (di *EsInfo) ExecApi(method, path string, data any, timeoutSecond ...int) (map[string]any, error) {
|
||||
request := httpx.NewReq(di.baseUrl + path)
|
||||
if di.authorization != "" {
|
||||
request.Header("Authorization", di.authorization)
|
||||
}
|
||||
if len(timeoutSecond) > 0 { // 设置超时时间
|
||||
request.Timeout(timeoutSecond[0])
|
||||
}
|
||||
|
||||
switch strings.ToUpper(method) {
|
||||
case http.MethodGet:
|
||||
if data != nil {
|
||||
return request.GetByQuery(structx.ToMap(data)).BodyToMap()
|
||||
}
|
||||
return request.Get().BodyToMap()
|
||||
|
||||
case http.MethodPost:
|
||||
return request.PostObj(data).BodyToMap()
|
||||
case http.MethodPut:
|
||||
return request.PutObj(data).BodyToMap()
|
||||
}
|
||||
|
||||
return nil, errorx.NewBiz("不支持的请求方法: %s", method)
|
||||
|
||||
}
|
||||
|
||||
// 如果使用了ssh隧道,将其host port改变其本地映射host port
|
||||
func (di *EsInfo) IfUseSshTunnelChangeIpPort() error {
|
||||
// 开启ssh隧道
|
||||
if di.SshTunnelMachineId > 0 {
|
||||
stm, err := GetSshTunnel(di.SshTunnelMachineId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
exposedIp, exposedPort, err := stm.OpenSshTunnel(fmt.Sprintf("es:%d", di.InstanceId), di.Host, di.Port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
di.Host = exposedIp
|
||||
di.Port = exposedPort
|
||||
di.useSshTunnel = true
|
||||
di.baseUrl = fmt.Sprintf("http://%s:%d", exposedIp, exposedPort)
|
||||
} else {
|
||||
di.baseUrl = fmt.Sprintf("http://%s:%d", di.Host, di.Port)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 根据ssh tunnel机器id返回ssh tunnel
|
||||
func GetSshTunnel(sshTunnelMachineId int) (*mcm.SshTunnelMachine, error) {
|
||||
return machineapp.GetMachineApp().GetSshTunnelMachine(sshTunnelMachineId)
|
||||
}
|
||||
9
server/internal/es/imsg/en.go
Normal file
9
server/internal/es/imsg/en.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package imsg
|
||||
|
||||
import "mayfly-go/pkg/i18n"
|
||||
|
||||
var En = map[i18n.MsgId]string{
|
||||
LogEsInstSave: "Es - Save Instance",
|
||||
LogEsInstDelete: "Es - Delete Instance",
|
||||
ErrEsInstExist: "The es instance already exists",
|
||||
}
|
||||
19
server/internal/es/imsg/imsg.go
Normal file
19
server/internal/es/imsg/imsg.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package imsg
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/pkg/consts"
|
||||
"mayfly-go/pkg/i18n"
|
||||
)
|
||||
|
||||
func init() {
|
||||
i18n.AppendLangMsg(i18n.Zh_CN, Zh_CN)
|
||||
i18n.AppendLangMsg(i18n.En, En)
|
||||
}
|
||||
|
||||
const (
|
||||
// es inst
|
||||
LogEsInstDelete = iota + consts.ImsgNumEs
|
||||
LogEsInstSave
|
||||
|
||||
ErrEsInstExist
|
||||
)
|
||||
9
server/internal/es/imsg/zh_cn.go
Normal file
9
server/internal/es/imsg/zh_cn.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package imsg
|
||||
|
||||
import "mayfly-go/pkg/i18n"
|
||||
|
||||
var Zh_CN = map[i18n.MsgId]string{
|
||||
LogEsInstSave: "ES-保存实例",
|
||||
LogEsInstDelete: "ES-删除实例",
|
||||
ErrEsInstExist: "ES实例已存在",
|
||||
}
|
||||
34
server/internal/es/infrastructure/persistence/es_instance.go
Normal file
34
server/internal/es/infrastructure/persistence/es_instance.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/es/domain/entity"
|
||||
"mayfly-go/internal/es/domain/repository"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
type instanceRepoImpl struct {
|
||||
base.RepoImpl[*entity.EsInstance]
|
||||
}
|
||||
|
||||
func NewInstanceRepo() repository.EsInstance {
|
||||
return &instanceRepoImpl{}
|
||||
}
|
||||
|
||||
// 分页获取数据库信息列表
|
||||
func (d *instanceRepoImpl) GetInstanceList(condition *entity.InstanceQuery, orderBy ...string) (*model.PageResult[*entity.EsInstance], error) {
|
||||
qd := model.NewCond().
|
||||
Eq("id", condition.Id).
|
||||
Eq("host", condition.Host).
|
||||
Like("name", condition.Name).
|
||||
Like("code", condition.Code).
|
||||
In("code", condition.Codes)
|
||||
|
||||
keyword := condition.Keyword
|
||||
if keyword != "" {
|
||||
keyword = "%" + keyword + "%"
|
||||
qd.And("host like ? or name like ? or code like ?", keyword, keyword, keyword)
|
||||
}
|
||||
|
||||
return d.PageByCond(qd, condition.PageParam)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"mayfly-go/pkg/ioc"
|
||||
)
|
||||
|
||||
func InitIoc() {
|
||||
ioc.Register(NewInstanceRepo(), ioc.WithComponentName("EsInstanceRepo"))
|
||||
}
|
||||
18
server/internal/es/init/init.go
Normal file
18
server/internal/es/init/init.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package init
|
||||
|
||||
import (
|
||||
"mayfly-go/initialize"
|
||||
"mayfly-go/internal/es/api"
|
||||
"mayfly-go/internal/es/application"
|
||||
"mayfly-go/internal/es/infrastructure/persistence"
|
||||
)
|
||||
|
||||
func init() {
|
||||
initialize.AddInitIocFunc(func() {
|
||||
persistence.InitIoc()
|
||||
application.InitIoc()
|
||||
api.InitIoc()
|
||||
})
|
||||
|
||||
initialize.AddInitFunc(application.Init)
|
||||
}
|
||||
41
server/internal/es/readme.md
Normal file
41
server/internal/es/readme.md
Normal file
@@ -0,0 +1,41 @@
|
||||
|
||||
# es 模块开发步骤
|
||||
|
||||
## 1、模块设计:
|
||||
|
||||
### es实例
|
||||
|
||||
- 支持录入es实例:所属标签、ip、端口、账号、密码、ssh跳板机、
|
||||
|
||||
### es操作
|
||||
|
||||
- 参照db操作,右侧标签树,实例列表,实例下子菜单:
|
||||
- 索引管理:支持右键菜单:刷新、添加索引、显示系统索引(以.开头的索引名)
|
||||
- 索引设置:过滤索引名^\..*
|
||||
- 索引列表:展开索引名列表,以索引名排序,支持右键菜单:复制名字、添加别名、索引迁移、关闭/打开索引、删除索引
|
||||
- 索引详情:
|
||||
- 索引增删改查
|
||||
- 索引迁移:
|
||||
- 如果 Mapping 中字段已经定义就不能修改其字段的类型等属性了,同时也不能改变分片的数量, 可以使用 Reindex API 来解决这个问题。
|
||||
- 支持迁移到其他实例的指定索引,默认选中当前实例
|
||||
- 数据浏览:
|
||||
- 跳转到:基础搜索、高级搜索
|
||||
- 基础搜索:
|
||||
- 保存es查询条件,指定查询名,关联:实例id、索引名
|
||||
- 可视化组装查询条件
|
||||
- 加载保存的查询条件列表、删除、修改、应用
|
||||
- 高级搜索:自己拼接查询json,返回并展示查询结果json
|
||||
- 仪表盘:一些指标数据:基本信息、节点信息、插件信息、集群状态、集群健康值
|
||||
- 设置:一些公共设置
|
||||
|
||||
## 开发路线
|
||||
1、后端封装所需接口
|
||||
|
||||
参考 src/components/es/api/ClusterApi.ts
|
||||
- 实例管理接口设计:/es/instance/:实例id/:index/具体接口
|
||||
- 实例代理接口设计:/es/instance/proxy/:实例id/:官方api接口
|
||||
|
||||
2、前端参考es-client相关页面逻辑
|
||||
|
||||
参照: https://gitee.com/liuzongyang/es-client
|
||||
|
||||
@@ -137,6 +137,8 @@ func (m *Machine) SimpleMachieInfo(rc *req.Ctx) {
|
||||
func (m *Machine) MachineStats(rc *req.Ctx) {
|
||||
cli, err := m.machineApp.GetCli(GetMachineId(rc))
|
||||
biz.ErrIsNilAppendErr(err, "connection error: %s")
|
||||
defer mcm.PutMachineCli(cli)
|
||||
|
||||
rc.ResData = cli.GetAllStats()
|
||||
}
|
||||
|
||||
@@ -198,6 +200,8 @@ func (m *Machine) GetProcess(rc *req.Ctx) {
|
||||
|
||||
cli, err := m.machineApp.GetCli(GetMachineId(rc))
|
||||
biz.ErrIsNilAppendErr(err, "connection error: %s")
|
||||
defer mcm.PutMachineCli(cli)
|
||||
|
||||
biz.ErrIsNilAppendErr(m.tagTreeApp.CanAccess(rc.GetLoginAccount().Id, cli.Info.CodePath...), "%s")
|
||||
|
||||
res, err := cli.Run(cmd)
|
||||
@@ -212,6 +216,8 @@ func (m *Machine) KillProcess(rc *req.Ctx) {
|
||||
|
||||
cli, err := m.machineApp.GetCli(GetMachineId(rc))
|
||||
biz.ErrIsNilAppendErr(err, "connection error: %s")
|
||||
defer mcm.PutMachineCli(cli)
|
||||
|
||||
biz.ErrIsNilAppendErr(m.tagTreeApp.CanAccess(rc.GetLoginAccount().Id, cli.Info.CodePath...), "%s")
|
||||
|
||||
res, err := cli.Run("sudo kill -9 " + pid)
|
||||
@@ -221,6 +227,8 @@ func (m *Machine) KillProcess(rc *req.Ctx) {
|
||||
func (m *Machine) GetUsers(rc *req.Ctx) {
|
||||
cli, err := m.machineApp.GetCli(GetMachineId(rc))
|
||||
biz.ErrIsNilAppendErr(err, "connection error: %s")
|
||||
defer mcm.PutMachineCli(cli)
|
||||
|
||||
res, err := cli.GetUsers()
|
||||
biz.ErrIsNil(err)
|
||||
rc.ResData = res
|
||||
@@ -229,6 +237,8 @@ func (m *Machine) GetUsers(rc *req.Ctx) {
|
||||
func (m *Machine) GetGroups(rc *req.Ctx) {
|
||||
cli, err := m.machineApp.GetCli(GetMachineId(rc))
|
||||
biz.ErrIsNilAppendErr(err, "connection error: %s")
|
||||
defer mcm.PutMachineCli(cli)
|
||||
|
||||
res, err := cli.GetGroups()
|
||||
biz.ErrIsNil(err)
|
||||
rc.ResData = res
|
||||
@@ -252,9 +262,12 @@ func (m *Machine) WsSSH(rc *req.Ctx) {
|
||||
err = req.PermissionHandler(rc)
|
||||
biz.ErrIsNil(err, mcm.GetErrorContentRn("You do not have permission to operate the machine terminal, please log in again and try again ~"))
|
||||
|
||||
cli, err := m.machineApp.NewCli(GetMachineAc(rc))
|
||||
cli, err := m.machineApp.GetCliByAc(GetMachineAc(rc))
|
||||
biz.ErrIsNilAppendErr(err, mcm.GetErrorContentRn("connection error: %s"))
|
||||
defer cli.Close()
|
||||
defer func() {
|
||||
cli.Close()
|
||||
mcm.PutMachineCli(cli)
|
||||
}()
|
||||
biz.ErrIsNilAppendErr(m.tagTreeApp.CanAccess(rc.GetLoginAccount().Id, cli.Info.CodePath...), mcm.GetErrorContentRn("%s"))
|
||||
|
||||
global.EventBus.Publish(rc.MetaCtx, event.EventTopicResourceOp, cli.Info.CodePath[0])
|
||||
|
||||
@@ -328,6 +328,8 @@ func (m *MachineFile) UploadFolder(rc *req.Ctx) {
|
||||
folderName := filepath.Dir(paths[0])
|
||||
mcli, err := m.machineFileApp.GetMachineCli(authCertName)
|
||||
biz.ErrIsNil(err)
|
||||
defer mcm.PutMachineCli(mcli)
|
||||
|
||||
mi := mcli.Info
|
||||
|
||||
sftpCli, err := mcli.GetSftpCli()
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"mayfly-go/internal/machine/api/vo"
|
||||
"mayfly-go/internal/machine/application"
|
||||
"mayfly-go/internal/machine/domain/entity"
|
||||
"mayfly-go/internal/machine/mcm"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/model"
|
||||
@@ -79,6 +80,8 @@ func (m *MachineScript) RunMachineScript(rc *req.Ctx) {
|
||||
}
|
||||
cli, err := m.machineApp.GetCliByAc(ac)
|
||||
biz.ErrIsNilAppendErr(err, "connection error: %s")
|
||||
defer mcm.PutMachineCli(cli)
|
||||
|
||||
biz.ErrIsNilAppendErr(m.tagApp.CanAccess(rc.GetLoginAccount().Id, cli.Info.CodePath...), "%s")
|
||||
|
||||
res, err := cli.Run(script)
|
||||
|
||||
@@ -239,11 +239,6 @@ func (m *machineAppImpl) GetCliByAc(authCertName string) (*mcm.Cli, error) {
|
||||
}
|
||||
|
||||
func (m *machineAppImpl) GetCli(machineId uint64) (*mcm.Cli, error) {
|
||||
cli, err := mcm.GetMachineCliById(machineId)
|
||||
if err == nil {
|
||||
return cli, nil
|
||||
}
|
||||
|
||||
_, authCert, err := m.getMachineAndAuthCert(machineId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"mayfly-go/internal/machine/application/dto"
|
||||
"mayfly-go/internal/machine/domain/entity"
|
||||
"mayfly-go/internal/machine/domain/repository"
|
||||
"mayfly-go/internal/machine/mcm"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
tagentity "mayfly-go/internal/tag/domain/entity"
|
||||
"mayfly-go/pkg/base"
|
||||
@@ -178,12 +179,14 @@ func (m *machineCronJobAppImpl) runCronJob0(mid uint64, cronJob *entity.MachineC
|
||||
ExecTime: time.Now(),
|
||||
}
|
||||
|
||||
machineCli, err := m.machineApp.GetCli(uint64(mid))
|
||||
machineCli, err := m.machineApp.GetCli(mid)
|
||||
res := ""
|
||||
if err != nil {
|
||||
machine, _ := m.machineApp.GetById(mid)
|
||||
execRes.MachineCode = machine.Code
|
||||
} else {
|
||||
defer mcm.PutMachineCli(machineCli)
|
||||
|
||||
execRes.MachineCode = machineCli.Info.Code
|
||||
res, err = machineCli.Run(cronJob.Script)
|
||||
if err != nil {
|
||||
|
||||
@@ -170,6 +170,8 @@ func (m *machineFileAppImpl) GetDirSize(ctx context.Context, opParam *dto.Machin
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer mcm.PutMachineCli(mcli)
|
||||
|
||||
res, err := mcli.Run(fmt.Sprintf("du -sh %s", path))
|
||||
if err != nil {
|
||||
// 若存在目录为空,则可能会返回如下内容。最后一行即为真正目录内容所占磁盘空间大小
|
||||
@@ -202,6 +204,8 @@ func (m *machineFileAppImpl) FileStat(ctx context.Context, opParam *dto.MachineF
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer mcm.PutMachineCli(mcli)
|
||||
|
||||
return mcli.Run(fmt.Sprintf("stat -L %s", path))
|
||||
}
|
||||
|
||||
@@ -379,6 +383,8 @@ func (m *machineFileAppImpl) RemoveFile(ctx context.Context, opParam *dto.Machin
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer mcm.PutMachineCli(mcli)
|
||||
|
||||
minfo := mcli.Info
|
||||
|
||||
// 优先使用命令删除(速度快),sftp需要递归遍历删除子文件等
|
||||
@@ -429,6 +435,7 @@ func (m *machineFileAppImpl) Copy(ctx context.Context, opParam *dto.MachineFileO
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer mcm.PutMachineCli(mcli)
|
||||
|
||||
mi := mcli.Info
|
||||
res, err := mcli.Run(fmt.Sprintf("cp -r %s %s", strings.Join(path, " "), toPath))
|
||||
@@ -458,6 +465,7 @@ func (m *machineFileAppImpl) Mv(ctx context.Context, opParam *dto.MachineFileOp,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer mcm.PutMachineCli(mcli)
|
||||
|
||||
mi := mcli.Info
|
||||
res, err := mcli.Run(fmt.Sprintf("mv %s %s", strings.Join(path, " "), toPath))
|
||||
@@ -493,6 +501,7 @@ func (m *machineFileAppImpl) GetMachineSftpCli(opParam *dto.MachineFileOp) (*mcm
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer mcm.PutMachineCli(mcli)
|
||||
|
||||
sftpCli, err := mcli.GetSftpCli()
|
||||
if err != nil {
|
||||
|
||||
@@ -18,6 +18,11 @@ type Cli struct {
|
||||
sftpClient *sftp.Client // sftp客户端
|
||||
}
|
||||
|
||||
func (c *Cli) Ping() error {
|
||||
_, _, err := c.sshClient.Conn.SendRequest("ping", true, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetSftpCli 获取sftp client
|
||||
func (c *Cli) GetSftpCli() (*sftp.Client, error) {
|
||||
if c.sshClient == nil {
|
||||
@@ -89,7 +94,7 @@ func (c *Cli) Close() {
|
||||
}
|
||||
if sshTunnelMachineId != 0 {
|
||||
logx.Debugf("close machine ssh tunnel -> machineId=%d, sshTunnelMachineId=%d", m.Id, sshTunnelMachineId)
|
||||
CloseSshTunnelMachine(int(sshTunnelMachineId), m.GetTunnelId())
|
||||
CloseSshTunnelMachine(sshTunnelMachineId, m.GetTunnelId())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,122 +1,77 @@
|
||||
package mcm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"mayfly-go/internal/pkg/consts"
|
||||
tagentity "mayfly-go/internal/tag/domain/entity"
|
||||
"mayfly-go/pkg/cache"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/pool"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 机器客户端连接缓存,指定时间内没有访问则会被关闭
|
||||
var cliCache = cache.NewTimedCache(consts.MachineConnExpireTime, 5*time.Second).
|
||||
WithUpdateAccessTime(true).
|
||||
OnEvicted(func(_, value any) {
|
||||
value.(*Cli).Close()
|
||||
})
|
||||
var mcConnPool = make(map[string]pool.Pool)
|
||||
var mcIdPool = make(map[uint64]pool.Pool)
|
||||
|
||||
func init() {
|
||||
AddCheckSshTunnelMachineUseFunc(func(machineId int) bool {
|
||||
// 遍历所有机器连接实例,若存在机器连接实例使用该ssh隧道机器,则返回true,表示还在使用中...
|
||||
items := cliCache.Items()
|
||||
for _, v := range items {
|
||||
sshTunnelMachine := v.Value.(*Cli).Info.SshTunnelMachine
|
||||
if sshTunnelMachine != nil && int(sshTunnelMachine.Id) == machineId {
|
||||
return true
|
||||
}
|
||||
}
|
||||
func getMcPool(authCertName string, getMachine func(string) (*MachineInfo, error)) (pool.Pool, error) {
|
||||
// 获取连接池,如果没有,则创建一个
|
||||
if p, ok := mcConnPool[authCertName]; !ok {
|
||||
var err error
|
||||
p, err = pool.NewChannelPool(&pool.Config{
|
||||
InitialCap: 1, //资源池初始连接数
|
||||
MaxCap: 10, //最大空闲连接数
|
||||
MaxIdle: 10, //最大并发连接数
|
||||
IdleTimeout: 10 * time.Minute, // 连接最大空闲时间,过期则失效
|
||||
Factory: func() (interface{}, error) {
|
||||
mi, err := getMachine(authCertName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mi.Key = authCertName
|
||||
return mi.Conn()
|
||||
},
|
||||
Close: func(v interface{}) error {
|
||||
v.(*Cli).Close()
|
||||
return nil
|
||||
},
|
||||
Ping: func(v interface{}) error {
|
||||
return v.(*Cli).Ping()
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return false
|
||||
})
|
||||
go checkClientAvailability(3 * time.Minute)
|
||||
mcConnPool[authCertName] = p
|
||||
return p, nil
|
||||
} else {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
func PutMachineCli(c *Cli) {
|
||||
if nil == c {
|
||||
return
|
||||
}
|
||||
if p, ok := mcConnPool[c.Info.AuthCertName]; ok {
|
||||
p.Put(c)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 从缓存中获取客户端信息,不存在则回调获取机器信息函数,并新建。
|
||||
// @param 机器的授权凭证名
|
||||
func GetMachineCli(authCertName string, getMachine func(string) (*MachineInfo, error)) (*Cli, error) {
|
||||
if load, ok := cliCache.Get(authCertName); ok {
|
||||
return load.(*Cli), nil
|
||||
}
|
||||
|
||||
mi, err := getMachine(authCertName)
|
||||
p, err := getMcPool(authCertName, getMachine)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mi.Key = authCertName
|
||||
c, err := mi.Conn()
|
||||
// 从连接池中获取一个可用的连接
|
||||
c, err := p.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cliCache.Put(authCertName, c)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// 根据机器id从已连接的机器客户端中获取特权账号连接, 若不存在特权账号,则随机返回一个
|
||||
func GetMachineCliById(machineId uint64) (*Cli, error) {
|
||||
// 遍历所有机器连接实例,删除指定机器id关联的连接...
|
||||
items := cliCache.Items()
|
||||
|
||||
var machineCli *Cli
|
||||
for _, v := range items {
|
||||
cli := v.Value.(*Cli)
|
||||
mi := cli.Info
|
||||
if mi.Id != machineId {
|
||||
continue
|
||||
}
|
||||
machineCli = cli
|
||||
|
||||
// 如果是特权账号,则跳出
|
||||
if mi.AuthCertType == tagentity.AuthCertTypePrivileged {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if machineCli != nil {
|
||||
return machineCli, nil
|
||||
}
|
||||
return nil, errors.New("no connection exists for this machine id")
|
||||
return c.(*Cli), nil
|
||||
}
|
||||
|
||||
// 删除指定机器缓存客户端,并关闭客户端连接
|
||||
func DeleteCli(id uint64) {
|
||||
// 遍历所有机器连接实例,删除指定机器id关联的连接...
|
||||
items := cliCache.Items()
|
||||
for _, v := range items {
|
||||
mi := v.Value.(*Cli).Info
|
||||
if mi.Id == id {
|
||||
cliCache.Delete(mi.Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查缓存中的客户端是否可用,不可用则关闭客户端连接
|
||||
func checkClientAvailability(interval time.Duration) {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
// 遍历所有机器连接实例,若存在机器连接实例使用该ssh隧道机器,则返回true,表示还在使用中...
|
||||
items := cliCache.Items()
|
||||
for _, v := range items {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
cli := v.Value.(*Cli)
|
||||
if cli.Info == nil {
|
||||
continue
|
||||
}
|
||||
if cli.sshClient == nil {
|
||||
continue
|
||||
}
|
||||
if cli.sshClient.Conn == nil {
|
||||
continue
|
||||
}
|
||||
if _, _, err := cli.sshClient.Conn.SendRequest("ping", true, nil); err != nil {
|
||||
logx.Errorf("machine[%s] cache client is not available: %s", cli.Info.Name, err.Error())
|
||||
DeleteCli(cli.Info.Id)
|
||||
}
|
||||
logx.Debugf("machine[%s] cache client is available", cli.Info.Name)
|
||||
}
|
||||
}
|
||||
delete(mcIdPool, id)
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ func (mi *MachineInfo) Conn() (*Cli, error) {
|
||||
sshClient, err := GetSshClient(mi, nil)
|
||||
if err != nil {
|
||||
if mi.UseSshTunnel() {
|
||||
CloseSshTunnelMachine(int(mi.TempSshMachineId), mi.GetTunnelId())
|
||||
CloseSshTunnelMachine(mi.TempSshMachineId, mi.GetTunnelId())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,59 +1,31 @@
|
||||
package mcm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/scheduler"
|
||||
"mayfly-go/pkg/pool"
|
||||
"mayfly-go/pkg/utils/netx"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var (
|
||||
sshTunnelMachines map[int]*SshTunnelMachine = make(map[int]*SshTunnelMachine)
|
||||
|
||||
mutex sync.Mutex
|
||||
|
||||
// 所有检测ssh隧道机器是否被使用的函数
|
||||
checkSshTunnelMachineHasUseFuncs []CheckSshTunnelMachineHasUseFunc
|
||||
|
||||
// 是否开启检查ssh隧道机器是否被使用,只有使用到了隧道机器才启用
|
||||
startCheckSshTunnelHasUse bool = false
|
||||
tunnelPool = make(map[int]pool.Pool)
|
||||
)
|
||||
|
||||
// 检查ssh隧道机器是否有被使用
|
||||
type CheckSshTunnelMachineHasUseFunc func(int) bool
|
||||
|
||||
func startCheckUse() {
|
||||
logx.Info("start periodically checking if the ssh tunnel machine is still in use")
|
||||
// 每十分钟检查一次隧道机器是否还有被使用
|
||||
scheduler.AddFun("@every 10m", func() {
|
||||
if !mutex.TryLock() {
|
||||
return
|
||||
}
|
||||
defer mutex.Unlock()
|
||||
// 遍历隧道机器,都未被使用将会被关闭
|
||||
for mid, sshTunnelMachine := range sshTunnelMachines {
|
||||
logx.Debugf("periodically check if the ssh tunnel machine [%d] is still in use...", mid)
|
||||
hasUse := false
|
||||
for _, checkUseFunc := range checkSshTunnelMachineHasUseFuncs {
|
||||
// 如果一个在使用则返回不关闭,不继续后续检查
|
||||
if checkUseFunc(mid) {
|
||||
hasUse = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasUse {
|
||||
// 都未被使用,则关闭
|
||||
sshTunnelMachine.Close()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 添加ssh隧道机器检测是否使用函数
|
||||
func AddCheckSshTunnelMachineUseFunc(checkFunc CheckSshTunnelMachineHasUseFunc) {
|
||||
if checkSshTunnelMachineHasUseFuncs == nil {
|
||||
@@ -64,12 +36,18 @@ func AddCheckSshTunnelMachineUseFunc(checkFunc CheckSshTunnelMachineHasUseFunc)
|
||||
|
||||
// ssh隧道机器
|
||||
type SshTunnelMachine struct {
|
||||
mi *MachineInfo
|
||||
machineId int // 隧道机器id
|
||||
SshClient *ssh.Client
|
||||
mutex sync.Mutex
|
||||
tunnels map[string]*Tunnel // 隧道id -> 隧道
|
||||
}
|
||||
|
||||
func (stm *SshTunnelMachine) Ping() error {
|
||||
_, _, err := stm.SshClient.Conn.SendRequest("ping", true, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (stm *SshTunnelMachine) OpenSshTunnel(id string, ip string, port int) (exposedIp string, exposedPort int, err error) {
|
||||
stm.mutex.Lock()
|
||||
defer stm.mutex.Unlock()
|
||||
@@ -77,6 +55,7 @@ func (stm *SshTunnelMachine) OpenSshTunnel(id string, ip string, port int) (expo
|
||||
tunnel := stm.tunnels[id]
|
||||
// 已存在该id隧道,则直接返回
|
||||
if tunnel != nil {
|
||||
// FIXME 后期改成池化连接,定时60秒检查连接可用性
|
||||
return tunnel.localHost, tunnel.localPort, nil
|
||||
}
|
||||
|
||||
@@ -85,7 +64,7 @@ func (stm *SshTunnelMachine) OpenSshTunnel(id string, ip string, port int) (expo
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
localHost := "0.0.0.0"
|
||||
localHost := "127.0.0.1"
|
||||
localAddr := fmt.Sprintf("%s:%d", localHost, localPort)
|
||||
listener, err := net.Listen("tcp", localAddr)
|
||||
if err != nil {
|
||||
@@ -104,13 +83,13 @@ func (stm *SshTunnelMachine) OpenSshTunnel(id string, ip string, port int) (expo
|
||||
go tunnel.Open(stm.SshClient)
|
||||
stm.tunnels[tunnel.id] = tunnel
|
||||
|
||||
return tunnel.localHost, tunnel.localPort, nil
|
||||
return localHost, localPort, nil
|
||||
}
|
||||
|
||||
func (st *SshTunnelMachine) GetDialConn(network string, addr string) (net.Conn, error) {
|
||||
st.mutex.Lock()
|
||||
defer st.mutex.Unlock()
|
||||
return st.SshClient.Dial(network, addr)
|
||||
func (stm *SshTunnelMachine) GetDialConn(network string, addr string) (net.Conn, error) {
|
||||
stm.mutex.Lock()
|
||||
defer stm.mutex.Unlock()
|
||||
return stm.SshClient.Dial(network, addr)
|
||||
}
|
||||
|
||||
func (stm *SshTunnelMachine) Close() {
|
||||
@@ -131,55 +110,82 @@ func (stm *SshTunnelMachine) Close() {
|
||||
logx.Errorf("error in closing ssh tunnel machine [%d]: %s", stm.machineId, err.Error())
|
||||
}
|
||||
}
|
||||
delete(sshTunnelMachines, stm.machineId)
|
||||
delete(tunnelPool, stm.machineId)
|
||||
}
|
||||
|
||||
func getTunnelPool(machineId int, getMachine func(uint64) (*MachineInfo, error)) (pool.Pool, error) {
|
||||
// 获取连接池,如果没有,则创建一个
|
||||
if p, ok := tunnelPool[machineId]; !ok {
|
||||
var err error
|
||||
p, err = pool.NewChannelPool(&pool.Config{
|
||||
InitialCap: 1, //资源池初始连接数
|
||||
MaxCap: 10, //最大空闲连接数
|
||||
MaxIdle: 10, //最大并发连接数
|
||||
IdleTimeout: 10 * time.Minute, // 连接最大空闲时间,过期则失效
|
||||
Factory: func() (interface{}, error) {
|
||||
mi, err := getMachine(uint64(machineId))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if mi == nil {
|
||||
return nil, errors.New("error get machine info")
|
||||
}
|
||||
sshClient, err := GetSshClient(mi, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stm := &SshTunnelMachine{SshClient: sshClient, machineId: machineId, tunnels: map[string]*Tunnel{}, mi: mi}
|
||||
logx.Infof("connect to the ssh tunnel machine for the first time[%d][%s:%d]", machineId, mi.Ip, mi.Port)
|
||||
|
||||
return stm, err
|
||||
},
|
||||
Close: func(v interface{}) error {
|
||||
v.(*SshTunnelMachine).Close()
|
||||
return nil
|
||||
},
|
||||
Ping: func(v interface{}) error {
|
||||
return v.(*SshTunnelMachine).Ping()
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tunnelPool[machineId] = p
|
||||
return p, nil
|
||||
} else {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 获取ssh隧道机器,方便统一管理充当ssh隧道的机器,避免创建多个ssh client
|
||||
func GetSshTunnelMachine(machineId int, getMachine func(uint64) (*MachineInfo, error)) (*SshTunnelMachine, error) {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
sshTunnelMachine := sshTunnelMachines[machineId]
|
||||
if sshTunnelMachine != nil {
|
||||
return sshTunnelMachine, nil
|
||||
p, err := getTunnelPool(machineId, getMachine)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
me, err := getMachine(uint64(machineId))
|
||||
// 从连接池中获取一个可用的连接
|
||||
c, err := p.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sshClient, err := GetSshClient(me, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sshTunnelMachine = &SshTunnelMachine{SshClient: sshClient, machineId: machineId, tunnels: map[string]*Tunnel{}}
|
||||
|
||||
logx.Infof("connect to the ssh tunnel machine for the first time[%d][%s:%d]", machineId, me.Ip, me.Port)
|
||||
sshTunnelMachines[machineId] = sshTunnelMachine
|
||||
|
||||
// 如果实用了隧道机器且还没开始定时检查是否还被实用,则执行定时任务检测隧道是否还被使用
|
||||
if !startCheckSshTunnelHasUse {
|
||||
startCheckUse()
|
||||
startCheckSshTunnelHasUse = true
|
||||
}
|
||||
return sshTunnelMachine, nil
|
||||
return c.(*SshTunnelMachine), nil
|
||||
}
|
||||
|
||||
// 关闭ssh隧道机器的指定隧道
|
||||
func CloseSshTunnelMachine(machineId int, tunnelId string) {
|
||||
sshTunnelMachine := sshTunnelMachines[machineId]
|
||||
if sshTunnelMachine == nil {
|
||||
return
|
||||
}
|
||||
|
||||
sshTunnelMachine.mutex.Lock()
|
||||
defer sshTunnelMachine.mutex.Unlock()
|
||||
t := sshTunnelMachine.tunnels[tunnelId]
|
||||
if t != nil {
|
||||
t.Close()
|
||||
delete(sshTunnelMachine.tunnels, tunnelId)
|
||||
}
|
||||
func CloseSshTunnelMachine(machineId uint64, tunnelId string) {
|
||||
//sshTunnelMachine := mcIdPool[machineId]
|
||||
//if sshTunnelMachine == nil {
|
||||
// return
|
||||
//}
|
||||
//
|
||||
//sshTunnelMachine.mutex.Lock()
|
||||
//defer sshTunnelMachine.mutex.Unlock()
|
||||
//t := sshTunnelMachine.tunnels[tunnelId]
|
||||
//if t != nil {
|
||||
// t.Close()
|
||||
// delete(sshTunnelMachine.tunnels, tunnelId)
|
||||
//}
|
||||
}
|
||||
|
||||
type Tunnel struct {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"mayfly-go/internal/mongo/application"
|
||||
"mayfly-go/internal/mongo/domain/entity"
|
||||
"mayfly-go/internal/mongo/imsg"
|
||||
"mayfly-go/internal/mongo/mgm"
|
||||
"mayfly-go/internal/pkg/consts"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
tagentity "mayfly-go/internal/tag/domain/entity"
|
||||
@@ -127,6 +128,8 @@ func (m *Mongo) DeleteMongo(rc *req.Ctx) {
|
||||
func (m *Mongo) Databases(rc *req.Ctx) {
|
||||
conn, err := m.mongoApp.GetMongoConn(m.GetMongoId(rc))
|
||||
biz.ErrIsNil(err)
|
||||
defer mgm.PutMongoConn(conn)
|
||||
|
||||
res, err := conn.Cli.ListDatabases(context.TODO(), bson.D{})
|
||||
biz.ErrIsNilAppendErr(err, "get mongo dbs error: %s")
|
||||
rc.ResData = res
|
||||
@@ -135,6 +138,7 @@ func (m *Mongo) Databases(rc *req.Ctx) {
|
||||
func (m *Mongo) Collections(rc *req.Ctx) {
|
||||
conn, err := m.mongoApp.GetMongoConn(m.GetMongoId(rc))
|
||||
biz.ErrIsNil(err)
|
||||
defer mgm.PutMongoConn(conn)
|
||||
|
||||
global.EventBus.Publish(rc.MetaCtx, event.EventTopicResourceOp, conn.Info.CodePath[0])
|
||||
|
||||
@@ -152,6 +156,8 @@ func (m *Mongo) RunCommand(rc *req.Ctx) {
|
||||
|
||||
conn, err := m.mongoApp.GetMongoConn(m.GetMongoId(rc))
|
||||
biz.ErrIsNil(err)
|
||||
defer mgm.PutMongoConn(conn)
|
||||
|
||||
rc.ReqParam = collx.Kvs("mongo", conn.Info, "cmd", commandForm)
|
||||
|
||||
// 顺序执行
|
||||
@@ -181,6 +187,8 @@ func (m *Mongo) FindCommand(rc *req.Ctx) {
|
||||
|
||||
conn, err := m.mongoApp.GetMongoConn(m.GetMongoId(rc))
|
||||
biz.ErrIsNil(err)
|
||||
defer mgm.PutMongoConn(conn)
|
||||
|
||||
cli := conn.Cli
|
||||
|
||||
limit := commandForm.Limit
|
||||
@@ -215,6 +223,8 @@ func (m *Mongo) UpdateByIdCommand(rc *req.Ctx) {
|
||||
|
||||
conn, err := m.mongoApp.GetMongoConn(m.GetMongoId(rc))
|
||||
biz.ErrIsNil(err)
|
||||
defer mgm.PutMongoConn(conn)
|
||||
|
||||
rc.ReqParam = collx.Kvs("mongo", conn.Info, "cmd", commandForm)
|
||||
|
||||
// 解析docId文档id,如果为string类型则使用ObjectId解析,解析失败则为普通字符串
|
||||
@@ -238,6 +248,8 @@ func (m *Mongo) DeleteByIdCommand(rc *req.Ctx) {
|
||||
|
||||
conn, err := m.mongoApp.GetMongoConn(m.GetMongoId(rc))
|
||||
biz.ErrIsNil(err)
|
||||
defer mgm.PutMongoConn(conn)
|
||||
|
||||
rc.ReqParam = collx.Kvs("mongo", conn.Info, "cmd", commandForm)
|
||||
|
||||
// 解析docId文档id,如果为string类型则使用ObjectId解析,解析失败则为普通字符串
|
||||
@@ -260,6 +272,8 @@ func (m *Mongo) InsertOneCommand(rc *req.Ctx) {
|
||||
|
||||
conn, err := m.mongoApp.GetMongoConn(m.GetMongoId(rc))
|
||||
biz.ErrIsNil(err)
|
||||
defer mgm.PutMongoConn(conn)
|
||||
|
||||
rc.ReqParam = collx.Kvs("mongo", conn.Info, "cmd", commandForm)
|
||||
|
||||
res, err := conn.Cli.Database(commandForm.Database).Collection(commandForm.Collection).InsertOne(context.TODO(), commandForm.Doc)
|
||||
|
||||
@@ -1,72 +1,80 @@
|
||||
package mgm
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/machine/mcm"
|
||||
"mayfly-go/internal/pkg/consts"
|
||||
"mayfly-go/pkg/cache"
|
||||
"mayfly-go/pkg/logx"
|
||||
"sync"
|
||||
"context"
|
||||
"mayfly-go/pkg/pool"
|
||||
"time"
|
||||
)
|
||||
|
||||
// mongo客户端连接缓存,指定时间内没有访问则会被关闭
|
||||
var connCache = cache.NewTimedCache(consts.MongoConnExpireTime, 5*time.Second).
|
||||
WithUpdateAccessTime(true).
|
||||
OnEvicted(func(key any, value any) {
|
||||
logx.Infof("删除mongo连接缓存: id = %v", key)
|
||||
value.(*MongoConn).Close()
|
||||
})
|
||||
var connPool = make(map[string]pool.Pool)
|
||||
|
||||
func init() {
|
||||
mcm.AddCheckSshTunnelMachineUseFunc(func(machineId int) bool {
|
||||
// 遍历所有mongo连接实例,若存在redis实例使用该ssh隧道机器,则返回true,表示还在使用中...
|
||||
items := connCache.Items()
|
||||
for _, v := range items {
|
||||
if v.Value.(*MongoConn).Info.SshTunnelMachineId == machineId {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
var mutex sync.Mutex
|
||||
func getPool(mongoId uint64, getMongoInfo func() (*MongoInfo, error)) (pool.Pool, error) {
|
||||
connId := getConnId(mongoId)
|
||||
|
||||
// 获取连接池,如果没有,则创建一个
|
||||
if p, ok := connPool[connId]; !ok {
|
||||
var err error
|
||||
p, err = pool.NewChannelPool(&pool.Config{
|
||||
InitialCap: 1, //资源池初始连接数
|
||||
MaxCap: 10, //最大空闲连接数
|
||||
MaxIdle: 10, //最大并发连接数
|
||||
IdleTimeout: 10 * time.Minute, // 连接最大空闲时间,过期则失效
|
||||
Factory: func() (interface{}, error) {
|
||||
// 若缓存中不存在,则从回调函数中获取MongoInfo
|
||||
mi, err := getMongoInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 连接mongo
|
||||
return mi.Conn()
|
||||
},
|
||||
Close: func(v interface{}) error {
|
||||
v.(*MongoConn).Close()
|
||||
return nil
|
||||
},
|
||||
Ping: func(v interface{}) error {
|
||||
return v.(*MongoConn).Cli.Ping(context.Background(), nil)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
connPool[connId] = p
|
||||
return p, nil
|
||||
} else {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
func PutMongoConn(c *MongoConn) {
|
||||
if nil == c {
|
||||
return
|
||||
}
|
||||
if p, ok := connPool[getConnId(c.Info.Id)]; ok {
|
||||
p.Put(c)
|
||||
}
|
||||
}
|
||||
|
||||
// 从缓存中获取mongo连接信息, 若缓存中不存在则会使用回调函数获取mongoInfo进行连接并缓存
|
||||
func GetMongoConn(mongoId uint64, getMongoInfo func() (*MongoInfo, error)) (*MongoConn, error) {
|
||||
connId := getConnId(mongoId)
|
||||
|
||||
// connId不为空,则为需要缓存
|
||||
needCache := connId != ""
|
||||
if needCache {
|
||||
load, ok := connCache.Get(connId)
|
||||
if ok {
|
||||
return load.(*MongoConn), nil
|
||||
}
|
||||
}
|
||||
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
// 若缓存中不存在,则从回调函数中获取MongoInfo
|
||||
mi, err := getMongoInfo()
|
||||
p, err := getPool(mongoId, getMongoInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 连接mongo
|
||||
mc, err := mi.Conn()
|
||||
// 从连接池中获取一个可用的连接
|
||||
c, err := p.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if needCache {
|
||||
connCache.Put(connId, mc)
|
||||
}
|
||||
return mc, nil
|
||||
return c.(*MongoConn), nil
|
||||
}
|
||||
|
||||
// 关闭连接,并移除缓存连接
|
||||
func CloseConn(mongoId uint64) {
|
||||
connCache.Delete(mongoId)
|
||||
connId := getConnId(mongoId)
|
||||
delete(connPool, connId)
|
||||
}
|
||||
|
||||
@@ -75,5 +75,5 @@ func getConnId(id uint64) string {
|
||||
if id == 0 {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%d", id)
|
||||
return fmt.Sprintf("mongo:%d", id)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const (
|
||||
DbConnExpireTime = 120 * time.Minute
|
||||
RedisConnExpireTime = 30 * time.Minute
|
||||
MongoConnExpireTime = 30 * time.Minute
|
||||
EsConnExpireTime = 30 * time.Minute
|
||||
|
||||
/**** 开发测试使用 ****/
|
||||
// MachineConnExpireTime = 4 * time.Minute
|
||||
@@ -20,6 +21,8 @@ const (
|
||||
ResourceTypeDbInstance int8 = 2
|
||||
ResourceTypeRedis int8 = 3
|
||||
ResourceTypeMongo int8 = 4
|
||||
ResourceTypeAuthCert int8 = 5
|
||||
ResourceTypeEsInstance int8 = 6
|
||||
|
||||
// imsg起始编号
|
||||
ImsgNumSys = 10000
|
||||
@@ -31,4 +34,5 @@ const (
|
||||
ImsgNumRedis = 70000
|
||||
ImsgNumMongo = 80000
|
||||
ImsgNumMsg = 90000
|
||||
ImsgNumEs = 100000
|
||||
)
|
||||
|
||||
@@ -46,6 +46,11 @@ func initMysql(m config.Mysql) *gorm.DB {
|
||||
sqlDB, _ := db.DB()
|
||||
sqlDB.SetMaxIdleConns(m.MaxIdleConns)
|
||||
sqlDB.SetMaxOpenConns(m.MaxOpenConns)
|
||||
|
||||
// 如果是开发环境时,打印sql语句
|
||||
if logx.GetConfig().IsDebug() {
|
||||
db = db.Debug()
|
||||
}
|
||||
return db
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +152,7 @@ func (r *Redis) DeleteRedis(rc *req.Ctx) {
|
||||
func (r *Redis) RedisInfo(rc *req.Ctx) {
|
||||
ri, err := r.redisApp.GetRedisConn(uint64(rc.PathParamInt("id")), 0)
|
||||
biz.ErrIsNil(err)
|
||||
defer rdm.PutRedisConn(ri)
|
||||
|
||||
section := rc.Query("section")
|
||||
mode := ri.Info.Mode
|
||||
@@ -229,6 +230,8 @@ func (r *Redis) RedisInfo(rc *req.Ctx) {
|
||||
func (r *Redis) ClusterInfo(rc *req.Ctx) {
|
||||
ri, err := r.redisApp.GetRedisConn(uint64(rc.PathParamInt("id")), 0)
|
||||
biz.ErrIsNil(err)
|
||||
defer rdm.PutRedisConn(ri)
|
||||
|
||||
biz.IsEquals(ri.Info.Mode, rdm.ClusterMode, "non-cluster mode")
|
||||
info, _ := ri.ClusterCli.ClusterInfo(context.Background()).Result()
|
||||
nodesStr, _ := ri.ClusterCli.ClusterNodes(context.Background()).Result()
|
||||
@@ -280,6 +283,8 @@ 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))
|
||||
biz.ErrIsNil(err)
|
||||
defer rdm.PutRedisConn(ri)
|
||||
|
||||
biz.ErrIsNilAppendErr(r.tagApp.CanAccess(rc.GetLoginAccount().Id, ri.Info.CodePath...), "%s")
|
||||
return ri
|
||||
}
|
||||
|
||||
@@ -1,73 +1,81 @@
|
||||
package rdm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mayfly-go/internal/machine/mcm"
|
||||
"mayfly-go/internal/pkg/consts"
|
||||
"mayfly-go/pkg/cache"
|
||||
"mayfly-go/pkg/logx"
|
||||
"sync"
|
||||
"context"
|
||||
"mayfly-go/pkg/pool"
|
||||
"time"
|
||||
)
|
||||
|
||||
// redis客户端连接缓存,指定时间内没有访问则会被关闭
|
||||
var connCache = cache.NewTimedCache(consts.RedisConnExpireTime, 5*time.Second).
|
||||
WithUpdateAccessTime(true).
|
||||
OnEvicted(func(key any, value any) {
|
||||
logx.Info(fmt.Sprintf("remove the redis connection cache id = %s", key))
|
||||
value.(*RedisConn).Close()
|
||||
})
|
||||
|
||||
func init() {
|
||||
mcm.AddCheckSshTunnelMachineUseFunc(func(machineId int) bool {
|
||||
// 遍历所有redis连接实例,若存在redis实例使用该ssh隧道机器,则返回true,表示还在使用中...
|
||||
items := connCache.Items()
|
||||
for _, v := range items {
|
||||
if v.Value.(*RedisConn).Info.SshTunnelMachineId == machineId {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
var mutex sync.Mutex
|
||||
var connPool = make(map[string]pool.Pool)
|
||||
|
||||
func getPool(redisId uint64, db int, getRedisInfo func() (*RedisInfo, error)) (pool.Pool, error) {
|
||||
connId := getConnId(redisId, db)
|
||||
// 获取连接池,如果没有,则创建一个
|
||||
if p, ok := connPool[connId]; !ok {
|
||||
var err error
|
||||
p, err = pool.NewChannelPool(&pool.Config{
|
||||
InitialCap: 1, //资源池初始连接数
|
||||
MaxCap: 10, //最大空闲连接数
|
||||
MaxIdle: 10, //最大并发连接数
|
||||
IdleTimeout: 10 * time.Minute, // 连接最大空闲时间,过期则失效
|
||||
Factory: func() (interface{}, error) {
|
||||
// 若缓存中不存在,则从回调函数中获取RedisInfo
|
||||
ri, err := getRedisInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 连接数据库
|
||||
return ri.Conn()
|
||||
},
|
||||
Close: func(v interface{}) error {
|
||||
v.(*RedisConn).Close()
|
||||
return nil
|
||||
},
|
||||
Ping: func(v interface{}) error {
|
||||
_, err := v.(*RedisConn).Cli.Ping(context.Background()).Result()
|
||||
return err
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
connPool[connId] = p
|
||||
return p, nil
|
||||
} else {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
func PutRedisConn(c *RedisConn) {
|
||||
if nil == c {
|
||||
return
|
||||
}
|
||||
|
||||
if p, ok := connPool[getConnId(c.Info.Id, c.Info.Db)]; ok {
|
||||
p.Put(c)
|
||||
}
|
||||
}
|
||||
|
||||
// 从缓存中获取redis连接信息, 若缓存中不存在则会使用回调函数获取redisInfo进行连接并缓存
|
||||
func GetRedisConn(redisId uint64, db int, getRedisInfo func() (*RedisInfo, error)) (*RedisConn, error) {
|
||||
connId := getConnId(redisId, db)
|
||||
|
||||
// connId不为空,则为需要缓存
|
||||
needCache := connId != ""
|
||||
if needCache {
|
||||
load, ok := connCache.Get(connId)
|
||||
if ok {
|
||||
return load.(*RedisConn), nil
|
||||
}
|
||||
}
|
||||
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
// 若缓存中不存在,则从回调函数中获取RedisInfo
|
||||
ri, err := getRedisInfo()
|
||||
p, err := getPool(redisId, db, getRedisInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 连接数据库
|
||||
rc, err := ri.Conn()
|
||||
// 从连接池中获取一个可用的连接
|
||||
c, err := p.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if needCache {
|
||||
connCache.Put(connId, rc)
|
||||
}
|
||||
return rc, nil
|
||||
// 用完后记的放回连接池
|
||||
return c.(*RedisConn), nil
|
||||
}
|
||||
|
||||
// 移除redis连接缓存并关闭redis连接
|
||||
func CloseConn(id uint64, db int) {
|
||||
connCache.Delete(getConnId(id, db))
|
||||
delete(connPool, getConnId(id, db))
|
||||
}
|
||||
|
||||
@@ -168,9 +168,15 @@ func (p *TagTree) CountTagResource(rc *req.Ctx) {
|
||||
CodePathLikes: collx.AsArray(tagPath),
|
||||
}).GetCodePaths()...)
|
||||
|
||||
esCodes := entity.GetCodesByCodePaths(entity.TagTypeEsInstance, p.tagTreeApp.GetAccountTags(accountId, &entity.TagTreeQuery{
|
||||
Types: collx.AsArray(entity.TagTypeEsInstance),
|
||||
CodePathLikes: collx.AsArray(tagPath),
|
||||
}).GetCodePaths()...)
|
||||
|
||||
rc.ResData = collx.M{
|
||||
"machine": len(machineCodes),
|
||||
"db": len(dbCodes),
|
||||
"es": len(esCodes),
|
||||
"redis": len(p.tagTreeApp.GetAccountTags(accountId, &entity.TagTreeQuery{
|
||||
Types: collx.AsArray(entity.TagTypeRedis),
|
||||
CodePathLikes: collx.AsArray(tagPath),
|
||||
|
||||
@@ -32,9 +32,10 @@ const (
|
||||
TagTypeTag TagType = -1
|
||||
TagTypeMachine TagType = TagType(consts.ResourceTypeMachine)
|
||||
TagTypeDbInstance TagType = TagType(consts.ResourceTypeDbInstance) // 数据库实例
|
||||
TagTypeEsInstance TagType = TagType(consts.ResourceTypeEsInstance) // es实例
|
||||
TagTypeRedis TagType = TagType(consts.ResourceTypeRedis)
|
||||
TagTypeMongo TagType = TagType(consts.ResourceTypeMongo)
|
||||
TagTypeAuthCert TagType = 5 // 授权凭证类型
|
||||
TagTypeAuthCert TagType = TagType(consts.ResourceTypeAuthCert) // 授权凭证类型
|
||||
|
||||
TagTypeDb TagType = 22 // 数据库名
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
_ "mayfly-go/internal/auth/init"
|
||||
_ "mayfly-go/internal/common/init"
|
||||
_ "mayfly-go/internal/db/init"
|
||||
_ "mayfly-go/internal/es/init"
|
||||
_ "mayfly-go/internal/file/init"
|
||||
_ "mayfly-go/internal/flow/init"
|
||||
_ "mayfly-go/internal/machine/init"
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
flowentity "mayfly-go/internal/flow/domain/entity"
|
||||
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
esentity "mayfly-go/internal/es/domain/entity"
|
||||
flowentity "mayfly-go/internal/flow/domain/entity"
|
||||
sysentity "mayfly-go/internal/sys/domain/entity"
|
||||
"mayfly-go/pkg/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
func V1_10() []*gormigrate.Migration {
|
||||
var migrations []*gormigrate.Migration
|
||||
migrations = append(migrations, V1_10_0()...)
|
||||
migrations = append(migrations, V1_10_1()...)
|
||||
return migrations
|
||||
}
|
||||
|
||||
@@ -22,7 +26,7 @@ func V1_10_0() []*gormigrate.Migration {
|
||||
&flowentity.Procinst{},
|
||||
&flowentity.Execution{},
|
||||
&flowentity.ProcinstTask{},
|
||||
flowentity.ProcinstTaskCandidate{},
|
||||
&flowentity.ProcinstTaskCandidate{},
|
||||
&flowentity.HisProcinstOp{})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -36,3 +40,108 @@ func V1_10_0() []*gormigrate.Migration {
|
||||
},
|
||||
}
|
||||
}
|
||||
func V1_10_1() []*gormigrate.Migration {
|
||||
return []*gormigrate.Migration{
|
||||
{
|
||||
ID: "20250422-v1.10.1-es",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
// 添加实例表
|
||||
entities := [...]any{
|
||||
new(esentity.EsInstance),
|
||||
}
|
||||
for _, e := range entities {
|
||||
if err := tx.AutoMigrate(e); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 添加菜单资源
|
||||
resources := []*sysentity.Resource{
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1745292787}}}},
|
||||
Pid: 0,
|
||||
UiPath: "lbOU73qg/",
|
||||
Name: "Elasticsearch",
|
||||
Code: "/es",
|
||||
Type: 1,
|
||||
Meta: `{"icon":"icon es/es-color","isKeepAlive":true,"routeName":"ES"}`,
|
||||
Weight: 7,
|
||||
},
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1745319348}}}},
|
||||
Pid: 1745292787,
|
||||
UiPath: "lbOU73qg/gZ2MHF0b/",
|
||||
Name: "es.instance",
|
||||
Code: "EsInstance ",
|
||||
Type: 1,
|
||||
Meta: `{"component":"ops/es/InstanceList","icon":"icon es/es-color","isKeepAlive":true,"routeName":"EsInstanceList"}`,
|
||||
Weight: 1745319348,
|
||||
},
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1745319410}}}},
|
||||
Pid: 1745319348,
|
||||
UiPath: "lbOU73qg/gZ2MHF0b/rcKBdxB5/",
|
||||
Name: "es.instanceSave",
|
||||
Code: "es:instance:save",
|
||||
Type: 2,
|
||||
Weight: 1745319410,
|
||||
},
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1745319424}}}},
|
||||
Pid: 1745319348,
|
||||
UiPath: "lbOU73qg/gZ2MHF0b/IMGhLSJK/",
|
||||
Name: "es.instanceDel",
|
||||
Code: "es:instance:del",
|
||||
Type: 2,
|
||||
Weight: 1745319424,
|
||||
},
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1745494931}}}},
|
||||
Pid: 1745292787,
|
||||
UiPath: "lbOU73qg/2sDi4isw/",
|
||||
Name: "es.operation",
|
||||
Code: "EsOperation",
|
||||
Type: 1,
|
||||
Meta: `{"component":"ops/es/EsOperation","icon":"icon es/es-color","isKeepAlive":true,"routeName":"EsOperation"}`,
|
||||
Weight: 1745319347,
|
||||
},
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1745659240}}}},
|
||||
Pid: 1745494931,
|
||||
UiPath: "lbOU73qg/2sDi4isw/SQNFhhhn/",
|
||||
Name: "es.dataSave",
|
||||
Code: "es:data:save",
|
||||
Type: 2,
|
||||
Weight: 1745659240,
|
||||
},
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1745659315}}}},
|
||||
Pid: 1745494931,
|
||||
UiPath: "lbOU73qg/2sDi4isw/XAgy5Uvp/",
|
||||
Name: "es.dataDel",
|
||||
Code: "es:data:del",
|
||||
Type: 2,
|
||||
Weight: 1745659315,
|
||||
},
|
||||
}
|
||||
now := time.Now()
|
||||
for _, res := range resources {
|
||||
res.Status = 1
|
||||
res.CreateTime = &now
|
||||
res.CreatorId = 1
|
||||
res.Creator = "admin"
|
||||
res.UpdateTime = &now
|
||||
res.ModifierId = 1
|
||||
res.Modifier = "admin"
|
||||
tx.Create(res)
|
||||
}
|
||||
// 给超管授权
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,8 +54,8 @@ func (r *Req) Header(name, value string) *Req {
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Req) Timeout(timeout int) *Req {
|
||||
r.timeout = timeout
|
||||
func (r *Req) Timeout(second int) *Req {
|
||||
r.timeout = second
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -107,6 +107,25 @@ func (r *Req) PostForm(params string) *Resp {
|
||||
return sendRequest(r)
|
||||
}
|
||||
|
||||
func (r *Req) PutJson(body string) *Resp {
|
||||
buf := bytes.NewBufferString(body)
|
||||
r.method = "PUT"
|
||||
r.body = buf
|
||||
if r.header == nil {
|
||||
r.header = make(map[string]string)
|
||||
}
|
||||
r.header["Content-type"] = "application/json"
|
||||
return sendRequest(r)
|
||||
}
|
||||
|
||||
func (r *Req) PutObj(body any) *Resp {
|
||||
marshal, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return &Resp{err: errors.New("解析json obj错误")}
|
||||
}
|
||||
return r.PutJson(string(marshal))
|
||||
}
|
||||
|
||||
func (r *Req) PostMulipart(files []MultipartFile, reqParams collx.M) *Resp {
|
||||
buf := &bytes.Buffer{}
|
||||
// 文件写入 buf
|
||||
|
||||
216
server/pkg/pool/channel.go
Normal file
216
server/pkg/pool/channel.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package pool
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"mayfly-go/pkg/logx"
|
||||
"sync"
|
||||
"time"
|
||||
//"reflect"
|
||||
)
|
||||
|
||||
var (
|
||||
//ErrMaxActiveConnReached 连接池超限
|
||||
ErrMaxActiveConnReached = errors.New("MaxActiveConnReached")
|
||||
)
|
||||
|
||||
// Config 连接池相关配置
|
||||
type Config struct {
|
||||
//连接池中拥有的最小连接数
|
||||
InitialCap int
|
||||
//最大并发存活连接数
|
||||
MaxCap int
|
||||
//最大空闲连接
|
||||
MaxIdle int
|
||||
//生成连接的方法
|
||||
Factory func() (interface{}, error)
|
||||
//关闭连接的方法
|
||||
Close func(interface{}) error
|
||||
//检查连接是否有效的方法
|
||||
Ping func(interface{}) error
|
||||
//连接最大空闲时间,超过该事件则将失效
|
||||
IdleTimeout time.Duration
|
||||
}
|
||||
|
||||
// channelPool 存放连接信息
|
||||
type channelPool struct {
|
||||
mu sync.RWMutex
|
||||
conns chan *idleConn
|
||||
factory func() (interface{}, error)
|
||||
close func(interface{}) error
|
||||
ping func(interface{}) error
|
||||
idleTimeout, waitTimeOut time.Duration
|
||||
maxActive int
|
||||
openingConns int
|
||||
}
|
||||
|
||||
type idleConn struct {
|
||||
conn interface{}
|
||||
t time.Time
|
||||
}
|
||||
|
||||
// NewChannelPool 初始化连接
|
||||
func NewChannelPool(poolConfig *Config) (Pool, error) {
|
||||
if !(poolConfig.InitialCap <= poolConfig.MaxIdle && poolConfig.MaxCap >= poolConfig.MaxIdle && poolConfig.InitialCap >= 0) {
|
||||
return nil, errors.New("invalid capacity settings")
|
||||
}
|
||||
if poolConfig.Factory == nil {
|
||||
return nil, errors.New("invalid factory func settings")
|
||||
}
|
||||
if poolConfig.Close == nil {
|
||||
return nil, errors.New("invalid close func settings")
|
||||
}
|
||||
|
||||
c := &channelPool{
|
||||
conns: make(chan *idleConn, poolConfig.MaxIdle),
|
||||
factory: poolConfig.Factory,
|
||||
close: poolConfig.Close,
|
||||
idleTimeout: poolConfig.IdleTimeout,
|
||||
maxActive: poolConfig.MaxCap,
|
||||
openingConns: poolConfig.InitialCap,
|
||||
}
|
||||
|
||||
if poolConfig.Ping != nil {
|
||||
c.ping = poolConfig.Ping
|
||||
}
|
||||
|
||||
for i := 0; i < poolConfig.InitialCap; i++ {
|
||||
conn, err := c.factory()
|
||||
if err != nil {
|
||||
c.Release()
|
||||
return nil, fmt.Errorf("factory is not able to fill the pool: %s", err)
|
||||
}
|
||||
c.conns <- &idleConn{conn: conn, t: time.Now()}
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// getConns 获取所有连接
|
||||
func (c *channelPool) getConns() chan *idleConn {
|
||||
c.mu.Lock()
|
||||
conns := c.conns
|
||||
c.mu.Unlock()
|
||||
return conns
|
||||
}
|
||||
|
||||
// Get 从pool中取一个连接
|
||||
func (c *channelPool) Get() (interface{}, error) {
|
||||
conns := c.getConns()
|
||||
if conns == nil {
|
||||
return nil, ErrClosed
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case wrapConn := <-conns:
|
||||
if wrapConn == nil {
|
||||
return nil, ErrClosed
|
||||
}
|
||||
//判断是否超时,超时则丢弃
|
||||
if timeout := c.idleTimeout; timeout > 0 {
|
||||
if wrapConn.t.Add(timeout).Before(time.Now()) {
|
||||
//丢弃并关闭该连接
|
||||
c.Close(wrapConn.conn)
|
||||
continue
|
||||
}
|
||||
}
|
||||
//判断是否失效,失效则丢弃,如果用户没有设定 ping 方法,就不检查
|
||||
if c.ping != nil {
|
||||
if err := c.Ping(wrapConn.conn); err != nil {
|
||||
c.Close(wrapConn.conn)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return wrapConn.conn, nil
|
||||
default:
|
||||
c.mu.Lock()
|
||||
logx.Debugf("openConn %v %v", c.openingConns, c.maxActive)
|
||||
defer c.mu.Unlock()
|
||||
if c.openingConns >= c.maxActive {
|
||||
return nil, ErrMaxActiveConnReached
|
||||
}
|
||||
if c.factory == nil {
|
||||
return nil, ErrClosed
|
||||
}
|
||||
conn, err := c.factory()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.openingConns++
|
||||
return conn, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Put 将连接放回pool中
|
||||
func (c *channelPool) Put(conn interface{}) error {
|
||||
if conn == nil {
|
||||
return errors.New("connection is nil. rejecting")
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
|
||||
if c.conns == nil {
|
||||
c.mu.Unlock()
|
||||
return c.Close(conn)
|
||||
}
|
||||
|
||||
select {
|
||||
case c.conns <- &idleConn{conn: conn, t: time.Now()}:
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
default:
|
||||
c.mu.Unlock()
|
||||
//连接池已满,直接关闭该连接
|
||||
return c.Close(conn)
|
||||
}
|
||||
}
|
||||
|
||||
// Close 关闭单条连接
|
||||
func (c *channelPool) Close(conn interface{}) error {
|
||||
if conn == nil {
|
||||
return errors.New("connection is nil. rejecting")
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.close == nil {
|
||||
return nil
|
||||
}
|
||||
c.openingConns--
|
||||
return c.close(conn)
|
||||
}
|
||||
|
||||
// Ping 检查单条连接是否有效
|
||||
func (c *channelPool) Ping(conn interface{}) error {
|
||||
if conn == nil {
|
||||
return errors.New("connection is nil. rejecting")
|
||||
}
|
||||
return c.ping(conn)
|
||||
}
|
||||
|
||||
// Release 释放连接池中所有连接
|
||||
func (c *channelPool) Release() {
|
||||
c.mu.Lock()
|
||||
conns := c.conns
|
||||
c.conns = nil
|
||||
c.factory = nil
|
||||
c.ping = nil
|
||||
closeFun := c.close
|
||||
c.close = nil
|
||||
c.mu.Unlock()
|
||||
|
||||
if conns == nil {
|
||||
return
|
||||
}
|
||||
|
||||
close(conns)
|
||||
for wrapConn := range conns {
|
||||
//log.Printf("Type %v\n",reflect.TypeOf(wrapConn.conn))
|
||||
_ = closeFun(wrapConn.conn)
|
||||
}
|
||||
}
|
||||
|
||||
// Len 连接池中已有的连接
|
||||
func (c *channelPool) Len() int {
|
||||
return len(c.getConns())
|
||||
}
|
||||
21
server/pkg/pool/pool.go
Normal file
21
server/pkg/pool/pool.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package pool
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
//ErrClosed 连接池已经关闭Error
|
||||
ErrClosed = errors.New("pool is closed")
|
||||
)
|
||||
|
||||
// Pool 基本方法
|
||||
type Pool interface {
|
||||
Get() (interface{}, error)
|
||||
|
||||
Put(interface{}) error
|
||||
|
||||
Close(interface{}) error
|
||||
|
||||
Release()
|
||||
|
||||
Len() int
|
||||
}
|
||||
@@ -43,6 +43,9 @@ func NewPut(path string, handler HandlerFunc) *Conf {
|
||||
func NewDelete(path string, handler HandlerFunc) *Conf {
|
||||
return New("DELETE", path, handler)
|
||||
}
|
||||
func NewAny(path string, handler HandlerFunc) *Conf {
|
||||
return New("any", path, handler)
|
||||
}
|
||||
|
||||
func (r *Conf) ToGinHFunc() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
@@ -82,7 +85,11 @@ func (r *Conf) NoRes() *Conf {
|
||||
|
||||
// 注册至group
|
||||
func (r *Conf) Group(gr *gin.RouterGroup) *Conf {
|
||||
gr.Handle(r.method, r.path, r.ToGinHFunc())
|
||||
if r.method == "any" {
|
||||
gr.Any(r.path, r.ToGinHFunc())
|
||||
} else {
|
||||
gr.Handle(r.method, r.path, r.ToGinHFunc())
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user