From ab0e2badb7813d0787dafe816c3dae5a9b6339ce Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Fri, 20 Jul 2018 15:10:02 -0700 Subject: [PATCH 01/12] Convert notify.warning calls to use toastNotifications (#20767) * Replace notify.warning with toastNotifications in region map, vega, index_pattern, redirect_when_missing, graph, monitoring, and ML * Link to index patterns from Graph toast. * Delete RouteBasedNotifier. * Remove courierNotifier and SearchTimeout and ShardFailure errors. * Remove warning and custom notifier types. --- .../public/region_map_vis_params.js | 8 +- .../public/region_map_visualization.js | 16 +- .../public/base_maps_visualization.js | 6 +- .../vega/public/vega_visualization.js | 4 +- src/ui/public/__tests__/errors.js | 4 - .../courier/fetch/call_response_handlers.js | 13 +- src/ui/public/courier/fetch/fetch_now.js | 3 +- src/ui/public/courier/fetch/notifier.js | 26 --- src/ui/public/errors.js | 24 --- .../__tests__/_index_pattern.js | 27 ++- .../public/index_patterns/_index_pattern.js | 17 +- src/ui/public/notify/__tests__/notifier.js | 181 ------------------ src/ui/public/notify/notifier.js | 52 ----- .../route_based_notifier/__tests__/index.js | 98 ---------- src/ui/public/route_based_notifier/index.js | 64 ------- src/ui/public/url/redirect_when_missing.js | 7 +- x-pack/plugins/graph/public/app.js | 17 +- .../job_select_list/job_select_service.js | 6 +- .../ml/public/license/check_license.js | 23 ++- .../timeseriesexplorer_controller.js | 12 +- x-pack/plugins/ml/public/util/index_utils.js | 9 +- .../directives/cluster/listing/index.js | 55 ++++-- .../public/views/cluster/listing/index.js | 2 +- .../apps/monitoring/cluster/list.js | 19 +- 24 files changed, 129 insertions(+), 564 deletions(-) delete mode 100644 src/ui/public/courier/fetch/notifier.js delete mode 100644 src/ui/public/route_based_notifier/__tests__/index.js delete mode 100644 src/ui/public/route_based_notifier/index.js diff --git a/src/core_plugins/region_map/public/region_map_vis_params.js b/src/core_plugins/region_map/public/region_map_vis_params.js index a47c2a0ef37b9..26dfc2a572325 100644 --- a/src/core_plugins/region_map/public/region_map_vis_params.js +++ b/src/core_plugins/region_map/public/region_map_vis_params.js @@ -18,15 +18,13 @@ */ import { uiModules } from 'ui/modules'; +import { toastNotifications } from 'ui/notify'; import regionMapVisParamsTemplate from './region_map_vis_params.html'; import { mapToLayerWithId } from './util'; import '../../tile_map/public/editors/wms_options'; uiModules.get('kibana/region_map') - .directive('regionMapVisParams', function (serviceSettings, regionmapsConfig, Notifier) { - - const notify = new Notifier({ location: 'Region map' }); - + .directive('regionMapVisParams', function (serviceSettings, regionmapsConfig) { return { restrict: 'E', template: regionMapVisParamsTemplate, @@ -84,7 +82,7 @@ uiModules.get('kibana/region_map') }) .catch(function (error) { - notify.warning(error.message); + toastNotifications.addWarning(error.message); }); } diff --git a/src/core_plugins/region_map/public/region_map_visualization.js b/src/core_plugins/region_map/public/region_map_visualization.js index 2f627b9ca1b71..881bfd4ea8dc6 100644 --- a/src/core_plugins/region_map/public/region_map_visualization.js +++ b/src/core_plugins/region_map/public/region_map_visualization.js @@ -24,8 +24,9 @@ import ChoroplethLayer from './choropleth_layer'; import { truncatedColorMaps } from 'ui/vislib/components/color/truncated_colormaps'; import AggResponsePointSeriesTooltipFormatterProvider from './tooltip_formatter'; import 'ui/vis/map/service_settings'; +import { toastNotifications } from 'ui/notify'; -export function RegionMapsVisualizationProvider(Private, Notifier, config) { +export function RegionMapsVisualizationProvider(Private, config) { const tooltipFormatter = Private(AggResponsePointSeriesTooltipFormatterProvider); const BaseMapsVisualization = Private(BaseMapsVisualizationProvider); @@ -36,10 +37,8 @@ export function RegionMapsVisualizationProvider(Private, Notifier, config) { super(container, vis); this._vis = this.vis; this._choroplethLayer = null; - this._notify = new Notifier({ location: 'Region map' }); } - async render(esResponse, status) { await super.render(esResponse, status); if (this._choroplethLayer) { @@ -47,7 +46,6 @@ export function RegionMapsVisualizationProvider(Private, Notifier, config) { } } - async _updateData(tableGroup) { this._chartData = tableGroup; let results; @@ -81,7 +79,6 @@ export function RegionMapsVisualizationProvider(Private, Notifier, config) { this._kibanaMap.useUiStateFromVisualization(this._vis); } - async _updateParams() { await super._updateParams(); @@ -156,13 +153,14 @@ export function RegionMapsVisualizationProvider(Private, Notifier, config) { const rowIndex = this._chartData.tables[0].rows.findIndex(row => row[0] === event); this._vis.API.events.addFilter(this._chartData.tables[0], 0, rowIndex, event); }); + this._choroplethLayer.on('styleChanged', (event) => { const shouldShowWarning = this._vis.params.isDisplayWarning && config.get('visualization:regionmap:showWarnings'); if (event.mismatches.length > 0 && shouldShowWarning) { - this._notify.warning(`Could not show ${event.mismatches.length} ${event.mismatches.length > 1 ? 'results' : 'result'} on the map.` - + ` To avoid this, ensure that each term can be matched to a corresponding shape on that shape's join field.` - + ` Could not match following terms: ${event.mismatches.join(',')}` - ); + toastNotifications.addWarning({ + title: `Unable to show ${event.mismatches.length} ${event.mismatches.length > 1 ? 'results' : 'result'} on map`, + text: `Ensure that each of these term matches a shape on that shape's join field: ${event.mismatches.join(', ')}`, + }); } }); diff --git a/src/core_plugins/tile_map/public/base_maps_visualization.js b/src/core_plugins/tile_map/public/base_maps_visualization.js index 6c6dbcdf424da..aafb19d9448db 100644 --- a/src/core_plugins/tile_map/public/base_maps_visualization.js +++ b/src/core_plugins/tile_map/public/base_maps_visualization.js @@ -22,7 +22,7 @@ import { KibanaMap } from 'ui/vis/map/kibana_map'; import * as Rx from 'rxjs'; import { filter, first } from 'rxjs/operators'; import 'ui/vis/map/service_settings'; - +import { toastNotifications } from 'ui/notify'; const MINZOOM = 0; const MAXZOOM = 22;//increase this to 22. Better for WMS @@ -142,7 +142,7 @@ export function BaseMapsVisualizationProvider(serviceSettings) { this._setTmsLayer(firstRoadMapLayer); } } catch (e) { - this._notify.warning(e.message); + toastNotifications.addWarning(e.message); return; } return; @@ -174,7 +174,7 @@ export function BaseMapsVisualizationProvider(serviceSettings) { } } catch (tmsLoadingError) { - this._notify.warning(tmsLoadingError.message); + toastNotifications.addWarning(tmsLoadingError.message); } diff --git a/src/core_plugins/vega/public/vega_visualization.js b/src/core_plugins/vega/public/vega_visualization.js index 83eaf6574ca0a..c51ebcbb2042e 100644 --- a/src/core_plugins/vega/public/vega_visualization.js +++ b/src/core_plugins/vega/public/vega_visualization.js @@ -17,7 +17,7 @@ * under the License. */ -import { Notifier } from 'ui/notify'; +import { toastNotifications, Notifier } from 'ui/notify'; import { VegaView } from './vega_view/vega_view'; import { VegaMapView } from './vega_view/vega_map_view'; import { SavedObjectsClientProvider, findObjectByTitle } from 'ui/saved_objects'; @@ -59,7 +59,7 @@ export function VegaVisualizationProvider(Private, vegaConfig, serviceSettings, */ async render(visData, status) { if (!visData && !this._vegaView) { - notify.warning('Unable to render without data'); + toastNotifications.addWarning('Unable to render without data'); return; } diff --git a/src/ui/public/__tests__/errors.js b/src/ui/public/__tests__/errors.js index 7168b38e2a93d..d328a6dc4e0ca 100644 --- a/src/ui/public/__tests__/errors.js +++ b/src/ui/public/__tests__/errors.js @@ -19,10 +19,8 @@ import expect from 'expect.js'; import { - SearchTimeout, RequestFailure, FetchFailure, - ShardFailure, VersionConflict, MappingConflict, RestrictedMapping, @@ -46,10 +44,8 @@ import { describe('ui/errors', () => { const errors = [ - new SearchTimeout(), new RequestFailure('an error', { }), new FetchFailure({ }), - new ShardFailure({ '_shards': 5 }), new VersionConflict({ }), new MappingConflict({ }), new RestrictedMapping('field', 'indexPattern'), diff --git a/src/ui/public/courier/fetch/call_response_handlers.js b/src/ui/public/courier/fetch/call_response_handlers.js index 13b0dcc81e728..9d305959bc712 100644 --- a/src/ui/public/courier/fetch/call_response_handlers.js +++ b/src/ui/public/courier/fetch/call_response_handlers.js @@ -17,10 +17,9 @@ * under the License. */ -import { RequestFailure, SearchTimeout, ShardFailure } from '../../errors'; - +import { toastNotifications } from '../../notify'; +import { RequestFailure } from '../../errors'; import { RequestStatus } from './req_status'; -import { courierNotifier } from './notifier'; export function CallResponseHandlersProvider(Private, Promise) { const ABORTED = RequestStatus.ABORTED; @@ -35,11 +34,15 @@ export function CallResponseHandlersProvider(Private, Promise) { const response = responses[index]; if (response.timed_out) { - courierNotifier.warning(new SearchTimeout()); + toastNotifications.addWarning({ + title: 'Data might be incomplete because your request timed out', + }); } if (response._shards && response._shards.failed) { - courierNotifier.warning(new ShardFailure(response)); + toastNotifications.addWarning({ + title: '${response._shards.failed} of ${response._shards.total} shards failed', + }); } function progress() { diff --git a/src/ui/public/courier/fetch/fetch_now.js b/src/ui/public/courier/fetch/fetch_now.js index 73a670100cece..70aa20a26619c 100644 --- a/src/ui/public/courier/fetch/fetch_now.js +++ b/src/ui/public/courier/fetch/fetch_now.js @@ -22,7 +22,6 @@ import { CallClientProvider } from './call_client'; import { CallResponseHandlersProvider } from './call_response_handlers'; import { ContinueIncompleteProvider } from './continue_incomplete'; import { RequestStatus } from './req_status'; -import { location } from './notifier'; /** * Fetch now provider should be used if you want the results searched and returned immediately. @@ -53,7 +52,7 @@ export function FetchNowProvider(Private, Promise) { return searchRequest.retry(); })) - .catch(error => fatalError(error, location)); + .catch(error => fatalError(error, 'Courier fetch')); } function fetchSearchResults(searchRequests) { diff --git a/src/ui/public/courier/fetch/notifier.js b/src/ui/public/courier/fetch/notifier.js deleted file mode 100644 index 57cbb18337a7b..0000000000000 --- a/src/ui/public/courier/fetch/notifier.js +++ /dev/null @@ -1,26 +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 { Notifier } from '../../notify'; - -export const location = 'Courier fetch'; - -export const courierNotifier = new Notifier({ - location, -}); diff --git a/src/ui/public/errors.js b/src/ui/public/errors.js index 5a33c6f40a9c9..20cb741857fdf 100644 --- a/src/ui/public/errors.js +++ b/src/ui/public/errors.js @@ -51,16 +51,6 @@ export class KbnError { // instanceof checks. createLegacyClass(KbnError).inherits(Error); -/** - * SearchTimeout error class - */ -export class SearchTimeout extends KbnError { - constructor() { - super('All or part of your request has timed out. The data shown may be incomplete.', - SearchTimeout); - } -} - /** * Request Failure - When an entire multi request fails * @param {Error} err - the Error that came back @@ -92,20 +82,6 @@ export class FetchFailure extends KbnError { } } -/** - * ShardFailure Error - when one or more shards fail - * @param {Object} resp - The response from es. - */ -export class ShardFailure extends KbnError { - constructor(resp) { - super( - `${resp._shards.failed} of ${resp._shards.total} shards failed.`, - ShardFailure); - - this.resp = resp; - } -} - /** * A doc was re-indexed but it was out of date. * @param {Object} resp - The response from es (one of the multi-response responses). diff --git a/src/ui/public/index_patterns/__tests__/_index_pattern.js b/src/ui/public/index_patterns/__tests__/_index_pattern.js index df63f35ce3b25..023e61bdf8a01 100644 --- a/src/ui/public/index_patterns/__tests__/_index_pattern.js +++ b/src/ui/public/index_patterns/__tests__/_index_pattern.js @@ -29,7 +29,7 @@ import { FixturesStubbedSavedObjectIndexPatternProvider } from 'fixtures/stubbed import { IndexPatternsIntervalsProvider } from '../_intervals'; import { IndexPatternProvider } from '../_index_pattern'; import NoDigestPromises from 'test_utils/no_digest_promises'; -import { Notifier } from '../../notify'; +import { toastNotifications } from '../../notify'; import { FieldsFetcherProvider } from '../fields_fetcher_provider'; import { StubIndexPatternsApiClientModule } from './stub_index_patterns_api_client'; @@ -37,8 +37,6 @@ import { IndexPatternsApiClientProvider } from '../index_patterns_api_client_pro import { IsUserAwareOfUnsupportedTimePatternProvider } from '../unsupported_time_patterns'; import { SavedObjectsClientProvider } from '../../saved_objects'; -const MARKDOWN_LINK_RE = /\[(.+?)\]\((.+?)\)/; - describe('index pattern', function () { NoDigestPromises.activateForSuite(); @@ -468,23 +466,22 @@ describe('index pattern', function () { } it('logs a warning when the index pattern source includes `intervalName`', async () => { - const indexPattern = await createUnsupportedTimePattern(); - expect(Notifier.prototype._notifs).to.have.length(1); - - const notif = Notifier.prototype._notifs.shift(); - expect(notif).to.have.property('type', 'warning'); - expect(notif.content).to.match(MARKDOWN_LINK_RE); - - const [, text, url] = notif.content.match(MARKDOWN_LINK_RE); - expect(text).to.contain(indexPattern.title); - expect(url).to.contain(indexPattern.id); - expect(url).to.contain('management/kibana/indices'); + await createUnsupportedTimePattern(); + expect(toastNotifications.list).to.have.length(1); }); it('does not notify if isUserAwareOfUnsupportedTimePattern() returns true', async () => { + // Ideally, _index_pattern.js shouldn't be tightly coupled to toastNotifications. Instead, it + // should notify its consumer of this state and the consumer should be responsible for + // notifying the user. This test verifies the side effect of the state until we can remove + // this coupling. + + // Clear existing toasts. + toastNotifications.list.splice(0); + isUserAwareOfUnsupportedTimePattern.returns(true); await createUnsupportedTimePattern(); - expect(Notifier.prototype._notifs).to.have.length(0); + expect(toastNotifications.list).to.have.length(0); }); }); }); diff --git a/src/ui/public/index_patterns/_index_pattern.js b/src/ui/public/index_patterns/_index_pattern.js index 8010d38d146d9..a158bba776bf3 100644 --- a/src/ui/public/index_patterns/_index_pattern.js +++ b/src/ui/public/index_patterns/_index_pattern.js @@ -17,6 +17,7 @@ * under the License. */ +import React, { Fragment } from 'react'; import _ from 'lodash'; import { SavedObjectNotFound, DuplicateField, IndexPatternMissingIndices } from '../errors'; import angular from 'angular'; @@ -119,13 +120,15 @@ export function IndexPatternProvider(Private, config, Promise, confirmModalPromi if (indexPattern.isUnsupportedTimePattern()) { if (!isUserAwareOfUnsupportedTimePattern(indexPattern)) { - const warning = ( - 'Support for time-intervals has been removed. ' + - `View the ["${indexPattern.title}" index pattern in management](` + - kbnUrl.getRouteHref(indexPattern, 'edit') + - ') for more information.' - ); - notify.warning(warning, { lifetime: Infinity }); + toastNotifications.addWarning({ + title: 'Support for time intervals was removed', + text: ( + + For more information, view the {' '} + {indexPattern.title} index pattern + + ), + }); } } diff --git a/src/ui/public/notify/__tests__/notifier.js b/src/ui/public/notify/__tests__/notifier.js index da31599d3f63f..6696dd831ab9e 100644 --- a/src/ui/public/notify/__tests__/notifier.js +++ b/src/ui/public/notify/__tests__/notifier.js @@ -29,17 +29,6 @@ describe('Notifier', function () { let notifier; let params; const message = 'Oh, the humanity!'; - const customText = 'fooMarkup'; - const customParams = { - title: 'fooTitle', - actions: [{ - text: 'Cancel', - callback: sinon.spy() - }, { - text: 'OK', - callback: sinon.spy() - }] - }; beforeEach(function () { ngMock.module('kibana'); @@ -150,176 +139,6 @@ describe('Notifier', function () { }); }); - describe('#warning', function () { - testVersionInfo('warning'); - - it('prepends location to message for content', function () { - expect(notify('warning').content).to.equal(params.location + ': ' + message); - }); - - it('sets type to "warning"', function () { - expect(notify('warning').type).to.equal('warning'); - }); - - it('sets icon to "warning"', function () { - expect(notify('warning').icon).to.equal('warning'); - }); - - it('sets title to "Warning"', function () { - expect(notify('warning').title).to.equal('Warning'); - }); - - it('sets lifetime to 10000', function () { - expect(notify('warning').lifetime).to.equal(10000); - }); - - it('does not allow reporting', function () { - const includesReport = _.includes(notify('warning').actions, 'report'); - expect(includesReport).to.false; - }); - - it('allows accepting', function () { - const includesAccept = _.includes(notify('warning').actions, 'accept'); - expect(includesAccept).to.true; - }); - - it('does not include stack', function () { - expect(notify('warning').stack).not.to.be.defined; - }); - - it('has css class helper functions', function () { - expect(notify('warning').getIconClass()).to.equal('fa fa-warning'); - expect(notify('warning').getButtonClass()).to.equal('kuiButton--warning'); - expect(notify('warning').getAlertClassStack()).to.equal('toast-stack alert alert-warning'); - expect(notify('warning').getAlertClass()).to.equal('toast alert alert-warning'); - expect(notify('warning').getButtonGroupClass()).to.equal('toast-controls'); - expect(notify('warning').getToastMessageClass()).to.equal('toast-message'); - }); - }); - - describe('#custom', function () { - let customNotification; - - beforeEach(() => { - customNotification = notifier.custom(customText, customParams); - }); - - afterEach(() => { - customNotification.clear(); - }); - - it('throws if second param is not an object', function () { - // destroy the default custom notification, avoid duplicate handling - customNotification.clear(); - - function callCustomIncorrectly() { - const badParam = null; - customNotification = notifier.custom(customText, badParam); - } - expect(callCustomIncorrectly).to.throwException(function (e) { - expect(e.message).to.be('Config param is required, and must be an object'); - }); - - }); - - it('has a custom function to make notifications', function () { - expect(notifier.custom).to.be.a('function'); - }); - - it('properly merges options', function () { - // destroy the default custom notification, avoid duplicate handling - customNotification.clear(); - - const overrideParams = _.defaults({ lifetime: 20000 }, customParams); - customNotification = notifier.custom(customText, overrideParams); - - expect(customNotification).to.have.property('type', 'info'); // default - expect(customNotification).to.have.property('title', overrideParams.title); // passed in thru customParams - expect(customNotification).to.have.property('lifetime', overrideParams.lifetime); // passed in thru overrideParams - - expect(overrideParams.type).to.be(undefined); - expect(overrideParams.title).to.be.a('string'); - expect(overrideParams.lifetime).to.be.a('number'); - }); - - it('sets the content', function () { - expect(customNotification).to.have.property('content', `${params.location}: ${customText}`); - expect(customNotification.content).to.be.a('string'); - }); - - it('uses custom actions', function () { - expect(customNotification).to.have.property('customActions'); - expect(customNotification.customActions).to.have.length(customParams.actions.length); - }); - - it('custom actions have getButtonClass method', function () { - customNotification.customActions.forEach((action, idx) => { - expect(action).to.have.property('getButtonClass'); - expect(action.getButtonClass).to.be.a('function'); - if (idx === 0) { - expect(action.getButtonClass()).to.be('kuiButton--primary kuiButton--primary'); - } else { - expect(action.getButtonClass()).to.be('kuiButton--basic kuiButton--primary'); - } - }); - }); - - it('gives a default action if none are provided', function () { - // destroy the default custom notification, avoid duplicate handling - customNotification.clear(); - - const noActionParams = _.defaults({ actions: [] }, customParams); - customNotification = notifier.custom(customText, noActionParams); - expect(customNotification).to.have.property('actions'); - expect(customNotification.actions).to.have.length(1); - }); - - it('defaults type and lifetime for "info" config', function () { - expect(customNotification.type).to.be('info'); - expect(customNotification.lifetime).to.be(5000); - }); - - it('dynamic lifetime for "warning" config', function () { - // destroy the default custom notification, avoid duplicate handling - customNotification.clear(); - - const errorTypeParams = _.defaults({ type: 'warning' }, customParams); - customNotification = notifier.custom(customText, errorTypeParams); - expect(customNotification.type).to.be('warning'); - expect(customNotification.lifetime).to.be(10000); - }); - - it('dynamic type and lifetime for "error" config', function () { - // destroy the default custom notification, avoid duplicate handling - customNotification.clear(); - - const errorTypeParams = _.defaults({ type: 'error' }, customParams); - customNotification = notifier.custom(customText, errorTypeParams); - expect(customNotification.type).to.be('danger'); - expect(customNotification.lifetime).to.be(300000); - }); - - it('dynamic type and lifetime for "danger" config', function () { - // destroy the default custom notification, avoid duplicate handling - customNotification.clear(); - - const errorTypeParams = _.defaults({ type: 'danger' }, customParams); - customNotification = notifier.custom(customText, errorTypeParams); - expect(customNotification.type).to.be('danger'); - expect(customNotification.lifetime).to.be(300000); - }); - - it('should wrap the callback functions in a close function', function () { - customNotification.customActions.forEach((action, idx) => { - expect(action.callback).not.to.equal(customParams.actions[idx]); - action.callback(); - }); - customParams.actions.forEach(action => { - expect(action.callback.called).to.true; - }); - }); - }); - function notify(fnName, opts) { notifier[fnName](message, opts); return latestNotification(); diff --git a/src/ui/public/notify/notifier.js b/src/ui/public/notify/notifier.js index 620d24df56364..6e94fb390b6cf 100644 --- a/src/ui/public/notify/notifier.js +++ b/src/ui/public/notify/notifier.js @@ -95,7 +95,6 @@ function restartNotifTimer(notif, cb) { const typeToButtonClassMap = { danger: 'kuiButton--danger', // NOTE: `error` type is internally named as `danger` - warning: 'kuiButton--warning', info: 'kuiButton--primary', }; const buttonHierarchyClass = (index) => { @@ -108,7 +107,6 @@ const buttonHierarchyClass = (index) => { }; const typeToAlertClassMap = { danger: `alert-danger`, - warning: `alert-warning`, info: `alert-info`, }; @@ -188,7 +186,6 @@ export function Notifier(opts) { const notificationLevels = [ 'error', - 'warning', ]; notificationLevels.forEach(function (m) { @@ -199,7 +196,6 @@ export function Notifier(opts) { Notifier.config = { bannerLifetime: 3000000, errorLifetime: 300000, - warningLifetime: 10000, infoLifetime: 5000, setInterval: window.setInterval, clearInterval: window.clearInterval @@ -264,28 +260,6 @@ Notifier.prototype.error = function (err, opts, cb) { return add(config, cb); }; -/** - * Warn the user abort something - * @param {String} msg - * @param {Function} cb - */ -Notifier.prototype.warning = function (msg, opts, cb) { - if (_.isFunction(opts)) { - cb = opts; - opts = {}; - } - - const config = _.assign({ - type: 'warning', - content: formatMsg(msg, this.from), - icon: 'warning', - title: 'Warning', - lifetime: Notifier.config.warningLifetime, - actions: ['accept'] - }, _.pick(opts, overridableOptions)); - return add(config, cb); -}; - /** * Display a banner message * @param {String} content @@ -357,8 +331,6 @@ function getDecoratedCustomConfig(config) { const getLifetime = (type) => { switch (type) { - case 'warning': - return Notifier.config.warningLifetime; case 'danger': return Notifier.config.errorLifetime; default: // info @@ -383,30 +355,6 @@ function getDecoratedCustomConfig(config) { return customConfig; } -/** - * Display a custom message - * @param {String} msg - required - * @param {Object} config - required - * @param {Function} cb - optional - * - * config = { - * title: 'Some Title here', - * type: 'info', - * actions: [{ - * text: 'next', - * callback: function() { next(); } - * }, { - * text: 'prev', - * callback: function() { prev(); } - * }] - * } - */ -Notifier.prototype.custom = function (msg, config, cb) { - const customConfig = getDecoratedCustomConfig(config); - customConfig.content = formatMsg(msg, this.from); - return add(customConfig, cb); -}; - /** * Display a scope-bound directive using template rendering in the message area * @param {Object} directive - required diff --git a/src/ui/public/route_based_notifier/__tests__/index.js b/src/ui/public/route_based_notifier/__tests__/index.js deleted file mode 100644 index fd5d4b3623ff2..0000000000000 --- a/src/ui/public/route_based_notifier/__tests__/index.js +++ /dev/null @@ -1,98 +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 { find } from 'lodash'; -import expect from 'expect.js'; -import ngMock from 'ng_mock'; -import { Notifier } from '../../notify/notifier'; -import { RouteBasedNotifierProvider } from '../index'; - -describe('ui/route_based_notifier', function () { - let $rootScope; - let routeBasedNotifier; - - beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(($injector) => { - const Private = $injector.get('Private'); - routeBasedNotifier = Private(RouteBasedNotifierProvider); - $rootScope = $injector.get('$rootScope'); - })); - - afterEach(() => { - Notifier.prototype._notifs.length = 0; - }); - - describe('#warning()', () => { - it('adds a warning notification', () => { - routeBasedNotifier.warning('wat'); - const notification = find(Notifier.prototype._notifs, { - type: 'warning', - content: 'wat' - }); - expect(notification).not.to.be(undefined); - }); - - it('can be used more than once for different notifications', () => { - routeBasedNotifier.warning('wat'); - routeBasedNotifier.warning('nowai'); - - const notification1 = find(Notifier.prototype._notifs, { - type: 'warning', - content: 'wat' - }); - const notification2 = find(Notifier.prototype._notifs, { - type: 'warning', - content: 'nowai' - }); - - expect(notification1).not.to.be(undefined); - expect(notification2).not.to.be(undefined); - }); - - it('only adds a notification if it was not previously added in the current route', () => { - routeBasedNotifier.warning('wat'); - routeBasedNotifier.warning('wat'); - - const notification = find(Notifier.prototype._notifs, { - type: 'warning', - content: 'wat' - }); - - expect(notification.count).to.equal(1); - }); - - it('can add a previously added notification so long as the route changes', () => { - routeBasedNotifier.warning('wat'); - const notification1 = find(Notifier.prototype._notifs, { - type: 'warning', - content: 'wat' - }); - expect(notification1.count).to.equal(1); - - $rootScope.$broadcast('$routeChangeSuccess'); - - routeBasedNotifier.warning('wat'); - const notification2 = find(Notifier.prototype._notifs, { - type: 'warning', - content: 'wat' - }); - expect(notification2.count).to.equal(2); - }); - }); -}); diff --git a/src/ui/public/route_based_notifier/index.js b/src/ui/public/route_based_notifier/index.js deleted file mode 100644 index 51814c17cb1bd..0000000000000 --- a/src/ui/public/route_based_notifier/index.js +++ /dev/null @@ -1,64 +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 { includes, mapValues } from 'lodash'; -import { Notifier } from '../notify'; - -/* - * Caches notification attempts so each one is only actually sent to the - * notifier service once per route. - */ -export function RouteBasedNotifierProvider($rootScope) { - const notifier = new Notifier(); - - let notifications = { - warnings: [] - }; - - // empty the tracked notifications whenever the route changes so we can start - // fresh for the next route cycle - $rootScope.$on('$routeChangeSuccess', () => { - notifications = mapValues(notifications, () => []); - }); - - // Executes the given notify function if the message has not been seen in - // this route cycle - function executeIfNew(messages, message, notifyFn) { - if (includes(messages, message)) { - return; - } - - messages.push(message); - notifyFn.call(notifier, message); - } - - return { - /** - * Notify a given warning once in this route cycle - * @param {string} message - */ - warning(message) { - executeIfNew( - notifications.warnings, - message, - notifier.warning - ); - } - }; -} diff --git a/src/ui/public/url/redirect_when_missing.js b/src/ui/public/url/redirect_when_missing.js index 8545007404b6c..04c7be8b78589 100644 --- a/src/ui/public/url/redirect_when_missing.js +++ b/src/ui/public/url/redirect_when_missing.js @@ -20,13 +20,12 @@ import { SavedObjectNotFound } from '../errors'; import { uiModules } from '../modules'; +import { toastNotifications } from 'ui/notify'; uiModules.get('kibana/url') .service('redirectWhenMissing', function (Private) { return Private(RedirectWhenMissingProvider); }); -export function RedirectWhenMissingProvider($location, kbnUrl, Notifier, Promise) { - const notify = new Notifier(); - +export function RedirectWhenMissingProvider($location, kbnUrl, Promise) { /** * Creates an error handler that will redirect to a url when a SavedObjectNotFound * error is thrown @@ -55,7 +54,7 @@ export function RedirectWhenMissingProvider($location, kbnUrl, Notifier, Promise url += (url.indexOf('?') >= 0 ? '&' : '?') + `notFound=${err.savedObjectType}`; - notify.warning(err); + toastNotifications.addWarning(err.message); kbnUrl.redirect(url); return Promise.halt(); }; diff --git a/x-pack/plugins/graph/public/app.js b/x-pack/plugins/graph/public/app.js index 49b35816b326f..10b398453957a 100644 --- a/x-pack/plugins/graph/public/app.js +++ b/x-pack/plugins/graph/public/app.js @@ -7,6 +7,7 @@ import d3 from 'd3'; import 'ace'; import rison from 'rison-node'; +import React from 'react'; // import the uiExports that we want to "use" import 'uiExports/fieldFormats'; @@ -390,7 +391,7 @@ app.controller('graphuiPlugin', function ($scope, $route, $interval, $http, kbnU return $http.post('../api/graph/graphExplore', request) .then(function (resp) { if (resp.data.resp.timed_out) { - notify.warning('Exploration timed out'); + toastNotifications.addWarning('Exploration timed out'); } responseHandler(resp.data.resp); }) @@ -538,7 +539,10 @@ app.controller('graphuiPlugin', function ($scope, $route, $interval, $http, kbnU $scope.saveUrlTemplate = function () { const found = $scope.newUrlTemplate.url.search(drillDownRegex) > -1; if (!found) { - notify.warning('Invalid URL - the url must contain a {{gquery}} string'); + toastNotifications.addWarning({ + title: 'Invalid URL', + text: 'The URL must contain a {{gquery}} string', + }); return; } if ($scope.newUrlTemplate.templateBeingEdited) { @@ -715,9 +719,14 @@ app.controller('graphuiPlugin', function ($scope, $route, $interval, $http, kbnU .on('zoom', redraw)); + const managementUrl = chrome.getNavLinkById('kibana:management').url; + const url = `${managementUrl}/kibana/indices`; if ($scope.indices.length === 0) { - notify.warning('Oops, no data sources. First head over to Kibana settings and define a choice of index pattern'); + toastNotifications.addWarning({ + title: 'No data source', + text:

Go to Management > Index Patterns and create an index pattern

, + }); } @@ -941,7 +950,7 @@ app.controller('graphuiPlugin', function ($scope, $route, $interval, $http, kbnU if ($scope.allSavingDisabled) { // It should not be possible to navigate to this function if allSavingDisabled is set // but adding check here as a safeguard. - notify.warning('Saving is disabled'); + toastNotifications.addWarning('Saving is disabled'); return; } initWorkspaceIfRequired(); diff --git a/x-pack/plugins/ml/public/components/job_select_list/job_select_service.js b/x-pack/plugins/ml/public/components/job_select_list/job_select_service.js index cda64fe84ed13..9e9f31c26ab08 100644 --- a/x-pack/plugins/ml/public/components/job_select_list/job_select_service.js +++ b/x-pack/plugins/ml/public/components/job_select_list/job_select_service.js @@ -9,7 +9,7 @@ // Service with functions used for broadcasting job picker changes import _ from 'lodash'; -import { notify } from 'ui/notify'; +import { toastNotifications } from 'ui/notify'; import { mlJobService } from 'plugins/ml/services/job_service'; @@ -43,7 +43,7 @@ export function JobSelectServiceProvider($rootScope, globalState) { // if there are no valid ids, warn and then select the first job if (validIds.length === 0) { const warningText = `No jobs selected, auto selecting first job`; - notify.warning(warningText, { lifetime: 30000 }); + toastNotifications.addWarning(warningText); if (mlJobService.jobs.length) { validIds = [mlJobService.jobs[0].job_id]; @@ -91,7 +91,7 @@ export function JobSelectServiceProvider($rootScope, globalState) { if (invalidIds.length > 0) { const warningText = (invalidIds.length === 1) ? `Requested job ${invalidIds} does not exist` : `Requested jobs ${invalidIds} do not exist`; - notify.warning(warningText, { lifetime: 30000 }); + toastNotifications.addWarning(warningText); } } diff --git a/x-pack/plugins/ml/public/license/check_license.js b/x-pack/plugins/ml/public/license/check_license.js index 59d18e7148593..2c9cca1958e63 100644 --- a/x-pack/plugins/ml/public/license/check_license.js +++ b/x-pack/plugins/ml/public/license/check_license.js @@ -5,14 +5,14 @@ */ - +import React from 'react'; import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info'; -import { notify, Notifier } from 'ui/notify'; -import _ from 'lodash'; - +import { Notifier, banners } from 'ui/notify'; import chrome from 'ui/chrome'; +import { EuiCallOut } from '@elastic/eui'; let licenseHasExpired = true; +let expiredLicenseBannerId; export function checkLicense(Private, kbnBaseUrl) { const xpackInfo = Private(XPackInfoProvider); @@ -36,10 +36,17 @@ export function checkLicense(Private, kbnBaseUrl) { // Therefore we need to keep the app enabled but show an info banner to the user. if(licenseHasExpired) { const message = features.message; - const exists = _.find(notify._notifs, (item) => item.content === message); - if (!exists) { - // Only show the banner once with no countdown - notify.warning(message, { lifetime: 0 }); + if (expiredLicenseBannerId === undefined) { + // Only show the banner once with no way to dismiss it + expiredLicenseBannerId = banners.add({ + component: ( + + ), + }); } } diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_controller.js b/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_controller.js index 72371ac16c698..8ce3a5c684149 100644 --- a/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_controller.js +++ b/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_controller.js @@ -18,7 +18,7 @@ import moment from 'moment'; import 'plugins/ml/components/anomalies_table'; import 'plugins/ml/components/controls'; -import { notify } from 'ui/notify'; +import { toastNotifications } from 'ui/notify'; import uiRoutes from 'ui/routes'; import { timefilter } from 'ui/timefilter'; import { parseInterval } from 'ui/utils/parse_interval'; @@ -129,24 +129,24 @@ module.controller('MlTimeSeriesExplorerController', function ( selectedJobIds = _.without(selectedJobIds, ...invalidIds); if (invalidIds.length > 0) { const s = invalidIds.length === 1 ? '' : 's'; - let warningText = `Requested job${s} ${invalidIds} cannot be viewed in this dashboard`; + let warningText = `You can't view requested job${s} ${invalidIds} in this dashboard`; if (selectedJobIds.length === 0 && timeSeriesJobIds.length > 0) { warningText += ', auto selecting first job'; } - notify.warning(warningText, { lifetime: 30000 }); + toastNotifications.addWarning(warningText); } if (selectedJobIds.length > 1 || mlJobSelectService.groupIds.length) { // if more than one job or a group has been loaded from the URL if (selectedJobIds.length > 1) { // if more than one job, select the first job from the selection. - notify.warning('Only one job may be viewed at a time in this dashboard', { lifetime: 30000 }); + toastNotifications.addWarning('You can only view one job at a time in this dashboard'); mlJobSelectService.setJobIds([selectedJobIds[0]]); } else { // if a group has been loaded if (selectedJobIds.length > 0) { // if the group contains valid jobs, select the first - notify.warning('Only one job may be viewed at a time in this dashboard', { lifetime: 30000 }); + toastNotifications.addWarning('You can only view one job at a time in this dashboard'); mlJobSelectService.setJobIds([selectedJobIds[0]]); } else if ($scope.jobs.length > 0) { // if there are no valid jobs in the group but there are valid jobs @@ -660,7 +660,7 @@ module.controller('MlTimeSeriesExplorerController', function ( let detectorIndex = appStateDtrIdx !== undefined ? appStateDtrIdx : +(viewableDetectors[0].index); if (_.find(viewableDetectors, { 'index': '' + detectorIndex }) === undefined) { const warningText = `Requested detector index ${detectorIndex} is not valid for job ${$scope.selectedJob.job_id}`; - notify.warning(warningText, { lifetime: 30000 }); + toastNotifications.addWarning(warningText); detectorIndex = +(viewableDetectors[0].index); $scope.appState.mlTimeSeriesExplorer.detectorIndex = detectorIndex; $scope.appState.save(); diff --git a/x-pack/plugins/ml/public/util/index_utils.js b/x-pack/plugins/ml/public/util/index_utils.js index fdd75b69774b9..b34e304d0976d 100644 --- a/x-pack/plugins/ml/public/util/index_utils.js +++ b/x-pack/plugins/ml/public/util/index_utils.js @@ -6,7 +6,7 @@ -import { notify } from 'ui/notify'; +import { toastNotifications } from 'ui/notify'; import { SavedObjectsClientProvider } from 'ui/saved_objects'; let indexPatternCache = []; @@ -69,9 +69,10 @@ export function getCurrentSavedSearch() { export function timeBasedIndexCheck(indexPattern, showNotification = false) { if (indexPattern.isTimeBased() === false) { if (showNotification) { - const message = `The index pattern ${indexPattern.title} is not time series based. \ - Anomaly detection can only be run over indices which are time based.`; - notify.warning(message, { lifetime: 0 }); + toastNotifications.addWarning({ + title: `The index pattern ${indexPattern.title} is not based on a time series`, + text: 'Anomaly detection only runs over time-based indices', + }); } return false; } else { diff --git a/x-pack/plugins/monitoring/public/directives/cluster/listing/index.js b/x-pack/plugins/monitoring/public/directives/cluster/listing/index.js index 6bc045cb3efa3..cc43e9621145d 100644 --- a/x-pack/plugins/monitoring/public/directives/cluster/listing/index.js +++ b/x-pack/plugins/monitoring/public/directives/cluster/listing/index.js @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { Fragment } from 'react'; import { render } from 'react-dom'; import { capitalize, get } from 'lodash'; import moment from 'moment'; import numeral from '@elastic/numeral'; import { uiModules } from 'ui/modules'; +import chrome from 'ui/chrome'; import { KuiTableRowCell, KuiTableRow @@ -18,7 +19,7 @@ import { EuiHealth, EuiLink, } from '@elastic/eui'; -import { Notifier } from 'ui/notify'; +import { toastNotifications } from 'ui/notify'; import { MonitoringTable } from 'plugins/monitoring/components/table'; import { Tooltip } from 'plugins/monitoring/components/tooltip'; import { AlertsIndicator } from 'plugins/monitoring/components/cluster/listing/alerts_indicator'; @@ -35,11 +36,11 @@ const columns = [ { title: 'Kibana', sortKey: 'kibana.count' }, { title: 'License', sortKey: 'license.type' } ]; + const clusterRowFactory = (scope, globalState, kbnUrl, showLicenseExpiration) => { return class ClusterRow extends React.Component { constructor(props) { super(props); - this.notify = new Notifier(); } changeCluster() { @@ -51,33 +52,45 @@ const clusterRowFactory = (scope, globalState, kbnUrl, showLicenseExpiration) => }); } - licenseWarning(message) { + licenseWarning({ title, text }) { scope.$evalAsync(() => { - this.notify.warning(message, { - lifetime: 60000 - }); + toastNotifications.addWarning({ title, text, 'data-test-subj': 'monitoringLicenseWarning' }); }); } handleClickIncompatibleLicense() { - this.licenseWarning( - `You can't view the "${this.props.cluster_name}" cluster because the -Basic license does not support multi-cluster monitoring. - -Need to monitor multiple clusters? [Get a license with full functionality](https://www.elastic.co/subscriptions/xpack) -to enjoy multi-cluster monitoring.` - ); + this.licenseWarning({ + title: `You can't view the "${this.props.cluster_name}" cluster`, + text: ( + +

The Basic license does not support multi-cluster monitoring.

+

+ Need to monitor multiple clusters?{' '} + Get a license with full functionality{' '} + to enjoy multi-cluster monitoring. +

+
+ ), + }); } handleClickInvalidLicense() { - this.licenseWarning( - `You can't view the "${this.props.cluster_name}" cluster because the -license information is invalid. + const licensingPath = `${chrome.getBasePath()}/app/kibana#/management/elasticsearch/license_management/home`; -Need a license? [Get a free Basic license](https://register.elastic.co/xpack_register) -or get a license with [full functionality](https://www.elastic.co/subscriptions/xpack) -to enjoy multi-cluster monitoring.` - ); + this.licenseWarning({ + title: `You can't view the "${this.props.cluster_name}" cluster`, + text: ( + +

The license information is invalid.

+

+ Need a license?{' '} + Get a free Basic license or{' '} + get a license with full functionality{' '} + to enjoy multi-cluster monitoring. +

+
+ ), + }); } getClusterAction() { diff --git a/x-pack/plugins/monitoring/public/views/cluster/listing/index.js b/x-pack/plugins/monitoring/public/views/cluster/listing/index.js index d192551b3d509..1ac9c633937d8 100644 --- a/x-pack/plugins/monitoring/public/views/cluster/listing/index.js +++ b/x-pack/plugins/monitoring/public/views/cluster/listing/index.js @@ -26,7 +26,7 @@ uiRoutes.when('/home', { return Promise.reject(); } if (clusters.length === 1) { - // Bypass the cluster listing if there is just 1 cluster + // Bypass the cluster listing if there is just 1 cluster kbnUrl.changePath('/overview'); return Promise.reject(); } diff --git a/x-pack/test/functional/apps/monitoring/cluster/list.js b/x-pack/test/functional/apps/monitoring/cluster/list.js index ce393019b8b40..95fd60a045445 100644 --- a/x-pack/test/functional/apps/monitoring/cluster/list.js +++ b/x-pack/test/functional/apps/monitoring/cluster/list.js @@ -10,6 +10,7 @@ import { getLifecycleMethods } from '../_get_lifecycle_methods'; export default function ({ getService, getPageObjects }) { const clusterList = getService('monitoringClusterList'); const clusterOverview = getService('monitoringClusterOverview'); + const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['monitoring', 'header']); describe('Cluster listing', () => { @@ -57,14 +58,7 @@ export default function ({ getService, getPageObjects }) { it('clicking the basic cluster shows a toast message', async () => { const basicClusterLink = await clusterList.getClusterLink(UNSUPPORTED_CLUSTER_UUID); await basicClusterLink.click(); - - const actualMessage = await PageObjects.header.getToastMessage(); - const expectedMessage = ( - `You can't view the "clustertwo" cluster because the Basic license does not support multi-cluster monitoring. -Need to monitor multiple clusters? Get a license with full functionality to enjoy multi-cluster monitoring.` - ); - expect(actualMessage).to.be(expectedMessage); - await PageObjects.header.clickToastOK(); + expect(await testSubjects.exists('monitoringLicenseWarning', 2000)).to.be(true); }); /* @@ -121,14 +115,7 @@ Need to monitor multiple clusters? Get a license with full functionality to enjo it('clicking the non-primary basic cluster shows a toast message', async () => { const basicClusterLink = await clusterList.getClusterLink(UNSUPPORTED_CLUSTER_UUID); await basicClusterLink.click(); - - const actualMessage = await PageObjects.header.getToastMessage(); - const expectedMessage = ( - `You can't view the "staging" cluster because the Basic license does not support multi-cluster monitoring. -Need to monitor multiple clusters? Get a license with full functionality to enjoy multi-cluster monitoring.` - ); - expect(actualMessage).to.be(expectedMessage); - await PageObjects.header.clickToastOK(); + expect(await testSubjects.exists('monitoringLicenseWarning', 2000)).to.be(true); }); it('clicking the primary basic cluster goes to overview', async () => { From cdafa2f0927e3bcc447a6a9769047fcd16fa9e5c Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Sat, 21 Jul 2018 11:49:41 +0100 Subject: [PATCH 02/12] [ML] Adding filters privileges (#21021) --- .../ml/public/privilege/get_privileges.js | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/x-pack/plugins/ml/public/privilege/get_privileges.js b/x-pack/plugins/ml/public/privilege/get_privileges.js index 98ea39fa8003a..635b074c30a6b 100644 --- a/x-pack/plugins/ml/public/privilege/get_privileges.js +++ b/x-pack/plugins/ml/public/privilege/get_privileges.js @@ -31,6 +31,7 @@ export function getPrivileges() { 'cluster:monitor/xpack/ml/job/stats/get', 'cluster:monitor/xpack/ml/datafeeds/get', 'cluster:monitor/xpack/ml/datafeeds/stats/get', + 'cluster:monitor/xpack/ml/calendars/get', 'cluster:admin/xpack/ml/job/put', 'cluster:admin/xpack/ml/job/delete', 'cluster:admin/xpack/ml/job/update', @@ -48,6 +49,10 @@ export function getPrivileges() { 'cluster:admin/xpack/ml/calendars/jobs/update', 'cluster:admin/xpack/ml/calendars/events/post', 'cluster:admin/xpack/ml/calendars/events/delete', + 'cluster:admin/xpack/ml/filters/put', + 'cluster:admin/xpack/ml/filters/get', + 'cluster:admin/xpack/ml/filters/update', + 'cluster:admin/xpack/ml/filters/delete', ] }; @@ -110,6 +115,10 @@ export function getPrivileges() { privileges.canPreviewDatafeed = true; } + if (resp.cluster['cluster:monitor/xpack/ml/calendars/get']) { + privileges.canGetCalendars = true; + } + if (resp.cluster['cluster:admin/xpack/ml/calendars/put'] && resp.cluster['cluster:admin/xpack/ml/calendars/jobs/update'] && resp.cluster['cluster:admin/xpack/ml/calendars/events/post']) { @@ -120,6 +129,20 @@ export function getPrivileges() { resp.cluster['cluster:admin/xpack/ml/calendars/events/delete']) { privileges.canDeleteCalendar = true; } + + if (resp.cluster['cluster:admin/xpack/ml/filters/get']) { + privileges.canGetFilters = true; + } + + if (resp.cluster['cluster:admin/xpack/ml/filters/put'] && + resp.cluster['cluster:admin/xpack/ml/filters/update']) { + privileges.canCreateFilter = true; + } + + if (resp.cluster['cluster:admin/xpack/ml/filters/delete']) { + privileges.canDeleteFilter = true; + } + } resolve(privileges); From 6ecc9902741a79637e881d4301eae9aafa7b4e80 Mon Sep 17 00:00:00 2001 From: "dave.snider@gmail.com" Date: Sat, 21 Jul 2018 08:02:50 -0700 Subject: [PATCH 03/12] Upgrade xpack to eui@3.0.0 (#20930) * upgrade to eui@3.0.1 --- .../dashboard_listing.test.js.snap | 34 +- .../dashboard/listing/dashboard_listing.js | 4 +- .../kibana/public/dashboard/styles/index.less | 5 - x-pack/package.json | 2 +- .../components/index_table/index_table.js | 139 ++--- .../index_management/public/styles/table.less | 4 + .../__snapshots__/license_status.test.js.snap | 4 +- .../__snapshots__/upload_license.test.js.snap | 182 +++--- .../public/styles/main.less | 1 + .../collection_enabled.test.js.snap | 1 + .../collection_interval.test.js.snap | 2 + .../monitoring/public/less/views/license.less | 1 + x-pack/yarn.lock | 532 +++++++++++++++++- yarn.lock | 21 - 14 files changed, 700 insertions(+), 232 deletions(-) diff --git a/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap b/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap index ea1f1ec62bf1f..0d01cb7ebd034 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap +++ b/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap @@ -6,12 +6,12 @@ exports[`after fetch hideWriteControls 1`] = ` data-test-subj="dashboardLandingPage" restrictWidth={false} > - + - +
- +
- +
- +
- + `; diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js index f7b9265960e75..e1a077c995c99 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js @@ -454,7 +454,7 @@ export class DashboardListing extends React.Component { } return ( - + {this.renderListingOrEmptyState()} ); @@ -463,7 +463,7 @@ export class DashboardListing extends React.Component { render() { return ( - + {this.renderPageContent()} diff --git a/src/core_plugins/kibana/public/dashboard/styles/index.less b/src/core_plugins/kibana/public/dashboard/styles/index.less index d9ca73adbe498..ea7b5b3c3bf88 100644 --- a/src/core_plugins/kibana/public/dashboard/styles/index.less +++ b/src/core_plugins/kibana/public/dashboard/styles/index.less @@ -438,8 +438,3 @@ dashboard-viewport-provider { min-height: 100vh; background: @globalColorLightestGray; } - -.dashboardLandingPage__content { - max-width: 1000px; - margin: auto; -} diff --git a/x-pack/package.json b/x-pack/package.json index 2319c331d7991..b8b20808e115e 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -77,7 +77,7 @@ "yargs": "4.7.1" }, "dependencies": { - "@elastic/eui": "1.1.0", + "@elastic/eui": "3.0.0", "@elastic/node-crypto": "0.1.2", "@elastic/node-phantom-simple": "2.2.4", "@elastic/numeral": "2.3.2", diff --git a/x-pack/plugins/index_management/public/sections/index_list/components/index_table/index_table.js b/x-pack/plugins/index_management/public/sections/index_list/components/index_table/index_table.js index 69005e262c077..1def408fd76b8 100644 --- a/x-pack/plugins/index_management/public/sections/index_list/components/index_table/index_table.js +++ b/x-pack/plugins/index_management/public/sections/index_list/components/index_table/index_table.js @@ -32,7 +32,8 @@ import { EuiTableRowCellCheckbox, EuiTitle, EuiText, - EuiPageBody + EuiPageBody, + EuiPageContent } from '@elastic/eui'; import { IndexActionsContextMenu } from '../../components'; @@ -216,78 +217,80 @@ export class IndexTable extends Component { return ( - - - -

Index management

-
- - -

Update your Elasticsearch indices individually or in bulk

-
-
- - showSystemIndicesChanged(event.target.checked)} - label="Include system indices" - /> - -
- - - {atLeastOneItemSelected ? ( + + - ( - { - this.setState({ selectedIndicesMap: {} }); - }} - /> - )} + +

Index management

+
+ + +

Update your Elasticsearch indices individually or in bulk

+
+
+ + showSystemIndicesChanged(event.target.checked)} + label="Include system indices" + /> + +
+ + + {atLeastOneItemSelected ? ( + + ( + { + this.setState({ selectedIndicesMap: {} }); + }} + /> + )} + /> + + ) : null} + + { + filterChanged(event.target.value); + }} + data-test-subj="indexTableFilterInput" + placeholder="Search" + aria-label="Search indices" /> - ) : null} - - { - filterChanged(event.target.value); - }} - data-test-subj="indexTableFilterInput" - placeholder="Search" - aria-label="Search indices" - /> - - +
- + - {indices.length > 0 ? ( - - - - - - {this.buildHeader()} - - {this.buildRows()} - - ) : ( - - )} - - {indices.length > 0 ? this.renderPager() : null} + {indices.length > 0 ? ( + + + + + + {this.buildHeader()} + + {this.buildRows()} + + ) : ( + + )} + + {indices.length > 0 ? this.renderPager() : null} +
); diff --git a/x-pack/plugins/index_management/public/styles/table.less b/x-pack/plugins/index_management/public/styles/table.less index bd2b7a65c443e..a93fe289f9b63 100644 --- a/x-pack/plugins/index_management/public/styles/table.less +++ b/x-pack/plugins/index_management/public/styles/table.less @@ -1,3 +1,7 @@ +#indexManagementReactRoot { + background-color: #F5F5F5; +} + .indexTable__link { text-align: left; } diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap index eb450e31af66b..4b9e830539cb8 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`LicenseStatus component should display display warning is expired 1`] = `"

Your Platinum license has expired

Your license expired on
"`; +exports[`LicenseStatus component should display display warning is expired 1`] = `"

Your Platinum license has expired

Your license expired on
"`; -exports[`LicenseStatus component should display normally when license is active 1`] = `"

Your Gold license is active

Your license will expire on October 12, 2099 7:00 PM EST
"`; +exports[`LicenseStatus component should display normally when license is active 1`] = `"

Your Gold license is active

Your license will expire on October 12, 2099 7:00 PM EST
"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.js.snap index d53f0e6e03b01..dd7b0fe314fbf 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.js.snap @@ -174,121 +174,125 @@ exports[`UploadLicense should display a modal when license requires acknowledgem - -
- + +
-
- Confirm License Upload -
- -
-
- -
- + Confirm License Upload +
+
+
+
+ +
-
-
- -
+
+ - Some functionality will be lost if you replace your +
+ Some functionality will be lost if you replace your TRIAL license with a BASIC license. Review the list of features below. -
-
- -
+ + -
    -
  • - Watcher will be disabled -
  • -
-
-
+
+
    +
  • + Watcher will be disabled +
  • +
+
+ +
-
- -
- - -
- +
+ + +
- - - - - -
-
+ + +
+ +
diff --git a/x-pack/plugins/license_management/public/styles/main.less b/x-pack/plugins/license_management/public/styles/main.less index 54628cc8bd925..0de6fa903ff17 100644 --- a/x-pack/plugins/license_management/public/styles/main.less +++ b/x-pack/plugins/license_management/public/styles/main.less @@ -3,6 +3,7 @@ .licenseFeature { flex-grow: 1; background: @globalColorLightestGray; + min-height: 100vh; } .licenseManagement__pageBody { diff --git a/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap index 0c6aeb5eb13de..db278ee996bca 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap @@ -23,6 +23,7 @@ exports[`ExplainCollectionEnabled should explain about xpack.monitoring.collecti = 1.34.0 < 2" + +compression@^1.6.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.3.tgz#27e0e176aaf260f7f2c2813c3e440adb9f1993db" + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.14" + debug "2.6.9" + on-headers "~1.0.1" + safe-buffer "5.1.2" + vary "~1.1.2" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -1566,6 +1677,10 @@ content-type-parser@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.2.tgz#caabe80623e63638b2502fd4c7f12ff4ce2352e7" +content-type@1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + content@3.x.x: version "3.0.6" resolved "https://registry.yarnpkg.com/content/-/content-3.0.6.tgz#9c2e301e9ae515ed65a4b877d78aa5659bb1b809" @@ -1593,8 +1708,8 @@ core-js@^2.4.0, core-js@^2.5.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e" core-js@^2.5.1: - version "2.5.5" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.5.tgz#b14dde936c640c0579a6b50cabcc132dd6127e3b" + version "2.5.7" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -1837,6 +1952,10 @@ d3@3.5.6: version "3.5.6" resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.6.tgz#9451c651ca733fb9672c81fb7f2655164a73a42d" +dargs@5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/dargs/-/dargs-5.1.0.tgz#ec7ea50c78564cd36c9d5ec18f66329fade27829" + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -1870,7 +1989,7 @@ debug@2.6.0: dependencies: ms "0.7.2" -debug@^2.2.0, debug@^2.3.3, debug@^2.6.8: +debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.8: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" dependencies: @@ -1910,6 +2029,10 @@ deep-equal@^1.0.0, deep-equal@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + deep-extend@~0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" @@ -1990,10 +2113,22 @@ delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" +depd@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + deprecated@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/deprecated/-/deprecated-0.0.1.tgz#f9c9af5464afa1e7a971458a8bdef2aa94d5bb19" +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + detect-file@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" @@ -2012,6 +2147,13 @@ detect-newline@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" +detect-port@1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/detect-port/-/detect-port-1.2.3.tgz#15bf49820d02deb84bfee0a74876b32d791bf610" + dependencies: + address "^1.0.1" + debug "^2.6.0" + dfa@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/dfa/-/dfa-1.1.0.tgz#d30218bd10d030fa421df3ebbc82285463a31781" @@ -2122,6 +2264,10 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + elasticsearch@^14.1.0: version "14.2.2" resolved "https://registry.yarnpkg.com/elasticsearch/-/elasticsearch-14.2.2.tgz#6bbb63b19b17fa97211b22eeacb0f91197f4d6b6" @@ -2133,6 +2279,10 @@ elasticsearch@^14.1.0: lodash.isempty "^4.4.0" lodash.trimend "^4.5.1" +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + encoding@^0.1.11: version "0.1.12" resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" @@ -2225,6 +2375,10 @@ es6-promise@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-2.0.1.tgz#ccc4963e679f0ca9fb187c777b9e583d3c7573c2" +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + escape-string-regexp@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz#4dbc2fe674e71949caf3fb2695ce7f2dc1d9a8d1" @@ -2309,6 +2463,10 @@ esutils@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/esutils/-/esutils-1.0.0.tgz#8151d358e20c8acc7fb745e7472c0025fe496570" +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + eventemitter3@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" @@ -2343,6 +2501,18 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +execa@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.8.0.tgz#d8d76bbc1b55217ed190fd6dd49d3c774ecfc8da" + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + exit-hook@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" @@ -2540,6 +2710,10 @@ fileset@^2.0.2: glob "^7.0.3" minimatch "^3.0.3" +filesize@3.6.1: + version "3.6.1" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317" + fill-keys@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/fill-keys/-/fill-keys-1.0.2.tgz#9a8fa36f4e8ad634e3bf6b4f3c8882551452eb20" @@ -2715,6 +2889,10 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + from2@^2.1.1: version "2.3.0" resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" @@ -2726,6 +2904,14 @@ from@^0.1.3: version "0.1.7" resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" +fs-extra@6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-6.0.1.tgz#8abc128f7946e310135ddc93b98bddb410e7a34b" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-mkdirp-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz#0b7815fc3201c6a69e14db98ce098c16935259eb" @@ -3228,7 +3414,7 @@ gulplog@^1.0.0: dependencies: glogg "^1.0.0" -handlebars@^4.0.3: +handlebars@4.0.11, handlebars@^4.0.3: version "4.0.11" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc" dependencies: @@ -3430,6 +3616,10 @@ hoek@3.x.x: version "3.0.4" resolved "https://registry.yarnpkg.com/hoek/-/hoek-3.0.4.tgz#268adff66bb6695c69b4789a88b1e0847c3f3123" +hoek@4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" + hoek@4.x.x: version "4.2.0" resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" @@ -3486,6 +3676,15 @@ http-cache-semantics@3.8.1: version "3.8.1" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" +http-errors@1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" + dependencies: + depd "1.1.1" + inherits "2.0.3" + setprototypeof "1.0.3" + statuses ">= 1.3.1 < 2" + http-errors@~1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.4.0.tgz#6c0242dea6b3df7afda153c71089b31c6e82aabf" @@ -3493,6 +3692,15 @@ http-errors@~1.4.0: inherits "2.0.1" statuses ">= 1.2.1 < 2" +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + http-signature@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" @@ -3569,7 +3777,7 @@ inherits@1: version "1.0.2" resolved "https://registry.yarnpkg.com/inherits/-/inherits-1.0.2.tgz#ca4309dadee6b54cc0b8d247e8d7c7a0975bdc9b" -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" @@ -3621,6 +3829,10 @@ invert-kv@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" +ip@1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + iron@4.x.x: version "4.0.5" resolved "https://registry.yarnpkg.com/iron/-/iron-4.0.5.tgz#4f042cceb8b9738f346b59aa734c83a89bc31428" @@ -3866,7 +4078,7 @@ is-retry-allowed@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34" -is-stream@^1.0.1, is-stream@^1.1.0: +is-stream@1.1.0, is-stream@^1.0.1, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -3900,6 +4112,10 @@ is-windows@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.1.tgz#310db70f742d259a16a369202b51af84233310d9" +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -4344,6 +4560,10 @@ js-tokens@^3.0.0, js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + js-yaml@^3.7.0: version "3.10.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" @@ -4422,6 +4642,12 @@ json5@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + optionalDependencies: + graceful-fs "^4.1.6" + jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" @@ -4519,7 +4745,7 @@ left-pad@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.2.0.tgz#d30a73c6b8201d8f7d8e7956ba9616087a68e0ee" -leven@^2.1.0: +leven@2.1.0, leven@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580" @@ -4841,12 +5067,18 @@ longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.0, loose-envify@^1.3.1: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" dependencies: js-tokens "^3.0.0" +loose-envify@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + loud-rejection@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" @@ -4948,6 +5180,22 @@ methods@^1.1.1, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" +micro-compress@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/micro-compress/-/micro-compress-1.0.0.tgz#53f5a80b4ad0320ca165a559b6e3df145d4f704f" + dependencies: + compression "^1.6.2" + +micro@9.3.1: + version "9.3.1" + resolved "https://registry.yarnpkg.com/micro/-/micro-9.3.1.tgz#0c37eba0171554b1beccda5215ff8ea4e7aa59d6" + dependencies: + arg "2.0.0" + chalk "2.4.0" + content-type "1.0.4" + is-stream "1.1.0" + raw-body "2.3.2" + micromatch@^2.1.5, micromatch@^2.3.11: version "2.3.11" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" @@ -4988,16 +5236,40 @@ mime-db@1.x.x: version "1.32.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.32.0.tgz#485b3848b01a3cda5f968b4882c0771e58e09414" +"mime-db@>= 1.34.0 < 2", mime-db@~1.35.0: + version "1.35.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.35.0.tgz#0569d657466491283709663ad379a99b90d9ab47" + mime-db@~1.30.0: version "1.30.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" +mime-db@~1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" + +mime-types@2.1.18: + version "2.1.18" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" + dependencies: + mime-db "~1.33.0" + mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.7: version "2.1.17" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" dependencies: mime-db "~1.30.0" +mime-types@~2.1.18: + version "2.1.19" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.19.tgz#71e464537a7ef81c15f2db9d97e913fc0ff606f0" + dependencies: + mime-db "~1.35.0" + +mime@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" + mime@^1.4.1: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" @@ -5137,6 +5409,10 @@ moment@2.x.x, "moment@>= 2.9.0", moment@^2.13.0, moment@^2.20.1: version "2.20.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.20.1.tgz#d6eb1a46cbcc14a2b2f9434112c1ff8907f313fd" +mri@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.0.tgz#5c0a3f29c8ccffbbb1ec941dcec09d71fa32f36a" + ms@0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" @@ -5215,6 +5491,10 @@ nearley@^2.7.10: railroad-diagrams "^1.0.0" randexp "^0.4.2" +negotiator@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" + ngreact@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/ngreact/-/ngreact-0.5.1.tgz#2dcccc1541771796689d13e51bb8d5010af41c57" @@ -5321,6 +5601,10 @@ node-sass@^4.9.0: stdout-stream "^1.4.0" "true-case-path" "^1.0.2" +node-version@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/node-version/-/node-version-1.1.3.tgz#1081c87cce6d2dbbd61d0e51e28c287782678496" + nodemailer@^4.6.4: version "4.6.4" resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-4.6.4.tgz#f0d72d0c6a6ec5f4369fa8f4bf5127a31baa2014" @@ -5512,6 +5796,16 @@ object.values@^1.0.4: function-bind "^1.1.0" has "^1.0.1" +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" + once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.3.3, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -5528,6 +5822,16 @@ onetime@^1.0.0: version "1.1.0" resolved "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" +openssl-self-signed-certificate@1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/openssl-self-signed-certificate/-/openssl-self-signed-certificate-1.1.6.tgz#9d3a4776b1a57e9847350392114ad2f915a83dd4" + +opn@5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.3.0.tgz#64871565c863875f052cfdf53d3e3cb5adb53b1c" + dependencies: + is-wsl "^1.1.0" + optimist@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" @@ -5725,7 +6029,7 @@ path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" -path-is-inside@^1.0.1: +path-is-inside@1.0.2, path-is-inside@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" @@ -5760,6 +6064,12 @@ path-to-regexp@^1.0.0, path-to-regexp@^1.7.0: dependencies: isarray "0.0.1" +path-type@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + dependencies: + pify "^3.0.0" + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -6122,6 +6432,28 @@ randomatic@^1.1.3: is-number "^3.0.0" kind-of "^4.0.0" +range-parser@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + +raw-body@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" + dependencies: + bytes "3.0.0" + http-errors "1.6.2" + iconv-lite "0.4.19" + unpipe "1.0.0" + +rc@^1.0.1, rc@^1.1.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + rc@^1.1.7: version "1.2.3" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.3.tgz#51575a900f8dd68381c710b4712c2154c3e2035b" @@ -6210,6 +6542,10 @@ react-is@^16.3.1: version "16.4.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.1.tgz#d624c4650d2c65dbd52c72622bbf389435d9776e" +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + react-markdown-renderer@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/react-markdown-renderer/-/react-markdown-renderer-1.4.0.tgz#f3b95bd9fc7f7bf8ab3f0150aa696b41740e7d01" @@ -6226,6 +6562,14 @@ react-motion@^0.4.8: prop-types "^15.5.8" raf "^3.1.0" +react-motion@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316" + dependencies: + performance-now "^0.2.0" + prop-types "^15.5.8" + raf "^3.1.0" + react-onclickoutside@^6.7.1: version "6.7.1" resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.7.1.tgz#6a5b5b8b4eae6b776259712c89c8a2b36b17be93" @@ -6330,14 +6674,37 @@ react-test-renderer@^16.0.0-0, react-test-renderer@^16.2.0: prop-types "^15.6.0" react-virtualized@^9.18.5: - version "9.18.5" - resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.18.5.tgz#42dd390ebaa7ea809bfcaf775d39872641679b89" + version "9.20.1" + resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.20.1.tgz#02dc08fe9070386b8c48e2ac56bce7af0208d22d" dependencies: babel-runtime "^6.26.0" classnames "^2.2.3" dom-helpers "^2.4.0 || ^3.0.0" loose-envify "^1.3.0" prop-types "^15.6.0" + react-lifecycles-compat "^3.0.4" + +react-vis@1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/react-vis/-/react-vis-1.10.2.tgz#7520bd31bb2f81a8faef49cc285f678fd0795242" + dependencies: + d3-array "^1.2.0" + d3-collection "^1.0.3" + d3-color "^1.0.3" + d3-contour "^1.1.0" + d3-format "^1.2.0" + d3-geo "^1.6.4" + d3-hierarchy "^1.1.4" + d3-interpolate "^1.1.4" + d3-sankey "^0.7.1" + d3-scale "^1.0.5" + d3-shape "^1.1.0" + d3-voronoi "^1.1.2" + deep-equal "^1.0.1" + global "^4.3.1" + hoek "4.2.1" + prop-types "^15.5.8" + react-motion "^0.5.2" react-vis@^1.8.1: version "1.8.2" @@ -6535,6 +6902,19 @@ regexpu-core@^2.0.0: regjsgen "^0.2.0" regjsparser "^0.1.4" +registry-auth-token@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.3.2.tgz#851fd49038eecb586911115af845260eec983f20" + dependencies: + rc "^1.1.6" + safe-buffer "^5.0.1" + +registry-url@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942" + dependencies: + rc "^1.0.1" + regjsgen@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" @@ -6852,10 +7232,14 @@ rxjs@^6.2.1: dependencies: tslib "^1.9.0" -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" +safe-buffer@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + samsam@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50" @@ -6930,10 +7314,55 @@ semver@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" +send@0.16.2: + version "0.16.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.6.2" + mime "1.4.1" + ms "2.0.0" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.4.0" + sequencify@~0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/sequencify/-/sequencify-0.0.7.tgz#90cff19d02e07027fd767f5ead3e7b95d1e7380c" +serve@^6.3.1: + version "6.5.8" + resolved "https://registry.yarnpkg.com/serve/-/serve-6.5.8.tgz#fd7ad6b9c10ba12084053030cc1a8b636c0a10a7" + dependencies: + args "4.0.0" + basic-auth "2.0.0" + bluebird "3.5.1" + boxen "1.3.0" + chalk "2.4.1" + clipboardy "1.2.3" + dargs "5.1.0" + detect-port "1.2.3" + filesize "3.6.1" + fs-extra "6.0.1" + handlebars "4.0.11" + ip "1.1.5" + micro "9.3.1" + micro-compress "1.0.0" + mime-types "2.1.18" + node-version "1.1.3" + openssl-self-signed-certificate "1.1.6" + opn "5.3.0" + path-is-inside "1.0.2" + path-type "3.0.0" + send "0.16.2" + update-check "1.5.1" + set-blocking@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-1.0.0.tgz#cd5e5d938048df1ac92dfe92e1f16add656f5ec5" @@ -6970,6 +7399,14 @@ setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" +setprototypeof@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + shallow-copy@~0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/shallow-copy/-/shallow-copy-0.0.1.tgz#415f42702d73d810330292cc5ee86eae1a11a170" @@ -7226,10 +7663,14 @@ static-module@^1.1.0: static-eval "~0.2.0" through2 "~0.4.1" -"statuses@>= 1.2.1 < 2": +"statuses@>= 1.2.1 < 2", statuses@~1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" +"statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + stdout-stream@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.0.tgz#a2c7c8587e54d9427ea9edb3ac3f2cd522df378b" @@ -7447,10 +7888,14 @@ tabbable@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-1.1.0.tgz#2c9a9c9f09db5bb0659f587d532548dd6ef2067b" -tabbable@^1.0.3, tabbable@^1.1.0: +tabbable@^1.0.3: version "1.1.2" resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-1.1.2.tgz#b171680aea6e0a3e9281ff23532e2e5de11c0d94" +tabbable@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-1.1.3.tgz#0e4ee376f3631e42d7977a074dbd2b3827843081" + tar-fs@1.13.0: version "1.13.0" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.13.0.tgz#4ac62c0de490dbba9e307d0a0312641091d5c45e" @@ -7514,6 +7959,12 @@ temp@^0.8.3: os-tmpdir "^1.0.0" rimraf "~2.2.6" +term-size@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69" + dependencies: + execa "^0.7.0" + test-exclude@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.1.1.tgz#4d84964b0966b0087ecc334a2ce002d3d9341e26" @@ -7843,6 +8294,14 @@ unique-stream@^2.0.2: json-stable-stringify "^1.0.0" through2-filter "^2.0.0" +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + +unpipe@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + unset-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" @@ -7850,6 +8309,13 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" +update-check@1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/update-check/-/update-check-1.5.1.tgz#24fc52266273cb8684d2f1bf9687c0e52dcf709f" + dependencies: + registry-auth-token "3.3.2" + registry-url "3.1.0" + urix@^0.1.0, urix@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" @@ -7920,6 +8386,10 @@ value-or-function@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/value-or-function/-/value-or-function-3.0.0.tgz#1c243a50b595c1be54a754bfece8563b9ff8d813" +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + venn.js@0.2.9: version "0.2.9" resolved "https://registry.yarnpkg.com/venn.js/-/venn.js-0.2.9.tgz#33c29075efa484731d59d884752900cc33033656" @@ -8118,6 +8588,12 @@ wide-align@^1.1.0: dependencies: string-width "^1.0.2" +widest-line@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.0.tgz#0142a4e8a243f8882c0233aa0e0281aa76152273" + dependencies: + string-width "^2.1.1" + window-size@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" diff --git a/yarn.lock b/yarn.lock index a48f35445bbcf..b5c25bd93d1e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -81,27 +81,6 @@ version "0.0.0" uid "" -"@elastic/eui@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-1.1.0.tgz#f4aa5702358133dd8c26295ba7c6a243c35616be" - dependencies: - classnames "^2.2.5" - core-js "^2.5.1" - focus-trap-react "^3.0.4" - highlight.js "^9.12.0" - html "^1.0.0" - keymirror "^0.1.1" - lodash "^3.10.1" - numeral "^2.0.6" - prop-types "^15.6.0" - react-ace "^5.5.0" - react-color "^2.13.8" - react-datepicker v1.4.1 - react-input-autosize "^2.2.1" - react-virtualized "^9.18.5" - tabbable "^1.1.0" - uuid "^3.1.0" - "@elastic/eui@3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-3.0.0.tgz#daa51d5ef32e0c00bcf7b53b7acea91df3a3704c" From 465ab78ef75fd0a28d3fa8ec9b6499012c1a468e Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Sun, 22 Jul 2018 14:01:45 +0100 Subject: [PATCH 04/12] [ML] Adds editor for configuring detector rules (#20989) * [ML] Adds editor for configuring detector rules * [ML] Edits to Rule Editor flyout following review --- .../ml/common/constants/detector_rule.js | 33 ++ .../ml/common/util/__tests__/job_utils.js | 63 ++ x-pack/plugins/ml/common/util/job_utils.js | 18 + .../anomalies_table/anomalies_table.js | 64 ++- .../components/anomalies_table/links_menu.js | 27 +- .../components/rule_editor/actions_section.js | 86 +++ .../rule_editor/condition_expression.js | 232 ++++++++ .../rule_editor/conditions_section.js | 70 +++ .../ml/public/components/rule_editor/index.js | 8 + .../rule_editor/rule_editor_flyout.js | 536 ++++++++++++++++++ .../rule_editor/scope_expression.js | 191 +++++++ .../components/rule_editor/scope_section.js | 123 ++++ .../select_rule_action/delete_rule_modal.js | 83 +++ .../rule_editor/select_rule_action/index.js | 8 + .../select_rule_action/rule_action_panel.js | 88 +++ .../select_rule_action/select_rule_action.js | 96 ++++ .../components/rule_editor/styles/main.less | 80 +++ .../ml/public/components/rule_editor/utils.js | 235 ++++++++ .../filter_lists/edit/styles/main.less | 1 + 19 files changed, 2015 insertions(+), 27 deletions(-) create mode 100644 x-pack/plugins/ml/common/constants/detector_rule.js create mode 100644 x-pack/plugins/ml/public/components/rule_editor/actions_section.js create mode 100644 x-pack/plugins/ml/public/components/rule_editor/condition_expression.js create mode 100644 x-pack/plugins/ml/public/components/rule_editor/conditions_section.js create mode 100644 x-pack/plugins/ml/public/components/rule_editor/index.js create mode 100644 x-pack/plugins/ml/public/components/rule_editor/rule_editor_flyout.js create mode 100644 x-pack/plugins/ml/public/components/rule_editor/scope_expression.js create mode 100644 x-pack/plugins/ml/public/components/rule_editor/scope_section.js create mode 100644 x-pack/plugins/ml/public/components/rule_editor/select_rule_action/delete_rule_modal.js create mode 100644 x-pack/plugins/ml/public/components/rule_editor/select_rule_action/index.js create mode 100644 x-pack/plugins/ml/public/components/rule_editor/select_rule_action/rule_action_panel.js create mode 100644 x-pack/plugins/ml/public/components/rule_editor/select_rule_action/select_rule_action.js create mode 100644 x-pack/plugins/ml/public/components/rule_editor/styles/main.less create mode 100644 x-pack/plugins/ml/public/components/rule_editor/utils.js diff --git a/x-pack/plugins/ml/common/constants/detector_rule.js b/x-pack/plugins/ml/common/constants/detector_rule.js new file mode 100644 index 0000000000000..cb8c7a71d59ef --- /dev/null +++ b/x-pack/plugins/ml/common/constants/detector_rule.js @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * Contains values for ML job detector rules. + */ + + +export const ACTION = { + SKIP_MODEL_UPDATE: 'skip_model_update', + SKIP_RESULT: 'skip_result', +}; + +export const FILTER_TYPE = { + EXCLUDE: 'exclude', + INCLUDE: 'include', +}; + +export const APPLIES_TO = { + ACTUAL: 'actual', + DIFF_FROM_TYPICAL: 'diff_from_typical', + TYPICAL: 'typical', +}; + +export const OPERATOR = { + LESS_THAN: 'lt', + LESS_THAN_OR_EQUAL: 'lte', + GREATER_THAN: 'gt', + GREATER_THAN_OR_EQUAL: 'gte', +}; diff --git a/x-pack/plugins/ml/common/util/__tests__/job_utils.js b/x-pack/plugins/ml/common/util/__tests__/job_utils.js index d9e03dfe1f6c6..685e76941ff37 100644 --- a/x-pack/plugins/ml/common/util/__tests__/job_utils.js +++ b/x-pack/plugins/ml/common/util/__tests__/job_utils.js @@ -12,6 +12,7 @@ import { isTimeSeriesViewJob, isTimeSeriesViewDetector, isTimeSeriesViewFunction, + getPartitioningFieldNames, isModelPlotEnabled, isJobVersionGte, mlFunctionToESAggregation, @@ -201,6 +202,68 @@ describe('ML - job utils', () => { }); }); + describe('getPartitioningFieldNames', () => { + const job = { + analysis_config: { + detectors: [ + { + function: 'count', + detector_description: 'count' + }, + { + function: 'count', + partition_field_name: 'clientip', + detector_description: 'Count by clientip' + }, + { + function: 'freq_rare', + by_field_name: 'uri', + over_field_name: 'clientip', + detector_description: 'Freq rare URI' + }, + { + function: 'sum', + field_name: 'bytes', + by_field_name: 'uri', + over_field_name: 'clientip', + partition_field_name: 'method', + detector_description: 'sum bytes' + }, + ] + } + }; + + it('returns empty array for a detector with no partitioning fields', () => { + const resp = getPartitioningFieldNames(job, 0); + expect(resp).to.be.an('array'); + expect(resp).to.be.empty(); + }); + + it('returns expected array for a detector with a partition field', () => { + const resp = getPartitioningFieldNames(job, 1); + expect(resp).to.be.an('array'); + expect(resp).to.have.length(1); + expect(resp).to.contain('clientip'); + }); + + it('returns expected array for a detector with by and over fields', () => { + const resp = getPartitioningFieldNames(job, 2); + expect(resp).to.be.an('array'); + expect(resp).to.have.length(2); + expect(resp).to.contain('uri'); + expect(resp).to.contain('clientip'); + }); + + it('returns expected array for a detector with partition, by and over fields', () => { + const resp = getPartitioningFieldNames(job, 3); + expect(resp).to.be.an('array'); + expect(resp).to.have.length(3); + expect(resp).to.contain('uri'); + expect(resp).to.contain('clientip'); + expect(resp).to.contain('method'); + }); + }); + describe('isModelPlotEnabled', () => { it('returns true for a job in which model plot has been enabled', () => { diff --git a/x-pack/plugins/ml/common/util/job_utils.js b/x-pack/plugins/ml/common/util/job_utils.js index 0cd006d8aed3b..2319ced16edcb 100644 --- a/x-pack/plugins/ml/common/util/job_utils.js +++ b/x-pack/plugins/ml/common/util/job_utils.js @@ -88,6 +88,24 @@ export function isTimeSeriesViewFunction(functionName) { return mlFunctionToESAggregation(functionName) !== null; } +// 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 = []; + const detector = job.analysis_config.detectors[detectorIndex]; + if (_.has(detector, 'partition_field_name')) { + fieldNames.push(detector.partition_field_name); + } + if (_.has(detector, 'by_field_name')) { + fieldNames.push(detector.by_field_name); + } + if (_.has(detector, 'over_field_name')) { + fieldNames.push(detector.over_field_name); + } + + return fieldNames; +} + // Returns a flag to indicate whether model plot has been enabled for a job. // If model plot is enabled for a job with a terms filter (comma separated // list of partition or by field names), performs additional checks that diff --git a/x-pack/plugins/ml/public/components/anomalies_table/anomalies_table.js b/x-pack/plugins/ml/public/components/anomalies_table/anomalies_table.js index 2842dc13ae64f..e229cae321168 100644 --- a/x-pack/plugins/ml/public/components/anomalies_table/anomalies_table.js +++ b/x-pack/plugins/ml/public/components/anomalies_table/anomalies_table.js @@ -37,6 +37,7 @@ import { mlAnomaliesTableService } from './anomalies_table_service'; import { mlFieldFormatService } from 'plugins/ml/services/field_format_service'; import { getSeverityColor } from 'plugins/ml/../common/util/anomaly_utils'; import { formatValue } from 'plugins/ml/formatters/format_value'; +import { RuleEditorFlyout } from 'plugins/ml/components/rule_editor'; const INFLUENCERS_LIMIT = 5; // Maximum number of influencers to display before a 'show more' link is added. @@ -53,7 +54,10 @@ function renderTime(date, aggregationInterval) { } function showLinksMenuForItem(item) { - return item.isTimeSeriesViewDetector || + // TODO - add in checking of user privileges to see if they can view / edit rules. + const canViewRules = true; + return canViewRules || + item.isTimeSeriesViewDetector || item.entityName === 'mlcategory' || item.customUrls !== undefined; } @@ -65,9 +69,11 @@ function getColumns( interval, timefilter, showViewSeriesLink, + showRuleEditorFlyout, itemIdToExpandedRowMap, toggleRow, filter) { + const columns = [ { name: '', @@ -186,12 +192,11 @@ function getColumns( sortable: true }); - const showExamples = items.some(item => item.entityName === 'mlcategory'); const showLinks = (showViewSeriesLink === true) || items.some(item => showLinksMenuForItem(item)); if (showLinks === true) { columns.push({ - name: 'links', + name: 'actions', render: (item) => { if (showLinksMenuForItem(item) === true) { return ( @@ -201,6 +206,7 @@ function getColumns( isAggregatedData={isAggregatedData} interval={interval} timefilter={timefilter} + showRuleEditorFlyout={showRuleEditorFlyout} /> ); } else { @@ -211,6 +217,7 @@ function getColumns( }); } + const showExamples = items.some(item => item.entityName === 'mlcategory'); if (showExamples === true) { columns.push({ name: 'category examples', @@ -238,7 +245,8 @@ class AnomaliesTable extends Component { super(props); this.state = { - itemIdToExpandedRowMap: {} + itemIdToExpandedRowMap: {}, + showRuleEditorFlyout: () => {} }; } @@ -313,6 +321,19 @@ class AnomaliesTable extends Component { } }; + setShowRuleEditorFlyoutFunction = (func) => { + this.setState({ + showRuleEditorFlyout: func + }); + } + + unsetShowRuleEditorFlyoutFunction = () => { + const showRuleEditorFlyout = () => {}; + this.setState({ + showRuleEditorFlyout + }); + } + render() { const { timefilter, tableData, filter } = this.props; @@ -336,6 +357,7 @@ class AnomaliesTable extends Component { tableData.interval, timefilter, tableData.showViewSeriesLink, + this.state.showRuleEditorFlyout, this.state.itemIdToExpandedRowMap, this.toggleRow, filter); @@ -355,20 +377,26 @@ class AnomaliesTable extends Component { }; return ( - + + + + ); } } diff --git a/x-pack/plugins/ml/public/components/anomalies_table/links_menu.js b/x-pack/plugins/ml/public/components/anomalies_table/links_menu.js index a7e9c7e6efddf..1855b5b50297e 100644 --- a/x-pack/plugins/ml/public/components/anomalies_table/links_menu.js +++ b/x-pack/plugins/ml/public/components/anomalies_table/links_menu.js @@ -12,7 +12,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { - EuiButtonEmpty, + EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem, EuiPopover @@ -337,15 +337,13 @@ export class LinksMenu extends Component { const { anomaly, showViewSeriesLink } = this.props; const button = ( - - Open link - + iconType="gear" + aria-label="Select action" + /> ); const items = []; @@ -387,6 +385,16 @@ export class LinksMenu extends Component { ); } + items.push( + { this.closePopover(); this.props.showRuleEditorFlyout(anomaly); }} + > + Configure rules + + ); + return ( + +

+ Choose the action(s) to take when the rule matches an anomaly. +

+
+ + + + -1} + onChange={onSkipResultChange} + /> + + + + + + + + + + + + -1} + onChange={onSkipModelUpdateChange} + /> + + + + + + + + ); + +} +ActionsSection.propTypes = { + actions: PropTypes.array.isRequired, + onSkipResultChange: PropTypes.func.isRequired, + onSkipModelUpdateChange: PropTypes.func.isRequired +}; diff --git a/x-pack/plugins/ml/public/components/rule_editor/condition_expression.js b/x-pack/plugins/ml/public/components/rule_editor/condition_expression.js new file mode 100644 index 0000000000000..be1ee6093b1f7 --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/condition_expression.js @@ -0,0 +1,232 @@ +/* + * 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. + */ + + +/* + * React component for rendering a rule condition numerical expression. + */ + +import PropTypes from 'prop-types'; +import React, { + Component, +} from 'react'; + +import { + EuiButtonIcon, + EuiExpression, + EuiExpressionButton, + EuiPopoverTitle, + EuiFlexItem, + EuiFlexGroup, + EuiPopover, + EuiSelect, + EuiFieldNumber, +} from '@elastic/eui'; + +import { APPLIES_TO, OPERATOR } from '../../../common/constants/detector_rule'; +import { appliesToText, operatorToText } from './utils'; + +// Raise the popovers above GuidePageSideNav +const POPOVER_STYLE = { zIndex: '200' }; + + +export class ConditionExpression extends Component { + constructor(props) { + super(props); + + this.state = { + isAppliesToOpen: false, + isOperatorValueOpen: false + }; + } + + openAppliesTo = () => { + this.setState({ + isAppliesToOpen: true, + isOperatorValueOpen: false + }); + }; + + closeAppliesTo = () => { + this.setState({ + isAppliesToOpen: false + }); + }; + + openOperatorValue = () => { + this.setState({ + isAppliesToOpen: false, + isOperatorValueOpen: true + }); + }; + + closeOperatorValue = () => { + this.setState({ + isOperatorValueOpen: false + }); + }; + + changeAppliesTo = (event) => { + const { + index, + operator, + value, + updateCondition } = this.props; + updateCondition(index, event.target.value, operator, value); + } + + changeOperator = (event) => { + const { + index, + appliesTo, + value, + updateCondition } = this.props; + updateCondition(index, appliesTo, event.target.value, value); + } + + changeValue = (event) => { + const { + index, + appliesTo, + operator, + updateCondition } = this.props; + updateCondition(index, appliesTo, operator, +event.target.value); + } + + renderAppliesToPopover() { + return ( +
+ When + + + +
+ ); + } + + renderOperatorValuePopover() { + return ( +
+ Is + + + + + + + + + + + +
+ ); + } + + render() { + const { + index, + appliesTo, + operator, + value, + deleteCondition + } = this.props; + + return ( + + + + )} + isOpen={this.state.isAppliesToOpen} + closePopover={this.closeAppliesTo} + panelPaddingSize="none" + ownFocus + withTitle + anchorPosition="downLeft" + > + {this.renderAppliesToPopover()} + + + + + + )} + isOpen={this.state.isOperatorValueOpen} + closePopover={this.closeOperatorValue} + panelPaddingSize="none" + ownFocus + withTitle + anchorPosition="downLeft" + > + {this.renderOperatorValuePopover()} + + + + deleteCondition(index)} + iconType="trash" + aria-label="Next" + /> + + + ); + } +} +ConditionExpression.propTypes = { + index: PropTypes.number.isRequired, + appliesTo: PropTypes.oneOf([ + APPLIES_TO.ACTUAL, + APPLIES_TO.TYPICAL, + APPLIES_TO.DIFF_FROM_TYPICAL + ]), + operator: PropTypes.oneOf([ + OPERATOR.LESS_THAN, + OPERATOR.LESS_THAN_OR_EQUAL, + OPERATOR.GREATER_THAN, + OPERATOR.GREATER_THAN_OR_EQUAL + ]), + value: PropTypes.number.isRequired, + updateCondition: PropTypes.func.isRequired, + deleteCondition: PropTypes.func.isRequired +}; diff --git a/x-pack/plugins/ml/public/components/rule_editor/conditions_section.js b/x-pack/plugins/ml/public/components/rule_editor/conditions_section.js new file mode 100644 index 0000000000000..d3972a2e29be7 --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/conditions_section.js @@ -0,0 +1,70 @@ +/* + * 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. + */ + + +/* + * React component for rendering the form fields for editing the conditions section of a rule. + */ + +import PropTypes from 'prop-types'; +import React from 'react'; + +import { + EuiButtonEmpty, + EuiSpacer, +} from '@elastic/eui'; + +import { ConditionExpression } from './condition_expression'; + + +export function ConditionsSection({ + isEnabled, + conditions, + addCondition, + updateCondition, + deleteCondition }) { + + if (isEnabled === false) { + return null; + } + + let expressions = []; + if (conditions !== undefined) { + expressions = conditions.map((condition, index) => { + return ( + + ); + }); + } + + return ( + + {expressions} + + addCondition()} + > + Add new condition + + + ); + +} +ConditionsSection.propTypes = { + isEnabled: PropTypes.bool.isRequired, + conditions: PropTypes.array, + addCondition: PropTypes.func.isRequired, + updateCondition: PropTypes.func.isRequired, + deleteCondition: PropTypes.func.isRequired +}; diff --git a/x-pack/plugins/ml/public/components/rule_editor/index.js b/x-pack/plugins/ml/public/components/rule_editor/index.js new file mode 100644 index 0000000000000..6d4c6188c519a --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/index.js @@ -0,0 +1,8 @@ +/* + * 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. + */ + + +export { RuleEditorFlyout } from './rule_editor_flyout'; diff --git a/x-pack/plugins/ml/public/components/rule_editor/rule_editor_flyout.js b/x-pack/plugins/ml/public/components/rule_editor/rule_editor_flyout.js new file mode 100644 index 0000000000000..1a719c6c93756 --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/rule_editor_flyout.js @@ -0,0 +1,536 @@ +/* + * 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. + */ + +/* + * Flyout component for viewing and editing job detector rules. + */ + +import PropTypes from 'prop-types'; +import React, { + Component, +} from 'react'; + +import { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiCheckbox, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +import { toastNotifications } from 'ui/notify'; + +import { ActionsSection } from './actions_section'; +import { ConditionsSection } from './conditions_section'; +import { ScopeSection } from './scope_section'; +import { SelectRuleAction } from './select_rule_action'; +import { + getNewRuleDefaults, + getNewConditionDefaults, + isValidRule, + saveJobRule, + deleteJobRule +} from './utils'; + +import { ACTION } from '../../../common/constants/detector_rule'; +import { getPartitioningFieldNames } from 'plugins/ml/../common/util/job_utils'; +import { mlJobService } from 'plugins/ml/services/job_service'; +import { ml } from 'plugins/ml/services/ml_api_service'; + +import './styles/main.less'; + +export class RuleEditorFlyout extends Component { + constructor(props) { + super(props); + + this.state = { + anomaly: {}, + job: {}, + ruleIndex: -1, + rule: getNewRuleDefaults(), + skipModelUpdate: false, + isConditionsEnabled: false, + isScopeEnabled: false, + filterListIds: [], + isFlyoutVisible: false + }; + + this.partitioningFieldNames = []; + } + + componentDidMount() { + if (typeof this.props.setShowFunction === 'function') { + this.props.setShowFunction(this.showFlyout); + } + } + + componentWillUnmount() { + if (typeof this.props.unsetShowFunction === 'function') { + this.props.unsetShowFunction(); + } + } + + showFlyout = (anomaly) => { + let ruleIndex = -1; + const job = mlJobService.getJob(anomaly.jobId); + if (job === undefined) { + // No details found for this job, display an error and + // don't open the Flyout as no edits can be made without the job. + toastNotifications.addDanger( + `Unable to configure rules as an error occurred obtaining details for job ID ${anomaly.jobId}`); + this.setState({ + job, + isFlyoutVisible: false + }); + + return; + } + + this.partitioningFieldNames = getPartitioningFieldNames(job, anomaly.detectorIndex); + + // Check if any rules are configured for this detector. + const detectorIndex = anomaly.detectorIndex; + const detector = job.analysis_config.detectors[detectorIndex]; + if (detector.custom_rules === undefined) { + ruleIndex = 0; + } + + let isConditionsEnabled = false; + if (ruleIndex === 0) { + // Configuring the first rule for a detector. + isConditionsEnabled = (this.partitioningFieldNames.length === 0); + } + + this.setState({ + anomaly, + job, + ruleIndex, + isConditionsEnabled, + isScopeEnabled: false, + isFlyoutVisible: true + }); + + if (this.partitioningFieldNames.length > 0) { + // Load the current list of filters. + ml.filters.filters() + .then((filters) => { + const filterListIds = filters.map(filter => filter.filter_id); + this.setState({ + filterListIds + }); + }) + .catch((resp) => { + console.log('Error loading list of filters:', resp); + toastNotifications.addDanger('Error loading the filter lists used in the rule scope'); + }); + } + } + + closeFlyout = () => { + this.setState({ isFlyoutVisible: false }); + } + + setEditRuleIndex = (ruleIndex) => { + const detectorIndex = this.state.anomaly.detectorIndex; + const detector = this.state.job.analysis_config.detectors[detectorIndex]; + const rules = detector.custom_rules; + const rule = (rules === undefined || ruleIndex >= rules.length) ? + getNewRuleDefaults() : rules[ruleIndex]; + + const isConditionsEnabled = (this.partitioningFieldNames.length === 0) || + (rule.conditions !== undefined && rule.conditions.length > 0); + const isScopeEnabled = (rule.scope !== undefined) && (Object.keys(rule.scope).length > 0); + + this.setState({ + ruleIndex, + rule, + isConditionsEnabled, + isScopeEnabled + }); + } + + onSkipResultChange = (e) => { + const checked = e.target.checked; + this.setState((prevState) => { + const actions = [...prevState.rule.actions]; + const idx = actions.indexOf(ACTION.SKIP_RESULT); + if ((idx === -1) && checked) { + actions.push(ACTION.SKIP_RESULT); + } else if ((idx > -1) && !checked) { + actions.splice(idx, 1); + } + + return { + rule: { ...prevState.rule, actions } + }; + }); + } + + onSkipModelUpdateChange = (e) => { + const checked = e.target.checked; + this.setState((prevState) => { + const actions = [...prevState.rule.actions]; + const idx = actions.indexOf(ACTION.SKIP_MODEL_UPDATE); + if ((idx === -1) && checked) { + actions.push(ACTION.SKIP_MODEL_UPDATE); + } else if ((idx > -1) && !checked) { + actions.splice(idx, 1); + } + + return { + rule: { ...prevState.rule, actions } + }; + }); + } + + onConditionsEnabledChange = (e) => { + const isConditionsEnabled = e.target.checked; + this.setState((prevState) => { + let conditions; + if (isConditionsEnabled === false) { + // Clear any conditions that have been added. + conditions = []; + } else { + // Add a default new condition. + conditions = [getNewConditionDefaults()]; + } + + return { + rule: { ...prevState.rule, conditions }, + isConditionsEnabled + }; + }); + } + + addCondition = () => { + this.setState((prevState) => { + const conditions = [...prevState.rule.conditions]; + conditions.push(getNewConditionDefaults()); + + return { + rule: { ...prevState.rule, conditions } + }; + }); + } + + updateCondition = (index, appliesTo, operator, value) => { + this.setState((prevState) => { + const conditions = [...prevState.rule.conditions]; + if (index < conditions.length) { + conditions[index] = { + applies_to: appliesTo, + operator, + value + }; + } + + return { + rule: { ...prevState.rule, conditions } + }; + }); + } + + deleteCondition = (index) => { + this.setState((prevState) => { + const conditions = [...prevState.rule.conditions]; + if (index < conditions.length) { + conditions.splice(index, 1); + } + + return { + rule: { ...prevState.rule, conditions } + }; + }); + } + + onScopeEnabledChange = (e) => { + const isScopeEnabled = e.target.checked; + this.setState((prevState) => { + const rule = { ...prevState.rule }; + if (isScopeEnabled === false) { + // Clear scope property. + delete rule.scope; + } + + return { + rule, + isScopeEnabled + }; + }); + } + + updateScope = (fieldName, filterId, filterType, enabled) => { + this.setState((prevState) => { + let scope = { ...prevState.rule.scope }; + if (enabled === true) { + if (scope === undefined) { + scope = {}; + } + + scope[fieldName] = { + filter_id: filterId, + filter_type: filterType + }; + } else { + if (scope !== undefined) { + delete scope[fieldName]; + } + } + + return { + rule: { ...prevState.rule, scope } + }; + }); + } + + saveEdit = () => { + const { + job, + anomaly, + rule, + ruleIndex + } = this.state; + + const jobId = job.job_id; + const detectorIndex = anomaly.detectorIndex; + + saveJobRule(job, detectorIndex, ruleIndex, rule) + .then((resp) => { + if (resp.success) { + toastNotifications.addSuccess(`Changes to ${jobId} detector rules saved`); + this.closeFlyout(); + } else { + toastNotifications.addDanger(`Error saving changes to ${jobId} detector rules`); + } + }) + .catch((error) => { + console.error(error); + toastNotifications.addDanger(`Error saving changes to ${jobId} detector rules`); + }); + } + + deleteRuleAtIndex = (index) => { + const { + job, + anomaly + } = this.state; + const jobId = job.job_id; + const detectorIndex = anomaly.detectorIndex; + + deleteJobRule(job, detectorIndex, index) + .then((resp) => { + if (resp.success) { + toastNotifications.addSuccess(`Rule deleted from ${jobId} detector`); + this.closeFlyout(); + } else { + toastNotifications.addDanger(`Error deleting rule from ${jobId} detector`); + } + }) + .catch((error) => { + console.error(error); + let errorMessage = `Error deleting rule from ${jobId} detector`; + if (error.message) { + errorMessage += ` : ${error.message}`; + } + toastNotifications.addDanger(errorMessage); + }); + } + + render() { + const { + isFlyoutVisible, + job, + anomaly, + ruleIndex, + rule, + filterListIds, + isConditionsEnabled, + isScopeEnabled } = this.state; + + if (isFlyoutVisible === false) { + return null; + } + + let flyout; + + const hasPartitioningFields = (this.partitioningFieldNames && this.partitioningFieldNames.length > 0); + + if (ruleIndex === -1) { + flyout = ( + + + +

+ Edit Rules +

+
+
+ + + + + + + + + + Close + + + + +
+ ); + } else { + const conditionsText = 'Add numeric conditions to take action according ' + + 'to the actual or typical values of the anomaly. Multiple conditions are ' + + 'combined using AND.'; + flyout = ( + + + +

+ Create Rule +

+
+
+ + + +

+ Rules allow you to provide feedback in order to customize the analytics, + skipping results for anomalies which though mathematically significant + are not action worthy. +

+
+ + + + +

Action

+
+ + + + + +

Conditions

+
+ + + + + + + + + + +

+ Changes to rules take effect for new results only. +

+

+ To apply these changes to existing results you must clone and rerun the job. + Note rerunning the job may take some time and should only be done once + you have completed all your changes to the rules for this job. +

+
+ +
+ + + + + + Close + + + + + Save + + + + +
+ ); + + } + + return ( + + {flyout} + + ); + + } +} +RuleEditorFlyout.propTypes = { + setShowFunction: PropTypes.func.isRequired, + unsetShowFunction: PropTypes.func.isRequired, +}; diff --git a/x-pack/plugins/ml/public/components/rule_editor/scope_expression.js b/x-pack/plugins/ml/public/components/rule_editor/scope_expression.js new file mode 100644 index 0000000000000..28c97c11180c2 --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/scope_expression.js @@ -0,0 +1,191 @@ +/* + * 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. + */ + + +/* + * React component for rendering a rule scope expression. + */ + +import PropTypes from 'prop-types'; +import React, { + Component, +} from 'react'; + +import { + EuiCheckbox, + EuiExpression, + EuiExpressionButton, + EuiPopoverTitle, + EuiFlexItem, + EuiFlexGroup, + EuiPopover, + EuiSelect, +} from '@elastic/eui'; + +import { FILTER_TYPE } from '../../../common/constants/detector_rule'; +import { filterTypeToText } from './utils'; + +// Raise the popovers above GuidePageSideNav +const POPOVER_STYLE = { zIndex: '200' }; + +function getFilterListOptions(filterListIds) { + return filterListIds.map(filterId => ({ value: filterId, text: filterId })); +} + +export class ScopeExpression extends Component { + constructor(props) { + super(props); + + this.state = { + isFilterListOpen: false + }; + } + + openFilterList = () => { + this.setState({ + isFilterListOpen: true + }); + } + + closeFilterList = () => { + this.setState({ + isFilterListOpen: false + }); + } + + onChangeFilterType = (event) => { + const { + fieldName, + filterId, + enabled, + updateScope } = this.props; + + updateScope(fieldName, filterId, event.target.value, enabled); + } + + onChangeFilterId = (event) => { + const { + fieldName, + filterType, + enabled, + updateScope } = this.props; + + updateScope(fieldName, event.target.value, filterType, enabled); + } + + onEnableChange = (event) => { + const { + fieldName, + filterId, + filterType, + updateScope } = this.props; + + updateScope(fieldName, filterId, filterType, event.target.checked); + } + + renderFilterListPopover() { + const { + filterId, + filterType, + filterListIds + } = this.props; + + return ( +
+ Is + + + + + + + + + + + +
+ ); + } + + render() { + const { + fieldName, + filterId, + filterType, + enabled, + filterListIds + } = this.props; + + return ( + + + + + + event.preventDefault()} + /> + + + {filterListIds !== undefined && filterListIds.length > 0 && + + + )} + isOpen={this.state.isFilterListOpen} + closePopover={this.closeFilterList} + panelPaddingSize="none" + ownFocus + withTitle + anchorPosition="downLeft" + > + {this.renderFilterListPopover()} + + + } + + ); + } +} +ScopeExpression.propTypes = { + fieldName: PropTypes.string.isRequired, + filterId: PropTypes.string, + filterType: PropTypes.oneOf([ + FILTER_TYPE.INCLUDE, + FILTER_TYPE.EXCLUDE + ]), + enabled: PropTypes.bool.isRequired, + filterListIds: PropTypes.array.isRequired, + updateScope: PropTypes.func.isRequired +}; diff --git a/x-pack/plugins/ml/public/components/rule_editor/scope_section.js b/x-pack/plugins/ml/public/components/rule_editor/scope_section.js new file mode 100644 index 0000000000000..d7c52fab492be --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/scope_section.js @@ -0,0 +1,123 @@ +/* + * 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. + */ + + +/* + * React component for rendering the form fields for editing the scope section of a rule. + */ + +import PropTypes from 'prop-types'; +import React from 'react'; + +import { + EuiCallOut, + EuiCheckbox, + EuiLink, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; + +import { ScopeExpression } from './scope_expression'; +import { getScopeFieldDefaults } from './utils'; + + +function getScopeText(partitioningFieldNames) { + if (partitioningFieldNames.length === 1) { + return `Specify whether the rule should only apply if the ${partitioningFieldNames[0]} is ` + + `in a chosen list of values.`; + } else { + return `Specify whether the rule should only apply if the ${partitioningFieldNames.join(' or ')} are ` + + `in a chosen list of values.`; + } +} + +function NoFilterListsCallOut() { + return ( + +

+ To configure scope, you must first use the  + Filter Lists settings page + to create the list of values you want to include or exclude in the rule. +

+
+ ); +} + + +export function ScopeSection({ + isEnabled, + onEnabledChange, + partitioningFieldNames, + filterListIds, + scope, + updateScope }) { + + if (partitioningFieldNames === null || partitioningFieldNames.length === 0) { + return null; + } + + let content; + if (filterListIds.length > 0) { + content = partitioningFieldNames.map((fieldName, index) => { + let filterValues; + let enabled = false; + if (scope !== undefined && scope[fieldName] !== undefined) { + filterValues = scope[fieldName]; + enabled = true; + } else { + filterValues = getScopeFieldDefaults(filterListIds); + } + + return ( + + ); + }); + } else { + content = ; + } + + return ( + + +

Scope

+
+ + + + {isEnabled && + + {content} + + } + +
+ ); + +} +ScopeSection.propTypes = { + isEnabled: PropTypes.bool.isRequired, + onEnabledChange: PropTypes.func.isRequired, + partitioningFieldNames: PropTypes.array.isRequired, + filterListIds: PropTypes.array.isRequired, + scope: PropTypes.object, + updateScope: PropTypes.func.isRequired +}; diff --git a/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/delete_rule_modal.js b/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/delete_rule_modal.js new file mode 100644 index 0000000000000..4892b9d474c7a --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/delete_rule_modal.js @@ -0,0 +1,83 @@ +/* + * 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. + */ + + +/* + * React component for rendering a modal to confirm deletion of a rule. + */ + +import PropTypes from 'prop-types'; +import React, { + Component, +} from 'react'; + +import { + EuiConfirmModal, + EuiLink, + EuiOverlayMask, + EUI_MODAL_CONFIRM_BUTTON, +} from '@elastic/eui'; + +export class DeleteRuleModal extends Component { + constructor(props) { + super(props); + + this.state = { + isModalVisible: false, + }; + } + + deleteRule = () => { + const { ruleIndex, deleteRuleAtIndex } = this.props; + deleteRuleAtIndex(ruleIndex); + this.closeModal(); + } + + closeModal = () => { + this.setState({ isModalVisible: false }); + } + + showModal = () => { + this.setState({ isModalVisible: true }); + } + + render() { + let modal; + + if (this.state.isModalVisible) { + modal = ( + + +

Are you sure you want to delete this rule?

+
+
+ ); + } + + return ( + + this.showModal()} + > + Delete rule + + {modal} + + ); + } +} +DeleteRuleModal.propTypes = { + ruleIndex: PropTypes.number.isRequired, + deleteRuleAtIndex: PropTypes.func.isRequired +}; diff --git a/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/index.js b/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/index.js new file mode 100644 index 0000000000000..60aae9ac50c92 --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/index.js @@ -0,0 +1,8 @@ +/* + * 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. + */ + + +export { SelectRuleAction } from './select_rule_action'; diff --git a/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/rule_action_panel.js b/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/rule_action_panel.js new file mode 100644 index 0000000000000..8116f207d417d --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/rule_action_panel.js @@ -0,0 +1,88 @@ +/* + * 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. + */ + + +/* + * Panel with a description of a rule and a list of actions that can be performed on the rule. + */ + +import PropTypes from 'prop-types'; +import React from 'react'; + +import { + EuiDescriptionList, + EuiLink, + EuiPanel, +} from '@elastic/eui'; + +import { DeleteRuleModal } from './delete_rule_modal'; +import { buildRuleDescription } from '../utils'; + +function getEditRuleLink(ruleIndex, setEditRuleIndex) { + return ( + setEditRuleIndex(ruleIndex)} + > + Edit rule + + ); +} + +function getDeleteRuleLink(ruleIndex, deleteRuleAtIndex) { + return ( + + ); +} + +export function RuleActionPanel({ + job, + detectorIndex, + ruleIndex, + setEditRuleIndex, + deleteRuleAtIndex, +}) { + const detector = job.analysis_config.detectors[detectorIndex]; + const rules = detector.custom_rules; + if (rules === undefined || ruleIndex >= rules.length) { + return null; + } + + const rule = rules[ruleIndex]; + + const descriptionListItems = [ + { + title: 'rule', + description: buildRuleDescription(rule), + }, + { + title: 'actions', + description: getEditRuleLink(ruleIndex, setEditRuleIndex), + }, + { + title: '', + description: getDeleteRuleLink(ruleIndex, deleteRuleAtIndex) + } + ]; + + return ( + + + + ); +} +RuleActionPanel.propTypes = { + detectorIndex: PropTypes.number.isRequired, + ruleIndex: PropTypes.number.isRequired, + setEditRuleIndex: PropTypes.func.isRequired, + deleteRuleAtIndex: PropTypes.func.isRequired, +}; + diff --git a/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/select_rule_action.js b/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/select_rule_action.js new file mode 100644 index 0000000000000..303887b5e5925 --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/select_rule_action.js @@ -0,0 +1,96 @@ +/* + * 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. + */ + + +/* + * React component for selecting the rule to edit, create or delete. + */ + +import PropTypes from 'prop-types'; +import React from 'react'; + +import { + EuiDescriptionList, + EuiLink, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +import { RuleActionPanel } from './rule_action_panel'; + + +export function SelectRuleAction({ + job, + anomaly, + detectorIndex, + setEditRuleIndex, + deleteRuleAtIndex }) { + + const detector = job.analysis_config.detectors[detectorIndex]; + const descriptionListItems = [ + { + title: 'job ID', + description: job.job_id, + }, + { + title: 'detector', + description: detector.detector_description, + } + ]; + + const rules = detector.custom_rules || []; + let ruleActionPanels; + if (rules.length > 0) { + ruleActionPanels = rules.map((rule, index) => { + return ( + + + + + ); + }); + } + + return ( + + {rules.length > 0 && + + + + {ruleActionPanels} + + + or  + + + } + setEditRuleIndex(rules.length)} + > + create a new rule + + + ); + +} +SelectRuleAction.propTypes = { + job: PropTypes.object.isRequired, + anomaly: PropTypes.object.isRequired, + detectorIndex: PropTypes.number.isRequired, + setEditRuleIndex: PropTypes.func.isRequired, + deleteRuleAtIndex: PropTypes.func.isRequired, +}; diff --git a/x-pack/plugins/ml/public/components/rule_editor/styles/main.less b/x-pack/plugins/ml/public/components/rule_editor/styles/main.less new file mode 100644 index 0000000000000..1cebe4004d6ec --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/styles/main.less @@ -0,0 +1,80 @@ +.ml-rule-editor-flyout { + font-size: 14px; + + .select-rule-description-list { + padding-left: 16px; + + .euiDescriptionList__title { + flex-basis: 15%; + } + + .euiDescriptionList__description { + flex-basis: 85%; + } + } + + .euiDescriptionList.select-rule-description-list.euiDescriptionList--column > * { + margin-top: 5px; + } + + .select-rule-action-panel { + padding-top:10px; + + .euiDescriptionList { + .euiDescriptionList__title { + flex-basis: 15%; + } + + .euiDescriptionList__description { + flex-basis: 85%; + } + + .euiDescriptionList__description:nth-child(2) { + color: #1a1a1a; + font-weight: 600; + } + } + + .euiDescriptionList.euiDescriptionList--column > * { + margin-top: 5px; + } + } + + .scope-enable-checkbox { + .euiCheckbox__input[disabled] ~ .euiCheckbox__label { + color: inherit; + } + } + + .scope-field-checkbox { + margin-right: 2px; + + .euiCheckbox { + margin-top: 6px; + } + } + + .scope-field-button { + pointer-events: none; + border-bottom: none; + } + + .scope-edit-filter-link { + line-height: 32px; + font-size: 12px; + } + + .euiExpressionButton.disabled { + pointer-events: none; + + .euiExpressionButton__value, + .euiExpressionButton__description { + color: #c5c5c5; + } + } + + .text-highlight { + font-weight: bold; + } + +} diff --git a/x-pack/plugins/ml/public/components/rule_editor/utils.js b/x-pack/plugins/ml/public/components/rule_editor/utils.js new file mode 100644 index 0000000000000..14a95caeed6d3 --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/utils.js @@ -0,0 +1,235 @@ +/* + * 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 { + ACTION, + FILTER_TYPE, + APPLIES_TO, + OPERATOR +} from '../../../common/constants/detector_rule'; + +import { cloneDeep } from 'lodash'; +import { mlJobService } from 'plugins/ml/services/job_service'; + +export function getNewConditionDefaults() { + return { + applies_to: APPLIES_TO.ACTUAL, + operator: OPERATOR.LESS_THAN, + value: 1 + }; +} + +export function getNewRuleDefaults() { + return { + actions: [ACTION.SKIP_RESULT], + conditions: [] + }; +} + +export function getScopeFieldDefaults(filterListIds) { + const defaults = { + filter_type: FILTER_TYPE.INCLUDE, + }; + + if (filterListIds !== undefined && filterListIds.length > 0) { + defaults.filter_id = filterListIds[0]; + } + + return defaults; +} + +export function isValidRule(rule) { + // Runs simple checks to make sure the minimum set of + // properties have values in the edited rule. + let isValid = false; + + // Check an action has been supplied. + const actions = rule.actions; + if (actions.length > 0) { + // Check either a condition or a scope property has been set. + const conditions = rule.conditions; + if (conditions !== undefined && conditions.length > 0) { + isValid = true; + } else { + const scope = rule.scope; + if (scope !== undefined && Object.keys(scope).length > 0) { + isValid = true; + } + } + } + + return isValid; +} + +export function saveJobRule(job, detectorIndex, ruleIndex, editedRule) { + const detector = job.analysis_config.detectors[detectorIndex]; + + let rules = []; + if (detector.custom_rules === undefined) { + rules = [editedRule]; + } else { + rules = cloneDeep(detector.custom_rules); + if (ruleIndex < rules.length) { + // Edit to an existing rule. + rules[ruleIndex] = editedRule; + } else { + // Add a new rule. + rules.push(editedRule); + } + } + + return updateJobRules(job, detectorIndex, rules); +} + +export function deleteJobRule(job, detectorIndex, ruleIndex) { + const detector = job.analysis_config.detectors[detectorIndex]; + let customRules = []; + if (detector.custom_rules !== undefined && ruleIndex < detector.custom_rules.length) { + customRules = cloneDeep(detector.custom_rules); + customRules.splice(ruleIndex, 1); + return updateJobRules(job, detectorIndex, customRules); + } else { + return Promise.reject(new Error( + `Rule no longer exists for detector index ${detectorIndex} in job ${job.job_id}`)); + } +} + +export function updateJobRules(job, detectorIndex, rules) { + // Pass just the detector with the edited rule to the updateJob endpoint. + const jobId = job.job_id; + const jobData = { + detectors: [ + { + detector_index: detectorIndex, + custom_rules: rules + } + ] + }; + + // If created_by is set in the job's custom_settings, remove it as the rules + // cannot currently be edited in the job wizards and so would be lost in a clone. + let customSettings = {}; + if (job.custom_settings !== undefined) { + customSettings = { ...job.custom_settings }; + delete customSettings.created_by; + jobData.custom_settings = customSettings; + } + + return new Promise((resolve, reject) => { + mlJobService.updateJob(jobId, jobData) + .then((resp) => { + if (resp.success) { + // Refresh the job data in the job service before resolving. + mlJobService.refreshJob(jobId) + .then(() => { + resolve({ success: true }); + }) + .catch((refreshResp) => { + reject(refreshResp); + }); + } else { + reject(resp); + } + }) + .catch((resp) => { + reject(resp); + }); + }); +} + +export function buildRuleDescription(rule) { + const { actions, conditions, scope } = rule; + let description = 'skip '; + actions.forEach((action, i) => { + if (i > 0) { + description += ' AND '; + } + switch (action) { + case ACTION.SKIP_RESULT: + description += 'result'; + break; + case ACTION.SKIP_MODEL_UPDATE: + description += 'model update'; + break; + } + }); + + description += ' when '; + if (conditions !== undefined) { + conditions.forEach((condition, i) => { + if (i > 0) { + description += ' AND '; + } + + description += `${condition.applies_to} is ${operatorToText(condition.operator)} ${condition.value}`; + }); + } + + if (scope !== undefined) { + if (conditions !== undefined && conditions.length > 0) { + description += ' AND '; + } + const fieldNames = Object.keys(scope); + fieldNames.forEach((fieldName, i) => { + if (i > 0) { + description += ' AND '; + } + + const filter = scope[fieldName]; + description += `${fieldName} is ${filterTypeToText(filter.filter_type)} ${filter.filter_id}`; + }); + } + + return description; +} + +export function filterTypeToText(filterType) { + switch (filterType) { + case FILTER_TYPE.INCLUDE: + return 'in'; + + case FILTER_TYPE.EXCLUDE: + return 'not in'; + + default: + return filterType; + } +} + +export function appliesToText(appliesTo) { + switch (appliesTo) { + case APPLIES_TO.ACTUAL: + return 'actual'; + + case APPLIES_TO.TYPICAL: + return 'typical'; + + case APPLIES_TO.DIFF_FROM_TYPICAL: + return 'diff from typical'; + + default: + return appliesTo; + } +} + +export function operatorToText(operator) { + switch (operator) { + case OPERATOR.LESS_THAN: + return 'less than'; + + case OPERATOR.LESS_THAN_OR_EQUAL: + return 'less than or equal to'; + + case OPERATOR.GREATER_THAN: + return 'greater than'; + + case OPERATOR.GREATER_THAN_OR_EQUAL: + return 'greater than or equal to'; + + default: + return operator; + } +} diff --git a/x-pack/plugins/ml/public/settings/filter_lists/edit/styles/main.less b/x-pack/plugins/ml/public/settings/filter_lists/edit/styles/main.less index fa55f9a4371c1..1fc0c0e190b13 100644 --- a/x-pack/plugins/ml/public/settings/filter_lists/edit/styles/main.less +++ b/x-pack/plugins/ml/public/settings/filter_lists/edit/styles/main.less @@ -1,6 +1,7 @@ .ml-edit-filter-lists { .ml-edit-filter-lists-content { max-width: 1100px; + width: 100%; margin-top: 16px; margin-bottom: 16px; } From b319b5a6769b54efe57c409f2d66bf0188a35486 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Sun, 22 Jul 2018 17:28:59 -0600 Subject: [PATCH 05/12] avoid day long gaps in sample data (#20897) * avoid day long gaps in sample data * avoid using toISOString to avoid an timezone problems * unskip sample test now that problem is fixed * use much better cj algorithm for translating time * cjcenizal review updates * update funtion name in install.js * push source reference date back a week --- .../sample_data/data_sets/flights/index.js | 2 +- src/server/sample_data/routes/install.js | 13 ++- .../routes/lib/adjust_timestamp.js | 48 -------- .../routes/lib/adjust_timestamp.test.js | 73 ------------ .../routes/lib/translate_timestamp.js | 75 ++++++++++++ .../routes/lib/translate_timestamp.test.js | 107 ++++++++++++++++++ test/functional/apps/home/_sample_data.js | 3 +- 7 files changed, 193 insertions(+), 128 deletions(-) delete mode 100644 src/server/sample_data/routes/lib/adjust_timestamp.js delete mode 100644 src/server/sample_data/routes/lib/adjust_timestamp.test.js create mode 100644 src/server/sample_data/routes/lib/translate_timestamp.js create mode 100644 src/server/sample_data/routes/lib/translate_timestamp.test.js diff --git a/src/server/sample_data/data_sets/flights/index.js b/src/server/sample_data/data_sets/flights/index.js index 2439e7b4a6507..dd2e477ccc98f 100644 --- a/src/server/sample_data/data_sets/flights/index.js +++ b/src/server/sample_data/data_sets/flights/index.js @@ -113,7 +113,7 @@ export function flightsSpecProvider() { } }, timeFields: ['timestamp'], - currentTimeMarker: '2018-01-02T00:00:00', + currentTimeMarker: '2018-01-09T00:00:00', preserveDayOfWeekTimeOfDay: true, savedObjects: savedObjects, }; diff --git a/src/server/sample_data/routes/install.js b/src/server/sample_data/routes/install.js index 8e096992a1982..0c00bcc739063 100644 --- a/src/server/sample_data/routes/install.js +++ b/src/server/sample_data/routes/install.js @@ -21,7 +21,11 @@ import Joi from 'joi'; import { loadData } from './lib/load_data'; import { createIndexName } from './lib/create_index_name'; -import { adjustTimestamp } from './lib/adjust_timestamp'; +import { + dateToIso8601IgnoringTime, + translateTimeRelativeToDifference, + translateTimeRelativeToWeek +} from './lib/translate_timestamp'; export const createInstallRoute = () => ({ path: '/api/sample_data/{id}', @@ -80,12 +84,13 @@ export const createInstallRoute = () => ({ return reply(errMsg).code(err.status); } - const now = new Date(); - const currentTimeMarker = new Date(Date.parse(sampleDataset.currentTimeMarker)); + const nowReference = dateToIso8601IgnoringTime(new Date()); function updateTimestamps(doc) { sampleDataset.timeFields.forEach(timeFieldName => { if (doc[timeFieldName]) { - doc[timeFieldName] = adjustTimestamp(doc[timeFieldName], currentTimeMarker, now, sampleDataset.preserveDayOfWeekTimeOfDay); + doc[timeFieldName] = sampleDataset.preserveDayOfWeekTimeOfDay + ? translateTimeRelativeToWeek(doc[timeFieldName], sampleDataset.currentTimeMarker, nowReference) + : translateTimeRelativeToDifference(doc[timeFieldName], sampleDataset.currentTimeMarker, nowReference); } }); return doc; diff --git a/src/server/sample_data/routes/lib/adjust_timestamp.js b/src/server/sample_data/routes/lib/adjust_timestamp.js deleted file mode 100644 index 749b181cbe544..0000000000000 --- a/src/server/sample_data/routes/lib/adjust_timestamp.js +++ /dev/null @@ -1,48 +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. - */ - - -const MILLISECONDS_IN_DAY = 86400000; - -/** - * Convert timestamp to timestamp that is relative to now - * - * @param {String} timestamp ISO8601 formated data string YYYY-MM-dd'T'HH:mm:ss.SSS - * @param {Date} currentTimeMarker "now" reference marker in sample dataset - * @param {Date} now - * @param {Boolean} preserveDayOfWeekTimeOfDay - * @return {String} ISO8601 formated data string YYYY-MM-dd'T'HH:mm:ss.SSS of timestamp adjusted to now - */ -export function adjustTimestamp(timestamp, currentTimeMarker, now, preserveDayOfWeekTimeOfDay) { - const timestampDate = new Date(Date.parse(timestamp)); - - if (!preserveDayOfWeekTimeOfDay) { - // Move timestamp relative to now, preserving distance between currentTimeMarker and timestamp - const timeDelta = timestampDate.getTime() - currentTimeMarker.getTime(); - return (new Date(now.getTime() + timeDelta)).toISOString(); - } - - // Move timestamp to current week, preserving day of week and time of day - const weekDelta = Math.round((timestampDate.getTime() - currentTimeMarker.getTime()) / (MILLISECONDS_IN_DAY * 7)); - const dayOfWeekDelta = timestampDate.getDay() - now.getDay(); - const daysDelta = dayOfWeekDelta * MILLISECONDS_IN_DAY + (weekDelta * MILLISECONDS_IN_DAY * 7); - const yearMonthDay = (new Date(now.getTime() + daysDelta)).toISOString().substring(0, 10); - return `${yearMonthDay}T${timestamp.substring(11)}`; - -} diff --git a/src/server/sample_data/routes/lib/adjust_timestamp.test.js b/src/server/sample_data/routes/lib/adjust_timestamp.test.js deleted file mode 100644 index ee74537058ece..0000000000000 --- a/src/server/sample_data/routes/lib/adjust_timestamp.test.js +++ /dev/null @@ -1,73 +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 { adjustTimestamp } from './adjust_timestamp'; - -const currentTimeMarker = new Date(Date.parse('2018-01-02T00:00:00Z')); -const now = new Date(Date.parse('2018-04-25T18:24:58.650Z')); // Wednesday - -describe('relative to now', () => { - test('adjusts time to 10 minutes in past from now', () => { - const originalTimestamp = '2018-01-01T23:50:00Z'; // -10 minutes relative to currentTimeMarker - const timestamp = adjustTimestamp(originalTimestamp, currentTimeMarker, now, false); - expect(timestamp).toBe('2018-04-25T18:14:58.650Z'); - }); - - test('adjusts time to 1 hour in future from now', () => { - const originalTimestamp = '2018-01-02T01:00:00Z'; // + 1 hour relative to currentTimeMarker - const timestamp = adjustTimestamp(originalTimestamp, currentTimeMarker, now, false); - expect(timestamp).toBe('2018-04-25T19:24:58.650Z'); - }); -}); - -describe('preserve day of week and time of day', () => { - test('adjusts time to monday of the same week as now', () => { - const originalTimestamp = '2018-01-01T23:50:00Z'; // Monday, same week relative to currentTimeMarker - const timestamp = adjustTimestamp(originalTimestamp, currentTimeMarker, now, true); - expect(timestamp).toBe('2018-04-23T23:50:00Z'); - }); - - test('adjusts time to friday of the same week as now', () => { - const originalTimestamp = '2017-12-29T23:50:00Z'; // Friday, same week relative to currentTimeMarker - const timestamp = adjustTimestamp(originalTimestamp, currentTimeMarker, now, true); - expect(timestamp).toBe('2018-04-27T23:50:00Z'); - }); - - test('adjusts time to monday of the previous week as now', () => { - const originalTimestamp = '2017-12-25T23:50:00Z'; // Monday, previous week relative to currentTimeMarker - const timestamp = adjustTimestamp(originalTimestamp, currentTimeMarker, now, true); - expect(timestamp).toBe('2018-04-16T23:50:00Z'); - }); - - test('adjusts time to friday of the week after now', () => { - const originalTimestamp = '2018-01-05T23:50:00Z'; // Friday, next week relative to currentTimeMarker - const timestamp = adjustTimestamp(originalTimestamp, currentTimeMarker, now, true); - expect(timestamp).toBe('2018-05-04T23:50:00Z'); - }); - - test('adjusts timestamp to correct day of week even when UTC day is on different day.', () => { - const currentTimeMarker = new Date(Date.parse('2018-01-02T00:00:00')); // Tuesday - const now = new Date(Date.parse('2018-06-14T10:38')); // Thurs - const originalTimestamp = '2018-01-01T17:57:25'; // Monday - const timestamp = adjustTimestamp(originalTimestamp, currentTimeMarker, now, true); - expect(timestamp).toBe('2018-06-11T17:57:25'); // Monday - }); -}); - diff --git a/src/server/sample_data/routes/lib/translate_timestamp.js b/src/server/sample_data/routes/lib/translate_timestamp.js new file mode 100644 index 0000000000000..f8d0f86e49709 --- /dev/null +++ b/src/server/sample_data/routes/lib/translate_timestamp.js @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const MILLISECONDS_IN_DAY = 86400000; + +function iso8601ToDateIgnoringTime(iso8601) { + const split = iso8601.split('-'); + if (split.length < 3) { + throw new Error('Unexpected timestamp format, expecting YYYY-MM-DDTHH:mm:ss'); + } + const year = parseInt(split[0]); + const month = parseInt(split[1]) - 1; // javascript months are zero-based indexed + const date = parseInt(split[2]); + return new Date(year, month, date); +} + +export function dateToIso8601IgnoringTime(date) { + // not using "Date.toISOString" because only using Date methods that deal with local time + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const monthString = month < 10 ? `0${month}` : `${month}`; + const dateString = date.getDate() < 10 ? `0${date.getDate()}` : `${date.getDate()}`; + return `${year}-${monthString}-${dateString}`; +} + +// Translate source timestamp by targetReference timestamp, +// perserving the distance between source and sourceReference +export function translateTimeRelativeToDifference(source, sourceReference, targetReference) { + const sourceDate = iso8601ToDateIgnoringTime(source); + const sourceReferenceDate = iso8601ToDateIgnoringTime(sourceReference); + const targetReferenceDate = iso8601ToDateIgnoringTime(targetReference); + + const timeDelta = sourceDate.getTime() - sourceReferenceDate.getTime(); + const translatedDate = (new Date(targetReferenceDate.getTime() + timeDelta)); + + return `${dateToIso8601IgnoringTime(translatedDate)}T${source.substring(11)}`; +} + +// Translate source timestamp by targetReference timestamp, +// perserving the week distance between source and sourceReference and day of week of the source timestamp +export function translateTimeRelativeToWeek(source, sourceReference, targetReference) { + const sourceReferenceDate = iso8601ToDateIgnoringTime(sourceReference); + const targetReferenceDate = iso8601ToDateIgnoringTime(targetReference); + + // If these dates were in the same week, how many days apart would they be? + const dayOfWeekDelta = sourceReferenceDate.getDay() - targetReferenceDate.getDay(); + + // If we pretend that the targetReference is actually the same day of the week as the + // sourceReference, then we can translate the source to the target while preserving their + // days of the week. + const normalizationDelta = dayOfWeekDelta * MILLISECONDS_IN_DAY; + const normalizedTargetReference = + dateToIso8601IgnoringTime(new Date(targetReferenceDate.getTime() + normalizationDelta)); + + return translateTimeRelativeToDifference( + source, + sourceReference, + normalizedTargetReference); +} diff --git a/src/server/sample_data/routes/lib/translate_timestamp.test.js b/src/server/sample_data/routes/lib/translate_timestamp.test.js new file mode 100644 index 0000000000000..348fd2b429167 --- /dev/null +++ b/src/server/sample_data/routes/lib/translate_timestamp.test.js @@ -0,0 +1,107 @@ +/* + * 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 { translateTimeRelativeToWeek } from './translate_timestamp'; + +describe('translateTimeRelativeToWeek', () => { + const sourceReference = '2018-01-02T00:00:00'; //Tuesday + const targetReference = '2018-04-25T18:24:58.650'; // Wednesday + + describe('2 weeks before', () => { + test('should properly adjust timestamp when day is before targetReference day of week', () => { + const source = '2017-12-18T23:50:00'; // Monday, -2 week relative to sourceReference + const timestamp = translateTimeRelativeToWeek(source, sourceReference, targetReference); + expect(timestamp).toBe('2018-04-09T23:50:00'); // Monday 2 week before targetReference week + }); + + test('should properly adjust timestamp when day is same as targetReference day of week', () => { + const source = '2017-12-20T23:50:00'; // Wednesday, -2 week relative to sourceReference + const timestamp = translateTimeRelativeToWeek(source, sourceReference, targetReference); + expect(timestamp).toBe('2018-04-11T23:50:00'); // Wednesday 2 week before targetReference week + }); + + test('should properly adjust timestamp when day is after targetReference day of week', () => { + const source = '2017-12-22T16:16:50'; // Friday, -2 week relative to sourceReference + const timestamp = translateTimeRelativeToWeek(source, sourceReference, targetReference); + expect(timestamp).toBe('2018-04-13T16:16:50'); // Friday 2 week before targetReference week + }); + }); + + describe('week before', () => { + test('should properly adjust timestamp when day is before targetReference day of week', () => { + const source = '2017-12-25T23:50:00'; // Monday, -1 week relative to sourceReference + const timestamp = translateTimeRelativeToWeek(source, sourceReference, targetReference); + expect(timestamp).toBe('2018-04-16T23:50:00'); // Monday 1 week before targetReference week + }); + + test('should properly adjust timestamp when day is same as targetReference day of week', () => { + const source = '2017-12-27T23:50:00'; // Wednesday, -1 week relative to sourceReference + const timestamp = translateTimeRelativeToWeek(source, sourceReference, targetReference); + expect(timestamp).toBe('2018-04-18T23:50:00'); // Wednesday 1 week before targetReference week + }); + + test('should properly adjust timestamp when day is after targetReference day of week', () => { + const source = '2017-12-29T16:16:50'; // Friday, -1 week relative to sourceReference + const timestamp = translateTimeRelativeToWeek(source, sourceReference, targetReference); + expect(timestamp).toBe('2018-04-20T16:16:50'); // Friday 1 week before targetReference week + }); + }); + + describe('same week', () => { + test('should properly adjust timestamp when day is before targetReference day of week', () => { + const source = '2018-01-01T23:50:00'; // Monday, same week relative to sourceReference + const timestamp = translateTimeRelativeToWeek(source, sourceReference, targetReference); + expect(timestamp).toBe('2018-04-23T23:50:00'); // Monday same week as targetReference + }); + + test('should properly adjust timestamp when day is same as targetReference day of week', () => { + const source = '2018-01-03T23:50:00'; // Wednesday, same week relative to sourceReference + const timestamp = translateTimeRelativeToWeek(source, sourceReference, targetReference); + expect(timestamp).toBe('2018-04-25T23:50:00'); // Wednesday same week as targetReference + }); + + test('should properly adjust timestamp when day is after targetReference day of week', () => { + const source = '2018-01-05T16:16:50'; // Friday, same week relative to sourceReference + const timestamp = translateTimeRelativeToWeek(source, sourceReference, targetReference); + expect(timestamp).toBe('2018-04-27T16:16:50'); // Friday same week as targetReference + }); + }); + + describe('week after', () => { + test('should properly adjust timestamp when day is before targetReference day of week', () => { + const source = '2018-01-08T23:50:00'; // Monday, 1 week after relative to sourceReference + const timestamp = translateTimeRelativeToWeek(source, sourceReference, targetReference); + expect(timestamp).toBe('2018-04-30T23:50:00'); // Monday 1 week after targetReference week + }); + + test('should properly adjust timestamp when day is same as targetReference day of week', () => { + const source = '2018-01-10T23:50:00'; // Wednesday, same week relative to sourceReference + const timestamp = translateTimeRelativeToWeek(source, sourceReference, targetReference); + expect(timestamp).toBe('2018-05-02T23:50:00'); // Wednesday 1 week after targetReference week + }); + + test('should properly adjust timestamp when day is after targetReference day of week', () => { + const source = '2018-01-12T16:16:50'; // Friday, same week relative to sourceReference + const timestamp = translateTimeRelativeToWeek(source, sourceReference, targetReference); + expect(timestamp).toBe('2018-05-04T16:16:50'); // Friday 1 week after targetReference week + }); + }); +}); + diff --git a/test/functional/apps/home/_sample_data.js b/test/functional/apps/home/_sample_data.js index c615ff5ea362a..48cc3cf509936 100644 --- a/test/functional/apps/home/_sample_data.js +++ b/test/functional/apps/home/_sample_data.js @@ -50,8 +50,7 @@ export default function ({ getService, getPageObjects }) { expect(isInstalled).to.be(true); }); - // Skipping issue # 20807 - describe.skip('dashboard', () => { + describe('dashboard', () => { after(async () => { await PageObjects.common.navigateToUrl('home', 'tutorial_directory/sampleData'); await PageObjects.header.waitUntilLoadingHasFinished(); From d6453fa3ba4e6358a002a1ffcd10f789c530cd84 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 23 Jul 2018 11:42:36 +0200 Subject: [PATCH 06/12] [ML] Migrates ml-form-label to EUI/React. (#21059) - Migrates the ml-form-label directive to use EUI/React. - Exposes both FormLabel and JsonTooltip as React components from individual files so they can be used in a React context when the wrapping element also has been already ported to React. - Adds jests based tests for the FormLabel and JsonTooltip components. They try where possible to make the same assertions like the mocha based tests. The mocha based tests are kept in the code for now so the code gets still tested in a angular based context and as a reference to have the same mocha/jest based tests side by side as a reference for migration. - The FormLabel component is done in a way so it supports transclusion in both cases when used with React alone (using the children prop) and angularjs (using a ref callback and angular's transclude()). --- .../__snapshots__/form_label.test.js.snap | 28 +++++++++ .../components/form_label/form_label.js | 52 +++++++++------- .../components/form_label/form_label.test.js | 32 ++++++++++ .../form_label/form_label_directive.js | 52 ++++++++++++++++ .../ml/public/components/form_label/index.js | 2 +- .../__snapshots__/json_tooltip.test.js.snap | 54 +++++++++++++++++ .../public/components/json_tooltip/index.js | 2 +- .../components/json_tooltip/json_tooltip.js | 59 +++++-------------- .../json_tooltip/json_tooltip.test.js | 36 +++++++++++ .../json_tooltip/json_tooltip_directive.js | 40 +++++++++++++ .../components/json_tooltip/styles/main.less | 4 +- 11 files changed, 293 insertions(+), 68 deletions(-) create mode 100644 x-pack/plugins/ml/public/components/form_label/__snapshots__/form_label.test.js.snap create mode 100644 x-pack/plugins/ml/public/components/form_label/form_label.test.js create mode 100644 x-pack/plugins/ml/public/components/form_label/form_label_directive.js create mode 100644 x-pack/plugins/ml/public/components/json_tooltip/__snapshots__/json_tooltip.test.js.snap create mode 100644 x-pack/plugins/ml/public/components/json_tooltip/json_tooltip.test.js create mode 100644 x-pack/plugins/ml/public/components/json_tooltip/json_tooltip_directive.js diff --git a/x-pack/plugins/ml/public/components/form_label/__snapshots__/form_label.test.js.snap b/x-pack/plugins/ml/public/components/form_label/__snapshots__/form_label.test.js.snap new file mode 100644 index 0000000000000..057ed54b10a09 --- /dev/null +++ b/x-pack/plugins/ml/public/components/form_label/__snapshots__/form_label.test.js.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FormLabel Basic initialization 1`] = ` + + +`; + +exports[`FormLabel Full initialization 1`] = ` + + + + +`; diff --git a/x-pack/plugins/ml/public/components/form_label/form_label.js b/x-pack/plugins/ml/public/components/form_label/form_label.js index f549259b4e68a..efdbf6b392651 100644 --- a/x-pack/plugins/ml/public/components/form_label/form_label.js +++ b/x-pack/plugins/ml/public/components/form_label/form_label.js @@ -4,34 +4,46 @@ * you may not use this file except in compliance with the Elastic License. */ +import './styles/main.less'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); +import { JsonTooltip } from '../json_tooltip/json_tooltip'; -// directive for creating a form label including a hoverable icon -// to provide additional information in a tooltip. label and tooltip +// Component for creating a form label including a hoverable icon +// to provide additional information in a tooltip. Label and tooltip // text elements get unique ids based on label-id so they can be // referenced by attributes, for example: // -// Label Text +// Label Text // -module.directive('mlFormLabel', function () { - return { - scope: { - labelId: '@', - tooltipAppendToBody: '@' - }, - restrict: 'E', - replace: false, - transclude: true, - template: ` - - - ` - }; -}); +// +// Writing this as a class based component because stateless components +// cannot use ref(). Once angular is completely gone this can be rewritten +// as a function stateless component. +export class FormLabel extends Component { + constructor(props) { + super(props); + this.labelRef = React.createRef(); + } + render() { + // labelClassName is used so we can override the class with 'kuiFormLabel' + // when used in an angular context. Once the component is no longer used from + // within angular, this prop can be removed and the className can be hardcoded. + const { labelId, labelClassName = 'euiFormLabel', children } = this.props; + return ( + + + + + ); + } +} +FormLabel.propTypes = { + labelId: PropTypes.string +}; diff --git a/x-pack/plugins/ml/public/components/form_label/form_label.test.js b/x-pack/plugins/ml/public/components/form_label/form_label.test.js new file mode 100644 index 0000000000000..70b6983209c96 --- /dev/null +++ b/x-pack/plugins/ml/public/components/form_label/form_label.test.js @@ -0,0 +1,32 @@ +/* + * 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 { shallow } from 'enzyme'; +import React from 'react'; + +import { FormLabel } from './form_label'; + +describe('FormLabel', () => { + + test('Basic initialization', () => { + const wrapper = shallow(); + const props = wrapper.props(); + expect(props.labelId).toBeUndefined(); + expect(wrapper.find('label').text()).toBe(''); + expect(wrapper).toMatchSnapshot(); + }); + + test('Full initialization', () => { + const labelId = 'uid'; + const labelText = 'Label Text'; + const wrapper = shallow({labelText}); + + const labelElement = wrapper.find('label'); + expect(labelElement.props().id).toBe(`ml_aria_label_${labelId}`); + expect(labelElement.text()).toBe(labelText); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/ml/public/components/form_label/form_label_directive.js b/x-pack/plugins/ml/public/components/form_label/form_label_directive.js new file mode 100644 index 0000000000000..85e004d4759a7 --- /dev/null +++ b/x-pack/plugins/ml/public/components/form_label/form_label_directive.js @@ -0,0 +1,52 @@ +/* + * 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 ReactDOM from 'react-dom'; + +import angular from 'angular'; +import { uiModules } from 'ui/modules'; +const module = uiModules.get('apps/ml', ['react']); + +import { FormLabel } from './form_label'; + +// directive for creating a form label including a hoverable icon +// to provide additional information in a tooltip. label and tooltip +// text elements get unique ids based on label-id so they can be +// referenced by attributes, for example: +// +// Label Text +// +module.directive('mlFormLabel', function () { + return { + scope: { + labelId: '@' + }, + restrict: 'E', + replace: false, + transclude: true, + link: (scope, element, attrs, ctrl, transclude) => { + const props = { + labelId: scope.labelId, + labelClassName: 'kuiFormLabel', + // transclude the label text/elements from the angular template + // to the labelRef from the react component. + ref: c => angular.element(c.labelRef.current).append(transclude()) + }; + + ReactDOM.render( + React.createElement(FormLabel, props), + element[0] + ); + } + }; +}); diff --git a/x-pack/plugins/ml/public/components/form_label/index.js b/x-pack/plugins/ml/public/components/form_label/index.js index 38f165d16c172..f850e7b7a2fd2 100644 --- a/x-pack/plugins/ml/public/components/form_label/index.js +++ b/x-pack/plugins/ml/public/components/form_label/index.js @@ -6,5 +6,5 @@ -import './form_label'; +import './form_label_directive'; import './styles/main.less'; diff --git a/x-pack/plugins/ml/public/components/json_tooltip/__snapshots__/json_tooltip.test.js.snap b/x-pack/plugins/ml/public/components/json_tooltip/__snapshots__/json_tooltip.test.js.snap new file mode 100644 index 0000000000000..03270b82e9275 --- /dev/null +++ b/x-pack/plugins/ml/public/components/json_tooltip/__snapshots__/json_tooltip.test.js.snap @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`JsonTooltip Initialization with a non-existing tooltip attribute doesn't throw an error 1`] = ` +
+ ); +} + +AgentMarker.propTypes = { + agentMark: PropTypes.object.isRequired, + x: PropTypes.number.isRequired +}; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.js b/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.js index a84e18fb2eb27..32c9db3fe762a 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.js +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.js @@ -5,10 +5,12 @@ */ import React from 'react'; +import PropTypes from 'prop-types'; import _ from 'lodash'; import { Sticky } from 'react-sticky'; import { XYPlot, XAxis } from 'react-vis'; import LastTickValue from './LastTickValue'; +import AgentMarker from './AgentMarker'; import { colors, px } from '../../../../style/variables'; import { getTimeFormatter } from '../../../../utils/formatters'; @@ -16,7 +18,7 @@ import { getTimeFormatter } from '../../../../utils/formatters'; const getXAxisTickValues = (tickValues, xMax) => _.last(tickValues) * 1.05 > xMax ? tickValues.slice(0, -1) : tickValues; -function TimelineAxis({ header, plotValues }) { +function TimelineAxis({ header, plotValues, agentMarks }) { const { margins, tickValues, width, xDomain, xMax, xScale } = plotValues; const tickFormat = getTimeFormatter(xMax); const xAxisTickValues = getXAxisTickValues(tickValues, xMax); @@ -60,6 +62,14 @@ function TimelineAxis({ header, plotValues }) { /> + + {agentMarks.map(agentMark => ( + + ))} ); @@ -68,4 +78,14 @@ function TimelineAxis({ header, plotValues }) { ); } +TimelineAxis.propTypes = { + header: PropTypes.node, + plotValues: PropTypes.object.isRequired, + agentMarks: PropTypes.array +}; + +TimelineAxis.defaultProps = { + agentMarks: [] +}; + export default TimelineAxis; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.js b/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.js index a2f4a017502ff..317f01290f403 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.js +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.js @@ -5,10 +5,11 @@ */ import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; import { XYPlot, VerticalGridLines } from 'react-vis'; import { colors } from '../../../../style/variables'; -export default class VerticalLines extends PureComponent { +class VerticalLines extends PureComponent { render() { const { width, @@ -19,6 +20,10 @@ export default class VerticalLines extends PureComponent { xMax } = this.props.plotValues; + const agentMarkTimes = this.props.agentMarks.map( + ({ timeAxis }) => timeAxis + ); + return (
+ @@ -47,3 +53,14 @@ export default class VerticalLines extends PureComponent { ); } } + +VerticalLines.propTypes = { + plotValues: PropTypes.object.isRequired, + agentMarks: PropTypes.array +}; + +VerticalLines.defaultProps = { + agentMarks: [] +}; + +export default VerticalLines; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/__test__/__snapshots__/Timeline.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/Timeline/__test__/__snapshots__/Timeline.test.js.snap index 1e3efb26f3971..3ba61791679f8 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/__test__/__snapshots__/Timeline.test.js.snap +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/__test__/__snapshots__/Timeline.test.js.snap @@ -1,6 +1,33 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Timeline should render with data 1`] = ` +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-size: 12px; + color: #666666; + cursor: pointer; + opacity: 1; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.c1 { + width: 11px; + height: 11px; + margin-right: 5.5px; + background: #999999; + border-radius: 100%; +} +
@@ -52,7 +79,7 @@ exports[`Timeline should render with data 1`] = ` onMouseLeave={[Function]} onMouseMove={[Function]} onWheel={[Function]} - width={100} + width={1000} > +
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
@@ -520,7 +634,7 @@ exports[`Timeline should render with data 1`] = ` style={ Object { "height": "216px", - "width": "100px", + "width": "1000px", } } > @@ -534,7 +648,7 @@ exports[`Timeline should render with data 1`] = ` onMouseLeave={[Function]} onMouseMove={[Function]} onWheel={[Function]} - width={100} + width={1000} > @@ -571,8 +685,8 @@ exports[`Timeline should render with data 1`] = ` "stroke": "#f5f5f5", } } - x1={0} - x2={0} + x1={175.8817293082474} + x2={175.8817293082474} y1={0} y2={116} /> @@ -583,8 +697,8 @@ exports[`Timeline should render with data 1`] = ` "stroke": "#f5f5f5", } } - x1={0} - x2={0} + x1={263.82259396237106} + x2={263.82259396237106} y1={0} y2={116} /> @@ -595,8 +709,8 @@ exports[`Timeline should render with data 1`] = ` "stroke": "#f5f5f5", } } - x1={0} - x2={0} + x1={351.7634586164948} + x2={351.7634586164948} y1={0} y2={116} /> @@ -607,8 +721,8 @@ exports[`Timeline should render with data 1`] = ` "stroke": "#f5f5f5", } } - x1={0} - x2={0} + x1={439.7043232706185} + x2={439.7043232706185} y1={0} y2={116} /> @@ -619,8 +733,8 @@ exports[`Timeline should render with data 1`] = ` "stroke": "#f5f5f5", } } - x1={0} - x2={0} + x1={527.6451879247421} + x2={527.6451879247421} y1={0} y2={116} /> @@ -631,8 +745,8 @@ exports[`Timeline should render with data 1`] = ` "stroke": "#f5f5f5", } } - x1={0} - x2={0} + x1={615.5860525788659} + x2={615.5860525788659} y1={0} y2={116} /> @@ -643,8 +757,8 @@ exports[`Timeline should render with data 1`] = ` "stroke": "#f5f5f5", } } - x1={0} - x2={0} + x1={703.5269172329896} + x2={703.5269172329896} y1={0} y2={116} /> @@ -655,8 +769,8 @@ exports[`Timeline should render with data 1`] = ` "stroke": "#f5f5f5", } } - x1={0} - x2={0} + x1={791.4677818871132} + x2={791.4677818871132} y1={0} y2={116} /> @@ -667,8 +781,8 @@ exports[`Timeline should render with data 1`] = ` "stroke": "#f5f5f5", } } - x1={0} - x2={0} + x1={879.408646541237} + x2={879.408646541237} y1={0} y2={116} /> @@ -684,8 +798,44 @@ exports[`Timeline should render with data 1`] = ` "stroke": "#999999", } } - x1={0} - x2={0} + x1={439.7043232706185} + x2={439.7043232706185} + y1={0} + y2={116} + /> + + + diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/__test__/props.json b/x-pack/plugins/apm/public/components/shared/charts/Timeline/__test__/props.json index b59a4ac6f515e..986a57c730302 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/__test__/props.json +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/__test__/props.json @@ -1,5 +1,5 @@ { - "width": 100, + "width": 1000, "duration": 204683, "height": 116, "margins": { @@ -8,5 +8,10 @@ "right": 50, "bottom": 0 }, - "animation": null + "animation": null, + "agentMarks": [ + { "timeLabel": 100000, "name": "timeToFirstByte", "timeAxis": 100000 }, + { "timeLabel": 110000, "name": "domInteractive", "timeAxis": 110000 }, + { "timeLabel": 190000, "name": "domComplete", "timeAxis": 190000 } + ] } diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/index.js b/x-pack/plugins/apm/public/components/shared/charts/Timeline/index.js index fd0be03d89d6b..99a88cdd0caf5 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/index.js +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/index.js @@ -23,8 +23,7 @@ class Timeline extends PureComponent { ); render() { - const { width, duration, header } = this.props; - + const { width, duration, header, agentMarks } = this.props; if (duration == null || !width) { return null; } @@ -33,14 +32,19 @@ class Timeline extends PureComponent { return (
- - + +
); } } Timeline.propTypes = { + agentMarks: PropTypes.array, duration: PropTypes.number.isRequired, height: PropTypes.number.isRequired, header: PropTypes.node, diff --git a/x-pack/plugins/apm/public/utils/__test__/formatters.test.js b/x-pack/plugins/apm/public/utils/__test__/formatters.test.js new file mode 100644 index 0000000000000..1bd7a4a4e493a --- /dev/null +++ b/x-pack/plugins/apm/public/utils/__test__/formatters.test.js @@ -0,0 +1,16 @@ +/* + * 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 { asTime } from '../formatters'; + +describe('formatters', () => { + it('asTime', () => { + expect(asTime(1000)).toBe('1 ms'); + expect(asTime(1000 * 1000)).toBe('1,000 ms'); + expect(asTime(1000 * 1000 * 10)).toBe('10,000 ms'); + expect(asTime(1000 * 1000 * 20)).toBe('20.0 s'); + }); +}); diff --git a/x-pack/plugins/apm/public/utils/formatters.js b/x-pack/plugins/apm/public/utils/formatters.js index 6960ba3218318..36b867c4606e4 100644 --- a/x-pack/plugins/apm/public/utils/formatters.js +++ b/x-pack/plugins/apm/public/utils/formatters.js @@ -7,7 +7,7 @@ import { memoize } from 'lodash'; import numeral from '@elastic/numeral'; -const UNIT_CUT_OFF = 10 * 1000000; +const UNIT_CUT_OFF = 10 * 1000000; // 10 seconds in microseconds export function asSeconds(value, withUnit = true) { const formatted = asDecimal(value / 1000000); @@ -34,6 +34,9 @@ export function timeUnit(max) { return max > UNIT_CUT_OFF ? 's' : 'ms'; } +/* + * value: time in microseconds + */ export function asTime(value) { return getTimeFormatter(value)(value); } From e503b87eb2c5fbcb8a9704ed53004c42f333d59b Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 23 Jul 2018 14:37:16 +0200 Subject: [PATCH 09/12] [ML] Job validation no longer reports an error when categorization job is properly setup. (#21075) For categorization jobs, job validation would report that mlcategory isn't an aggregatable field. This fix checks the job configuration and only reports the error if the job config isn't using categorization_field_name and the detector field isn't set to mlcategory. --- .../__tests__/job_validation.js | 34 +++++++++++++++++++ .../job_validation/validate_cardinality.js | 15 +++++--- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/job_validation.js b/x-pack/plugins/ml/server/models/job_validation/__tests__/job_validation.js index 46a47f4de9797..d11a07e85541d 100644 --- a/x-pack/plugins/ml/server/models/job_validation/__tests__/job_validation.js +++ b/x-pack/plugins/ml/server/models/job_validation/__tests__/job_validation.js @@ -291,6 +291,40 @@ describe('ML - validateJob', () => { ); }); + it('categorization job using mlcategory passes aggregatable field check', () => { + const payload = { + job: { + job_id: 'categorization_test', + analysis_config: { + bucket_span: '15m', + detectors: [{ + function: 'count', + by_field_name: 'mlcategory' + }], + categorization_field_name: 'message_text', + influencers: [] + }, + data_description: { time_field: '@timestamp' }, + datafeed_config: { indices: [] } + }, + fields: { testField: {} } + }; + + return validateJob(callWithRequest, payload).then( + (messages) => { + const ids = messages.map(m => m.id); + expect(ids).to.eql([ + 'job_id_valid', + 'detectors_function_not_empty', + 'index_fields_valid', + 'success_cardinality', + 'time_field_invalid', + 'influencer_low_suggestion' + ]); + } + ); + }); + // the following two tests validate the correct template rendering of // urls in messages with {{version}} in them to be replaced with the // specified version. (defaulting to 'current') diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.js b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.js index dc6e4166b0c78..0cee9d888556d 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.js +++ b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.js @@ -65,10 +65,17 @@ const validateFactory = (callWithRequest, job) => { }); } } else { - messages.push({ - id: 'field_not_aggregatable', - fieldName: uniqueFieldName - }); + // when the job is using categorization and the field name is set to 'mlcategory', + // then don't report the field as not being able to be aggregated. + if (!( + typeof job.analysis_config.categorization_field_name !== 'undefined' && + uniqueFieldName === 'mlcategory' + )) { + messages.push({ + id: 'field_not_aggregatable', + fieldName: uniqueFieldName + }); + } } }); } catch (e) { From d171aa8f12214a2ffd2ca368477b792cc6f09f1a Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Mon, 23 Jul 2018 16:21:45 +0200 Subject: [PATCH 10/12] TypeScriptify visualization components (#20940) * Refactor vis components to TypeScript * Fix issue with ResizeChecker * Fix calling onInit for no data * Explicit named export * Add title to vistype * Fix error in test file * Move onInit to no VisualizationNoResults * Make listenOnChange changeable * Add memoize util * Use memoize for no results check * Address issue with uiState * Optimize memoize function --- .../index.js => persisted_state/index.d.ts} | 4 +- .../persisted_state.d.ts} | 17 +- src/ui/public/utils/memoize.test.ts | 50 ++++++ src/ui/public/utils/memoize.ts | 61 +++++++ src/ui/public/vis/index.d.ts | 1 + src/ui/public/vis/update_status.ts | 3 +- src/ui/public/vis/vis.d.ts | 12 +- src/ui/public/vis/vis_types/vis_type.d.ts | 40 +++++ .../visualization_noresults.test.js.snap | 29 ++++ src/ui/public/visualize/components/index.ts | 20 +++ .../visualize/components/visualization.js | 87 ---------- .../components/visualization.test.js | 60 ++++--- .../visualize/components/visualization.tsx | 107 ++++++++++++ .../components/visualization_chart.js | 113 ------------- .../components/visualization_chart.test.js | 2 +- .../components/visualization_chart.tsx | 156 ++++++++++++++++++ .../visualization_noresults.test.js | 5 + .../components/visualization_noresults.tsx | 53 ++++++ 18 files changed, 577 insertions(+), 243 deletions(-) rename src/ui/public/{visualize/components/index.js => persisted_state/index.d.ts} (87%) rename src/ui/public/{visualize/components/visualization_noresults.js => persisted_state/persisted_state.d.ts} (66%) create mode 100644 src/ui/public/utils/memoize.test.ts create mode 100644 src/ui/public/utils/memoize.ts create mode 100644 src/ui/public/vis/vis_types/vis_type.d.ts create mode 100644 src/ui/public/visualize/components/__snapshots__/visualization_noresults.test.js.snap create mode 100644 src/ui/public/visualize/components/index.ts delete mode 100644 src/ui/public/visualize/components/visualization.js create mode 100644 src/ui/public/visualize/components/visualization.tsx delete mode 100644 src/ui/public/visualize/components/visualization_chart.js create mode 100644 src/ui/public/visualize/components/visualization_chart.tsx create mode 100644 src/ui/public/visualize/components/visualization_noresults.tsx diff --git a/src/ui/public/visualize/components/index.js b/src/ui/public/persisted_state/index.d.ts similarity index 87% rename from src/ui/public/visualize/components/index.js rename to src/ui/public/persisted_state/index.d.ts index b11aba5ee8278..ab5a3e7be7d28 100644 --- a/src/ui/public/visualize/components/index.js +++ b/src/ui/public/persisted_state/index.d.ts @@ -17,6 +17,4 @@ * under the License. */ -export * from './visualization'; -export * from './visualization_chart'; -export * from './visualization_noresults'; +export { PersistedState } from './persisted_state'; diff --git a/src/ui/public/visualize/components/visualization_noresults.js b/src/ui/public/persisted_state/persisted_state.d.ts similarity index 66% rename from src/ui/public/visualize/components/visualization_noresults.js rename to src/ui/public/persisted_state/persisted_state.d.ts index 17ece5ffb4758..6a02df8f67f7b 100644 --- a/src/ui/public/visualize/components/visualization_noresults.js +++ b/src/ui/public/persisted_state/persisted_state.d.ts @@ -17,17 +17,6 @@ * under the License. */ -import React from 'react'; - -export function VisualizationNoResults() { - return ( -
-
-
-