diff --git a/docs/user/security/images/mutual-tls-role-mapping.png b/docs/user/security/images/mutual-tls-role-mapping.png new file mode 100644 index 0000000000000..d95ce41e130c2 Binary files /dev/null and b/docs/user/security/images/mutual-tls-role-mapping.png differ diff --git a/docs/user/security/securing-communications/elasticsearch-mutual-tls.asciidoc b/docs/user/security/securing-communications/elasticsearch-mutual-tls.asciidoc new file mode 100644 index 0000000000000..1c7e7080e676b --- /dev/null +++ b/docs/user/security/securing-communications/elasticsearch-mutual-tls.asciidoc @@ -0,0 +1,120 @@ +[role="xpack"] +[[elasticsearch-mutual-tls]] +=== Mutual TLS authentication between {kib} and {es} +++++ +Mutual TLS with {es} +++++ + +In a standard Transport Layer Security (TLS/SSL) configuration, the server presents a signed certificate to authenticate itself to the +client. In a mutual TLS configuration, the client also presents a signed certificate to authenticate itself to the server. + +When {security} is enabled on your cluster, each request that {kib} makes to {es} must be authenticated. Most requests made through {kib} to +{es} are authenticated by using the credentials of the logged-in user. There are, however, a few internal requests that the {kib} server +needs to make to the {es} cluster. For this reason, you must configure credentials for the {kib} server to use for those requests. + +If {kib} has `elasticsearch.username` and `elasticsearch.password` configured, it will attempt to use these to authenticate to {es} via the +{ref}/native-realm.html[Native realm]. However, {kib} also supports mutual TLS authentication with {es} via a {ref}/pki-realm.html[Public +Key Infrastructure (PKI) realm]. To do so, {es} needs to verify the signature on the {kib} client certificate, and it also needs to map the +certificate's distinguished name (DN) to the appropriate `kibana_system` role. + +NOTE: Using a PKI realm is a gold feature. For a comparison of the Elastic license levels, see https://www.elastic.co/subscriptions[the +subscription page]. + +To configure {kib} and {es} to use mutual TLS authentication: + +. <> with a username and password. + +. <>. At a minimum, this requires a server certificate for {es}. + +. Create a client certificate and private key for {kib} to use when connecting to {es}. ++ +-- +NOTE: This is not the same as the <> that {kib} will present to web browsers. + +You may choose to generate a certificate and private key using {ref}/certutil.html[the {es} certutil tool]. At this point, you will have +already set up a certificate authority (CA) to sign the {es} server certificate. You may choose to use the same CA to sign the {kib} client +certificate. You would do this like so: + +[source,sh] +-------------------------------------------------------------------------------- +bin/elasticsearch-certutil cert -ca elastic-stack-ca.p12 -name kibana-client +-------------------------------------------------------------------------------- + +This will generate a certificate and private key in a PKCS #12 keystore named `kibana-client.p12`. The certificate has a Common Name (CN) of +"kibana-client". + +You will also need to use the CA certificate when setting up the PKI realm in {es}. While you could use the CA keystore in the above example +for this purpose, it is bad practice to expose the CA's private key in such a manner. Instead, you can extract the CA certificate (without +its private key) like so: + +[source,sh] +-------------------------------------------------------------------------------- +openssl pkcs12 -in kibana-client.p12 -cacerts -nokeys -out ca.crt +-------------------------------------------------------------------------------- +-- + +. Configure a PKI realm and a Native realm in your {es} cluster: ++ +-- +By default, {es} provides a Native realm. However, to support both a PKI realm (for {kib}) and a Native realm (for end users), you must +configure each realm in `elasticsearch.yml`: + +[source,yaml] +-------------------------------------------------------------------------------- +xpack.security.authc.realms.pki.realm1.order: 1 +xpack.security.authc.realms.pki.realm1.certificate_authorities: "/path/to/ca.crt" +xpack.security.authc.realms.native.realm2.order: 2 +-------------------------------------------------------------------------------- + +-- + +. Configure your {es} cluster to request client certificates: ++ +-- +By default, {es} will not request a client certificate when establishing a TLS connection. To change this, you must set up optional client +certificate authentication in `elasticsearch.yml`: + +[source,yaml] +-------------------------------------------------------------------------------- +xpack.security.http.ssl.client_authentication: "optional" +-------------------------------------------------------------------------------- +-- + +. Restart your {es} cluster. + +. Use {kib} to create a <> for your new client certificate: ++ +-- +This role mapping will assign the `kibana_system` role to any user that matches the included mapping rule, which is set to equal the client +certificate's DN attribute: + +[role="screenshot"] +image:user/security/images/mutual-tls-role-mapping.png["Role mapping for the {kib} client certificate"] +-- + +. Configure {kib} to use the client certificate: ++ +-- +Assuming you used the {es} certutil tool to generate a certificate and private key in a PKCS #12 keystore, add the following values to +`kibana.yml`: + +[source,yaml] +-------------------------------------------------------------------------------- +elasticsearch.ssl.keystore.path: "/path/to/kibana-client.p12" +elasticsearch.ssl.keystore.password: "decryption password" +-------------------------------------------------------------------------------- + +The decryption password should match what you entered when prompted by the {es} certutil tool. + +You must also remove the `elasticsearch.username` and `elasticsearch.password` values from the configuration file. Otherwise, {kib} will +attempt to use those to authenticate via the Native realm. + +TIP: Alternatively, {kib} also supports using a client certificate and private key in PEM format with the `elasticsearch.ssl.certificate` +and `elasticsearch.ssl.key` settings. For more information, see <>. +-- + +. Restart {kib}. + +NOTE: The steps above enable {kib} to authenticate to {es} using a certificate. However, end users will only be able to authenticate to +{kib} with a username and password. To allow end users to authenticate to {kib} using certificates, see <>. diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc index a68a2ee285ee3..2d07b57bfabe1 100644 --- a/docs/user/security/securing-kibana.asciidoc +++ b/docs/user/security/securing-kibana.asciidoc @@ -88,6 +88,8 @@ xpack.security.session.lifespan: "8h" . Optional: <>. +. Optional: <>. + . Restart {kib}. . [[kibana-roles]]Choose an authentication mechanism and grant users the privileges they need to @@ -141,4 +143,5 @@ NOTE: This must be a user who has been assigned < = ({ fetchStreaming }) => { + const isMounted = useMountedState(); + const [data, setData] = useState(5); + const [showingResults, setShowingResults] = useState(false); + const [results, { push: pushResult, clear: clearList }] = useList([]); + const [completed, setCompleted] = useState(false); + const [error, setError] = useState(null); + + const handleSubmit = () => { + setShowingResults(true); + const { stream } = fetchStreaming({ + url: '/bfetch_explorer/count', + body: JSON.stringify({ data }), + }); + stream.subscribe({ + next: (next: string) => { + if (!isMounted()) return; + pushResult(next); + }, + error: (nextError: any) => { + if (!isMounted()) return; + setError(nextError); + }, + complete: () => { + if (!isMounted()) return; + setCompleted(true); + }, + }); + }; + + const handleReset = () => { + setShowingResults(false); + clearList(); + setError(null); + setCompleted(false); + }; + + if (showingResults) { + return ( + +
{JSON.stringify(error || results, null, 4)}
+ + + Reset + +
+ ); + } + + return ( + + + setData(Number(e.target.value))} + /> + + + Start + + + ); +}; diff --git a/examples/bfetch_explorer/public/components/double_integers/index.tsx b/examples/bfetch_explorer/public/components/double_integers/index.tsx new file mode 100644 index 0000000000000..d8fbe33ec73be --- /dev/null +++ b/examples/bfetch_explorer/public/components/double_integers/index.tsx @@ -0,0 +1,105 @@ +/* + * 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 React, { useState } from 'react'; +import useMountedState from 'react-use/lib/useMountedState'; +import useList from 'react-use/lib/useList'; +import useCounter from 'react-use/lib/useCounter'; +import { EuiForm, EuiSpacer, EuiTextArea, EuiFormRow, EuiButton } from '@elastic/eui'; +import { ExplorerService } from '../../plugin'; + +interface ResultItem { + num: number; + result?: { + num: number; + }; + error?: any; +} + +const defaultNumbers = [2000, 300, -1, 1000].join('\n'); + +export interface Props { + double: ExplorerService['double']; +} + +export const DoubleIntegers: React.FC = ({ double }) => { + const isMounted = useMountedState(); + const [numbers, setNumbers] = useState(defaultNumbers); + const [showingResults, setShowingResults] = useState(false); + const [numberOfResultsAwaiting, counter] = useCounter(0); + const [results, { push: pushResult, clear: clearList }] = useList([]); + + const handleSubmit = () => { + setShowingResults(true); + const nums = numbers + .split('\n') + .map(num => num.trim()) + .filter(Boolean) + .map(Number); + counter.set(nums.length); + nums.forEach(num => { + double({ num }).then( + result => { + if (!isMounted()) return; + counter.dec(); + pushResult({ num, result }); + }, + error => { + if (!isMounted()) return; + counter.dec(); + pushResult({ num, error }); + } + ); + }); + }; + + const handleReset = () => { + setShowingResults(false); + counter.reset(); + clearList(); + }; + + if (showingResults) { + return ( + +
{JSON.stringify(results, null, 4)}
+ + + Reset + +
+ ); + } + + return ( + + + setNumbers(e.target.value)} + /> + + + Send + + + ); +}; diff --git a/examples/bfetch_explorer/public/components/page/index.tsx b/examples/bfetch_explorer/public/components/page/index.tsx new file mode 100644 index 0000000000000..0e7855178a884 --- /dev/null +++ b/examples/bfetch_explorer/public/components/page/index.tsx @@ -0,0 +1,51 @@ +/* + * 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 * as React from 'react'; +import { + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiTitle, +} from '@elastic/eui'; + +export interface PageProps { + title?: React.ReactNode; +} + +export const Page: React.FC = ({ title = 'Untitled', children }) => { + return ( + + + + +

{title}

+
+
+
+ + + {children} + + +
+ ); +}; diff --git a/examples/bfetch_explorer/public/containers/app/index.tsx b/examples/bfetch_explorer/public/containers/app/index.tsx new file mode 100644 index 0000000000000..a448c9e4f3a6a --- /dev/null +++ b/examples/bfetch_explorer/public/containers/app/index.tsx @@ -0,0 +1,48 @@ +/* + * 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 React from 'react'; +import { BrowserRouter as Router, Route, Redirect, Switch } from 'react-router-dom'; +import { EuiPage } from '@elastic/eui'; +import { useDeps } from '../../hooks/use_deps'; +import { Sidebar } from './sidebar'; +import { routes } from '../../routes'; + +export const App: React.FC = () => { + const { appBasePath } = useDeps(); + + const routeElements: React.ReactElement[] = []; + for (const { items } of routes) { + for (const { id, component } of items) { + routeElements.push( component} />); + } + } + + return ( + + + + + {routeElements} + + + + + ); +}; diff --git a/examples/bfetch_explorer/public/containers/app/pages/page_count_until/index.tsx b/examples/bfetch_explorer/public/containers/app/pages/page_count_until/index.tsx new file mode 100644 index 0000000000000..7b4eac6eea44c --- /dev/null +++ b/examples/bfetch_explorer/public/containers/app/pages/page_count_until/index.tsx @@ -0,0 +1,45 @@ +/* + * 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 * as React from 'react'; +import { EuiPanel, EuiText } from '@elastic/eui'; +import { CountUntil } from '../../../../components/count_until'; +import { Page } from '../../../../components/page'; +import { useDeps } from '../../../../hooks/use_deps'; + +// eslint-disable-next-line +export interface Props {} + +export const PageCountUntil: React.FC = () => { + const { plugins } = useDeps(); + + return ( + + + This demo sends a single number N using fetchStreaming to the server. The + server will stream back N number of messages with 1 second delay each containing a number + from 1 to N, after which it will close the stream. + +
+ + + +
+ ); +}; diff --git a/examples/bfetch_explorer/public/containers/app/pages/page_double_integers/index.tsx b/examples/bfetch_explorer/public/containers/app/pages/page_double_integers/index.tsx new file mode 100644 index 0000000000000..7bd5feb836674 --- /dev/null +++ b/examples/bfetch_explorer/public/containers/app/pages/page_double_integers/index.tsx @@ -0,0 +1,45 @@ +/* + * 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 * as React from 'react'; +import { EuiPanel, EuiText } from '@elastic/eui'; +import { DoubleIntegers } from '../../../../components/double_integers'; +import { Page } from '../../../../components/page'; +import { useDeps } from '../../../../hooks/use_deps'; + +// eslint-disable-next-line +export interface Props {} + +export const PageDoubleIntegers: React.FC = () => { + const { explorer } = useDeps(); + + return ( + + + Below is a list of numbers in milliseconds. They are sent as a batch to the server. For each + number server waits given number of milliseconds then doubles the number and streams it + back. + +
+ + + +
+ ); +}; diff --git a/examples/bfetch_explorer/public/containers/app/sidebar/index.tsx b/examples/bfetch_explorer/public/containers/app/sidebar/index.tsx new file mode 100644 index 0000000000000..cc50698e05908 --- /dev/null +++ b/examples/bfetch_explorer/public/containers/app/sidebar/index.tsx @@ -0,0 +1,54 @@ +/* + * 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 React from 'react'; +import { EuiPageSideBar, EuiSideNav } from '@elastic/eui'; +import { useHistory } from 'react-router-dom'; +import { routes } from '../../../routes'; + +// eslint-disable-next-line +interface SidebarProps {} + +export const Sidebar: React.FC = () => { + const history = useHistory(); + + return ( + + ({ + id, + name: title, + isSelected: true, + items: items.map(route => ({ + id: route.id, + name: route.title, + onClick: () => history.push(`/${route.id}`), + 'data-test-subj': route.id, + })), + })), + }, + ]} + /> + + ); +}; diff --git a/examples/bfetch_explorer/public/hooks/use_deps.ts b/examples/bfetch_explorer/public/hooks/use_deps.ts new file mode 100644 index 0000000000000..c68b4e759c21c --- /dev/null +++ b/examples/bfetch_explorer/public/hooks/use_deps.ts @@ -0,0 +1,23 @@ +/* + * 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 { useKibana } from '../../../../src/plugins/kibana_react/public'; +import { BfetchDeps } from '../mount'; + +export const useDeps = () => useKibana().services as BfetchDeps; diff --git a/src/legacy/core_plugins/interpreter/public/canvas/consts.ts b/examples/bfetch_explorer/public/index.ts similarity index 86% rename from src/legacy/core_plugins/interpreter/public/canvas/consts.ts rename to examples/bfetch_explorer/public/index.ts index 2600ada36afdc..76d0a1d1c6334 100644 --- a/src/legacy/core_plugins/interpreter/public/canvas/consts.ts +++ b/examples/bfetch_explorer/public/index.ts @@ -17,5 +17,6 @@ * under the License. */ -// The server endpoint for retrieiving and running Canvas functions. -export const FUNCTIONS_URL = '/api/interpreter/fns'; +import { BfetchExplorerPlugin } from './plugin'; + +export const plugin = () => new BfetchExplorerPlugin(); diff --git a/examples/bfetch_explorer/public/mount.tsx b/examples/bfetch_explorer/public/mount.tsx new file mode 100644 index 0000000000000..5ad53ef4a1988 --- /dev/null +++ b/examples/bfetch_explorer/public/mount.tsx @@ -0,0 +1,47 @@ +/* + * 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 * as React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { CoreSetup, CoreStart, AppMountParameters } from 'kibana/public'; +import { KibanaContextProvider } from '../../../src/plugins/kibana_react/public'; +import { BfetchExplorerStartPlugins, ExplorerService } from './plugin'; +import { App } from './containers/app'; + +export interface BfetchDeps { + appBasePath: string; + core: CoreStart; + plugins: BfetchExplorerStartPlugins; + explorer: ExplorerService; +} + +export const mount = ( + coreSetup: CoreSetup, + explorer: ExplorerService +) => async ({ appBasePath, element }: AppMountParameters) => { + const [core, plugins] = await coreSetup.getStartServices(); + const deps: BfetchDeps = { appBasePath, core, plugins, explorer }; + const reactElement = ( + + + + ); + render(reactElement, element); + return () => unmountComponentAtNode(element); +}; diff --git a/examples/bfetch_explorer/public/plugin.tsx b/examples/bfetch_explorer/public/plugin.tsx new file mode 100644 index 0000000000000..3155354c91fd4 --- /dev/null +++ b/examples/bfetch_explorer/public/plugin.tsx @@ -0,0 +1,55 @@ +/* + * 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 { Plugin, CoreSetup } from 'kibana/public'; +import { BfetchPublicSetup, BfetchPublicStart } from '../../../src/plugins/bfetch/public'; +import { mount } from './mount'; + +export interface ExplorerService { + double: (number: { num: number }) => Promise<{ num: number }>; +} + +export interface BfetchExplorerSetupPlugins { + bfetch: BfetchPublicSetup; +} + +export interface BfetchExplorerStartPlugins { + bfetch: BfetchPublicStart; +} + +export class BfetchExplorerPlugin implements Plugin { + public setup(core: CoreSetup, plugins: BfetchExplorerSetupPlugins) { + const double = plugins.bfetch.batchedFunction<{ num: number }, { num: number }>({ + url: '/bfetch_explorer/double', + }); + + const explorer: ExplorerService = { + double, + }; + + core.application.register({ + id: 'bfetch-explorer', + title: 'bfetch explorer', + mount: mount(core, explorer), + }); + } + + public start() {} + public stop() {} +} diff --git a/examples/bfetch_explorer/public/routes.tsx b/examples/bfetch_explorer/public/routes.tsx new file mode 100644 index 0000000000000..2008811d75795 --- /dev/null +++ b/examples/bfetch_explorer/public/routes.tsx @@ -0,0 +1,59 @@ +/* + * 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 React from 'react'; +import { PageDoubleIntegers } from './containers/app/pages/page_double_integers'; +import { PageCountUntil } from './containers/app/pages/page_count_until'; + +interface RouteSectionDef { + title: string; + id: string; + items: RouteDef[]; +} + +interface RouteDef { + title: string; + id: string; + component: React.ReactNode; +} + +export const routes: RouteSectionDef[] = [ + { + title: 'fetchStreaming', + id: 'fetchStreaming', + items: [ + { + title: 'Count until', + id: 'count-until', + component: , + }, + ], + }, + { + title: 'batchedFunction', + id: 'batchedFunction', + items: [ + { + title: 'Double integers', + id: 'double-integers', + component: , + }, + ], + }, +]; diff --git a/src/legacy/core_plugins/interpreter/server/routes/index.ts b/examples/bfetch_explorer/server/index.ts similarity index 85% rename from src/legacy/core_plugins/interpreter/server/routes/index.ts rename to examples/bfetch_explorer/server/index.ts index 50385147dd38e..76d0a1d1c6334 100644 --- a/src/legacy/core_plugins/interpreter/server/routes/index.ts +++ b/examples/bfetch_explorer/server/index.ts @@ -17,8 +17,6 @@ * under the License. */ -import { registerServerFunctions } from './server_functions'; +import { BfetchExplorerPlugin } from './plugin'; -export function routes(server: any) { - registerServerFunctions(server); -} +export const plugin = () => new BfetchExplorerPlugin(); diff --git a/examples/bfetch_explorer/server/plugin.ts b/examples/bfetch_explorer/server/plugin.ts new file mode 100644 index 0000000000000..bf3b7f50ca6c8 --- /dev/null +++ b/examples/bfetch_explorer/server/plugin.ts @@ -0,0 +1,68 @@ +/* + * 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 { Subject } from 'rxjs'; +import { Plugin, CoreSetup, CoreStart } from '../../../src/core/server'; +import { BfetchServerSetup, BfetchServerStart } from '../../../src/plugins/bfetch/server'; + +export interface BfetchExplorerSetupPlugins { + bfetch: BfetchServerSetup; +} + +export interface BfetchExplorerStartPlugins { + bfetch: BfetchServerStart; +} + +export class BfetchExplorerPlugin implements Plugin { + public setup(core: CoreSetup, plugins: BfetchExplorerSetupPlugins) { + plugins.bfetch.addStreamingResponseRoute('/bfetch_explorer/count', () => ({ + getResponseStream: ({ data }: any) => { + const subject = new Subject(); + const countTo = Number(data); + for (let cnt = 1; cnt <= countTo; cnt++) { + setTimeout(() => { + subject.next(String(cnt)); + }, cnt * 1000); + } + setTimeout(() => { + subject.complete(); + }, countTo * 1000); + return subject; + }, + })); + + plugins.bfetch.addBatchProcessingRoute<{ num: number }, { num: number }>( + '/bfetch_explorer/double', + () => ({ + onBatchItem: async ({ num }) => { + // Validate inputs. + if (num < 0) throw new Error('Invalid number'); + // Wait number of specified milliseconds. + await new Promise(r => setTimeout(r, num)); + // Double the number and send it back. + return { num: 2 * num }; + }, + }) + ); + } + + public start(core: CoreStart, plugins: BfetchExplorerStartPlugins) {} + + public stop() {} +} diff --git a/examples/bfetch_explorer/tsconfig.json b/examples/bfetch_explorer/tsconfig.json new file mode 100644 index 0000000000000..d508076b33199 --- /dev/null +++ b/examples/bfetch_explorer/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../typings/**/*", + ], + "exclude": [] +} diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 8213bdeeea2da..1dce53b6c2a84 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -18,6 +18,7 @@ */ export const storybookAliases = { + apm: 'x-pack/legacy/plugins/apm/scripts/storybook.js', canvas: 'x-pack/legacy/plugins/canvas/scripts/storybook_new.js', embeddable: 'src/plugins/embeddable/scripts/storybook.js', infra: 'x-pack/legacy/plugins/infra/scripts/storybook.js', diff --git a/src/legacy/core_plugins/interpreter/init.ts b/src/legacy/core_plugins/interpreter/init.ts index 768d76fbf744e..46da1539afadb 100644 --- a/src/legacy/core_plugins/interpreter/init.ts +++ b/src/legacy/core_plugins/interpreter/init.ts @@ -22,35 +22,10 @@ // @ts-ignore import { register, registryFactory, Registry, Fn } from '@kbn/interpreter/common'; -// @ts-ignore -import { routes } from './server/routes'; - -import { typeSpecs as types, Type } from '../../../plugins/expressions/common'; import { Legacy } from '../../../../kibana'; -export class TypesRegistry extends Registry { - wrapper(obj: any) { - return new (Type as any)(obj); - } -} - -export class FunctionsRegistry extends Registry { - wrapper(obj: any) { - return new Fn(obj); - } -} - -export const registries = { - types: new TypesRegistry(), - serverFunctions: new FunctionsRegistry(), -}; - export async function init(server: Legacy.Server /* options */) { server.injectUiAppVars('canvas', () => { - register(registries, { - types, - }); - const config = server.config(); const basePath = config.get('server.basePath'); const reportingBrowserType = (() => { @@ -63,7 +38,9 @@ export async function init(server: Legacy.Server /* options */) { return { kbnIndex: config.get('kibana.index'), - serverFunctions: registries.serverFunctions.toArray(), + serverFunctions: (server.newPlatform.setup.plugins.expressions as any).__LEGACY + .registries() + .serverFunctions.toArray(), basePath, reportingBrowserType, }; @@ -71,7 +48,5 @@ export async function init(server: Legacy.Server /* options */) { // Expose server.plugins.interpreter.register(specs) and // server.plugins.interpreter.registries() (a getter). - server.expose(registryFactory(registries)); - - routes(server); + server.expose((server.newPlatform.setup.plugins.expressions as any).__LEGACY); } diff --git a/src/legacy/core_plugins/interpreter/public/canvas/load_legacy_server_function_wrappers.ts b/src/legacy/core_plugins/interpreter/public/canvas/load_legacy_server_function_wrappers.ts index 2c2f79b3d6f51..fed157846a1a1 100644 --- a/src/legacy/core_plugins/interpreter/public/canvas/load_legacy_server_function_wrappers.ts +++ b/src/legacy/core_plugins/interpreter/public/canvas/load_legacy_server_function_wrappers.ts @@ -28,62 +28,6 @@ * server side, it should be respective function's internal implementation detail. */ -import { get, identity } from 'lodash'; -// @ts-ignore -import { npSetup, npStart } from 'ui/new_platform'; -import { FUNCTIONS_URL } from './consts'; -import { batchedFetch } from './batched_fetch'; +import { npSetup } from 'ui/new_platform'; -export function getType(node: any) { - if (node == null) return 'null'; - if (typeof node === 'object') { - if (!node.type) throw new Error('Objects must have a type property'); - return node.type; - } - return typeof node; -} - -export function serializeProvider(types: any) { - return { - serialize: provider('serialize'), - deserialize: provider('deserialize'), - }; - - function provider(key: any) { - return (context: any) => { - const type = getType(context); - const typeDef = types[type]; - const fn: any = get(typeDef, key) || identity; - return fn(context); - }; - } -} - -let cached: Promise | null = null; - -export const loadLegacyServerFunctionWrappers = async () => { - if (!cached) { - cached = (async () => { - const serverFunctionList = await npSetup.core.http.get(FUNCTIONS_URL); - const types = npSetup.plugins.expressions.__LEGACY.types.toJS(); - const { serialize } = serializeProvider(types); - const batch = batchedFetch({ - fetchStreaming: npStart.plugins.bfetch.fetchStreaming, - serialize, - }); - - // For every sever-side function, register a client-side - // function that matches its definition, but which simply - // calls the server-side function endpoint. - Object.keys(serverFunctionList).forEach(functionName => { - const fn = () => ({ - ...serverFunctionList[functionName], - fn: (context: any, args: any) => batch({ functionName, args, context }), - }); - npSetup.plugins.expressions.registerFunction(fn); - }); - })(); - } - - return cached; -}; +export const { loadLegacyServerFunctionWrappers } = npSetup.plugins.expressions.__LEGACY; diff --git a/src/legacy/core_plugins/interpreter/public/registries.karma_mock.ts b/src/legacy/core_plugins/interpreter/public/registries.karma_mock.ts index 66c51167c7b59..0f37f33cc1b13 100644 --- a/src/legacy/core_plugins/interpreter/public/registries.karma_mock.ts +++ b/src/legacy/core_plugins/interpreter/public/registries.karma_mock.ts @@ -26,6 +26,7 @@ export const registries = { browserFunctions: functionsRegistry, renderers: renderersRegistry, types: typesRegistry, + loadLegacyServerFunctionWrappers: () => Promise.resolve(), }; const resetRegistry = (registry: any) => { diff --git a/src/legacy/core_plugins/interpreter/server/lib/__tests__/create_handlers.ts b/src/legacy/core_plugins/interpreter/server/lib/__tests__/create_handlers.ts deleted file mode 100644 index 0088663080774..0000000000000 --- a/src/legacy/core_plugins/interpreter/server/lib/__tests__/create_handlers.ts +++ /dev/null @@ -1,65 +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 expect from '@kbn/expect'; -import { createHandlers } from '../create_handlers'; - -const mockRequest = { - headers: 'i can haz headers', -}; - -const mockServer = { - plugins: { - elasticsearch: { - getCluster: () => ({ - callWithRequest: (...args: any) => Promise.resolve(args), - }), - }, - }, - config: () => ({ - has: () => false, - get: (val: any) => val, - }), - info: { - uri: 'serveruri', - }, -}; - -describe('server createHandlers', () => { - it('provides helper methods and properties', () => { - const handlers = createHandlers(mockRequest, mockServer); - - expect(handlers).to.have.property('environment', 'server'); - expect(handlers).to.have.property('serverUri'); - expect(handlers).to.have.property('elasticsearchClient'); - }); - - describe('elasticsearchClient', () => { - it('executes callWithRequest', async () => { - const handlers = createHandlers(mockRequest, mockServer); - const [request, endpoint, payload] = await handlers.elasticsearchClient( - 'endpoint', - 'payload' - ); - expect(request).to.equal(mockRequest); - expect(endpoint).to.equal('endpoint'); - expect(payload).to.equal('payload'); - }); - }); -}); diff --git a/src/legacy/core_plugins/interpreter/server/routes/server_functions.ts b/src/legacy/core_plugins/interpreter/server/routes/server_functions.ts deleted file mode 100644 index e03ad361b5555..0000000000000 --- a/src/legacy/core_plugins/interpreter/server/routes/server_functions.ts +++ /dev/null @@ -1,166 +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 Boom from 'boom'; -import Joi from 'joi'; -import { serializeProvider } from '../../../../../plugins/expressions/common'; -import { createHandlers } from '../lib/create_handlers'; - -const API_ROUTE = '/api/interpreter'; - -/** - * Register the Canvas function endopints. - * - * @param {*} server - The Kibana server - */ -export function registerServerFunctions(server: any) { - getServerFunctions(server); - runServerFunctions(server); -} - -/** - * Register the endpoint that executes a batch of functions, and sends the result back as a single response. - * - * @param {*} server - The Kibana server - */ -function runServerFunctions(server: any) { - server.route({ - method: 'POST', - path: `${API_ROUTE}/fns`, - options: { - payload: { - allow: 'application/json', - maxBytes: 26214400, // 25MB payload limit - }, - validate: { - payload: Joi.object({ - functions: Joi.array() - .items( - Joi.object().keys({ - id: Joi.number().required(), - functionName: Joi.string().required(), - args: Joi.object().default({}), - context: Joi.any().default(null), - }) - ) - .required(), - }).required(), - }, - }, - async handler(req: any) { - const handlers = await createHandlers(req, server); - const { functions } = req.payload; - - // Grab the raw Node response object. - const res = req.raw.res; - - // Tell Hapi not to manage the response https://github.com/hapijs/hapi/issues/3884 - req._isReplied = true; - - // Send the initial headers. - res.writeHead(200, { - 'Content-Type': 'application/x-ndjson', - Connection: 'keep-alive', - 'Transfer-Encoding': 'chunked', - 'Cache-Control': 'no-cache', - }); - - // Write a length-delimited response - const streamResult = (result: any) => { - res.write(JSON.stringify(result) + '\n'); - }; - - // Tries to run an interpreter function, and ensures a consistent error payload on failure. - const tryFunction = async (id: any, fnCall: any) => { - try { - const result = await runFunction(server, handlers, fnCall); - - if (typeof result === 'undefined') { - return batchError(id, `Function ${fnCall.functionName} did not return anything.`); - } - - return { id, statusCode: 200, result }; - } catch (err) { - if (Boom.isBoom(err)) { - return batchError(id, err.output.payload, (err as any).statusCode); - } else if (err instanceof Error) { - return batchError(id, err.message); - } - - server.log(['interpreter', 'error'], err); - return batchError(id, 'See server logs for details.'); - } - }; - - // Process each function individually, and stream the responses back to the client - await Promise.all( - functions.map(({ id, ...fnCall }: any) => tryFunction(id, fnCall).then(streamResult)) - ); - - // All of the responses have been written, so we can close the response. - res.end(); - }, - }); -} - -/** - * A helper function for bundling up errors. - */ -function batchError(id: any, message: any, statusCode = 500) { - return { - id, - statusCode, - result: { statusCode, message }, - }; -} - -/** - * Register the endpoint that returns the list of server-only functions. - * @param {*} server - The Kibana server - */ -function getServerFunctions(server: any) { - server.route({ - method: 'GET', - path: `${API_ROUTE}/fns`, - handler() { - return server.plugins.interpreter.registries().serverFunctions.toJS(); - }, - }); -} - -/** - * Run a single Canvas function. - * - * @param {*} server - The Kibana server object - * @param {*} handlers - The Canvas handlers - * @param {*} fnCall - Describes the function being run `{ functionName, args, context }` - */ -async function runFunction(server: any, handlers: any, fnCall: any) { - const registries = server.plugins.interpreter.registries(); - const { functionName, args, context } = fnCall; - const types = registries.types.toJS(); - const { deserialize } = serializeProvider(types); - const fnDef = registries.serverFunctions.toJS()[functionName]; - - if (!fnDef) { - throw Boom.notFound(`Function "${functionName}" could not be found.`); - } - - return fnDef.fn(deserialize(context), args, handlers); -} diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js index 78ac99567d10e..2927565e61dce 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js @@ -826,9 +826,14 @@ function discoverController( }; $scope.updateQueryAndFetch = function({ query, dateRange }) { + const oldDateRange = timefilter.getTime(); timefilter.setTime(dateRange); $state.query = query; - $scope.fetch(); + // storing the updated timerange in the state will trigger a fetch + // call automatically, so only trigger fetch in case this is a refresh call (no changes in parameters). + if (_.isEqual(oldDateRange, dateRange)) { + $scope.fetch(); + } }; function onResults(resp) { diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/fixture.json b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/fixture.json deleted file mode 100644 index 178704a36372d..0000000000000 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/fixture.json +++ /dev/null @@ -1,984 +0,0 @@ -{ - "_shards": { - "failed": 0, - "successful": 5, - "total": 5 - }, - "aggregations": { - "c9b5f9c0-e403-11e6-be91-6f7688e9fac7": { - "doc_count": 128145, - "timeseries": { - "buckets": [ - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.057 - }, - "doc_count": 368, - "key": 1485549090000, - "key_as_string": "2017-01-27T20:31:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.07466666666666667 - }, - "doc_count": 1106, - "key": 1485549120000, - "key_as_string": "2017-01-27T20:32:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.08033333333333335 - }, - "doc_count": 1107, - "key": 1485549150000, - "key_as_string": "2017-01-27T20:32:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.066 - }, - "doc_count": 1109, - "key": 1485549180000, - "key_as_string": "2017-01-27T20:33:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.05366666666666667 - }, - "doc_count": 1093, - "key": 1485549210000, - "key_as_string": "2017-01-27T20:33:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.04533333333333334 - }, - "doc_count": 1086, - "key": 1485549240000, - "key_as_string": "2017-01-27T20:34:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.037333333333333336 - }, - "doc_count": 1086, - "key": 1485549270000, - "key_as_string": "2017-01-27T20:34:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.033 - }, - "doc_count": 1090, - "key": 1485549300000, - "key_as_string": "2017-01-27T20:35:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.036000000000000004 - }, - "doc_count": 1085, - "key": 1485549330000, - "key_as_string": "2017-01-27T20:35:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034333333333333334 - }, - "doc_count": 1082, - "key": 1485549360000, - "key_as_string": "2017-01-27T20:36:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.033 - }, - "doc_count": 1080, - "key": 1485549390000, - "key_as_string": "2017-01-27T20:36:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.037000000000000005 - }, - "doc_count": 1082, - "key": 1485549420000, - "key_as_string": "2017-01-27T20:37:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.036000000000000004 - }, - "doc_count": 1079, - "key": 1485549450000, - "key_as_string": "2017-01-27T20:37:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03233333333333333 - }, - "doc_count": 1080, - "key": 1485549480000, - "key_as_string": "2017-01-27T20:38:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03366666666666667 - }, - "doc_count": 1080, - "key": 1485549510000, - "key_as_string": "2017-01-27T20:38:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03366666666666667 - }, - "doc_count": 1082, - "key": 1485549540000, - "key_as_string": "2017-01-27T20:39:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.042666666666666665 - }, - "doc_count": 1079, - "key": 1485549570000, - "key_as_string": "2017-01-27T20:39:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.033 - }, - "doc_count": 1077, - "key": 1485549600000, - "key_as_string": "2017-01-27T20:40:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03833333333333334 - }, - "doc_count": 1075, - "key": 1485549630000, - "key_as_string": "2017-01-27T20:40:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034 - }, - "doc_count": 1076, - "key": 1485549660000, - "key_as_string": "2017-01-27T20:41:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03366666666666667 - }, - "doc_count": 1076, - "key": 1485549690000, - "key_as_string": "2017-01-27T20:41:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034 - }, - "doc_count": 1074, - "key": 1485549720000, - "key_as_string": "2017-01-27T20:42:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03266666666666667 - }, - "doc_count": 1072, - "key": 1485549750000, - "key_as_string": "2017-01-27T20:42:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03266666666666667 - }, - "doc_count": 1067, - "key": 1485549780000, - "key_as_string": "2017-01-27T20:43:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.036000000000000004 - }, - "doc_count": 1065, - "key": 1485549810000, - "key_as_string": "2017-01-27T20:43:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03266666666666667 - }, - "doc_count": 1065, - "key": 1485549840000, - "key_as_string": "2017-01-27T20:44:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.036333333333333336 - }, - "doc_count": 1062, - "key": 1485549870000, - "key_as_string": "2017-01-27T20:44:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034333333333333334 - }, - "doc_count": 1063, - "key": 1485549900000, - "key_as_string": "2017-01-27T20:45:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034333333333333334 - }, - "doc_count": 1065, - "key": 1485549930000, - "key_as_string": "2017-01-27T20:45:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03366666666666667 - }, - "doc_count": 1065, - "key": 1485549960000, - "key_as_string": "2017-01-27T20:46:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034333333333333334 - }, - "doc_count": 1069, - "key": 1485549990000, - "key_as_string": "2017-01-27T20:46:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03333333333333333 - }, - "doc_count": 1068, - "key": 1485550020000, - "key_as_string": "2017-01-27T20:47:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.033 - }, - "doc_count": 1068, - "key": 1485550050000, - "key_as_string": "2017-01-27T20:47:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.033 - }, - "doc_count": 1068, - "key": 1485550080000, - "key_as_string": "2017-01-27T20:48:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03366666666666667 - }, - "doc_count": 1074, - "key": 1485550110000, - "key_as_string": "2017-01-27T20:48:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03133333333333333 - }, - "doc_count": 1074, - "key": 1485550140000, - "key_as_string": "2017-01-27T20:49:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03266666666666667 - }, - "doc_count": 1074, - "key": 1485550170000, - "key_as_string": "2017-01-27T20:49:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03133333333333333 - }, - "doc_count": 1073, - "key": 1485550200000, - "key_as_string": "2017-01-27T20:50:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.033 - }, - "doc_count": 1077, - "key": 1485550230000, - "key_as_string": "2017-01-27T20:50:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03166666666666667 - }, - "doc_count": 1074, - "key": 1485550260000, - "key_as_string": "2017-01-27T20:51:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.031 - }, - "doc_count": 1074, - "key": 1485550290000, - "key_as_string": "2017-01-27T20:51:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03266666666666667 - }, - "doc_count": 1072, - "key": 1485550320000, - "key_as_string": "2017-01-27T20:52:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.033 - }, - "doc_count": 1073, - "key": 1485550350000, - "key_as_string": "2017-01-27T20:52:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03466666666666667 - }, - "doc_count": 1071, - "key": 1485550380000, - "key_as_string": "2017-01-27T20:53:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03266666666666667 - }, - "doc_count": 1071, - "key": 1485550410000, - "key_as_string": "2017-01-27T20:53:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03166666666666667 - }, - "doc_count": 1069, - "key": 1485550440000, - "key_as_string": "2017-01-27T20:54:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034 - }, - "doc_count": 1069, - "key": 1485550470000, - "key_as_string": "2017-01-27T20:54:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.032 - }, - "doc_count": 1068, - "key": 1485550500000, - "key_as_string": "2017-01-27T20:55:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03366666666666667 - }, - "doc_count": 1067, - "key": 1485550530000, - "key_as_string": "2017-01-27T20:55:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03233333333333333 - }, - "doc_count": 1065, - "key": 1485550560000, - "key_as_string": "2017-01-27T20:56:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03366666666666667 - }, - "doc_count": 1069, - "key": 1485550590000, - "key_as_string": "2017-01-27T20:56:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03166666666666667 - }, - "doc_count": 1068, - "key": 1485550620000, - "key_as_string": "2017-01-27T20:57:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03333333333333333 - }, - "doc_count": 1068, - "key": 1485550650000, - "key_as_string": "2017-01-27T20:57:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03266666666666667 - }, - "doc_count": 1068, - "key": 1485550680000, - "key_as_string": "2017-01-27T20:58:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034 - }, - "doc_count": 1071, - "key": 1485550710000, - "key_as_string": "2017-01-27T20:58:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03333333333333333 - }, - "doc_count": 1074, - "key": 1485550740000, - "key_as_string": "2017-01-27T20:59:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034333333333333334 - }, - "doc_count": 1074, - "key": 1485550770000, - "key_as_string": "2017-01-27T20:59:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.04 - }, - "doc_count": 1074, - "key": 1485550800000, - "key_as_string": "2017-01-27T21:00:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.032 - }, - "doc_count": 1076, - "key": 1485550830000, - "key_as_string": "2017-01-27T21:00:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034 - }, - "doc_count": 1078, - "key": 1485550860000, - "key_as_string": "2017-01-27T21:01:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03266666666666667 - }, - "doc_count": 1077, - "key": 1485550890000, - "key_as_string": "2017-01-27T21:01:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03466666666666667 - }, - "doc_count": 1071, - "key": 1485550920000, - "key_as_string": "2017-01-27T21:02:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03466666666666667 - }, - "doc_count": 1071, - "key": 1485550950000, - "key_as_string": "2017-01-27T21:02:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03566666666666667 - }, - "doc_count": 1073, - "key": 1485550980000, - "key_as_string": "2017-01-27T21:03:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03266666666666667 - }, - "doc_count": 1071, - "key": 1485551010000, - "key_as_string": "2017-01-27T21:03:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03466666666666667 - }, - "doc_count": 1069, - "key": 1485551040000, - "key_as_string": "2017-01-27T21:04:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03566666666666667 - }, - "doc_count": 1068, - "key": 1485551070000, - "key_as_string": "2017-01-27T21:04:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03366666666666667 - }, - "doc_count": 1075, - "key": 1485551100000, - "key_as_string": "2017-01-27T21:05:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034 - }, - "doc_count": 1074, - "key": 1485551130000, - "key_as_string": "2017-01-27T21:05:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03133333333333333 - }, - "doc_count": 1073, - "key": 1485551160000, - "key_as_string": "2017-01-27T21:06:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03266666666666667 - }, - "doc_count": 1071, - "key": 1485551190000, - "key_as_string": "2017-01-27T21:06:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03333333333333333 - }, - "doc_count": 1075, - "key": 1485551220000, - "key_as_string": "2017-01-27T21:07:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03133333333333333 - }, - "doc_count": 1071, - "key": 1485551250000, - "key_as_string": "2017-01-27T21:07:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.04733333333333334 - }, - "doc_count": 1081, - "key": 1485551280000, - "key_as_string": "2017-01-27T21:08:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.044333333333333336 - }, - "doc_count": 1078, - "key": 1485551310000, - "key_as_string": "2017-01-27T21:08:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.037000000000000005 - }, - "doc_count": 1079, - "key": 1485551340000, - "key_as_string": "2017-01-27T21:09:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034 - }, - "doc_count": 1077, - "key": 1485551370000, - "key_as_string": "2017-01-27T21:09:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03866666666666666 - }, - "doc_count": 1077, - "key": 1485551400000, - "key_as_string": "2017-01-27T21:10:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.037666666666666675 - }, - "doc_count": 1075, - "key": 1485551430000, - "key_as_string": "2017-01-27T21:10:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.038000000000000006 - }, - "doc_count": 1078, - "key": 1485551460000, - "key_as_string": "2017-01-27T21:11:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.037 - }, - "doc_count": 1074, - "key": 1485551490000, - "key_as_string": "2017-01-27T21:11:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.036666666666666674 - }, - "doc_count": 1074, - "key": 1485551520000, - "key_as_string": "2017-01-27T21:12:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.037333333333333336 - }, - "doc_count": 1076, - "key": 1485551550000, - "key_as_string": "2017-01-27T21:12:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03733333333333333 - }, - "doc_count": 1075, - "key": 1485551580000, - "key_as_string": "2017-01-27T21:13:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.04533333333333334 - }, - "doc_count": 1077, - "key": 1485551610000, - "key_as_string": "2017-01-27T21:13:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.039 - }, - "doc_count": 1080, - "key": 1485551640000, - "key_as_string": "2017-01-27T21:14:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.057666666666666665 - }, - "doc_count": 1080, - "key": 1485551670000, - "key_as_string": "2017-01-27T21:14:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.045000000000000005 - }, - "doc_count": 1080, - "key": 1485551700000, - "key_as_string": "2017-01-27T21:15:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.037666666666666675 - }, - "doc_count": 1080, - "key": 1485551730000, - "key_as_string": "2017-01-27T21:15:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034333333333333334 - }, - "doc_count": 1080, - "key": 1485551760000, - "key_as_string": "2017-01-27T21:16:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.038 - }, - "doc_count": 1080, - "key": 1485551790000, - "key_as_string": "2017-01-27T21:16:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034333333333333334 - }, - "doc_count": 1080, - "key": 1485551820000, - "key_as_string": "2017-01-27T21:17:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.04966666666666667 - }, - "doc_count": 1080, - "key": 1485551850000, - "key_as_string": "2017-01-27T21:17:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03266666666666667 - }, - "doc_count": 1080, - "key": 1485551880000, - "key_as_string": "2017-01-27T21:18:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.04 - }, - "doc_count": 1080, - "key": 1485551910000, - "key_as_string": "2017-01-27T21:18:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03766666666666666 - }, - "doc_count": 1080, - "key": 1485551940000, - "key_as_string": "2017-01-27T21:19:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034 - }, - "doc_count": 1076, - "key": 1485551970000, - "key_as_string": "2017-01-27T21:19:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034333333333333334 - }, - "doc_count": 1077, - "key": 1485552000000, - "key_as_string": "2017-01-27T21:20:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03366666666666667 - }, - "doc_count": 1077, - "key": 1485552030000, - "key_as_string": "2017-01-27T21:20:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.029666666666666664 - }, - "doc_count": 1077, - "key": 1485552060000, - "key_as_string": "2017-01-27T21:21:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.02766666666666667 - }, - "doc_count": 1077, - "key": 1485552090000, - "key_as_string": "2017-01-27T21:21:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.028 - }, - "doc_count": 1077, - "key": 1485552120000, - "key_as_string": "2017-01-27T21:22:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.030666666666666665 - }, - "doc_count": 1077, - "key": 1485552150000, - "key_as_string": "2017-01-27T21:22:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03833333333333334 - }, - "doc_count": 1083, - "key": 1485552180000, - "key_as_string": "2017-01-27T21:23:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.04966666666666667 - }, - "doc_count": 1083, - "key": 1485552210000, - "key_as_string": "2017-01-27T21:23:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.041 - }, - "doc_count": 1082, - "key": 1485552240000, - "key_as_string": "2017-01-27T21:24:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.037333333333333336 - }, - "doc_count": 1087, - "key": 1485552270000, - "key_as_string": "2017-01-27T21:24:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.039 - }, - "doc_count": 1083, - "key": 1485552300000, - "key_as_string": "2017-01-27T21:25:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03933333333333333 - }, - "doc_count": 1083, - "key": 1485552330000, - "key_as_string": "2017-01-27T21:25:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.037666666666666675 - }, - "doc_count": 1083, - "key": 1485552360000, - "key_as_string": "2017-01-27T21:26:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.04033333333333333 - }, - "doc_count": 1083, - "key": 1485552390000, - "key_as_string": "2017-01-27T21:26:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03866666666666666 - }, - "doc_count": 1082, - "key": 1485552420000, - "key_as_string": "2017-01-27T21:27:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.04033333333333334 - }, - "doc_count": 1083, - "key": 1485552450000, - "key_as_string": "2017-01-27T21:27:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.04 - }, - "doc_count": 1083, - "key": 1485552480000, - "key_as_string": "2017-01-27T21:28:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.04033333333333333 - }, - "doc_count": 1084, - "key": 1485552510000, - "key_as_string": "2017-01-27T21:28:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03333333333333333 - }, - "doc_count": 1083, - "key": 1485552540000, - "key_as_string": "2017-01-27T21:29:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03566666666666667 - }, - "doc_count": 1083, - "key": 1485552570000, - "key_as_string": "2017-01-27T21:29:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.04466666666666667 - }, - "doc_count": 1083, - "key": 1485552600000, - "key_as_string": "2017-01-27T21:30:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03233333333333333 - }, - "doc_count": 1083, - "key": 1485552630000, - "key_as_string": "2017-01-27T21:30:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.0505 - }, - "doc_count": 722, - "key": 1485552660000, - "key_as_string": "2017-01-27T21:31:00.000Z" - } - ] - } - } - }, - "hits": { - "hits": [], - "max_score": 0, - "total": 128145 - }, - "status": 200, - "timed_out": false, - "took": 28 -} diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/fixtures/std_metric_fixture.json b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/fixtures/std_metric_fixture.json deleted file mode 100644 index d587e48b31881..0000000000000 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/fixtures/std_metric_fixture.json +++ /dev/null @@ -1,985 +0,0 @@ -{ - "_shards": { - "failed": 0, - "successful": 5, - "total": 5 - }, - "aggregations": { - "c9b5f9c0-e403-11e6-be91-6f7688e9fac7": { - "doc_count": 128145, - "timeseries": { - "buckets": [ - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.057 - }, - "doc_count": 368, - "key": 1485549090000, - "key_as_string": "2017-01-27T20:31:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.07466666666666667 - }, - "doc_count": 1106, - "key": 1485549120000, - "key_as_string": "2017-01-27T20:32:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.08033333333333335 - }, - "doc_count": 1107, - "key": 1485549150000, - "key_as_string": "2017-01-27T20:32:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.066 - }, - "doc_count": 1109, - "key": 1485549180000, - "key_as_string": "2017-01-27T20:33:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.05366666666666667 - }, - "doc_count": 1093, - "key": 1485549210000, - "key_as_string": "2017-01-27T20:33:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.04533333333333334 - }, - "doc_count": 1086, - "key": 1485549240000, - "key_as_string": "2017-01-27T20:34:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.037333333333333336 - }, - "doc_count": 1086, - "key": 1485549270000, - "key_as_string": "2017-01-27T20:34:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.033 - }, - "doc_count": 1090, - "key": 1485549300000, - "key_as_string": "2017-01-27T20:35:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.036000000000000004 - }, - "doc_count": 1085, - "key": 1485549330000, - "key_as_string": "2017-01-27T20:35:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034333333333333334 - }, - "doc_count": 1082, - "key": 1485549360000, - "key_as_string": "2017-01-27T20:36:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.033 - }, - "doc_count": 1080, - "key": 1485549390000, - "key_as_string": "2017-01-27T20:36:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.037000000000000005 - }, - "doc_count": 1082, - "key": 1485549420000, - "key_as_string": "2017-01-27T20:37:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.036000000000000004 - }, - "doc_count": 1079, - "key": 1485549450000, - "key_as_string": "2017-01-27T20:37:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03233333333333333 - }, - "doc_count": 1080, - "key": 1485549480000, - "key_as_string": "2017-01-27T20:38:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03366666666666667 - }, - "doc_count": 1080, - "key": 1485549510000, - "key_as_string": "2017-01-27T20:38:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03366666666666667 - }, - "doc_count": 1082, - "key": 1485549540000, - "key_as_string": "2017-01-27T20:39:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.042666666666666665 - }, - "doc_count": 1079, - "key": 1485549570000, - "key_as_string": "2017-01-27T20:39:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.033 - }, - "doc_count": 1077, - "key": 1485549600000, - "key_as_string": "2017-01-27T20:40:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03833333333333334 - }, - "doc_count": 1075, - "key": 1485549630000, - "key_as_string": "2017-01-27T20:40:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034 - }, - "doc_count": 1076, - "key": 1485549660000, - "key_as_string": "2017-01-27T20:41:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03366666666666667 - }, - "doc_count": 1076, - "key": 1485549690000, - "key_as_string": "2017-01-27T20:41:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034 - }, - "doc_count": 1074, - "key": 1485549720000, - "key_as_string": "2017-01-27T20:42:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03266666666666667 - }, - "doc_count": 1072, - "key": 1485549750000, - "key_as_string": "2017-01-27T20:42:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03266666666666667 - }, - "doc_count": 1067, - "key": 1485549780000, - "key_as_string": "2017-01-27T20:43:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.036000000000000004 - }, - "doc_count": 1065, - "key": 1485549810000, - "key_as_string": "2017-01-27T20:43:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03266666666666667 - }, - "doc_count": 1065, - "key": 1485549840000, - "key_as_string": "2017-01-27T20:44:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.036333333333333336 - }, - "doc_count": 1062, - "key": 1485549870000, - "key_as_string": "2017-01-27T20:44:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034333333333333334 - }, - "doc_count": 1063, - "key": 1485549900000, - "key_as_string": "2017-01-27T20:45:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034333333333333334 - }, - "doc_count": 1065, - "key": 1485549930000, - "key_as_string": "2017-01-27T20:45:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03366666666666667 - }, - "doc_count": 1065, - "key": 1485549960000, - "key_as_string": "2017-01-27T20:46:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034333333333333334 - }, - "doc_count": 1069, - "key": 1485549990000, - "key_as_string": "2017-01-27T20:46:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03333333333333333 - }, - "doc_count": 1068, - "key": 1485550020000, - "key_as_string": "2017-01-27T20:47:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.033 - }, - "doc_count": 1068, - "key": 1485550050000, - "key_as_string": "2017-01-27T20:47:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.033 - }, - "doc_count": 1068, - "key": 1485550080000, - "key_as_string": "2017-01-27T20:48:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03366666666666667 - }, - "doc_count": 1074, - "key": 1485550110000, - "key_as_string": "2017-01-27T20:48:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03133333333333333 - }, - "doc_count": 1074, - "key": 1485550140000, - "key_as_string": "2017-01-27T20:49:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03266666666666667 - }, - "doc_count": 1074, - "key": 1485550170000, - "key_as_string": "2017-01-27T20:49:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03133333333333333 - }, - "doc_count": 1073, - "key": 1485550200000, - "key_as_string": "2017-01-27T20:50:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.033 - }, - "doc_count": 1077, - "key": 1485550230000, - "key_as_string": "2017-01-27T20:50:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03166666666666667 - }, - "doc_count": 1074, - "key": 1485550260000, - "key_as_string": "2017-01-27T20:51:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.031 - }, - "doc_count": 1074, - "key": 1485550290000, - "key_as_string": "2017-01-27T20:51:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03266666666666667 - }, - "doc_count": 1072, - "key": 1485550320000, - "key_as_string": "2017-01-27T20:52:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.033 - }, - "doc_count": 1073, - "key": 1485550350000, - "key_as_string": "2017-01-27T20:52:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03466666666666667 - }, - "doc_count": 1071, - "key": 1485550380000, - "key_as_string": "2017-01-27T20:53:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03266666666666667 - }, - "doc_count": 1071, - "key": 1485550410000, - "key_as_string": "2017-01-27T20:53:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03166666666666667 - }, - "doc_count": 1069, - "key": 1485550440000, - "key_as_string": "2017-01-27T20:54:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034 - }, - "doc_count": 1069, - "key": 1485550470000, - "key_as_string": "2017-01-27T20:54:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.032 - }, - "doc_count": 1068, - "key": 1485550500000, - "key_as_string": "2017-01-27T20:55:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03366666666666667 - }, - "doc_count": 1067, - "key": 1485550530000, - "key_as_string": "2017-01-27T20:55:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03233333333333333 - }, - "doc_count": 1065, - "key": 1485550560000, - "key_as_string": "2017-01-27T20:56:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03366666666666667 - }, - "doc_count": 1069, - "key": 1485550590000, - "key_as_string": "2017-01-27T20:56:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03166666666666667 - }, - "doc_count": 1068, - "key": 1485550620000, - "key_as_string": "2017-01-27T20:57:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03333333333333333 - }, - "doc_count": 1068, - "key": 1485550650000, - "key_as_string": "2017-01-27T20:57:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03266666666666667 - }, - "doc_count": 1068, - "key": 1485550680000, - "key_as_string": "2017-01-27T20:58:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034 - }, - "doc_count": 1071, - "key": 1485550710000, - "key_as_string": "2017-01-27T20:58:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03333333333333333 - }, - "doc_count": 1074, - "key": 1485550740000, - "key_as_string": "2017-01-27T20:59:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034333333333333334 - }, - "doc_count": 1074, - "key": 1485550770000, - "key_as_string": "2017-01-27T20:59:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.04 - }, - "doc_count": 1074, - "key": 1485550800000, - "key_as_string": "2017-01-27T21:00:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.032 - }, - "doc_count": 1076, - "key": 1485550830000, - "key_as_string": "2017-01-27T21:00:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034 - }, - "doc_count": 1078, - "key": 1485550860000, - "key_as_string": "2017-01-27T21:01:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03266666666666667 - }, - "doc_count": 1077, - "key": 1485550890000, - "key_as_string": "2017-01-27T21:01:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03466666666666667 - }, - "doc_count": 1071, - "key": 1485550920000, - "key_as_string": "2017-01-27T21:02:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03466666666666667 - }, - "doc_count": 1071, - "key": 1485550950000, - "key_as_string": "2017-01-27T21:02:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03566666666666667 - }, - "doc_count": 1073, - "key": 1485550980000, - "key_as_string": "2017-01-27T21:03:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03266666666666667 - }, - "doc_count": 1071, - "key": 1485551010000, - "key_as_string": "2017-01-27T21:03:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03466666666666667 - }, - "doc_count": 1069, - "key": 1485551040000, - "key_as_string": "2017-01-27T21:04:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03566666666666667 - }, - "doc_count": 1068, - "key": 1485551070000, - "key_as_string": "2017-01-27T21:04:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03366666666666667 - }, - "doc_count": 1075, - "key": 1485551100000, - "key_as_string": "2017-01-27T21:05:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034 - }, - "doc_count": 1074, - "key": 1485551130000, - "key_as_string": "2017-01-27T21:05:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03133333333333333 - }, - "doc_count": 1073, - "key": 1485551160000, - "key_as_string": "2017-01-27T21:06:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03266666666666667 - }, - "doc_count": 1071, - "key": 1485551190000, - "key_as_string": "2017-01-27T21:06:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03333333333333333 - }, - "doc_count": 1075, - "key": 1485551220000, - "key_as_string": "2017-01-27T21:07:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03133333333333333 - }, - "doc_count": 1071, - "key": 1485551250000, - "key_as_string": "2017-01-27T21:07:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.04733333333333334 - }, - "doc_count": 1081, - "key": 1485551280000, - "key_as_string": "2017-01-27T21:08:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.044333333333333336 - }, - "doc_count": 1078, - "key": 1485551310000, - "key_as_string": "2017-01-27T21:08:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.037000000000000005 - }, - "doc_count": 1079, - "key": 1485551340000, - "key_as_string": "2017-01-27T21:09:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034 - }, - "doc_count": 1077, - "key": 1485551370000, - "key_as_string": "2017-01-27T21:09:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03866666666666666 - }, - "doc_count": 1077, - "key": 1485551400000, - "key_as_string": "2017-01-27T21:10:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.037666666666666675 - }, - "doc_count": 1075, - "key": 1485551430000, - "key_as_string": "2017-01-27T21:10:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.038000000000000006 - }, - "doc_count": 1078, - "key": 1485551460000, - "key_as_string": "2017-01-27T21:11:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.037 - }, - "doc_count": 1074, - "key": 1485551490000, - "key_as_string": "2017-01-27T21:11:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.036666666666666674 - }, - "doc_count": 1074, - "key": 1485551520000, - "key_as_string": "2017-01-27T21:12:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.037333333333333336 - }, - "doc_count": 1076, - "key": 1485551550000, - "key_as_string": "2017-01-27T21:12:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03733333333333333 - }, - "doc_count": 1075, - "key": 1485551580000, - "key_as_string": "2017-01-27T21:13:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.04533333333333334 - }, - "doc_count": 1077, - "key": 1485551610000, - "key_as_string": "2017-01-27T21:13:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.039 - }, - "doc_count": 1080, - "key": 1485551640000, - "key_as_string": "2017-01-27T21:14:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.057666666666666665 - }, - "doc_count": 1080, - "key": 1485551670000, - "key_as_string": "2017-01-27T21:14:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.045000000000000005 - }, - "doc_count": 1080, - "key": 1485551700000, - "key_as_string": "2017-01-27T21:15:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.037666666666666675 - }, - "doc_count": 1080, - "key": 1485551730000, - "key_as_string": "2017-01-27T21:15:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034333333333333334 - }, - "doc_count": 1080, - "key": 1485551760000, - "key_as_string": "2017-01-27T21:16:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.038 - }, - "doc_count": 1080, - "key": 1485551790000, - "key_as_string": "2017-01-27T21:16:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034333333333333334 - }, - "doc_count": 1080, - "key": 1485551820000, - "key_as_string": "2017-01-27T21:17:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.04966666666666667 - }, - "doc_count": 1080, - "key": 1485551850000, - "key_as_string": "2017-01-27T21:17:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03266666666666667 - }, - "doc_count": 1080, - "key": 1485551880000, - "key_as_string": "2017-01-27T21:18:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.04 - }, - "doc_count": 1080, - "key": 1485551910000, - "key_as_string": "2017-01-27T21:18:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03766666666666666 - }, - "doc_count": 1080, - "key": 1485551940000, - "key_as_string": "2017-01-27T21:19:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034 - }, - "doc_count": 1076, - "key": 1485551970000, - "key_as_string": "2017-01-27T21:19:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.034333333333333334 - }, - "doc_count": 1077, - "key": 1485552000000, - "key_as_string": "2017-01-27T21:20:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03366666666666667 - }, - "doc_count": 1077, - "key": 1485552030000, - "key_as_string": "2017-01-27T21:20:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.029666666666666664 - }, - "doc_count": 1077, - "key": 1485552060000, - "key_as_string": "2017-01-27T21:21:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.02766666666666667 - }, - "doc_count": 1077, - "key": 1485552090000, - "key_as_string": "2017-01-27T21:21:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.028 - }, - "doc_count": 1077, - "key": 1485552120000, - "key_as_string": "2017-01-27T21:22:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.030666666666666665 - }, - "doc_count": 1077, - "key": 1485552150000, - "key_as_string": "2017-01-27T21:22:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03833333333333334 - }, - "doc_count": 1083, - "key": 1485552180000, - "key_as_string": "2017-01-27T21:23:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.04966666666666667 - }, - "doc_count": 1083, - "key": 1485552210000, - "key_as_string": "2017-01-27T21:23:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.041 - }, - "doc_count": 1082, - "key": 1485552240000, - "key_as_string": "2017-01-27T21:24:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.037333333333333336 - }, - "doc_count": 1087, - "key": 1485552270000, - "key_as_string": "2017-01-27T21:24:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.039 - }, - "doc_count": 1083, - "key": 1485552300000, - "key_as_string": "2017-01-27T21:25:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03933333333333333 - }, - "doc_count": 1083, - "key": 1485552330000, - "key_as_string": "2017-01-27T21:25:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.037666666666666675 - }, - "doc_count": 1083, - "key": 1485552360000, - "key_as_string": "2017-01-27T21:26:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.04033333333333333 - }, - "doc_count": 1083, - "key": 1485552390000, - "key_as_string": "2017-01-27T21:26:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03866666666666666 - }, - "doc_count": 1082, - "key": 1485552420000, - "key_as_string": "2017-01-27T21:27:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.04033333333333334 - }, - "doc_count": 1083, - "key": 1485552450000, - "key_as_string": "2017-01-27T21:27:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.04 - }, - "doc_count": 1083, - "key": 1485552480000, - "key_as_string": "2017-01-27T21:28:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.04033333333333333 - }, - "doc_count": 1084, - "key": 1485552510000, - "key_as_string": "2017-01-27T21:28:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03333333333333333 - }, - "doc_count": 1083, - "key": 1485552540000, - "key_as_string": "2017-01-27T21:29:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03566666666666667 - }, - "doc_count": 1083, - "key": 1485552570000, - "key_as_string": "2017-01-27T21:29:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.04466666666666667 - }, - "doc_count": 1083, - "key": 1485552600000, - "key_as_string": "2017-01-27T21:30:00.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.03233333333333333 - }, - "doc_count": 1083, - "key": 1485552630000, - "key_as_string": "2017-01-27T21:30:30.000Z" - }, - { - "c9b5f9c1-e403-11e6-be91-6f7688e9fac7": { - "value": 0.0505 - }, - "doc_count": 722, - "key": 1485552660000, - "key_as_string": "2017-01-27T21:31:00.000Z" - } - ] - } - } - }, - "hits": { - "hits": [], - "max_score": 0, - "total": 128145 - }, - "status": 200, - "timed_out": false, - "took": 28 -} - diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/bucket_transform.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/bucket_transform.js deleted file mode 100644 index 1414435017c86..0000000000000 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/bucket_transform.js +++ /dev/null @@ -1,388 +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 { expect } from 'chai'; -import { bucketTransform } from '../../helpers/bucket_transform'; - -describe('bucketTransform', () => { - describe('count', () => { - it('returns count agg', () => { - const metric = { id: 'test', type: 'count' }; - const fn = bucketTransform.count; - expect(fn(metric)).to.eql({ - bucket_script: { - buckets_path: { count: '_count' }, - script: { source: 'count * 1', lang: 'expression' }, - gap_policy: 'skip', - }, - }); - }); - }); - - describe('std metric', () => { - ['avg', 'max', 'min', 'sum', 'cardinality', 'value_count'].forEach(type => { - it(`returns ${type} agg`, () => { - const metric = { id: 'test', type: type, field: 'cpu.pct' }; - const fn = bucketTransform[type]; - const result = {}; - result[type] = { field: 'cpu.pct' }; - expect(fn(metric)).to.eql(result); - }); - }); - - it('throws error if type is missing', () => { - const run = () => bucketTransform.avg({ id: 'test', field: 'cpu.pct' }); - expect(run).to.throw(Error, 'Metric missing type'); - }); - - it('throws error if field is missing', () => { - const run = () => bucketTransform.avg({ id: 'test', type: 'avg' }); - expect(run).to.throw(Error, 'Metric missing field'); - }); - }); - - describe('extended stats', () => { - ['std_deviation', 'variance', 'sum_of_squares'].forEach(type => { - it(`returns ${type} agg`, () => { - const fn = bucketTransform[type]; - const metric = { id: 'test', type: type, field: 'cpu.pct' }; - expect(fn(metric)).to.eql({ extended_stats: { field: 'cpu.pct' } }); - }); - }); - - it('returns std_deviation agg with sigma', () => { - const fn = bucketTransform.std_deviation; - const metric = { - id: 'test', - type: 'std_deviation', - field: 'cpu.pct', - sigma: 2, - }; - expect(fn(metric)).to.eql({ - extended_stats: { field: 'cpu.pct', sigma: 2 }, - }); - }); - - it('throws error if type is missing', () => { - const run = () => bucketTransform.std_deviation({ id: 'test', field: 'cpu.pct' }); - expect(run).to.throw(Error, 'Metric missing type'); - }); - - it('throws error if field is missing', () => { - const run = () => bucketTransform.std_deviation({ id: 'test', type: 'avg' }); - expect(run).to.throw(Error, 'Metric missing field'); - }); - }); - - describe('percentiles', () => { - it('returns percentiles agg', () => { - const metric = { - id: 'test', - type: 'percentile', - field: 'cpu.pct', - percentiles: [ - { value: 50, mode: 'line' }, - { value: 10, mode: 'band', percentile: 90 }, - ], - }; - const fn = bucketTransform.percentile; - expect(fn(metric)).to.eql({ - percentiles: { - field: 'cpu.pct', - percents: [50, 10, 90], - }, - }); - }); - - it('define a default 0 value if it was not provided', () => { - const metric = { - id: 'test', - type: 'percentile', - field: 'cpu.pct', - percentiles: [ - { value: 50, mode: 'line' }, - { mode: 'line' }, - { value: undefined, mode: 'line' }, - { value: '', mode: 'line' }, - { value: null, mode: 'line' }, - { value: 0, mode: 'line' }, - ], - }; - expect(bucketTransform.percentile(metric)).to.eql({ - percentiles: { - field: 'cpu.pct', - percents: [50, 0, 0, 0, 0, 0], - }, - }); - }); - - it('throws error if type is missing', () => { - const run = () => - bucketTransform.percentile({ - id: 'test', - field: 'cpu.pct', - percentiles: [{ value: 50, mode: 'line' }], - }); - expect(run).to.throw(Error, 'Metric missing type'); - }); - - it('throws error if field is missing', () => { - const run = () => - bucketTransform.percentile({ - id: 'test', - type: 'avg', - percentiles: [{ value: 50, mode: 'line' }], - }); - expect(run).to.throw(Error, 'Metric missing field'); - }); - - it('throws error if percentiles is missing', () => { - const run = () => - bucketTransform.percentile({ - id: 'test', - type: 'avg', - field: 'cpu.pct', - }); - expect(run).to.throw(Error, 'Metric missing percentiles'); - }); - }); - - describe('derivative', () => { - it('returns derivative agg with defaults', () => { - const metric = { - id: '2', - type: 'derivative', - field: '1', - }; - const metrics = [{ id: '1', type: 'max', field: 'cpu.pct' }, metric]; - const fn = bucketTransform.derivative; - expect(fn(metric, metrics, '10s')).is.eql({ - derivative: { - buckets_path: '1', - gap_policy: 'skip', - unit: '10s', - }, - }); - }); - - it('returns derivative agg with unit', () => { - const metric = { - id: '2', - type: 'derivative', - field: '1', - unit: '1s', - }; - const metrics = [{ id: '1', type: 'max', field: 'cpu.pct' }, metric]; - const fn = bucketTransform.derivative; - expect(fn(metric, metrics, '10s')).is.eql({ - derivative: { - buckets_path: '1', - gap_policy: 'skip', - unit: '1s', - }, - }); - }); - - it('returns derivative agg with gap_policy', () => { - const metric = { - id: '2', - type: 'derivative', - field: '1', - gap_policy: 'zero_fill', - }; - const metrics = [{ id: '1', type: 'max', field: 'cpu.pct' }, metric]; - const fn = bucketTransform.derivative; - expect(fn(metric, metrics, '10s')).is.eql({ - derivative: { - buckets_path: '1', - gap_policy: 'zero_fill', - unit: '10s', - }, - }); - }); - - it('throws error if type is missing', () => { - const run = () => bucketTransform.derivative({ id: 'test', field: 'cpu.pct' }); - expect(run).to.throw(Error, 'Metric missing type'); - }); - - it('throws error if field is missing', () => { - const run = () => bucketTransform.derivative({ id: 'test', type: 'derivative' }); - expect(run).to.throw(Error, 'Metric missing field'); - }); - }); - - describe('serial_diff', () => { - it('returns serial_diff agg with defaults', () => { - const metric = { - id: '2', - type: 'serial_diff', - field: '1', - }; - const metrics = [{ id: '1', type: 'max', field: 'cpu.pct' }, metric]; - const fn = bucketTransform.serial_diff; - expect(fn(metric, metrics)).is.eql({ - serial_diff: { - buckets_path: '1', - gap_policy: 'skip', - lag: 1, - }, - }); - }); - - it('returns serial_diff agg with lag', () => { - const metric = { - id: '2', - type: 'serial_diff', - field: '1', - lag: 10, - }; - const metrics = [{ id: '1', type: 'max', field: 'cpu.pct' }, metric]; - const fn = bucketTransform.serial_diff; - expect(fn(metric, metrics)).is.eql({ - serial_diff: { - buckets_path: '1', - gap_policy: 'skip', - lag: 10, - }, - }); - }); - - it('returns serial_diff agg with gap_policy', () => { - const metric = { - id: '2', - type: 'serial_diff', - field: '1', - gap_policy: 'zero_fill', - }; - const metrics = [{ id: '1', type: 'max', field: 'cpu.pct' }, metric]; - const fn = bucketTransform.serial_diff; - expect(fn(metric, metrics)).is.eql({ - serial_diff: { - buckets_path: '1', - gap_policy: 'zero_fill', - lag: 1, - }, - }); - }); - - it('throws error if type is missing', () => { - const run = () => bucketTransform.serial_diff({ id: 'test', field: 'cpu.pct' }); - expect(run).to.throw(Error, 'Metric missing type'); - }); - - it('throws error if field is missing', () => { - const run = () => bucketTransform.serial_diff({ id: 'test', type: 'serial_diff' }); - expect(run).to.throw(Error, 'Metric missing field'); - }); - }); - - describe('cumulative_sum', () => { - it('returns cumulative_sum agg', () => { - const metric = { id: '2', type: 'cumulative_sum', field: '1' }; - const metrics = [{ id: '1', type: 'sum', field: 'cpu.pct' }, metric]; - const fn = bucketTransform.cumulative_sum; - expect(fn(metric, metrics, '10s')).is.eql({ - cumulative_sum: { buckets_path: '1' }, - }); - }); - - it('throws error if type is missing', () => { - const run = () => bucketTransform.cumulative_sum({ id: 'test', field: 'cpu.pct' }); - expect(run).to.throw(Error, 'Metric missing type'); - }); - - it('throws error if field is missing', () => { - const run = () => bucketTransform.cumulative_sum({ id: 'test', type: 'cumulative_sum' }); - expect(run).to.throw(Error, 'Metric missing field'); - }); - }); - - describe('calculation', () => { - it('returns calculation(bucket_script)', () => { - const metric = { - id: '2', - type: 'calculation', - script: 'params.idle != null ? 1 - params.idle : 0', - variables: [{ name: 'idle', field: '1' }], - }; - const metrics = [{ id: '1', type: 'avg', field: 'cpu.idle.pct' }, metric]; - const fn = bucketTransform.calculation; - expect(fn(metric, metrics, '10s')).is.eql({ - bucket_script: { - buckets_path: { - idle: '1', - }, - gap_policy: 'skip', - script: { - source: 'params.idle != null ? 1 - params.idle : 0', - lang: 'painless', - params: { - _interval: 10000, - }, - }, - }, - }); - }); - - it('throws error if variables is missing', () => { - const run = () => - bucketTransform.calculation({ - id: 'test', - type: 'calculation', - script: 'params.idle != null ? 1 - params.idle : null', - }); - expect(run).to.throw(Error, 'Metric missing variables'); - }); - - it('throws error if script is missing', () => { - const run = () => - bucketTransform.calculation({ - id: 'test', - type: 'calculation', - variables: [{ field: '1', name: 'idle' }], - }); - expect(run).to.throw(Error, 'Metric missing script'); - }); - }); - - describe('positive_only', () => { - it('returns bucket_script', () => { - const metric = { - id: '2', - type: 'positive_only', - field: '1', - }; - const metrics = [{ id: '1', type: 'avg', field: 'cpu.idle.pct' }, metric]; - const fn = bucketTransform.positive_only; - expect(fn(metric, metrics, '10s')).is.eql({ - bucket_script: { - buckets_path: { - value: '1', - }, - gap_policy: 'skip', - script: { - source: 'params.value > 0.0 ? params.value : 0.0', - lang: 'painless', - }, - }, - }); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/build_processor_function.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/build_processor_function.test.ts similarity index 58% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/build_processor_function.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/build_processor_function.test.ts index 10ad9e467610e..cf5244f8e5a7d 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/build_processor_function.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/build_processor_function.test.ts @@ -17,43 +17,41 @@ * under the License. */ -import sinon from 'sinon'; -import { expect } from 'chai'; -import { buildProcessorFunction } from '../build_processor_function'; +import { buildProcessorFunction } from './build_processor_function'; describe('buildProcessorFunction(chain, ...args)', () => { const req = {}; const panel = {}; const series = {}; - it('should call each processor', () => { - const first = sinon.spy(() => next => doc => next(doc)); - const second = sinon.spy(() => next => doc => next(doc)); + test('should call each processor', () => { + const first = jest.fn(() => (next: any) => (doc: any) => next(doc)); + const second = jest.fn(() => (next: any) => (doc: any) => next(doc)); buildProcessorFunction([first, second], req, panel, series); - expect(first.calledOnce).to.equal(true); - expect(second.calledOnce).to.equal(true); + expect(first.mock.calls.length).toEqual(1); + expect(second.mock.calls.length).toEqual(1); }); - it('should chain each processor', () => { - const first = sinon.spy(next => doc => next(doc)); - const second = sinon.spy(next => doc => next(doc)); + test('should chain each processor', () => { + const first = jest.fn(() => (next: any) => (doc: any) => next(doc)); + const second = jest.fn(() => (next: any) => (doc: any) => next(doc)); buildProcessorFunction([() => first, () => second], req, panel, series); - expect(first.calledOnce).to.equal(true); - expect(second.calledOnce).to.equal(true); + expect(first.mock.calls.length).toEqual(1); + expect(second.mock.calls.length).toEqual(1); }); - it('should next of each processor', () => { - const first = sinon.spy(); - const second = sinon.spy(); + test('should next of each processor', () => { + const first = jest.fn(); + const second = jest.fn(); const fn = buildProcessorFunction( [ - () => next => doc => { + () => (next: any) => (doc: any) => { first(); next(doc); }, - () => next => doc => { + () => (next: any) => (doc: any) => { second(); next(doc); }, @@ -63,7 +61,7 @@ describe('buildProcessorFunction(chain, ...args)', () => { series ); fn({}); - expect(first.calledOnce).to.equal(true); - expect(second.calledOnce).to.equal(true); + expect(first.mock.calls.length).toEqual(1); + expect(second.mock.calls.length).toEqual(1); }); }); diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/build_processor_function.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/build_processor_function.ts similarity index 86% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/build_processor_function.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/build_processor_function.ts index 5896a089bafe2..b898f4dbf7a5e 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/build_processor_function.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/build_processor_function.ts @@ -17,11 +17,9 @@ * under the License. */ -export function buildProcessorFunction(chain, ...args) { +export function buildProcessorFunction(chain: any[], ...args: any) { return chain.reduceRight( - (next, fn) => { - return fn(...args)(next); - }, - doc => doc + (next, fn) => fn(...args)(next), + (doc: any) => doc ); } diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/get_interval_and_timefield.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.test.js similarity index 79% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/get_interval_and_timefield.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.test.js index dc2c2b2cfb1f2..f3e15f2fc65b6 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/get_interval_and_timefield.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.test.js @@ -17,27 +17,26 @@ * under the License. */ -import { expect } from 'chai'; -import { getIntervalAndTimefield } from '../get_interval_and_timefield'; +import { getIntervalAndTimefield } from './get_interval_and_timefield'; describe('getIntervalAndTimefield(panel, series)', () => { - it('returns the panel interval and timefield', () => { + test('returns the panel interval and timefield', () => { const panel = { time_field: '@timestamp', interval: 'auto' }; const series = {}; - expect(getIntervalAndTimefield(panel, series)).to.eql({ + expect(getIntervalAndTimefield(panel, series)).toEqual({ timeField: '@timestamp', interval: 'auto', }); }); - it('returns the series interval and timefield', () => { + test('returns the series interval and timefield', () => { const panel = { time_field: '@timestamp', interval: 'auto' }; const series = { override_index_pattern: true, series_interval: '1m', series_time_field: 'time', }; - expect(getIntervalAndTimefield(panel, series)).to.eql({ + expect(getIntervalAndTimefield(panel, series)).toEqual({ timeField: 'time', interval: '1m', }); diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.test.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.test.js index 3def18997863e..db0e8fa3d6bb9 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.test.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.test.js @@ -94,4 +94,371 @@ describe('src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/bucket_tra }); }); }); + + describe('bucketTransform additional', () => { + describe('count', () => { + test('returns count agg', () => { + const metric = { id: 'test', type: 'count' }; + const fn = bucketTransform.count; + expect(fn(metric)).toEqual({ + bucket_script: { + buckets_path: { count: '_count' }, + script: { source: 'count * 1', lang: 'expression' }, + gap_policy: 'skip', + }, + }); + }); + }); + + describe('std metric', () => { + ['avg', 'max', 'min', 'sum', 'cardinality', 'value_count'].forEach(type => { + test(`returns ${type} agg`, () => { + const metric = { id: 'test', type: type, field: 'cpu.pct' }; + const fn = bucketTransform[type]; + const result = {}; + result[type] = { field: 'cpu.pct' }; + expect(fn(metric)).toEqual(result); + }); + }); + + test('throws error if type is missing', () => { + const run = () => bucketTransform.avg({ id: 'test', field: 'cpu.pct' }); + expect(run).toThrow(new Error('Metric missing type')); + }); + + test('throws error if field is missing', () => { + const run = () => bucketTransform.avg({ id: 'test', type: 'avg' }); + expect(run).toThrow(new Error('Metric missing field')); + }); + }); + + describe('extended stats', () => { + ['std_deviation', 'variance', 'sum_of_squares'].forEach(type => { + test(`returns ${type} agg`, () => { + const fn = bucketTransform[type]; + const metric = { id: 'test', type: type, field: 'cpu.pct' }; + expect(fn(metric)).toEqual({ extended_stats: { field: 'cpu.pct' } }); + }); + }); + + test('returns std_deviation agg with sigma', () => { + const fn = bucketTransform.std_deviation; + const metric = { + id: 'test', + type: 'std_deviation', + field: 'cpu.pct', + sigma: 2, + }; + expect(fn(metric)).toEqual({ + extended_stats: { field: 'cpu.pct', sigma: 2 }, + }); + }); + + test('throws error if type is missing', () => { + const run = () => bucketTransform.std_deviation({ id: 'test', field: 'cpu.pct' }); + expect(run).toThrow(new Error('Metric missing type')); + }); + + test('throws error if field is missing', () => { + const run = () => bucketTransform.std_deviation({ id: 'test', type: 'avg' }); + expect(run).toThrow(new Error('Metric missing field')); + }); + }); + + describe('percentiles', () => { + test('returns percentiles agg', () => { + const metric = { + id: 'test', + type: 'percentile', + field: 'cpu.pct', + percentiles: [ + { value: 50, mode: 'line' }, + { value: 10, mode: 'band', percentile: 90 }, + ], + }; + const fn = bucketTransform.percentile; + expect(fn(metric)).toEqual({ + percentiles: { + field: 'cpu.pct', + percents: [50, 10, 90], + }, + }); + }); + + test('define a default 0 value if it was not provided', () => { + const metric = { + id: 'test', + type: 'percentile', + field: 'cpu.pct', + percentiles: [ + { value: 50, mode: 'line' }, + { mode: 'line' }, + { value: undefined, mode: 'line' }, + { value: '', mode: 'line' }, + { value: null, mode: 'line' }, + { value: 0, mode: 'line' }, + ], + }; + expect(bucketTransform.percentile(metric)).toEqual({ + percentiles: { + field: 'cpu.pct', + percents: [50, 0, 0, 0, 0, 0], + }, + }); + }); + + test('throws error if type is missing', () => { + const run = () => + bucketTransform.percentile({ + id: 'test', + field: 'cpu.pct', + percentiles: [{ value: 50, mode: 'line' }], + }); + expect(run).toThrow(new Error('Metric missing type')); + }); + + test('throws error if field is missing', () => { + const run = () => + bucketTransform.percentile({ + id: 'test', + type: 'avg', + percentiles: [{ value: 50, mode: 'line' }], + }); + expect(run).toThrow(new Error('Metric missing field')); + }); + + test('throws error if percentiles is missing', () => { + const run = () => + bucketTransform.percentile({ + id: 'test', + type: 'avg', + field: 'cpu.pct', + }); + expect(run).toThrow(new Error('Metric missing percentiles')); + }); + }); + + describe('derivative', () => { + test('returns derivative agg with defaults', () => { + const metric = { + id: '2', + type: 'derivative', + field: '1', + }; + const metrics = [{ id: '1', type: 'max', field: 'cpu.pct' }, metric]; + const fn = bucketTransform.derivative; + expect(fn(metric, metrics, '10s')).toEqual({ + derivative: { + buckets_path: '1', + gap_policy: 'skip', + unit: '10s', + }, + }); + }); + + test('returns derivative agg with unit', () => { + const metric = { + id: '2', + type: 'derivative', + field: '1', + unit: '1s', + }; + const metrics = [{ id: '1', type: 'max', field: 'cpu.pct' }, metric]; + const fn = bucketTransform.derivative; + expect(fn(metric, metrics, '10s')).toEqual({ + derivative: { + buckets_path: '1', + gap_policy: 'skip', + unit: '1s', + }, + }); + }); + + test('returns derivative agg with gap_policy', () => { + const metric = { + id: '2', + type: 'derivative', + field: '1', + gap_policy: 'zero_fill', + }; + const metrics = [{ id: '1', type: 'max', field: 'cpu.pct' }, metric]; + const fn = bucketTransform.derivative; + expect(fn(metric, metrics, '10s')).toEqual({ + derivative: { + buckets_path: '1', + gap_policy: 'zero_fill', + unit: '10s', + }, + }); + }); + + test('throws error if type is missing', () => { + const run = () => bucketTransform.derivative({ id: 'test', field: 'cpu.pct' }); + expect(run).toThrow(new Error('Metric missing type')); + }); + + test('throws error if field is missing', () => { + const run = () => bucketTransform.derivative({ id: 'test', type: 'derivative' }); + expect(run).toThrow(new Error('Metric missing field')); + }); + }); + + describe('serial_diff', () => { + test('returns serial_diff agg with defaults', () => { + const metric = { + id: '2', + type: 'serial_diff', + field: '1', + }; + const metrics = [{ id: '1', type: 'max', field: 'cpu.pct' }, metric]; + const fn = bucketTransform.serial_diff; + expect(fn(metric, metrics)).toEqual({ + serial_diff: { + buckets_path: '1', + gap_policy: 'skip', + lag: 1, + }, + }); + }); + + test('returns serial_diff agg with lag', () => { + const metric = { + id: '2', + type: 'serial_diff', + field: '1', + lag: 10, + }; + const metrics = [{ id: '1', type: 'max', field: 'cpu.pct' }, metric]; + const fn = bucketTransform.serial_diff; + expect(fn(metric, metrics)).toEqual({ + serial_diff: { + buckets_path: '1', + gap_policy: 'skip', + lag: 10, + }, + }); + }); + + test('returns serial_diff agg with gap_policy', () => { + const metric = { + id: '2', + type: 'serial_diff', + field: '1', + gap_policy: 'zero_fill', + }; + const metrics = [{ id: '1', type: 'max', field: 'cpu.pct' }, metric]; + const fn = bucketTransform.serial_diff; + expect(fn(metric, metrics)).toEqual({ + serial_diff: { + buckets_path: '1', + gap_policy: 'zero_fill', + lag: 1, + }, + }); + }); + + test('throws error if type is missing', () => { + const run = () => bucketTransform.serial_diff({ id: 'test', field: 'cpu.pct' }); + expect(run).toThrow(new Error('Metric missing type')); + }); + + test('throws error if field is missing', () => { + const run = () => bucketTransform.serial_diff({ id: 'test', type: 'serial_diff' }); + expect(run).toThrow(new Error('Metric missing field')); + }); + }); + + describe('cumulative_sum', () => { + test('returns cumulative_sum agg', () => { + const metric = { id: '2', type: 'cumulative_sum', field: '1' }; + const metrics = [{ id: '1', type: 'sum', field: 'cpu.pct' }, metric]; + const fn = bucketTransform.cumulative_sum; + expect(fn(metric, metrics, '10s')).toEqual({ + cumulative_sum: { buckets_path: '1' }, + }); + }); + + test('throws error if type is missing', () => { + const run = () => bucketTransform.cumulative_sum({ id: 'test', field: 'cpu.pct' }); + expect(run).toThrow(new Error('Metric missing type')); + }); + + test('throws error if field is missing', () => { + const run = () => bucketTransform.cumulative_sum({ id: 'test', type: 'cumulative_sum' }); + expect(run).toThrow(new Error('Metric missing field')); + }); + }); + + describe('calculation', () => { + test('returns calculation(bucket_script)', () => { + const metric = { + id: '2', + type: 'calculation', + script: 'params.idle != null ? 1 - params.idle : 0', + variables: [{ name: 'idle', field: '1' }], + }; + const metrics = [{ id: '1', type: 'avg', field: 'cpu.idle.pct' }, metric]; + const fn = bucketTransform.calculation; + expect(fn(metric, metrics, '10s')).toEqual({ + bucket_script: { + buckets_path: { + idle: '1', + }, + gap_policy: 'skip', + script: { + source: 'params.idle != null ? 1 - params.idle : 0', + lang: 'painless', + params: { + _interval: 10000, + }, + }, + }, + }); + }); + + test('throws error if variables is missing', () => { + const run = () => + bucketTransform.calculation({ + id: 'test', + type: 'calculation', + script: 'params.idle != null ? 1 - params.idle : null', + }); + expect(run).toThrow(new Error('Metric missing variables')); + }); + + test('throws error if script is missing', () => { + const run = () => + bucketTransform.calculation({ + id: 'test', + type: 'calculation', + variables: [{ field: '1', name: 'idle' }], + }); + expect(run).toThrow(new Error('Metric missing script')); + }); + }); + + describe('positive_only', () => { + test('returns bucket_script', () => { + const metric = { + id: '2', + type: 'positive_only', + field: '1', + }; + const metrics = [{ id: '1', type: 'avg', field: 'cpu.idle.pct' }, metric]; + const fn = bucketTransform.positive_only; + expect(fn(metric, metrics, '10s')).toEqual({ + bucket_script: { + buckets_path: { + value: '1', + }, + gap_policy: 'skip', + script: { + source: 'params.value > 0.0 ? params.value : 0.0', + lang: 'painless', + }, + }, + }); + }); + }); + }); }); diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_agg_value.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_agg_value.test.js similarity index 95% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_agg_value.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_agg_value.test.js index 234953c8f4617..5f5e5ebafa560 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_agg_value.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_agg_value.test.js @@ -17,16 +17,15 @@ * under the License. */ -import { expect } from 'chai'; -import { getAggValue } from '../../helpers/get_agg_value'; +import { getAggValue } from './get_agg_value'; function testAgg(row, metric, expected) { let name = metric.type; if (metric.mode) name += `(${metric.mode})`; if (metric.percent) name += `(${metric.percent})`; - it(`it should return ${name}(${expected})`, () => { + test(`it should return ${name}(${expected})`, () => { const value = getAggValue(row, metric); - expect(value).to.eql(expected); + expect(value).toEqual(expected); }); } diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_bucket_size.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.test.js similarity index 54% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_bucket_size.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.test.js index e62cfff1083cf..99bef2de6b72d 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_bucket_size.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.test.js @@ -17,8 +17,7 @@ * under the License. */ -import { expect } from 'chai'; -import { getBucketSize } from '../../helpers/get_bucket_size'; +import { getBucketSize } from './get_bucket_size'; describe('getBucketSize', () => { const req = { @@ -30,39 +29,39 @@ describe('getBucketSize', () => { }, }; - it('returns auto calculated buckets', () => { + test('returns auto calculated buckets', () => { const result = getBucketSize(req, 'auto'); - expect(result).to.have.property('bucketSize', 30); - expect(result).to.have.property('intervalString', '30s'); + expect(result).toHaveProperty('bucketSize', 30); + expect(result).toHaveProperty('intervalString', '30s'); }); - it('returns overridden buckets (1s)', () => { + test('returns overridden buckets (1s)', () => { const result = getBucketSize(req, '1s'); - expect(result).to.have.property('bucketSize', 1); - expect(result).to.have.property('intervalString', '1s'); + expect(result).toHaveProperty('bucketSize', 1); + expect(result).toHaveProperty('intervalString', '1s'); }); - it('returns overridden buckets (10m)', () => { + test('returns overridden buckets (10m)', () => { const result = getBucketSize(req, '10m'); - expect(result).to.have.property('bucketSize', 600); - expect(result).to.have.property('intervalString', '10m'); + expect(result).toHaveProperty('bucketSize', 600); + expect(result).toHaveProperty('intervalString', '10m'); }); - it('returns overridden buckets (1d)', () => { + test('returns overridden buckets (1d)', () => { const result = getBucketSize(req, '1d'); - expect(result).to.have.property('bucketSize', 86400); - expect(result).to.have.property('intervalString', '1d'); + expect(result).toHaveProperty('bucketSize', 86400); + expect(result).toHaveProperty('intervalString', '1d'); }); - it('returns overridden buckets (>=2d)', () => { + test('returns overridden buckets (>=2d)', () => { const result = getBucketSize(req, '>=2d'); - expect(result).to.have.property('bucketSize', 86400 * 2); - expect(result).to.have.property('intervalString', '2d'); + expect(result).toHaveProperty('bucketSize', 86400 * 2); + expect(result).toHaveProperty('intervalString', '2d'); }); - it('returns overridden buckets (>=10s)', () => { + test('returns overridden buckets (>=10s)', () => { const result = getBucketSize(req, '>=10s'); - expect(result).to.have.property('bucketSize', 30); - expect(result).to.have.property('intervalString', '30s'); + expect(result).toHaveProperty('bucketSize', 30); + expect(result).toHaveProperty('intervalString', '30s'); }); }); diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_buckets_path.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_buckets_path.test.js similarity index 53% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_buckets_path.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_buckets_path.test.js index f5c31fa8ba7ce..0a0e18a696588 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_buckets_path.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_buckets_path.test.js @@ -17,8 +17,7 @@ * under the License. */ -import { expect } from 'chai'; -import { getBucketsPath } from '../../helpers/get_buckets_path'; +import { getBucketsPath } from './get_buckets_path'; describe('getBucketsPath', () => { const metrics = [ @@ -33,43 +32,43 @@ describe('getBucketsPath', () => { { id: 9, type: 'max' }, ]; - it('return path for derivative', () => { - expect(getBucketsPath(1, metrics)).to.equal('1[normalized_value]'); + test('return path for derivative', () => { + expect(getBucketsPath(1, metrics)).toEqual('1[normalized_value]'); }); - it('return path for percentile(50)', () => { - expect(getBucketsPath(2, metrics)).to.equal('2[50.0]'); + test('return path for percentile(50)', () => { + expect(getBucketsPath(2, metrics)).toEqual('2[50.0]'); }); - it('return path for percentile(20.0)', () => { - expect(getBucketsPath(3, metrics)).to.equal('3[20.0]'); + test('return path for percentile(20.0)', () => { + expect(getBucketsPath(3, metrics)).toEqual('3[20.0]'); }); - it('return path for percentile(10.0) with alt id', () => { - expect(getBucketsPath('3[10.0]', metrics)).to.equal('3[10.0]'); + test('return path for percentile(10.0) with alt id', () => { + expect(getBucketsPath('3[10.0]', metrics)).toEqual('3[10.0]'); }); - it('return path for std_deviation(raw)', () => { - expect(getBucketsPath(4, metrics)).to.equal('4[std_deviation]'); + test('return path for std_deviation(raw)', () => { + expect(getBucketsPath(4, metrics)).toEqual('4[std_deviation]'); }); - it('return path for std_deviation(upper)', () => { - expect(getBucketsPath(5, metrics)).to.equal('5[std_upper]'); + test('return path for std_deviation(upper)', () => { + expect(getBucketsPath(5, metrics)).toEqual('5[std_upper]'); }); - it('return path for std_deviation(lower)', () => { - expect(getBucketsPath(6, metrics)).to.equal('6[std_lower]'); + test('return path for std_deviation(lower)', () => { + expect(getBucketsPath(6, metrics)).toEqual('6[std_lower]'); }); - it('return path for sum_of_squares', () => { - expect(getBucketsPath(7, metrics)).to.equal('7[sum_of_squares]'); + test('return path for sum_of_squares', () => { + expect(getBucketsPath(7, metrics)).toEqual('7[sum_of_squares]'); }); - it('return path for variance', () => { - expect(getBucketsPath(8, metrics)).to.equal('8[variance]'); + test('return path for variance', () => { + expect(getBucketsPath(8, metrics)).toEqual('8[variance]'); }); - it('return path for basic metric', () => { - expect(getBucketsPath(9, metrics)).to.equal('9'); + test('return path for basic metric', () => { + expect(getBucketsPath(9, metrics)).toEqual('9'); }); }); diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_default_decoration.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_default_decoration.test.js similarity index 51% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_default_decoration.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_default_decoration.test.js index 5cc94dda6d21a..2529c8c649485 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_default_decoration.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_default_decoration.test.js @@ -17,44 +17,43 @@ * under the License. */ -import { expect } from 'chai'; -import { getDefaultDecoration } from '../../helpers/get_default_decoration'; +import { getDefaultDecoration } from './get_default_decoration'; describe('getDefaultDecoration', () => { describe('stack option', () => { - it('should set a stack option to none', () => { + test('should set a stack option to none', () => { const series = { id: 'test_id', stacked: 'none', }; - expect(getDefaultDecoration(series)).to.have.property('stack', 'none'); + expect(getDefaultDecoration(series)).toHaveProperty('stack', 'none'); }); - it('should set a stack option to stacked/percent', () => { + test('should set a stack option to stacked/percent', () => { const series = { stacked: 'stacked', id: 'test_id', }; - expect(getDefaultDecoration(series)).to.have.property('stack', 'stacked'); + expect(getDefaultDecoration(series)).toHaveProperty('stack', 'stacked'); series.stacked = 'percent'; - expect(getDefaultDecoration(series)).to.have.property('stack', 'percent'); + expect(getDefaultDecoration(series)).toHaveProperty('stack', 'percent'); }); - it('should set a stack option to stacked_within_series', () => { + test('should set a stack option to stacked_within_series', () => { const series = { stacked: 'stacked_within_series', id: 'test_id', }; - expect(getDefaultDecoration(series)).to.have.property('stack', 'stacked_within_series'); + expect(getDefaultDecoration(series)).toHaveProperty('stack', 'stacked_within_series'); }); }); describe('lines', () => { - it('return decoration for lines', () => { + test('return decoration for lines', () => { const series = { point_size: 10, chart_type: 'line', @@ -62,29 +61,29 @@ describe('getDefaultDecoration', () => { fill: 1, }; const result = getDefaultDecoration(series); - expect(result.lines).to.have.property('show', true); - expect(result.lines).to.have.property('fill', 1); - expect(result.lines).to.have.property('lineWidth', 10); - expect(result.points).to.have.property('show', true); - expect(result.points).to.have.property('radius', 1); - expect(result.points).to.have.property('lineWidth', 10); - expect(result.bars).to.have.property('show', false); - expect(result.bars).to.have.property('fill', 1); - expect(result.bars).to.have.property('lineWidth', 10); + expect(result.lines).toHaveProperty('show', true); + expect(result.lines).toHaveProperty('fill', 1); + expect(result.lines).toHaveProperty('lineWidth', 10); + expect(result.points).toHaveProperty('show', true); + expect(result.points).toHaveProperty('radius', 1); + expect(result.points).toHaveProperty('lineWidth', 10); + expect(result.bars).toHaveProperty('show', false); + expect(result.bars).toHaveProperty('fill', 1); + expect(result.bars).toHaveProperty('lineWidth', 10); }); - it('return decoration for lines without points', () => { + test('return decoration for lines without points', () => { const series = { chart_type: 'line', line_width: 10, fill: 1, }; const result = getDefaultDecoration(series); - expect(result.points).to.have.property('show', true); - expect(result.points).to.have.property('lineWidth', 10); + expect(result.points).toHaveProperty('show', true); + expect(result.points).toHaveProperty('lineWidth', 10); }); - it('return decoration for lines with points set to zero (off)', () => { + test('return decoration for lines with points set to zero (off)', () => { const series = { chart_type: 'line', line_width: 10, @@ -92,32 +91,32 @@ describe('getDefaultDecoration', () => { point_size: 0, }; const result = getDefaultDecoration(series); - expect(result.points).to.have.property('show', false); + expect(result.points).toHaveProperty('show', false); }); - it('return decoration for lines (off)', () => { + test('return decoration for lines (off)', () => { const series = { chart_type: 'line', line_width: 0, }; const result = getDefaultDecoration(series); - expect(result.lines).to.have.property('show', false); + expect(result.lines).toHaveProperty('show', false); }); }); describe('bars', () => { - it('return decoration for bars', () => { + test('return decoration for bars', () => { const series = { chart_type: 'bar', line_width: 10, fill: 1, }; const result = getDefaultDecoration(series); - expect(result.lines).to.have.property('show', false); - expect(result.points).to.have.property('show', false); - expect(result.bars).to.have.property('show', true); - expect(result.bars).to.have.property('fill', 1); - expect(result.bars).to.have.property('lineWidth', 10); + expect(result.lines).toHaveProperty('show', false); + expect(result.points).toHaveProperty('show', false); + expect(result.bars).toHaveProperty('show', true); + expect(result.bars).toHaveProperty('fill', 1); + expect(result.bars).toHaveProperty('lineWidth', 10); }); }); }); diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_es_shard_timeout.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_es_shard_timeout.test.js similarity index 82% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_es_shard_timeout.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_es_shard_timeout.test.js index b19f6a3241597..13f62739a5485 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_es_shard_timeout.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_es_shard_timeout.test.js @@ -17,11 +17,10 @@ * under the License. */ -import { expect } from 'chai'; -import { getEsShardTimeout } from '../../helpers/get_es_shard_timeout'; +import { getEsShardTimeout } from './get_es_shard_timeout'; describe('getEsShardTimeout', () => { - it('should return the elasticsearch.shardTimeout', async () => { + test('should return the elasticsearch.shardTimeout', async () => { const req = { getEsShardTimeout: async () => { return 12345; @@ -30,6 +29,6 @@ describe('getEsShardTimeout', () => { const timeout = await getEsShardTimeout(req); - expect(timeout).to.equal(12345); + expect(timeout).toEqual(12345); }); }); diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_last_metric.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_last_metric.test.js similarity index 76% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_last_metric.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_last_metric.test.js index be99f81dfccc4..42e6cc2c01836 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_last_metric.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_last_metric.test.js @@ -17,26 +17,25 @@ * under the License. */ -import { expect } from 'chai'; -import { getLastMetric } from '../../helpers/get_last_metric'; +import { getLastMetric } from './get_last_metric'; describe('getLastMetric(series)', () => { - it('returns the last metric', () => { + test('returns the last metric', () => { const series = { metrics: [ { id: 1, type: 'avg' }, { id: 2, type: 'moving_average' }, ], }; - expect(getLastMetric(series)).to.eql({ id: 2, type: 'moving_average' }); + expect(getLastMetric(series)).toEqual({ id: 2, type: 'moving_average' }); }); - it('returns the last metric that not a series_agg', () => { + test('returns the last metric that not a series_agg', () => { const series = { metrics: [ { id: 1, type: 'avg' }, { id: 2, type: 'series_agg' }, ], }; - expect(getLastMetric(series)).to.eql({ id: 1, type: 'avg' }); + expect(getLastMetric(series)).toEqual({ id: 1, type: 'avg' }); }); }); diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_sibling_agg_value.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_sibling_agg_value.test.js similarity index 69% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_sibling_agg_value.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_sibling_agg_value.test.js index 68bfa77e25ea3..755568546949c 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_sibling_agg_value.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_sibling_agg_value.test.js @@ -17,8 +17,7 @@ * under the License. */ -import { expect } from 'chai'; -import { getSiblingAggValue } from '../../helpers/get_sibling_agg_value'; +import { getSiblingAggValue } from './get_sibling_agg_value'; describe('getSiblingAggValue', () => { const row = { @@ -32,23 +31,23 @@ describe('getSiblingAggValue', () => { }, }; - it('returns the value for std_deviation_bounds.upper', () => { + test('returns the value for std_deviation_bounds.upper', () => { const metric = { id: 'test', type: 'std_deviation_bucket', mode: 'upper' }; - expect(getSiblingAggValue(row, metric)).to.equal(2); + expect(getSiblingAggValue(row, metric)).toEqual(2); }); - it('returns the value for std_deviation_bounds.lower', () => { + test('returns the value for std_deviation_bounds.lower', () => { const metric = { id: 'test', type: 'std_deviation_bucket', mode: 'lower' }; - expect(getSiblingAggValue(row, metric)).to.equal(1); + expect(getSiblingAggValue(row, metric)).toEqual(1); }); - it('returns the value for std_deviation', () => { + test('returns the value for std_deviation', () => { const metric = { id: 'test', type: 'std_deviation_bucket', mode: 'raw' }; - expect(getSiblingAggValue(row, metric)).to.equal(1.5); + expect(getSiblingAggValue(row, metric)).toEqual(1.5); }); - it('returns the value for basic (max)', () => { + test('returns the value for basic (max)', () => { const metric = { id: 'test', type: 'max_bucket' }; - expect(getSiblingAggValue(row, metric)).to.equal(3); + expect(getSiblingAggValue(row, metric)).toEqual(3); }); }); diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_splits.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js similarity index 90% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_splits.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js index 1057248d2f362..0874d944033f5 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_splits.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js @@ -17,11 +17,10 @@ * under the License. */ -import { expect } from 'chai'; -import { getSplits } from '../../helpers/get_splits'; +import { getSplits } from './get_splits'; describe('getSplits(resp, panel, series)', () => { - it('should return a splits for everything/filter group bys', () => { + test('should return a splits for everything/filter group bys', () => { const resp = { aggregations: { SERIES: { @@ -41,7 +40,7 @@ describe('getSplits(resp, panel, series)', () => { { id: 'SIBAGG', type: 'avg_bucket', field: 'AVG' }, ], }; - expect(getSplits(resp, panel, series)).to.eql([ + expect(getSplits(resp, panel, series)).toEqual([ { id: 'SERIES', label: 'Overall Average of Average of cpu', @@ -53,7 +52,7 @@ describe('getSplits(resp, panel, series)', () => { ]); }); - it('should return a splits for terms group bys for top_n', () => { + test('should return a splits for terms group bys for top_n', () => { const resp = { aggregations: { SERIES: { @@ -85,7 +84,7 @@ describe('getSplits(resp, panel, series)', () => { ], }; const panel = { type: 'top_n' }; - expect(getSplits(resp, panel, series)).to.eql([ + expect(getSplits(resp, panel, series)).toEqual([ { id: 'SERIES:example-01', key: 'example-01', @@ -107,7 +106,7 @@ describe('getSplits(resp, panel, series)', () => { ]); }); - it('should return a splits for terms group bys', () => { + test('should return a splits for terms group bys', () => { const resp = { aggregations: { SERIES: { @@ -139,7 +138,7 @@ describe('getSplits(resp, panel, series)', () => { ], }; const panel = { type: 'timeseries' }; - expect(getSplits(resp, panel, series)).to.eql([ + expect(getSplits(resp, panel, series)).toEqual([ { id: 'SERIES:example-01', key: 'example-01', @@ -161,7 +160,7 @@ describe('getSplits(resp, panel, series)', () => { ]); }); - it('should return a splits for filters group bys', () => { + test('should return a splits for filters group bys', () => { const resp = { aggregations: { SERIES: { @@ -188,7 +187,7 @@ describe('getSplits(resp, panel, series)', () => { metrics: [{ id: 'COUNT', type: 'count' }], }; const panel = { type: 'timeseries' }; - expect(getSplits(resp, panel, series)).to.eql([ + expect(getSplits(resp, panel, series)).toEqual([ { id: 'SERIES:filter-1', key: 'filter-1', diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_timerange.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_timerange.test.js similarity index 72% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_timerange.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_timerange.test.js index f79d1002b8546..1a1b12c651992 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_timerange.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_timerange.test.js @@ -17,12 +17,11 @@ * under the License. */ -import { expect } from 'chai'; -import { getTimerange } from '../../helpers/get_timerange'; +import { getTimerange } from './get_timerange'; import moment from 'moment'; describe('getTimerange(req)', () => { - it('should return a moment object for to and from', () => { + test('should return a moment object for to and from', () => { const req = { payload: { timerange: { @@ -32,9 +31,9 @@ describe('getTimerange(req)', () => { }, }; const { from, to } = getTimerange(req); - expect(moment.isMoment(from)).to.equal(true); - expect(moment.isMoment(to)).to.equal(true); - expect(moment.utc('2017-01-01T00:00:00Z').isSame(from)).to.equal(true); - expect(moment.utc('2017-01-01T01:00:00Z').isSame(to)).to.equal(true); + expect(moment.isMoment(from)).toEqual(true); + expect(moment.isMoment(to)).toEqual(true); + expect(moment.utc('2017-01-01T00:00:00Z').isSame(from)).toEqual(true); + expect(moment.utc('2017-01-01T01:00:00Z').isSame(to)).toEqual(true); }); }); diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/map_bucket.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/map_bucket.test.js similarity index 72% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/map_bucket.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/map_bucket.test.js index e3223ef7b618f..0b007416681ee 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/map_bucket.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/map_bucket.test.js @@ -17,40 +17,39 @@ * under the License. */ -import { mapBucket } from '../../helpers/map_bucket'; -import { expect } from 'chai'; +import { mapBucket } from './map_bucket'; describe('mapBucket(metric)', () => { - it('returns bucket key and value for basic metric', () => { + test('returns bucket key and value for basic metric', () => { const metric = { id: 'AVG', type: 'avg' }; const bucket = { key: 1234, AVG: { value: 1 }, }; - expect(mapBucket(metric)(bucket)).to.eql([1234, 1]); + expect(mapBucket(metric)(bucket)).toEqual([1234, 1]); }); - it('returns bucket key and value for std_deviation', () => { + test('returns bucket key and value for std_deviation', () => { const metric = { id: 'STDDEV', type: 'std_deviation' }; const bucket = { key: 1234, STDDEV: { std_deviation: 1 }, }; - expect(mapBucket(metric)(bucket)).to.eql([1234, 1]); + expect(mapBucket(metric)(bucket)).toEqual([1234, 1]); }); - it('returns bucket key and value for percentiles', () => { + test('returns bucket key and value for percentiles', () => { const metric = { id: 'PCT', type: 'percentile', percent: 50 }; const bucket = { key: 1234, PCT: { values: { '50.0': 1 } }, }; - expect(mapBucket(metric)(bucket)).to.eql([1234, 1]); + expect(mapBucket(metric)(bucket)).toEqual([1234, 1]); }); - it('returns bucket key and value for derivative', () => { + test('returns bucket key and value for derivative', () => { const metric = { id: 'DERV', type: 'derivative', field: 'io', unit: '1s' }; const bucket = { key: 1234, DERV: { value: 100, normalized_value: 1 }, }; - expect(mapBucket(metric)(bucket)).to.eql([1234, 1]); + expect(mapBucket(metric)(bucket)).toEqual([1234, 1]); }); }); diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/parse_settings.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/parse_settings.test.js similarity index 70% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/parse_settings.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/parse_settings.test.js index bf16b6e22e8ef..eab06cdccfa66 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/parse_settings.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/parse_settings.test.js @@ -17,41 +17,40 @@ * under the License. */ -import { expect } from 'chai'; -import { parseSettings } from '../../helpers/parse_settings'; +import { parseSettings } from './parse_settings'; describe('parseSettings', () => { - it('returns the true for "true"', () => { + test('returns the true for "true"', () => { const settings = 'pad=true'; - expect(parseSettings(settings)).to.eql({ + expect(parseSettings(settings)).toEqual({ pad: true, }); }); - it('returns the false for "false"', () => { + test('returns the false for "false"', () => { const settings = 'pad=false'; - expect(parseSettings(settings)).to.eql({ + expect(parseSettings(settings)).toEqual({ pad: false, }); }); - it('returns the true for 1', () => { + test('returns the true for 1', () => { const settings = 'pad=1'; - expect(parseSettings(settings)).to.eql({ + expect(parseSettings(settings)).toEqual({ pad: true, }); }); - it('returns the false for 0', () => { + test('returns the false for 0', () => { const settings = 'pad=0'; - expect(parseSettings(settings)).to.eql({ + expect(parseSettings(settings)).toEqual({ pad: false, }); }); - it('returns the settings as an object', () => { + test('returns the settings as an object', () => { const settings = 'alpha=0.9 beta=0.4 gamma=0.2 period=5 pad=false type=add'; - expect(parseSettings(settings)).to.eql({ + expect(parseSettings(settings)).toEqual({ alpha: 0.9, beta: 0.4, gamma: 0.2, diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/offset_time.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/offset_time.test.js similarity index 64% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/offset_time.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/offset_time.test.js index 55e43cf7bd8ce..b9b73002bafb3 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/offset_time.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/offset_time.test.js @@ -17,12 +17,11 @@ * under the License. */ -import { expect } from 'chai'; import moment from 'moment'; -import { offsetTime } from '../offset_time'; +import { offsetTime } from './offset_time'; describe('offsetTime(req, by)', () => { - it('should return a moment object for to and from', () => { + test('should return a moment object for to and from', () => { const req = { payload: { timerange: { @@ -32,13 +31,13 @@ describe('offsetTime(req, by)', () => { }, }; const { from, to } = offsetTime(req, ''); - expect(moment.isMoment(from)).to.equal(true); - expect(moment.isMoment(to)).to.equal(true); - expect(moment.utc('2017-01-01T00:00:00Z').isSame(from)).to.equal(true); - expect(moment.utc('2017-01-01T01:00:00Z').isSame(to)).to.equal(true); + expect(moment.isMoment(from)).toBeTruthy(); + expect(moment.isMoment(to)).toBeTruthy(); + expect(moment.utc('2017-01-01T00:00:00Z').isSame(from)).toBeTruthy(); + expect(moment.utc('2017-01-01T01:00:00Z').isSame(to)).toBeTruthy(); }); - it('should return a moment object for to and from offset by 1 hour', () => { + test('should return a moment object for to and from offset by 1 hour', () => { const req = { payload: { timerange: { @@ -48,23 +47,23 @@ describe('offsetTime(req, by)', () => { }, }; const { from, to } = offsetTime(req, '1h'); - expect(moment.isMoment(from)).to.equal(true); - expect(moment.isMoment(to)).to.equal(true); + expect(moment.isMoment(from)).toBeTruthy(); + expect(moment.isMoment(to)).toBeTruthy(); expect( moment .utc('2017-01-01T00:00:00Z') .subtract(1, 'h') .isSame(from) - ).to.equal(true); + ).toBeTruthy(); expect( moment .utc('2017-01-01T01:00:00Z') .subtract(1, 'h') .isSame(to) - ).to.equal(true); + ).toBeTruthy(); }); - it('should return a moment object for to and from offset by -2 minute', () => { + test('should return a moment object for to and from offset by -2 minute', () => { const req = { payload: { timerange: { @@ -74,13 +73,13 @@ describe('offsetTime(req, by)', () => { }, }; const { from, to } = offsetTime(req, '-2m'); - expect(moment.isMoment(from)).to.equal(true); - expect(moment.isMoment(to)).to.equal(true); - expect(moment.utc('2017-01-10T01:02:00Z').isSame(from)).to.equal(true); - expect(moment.utc('2017-01-10T02:02:00Z').isSame(to)).to.equal(true); + expect(moment.isMoment(from)).toBeTruthy(); + expect(moment.isMoment(to)).toBeTruthy(); + expect(moment.utc('2017-01-10T01:02:00Z').isSame(from)).toBeTruthy(); + expect(moment.utc('2017-01-10T02:02:00Z').isSame(to)).toBeTruthy(); }); - it('should work when prefixing positive offsets with the plus sign', () => { + test('should work when prefixing positive offsets with the plus sign', () => { const req = { payload: { timerange: { @@ -92,7 +91,7 @@ describe('offsetTime(req, by)', () => { const { from: fromSigned, to: toSigned } = offsetTime(req, '+1m'); const { from, to } = offsetTime(req, '1m'); - expect(fromSigned.isSame(from)).to.equal(true); - expect(toSigned.isSame(to)).to.equal(true); + expect(fromSigned.isSame(from)).toBeTruthy(); + expect(toSigned.isSame(to)).toBeTruthy(); }); }); diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/date_histogram.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js similarity index 85% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/date_histogram.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js index 40eaba621aabb..7f309b44d13f4 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/date_histogram.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js @@ -17,10 +17,8 @@ * under the License. */ -import { expect } from 'chai'; -import sinon from 'sinon'; -import { DefaultSearchCapabilities } from '../../../../search_strategies/default_search_capabilities'; -import { dateHistogram } from '../date_histogram'; +import { DefaultSearchCapabilities } from '../../../search_strategies/default_search_capabilities'; +import { dateHistogram } from './date_histogram'; describe('dateHistogram(req, panel, series)', () => { let panel; @@ -54,18 +52,18 @@ describe('dateHistogram(req, panel, series)', () => { capabilities = new DefaultSearchCapabilities(req); }); - it('calls next when finished', () => { - const next = sinon.spy(); + test('calls next when finished', () => { + const next = jest.fn(); dateHistogram(req, panel, series, config, indexPatternObject, capabilities)(next)({}); - expect(next.calledOnce).to.equal(true); + expect(next.mock.calls.length).toEqual(1); }); - it('returns valid date histogram', () => { + test('returns valid date histogram', () => { const next = doc => doc; const doc = dateHistogram(req, panel, series, config, indexPatternObject, capabilities)(next)( {} ); - expect(doc).to.eql({ + expect(doc).toEqual({ aggs: { test: { aggs: { @@ -93,13 +91,13 @@ describe('dateHistogram(req, panel, series)', () => { }); }); - it('returns valid date histogram (offset by 1h)', () => { + test('returns valid date histogram (offset by 1h)', () => { series.offset_time = '1h'; const next = doc => doc; const doc = dateHistogram(req, panel, series, config, indexPatternObject, capabilities)(next)( {} ); - expect(doc).to.eql({ + expect(doc).toEqual({ aggs: { test: { aggs: { @@ -127,7 +125,7 @@ describe('dateHistogram(req, panel, series)', () => { }); }); - it('returns valid date histogram with overridden index pattern', () => { + test('returns valid date histogram with overridden index pattern', () => { series.override_index_pattern = 1; series.series_index_pattern = '*'; series.series_time_field = 'timestamp'; @@ -136,7 +134,7 @@ describe('dateHistogram(req, panel, series)', () => { const doc = dateHistogram(req, panel, series, config, indexPatternObject, capabilities)(next)( {} ); - expect(doc).to.eql({ + expect(doc).toEqual({ aggs: { test: { aggs: { @@ -165,7 +163,7 @@ describe('dateHistogram(req, panel, series)', () => { }); describe('dateHistogram for entire time range mode', () => { - it('should ignore entire range mode for timeseries', () => { + test('should ignore entire range mode for timeseries', () => { panel.time_range_mode = 'entire_time_range'; panel.type = 'timeseries'; @@ -174,18 +172,18 @@ describe('dateHistogram(req, panel, series)', () => { {} ); - expect(doc.aggs.test.aggs.timeseries.auto_date_histogram).to.eql(undefined); - expect(doc.aggs.test.aggs.timeseries.date_histogram).to.exist; + expect(doc.aggs.test.aggs.timeseries.auto_date_histogram).toBeUndefined(); + expect(doc.aggs.test.aggs.timeseries.date_histogram).toBeDefined(); }); - it('should returns valid date histogram for entire range mode', () => { + test('should returns valid date histogram for entire range mode', () => { panel.time_range_mode = 'entire_time_range'; const next = doc => doc; const doc = dateHistogram(req, panel, series, config, indexPatternObject, capabilities)(next)( {} ); - expect(doc).to.eql({ + expect(doc).toEqual({ aggs: { test: { aggs: { diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/filter_ratios.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.test.js similarity index 92% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/filter_ratios.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.test.js index 0449ee440da81..f3d25a8e44ba7 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/filter_ratios.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.test.js @@ -17,9 +17,7 @@ * under the License. */ -import { ratios } from '../filter_ratios'; -import { expect } from 'chai'; -import sinon from 'sinon'; +import { ratios } from './filter_ratios'; describe('ratios(req, panel, series)', () => { let panel; @@ -55,16 +53,16 @@ describe('ratios(req, panel, series)', () => { }; }); - it('calls next when finished', () => { - const next = sinon.spy(); + test('calls next when finished', () => { + const next = jest.fn(); ratios(req, panel, series)(next)({}); - expect(next.calledOnce).to.equal(true); + expect(next.mock.calls.length).toEqual(1); }); - it('returns filter ratio aggs', () => { + test('returns filter ratio aggs', () => { const next = doc => doc; const doc = ratios(req, panel, series)(next)({}); - expect(doc).to.eql({ + expect(doc).toEqual({ aggs: { test: { aggs: { @@ -119,11 +117,11 @@ describe('ratios(req, panel, series)', () => { }); }); - it('returns empty object when field is not set', () => { + test('returns empty object when field is not set', () => { delete series.metrics[0].field; const next = doc => doc; const doc = ratios(req, panel, series)(next)({}); - expect(doc).to.eql({ + expect(doc).toEqual({ aggs: { test: { aggs: { diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/metric_buckets.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.test.js similarity index 88% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/metric_buckets.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.test.js index 9393882f12b7e..eaf8ee1549f0b 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/metric_buckets.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.test.js @@ -17,9 +17,7 @@ * under the License. */ -import { metricBuckets } from '../metric_buckets'; -import { expect } from 'chai'; -import sinon from 'sinon'; +import { metricBuckets } from './metric_buckets'; describe('metricBuckets(req, panel, series)', () => { let panel; @@ -63,16 +61,16 @@ describe('metricBuckets(req, panel, series)', () => { }; }); - it('calls next when finished', () => { - const next = sinon.spy(); + test('calls next when finished', () => { + const next = jest.fn(); metricBuckets(req, panel, series)(next)({}); - expect(next.calledOnce).to.equal(true); + expect(next.mock.calls.length).toEqual(1); }); - it('returns metric aggs', () => { + test('returns metric aggs', () => { const next = doc => doc; const doc = metricBuckets(req, panel, series)(next)({}); - expect(doc).to.eql({ + expect(doc).toEqual({ aggs: { test: { aggs: { diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/query.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.test.js similarity index 91% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/query.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.test.js index 2f2e5f46f834e..06da636f818d5 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/query.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/query.test.js @@ -17,9 +17,7 @@ * under the License. */ -import { query } from '../query'; -import { expect } from 'chai'; -import sinon from 'sinon'; +import { query } from './query'; describe('query(req, panel, series)', () => { let panel; @@ -47,16 +45,16 @@ describe('query(req, panel, series)', () => { series = { id: 'test' }; }); - it('calls next when finished', () => { - const next = sinon.spy(); + test('calls next when finished', () => { + const next = jest.fn(); query(req, panel, series, config)(next)({}); - expect(next.calledOnce).to.equal(true); + expect(next.mock.calls.length).toEqual(1); }); - it('returns doc with query for timerange', () => { + test('returns doc with query for timerange', () => { const next = doc => doc; const doc = query(req, panel, series, config)(next)({}); - expect(doc).to.eql({ + expect(doc).toEqual({ size: 0, query: { bool: { @@ -79,11 +77,11 @@ describe('query(req, panel, series)', () => { }); }); - it('returns doc with query for timerange (offset by 1h)', () => { + test('returns doc with query for timerange (offset by 1h)', () => { series.offset_time = '1h'; const next = doc => doc; const doc = query(req, panel, series, config)(next)({}); - expect(doc).to.eql({ + expect(doc).toEqual({ size: 0, query: { bool: { @@ -106,7 +104,7 @@ describe('query(req, panel, series)', () => { }); }); - it('returns doc with global query', () => { + test('returns doc with global query', () => { req.payload.filters = [ { bool: { @@ -122,7 +120,7 @@ describe('query(req, panel, series)', () => { ]; const next = doc => doc; const doc = query(req, panel, series, config)(next)({}); - expect(doc).to.eql({ + expect(doc).toEqual({ size: 0, query: { bool: { @@ -157,11 +155,11 @@ describe('query(req, panel, series)', () => { }); }); - it('returns doc with series filter', () => { + test('returns doc with series filter', () => { series.filter = { query: 'host:web-server', language: 'lucene' }; const next = doc => doc; const doc = query(req, panel, series, config)(next)({}); - expect(doc).to.eql({ + expect(doc).toEqual({ size: 0, query: { bool: { @@ -198,7 +196,7 @@ describe('query(req, panel, series)', () => { }, }); }); - it('returns doc with panel filter and global', () => { + test('returns doc with panel filter and global', () => { req.payload.filters = [ { bool: { @@ -215,7 +213,7 @@ describe('query(req, panel, series)', () => { panel.filter = { query: 'host:web-server', language: 'lucene' }; const next = doc => doc; const doc = query(req, panel, series, config)(next)({}); - expect(doc).to.eql({ + expect(doc).toEqual({ size: 0, query: { bool: { @@ -265,7 +263,7 @@ describe('query(req, panel, series)', () => { }); }); - it('returns doc with panel filter (ignoring globals)', () => { + test('returns doc with panel filter (ignoring globals)', () => { req.payload.filters = [ { bool: { @@ -283,7 +281,7 @@ describe('query(req, panel, series)', () => { panel.ignore_global_filter = true; const next = doc => doc; const doc = query(req, panel, series, config)(next)({}); - expect(doc).to.eql({ + expect(doc).toEqual({ size: 0, query: { bool: { diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/sibling_buckets.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.test.js similarity index 86% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/sibling_buckets.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.test.js index 0f02c755cabcd..8e1c9b0f9ecd9 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/sibling_buckets.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.test.js @@ -17,9 +17,7 @@ * under the License. */ -import { siblingBuckets } from '../sibling_buckets'; -import { expect } from 'chai'; -import sinon from 'sinon'; +import { siblingBuckets } from './sibling_buckets'; describe('siblingBuckets(req, panel, series)', () => { let panel; @@ -57,16 +55,16 @@ describe('siblingBuckets(req, panel, series)', () => { }; }); - it('calls next when finished', () => { - const next = sinon.spy(); + test('calls next when finished', () => { + const next = jest.fn(); siblingBuckets(req, panel, series)(next)({}); - expect(next.calledOnce).to.equal(true); + expect(next.mock.calls.length).toEqual(1); }); - it('returns sibling aggs', () => { + test('returns sibling aggs', () => { const next = doc => doc; const doc = siblingBuckets(req, panel, series)(next)({}); - expect(doc).to.eql({ + expect(doc).toEqual({ aggs: { test: { aggs: { diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/split_by_everything.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_everything.test.js similarity index 83% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/split_by_everything.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_everything.test.js index 4e2775d3608e6..5c2468f6b7c35 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/split_by_everything.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_everything.test.js @@ -17,9 +17,7 @@ * under the License. */ -import { splitByEverything } from '../split_by_everything'; -import { expect } from 'chai'; -import sinon from 'sinon'; +import { splitByEverything } from './split_by_everything'; describe('splitByEverything(req, panel, series)', () => { let panel; @@ -39,15 +37,15 @@ describe('splitByEverything(req, panel, series)', () => { }); it('calls next when finished', () => { - const next = sinon.spy(); + const next = jest.fn(); splitByEverything(req, panel, series)(next)({}); - expect(next.calledOnce).to.equal(true); + expect(next.mock.calls.length).toEqual(1); }); it('returns a valid filter with match_all', () => { const next = doc => doc; const doc = splitByEverything(req, panel, series)(next)({}); - expect(doc).to.eql({ + expect(doc).toEqual({ aggs: { test: { filter: { @@ -61,9 +59,9 @@ describe('splitByEverything(req, panel, series)', () => { it('calls next and does not add a filter', () => { series.split_mode = 'terms'; series.terms_field = 'host'; - const next = sinon.spy(doc => doc); + const next = jest.fn(doc => doc); const doc = splitByEverything(req, panel, series)(next)({}); - expect(next.calledOnce).to.equal(true); - expect(doc).to.eql({}); + expect(next.mock.calls.length).toEqual(1); + expect(doc).toEqual({}); }); }); diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/split_by_filter.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.test.js similarity index 79% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/split_by_filter.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.test.js index 782310ba4d2bf..75ca782a6d7e7 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/split_by_filter.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.test.js @@ -17,9 +17,7 @@ * under the License. */ -import { splitByFilter } from '../split_by_filter'; -import { expect } from 'chai'; -import sinon from 'sinon'; +import { splitByFilter } from './split_by_filter'; describe('splitByFilter(req, panel, series)', () => { let panel; @@ -42,16 +40,16 @@ describe('splitByFilter(req, panel, series)', () => { }; }); - it('calls next when finished', () => { - const next = sinon.spy(); + test('calls next when finished', () => { + const next = jest.fn(); splitByFilter(req, panel, series)(next)({}); - expect(next.calledOnce).to.equal(true); + expect(next.mock.calls.length).toEqual(1); }); - it('returns a valid filter with a query_string', () => { + test('returns a valid filter with a query_string', () => { const next = doc => doc; const doc = splitByFilter(req, panel, series)(next)({}); - expect(doc).to.eql({ + expect(doc).toEqual({ aggs: { test: { filter: { @@ -73,11 +71,11 @@ describe('splitByFilter(req, panel, series)', () => { }); }); - it('calls next and does not add a filter', () => { + test('calls next and does not add a filter', () => { series.split_mode = 'terms'; - const next = sinon.spy(doc => doc); + const next = jest.fn(doc => doc); const doc = splitByFilter(req, panel, series)(next)({}); - expect(next.calledOnce).to.equal(true); - expect(doc).to.eql({}); + expect(next.mock.calls.length).toEqual(1); + expect(doc).toEqual({}); }); }); diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/split_by_filters.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.test.js similarity index 85% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/split_by_filters.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.test.js index 018cb4b003ecf..6108b2bf2cf2b 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/split_by_filters.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.test.js @@ -17,9 +17,7 @@ * under the License. */ -import { splitByFilters } from '../split_by_filters'; -import { expect } from 'chai'; -import sinon from 'sinon'; +import { splitByFilters } from './split_by_filters'; describe('splitByFilters(req, panel, series)', () => { let panel; @@ -58,16 +56,16 @@ describe('splitByFilters(req, panel, series)', () => { }; }); - it('calls next when finished', () => { - const next = sinon.spy(); + test('calls next when finished', () => { + const next = jest.fn(); splitByFilters(req, panel, series)(next)({}); - expect(next.calledOnce).to.equal(true); + expect(next.mock.calls.length).toEqual(1); }); - it('returns a valid terms agg', () => { + test('returns a valid terms agg', () => { const next = doc => doc; const doc = splitByFilters(req, panel, series)(next)({}); - expect(doc).to.eql({ + expect(doc).toEqual({ aggs: { test: { filters: { @@ -107,11 +105,11 @@ describe('splitByFilters(req, panel, series)', () => { }); }); - it('calls next and does not add a terms agg', () => { + test('calls next and does not add a terms agg', () => { series.split_mode = 'everything'; - const next = sinon.spy(doc => doc); + const next = jest.fn(doc => doc); const doc = splitByFilters(req, panel, series)(next)({}); - expect(next.calledOnce).to.equal(true); - expect(doc).to.eql({}); + expect(next.mock.calls.length).toEqual(1); + expect(doc).toEqual({}); }); }); diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/split_by_terms.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.test.js similarity index 80% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/split_by_terms.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.test.js index eb1a81baa7fc2..ff7139c380af9 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/__tests__/split_by_terms.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.test.js @@ -17,9 +17,7 @@ * under the License. */ -import { splitByTerms } from '../split_by_terms'; -import { expect } from 'chai'; -import sinon from 'sinon'; +import { splitByTerms } from './split_by_terms'; describe('splitByTerms(req, panel, series)', () => { let panel; @@ -46,16 +44,16 @@ describe('splitByTerms(req, panel, series)', () => { }; }); - it('calls next when finished', () => { - const next = sinon.spy(); + test('calls next when finished', () => { + const next = jest.fn(); splitByTerms(req, panel, series)(next)({}); - expect(next.calledOnce).to.equal(true); + expect(next.mock.calls.length).toEqual(1); }); - it('returns a valid terms agg', () => { + test('returns a valid terms agg', () => { const next = doc => doc; const doc = splitByTerms(req, panel, series)(next)({}); - expect(doc).to.eql({ + expect(doc).toEqual({ aggs: { test: { terms: { @@ -70,12 +68,12 @@ describe('splitByTerms(req, panel, series)', () => { }); }); - it('returns a valid terms agg sort by terms', () => { + test('returns a valid terms agg sort by terms', () => { const next = doc => doc; series.terms_order_by = '_key'; series.terms_direction = 'asc'; const doc = splitByTerms(req, panel, series)(next)({}); - expect(doc).to.eql({ + expect(doc).toEqual({ aggs: { test: { terms: { @@ -90,11 +88,11 @@ describe('splitByTerms(req, panel, series)', () => { }); }); - it('returns a valid terms agg with custom sort', () => { + test('returns a valid terms agg with custom sort', () => { series.terms_order_by = 'avgmetric'; const next = doc => doc; const doc = splitByTerms(req, panel, series)(next)({}); - expect(doc).to.eql({ + expect(doc).toEqual({ aggs: { test: { terms: { @@ -116,11 +114,11 @@ describe('splitByTerms(req, panel, series)', () => { }); }); - it('calls next and does not add a terms agg', () => { + test('calls next and does not add a terms agg', () => { series.split_mode = 'everything'; - const next = sinon.spy(doc => doc); + const next = jest.fn(doc => doc); const doc = splitByTerms(req, panel, series)(next)({}); - expect(next.calledOnce).to.equal(true); - expect(doc).to.eql({}); + expect(next.mock.calls.length).toEqual(1); + expect(doc).toEqual({}); }); }); diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/__tests__/_series_agg.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/_series_agg.test.js similarity index 68% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/__tests__/_series_agg.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/_series_agg.test.js index 582466f8eb9fd..22ea2c852e254 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/__tests__/_series_agg.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/_series_agg.test.js @@ -17,8 +17,7 @@ * under the License. */ -import { expect } from 'chai'; -import { SeriesAgg as seriesAgg } from '../_series_agg'; +import { SeriesAgg as seriesAgg } from './_series_agg'; describe('seriesAgg', () => { const series = [ @@ -40,8 +39,8 @@ describe('seriesAgg', () => { ]; describe('basic', () => { - it('returns the series sum', () => { - expect(seriesAgg.sum(series)).to.eql([ + test('returns the series sum', () => { + expect(seriesAgg.sum(series)).toEqual([ [ [0, 8], [1, 4], @@ -50,8 +49,8 @@ describe('seriesAgg', () => { ]); }); - it('returns the series max', () => { - expect(seriesAgg.max(series)).to.eql([ + test('returns the series max', () => { + expect(seriesAgg.max(series)).toEqual([ [ [0, 4], [1, 2], @@ -60,8 +59,8 @@ describe('seriesAgg', () => { ]); }); - it('returns the series min', () => { - expect(seriesAgg.min(series)).to.eql([ + test('returns the series min', () => { + expect(seriesAgg.min(series)).toEqual([ [ [0, 2], [1, 1], @@ -70,8 +69,8 @@ describe('seriesAgg', () => { ]); }); - it('returns the series mean', () => { - expect(seriesAgg.mean(series)).to.eql([ + test('returns the series mean', () => { + expect(seriesAgg.mean(series)).toEqual([ [ [0, 8 / 3], [1, 4 / 3], @@ -82,8 +81,8 @@ describe('seriesAgg', () => { }); describe('overall', () => { - it('returns the series overall sum', () => { - expect(seriesAgg.overall_sum(series)).to.eql([ + test('returns the series overall sum', () => { + expect(seriesAgg.overall_sum(series)).toEqual([ [ [0, 21], [1, 21], @@ -92,8 +91,8 @@ describe('seriesAgg', () => { ]); }); - it('returns the series overall max', () => { - expect(seriesAgg.overall_max(series)).to.eql([ + test('returns the series overall max', () => { + expect(seriesAgg.overall_max(series)).toEqual([ [ [0, 4], [1, 4], @@ -102,8 +101,8 @@ describe('seriesAgg', () => { ]); }); - it('returns the series overall min', () => { - expect(seriesAgg.overall_min(series)).to.eql([ + test('returns the series overall min', () => { + expect(seriesAgg.overall_min(series)).toEqual([ [ [0, 1], [1, 1], @@ -112,9 +111,9 @@ describe('seriesAgg', () => { ]); }); - it('returns the series overall mean', () => { + test('returns the series overall mean', () => { const value = (8 + 4 + 9) / 3; - expect(seriesAgg.overall_avg(series)).to.eql([ + expect(seriesAgg.overall_avg(series)).toEqual([ [ [0, value], [1, value], @@ -125,8 +124,8 @@ describe('seriesAgg', () => { }); describe('cumulative sum', () => { - it('returns the series cumulative sum', () => { - expect(seriesAgg.cumulative_sum(series)).to.eql([ + test('returns the series cumulative sum', () => { + expect(seriesAgg.cumulative_sum(series)).toEqual([ [ [0, 8], [1, 12], diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/__tests__/percentile.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile.test.js similarity index 57% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/__tests__/percentile.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile.test.js index d40a75c903602..9cb08de8dad23 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/__tests__/percentile.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile.test.js @@ -17,9 +17,7 @@ * under the License. */ -import { percentile } from '../percentile'; -import { expect } from 'chai'; -import sinon from 'sinon'; +import { percentile } from './percentile'; describe('percentile(resp, panel, series)', () => { let panel; @@ -82,72 +80,72 @@ describe('percentile(resp, panel, series)', () => { }; }); - it('calls next when finished', () => { - const next = sinon.spy(); + test('calls next when finished', () => { + const next = jest.fn(); percentile(resp, panel, series)(next)([]); - expect(next.calledOnce).to.equal(true); + expect(next.mock.calls.length).toEqual(1); }); - it('creates a series', () => { + test('creates a series', () => { const next = results => results; const results = percentile(resp, panel, series)(next)([]); - expect(results).to.have.length(3); + expect(results).toHaveLength(3); - expect(results[0]).to.have.property('id', 'test:10-90'); - expect(results[0]).to.have.property('color', 'rgb(255, 0, 0)'); - expect(results[0]).to.have.property('fillBetween', 'test:10-90:90'); - expect(results[0]).to.have.property('label', 'Percentile of cpu (10)'); - expect(results[0]).to.have.property('legend', false); - expect(results[0]).to.have.property('lines'); - expect(results[0].lines).to.eql({ + expect(results[0]).toHaveProperty('id', 'test:10-90'); + expect(results[0]).toHaveProperty('color', 'rgb(255, 0, 0)'); + expect(results[0]).toHaveProperty('fillBetween', 'test:10-90:90'); + expect(results[0]).toHaveProperty('label', 'Percentile of cpu (10)'); + expect(results[0]).toHaveProperty('legend', false); + expect(results[0]).toHaveProperty('lines'); + expect(results[0].lines).toEqual({ fill: 0.2, lineWidth: 0, show: true, }); - expect(results[0]).to.have.property('points'); - expect(results[0].points).to.eql({ show: false }); - expect(results[0].data).to.eql([ + expect(results[0]).toHaveProperty('points'); + expect(results[0].points).toEqual({ show: false }); + expect(results[0].data).toEqual([ [1, 1], [2, 1.2], ]); - expect(results[1]).to.have.property('id', 'test:10-90:90'); - expect(results[1]).to.have.property('color', 'rgb(255, 0, 0)'); - expect(results[1]).to.have.property('label', 'Percentile of cpu (10)'); - expect(results[1]).to.have.property('legend', false); - expect(results[1]).to.have.property('lines'); - expect(results[1].lines).to.eql({ + expect(results[1]).toHaveProperty('id', 'test:10-90:90'); + expect(results[1]).toHaveProperty('color', 'rgb(255, 0, 0)'); + expect(results[1]).toHaveProperty('label', 'Percentile of cpu (10)'); + expect(results[1]).toHaveProperty('legend', false); + expect(results[1]).toHaveProperty('lines'); + expect(results[1].lines).toEqual({ fill: false, lineWidth: 0, show: true, }); - expect(results[1]).to.have.property('points'); - expect(results[1].points).to.eql({ show: false }); - expect(results[1].data).to.eql([ + expect(results[1]).toHaveProperty('points'); + expect(results[1].points).toEqual({ show: false }); + expect(results[1].data).toEqual([ [1, 5], [2, 5.3], ]); - expect(results[2]).to.have.property('id', 'test:50'); - expect(results[2]).to.have.property('color', 'rgb(255, 0, 0)'); - expect(results[2]).to.have.property('label', 'Percentile of cpu (50)'); - expect(results[2]).to.have.property('stack', false); - expect(results[2]).to.have.property('lines'); - expect(results[2].lines).to.eql({ + expect(results[2]).toHaveProperty('id', 'test:50'); + expect(results[2]).toHaveProperty('color', 'rgb(255, 0, 0)'); + expect(results[2]).toHaveProperty('label', 'Percentile of cpu (50)'); + expect(results[2]).toHaveProperty('stack', false); + expect(results[2]).toHaveProperty('lines'); + expect(results[2].lines).toEqual({ fill: 0, lineWidth: 1, show: true, steps: false, }); - expect(results[2]).to.have.property('bars'); - expect(results[2].bars).to.eql({ + expect(results[2]).toHaveProperty('bars'); + expect(results[2].bars).toEqual({ fill: 0, lineWidth: 1, show: false, }); - expect(results[2]).to.have.property('points'); - expect(results[2].points).to.eql({ show: true, lineWidth: 1, radius: 1 }); - expect(results[2].data).to.eql([ + expect(results[2]).toHaveProperty('points'); + expect(results[2].points).toEqual({ show: true, lineWidth: 1, radius: 1 }); + expect(results[2].data).toEqual([ [1, 2.5], [2, 2.7], ]); diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/__tests__/series_agg.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/series_agg.test.js similarity index 88% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/__tests__/series_agg.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/series_agg.test.js index 2391cd3bc7698..3e09c51d9184f 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/__tests__/series_agg.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/series_agg.test.js @@ -17,10 +17,8 @@ * under the License. */ -import { seriesAgg } from '../series_agg'; -import { stdMetric } from '../std_metric'; -import { expect } from 'chai'; -import sinon from 'sinon'; +import { seriesAgg } from './series_agg'; +import { stdMetric } from './std_metric'; describe('seriesAgg(resp, panel, series)', () => { let panel; @@ -93,18 +91,18 @@ describe('seriesAgg(resp, panel, series)', () => { }; }); - it('calls next when finished', () => { - const next = sinon.spy(); + test('calls next when finished', () => { + const next = jest.fn(); seriesAgg(resp, panel, series)(next)([]); - expect(next.calledOnce).to.equal(true); + expect(next.mock.calls.length).toEqual(1); }); - it('creates a series', () => { + test('creates a series', () => { const next = seriesAgg(resp, panel, series)(results => results); const results = stdMetric(resp, panel, series)(next)([]); - expect(results).to.have.length(1); + expect(results).toHaveLength(1); - expect(results[0]).to.eql({ + expect(results[0]).toEqual({ id: 'test', color: '#F00', label: 'Total CPU', diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/__tests__/std_deviation_bands.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_bands.test.js similarity index 87% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/__tests__/std_deviation_bands.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_bands.test.js index 488ae4a8351b7..77949ff94dc4c 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/__tests__/std_deviation_bands.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_bands.test.js @@ -17,9 +17,7 @@ * under the License. */ -import { stdDeviationBands } from '../std_deviation_bands'; -import { expect } from 'chai'; -import sinon from 'sinon'; +import { stdDeviationBands } from './std_deviation_bands'; describe('stdDeviationBands(resp, panel, series)', () => { let panel; @@ -79,18 +77,18 @@ describe('stdDeviationBands(resp, panel, series)', () => { }; }); - it('calls next when finished', () => { - const next = sinon.spy(); + test('calls next when finished', () => { + const next = jest.fn(); stdDeviationBands(resp, panel, series)(next)([]); - expect(next.calledOnce).to.equal(true); + expect(next.mock.calls.length).toEqual(1); }); - it('creates a series', () => { + test('creates a series', () => { const next = results => results; const results = stdDeviationBands(resp, panel, series)(next)([]); - expect(results).to.have.length(2); + expect(results).toHaveLength(2); - expect(results[0]).to.eql({ + expect(results[0]).toEqual({ id: 'test:upper', label: 'Std. Deviation of cpu', color: 'rgb(255, 0, 0)', @@ -103,7 +101,7 @@ describe('stdDeviationBands(resp, panel, series)', () => { ], }); - expect(results[1]).to.eql({ + expect(results[1]).toEqual({ id: 'test:lower', color: 'rgb(255, 0, 0)', lines: { show: true, fill: false, lineWidth: 0 }, diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/__tests__/std_deviation_sibling.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.test.js similarity index 87% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/__tests__/std_deviation_sibling.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.test.js index 14c2c16ef728f..adc5a3a4a991b 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/__tests__/std_deviation_sibling.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_deviation_sibling.test.js @@ -17,9 +17,7 @@ * under the License. */ -import { stdDeviationSibling } from '../std_deviation_sibling'; -import { expect } from 'chai'; -import sinon from 'sinon'; +import { stdDeviationSibling } from './std_deviation_sibling'; describe('stdDeviationSibling(resp, panel, series)', () => { let panel; @@ -79,18 +77,18 @@ describe('stdDeviationSibling(resp, panel, series)', () => { }; }); - it('calls next when finished', () => { - const next = sinon.spy(); + test('calls next when finished', () => { + const next = jest.fn(); stdDeviationSibling(resp, panel, series)(next)([]); - expect(next.calledOnce).to.equal(true); + expect(next.mock.calls.length).toEqual(1); }); - it('creates a series', () => { + test('creates a series', () => { const next = results => results; const results = stdDeviationSibling(resp, panel, series)(next)([]); - expect(results).to.have.length(2); + expect(results).toHaveLength(2); - expect(results[0]).to.eql({ + expect(results[0]).toEqual({ id: 'test:lower', color: 'rgb(255, 0, 0)', lines: { show: true, fill: false, lineWidth: 0 }, @@ -101,7 +99,7 @@ describe('stdDeviationSibling(resp, panel, series)', () => { ], }); - expect(results[1]).to.eql({ + expect(results[1]).toEqual({ id: 'test:upper', label: 'Overall Std. Deviation of Average of cpu', color: 'rgb(255, 0, 0)', diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/__tests__/std_metric.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_metric.test.js similarity index 65% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/__tests__/std_metric.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_metric.test.js index 5039a406a6acc..6ddd6f907fa97 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/__tests__/std_metric.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_metric.test.js @@ -17,9 +17,7 @@ * under the License. */ -import sinon from 'sinon'; -import { expect } from 'chai'; -import { stdMetric } from '../std_metric'; +import { stdMetric } from './std_metric'; describe('stdMetric(resp, panel, series)', () => { let panel; @@ -60,41 +58,41 @@ describe('stdMetric(resp, panel, series)', () => { }; }); - it('calls next when finished', () => { - const next = sinon.spy(); + test('calls next when finished', () => { + const next = jest.fn(); stdMetric(resp, panel, series)(next)([]); - expect(next.calledOnce).to.equal(true); + expect(next.mock.calls.length).toEqual(1); }); - it('calls next when finished (percentile)', () => { + test('calls next when finished (percentile)', () => { series.metrics[0].type = 'percentile'; - const next = sinon.spy(d => d); + const next = jest.fn(d => d); const results = stdMetric(resp, panel, series)(next)([]); - expect(next.calledOnce).to.equal(true); - expect(results).to.have.length(0); + expect(next.mock.calls.length).toEqual(1); + expect(results).toHaveLength(0); }); - it('calls next when finished (std_deviation band)', () => { + test('calls next when finished (std_deviation band)', () => { series.metrics[0].type = 'std_deviation'; series.metrics[0].mode = 'band'; - const next = sinon.spy(d => d); + const next = jest.fn(d => d); const results = stdMetric(resp, panel, series)(next)([]); - expect(next.calledOnce).to.equal(true); - expect(results).to.have.length(0); + expect(next.mock.calls.length).toEqual(1); + expect(results).toHaveLength(0); }); - it('creates a series', () => { + test('creates a series', () => { const next = results => results; const results = stdMetric(resp, panel, series)(next)([]); - expect(results).to.have.length(1); - expect(results[0]).to.have.property('color', 'rgb(255, 0, 0)'); - expect(results[0]).to.have.property('id', 'test'); - expect(results[0]).to.have.property('label', 'Average of cpu'); - expect(results[0]).to.have.property('lines'); - expect(results[0]).to.have.property('stack'); - expect(results[0]).to.have.property('bars'); - expect(results[0]).to.have.property('points'); - expect(results[0].data).to.eql([ + expect(results).toHaveLength(1); + expect(results[0]).toHaveProperty('color', 'rgb(255, 0, 0)'); + expect(results[0]).toHaveProperty('id', 'test'); + expect(results[0]).toHaveProperty('label', 'Average of cpu'); + expect(results[0]).toHaveProperty('lines'); + expect(results[0]).toHaveProperty('stack'); + expect(results[0]).toHaveProperty('bars'); + expect(results[0]).toHaveProperty('points'); + expect(results[0].data).toEqual([ [1, 1], [2, 2], ]); diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/__tests__/std_sibling.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_sibling.test.js similarity index 82% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/__tests__/std_sibling.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_sibling.test.js index a243ad525332c..9ed60a6d16492 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/__tests__/std_sibling.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/std_sibling.test.js @@ -17,9 +17,7 @@ * under the License. */ -import { stdSibling } from '../std_sibling'; -import { expect } from 'chai'; -import sinon from 'sinon'; +import { stdSibling } from './std_sibling'; describe('stdSibling(resp, panel, series)', () => { let panel; @@ -74,26 +72,26 @@ describe('stdSibling(resp, panel, series)', () => { }; }); - it('calls next when finished', () => { - const next = sinon.spy(); + test('calls next when finished', () => { + const next = jest.fn(); stdSibling(resp, panel, series)(next)([]); - expect(next.calledOnce).to.equal(true); + expect(next.mock.calls.length).toEqual(1); }); - it('calls next when std. deviation bands set', () => { + test('calls next when std. deviation bands set', () => { series.metrics[1].mode = 'band'; - const next = sinon.spy(results => results); + const next = jest.fn(results => results); const results = stdSibling(resp, panel, series)(next)([]); - expect(next.calledOnce).to.equal(true); - expect(results).to.have.length(0); + expect(next.mock.calls.length).toEqual(1); + expect(results).toHaveLength(0); }); - it('creates a series', () => { + test('creates a series', () => { const next = results => results; const results = stdSibling(resp, panel, series)(next)([]); - expect(results).to.have.length(1); + expect(results).toHaveLength(1); - expect(results[0]).to.eql({ + expect(results[0]).toEqual({ id: 'test', label: 'Overall Std. Deviation of Average of cpu', color: 'rgb(255, 0, 0)', diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/__tests__/time_shift.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/time_shift.test.js similarity index 71% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/__tests__/time_shift.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/time_shift.test.js index 1e6da85f88164..7047c54fa1c0e 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/__tests__/time_shift.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/time_shift.test.js @@ -17,10 +17,8 @@ * under the License. */ -import sinon from 'sinon'; -import { expect } from 'chai'; -import { timeShift } from '../time_shift'; -import { stdMetric } from '../std_metric'; +import { timeShift } from './time_shift'; +import { stdMetric } from './std_metric'; describe('timeShift(resp, panel, series)', () => { let panel; @@ -62,24 +60,24 @@ describe('timeShift(resp, panel, series)', () => { }; }); - it('calls next when finished', () => { - const next = sinon.spy(); + test('calls next when finished', () => { + const next = jest.fn(); timeShift(resp, panel, series)(next)([]); - expect(next.calledOnce).to.equal(true); + expect(next.mock.calls.length).toEqual(1); }); - it('creates a series', () => { + test('creates a series', () => { const next = timeShift(resp, panel, series)(results => results); const results = stdMetric(resp, panel, series)(next)([]); - expect(results).to.have.length(1); - expect(results[0]).to.have.property('color', 'rgb(255, 0, 0)'); - expect(results[0]).to.have.property('id', 'test'); - expect(results[0]).to.have.property('label', 'Average of cpu'); - expect(results[0]).to.have.property('lines'); - expect(results[0]).to.have.property('stack'); - expect(results[0]).to.have.property('bars'); - expect(results[0]).to.have.property('points'); - expect(results[0].data).to.eql([ + expect(results).toHaveLength(1); + expect(results[0]).toHaveProperty('color', 'rgb(255, 0, 0)'); + expect(results[0]).toHaveProperty('id', 'test'); + expect(results[0]).toHaveProperty('label', 'Average of cpu'); + expect(results[0]).toHaveProperty('lines'); + expect(results[0]).toHaveProperty('stack'); + expect(results[0]).toHaveProperty('bars'); + expect(results[0]).toHaveProperty('points'); + expect(results[0].data).toEqual([ [1483225200000 + 3600000, 1], [1483225210000 + 3600000, 2], ]); diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/series/__tests__/build_request_body.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts similarity index 95% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/series/__tests__/build_request_body.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts index f21bf3d197969..0c75e6ef1c5bd 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/series/__tests__/build_request_body.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts @@ -17,6 +17,8 @@ * under the License. */ +import { buildRequestBody } from './build_request_body'; + const body = JSON.parse(` { "filters": [ @@ -75,15 +77,11 @@ const body = JSON.parse(` } `); -import sinon from 'sinon'; -import { expect } from 'chai'; -import { buildRequestBody } from '../build_request_body'; - describe('buildRequestBody(req)', () => { - it('returns a valid body', () => { + test('returns a valid body', () => { const panel = body.panels[0]; const series = panel.series[0]; - const getValidTimeInterval = sinon.spy(() => '10s'); + const getValidTimeInterval = jest.fn(() => '10s'); const capabilities = { searchTimezone: 'UTC', getValidTimeInterval, @@ -102,7 +100,7 @@ describe('buildRequestBody(req)', () => { capabilities ); - expect(doc).to.eql({ + expect(doc).toEqual({ size: 0, query: { bool: { diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.ts similarity index 95% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.js rename to src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.ts index fe3137a8f86ba..85e1f8f7eb12b 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.ts @@ -18,6 +18,7 @@ */ import { buildProcessorFunction } from '../build_processor_function'; +// @ts-ignore import { processors } from '../request_processors/series'; /** @@ -33,7 +34,7 @@ import { processors } from '../request_processors/series'; * ] * @returns {Object} doc - processed body */ -export function buildRequestBody(...args) { +export function buildRequestBody(...args: any[]) { const processor = buildProcessorFunction(processors, ...args); const doc = processor({}); return doc; diff --git a/src/legacy/core_plugins/visualizations/public/legacy_imports.ts b/src/legacy/core_plugins/visualizations/public/legacy_imports.ts index 92d8ac2c7db3a..e10cbe9aefea6 100644 --- a/src/legacy/core_plugins/visualizations/public/legacy_imports.ts +++ b/src/legacy/core_plugins/visualizations/public/legacy_imports.ts @@ -27,4 +27,6 @@ export { } from '../../../ui/public/agg_types/buckets/date_histogram'; export { createFormat } from '../../../ui/public/visualize/loader/pipeline_helpers/utilities'; export { I18nContext } from '../../../ui/public/i18n'; +import chrome from '../../../ui/public/chrome'; +export { chrome as legacyChrome }; import '../../../ui/public/directives/bind'; diff --git a/src/legacy/core_plugins/visualizations/public/expressions/vis.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/expressions/vis.js similarity index 96% rename from src/legacy/core_plugins/visualizations/public/expressions/vis.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/expressions/vis.js index cbadfd9da3788..81224b65f7786 100644 --- a/src/legacy/core_plugins/visualizations/public/expressions/vis.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/expressions/vis.js @@ -29,8 +29,9 @@ import { EventEmitter } from 'events'; import _ from 'lodash'; -import { PersistedState } from '../../../../ui/public/persisted_state'; -import { getTypes } from '../np_ready/public/services'; +import { PersistedState } from '../../../legacy_imports'; + +import { getTypes } from '../services'; export class Vis extends EventEmitter { constructor(visState = { type: 'histogram' }) { diff --git a/src/legacy/core_plugins/visualizations/public/expressions/visualization_function.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/expressions/visualization_function.ts similarity index 87% rename from src/legacy/core_plugins/visualizations/public/expressions/visualization_function.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/expressions/visualization_function.ts index 37b437c1c2dd6..24eb974f6edee 100644 --- a/src/legacy/core_plugins/visualizations/public/expressions/visualization_function.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/expressions/visualization_function.ts @@ -19,13 +19,10 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import chrome from 'ui/chrome'; -import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; -import { PersistedState } from 'ui/persisted_state'; import { VisResponseValue } from 'src/plugins/visualizations/public'; import { ExpressionFunction, Render } from 'src/plugins/expressions/public'; -import { npStart } from 'ui/new_platform'; -import { getTypes } from '../np_ready/public/services'; +import { PersistedState } from '../../../legacy_imports'; +import { getTypes, getIndexPatterns, getFilterManager } from '../services'; interface Arguments { index?: string | null; @@ -90,15 +87,10 @@ export const visualization = (): ExpressionFunctionVisualization => ({ }, }, async fn(context, args, handlers) { - const $injector = await chrome.dangerouslyGetActiveInjector(); - const Private = $injector.get('Private') as any; - const indexPatterns = npStart.plugins.data.indexPatterns; - const queryFilter = Private(FilterBarQueryFilterProvider); - const visConfigParams = args.visConfig ? JSON.parse(args.visConfig) : {}; const schemas = args.schemas ? JSON.parse(args.schemas) : {}; const visType = getTypes().get(args.type || 'histogram') as any; - const indexPattern = args.index ? await indexPatterns.get(args.index) : null; + const indexPattern = args.index ? await getIndexPatterns().get(args.index) : null; const uiStateParams = args.uiState ? JSON.parse(args.uiState) : {}; const uiState = new PersistedState(uiStateParams); @@ -114,7 +106,7 @@ export const visualization = (): ExpressionFunctionVisualization => ({ filters: get(context, 'filters', null), uiState, inspectorAdapters: handlers.inspectorAdapters, - queryFilter, + queryFilter: getFilterManager(), forceFetch: true, }); } diff --git a/src/legacy/core_plugins/visualizations/public/expressions/visualization_renderer.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/expressions/visualization_renderer.tsx similarity index 92% rename from src/legacy/core_plugins/visualizations/public/expressions/visualization_renderer.tsx rename to src/legacy/core_plugins/visualizations/public/np_ready/public/expressions/visualization_renderer.tsx index 5f894c7eb60e3..2a12884ecf7c8 100644 --- a/src/legacy/core_plugins/visualizations/public/expressions/visualization_renderer.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/expressions/visualization_renderer.tsx @@ -17,12 +17,12 @@ * under the License. */ -import chrome from 'ui/chrome'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; +import { legacyChrome } from '../../../legacy_imports'; // @ts-ignore import { Vis } from './vis'; -import { Visualization } from '../np_ready/public/components'; +import { Visualization } from '../components'; export const visualization = () => ({ name: 'visualization', @@ -31,7 +31,7 @@ export const visualization = () => ({ render: async (domNode: HTMLElement, config: any, handlers: any) => { const { visData, visConfig, params } = config; const visType = config.visType || visConfig.type; - const $injector = await chrome.dangerouslyGetActiveInjector(); + const $injector = await legacyChrome.dangerouslyGetActiveInjector(); const $rootScope = $injector.get('$rootScope') as any; if (handlers.vis) { diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts index 7584556324e8b..cfd22f88167c5 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ + import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { TypesService, TypesSetup, TypesStart } from './types'; import { @@ -27,13 +28,14 @@ import { setIndexPatterns, setSavedObjects, setUsageCollector, + setFilterManager, } from './services'; import { VisualizeEmbeddableFactory } from '../../embeddable/visualize_embeddable_factory'; import { VISUALIZE_EMBEDDABLE_TYPE } from '../../embeddable'; import { ExpressionsSetup } from '../../../../../../plugins/expressions/public'; import { IEmbeddableSetup } from '../../../../../../plugins/embeddable/public'; -import { visualization as visualizationFunction } from '../../expressions/visualization_function'; -import { visualization as visualizationRenderer } from '../../expressions/visualization_renderer'; +import { visualization as visualizationFunction } from './expressions/visualization_function'; +import { visualization as visualizationRenderer } from './expressions/visualization_renderer'; import { DataPublicPluginStart } from '../../../../../../plugins/data/public'; import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/public'; /** @@ -106,6 +108,7 @@ export class VisualizationsPlugin setHttp(core.http); setSavedObjects(core.savedObjects); setIndexPatterns(data.indexPatterns); + setFilterManager(data.query.filterManager); return { types, diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/services.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/services.ts index 4b426fee6c7da..433c5c7b6df0d 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/services.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/services.ts @@ -26,7 +26,7 @@ import { } from 'src/core/public'; import { TypesStart } from './types'; import { createGetterSetter } from '../../../../../../plugins/kibana_utils/public'; -import { IndexPatternsContract } from '../../../../../../plugins/data/public'; +import { FilterManager, IndexPatternsContract } from '../../../../../../plugins/data/public'; import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/public'; export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); @@ -43,6 +43,10 @@ export const [getTypes, setTypes] = createGetterSetter('Types'); export const [getI18n, setI18n] = createGetterSetter('I18n'); +export const [getFilterManager, setFilterManager] = createGetterSetter( + 'FilterManager' +); + export const [getIndexPatterns, setIndexPatterns] = createGetterSetter( 'IndexPatterns' ); diff --git a/src/plugins/bfetch/README.md b/src/plugins/bfetch/README.md index 9c18720e30d96..9ed90a4de306e 100644 --- a/src/plugins/bfetch/README.md +++ b/src/plugins/bfetch/README.md @@ -3,7 +3,54 @@ `bfetch` allows to batch HTTP requests and streams responses back. +# Example + +We will create a batch processing endpoint that receives a number then doubles it +and streams it back. We will also consider the number to be time in milliseconds +and before streaming the number back the server will wait for the specified number of +milliseconds. + +To do that, first create server-side batch processing route using [`addBatchProcessingRoute`](./docs/server/reference.md#addBatchProcessingRoute). + +```ts +plugins.bfetch.addBatchProcessingRoute<{ num: number }, { num: number }>( + '/my-plugin/double', + () => ({ + onBatchItem: async ({ num }) => { + // Validate inputs. + if (num < 0) throw new Error('Invalid number'); + // Wait number of specified milliseconds. + await new Promise(r => setTimeout(r, num)); + // Double the number and send it back. + return { num: 2 * num }; + }, + }) +); +``` + +Now on client-side create `double` function using [`batchedFunction`](./docs/browser/reference.md#batchedFunction). +The newly created `double` function can be called many times and it +will package individual calls into batches and send them to the server. + +```ts +const double = plugins.bfetch.batchedFunction<{ num: number }, { num: number }>({ + url: '/my-plugin/double', +}); +``` + +Note: the created `double` must accept a single object argument (`{ num: number }` in this case) +and it will return a promise that resolves into an object, too (also `{ num: number }` in this case). + +Use the `double` function. + +```ts +double({ num: 1 }).then(console.log, console.error); // { num: 2 } +double({ num: 2 }).then(console.log, console.error); // { num: 4 } +double({ num: 3 }).then(console.log, console.error); // { num: 6 } +``` + + ## Reference - [Browser](./docs/browser/reference.md) -- Server +- [Server](./docs/server/reference.md) diff --git a/src/plugins/bfetch/common/batch.ts b/src/plugins/bfetch/common/batch.ts new file mode 100644 index 0000000000000..6fd2c7e35ed91 --- /dev/null +++ b/src/plugins/bfetch/common/batch.ts @@ -0,0 +1,32 @@ +/* + * 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 interface ErrorLike { + message: string; +} + +export interface BatchRequestData { + batch: Item[]; +} + +export interface BatchResponseItem { + id: number; + result?: Result; + error?: Error; +} diff --git a/src/plugins/bfetch/common/buffer/create_batched_function.ts b/src/plugins/bfetch/common/buffer/create_batched_function.ts new file mode 100644 index 0000000000000..24f28659863a7 --- /dev/null +++ b/src/plugins/bfetch/common/buffer/create_batched_function.ts @@ -0,0 +1,49 @@ +/* + * 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 { ItemBufferParams } from './item_buffer'; +import { TimedItemBufferParams, TimedItemBuffer } from './timed_item_buffer'; + +type Fn = (...args: any) => any; + +export interface BatchedFunctionParams { + onCall: (...args: Parameters) => [ReturnType, BatchEntry]; + onBatch: (items: BatchEntry[]) => void; + flushOnMaxItems?: ItemBufferParams['flushOnMaxItems']; + maxItemAge?: TimedItemBufferParams['maxItemAge']; +} + +export const createBatchedFunction = ( + params: BatchedFunctionParams +): [Func, TimedItemBuffer] => { + const { onCall, onBatch, maxItemAge = 10, flushOnMaxItems = 25 } = params; + const buffer = new TimedItemBuffer({ + onFlush: onBatch, + maxItemAge, + flushOnMaxItems, + }); + + const fn: Func = ((...args) => { + const [result, batchEntry] = onCall(...args); + buffer.write(batchEntry); + return result; + }) as Func; + + return [fn, buffer]; +}; diff --git a/src/plugins/bfetch/common/buffer/index.ts b/src/plugins/bfetch/common/buffer/index.ts new file mode 100644 index 0000000000000..33bc52733289b --- /dev/null +++ b/src/plugins/bfetch/common/buffer/index.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. + */ + +export * from './item_buffer'; +export * from './timed_item_buffer'; +export * from './create_batched_function'; diff --git a/src/plugins/bfetch/common/buffer/item_buffer.ts b/src/plugins/bfetch/common/buffer/item_buffer.ts new file mode 100644 index 0000000000000..663aa5d7b0b7f --- /dev/null +++ b/src/plugins/bfetch/common/buffer/item_buffer.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. + */ + +export interface ItemBufferParams { + /** + * Flushes buffer automatically if number of items in the buffer reaches + * this number. Omit it or set to `Infinity` to never flush on max buffer + * size automatically. + */ + flushOnMaxItems?: number; + + /** + * Callback that is called every time buffer is flushed. It receives a single + * argument which is a list of all buffered items. If `.flush()` is called + * when buffer is empty, `.onflush` is called with empty array. + */ + onFlush: (items: Item[]) => void; +} + +/** + * A simple buffer that collects items. Can be cleared or flushed; and can + * automatically flush when specified number of items is reached. + */ +export class ItemBuffer { + private list: Item[] = []; + + constructor(public readonly params: ItemBufferParams) {} + + /** + * Get current buffer size. + */ + public get length(): number { + return this.list.length; + } + + /** + * Add item to the buffer. + */ + public write(item: Item) { + this.list.push(item); + + const { flushOnMaxItems } = this.params; + if (flushOnMaxItems) { + if (this.list.length >= flushOnMaxItems) { + this.flush(); + } + } + } + + /** + * Remove all items from the buffer. + */ + public clear() { + this.list = []; + } + + /** + * Call `.onflush` method and clear buffer. + */ + public flush() { + let list; + [list, this.list] = [this.list, []]; + this.params.onFlush(list); + } +} diff --git a/src/plugins/bfetch/common/buffer/tests/create_batched_function.test.ts b/src/plugins/bfetch/common/buffer/tests/create_batched_function.test.ts new file mode 100644 index 0000000000000..5b145a2523070 --- /dev/null +++ b/src/plugins/bfetch/common/buffer/tests/create_batched_function.test.ts @@ -0,0 +1,75 @@ +/* + * 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 { createBatchedFunction } from '../create_batched_function'; + +describe('createBatchedFunction', () => { + test('calls onCall every time fn is called, calls onBatch once flushOnMaxItems reached', async () => { + const onBatch = jest.fn(); + const onCall = jest.fn(() => [1, 2] as any); + const [fn] = createBatchedFunction({ + onBatch, + onCall, + flushOnMaxItems: 2, + maxItemAge: 10, + }); + + expect(onCall).toHaveBeenCalledTimes(0); + expect(onBatch).toHaveBeenCalledTimes(0); + + fn(123); + + expect(onCall).toHaveBeenCalledTimes(1); + expect(onCall).toHaveBeenCalledWith(123); + expect(onBatch).toHaveBeenCalledTimes(0); + + fn(456); + + expect(onCall).toHaveBeenCalledTimes(2); + expect(onCall).toHaveBeenCalledWith(456); + expect(onBatch).toHaveBeenCalledTimes(1); + expect(onBatch).toHaveBeenCalledWith([2, 2]); + }); + + test('calls onBatch once timeout is reached', async () => { + const onBatch = jest.fn(); + const onCall = jest.fn(() => [4, 3] as any); + const [fn] = createBatchedFunction({ + onBatch, + onCall, + flushOnMaxItems: 2, + maxItemAge: 10, + }); + + expect(onCall).toHaveBeenCalledTimes(0); + expect(onBatch).toHaveBeenCalledTimes(0); + + fn(123); + + expect(onCall).toHaveBeenCalledTimes(1); + expect(onCall).toHaveBeenCalledWith(123); + expect(onBatch).toHaveBeenCalledTimes(0); + + await new Promise(r => setTimeout(r, 15)); + + expect(onCall).toHaveBeenCalledTimes(1); + expect(onBatch).toHaveBeenCalledTimes(1); + expect(onBatch).toHaveBeenCalledWith([3]); + }); +}); diff --git a/src/plugins/bfetch/common/buffer/tests/item_buffer.test.ts b/src/plugins/bfetch/common/buffer/tests/item_buffer.test.ts new file mode 100644 index 0000000000000..a921fa8e589a3 --- /dev/null +++ b/src/plugins/bfetch/common/buffer/tests/item_buffer.test.ts @@ -0,0 +1,23 @@ +/* + * 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 { ItemBuffer } from '../item_buffer'; +import { runItemBufferTests } from './run_item_buffer_tests'; + +runItemBufferTests(ItemBuffer); diff --git a/src/plugins/bfetch/common/buffer/tests/run_item_buffer_tests.ts b/src/plugins/bfetch/common/buffer/tests/run_item_buffer_tests.ts new file mode 100644 index 0000000000000..b3ba9375448dc --- /dev/null +++ b/src/plugins/bfetch/common/buffer/tests/run_item_buffer_tests.ts @@ -0,0 +1,239 @@ +/* + * 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 { ItemBuffer, ItemBufferParams } from '../item_buffer'; + +export const runItemBufferTests = ( + Buffer: new >(params: Params) => ItemBuffer +) => { + describe('ItemBuffer', () => { + test('can create with or without optional "flushOnMaxItems" param', () => { + new Buffer({ + onFlush: () => {}, + }); + + new Buffer({ + onFlush: () => {}, + flushOnMaxItems: 123, + }); + }); + + test('can add items to the buffer', () => { + const onFlush = jest.fn(); + const buf = new Buffer({ + onFlush, + }); + + buf.write('a'); + buf.write('b'); + buf.write('c'); + }); + + test('returns number of items in the buffer', () => { + const onFlush = jest.fn(); + const buf = new Buffer({ + onFlush, + }); + + expect(buf.length).toBe(0); + buf.write('a'); + expect(buf.length).toBe(1); + buf.write('b'); + expect(buf.length).toBe(2); + buf.write('c'); + expect(buf.length).toBe(3); + }); + + test('returns correct number of items after .clear() was called', () => { + const onFlush = jest.fn(); + const buf = new Buffer({ + onFlush, + }); + + expect(buf.length).toBe(0); + buf.write('a'); + expect(buf.length).toBe(1); + buf.clear(); + buf.write('b'); + expect(buf.length).toBe(1); + buf.write('c'); + expect(buf.length).toBe(2); + }); + + test('returns correct number of items after .flush() was called', () => { + const onFlush = jest.fn(); + const buf = new Buffer({ + onFlush, + }); + + expect(buf.length).toBe(0); + buf.write('a'); + expect(buf.length).toBe(1); + buf.flush(); + buf.write('b'); + expect(buf.length).toBe(1); + buf.write('c'); + expect(buf.length).toBe(2); + }); + + test('can flush buffer and receive items in chronological order', () => { + const onFlush = jest.fn(); + const buf = new Buffer({ + onFlush, + }); + + buf.write('a'); + buf.write('b'); + buf.write('c'); + + buf.flush(); + + expect(onFlush).toHaveBeenCalledTimes(1); + expect(onFlush.mock.calls[0][0]).toEqual(['a', 'b', 'c']); + }); + + test('clears buffer after flush', () => { + const onFlush = jest.fn(); + const buf = new Buffer({ + onFlush, + }); + + buf.write('a'); + buf.write('b'); + buf.write('c'); + + buf.flush(); + + expect(onFlush).toHaveBeenCalledTimes(1); + expect(onFlush.mock.calls[0][0]).toEqual(['a', 'b', 'c']); + + buf.write('d'); + + buf.flush(); + + expect(onFlush).toHaveBeenCalledTimes(2); + expect(onFlush.mock.calls[1][0]).toEqual(['d']); + }); + + test('can call .flush() any time as many times as needed', () => { + const onFlush = jest.fn(); + const buf = new Buffer({ + onFlush, + }); + + buf.flush(); + buf.write(123); + buf.flush(); + buf.flush(); + buf.flush(); + + expect(onFlush).toHaveBeenCalledTimes(4); + expect(onFlush.mock.calls[0][0]).toEqual([]); + expect(onFlush.mock.calls[1][0]).toEqual([123]); + expect(onFlush.mock.calls[2][0]).toEqual([]); + expect(onFlush.mock.calls[3][0]).toEqual([]); + }); + + test('calling .clear() before .flush() cases to return empty list', () => { + const onFlush = jest.fn(); + const buf = new Buffer({ + onFlush, + }); + + buf.write(1); + buf.write(2); + buf.clear(); + buf.flush(); + + expect(onFlush).toHaveBeenCalledTimes(1); + expect(onFlush.mock.calls[0][0]).toEqual([]); + }); + + test('can call .clear() any time as many times as needed', () => { + const onFlush = jest.fn(); + const buf = new Buffer({ + onFlush, + }); + + buf.clear(); + buf.flush(); + buf.write(123); + buf.clear(); + buf.flush(); + buf.clear(); + buf.clear(); + buf.flush(); + buf.flush(); + + expect(onFlush).toHaveBeenCalledTimes(4); + expect(onFlush.mock.calls[0][0]).toEqual([]); + expect(onFlush.mock.calls[1][0]).toEqual([]); + expect(onFlush.mock.calls[2][0]).toEqual([]); + expect(onFlush.mock.calls[3][0]).toEqual([]); + }); + + describe('when `flushOnMaxItems` is set', () => { + test('does not flush automatically before `flushOnMaxItems` is reached', () => { + const onFlush = jest.fn(); + const buf = new Buffer({ + onFlush, + flushOnMaxItems: 2, + }); + + buf.write(1); + + expect(onFlush).toHaveBeenCalledTimes(0); + }); + + test('automatically flushes buffer when `flushOnMaxItems` is reached', () => { + const onFlush = jest.fn(); + const buf = new Buffer({ + onFlush, + flushOnMaxItems: 2, + }); + + buf.write(1); + buf.write(2); + + expect(onFlush).toHaveBeenCalledTimes(1); + expect(onFlush.mock.calls[0][0]).toEqual([1, 2]); + }); + + test('flushes again when `flushOnMaxItems` limit is reached the second time', () => { + const onFlush = jest.fn(); + const buf = new Buffer({ + onFlush, + flushOnMaxItems: 2, + }); + + buf.write(1); + buf.write(2); + buf.write(3); + buf.write(4); + buf.write(5); + buf.flush(); + + expect(onFlush).toHaveBeenCalledTimes(3); + expect(onFlush.mock.calls[0][0]).toEqual([1, 2]); + expect(onFlush.mock.calls[1][0]).toEqual([3, 4]); + expect(onFlush.mock.calls[2][0]).toEqual([5]); + }); + }); + }); +}; diff --git a/src/plugins/bfetch/common/buffer/tests/timed_item_buffer.test.ts b/src/plugins/bfetch/common/buffer/tests/timed_item_buffer.test.ts new file mode 100644 index 0000000000000..c1c6a8f187a44 --- /dev/null +++ b/src/plugins/bfetch/common/buffer/tests/timed_item_buffer.test.ts @@ -0,0 +1,104 @@ +/* + * 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 { TimedItemBuffer } from '../timed_item_buffer'; +import { runItemBufferTests } from './run_item_buffer_tests'; + +describe('TimedItemBuffer', () => { + runItemBufferTests(TimedItemBuffer); + + test('does not do unnecessary flushes', async () => { + const onFlush = jest.fn(); + const buf = new TimedItemBuffer({ + onFlush, + maxItemAge: 3, + }); + + expect(onFlush).toHaveBeenCalledTimes(0); + buf.write(0); + expect(onFlush).toHaveBeenCalledTimes(0); + buf.flush(); + expect(onFlush).toHaveBeenCalledTimes(1); + }); + + test('does not do extra flush after timeout if buffer was flushed during timeout wait', async () => { + const onFlush = jest.fn(); + const buf = new TimedItemBuffer({ + onFlush, + maxItemAge: 10, + }); + + buf.write(0); + await new Promise(r => setTimeout(r, 3)); + buf.flush(); + await new Promise(r => setTimeout(r, 11)); + + expect(onFlush).toHaveBeenCalledTimes(1); + }); + + test('flushes buffer automatically after timeout reached', async () => { + const onFlush = jest.fn(); + const buf = new TimedItemBuffer({ + onFlush, + maxItemAge: 2, + }); + + buf.write(1); + buf.write(2); + expect(onFlush).toHaveBeenCalledTimes(0); + + await new Promise(r => setTimeout(r, 3)); + expect(onFlush).toHaveBeenCalledTimes(1); + expect(onFlush).toHaveBeenCalledWith([1, 2]); + }); + + test('does not call flush after timeout if flush was triggered because buffer size reached', async () => { + const onFlush = jest.fn(); + const buf = new TimedItemBuffer({ + onFlush, + flushOnMaxItems: 2, + maxItemAge: 2, + }); + + buf.write(1); + buf.write(2); + + expect(onFlush).toHaveBeenCalledTimes(1); + await new Promise(r => setTimeout(r, 3)); + expect(onFlush).toHaveBeenCalledTimes(1); + }); + + test('does not automatically flush if `.clear()` was called', async () => { + const onFlush = jest.fn(); + const buf = new TimedItemBuffer({ + onFlush, + flushOnMaxItems: 25, + maxItemAge: 5, + }); + + buf.write(1); + buf.write(2); + await new Promise(r => setImmediate(r)); + buf.clear(); + + expect(onFlush).toHaveBeenCalledTimes(0); + await new Promise(r => setTimeout(r, 6)); + expect(onFlush).toHaveBeenCalledTimes(0); + }); +}); diff --git a/src/plugins/bfetch/common/buffer/timed_item_buffer.ts b/src/plugins/bfetch/common/buffer/timed_item_buffer.ts new file mode 100644 index 0000000000000..8d0f9e4856f8c --- /dev/null +++ b/src/plugins/bfetch/common/buffer/timed_item_buffer.ts @@ -0,0 +1,58 @@ +/* + * 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 { ItemBuffer, ItemBufferParams } from './item_buffer'; + +export interface TimedItemBufferParams extends ItemBufferParams { + /** + * Flushes buffer when oldest item reaches age specified by this parameter, + * in milliseconds. + */ + maxItemAge?: number; +} + +export class TimedItemBuffer extends ItemBuffer { + private timer: any; + + constructor(public readonly params: TimedItemBufferParams) { + super(params); + } + + public write(item: Item) { + super.write(item); + + if (this.params.maxItemAge && this.length === 1) { + this.timer = setTimeout(this.onTimeout, this.params.maxItemAge); + } + } + + public clear() { + clearTimeout(this.timer); + super.clear(); + } + + public flush() { + clearTimeout(this.timer); + super.flush(); + } + + private onTimeout = () => { + this.flush(); + }; +} diff --git a/src/plugins/bfetch/common/index.ts b/src/plugins/bfetch/common/index.ts index afa73ade80084..085b8e7c58a67 100644 --- a/src/plugins/bfetch/common/index.ts +++ b/src/plugins/bfetch/common/index.ts @@ -19,3 +19,5 @@ export * from './util'; export * from './streaming'; +export * from './buffer'; +export * from './batch'; diff --git a/src/plugins/bfetch/common/streaming/types.ts b/src/plugins/bfetch/common/streaming/types.ts index 1ee92edbc89ff..197ee9a52ff01 100644 --- a/src/plugins/bfetch/common/streaming/types.ts +++ b/src/plugins/bfetch/common/streaming/types.ts @@ -20,5 +20,5 @@ import { Observable } from 'rxjs'; export interface StreamingResponseHandler { - onRequest(payload: Payload): Observable; + getResponseStream(payload: Payload): Observable; } diff --git a/src/plugins/bfetch/common/util/index.ts b/src/plugins/bfetch/common/util/index.ts index 02843af9b4350..b5d1fcabbcd85 100644 --- a/src/plugins/bfetch/common/util/index.ts +++ b/src/plugins/bfetch/common/util/index.ts @@ -17,4 +17,5 @@ * under the License. */ +export * from './normalize_error'; export * from './remove_leading_slash'; diff --git a/src/plugins/bfetch/common/util/normalize_error.ts b/src/plugins/bfetch/common/util/normalize_error.ts new file mode 100644 index 0000000000000..c2ee3d83f5eb5 --- /dev/null +++ b/src/plugins/bfetch/common/util/normalize_error.ts @@ -0,0 +1,40 @@ +/* + * 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 { ErrorLike } from '../batch'; + +export const normalizeError = (err: any): E => { + if (!err) { + return { + message: 'Unknown error.', + } as E; + } + if (err instanceof Error) { + return { message: err.message } as E; + } + if (typeof err === 'object') { + return { + ...err, + message: err.message || 'Unknown error.', + } as E; + } + return { + message: String(err), + } as E; +}; diff --git a/src/plugins/bfetch/docs/browser/reference.md b/src/plugins/bfetch/docs/browser/reference.md index 47a67c08a4c1f..444b1aa08a98e 100644 --- a/src/plugins/bfetch/docs/browser/reference.md +++ b/src/plugins/bfetch/docs/browser/reference.md @@ -1,8 +1,37 @@ # `bfetch` browser reference +- [`batchedFunction`](#batchedFunction) - [`fetchStreaming`](#fetchStreaming) +## `batchedFunction` + +Creates a function that will buffer its calls (until timeout—10ms default— or capacity reached—25 default) +and send all calls in one batch to the specified endpoint. The endpoint is expected +to stream results back in ND-JSON format using `Transfer-Encoding: chunked`, which is +implemented by `addBatchProcessingRoute` server-side method of `bfetch` plugin. + +The created function is expected to be called with a single object argument and will +return a promise that will resolve to an object. + +```ts +const fn = bfetch.batchedFunction({ url: '/my-plugin/something' }); + +const result = await fn({ foo: 'bar' }); +``` + +Options: + +- `url` — URL endpoint that will receive a batch of requests. This endpoint is expected + to receive batch as a serialized JSON array. It should stream responses back + in ND-JSON format using `Transfer-Encoding: chunked` HTTP/1 streaming. +- `fetchStreaming` — The instance of `fetchStreaming` function that will perform ND-JSON handling. + There should be a version of this function available in setup contract of `bfetch` plugin. +- `flushOnMaxItems` — The maximum size of function call buffer before sending the batch request. +- `maxItemAge` — The maximum timeout in milliseconds of the oldest item in the batch + before sending the batch request. + + ## `fetchStreaming` Executes an HTTP request and expects that server streams back results using @@ -12,4 +41,4 @@ HTTP/1 `Transfer-Encoding: chunked`. const { stream } = bfetch.fetchStreaming({ url: 'http://elastic.co' }); stream.subscribe(value => {}); -``` \ No newline at end of file +``` diff --git a/src/plugins/bfetch/docs/server/reference.md b/src/plugins/bfetch/docs/server/reference.md new file mode 100644 index 0000000000000..424532a50b817 --- /dev/null +++ b/src/plugins/bfetch/docs/server/reference.md @@ -0,0 +1,54 @@ +# `bfetch` server reference + +- [`addBatchProcessingRoute`](#addBatchProcessingRoute) +- [`addStreamingResponseRoute`](#addStreamingResponseRoute) + + +## `addBatchProcessingRoute` + +Sets up a server endpoint that expects to work with [`batchedFunction`](../browser/reference.md#batchedFunction). +The endpoint receives a batch of requests, processes each request and streams results +back immediately as they become available. You only need to implement the +processing of each request (`onBatchItem` function), everything else is handled. + +`onBatchItem` function is called for each individual request in the batch. +`onBatchItem` function receives a single object argument which is the payload +of one request; and it must return a promise that resolves to an object, too. +`onBatchItem` function is allowed to throw, in that case the error will be forwarded +to the browser only to the individual request, the rest of the batch will still continue +executing. + +```ts +plugins.bfetch.addBatchProcessingRoute( + '/my-plugin/double', + request => ({ + onBatchItem: async (payload) => { + // ... + return {}; + }, + }) +); +``` + +`request` is the `KibanaRequest` object. `addBatchProcessingRoute` together with `batchedFunction` +ensure that errors are handled and that all items in the batch get executed. + + +## `addStreamingResponseRoute` + +`addStreamingResponseRoute` is a lower-level interface that receives and `payload` +message returns and observable which results are streamed back as ND-JSON messages +until the observable completes. `addStreamingResponseRoute` does not know about the +type of the messages, it does not handle errors, and it does not have a concept of +batch size—observable can stream any number of messages until it completes. + +```ts +plugins.bfetch.addStreamingResponseRoute('/my-plugin/foo', request => ({ + getResponseStream: (payload) => { + const subject = new Subject(); + setTimeout(() => { subject.next('123'); }, 100); + setTimeout(() => { subject.complete(); }, 200); + return subject; + }, +})); +``` diff --git a/src/plugins/bfetch/public/batching/create_streaming_batched_function.test.ts b/src/plugins/bfetch/public/batching/create_streaming_batched_function.test.ts new file mode 100644 index 0000000000000..064b791327e69 --- /dev/null +++ b/src/plugins/bfetch/public/batching/create_streaming_batched_function.test.ts @@ -0,0 +1,521 @@ +/* + * 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 { createStreamingBatchedFunction } from './create_streaming_batched_function'; +import { fetchStreaming as fetchStreamingReal } from '../streaming/fetch_streaming'; +import { defer, of } from '../../../kibana_utils/public'; +import { Subject } from 'rxjs'; + +const getPromiseState = (promise: Promise): Promise<'resolved' | 'rejected' | 'pending'> => + Promise.race<'resolved' | 'rejected' | 'pending'>([ + new Promise(resolve => + promise.then( + () => resolve('resolved'), + () => resolve('rejected') + ) + ), + new Promise<'pending'>(resolve => resolve()).then(() => 'pending'), + ]); + +const isPending = (promise: Promise): Promise => + getPromiseState(promise).then(state => state === 'pending'); + +const setup = () => { + const xhr = ({} as unknown) as XMLHttpRequest; + const { promise, resolve, reject } = defer(); + const stream = new Subject(); + + const fetchStreaming = (jest.fn(() => ({ + xhr, + promise, + stream, + })) as unknown) as jest.SpyInstance & typeof fetchStreamingReal; + + return { + fetchStreaming, + xhr, + promise, + resolve, + reject, + stream, + }; +}; + +describe('createStreamingBatchedFunction()', () => { + test('returns a function', () => { + const { fetchStreaming } = setup(); + const fn = createStreamingBatchedFunction({ + url: '/test', + fetchStreaming, + }); + expect(typeof fn).toBe('function'); + }); + + test('returned function is async', () => { + const { fetchStreaming } = setup(); + const fn = createStreamingBatchedFunction({ + url: '/test', + fetchStreaming, + }); + const res = fn({}); + expect(typeof res.then).toBe('function'); + }); + + describe('when timeout is reached', () => { + test('dispatches batch', async () => { + const { fetchStreaming } = setup(); + const fn = createStreamingBatchedFunction({ + url: '/test', + fetchStreaming, + maxItemAge: 5, + flushOnMaxItems: 3, + }); + + expect(fetchStreaming).toHaveBeenCalledTimes(0); + fn({ foo: 'bar' }); + expect(fetchStreaming).toHaveBeenCalledTimes(0); + fn({ baz: 'quix' }); + expect(fetchStreaming).toHaveBeenCalledTimes(0); + + await new Promise(r => setTimeout(r, 6)); + expect(fetchStreaming).toHaveBeenCalledTimes(1); + }); + + test('does nothing is buffer is empty', async () => { + const { fetchStreaming } = setup(); + createStreamingBatchedFunction({ + url: '/test', + fetchStreaming, + maxItemAge: 5, + flushOnMaxItems: 3, + }); + + expect(fetchStreaming).toHaveBeenCalledTimes(0); + await new Promise(r => setTimeout(r, 6)); + expect(fetchStreaming).toHaveBeenCalledTimes(0); + }); + + test('sends POST request to correct endpoint', async () => { + const { fetchStreaming } = setup(); + const fn = createStreamingBatchedFunction({ + url: '/test', + fetchStreaming, + maxItemAge: 5, + flushOnMaxItems: 3, + }); + + fn({ foo: 'bar' }); + await new Promise(r => setTimeout(r, 6)); + + expect(fetchStreaming.mock.calls[0][0]).toMatchObject({ + url: '/test', + method: 'POST', + }); + }); + + test('collects calls into an array batch ordered by in same order as calls', async () => { + const { fetchStreaming } = setup(); + const fn = createStreamingBatchedFunction({ + url: '/test', + fetchStreaming, + maxItemAge: 5, + flushOnMaxItems: 3, + }); + + fn({ foo: 'bar' }); + fn({ baz: 'quix' }); + + await new Promise(r => setTimeout(r, 6)); + const { body } = fetchStreaming.mock.calls[0][0]; + expect(JSON.parse(body)).toEqual({ + batch: [{ foo: 'bar' }, { baz: 'quix' }], + }); + }); + }); + + describe('when buffer becomes full', () => { + test('dispatches batch request', async () => { + const { fetchStreaming } = setup(); + const fn = createStreamingBatchedFunction({ + url: '/test', + fetchStreaming, + maxItemAge: 5, + flushOnMaxItems: 3, + }); + + expect(fetchStreaming).toHaveBeenCalledTimes(0); + fn({ foo: 'bar' }); + expect(fetchStreaming).toHaveBeenCalledTimes(0); + fn({ baz: 'quix' }); + expect(fetchStreaming).toHaveBeenCalledTimes(0); + fn({ full: 'yep' }); + expect(fetchStreaming).toHaveBeenCalledTimes(1); + }); + + test('sends POST request to correct endpoint with items in array batched sorted in call order', async () => { + const { fetchStreaming } = setup(); + const fn = createStreamingBatchedFunction({ + url: '/test', + fetchStreaming, + maxItemAge: 5, + flushOnMaxItems: 3, + }); + + fn({ a: '1' }); + fn({ b: '2' }); + fn({ c: '3' }); + + expect(fetchStreaming.mock.calls[0][0]).toMatchObject({ + url: '/test', + method: 'POST', + }); + const { body } = fetchStreaming.mock.calls[0][0]; + expect(JSON.parse(body)).toEqual({ + batch: [{ a: '1' }, { b: '2' }, { c: '3' }], + }); + }); + + test('dispatches batch on full buffer and also on timeout', async () => { + const { fetchStreaming } = setup(); + const fn = createStreamingBatchedFunction({ + url: '/test', + fetchStreaming, + maxItemAge: 5, + flushOnMaxItems: 3, + }); + + fn({ a: '1' }); + fn({ b: '2' }); + fn({ c: '3' }); + expect(fetchStreaming).toHaveBeenCalledTimes(1); + fn({ d: '4' }); + await new Promise(r => setTimeout(r, 6)); + expect(fetchStreaming).toHaveBeenCalledTimes(2); + }); + }); + + describe('when receiving results', () => { + test('does not resolve call promises until request finishes', async () => { + const { fetchStreaming } = setup(); + const fn = createStreamingBatchedFunction({ + url: '/test', + fetchStreaming, + maxItemAge: 5, + flushOnMaxItems: 3, + }); + + const promise1 = fn({ a: '1' }); + const promise2 = fn({ b: '2' }); + await new Promise(r => setTimeout(r, 6)); + + expect(await isPending(promise1)).toBe(true); + expect(await isPending(promise2)).toBe(true); + }); + + test('resolves only promise of result that was streamed back', async () => { + const { fetchStreaming, stream } = setup(); + const fn = createStreamingBatchedFunction({ + url: '/test', + fetchStreaming, + maxItemAge: 5, + flushOnMaxItems: 3, + }); + + const promise1 = fn({ a: '1' }); + const promise2 = fn({ b: '2' }); + const promise3 = fn({ c: '3' }); + await new Promise(r => setTimeout(r, 6)); + + expect(await isPending(promise1)).toBe(true); + expect(await isPending(promise2)).toBe(true); + expect(await isPending(promise3)).toBe(true); + + stream.next( + JSON.stringify({ + id: 1, + result: { foo: 'bar' }, + }) + '\n' + ); + + expect(await isPending(promise1)).toBe(true); + expect(await isPending(promise2)).toBe(false); + expect(await isPending(promise3)).toBe(true); + + stream.next( + JSON.stringify({ + id: 0, + result: { foo: 'bar 2' }, + }) + '\n' + ); + + expect(await isPending(promise1)).toBe(false); + expect(await isPending(promise2)).toBe(false); + expect(await isPending(promise3)).toBe(true); + }); + + test('resolves each promise with correct data', async () => { + const { fetchStreaming, stream } = setup(); + const fn = createStreamingBatchedFunction({ + url: '/test', + fetchStreaming, + maxItemAge: 5, + flushOnMaxItems: 3, + }); + + const promise1 = fn({ a: '1' }); + const promise2 = fn({ b: '2' }); + const promise3 = fn({ c: '3' }); + await new Promise(r => setTimeout(r, 6)); + + stream.next( + JSON.stringify({ + id: 1, + result: { foo: 'bar' }, + }) + '\n' + ); + stream.next( + JSON.stringify({ + id: 2, + result: { foo: 'bar 2' }, + }) + '\n' + ); + + expect(await isPending(promise1)).toBe(true); + expect(await isPending(promise2)).toBe(false); + expect(await isPending(promise3)).toBe(false); + expect(await promise2).toEqual({ foo: 'bar' }); + expect(await promise3).toEqual({ foo: 'bar 2' }); + }); + + test('rejects promise on error response', async () => { + const { fetchStreaming, stream } = setup(); + const fn = createStreamingBatchedFunction({ + url: '/test', + fetchStreaming, + maxItemAge: 5, + flushOnMaxItems: 3, + }); + + const promise = fn({ a: '1' }); + await new Promise(r => setTimeout(r, 6)); + + expect(await isPending(promise)).toBe(true); + + stream.next( + JSON.stringify({ + id: 0, + error: { message: 'oops' }, + }) + '\n' + ); + + expect(await isPending(promise)).toBe(false); + const [, error] = await of(promise); + expect(error).toEqual({ + message: 'oops', + }); + }); + + test('resolves successful requests even after rejected ones', async () => { + const { fetchStreaming, stream } = setup(); + const fn = createStreamingBatchedFunction({ + url: '/test', + fetchStreaming, + maxItemAge: 5, + flushOnMaxItems: 3, + }); + + const promise1 = of(fn({ a: '1' })); + const promise2 = of(fn({ a: '2' })); + const promise3 = of(fn({ a: '3' })); + + await new Promise(r => setTimeout(r, 6)); + + stream.next( + JSON.stringify({ + id: 2, + result: { b: '3' }, + }) + '\n' + ); + + await new Promise(r => setTimeout(r, 1)); + + stream.next( + JSON.stringify({ + id: 1, + error: { b: '2' }, + }) + '\n' + ); + + await new Promise(r => setTimeout(r, 1)); + + stream.next( + JSON.stringify({ + id: 0, + result: { b: '1' }, + }) + '\n' + ); + + await new Promise(r => setTimeout(r, 1)); + + const [result1] = await promise1; + const [, error2] = await promise2; + const [result3] = await promise3; + + expect(result1).toEqual({ b: '1' }); + expect(error2).toEqual({ b: '2' }); + expect(result3).toEqual({ b: '3' }); + }); + + describe('when stream closes prematurely', () => { + test('rejects pending promises with CONNECTION error code', async () => { + const { fetchStreaming, stream } = setup(); + const fn = createStreamingBatchedFunction({ + url: '/test', + fetchStreaming, + maxItemAge: 5, + flushOnMaxItems: 3, + }); + + const promise1 = of(fn({ a: '1' })); + const promise2 = of(fn({ a: '2' })); + + await new Promise(r => setTimeout(r, 6)); + + stream.complete(); + + await new Promise(r => setTimeout(r, 1)); + + const [, error1] = await promise1; + const [, error2] = await promise2; + expect(error1).toMatchObject({ + message: 'Connection terminated prematurely.', + code: 'CONNECTION', + }); + expect(error2).toMatchObject({ + message: 'Connection terminated prematurely.', + code: 'CONNECTION', + }); + }); + + test('rejects with CONNECTION error only pending promises', async () => { + const { fetchStreaming, stream } = setup(); + const fn = createStreamingBatchedFunction({ + url: '/test', + fetchStreaming, + maxItemAge: 5, + flushOnMaxItems: 3, + }); + + const promise1 = of(fn({ a: '1' })); + const promise2 = of(fn({ a: '2' })); + + await new Promise(r => setTimeout(r, 6)); + + stream.next( + JSON.stringify({ + id: 1, + result: { b: '1' }, + }) + '\n' + ); + stream.complete(); + + await new Promise(r => setTimeout(r, 1)); + + const [, error1] = await promise1; + const [result1] = await promise2; + expect(error1).toMatchObject({ + message: 'Connection terminated prematurely.', + code: 'CONNECTION', + }); + expect(result1).toMatchObject({ + b: '1', + }); + }); + }); + + describe('when stream errors', () => { + test('rejects pending promises with STREAM error code', async () => { + const { fetchStreaming, stream } = setup(); + const fn = createStreamingBatchedFunction({ + url: '/test', + fetchStreaming, + maxItemAge: 5, + flushOnMaxItems: 3, + }); + + const promise1 = of(fn({ a: '1' })); + const promise2 = of(fn({ a: '2' })); + + await new Promise(r => setTimeout(r, 6)); + + stream.error({ + message: 'something went wrong', + }); + + await new Promise(r => setTimeout(r, 1)); + + const [, error1] = await promise1; + const [, error2] = await promise2; + expect(error1).toMatchObject({ + message: 'something went wrong', + code: 'STREAM', + }); + expect(error2).toMatchObject({ + message: 'something went wrong', + code: 'STREAM', + }); + }); + + test('rejects with STREAM error only pending promises', async () => { + const { fetchStreaming, stream } = setup(); + const fn = createStreamingBatchedFunction({ + url: '/test', + fetchStreaming, + maxItemAge: 5, + flushOnMaxItems: 3, + }); + + const promise1 = of(fn({ a: '1' })); + const promise2 = of(fn({ a: '2' })); + + await new Promise(r => setTimeout(r, 6)); + + stream.next( + JSON.stringify({ + id: 1, + result: { b: '1' }, + }) + '\n' + ); + stream.error('oops'); + + await new Promise(r => setTimeout(r, 1)); + + const [, error1] = await promise1; + const [result1] = await promise2; + expect(error1).toMatchObject({ + message: 'oops', + code: 'STREAM', + }); + expect(result1).toMatchObject({ + b: '1', + }); + }); + }); + }); +}); diff --git a/src/plugins/bfetch/public/batching/create_streaming_batched_function.ts b/src/plugins/bfetch/public/batching/create_streaming_batched_function.ts new file mode 100644 index 0000000000000..07d5724a2520d --- /dev/null +++ b/src/plugins/bfetch/public/batching/create_streaming_batched_function.ts @@ -0,0 +1,140 @@ +/* + * 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 { defer, Defer } from '../../../kibana_utils/public'; +import { + ItemBufferParams, + TimedItemBufferParams, + createBatchedFunction, + BatchResponseItem, + ErrorLike, +} from '../../common'; +import { fetchStreaming, split } from '../streaming'; +import { normalizeError } from '../../common'; + +export interface BatchItem { + payload: Payload; + future: Defer; +} + +export type BatchedFunc = (payload: Payload) => Promise; + +export interface BatchedFunctionProtocolError extends ErrorLike { + code: string; +} + +export interface StreamingBatchedFunctionParams { + /** + * URL endpoint that will receive a batch of requests. This endpoint is expected + * to receive batch as a serialized JSON array. It should stream responses back + * in ND-JSON format using `Transfer-Encoding: chunked` HTTP/1 streaming. + */ + url: string; + + /** + * The instance of `fetchStreaming` function that will perform ND-JSON handling. + * There should be a version of this function available in setup contract of `bfetch` + * plugin. + */ + fetchStreaming?: typeof fetchStreaming; + + /** + * The maximum size of function call buffer before sending the batch request. + */ + flushOnMaxItems?: ItemBufferParams['flushOnMaxItems']; + + /** + * The maximum timeout in milliseconds of the oldest item in the batch + * before sending the batch request. + */ + maxItemAge?: TimedItemBufferParams['maxItemAge']; +} + +/** + * Returns a function that does not execute immediately but buffers the call internally until + * `params.flushOnMaxItems` is reached or after `params.maxItemAge` timeout in milliseconds is reached. Once + * one of those thresholds is reached all buffered calls are sent in one batch to the + * server using `params.fetchStreaming` in a POST request. Responses are streamed back + * and each batch item is resolved once corresponding response is received. + */ +export const createStreamingBatchedFunction = ( + params: StreamingBatchedFunctionParams +): BatchedFunc => { + const { + url, + fetchStreaming: fetchStreamingInjected = fetchStreaming, + flushOnMaxItems = 25, + maxItemAge = 10, + } = params; + const [fn] = createBatchedFunction, BatchItem>({ + onCall: (payload: Payload) => { + const future = defer(); + const entry: BatchItem = { + payload, + future, + }; + return [future.promise, entry]; + }, + onBatch: async items => { + try { + let responsesReceived = 0; + const batch = items.map(({ payload }) => payload); + const { stream } = fetchStreamingInjected({ + url, + body: JSON.stringify({ batch }), + method: 'POST', + }); + stream.pipe(split('\n')).subscribe({ + next: (json: string) => { + const response = JSON.parse(json) as BatchResponseItem; + if (response.error) { + responsesReceived++; + items[response.id].future.reject(response.error); + } else if (response.result) { + responsesReceived++; + items[response.id].future.resolve(response.result); + } + }, + error: error => { + const normalizedError = normalizeError(error); + normalizedError.code = 'STREAM'; + for (const { future } of items) future.reject(normalizedError); + }, + complete: () => { + const streamTerminatedPrematurely = responsesReceived !== items.length; + if (streamTerminatedPrematurely) { + const error: BatchedFunctionProtocolError = { + message: 'Connection terminated prematurely.', + code: 'CONNECTION', + }; + for (const { future } of items) future.reject(error); + } + }, + }); + await stream.toPromise(); + } catch (error) { + for (const item of items) item.future.reject(error); + } + }, + flushOnMaxItems, + maxItemAge, + }); + + return fn; +}; diff --git a/src/plugins/bfetch/public/index.ts b/src/plugins/bfetch/public/index.ts index a57dd77fe7e67..8707e5a438159 100644 --- a/src/plugins/bfetch/public/index.ts +++ b/src/plugins/bfetch/public/index.ts @@ -20,7 +20,8 @@ import { PluginInitializerContext } from '../../../core/public'; import { BfetchPublicPlugin } from './plugin'; -export { BfetchPublicSetup, BfetchPublicStart, BfetchPublicApi } from './plugin'; +export { BfetchPublicSetup, BfetchPublicStart, BfetchPublicContract } from './plugin'; +export { split } from './streaming'; export function plugin(initializerContext: PluginInitializerContext) { return new BfetchPublicPlugin(initializerContext); diff --git a/src/plugins/bfetch/public/mocks.ts b/src/plugins/bfetch/public/mocks.ts index e8caf5c9cb739..f457b9ae5d671 100644 --- a/src/plugins/bfetch/public/mocks.ts +++ b/src/plugins/bfetch/public/mocks.ts @@ -27,6 +27,7 @@ export type Start = jest.Mocked; const createSetupContract = (): Setup => { const setupContract: Setup = { fetchStreaming: jest.fn(), + batchedFunction: jest.fn(), }; return setupContract; }; @@ -34,6 +35,7 @@ const createSetupContract = (): Setup => { const createStartContract = (): Start => { const startContract: Start = { fetchStreaming: jest.fn(), + batchedFunction: jest.fn(), }; return startContract; @@ -56,7 +58,7 @@ const createPlugin = async () => { }; }; -export const uiActionsPluginMock = { +export const bfetchPluginMock = { createSetupContract, createStartContract, createPlugin, diff --git a/src/plugins/bfetch/public/plugin.ts b/src/plugins/bfetch/public/plugin.ts index db18a15afa1e7..783c448c567e5 100644 --- a/src/plugins/bfetch/public/plugin.ts +++ b/src/plugins/bfetch/public/plugin.ts @@ -20,6 +20,11 @@ import { CoreStart, PluginInitializerContext, CoreSetup, Plugin } from 'src/core/public'; import { fetchStreaming as fetchStreamingStatic, FetchStreamingParams } from './streaming'; import { removeLeadingSlash } from '../common'; +import { + createStreamingBatchedFunction, + BatchedFunc, + StreamingBatchedFunctionParams, +} from './batching/create_streaming_batched_function'; // eslint-disable-next-line export interface BfetchPublicSetupDependencies {} @@ -27,12 +32,15 @@ export interface BfetchPublicSetupDependencies {} // eslint-disable-next-line export interface BfetchPublicStartDependencies {} -export interface BfetchPublicApi { +export interface BfetchPublicContract { fetchStreaming: (params: FetchStreamingParams) => ReturnType; + batchedFunction: ( + params: StreamingBatchedFunctionParams + ) => BatchedFunc; } -export type BfetchPublicSetup = BfetchPublicApi; -export type BfetchPublicStart = BfetchPublicApi; +export type BfetchPublicSetup = BfetchPublicContract; +export type BfetchPublicStart = BfetchPublicContract; export class BfetchPublicPlugin implements @@ -42,7 +50,7 @@ export class BfetchPublicPlugin BfetchPublicSetupDependencies, BfetchPublicStartDependencies > { - private api!: BfetchPublicApi; + private contract!: BfetchPublicContract; constructor(private readonly initializerContext: PluginInitializerContext) {} @@ -51,16 +59,18 @@ export class BfetchPublicPlugin const basePath = core.http.basePath.get(); const fetchStreaming = this.fetchStreaming(version, basePath); + const batchedFunction = this.batchedFunction(fetchStreaming); - this.api = { + this.contract = { fetchStreaming, + batchedFunction, }; - return this.api; + return this.contract; } public start(core: CoreStart, plugins: BfetchPublicStartDependencies): BfetchPublicStart { - return this.api; + return this.contract; } public stop() {} @@ -78,4 +88,12 @@ export class BfetchPublicPlugin ...(params.headers || {}), }, }); + + private batchedFunction = ( + fetchStreaming: BfetchPublicContract['fetchStreaming'] + ): BfetchPublicContract['batchedFunction'] => params => + createStreamingBatchedFunction({ + ...params, + fetchStreaming: params.fetchStreaming || fetchStreaming, + }); } diff --git a/src/plugins/bfetch/public/streaming/fetch_streaming.test.ts b/src/plugins/bfetch/public/streaming/fetch_streaming.test.ts index e59af71cb76bc..7845616026ea1 100644 --- a/src/plugins/bfetch/public/streaming/fetch_streaming.test.ts +++ b/src/plugins/bfetch/public/streaming/fetch_streaming.test.ts @@ -36,14 +36,6 @@ test('returns XHR request', () => { expect(typeof xhr.readyState).toBe('number'); }); -test('returns promise', () => { - setup(); - const { promise } = fetchStreaming({ - url: 'http://example.com', - }); - expect(typeof promise.then).toBe('function'); -}); - test('returns stream', () => { setup(); const { stream } = fetchStreaming({ @@ -54,12 +46,12 @@ test('returns stream', () => { test('promise resolves when request completes', async () => { const env = setup(); - const { promise } = fetchStreaming({ + const { stream } = fetchStreaming({ url: 'http://example.com', }); let resolved = false; - promise.then(() => (resolved = true)); + stream.toPromise().then(() => (resolved = true)); await tick(); expect(resolved).toBe(false); @@ -142,12 +134,12 @@ test('completes stream observable when request finishes', async () => { test('promise throws when request errors', async () => { const env = setup(); - const { promise } = fetchStreaming({ + const { stream } = fetchStreaming({ url: 'http://example.com', }); const spy = jest.fn(); - promise.catch(spy); + stream.toPromise().catch(spy); await tick(); expect(spy).toHaveBeenCalledTimes(0); @@ -168,12 +160,11 @@ test('promise throws when request errors', async () => { test('stream observable errors when request errors', async () => { const env = setup(); - const { promise, stream } = fetchStreaming({ + const { stream } = fetchStreaming({ url: 'http://example.com', }); const spy = jest.fn(); - promise.catch(() => {}); stream.subscribe({ error: spy, }); diff --git a/src/plugins/bfetch/public/streaming/fetch_streaming.ts b/src/plugins/bfetch/public/streaming/fetch_streaming.ts index 44a3693e7010b..899e8a1824a41 100644 --- a/src/plugins/bfetch/public/streaming/fetch_streaming.ts +++ b/src/plugins/bfetch/public/streaming/fetch_streaming.ts @@ -17,7 +17,6 @@ * under the License. */ -import { defer } from '../../../kibana_utils/common'; import { fromStreamingXhr } from './from_streaming_xhr'; export interface FetchStreamingParams { @@ -38,7 +37,6 @@ export function fetchStreaming({ body = '', }: FetchStreamingParams) { const xhr = new window.XMLHttpRequest(); - const { promise, resolve, reject } = defer(); // Begin the request xhr.open(method, url); @@ -49,17 +47,11 @@ export function fetchStreaming({ const stream = fromStreamingXhr(xhr); - stream.subscribe({ - complete: () => resolve(), - error: error => reject(error), - }); - // Send the payload to the server xhr.send(body); return { xhr, - promise, stream, }; } diff --git a/src/plugins/bfetch/server/index.ts b/src/plugins/bfetch/server/index.ts index f1a3f7fd44cf6..06b7c793c537e 100644 --- a/src/plugins/bfetch/server/index.ts +++ b/src/plugins/bfetch/server/index.ts @@ -20,7 +20,7 @@ import { PluginInitializerContext } from '../../../core/server'; import { BfetchServerPlugin } from './plugin'; -export { BfetchServerSetup, BfetchServerStart } from './plugin'; +export { BfetchServerSetup, BfetchServerStart, BatchProcessingRouteParams } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { return new BfetchServerPlugin(initializerContext); diff --git a/src/plugins/bfetch/server/mocks.ts b/src/plugins/bfetch/server/mocks.ts index 8ec68650a60dc..e0a76ba8da325 100644 --- a/src/plugins/bfetch/server/mocks.ts +++ b/src/plugins/bfetch/server/mocks.ts @@ -26,6 +26,7 @@ export type Start = jest.Mocked; const createSetupContract = (): Setup => { const setupContract: Setup = { + addBatchProcessingRoute: jest.fn(), addStreamingResponseRoute: jest.fn(), }; return setupContract; @@ -54,7 +55,7 @@ const createPlugin = async () => { }; }; -export const uiActionsPluginMock = { +export const bfetchPluginMock = { createSetupContract, createStartContract, createPlugin, diff --git a/src/plugins/bfetch/server/plugin.ts b/src/plugins/bfetch/server/plugin.ts index 75baeafc17669..fd1fe009e93ae 100644 --- a/src/plugins/bfetch/server/plugin.ts +++ b/src/plugins/bfetch/server/plugin.ts @@ -17,9 +17,24 @@ * under the License. */ -import { CoreStart, PluginInitializerContext, CoreSetup, Plugin, Logger } from 'src/core/server'; +import { + CoreStart, + PluginInitializerContext, + CoreSetup, + Plugin, + Logger, + KibanaRequest, +} from 'src/core/server'; import { schema } from '@kbn/config-schema'; -import { StreamingResponseHandler, removeLeadingSlash } from '../common'; +import { Subject } from 'rxjs'; +import { + StreamingResponseHandler, + BatchRequestData, + BatchResponseItem, + ErrorLike, + removeLeadingSlash, + normalizeError, +} from '../common'; import { createNDJSONStream } from './streaming'; // eslint-disable-next-line @@ -28,8 +43,19 @@ export interface BfetchServerSetupDependencies {} // eslint-disable-next-line export interface BfetchServerStartDependencies {} +export interface BatchProcessingRouteParams { + onBatchItem: (data: BatchItemData) => Promise; +} + export interface BfetchServerSetup { - addStreamingResponseRoute: (path: string, handler: StreamingResponseHandler) => void; + addBatchProcessingRoute: ( + path: string, + handler: (request: KibanaRequest) => BatchProcessingRouteParams + ) => void; + addStreamingResponseRoute: ( + path: string, + params: (request: KibanaRequest) => StreamingResponseHandler + ) => void; } // eslint-disable-next-line @@ -49,8 +75,10 @@ export class BfetchServerPlugin const logger = this.initializerContext.logger.get(); const router = core.http.createRouter(); const addStreamingResponseRoute = this.addStreamingResponseRoute({ router, logger }); + const addBatchProcessingRoute = this.addBatchProcessingRoute(addStreamingResponseRoute); return { + addBatchProcessingRoute, addStreamingResponseRoute, }; } @@ -76,17 +104,56 @@ export class BfetchServerPlugin }, }, async (context, request, response) => { + const handlerInstance = handler(request); const data = request.body; + const headers = { + 'Content-Type': 'application/x-ndjson', + Connection: 'keep-alive', + 'Transfer-Encoding': 'chunked', + 'Cache-Control': 'no-cache', + }; return response.ok({ - headers: { - 'Content-Type': 'application/x-ndjson', - Connection: 'keep-alive', - 'Transfer-Encoding': 'chunked', - 'Cache-Control': 'no-cache', - }, - body: createNDJSONStream(data, handler, logger), + headers, + body: createNDJSONStream(data, handlerInstance, logger), }); } ); }; + + private addBatchProcessingRoute = ( + addStreamingResponseRoute: BfetchServerSetup['addStreamingResponseRoute'] + ): BfetchServerSetup['addBatchProcessingRoute'] => < + BatchItemData extends object, + BatchItemResult extends object, + E extends ErrorLike = ErrorLike + >( + path: string, + handler: (request: KibanaRequest) => BatchProcessingRouteParams + ) => { + addStreamingResponseRoute< + BatchRequestData, + BatchResponseItem + >(path, request => { + const handlerInstance = handler(request); + return { + getResponseStream: ({ batch }) => { + const subject = new Subject>(); + let cnt = batch.length; + batch.forEach(async (batchItem, id) => { + try { + const result = await handlerInstance.onBatchItem(batchItem); + subject.next({ id, result }); + } catch (err) { + const error = normalizeError(err); + subject.next({ id, error }); + } finally { + cnt--; + if (!cnt) subject.complete(); + } + }); + return subject; + }, + }; + }); + }; } diff --git a/src/plugins/bfetch/server/streaming/create_ndjson_stream.ts b/src/plugins/bfetch/server/streaming/create_ndjson_stream.ts index b1f39f4acbcb5..82fe31906e8bf 100644 --- a/src/plugins/bfetch/server/streaming/create_ndjson_stream.ts +++ b/src/plugins/bfetch/server/streaming/create_ndjson_stream.ts @@ -29,7 +29,7 @@ export const createNDJSONStream = ( logger: Logger ): Stream => { const stream = new PassThrough(); - const results = handler.onRequest(payload); + const results = handler.getResponseStream(payload); results.subscribe({ next: (message: Response) => { diff --git a/src/plugins/expressions/common/type.ts b/src/plugins/expressions/common/type.ts index de9c43d01a0aa..c9daed9b6785a 100644 --- a/src/plugins/expressions/common/type.ts +++ b/src/plugins/expressions/common/type.ts @@ -30,11 +30,6 @@ export function getType(node: any) { } export function serializeProvider(types: any) { - return { - serialize: provider('serialize'), - deserialize: provider('deserialize'), - }; - function provider(key: any) { return (context: any) => { const type = getType(context); @@ -43,6 +38,11 @@ export function serializeProvider(types: any) { return fn(context); }; } + + return { + serialize: provider('serialize'), + deserialize: provider('deserialize'), + }; } export class Type { diff --git a/src/plugins/expressions/kibana.json b/src/plugins/expressions/kibana.json index ec87b56f3745e..cba693dd4bc20 100644 --- a/src/plugins/expressions/kibana.json +++ b/src/plugins/expressions/kibana.json @@ -1,9 +1,10 @@ { "id": "expressions", "version": "kibana", - "server": false, + "server": true, "ui": true, "requiredPlugins": [ + "bfetch", "inspector" ] } diff --git a/src/legacy/core_plugins/interpreter/public/canvas/batched_fetch.test.ts b/src/plugins/expressions/public/batched_fetch.test.ts similarity index 97% rename from src/legacy/core_plugins/interpreter/public/canvas/batched_fetch.test.ts rename to src/plugins/expressions/public/batched_fetch.test.ts index 3da15cf54cda0..7273be872a725 100644 --- a/src/legacy/core_plugins/interpreter/public/canvas/batched_fetch.test.ts +++ b/src/plugins/expressions/public/batched_fetch.test.ts @@ -18,7 +18,7 @@ */ import { batchedFetch, Request } from './batched_fetch'; -import { defer } from '../../../../../plugins/kibana_utils/public'; +import { defer } from '../../kibana_utils/public'; import { Subject } from 'rxjs'; const serialize = (o: any) => JSON.stringify(o); diff --git a/src/legacy/core_plugins/interpreter/public/canvas/batched_fetch.ts b/src/plugins/expressions/public/batched_fetch.ts similarity index 87% rename from src/legacy/core_plugins/interpreter/public/canvas/batched_fetch.ts rename to src/plugins/expressions/public/batched_fetch.ts index 717a87fc90f9f..6a155b7d42b72 100644 --- a/src/legacy/core_plugins/interpreter/public/canvas/batched_fetch.ts +++ b/src/plugins/expressions/public/batched_fetch.ts @@ -20,13 +20,11 @@ import _ from 'lodash'; import { filter, map } from 'rxjs/operators'; // eslint-disable-next-line -import { split } from '../../../../../plugins/bfetch/public/streaming'; -import { BfetchPublicApi } from '../../../../../plugins/bfetch/public'; -import { defer } from '../../../../../plugins/kibana_utils/public'; -import { FUNCTIONS_URL } from './consts'; +import { split, BfetchPublicContract } from '../../bfetch/public'; +import { defer } from '../../kibana_utils/public'; export interface Options { - fetchStreaming: BfetchPublicApi['fetchStreaming']; + fetchStreaming: BfetchPublicContract['fetchStreaming']; serialize: any; ms?: number; } @@ -111,9 +109,9 @@ export function batchedFetch({ fetchStreaming, serialize, ms = 10 }: Options) { * Runs the specified batch of functions on the server, then resolves * the related promises. */ -async function processBatch(fetchStreaming: BfetchPublicApi['fetchStreaming'], batch: Batch) { - const { stream, promise } = fetchStreaming({ - url: FUNCTIONS_URL, +async function processBatch(fetchStreaming: BfetchPublicContract['fetchStreaming'], batch: Batch) { + const { stream } = fetchStreaming({ + url: `/api/interpreter/fns`, body: JSON.stringify({ functions: Object.values(batch).map(({ request }) => request), }), @@ -137,7 +135,7 @@ async function processBatch(fetchStreaming: BfetchPublicApi['fetchStreaming'], b }); try { - await promise; + await stream.toPromise(); } catch (error) { Object.values(batch).forEach(({ future }) => { future.reject(error); diff --git a/src/plugins/expressions/public/mocks.tsx b/src/plugins/expressions/public/mocks.tsx index 089c324677712..a3476a24dd7ed 100644 --- a/src/plugins/expressions/public/mocks.tsx +++ b/src/plugins/expressions/public/mocks.tsx @@ -23,6 +23,7 @@ import { ExpressionsSetup, ExpressionsStart, plugin as pluginInitializer } from /* eslint-disable */ import { coreMock } from '../../../core/public/mocks'; import { inspectorPluginMock } from '../../inspector/public/mocks'; +import { bfetchPluginMock } from '../../bfetch/public/mocks'; /* eslint-enable */ export type Setup = jest.Mocked; @@ -48,6 +49,7 @@ const createSetupContract = (): Setup => { interpretAst: () => {}, }, }), + loadLegacyServerFunctionWrappers: () => Promise.resolve(), }, }; return setupContract; @@ -71,6 +73,7 @@ const createPlugin = async () => { const coreStart = coreMock.createStart(); const plugin = pluginInitializer(pluginInitializerContext); const setup = await plugin.setup(coreSetup, { + bfetch: bfetchPluginMock.createSetupContract(), inspector: inspectorPluginMock.createSetupContract(), }); @@ -82,6 +85,7 @@ const createPlugin = async () => { setup, doStart: async () => await plugin.start(coreStart, { + bfetch: bfetchPluginMock.createStartContract(), inspector: inspectorPluginMock.createStartContract(), }), }; diff --git a/src/plugins/expressions/public/plugin.ts b/src/plugins/expressions/public/plugin.ts index 11f804464704e..2ba10be76cd92 100644 --- a/src/plugins/expressions/public/plugin.ts +++ b/src/plugins/expressions/public/plugin.ts @@ -20,6 +20,7 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; import { ExpressionInterpretWithHandlers, ExpressionExecutor } from './types'; import { FunctionsRegistry, RenderFunctionsRegistry, TypesRegistry } from './registries'; +import { BfetchPublicSetup, BfetchPublicStart } from '../../bfetch/public'; import { Setup as InspectorSetup, Start as InspectorStart } from '../../inspector/public'; import { setCoreStart, @@ -58,12 +59,15 @@ import { ExpressionLoader, loader } from './loader'; import { ExpressionDataHandler, execute } from './execute'; import { render, ExpressionRenderHandler } from './render'; import { AnyExpressionFunction, AnyExpressionType } from '../common/types'; +import { serializeProvider } from '../common'; export interface ExpressionsSetupDeps { + bfetch: BfetchPublicSetup; inspector: InspectorSetup; } export interface ExpressionsStartDeps { + bfetch: BfetchPublicStart; inspector: InspectorStart; } @@ -76,6 +80,7 @@ export interface ExpressionsSetup { renderers: RenderFunctionsRegistry; types: TypesRegistry; getExecutor: () => ExpressionExecutor; + loadLegacyServerFunctionWrappers: () => Promise; }; } @@ -98,7 +103,7 @@ export class ExpressionsPublicPlugin constructor(initializerContext: PluginInitializerContext) {} - public setup(core: CoreSetup, { inspector }: ExpressionsSetupDeps): ExpressionsSetup { + public setup(core: CoreSetup, { inspector, bfetch }: ExpressionsSetupDeps): ExpressionsSetup { const { functions, renderers, types } = this; setRenderersRegistry(renderers); @@ -146,6 +151,31 @@ export class ExpressionsPublicPlugin setInterpreter(getExecutor().interpreter); + let cached: Promise | null = null; + const loadLegacyServerFunctionWrappers = async () => { + if (!cached) { + cached = (async () => { + const serverFunctionList = await core.http.get(`/api/interpreter/fns`); + const batchedFunction = bfetch.batchedFunction({ url: `/api/interpreter/fns` }); + const { serialize } = serializeProvider(types.toJS()); + + // For every sever-side function, register a client-side + // function that matches its definition, but which simply + // calls the server-side function endpoint. + Object.keys(serverFunctionList).forEach(functionName => { + const fn = () => ({ + ...serverFunctionList[functionName], + fn: (context: any, args: any) => { + return batchedFunction({ functionName, args, context: serialize(context) }); + }, + }); + registerFunction(fn); + }); + })(); + } + return cached; + }; + const setup: ExpressionsSetup = { registerFunction, registerRenderer: (renderer: any) => { @@ -159,6 +189,7 @@ export class ExpressionsPublicPlugin renderers, types, getExecutor, + loadLegacyServerFunctionWrappers, }, }; diff --git a/src/plugins/expressions/server/index.ts b/src/plugins/expressions/server/index.ts new file mode 100644 index 0000000000000..6718602ccdef5 --- /dev/null +++ b/src/plugins/expressions/server/index.ts @@ -0,0 +1,27 @@ +/* + * 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 { PluginInitializerContext } from '../../../core/server'; +import { ExpressionsServerPlugin } from './plugin'; + +export { ExpressionsServerSetup, ExpressionsServerStart } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new ExpressionsServerPlugin(initializerContext); +} diff --git a/src/plugins/expressions/server/legacy.ts b/src/plugins/expressions/server/legacy.ts new file mode 100644 index 0000000000000..54e2a5a387342 --- /dev/null +++ b/src/plugins/expressions/server/legacy.ts @@ -0,0 +1,135 @@ +/* + * 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. + */ + +/* eslint-disable max-classes-per-file */ + +// TODO: Remove this file once https://github.com/elastic/kibana/issues/46906 is complete. + +// @ts-ignore +import { register, registryFactory, Registry, Fn } from '@kbn/interpreter/common'; + +import Boom from 'boom'; +import { schema } from '@kbn/config-schema'; +import { CoreSetup, Logger } from 'src/core/server'; +import { ExpressionsServerSetupDependencies } from './plugin'; +import { typeSpecs as types, Type } from '../common'; +import { serializeProvider } from '../common'; + +export class TypesRegistry extends Registry { + wrapper(obj: any) { + return new (Type as any)(obj); + } +} + +export class FunctionsRegistry extends Registry { + wrapper(obj: any) { + return new Fn(obj); + } +} + +export const registries = { + types: new TypesRegistry(), + serverFunctions: new FunctionsRegistry(), +}; + +export interface LegacyInterpreterServerApi { + registries(): typeof registries; + register(specs: Record): typeof registries; +} + +export const createLegacyServerInterpreterApi = (): LegacyInterpreterServerApi => { + const api = registryFactory(registries); + + register(registries, { + types, + }); + + return api; +}; + +export const createLegacyServerEndpoints = ( + api: LegacyInterpreterServerApi, + logger: Logger, + core: CoreSetup, + plugins: ExpressionsServerSetupDependencies +) => { + const router = core.http.createRouter(); + + /** + * Register the endpoint that returns the list of server-only functions. + */ + router.get( + { + path: `/api/interpreter/fns`, + validate: { + body: schema.any(), + }, + }, + async (context, request, response) => { + const functions = api.registries().serverFunctions.toJS(); + const body = JSON.stringify(functions); + return response.ok({ + body, + }); + } + ); + + /** + * Run a single Canvas function. + * + * @param {*} server - The Kibana server object + * @param {*} handlers - The Canvas handlers + * @param {*} fnCall - Describes the function being run `{ functionName, args, context }` + */ + async function runFunction(handlers: any, fnCall: any) { + const { functionName, args, context } = fnCall; + const { deserialize } = serializeProvider(registries.types.toJS()); + const fnDef = registries.serverFunctions.toJS()[functionName]; + if (!fnDef) throw Boom.notFound(`Function "${functionName}" could not be found.`); + const deserialized = deserialize(context); + const result = fnDef.fn(deserialized, args, handlers); + return result; + } + + /** + * Register an endpoint that executes a batch of functions, and streams the + * results back using ND-JSON. + */ + plugins.bfetch.addBatchProcessingRoute(`/api/interpreter/fns`, request => { + const scopedClient = core.elasticsearch.dataClient.asScoped(request); + const handlers = { + environment: 'server', + elasticsearchClient: async ( + endpoint: string, + clientParams: Record = {}, + options?: any + ) => scopedClient.callAsCurrentUser(endpoint, clientParams, options), + }; + + return { + onBatchItem: async (fnCall: any) => { + const result = await runFunction(handlers, fnCall); + if (typeof result === 'undefined') { + throw new Error(`Function ${fnCall.functionName} did not return anything.`); + } + return result; + }, + }; + }); +}; diff --git a/src/plugins/expressions/server/mocks.ts b/src/plugins/expressions/server/mocks.ts new file mode 100644 index 0000000000000..4510ae6dc0b4a --- /dev/null +++ b/src/plugins/expressions/server/mocks.ts @@ -0,0 +1,73 @@ +/* + * 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 { ExpressionsServerSetup, ExpressionsServerStart } from '.'; +import { plugin as pluginInitializer } from '.'; +import { coreMock } from '../../../core/server/mocks'; + +/* eslint-disable */ +import { bfetchPluginMock } from '../../bfetch/server/mocks'; +/* eslint-enable */ + +export type Setup = jest.Mocked; +export type Start = jest.Mocked; + +const createSetupContract = (): Setup => { + const setupContract: Setup = { + __LEGACY: { + register: jest.fn(), + registries: jest.fn(), + }, + }; + return setupContract; +}; + +const createStartContract = (): Start => { + const startContract: Start = {}; + + return startContract; +}; + +const createPlugin = async () => { + const pluginInitializerContext = coreMock.createPluginInitializerContext(); + const coreSetup = coreMock.createSetup(); + const coreStart = coreMock.createStart(); + const plugin = pluginInitializer(pluginInitializerContext); + const setup = await plugin.setup(coreSetup, { + bfetch: bfetchPluginMock.createSetupContract(), + }); + + return { + pluginInitializerContext, + coreSetup, + coreStart, + plugin, + setup, + doStart: async () => + await plugin.start(coreStart, { + bfetch: bfetchPluginMock.createStartContract(), + }), + }; +}; + +export const expressionsPluginMock = { + createSetupContract, + createStartContract, + createPlugin, +}; diff --git a/src/plugins/expressions/server/plugin.ts b/src/plugins/expressions/server/plugin.ts new file mode 100644 index 0000000000000..84c780b5ca226 --- /dev/null +++ b/src/plugins/expressions/server/plugin.ts @@ -0,0 +1,77 @@ +/* + * 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 { CoreStart, PluginInitializerContext, CoreSetup, Plugin } from 'src/core/server'; +import { BfetchServerSetup, BfetchServerStart } from '../../bfetch/server'; +import { + LegacyInterpreterServerApi, + createLegacyServerInterpreterApi, + createLegacyServerEndpoints, +} from './legacy'; + +// eslint-disable-next-line +export interface ExpressionsServerSetupDependencies { + bfetch: BfetchServerSetup; +} + +// eslint-disable-next-line +export interface ExpressionsServerStartDependencies { + bfetch: BfetchServerStart; +} + +export interface ExpressionsServerSetup { + __LEGACY: LegacyInterpreterServerApi; +} + +// eslint-disable-next-line +export interface ExpressionsServerStart {} + +export class ExpressionsServerPlugin + implements + Plugin< + ExpressionsServerSetup, + ExpressionsServerStart, + ExpressionsServerSetupDependencies, + ExpressionsServerStartDependencies + > { + constructor(private readonly initializerContext: PluginInitializerContext) {} + + public setup( + core: CoreSetup, + plugins: ExpressionsServerSetupDependencies + ): ExpressionsServerSetup { + const logger = this.initializerContext.logger.get(); + + const legacyApi = createLegacyServerInterpreterApi(); + createLegacyServerEndpoints(legacyApi, logger, core, plugins); + + return { + __LEGACY: legacyApi, + }; + } + + public start( + core: CoreStart, + plugins: ExpressionsServerStartDependencies + ): ExpressionsServerStart { + return {}; + } + + public stop() {} +} diff --git a/src/plugins/kibana_utils/common/index.ts b/src/plugins/kibana_utils/common/index.ts index eb3bb96c8e874..bfb45b88964d8 100644 --- a/src/plugins/kibana_utils/common/index.ts +++ b/src/plugins/kibana_utils/common/index.ts @@ -18,4 +18,5 @@ */ export * from './defer'; +export * from './of'; export { distinctUntilChangedWithInitialValue } from './distinct_until_changed_with_initial_value'; diff --git a/src/plugins/kibana_utils/common/of.test.ts b/src/plugins/kibana_utils/common/of.test.ts new file mode 100644 index 0000000000000..6c3f0ec1592bd --- /dev/null +++ b/src/plugins/kibana_utils/common/of.test.ts @@ -0,0 +1,63 @@ +/* + * 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 { of } from './of'; + +describe('of()', () => { + describe('when promise resolves', () => { + const promise = new Promise(resolve => resolve()).then(() => 123); + + test('first member of 3-tuple is the promise value', async () => { + const [result] = await of(promise); + expect(result).toBe(123); + }); + + test('second member of 3-tuple is undefined', async () => { + const [, error] = await of(promise); + expect(error).toBe(undefined); + }); + + test('third, flag member, of 3-tuple is true', async () => { + const [, , resolved] = await of(promise); + expect(resolved).toBe(true); + }); + }); + + describe('when promise rejects', () => { + const promise = new Promise(resolve => resolve()).then(() => { + // eslint-disable-next-line no-throw-literal + throw 123; + }); + + test('first member of 3-tuple is undefined', async () => { + const [result] = await of(promise); + expect(result).toBe(undefined); + }); + + test('second member of 3-tuple is thrown error', async () => { + const [, error] = await of(promise); + expect(error).toBe(123); + }); + + test('third, flag member, of 3-tuple is false', async () => { + const [, , resolved] = await of(promise); + expect(resolved).toBe(false); + }); + }); +}); diff --git a/src/legacy/core_plugins/interpreter/server/lib/create_handlers.ts b/src/plugins/kibana_utils/common/of.ts similarity index 56% rename from src/legacy/core_plugins/interpreter/server/lib/create_handlers.ts rename to src/plugins/kibana_utils/common/of.ts index 6e295d0aecaa5..fa0ec8b0ce306 100644 --- a/src/legacy/core_plugins/interpreter/server/lib/create_handlers.ts +++ b/src/plugins/kibana_utils/common/of.ts @@ -17,16 +17,21 @@ * under the License. */ -export const createHandlers = (request: any, server: any) => { - const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); - const config = server.config(); - - return { - environment: 'server', - serverUri: - config.has('server.rewriteBasePath') && config.get('server.rewriteBasePath') - ? `${server.info.uri}${config.get('server.basePath')}` - : server.info.uri, - elasticsearchClient: async (...args: any) => callWithRequest(request, ...args), - }; +/** + * Given a promise awaits it and returns a 3-tuple, with the following members: + * + * - First entry is either the resolved value of the promise or `undefined`. + * - Second entry is either the error thrown by promise or `undefined`. + * - Third entry is a boolean, truthy if promise was resolved and falsy if rejected. + * + * @param promise Promise to convert to 3-tuple. + */ +export const of = async ( + promise: Promise +): Promise<[T | undefined, E | undefined, boolean]> => { + try { + return [await promise, undefined, true]; + } catch (error) { + return [undefined, error, false]; + } }; diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 0ba444c4e9395..fa58a61e51232 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -17,7 +17,7 @@ * under the License. */ -export { defer } from '../common'; +export { defer, Defer, of } from '../common'; export * from './core'; export * from './errors'; export * from './field_mapping'; diff --git a/test/plugin_functional/config.js b/test/plugin_functional/config.js index e9a4f3bcc4b1a..e63054f1b6912 100644 --- a/test/plugin_functional/config.js +++ b/test/plugin_functional/config.js @@ -38,6 +38,7 @@ export default async function({ readConfigFile }) { require.resolve('./test_suites/embeddable_explorer'), require.resolve('./test_suites/core_plugins'), require.resolve('./test_suites/management'), + require.resolve('./test_suites/bfetch_explorer'), ], services: { ...functionalConfig.get('services'), diff --git a/test/plugin_functional/plugins/kbn_tp_bfetch_explorer/kibana.json b/test/plugin_functional/plugins/kbn_tp_bfetch_explorer/kibana.json new file mode 100644 index 0000000000000..1acc7df871c94 --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_bfetch_explorer/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "kbn_tp_bfetch_explorer", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["kbn_tp_bfetch_explorer"], + "server": true, + "ui": true, + "requiredPlugins": ["bfetch"], + "optionalPlugins": [] +} diff --git a/test/plugin_functional/plugins/kbn_tp_bfetch_explorer/package.json b/test/plugin_functional/plugins/kbn_tp_bfetch_explorer/package.json new file mode 100644 index 0000000000000..e396489a1ffc4 --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_bfetch_explorer/package.json @@ -0,0 +1,17 @@ +{ + "name": "kbn_tp_bfetch_explorer", + "version": "1.0.0", + "main": "target/examples/kbn_tp_bfetch_explorer", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.7.2" + } +} diff --git a/test/plugin_functional/plugins/kbn_tp_bfetch_explorer/public/index.ts b/test/plugin_functional/plugins/kbn_tp_bfetch_explorer/public/index.ts new file mode 100644 index 0000000000000..547dfe2aa38d2 --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_bfetch_explorer/public/index.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 * from '../../../../../examples/bfetch_explorer/public'; diff --git a/test/plugin_functional/plugins/kbn_tp_bfetch_explorer/server/index.ts b/test/plugin_functional/plugins/kbn_tp_bfetch_explorer/server/index.ts new file mode 100644 index 0000000000000..b4370eb53311e --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_bfetch_explorer/server/index.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 * from '../../../../../examples/bfetch_explorer/server'; diff --git a/test/plugin_functional/plugins/kbn_tp_bfetch_explorer/tsconfig.json b/test/plugin_functional/plugins/kbn_tp_bfetch_explorer/tsconfig.json new file mode 100644 index 0000000000000..994f81e396763 --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_bfetch_explorer/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true, + "types": [ + "node", + "jest", + "react" + ] + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "server/**/*.tsx", + "../../../../typings/**/*", + ], + "exclude": [] +} diff --git a/test/plugin_functional/test_suites/bfetch_explorer/batched_function.ts b/test/plugin_functional/test_suites/bfetch_explorer/batched_function.ts new file mode 100644 index 0000000000000..cb2a0b41694c2 --- /dev/null +++ b/test/plugin_functional/test_suites/bfetch_explorer/batched_function.ts @@ -0,0 +1,93 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('batchedFunction', () => { + beforeEach(async () => { + await appsMenu.clickLink('bfetch explorer'); + await testSubjects.click('count-until'); + await testSubjects.click('double-integers'); + }); + + it('executes all requests in a batch', async () => { + const form = await testSubjects.find('DoubleIntegers'); + const btn = await form.findByCssSelector('button'); + await btn.click(); + await new Promise(r => setTimeout(r, 4000)); + const pre = await form.findByCssSelector('pre'); + const text = await pre.getVisibleText(); + const json = JSON.parse(text); + + expect(json).to.eql([ + { + num: -1, + error: { + message: 'Invalid number', + }, + }, + { + num: 300, + result: { + num: 600, + }, + }, + { + num: 1000, + result: { + num: 2000, + }, + }, + { + num: 2000, + result: { + num: 4000, + }, + }, + ]); + }); + + it('streams results back', async () => { + const form = await testSubjects.find('DoubleIntegers'); + const btn = await form.findByCssSelector('button'); + await btn.click(); + + await new Promise(r => setTimeout(r, 500)); + const pre = await form.findByCssSelector('pre'); + + const text1 = await pre.getVisibleText(); + const json1 = JSON.parse(text1); + + expect(json1.length > 0).to.be(true); + expect(json1.length < 4).to.be(true); + + await new Promise(r => setTimeout(r, 3500)); + + const text2 = await pre.getVisibleText(); + const json2 = JSON.parse(text2); + + expect(json2.length).to.be(4); + }); + }); +} diff --git a/test/plugin_functional/test_suites/bfetch_explorer/index.ts b/test/plugin_functional/test_suites/bfetch_explorer/index.ts new file mode 100644 index 0000000000000..54f127d6de89a --- /dev/null +++ b/test/plugin_functional/test_suites/bfetch_explorer/index.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. + */ + +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; + +export default function({ getService, getPageObjects, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const appsMenu = getService('appsMenu'); + const PageObjects = getPageObjects(['common', 'header']); + + describe('bfetch explorer', function() { + before(async () => { + await browser.setWindowSize(1300, 900); + await PageObjects.common.navigateToApp('settings'); + await appsMenu.clickLink('bfetch explorer'); + }); + + loadTestFile(require.resolve('./batched_function')); + }); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/PlatinumLicensePrompt.stories.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/PlatinumLicensePrompt.stories.tsx new file mode 100644 index 0000000000000..80281c1a0a8fc --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/PlatinumLicensePrompt.stories.tsx @@ -0,0 +1,33 @@ +/* + * 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. + */ + +import { storiesOf } from '@storybook/react'; +import React from 'react'; +import { PlatinumLicensePrompt } from './PlatinumLicensePrompt'; +import { + ApmPluginContext, + ApmPluginContextValue +} from '../../../context/ApmPluginContext'; + +storiesOf('app/ServiceMap/PlatinumLicensePrompt', module).add( + 'example', + () => { + const contextMock = ({ + core: { http: { basePath: { prepend: () => {} } } } + } as unknown) as ApmPluginContextValue; + + return ( + + + + ); + }, + { + info: { + source: false + } + } +); diff --git a/x-pack/legacy/plugins/apm/readme.md b/x-pack/legacy/plugins/apm/readme.md index 6b21f08b7695e..2106243d12aea 100644 --- a/x-pack/legacy/plugins/apm/readme.md +++ b/x-pack/legacy/plugins/apm/readme.md @@ -40,7 +40,6 @@ For testing purposes APM uses 3 custom users: **kibana_write_user** Apps: read/write. Indices: None - To create the users with the correct roles run the following script: ```sh @@ -88,6 +87,12 @@ yarn prettier "./x-pack/legacy/plugins/apm/**/*.{tsx,ts,js}" --write yarn eslint ./x-pack/legacy/plugins/apm --fix ``` +#### Storybook + +Start the [Storybook](https://storybook.js.org/) development environment with +`yarn storybook apm`. All files with a .stories.tsx extension will be loaded. +You can access the development environment at http://localhost:9001. + #### Further resources - [Cypress integration tests](cypress/README.md) diff --git a/x-pack/legacy/plugins/apm/scripts/storybook.js b/x-pack/legacy/plugins/apm/scripts/storybook.js new file mode 100644 index 0000000000000..846ee1f301e30 --- /dev/null +++ b/x-pack/legacy/plugins/apm/scripts/storybook.js @@ -0,0 +1,15 @@ +/* + * 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. + */ + +import { join } from 'path'; + +// eslint-disable-next-line +require('@kbn/storybook').runStorybookCli({ + name: 'apm', + storyGlobs: [ + join(__dirname, '..', 'public', 'components', '**', '*.stories.tsx') + ] +}); diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/load_mappings/load_mappings_provider.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/load_mappings/load_mappings_provider.tsx index 9402a64b22dde..a55bd96dce3d0 100644 --- a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/load_mappings/load_mappings_provider.tsx +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/load_mappings/load_mappings_provider.tsx @@ -234,7 +234,7 @@ export const LoadMappingsProvider = ({ onJson, children }: Props) => { mappings, }} diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/parameters_definition.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/parameters_definition.tsx index 581b1223b7892..1277568b71bbd 100644 --- a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/parameters_definition.tsx +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/constants/parameters_definition.tsx @@ -100,14 +100,10 @@ const fielddataFrequencyFilterParam = { }, }, }, - schema: t.intersection([ - t.partial({ - min: t.number, - max: t.number, - min_segment_size: t.number, - }), - t.brand(t.UnknownRecord, (v: any): v is any => !Array.isArray(v), 'Array'), - ]), + schema: t.record( + t.union([t.literal('min'), t.literal('max'), t.literal('min_segment_size')]), + t.number + ), }; const analyzerValidations = [ diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/error_reporter.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/error_reporter.ts index 363ccfc2a5fab..e9beee1071597 100644 --- a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/error_reporter.ts +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/error_reporter.ts @@ -3,14 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ValidationError, Validation } from 'io-ts'; +import { ValidationError } from 'io-ts'; import { fold } from 'fp-ts/lib/Either'; import { Reporter } from 'io-ts/lib/Reporter'; export type ReporterResult = Array<{ path: string[]; message: string }>; -export const failure = (validation: any): ReporterResult => { - return validation.map((e: ValidationError) => { +const failure = (validation: ValidationError[]): ReporterResult => { + return validation.map(e => { const path: string[] = []; let validationName = ''; @@ -37,6 +37,6 @@ export const failure = (validation: any): ReporterResult => { const empty: never[] = []; const success = () => empty; -export const ErrorReporter: Reporter = { - report: (validation: Validation) => fold(failure, success)(validation as any), +export const errorReporter: Reporter = { + report: fold(failure, success), }; diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/mappings_validator.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/mappings_validator.ts index 990d5ec961a6f..fff735da2e758 100644 --- a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/mappings_validator.ts +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/lib/mappings_validator.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; import { ordString } from 'fp-ts/lib/Ord'; import { toArray } from 'fp-ts/lib/Set'; import { isLeft, isRight } from 'fp-ts/lib/Either'; -import { ErrorReporter } from './error_reporter'; +import { errorReporter } from './error_reporter'; import { ALL_DATA_TYPES, PARAMETERS_DEFINITION } from '../constants'; import { FieldMeta } from '../types'; import { getFieldMeta } from '../lib'; @@ -218,7 +218,7 @@ const mappingsConfigurationSchemaKeys = Object.keys(mappingsConfigurationSchema. const validateMappingsConfiguration = ( mappingsConfiguration: any ): { value: any; errors: MappingsValidationError[] } => { - // Array to keep track of invalid configuration parameters. + // Set to keep track of invalid configuration parameters. const configurationRemoved: Set = new Set(); let copyOfMappingsConfig = { ...mappingsConfiguration }; @@ -228,7 +228,7 @@ const validateMappingsConfiguration = ( /** * To keep the logic simple we will strip out the parameters that contain errors */ - const errors = ErrorReporter.report(result); + const errors = errorReporter.report(result); errors.forEach(error => { const configurationName = error.path[0]; configurationRemoved.add(configurationName); diff --git a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js index b40808d51dd66..206b9e01bab8c 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js @@ -63,9 +63,6 @@ describe('AnomaliesTable', () => { expect(columns).toEqual( expect.arrayContaining([ - expect.objectContaining({ - name: '', - }), expect.objectContaining({ name: 'time', }), diff --git a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js index 5454911673fe2..58f1214c11e10 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js +++ b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonIcon, EuiLink } from '@elastic/eui'; +import { EuiButtonIcon, EuiLink, EuiScreenReaderOnly } from '@elastic/eui'; import React from 'react'; import _ from 'lodash'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { formatHumanReadableDate, @@ -65,7 +66,16 @@ export function getColumns( ) { const columns = [ { - name: '', + name: ( + +

+ +

+
+ ), render: item => ( toggleRow(item)} diff --git a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js index 8cbee27bdd9a8..074a584f3a136 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js +++ b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js @@ -479,7 +479,6 @@ export const LinksMenu = injectI18n( return ( { describe('Data Frame Analytics: ', () => { test('Minimal initialization', () => { const wrapper = shallow( - + + + ); // Without the jobConfig being loaded, the component will just return empty. expect(wrapper.text()).toMatch(''); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx index 9691a0706121c..098f8f07bee44 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx @@ -55,6 +55,7 @@ import { SEARCH_SIZE, defaultSearchQuery, } from '../../../../common'; +import { isKeywordAndTextType } from '../../../../common/fields'; import { getOutlierScoreFieldName } from './common'; import { useExploreData, TableItem } from './use_explore_data'; @@ -64,6 +65,10 @@ import { } from '../../../analytics_management/components/analytics_list/common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; import { SavedSearchQuery } from '../../../../../contexts/kibana'; +import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; +import { IIndexPattern } from '../../../../../../../../../../../src/plugins/data/common/index_patterns'; +import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; +import { useKibanaContext } from '../../../../../contexts/kibana'; const FEATURE_INFLUENCE = 'feature_influence'; @@ -110,6 +115,19 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { const [searchError, setSearchError] = useState(undefined); const [searchString, setSearchString] = useState(undefined); + const kibanaContext = useKibanaContext(); + + const initializeJobCapsService = async () => { + if (jobConfig !== undefined) { + const sourceIndex = jobConfig.source.index[0]; + const indexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; + const indexPattern: IIndexPattern = await kibanaContext.indexPatterns.get(indexPatternId); + if (indexPattern !== undefined) { + await newJobCapsService.initializeFromIndexPattern(indexPattern, false, false); + } + } + }; + useEffect(() => { (async function() { const analyticsConfigs: GetDataFrameAnalyticsResponse = await ml.dataFrameAnalytics.getDataFrameAnalytics( @@ -124,6 +142,10 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { })(); }, []); + useEffect(() => { + initializeJobCapsService(); + }, [jobConfig && jobConfig.id]); + const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false); @@ -293,10 +315,16 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { if (jobConfig !== undefined) { const outlierScoreFieldName = getOutlierScoreFieldName(jobConfig); const outlierScoreFieldSelected = selectedFields.includes(outlierScoreFieldName); + let requiresKeyword = false; const field = outlierScoreFieldSelected ? outlierScoreFieldName : selectedFields[0]; const direction = outlierScoreFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC; - loadExploreData({ field, direction, searchQuery }); + + if (outlierScoreFieldSelected === false) { + requiresKeyword = isKeywordAndTextType(field); + } + + loadExploreData({ field, direction, searchQuery, requiresKeyword }); } }, [JSON.stringify(searchQuery)]); @@ -307,10 +335,16 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { if (jobConfig !== undefined && columns.length > 0 && !selectedFields.includes(sortField)) { const outlierScoreFieldName = getOutlierScoreFieldName(jobConfig); const outlierScoreFieldSelected = selectedFields.includes(outlierScoreFieldName); + let requiresKeyword = false; const field = outlierScoreFieldSelected ? outlierScoreFieldName : selectedFields[0]; const direction = outlierScoreFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC; - loadExploreData({ field, direction, searchQuery }); + + if (outlierScoreFieldSelected === false) { + requiresKeyword = isKeywordAndTextType(field); + } + + loadExploreData({ field, direction, searchQuery, requiresKeyword }); return; } }, [jobConfig, columns.length, sortField, sortDirection, tableItems.length]); @@ -334,8 +368,17 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { setPageIndex(index); setPageSize(size); - if (sort.field !== sortField || sort.direction !== sortDirection) { - loadExploreData({ ...sort, searchQuery }); + if ( + (sort.field !== sortField || sort.direction !== sortDirection) && + jobConfig !== undefined + ) { + const outlierScoreFieldName = getOutlierScoreFieldName(jobConfig); + let requiresKeyword = false; + + if (outlierScoreFieldName !== sort.field) { + requiresKeyword = isKeywordAndTextType(sort.field); + } + loadExploreData({ ...sort, searchQuery, requiresKeyword }); } }; } diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/use_explore_data.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/use_explore_data.ts index e76cbaa463f1d..24cc8d000de7e 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/use_explore_data.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/use_explore_data.ts @@ -23,18 +23,12 @@ import { defaultSearchQuery, SearchQuery, } from '../../../../common'; +import { LoadExploreDataArg } from '../../../../common/analytics'; import { getOutlierScoreFieldName } from './common'; -import { SavedSearchQuery } from '../../../../../contexts/kibana'; export type TableItem = Record; -interface LoadExploreDataArg { - field: string; - direction: SortDirection; - searchQuery: SavedSearchQuery; -} - export interface UseExploreDataReturnType { errorMessage: string; loadExploreData: (arg: LoadExploreDataArg) => void; @@ -55,7 +49,12 @@ export const useExploreData = ( const [sortField, setSortField] = useState(''); const [sortDirection, setSortDirection] = useState(SORT_DIRECTION.ASC); - const loadExploreData = async ({ field, direction, searchQuery }: LoadExploreDataArg) => { + const loadExploreData = async ({ + field, + direction, + searchQuery, + requiresKeyword, + }: LoadExploreDataArg) => { if (jobConfig !== undefined) { setErrorMessage(''); setStatus(INDEX_STATUS.LOADING); @@ -70,7 +69,7 @@ export const useExploreData = ( if (field !== undefined) { body.sort = [ { - [field]: { + [`${field}${requiresKeyword ? '.keyword' : ''}`]: { order: direction, }, }, diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx index 1f4ad65bd1879..86f1324cc0377 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx @@ -6,12 +6,14 @@ import React, { Fragment } from 'react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiBadge, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiProgress, + EuiScreenReaderOnly, EuiText, EuiToolTip, RIGHT_ALIGNMENT, @@ -142,6 +144,16 @@ export const getColumns = ( // update possible column types to something like (FieldDataColumn | ComputedColumn | ActionsColumn)[] when they have been added to EUI const columns: any[] = [ { + name: ( + +

+ +

+
+ ), align: RIGHT_ALIGNMENT, width: '40px', isExpander: true, diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js index 818f1b79b3143..7a6a1c22e39c5 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js @@ -16,8 +16,8 @@ import { JobDescription } from './job_description'; import { JobIcon } from '../../../../components/job_message_icon'; import { getJobIdUrl } from '../utils'; -import { EuiBadge, EuiBasicTable, EuiButtonIcon, EuiLink } from '@elastic/eui'; -import { injectI18n } from '@kbn/i18n/react'; +import { EuiBadge, EuiBasicTable, EuiButtonIcon, EuiLink, EuiScreenReaderOnly } from '@elastic/eui'; +import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; const PAGE_SIZE = 10; @@ -124,7 +124,16 @@ class JobsListUI extends Component { // be updated if we move to always using props for width. const columns = [ { - name: '', + name: ( + +

+ +

+
+ ), render: item => ( this.toggleRow(item)} @@ -167,7 +176,16 @@ class JobsListUI extends Component { }, { field: 'auditMessage', - name: '', + name: ( + +

+ +

+
+ ), render: item => , }, { @@ -272,7 +290,16 @@ class JobsListUI extends Component { width: '15%', }); columns.push({ - name: '', + name: ( + +

+ +

+
+ ), actions: actionsMenuContent( this.props.showEditJobFlyout, this.props.showDeleteJobModal, diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts index 9a915b0e77d44..fbf75e8a854c6 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts @@ -6,15 +6,19 @@ import { drag, drop } from '../../lib/drag_n_drop/helpers'; import { populateTimeline } from '../../lib/fields_browser/helpers'; -import { toggleFirstTimelineEventDetails } from '../../lib/timeline/helpers'; +import { createNewTimeline, toggleFirstTimelineEventDetails } from '../../lib/timeline/helpers'; import { HOSTS_PAGE } from '../../lib/urls'; import { loginAndWaitForPage, DEFAULT_TIMEOUT } from '../../lib/util/helpers'; describe('toggle column in timeline', () => { - beforeEach(() => { + before(() => { loginAndWaitForPage(HOSTS_PAGE); }); + afterEach(() => { + createNewTimeline(); + }); + const timestampField = '@timestamp'; const idField = '_id'; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx index ebe4f7efda055..e2f92e0acd645 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx @@ -6,12 +6,14 @@ import React, { Fragment } from 'react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiBadge, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiProgress, + EuiScreenReaderOnly, EuiText, EuiToolTip, RIGHT_ALIGNMENT, @@ -98,6 +100,16 @@ export const getColumns = ( ActionsColumnType ] = [ { + name: ( + +

+ +

+
+ ), align: RIGHT_ALIGNMENT, width: '40px', isExpander: true, diff --git a/x-pack/package.json b/x-pack/package.json index 84f15d3d38828..bd43d19135aab 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -252,7 +252,7 @@ "immer": "^1.5.0", "inline-style": "^2.0.0", "intl": "^1.2.5", - "io-ts": "^2.0.1", + "io-ts": "^2.0.5", "isbinaryfile": "4.0.2", "joi": "^13.5.2", "jquery": "^3.4.1", diff --git a/yarn.lock b/yarn.lock index 587af6ba2e99d..ce222e727d08e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16849,10 +16849,10 @@ invert-kv@^2.0.0: resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== -io-ts@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-2.0.1.tgz#1261c12f915c2f48d16393a36966636b48a45aa1" - integrity sha512-RezD+WcCfW4VkMkEcQWL/Nmy/nqsWTvTYg7oUmTGzglvSSV2P9h2z1PVeREPFf0GWNzruYleAt1XCMQZSg1xxQ== +io-ts@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-2.0.5.tgz#e6e3db9df8b047f9cbd6b69e7d2ad3e6437a0b13" + integrity sha512-pL7uUptryanI5Glv+GUv7xh+aLBjxGEDmLwmEYNSx0yOD3djK0Nw5Bt0N6BAkv9LadOUU7QKpRsLcqnTh3UlLA== ip-regex@^2.1.0: version "2.1.0"