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;
searchFields?: string[];
hasReference?: { type: string; id: string };
defaultSearchOperator?: 'AND' | 'OR';
}
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 React from 'react';
import PropTypes from 'prop-types';
import React from 'react';
import { InjectedIntlProps } from 'react-intl';
import chrome from 'ui/chrome';
import {
EuiFieldSearch,
EuiBasicTable,
EuiLink,
EuiFieldSearch,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiTableCriteria,
} from '@elastic/eui';
import { Direction } from '@elastic/eui/src/services/sort/sort_direction';
import { injectI18n } from '@kbn/i18n/react';
class SavedObjectFinderUI extends React.Component {
constructor(props) {
import { SavedObjectAttributes } from '../../../../server/saved_objects';
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);
this.state = {
@ -45,29 +138,38 @@ class SavedObjectFinderUI extends React.Component {
};
}
componentWillUnmount() {
this._isMounted = false;
public componentWillUnmount() {
this.isComponentMounted = false;
this.debouncedFetch.cancel();
}
componentDidMount() {
this._isMounted = true;
public componentDidMount() {
this.isComponentMounted = true;
this.fetchItems();
}
onTableChange = ({ page, sort = {} }) => {
let {
field: sortField,
direction: sortDirection,
} = sort;
public render() {
return (
<React.Fragment>
{this.renderSearchBar()}
{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)
// when switching from desc to asc for the same field - use native order
if (this.state.sortField === sortField
&& this.state.sortDirection === 'desc'
&& sortDirection === 'asc') {
sortField = null;
sortDirection = null;
if (
this.state.sortField === sortField &&
this.state.sortDirection === 'desc' &&
sortDirection === 'asc'
) {
sortField = undefined;
sortDirection = undefined;
}
this.setState({
@ -76,21 +178,22 @@ class SavedObjectFinderUI extends React.Component {
sortField,
sortDirection,
});
}
};
// server-side paging not supported
// 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,
// for example, visualizations need to be search by isLab but this is not possible in Elasticsearch side
// with the current mappings
getPageOfItems = () => {
private getPageOfItems = () => {
// do not sort original list to preserve elasticsearch ranking order
const items = this.state.items.slice();
const { sortField } = this.state;
if (this.state.sortField) {
if (sortField) {
items.sort((a, b) => {
const fieldA = _.get(a, this.state.sortField, '');
const fieldB = _.get(b, this.state.sortField, '');
const fieldA = _.get(a, sortField, '');
const fieldB = _.get(b, sortField, '');
let order = 1;
if (this.state.sortDirection === 'desc') {
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).
const lastIndex = startIndex + this.state.perPage;
return items.slice(startIndex, lastIndex);
}
};
debouncedFetch = _.debounce(async (filter) => {
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',
});
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 fetchItems = () => {
this.setState(
{
isFetchingItems: true,
},
this.debouncedFetch.bind(null, this.state.filter)
);
};
private renderSearchBar() {
return (
<EuiFlexGroup>
<EuiFlexItem grow={true}>
@ -173,29 +229,34 @@ class SavedObjectFinderUI extends React.Component {
})}
fullWidth
value={this.state.filter}
onChange={(e) => {
this.setState({
filter: e.target.value
}, this.fetchItems);
onChange={e => {
this.setState(
{
filter: e.target.value,
},
this.fetchItems
);
}}
data-test-subj="savedObjectFinderSearchInput"
/>
</EuiFlexItem>
{actionBtn}
{this.props.callToActionButton && (
<EuiFlexItem grow={false}>{this.props.callToActionButton}</EuiFlexItem>
)}
</EuiFlexGroup>
);
}
renderTable() {
private renderTable() {
const pagination = {
pageIndex: this.state.page,
pageSize: this.state.perPage,
totalItemCount: this.state.items.length,
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) {
sorting.sort = {
field: this.state.sortField,
@ -210,11 +271,8 @@ class SavedObjectFinderUI extends React.Component {
defaultMessage: 'Title',
}),
sortable: true,
render: (title, record) => {
const {
onChoose,
makeUrl
} = this.props;
render: (title: string, record: SavedObject<SavedObjectAttributes>) => {
const { onChoose, makeUrl } = this.props;
if (!onChoose && !makeUrl) {
return <span>{title}</span>;
@ -222,15 +280,21 @@ class SavedObjectFinderUI extends React.Component {
return (
<EuiLink
onClick={onChoose ? () => { onChoose(record.id, record.type); } : undefined}
onClick={
onChoose
? () => {
onChoose(record.id, record.type);
}
: undefined
}
href={makeUrl ? makeUrl(record.id) : undefined}
data-test-subj={`savedObjectTitle${title.split(' ').join('-')}`}
>
{title}
</EuiLink>
);
}
}
},
},
];
const items = this.state.items.length === 0 ? [] : this.getPageOfItems();
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);

View file

@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Direction } from '@elastic/eui/src/services/sort/sort_direction';
// TODO: Remove once typescript definitions are in EUI
@ -24,4 +25,17 @@ declare module '@elastic/eui' {
export const EuiCopy: React.SFC<any>;
export const EuiOutsideClickDetector: 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;
}>;
}