Convert notify.warning calls to use toastNotifications (#20767) (#21054)

* 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.
This commit is contained in:
CJ Cenizal 2018-07-20 16:44:22 -07:00 committed by GitHub
parent 54d4c5332c
commit b003d29751
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 129 additions and 564 deletions

View file

@ -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);
});
}

View file

@ -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(', ')}`,
});
}
});

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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'),

View file

@ -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() {

View file

@ -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) {

View file

@ -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,
});

View file

@ -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).

View file

@ -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);
});
});
});

View file

@ -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: (
<Fragment>
For more information, view the {' '}
<a href={kbnUrl.getRouteHref(indexPattern, 'edit')}>{indexPattern.title} index pattern</a>
</Fragment>
),
});
}
}

View file

@ -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();

View file

@ -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

View file

@ -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);
});
});
});

View file

@ -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
);
}
};
}

View file

@ -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();
};

View file

@ -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: <p>Go to <a href={url}>Management &gt; Index Patterns</a> and create an index pattern</p>,
});
}
@ -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();

View file

@ -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);
}
}

View file

@ -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: (
<EuiCallOut
iconType="iInCircle"
color="warning"
title={message}
/>
),
});
}
}

View file

@ -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();

View file

@ -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 {

View file

@ -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: (
<Fragment>
<p>The Basic license does not support multi-cluster monitoring.</p>
<p>
Need to monitor multiple clusters?{' '}
<a href="https://www.elastic.co/subscriptions/xpack" target="_blank">Get a license with full functionality</a>{' '}
to enjoy multi-cluster monitoring.
</p>
</Fragment>
),
});
}
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: (
<Fragment>
<p>The license information is invalid.</p>
<p>
Need a license?{' '}
<a href={licensingPath}>Get a free Basic license</a> or{' '}
<a href="https://www.elastic.co/subscriptions/xpack" target="_blank">get a license with full functionality</a>{' '}
to enjoy multi-cluster monitoring.
</p>
</Fragment>
),
});
}
getClusterAction() {

View file

@ -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();
}

View file

@ -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 () => {