Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Indexed view using DATA_STRUCTURE and fetch #8845

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
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
1 change: 1 addition & 0 deletions src/plugins/data/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const DEFAULT_DATA = {
SET_TYPES: {
INDEX_PATTERN: 'INDEX_PATTERN',
INDEX: 'INDEXES',
DATASET: 'DATASET',
},

SOURCE_TYPES: {
Expand Down
6 changes: 6 additions & 0 deletions src/plugins/data/common/datasets/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ export interface DataSourceMeta {
sessionId?: string;
/** Optional supportsTimeFilter determines if a time filter is needed */
supportsTimeFilter?: boolean;
/** Optional reference to the original dataset */
ref?: {
id: string;
type: string;
title: string;
};
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ const createSetupDatasetServiceMock = (): jest.Mocked<DatasetServiceContract> =>
fetchOptions: jest.fn(),
getRecentDatasets: jest.fn(),
addRecentDataset: jest.fn(),
clearCache: jest.fn(),
getLastCacheTime: jest.fn(),
removeFromRecentDatasets: jest.fn(),
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,11 @@ export class DatasetService {
dataset: Dataset,
services: Partial<IDataPluginServices>
): Promise<void> {
const type = this.getType(dataset?.type);
const datasetType = dataset.dataSource?.meta?.ref?.type || dataset.type;
const type = this.getType(datasetType);
try {
const asyncType = type?.meta.isFieldLoadAsync ?? false;
if (dataset && dataset.type !== DEFAULT_DATA.SET_TYPES.INDEX_PATTERN) {
if (datasetType !== DEFAULT_DATA.SET_TYPES.INDEX_PATTERN) {
const fetchedFields = asyncType
? ({} as IndexPatternFieldMap)
: await type?.fetchFields(dataset, services);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export interface DatasetTypeConfig {
searchOnLoad?: boolean;
/** Optional supportsTimeFilter determines if a time filter is needed */
supportsTimeFilter?: boolean;
/** Optional supportsIndexedViews determines if indexed views are supported */
supportsIndexedViews?: boolean;
/** Optional isFieldLoadAsync determines if field loads are async */
isFieldLoadAsync?: boolean;
/** Optional cacheOptions determines if the data structure is cacheable. Defaults to false */
Expand Down
119 changes: 116 additions & 3 deletions src/plugins/data/public/ui/dataset_selector/configurator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,15 @@ import {
import { i18n } from '@osd/i18n';
import { FormattedMessage } from '@osd/i18n/react';
import React, { useEffect, useMemo, useState } from 'react';
import { BaseDataset, DEFAULT_DATA, Dataset, DatasetField, Query } from '../../../common';
import {
BaseDataset,
DEFAULT_DATA,
DataStructure,
DataStructureMeta,
Dataset,
DatasetField,
Query,
} from '../../../common';
import { getIndexPatterns, getQueryService } from '../../services';
import { IDataPluginServices } from '../../types';

Expand Down Expand Up @@ -52,6 +60,23 @@ export const Configurator = ({
defaultMessage: "I don't want to use the time filter",
}
);
const [indexedViews, setIndexedViews] = useState<DataStructure[]>([]);
const [indexedView, setIndexedView] = useState<DataStructure | undefined>(
dataset.dataSource?.meta?.ref
? ({
id: dataset.id,
title: dataset.title,
type: DEFAULT_DATA.SET_TYPES.INDEX,
meta: dataset.dataSource.meta,
} as DataStructure)
: undefined
);
const noIndexedView = i18n.translate(
'data.explorer.datasetSelector.advancedSelector.configurator.indexedView.noIndexedViewOptionLabel',
{
defaultMessage: "I don't want to use an indexed view",
}
);
const [language, setLanguage] = useState<string>(() => {
const currentLanguage = queryString.getQuery().language;
if (languages.includes(currentLanguage)) {
Expand Down Expand Up @@ -91,6 +116,40 @@ export const Configurator = ({
fetchFields();
}, [baseDataset, indexPatternsService, queryString, timeFields.length]);

useEffect(() => {
const fetchViews = async () => {
if (type?.meta.supportsIndexedViews) {
try {
const dataSourceStructure: DataStructure = {
id: dataset.dataSource?.id || '',
title: dataset.dataSource?.title || '',
type: 'DATA_SOURCE',
children: [],
};

const datasetStructure: DataStructure = {
id: dataset.id,
title: dataset.title,
type: DEFAULT_DATA.SET_TYPES.DATASET,
meta: dataset.dataSource?.meta as DataStructureMeta,
children: [],
};

const path = dataset.dataSource
? [dataSourceStructure, datasetStructure]
: [datasetStructure];

const result = await type.fetch(services, path);
setIndexedViews(result.children || []);
} catch (error) {
setIndexedViews([]);
}
}
};

fetchViews();
}, [dataset, type?.meta.supportsIndexedViews, services, type]);

return (
<>
<EuiModalHeader>
Expand Down Expand Up @@ -185,6 +244,40 @@ export const Configurator = ({
/>
</EuiFormRow>
))}
<EuiFormRow
label={i18n.translate(
'data.explorer.datasetSelector.advancedSelector.configurator.indexedViewLabel',
{
defaultMessage: 'Available indexed views',
}
)}
helpText={i18n.translate(
'data.explorer.datasetSelector.advancedSelector.configurator.indexedViewHelpText',
{
defaultMessage: 'Select an indexed view to speed up your query.',
}
)}
>
<EuiSelect
options={[
...indexedViews.map((view) => ({
text: view.title,
value: view.id,
})),
{ text: '-----', value: '-----', disabled: true },
{ text: noIndexedView, value: noIndexedView },
]}
value={indexedView?.id || '-----'}
onChange={(e) => {
const value = e.target.value === noIndexedView ? undefined : e.target.value;
if (!dataset.dataSource) return;
// if the indexed views are properly structured we can just set it directly without building it here
// see s3 type mock response how we can return the index type and with the correct id
const view = indexedViews.find((v) => v.id === value);
setIndexedView(view);
}}
/>
</EuiFormRow>
</EuiForm>
</EuiModalBody>
<EuiModalFooter>
Expand All @@ -202,8 +295,28 @@ export const Configurator = ({
</EuiButton>
<EuiButton
onClick={async () => {
await queryString.getDatasetService().cacheDataset(dataset, services);
onConfirm({ dataset, language });
let configuredDataset = { ...dataset };
if (indexedView) {
configuredDataset = {
id: indexedView.id,
title: indexedView.title,
type: DEFAULT_DATA.SET_TYPES.INDEX,
timeFieldName: dataset.timeFieldName,
dataSource: {
...dataset.dataSource!,
meta: {
...dataset.dataSource?.meta,
...indexedView.meta, // This includes the ref to original dataset
},
},
};
}

await queryString.getDatasetService().cacheDataset(configuredDataset, services);
onConfirm({
dataset: configuredDataset,
language,
});
}}
fill
disabled={submitDisabled}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ export const DatasetSelector = ({
const { overlays } = services;
const datasetService = getQueryService().queryString.getDatasetService();
const datasetIcon =
datasetService.getType(selectedDataset?.type || '')?.meta.icon.type || 'database';
datasetService.getType(
selectedDataset?.dataSource?.meta?.ref?.type || selectedDataset?.type || ''
)?.meta.icon.type || 'database';

useEffect(() => {
isMounted.current = true;
Expand Down
106 changes: 106 additions & 0 deletions src/plugins/query_enhancements/public/datasets/s3_type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import {
} from '../../common';
import S3_ICON from '../assets/s3_mark.svg';

const mockFetchIndexedViews = true;

export const s3TypeConfig: DatasetTypeConfig = {
id: DATASET.S3,
title: 'S3 Connections',
Expand All @@ -35,6 +37,7 @@ export const s3TypeConfig: DatasetTypeConfig = {
tooltip: 'Amazon S3 Connections',
searchOnLoad: true,
supportsTimeFilter: false,
supportsIndexedViews: true,
isFieldLoadAsync: true,
cacheOptions: true,
},
Expand Down Expand Up @@ -98,6 +101,18 @@ export const s3TypeConfig: DatasetTypeConfig = {
children: tables,
};
}
// Use dataset type (could be TABLE, QUERY, etc) to fetch indexed views
case 'DATASET': {
const indexedViews = !mockFetchIndexedViews
? await fetchIndexedViews(http, path)
: fetchIndexedViewsMock(dataStructure);
return {
...dataStructure,
columnHeader: 'Indexed Views',
hasNext: false,
children: indexedViews,
};
}
default: {
const dataSources = await fetchDataSources(client);
return {
Expand All @@ -114,6 +129,7 @@ export const s3TypeConfig: DatasetTypeConfig = {
dataset: Dataset,
services?: Partial<IDataPluginServices>
): Promise<DatasetField[]> => {
if (mockFetchIndexedViews) return [];
const http = services?.http;
if (!http) return [];
return await fetchFields(http, dataset);
Expand Down Expand Up @@ -294,6 +310,96 @@ const fetchTables = async (http: HttpSetup, path: DataStructure[]): Promise<Data
return fetch(http, path, 'TABLE');
};

const fetchIndexedViewsMock = (dataStructure: DataStructure): DataStructure[] => {
return [
{
id: `${dataStructure.id}.logstash-2015.09.20`,
title: 'logstash-2015.09.20',
type: DEFAULT_DATA.SET_TYPES.INDEX,
parent: dataStructure,
meta: {
type: DATA_STRUCTURE_META_TYPES.CUSTOM,
sessionId: (dataStructure.meta as DataStructureCustomMeta)?.sessionId,
name: (dataStructure.meta as DataStructureCustomMeta)?.name,
ref: {
id: dataStructure.id,
type: DATASET.S3,
title: dataStructure.title,
},
} as DataStructureCustomMeta,
},
{
id: `${dataStructure.id}.logstash-2015.09.22`,
title: 'logstash-2015.09.22',
type: DEFAULT_DATA.SET_TYPES.INDEX,
parent: dataStructure,
meta: {
type: DATA_STRUCTURE_META_TYPES.CUSTOM,
sessionId: (dataStructure.meta as DataStructureCustomMeta)?.sessionId,
name: (dataStructure.meta as DataStructureCustomMeta)?.name,
ref: {
id: dataStructure.id,
type: DATASET.S3,
title: dataStructure.title,
},
} as DataStructureCustomMeta,
},
];
};

const fetchIndexedViews = async (
http: HttpSetup,
path: DataStructure[]
): Promise<DataStructure[]> => {
const abortController = new AbortController();
const dataSource = path.find((ds) => ds.type === 'DATA_SOURCE');
const dataStructure = path[path.length - 1];
const meta = dataStructure.meta as DataStructureCustomMeta;

try {
const response = await http.fetch({
method: 'POST',
path: trimEnd(API.DATA_SOURCE.ASYNC_JOBS),
body: JSON.stringify({
lang: 'sql',
query: `SHOW MATERIALIZED VIEWS FOR ${dataStructure.title}`,
datasource: meta.name,
...(meta.sessionId && { sessionId: meta.sessionId }),
}),
query: {
id: dataSource?.id,
},
signal: abortController.signal,
});

const views = await handleQueryStatus({
fetchStatus: () =>
http.fetch({
method: 'GET',
path: trimEnd(API.DATA_SOURCE.ASYNC_JOBS),
query: {
id: dataSource?.id,
queryId: response.queryId,
},
}),
});

return views.datarows.map((view: string[]) => ({
id: `${dataStructure.id}.${view[0]}`,
title: view[0],
type: DEFAULT_DATA.SET_TYPES.INDEX,
parent: dataStructure,
meta: {
type: DATA_STRUCTURE_META_TYPES.CUSTOM,
sessionId: meta.sessionId,
name: meta.name,
} as DataStructureCustomMeta,
}));
} catch (err) {
return [];
}
};

/**
* Mapping function from S3_FIELD_TYPES to OSD_FIELD_TYPES
*
Expand Down
Loading