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