feat: tag search

This commit is contained in:
Unitwk 2024-09-02 16:54:14 +08:00
parent 0c8da65d1d
commit f35ff84158
13 changed files with 141 additions and 55 deletions

5
common/src/array.ts Normal file
View 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));
}

View File

@ -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 };

View File

@ -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
});
});

View File

@ -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>

View File

@ -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();
}

View File

@ -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
};
}

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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.",

View File

@ -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 代码。",

View File

@ -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;

View File

@ -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