mirror of
https://github.com/MCSManager/MCSManager.git
synced 2025-02-17 15:59:41 +08:00
feat: tag search
This commit is contained in:
parent
0c8da65d1d
commit
f35ff84158
5
common/src/array.ts
Normal file
5
common/src/array.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export function arrayUnique<T>(arr: T[], felidName?: string): T[] {
|
||||
if (!felidName) return Array.from(new Set(arr));
|
||||
const map = new Map();
|
||||
return arr.filter((v: any) => !map.has(v[felidName]) && map.set(v[felidName], v));
|
||||
}
|
@ -1,17 +1,20 @@
|
||||
import StorageSubsystem from "./system_storage";
|
||||
import GlobalVariable from "./global_variable";
|
||||
import InstanceStreamListener from "./instance_stream";
|
||||
import { ProcessWrapper, killProcess } from "./process_tools";
|
||||
import { systemInfo } from "./system_info";
|
||||
|
||||
import MCServerStatus from "./mcping";
|
||||
import {
|
||||
|
||||
export { ProcessWrapper, killProcess } from "./process_tools";
|
||||
export { systemInfo } from "./system_info";
|
||||
export {
|
||||
QueryMapWrapper,
|
||||
IDataSource,
|
||||
MySqlSource,
|
||||
LocalFileSource,
|
||||
QueryWrapper
|
||||
} from "./query_wrapper";
|
||||
import {
|
||||
|
||||
export {
|
||||
configureEntityParams,
|
||||
toText,
|
||||
toBoolean,
|
||||
@ -20,25 +23,8 @@ import {
|
||||
supposeValue
|
||||
} from "./typecheck";
|
||||
|
||||
export { arrayUnique } from "./array";
|
||||
|
||||
export { removeTrail } from "./string_utils";
|
||||
|
||||
export {
|
||||
MCServerStatus,
|
||||
StorageSubsystem,
|
||||
GlobalVariable,
|
||||
InstanceStreamListener,
|
||||
ProcessWrapper,
|
||||
QueryMapWrapper,
|
||||
IDataSource,
|
||||
MySqlSource,
|
||||
LocalFileSource,
|
||||
QueryWrapper,
|
||||
killProcess,
|
||||
configureEntityParams,
|
||||
toText,
|
||||
toBoolean,
|
||||
toNumber,
|
||||
isEmpty,
|
||||
supposeValue,
|
||||
systemInfo
|
||||
};
|
||||
export { MCServerStatus, StorageSubsystem, GlobalVariable, InstanceStreamListener };
|
||||
|
@ -19,6 +19,8 @@ import RestartCommand from "../entity/commands/restart";
|
||||
import { TaskCenter } from "../service/async_task_service";
|
||||
import { createQuickInstallTask } from "../service/async_task_service/quick_install";
|
||||
import { QuickInstallTask } from "../service/async_task_service/quick_install";
|
||||
import { toNumber, toText } from "common";
|
||||
import { arrayUnique } from "common";
|
||||
|
||||
// Some instances operate router authentication middleware
|
||||
routerApp.use((event, ctx, data, next) => {
|
||||
@ -43,17 +45,29 @@ routerApp.use((event, ctx, data, next) => {
|
||||
|
||||
// Get the list of instances of this daemon (query)
|
||||
routerApp.on("instance/select", (ctx, data) => {
|
||||
const page = data.page || 1;
|
||||
const pageSize = data.pageSize || 1;
|
||||
const page = toNumber(data.page) ?? 1;
|
||||
const pageSize = toNumber(data.pageSize) ?? 1;
|
||||
const condition = data.condition;
|
||||
const targetTag = data.condition.tag;
|
||||
const overview: IInstanceDetail[] = [];
|
||||
// keyword condition query
|
||||
const queryWrapper = InstanceSubsystem.getQueryMapWrapper();
|
||||
const allTags: string[] = [];
|
||||
|
||||
let tagText = "";
|
||||
if (targetTag instanceof Array && targetTag.length > 0) {
|
||||
tagText = targetTag.sort((a, b) => (a > b ? 1 : -1)).join(",");
|
||||
}
|
||||
|
||||
let result = queryWrapper.select<Instance>((v) => {
|
||||
if (v.config.tag) allTags.push(...v.config.tag);
|
||||
if (InstanceSubsystem.isGlobalInstance(v)) return false;
|
||||
if (!v.config.nickname.toLowerCase().includes(condition.instanceName.toLowerCase()))
|
||||
return false;
|
||||
if (condition.status && v.instanceStatus !== Number(condition.status)) return false;
|
||||
if (tagText) {
|
||||
if (!v.config.tag.join(",").includes(tagText)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
result = result.sort((a, b) => (a.config.nickname > b.config.nickname ? 1 : -1));
|
||||
@ -78,6 +92,7 @@ routerApp.on("instance/select", (ctx, data) => {
|
||||
page: pageResult.page,
|
||||
pageSize: pageResult.pageSize,
|
||||
maxPage: pageResult.maxPage,
|
||||
allTags: arrayUnique(allTags),
|
||||
data: overview
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, reactive, nextTick, unref } from "vue";
|
||||
import { ref, reactive, nextTick } from "vue";
|
||||
import type { MountComponent } from "@/types";
|
||||
import { t } from "@/lang/i18n";
|
||||
|
||||
@ -23,11 +23,14 @@ const state = reactive({
|
||||
inputValue: ""
|
||||
});
|
||||
|
||||
const open = ref(false);
|
||||
const { tagTips } = useInstanceTagTips();
|
||||
const { removeTag, addTag, instanceTags, saveTags, saveLoading, instanceTagsTips } =
|
||||
useInstanceTags(props.instanceId, props.daemonId, props.tags, tagTips.value);
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let resolve: (tags: string[]) => void;
|
||||
const open = ref(false);
|
||||
|
||||
const handleInputConfirm = () => {
|
||||
if (state.inputValue) {
|
||||
addTag(state.inputValue);
|
||||
@ -45,6 +48,7 @@ const showInput = () => {
|
||||
|
||||
const cancel = async () => {
|
||||
open.value = false;
|
||||
resolve(instanceTags.value);
|
||||
if (props.destroyComponent) props.destroyComponent(1000);
|
||||
};
|
||||
|
||||
@ -52,15 +56,20 @@ const submit = async () => {
|
||||
try {
|
||||
await saveTags();
|
||||
message.success(t("保存成功"));
|
||||
await cancel();
|
||||
} catch (error) {
|
||||
reportErrorMsg(error);
|
||||
}
|
||||
await cancel();
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
const openDialog = () => {
|
||||
open.value = true;
|
||||
});
|
||||
return new Promise<string[]>((_resolve) => {
|
||||
resolve = _resolve;
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({ openDialog });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -90,6 +99,11 @@ onMounted(async () => {
|
||||
{{ t("此实例的标签列表") }}
|
||||
</a-typography-text>
|
||||
</a-typography-paragraph>
|
||||
<a-typography-paragraph>
|
||||
<a-typography-text type="secondary">
|
||||
{{ t("单个实例最多支持 6 个标签,每个标签不可超过 9 个字符。") }}
|
||||
</a-typography-text>
|
||||
</a-typography-paragraph>
|
||||
<div class="tag-container">
|
||||
<a-tag
|
||||
v-for="tag in instanceTags"
|
||||
@ -129,7 +143,7 @@ onMounted(async () => {
|
||||
<a-typography-text type="secondary">
|
||||
{{
|
||||
t(
|
||||
"这些可选择的标签由当前页实例列表计算而来,并不包含所有已存在的标签,此处最多展示 20 个"
|
||||
"这些可选择的标签由当前页实例列表计算而来,并不包含所有已存在的标签,此处最多展示 30 个"
|
||||
)
|
||||
}}
|
||||
</a-typography-text>
|
||||
|
@ -139,10 +139,12 @@ export async function openInstanceTagsEditor(
|
||||
tags: string[],
|
||||
tagsTips?: string[]
|
||||
) {
|
||||
return useMountComponent({
|
||||
return await useMountComponent({
|
||||
instanceId,
|
||||
daemonId,
|
||||
tagsTips,
|
||||
tags
|
||||
}).load<InstanceType<typeof TagsDialog>>(TagsDialog);
|
||||
})
|
||||
.load<InstanceType<typeof TagsDialog>>(TagsDialog)
|
||||
.openDialog();
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import _ from "lodash";
|
||||
import { computed, ref } from "vue";
|
||||
import { updateInstanceConfig } from "@/services/apis/instance";
|
||||
import { arrayUnique } from "@/tools/array";
|
||||
import type { InstanceDetail } from "@/types";
|
||||
import { createGlobalState } from "@vueuse/core";
|
||||
|
||||
export function useInstanceTags(
|
||||
@ -16,7 +15,7 @@ export function useInstanceTags(
|
||||
if (tagsTips) {
|
||||
const tmp = tagsTips.filter((tag) => !_.includes(instanceTags.value, tag));
|
||||
return arrayUnique(tmp)
|
||||
.slice(0, 20)
|
||||
.slice(0, 30)
|
||||
.sort((a, b) => (a > b ? 1 : -1));
|
||||
}
|
||||
return [];
|
||||
@ -25,7 +24,10 @@ export function useInstanceTags(
|
||||
|
||||
const removeTag = (tag: string) => {
|
||||
tag = tag.trim();
|
||||
instanceTags.value.splice(tags.indexOf(tag), 1);
|
||||
instanceTags.value.splice(
|
||||
instanceTags.value.findIndex((v) => v === tag),
|
||||
1
|
||||
);
|
||||
};
|
||||
|
||||
const addTag = (tag: string) => {
|
||||
@ -33,6 +35,7 @@ export function useInstanceTags(
|
||||
if (!_.includes(instanceTags.value, tag)) {
|
||||
instanceTags.value.push(tag);
|
||||
}
|
||||
instanceTags.value = instanceTags.value.sort((a, b) => (a > b ? 1 : -1));
|
||||
};
|
||||
|
||||
const saveTags = async () => {
|
||||
@ -59,10 +62,8 @@ export function useInstanceTags(
|
||||
|
||||
export const useInstanceTagTips = createGlobalState(() => {
|
||||
const tags = ref<string[]>([]);
|
||||
const updateTagTips = (instances: InstanceDetail[]) => {
|
||||
instances.forEach((instance) => {
|
||||
tags.value = arrayUnique(tags.value.concat(instance?.config?.tag || []));
|
||||
});
|
||||
const updateTagTips = (allTags: string[]) => {
|
||||
tags.value = allTags;
|
||||
};
|
||||
|
||||
return {
|
||||
@ -70,3 +71,34 @@ export const useInstanceTagTips = createGlobalState(() => {
|
||||
updateTagTips
|
||||
};
|
||||
});
|
||||
|
||||
export function useInstanceTagSearch() {
|
||||
const tags = ref<string[]>([]);
|
||||
let searchFn: Function = () => {};
|
||||
|
||||
const selectTag = (tag: string) => {
|
||||
tags.value.push(tag);
|
||||
searchFn();
|
||||
};
|
||||
|
||||
const removeTag = (tag: string) => {
|
||||
tags.value = tags.value.filter((v) => v !== tag);
|
||||
searchFn();
|
||||
};
|
||||
|
||||
const setRefreshFn = (fn: Function) => {
|
||||
searchFn = fn;
|
||||
};
|
||||
|
||||
const isTagSelected = (tag: string) => {
|
||||
return _.includes(tags.value, tag);
|
||||
};
|
||||
|
||||
return {
|
||||
tags,
|
||||
selectTag,
|
||||
removeTag,
|
||||
setRefreshFn,
|
||||
isTagSelected
|
||||
};
|
||||
}
|
||||
|
@ -92,6 +92,7 @@ export const remoteInstances = useDefineApi<
|
||||
page_size: number;
|
||||
instance_name?: string;
|
||||
status: string;
|
||||
tag?: string;
|
||||
};
|
||||
},
|
||||
{
|
||||
@ -99,6 +100,7 @@ export const remoteInstances = useDefineApi<
|
||||
page: 1;
|
||||
pageSize: 10;
|
||||
data: InstanceDetail[];
|
||||
allTags: string[];
|
||||
}
|
||||
>({
|
||||
url: "/api/service/remote_service_instances"
|
||||
|
@ -38,7 +38,7 @@ import { useScreen } from "@/hooks/useScreen";
|
||||
import { reportErrorMsg } from "@/tools/validator";
|
||||
import { INSTANCE_STATUS } from "@/types/const";
|
||||
import Shortcut from "./instance/Shortcut.vue";
|
||||
import { useInstanceTagTips } from "@/hooks/useInstanceTag";
|
||||
import { useInstanceTagSearch, useInstanceTagTips } from "@/hooks/useInstanceTag";
|
||||
|
||||
defineProps<{
|
||||
card: LayoutCard;
|
||||
@ -57,6 +57,13 @@ const currentRemoteNode = ref<NodeStatus>();
|
||||
const { execute: getNodes, state: nodes, isLoading: isLoading1 } = remoteNodeList();
|
||||
const { execute: getInstances, state: instances, isLoading: isLoading2 } = remoteInstances();
|
||||
const { updateTagTips, tagTips } = useInstanceTagTips();
|
||||
const {
|
||||
tags: searchTags,
|
||||
setRefreshFn,
|
||||
selectTag,
|
||||
removeTag,
|
||||
isTagSelected
|
||||
} = useInstanceTagSearch();
|
||||
|
||||
const isLoading = computed(() => isLoading1.value || isLoading2.value);
|
||||
|
||||
@ -98,10 +105,11 @@ const initInstancesData = async (resetPage?: boolean) => {
|
||||
page: operationForm.value.currentPage,
|
||||
page_size: operationForm.value.pageSize,
|
||||
status: operationForm.value.status,
|
||||
instance_name: operationForm.value.instanceName.trim()
|
||||
instance_name: operationForm.value.instanceName.trim(),
|
||||
tag: JSON.stringify(searchTags.value)
|
||||
}
|
||||
});
|
||||
updateTagTips(unref(instancesMoreInfo));
|
||||
updateTagTips(instances.value?.allTags || []);
|
||||
} catch (err) {
|
||||
return reportErrorMsg(t("TXT_CODE_e109c091"));
|
||||
}
|
||||
@ -299,6 +307,7 @@ const batchDeleteInstance = async (deleteFile: boolean) => {
|
||||
|
||||
onMounted(async () => {
|
||||
await initInstancesData();
|
||||
setRefreshFn(initInstancesData);
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -450,7 +459,13 @@ onMounted(async () => {
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<div v-if="tagTips && tagTips?.length > 0" class="instances-tag-container">
|
||||
<a-tag v-for="item in tagTips" :key="item" class="my-tag" color="purple">
|
||||
<a-tag
|
||||
v-for="item in tagTips"
|
||||
:key="item"
|
||||
class="my-tag"
|
||||
:color="isTagSelected(item) ? 'blue' : ''"
|
||||
@click="isTagSelected(item) ? removeTag(item) : selectTag(item)"
|
||||
>
|
||||
{{ item }}
|
||||
</a-tag>
|
||||
</div>
|
||||
|
@ -202,11 +202,12 @@ const instanceOperations = computed(() =>
|
||||
{
|
||||
title: t("标签分组"),
|
||||
icon: TagsOutlined,
|
||||
click: (event: MouseEvent) => {
|
||||
click: async (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
if (instanceId && daemonId) {
|
||||
const tags = instanceInfo.value?.config.tag || [];
|
||||
openInstanceTagsEditor(instanceId, daemonId, tags);
|
||||
await openInstanceTagsEditor(instanceId, daemonId, tags);
|
||||
refreshList();
|
||||
}
|
||||
},
|
||||
disabled: containerState.isDesignMode
|
||||
@ -312,4 +313,11 @@ const instanceOperations = computed(() =>
|
||||
border: 1px solid var(--color-gray-8);
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
.instance-tag-container {
|
||||
margin-left: -4px;
|
||||
margin-right: -4px;
|
||||
.my-tag {
|
||||
margin: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1686,7 +1686,7 @@
|
||||
"TXT_CODE_a7f6b0e0": "Container Resource Limitation",
|
||||
"TXT_CODE_77000411": "Image Name",
|
||||
"TXT_CODE_d32301c1": "Available Ports",
|
||||
"TXT_CODE_5415f009": "No content. Please select a daemon from the dropdown in the upper right corner, or create a new instance",
|
||||
"TXT_CODE_5415f009": "No content, please try changing the node or search criteria",
|
||||
"TXT_CODE_c846074d": "Please complete your account profile!",
|
||||
"TXT_CODE_e520908a": "The version of the remote daemon is inconsistent with the version required by the panel. This may cause critical issues. Please update the remote daemon immediately!",
|
||||
"TXT_CODE_c565b2e0": "Allows uploading HTML files. This card will load the HTML and run Javascript code.",
|
||||
|
@ -1687,7 +1687,7 @@
|
||||
"TXT_CODE_a7f6b0e0": "容器资源限制信息",
|
||||
"TXT_CODE_77000411": "镜像名",
|
||||
"TXT_CODE_d32301c1": "可用端口",
|
||||
"TXT_CODE_5415f009": "无内容,请在右上角下拉框选择节点,或点击新建应用",
|
||||
"TXT_CODE_5415f009": "无内容,请尝试更改节点或搜索条件",
|
||||
"TXT_CODE_c846074d": "请完善账号信息!",
|
||||
"TXT_CODE_e520908a": "远程节点版本与面板端所需版本不一致,这可能会导致工作异常,请立即更新远程节点!",
|
||||
"TXT_CODE_c565b2e0": "支持上传 HTML 文件,此卡片会加载 HTML 并运行 Javascript 代码。",
|
||||
|
@ -38,13 +38,15 @@ router.get(
|
||||
const pageSize = Number(ctx.query.page_size);
|
||||
const instanceName = ctx.query.instance_name;
|
||||
const status = String(ctx.query.status);
|
||||
const tag = String(ctx.query.tag);
|
||||
const remoteService = RemoteServiceSubsystem.getInstance(daemonId);
|
||||
const result = await new RemoteRequest(remoteService).request("instance/select", {
|
||||
page,
|
||||
pageSize,
|
||||
condition: {
|
||||
instanceName,
|
||||
status
|
||||
status,
|
||||
tag: tag ? JSON.parse(tag) : null
|
||||
}
|
||||
});
|
||||
ctx.body = result;
|
||||
|
@ -372,11 +372,16 @@ router.put(
|
||||
|
||||
let instanceTags: string[] | null = null;
|
||||
|
||||
if (isTopPermissionByUuid(getUserUuid(ctx))) {
|
||||
instanceTags =
|
||||
config?.tag instanceof Array
|
||||
? (config.tag as any[]).map((tag: any) => String(tag).trim())
|
||||
: null;
|
||||
if (config.tag instanceof Array && isTopPermissionByUuid(getUserUuid(ctx))) {
|
||||
instanceTags = (config.tag as any[]).map((tag: any) => {
|
||||
const tmp = String(tag).trim();
|
||||
if (tmp.length > 9) throw new Error($t("保存失败,单个标签最多只支持9个字符!"));
|
||||
return tmp;
|
||||
});
|
||||
if (instanceTags.length > 6) {
|
||||
throw new Error($t("保存失败,单个实例最多只支持6个标签!"));
|
||||
}
|
||||
instanceTags = instanceTags!.sort((a, b) => (a > b ? 1 : -1));
|
||||
}
|
||||
|
||||
// Steam Rcon configuration
|
||||
|
Loading…
Reference in New Issue
Block a user