Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -36,13 +37,13 @@ export const createAliyunConfig = (
itemType: '实例',
icon: <IconCloud />,
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 ||
Expand All @@ -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,
Expand All @@ -104,35 +105,34 @@ 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,
): InstanceSelectionConfig<ZabbixHost> => ({
title: '选择主机',
description: '选择要监控的主机,可以选择多个主机',
emptyDescription: '暂无可用的主机',
searchPlaceholder: '搜索主机名称...',
searchPlaceholder: '搜索主机名称 (支持正则)...',
itemType: '主机',
icon: <IconDesktop />,
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
});
135 changes: 135 additions & 0 deletions frontend/apps/veaiops/src/components/wizard/utils/filter.ts
Original file line number Diff line number Diff line change
@@ -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());
};