Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,9 @@ import './index_data_structure_creator.scss';

type SelectionMode = 'single' | 'prefix';

export const IndexDataStructureCreator: React.FC<DataStructureCreatorProps> = ({
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;
Expand Down Expand Up @@ -117,7 +115,7 @@ export const IndexDataStructureCreator: React.FC<DataStructureCreatorProps> = ({

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);
Expand Down Expand Up @@ -148,10 +146,11 @@ export const IndexDataStructureCreator: React.FC<DataStructureCreatorProps> = ({
customPrefix={customPrefix}
validationError={validationError}
onPrefixChange={handlePrefixChange}
children={current.children}
children={current.children || []}
selectedIndexId={selectedIndexId}
isFinal={isFinal}
onIndexSelectionChange={handleIndexSelectionChange}
services={services}
/>

<EuiSpacer size="s" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,125 @@
* 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 {
children: DataStructure[] | undefined;
selectedIndexId: string | null;
isFinal: boolean;
onSelectionChange: (selectedId: string | null) => void;
httpService?: any; // HTTP service for dynamic search
}

export const IndexSelector: React.FC<IndexSelectorProps> = ({
children,
selectedIndexId,
isFinal,
onSelectionChange,
httpService,
}) => {
const options = (children || []).map((child) => ({
const [searchQuery, setSearchQuery] = useState('');
const [isSearching, setIsSearching] = useState(false);
const [dynamicIndices, setDynamicIndices] = useState<DataStructure[]>([]);

// 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,
Expand All @@ -51,8 +144,11 @@ export const IndexSelector: React.FC<IndexSelectorProps> = ({
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 <span>{option.label}</span>;

const prependIcon = child.meta?.type === DATA_STRUCTURE_META_TYPES.TYPE &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ interface ModeSelectionRowProps {
selectedIndexId: string | null;
isFinal: boolean;
onIndexSelectionChange: (selectedId: string | null) => void;
// Props for dynamic search
onSearchChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
isSearching?: boolean;
services?: any;
}

export const ModeSelectionRow: React.FC<ModeSelectionRowProps> = ({
Expand All @@ -36,6 +40,7 @@ export const ModeSelectionRow: React.FC<ModeSelectionRowProps> = ({
selectedIndexId,
isFinal,
onIndexSelectionChange,
services,
}) => {
const modeOptions = [
{
Expand Down Expand Up @@ -101,6 +106,7 @@ export const ModeSelectionRow: React.FC<ModeSelectionRowProps> = ({
selectedIndexId={selectedIndexId}
isFinal={isFinal}
onSelectionChange={onIndexSelectionChange}
httpService={services?.http}
/>
)}
</EuiFormRow>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -203,13 +214,17 @@ const mapDataSourceSavedObjectToDataStructure = (
};
};

const fetchIndices = async (dataStructure: DataStructure): Promise<string[]> => {
const fetchIndicesViaSearch = async (
dataStructure: DataStructure,
pattern: string = '*'
): Promise<MatchedIndex[]> => {
const search = getSearchService();

const buildSearchRequest = () => ({
params: {
ignoreUnavailable: true,
expand_wildcards: 'all',
index: '*',
index: pattern,
body: {
size: 0,
aggs: {
Expand Down Expand Up @@ -241,29 +256,125 @@ const fetchIndices = async (dataStructure: DataStructure): Promise<string[]> =>
// 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<MatchedIndex[]> => {
try {
const query: any = {
expand_wildcards: 'all',
};

if (dataStructure.id && dataStructure.id !== '') {
query.data_source = dataStructure.id;
}

const response = await http.get<ResolveIndexResponse>(
`/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<MatchedIndex[]> => {
// 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<string, MatchedIndex>();

// 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<Array<{ name: string; isRemoteIndex: boolean }>> => {
// 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
Expand Down
Loading
Loading