diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 477089e48367f..75a415ee7e128 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -163,6 +163,7 @@ /packages/kbn-analytics/ @elastic/pulse /src/legacy/core_plugins/ui_metric/ @elastic/pulse /src/plugins/kibana_usage_collection/ @elastic/pulse +/src/plugins/newsfeed/ @elastic/pulse /src/plugins/telemetry/ @elastic/pulse /src/plugins/telemetry_collection_manager/ @elastic/pulse /src/plugins/telemetry_management_section/ @elastic/pulse diff --git a/src/core/CONVENTIONS.md b/src/core/CONVENTIONS.md index a82cc27839a1d..c124169580337 100644 --- a/src/core/CONVENTIONS.md +++ b/src/core/CONVENTIONS.md @@ -201,6 +201,36 @@ export class MyPlugin implements Plugin { } ``` +Prefer the pattern shown above, using `core.getStartServices()`, rather than store local references retrieved from `start`. + +**Bad:** +```ts +export class MyPlugin implements Plugin { + // Anti pattern + private coreStart?: CoreStart; + private depsStart?: DepsStart; + + public setup(core) { + core.application.register({ + id: 'my-app', + async mount(params) { + const { renderApp } = await import('./application/my_app'); + // Anti pattern - use `core.getStartServices()` instead! + return renderApp(this.coreStart, this.depsStart, params); + } + }); + } + + public start(core, deps) { + // Anti pattern + this.coreStart = core; + this.depsStart = deps; + } +} +``` + +The main reason to prefer the provided async accessor, is that it doesn't requires the developer to understand and reason about when that function can be called. Having an API that fails sometimes isn't a good API design, and it makes accurately testing this difficult. + #### Services Service structure should mirror the plugin lifecycle to make reasoning about how the service is executed more clear. diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index ed97db020035e..9bb101334beb7 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -125,12 +125,14 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "aria-label": "recent 1", "href": "recent 1", "label": "recent 1", + "onClick": undefined, "title": "recent 1", }, Object { "aria-label": "recent 2", "href": "recent 2", "label": "recent 2", + "onClick": undefined, "title": "recent 2", }, ] @@ -298,6 +300,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` >
@@ -375,6 +379,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` @@ -934,6 +939,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` >
@@ -1011,6 +1018,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` @@ -1638,6 +1646,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` className="euiFlexItem eui-yScroll" > } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-recentlyViewed" id="mockId" initialIsOpen={true} onToggle={[Function]} @@ -1675,6 +1685,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` >
} className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-recentlyViewed" id="mockId" initialIsOpen={true} onToggle={[Function]} @@ -3311,6 +3336,7 @@ exports[`CollapsibleNav renders the default nav 2`] = ` >
} className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-recentlyViewed" id="mockId" initialIsOpen={true} onToggle={[Function]} @@ -4235,6 +4265,7 @@ exports[`CollapsibleNav renders the default nav 3`] = ` >
({ htmlIdGenerator: () => () => 'mockId', @@ -31,24 +31,25 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ const { kibana, observability, security, management } = DEFAULT_APP_CATEGORIES; -function mockLink(label: string, category?: AppCategory) { +function mockLink({ label = 'discover', category, onClick }: Partial) { return { key: label, label, href: label, isActive: true, - onClick: () => {}, + onClick: onClick || (() => {}), category, 'data-test-subj': label, }; } -function mockRecentNavLink(label: string) { +function mockRecentNavLink({ label = 'recent', onClick }: Partial) { return { href: label, label, title: label, 'aria-label': label, + onClick, }; } @@ -67,6 +68,20 @@ function mockProps() { }; } +function expectShownNavLinksCount(component: ReactWrapper, count: number) { + expect( + component.find('.euiAccordion-isOpen a[data-test-subj^="collapsibleNavAppLink"]').length + ).toEqual(count); +} + +function expectNavIsClosed(component: ReactWrapper) { + expectShownNavLinksCount(component, 0); +} + +function clickGroup(component: ReactWrapper, group: string) { + component.find(`[data-test-subj="collapsibleNavGroup-${group}"] button`).simulate('click'); +} + describe('CollapsibleNav', () => { // this test is mostly an "EUI works as expected" sanity check it('renders the default nav', () => { @@ -88,16 +103,19 @@ describe('CollapsibleNav', () => { it('renders links grouped by category', () => { // just a test of category functionality, categories are not accurate const navLinks = [ - mockLink('discover', kibana), - mockLink('siem', security), - mockLink('metrics', observability), - mockLink('monitoring', management), - mockLink('visualize', kibana), - mockLink('dashboard', kibana), - mockLink('canvas'), // links should be able to be rendered top level as well - mockLink('logs', observability), + mockLink({ label: 'discover', category: kibana }), + mockLink({ label: 'siem', category: security }), + mockLink({ label: 'metrics', category: observability }), + mockLink({ label: 'monitoring', category: management }), + mockLink({ label: 'visualize', category: kibana }), + mockLink({ label: 'dashboard', category: kibana }), + mockLink({ label: 'canvas' }), // links should be able to be rendered top level as well + mockLink({ label: 'logs', category: observability }), + ]; + const recentNavLinks = [ + mockRecentNavLink({ label: 'recent 1' }), + mockRecentNavLink({ label: 'recent 2' }), ]; - const recentNavLinks = [mockRecentNavLink('recent 1'), mockRecentNavLink('recent 2')]; const component = mount( { }); it('remembers collapsible section state', () => { - function expectNavLinksCount(component: ReactWrapper, count: number) { - expect( - component.find('.euiAccordion-isOpen a[data-test-subj="collapsibleNavAppLink"]').length - ).toEqual(count); - } - - const navLinks = [ - mockLink('discover', kibana), - mockLink('siem', security), - mockLink('metrics', observability), - mockLink('monitoring', management), - mockLink('visualize', kibana), - mockLink('dashboard', kibana), - mockLink('logs', observability), - ]; - const component = mount(); - expectNavLinksCount(component, 7); - component.find('[data-test-subj="collapsibleNavGroup-kibana"] button').simulate('click'); - expectNavLinksCount(component, 4); + const navLinks = [mockLink({ category: kibana }), mockLink({ category: observability })]; + const recentNavLinks = [mockRecentNavLink({})]; + const component = mount( + + ); + expectShownNavLinksCount(component, 3); + clickGroup(component, 'kibana'); + clickGroup(component, 'recentlyViewed'); + expectShownNavLinksCount(component, 1); component.setProps({ isOpen: false }); - expectNavLinksCount(component, 0); // double check the nav closed + expectNavIsClosed(component); + component.setProps({ isOpen: true }); + expectShownNavLinksCount(component, 1); + }); + + it('closes the nav after clicking a link', () => { + const onClick = sinon.spy(); + const onIsOpenUpdate = sinon.spy(); + const navLinks = [mockLink({ category: kibana, onClick })]; + const recentNavLinks = [mockRecentNavLink({ onClick })]; + const component = mount( + + ); + component.setProps({ + onIsOpenUpdate: (isOpen: boolean) => { + component.setProps({ isOpen }); + onIsOpenUpdate(); + }, + }); + + component.find('[data-test-subj="collapsibleNavGroup-recentlyViewed"] a').simulate('click'); + expect(onClick.callCount).toEqual(1); + expect(onIsOpenUpdate.callCount).toEqual(1); + expectNavIsClosed(component); component.setProps({ isOpen: true }); - expectNavLinksCount(component, 4); + component.find('[data-test-subj="collapsibleNavGroup-kibana"] a').simulate('click'); + expect(onClick.callCount).toEqual(2); + expect(onIsOpenUpdate.callCount).toEqual(2); }); }); diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index 9adcc19b0f0e7..81970bc4a2675 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -159,18 +159,23 @@ export function CollapsibleNav({ isCollapsible={true} initialIsOpen={getIsCategoryOpen('recentlyViewed', storage)} onToggle={isCategoryOpen => setIsCategoryOpen('recentlyViewed', isCategoryOpen, storage)} + data-test-subj="collapsibleNavGroup-recentlyViewed" > {recentNavLinks.length > 0 ? ( { - // TODO #64541 - // Can remove icon from recent links completely - const { iconType, ...linkWithoutIcon } = link; - return linkWithoutIcon; - })} + // TODO #64541 + // Can remove icon from recent links completely + listItems={recentNavLinks.map(({ iconType, onClick = () => {}, ...link }) => ({ + 'data-test-subj': 'collapsibleNavAppLink--recent', + onClick: (e: React.MouseEvent) => { + onIsOpenUpdate(false); + onClick(e); + }, + ...link, + }))} maxWidth="none" color="subdued" gutterSize="none" @@ -191,7 +196,7 @@ export function CollapsibleNav({ {orderedCategories.map((categoryName, i) => { const category = categoryDictionary[categoryName]!; const links = allCategorizedLinks[categoryName].map( - ({ label, href, isActive, isDisabled, onClick }: NavLink) => ({ + ({ label, href, isActive, isDisabled, onClick }) => ({ label, href, isActive, diff --git a/src/core/public/chrome/ui/header/nav_link.tsx b/src/core/public/chrome/ui/header/nav_link.tsx index 22708c796d7dc..8003c22b99a36 100644 --- a/src/core/public/chrome/ui/header/nav_link.tsx +++ b/src/core/public/chrome/ui/header/nav_link.tsx @@ -142,6 +142,7 @@ export interface RecentNavLink { title: string; 'aria-label': string; iconType?: string; + onClick?(event: React.MouseEvent): void; } /** diff --git a/src/legacy/core_plugins/newsfeed/index.ts b/src/legacy/core_plugins/newsfeed/index.ts deleted file mode 100644 index cf8852be09a1e..0000000000000 --- a/src/legacy/core_plugins/newsfeed/index.ts +++ /dev/null @@ -1,71 +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 { resolve } from 'path'; -import { LegacyPluginApi, LegacyPluginSpec, ArrayOrItem } from 'src/legacy/plugin_discovery/types'; -import { Legacy } from 'kibana'; -import { NewsfeedPluginInjectedConfig } from '../../../plugins/newsfeed/types'; -import { - PLUGIN_ID, - DEFAULT_SERVICE_URLROOT, - DEV_SERVICE_URLROOT, - DEFAULT_SERVICE_PATH, -} from './constants'; - -// eslint-disable-next-line import/no-default-export -export default function(kibana: LegacyPluginApi): ArrayOrItem { - const pluginSpec: Legacy.PluginSpecOptions = { - id: PLUGIN_ID, - config(Joi: any) { - // NewsfeedPluginInjectedConfig in Joi form - return Joi.object({ - enabled: Joi.boolean().default(true), - service: Joi.object({ - pathTemplate: Joi.string().default(DEFAULT_SERVICE_PATH), - urlRoot: Joi.when('$prod', { - is: true, - then: Joi.string().default(DEFAULT_SERVICE_URLROOT), - otherwise: Joi.string().default(DEV_SERVICE_URLROOT), - }), - }).default(), - defaultLanguage: Joi.string().default('en'), - mainInterval: Joi.number().default(120 * 1000), // (2min) How often to retry failed fetches, and/or check if newsfeed items need to be refreshed from remote - fetchInterval: Joi.number().default(86400 * 1000), // (1day) How often to fetch remote and reset the last fetched time - }).default(); - }, - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - injectDefaultVars(server): NewsfeedPluginInjectedConfig { - const config = server.config(); - return { - newsfeed: { - service: { - pathTemplate: config.get('newsfeed.service.pathTemplate') as string, - urlRoot: config.get('newsfeed.service.urlRoot') as string, - }, - defaultLanguage: config.get('newsfeed.defaultLanguage') as string, - mainInterval: config.get('newsfeed.mainInterval') as number, - fetchInterval: config.get('newsfeed.fetchInterval') as number, - }, - }; - }, - }, - }; - return new kibana.Plugin(pluginSpec); -} diff --git a/src/legacy/core_plugins/newsfeed/package.json b/src/legacy/core_plugins/newsfeed/package.json deleted file mode 100644 index d4d753f32b0f9..0000000000000 --- a/src/legacy/core_plugins/newsfeed/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "newsfeed", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/newsfeed/public/index.scss b/src/legacy/core_plugins/newsfeed/public/index.scss deleted file mode 100644 index a77132379041c..0000000000000 --- a/src/legacy/core_plugins/newsfeed/public/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - -@import './np_ready/components/header_alert/_index'; diff --git a/src/legacy/core_plugins/newsfeed/public/np_ready/components/header_alert/_index.scss b/src/legacy/core_plugins/newsfeed/public/np_ready/components/header_alert/_index.scss deleted file mode 100644 index e25dbd25daaf5..0000000000000 --- a/src/legacy/core_plugins/newsfeed/public/np_ready/components/header_alert/_index.scss +++ /dev/null @@ -1,27 +0,0 @@ -@import '@elastic/eui/src/components/header/variables'; - -.kbnNews__flyout { - top: $euiHeaderChildSize + 1px; - height: calc(100% - #{$euiHeaderChildSize}); -} - -.kbnNewsFeed__headerAlert.euiHeaderAlert { - margin-bottom: $euiSizeL; - padding: 0 $euiSizeS $euiSizeL; - border-bottom: $euiBorderThin; - border-top: none; - - .euiHeaderAlert__title { - @include euiTitle('xs'); - margin-bottom: $euiSizeS; - } - - .euiHeaderAlert__text { - @include euiFontSizeS; - margin-bottom: $euiSize; - } - - .euiHeaderAlert__action { - @include euiFontSizeS; - } -} diff --git a/src/legacy/core_plugins/newsfeed/public/np_ready/components/header_alert/header_alert.tsx b/src/legacy/core_plugins/newsfeed/public/np_ready/components/header_alert/header_alert.tsx deleted file mode 100644 index c3c3e4144fca8..0000000000000 --- a/src/legacy/core_plugins/newsfeed/public/np_ready/components/header_alert/header_alert.tsx +++ /dev/null @@ -1,76 +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 React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -import { EuiFlexGroup, EuiFlexItem, EuiI18n } from '@elastic/eui'; - -interface IEuiHeaderAlertProps { - action: JSX.Element; - className?: string; - date: string; - text: string; - title: string; - badge?: JSX.Element; - rest?: string[]; -} - -export const EuiHeaderAlert = ({ - action, - className, - date, - text, - title, - badge, - ...rest -}: IEuiHeaderAlertProps) => { - const classes = classNames('euiHeaderAlert', 'kbnNewsFeed__headerAlert', className); - - const badgeContent = badge || null; - - return ( - - {(dismiss: any) => ( -
- - -
{date}
-
- {badgeContent} -
- -
{title}
-
{text}
-
{action}
-
- )} -
- ); -}; - -EuiHeaderAlert.propTypes = { - action: PropTypes.node, - className: PropTypes.string, - date: PropTypes.node.isRequired, - text: PropTypes.node, - title: PropTypes.node.isRequired, - badge: PropTypes.node, -}; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts index 6feafe5e5e597..5fa37a5ac28eb 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts @@ -112,13 +112,17 @@ export function createTableRowDirective($compile: ng.ICompileService, $httpParam const globalFilters: any = getServices().filterManager.getGlobalFilters(); const appFilters: any = getServices().filterManager.getAppFilters(); const hash = $httpParamSerializer({ - _g: rison.encode({ - filters: globalFilters || [], - }), - _a: rison.encode({ - columns: $scope.columns, - filters: (appFilters || []).map(esFilters.disableFilter), - }), + _g: encodeURI( + rison.encode({ + filters: globalFilters || [], + }) + ), + _a: encodeURI( + rison.encode({ + columns: $scope.columns, + filters: (appFilters || []).map(esFilters.disableFilter), + }) + ), }); return `${path}?${hash}`; diff --git a/test/common/fixtures/plugins/newsfeed/index.ts b/src/plugins/newsfeed/common/constants.ts similarity index 65% rename from test/common/fixtures/plugins/newsfeed/index.ts rename to src/plugins/newsfeed/common/constants.ts index beee9bb5c6069..6bc95873a342d 100644 --- a/test/common/fixtures/plugins/newsfeed/index.ts +++ b/src/plugins/newsfeed/common/constants.ts @@ -17,17 +17,10 @@ * under the License. */ -import Hapi from 'hapi'; -import { initPlugin as initNewsfeed } from './newsfeed_simulation'; +export const NEWSFEED_FALLBACK_LANGUAGE = 'en'; +export const NEWSFEED_LAST_FETCH_STORAGE_KEY = 'newsfeed.lastfetchtime'; +export const NEWSFEED_HASH_SET_STORAGE_KEY = 'newsfeed.hashes'; -const NAME = 'newsfeed-FTS-external-service-simulators'; - -// eslint-disable-next-line import/no-default-export -export default function(kibana: any) { - return new kibana.Plugin({ - name: NAME, - init: (server: Hapi.Server) => { - initNewsfeed(server, `/api/_${NAME}`); - }, - }); -} +export const NEWSFEED_DEFAULT_SERVICE_BASE_URL = 'https://feeds.elastic.co'; +export const NEWSFEED_DEV_SERVICE_BASE_URL = 'https://feeds-staging.elastic.co'; +export const NEWSFEED_DEFAULT_SERVICE_PATH = '/kibana/v{VERSION}.json'; diff --git a/src/plugins/newsfeed/kibana.json b/src/plugins/newsfeed/kibana.json index 9d49b42424a06..b9f37b67f6921 100644 --- a/src/plugins/newsfeed/kibana.json +++ b/src/plugins/newsfeed/kibana.json @@ -1,6 +1,6 @@ { "id": "newsfeed", "version": "kibana", - "server": false, + "server": true, "ui": true } diff --git a/src/plugins/newsfeed/public/components/flyout_list.tsx b/src/plugins/newsfeed/public/components/flyout_list.tsx index bd554d7d98b7d..6e9444c950107 100644 --- a/src/plugins/newsfeed/public/components/flyout_list.tsx +++ b/src/plugins/newsfeed/public/components/flyout_list.tsx @@ -29,11 +29,11 @@ import { EuiButtonEmpty, EuiText, EuiBadge, + EuiHeaderAlert, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiHeaderAlert } from '../../../../legacy/core_plugins/newsfeed/public/np_ready/components/header_alert/header_alert'; import { NewsfeedContext } from './newsfeed_header_nav_button'; -import { NewsfeedItem } from '../../types'; +import { NewsfeedItem } from '../types'; import { NewsEmptyPrompt } from './empty_news'; import { NewsLoadingPrompt } from './loading_news'; diff --git a/src/plugins/newsfeed/public/components/newsfeed_header_nav_button.tsx b/src/plugins/newsfeed/public/components/newsfeed_header_nav_button.tsx index da042f0fce7b6..fd938e9071074 100644 --- a/src/plugins/newsfeed/public/components/newsfeed_header_nav_button.tsx +++ b/src/plugins/newsfeed/public/components/newsfeed_header_nav_button.tsx @@ -21,7 +21,7 @@ import React, { useState, Fragment, useEffect } from 'react'; import * as Rx from 'rxjs'; import { EuiHeaderSectionItemButton, EuiIcon, EuiNotificationBadge } from '@elastic/eui'; import { NewsfeedFlyout } from './flyout_list'; -import { FetchResult } from '../../types'; +import { FetchResult } from '../types'; export interface INewsfeedContext { setFlyoutVisible: React.Dispatch>; diff --git a/src/plugins/newsfeed/public/lib/api.test.ts b/src/plugins/newsfeed/public/lib/api.test.ts index 9d64dd26da047..5db578f1bd4e9 100644 --- a/src/plugins/newsfeed/public/lib/api.test.ts +++ b/src/plugins/newsfeed/public/lib/api.test.ts @@ -22,8 +22,11 @@ import { interval, race } from 'rxjs'; import sinon, { stub } from 'sinon'; import moment from 'moment'; import { HttpSetup } from 'src/core/public'; -import { NEWSFEED_HASH_SET_STORAGE_KEY, NEWSFEED_LAST_FETCH_STORAGE_KEY } from '../../constants'; -import { ApiItem, NewsfeedItem, NewsfeedPluginInjectedConfig } from '../../types'; +import { + NEWSFEED_HASH_SET_STORAGE_KEY, + NEWSFEED_LAST_FETCH_STORAGE_KEY, +} from '../../common/constants'; +import { ApiItem, NewsfeedItem, NewsfeedPluginBrowserConfig } from '../types'; import { NewsfeedApiDriver, getApi } from './api'; const localStorageGet = sinon.stub(); @@ -458,7 +461,7 @@ describe('getApi', () => { } return Promise.reject('wrong args!'); }; - let configMock: NewsfeedPluginInjectedConfig; + let configMock: NewsfeedPluginBrowserConfig; afterEach(() => { jest.resetAllMocks(); @@ -466,15 +469,12 @@ describe('getApi', () => { beforeEach(() => { configMock = { - newsfeed: { - service: { - urlRoot: 'http://fakenews.co', - pathTemplate: '/kibana-test/v{VERSION}.json', - }, - defaultLanguage: 'en', - mainInterval: 86400000, - fetchInterval: 86400000, + service: { + urlRoot: 'http://fakenews.co', + pathTemplate: '/kibana-test/v{VERSION}.json', }, + mainInterval: moment.duration(86400000), + fetchInterval: moment.duration(86400000), }; httpMock = ({ fetch: mockHttpGet, @@ -483,7 +483,7 @@ describe('getApi', () => { it('creates a result', done => { mockHttpGet.mockImplementationOnce(() => Promise.resolve({ items: [] })); - getApi(httpMock, configMock.newsfeed, '6.8.2').subscribe(result => { + getApi(httpMock, configMock, '6.8.2').subscribe(result => { expect(result).toMatchInlineSnapshot(` Object { "error": null, @@ -528,7 +528,7 @@ describe('getApi', () => { mockHttpGet.mockImplementationOnce(getHttpMockWithItems(mockApiItems)); - getApi(httpMock, configMock.newsfeed, '6.8.2').subscribe(result => { + getApi(httpMock, configMock, '6.8.2').subscribe(result => { expect(result).toMatchInlineSnapshot(` Object { "error": null, @@ -568,7 +568,7 @@ describe('getApi', () => { }, ]; mockHttpGet.mockImplementationOnce(getHttpMockWithItems(mockApiItems)); - getApi(httpMock, configMock.newsfeed, '6.8.2').subscribe(result => { + getApi(httpMock, configMock, '6.8.2').subscribe(result => { expect(result).toMatchInlineSnapshot(` Object { "error": null, @@ -595,7 +595,7 @@ describe('getApi', () => { it('forwards an error', done => { mockHttpGet.mockImplementationOnce((arg1, arg2) => Promise.reject('sorry, try again later!')); - getApi(httpMock, configMock.newsfeed, '6.8.2').subscribe(result => { + getApi(httpMock, configMock, '6.8.2').subscribe(result => { expect(result).toMatchInlineSnapshot(` Object { "error": "sorry, try again later!", @@ -623,14 +623,14 @@ describe('getApi', () => { ]; it("retries until fetch doesn't error", done => { - configMock.newsfeed.mainInterval = 10; // fast retry for testing + configMock.mainInterval = moment.duration(10); // fast retry for testing mockHttpGet .mockImplementationOnce(() => Promise.reject('Sorry, try again later!')) .mockImplementationOnce(() => Promise.reject('Sorry, internal server error!')) .mockImplementationOnce(() => Promise.reject("Sorry, it's too cold to go outside!")) .mockImplementationOnce(getHttpMockWithItems(successItems)); - getApi(httpMock, configMock.newsfeed, '6.8.2') + getApi(httpMock, configMock, '6.8.2') .pipe(take(4), toArray()) .subscribe(result => { expect(result).toMatchInlineSnapshot(` @@ -677,13 +677,13 @@ describe('getApi', () => { }); it("doesn't retry if fetch succeeds", done => { - configMock.newsfeed.mainInterval = 10; // fast retry for testing + configMock.mainInterval = moment.duration(10); // fast retry for testing mockHttpGet.mockImplementation(getHttpMockWithItems(successItems)); const timeout$ = interval(1000); // lets us capture some results after a short time let timesFetched = 0; - const get$ = getApi(httpMock, configMock.newsfeed, '6.8.2').pipe( + const get$ = getApi(httpMock, configMock, '6.8.2').pipe( tap(() => { timesFetched++; }) diff --git a/src/plugins/newsfeed/public/lib/api.ts b/src/plugins/newsfeed/public/lib/api.ts index bfeff4aa3e37b..2924f3d340662 100644 --- a/src/plugins/newsfeed/public/lib/api.ts +++ b/src/plugins/newsfeed/public/lib/api.ts @@ -26,10 +26,10 @@ import { NEWSFEED_FALLBACK_LANGUAGE, NEWSFEED_LAST_FETCH_STORAGE_KEY, NEWSFEED_HASH_SET_STORAGE_KEY, -} from '../../constants'; -import { NewsfeedPluginInjectedConfig, ApiItem, NewsfeedItem, FetchResult } from '../../types'; +} from '../../common/constants'; +import { ApiItem, NewsfeedItem, FetchResult, NewsfeedPluginBrowserConfig } from '../types'; -type ApiConfig = NewsfeedPluginInjectedConfig['newsfeed']['service']; +type ApiConfig = NewsfeedPluginBrowserConfig['service']; export class NewsfeedApiDriver { private readonly loadedTime = moment().utc(); // the date is compared to time in UTC format coming from the service @@ -167,14 +167,14 @@ export class NewsfeedApiDriver { */ export function getApi( http: HttpSetup, - config: NewsfeedPluginInjectedConfig['newsfeed'], + config: NewsfeedPluginBrowserConfig, kibanaVersion: string ): Rx.Observable { - const userLanguage = i18n.getLocale() || config.defaultLanguage; - const fetchInterval = config.fetchInterval; + const userLanguage = i18n.getLocale(); + const fetchInterval = config.fetchInterval.asMilliseconds(); const driver = new NewsfeedApiDriver(kibanaVersion, userLanguage, fetchInterval); - return Rx.timer(0, config.mainInterval).pipe( + return Rx.timer(0, config.mainInterval.asMilliseconds()).pipe( filter(() => driver.shouldFetch()), mergeMap(() => driver.fetchNewsfeedItems(http, config.service).pipe( diff --git a/src/plugins/newsfeed/public/plugin.tsx b/src/plugins/newsfeed/public/plugin.tsx index d21cf75a1a65e..e61070ab184f3 100644 --- a/src/plugins/newsfeed/public/plugin.tsx +++ b/src/plugins/newsfeed/public/plugin.tsx @@ -21,9 +21,10 @@ import * as Rx from 'rxjs'; import { catchError, takeUntil } from 'rxjs/operators'; import ReactDOM from 'react-dom'; import React from 'react'; +import moment from 'moment'; import { I18nProvider } from '@kbn/i18n/react'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { FetchResult, NewsfeedPluginInjectedConfig } from '../types'; +import { NewsfeedPluginBrowserConfig } from './types'; import { NewsfeedNavButton, NewsfeedApiFetchResult } from './components/newsfeed_header_nav_button'; import { getApi } from './lib/api'; @@ -32,10 +33,18 @@ export type Start = object; export class NewsfeedPublicPlugin implements Plugin { private readonly kibanaVersion: string; + private readonly config: NewsfeedPluginBrowserConfig; private readonly stop$ = new Rx.ReplaySubject(1); - constructor(initializerContext: PluginInitializerContext) { + constructor(initializerContext: PluginInitializerContext) { this.kibanaVersion = initializerContext.env.packageInfo.version; + const config = initializerContext.config.get(); + this.config = Object.freeze({ + ...config, + // We need wrap them in moment.duration because exposeToBrowser stringifies it. + mainInterval: moment.duration(config.mainInterval), + fetchInterval: moment.duration(config.fetchInterval), + }); } public setup(core: CoreSetup): Setup { @@ -57,16 +66,8 @@ export class NewsfeedPublicPlugin implements Plugin { } private fetchNewsfeed(core: CoreStart) { - const { http, injectedMetadata } = core; - const config = injectedMetadata.getInjectedVar('newsfeed') as - | NewsfeedPluginInjectedConfig['newsfeed'] - | undefined; - - if (!config) { - // running in new platform, injected metadata not available - return new Rx.Observable(); - } - return getApi(http, config, this.kibanaVersion).pipe( + const { http } = core; + return getApi(http, this.config, this.kibanaVersion).pipe( takeUntil(this.stop$), // stop the interval when stop method is called catchError(() => Rx.of(null)) // do not throw error ); diff --git a/src/plugins/newsfeed/types.ts b/src/plugins/newsfeed/public/types.ts similarity index 80% rename from src/plugins/newsfeed/types.ts rename to src/plugins/newsfeed/public/types.ts index 78485c6ee4f59..a9dab6791d7aa 100644 --- a/src/plugins/newsfeed/types.ts +++ b/src/plugins/newsfeed/public/types.ts @@ -17,18 +17,16 @@ * under the License. */ -import { Moment } from 'moment'; +import { Duration, Moment } from 'moment'; -export interface NewsfeedPluginInjectedConfig { - newsfeed: { - service: { - urlRoot: string; - pathTemplate: string; - }; - defaultLanguage: string; - mainInterval: number; // how often to check last updated time - fetchInterval: number; // how often to fetch remote service and set last updated +// Ideally, we may want to obtain the type from the configSchema and exposeToBrowser keys... +export interface NewsfeedPluginBrowserConfig { + service: { + urlRoot: string; + pathTemplate: string; }; + mainInterval: Duration; // how often to check last updated time + fetchInterval: Duration; // how often to fetch remote service and set last updated } export interface ApiItem { diff --git a/src/plugins/newsfeed/server/config.ts b/src/plugins/newsfeed/server/config.ts new file mode 100644 index 0000000000000..e42066f4d20ec --- /dev/null +++ b/src/plugins/newsfeed/server/config.ts @@ -0,0 +1,44 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; +import { + NEWSFEED_DEFAULT_SERVICE_PATH, + NEWSFEED_DEFAULT_SERVICE_BASE_URL, + NEWSFEED_DEV_SERVICE_BASE_URL, + NEWSFEED_FALLBACK_LANGUAGE, +} from '../common/constants'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), + service: schema.object({ + pathTemplate: schema.string({ defaultValue: NEWSFEED_DEFAULT_SERVICE_PATH }), + urlRoot: schema.conditional( + schema.contextRef('prod'), + schema.literal(true), // Point to staging if it's not a production release + schema.string({ defaultValue: NEWSFEED_DEFAULT_SERVICE_BASE_URL }), + schema.string({ defaultValue: NEWSFEED_DEV_SERVICE_BASE_URL }) + ), + }), + defaultLanguage: schema.string({ defaultValue: NEWSFEED_FALLBACK_LANGUAGE }), // TODO: Deprecate since no longer used + mainInterval: schema.duration({ defaultValue: '2m' }), // (2min) How often to retry failed fetches, and/or check if newsfeed items need to be refreshed from remote + fetchInterval: schema.duration({ defaultValue: '1d' }), // (1day) How often to fetch remote and reset the last fetched time +}); + +export type NewsfeedConfigType = TypeOf; diff --git a/src/plugins/newsfeed/server/index.ts b/src/plugins/newsfeed/server/index.ts new file mode 100644 index 0000000000000..e62bb95df8862 --- /dev/null +++ b/src/plugins/newsfeed/server/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 { PluginConfigDescriptor } from 'kibana/server'; +import { NewsfeedPlugin } from './plugin'; +import { configSchema, NewsfeedConfigType } from './config'; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + exposeToBrowser: { + service: true, + mainInterval: true, + fetchInterval: true, + }, + deprecations: ({ unused }) => [unused('defaultLanguage')], +}; + +export function plugin() { + return new NewsfeedPlugin(); +} diff --git a/src/plugins/newsfeed/constants.ts b/src/plugins/newsfeed/server/plugin.ts similarity index 81% rename from src/plugins/newsfeed/constants.ts rename to src/plugins/newsfeed/server/plugin.ts index ddcbbb6cb1dbe..60b69b3977bf5 100644 --- a/src/plugins/newsfeed/constants.ts +++ b/src/plugins/newsfeed/server/plugin.ts @@ -17,6 +17,12 @@ * under the License. */ -export const NEWSFEED_FALLBACK_LANGUAGE = 'en'; -export const NEWSFEED_LAST_FETCH_STORAGE_KEY = 'newsfeed.lastfetchtime'; -export const NEWSFEED_HASH_SET_STORAGE_KEY = 'newsfeed.hashes'; +import { Plugin } from 'kibana/server'; + +export class NewsfeedPlugin implements Plugin { + public setup() {} + + public start() {} + + public stop() {} +} diff --git a/test/common/fixtures/plugins/newsfeed/kibana.json b/test/common/fixtures/plugins/newsfeed/kibana.json new file mode 100644 index 0000000000000..110b53fc6b2e9 --- /dev/null +++ b/test/common/fixtures/plugins/newsfeed/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "newsfeed-fixtures", + "version": "kibana", + "server": true, + "ui": false +} diff --git a/test/common/fixtures/plugins/newsfeed/newsfeed_simulation.ts b/test/common/fixtures/plugins/newsfeed/newsfeed_simulation.ts deleted file mode 100644 index 5aa44b48f9d59..0000000000000 --- a/test/common/fixtures/plugins/newsfeed/newsfeed_simulation.ts +++ /dev/null @@ -1,126 +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 Hapi from 'hapi'; - -interface WebhookRequest extends Hapi.Request { - payload: string; -} - -export async function initPlugin(server: Hapi.Server, path: string) { - server.route({ - method: ['GET'], - path: `${path}/kibana/v{version}.json`, - options: { - cors: { - origin: ['*'], - additionalHeaders: [ - 'Sec-Fetch-Mode', - 'Access-Control-Request-Method', - 'Access-Control-Request-Headers', - 'cache-control', - 'x-requested-with', - 'Origin', - 'User-Agent', - 'DNT', - 'content-type', - 'kbn-version', - ], - }, - }, - handler: newsfeedHandler as Hapi.Lifecycle.Method, - }); - - server.route({ - method: ['GET'], - path: `${path}/kibana/crash.json`, - options: { - cors: { - origin: ['*'], - additionalHeaders: [ - 'Sec-Fetch-Mode', - 'Access-Control-Request-Method', - 'Access-Control-Request-Headers', - 'cache-control', - 'x-requested-with', - 'Origin', - 'User-Agent', - 'DNT', - 'content-type', - 'kbn-version', - ], - }, - }, - handler() { - throw new Error('Internal server error'); - }, - }); -} - -function newsfeedHandler(request: WebhookRequest, h: any) { - return htmlResponse(h, 200, JSON.stringify(mockNewsfeed(request.params.version))); -} - -const mockNewsfeed = (version: string) => ({ - items: [ - { - title: { en: `You are functionally testing the newsfeed widget with fixtures!` }, - description: { en: 'See test/common/fixtures/plugins/newsfeed/newsfeed_simulation' }, - link_text: { en: 'Generic feed-viewer could go here' }, - link_url: { en: 'https://feeds.elastic.co' }, - languages: null, - badge: null, - image_url: null, - publish_on: '2019-06-21T00:00:00', - expire_on: '2040-01-31T00:00:00', - hash: '39ca7d409c7eb25f4c69a5a6a11309b2f5ced7ca3f9b3a0109517126e0fd91ca', - }, - { - title: { en: 'Staging too!' }, - description: { en: 'Hello world' }, - link_text: { en: 'Generic feed-viewer could go here' }, - link_url: { en: 'https://feeds-staging.elastic.co' }, - languages: null, - badge: null, - image_url: null, - publish_on: '2019-06-21T00:00:00', - expire_on: '2040-01-31T00:00:00', - hash: 'db445c9443eb50ea2eb15f20edf89cf0f7dac2b058b11cafc2c8c288b6e4ce2a', - }, - { - title: { en: 'This item is expired!' }, - description: { en: 'This should not show up.' }, - link_text: { en: 'Generic feed-viewer could go here' }, - link_url: { en: 'https://feeds-staging.elastic.co' }, - languages: null, - badge: null, - image_url: null, - publish_on: '2019-06-21T00:00:00', - expire_on: '2019-12-31T00:00:00', - hash: 'db445c9443eb50ea2eb15f20edf89cf0f7dac2b058b11cafc2c8c288b6e4ce2a', - }, - ], -}); - -function htmlResponse(h: any, code: number, text: string) { - return h - .response(text) - .type('application/json') - .code(code); -} diff --git a/test/common/fixtures/plugins/newsfeed/package.json b/test/common/fixtures/plugins/newsfeed/package.json deleted file mode 100644 index 5291b1031b0a9..0000000000000 --- a/test/common/fixtures/plugins/newsfeed/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "newsfeed-fixtures", - "version": "0.0.0", - "kibana": { - "version": "kibana" - } -} diff --git a/src/legacy/core_plugins/newsfeed/constants.ts b/test/common/fixtures/plugins/newsfeed/server/index.ts similarity index 76% rename from src/legacy/core_plugins/newsfeed/constants.ts rename to test/common/fixtures/plugins/newsfeed/server/index.ts index 55a0c51c2ac65..979aadfe86606 100644 --- a/src/legacy/core_plugins/newsfeed/constants.ts +++ b/test/common/fixtures/plugins/newsfeed/server/index.ts @@ -17,7 +17,9 @@ * under the License. */ -export const PLUGIN_ID = 'newsfeed'; -export const DEFAULT_SERVICE_URLROOT = 'https://feeds.elastic.co'; -export const DEV_SERVICE_URLROOT = 'https://feeds-staging.elastic.co'; -export const DEFAULT_SERVICE_PATH = '/kibana/v{VERSION}.json'; +import { PluginInitializerContext } from 'kibana/public'; +import { NewsFeedSimulatorPlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new NewsFeedSimulatorPlugin(initializerContext); +} diff --git a/test/common/fixtures/plugins/newsfeed/server/plugin.ts b/test/common/fixtures/plugins/newsfeed/server/plugin.ts new file mode 100644 index 0000000000000..60044d4e0b2df --- /dev/null +++ b/test/common/fixtures/plugins/newsfeed/server/plugin.ts @@ -0,0 +1,97 @@ +/* + * 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 { CoreSetup, Plugin } from 'kibana/server'; +import { PluginInitializerContext } from 'kibana/public'; + +export class NewsFeedSimulatorPlugin implements Plugin { + constructor(private readonly initializerContext: PluginInitializerContext) {} + + public setup({ http }: CoreSetup) { + const router = http.createRouter(); + const version = this.initializerContext.env.packageInfo.version; + + router.get( + { + path: `/api/_newsfeed-FTS-external-service-simulators/kibana/v${version}.json`, + validate: false, + options: { authRequired: false }, + }, + (context, req, res) => { + return res.ok({ body: this.mockNewsfeed() }); + } + ); + + router.get( + { + path: '/api/_newsfeed-FTS-external-service-simulators/kibana/crash.json', + validate: false, + options: { authRequired: false }, + }, + (context, req, res) => { + return res.internalError({ body: new Error('Internal server error') }); + } + ); + } + + public start() {} + + private mockNewsfeed() { + return { + items: [ + { + title: { en: `You are functionally testing the newsfeed widget with fixtures!` }, + description: { en: 'See test/common/fixtures/plugins/newsfeed/newsfeed_simulation' }, + link_text: { en: 'Generic feed-viewer could go here' }, + link_url: { en: 'https://feeds.elastic.co' }, + languages: null, + badge: null, + image_url: null, + publish_on: '2019-06-21T00:00:00', + expire_on: '2040-01-31T00:00:00', + hash: '39ca7d409c7eb25f4c69a5a6a11309b2f5ced7ca3f9b3a0109517126e0fd91ca', + }, + { + title: { en: 'Staging too!' }, + description: { en: 'Hello world' }, + link_text: { en: 'Generic feed-viewer could go here' }, + link_url: { en: 'https://feeds-staging.elastic.co' }, + languages: null, + badge: null, + image_url: null, + publish_on: '2019-06-21T00:00:00', + expire_on: '2040-01-31T00:00:00', + hash: 'db445c9443eb50ea2eb15f20edf89cf0f7dac2b058b11cafc2c8c288b6e4ce2a', + }, + { + title: { en: 'This item is expired!' }, + description: { en: 'This should not show up.' }, + link_text: { en: 'Generic feed-viewer could go here' }, + link_url: { en: 'https://feeds-staging.elastic.co' }, + languages: null, + badge: null, + image_url: null, + publish_on: '2019-06-21T00:00:00', + expire_on: '2019-12-31T00:00:00', + hash: 'db445c9443eb50ea2eb15f20edf89cf0f7dac2b058b11cafc2c8c288b6e4ce2a', + }, + ], + }; + } +} diff --git a/test/functional/apps/context/_context_navigation.js b/test/functional/apps/context/_context_navigation.js index b2312691599ba..42c67a37bd4bb 100644 --- a/test/functional/apps/context/_context_navigation.js +++ b/test/functional/apps/context/_context_navigation.js @@ -23,6 +23,10 @@ const TEST_COLUMN_NAMES = ['@message']; const TEST_FILTER_COLUMN_NAMES = [ ['extension', 'jpg'], ['geo.src', 'IN'], + [ + 'agent', + 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24', + ], ]; export default function({ getService, getPageObjects }) { @@ -56,7 +60,7 @@ export default function({ getService, getPageObjects }) { await browser.goBack(); await PageObjects.discover.waitForDocTableLoadingComplete(); const hitCount = await PageObjects.discover.getHitCount(); - expect(hitCount).to.be('1,556'); + expect(hitCount).to.be('522'); }); }); } diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index 4f1251321b005..75f8a3c156e01 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -8,12 +8,7 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) { const fileMockPath = `${kibanaDirectory}/src/dev/jest/mocks/file_mock.js`; return { rootDir: xPackKibanaDirectory, - roots: [ - '/plugins', - '/legacy/plugins', - '/legacy/server', - '/test_utils/jest/contract_tests', - ], + roots: ['/plugins', '/legacy/plugins', '/legacy/server'], moduleFileExtensions: ['js', 'json', 'ts', 'tsx'], moduleNameMapper: { '@elastic/eui$': `${kibanaDirectory}/node_modules/@elastic/eui/test-env`, diff --git a/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js index a462f4a504145..f1633799ea583 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js +++ b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js @@ -1,16 +1,10 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - module.exports = { - APM: { - 'Transaction duration charts': { - '1': '500 ms', - '2': '250 ms', - '3': '0 ms' + "APM": { + "Transaction duration charts": { + "1": "350 ms", + "2": "175 ms", + "3": "0 ms" } }, - __version: '4.2.0' -}; + "__version": "4.5.0" +} diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts index f58118f3352ea..7160d5524759d 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts @@ -28,6 +28,9 @@ Then(`should have correct y-axis ticks`, () => { const yAxisTick = '[data-cy=transaction-duration-charts] .rv-xy-plot__axis--vertical .rv-xy-plot__axis__tick__text'; + // wait for all loading to finish + cy.get('kbnLoadingIndicator').should('not.be.visible'); + cy.get(yAxisTick) .eq(2) .invoke('text') diff --git a/x-pack/plugins/apm/e2e/package.json b/x-pack/plugins/apm/e2e/package.json index 150ad5bb13d0b..1fa96b24d625d 100644 --- a/x-pack/plugins/apm/e2e/package.json +++ b/x-pack/plugins/apm/e2e/package.json @@ -9,21 +9,19 @@ }, "dependencies": { "@cypress/snapshot": "^2.1.3", - "@cypress/webpack-preprocessor": "^4.1.3", + "@cypress/webpack-preprocessor": "^5.2.0", "@types/cypress-cucumber-preprocessor": "^1.14.1", - "@types/js-yaml": "^3.12.1", - "@types/node": "^10.12.11", + "@types/node": "^14.0.1", "axios": "^0.19.2", - "cypress": "^4.2.0", - "cypress-cucumber-preprocessor": "^2.0.1", - "js-yaml": "^3.13.1", - "ora": "^4.0.3", - "p-limit": "^2.2.1", + "cypress": "^4.5.0", + "cypress-cucumber-preprocessor": "^2.3.1", + "ora": "^4.0.4", + "p-limit": "^2.3.0", "p-retry": "^4.2.0", - "ts-loader": "^6.2.2", - "typescript": "3.8.3", - "yargs": "^15.3.1", - "wait-on": "^4.0.1", - "webpack": "^4.42.1" + "ts-loader": "^7.0.4", + "typescript": "3.9.2", + "wait-on": "^5.0.0", + "webpack": "^4.43.0", + "yargs": "^15.3.1" } } diff --git a/x-pack/plugins/apm/e2e/run-e2e.sh b/x-pack/plugins/apm/e2e/run-e2e.sh index 818d45abb0e65..ebfd29377e013 100755 --- a/x-pack/plugins/apm/e2e/run-e2e.sh +++ b/x-pack/plugins/apm/e2e/run-e2e.sh @@ -111,7 +111,7 @@ fi ################################################## echo "\n${bold}Waiting for Kibana to start...${normal}" echo "Note: you need to start Kibana manually. Find the instructions at the top." -yarn wait-on -i 500 -w 500 http://localhost:$KIBANA_PORT > /dev/null +yarn wait-on -i 500 -w 500 http-get://admin:changeme@localhost:$KIBANA_PORT/api/status > /dev/null echo "\n✅ Setup completed successfully. Running tests...\n" diff --git a/x-pack/plugins/apm/e2e/yarn.lock b/x-pack/plugins/apm/e2e/yarn.lock index c023c64eb1cf4..a6729c56ecb09 100644 --- a/x-pack/plugins/apm/e2e/yarn.lock +++ b/x-pack/plugins/apm/e2e/yarn.lock @@ -9,15 +9,6 @@ dependencies: "@babel/highlight" "^7.8.3" -"@babel/compat-data@^7.8.4": - version "7.8.5" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.8.5.tgz#d28ce872778c23551cbb9432fc68d28495b613b9" - integrity sha512-jWYUqQX/ObOhG1UiEkbH5SANsE/8oKXiQWjj7p7xgj9Zmnt//aUvyz4dBkK0HNsS8/cbyC5NmmH87VekW+mXFg== - dependencies: - browserslist "^4.8.5" - invariant "^2.2.4" - semver "^5.5.0" - "@babel/core@7.4.5": version "7.4.5" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.5.tgz#081f97e8ffca65a9b4b0fdc7e274e703f000c06a" @@ -38,27 +29,6 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/core@^7.0.1": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.8.4.tgz#d496799e5c12195b3602d0fddd77294e3e38e80e" - integrity sha512-0LiLrB2PwrVI+a2/IEskBopDYSd8BCb3rOvH7D5tzoWd696TBEduBvuLVm4Nx6rltrLZqvI3MCalB2K2aVzQjA== - dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.8.4" - "@babel/helpers" "^7.8.4" - "@babel/parser" "^7.8.4" - "@babel/template" "^7.8.3" - "@babel/traverse" "^7.8.4" - "@babel/types" "^7.8.3" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.1" - json5 "^2.1.0" - lodash "^4.17.13" - resolve "^1.3.2" - semver "^5.4.1" - source-map "^0.5.0" - "@babel/generator@^7.4.4", "@babel/generator@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.8.4.tgz#35bbc74486956fe4251829f9f6c48330e8d0985e" @@ -101,17 +71,6 @@ "@babel/traverse" "^7.8.3" "@babel/types" "^7.8.3" -"@babel/helper-compilation-targets@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.8.4.tgz#03d7ecd454b7ebe19a254f76617e61770aed2c88" - integrity sha512-3k3BsKMvPp5bjxgMdrFyq0UaEO48HciVrOVF0+lon8pp95cyJ2ujAh0TrBHNMnJGT2rr0iKOJPFFbSqjDyf/Pg== - dependencies: - "@babel/compat-data" "^7.8.4" - browserslist "^4.8.5" - invariant "^2.2.4" - levenary "^1.1.1" - semver "^5.5.0" - "@babel/helper-create-class-features-plugin@^7.3.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.8.3.tgz#5b94be88c255f140fd2c10dd151e7f98f4bff397" @@ -263,7 +222,7 @@ "@babel/traverse" "^7.8.3" "@babel/types" "^7.8.3" -"@babel/helpers@^7.4.4", "@babel/helpers@^7.8.4": +"@babel/helpers@^7.4.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.8.4.tgz#754eb3ee727c165e0a240d6c207de7c455f36f73" integrity sha512-VPbe7wcQ4chu4TDQjimHv/5tj73qz88o12EPkO2ValS2QiQS/1F2SsjyIGNnAD0vF/nZS6Cf9i+vW6HIlnaR8w== @@ -286,7 +245,7 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.8.4.tgz#d1dbe64691d60358a974295fa53da074dd2ce8e8" integrity sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw== -"@babel/plugin-proposal-async-generator-functions@^7.2.0", "@babel/plugin-proposal-async-generator-functions@^7.8.3": +"@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.8.3.tgz#bad329c670b382589721b27540c7d288601c6e6f" integrity sha512-NZ9zLv848JsV3hs8ryEh7Uaz/0KsmPLqv0+PdkDJL1cJy0K4kOCFa8zc1E3mp+RHPQcpdfb/6GovEsW4VDrOMw== @@ -303,15 +262,7 @@ "@babel/helper-create-class-features-plugin" "^7.3.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-proposal-dynamic-import@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.8.3.tgz#38c4fe555744826e97e2ae930b0fb4cc07e66054" - integrity sha512-NyaBbyLFXFLT9FP+zk0kYlUlA8XtCUbehs67F0nnEg7KICgMc2mNkIeu9TYhKzyXMkrapZFwAhXLdnt4IYHy1w== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-dynamic-import" "^7.8.0" - -"@babel/plugin-proposal-json-strings@^7.2.0", "@babel/plugin-proposal-json-strings@^7.8.3": +"@babel/plugin-proposal-json-strings@^7.2.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.8.3.tgz#da5216b238a98b58a1e05d6852104b10f9a70d6b" integrity sha512-KGhQNZ3TVCQG/MjRbAUwuH+14y9q0tpxs1nWWs3pbSleRdDro9SAMMDyye8HhY1gqZ7/NqIc8SKhya0wRDgP1Q== @@ -319,14 +270,6 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-json-strings" "^7.8.0" -"@babel/plugin-proposal-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.8.3.tgz#e4572253fdeed65cddeecfdab3f928afeb2fd5d2" - integrity sha512-TS9MlfzXpXKt6YYomudb/KU7nQI6/xnapG6in1uZxoxDghuSMZsPb6D2fyUwNYSAp4l1iR7QtFOjkqcRYcUsfw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" - "@babel/plugin-proposal-object-rest-spread@7.3.2": version "7.3.2" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.2.tgz#6d1859882d4d778578e41f82cc5d7bf3d5daf6c1" @@ -335,7 +278,7 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-object-rest-spread" "^7.2.0" -"@babel/plugin-proposal-object-rest-spread@^7.4.4", "@babel/plugin-proposal-object-rest-spread@^7.8.3": +"@babel/plugin-proposal-object-rest-spread@^7.4.4": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.8.3.tgz#eb5ae366118ddca67bed583b53d7554cad9951bb" integrity sha512-8qvuPwU/xxUCt78HocNlv0mXXo0wdh9VT1R04WU8HGOfaOob26pF+9P5/lYjN/q7DHOX1bvX60hnhOvuQUJdbA== @@ -343,7 +286,7 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-object-rest-spread" "^7.8.0" -"@babel/plugin-proposal-optional-catch-binding@^7.2.0", "@babel/plugin-proposal-optional-catch-binding@^7.8.3": +"@babel/plugin-proposal-optional-catch-binding@^7.2.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.8.3.tgz#9dee96ab1650eed88646ae9734ca167ac4a9c5c9" integrity sha512-0gkX7J7E+AtAw9fcwlVQj8peP61qhdg/89D5swOkjYbkboA2CVckn3kiyum1DE0wskGb7KJJxBdyEBApDLLVdw== @@ -351,15 +294,7 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" -"@babel/plugin-proposal-optional-chaining@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.8.3.tgz#ae10b3214cb25f7adb1f3bc87ba42ca10b7e2543" - integrity sha512-QIoIR9abkVn+seDE3OjA08jWcs3eZ9+wJCKSRgo3WdEU2csFYgdScb+8qHB3+WXsGJD55u+5hWCISI7ejXS+kg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.0" - -"@babel/plugin-proposal-unicode-property-regex@^7.4.4", "@babel/plugin-proposal-unicode-property-regex@^7.8.3": +"@babel/plugin-proposal-unicode-property-regex@^7.4.4": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.3.tgz#b646c3adea5f98800c9ab45105ac34d06cd4a47f" integrity sha512-1/1/rEZv2XGweRwwSkLpY+s60za9OZ1hJs4YDqFHCw0kYWYwL5IFljVY1MYBL+weT1l9pokDO2uhSTLVxzoHkQ== @@ -374,13 +309,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-dynamic-import@^7.8.0": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" - integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - "@babel/plugin-syntax-json-strings@^7.2.0", "@babel/plugin-syntax-json-strings@^7.8.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" @@ -395,13 +323,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" - integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - "@babel/plugin-syntax-object-rest-spread@^7.2.0", "@babel/plugin-syntax-object-rest-spread@^7.8.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" @@ -416,28 +337,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-optional-chaining@^7.8.0": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-top-level-await@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.8.3.tgz#3acdece695e6b13aaf57fc291d1a800950c71391" - integrity sha512-kwj1j9lL/6Wd0hROD3b/OZZ7MSrZLqqn9RAZ5+cYYsflQ9HZBIKCUkr3+uL1MEJ1NePiUbf98jjiMQSv0NMR9g== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-transform-arrow-functions@^7.2.0", "@babel/plugin-transform-arrow-functions@^7.8.3": +"@babel/plugin-transform-arrow-functions@^7.2.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.8.3.tgz#82776c2ed0cd9e1a49956daeb896024c9473b8b6" integrity sha512-0MRF+KC8EqH4dbuITCWwPSzsyO3HIWWlm30v8BbbpOrS1B++isGxPnnuq/IZvOX5J2D/p7DQalQm+/2PnlKGxg== dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-async-to-generator@^7.4.4", "@babel/plugin-transform-async-to-generator@^7.8.3": +"@babel/plugin-transform-async-to-generator@^7.4.4": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.8.3.tgz#4308fad0d9409d71eafb9b1a6ee35f9d64b64086" integrity sha512-imt9tFLD9ogt56Dd5CI/6XgpukMwd/fLGSrix2httihVe7LOGVPhyhMh1BU5kDM7iHD08i8uUtmV2sWaBFlHVQ== @@ -446,14 +353,14 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/helper-remap-async-to-generator" "^7.8.3" -"@babel/plugin-transform-block-scoped-functions@^7.2.0", "@babel/plugin-transform-block-scoped-functions@^7.8.3": +"@babel/plugin-transform-block-scoped-functions@^7.2.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.8.3.tgz#437eec5b799b5852072084b3ae5ef66e8349e8a3" integrity sha512-vo4F2OewqjbB1+yaJ7k2EJFHlTP3jR634Z9Cj9itpqNjuLXvhlVxgnjsHsdRgASR8xYDrx6onw4vW5H6We0Jmg== dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-block-scoping@^7.4.4", "@babel/plugin-transform-block-scoping@^7.8.3": +"@babel/plugin-transform-block-scoping@^7.4.4": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.8.3.tgz#97d35dab66857a437c166358b91d09050c868f3a" integrity sha512-pGnYfm7RNRgYRi7bids5bHluENHqJhrV4bCZRwc5GamaWIIs07N4rZECcmJL6ZClwjDz1GbdMZFtPs27hTB06w== @@ -461,7 +368,7 @@ "@babel/helper-plugin-utils" "^7.8.3" lodash "^4.17.13" -"@babel/plugin-transform-classes@^7.4.4", "@babel/plugin-transform-classes@^7.8.3": +"@babel/plugin-transform-classes@^7.4.4": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.8.3.tgz#46fd7a9d2bb9ea89ce88720477979fe0d71b21b8" integrity sha512-SjT0cwFJ+7Rbr1vQsvphAHwUHvSUPmMjMU/0P59G8U2HLFqSa082JO7zkbDNWs9kH/IUqpHI6xWNesGf8haF1w== @@ -475,21 +382,21 @@ "@babel/helper-split-export-declaration" "^7.8.3" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.2.0", "@babel/plugin-transform-computed-properties@^7.8.3": +"@babel/plugin-transform-computed-properties@^7.2.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.8.3.tgz#96d0d28b7f7ce4eb5b120bb2e0e943343c86f81b" integrity sha512-O5hiIpSyOGdrQZRQ2ccwtTVkgUDBBiCuK//4RJ6UfePllUTCENOzKxfh6ulckXKc0DixTFLCfb2HVkNA7aDpzA== dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-destructuring@^7.4.4", "@babel/plugin-transform-destructuring@^7.8.3": +"@babel/plugin-transform-destructuring@^7.4.4": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.8.3.tgz#20ddfbd9e4676906b1056ee60af88590cc7aaa0b" integrity sha512-H4X646nCkiEcHZUZaRkhE2XVsoz0J/1x3VVujnn96pSoGCtKPA99ZZA+va+gK+92Zycd6OBKCD8tDb/731bhgQ== dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-dotall-regex@^7.4.4", "@babel/plugin-transform-dotall-regex@^7.8.3": +"@babel/plugin-transform-dotall-regex@^7.4.4": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.8.3.tgz#c3c6ec5ee6125c6993c5cbca20dc8621a9ea7a6e" integrity sha512-kLs1j9Nn4MQoBYdRXH6AeaXMbEJFaFu/v1nQkvib6QzTj8MZI5OQzqmD83/2jEM1z0DLilra5aWO5YpyC0ALIw== @@ -497,14 +404,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-duplicate-keys@^7.2.0", "@babel/plugin-transform-duplicate-keys@^7.8.3": +"@babel/plugin-transform-duplicate-keys@^7.2.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.8.3.tgz#8d12df309aa537f272899c565ea1768e286e21f1" integrity sha512-s8dHiBUbcbSgipS4SMFuWGqCvyge5V2ZeAWzR6INTVC3Ltjig/Vw1G2Gztv0vU/hRG9X8IvKvYdoksnUfgXOEQ== dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-exponentiation-operator@^7.2.0", "@babel/plugin-transform-exponentiation-operator@^7.8.3": +"@babel/plugin-transform-exponentiation-operator@^7.2.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.8.3.tgz#581a6d7f56970e06bf51560cd64f5e947b70d7b7" integrity sha512-zwIpuIymb3ACcInbksHaNcR12S++0MDLKkiqXHl3AzpgdKlFNhog+z/K0+TGW+b0w5pgTq4H6IwV/WhxbGYSjQ== @@ -512,14 +419,14 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-for-of@^7.4.4", "@babel/plugin-transform-for-of@^7.8.4": +"@babel/plugin-transform-for-of@^7.4.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.8.4.tgz#6fe8eae5d6875086ee185dd0b098a8513783b47d" integrity sha512-iAXNlOWvcYUYoV8YIxwS7TxGRJcxyl8eQCfT+A5j8sKUzRFvJdcyjp97jL2IghWSRDaL2PU2O2tX8Cu9dTBq5A== dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-function-name@^7.4.4", "@babel/plugin-transform-function-name@^7.8.3": +"@babel/plugin-transform-function-name@^7.4.4": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.8.3.tgz#279373cb27322aaad67c2683e776dfc47196ed8b" integrity sha512-rO/OnDS78Eifbjn5Py9v8y0aR+aSYhDhqAwVfsTl0ERuMZyr05L1aFSCJnbv2mmsLkit/4ReeQ9N2BgLnOcPCQ== @@ -527,21 +434,21 @@ "@babel/helper-function-name" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-literals@^7.2.0", "@babel/plugin-transform-literals@^7.8.3": +"@babel/plugin-transform-literals@^7.2.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.8.3.tgz#aef239823d91994ec7b68e55193525d76dbd5dc1" integrity sha512-3Tqf8JJ/qB7TeldGl+TT55+uQei9JfYaregDcEAyBZ7akutriFrt6C/wLYIer6OYhleVQvH/ntEhjE/xMmy10A== dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-member-expression-literals@^7.2.0", "@babel/plugin-transform-member-expression-literals@^7.8.3": +"@babel/plugin-transform-member-expression-literals@^7.2.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.8.3.tgz#963fed4b620ac7cbf6029c755424029fa3a40410" integrity sha512-3Wk2EXhnw+rP+IDkK6BdtPKsUE5IeZ6QOGrPYvw52NwBStw9V1ZVzxgK6fSKSxqUvH9eQPR3tm3cOq79HlsKYA== dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-modules-amd@^7.2.0", "@babel/plugin-transform-modules-amd@^7.8.3": +"@babel/plugin-transform-modules-amd@^7.2.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.8.3.tgz#65606d44616b50225e76f5578f33c568a0b876a5" integrity sha512-MadJiU3rLKclzT5kBH4yxdry96odTUwuqrZM+GllFI/VhxfPz+k9MshJM+MwhfkCdxxclSbSBbUGciBngR+kEQ== @@ -550,7 +457,7 @@ "@babel/helper-plugin-utils" "^7.8.3" babel-plugin-dynamic-import-node "^2.3.0" -"@babel/plugin-transform-modules-commonjs@7.8.3", "@babel/plugin-transform-modules-commonjs@^7.4.4", "@babel/plugin-transform-modules-commonjs@^7.8.3": +"@babel/plugin-transform-modules-commonjs@7.8.3", "@babel/plugin-transform-modules-commonjs@^7.4.4": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.8.3.tgz#df251706ec331bd058a34bdd72613915f82928a5" integrity sha512-JpdMEfA15HZ/1gNuB9XEDlZM1h/gF/YOH7zaZzQu2xCFRfwc01NXBMHHSTT6hRjlXJJs5x/bfODM3LiCk94Sxg== @@ -560,7 +467,7 @@ "@babel/helper-simple-access" "^7.8.3" babel-plugin-dynamic-import-node "^2.3.0" -"@babel/plugin-transform-modules-systemjs@^7.4.4", "@babel/plugin-transform-modules-systemjs@^7.8.3": +"@babel/plugin-transform-modules-systemjs@^7.4.4": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.8.3.tgz#d8bbf222c1dbe3661f440f2f00c16e9bb7d0d420" integrity sha512-8cESMCJjmArMYqa9AO5YuMEkE4ds28tMpZcGZB/jl3n0ZzlsxOAi3mC+SKypTfT8gjMupCnd3YiXCkMjj2jfOg== @@ -570,7 +477,7 @@ "@babel/helper-plugin-utils" "^7.8.3" babel-plugin-dynamic-import-node "^2.3.0" -"@babel/plugin-transform-modules-umd@^7.2.0", "@babel/plugin-transform-modules-umd@^7.8.3": +"@babel/plugin-transform-modules-umd@^7.2.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.8.3.tgz#592d578ce06c52f5b98b02f913d653ffe972661a" integrity sha512-evhTyWhbwbI3/U6dZAnx/ePoV7H6OUG+OjiJFHmhr9FPn0VShjwC2kdxqIuQ/+1P50TMrneGzMeyMTFOjKSnAw== @@ -578,21 +485,21 @@ "@babel/helper-module-transforms" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-named-capturing-groups-regex@^7.4.5", "@babel/plugin-transform-named-capturing-groups-regex@^7.8.3": +"@babel/plugin-transform-named-capturing-groups-regex@^7.4.5": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.8.3.tgz#a2a72bffa202ac0e2d0506afd0939c5ecbc48c6c" integrity sha512-f+tF/8UVPU86TrCb06JoPWIdDpTNSGGcAtaD9mLP0aYGA0OS0j7j7DHJR0GTFrUZPUU6loZhbsVZgTh0N+Qdnw== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.8.3" -"@babel/plugin-transform-new-target@^7.4.4", "@babel/plugin-transform-new-target@^7.8.3": +"@babel/plugin-transform-new-target@^7.4.4": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.8.3.tgz#60cc2ae66d85c95ab540eb34babb6434d4c70c43" integrity sha512-QuSGysibQpyxexRyui2vca+Cmbljo8bcRckgzYV4kRIsHpVeyeC3JDO63pY+xFZ6bWOBn7pfKZTqV4o/ix9sFw== dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-object-super@^7.2.0", "@babel/plugin-transform-object-super@^7.8.3": +"@babel/plugin-transform-object-super@^7.2.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.8.3.tgz#ebb6a1e7a86ffa96858bd6ac0102d65944261725" integrity sha512-57FXk+gItG/GejofIyLIgBKTas4+pEU47IXKDBWFTxdPd7F80H8zybyAY7UoblVfBhBGs2EKM+bJUu2+iUYPDQ== @@ -600,7 +507,7 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/helper-replace-supers" "^7.8.3" -"@babel/plugin-transform-parameters@^7.4.4", "@babel/plugin-transform-parameters@^7.8.4": +"@babel/plugin-transform-parameters@^7.4.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.8.4.tgz#1d5155de0b65db0ccf9971165745d3bb990d77d3" integrity sha512-IsS3oTxeTsZlE5KqzTbcC2sV0P9pXdec53SU+Yxv7o/6dvGM5AkTotQKhoSffhNgZ/dftsSiOoxy7evCYJXzVA== @@ -609,7 +516,7 @@ "@babel/helper-get-function-arity" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-property-literals@^7.2.0", "@babel/plugin-transform-property-literals@^7.8.3": +"@babel/plugin-transform-property-literals@^7.2.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.8.3.tgz#33194300d8539c1ed28c62ad5087ba3807b98263" integrity sha512-uGiiXAZMqEoQhRWMK17VospMZh5sXWg+dlh2soffpkAl96KAm+WZuJfa6lcELotSRmooLqg0MWdH6UUq85nmmg== @@ -648,14 +555,14 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-jsx" "^7.8.3" -"@babel/plugin-transform-regenerator@^7.4.5", "@babel/plugin-transform-regenerator@^7.8.3": +"@babel/plugin-transform-regenerator@^7.4.5": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.8.3.tgz#b31031e8059c07495bf23614c97f3d9698bc6ec8" integrity sha512-qt/kcur/FxrQrzFR432FGZznkVAjiyFtCOANjkAKwCbt465L6ZCiUQh2oMYGU3Wo8LRFJxNDFwWn106S5wVUNA== dependencies: regenerator-transform "^0.14.0" -"@babel/plugin-transform-reserved-words@^7.2.0", "@babel/plugin-transform-reserved-words@^7.8.3": +"@babel/plugin-transform-reserved-words@^7.2.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.8.3.tgz#9a0635ac4e665d29b162837dd3cc50745dfdf1f5" integrity sha512-mwMxcycN3omKFDjDQUl+8zyMsBfjRFr0Zn/64I41pmjv4NJuqcYlEtezwYtw9TFd9WR1vN5kiM+O0gMZzO6L0A== @@ -672,21 +579,21 @@ resolve "^1.8.1" semver "^5.5.1" -"@babel/plugin-transform-shorthand-properties@^7.2.0", "@babel/plugin-transform-shorthand-properties@^7.8.3": +"@babel/plugin-transform-shorthand-properties@^7.2.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.8.3.tgz#28545216e023a832d4d3a1185ed492bcfeac08c8" integrity sha512-I9DI6Odg0JJwxCHzbzW08ggMdCezoWcuQRz3ptdudgwaHxTjxw5HgdFJmZIkIMlRymL6YiZcped4TTCB0JcC8w== dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-spread@^7.2.0", "@babel/plugin-transform-spread@^7.8.3": +"@babel/plugin-transform-spread@^7.2.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.8.3.tgz#9c8ffe8170fdfb88b114ecb920b82fb6e95fe5e8" integrity sha512-CkuTU9mbmAoFOI1tklFWYYbzX5qCIZVXPVy0jpXgGwkplCndQAa58s2jr66fTeQnA64bDox0HL4U56CFYoyC7g== dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-sticky-regex@^7.2.0", "@babel/plugin-transform-sticky-regex@^7.8.3": +"@babel/plugin-transform-sticky-regex@^7.2.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.8.3.tgz#be7a1290f81dae767475452199e1f76d6175b100" integrity sha512-9Spq0vGCD5Bb4Z/ZXXSK5wbbLFMG085qd2vhL1JYu1WcQ5bXqZBAYRzU1d+p79GcHs2szYv5pVQCX13QgldaWw== @@ -694,7 +601,7 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/helper-regex" "^7.8.3" -"@babel/plugin-transform-template-literals@^7.4.4", "@babel/plugin-transform-template-literals@^7.8.3": +"@babel/plugin-transform-template-literals@^7.4.4": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.8.3.tgz#7bfa4732b455ea6a43130adc0ba767ec0e402a80" integrity sha512-820QBtykIQOLFT8NZOcTRJ1UNuztIELe4p9DCgvj4NK+PwluSJ49we7s9FB1HIGNIYT7wFUJ0ar2QpCDj0escQ== @@ -702,14 +609,14 @@ "@babel/helper-annotate-as-pure" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-typeof-symbol@^7.2.0", "@babel/plugin-transform-typeof-symbol@^7.8.4": +"@babel/plugin-transform-typeof-symbol@^7.2.0": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.8.4.tgz#ede4062315ce0aaf8a657a920858f1a2f35fc412" integrity sha512-2QKyfjGdvuNfHsb7qnBBlKclbD4CfshH2KvDabiijLMGXPHJXGxtDzwIF7bQP+T0ysw8fYTtxPafgfs/c1Lrqg== dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-unicode-regex@^7.4.4", "@babel/plugin-transform-unicode-regex@^7.8.3": +"@babel/plugin-transform-unicode-regex@^7.4.4": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.8.3.tgz#0cef36e3ba73e5c57273effb182f46b91a1ecaad" integrity sha512-+ufgJjYdmWfSQ+6NS9VGUR2ns8cjJjYbrbi11mZBTaWm+Fui/ncTLFF28Ei1okavY+xkojGr1eJxNsWYeA5aZw== @@ -771,69 +678,6 @@ js-levenshtein "^1.1.3" semver "^5.5.0" -"@babel/preset-env@^7.0.0": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.8.4.tgz#9dac6df5f423015d3d49b6e9e5fa3413e4a72c4e" - integrity sha512-HihCgpr45AnSOHRbS5cWNTINs0TwaR8BS8xIIH+QwiW8cKL0llV91njQMpeMReEPVs+1Ao0x3RLEBLtt1hOq4w== - dependencies: - "@babel/compat-data" "^7.8.4" - "@babel/helper-compilation-targets" "^7.8.4" - "@babel/helper-module-imports" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-proposal-async-generator-functions" "^7.8.3" - "@babel/plugin-proposal-dynamic-import" "^7.8.3" - "@babel/plugin-proposal-json-strings" "^7.8.3" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-proposal-object-rest-spread" "^7.8.3" - "@babel/plugin-proposal-optional-catch-binding" "^7.8.3" - "@babel/plugin-proposal-optional-chaining" "^7.8.3" - "@babel/plugin-proposal-unicode-property-regex" "^7.8.3" - "@babel/plugin-syntax-async-generators" "^7.8.0" - "@babel/plugin-syntax-dynamic-import" "^7.8.0" - "@babel/plugin-syntax-json-strings" "^7.8.0" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" - "@babel/plugin-syntax-object-rest-spread" "^7.8.0" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" - "@babel/plugin-syntax-optional-chaining" "^7.8.0" - "@babel/plugin-syntax-top-level-await" "^7.8.3" - "@babel/plugin-transform-arrow-functions" "^7.8.3" - "@babel/plugin-transform-async-to-generator" "^7.8.3" - "@babel/plugin-transform-block-scoped-functions" "^7.8.3" - "@babel/plugin-transform-block-scoping" "^7.8.3" - "@babel/plugin-transform-classes" "^7.8.3" - "@babel/plugin-transform-computed-properties" "^7.8.3" - "@babel/plugin-transform-destructuring" "^7.8.3" - "@babel/plugin-transform-dotall-regex" "^7.8.3" - "@babel/plugin-transform-duplicate-keys" "^7.8.3" - "@babel/plugin-transform-exponentiation-operator" "^7.8.3" - "@babel/plugin-transform-for-of" "^7.8.4" - "@babel/plugin-transform-function-name" "^7.8.3" - "@babel/plugin-transform-literals" "^7.8.3" - "@babel/plugin-transform-member-expression-literals" "^7.8.3" - "@babel/plugin-transform-modules-amd" "^7.8.3" - "@babel/plugin-transform-modules-commonjs" "^7.8.3" - "@babel/plugin-transform-modules-systemjs" "^7.8.3" - "@babel/plugin-transform-modules-umd" "^7.8.3" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.8.3" - "@babel/plugin-transform-new-target" "^7.8.3" - "@babel/plugin-transform-object-super" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.8.4" - "@babel/plugin-transform-property-literals" "^7.8.3" - "@babel/plugin-transform-regenerator" "^7.8.3" - "@babel/plugin-transform-reserved-words" "^7.8.3" - "@babel/plugin-transform-shorthand-properties" "^7.8.3" - "@babel/plugin-transform-spread" "^7.8.3" - "@babel/plugin-transform-sticky-regex" "^7.8.3" - "@babel/plugin-transform-template-literals" "^7.8.3" - "@babel/plugin-transform-typeof-symbol" "^7.8.4" - "@babel/plugin-transform-unicode-regex" "^7.8.3" - "@babel/types" "^7.8.3" - browserslist "^4.8.5" - core-js-compat "^3.6.2" - invariant "^2.2.2" - levenary "^1.1.1" - semver "^5.5.0" - "@babel/preset-react@7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.0.0.tgz#e86b4b3d99433c7b3e9e91747e2653958bc6b3c0" @@ -918,6 +762,32 @@ date-fns "^1.27.2" figures "^1.7.0" +"@cypress/request@2.88.5": + version "2.88.5" + resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.5.tgz#8d7ecd17b53a849cfd5ab06d5abe7d84976375d7" + integrity sha512-TzEC1XMi1hJkywWpRfD2clreTa/Z+lOrXDCxxBTBPEcY5azdPi56A6Xw+O4tWJnaJH3iIE7G5aDXZC6JgRZLcA== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + "@cypress/snapshot@^2.1.3": version "2.1.7" resolved "https://registry.yarnpkg.com/@cypress/snapshot/-/snapshot-2.1.7.tgz#e7360eb628b062f28f03036382619ec72cfb1831" @@ -932,17 +802,13 @@ snap-shot-compare "2.8.3" snap-shot-store "1.2.3" -"@cypress/webpack-preprocessor@^4.1.3": - version "4.1.3" - resolved "https://registry.yarnpkg.com/@cypress/webpack-preprocessor/-/webpack-preprocessor-4.1.3.tgz#d5fad767a304c16ec05ca08034827c601f1c9c0c" - integrity sha512-VtTzStrKtwyftLkcgopwCHzgjefK3uHHL6FgbAQP1o5N1pa/zYUb0g7hH2skrMAlKOmLGdbySlISkUl18Y3wHg== +"@cypress/webpack-preprocessor@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@cypress/webpack-preprocessor/-/webpack-preprocessor-5.2.0.tgz#3a17b478f6e2d600e536e6dda9c2e349d25a297e" + integrity sha512-uvo0FfKL+rIXrBGS6qPIaJRD8euK+t6YoZvrTuLPnStprzlgeGfSCnCDUEMJZqFk9LwBd1NtOop+J7qNuv74ng== dependencies: bluebird "3.7.1" debug "4.1.1" - optionalDependencies: - "@babel/core" "^7.0.1" - "@babel/preset-env" "^7.0.0" - babel-loader "^8.0.2" "@cypress/xvfb@1.2.4": version "1.2.4" @@ -952,10 +818,10 @@ debug "^3.1.0" lodash.once "^4.1.1" -"@hapi/address@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@hapi/address/-/address-4.0.0.tgz#36affb4509b5a6adc628bcc394450f2a7d51d111" - integrity sha512-GDDpkCdSUfkQCznmWUHh9dDN85BWf/V8TFKQ2JLuHdGB4Yy3YTEGBzZxoBNxfNBEvreSR/o+ZxBBSNNEVzY+lQ== +"@hapi/address@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@hapi/address/-/address-4.0.1.tgz#267301ddf7bc453718377a6fb3832a2f04a721dd" + integrity sha512-0oEP5UiyV4f3d6cBL8F3Z5S7iWSX39Knnl0lY8i+6gfmmIBj44JCBNtcMgwyS+5v7j3VYavNay0NFHDS+UGQcw== dependencies: "@hapi/hoek" "^9.0.0" @@ -969,12 +835,12 @@ resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.0.3.tgz#e49e637d5de8faa4f0d313c2590b455d7c00afd7" integrity sha512-jKtjLLDiH95b002sJVc5c74PE6KKYftuyVdVmsuYId5stTaWcRFqE+5ukZI4gDUKjGn8wv2C3zPn3/nyjEI7gg== -"@hapi/joi@^17.1.0": - version "17.1.0" - resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-17.1.0.tgz#cc4000b6c928a6a39b9bef092151b6bdee10ce55" - integrity sha512-ob67RcPlwRWxBzLCnWvcwx5qbwf88I3ykD7gcJLWOTRfLLgosK7r6aeChz4thA3XRvuBfI0KB1tPVl2EQFlPXw== +"@hapi/joi@^17.1.1": + version "17.1.1" + resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-17.1.1.tgz#9cc8d7e2c2213d1e46708c6260184b447c661350" + integrity sha512-p4DKeZAoeZW4g3u7ZeRo+vCDuSDgSvtsB/NpfjXEHTUjSeINAi/RrVOWiVQ1isaoLzMvFEhe8n5065mQq1AdQg== dependencies: - "@hapi/address" "^4.0.0" + "@hapi/address" "^4.0.1" "@hapi/formula" "^2.0.0" "@hapi/hoek" "^9.0.0" "@hapi/pinpoint" "^2.0.0" @@ -999,6 +865,34 @@ dependencies: any-observable "^0.3.0" +"@types/blob-util@1.3.3": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@types/blob-util/-/blob-util-1.3.3.tgz#adba644ae34f88e1dd9a5864c66ad651caaf628a" + integrity sha512-4ahcL/QDnpjWA2Qs16ZMQif7HjGP2cw3AGjHabybjw7Vm1EKu+cfQN1D78BaZbS1WJNa1opSMF5HNMztx7lR0w== + +"@types/bluebird@3.5.29": + version "3.5.29" + resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.29.tgz#7cd933c902c4fc83046517a1bef973886d00bdb6" + integrity sha512-kmVtnxTuUuhCET669irqQmPAez4KFnFVKvpleVRyfC3g+SHD1hIkFZcWLim9BVcwUBLO59o8VZE4yGCmTif8Yw== + +"@types/chai-jquery@1.1.40": + version "1.1.40" + resolved "https://registry.yarnpkg.com/@types/chai-jquery/-/chai-jquery-1.1.40.tgz#445bedcbbb2ae4e3027f46fa2c1733c43481ffa1" + integrity sha512-mCNEZ3GKP7T7kftKeIs7QmfZZQM7hslGSpYzKbOlR2a2HCFf9ph4nlMRA9UnuOETeOQYJVhJQK7MwGqNZVyUtQ== + dependencies: + "@types/chai" "*" + "@types/jquery" "*" + +"@types/chai@*": + version "4.2.11" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.11.tgz#d3614d6c5f500142358e6ed24e1bf16657536c50" + integrity sha512-t7uW6eFafjO+qJ3BIV2gGUyZs27egcNRkUdalkud+Qa3+kg//f129iuOFivHDXQ+vnU3fDXuwgv0cqMCbcE8sw== + +"@types/chai@4.2.7": + version "4.2.7" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.7.tgz#1c8c25cbf6e59ffa7d6b9652c78e547d9a41692d" + integrity sha512-luq8meHGYwvky0O7u0eQZdA7B4Wd9owUCqvbw2m3XCrCU8mplYOujMBbvyS547AxJkC+pGnd0Cm15eNxEUNU8g== + "@types/color-name@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" @@ -1009,22 +903,71 @@ resolved "https://registry.yarnpkg.com/@types/cypress-cucumber-preprocessor/-/cypress-cucumber-preprocessor-1.14.1.tgz#9787f4e89553ebc6359ce157a26ad51ed14aa98b" integrity sha512-CpYsiQ49UrOmadhFg0G5RkokPUmGGctD01mOWjNxFxHw5VgIRv33L2RyFHL8klaAI4HaedGN3Tcj4HTQ65hn+A== -"@types/js-yaml@^3.12.1": - version "3.12.2" - resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.2.tgz#a35a1809c33a68200fb6403d1ad708363c56470a" - integrity sha512-0CFu/g4mDSNkodVwWijdlr8jH7RoplRWNgovjFLEZeT+QEbbZXjBmCe3HwaWheAlCbHwomTwzZoSedeOycABug== +"@types/jquery@*": + version "3.3.38" + resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.38.tgz#6385f1e1b30bd2bff55ae8ee75ea42a999cc3608" + integrity sha512-nkDvmx7x/6kDM5guu/YpXkGZ/Xj/IwGiLDdKM99YA5Vag7pjGyTJ8BNUh/6hxEn/sEu5DKtyRgnONJ7EmOoKrA== + dependencies: + "@types/sizzle" "*" -"@types/node@^10.12.11": - version "10.17.14" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.14.tgz#b6c60ebf2fb5e4229fdd751ff9ddfae0f5f31541" - integrity sha512-G0UmX5uKEmW+ZAhmZ6PLTQ5eu/VPaT+d/tdLd5IFsKRPcbe6lPxocBtcYBFSaLaCW8O60AX90e91Nsp8lVHCNw== +"@types/jquery@3.3.31": + version "3.3.31" + resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.31.tgz#27c706e4bf488474e1cb54a71d8303f37c93451b" + integrity sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg== + dependencies: + "@types/sizzle" "*" + +"@types/lodash@4.14.149": + version "4.14.149" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440" + integrity sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ== + +"@types/minimatch@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" + integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== + +"@types/mocha@5.2.7": + version "5.2.7" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea" + integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ== + +"@types/node@^14.0.1": + version "14.0.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.1.tgz#5d93e0a099cd0acd5ef3d5bde3c086e1f49ff68c" + integrity sha512-FAYBGwC+W6F9+huFIDtn43cpy7+SzG+atzRiTfdp3inUKL2hXnd4rG8hylJLIh4+hqrQy1P17kvJByE/z825hA== "@types/retry@^0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== -"@types/sizzle@2.3.2": +"@types/sinon-chai@3.2.3": + version "3.2.3" + resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.3.tgz#afe392303dda95cc8069685d1e537ff434fa506e" + integrity sha512-TOUFS6vqS0PVL1I8NGVSNcFaNJtFoyZPXZ5zur+qlhDfOmQECZZM4H4kKgca6O8L+QceX/ymODZASfUfn+y4yQ== + dependencies: + "@types/chai" "*" + "@types/sinon" "*" + +"@types/sinon@*": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.0.tgz#5b70a360f55645dd64f205defd2a31b749a59799" + integrity sha512-v2TkYHkts4VXshMkcmot/H+ERZ2SevKa10saGaJPGCJ8vh3lKrC4u663zYEeRZxep+VbG6YRDtQ6gVqw9dYzPA== + dependencies: + "@types/sinonjs__fake-timers" "*" + +"@types/sinon@7.5.1": + version "7.5.1" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.5.1.tgz#d27b81af0d1cfe1f9b24eebe7a24f74ae40f5b7c" + integrity sha512-EZQUP3hSZQyTQRfiLqelC9NMWd1kqLcmQE0dMiklxBkgi84T+cHOhnKpgk4NnOWpGX863yE6+IaGnOXUNFqDnQ== + +"@types/sinonjs__fake-timers@*": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e" + integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA== + +"@types/sizzle@*", "@types/sizzle@2.3.2": version "2.3.2" resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== @@ -1216,10 +1159,10 @@ acorn-walk@^7.0.0: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.1.0.tgz#48387aa9a83bba67a9909164acab4bbc5796cf87" integrity sha512-4ufNLdC8gOf1dlOjC1nrn2NfzevyDtrDPp/DOtmoOHAFA/1pQc6bWf7oZ71qDURTODPLQ03+oFOvwxq5BvjXug== -acorn@^6.2.1: - version "6.4.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.0.tgz#b659d2ffbafa24baf5db1cdbb2c94a983ecd2784" - integrity sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw== +acorn@^6.4.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" + integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== acorn@^7.0.0: version "7.1.0" @@ -1436,16 +1379,6 @@ axios@^0.19.2: dependencies: follow-redirects "1.5.10" -babel-loader@^8.0.2: - version "8.0.6" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.0.6.tgz#e33bdb6f362b03f4bb141a0c21ab87c501b70dfb" - integrity sha512-4BmWKtBOBm13uoUwd08UwjZlaw3O9GWf456R9j+5YykFZ6LUIjIKLc0zEZf+hauxPOJs96C8k6FvYD09vWzhYw== - dependencies: - find-cache-dir "^2.0.0" - loader-utils "^1.0.2" - mkdirp "^0.5.1" - pify "^4.0.1" - babel-plugin-dynamic-import-node@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f" @@ -1760,7 +1693,7 @@ browserify@^16.1.0: vm-browserify "^1.0.0" xtend "^4.0.0" -browserslist@^4.6.0, browserslist@^4.8.3, browserslist@^4.8.5: +browserslist@^4.6.0, browserslist@^4.8.3: version "4.8.6" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.8.6.tgz#96406f3f5f0755d272e27a66f4163ca821590a7e" integrity sha512-ZHao85gf0eZ0ESxLfCp73GG9O/VTytYDIkIiZDlURppLTI9wErSM/5yAKEq6rcUdxBLjMELmrYUJGg5sxGKMHg== @@ -1917,7 +1850,7 @@ check-more-types@2.24.0: resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA= -chokidar@^2.0.2, chokidar@^2.0.4, chokidar@^2.1.1: +chokidar@^2.0.4, chokidar@^2.1.1, chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== @@ -2143,7 +2076,7 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@1.6.2, concat-stream@^1.5.0, concat-stream@^1.6.0, concat-stream@^1.6.1, concat-stream@~1.6.0: +concat-stream@^1.5.0, concat-stream@^1.6.0, concat-stream@^1.6.1, concat-stream@^1.6.2, concat-stream@~1.6.0: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== @@ -2171,7 +2104,7 @@ constants-browserify@^1.0.0, constants-browserify@~1.0.0: resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= -convert-source-map@^1.1.0, convert-source-map@^1.3.0, convert-source-map@^1.7.0: +convert-source-map@^1.1.0, convert-source-map@^1.3.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== @@ -2200,7 +2133,7 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= -core-js-compat@^3.1.1, core-js-compat@^3.6.2: +core-js-compat@^3.1.1: version "3.6.4" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.6.4.tgz#938476569ebb6cda80d339bcf199fae4f16fff17" integrity sha512-zAa3IZPvsJ0slViBQ2z+vgyyTuhd3MFn1rBQjZSKVEgB0UMYhUkCj9jJUVPgGTGqWvsBVmfnruXgTcNyTlEiSA== @@ -2345,10 +2278,10 @@ cyclist@^1.0.1: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= -cypress-cucumber-preprocessor@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/cypress-cucumber-preprocessor/-/cypress-cucumber-preprocessor-2.0.1.tgz#b6e64041efb4620ca559487152784bc044b35af5" - integrity sha512-i7WjLtv18O6/RoHeVLtfwNZmDL9DgPv4E8+6z5bQ4lqtBSjAKzRNrTnxAKAgUvaLK0TMeUm8GJu4eQ7B4LBqew== +cypress-cucumber-preprocessor@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/cypress-cucumber-preprocessor/-/cypress-cucumber-preprocessor-2.3.1.tgz#dc9dee8d59d3c787c5c70fc4271c32e95575b083" + integrity sha512-cKa7/VsOthzvdSQSdFiLwSWtBrtDE2q/qAPDL6NWOF4Tqm/AWvvOv18b9l9Z1t4SpphezR7RGnG1QIU45y9PPw== dependencies: "@cypress/browserify-preprocessor" "^2.1.1" chai "^4.1.2" @@ -2364,13 +2297,24 @@ cypress-cucumber-preprocessor@^2.0.1: minimist "^1.2.0" through "^2.3.8" -cypress@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.2.0.tgz#45673fb648b1a77b9a78d73e58b89ed05212d243" - integrity sha512-8LdreL91S/QiTCLYLNbIjLL8Ht4fJmu/4HGLxUI20Tc7JSfqEfCmXELrRfuPT0kjosJwJJZacdSji9XSRkPKUw== +cypress@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.5.0.tgz#01940d085f6429cec3c87d290daa47bb976a7c7b" + integrity sha512-2A4g5FW5d2fHzq8HKUGAMVTnW6P8nlWYQALiCoGN4bqBLvgwhYM/oG9oKc2CS6LnvgHFiKivKzpm9sfk3uU3zQ== dependencies: "@cypress/listr-verbose-renderer" "0.4.1" + "@cypress/request" "2.88.5" "@cypress/xvfb" "1.2.4" + "@types/blob-util" "1.3.3" + "@types/bluebird" "3.5.29" + "@types/chai" "4.2.7" + "@types/chai-jquery" "1.1.40" + "@types/jquery" "3.3.31" + "@types/lodash" "4.14.149" + "@types/minimatch" "3.0.3" + "@types/mocha" "5.2.7" + "@types/sinon" "7.5.1" + "@types/sinon-chai" "3.2.3" "@types/sizzle" "2.3.2" arch "2.1.1" bluebird "3.7.2" @@ -2384,7 +2328,7 @@ cypress@^4.2.0: eventemitter2 "4.1.2" execa "1.0.0" executable "4.1.1" - extract-zip "1.6.7" + extract-zip "1.7.0" fs-extra "8.1.0" getos "3.1.4" is-ci "2.0.0" @@ -2393,12 +2337,11 @@ cypress@^4.2.0: listr "0.14.3" lodash "4.17.15" log-symbols "3.0.0" - minimist "1.2.2" + minimist "1.2.5" moment "2.24.0" ospath "1.2.2" pretty-bytes "5.3.0" ramda "0.26.1" - request cypress-io/request#b5af0d1fa47eec97ba980cde90a13e69a2afcd16 request-progress "3.0.0" supports-color "7.1.0" tmp "0.1.0" @@ -2431,13 +2374,6 @@ date-fns@^1.27.2: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== -debug@2.6.9, debug@^2.2.0, debug@^2.3.3: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - debug@3.1.0, debug@=3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" @@ -2452,6 +2388,13 @@ debug@4.1.1, debug@^4.1.0: dependencies: ms "^2.1.1" +debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + debug@^3.0.1, debug@^3.1.0: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" @@ -2857,15 +2800,15 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" -extract-zip@1.6.7: - version "1.6.7" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9" - integrity sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k= +extract-zip@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" + integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== dependencies: - concat-stream "1.6.2" - debug "2.6.9" - mkdirp "0.5.1" - yauzl "2.4.1" + concat-stream "^1.6.2" + debug "^2.6.9" + mkdirp "^0.5.4" + yauzl "^2.10.0" extsprintf@1.3.0: version "1.3.0" @@ -2892,13 +2835,6 @@ fast-safe-stringify@^2.0.7: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== -fd-slicer@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" - integrity sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU= - dependencies: - pend "~1.2.0" - fd-slicer@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" @@ -2948,7 +2884,7 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -find-cache-dir@^2.0.0, find-cache-dir@^2.1.0: +find-cache-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== @@ -3077,11 +3013,6 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -gensync@^1.0.0-beta.1: - version "1.0.0-beta.1" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" - integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== - get-assigned-identifiers@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz#6dbf411de648cbaf8d9169ebb0d2d576191e2ff1" @@ -3358,7 +3289,7 @@ insert-module-globals@^7.0.0: undeclared-identifiers "^1.1.2" xtend "^4.0.0" -invariant@^2.2.2, invariant@^2.2.4: +invariant@^2.2.2: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== @@ -3622,7 +3553,7 @@ js-string-escape@^1.0.1: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.13.1, js-yaml@^3.9.0: +js-yaml@^3.9.0: version "3.13.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== @@ -3757,18 +3688,6 @@ lazy-ass@1.6.0: resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" integrity sha1-eZllXoZGwX8In90YfRUNMyTVRRM= -leven@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" - integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== - -levenary@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/levenary/-/levenary-1.1.1.tgz#842a9ee98d2075aa7faeedbe32679e9205f46f77" - integrity sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ== - dependencies: - leven "^3.1.0" - listr-silent-renderer@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e" @@ -4036,21 +3955,16 @@ minimist@0.0.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= -minimist@1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.2.tgz#b00a00230a1108c48c169e69a291aafda3aacd63" - integrity sha512-rIqbOrKb8GJmx/5bc2M0QchhUouMXSpd1RTclXsB41JdL+VtnojfaJR+h7F9k18/4kHUsBFgk80Uk+q569vjPA== +minimist@1.2.5, minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= -minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - mississippi@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" @@ -4075,7 +3989,7 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1: +mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= @@ -4089,6 +4003,13 @@ mkdirp@^0.5.3: dependencies: minimist "^1.2.5" +mkdirp@^0.5.4: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + module-deps@^6.0.0: version "6.2.2" resolved "https://registry.yarnpkg.com/module-deps/-/module-deps-6.2.2.tgz#d8a15c2265dfc119153c29bb47386987d0ee423b" @@ -4337,10 +4258,10 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" -ora@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/ora/-/ora-4.0.3.tgz#752a1b7b4be4825546a7a3d59256fa523b6b6d05" - integrity sha512-fnDebVFyz309A73cqCipVL1fBZewq4vwgSHfxh43vVy31mbyoQ8sCH3Oeaog/owYOs/lLlGVPCISQonTneg6Pg== +ora@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/ora/-/ora-4.0.4.tgz#e8da697cc5b6a47266655bf68e0fb588d29a545d" + integrity sha512-77iGeVU1cIdRhgFzCK8aw1fbtT1B/iZAvWjS+l/o1x0RShMgxHUZaD2yDpWsNCPwXg9z1ZA78Kbdvr8kBmG/Ww== dependencies: chalk "^3.0.0" cli-cursor "^3.1.0" @@ -4391,13 +4312,20 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= -p-limit@^2.0.0, p-limit@^2.2.0, p-limit@^2.2.1: +p-limit@^2.0.0, p-limit@^2.2.0: version "2.2.2" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e" integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ== dependencies: p-try "^2.0.0" +p-limit@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + p-locate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" @@ -4844,73 +4772,6 @@ request-progress@3.0.0: dependencies: throttleit "^1.0.0" -request-promise-core@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9" - integrity sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ== - dependencies: - lodash "^4.17.15" - -request-promise-native@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36" - integrity sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ== - dependencies: - request-promise-core "1.1.3" - stealthy-require "^1.1.1" - tough-cookie "^2.3.3" - -request@^2.88.0: - version "2.88.2" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" - integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - -request@cypress-io/request#b5af0d1fa47eec97ba980cde90a13e69a2afcd16: - version "2.88.1" - resolved "https://codeload.github.com/cypress-io/request/tar.gz/b5af0d1fa47eec97ba980cde90a13e69a2afcd16" - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -4999,13 +4860,20 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" -rxjs@^6.3.3, rxjs@^6.5.4: +rxjs@^6.3.3: version "6.5.4" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c" integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q== dependencies: tslib "^1.9.0" +rxjs@^6.5.5: + version "6.5.5" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.5.tgz#c5c884e3094c8cfee31bf27eb87e54ccfc87f9ec" + integrity sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ== + dependencies: + tslib "^1.9.0" + safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" @@ -5321,11 +5189,6 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" -stealthy-require@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" - integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= - stream-browserify@^2.0.0, stream-browserify@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" @@ -5632,7 +5495,7 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" -tough-cookie@^2.3.3, tough-cookie@~2.5.0: +tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== @@ -5640,10 +5503,10 @@ tough-cookie@^2.3.3, tough-cookie@~2.5.0: psl "^1.1.28" punycode "^2.1.1" -ts-loader@^6.2.2: - version "6.2.2" - resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-6.2.2.tgz#dffa3879b01a1a1e0a4b85e2b8421dc0dfff1c58" - integrity sha512-HDo5kXZCBml3EUPcc7RlZOV/JGlLHwppTLEHb3SHnr5V7NXD4klMEkrhJe5wgRbaWsSXi+Y1SIBN/K9B6zWGWQ== +ts-loader@^7.0.4: + version "7.0.4" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-7.0.4.tgz#5d9b95227de5afb91fdd9668f8920eb193cfe0cc" + integrity sha512-5du6OQHl+4ZjO4crEyoYUyWSrmmo7bAO+inkaILZ68mvahqrfoa4nn0DRmpQ4ruT4l+cuJCgF0xD7SBIyLeeow== dependencies: chalk "^2.3.0" enhanced-resolve "^4.0.0" @@ -5698,10 +5561,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@3.8.3: - version "3.8.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" - integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== +typescript@3.9.2: + version "3.9.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.2.tgz#64e9c8e9be6ea583c54607677dd4680a1cf35db9" + integrity sha512-q2ktq4n/uLuNNShyayit+DTobV2ApPEo/6so68JaD5ojvc/6GClBipedB9zNWYxRSAlZXAe405Rlijzl6qDiSw== umd@^3.0.0: version "3.0.3" @@ -5877,17 +5740,16 @@ vm-browserify@^1.0.0, vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== -wait-on@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-4.0.1.tgz#c49ca18b1ea60580404feed9df76ab3af2425a56" - integrity sha512-x83fmTH2X0KL7vXoGt9aV5x4SMCvO8A/NbwWpaYYh4NJ16d3KSgbHwBy9dVdHj0B30cEhOFRvDob4fnpUmZxvA== +wait-on@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-5.0.0.tgz#72e554b338490bbc7131362755ca1af04f46d029" + integrity sha512-6v9lttmGGRT7Lr16E/0rISTBIV1DN72n9+77Bpt1iBfzmhBI+75RDlacFe0Q+JizkmwWXmgHUcFG5cgx3Bwqzw== dependencies: - "@hapi/joi" "^17.1.0" + "@hapi/joi" "^17.1.1" + axios "^0.19.2" lodash "^4.17.15" - minimist "^1.2.0" - request "^2.88.0" - request-promise-native "^1.0.8" - rxjs "^6.5.4" + minimist "^1.2.5" + rxjs "^6.5.5" watchify@3.11.1: version "3.11.1" @@ -5902,12 +5764,12 @@ watchify@3.11.1: through2 "^2.0.0" xtend "^4.0.0" -watchpack@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" - integrity sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA== +watchpack@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.1.tgz#280da0a8718592174010c078c7585a74cd8cd0e2" + integrity sha512-+IF9hfUFOrYOOaKyfaI7h7dquUIOgyEMoQMLA7OP5FxegKA2+XdXThAZ9TU2kucfhDH7rfMHs1oPYziVGWRnZA== dependencies: - chokidar "^2.0.2" + chokidar "^2.1.8" graceful-fs "^4.1.2" neo-async "^2.5.0" @@ -5926,16 +5788,16 @@ webpack-sources@^1.4.0, webpack-sources@^1.4.1: source-list-map "^2.0.0" source-map "~0.6.1" -webpack@^4.42.1: - version "4.42.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.42.1.tgz#ae707baf091f5ca3ef9c38b884287cfe8f1983ef" - integrity sha512-SGfYMigqEfdGchGhFFJ9KyRpQKnipvEvjc1TwrXEPCM6H5Wywu10ka8o3KGrMzSMxMQKt8aCHUFh5DaQ9UmyRg== +webpack@^4.43.0: + version "4.43.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.43.0.tgz#c48547b11d563224c561dad1172c8aa0b8a678e6" + integrity sha512-GW1LjnPipFW2Y78OOab8NJlCflB7EFskMih2AHdvjbpKMeDJqEgSx24cXXXiPS65+WSwVyxtDsJH6jGX2czy+g== dependencies: "@webassemblyjs/ast" "1.9.0" "@webassemblyjs/helper-module-context" "1.9.0" "@webassemblyjs/wasm-edit" "1.9.0" "@webassemblyjs/wasm-parser" "1.9.0" - acorn "^6.2.1" + acorn "^6.4.1" ajv "^6.10.2" ajv-keywords "^3.4.1" chrome-trace-event "^1.0.2" @@ -5952,7 +5814,7 @@ webpack@^4.42.1: schema-utils "^1.0.0" tapable "^1.1.3" terser-webpack-plugin "^1.4.3" - watchpack "^1.6.0" + watchpack "^1.6.1" webpack-sources "^1.4.1" which-module@^2.0.0: @@ -6041,17 +5903,10 @@ yargs@^15.3.1: y18n "^4.0.0" yargs-parser "^18.1.1" -yauzl@2.10.0: +yauzl@2.10.0, yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= dependencies: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" - -yauzl@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" - integrity sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU= - dependencies: - fd-slicer "~1.0.1" diff --git a/x-pack/plugins/index_management/__jest__/components/__snapshots__/index_table.test.js.snap b/x-pack/plugins/index_management/__jest__/components/__snapshots__/index_table.test.js.snap index 1b8f28569fedc..38d97fbe08101 100644 --- a/x-pack/plugins/index_management/__jest__/components/__snapshots__/index_table.test.js.snap +++ b/x-pack/plugins/index_management/__jest__/components/__snapshots__/index_table.test.js.snap @@ -170,7 +170,7 @@ Array [ ] `; -exports[`index table show mapping button works from context menu 1`] = `"Mapping"`; +exports[`index table show mappings button works from context menu 1`] = `"Mappings"`; exports[`index table show settings button works from context menu 1`] = `"Settings"`; diff --git a/x-pack/plugins/index_management/__jest__/components/index_table.test.js b/x-pack/plugins/index_management/__jest__/components/index_table.test.js index 84fbc04aa5a31..5e3edc78da5a8 100644 --- a/x-pack/plugins/index_management/__jest__/components/index_table.test.js +++ b/x-pack/plugins/index_management/__jest__/components/index_table.test.js @@ -354,7 +354,7 @@ describe('index table', () => { test('show settings button works from context menu', () => { testEditor(0); }); - test('show mapping button works from context menu', () => { + test('show mappings button works from context menu', () => { testEditor(1); }); test('show stats button works from context menu', () => { diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/detail_panel.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/detail_panel.js index e0128b1d51d02..b891acee36e20 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/detail_panel.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/detail_panel.js @@ -43,7 +43,7 @@ const tabToHumanizedMap = { ), [TAB_MAPPING]: ( - + ), [TAB_STATS]: ( diff --git a/x-pack/plugins/index_management/server/routes/api/mapping/register_mapping_route.ts b/x-pack/plugins/index_management/server/routes/api/mapping/register_mapping_route.ts index 20d7e6b4d7232..35121af223207 100644 --- a/x-pack/plugins/index_management/server/routes/api/mapping/register_mapping_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/mapping/register_mapping_route.ts @@ -13,9 +13,9 @@ const paramsSchema = schema.object({ }); function formatHit(hit: { [key: string]: { mappings: any } }, indexName: string) { - const mapping = hit[indexName].mappings; + const mappings = hit[indexName].mappings; return { - mapping, + mappings, }; } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts index a63feda504e95..5e83a976bd7a4 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts @@ -9,18 +9,18 @@ import { createStream } from './agent'; describe('createStream', () => { it('should work', () => { const streamTemplate = ` - input: log - paths: - {{#each paths}} - - {{this}} - {{/each}} - exclude_files: [".gz$"] - processors: - - add_locale: ~ - password: {{password}} - {{#if password}} - hidden_password: {{password}} - {{/if}} +input: log +paths: +{{#each paths}} + - {{this}} +{{/each}} +exclude_files: [".gz$"] +processors: + - add_locale: ~ +password: {{password}} +{{#if password}} +hidden_password: {{password}} +{{/if}} `; const vars = { paths: { value: ['/usr/local/var/log/nginx/access.log'] }, @@ -39,13 +39,16 @@ describe('createStream', () => { it('should support yaml values', () => { const streamTemplate = ` - input: redis/metrics - metricsets: ["key"] - test: null - password: {{password}} - {{#if key.patterns}} - key.patterns: {{key.patterns}} - {{/if}} +input: redis/metrics +metricsets: ["key"] +test: null +password: {{password}} +{{custom}} +custom: {{ custom }} +{{#if key.patterns}} +key.patterns: {{key.patterns}} +{{/if}} +{{ testEmpty }} `; const vars = { 'key.patterns': { @@ -55,6 +58,12 @@ describe('createStream', () => { pattern: '*' `, }, + custom: { + type: 'yaml', + value: ` +foo: bar + `, + }, password: { type: 'password', value: '' }, }; @@ -70,6 +79,8 @@ describe('createStream', () => { }, ], password: '', + foo: 'bar', + custom: { foo: 'bar' }, }); }); }); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts index 35411a2e95acf..61f2f95fe20a9 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts @@ -5,9 +5,32 @@ */ import Handlebars from 'handlebars'; -import { safeLoad } from 'js-yaml'; +import { safeLoad, safeDump } from 'js-yaml'; import { DatasourceConfigRecord } from '../../../../common'; +export function createStream(variables: DatasourceConfigRecord, streamTemplate: string) { + const { vars, yamlValues } = buildTemplateVariables(variables, streamTemplate); + + const template = Handlebars.compile(streamTemplate, { noEscape: true }); + let stream = template(vars); + stream = replaceRootLevelYamlVariables(yamlValues, stream); + + const yamlFromStream = safeLoad(stream, {}); + + // Hack to keep empty string ('') values around in the end yaml because + // `safeLoad` replaces empty strings with null + const patchedYamlFromStream = Object.entries(yamlFromStream).reduce((acc, [key, value]) => { + if (value === null && typeof vars[key] === 'string' && vars[key].trim() === '') { + acc[key] = ''; + } else { + acc[key] = value; + } + return acc; + }, {} as { [k: string]: any }); + + return replaceVariablesInYaml(yamlValues, patchedYamlFromStream); +} + function isValidKey(key: string) { return key !== '__proto__' && key !== 'constructor' && key !== 'prototype'; } @@ -29,7 +52,7 @@ function replaceVariablesInYaml(yamlVariables: { [k: string]: any }, yaml: any) return yaml; } -function buildTemplateVariables(variables: DatasourceConfigRecord) { +function buildTemplateVariables(variables: DatasourceConfigRecord, streamTemplate: string) { const yamlValues: { [k: string]: any } = {}; const vars = Object.entries(variables).reduce((acc, [key, recordEntry]) => { // support variables with . like key.patterns @@ -64,23 +87,15 @@ function buildTemplateVariables(variables: DatasourceConfigRecord) { return { vars, yamlValues }; } -export function createStream(variables: DatasourceConfigRecord, streamTemplate: string) { - const { vars, yamlValues } = buildTemplateVariables(variables); - - const template = Handlebars.compile(streamTemplate, { noEscape: true }); - const stream = template(vars); - const yamlFromStream = safeLoad(stream, {}); +function replaceRootLevelYamlVariables(yamlVariables: { [k: string]: any }, yamlTemplate: string) { + if (Object.keys(yamlVariables).length === 0 || !yamlTemplate) { + return yamlTemplate; + } - // Hack to keep empty string ('') values around in the end yaml because - // `safeLoad` replaces empty strings with null - const patchedYamlFromStream = Object.entries(yamlFromStream).reduce((acc, [key, value]) => { - if (value === null && typeof vars[key] === 'string' && vars[key].trim() === '') { - acc[key] = ''; - } else { - acc[key] = value; - } - return acc; - }, {} as { [k: string]: any }); + let patchedTemplate = yamlTemplate; + Object.entries(yamlVariables).forEach(([key, val]) => { + patchedTemplate = patchedTemplate.replace(new RegExp(`^"${key}"`, 'gm'), safeDump(val)); + }); - return replaceVariablesInYaml(yamlValues, patchedYamlFromStream); + return patchedTemplate; } diff --git a/x-pack/plugins/lens/public/pie_visualization/register_expression.tsx b/x-pack/plugins/lens/public/pie_visualization/register_expression.tsx index 998d2162f7f5d..7babf7ed7ff46 100644 --- a/x-pack/plugins/lens/public/pie_visualization/register_expression.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/register_expression.tsx @@ -7,6 +7,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { i18n } from '@kbn/i18n'; +import { I18nProvider } from '@kbn/i18n/react'; import { PartialTheme } from '@elastic/charts'; import { IInterpreterRenderHandlers, @@ -111,13 +112,15 @@ export const getPieRenderer = (dependencies: { const executeTriggerActions = getExecuteTriggerActions(); const formatFactory = await dependencies.formatFactory; ReactDOM.render( - , + + + , domNode, () => { handlers.done(); diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx index bdc8004540bae..b0d4e0d2cc52b 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx @@ -10,6 +10,7 @@ import { shallow } from 'enzyme'; import { LensMultiTable } from '../types'; import { PieComponent } from './render_function'; import { PieExpressionArgs } from './types'; +import { EmptyPlaceholder } from '../shared_components'; describe('PieVisualization component', () => { let getFormatSpy: jest.Mock; @@ -109,5 +110,26 @@ describe('PieVisualization component', () => { ); expect(component.find(Settings).prop('legendMaxDepth')).toBeUndefined(); }); + + test('it shows emptyPlaceholder for undefined grouped data', () => { + const defaultData = getDefaultArgs().data; + const emptyData: LensMultiTable = { + ...defaultData, + tables: { + first: { + ...defaultData.tables.first, + rows: [ + { a: undefined, b: undefined, c: 'I', d: 'Row 1' }, + { a: undefined, b: undefined, c: 'J', d: 'Row 2' }, + ], + }, + }, + }; + + const component = shallow( + + ); + expect(component.find(EmptyPlaceholder).prop('icon')).toEqual('visPie'); + }); }); }); diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index 0c27a3e4b44e3..56019b3e6c891 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -31,6 +31,7 @@ import { CHART_NAMES, DEFAULT_PERCENT_DECIMALS } from './constants'; import { ColumnGroups, PieExpressionProps } from './types'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import { getSliceValueWithFallback, getFilterContext } from './render_helpers'; +import { EmptyPlaceholder } from '../shared_components'; import './visualization.scss'; const EMPTY_SLICE = Symbol('empty_slice'); @@ -201,27 +202,29 @@ export function PieComponent( const value = row[metricColumn.id]; return typeof value === 'number' && value < 0; }); - if (firstTable.rows.length === 0 || hasNegative) { + const isEmpty = + firstTable.rows.length === 0 || + firstTable.rows.every(row => + groups.every(colId => !row[colId] || typeof row[colId] === 'undefined') + ); + + if (isEmpty) { + return ; + } + + if (hasNegative) { return ( - {hasNegative ? ( - - ) : ( - - )} + ); } - return ( diff --git a/x-pack/plugins/lens/public/shared_components/empty_placeholder.tsx b/x-pack/plugins/lens/public/shared_components/empty_placeholder.tsx new file mode 100644 index 0000000000000..a2ea5c10de130 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/empty_placeholder.tsx @@ -0,0 +1,24 @@ +/* + * 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 React from 'react'; +import { EuiIcon, EuiText, IconType, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export const EmptyPlaceholder = (props: { icon: IconType }) => ( + <> + + + +

+ +

+
+ +); diff --git a/x-pack/plugins/ml/server/models/job_validation/messages.d.ts b/x-pack/plugins/lens/public/shared_components/index.ts similarity index 78% rename from x-pack/plugins/ml/server/models/job_validation/messages.d.ts rename to x-pack/plugins/lens/public/shared_components/index.ts index 772d78b4187dd..ad662fd7a59d9 100644 --- a/x-pack/plugins/ml/server/models/job_validation/messages.d.ts +++ b/x-pack/plugins/lens/public/shared_components/index.ts @@ -4,7 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface ValidationMessage { - id: string; - url: string; -} +export * from './empty_placeholder'; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx index d069a76f214d7..d598b9c740655 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -26,8 +26,7 @@ import { ExpressionFunctionDefinition, ExpressionValueSearchContext, } from 'src/plugins/expressions/public'; -import { EuiIcon, EuiText, IconType, EuiSpacer } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { IconType } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ValueClickTriggerContext, @@ -41,6 +40,7 @@ import { isHorizontalChart } from './state_helpers'; import { getExecuteTriggerActions } from '../services'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import { parseInterval } from '../../../../../src/plugins/data/common'; +import { EmptyPlaceholder } from '../shared_components'; type InferPropType = T extends React.FunctionComponent ? P : T; type SeriesSpec = InferPropType & @@ -193,18 +193,7 @@ export function XYChart({ if (filteredLayers.length === 0) { const icon: IconType = layers.length > 0 ? getIconForSeriesType(layers[0].seriesType) : 'bar'; - return ( - - - -

- -

-
- ); + return ; } // use formatting hint of first x axis column to format ticks diff --git a/x-pack/plugins/ml/server/models/job_validation/messages.js b/x-pack/plugins/ml/common/constants/messages.ts similarity index 88% rename from x-pack/plugins/ml/server/models/job_validation/messages.js rename to x-pack/plugins/ml/common/constants/messages.ts index 6cdbc457e6ade..b42eacbf7df5c 100644 --- a/x-pack/plugins/ml/server/models/job_validation/messages.js +++ b/x-pack/plugins/ml/common/constants/messages.ts @@ -4,21 +4,47 @@ * you may not use this file except in compliance with the Elastic License. */ +import { once } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { JOB_ID_MAX_LENGTH } from '../../../common/constants/validation'; +import { JOB_ID_MAX_LENGTH, VALIDATION_STATUS } from './validation'; -let messages; +export type MessageId = keyof ReturnType; -export const getMessages = () => { - if (messages) { - return messages; - } +export interface JobValidationMessageDef { + status: VALIDATION_STATUS; + text: string; + url?: string; + heading?: string; +} +export type JobValidationMessageId = + | MessageId + | 'model_memory_limit_invalid' + | 'model_memory_limit_valid' + | 'model_memory_limit_units_invalid' + | 'model_memory_limit_units_valid' + | 'query_delay_invalid' + | 'query_delay_valid' + | 'frequency_valid' + | 'frequency_invalid' + // because we have some spread around + | string; + +export type JobValidationMessage = { + id: JobValidationMessageId; + url?: string; + fieldName?: string; + modelPlotCardinality?: number; +} & { + [key: string]: any; +}; + +export const getMessages = once(() => { const createJobsDocsUrl = `https://www.elastic.co/guide/en/machine-learning/{{version}}/create-jobs.html`; - return (messages = { + return { field_not_aggregatable: { - status: 'ERROR', + status: VALIDATION_STATUS.ERROR, text: i18n.translate('xpack.ml.models.jobValidation.messages.fieldNotAggregatableMessage', { defaultMessage: 'Detector field {fieldName} is not an aggregatable field.', values: { @@ -29,7 +55,7 @@ export const getMessages = () => { 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-configuring-aggregation.html', }, fields_not_aggregatable: { - status: 'ERROR', + status: VALIDATION_STATUS.ERROR, text: i18n.translate('xpack.ml.models.jobValidation.messages.fieldsNotAggregatableMessage', { defaultMessage: 'One of the detector fields is not an aggregatable field.', }), @@ -37,7 +63,7 @@ export const getMessages = () => { 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-configuring-aggregation.html', }, cardinality_by_field: { - status: 'WARNING', + status: VALIDATION_STATUS.WARNING, text: i18n.translate('xpack.ml.models.jobValidation.messages.cardinalityByFieldMessage', { defaultMessage: 'Cardinality of {fieldName} is above 1000 and might result in high memory usage.', @@ -48,7 +74,7 @@ export const getMessages = () => { url: `${createJobsDocsUrl}#cardinality`, }, cardinality_over_field_low: { - status: 'WARNING', + status: VALIDATION_STATUS.WARNING, text: i18n.translate( 'xpack.ml.models.jobValidation.messages.cardinalityOverFieldLowMessage', { @@ -62,7 +88,7 @@ export const getMessages = () => { url: `${createJobsDocsUrl}#cardinality`, }, cardinality_over_field_high: { - status: 'WARNING', + status: VALIDATION_STATUS.WARNING, text: i18n.translate( 'xpack.ml.models.jobValidation.messages.cardinalityOverFieldHighMessage', { @@ -76,7 +102,7 @@ export const getMessages = () => { url: `${createJobsDocsUrl}#cardinality`, }, cardinality_partition_field: { - status: 'WARNING', + status: VALIDATION_STATUS.WARNING, text: i18n.translate( 'xpack.ml.models.jobValidation.messages.cardinalityPartitionFieldMessage', { @@ -90,7 +116,7 @@ export const getMessages = () => { url: `${createJobsDocsUrl}#cardinality`, }, cardinality_model_plot_high: { - status: 'WARNING', + status: VALIDATION_STATUS.WARNING, text: i18n.translate( 'xpack.ml.models.jobValidation.messages.cardinalityModelPlotHighMessage', { @@ -104,7 +130,7 @@ export const getMessages = () => { ), }, categorization_filters_valid: { - status: 'SUCCESS', + status: VALIDATION_STATUS.SUCCESS, text: i18n.translate( 'xpack.ml.models.jobValidation.messages.categorizationFiltersValidMessage', { @@ -115,7 +141,7 @@ export const getMessages = () => { 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-configuring-categories.html', }, categorization_filters_invalid: { - status: 'ERROR', + status: VALIDATION_STATUS.ERROR, text: i18n.translate( 'xpack.ml.models.jobValidation.messages.categorizationFiltersInvalidMessage', { @@ -131,7 +157,7 @@ export const getMessages = () => { 'https://www.elastic.co/guide/en/elasticsearch/reference/{{version}}/ml-job-resource.html#ml-analysisconfig', }, bucket_span_empty: { - status: 'ERROR', + status: VALIDATION_STATUS.ERROR, text: i18n.translate('xpack.ml.models.jobValidation.messages.bucketSpanEmptyMessage', { defaultMessage: 'The bucket span field must be specified.', }), @@ -139,7 +165,7 @@ export const getMessages = () => { 'https://www.elastic.co/guide/en/elasticsearch/reference/{{version}}/ml-job-resource.html#ml-analysisconfig', }, bucket_span_estimation_mismatch: { - status: 'INFO', + status: VALIDATION_STATUS.INFO, heading: i18n.translate( 'xpack.ml.models.jobValidation.messages.bucketSpanEstimationMismatchHeading', { @@ -160,7 +186,7 @@ export const getMessages = () => { url: `${createJobsDocsUrl}#bucket-span`, }, bucket_span_high: { - status: 'INFO', + status: VALIDATION_STATUS.INFO, heading: i18n.translate('xpack.ml.models.jobValidation.messages.bucketSpanHighHeading', { defaultMessage: 'Bucket span', }), @@ -171,7 +197,7 @@ export const getMessages = () => { url: `${createJobsDocsUrl}#bucket-span`, }, bucket_span_valid: { - status: 'SUCCESS', + status: VALIDATION_STATUS.SUCCESS, heading: i18n.translate('xpack.ml.models.jobValidation.messages.bucketSpanValidHeading', { defaultMessage: 'Bucket span', }), @@ -185,7 +211,7 @@ export const getMessages = () => { 'https://www.elastic.co/guide/en/elasticsearch/reference/{{version}}/ml-job-resource.html#ml-analysisconfig', }, bucket_span_invalid: { - status: 'ERROR', + status: VALIDATION_STATUS.ERROR, heading: i18n.translate('xpack.ml.models.jobValidation.messages.bucketSpanInvalidHeading', { defaultMessage: 'Bucket span', }), @@ -197,7 +223,7 @@ export const getMessages = () => { 'https://www.elastic.co/guide/en/elasticsearch/reference/{{version}}/ml-job-resource.html#ml-analysisconfig', }, detectors_duplicates: { - status: 'ERROR', + status: VALIDATION_STATUS.ERROR, text: i18n.translate('xpack.ml.models.jobValidation.messages.detectorsDuplicatesMessage', { defaultMessage: 'Duplicate detectors were found. Detectors having the same combined configuration for ' + @@ -214,21 +240,21 @@ export const getMessages = () => { url: `${createJobsDocsUrl}#detectors`, }, detectors_empty: { - status: 'ERROR', + status: VALIDATION_STATUS.ERROR, text: i18n.translate('xpack.ml.models.jobValidation.messages.detectorsEmptyMessage', { defaultMessage: 'No detectors were found. At least one detector must be specified.', }), url: `${createJobsDocsUrl}#detectors`, }, detectors_function_empty: { - status: 'ERROR', + status: VALIDATION_STATUS.ERROR, text: i18n.translate('xpack.ml.models.jobValidation.messages.detectorsFunctionEmptyMessage', { defaultMessage: 'One of the detector functions is empty.', }), url: `${createJobsDocsUrl}#detectors`, }, detectors_function_not_empty: { - status: 'SUCCESS', + status: VALIDATION_STATUS.SUCCESS, heading: i18n.translate( 'xpack.ml.models.jobValidation.messages.detectorsFunctionNotEmptyHeading', { @@ -244,19 +270,19 @@ export const getMessages = () => { url: `${createJobsDocsUrl}#detectors`, }, index_fields_invalid: { - status: 'ERROR', + status: VALIDATION_STATUS.ERROR, text: i18n.translate('xpack.ml.models.jobValidation.messages.indexFieldsInvalidMessage', { defaultMessage: 'Could not load fields from index.', }), }, index_fields_valid: { - status: 'SUCCESS', + status: VALIDATION_STATUS.SUCCESS, text: i18n.translate('xpack.ml.models.jobValidation.messages.indexFieldsValidMessage', { defaultMessage: 'Index fields are present in the datafeed.', }), }, influencer_high: { - status: 'WARNING', + status: VALIDATION_STATUS.WARNING, text: i18n.translate('xpack.ml.models.jobValidation.messages.influencerHighMessage', { defaultMessage: 'The job configuration includes more than 3 influencers. ' + @@ -265,7 +291,7 @@ export const getMessages = () => { url: 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-influencers.html', }, influencer_low: { - status: 'WARNING', + status: VALIDATION_STATUS.WARNING, text: i18n.translate('xpack.ml.models.jobValidation.messages.influencerLowMessage', { defaultMessage: 'No influencers have been configured. Picking an influencer is strongly recommended.', @@ -273,7 +299,7 @@ export const getMessages = () => { url: 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-influencers.html', }, influencer_low_suggestion: { - status: 'WARNING', + status: VALIDATION_STATUS.WARNING, text: i18n.translate( 'xpack.ml.models.jobValidation.messages.influencerLowSuggestionMessage', { @@ -285,7 +311,7 @@ export const getMessages = () => { url: 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-influencers.html', }, influencer_low_suggestions: { - status: 'WARNING', + status: VALIDATION_STATUS.WARNING, text: i18n.translate( 'xpack.ml.models.jobValidation.messages.influencerLowSuggestionsMessage', { @@ -297,7 +323,7 @@ export const getMessages = () => { url: 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-influencers.html', }, job_id_empty: { - status: 'ERROR', + status: VALIDATION_STATUS.ERROR, text: i18n.translate('xpack.ml.models.jobValidation.messages.jobIdEmptyMessage', { defaultMessage: 'Job ID field must not be empty.', }), @@ -305,7 +331,7 @@ export const getMessages = () => { 'https://www.elastic.co/guide/en/elasticsearch/reference/{{version}}/ml-job-resource.html#ml-job-resource', }, job_id_invalid: { - status: 'ERROR', + status: VALIDATION_STATUS.ERROR, text: i18n.translate('xpack.ml.models.jobValidation.messages.jobIdInvalidMessage', { defaultMessage: 'Job ID is invalid. It can contain lowercase alphanumeric (a-z and 0-9) characters, ' + @@ -315,7 +341,7 @@ export const getMessages = () => { 'https://www.elastic.co/guide/en/elasticsearch/reference/{{version}}/ml-job-resource.html#ml-job-resource', }, job_id_invalid_max_length: { - status: 'ERROR', + status: VALIDATION_STATUS.ERROR, text: i18n.translate( 'xpack.ml.models.jobValidation.messages.jobIdInvalidMaxLengthErrorMessage', { @@ -330,7 +356,7 @@ export const getMessages = () => { 'https://www.elastic.co/guide/en/elasticsearch/reference/{{version}}/ml-job-resource.html#ml-job-resource', }, job_id_valid: { - status: 'SUCCESS', + status: VALIDATION_STATUS.SUCCESS, heading: i18n.translate('xpack.ml.models.jobValidation.messages.jobIdValidHeading', { defaultMessage: 'Job ID format is valid', }), @@ -347,7 +373,7 @@ export const getMessages = () => { 'https://www.elastic.co/guide/en/elasticsearch/reference/{{version}}/ml-job-resource.html#ml-job-resource', }, job_group_id_invalid: { - status: 'ERROR', + status: VALIDATION_STATUS.ERROR, text: i18n.translate('xpack.ml.models.jobValidation.messages.jobGroupIdInvalidMessage', { defaultMessage: 'One of the job group names is invalid. They can contain lowercase ' + @@ -357,7 +383,7 @@ export const getMessages = () => { 'https://www.elastic.co/guide/en/elasticsearch/reference/{{version}}/ml-job-resource.html#ml-job-resource', }, job_group_id_invalid_max_length: { - status: 'ERROR', + status: VALIDATION_STATUS.ERROR, text: i18n.translate( 'xpack.ml.models.jobValidation.messages.jobGroupIdInvalidMaxLengthErrorMessage', { @@ -372,7 +398,7 @@ export const getMessages = () => { 'https://www.elastic.co/guide/en/elasticsearch/reference/{{version}}/ml-job-resource.html#ml-job-resource', }, job_group_id_valid: { - status: 'SUCCESS', + status: VALIDATION_STATUS.SUCCESS, heading: i18n.translate('xpack.ml.models.jobValidation.messages.jobGroupIdValidHeading', { defaultMessage: 'Job group id formats are valid', }), @@ -389,14 +415,14 @@ export const getMessages = () => { 'https://www.elastic.co/guide/en/elasticsearch/reference/{{version}}/ml-job-resource.html#ml-job-resource', }, skipped_extended_tests: { - status: 'WARNING', + status: VALIDATION_STATUS.WARNING, text: i18n.translate('xpack.ml.models.jobValidation.messages.skippedExtendedTestsMessage', { defaultMessage: 'Skipped additional checks because the basic requirements of the job configuration were not met.', }), }, success_cardinality: { - status: 'SUCCESS', + status: VALIDATION_STATUS.SUCCESS, heading: i18n.translate('xpack.ml.models.jobValidation.messages.successCardinalityHeading', { defaultMessage: 'Cardinality', }), @@ -406,7 +432,7 @@ export const getMessages = () => { url: `${createJobsDocsUrl}#cardinality`, }, success_bucket_span: { - status: 'SUCCESS', + status: VALIDATION_STATUS.SUCCESS, heading: i18n.translate('xpack.ml.models.jobValidation.messages.successBucketSpanHeading', { defaultMessage: 'Bucket span', }), @@ -417,14 +443,14 @@ export const getMessages = () => { url: `${createJobsDocsUrl}#bucket-span`, }, success_influencers: { - status: 'SUCCESS', + status: VALIDATION_STATUS.SUCCESS, text: i18n.translate('xpack.ml.models.jobValidation.messages.successInfluencersMessage', { defaultMessage: 'Influencer configuration passed the validation checks.', }), url: 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-influencers.html', }, estimated_mml_greater_than_max_mml: { - status: 'WARNING', + status: VALIDATION_STATUS.WARNING, text: i18n.translate( 'xpack.ml.models.jobValidation.messages.estimatedMmlGreaterThanMaxMmlMessage', { @@ -434,7 +460,7 @@ export const getMessages = () => { ), }, mml_greater_than_effective_max_mml: { - status: 'WARNING', + status: VALIDATION_STATUS.WARNING, text: i18n.translate( 'xpack.ml.models.jobValidation.messages.mmlGreaterThanEffectiveMaxMmlMessage', { @@ -445,14 +471,14 @@ export const getMessages = () => { ), }, mml_greater_than_max_mml: { - status: 'ERROR', + status: VALIDATION_STATUS.ERROR, text: i18n.translate('xpack.ml.models.jobValidation.messages.mmlGreaterThanMaxMmlMessage', { defaultMessage: 'The model memory limit is greater than the max model memory limit configured for this cluster.', }), }, mml_value_invalid: { - status: 'ERROR', + status: VALIDATION_STATUS.ERROR, text: i18n.translate('xpack.ml.models.jobValidation.messages.mmlValueInvalidMessage', { defaultMessage: '{mml} is not a valid value for model memory limit. The value needs to be at least ' + @@ -462,7 +488,7 @@ export const getMessages = () => { url: `${createJobsDocsUrl}#model-memory-limits`, }, half_estimated_mml_greater_than_mml: { - status: 'WARNING', + status: VALIDATION_STATUS.WARNING, text: i18n.translate( 'xpack.ml.models.jobValidation.messages.halfEstimatedMmlGreaterThanMmlMessage', { @@ -474,7 +500,7 @@ export const getMessages = () => { url: `${createJobsDocsUrl}#model-memory-limits`, }, estimated_mml_greater_than_mml: { - status: 'INFO', + status: VALIDATION_STATUS.INFO, text: i18n.translate( 'xpack.ml.models.jobValidation.messages.estimatedMmlGreaterThanMmlMessage', { @@ -485,7 +511,7 @@ export const getMessages = () => { url: `${createJobsDocsUrl}#model-memory-limits`, }, success_mml: { - status: 'SUCCESS', + status: VALIDATION_STATUS.SUCCESS, heading: i18n.translate('xpack.ml.models.jobValidation.messages.successMmlHeading', { defaultMessage: 'Model memory limit', }), @@ -495,7 +521,7 @@ export const getMessages = () => { url: `${createJobsDocsUrl}#model-memory-limits`, }, success_time_range: { - status: 'SUCCESS', + status: VALIDATION_STATUS.SUCCESS, heading: i18n.translate('xpack.ml.models.jobValidation.messages.successTimeRangeHeading', { defaultMessage: 'Time range', }), @@ -504,7 +530,7 @@ export const getMessages = () => { }), }, time_field_invalid: { - status: 'ERROR', + status: VALIDATION_STATUS.ERROR, text: i18n.translate('xpack.ml.models.jobValidation.messages.timeFieldInvalidMessage', { defaultMessage: `{timeField} cannot be used as the time field because it is not a field of type 'date' or 'date_nanos'.`, values: { @@ -513,7 +539,7 @@ export const getMessages = () => { }), }, time_range_short: { - status: 'WARNING', + status: VALIDATION_STATUS.WARNING, heading: i18n.translate('xpack.ml.models.jobValidation.messages.timeRangeShortHeading', { defaultMessage: 'Time range', }), @@ -528,7 +554,7 @@ export const getMessages = () => { }), }, time_range_before_epoch: { - status: 'WARNING', + status: VALIDATION_STATUS.WARNING, heading: i18n.translate( 'xpack.ml.models.jobValidation.messages.timeRangeBeforeEpochHeading', { @@ -541,5 +567,5 @@ export const getMessages = () => { 'the UNIX epoch beginning. Timestamps before 01/01/1970 00:00:00 (UTC) are not supported for machine learning jobs.', }), }, - }); -}; + }; +}); diff --git a/x-pack/plugins/ml/common/types/data_recognizer.ts b/x-pack/plugins/ml/common/types/data_recognizer.ts index 2f77cc8c33896..35566970d80ca 100644 --- a/x-pack/plugins/ml/common/types/data_recognizer.ts +++ b/x-pack/plugins/ml/common/types/data_recognizer.ts @@ -8,7 +8,7 @@ export interface JobStat { id: string; earliestTimestampMs: number; latestTimestampMs: number; - latestResultsTimestampMs: number; + latestResultsTimestampMs: number | undefined; } export interface JobExistResult { diff --git a/x-pack/plugins/ml/common/util/anomaly_utils.ts b/x-pack/plugins/ml/common/util/anomaly_utils.ts index 36b91f5580b39..16802040059a7 100644 --- a/x-pack/plugins/ml/common/util/anomaly_utils.ts +++ b/x-pack/plugins/ml/common/util/anomaly_utils.ts @@ -29,7 +29,7 @@ export enum ENTITY_FIELD_TYPE { export interface EntityField { fieldName: string; fieldValue: string | number | undefined; - fieldType: ENTITY_FIELD_TYPE; + fieldType?: ENTITY_FIELD_TYPE; } // List of function descriptions for which actual values from record level results should be displayed. diff --git a/x-pack/plugins/ml/common/util/job_utils.d.ts b/x-pack/plugins/ml/common/util/job_utils.d.ts deleted file mode 100644 index 170e42aabc67d..0000000000000 --- a/x-pack/plugins/ml/common/util/job_utils.d.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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 { Job } from '../types/anomaly_detection_jobs'; - -export interface ValidationMessage { - id: string; -} -export interface ValidationResults { - messages: ValidationMessage[]; - valid: boolean; - contains: (id: string) => boolean; - find: (id: string) => ValidationMessage | undefined; -} -export function calculateDatafeedFrequencyDefaultSeconds(bucketSpanSeconds: number): number; - -// TODO - use real types for job. Job interface first needs to move to a common location -export function isTimeSeriesViewJob(job: any): boolean; -export function basicJobValidation( - job: any, - fields: any[] | undefined, - limits: any, - skipMmlCheck?: boolean -): ValidationResults; - -export function basicDatafeedValidation(job: any): ValidationResults; - -export const ML_MEDIAN_PERCENTS: number; - -export const ML_DATA_PREVIEW_COUNT: number; - -export function isJobIdValid(jobId: string): boolean; - -export function validateModelMemoryLimitUnits( - modelMemoryLimit: string -): { valid: boolean; messages: any[]; contains: () => boolean; find: () => void }; - -export function processCreatedBy(customSettings: { created_by?: string }): void; - -export function mlFunctionToESAggregation(functionName: string): string | null; - -export function isModelPlotEnabled(job: Job, detectorIndex: number, entityFields: any[]): boolean; - -export function isModelPlotChartableForDetector(job: Job, detectorIndex: number): boolean; - -export function getSafeAggregationName(fieldName: string, index: number): string; - -export function getLatestDataOrBucketTimestamp( - latestDataTimestamp: number, - latestBucketTimestamp: number -): number; - -export function prefixDatafeedId(datafeedId: string, prefix: string): string; - -export function splitIndexPatternNames(indexPatternName: string): string[]; diff --git a/x-pack/plugins/ml/common/util/job_utils.test.js b/x-pack/plugins/ml/common/util/job_utils.test.ts similarity index 96% rename from x-pack/plugins/ml/common/util/job_utils.test.js rename to x-pack/plugins/ml/common/util/job_utils.test.ts index de269676a96ed..233e2c2cd19ac 100644 --- a/x-pack/plugins/ml/common/util/job_utils.test.js +++ b/x-pack/plugins/ml/common/util/job_utils.test.ts @@ -15,11 +15,11 @@ import { isJobVersionGte, mlFunctionToESAggregation, isJobIdValid, - ML_MEDIAN_PERCENTS, prefixDatafeedId, getSafeAggregationName, getLatestDataOrBucketTimestamp, } from './job_utils'; +import { CombinedJob, Job } from '../types/anomaly_detection_jobs'; describe('ML - job utils', () => { describe('calculateDatafeedFrequencyDefaultSeconds', () => { @@ -51,7 +51,7 @@ describe('ML - job utils', () => { describe('isTimeSeriesViewJob', () => { test('returns true when job has a single detector with a metric function', () => { - const job = { + const job = ({ analysis_config: { detectors: [ { @@ -61,13 +61,13 @@ describe('ML - job utils', () => { }, ], }, - }; + } as unknown) as CombinedJob; expect(isTimeSeriesViewJob(job)).toBe(true); }); test('returns true when job has at least one detector with a metric function', () => { - const job = { + const job = ({ analysis_config: { detectors: [ { @@ -83,13 +83,13 @@ describe('ML - job utils', () => { }, ], }, - }; + } as unknown) as CombinedJob; expect(isTimeSeriesViewJob(job)).toBe(true); }); test('returns false when job does not have at least one detector with a metric function', () => { - const job = { + const job = ({ analysis_config: { detectors: [ { @@ -105,13 +105,13 @@ describe('ML - job utils', () => { }, ], }, - }; + } as unknown) as CombinedJob; expect(isTimeSeriesViewJob(job)).toBe(false); }); test('returns false when job has a single count by category detector', () => { - const job = { + const job = ({ analysis_config: { detectors: [ { @@ -121,14 +121,14 @@ describe('ML - job utils', () => { }, ], }, - }; + } as unknown) as CombinedJob; expect(isTimeSeriesViewJob(job)).toBe(false); }); }); describe('isTimeSeriesViewDetector', () => { - const job = { + const job = ({ analysis_config: { detectors: [ { @@ -168,7 +168,7 @@ describe('ML - job utils', () => { }, }, }, - }; + } as unknown) as CombinedJob; test('returns true for a detector with a metric function', () => { expect(isTimeSeriesViewDetector(job, 0)).toBe(true); @@ -192,7 +192,7 @@ describe('ML - job utils', () => { }); describe('isSourceDataChartableForDetector', () => { - const job = { + const job = ({ analysis_config: { detectors: [ { function: 'count' }, // 0 @@ -251,7 +251,7 @@ describe('ML - job utils', () => { }, }, }, - }; + } as unknown) as CombinedJob; test('returns true for expected detectors', () => { expect(isSourceDataChartableForDetector(job, 0)).toBe(true); @@ -299,13 +299,13 @@ describe('ML - job utils', () => { }); describe('isModelPlotChartableForDetector', () => { - const job1 = { + const job1 = ({ analysis_config: { detectors: [{ function: 'count' }], }, - }; + } as unknown) as Job; - const job2 = { + const job2 = ({ analysis_config: { detectors: [ { function: 'count' }, @@ -319,7 +319,7 @@ describe('ML - job utils', () => { model_plot_config: { enabled: true, }, - }; + } as unknown) as Job; test('returns false when model plot is not enabled', () => { expect(isModelPlotChartableForDetector(job1, 0)).toBe(false); @@ -339,7 +339,7 @@ describe('ML - job utils', () => { }); describe('getPartitioningFieldNames', () => { - const job = { + const job = ({ analysis_config: { detectors: [ { @@ -367,7 +367,7 @@ describe('ML - job utils', () => { }, ], }, - }; + } as unknown) as CombinedJob; test('returns empty array for a detector with no partitioning fields', () => { const resp = getPartitioningFieldNames(job, 0); @@ -392,7 +392,7 @@ describe('ML - job utils', () => { describe('isModelPlotEnabled', () => { test('returns true for a job in which model plot has been enabled', () => { - const job = { + const job = ({ analysis_config: { detectors: [ { @@ -405,13 +405,13 @@ describe('ML - job utils', () => { model_plot_config: { enabled: true, }, - }; + } as unknown) as Job; expect(isModelPlotEnabled(job, 0)).toBe(true); }); test('returns expected values for a job in which model plot has been enabled with terms', () => { - const job = { + const job = ({ analysis_config: { detectors: [ { @@ -426,7 +426,7 @@ describe('ML - job utils', () => { enabled: true, terms: 'US,AAL', }, - }; + } as unknown) as Job; expect( isModelPlotEnabled(job, 0, [ @@ -450,7 +450,7 @@ describe('ML - job utils', () => { }); test('returns true for jobs in which model plot has not been enabled', () => { - const job1 = { + const job1 = ({ analysis_config: { detectors: [ { @@ -463,8 +463,8 @@ describe('ML - job utils', () => { model_plot_config: { enabled: false, }, - }; - const job2 = {}; + } as unknown) as CombinedJob; + const job2 = ({} as unknown) as CombinedJob; expect(isModelPlotEnabled(job1, 0)).toBe(false); expect(isModelPlotEnabled(job2, 0)).toBe(false); @@ -472,9 +472,9 @@ describe('ML - job utils', () => { }); describe('isJobVersionGte', () => { - const job = { + const job = ({ job_version: '6.1.1', - }; + } as unknown) as CombinedJob; test('returns true for later job version', () => { expect(isJobVersionGte(job, '6.1.0')).toBe(true); @@ -548,12 +548,6 @@ describe('ML - job utils', () => { }); }); - describe('ML_MEDIAN_PERCENTS', () => { - test("is '50.0'", () => { - expect(ML_MEDIAN_PERCENTS).toBe('50.0'); - }); - }); - describe('prefixDatafeedId', () => { test('returns datafeed-prefix-job from datafeed-job"', () => { expect(prefixDatafeedId('datafeed-job', 'prefix-')).toBe('datafeed-prefix-job'); diff --git a/x-pack/plugins/ml/common/util/job_utils.js b/x-pack/plugins/ml/common/util/job_utils.ts similarity index 68% rename from x-pack/plugins/ml/common/util/job_utils.js rename to x-pack/plugins/ml/common/util/job_utils.ts index 1217139872fc1..3822e54ddd53c 100644 --- a/x-pack/plugins/ml/common/util/job_utils.js +++ b/x-pack/plugins/ml/common/util/job_utils.ts @@ -6,15 +6,28 @@ import _ from 'lodash'; import semver from 'semver'; +// @ts-ignore import numeral from '@elastic/numeral'; import { ALLOWED_DATA_UNITS, JOB_ID_MAX_LENGTH } from '../constants/validation'; import { parseInterval } from './parse_interval'; import { maxLengthValidator } from './validators'; import { CREATED_BY_LABEL } from '../constants/new_job'; +import { CombinedJob, CustomSettings, Datafeed, JobId, Job } from '../types/anomaly_detection_jobs'; +import { EntityField } from './anomaly_utils'; +import { MlServerLimits } from '../types/ml_server_info'; +import { JobValidationMessage, JobValidationMessageId } from '../constants/messages'; +import { ES_AGGREGATION, ML_JOB_AGGREGATION } from '../constants/aggregation_types'; + +export interface ValidationResults { + valid: boolean; + messages: JobValidationMessage[]; + contains: (id: JobValidationMessageId) => boolean; + find: (id: JobValidationMessageId) => { id: JobValidationMessageId } | undefined; +} // work out the default frequency based on the bucket_span in seconds -export function calculateDatafeedFrequencyDefaultSeconds(bucketSpanSeconds) { +export function calculateDatafeedFrequencyDefaultSeconds(bucketSpanSeconds: number): number { let freq = 3600; if (bucketSpanSeconds <= 120) { freq = 60; @@ -29,40 +42,36 @@ export function calculateDatafeedFrequencyDefaultSeconds(bucketSpanSeconds) { // Returns a flag to indicate whether the job is suitable for viewing // in the Time Series dashboard. -export function isTimeSeriesViewJob(job) { +export function isTimeSeriesViewJob(job: CombinedJob): boolean { // only allow jobs with at least one detector whose function corresponds to // an ES aggregation which can be viewed in the single metric view and which // doesn't use a scripted field which can be very difficult or impossible to // invert to a reverse search, or when model plot has been enabled. - let isViewable = false; - const dtrs = job.analysis_config.detectors; - - for (let i = 0; i < dtrs.length; i++) { - isViewable = isTimeSeriesViewDetector(job, i); - if (isViewable === true) { - break; + for (let i = 0; i < job.analysis_config.detectors.length; i++) { + if (isTimeSeriesViewDetector(job, i)) { + return true; } } - return isViewable; + return false; } // Returns a flag to indicate whether the detector at the index in the specified job // is suitable for viewing in the Time Series dashboard. -export function isTimeSeriesViewDetector(job, dtrIndex) { +export function isTimeSeriesViewDetector(job: CombinedJob, detectorIndex: number): boolean { return ( - isSourceDataChartableForDetector(job, dtrIndex) || - isModelPlotChartableForDetector(job, dtrIndex) + isSourceDataChartableForDetector(job, detectorIndex) || + isModelPlotChartableForDetector(job, detectorIndex) ); } // Returns a flag to indicate whether the source data can be plotted in a time // series chart for the specified detector. -export function isSourceDataChartableForDetector(job, detectorIndex) { +export function isSourceDataChartableForDetector(job: CombinedJob, detectorIndex: number): boolean { let isSourceDataChartable = false; - const dtrs = job.analysis_config.detectors; - if (detectorIndex >= 0 && detectorIndex < dtrs.length) { - const dtr = dtrs[detectorIndex]; + const { detectors } = job.analysis_config; + if (detectorIndex >= 0 && detectorIndex < detectors.length) { + const dtr = detectors[detectorIndex]; const functionName = dtr.function; // Check that the function maps to an ES aggregation, @@ -79,15 +88,14 @@ export function isSourceDataChartableForDetector(job, detectorIndex) { // If the datafeed uses script fields, we can only plot the time series if // model plot is enabled. Without model plot it will be very difficult or impossible // to invert to a reverse search of the underlying metric data. - const usesScriptFields = _.has(job, 'datafeed_config.script_fields'); - if (isSourceDataChartable === true && usesScriptFields === true) { + if (isSourceDataChartable === true && typeof job.datafeed_config?.script_fields === 'object') { // Perform extra check to see if the detector is using a scripted field. - const scriptFields = usesScriptFields ? _.keys(job.datafeed_config.script_fields) : []; + const scriptFields = Object.keys(job.datafeed_config.script_fields); isSourceDataChartable = - scriptFields.indexOf(dtr.field_name) === -1 && - scriptFields.indexOf(dtr.partition_field_name) === -1 && - scriptFields.indexOf(dtr.by_field_name) === -1 && - scriptFields.indexOf(dtr.over_field_name) === -1; + scriptFields.indexOf(dtr.field_name!) === -1 && + scriptFields.indexOf(dtr.partition_field_name!) === -1 && + scriptFields.indexOf(dtr.by_field_name!) === -1 && + scriptFields.indexOf(dtr.over_field_name!) === -1; } } @@ -96,29 +104,29 @@ export function isSourceDataChartableForDetector(job, detectorIndex) { // Returns a flag to indicate whether model plot data can be plotted in a time // series chart for the specified detector. -export function isModelPlotChartableForDetector(job, detectorIndex) { +export function isModelPlotChartableForDetector(job: Job, detectorIndex: number): boolean { let isModelPlotChartable = false; - const modelPlotEnabled = _.get(job, ['model_plot_config', 'enabled'], false); - const dtrs = job.analysis_config.detectors; - if (detectorIndex >= 0 && detectorIndex < dtrs.length && modelPlotEnabled === true) { - const dtr = dtrs[detectorIndex]; - const functionName = dtr.function; + const modelPlotEnabled = job.model_plot_config?.enabled ?? false; + const { detectors } = job.analysis_config; + if (detectorIndex >= 0 && detectorIndex < detectors.length && modelPlotEnabled) { + const dtr = detectors[detectorIndex]; + const functionName = dtr.function as ML_JOB_AGGREGATION; // Model plot can be charted for any of the functions which map to ES aggregations // (except rare, for which no model plot results are generated), // plus varp and info_content functions. isModelPlotChartable = - functionName !== 'rare' && + functionName !== ML_JOB_AGGREGATION.RARE && (mlFunctionToESAggregation(functionName) !== null || [ - 'varp', - 'high_varp', - 'low_varp', - 'info_content', - 'high_info_content', - 'low_info_content', - ].includes(functionName) === true); + ML_JOB_AGGREGATION.VARP, + ML_JOB_AGGREGATION.HIGH_VARP, + ML_JOB_AGGREGATION.LOW_VARP, + ML_JOB_AGGREGATION.INFO_CONTENT, + ML_JOB_AGGREGATION.HIGH_INFO_CONTENT, + ML_JOB_AGGREGATION.LOW_INFO_CONTENT, + ].includes(functionName)); } return isModelPlotChartable; @@ -126,16 +134,16 @@ export function isModelPlotChartableForDetector(job, detectorIndex) { // Returns the names of the partition, by, and over fields for the detector with the // specified index from the supplied ML job configuration. -export function getPartitioningFieldNames(job, detectorIndex) { - const fieldNames = []; +export function getPartitioningFieldNames(job: CombinedJob, detectorIndex: number): string[] { + const fieldNames: string[] = []; const detector = job.analysis_config.detectors[detectorIndex]; - if (_.has(detector, 'partition_field_name')) { + if (typeof detector.partition_field_name === 'string') { fieldNames.push(detector.partition_field_name); } - if (_.has(detector, 'by_field_name')) { + if (typeof detector.by_field_name === 'string') { fieldNames.push(detector.by_field_name); } - if (_.has(detector, 'over_field_name')) { + if (typeof detector.over_field_name === 'string') { fieldNames.push(detector.over_field_name); } @@ -148,31 +156,41 @@ export function getPartitioningFieldNames(job, detectorIndex) { // the supplied entities contains 'by' and 'partition' fields in the detector, // if configured, whose values are in the configured model_plot_config terms, // where entityFields is in the format [{fieldName:status, fieldValue:404}]. -export function isModelPlotEnabled(job, detectorIndex, entityFields) { +export function isModelPlotEnabled( + job: Job, + detectorIndex: number, + entityFields?: EntityField[] +): boolean { // Check if model_plot_config is enabled. - let isEnabled = _.get(job, ['model_plot_config', 'enabled'], false); + let isEnabled = job.model_plot_config?.enabled ?? false; - if (isEnabled === true && entityFields !== undefined && entityFields.length > 0) { + if (isEnabled && entityFields !== undefined && entityFields.length > 0) { // If terms filter is configured in model_plot_config, check supplied entities. - const termsStr = _.get(job, ['model_plot_config', 'terms'], ''); + const termsStr = job.model_plot_config?.terms ?? ''; if (termsStr !== '') { // NB. Do not currently support empty string values as being valid 'by' or // 'partition' field values even though this is supported on the back-end. // If supplied, check both the by and partition entities are in the terms. const detector = job.analysis_config.detectors[detectorIndex]; - const detectorHasPartitionField = _.has(detector, 'partition_field_name'); - const detectorHasByField = _.has(detector, 'by_field_name'); + const detectorHasPartitionField = detector.hasOwnProperty('partition_field_name'); + const detectorHasByField = detector.hasOwnProperty('by_field_name'); const terms = termsStr.split(','); - if (detectorHasPartitionField === true) { - const partitionEntity = _.find(entityFields, { fieldName: detector.partition_field_name }); + if (detectorHasPartitionField) { + const partitionEntity = entityFields.find( + entityField => entityField.fieldName === detector.partition_field_name + ); isEnabled = - partitionEntity !== undefined && terms.indexOf(partitionEntity.fieldValue) !== -1; + partitionEntity?.fieldValue !== undefined && + terms.indexOf(String(partitionEntity.fieldValue)) !== -1; } if (isEnabled === true && detectorHasByField === true) { - const byEntity = _.find(entityFields, { fieldName: detector.by_field_name }); - isEnabled = byEntity !== undefined && terms.indexOf(byEntity.fieldValue) !== -1; + const byEntity = entityFields.find( + entityField => entityField.fieldName === detector.by_field_name + ); + isEnabled = + byEntity?.fieldValue !== undefined && terms.indexOf(String(byEntity.fieldValue)) !== -1; } } } @@ -182,8 +200,8 @@ export function isModelPlotEnabled(job, detectorIndex, entityFields) { // Returns whether the version of the job (the version number of the elastic stack that the job was // created with) is greater than or equal to the supplied version (e.g. '6.1.0'). -export function isJobVersionGte(job, version) { - const jobVersion = _.get(job, 'job_version', '0.0.0'); +export function isJobVersionGte(job: CombinedJob, version: string): boolean { + const jobVersion = job.job_version ?? '0.0.0'; return semver.gte(jobVersion, version); } @@ -191,60 +209,62 @@ export function isJobVersionGte(job, version) { // for querying metric data. Returns null if there is no suitable ES aggregation. // Note that the 'function' field in a record contains what the user entered e.g. 'high_count', // whereas the 'function_description' field holds an ML-built display hint for function e.g. 'count'. -export function mlFunctionToESAggregation(functionName) { +export function mlFunctionToESAggregation( + functionName: ML_JOB_AGGREGATION | string +): ES_AGGREGATION | null { if ( - functionName === 'mean' || - functionName === 'high_mean' || - functionName === 'low_mean' || - functionName === 'metric' + functionName === ML_JOB_AGGREGATION.MEAN || + functionName === ML_JOB_AGGREGATION.HIGH_MEAN || + functionName === ML_JOB_AGGREGATION.LOW_MEAN || + functionName === ML_JOB_AGGREGATION.METRIC ) { - return 'avg'; + return ES_AGGREGATION.AVG; } if ( - functionName === 'sum' || - functionName === 'high_sum' || - functionName === 'low_sum' || - functionName === 'non_null_sum' || - functionName === 'low_non_null_sum' || - functionName === 'high_non_null_sum' + functionName === ML_JOB_AGGREGATION.SUM || + functionName === ML_JOB_AGGREGATION.HIGH_SUM || + functionName === ML_JOB_AGGREGATION.LOW_SUM || + functionName === ML_JOB_AGGREGATION.NON_NULL_SUM || + functionName === ML_JOB_AGGREGATION.LOW_NON_NULL_SUM || + functionName === ML_JOB_AGGREGATION.HIGH_NON_NULL_SUM ) { - return 'sum'; + return ES_AGGREGATION.SUM; } if ( - functionName === 'count' || - functionName === 'high_count' || - functionName === 'low_count' || - functionName === 'non_zero_count' || - functionName === 'low_non_zero_count' || - functionName === 'high_non_zero_count' + functionName === ML_JOB_AGGREGATION.COUNT || + functionName === ML_JOB_AGGREGATION.HIGH_COUNT || + functionName === ML_JOB_AGGREGATION.LOW_COUNT || + functionName === ML_JOB_AGGREGATION.NON_ZERO_COUNT || + functionName === ML_JOB_AGGREGATION.LOW_NON_ZERO_COUNT || + functionName === ML_JOB_AGGREGATION.HIGH_NON_ZERO_COUNT ) { - return 'count'; + return ES_AGGREGATION.COUNT; } if ( - functionName === 'distinct_count' || - functionName === 'low_distinct_count' || - functionName === 'high_distinct_count' + functionName === ML_JOB_AGGREGATION.DISTINCT_COUNT || + functionName === ML_JOB_AGGREGATION.LOW_DISTINCT_COUNT || + functionName === ML_JOB_AGGREGATION.HIGH_DISTINCT_COUNT ) { - return 'cardinality'; + return ES_AGGREGATION.CARDINALITY; } if ( - functionName === 'median' || - functionName === 'high_median' || - functionName === 'low_median' + functionName === ML_JOB_AGGREGATION.MEDIAN || + functionName === ML_JOB_AGGREGATION.HIGH_MEDIAN || + functionName === ML_JOB_AGGREGATION.LOW_MEDIAN ) { - return 'percentiles'; + return ES_AGGREGATION.PERCENTILES; } - if (functionName === 'min' || functionName === 'max') { - return functionName; + if (functionName === ML_JOB_AGGREGATION.MIN || functionName === ML_JOB_AGGREGATION.MAX) { + return (functionName as unknown) as ES_AGGREGATION; } - if (functionName === 'rare') { - return 'count'; + if (functionName === ML_JOB_AGGREGATION.RARE) { + return ES_AGGREGATION.COUNT; } // Return null if ML function does not map to an ES aggregation. @@ -256,7 +276,7 @@ export function mlFunctionToESAggregation(functionName) { // Job name must contain lowercase alphanumeric (a-z and 0-9), hyphens or underscores; // it must also start and end with an alphanumeric character' -export function isJobIdValid(jobId) { +export function isJobIdValid(jobId: JobId): boolean { return /^[a-z0-9\-\_]+$/g.test(jobId) && !/^([_-].*)?(.*[_-])?$/g.test(jobId); } @@ -270,7 +290,7 @@ export const ML_MEDIAN_PERCENTS = '50.0'; export const ML_DATA_PREVIEW_COUNT = 10; // add a prefix to a datafeed id before the "datafeed-" part of the name -export function prefixDatafeedId(datafeedId, prefix) { +export function prefixDatafeedId(datafeedId: string, prefix: string): string { return datafeedId.match(/^datafeed-/) ? datafeedId.replace(/^datafeed-/, `datafeed-${prefix}`) : `datafeed-${prefix}${datafeedId}`; @@ -280,13 +300,13 @@ export function prefixDatafeedId(datafeedId, prefix) { // field name. Aggregation names must be alpha-numeric and can only contain '_' and '-' characters, // so if the supplied field names contains disallowed characters, the provided index // identifier is used to return a safe 'dummy' name in the format 'field_index' e.g. field_0, field_1 -export function getSafeAggregationName(fieldName, index) { +export function getSafeAggregationName(fieldName: string, index: number): string { return fieldName.match(/^[a-zA-Z0-9-_.]+$/) ? fieldName : `field_${index}`; } -export function uniqWithIsEqual(arr) { +export function uniqWithIsEqual(arr: T): T { return arr.reduce((dedupedArray, value) => { - if (dedupedArray.filter(compareValue => _.isEqual(compareValue, value)).length === 0) { + if (dedupedArray.filter((compareValue: any) => _.isEqual(compareValue, value)).length === 0) { dedupedArray.push(value); } return dedupedArray; @@ -296,8 +316,13 @@ export function uniqWithIsEqual(arr) { // check job without manipulating UI and return a list of messages // job and fields get passed as arguments and are not accessed as $scope.* via the outer scope // because the plan is to move this function to the common code area so that it can be used on the server side too. -export function basicJobValidation(job, fields, limits, skipMmlChecks = false) { - const messages = []; +export function basicJobValidation( + job: Job, + fields: object | undefined, + limits: MlServerLimits, + skipMmlChecks = false +): ValidationResults { + const messages: ValidationResults['messages'] = []; let valid = true; if (job) { @@ -459,8 +484,8 @@ export function basicJobValidation(job, fields, limits, skipMmlChecks = false) { }; } -export function basicDatafeedValidation(datafeed) { - const messages = []; +export function basicDatafeedValidation(datafeed: Datafeed): ValidationResults { + const messages: ValidationResults['messages'] = []; let valid = true; if (datafeed) { @@ -487,8 +512,8 @@ export function basicDatafeedValidation(datafeed) { }; } -export function validateModelMemoryLimit(job, limits) { - const messages = []; +export function validateModelMemoryLimit(job: Job, limits: MlServerLimits): ValidationResults { + const messages: ValidationResults['messages'] = []; let valid = true; // model memory limit if ( @@ -499,7 +524,9 @@ export function validateModelMemoryLimit(job, limits) { const max = limits.max_model_memory_limit.toUpperCase(); const mml = job.analysis_limits.model_memory_limit.toUpperCase(); + // @ts-ignore const mmlBytes = numeral(mml).value(); + // @ts-ignore const maxBytes = numeral(max).value(); if (mmlBytes > maxBytes) { @@ -518,8 +545,10 @@ export function validateModelMemoryLimit(job, limits) { }; } -export function validateModelMemoryLimitUnits(modelMemoryLimit) { - const messages = []; +export function validateModelMemoryLimitUnits( + modelMemoryLimit: string | undefined +): ValidationResults { + const messages: ValidationResults['messages'] = []; let valid = true; if (modelMemoryLimit !== undefined) { @@ -527,13 +556,14 @@ export function validateModelMemoryLimitUnits(modelMemoryLimit) { const mmlSplit = mml.match(/\d+(\w+)$/); const unit = mmlSplit && mmlSplit.length === 2 ? mmlSplit[1] : null; - if (ALLOWED_DATA_UNITS.indexOf(unit) === -1) { + if (unit === null || ALLOWED_DATA_UNITS.indexOf(unit) === -1) { messages.push({ id: 'model_memory_limit_units_invalid' }); valid = false; } else { messages.push({ id: 'model_memory_limit_units_valid' }); } } + return { valid, messages, @@ -542,9 +572,9 @@ export function validateModelMemoryLimitUnits(modelMemoryLimit) { }; } -export function validateGroupNames(job) { +export function validateGroupNames(job: Job): ValidationResults { const { groups = [] } = job; - const errorMessages = [ + const errorMessages: ValidationResults['messages'] = [ ...(groups.some(group => !isJobIdValid(group)) ? [{ id: 'job_group_id_invalid' }] : []), ...(groups.some(group => maxLengthValidator(JOB_ID_MAX_LENGTH)(group)) ? [{ id: 'job_group_id_invalid_max_length' }] @@ -561,18 +591,21 @@ export function validateGroupNames(job) { }; } -function isValidTimeFormat(value) { +function isValidTimeFormat(value: string | undefined): boolean { if (value === undefined) { return true; } - const interval = parseInterval(value, false); + const interval = parseInterval(value); return interval !== null && interval.asMilliseconds() !== 0; } // Returns the latest of the last source data and last processed bucket timestamp, // as used for example in setting the end time of results views for cases where // anomalies might have been raised after the point at which data ingest has stopped. -export function getLatestDataOrBucketTimestamp(latestDataTimestamp, latestBucketTimestamp) { +export function getLatestDataOrBucketTimestamp( + latestDataTimestamp: number | undefined, + latestBucketTimestamp: number | undefined +): number | undefined { if (latestDataTimestamp !== undefined && latestBucketTimestamp !== undefined) { return Math.max(latestDataTimestamp, latestBucketTimestamp); } else { @@ -585,13 +618,13 @@ export function getLatestDataOrBucketTimestamp(latestDataTimestamp, latestBucket * it was created by a job wizard as the rules cannot currently be edited * in the job wizards and so would be lost in a clone. */ -export function processCreatedBy(customSettings) { - if (Object.values(CREATED_BY_LABEL).includes(customSettings.created_by)) { +export function processCreatedBy(customSettings: CustomSettings) { + if (Object.values(CREATED_BY_LABEL).includes(customSettings.created_by!)) { delete customSettings.created_by; } } -export function splitIndexPatternNames(indexPatternName) { +export function splitIndexPatternNames(indexPatternName: string): string[] { return indexPatternName.includes(',') ? indexPatternName.split(',').map(i => i.trim()) : [indexPatternName]; diff --git a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx index decd1275fe884..3ca191d6251cf 100644 --- a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx +++ b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx @@ -96,6 +96,11 @@ const Tooltip: FC<{ service: ChartTooltipService }> = React.memo(({ service }) = return ( ( export const progressColumn = { name: i18n.translate('xpack.ml.dataframe.analyticsList.progress', { - defaultMessage: 'Progress per Step', + defaultMessage: 'Progress', }), sortable: (item: DataFrameAnalyticsListRow) => getDataFrameAnalyticsProgress(item.stats), truncateText: true, render(item: DataFrameAnalyticsListRow) { - const totalSteps = item.stats.progress.length; - let step = 0; - let progress = 0; - - for (const progressStep of item.stats.progress) { - step++; - progress = progressStep.progress_percent; - if (progressStep.progress_percent < 100) { - break; - } - } + const { currentPhase, progress, totalPhases } = getDataFrameAnalyticsProgressPhase(item.stats); // For now all analytics jobs are batch jobs. const isBatchTransform = true; @@ -96,25 +87,30 @@ export const progressColumn = { {isBatchTransform && ( - - - {progress}% - - - - {`${progress}%`} - - + - {step}/{totalSteps} + Phase {currentPhase}/{totalPhases} + + + + + )} {!isBatchTransform && ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.test.ts index 19a3857f3f71c..20cfad2b02c2c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.test.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.test.ts @@ -7,6 +7,8 @@ import StatsMock from './__mocks__/analytics_stats.json'; import { + getDataFrameAnalyticsProgress, + getDataFrameAnalyticsProgressPhase, isCompletedAnalyticsJob, isDataFrameAnalyticsRunning, isDataFrameAnalyticsStats, @@ -17,13 +19,15 @@ import { const completedJob = StatsMock.data_frame_analytics[0] as DataFrameAnalyticsStats; const runningJob = StatsMock.data_frame_analytics[1] as DataFrameAnalyticsStats; -describe('Data Frame Analytics: common utils', () => { - test('isCompletedAnalyticsJob()', () => { +describe('Data Frame Analytics: isCompletedAnalyticsJob()', () => { + test('should report if job is completed', () => { expect(isCompletedAnalyticsJob(completedJob)).toBe(true); expect(isCompletedAnalyticsJob(runningJob)).toBe(false); }); +}); - test('isDataFrameAnalyticsRunning()', () => { +describe('Data Frame Analytics: isDataFrameAnalyticsRunning()', () => { + test('should report if job is running', () => { expect(isDataFrameAnalyticsRunning(completedJob.state)).toBe(false); expect(isDataFrameAnalyticsRunning(runningJob.state)).toBe(true); runningJob.state = DATA_FRAME_TASK_STATE.STARTED; @@ -35,11 +39,35 @@ describe('Data Frame Analytics: common utils', () => { runningJob.state = DATA_FRAME_TASK_STATE.FAILED; expect(isDataFrameAnalyticsRunning(runningJob.state)).toBe(false); }); +}); - test('isDataFrameAnalyticsStats()', () => { +describe('Data Frame Analytics: isDataFrameAnalyticsStats()', () => { + test('should return if valid analytics stats', () => { expect(isDataFrameAnalyticsStats(completedJob)).toBe(true); expect(isDataFrameAnalyticsStats(runningJob)).toBe(true); expect(isDataFrameAnalyticsStats({})).toBe(false); expect(isDataFrameAnalyticsStats('no-object')).toBe(false); }); }); + +describe('Data Frame Analytics: getDataFrameAnalyticsProgress()', () => { + test('should report overall job progress percentage', () => { + expect(getDataFrameAnalyticsProgress(completedJob)).toBe(100); + expect(getDataFrameAnalyticsProgress(runningJob)).toBe(59); + }); +}); + +describe('Data Frame Analytics: getDataFrameAnalyticsProgressPhase()', () => { + test('should report progress by current phase', () => { + expect(getDataFrameAnalyticsProgressPhase(completedJob)).toStrictEqual({ + currentPhase: 4, + progress: 100, + totalPhases: 4, + }); + expect(getDataFrameAnalyticsProgressPhase(runningJob)).toStrictEqual({ + currentPhase: 3, + progress: 37, + totalPhases: 4, + }); + }); +}); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts index 6f49e546981bb..e0622efe35ab6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts @@ -81,6 +81,22 @@ export function getDataFrameAnalyticsProgress(stats: DataFrameAnalyticsStats) { return undefined; } +export function getDataFrameAnalyticsProgressPhase( + stats: DataFrameAnalyticsStats +): { currentPhase: number; progress: number; totalPhases: number } { + let phase = 0; + let progress = 0; + + for (const progressPhase of stats.progress) { + phase++; + progress = progressPhase.progress_percent; + if (progressPhase.progress_percent < 100) { + break; + } + } + return { currentPhase: phase, progress, totalPhases: stats.progress.length }; +} + export interface DataFrameAnalyticsListRow { id: DataFrameAnalyticsId; checkpointing: object; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx index adb6822c524ab..94dc7ec87cc61 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx @@ -25,7 +25,7 @@ import { Eval, } from '../../../../common'; import { getTaskStateBadge } from './columns'; -import { isCompletedAnalyticsJob } from './common'; +import { getDataFrameAnalyticsProgressPhase, isCompletedAnalyticsJob } from './common'; import { isRegressionAnalysis, ANALYSIS_CONFIG_TYPE, @@ -171,14 +171,7 @@ export const ExpandedRow: FC = ({ item }) => { position: 'left', }; - const totalSteps = item.stats.progress.length; - let step = 0; - for (const progressStep of item.stats.progress) { - step++; - if (progressStep.progress_percent < 100) { - break; - } - } + const { currentPhase, totalPhases } = getDataFrameAnalyticsProgressPhase(item.stats); const progress: SectionConfig = { title: i18n.translate( @@ -188,10 +181,10 @@ export const ExpandedRow: FC = ({ item }) => { items: [ { title: i18n.translate( - 'xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.step', - { defaultMessage: 'Step' } + 'xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.phase', + { defaultMessage: 'Phase' } ), - description: `${step}/${totalSteps}`, + description: `${currentPhase}/${totalPhases}`, }, ...item.stats.progress.map(s => { return { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts index 39e3ef2ae0007..2ab58714830e3 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts @@ -12,8 +12,9 @@ import { JOB_ID_MAX_LENGTH, } from '../../../../../../common/constants/validation'; import { getNewJobLimits } from '../../../../services/ml_server_info'; -import { ValidationResults, ValidationMessage } from '../../../../../../common/util/job_utils'; +import { ValidationResults } from '../../../../../../common/util/job_utils'; import { ExistingJobsAndGroups } from '../../../../services/job_service'; +import { JobValidationMessage } from '../../../../../../common/constants/messages'; export function populateValidationMessages( validationResults: ValidationResults, @@ -176,7 +177,7 @@ export function checkForExistingJobAndGroupIds( groupIds: string[], existingJobsAndGroups: ExistingJobsAndGroups ): ValidationResults { - const messages: ValidationMessage[] = []; + const messages: JobValidationMessage[] = []; // check that job id does not already exist as a job or group or a newly created group if ( diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer.scss b/x-pack/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer.scss index 06f93ff7b158e..8d4f6ad0a6844 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer.scss +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer.scss @@ -1,6 +1,4 @@ .ml-time-series-explorer { - width: 100%; - display: inline-block; color: $euiColorDarkShade; .no-results-container { @@ -31,8 +29,6 @@ } .results-container { - padding: $euiSize; - .panel-title { font-size: $euiFontSizeM; font-weight: 400; @@ -46,8 +42,6 @@ } .series-controls { - padding: $euiSize $euiSize 0px $euiSize; - div.entity-controls { display: inline-block; padding-left: $euiSize; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts index 6e46ab0023ce4..4c57eda65a9da 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts @@ -18,11 +18,12 @@ import { buildConfigFromDetector } from '../util/chart_config_builder'; import { mlResultsService } from '../services/results_service'; import { ModelPlotOutput } from '../services/results_service/result_service_rx'; import { Job } from '../../../common/types/anomaly_detection_jobs'; +import { EntityField } from '../..'; function getMetricData( job: Job, detectorIndex: number, - entityFields: object[], + entityFields: EntityField[], earliestMs: number, latestMs: number, interval: string diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 8bf42fe545152..4e04a63640a87 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -1180,6 +1180,8 @@ export class TimeSeriesExplorer extends React.Component {
+ + {fullRefresh && loading === true && ( = ({ ref={resizeRef} data-test-subj="mlPageSingleMetricViewer" > - +

diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts b/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts index c1a5ad5e38ecc..a198fac4e3fef 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts @@ -155,7 +155,7 @@ function getSearchJsonFromConfig( json.body.query = query; - const aggs: Record> = {}; + const aggs: Record> = {}; aggFieldNamePairs.forEach(({ agg, field }, i) => { if (field !== null && field !== EVENT_RATE_FIELD_ID) { diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.d.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.d.ts deleted file mode 100644 index 6a9a7a0c13395..0000000000000 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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 { APICaller } from 'kibana/server'; -import { TypeOf } from '@kbn/config-schema'; - -import { DeepPartial } from '../../../common/types/common'; - -import { validateJobSchema } from '../../routes/schemas/job_validation_schema'; - -import { ValidationMessage } from './messages'; - -export type ValidateJobPayload = TypeOf; - -export function validateJob( - callAsCurrentUser: APICaller, - payload?: DeepPartial, - kbnVersion?: string, - callAsInternalUser?: APICaller, - isSecurityDisabled?: boolean -): Promise; diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts index d907677855c12..3a86693e91828 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts @@ -6,7 +6,8 @@ import { APICaller } from 'kibana/server'; -import { validateJob } from './job_validation'; +import { validateJob, ValidateJobPayload } from './job_validation'; +import { JobValidationMessage } from '../../../common/constants/messages'; // mock callWithRequest const callWithRequest: APICaller = (method: string) => { @@ -30,48 +31,10 @@ const callWithRequest: APICaller = (method: string) => { // so we can simulate possible runtime payloads // that don't satisfy the TypeScript specs. describe('ML - validateJob', () => { - it('calling factory without payload throws an error', done => { - validateJob(callWithRequest).then( - () => done(new Error('Promise should not resolve for this test without payload.')), - () => done() - ); - }); - - it('calling factory with incomplete payload throws an error', done => { - const payload = {}; - - validateJob(callWithRequest, payload).then( - () => done(new Error('Promise should not resolve for this test with incomplete payload.')), - () => done() - ); - }); - - it('throws an error because job.analysis_config is not an object', done => { - const payload = { job: {} }; - - validateJob(callWithRequest, payload).then( - () => - done( - new Error( - 'Promise should not resolve for this test with job.analysis_config not being an object.' - ) - ), - () => done() - ); - }); - - it('throws an error because job.analysis_config.detectors is not an Array', done => { - const payload = { job: { analysis_config: {} } }; - - validateJob(callWithRequest, payload).then( - () => - done(new Error('Promise should not resolve for this test when detectors is not an Array.')), - () => done() - ); - }); - it('basic validation messages', () => { - const payload = { job: { analysis_config: { detectors: [] } } }; + const payload = ({ + job: { analysis_config: { detectors: [] } }, + } as unknown) as ValidateJobPayload; return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); @@ -87,12 +50,12 @@ describe('ML - validateJob', () => { const jobIdTests = (testIds: string[], messageId: string) => { const promises = testIds.map(id => { - const payload = { + const payload = ({ job: { analysis_config: { detectors: [] }, job_id: id, }, - }; + } as unknown) as ValidateJobPayload; return validateJob(callWithRequest, payload).catch(() => { new Error('Promise should not fail for jobIdTests.'); }); @@ -110,7 +73,9 @@ describe('ML - validateJob', () => { }; const jobGroupIdTest = (testIds: string[], messageId: string) => { - const payload = { job: { analysis_config: { detectors: [] }, groups: testIds } }; + const payload = ({ + job: { analysis_config: { detectors: [] }, groups: testIds }, + } as unknown) as ValidateJobPayload; return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); @@ -149,7 +114,9 @@ describe('ML - validateJob', () => { const bucketSpanFormatTests = (testFormats: string[], messageId: string) => { const promises = testFormats.map(format => { - const payload = { job: { analysis_config: { bucket_span: format, detectors: [] } } }; + const payload = ({ + job: { analysis_config: { bucket_span: format, detectors: [] } }, + } as unknown) as ValidateJobPayload; return validateJob(callWithRequest, payload).catch(() => { new Error('Promise should not fail for bucketSpanFormatTests.'); }); @@ -175,7 +142,9 @@ describe('ML - validateJob', () => { }); it('at least one detector function is empty', () => { - const payload = { job: { analysis_config: { detectors: [] as Array<{ function?: string }> } } }; + const payload = ({ + job: { analysis_config: { detectors: [] as Array<{ function?: string }> } }, + } as unknown) as ValidateJobPayload; payload.job.analysis_config.detectors.push({ function: 'count', }); @@ -183,6 +152,7 @@ describe('ML - validateJob', () => { function: '', }); payload.job.analysis_config.detectors.push({ + // @ts-ignore function: undefined, }); @@ -193,7 +163,9 @@ describe('ML - validateJob', () => { }); it('detector function is not empty', () => { - const payload = { job: { analysis_config: { detectors: [] as Array<{ function?: string }> } } }; + const payload = ({ + job: { analysis_config: { detectors: [] as Array<{ function?: string }> } }, + } as unknown) as ValidateJobPayload; payload.job.analysis_config.detectors.push({ function: 'count', }); @@ -205,10 +177,10 @@ describe('ML - validateJob', () => { }); it('invalid index fields', () => { - const payload = { + const payload = ({ job: { analysis_config: { detectors: [] } }, fields: {}, - }; + } as unknown) as ValidateJobPayload; return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); @@ -217,10 +189,10 @@ describe('ML - validateJob', () => { }); it('valid index fields', () => { - const payload = { + const payload = ({ job: { analysis_config: { detectors: [] } }, fields: { testField: {} }, - }; + } as unknown) as ValidateJobPayload; return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); @@ -429,15 +401,19 @@ describe('ML - validateJob', () => { docsTestPayload.job.analysis_config.detectors = [{ function: 'count', by_field_name: 'airline' }]; it('creates a docs url pointing to the current docs version', () => { return validateJob(callWithRequest, docsTestPayload).then(messages => { - const message = messages[messages.findIndex(m => m.id === 'field_not_aggregatable')]; - expect(message.url.search('/current/')).not.toBe(-1); + const message = messages[ + messages.findIndex(m => m.id === 'field_not_aggregatable') + ] as JobValidationMessage; + expect(message.url!.search('/current/')).not.toBe(-1); }); }); it('creates a docs url pointing to the master docs version', () => { return validateJob(callWithRequest, docsTestPayload, 'master').then(messages => { - const message = messages[messages.findIndex(m => m.id === 'field_not_aggregatable')]; - expect(message.url.search('/master/')).not.toBe(-1); + const message = messages[ + messages.findIndex(m => m.id === 'field_not_aggregatable') + ] as JobValidationMessage; + expect(message.url!.search('/master/')).not.toBe(-1); }); }); }); diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.js b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts similarity index 67% rename from x-pack/plugins/ml/server/models/job_validation/job_validation.js rename to x-pack/plugins/ml/server/models/job_validation/job_validation.ts index ce4643e313c8e..f852de785c70a 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.js +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts @@ -6,67 +6,48 @@ import { i18n } from '@kbn/i18n'; import Boom from 'boom'; +import { APICaller } from 'kibana/server'; +import { TypeOf } from '@kbn/config-schema'; import { fieldsServiceProvider } from '../fields_service'; import { renderTemplate } from '../../../common/util/string_utils'; -import { getMessages } from './messages'; +import { + getMessages, + MessageId, + JobValidationMessageDef, +} from '../../../common/constants/messages'; import { VALIDATION_STATUS } from '../../../common/constants/validation'; import { basicJobValidation, uniqWithIsEqual } from '../../../common/util/job_utils'; +// @ts-ignore import { validateBucketSpan } from './validate_bucket_span'; import { validateCardinality } from './validate_cardinality'; import { validateInfluencers } from './validate_influencers'; import { validateModelMemoryLimit } from './validate_model_memory_limit'; import { validateTimeRange, isValidTimeField } from './validate_time_range'; +import { validateJobSchema } from '../../routes/schemas/job_validation_schema'; +import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; +export type ValidateJobPayload = TypeOf; + +/** + * Validates the job configuration after + * @kbn/config-schema has checked the payload {@link validateJobSchema}. + */ export async function validateJob( - callWithRequest, - payload, + callWithRequest: APICaller, + payload: ValidateJobPayload, kbnVersion = 'current', - callAsInternalUser, - isSecurityDisabled + callAsInternalUser?: APICaller, + isSecurityDisabled?: boolean ) { const messages = getMessages(); try { - if (typeof payload !== 'object' || payload === null) { - throw new Error( - i18n.translate('xpack.ml.models.jobValidation.payloadIsNotObjectErrorMessage', { - defaultMessage: 'Invalid {invalidParamName}: Needs to be an object.', - values: { invalidParamName: 'payload' }, - }) - ); - } - - const { fields, job } = payload; + const { fields } = payload; let { duration } = payload; - if (typeof job !== 'object') { - throw new Error( - i18n.translate('xpack.ml.models.jobValidation.jobIsNotObjectErrorMessage', { - defaultMessage: 'Invalid {invalidParamName}: Needs to be an object.', - values: { invalidParamName: 'job' }, - }) - ); - } - - if (typeof job.analysis_config !== 'object') { - throw new Error( - i18n.translate('xpack.ml.models.jobValidation.analysisConfigIsNotObjectErrorMessage', { - defaultMessage: 'Invalid {invalidParamName}: Needs to be an object.', - values: { invalidParamName: 'job.analysis_config' }, - }) - ); - } - - if (!Array.isArray(job.analysis_config.detectors)) { - throw new Error( - i18n.translate('xpack.ml.models.jobValidation.detectorsAreNotArrayErrorMessage', { - defaultMessage: 'Invalid {invalidParamName}: Needs to be an array.', - values: { invalidParamName: 'job.analysis_config.detectors' }, - }) - ); - } + const job = payload.job as CombinedJob; // check if basic tests pass the requirements to run the extended tests. // if so, run the extended tests and merge the messages. @@ -103,7 +84,7 @@ export async function validateJob( const cardinalityMessages = await validateCardinality(callWithRequest, job); validationMessages.push(...cardinalityMessages); const cardinalityError = cardinalityMessages.some(m => { - return VALIDATION_STATUS[messages[m.id].status] === VALIDATION_STATUS.ERROR; + return messages[m.id as MessageId].status === VALIDATION_STATUS.ERROR; }); validationMessages.push( @@ -131,27 +112,29 @@ export async function validateJob( } return uniqWithIsEqual(validationMessages).map(message => { - if (typeof messages[message.id] !== 'undefined') { + const messageId = message.id as MessageId; + const messageDef = messages[messageId] as JobValidationMessageDef; + if (typeof messageDef !== 'undefined') { // render the message template with the provided metadata - if (typeof messages[message.id].heading !== 'undefined') { - message.heading = renderTemplate(messages[message.id].heading, message); + if (typeof messageDef.heading !== 'undefined') { + message.heading = renderTemplate(messageDef.heading, message); } - message.text = renderTemplate(messages[message.id].text, message); + message.text = renderTemplate(messageDef.text, message); // check if the error message provides a link with further information // if so, add it to the message to be returned with it - if (typeof messages[message.id].url !== 'undefined') { + if (typeof messageDef.url !== 'undefined') { // the link is also treated as a template so we're able to dynamically link to // documentation links matching the running version of Kibana. - message.url = renderTemplate(messages[message.id].url, { version: kbnVersion }); + message.url = renderTemplate(messageDef.url, { version: kbnVersion! }); } - message.status = VALIDATION_STATUS[messages[message.id].status]; + message.status = messageDef.status; } else { message.text = i18n.translate( 'xpack.ml.models.jobValidation.unknownMessageIdErrorMessage', { defaultMessage: '{messageId} (unknown message id)', - values: { messageId: message.id }, + values: { messageId }, } ); } diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js index d1f90d76144bc..883f1aed1209e 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js +++ b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js @@ -65,7 +65,7 @@ export async function validateBucketSpan( } const messages = []; - const parsedBucketSpan = parseInterval(job.analysis_config.bucket_span, false); + const parsedBucketSpan = parseInterval(job.analysis_config.bucket_span); if (parsedBucketSpan === null || parsedBucketSpan.asMilliseconds() === 0) { messages.push({ id: 'bucket_span_invalid' }); return messages; diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.test.ts b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.test.ts index 4001697d74320..84f865879d67f 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.test.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.test.ts @@ -6,7 +6,7 @@ import { SKIP_BUCKET_SPAN_ESTIMATION } from '../../../common/constants/validation'; -import { ValidationMessage } from './messages'; +import { JobValidationMessage } from '../../../common/constants/messages'; // @ts-ignore import { validateBucketSpan } from './validate_bucket_span'; @@ -88,7 +88,7 @@ describe('ML - validateBucketSpan', () => { }; return validateBucketSpan(callWithRequestFactory(mockFareQuoteSearchResponse), job).then( - (messages: ValidationMessage[]) => { + (messages: JobValidationMessage[]) => { const ids = messages.map(m => m.id); expect(ids).toStrictEqual([]); } @@ -113,7 +113,7 @@ describe('ML - validateBucketSpan', () => { callWithRequestFactory(mockFareQuoteSearchResponse), job, duration - ).then((messages: ValidationMessage[]) => { + ).then((messages: JobValidationMessage[]) => { const ids = messages.map(m => m.id); expect(ids).toStrictEqual(['success_bucket_span']); }); @@ -127,7 +127,7 @@ describe('ML - validateBucketSpan', () => { callWithRequestFactory(mockFareQuoteSearchResponse), job, duration - ).then((messages: ValidationMessage[]) => { + ).then((messages: JobValidationMessage[]) => { const ids = messages.map(m => m.id); expect(ids).toStrictEqual(['bucket_span_high']); }); @@ -148,7 +148,7 @@ describe('ML - validateBucketSpan', () => { }); return validateBucketSpan(callWithRequestFactory(mockSearchResponse), job, {}).then( - (messages: ValidationMessage[]) => { + (messages: JobValidationMessage[]) => { const ids = messages.map(m => m.id); test(ids); } diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts index cf3d6d004c37e..2ad483fb07ca7 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts @@ -10,6 +10,7 @@ import { DataVisualizer } from '../data_visualizer'; import { validateJobObject } from './validate_job_object'; import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import { Detector } from '../../../common/types/anomaly_detection_jobs'; +import { MessageId, JobValidationMessage } from '../../../common/constants/messages'; function isValidCategorizationConfig(job: CombinedJob, fieldName: string): boolean { return ( @@ -31,12 +32,12 @@ const PARTITION_FIELD_CARDINALITY_THRESHOLD = 1000; const BY_FIELD_CARDINALITY_THRESHOLD = 1000; const MODEL_PLOT_THRESHOLD_HIGH = 100; -type Messages = Array<{ id: string; fieldName?: string }>; +export type Messages = JobValidationMessage[]; type Validator = (obj: { type: string; isInvalid: (cardinality: number) => boolean; - messageId?: string; + messageId?: MessageId; }) => Promise<{ modelPlotCardinality: number; messages: Messages; @@ -105,7 +106,7 @@ const validateFactory = (callWithRequest: APICaller, job: CombinedJob): Validato if (isInvalid(field.stats.cardinality!)) { messages.push({ - id: messageId || `cardinality_${type}_field`, + id: messageId || (`cardinality_${type}_field` as MessageId), fieldName: uniqueFieldName, }); } @@ -149,8 +150,8 @@ const validateFactory = (callWithRequest: APICaller, job: CombinedJob): Validato export async function validateCardinality( callWithRequest: APICaller, job?: CombinedJob -): Promise> | never { - const messages = []; +): Promise | never { + const messages: Messages = []; if (!validateJobObject(job)) { // required for TS type casting, validateJobObject throws an error internally. diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts index 4fb09af94dcc6..be6c9a7157aeb 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts @@ -47,7 +47,7 @@ export async function isValidTimeField(callAsCurrentUser: APICaller, job: Combin export async function validateTimeRange( callAsCurrentUser: APICaller, job: CombinedJob, - timeRange?: TimeRange + timeRange?: Partial ) { const messages: ValidateTimeRangeMessage[] = []; diff --git a/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts index f12c85962a28d..ddfb49ce42cb8 100644 --- a/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts @@ -29,10 +29,12 @@ export const modelMemoryLimitSchema = schema.object({ }); export const validateJobSchema = schema.object({ - duration: schema.object({ - start: schema.maybe(schema.number()), - end: schema.maybe(schema.number()), - }), + duration: schema.maybe( + schema.object({ + start: schema.maybe(schema.number()), + end: schema.maybe(schema.number()), + }) + ), fields: schema.maybe(schema.any()), job: schema.object(anomalyDetectionJobSchema), }); diff --git a/x-pack/plugins/snapshot_restore/public/application/services/http/snapshot_requests.ts b/x-pack/plugins/snapshot_restore/public/application/services/http/snapshot_requests.ts index 7f5bd09a69a51..96e941abf8392 100644 --- a/x-pack/plugins/snapshot_restore/public/application/services/http/snapshot_requests.ts +++ b/x-pack/plugins/snapshot_restore/public/application/services/http/snapshot_requests.ts @@ -35,10 +35,9 @@ export const deleteSnapshots = async ( snapshotIds: Array<{ snapshot: string; repository: string }> ) => { const result = await sendRequest({ - path: `${API_BASE_PATH}snapshots/${snapshotIds - .map(({ snapshot, repository }) => encodeURIComponent(`${repository}/${snapshot}`)) - .join(',')}`, - method: 'delete', + path: `${API_BASE_PATH}snapshots/bulk_delete`, + method: 'post', + body: snapshotIds, }); uiMetricService.trackUiMetric( diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts index 61b3f5a4d1ca1..f913299fc3992 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts @@ -225,14 +225,19 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { }); describe('deleteHandler()', () => { - const ids = ['fooRepository/snapshot-1', 'barRepository/snapshot-2']; - const mockRequest: RequestMock = { - method: 'delete', - path: addBasePath('snapshots/{ids}'), - params: { - ids: ids.join(','), - }, + method: 'post', + path: addBasePath('snapshots/bulk_delete'), + body: [ + { + repository: 'fooRepository', + snapshot: 'snapshot-1', + }, + { + repository: 'barRepository', + snapshot: 'snapshot-2', + }, + ], }; it('should return successful ES responses', async () => { diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts index 35eb0463cc7e7..13d2beede299c 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts @@ -179,17 +179,19 @@ export function registerSnapshotsRoutes({ }) ); - const deleteParamsSchema = schema.object({ - ids: schema.string(), - }); + const deleteSchema = schema.arrayOf( + schema.object({ + repository: schema.string(), + snapshot: schema.string(), + }) + ); // DELETE one or multiple snapshots - router.delete( - { path: addBasePath('snapshots/{ids}'), validate: { params: deleteParamsSchema } }, + router.post( + { path: addBasePath('snapshots/bulk_delete'), validate: { body: deleteSchema } }, license.guardApiRoute(async (ctx, req, res) => { const { callAsCurrentUser } = ctx.snapshotRestore!.client; - const { ids } = req.params as TypeOf; - const snapshotIds = ids.split(','); + const response: { itemsDeleted: Array<{ snapshot: string; repository: string }>; errors: any[]; @@ -198,17 +200,13 @@ export function registerSnapshotsRoutes({ errors: [], }; + const snapshots = req.body; + try { // We intentially perform deletion requests sequentially (blocking) instead of in parallel (non-blocking) // because there can only be one snapshot deletion task performed at a time (ES restriction). - for (let i = 0; i < snapshotIds.length; i++) { - // IDs come in the format of `repository-name/snapshot-name` - // Extract the two parts by splitting at last occurrence of `/` in case - // repository name contains '/` (from older versions) - const id = snapshotIds[i]; - const indexOfDivider = id.lastIndexOf('/'); - const snapshot = id.substring(indexOfDivider + 1); - const repository = id.substring(0, indexOfDivider); + for (let i = 0; i < snapshots.length; i++) { + const { snapshot, repository } = snapshots[i]; await callAsCurrentUser('snapshot.delete', { snapshot, repository }) .then(() => response.itemsDeleted.push({ snapshot, repository })) diff --git a/x-pack/plugins/snapshot_restore/server/services/license.ts b/x-pack/plugins/snapshot_restore/server/services/license.ts index 31d3654c51e3e..0a4748bd0ace0 100644 --- a/x-pack/plugins/snapshot_restore/server/services/license.ts +++ b/x-pack/plugins/snapshot_restore/server/services/license.ts @@ -53,12 +53,12 @@ export class License { }); } - guardApiRoute(handler: RequestHandler) { + guardApiRoute(handler: RequestHandler) { const license = this; return function licenseCheck( ctx: RequestHandlerContext, - request: KibanaRequest, + request: KibanaRequest, response: KibanaResponseFactory ) { const licenseStatus = license.getStatus(); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 149c13ea55fb2..b655a1c3dc885 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9999,9 +9999,6 @@ "xpack.ml.models.jobService.deletingJob": "削除中", "xpack.ml.models.jobService.jobHasNoDatafeedErrorMessage": "ジョブにデータフィードがありません", "xpack.ml.models.jobService.requestToActionTimedOutErrorMessage": "「{id}」を{action}するリクエストがタイムアウトしました。{extra}", - "xpack.ml.models.jobValidation.analysisConfigIsNotObjectErrorMessage": "無効な {invalidParamName}:オブジェクトでなければなりません。", - "xpack.ml.models.jobValidation.detectorsAreNotArrayErrorMessage": "無効な {invalidParamName}:配列でなければなりません。", - "xpack.ml.models.jobValidation.jobIsNotObjectErrorMessage": "無効な {invalidParamName}:オブジェクトでなければなりません。", "xpack.ml.models.jobValidation.messages.bucketSpanEmptyMessage": "バケットスパンフィールドを指定する必要があります。", "xpack.ml.models.jobValidation.messages.bucketSpanEstimationMismatchHeading": "バケットスパン", "xpack.ml.models.jobValidation.messages.bucketSpanEstimationMismatchMessage": "現在のバケットスパンは {currentBucketSpan} ですが、バケットスパンの予測からは {estimateBucketSpan} が返されました。", @@ -10059,7 +10056,6 @@ "xpack.ml.models.jobValidation.messages.timeRangeBeforeEpochMessage": "選択された、または利用可能な時間範囲には、UNIX 時間の開始以前のタイムスタンプのデータが含まれています。01/01/1970 00:00:00 (UTC) よりも前のタイムスタンプは機械学習ジョブでサポートされていません。", "xpack.ml.models.jobValidation.messages.timeRangeShortHeading": "時間範囲", "xpack.ml.models.jobValidation.messages.timeRangeShortMessage": "選択された、または利用可能な時間範囲が短すぎます。推奨最低時間範囲は {minTimeSpanReadable} で、バケットスパンの {bucketSpanCompareFactor} 倍です。", - "xpack.ml.models.jobValidation.payloadIsNotObjectErrorMessage": "無効な {invalidParamName}:オブジェクトでなければなりません。", "xpack.ml.models.jobValidation.unknownMessageIdErrorMessage": "{messageId} (不明なメッセージ ID)", "xpack.ml.models.jobValidation.validateJobObject.analysisConfigIsNotObjectErrorMessage": "無効な {invalidParamName}:オブジェクトでなければなりません。", "xpack.ml.models.jobValidation.validateJobObject.dataDescriptionIsNotObjectErrorMessage": "無効な {invalidParamName}:オブジェクトでなければなりません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index bd22f2a14a292..8e3108d30515b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10005,9 +10005,6 @@ "xpack.ml.models.jobService.deletingJob": "正在删除", "xpack.ml.models.jobService.jobHasNoDatafeedErrorMessage": "作业没有数据馈送", "xpack.ml.models.jobService.requestToActionTimedOutErrorMessage": "对 {action} “{id}” 的请求超时。{extra}", - "xpack.ml.models.jobValidation.analysisConfigIsNotObjectErrorMessage": "无效的 {invalidParamName}:需要是对象。", - "xpack.ml.models.jobValidation.detectorsAreNotArrayErrorMessage": "无效的 {invalidParamName}:需要是数组。", - "xpack.ml.models.jobValidation.jobIsNotObjectErrorMessage": "无效的 {invalidParamName}:需要是对象。", "xpack.ml.models.jobValidation.messages.bucketSpanEmptyMessage": "必须指定存储桶跨度字段。", "xpack.ml.models.jobValidation.messages.bucketSpanEstimationMismatchHeading": "存储桶跨度", "xpack.ml.models.jobValidation.messages.bucketSpanEstimationMismatchMessage": "当前存储桶跨度为 {currentBucketSpan},但存储桶跨度估计返回 {estimateBucketSpan}。", @@ -10065,7 +10062,6 @@ "xpack.ml.models.jobValidation.messages.timeRangeBeforeEpochMessage": "选定或可用时间范围包含时间戳在 UNIX epoch 开始之前的数据。Machine Learning 作业不支持在 01/01/1970 00:00:00 (UTC) 之前的时间戳。", "xpack.ml.models.jobValidation.messages.timeRangeShortHeading": "时间范围", "xpack.ml.models.jobValidation.messages.timeRangeShortMessage": "选定或可用时间范围可能过短。建议的最小时间范围应至少为 {minTimeSpanReadable} 且是存储桶跨度的 {bucketSpanCompareFactor} 倍。", - "xpack.ml.models.jobValidation.payloadIsNotObjectErrorMessage": "无效的 {invalidParamName}:需要是对象。", "xpack.ml.models.jobValidation.unknownMessageIdErrorMessage": "{messageId}(未知消息 ID)", "xpack.ml.models.jobValidation.validateJobObject.analysisConfigIsNotObjectErrorMessage": "无效的 {invalidParamName}:需要是对象。", "xpack.ml.models.jobValidation.validateJobObject.dataDescriptionIsNotObjectErrorMessage": "无效的 {invalidParamName}:需要是对象。", diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap index 727cf2518549d..55c2f591d084f 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap @@ -745,7 +745,7 @@ exports[`MonitorList component renders the monitor list 1`] = ` scope="col" >
- @@ -1092,7 +1092,7 @@ exports[`MonitorList component renders the monitor list 1`] = ` TLS Certificate
- diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx index ae07cd8dddfb4..69ffe4e4a6dc6 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx @@ -144,7 +144,7 @@ export const MonitorListComponent: React.FC = ({ ), }, { - align: 'center' as const, + align: 'left' as const, field: 'state.tls', name: labels.TLS_COLUMN_LABEL, render: (tls: any) => , diff --git a/x-pack/test/api_integration/apis/management/index_management/mapping.js b/x-pack/test/api_integration/apis/management/index_management/mapping.js index fa0f6e04a7a4d..2b786542553de 100644 --- a/x-pack/test/api_integration/apis/management/index_management/mapping.js +++ b/x-pack/test/api_integration/apis/management/index_management/mapping.js @@ -32,7 +32,7 @@ export default function({ getService }) { const { body } = await getIndexMapping(index).expect(200); - expect(body.mapping).to.eql(mappings); + expect(body.mappings).to.eql(mappings); }); }); } diff --git a/x-pack/test/api_integration/apis/ml/anomaly_detectors/get.ts b/x-pack/test/api_integration/apis/ml/anomaly_detectors/get.ts index 255afecde74cb..c2d904e379dd6 100644 --- a/x-pack/test/api_integration/apis/ml/anomaly_detectors/get.ts +++ b/x-pack/test/api_integration/apis/ml/anomaly_detectors/get.ts @@ -145,7 +145,7 @@ export default ({ getService }: FtrProviderContext) => { expect(body.count).to.eql(2); expect(body.jobs.length).to.eql(2); expect(body.jobs[0].job_id).to.eql(`${jobId}_1`); - expect(body.jobs[0]).to.keys( + expect(body.jobs[0]).to.have.keys( 'timing_stats', 'state', 'forecasts_stats', @@ -178,7 +178,7 @@ export default ({ getService }: FtrProviderContext) => { expect(body.count).to.eql(1); expect(body.jobs.length).to.eql(1); expect(body.jobs[0].job_id).to.eql(`${jobId}_1`); - expect(body.jobs[0]).to.keys( + expect(body.jobs[0]).to.have.keys( 'timing_stats', 'state', 'forecasts_stats', @@ -197,7 +197,7 @@ export default ({ getService }: FtrProviderContext) => { expect(body.count).to.eql(2); expect(body.jobs.length).to.eql(2); expect(body.jobs[0].job_id).to.eql(`${jobId}_1`); - expect(body.jobs[0]).to.keys( + expect(body.jobs[0]).to.have.keys( 'timing_stats', 'state', 'forecasts_stats', diff --git a/x-pack/test/api_integration/apis/ml/job_validation/index.ts b/x-pack/test/api_integration/apis/ml/job_validation/index.ts index fa894de839cd2..774f2ef7b4016 100644 --- a/x-pack/test/api_integration/apis/ml/job_validation/index.ts +++ b/x-pack/test/api_integration/apis/ml/job_validation/index.ts @@ -10,5 +10,6 @@ export default function({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./bucket_span_estimator')); loadTestFile(require.resolve('./calculate_model_memory_limit')); loadTestFile(require.resolve('./cardinality')); + loadTestFile(require.resolve('./validate')); }); } diff --git a/x-pack/test/api_integration/apis/ml/job_validation/validate.ts b/x-pack/test/api_integration/apis/ml/job_validation/validate.ts new file mode 100644 index 0000000000000..aaeead57345bc --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/job_validation/validate.ts @@ -0,0 +1,391 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('Validate job', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/ecommerce'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it(`should recognize a valid job configuration`, async () => { + const requestBody = { + duration: { start: 1586995459000, end: 1589672736000 }, + job: { + job_id: 'test', + description: '', + groups: [], + analysis_config: { + bucket_span: '15m', + detectors: [{ function: 'mean', field_name: 'products.discount_amount' }], + influencers: [], + summary_count_field_name: 'doc_count', + }, + data_description: { time_field: 'order_date' }, + analysis_limits: { model_memory_limit: '11MB' }, + model_plot_config: { enabled: true }, + datafeed_config: { + datafeed_id: 'datafeed-test', + job_id: 'test', + indices: ['ft_ecommerce'], + query: { bool: { must: [{ match_all: {} }], filter: [], must_not: [] } }, + aggregations: { + buckets: { + date_histogram: { field: 'order_date', fixed_interval: '90000ms' }, + aggregations: { + 'products.discount_amount': { avg: { field: 'products.discount_amount' } }, + order_date: { max: { field: 'order_date' } }, + }, + }, + }, + }, + }, + }; + + const { body } = await supertest + .post('/api/ml/validate/job') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_HEADERS) + .send(requestBody) + .expect(200); + + expect(body).to.eql([ + { + id: 'job_id_valid', + heading: 'Job ID format is valid', + text: + 'Lowercase alphanumeric (a-z and 0-9) characters, hyphens or underscores, starts and ends with an alphanumeric character, and is no more than 64 characters long.', + url: + 'https://www.elastic.co/guide/en/elasticsearch/reference/master/ml-job-resource.html#ml-job-resource', + status: 'success', + }, + { + id: 'detectors_function_not_empty', + heading: 'Detector functions', + text: 'Presence of detector functions validated in all detectors.', + url: 'https://www.elastic.co/guide/en/machine-learning/master/create-jobs.html#detectors', + status: 'success', + }, + { + id: 'success_bucket_span', + bucketSpan: '15m', + heading: 'Bucket span', + text: 'Format of "15m" is valid and passed validation checks.', + url: + 'https://www.elastic.co/guide/en/machine-learning/master/create-jobs.html#bucket-span', + status: 'success', + }, + { + id: 'success_time_range', + heading: 'Time range', + text: 'Valid and long enough to model patterns in the data.', + status: 'success', + }, + { + id: 'success_mml', + heading: 'Model memory limit', + text: 'Valid and within the estimated model memory limit.', + url: + 'https://www.elastic.co/guide/en/machine-learning/master/create-jobs.html#model-memory-limits', + status: 'success', + }, + ]); + }); + + it('should recognize a basic invalid job configuration and skip advanced checks', async () => { + const requestBody = { + duration: { start: 1586995459000, end: 1589672736000 }, + job: { + job_id: '-(*&^', + description: '', + groups: [], + analysis_config: { + bucket_span: '15m', + detectors: [{ function: 'mean', field_name: 'products.discount_amount' }], + influencers: [], + summary_count_field_name: 'doc_count', + }, + data_description: { time_field: 'order_date' }, + analysis_limits: { model_memory_limit: '11MB' }, + model_plot_config: { enabled: true }, + datafeed_config: { + datafeed_id: 'datafeed-test', + job_id: 'test', + indices: ['ft_ecommerce'], + query: { bool: { must: [{ match_all: {} }], filter: [], must_not: [] } }, + aggregations: { + buckets: { + date_histogram: { field: 'order_date', fixed_interval: '90000ms' }, + aggregations: { + 'products.discount_amount': { avg: { field: 'products.discount_amount' } }, + order_date: { max: { field: 'order_date' } }, + }, + }, + }, + }, + }, + }; + + const { body } = await supertest + .post('/api/ml/validate/job') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_HEADERS) + .send(requestBody) + .expect(200); + + expect(body).to.eql([ + { + id: 'job_id_invalid', + text: + 'Job ID is invalid. It can contain lowercase alphanumeric (a-z and 0-9) characters, hyphens or underscores and must start and end with an alphanumeric character.', + url: + 'https://www.elastic.co/guide/en/elasticsearch/reference/master/ml-job-resource.html#ml-job-resource', + status: 'error', + }, + { + id: 'detectors_function_not_empty', + heading: 'Detector functions', + text: 'Presence of detector functions validated in all detectors.', + url: 'https://www.elastic.co/guide/en/machine-learning/master/create-jobs.html#detectors', + status: 'success', + }, + { + id: 'bucket_span_valid', + bucketSpan: '15m', + heading: 'Bucket span', + text: 'Format of "15m" is valid.', + url: + 'https://www.elastic.co/guide/en/elasticsearch/reference/master/ml-job-resource.html#ml-analysisconfig', + status: 'success', + }, + { + id: 'skipped_extended_tests', + text: + 'Skipped additional checks because the basic requirements of the job configuration were not met.', + status: 'warning', + }, + ]); + }); + + it('should recognize non-basic issues in job configuration', async () => { + const requestBody = { + duration: { start: 1586995459000, end: 1589672736000 }, + job: { + job_id: 'test', + description: '', + groups: [], + analysis_config: { + bucket_span: '1000000m', + detectors: [ + { + function: 'mean', + field_name: 'products.base_price', + // some high cardinality field + partition_field_name: 'order_id', + }, + ], + influencers: ['order_id'], + }, + data_description: { time_field: 'order_date' }, + analysis_limits: { model_memory_limit: '1MB' }, + model_plot_config: { enabled: true }, + datafeed_config: { + datafeed_id: 'datafeed-test', + job_id: 'test', + indices: ['ft_ecommerce'], + query: { bool: { must: [{ match_all: {} }], filter: [], must_not: [] } }, + aggregations: { + buckets: { + date_histogram: { field: 'order_date', fixed_interval: '90000ms' }, + aggregations: { + 'products.discount_amount': { avg: { field: 'products.discount_amount' } }, + order_date: { max: { field: 'order_date' } }, + }, + }, + }, + }, + }, + }; + + const { body } = await supertest + .post('/api/ml/validate/job') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_HEADERS) + .send(requestBody) + .expect(200); + + expect(body).to.eql([ + { + id: 'job_id_valid', + heading: 'Job ID format is valid', + text: + 'Lowercase alphanumeric (a-z and 0-9) characters, hyphens or underscores, starts and ends with an alphanumeric character, and is no more than 64 characters long.', + url: + 'https://www.elastic.co/guide/en/elasticsearch/reference/master/ml-job-resource.html#ml-job-resource', + status: 'success', + }, + { + id: 'detectors_function_not_empty', + heading: 'Detector functions', + text: 'Presence of detector functions validated in all detectors.', + url: 'https://www.elastic.co/guide/en/machine-learning/master/create-jobs.html#detectors', + status: 'success', + }, + { + id: 'cardinality_model_plot_high', + modelPlotCardinality: 4711, + text: + 'The estimated cardinality of 4711 of fields relevant to creating model plots might result in resource intensive jobs.', + status: 'warning', + }, + { + id: 'cardinality_partition_field', + fieldName: 'order_id', + text: + 'Cardinality of partition_field "order_id" is above 1000 and might result in high memory usage.', + url: + 'https://www.elastic.co/guide/en/machine-learning/master/create-jobs.html#cardinality', + status: 'warning', + }, + { + id: 'bucket_span_high', + heading: 'Bucket span', + text: + 'Bucket span is 1 day or more. Be aware that days are considered as UTC days, not local days.', + url: + 'https://www.elastic.co/guide/en/machine-learning/master/create-jobs.html#bucket-span', + status: 'info', + }, + { + bucketSpanCompareFactor: 25, + id: 'time_range_short', + minTimeSpanReadable: '2 hours', + heading: 'Time range', + text: + 'The selected or available time range might be too short. The recommended minimum time range should be at least 2 hours and 25 times the bucket span.', + status: 'warning', + }, + { + id: 'success_influencers', + text: 'Influencer configuration passed the validation checks.', + url: 'https://www.elastic.co/guide/en/machine-learning/master/ml-influencers.html', + status: 'success', + }, + { + id: 'half_estimated_mml_greater_than_mml', + mml: '1MB', + text: + 'The specified model memory limit is less than half of the estimated model memory limit and will likely hit the hard limit.', + url: + 'https://www.elastic.co/guide/en/machine-learning/master/create-jobs.html#model-memory-limits', + status: 'warning', + }, + ]); + }); + + it('should not validate configuration in case request payload is invalid', async () => { + const requestBody = { + duration: { start: 1586995459000, end: 1589672736000 }, + job: { + job_id: 'test', + description: '', + groups: [], + // missing analysis_config + data_description: { time_field: 'order_date' }, + analysis_limits: { model_memory_limit: '11MB' }, + model_plot_config: { enabled: true }, + datafeed_config: { + datafeed_id: 'datafeed-test', + job_id: 'test', + indices: ['ft_ecommerce'], + query: { bool: { must: [{ match_all: {} }], filter: [], must_not: [] } }, + aggregations: { + buckets: { + date_histogram: { field: 'order_date', fixed_interval: '90000ms' }, + aggregations: { + 'products.discount_amount': { avg: { field: 'products.discount_amount' } }, + order_date: { max: { field: 'order_date' } }, + }, + }, + }, + }, + }, + }; + + const { body } = await supertest + .post('/api/ml/validate/job') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_HEADERS) + .send(requestBody) + .expect(400); + + expect(body.error).to.eql('Bad Request'); + expect(body.message).to.eql( + '[request body.job.analysis_config.detectors]: expected value of type [array] but got [undefined]' + ); + }); + + it('should not validate if the user does not have required permissions', async () => { + const requestBody = { + job: { + job_id: 'test', + description: '', + groups: [], + analysis_config: { + bucket_span: '15m', + detectors: [{ function: 'mean', field_name: 'products.discount_amount' }], + influencers: [], + summary_count_field_name: 'doc_count', + }, + data_description: { time_field: 'order_date' }, + analysis_limits: { model_memory_limit: '11MB' }, + model_plot_config: { enabled: true }, + datafeed_config: { + datafeed_id: 'datafeed-test', + job_id: 'test', + indices: ['ft_ecommerce'], + query: { bool: { must: [{ match_all: {} }], filter: [], must_not: [] } }, + aggregations: { + buckets: { + date_histogram: { field: 'order_date', fixed_interval: '90000ms' }, + aggregations: { + 'products.discount_amount': { avg: { field: 'products.discount_amount' } }, + order_date: { max: { field: 'order_date' } }, + }, + }, + }, + }, + }, + }; + + const { body } = await supertest + .post('/api/ml/validate/job') + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_HEADERS) + .send(requestBody) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + }); + }); +}; diff --git a/x-pack/test_utils/jest/config.js b/x-pack/test_utils/jest/config.js index 6ca2818f9e6af..66b88cbdeba17 100644 --- a/x-pack/test_utils/jest/config.js +++ b/x-pack/test_utils/jest/config.js @@ -13,7 +13,6 @@ export default { '/legacy/server', '/legacy/common', '/test_utils/jest/integration_tests', - '/test_utils/jest/contract_tests', ], collectCoverageFrom: ['legacy/plugins/**/*.js', 'legacy/common/**/*.js', 'legacy/server/**/*.js'], moduleNameMapper: {