mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 15:30:25 +08:00
fix: 资源关联多标签删除、数据库实例删除等问题修复与数据库等名称过滤优化
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
|
||||
### 介绍
|
||||
|
||||
web 版 **linux(终端[终端回放、命令过滤] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle sqlserver 达梦 高斯 sqlite)数据同步 数据迁移、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台**
|
||||
web 版 **linux(终端[终端回放、命令过滤] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle sqlserver 达梦 高斯 sqlite)数据操作 数据同步 数据迁移、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台**
|
||||
|
||||
### 开发语言与主要框架
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"cropperjs": "^1.6.1",
|
||||
"dayjs": "^1.11.11",
|
||||
"echarts": "^5.5.0",
|
||||
"element-plus": "^2.7.3",
|
||||
"element-plus": "^2.7.4",
|
||||
"js-base64": "^3.7.7",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -57,7 +57,7 @@
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.77.1",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.11",
|
||||
"vite": "^5.2.12",
|
||||
"vue-eslint-parser": "^9.4.2"
|
||||
},
|
||||
"browserslist": [
|
||||
|
||||
@@ -97,43 +97,6 @@ export function getTextWidth(str: string) {
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内容所需要占用的宽度
|
||||
*/
|
||||
export function getContentWidth(content: any): number {
|
||||
if (!content) {
|
||||
return 50;
|
||||
}
|
||||
// 以下分配的单位长度可根据实际需求进行调整
|
||||
let flexWidth = 0;
|
||||
for (const char of content) {
|
||||
if (flexWidth > 500) {
|
||||
break;
|
||||
}
|
||||
if ((char >= '0' && char <= '9') || (char >= 'a' && char <= 'z')) {
|
||||
// 小写字母、数字字符
|
||||
flexWidth += 9.3;
|
||||
continue;
|
||||
}
|
||||
if (char >= 'A' && char <= 'Z') {
|
||||
flexWidth += 9;
|
||||
continue;
|
||||
}
|
||||
if (char >= '\u4e00' && char <= '\u9fa5') {
|
||||
// 如果是中文字符,为字符分配16个单位宽度
|
||||
flexWidth += 20;
|
||||
} else {
|
||||
// 其他种类字符
|
||||
flexWidth += 8;
|
||||
}
|
||||
}
|
||||
// if (flexWidth > 450) {
|
||||
// // 设置最大宽度
|
||||
// flexWidth = 450;
|
||||
// }
|
||||
return flexWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns uuid
|
||||
@@ -179,3 +142,38 @@ export async function copyToClipboard(txt: string, selector: string = '#copyValu
|
||||
clipboard.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
export function fuzzyMatchField(keyword: string, fields: any[], ...valueExtractFuncs: Function[]) {
|
||||
keyword = keyword?.toLowerCase();
|
||||
return fields.filter((field) => {
|
||||
for (let valueExtractFunc of valueExtractFuncs) {
|
||||
const value = valueExtractFunc(field)?.toLowerCase();
|
||||
if (isPrefixSubsequence(keyword, value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配是否为前缀子序列 targetTemplate=username prefix=uname -> true,prefix=uname2 -> false
|
||||
* @param prefix 字符串前缀(不连续也可以,但不改变字符的相对顺序)
|
||||
* @param targetTemplate 目标模板
|
||||
* @returns 是否匹配
|
||||
*/
|
||||
export function isPrefixSubsequence(prefix: string, targetTemplate: string) {
|
||||
let i = 0; // 指向prefix的索引
|
||||
let j = 0; // 指向targetTemplate的索引
|
||||
|
||||
while (i < prefix.length && j < targetTemplate.length) {
|
||||
if (prefix[i] === targetTemplate[j]) {
|
||||
// 字符匹配,两个指针都向前移动
|
||||
i++;
|
||||
}
|
||||
j++; // 目标字符串指针始终向前移动
|
||||
}
|
||||
|
||||
// 如果prefix的所有字符都被找到,返回true
|
||||
return i === prefix.length;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ import { NodeType, TagTreeNode } from './tag';
|
||||
import TagInfo from './TagInfo.vue';
|
||||
import { Contextmenu } from '@/components/contextmenu';
|
||||
import { tagApi } from '../tag/api';
|
||||
import { isPrefixSubsequence } from '@/common/utils/string';
|
||||
|
||||
const props = defineProps({
|
||||
resourceType: {
|
||||
@@ -105,8 +106,7 @@ watch(filterText, (val) => {
|
||||
});
|
||||
|
||||
const filterNode = (value: string, data: any) => {
|
||||
if (!value) return true;
|
||||
return data.label.includes(value);
|
||||
return !value || isPrefixSubsequence(value, data.label);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -50,6 +50,7 @@ import { ref, reactive, onMounted } from 'vue';
|
||||
import { tagApi } from '../tag/api';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import EnumValue from '@/common/Enum';
|
||||
import { isPrefixSubsequence } from '@/common/utils/string';
|
||||
|
||||
const props = defineProps({
|
||||
height: {
|
||||
@@ -102,10 +103,7 @@ const search = async () => {
|
||||
};
|
||||
|
||||
const filterNode = (value: string, data: any) => {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
return data.codePath.toLowerCase().includes(value) || data.name.includes(value);
|
||||
return !value || isPrefixSubsequence(value, data.codePath) || isPrefixSubsequence(value, data.name);
|
||||
};
|
||||
|
||||
const onFilterValChanged = (val: string) => {
|
||||
|
||||
@@ -259,7 +259,7 @@ import DbTableData from './DbTableData.vue';
|
||||
import { DbDialect } from '@/views/ops/db/dialect';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { useEventListener, useStorage } from '@vueuse/core';
|
||||
import { copyToClipboard } from '@/common/utils/string';
|
||||
import { copyToClipboard, fuzzyMatchField } from '@/common/utils/string';
|
||||
import DbTableDataForm from './DbTableDataForm.vue';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -476,10 +476,7 @@ const getColumnTips = (queryString: string, callback: any) => {
|
||||
|
||||
let res = [];
|
||||
if (columnNameSearch) {
|
||||
columnNameSearch = columnNameSearch.toLowerCase();
|
||||
res = columns.filter((data: any) => {
|
||||
return data.columnName.toLowerCase().includes(columnNameSearch);
|
||||
});
|
||||
res = fuzzyMatchField(columnNameSearch, columns, (x: any) => x.columnName);
|
||||
}
|
||||
|
||||
completeCond = condition.value;
|
||||
@@ -534,10 +531,12 @@ const filterColumns = (searchKey: string) => {
|
||||
if (!searchKey) {
|
||||
return columns;
|
||||
}
|
||||
searchKey = searchKey.toLowerCase();
|
||||
return columns.filter((data: any) => {
|
||||
return data.columnName.toLowerCase().includes(searchKey) || data.columnComment.toLowerCase().includes(searchKey);
|
||||
});
|
||||
return fuzzyMatchField(
|
||||
searchKey,
|
||||
columns,
|
||||
(x: any) => x.columnName,
|
||||
(x: any) => x.columnComment
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -131,6 +131,7 @@ import { compatibleMysql, editDbTypes, getDbDialect } from '../../dialect/index'
|
||||
import { DbInst } from '../../db';
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import { format as sqlFormatter } from 'sql-formatter';
|
||||
import { fuzzyMatchField } from '@/common/utils/string';
|
||||
|
||||
const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue'));
|
||||
|
||||
@@ -219,17 +220,11 @@ const filterTableInfos = computed(() => {
|
||||
if (!tableNameSearch && !tableCommentSearch) {
|
||||
return tables;
|
||||
}
|
||||
return tables.filter((data: any) => {
|
||||
let tnMatch = true;
|
||||
let tcMatch = true;
|
||||
if (tableNameSearch) {
|
||||
tnMatch = data.tableName.toLowerCase().includes(tableNameSearch.toLowerCase());
|
||||
}
|
||||
if (tableCommentSearch) {
|
||||
tcMatch = data.tableComment.includes(tableCommentSearch);
|
||||
}
|
||||
return tnMatch && tcMatch;
|
||||
});
|
||||
|
||||
if (tableNameSearch) {
|
||||
return fuzzyMatchField(tableNameSearch, tables, (table: any) => table.tableName);
|
||||
}
|
||||
return fuzzyMatchField(tableCommentSearch, tables, (table: any) => table.tableComment);
|
||||
});
|
||||
|
||||
const getTables = async () => {
|
||||
|
||||
@@ -293,6 +293,7 @@ import { getToken } from '@/common/utils/storage';
|
||||
import { convertToBytes, formatByteSize } from '@/common/utils/format';
|
||||
import { getMachineConfig } from '@/common/sysconfig';
|
||||
import { MachineProtocolEnum } from '../enums';
|
||||
import { fuzzyMatchField } from '@/common/utils/string';
|
||||
|
||||
const props = defineProps({
|
||||
machineId: { type: Number },
|
||||
@@ -371,33 +372,7 @@ onMounted(async () => {
|
||||
state.machineConfig = await getMachineConfig();
|
||||
});
|
||||
|
||||
// watch(
|
||||
// () => props.machineId,
|
||||
// () => {
|
||||
// if (props.protocol != MachineProtocolEnum.Ssh.value) {
|
||||
// userMap.clear();
|
||||
// groupMap.clear();
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const machineId = props.machineId;
|
||||
// machineApi.users.request({ machineId }).then((res: any) => {
|
||||
// for (let user of res) {
|
||||
// userMap.set(user.uid, user);
|
||||
// }
|
||||
// });
|
||||
|
||||
// machineApi.groups.request({ machineId }).then((res: any) => {
|
||||
// for (let group of res) {
|
||||
// groupMap.set(group.gid, group);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// );
|
||||
|
||||
const filterFiles = computed(() =>
|
||||
state.files.filter((data: any) => !state.fileNameFilter || data.name.toLowerCase().includes(state.fileNameFilter.toLowerCase()))
|
||||
);
|
||||
const filterFiles = computed(() => fuzzyMatchField(state.fileNameFilter, state.files, (file: any) => file.name));
|
||||
|
||||
const filePathNav = computed(() => {
|
||||
let basePath = state.basePath;
|
||||
|
||||
@@ -156,6 +156,7 @@ import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||
import EnumValue from '@/common/Enum';
|
||||
import TagCodePath from '../component/TagCodePath.vue';
|
||||
import { isPrefixSubsequence } from '@/common/utils/string';
|
||||
|
||||
const MachineList = defineAsyncComponent(() => import('../machine/MachineList.vue'));
|
||||
const InstanceList = defineAsyncComponent(() => import('../db/InstanceList.vue'));
|
||||
@@ -371,8 +372,7 @@ const setNowTabData = () => {
|
||||
};
|
||||
|
||||
const filterNode = (value: string, data: Tree) => {
|
||||
if (!value) return true;
|
||||
return data.codePath.toLowerCase().includes(value) || data.name.includes(value);
|
||||
return !value || isPrefixSubsequence(value, data.codePath) || isPrefixSubsequence(value, data.name);
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
|
||||
@@ -123,6 +123,7 @@ import { formatDate } from '@/common/utils/format';
|
||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||
import { Splitpanes, Pane } from 'splitpanes';
|
||||
import { isPrefixSubsequence } from '@/common/utils/string';
|
||||
|
||||
const menuTypeValue = ResourceTypeEnum.Menu.value;
|
||||
const permissionTypeValue = ResourceTypeEnum.Permission.value;
|
||||
@@ -209,10 +210,7 @@ watch(filterResource, (val) => {
|
||||
});
|
||||
|
||||
const filterNode = (value: string, data: any) => {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
return data.name.includes(value);
|
||||
return !value || isPrefixSubsequence(value, data.name);
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
|
||||
@@ -32,8 +32,8 @@ require (
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/veops/go-ansiterm v0.0.5
|
||||
go.mongodb.org/mongo-driver v1.15.0 // mongo
|
||||
golang.org/x/crypto v0.23.0 // ssh
|
||||
golang.org/x/oauth2 v0.20.0
|
||||
golang.org/x/crypto v0.24.0 // ssh
|
||||
golang.org/x/oauth2 v0.21.0
|
||||
golang.org/x/sync v0.7.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
@@ -94,8 +94,8 @@ require (
|
||||
golang.org/x/exp v0.0.0-20230519143937-03e91628a987 // indirect
|
||||
golang.org/x/image v0.13.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230131230820-1c016267d619 // indirect
|
||||
google.golang.org/grpc v1.52.3 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
|
||||
@@ -2,7 +2,6 @@ package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"mayfly-go/internal/common/consts"
|
||||
"mayfly-go/internal/db/application/dto"
|
||||
"mayfly-go/internal/db/dbm"
|
||||
@@ -13,14 +12,11 @@ import (
|
||||
tagdto "mayfly-go/internal/tag/application/dto"
|
||||
tagentity "mayfly-go/internal/tag/domain/entity"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/structx"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Instance interface {
|
||||
@@ -171,7 +167,7 @@ func (app *instanceAppImpl) SaveDbInstance(ctx context.Context, instance *dto.Sa
|
||||
}
|
||||
|
||||
func (app *instanceAppImpl) Delete(ctx context.Context, instanceId uint64) error {
|
||||
instance, err := app.GetById(instanceId, "name")
|
||||
instance, err := app.GetById(instanceId)
|
||||
if err != nil {
|
||||
return errorx.NewBiz("获取数据库实例错误,数据库实例ID为: %d", instance.Id)
|
||||
}
|
||||
@@ -180,26 +176,16 @@ func (app *instanceAppImpl) Delete(ctx context.Context, instanceId uint64) error
|
||||
DbInstanceId: instanceId,
|
||||
}
|
||||
err = app.restoreApp.restoreRepo.GetByCond(restore)
|
||||
switch {
|
||||
case err == nil:
|
||||
biz.ErrNotNil(err, "不能删除数据库实例【%s】,请先删除关联的数据库恢复任务。", instance.Name)
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
break
|
||||
default:
|
||||
biz.ErrIsNil(err, "删除数据库实例失败: %v", err)
|
||||
if err != nil {
|
||||
return errorx.NewBiz("不能删除数据库实例【%s】,请先删除关联的数据库恢复任务。", instance.Name)
|
||||
}
|
||||
|
||||
backup := &entity.DbBackup{
|
||||
DbInstanceId: instanceId,
|
||||
}
|
||||
err = app.backupApp.backupRepo.GetByCond(backup)
|
||||
switch {
|
||||
case err == nil:
|
||||
biz.ErrNotNil(err, "不能删除数据库实例【%s】,请先删除关联的数据库备份任务。", instance.Name)
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
break
|
||||
default:
|
||||
biz.ErrIsNil(err, "删除数据库实例失败: %v", err)
|
||||
if err != nil {
|
||||
return errorx.NewBiz("不能删除数据库实例【%s】,请先删除关联的数据库备份任务。", instance.Name)
|
||||
}
|
||||
|
||||
dbs, _ := app.dbApp.ListByCond(&entity.Db{
|
||||
|
||||
@@ -264,6 +264,9 @@ func getInterfaceInfo(iInfo string, stats *Stats) (err error) {
|
||||
}
|
||||
|
||||
func getCPU(cpuInfo string, stats *Stats) (err error) {
|
||||
if !strings.Contains(cpuInfo, ":") {
|
||||
return
|
||||
}
|
||||
// %Cpu(s): 6.1 us, 3.0 sy, 0.0 ni, 90.9 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
value := strings.Split(cpuInfo, ":")[1]
|
||||
values := strings.Split(value, ",")
|
||||
|
||||
@@ -312,6 +312,7 @@ func (p *tagTreeAppImpl) DeleteTagByParam(ctx context.Context, param *dto.DelRes
|
||||
}
|
||||
|
||||
delTagType := param.ChildType
|
||||
var childrenTagIds []uint64
|
||||
for _, resourceTag := range resourceTags {
|
||||
// 获取所有关联的子标签
|
||||
childrenTag, _ := p.ListByCond(model.NewCond().RLike("code_path", resourceTag.CodePath).Eq("type", delTagType))
|
||||
@@ -319,14 +320,16 @@ func (p *tagTreeAppImpl) DeleteTagByParam(ctx context.Context, param *dto.DelRes
|
||||
continue
|
||||
}
|
||||
|
||||
childrenTagIds := collx.ArrayMap(childrenTag, func(item *entity.TagTree) uint64 {
|
||||
childrenTagIds = append(childrenTagIds, collx.ArrayMap(childrenTag, func(item *entity.TagTree) uint64 {
|
||||
return item.Id
|
||||
})
|
||||
// 删除code_path下的所有子标签
|
||||
return p.deleteByIds(ctx, childrenTagIds)
|
||||
})...)
|
||||
}
|
||||
|
||||
return nil
|
||||
if len(childrenTagIds) == 0 {
|
||||
return nil
|
||||
}
|
||||
// 删除code_path下的所有子标签
|
||||
return p.deleteByIds(ctx, collx.ArrayDeduplicate(childrenTagIds))
|
||||
}
|
||||
|
||||
func (p *tagTreeAppImpl) ListByQuery(condition *entity.TagTreeQuery, toEntity any) {
|
||||
|
||||
Reference in New Issue
Block a user