diff --git a/frontend/apps/veaiops/src/components/wizard/steps/host-selection/components/shared/instance-selection-configs.tsx b/frontend/apps/veaiops/src/components/wizard/steps/host-selection/components/shared/instance-selection-configs.tsx
index a2826267..46bb1934 100644
--- a/frontend/apps/veaiops/src/components/wizard/steps/host-selection/components/shared/instance-selection-configs.tsx
+++ b/frontend/apps/veaiops/src/components/wizard/steps/host-selection/components/shared/instance-selection-configs.tsx
@@ -13,17 +13,18 @@
// limitations under the License.
/**
- * 实例选择配置工厂
- * @description 为不同数据源提供预定义的配置
+ * Instance selection config factory.
+ * @description Provides predefined instance selection configs for different data sources.
*/
import { IconCloud, IconDesktop } from '@arco-design/web-react/icon';
+import type { AliyunInstance, VolcengineInstance } from '@wizard/types';
+import { checkMatch } from '@wizard/utils/filter';
import type { ZabbixHost } from 'api-generate';
-import type { AliyunInstance, VolcengineInstance } from '../../../../types';
import type { InstanceSelectionConfig } from './instance-selection-config';
/**
- * 阿里云实例选择配置
+ * Aliyun instance selection config.
*/
export const createAliyunConfig = (
selectionAction: (instances: AliyunInstance[]) => void,
@@ -36,13 +37,13 @@ export const createAliyunConfig = (
itemType: '实例',
icon: ,
dataTransformer: (instance) => {
- // 当只有 userId 而没有 instanceId 时,使用 userId 作为 id
- // 这样可以确保标题和显示正确
+ // When only userId exists (no instanceId), use userId as the id
+ // This ensures the title and display are still meaningful
const id =
instance.instanceId ||
instance.dimensions?.instanceId ||
instance.dimensions?.userId ||
- instance.userId ||
+ (instance as AliyunInstance & { userId?: string }).userId ||
'';
const name =
instance.instanceName ||
@@ -62,28 +63,28 @@ export const createAliyunConfig = (
},
selectionAction,
searchFilter: (instance, searchValue) => {
- const searchLower = searchValue.toLowerCase();
return (
- (instance.instanceId?.toLowerCase() || '').includes(searchLower) ||
- (instance.instanceName?.toLowerCase() || '').includes(searchLower) ||
- (instance.region?.toLowerCase() || '').includes(searchLower) ||
- // 当只有 userId 时,也支持搜索 userId
- (instance.dimensions?.userId?.toLowerCase() || '').includes(
- searchLower,
- ) ||
- (instance.userId?.toLowerCase() || '').includes(searchLower)
+ checkMatch(instance.instanceId, searchValue) ||
+ checkMatch(instance.instanceName, searchValue) ||
+ checkMatch(instance.region, searchValue) ||
+ // When only userId exists, also allow searching by userId
+ checkMatch(instance.dimensions?.userId, searchValue) ||
+ checkMatch(
+ (instance as AliyunInstance & { userId?: string }).userId,
+ searchValue,
+ )
);
},
getId: (instance) =>
instance.instanceId ||
instance.dimensions?.instanceId ||
instance.dimensions?.userId ||
- instance.userId ||
+ (instance as AliyunInstance & { userId?: string }).userId ||
'',
});
/**
- * 火山引擎实例选择配置
+ * Volcengine instance selection config.
*/
export const createVolcengineConfig = (
selectionAction: (instances: VolcengineInstance[]) => void,
@@ -104,15 +105,15 @@ export const createVolcengineConfig = (
}),
selectionAction,
searchFilter: (instance, searchValue) =>
- instance.instanceId.toLowerCase().includes(searchValue) ||
- (instance.instanceName?.toLowerCase() || '').includes(searchValue) ||
- (instance.region?.toLowerCase() || '').includes(searchValue) ||
- (instance.namespace?.toLowerCase() || '').includes(searchValue),
+ checkMatch(instance.instanceId, searchValue) ||
+ checkMatch(instance.instanceName, searchValue) ||
+ checkMatch(instance.region, searchValue) ||
+ checkMatch(instance.namespace, searchValue),
getId: (instance) => instance.instanceId,
});
/**
- * Zabbix主机选择配置
+ * Zabbix host selection config.
*/
export const createZabbixConfig = (
selectionAction: (hosts: ZabbixHost[]) => void,
@@ -120,19 +121,18 @@ export const createZabbixConfig = (
title: '选择主机',
description: '选择要监控的主机,可以选择多个主机',
emptyDescription: '暂无可用的主机',
- searchPlaceholder: '搜索主机名称...',
+ searchPlaceholder: '搜索主机名称 (支持正则)...',
itemType: '主机',
icon: ,
dataTransformer: (host) => ({
- id: host.host, // 使用 host 作为唯一标识
+ id: host.host, // Use host as the unique identifier
name: host.name,
- region: undefined, // Zabbix没有region概念
- dimensions: undefined, // Zabbix没有dimensions概念
+ region: undefined, // Zabbix has no region concept
+ dimensions: undefined, // Zabbix has no dimensions concept
}),
selectionAction,
searchFilter: (host, searchValue) =>
- host.host.toLowerCase().includes(searchValue) ||
- host.name.toLowerCase().includes(searchValue),
- getId: (host) => host.host, // 使用 host 作为唯一标识
- useHostList: true, // 使用特殊的主机列表组件
+ checkMatch(host.host, searchValue) || checkMatch(host.name, searchValue),
+ getId: (host) => host.host, // Use host as the unique identifier
+ useHostList: true, // Use the specialized host list component
});
diff --git a/frontend/apps/veaiops/src/components/wizard/utils/filter.ts b/frontend/apps/veaiops/src/components/wizard/utils/filter.ts
new file mode 100644
index 00000000..626524ab
--- /dev/null
+++ b/frontend/apps/veaiops/src/components/wizard/utils/filter.ts
@@ -0,0 +1,135 @@
+// Copyright 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * Check whether the given text matches the search value.
+ * Supports both fuzzy substring matching and regular expressions.
+ * @param text Text to check
+ * @param searchValue Search value (plain string or regex pattern string)
+ * @returns Whether the text matches the search condition
+ */
+export const isMatch = (
+ text: string | undefined | null,
+ searchValue: string,
+): boolean => {
+ if (!text) {
+ return false;
+ }
+
+ const safeText = text.toLowerCase();
+ const safeSearch = searchValue.toLowerCase().trim();
+
+ if (!safeSearch) {
+ return true;
+ }
+
+ try {
+ // Try regex matching first
+ // Typical user inputs:
+ // - AA-\d+-BB style regex
+ // - AA-* style wildcard
+ // - Simple substring like AA
+
+ // Strategy:
+ // 1. Try to interpret the query as regex
+ // 2. If it fails and contains *, treat it as a wildcard
+ // 3. Otherwise fallback to plain substring match
+
+ // We directly build RegExp here to fully support regex syntax
+
+ const regex = new RegExp(safeSearch, 'i');
+ if (regex.test(text)) {
+ return true;
+ }
+ } catch (e) {
+ // If regex parsing fails (e.g. syntax error), try wildcard handling instead
+ }
+
+ // Wildcard handling (simple * to .*)
+ // Only used when query contains * and regex parsing failed
+ if (safeSearch.includes('*')) {
+ try {
+ // Escape all regex metacharacters except *
+ const pattern = safeSearch
+ .replace(/[.+?^${}()|[\]\\]/g, '\\$&')
+ .replace(/\*/g, '.*');
+ const wildcardRegex = new RegExp(`^${pattern}$`, 'i'); // Wildcard usually implies full match
+ if (wildcardRegex.test(text)) {
+ return true;
+ }
+ } catch (e) {
+ // Ignore and continue to substring fallback
+ }
+ }
+
+ // Final fallback: plain substring match
+ return safeText.includes(safeSearch);
+};
+
+/**
+ * Matching helper optimized for AA-number-BB style patterns
+ * while still supporting generic regex and wildcard queries.
+ */
+export const checkMatch = (
+ text: string | undefined | null,
+ searchValue: string,
+): boolean => {
+ if (!text) {
+ return false;
+ }
+
+ const safeText = text.trim();
+ const query = searchValue.trim();
+
+ if (!query) {
+ return true;
+ }
+
+ // 1. Try regex match first
+ // Typical usage:
+ // - AA-\d+-BB style patterns with \d for digits
+ // - ^server.* for prefix matching
+ // Using RegExp directly gives maximum flexibility for power users.
+
+ try {
+ // Use 'i' flag to make the match case-insensitive
+ const regex = new RegExp(query, 'i');
+ if (regex.test(safeText)) {
+ return true;
+ }
+ } catch (e) {
+ // If regex parsing fails (e.g. unclosed parenthesis), fall through to wildcard or substring
+ }
+
+ // 2. Try wildcard * match
+ // Only used when query contains * and regex parsing did not succeed
+ if (query.includes('*')) {
+ try {
+ // Escape all regex metacharacters except *
+ const pattern = query
+ .replace(/[.+?^${}()|[\]\\]/g, '\\$&')
+ .replace(/\*/g, '.*');
+ const wildcardRegex = new RegExp(`^${pattern}$`, 'i'); // Wildcard usually implies full match
+ if (wildcardRegex.test(safeText)) {
+ return true;
+ }
+ } catch (e) {
+ // Ignore and fallback to plain substring match
+ }
+ }
+
+ // 3. Plain substring match (case-insensitive)
+ // Fallback for queries like "server(" which are invalid regex but valid text
+ return safeText.toLowerCase().includes(query.toLowerCase());
+};