feat(接口测试): 接口调试-模块树重命名&删除、部分bug修复

This commit is contained in:
baiqi 2024-02-19 16:02:17 +08:00 committed by Craftsman
parent 12071962b7
commit b35fdf8355
28 changed files with 328 additions and 110 deletions

View File

@ -3,6 +3,7 @@ import {
AddApiDebugUrl,
AddDebugModuleUrl,
DeleteDebugModuleUrl,
DeleteDebugUrl,
ExecuteApiDebugUrl,
GetApiDebugDetailUrl,
GetDebugModuleCountUrl,
@ -71,3 +72,8 @@ export function updateDebug(data: UpdateDebugParams) {
export function getDebugDetail(id: string) {
return MSR.get<DebugDetail>({ url: GetApiDebugDetailUrl, params: id });
}
// 删除接口调试
export function deleteDebug(id: string) {
return MSR.get({ url: DeleteDebugUrl, params: id });
}

View File

@ -2,6 +2,7 @@ export const ExecuteApiDebugUrl = '/api/debug/debug'; // 执行调试
export const AddApiDebugUrl = '/api/debug/add'; // 新增调试
export const UpdateApiDebugUrl = '/api/debug/update'; // 更新调试
export const GetApiDebugDetailUrl = '/api/debug/get'; // 获取接口调试详情
export const DeleteDebugUrl = '/api/debug/delete'; // 删除调试
export const UpdateDebugModuleUrl = '/api/debug/module/update'; // 更新模块
export const MoveDebugModuleUrl = '/api/debug/module/move'; // 移动模块
export const GetDebugModuleCountUrl = '/api/debug/module/count'; // 模块统计数量

View File

@ -215,6 +215,10 @@
}
.arco-btn-size-mini {
line-height: 16px;
.arco-icon-loading {
font-size: 14px;
line-height: 16px;
}
}
/** 输入框,选择器,文本域 **/

View File

@ -249,7 +249,7 @@
margin-left: -16px !important;
border-radius: 0 4px 4px 0 !important;
background-color: var(--color-text-n8) !important;
&:hover {
&:hover:not(.arco-select-view-disabled) {
border-color: rgb(var(--primary-5)) !important;
background-color: var(--color-text-n8) !important;
}

View File

@ -388,7 +388,7 @@
let mouseEnterTimer;
//
const renderMenuItem = (element, icon) =>
const renderMenuItem = (element: RouteRecordRaw | null, icon) =>
element?.name === SettingRouteEnum.SETTING_ORGANIZATION ? (
<a-menu-item key={element?.name} v-slots={{ icon }} onClick={() => goto(element)}>
<div class="inline-flex w-[calc(100%-34px)] items-center justify-between !bg-transparent">

View File

@ -65,7 +65,7 @@
<div class="flex items-center justify-between px-[16px]">
<MsTableMoreAction :list="actions" trigger="click" @select="handleMoreActionSelect($event, item)">
<a-button
v-permission="['SYSTEM_PERSONAL_API_KEY:READ+UPDATE']"
v-permission="['SYSTEM_PERSONAL_API_KEY:READ+UPDATE', 'SYSTEM_PERSONAL_API_KEY:READ+DELETE']"
size="mini"
type="outline"
class="arco-btn-outline--secondary"
@ -83,10 +83,10 @@
</div>
</div>
<div v-if="apiKeyList.length === 0" class="col-span-2 flex w-full items-center justify-center p-[44px]">
{{ t('ms.personal.nodata') }}
<MsButton v-permission="['SYSTEM_PERSONAL_API_KEY:READ+ADD']" type="text" class="ml-[8px]" @click="newApiKey">{{
t('common.new')
}}</MsButton>
{{ hasCratePermission ? t('ms.personal.noData') : t('ms.personal.empty') }}
<MsButton v-permission="['SYSTEM_PERSONAL_API_KEY:READ+ADD']" type="text" class="ml-[8px]" @click="newApiKey">
{{ t('common.new') }}
</MsButton>
</div>
</a-spin>
</div>
@ -156,6 +156,7 @@
} from '@/api/modules/user/index';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import { hasAnyPermission } from '@/utils/permission';
import { APIKEY } from '@/models/user';
@ -169,6 +170,7 @@
desensitization: boolean;
}
const apiKeyList = ref<APIKEYItem[]>([]);
const hasCratePermission = hasAnyPermission(['SYSTEM_PERSONAL_API_KEY:READ+ADD']);
async function initApiKeys() {
try {
@ -210,6 +212,7 @@
{
label: t('ms.personal.validTime'),
eventTag: 'time',
permission: ['SYSTEM_PERSONAL_API_KEY:READ+UPDATE'],
},
{
isDivider: true,
@ -218,6 +221,7 @@
label: t('common.delete'),
danger: true,
eventTag: 'delete',
permission: ['SYSTEM_PERSONAL_API_KEY:READ+DELETE'],
},
];

View File

@ -14,7 +14,7 @@
<div v-for="config of dynamicForm" :key="config.key" class="platform-card">
<div class="mb-[16px] flex items-center">
<a-image :src="`/plugin/image/${config.key}?imagePath=static/${config.key}.jpg`" width="24"></a-image>
<div class="ml-[8px] mr-[4px] font-medium text-[var(--color-text-1)]">{{ config.key }}</div>
<div class="ml-[8px] mr-[4px] font-medium text-[var(--color-text-1)]">{{ config.name }}</div>
<a-tooltip v-if="config.tooltip" :content="config.tooltip" position="right">
<icon-exclamation-circle
class="mr-[8px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
@ -122,6 +122,7 @@
Object.keys(res).forEach((key) => {
dynamicForm.value[key] = {
key,
name: res[key].name,
status: 0,
formModel: {},
formRules: res[key].formItems,

View File

@ -87,5 +87,6 @@ export default {
'ms.personal.azureTip':
'This information is the user token information for submitting defects through Azure Devops. If not filled in, the default information configured by the organization will be used.',
'ms.personal.azurePlaceholder': 'Please enter Personal Access Tokens',
'ms.personal.nodata': 'No data yet, please ',
'ms.personal.noData': 'No data yet, please ',
'ms.personal.empty': 'No data',
};

View File

@ -79,5 +79,6 @@ export default {
'ms.personal.zendaoTip': '该信息为通过禅道提交缺陷的的用户名、密码,若未填写,则使用组织配置的默认信息',
'ms.personal.azureTip': '该信息为通过Azure Devops提交缺陷的用户令牌信息若未填写则使用组织配置的默认信息',
'ms.personal.azurePlaceholder': '请输入 Personal Access Tokens',
'ms.personal.nodata': '暂无数据,请 ',
'ms.personal.noData': '暂无数据,请 ',
'ms.personal.empty': '暂无数据',
};

View File

@ -355,8 +355,10 @@
<style lang="less">
.ms-tree-container {
.ms-container--shadow-y();
@apply h-full;
.ms-tree {
.ms-scroll-bar();
@apply h-full;
.arco-tree-node {
border-radius: var(--border-radius-small);
&:hover {

View File

@ -8,7 +8,12 @@
</slot>
<template #content>
<template v-for="item of props.list">
<a-divider v-if="item.isDivider" :key="`${item.label}-divider`" margin="4px" />
<a-divider
v-if="item.isDivider"
:key="`${item.label}-divider`"
:class="beforeDividerHasAction && afterDividerHasAction ? '' : 'hidden'"
margin="4px"
/>
<a-doption
v-else
:key="item.label"
@ -31,6 +36,7 @@
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import { useI18n } from '@/hooks/useI18n';
import { hasAnyPermission } from '@/utils/permission';
import type { ActionsItem, SelectedValue } from './types';
@ -42,6 +48,38 @@
const emit = defineEmits(['select', 'close']);
// 线action
const beforeDividerHasAction = computed(() => {
let result = false;
for (let i = 0; i < props.list.length; i++) {
const item = props.list[i];
if (!item.isDivider) {
result = hasAnyPermission(item.permission || []);
if (result) {
return true;
}
} else {
return false;
}
}
});
// 线action
const afterDividerHasAction = computed(() => {
let result = false;
for (let i = props.list.length - 1; i > 0; i--) {
const item = props.list[i];
if (!item.isDivider) {
result = hasAnyPermission(item.permission || []);
if (result) {
return true;
}
} else {
return false;
}
}
});
function selectHandler(value: SelectedValue) {
const item = props.list.find((e: ActionsItem) => e.eventTag === value);
emit('select', item);

View File

@ -256,7 +256,8 @@ export default function useTableProps<T>(
const data = await loadListFunc({ keyword: keyword.value, ...loadListParams.value });
if (data.length === 0) {
setTableErrorStatus('empty');
return;
propsRes.value.data = [];
return data;
}
setTableErrorStatus(false);
propsRes.value.data = data.map((item: MsTableDataItem<T>) => {

View File

@ -1,6 +1,6 @@
import { DirectiveBinding } from 'vue';
import { hasAnyPermission } from '@/utils/permission';
import { hasAllPermission, hasAnyPermission } from '@/utils/permission';
/**
*
@ -8,10 +8,11 @@ import { hasAnyPermission } from '@/utils/permission';
* @param binding vue
*/
function checkPermission(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding;
const { value, modifiers } = binding;
if (Array.isArray(value)) {
if (value.length > 0) {
const hasPermission = hasAnyPermission(value);
// 如果有 all 修饰符,表示需要全部权限;否则只需要其中一个权限
const hasPermission = modifiers.all ? hasAllPermission(value) : hasAnyPermission(value);
if (!hasPermission && el.parentNode) {
el.parentNode.removeChild(el);
}

View File

@ -313,10 +313,10 @@ export interface SaveDebugParams {
linkFileIds: string[];
}
// 更新接口调试入参
export interface UpdateDebugParams extends SaveDebugParams {
export interface UpdateDebugParams extends Partial<SaveDebugParams> {
id: string;
deleteFileIds: string[];
unLinkRefIds: string[];
deleteFileIds?: string[];
unLinkRefIds?: string[];
}
// 更新模块入参
export interface UpdateDebugModule {

View File

@ -16,6 +16,9 @@ const Setting: AppRouteRecordRaw = {
'SYSTEM_USER_ROLE:READ',
'SYSTEM_ORGANIZATION_PROJECT:READ',
'SYSTEM_PARAMETER_SETTING_BASE:READ',
'SYSTEM_PARAMETER_SETTING_DISPLAY:READ',
'SYSTEM_PARAMETER_SETTING_AUTH:READ',
'SYSTEM_PARAMETER_SETTING_MEMORY_CLEAN:READ',
'SYSTEM_TEST_RESOURCE_POOL:READ',
'SYSTEM_AUTH:READ',
'SYSTEM_PLUGIN:READ',
@ -40,6 +43,9 @@ const Setting: AppRouteRecordRaw = {
'SYSTEM_USER_ROLE:READ',
'SYSTEM_ORGANIZATION_PROJECT:READ',
'SYSTEM_PARAMETER_SETTING_BASE:READ',
'SYSTEM_PARAMETER_SETTING_DISPLAY:READ',
'SYSTEM_PARAMETER_SETTING_AUTH:READ',
'SYSTEM_PARAMETER_SETTING_MEMORY_CLEAN:READ',
'SYSTEM_TEST_RESOURCE_POOL:READ',
'SYSTEM_AUTH:READ',
'SYSTEM_PLUGIN:READ',
@ -84,7 +90,12 @@ const Setting: AppRouteRecordRaw = {
component: () => import('@/views/setting/system/config/index.vue'),
meta: {
locale: 'menu.settings.system.parameter',
roles: ['SYSTEM_PARAMETER_SETTING_BASE:READ'],
roles: [
'SYSTEM_PARAMETER_SETTING_BASE:READ',
'SYSTEM_PARAMETER_SETTING_DISPLAY:READ',
'SYSTEM_PARAMETER_SETTING_AUTH:READ',
'SYSTEM_PARAMETER_SETTING_MEMORY_CLEAN:READ',
],
isTopMenu: true,
},
},

View File

@ -548,3 +548,25 @@ export function tableParamsToRequestParams(params: BatchActionQueryParams) {
condition,
};
}
/**
* URL
* @param url URL
*/
interface QueryParam {
key: string;
value: string;
}
export function parseQueryParams(url: string): QueryParam[] {
const queryParams: QueryParam[] = [];
// 从 URL 中提取查询参数部分
const queryString = url.split('?')[1];
if (queryString) {
const params = new URLSearchParams(queryString);
// 遍历查询参数,将每个参数添加到数组中
params.forEach((value, key) => {
queryParams.push({ key, value });
});
}
return queryParams;
}

View File

@ -11,10 +11,7 @@
>
<template #content>
<div class="mb-[8px] font-medium">
{{
props.title ||
(props.mode === 'add' ? t('project.fileManagement.addSubModule') : t('project.fileManagement.rename'))
}}
{{ props.title || (props.mode === 'add' ? t('project.fileManagement.addSubModule') : t('common.rename')) }}
</div>
<a-form ref="formRef" :model="form" layout="vertical">
<a-form-item
@ -51,7 +48,7 @@
import { ref, watch } from 'vue';
import { Message } from '@arco-design/web-vue';
import { addDebugModule, updateDebugModule } from '@/api/modules/api-test/debug';
import { addDebugModule, updateDebug, updateDebugModule } from '@/api/modules/api-test/debug';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
@ -67,6 +64,7 @@
const props = defineProps<{
mode: 'add' | 'rename';
nodeType?: 'MODULE' | 'API';
visible?: boolean;
title?: string;
allNames: string[];
@ -128,16 +126,24 @@
parentId: props.parentId || '',
name: form.value.field,
});
Message.success(t('project.fileManagement.addSubModuleSuccess'));
Message.success(t('common.addSuccess'));
emit('addFinish', form.value.field);
} else if (props.mode === 'rename' && props.nodeType === 'API') {
//
await updateDebug({
id: props.nodeId || '',
name: form.value.field,
});
Message.success(t('common.updateSuccess'));
emit('renameFinish', form.value.field, props.nodeId);
} else if (props.mode === 'rename') {
//
await updateDebugModule({
id: props.nodeId || '',
name: form.value.field,
});
Message.success(t('project.fileManagement.renameSuccess'));
emit('renameFinish', form.value.field);
Message.success(t('common.updateSuccess'));
emit('renameFinish', form.value.field, props.nodeId);
}
if (done) {
done(true);

View File

@ -21,7 +21,7 @@
v-model:model-value="requestVModel.url"
:max-length="255"
:placeholder="t('apiTestDebug.urlPlaceholder')"
@change="handleActiveDebugChange"
@change="handleUrlChange"
/>
</a-input-group>
</div>
@ -230,14 +230,14 @@
import { getLocalConfig } from '@/api/modules/user/index';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import { filterTree, getGenerateId } from '@/utils';
import { filterTree, getGenerateId, parseQueryParams } from '@/utils';
import { scrollIntoView } from '@/utils/dom';
import { registerCatchSaveShortcut, removeCatchSaveShortcut } from '@/utils/event';
import { PluginConfig } from '@/models/apiTest/common';
import { ExecuteHTTPRequestFullParams } from '@/models/apiTest/debug';
import { ModuleTreeNode } from '@/models/common';
import { RequestComposition, RequestMethods } from '@/enums/apiEnum';
import { RequestComposition, RequestMethods, RequestParamsType } from '@/enums/apiEnum';
import { Api } from '@form-create/arco-design';
@ -250,6 +250,7 @@
export interface RequestCustomAttr {
isNew: boolean;
protocol: string;
activeTab: RequestComposition;
}
export type RequestParam = ExecuteHTTPRequestFullParams & RequestCustomAttr & TabItem;
@ -479,6 +480,32 @@
}
);
/**
* 处理url输入框变化解析成参数表格
*/
function handleUrlChange(val: string) {
const params = parseQueryParams(val.trim());
if (params.length > 0) {
requestVModel.value.query.splice(
0,
requestVModel.value.query.length - 2,
...params.map((e, i) => ({
id: (new Date().getTime() + i).toString(),
paramType: RequestParamsType.STRING,
description: '',
required: false,
maxLength: undefined,
minLength: undefined,
encode: false,
enable: true,
...e,
}))
);
requestVModel.value.activeTab = RequestComposition.QUERY;
}
handleActiveDebugChange();
}
const splitBoxSize = ref<string | number>(0.6);
const activeLayout = ref<'horizontal' | 'vertical'>('vertical');
const splitContainerRef = ref<HTMLElement>();

View File

@ -1,5 +1,5 @@
<template>
<div>
<div class="flex h-full flex-col p-[24px]">
<div class="mb-[8px] flex items-center gap-[8px]">
<a-input v-model:model-value="moduleKeyword" :placeholder="t('apiTestDebug.searchTip')" allow-clear />
<a-dropdown @select="handleSelect">
@ -34,7 +34,7 @@
</div>
</div>
<a-divider class="my-[8px]" />
<a-spin class="min-h-[400px] w-full" :loading="loading">
<a-spin class="h-[calc(100%-98px)] w-full" :loading="loading">
<MsTree
v-model:focus-node-key="focusNodeKey"
:data="folderTree"
@ -43,8 +43,9 @@
:default-expand-all="isExpandAll"
:expand-all="isExpandAll"
:empty-text="t('apiTestDebug.noMatchModule')"
:draggable="true"
:virtual-list-props="virtualListProps"
:virtual-list-props="{
height: '100%',
}"
:field-names="{
title: 'name',
key: 'id',
@ -93,8 +94,9 @@
:node-id="nodeData.id"
:field-config="{ field: renameFolderTitle }"
:all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')"
:node-type="nodeData.type"
@close="resetFocusNodeKey"
@rename-finish="initModules"
@rename-finish="handleRenameFinish"
>
<span :id="`renameSpan${nodeData.id}`" class="relative"></span>
</popConfirm>
@ -117,6 +119,7 @@
import popConfirm from '@/views/api-test/components/popConfirm.vue';
import {
deleteDebug,
deleteDebugModule,
getDebugModuleCount,
getDebugModules,
@ -131,7 +134,7 @@
const props = defineProps<{
isExpandAll?: boolean; //
}>();
const emit = defineEmits(['init', 'clickApiNode', 'newApi', 'import']);
const emit = defineEmits(['init', 'clickApiNode', 'newApi', 'import', 'renameFinish']);
const { t } = useI18n();
const { openModal } = useModal();
@ -150,12 +153,6 @@
}
}
const virtualListProps = computed(() => {
return {
height: 'calc(100% - 180px)',
};
});
const isExpandAll = ref(props.isExpandAll);
const rootModulesName = ref<string[]>([]); //
@ -204,9 +201,9 @@
return {
...e,
hideMoreAction: e.id === 'root',
draggable: e.id !== 'root',
};
});
rootModulesName.value = folderTree.value.map((e) => e.name || '');
emit('init', folderTree.value);
} catch (error) {
// eslint-disable-next-line no-console
@ -228,7 +225,6 @@
return {
...node,
count: res[node.id] || 0,
draggable: node.id !== 'root',
};
});
} catch (error) {
@ -273,6 +269,34 @@
renameFolderTitle.value = '';
}
/**
* 删除接口调试
* @param node 节点信息
*/
function deleteApiDebug(node: MsTreeNodeData) {
openModal({
type: 'error',
title: t('apiTestDebug.deleteDebugTipTitle', { name: node.name }),
content: t('apiTestDebug.deleteDebugTipContent'),
okText: t('apiTestDebug.deleteConfirm'),
okButtonProps: {
status: 'danger',
},
maskClosable: false,
onBeforeOk: async () => {
try {
await deleteDebug(node.id);
Message.success(t('apiTestDebug.deleteSuccess'));
initModules();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
},
hideCancel: false,
});
}
/**
* 处理树节点更多按钮事件
* @param item
@ -280,7 +304,11 @@
function handleFolderMoreSelect(item: ActionsItem, node: MsTreeNodeData) {
switch (item.eventTag) {
case 'delete':
deleteFolder(node);
if (node.type === 'MODULE') {
deleteFolder(node);
} else {
deleteApiDebug(node);
}
resetFocusNodeKey();
break;
case 'rename':
@ -319,7 +347,7 @@
console.log(error);
} finally {
loading.value = false;
initModules();
await initModules();
initModuleCount();
}
}
@ -331,8 +359,13 @@
}
}
onBeforeMount(() => {
function handleRenameFinish(newName: string, id: string) {
initModules();
emit('renameFinish', newName, id);
}
onBeforeMount(async () => {
await initModules();
initModuleCount();
});

View File

@ -3,15 +3,14 @@
<MsCard :loading="loading" simple no-content-padding>
<MsSplitBox :size="0.25" :max="0.5">
<template #first>
<div class="p-[24px]">
<moduleTree
ref="moduleTreeRef"
@init="(val) => (folderTree = val)"
@new-api="addDebugTab"
@click-api-node="openApiTab"
@import="importDrawerVisible = true"
/>
</div>
<moduleTree
ref="moduleTreeRef"
@init="(val) => (folderTree = val)"
@new-api="addDebugTab"
@click-api-node="openApiTab"
@import="importDrawerVisible = true"
@rename-finish="handleRenameFinish"
/>
</template>
<template #second>
<div class="flex h-full flex-col">
@ -113,8 +112,8 @@
const curlCode = ref('');
const loading = ref(false);
function handleDebugAddDone() {
moduleTreeRef.value?.initModules();
async function handleDebugAddDone() {
await moduleTreeRef.value?.initModules();
moduleTreeRef.value?.initModuleCount();
}
@ -289,6 +288,16 @@
handleActiveDebugChange();
});
}
function handleRenameFinish(name: string, id: string) {
debugTabs.value = debugTabs.value.map((tab) => {
if (tab.id === id) {
tab.label = name;
tab.name = name;
}
return tab;
});
}
</script>
<style lang="less" scoped></style>

View File

@ -96,6 +96,8 @@ export default {
'apiTestDebug.deleteFolderTipTitle': 'Remove the `{name}` module?',
'apiTestDebug.deleteFolderTipContent':
'This operation will delete the module and all resources under it, please operate with caution!',
'apiTestDebug.deleteDebugTipTitle': 'Remove the {name}?',
'apiTestDebug.deleteDebugTipContent': 'Deletion cannot be restored, please proceed with caution!',
'apiTestDebug.deleteConfirm': 'Confirm delete',
'apiTestDebug.deleteSuccess': 'Successfully deleted',
'apiTestDebug.moduleMoveSuccess': 'Module moved successfully',

View File

@ -88,8 +88,10 @@ export default {
'apiTestDebug.extractParameter': '提取参数',
'apiTestDebug.searchTip': '请输入模块/请求名称',
'apiTestDebug.allRequest': '全部请求',
'apiTestDebug.deleteFolderTipTitle': '是否删除 `{name}` 模块?',
'apiTestDebug.deleteFolderTipTitle': '是否删除 {name} 模块?',
'apiTestDebug.deleteFolderTipContent': '该操作会删除模块及其下所有资源,请谨慎操作!',
'apiTestDebug.deleteDebugTipTitle': '是否删除 {name}',
'apiTestDebug.deleteDebugTipContent': '删除后无法恢复,请谨慎操作!',
'apiTestDebug.deleteConfirm': '确认删除',
'apiTestDebug.deleteSuccess': '删除成功',
'apiTestDebug.moduleMoveSuccess': '模块移动成功',

View File

@ -276,6 +276,7 @@
const innerVisible = ref(false);
const fileDescriptions = ref<Description[]>([]);
const detailDrawerRef = ref<InstanceType<typeof MsDetailDrawer>>();
const innerFileId = ref(props.fileId);
watch(
() => props.visible,
@ -293,7 +294,7 @@
async function handleEnableIntercept(newValue: string | number | boolean) {
try {
await toggleJarFileStatus(props.fileId, newValue as boolean);
await toggleJarFileStatus(innerFileId.value, newValue as boolean);
return true;
} catch (error) {
// eslint-disable-next-line no-console
@ -332,7 +333,7 @@
fileLoading.value = true;
await reuploadFile({
request: {
fileId: props.fileId,
fileId: innerFileId.value,
enable: false,
},
file: data,
@ -362,7 +363,7 @@
async function addFileTag(val: string, item: Description) {
await updateFile({
id: props.fileId,
id: innerFileId.value,
tags: Array.isArray(item.value) ? [...item.value, val] : [item.value, val],
});
}
@ -371,7 +372,7 @@
try {
const lastTags = Array.isArray(item.value) ? item.value.filter((e) => e !== tag) : [];
await updateFile({
id: props.fileId,
id: innerFileId.value,
tags: lastTags,
});
item.value = [...lastTags];
@ -387,7 +388,7 @@
async function upgradeRepositoryFile() {
try {
fileLoading.value = true;
await updateRepositoryFile(props.fileId);
await updateRepositoryFile(innerFileId.value);
Message.success(t('common.updateSuccess'));
detailDrawerRef.value?.initDetail();
} catch (error) {
@ -534,18 +535,19 @@
function loadTable() {
if (activeTab.value === 'case') {
setLoadListParams({
id: props.fileId,
id: innerFileId.value,
});
loadCaseList();
} else {
setVersionLoadListParams({
id: props.fileId,
id: innerFileId.value,
});
loadVersionList();
}
}
function loadedFile(detail: FileDetail) {
innerFileId.value = detail.id;
fileType.value = detail.fileType;
renameTitle.value = detail.name;
fileDescriptions.value = [

View File

@ -1,6 +1,6 @@
<template>
<div>
<MsCard class="mb-[16px]" :loading="baseloading" simple auto-height>
<MsCard class="mb-[16px]" :loading="baseLoading" simple auto-height>
<div class="mb-[16px] flex justify-between">
<div class="text-[var(--color-text-000)]">{{ t('system.config.baseInfo') }}</div>
<a-button
@ -12,7 +12,7 @@
{{ t('system.config.update') }}
</a-button>
</div>
<MsDescription :descriptions="baseInfoDescs" class="no-bottom" :column="2" />
<MsDescription :descriptions="baseInfoDesc" class="no-bottom" :column="2" />
</MsCard>
<MsCard class="mb-[16px]" :loading="emailLoading" simple auto-height>
<div class="mb-[16px] flex justify-between">
@ -26,7 +26,7 @@
{{ t('system.config.update') }}
</a-button>
</div>
<MsDescription :descriptions="emailInfoDescs" :column="2">
<MsDescription :descriptions="emailInfoDesc" :column="2">
<template #value="{ item }">
<template v-if="item.key && ['ssl', 'tsl'].includes(item.key)">
<div v-if="item.value === 'true'" class="flex items-center">
@ -230,7 +230,7 @@
const { t } = useI18n();
const baseloading = ref(false);
const baseLoading = ref(false);
const baseDrawerLoading = ref(false);
const baseInfoDrawerVisible = ref(false);
const baseFormRef = ref<FormInstance>();
@ -239,7 +239,7 @@
prometheusHost: 'http://prometheus:9090',
});
const baseInfoForm = ref({ ...baseInfo.value });
const baseInfoDescs = ref<Description[]>([]);
const baseInfoDesc = ref<Description[]>([]);
//
const defaultUrl = 'https://metersphere.com';
const defaultPrometheus = 'http://prometheus:9090';
@ -259,12 +259,12 @@
const licenseStore = useLicenseStore();
async function initBaseInfo() {
try {
baseloading.value = true;
baseLoading.value = true;
const res = await getBaseInfo();
baseInfo.value = { ...res };
baseInfoForm.value = { ...res };
if (licenseStore.hasLicense()) {
baseInfoDescs.value = [
baseInfoDesc.value = [
{
label: t('system.config.pageUrl'),
value: res.url,
@ -275,7 +275,7 @@
},
];
} else {
baseInfoDescs.value = [
baseInfoDesc.value = [
{
label: t('system.config.pageUrl'),
value: res.url,
@ -283,9 +283,10 @@
];
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
baseloading.value = false;
baseLoading.value = false;
}
}
@ -313,6 +314,7 @@
baseInfoDrawerVisible.value = false;
initBaseInfo();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
baseDrawerLoading.value = false;
@ -343,7 +345,7 @@
});
const emailConfigForm = ref({ ...emailConfig.value });
const emailFormRef = ref<FormInstance>();
const emailInfoDescs = ref<Description[]>([]);
const emailInfoDesc = ref<Description[]>([]);
const pswInVisible = ref(false); //
@ -366,12 +368,12 @@
try {
emailLoading.value = true;
const res = await getEmailInfo();
const _ssl = Boolean(res.ssl);
const _tsl = Boolean(res.tsl);
emailConfig.value = { ...res, ssl: _ssl, tsl: _tsl };
emailConfigForm.value = { ...res, ssl: _ssl, tsl: _tsl };
const { host, port, account, password, from, recipient, ssl, tsl } = res;
emailInfoDescs.value = [
const ssl = res.ssl === 'true';
const tsl = res.tsl === 'true';
emailConfig.value = { ...res, ssl, tsl };
emailConfigForm.value = { ...res, ssl, tsl };
const { host, port, account, password, from, recipient } = res;
emailInfoDesc.value = [
{
label: t('system.config.email.host'),
value: host,
@ -399,16 +401,17 @@
},
{
label: t('system.config.email.ssl'),
value: ssl,
value: ssl.toString(),
key: 'ssl',
},
{
label: t('system.config.email.tsl'),
value: tsl,
value: tsl.toString(),
key: 'tsl',
},
];
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
emailLoading.value = false;
@ -445,6 +448,7 @@
emailConfigDrawerVisible.value = false;
initEmailInfo();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
emailDrawerLoading.value = false;
@ -509,6 +513,7 @@
await testEmail(params);
Message.success(t('system.config.email.testSuccess'));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
testLoading.value = false;

View File

@ -21,7 +21,7 @@
<a-input-number
v-model:model-value="timeCount"
class="w-[130px]"
:disabled="saveLoading || !isHasAdminPermission"
:disabled="saveLoading || !hasPermission"
:min="0"
@blur="() => saveConfig()"
>
@ -30,6 +30,7 @@
v-model:model-value="activeTime"
:options="timeOptions"
class="select-input-append"
:disabled="!hasPermission"
:loading="saveLoading"
@change="() => saveConfig()"
/>
@ -49,7 +50,7 @@
<a-input-number
v-model:model-value="historyCount"
class="w-[130px]"
:disabled="saveLoading || !isHasAdminPermission"
:disabled="saveLoading || !hasPermission"
:min="0"
@blur="() => saveConfig()"
/>
@ -65,9 +66,8 @@
import { getCleanupConfig, saveCleanupConfig } from '@/api/modules/setting/config';
import { useI18n } from '@/hooks/useI18n';
import { useUserStore } from '@/store';
import { hasAnyPermission } from '@/utils/permission';
const userStore = useUserStore();
const { t } = useI18n();
const loading = ref(false);
@ -114,14 +114,14 @@
}
});
const isHasAdminPermission = computed(() => {
return userStore.isAdmin;
const hasPermission = computed(() => {
return hasAnyPermission(['SYSTEM_PARAMETER_SETTING_MEMORY_CLEAN:READ+UPDATE']);
});
const saveLoading = ref(false);
async function saveConfig() {
if (!isHasAdminPermission) {
if (!hasPermission) {
return;
}
saveLoading.value = true;

View File

@ -1,6 +1,6 @@
<template>
<MsTabCard v-model:active-tab="activeTab" :title="t('system.config.parameterConfig')" :tab-list="tabList" />
<baseConfig v-show="activeTab === 'baseConfig'" />
<baseConfig v-if="activeTab === 'baseConfig'" v-show="activeTab === 'baseConfig'" />
<pageConfig v-if="isInitPageConfig" v-show="activeTab === 'pageConfig'" />
<authConfig v-if="isInitAuthConfig" v-show="activeTab === 'authConfig'" ref="authConfigRef" />
<memoryCleanup v-if="isInitMemoryCleanup" v-show="activeTab === 'memoryCleanup'" />
@ -14,13 +14,17 @@
import { useRoute } from 'vue-router';
import MsTabCard from '@/components/pure/ms-tab-card/index.vue';
import authConfig, { AuthConfigInstance } from './components/authConfig.vue';
import baseConfig from './components/baseConfig.vue';
import memoryCleanup from './components/memoryCleanup.vue';
import pageConfig from './components/pageConfig.vue';
import { useI18n } from '@/hooks/useI18n';
import useLicenseStore from '@/store/modules/setting/license';
import { hasAnyPermission } from '@/utils/permission';
import type { AuthConfigInstance } from './components/authConfig.vue';
//
const baseConfig = defineAsyncComponent(() => import('./components/baseConfig.vue'));
const pageConfig = defineAsyncComponent(() => import('./components/pageConfig.vue'));
const authConfig = defineAsyncComponent(() => import('./components/authConfig.vue'));
const memoryCleanup = defineAsyncComponent(() => import('./components/memoryCleanup.vue'));
const { t } = useI18n();
const route = useRoute();
@ -33,12 +37,11 @@
const tabList = ref([
{ key: 'baseConfig', title: t('system.config.baseConfig'), permission: ['SYSTEM_PARAMETER_SETTING_BASE:READ'] },
{ key: 'pageConfig', title: t('system.config.pageConfig'), permission: ['SYSTEM_PARAMETER_SETTING_DISPLAY:READ'] },
// TODO
{ key: 'authConfig', title: t('system.config.authConfig'), permission: ['SYSTEM_PARAMETER_SETTING_AUTH:READ'] },
{
key: 'memoryCleanup',
title: t('system.config.memoryCleanup'),
permission: ['SYSTEM_PARAMETER_SETTING_MEMORY_CLEAN:READ+UPDATE'],
permission: ['SYSTEM_PARAMETER_SETTING_MEMORY_CLEAN:READ'],
},
]);
@ -66,11 +69,17 @@
tabList.value = tabList.value.filter((item: any) => excludes.includes(item.key));
}
}
onBeforeMount(() => {
getXpackTab();
const firstHasPermissionTab = tabList.value.find((item: any) => hasAnyPermission(item.permission));
activeTab.value = firstHasPermissionTab?.key || 'baseConfig';
});
onMounted(() => {
if (route.query.tab === 'authConfig' && route.query.id) {
authConfigRef.value?.openAuthDetail(route.query.id as string);
}
getXpackTab();
});
</script>

View File

@ -105,11 +105,21 @@
</div>
<ms-base-table v-bind="propsRes" no-disable sticky-header v-on="propsEvent">
<template #range="{ record }">
{{
record.organizationId === 'SYSTEM'
? t('system.log.system')
: `${record.organizationName}${record.projectName ? `/${record.projectName}` : ''}`
}}
<a-tooltip
:content="
record.organizationId === 'SYSTEM'
? t('system.log.system')
: `${record.organizationName}${record.projectName ? `/${record.projectName}` : ''}`
"
>
<div class="one-line-text">
{{
record.organizationId === 'SYSTEM'
? t('system.log.system')
: `${record.organizationName}${record.projectName ? `/${record.projectName}` : ''}`
}}
</div>
</a-tooltip>
</template>
<template #module="{ record }">
{{ getModuleLocale(record.module) }}
@ -447,22 +457,26 @@
{
title: 'system.log.operator',
dataIndex: 'userName',
width: 100,
showTooltip: true,
},
{
title: 'system.log.operateRange',
dataIndex: 'operateRange',
slotName: 'range',
width: 100,
},
{
title: 'system.log.operateTarget',
dataIndex: 'module',
slotName: 'module',
width: 100,
},
{
title: 'system.log.operateType',
dataIndex: 'type',
slotName: 'type',
width: 120,
width: 80,
},
{
title: 'system.log.operateName',
@ -475,7 +489,7 @@
title: 'system.log.time',
dataIndex: 'createTime',
fixed: 'right',
width: 180,
width: 100,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
@ -487,6 +501,7 @@
const { propsRes, propsEvent, loadList, setLoadListParams, resetPagination } = useTable(
requestFuncMap[props.mode].listFunc,
{
scroll: { x: 1100 },
tableKey: TableKeyEnum.SYSTEM_LOG,
columns,
selectable: false,

View File

@ -3,14 +3,19 @@
<div class="mb-4 flex items-center justify-between">
<div>
<a-button
v-permission="['SYSTEM_USER:READ+ADD', 'SYSTEM_USER_ROLE:READ']"
v-permission.all="['SYSTEM_USER:READ+ADD', 'SYSTEM_USER_ROLE:READ']"
class="mr-3"
type="primary"
@click="showUserModal('create')"
>
{{ t('system.user.createUser') }}
</a-button>
<a-button v-permission="['SYSTEM_USER_INVITE']" class="mr-3" type="outline" @click="showEmailInviteModal">
<a-button
v-permission.all="['SYSTEM_USER:READ+INVITE', 'SYSTEM_USER_ROLE:READ']"
class="mr-3"
type="outline"
@click="showEmailInviteModal"
>
{{ t('system.user.emailInvite') }}
</a-button>
<a-button v-permission="['SYSTEM_USER:READ+IMPORT']" class="mr-3" type="outline" @click="showImportModal">
@ -586,7 +591,12 @@
{
label: 'system.user.batchActionAddProject',
eventTag: 'batchAddProject',
permission: ['SYSTEM_USER:READ+UPDATE', 'SYSTEM_ORGANIZATION_PROJECT:READ'],
permission: [
'SYSTEM_USER:READ+UPDATE',
'SYSTEM_USER_ROLE:READ',
'SYSTEM_ORGANIZATION_PROJECT:READ',
'SYSTEM_ORGANIZATION_PROJECT_MEMBER:ADD',
],
},
{
label: 'system.user.batchActionAddUserGroup',
@ -596,7 +606,12 @@
{
label: 'system.user.batchActionAddOrganization',
eventTag: 'batchAddOrganization',
permission: ['SYSTEM_USER:READ+UPDATE', 'SYSTEM_ORGANIZATION_PROJECT:READ'],
permission: [
'SYSTEM_USER:READ+UPDATE',
'SYSTEM_USER_ROLE:READ',
'SYSTEM_ORGANIZATION_PROJECT:READ',
'SYSTEM_ORGANIZATION_PROJECT_MEMBER:ADD',
],
},
],
moreAction: [