mirror of
https://gitee.com/dromara/mayfly-go
synced 2026-06-11 12:35:21 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db8f30f27f | ||
|
|
434c1fdfb3 | ||
|
|
96ef4d2d6f | ||
|
|
fab45f0823 | ||
|
|
44b5f6ebfd | ||
|
|
f234aff250 | ||
|
|
a17fa5a103 | ||
|
|
519089d8d0 |
@@ -19,17 +19,16 @@
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"asciinema-player": "^3.15.1",
|
||||
"axios": "^1.16.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.20",
|
||||
"echarts": "^6.0.0",
|
||||
"element-plus": "^2.14.0",
|
||||
"dayjs": "^1.11.21",
|
||||
"echarts": "^6.1.0",
|
||||
"element-plus": "^2.14.1",
|
||||
"js-base64": "^3.7.8",
|
||||
"jsencrypt": "^3.5.4",
|
||||
"json-bigint": "^1.0.0",
|
||||
"mermaid": "^11.15.0",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"monaco-sql-languages": "^1.0.0",
|
||||
"monaco-sql-languages": "^1.1.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^3.0.4",
|
||||
"qrcode.vue": "^3.9.1",
|
||||
@@ -42,7 +41,7 @@
|
||||
"vue": "3.6.0-beta.11",
|
||||
"vue-element-plus-x": "^2.0.3",
|
||||
"vue-i18n": "^11.4.4",
|
||||
"vue-router": "^5.0.7",
|
||||
"vue-router": "^5.1.0",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"x-markdown-vue": "0.0.200",
|
||||
"xlsx": "^0.18.5"
|
||||
@@ -68,7 +67,7 @@
|
||||
"tailwindcss": "^4.3.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.59.2",
|
||||
"vite": "^8.0.14",
|
||||
"vite": "^8.0.16",
|
||||
"vite-plugin-progress": "0.0.7",
|
||||
"vue-eslint-parser": "^10.4.0"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { templateResolve } from '@/common/utils/string';
|
||||
import { RequestOptions, useApiFetch } from '@/hooks/useRequest';
|
||||
import config from './config';
|
||||
import request, { joinClientParams } from './request';
|
||||
import { getToken } from './utils/storage';
|
||||
|
||||
/**
|
||||
* 文件上传选项
|
||||
@@ -78,17 +78,6 @@ class Api<T = any, P = any> {
|
||||
return (data.value as T) || (res as T);
|
||||
}
|
||||
|
||||
/**
|
||||
* xhr 请求对应的该api
|
||||
* @param {Object} param 请求该api的参数
|
||||
*/
|
||||
async xhrReq(param: any = null, options: any = {}): Promise<T> {
|
||||
if (this.beforeHandler) {
|
||||
await this.beforeHandler(param);
|
||||
}
|
||||
return request.xhrReq(this.method, this.url, param, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传请求
|
||||
* @param formData FormData 对象(调用方自行构建,包含文件和其他参数)
|
||||
@@ -137,14 +126,44 @@ class Api<T = any, P = any> {
|
||||
/**
|
||||
* 原始文件流上传请求(直接使用文件流作为 body,参数通过 URL query 传递)
|
||||
* @param file 文件对象
|
||||
* @param queryParams URL 查询参数字符串
|
||||
* @param queryParams URL 查询参数对象(可选)
|
||||
* @param options 上传选项(可包含自定义 headers)
|
||||
* @returns { abort: () => void } 返回中止方法
|
||||
*/
|
||||
uploadRaw(file: File, queryParams: string, options: UploadOptions & { headers?: Record<string, string> } = {}): { abort: () => void } {
|
||||
uploadRaw(file: File, queryParams?: Record<string, string>, options: UploadOptions & { headers?: Record<string, string> } = {}): { abort: () => void } {
|
||||
const { onSuccess, onError, headers = {} } = options;
|
||||
|
||||
const url = `${config.baseApiUrl}${this.url}?${queryParams}&${joinClientParams()}`;
|
||||
// 构建 URL,兼容没有 queryParams 的情况
|
||||
let url = `${config.baseApiUrl}${this.url}`;
|
||||
// 简单判断该url是否是restful风格
|
||||
if (url.indexOf('{') != -1 && queryParams) {
|
||||
url = templateResolve(url, queryParams);
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
// 添加业务参数
|
||||
if (queryParams) {
|
||||
Object.entries(queryParams).forEach(([key, value]) => {
|
||||
searchParams.append(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
// 添加客户端参数
|
||||
const clientParams = joinClientParams();
|
||||
if (clientParams) {
|
||||
// 将 joinClientParams 返回的字符串追加到 searchParams
|
||||
const clientParamsObj = new URLSearchParams(clientParams);
|
||||
clientParamsObj.forEach((value, key) => {
|
||||
searchParams.append(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
// 拼接完整的 query string
|
||||
const queryString = searchParams.toString();
|
||||
if (queryString) {
|
||||
url += `?${queryString}`;
|
||||
}
|
||||
|
||||
// 创建 AbortController 用于取消请求
|
||||
const abortController = new AbortController();
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
import router from '../router';
|
||||
import config from './config';
|
||||
import { getClientId, getToken } from './utils/storage';
|
||||
import { templateResolve } from './utils/string';
|
||||
import { Msg } from '@/hooks/useI18n';
|
||||
import axios from 'axios';
|
||||
import JSONBig from 'json-bigint';
|
||||
import { useApiFetch } from '../hooks/useRequest';
|
||||
import Api from './Api';
|
||||
|
||||
// 配置 JSONBig:将大数(int64/uint64)转为字符串,避免精度丢失
|
||||
// storeAsString: 将大数存储为字符串,而不是 BigNumber 对象
|
||||
const JSONBigString = JSONBig({ storeAsString: true });
|
||||
import config from './config';
|
||||
import { getClientId, getToken } from './utils/storage';
|
||||
|
||||
export default {
|
||||
request,
|
||||
xhrReq,
|
||||
get,
|
||||
post,
|
||||
put,
|
||||
@@ -50,138 +40,6 @@ export const baseUrl: string = config.baseApiUrl;
|
||||
// const baseUrl: string = 'http://localhost:18888/api';
|
||||
// const baseWsUrl: string = config.baseWsUrl;
|
||||
|
||||
/**
|
||||
* 通知错误消息
|
||||
* @param msg 错误消息
|
||||
*/
|
||||
function notifyErrorMsg(msg: string) {
|
||||
// 危险通知
|
||||
Msg.error(msg);
|
||||
}
|
||||
|
||||
// create an axios instance
|
||||
const axiosInst = axios.create({
|
||||
baseURL: baseUrl, // url = base url + request url
|
||||
timeout: 60000, // request timeout
|
||||
// 使用 json-bigint 处理响应数据,解决 int64/uint64 精度丢失问题
|
||||
transformResponse: [
|
||||
function (data) {
|
||||
// 对响应数据进行转换
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
// 使用 JSONBigString 解析,大数会被转为字符串
|
||||
return JSONBigString.parse(data);
|
||||
} catch (err) {
|
||||
// 如果解析失败,返回原始数据
|
||||
return data;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// request interceptor
|
||||
axiosInst.interceptors.request.use(
|
||||
(config: any) => {
|
||||
// do something before request is sent
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
// 设置token
|
||||
config.headers['Authorization'] = token;
|
||||
config.headers['ClientId'] = getClientId();
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// response interceptor
|
||||
axiosInst.interceptors.response.use(
|
||||
(response) => response,
|
||||
(e: any) => {
|
||||
const rejectPromise = Promise.reject(e);
|
||||
|
||||
if (axios.isCancel(e)) {
|
||||
console.log('请求已取消');
|
||||
return rejectPromise;
|
||||
}
|
||||
|
||||
const statusCode = e.response?.status;
|
||||
if (statusCode == 500) {
|
||||
notifyErrorMsg('服务器未知异常');
|
||||
return rejectPromise;
|
||||
}
|
||||
|
||||
if (statusCode == 404) {
|
||||
notifyErrorMsg('请求接口未找到');
|
||||
return rejectPromise;
|
||||
}
|
||||
|
||||
if (e.message) {
|
||||
// 对响应错误做点什么
|
||||
if (e.message.indexOf('timeout') != -1) {
|
||||
notifyErrorMsg('网络请求超时');
|
||||
return rejectPromise;
|
||||
}
|
||||
|
||||
if (e.message == 'Network Error') {
|
||||
notifyErrorMsg('网络连接错误');
|
||||
return rejectPromise;
|
||||
}
|
||||
}
|
||||
|
||||
notifyErrorMsg('网络请求错误');
|
||||
return rejectPromise;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* xhr请求url
|
||||
*
|
||||
* @param method 请求方法
|
||||
* @param url url
|
||||
* @param params 参数
|
||||
* @param options 可选
|
||||
* @returns
|
||||
*/
|
||||
export function xhrReq(method: string, url: string, params: any = null, options: any = {}) {
|
||||
if (!url) {
|
||||
throw new Error('请求url不能为空');
|
||||
}
|
||||
|
||||
// 简单判断该url是否是restful风格
|
||||
if (url.indexOf('{') != -1) {
|
||||
url = templateResolve(url, params);
|
||||
}
|
||||
|
||||
const req: any = {
|
||||
method,
|
||||
url,
|
||||
...options,
|
||||
};
|
||||
|
||||
// post和put使用json格式传参
|
||||
if (method === 'post' || method === 'put') {
|
||||
req.data = params;
|
||||
} else {
|
||||
req.params = params;
|
||||
}
|
||||
|
||||
return axiosInst
|
||||
.request(req)
|
||||
.then((response) => {
|
||||
// 获取请求返回结果
|
||||
const result: Result = response.data;
|
||||
return parseResult(result);
|
||||
})
|
||||
.catch((e) => {
|
||||
return Promise.reject(e);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch请求url
|
||||
*
|
||||
@@ -277,23 +135,3 @@ export function downloadFile(key: string) {
|
||||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
|
||||
function parseResult(result: Result) {
|
||||
if (result.code === ResultEnum.SUCCESS) {
|
||||
return result.data;
|
||||
}
|
||||
|
||||
// 如果提示没有权限,则移除token,使其重新登录
|
||||
if (result.code === ResultEnum.NO_PERMISSION) {
|
||||
router.push({
|
||||
path: '/401',
|
||||
});
|
||||
}
|
||||
|
||||
// 如果返回的code不为成功,则会返回对应的错误msg,则直接统一通知即可。忽略登录超时或没有权限的提示(直接跳转至401页面)
|
||||
if (result.msg && result?.code != ResultEnum.NO_PERMISSION) {
|
||||
notifyErrorMsg(result.msg);
|
||||
}
|
||||
|
||||
return Promise.reject(result);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
<!-- Drawer 模式 -->
|
||||
<el-drawer
|
||||
:append-to-body="false"
|
||||
:title="props.title"
|
||||
v-model="dialogVisible"
|
||||
:size="props.drawerSize || '50%'"
|
||||
|
||||
@@ -418,19 +418,22 @@ const showContextMenu = (event: MouseEvent, selectedText: string) => {
|
||||
.withHideFunc(() => !selectedText)
|
||||
.withOnClick(() => {
|
||||
downloadSelectedFile(state.contextmenu.selectedItem);
|
||||
}),
|
||||
})
|
||||
.withPermission('machine:file:upload'),
|
||||
new ContextmenuItem('uploadFile', 'components.terminal.uploadFileToCurrentDir')
|
||||
.withIcon('Upload')
|
||||
.withHideFunc(() => !props.machineId || !props.authCertName)
|
||||
.withOnClick(() => {
|
||||
triggerFilesUpload();
|
||||
}),
|
||||
})
|
||||
.withPermission('machine:file:upload'),
|
||||
new ContextmenuItem('uploadFolder', 'components.terminal.uploadFolderToCurrentDir')
|
||||
.withIcon('Upload')
|
||||
.withHideFunc(() => !props.machineId || !props.authCertName)
|
||||
.withOnClick(() => {
|
||||
triggerFolderUpload();
|
||||
}),
|
||||
})
|
||||
.withPermission('machine:file:upload'),
|
||||
];
|
||||
|
||||
// 打开右键菜单
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer v-model="visible" :before-close="cancel" size="50%" body-class="flex flex-col">
|
||||
<el-drawer :append-to-body="false" v-model="visible" :before-close="cancel" size="50%" body-class="flex flex-col">
|
||||
<template #header>
|
||||
<DrawerHeader :header="props.title" :back="cancel">
|
||||
<template #extra>
|
||||
|
||||
@@ -119,6 +119,8 @@ export default {
|
||||
close: 'Close',
|
||||
closeOther: 'Close Other',
|
||||
closeAll: 'Close All',
|
||||
closeLeft: 'Close Left',
|
||||
closeRight: 'Close Right',
|
||||
fullscreen: 'Fullscreen',
|
||||
closeFullscreen: 'Close Fullscreen',
|
||||
},
|
||||
|
||||
@@ -38,6 +38,7 @@ export default {
|
||||
stopImageConfirm: 'Are you sure to stop image [{name}] ?',
|
||||
export: 'Export',
|
||||
imageUploading: 'Image uploading, please wait...',
|
||||
uploadSuccess: 'Image uploaded successfully',
|
||||
imageTips: 'Support manual input and select',
|
||||
forcePull: 'Force Pull Image',
|
||||
hostPortPlaceholder: '80',
|
||||
|
||||
@@ -29,6 +29,8 @@ export default {
|
||||
indexStats: 'Stats',
|
||||
opViewColumns: 'Option View Columns',
|
||||
opIndex: 'Index Management',
|
||||
opDataManage: 'Data Management',
|
||||
selectIndexFirst: 'Please select an index first',
|
||||
opSearch: 'Search',
|
||||
searchParamsPreview: 'Search Params Preview',
|
||||
opBasicSearch: 'Basic Search',
|
||||
@@ -98,6 +100,31 @@ export default {
|
||||
text: 'Text',
|
||||
startAnalyze: 'Start Analyze',
|
||||
},
|
||||
export: {
|
||||
title: 'Export Data',
|
||||
selectedCount: '{count} rows selected',
|
||||
exportAll: 'Export All Data',
|
||||
exportSelected: 'Export Selected Data',
|
||||
exportQuery: 'Export Query Results',
|
||||
exportType: 'Export Type',
|
||||
csv: 'CSV File',
|
||||
excel: 'Excel File',
|
||||
json: 'JSON File',
|
||||
confirm: 'Confirm Export',
|
||||
exporting: 'Exporting...',
|
||||
exportAllConfirm: 'Export all data from index [{name}] ({total} docs total). Continue?',
|
||||
largeExportTip: 'Large dataset ({total} docs), will be exported via backend batch processing and compressed download',
|
||||
selectAllFields: 'Select All Fields',
|
||||
exportFields: 'Export Fields',
|
||||
noData: 'No data to export',
|
||||
phase: {
|
||||
querying: 'Querying...',
|
||||
exporting: 'Exporting...',
|
||||
compressing: 'Compressing...',
|
||||
completed: 'Completed',
|
||||
unknown: 'Unknown',
|
||||
},
|
||||
},
|
||||
contextmenu: {
|
||||
index: {
|
||||
addIndex: 'Add Index',
|
||||
|
||||
@@ -83,6 +83,12 @@ export default {
|
||||
searchGroup: 'Enter group name',
|
||||
selectGroupPlaceholder: 'select group',
|
||||
Members: 'Members',
|
||||
groupMembers: 'Consumer Group Members',
|
||||
clientHost: 'Client Host',
|
||||
clientID: 'Client ID',
|
||||
instanceID: 'Instance ID',
|
||||
memberID: 'Member ID',
|
||||
assignedTopics: 'Assigned Topic Partitions',
|
||||
partitionsFeatureComingSoon: 'Partitions feature coming soon',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -16,9 +16,12 @@ export default {
|
||||
rootTag: 'Root Tag',
|
||||
selectTagPlaceholder: 'Select the associated tag',
|
||||
machineOp: 'Machine Operation',
|
||||
machineTerminal: 'Machine Terminal',
|
||||
machineFile: 'Machine File',
|
||||
dbDataOp: 'Db Operation',
|
||||
redisDataOp: 'Redis Operation',
|
||||
esDataOp: 'Es Operation',
|
||||
esIndexData: 'ES Index Data',
|
||||
mongoDataOp: 'Mongo Operation',
|
||||
allResource: 'All Resource',
|
||||
mq: {
|
||||
|
||||
@@ -117,6 +117,8 @@ export default {
|
||||
close: '关闭',
|
||||
closeOther: '关闭其它',
|
||||
closeAll: '全部关闭',
|
||||
closeLeft: '关闭左侧',
|
||||
closeRight: '关闭右侧',
|
||||
fullscreen: '当前页全屏',
|
||||
closeFullscreen: '关闭全屏',
|
||||
},
|
||||
|
||||
@@ -38,6 +38,7 @@ export default {
|
||||
stopImageConfirm: '确定删除该镜像?',
|
||||
export: '导出',
|
||||
imageUploading: '镜像导入中,请稍后...',
|
||||
uploadSuccess: '镜像导入成功',
|
||||
imageTips: '支持手动输入并选择',
|
||||
forcePull: '强制拉取镜像',
|
||||
hostPortPlaceholder: '80',
|
||||
|
||||
@@ -29,6 +29,8 @@ export default {
|
||||
indexStats: '统计信息',
|
||||
opViewColumns: '设置显示字段',
|
||||
opIndex: '索引管理',
|
||||
opDataManage: '数据管理',
|
||||
selectIndexFirst: '请先选择索引',
|
||||
opSearch: '搜索',
|
||||
searchParamsPreview: '搜索条件预览',
|
||||
opBasicSearch: '基础搜索',
|
||||
@@ -97,6 +99,31 @@ export default {
|
||||
text: '文本',
|
||||
startAnalyze: '开始分析',
|
||||
},
|
||||
export: {
|
||||
title: '导出数据',
|
||||
selectedCount: '已选择 {count} 条数据',
|
||||
exportAll: '导出所有数据',
|
||||
exportSelected: '导出已选数据',
|
||||
exportQuery: '导出查询结果',
|
||||
exportType: '导出类型',
|
||||
csv: 'CSV 文件',
|
||||
excel: 'Excel 文件',
|
||||
json: 'JSON 文件',
|
||||
confirm: '确认导出',
|
||||
exporting: '正在导出...',
|
||||
exportAllConfirm: '将导出索引 [{name}] 的所有数据(共 {total} 条),确认继续吗?',
|
||||
largeExportTip: '数据量较大(共 {total} 条),将通过后台分批查询并压缩后下载',
|
||||
selectAllFields: '全选字段',
|
||||
exportFields: '导出字段',
|
||||
noData: '没有可导出的数据',
|
||||
phase: {
|
||||
querying: '正在查询...',
|
||||
exporting: '正在导出数据...',
|
||||
compressing: '正在压缩...',
|
||||
completed: '导出完成',
|
||||
unknown: '未知状态',
|
||||
},
|
||||
},
|
||||
contextmenu: {
|
||||
index: {
|
||||
addIndex: '添加索引',
|
||||
|
||||
@@ -83,6 +83,12 @@ export default {
|
||||
searchGroup: '输入组名称',
|
||||
selectGroupPlaceholder: '选择分组',
|
||||
Members: '成员',
|
||||
groupMembers: '消费者组成员',
|
||||
clientHost: '客户端地址',
|
||||
clientID: '客户端 ID',
|
||||
instanceID: '实例 ID',
|
||||
memberID: '成员 ID',
|
||||
assignedTopics: '分配的 Topic 分区',
|
||||
partitionsFeatureComingSoon: '分区详情功能即将上线',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -18,9 +18,12 @@ export default {
|
||||
rootTag: '根标签',
|
||||
selectTagPlaceholder: '请选择关联标签',
|
||||
machineOp: '机器操作',
|
||||
machineTerminal: '机器终端',
|
||||
machineFile: '机器文件',
|
||||
dbDataOp: '数据库操作',
|
||||
redisDataOp: 'Redis操作',
|
||||
esDataOp: 'ES操作',
|
||||
esIndexData: 'ES索引数据',
|
||||
mongoDataOp: 'Mongo操作',
|
||||
containerOp: '容器操作',
|
||||
allResource: '所有资源',
|
||||
|
||||
@@ -580,19 +580,16 @@ const initSetLayoutChange = () => {
|
||||
themeConfig.value.isShowLogo = true;
|
||||
themeConfig.value.isBreadcrumb = false;
|
||||
themeConfig.value.isCollapse = false;
|
||||
themeConfig.value.isTagsview = true;
|
||||
themeConfig.value.isClassicSplitMenu = false;
|
||||
} else if (themeConfig.value.layout === 'columns') {
|
||||
themeConfig.value.isShowLogo = true;
|
||||
themeConfig.value.isBreadcrumb = true;
|
||||
themeConfig.value.isCollapse = false;
|
||||
themeConfig.value.isTagsview = true;
|
||||
themeConfig.value.isClassicSplitMenu = false;
|
||||
} else {
|
||||
themeConfig.value.isShowLogo = false;
|
||||
themeConfig.value.isBreadcrumb = true;
|
||||
themeConfig.value.isCollapse = false;
|
||||
themeConfig.value.isTagsview = true;
|
||||
themeConfig.value.isClassicSplitMenu = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import { useUserInfo } from '@/store/userInfo';
|
||||
import { getServerConf, getSysStyleConfig } from '@/common/sysconfig';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import { getLocal, getThemeConfig } from '@/common/utils/storage';
|
||||
import { useUserInfo } from '@/store/userInfo';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
// 系统默认logo图标,对应于@/assets/image/logo.svg
|
||||
const logoIcon =
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:append-to-body="false"
|
||||
:title="title"
|
||||
v-model="dialogVisible"
|
||||
z-index="2000"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer :title="props.title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="50%">
|
||||
<el-drawer :append-to-body="false" :title="props.title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="50%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer :title="title" v-model="visible" :before-close="onCancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||
<el-drawer :append-to-body="false" :title="title" v-model="visible" :before-close="onCancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="onCancel" />
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:append-to-body="false"
|
||||
:title="props.title"
|
||||
v-model="visible"
|
||||
:before-close="cancel"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:append-to-body="false"
|
||||
:title="title"
|
||||
v-model="visible"
|
||||
:before-close="cancel"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:append-to-body="false"
|
||||
body-class="!pt-2"
|
||||
header-class="!mb-2"
|
||||
:title="title"
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<img :src="userInfo.photo" class="w-full h-full rounded transition-transform duration-300 hover:scale-110" />
|
||||
</el-upload>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 px-3.75">
|
||||
<div class="mb-4 text-lg truncate">{{ $t('home.welcomeMsg', { name: userInfo.name }) }}</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-1.5 text-[13px]">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer :title="title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||
<el-drawer :append-to-body="false" :title="title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer :title="title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||
<el-drawer :append-to-body="false" :title="title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
</template>
|
||||
|
||||
@@ -1,51 +1,6 @@
|
||||
import { OptionsApi, SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { ContextmenuItem } from '@/components/contextmenu';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { tagApi } from '../tag/api';
|
||||
import { markRaw } from 'vue';
|
||||
|
||||
// 资源配置
|
||||
export interface ResourceConfig {
|
||||
order?: number;
|
||||
resourceType: number; // 资源类型
|
||||
rootNodeType: NodeType; // 资源根节点类型
|
||||
|
||||
// 资源管理组件配置
|
||||
manager?: {
|
||||
componentConf: ResourceComponentConfig; // 组件
|
||||
countKey?: string; // 统计数key,tab展示的数字对象key
|
||||
permCode?: string; // 权限码
|
||||
};
|
||||
}
|
||||
|
||||
export interface ResourceComponentConfig {
|
||||
name: string; // 名称
|
||||
component?: any; // 组件
|
||||
icon?: {
|
||||
name: string;
|
||||
color?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ResourceOpCtx {
|
||||
/**
|
||||
* 添加资源相关组件
|
||||
* @param component 资源相关组件配置
|
||||
* @returns 组件引用
|
||||
*/
|
||||
addResourceComponent(component: ResourceComponentConfig): Promise<any>;
|
||||
|
||||
/**
|
||||
* 获取树节点
|
||||
* @param nodeKey 节点key
|
||||
*/
|
||||
getTreeNode(nodeKey: string): any;
|
||||
|
||||
setCurrentTreeKey(nodeKey: string): void;
|
||||
|
||||
reloadTreeNode(nodeKey: string): void;
|
||||
}
|
||||
|
||||
export class TagTreeNode {
|
||||
/**
|
||||
* 节点id
|
||||
@@ -87,11 +42,6 @@ export class TagTreeNode {
|
||||
// 节点组件
|
||||
nodeComponent?: any;
|
||||
|
||||
/**
|
||||
* 节点上下文
|
||||
*/
|
||||
ctx?: ResourceOpCtx;
|
||||
|
||||
static TagPath = -1;
|
||||
|
||||
constructor(key: any, label: string, type?: NodeType) {
|
||||
@@ -101,7 +51,7 @@ export class TagTreeNode {
|
||||
}
|
||||
|
||||
static new(parent: TagTreeNode, key: any, label: string, type?: NodeType) {
|
||||
return new TagTreeNode(key, label, type).withContext(parent.ctx);
|
||||
return new TagTreeNode(key, label, type);
|
||||
}
|
||||
|
||||
withLabelRemark(labelRemark: any) {
|
||||
@@ -134,14 +84,6 @@ export class TagTreeNode {
|
||||
return this;
|
||||
}
|
||||
|
||||
withContext(ctx: ResourceOpCtx | undefined | null) {
|
||||
if (!ctx) {
|
||||
return this;
|
||||
}
|
||||
this.ctx = ctx;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载子节点,使用节点类型的loadNodesFunc去加载子节点
|
||||
* @returns 子节点信息
|
||||
@@ -178,6 +120,11 @@ export class NodeType {
|
||||
// 节点双击事件
|
||||
nodeDblclickFunc?: (node: TagTreeNode) => void;
|
||||
|
||||
/**
|
||||
* 折叠时是否删除子节点以释放内存(展开时重新懒加载)
|
||||
*/
|
||||
collapseRemoveChildren: boolean = false;
|
||||
|
||||
constructor(value: number) {
|
||||
this.value = value;
|
||||
}
|
||||
@@ -221,24 +168,14 @@ export class NodeType {
|
||||
this.contextMenuItems = contextMenuItems;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取标签搜索项配置
|
||||
* @param resourceType 资源类型
|
||||
* @returns
|
||||
*/
|
||||
export function getTagPathSearchItem(resourceType: any) {
|
||||
return SearchItem.select('tagPath', 'common.tag').withOptionsApi(
|
||||
OptionsApi.new(tagApi.getResourceTagPaths, { resourceType }).withConvertFn((res: any) => {
|
||||
return res.map((x: any) => {
|
||||
return {
|
||||
label: x,
|
||||
value: x,
|
||||
};
|
||||
});
|
||||
})
|
||||
);
|
||||
/**
|
||||
* 设置折叠时是否删除子节点
|
||||
*/
|
||||
withCollapseRemoveChildren(collapseRemoveChildren: boolean = true) {
|
||||
this.collapseRemoveChildren = collapseRemoveChildren;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export function expandCodePath(codePath: string) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="db-list">
|
||||
<el-drawer
|
||||
:append-to-body="false"
|
||||
:title="title"
|
||||
v-model="dialogVisible"
|
||||
@open="search"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||
<el-drawer :append-to-body="false" :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>
|
||||
|
||||
@@ -76,7 +76,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { TagResourceTypePath } from '@/common/commonEnum';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
@@ -87,7 +86,6 @@ import { Msg, useI18nCreateTitle, useI18nDeleteConfirm, useI18nEditTitle } from
|
||||
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import ResourceAuthCert from '../component/ResourceAuthCert.vue';
|
||||
import { getTagPathSearchItem } from '../component/tag';
|
||||
import TagCodePath from '../component/TagCodePath.vue';
|
||||
import { dbApi } from './api';
|
||||
import { getDbDialect } from './dialect';
|
||||
@@ -110,7 +108,7 @@ const perms = {
|
||||
saveDb: 'db:save',
|
||||
};
|
||||
|
||||
const searchItems = [SearchItem.input('keyword', 'common.keyword').withPlaceholder('db.keywordPlaceholder'), getTagPathSearchItem(TagResourceTypePath.Db)];
|
||||
const searchItems = [SearchItem.input('keyword', 'common.keyword').withPlaceholder('db.keywordPlaceholder')];
|
||||
|
||||
const columns = ref([
|
||||
TableColumn.new('name', 'common.name').isSlot('name').setAddWidth(15),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Api from '@/common/Api';
|
||||
import { registerSqlExecAborter, createSqlExecNotification } from '@/components/sysmsg/db/db-sql-exec-progress';
|
||||
import { AesEncrypt } from '@/common/crypto';
|
||||
import { joinClientParams } from '@/common/request';
|
||||
import { createSqlExecNotification, registerSqlExecAborter } from '@/components/sysmsg/db/db-sql-exec-progress';
|
||||
|
||||
export const dbApi = {
|
||||
// 获取权限列表
|
||||
@@ -103,12 +102,12 @@ export function uploadSqlFile(
|
||||
// 生成 uploadId
|
||||
const uploadId = `sql_exec_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
||||
|
||||
// 使用 URLSearchParams 构建查询参数
|
||||
const queryParams = new URLSearchParams({
|
||||
// 构建查询参数对象
|
||||
const queryParams: Record<string, string> = {
|
||||
db: params.dbName,
|
||||
uploadId: uploadId,
|
||||
filename: file.name,
|
||||
}).toString();
|
||||
};
|
||||
|
||||
// 创建 Api 实例
|
||||
const api = Api.newPost(`/dbs/${params.dbId}/exec-sql-file`);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="card p-1! flex items-center justify-between">
|
||||
<div>
|
||||
<el-link @click="onRunSql()" underline="never" class="ml-3.5" icon="VideoPlay"> </el-link>
|
||||
@@ -40,7 +40,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-splitter style="height: calc(100vh - 220px)" layout="vertical" @resize-end="onResizeTableHeight">
|
||||
<el-splitter ref="splitterRef" class="flex-1 min-h-0" layout="vertical" @resize-end="onResizeTableHeight">
|
||||
<el-splitter-panel :size="state.editorSize" max="80%">
|
||||
<MonacoEditor ref="monacoEditorRef" class="mt-1" v-model="state.sql" language="sql" height="100%" :id="'MonacoTextarea-' + getKey()" />
|
||||
</el-splitter-panel>
|
||||
@@ -56,7 +56,14 @@
|
||||
>
|
||||
<el-tab-pane class="h-full!" closable v-for="dt in state.execResTabs" :label="dt.id" :name="dt.id" :key="dt.id">
|
||||
<template #label>
|
||||
<el-popover :show-after="1000" placement="top-start" :title="$t('db.execInfo')" trigger="hover" :width="300">
|
||||
<el-popover
|
||||
:show-after="1000"
|
||||
placement="top-start"
|
||||
:title="$t('db.execInfo')"
|
||||
trigger="hover"
|
||||
:width="300"
|
||||
:teleported="false"
|
||||
>
|
||||
<template #reference>
|
||||
<div>
|
||||
<span>
|
||||
@@ -135,7 +142,7 @@ import config from '@/common/config';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { format as sqlFormatter } from 'sql-formatter';
|
||||
import { nextTick, onMounted, reactive, ref, toRefs, unref } from 'vue';
|
||||
import { nextTick, onMounted, reactive, ref, toRefs, unref, useTemplateRef } from 'vue';
|
||||
|
||||
import { editor } from 'monaco-editor';
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
@@ -276,13 +283,17 @@ const onRemoveTab = (targetId: number) => {
|
||||
}
|
||||
};
|
||||
|
||||
const splitterRef = useTemplateRef<HTMLElement>('splitterRef');
|
||||
|
||||
const onResizeTableHeight = (index: number, sizes: number[]) => {
|
||||
if (!sizes || sizes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const vh = window.innerHeight;
|
||||
const plitpaneHeight = vh - 200;
|
||||
// 基于splitter容器实际高度计算,兼容全屏模式
|
||||
const splitterEl = splitterRef.value?.$el || splitterRef.value;
|
||||
const containerHeight = splitterEl ? (splitterEl as HTMLElement).getBoundingClientRect().height : window.innerHeight - 220;
|
||||
const plitpaneHeight = containerHeight - 10;
|
||||
|
||||
let editorHeight = sizes[0];
|
||||
if (editorHeight < 0 || editorHeight > plitpaneHeight - 43) {
|
||||
@@ -290,7 +301,7 @@ const onResizeTableHeight = (index: number, sizes: number[]) => {
|
||||
editorHeight = plitpaneHeight / 2;
|
||||
}
|
||||
|
||||
let tableDataHeight = plitpaneHeight - editorHeight - 47;
|
||||
let tableDataHeight = plitpaneHeight - editorHeight - 15;
|
||||
|
||||
state.editorSize = editorHeight;
|
||||
state.tableDataHeight = tableDataHeight + 'px';
|
||||
|
||||
@@ -1,176 +1,172 @@
|
||||
<template>
|
||||
<div class="db-table-data mt-1" :style="{ height: tableHeight }">
|
||||
<el-auto-resizer>
|
||||
<template #default="{ height, width }">
|
||||
<el-table-v2
|
||||
ref="tableRef"
|
||||
:header-height="showColumnTip && dbConfig.showColumnComment ? 48 : 30"
|
||||
:row-height="30"
|
||||
:row-class="rowClass"
|
||||
:row-key="null"
|
||||
:columns="state.columns"
|
||||
:data="datas"
|
||||
:width="width"
|
||||
:height="height"
|
||||
fixed
|
||||
class="table"
|
||||
:row-event-handlers="rowEventHandlers"
|
||||
@scroll="onTableScroll"
|
||||
>
|
||||
<template #header="{ columns }">
|
||||
<div v-for="(column, i) in columns" :key="i">
|
||||
<div
|
||||
:style="{
|
||||
width: `${column.width}px`,
|
||||
height: '100%',
|
||||
textAlign: 'center',
|
||||
borderRight: 'var(--el-table-border)',
|
||||
borderTop: 'var(--el-table-border)',
|
||||
}"
|
||||
>
|
||||
<!-- 行号列 -->
|
||||
<div v-if="column.key == rowNoColumn.key" class="header-column-title">
|
||||
<b class="el-text" tag="b"> {{ column.title }} </b>
|
||||
</div>
|
||||
|
||||
<!-- 字段名列 -->
|
||||
<div v-else style="position: relative" @mouseenter="showColumnAction(column)" @mouseleave="hideColumnAction">
|
||||
<!-- 字段列的数据类型 -->
|
||||
<div class="column-type">
|
||||
<span v-if="column.dataTypeSubscript === 'icon-clock'">
|
||||
<SvgIcon :size="9" name="Clock" style="cursor: unset" />
|
||||
</span>
|
||||
<span class="text-[8px]!" v-else>{{ column.dataTypeSubscript }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="showColumnTip">
|
||||
<div class="header-column-title">
|
||||
<b :title="column.remark" class="el-text cursor-pointer">
|
||||
{{ column.title }}
|
||||
</b>
|
||||
</div>
|
||||
|
||||
<!-- 字段备注信息 -->
|
||||
<div
|
||||
v-if="dbConfig.showColumnComment"
|
||||
style="color: var(--el-color-info-light-3)"
|
||||
class="text-[10px]! el-text el-text--small is-truncated"
|
||||
>
|
||||
{{ column.columnComment }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="header-column-title">
|
||||
<b class="el-text"> {{ column.title }} </b>
|
||||
</div>
|
||||
|
||||
<!-- 字段列右部分内容 -->
|
||||
<div class="column-right">
|
||||
<el-dropdown
|
||||
@command="handleColumnCommand(column, $event)"
|
||||
@visibleChange="onColumnActionVisibleChange(column, $event)"
|
||||
trigger="click"
|
||||
v-if="column.key !== rowNoColumn.key"
|
||||
size="small"
|
||||
placement="bottom-start"
|
||||
>
|
||||
<span class="column-actions-trigger">
|
||||
<!-- 排序箭头图标 -->
|
||||
<SvgIcon
|
||||
v-if="
|
||||
column.key == nowSortColumn?.key && !showColumnActions[column.key] && !columnActionVisible[column.key]
|
||||
"
|
||||
:color="'var(--el-color-primary)'"
|
||||
:name="nowSortColumn?.order == 'asc' ? 'top' : 'bottom'"
|
||||
:size="14"
|
||||
/>
|
||||
<!-- 更多操作图标 -->
|
||||
<SvgIcon
|
||||
v-if="columnActionVisible[column.key] || showColumnActions[column.key]"
|
||||
name="MoreFilled"
|
||||
:size="14"
|
||||
:color="'var(--el-color-primary)'"
|
||||
class="column-more-icon"
|
||||
:class="{ 'column-more-icon-visible': columnActionVisible[column.key] || showColumnActions[column.key] }"
|
||||
/>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-if="showColumnActionSort" command="sort-asc">
|
||||
<SvgIcon name="top" class="mr-1" />
|
||||
{{ $t('db.asc') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="showColumnActionSort" command="sort-desc">
|
||||
<SvgIcon name="bottom" class="mr-1" />
|
||||
{{ $t('db.desc') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="showColumnActionFixed && !column.fixed" command="fix">
|
||||
<SvgIcon name="Paperclip" class="mr-1" />
|
||||
{{ $t('db.fixed') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="showColumnActionFixed && column.fixed" command="unfix">
|
||||
<SvgIcon name="Minus" class="mr-1" />
|
||||
{{ $t('db.cancelFiexd') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="containerRef" class="db-table-data" :style="height ? { height } : {}">
|
||||
<el-table-v2
|
||||
ref="tableRef"
|
||||
:header-height="showColumnTip && dbConfig.showColumnComment ? 48 : 30"
|
||||
:row-height="30"
|
||||
:row-class="rowClass"
|
||||
:row-key="null"
|
||||
:columns="state.columns"
|
||||
:data="datas"
|
||||
:width="state.containerWidth"
|
||||
:height="state.containerHeight"
|
||||
fixed
|
||||
class="table"
|
||||
:row-event-handlers="rowEventHandlers"
|
||||
@scroll="onTableScroll"
|
||||
>
|
||||
<template #header="{ columns }">
|
||||
<div v-for="(column, i) in columns" :key="i">
|
||||
<div
|
||||
:style="{
|
||||
width: `${column.width}px`,
|
||||
height: '100%',
|
||||
textAlign: 'center',
|
||||
borderRight: 'var(--el-table-border)',
|
||||
borderTop: 'var(--el-table-border)',
|
||||
}"
|
||||
>
|
||||
<!-- 行号列 -->
|
||||
<div v-if="column.key == rowNoColumn.key" class="header-column-title">
|
||||
<b class="el-text" tag="b"> {{ column.title }} </b>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell="{ rowData, column, rowIndex, columnIndex }">
|
||||
<div @contextmenu="dataContextmenuClick($event, rowIndex, column, rowData)" class="table-data-cell">
|
||||
<!-- 行号列 -->
|
||||
<div v-if="column.key == rowNoColumn.key">
|
||||
<b class="el-text el-text--small">
|
||||
{{ (pageNum - 1) * pageSize + rowIndex + 1 }}
|
||||
</b>
|
||||
<!-- 字段名列 -->
|
||||
<div v-else style="position: relative" @mouseenter="showColumnAction(column)" @mouseleave="hideColumnAction">
|
||||
<!-- 字段列的数据类型 -->
|
||||
<div class="column-type">
|
||||
<span v-if="column.dataTypeSubscript === 'icon-clock'">
|
||||
<SvgIcon :size="9" name="Clock" style="cursor: unset" />
|
||||
</span>
|
||||
<span class="text-[8px]!" v-else>{{ column.dataTypeSubscript }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 数据列 -->
|
||||
<div v-else @dblclick="onEnterEditMode(rowData, column, rowIndex, columnIndex)">
|
||||
<div v-if="canEdit(rowIndex, columnIndex)">
|
||||
<ColumnFormItem
|
||||
v-model="rowData[column.key!]"
|
||||
:data-type="column.dataType"
|
||||
@blur="onExitEditMode(rowData, column, rowIndex)"
|
||||
:column-name="column.columnName"
|
||||
focus
|
||||
/>
|
||||
<div v-if="showColumnTip">
|
||||
<div class="header-column-title">
|
||||
<b :title="column.remark" class="el-text cursor-pointer">
|
||||
{{ column.title }}
|
||||
</b>
|
||||
</div>
|
||||
|
||||
<div v-else :class="isUpdated(rowIndex, column.key) ? 'update_field_active ml-0.5 mr-0.5' : 'ml-0.5 mr-0.5'">
|
||||
<span v-if="rowData[column.key!] === null" style="color: var(--el-color-info-light-5)"> NULL </span>
|
||||
<!-- 字段备注信息 -->
|
||||
<div
|
||||
v-if="dbConfig.showColumnComment"
|
||||
style="color: var(--el-color-info-light-3)"
|
||||
class="text-[10px]! el-text el-text--small is-truncated"
|
||||
>
|
||||
{{ column.columnComment }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span v-else :title="rowData[column.key!]" class="el-text el-text--small is-truncated">
|
||||
{{ rowData[column.key!] }}
|
||||
<div v-else class="header-column-title">
|
||||
<b class="el-text"> {{ column.title }} </b>
|
||||
</div>
|
||||
|
||||
<!-- 字段列右部分内容 -->
|
||||
<div class="column-right">
|
||||
<el-dropdown
|
||||
@command="handleColumnCommand(column, $event)"
|
||||
@visibleChange="onColumnActionVisibleChange(column, $event)"
|
||||
trigger="click"
|
||||
v-if="column.key !== rowNoColumn.key"
|
||||
size="small"
|
||||
placement="bottom-start"
|
||||
>
|
||||
<span class="column-actions-trigger">
|
||||
<!-- 排序箭头图标 -->
|
||||
<SvgIcon
|
||||
v-if="
|
||||
column.key == nowSortColumn?.key && !showColumnActions[column.key] && !columnActionVisible[column.key]
|
||||
"
|
||||
:color="'var(--el-color-primary)'"
|
||||
:name="nowSortColumn?.order == 'asc' ? 'top' : 'bottom'"
|
||||
:size="14"
|
||||
/>
|
||||
<!-- 更多操作图标 -->
|
||||
<SvgIcon
|
||||
v-if="columnActionVisible[column.key] || showColumnActions[column.key]"
|
||||
name="MoreFilled"
|
||||
:size="14"
|
||||
:color="'var(--el-color-primary)'"
|
||||
class="column-more-icon"
|
||||
:class="{ 'column-more-icon-visible': columnActionVisible[column.key] || showColumnActions[column.key] }"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-if="showColumnActionSort" command="sort-asc">
|
||||
<SvgIcon name="top" class="mr-1" />
|
||||
{{ $t('db.asc') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="showColumnActionSort" command="sort-desc">
|
||||
<SvgIcon name="bottom" class="mr-1" />
|
||||
{{ $t('db.desc') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="showColumnActionFixed && !column.fixed" command="fix">
|
||||
<SvgIcon name="Paperclip" class="mr-1" />
|
||||
{{ $t('db.fixed') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="showColumnActionFixed && column.fixed" command="unfix">
|
||||
<SvgIcon name="Minus" class="mr-1" />
|
||||
{{ $t('db.cancelFiexd') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="state.loading" #overlay>
|
||||
<div class="el-loading-mask flex flex-col items-center justify-center">
|
||||
<div>
|
||||
<SvgIcon class="is-loading" name="loading" color="var(--el-color-primary)" :size="28" />
|
||||
<el-text class="ml-1" tag="b">{{ $t('db.execTime') }} - {{ state.execTime.toFixed(1) }}s</el-text>
|
||||
</div>
|
||||
<div v-if="loading && abortFn" class="mt-2!">
|
||||
<el-button @click="cancelLoading" type="info" size="small" plain>{{ $t('common.cancel') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #empty>
|
||||
<el-empty class="text-center" :description="props.emptyText" :image-size="60" />
|
||||
</template>
|
||||
</el-table-v2>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-auto-resizer>
|
||||
|
||||
<template #cell="{ rowData, column, rowIndex, columnIndex }">
|
||||
<div @contextmenu="dataContextmenuClick($event, rowIndex, column, rowData)" class="table-data-cell">
|
||||
<!-- 行号列 -->
|
||||
<div v-if="column.key == rowNoColumn.key">
|
||||
<b class="el-text el-text--small">
|
||||
{{ (pageNum - 1) * pageSize + rowIndex + 1 }}
|
||||
</b>
|
||||
</div>
|
||||
|
||||
<!-- 数据列 -->
|
||||
<div v-else @dblclick="onEnterEditMode(rowData, column, rowIndex, columnIndex)">
|
||||
<div v-if="canEdit(rowIndex, columnIndex)">
|
||||
<ColumnFormItem
|
||||
v-model="rowData[column.key!]"
|
||||
:data-type="column.dataType"
|
||||
@blur="onExitEditMode(rowData, column, rowIndex)"
|
||||
:column-name="column.columnName"
|
||||
focus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else :class="isUpdated(rowIndex, column.key) ? 'update_field_active ml-0.5 mr-0.5' : 'ml-0.5 mr-0.5'">
|
||||
<span v-if="rowData[column.key!] === null" style="color: var(--el-color-info-light-5)"> NULL </span>
|
||||
|
||||
<span v-else :title="rowData[column.key!]" class="el-text el-text--small is-truncated">
|
||||
{{ rowData[column.key!] }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="state.loading" #overlay>
|
||||
<div class="el-loading-mask flex flex-col items-center justify-center">
|
||||
<div>
|
||||
<SvgIcon class="is-loading" name="loading" color="var(--el-color-primary)" :size="28" />
|
||||
<el-text class="ml-1" tag="b">{{ $t('db.execTime') }} - {{ state.execTime.toFixed(1) }}s</el-text>
|
||||
</div>
|
||||
<div v-if="loading && abortFn" class="mt-2!">
|
||||
<el-button @click="cancelLoading" type="info" size="small" plain>{{ $t('common.cancel') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #empty>
|
||||
<el-empty class="text-center" :description="props.emptyText" :image-size="60" />
|
||||
</template>
|
||||
</el-table-v2>
|
||||
|
||||
<el-dialog @close="state.genTxtDialog.visible = false" v-model="state.genTxtDialog.visible" :title="state.genTxtDialog.title" width="1000px">
|
||||
<template #header>
|
||||
@@ -208,7 +204,7 @@ import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { DbInst, DbThemeConfig } from '@/views/ops/db/db';
|
||||
import { useIntervalFn, useStorage } from '@vueuse/core';
|
||||
import { ElInput } from 'element-plus';
|
||||
import { Ref, computed, onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue';
|
||||
import { Ref, computed, onBeforeUnmount, onMounted, reactive, ref, toRefs, useTemplateRef, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Msg } from '../../../../../hooks/useI18n';
|
||||
import { ColumnTypeSubscript, DataType, DbDialect, getDbDialect } from '../../dialect/index';
|
||||
@@ -259,7 +255,7 @@ const props = defineProps({
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '600px',
|
||||
default: '',
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
@@ -273,6 +269,8 @@ const props = defineProps({
|
||||
|
||||
const contextmenuRef = ref();
|
||||
const tableRef = ref();
|
||||
const containerRef = useTemplateRef<HTMLElement>('containerRef');
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
// 用于控制列操作按钮的显示
|
||||
const showColumnActions = ref({} as any);
|
||||
@@ -370,6 +368,9 @@ let nowUpdateCell: Ref<NowUpdateCell> = ref(null) as any;
|
||||
// 选中的数据, key->rowIndex value->primaryKeyValue
|
||||
const selectionRowsMap = ref(new Map<number, any>());
|
||||
|
||||
// 最后一次点击的行索引,用于 shift 批量选择
|
||||
let lastSelectedRowIndex: number | null = null;
|
||||
|
||||
// 更新单元格 key-> rowIndex value -> 更新行
|
||||
const cellUpdateMap = ref(new Map<number, UpdatedRow>());
|
||||
|
||||
@@ -387,6 +388,8 @@ const state = reactive({
|
||||
columns: [] as any,
|
||||
loading: false,
|
||||
tableHeight: '600px',
|
||||
containerHeight: 600,
|
||||
containerWidth: 800,
|
||||
execTime: 0,
|
||||
contextmenu: {
|
||||
dropdown: {
|
||||
@@ -407,7 +410,7 @@ const state = reactive({
|
||||
},
|
||||
});
|
||||
|
||||
const { tableHeight, datas } = toRefs(state);
|
||||
const { containerHeight, containerWidth, datas } = toRefs(state);
|
||||
|
||||
const dbConfig = useStorage('dbConfig', DbThemeConfig);
|
||||
|
||||
@@ -487,6 +490,22 @@ onMounted(async () => {
|
||||
state.tableHeight = props.height;
|
||||
state.loading = props.loading;
|
||||
|
||||
// 使用 ResizeObserver 自动测量容器尺寸,确保 el-table-v2 固定表头 + body滚动
|
||||
if (containerRef.value) {
|
||||
const rect = containerRef.value.getBoundingClientRect();
|
||||
state.containerHeight = rect.height || 600;
|
||||
state.containerWidth = rect.width || 800;
|
||||
|
||||
resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const { height, width } = entry.contentRect;
|
||||
if (height > 0) state.containerHeight = height;
|
||||
if (width > 0) state.containerWidth = width;
|
||||
}
|
||||
});
|
||||
resizeObserver.observe(containerRef.value);
|
||||
}
|
||||
|
||||
state.dbId = props.dbId;
|
||||
state.dbType = getNowDbInst().type;
|
||||
dbDialect = getDbDialect(state.dbType);
|
||||
@@ -502,12 +521,14 @@ onMounted(async () => {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
endLoading();
|
||||
resizeObserver?.disconnect();
|
||||
});
|
||||
|
||||
const setTableData = (datas: any) => {
|
||||
tableRef.value?.scrollTo({ scrollLeft: 0, scrollTop: 0 });
|
||||
selectionRowsMap.value.clear();
|
||||
cellUpdateMap.value.clear();
|
||||
lastSelectedRowIndex = null;
|
||||
// formatDataValues(datas);
|
||||
state.datas = datas;
|
||||
setTableColumns(props.columns);
|
||||
@@ -633,7 +654,7 @@ const isSelection = (rowIndex: number): boolean => {
|
||||
*/
|
||||
const selectionRow = (rowIndex: number, rowData: any, isMultiple = false) => {
|
||||
if (isMultiple) {
|
||||
// 如果重复点击,则取消改选中数据
|
||||
// 如果重复点击,则取消该选中数据
|
||||
if (selectionRowsMap.value.get(rowIndex)) {
|
||||
selectionRowsMap.value.delete(rowIndex);
|
||||
return;
|
||||
@@ -642,6 +663,21 @@ const selectionRow = (rowIndex: number, rowData: any, isMultiple = false) => {
|
||||
selectionRowsMap.value.clear();
|
||||
}
|
||||
selectionRowsMap.value.set(rowIndex, rowData);
|
||||
lastSelectedRowIndex = rowIndex;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shift 批量选择:选中起始行到当前行之间的所有行
|
||||
*/
|
||||
const selectionRowRange = (startIndex: number, endIndex: number) => {
|
||||
const from = Math.min(startIndex, endIndex);
|
||||
const to = Math.max(startIndex, endIndex);
|
||||
for (let i = from; i <= to; i++) {
|
||||
const rowData = state.datas[i];
|
||||
if (rowData) {
|
||||
selectionRowsMap.value.set(i, rowData);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -652,11 +688,17 @@ const rowEventHandlers = {
|
||||
const event = e.event;
|
||||
const rowIndex = e.rowIndex;
|
||||
const rowData = e.rowData;
|
||||
// 按住ctrl点击,则新建标签页打开, metaKey对应mac command键
|
||||
// 按住ctrl/meta点击,则多选切换
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
selectionRow(rowIndex, rowData, true);
|
||||
return;
|
||||
}
|
||||
// 按住shift点击,则批量选择起始行到当前行
|
||||
if (event.shiftKey && lastSelectedRowIndex !== null) {
|
||||
selectionRowsMap.value.clear();
|
||||
selectionRowRange(lastSelectedRowIndex, rowIndex);
|
||||
return;
|
||||
}
|
||||
selectionRow(rowIndex, rowData);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row>
|
||||
<div class="h-full flex flex-col gap-1">
|
||||
<el-row class="flex-shrink-0">
|
||||
<el-col :span="8">
|
||||
<div class="mt-1">
|
||||
<el-link :disabled="state.loading" @click="onRefresh()" icon="refresh" underline="never" class="ml-1"> </el-link>
|
||||
@@ -12,6 +12,7 @@
|
||||
width="auto"
|
||||
:title="$t('db.tableFieldConf')"
|
||||
trigger="click"
|
||||
:teleported="false"
|
||||
@hide="triggerCheckedColumns"
|
||||
>
|
||||
<div><el-input v-model="checkedShowColumns.searchKey" size="small" :placeholder="$t('db.columnFilterPlaceholder')" /></div>
|
||||
@@ -45,16 +46,16 @@
|
||||
<el-link @click="onShowAddDataDialog()" type="primary" icon="plus" underline="never"></el-link>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip :show-after="500" effect="dark" content="commit" placement="top">
|
||||
<el-tooltip :show-after="500" effect="dark" content="commit" placement="top" :teleported="false">
|
||||
<el-link @click="onCommit()" type="success" icon="CircleCheck" underline="never"> </el-link>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip :show-after="500" v-if="hasUpdatedFileds" :content="$t('db.submitUpdate')" placement="top">
|
||||
<el-tooltip :show-after="500" v-if="hasUpdatedFileds" :content="$t('db.submitUpdate')" placement="top" :teleported="false">
|
||||
<el-link @click="submitUpdateFields()" type="success" underline="never" class="!text-[12px]">{{ $t('common.submit') }}</el-link>
|
||||
</el-tooltip>
|
||||
<el-divider v-if="hasUpdatedFileds" direction="vertical" border-style="dashed" />
|
||||
<el-tooltip :show-after="500" v-if="hasUpdatedFileds" :content="$t('db.cancelUpdate')" placement="top">
|
||||
<el-tooltip :show-after="500" v-if="hasUpdatedFileds" :content="$t('db.cancelUpdate')" placement="top" :teleported="false">
|
||||
<el-link @click="cancelUpdateFields" type="warning" underline="never" class="!text-[12px]">{{ $t('common.cancel') }}</el-link>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
@@ -74,6 +75,7 @@
|
||||
highlight-first-item
|
||||
value-key="columnName"
|
||||
ref="condInputRef"
|
||||
:teleported="false"
|
||||
>
|
||||
<template #suffix>
|
||||
<SvgIcon @click="onSelectByCondition" name="search" />
|
||||
@@ -95,7 +97,7 @@
|
||||
</template>
|
||||
|
||||
<template #prepend>
|
||||
<el-popover :visible="state.condPopVisible" trigger="click" :width="320" placement="right">
|
||||
<el-popover :visible="state.condPopVisible" trigger="click" :width="320" placement="right" :teleported="false">
|
||||
<template #reference>
|
||||
<el-button @click.stop="chooseCondColumnName" style="color: var(--el-color-success)" text size="small">
|
||||
{{ $t('db.selectColumn') }}
|
||||
@@ -133,13 +135,13 @@
|
||||
|
||||
<db-table-data
|
||||
ref="dbTableRef"
|
||||
class="flex-1 min-h-0 overflow-hidden"
|
||||
:db-id="dbId"
|
||||
:db="dbName"
|
||||
:data="datas"
|
||||
:table="tableName"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:height="tableHeight"
|
||||
:page-size="pageSize"
|
||||
:page-num="pageNum"
|
||||
:show-column-tip="true"
|
||||
@@ -149,7 +151,7 @@
|
||||
@data-delete="onRefresh"
|
||||
></db-table-data>
|
||||
|
||||
<el-row type="flex" class="mt-2" :gutter="10" justify="space-between" style="user-select: none">
|
||||
<el-row type="flex" class="flex-shrink-0" :gutter="10" justify="space-between" style="user-select: none">
|
||||
<el-col :span="12">
|
||||
<el-text
|
||||
id="copyValue"
|
||||
@@ -185,7 +187,7 @@
|
||||
<el-link class="op-page" underline="never" @click="++pageNum" :disabled="datas.length < pageSize" icon="Right" />
|
||||
<el-link class="op-page" underline="never" @click="handleEndPage" :disabled="datas.length < pageSize" icon="DArrowRight" />
|
||||
<div style="width: 90px" class="op-page ml-2">
|
||||
<el-select size="small" :default-first-option="true" v-model="pageSize" @change="handleSizeChange">
|
||||
<el-select size="small" :default-first-option="true" v-model="pageSize" @change="handleSizeChange" :teleported="false">
|
||||
<el-option
|
||||
style="font-size: 12px; height: 24px; line-height: 24px"
|
||||
v-for="(op, i) in pageSizes"
|
||||
@@ -206,7 +208,7 @@
|
||||
<el-dialog v-model="conditionDialog.visible" :title="conditionDialog.title" width="500px">
|
||||
<el-row gutter="5">
|
||||
<el-col :span="5">
|
||||
<el-select v-model="conditionDialog.condition">
|
||||
<el-select v-model="conditionDialog.condition" :teleported="false">
|
||||
<el-option label="=" value="="> </el-option>
|
||||
<el-option label="LIKE" value="LIKE"> </el-option>
|
||||
<el-option label=">" value=">"> </el-option>
|
||||
@@ -273,10 +275,6 @@ const props = defineProps({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
tableHeight: {
|
||||
type: [String],
|
||||
default: '600px',
|
||||
},
|
||||
});
|
||||
|
||||
const dbTableRef: Ref = ref(null);
|
||||
@@ -325,7 +323,6 @@ const state = reactive({
|
||||
title: '',
|
||||
visible: false,
|
||||
},
|
||||
tableHeight: '600px',
|
||||
hasUpdatedFileds: false,
|
||||
dbDialect: {} as DbDialect,
|
||||
|
||||
@@ -340,20 +337,12 @@ const state = reactive({
|
||||
const { datas, condition, loading, columns, checkedShowColumns, pageNum, pageSize, pageSizes, sql, hasUpdatedFileds, conditionDialog, addDataDialog } =
|
||||
toRefs(state);
|
||||
|
||||
watch(
|
||||
() => props.tableHeight,
|
||||
(newValue: any) => {
|
||||
state.tableHeight = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
const getNowDbInst = () => {
|
||||
return DbInst.getInst(props.dbId);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
console.log('in table data mounted');
|
||||
state.tableHeight = props.tableHeight;
|
||||
await onRefresh();
|
||||
|
||||
state.dbDialect = getNowDbInst().getDialect();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="75%">
|
||||
<el-drawer :append-to-body="false" :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="75%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="db-table">
|
||||
<div class="db-table flex flex-col gap-1">
|
||||
<el-row class="mb-1">
|
||||
<el-popover v-model:visible="state.dumpInfo.visible" trigger="click" :width="470" placement="right">
|
||||
<el-popover v-model:visible="state.dumpInfo.visible" trigger="click" :width="470" placement="right" :teleported="false">
|
||||
<template #reference>
|
||||
<el-button v-auth="'db:data:export'" :disabled="state.dumpInfo.tables?.length == 0" class="ml-1" type="success" size="small">
|
||||
{{ $t('db.dump') }}
|
||||
@@ -353,4 +353,11 @@ const onSubmitSql = async (row: { tableName: string }) => {
|
||||
await getTables();
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
<style lang="scss">
|
||||
.db-table {
|
||||
> .el-table {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -404,7 +404,7 @@ export class DbInst {
|
||||
* @param inst 数据库实例,后端返回的列表接口中的信息
|
||||
* @returns DbInst
|
||||
*/
|
||||
static async getOrNewInst(inst: any) {
|
||||
static getOrNewInst(inst: any) {
|
||||
if (!inst) {
|
||||
throw new Error('inst不能为空');
|
||||
}
|
||||
@@ -427,7 +427,9 @@ export class DbInst {
|
||||
dbInst.databases = inst.databases;
|
||||
|
||||
if (dbInst.databases?.[0]) {
|
||||
dbInst.version = await dbApi.getCompatibleDbVersion.request({ id: inst.id, db: dbInst.databases?.[0] });
|
||||
dbApi.getCompatibleDbVersion.request({ id: inst.id, db: dbInst.databases?.[0] }).then((version) => {
|
||||
dbInst.version = version;
|
||||
});
|
||||
}
|
||||
|
||||
dbInstCache.set(dbInst.id, dbInst);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="db-sql-exec h-full">
|
||||
<div class="db-sql-exec h-full flex flex-col">
|
||||
<el-row>
|
||||
<el-col :span="24" v-if="state.db">
|
||||
<el-descriptions :column="4" size="small" border>
|
||||
@@ -27,6 +27,7 @@
|
||||
width="auto"
|
||||
:title="$t('db.dbShowSetting')"
|
||||
trigger="click"
|
||||
:teleported="false"
|
||||
>
|
||||
<el-row>
|
||||
<el-checkbox
|
||||
@@ -79,18 +80,18 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div id="data-exec" class="mt-1">
|
||||
<div id="data-exec" ref="dataExecRef" class="mt-1 flex-1 min-h-0 overflow-visible">
|
||||
<el-tabs
|
||||
v-if="state.tabs.size > 0"
|
||||
type="card"
|
||||
@tab-remove="onRemoveTab"
|
||||
@tab-change="onTabChange"
|
||||
v-model="state.activeName"
|
||||
class="h-full! w-full"
|
||||
class="db-data-tabs w-full"
|
||||
>
|
||||
<el-tab-pane class="h-full!" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
|
||||
<template #label>
|
||||
<el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
|
||||
<el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250" :teleported="false">
|
||||
<template #reference>
|
||||
<span @contextmenu.prevent="onTabContextmenu(dt, $event)" class="text-[12px]!">{{ dt.label }}</span>
|
||||
</template>
|
||||
@@ -119,7 +120,6 @@
|
||||
:db-id="dt.dbId"
|
||||
:db-name="dt.db"
|
||||
:table-name="dt.params.table"
|
||||
:table-height="state.dataTabsTableHeight"
|
||||
:ref="(el: any) => (dt.componentRef = el)"
|
||||
></db-table-data-op>
|
||||
|
||||
@@ -170,14 +170,12 @@ import { dispposeCompletionItemProvider } from '@/components/monaco/completionIt
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { Msg, useI18nCreateTitle, useI18nDeleteConfirm, useI18nEditTitle } from '@/hooks/useI18n';
|
||||
import { ResourceOpCtx } from '@/views/ops/component/tag';
|
||||
import SqlExecBox from '@/views/ops/db/component/sqleditor/SqlExecBox';
|
||||
import { DbDataOpComp } from '@/views/ops/db/resource';
|
||||
import { ResourceOpCtxKey } from '@/views/ops/resource/resource';
|
||||
import { ResourceOpCtx, ResourceOpCtxKey } from '@/views/ops/resource/resourceOp';
|
||||
import { useEventListener, useStorage } from '@vueuse/core';
|
||||
import { ElCheckbox, ElMessageBox } from 'element-plus';
|
||||
import { format as sqlFormatter } from 'sql-formatter';
|
||||
import { defineAsyncComponent, getCurrentInstance, h, inject, onBeforeUnmount, onMounted, reactive, ref, toRefs, useTemplateRef } from 'vue';
|
||||
import { defineAsyncComponent, h, inject, onBeforeUnmount, onMounted, reactive, ref, toRefs, useTemplateRef } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { dbApi } from '../api';
|
||||
import { DbInst, DbThemeConfig, registerDbCompletionItemProvider, TabInfo, TabType } from '../db';
|
||||
@@ -192,6 +190,11 @@ const { t } = useI18n();
|
||||
|
||||
const resourceOpCtx: ResourceOpCtx | undefined = inject(ResourceOpCtxKey);
|
||||
|
||||
const props = defineProps<{
|
||||
dbInfo: any;
|
||||
db: string;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits(['init']);
|
||||
|
||||
const tabContextmenuRef: any = useTemplateRef('tabContextmenuRef');
|
||||
@@ -227,7 +230,6 @@ const state = reactive({
|
||||
dropdown: { x: 0, y: 0 },
|
||||
items: tabContextmenuItems,
|
||||
},
|
||||
dataTabsTableHeight: '600px',
|
||||
tablesOpHeight: '600',
|
||||
dbServerInfo: {
|
||||
loading: true,
|
||||
@@ -256,28 +258,35 @@ const { nowDbInst, tableCreateDialog } = toRefs(state);
|
||||
const dbConfig = useStorage('dbConfig', DbThemeConfig);
|
||||
|
||||
onMounted(() => {
|
||||
changeDb(props.dbInfo, props.db);
|
||||
state.reloadStatus = !dbConfig.value.cacheTable;
|
||||
emits('init', { name: DbDataOpComp.name, ref: getCurrentInstance()?.exposed });
|
||||
setHeight();
|
||||
// 监听浏览器窗口大小变化,更新对应组件高度
|
||||
useEventListener(window, 'resize', setHeight);
|
||||
});
|
||||
|
||||
const dataExecRef = useTemplateRef<HTMLElement>('dataExecRef');
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
dispposeCompletionItemProvider('sql');
|
||||
});
|
||||
|
||||
/**
|
||||
* 设置editor高度和数据表高度
|
||||
* 设置editor高度和数据表高度(基于容器位置计算,兼容全屏模式)
|
||||
*/
|
||||
const setHeight = () => {
|
||||
state.dataTabsTableHeight = window.innerHeight - 253 + 'px';
|
||||
state.tablesOpHeight = window.innerHeight - 225 + 'px';
|
||||
const el = document.getElementById('data-exec');
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
state.tablesOpHeight = window.innerHeight - rect.top - 60 + 'px';
|
||||
} else {
|
||||
state.tablesOpHeight = window.innerHeight - 225 + 'px';
|
||||
}
|
||||
};
|
||||
|
||||
// 选择数据库,改变当前正在操作的数据库信息
|
||||
const changeDb = async (db: any, dbName: string) => {
|
||||
state.nowDbInst = await DbInst.getOrNewInst(db);
|
||||
const changeDb = (db: any, dbName: string) => {
|
||||
state.nowDbInst = DbInst.getOrNewInst(db);
|
||||
state.nowDbInst.databases = db.databases;
|
||||
state.db = dbName;
|
||||
};
|
||||
@@ -287,7 +296,7 @@ const loadTableData = async (db: any, dbName: string, tableName: string) => {
|
||||
if (tableName == '') {
|
||||
return;
|
||||
}
|
||||
await changeDb(db, dbName);
|
||||
changeDb(db, dbName);
|
||||
|
||||
const key = `tableData:${db.id}.${dbName}.${tableName}`;
|
||||
let tab = state.tabs.get(key);
|
||||
@@ -316,7 +325,7 @@ const addQueryTab = async (db: any, dbName: string, sqlName: string = '') => {
|
||||
Msg.warning('db.noDbInstMsg');
|
||||
return;
|
||||
}
|
||||
await changeDb(db, dbName);
|
||||
changeDb(db, dbName);
|
||||
|
||||
const dbId = db.id;
|
||||
let label;
|
||||
@@ -367,7 +376,7 @@ const addTablesOpTab = async (db: any) => {
|
||||
Msg.warning('db.noDbInstMsg');
|
||||
return;
|
||||
}
|
||||
await changeDb(db, dbName);
|
||||
changeDb(db, dbName);
|
||||
|
||||
const dbId = db.id;
|
||||
let key = `tablesOp:${dbId}.${dbName}`;
|
||||
@@ -642,7 +651,12 @@ const loadTables = async (dbInfo: any) => {
|
||||
return tables;
|
||||
};
|
||||
|
||||
const onRefresh = () => {
|
||||
state.tabs.clear();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
onRefresh,
|
||||
onChangeDb: changeDb,
|
||||
loadTables,
|
||||
loadTableData,
|
||||
@@ -663,15 +677,29 @@ defineExpose({
|
||||
<style lang="scss" scoped>
|
||||
.db-sql-exec {
|
||||
#data-exec {
|
||||
::v-deep(.el-tabs) {
|
||||
::v-deep(.db-data-tabs) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
--el-tabs-header-height: 30px;
|
||||
}
|
||||
|
||||
::v-deep(.el-tabs__header) {
|
||||
margin: 0 0 5px;
|
||||
> .el-tabs__header {
|
||||
margin: 0 0 5px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.el-tabs__item {
|
||||
padding: 0 5px;
|
||||
.el-tabs__item {
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
> .el-tabs__content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.el-tab-pane {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { ContextmenuItem } from '@/components/contextmenu';
|
||||
|
||||
import { NodeType, TagTreeNode, ResourceConfig } from '../../component/tag';
|
||||
import { ResourceTypeEnum, TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { dbApi } from '../api';
|
||||
import { sleep } from '@/common/utils/loading';
|
||||
import { DbInst } from '../db';
|
||||
import { schemaDbTypes } from '../dialect/index';
|
||||
import { i18n } from '@/i18n';
|
||||
import { formatByteSize } from '@/common/utils/format';
|
||||
import { sleep } from '@/common/utils/loading';
|
||||
import { i18n } from '@/i18n';
|
||||
import type { ResourceConfig } from '@/views/ops/resource/resource';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { NodeType, TagTreeNode } from '../../component/tag';
|
||||
import { createResourceOpTab } from '../../resource/resourceOp';
|
||||
import { dbApi } from '../api';
|
||||
import { DbInst } from '../db';
|
||||
import { getDbDialect, schemaDbTypes } from '../dialect/index';
|
||||
|
||||
const DbInstList = defineAsyncComponent(() => import('../InstanceList.vue'));
|
||||
const DbDataOp = defineAsyncComponent(() => import('./DbDataOp.vue'));
|
||||
@@ -37,19 +39,14 @@ const SqlIcon = {
|
||||
color: '#f56c6c',
|
||||
};
|
||||
|
||||
export const DbDataOpComp = {
|
||||
name: 'tag.dbDataOp',
|
||||
component: DbDataOp,
|
||||
icon: DbIcon,
|
||||
};
|
||||
|
||||
// node节点点击时,触发改变db事件
|
||||
const nodeClickChangeDb = async (nodeData: TagTreeNode) => {
|
||||
const params = nodeData.params;
|
||||
if (params.db) {
|
||||
const compRef = await nodeData.ctx?.addResourceComponent(DbDataOpComp);
|
||||
compRef.onChangeDb(
|
||||
{
|
||||
const getDbOpTab = async (params: any) => {
|
||||
const tabKey = `${params.instCode}.${params.dbCode}.${params.db}`;
|
||||
return await createResourceOpTab({
|
||||
key: tabKey,
|
||||
name: `${params.name}/${params.db}`,
|
||||
component: DbDataOp,
|
||||
componentProps: {
|
||||
dbInfo: {
|
||||
id: params.id,
|
||||
host: `${params.host}`,
|
||||
name: params.name,
|
||||
@@ -57,18 +54,24 @@ const nodeClickChangeDb = async (nodeData: TagTreeNode) => {
|
||||
tagPath: params.tagPath,
|
||||
databases: params.dbs,
|
||||
},
|
||||
params.db
|
||||
);
|
||||
}
|
||||
db: params.db,
|
||||
},
|
||||
tabComponentProps: {
|
||||
icon: { name: getDbDialect(params.type)?.getInfo().icon },
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const getDbOpTabCompInst = async (params: any) => {
|
||||
return (await getDbOpTab(params)).componentInstance;
|
||||
};
|
||||
|
||||
const ContextmenuItemRefresh = new ContextmenuItem('refresh', 'common.refresh')
|
||||
.withIcon('RefreshRight')
|
||||
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).reloadNode(node.key));
|
||||
.withOnClick(async (node: TagTreeNode) => (await getDbOpTabCompInst(node.params))?.reloadNode(node.key));
|
||||
|
||||
// 数据库实例节点类型
|
||||
export const NodeTypeDbInst = new NodeType(TagResourceTypeEnum.DbInstance.value).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
parentNode.ctx?.addResourceComponent(DbDataOpComp);
|
||||
const tagPath = parentNode.params.tagPath;
|
||||
|
||||
const dbInstancesRes = await dbApi.instances.request({ tagPath, pageSize: 100 });
|
||||
@@ -110,7 +113,7 @@ export const NodeTypeDbConf = new NodeType(TagResourceTypeEnum.Db.value)
|
||||
x.username = authCerts[x.authCertName]?.username;
|
||||
x.instCode = params.instCode;
|
||||
x.dbCode = x.code;
|
||||
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeDbs).withParams(x).withIcon(DbIcon).withNodeComponent(NodeDb);
|
||||
return TagTreeNode.new(parentNode, `${parentNode.key}.${x.code}`, x.name, NodeTypeDbs).withParams(x).withIcon(DbIcon).withNodeComponent(NodeDb);
|
||||
});
|
||||
})
|
||||
.withContextMenuItems([ContextmenuItemRefresh]);
|
||||
@@ -141,37 +144,32 @@ export const NodeTypeDbs = new NodeType(222).withLoadNodesFunc(async (parentNode
|
||||
});
|
||||
|
||||
// 数据库节点
|
||||
export const NodeTypeDb = new NodeType(223)
|
||||
.withContextMenuItems([ContextmenuItemRefresh])
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
params.parentKey = parentNode.key;
|
||||
// pg类数据库会多一层schema
|
||||
if (schemaDbTypes.includes(params.type)) {
|
||||
const { id, db } = params;
|
||||
const schemaNames = await dbApi.pgSchemas.request({ id, db });
|
||||
return schemaNames.map((sn: any) => {
|
||||
// 将db变更为 db/schema;
|
||||
const nParams = { ...params };
|
||||
nParams.schema = sn;
|
||||
nParams.db = nParams.db + '/' + sn;
|
||||
nParams.dbs = schemaNames;
|
||||
return TagTreeNode.new(parentNode, `${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresSchema)
|
||||
.withParams(nParams)
|
||||
.withIcon(SchemaIcon);
|
||||
});
|
||||
}
|
||||
export const NodeTypeDb = new NodeType(223).withContextMenuItems([ContextmenuItemRefresh]).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
params.parentKey = parentNode.key;
|
||||
// pg类数据库会多一层schema
|
||||
if (schemaDbTypes.includes(params.type)) {
|
||||
const { id, db } = params;
|
||||
const schemaNames = await dbApi.pgSchemas.request({ id, db });
|
||||
return schemaNames.map((sn: any) => {
|
||||
// 将db变更为 db/schema;
|
||||
const nParams = { ...params };
|
||||
nParams.schema = sn;
|
||||
nParams.db = nParams.db + '/' + sn;
|
||||
// nParams.dbs = schemaNames;
|
||||
return TagTreeNode.new(parentNode, `${parentNode.key}/${sn}`, sn, NodeTypePostgresSchema).withParams(nParams).withIcon(SchemaIcon);
|
||||
});
|
||||
}
|
||||
|
||||
return getNodeTypeTables(parentNode);
|
||||
})
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
return getNodeTypeTables(parentNode);
|
||||
});
|
||||
|
||||
export const getNodeTypeTables = (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
let tableKey = `${params.id}.${params.db}.table-menu`;
|
||||
let sqlKey = getSqlMenuNodeKey(params.id, params.db);
|
||||
let tableKey = `${parentNode.key}.table-menu`;
|
||||
let sqlKey = `${parentNode.key}.sql-menu`;
|
||||
return [
|
||||
TagTreeNode.new(parentNode, `${params.id}.${params.db}.table-menu`, i18n.global.t('db.table'), NodeTypeTableMenu)
|
||||
TagTreeNode.new(parentNode, tableKey, i18n.global.t('db.table'), NodeTypeTableMenu)
|
||||
.withParams({
|
||||
...params,
|
||||
key: tableKey,
|
||||
@@ -185,25 +183,23 @@ export const getNodeTypeTables = (parentNode: TagTreeNode) => {
|
||||
};
|
||||
|
||||
// postgres schema模式
|
||||
export const NodeTypePostgresSchema = new NodeType(224)
|
||||
.withContextMenuItems([ContextmenuItemRefresh])
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
params.parentKey = parentNode.key;
|
||||
return getNodeTypeTables(parentNode);
|
||||
})
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
export const NodeTypePostgresSchema = new NodeType(224).withContextMenuItems([ContextmenuItemRefresh]).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
params.parentKey = parentNode.key;
|
||||
return getNodeTypeTables(parentNode);
|
||||
});
|
||||
|
||||
// 数据库表菜单节点
|
||||
const NodeTypeTableMenu = new NodeType(4)
|
||||
.withCollapseRemoveChildren()
|
||||
.withContextMenuItems([
|
||||
ContextmenuItemRefresh,
|
||||
new ContextmenuItem('createTable', 'db.createTable').withIcon('Plus').withOnClick(async (parentNode: TagTreeNode) => {
|
||||
(await parentNode.ctx?.addResourceComponent(DbDataOpComp))?.onEditTable(parentNode);
|
||||
(await getDbOpTabCompInst(parentNode.params))?.onEditTable(parentNode);
|
||||
}),
|
||||
new ContextmenuItem('tablesOp', 'db.tableOp').withIcon('Setting').withOnClick(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
(await parentNode.ctx?.addResourceComponent(DbDataOpComp)).addTablesOpTab({
|
||||
(await getDbOpTabCompInst(params))?.addTablesOpTab({
|
||||
id: params.id,
|
||||
db: params.db,
|
||||
type: params.type,
|
||||
@@ -212,24 +208,19 @@ const NodeTypeTableMenu = new NodeType(4)
|
||||
}),
|
||||
])
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const compRef = await parentNode.ctx?.addResourceComponent(DbDataOpComp);
|
||||
const params = parentNode.params;
|
||||
const compRef = await getDbOpTabCompInst(params);
|
||||
// // 获取当前库的所有表信息
|
||||
const tables = await compRef.loadTables(params);
|
||||
let { id, db, type, schema, version } = params;
|
||||
let dbTableSize = 0;
|
||||
const tablesNode = tables.map((x: any) => {
|
||||
const tableSize = x.dataLength + x.indexLength;
|
||||
dbTableSize += tableSize;
|
||||
const key = `${id}.${db}.${x.tableName}`;
|
||||
const key = `${parentNode.key}.${x.tableName}`;
|
||||
return TagTreeNode.new(parentNode, key, x.tableName, NodeTypeTable)
|
||||
.withIsLeaf(true)
|
||||
.withParams({
|
||||
id,
|
||||
db,
|
||||
type,
|
||||
schema,
|
||||
version,
|
||||
...params,
|
||||
key: key,
|
||||
parentKey: parentNode.key,
|
||||
tableName: x.tableName,
|
||||
@@ -244,72 +235,56 @@ const NodeTypeTableMenu = new NodeType(4)
|
||||
parentNode.params.dbTableSize = dbTableSize == 0 ? '' : formatByteSize(dbTableSize);
|
||||
return tablesNode;
|
||||
});
|
||||
// .withNodeDblclickFunc((node: TagTreeNode) => {
|
||||
// const params = node.params;
|
||||
// addTablesOpTab({ id: params.id, db: params.db, type: params.type, version: params.version, nodeKey: node.key });
|
||||
// });
|
||||
|
||||
// 数据库sql模板菜单节点
|
||||
const NodeTypeSqlMenu = new NodeType(225)
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
const id = params.id;
|
||||
const db = params.db;
|
||||
const dbs = params.dbs;
|
||||
// 加载用户保存的sql脚本
|
||||
const sqls = await dbApi.getSqlNames.request({ id: id, db: db });
|
||||
return sqls.map((x: any) => {
|
||||
return TagTreeNode.new(parentNode, `${id}.${db}.${x.name}`, x.name, NodeTypeSql)
|
||||
.withIsLeaf(true)
|
||||
.withParams({ id, db, dbs, sqlName: x.name })
|
||||
.withIcon(SqlIcon);
|
||||
});
|
||||
})
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
const NodeTypeSqlMenu = new NodeType(225).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
// 加载用户保存的sql脚本
|
||||
const sqls = await dbApi.getSqlNames.request({ id: params.id, db: params.db });
|
||||
return sqls.map((x: any) => {
|
||||
return TagTreeNode.new(parentNode, `${parentNode.key}.${x.name}`, x.name, NodeTypeSql)
|
||||
.withIsLeaf(true)
|
||||
.withParams({ ...params, sqlName: x.name })
|
||||
.withIcon(SqlIcon);
|
||||
});
|
||||
});
|
||||
|
||||
// 表节点类型
|
||||
const NodeTypeTable = new NodeType(226)
|
||||
.withContextMenuItems([
|
||||
new ContextmenuItem('copyTable', 'db.copyTable')
|
||||
.withIcon('copyDocument')
|
||||
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).onCopyTable(node)),
|
||||
.withOnClick(async (node: TagTreeNode) => (await getDbOpTabCompInst(node.params))?.onCopyTable(node)),
|
||||
new ContextmenuItem('renameTable', 'db.renameTable')
|
||||
.withIcon('edit')
|
||||
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).onRenameTable(node)),
|
||||
.withOnClick(async (node: TagTreeNode) => (await getDbOpTabCompInst(node.params))?.onRenameTable(node)),
|
||||
new ContextmenuItem('editTable', 'db.editTable')
|
||||
.withIcon('edit')
|
||||
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).onEditTable(node)),
|
||||
.withOnClick(async (node: TagTreeNode) => (await getDbOpTabCompInst(node.params))?.onEditTable(node)),
|
||||
new ContextmenuItem('delTable', 'db.delTable')
|
||||
.withIcon('Delete')
|
||||
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).onDeleteTable(node)),
|
||||
.withOnClick(async (node: TagTreeNode) => (await getDbOpTabCompInst(node.params))?.onDeleteTable(node)),
|
||||
new ContextmenuItem('ddl', 'DDL')
|
||||
.withIcon('Document')
|
||||
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).onGenDdl(node)),
|
||||
.withOnClick(async (node: TagTreeNode) => (await getDbOpTabCompInst(node.params))?.onGenDdl(node)),
|
||||
])
|
||||
.withNodeClickFunc(async (node: TagTreeNode) => {
|
||||
const params = node.params;
|
||||
(await node.ctx?.addResourceComponent(DbDataOpComp)).loadTableData({ id: params.id, nodeKey: node.key }, params.db, params.tableName);
|
||||
(await getDbOpTabCompInst(node.params))?.loadTableData({ id: params.id, nodeKey: node.key }, params.db, params.tableName);
|
||||
});
|
||||
|
||||
// sql模板节点类型
|
||||
const NodeTypeSql = new NodeType(227)
|
||||
.withNodeClickFunc(async (parentNode: TagTreeNode) => {
|
||||
const compRef = await parentNode.ctx?.addResourceComponent(DbDataOpComp);
|
||||
const params = parentNode.params;
|
||||
compRef.addQueryTab({ id: params.id, nodeKey: parentNode.key, dbs: params.dbs }, params.db, params.sqlName);
|
||||
(await getDbOpTabCompInst(params))?.addQueryTab({ id: params.id, nodeKey: parentNode.key, dbs: params.dbs }, params.db, params.sqlName);
|
||||
})
|
||||
.withContextMenuItems([
|
||||
new ContextmenuItem('delSql', 'common.delete')
|
||||
.withIcon('delete')
|
||||
.withOnClick(async (node: TagTreeNode) =>
|
||||
(await node.ctx?.addResourceComponent(DbDataOpComp)).deleteSql(node.params.id, node.params.db, node.params.sqlName)
|
||||
),
|
||||
.withOnClick(async (node: TagTreeNode) => (await getDbOpTabCompInst(node.params))?.deleteSql(node.params.id, node.params.db, node.params.sqlName)),
|
||||
]);
|
||||
|
||||
const getSqlMenuNodeKey = (dbId: number, db: string) => {
|
||||
return `${dbId}.${db}.sql-menu`;
|
||||
};
|
||||
|
||||
export default {
|
||||
order: 2,
|
||||
resourceType: ResourceTypeEnum.Db.value,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="sync-task-edit">
|
||||
<el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="45%">
|
||||
<el-drawer :append-to-body="false" :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="45%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="db-transfer-edit">
|
||||
<el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="45%">
|
||||
<el-drawer :append-to-body="false" :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="45%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
</template>
|
||||
|
||||
@@ -58,7 +58,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
@@ -67,7 +66,6 @@ import { Msg, useI18nCreateTitle, useI18nDeleteConfirm, useI18nEditTitle } from
|
||||
import TagCodePath from '@/views/ops/component/TagCodePath.vue';
|
||||
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { getTagPathSearchItem } from '../component/tag';
|
||||
import { dockerApi } from './api';
|
||||
|
||||
const ContainerConfEdit = defineAsyncComponent(() => import('./CotainerConfEdit.vue'));
|
||||
@@ -82,10 +80,7 @@ const props = defineProps({
|
||||
const route = useRoute();
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
|
||||
const searchItems = [
|
||||
SearchItem.input('keyword', 'common.keyword').withPlaceholder('redis.keywordPlaceholder'),
|
||||
getTagPathSearchItem(TagResourceTypeEnum.Container.value),
|
||||
];
|
||||
const searchItems = [SearchItem.input('keyword', 'common.keyword').withPlaceholder('redis.keywordPlaceholder')];
|
||||
|
||||
const columns = ref([
|
||||
TableColumn.new('name', 'common.name').isSlot('name').setAddWidth(15),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer :title="title" v-model="dialogVisible" :before-close="onCancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||
<el-drawer :append-to-body="false" :title="title" v-model="dialogVisible" :before-close="onCancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="onCancel" />
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer title="Docker" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="true" size="80%">
|
||||
<el-drawer :append-to-body="false" title="Docker" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="true" size="80%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="props.host" :back="cancel">
|
||||
<template #extra>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-drawer v-model="dialogVisible" :append-to-body="true" :destroy-on-close="true" :close-on-click-modal="false" :before-close="cancel" size="40%">
|
||||
<el-drawer v-model="dialogVisible" :append-to-body="false" :destroy-on-close="true" :close-on-click-modal="false" :before-close="cancel" size="40%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="$t('docker.createContainer')" :back="cancel">
|
||||
<template #extra>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="card !p-2">
|
||||
<el-row justify="space-between">
|
||||
<div class="component-container">
|
||||
<div class="card !p-2">
|
||||
<el-row justify="space-between">
|
||||
<el-col :span="16">
|
||||
<el-row :gutter="5">
|
||||
<el-col :span="6">
|
||||
@@ -21,15 +22,15 @@
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="filterTableDatas" v-loading="state.loadingContainers">
|
||||
<el-table :data="filterTableDatas" v-loading="state.loadingContainers">
|
||||
<el-table-column prop="name" :label="$t('docker.name')" :min-width="120" show-overflow-tooltip> </el-table-column>
|
||||
<el-table-column prop="imageName" :label="$t('docker.image')" :min-width="150" show-overflow-tooltip> </el-table-column>
|
||||
|
||||
<el-table-column prop="state" :label="$t('common.status')" :min-width="110">
|
||||
<template #default="{ row }">
|
||||
<el-dropdown @command="handleCommand">
|
||||
<el-dropdown @command="handleCommand" :teleported="false">
|
||||
<el-button :type="EnumValue.getEnumByValue(ContainerStateEnum, row.state)?.tag.type" round plain size="small">
|
||||
{{ $t(EnumValue.getLabelByValue(ContainerStateEnum, row.state)) || '-' }}
|
||||
<SvgIcon class="ml-1" :name="EnumValue.getEnumByValue(ContainerStateEnum, row.state)?.extra.icon" />
|
||||
@@ -68,7 +69,7 @@
|
||||
<span>{{ Number(row.stats.memoryPercent).toFixed(2) }}%</span>
|
||||
</el-text>
|
||||
|
||||
<el-popover placement="right" :width="300" trigger="hover">
|
||||
<el-popover placement="right" :width="300" trigger="hover" :teleported="false">
|
||||
<template #reference>
|
||||
<SvgIcon class="mt5 ml5" color="var(--el-color-primary)" name="MoreFilled" />
|
||||
</template>
|
||||
@@ -129,7 +130,7 @@
|
||||
|
||||
<el-button @click="openLog(row)" type="success" link plain>{{ $t('docker.log') }}</el-button>
|
||||
|
||||
<el-dropdown @command="handleCommand">
|
||||
<el-dropdown @command="handleCommand" :teleported="false">
|
||||
<el-button type="primary" link plain class="ml-3"> {{ $t('common.more') }} <SvgIcon name="arrow-down" :size="12" /> </el-button>
|
||||
|
||||
<template #dropdown>
|
||||
@@ -144,6 +145,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-if="terminalDialog.visible"
|
||||
@@ -358,3 +360,17 @@ function formatCpuValue(t: number) {
|
||||
return Number((t / Math.pow(num, 3)).toFixed(2)) + ' s';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.component-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.component-container :deep(.el-table) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer title="logs" v-model="visible" @close="close" :destroy-on-close="true" :close-on-click-modal="true" size="60%">
|
||||
<el-drawer :append-to-body="false" title="logs" v-model="visible" @close="close" :destroy-on-close="true" :close-on-click-modal="true" size="60%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="`${props.title}`" :back="() => (visible = false)">
|
||||
<template #extra>
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="flex flex-col flex-1">
|
||||
<el-row :gutter="10" class="mb-2">
|
||||
<el-col :span="6">
|
||||
<el-select @change="searchLog" v-model.number="state.tail">
|
||||
<el-select @change="searchLog" v-model.number="state.tail" :teleported="false">
|
||||
<template #prefix>{{ $t('docker.lines') }}</template>
|
||||
<el-option :value="100" :label="100" />
|
||||
<el-option :value="200" :label="200" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="card !p-2">
|
||||
<el-row :gutter="5">
|
||||
<div class="component-container">
|
||||
<div class="card !p-2">
|
||||
<el-row :gutter="5">
|
||||
<el-col :span="4">
|
||||
<el-input :placeholder="$t('docker.imageName')" v-model="params.name" plain clearable></el-input>
|
||||
</el-col>
|
||||
@@ -18,12 +19,12 @@
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="filterTableDatas" v-loading="state.loadingImages">
|
||||
<el-table :data="filterTableDatas" v-loading="state.loadingImages">
|
||||
<el-table-column prop="id" label="ID" :min-width="100" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" :underline="false">
|
||||
<el-link type="primary" underline="never">
|
||||
{{ row.id.split(':')[1].substring(0, 12) }}
|
||||
</el-link>
|
||||
</template>
|
||||
@@ -67,6 +68,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-if="terminalDialog.visible"
|
||||
@@ -86,6 +88,7 @@
|
||||
<script lang="ts" setup>
|
||||
import config from '@/common/config';
|
||||
import { joinClientParams } from '@/common/request';
|
||||
import { downloadFile } from '@/common/utils/file';
|
||||
import { formatByteSize, formatDate } from '@/common/utils/format';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
import { fuzzyMatchField } from '@/common/utils/string';
|
||||
@@ -159,33 +162,28 @@ const getImages = async () => {
|
||||
};
|
||||
|
||||
const exportImage = async (row: any) => {
|
||||
const a = document.createElement('a');
|
||||
a.setAttribute('href', `${config.baseApiUrl}/docker/${props.id}/images/save?id=${props.id}&tag=${row.tags[0]}&${joinClientParams()}`);
|
||||
a.setAttribute('target', '_blank');
|
||||
a.click();
|
||||
downloadFile(`${config.baseApiUrl}/docker/${props.id}/images/save?id=${props.id}&tag=${row.tags[0]}&${joinClientParams()}`);
|
||||
};
|
||||
|
||||
const uploadImage = (content: any) => {
|
||||
const params = new FormData();
|
||||
// const path = state.nowPath;
|
||||
params.append('file', content.file);
|
||||
params.append('id', props.id + '');
|
||||
params.append('token', token);
|
||||
dockerApi.imageUpload
|
||||
.xhrReq(params, {
|
||||
headers: { 'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundaryF1uyUD0tWdqmJqpl' },
|
||||
// onUploadProgress: onUploadProgress,
|
||||
timeout: 3 * 60 * 60 * 1000,
|
||||
})
|
||||
.then(() => {
|
||||
Msg.success('machine.uploadSuccess');
|
||||
setTimeout(() => {
|
||||
getImages();
|
||||
}, 3000);
|
||||
})
|
||||
.catch(() => {
|
||||
// state.uploadProgressShow = false;
|
||||
});
|
||||
const file = content.file;
|
||||
// 直接使用文件流作为 body,不包装为 FormData
|
||||
dockerApi.imageUpload.uploadRaw(
|
||||
file,
|
||||
{ id: String(props.id) },
|
||||
{
|
||||
onSuccess: () => {
|
||||
Msg.success('docker.uploadSuccess');
|
||||
setTimeout(() => {
|
||||
getImages();
|
||||
}, 1000);
|
||||
},
|
||||
onError: (error) => {
|
||||
Msg.error(error.message);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
Msg.info('docker.imageUploading');
|
||||
};
|
||||
|
||||
@@ -210,3 +208,17 @@ const closeTerminal = () => {
|
||||
state.terminalDialog.visible = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.component-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.component-container :deep(.el-table) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="card h-full">
|
||||
<el-tabs v-model="activeName" @tab-change="handleTabChange">
|
||||
<div class="card h-full flex flex-col">
|
||||
<el-tabs v-model="activeName" @tab-change="handleTabChange" class="container-op-tabs">
|
||||
<el-tab-pane :label="$t('docker.container')" :name="containerTab">
|
||||
<ContainerList :id="containerConfId" />
|
||||
</el-tab-pane>
|
||||
@@ -12,18 +12,22 @@
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ContainerOpComp } from '@/views/ops/docker/resource';
|
||||
import { toRefs, reactive, onMounted, defineAsyncComponent, ref, getCurrentInstance } from 'vue';
|
||||
import { defineAsyncComponent, onMounted, reactive, ref, toRefs } from 'vue';
|
||||
|
||||
const ContainerList = defineAsyncComponent(() => import('../container/ContainerList.vue'));
|
||||
const ImageList = defineAsyncComponent(() => import('../image/ImageList.vue'));
|
||||
|
||||
const props = defineProps<{
|
||||
containerId?: number;
|
||||
tabKey?: string;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits(['init']);
|
||||
|
||||
const containerTab = 'containerTab';
|
||||
const imageTab = 'imageTab';
|
||||
|
||||
const containerConfId = ref<number>(0);
|
||||
const containerConfId = ref<number>(props.containerId || 0);
|
||||
|
||||
const state = reactive({
|
||||
activeName: containerTab,
|
||||
@@ -33,7 +37,6 @@ const state = reactive({
|
||||
const { activeName } = toRefs(state);
|
||||
|
||||
onMounted(async () => {
|
||||
emits('init', { name: ContainerOpComp.name, ref: getCurrentInstance()?.exposed });
|
||||
state.activeName = containerTab;
|
||||
});
|
||||
|
||||
@@ -45,3 +48,22 @@ defineExpose({
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container-op-tabs {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
:deep(.el-tabs__content) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
:deep(.el-tab-pane) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { ResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { NodeType, TagTreeNode, ResourceComponentConfig, ResourceConfig } from '@/views/ops/component/tag';
|
||||
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
|
||||
import { dockerApi } from '@/views/ops/docker/api';
|
||||
import type { ResourceConfig } from '@/views/ops/resource/resource';
|
||||
import { createResourceOpTab } from '@/views/ops/resource/resourceOp';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
const ContainerConfList = defineAsyncComponent(() => import('../ContainerConfList.vue'));
|
||||
const ContainerOp = defineAsyncComponent(() => import('./ContainerOp.vue'));
|
||||
@@ -11,10 +13,18 @@ const Icon = {
|
||||
color: ResourceTypeEnum.Container.extra.iconColor,
|
||||
};
|
||||
|
||||
export const ContainerOpComp: ResourceComponentConfig = {
|
||||
name: 'tag.containerOp',
|
||||
component: ContainerOp,
|
||||
icon: Icon,
|
||||
const getContainerOpTab = async (container: any) => {
|
||||
const tabKey = `${container.code}`;
|
||||
return await createResourceOpTab({
|
||||
key: tabKey,
|
||||
name: container.name,
|
||||
component: ContainerOp,
|
||||
tabComponentProps: { icon: Icon },
|
||||
});
|
||||
};
|
||||
|
||||
const getContainerOpTabCompInst = async (container: any) => {
|
||||
return (await getContainerOpTab(container)).componentInstance;
|
||||
};
|
||||
|
||||
export const NodeTypeContainerTag = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (node: TagTreeNode) => {
|
||||
@@ -23,11 +33,13 @@ export const NodeTypeContainerTag = new NodeType(TagTreeNode.TagPath).withLoadNo
|
||||
// 把list 根据name字段排序
|
||||
return res?.list
|
||||
.sort((a: any, b: any) => a.name.localeCompare(b.name))
|
||||
.map((x: any) => TagTreeNode.new(node, x.code, x.name, NodeTypeContainer).withIsLeaf(true).withParams(x).withIcon(Icon));
|
||||
.map((x: any) => TagTreeNode.new(node, `${x.code}`, x.name, NodeTypeContainer).withIsLeaf(true).withParams(x).withIcon(Icon));
|
||||
});
|
||||
|
||||
const NodeTypeContainer = new NodeType(11).withNodeClickFunc(async (node: TagTreeNode) => {
|
||||
(await node.ctx?.addResourceComponent(ContainerOpComp)).init(node.params.id);
|
||||
const container = node.params;
|
||||
const compRef = await getContainerOpTabCompInst(container);
|
||||
compRef?.init?.(container.id);
|
||||
});
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer :title="title" v-model="dialogVisible" :before-close="onCancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||
<el-drawer
|
||||
:append-to-body="false"
|
||||
:title="title"
|
||||
v-model="dialogVisible"
|
||||
:before-close="onCancel"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
size="40%"
|
||||
>
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="onCancel" />
|
||||
</template>
|
||||
@@ -160,7 +168,7 @@ const onTestConn = async (authCert: any) => {
|
||||
submitForm.authCerts = [authCert];
|
||||
}
|
||||
await testConnExec(submitForm);
|
||||
state.form.version = testConnRes.value.version.number;
|
||||
state.form.version = testConnRes.value.version?.number;
|
||||
Msg.success('es.connSuccess');
|
||||
};
|
||||
|
||||
|
||||
@@ -64,7 +64,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { TagResourceTypePath } from '@/common/commonEnum';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
@@ -73,7 +72,6 @@ import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { Msg, useI18nCreateTitle, useI18nDeleteConfirm, useI18nEditTitle } from '@/hooks/useI18n';
|
||||
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
||||
import ResourceAuthCert from '../component/ResourceAuthCert.vue';
|
||||
import { getTagPathSearchItem } from '../component/tag';
|
||||
import TagCodePath from '../component/TagCodePath.vue';
|
||||
import { esApi } from './api';
|
||||
|
||||
@@ -91,7 +89,7 @@ const perms = {
|
||||
delInstance: 'es:instance:del',
|
||||
};
|
||||
|
||||
const searchItems = [SearchItem.input('keyword', 'common.keyword').withPlaceholder('es.keywordPlaceholder'), getTagPathSearchItem(TagResourceTypePath.Es)];
|
||||
const searchItems = [SearchItem.input('keyword', 'common.keyword').withPlaceholder('es.keywordPlaceholder')];
|
||||
|
||||
const columns = ref([
|
||||
TableColumn.new('name', 'common.name').isSlot('name').setAddWidth(15),
|
||||
|
||||
@@ -7,6 +7,8 @@ export const esApi = {
|
||||
deleteInstance: Api.newDelete('/es/instance/{id}'),
|
||||
saveInstance: Api.newPost('/es/instance'),
|
||||
testConn: Api.newPost('/es/instance/test-conn'),
|
||||
exportData: Api.newPost('/es/instance/export/{instanceId}'),
|
||||
exportProgress: Api.newGet('/es/instance/export/progress/{exportId}'),
|
||||
|
||||
// proxyGet: Api.newGet('/es/instance/proxy/{id}/{path}'),
|
||||
// proxyPost: Api.newPost('/es/instance/proxy/{id}/{path}'),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
<template>
|
||||
<el-drawer
|
||||
:append-to-body="false"
|
||||
:title="t('es.addIndex')"
|
||||
v-model="visible"
|
||||
size="50%"
|
||||
@@ -18,7 +19,7 @@
|
||||
</el-form-item>
|
||||
<el-space>
|
||||
<el-form-item>
|
||||
<el-select v-model="formData.copyIdxName">
|
||||
<el-select v-model="formData.copyIdxName" style="width: 200px;" filterable>
|
||||
<el-option v-for="idx in idxNames" :key="idx" :value="idx" :label="idx" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
<template>
|
||||
<el-tabs v-model="state.tabName" type="card">
|
||||
<el-tabs v-model="state.tabName" type="card" class="es-dashboard-tabs">
|
||||
<el-tab-pane name="idxManage" :label="t('es.opIndex')">
|
||||
<div class="idx-manage-content">
|
||||
<EsIndexManage :instId="props.instId" @viewData="onViewIndexData" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane name="dataManage" :label="t('es.opDataManage')">
|
||||
<div class="idx-manage-content">
|
||||
<EsIndexData ref="esIndexDataRef" :instId="props.instId" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane name="nodesStats" v-loading="state.nodesStatsLoading" style="height: calc(100vh - 200px); overflow-y: auto">
|
||||
<template #label>
|
||||
{{ t('es.dashboard.nodes') }}
|
||||
@@ -142,12 +154,12 @@
|
||||
<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-select v-model="state.analyze.idxName" filterable clearable :teleported="false" @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-select v-model="state.analyze.field" filterable clearable :teleported="false">
|
||||
<el-option v-for="field in state.analyze.fields" :key="field" :value="field" :label="field" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -169,11 +181,14 @@
|
||||
</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 { esApi } from '@/views/ops/es/api';
|
||||
import dayjs from 'dayjs';
|
||||
import { defineAsyncComponent, nextTick, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const EsIndexData = defineAsyncComponent(() => import('./EsIndexData.vue'));
|
||||
const EsIndexManage = defineAsyncComponent(() => import('./EsIndexManage.vue'));
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -183,9 +198,16 @@ interface Props {
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const analyzeFormRef = ref();
|
||||
const esIndexDataRef = ref();
|
||||
|
||||
const onViewIndexData = async (idxName: string) => {
|
||||
state.tabName = 'dataManage';
|
||||
await nextTick();
|
||||
esIndexDataRef.value?.selectIndex(idxName);
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
tabName: 'nodesStats',
|
||||
tabName: 'idxManage',
|
||||
instInfo: [] as any[],
|
||||
clusterHealth: [] as any[],
|
||||
nodesStats: { _nodes: {} as any, nodes: [] as any[] } as any,
|
||||
@@ -246,7 +268,7 @@ const fetchInstInfo = async () => {
|
||||
|
||||
function flattenObject(obj: Record<string, any>, parentKey = '', result: Record<string, any> = {}): Record<string, any> {
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
const newKey = parentKey ? `${parentKey}.${key}` : key;
|
||||
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
|
||||
flattenObject(obj[key], newKey, result);
|
||||
@@ -379,6 +401,29 @@ const onAnalyze = async () => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.es-dashboard-tabs {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
:deep(.el-tabs__content) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:deep(.el-tab-pane) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.idx-manage-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nodes-num {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:append-to-body="false"
|
||||
:title="`${model.isAdd ? t('common.add') : t('common.edit')} ${model.idxName}`"
|
||||
v-model="visible"
|
||||
:destroy-on-close="false"
|
||||
|
||||
1024
frontend/src/views/ops/es/component/EsIndexData.vue
Normal file
1024
frontend/src/views/ops/es/component/EsIndexData.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:append-to-body="false"
|
||||
:title="t('es.indexDetail') + ' - ' + state.idxName"
|
||||
v-model="visible"
|
||||
size="50%"
|
||||
|
||||
472
frontend/src/views/ops/es/component/EsIndexManage.vue
Normal file
472
frontend/src/views/ops/es/component/EsIndexManage.vue
Normal file
@@ -0,0 +1,472 @@
|
||||
<template>
|
||||
<div class="es-index-manage-box">
|
||||
<div class="es-idx-toolbar flex-shrink-0">
|
||||
<el-space>
|
||||
<el-button type="primary" icon="Plus" size="small" @click="onAddIndex">{{ t('es.addIndex') }}</el-button>
|
||||
<el-button icon="Refresh" size="small" @click="fetchIndices">{{ t('common.refresh') }}</el-button>
|
||||
<el-button type="primary" size="small" @click="templateVisible = true">{{ t('es.templates') }}</el-button>
|
||||
<el-checkbox v-model="showSysIndex" size="small" @change="fetchIndices">{{ t('es.contextmenu.index.showSys') }}</el-checkbox>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="es-idx-table">
|
||||
<el-auto-resizer>
|
||||
<template #default="{ height, width }">
|
||||
<el-table-v2
|
||||
:columns="tableColumns"
|
||||
:data="filteredIndices"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:row-height="40"
|
||||
v-loading="loading"
|
||||
:sort-state="sortState"
|
||||
@column-sort="onColumnSort"
|
||||
fixed
|
||||
/>
|
||||
</template>
|
||||
</el-auto-resizer>
|
||||
</div>
|
||||
|
||||
<!-- 查看/编辑 Mapping 对话框 -->
|
||||
<el-drawer
|
||||
v-model="mappingDrawer.visible"
|
||||
:title="`${t('es.indexMapping')} - ${mappingDrawer.idxName}`"
|
||||
size="55%"
|
||||
:append-to-body="false"
|
||||
:destroy-on-close="false"
|
||||
>
|
||||
<el-auto-resizer>
|
||||
<template #default="{ height, width }">
|
||||
<monaco-editor
|
||||
v-model="mappingDrawer.content"
|
||||
language="json"
|
||||
:height="height - 60 + 'px'"
|
||||
:width="width + 'px'"
|
||||
:options="{ tabSize: 2, readOnly: !mappingDrawer.editable }"
|
||||
/>
|
||||
</template>
|
||||
</el-auto-resizer>
|
||||
<template #footer>
|
||||
<el-space>
|
||||
<el-button @click="mappingDrawer.editable = !mappingDrawer.editable">
|
||||
{{ mappingDrawer.editable ? t('common.cancel') : t('common.edit') }}
|
||||
</el-button>
|
||||
<el-button v-if="mappingDrawer.editable" type="primary" @click="onSaveMapping" :loading="mappingDrawer.saving">{{
|
||||
t('common.save')
|
||||
}}</el-button>
|
||||
</el-space>
|
||||
</template>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 添加索引对话框 -->
|
||||
<EsAddIndex :instId="props.instId" :idxNames="idxNames" v-model:visible="addIndexVisible" @success="fetchIndices" />
|
||||
|
||||
<!-- 索引迁移对话框 -->
|
||||
<EsReindex
|
||||
:instId="reindexState.instId"
|
||||
:idxName="reindexState.idxName"
|
||||
:idxNames="reindexState.idxNames"
|
||||
v-model:visible="reindexState.visible"
|
||||
@success="fetchIndices"
|
||||
/>
|
||||
|
||||
<!-- 索引详情 -->
|
||||
<EsIndexDetail ref="esIndexDetailRef" />
|
||||
|
||||
<!-- 索引模板管理 -->
|
||||
<EsIndexTemplate :instId="props.instId" :version="esVersion" v-model="templateVisible" />
|
||||
|
||||
<!-- 添加别名对话框 -->
|
||||
<el-dialog v-model="aliasDialog.visible" :title="t('es.addAlias')" width="400" :append-to-body="false">
|
||||
<el-form @submit.prevent="onSubmitAddAlias">
|
||||
<el-form-item :label="t('es.aliases')">
|
||||
<el-input v-model="aliasDialog.name" autocomplete="off" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="aliasDialog.visible = false">{{ t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="onSubmitAddAlias" :loading="aliasDialog.loading">{{ t('common.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, defineAsyncComponent, h, onMounted, reactive, ref } from 'vue';
|
||||
import { ElButton, ElDropdown, ElDropdownItem, ElDropdownMenu, ElIcon, ElTag } from 'element-plus';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ArrowDown, Close, CopyDocument, Delete, Plus, Refresh, Select, Switch } from '@element-plus/icons-vue';
|
||||
import { esApi } from '@/views/ops/es/api';
|
||||
import { copyToClipboard } from '@/common/utils/string';
|
||||
import { Msg, useI18nConfirm, useI18nDeleteConfirm } from '@/hooks/useI18n';
|
||||
|
||||
const MonacoEditor = defineAsyncComponent(() => import('@/components/monaco/MonacoEditor.vue'));
|
||||
const EsAddIndex = defineAsyncComponent(() => import('./EsAddIndex.vue'));
|
||||
const EsReindex = defineAsyncComponent(() => import('./EsReindex.vue'));
|
||||
const EsIndexDetail = defineAsyncComponent(() => import('./EsIndexDetail.vue'));
|
||||
const EsIndexTemplate = defineAsyncComponent(() => import('./EsIndexTemplate.vue'));
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
instId: any;
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const showSysIndex = ref(false);
|
||||
const indices = ref<any[]>([]);
|
||||
const aliasesMap = reactive<Record<string, string[]>>({});
|
||||
const sortState = ref({ index: 'ascending' });
|
||||
|
||||
// tableColumns for el-table-v2
|
||||
const tableColumns = computed(() => [
|
||||
{
|
||||
dataKey: 'index',
|
||||
key: 'index',
|
||||
title: t('es.indexName'),
|
||||
width: 220,
|
||||
sortable: true,
|
||||
cellRenderer: ({ rowData }: any) => h('a', {
|
||||
href: 'javascript:void(0)',
|
||||
style: { color: 'var(--el-color-primary)', textDecoration: 'none' },
|
||||
onClick: () => emit('viewData', rowData.index)
|
||||
}, rowData.index)
|
||||
},
|
||||
{
|
||||
dataKey: 'aliases',
|
||||
key: 'aliases',
|
||||
title: t('es.aliases'),
|
||||
width: 200,
|
||||
cellRenderer: ({ rowData }: any) => {
|
||||
const aliases = aliasesMap[rowData.index] || [];
|
||||
return h('div', { class: 'flex items-center gap-1 flex-wrap' },
|
||||
[...aliases.map((alias: string) => h(ElTag, {
|
||||
closable: true,
|
||||
size: 'small',
|
||||
type: 'info',
|
||||
onClose: () => onRemoveAlias(rowData.index, alias)
|
||||
}, () => alias)),
|
||||
h(ElButton, {
|
||||
link: true,
|
||||
type: 'primary',
|
||||
size: 'small',
|
||||
onClick: () => onAddAlias(rowData)
|
||||
}, () => h(ElIcon, () => h(Plus)))]
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
dataKey: 'health',
|
||||
key: 'health',
|
||||
title: t('es.health'),
|
||||
width: 100,
|
||||
sortable: true,
|
||||
align: 'center',
|
||||
cellRenderer: ({ rowData }: any) => h(ElTag, { size: 'small', type: getHealthTagType(rowData.health) }, () => rowData.health)
|
||||
},
|
||||
{
|
||||
dataKey: 'status',
|
||||
key: 'status',
|
||||
title: t('es.status'),
|
||||
width: 100,
|
||||
sortable: true,
|
||||
align: 'center',
|
||||
cellRenderer: ({ rowData }: any) => h(ElTag, { size: 'small', type: rowData.status === 'open' ? 'success' : 'danger' }, () => rowData.status)
|
||||
},
|
||||
{ dataKey: 'pri', key: 'pri', title: 'pri', width: 70, align: 'center' },
|
||||
{ dataKey: 'rep', key: 'rep', title: 'rep', width: 70, align: 'center' },
|
||||
{
|
||||
dataKey: 'docs.count',
|
||||
key: 'docs.count',
|
||||
title: t('es.docs'),
|
||||
width: 120,
|
||||
sortable: true,
|
||||
align: 'right',
|
||||
cellRenderer: ({ rowData }: any) => rowData['docs.count'] ?? '-'
|
||||
},
|
||||
{ dataKey: 'store.size', key: 'store.size', title: t('es.size'), width: 120, sortable: true, align: 'right' },
|
||||
{
|
||||
dataKey: 'operation',
|
||||
key: 'operation',
|
||||
title: t('common.operation'),
|
||||
width: 200,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
cellRenderer: ({ rowData }: any) => {
|
||||
const dropdownTrigger = h(ElButton, { link: true, type: 'primary', size: 'small' }, () => [t('common.more'), h(ElIcon, () => h(ArrowDown))]);
|
||||
const dropdownMenu = [
|
||||
h(ElDropdownItem, { key: 'copyName', command: 'copyName' }, () => t('es.contextmenu.index.copyName')),
|
||||
h(ElDropdownItem, { key: 'refresh', command: 'refresh' }, () => t('es.contextmenu.index.refresh')),
|
||||
h(ElDropdownItem, { key: 'flush', command: 'flush' }, () => t('es.contextmenu.index.flush')),
|
||||
h(ElDropdownItem, { key: 'clearCache', command: 'clearCache' }, () => t('es.contextmenu.index.clearCache')),
|
||||
h(ElDropdownItem, { key: 'reindex', command: 'reindex' }, () => t('es.Reindex')),
|
||||
rowData.status === 'open'
|
||||
? h(ElDropdownItem, { key: 'close', command: 'close' }, () => t('es.contextmenu.index.Close'))
|
||||
: h(ElDropdownItem, { key: 'open', command: 'open' }, () => t('es.contextmenu.index.Open')),
|
||||
h(ElDropdownItem, { key: 'delete', command: 'delete', divided: true }, () => t('common.delete'))
|
||||
];
|
||||
return h('div', { class: 'flex items-center justify-center gap-1' }, [
|
||||
h(ElButton, { link: true, type: 'primary', size: 'small', onClick: () => onViewDetail(rowData) }, () => t('es.indexDetail')),
|
||||
h(ElDropdown, {
|
||||
trigger: 'click',
|
||||
onCommand: (cmd: string) => onRowCommand(cmd, rowData)
|
||||
}, { default: () => dropdownTrigger, dropdown: () => h(ElDropdownMenu, {}, () => dropdownMenu) })
|
||||
]);
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
const addIndexVisible = ref(false);
|
||||
const templateVisible = ref(false);
|
||||
const esVersion = ref('');
|
||||
|
||||
const emit = defineEmits(['viewData']);
|
||||
|
||||
const esIndexDetailRef = ref();
|
||||
|
||||
const aliasDialog = reactive({
|
||||
visible: false,
|
||||
idxName: '',
|
||||
name: '',
|
||||
loading: false,
|
||||
});
|
||||
|
||||
const reindexState = reactive({
|
||||
instId: '' as any,
|
||||
idxName: '',
|
||||
visible: false,
|
||||
idxNames: [] as string[],
|
||||
});
|
||||
|
||||
const mappingDrawer = reactive({
|
||||
visible: false,
|
||||
idxName: '',
|
||||
content: '',
|
||||
editable: false,
|
||||
saving: false,
|
||||
});
|
||||
|
||||
const idxNames = computed(() => indices.value.map((idx: any) => idx.index).filter((n: string) => !n.startsWith('.')));
|
||||
|
||||
const filteredIndices = computed(() => {
|
||||
const data = [...indices.value];
|
||||
const entries = Object.entries(sortState.value);
|
||||
if (entries.length === 0) return data;
|
||||
const [key, order] = entries[0];
|
||||
const dir = order === 'ascending' ? 1 : -1;
|
||||
return data.sort((a, b) => {
|
||||
const va = a[key] ?? '';
|
||||
const vb = b[key] ?? '';
|
||||
if (typeof va === 'number' && typeof vb === 'number') return (va - vb) * dir;
|
||||
return String(va).localeCompare(String(vb)) * dir;
|
||||
});
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
fetchIndices();
|
||||
fetchVersion();
|
||||
});
|
||||
|
||||
const fetchVersion = async () => {
|
||||
try {
|
||||
const res = await esApi.proxyReq('get', props.instId, '/');
|
||||
esVersion.value = res?.version?.number || '';
|
||||
} catch {
|
||||
// non-critical
|
||||
}
|
||||
};
|
||||
|
||||
const fetchIndices = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await esApi.proxyReq('get', props.instId, `/_cat/indices/?h=index,health,status,uuid,pri,rep,docs.count,docs.deleted,store.size,sc,cd`);
|
||||
const list = res || [];
|
||||
indices.value = showSysIndex.value ? list : list.filter((idx: any) => !idx.index.startsWith('.'));
|
||||
// Fetch aliases for all indices
|
||||
await fetchAliases();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAliases = async () => {
|
||||
try {
|
||||
const res = await esApi.proxyReq('get', props.instId, '/_alias');
|
||||
// Clear and rebuild
|
||||
for (const key of Object.keys(aliasesMap)) {
|
||||
delete aliasesMap[key];
|
||||
}
|
||||
for (const idxName of Object.keys(res || {})) {
|
||||
const aliases = Object.keys(res[idxName]?.aliases || {});
|
||||
if (aliases.length > 0) {
|
||||
aliasesMap[idxName] = aliases;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Alias fetch failure is non-critical
|
||||
}
|
||||
};
|
||||
|
||||
const onColumnSort = ({ key, order }: any) => {
|
||||
sortState.value = { [key]: order } as any;
|
||||
};
|
||||
|
||||
const onAddIndex = () => {
|
||||
addIndexVisible.value = true;
|
||||
};
|
||||
|
||||
const getHealthTagType = (health: string) => {
|
||||
return health == 'green' ? 'success' : health == 'yellow' ? 'warning' : 'danger';
|
||||
};
|
||||
|
||||
// ---- Index operations ----
|
||||
|
||||
const onRowCommand = async (cmd: string, row: any) => {
|
||||
switch (cmd) {
|
||||
case 'copyName':
|
||||
await copyToClipboard(row.index);
|
||||
break;
|
||||
case 'refresh':
|
||||
await esApi.proxyReq('post', props.instId, `/${row.index}/_refresh`);
|
||||
Msg.operateSuccess();
|
||||
break;
|
||||
case 'mapping':
|
||||
await onViewMapping(row);
|
||||
break;
|
||||
case 'flush':
|
||||
await onFlushIndex(row);
|
||||
break;
|
||||
case 'clearCache':
|
||||
await onClearCache(row);
|
||||
break;
|
||||
case 'reindex':
|
||||
onReindex(row);
|
||||
break;
|
||||
case 'close':
|
||||
await onCloseIndex(row);
|
||||
break;
|
||||
case 'open':
|
||||
await onOpenIndex(row);
|
||||
break;
|
||||
case 'delete':
|
||||
await onDeleteIndex(row);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const onViewDetail = (row: any) => {
|
||||
esIndexDetailRef.value?.open({ idxName: row.index, instId: props.instId });
|
||||
};
|
||||
|
||||
const onReindex = async (row: any) => {
|
||||
reindexState.instId = props.instId;
|
||||
reindexState.idxName = row.index;
|
||||
reindexState.idxNames = idxNames.value.filter((n: string) => n !== row.index);
|
||||
reindexState.visible = true;
|
||||
};
|
||||
|
||||
const onViewMapping = async (row: any) => {
|
||||
const res = await esApi.proxyReq('get', props.instId, `/${row.index}/_mappings`);
|
||||
mappingDrawer.idxName = row.index;
|
||||
mappingDrawer.content = JSON.stringify(res[row.index]?.mappings || {}, null, 2);
|
||||
mappingDrawer.editable = false;
|
||||
mappingDrawer.saving = false;
|
||||
mappingDrawer.visible = true;
|
||||
};
|
||||
|
||||
const onSaveMapping = async () => {
|
||||
mappingDrawer.saving = true;
|
||||
try {
|
||||
await esApi.proxyReq('put', props.instId, `/${mappingDrawer.idxName}/_mappings`, JSON.parse(mappingDrawer.content));
|
||||
Msg.saveSuccess();
|
||||
mappingDrawer.editable = false;
|
||||
} finally {
|
||||
mappingDrawer.saving = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onCloseIndex = async (row: any) => {
|
||||
await useI18nConfirm('es.closeIndexConfirm', { name: row.index });
|
||||
await esApi.proxyReq('post', props.instId, `/${row.index}/_close`);
|
||||
row.status = 'close';
|
||||
Msg.operateSuccess();
|
||||
};
|
||||
|
||||
const onOpenIndex = async (row: any) => {
|
||||
await useI18nConfirm('es.openIndexConfirm', { name: row.index });
|
||||
await esApi.proxyReq('post', props.instId, `/${row.index}/_open`);
|
||||
row.status = 'open';
|
||||
Msg.operateSuccess();
|
||||
};
|
||||
|
||||
const onFlushIndex = async (row: any) => {
|
||||
await esApi.proxyReq('post', props.instId, `/${row.index}/_flush`);
|
||||
Msg.operateSuccess();
|
||||
};
|
||||
|
||||
const onClearCache = async (row: any) => {
|
||||
await useI18nConfirm('es.clearCacheConfirm', { name: row.index });
|
||||
await esApi.proxyReq('post', props.instId, `/${row.index}/_cache/clear`);
|
||||
Msg.operateSuccess();
|
||||
};
|
||||
|
||||
const onDeleteIndex = async (row: any) => {
|
||||
await useI18nDeleteConfirm(row.index);
|
||||
await esApi.proxyReq('delete', props.instId, row.index);
|
||||
Msg.deleteSuccess();
|
||||
await fetchIndices();
|
||||
};
|
||||
|
||||
// ---- Alias operations ----
|
||||
|
||||
const onAddAlias = (row: any) => {
|
||||
aliasDialog.idxName = row.index;
|
||||
aliasDialog.name = '';
|
||||
aliasDialog.loading = false;
|
||||
aliasDialog.visible = true;
|
||||
};
|
||||
|
||||
const onSubmitAddAlias = async () => {
|
||||
if (!aliasDialog.name) return;
|
||||
aliasDialog.loading = true;
|
||||
try {
|
||||
await esApi.proxyReq('put', props.instId, `/${aliasDialog.idxName}/_alias/${aliasDialog.name}`);
|
||||
Msg.saveSuccess();
|
||||
// Update local aliases
|
||||
if (!aliasesMap[aliasDialog.idxName]) {
|
||||
aliasesMap[aliasDialog.idxName] = [];
|
||||
}
|
||||
aliasesMap[aliasDialog.idxName].push(aliasDialog.name);
|
||||
aliasDialog.visible = false;
|
||||
} finally {
|
||||
aliasDialog.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onRemoveAlias = async (idxName: string, alias: string) => {
|
||||
await useI18nDeleteConfirm(`${t('es.aliases')}: ${alias}`);
|
||||
await esApi.proxyReq('delete', props.instId, `/${idxName}/_alias/${alias}`);
|
||||
Msg.deleteSuccess();
|
||||
// Update local aliases
|
||||
if (aliasesMap[idxName]) {
|
||||
aliasesMap[idxName] = aliasesMap[idxName].filter((a: string) => a !== alias);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.es-index-manage-box {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.es-idx-toolbar {
|
||||
padding: 6px 8px;
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
}
|
||||
|
||||
.es-idx-table {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:append-to-body="false"
|
||||
size="50%"
|
||||
:destroy-on-close="false"
|
||||
:close-on-click-modal="false"
|
||||
@@ -53,6 +54,7 @@
|
||||
</el-drawer>
|
||||
|
||||
<el-drawer
|
||||
:append-to-body="false"
|
||||
size="50%"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:append-to-body="false"
|
||||
:title="t('es.Reindex')"
|
||||
v-model="visible"
|
||||
size="40%"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,10 @@
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { ResourceTypeEnum, TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { sleep } from '@/common/utils/loading';
|
||||
import { ContextmenuItem } from '@/components/contextmenu';
|
||||
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
|
||||
import { esApi } from '@/views/ops/es/api';
|
||||
import { i18n } from '@/i18n';
|
||||
import { NodeType, TagTreeNode, ResourceComponentConfig } from '@/views/ops/component/tag';
|
||||
import { ResourceConfig } from '../../component/tag';
|
||||
import type { ResourceConfig } from '@/views/ops/resource/resource';
|
||||
import { createResourceOpTab } from '@/views/ops/resource/resourceOp';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
const Icon = {
|
||||
name: ResourceTypeEnum.Es.extra.icon,
|
||||
@@ -13,151 +12,39 @@ const Icon = {
|
||||
};
|
||||
|
||||
const EsInstanceList = defineAsyncComponent(() => import('../EsInstanceList.vue'));
|
||||
const EsDataOp = defineAsyncComponent(() => import('./EsDataOp.vue'));
|
||||
const EsDashboard = defineAsyncComponent(() => import('../component/EsDashboard.vue'));
|
||||
|
||||
const NodeEs = defineAsyncComponent(() => import('./NodeEs.vue'));
|
||||
const NodeEsIndex = defineAsyncComponent(() => import('./NodeEsIndex.vue'));
|
||||
|
||||
export const EsOpComp: ResourceComponentConfig = {
|
||||
name: 'tag.esDataOp',
|
||||
component: EsDataOp,
|
||||
icon: Icon,
|
||||
};
|
||||
|
||||
// tagpath 节点类型
|
||||
const NodeTypeEsTag = new NodeType(TagTreeNode.TagPath)
|
||||
.withContextMenuItems([
|
||||
new ContextmenuItem('refresh', 'common.refresh')
|
||||
.withIcon('refresh')
|
||||
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).reloadNode(nodeData.key)),
|
||||
])
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
parentNode.ctx?.addResourceComponent(EsOpComp);
|
||||
// 加载es实例列表
|
||||
const res = await esApi.instances.request({ tagPath: parentNode.params.tagPath });
|
||||
if (!res.total) {
|
||||
return [];
|
||||
}
|
||||
const insts = res.list;
|
||||
await sleep(100);
|
||||
return insts?.map((x: any) => {
|
||||
x.tagPath = parentNode.key;
|
||||
return TagTreeNode.new(parentNode, `es.inst.${x.code}`, x.name, NodeTypeInst).withNodeComponent(NodeEs).withParams(x);
|
||||
});
|
||||
const NodeTypeEsTag = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
// 加载es实例列表
|
||||
const res = await esApi.instances.request({ tagPath: parentNode.params.tagPath });
|
||||
if (!res.total) {
|
||||
return [];
|
||||
}
|
||||
const insts = res.list;
|
||||
await sleep(100);
|
||||
return insts?.map((x: any) => {
|
||||
x.tagPath = parentNode.key;
|
||||
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeInst).withNodeComponent(NodeEs).withIsLeaf(true).withParams(x);
|
||||
});
|
||||
});
|
||||
|
||||
// 加载实例列表
|
||||
const NodeTypeInst = new NodeType(1)
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
|
||||
let oiKey = `es.${params.id}.opIndex`;
|
||||
let bsKey = `es.${params.id}.opBasicSearch`;
|
||||
let ssKey = `es.${params.id}.opSeniorSearch`;
|
||||
let dbKey = `es.${params.id}.opDashboard`;
|
||||
let stKey = `es.${params.id}.opSettings`;
|
||||
let tpKey = `es.${params.id}.optemplates`;
|
||||
|
||||
let nodeParams = { inst: params, instId: params.id };
|
||||
|
||||
return [
|
||||
TagTreeNode.new(parentNode, oiKey, i18n.global.t('es.opIndex'), NodeTypeIndexs).withParams(nodeParams).withIcon({ name: 'Document' }),
|
||||
// new TagTreeNode(ssKey, t('es.opSeniorSearch'), NodeTypeSeniorSearch).withParams(nodeParams).withIsLeaf(true),
|
||||
// new TagTreeNode(dbKey, t('es.opDashboard'), NodeTypeDashboard).withParams(nodeParams).withIsLeaf(true),
|
||||
// new TagTreeNode(stKey, t('es.opSettings'), NodeTypeSettings).withParams(nodeParams),
|
||||
];
|
||||
})
|
||||
.withNodeClickFunc(async (nodeData: TagTreeNode) => {
|
||||
// 添加一个dashboard tab
|
||||
(await nodeData.ctx?.addResourceComponent(EsOpComp)).onInstClick(nodeData);
|
||||
});
|
||||
|
||||
const NodeTypeIndexs = new NodeType(2)
|
||||
.withContextMenuItems([
|
||||
new ContextmenuItem('refresh', 'common.refresh')
|
||||
.withIcon('refresh')
|
||||
.withOnClick(async (nodeData: TagTreeNode) =>
|
||||
(await nodeData.ctx?.addResourceComponent(EsOpComp)).onRefreshIndices(nodeData.params.instId, nodeData.key)
|
||||
),
|
||||
new ContextmenuItem('addIndex', 'es.contextmenu.index.addIndex')
|
||||
.withIcon('plus')
|
||||
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onAddIndex(nodeData)),
|
||||
new ContextmenuItem('showSys', 'es.contextmenu.index.showSys')
|
||||
.withIcon('View')
|
||||
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onShowSysIndex(nodeData)),
|
||||
new ContextmenuItem('idxTemplate', 'es.templates')
|
||||
.withIcon('DocumentCopy')
|
||||
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onShowTemplate(nodeData)),
|
||||
])
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
console.log(params);
|
||||
// 展示索引列表,显示索引名,文档总数
|
||||
// 加载索引列表
|
||||
let indicesRes = await (await parentNode.ctx?.addResourceComponent(EsOpComp)).loadIdxs(params);
|
||||
|
||||
let idxNodes = [];
|
||||
for (let idx of indicesRes) {
|
||||
idxNodes.push(
|
||||
TagTreeNode.new(parentNode, idx.key, idx.idxName, NodeTypeIndex)
|
||||
.withIsLeaf(true)
|
||||
.withParams({
|
||||
parentKey: parentNode.key,
|
||||
...idx,
|
||||
})
|
||||
.withNodeComponent(NodeEsIndex)
|
||||
);
|
||||
}
|
||||
return idxNodes;
|
||||
});
|
||||
|
||||
// 索引操作
|
||||
const NodeTypeIndex = new NodeType(3)
|
||||
.withContextMenuItems([
|
||||
// 右键菜单支持:复制名字、新增别名、迁移索引、关闭、启用、删除、数据浏览、跳转基础查询、跳转高级查询
|
||||
new ContextmenuItem('copyName', 'es.contextmenu.index.copyName')
|
||||
.withIcon('copyDocument')
|
||||
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onIdxCopyName(nodeData)),
|
||||
new ContextmenuItem('refresh', 'es.contextmenu.index.refresh')
|
||||
.withIcon('refresh')
|
||||
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onRefreshIdx(nodeData)),
|
||||
new ContextmenuItem('clearCache', 'es.contextmenu.index.clearCache')
|
||||
.withIcon('refresh')
|
||||
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onClearIdxCache(nodeData)),
|
||||
new ContextmenuItem('flush', 'es.contextmenu.index.flush')
|
||||
.withIcon('refresh')
|
||||
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onFlushIdx(nodeData)),
|
||||
new ContextmenuItem('Reindex', 'es.Reindex')
|
||||
.withIcon('Switch')
|
||||
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onIdxReindex(nodeData)),
|
||||
new ContextmenuItem('Close', 'es.contextmenu.index.Close')
|
||||
.withIcon('Close')
|
||||
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onIdxClose(nodeData))
|
||||
.withHideFunc((data: any) => {
|
||||
return data.params.idx.status !== 'open';
|
||||
}),
|
||||
new ContextmenuItem('Open', 'es.contextmenu.index.Open')
|
||||
.withIcon('Select')
|
||||
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onIdxOpen(nodeData))
|
||||
.withHideFunc((data: any) => {
|
||||
return data.params.idx.status === 'open';
|
||||
}),
|
||||
new ContextmenuItem('Delete', 'es.contextmenu.index.Delete')
|
||||
.withIcon('Delete')
|
||||
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onIdxDelete(nodeData)),
|
||||
new ContextmenuItem('BaseSearch', 'es.contextmenu.index.BaseSearch')
|
||||
.withIcon('Search')
|
||||
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onIdxBaseSearch(nodeData)),
|
||||
// new ContextmenuItem('SeniorSearch', 'es.contextmenu.index.SeniorSearch').withIcon('Search').withOnClick((data: any) => onIdxSeniorSearch(data)),
|
||||
new ContextmenuItem('IndexDetail', 'es.indexDetail')
|
||||
.withIcon('InfoFilled')
|
||||
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onIndexDetail(nodeData)),
|
||||
])
|
||||
|
||||
.withNodeClickFunc(async (nodeData: TagTreeNode) => {
|
||||
const params = nodeData.params;
|
||||
(await nodeData.ctx?.addResourceComponent(EsOpComp)).loadIndexData(params.params.inst.id, params);
|
||||
const NodeTypeInst = new NodeType(1).withNodeClickFunc(async (nodeData: TagTreeNode) => {
|
||||
const inst = nodeData.params;
|
||||
const tabKey = `${inst.code}`;
|
||||
createResourceOpTab({
|
||||
key: tabKey,
|
||||
name: inst.name,
|
||||
component: EsDashboard,
|
||||
componentProps: {
|
||||
instId: inst.id,
|
||||
},
|
||||
tabComponentProps: { icon: Icon },
|
||||
});
|
||||
});
|
||||
|
||||
export default {
|
||||
order: 5,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer :title="title" v-model="dialogVisible" :before-close="onCancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||
<el-drawer :append-to-body="false" :title="title" v-model="dialogVisible" :before-close="onCancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="onCancel" />
|
||||
</template>
|
||||
|
||||
@@ -262,7 +262,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { TagResourceTypePath } from '@/common/commonEnum';
|
||||
import { formatByteSize, formatDate } from '@/common/utils/format';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
@@ -272,7 +271,6 @@ import { Msg, useI18nDeleteConfirm } from '@/hooks/useI18n';
|
||||
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { getTagPathSearchItem } from '../component/tag';
|
||||
import TagCodePath from '../component/TagCodePath.vue';
|
||||
import { getMachineTerminalSocketUrl, machineApi } from './api';
|
||||
import { MachineProtocolEnum } from './enums';
|
||||
@@ -310,10 +308,7 @@ const perms = {
|
||||
terminal: 'machine:terminal',
|
||||
};
|
||||
|
||||
const searchItems = [
|
||||
SearchItem.input('keyword', 'common.keyword').withPlaceholder('machine.keywordPlaceholder'),
|
||||
getTagPathSearchItem(TagResourceTypePath.MachineAuthCert),
|
||||
];
|
||||
const searchItems = [SearchItem.input('keyword', 'common.keyword').withPlaceholder('machine.keywordPlaceholder')];
|
||||
|
||||
const columns = [
|
||||
TableColumn.new('name', 'common.name').isSlot('name').setAddWidth(15),
|
||||
|
||||
@@ -97,14 +97,18 @@ const state = reactive({
|
||||
|
||||
const { dialogVisible, query, playerDialogVisible, execCmdsDialogVisible } = toRefs(state);
|
||||
|
||||
watch(props, async (newValue: any) => {
|
||||
const visible = newValue.visible;
|
||||
state.dialogVisible = visible;
|
||||
if (visible) {
|
||||
state.query.machineId = newValue.machineId;
|
||||
state.title = newValue.title;
|
||||
}
|
||||
});
|
||||
watch(
|
||||
props,
|
||||
async (newValue: any) => {
|
||||
const visible = newValue.visible;
|
||||
state.dialogVisible = visible;
|
||||
if (visible) {
|
||||
state.query.machineId = newValue.machineId;
|
||||
state.title = newValue.title;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const getTermOps = async () => {
|
||||
pageTableRef.value.search();
|
||||
|
||||
@@ -105,21 +105,25 @@ const state = reactive({
|
||||
|
||||
const { dialogVisible, stats, netInter } = toRefs(state);
|
||||
|
||||
watch(props, async (newValue: any) => {
|
||||
const visible = newValue.visible;
|
||||
if (visible) {
|
||||
await setStats();
|
||||
}
|
||||
state.dialogVisible = visible;
|
||||
if (visible) {
|
||||
initCharts();
|
||||
}
|
||||
});
|
||||
|
||||
const setStats = async () => {
|
||||
state.stats = await machineApi.stats.request({ id: props.machineId });
|
||||
};
|
||||
|
||||
watch(
|
||||
props,
|
||||
async (newValue: any) => {
|
||||
const visible = newValue.visible;
|
||||
if (visible) {
|
||||
await setStats();
|
||||
}
|
||||
state.dialogVisible = visible;
|
||||
if (visible) {
|
||||
initCharts();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const onRefresh = async () => {
|
||||
await setStats();
|
||||
initCharts();
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
<template>
|
||||
<div class="file-manage">
|
||||
<el-dialog :title="$t('machine.process')" v-model="dialogVisible" :destroy-on-close="true" :show-close="true" :before-close="handleClose" width="65%">
|
||||
<el-dialog
|
||||
:title="$t('machine.process') + `: ${title}`"
|
||||
v-model="dialogVisible"
|
||||
:destroy-on-close="true"
|
||||
:show-close="true"
|
||||
:before-close="handleClose"
|
||||
width="65%"
|
||||
>
|
||||
<div class="card p-1!">
|
||||
<el-row>
|
||||
<el-col :span="4">
|
||||
@@ -123,14 +130,6 @@ const state = reactive({
|
||||
|
||||
const { dialogVisible, params, processList } = toRefs(state);
|
||||
|
||||
watch(props, (newValue) => {
|
||||
if (props.machineId) {
|
||||
state.params.id = props.machineId;
|
||||
getProcess();
|
||||
}
|
||||
state.dialogVisible = newValue.visible;
|
||||
});
|
||||
|
||||
const getProcess = async () => {
|
||||
const res = await machineApi.process.request(state.params);
|
||||
// 解析字符串
|
||||
@@ -173,6 +172,18 @@ const getProcess = async () => {
|
||||
state.processList = ps as any;
|
||||
};
|
||||
|
||||
watch(
|
||||
props,
|
||||
(newValue) => {
|
||||
if (props.machineId) {
|
||||
state.params.id = props.machineId;
|
||||
getProcess();
|
||||
}
|
||||
state.dialogVisible = newValue.visible;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const confirmKillProcess = async (pid: any) => {
|
||||
await machineApi.killProcess.request({
|
||||
pid,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer
|
||||
:append-to-body="false"
|
||||
:title="title"
|
||||
v-model="dialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
width="80%"
|
||||
:close-on-click-modal="false"
|
||||
:modal="false"
|
||||
@close="closeTermnial"
|
||||
@close="closeTerminal"
|
||||
body-class="h-[65vh]"
|
||||
draggable
|
||||
append-to-body
|
||||
@@ -98,10 +98,9 @@
|
||||
import { DynamicFormDialog } from '@/components/dynamic-form';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { OptionsApi } from '@/components/pagetable/SearchForm/index';
|
||||
import { SearchItem, OptionsApi } from '@/components/pagetable/SearchForm';
|
||||
import { Msg, useI18nCreateTitle, useI18nDeleteConfirm, useI18nEditTitle } from '@/hooks/useI18n';
|
||||
import { defineAsyncComponent, reactive, ref, Ref, toRefs } from 'vue';
|
||||
import { defineAsyncComponent, nextTick, onMounted, reactive, ref, Ref, toRefs, watch } from 'vue';
|
||||
import { getMachineTerminalSocketUrl, machineApi } from './api';
|
||||
import { ScriptResultEnum, ScriptTypeEnum } from './enums';
|
||||
|
||||
@@ -174,7 +173,7 @@ const state = reactive({
|
||||
const { columns, selectionData, query, editDialog, scriptParamsDialog, resultDialog, terminalDialog } = toRefs(state);
|
||||
|
||||
const getScripts = async () => {
|
||||
pageTableRef.value.search();
|
||||
pageTableRef.value?.search();
|
||||
};
|
||||
|
||||
const checkScriptType = (query: any) => {
|
||||
@@ -215,7 +214,7 @@ const run = async (script: any) => {
|
||||
if (script.type == ScriptResultEnum.Result.value || noResult) {
|
||||
const res = await machineApi.runScript.request({
|
||||
machineId: props.machineId,
|
||||
ac: props.authCertName,
|
||||
acName: props.authCertName,
|
||||
scriptId: script.id,
|
||||
params: JSON.stringify(state.scriptParamsDialog.params),
|
||||
});
|
||||
@@ -254,7 +253,7 @@ function templateResolve(template: string, param: any) {
|
||||
});
|
||||
}
|
||||
|
||||
const closeTermnial = () => {
|
||||
const closeTerminal = () => {
|
||||
state.terminalDialog.visible = false;
|
||||
};
|
||||
|
||||
@@ -293,5 +292,9 @@ const handleClose = () => {
|
||||
state.query.type = ScriptTypeEnum.Private.value;
|
||||
state.scriptParamsDialog.paramsFormItem = [];
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(getScripts);
|
||||
});
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -24,7 +24,7 @@ export const machineApi = {
|
||||
del: Api.newDelete('/machines/{id}'),
|
||||
scripts: Api.newGet('/machines/{machineId}/scripts'),
|
||||
scriptCategorys: Api.newGet('/machines/scripts/categorys'),
|
||||
runScript: Api.newGet('/machines/scripts/{scriptId}/{ac}/run'),
|
||||
runScript: Api.newGet('/machines/scripts/{scriptId}/{acName}/run'),
|
||||
saveScript: Api.newPost('/machines/{machineId}/scripts'),
|
||||
deleteScript: Api.newDelete('/machines/{machineId}/scripts/{scriptId}'),
|
||||
// 获取配置文件列表
|
||||
@@ -117,8 +117,8 @@ export function uploadFile(file: File, params: UploadParams, options: UploadOpti
|
||||
// 业务层生成 uploadId
|
||||
const uploadId = params.uploadId || `upload_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
||||
|
||||
// 构建查询参数
|
||||
const queryParams = new URLSearchParams({
|
||||
// 构建查询参数对象
|
||||
const queryParams: Record<string, string> = {
|
||||
machineId: String(params.machineId),
|
||||
authCertName: params.authCertName,
|
||||
protocol: String(params.protocol),
|
||||
@@ -126,15 +126,15 @@ export function uploadFile(file: File, params: UploadParams, options: UploadOpti
|
||||
path: params.path,
|
||||
uploadId: uploadId,
|
||||
filename: file.name,
|
||||
});
|
||||
};
|
||||
|
||||
// 如果是文件夹上传,添加标识参数
|
||||
if (params.isFolderUpload) {
|
||||
queryParams.set('isFolderUpload', 'true');
|
||||
queryParams['isFolderUpload'] = 'true';
|
||||
}
|
||||
|
||||
// 直接使用文件流作为 body,不包装为 FormData
|
||||
const { abort } = machineApi.uploadFile.uploadRaw(file, queryParams.toString(), {
|
||||
const { abort } = machineApi.uploadFile.uploadRaw(file, queryParams, {
|
||||
...options,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="mock-data-dialog">
|
||||
<el-drawer
|
||||
:append-to-body="false"
|
||||
:title="title"
|
||||
v-model="dialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
</el-dialog>
|
||||
|
||||
<el-drawer
|
||||
:append-to-body="false"
|
||||
resizable
|
||||
destroy-on-close
|
||||
:title="fileDialog.title"
|
||||
@@ -74,7 +75,7 @@
|
||||
<script lang="ts" setup>
|
||||
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
|
||||
import { Msg, useI18nDeleteConfirm } from '@/hooks/useI18n';
|
||||
import { defineAsyncComponent, reactive, toRefs, watch } from 'vue';
|
||||
import {defineAsyncComponent, onMounted, reactive, toRefs, watch} from 'vue';
|
||||
import { machineApi } from '../api';
|
||||
import { FileTypeEnum } from '../enums';
|
||||
|
||||
@@ -137,8 +138,11 @@ watch(props, async (newValue) => {
|
||||
|
||||
const getFiles = async () => {
|
||||
try {
|
||||
state.loading = true;
|
||||
state.query.id = props.machineId as any;
|
||||
if (!state.query.id){
|
||||
return
|
||||
}
|
||||
state.loading = true;
|
||||
const res = await files.request(state.query);
|
||||
state.fileTable = res.list || [];
|
||||
state.total = res.total;
|
||||
@@ -147,6 +151,8 @@ const getFiles = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(getFiles)
|
||||
|
||||
const handlePageChange = (curPage: number) => {
|
||||
state.query.pageNum = curPage;
|
||||
getFiles();
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<el-button class="!ml-1" type="primary" circle size="small" icon="Refresh" @click="refresh()"> </el-button>
|
||||
|
||||
<!-- 文件&文件夹上传 -->
|
||||
<el-dropdown class="machine-file-upload-exec" trigger="click" size="small">
|
||||
<el-dropdown class="machine-file-upload-exec" trigger="click" size="small" :teleported="false">
|
||||
<span>
|
||||
<el-button
|
||||
v-auth="'machine:file:upload'"
|
||||
@@ -131,7 +131,7 @@
|
||||
</el-button>
|
||||
|
||||
<el-button-group v-if="state.copyOrMvFile.paths.length > 0" size="small" class="!ml-1">
|
||||
<el-tooltip effect="customized" raw-content placement="top">
|
||||
<el-tooltip effect="customized" raw-content placement="top" :teleported="false">
|
||||
<template #content>
|
||||
<div v-for="path in state.copyOrMvFile.paths" v-bind:key="path">{{ path }}</div>
|
||||
</template>
|
||||
@@ -153,24 +153,26 @@
|
||||
</template>
|
||||
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.isFolder">
|
||||
<SvgIcon :size="15" name="folder" color="#007AFF" />
|
||||
</span>
|
||||
<span v-else>
|
||||
<SvgIcon :size="15" :name="scope.row.icon" />
|
||||
</span>
|
||||
<div class="w-full cursor-pointer" @click="getFile(scope.row)">
|
||||
<span v-if="scope.row.isFolder">
|
||||
<SvgIcon :size="15" name="folder" color="#007AFF" />
|
||||
</span>
|
||||
<span v-else>
|
||||
<SvgIcon :size="15" :name="scope.row.icon" />
|
||||
</span>
|
||||
|
||||
<span class="!ml-1 inline-block w-[90%]">
|
||||
<div v-if="scope.row.nameEdit">
|
||||
<el-input
|
||||
@keyup.enter="fileRename(scope.row)"
|
||||
:ref="(el: any) => el?.focus()"
|
||||
@blur="filenameBlur(scope.row)"
|
||||
v-model="scope.row.name"
|
||||
/>
|
||||
</div>
|
||||
<el-link v-else @click="getFile(scope.row)" style="font-weight: bold" underline="never">{{ scope.row.name }}</el-link>
|
||||
</span>
|
||||
<span class="!ml-1 inline-block w-[90%]">
|
||||
<div v-if="scope.row.nameEdit">
|
||||
<el-input
|
||||
@keyup.enter="fileRename(scope.row)"
|
||||
:ref="(el: any) => el?.focus()"
|
||||
@blur="filenameBlur(scope.row)"
|
||||
v-model="scope.row.name"
|
||||
/>
|
||||
</div>
|
||||
<el-link v-else style="font-weight: bold" underline="never">{{ scope.row.name }}</el-link>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -204,7 +206,7 @@
|
||||
|
||||
<el-table-column :width="100">
|
||||
<template #header>
|
||||
<el-popover placement="top" :width="270" trigger="hover">
|
||||
<el-popover placement="top" :width="270" trigger="hover" :teleported="false">
|
||||
<template #reference>
|
||||
<SvgIcon name="QuestionFilled" :size="18" class="pointer-icon mr-2" />
|
||||
</template>
|
||||
@@ -308,7 +310,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { Msg } from '@/hooks/useI18n';
|
||||
import { ElInput } from 'element-plus';
|
||||
import { computed, defineAsyncComponent, onMounted, reactive, ref, toRefs } from 'vue';
|
||||
import { computed, defineAsyncComponent, getCurrentInstance, onMounted, reactive, ref, toRefs } from 'vue';
|
||||
import { machineApi, uploadFile, uploadFolder } from '../api';
|
||||
|
||||
import { isTrue, notBlank } from '@/common/assert';
|
||||
@@ -333,6 +335,7 @@ const props = defineProps({
|
||||
fileId: { type: Number, default: 0 },
|
||||
path: { type: String, default: '' },
|
||||
isFolder: { type: Boolean, default: true },
|
||||
tabKey: { type: String, default: '' },
|
||||
});
|
||||
|
||||
const token = getToken();
|
||||
@@ -379,7 +382,12 @@ const state = reactive({
|
||||
|
||||
const { basePath, nowPath, loading, fileNameFilter, fileContent, createFileDialog } = toRefs(state);
|
||||
|
||||
const emits = defineEmits(['init']);
|
||||
|
||||
// Init as MachineOp component
|
||||
onMounted(async () => {
|
||||
emits('init', { name: 'tag.machineOp', tabKey: props.tabKey, ref: getCurrentInstance()?.exposed });
|
||||
|
||||
state.basePath = props.path;
|
||||
const machineId = props.machineId;
|
||||
|
||||
@@ -877,7 +885,7 @@ const dontOperate = (data: any) => {
|
||||
return ls.indexOf(path) != -1;
|
||||
};
|
||||
|
||||
defineExpose({ showFileContent });
|
||||
defineExpose({ showFileContent, onRefresh: refresh });
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.machine-file-upload-exec {
|
||||
|
||||
@@ -1,487 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full machine-terminal-tabs">
|
||||
<el-tabs v-if="state.tabs.size > 0" type="card" @tab-remove="onRemoveTab" v-model="state.activeTermName" class="!h-full w-full">
|
||||
<el-tab-pane class="h-full! flex flex-col" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
|
||||
<template #label>
|
||||
<el-popconfirm @confirm="handleReconnect(dt, true)" :title="$t('machine.reConnTips')" v-if="dt.type === 'terminal'">
|
||||
<template #reference>
|
||||
<el-icon
|
||||
class="mr-1"
|
||||
:color="EnumValue.getEnumByValue(TerminalStatusEnum, dt.status)?.extra?.iconColor"
|
||||
:title="dt.status == TerminalStatusEnum.Connected.value ? '' : $t('machine.clickReConn')"
|
||||
>
|
||||
<Connection />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
<el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
|
||||
<template #reference>
|
||||
<div>
|
||||
<span class="machine-terminal-tab-label">{{ dt.label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<el-descriptions :column="1" size="small">
|
||||
<el-descriptions-item :label="$t('common.name')"> {{ dt.params?.name }} </el-descriptions-item>
|
||||
<el-descriptions-item label="host"> {{ dt.params?.ip }} : {{ dt.params?.port }} </el-descriptions-item>
|
||||
<el-descriptions-item label="username"> {{ dt.params?.selectAuthCert.username }} </el-descriptions-item>
|
||||
<el-descriptions-item label="remark"> {{ dt.params?.remark }} </el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<!-- 终端类型 tab -->
|
||||
<div v-if="dt.type === 'terminal'" class="terminal-wrapper flex-1 min-h-0">
|
||||
<TerminalBody
|
||||
v-if="dt.params.protocol == MachineProtocolEnum.Ssh.value"
|
||||
:mount-init="false"
|
||||
@status-change="terminalStatusChange(dt.key, $event)"
|
||||
:ref="(el: any) => setTerminalRef(el, dt.key)"
|
||||
:socket-url="dt.socketUrl"
|
||||
:machine-id="dt.params.id"
|
||||
:auth-cert-name="dt.authCert"
|
||||
:file-id="0"
|
||||
:protocol="dt.params.protocol"
|
||||
/>
|
||||
<machine-rdp
|
||||
v-if="dt.params.protocol != MachineProtocolEnum.Ssh.value"
|
||||
:machine-id="dt.params.id"
|
||||
:auth-cert="dt.authCert"
|
||||
:protocol="dt.params.protocol"
|
||||
:ref="(el: any) => setTerminalRef(el, dt.key)"
|
||||
@status-change="terminalStatusChange(dt.key, $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 文件操作类型 tab -->
|
||||
<div v-if="dt.type === 'file'" class="file-wrapper flex-1 min-h-0">
|
||||
<machine-file :machine-id="dt.machineId" :auth-cert-name="dt.authCertName" :protocol="dt.protocol" :file-id="dt.fileId" :path="dt.path" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-dialog v-if="infoDialog.visible" v-model="infoDialog.visible">
|
||||
<el-descriptions :title="$t('common.detail')" :column="3" border>
|
||||
<el-descriptions-item :span="1.5" label="ID">{{ infoDialog.data.id }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1.5" :label="$t('common.name')">{{ infoDialog.data.name }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" :label="$t('tag.relateTag')">
|
||||
<TagCodePath :path="infoDialog.data.tags" />
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="IP">{{ infoDialog.data.ip }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('machine.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="1.5" :label="$t('machine.sshTunnel')"
|
||||
>{{ infoDialog.data.sshTunnelMachineId > 0 ? $t('common.yes') : $t('common.no') }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :span="1.5" :label="$t('machine.terminalPlayback')"
|
||||
>{{ infoDialog.data.enableRecorder == 1 ? $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>
|
||||
|
||||
<process-list v-model:visible="processDialog.visible" v-model:machineId="processDialog.machineId" />
|
||||
|
||||
<script-manage
|
||||
:title="serviceDialog.title"
|
||||
v-model:visible="serviceDialog.visible"
|
||||
v-model:machineId="serviceDialog.machineId"
|
||||
:auth-cert-name="serviceDialog.authCertName"
|
||||
/>
|
||||
|
||||
<file-conf-list
|
||||
v-model:visible="fileDialog.visible"
|
||||
:machine-id="fileDialog.machine?.id"
|
||||
:auth-cert-name="fileDialog.machine?.selectAuthCert?.name"
|
||||
:protocol="fileDialog.machine?.protocol"
|
||||
:open-file-manager="false"
|
||||
@select="onFileConfigSelect"
|
||||
/>
|
||||
|
||||
<machine-stats v-model:visible="machineStatsDialog.visible" :machineId="machineStatsDialog.machineId" :title="machineStatsDialog.title" />
|
||||
|
||||
<machine-rec v-model:visible="machineRecDialog.visible" :machineId="machineRecDialog.machineId" :title="machineRecDialog.title" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import EnumValue from '@/common/Enum';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import MachineRdp from '@/components/terminal-rdp/MachineRdp.vue';
|
||||
import TerminalBody from '@/components/terminal/TerminalBody.vue';
|
||||
import { TerminalStatus, TerminalStatusEnum } from '@/components/terminal/common';
|
||||
import { ResourceOpCtx } from '@/views/ops/component/tag';
|
||||
import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
|
||||
import { MachineOpComp } from '@/views/ops/machine/resource';
|
||||
import { ResourceOpCtxKey } from '@/views/ops/resource/resource';
|
||||
import { defineAsyncComponent, getCurrentInstance, inject, nextTick, onMounted, reactive, toRefs, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import TagCodePath from '../../component/TagCodePath.vue';
|
||||
import { getMachineTerminalSocketUrl } from '../api';
|
||||
import { MachineProtocolEnum } from '../enums';
|
||||
|
||||
// 组件
|
||||
const ScriptManage = defineAsyncComponent(() => import('../ScriptManage.vue'));
|
||||
const FileConfList = defineAsyncComponent(() => import('../file/FileConfList.vue'));
|
||||
const MachineStats = defineAsyncComponent(() => import('../MachineStats.vue'));
|
||||
const MachineRec = defineAsyncComponent(() => import('../MachineRec.vue'));
|
||||
const ProcessList = defineAsyncComponent(() => import('../ProcessList.vue'));
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 机器信息类型定义
|
||||
interface MachineInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
protocol: number;
|
||||
remark?: string;
|
||||
selectAuthCert: {
|
||||
name: string;
|
||||
username: string;
|
||||
};
|
||||
}
|
||||
|
||||
const perms = {
|
||||
addMachine: 'machine:add',
|
||||
updateMachine: 'machine:update',
|
||||
delMachine: 'machine:del',
|
||||
terminal: 'machine:terminal',
|
||||
closeCli: 'machine:close-cli',
|
||||
};
|
||||
|
||||
// 该用户拥有的的操作列按钮权限,使用v-if进行判断,v-auth对el-dropdown-item无效
|
||||
const actionBtns = hasPerms([perms.updateMachine, perms.closeCli]);
|
||||
|
||||
const emits = defineEmits(['init']);
|
||||
|
||||
const resourceOpCtx: ResourceOpCtx | undefined = inject(ResourceOpCtxKey);
|
||||
|
||||
const state = reactive({
|
||||
defaultExpendKey: [] as any,
|
||||
params: {
|
||||
pageNum: 1,
|
||||
pageSize: 0,
|
||||
ip: null,
|
||||
name: null,
|
||||
tagPath: '',
|
||||
},
|
||||
infoDialog: {
|
||||
visible: false,
|
||||
data: null as any,
|
||||
},
|
||||
serviceDialog: {
|
||||
visible: false,
|
||||
machineId: 0,
|
||||
authCertName: '',
|
||||
title: '',
|
||||
},
|
||||
processDialog: {
|
||||
visible: false,
|
||||
machineId: 0,
|
||||
},
|
||||
fileDialog: {
|
||||
visible: false,
|
||||
machine: null as MachineInfo | null,
|
||||
},
|
||||
machineStatsDialog: {
|
||||
visible: false,
|
||||
stats: null,
|
||||
title: '',
|
||||
machineId: 0,
|
||||
},
|
||||
machineRecDialog: {
|
||||
visible: false,
|
||||
machineId: 0,
|
||||
title: '',
|
||||
},
|
||||
activeTermName: '',
|
||||
tabs: new Map<string, any>(),
|
||||
});
|
||||
|
||||
const { infoDialog, serviceDialog, processDialog, fileDialog, machineStatsDialog, machineRecDialog } = toRefs(state);
|
||||
|
||||
let openIds: any = {};
|
||||
|
||||
watch(
|
||||
() => state.activeTermName,
|
||||
(newValue, oldValue) => {
|
||||
fitTerminal();
|
||||
|
||||
// 只有终端类型才需要 blur/focus
|
||||
const oldTab = state.tabs.get(oldValue);
|
||||
const newTab = state.tabs.get(newValue);
|
||||
|
||||
if (oldTab?.type === 'terminal') {
|
||||
terminalRefs[oldValue]?.blur && terminalRefs[oldValue]?.blur();
|
||||
}
|
||||
if (newTab?.type === 'terminal') {
|
||||
terminalRefs[newValue]?.focus && terminalRefs[newValue]?.focus();
|
||||
}
|
||||
|
||||
resourceOpCtx?.setCurrentTreeKey(newTab?.authCert || newTab?.authCertName);
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
emits('init', { name: MachineOpComp.name, ref: getCurrentInstance()?.exposed });
|
||||
});
|
||||
|
||||
const openTerminal = (machine: any, ex?: boolean) => {
|
||||
// 授权凭证名
|
||||
const ac = machine.selectAuthCert.name;
|
||||
|
||||
// 新窗口打开
|
||||
if (ex) {
|
||||
if (machine.protocol == MachineProtocolEnum.Ssh.value) {
|
||||
const { href } = router.resolve({
|
||||
path: `/machine/terminal`,
|
||||
query: {
|
||||
ac,
|
||||
name: machine.name,
|
||||
},
|
||||
});
|
||||
window.open(href, '_blank');
|
||||
return;
|
||||
}
|
||||
if (machine.protocol == MachineProtocolEnum.Rdp.value) {
|
||||
const { href } = router.resolve({
|
||||
path: `/machine/terminal-rdp`,
|
||||
query: {
|
||||
machineId: machine.id,
|
||||
ac: ac,
|
||||
name: machine.name,
|
||||
},
|
||||
});
|
||||
window.open(href, '_blank');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let { name } = machine;
|
||||
const labelName = `${machine.selectAuthCert.username}@${name}`;
|
||||
|
||||
// 同一个机器的终端打开多次,key后添加下划线和数字区分
|
||||
openIds[ac] = openIds[ac] ? ++openIds[ac] : 1;
|
||||
let sameIndex = openIds[ac];
|
||||
|
||||
let key = `${ac}_${sameIndex}`;
|
||||
// 只保留name的15个字,超出部分只保留前后10个字符,中间用省略号代替
|
||||
const label = labelName.length > 15 ? labelName.slice(0, 10) + '...' + labelName.slice(-10) : labelName;
|
||||
|
||||
let tab = {
|
||||
key,
|
||||
label: `${label}${sameIndex === 1 ? '' : ':' + sameIndex}`, // label组成为:总打开term次数+name+同一个机器打开的次数
|
||||
type: 'terminal',
|
||||
params: machine,
|
||||
authCert: ac,
|
||||
socketUrl: getMachineTerminalSocketUrl(ac),
|
||||
status: TerminalStatusEnum.Disconnected.value,
|
||||
};
|
||||
|
||||
state.tabs.set(key, tab);
|
||||
|
||||
nextTick(() => {
|
||||
handleReconnect(tab);
|
||||
state.activeTermName = key;
|
||||
setTimeout(() => fitTerminal(), 300);
|
||||
});
|
||||
};
|
||||
|
||||
const serviceManager = (row: any) => {
|
||||
const authCert = row.selectAuthCert;
|
||||
state.serviceDialog.machineId = row.id;
|
||||
state.serviceDialog.visible = true;
|
||||
state.serviceDialog.authCertName = authCert.name;
|
||||
state.serviceDialog.title = `${row.name} => ${authCert.username}@${row.ip}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 显示机器状态统计信息
|
||||
*/
|
||||
const showMachineStats = (machine: any) => {
|
||||
state.machineStatsDialog.machineId = machine.id;
|
||||
state.machineStatsDialog.title = `${t('machine.machineState')}: ${machine.name} => ${machine.ip}`;
|
||||
state.machineStatsDialog.visible = true;
|
||||
};
|
||||
|
||||
const showFileManage = (selectionData: any) => {
|
||||
state.fileDialog.machine = selectionData;
|
||||
state.fileDialog.visible = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理文件配置选择事件
|
||||
*/
|
||||
const onFileConfigSelect = (fileConfig: { fileId: number; path: string; name: string; type: number }) => {
|
||||
const machine = state.fileDialog.machine;
|
||||
if (!machine) return;
|
||||
|
||||
// 获取当前机器信息
|
||||
const machineId = machine.id;
|
||||
const authCertName = machine.selectAuthCert.name;
|
||||
|
||||
// 生成文件操作 tab 的 key
|
||||
const fileTabKey = `file_${machineId}_${authCertName}_${fileConfig.fileId}`;
|
||||
|
||||
// 检查是否已经存在该文件操作 tab
|
||||
if (state.tabs.has(fileTabKey)) {
|
||||
// 如果已存在,直接切换到该 tab
|
||||
state.activeTermName = fileTabKey;
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用国际化前缀拼接 tab 标签
|
||||
const labelName = `${t('machine.fileTabPrefix')}${machine.selectAuthCert.username}@${machine.name}/${fileConfig.name}`;
|
||||
|
||||
let tab = {
|
||||
key: fileTabKey,
|
||||
label: labelName.length > 25 ? labelName.slice(0, 18) + '...' + labelName.slice(-7) : labelName,
|
||||
type: 'file',
|
||||
machineId: machineId,
|
||||
authCertName: authCertName,
|
||||
protocol: machine.protocol,
|
||||
fileId: fileConfig.fileId,
|
||||
path: fileConfig.path,
|
||||
params: machine,
|
||||
};
|
||||
|
||||
state.tabs.set(fileTabKey, tab);
|
||||
state.activeTermName = fileTabKey;
|
||||
};
|
||||
|
||||
const showInfo = (info: any) => {
|
||||
state.infoDialog.data = info;
|
||||
state.infoDialog.visible = true;
|
||||
};
|
||||
|
||||
const showProcess = (row: any) => {
|
||||
state.processDialog.machineId = row.id;
|
||||
state.processDialog.visible = true;
|
||||
};
|
||||
|
||||
const showRec = (row: any) => {
|
||||
state.machineRecDialog.title = `${row.name}[${row.ip}]-${t('machine.terminalPlayback')}`;
|
||||
state.machineRecDialog.machineId = row.id;
|
||||
state.machineRecDialog.visible = true;
|
||||
};
|
||||
|
||||
const onRemoveTab = (targetName: string) => {
|
||||
let activeTermName = state.activeTermName;
|
||||
const tabNames = [...state.tabs.keys()];
|
||||
for (let i = 0; i < tabNames.length; i++) {
|
||||
const tabName = tabNames[i];
|
||||
if (tabName !== targetName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tab = state.tabs.get(targetName);
|
||||
|
||||
// 只有终端类型才需要关闭连接
|
||||
if (tab?.type === 'terminal') {
|
||||
terminalRefs[targetName]?.close();
|
||||
}
|
||||
|
||||
state.tabs.delete(targetName);
|
||||
|
||||
if (activeTermName != targetName) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 如果删除的 tab 是当前激活的 tab,则切换到前一个或后一个 tab
|
||||
const nextTab = tabNames[i + 1] || tabNames[i - 1];
|
||||
if (nextTab) {
|
||||
activeTermName = nextTab;
|
||||
} else {
|
||||
activeTermName = '';
|
||||
}
|
||||
|
||||
state.activeTermName = activeTermName;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const terminalStatusChange = (key: string, status: TerminalStatus) => {
|
||||
state.tabs.get(key).status = status;
|
||||
};
|
||||
|
||||
const terminalRefs: any = {};
|
||||
const setTerminalRef = (el: any, key: any) => {
|
||||
if (key) {
|
||||
terminalRefs[key] = el;
|
||||
}
|
||||
};
|
||||
|
||||
const fitTerminal = () => {
|
||||
setTimeout(() => {
|
||||
let info = state.tabs.get(state.activeTermName);
|
||||
// 只有终端类型才需要调整大小
|
||||
if (info && info.type === 'terminal') {
|
||||
terminalRefs[info.key]?.fitTerminal && terminalRefs[info.key]?.fitTerminal();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleReconnect = (tab: any, force = false) => {
|
||||
// 只有终端类型才需要重连
|
||||
if (tab?.type === 'terminal') {
|
||||
terminalRefs[tab.key]?.init();
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
openTerminal,
|
||||
onResize: fitTerminal,
|
||||
showInfo,
|
||||
showProcess,
|
||||
showRec,
|
||||
showMachineStats,
|
||||
showFileManage,
|
||||
serviceManager,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.machine-terminal-tabs {
|
||||
--el-tabs-header-height: 30px;
|
||||
|
||||
.el-tabs {
|
||||
--el-tabs-header-height: 30px;
|
||||
}
|
||||
|
||||
.machine-terminal-tab-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.el-tabs__header {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
padding: 0 8px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +1,22 @@
|
||||
import { ContextmenuItem } from '@/components/contextmenu';
|
||||
import { ResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { NodeType, TagTreeNode, ResourceComponentConfig, ResourceConfig } from '@/views/ops/component/tag';
|
||||
import { ContextmenuItem } from '@/components/contextmenu';
|
||||
import router from '@/router';
|
||||
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
|
||||
import { machineApi } from '@/views/ops/machine/api';
|
||||
import { MachineProtocolEnum } from '@/views/ops/machine/enums';
|
||||
import type { ResourceConfig } from '@/views/ops/resource/resource';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { createResourceOpTab, showResourceOpOverlay } from '../../resource/resourceOp';
|
||||
|
||||
const MachineList = defineAsyncComponent(() => import('../MachineList.vue'));
|
||||
const MachineOp = defineAsyncComponent(() => import('./MachineOp.vue'));
|
||||
|
||||
const TerminalTab = defineAsyncComponent(() => import('./tabs/TerminalTab.vue'));
|
||||
const TerminalTabLabel = defineAsyncComponent(() => import('./tabs/TerminalTabLabel.vue'));
|
||||
const FileTab = defineAsyncComponent(() => import('./tabs/FileTab.vue'));
|
||||
const ScriptManage = defineAsyncComponent(() => import('../ScriptManage.vue'));
|
||||
const MachineDetailDialog = defineAsyncComponent(() => import('./tabs/MachineDetailDialog.vue'));
|
||||
const MachineStats = defineAsyncComponent(() => import('../MachineStats.vue'));
|
||||
const ProcessList = defineAsyncComponent(() => import('../ProcessList.vue'));
|
||||
const MachineRec = defineAsyncComponent(() => import('../MachineRec.vue'));
|
||||
const NodeMachineAc = defineAsyncComponent(() => import('./NodeMachineAc.vue'));
|
||||
|
||||
const MachineIcon = {
|
||||
@@ -15,21 +24,14 @@ const MachineIcon = {
|
||||
color: ResourceTypeEnum.Machine.extra.iconColor,
|
||||
};
|
||||
|
||||
export const MachineOpComp: ResourceComponentConfig = {
|
||||
name: 'tag.machineOp',
|
||||
component: MachineOp,
|
||||
icon: MachineIcon,
|
||||
};
|
||||
const FileIcon = { name: 'FolderOpened', color: '#E6A23C' };
|
||||
|
||||
export const NodeTypeMachineTag = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (node: TagTreeNode) => {
|
||||
node.ctx?.addResourceComponent(MachineOpComp);
|
||||
// 加载标签树下的机器列表
|
||||
const res = await machineApi.list.request({ tagPath: node.params.tagPath });
|
||||
// 把list 根据name字段排序
|
||||
return res?.list
|
||||
.sort((a: any, b: any) => a.name.localeCompare(b.name))
|
||||
.map((x: any) =>
|
||||
TagTreeNode.new(node, x.code, x.name, NodeTypeMachine)
|
||||
TagTreeNode.new(node, `${x.code}`, x.name, NodeTypeMachine)
|
||||
.withParams(x)
|
||||
.withDisabled(x.status == -1 && x.protocol == MachineProtocolEnum.Ssh.value)
|
||||
.withIcon(MachineIcon)
|
||||
@@ -39,10 +41,9 @@ export const NodeTypeMachineTag = new NodeType(TagTreeNode.TagPath).withLoadNode
|
||||
export const NodeTypeMachine = new NodeType(11)
|
||||
.withLoadNodesFunc((node: TagTreeNode) => {
|
||||
const machine = node.params;
|
||||
// 获取授权凭证列表
|
||||
const authCerts = machine.authCerts;
|
||||
return authCerts.map((x: any) =>
|
||||
TagTreeNode.new(node, x.name, x.username, NodeTypeAuthCert)
|
||||
TagTreeNode.new(node, `${node.key}.${x.name}`, x.username, NodeTypeAuthCert)
|
||||
.withNodeComponent(NodeMachineAc)
|
||||
.withParams({ ...machine, selectAuthCert: x })
|
||||
.withDisabled(machine.status == -1 && machine.protocol == MachineProtocolEnum.Ssh.value)
|
||||
@@ -55,57 +56,167 @@ export const NodeTypeMachine = new NodeType(11)
|
||||
})
|
||||
.withContextMenuItems([
|
||||
new ContextmenuItem('detail', 'common.detail').withIcon('More').withOnClick(async (node: TagTreeNode) => {
|
||||
(await node.ctx?.addResourceComponent(MachineOpComp))?.showInfo(node.params);
|
||||
const m = node.params;
|
||||
|
||||
// 显示机器详情弹窗(已存在则更新 props,否则注册)
|
||||
showResourceOpOverlay('machine_detail', MachineDetailDialog, {
|
||||
code: m.code,
|
||||
});
|
||||
}),
|
||||
|
||||
new ContextmenuItem('status', 'common.status')
|
||||
.withIcon('Compass')
|
||||
.withHideFunc((node: any) => node.params.protocol != MachineProtocolEnum.Ssh.value)
|
||||
.withOnClick(async (node: TagTreeNode) => {
|
||||
(await node.ctx?.addResourceComponent(MachineOpComp))?.showMachineStats(node.params);
|
||||
const m = node.params;
|
||||
|
||||
// 显示机器状态弹窗
|
||||
showResourceOpOverlay('machine_stats', MachineStats, {
|
||||
machineId: m.id,
|
||||
title: `${m.name} => ${m.ip}`,
|
||||
});
|
||||
}),
|
||||
|
||||
new ContextmenuItem('process', 'machine.process')
|
||||
.withIcon('DataLine')
|
||||
.withHideFunc((node: any) => node.params.protocol != MachineProtocolEnum.Ssh.value)
|
||||
.withOnClick(async (node: TagTreeNode) => {
|
||||
(await node.ctx?.addResourceComponent(MachineOpComp))?.showProcess(node.params);
|
||||
const m = node.params;
|
||||
|
||||
// 显示进程列表弹窗
|
||||
showResourceOpOverlay('machine_process', ProcessList, {
|
||||
machineId: m.id,
|
||||
title: `${m.name} => ${m.ip}`,
|
||||
});
|
||||
}),
|
||||
|
||||
new ContextmenuItem('edit', 'machine.terminalPlayback')
|
||||
.withIcon('Compass')
|
||||
.withOnClick(async (node: TagTreeNode) => {
|
||||
(await node.ctx?.addResourceComponent(MachineOpComp))?.showRec(node.params);
|
||||
const m = node.params;
|
||||
|
||||
showResourceOpOverlay('machine_rec', MachineRec, {
|
||||
machineId: m.id,
|
||||
title: `${m.name} => ${m.ip}`,
|
||||
});
|
||||
})
|
||||
.withHideFunc((node: any) => node.params.enableRecorder == 1),
|
||||
]);
|
||||
|
||||
export const NodeTypeAuthCert = new NodeType(12)
|
||||
.withNodeDblclickFunc(async (node: TagTreeNode) => {
|
||||
(await node.ctx?.addResourceComponent(MachineOpComp))?.openTerminal(node.params);
|
||||
const m = node.params;
|
||||
|
||||
const key = `${m.code}.${m.selectAuthCert.name}.${new Date().getTime()}`;
|
||||
createResourceOpTab({
|
||||
key,
|
||||
name: `${m.selectAuthCert.username}@${m.name}`,
|
||||
|
||||
component: TerminalTab,
|
||||
componentProps: {
|
||||
tabKey: key,
|
||||
machineId: m.id,
|
||||
authCertName: m.selectAuthCert.name,
|
||||
protocol: m.protocol,
|
||||
},
|
||||
|
||||
tabComponent: TerminalTabLabel,
|
||||
tabComponentProps: {
|
||||
icon: MachineIcon,
|
||||
status: 'disconnected',
|
||||
},
|
||||
});
|
||||
})
|
||||
.withContextMenuItems([
|
||||
new ContextmenuItem('term', 'machine.openTerminal')
|
||||
.withIcon('Monitor')
|
||||
.withPermission('machine:terminal')
|
||||
.withOnClick(async (node: TagTreeNode) => {
|
||||
(await node.ctx?.addResourceComponent(MachineOpComp))?.openTerminal(node.params);
|
||||
const m = node.params;
|
||||
|
||||
const key = `${m.code}.${m.selectAuthCert.name}.${new Date().getTime()}`;
|
||||
createResourceOpTab({
|
||||
key,
|
||||
name: `${m.selectAuthCert.username}@${m.name}`,
|
||||
component: TerminalTab,
|
||||
tabComponent: TerminalTabLabel,
|
||||
tabComponentProps: {
|
||||
icon: MachineIcon,
|
||||
status: 'disconnected',
|
||||
},
|
||||
componentProps: {
|
||||
tabKey: key,
|
||||
machineId: m.id,
|
||||
authCertName: m.selectAuthCert.name,
|
||||
protocol: m.protocol,
|
||||
},
|
||||
});
|
||||
}),
|
||||
new ContextmenuItem('term-ex', 'machine.newTabOpenTerminal')
|
||||
.withIcon('Monitor')
|
||||
.withPermission('machine:terminal')
|
||||
.withOnClick(async (node: TagTreeNode) => {
|
||||
(await node.ctx?.addResourceComponent(MachineOpComp))?.openTerminal(node.params, true);
|
||||
const machine = node.params;
|
||||
const ac = machine.selectAuthCert.name;
|
||||
|
||||
if (machine.protocol == MachineProtocolEnum.Ssh.value) {
|
||||
const { href } = router.resolve({
|
||||
path: `/machine/terminal`,
|
||||
query: {
|
||||
ac,
|
||||
name: machine.name,
|
||||
},
|
||||
});
|
||||
window.open(href, '_blank');
|
||||
return;
|
||||
}
|
||||
if (machine.protocol == MachineProtocolEnum.Rdp.value) {
|
||||
const { href } = router.resolve({
|
||||
path: `/machine/terminal-rdp`,
|
||||
query: {
|
||||
machineId: machine.id,
|
||||
ac: ac,
|
||||
name: machine.name,
|
||||
},
|
||||
});
|
||||
window.open(href, '_blank');
|
||||
return;
|
||||
}
|
||||
}),
|
||||
new ContextmenuItem('files', 'machine.fileManage').withIcon('FolderOpened').withOnClick(async (node: any) => {
|
||||
(await node.ctx?.addResourceComponent(MachineOpComp))?.showFileManage(node.params);
|
||||
const m = node.params;
|
||||
const acName = m.selectAuthCert.name;
|
||||
|
||||
// 直接打开文件管理 tab,FileTab 内部会处理配置选择
|
||||
const tabKey = `${m.code}.${acName}`;
|
||||
createResourceOpTab({
|
||||
key: tabKey,
|
||||
name: `${m.selectAuthCert.username}@${m.name}`,
|
||||
component: FileTab,
|
||||
tabComponentProps: {
|
||||
icon: FileIcon,
|
||||
},
|
||||
componentProps: {
|
||||
tabKey: tabKey,
|
||||
machineId: m.id,
|
||||
authCertName: acName,
|
||||
protocol: m.protocol,
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
new ContextmenuItem('scripts', 'machine.scriptManage')
|
||||
.withIcon('Files')
|
||||
.withHideFunc((node: any) => node.params.protocol != MachineProtocolEnum.Ssh.value)
|
||||
.withOnClick(async (node: any) => {
|
||||
(await node.ctx?.addResourceComponent(MachineOpComp))?.serviceManager(node.params);
|
||||
const m = node.params;
|
||||
|
||||
// 显示脚本管理弹窗(已存在则更新 props,否则注册)
|
||||
showResourceOpOverlay('script_manage', ScriptManage, {
|
||||
machineId: m.id,
|
||||
authCertName: m.selectAuthCert.name,
|
||||
title: `${m.name} => ${m.selectAuthCert.username}@${m.ip}`,
|
||||
});
|
||||
}),
|
||||
]);
|
||||
|
||||
|
||||
62
frontend/src/views/ops/machine/resource/tabs/FileTab.vue
Normal file
62
frontend/src/views/ops/machine/resource/tabs/FileTab.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div class="machine-file-tab h-full">
|
||||
<!-- 如果已选择文件配置,显示文件管理器 -->
|
||||
<MachineFile
|
||||
v-if="selectedFileConf"
|
||||
:machine-id="machineId"
|
||||
:auth-cert-name="authCertName"
|
||||
:protocol="protocol"
|
||||
:file-id="selectedFileConf.fileId"
|
||||
:path="selectedFileConf.path"
|
||||
/>
|
||||
<!-- 否则显示文件配置选择列表 -->
|
||||
<FileConfList
|
||||
v-else
|
||||
:machine-id="machineId"
|
||||
:auth-cert-name="authCertName"
|
||||
:protocol="protocol"
|
||||
:visible="true"
|
||||
:open-file-manager="false"
|
||||
@select="handleSelect"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
|
||||
import FileConfList from '@/views/ops/machine/file/FileConfList.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
tabKey?: string;
|
||||
machineId: number;
|
||||
authCertName: string;
|
||||
protocol: number;
|
||||
fileId?: number; // 可选:直接指定文件配置 ID
|
||||
initialPath?: string; // 可选:初始路径
|
||||
}>();
|
||||
|
||||
// 已选择的文件配置
|
||||
const selectedFileConf = ref<{ fileId: number; path: string; name: string } | null>(
|
||||
// 如果传入了 fileId 和 initialPath,直接初始化
|
||||
props.fileId && props.initialPath
|
||||
? { fileId: props.fileId, path: props.initialPath, name: '' }
|
||||
: null
|
||||
);
|
||||
|
||||
// 处理文件配置选择
|
||||
const handleSelect = (fileConf: any) => {
|
||||
selectedFileConf.value = {
|
||||
fileId: fileConf.fileId,
|
||||
path: fileConf.path,
|
||||
name: fileConf.name,
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.machine-file-tab {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<el-dialog v-model="dialogVisible" :title="$t('common.detail')" width="600px" :destroy-on-close="true">
|
||||
<el-descriptions v-loading="loading" :column="3" border>
|
||||
<el-descriptions-item :span="1" label="ID">{{ machineDetail.id }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('common.code')">{{ machineDetail.code }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('common.name')">{{ machineDetail.name }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" :label="$t('tag.relateTag')"><TagCodePath :code="machineDetail.code" /></el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="IP">{{ machineDetail.ip }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('machine.port')">{{ machineDetail.port }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" :label="$t('common.remark')">{{ machineDetail.remark }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="1.5" :label="$t('machine.sshTunnel')">
|
||||
{{ machineDetail.sshTunnelMachineId > 0 ? $t('common.yes') : $t('common.no') }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :span="1.5" :label="$t('machine.terminalPlayback')">
|
||||
{{ machineDetail.enableRecorder == 1 ? $t('common.yes') : $t('common.no') }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" :label="$t('common.createTime')">{{ formatDate(machineDetail.createTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('common.creator')">{{ machineDetail.creator }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" :label="$t('common.updateTime')">{{ formatDate(machineDetail.updateTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('common.modifier')">{{ machineDetail.modifier }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import TagCodePath from '@/views/ops/component/TagCodePath.vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { machineApi } from '../../api';
|
||||
|
||||
const props = defineProps({
|
||||
code: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const dialogVisible = defineModel<boolean>('visible', { default: false });
|
||||
const loading = ref(false);
|
||||
const machineDetail = ref<any>({});
|
||||
|
||||
const getMachineDetail = async () => {
|
||||
try {
|
||||
machineDetail.value = {};
|
||||
loading.value = true;
|
||||
const res = await machineApi.list.request({
|
||||
code: props.code,
|
||||
});
|
||||
if (res.total == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
machineDetail.value = res.list?.[0];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 监听 visible 变化,打开时加载数据
|
||||
watch(
|
||||
dialogVisible,
|
||||
(val: boolean) => {
|
||||
if (val) {
|
||||
getMachineDetail();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
42
frontend/src/views/ops/machine/resource/tabs/ScriptTab.vue
Normal file
42
frontend/src/views/ops/machine/resource/tabs/ScriptTab.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="machine-script-tab h-full">
|
||||
<ScriptManage
|
||||
:machine-id="machineId"
|
||||
:auth-cert-name="authCertName"
|
||||
:title="title"
|
||||
v-model:visible="dialogVisible"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onActivated, watch } from 'vue';
|
||||
import ScriptManage from '@/views/ops/machine/ScriptManage.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
tabKey?: string;
|
||||
machineId: number;
|
||||
authCertName: string;
|
||||
title?: string;
|
||||
}>();
|
||||
|
||||
// 控制 dialog 显示
|
||||
const dialogVisible = ref(false);
|
||||
|
||||
// 组件挂载时打开 dialog
|
||||
onMounted(() => {
|
||||
dialogVisible.value = true;
|
||||
});
|
||||
|
||||
// keep-alive 激活时也打开 dialog
|
||||
onActivated(() => {
|
||||
dialogVisible.value = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.machine-script-tab {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
129
frontend/src/views/ops/machine/resource/tabs/TerminalTab.vue
Normal file
129
frontend/src/views/ops/machine/resource/tabs/TerminalTab.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<div class="machine-terminal-tab h-full flex flex-col">
|
||||
<!-- Terminal body -->
|
||||
<div class="terminal-body flex-1 min-h-0">
|
||||
<TerminalBody
|
||||
v-if="protocol == MachineProtocolEnum.Ssh.value"
|
||||
:mount-init="false"
|
||||
@status-change="onStatusChange"
|
||||
ref="terminalRef"
|
||||
:socket-url="socketUrl"
|
||||
:machine-id="machineId"
|
||||
:auth-cert-name="authCertName"
|
||||
:file-id="0"
|
||||
:protocol="protocol"
|
||||
/>
|
||||
<MachineRdp
|
||||
v-if="protocol != MachineProtocolEnum.Ssh.value"
|
||||
:machine-id="machineId"
|
||||
:auth-cert="authCertName"
|
||||
:protocol="protocol"
|
||||
ref="terminalRef"
|
||||
@status-change="onStatusChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MachineRdp from '@/components/terminal-rdp/MachineRdp.vue';
|
||||
import TerminalBody from '@/components/terminal/TerminalBody.vue';
|
||||
import { TerminalStatus, TerminalStatusEnum } from '@/components/terminal/common';
|
||||
import { getMachineTerminalSocketUrl } from '@/views/ops/machine/api';
|
||||
import { MachineProtocolEnum } from '@/views/ops/machine/enums';
|
||||
import { updateTabComponentProps } from '@/views/ops/resource/resourceOp';
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
tabKey?: string;
|
||||
machineId: number;
|
||||
authCertName: string;
|
||||
protocol: number; // Machine protocol
|
||||
}>();
|
||||
|
||||
const terminalRef = ref();
|
||||
const status = ref(TerminalStatusEnum.Disconnected.value);
|
||||
|
||||
// 映射终端状态到 tab 标签状态
|
||||
const getTabStatus = (terminalStatus: number): string => {
|
||||
switch (terminalStatus) {
|
||||
case TerminalStatusEnum.Connected.value:
|
||||
return 'connected';
|
||||
case TerminalStatusEnum.NoConnected.value:
|
||||
return 'disconnected';
|
||||
case TerminalStatusEnum.Error.value:
|
||||
return 'error';
|
||||
case TerminalStatusEnum.Disconnected.value:
|
||||
default:
|
||||
return 'disconnected';
|
||||
}
|
||||
};
|
||||
|
||||
// Watch status changes and update tab component props
|
||||
watch(status, (newStatus: number) => {
|
||||
if (props.tabKey) {
|
||||
updateTabComponentProps(props.tabKey, {
|
||||
status: getTabStatus(newStatus),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Compute socket URL
|
||||
const socketUrl = computed(() => {
|
||||
return getMachineTerminalSocketUrl(props.authCertName);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// Auto-connect terminal on mount
|
||||
nextTick(() => {
|
||||
handleReconnect();
|
||||
setTimeout(() => fitTerminal(), 300);
|
||||
});
|
||||
});
|
||||
|
||||
const onStatusChange = (statusValue: TerminalStatus) => {
|
||||
status.value = statusValue;
|
||||
};
|
||||
|
||||
const handleReconnect = () => {
|
||||
terminalRef.value?.init?.();
|
||||
};
|
||||
|
||||
const fitTerminal = () => {
|
||||
terminalRef.value?.fitTerminal?.();
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
terminalRef.value?.close?.();
|
||||
};
|
||||
|
||||
const focus = () => {
|
||||
terminalRef.value?.focus?.();
|
||||
};
|
||||
|
||||
const blur = () => {
|
||||
terminalRef.value?.blur?.();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
onRefresh: handleReconnect,
|
||||
onActivate: focus,
|
||||
onResize: fitTerminal,
|
||||
close,
|
||||
fitTerminal,
|
||||
focus,
|
||||
blur,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.machine-terminal-tab {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.terminal-body {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="terminal-tab-label flex items-center gap-1.5">
|
||||
<!-- 连接状态指示器 -->
|
||||
<span
|
||||
class="w-2 h-2 rounded-full shrink-0 transition-all duration-300"
|
||||
:class="statusClasses"
|
||||
></span>
|
||||
<!-- 终端图标 -->
|
||||
<SvgIcon v-if="icon" :name="icon.name" :color="icon.color" class="text-xs shrink-0" />
|
||||
<!-- 终端名称(从外层 tab.name 获取) -->
|
||||
<span class="max-w-[120px] overflow-hidden text-ellipsis" :title="tabName">{{ tabName }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
tabName?: string; // 从外层 tab.name 传入
|
||||
icon?: { name: string; color?: string };
|
||||
status?: 'connected' | 'disconnected' | 'connecting' | 'error' | string;
|
||||
}>(),
|
||||
{
|
||||
tabName: '终端',
|
||||
status: 'disconnected',
|
||||
}
|
||||
);
|
||||
|
||||
// 计算状态样式
|
||||
const statusClasses = computed(() => {
|
||||
switch (props.status) {
|
||||
case 'connected':
|
||||
return 'bg-green-500';
|
||||
case 'connecting':
|
||||
return 'bg-yellow-500 animate-pulse';
|
||||
case 'error':
|
||||
return 'bg-red-500 animate-pulse';
|
||||
case 'disconnected':
|
||||
default:
|
||||
return 'bg-red-500 animate-pulse';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -31,6 +31,7 @@
|
||||
</el-table>
|
||||
|
||||
<el-drawer
|
||||
:append-to-body="false"
|
||||
:title="$t('machine.cmdConfig')"
|
||||
v-model="dialogVisible"
|
||||
:show-close="false"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer :title="title" v-model="dialogVisible" :before-close="onCancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||
<el-drawer :append-to-body="false" :title="title" v-model="dialogVisible" :before-close="onCancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="onCancel" />
|
||||
</template>
|
||||
|
||||
@@ -37,17 +37,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TagResourceTypePath } from '@/common/commonEnum';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { Msg, useI18nCreateTitle, useI18nDeleteConfirm, useI18nEditTitle } from '@/hooks/useI18n';
|
||||
import { getTagPathSearchItem } from '@/views/ops/component/tag';
|
||||
import { defineAsyncComponent, ref, Ref } from 'vue';
|
||||
import ResourceAuthCert from '../component/ResourceAuthCert.vue';
|
||||
import TagCodePath from '../component/TagCodePath.vue';
|
||||
import { milvusApi, perms } from './api';
|
||||
import type { IMilvus } from './types';
|
||||
import TagCodePath from '../component/TagCodePath.vue';
|
||||
import ResourceAuthCert from '../component/ResourceAuthCert.vue';
|
||||
|
||||
const MilvusEdit = defineAsyncComponent(() => import('./MilvusEdit.vue'));
|
||||
|
||||
@@ -60,7 +58,7 @@ const query = ref({
|
||||
|
||||
const selectionData = ref([]);
|
||||
|
||||
const searchItems = [SearchItem.input('keyword', 'common.keyword').withPlaceholder('db.keywordPlaceholder'), getTagPathSearchItem(TagResourceTypePath.Db)];
|
||||
const searchItems = [SearchItem.input('keyword', 'common.keyword').withPlaceholder('db.keywordPlaceholder')];
|
||||
|
||||
const columns = ref([
|
||||
TableColumn.new('name', 'common.name').isSlot('name').setAddWidth(15),
|
||||
|
||||
@@ -88,7 +88,7 @@ export const milvusApi = {
|
||||
insertSampleData: (milvusId: number, collection: string, data: any) =>
|
||||
Api.newPost(`/milvus/${milvusId}/collections/${collection}/insert-sample-data?db=${db}`).request(withAc(data)),
|
||||
importFile: (milvusId: number, collection: string, formData: FormData) =>
|
||||
Api.newPost(`/milvus/${milvusId}/collections/${collection}/import-file?db=${db}`).request(formData),
|
||||
Api.newPost(`/milvus/${milvusId}/collections/${collection}/import-file?db=${db}&ac=${currentAcName}`).request(formData),
|
||||
|
||||
// 用户权限
|
||||
listUsers: (milvusId: number) => Api.newGet(`/milvus/${milvusId}/users`).request(withAc()),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<el-space>
|
||||
<el-tooltip :content="t('db.selectDbPlaceholder')" placement="top">
|
||||
<el-select size="small" v-model="selectedDb" style="width: 150px" @change="onChangeDb">
|
||||
<div class="component-container">
|
||||
<el-space>
|
||||
<el-tooltip :content="t('db.selectDbPlaceholder')" placement="top" :teleported="false">
|
||||
<el-select size="small" v-model="selectedDb" style="width: 150px" @change="onChangeDb" :teleported="false">
|
||||
<el-option v-for="item in dbs" :key="item.name" :value="item.name">{{ item.name }}</el-option>
|
||||
</el-select>
|
||||
</el-tooltip>
|
||||
@@ -53,6 +54,8 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
</div>
|
||||
|
||||
<CollectionsCreate v-model:visible="createDrawerVisible" :milvus-id="milvusId" :mode="drawerMode" :edit-data="editData" @success="loadList" />
|
||||
|
||||
<el-dialog v-model="aliasDialogVisible" :title="$t('milvus.addAlias')" width="400px">
|
||||
@@ -79,13 +82,14 @@ import { useI18n } from 'vue-i18n';
|
||||
import { milvusApi } from '../api';
|
||||
import CollectionsCreate from './CollectionsCreate.vue';
|
||||
|
||||
const milvusStore = useMilvusStore();
|
||||
const { dbs, selectedDb } = storeToRefs(milvusStore);
|
||||
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
milvusId: number;
|
||||
tabKey?: string;
|
||||
}>();
|
||||
|
||||
const milvusStore = useMilvusStore(props.tabKey || 'milvusStore');
|
||||
const { dbs, selectedDb } = storeToRefs(milvusStore);
|
||||
const emit = defineEmits(['changeTab']);
|
||||
|
||||
const list = ref<any[]>([]);
|
||||
@@ -313,6 +317,18 @@ watch(
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.component-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.component-container :deep(.el-table) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #f5f7fa;
|
||||
padding: 15px;
|
||||
|
||||
@@ -1,187 +1,189 @@
|
||||
<template>
|
||||
<!-- 顶部查询工具栏 -->
|
||||
<el-space>
|
||||
<el-button size="small" type="primary" @click="() => handleQuery()" :loading="queryLoading" icon="search">
|
||||
{{ $t('milvus.query') }}
|
||||
</el-button>
|
||||
<div ref="tableContainerRef" style="height: 100%">
|
||||
<!-- 顶部查询工具栏 -->
|
||||
<el-space>
|
||||
<el-button size="small" type="primary" @click="() => handleQuery()" :loading="queryLoading" icon="search">
|
||||
{{ $t('milvus.query') }}
|
||||
</el-button>
|
||||
|
||||
<el-button size="small" text @click="handleReset" icon="refresh" :disabled="queryLoading">
|
||||
{{ $t('milvus.reset') }}
|
||||
</el-button>
|
||||
<el-button size="small" text @click="handleReset" icon="refresh" :disabled="queryLoading">
|
||||
{{ $t('milvus.reset') }}
|
||||
</el-button>
|
||||
|
||||
<el-tooltip content="collection" placement="top">
|
||||
<el-select size="small" v-model="selectedCollection" style="min-width: 200px" filterable clearable>
|
||||
<el-option v-for="item in collections" :key="item" :label="item" :value="item" />
|
||||
<el-tooltip content="collection" placement="top" :teleported="false">
|
||||
<el-select size="small" v-model="selectedCollection" style="min-width: 200px" filterable clearable :teleported="false">
|
||||
<el-option v-for="item in collections" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-tooltip>
|
||||
|
||||
<el-input
|
||||
size="small"
|
||||
style="min-width: 180px"
|
||||
v-model="queryExpr"
|
||||
:placeholder="$t('milvus.queryExpr') + $t('milvus.queryExprPlaceholder')"
|
||||
clearable
|
||||
@keyup.enter="() => handleQuery()"
|
||||
>
|
||||
</el-input>
|
||||
|
||||
<el-tooltip :content="$t('milvus.consistencyLevel')" placement="top" :teleported="false">
|
||||
<el-select size="small" v-model="consistencyLevel" style="width: 130px" :teleported="false">
|
||||
<el-option label="Strong" :value="0" />
|
||||
<el-option label="Session" :value="1" />
|
||||
<el-option label="Bounded" :value="2" />
|
||||
<el-option label="Eventually" :value="3" />
|
||||
<el-option label="Customized" :value="4" />
|
||||
</el-select>
|
||||
</el-tooltip>
|
||||
|
||||
<el-select size="small" v-model="selectedPartition" :placeholder="$t('milvus.partitionManagement')" style="width: 120px" clearable :teleported="false">
|
||||
<el-option size="small" :label="$t('milvus.allPartitions')" value="" />
|
||||
<el-option size="small" v-for="partition in partitions" :key="partition" :label="partition" :value="partition" />
|
||||
</el-select>
|
||||
</el-tooltip>
|
||||
|
||||
<el-input
|
||||
size="small"
|
||||
style="min-width: 180px"
|
||||
v-model="queryExpr"
|
||||
:placeholder="$t('milvus.queryExpr') + $t('milvus.queryExprPlaceholder')"
|
||||
clearable
|
||||
@keyup.enter="() => handleQuery()"
|
||||
>
|
||||
</el-input>
|
||||
|
||||
<el-tooltip :content="$t('milvus.consistencyLevel')" placement="top">
|
||||
<el-select size="small" v-model="consistencyLevel" style="width: 130px">
|
||||
<el-option label="Strong" :value="0" />
|
||||
<el-option label="Session" :value="1" />
|
||||
<el-option label="Bounded" :value="2" />
|
||||
<el-option label="Eventually" :value="3" />
|
||||
<el-option label="Customized" :value="4" />
|
||||
</el-select>
|
||||
</el-tooltip>
|
||||
|
||||
<el-select size="small" v-model="selectedPartition" :placeholder="$t('milvus.partitionManagement')" style="width: 120px" clearable>
|
||||
<el-option size="small" :label="$t('milvus.allPartitions')" value="" />
|
||||
<el-option size="small" v-for="partition in partitions" :key="partition" :label="partition" :value="partition" />
|
||||
</el-select>
|
||||
|
||||
<el-dropdown size="small" trigger="click">
|
||||
<el-button size="small" text icon="grid"> {{ $t('milvus.outputFields') }} ({{ selectedFields.length }}/{{ collectionFields.length }}) </el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu class="fields-dropdown-menu">
|
||||
<div class="fields-dropdown-header">
|
||||
<el-checkbox
|
||||
:model-value="selectedFields.length === collectionFields.length && collectionFields.length > 0"
|
||||
:indeterminate="selectedFields.length > 0 && selectedFields.length < collectionFields.length"
|
||||
@change="handleSelectAll"
|
||||
>
|
||||
{{ $t('milvus.outputFields') }}
|
||||
</el-checkbox>
|
||||
</div>
|
||||
<div class="fields-dropdown-list">
|
||||
<div v-for="field in collectionFields" :key="field" class="field-item" @click="toggleField(field)">
|
||||
<el-checkbox :model-value="selectedFields.includes(field)">
|
||||
<span class="field-label">
|
||||
<el-icon v-if="isPrimaryKey(field)" class="field-icon primary">
|
||||
<Key />
|
||||
</el-icon>
|
||||
<el-icon v-else-if="isVectorField(field)" class="field-icon vector">
|
||||
<DataAnalysis />
|
||||
</el-icon>
|
||||
<el-icon v-else-if="isDynamicField(field)" class="field-icon dynamic">
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
<el-icon v-else class="field-icon normal">
|
||||
<Grid />
|
||||
</el-icon>
|
||||
{{ field }}
|
||||
</span>
|
||||
<el-dropdown size="small" trigger="click" :teleported="false">
|
||||
<el-button size="small" text icon="grid"> {{ $t('milvus.outputFields') }} ({{ selectedFields.length }}/{{ collectionFields.length }}) </el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu class="fields-dropdown-menu">
|
||||
<div class="fields-dropdown-header">
|
||||
<el-checkbox
|
||||
:model-value="selectedFields.length === collectionFields.length && collectionFields.length > 0"
|
||||
:indeterminate="selectedFields.length > 0 && selectedFields.length < collectionFields.length"
|
||||
@change="handleSelectAll"
|
||||
>
|
||||
{{ $t('milvus.outputFields') }}
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-space>
|
||||
<div class="fields-dropdown-list">
|
||||
<div v-for="field in collectionFields" :key="field" class="field-item" @click="toggleField(field)">
|
||||
<el-checkbox :model-value="selectedFields.includes(field)">
|
||||
<span class="field-label">
|
||||
<el-icon v-if="isPrimaryKey(field)" class="field-icon primary">
|
||||
<Key />
|
||||
</el-icon>
|
||||
<el-icon v-else-if="isVectorField(field)" class="field-icon vector">
|
||||
<DataAnalysis />
|
||||
</el-icon>
|
||||
<el-icon v-else-if="isDynamicField(field)" class="field-icon dynamic">
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
<el-icon v-else class="field-icon normal">
|
||||
<Grid />
|
||||
</el-icon>
|
||||
{{ field }}
|
||||
</span>
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-space>
|
||||
|
||||
<!-- 操作工具栏 -->
|
||||
<el-space style="padding: 5px 0">
|
||||
<el-button text size="small" icon="upload" @click="handleImportFile">
|
||||
{{ $t('milvus.importFile') }}
|
||||
</el-button>
|
||||
<el-button text size="small" icon="plus" @click="handleInsertSample">
|
||||
{{ $t('milvus.insertSampleData') }}
|
||||
</el-button>
|
||||
<el-button text size="small" icon="delete" @click="handleClearData" :disabled="queryResults.length === 0">
|
||||
{{ $t('milvus.clearData') }}
|
||||
</el-button>
|
||||
<el-button text size="small" icon="edit" @click="handleEditData" :disabled="selectedRows.length === 0">
|
||||
{{ $t('common.edit') }}
|
||||
</el-button>
|
||||
<el-button text size="small" icon="download" :disabled="queryResults.length === 0"> {{ $t('milvus.export') }} ({{ selectedRows.length }}) </el-button>
|
||||
<el-button text size="small" icon="document-copy" :disabled="selectedRows.length === 0" @click="handleCopySelected">
|
||||
{{ $t('common.copy') }} JSON
|
||||
</el-button>
|
||||
<el-button text size="small" icon="delete" :disabled="selectedRows.length === 0" @click="handleDeleteSelected">
|
||||
{{ $t('common.delete') }}
|
||||
</el-button>
|
||||
</el-space>
|
||||
<!-- 操作工具栏 -->
|
||||
<el-space style="padding: 5px 0">
|
||||
<el-button text size="small" icon="upload" @click="handleImportFile">
|
||||
{{ $t('milvus.importFile') }}
|
||||
</el-button>
|
||||
<el-button text size="small" icon="plus" @click="handleInsertSample">
|
||||
{{ $t('milvus.insertSampleData') }}
|
||||
</el-button>
|
||||
<el-button text size="small" icon="delete" @click="handleClearData" :disabled="queryResults.length === 0">
|
||||
{{ $t('milvus.clearData') }}
|
||||
</el-button>
|
||||
<el-button text size="small" icon="edit" @click="handleEditData" :disabled="selectedRows.length === 0">
|
||||
{{ $t('common.edit') }}
|
||||
</el-button>
|
||||
<el-button text size="small" icon="download" :disabled="queryResults.length === 0"> {{ $t('milvus.export') }} ({{ selectedRows.length }}) </el-button>
|
||||
<el-button text size="small" icon="document-copy" :disabled="selectedRows.length === 0" @click="handleCopySelected">
|
||||
{{ $t('common.copy') }} JSON
|
||||
</el-button>
|
||||
<el-button text size="small" icon="delete" :disabled="selectedRows.length === 0" @click="handleDeleteSelected">
|
||||
{{ $t('common.delete') }}
|
||||
</el-button>
|
||||
</el-space>
|
||||
|
||||
<el-table
|
||||
v-loading="queryLoading"
|
||||
:data="queryResults"
|
||||
style="width: 100%"
|
||||
@selection-change="handleSelectionChange"
|
||||
border
|
||||
stripe
|
||||
height="calc(100vh - 288px)"
|
||||
:loading="queryLoading"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table
|
||||
v-loading="queryLoading"
|
||||
:data="queryResults"
|
||||
style="width: 100%"
|
||||
@selection-change="handleSelectionChange"
|
||||
border
|
||||
stripe
|
||||
:loading="queryLoading"
|
||||
:height="tableHeight"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
|
||||
<el-table-column v-for="field in displayFields" :key="field" :label="field" :min-width="getMinWidth(field)">
|
||||
<template #header>
|
||||
<span class="field-label">
|
||||
<el-icon v-if="isPrimaryKey(field)" title="Primary Key">
|
||||
<Key />
|
||||
</el-icon>
|
||||
<el-icon v-else-if="isVectorField(field)" title="Vector Field">
|
||||
<DataAnalysis />
|
||||
</el-icon>
|
||||
<el-icon v-else-if="isDynamicField(field)" title="Dynamic Fields">
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
<el-icon v-else>
|
||||
<Grid />
|
||||
</el-icon>
|
||||
{{ getDisplayLabel(field) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<div class="cell-content">
|
||||
<div
|
||||
class="cell-value"
|
||||
:class="{ 'url-link': isUrl(row[field]), 'vector-cell': isVectorField(field) }"
|
||||
:title="formatCellValue(row[field], field)"
|
||||
@click="handleCellClick(row[field], field)"
|
||||
>
|
||||
{{ formatCellValue(row[field], field) }}
|
||||
</div>
|
||||
<div class="cell-actions">
|
||||
<el-icon class="copy-icon" @click.stop="copyToClipboard(row[field], field)">
|
||||
<DocumentCopy />
|
||||
<el-table-column v-for="field in displayFields" :key="field" :label="field" :min-width="getMinWidth(field)">
|
||||
<template #header>
|
||||
<span class="field-label">
|
||||
<el-icon v-if="isPrimaryKey(field)" title="Primary Key">
|
||||
<Key />
|
||||
</el-icon>
|
||||
<el-icon v-else-if="isVectorField(field)" title="Vector Field">
|
||||
<DataAnalysis />
|
||||
</el-icon>
|
||||
<el-icon v-else-if="isDynamicField(field)" title="Dynamic Fields">
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
<el-icon v-else>
|
||||
<Grid />
|
||||
</el-icon>
|
||||
{{ getDisplayLabel(field) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<div class="cell-content">
|
||||
<div
|
||||
class="cell-value"
|
||||
:class="{ 'url-link': isUrl(row[field]), 'vector-cell': isVectorField(field) }"
|
||||
:title="formatCellValue(row[field], field)"
|
||||
@click="handleCellClick(row[field], field)"
|
||||
>
|
||||
{{ formatCellValue(row[field], field) }}
|
||||
</div>
|
||||
<div class="cell-actions">
|
||||
<el-icon class="copy-icon" @click.stop="copyToClipboard(row[field], field)">
|
||||
<DocumentCopy />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页控件 -->
|
||||
<div class="pagination-container">
|
||||
<div class="pagination-info">
|
||||
<span>{{ $t('milvus.paginationInfo', { total: totalRecords, current: currentPage, pages: totalPages }) }}</span>
|
||||
<el-select size="small" v-model="pageSize" @change="handlePageSizeChange" style="width: 110px; margin-left: 12px">
|
||||
<el-option size="small" :label="$t('milvus.pageSize10')" :value="10" />
|
||||
<el-option size="small" :label="$t('milvus.pageSize20')" :value="20" />
|
||||
<el-option size="small" :label="$t('milvus.pageSize50')" :value="50" />
|
||||
<el-option size="small" :label="$t('milvus.pageSize100')" :value="100" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="pagination-controls">
|
||||
<el-button text :disabled="currentPage <= 1" @click="handlePageChange(currentPage - 1)" icon="ArrowLeft" size="small">
|
||||
{{ $t('milvus.prevPage') }}
|
||||
</el-button>
|
||||
<div class="pagination-jump">
|
||||
<span>{{ $t('milvus.jumpTo') }}</span>
|
||||
<el-input-number
|
||||
v-model="jumpPage"
|
||||
:min="1"
|
||||
:max="totalPages || 999999"
|
||||
size="small"
|
||||
style="width: 80px"
|
||||
@change="handleJumpPage"
|
||||
@keyup.enter="handleJumpPage"
|
||||
/>
|
||||
<!-- 分页控件 -->
|
||||
<div class="pagination-container">
|
||||
<div class="pagination-info">
|
||||
<span>{{ $t('milvus.paginationInfo', { total: totalRecords, current: currentPage, pages: totalPages }) }}</span>
|
||||
<el-select size="small" v-model="pageSize" @change="handlePageSizeChange" style="width: 110px; margin-left: 12px" :teleported="false">
|
||||
<el-option size="small" :label="$t('milvus.pageSize10')" :value="10" />
|
||||
<el-option size="small" :label="$t('milvus.pageSize20')" :value="20" />
|
||||
<el-option size="small" :label="$t('milvus.pageSize50')" :value="50" />
|
||||
<el-option size="small" :label="$t('milvus.pageSize100')" :value="100" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="pagination-controls">
|
||||
<el-button text :disabled="currentPage <= 1" @click="handlePageChange(currentPage - 1)" icon="ArrowLeft" size="small">
|
||||
{{ $t('milvus.prevPage') }}
|
||||
</el-button>
|
||||
<div class="pagination-jump">
|
||||
<span>{{ $t('milvus.jumpTo') }}</span>
|
||||
<el-input-number
|
||||
v-model="jumpPage"
|
||||
:min="1"
|
||||
:max="totalPages || 999999"
|
||||
size="small"
|
||||
style="width: 80px"
|
||||
@change="handleJumpPage"
|
||||
@keyup.enter="handleJumpPage"
|
||||
/>
|
||||
</div>
|
||||
<el-button text :disabled="currentPage >= totalPages" @click="handlePageChange(currentPage + 1)" icon="ArrowRight" size="small">
|
||||
{{ $t('milvus.nextPage') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button text :disabled="currentPage >= totalPages" @click="handlePageChange(currentPage + 1)" icon="ArrowRight" size="small">
|
||||
{{ $t('milvus.nextPage') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -201,7 +203,7 @@
|
||||
<!-- 分区选择 -->
|
||||
<div class="import-field">
|
||||
<label class="field-label">{{ $t('milvus.partition') }}</label>
|
||||
<el-select v-model="selectedPartitionForImport" style="width: 100%">
|
||||
<el-select v-model="selectedPartitionForImport" style="width: 100%" :teleported="false">
|
||||
<el-option v-for="partition in partitions" :key="partition" :label="partition" :value="partition" />
|
||||
</el-select>
|
||||
</div>
|
||||
@@ -256,22 +258,52 @@ import MonacoEditorBox from '@/components/monaco/MonacoEditorBox';
|
||||
import { Msg } from '@/hooks/useI18n';
|
||||
import { useMilvusStore } from '@/views/ops/milvus/resource/store';
|
||||
import { DataAnalysis, DocumentCopy, Grid, InfoFilled, Key } from '@element-plus/icons-vue';
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { useClipboard, useResizeObserver } from '@vueuse/core';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { milvusApi } from '../api';
|
||||
|
||||
// 表格动态高度(仅 body 滚动,表头固定)
|
||||
const tableContainerRef = ref<HTMLElement>();
|
||||
const tableHeight = ref(300);
|
||||
let resizeObserver: ReturnType<typeof useResizeObserver> | undefined;
|
||||
|
||||
const calcTableHeight = () => {
|
||||
if (!tableContainerRef.value) return;
|
||||
const table = tableContainerRef.value.querySelector('.el-table');
|
||||
if (!table) return;
|
||||
const containerRect = tableContainerRef.value.getBoundingClientRect();
|
||||
const tableRect = table.getBoundingClientRect();
|
||||
const offset = tableRect.top - containerRect.top;
|
||||
const paginationEl = tableContainerRef.value.querySelector('.pagination-container') as HTMLElement;
|
||||
const paginationH = paginationEl ? paginationEl.offsetHeight + 15 : 0;
|
||||
const newHeight = containerRect.height - offset - paginationH;
|
||||
if (newHeight > 100) tableHeight.value = newHeight;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
resizeObserver = useResizeObserver(tableContainerRef, () => {
|
||||
nextTick(calcTableHeight);
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resizeObserver?.stop();
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const { copy } = useClipboard();
|
||||
const milvusStore = useMilvusStore();
|
||||
const { collections, selectedCollection } = storeToRefs(milvusStore);
|
||||
|
||||
const props = defineProps<{
|
||||
milvusId: number;
|
||||
tabKey?: string;
|
||||
}>();
|
||||
|
||||
const milvusStore = useMilvusStore(props.tabKey || 'milvusStore');
|
||||
const { collections, selectedCollection } = storeToRefs(milvusStore);
|
||||
|
||||
// 查询状态
|
||||
const queryLoading = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<template>
|
||||
<el-button size="small" type="primary" @click="handleCreate" icon="plus">{{ $t('milvus.createDatabase') }}</el-button>
|
||||
<el-button size="small" text icon="refresh" @click="loadList" :loading="loading" />
|
||||
<div class="component-container">
|
||||
<div>
|
||||
<el-button size="small" type="primary" @click="handleCreate" icon="plus">{{ $t('milvus.createDatabase') }}</el-button>
|
||||
<el-button size="small" text icon="refresh" @click="loadList" :loading="loading" />
|
||||
</div>
|
||||
|
||||
<el-table :data="dbs" style="width: 100%">
|
||||
<el-table :data="dbs" style="width: 100%">
|
||||
<el-table-column prop="name" :label="$t('milvus.dbName')" sortable>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" underline="never" @click="handleUse(row)">{{ row.name }}</el-link>
|
||||
@@ -15,7 +18,8 @@
|
||||
<el-button type="danger" size="small" @click="handleDrop(row)">{{ $t('common.delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="createDialog.visible" :title="$t('milvus.createDatabase')" width="500px">
|
||||
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="auto">
|
||||
@@ -61,13 +65,14 @@ import { storeToRefs } from 'pinia';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { milvusApi, timezones } from '../api';
|
||||
|
||||
const milvusStore = useMilvusStore();
|
||||
const { dbs } = storeToRefs(milvusStore);
|
||||
|
||||
const props = defineProps<{
|
||||
milvusId: number;
|
||||
tabKey?: string;
|
||||
}>();
|
||||
|
||||
const milvusStore = useMilvusStore(props.tabKey || 'milvusStore');
|
||||
const { dbs } = storeToRefs(milvusStore);
|
||||
|
||||
const emits = defineEmits(['use']);
|
||||
|
||||
const createDialog = ref({
|
||||
@@ -194,4 +199,16 @@ watch(
|
||||
onMounted(loadList);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.component-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.component-container :deep(.el-table) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<el-space>
|
||||
<el-select size="small" v-model="selectedCollection" style="min-width: 200px" @change="loadList" filterable clearable>
|
||||
<div class="component-container">
|
||||
<el-space>
|
||||
<el-select size="small" v-model="selectedCollection" style="min-width: 200px" @change="loadList" filterable clearable :teleported="false">
|
||||
<el-option v-for="item in collections" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
|
||||
@@ -20,7 +21,8 @@
|
||||
<el-button size="small" type="danger" @click="handleDrop(row)">{{ $t('common.delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="createDialog.visible" :title="$t('milvus.createPartition')" width="500px">
|
||||
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="auto">
|
||||
@@ -44,13 +46,14 @@ import { storeToRefs } from 'pinia';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { milvusApi } from '../api';
|
||||
|
||||
const milvusStore = useMilvusStore();
|
||||
const { collections, selectedCollection } = storeToRefs(milvusStore);
|
||||
|
||||
const props = defineProps<{
|
||||
milvusId: number;
|
||||
tabKey?: string;
|
||||
}>();
|
||||
|
||||
const milvusStore = useMilvusStore(props.tabKey || 'milvusStore');
|
||||
const { collections, selectedCollection } = storeToRefs(milvusStore);
|
||||
|
||||
const list = ref<any[]>([]);
|
||||
const createDialog = ref({
|
||||
visible: false,
|
||||
@@ -126,4 +129,16 @@ watch(
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.component-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.component-container :deep(.el-table) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="component-container">
|
||||
<!-- 工具栏 -->
|
||||
<div class="mb5" style="display: flex; align-items: center">
|
||||
<el-button size="small" type="primary" icon="plus" @click="handleCreate">{{ $t('milvus.addPrivilegeGroup') }}</el-button>
|
||||
@@ -116,3 +116,17 @@ watch(
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.component-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.component-container :deep(.el-table) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<template>
|
||||
<el-button size="small" icon="plus" type="primary" @click="handleCreate">{{ $t('milvus.createResourceGroup') }}</el-button>
|
||||
<el-button size="small" text icon="refresh" @click="loadList" :loading="loading" />
|
||||
<div class="component-container">
|
||||
<div>
|
||||
<el-button size="small" icon="plus" type="primary" @click="handleCreate">{{ $t('milvus.createResourceGroup') }}</el-button>
|
||||
<el-button size="small" text icon="refresh" @click="loadList" :loading="loading" />
|
||||
</div>
|
||||
|
||||
<el-table :data="list">
|
||||
<el-table :data="list">
|
||||
<el-table-column prop="name" :label="$t('milvus.resourceGroupName')" />
|
||||
<el-table-column :label="$t('common.operation')" width="250">
|
||||
<template #default="{ row }">
|
||||
@@ -10,7 +13,8 @@
|
||||
<el-button size="small" type="danger" @click="handleDrop(row)">{{ $t('common.delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="createDialog.visible" :title="$t('milvus.createResourceGroup')" width="500px">
|
||||
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="auto">
|
||||
@@ -35,12 +39,14 @@ import { onMounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { milvusApi } from '../api';
|
||||
|
||||
const milvusStore = useMilvusStore();
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
milvusId: number;
|
||||
tabKey?: string;
|
||||
}>();
|
||||
|
||||
const milvusStore = useMilvusStore(props.tabKey || 'milvusStore');
|
||||
|
||||
const list = ref<any[]>([]);
|
||||
const createDialog = ref({
|
||||
visible: false,
|
||||
@@ -127,4 +133,16 @@ watch(
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.component-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.component-container :deep(.el-table) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<template>
|
||||
<el-button size="small" icon="plus" type="primary" @click="handleCreate">{{ $t('milvus.createRole') }}</el-button>
|
||||
<el-button size="small" text icon="refresh" @click="loadList" :loading="loading" />
|
||||
<div class="component-container">
|
||||
<div>
|
||||
<el-button size="small" icon="plus" type="primary" @click="handleCreate">{{ $t('milvus.createRole') }}</el-button>
|
||||
<el-button size="small" text icon="refresh" @click="loadList" :loading="loading" />
|
||||
</div>
|
||||
|
||||
<el-table :data="list">
|
||||
<el-table :data="list">
|
||||
<el-table-column prop="roleName" :label="$t('milvus.roleName')" />
|
||||
<el-table-column :label="$t('common.operation')" width="250">
|
||||
<template #default="{ row }">
|
||||
@@ -12,7 +15,8 @@
|
||||
<el-button size="small" type="danger" @click="handleDrop(row)">{{ $t('common.delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 创建角色弹窗 -->
|
||||
<el-dialog v-model="createDialog.visible" :title="$t('milvus.createRole')" width="500px">
|
||||
@@ -40,12 +44,13 @@ import { onMounted, ref, watch } from 'vue';
|
||||
import { milvusApi } from '../api';
|
||||
import RolesGrantPrivilege from './RolesGrantPrivilege.vue';
|
||||
|
||||
const milvusStore = useMilvusStore();
|
||||
|
||||
const props = defineProps<{
|
||||
milvusId: number;
|
||||
tabKey?: string;
|
||||
}>();
|
||||
|
||||
const milvusStore = useMilvusStore(props.tabKey || 'milvusStore');
|
||||
|
||||
const list = ref<any[]>([]);
|
||||
const createDialog = ref({
|
||||
visible: false,
|
||||
@@ -120,3 +125,17 @@ watch(
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.component-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.component-container :deep(.el-table) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -209,14 +209,15 @@ import { useI18n } from 'vue-i18n';
|
||||
import { milvusApi } from '../api';
|
||||
import { useMilvusStore } from '@/views/ops/milvus/resource/store';
|
||||
|
||||
const milvusStore = useMilvusStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
milvusId: number;
|
||||
tabKey?: string;
|
||||
}>();
|
||||
|
||||
const milvusStore = useMilvusStore(props.tabKey || 'milvusStore');
|
||||
|
||||
const loading = ref(false);
|
||||
const versionLoading = ref(false);
|
||||
const healthLoading = ref(false);
|
||||
@@ -414,6 +415,8 @@ watch([() => props.milvusId, () => milvusStore.authCertName], () => {
|
||||
|
||||
<style scoped>
|
||||
.system-info-container {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<template>
|
||||
<el-button size="small" icon="plus" type="primary" @click="handleCreate">{{ $t('milvus.createUser') }}</el-button>
|
||||
<el-button text icon="refresh" @click="loadList" :loading="loading" />
|
||||
<div class="component-container">
|
||||
<div>
|
||||
<el-button size="small" icon="plus" type="primary" @click="handleCreate">{{ $t('milvus.createUser') }}</el-button>
|
||||
<el-button size="small" text icon="refresh" @click="loadList" :loading="loading" />
|
||||
</div>
|
||||
|
||||
<el-table :data="list">
|
||||
<el-table :data="list">
|
||||
<el-table-column prop="name" :label="$t('common.username')" />
|
||||
<el-table-column :label="$t('common.operation')" width="350">
|
||||
<template #default="{ row }">
|
||||
@@ -11,7 +14,8 @@
|
||||
<el-button size="small" type="danger" @click="handleDelete(row)">{{ $t('common.delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="createDialog.visible" :title="$t('milvus.createUser')" width="500px">
|
||||
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="auto">
|
||||
@@ -72,12 +76,13 @@ import { FormInstance } from 'element-plus';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { milvusApi } from '../api';
|
||||
|
||||
const milvusStore = useMilvusStore();
|
||||
|
||||
const props = defineProps<{
|
||||
milvusId: number;
|
||||
tabKey?: string;
|
||||
}>();
|
||||
|
||||
const milvusStore = useMilvusStore(props.tabKey || 'milvusStore');
|
||||
|
||||
const list = ref<any[]>([]);
|
||||
const createDialog = ref({
|
||||
visible: false,
|
||||
@@ -239,4 +244,16 @@ watch(
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.component-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.component-container :deep(.el-table) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,84 +1,126 @@
|
||||
<template>
|
||||
<el-tabs v-model="activeTab" type="border-card">
|
||||
<el-tabs v-model="activeTab" type="border-card" class="milvus-tabs">
|
||||
<!-- 数据库管理 -->
|
||||
<el-tab-pane :label="$t('milvus.databaseManagement')" name="databases">
|
||||
<Databases :milvus-id="milvusId" v-if="activeTab === 'databases'" @use="onUseDb" />
|
||||
<Databases :milvus-id="milvusId" :tab-key="tabKey" v-if="activeTab === 'databases'" @use="onUseDb" />
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Collection 管理 -->
|
||||
<el-tab-pane :label="$t('milvus.collectionManagement')" name="collections">
|
||||
<Collections :milvus-id="milvusId" v-if="activeTab === 'collections'" @change-tab="(name) => (activeTab = name)" />
|
||||
<Collections :milvus-id="milvusId" :tab-key="tabKey" v-if="activeTab === 'collections'" @change-tab="(name) => (activeTab = name)" />
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 数据操作 -->
|
||||
<el-tab-pane :label="$t('milvus.dataOperation')" name="data">
|
||||
<DataOperation :milvus-id="milvusId" v-if="activeTab === 'data'" />
|
||||
<DataOperation :milvus-id="milvusId" :tab-key="tabKey" v-if="activeTab === 'data'" />
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 分区管理 -->
|
||||
<el-tab-pane :label="$t('milvus.partitionManagement')" name="partitions">
|
||||
<Partitions :milvus-id="milvusId" v-if="activeTab === 'partitions'" />
|
||||
<Partitions :milvus-id="milvusId" :tab-key="tabKey" v-if="activeTab === 'partitions'" />
|
||||
</el-tab-pane>
|
||||
<!-- 用户权限 -->
|
||||
<el-tab-pane :label="$t('milvus.userPermission')" name="users">
|
||||
<Users :milvus-id="milvusId" v-if="activeTab === 'users'" />
|
||||
<Users :milvus-id="milvusId" :tab-key="tabKey" v-if="activeTab === 'users'" />
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 角色管理 -->
|
||||
<el-tab-pane :label="$t('milvus.roleManagement')" name="roles">
|
||||
<Roles :milvus-id="milvusId" v-if="activeTab === 'roles'" />
|
||||
<Roles :milvus-id="milvusId" :tab-key="tabKey" v-if="activeTab === 'roles'" />
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 资源组 -->
|
||||
<el-tab-pane :label="$t('milvus.resourceGroup')" name="resourceGroups">
|
||||
<ResourceGroups :milvus-id="milvusId" v-if="activeTab === 'resourceGroups'" />
|
||||
<ResourceGroups :milvus-id="milvusId" :tab-key="tabKey" v-if="activeTab === 'resourceGroups'" />
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 系统信息 -->
|
||||
<el-tab-pane :label="$t('milvus.systemInfo')" name="system">
|
||||
<SystemInfo :milvus-id="milvusId" v-if="activeTab === 'system'" />
|
||||
<SystemInfo :milvus-id="milvusId" :tab-key="tabKey" v-if="activeTab === 'system'" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, getCurrentInstance } from 'vue';
|
||||
import Databases from '../components/Databases.vue';
|
||||
import { setCurrentAcName } from '@/views/ops/milvus/resource/authCert';
|
||||
import { useMilvusStore } from '@/views/ops/milvus/resource/store';
|
||||
import { onActivated, onBeforeMount, onMounted, ref } from 'vue';
|
||||
import Collections from '../components/Collections.vue';
|
||||
import Databases from '../components/Databases.vue';
|
||||
import DataOperation from '../components/DataOperation.vue';
|
||||
import Partitions from '../components/Partitions.vue';
|
||||
import Users from '../components/Users.vue';
|
||||
import Roles from '../components/Roles.vue';
|
||||
import PrivilegeGroups from '../components/PrivilegeGroups.vue';
|
||||
import ResourceGroups from '../components/ResourceGroups.vue';
|
||||
import Roles from '../components/Roles.vue';
|
||||
import SystemInfo from '../components/SystemInfo.vue';
|
||||
import { MilvusOpComp } from '@/views/ops/milvus/resource/index';
|
||||
import { useMilvusStore } from '@/views/ops/milvus/resource/store';
|
||||
import Users from '../components/Users.vue';
|
||||
|
||||
const milvusStore = useMilvusStore();
|
||||
const milvusId = ref<number>(0);
|
||||
const props = defineProps<{
|
||||
milvusId: number;
|
||||
acName: string;
|
||||
tabKey: string;
|
||||
}>();
|
||||
|
||||
// 使用 per-tab 独立 store,实现多标签页状态隔离
|
||||
const milvusStore = useMilvusStore(props.tabKey || 'milvusStore');
|
||||
|
||||
const emits = defineEmits(['init']);
|
||||
const initMilvus = (params: any) => {
|
||||
activeTab.value = 'databases';
|
||||
milvusId.value = params.id;
|
||||
// 设置当前选中的授权凭证名(无论是否选择凭证,都重置,确保切换实例后不会残留旧凭证)
|
||||
milvusStore.setAuthCertName(params.selectAuthCert?.name || '');
|
||||
};
|
||||
|
||||
const activeTab = ref('databases');
|
||||
|
||||
const onUseDb = (db: string) => {
|
||||
// 在子组件挂载前同步全局 ac,确保子组件 watcher / onMounted 发出的 API 请求使用正确的凭证
|
||||
onBeforeMount(() => {
|
||||
if (props.acName) {
|
||||
setCurrentAcName(props.acName);
|
||||
}
|
||||
});
|
||||
|
||||
const initMilvus = (params: any) => {
|
||||
// 设置当前选中的授权凭证名(同步全局 ac,确保 API 调用使用正确的凭证)
|
||||
const newAcName = params.selectAuthCert?.name || '';
|
||||
milvusStore.setAuthCertName(newAcName);
|
||||
};
|
||||
|
||||
// 标签页激活时同步全局 ac(确保 API 调用使用正确的凭证)
|
||||
const onActivate = () => {
|
||||
if (milvusStore.authCertName) {
|
||||
milvusStore.setAuthCertName(milvusStore.authCertName);
|
||||
}
|
||||
};
|
||||
|
||||
const onUseDb = (_db: string) => {
|
||||
activeTab.value = 'collections';
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
emits('init', { name: MilvusOpComp.name, ref: getCurrentInstance()?.exposed });
|
||||
onMounted(() => {});
|
||||
|
||||
// keep-alive 激活时重新同步全局 ac(确保切换标签后 API 调用正确)
|
||||
onActivated(() => {
|
||||
if (milvusStore.authCertName) {
|
||||
milvusStore.setAuthCertName(milvusStore.authCertName);
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
initMilvus,
|
||||
onActivate,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped lang="scss">
|
||||
.milvus-tabs {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
:deep(.el-tabs__content) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: visible;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-tab-pane) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { NodeType, TagTreeNode, ResourceComponentConfig, ResourceConfig } from '../../component/tag';
|
||||
import { ResourceTypeEnum, TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { sleep } from '@/common/utils/loading';
|
||||
import { milvusApi, perms } from '@/views/ops/milvus/api';
|
||||
import type { ResourceConfig } from '@/views/ops/resource/resource';
|
||||
import { createResourceOpTab } from '@/views/ops/resource/resourceOp';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { NodeType, TagTreeNode } from '../../component/tag';
|
||||
|
||||
export const MilvusIcon = {
|
||||
name: ResourceTypeEnum.Milvus.extra.icon,
|
||||
@@ -15,25 +17,39 @@ const MilvusOp = defineAsyncComponent(() => import('./MilvusOp.vue'));
|
||||
const NodeMilvus = defineAsyncComponent(() => import('./NodeMilvus.vue'));
|
||||
const NodeMilvusAc = defineAsyncComponent(() => import('./NodeMilvusAc.vue'));
|
||||
|
||||
export const MilvusOpComp: ResourceComponentConfig = {
|
||||
name: 'tag.milvusOp',
|
||||
component: MilvusOp,
|
||||
icon: MilvusIcon,
|
||||
const getMilvusOpTab = async (milvus: any, acName: string) => {
|
||||
const tabKey = `milvus.${milvus.id}.${acName}`;
|
||||
return await createResourceOpTab({
|
||||
key: tabKey,
|
||||
name: milvus.acUsername ? `${milvus.name} (${milvus.acUsername})` : milvus.name,
|
||||
component: MilvusOp,
|
||||
componentProps: {
|
||||
milvusId: milvus.id,
|
||||
acName,
|
||||
tabKey,
|
||||
},
|
||||
tabComponentProps: { icon: MilvusIcon },
|
||||
});
|
||||
};
|
||||
|
||||
// milvus 授权凭证节点类型
|
||||
const getMilvusOpTabCompInst = async (milvus: any, acName: string) => {
|
||||
return (await getMilvusOpTab(milvus, acName)).componentInstance;
|
||||
};
|
||||
|
||||
// milvus 授权凭证节点类型:点击后打开独立标签页
|
||||
const NodeTypeMilvusAc = new NodeType(TagResourceTypeEnum.Milvus.value * 10 + 1).withNodeClickFunc(async (node: TagTreeNode) => {
|
||||
|
||||
(await node.ctx?.addResourceComponent(MilvusOpComp))?.initMilvus(node.params);
|
||||
|
||||
console.log(node.params);
|
||||
const milvus = node.params;
|
||||
const acName = milvus.selectAuthCert?.name || '';
|
||||
// 仅在首次创建时初始化(已存在的标签页只是激活,不重置状态)
|
||||
const compRef = await getMilvusOpTabCompInst(milvus, acName);
|
||||
compRef?.initMilvus?.(milvus);
|
||||
});
|
||||
|
||||
const NodeTypeMilvus = new NodeType(TagResourceTypeEnum.Milvus.value).withLoadNodesFunc((node: TagTreeNode) => {
|
||||
const milvus = node.params;
|
||||
const authCerts = milvus.authCerts || [];
|
||||
return authCerts.map((x: any) =>
|
||||
TagTreeNode.new(node, x.name, x.username, NodeTypeMilvusAc)
|
||||
TagTreeNode.new(node, `milvus.${milvus.id}.${x.name}`, x.username, NodeTypeMilvusAc)
|
||||
.withNodeComponent(NodeMilvusAc)
|
||||
.withParams({ ...milvus, selectAuthCert: x })
|
||||
.withIsLeaf(true)
|
||||
@@ -51,7 +67,7 @@ const NodeTypeMilvusTag = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(as
|
||||
const milvusInfos = res.list;
|
||||
await sleep(100);
|
||||
return milvusInfos.map((x: any) => {
|
||||
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeMilvus).withParams(x).withNodeComponent(NodeMilvus);
|
||||
return TagTreeNode.new(parentNode, `milvus.${x.id}`, x.name, NodeTypeMilvus).withParams(x).withNodeComponent(NodeMilvus);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,57 +3,58 @@ import { milvusApi } from '@/views/ops/milvus/api';
|
||||
import { setCurrentAcName } from './authCert';
|
||||
|
||||
/**
|
||||
* 缓存milvus一些参数
|
||||
* Milvus 动态 store 工厂函数,支持按 tabKey 隔离状态。
|
||||
* - 不传 id 或传 'milvusStore' 时返回全局默认 store(向后兼容)
|
||||
* - 传自定义 id(如 tabKey)时返回独立的 per-tab store
|
||||
*/
|
||||
export const useMilvusStore = defineStore('milvusStore', {
|
||||
state: (): MilvusState => ({
|
||||
dbs: [],
|
||||
selectedDb: 'default',
|
||||
collections: [],
|
||||
selectedCollection: '',
|
||||
authCertName: '',
|
||||
}),
|
||||
actions: {
|
||||
setDbs(dbs: any[]) {
|
||||
this.dbs = dbs;
|
||||
export const useMilvusStore = (id: string = 'milvusStore') =>
|
||||
defineStore(id, {
|
||||
state: (): MilvusState => ({
|
||||
dbs: [],
|
||||
selectedDb: 'default',
|
||||
collections: [],
|
||||
selectedCollection: '',
|
||||
authCertName: '',
|
||||
}),
|
||||
actions: {
|
||||
setDbs(dbs: any[]) {
|
||||
this.dbs = dbs;
|
||||
},
|
||||
async refreshDbs(milvusId: number) {
|
||||
const res = await milvusApi.listDatabases(milvusId);
|
||||
// res 通过dbid排序
|
||||
res.sort((a: any, b: any) => {
|
||||
return a.create_time.localeCompare(b.create_time);
|
||||
});
|
||||
this.dbs = res;
|
||||
if (res.length > 0) {
|
||||
this.selectedDb = res[0].name;
|
||||
milvusApi.useDatabase(res[0].id, res[0].name);
|
||||
}
|
||||
},
|
||||
setSelectedDb(db: string) {
|
||||
this.selectedDb = db;
|
||||
},
|
||||
setSelectedCollection(coll: string) {
|
||||
this.selectedCollection = coll;
|
||||
},
|
||||
setCollections(collections: string[]) {
|
||||
collections.sort();
|
||||
this.collections = collections;
|
||||
// 默认选中第一个 collection
|
||||
if (!this.selectedCollection && this.collections.length > 0) {
|
||||
this.setSelectedCollection(this.collections[0]);
|
||||
}
|
||||
},
|
||||
clear() {
|
||||
this.collections = [];
|
||||
this.selectedCollection = '';
|
||||
this.selectedDb = 'default';
|
||||
this.dbs = [];
|
||||
},
|
||||
setAuthCertName(name: string) {
|
||||
this.authCertName = name;
|
||||
setCurrentAcName(name);
|
||||
},
|
||||
},
|
||||
async refreshDbs(milvusId: number) {
|
||||
const res = await milvusApi.listDatabases(milvusId);
|
||||
// res 通过dbid排序
|
||||
res.sort((a: any, b: any) => {
|
||||
return a.create_time.localeCompare(b.create_time);
|
||||
});
|
||||
this.dbs = res;
|
||||
if (res.length > 0) {
|
||||
this.selectedDb = res[0].name;
|
||||
milvusApi.useDatabase(res[0].id, res[0].name);
|
||||
}
|
||||
},
|
||||
setSelectedDb(db: string) {
|
||||
this.selectedDb = db;
|
||||
},
|
||||
setSelectedCollection(coll: string) {
|
||||
console.log('[MilvusStore] 切换 collection:', coll);
|
||||
this.selectedCollection = coll;
|
||||
},
|
||||
setCollections(collections: string[]) {
|
||||
collections.sort();
|
||||
this.collections = collections;
|
||||
// 默认选中第一个 collection
|
||||
if (!this.selectedCollection && this.collections.length > 0) {
|
||||
this.setSelectedCollection(this.collections[0]);
|
||||
}
|
||||
},
|
||||
clear() {
|
||||
console.log('[MilvusStore] 清空状态');
|
||||
this.collections = [];
|
||||
this.selectedCollection = '';
|
||||
this.selectedDb = 'default';
|
||||
this.dbs = [];
|
||||
},
|
||||
setAuthCertName(name: string) {
|
||||
this.authCertName = name;
|
||||
setCurrentAcName(name);
|
||||
},
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -46,14 +46,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { Msg, useI18nCreateTitle, useI18nDeleteConfirm, useI18nEditTitle } from '@/hooks/useI18n';
|
||||
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { getTagPathSearchItem } from '../component/tag';
|
||||
import TagCodePath from '../component/TagCodePath.vue';
|
||||
import { mongoApi } from './api';
|
||||
|
||||
@@ -71,10 +69,7 @@ const props = defineProps({
|
||||
const route = useRoute();
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
|
||||
const searchItems = [
|
||||
SearchItem.input('keyword', 'common.keyword').withPlaceholder('mongo.keywordPlaceholder'),
|
||||
getTagPathSearchItem(TagResourceTypeEnum.Mongo.value),
|
||||
];
|
||||
const searchItems = [SearchItem.input('keyword', 'common.keyword').withPlaceholder('mongo.keywordPlaceholder')];
|
||||
|
||||
const columns = [
|
||||
TableColumn.new('name', 'common.name').isSlot('name').setAddWidth(25),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="mongo-data-tab card h-full !p-1 w-full">
|
||||
<div class="mongo-data-tab card h-full !p-1 w-full flex flex-col">
|
||||
<el-row v-if="nowColl">
|
||||
<el-descriptions class="!w-full" :column="10" size="small" border>
|
||||
<!-- <el-descriptions-item label-align="right" label="tag">xxx</el-descriptions-item> -->
|
||||
@@ -28,8 +28,8 @@
|
||||
</el-descriptions>
|
||||
</el-row>
|
||||
|
||||
<el-row type="flex">
|
||||
<el-tabs @tab-remove="removeDataTab" class="!w-full ml-1" v-model="state.activeName">
|
||||
<el-row type="flex" class="flex-1 min-h-0">
|
||||
<el-tabs @tab-remove="removeDataTab" class="!w-full ml-1 h-full flex flex-col" v-model="state.activeName">
|
||||
<el-tab-pane closable v-for="dt in state.dataTabs" :key="dt.key" :label="dt.label" :name="dt.key">
|
||||
<el-row>
|
||||
<el-col :span="2">
|
||||
@@ -50,7 +50,7 @@
|
||||
</el-input>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-scrollbar class="mongo-data-tab-data">
|
||||
<el-scrollbar class="mongo-data-tab-data flex-1 min-h-0" v-loading="findLoading">
|
||||
<el-row>
|
||||
<el-col :span="6" v-for="item in dt.datas" :key="item">
|
||||
<el-card :body-style="{ padding: '0px', position: 'relative' }">
|
||||
@@ -61,7 +61,12 @@
|
||||
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-popconfirm @confirm="onDeleteDoc(item.value)" :title="$t('mongo.deleteDocConfirm')" width="160">
|
||||
<el-popconfirm
|
||||
@confirm="onDeleteDoc(item.value)"
|
||||
:title="$t('mongo.deleteDocConfirm')"
|
||||
width="160"
|
||||
:teleported="false"
|
||||
>
|
||||
<template #reference>
|
||||
<el-link v-auth="perms.delData" underline="never" type="danger" icon="DocumentDelete"> </el-link>
|
||||
</template>
|
||||
@@ -121,9 +126,8 @@ import { isTrue, notBlank } from '@/common/assert';
|
||||
import { formatByteSize } from '@/common/utils/format';
|
||||
import { Msg } from '@/hooks/useI18n';
|
||||
import { mongoApi } from '@/views/ops/mongo/api';
|
||||
import { MongoOpComp } from '@/views/ops/mongo/resource';
|
||||
import { ResourceOpCtxKey } from '@/views/ops/resource/resource';
|
||||
import { computed, defineAsyncComponent, getCurrentInstance, inject, onMounted, reactive, ref, toRefs } from 'vue';
|
||||
import { ResourceOpCtxKey } from '@/views/ops/resource/resourceOp';
|
||||
import { computed, defineAsyncComponent, inject, onMounted, reactive, ref, toRefs } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ResourceOpCtx } from '../../component/tag';
|
||||
|
||||
@@ -138,6 +142,10 @@ const perms = {
|
||||
|
||||
const resourceOpCtx: ResourceOpCtx | undefined = inject(ResourceOpCtxKey);
|
||||
|
||||
const props = defineProps<{
|
||||
tabKey?: string;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits(['init']);
|
||||
|
||||
const findParamInputRef: any = ref(null);
|
||||
@@ -179,12 +187,10 @@ const nowColl = computed(() => {
|
||||
return getNowDataTab();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
emits('init', { name: MongoOpComp.name, ref: getCurrentInstance()?.exposed });
|
||||
});
|
||||
onMounted(() => {});
|
||||
|
||||
const changeCollection = async (id: any, schema: string, collection: string) => {
|
||||
const label = `${id}:\`${schema}\`.${collection}`;
|
||||
const label = `${schema}.${collection}`;
|
||||
let dataTab = state.dataTabs[label];
|
||||
if (!dataTab) {
|
||||
// 默认查询参数
|
||||
@@ -230,6 +236,8 @@ const confirmFindDialog = () => {
|
||||
findCommand(state.activeName);
|
||||
};
|
||||
|
||||
const findLoading = ref(false);
|
||||
|
||||
const findCommand = async (key: string) => {
|
||||
const dataTab = getNowDataTab();
|
||||
const findParma = dataTab.findParam;
|
||||
@@ -242,27 +250,32 @@ const findCommand = async (key: string) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const datas = await mongoApi.findCommand.request({
|
||||
id: dataTab.mongoId,
|
||||
database: dataTab.database,
|
||||
collection: dataTab.collection,
|
||||
filter,
|
||||
sort,
|
||||
limit: findParma.limit || 12,
|
||||
skip: findParma.skip || 0,
|
||||
});
|
||||
state.dataTabs[key].datas = wrapDatas(datas);
|
||||
try {
|
||||
findLoading.value = true;
|
||||
const datas = await mongoApi.findCommand.request({
|
||||
id: dataTab.mongoId,
|
||||
database: dataTab.database,
|
||||
collection: dataTab.collection,
|
||||
filter,
|
||||
sort,
|
||||
limit: findParma.limit || 12,
|
||||
skip: findParma.skip || 0,
|
||||
});
|
||||
state.dataTabs[key].datas = wrapDatas(datas);
|
||||
|
||||
// 获取coll stats
|
||||
state.dataTabs[key].stats = await mongoApi.runCommand.request({
|
||||
id: dataTab.mongoId,
|
||||
database: dataTab.database,
|
||||
command: [
|
||||
{
|
||||
collStats: dataTab.collection,
|
||||
},
|
||||
],
|
||||
});
|
||||
// 获取coll stats
|
||||
state.dataTabs[key].stats = await mongoApi.runCommand.request({
|
||||
id: dataTab.mongoId,
|
||||
database: dataTab.database,
|
||||
command: [
|
||||
{
|
||||
collStats: dataTab.collection,
|
||||
},
|
||||
],
|
||||
});
|
||||
} finally {
|
||||
findLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -393,6 +406,9 @@ const getNowDataTab = () => {
|
||||
|
||||
defineExpose({
|
||||
changeCollection,
|
||||
onRefresh: () => {
|
||||
findCommand(state.activeName);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -406,8 +422,16 @@ defineExpose({
|
||||
}
|
||||
|
||||
.mongo-data-tab {
|
||||
.mongo-data-tab-data {
|
||||
height: calc(100vh - 230px);
|
||||
.el-tabs__content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.el-tab-pane {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__header {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user