[Canvas] Use data views service (#139610)

* Use data views service in Canvas

* Use getIndexPattern instead of title property

* Fix ts errors

* Remove console log statement
This commit is contained in:
Catherine Liu 2022-10-24 12:33:36 -07:00 committed by GitHub
parent 511f95a16a
commit 670fe25673
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 201 additions and 168 deletions

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import { lastValueFrom } from 'rxjs';
import {
ExpressionFunctionDefinition,
ExpressionValueFilter,
@ -12,10 +14,8 @@ import {
// @ts-expect-error untyped local
import { buildESRequest } from '../../../common/lib/request/build_es_request';
import { searchService } from '../../../public/services';
import { getFunctionHelp } from '../../../i18n';
import { searchService } from '../../../public/services';
interface Arguments {
index: string | null;
@ -46,6 +46,7 @@ export function escount(): ExpressionFunctionDefinition<
},
index: {
types: ['string'],
aliases: ['dataView'],
default: '_all',
help: argHelp.index,
},
@ -83,12 +84,9 @@ export function escount(): ExpressionFunctionDefinition<
},
};
return search
.search(req)
.toPromise()
.then((resp: any) => {
return resp.rawResponse.hits.total;
});
return lastValueFrom(search.search(req)).then((resp: any) => {
return resp.rawResponse.hits.total;
});
},
};
}

View file

@ -70,6 +70,7 @@ export function esdocs(): ExpressionFunctionDefinition<
},
index: {
types: ['string'],
aliases: ['dataView'],
default: '_all',
help: argHelp.index,
},

View file

@ -20,7 +20,7 @@ import {
import { getSimpleArg, setSimpleArg } from '../../../public/lib/arg_helpers';
import { ESFieldsSelect } from '../../../public/components/es_fields_select';
import { ESFieldSelect } from '../../../public/components/es_field_select';
import { ESIndexSelect } from '../../../public/components/es_index_select';
import { ESDataViewSelect } from '../../../public/components/es_data_view_select';
import { templateFromReactComponent } from '../../../public/lib/template_from_react_component';
import { DataSourceStrings, LUCENE_QUERY_URL } from '../../../i18n';
@ -29,6 +29,7 @@ const { ESDocs: strings } = DataSourceStrings;
const EsdocsDatasource = ({ args, updateArgs, defaultIndex }) => {
const setArg = useCallback(
(name, value) => {
console.log({ name, value });
updateArgs &&
updateArgs({
...args,
@ -94,7 +95,7 @@ const EsdocsDatasource = ({ args, updateArgs, defaultIndex }) => {
helpText={strings.getIndexLabel()}
display="rowCompressed"
>
<ESIndexSelect value={index} onChange={(index) => setArg('index', index)} />
<ESDataViewSelect value={index} onChange={(index) => setArg('index', index)} />
</EuiFormRow>
<EuiFormRow

View file

@ -461,11 +461,11 @@ export const DataSourceStrings = {
}),
getIndexTitle: () =>
i18n.translate('xpack.canvas.uis.dataSources.esdocs.indexTitle', {
defaultMessage: 'Index',
defaultMessage: 'Data view',
}),
getIndexLabel: () =>
i18n.translate('xpack.canvas.uis.dataSources.esdocs.indexLabel', {
defaultMessage: 'Enter an index name or select a data view',
defaultMessage: 'Select a data view or enter an index name.',
}),
getQueryTitle: () =>
i18n.translate('xpack.canvas.uis.dataSources.esdocs.queryTitle', {

View file

@ -14,6 +14,7 @@
"bfetch",
"charts",
"data",
"dataViews",
"embeddable",
"expressionError",
"expressionImage",

View file

@ -20,7 +20,7 @@ import {
import { isEqual } from 'lodash';
import { i18n } from '@kbn/i18n';
import { getDefaultIndex } from '../../lib/es_service';
import { pluginServices } from '../../services';
import { DatasourceSelector } from './datasource_selector';
import { DatasourcePreview } from './datasource_preview';
@ -67,7 +67,12 @@ export class DatasourceComponent extends PureComponent {
state = { defaultIndex: '' };
componentDidMount() {
getDefaultIndex().then((defaultIndex) => this.setState({ defaultIndex }));
pluginServices
.getServices()
.dataViews.getDefaultDataView()
.then((defaultDataView) => {
this.setState({ defaultIndex: defaultDataView.title });
});
}
componentDidUpdate(prevProps) {

View file

@ -7,37 +7,47 @@
import React, { FocusEventHandler } from 'react';
import { EuiComboBox } from '@elastic/eui';
import { DataView } from '@kbn/data-views-plugin/common';
export interface ESIndexSelectProps {
type DataViewOption = Pick<DataView, 'id' | 'name' | 'title'>;
export interface ESDataViewSelectProps {
loading: boolean;
value: string;
indices: string[];
onChange: (index: string) => void;
dataViews: DataViewOption[];
onChange: (string: string) => void;
onBlur: FocusEventHandler<HTMLDivElement> | undefined;
onFocus: FocusEventHandler<HTMLDivElement> | undefined;
}
const defaultIndex = '_all';
const defaultOption = { value: defaultIndex, label: defaultIndex };
export const ESIndexSelect: React.FunctionComponent<ESIndexSelectProps> = ({
export const ESDataViewSelect: React.FunctionComponent<ESDataViewSelectProps> = ({
value = defaultIndex,
loading,
indices,
dataViews,
onChange,
onFocus,
onBlur,
}) => {
const selectedOption = value !== defaultIndex ? [{ label: value }] : [];
const options = indices.map((index) => ({ label: index }));
const selectedDataView = dataViews.find((view) => value === view.title) as DataView;
const selectedOption = selectedDataView
? { value: selectedDataView.title, label: selectedDataView.name }
: { value, label: value };
const options = dataViews.map(({ name, title }) => ({ value: title, label: name }));
return (
<EuiComboBox
selectedOptions={selectedOption}
onChange={([index]) => onChange(index?.label ?? defaultIndex)}
selectedOptions={[selectedOption]}
onChange={([view]) => {
onChange(view.value || defaultOption.value);
}}
onSearchChange={(searchValue) => {
// resets input when user starts typing
if (searchValue) {
onChange(defaultIndex);
onChange(defaultOption.value);
}
}}
onBlur={onBlur}
@ -46,7 +56,7 @@ export const ESIndexSelect: React.FunctionComponent<ESIndexSelectProps> = ({
options={options}
singleSelection={{ asPlainText: true }}
isClearable={false}
onCreateOption={(input) => onChange(input || defaultIndex)}
onCreateOption={(input) => onChange(input || defaultOption.value)}
compressed
/>
);

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { DataView } from '@kbn/data-views-plugin/common';
import { sortBy } from 'lodash';
import React, { FC, useRef, useState } from 'react';
import useEffectOnce from 'react-use/lib/useEffectOnce';
import { useDataViewsService } from '../../services';
import {
ESDataViewSelect as Component,
ESDataViewSelectProps as Props,
} from './es_data_view_select.component';
type ESDataViewSelectProps = Omit<Props, 'indices' | 'loading'>;
export const ESDataViewSelect: FC<ESDataViewSelectProps> = (props) => {
const { value, onChange } = props;
const [dataViews, setDataViews] = useState<Array<Pick<DataView, 'id' | 'name' | 'title'>>>([]);
const [loading, setLoading] = useState<boolean>(true);
const mounted = useRef(true);
const { getDataViews } = useDataViewsService();
useEffectOnce(() => {
getDataViews().then((newDataViews) => {
if (!mounted.current) {
return;
}
if (!newDataViews) {
newDataViews = [];
}
setLoading(false);
setDataViews(sortBy(newDataViews, 'name'));
if (!value && newDataViews.length) {
onChange(newDataViews[0].title);
}
});
return () => {
mounted.current = false;
};
});
return <Component {...props} dataViews={dataViews} loading={loading} />;
};

View file

@ -5,5 +5,5 @@
* 2.0.
*/
export { ESIndexSelect } from './es_index_select';
export { ESIndexSelect as ESIndexSelectComponent } from './es_index_select.component';
export { ESDataViewSelect } from './es_data_view_select';
export { ESDataViewSelect as ESDataViewSelectComponent } from './es_data_view_select.component';

View file

@ -6,7 +6,7 @@
*/
import React, { useState, useEffect, useRef } from 'react';
import { getFields } from '../../lib/es_service';
import { useDataViewsService } from '../../services';
import { ESFieldSelect as Component, ESFieldSelectProps as Props } from './es_field_select';
type ESFieldSelectProps = Omit<Props, 'fields'>;
@ -15,15 +15,17 @@ export const ESFieldSelect: React.FunctionComponent<ESFieldSelectProps> = (props
const { index, value, onChange } = props;
const [fields, setFields] = useState<string[]>([]);
const loadingFields = useRef(false);
const { getFields } = useDataViewsService();
useEffect(() => {
loadingFields.current = true;
getFields(index)
.then((newFields) => setFields(newFields || []))
.finally(() => {
loadingFields.current = false;
});
}, [index]);
}, [index, getFields]);
useEffect(() => {
if (!loadingFields.current && value && !fields.includes(value)) {

View file

@ -8,7 +8,7 @@
import React, { useState, useEffect, useRef } from 'react';
import { isEqual } from 'lodash';
import usePrevious from 'react-use/lib/usePrevious';
import { getFields } from '../../lib/es_service';
import { useDataViewsService } from '../../services';
import {
ESFieldsSelect as Component,
ESFieldsSelectProps as Props,
@ -21,6 +21,7 @@ export const ESFieldsSelect: React.FunctionComponent<ESFieldsSelectProps> = (pro
const [fields, setFields] = useState<string[]>([]);
const prevIndex = usePrevious(index);
const mounted = useRef(true);
const { getFields } = useDataViewsService();
useEffect(() => {
if (prevIndex !== index) {
@ -36,7 +37,7 @@ export const ESFieldsSelect: React.FunctionComponent<ESFieldsSelectProps> = (pro
}
});
}
}, [fields, index, onChange, prevIndex, selected]);
}, [fields, index, onChange, prevIndex, selected, getFields]);
useEffect(
() => () => {

View file

@ -1,48 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useRef, useState } from 'react';
import useEffectOnce from 'react-use/lib/useEffectOnce';
import { getIndices } from '../../lib/es_service';
import {
ESIndexSelect as Component,
ESIndexSelectProps as Props,
} from './es_index_select.component';
type ESIndexSelectProps = Omit<Props, 'indices' | 'loading'>;
export const ESIndexSelect: React.FunctionComponent<ESIndexSelectProps> = (props) => {
const { value, onChange } = props;
const [indices, setIndices] = useState<string[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const mounted = useRef(true);
useEffectOnce(() => {
getIndices().then((newIndices) => {
if (!mounted.current) {
return;
}
if (!newIndices) {
newIndices = [];
}
setLoading(false);
setIndices(newIndices.sort());
if (!value && newIndices.length) {
onChange(newIndices[0]);
}
});
return () => {
mounted.current = false;
};
});
return <Component {...props} indices={indices} loading={loading} />;
};

View file

@ -1,79 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
// TODO - clint: convert to service abstraction
import { API_ROUTE } from '../../common/lib/constants';
import { fetch } from '../../common/lib/fetch';
import { ErrorStrings } from '../../i18n';
import { pluginServices } from '../services';
const { esService: strings } = ErrorStrings;
const getApiPath = function () {
const platformService = pluginServices.getServices().platform;
const basePath = platformService.getBasePath();
return basePath + API_ROUTE;
};
const getSavedObjectsClient = function () {
const platformService = pluginServices.getServices().platform;
return platformService.getSavedObjectsClient();
};
const getAdvancedSettings = function () {
const platformService = pluginServices.getServices().platform;
return platformService.getUISettings();
};
export const getFields = (index = '_all') => {
return fetch
.get(`${getApiPath()}/es_fields?index=${index}`)
.then(({ data: mapping }: { data: object }) =>
Object.keys(mapping)
.filter((field) => !field.startsWith('_')) // filters out meta fields
.sort()
)
.catch((err: Error) => {
const notifyService = pluginServices.getServices().notify;
notifyService.error(err, {
title: strings.getFieldsFetchErrorMessage(index),
});
});
};
export const getIndices = () =>
getSavedObjectsClient()
.find<{ title: string }>({
type: 'index-pattern',
fields: ['title'],
searchFields: ['title'],
perPage: 1000,
})
.then((resp) => {
return resp.savedObjects.map((savedObject) => {
return savedObject.attributes.title;
});
})
.catch((err: Error) => {
const notifyService = pluginServices.getServices().notify;
notifyService.error(err, { title: strings.getIndicesFetchErrorMessage() });
});
export const getDefaultIndex = () => {
const defaultIndexId = getAdvancedSettings().get('defaultIndex');
return defaultIndexId
? getSavedObjectsClient()
.get<{ title: string }>('index-pattern', defaultIndexId)
.then((defaultIndex) => defaultIndex.attributes.title)
.catch((err) => {
const notifyService = pluginServices.getServices().notify;
notifyService.error(err, { title: strings.getDefaultIndexFetchErrorMessage() });
})
: Promise.resolve('');
};

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { DataView } from '@kbn/data-views-plugin/common';
export interface CanvasDataViewsService {
getFields: (index: string) => Promise<string[]>;
getDataViews: () => Promise<Array<Pick<DataView, 'id' | 'name' | 'title'>>>;
getDefaultDataView: () => Promise<Pick<DataView, 'id' | 'name' | 'title'> | undefined>;
}

View file

@ -10,6 +10,7 @@ export * from './legacy';
import { PluginServices } from '@kbn/presentation-util-plugin/public';
import { CanvasCustomElementService } from './custom_element';
import { CanvasDataViewsService } from './data_views';
import { CanvasEmbeddablesService } from './embeddables';
import { CanvasExpressionsService } from './expressions';
import { CanvasFiltersService } from './filters';
@ -23,6 +24,7 @@ import { CanvasWorkpadService } from './workpad';
export interface CanvasPluginServices {
customElement: CanvasCustomElementService;
dataViews: CanvasDataViewsService;
embeddables: CanvasEmbeddablesService;
expressions: CanvasExpressionsService;
filters: CanvasFiltersService;
@ -39,6 +41,7 @@ export const pluginServices = new PluginServices<CanvasPluginServices>();
export const useCustomElementService = () =>
(() => pluginServices.getHooks().customElement.useService())();
export const useDataViewsService = () => (() => pluginServices.getHooks().dataViews.useService())();
export const useEmbeddablesService = () =>
(() => pluginServices.getHooks().embeddables.useService())();
export const useExpressionsService = () =>

View file

@ -0,0 +1,50 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { DataView } from '@kbn/data-views-plugin/public';
import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public';
import { ErrorStrings } from '../../../i18n';
import { CanvasStartDeps } from '../../plugin';
import { CanvasDataViewsService } from '../data_views';
import { CanvasNotifyService } from '../notify';
const { esService: strings } = ErrorStrings;
export type DataViewsServiceFactory = KibanaPluginServiceFactory<
CanvasDataViewsService,
CanvasStartDeps,
{
notify: CanvasNotifyService;
}
>;
export const dataViewsServiceFactory: DataViewsServiceFactory = ({ startPlugins }, { notify }) => ({
getDataViews: async () => {
try {
const dataViews = await startPlugins.data.dataViews.getIdsWithTitle();
return dataViews.map(({ id, name, title }) => ({ id, name, title } as DataView));
} catch (e) {
notify.error(e, { title: strings.getIndicesFetchErrorMessage() });
}
return [];
},
getFields: async (dataViewTitle: string) => {
const dataView = await startPlugins.data.dataViews.create({ title: dataViewTitle });
return dataView.fields
.filter((field) => !field.name.startsWith('_'))
.map((field) => field.name);
},
getDefaultDataView: async () => {
const dataView = await startPlugins.data.dataViews.getDefaultDataView();
return dataView
? { id: dataView.id, name: dataView.name, title: dataView.getIndexPattern() }
: undefined;
},
});

View file

@ -15,6 +15,7 @@ import {
import { CanvasPluginServices } from '..';
import { CanvasStartDeps } from '../../plugin';
import { customElementServiceFactory } from './custom_element';
import { dataViewsServiceFactory } from './data_views';
import { embeddablesServiceFactory } from './embeddables';
import { expressionsServiceFactory } from './expressions';
import { labsServiceFactory } from './labs';
@ -27,6 +28,7 @@ import { workpadServiceFactory } from './workpad';
import { filtersServiceFactory } from './filters';
export { customElementServiceFactory } from './custom_element';
export { dataViewsServiceFactory } from './data_views';
export { embeddablesServiceFactory } from './embeddables';
export { expressionsServiceFactory } from './expressions';
export { filtersServiceFactory } from './filters';
@ -42,6 +44,7 @@ export const pluginServiceProviders: PluginServiceProviders<
KibanaPluginServiceParams<CanvasStartDeps>
> = {
customElement: new PluginServiceProvider(customElementServiceFactory),
dataViews: new PluginServiceProvider(dataViewsServiceFactory, ['notify']),
embeddables: new PluginServiceProvider(embeddablesServiceFactory),
expressions: new PluginServiceProvider(expressionsServiceFactory, ['filters', 'notify']),
filters: new PluginServiceProvider(filtersServiceFactory),

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public';
import { CanvasDataViewsService } from '../data_views';
type DataViewsServiceFactory = PluginServiceFactory<CanvasDataViewsService>;
export const dataViewsServiceFactory: DataViewsServiceFactory = () => ({
getDataViews: () =>
Promise.resolve([
{ id: 'dataview1', title: 'dataview1', name: 'Data view 1' },
{ id: 'dataview2', title: 'dataview2', name: 'Data view 2' },
]),
getFields: () => Promise.resolve(['field1', 'field2']),
getDefaultDataView: () =>
Promise.resolve({
id: 'defaultDataViewId',
title: 'defaultDataView',
name: 'Default data view',
}),
});

View file

@ -15,6 +15,7 @@ import {
import { CanvasPluginServices } from '..';
import { customElementServiceFactory } from './custom_element';
import { dataViewsServiceFactory } from './data_views';
import { embeddablesServiceFactory } from './embeddables';
import { expressionsServiceFactory } from './expressions';
import { labsServiceFactory } from './labs';
@ -27,6 +28,7 @@ import { workpadServiceFactory } from './workpad';
import { filtersServiceFactory } from './filters';
export { customElementServiceFactory } from './custom_element';
export { dataViewsServiceFactory } from './data_views';
export { expressionsServiceFactory } from './expressions';
export { filtersServiceFactory } from './filters';
export { labsServiceFactory } from './labs';
@ -39,6 +41,7 @@ export { workpadServiceFactory } from './workpad';
export const pluginServiceProviders: PluginServiceProviders<CanvasPluginServices> = {
customElement: new PluginServiceProvider(customElementServiceFactory),
dataViews: new PluginServiceProvider(dataViewsServiceFactory),
embeddables: new PluginServiceProvider(embeddablesServiceFactory),
expressions: new PluginServiceProvider(expressionsServiceFactory, ['filters', 'notify']),
filters: new PluginServiceProvider(filtersServiceFactory),

View file

@ -56,10 +56,6 @@ export const canvasWebpack = {
resolve: {
alias: {
'src/plugins': resolve(KIBANA_ROOT, 'src/plugins'),
'../../lib/es_service': resolve(
KIBANA_ROOT,
'x-pack/plugins/canvas/storybook/__mocks__/es_service.ts'
),
},
},
};

View file

@ -85,11 +85,6 @@ if (!fs.existsSync(cssDir)) {
fs.mkdirSync(cssDir, { recursive: true });
}
// Mock index for datasource stories
jest.mock('../public/lib/es_service', () => ({
getDefaultIndex: () => Promise.resolve('test index'),
}));
addSerializer(styleSheetSerializer);
const emotionSerializer = createSerializer({