Add typings for saved_object_finder (#30067) (#30621)

This commit is contained in:
Joe Reuter 2019-02-11 11:01:56 +01:00 committed by GitHub
parent c12d9e6a5d
commit 8038aa1aee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 202 additions and 116 deletions

View file

@ -50,6 +50,7 @@ export interface FindOptions extends BaseOptions {
search?: string; search?: string;
searchFields?: string[]; searchFields?: string[];
hasReference?: { type: string; id: string }; hasReference?: { type: string; id: string };
defaultSearchOperator?: 'AND' | 'OR';
} }
export interface FindResponse<T extends SavedObjectAttributes = any> { export interface FindResponse<T extends SavedObjectAttributes = any> {

25
src/ui/public/registry/vis_types.d.ts vendored Normal file
View file

@ -0,0 +1,25 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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
*
* http://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.
*/
import { VisType } from '../vis';
import { UIRegistry } from './_registry';
declare type VisTypesRegistryProvider = UIRegistry<VisType> & {
byName: { [typeName: string]: VisType };
};

View file

@ -18,22 +18,115 @@
*/ */
import _ from 'lodash'; import _ from 'lodash';
import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react';
import { InjectedIntlProps } from 'react-intl';
import chrome from 'ui/chrome'; import chrome from 'ui/chrome';
import { import {
EuiFieldSearch,
EuiBasicTable, EuiBasicTable,
EuiLink, EuiFieldSearch,
EuiFlexGroup, EuiFlexGroup,
EuiFlexItem, EuiFlexItem,
EuiLink,
EuiTableCriteria,
} from '@elastic/eui'; } from '@elastic/eui';
import { Direction } from '@elastic/eui/src/services/sort/sort_direction';
import { injectI18n } from '@kbn/i18n/react'; import { injectI18n } from '@kbn/i18n/react';
class SavedObjectFinderUI extends React.Component { import { SavedObjectAttributes } from '../../../../server/saved_objects';
constructor(props) { import { VisTypesRegistryProvider } from '../../registry/vis_types';
import { SavedObject } from '../saved_object';
interface SavedObjectFinderUIState {
items: Array<{
title: string | null;
id: SavedObject<SavedObjectAttributes>['id'];
type: SavedObject<SavedObjectAttributes>['type'];
}>;
filter: string;
isFetchingItems: boolean;
page: number;
perPage: number;
sortField?: string;
sortDirection?: Direction;
}
interface SavedObjectFinderUIProps extends InjectedIntlProps {
callToActionButton?: React.ReactNode;
onChoose?: (
id: SavedObject<SavedObjectAttributes>['id'],
type: SavedObject<SavedObjectAttributes>['type']
) => void;
makeUrl?: (id: SavedObject<SavedObjectAttributes>['id']) => void;
noItemsMessage?: React.ReactNode;
savedObjectType: 'visualization' | 'search';
visTypes?: VisTypesRegistryProvider;
}
class SavedObjectFinderUI extends React.Component<
SavedObjectFinderUIProps,
SavedObjectFinderUIState
> {
public static propTypes = {
callToActionButton: PropTypes.node,
onChoose: PropTypes.func,
makeUrl: PropTypes.func,
noItemsMessage: PropTypes.node,
savedObjectType: PropTypes.oneOf(['visualization', 'search']).isRequired,
visTypes: PropTypes.object,
};
private isComponentMounted: boolean = false;
private debouncedFetch = _.debounce(async (filter: string) => {
const resp = await chrome.getSavedObjectsClient().find({
type: this.props.savedObjectType,
fields: ['title', 'visState'],
search: filter ? `${filter}*` : undefined,
page: 1,
perPage: chrome.getUiSettingsClient().get('savedObjects:listingLimit'),
searchFields: ['title^3', 'description'],
defaultSearchOperator: 'AND',
});
const { savedObjectType, visTypes } = this.props;
if (
savedObjectType === 'visualization' &&
!chrome.getUiSettingsClient().get('visualize:enableLabs') &&
visTypes
) {
resp.savedObjects = resp.savedObjects.filter(savedObject => {
if (typeof savedObject.attributes.visState !== 'string') {
return false;
}
const typeName: string = JSON.parse(savedObject.attributes.visState).type;
const visType = visTypes.byName[typeName];
return visType.stage !== 'experimental';
});
}
if (!this.isComponentMounted) {
return;
}
// We need this check to handle the case where search results come back in a different
// order than they were sent out. Only load results for the most recent search.
if (filter === this.state.filter) {
this.setState({
isFetchingItems: false,
items: resp.savedObjects.map(({ attributes: { title }, id, type }) => {
return {
title: typeof title === 'string' ? title : '',
id,
type,
};
}),
});
}
}, 300);
constructor(props: SavedObjectFinderUIProps) {
super(props); super(props);
this.state = { this.state = {
@ -45,29 +138,38 @@ class SavedObjectFinderUI extends React.Component {
}; };
} }
componentWillUnmount() { public componentWillUnmount() {
this._isMounted = false; this.isComponentMounted = false;
this.debouncedFetch.cancel(); this.debouncedFetch.cancel();
} }
componentDidMount() { public componentDidMount() {
this._isMounted = true; this.isComponentMounted = true;
this.fetchItems(); this.fetchItems();
} }
onTableChange = ({ page, sort = {} }) => { public render() {
let { return (
field: sortField, <React.Fragment>
direction: sortDirection, {this.renderSearchBar()}
} = sort; {this.renderTable()}
</React.Fragment>
);
}
private onTableChange = ({ page, sort = {} }: EuiTableCriteria) => {
let sortField: string | undefined = sort.field;
let sortDirection: Direction | undefined = sort.direction;
// 3rd sorting state that is not captured by sort - native order (no sort) // 3rd sorting state that is not captured by sort - native order (no sort)
// when switching from desc to asc for the same field - use native order // when switching from desc to asc for the same field - use native order
if (this.state.sortField === sortField if (
&& this.state.sortDirection === 'desc' this.state.sortField === sortField &&
&& sortDirection === 'asc') { this.state.sortDirection === 'desc' &&
sortField = null; sortDirection === 'asc'
sortDirection = null; ) {
sortField = undefined;
sortDirection = undefined;
} }
this.setState({ this.setState({
@ -76,21 +178,22 @@ class SavedObjectFinderUI extends React.Component {
sortField, sortField,
sortDirection, sortDirection,
}); });
} };
// server-side paging not supported // server-side paging not supported
// 1) saved object client does not support sorting by title because title is only mapped as analyzed // 1) saved object client does not support sorting by title because title is only mapped as analyzed
// 2) can not search on anything other than title because all other fields are stored in opaque JSON strings, // 2) can not search on anything other than title because all other fields are stored in opaque JSON strings,
// for example, visualizations need to be search by isLab but this is not possible in Elasticsearch side // for example, visualizations need to be search by isLab but this is not possible in Elasticsearch side
// with the current mappings // with the current mappings
getPageOfItems = () => { private getPageOfItems = () => {
// do not sort original list to preserve elasticsearch ranking order // do not sort original list to preserve elasticsearch ranking order
const items = this.state.items.slice(); const items = this.state.items.slice();
const { sortField } = this.state;
if (this.state.sortField) { if (sortField) {
items.sort((a, b) => { items.sort((a, b) => {
const fieldA = _.get(a, this.state.sortField, ''); const fieldA = _.get(a, sortField, '');
const fieldB = _.get(b, this.state.sortField, ''); const fieldB = _.get(b, sortField, '');
let order = 1; let order = 1;
if (this.state.sortDirection === 'desc') { if (this.state.sortDirection === 'desc') {
order = -1; order = -1;
@ -104,65 +207,18 @@ class SavedObjectFinderUI extends React.Component {
// If end is greater than the length of the sequence, slice extracts through to the end of the sequence (arr.length). // If end is greater than the length of the sequence, slice extracts through to the end of the sequence (arr.length).
const lastIndex = startIndex + this.state.perPage; const lastIndex = startIndex + this.state.perPage;
return items.slice(startIndex, lastIndex); return items.slice(startIndex, lastIndex);
} };
debouncedFetch = _.debounce(async (filter) => { private fetchItems = () => {
const resp = await chrome.getSavedObjectsClient().find({ this.setState(
type: this.props.savedObjectType, {
fields: ['title', 'visState'], isFetchingItems: true,
search: filter ? `${filter}*` : undefined, },
page: 1, this.debouncedFetch.bind(null, this.state.filter)
perPage: chrome.getUiSettingsClient().get('savedObjects:listingLimit'), );
searchFields: ['title^3', 'description'], };
defaultSearchOperator: 'AND',
});
if (this.props.savedObjectType === 'visualization'
&& !chrome.getUiSettingsClient().get('visualize:enableLabs')
&& this.props.visTypes) {
resp.savedObjects = resp.savedObjects.filter(savedObject => {
const typeName = JSON.parse(savedObject.attributes.visState).type;
const visType = this.props.visTypes.byName[typeName];
return visType.stage !== 'experimental';
});
}
if (!this._isMounted) {
return;
}
// We need this check to handle the case where search results come back in a different
// order than they were sent out. Only load results for the most recent search.
if (filter === this.state.filter) {
this.setState({
isFetchingItems: false,
items: resp.savedObjects.map(savedObject => {
return {
title: savedObject.attributes.title,
id: savedObject.id,
type: savedObject.type,
};
}),
});
}
}, 300);
fetchItems = () => {
this.setState({
isFetchingItems: true,
}, this.debouncedFetch.bind(null, this.state.filter));
}
renderSearchBar() {
let actionBtn;
if (this.props.callToActionButton) {
actionBtn = (
<EuiFlexItem grow={false}>
{this.props.callToActionButton}
</EuiFlexItem>
);
}
private renderSearchBar() {
return ( return (
<EuiFlexGroup> <EuiFlexGroup>
<EuiFlexItem grow={true}> <EuiFlexItem grow={true}>
@ -173,29 +229,34 @@ class SavedObjectFinderUI extends React.Component {
})} })}
fullWidth fullWidth
value={this.state.filter} value={this.state.filter}
onChange={(e) => { onChange={e => {
this.setState({ this.setState(
filter: e.target.value {
}, this.fetchItems); filter: e.target.value,
},
this.fetchItems
);
}} }}
data-test-subj="savedObjectFinderSearchInput" data-test-subj="savedObjectFinderSearchInput"
/> />
</EuiFlexItem> </EuiFlexItem>
{actionBtn} {this.props.callToActionButton && (
<EuiFlexItem grow={false}>{this.props.callToActionButton}</EuiFlexItem>
)}
</EuiFlexGroup> </EuiFlexGroup>
); );
} }
renderTable() { private renderTable() {
const pagination = { const pagination = {
pageIndex: this.state.page, pageIndex: this.state.page,
pageSize: this.state.perPage, pageSize: this.state.perPage,
totalItemCount: this.state.items.length, totalItemCount: this.state.items.length,
pageSizeOptions: [5, 10], pageSizeOptions: [5, 10],
}; };
const sorting = {}; // TODO there should be a Type in EUI for that, replace if it exists
const sorting: { sort?: EuiTableCriteria['sort'] } = {};
if (this.state.sortField) { if (this.state.sortField) {
sorting.sort = { sorting.sort = {
field: this.state.sortField, field: this.state.sortField,
@ -210,11 +271,8 @@ class SavedObjectFinderUI extends React.Component {
defaultMessage: 'Title', defaultMessage: 'Title',
}), }),
sortable: true, sortable: true,
render: (title, record) => { render: (title: string, record: SavedObject<SavedObjectAttributes>) => {
const { const { onChoose, makeUrl } = this.props;
onChoose,
makeUrl
} = this.props;
if (!onChoose && !makeUrl) { if (!onChoose && !makeUrl) {
return <span>{title}</span>; return <span>{title}</span>;
@ -222,15 +280,21 @@ class SavedObjectFinderUI extends React.Component {
return ( return (
<EuiLink <EuiLink
onClick={onChoose ? () => { onChoose(record.id, record.type); } : undefined} onClick={
onChoose
? () => {
onChoose(record.id, record.type);
}
: undefined
}
href={makeUrl ? makeUrl(record.id) : undefined} href={makeUrl ? makeUrl(record.id) : undefined}
data-test-subj={`savedObjectTitle${title.split(' ').join('-')}`} data-test-subj={`savedObjectTitle${title.split(' ').join('-')}`}
> >
{title} {title}
</EuiLink> </EuiLink>
); );
} },
} },
]; ];
const items = this.state.items.length === 0 ? [] : this.getPageOfItems(); const items = this.state.items.length === 0 ? [] : this.getPageOfItems();
return ( return (
@ -245,24 +309,6 @@ class SavedObjectFinderUI extends React.Component {
/> />
); );
} }
render() {
return (
<React.Fragment>
{this.renderSearchBar()}
{this.renderTable()}
</React.Fragment>
);
}
} }
SavedObjectFinderUI.propTypes = {
callToActionButton: PropTypes.node,
onChoose: PropTypes.func,
makeUrl: PropTypes.func,
noItemsMessage: PropTypes.node,
savedObjectType: PropTypes.oneOf(['visualization', 'search']).isRequired,
visTypes: PropTypes.object,
};
export const SavedObjectFinder = injectI18n(SavedObjectFinderUI); export const SavedObjectFinder = injectI18n(SavedObjectFinderUI);

View file

@ -16,6 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { Direction } from '@elastic/eui/src/services/sort/sort_direction';
// TODO: Remove once typescript definitions are in EUI // TODO: Remove once typescript definitions are in EUI
@ -24,4 +25,17 @@ declare module '@elastic/eui' {
export const EuiCopy: React.SFC<any>; export const EuiCopy: React.SFC<any>;
export const EuiOutsideClickDetector: React.SFC<any>; export const EuiOutsideClickDetector: React.SFC<any>;
export const EuiSideNav: React.SFC<any>; export const EuiSideNav: React.SFC<any>;
export interface EuiTableCriteria {
page: { index: number; size: number };
sort?: {
field?: string;
direction?: Direction;
};
}
export const EuiBasicTable: React.ComponentClass<{
onTableChange?: (criteria: EuiTableCriteria) => void;
sorting: { sort?: EuiTableCriteria['sort'] };
[key: string]: any;
}>;
} }