refactor: 数据库实例与凭证关联至标签&其他问题修复重构等

This commit is contained in:
meilin.huang
2024-04-17 21:28:28 +08:00
parent f4162c38db
commit 01d3e1ad28
68 changed files with 1421 additions and 809 deletions

View File

@@ -1,14 +1,24 @@
import EnumValue from './Enum';
// 资源类型
export const ResourceTypeEnum = {
Machine: EnumValue.of(1, '机器').setExtra({ icon: 'Monitor', iconColor: 'var(--el-color-primary)' }).tagTypeSuccess(),
Db: EnumValue.of(2, '数据库实例').setExtra({ icon: 'Coin', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
Redis: EnumValue.of(3, 'redis').setExtra({ icon: 'iconfont icon-redis', iconColor: 'var(--el-color-danger)' }).tagTypeInfo(),
Mongo: EnumValue.of(4, 'mongo').setExtra({ icon: 'iconfont icon-mongo', iconColor: 'var(--el-color-success)' }).tagTypeDanger(),
};
// 标签关联的资源类型
export const TagResourceTypeEnum = {
AuthCert: EnumValue.of(-2, '公共凭证').setExtra({ icon: 'Ticket' }),
Tag: EnumValue.of(-1, '标签').setExtra({ icon: 'CollectionTag' }),
Machine: EnumValue.of(1, '机器').setExtra({ icon: 'Monitor' }).tagTypeSuccess(),
Db: EnumValue.of(2, '数据库').setExtra({ icon: 'Coin' }).tagTypeWarning(),
Redis: EnumValue.of(3, 'redis').setExtra({ icon: 'iconfont icon-redis' }).tagTypeInfo(),
Mongo: EnumValue.of(4, 'mongo').setExtra({ icon: 'iconfont icon-mongo' }).tagTypeDanger(),
Machine: ResourceTypeEnum.Machine,
Db: ResourceTypeEnum.Db,
Redis: ResourceTypeEnum.Redis,
Mongo: ResourceTypeEnum.Mongo,
MachineAuthCert: EnumValue.of(11, '机器-授权凭证').setExtra({ icon: 'Ticket' }),
MachineAuthCert: EnumValue.of(11, '机器-授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
DbAuthCert: EnumValue.of(21, '数据库-授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
DbName: EnumValue.of(22, '数据库').setExtra({ icon: 'Coin' }),
};

View File

@@ -15,7 +15,7 @@ const config = {
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
// 系统版本
version: 'v1.8.0',
version: 'v1.8.1',
};
export default config;

View File

@@ -1,9 +1,9 @@
export const AccountUsernamePattern = {
pattern: /^[a-zA-Z0-9_]{5,20}$/g,
message: '只允许输入5-20位大小写字母、数字、下划线',
message: '只允许输入5-20位大小写字母、数字、_-.:',
};
export const ResourceCodePattern = {
pattern: /^[a-zA-Z0-9_.:]{1,32}$/g,
message: '只允许输入1-32位大小写字母、数字、_.:',
pattern: /^[a-zA-Z0-9_\-.:]{1,32}$/g,
message: '只允许输入1-32位大小写字母、数字、_-.:',
};

View File

@@ -44,6 +44,7 @@ export const usePageTable = (
}
let res = await api.request(sp);
res.list = res.list || [];
dataCallBack && (res = await dataCallBack(res));
if (pageable) {

View File

@@ -159,6 +159,6 @@ export function dynamicImport(dynamicViewsModules: Record<string, Function>, com
return null;
}
console.error(`未匹配到[${component}]组件名对应的组件文件`);
console.warn(`未匹配到[${component}]组件名对应的组件文件`);
return null;
}

View File

@@ -1,6 +1,6 @@
<template>
<div v-if="props.authCerts">
<el-select default-first-option value-key="name" style="width: 100%" v-model="selectAuthCert" size="small">
<el-select value-key="name" v-model="selectAuthCert" size="small">
<el-option v-for="item in props.authCerts" :key="item.name" :label="item.username" :value="item">
{{ item.username }}
<el-divider direction="vertical" border-style="dashed" />

View File

@@ -112,7 +112,7 @@
</template>
<script lang="ts" setup>
import { reactive, ref, toRefs, onMounted, watch, computed } from 'vue';
import { reactive, ref, toRefs, computed, watch } from 'vue';
import { AuthCertTypeEnum, AuthCertCiphertextTypeEnum } from '../tag/enums';
import EnumTag from '@/components/enumtag/EnumTag.vue';
import { resourceAuthCertApi } from '../tag/api';
@@ -166,6 +166,13 @@ const rules = {
trigger: ['blur'],
},
],
resourceCode: [
{
required: true,
message: '请输入资源编号',
trigger: ['change', 'blur'],
},
],
};
const emit = defineEmits(['confirm', 'cancel']);
@@ -184,10 +191,6 @@ const showResourceEdit = computed(() => {
return state.form.type != AuthCertTypeEnum.Public.value && !props.resourceEdit;
});
onMounted(() => {
setForm(props.authCert);
});
watch(
() => props.authCert,
(val: any) => {
@@ -217,7 +220,6 @@ const changeType = (val: any) => {
const changeCiphertextType = (val: any) => {
if (val == AuthCertCiphertextTypeEnum.Public.value) {
state.form.type = AuthCertTypeEnum.Private.value;
getPublicAuthCerts();
}
};

View File

@@ -1,18 +1,18 @@
<template>
<div v-if="props.tags">
<el-row v-for="(tag, idx) in props.tags?.slice(0, 1)" :key="idx">
<TagInfo :tag-path="tag.tagPath" />
<span class="ml3">{{ tag.tagPath }}</span>
<el-row v-for="(tagPath, idx) in tagPaths?.slice(0, 1)" :key="idx">
<TagInfo :tag-path="tagPath" />
<span class="ml3">{{ tagPath }}</span>
<!-- 展示剩余的标签信息 -->
<el-popover :show-after="300" v-if="props.tags.length > 1 && idx == 0" placement="top-start" width="230" trigger="hover">
<el-popover :show-after="300" v-if="tagPaths?.length > 1 && idx == 0" placement="top-start" width="230" trigger="hover">
<template #reference>
<SvgIcon class="mt5 ml5" color="var(--el-color-primary)" name="MoreFilled" />
</template>
<el-row v-for="i in props.tags.slice(1)" :key="i">
<TagInfo :tag-path="i.tagPath" />
<span class="ml3">{{ i.tagPath }}</span>
<el-row v-for="i in tagPaths.slice(1)" :key="i">
<TagInfo :tag-path="i" />
<span class="ml3">{{ i }}</span>
</el-row>
</el-popover>
</el-row>
@@ -22,12 +22,18 @@
<script lang="ts" setup>
import SvgIcon from '@/components/svgIcon/index.vue';
import TagInfo from './TagInfo.vue';
import { computed } from 'vue';
import { getTagPath } from './tag';
const props = defineProps({
tags: {
type: [Array<any>],
required: true,
},
});
const tagPaths = computed(() => {
return props.tags?.map((item) => getTagPath(item.codePath)) as any;
});
</script>
<style lang="scss"></style>

View File

@@ -4,15 +4,14 @@
v-bind="$attrs"
v-model="state.selectTags"
@change="changeTag"
style="width: 100%"
:data="tags"
placeholder="请选择关联标签"
:render-after-expand="true"
:default-expanded-keys="[state.selectTags]"
show-checkbox
node-key="id"
node-key="codePath"
:props="{
value: 'id',
value: 'codePath',
label: 'codePath',
children: 'children',
}"
@@ -35,6 +34,7 @@
<script lang="ts" setup>
import { toRefs, reactive, onMounted } from 'vue';
import { tagApi } from '../tag/api';
import { getTagPath } from './tag';
//定义事件
const emit = defineEmits(['update:modelValue', 'changeTag', 'input']);
@@ -47,7 +47,7 @@ const props = defineProps({
const state = reactive({
tags: [],
// 单选则为id多选为id数组
// 单选则为codePath多选为codePath数组
selectTags: [] as any,
});
@@ -55,7 +55,7 @@ const { tags } = toRefs(state);
onMounted(async () => {
if (props.selectTags) {
state.selectTags = props.selectTags;
state.selectTags = props.selectTags.map((item: any) => getTagPath(item));
}
state.tags = await tagApi.getTagTrees.request({ type: -1 });

View File

@@ -171,3 +171,29 @@ export function getTagPathSearchItem(resourceType: number) {
})
);
}
/**
* 根据codepath获取对应的tagPath
* @param codePath codePath tag1/tag2/1|testmachien1/
* @returns tagPath tag1/tag2/
*/
export function getTagPath(codePath: string) {
// 以资源分隔符 "|" 对字符串进行分割
let parts = codePath.split('|');
if (parts.length < 2) {
return codePath;
}
// 从分割后的第一个子串中提取所需部分
let substringBeforeNumber = parts[0];
// 找到最后一个 "/" 的位置
let lastSlashIndex = substringBeforeNumber.lastIndexOf('/');
// 如果找到最后一个 "/" 符号,则截取子串
if (lastSlashIndex !== -1) {
return substringBeforeNumber.slice(0, lastSlashIndex + 1);
}
return codePath;
}

View File

@@ -10,46 +10,20 @@
width="38%"
>
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
<el-form-item ref="tagSelectRef" prop="tagId" label="标签" required>
<tag-tree-select
@change-tag="
(tagIds) => {
form.tagId = tagIds;
tagSelectRef.validate();
}
"
multiple
:select-tags="form.tagId"
style="width: 100%"
/>
<el-form-item prop="code" label="编号" required>
<el-input
:disabled="form.id"
v-model.trim="form.code"
placeholder="请输入编号 (大小写字母、数字、_-.:), 不可修改"
auto-complete="off"
></el-input>
</el-form-item>
<el-form-item prop="instanceId" label="数据库实例" required>
<el-select
:disabled="form.id !== undefined"
remote
:remote-method="getInstances"
@change="changeInstance"
v-model="state.selectInstalce"
value-key="id"
placeholder="请输入实例名称搜索并选择实例"
filterable
clearable
class="w100"
>
<el-option v-for="item in state.instances" :key="item.id" :label="`${item.name}`" :value="item">
{{ item.name }}
<el-divider direction="vertical" border-style="dashed" />
{{ item.type }} / {{ item.host }}:{{ item.port }}
<el-divider direction="vertical" border-style="dashed" />
{{ item.username }}
</el-option>
</el-select>
<el-form-item prop="name" label="名称" required>
<el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="authCertName" label="授权凭证" required>
<el-select @focus="getAuthCerts" @change="changeAuthCert" v-model="form.authCertName" placeholder="请选择授权凭证" filterable>
<el-select @change="changeAuthCert" v-model="form.authCertName" placeholder="请选择授权凭证" filterable>
<el-option v-for="item in state.authCerts" :key="item.id" :label="`${item.name}`" :value="item.name">
{{ item.name }}
@@ -65,13 +39,6 @@
</el-select>
</el-form-item>
<el-form-item prop="code" label="编号" required>
<el-input :disabled="form.id" v-model.trim="form.code" placeholder="请输入编号 (数字字母下划线), 不可修改" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="name" label="别名" required>
<el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="database" label="数据库名">
<el-select
v-model="dbNamesSelected"
@@ -102,7 +69,7 @@
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel()"> </el-button>
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">确 定</el-button>
<el-button type="primary" @click="btnOk"> </el-button>
</div>
</template>
</el-dialog>
@@ -110,23 +77,27 @@
</template>
<script lang="ts" setup>
import { toRefs, reactive, watch, ref } from 'vue';
import { toRefs, reactive, watch, ref, watchEffect } from 'vue';
import { dbApi } from './api';
import { ElMessage } from 'element-plus';
import TagTreeSelect from '../component/TagTreeSelect.vue';
// import TagTreeSelect from '../component/TagTreeSelect.vue';
import type { CheckboxValueType } from 'element-plus';
import ProcdefSelectFormItem from '@/views/flow/components/ProcdefSelectFormItem.vue';
import { DbType } from '@/views/ops/db/dialect';
import { ResourceCodePattern } from '@/common/pattern';
import { resourceAuthCertApi } from '../tag/api';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import EnumTag from '@/components/enumtag/EnumTag.vue';
import { AuthCertCiphertextTypeEnum } from '../tag/enums';
import { resourceAuthCertApi } from '../tag/api';
import { TagResourceTypeEnum } from '@/common/commonEnum';
const props = defineProps({
visible: {
type: Boolean,
},
instance: {
type: [Boolean, Object],
},
db: {
type: [Boolean, Object],
},
@@ -136,7 +107,7 @@ const props = defineProps({
});
//定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'confirm']);
const rules = {
tagId: [
@@ -186,7 +157,7 @@ const checkAllDbNames = ref(false);
const indeterminateDbNames = ref(false);
const dbForm: any = ref(null);
const tagSelectRef: any = ref(null);
// const tagSelectRef: any = ref(null);
const state = reactive({
dialogVisible: false,
@@ -198,7 +169,7 @@ const state = reactive({
authCerts: [] as any,
form: {
id: null,
tagId: [],
// tagId: [],
name: null,
code: '',
database: '',
@@ -212,71 +183,53 @@ const state = reactive({
const { dialogVisible, allDatabases, form, dbNamesSelected } = toRefs(state);
const { isFetching: saveBtnLoading, execute: saveDbExec } = dbApi.saveDb.useApi(form);
watch(props, async (newValue: any) => {
state.dialogVisible = newValue.visible;
watchEffect(() => {
state.dialogVisible = props.visible;
if (!state.dialogVisible) {
return;
}
if (newValue.db) {
state.form = { ...newValue.db };
state.form.tagId = newValue.db.tags.map((t: any) => t.tagId);
const db: any = props.db;
if (db.code) {
state.form = { ...db };
// state.form.tagId = newValue.db.tags.map((t: any) => t.tagId);
// 将数据库名使用空格切割,获取所有数据库列表
state.dbNamesSelected = newValue.db.database.split(' ');
state.dbNamesSelected = db.database.split(' ');
} else {
state.form = {} as any;
state.dbNamesSelected = [];
}
});
const changeInstance = async () => {
state.dbNamesSelected = [];
state.form.instanceId = state.selectInstalce.id;
const changeAuthCert = (val: string) => {
getAllDatabase(val);
};
const getAuthCerts = async () => {
const inst: any = props.instance;
const res = await resourceAuthCertApi.listByQuery.request({
resourceCode: state.selectInstalce.code,
resourceCode: inst.code,
resourceType: TagResourceTypeEnum.Db.value,
pageSize: 100,
});
state.authCerts = res.list || [];
};
const changeAuthCert = (val: string) => {
getAllDatabase(val);
};
const getAllDatabase = async (authCertName: string) => {
if (state.form.instanceId > 0) {
let dbs = await dbApi.getAllDatabase.request({ instanceId: state.form.instanceId, authCertName });
state.allDatabases = dbs;
const req = { ...(props.instance as any) };
req.authCert = state.authCerts?.find((x: any) => x.name == authCertName);
let dbs = await dbApi.getAllDatabase.request(req);
state.allDatabases = dbs;
// 如果是oracle且没查出数据库列表则取实例sid
let instance = state.instances.find((item: any) => item.id === state.form.instanceId);
if (instance && instance.type === DbType.oracle && dbs.length === 0) {
state.allDatabases = [instance.sid];
}
}
};
const getInstances = async (instanceName: string = '', id = 0) => {
if (!id && !instanceName) {
state.instances = [];
return;
}
const data = await dbApi.instances.request({ id, name: instanceName });
if (data) {
state.instances = data.list;
// 如果是oracle且没查出数据库列表则取实例sid
let instance = state.instances.find((item: any) => item.id === state.form.instanceId);
if (instance && instance.type === DbType.oracle && dbs.length === 0) {
state.allDatabases = [instance.sid];
}
};
const open = async () => {
if (state.form.instanceId) {
// 根据id获取因为需要回显实例名称
await getInstances('', state.form.instanceId);
state.selectInstalce = state.instances[0];
await getAuthCerts();
if (state.form.authCertName) {
await getAllDatabase(state.form.authCertName);
}
};
@@ -288,10 +241,11 @@ const btnOk = async () => {
return false;
}
await saveDbExec();
ElMessage.success('保存成功');
emit('val-change', state.form);
cancel();
emit('confirm', state.form);
// await saveDbExec();
// ElMessage.success('保存成功');
// emit('val-change', state.form);
// cancel();
});
};

View File

@@ -6,8 +6,6 @@
:before-query-fn="checkRouteTagPath"
:search-items="searchItems"
v-model:query-form="query"
:show-selection="true"
v-model:selection-data="state.selectionData"
:columns="columns"
lazy
>
@@ -24,11 +22,6 @@
</el-select>
</template>
<template #tableHeader>
<el-button v-auth="perms.saveDb" type="primary" icon="plus" @click="editDb(false)">添加</el-button>
<el-button v-auth="perms.delDb" :disabled="selectionData.length < 1" @click="deleteDb()" type="danger" icon="delete">删除</el-button>
</template>
<template #type="{ data }">
<el-tooltip :content="data.type" placement="top">
<SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
@@ -39,16 +32,26 @@
{{ `${data.host}:${data.port}` }}
</template>
<template #database="{ data }">
<el-popover placement="bottom" :width="200" trigger="click">
<template #reference>
<el-button @click="state.currentDbs = data.database" type="primary" link>查看库</el-button>
</template>
<el-table :data="filterDbs" size="small">
<el-table-column prop="dbName" label="数据库">
<template #header>
<el-input v-model="state.dbNameSearch" size="small" placeholder="库名: 输入可过滤" clearable />
</template>
</el-table-column>
</el-table>
</el-popover>
</template>
<template #tagPath="{ data }">
<ResourceTags :tags="data.tags" />
</template>
<template #action="{ data }">
<span v-if="actionBtns[perms.saveDb]">
<el-button type="primary" @click="editDb(data)" link>编辑</el-button>
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-button type="primary" @click="onShowSqlExec(data)" link>SQL记录</el-button>
<el-divider direction="vertical" border-style="dashed" />
@@ -176,7 +179,7 @@
<el-descriptions-item :span="2" label="主机">{{ infoDialog.instance?.host }}</el-descriptions-item>
<el-descriptions-item :span="1" label="端口">{{ infoDialog.instance?.port }}</el-descriptions-item>
<el-descriptions-item :span="2" label="用户名">{{ infoDialog.instance?.username }}</el-descriptions-item>
<el-descriptions-item :span="2" label="授权凭证">{{ infoDialog.instance.authCertName }}</el-descriptions-item>
<el-descriptions-item :span="1" label="类型">
<SvgIcon :name="getDbDialect(infoDialog.instance?.type).getInfo().icon" :size="20" />{{ infoDialog.instance?.type }}
</el-descriptions-item>
@@ -199,8 +202,7 @@
</template>
<script lang="ts" setup>
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { computed, defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
import { dbApi } from './api';
import config from '@/common/config';
import { joinClientParams } from '@/common/request';
@@ -224,23 +226,8 @@ import { sleep } from '@/common/utils/loading';
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
const props = defineProps({
lazy: {
type: [Boolean],
default: false,
},
});
const perms = {
base: 'db',
saveDb: 'db:save',
delDb: 'db:del',
backupDb: 'db:backup',
restoreDb: 'db:restore',
};
const searchItems = [
getTagPathSearchItem(TagResourceTypeEnum.Db.value),
getTagPathSearchItem(TagResourceTypeEnum.DbName.value),
SearchItem.slot('instanceId', '实例', 'instanceSelect'),
SearchItem.input('code', '编号'),
];
@@ -252,15 +239,21 @@ const columns = ref([
TableColumn.new('instanceName', '实例名'),
TableColumn.new('host', 'ip:port').isSlot().setAddWidth(40),
TableColumn.new('authCertName', '授权凭证'),
TableColumn.new('database', '库').isSlot().setMinWidth(80),
TableColumn.new('flowProcdefKey', '关联流程'),
TableColumn.new('remark', '备注'),
TableColumn.new('code', '编号'),
]);
const perms = {
backupDb: 'db:backup',
restoreDb: 'db:restore',
};
// 该用户拥有的的操作列按钮权限
// const actionBtns = hasPerms([perms.base, perms.saveDb]);
const actionBtns = hasPerms(Object.values(perms));
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter();
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight().alignCenter();
const route = useRoute();
const pageTableRef: Ref<any> = ref(null);
@@ -268,6 +261,8 @@ const state = reactive({
row: {} as any,
dbId: 0,
db: '',
currentDbs: '',
dbNameSearch: '',
instances: [] as any,
/**
* 选中的数据
@@ -343,16 +338,26 @@ const state = reactive({
},
});
const { db, selectionData, query, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbBackupHistoryDialog, dbRestoreDialog } =
toRefs(state);
const { db, query, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbBackupHistoryDialog, dbRestoreDialog } = toRefs(state);
onMounted(async () => {
if (Object.keys(actionBtns).length > 0) {
columns.value.push(actionColumn);
}
if (!props.lazy) {
search();
search();
});
const filterDbs = computed(() => {
const dbsStr = state.currentDbs;
if (!dbsStr) {
return [];
}
const dbs = dbsStr.split(' ').map((db: any) => {
return { dbName: db };
});
return dbs.filter((db: any) => {
return db.dbName.includes(state.dbNameSearch);
});
});
const checkRouteTagPath = (query: any) => {
@@ -402,10 +407,6 @@ const handleMoreActionCommand = (commond: any) => {
showInfo(data);
return;
}
case 'edit': {
editDb(data);
return;
}
case 'dumpDb': {
onDumpDbs(data);
return;
@@ -425,32 +426,6 @@ const handleMoreActionCommand = (commond: any) => {
}
};
const editDb = async (data: any) => {
if (!data) {
state.dbEditDialog.data = null;
state.dbEditDialog.title = '新增数据库资源';
} else {
state.dbEditDialog.data = data;
state.dbEditDialog.title = '修改数据库资源';
}
state.dbEditDialog.visible = true;
};
const deleteDb = async () => {
try {
await ElMessageBox.confirm(`确定删除【${state.selectionData.map((x: any) => x.name).join(', ')}】库?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.deleteDb.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
ElMessage.success('删除成功');
search();
} catch (err) {
//
}
};
const onShowSqlExec = async (row: any) => {
state.sqlExecLogDialog.title = `${row.name}`;
state.sqlExecLogDialog.dbId = row.id;

View File

@@ -0,0 +1,171 @@
<template>
<div>
<el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="50%">
<template #header>
<DrawerHeader :header="title" :back="cancel" />
</template>
<el-table :data="state.dbs" stripe>
<el-table-column prop="name" label="名称" show-overflow-tooltip min-width="100"> </el-table-column>
<el-table-column prop="authCertName" label="授权凭证" min-width="120" show-overflow-tooltip> </el-table-column>
<el-table-column prop="database" label="库" min-width="80">
<template #default="scope">
<el-popover placement="bottom" :width="200" trigger="click">
<template #reference>
<el-button @click="state.currentDbs = scope.row.database" type="primary" link>查看库</el-button>
</template>
<el-table :data="filterDbs" size="small">
<el-table-column prop="dbName" label="数据库">
<template #header>
<el-input v-model="state.dbNameSearch" size="small" placeholder="库名: 输入可过滤" clearable />
</template>
</el-table-column>
</el-table>
</el-popover>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" show-overflow-tooltip min-width="120"> </el-table-column>
<el-table-column prop="flowProcdefKey" label="关联流程" min-width="120" show-overflow-tooltip> </el-table-column>
<el-table-column prop="code" label="编号" show-overflow-tooltip min-width="120"> </el-table-column>
<el-table-column min-wdith="120px">
<template #header>
操作
<el-button v-auth="perms.saveDb" type="primary" circle size="small" icon="Plus" @click="editDb(null)"> </el-button>
</template>
<template #default="scope">
<el-button v-auth="perms.saveDb" @click="editDb(scope.row)" type="primary" icon="edit" link></el-button>
<el-button class="ml1" v-auth="perms.delDb" type="danger" @click="deleteDb(scope.row)" icon="delete" link></el-button>
</template>
</el-table-column>
</el-table>
<db-edit
@confirm="confirmEditDb"
@cancel="cancelEditDb"
:title="dbEditDialog.title"
v-model:visible="dbEditDialog.visible"
:instance="props.instance"
v-model:db="dbEditDialog.data"
></db-edit>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { computed, reactive, toRefs, watchEffect } from 'vue';
import { dbApi } from './api';
import { ElMessage, ElMessageBox } from 'element-plus';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import DbEdit from './DbEdit.vue';
const props = defineProps({
visible: {
type: Boolean,
},
instance: {
type: [Object],
required: true,
},
title: {
type: String,
},
});
const perms = {
base: 'db',
saveDb: 'db:save',
delDb: 'db:del',
};
//定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
const state = reactive({
dialogVisible: false,
dbs: [] as any,
currentDbs: '', // 当前数据库名,空格分割库名
dbNameSearch: '',
dbEditDialog: {
visible: false,
data: null as any,
title: '新增数据库',
},
});
const { dialogVisible, dbEditDialog } = toRefs(state);
watchEffect(() => {
state.dialogVisible = props.visible;
if (!state.dialogVisible) {
return;
}
getDbs();
});
const filterDbs = computed(() => {
const dbsStr = state.currentDbs;
if (!dbsStr) {
return [];
}
const dbs = dbsStr.split(' ').map((db: any) => {
return { dbName: db };
});
return dbs.filter((db: any) => {
return db.dbName.includes(state.dbNameSearch);
});
});
const cancel = () => {
emit('update:visible', false);
emit('cancel');
};
const getDbs = () => {
dbApi.dbs.request({ pageSize: 200, instanceId: props.instance.id }).then((res: any) => {
state.dbs = res.list || [];
});
};
const editDb = (data: any) => {
if (data) {
state.dbEditDialog.data = { ...data };
} else {
state.dbEditDialog.data = {
instanceId: props.instance.id,
};
}
state.dbEditDialog.title = data ? '编辑数据库' : '新增数据库';
state.dbEditDialog.visible = true;
};
const deleteDb = async (db: any) => {
try {
await ElMessageBox.confirm(`确定删除【${db.name}】库?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.deleteDb.request({ id: db.id });
ElMessage.success('删除成功');
getDbs();
} catch (err) {
//
}
};
const confirmEditDb = async (db: any) => {
db.instanceId = props.instance.id;
await dbApi.saveDb.request(db);
ElMessage.success('保存成功');
getDbs();
cancelEditDb();
};
const cancelEditDb = () => {
state.dbEditDialog.visible = false;
state.dbEditDialog.data = {};
};
</script>
<style lang="scss"></style>

View File

@@ -7,9 +7,30 @@
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
<el-divider content-position="left">基本</el-divider>
<el-form-item prop="code" label="编号" required>
<el-input :disabled="form.id" v-model.trim="form.code" placeholder="请输入编号 (数字字母下划线), 不可修改" auto-complete="off"></el-input>
<el-form-item ref="tagSelectRef" prop="tagCodePaths" label="标签">
<tag-tree-select
multiple
@change-tag="
(paths: any) => {
form.tagCodePaths = paths;
tagSelectRef.validate();
}
"
:select-tags="form.tagCodePaths"
style="width: 100%"
/>
</el-form-item>
<el-form-item prop="code" label="编号" required>
<el-input
:disabled="form.id"
v-model.trim="form.code"
placeholder="请输入编号 (大小写字母数字_-.:), 不可修改"
auto-complete="off"
></el-input>
</el-form-item>
<el-form-item prop="name" label="名称" required>
<el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
</el-form-item>
@@ -34,7 +55,7 @@
<el-form-item v-if="form.type !== DbType.sqlite" prop="host" label="host" required>
<el-col :span="18">
<el-input :disabled="form.id !== undefined" v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input>
<el-input v-model.trim="form.host" placeholder="请输入ip" auto-complete="off"></el-input>
</el-col>
<el-col style="text-align: center" :span="1">:</el-col>
<el-col :span="5">
@@ -87,18 +108,7 @@
<el-divider content-position="left">其他</el-divider>
<el-form-item prop="params" label="连接参数">
<el-input v-model.trim="form.params" placeholder="其他连接参数,形如: key1=value1&key2=value2">
<!-- <template #suffix>
<el-link
target="_blank"
href="https://github.com/go-sql-driver/mysql#parameters"
:underline="false"
type="primary"
class="mr5"
>参数参考</el-link
>
</template> -->
</el-input>
<el-input v-model.trim="form.params" placeholder="其他连接参数形如: key1=value1&key2=value2"> </el-input>
</el-form-item>
<el-form-item prop="sshTunnelMachineId" label="SSH隧道">
@@ -107,17 +117,15 @@
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel()"> </el-button>
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk"> </el-button>
</div>
<el-button @click="cancel()">取 消</el-button>
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">确 定</el-button>
</template>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, toRefs, watch } from 'vue';
import { reactive, ref, toRefs, watchEffect } from 'vue';
import { dbApi } from './api';
import { ElMessage } from 'element-plus';
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
@@ -128,6 +136,8 @@ import { ResourceCodePattern } from '@/common/pattern';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import ResourceAuthCertTableEdit from '../component/ResourceAuthCertTableEdit.vue';
import { AuthCertCiphertextTypeEnum } from '../tag/enums';
import TagTreeSelect from '../component/TagTreeSelect.vue';
import { getTagPath } from '../component/tag';
const props = defineProps({
visible: {
@@ -145,6 +155,13 @@ const props = defineProps({
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
const rules = {
tagCodePaths: [
{
required: true,
message: '请选择标签',
trigger: ['change'],
},
],
code: [
{
required: true,
@@ -188,61 +205,63 @@ const rules = {
};
const dbForm: any = ref(null);
const tagSelectRef: any = ref(null);
const DefaultForm = {
id: null,
type: DbType.mysql,
code: '',
name: null,
host: '',
port: null,
extra: '', // 连接需要的额外参数json字符串
params: null,
remark: '',
sshTunnelMachineId: null as any,
authCerts: [],
tagCodePaths: [],
};
const state = reactive({
dialogVisible: false,
extra: {} as any, // 连接需要的额外参数json
form: {
id: null,
type: '',
code: '',
name: null,
host: '',
port: null,
authCerts: [],
extra: '', // 连接需要的额外参数json字符串
params: null,
remark: '',
sshTunnelMachineId: null as any,
},
form: DefaultForm,
submitForm: {} as any,
});
const { dialogVisible, form, submitForm } = toRefs(state);
const { isFetching: saveBtnLoading, execute: saveInstanceExec } = dbApi.saveInstance.useApi(submitForm);
const { isFetching: saveBtnLoading, execute: saveInstanceExec, data: saveInstanceRes } = dbApi.saveInstance.useApi(submitForm);
const { isFetching: testConnBtnLoading, execute: testConnExec } = dbApi.testConn.useApi(submitForm);
watch(props, (newValue: any) => {
state.dialogVisible = newValue.visible;
watchEffect(() => {
state.dialogVisible = props.visible;
if (!state.dialogVisible) {
return;
}
if (newValue.data) {
state.form = { ...newValue.data };
const dbInst: any = props.data;
if (dbInst) {
state.form = { ...dbInst };
state.form.tagCodePaths = dbInst.tags.map((t: any) => t.codePath) || [];
try {
state.extra = JSON.parse(state.form.extra);
} catch (e) {
state.extra = {};
}
} else {
state.form = { port: null, type: DbType.mysql } as any;
state.form = { ...DefaultForm };
state.form.authCerts = [];
}
});
const changeDbType = (val: string) => {
if (!state.form.id) {
state.form.port = getDbDialect(val).getInfo().defaultPort as any;
}
state.extra = {};
};
const getReqForm = async () => {
const reqForm = { ...state.form };
const reqForm: any = { ...state.form };
reqForm.selectAuthCert = null;
reqForm.tags = null;
if (!state.form.sshTunnelMachineId) {
reqForm.sshTunnelMachineId = -1;
}
reqForm.tagCodePaths = state.form.tagCodePaths.map((t: any) => getTagPath(t)) as any;
if (Object.keys(state.extra).length > 0) {
reqForm.extra = JSON.stringify(state.extra);
}
@@ -273,6 +292,7 @@ const btnOk = async () => {
state.submitForm = await getReqForm();
await saveInstanceExec();
ElMessage.success('保存成功');
state.form.id = saveInstanceRes as any;
emit('val-change', state.form);
cancel();
});
@@ -283,5 +303,12 @@ const cancel = () => {
emit('cancel');
state.extra = {};
};
const changeDbType = (val: string) => {
if (!state.form.id) {
state.form.port = getDbDialect(val).getInfo().defaultPort as any;
}
state.extra = {};
};
</script>
<style lang="scss"></style>

View File

@@ -9,6 +9,7 @@
:show-selection="true"
v-model:selection-data="state.selectionData"
:columns="columns"
lazy
>
<template #tableHeader>
<el-button v-auth="perms.saveInstance" type="primary" icon="plus" @click="editInstance(false)">添加</el-button>
@@ -17,6 +18,10 @@
>
</template>
<template #tagPath="{ data }">
<ResourceTags :tags="data.tags" />
</template>
<template #authCert="{ data }">
<ResourceAuthCert v-model:select-auth-cert="data.selectAuthCert" :auth-certs="data.authCerts" />
</template>
@@ -30,6 +35,7 @@
<template #action="{ data }">
<el-button @click="showInfo(data)" link>详情</el-button>
<el-button v-if="actionBtns[perms.saveInstance]" @click="editInstance(data)" type="primary" link>编辑</el-button>
<el-button v-if="actionBtns[perms.saveDb]" @click="editDb(data)" type="primary" link>库配置</el-button>
</template>
</page-table>
@@ -56,11 +62,13 @@
</el-dialog>
<instance-edit
@val-change="search"
@val-change="search()"
:title="instanceEditDialog.title"
v-model:visible="instanceEditDialog.visible"
v-model:data="instanceEditDialog.data"
></instance-edit>
<instance-db-conf :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" :instance="dbEditDialog.instance" />
</div>
</template>
@@ -76,17 +84,30 @@ import SvgIcon from '@/components/svgIcon/index.vue';
import { getDbDialect } from './dialect';
import { SearchItem } from '@/components/SearchForm';
import ResourceAuthCert from '../component/ResourceAuthCert.vue';
import ResourceTags from '../component/ResourceTags.vue';
import { getTagPathSearchItem } from '../component/tag';
import { TagResourceTypeEnum } from '@/common/commonEnum';
const InstanceEdit = defineAsyncComponent(() => import('./InstanceEdit.vue'));
const InstanceDbConf = defineAsyncComponent(() => import('./InstanceDbConf.vue'));
const props = defineProps({
lazy: {
type: [Boolean],
default: false,
},
});
const perms = {
saveInstance: 'db:instance:save',
delInstance: 'db:instance:del',
saveDb: 'db:save',
};
const searchItems = [SearchItem.input('code', '编号'), SearchItem.input('name', '名称')];
const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Db.value), SearchItem.input('code', '编号'), SearchItem.input('name', '名称')];
const columns = ref([
TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),
TableColumn.new('name', '名称'),
TableColumn.new('type', '类型').isSlot().setAddWidth(-15).alignCenter(),
TableColumn.new('host', 'host:port').setFormatFunc((data: any) => `${data.host}:${data.port}`),
@@ -98,7 +119,7 @@ const columns = ref([
// 该用户拥有的的操作列按钮权限
const actionBtns = hasPerms(Object.values(perms));
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(110).fixedRight().alignCenter();
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight().alignCenter();
const pageTableRef: Ref<any> = ref(null);
const state = reactive({
@@ -114,6 +135,7 @@ const state = reactive({
*/
query: {
name: null,
tagPath: '',
pageNum: 1,
pageSize: 0,
},
@@ -126,17 +148,28 @@ const state = reactive({
data: null as any,
title: '新增数据库实例',
},
dbEditDialog: {
visible: false,
instance: null as any,
title: '新增数据库实例',
},
});
const { selectionData, query, infoDialog, instanceEditDialog } = toRefs(state);
const { selectionData, query, infoDialog, instanceEditDialog, dbEditDialog } = toRefs(state);
onMounted(async () => {
if (Object.keys(actionBtns).length > 0) {
columns.value.push(actionColumn);
}
if (!props.lazy) {
search();
}
});
const search = () => {
const search = (tagPath: string = '') => {
if (tagPath) {
state.query.tagPath = tagPath;
}
pageTableRef.value.search();
};
@@ -179,5 +212,13 @@ const deleteInstance = async () => {
//
}
};
const editDb = (data: any) => {
state.dbEditDialog.instance = data;
state.dbEditDialog.title = `配置 "${data.name}" 数据库`;
state.dbEditDialog.visible = true;
};
defineExpose({ search });
</script>
<style lang="scss"></style>

View File

@@ -2,7 +2,7 @@
<div class="db-sql-exec">
<Splitpanes class="default-theme">
<Pane size="20" max-size="30">
<tag-tree :resource-type="TagResourceTypeEnum.Db.value" :tag-path-node-type="NodeTypeTagPath" ref="tagTreeRef">
<tag-tree :resource-type="TagResourceTypeEnum.DbName.value" :tag-path-node-type="NodeTypeTagPath" ref="tagTreeRef">
<template #prefix="{ data }">
<span v-if="data.type.value == SqlExecNodeType.DbInst">
<el-popover

View File

@@ -39,7 +39,7 @@ export const dbApi = {
instances: Api.newGet('/instances'),
getInstance: Api.newGet('/instances/{instanceId}'),
getAllDatabase: Api.newGet('/instances/{instanceId}/databases'),
getAllDatabase: Api.newPost('/instances/databases'),
getInstanceServerInfo: Api.newGet('/instances/{instanceId}/server-info'),
testConn: Api.newPost('/instances/test-conn'),
saveInstance: Api.newPost('/instances'),

View File

@@ -113,7 +113,7 @@
:loading="dt.loading"
:abort-fn="dt.abortFn"
:height="tableDataHeight"
empty-text="tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改"
:empty-text="state.tableDataEmptyText"
@change-updated-field="changeUpdatedField($event, dt)"
@data-delete="onDeleteData($event, dt)"
></db-table-data>
@@ -221,6 +221,7 @@ const state = reactive({
activeTab: 1,
editorHeight: '500',
tableDataHeight: '250px',
tableDataEmptyText: 'tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改',
});
const { tableDataHeight } = toRefs(state);
@@ -368,9 +369,8 @@ const onRunSql = async (newTab = false) => {
await execute();
const colAndData: any = data.value;
if (!colAndData.res || colAndData.res.length === 0) {
ElMessage.warning('未查询到结果集');
return;
if (colAndData.res.length == 0) {
state.tableDataEmptyText = '查无数据';
}
// 要实时响应,故需要用索引改变数据才生效
@@ -467,7 +467,7 @@ const formatSql = () => {
return;
}
const formatDialect = getNowDbInst().getDialect().getInfo().formatSqlDialect;
const formatDialect: any = getNowDbInst().getDialect().getInfo().formatSqlDialect;
let sql = monacoEditor.getModel()?.getValueInRange(selection);
// 有选中sql则格式化并替换选中sql, 否则格式化编辑器所有内容

View File

@@ -121,7 +121,7 @@
<template #empty>
<div style="text-align: center">
<el-empty class="h100" :description="state.emptyText" :image-size="100" />
<el-empty class="h100" :description="props.emptyText" :image-size="100" />
</div>
</template>
</el-table-v2>
@@ -342,8 +342,6 @@ const state = reactive({
columns: [] as any,
loading: false,
tableHeight: '600px',
emptyText: '',
execTime: 0,
contextmenu: {
dropdown: {
@@ -434,7 +432,6 @@ onMounted(async () => {
console.log('in DbTable mounted');
state.tableHeight = props.height;
state.loading = props.loading;
state.emptyText = props.emptyText;
state.dbId = props.dbId;
state.dbType = getNowDbInst().type;

View File

@@ -308,7 +308,7 @@ const showCreateDdl = async (row: any) => {
tableName: row.tableName,
});
state.ddlDialog.ddl = sqlFormatter(res, { language: getDbDialect(props.dbType).getInfo().formatSqlDialect });
state.ddlDialog.ddl = sqlFormatter(res, { language: getDbDialect(props.dbType).getInfo().formatSqlDialect as any });
state.ddlDialog.visible = true;
};

View File

@@ -7,22 +7,26 @@
<el-form :model="form" ref="machineForm" :rules="rules" label-width="auto">
<el-divider content-position="left">基本</el-divider>
<el-form-item ref="tagSelectRef" prop="tagId" label="标签">
<el-form-item ref="tagSelectRef" prop="tagCodePaths" label="标签">
<tag-tree-select
multiple
@change-tag="
(tagIds) => {
form.tagId = tagIds;
(paths) => {
form.tagCodePaths = paths;
tagSelectRef.validate();
}
"
:tag-path="form.tagPath"
:select-tags="form.tagId"
:select-tags="form.tagCodePaths"
style="width: 100%"
/>
</el-form-item>
<el-form-item prop="code" label="编号" required>
<el-input :disabled="form.id" v-model.trim="form.code" placeholder="请输入编号 (数字字母下划线), 不可修改" auto-complete="off"></el-input>
<el-input
:disabled="form.id"
v-model.trim="form.code"
placeholder="请输入编号 (大小写字母数字_-.:), 不可修改"
auto-complete="off"
></el-input>
</el-form-item>
<el-form-item prop="name" label="名称" required>
<el-input v-model.trim="form.name" placeholder="请输入机器别名" auto-complete="off"></el-input>
@@ -78,7 +82,7 @@
</template>
<script lang="ts" setup>
import { reactive, ref, toRefs, watch } from 'vue';
import { reactive, ref, toRefs, watchEffect } from 'vue';
import { machineApi } from './api';
import { ElMessage } from 'element-plus';
import TagTreeSelect from '../component/TagTreeSelect.vue';
@@ -88,6 +92,7 @@ import { MachineProtocolEnum } from './enums';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { ResourceCodePattern } from '@/common/pattern';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { getTagPath } from '../component/tag';
const props = defineProps({
visible: {
@@ -105,7 +110,7 @@ const props = defineProps({
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
const rules = {
tagId: [
tagCodePaths: [
{
required: true,
message: '请选择标签',
@@ -159,7 +164,7 @@ const defaultForm = {
protocol: MachineProtocolEnum.Ssh.value,
name: null,
authCerts: [],
tagId: [],
tagCodePaths: [],
remark: '',
sshTunnelMachineId: null as any,
enableRecorder: -1,
@@ -178,16 +183,17 @@ const { dialogVisible, form, submitForm } = toRefs(state);
const { isFetching: testConnBtnLoading, execute: testConnExec } = machineApi.testConn.useApi(submitForm);
const { isFetching: saveBtnLoading, execute: saveMachineExec } = machineApi.saveMachine.useApi(submitForm);
watch(props, async (newValue: any) => {
state.dialogVisible = newValue.visible;
watchEffect(() => {
state.dialogVisible = props.visible;
if (!state.dialogVisible) {
state.form = defaultForm;
return;
}
if (newValue.machine) {
state.form = { ...newValue.machine };
state.form.tagId = newValue.machine.tags.map((t: any) => t.tagId);
state.form.authCerts = newValue.machine.authCerts || [];
const machine: any = props.machine;
if (machine) {
state.form = { ...machine };
state.form.tagCodePaths = machine.tags.map((t: any) => t.codePath);
state.form.authCerts = machine.authCerts || [];
}
});
@@ -230,6 +236,7 @@ const getReqForm = () => {
if (!state.form.sshTunnelMachineId || state.form.sshTunnelMachineId <= 0) {
reqForm.sshTunnelMachineId = -1;
}
reqForm.tagCodePaths = state.form.tagCodePaths.map((t: any) => getTagPath(t)) as any;
return reqForm;
};

View File

@@ -4,16 +4,16 @@
<el-form :model="form" ref="mongoForm" :rules="rules" label-width="85px">
<el-tabs v-model="tabActiveName">
<el-tab-pane label="基础信息" name="basic">
<el-form-item ref="tagSelectRef" prop="tagId" label="标签" required>
<el-form-item ref="tagSelectRef" prop="tagCodePaths" label="标签" required>
<tag-tree-select
@change-tag="
(tagIds) => {
form.tagId = tagIds;
(tagCodePaths) => {
form.tagCodePaths = tagCodePaths;
tagSelectRef.validate();
}
"
multiple
:select-tags="form.tagId"
:select-tags="form.tagCodePaths"
style="width: 100%"
/>
</el-form-item>
@@ -21,7 +21,7 @@
<el-input
:disabled="form.id"
v-model.trim="form.code"
placeholder="请输入编号 (数字字母下划线), 不可修改"
placeholder="请输入编号 (大小写字母数字_-.:), 不可修改"
auto-complete="off"
></el-input>
</el-form-item>
@@ -59,12 +59,13 @@
</template>
<script lang="ts" setup>
import { toRefs, reactive, watch, ref } from 'vue';
import { toRefs, reactive, ref, watchEffect } from 'vue';
import { mongoApi } from './api';
import { ElMessage } from 'element-plus';
import TagTreeSelect from '../component/TagTreeSelect.vue';
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
import { ResourceCodePattern } from '@/common/pattern';
import { getTagPath } from '../component/tag';
const props = defineProps({
visible: {
@@ -82,7 +83,7 @@ const props = defineProps({
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
const rules = {
tagId: [
tagCodePaths: [
{
required: true,
message: '请选择标签',
@@ -129,7 +130,7 @@ const state = reactive({
name: null,
uri: null,
sshTunnelMachineId: null as any,
tagId: [],
tagCodePaths: [],
},
submitForm: {},
});
@@ -139,15 +140,16 @@ const { dialogVisible, tabActiveName, form, submitForm } = toRefs(state);
const { isFetching: testConnBtnLoading, execute: testConnExec } = mongoApi.testConn.useApi(submitForm);
const { isFetching: saveBtnLoading, execute: saveMongoExec } = mongoApi.saveMongo.useApi(submitForm);
watch(props, async (newValue: any) => {
state.dialogVisible = newValue.visible;
watchEffect(() => {
state.dialogVisible = props.visible;
if (!state.dialogVisible) {
return;
}
state.tabActiveName = 'basic';
if (newValue.mongo) {
state.form = { ...newValue.mongo };
state.form.tagId = newValue.mongo.tags.map((t: any) => t.tagId);
const mongo: any = props.mongo;
if (mongo) {
state.form = { ...mongo };
state.form.tagCodePaths = mongo.tags.map((t: any) => t.codePath);
} else {
state.form = { db: 0 } as any;
}
@@ -158,6 +160,7 @@ const getReqForm = () => {
if (!state.form.sshTunnelMachineId || state.form.sshTunnelMachineId <= 0) {
reqForm.sshTunnelMachineId = -1;
}
reqForm.tagCodePaths = state.form.tagCodePaths.map((t: any) => getTagPath(t)) as any;
return reqForm;
};

View File

@@ -34,7 +34,7 @@
<mongo-run-command v-model:visible="usersVisible" :id="state.dbOps.dbId" />
<mongo-edit
@val-change="search"
@val-change="search()"
:title="mongoEditDialog.title"
v-model:visible="mongoEditDialog.visible"
v-model:mongo="mongoEditDialog.data"

View File

@@ -8,16 +8,16 @@
<el-form :model="form" ref="redisForm" :rules="rules" label-width="auto">
<el-tabs v-model="tabActiveName">
<el-tab-pane label="基础信息" name="basic">
<el-form-item ref="tagSelectRef" prop="tagId" label="标签" required>
<el-form-item ref="tagSelectRef" prop="tagCodePaths" label="标签" required>
<tag-tree-select
@change-tag="
(tagIds) => {
form.tagId = tagIds;
(tagCodePaths) => {
form.tagCodePaths = tagCodePaths;
tagSelectRef.validate();
}
"
multiple
:select-tags="form.tagId"
:select-tags="form.tagCodePaths"
style="width: 100%"
/>
</el-form-item>
@@ -25,7 +25,7 @@
<el-input
:disabled="form.id"
v-model.trim="form.code"
placeholder="请输入编号 (数字字母下划线), 不可修改"
placeholder="请输入编号 (大小写字母数字_-.:), 不可修改"
auto-complete="off"
></el-input>
</el-form-item>
@@ -108,7 +108,7 @@
</template>
<script lang="ts" setup>
import { toRefs, reactive, watch, ref } from 'vue';
import { toRefs, reactive, ref, watchEffect } from 'vue';
import { redisApi } from './api';
import { ElMessage } from 'element-plus';
import TagTreeSelect from '../component/TagTreeSelect.vue';
@@ -116,6 +116,7 @@ import SshTunnelSelect from '../component/SshTunnelSelect.vue';
import ProcdefSelectFormItem from '@/views/flow/components/ProcdefSelectFormItem.vue';
import { ResourceCodePattern } from '@/common/pattern';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { getTagPath } from '../component/tag';
const props = defineProps({
visible: {
@@ -132,7 +133,7 @@ const props = defineProps({
const emit = defineEmits(['update:visible', 'val-change', 'cancel']);
const rules = {
tagId: [
tagCodePaths: [
{
required: true,
message: '请选择标签',
@@ -190,7 +191,7 @@ const state = reactive({
form: {
id: null,
code: '',
tagId: [],
tagCodePaths: [],
name: null,
mode: 'standalone',
host: '',
@@ -211,15 +212,16 @@ const { dialogVisible, tabActiveName, form, submitForm, dbList } = toRefs(state)
const { isFetching: testConnBtnLoading, execute: testConnExec } = redisApi.testConn.useApi(submitForm);
const { isFetching: saveBtnLoading, execute: saveRedisExec } = redisApi.saveRedis.useApi(submitForm);
watch(props, async (newValue: any) => {
state.dialogVisible = newValue.visible;
watchEffect(() => {
state.dialogVisible = props.visible;
if (!state.dialogVisible) {
return;
}
state.tabActiveName = 'basic';
if (newValue.redis) {
state.form = { ...newValue.redis };
state.form.tagId = newValue.redis.tags.map((t: any) => t.tagId);
const redis: any = props.redis;
if (redis) {
state.form = { ...redis };
state.form.tagCodePaths = redis.tags.map((t: any) => t.codePath);
convertDb(state.form.db);
} else {
state.form = { db: '0' } as any;
@@ -247,6 +249,7 @@ const getReqForm = async () => {
if (!state.form.sshTunnelMachineId || state.form.sshTunnelMachineId <= 0) {
reqForm.sshTunnelMachineId = -1;
}
reqForm.tagCodePaths = state.form.tagCodePaths.map((t: any) => getTagPath(t)) as any;
return reqForm;
};

View File

@@ -39,7 +39,7 @@ import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/SearchForm';
import { AuthCertCiphertextTypeEnum, AuthCertTypeEnum } from './enums';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { ResourceTypeEnum, TagResourceTypeEnum } from '@/common/commonEnum';
import ResourceAuthCertEdit from '../component/ResourceAuthCertEdit.vue';
const pageTableRef: Ref<any> = ref(null);
@@ -51,6 +51,7 @@ const state = reactive({
},
searchItems: [
SearchItem.input('name', '凭证名称'),
SearchItem.select('resourceType', '资源类型').withEnum(ResourceTypeEnum),
SearchItem.select('type', '凭证类型').withEnum(AuthCertTypeEnum),
SearchItem.select('ciphertextType', '密文类型').withEnum(AuthCertCiphertextTypeEnum),
],

View File

@@ -3,7 +3,7 @@
<Splitpanes class="default-theme">
<Pane size="30" min-size="25" max-size="35">
<div class="card pd5 mr5">
<el-input v-model="filterTag" clearable placeholder="关键字过滤(右击操作)" style="width: 200px; margin-right: 10px" />
<el-input v-model="filterTag" clearable placeholder="关键字过滤(右击节点操作)" style="width: 200px; margin-right: 10px" />
<el-button
v-if="useUserInfo().userInfo.username == 'admin'"
v-auth="'tag:save'"
@@ -42,7 +42,10 @@
>
<template #default="{ data }">
<span class="custom-tree-node">
<SvgIcon :name="EnumValue.getEnumByValue(TagResourceTypeEnum, data.type)?.extra.icon" />
<SvgIcon
:name="EnumValue.getEnumByValue(TagResourceTypeEnum, data.type)?.extra.icon"
:color="EnumValue.getEnumByValue(TagResourceTypeEnum, data.type)?.extra.iconColor"
/>
<span class="ml5">
{{ data.code }}
@@ -94,7 +97,7 @@
</el-tab-pane>
<el-tab-pane :disabled="currentTag.type != TagResourceTypeEnum.Tag.value" :label="`数据库 (${resourceCount.db || 0})`" :name="DbTag">
<DbList lazy ref="dbListRef" />
<InstanceList lazy ref="dbInstanceListRef" />
</el-tab-pane>
<el-tab-pane
@@ -152,10 +155,10 @@ import { Splitpanes, Pane } from 'splitpanes';
import MachineList from '../machine/MachineList.vue';
import RedisList from '../redis/RedisList.vue';
import MongoList from '../mongo/MongoList.vue';
import DbList from '../db/DbList.vue';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import EnumTag from '@/components/enumtag/EnumTag.vue';
import EnumValue from '@/common/Enum';
import InstanceList from '../db/InstanceList.vue';
interface Tree {
id: number;
@@ -169,7 +172,7 @@ const tagTreeRef: any = ref(null);
const filterTag = ref('');
const contextmenuRef = ref();
const machineListRef: Ref<any> = ref(null);
const dbListRef: Ref<any> = ref(null);
const dbInstanceListRef: Ref<any> = ref(null);
const redisListRef: Ref<any> = ref(null);
const mongoListRef: Ref<any> = ref(null);
@@ -305,7 +308,7 @@ const setNowTabData = () => {
machineListRef.value.search(tagPath);
break;
case DbTag:
dbListRef.value.search(tagPath);
dbInstanceListRef.value.search(tagPath);
break;
case RedisTag:
redisListRef.value.search(tagPath);
@@ -320,7 +323,7 @@ const setNowTabData = () => {
const filterNode = (value: string, data: Tree) => {
if (!value) return true;
return data.codePath.includes(value) || data.name.includes(value);
return data.codePath.toLowerCase().includes(value) || data.name.includes(value);
};
const search = async () => {

View File

@@ -48,41 +48,45 @@
</el-form-item>
<el-form-item prop="tag" label="标签">
<el-scrollbar style="height: calc(100vh - 300px); border: 1px solid var(--el-border-color)">
<el-tree
ref="tagTreeRef"
style="width: 100%"
:data="state.tags"
:default-expanded-keys="state.addTeamDialog.form.tags"
:default-checked-keys="state.addTeamDialog.form.tags"
multiple
:render-after-expand="true"
show-checkbox
check-strictly
node-key="id"
:props="{
value: 'id',
label: 'codePath',
children: 'children',
disabled: 'disabled',
}"
@check="tagTreeNodeCheck"
>
<template #default="{ data }">
<span class="custom-tree-node">
<SvgIcon :name="EnumValue.getEnumByValue(TagResourceTypeEnum, data.type)?.extra.icon" />
<div class="w100" style="border: 1px solid var(--el-border-color)">
<el-input v-model="filterTag" clearable placeholder="输入关键字过滤" size="small" />
<el-scrollbar style="height: calc(100vh - 330px)">
<el-tree
ref="tagTreeRef"
style="width: 100%"
:data="state.tags"
:default-expanded-keys="state.addTeamDialog.form.tags"
:default-checked-keys="state.addTeamDialog.form.tags"
multiple
:render-after-expand="true"
show-checkbox
check-strictly
node-key="id"
:props="{
value: 'id',
label: 'codePath',
children: 'children',
disabled: 'disabled',
}"
@check="tagTreeNodeCheck"
:filter-node-method="filterNode"
>
<template #default="{ data }">
<span class="custom-tree-node">
<SvgIcon :name="EnumValue.getEnumByValue(TagResourceTypeEnum, data.type)?.extra.icon" />
<span class="font13 ml5">
{{ data.code }}
<span style="color: #3c8dbc"></span>
{{ data.name }}
<span style="color: #3c8dbc"></span>
<el-tag v-if="data.children !== null" size="small">{{ data.children.length }} </el-tag>
<span class="font13 ml5">
{{ data.code }}
<span style="color: #3c8dbc"></span>
{{ data.name }}
<span style="color: #3c8dbc"></span>
<el-tag v-if="data.children !== null" size="small">{{ data.children.length }} </el-tag>
</span>
</span>
</span>
</template>
</el-tree>
</el-scrollbar>
</template>
</el-tree>
</el-scrollbar>
</div>
</el-form-item>
</el-form>
<template #footer>
@@ -127,7 +131,7 @@
</template>
<script lang="ts" setup>
import { ref, toRefs, reactive, onMounted, Ref } from 'vue';
import { ref, toRefs, reactive, onMounted, Ref, watch } from 'vue';
import { tagApi } from './api';
import { ElMessage, ElMessageBox } from 'element-plus';
import { notBlank } from '@/common/assert';
@@ -143,6 +147,7 @@ const teamForm: any = ref(null);
const tagTreeRef: any = ref(null);
const pageTableRef: Ref<any> = ref(null);
const showMemPageTableRef: Ref<any> = ref(null);
const filterTag = ref('');
const searchItems = [SearchItem.input('name', '团队名称')];
const columns = [
@@ -206,6 +211,17 @@ const search = async () => {
pageTableRef.value.search();
};
watch(filterTag, (val) => {
tagTreeRef.value!.filter(val);
});
const filterNode = (value: string, data: any) => {
if (!value) {
return true;
}
return data.codePath.toLowerCase().includes(value) || data.name.includes(value);
};
const showSaveTeamDialog = async (data: any) => {
state.tags = await tagApi.getTagTrees.request(null);

View File

@@ -30,7 +30,7 @@
</template>
<script lang="ts" setup>
import { toRefs, reactive, watch, ref } from 'vue';
import { toRefs, reactive, watch, ref, watchEffect } from 'vue';
import { accountApi } from '../api';
import { ElMessage } from 'element-plus';
import { AccountUsernamePattern } from '@/common/pattern';
@@ -101,6 +101,18 @@ watch(props, (newValue: any) => {
state.dialogVisible = newValue.visible;
});
watchEffect(() => {
const account: any = props.account;
if (account) {
state.form = { ...account };
state.edit = true;
} else {
state.edit = false;
state.form = {} as any;
}
state.dialogVisible = props.visible;
});
const btnOk = async () => {
accountForm.value.validate(async (valid: boolean) => {
if (!valid) {

View File

@@ -41,7 +41,7 @@
</template>
<script lang="ts" setup>
import { ref, toRefs, reactive, watch } from 'vue';
import { ref, toRefs, reactive, watch, watchEffect } from 'vue';
import { configApi, accountApi } from '../api';
import { DynamicFormEdit } from '@/components/dynamic-form';
@@ -82,14 +82,14 @@ const { dvisible, params, form } = toRefs(state);
const { isFetching: saveBtnLoading, execute: saveConfigExec } = configApi.save.useApi(form);
watch(props, (newValue: any) => {
state.dvisible = newValue.visible;
watchEffect(() => {
state.dvisible = props.visible;
if (!state.dvisible) {
return;
}
if (newValue.data) {
state.form = { ...newValue.data };
if (props.data) {
state.form = { ...(props.data as any) };
if (state.form.params) {
state.params = JSON.parse(state.form.params);
} else {

View File

@@ -128,7 +128,7 @@
</el-col>
</el-row>
</el-form>
e
<template #footer>
<div>
<el-button @click="cancel()"> </el-button>
@@ -140,7 +140,7 @@
</template>
<script lang="ts" setup>
import { ref, toRefs, reactive, watch } from 'vue';
import { ref, toRefs, reactive, watchEffect } from 'vue';
import { ElMessage } from 'element-plus';
import { resourceApi } from '../api';
import { ResourceTypeEnum } from '../enums';
@@ -229,10 +229,10 @@ const { dialogVisible, form, submitForm } = toRefs(state);
const { isFetching: saveBtnLoading, execute: saveResouceExec } = resourceApi.save.useApi(submitForm);
watch(props, (newValue: any) => {
state.dialogVisible = newValue.visible;
if (newValue.data) {
state.form = { ...newValue.data };
watchEffect(() => {
state.dialogVisible = props.visible;
if (props.data) {
state.form = { ...(props.data as any) };
} else {
state.form = {} as any;
}

View File

@@ -28,7 +28,7 @@
</template>
<script lang="ts" setup>
import { ref, toRefs, reactive, watch } from 'vue';
import { ref, toRefs, reactive, watchEffect } from 'vue';
import { roleApi } from '../api';
import { RoleStatusEnum } from '../enums';
@@ -63,10 +63,10 @@ const { dvisible, form } = toRefs(state);
const { isFetching: saveBtnLoading, execute: saveRoleExec } = roleApi.save.useApi(form);
watch(props, (newValue: any) => {
state.dvisible = newValue.visible;
if (newValue.data) {
state.form = { ...newValue.data };
watchEffect(() => {
state.dvisible = props.visible;
if (props.data) {
state.form = { ...(props.data as any) };
} else {
state.form = {} as any;
}