mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Add typings for saved_object_finder (#30067)
This commit is contained in:
parent
2de68cbfee
commit
c6ba32d3b4
4 changed files with 202 additions and 116 deletions
|
@ -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
25
src/ui/public/registry/vis_types.d.ts
vendored
Normal 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 };
|
||||
};
|
|
@ -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);
|
14
typings/@elastic/eui/index.d.ts
vendored
14
typings/@elastic/eui/index.d.ts
vendored
|
@ -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;
|
||||
}>;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue