From 1d14b1fa5b0bc74212494079ecff0c6d0d2b1a7b Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Mon, 4 Feb 2019 14:48:43 +0100 Subject: [PATCH 01/35] WIP typings for saved object client --- src/server/saved_objects/index.d.ts | 4 + src/server/saved_objects/service/index.d.ts | 9 +- .../service/saved_objects_client.d.ts | 7 + ...te_index.js => error_auto_create_index.ts} | 20 +- .../{index.js => index.ts} | 0 src/ui/public/saved_objects/saved_object.js | 67 ---- src/ui/public/saved_objects/saved_object.ts | 81 ++++ .../saved_objects/saved_objects_client.js | 260 ------------- .../saved_objects/saved_objects_client.ts | 352 ++++++++++++++++++ 9 files changed, 464 insertions(+), 336 deletions(-) rename src/ui/public/error_auto_create_index/{error_auto_create_index.js => error_auto_create_index.ts} (73%) rename src/ui/public/error_auto_create_index/{index.js => index.ts} (100%) delete mode 100644 src/ui/public/saved_objects/saved_object.js create mode 100644 src/ui/public/saved_objects/saved_object.ts delete mode 100644 src/ui/public/saved_objects/saved_objects_client.js create mode 100644 src/ui/public/saved_objects/saved_objects_client.ts diff --git a/src/server/saved_objects/index.d.ts b/src/server/saved_objects/index.d.ts index 99b6af3c787aa..c62cad7a6d12b 100644 --- a/src/server/saved_objects/index.d.ts +++ b/src/server/saved_objects/index.d.ts @@ -18,8 +18,12 @@ */ export { + FindOptions, + MigrationVersion, SavedObject, + SavedObjectAttributes, SavedObjectsClient, SavedObjectsClientWrapperFactory, + SavedObjectReference, SavedObjectsService, } from './service'; diff --git a/src/server/saved_objects/service/index.d.ts b/src/server/saved_objects/service/index.d.ts index dc6b2c545ca73..550c601e3e432 100644 --- a/src/server/saved_objects/service/index.d.ts +++ b/src/server/saved_objects/service/index.d.ts @@ -19,4 +19,11 @@ export { SavedObjectsService } from './create_saved_objects_service'; export { SavedObjectsClientWrapperFactory } from './lib'; -export { SavedObject, SavedObjectsClient } from './saved_objects_client'; +export { + FindOptions, + MigrationVersion, + SavedObject, + SavedObjectAttributes, + SavedObjectsClient, + SavedObjectReference, +} from './saved_objects_client'; diff --git a/src/server/saved_objects/service/saved_objects_client.d.ts b/src/server/saved_objects/service/saved_objects_client.d.ts index a58cec023b335..c9fcf76f0d3ac 100644 --- a/src/server/saved_objects/service/saved_objects_client.d.ts +++ b/src/server/saved_objects/service/saved_objects_client.d.ts @@ -26,6 +26,7 @@ export interface BaseOptions { export interface CreateOptions extends BaseOptions { id?: string; override?: boolean; + references?: SavedObjectReference[]; } export interface BulkCreateObject { @@ -48,6 +49,7 @@ export interface FindOptions extends BaseOptions { fields?: string[]; search?: string; searchFields?: string[]; + hasReference?: { type: string; id: string }; } export interface FindResponse { @@ -71,6 +73,10 @@ export interface BulkGetResponse { saved_objects: Array>; } +export interface MigrationVersion { + [pluginName: string]: string; +} + export interface SavedObjectAttributes { [key: string]: SavedObjectAttributes | string | number | boolean | null; } @@ -85,6 +91,7 @@ export interface SavedObject { }; attributes: T; references: SavedObjectReference[]; + migrationVersion?: MigrationVersion; } export interface SavedObjectReference { diff --git a/src/ui/public/error_auto_create_index/error_auto_create_index.js b/src/ui/public/error_auto_create_index/error_auto_create_index.ts similarity index 73% rename from src/ui/public/error_auto_create_index/error_auto_create_index.js rename to src/ui/public/error_auto_create_index/error_auto_create_index.ts index 4c213d096b020..e5287f18e1f7a 100644 --- a/src/ui/public/error_auto_create_index/error_auto_create_index.js +++ b/src/ui/public/error_auto_create_index/error_auto_create_index.ts @@ -24,16 +24,20 @@ import uiRoutes from '../routes'; import template from './error_auto_create_index.html'; -uiRoutes - .when('/error/action.auto_create_index', { - template, - k7Breadcrumbs: () => [{ text: i18n.translate('common.ui.errorAutoCreateIndex.breadcrumbs.errorText', { defaultMessage: 'Error' }) }], - }); +uiRoutes.when('/error/action.auto_create_index', { + template, + k7Breadcrumbs: () => [ + { + text: i18n.translate('common.ui.errorAutoCreateIndex.breadcrumbs.errorText', { + defaultMessage: 'Error', + }), + }, + ], +}); -export function isAutoCreateIndexError(error) { +export function isAutoCreateIndexError(error: object) { return ( - get(error, 'res.status') === 503 && - get(error, 'body.code') === 'ES_AUTO_CREATE_INDEX_ERROR' + get(error, 'res.status') === 503 && get(error, 'body.code') === 'ES_AUTO_CREATE_INDEX_ERROR' ); } diff --git a/src/ui/public/error_auto_create_index/index.js b/src/ui/public/error_auto_create_index/index.ts similarity index 100% rename from src/ui/public/error_auto_create_index/index.js rename to src/ui/public/error_auto_create_index/index.ts diff --git a/src/ui/public/saved_objects/saved_object.js b/src/ui/public/saved_objects/saved_object.js deleted file mode 100644 index ed3bbfe9b0fb1..0000000000000 --- a/src/ui/public/saved_objects/saved_object.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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 _ from 'lodash'; - -export class SavedObject { - constructor(client, { id, type, version, attributes, error, migrationVersion, references } = {}) { - this._client = client; - this.id = id; - this.type = type; - this.attributes = attributes || {}; - this.references = references || []; - this._version = version; - this.migrationVersion = migrationVersion; - if (error) { - this.error = error; - } - } - - get(key) { - return _.get(this.attributes, key); - } - - set(key, value) { - return _.set(this.attributes, key, value); - } - - has(key) { - return _.has(this.attributes, key); - } - - save() { - if (this.id) { - return this._client.update( - this.type, - this.id, - this.attributes, - { - migrationVersion: this.migrationVersion, - references: this.references, - }, - ); - } else { - return this._client.create(this.type, this.attributes, { migrationVersion: this.migrationVersion, references: this.references }); - } - } - - delete() { - return this._client.delete(this.type, this.id); - } -} diff --git a/src/ui/public/saved_objects/saved_object.ts b/src/ui/public/saved_objects/saved_object.ts new file mode 100644 index 0000000000000..a34cc2f150e51 --- /dev/null +++ b/src/ui/public/saved_objects/saved_object.ts @@ -0,0 +1,81 @@ +/* + * 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 { get, has, set } from 'lodash'; +import { + SavedObject as SavedObjectType, + SavedObjectAttributes, +} from '../../../server/saved_objects'; +import { SavedObjectsClient } from './saved_objects_client'; + +export class SavedObject { + public attributes: T; + // tslint:disable-next-line variable-name We want to use the same interface this class had in JS + public _version?: SavedObjectType['version']; + public id: SavedObjectType['id']; + public type: SavedObjectType['type']; + public migrationVersion: SavedObjectType['migrationVersion']; + public error: SavedObjectType['error']; + public references: SavedObjectType['references']; + + constructor( + private client: SavedObjectsClient, + { id, type, version, attributes, error, references, migrationVersion }: SavedObjectType + ) { + this.id = id; + this.type = type; + this.attributes = attributes || {}; + this.references = references || []; + this._version = version; + this.migrationVersion = migrationVersion; + if (error) { + this.error = error; + } + } + + public get(key: string): any { + return get(this.attributes, key); + } + + public set(key: string, value: any): T { + return set(this.attributes, key, value); + } + + public has(key: string): boolean { + return has(this.attributes, key); + } + + public save() { + if (this.id) { + return this.client.update(this.type, this.id, this.attributes, { + migrationVersion: this.migrationVersion, + references: this.references, + }); + } else { + return this.client.create(this.type, this.attributes, { + migrationVersion: this.migrationVersion, + references: this.references, + }); + } + } + + public delete() { + return this.client.delete(this.type, this.id); + } +} diff --git a/src/ui/public/saved_objects/saved_objects_client.js b/src/ui/public/saved_objects/saved_objects_client.js deleted file mode 100644 index 0ee4c2ff47ad3..0000000000000 --- a/src/ui/public/saved_objects/saved_objects_client.js +++ /dev/null @@ -1,260 +0,0 @@ -/* - * 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 _ from 'lodash'; - -import { resolve as resolveUrl } from 'url'; -import { keysToSnakeCaseShallow, keysToCamelCaseShallow } from '../../../utils/case_conversion'; -import { SavedObject } from './saved_object'; -import { isAutoCreateIndexError, showAutoCreateIndexErrorPage } from '../error_auto_create_index'; -import { kfetch } from 'ui/kfetch'; - -const join = (...uriComponents) => ( - uriComponents.filter(Boolean).map(encodeURIComponent).join('/') -); - -/** - * Interval that requests are batched for - * @type {integer} - */ -const BATCH_INTERVAL = 100; - -const API_BASE_URL = '/api/saved_objects/'; - -export class SavedObjectsClient { - constructor() { - this.batchQueue = []; - } - - /** - * Persists an object - * - * @param {string} type - * @param {object} [attributes={}] - * @param {object} [options={}] - * @property {string} [options.id] - force id on creation, not recommended - * @property {boolean} [options.overwrite=false] - * @property {object} [options.migrationVersion] - * @property {array} [options.references] [{ name, type, id }] - * @returns {promise} - SavedObject({ id, type, version, attributes }) - */ - create = (type, attributes = {}, options = {}) => { - if (!type || !attributes) { - return Promise.reject(new Error('requires type and attributes')); - } - - const path = this._getPath([type, options.id]); - const query = _.pick(options, ['overwrite']); - - return this - ._request({ - method: 'POST', - path, - query, - body: { - attributes, - migrationVersion: options.migrationVersion, - references: options.references, - }, - }) - .catch(error => { - if (isAutoCreateIndexError(error)) { - return showAutoCreateIndexErrorPage(); - } - - throw error; - }) - .then(resp => this._createSavedObject(resp)); - } - - /** - * Creates multiple documents at once - * - * @param {array} objects - [{ type, id, attributes, references, migrationVersion }] - * @param {object} [options={}] - * @property {boolean} [options.overwrite=false] - * @returns {promise} - { savedObjects: [{ id, type, version, attributes, error: { message } }]} - */ - bulkCreate = (objects = [], options = {}) => { - const path = this._getPath(['_bulk_create']); - const query = _.pick(options, ['overwrite']); - - return this._request({ method: 'POST', path, query, body: objects }).then(resp => { - resp.saved_objects = resp.saved_objects.map(d => this._createSavedObject(d)); - return keysToCamelCaseShallow(resp); - }); - } - - /** - * Deletes an object - * - * @param {string} type - * @param {string} id - * @returns {promise} - */ - delete = (type, id) => { - if (!type || !id) { - return Promise.reject(new Error('requires type and id')); - } - - return this._request({ method: 'DELETE', path: this._getPath([type, id]) }); - } - - /** - * Search for objects - * - * @param {object} [options={}] - * @property {string} options.type - * @property {string} options.search - * @property {string} options.defaultSearchOperator - * @property {string} options.searchFields - see Elasticsearch Simple Query String - * Query field argument for more information - * @property {integer} [options.page=1] - * @property {integer} [options.perPage=20] - * @property {array} options.fields - * @property {object} [options.hasReference] - { type, id } - * @returns {promise} - { savedObjects: [ SavedObject({ id, type, version, attributes }) ]} - */ - find = (options = {}) => { - const path = this._getPath(['_find']); - const query = keysToSnakeCaseShallow(options); - - return this._request({ method: 'GET', path, query }).then(resp => { - resp.saved_objects = resp.saved_objects.map(d => this._createSavedObject(d)); - return keysToCamelCaseShallow(resp); - }); - } - - /** - * Fetches a single object - * - * @param {string} type - * @param {string} id - * @returns {promise} - SavedObject({ id, type, version, attributes }) - */ - get = (type, id) => { - if (!type || !id) { - return Promise.reject(new Error('requires type and id')); - } - - return new Promise((resolve, reject) => { - this.batchQueue.push({ type, id, resolve, reject }); - this._processBatchQueue(); - }); - } - - /** - * Returns an array of objects by id - * - * @param {array} objects - an array ids, or an array of objects containing id and optionally type - * @returns {promise} - { savedObjects: [ SavedObject({ id, type, version, attributes }) ] } - * @example - * - * bulkGet([ - * { id: 'one', type: 'config' }, - * { id: 'foo', type: 'index-pattern' } - * ]) - */ - bulkGet = (objects = []) => { - const path = this._getPath(['_bulk_get']); - const filteredObjects = objects.map(obj => _.pick(obj, ['id', 'type'])); - - return this._request({ method: 'POST', path, body: filteredObjects }).then(resp => { - resp.saved_objects = resp.saved_objects.map(d => this._createSavedObject(d)); - return keysToCamelCaseShallow(resp); - }); - } - - /** - * Updates an object - * - * @param {string} type - * @param {string} id - * @param {object} attributes - * @param {object} options - * @prop {integer} options.version - ensures version matches that of persisted object - * @prop {object} options.migrationVersion - The optional migrationVersion of this document - * @prop {array} option.references - the references of the saved object - * @returns {promise} - */ - update(type, id, attributes, { version, migrationVersion, references } = {}) { - if (!type || !id || !attributes) { - return Promise.reject(new Error('requires type, id and attributes')); - } - - const path = this._getPath([type, id]); - const body = { - attributes, - migrationVersion, - references, - version - }; - - return this._request({ method: 'PUT', path, body }).then(resp => { - return this._createSavedObject(resp); - }); - } - - /** - * Throttled processing of get requests into bulk requests at 100ms interval - */ - _processBatchQueue = _.throttle(() => { - const queue = _.cloneDeep(this.batchQueue); - this.batchQueue = []; - - this.bulkGet(queue).then(({ savedObjects }) => { - queue.forEach((queueItem) => { - const foundObject = savedObjects.find(savedObject => { - return savedObject.id === queueItem.id & savedObject.type === queueItem.type; - }); - - if (!foundObject) { - return queueItem.resolve(this._createSavedObject(_.pick(queueItem, ['id', 'type']))); - } - - queueItem.resolve(foundObject); - }); - }).catch((err) => { - queue.forEach((queueItem) => { - queueItem.reject(err); - }); - }); - - }, BATCH_INTERVAL, { leading: false }); - - _createSavedObject(options) { - return new SavedObject(this, options); - } - - _getPath(path) { - if (!path) { - return API_BASE_URL; - } - - return resolveUrl(API_BASE_URL, join(...path)); - } - - _request({ method, path, query, body }) { - if (method === 'GET' && body) { - return Promise.reject(new Error('body not permitted for GET requests')); - } - - return kfetch({ method, pathname: path, query, body: JSON.stringify(body) }); - } -} diff --git a/src/ui/public/saved_objects/saved_objects_client.ts b/src/ui/public/saved_objects/saved_objects_client.ts new file mode 100644 index 0000000000000..10f082ee4f439 --- /dev/null +++ b/src/ui/public/saved_objects/saved_objects_client.ts @@ -0,0 +1,352 @@ +/* + * 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 { cloneDeep, pick, throttle } from 'lodash'; + +import { kfetch } from 'ui/kfetch'; +import { resolve as resolveUrl } from 'url'; +import { + FindOptions, + MigrationVersion, + SavedObject as PlainSavedObject, + SavedObjectAttributes, + SavedObjectReference, + SavedObjectsClient as SavedObjectsApi, +} from '../../../server/saved_objects'; +import { keysToCamelCaseShallow, keysToSnakeCaseShallow } from '../../../utils/case_conversion'; +import { isAutoCreateIndexError, showAutoCreateIndexErrorPage } from '../error_auto_create_index'; +import { SavedObject } from './saved_object'; + +interface RequestParams { + method: 'POST' | 'GET' | 'PUT' | 'DELETE'; + path: string; + query?: { [paramName: string]: string }; + body?: object; +} + +interface CreateOptions { + id?: string; + overwrite?: boolean; + migrationVersion?: MigrationVersion; + references?: SavedObjectReference[]; +} + +interface UpdateOptions { + version?: string; + migrationVersion?: MigrationVersion; + references?: SavedObjectReference[]; +} + +interface FindResults { + savedObjects: Array>; + total: number; + perPage: number; + page: number; +} + +interface BatchQueueEntry { + type: string; + id: string; + resolve: () => void; + reject: () => void; +} + +const join = (...uriComponents: Array) => + uriComponents + .filter((comp): comp is string => Boolean(comp)) + .map(encodeURIComponent) + .join('/'); + +/** + * Interval that requests are batched for + * @type {integer} + */ +const BATCH_INTERVAL = 100; + +const API_BASE_URL = '/api/saved_objects/'; + +export class SavedObjectsClient { + /** + * Throttled processing of get requests into bulk requests at 100ms interval + */ + // TODO: Type this function + private processBatchQueue = throttle( + () => { + const queue = cloneDeep(this.batchQueue); + this.batchQueue = []; + + this.bulkGet(queue) + .then(({ savedObjects }) => { + queue.forEach(queueItem => { + const foundObject = savedObjects.find(savedObject => { + return savedObject.id === queueItem.id && savedObject.type === queueItem.type; + }); + + if (!foundObject) { + return queueItem.resolve(this.createSavedObject(pick(queueItem, ['id', 'type']))); + } + + queueItem.resolve(foundObject); + }); + }) + .catch(err => { + queue.forEach(queueItem => { + queueItem.reject(err); + }); + }); + }, + BATCH_INTERVAL, + { leading: false } + ); + + private batchQueue: BatchQueueEntry[]; + + constructor() { + this.batchQueue = []; + } + + /** + * Persists an object + * + * @param {string} type + * @param {object} [attributes={}] + * @param {object} [options={}] + * @property {string} [options.id] - force id on creation, not recommended + * @property {boolean} [options.overwrite=false] + * @property {object} [options.migrationVersion] + * @returns {promise} - SavedObject({ id, type, version, attributes }) + */ + public create = ( + type: string, + attributes: T, + options: CreateOptions = {} + ): Promise> => { + if (!type || !attributes) { + return Promise.reject(new Error('requires type and attributes')); + } + + const path = this.getPath([type, options.id]); + const query = { + overwrite: options.overwrite, + }; + + const createRequest: ReturnType = this.request({ + method: 'POST', + path, + query, + body: { + attributes, + migrationVersion: options.migrationVersion, + references: options.references, + }, + }); + + return ( + createRequest + .catch((error: object) => { + if (isAutoCreateIndexError(error)) { + return showAutoCreateIndexErrorPage(); + } + + throw error; + }) + // TODO: Why is this `void` in here? + .then(resp => this.createSavedObject(resp)) + ); + }; + + /** + * Creates multiple documents at once + * + * @param {array} objects - [{ type, id, attributes, references, migrationVersion }] + * @param {object} [options={}] + * @property {boolean} [options.overwrite=false] + * @returns {promise} - { savedObjects: [{ id, type, version, attributes, error: { message } }]} + */ + public bulkCreate = (objects = [], options = {}) => { + const path = this.getPath(['_bulk_create']); + const query = pick(options, ['overwrite']); + + const request: ReturnType = this.request({ + method: 'POST', + path, + query, + body: objects, + }); + return request.then(resp => { + resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d)); + return keysToCamelCaseShallow(resp); + }); + }; + + /** + * Deletes an object + * + * @param type + * @param id + * @returns + */ + public delete = (type: string, id: string): ReturnType => { + if (!type || !id) { + return Promise.reject(new Error('requires type and id')); + } + + return this.request({ method: 'DELETE', path: this.getPath([type, id]) }); + }; + + /** + * Search for objects + * + * @param {object} [options={}] + * @property {string} options.type + * @property {string} options.search + * @property {string} options.searchFields - see Elasticsearch Simple Query String + * Query field argument for more information + * @property {integer} [options.page=1] + * @property {integer} [options.perPage=20] + * @property {array} options.fields + * @property {object} [options.hasReference] - { type, id } + * @returns {promise} - { savedObjects: [ SavedObject({ id, type, version, attributes }) ]} + */ + public find = (options: FindOptions = {}): Promise => { + const path = this.getPath(['_find']); + const query = keysToSnakeCaseShallow(options); + + const request: ReturnType = this.request({ + method: 'GET', + path, + query, + }); + return request.then(resp => { + resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d)); + return keysToCamelCaseShallow(resp) as FindResults; + }); + }; + + /** + * Fetches a single object + * + * @param {string} type + * @param {string} id + * @returns {promise} - SavedObject({ id, type, version, attributes }) + */ + public get = ( + type: string, + id: string + ): Promise> => { + if (!type || !id) { + return Promise.reject(new Error('requires type and id')); + } + + return new Promise((resolve, reject) => { + this.batchQueue.push({ type, id, resolve, reject }); + this.processBatchQueue(); + }); + }; + + /** + * Returns an array of objects by id + * + * @param {array} objects - an array ids, or an array of objects containing id and optionally type + * @returns {promise} - { savedObjects: [ SavedObject({ id, type, version, attributes }) ] } + * @example + * + * bulkGet([ + * { id: 'one', type: 'config' }, + * { id: 'foo', type: 'index-pattern' } + * ]) + */ + // TODO: Add return type + public bulkGet = (objects: Array<{ id: string; type: string }> = []) => { + const path = this.getPath(['_bulk_get']); + const filteredObjects = objects.map(obj => pick(obj, ['id', 'type'])); + + const request: ReturnType = this.request({ + method: 'POST', + path, + body: filteredObjects, + }); + return request.then(resp => { + resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d)); + return keysToCamelCaseShallow(resp); + }); + }; + + /** + * Updates an object + * + * @param {string} type + * @param {string} id + * @param {object} attributes + * @param {object} options + * @prop {integer} options.version - ensures version matches that of persisted object + * @prop {object} options.migrationVersion - The optional migrationVersion of this document + * @returns {promise} + */ + public update( + type: string, + id: string, + attributes: T, + { version, migrationVersion, references }: UpdateOptions = {} + ): Promise> { + if (!type || !id || !attributes) { + return Promise.reject(new Error('requires type, id and attributes')); + } + + const path = this.getPath([type, id]); + const body = { + attributes, + migrationVersion, + references, + version, + }; + + const request: ReturnType = this.request({ + method: 'PUT', + path, + body, + }); + // TODO: can we somehow get T into the above ReturnType definition, so it knows it's actually ReturnTypeupdate> + return request.then(resp => { + return this.createSavedObject(resp); + }); + } + + private createSavedObject( + options: PlainSavedObject + ): SavedObject { + return new SavedObject(this, options); + } + + private getPath(path: Array): string { + if (!path) { + return API_BASE_URL; + } + + return resolveUrl(API_BASE_URL, join(...path)); + } + + private request({ method, path, query, body }: RequestParams) { + if (method === 'GET' && body) { + return Promise.reject(new Error('body not permitted for GET requests')); + } + + return kfetch({ method, pathname: path, query, body: JSON.stringify(body) }); + } +} From d3a7cb61c710a52cfc1b2764e5da19f7ea4d8195 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Mon, 4 Feb 2019 15:02:46 +0100 Subject: [PATCH 02/35] Move more files to TS --- src/ui/public/chrome/index.d.ts | 2 + ...ct_by_title.js => find_object_by_title.ts} | 39 ++++++++++++------- .../saved_objects/{index.js => index.ts} | 2 + 3 files changed, 29 insertions(+), 14 deletions(-) rename src/ui/public/saved_objects/{find_object_by_title.js => find_object_by_title.ts} (62%) rename src/ui/public/saved_objects/{index.js => index.ts} (97%) diff --git a/src/ui/public/chrome/index.d.ts b/src/ui/public/chrome/index.d.ts index 6b6a40270e1e7..c93e8a693ab70 100644 --- a/src/ui/public/chrome/index.d.ts +++ b/src/ui/public/chrome/index.d.ts @@ -18,6 +18,7 @@ */ import { Brand } from '../../../core/public/chrome'; +import { SavedObjectsClient } from '../saved_objects'; import { BreadcrumbsApi } from './api/breadcrumbs'; import { HelpExtensionApi } from './api/help_extension'; import { ChromeNavLinks } from './api/nav'; @@ -34,6 +35,7 @@ declare interface Chrome extends ChromeNavLinks { getBasePath(): string; getXsrfToken(): string; getKibanaVersion(): string; + getSavedObjectsClient(): SavedObjectsClient; getUiSettingsClient(): any; setVisible(visible: boolean): any; getInjected(key: string, defaultValue?: any): any; diff --git a/src/ui/public/saved_objects/find_object_by_title.js b/src/ui/public/saved_objects/find_object_by_title.ts similarity index 62% rename from src/ui/public/saved_objects/find_object_by_title.js rename to src/ui/public/saved_objects/find_object_by_title.ts index 6b148d615c712..82c8ad440d8ef 100644 --- a/src/ui/public/saved_objects/find_object_by_title.js +++ b/src/ui/public/saved_objects/find_object_by_title.ts @@ -18,6 +18,9 @@ */ import { find } from 'lodash'; +import { SavedObjectAttributes } from 'src/server/saved_objects'; +import { SavedObject } from './saved_object'; +import { SavedObjectsClient } from './saved_objects_client'; /** * Returns an object matching a given title @@ -27,22 +30,30 @@ import { find } from 'lodash'; * @param title {string} * @returns {Promise} */ -export function findObjectByTitle(savedObjectsClient, type, title) { - if (!title) return Promise.resolve(); +export function findObjectByTitle( + savedObjectsClient: SavedObjectsClient, + type: string, + title: string +): Promise | void> { + if (!title) { + return Promise.resolve(); + } // Elastic search will return the most relevant results first, which means exact matches should come // first, and so we shouldn't need to request everything. Using 10 just to be on the safe side. - return savedObjectsClient.find({ - type, - perPage: 10, - search: `"${title}"`, - searchFields: ['title'], - fields: ['title'] - }).then(response => { - const match = find(response.savedObjects, (obj) => { - return obj.get('title').toLowerCase() === title.toLowerCase(); - }); + return savedObjectsClient + .find({ + type, + perPage: 10, + search: `"${title}"`, + searchFields: ['title'], + fields: ['title'], + }) + .then(response => { + const match = find(response.savedObjects, obj => { + return obj.get('title').toLowerCase() === title.toLowerCase(); + }); - return match; - }); + return match; + }); } diff --git a/src/ui/public/saved_objects/index.js b/src/ui/public/saved_objects/index.ts similarity index 97% rename from src/ui/public/saved_objects/index.js rename to src/ui/public/saved_objects/index.ts index 2571c80750677..9218c91e07f61 100644 --- a/src/ui/public/saved_objects/index.js +++ b/src/ui/public/saved_objects/index.ts @@ -18,7 +18,9 @@ */ export { SavedObjectsClient } from './saved_objects_client'; +// @ts-ignore export { SavedObjectRegistryProvider } from './saved_object_registry'; +// @ts-ignore export { SavedObjectsClientProvider } from './saved_objects_client_provider'; export { SavedObject } from './saved_object'; export { findObjectByTitle } from './find_object_by_title'; From a755c2ac1da0dcf30ab1141ffbf1385e2987c4b9 Mon Sep 17 00:00:00 2001 From: Johannes Reuter Date: Mon, 4 Feb 2019 16:29:53 +0100 Subject: [PATCH 03/35] type saved objects client --- .../service/saved_objects_client.d.ts | 5 ++ src/ui/public/kfetch/kfetch.ts | 2 +- .../saved_objects/saved_objects_client.ts | 51 +++++++++---------- 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/src/server/saved_objects/service/saved_objects_client.d.ts b/src/server/saved_objects/service/saved_objects_client.d.ts index c9fcf76f0d3ac..92ee941fe99bb 100644 --- a/src/server/saved_objects/service/saved_objects_client.d.ts +++ b/src/server/saved_objects/service/saved_objects_client.d.ts @@ -73,6 +73,11 @@ export interface BulkGetResponse { saved_objects: Array>; } +// TODO find better name for that +export interface CamelCaseBulkGetResponse { + savedObjects: Array>; +} + export interface MigrationVersion { [pluginName: string]: string; } diff --git a/src/ui/public/kfetch/kfetch.ts b/src/ui/public/kfetch/kfetch.ts index 01515c9c0e0aa..086692a66261b 100644 --- a/src/ui/public/kfetch/kfetch.ts +++ b/src/ui/public/kfetch/kfetch.ts @@ -24,7 +24,7 @@ import url from 'url'; import chrome from '../chrome'; import { KFetchError } from './kfetch_error'; -interface KFetchQuery { +export interface KFetchQuery { [key: string]: string | number | boolean | undefined; } diff --git a/src/ui/public/saved_objects/saved_objects_client.ts b/src/ui/public/saved_objects/saved_objects_client.ts index 10f082ee4f439..86dbe5f36f43a 100644 --- a/src/ui/public/saved_objects/saved_objects_client.ts +++ b/src/ui/public/saved_objects/saved_objects_client.ts @@ -19,8 +19,6 @@ import { cloneDeep, pick, throttle } from 'lodash'; -import { kfetch } from 'ui/kfetch'; -import { resolve as resolveUrl } from 'url'; import { FindOptions, MigrationVersion, @@ -28,7 +26,14 @@ import { SavedObjectAttributes, SavedObjectReference, SavedObjectsClient as SavedObjectsApi, -} from '../../../server/saved_objects'; +} from 'src/server/saved_objects'; +import { + BulkGetResponse, + CamelCaseBulkGetResponse, +} from 'src/server/saved_objects/service/saved_objects_client'; +import { kfetch } from 'ui/kfetch'; +import { KFetchQuery } from 'ui/kfetch/kfetch'; +import { resolve as resolveUrl } from 'url'; import { keysToCamelCaseShallow, keysToSnakeCaseShallow } from '../../../utils/case_conversion'; import { isAutoCreateIndexError, showAutoCreateIndexErrorPage } from '../error_auto_create_index'; import { SavedObject } from './saved_object'; @@ -36,7 +41,7 @@ import { SavedObject } from './saved_object'; interface RequestParams { method: 'POST' | 'GET' | 'PUT' | 'DELETE'; path: string; - query?: { [paramName: string]: string }; + query?: KFetchQuery; body?: object; } @@ -63,8 +68,8 @@ interface FindResults { interface BatchQueueEntry { type: string; id: string; - resolve: () => void; - reject: () => void; + resolve: (value: SavedObject | PlainSavedObject) => void; + reject: (reason?: any) => void; } const join = (...uriComponents: Array) => @@ -85,7 +90,6 @@ export class SavedObjectsClient { /** * Throttled processing of get requests into bulk requests at 100ms interval */ - // TODO: Type this function private processBatchQueue = throttle( () => { const queue = cloneDeep(this.batchQueue); @@ -157,18 +161,15 @@ export class SavedObjectsClient { }, }); - return ( - createRequest - .catch((error: object) => { - if (isAutoCreateIndexError(error)) { - return showAutoCreateIndexErrorPage(); - } + return createRequest + .then(resp => this.createSavedObject(resp)) + .catch((error: object) => { + if (isAutoCreateIndexError(error)) { + showAutoCreateIndexErrorPage(); + } - throw error; - }) - // TODO: Why is this `void` in here? - .then(resp => this.createSavedObject(resp)) - ); + throw error; + }) as Promise>; }; /** @@ -179,9 +180,9 @@ export class SavedObjectsClient { * @property {boolean} [options.overwrite=false] * @returns {promise} - { savedObjects: [{ id, type, version, attributes, error: { message } }]} */ - public bulkCreate = (objects = [], options = {}) => { + public bulkCreate = (objects = [], options: KFetchQuery = {}) => { const path = this.getPath(['_bulk_create']); - const query = pick(options, ['overwrite']); + const query = pick(options, ['overwrite']) as Pick; const request: ReturnType = this.request({ method: 'POST', @@ -255,7 +256,7 @@ export class SavedObjectsClient { } return new Promise((resolve, reject) => { - this.batchQueue.push({ type, id, resolve, reject }); + this.batchQueue.push({ type, id, resolve, reject } as BatchQueueEntry); this.processBatchQueue(); }); }; @@ -272,7 +273,6 @@ export class SavedObjectsClient { * { id: 'foo', type: 'index-pattern' } * ]) */ - // TODO: Add return type public bulkGet = (objects: Array<{ id: string; type: string }> = []) => { const path = this.getPath(['_bulk_get']); const filteredObjects = objects.map(obj => pick(obj, ['id', 'type'])); @@ -282,9 +282,9 @@ export class SavedObjectsClient { path, body: filteredObjects, }); - return request.then(resp => { + return request.then((resp: BulkGetResponse) => { resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d)); - return keysToCamelCaseShallow(resp); + return keysToCamelCaseShallow(resp) as CamelCaseBulkGetResponse; }); }; @@ -322,10 +322,9 @@ export class SavedObjectsClient { path, body, }); - // TODO: can we somehow get T into the above ReturnType definition, so it knows it's actually ReturnTypeupdate> return request.then(resp => { return this.createSavedObject(resp); - }); + }) as Promise>; } private createSavedObject( From dcbcc418f8c497c0b30ced2085219f5031df6857 Mon Sep 17 00:00:00 2001 From: Johannes Reuter Date: Tue, 5 Feb 2019 10:49:07 +0100 Subject: [PATCH 04/35] clean up typings for saved object client --- .../service/saved_objects_client.d.ts | 11 ++++---- src/ui/public/kfetch/index.ts | 2 +- .../saved_objects/saved_objects_client.ts | 27 +++++++++---------- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/server/saved_objects/service/saved_objects_client.d.ts b/src/server/saved_objects/service/saved_objects_client.d.ts index 92ee941fe99bb..b773665e30108 100644 --- a/src/server/saved_objects/service/saved_objects_client.d.ts +++ b/src/server/saved_objects/service/saved_objects_client.d.ts @@ -73,11 +73,6 @@ export interface BulkGetResponse { saved_objects: Array>; } -// TODO find better name for that -export interface CamelCaseBulkGetResponse { - savedObjects: Array>; -} - export interface MigrationVersion { [pluginName: string]: string; } @@ -105,6 +100,10 @@ export interface SavedObjectReference { id: string; } +export type SavedObjectsClientCreateResponse = Promise< + SavedObject +>; + export declare class SavedObjectsClient { public static errors: typeof errors; public errors: typeof errors; @@ -115,7 +114,7 @@ export declare class SavedObjectsClient { type: string, attributes: T, options?: CreateOptions - ): Promise>; + ): SavedObjectsClientCreateResponse; public bulkCreate( objects: Array>, options?: CreateOptions diff --git a/src/ui/public/kfetch/index.ts b/src/ui/public/kfetch/index.ts index f871d97f0fd54..234304b5950aa 100644 --- a/src/ui/public/kfetch/index.ts +++ b/src/ui/public/kfetch/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { kfetch, addInterceptor, KFetchOptions } from './kfetch'; +export { kfetch, addInterceptor, KFetchOptions, KFetchQuery } from './kfetch'; export { kfetchAbortable } from './kfetch_abortable'; diff --git a/src/ui/public/saved_objects/saved_objects_client.ts b/src/ui/public/saved_objects/saved_objects_client.ts index 86dbe5f36f43a..bb7e797feb2e3 100644 --- a/src/ui/public/saved_objects/saved_objects_client.ts +++ b/src/ui/public/saved_objects/saved_objects_client.ts @@ -18,6 +18,7 @@ */ import { cloneDeep, pick, throttle } from 'lodash'; +import { resolve as resolveUrl } from 'url'; import { FindOptions, @@ -26,16 +27,11 @@ import { SavedObjectAttributes, SavedObjectReference, SavedObjectsClient as SavedObjectsApi, -} from 'src/server/saved_objects'; -import { - BulkGetResponse, - CamelCaseBulkGetResponse, -} from 'src/server/saved_objects/service/saved_objects_client'; -import { kfetch } from 'ui/kfetch'; -import { KFetchQuery } from 'ui/kfetch/kfetch'; -import { resolve as resolveUrl } from 'url'; +} from '../../../../src/server/saved_objects'; +import { SavedObjectsClientCreateResponse } from '../../../../src/server/saved_objects/service/saved_objects_client'; import { keysToCamelCaseShallow, keysToSnakeCaseShallow } from '../../../utils/case_conversion'; import { isAutoCreateIndexError, showAutoCreateIndexErrorPage } from '../error_auto_create_index'; +import { kfetch, KFetchQuery } from '../kfetch'; import { SavedObject } from './saved_object'; interface RequestParams { @@ -58,8 +54,11 @@ interface UpdateOptions { references?: SavedObjectReference[]; } -interface FindResults { +interface BatchResponse { savedObjects: Array>; +} + +interface FindResults extends BatchResponse { total: number; perPage: number; page: number; @@ -150,7 +149,7 @@ export class SavedObjectsClient { overwrite: options.overwrite, }; - const createRequest: ReturnType = this.request({ + const createRequest: SavedObjectsClientCreateResponse = this.request({ method: 'POST', path, query, @@ -169,7 +168,7 @@ export class SavedObjectsClient { } throw error; - }) as Promise>; + }); }; /** @@ -192,7 +191,7 @@ export class SavedObjectsClient { }); return request.then(resp => { resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d)); - return keysToCamelCaseShallow(resp); + return keysToCamelCaseShallow(resp) as BatchResponse; }); }; @@ -282,9 +281,9 @@ export class SavedObjectsClient { path, body: filteredObjects, }); - return request.then((resp: BulkGetResponse) => { + return request.then(resp => { resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d)); - return keysToCamelCaseShallow(resp) as CamelCaseBulkGetResponse; + return keysToCamelCaseShallow(resp) as BatchResponse; }); }; From 605ec6033bb657b5f54045672cf7f2e38cef9d69 Mon Sep 17 00:00:00 2001 From: Johannes Reuter Date: Tue, 5 Feb 2019 11:09:12 +0100 Subject: [PATCH 05/35] tie typings form server and client for saved objects together --- src/server/saved_objects/index.d.ts | 1 - src/server/saved_objects/service/index.d.ts | 3 +++ .../saved_objects/service/saved_objects_client.d.ts | 12 ++++++------ src/ui/public/saved_objects/saved_objects_client.ts | 13 ++++++++----- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/server/saved_objects/index.d.ts b/src/server/saved_objects/index.d.ts index c62cad7a6d12b..8e6df957b8a6e 100644 --- a/src/server/saved_objects/index.d.ts +++ b/src/server/saved_objects/index.d.ts @@ -18,7 +18,6 @@ */ export { - FindOptions, MigrationVersion, SavedObject, SavedObjectAttributes, diff --git a/src/server/saved_objects/service/index.d.ts b/src/server/saved_objects/service/index.d.ts index 550c601e3e432..47f3f382c9daf 100644 --- a/src/server/saved_objects/service/index.d.ts +++ b/src/server/saved_objects/service/index.d.ts @@ -21,6 +21,9 @@ export { SavedObjectsService } from './create_saved_objects_service'; export { SavedObjectsClientWrapperFactory } from './lib'; export { FindOptions, + GetResponse, + UpdateResponse, + CreateResponse, MigrationVersion, SavedObject, SavedObjectAttributes, diff --git a/src/server/saved_objects/service/saved_objects_client.d.ts b/src/server/saved_objects/service/saved_objects_client.d.ts index b773665e30108..86e5eec3abd78 100644 --- a/src/server/saved_objects/service/saved_objects_client.d.ts +++ b/src/server/saved_objects/service/saved_objects_client.d.ts @@ -100,9 +100,9 @@ export interface SavedObjectReference { id: string; } -export type SavedObjectsClientCreateResponse = Promise< - SavedObject ->; +export type GetResponse = SavedObject; +export type CreateResponse = SavedObject; +export type UpdateResponse = SavedObject; export declare class SavedObjectsClient { public static errors: typeof errors; @@ -114,7 +114,7 @@ export declare class SavedObjectsClient { type: string, attributes: T, options?: CreateOptions - ): SavedObjectsClientCreateResponse; + ): Promise>; public bulkCreate( objects: Array>, options?: CreateOptions @@ -131,11 +131,11 @@ export declare class SavedObjectsClient { type: string, id: string, options?: BaseOptions - ): Promise>; + ): Promise>; public update( type: string, id: string, attributes: Partial, options?: UpdateOptions - ): Promise>; + ): Promise>; } diff --git a/src/ui/public/saved_objects/saved_objects_client.ts b/src/ui/public/saved_objects/saved_objects_client.ts index bb7e797feb2e3..6927b69c11b15 100644 --- a/src/ui/public/saved_objects/saved_objects_client.ts +++ b/src/ui/public/saved_objects/saved_objects_client.ts @@ -21,14 +21,17 @@ import { cloneDeep, pick, throttle } from 'lodash'; import { resolve as resolveUrl } from 'url'; import { - FindOptions, MigrationVersion, SavedObject as PlainSavedObject, SavedObjectAttributes, SavedObjectReference, SavedObjectsClient as SavedObjectsApi, } from '../../../../src/server/saved_objects'; -import { SavedObjectsClientCreateResponse } from '../../../../src/server/saved_objects/service/saved_objects_client'; +import { + CreateResponse, + FindOptions, + UpdateResponse, +} from '../../../../src/server/saved_objects/service'; import { keysToCamelCaseShallow, keysToSnakeCaseShallow } from '../../../utils/case_conversion'; import { isAutoCreateIndexError, showAutoCreateIndexErrorPage } from '../error_auto_create_index'; import { kfetch, KFetchQuery } from '../kfetch'; @@ -149,7 +152,7 @@ export class SavedObjectsClient { overwrite: options.overwrite, }; - const createRequest: SavedObjectsClientCreateResponse = this.request({ + const createRequest: Promise> = this.request({ method: 'POST', path, query, @@ -316,14 +319,14 @@ export class SavedObjectsClient { version, }; - const request: ReturnType = this.request({ + const request: Promise> = this.request({ method: 'PUT', path, body, }); return request.then(resp => { return this.createSavedObject(resp); - }) as Promise>; + }); } private createSavedObject( From 830fd7f9122d81c4d99e935ce06df83691f353de Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Tue, 5 Feb 2019 13:28:02 +0100 Subject: [PATCH 06/35] add missing html import typing to x-pack --- x-pack/tsconfig.json | 1 + x-pack/typings/index.d.ts | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 x-pack/typings/index.d.ts diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 4f7c9e3e8b275..287d829792ee0 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.json", "include": [ + "typings/**/*", "common/**/*", "server/**/*", "plugins/**/*", diff --git a/x-pack/typings/index.d.ts b/x-pack/typings/index.d.ts new file mode 100644 index 0000000000000..38dbf632e578c --- /dev/null +++ b/x-pack/typings/index.d.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +declare module '*.html' { + const template: string; + export default template; +} From 96a89223b48016f56ef15b36b242633cc5a7ba62 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Tue, 5 Feb 2019 13:50:42 +0100 Subject: [PATCH 07/35] Add missing buildSourcePatterns --- x-pack/.kibana-plugin-helpers.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/.kibana-plugin-helpers.json b/x-pack/.kibana-plugin-helpers.json index 5120016237f71..b23525ef6f38d 100644 --- a/x-pack/.kibana-plugin-helpers.json +++ b/x-pack/.kibana-plugin-helpers.json @@ -16,6 +16,7 @@ "common/**/*", "plugins/**/*", "server/**/*", + "typings/**/*", "webpackShims/*", "!**/README.md", "!__tests__", @@ -25,7 +26,7 @@ "!plugins/**/*.test.js", "!plugins/**/__snapshots__", "!plugins/**/__snapshots__/*", - "!plugins/**/__mocks__/*" + "!plugins/**/__mocks__/*", ], "skipInstallDependencies": true } From c5fccaa5a6ab46bbed8c214f69d4d5fe08cbbeb0 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Tue, 5 Feb 2019 14:10:05 +0100 Subject: [PATCH 08/35] Removed accidental comma --- x-pack/.kibana-plugin-helpers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/.kibana-plugin-helpers.json b/x-pack/.kibana-plugin-helpers.json index b23525ef6f38d..8c6e9758cae9d 100644 --- a/x-pack/.kibana-plugin-helpers.json +++ b/x-pack/.kibana-plugin-helpers.json @@ -26,7 +26,7 @@ "!plugins/**/*.test.js", "!plugins/**/__snapshots__", "!plugins/**/__snapshots__/*", - "!plugins/**/__mocks__/*", + "!plugins/**/__mocks__/*" ], "skipInstallDependencies": true } From 6f9e95f61914262d35eddcb1dd2380ca86db9e0a Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Mon, 4 Feb 2019 14:48:43 +0100 Subject: [PATCH 09/35] WIP typings for saved object client --- src/server/saved_objects/index.d.ts | 4 + src/server/saved_objects/service/index.d.ts | 9 +- .../service/saved_objects_client.d.ts | 7 + ...te_index.js => error_auto_create_index.ts} | 20 +- .../{index.js => index.ts} | 0 src/ui/public/saved_objects/saved_object.js | 67 ---- src/ui/public/saved_objects/saved_object.ts | 81 ++++ .../saved_objects/saved_objects_client.js | 260 ------------- .../saved_objects/saved_objects_client.ts | 352 ++++++++++++++++++ 9 files changed, 464 insertions(+), 336 deletions(-) rename src/ui/public/error_auto_create_index/{error_auto_create_index.js => error_auto_create_index.ts} (73%) rename src/ui/public/error_auto_create_index/{index.js => index.ts} (100%) delete mode 100644 src/ui/public/saved_objects/saved_object.js create mode 100644 src/ui/public/saved_objects/saved_object.ts delete mode 100644 src/ui/public/saved_objects/saved_objects_client.js create mode 100644 src/ui/public/saved_objects/saved_objects_client.ts diff --git a/src/server/saved_objects/index.d.ts b/src/server/saved_objects/index.d.ts index 99b6af3c787aa..c62cad7a6d12b 100644 --- a/src/server/saved_objects/index.d.ts +++ b/src/server/saved_objects/index.d.ts @@ -18,8 +18,12 @@ */ export { + FindOptions, + MigrationVersion, SavedObject, + SavedObjectAttributes, SavedObjectsClient, SavedObjectsClientWrapperFactory, + SavedObjectReference, SavedObjectsService, } from './service'; diff --git a/src/server/saved_objects/service/index.d.ts b/src/server/saved_objects/service/index.d.ts index dc6b2c545ca73..550c601e3e432 100644 --- a/src/server/saved_objects/service/index.d.ts +++ b/src/server/saved_objects/service/index.d.ts @@ -19,4 +19,11 @@ export { SavedObjectsService } from './create_saved_objects_service'; export { SavedObjectsClientWrapperFactory } from './lib'; -export { SavedObject, SavedObjectsClient } from './saved_objects_client'; +export { + FindOptions, + MigrationVersion, + SavedObject, + SavedObjectAttributes, + SavedObjectsClient, + SavedObjectReference, +} from './saved_objects_client'; diff --git a/src/server/saved_objects/service/saved_objects_client.d.ts b/src/server/saved_objects/service/saved_objects_client.d.ts index 6f3578acb6e4d..c874c5a9619b2 100644 --- a/src/server/saved_objects/service/saved_objects_client.d.ts +++ b/src/server/saved_objects/service/saved_objects_client.d.ts @@ -26,6 +26,7 @@ export interface BaseOptions { export interface CreateOptions extends BaseOptions { id?: string; override?: boolean; + references?: SavedObjectReference[]; } export interface BulkCreateObject { @@ -48,6 +49,7 @@ export interface FindOptions extends BaseOptions { fields?: string[]; search?: string; searchFields?: string[]; + hasReference?: { type: string; id: string }; } export interface FindResponse { @@ -71,6 +73,10 @@ export interface BulkGetResponse { saved_objects: Array>; } +export interface MigrationVersion { + [pluginName: string]: string; +} + export interface SavedObjectAttributes { [key: string]: SavedObjectAttributes | string | number | boolean | null; } @@ -85,6 +91,7 @@ export interface SavedObject { }; attributes: T; references: SavedObjectReference[]; + migrationVersion?: MigrationVersion; } export interface SavedObjectReference { diff --git a/src/ui/public/error_auto_create_index/error_auto_create_index.js b/src/ui/public/error_auto_create_index/error_auto_create_index.ts similarity index 73% rename from src/ui/public/error_auto_create_index/error_auto_create_index.js rename to src/ui/public/error_auto_create_index/error_auto_create_index.ts index 4c213d096b020..e5287f18e1f7a 100644 --- a/src/ui/public/error_auto_create_index/error_auto_create_index.js +++ b/src/ui/public/error_auto_create_index/error_auto_create_index.ts @@ -24,16 +24,20 @@ import uiRoutes from '../routes'; import template from './error_auto_create_index.html'; -uiRoutes - .when('/error/action.auto_create_index', { - template, - k7Breadcrumbs: () => [{ text: i18n.translate('common.ui.errorAutoCreateIndex.breadcrumbs.errorText', { defaultMessage: 'Error' }) }], - }); +uiRoutes.when('/error/action.auto_create_index', { + template, + k7Breadcrumbs: () => [ + { + text: i18n.translate('common.ui.errorAutoCreateIndex.breadcrumbs.errorText', { + defaultMessage: 'Error', + }), + }, + ], +}); -export function isAutoCreateIndexError(error) { +export function isAutoCreateIndexError(error: object) { return ( - get(error, 'res.status') === 503 && - get(error, 'body.code') === 'ES_AUTO_CREATE_INDEX_ERROR' + get(error, 'res.status') === 503 && get(error, 'body.code') === 'ES_AUTO_CREATE_INDEX_ERROR' ); } diff --git a/src/ui/public/error_auto_create_index/index.js b/src/ui/public/error_auto_create_index/index.ts similarity index 100% rename from src/ui/public/error_auto_create_index/index.js rename to src/ui/public/error_auto_create_index/index.ts diff --git a/src/ui/public/saved_objects/saved_object.js b/src/ui/public/saved_objects/saved_object.js deleted file mode 100644 index ed3bbfe9b0fb1..0000000000000 --- a/src/ui/public/saved_objects/saved_object.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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 _ from 'lodash'; - -export class SavedObject { - constructor(client, { id, type, version, attributes, error, migrationVersion, references } = {}) { - this._client = client; - this.id = id; - this.type = type; - this.attributes = attributes || {}; - this.references = references || []; - this._version = version; - this.migrationVersion = migrationVersion; - if (error) { - this.error = error; - } - } - - get(key) { - return _.get(this.attributes, key); - } - - set(key, value) { - return _.set(this.attributes, key, value); - } - - has(key) { - return _.has(this.attributes, key); - } - - save() { - if (this.id) { - return this._client.update( - this.type, - this.id, - this.attributes, - { - migrationVersion: this.migrationVersion, - references: this.references, - }, - ); - } else { - return this._client.create(this.type, this.attributes, { migrationVersion: this.migrationVersion, references: this.references }); - } - } - - delete() { - return this._client.delete(this.type, this.id); - } -} diff --git a/src/ui/public/saved_objects/saved_object.ts b/src/ui/public/saved_objects/saved_object.ts new file mode 100644 index 0000000000000..a34cc2f150e51 --- /dev/null +++ b/src/ui/public/saved_objects/saved_object.ts @@ -0,0 +1,81 @@ +/* + * 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 { get, has, set } from 'lodash'; +import { + SavedObject as SavedObjectType, + SavedObjectAttributes, +} from '../../../server/saved_objects'; +import { SavedObjectsClient } from './saved_objects_client'; + +export class SavedObject { + public attributes: T; + // tslint:disable-next-line variable-name We want to use the same interface this class had in JS + public _version?: SavedObjectType['version']; + public id: SavedObjectType['id']; + public type: SavedObjectType['type']; + public migrationVersion: SavedObjectType['migrationVersion']; + public error: SavedObjectType['error']; + public references: SavedObjectType['references']; + + constructor( + private client: SavedObjectsClient, + { id, type, version, attributes, error, references, migrationVersion }: SavedObjectType + ) { + this.id = id; + this.type = type; + this.attributes = attributes || {}; + this.references = references || []; + this._version = version; + this.migrationVersion = migrationVersion; + if (error) { + this.error = error; + } + } + + public get(key: string): any { + return get(this.attributes, key); + } + + public set(key: string, value: any): T { + return set(this.attributes, key, value); + } + + public has(key: string): boolean { + return has(this.attributes, key); + } + + public save() { + if (this.id) { + return this.client.update(this.type, this.id, this.attributes, { + migrationVersion: this.migrationVersion, + references: this.references, + }); + } else { + return this.client.create(this.type, this.attributes, { + migrationVersion: this.migrationVersion, + references: this.references, + }); + } + } + + public delete() { + return this.client.delete(this.type, this.id); + } +} diff --git a/src/ui/public/saved_objects/saved_objects_client.js b/src/ui/public/saved_objects/saved_objects_client.js deleted file mode 100644 index 0ee4c2ff47ad3..0000000000000 --- a/src/ui/public/saved_objects/saved_objects_client.js +++ /dev/null @@ -1,260 +0,0 @@ -/* - * 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 _ from 'lodash'; - -import { resolve as resolveUrl } from 'url'; -import { keysToSnakeCaseShallow, keysToCamelCaseShallow } from '../../../utils/case_conversion'; -import { SavedObject } from './saved_object'; -import { isAutoCreateIndexError, showAutoCreateIndexErrorPage } from '../error_auto_create_index'; -import { kfetch } from 'ui/kfetch'; - -const join = (...uriComponents) => ( - uriComponents.filter(Boolean).map(encodeURIComponent).join('/') -); - -/** - * Interval that requests are batched for - * @type {integer} - */ -const BATCH_INTERVAL = 100; - -const API_BASE_URL = '/api/saved_objects/'; - -export class SavedObjectsClient { - constructor() { - this.batchQueue = []; - } - - /** - * Persists an object - * - * @param {string} type - * @param {object} [attributes={}] - * @param {object} [options={}] - * @property {string} [options.id] - force id on creation, not recommended - * @property {boolean} [options.overwrite=false] - * @property {object} [options.migrationVersion] - * @property {array} [options.references] [{ name, type, id }] - * @returns {promise} - SavedObject({ id, type, version, attributes }) - */ - create = (type, attributes = {}, options = {}) => { - if (!type || !attributes) { - return Promise.reject(new Error('requires type and attributes')); - } - - const path = this._getPath([type, options.id]); - const query = _.pick(options, ['overwrite']); - - return this - ._request({ - method: 'POST', - path, - query, - body: { - attributes, - migrationVersion: options.migrationVersion, - references: options.references, - }, - }) - .catch(error => { - if (isAutoCreateIndexError(error)) { - return showAutoCreateIndexErrorPage(); - } - - throw error; - }) - .then(resp => this._createSavedObject(resp)); - } - - /** - * Creates multiple documents at once - * - * @param {array} objects - [{ type, id, attributes, references, migrationVersion }] - * @param {object} [options={}] - * @property {boolean} [options.overwrite=false] - * @returns {promise} - { savedObjects: [{ id, type, version, attributes, error: { message } }]} - */ - bulkCreate = (objects = [], options = {}) => { - const path = this._getPath(['_bulk_create']); - const query = _.pick(options, ['overwrite']); - - return this._request({ method: 'POST', path, query, body: objects }).then(resp => { - resp.saved_objects = resp.saved_objects.map(d => this._createSavedObject(d)); - return keysToCamelCaseShallow(resp); - }); - } - - /** - * Deletes an object - * - * @param {string} type - * @param {string} id - * @returns {promise} - */ - delete = (type, id) => { - if (!type || !id) { - return Promise.reject(new Error('requires type and id')); - } - - return this._request({ method: 'DELETE', path: this._getPath([type, id]) }); - } - - /** - * Search for objects - * - * @param {object} [options={}] - * @property {string} options.type - * @property {string} options.search - * @property {string} options.defaultSearchOperator - * @property {string} options.searchFields - see Elasticsearch Simple Query String - * Query field argument for more information - * @property {integer} [options.page=1] - * @property {integer} [options.perPage=20] - * @property {array} options.fields - * @property {object} [options.hasReference] - { type, id } - * @returns {promise} - { savedObjects: [ SavedObject({ id, type, version, attributes }) ]} - */ - find = (options = {}) => { - const path = this._getPath(['_find']); - const query = keysToSnakeCaseShallow(options); - - return this._request({ method: 'GET', path, query }).then(resp => { - resp.saved_objects = resp.saved_objects.map(d => this._createSavedObject(d)); - return keysToCamelCaseShallow(resp); - }); - } - - /** - * Fetches a single object - * - * @param {string} type - * @param {string} id - * @returns {promise} - SavedObject({ id, type, version, attributes }) - */ - get = (type, id) => { - if (!type || !id) { - return Promise.reject(new Error('requires type and id')); - } - - return new Promise((resolve, reject) => { - this.batchQueue.push({ type, id, resolve, reject }); - this._processBatchQueue(); - }); - } - - /** - * Returns an array of objects by id - * - * @param {array} objects - an array ids, or an array of objects containing id and optionally type - * @returns {promise} - { savedObjects: [ SavedObject({ id, type, version, attributes }) ] } - * @example - * - * bulkGet([ - * { id: 'one', type: 'config' }, - * { id: 'foo', type: 'index-pattern' } - * ]) - */ - bulkGet = (objects = []) => { - const path = this._getPath(['_bulk_get']); - const filteredObjects = objects.map(obj => _.pick(obj, ['id', 'type'])); - - return this._request({ method: 'POST', path, body: filteredObjects }).then(resp => { - resp.saved_objects = resp.saved_objects.map(d => this._createSavedObject(d)); - return keysToCamelCaseShallow(resp); - }); - } - - /** - * Updates an object - * - * @param {string} type - * @param {string} id - * @param {object} attributes - * @param {object} options - * @prop {integer} options.version - ensures version matches that of persisted object - * @prop {object} options.migrationVersion - The optional migrationVersion of this document - * @prop {array} option.references - the references of the saved object - * @returns {promise} - */ - update(type, id, attributes, { version, migrationVersion, references } = {}) { - if (!type || !id || !attributes) { - return Promise.reject(new Error('requires type, id and attributes')); - } - - const path = this._getPath([type, id]); - const body = { - attributes, - migrationVersion, - references, - version - }; - - return this._request({ method: 'PUT', path, body }).then(resp => { - return this._createSavedObject(resp); - }); - } - - /** - * Throttled processing of get requests into bulk requests at 100ms interval - */ - _processBatchQueue = _.throttle(() => { - const queue = _.cloneDeep(this.batchQueue); - this.batchQueue = []; - - this.bulkGet(queue).then(({ savedObjects }) => { - queue.forEach((queueItem) => { - const foundObject = savedObjects.find(savedObject => { - return savedObject.id === queueItem.id & savedObject.type === queueItem.type; - }); - - if (!foundObject) { - return queueItem.resolve(this._createSavedObject(_.pick(queueItem, ['id', 'type']))); - } - - queueItem.resolve(foundObject); - }); - }).catch((err) => { - queue.forEach((queueItem) => { - queueItem.reject(err); - }); - }); - - }, BATCH_INTERVAL, { leading: false }); - - _createSavedObject(options) { - return new SavedObject(this, options); - } - - _getPath(path) { - if (!path) { - return API_BASE_URL; - } - - return resolveUrl(API_BASE_URL, join(...path)); - } - - _request({ method, path, query, body }) { - if (method === 'GET' && body) { - return Promise.reject(new Error('body not permitted for GET requests')); - } - - return kfetch({ method, pathname: path, query, body: JSON.stringify(body) }); - } -} diff --git a/src/ui/public/saved_objects/saved_objects_client.ts b/src/ui/public/saved_objects/saved_objects_client.ts new file mode 100644 index 0000000000000..10f082ee4f439 --- /dev/null +++ b/src/ui/public/saved_objects/saved_objects_client.ts @@ -0,0 +1,352 @@ +/* + * 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 { cloneDeep, pick, throttle } from 'lodash'; + +import { kfetch } from 'ui/kfetch'; +import { resolve as resolveUrl } from 'url'; +import { + FindOptions, + MigrationVersion, + SavedObject as PlainSavedObject, + SavedObjectAttributes, + SavedObjectReference, + SavedObjectsClient as SavedObjectsApi, +} from '../../../server/saved_objects'; +import { keysToCamelCaseShallow, keysToSnakeCaseShallow } from '../../../utils/case_conversion'; +import { isAutoCreateIndexError, showAutoCreateIndexErrorPage } from '../error_auto_create_index'; +import { SavedObject } from './saved_object'; + +interface RequestParams { + method: 'POST' | 'GET' | 'PUT' | 'DELETE'; + path: string; + query?: { [paramName: string]: string }; + body?: object; +} + +interface CreateOptions { + id?: string; + overwrite?: boolean; + migrationVersion?: MigrationVersion; + references?: SavedObjectReference[]; +} + +interface UpdateOptions { + version?: string; + migrationVersion?: MigrationVersion; + references?: SavedObjectReference[]; +} + +interface FindResults { + savedObjects: Array>; + total: number; + perPage: number; + page: number; +} + +interface BatchQueueEntry { + type: string; + id: string; + resolve: () => void; + reject: () => void; +} + +const join = (...uriComponents: Array) => + uriComponents + .filter((comp): comp is string => Boolean(comp)) + .map(encodeURIComponent) + .join('/'); + +/** + * Interval that requests are batched for + * @type {integer} + */ +const BATCH_INTERVAL = 100; + +const API_BASE_URL = '/api/saved_objects/'; + +export class SavedObjectsClient { + /** + * Throttled processing of get requests into bulk requests at 100ms interval + */ + // TODO: Type this function + private processBatchQueue = throttle( + () => { + const queue = cloneDeep(this.batchQueue); + this.batchQueue = []; + + this.bulkGet(queue) + .then(({ savedObjects }) => { + queue.forEach(queueItem => { + const foundObject = savedObjects.find(savedObject => { + return savedObject.id === queueItem.id && savedObject.type === queueItem.type; + }); + + if (!foundObject) { + return queueItem.resolve(this.createSavedObject(pick(queueItem, ['id', 'type']))); + } + + queueItem.resolve(foundObject); + }); + }) + .catch(err => { + queue.forEach(queueItem => { + queueItem.reject(err); + }); + }); + }, + BATCH_INTERVAL, + { leading: false } + ); + + private batchQueue: BatchQueueEntry[]; + + constructor() { + this.batchQueue = []; + } + + /** + * Persists an object + * + * @param {string} type + * @param {object} [attributes={}] + * @param {object} [options={}] + * @property {string} [options.id] - force id on creation, not recommended + * @property {boolean} [options.overwrite=false] + * @property {object} [options.migrationVersion] + * @returns {promise} - SavedObject({ id, type, version, attributes }) + */ + public create = ( + type: string, + attributes: T, + options: CreateOptions = {} + ): Promise> => { + if (!type || !attributes) { + return Promise.reject(new Error('requires type and attributes')); + } + + const path = this.getPath([type, options.id]); + const query = { + overwrite: options.overwrite, + }; + + const createRequest: ReturnType = this.request({ + method: 'POST', + path, + query, + body: { + attributes, + migrationVersion: options.migrationVersion, + references: options.references, + }, + }); + + return ( + createRequest + .catch((error: object) => { + if (isAutoCreateIndexError(error)) { + return showAutoCreateIndexErrorPage(); + } + + throw error; + }) + // TODO: Why is this `void` in here? + .then(resp => this.createSavedObject(resp)) + ); + }; + + /** + * Creates multiple documents at once + * + * @param {array} objects - [{ type, id, attributes, references, migrationVersion }] + * @param {object} [options={}] + * @property {boolean} [options.overwrite=false] + * @returns {promise} - { savedObjects: [{ id, type, version, attributes, error: { message } }]} + */ + public bulkCreate = (objects = [], options = {}) => { + const path = this.getPath(['_bulk_create']); + const query = pick(options, ['overwrite']); + + const request: ReturnType = this.request({ + method: 'POST', + path, + query, + body: objects, + }); + return request.then(resp => { + resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d)); + return keysToCamelCaseShallow(resp); + }); + }; + + /** + * Deletes an object + * + * @param type + * @param id + * @returns + */ + public delete = (type: string, id: string): ReturnType => { + if (!type || !id) { + return Promise.reject(new Error('requires type and id')); + } + + return this.request({ method: 'DELETE', path: this.getPath([type, id]) }); + }; + + /** + * Search for objects + * + * @param {object} [options={}] + * @property {string} options.type + * @property {string} options.search + * @property {string} options.searchFields - see Elasticsearch Simple Query String + * Query field argument for more information + * @property {integer} [options.page=1] + * @property {integer} [options.perPage=20] + * @property {array} options.fields + * @property {object} [options.hasReference] - { type, id } + * @returns {promise} - { savedObjects: [ SavedObject({ id, type, version, attributes }) ]} + */ + public find = (options: FindOptions = {}): Promise => { + const path = this.getPath(['_find']); + const query = keysToSnakeCaseShallow(options); + + const request: ReturnType = this.request({ + method: 'GET', + path, + query, + }); + return request.then(resp => { + resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d)); + return keysToCamelCaseShallow(resp) as FindResults; + }); + }; + + /** + * Fetches a single object + * + * @param {string} type + * @param {string} id + * @returns {promise} - SavedObject({ id, type, version, attributes }) + */ + public get = ( + type: string, + id: string + ): Promise> => { + if (!type || !id) { + return Promise.reject(new Error('requires type and id')); + } + + return new Promise((resolve, reject) => { + this.batchQueue.push({ type, id, resolve, reject }); + this.processBatchQueue(); + }); + }; + + /** + * Returns an array of objects by id + * + * @param {array} objects - an array ids, or an array of objects containing id and optionally type + * @returns {promise} - { savedObjects: [ SavedObject({ id, type, version, attributes }) ] } + * @example + * + * bulkGet([ + * { id: 'one', type: 'config' }, + * { id: 'foo', type: 'index-pattern' } + * ]) + */ + // TODO: Add return type + public bulkGet = (objects: Array<{ id: string; type: string }> = []) => { + const path = this.getPath(['_bulk_get']); + const filteredObjects = objects.map(obj => pick(obj, ['id', 'type'])); + + const request: ReturnType = this.request({ + method: 'POST', + path, + body: filteredObjects, + }); + return request.then(resp => { + resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d)); + return keysToCamelCaseShallow(resp); + }); + }; + + /** + * Updates an object + * + * @param {string} type + * @param {string} id + * @param {object} attributes + * @param {object} options + * @prop {integer} options.version - ensures version matches that of persisted object + * @prop {object} options.migrationVersion - The optional migrationVersion of this document + * @returns {promise} + */ + public update( + type: string, + id: string, + attributes: T, + { version, migrationVersion, references }: UpdateOptions = {} + ): Promise> { + if (!type || !id || !attributes) { + return Promise.reject(new Error('requires type, id and attributes')); + } + + const path = this.getPath([type, id]); + const body = { + attributes, + migrationVersion, + references, + version, + }; + + const request: ReturnType = this.request({ + method: 'PUT', + path, + body, + }); + // TODO: can we somehow get T into the above ReturnType definition, so it knows it's actually ReturnTypeupdate> + return request.then(resp => { + return this.createSavedObject(resp); + }); + } + + private createSavedObject( + options: PlainSavedObject + ): SavedObject { + return new SavedObject(this, options); + } + + private getPath(path: Array): string { + if (!path) { + return API_BASE_URL; + } + + return resolveUrl(API_BASE_URL, join(...path)); + } + + private request({ method, path, query, body }: RequestParams) { + if (method === 'GET' && body) { + return Promise.reject(new Error('body not permitted for GET requests')); + } + + return kfetch({ method, pathname: path, query, body: JSON.stringify(body) }); + } +} From 526046c80b5e173aa334def745bf319f5213c954 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Mon, 4 Feb 2019 15:02:46 +0100 Subject: [PATCH 10/35] Move more files to TS --- src/ui/public/chrome/index.d.ts | 2 + ...ct_by_title.js => find_object_by_title.ts} | 39 ++++++++++++------- .../saved_objects/{index.js => index.ts} | 2 + 3 files changed, 29 insertions(+), 14 deletions(-) rename src/ui/public/saved_objects/{find_object_by_title.js => find_object_by_title.ts} (62%) rename src/ui/public/saved_objects/{index.js => index.ts} (97%) diff --git a/src/ui/public/chrome/index.d.ts b/src/ui/public/chrome/index.d.ts index 6b6a40270e1e7..c93e8a693ab70 100644 --- a/src/ui/public/chrome/index.d.ts +++ b/src/ui/public/chrome/index.d.ts @@ -18,6 +18,7 @@ */ import { Brand } from '../../../core/public/chrome'; +import { SavedObjectsClient } from '../saved_objects'; import { BreadcrumbsApi } from './api/breadcrumbs'; import { HelpExtensionApi } from './api/help_extension'; import { ChromeNavLinks } from './api/nav'; @@ -34,6 +35,7 @@ declare interface Chrome extends ChromeNavLinks { getBasePath(): string; getXsrfToken(): string; getKibanaVersion(): string; + getSavedObjectsClient(): SavedObjectsClient; getUiSettingsClient(): any; setVisible(visible: boolean): any; getInjected(key: string, defaultValue?: any): any; diff --git a/src/ui/public/saved_objects/find_object_by_title.js b/src/ui/public/saved_objects/find_object_by_title.ts similarity index 62% rename from src/ui/public/saved_objects/find_object_by_title.js rename to src/ui/public/saved_objects/find_object_by_title.ts index 6b148d615c712..82c8ad440d8ef 100644 --- a/src/ui/public/saved_objects/find_object_by_title.js +++ b/src/ui/public/saved_objects/find_object_by_title.ts @@ -18,6 +18,9 @@ */ import { find } from 'lodash'; +import { SavedObjectAttributes } from 'src/server/saved_objects'; +import { SavedObject } from './saved_object'; +import { SavedObjectsClient } from './saved_objects_client'; /** * Returns an object matching a given title @@ -27,22 +30,30 @@ import { find } from 'lodash'; * @param title {string} * @returns {Promise} */ -export function findObjectByTitle(savedObjectsClient, type, title) { - if (!title) return Promise.resolve(); +export function findObjectByTitle( + savedObjectsClient: SavedObjectsClient, + type: string, + title: string +): Promise | void> { + if (!title) { + return Promise.resolve(); + } // Elastic search will return the most relevant results first, which means exact matches should come // first, and so we shouldn't need to request everything. Using 10 just to be on the safe side. - return savedObjectsClient.find({ - type, - perPage: 10, - search: `"${title}"`, - searchFields: ['title'], - fields: ['title'] - }).then(response => { - const match = find(response.savedObjects, (obj) => { - return obj.get('title').toLowerCase() === title.toLowerCase(); - }); + return savedObjectsClient + .find({ + type, + perPage: 10, + search: `"${title}"`, + searchFields: ['title'], + fields: ['title'], + }) + .then(response => { + const match = find(response.savedObjects, obj => { + return obj.get('title').toLowerCase() === title.toLowerCase(); + }); - return match; - }); + return match; + }); } diff --git a/src/ui/public/saved_objects/index.js b/src/ui/public/saved_objects/index.ts similarity index 97% rename from src/ui/public/saved_objects/index.js rename to src/ui/public/saved_objects/index.ts index 2571c80750677..9218c91e07f61 100644 --- a/src/ui/public/saved_objects/index.js +++ b/src/ui/public/saved_objects/index.ts @@ -18,7 +18,9 @@ */ export { SavedObjectsClient } from './saved_objects_client'; +// @ts-ignore export { SavedObjectRegistryProvider } from './saved_object_registry'; +// @ts-ignore export { SavedObjectsClientProvider } from './saved_objects_client_provider'; export { SavedObject } from './saved_object'; export { findObjectByTitle } from './find_object_by_title'; From cacc55ffcb0180a2a53a1eee4c6f859927949f5e Mon Sep 17 00:00:00 2001 From: Johannes Reuter Date: Mon, 4 Feb 2019 16:29:53 +0100 Subject: [PATCH 11/35] type saved objects client --- .../service/saved_objects_client.d.ts | 5 ++ src/ui/public/kfetch/kfetch.ts | 2 +- .../saved_objects/saved_objects_client.ts | 51 +++++++++---------- 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/src/server/saved_objects/service/saved_objects_client.d.ts b/src/server/saved_objects/service/saved_objects_client.d.ts index c874c5a9619b2..eeb47cbc3e3e8 100644 --- a/src/server/saved_objects/service/saved_objects_client.d.ts +++ b/src/server/saved_objects/service/saved_objects_client.d.ts @@ -73,6 +73,11 @@ export interface BulkGetResponse { saved_objects: Array>; } +// TODO find better name for that +export interface CamelCaseBulkGetResponse { + savedObjects: Array>; +} + export interface MigrationVersion { [pluginName: string]: string; } diff --git a/src/ui/public/kfetch/kfetch.ts b/src/ui/public/kfetch/kfetch.ts index 01515c9c0e0aa..086692a66261b 100644 --- a/src/ui/public/kfetch/kfetch.ts +++ b/src/ui/public/kfetch/kfetch.ts @@ -24,7 +24,7 @@ import url from 'url'; import chrome from '../chrome'; import { KFetchError } from './kfetch_error'; -interface KFetchQuery { +export interface KFetchQuery { [key: string]: string | number | boolean | undefined; } diff --git a/src/ui/public/saved_objects/saved_objects_client.ts b/src/ui/public/saved_objects/saved_objects_client.ts index 10f082ee4f439..86dbe5f36f43a 100644 --- a/src/ui/public/saved_objects/saved_objects_client.ts +++ b/src/ui/public/saved_objects/saved_objects_client.ts @@ -19,8 +19,6 @@ import { cloneDeep, pick, throttle } from 'lodash'; -import { kfetch } from 'ui/kfetch'; -import { resolve as resolveUrl } from 'url'; import { FindOptions, MigrationVersion, @@ -28,7 +26,14 @@ import { SavedObjectAttributes, SavedObjectReference, SavedObjectsClient as SavedObjectsApi, -} from '../../../server/saved_objects'; +} from 'src/server/saved_objects'; +import { + BulkGetResponse, + CamelCaseBulkGetResponse, +} from 'src/server/saved_objects/service/saved_objects_client'; +import { kfetch } from 'ui/kfetch'; +import { KFetchQuery } from 'ui/kfetch/kfetch'; +import { resolve as resolveUrl } from 'url'; import { keysToCamelCaseShallow, keysToSnakeCaseShallow } from '../../../utils/case_conversion'; import { isAutoCreateIndexError, showAutoCreateIndexErrorPage } from '../error_auto_create_index'; import { SavedObject } from './saved_object'; @@ -36,7 +41,7 @@ import { SavedObject } from './saved_object'; interface RequestParams { method: 'POST' | 'GET' | 'PUT' | 'DELETE'; path: string; - query?: { [paramName: string]: string }; + query?: KFetchQuery; body?: object; } @@ -63,8 +68,8 @@ interface FindResults { interface BatchQueueEntry { type: string; id: string; - resolve: () => void; - reject: () => void; + resolve: (value: SavedObject | PlainSavedObject) => void; + reject: (reason?: any) => void; } const join = (...uriComponents: Array) => @@ -85,7 +90,6 @@ export class SavedObjectsClient { /** * Throttled processing of get requests into bulk requests at 100ms interval */ - // TODO: Type this function private processBatchQueue = throttle( () => { const queue = cloneDeep(this.batchQueue); @@ -157,18 +161,15 @@ export class SavedObjectsClient { }, }); - return ( - createRequest - .catch((error: object) => { - if (isAutoCreateIndexError(error)) { - return showAutoCreateIndexErrorPage(); - } + return createRequest + .then(resp => this.createSavedObject(resp)) + .catch((error: object) => { + if (isAutoCreateIndexError(error)) { + showAutoCreateIndexErrorPage(); + } - throw error; - }) - // TODO: Why is this `void` in here? - .then(resp => this.createSavedObject(resp)) - ); + throw error; + }) as Promise>; }; /** @@ -179,9 +180,9 @@ export class SavedObjectsClient { * @property {boolean} [options.overwrite=false] * @returns {promise} - { savedObjects: [{ id, type, version, attributes, error: { message } }]} */ - public bulkCreate = (objects = [], options = {}) => { + public bulkCreate = (objects = [], options: KFetchQuery = {}) => { const path = this.getPath(['_bulk_create']); - const query = pick(options, ['overwrite']); + const query = pick(options, ['overwrite']) as Pick; const request: ReturnType = this.request({ method: 'POST', @@ -255,7 +256,7 @@ export class SavedObjectsClient { } return new Promise((resolve, reject) => { - this.batchQueue.push({ type, id, resolve, reject }); + this.batchQueue.push({ type, id, resolve, reject } as BatchQueueEntry); this.processBatchQueue(); }); }; @@ -272,7 +273,6 @@ export class SavedObjectsClient { * { id: 'foo', type: 'index-pattern' } * ]) */ - // TODO: Add return type public bulkGet = (objects: Array<{ id: string; type: string }> = []) => { const path = this.getPath(['_bulk_get']); const filteredObjects = objects.map(obj => pick(obj, ['id', 'type'])); @@ -282,9 +282,9 @@ export class SavedObjectsClient { path, body: filteredObjects, }); - return request.then(resp => { + return request.then((resp: BulkGetResponse) => { resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d)); - return keysToCamelCaseShallow(resp); + return keysToCamelCaseShallow(resp) as CamelCaseBulkGetResponse; }); }; @@ -322,10 +322,9 @@ export class SavedObjectsClient { path, body, }); - // TODO: can we somehow get T into the above ReturnType definition, so it knows it's actually ReturnTypeupdate> return request.then(resp => { return this.createSavedObject(resp); - }); + }) as Promise>; } private createSavedObject( From d991a93d1df4075152702569997b987f3b503b94 Mon Sep 17 00:00:00 2001 From: Johannes Reuter Date: Tue, 5 Feb 2019 10:49:07 +0100 Subject: [PATCH 12/35] clean up typings for saved object client --- .../service/saved_objects_client.d.ts | 11 ++++---- src/ui/public/kfetch/index.ts | 2 +- .../saved_objects/saved_objects_client.ts | 27 +++++++++---------- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/server/saved_objects/service/saved_objects_client.d.ts b/src/server/saved_objects/service/saved_objects_client.d.ts index eeb47cbc3e3e8..a7cf9b2217888 100644 --- a/src/server/saved_objects/service/saved_objects_client.d.ts +++ b/src/server/saved_objects/service/saved_objects_client.d.ts @@ -73,11 +73,6 @@ export interface BulkGetResponse { saved_objects: Array>; } -// TODO find better name for that -export interface CamelCaseBulkGetResponse { - savedObjects: Array>; -} - export interface MigrationVersion { [pluginName: string]: string; } @@ -105,6 +100,10 @@ export interface SavedObjectReference { id: string; } +export type SavedObjectsClientCreateResponse = Promise< + SavedObject +>; + export declare class SavedObjectsClient { public static errors: typeof errors; public errors: typeof errors; @@ -115,7 +114,7 @@ export declare class SavedObjectsClient { type: string, attributes: T, options?: CreateOptions - ): Promise>; + ): SavedObjectsClientCreateResponse; public bulkCreate( objects: Array>, options?: CreateOptions diff --git a/src/ui/public/kfetch/index.ts b/src/ui/public/kfetch/index.ts index f871d97f0fd54..234304b5950aa 100644 --- a/src/ui/public/kfetch/index.ts +++ b/src/ui/public/kfetch/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { kfetch, addInterceptor, KFetchOptions } from './kfetch'; +export { kfetch, addInterceptor, KFetchOptions, KFetchQuery } from './kfetch'; export { kfetchAbortable } from './kfetch_abortable'; diff --git a/src/ui/public/saved_objects/saved_objects_client.ts b/src/ui/public/saved_objects/saved_objects_client.ts index 86dbe5f36f43a..bb7e797feb2e3 100644 --- a/src/ui/public/saved_objects/saved_objects_client.ts +++ b/src/ui/public/saved_objects/saved_objects_client.ts @@ -18,6 +18,7 @@ */ import { cloneDeep, pick, throttle } from 'lodash'; +import { resolve as resolveUrl } from 'url'; import { FindOptions, @@ -26,16 +27,11 @@ import { SavedObjectAttributes, SavedObjectReference, SavedObjectsClient as SavedObjectsApi, -} from 'src/server/saved_objects'; -import { - BulkGetResponse, - CamelCaseBulkGetResponse, -} from 'src/server/saved_objects/service/saved_objects_client'; -import { kfetch } from 'ui/kfetch'; -import { KFetchQuery } from 'ui/kfetch/kfetch'; -import { resolve as resolveUrl } from 'url'; +} from '../../../../src/server/saved_objects'; +import { SavedObjectsClientCreateResponse } from '../../../../src/server/saved_objects/service/saved_objects_client'; import { keysToCamelCaseShallow, keysToSnakeCaseShallow } from '../../../utils/case_conversion'; import { isAutoCreateIndexError, showAutoCreateIndexErrorPage } from '../error_auto_create_index'; +import { kfetch, KFetchQuery } from '../kfetch'; import { SavedObject } from './saved_object'; interface RequestParams { @@ -58,8 +54,11 @@ interface UpdateOptions { references?: SavedObjectReference[]; } -interface FindResults { +interface BatchResponse { savedObjects: Array>; +} + +interface FindResults extends BatchResponse { total: number; perPage: number; page: number; @@ -150,7 +149,7 @@ export class SavedObjectsClient { overwrite: options.overwrite, }; - const createRequest: ReturnType = this.request({ + const createRequest: SavedObjectsClientCreateResponse = this.request({ method: 'POST', path, query, @@ -169,7 +168,7 @@ export class SavedObjectsClient { } throw error; - }) as Promise>; + }); }; /** @@ -192,7 +191,7 @@ export class SavedObjectsClient { }); return request.then(resp => { resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d)); - return keysToCamelCaseShallow(resp); + return keysToCamelCaseShallow(resp) as BatchResponse; }); }; @@ -282,9 +281,9 @@ export class SavedObjectsClient { path, body: filteredObjects, }); - return request.then((resp: BulkGetResponse) => { + return request.then(resp => { resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d)); - return keysToCamelCaseShallow(resp) as CamelCaseBulkGetResponse; + return keysToCamelCaseShallow(resp) as BatchResponse; }); }; From 63b6fd943f0e7947e0ef60495eea34920247e0df Mon Sep 17 00:00:00 2001 From: Johannes Reuter Date: Tue, 5 Feb 2019 11:09:12 +0100 Subject: [PATCH 13/35] tie typings form server and client for saved objects together --- src/server/saved_objects/index.d.ts | 1 - src/server/saved_objects/service/index.d.ts | 3 +++ .../saved_objects/service/saved_objects_client.d.ts | 12 ++++++------ src/ui/public/saved_objects/saved_objects_client.ts | 13 ++++++++----- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/server/saved_objects/index.d.ts b/src/server/saved_objects/index.d.ts index c62cad7a6d12b..8e6df957b8a6e 100644 --- a/src/server/saved_objects/index.d.ts +++ b/src/server/saved_objects/index.d.ts @@ -18,7 +18,6 @@ */ export { - FindOptions, MigrationVersion, SavedObject, SavedObjectAttributes, diff --git a/src/server/saved_objects/service/index.d.ts b/src/server/saved_objects/service/index.d.ts index 550c601e3e432..47f3f382c9daf 100644 --- a/src/server/saved_objects/service/index.d.ts +++ b/src/server/saved_objects/service/index.d.ts @@ -21,6 +21,9 @@ export { SavedObjectsService } from './create_saved_objects_service'; export { SavedObjectsClientWrapperFactory } from './lib'; export { FindOptions, + GetResponse, + UpdateResponse, + CreateResponse, MigrationVersion, SavedObject, SavedObjectAttributes, diff --git a/src/server/saved_objects/service/saved_objects_client.d.ts b/src/server/saved_objects/service/saved_objects_client.d.ts index a7cf9b2217888..2d823276a6f76 100644 --- a/src/server/saved_objects/service/saved_objects_client.d.ts +++ b/src/server/saved_objects/service/saved_objects_client.d.ts @@ -100,9 +100,9 @@ export interface SavedObjectReference { id: string; } -export type SavedObjectsClientCreateResponse = Promise< - SavedObject ->; +export type GetResponse = SavedObject; +export type CreateResponse = SavedObject; +export type UpdateResponse = SavedObject; export declare class SavedObjectsClient { public static errors: typeof errors; @@ -114,7 +114,7 @@ export declare class SavedObjectsClient { type: string, attributes: T, options?: CreateOptions - ): SavedObjectsClientCreateResponse; + ): Promise>; public bulkCreate( objects: Array>, options?: CreateOptions @@ -131,11 +131,11 @@ export declare class SavedObjectsClient { type: string, id: string, options?: BaseOptions - ): Promise>; + ): Promise>; public update( type: string, id: string, attributes: Partial, options?: UpdateOptions - ): Promise>; + ): Promise>; } diff --git a/src/ui/public/saved_objects/saved_objects_client.ts b/src/ui/public/saved_objects/saved_objects_client.ts index bb7e797feb2e3..6927b69c11b15 100644 --- a/src/ui/public/saved_objects/saved_objects_client.ts +++ b/src/ui/public/saved_objects/saved_objects_client.ts @@ -21,14 +21,17 @@ import { cloneDeep, pick, throttle } from 'lodash'; import { resolve as resolveUrl } from 'url'; import { - FindOptions, MigrationVersion, SavedObject as PlainSavedObject, SavedObjectAttributes, SavedObjectReference, SavedObjectsClient as SavedObjectsApi, } from '../../../../src/server/saved_objects'; -import { SavedObjectsClientCreateResponse } from '../../../../src/server/saved_objects/service/saved_objects_client'; +import { + CreateResponse, + FindOptions, + UpdateResponse, +} from '../../../../src/server/saved_objects/service'; import { keysToCamelCaseShallow, keysToSnakeCaseShallow } from '../../../utils/case_conversion'; import { isAutoCreateIndexError, showAutoCreateIndexErrorPage } from '../error_auto_create_index'; import { kfetch, KFetchQuery } from '../kfetch'; @@ -149,7 +152,7 @@ export class SavedObjectsClient { overwrite: options.overwrite, }; - const createRequest: SavedObjectsClientCreateResponse = this.request({ + const createRequest: Promise> = this.request({ method: 'POST', path, query, @@ -316,14 +319,14 @@ export class SavedObjectsClient { version, }; - const request: ReturnType = this.request({ + const request: Promise> = this.request({ method: 'PUT', path, body, }); return request.then(resp => { return this.createSavedObject(resp); - }) as Promise>; + }); } private createSavedObject( From ddbe49e36a597db80ba2116246978ad86cf1b15a Mon Sep 17 00:00:00 2001 From: Johannes Reuter Date: Tue, 5 Feb 2019 14:31:08 +0100 Subject: [PATCH 14/35] add types for saved_object_finder --- .../service/saved_objects_client.d.ts | 1 + src/ui/public/registry/vis_types.d.ts | 22 ++ ...ject_finder.js => saved_object_finder.tsx} | 270 ++++++++++-------- 3 files changed, 179 insertions(+), 114 deletions(-) create mode 100644 src/ui/public/registry/vis_types.d.ts rename src/ui/public/saved_objects/components/{saved_object_finder.js => saved_object_finder.tsx} (59%) diff --git a/src/server/saved_objects/service/saved_objects_client.d.ts b/src/server/saved_objects/service/saved_objects_client.d.ts index 2d823276a6f76..6090337639d54 100644 --- a/src/server/saved_objects/service/saved_objects_client.d.ts +++ b/src/server/saved_objects/service/saved_objects_client.d.ts @@ -50,6 +50,7 @@ export interface FindOptions extends BaseOptions { search?: string; searchFields?: string[]; hasReference?: { type: string; id: string }; + defaultSearchOperator?: 'AND' | 'OR'; } export interface FindResponse { diff --git a/src/ui/public/registry/vis_types.d.ts b/src/ui/public/registry/vis_types.d.ts new file mode 100644 index 0000000000000..efabe9af130dc --- /dev/null +++ b/src/ui/public/registry/vis_types.d.ts @@ -0,0 +1,22 @@ +/* + * 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 { UIRegistry } from './_registry'; + +declare type VisTypesRegistryProvider = UIRegistry & { byName: any[] }; diff --git a/src/ui/public/saved_objects/components/saved_object_finder.js b/src/ui/public/saved_objects/components/saved_object_finder.tsx similarity index 59% rename from src/ui/public/saved_objects/components/saved_object_finder.js rename to src/ui/public/saved_objects/components/saved_object_finder.tsx index 395a24a3b5520..e464a0af43130 100644 --- a/src/ui/public/saved_objects/components/saved_object_finder.js +++ b/src/ui/public/saved_objects/components/saved_object_finder.tsx @@ -18,22 +18,111 @@ */ 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, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; +// TODO: Remove once typescript definitions are in EUI +declare module '@elastic/eui' { + export const EuiBasicTable: React.SFC; +} +import { EuiBasicTable, EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiLink } 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 { VisTypesRegistryProvider } from '../../registry/vis_types'; +import { SavedObject } from '../saved_object'; + +interface SavedObjectFinderUIState { + items: any[]; + filter: any; + isFetchingItems: boolean; + page: number; + perPage: number; + sortField?: string; + sortDirection?: Direction; +} + +interface SavedObjectFinderUIProps extends InjectedIntlProps { + callToActionButton?: React.ReactNode; + onChoose?: (id: SavedObject['id'], type: SavedObject['type']) => void; + makeUrl?: (id: SavedObject['id']) => void; + noItemsMessage?: React.ReactNode; + savedObjectType: 'visualization' | 'search'; + visTypes?: VisTypesRegistryProvider; +} + +// TODO there should be a Type in EUI for that, replace if it exists +interface TableCriteria { + page: { index: number; size: number }; + sort?: { + field?: string; + direction?: Direction; + }; +} + +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 isMounted: boolean = false; + + private debouncedFetch = _.debounce(async (filter: any) => { + 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 => { + const typeName = JSON.parse(savedObject.attributes.visState).type; + const visType = 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); + + constructor(props: SavedObjectFinderUIProps) { super(props); this.state = { @@ -45,52 +134,53 @@ class SavedObjectFinderUI extends React.Component { }; } - componentWillUnmount() { - this._isMounted = false; + public componentWillUnmount() { + this.isMounted = false; this.debouncedFetch.cancel(); } - componentDidMount() { - this._isMounted = true; + public componentDidMount() { + this.isMounted = true; this.fetchItems(); } - onTableChange = ({ page, sort = {} }) => { - let { - field: sortField, - direction: sortDirection, - } = sort; + public onTableChange = ({ page, sort = {} }: TableCriteria) => { + 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({ + this.setState(() => ({ page: page.index, perPage: page.size, 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 = () => { + public 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 +194,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 = ( - - {this.props.callToActionButton} - - ); - } + public fetchItems = () => { + this.setState( + { + isFetchingItems: true, + }, + this.debouncedFetch.bind(null, this.state.filter) + ); + }; + public renderSearchBar() { return ( @@ -173,29 +216,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" /> - {actionBtn} - + {this.props.callToActionButton && ( + {this.props.callToActionButton} + )} ); } - renderTable() { + public 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?: TableCriteria['sort'] } = {}; if (this.state.sortField) { sorting.sort = { field: this.state.sortField, @@ -210,11 +258,8 @@ class SavedObjectFinderUI extends React.Component { defaultMessage: 'Title', }), sortable: true, - render: (title, record) => { - const { - onChoose, - makeUrl - } = this.props; + render: (title: string, record: SavedObject) => { + const { onChoose, makeUrl } = this.props; if (!onChoose && !makeUrl) { return {title}; @@ -222,15 +267,21 @@ class SavedObjectFinderUI extends React.Component { return ( { 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} ); - } - } + }, + }, ]; const items = this.state.items.length === 0 ? [] : this.getPageOfItems(); return ( @@ -246,7 +297,7 @@ class SavedObjectFinderUI extends React.Component { ); } - render() { + public render() { return ( {this.renderSearchBar()} @@ -256,13 +307,4 @@ class SavedObjectFinderUI extends React.Component { } } -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); From b04e950ff272539c726e3b610970ef15e3648a7e Mon Sep 17 00:00:00 2001 From: Johannes Reuter Date: Tue, 5 Feb 2019 14:47:58 +0100 Subject: [PATCH 15/35] remove unnecessary setState updater function --- .../public/saved_objects/components/saved_object_finder.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/public/saved_objects/components/saved_object_finder.tsx b/src/ui/public/saved_objects/components/saved_object_finder.tsx index e464a0af43130..acb99792a6973 100644 --- a/src/ui/public/saved_objects/components/saved_object_finder.tsx +++ b/src/ui/public/saved_objects/components/saved_object_finder.tsx @@ -159,12 +159,12 @@ class SavedObjectFinderUI extends React.Component< sortDirection = undefined; } - this.setState(() => ({ + this.setState({ page: page.index, perPage: page.size, sortField, sortDirection, - })); + }); }; // server-side paging not supported From 93e1d77d2335ea97c9b649d6e0b5d8f71c60e6e7 Mon Sep 17 00:00:00 2001 From: Johannes Reuter Date: Tue, 5 Feb 2019 16:20:07 +0100 Subject: [PATCH 16/35] add typings for saved_object_client tests and fix test cases --- ...t.test.js => saved_objects_client.test.ts} | 243 +++++++++--------- .../saved_objects/saved_objects_client.ts | 7 +- 2 files changed, 123 insertions(+), 127 deletions(-) rename src/ui/public/saved_objects/{__tests__/saved_objects_client.test.js => saved_objects_client.test.ts} (60%) diff --git a/src/ui/public/saved_objects/__tests__/saved_objects_client.test.js b/src/ui/public/saved_objects/saved_objects_client.test.ts similarity index 60% rename from src/ui/public/saved_objects/__tests__/saved_objects_client.test.js rename to src/ui/public/saved_objects/saved_objects_client.test.ts index 6819e9b7c4ad5..b2ff9f8d166e1 100644 --- a/src/ui/public/saved_objects/__tests__/saved_objects_client.test.js +++ b/src/ui/public/saved_objects/saved_objects_client.test.ts @@ -19,79 +19,41 @@ jest.mock('ui/kfetch', () => ({})); -import sinon from 'sinon'; +// @ts-ignore import expect from 'expect.js'; -import { SavedObjectsClient } from '../saved_objects_client'; -import { SavedObject } from '../saved_object'; +import * as sinon from 'sinon'; +import { FindOptions } from '../../../server/saved_objects/service'; +import { SavedObject } from './saved_object'; +import { SavedObjectsClient } from './saved_objects_client'; -describe('SavedObjectsClient', () => { +describe.only('SavedObjectsClient', () => { const doc = { id: 'AVwSwFxtcMV38qjDZoQg', type: 'config', attributes: { title: 'Example title' }, - version: 'foo' + version: 'foo', }; - let kfetchStub; - let savedObjectsClient; + let kfetchStub: sinon.SinonStub; + let savedObjectsClient: SavedObjectsClient; beforeEach(() => { kfetchStub = sinon.stub(); - require('ui/kfetch').kfetch = async (...args) => { + require('ui/kfetch').kfetch = async (...args: any[]) => { return kfetchStub(...args); }; savedObjectsClient = new SavedObjectsClient(); }); - describe('#_getPath', () => { - test('returns without arguments', () => { - const path = savedObjectsClient._getPath(); - const expected = `/api/saved_objects/`; - - expect(path).to.be(expected); - }); - - test('appends path', () => { - const path = savedObjectsClient._getPath(['some', 'path']); - const expected = `/api/saved_objects/some/path`; - - expect(path).to.be(expected); - }); - }); - - describe('#_request', () => { - const body = { foo: 'Foo', bar: 'Bar' }; - - test('passes options to kfetch', () => { - kfetchStub.withArgs({ - method: 'POST', - pathname: '/api/path', - query: undefined, - body: JSON.stringify(body) - }).returns(Promise.resolve({})); - - savedObjectsClient._request({ method: 'POST', path: '/api/path', body }); - - sinon.assert.calledOnce(kfetchStub); - }); - - test('throws error when body is provided for GET', async () => { - try { - await savedObjectsClient._request({ method: 'GET', path: '/api/path', body }); - expect().fail('should have error'); - } catch (e) { - expect(e.message).to.eql('body not permitted for GET requests'); - } - }); - }); - describe('#get', () => { beforeEach(() => { - kfetchStub.withArgs({ - method: 'POST', - pathname: `/api/saved_objects/_bulk_get`, - query: undefined, - body: sinon.match.any - }).returns(Promise.resolve({ saved_objects: [doc] })); + kfetchStub + .withArgs({ + method: 'POST', + pathname: `/api/saved_objects/_bulk_get`, + query: undefined, + body: sinon.match.any, + }) + .returns(Promise.resolve({ saved_objects: [doc] })); }); test('returns a promise', () => { @@ -100,7 +62,7 @@ describe('SavedObjectsClient', () => { test('requires type', async () => { try { - await savedObjectsClient.get(); + await savedObjectsClient.get(undefined as any, undefined as any); expect().fail('should have error'); } catch (e) { expect(e.message).to.be('requires type and id'); @@ -109,7 +71,7 @@ describe('SavedObjectsClient', () => { test('requires id', async () => { try { - await savedObjectsClient.get('index-pattern'); + await savedObjectsClient.get('index-pattern', undefined as any); expect().throw('should have error'); } catch (e) { expect(e.message).to.be('requires type and id'); @@ -121,7 +83,6 @@ describe('SavedObjectsClient', () => { expect(response).to.be.a(SavedObject); expect(response.type).to.eql('config'); expect(response.get('title')).to.eql('Example title'); - expect(response._client).to.be.a(SavedObjectsClient); }); test('makes HTTP call', async () => { @@ -130,12 +91,14 @@ describe('SavedObjectsClient', () => { }); test('handles HTTP call when it fails', async () => { - kfetchStub.withArgs({ - method: 'POST', - pathname: `/api/saved_objects/_bulk_get`, - query: undefined, - body: sinon.match.any - }).rejects(new Error('Request failed')); + kfetchStub + .withArgs({ + method: 'POST', + pathname: `/api/saved_objects/_bulk_get`, + query: undefined, + body: sinon.match.any, + }) + .rejects(new Error('Request failed')); try { await savedObjectsClient.get(doc.type, doc.id); throw new Error('should have error'); @@ -147,12 +110,14 @@ describe('SavedObjectsClient', () => { describe('#delete', () => { beforeEach(() => { - kfetchStub.withArgs({ - method: 'DELETE', - pathname: `/api/saved_objects/index-pattern/logstash-*`, - query: undefined, - body: undefined, - }).returns(Promise.resolve({})); + kfetchStub + .withArgs({ + method: 'DELETE', + pathname: `/api/saved_objects/index-pattern/logstash-*`, + query: undefined, + body: undefined, + }) + .returns(Promise.resolve({})); }); test('returns a promise', () => { @@ -161,7 +126,7 @@ describe('SavedObjectsClient', () => { test('requires type', async () => { try { - await savedObjectsClient.delete(); + await savedObjectsClient.delete(undefined as any, undefined as any); expect().throw('should have error'); } catch (e) { expect(e.message).to.be('requires type and id'); @@ -170,7 +135,7 @@ describe('SavedObjectsClient', () => { test('requires id', async () => { try { - await savedObjectsClient.delete('index-pattern'); + await savedObjectsClient.delete('index-pattern', undefined as any); expect().throw('should have error'); } catch (e) { expect(e.message).to.be('requires type and id'); @@ -187,12 +152,14 @@ describe('SavedObjectsClient', () => { const requireMessage = 'requires type, id and attributes'; beforeEach(() => { - kfetchStub.withArgs({ - method: 'PUT', - pathname: `/api/saved_objects/index-pattern/logstash-*`, - query: undefined, - body: sinon.match.any - }).returns(Promise.resolve({ data: 'api-response' })); + kfetchStub + .withArgs({ + method: 'PUT', + pathname: `/api/saved_objects/index-pattern/logstash-*`, + query: undefined, + body: sinon.match.any, + }) + .returns(Promise.resolve({ data: 'api-response' })); }); test('returns a promise', () => { @@ -201,7 +168,7 @@ describe('SavedObjectsClient', () => { test('requires type', async () => { try { - await savedObjectsClient.update(); + await savedObjectsClient.update(undefined as any, undefined as any, undefined as any); expect().throw('should have error'); } catch (e) { expect(e.message).to.be(requireMessage); @@ -210,7 +177,7 @@ describe('SavedObjectsClient', () => { test('requires id', async () => { try { - await savedObjectsClient.update('index-pattern'); + await savedObjectsClient.update('index-pattern', undefined as any, undefined as any); expect().throw('should have error'); } catch (e) { expect(e.message).to.be(requireMessage); @@ -219,7 +186,7 @@ describe('SavedObjectsClient', () => { test('requires attributes', async () => { try { - await savedObjectsClient.update('index-pattern', 'logstash-*'); + await savedObjectsClient.update('index-pattern', 'logstash-*', undefined as any); expect().throw('should have error'); } catch (e) { expect(e.message).to.be(requireMessage); @@ -233,9 +200,12 @@ describe('SavedObjectsClient', () => { savedObjectsClient.update('index-pattern', 'logstash-*', attributes, options); sinon.assert.calledOnce(kfetchStub); - sinon.assert.calledWithExactly(kfetchStub, sinon.match({ - body: JSON.stringify(body) - })); + sinon.assert.calledWithExactly( + kfetchStub, + sinon.match({ + body: JSON.stringify(body), + }) + ); }); }); @@ -243,12 +213,14 @@ describe('SavedObjectsClient', () => { const requireMessage = 'requires type and attributes'; beforeEach(() => { - kfetchStub.withArgs({ - method: 'POST', - pathname: `/api/saved_objects/index-pattern`, - query: undefined, - body: sinon.match.any - }).returns(Promise.resolve({})); + kfetchStub + .withArgs({ + method: 'POST', + pathname: `/api/saved_objects/index-pattern`, + query: undefined, + body: sinon.match.any, + }) + .returns(Promise.resolve({})); }); test('returns a promise', () => { @@ -257,7 +229,7 @@ describe('SavedObjectsClient', () => { test('requires type', async () => { try { - await savedObjectsClient.create(); + await savedObjectsClient.create(undefined as any, undefined as any); expect().throw('should have error'); } catch (e) { expect(e.message).to.be(requireMessage); @@ -267,19 +239,24 @@ describe('SavedObjectsClient', () => { test('allows for id to be provided', () => { const attributes = { foo: 'Foo', bar: 'Bar' }; const path = `/api/saved_objects/index-pattern/myId`; - kfetchStub.withArgs({ - method: 'POST', - pathname: path, - query: undefined, - body: sinon.match.any - }).returns(Promise.resolve({})); + kfetchStub + .withArgs({ + method: 'POST', + pathname: path, + query: undefined, + body: sinon.match.any, + }) + .returns(Promise.resolve({})); savedObjectsClient.create('index-pattern', attributes, { id: 'myId' }); sinon.assert.calledOnce(kfetchStub); - sinon.assert.calledWithExactly(kfetchStub, sinon.match({ - pathname: path - })); + sinon.assert.calledWithExactly( + kfetchStub, + sinon.match({ + pathname: path, + }) + ); }); test('makes HTTP call', () => { @@ -287,21 +264,26 @@ describe('SavedObjectsClient', () => { savedObjectsClient.create('index-pattern', attributes); sinon.assert.calledOnce(kfetchStub); - sinon.assert.calledWithExactly(kfetchStub, sinon.match({ - pathname: sinon.match.string, - body: JSON.stringify({ attributes }), - })); + sinon.assert.calledWithExactly( + kfetchStub, + sinon.match({ + pathname: sinon.match.string, + body: JSON.stringify({ attributes }), + }) + ); }); }); describe('#bulk_create', () => { beforeEach(() => { - kfetchStub.withArgs({ - method: 'POST', - pathname: `/api/saved_objects/_bulk_create`, - query: sinon.match.any, - body: sinon.match.any - }).returns(Promise.resolve({ saved_objects: [doc] })); + kfetchStub + .withArgs({ + method: 'POST', + pathname: `/api/saved_objects/_bulk_create`, + query: sinon.match.any, + body: sinon.match.any, + }) + .returns(Promise.resolve({ saved_objects: [doc] })); }); test('returns a promise', () => { @@ -337,10 +319,13 @@ describe('SavedObjectsClient', () => { savedObjectsClient.find(body); sinon.assert.calledOnce(kfetchStub); - sinon.assert.calledWithExactly(kfetchStub, sinon.match({ - pathname: `/api/saved_objects/_find`, - query: { type: 'index-pattern', invalid: true } - })); + sinon.assert.calledWithExactly( + kfetchStub, + sinon.match({ + pathname: `/api/saved_objects/_find`, + query: { type: 'index-pattern', invalid: true }, + }) + ); }); test('accepts fields', () => { @@ -348,21 +333,27 @@ describe('SavedObjectsClient', () => { savedObjectsClient.find(body); sinon.assert.calledOnce(kfetchStub); - sinon.assert.calledWithExactly(kfetchStub, sinon.match({ - pathname: `/api/saved_objects/_find`, - query: { fields: [ 'title', 'description' ] } - })); + sinon.assert.calledWithExactly( + kfetchStub, + sinon.match({ + pathname: `/api/saved_objects/_find`, + query: { fields: ['title', 'description'] }, + }) + ); }); - test('accepts from/size', () => { - const body = { from: 50, size: 10 }; + test.only('accepts pagination params', () => { + const options: FindOptions = { perPage: 10, page: 6 }; - savedObjectsClient.find(body); + savedObjectsClient.find(options); sinon.assert.calledOnce(kfetchStub); - sinon.assert.alwaysCalledWith(kfetchStub, sinon.match({ - pathname: `/api/saved_objects/_find`, - query: { from: 50, size: 10 } - })); + sinon.assert.alwaysCalledWith( + kfetchStub, + sinon.match({ + pathname: `/api/saved_objects/_find`, + query: { per_page: 10, page: 6 }, + }) + ); }); }); }); diff --git a/src/ui/public/saved_objects/saved_objects_client.ts b/src/ui/public/saved_objects/saved_objects_client.ts index 6927b69c11b15..273791bc79d47 100644 --- a/src/ui/public/saved_objects/saved_objects_client.ts +++ b/src/ui/public/saved_objects/saved_objects_client.ts @@ -51,6 +51,11 @@ interface CreateOptions { references?: SavedObjectReference[]; } +interface BulkCreateOptions extends CreateOptions { + type: string; + attributes: T; +} + interface UpdateOptions { version?: string; migrationVersion?: MigrationVersion; @@ -182,7 +187,7 @@ export class SavedObjectsClient { * @property {boolean} [options.overwrite=false] * @returns {promise} - { savedObjects: [{ id, type, version, attributes, error: { message } }]} */ - public bulkCreate = (objects = [], options: KFetchQuery = {}) => { + public bulkCreate = (objects: BulkCreateOptions[] = [], options: KFetchQuery = {}) => { const path = this.getPath(['_bulk_create']); const query = pick(options, ['overwrite']) as Pick; From e71e163a1b016c51265b3f641c6f2678807cdb5f Mon Sep 17 00:00:00 2001 From: Johannes Reuter Date: Tue, 5 Feb 2019 16:24:54 +0100 Subject: [PATCH 17/35] refine types for saved_object_finder --- .../components/saved_object_finder.tsx | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/src/ui/public/saved_objects/components/saved_object_finder.tsx b/src/ui/public/saved_objects/components/saved_object_finder.tsx index acb99792a6973..64c6f407a18e1 100644 --- a/src/ui/public/saved_objects/components/saved_object_finder.tsx +++ b/src/ui/public/saved_objects/components/saved_object_finder.tsx @@ -23,11 +23,7 @@ import React from 'react'; import { InjectedIntlProps } from 'react-intl'; import chrome from 'ui/chrome'; -// TODO: Remove once typescript definitions are in EUI -declare module '@elastic/eui' { - export const EuiBasicTable: React.SFC; -} - +// @ts-ignore import { EuiBasicTable, EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; import { Direction } from '@elastic/eui/src/services/sort/sort_direction'; import { injectI18n } from '@kbn/i18n/react'; @@ -144,7 +140,16 @@ class SavedObjectFinderUI extends React.Component< this.fetchItems(); } - public onTableChange = ({ page, sort = {} }: TableCriteria) => { + public render() { + return ( + + {this.renderSearchBar()} + {this.renderTable()} + + ); + } + + private onTableChange = ({ page, sort = {} }: TableCriteria) => { let sortField: string | undefined = sort.field; let sortDirection: Direction | undefined = sort.direction; @@ -172,7 +177,7 @@ class SavedObjectFinderUI extends React.Component< // 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 - public getPageOfItems = () => { + private getPageOfItems = () => { // do not sort original list to preserve elasticsearch ranking order const items = this.state.items.slice(); const { sortField } = this.state; @@ -196,7 +201,7 @@ class SavedObjectFinderUI extends React.Component< return items.slice(startIndex, lastIndex); }; - public fetchItems = () => { + private fetchItems = () => { this.setState( { isFetchingItems: true, @@ -205,7 +210,7 @@ class SavedObjectFinderUI extends React.Component< ); }; - public renderSearchBar() { + private renderSearchBar() { return ( @@ -235,7 +240,7 @@ class SavedObjectFinderUI extends React.Component< ); } - public renderTable() { + private renderTable() { const pagination = { pageIndex: this.state.page, pageSize: this.state.perPage, @@ -296,15 +301,6 @@ class SavedObjectFinderUI extends React.Component< /> ); } - - public render() { - return ( - - {this.renderSearchBar()} - {this.renderTable()} - - ); - } } export const SavedObjectFinder = injectI18n(SavedObjectFinderUI); From 41ff9a559c03a1913274ed496687a4dfcde7d580 Mon Sep 17 00:00:00 2001 From: Johannes Reuter Date: Wed, 6 Feb 2019 08:47:42 +0100 Subject: [PATCH 18/35] duplicate case_conversion helpers for the moment --- .../saved_objects/saved_objects_client.ts | 2 +- src/ui/public/utils/case_conversion.ts | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 src/ui/public/utils/case_conversion.ts diff --git a/src/ui/public/saved_objects/saved_objects_client.ts b/src/ui/public/saved_objects/saved_objects_client.ts index 273791bc79d47..94eb0ea3ecd9d 100644 --- a/src/ui/public/saved_objects/saved_objects_client.ts +++ b/src/ui/public/saved_objects/saved_objects_client.ts @@ -32,9 +32,9 @@ import { FindOptions, UpdateResponse, } from '../../../../src/server/saved_objects/service'; -import { keysToCamelCaseShallow, keysToSnakeCaseShallow } from '../../../utils/case_conversion'; import { isAutoCreateIndexError, showAutoCreateIndexErrorPage } from '../error_auto_create_index'; import { kfetch, KFetchQuery } from '../kfetch'; +import { keysToCamelCaseShallow, keysToSnakeCaseShallow } from '../utils/case_conversion'; import { SavedObject } from './saved_object'; interface RequestParams { diff --git a/src/ui/public/utils/case_conversion.ts b/src/ui/public/utils/case_conversion.ts new file mode 100644 index 0000000000000..fe2cb8aa209e5 --- /dev/null +++ b/src/ui/public/utils/case_conversion.ts @@ -0,0 +1,36 @@ +/* + * 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. + */ + +// TODO: This file is copied from src/utils/case_conversion.ts +// because TS-imports from utils in ui are currently not possible. +// When the build process is updated, this file can be removed + +import _ from 'lodash'; + +export function keysToSnakeCaseShallow(object: Record) { + return _.mapKeys(object, (value, key) => { + return _.snakeCase(key); + }); +} + +export function keysToCamelCaseShallow(object: Record) { + return _.mapKeys(object, (value, key) => { + return _.camelCase(key); + }); +} From 3b437e932fdcb81e91e180b9395dc473e9b59d44 Mon Sep 17 00:00:00 2001 From: Johannes Reuter Date: Wed, 6 Feb 2019 09:55:39 +0100 Subject: [PATCH 19/35] rename ismounted flag --- .../saved_objects/components/saved_object_finder.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ui/public/saved_objects/components/saved_object_finder.tsx b/src/ui/public/saved_objects/components/saved_object_finder.tsx index 64c6f407a18e1..ae9d1fe85a231 100644 --- a/src/ui/public/saved_objects/components/saved_object_finder.tsx +++ b/src/ui/public/saved_objects/components/saved_object_finder.tsx @@ -72,7 +72,7 @@ class SavedObjectFinderUI extends React.Component< visTypes: PropTypes.object, }; - private isMounted: boolean = false; + private isComponentMounted: boolean = false; private debouncedFetch = _.debounce(async (filter: any) => { const resp = await chrome.getSavedObjectsClient().find({ @@ -98,7 +98,7 @@ class SavedObjectFinderUI extends React.Component< }); } - if (!this.isMounted) { + if (!this.isComponentMounted) { return; } @@ -131,12 +131,12 @@ class SavedObjectFinderUI extends React.Component< } public componentWillUnmount() { - this.isMounted = false; + this.isComponentMounted = false; this.debouncedFetch.cancel(); } public componentDidMount() { - this.isMounted = true; + this.isComponentMounted = true; this.fetchItems(); } From 46a1c7872ffaff263f89ebc1f4d51b3be9ff330e Mon Sep 17 00:00:00 2001 From: Johannes Reuter Date: Wed, 6 Feb 2019 11:38:34 +0100 Subject: [PATCH 20/35] improve saved_object_finder typings --- .../public/saved_objects/components/saved_object_finder.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/public/saved_objects/components/saved_object_finder.tsx b/src/ui/public/saved_objects/components/saved_object_finder.tsx index ae9d1fe85a231..4bcd3715de02e 100644 --- a/src/ui/public/saved_objects/components/saved_object_finder.tsx +++ b/src/ui/public/saved_objects/components/saved_object_finder.tsx @@ -32,8 +32,8 @@ import { VisTypesRegistryProvider } from '../../registry/vis_types'; import { SavedObject } from '../saved_object'; interface SavedObjectFinderUIState { - items: any[]; - filter: any; + items: Array<{ title: string; id: SavedObject['id']; type: SavedObject['type'] }>; + filter: string; isFetchingItems: boolean; page: number; perPage: number; @@ -74,7 +74,7 @@ class SavedObjectFinderUI extends React.Component< private isComponentMounted: boolean = false; - private debouncedFetch = _.debounce(async (filter: any) => { + private debouncedFetch = _.debounce(async (filter: string) => { const resp = await chrome.getSavedObjectsClient().find({ type: this.props.savedObjectType, fields: ['title', 'visState'], From 8afcd38591e5c4b2344d54b1ff7b53058b39ef40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Wed, 6 Feb 2019 16:18:50 +0100 Subject: [PATCH 21/35] Remove only marker form unit tests Co-Authored-By: flash1293 --- src/ui/public/saved_objects/saved_objects_client.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/public/saved_objects/saved_objects_client.test.ts b/src/ui/public/saved_objects/saved_objects_client.test.ts index b2ff9f8d166e1..460181367daa3 100644 --- a/src/ui/public/saved_objects/saved_objects_client.test.ts +++ b/src/ui/public/saved_objects/saved_objects_client.test.ts @@ -26,7 +26,7 @@ import { FindOptions } from '../../../server/saved_objects/service'; import { SavedObject } from './saved_object'; import { SavedObjectsClient } from './saved_objects_client'; -describe.only('SavedObjectsClient', () => { +describe('SavedObjectsClient', () => { const doc = { id: 'AVwSwFxtcMV38qjDZoQg', type: 'config', From 39b1bf35e779ec0c066223e7a0ba24f8b7eda9a2 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Wed, 6 Feb 2019 18:04:09 +0100 Subject: [PATCH 22/35] Address PR review --- package.json | 1 + .../saved_objects/find_object_by_title.ts | 4 ++-- .../saved_objects_client.test.ts | 5 ++--- .../saved_objects/saved_objects_client.ts | 20 +++++++++---------- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 98352d3f2c4d4..d69d857c3adc7 100644 --- a/package.json +++ b/package.json @@ -270,6 +270,7 @@ "@types/enzyme": "^3.1.12", "@types/eslint": "^4.16.2", "@types/execa": "^0.9.0", + "@types/expect.js": "^0.3.29", "@types/fetch-mock": "7.2.1", "@types/getopts": "^2.0.0", "@types/glob": "^5.0.35", diff --git a/src/ui/public/saved_objects/find_object_by_title.ts b/src/ui/public/saved_objects/find_object_by_title.ts index 82c8ad440d8ef..de6829bdc76e4 100644 --- a/src/ui/public/saved_objects/find_object_by_title.ts +++ b/src/ui/public/saved_objects/find_object_by_title.ts @@ -18,7 +18,7 @@ */ import { find } from 'lodash'; -import { SavedObjectAttributes } from 'src/server/saved_objects'; +import { SavedObjectAttributes } from '../../../server/saved_objects'; import { SavedObject } from './saved_object'; import { SavedObjectsClient } from './saved_objects_client'; @@ -42,7 +42,7 @@ export function findObjectByTitle( // Elastic search will return the most relevant results first, which means exact matches should come // first, and so we shouldn't need to request everything. Using 10 just to be on the safe side. return savedObjectsClient - .find({ + .find({ type, perPage: 10, search: `"${title}"`, diff --git a/src/ui/public/saved_objects/saved_objects_client.test.ts b/src/ui/public/saved_objects/saved_objects_client.test.ts index b2ff9f8d166e1..16f2378c333c5 100644 --- a/src/ui/public/saved_objects/saved_objects_client.test.ts +++ b/src/ui/public/saved_objects/saved_objects_client.test.ts @@ -19,14 +19,13 @@ jest.mock('ui/kfetch', () => ({})); -// @ts-ignore import expect from 'expect.js'; import * as sinon from 'sinon'; import { FindOptions } from '../../../server/saved_objects/service'; import { SavedObject } from './saved_object'; import { SavedObjectsClient } from './saved_objects_client'; -describe.only('SavedObjectsClient', () => { +describe('SavedObjectsClient', () => { const doc = { id: 'AVwSwFxtcMV38qjDZoQg', type: 'config', @@ -342,7 +341,7 @@ describe.only('SavedObjectsClient', () => { ); }); - test.only('accepts pagination params', () => { + test('accepts pagination params', () => { const options: FindOptions = { perPage: 10, page: 6 }; savedObjectsClient.find(options); diff --git a/src/ui/public/saved_objects/saved_objects_client.ts b/src/ui/public/saved_objects/saved_objects_client.ts index 94eb0ea3ecd9d..8b7958cfb5e3f 100644 --- a/src/ui/public/saved_objects/saved_objects_client.ts +++ b/src/ui/public/saved_objects/saved_objects_client.ts @@ -51,7 +51,8 @@ interface CreateOptions { references?: SavedObjectReference[]; } -interface BulkCreateOptions extends CreateOptions { +interface BulkCreateOptions + extends CreateOptions { type: string; attributes: T; } @@ -62,11 +63,12 @@ interface UpdateOptions { references?: SavedObjectReference[]; } -interface BatchResponse { +interface BatchResponse { savedObjects: Array>; } -interface FindResults extends BatchResponse { +interface FindResults + extends BatchResponse { total: number; perPage: number; page: number; @@ -141,7 +143,7 @@ export class SavedObjectsClient { * @property {string} [options.id] - force id on creation, not recommended * @property {boolean} [options.overwrite=false] * @property {object} [options.migrationVersion] - * @returns {promise} - SavedObject({ id, type, version, attributes }) + * @returns */ public create = ( type: string, @@ -232,7 +234,9 @@ export class SavedObjectsClient { * @property {object} [options.hasReference] - { type, id } * @returns {promise} - { savedObjects: [ SavedObject({ id, type, version, attributes }) ]} */ - public find = (options: FindOptions = {}): Promise => { + public find = ( + options: FindOptions = {} + ): Promise> => { const path = this.getPath(['_find']); const query = keysToSnakeCaseShallow(options); @@ -243,7 +247,7 @@ export class SavedObjectsClient { }); return request.then(resp => { resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d)); - return keysToCamelCaseShallow(resp) as FindResults; + return keysToCamelCaseShallow(resp) as FindResults; }); }; @@ -341,10 +345,6 @@ export class SavedObjectsClient { } private getPath(path: Array): string { - if (!path) { - return API_BASE_URL; - } - return resolveUrl(API_BASE_URL, join(...path)); } From 0e0c428bb7a3ebdee1b4e4353e54fe70c8b946fe Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Wed, 6 Feb 2019 18:06:05 +0100 Subject: [PATCH 23/35] Fix some documentation --- src/ui/public/saved_objects/saved_objects_client.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ui/public/saved_objects/saved_objects_client.ts b/src/ui/public/saved_objects/saved_objects_client.ts index 8b7958cfb5e3f..a048efe7f42b7 100644 --- a/src/ui/public/saved_objects/saved_objects_client.ts +++ b/src/ui/public/saved_objects/saved_objects_client.ts @@ -187,7 +187,7 @@ export class SavedObjectsClient { * @param {array} objects - [{ type, id, attributes, references, migrationVersion }] * @param {object} [options={}] * @property {boolean} [options.overwrite=false] - * @returns {promise} - { savedObjects: [{ id, type, version, attributes, error: { message } }]} + * @returns The result of the create operation containing created saved objects. */ public bulkCreate = (objects: BulkCreateOptions[] = [], options: KFetchQuery = {}) => { const path = this.getPath(['_bulk_create']); @@ -232,7 +232,7 @@ export class SavedObjectsClient { * @property {integer} [options.perPage=20] * @property {array} options.fields * @property {object} [options.hasReference] - { type, id } - * @returns {promise} - { savedObjects: [ SavedObject({ id, type, version, attributes }) ]} + * @returns A find result with objects matching the specified search. */ public find = ( options: FindOptions = {} @@ -256,7 +256,7 @@ export class SavedObjectsClient { * * @param {string} type * @param {string} id - * @returns {promise} - SavedObject({ id, type, version, attributes }) + * @returns The saved object for the given type and id. */ public get = ( type: string, @@ -276,7 +276,7 @@ export class SavedObjectsClient { * Returns an array of objects by id * * @param {array} objects - an array ids, or an array of objects containing id and optionally type - * @returns {promise} - { savedObjects: [ SavedObject({ id, type, version, attributes }) ] } + * @returns The saved objects with the given type and ids requested * @example * * bulkGet([ @@ -308,7 +308,7 @@ export class SavedObjectsClient { * @param {object} options * @prop {integer} options.version - ensures version matches that of persisted object * @prop {object} options.migrationVersion - The optional migrationVersion of this document - * @returns {promise} + * @returns */ public update( type: string, From 919d5d04f3234f57afe88a95be10d4fc2b033162 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Wed, 6 Feb 2019 18:09:20 +0100 Subject: [PATCH 24/35] Replace ts-ignore by any imports --- src/ui/public/saved_objects/index.ts | 2 -- .../saved_objects/saved_object_registry.d.ts | 20 +++++++++++++++++++ .../saved_objects_client_provider.d.ts | 20 +++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 src/ui/public/saved_objects/saved_object_registry.d.ts create mode 100644 src/ui/public/saved_objects/saved_objects_client_provider.d.ts diff --git a/src/ui/public/saved_objects/index.ts b/src/ui/public/saved_objects/index.ts index 9218c91e07f61..2571c80750677 100644 --- a/src/ui/public/saved_objects/index.ts +++ b/src/ui/public/saved_objects/index.ts @@ -18,9 +18,7 @@ */ export { SavedObjectsClient } from './saved_objects_client'; -// @ts-ignore export { SavedObjectRegistryProvider } from './saved_object_registry'; -// @ts-ignore export { SavedObjectsClientProvider } from './saved_objects_client_provider'; export { SavedObject } from './saved_object'; export { findObjectByTitle } from './find_object_by_title'; diff --git a/src/ui/public/saved_objects/saved_object_registry.d.ts b/src/ui/public/saved_objects/saved_object_registry.d.ts new file mode 100644 index 0000000000000..1992b6ac7260d --- /dev/null +++ b/src/ui/public/saved_objects/saved_object_registry.d.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export type SavedObjectRegistryProvider = any; diff --git a/src/ui/public/saved_objects/saved_objects_client_provider.d.ts b/src/ui/public/saved_objects/saved_objects_client_provider.d.ts new file mode 100644 index 0000000000000..f45735b821f7f --- /dev/null +++ b/src/ui/public/saved_objects/saved_objects_client_provider.d.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export type SavedObjectsClientProvider = any; From 8699c2a7d06736c15519dabddbb1f82a173d48e7 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Wed, 6 Feb 2019 18:52:11 +0100 Subject: [PATCH 25/35] Remove expect.js from test --- package.json | 1 - .../saved_objects_client.test.ts | 59 +++++++++---------- 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index d69d857c3adc7..98352d3f2c4d4 100644 --- a/package.json +++ b/package.json @@ -270,7 +270,6 @@ "@types/enzyme": "^3.1.12", "@types/eslint": "^4.16.2", "@types/execa": "^0.9.0", - "@types/expect.js": "^0.3.29", "@types/fetch-mock": "7.2.1", "@types/getopts": "^2.0.0", "@types/glob": "^5.0.35", diff --git a/src/ui/public/saved_objects/saved_objects_client.test.ts b/src/ui/public/saved_objects/saved_objects_client.test.ts index 16f2378c333c5..fe8d144407d8e 100644 --- a/src/ui/public/saved_objects/saved_objects_client.test.ts +++ b/src/ui/public/saved_objects/saved_objects_client.test.ts @@ -19,7 +19,6 @@ jest.mock('ui/kfetch', () => ({})); -import expect from 'expect.js'; import * as sinon from 'sinon'; import { FindOptions } from '../../../server/saved_objects/service'; import { SavedObject } from './saved_object'; @@ -56,32 +55,32 @@ describe('SavedObjectsClient', () => { }); test('returns a promise', () => { - expect(savedObjectsClient.get('index-pattern', 'logstash-*')).to.be.a(Promise); + expect(savedObjectsClient.get('index-pattern', 'logstash-*')).toBeInstanceOf(Promise); }); test('requires type', async () => { try { await savedObjectsClient.get(undefined as any, undefined as any); - expect().fail('should have error'); + fail('should have error'); } catch (e) { - expect(e.message).to.be('requires type and id'); + expect(e.message).toBe('requires type and id'); } }); test('requires id', async () => { try { await savedObjectsClient.get('index-pattern', undefined as any); - expect().throw('should have error'); + fail('should have error'); } catch (e) { - expect(e.message).to.be('requires type and id'); + expect(e.message).toBe('requires type and id'); } }); test('resolves with instantiated SavedObject', async () => { const response = await savedObjectsClient.get(doc.type, doc.id); - expect(response).to.be.a(SavedObject); - expect(response.type).to.eql('config'); - expect(response.get('title')).to.eql('Example title'); + expect(response).toBeInstanceOf(SavedObject); + expect(response.type).toBe('config'); + expect(response.get('title')).toBe('Example title'); }); test('makes HTTP call', async () => { @@ -102,7 +101,7 @@ describe('SavedObjectsClient', () => { await savedObjectsClient.get(doc.type, doc.id); throw new Error('should have error'); } catch (e) { - expect(e.message).to.be('Request failed'); + expect(e.message).toBe('Request failed'); } }); }); @@ -120,24 +119,24 @@ describe('SavedObjectsClient', () => { }); test('returns a promise', () => { - expect(savedObjectsClient.delete('index-pattern', 'logstash-*')).to.be.a(Promise); + expect(savedObjectsClient.delete('index-pattern', 'logstash-*')).toBeInstanceOf(Promise); }); test('requires type', async () => { try { await savedObjectsClient.delete(undefined as any, undefined as any); - expect().throw('should have error'); + fail('should have error'); } catch (e) { - expect(e.message).to.be('requires type and id'); + expect(e.message).toBe('requires type and id'); } }); test('requires id', async () => { try { await savedObjectsClient.delete('index-pattern', undefined as any); - expect().throw('should have error'); + fail('should have error'); } catch (e) { - expect(e.message).to.be('requires type and id'); + expect(e.message).toBe('requires type and id'); } }); @@ -162,33 +161,33 @@ describe('SavedObjectsClient', () => { }); test('returns a promise', () => { - expect(savedObjectsClient.update('index-pattern', 'logstash-*', {})).to.be.a(Promise); + expect(savedObjectsClient.update('index-pattern', 'logstash-*', {})).toBeInstanceOf(Promise); }); test('requires type', async () => { try { await savedObjectsClient.update(undefined as any, undefined as any, undefined as any); - expect().throw('should have error'); + fail('should have error'); } catch (e) { - expect(e.message).to.be(requireMessage); + expect(e.message).toBe(requireMessage); } }); test('requires id', async () => { try { await savedObjectsClient.update('index-pattern', undefined as any, undefined as any); - expect().throw('should have error'); + fail('should have error'); } catch (e) { - expect(e.message).to.be(requireMessage); + expect(e.message).toBe(requireMessage); } }); test('requires attributes', async () => { try { await savedObjectsClient.update('index-pattern', 'logstash-*', undefined as any); - expect().throw('should have error'); + fail('should have error'); } catch (e) { - expect(e.message).to.be(requireMessage); + expect(e.message).toBe(requireMessage); } }); @@ -223,15 +222,15 @@ describe('SavedObjectsClient', () => { }); test('returns a promise', () => { - expect(savedObjectsClient.create('index-pattern', {})).to.be.a(Promise); + expect(savedObjectsClient.create('index-pattern', {})).toBeInstanceOf(Promise); }); test('requires type', async () => { try { await savedObjectsClient.create(undefined as any, undefined as any); - expect().throw('should have error'); + fail('should have error'); } catch (e) { - expect(e.message).to.be(requireMessage); + expect(e.message).toBe(requireMessage); } }); @@ -286,14 +285,14 @@ describe('SavedObjectsClient', () => { }); test('returns a promise', () => { - expect(savedObjectsClient.bulkCreate([doc], {})).to.be.a(Promise); + expect(savedObjectsClient.bulkCreate([doc], {})).toBeInstanceOf(Promise); }); test('resolves with instantiated SavedObjects', async () => { const response = await savedObjectsClient.bulkCreate([doc], {}); - expect(response).to.have.property('savedObjects'); - expect(response.savedObjects.length).to.eql(1); - expect(response.savedObjects[0]).to.be.a(SavedObject); + expect(response).toHaveProperty('savedObjects'); + expect(response.savedObjects.length).toBe(1); + expect(response.savedObjects[0]).toBeInstanceOf(SavedObject); }); test('makes HTTP call', async () => { @@ -310,7 +309,7 @@ describe('SavedObjectsClient', () => { }); test('returns a promise', () => { - expect(savedObjectsClient.find()).to.be.a(Promise); + expect(savedObjectsClient.find()).toBeInstanceOf(Promise); }); test('accepts type', () => { From 7e66b7adbf7ae14680a0df8cfe39fbcf3996fac8 Mon Sep 17 00:00:00 2001 From: Johannes Reuter Date: Thu, 7 Feb 2019 14:29:53 +0100 Subject: [PATCH 26/35] fix ts errors --- .../components/saved_object_finder.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/ui/public/saved_objects/components/saved_object_finder.tsx b/src/ui/public/saved_objects/components/saved_object_finder.tsx index 4bcd3715de02e..1c69a3dc66afe 100644 --- a/src/ui/public/saved_objects/components/saved_object_finder.tsx +++ b/src/ui/public/saved_objects/components/saved_object_finder.tsx @@ -28,11 +28,16 @@ import { EuiBasicTable, EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiLink } fro import { Direction } from '@elastic/eui/src/services/sort/sort_direction'; import { injectI18n } from '@kbn/i18n/react'; +import { SavedObjectAttributes } from '../../../../server/saved_objects'; import { VisTypesRegistryProvider } from '../../registry/vis_types'; import { SavedObject } from '../saved_object'; interface SavedObjectFinderUIState { - items: Array<{ title: string; id: SavedObject['id']; type: SavedObject['type'] }>; + items: Array<{ + title: string | null; + id: SavedObject['id']; + type: SavedObject['type']; + }>; filter: string; isFetchingItems: boolean; page: number; @@ -43,8 +48,11 @@ interface SavedObjectFinderUIState { interface SavedObjectFinderUIProps extends InjectedIntlProps { callToActionButton?: React.ReactNode; - onChoose?: (id: SavedObject['id'], type: SavedObject['type']) => void; - makeUrl?: (id: SavedObject['id']) => void; + onChoose?: ( + id: SavedObject['id'], + type: SavedObject['type'] + ) => void; + makeUrl?: (id: SavedObject['id']) => void; noItemsMessage?: React.ReactNode; savedObjectType: 'visualization' | 'search'; visTypes?: VisTypesRegistryProvider; @@ -92,7 +100,7 @@ class SavedObjectFinderUI extends React.Component< visTypes ) { resp.savedObjects = resp.savedObjects.filter(savedObject => { - const typeName = JSON.parse(savedObject.attributes.visState).type; + const typeName = JSON.parse(savedObject.attributes.visState as string).type; const visType = visTypes.byName[typeName]; return visType.stage !== 'experimental'; }); @@ -109,7 +117,7 @@ class SavedObjectFinderUI extends React.Component< isFetchingItems: false, items: resp.savedObjects.map(savedObject => { return { - title: savedObject.attributes.title, + title: savedObject.attributes.title as string, id: savedObject.id, type: savedObject.type, }; From c32eab9ae831c1e5856ba3145d65c7da93fc04e9 Mon Sep 17 00:00:00 2001 From: Johannes Reuter Date: Fri, 8 Feb 2019 09:58:45 +0100 Subject: [PATCH 27/35] remove any --- src/ui/public/saved_objects/components/saved_object_finder.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/public/saved_objects/components/saved_object_finder.tsx b/src/ui/public/saved_objects/components/saved_object_finder.tsx index 1c69a3dc66afe..724ee2b530169 100644 --- a/src/ui/public/saved_objects/components/saved_object_finder.tsx +++ b/src/ui/public/saved_objects/components/saved_object_finder.tsx @@ -271,7 +271,7 @@ class SavedObjectFinderUI extends React.Component< defaultMessage: 'Title', }), sortable: true, - render: (title: string, record: SavedObject) => { + render: (title: string, record: SavedObject) => { const { onChoose, makeUrl } = this.props; if (!onChoose && !makeUrl) { From 6884b428491728a1a4d83f788d1e85b39b455b36 Mon Sep 17 00:00:00 2001 From: Johannes Reuter Date: Fri, 8 Feb 2019 11:12:28 +0100 Subject: [PATCH 28/35] adress review notes --- src/ui/public/registry/vis_types.d.ts | 3 +- .../components/saved_object_finder.tsx | 36 +++++++++---------- typings/@elastic/eui/index.d.ts | 14 ++++++++ 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/ui/public/registry/vis_types.d.ts b/src/ui/public/registry/vis_types.d.ts index efabe9af130dc..937ebae583f23 100644 --- a/src/ui/public/registry/vis_types.d.ts +++ b/src/ui/public/registry/vis_types.d.ts @@ -17,6 +17,7 @@ * under the License. */ +import { VisType } from '../vis'; import { UIRegistry } from './_registry'; -declare type VisTypesRegistryProvider = UIRegistry & { byName: any[] }; +declare type VisTypesRegistryProvider = UIRegistry & { byName: VisType[] }; diff --git a/src/ui/public/saved_objects/components/saved_object_finder.tsx b/src/ui/public/saved_objects/components/saved_object_finder.tsx index 724ee2b530169..3ca5f3f8ca530 100644 --- a/src/ui/public/saved_objects/components/saved_object_finder.tsx +++ b/src/ui/public/saved_objects/components/saved_object_finder.tsx @@ -23,8 +23,14 @@ import React from 'react'; import { InjectedIntlProps } from 'react-intl'; import chrome from 'ui/chrome'; -// @ts-ignore -import { EuiBasicTable, EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; +import { + EuiBasicTable, + EuiFieldSearch, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiTableCriteria, +} from '@elastic/eui'; import { Direction } from '@elastic/eui/src/services/sort/sort_direction'; import { injectI18n } from '@kbn/i18n/react'; @@ -58,15 +64,6 @@ interface SavedObjectFinderUIProps extends InjectedIntlProps { visTypes?: VisTypesRegistryProvider; } -// TODO there should be a Type in EUI for that, replace if it exists -interface TableCriteria { - page: { index: number; size: number }; - sort?: { - field?: string; - direction?: Direction; - }; -} - class SavedObjectFinderUI extends React.Component< SavedObjectFinderUIProps, SavedObjectFinderUIState @@ -100,7 +97,10 @@ class SavedObjectFinderUI extends React.Component< visTypes ) { resp.savedObjects = resp.savedObjects.filter(savedObject => { - const typeName = JSON.parse(savedObject.attributes.visState as string).type; + if (typeof savedObject.attributes.visState !== 'string') { + return false; + } + const typeName = JSON.parse(savedObject.attributes.visState).type; const visType = visTypes.byName[typeName]; return visType.stage !== 'experimental'; }); @@ -115,11 +115,11 @@ class SavedObjectFinderUI extends React.Component< if (filter === this.state.filter) { this.setState({ isFetchingItems: false, - items: resp.savedObjects.map(savedObject => { + items: resp.savedObjects.map(({ attributes: { title }, id, type }) => { return { - title: savedObject.attributes.title as string, - id: savedObject.id, - type: savedObject.type, + title: typeof title === 'string' ? title : '', + id, + type, }; }), }); @@ -157,7 +157,7 @@ class SavedObjectFinderUI extends React.Component< ); } - private onTableChange = ({ page, sort = {} }: TableCriteria) => { + private onTableChange = ({ page, sort = {} }: EuiTableCriteria) => { let sortField: string | undefined = sort.field; let sortDirection: Direction | undefined = sort.direction; @@ -256,7 +256,7 @@ class SavedObjectFinderUI extends React.Component< pageSizeOptions: [5, 10], }; // TODO there should be a Type in EUI for that, replace if it exists - const sorting: { sort?: TableCriteria['sort'] } = {}; + const sorting: { sort?: EuiTableCriteria['sort'] } = {}; if (this.state.sortField) { sorting.sort = { field: this.state.sortField, diff --git a/typings/@elastic/eui/index.d.ts b/typings/@elastic/eui/index.d.ts index 7be970db272d7..fcdc481c17770 100644 --- a/typings/@elastic/eui/index.d.ts +++ b/typings/@elastic/eui/index.d.ts @@ -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; export const EuiOutsideClickDetector: React.SFC; export const EuiSideNav: React.SFC; + + 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; + }>; } From 3d20d99d2582aa00592cc3254252897c50b34691 Mon Sep 17 00:00:00 2001 From: Johannes Reuter Date: Fri, 8 Feb 2019 12:42:50 +0100 Subject: [PATCH 29/35] make add panel flyout wider --- .../core_plugins/kibana/public/dashboard/top_nav/add_panel.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.js b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.js index 70b957954a84c..e9f4709c7e927 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.js @@ -146,7 +146,6 @@ class DashboardAddPanelUi extends React.Component { From e636441d2e8171e65779eec545d0baa854278c30 Mon Sep 17 00:00:00 2001 From: Johannes Reuter Date: Fri, 8 Feb 2019 13:52:51 +0100 Subject: [PATCH 30/35] fix typing of registry --- src/ui/public/registry/vis_types.d.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ui/public/registry/vis_types.d.ts b/src/ui/public/registry/vis_types.d.ts index 937ebae583f23..77d5515dc5745 100644 --- a/src/ui/public/registry/vis_types.d.ts +++ b/src/ui/public/registry/vis_types.d.ts @@ -20,4 +20,6 @@ import { VisType } from '../vis'; import { UIRegistry } from './_registry'; -declare type VisTypesRegistryProvider = UIRegistry & { byName: VisType[] }; +declare type VisTypesRegistryProvider = UIRegistry & { + byName: { [typeName: string]: VisType }; +}; From 71df162767750ae76453569306805f1e922d7dc0 Mon Sep 17 00:00:00 2001 From: Johannes Reuter Date: Fri, 8 Feb 2019 13:57:54 +0100 Subject: [PATCH 31/35] small type improvement --- src/ui/public/saved_objects/components/saved_object_finder.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/public/saved_objects/components/saved_object_finder.tsx b/src/ui/public/saved_objects/components/saved_object_finder.tsx index 3ca5f3f8ca530..49a775d99b40e 100644 --- a/src/ui/public/saved_objects/components/saved_object_finder.tsx +++ b/src/ui/public/saved_objects/components/saved_object_finder.tsx @@ -100,7 +100,7 @@ class SavedObjectFinderUI extends React.Component< if (typeof savedObject.attributes.visState !== 'string') { return false; } - const typeName = JSON.parse(savedObject.attributes.visState).type; + const typeName: string = JSON.parse(savedObject.attributes.visState).type; const visType = visTypes.byName[typeName]; return visType.stage !== 'experimental'; }); From 356932fdb5908b9e00e86159180d29b4d065465d Mon Sep 17 00:00:00 2001 From: Johannes Reuter Date: Fri, 8 Feb 2019 14:02:11 +0100 Subject: [PATCH 32/35] remove unused typings --- .../saved_objects/saved_object_registry.d.ts | 20 ------------------- .../saved_objects_client_provider.d.ts | 20 ------------------- 2 files changed, 40 deletions(-) delete mode 100644 src/ui/public/saved_objects/saved_object_registry.d.ts delete mode 100644 src/ui/public/saved_objects/saved_objects_client_provider.d.ts diff --git a/src/ui/public/saved_objects/saved_object_registry.d.ts b/src/ui/public/saved_objects/saved_object_registry.d.ts deleted file mode 100644 index 1992b6ac7260d..0000000000000 --- a/src/ui/public/saved_objects/saved_object_registry.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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. - */ - -export type SavedObjectRegistryProvider = any; diff --git a/src/ui/public/saved_objects/saved_objects_client_provider.d.ts b/src/ui/public/saved_objects/saved_objects_client_provider.d.ts deleted file mode 100644 index f45735b821f7f..0000000000000 --- a/src/ui/public/saved_objects/saved_objects_client_provider.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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. - */ - -export type SavedObjectsClientProvider = any; From e1851bdd71c25dfa95d16774b70228be7931a9a4 Mon Sep 17 00:00:00 2001 From: Johannes Reuter Date: Fri, 8 Feb 2019 14:23:04 +0100 Subject: [PATCH 33/35] increase pagesize in saved object finder and make it configurable --- .../public/saved_objects/components/saved_object_finder.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ui/public/saved_objects/components/saved_object_finder.tsx b/src/ui/public/saved_objects/components/saved_object_finder.tsx index 49a775d99b40e..8ec7317c266d9 100644 --- a/src/ui/public/saved_objects/components/saved_object_finder.tsx +++ b/src/ui/public/saved_objects/components/saved_object_finder.tsx @@ -62,6 +62,7 @@ interface SavedObjectFinderUIProps extends InjectedIntlProps { noItemsMessage?: React.ReactNode; savedObjectType: 'visualization' | 'search'; visTypes?: VisTypesRegistryProvider; + initialPageSize?: number; } class SavedObjectFinderUI extends React.Component< @@ -75,6 +76,7 @@ class SavedObjectFinderUI extends React.Component< noItemsMessage: PropTypes.node, savedObjectType: PropTypes.oneOf(['visualization', 'search']).isRequired, visTypes: PropTypes.object, + initialPageSize: PropTypes.number, }; private isComponentMounted: boolean = false; @@ -133,7 +135,7 @@ class SavedObjectFinderUI extends React.Component< items: [], isFetchingItems: false, page: 0, - perPage: 10, + perPage: props.initialPageSize || 20, filter: '', }; } @@ -253,7 +255,7 @@ class SavedObjectFinderUI extends React.Component< pageIndex: this.state.page, pageSize: this.state.perPage, totalItemCount: this.state.items.length, - pageSizeOptions: [5, 10], + pageSizeOptions: [5, 10, 20], }; // TODO there should be a Type in EUI for that, replace if it exists const sorting: { sort?: EuiTableCriteria['sort'] } = {}; From a3808ebffeb8c3d2a1084d8b46cce5d5ba319caa Mon Sep 17 00:00:00 2001 From: Johannes Reuter Date: Fri, 8 Feb 2019 14:30:40 +0100 Subject: [PATCH 34/35] change max rows per page --- .../public/saved_objects/components/saved_object_finder.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/public/saved_objects/components/saved_object_finder.tsx b/src/ui/public/saved_objects/components/saved_object_finder.tsx index 8ec7317c266d9..c8b9258c53d32 100644 --- a/src/ui/public/saved_objects/components/saved_object_finder.tsx +++ b/src/ui/public/saved_objects/components/saved_object_finder.tsx @@ -62,7 +62,7 @@ interface SavedObjectFinderUIProps extends InjectedIntlProps { noItemsMessage?: React.ReactNode; savedObjectType: 'visualization' | 'search'; visTypes?: VisTypesRegistryProvider; - initialPageSize?: number; + initialPageSize?: 5 | 10 | 15; } class SavedObjectFinderUI extends React.Component< @@ -135,7 +135,7 @@ class SavedObjectFinderUI extends React.Component< items: [], isFetchingItems: false, page: 0, - perPage: props.initialPageSize || 20, + perPage: props.initialPageSize || 15, filter: '', }; } @@ -255,7 +255,7 @@ class SavedObjectFinderUI extends React.Component< pageIndex: this.state.page, pageSize: this.state.perPage, totalItemCount: this.state.items.length, - pageSizeOptions: [5, 10, 20], + pageSizeOptions: [5, 10, 15], }; // TODO there should be a Type in EUI for that, replace if it exists const sorting: { sort?: EuiTableCriteria['sort'] } = {}; From 5cd6d68cfeafff796433e2b79c6a987bbf330d3f Mon Sep 17 00:00:00 2001 From: Johannes Reuter Date: Fri, 8 Feb 2019 15:19:02 +0100 Subject: [PATCH 35/35] fix snapshot test --- .../dashboard/top_nav/__snapshots__/add_panel.test.js.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/add_panel.test.js.snap b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/add_panel.test.js.snap index 379eb82f438ec..d84517a1beccf 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/add_panel.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/add_panel.test.js.snap @@ -8,7 +8,7 @@ exports[`render 1`] = ` maxWidth={false} onClose={[Function]} ownFocus={true} - size="s" + size="m" >