mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
parent
c12d9e6a5d
commit
8038aa1aee
4 changed files with 202 additions and 116 deletions
|
@ -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
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 _ 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);
|
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
|
* 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;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue