diff --git a/src/plugins/data/public/query/query_string/dataset_service/lib/index_data_structure_creator/index_data_structure_creator.tsx b/src/plugins/data/public/query/query_string/dataset_service/lib/index_data_structure_creator/index_data_structure_creator.tsx index 992028154f65..38622003cf97 100644 --- a/src/plugins/data/public/query/query_string/dataset_service/lib/index_data_structure_creator/index_data_structure_creator.tsx +++ b/src/plugins/data/public/query/query_string/dataset_service/lib/index_data_structure_creator/index_data_structure_creator.tsx @@ -19,11 +19,9 @@ import './index_data_structure_creator.scss'; type SelectionMode = 'single' | 'prefix'; -export const IndexDataStructureCreator: React.FC = ({ - path, - index, - selectDataStructure, -}) => { +export const IndexDataStructureCreator: React.FC< + DataStructureCreatorProps & { services?: any } +> = ({ path, index, selectDataStructure, services }) => { const current = path[index]; const isLast = index === path.length - 1; const isFinal = isLast && !current.hasNext; @@ -117,7 +115,7 @@ export const IndexDataStructureCreator: React.FC = ({ const handleIndexSelectionChange = (selectedId: string | null) => { if (selectedId) { - const item = current.children?.find((child) => child.id === selectedId); + const item = (current.children || []).find((child: DataStructure) => child.id === selectedId); if (item) { if (isFinal) { setSelectedIndexId(selectedId); @@ -148,10 +146,11 @@ export const IndexDataStructureCreator: React.FC = ({ customPrefix={customPrefix} validationError={validationError} onPrefixChange={handlePrefixChange} - children={current.children} + children={current.children || []} selectedIndexId={selectedIndexId} isFinal={isFinal} onIndexSelectionChange={handleIndexSelectionChange} + services={services} /> diff --git a/src/plugins/data/public/query/query_string/dataset_service/lib/index_data_structure_creator/index_selector.tsx b/src/plugins/data/public/query/query_string/dataset_service/lib/index_data_structure_creator/index_selector.tsx index 71456e154c59..87acc06e9003 100644 --- a/src/plugins/data/public/query/query_string/dataset_service/lib/index_data_structure_creator/index_selector.tsx +++ b/src/plugins/data/public/query/query_string/dataset_service/lib/index_data_structure_creator/index_selector.tsx @@ -3,11 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import React, { useState, useCallback, useMemo } from 'react'; import { EuiComboBox, EuiIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@osd/i18n'; +import { debounce } from 'lodash'; import { DataStructure, DATA_STRUCTURE_META_TYPES } from '../../../../../../common'; import { appendIcon } from './index_data_structure_creator_utils'; +import { fetchIndicesByPattern } from '../index_type'; import './index_selector.scss'; interface IndexSelectorProps { @@ -15,6 +17,7 @@ interface IndexSelectorProps { selectedIndexId: string | null; isFinal: boolean; onSelectionChange: (selectedId: string | null) => void; + httpService?: any; // HTTP service for dynamic search } export const IndexSelector: React.FC = ({ @@ -22,13 +25,103 @@ export const IndexSelector: React.FC = ({ selectedIndexId, isFinal, onSelectionChange, + httpService, }) => { - const options = (children || []).map((child) => ({ + const [searchQuery, setSearchQuery] = useState(''); + const [isSearching, setIsSearching] = useState(false); + const [dynamicIndices, setDynamicIndices] = useState([]); + + // Dynamic search function + const performSearch = useCallback( + async (query: string) => { + if (!query || query.length < 1) { + setDynamicIndices([]); + setIsSearching(false); + return; + } + + setIsSearching(true); + + try { + // Find data source from current children + const firstChild = children?.[0]; + + if (!firstChild) { + setDynamicIndices([]); + setIsSearching(false); + return; + } + + // Extract data source ID from the child ID (format: dataSourceId::indexName) + const dataSourceId = firstChild.id.includes('::') ? firstChild.id.split('::')[0] : 'local'; + + // Create mock data source structure for search + const dataSource = { + id: dataSourceId, + type: 'DATA_SOURCE', + title: dataSourceId, + }; + + // Create pattern for search - add wildcard if not present + const searchPattern = query.includes('*') ? query : `${query}*`; + + if (httpService) { + const matchedIndices = await fetchIndicesByPattern( + dataSource, + httpService, + searchPattern + ); + + // Convert to DataStructure format + const indexStructures: DataStructure[] = matchedIndices.map((matchedIndex) => ({ + id: `${dataSourceId}::${matchedIndex.name}`, + title: matchedIndex.name, + type: 'INDEX', + meta: { + type: DATA_STRUCTURE_META_TYPES.CUSTOM, + isRemoteIndex: matchedIndex.isRemoteIndex, + }, + })); + + setDynamicIndices(indexStructures); + } + } catch (error) { + setDynamicIndices([]); + } finally { + setIsSearching(false); + } + }, + [children, httpService] + ); + + // Debounced search (500ms to match explore plugin standards) + const debouncedSearch = useMemo(() => debounce((query: string) => performSearch(query), 500), [ + performSearch, + ]); + + // Handle search change + const handleSearchChange = useCallback( + (searchValue: string) => { + setSearchQuery(searchValue); + debouncedSearch(searchValue); + }, + [debouncedSearch] + ); + + // Get combined options - use dynamic search results if searching, otherwise use static children + const allIndices = useMemo(() => { + if (searchQuery && searchQuery.length >= 1) { + return dynamicIndices; + } + return children || []; + }, [searchQuery, dynamicIndices, children]); + + const options = allIndices.map((child) => ({ label: child.parent ? `${child.parent.title}::${child.title}` : child.title, value: child.id, })); - const selectedOptions = (children || []) + const selectedOptions = allIndices .filter((child) => child.id === selectedIndexId) .map((child) => ({ label: child.parent ? `${child.parent.title}::${child.title}` : child.title, @@ -51,8 +144,11 @@ export const IndexSelector: React.FC = ({ onSelectionChange(null); } }} + onSearchChange={handleSearchChange} + isLoading={isSearching} + async renderOption={(option) => { - const child = (children || []).find((c) => c.id === option.value); + const child = allIndices.find((c) => c.id === option.value); if (!child) return {option.label}; const prependIcon = child.meta?.type === DATA_STRUCTURE_META_TYPES.TYPE && diff --git a/src/plugins/data/public/query/query_string/dataset_service/lib/index_data_structure_creator/mode_selection_row.tsx b/src/plugins/data/public/query/query_string/dataset_service/lib/index_data_structure_creator/mode_selection_row.tsx index 7e12256b92fa..da9f46e676c3 100644 --- a/src/plugins/data/public/query/query_string/dataset_service/lib/index_data_structure_creator/mode_selection_row.tsx +++ b/src/plugins/data/public/query/query_string/dataset_service/lib/index_data_structure_creator/mode_selection_row.tsx @@ -24,6 +24,10 @@ interface ModeSelectionRowProps { selectedIndexId: string | null; isFinal: boolean; onIndexSelectionChange: (selectedId: string | null) => void; + // Props for dynamic search + onSearchChange?: (e: React.ChangeEvent) => void; + isSearching?: boolean; + services?: any; } export const ModeSelectionRow: React.FC = ({ @@ -36,6 +40,7 @@ export const ModeSelectionRow: React.FC = ({ selectedIndexId, isFinal, onIndexSelectionChange, + services, }) => { const modeOptions = [ { @@ -101,6 +106,7 @@ export const ModeSelectionRow: React.FC = ({ selectedIndexId={selectedIndexId} isFinal={isFinal} onSelectionChange={onIndexSelectionChange} + httpService={services?.http} /> )} diff --git a/src/plugins/data/public/query/query_string/dataset_service/lib/index_type.test.ts b/src/plugins/data/public/query/query_string/dataset_service/lib/index_type.test.ts index 8beb660f6121..6b71f4cea335 100644 --- a/src/plugins/data/public/query/query_string/dataset_service/lib/index_type.test.ts +++ b/src/plugins/data/public/query/query_string/dataset_service/lib/index_type.test.ts @@ -210,7 +210,7 @@ describe('indexTypeConfig', () => { ).toBeDefined(); }); - describe('fetchIndices', () => { + describe('fetch indices for DATA_SOURCE', () => { test('should extract index names correctly from different formats', async () => { const mockResponse = { rawResponse: { diff --git a/src/plugins/data/public/query/query_string/dataset_service/lib/index_type.ts b/src/plugins/data/public/query/query_string/dataset_service/lib/index_type.ts index 46b55624d36f..96c4a71e77a8 100644 --- a/src/plugins/data/public/query/query_string/dataset_service/lib/index_type.ts +++ b/src/plugins/data/public/query/query_string/dataset_service/lib/index_type.ts @@ -27,6 +27,17 @@ import { import { DataSourceEngineType } from '../../../../../../../plugins/data_source/common/data_sources'; import { IndexDataStructureCreator } from './index_data_structure_creator/index_data_structure_creator'; +interface MatchedIndex { + name: string; + isRemoteIndex: boolean; +} + +interface ResolveIndexResponse { + indices?: Array<{ name: string; attributes?: string[] }>; + aliases?: Array<{ name: string }>; + data_streams?: Array<{ name: string }>; +} + export const DELIMITER = '::'; export const indexTypeConfig: DatasetTypeConfig = { @@ -203,13 +214,17 @@ const mapDataSourceSavedObjectToDataStructure = ( }; }; -const fetchIndices = async (dataStructure: DataStructure): Promise => { +const fetchIndicesViaSearch = async ( + dataStructure: DataStructure, + pattern: string = '*' +): Promise => { const search = getSearchService(); + const buildSearchRequest = () => ({ params: { ignoreUnavailable: true, expand_wildcards: 'all', - index: '*', + index: pattern, body: { size: 0, aggs: { @@ -241,29 +256,125 @@ const fetchIndices = async (dataStructure: DataStructure): Promise => // extract index name or return original key if pattern doesn't match uniqueResponses.add(lastPart.split(':')[0] || key); }); - return Array.from(uniqueResponses); + + return Array.from(uniqueResponses).map((name) => ({ + name, + isRemoteIndex: false, + })); }; - return search - .getDefaultSearchInterceptor() - .search(buildSearchRequest()) - .pipe(map(searchResponseToArray)) - .toPromise(); + try { + return await search + .getDefaultSearchInterceptor() + .search(buildSearchRequest()) + .pipe(map(searchResponseToArray)) + .toPromise(); + } catch (error) { + return []; + } +}; + +const fetchIndicesViaResolve = async ( + dataStructure: DataStructure, + http: HttpSetup, + pattern: string = '*' +): Promise => { + try { + const query: any = { + expand_wildcards: 'all', + }; + + if (dataStructure.id && dataStructure.id !== '') { + query.data_source = dataStructure.id; + } + + const response = await http.get( + `/internal/index-pattern-management/resolve_index/${pattern}`, + { query } + ); + + if (!response) { + return []; + } + + const indices: MatchedIndex[] = []; + + // Add regular indices + if (response.indices) { + response.indices.forEach((index) => { + indices.push({ + name: index.name, + isRemoteIndex: false, + }); + }); + } + + // Add aliases as indices + if (response.aliases) { + response.aliases.forEach((alias) => { + indices.push({ + name: alias.name, + isRemoteIndex: false, + }); + }); + } + + // Add data streams as indices + if (response.data_streams) { + response.data_streams.forEach((dataStream) => { + indices.push({ + name: dataStream.name, + isRemoteIndex: false, + }); + }); + } + + return indices; + } catch (error) { + return []; + } +}; + +export const fetchIndicesByPattern = async ( + dataStructure: DataStructure, + http: HttpSetup, + pattern: string = '*' +): Promise => { + // Validate pattern + if (pattern === '*:' || pattern === '' || pattern.startsWith(',')) { + return []; + } + + const [searchIndices, resolveIndices] = await Promise.all([ + fetchIndicesViaSearch(dataStructure, pattern), + fetchIndicesViaResolve(dataStructure, http, pattern), + ]); + + // Deduplicate results (prioritize resolve API results) + const indexMap = new Map(); + + // Add search results first + searchIndices.forEach((index) => { + indexMap.set(index.name, index); + }); + + // Add resolve results (will overwrite duplicates) + resolveIndices.forEach((index) => { + indexMap.set(index.name, index); + }); + + return Array.from(indexMap.values()).sort((a, b) => a.name.localeCompare(b.name)); }; const fetchAllIndices = async ( dataStructure: DataStructure, - http: HttpSetup + http: HttpSetup, + pattern: string = '*' ): Promise> => { // Create promises for both local and remote indices const [localIndices, remoteIndices] = await Promise.all([ - // Fetch local indices - fetchIndices(dataStructure).then((indices) => - indices.map((index) => ({ - name: index, - isRemoteIndex: false, - })) - ), + // Fetch local indices using new dynamic approach + fetchIndicesByPattern(dataStructure, http, pattern), // Fetch remote indices if they exist dataStructure?.remoteConnections diff --git a/src/plugins/data/public/ui/dataset_selector/dataset_explorer.tsx b/src/plugins/data/public/ui/dataset_selector/dataset_explorer.tsx index 68256c60ea75..425aa1249b4d 100644 --- a/src/plugins/data/public/ui/dataset_selector/dataset_explorer.tsx +++ b/src/plugins/data/public/ui/dataset_selector/dataset_explorer.tsx @@ -190,6 +190,7 @@ export const DatasetExplorer = ({ selectDataStructure={selectDataStructure} // @ts-ignore custom component can have their own fetch options fetchDataStructure={fetchNextDataStructure} + services={services} /> ) : current.multiSelect ? (