mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
Merge remote-tracking branch 'origin/master' into feature/merge-code
This commit is contained in:
commit
d13c15b862
198 changed files with 4945 additions and 2410 deletions
|
@ -361,7 +361,7 @@
|
|||
"jest-raw-loader": "^1.0.1",
|
||||
"jimp": "0.2.28",
|
||||
"json5": "^1.0.1",
|
||||
"karma": "1.7.0",
|
||||
"karma": "3.1.4",
|
||||
"karma-chrome-launcher": "2.1.1",
|
||||
"karma-coverage": "1.1.1",
|
||||
"karma-firefox-launcher": "1.0.1",
|
||||
|
|
|
@ -19,6 +19,6 @@
|
|||
|
||||
import { clog } from './clog';
|
||||
|
||||
export const commonFunctions = [
|
||||
export const browserFunctions = [
|
||||
clog,
|
||||
];
|
|
@ -17,4 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import '../common/register';
|
||||
import { browserFunctions } from './index';
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
browserFunctions.forEach(canvas.register);
|
||||
|
|
|
@ -17,7 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { commonFunctions } from './index';
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
commonFunctions.forEach(canvas.register);
|
||||
import '../common/register';
|
||||
|
|
|
@ -84,6 +84,21 @@ export const schema = Joi.object().keys({
|
|||
esRequestTimeout: Joi.number().default(30000),
|
||||
kibanaStabilize: Joi.number().default(15000),
|
||||
navigateStatusPageCheck: Joi.number().default(250),
|
||||
|
||||
// Many of our tests use the `exists` functions to determine where the user is. For
|
||||
// example, you'll see a lot of code like:
|
||||
// if (!testSubjects.exists('someElementOnPageA')) {
|
||||
// navigateToPageA();
|
||||
// }
|
||||
// If the element doesn't exist, selenium would wait up to defaultFindTimeout for it to
|
||||
// appear. Because there are many times when we expect it to not be there, we don't want
|
||||
// to wait the full amount of time, or it would greatly slow our tests down. We used to have
|
||||
// this value at 1 second, but this caused flakiness because sometimes the element was deemed missing
|
||||
// only because the page hadn't finished loading.
|
||||
// The best path forward it to prefer functions like `testSubjects.existOrFail` or
|
||||
// `testSubjects.missingOrFail` instead of just the `exists` checks, and be deterministic about
|
||||
// where your user is and what they should click next.
|
||||
waitForExists: Joi.number().default(2500),
|
||||
}).default(),
|
||||
|
||||
mochaOpts: Joi.object().keys({
|
||||
|
|
|
@ -17,18 +17,23 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
const createdInstanceProxies = new WeakSet();
|
||||
const INITIALIZING = Symbol('async instance initializing');
|
||||
const asyncInitFns = new WeakSet();
|
||||
|
||||
export const isAsyncInstance = val =>(
|
||||
createdInstanceProxies.has(val)
|
||||
export const isAsyncInstance = val => (
|
||||
val && asyncInitFns.has(val.init)
|
||||
);
|
||||
|
||||
export const createAsyncInstance = (type, name, promiseForValue) => {
|
||||
let instance = INITIALIZING;
|
||||
|
||||
const initPromise = promiseForValue.then(v => instance = v);
|
||||
const initFn = () => initPromise;
|
||||
const loadingTarget = {
|
||||
init() {
|
||||
return initPromise;
|
||||
}
|
||||
};
|
||||
asyncInitFns.add(loadingTarget.init);
|
||||
|
||||
const assertReady = desc => {
|
||||
if (instance === INITIALIZING) {
|
||||
|
@ -46,7 +51,7 @@ export const createAsyncInstance = (type, name, promiseForValue) => {
|
|||
}
|
||||
};
|
||||
|
||||
const proxy = new Proxy({}, {
|
||||
return new Proxy(loadingTarget, {
|
||||
apply(target, context, args) {
|
||||
assertReady(`${name}()`);
|
||||
return Reflect.apply(instance, context, args);
|
||||
|
@ -68,13 +73,19 @@ export const createAsyncInstance = (type, name, promiseForValue) => {
|
|||
},
|
||||
|
||||
get(target, prop, receiver) {
|
||||
if (prop === 'init') return initFn;
|
||||
if (loadingTarget.hasOwnProperty(prop)) {
|
||||
return Reflect.get(loadingTarget, prop, receiver);
|
||||
}
|
||||
|
||||
assertReady(`${name}.${prop}`);
|
||||
return Reflect.get(instance, prop, receiver);
|
||||
},
|
||||
|
||||
getOwnPropertyDescriptor(target, prop) {
|
||||
if (loadingTarget.hasOwnProperty(prop)) {
|
||||
return Reflect.getOwnPropertyDescriptor(loadingTarget, prop);
|
||||
}
|
||||
|
||||
assertReady(`${name}.${prop}`);
|
||||
return Reflect.getOwnPropertyDescriptor(instance, prop);
|
||||
},
|
||||
|
@ -85,7 +96,9 @@ export const createAsyncInstance = (type, name, promiseForValue) => {
|
|||
},
|
||||
|
||||
has(target, prop) {
|
||||
if (prop === 'init') return true;
|
||||
if (!loadingTarget.hasOwnProperty(prop)) {
|
||||
return Reflect.has(loadingTarget, prop);
|
||||
}
|
||||
|
||||
assertReady(`${name}.${prop}`);
|
||||
return Reflect.has(instance, prop);
|
||||
|
@ -116,10 +129,4 @@ export const createAsyncInstance = (type, name, promiseForValue) => {
|
|||
return Reflect.setPrototypeOf(instance, prototype);
|
||||
}
|
||||
});
|
||||
|
||||
// add the created provider to the WeakMap so we can
|
||||
// check for it later in `isAsyncProvider()`
|
||||
createdInstanceProxies.add(proxy);
|
||||
|
||||
return proxy;
|
||||
};
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
import { loadTracer } from '../load_tracer';
|
||||
import { createAsyncInstance, isAsyncInstance } from './async_instance';
|
||||
import { createVerboseInstance } from './verbose_instance';
|
||||
|
||||
export class ProviderCollection {
|
||||
constructor(log, providers) {
|
||||
|
@ -104,6 +105,14 @@ export class ProviderCollection {
|
|||
instance = createAsyncInstance(type, name, instance);
|
||||
}
|
||||
|
||||
if (name !== '__leadfoot__' && name !== 'log' && name !== 'config' && instance && typeof instance === 'object') {
|
||||
instance = createVerboseInstance(
|
||||
this._log,
|
||||
type === 'PageObject' ? `PageObjects.${name}` : name,
|
||||
instance
|
||||
);
|
||||
}
|
||||
|
||||
instances.set(provider, instance);
|
||||
}
|
||||
|
||||
|
|
81
src/functional_test_runner/lib/providers/verbose_instance.js
Normal file
81
src/functional_test_runner/lib/providers/verbose_instance.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { inspect } from 'util';
|
||||
|
||||
function printArgs(args) {
|
||||
return args.map((arg) => {
|
||||
if (typeof arg === 'string' || typeof arg === 'number' || arg instanceof Date) {
|
||||
return inspect(arg);
|
||||
}
|
||||
|
||||
if (Array.isArray(arg)) {
|
||||
return `[${printArgs(arg)}]`;
|
||||
}
|
||||
|
||||
return Object.prototype.toString.call(arg);
|
||||
}).join(', ');
|
||||
}
|
||||
|
||||
export function createVerboseInstance(log, name, instance) {
|
||||
if (!log.getWriters().some(l => l.level.flags.verbose)) {
|
||||
return instance;
|
||||
}
|
||||
|
||||
return new Proxy(instance, {
|
||||
get(_, prop) {
|
||||
const value = instance[prop];
|
||||
|
||||
if (typeof value !== 'function' || prop === 'init') {
|
||||
return value;
|
||||
}
|
||||
|
||||
return function (...args) {
|
||||
log.verbose(`${name}.${prop}(${printArgs(args)})`);
|
||||
log.indent(2);
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = {
|
||||
returned: value.apply(this, args)
|
||||
};
|
||||
} catch (error) {
|
||||
result = {
|
||||
thrown: error
|
||||
};
|
||||
}
|
||||
|
||||
if (result.hasOwnProperty('thrown')) {
|
||||
log.indent(-2);
|
||||
throw result.thrown;
|
||||
}
|
||||
|
||||
const { returned } = result;
|
||||
if (returned && typeof returned.then === 'function') {
|
||||
return returned.finally(() => {
|
||||
log.indent(-2);
|
||||
});
|
||||
}
|
||||
|
||||
log.indent(-2);
|
||||
return returned;
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
|
@ -88,7 +88,22 @@ uiModules
|
|||
const list = self.getList();
|
||||
if (!list) return;
|
||||
|
||||
list.push(_.last(list) + 1);
|
||||
function getNext() {
|
||||
if (list.length === 0) {
|
||||
// returning NaN adds an empty input
|
||||
return NaN;
|
||||
}
|
||||
|
||||
const next = _.last(list) + 1;
|
||||
if (next < self.range.max) {
|
||||
return next;
|
||||
}
|
||||
|
||||
return self.range.max - 1;
|
||||
}
|
||||
|
||||
const next = getNext();
|
||||
list.push(next);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -21,51 +21,77 @@ import { resolve } from 'path';
|
|||
import { tmpdir } from 'os';
|
||||
import { styleSheetPaths } from './style_sheet_paths';
|
||||
|
||||
describe('uiExports.styleSheetPaths', () => {
|
||||
const dir = tmpdir();
|
||||
const pluginSpec = {
|
||||
getId: jest.fn(() => 'test'),
|
||||
getPublicDir: jest.fn(() => resolve(dir, 'kibana/public'))
|
||||
};
|
||||
const dir = tmpdir();
|
||||
const pluginSpec = {
|
||||
getId: jest.fn(() => 'test'),
|
||||
getPublicDir: jest.fn(() => resolve(dir, 'kibana/public')),
|
||||
};
|
||||
|
||||
expect.addSnapshotSerializer({
|
||||
test: value => typeof value === 'string' && value.startsWith(dir),
|
||||
print: value => value.replace(dir, '<absolute>'),
|
||||
});
|
||||
|
||||
describe('uiExports.styleSheetPaths', () => {
|
||||
it('does not support relative paths', () => {
|
||||
expect(() => styleSheetPaths([], 'public/bar.css', 'styleSheetPaths', pluginSpec))
|
||||
.toThrowError(/\[plugin:test\] uiExports.styleSheetPaths must be an absolute path/);
|
||||
expect(() => styleSheetPaths([], 'public/bar.css', 'styleSheetPaths', pluginSpec)).toThrowError(
|
||||
/\[plugin:test\] uiExports.styleSheetPaths must be an absolute path/
|
||||
);
|
||||
});
|
||||
|
||||
it('path must be child of public path', () => {
|
||||
expect(() => styleSheetPaths([], '/another/public/bar.css', 'styleSheetPaths', pluginSpec))
|
||||
.toThrowError(/\[plugin:test\] uiExports.styleSheetPaths must be child of publicDir/);
|
||||
expect(() =>
|
||||
styleSheetPaths([], '/another/public/bar.css', 'styleSheetPaths', pluginSpec)
|
||||
).toThrowError(/\[plugin:test\] uiExports.styleSheetPaths must be child of publicDir/);
|
||||
});
|
||||
|
||||
it('only supports css or scss extensions', () => {
|
||||
expect(() => styleSheetPaths([], '/kibana/public/bar.bad', 'styleSheetPaths', pluginSpec))
|
||||
.toThrowError('[plugin:test] uiExports.styleSheetPaths supported extensions [.css, .scss], got ".bad"');
|
||||
expect(() =>
|
||||
styleSheetPaths([], '/kibana/public/bar.bad', 'styleSheetPaths', pluginSpec)
|
||||
).toThrowError(
|
||||
'[plugin:test] uiExports.styleSheetPaths supported extensions [.css, .scss], got ".bad"'
|
||||
);
|
||||
});
|
||||
|
||||
it('provides publicPath for scss extensions', () => {
|
||||
const localPath = resolve(dir, 'kibana/public/bar.scss');
|
||||
const uiExports = styleSheetPaths([], localPath, 'styleSheetPaths', pluginSpec);
|
||||
|
||||
expect(uiExports.styleSheetPaths).toHaveLength(1);
|
||||
expect(uiExports.styleSheetPaths[0].localPath).toEqual(localPath);
|
||||
expect(uiExports.styleSheetPaths[0].publicPath).toEqual('plugins/test/bar.css');
|
||||
expect(uiExports.styleSheetPaths).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"localPath": <absolute>/kibana/public/bar.scss,
|
||||
"publicPath": "plugins/test/bar.css",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('provides publicPath for css extensions', () => {
|
||||
const localPath = resolve(dir, 'kibana/public/bar.scss');
|
||||
const uiExports = styleSheetPaths([], localPath, 'styleSheetPaths', pluginSpec);
|
||||
|
||||
expect(uiExports.styleSheetPaths).toHaveLength(1);
|
||||
expect(uiExports.styleSheetPaths[0].localPath).toEqual(localPath);
|
||||
expect(uiExports.styleSheetPaths[0].publicPath).toEqual('plugins/test/bar.css');
|
||||
expect(uiExports.styleSheetPaths).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"localPath": <absolute>/kibana/public/bar.scss,
|
||||
"publicPath": "plugins/test/bar.css",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should normalize mixed slashes', () => {
|
||||
const localPath = resolve(dir, 'kibana/public\\bar.scss');
|
||||
const uiExports = styleSheetPaths([], localPath, 'styleSheetPaths', pluginSpec);
|
||||
|
||||
expect(uiExports.styleSheetPaths).toHaveLength(1);
|
||||
expect(uiExports.styleSheetPaths[0].localPath).toEqual(localPath);
|
||||
expect(uiExports.styleSheetPaths).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"localPath": <absolute>/kibana/public\\bar.scss,
|
||||
"publicPath": "plugins/test/../public/bar.css",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -113,7 +113,8 @@ export default function ({ getService, getPageObjects }) {
|
|||
expect(pieData).to.eql(expectedTableData);
|
||||
});
|
||||
|
||||
it('should apply correct filter on other bucket', async () => {
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/25955
|
||||
it.skip('should apply correct filter on other bucket', async () => {
|
||||
const expectedTableData = [ 'Missing', 'osx' ];
|
||||
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
@ -125,7 +126,8 @@ export default function ({ getService, getPageObjects }) {
|
|||
await filterBar.removeFilter('machine.os.raw');
|
||||
});
|
||||
|
||||
it('should apply correct filter on other bucket by clicking on a legend', async () => {
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/26323
|
||||
it.skip('should apply correct filter on other bucket by clicking on a legend', async () => {
|
||||
const expectedTableData = [ 'Missing', 'osx' ];
|
||||
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
|
|
@ -249,7 +249,7 @@ export function CommonPageProvider({ getService, getPageObjects }) {
|
|||
}
|
||||
|
||||
async pressEnterKey() {
|
||||
await browser.pressKeys('\uE007');
|
||||
await browser.pressKeys(browser.keys.ENTER);
|
||||
}
|
||||
|
||||
// pass in true if your test will show multiple modals
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import Keys from 'leadfoot/keys';
|
||||
|
||||
export function VisualBuilderPageProvider({ getService, getPageObjects }) {
|
||||
const find = getService('find');
|
||||
const retry = getService('retry');
|
||||
|
@ -65,12 +63,12 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }) {
|
|||
// a textarea we must really select all text and remove it, and cannot use
|
||||
// clearValue().
|
||||
if (process.platform === 'darwin') {
|
||||
await browser.pressKeys([Keys.COMMAND, 'a']); // Select all Mac
|
||||
await input.pressKeys([browser.keys.COMMAND, 'a']); // Select all Mac
|
||||
} else {
|
||||
await browser.pressKeys([Keys.CONTROL, 'a']); // Select all for everything else
|
||||
await input.pressKeys([browser.keys.CONTROL, 'a']); // Select all for everything else
|
||||
}
|
||||
await browser.pressKeys(Keys.NULL); // Release modifier keys
|
||||
await browser.pressKeys(Keys.BACKSPACE); // Delete all content
|
||||
await input.pressKeys(browser.keys.NULL); // Release modifier keys
|
||||
await input.pressKeys(browser.keys.BACKSPACE); // Delete all content
|
||||
await input.type(markdown);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
}
|
||||
|
@ -208,7 +206,7 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }) {
|
|||
const el = await testSubjects.find('comboBoxSearchInput');
|
||||
await el.clearValue();
|
||||
await el.type(timeField);
|
||||
await browser.pressKeys(Keys.RETURN);
|
||||
await el.pressKeys(browser.keys.RETURN);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
import { VisualizeConstants } from '../../../src/legacy/core_plugins/kibana/public/visualize/visualize_constants';
|
||||
import Bluebird from 'bluebird';
|
||||
import expect from 'expect.js';
|
||||
import Keys from 'leadfoot/keys';
|
||||
|
||||
export function VisualizePageProvider({ getService, getPageObjects }) {
|
||||
const browser = getService('browser');
|
||||
|
@ -399,7 +398,7 @@ export function VisualizePageProvider({ getService, getPageObjects }) {
|
|||
await find.clickByCssSelector(selector);
|
||||
const input = await find.byCssSelector(`${selector} input.ui-select-search`);
|
||||
await input.type(myString);
|
||||
await browser.pressKeys(Keys.RETURN);
|
||||
await input.pressKeys(browser.keys.RETURN);
|
||||
});
|
||||
await PageObjects.common.sleep(500);
|
||||
}
|
||||
|
@ -508,7 +507,8 @@ export function VisualizePageProvider({ getService, getPageObjects }) {
|
|||
`;
|
||||
await find.clickByCssSelector(selector);
|
||||
await find.setValue(`${selector} input.ui-select-search`, fieldValue);
|
||||
await browser.pressKeys(Keys.RETURN);
|
||||
const input = await find.byCssSelector(`${selector} input.ui-select-search`);
|
||||
await input.pressKeys(browser.keys.RETURN);
|
||||
}
|
||||
|
||||
async selectFieldById(fieldValue, id) {
|
||||
|
@ -546,7 +546,7 @@ export function VisualizePageProvider({ getService, getPageObjects }) {
|
|||
// was a long enough gap from the typing above to the space click. Hence the
|
||||
// need for the sleep.
|
||||
await PageObjects.common.sleep(500);
|
||||
await browser.pressKeys(Keys.SPACE);
|
||||
await input.pressKeys(browser.keys.SPACE);
|
||||
}
|
||||
|
||||
async setCustomInterval(newValue) {
|
||||
|
@ -612,7 +612,7 @@ export function VisualizePageProvider({ getService, getPageObjects }) {
|
|||
|
||||
async sizeUpEditor() {
|
||||
await testSubjects.click('visualizeEditorResizer');
|
||||
await browser.pressKeys(Keys.ARROW_RIGHT);
|
||||
await browser.pressKeys(browser.keys.ARROW_RIGHT);
|
||||
}
|
||||
|
||||
async clickOptions() {
|
||||
|
|
|
@ -18,11 +18,18 @@
|
|||
*/
|
||||
|
||||
import { modifyUrl } from '../../../src/core/utils';
|
||||
import Keys from 'leadfoot/keys';
|
||||
|
||||
export function BrowserProvider({ getService }) {
|
||||
const leadfoot = getService('__leadfoot__');
|
||||
|
||||
return new class BrowserService {
|
||||
class BrowserService {
|
||||
|
||||
/**
|
||||
* Keyboard events
|
||||
*/
|
||||
keys = Keys;
|
||||
|
||||
/**
|
||||
* Gets the dimensions of a window.
|
||||
* https://theintern.io/leadfoot/module-leadfoot_Session.html#getWindowSize
|
||||
|
@ -34,7 +41,6 @@ export function BrowserProvider({ getService }) {
|
|||
return await leadfoot.getWindowSize(...args);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the dimensions of a window.
|
||||
* https://theintern.io/leadfoot/module-leadfoot_Session.html#setWindowSize
|
||||
|
@ -245,5 +251,7 @@ export function BrowserProvider({ getService }) {
|
|||
async execute(...args) {
|
||||
return await leadfoot.execute(...args);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return new BrowserService();
|
||||
}
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import Keys from 'leadfoot/keys';
|
||||
|
||||
export function FilterBarProvider({ getService, getPageObjects }) {
|
||||
const browser = getService('browser');
|
||||
const testSubjects = getService('testSubjects');
|
||||
|
@ -92,7 +90,7 @@ export function FilterBarProvider({ getService, getPageObjects }) {
|
|||
}
|
||||
for (let j = 0; j < fieldValues.length; j++) {
|
||||
await paramFields[i].type(fieldValues[j]);
|
||||
await browser.pressKeys(Keys.RETURN);
|
||||
await paramFields[i].pressKeys(browser.keys.RETURN);
|
||||
}
|
||||
}
|
||||
await testSubjects.click('saveFilter');
|
||||
|
|
|
@ -19,27 +19,13 @@
|
|||
|
||||
import { LeadfootElementWrapper } from './lib/leadfoot_element_wrapper';
|
||||
|
||||
// Many of our tests use the `exists` functions to determine where the user is. For
|
||||
// example, you'll see a lot of code like:
|
||||
// if (!testSubjects.exists('someElementOnPageA')) {
|
||||
// navigateToPageA();
|
||||
// }
|
||||
// If the element doesn't exist, selenium would wait up to defaultFindTimeout for it to
|
||||
// appear. Because there are many times when we expect it to not be there, we don't want
|
||||
// to wait the full amount of time, or it would greatly slow our tests down. We used to have
|
||||
// this value at 1 second, but this caused flakiness because sometimes the element was deemed missing
|
||||
// only because the page hadn't finished loading.
|
||||
// The best path forward it to prefer functions like `testSubjects.existOrFail` or
|
||||
// `testSubjects.missingOrFail` instead of just the `exists` checks, and be deterministic about
|
||||
// where your user is and what they should click next.
|
||||
export const WAIT_FOR_EXISTS_TIME = 2500;
|
||||
|
||||
export function FindProvider({ getService }) {
|
||||
const log = getService('log');
|
||||
const config = getService('config');
|
||||
const leadfoot = getService('__leadfoot__');
|
||||
const retry = getService('retry');
|
||||
|
||||
const WAIT_FOR_EXISTS_TIME = config.get('timeouts.waitForExists');
|
||||
const defaultFindTimeout = config.get('timeouts.find');
|
||||
|
||||
const wrap = leadfootElement => (
|
||||
|
|
|
@ -313,4 +313,15 @@ export class LeadfootElementWrapper {
|
|||
async findByXpath(xpath) {
|
||||
return this._wrap(await this._leadfootElement.findByXpath(xpath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends key event into element.
|
||||
* https://theintern.io/leadfoot/module-leadfoot_Session.html#pressKeys
|
||||
*
|
||||
* @param {string|string[]} keys
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async pressKeys(...args) {
|
||||
await this._leadfoot.pressKeys(...args);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,15 +24,15 @@ import {
|
|||
map as mapAsync,
|
||||
} from 'bluebird';
|
||||
|
||||
import { WAIT_FOR_EXISTS_TIME } from './find';
|
||||
|
||||
export function TestSubjectsProvider({ getService }) {
|
||||
const log = getService('log');
|
||||
const retry = getService('retry');
|
||||
const browser = getService('browser');
|
||||
const find = getService('find');
|
||||
const config = getService('config');
|
||||
const defaultFindTimeout = config.get('timeouts.find');
|
||||
|
||||
const FIND_TIME = config.get('timeouts.find');
|
||||
const WAIT_FOR_EXISTS_TIME = config.get('timeouts.waitForExists');
|
||||
|
||||
class TestSubjects {
|
||||
async exists(selector, timeout = WAIT_FOR_EXISTS_TIME) {
|
||||
|
@ -65,17 +65,17 @@ export function TestSubjectsProvider({ getService }) {
|
|||
});
|
||||
}
|
||||
|
||||
async clickWhenNotDisabled(selector, { timeout } = { timeout: defaultFindTimeout }) {
|
||||
async clickWhenNotDisabled(selector, { timeout = FIND_TIME } = {}) {
|
||||
log.debug(`TestSubjects.click(${selector})`);
|
||||
await find.clickByCssSelectorWhenNotDisabled(testSubjSelector(selector), { timeout });
|
||||
}
|
||||
|
||||
async click(selector, timeout = defaultFindTimeout) {
|
||||
async click(selector, timeout = FIND_TIME) {
|
||||
log.debug(`TestSubjects.click(${selector})`);
|
||||
await find.clickByCssSelector(testSubjSelector(selector), timeout);
|
||||
}
|
||||
|
||||
async doubleClick(selector, timeout = defaultFindTimeout) {
|
||||
async doubleClick(selector, timeout = FIND_TIME) {
|
||||
log.debug(`TestSubjects.doubleClick(${selector})`);
|
||||
return await retry.try(async () => {
|
||||
const element = await this.find(selector, timeout);
|
||||
|
|
|
@ -79,7 +79,7 @@ exports[`DetailView should render StickyProperties 1`] = `
|
|||
pathname="/app/apm"
|
||||
query={
|
||||
Object {
|
||||
"traceid": "traceId",
|
||||
"traceId": "traceId",
|
||||
"transactionId": "myTransactionName",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,42 +104,61 @@ export function DetailView({ errorGroup, urlParams, location }: Props) {
|
|||
}
|
||||
|
||||
const transactionLink = getTransactionLink(error, transaction);
|
||||
const notAvailableLabel = i18n.translate(
|
||||
'xpack.apm.errorGroupDetails.notAvailableLabel',
|
||||
{
|
||||
defaultMessage: 'N/A'
|
||||
}
|
||||
);
|
||||
const stickyProperties = [
|
||||
{
|
||||
fieldName: '@timestamp',
|
||||
label: 'Timestamp',
|
||||
label: i18n.translate('xpack.apm.errorGroupDetails.timestampLabel', {
|
||||
defaultMessage: 'Timestamp'
|
||||
}),
|
||||
val: error['@timestamp'],
|
||||
width: '50%'
|
||||
},
|
||||
{
|
||||
fieldName: REQUEST_URL_FULL,
|
||||
label: 'URL',
|
||||
val: get(error, REQUEST_URL_FULL, 'N/A'),
|
||||
val: get(error, REQUEST_URL_FULL, notAvailableLabel),
|
||||
truncated: true,
|
||||
width: '50%'
|
||||
},
|
||||
{
|
||||
fieldName: REQUEST_METHOD,
|
||||
label: 'Request method',
|
||||
val: get(error, REQUEST_METHOD, 'N/A'),
|
||||
label: i18n.translate('xpack.apm.errorGroupDetails.requestMethodLabel', {
|
||||
defaultMessage: 'Request method'
|
||||
}),
|
||||
val: get(error, REQUEST_METHOD, notAvailableLabel),
|
||||
width: '25%'
|
||||
},
|
||||
{
|
||||
fieldName: ERROR_EXC_HANDLED,
|
||||
label: 'Handled',
|
||||
val: String(get(error, ERROR_EXC_HANDLED, 'N/A')),
|
||||
label: i18n.translate('xpack.apm.errorGroupDetails.handledLabel', {
|
||||
defaultMessage: 'Handled'
|
||||
}),
|
||||
val: String(get(error, ERROR_EXC_HANDLED, notAvailableLabel)),
|
||||
width: '25%'
|
||||
},
|
||||
{
|
||||
fieldName: TRANSACTION_ID,
|
||||
label: 'Transaction sample ID',
|
||||
val: transactionLink || 'N/A',
|
||||
label: i18n.translate(
|
||||
'xpack.apm.errorGroupDetails.transactionSampleIdLabel',
|
||||
{
|
||||
defaultMessage: 'Transaction sample ID'
|
||||
}
|
||||
),
|
||||
val: transactionLink || notAvailableLabel,
|
||||
width: '25%'
|
||||
},
|
||||
{
|
||||
fieldName: USER_ID,
|
||||
label: 'User ID',
|
||||
val: get(error, USER_ID, 'N/A'),
|
||||
label: i18n.translate('xpack.apm.errorGroupDetails.userIdLabel', {
|
||||
defaultMessage: 'User ID'
|
||||
}),
|
||||
val: get(error, USER_ID, notAvailableLabel),
|
||||
width: '25%'
|
||||
}
|
||||
];
|
||||
|
@ -151,11 +170,25 @@ export function DetailView({ errorGroup, urlParams, location }: Props) {
|
|||
<Container>
|
||||
<HeaderContainer>
|
||||
<EuiTitle size="s">
|
||||
<h3>Error occurrence</h3>
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.apm.errorGroupDetails.errorOccurrenceTitle',
|
||||
{
|
||||
defaultMessage: 'Error occurrence'
|
||||
}
|
||||
)}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<DiscoverErrorButton error={error} kuery={urlParams.kuery}>
|
||||
<EuiButtonEmpty iconType="discoverApp">
|
||||
{`View ${occurrencesCount} occurrences in Discover`}
|
||||
{i18n.translate(
|
||||
'xpack.apm.errorGroupDetails.viewOccurrencesInDiscoverButtonLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'View {occurrencesCount} occurrences in Discover',
|
||||
values: { occurrencesCount }
|
||||
}
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
</DiscoverErrorButton>
|
||||
</HeaderContainer>
|
||||
|
@ -212,7 +245,7 @@ function getTransactionLink(error: APMError, transaction?: Transaction) {
|
|||
hash={path}
|
||||
query={{
|
||||
transactionId: transaction.transaction.id,
|
||||
traceid: get(transaction, TRACE_ID)
|
||||
traceId: get(transaction, TRACE_ID)
|
||||
}}
|
||||
>
|
||||
{transaction.transaction.id}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import Histogram from '../../../shared/charts/Histogram';
|
||||
import { EmptyMessage } from '../../../shared/EmptyMessage';
|
||||
|
@ -23,7 +24,12 @@ export function getFormattedBuckets(buckets, bucketSize) {
|
|||
});
|
||||
}
|
||||
|
||||
function Distribution({ distribution, title = 'Occurrences' }) {
|
||||
function Distribution({
|
||||
distribution,
|
||||
title = i18n.translate('xpack.apm.errorGroupDetails.occurrencesChartLabel', {
|
||||
defaultMessage: 'Occurrences'
|
||||
})
|
||||
}) {
|
||||
const buckets = getFormattedBuckets(
|
||||
distribution.buckets,
|
||||
distribution.bucketSize
|
||||
|
@ -32,7 +38,13 @@ function Distribution({ distribution, title = 'Occurrences' }) {
|
|||
const isEmpty = distribution.totalHits === 0;
|
||||
|
||||
if (isEmpty) {
|
||||
return <EmptyMessage heading="No errors were found" />;
|
||||
return (
|
||||
<EmptyMessage
|
||||
heading={i18n.translate('xpack.apm.errorGroupDetails.noErrorsLabel', {
|
||||
defaultMessage: 'No errors were found'
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -45,8 +57,18 @@ function Distribution({ distribution, title = 'Occurrences' }) {
|
|||
xType="time"
|
||||
buckets={buckets}
|
||||
bucketSize={distribution.bucketSize}
|
||||
formatYShort={value => `${value} occ.`}
|
||||
formatYLong={value => `${value} occurrences`}
|
||||
formatYShort={value =>
|
||||
i18n.translate('xpack.apm.errorGroupDetails.occurrencesShortLabel', {
|
||||
defaultMessage: '{occCount} occ.',
|
||||
values: { occCount: value }
|
||||
})
|
||||
}
|
||||
formatYLong={value =>
|
||||
i18n.translate('xpack.apm.errorGroupDetails.occurrencesLongLabel', {
|
||||
defaultMessage: '{occCount} occurrences',
|
||||
values: { occCount: value }
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { EuiBadge, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { get } from 'lodash';
|
||||
import React, { Fragment } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
@ -57,9 +58,16 @@ const Culprit = styled.div`
|
|||
font-family: ${fontFamilyCode};
|
||||
`;
|
||||
|
||||
const notAvailableLabel = i18n.translate(
|
||||
'xpack.apm.errorGroupDetails.notAvailableLabel',
|
||||
{
|
||||
defaultMessage: 'N/A'
|
||||
}
|
||||
);
|
||||
|
||||
function getShortGroupId(errorGroupId?: string) {
|
||||
if (!errorGroupId) {
|
||||
return 'N/A';
|
||||
return notAvailableLabel;
|
||||
}
|
||||
|
||||
return errorGroupId.slice(0, 5);
|
||||
|
@ -87,9 +95,21 @@ export function ErrorGroupDetails({ urlParams, location }: Props) {
|
|||
<div>
|
||||
<EuiTitle>
|
||||
<span>
|
||||
Error group {getShortGroupId(urlParams.errorGroupId)}
|
||||
{i18n.translate('xpack.apm.errorGroupDetails.errorGroupTitle', {
|
||||
defaultMessage: 'Error group {errorGroupId}',
|
||||
values: {
|
||||
errorGroupId: getShortGroupId(urlParams.errorGroupId)
|
||||
}
|
||||
})}
|
||||
{isUnhandled && (
|
||||
<UnhandledBadge color="warning">Unhandled</UnhandledBadge>
|
||||
<UnhandledBadge color="warning">
|
||||
{i18n.translate(
|
||||
'xpack.apm.errorGroupDetails.unhandledLabel',
|
||||
{
|
||||
defaultMessage: 'Unhandled'
|
||||
}
|
||||
)}
|
||||
</UnhandledBadge>
|
||||
)}
|
||||
</span>
|
||||
</EuiTitle>
|
||||
|
@ -105,14 +125,35 @@ export function ErrorGroupDetails({ urlParams, location }: Props) {
|
|||
<EuiText>
|
||||
{logMessage && (
|
||||
<Fragment>
|
||||
<Label>Log message</Label>
|
||||
<Label>
|
||||
{i18n.translate(
|
||||
'xpack.apm.errorGroupDetails.logMessageLabel',
|
||||
{
|
||||
defaultMessage: 'Log message'
|
||||
}
|
||||
)}
|
||||
</Label>
|
||||
<Message>{logMessage}</Message>
|
||||
</Fragment>
|
||||
)}
|
||||
<Label>Exception message</Label>
|
||||
<Message>{excMessage || 'N/A'}</Message>
|
||||
<Label>Culprit</Label>
|
||||
<Culprit>{culprit || 'N/A'}</Culprit>
|
||||
<Label>
|
||||
{i18n.translate(
|
||||
'xpack.apm.errorGroupDetails.exceptionMessageLabel',
|
||||
{
|
||||
defaultMessage: 'Exception message'
|
||||
}
|
||||
)}
|
||||
</Label>
|
||||
<Message>{excMessage || notAvailableLabel}</Message>
|
||||
<Label>
|
||||
{i18n.translate(
|
||||
'xpack.apm.errorGroupDetails.culpritLabel',
|
||||
{
|
||||
defaultMessage: 'Culprit'
|
||||
}
|
||||
)}
|
||||
</Label>
|
||||
<Culprit>{culprit || notAvailableLabel}</Culprit>
|
||||
</EuiText>
|
||||
</Titles>
|
||||
)}
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
fontSizes,
|
||||
truncate
|
||||
} from '../../../../style/variables';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
function paginateItems({ items, pageIndex, pageSize }) {
|
||||
return items.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
|
||||
|
@ -47,6 +48,13 @@ const Culprit = styled.div`
|
|||
font-family: ${fontFamilyCode};
|
||||
`;
|
||||
|
||||
const notAvailableLabel = i18n.translate(
|
||||
'xpack.apm.errorsTable.notAvailableLabel',
|
||||
{
|
||||
defaultMessage: 'N/A'
|
||||
}
|
||||
);
|
||||
|
||||
class List extends Component {
|
||||
state = {
|
||||
page: {
|
||||
|
@ -82,33 +90,40 @@ class List extends Component {
|
|||
|
||||
const columns = [
|
||||
{
|
||||
name: 'Group ID',
|
||||
name: i18n.translate('xpack.apm.errorsTable.groupIdColumnLabel', {
|
||||
defaultMessage: 'Group ID'
|
||||
}),
|
||||
field: 'groupId',
|
||||
sortable: false,
|
||||
width: px(unit * 6),
|
||||
render: groupId => {
|
||||
return (
|
||||
<GroupIdLink path={`/${serviceName}/errors/${groupId}`}>
|
||||
{groupId.slice(0, 5) || 'N/A'}
|
||||
{groupId.slice(0, 5) || notAvailableLabel}
|
||||
</GroupIdLink>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Error message and culprit',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.errorsTable.errorMessageAndCulpritColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Error message and culprit'
|
||||
}
|
||||
),
|
||||
field: 'message',
|
||||
sortable: false,
|
||||
width: '50%',
|
||||
render: (message, item) => {
|
||||
return (
|
||||
<MessageAndCulpritCell>
|
||||
<TooltipOverlay content={message || 'N/A'}>
|
||||
<TooltipOverlay content={message || notAvailableLabel}>
|
||||
<MessageLink path={`/${serviceName}/errors/${item.groupId}`}>
|
||||
{message || 'N/A'}
|
||||
{message || notAvailableLabel}
|
||||
</MessageLink>
|
||||
</TooltipOverlay>
|
||||
<TooltipOverlay content={item.culprit || 'N/A'}>
|
||||
<Culprit>{item.culprit || 'N/A'}</Culprit>
|
||||
<TooltipOverlay content={item.culprit || notAvailableLabel}>
|
||||
<Culprit>{item.culprit || notAvailableLabel}</Culprit>
|
||||
</TooltipOverlay>
|
||||
</MessageAndCulpritCell>
|
||||
);
|
||||
|
@ -121,28 +136,42 @@ class List extends Component {
|
|||
align: 'right',
|
||||
render: isUnhandled =>
|
||||
isUnhandled === false && (
|
||||
<EuiBadge color="warning">Unhandled</EuiBadge>
|
||||
<EuiBadge color="warning">
|
||||
{i18n.translate('xpack.apm.errorsTable.unhandledLabel', {
|
||||
defaultMessage: 'Unhandled'
|
||||
})}
|
||||
</EuiBadge>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'Occurrences',
|
||||
name: i18n.translate('xpack.apm.errorsTable.occurrencesColumnLabel', {
|
||||
defaultMessage: 'Occurrences'
|
||||
}),
|
||||
field: 'occurrenceCount',
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: value => (value ? numeral(value).format('0.[0]a') : 'N/A')
|
||||
render: value =>
|
||||
value ? numeral(value).format('0.[0]a') : notAvailableLabel
|
||||
},
|
||||
{
|
||||
field: 'latestOccurrenceAt',
|
||||
sortable: true,
|
||||
name: 'Latest occurrence',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.errorsTable.latestOccurrenceColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Latest occurrence'
|
||||
}
|
||||
),
|
||||
align: 'right',
|
||||
render: value => (value ? moment(value).fromNow() : 'N/A')
|
||||
render: value => (value ? moment(value).fromNow() : notAvailableLabel)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
noItemsMessage="No errors were found"
|
||||
noItemsMessage={i18n.translate('xpack.apm.errorsTable.noErrorsLabel', {
|
||||
defaultMessage: 'No errors were found'
|
||||
})}
|
||||
items={paginatedItems}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import {
|
||||
HistoryTabs,
|
||||
|
@ -19,12 +20,16 @@ import { TraceOverview } from '../TraceOverview';
|
|||
const homeTabs: IHistoryTab[] = [
|
||||
{
|
||||
path: '/services',
|
||||
name: 'Services',
|
||||
name: i18n.translate('xpack.apm.home.servicesTabLabel', {
|
||||
defaultMessage: 'Services'
|
||||
}),
|
||||
render: props => <ServiceOverview {...props} />
|
||||
},
|
||||
{
|
||||
path: '/traces',
|
||||
name: 'Traces',
|
||||
name: i18n.translate('xpack.apm.home.tracesTabLabel', {
|
||||
defaultMessage: 'Traces'
|
||||
}),
|
||||
render: props => <TraceOverview {...props} />
|
||||
}
|
||||
];
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
|
@ -56,7 +57,9 @@ export const routes = [
|
|||
exact: true,
|
||||
path: '/:serviceName/errors',
|
||||
component: ServiceDetails,
|
||||
breadcrumb: 'Errors'
|
||||
breadcrumb: i18n.translate('xpack.apm.breadcrumb.errorsTitle', {
|
||||
defaultMessage: 'Errors'
|
||||
})
|
||||
},
|
||||
{
|
||||
switch: true,
|
||||
|
@ -64,16 +67,33 @@ export const routes = [
|
|||
{
|
||||
exact: true,
|
||||
path: '/invalid-license',
|
||||
breadcrumb: 'Invalid License',
|
||||
render: () => <div>Invalid license</div>
|
||||
breadcrumb: i18n.translate('xpack.apm.breadcrumb.invalidLicenseTitle', {
|
||||
defaultMessage: 'Invalid License'
|
||||
}),
|
||||
render: () => (
|
||||
<div>
|
||||
{i18n.translate('xpack.apm.invalidLicenseLabel', {
|
||||
defaultMessage: 'Invalid license'
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/services',
|
||||
component: Home,
|
||||
breadcrumb: 'Services'
|
||||
breadcrumb: i18n.translate('xpack.apm.breadcrumb.servicesTitle', {
|
||||
defaultMessage: 'Services'
|
||||
})
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/traces',
|
||||
component: Home,
|
||||
breadcrumb: i18n.translate('xpack.apm.breadcrumb.tracesTitle', {
|
||||
defaultMessage: 'Traces'
|
||||
})
|
||||
},
|
||||
{ exact: true, path: '/traces', component: Home, breadcrumb: 'Traces' },
|
||||
{
|
||||
exact: true,
|
||||
path: '/:serviceName',
|
||||
|
@ -89,7 +109,9 @@ export const routes = [
|
|||
exact: true,
|
||||
path: '/:serviceName/transactions',
|
||||
component: ServiceDetails,
|
||||
breadcrumb: 'Transactions'
|
||||
breadcrumb: i18n.translate('xpack.apm.breadcrumb.transactionsTitle', {
|
||||
defaultMessage: 'Transactions'
|
||||
})
|
||||
},
|
||||
// Have to split this out as its own route to prevent duplicate breadcrumbs from both matching
|
||||
// if we use :transactionType? as a single route here
|
||||
|
@ -103,7 +125,9 @@ export const routes = [
|
|||
exact: true,
|
||||
path: '/:serviceName/metrics',
|
||||
component: ServiceDetails,
|
||||
breadcrumb: 'Metrics'
|
||||
breadcrumb: i18n.translate('xpack.apm.breadcrumb.metricsTitle', {
|
||||
defaultMessage: 'Metrics'
|
||||
})
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
// @ts-ignore
|
||||
import CustomPlot from 'x-pack/plugins/apm/public/components/shared/charts/CustomPlot';
|
||||
|
@ -22,7 +23,14 @@ export function CPUUsageChart({ data, hoverXHandlers }: Props) {
|
|||
return (
|
||||
<React.Fragment>
|
||||
<EuiTitle size="s">
|
||||
<span>CPU usage</span>
|
||||
<span>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.metrics.cpuUsageChartTitle',
|
||||
{
|
||||
defaultMessage: 'CPU usage'
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</EuiTitle>
|
||||
<CustomPlot
|
||||
{...hoverXHandlers}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
// @ts-ignore
|
||||
import CustomPlot from 'x-pack/plugins/apm/public/components/shared/charts/CustomPlot';
|
||||
|
@ -22,7 +23,14 @@ export function MemoryUsageChart({ data, hoverXHandlers }: Props) {
|
|||
return (
|
||||
<React.Fragment>
|
||||
<EuiTitle size="s">
|
||||
<span>Memory usage</span>
|
||||
<span>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.metrics.memoryUsageChartTitle',
|
||||
{
|
||||
defaultMessage: 'Memory usage'
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</EuiTitle>
|
||||
<CustomPlot
|
||||
{...hoverXHandlers}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { HistoryTabs } from 'x-pack/plugins/apm/public/components/shared/HistoryTabs';
|
||||
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
||||
|
@ -23,7 +24,9 @@ export class ServiceDetailTabs extends React.Component<TabsProps> {
|
|||
const { serviceName } = urlParams;
|
||||
const tabs = [
|
||||
{
|
||||
name: 'Transactions',
|
||||
name: i18n.translate('xpack.apm.serviceDetails.transactionsTabLabel', {
|
||||
defaultMessage: 'Transactions'
|
||||
}),
|
||||
path: `/${serviceName}/transactions/${transactionTypes[0]}`,
|
||||
routePath: `/${serviceName}/transactions/:transactionType?`,
|
||||
render: () => (
|
||||
|
@ -34,7 +37,9 @@ export class ServiceDetailTabs extends React.Component<TabsProps> {
|
|||
)
|
||||
},
|
||||
{
|
||||
name: 'Errors',
|
||||
name: i18n.translate('xpack.apm.serviceDetails.errorsTabLabel', {
|
||||
defaultMessage: 'Errors'
|
||||
}),
|
||||
path: `/${serviceName}/errors`,
|
||||
render: () => {
|
||||
return (
|
||||
|
@ -43,7 +48,9 @@ export class ServiceDetailTabs extends React.Component<TabsProps> {
|
|||
}
|
||||
},
|
||||
{
|
||||
name: 'Metrics',
|
||||
name: i18n.translate('xpack.apm.serviceDetails.metricsTabLabel', {
|
||||
defaultMessage: 'Metrics'
|
||||
}),
|
||||
path: `/${serviceName}/metrics`,
|
||||
render: () => <ServiceMetrics urlParams={urlParams} />
|
||||
}
|
||||
|
|
|
@ -18,6 +18,8 @@ import {
|
|||
EuiText,
|
||||
EuiTitle
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { Component } from 'react';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import {
|
||||
|
@ -98,11 +100,21 @@ export class MachineLearningFlyout extends Component<FlyoutProps, FlyoutState> {
|
|||
}
|
||||
|
||||
toastNotifications.addWarning({
|
||||
title: 'Job creation failed',
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreationFailedNotificationTitle',
|
||||
{
|
||||
defaultMessage: 'Job creation failed'
|
||||
}
|
||||
),
|
||||
text: (
|
||||
<p>
|
||||
Your current license may not allow for creating machine learning jobs,
|
||||
or this job may already exist.
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreationFailedNotificationText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Your current license may not allow for creating machine learning jobs, or this job may already exist.'
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
)
|
||||
});
|
||||
|
@ -113,18 +125,36 @@ export class MachineLearningFlyout extends Component<FlyoutProps, FlyoutState> {
|
|||
const { serviceName = 'unknown', transactionType } = urlParams;
|
||||
|
||||
toastNotifications.addSuccess({
|
||||
title: 'Job successfully created',
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationTitle',
|
||||
{
|
||||
defaultMessage: 'Job successfully created'
|
||||
}
|
||||
),
|
||||
text: (
|
||||
<p>
|
||||
The analysis is now running for {serviceName} ({transactionType}
|
||||
). It might take a while before results are added to the response
|
||||
times graph.{' '}
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText',
|
||||
{
|
||||
defaultMessage:
|
||||
'The analysis is now running for {serviceName} ({transactionType}). It might take a while before results are added to the response times graph.',
|
||||
values: {
|
||||
serviceName,
|
||||
transactionType: transactionType as string
|
||||
}
|
||||
}
|
||||
)}{' '}
|
||||
<ViewMLJob
|
||||
serviceName={serviceName}
|
||||
transactionType={transactionType}
|
||||
location={location}
|
||||
>
|
||||
View job
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText.viewJobLinkText',
|
||||
{
|
||||
defaultMessage: 'View job'
|
||||
}
|
||||
)}
|
||||
</ViewMLJob>
|
||||
</p>
|
||||
)
|
||||
|
@ -160,7 +190,14 @@ export class MachineLearningFlyout extends Component<FlyoutProps, FlyoutState> {
|
|||
<EuiFlyout onClose={onClose} size="s">
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle>
|
||||
<h2>Enable anomaly detection</h2>
|
||||
<h2>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.enableAnomalyDetectionTitle',
|
||||
{
|
||||
defaultMessage: 'Enable anomaly detection'
|
||||
}
|
||||
)}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
</EuiFlyoutHeader>
|
||||
|
@ -168,20 +205,38 @@ export class MachineLearningFlyout extends Component<FlyoutProps, FlyoutState> {
|
|||
{hasMLJob && (
|
||||
<div>
|
||||
<EuiCallOut
|
||||
title="Job already exists"
|
||||
title={i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsTitle',
|
||||
{
|
||||
defaultMessage: 'Job already exists'
|
||||
}
|
||||
)}
|
||||
color="success"
|
||||
iconType="check"
|
||||
>
|
||||
<p>
|
||||
There is currently a job running for {serviceName} (
|
||||
{transactionType}
|
||||
).{' '}
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'There is currently a job running for {serviceName} ({transactionType}).',
|
||||
values: {
|
||||
serviceName,
|
||||
transactionType: transactionType as string
|
||||
}
|
||||
}
|
||||
)}{' '}
|
||||
<ViewMLJob
|
||||
serviceName={serviceName}
|
||||
transactionType={transactionType}
|
||||
location={location}
|
||||
>
|
||||
View existing job
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription.viewJobLinkText',
|
||||
{
|
||||
defaultMessage: 'View existing job'
|
||||
}
|
||||
)}
|
||||
</ViewMLJob>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
|
@ -194,14 +249,25 @@ export class MachineLearningFlyout extends Component<FlyoutProps, FlyoutState> {
|
|||
<EuiCallOut
|
||||
title={
|
||||
<span>
|
||||
No APM index pattern available. To create a job,
|
||||
please import the APM index pattern via the{' '}
|
||||
<KibanaLink
|
||||
pathname={'/app/kibana'}
|
||||
hash={`/home/tutorial/apm`}
|
||||
>
|
||||
Setup Instructions
|
||||
</KibanaLink>
|
||||
<FormattedMessage
|
||||
id="xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.noPatternTitle"
|
||||
defaultMessage="No APM index pattern available. To create a job, please import the APM index pattern via the {setupInstructionLink}"
|
||||
values={{
|
||||
setupInstructionLink: (
|
||||
<KibanaLink
|
||||
pathname={'/app/kibana'}
|
||||
hash={`/home/tutorial/apm`}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.noPatternTitle.setupInstructionLinkText',
|
||||
{
|
||||
defaultMessage: 'Setup Instructions'
|
||||
}
|
||||
)}
|
||||
</KibanaLink>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
color="warning"
|
||||
|
@ -213,24 +279,53 @@ export class MachineLearningFlyout extends Component<FlyoutProps, FlyoutState> {
|
|||
|
||||
<EuiText>
|
||||
<p>
|
||||
Here you can create a machine learning job to calculate
|
||||
anomaly scores on durations for APM transactions within the{' '}
|
||||
{serviceName} service. Once enabled,{' '}
|
||||
<b>the transaction duration graph</b> will show the expected
|
||||
bounds and annotate the graph once the anomaly score is
|
||||
>=75.
|
||||
<FormattedMessage
|
||||
id="xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createMLJobDescription"
|
||||
defaultMessage="Here you can create a machine learning job to calculate anomaly scores on durations for APM transactions
|
||||
within the {serviceName} service. Once enabled, {transactionDurationGraphText} will show the expected bounds and annotate
|
||||
the graph once the anomaly score is >=75."
|
||||
values={{
|
||||
serviceName,
|
||||
transactionDurationGraphText: (
|
||||
<b>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createMLJobDescription.transactionDurationGraphText',
|
||||
{
|
||||
defaultMessage: 'the transaction duration graph'
|
||||
}
|
||||
)}
|
||||
</b>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
Jobs can be created for each service + transaction type
|
||||
combination. Once a job is created, you can manage it and
|
||||
see more details in the{' '}
|
||||
<KibanaLink pathname={'/app/ml'}>
|
||||
Machine Learning jobs management page
|
||||
</KibanaLink>
|
||||
.{' '}
|
||||
<FormattedMessage
|
||||
id="xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription"
|
||||
defaultMessage="Jobs can be created for each service + transaction type combination.
|
||||
Once a job is created, you can manage it and see more details in the {mlJobsPageLink}."
|
||||
values={{
|
||||
mlJobsPageLink: (
|
||||
<KibanaLink pathname={'/app/ml'}>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription.mlJobsPageLinkText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Machine Learning jobs management page'
|
||||
}
|
||||
)}
|
||||
</KibanaLink>
|
||||
)
|
||||
}}
|
||||
/>{' '}
|
||||
<em>
|
||||
Note: It might take a few minutes for the job to begin
|
||||
calculating results.
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription.noteText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Note: It might take a few minutes for the job to begin calculating results.'
|
||||
}
|
||||
)}
|
||||
</em>
|
||||
</p>
|
||||
</EuiText>
|
||||
|
@ -263,7 +358,12 @@ export class MachineLearningFlyout extends Component<FlyoutProps, FlyoutState> {
|
|||
fill
|
||||
disabled={isLoading || hasMLJob || !hasIndexPattern}
|
||||
>
|
||||
Create new job
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createNewJobButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Create new job'
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
EuiText,
|
||||
EuiToolTip
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { MLJobApiResponse } from 'x-pack/plugins/apm/public/services/rest/ml';
|
||||
|
||||
|
@ -31,7 +32,14 @@ export const TransactionSelect: React.SFC<TransactionSelectProps> = ({
|
|||
onChange
|
||||
}) => {
|
||||
return (
|
||||
<EuiFormRow label="Select a transaction type for this job">
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.selectTransactionTypeLabel',
|
||||
{
|
||||
defaultMessage: 'Select a transaction type for this job'
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiSuperSelect
|
||||
valueOfSelected={selected}
|
||||
onChange={onChange}
|
||||
|
@ -49,7 +57,14 @@ export const TransactionSelect: React.SFC<TransactionSelectProps> = ({
|
|||
job => job.jobId && job.jobId.includes(type)
|
||||
)
|
||||
) ? (
|
||||
<EuiToolTip content="ML job exists for this type">
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.existedJobTooltip',
|
||||
{
|
||||
defaultMessage: 'ML job exists for this type'
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiIcon type="machineLearningApp" />
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
|
|
|
@ -24,6 +24,8 @@ import {
|
|||
EuiText,
|
||||
EuiTitle
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { memoize, padLeft, range } from 'lodash';
|
||||
import moment from 'moment-timezone';
|
||||
import React, { Component } from 'react';
|
||||
|
@ -210,24 +212,57 @@ export class WatcherFlyout extends Component<
|
|||
|
||||
public addErrorToast = () => {
|
||||
toastNotifications.addWarning({
|
||||
title: 'Watch creation failed',
|
||||
text: <p>Make sure your user has permission to create watches.</p>
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreationFailedNotificationTitle',
|
||||
{
|
||||
defaultMessage: 'Watch creation failed'
|
||||
}
|
||||
),
|
||||
text: (
|
||||
<p>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreationFailedNotificationText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Make sure your user has permission to create watches.'
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
public addSuccessToast = (id: string) => {
|
||||
toastNotifications.addSuccess({
|
||||
title: 'New watch created!',
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreatedNotificationTitle',
|
||||
{
|
||||
defaultMessage: 'New watch created!'
|
||||
}
|
||||
),
|
||||
text: (
|
||||
<p>
|
||||
The watch is now ready and will send error reports for{' '}
|
||||
{this.props.urlParams.serviceName}.{' '}
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreatedNotificationText',
|
||||
{
|
||||
defaultMessage:
|
||||
'The watch is now ready and will send error reports for {serviceName}.',
|
||||
values: {
|
||||
serviceName: this.props.urlParams.serviceName as string
|
||||
}
|
||||
}
|
||||
)}{' '}
|
||||
<UnconnectedKibanaLink
|
||||
location={this.props.location}
|
||||
pathname={'/app/kibana'}
|
||||
hash={`/management/elasticsearch/watcher/watches/watch/${id}`}
|
||||
>
|
||||
View watch.
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreatedNotificationText.viewWatchLinkText',
|
||||
{
|
||||
defaultMessage: 'View watch'
|
||||
}
|
||||
)}
|
||||
</UnconnectedKibanaLink>
|
||||
</p>
|
||||
)
|
||||
|
@ -259,20 +294,48 @@ export class WatcherFlyout extends Component<
|
|||
const flyoutBody = (
|
||||
<EuiText>
|
||||
<p>
|
||||
This form will assist in creating a Watch that can notify you of error
|
||||
occurrences from this service. To learn more about Watcher, please
|
||||
read our{' '}
|
||||
<EuiLink target="_blank" href={XPACK_DOCS.xpackWatcher}>
|
||||
documentation
|
||||
</EuiLink>
|
||||
.
|
||||
<FormattedMessage
|
||||
id="xpack.apm.serviceDetails.enableErrorReportsPanel.formDescription"
|
||||
defaultMessage="This form will assist in creating a Watch that can notify you of error occurrences from this service.
|
||||
To learn more about Watcher, please read our {documentationLink}."
|
||||
values={{
|
||||
documentationLink: (
|
||||
<EuiLink target="_blank" href={XPACK_DOCS.xpackWatcher}>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.formDescription.documentationLinkText',
|
||||
{
|
||||
defaultMessage: 'documentation'
|
||||
}
|
||||
)}
|
||||
</EuiLink>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<EuiForm>
|
||||
<h3>Condition</h3>
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.conditionTitle',
|
||||
{
|
||||
defaultMessage: 'Condition'
|
||||
}
|
||||
)}
|
||||
</h3>
|
||||
<EuiFormRow
|
||||
label="Occurrences threshold per error group"
|
||||
helpText="Threshold to be met for error group to be included in report."
|
||||
label={i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.occurrencesThresholdLabel',
|
||||
{
|
||||
defaultMessage: 'Occurrences threshold per error group'
|
||||
}
|
||||
)}
|
||||
helpText={i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.occurrencesThresholdHelpText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Threshold to be met for error group to be included in report.'
|
||||
}
|
||||
)}
|
||||
compressed
|
||||
>
|
||||
<EuiFieldNumber
|
||||
|
@ -283,16 +346,33 @@ export class WatcherFlyout extends Component<
|
|||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<h3>Trigger schedule</h3>
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.triggerScheduleTitle',
|
||||
{
|
||||
defaultMessage: 'Trigger schedule'
|
||||
}
|
||||
)}
|
||||
</h3>
|
||||
|
||||
<p>
|
||||
Choose the time interval for the report, when the threshold is
|
||||
exceeded.
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.triggerScheduleDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Choose the time interval for the report, when the threshold is exceeded.'
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
|
||||
<EuiRadio
|
||||
id="daily"
|
||||
label="Daily report"
|
||||
label={i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.dailyReportRadioButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Daily report'
|
||||
}
|
||||
)}
|
||||
onChange={() => this.onChangeSchedule('daily')}
|
||||
checked={this.state.schedule === 'daily'}
|
||||
/>
|
||||
|
@ -300,7 +380,14 @@ export class WatcherFlyout extends Component<
|
|||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFormRow
|
||||
helpText={`The daily report will be sent at ${dailyTimeFormatted} / ${dailyTime12HourFormatted}.`}
|
||||
helpText={i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.dailyReportHelpText',
|
||||
{
|
||||
defaultMessage:
|
||||
'The daily report will be sent at {dailyTimeFormatted} / {dailyTime12HourFormatted}.',
|
||||
values: { dailyTimeFormatted, dailyTime12HourFormatted }
|
||||
}
|
||||
)}
|
||||
compressed
|
||||
>
|
||||
<EuiSelect
|
||||
|
@ -313,7 +400,12 @@ export class WatcherFlyout extends Component<
|
|||
|
||||
<EuiRadio
|
||||
id="interval"
|
||||
label="Interval"
|
||||
label={i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.intervalRadioButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Interval'
|
||||
}
|
||||
)}
|
||||
onChange={() => this.onChangeSchedule('interval')}
|
||||
checked={this.state.schedule === 'interval'}
|
||||
/>
|
||||
|
@ -324,7 +416,12 @@ export class WatcherFlyout extends Component<
|
|||
<EuiFlexItem grow={false}>
|
||||
<SmallInput>
|
||||
<EuiFormRow
|
||||
helpText="Time interval between reports."
|
||||
helpText={i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.intervalHelpText',
|
||||
{
|
||||
defaultMessage: 'Time interval between reports.'
|
||||
}
|
||||
)}
|
||||
compressed
|
||||
>
|
||||
<EuiFieldNumber
|
||||
|
@ -345,11 +442,21 @@ export class WatcherFlyout extends Component<
|
|||
options={[
|
||||
{
|
||||
value: 'm',
|
||||
text: 'mins'
|
||||
text: i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.intervalUnit.minsLabel',
|
||||
{
|
||||
defaultMessage: 'mins'
|
||||
}
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'h',
|
||||
text: 'hrs'
|
||||
text: i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.intervalUnit.hrsLabel',
|
||||
{
|
||||
defaultMessage: 'hrs'
|
||||
}
|
||||
)
|
||||
}
|
||||
]}
|
||||
disabled={this.state.schedule !== 'interval'}
|
||||
|
@ -358,13 +465,30 @@ export class WatcherFlyout extends Component<
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<h3>Actions</h3>
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.actionsTitle',
|
||||
{
|
||||
defaultMessage: 'Actions'
|
||||
}
|
||||
)}
|
||||
</h3>
|
||||
<p>
|
||||
Reports can be sent by email or posted to a Slack channel. Each
|
||||
report will include the top 10 errors sorted by occurrence.
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.actionsDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Reports can be sent by email or posted to a Slack channel. Each report will include the top 10 errors sorted by occurrence.'
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
<EuiSwitch
|
||||
label="Send email"
|
||||
label={i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.sendEmailLabel',
|
||||
{
|
||||
defaultMessage: 'Send email'
|
||||
}
|
||||
)}
|
||||
checked={this.state.actions.email}
|
||||
onChange={() => this.onChangeAction('email')}
|
||||
/>
|
||||
|
@ -372,15 +496,31 @@ export class WatcherFlyout extends Component<
|
|||
<EuiSpacer size="m" />
|
||||
{this.state.actions.email && (
|
||||
<EuiFormRow
|
||||
label="Recipients (separated with comma)"
|
||||
label={i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.recipientsLabel',
|
||||
{
|
||||
defaultMessage: 'Recipients (separated with comma)'
|
||||
}
|
||||
)}
|
||||
compressed
|
||||
helpText={
|
||||
<span>
|
||||
If you have not configured email, please see the{' '}
|
||||
<EuiLink target="_blank" href={XPACK_DOCS.xpackEmails}>
|
||||
documentation
|
||||
</EuiLink>
|
||||
.
|
||||
<FormattedMessage
|
||||
id="xpack.apm.serviceDetails.enableErrorReportsPanel.recipientsHelpText"
|
||||
defaultMessage="If you have not configured email, please see the {documentationLink}."
|
||||
values={{
|
||||
documentationLink: (
|
||||
<EuiLink target="_blank" href={XPACK_DOCS.xpackEmails}>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.recipientsHelpText.documentationLinkText',
|
||||
{
|
||||
defaultMessage: 'documentation'
|
||||
}
|
||||
)}
|
||||
</EuiLink>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
|
@ -393,7 +533,12 @@ export class WatcherFlyout extends Component<
|
|||
)}
|
||||
|
||||
<EuiSwitch
|
||||
label="Send Slack notification"
|
||||
label={i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.sendSlackNotificationLabel',
|
||||
{
|
||||
defaultMessage: 'Send Slack notification'
|
||||
}
|
||||
)}
|
||||
checked={this.state.actions.slack}
|
||||
onChange={() => this.onChangeAction('slack')}
|
||||
/>
|
||||
|
@ -401,18 +546,34 @@ export class WatcherFlyout extends Component<
|
|||
|
||||
{this.state.actions.slack && (
|
||||
<EuiFormRow
|
||||
label="Slack Webhook URL"
|
||||
label={i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.slackWebhookURLLabel',
|
||||
{
|
||||
defaultMessage: 'Slack Webhook URL'
|
||||
}
|
||||
)}
|
||||
compressed
|
||||
helpText={
|
||||
<span>
|
||||
To get a Slack webhook, please see the{' '}
|
||||
<EuiLink
|
||||
target="_blank"
|
||||
href="https://get.slack.help/hc/en-us/articles/115005265063-Incoming-WebHooks-for-Slack"
|
||||
>
|
||||
documentation
|
||||
</EuiLink>
|
||||
.
|
||||
<FormattedMessage
|
||||
id="xpack.apm.serviceDetails.enableErrorReportsPanel.slackWebhookURLHelpText"
|
||||
defaultMessage="To get a Slack webhook, please see the {documentationLink}."
|
||||
values={{
|
||||
documentationLink: (
|
||||
<EuiLink
|
||||
target="_blank"
|
||||
href="https://get.slack.help/hc/en-us/articles/115005265063-Incoming-WebHooks-for-Slack"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.slackWebhookURLHelpText.documentationLinkText',
|
||||
{
|
||||
defaultMessage: 'documentation'
|
||||
}
|
||||
)}
|
||||
</EuiLink>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
|
@ -431,7 +592,14 @@ export class WatcherFlyout extends Component<
|
|||
<EuiFlyout onClose={this.props.onClose} size="s">
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle>
|
||||
<h2>Enable error reports</h2>
|
||||
<h2>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.enableErrorReportsTitle',
|
||||
{
|
||||
defaultMessage: 'Enable error reports'
|
||||
}
|
||||
)}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>{flyoutBody}</EuiFlyoutBody>
|
||||
|
@ -441,7 +609,12 @@ export class WatcherFlyout extends Component<
|
|||
fill
|
||||
disabled={!this.state.actions.email && !this.state.actions.slack}
|
||||
>
|
||||
Create watch
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.createWatchButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Create watch'
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
EuiContextMenuPanelItemDescriptor,
|
||||
EuiPopover
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { memoize } from 'lodash';
|
||||
import React from 'react';
|
||||
import chrome from 'ui/chrome';
|
||||
|
@ -46,16 +47,31 @@ export class ServiceIntegrationsView extends React.Component<
|
|||
public getMLPanelItems = () => {
|
||||
return [
|
||||
{
|
||||
name: 'Enable ML anomaly detection',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.serviceDetails.integrationsMenu.enableMLAnomalyDetectionButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Enable ML anomaly detection'
|
||||
}
|
||||
),
|
||||
icon: 'machineLearningApp',
|
||||
toolTipContent: 'Set up a machine learning job for this service',
|
||||
toolTipContent: i18n.translate(
|
||||
'xpack.apm.serviceDetails.integrationsMenu.enableMLAnomalyDetectionButtonTooltip',
|
||||
{
|
||||
defaultMessage: 'Set up a machine learning job for this service'
|
||||
}
|
||||
),
|
||||
onClick: () => {
|
||||
this.closePopover();
|
||||
this.openFlyout('ML');
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'View existing ML jobs',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.serviceDetails.integrationsMenu.viewMLJobsButtonLabel',
|
||||
{
|
||||
defaultMessage: 'View existing ML jobs'
|
||||
}
|
||||
),
|
||||
icon: 'machineLearningApp',
|
||||
href: chrome.addBasePath('/app/ml'),
|
||||
target: '_blank',
|
||||
|
@ -67,7 +83,12 @@ export class ServiceIntegrationsView extends React.Component<
|
|||
public getWatcherPanelItems = () => {
|
||||
return [
|
||||
{
|
||||
name: 'Enable watcher error reports',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.serviceDetails.integrationsMenu.enableWatcherErrorReportsButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Enable watcher error reports'
|
||||
}
|
||||
),
|
||||
icon: 'watchesApp',
|
||||
onClick: () => {
|
||||
this.closePopover();
|
||||
|
@ -75,7 +96,12 @@ export class ServiceIntegrationsView extends React.Component<
|
|||
}
|
||||
},
|
||||
{
|
||||
name: 'View existing watches',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.serviceDetails.integrationsMenu.viewWatchesButtonLabel',
|
||||
{
|
||||
defaultMessage: 'View existing watches'
|
||||
}
|
||||
),
|
||||
icon: 'watchesApp',
|
||||
href: chrome.addBasePath(
|
||||
'/app/kibana#/management/elasticsearch/watcher'
|
||||
|
@ -108,7 +134,12 @@ export class ServiceIntegrationsView extends React.Component<
|
|||
iconSide="right"
|
||||
onClick={this.openPopover}
|
||||
>
|
||||
Integrations
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.integrationsMenu.integrationsButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Integrations'
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
);
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
EuiSpacer,
|
||||
EuiTitle
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
// @ts-ignore
|
||||
import Distribution from 'x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution';
|
||||
|
@ -54,7 +55,14 @@ export const ServiceMetrics: React.SFC<ServiceMetricsProps> = ({
|
|||
distribution={data}
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<span>Error occurrences</span>
|
||||
<span>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.metrics.errorOccurrencesChartTitle',
|
||||
{
|
||||
defaultMessage: 'Error occurrences'
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</EuiTitle>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { IServiceListItem } from 'x-pack/plugins/apm/server/lib/services/get_services';
|
||||
|
@ -29,7 +30,12 @@ function formatNumber(value: number) {
|
|||
}
|
||||
|
||||
function formatString(value?: string | null) {
|
||||
return value || 'N/A';
|
||||
return (
|
||||
value ||
|
||||
i18n.translate('xpack.apm.servicesTable.notAvailableLabel', {
|
||||
defaultMessage: 'N/A'
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const AppLink = styled(RelativeLink)`
|
||||
|
@ -40,7 +46,9 @@ const AppLink = styled(RelativeLink)`
|
|||
export const SERVICE_COLUMNS = [
|
||||
{
|
||||
field: 'serviceName',
|
||||
name: 'Name',
|
||||
name: i18n.translate('xpack.apm.servicesTable.nameColumnLabel', {
|
||||
defaultMessage: 'Name'
|
||||
}),
|
||||
width: '50%',
|
||||
sortable: true,
|
||||
render: (serviceName: string) => (
|
||||
|
@ -53,30 +61,53 @@ export const SERVICE_COLUMNS = [
|
|||
},
|
||||
{
|
||||
field: 'agentName',
|
||||
name: 'Agent',
|
||||
name: i18n.translate('xpack.apm.servicesTable.agentColumnLabel', {
|
||||
defaultMessage: 'Agent'
|
||||
}),
|
||||
sortable: true,
|
||||
render: (agentName: string) => formatString(agentName)
|
||||
},
|
||||
{
|
||||
field: 'avgResponseTime',
|
||||
name: 'Avg. response time',
|
||||
name: i18n.translate('xpack.apm.servicesTable.avgResponseTimeColumnLabel', {
|
||||
defaultMessage: 'Avg. response time'
|
||||
}),
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: (value: number) => asMillis(value)
|
||||
},
|
||||
{
|
||||
field: 'transactionsPerMinute',
|
||||
name: 'Trans. per minute',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.servicesTable.transactionsPerMinuteColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Trans. per minute'
|
||||
}
|
||||
),
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: (value: number) => `${formatNumber(value)} tpm`
|
||||
render: (value: number) =>
|
||||
`${formatNumber(value)} ${i18n.translate(
|
||||
'xpack.apm.servicesTable.transactionsPerMinuteUnitLabel',
|
||||
{
|
||||
defaultMessage: 'tpm'
|
||||
}
|
||||
)}`
|
||||
},
|
||||
{
|
||||
field: 'errorsPerMinute',
|
||||
name: 'Errors per minute',
|
||||
name: i18n.translate('xpack.apm.servicesTable.errorsPerMinuteColumnLabel', {
|
||||
defaultMessage: 'Errors per minute'
|
||||
}),
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: (value: number) => `${formatNumber(value)} err.`
|
||||
render: (value: number) =>
|
||||
`${formatNumber(value)} ${i18n.translate(
|
||||
'xpack.apm.servicesTable.errorsPerMinuteUnitLabel',
|
||||
{
|
||||
defaultMessage: 'err.'
|
||||
}
|
||||
)}`
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { Component } from 'react';
|
||||
import { RRRRenderResponse } from 'react-redux-request';
|
||||
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
||||
|
@ -43,8 +44,12 @@ export class ServiceOverview extends Component<Props, State> {
|
|||
<EmptyMessage
|
||||
heading={
|
||||
historicalDataFound
|
||||
? 'No services were found'
|
||||
: "Looks like you don't have any services with APM installed. Let's add some!"
|
||||
? i18n.translate('xpack.apm.servicesTable.notFoundLabel', {
|
||||
defaultMessage: 'No services were found'
|
||||
})
|
||||
: i18n.translate('xpack.apm.servicesTable.noServicesLabel', {
|
||||
defaultMessage: `Looks like you don't have any services with APM installed. Let's add some!`
|
||||
})
|
||||
}
|
||||
subheading={
|
||||
!historicalDataFound ? <SetupInstructionsLink buttonFill /> : null
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { ITransactionGroup } from 'x-pack/plugins/apm/server/lib/transaction_groups/transform';
|
||||
|
@ -29,7 +30,9 @@ interface Props {
|
|||
const traceListColumns: ITableColumn[] = [
|
||||
{
|
||||
field: 'name',
|
||||
name: 'Name',
|
||||
name: i18n.translate('xpack.apm.tracesTable.nameColumnLabel', {
|
||||
defaultMessage: 'Name'
|
||||
}),
|
||||
width: '40%',
|
||||
sortable: true,
|
||||
render: (name, group: ITransactionGroup) => (
|
||||
|
@ -42,26 +45,43 @@ const traceListColumns: ITableColumn[] = [
|
|||
},
|
||||
{
|
||||
field: 'sample.context.service.name',
|
||||
name: 'Originating service',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.tracesTable.originatingServiceColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Originating service'
|
||||
}
|
||||
),
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
field: 'averageResponseTime',
|
||||
name: 'Avg. response time',
|
||||
name: i18n.translate('xpack.apm.tracesTable.avgResponseTimeColumnLabel', {
|
||||
defaultMessage: 'Avg. response time'
|
||||
}),
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: (value: number) => asMillis(value)
|
||||
},
|
||||
{
|
||||
field: 'transactionsPerMinute',
|
||||
name: 'Traces per minute',
|
||||
name: i18n.translate('xpack.apm.tracesTable.tracesPerMinuteColumnLabel', {
|
||||
defaultMessage: 'Traces per minute'
|
||||
}),
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: (value: number) => `${value.toLocaleString()} tpm`
|
||||
render: (value: number) =>
|
||||
`${value.toLocaleString()} ${i18n.translate(
|
||||
'xpack.apm.tracesTable.tracesPerMinuteUnitLabel',
|
||||
{
|
||||
defaultMessage: 'tpm'
|
||||
}
|
||||
)}`
|
||||
},
|
||||
{
|
||||
field: 'impact',
|
||||
name: 'Impact',
|
||||
name: i18n.translate('xpack.apm.tracesTable.impactColumnLabel', {
|
||||
defaultMessage: 'Impact'
|
||||
}),
|
||||
width: '20%',
|
||||
align: 'right',
|
||||
sortable: true,
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { RRRRenderResponse } from 'react-redux-request';
|
||||
import { TraceListAPIResponse } from 'x-pack/plugins/apm/server/lib/traces/get_top_traces';
|
||||
|
@ -27,7 +28,11 @@ export function TraceOverview(props: Props) {
|
|||
items={data}
|
||||
isLoading={status === 'LOADING'}
|
||||
noItemsMessage={
|
||||
<EmptyMessage heading="No traces found for this query" />
|
||||
<EmptyMessage
|
||||
heading={i18n.translate('xpack.apm.tracesTable.notFoundLabel', {
|
||||
defaultMessage: 'No traces found for this query'
|
||||
})}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { units, borderRadius, px, colors } from '../../../../style/variables';
|
||||
|
||||
const ImpactBarBackground = styled.div`
|
||||
height: ${px(units.minus)};
|
||||
border-radius: ${borderRadius};
|
||||
background: ${colors.gray4};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const ImpactBar = styled.div`
|
||||
height: ${px(units.minus)};
|
||||
background: ${colors.blue2};
|
||||
border-radius: ${borderRadius};
|
||||
`;
|
||||
|
||||
function ImpactSparkline({ impact }) {
|
||||
if (!impact && impact !== 0) {
|
||||
return <div>N/A</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ImpactBarBackground>
|
||||
<ImpactBar style={{ width: `${impact}%` }} />
|
||||
</ImpactBarBackground>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImpactSparkline;
|
|
@ -1,36 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { units, px } from '../../../../style/variables';
|
||||
import { EuiIcon } from '@elastic/eui';
|
||||
import { Tooltip } from 'pivotal-ui/react/tooltip';
|
||||
import { OverlayTrigger } from 'pivotal-ui/react/overlay-trigger';
|
||||
|
||||
const TooltipWrapper = styled.div`
|
||||
margin-left: ${px(units.half)};
|
||||
`;
|
||||
|
||||
const ImpactTooltip = () => (
|
||||
<TooltipWrapper>
|
||||
<OverlayTrigger
|
||||
placement="top"
|
||||
trigger="hover"
|
||||
overlay={
|
||||
<Tooltip>
|
||||
Impact shows the most used and
|
||||
<br />
|
||||
slowest endpoints in your service.
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<EuiIcon type="questionInCircle" />
|
||||
</OverlayTrigger>
|
||||
</TooltipWrapper>
|
||||
);
|
||||
|
||||
export default ImpactTooltip;
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import TooltipOverlay from '../../../shared/TooltipOverlay';
|
||||
|
@ -19,10 +20,18 @@ const TransactionNameLink = styled(RelativeLink)`
|
|||
`;
|
||||
|
||||
export default function TransactionList({ items, serviceName, ...rest }) {
|
||||
const notAvailableLabel = i18n.translate(
|
||||
'xpack.apm.transactionsTable.notAvailableLabel',
|
||||
{
|
||||
defaultMessage: 'N/A'
|
||||
}
|
||||
);
|
||||
const columns = [
|
||||
{
|
||||
field: 'name',
|
||||
name: 'Name',
|
||||
name: i18n.translate('xpack.apm.transactionsTable.nameColumnLabel', {
|
||||
defaultMessage: 'Name'
|
||||
}),
|
||||
width: '50%',
|
||||
sortable: true,
|
||||
render: (transactionName, data) => {
|
||||
|
@ -33,9 +42,9 @@ export default function TransactionList({ items, serviceName, ...rest }) {
|
|||
const transactionPath = `/${serviceName}/transactions/${encodedType}/${encodedName}`;
|
||||
|
||||
return (
|
||||
<TooltipOverlay content={transactionName || 'N/A'}>
|
||||
<TooltipOverlay content={transactionName || notAvailableLabel}>
|
||||
<TransactionNameLink path={transactionPath}>
|
||||
{transactionName || 'N/A'}
|
||||
{transactionName || notAvailableLabel}
|
||||
</TransactionNameLink>
|
||||
</TooltipOverlay>
|
||||
);
|
||||
|
@ -43,28 +52,51 @@ export default function TransactionList({ items, serviceName, ...rest }) {
|
|||
},
|
||||
{
|
||||
field: 'averageResponseTime',
|
||||
name: 'Avg. duration',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.transactionsTable.avgDurationColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Avg. duration'
|
||||
}
|
||||
),
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: value => asMillis(value)
|
||||
},
|
||||
{
|
||||
field: 'p95',
|
||||
name: '95th percentile',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.transactionsTable.95thPercentileColumnLabel',
|
||||
{
|
||||
defaultMessage: '95th percentile'
|
||||
}
|
||||
),
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: value => asMillis(value)
|
||||
},
|
||||
{
|
||||
field: 'transactionsPerMinute',
|
||||
name: 'Trans. per minute',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.transactionsTable.transactionsPerMinuteColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Trans. per minute'
|
||||
}
|
||||
),
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: value => `${asDecimal(value)} tpm`
|
||||
render: value =>
|
||||
`${asDecimal(value)} ${i18n.translate(
|
||||
'xpack.apm.transactionsTable.transactionsPerMinuteUnitLabel',
|
||||
{
|
||||
defaultMessage: 'tpm'
|
||||
}
|
||||
)}`
|
||||
},
|
||||
{
|
||||
field: 'impact',
|
||||
name: 'Impact',
|
||||
name: i18n.translate('xpack.apm.transactionsTable.impactColumnLabel', {
|
||||
defaultMessage: 'Impact'
|
||||
}),
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: value => <ImpactBar value={value} />
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { EuiFormRow, EuiSelect, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { TransactionCharts } from 'x-pack/plugins/apm/public/components/shared/charts/TransactionCharts';
|
||||
|
@ -48,7 +49,14 @@ export class TransactionOverviewView extends React.Component<
|
|||
return (
|
||||
<React.Fragment>
|
||||
{serviceTransactionTypes.length > 1 ? (
|
||||
<EuiFormRow label="Filter by type">
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.apm.transactionsTable.filterByTypeLabel',
|
||||
{
|
||||
defaultMessage: 'Filter by type'
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiSelect
|
||||
options={serviceTransactionTypes.map(type => ({
|
||||
text: `${type}`,
|
||||
|
|
|
@ -17,7 +17,7 @@ function getDiscoverQuery(error: APMError, kuery?: string) {
|
|||
const groupId = error.error.grouping_key;
|
||||
let query = `${SERVICE_NAME}:"${serviceName}" AND ${ERROR_GROUP_ID}:"${groupId}"`;
|
||||
if (kuery) {
|
||||
query = ` AND ${kuery}`;
|
||||
query += ` AND ${kuery}`;
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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, ShallowWrapper } from 'enzyme';
|
||||
import 'jest-styled-components';
|
||||
import React from 'react';
|
||||
import { APMError } from 'x-pack/plugins/apm/typings/es_schemas/Error';
|
||||
import { DiscoverErrorButton } from '../DiscoverErrorButton';
|
||||
|
||||
describe('DiscoverErrorButton without kuery', () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
beforeEach(() => {
|
||||
const error = {
|
||||
context: { service: { name: 'myServiceName' } },
|
||||
error: { grouping_key: 'myGroupingKey' }
|
||||
} as APMError;
|
||||
|
||||
wrapper = shallow(<DiscoverErrorButton error={error} />);
|
||||
});
|
||||
|
||||
it('should have correct query', () => {
|
||||
const queryProp = wrapper.prop('query') as any;
|
||||
expect(queryProp._a.query.query).toEqual(
|
||||
'context.service.name:"myServiceName" AND error.grouping_key:"myGroupingKey"'
|
||||
);
|
||||
});
|
||||
|
||||
it('should match snapshot', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DiscoverErrorButton with kuery', () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
beforeEach(() => {
|
||||
const error = {
|
||||
context: { service: { name: 'myServiceName' } },
|
||||
error: { grouping_key: 'myGroupingKey' }
|
||||
} as APMError;
|
||||
|
||||
const kuery = 'transaction.sampled: true';
|
||||
|
||||
wrapper = shallow(<DiscoverErrorButton error={error} kuery={kuery} />);
|
||||
});
|
||||
|
||||
it('should have correct query', () => {
|
||||
const queryProp = wrapper.prop('query') as any;
|
||||
expect(queryProp._a.query.query).toEqual(
|
||||
'context.service.name:"myServiceName" AND error.grouping_key:"myGroupingKey" AND transaction.sampled: true'
|
||||
);
|
||||
});
|
||||
|
||||
it('should match snapshot', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DiscoverErrorButton with kuery should match snapshot 1`] = `
|
||||
<DiscoverButton
|
||||
query={
|
||||
Object {
|
||||
"_a": Object {
|
||||
"interval": "auto",
|
||||
"query": Object {
|
||||
"language": "lucene",
|
||||
"query": "context.service.name:\\"myServiceName\\" AND error.grouping_key:\\"myGroupingKey\\" AND transaction.sampled: true",
|
||||
},
|
||||
"sort": Object {
|
||||
"@timestamp": "desc",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`DiscoverErrorButton without kuery should match snapshot 1`] = `
|
||||
<DiscoverButton
|
||||
query={
|
||||
Object {
|
||||
"_a": Object {
|
||||
"interval": "auto",
|
||||
"query": Object {
|
||||
"language": "lucene",
|
||||
"query": "context.service.name:\\"myServiceName\\" AND error.grouping_key:\\"myGroupingKey\\"",
|
||||
},
|
||||
"sort": Object {
|
||||
"@timestamp": "desc",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { EuiEmptyPrompt, EuiEmptyPromptProps } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
|
@ -14,14 +15,18 @@ interface Props {
|
|||
}
|
||||
|
||||
const EmptyMessage: React.SFC<Props> = ({
|
||||
heading = 'No data found.',
|
||||
subheading = 'Try another time range or reset the search filter.',
|
||||
heading = i18n.translate('xpack.apm.emptyMessage.noDataFoundLabel', {
|
||||
defaultMessage: 'No data found.'
|
||||
}),
|
||||
subheading = i18n.translate('xpack.apm.emptyMessage.noDataFoundDescription', {
|
||||
defaultMessage: 'Try another time range or reset the search filter.'
|
||||
}),
|
||||
hideSubheading = false
|
||||
}) => {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
titleSize="s"
|
||||
title={<div>{heading || 'No data found.'}</div>}
|
||||
title={<div>{heading}</div>}
|
||||
body={!hideSubheading && subheading}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -9,6 +9,7 @@ import PropTypes from 'prop-types';
|
|||
import Suggestions from './Suggestions';
|
||||
import ClickOutside from './ClickOutside';
|
||||
import { EuiFieldSearch, EuiProgress } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
const KEY_CODES = {
|
||||
LEFT: 37,
|
||||
|
@ -166,7 +167,17 @@ export class Typeahead extends Component {
|
|||
style={{
|
||||
backgroundImage: 'none'
|
||||
}}
|
||||
placeholder="Search transactions and errors... (E.g. transaction.duration.us > 300000 AND context.response.status_code >= 400)"
|
||||
placeholder={i18n.translate(
|
||||
'xpack.apm.kueryBar.searchPlaceholder',
|
||||
{
|
||||
defaultMessage:
|
||||
'Search transactions and errors… (E.g. {queryExample})',
|
||||
values: {
|
||||
queryExample:
|
||||
'transaction.duration.us > 300000 AND context.response.status_code >= 400'
|
||||
}
|
||||
}
|
||||
)}
|
||||
inputRef={node => {
|
||||
if (node) {
|
||||
this.inputRef = node;
|
||||
|
|
|
@ -25,6 +25,8 @@ import {
|
|||
import styled from 'styled-components';
|
||||
|
||||
import { getBoolFilter } from './get_bool_filter';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
const Container = styled.div`
|
||||
margin-bottom: 10px;
|
||||
|
@ -112,16 +114,24 @@ class KueryBarView extends Component {
|
|||
style={{ display: 'inline-block', marginTop: '10px' }}
|
||||
title={
|
||||
<div>
|
||||
There's no APM index pattern with the title "
|
||||
{apmIndexPatternTitle}
|
||||
" available. To use the Query bar, please choose to import
|
||||
the APM index pattern via the{' '}
|
||||
<KibanaLink
|
||||
pathname={'/app/kibana'}
|
||||
hash={`/home/tutorial/apm`}
|
||||
>
|
||||
Setup Instructions.
|
||||
</KibanaLink>
|
||||
<FormattedMessage
|
||||
id="xpack.apm.kueryBar.indexPatternMissingWarningMessage"
|
||||
defaultMessage="There's no APM index pattern with the title {apmIndexPatternTitle} available. To use the Query bar, please choose to import the APM index pattern via the {setupInstructionsLink}."
|
||||
values={{
|
||||
apmIndexPatternTitle: `"${apmIndexPatternTitle}"`,
|
||||
setupInstructionsLink: (
|
||||
<KibanaLink
|
||||
pathname={'/app/kibana'}
|
||||
hash={`/home/tutorial/apm`}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.kueryBar.setupInstructionsLinkLabel',
|
||||
{ defaultMessage: 'Setup Instructions' }
|
||||
)}
|
||||
</KibanaLink>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
color="warning"
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { KibanaLink } from '../../utils/url';
|
||||
|
||||
|
@ -16,7 +17,9 @@ export function SetupInstructionsLink({
|
|||
return (
|
||||
<KibanaLink pathname={'/app/kibana'} hash={'/home/tutorial/apm'}>
|
||||
<EuiButton size="s" color="primary" fill={buttonFill}>
|
||||
Setup Instructions
|
||||
{i18n.translate('xpack.apm.setupInstructionsButtonLabel', {
|
||||
defaultMessage: 'Setup Instructions'
|
||||
})}
|
||||
</EuiButton>
|
||||
</KibanaLink>
|
||||
);
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
EuiLink,
|
||||
EuiPopover
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import idx from 'idx';
|
||||
import React from 'react';
|
||||
import { getKibanaHref } from 'x-pack/plugins/apm/public/utils/url';
|
||||
|
@ -40,7 +41,9 @@ function getInfraMetricsQuery(transaction: Transaction) {
|
|||
function ActionMenuButton({ onClick }: { onClick: () => void }) {
|
||||
return (
|
||||
<EuiButtonEmpty iconType="arrowDown" iconSide="right" onClick={onClick}>
|
||||
Actions
|
||||
{i18n.translate('xpack.apm.transactionActionMenu.actionsButtonLabel', {
|
||||
defaultMessage: 'Actions'
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
}
|
||||
|
@ -81,7 +84,10 @@ export class TransactionActionMenu extends React.Component<Props, State> {
|
|||
return [
|
||||
{
|
||||
icon: 'loggingApp',
|
||||
label: 'Show pod logs',
|
||||
label: i18n.translate(
|
||||
'xpack.apm.transactionActionMenu.showPodLogsLinkLabel',
|
||||
{ defaultMessage: 'Show pod logs' }
|
||||
),
|
||||
target: podId,
|
||||
hash: `/link-to/pod-logs/${podId}`,
|
||||
query: { time }
|
||||
|
@ -89,7 +95,10 @@ export class TransactionActionMenu extends React.Component<Props, State> {
|
|||
|
||||
{
|
||||
icon: 'loggingApp',
|
||||
label: 'Show container logs',
|
||||
label: i18n.translate(
|
||||
'xpack.apm.transactionActionMenu.showContainerLogsLinkLabel',
|
||||
{ defaultMessage: 'Show container logs' }
|
||||
),
|
||||
target: containerId,
|
||||
hash: `/link-to/container-logs/${containerId}`,
|
||||
query: { time }
|
||||
|
@ -97,7 +106,10 @@ export class TransactionActionMenu extends React.Component<Props, State> {
|
|||
|
||||
{
|
||||
icon: 'loggingApp',
|
||||
label: 'Show host logs',
|
||||
label: i18n.translate(
|
||||
'xpack.apm.transactionActionMenu.showHostLogsLinkLabel',
|
||||
{ defaultMessage: 'Show host logs' }
|
||||
),
|
||||
target: hostName,
|
||||
hash: `/link-to/host-logs/${hostName}`,
|
||||
query: { time }
|
||||
|
@ -105,7 +117,10 @@ export class TransactionActionMenu extends React.Component<Props, State> {
|
|||
|
||||
{
|
||||
icon: 'infraApp',
|
||||
label: 'Show pod metrics',
|
||||
label: i18n.translate(
|
||||
'xpack.apm.transactionActionMenu.showPodMetricsLinkLabel',
|
||||
{ defaultMessage: 'Show pod metrics' }
|
||||
),
|
||||
target: podId,
|
||||
hash: `/link-to/pod-detail/${podId}`,
|
||||
query: infraMetricsQuery
|
||||
|
@ -113,7 +128,10 @@ export class TransactionActionMenu extends React.Component<Props, State> {
|
|||
|
||||
{
|
||||
icon: 'infraApp',
|
||||
label: 'Show container metrics',
|
||||
label: i18n.translate(
|
||||
'xpack.apm.transactionActionMenu.showContainerMetricsLinkLabel',
|
||||
{ defaultMessage: 'Show container metrics' }
|
||||
),
|
||||
target: containerId,
|
||||
hash: `/link-to/container-detail/${containerId}`,
|
||||
query: infraMetricsQuery
|
||||
|
@ -121,7 +139,10 @@ export class TransactionActionMenu extends React.Component<Props, State> {
|
|||
|
||||
{
|
||||
icon: 'infraApp',
|
||||
label: 'Show host metrics',
|
||||
label: i18n.translate(
|
||||
'xpack.apm.transactionActionMenu.showHostMetricsLinkLabel',
|
||||
{ defaultMessage: 'Show host metrics' }
|
||||
),
|
||||
target: hostName,
|
||||
hash: `/link-to/host-detail/${hostName}`,
|
||||
query: infraMetricsQuery
|
||||
|
@ -172,7 +193,14 @@ export class TransactionActionMenu extends React.Component<Props, State> {
|
|||
>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiLink>View sample document</EuiLink>
|
||||
<EuiLink>
|
||||
{i18n.translate(
|
||||
'xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel',
|
||||
{
|
||||
defaultMessage: 'View sample document'
|
||||
}
|
||||
)}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="popout" />
|
||||
|
@ -190,7 +218,13 @@ export class TransactionActionMenu extends React.Component<Props, State> {
|
|||
anchorPosition="downRight"
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiContextMenuPanel items={items} title="Actions" />
|
||||
<EuiContextMenuPanel
|
||||
items={items}
|
||||
title={i18n.translate(
|
||||
'xpack.apm.transactionActionMenu.actionsLabel',
|
||||
{ defaultMessage: 'Actions' }
|
||||
)}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
}}
|
||||
|
|
|
@ -20,6 +20,7 @@ import { rgba } from 'polished';
|
|||
|
||||
import StatusText from './StatusText';
|
||||
import { SharedPlot } from './plotUtils';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
const X_TICK_TOTAL = 7;
|
||||
class StaticPlot extends PureComponent {
|
||||
|
@ -112,7 +113,12 @@ class StaticPlot extends PureComponent {
|
|||
/>
|
||||
|
||||
{noHits ? (
|
||||
<StatusText marginLeft={30} text="No data within this time range." />
|
||||
<StatusText
|
||||
marginLeft={30}
|
||||
text={i18n.translate('xpack.apm.metrics.plot.noDataLabel', {
|
||||
defaultMessage: 'No data within this time range.'
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
this.getVisSeries(series, plotValues)
|
||||
)}
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
EuiText,
|
||||
EuiTitle
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { Component } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { ITransactionChartData } from 'x-pack/plugins/apm/public/store/selectors/chartSelectors';
|
||||
|
@ -44,13 +45,22 @@ const ShiftedEuiText = styled(EuiText)`
|
|||
top: 5px;
|
||||
`;
|
||||
|
||||
const msTimeUnitLabel = i18n.translate(
|
||||
'xpack.apm.metrics.transactionChart.msTimeUnitLabel',
|
||||
{
|
||||
defaultMessage: 'ms'
|
||||
}
|
||||
);
|
||||
|
||||
export class TransactionChartsView extends Component<TransactionChartProps> {
|
||||
public getResponseTimeTickFormatter = (t: number) => {
|
||||
return this.props.charts.noHits ? '- ms' : asMillis(t);
|
||||
return this.props.charts.noHits ? `- ${msTimeUnitLabel}` : asMillis(t);
|
||||
};
|
||||
|
||||
public getResponseTimeTooltipFormatter = (p: Coordinate) => {
|
||||
return this.props.charts.noHits || !p ? '- ms' : asMillis(p.y);
|
||||
return this.props.charts.noHits || !p
|
||||
? `- ${msTimeUnitLabel}`
|
||||
: asMillis(p.y);
|
||||
};
|
||||
|
||||
public getTPMFormatter = (t: number | null) => {
|
||||
|
@ -80,9 +90,24 @@ export class TransactionChartsView extends Component<TransactionChartProps> {
|
|||
<EuiFlexItem grow={false}>
|
||||
<ShiftedEuiText size="xs">
|
||||
<ShiftedIconWrapper>
|
||||
<EuiIconTip content="The stream around the average duration shows the expected bounds. An annotation is shown for anomaly scores >= 75." />
|
||||
<EuiIconTip
|
||||
content={i18n.translate(
|
||||
'xpack.apm.metrics.transactionChart.machineLearningTooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'The stream around the average duration shows the expected bounds. An annotation is shown for anomaly scores >= 75.'
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</ShiftedIconWrapper>
|
||||
<span>Machine learning: </span>
|
||||
<span>
|
||||
{i18n.translate(
|
||||
'xpack.apm.metrics.transactionChart.machineLearningLabel',
|
||||
{
|
||||
defaultMessage: 'Machine learning:'
|
||||
}
|
||||
)}{' '}
|
||||
</span>
|
||||
<ViewMLJob
|
||||
serviceName={serviceName}
|
||||
transactionType={transactionType}
|
||||
|
@ -149,16 +174,43 @@ export class TransactionChartsView extends Component<TransactionChartProps> {
|
|||
}
|
||||
|
||||
function tpmLabel(type?: string) {
|
||||
return type === 'request' ? 'Requests per minute' : 'Transactions per minute';
|
||||
return type === 'request'
|
||||
? i18n.translate(
|
||||
'xpack.apm.metrics.transactionChart.requestsPerMinuteLabel',
|
||||
{
|
||||
defaultMessage: 'Requests per minute'
|
||||
}
|
||||
)
|
||||
: i18n.translate(
|
||||
'xpack.apm.metrics.transactionChart.transactionsPerMinuteLabel',
|
||||
{
|
||||
defaultMessage: 'Transactions per minute'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function responseTimeLabel(type?: string) {
|
||||
switch (type) {
|
||||
case 'page-load':
|
||||
return 'Page load times';
|
||||
return i18n.translate(
|
||||
'xpack.apm.metrics.transactionChart.pageLoadTimesLabel',
|
||||
{
|
||||
defaultMessage: 'Page load times'
|
||||
}
|
||||
);
|
||||
case 'route-change':
|
||||
return 'Route change times';
|
||||
return i18n.translate(
|
||||
'xpack.apm.metrics.transactionChart.routeChangeTimesLabel',
|
||||
{
|
||||
defaultMessage: 'Route change times'
|
||||
}
|
||||
);
|
||||
default:
|
||||
return 'Transaction duration';
|
||||
return i18n.translate(
|
||||
'xpack.apm.metrics.transactionChart.transactionDurationLabel',
|
||||
{
|
||||
defaultMessage: 'Transaction duration'
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ import LicenseChecker from './components/app/Main/LicenseChecker';
|
|||
|
||||
import { history } from './utils/url';
|
||||
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
|
||||
chrome.setRootTemplate(template);
|
||||
const store = configureStore();
|
||||
|
||||
|
@ -43,15 +45,17 @@ initTimepicker(history, store.dispatch).then(() => {
|
|||
);
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<Fragment>
|
||||
<GlobalProgress />
|
||||
<LicenseChecker />
|
||||
<Router history={history}>
|
||||
<Main />
|
||||
</Router>
|
||||
</Fragment>
|
||||
</Provider>,
|
||||
<I18nProvider>
|
||||
<Provider store={store}>
|
||||
<Fragment>
|
||||
<GlobalProgress />
|
||||
<LicenseChecker />
|
||||
<Router history={history}>
|
||||
<Main />
|
||||
</Router>
|
||||
</Fragment>
|
||||
</Provider>
|
||||
</I18nProvider>,
|
||||
document.getElementById('react-apm-root')
|
||||
);
|
||||
});
|
||||
|
|
|
@ -14,7 +14,7 @@ import { IUrlParams } from '../urlParams';
|
|||
import { createInitialDataSelector } from './helpers';
|
||||
|
||||
const ID = 'errorGroupDetails';
|
||||
const INITIAL_DATA: ErrorGroupAPIResponse = {};
|
||||
const INITIAL_DATA: ErrorGroupAPIResponse = { occurrencesCount: 0 };
|
||||
const withInitialData = createInitialDataSelector(INITIAL_DATA);
|
||||
|
||||
export function getErrorGroupDetails(state: IReduxState) {
|
||||
|
|
|
@ -20,7 +20,7 @@ import { getTransaction } from '../transactions/get_transaction';
|
|||
export interface ErrorGroupAPIResponse {
|
||||
transaction?: Transaction;
|
||||
error?: APMError;
|
||||
occurrencesCount?: number;
|
||||
occurrencesCount: number;
|
||||
}
|
||||
|
||||
// TODO: rename from "getErrorGroup" to "getErrorGroupSample" (since a single error is returned, not an errorGroup)
|
||||
|
@ -82,6 +82,6 @@ export async function getErrorGroup({
|
|||
return {
|
||||
transaction,
|
||||
error,
|
||||
occurrencesCount: oc(resp).hits.total()
|
||||
occurrencesCount: resp.hits.total
|
||||
};
|
||||
}
|
||||
|
|
|
@ -35,8 +35,8 @@ export function beats(kibana: any) {
|
|||
},
|
||||
config: () => config,
|
||||
configPrefix: CONFIG_PREFIX,
|
||||
init(server: KibanaLegacyServer) {
|
||||
initServerWithKibana(server);
|
||||
async init(server: KibanaLegacyServer) {
|
||||
await initServerWithKibana(server);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { compose } from './lib/compose/kibana';
|
||||
import { initManagementServer } from './management_server';
|
||||
|
||||
export const initServerWithKibana = (hapiServer: any) => {
|
||||
export const initServerWithKibana = async (hapiServer: any) => {
|
||||
const libs = compose(hapiServer);
|
||||
initManagementServer(libs);
|
||||
await initManagementServer(libs);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 PropTypes from 'prop-types';
|
||||
import {
|
||||
EuiSelect,
|
||||
EuiFormRow,
|
||||
} from '@elastic/eui';
|
||||
|
||||
const NO_REGIONMAP_LAYERS_MSG =
|
||||
'No vector layers are available.' +
|
||||
' Contact your system administrator to enable vector layers by setting "map.regionmap" in kibana.yml.';
|
||||
|
||||
export function CreateSourceEditor({ onSelect, regionmapLayers }) {
|
||||
|
||||
const regionmapOptions = regionmapLayers.map(({ name, url }) => {
|
||||
return {
|
||||
value: url,
|
||||
text: name
|
||||
};
|
||||
});
|
||||
|
||||
const onChange = ({ target }) => {
|
||||
const selectedName = target.options[target.selectedIndex].text;
|
||||
onSelect({ name: selectedName });
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label="Vector layer"
|
||||
helpText={regionmapLayers.length === 0 ? NO_REGIONMAP_LAYERS_MSG : null}
|
||||
>
|
||||
<EuiSelect
|
||||
hasNoInitialSelection
|
||||
options={regionmapOptions}
|
||||
onChange={onChange}
|
||||
disabled={regionmapLayers.length === 0}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
CreateSourceEditor.propTypes = {
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
regionmapLayers: PropTypes.arrayOf(PropTypes.shape({
|
||||
url: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
})),
|
||||
};
|
||||
|
||||
CreateSourceEditor.defaultProps = {
|
||||
regionmapLayers: [],
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 { KibanaRegionmapSource } from './kibana_regionmap_source';
|
|
@ -4,14 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { VectorSource } from './vector_source';
|
||||
import _ from 'lodash';
|
||||
import { VectorSource } from '../vector_source';
|
||||
import React, { Fragment } from 'react';
|
||||
import {
|
||||
EuiText,
|
||||
EuiSelect,
|
||||
EuiFormRow,
|
||||
EuiSpacer
|
||||
} from '@elastic/eui';
|
||||
import { CreateSourceEditor } from './create_source_editor';
|
||||
|
||||
export class KibanaRegionmapSource extends VectorSource {
|
||||
|
||||
|
@ -23,35 +23,27 @@ export class KibanaRegionmapSource extends VectorSource {
|
|||
this._regionList = ymlFileLayers;
|
||||
}
|
||||
|
||||
static createDescriptor(name) {
|
||||
static createDescriptor(options) {
|
||||
return {
|
||||
type: KibanaRegionmapSource.type,
|
||||
name: name
|
||||
name: options.name
|
||||
};
|
||||
}
|
||||
|
||||
static renderEditor = ({ dataSourcesMeta, onPreviewSource }) => {
|
||||
const regionmapOptionsRaw = (dataSourcesMeta) ? dataSourcesMeta.kibana.regionmap : [];
|
||||
const regionmapOptions = regionmapOptionsRaw ? regionmapOptionsRaw.map((file) => ({
|
||||
value: file.url,
|
||||
text: file.name
|
||||
})) : [];
|
||||
const regionmapLayers = _.get(dataSourcesMeta, 'kibana.regionmap', []);
|
||||
|
||||
const onChange = ({ target }) => {
|
||||
const selectedName = target.options[target.selectedIndex].text;
|
||||
const kibanaRegionmapSourceDescriptor = KibanaRegionmapSource.createDescriptor(selectedName);
|
||||
const kibanaRegionmapSource = new KibanaRegionmapSource(kibanaRegionmapSourceDescriptor, regionmapOptionsRaw);
|
||||
onPreviewSource(kibanaRegionmapSource);
|
||||
const onSelect = (layerConfig) => {
|
||||
const sourceDescriptor = KibanaRegionmapSource.createDescriptor(layerConfig);
|
||||
const source = new KibanaRegionmapSource(sourceDescriptor, { ymlFileLayers: regionmapLayers });
|
||||
onPreviewSource(source);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFormRow label="File">
|
||||
<EuiSelect
|
||||
hasNoInitialSelection
|
||||
options={regionmapOptions}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<CreateSourceEditor
|
||||
onSelect={onSelect}
|
||||
regionmapLayers={regionmapLayers}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -105,4 +97,8 @@ export class KibanaRegionmapSource extends VectorSource {
|
|||
async isTimeAware() {
|
||||
return false;
|
||||
}
|
||||
|
||||
canFormatFeatureProperties() {
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -47,7 +47,3 @@ ColorRampSelector.propTypes = {
|
|||
color: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
ColorRampSelector.defaultProps = {
|
||||
color: '',
|
||||
};
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
EuiFormRow,
|
||||
EuiFlexGroup,
|
||||
|
@ -14,75 +13,86 @@ import {
|
|||
EuiRange
|
||||
} from '@elastic/eui';
|
||||
|
||||
|
||||
|
||||
const DEFAULT_MIN_SIZE = 1;
|
||||
const DEFAULT_MAX_SIZE = 100;
|
||||
const DEFAULT_MAX_SIZE = 64;
|
||||
|
||||
export function SizeRangeSelector({ minSize, maxSize, onChange }) {
|
||||
export class SizeRangeSelector extends React.Component {
|
||||
|
||||
const sizeChange = (minSize, maxSize)=>{
|
||||
onChange({
|
||||
minSize: minSize,
|
||||
maxSize: maxSize
|
||||
_onSizeChange = (min, max) => {
|
||||
this.props.onChange({
|
||||
minSize: min,
|
||||
maxSize: max
|
||||
});
|
||||
};
|
||||
|
||||
const onMinSizeChange = (e) => {
|
||||
const updatedMinSize = parseInt(e.target.value, 10);
|
||||
sizeChange(updatedMinSize, updatedMinSize > maxSize ? updatedMinSize : maxSize);
|
||||
};
|
||||
_areSizesValid() {
|
||||
return typeof this.props.minSize === 'number' && typeof this.props.maxSize === 'number';
|
||||
}
|
||||
|
||||
const onMaxSizeChange = (e) => {
|
||||
const updatedMaxSize = parseInt(e.target.value, 10);
|
||||
sizeChange(updatedMaxSize < minSize ? updatedMaxSize : minSize, updatedMaxSize);
|
||||
};
|
||||
componentDidUpdate() {
|
||||
if (!this._areSizesValid()) {
|
||||
this._onSizeChange(DEFAULT_MIN_SIZE, DEFAULT_MAX_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFormRow>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label="Min size"
|
||||
compressed
|
||||
>
|
||||
<EuiRange
|
||||
min={DEFAULT_MIN_SIZE}
|
||||
max={DEFAULT_MAX_SIZE}
|
||||
value={minSize.toString()}
|
||||
onChange={onMinSizeChange}
|
||||
showInput
|
||||
showRange
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label="Max size"
|
||||
compressed
|
||||
>
|
||||
<EuiRange
|
||||
min={DEFAULT_MIN_SIZE}
|
||||
max={DEFAULT_MAX_SIZE}
|
||||
value={maxSize.toString()}
|
||||
onChange={onMaxSizeChange}
|
||||
showInput
|
||||
showRange
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
);
|
||||
render() {
|
||||
|
||||
if (!this._areSizesValid()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onMinSizeChange = (e) => {
|
||||
const updatedMinSize = parseInt(e.target.value, 10);
|
||||
this._onSizeChange(updatedMinSize, updatedMinSize > this.props.maxSize ? updatedMinSize : this.props.maxSize);
|
||||
};
|
||||
|
||||
const onMaxSizeChange = (e) => {
|
||||
const updatedMaxSize = parseInt(e.target.value, 10);
|
||||
this._onSizeChange(updatedMaxSize < this.props.minSize ? updatedMaxSize : this.props.minSize, updatedMaxSize);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFormRow>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label="Min size"
|
||||
compressed
|
||||
>
|
||||
<EuiRange
|
||||
min={DEFAULT_MIN_SIZE}
|
||||
max={DEFAULT_MAX_SIZE}
|
||||
value={this.props.minSize.toString()}
|
||||
onChange={onMinSizeChange}
|
||||
showInput
|
||||
showRange
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label="Max size"
|
||||
compressed
|
||||
>
|
||||
<EuiRange
|
||||
min={DEFAULT_MIN_SIZE}
|
||||
max={DEFAULT_MAX_SIZE}
|
||||
value={this.props.maxSize.toString()}
|
||||
onChange={onMaxSizeChange}
|
||||
showInput
|
||||
showRange
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
SizeRangeSelector.propTypes = {
|
||||
minSize: PropTypes.number,
|
||||
maxSize: PropTypes.number,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
SizeRangeSelector.defaultProps = {
|
||||
minSize: DEFAULT_MIN_SIZE,
|
||||
maxSize: DEFAULT_MAX_SIZE,
|
||||
};
|
||||
|
|
|
@ -15,6 +15,8 @@ import {
|
|||
EuiFieldNumber,
|
||||
EuiDescribedFormGroup,
|
||||
EuiButton,
|
||||
EuiSwitch,
|
||||
EuiTextColor,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
PHASE_COLD,
|
||||
|
@ -24,10 +26,11 @@ import {
|
|||
PHASE_ROLLOVER_MINIMUM_AGE_UNITS,
|
||||
PHASE_NODE_ATTRS,
|
||||
PHASE_REPLICA_COUNT,
|
||||
PHASE_FREEZE_ENABLED
|
||||
} from '../../../../store/constants';
|
||||
import { ErrableFormRow } from '../../form_errors';
|
||||
import { MinAgeInput } from '../min_age_input';
|
||||
import { ActiveBadge, PhaseErrorMessage, OptionalLabel } from '../../../components';
|
||||
import { LearnMoreLink, ActiveBadge, PhaseErrorMessage, OptionalLabel } from '../../../components';
|
||||
import { NodeAllocation } from '../node_allocation';
|
||||
|
||||
class ColdPhaseUi extends PureComponent {
|
||||
|
@ -57,123 +60,163 @@ class ColdPhaseUi extends PureComponent {
|
|||
intl,
|
||||
hotPhaseRolloverEnabled
|
||||
} = this.props;
|
||||
|
||||
const freezeLabel = intl.formatMessage({
|
||||
id: 'xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel',
|
||||
defaultMessage: 'Freeze index',
|
||||
});
|
||||
return (
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<div>
|
||||
<span className="eui-displayInlineBlock eui-alignMiddle">
|
||||
<FormattedMessage
|
||||
id="xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseLabel"
|
||||
defaultMessage="Cold phase"
|
||||
/>
|
||||
</span>{' '}
|
||||
{phaseData[PHASE_ENABLED] && !isShowingErrors ? <ActiveBadge /> : null}
|
||||
<PhaseErrorMessage isShowingErrors={isShowingErrors} />
|
||||
</div>
|
||||
}
|
||||
titleSize="s"
|
||||
description={
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseDescriptionText"
|
||||
defaultMessage="You are querying your index less frequently, so you can allocate shards
|
||||
<Fragment>
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<div>
|
||||
<span className="eui-displayInlineBlock eui-alignMiddle">
|
||||
<FormattedMessage
|
||||
id="xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseLabel"
|
||||
defaultMessage="Cold phase"
|
||||
/>
|
||||
</span>{' '}
|
||||
{phaseData[PHASE_ENABLED] && !isShowingErrors ? <ActiveBadge /> : null}
|
||||
<PhaseErrorMessage isShowingErrors={isShowingErrors} />
|
||||
</div>
|
||||
}
|
||||
titleSize="s"
|
||||
description={
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseDescriptionText"
|
||||
defaultMessage="You are querying your index less frequently, so you can allocate shards
|
||||
on significantly less performant hardware.
|
||||
Because your queries are slower, you can reduce the number of replicas."
|
||||
/>
|
||||
</p>
|
||||
{phaseData[PHASE_ENABLED] ? (
|
||||
<EuiButton
|
||||
color="danger"
|
||||
onClick={async () => {
|
||||
await setPhaseData(PHASE_ENABLED, false);
|
||||
}}
|
||||
aria-controls="coldPhaseContent"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.indexLifecycleMgmt.editPolicy.coldhase.deactivateColdPhaseButton"
|
||||
defaultMessage="Deactivate cold phase"
|
||||
/>
|
||||
</EuiButton>
|
||||
) : (
|
||||
<EuiButton
|
||||
data-test-subj="activatePhaseButton-cold"
|
||||
onClick={async () => {
|
||||
await setPhaseData(PHASE_ENABLED, true);
|
||||
}}
|
||||
aria-controls="coldPhaseContent"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.indexLifecycleMgmt.editPolicy.coldPhase.activateColdPhaseButton"
|
||||
defaultMessage="Activate cold phase"
|
||||
/>
|
||||
</EuiButton>
|
||||
)}
|
||||
</Fragment>
|
||||
}
|
||||
fullWidth
|
||||
>
|
||||
<div id="coldPhaseContent" aria-live="polite" role="region">
|
||||
{phaseData[PHASE_ENABLED] ? (
|
||||
<Fragment>
|
||||
<MinAgeInput
|
||||
errors={errors}
|
||||
phaseData={phaseData}
|
||||
phase={PHASE_COLD}
|
||||
isShowingErrors={isShowingErrors}
|
||||
setPhaseData={setPhaseData}
|
||||
rolloverEnabled={hotPhaseRolloverEnabled}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
|
||||
<NodeAllocation
|
||||
phase={PHASE_COLD}
|
||||
setPhaseData={setPhaseData}
|
||||
showNodeDetailsFlyout={showNodeDetailsFlyout}
|
||||
errors={errors}
|
||||
phaseData={phaseData}
|
||||
isShowingErrors={isShowingErrors}
|
||||
/>
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false} style={{ maxWidth: 188 }}>
|
||||
<ErrableFormRow
|
||||
id={`${PHASE_COLD}-${PHASE_REPLICA_COUNT}`}
|
||||
label={
|
||||
<Fragment>
|
||||
<FormattedMessage
|
||||
id="xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel"
|
||||
defaultMessage="Number of replicas"
|
||||
/>
|
||||
<OptionalLabel />
|
||||
</Fragment>
|
||||
}
|
||||
errorKey={PHASE_REPLICA_COUNT}
|
||||
isShowingErrors={isShowingErrors}
|
||||
errors={errors}
|
||||
helpText={
|
||||
intl.formatMessage({
|
||||
id: 'xpack.indexLifecycleMgmt.coldPhase.replicaCountHelpText',
|
||||
defaultMessage: 'By default, the number of replicas remains the same.'
|
||||
})
|
||||
}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
id={`${PHASE_COLD}-${PHASE_REPLICA_COUNT}`}
|
||||
value={phaseData[PHASE_REPLICA_COUNT]}
|
||||
onChange={async e => {
|
||||
await setPhaseData(PHASE_REPLICA_COUNT, e.target.value);
|
||||
}}
|
||||
min={0}
|
||||
/>
|
||||
</ErrableFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</p>
|
||||
{phaseData[PHASE_ENABLED] ? (
|
||||
<EuiButton
|
||||
color="danger"
|
||||
onClick={async () => {
|
||||
await setPhaseData(PHASE_ENABLED, false);
|
||||
}}
|
||||
aria-controls="coldPhaseContent"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.indexLifecycleMgmt.editPolicy.coldhase.deactivateColdPhaseButton"
|
||||
defaultMessage="Deactivate cold phase"
|
||||
/>
|
||||
</EuiButton>
|
||||
) : (
|
||||
<EuiButton
|
||||
data-test-subj="activatePhaseButton-cold"
|
||||
onClick={async () => {
|
||||
await setPhaseData(PHASE_ENABLED, true);
|
||||
}}
|
||||
aria-controls="coldPhaseContent"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.indexLifecycleMgmt.editPolicy.coldPhase.activateColdPhaseButton"
|
||||
defaultMessage="Activate cold phase"
|
||||
/>
|
||||
</EuiButton>
|
||||
)}
|
||||
</Fragment>
|
||||
) : <div />}
|
||||
</div>
|
||||
</EuiDescribedFormGroup>
|
||||
}
|
||||
fullWidth
|
||||
>
|
||||
<div id="coldPhaseContent" aria-live="polite" role="region">
|
||||
{phaseData[PHASE_ENABLED] ? (
|
||||
<Fragment>
|
||||
<MinAgeInput
|
||||
errors={errors}
|
||||
phaseData={phaseData}
|
||||
phase={PHASE_COLD}
|
||||
isShowingErrors={isShowingErrors}
|
||||
setPhaseData={setPhaseData}
|
||||
rolloverEnabled={hotPhaseRolloverEnabled}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
|
||||
<NodeAllocation
|
||||
phase={PHASE_COLD}
|
||||
setPhaseData={setPhaseData}
|
||||
showNodeDetailsFlyout={showNodeDetailsFlyout}
|
||||
errors={errors}
|
||||
phaseData={phaseData}
|
||||
isShowingErrors={isShowingErrors}
|
||||
/>
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false} style={{ maxWidth: 188 }}>
|
||||
<ErrableFormRow
|
||||
id={`${PHASE_COLD}-${PHASE_REPLICA_COUNT}`}
|
||||
label={
|
||||
<Fragment>
|
||||
<FormattedMessage
|
||||
id="xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel"
|
||||
defaultMessage="Number of replicas"
|
||||
/>
|
||||
<OptionalLabel />
|
||||
</Fragment>
|
||||
}
|
||||
errorKey={PHASE_REPLICA_COUNT}
|
||||
isShowingErrors={isShowingErrors}
|
||||
errors={errors}
|
||||
helpText={
|
||||
intl.formatMessage({
|
||||
id: 'xpack.indexLifecycleMgmt.coldPhase.replicaCountHelpText',
|
||||
defaultMessage: 'By default, the number of replicas remains the same.'
|
||||
})
|
||||
}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
id={`${PHASE_COLD}-${PHASE_REPLICA_COUNT}`}
|
||||
value={phaseData[PHASE_REPLICA_COUNT]}
|
||||
onChange={async e => {
|
||||
await setPhaseData(PHASE_REPLICA_COUNT, e.target.value);
|
||||
}}
|
||||
min={0}
|
||||
/>
|
||||
</ErrableFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
</Fragment>
|
||||
) : <div />}
|
||||
</div>
|
||||
</EuiDescribedFormGroup>
|
||||
{phaseData[PHASE_ENABLED] ? (
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeText"
|
||||
defaultMessage="Freeze"
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
description={
|
||||
<EuiTextColor color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeIndexExplanationText"
|
||||
defaultMessage="A frozen index has little overhead on the cluster and is blocked for write operations.
|
||||
You can search a frozen index, but expect queries to be slower."
|
||||
/>{' '}
|
||||
<LearnMoreLink docPath="frozen-indices.html" />
|
||||
</EuiTextColor>
|
||||
}
|
||||
fullWidth
|
||||
titleSize="xs"
|
||||
>
|
||||
<EuiSwitch
|
||||
data-test-subj="freezeSwitch"
|
||||
checked={phaseData[PHASE_FREEZE_ENABLED]}
|
||||
onChange={async e => {
|
||||
await setPhaseData(PHASE_FREEZE_ENABLED, e.target.checked);
|
||||
}}
|
||||
label={freezeLabel}
|
||||
aria-label={freezeLabel}
|
||||
/>
|
||||
</EuiDescribedFormGroup>
|
||||
) : null }
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -102,8 +102,8 @@ class WarmPhaseUi extends PureComponent {
|
|||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.indexLifecycleMgmt.editPolicy.warmPhase.warmPhaseDescriptionMessage"
|
||||
defaultMessage="You are still querying your index, but it is read-only, and you are no longer
|
||||
updating it. You can allocate shards to less performant hardware.
|
||||
defaultMessage="You are still querying your index, but it is read-only.
|
||||
You can allocate shards to less performant hardware.
|
||||
For faster searches, you can reduce the number of shards and force merge segments."
|
||||
/>
|
||||
</p>
|
||||
|
|
|
@ -27,6 +27,7 @@ export const PHASE_ROLLOVER_MINIMUM_AGE_UNITS = 'selectedMinimumAgeUnits';
|
|||
|
||||
export const PHASE_FORCE_MERGE_SEGMENTS = 'selectedForceMergeSegments';
|
||||
export const PHASE_FORCE_MERGE_ENABLED = 'forceMergeEnabled';
|
||||
export const PHASE_FREEZE_ENABLED = 'freezeEnabled';
|
||||
|
||||
export const PHASE_SHRINK_ENABLED = 'shrinkEnabled';
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
PHASE_REPLICA_COUNT,
|
||||
PHASE_ROLLOVER_MINIMUM_AGE_UNITS,
|
||||
PHASE_ROLLOVER_ALIAS,
|
||||
PHASE_FREEZE_ENABLED,
|
||||
} from '../constants';
|
||||
|
||||
export const defaultColdPhase = {
|
||||
|
@ -18,5 +19,6 @@ export const defaultColdPhase = {
|
|||
[PHASE_ROLLOVER_MINIMUM_AGE]: '',
|
||||
[PHASE_ROLLOVER_MINIMUM_AGE_UNITS]: 'd',
|
||||
[PHASE_NODE_ATTRS]: '',
|
||||
[PHASE_REPLICA_COUNT]: ''
|
||||
[PHASE_REPLICA_COUNT]: '',
|
||||
[PHASE_FREEZE_ENABLED]: false
|
||||
};
|
|
@ -35,7 +35,8 @@ import {
|
|||
PHASE_ATTRIBUTES_THAT_ARE_NUMBERS,
|
||||
MAX_SIZE_TYPE_DOCUMENT,
|
||||
WARM_PHASE_ON_ROLLOVER,
|
||||
PHASE_SHRINK_ENABLED
|
||||
PHASE_SHRINK_ENABLED,
|
||||
PHASE_FREEZE_ENABLED
|
||||
} from '../constants';
|
||||
import { filterItems, sortTable } from '../../services';
|
||||
|
||||
|
@ -194,6 +195,9 @@ export const phaseFromES = (phase, phaseName, defaultPolicy) => {
|
|||
if (actions.shrink) {
|
||||
policy[PHASE_PRIMARY_SHARD_COUNT] = actions.shrink.number_of_shards;
|
||||
}
|
||||
if (actions.freeze) {
|
||||
policy[PHASE_FREEZE_ENABLED] = true;
|
||||
}
|
||||
}
|
||||
return policy;
|
||||
};
|
||||
|
@ -276,5 +280,11 @@ export const phaseToES = (phase, originalEsPhase) => {
|
|||
} else {
|
||||
delete esPhase.actions.shrink;
|
||||
}
|
||||
|
||||
if (phase[PHASE_FREEZE_ENABLED]) {
|
||||
esPhase.actions.freeze = {};
|
||||
} else {
|
||||
delete esPhase.actions.freeze;
|
||||
}
|
||||
return esPhase;
|
||||
};
|
||||
|
|
|
@ -58,17 +58,9 @@ const initialState = {
|
|||
value: 0,
|
||||
color: '#D3DAE6',
|
||||
},
|
||||
{
|
||||
value: 0.65,
|
||||
color: '#00B3A4',
|
||||
},
|
||||
{
|
||||
value: 0.8,
|
||||
color: '#E6C220',
|
||||
},
|
||||
{
|
||||
value: 1,
|
||||
color: '#DB1374',
|
||||
color: '#3185FC',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -82,7 +74,7 @@ interface WithOptionsProps {
|
|||
|
||||
type State = Readonly<typeof initialState>;
|
||||
|
||||
export const withOptions = <P extends InfraOptions>(WrappedComponent: React.ComponentType<P>) => (
|
||||
export const withOptions = (WrappedComponent: React.ComponentType<InfraOptions>) => (
|
||||
<WithOptions>{args => <WrappedComponent {...args} />}</WithOptions>
|
||||
);
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ export const nginxLayoutCreator: InfraMetricLayoutCreator = theme => [
|
|||
defaultMessage: 'Request Rate',
|
||||
}
|
||||
),
|
||||
requires: ['nginx.statusstub'],
|
||||
requires: ['nginx.stubstatus'],
|
||||
type: InfraMetricLayoutSectionType.chart,
|
||||
visConfig: {
|
||||
formatter: InfraFormatterType.abbreviatedNumber,
|
||||
|
@ -66,7 +66,7 @@ export const nginxLayoutCreator: InfraMetricLayoutCreator = theme => [
|
|||
defaultMessage: 'Active Connections',
|
||||
}
|
||||
),
|
||||
requires: ['nginx.statusstub'],
|
||||
requires: ['nginx.stubstatus'],
|
||||
type: InfraMetricLayoutSectionType.chart,
|
||||
visConfig: {
|
||||
formatter: InfraFormatterType.abbreviatedNumber,
|
||||
|
@ -86,7 +86,7 @@ export const nginxLayoutCreator: InfraMetricLayoutCreator = theme => [
|
|||
defaultMessage: 'Requests per Connections',
|
||||
}
|
||||
),
|
||||
requires: ['nginx.statusstub'],
|
||||
requires: ['nginx.stubstatus'],
|
||||
type: InfraMetricLayoutSectionType.chart,
|
||||
visConfig: {
|
||||
formatter: InfraFormatterType.abbreviatedNumber,
|
||||
|
|
|
@ -24,8 +24,20 @@ export const containerDiskIOBytes: InfraMetricModelCreator = (
|
|||
metrics: [
|
||||
{
|
||||
field: 'docker.diskio.read.bytes',
|
||||
id: 'avg-diskio-bytes',
|
||||
type: InfraMetricModelMetricType.avg,
|
||||
id: 'max-diskio-read-bytes',
|
||||
type: InfraMetricModelMetricType.max,
|
||||
},
|
||||
{
|
||||
field: 'max-diskio-read-bytes',
|
||||
id: 'deriv-max-diskio-read-bytes',
|
||||
type: InfraMetricModelMetricType.derivative,
|
||||
unit: '1s',
|
||||
},
|
||||
{
|
||||
id: 'posonly-deriv-max-diskio-read-bytes',
|
||||
type: InfraMetricModelMetricType.calculation,
|
||||
variables: [{ id: 'var-rate', name: 'rate', field: 'deriv-max-diskio-read-bytes' }],
|
||||
script: 'params.rate > 0.0 ? params.rate : 0.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -35,8 +47,32 @@ export const containerDiskIOBytes: InfraMetricModelCreator = (
|
|||
metrics: [
|
||||
{
|
||||
field: 'docker.diskio.write.bytes',
|
||||
id: 'avg-diskio-bytes',
|
||||
type: InfraMetricModelMetricType.avg,
|
||||
id: 'max-diskio-write-bytes',
|
||||
type: InfraMetricModelMetricType.max,
|
||||
},
|
||||
{
|
||||
field: 'max-diskio-write-bytes',
|
||||
id: 'deriv-max-diskio-write-bytes',
|
||||
type: InfraMetricModelMetricType.derivative,
|
||||
unit: '1s',
|
||||
},
|
||||
{
|
||||
id: 'posonly-deriv-max-diskio-write-bytes',
|
||||
type: InfraMetricModelMetricType.calculation,
|
||||
variables: [{ id: 'var-rate', name: 'rate', field: 'deriv-max-diskio-write-bytes' }],
|
||||
script: 'params.rate > 0.0 ? params.rate : 0.0',
|
||||
},
|
||||
{
|
||||
id: 'calc-invert-rate',
|
||||
script: 'params.rate * -1',
|
||||
type: InfraMetricModelMetricType.calculation,
|
||||
variables: [
|
||||
{
|
||||
field: 'posonly-deriv-max-diskio-write-bytes',
|
||||
id: 'var-rate',
|
||||
name: 'rate',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -20,8 +20,20 @@ export const containerDiskIOOps: InfraMetricModelCreator = (timeField, indexPatt
|
|||
metrics: [
|
||||
{
|
||||
field: 'docker.diskio.read.ops',
|
||||
id: 'avg-diskio-ops',
|
||||
type: InfraMetricModelMetricType.avg,
|
||||
id: 'max-diskio-read-ops',
|
||||
type: InfraMetricModelMetricType.max,
|
||||
},
|
||||
{
|
||||
field: 'max-diskio-read-ops',
|
||||
id: 'deriv-max-diskio-read-ops',
|
||||
type: InfraMetricModelMetricType.derivative,
|
||||
unit: '1s',
|
||||
},
|
||||
{
|
||||
id: 'posonly-deriv-max-diskio-read-ops',
|
||||
type: InfraMetricModelMetricType.calculation,
|
||||
variables: [{ id: 'var-rate', name: 'rate', field: 'deriv-max-diskio-read-ops' }],
|
||||
script: 'params.rate > 0.0 ? params.rate : 0.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -31,8 +43,32 @@ export const containerDiskIOOps: InfraMetricModelCreator = (timeField, indexPatt
|
|||
metrics: [
|
||||
{
|
||||
field: 'docker.diskio.write.ops',
|
||||
id: 'avg-diskio-ops',
|
||||
type: InfraMetricModelMetricType.avg,
|
||||
id: 'max-diskio-write-ops',
|
||||
type: InfraMetricModelMetricType.max,
|
||||
},
|
||||
{
|
||||
field: 'max-diskio-write-ops',
|
||||
id: 'deriv-max-diskio-write-ops',
|
||||
type: InfraMetricModelMetricType.derivative,
|
||||
unit: '1s',
|
||||
},
|
||||
{
|
||||
id: 'posonly-deriv-max-diskio-write-ops',
|
||||
type: InfraMetricModelMetricType.calculation,
|
||||
variables: [{ id: 'var-rate', name: 'rate', field: 'deriv-max-diskio-write-ops' }],
|
||||
script: 'params.rate > 0.0 ? params.rate : 0.0',
|
||||
},
|
||||
{
|
||||
id: 'calc-invert-rate',
|
||||
script: 'params.rate * -1',
|
||||
type: InfraMetricModelMetricType.calculation,
|
||||
variables: [
|
||||
{
|
||||
field: 'posonly-deriv-max-diskio-write-ops',
|
||||
id: 'var-rate',
|
||||
name: 'rate',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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 { compileFormattingRules } from '../message';
|
||||
import { filebeatApache2Rules } from './filebeat_apache2';
|
||||
|
||||
const { format } = compileFormattingRules(filebeatApache2Rules);
|
||||
describe('Filebeat Rules', () => {
|
||||
test('Apache2 Access', () => {
|
||||
const event = {
|
||||
'apache2.access': true,
|
||||
'apache2.access.remote_ip': '192.168.1.42',
|
||||
'apache2.access.user_name': 'admin',
|
||||
'apache2.access.method': 'GET',
|
||||
'apache2.access.url': '/faqs',
|
||||
'apache2.access.http_version': '1.1',
|
||||
'apache2.access.response_code': '200',
|
||||
'apache2.access.body_sent.bytes': 1024,
|
||||
};
|
||||
const message = format(event);
|
||||
expect(message).toEqual([
|
||||
{
|
||||
constant: '[Apache][access] ',
|
||||
},
|
||||
{
|
||||
field: 'apache2.access.remote_ip',
|
||||
highlights: [],
|
||||
value: '192.168.1.42',
|
||||
},
|
||||
{
|
||||
constant: ' ',
|
||||
},
|
||||
{
|
||||
field: 'apache2.access.user_name',
|
||||
highlights: [],
|
||||
value: 'admin',
|
||||
},
|
||||
{
|
||||
constant: ' "',
|
||||
},
|
||||
{
|
||||
field: 'apache2.access.method',
|
||||
highlights: [],
|
||||
value: 'GET',
|
||||
},
|
||||
{
|
||||
constant: ' ',
|
||||
},
|
||||
{
|
||||
field: 'apache2.access.url',
|
||||
highlights: [],
|
||||
value: '/faqs',
|
||||
},
|
||||
{
|
||||
constant: ' HTTP/',
|
||||
},
|
||||
{
|
||||
field: 'apache2.access.http_version',
|
||||
highlights: [],
|
||||
value: '1.1',
|
||||
},
|
||||
{
|
||||
constant: '" ',
|
||||
},
|
||||
{
|
||||
field: 'apache2.access.response_code',
|
||||
highlights: [],
|
||||
value: '200',
|
||||
},
|
||||
{
|
||||
constant: ' ',
|
||||
},
|
||||
{
|
||||
field: 'apache2.access.body_sent.bytes',
|
||||
highlights: [],
|
||||
value: '1024',
|
||||
},
|
||||
]);
|
||||
});
|
||||
test('Apache2 Error', () => {
|
||||
const event = {
|
||||
'apache2.error.message':
|
||||
'AH00489: Apache/2.4.18 (Ubuntu) configured -- resuming normal operations',
|
||||
'apache2.error.level': 'notice',
|
||||
};
|
||||
const message = format(event);
|
||||
expect(message).toEqual([
|
||||
{
|
||||
constant: '[Apache][',
|
||||
},
|
||||
{
|
||||
field: 'apache2.error.level',
|
||||
highlights: [],
|
||||
value: 'notice',
|
||||
},
|
||||
{
|
||||
constant: '] ',
|
||||
},
|
||||
{
|
||||
field: 'apache2.error.message',
|
||||
highlights: [],
|
||||
value: 'AH00489: Apache/2.4.18 (Ubuntu) configured -- resuming normal operations',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -11,10 +11,7 @@ export const filebeatApache2Rules = [
|
|||
},
|
||||
format: [
|
||||
{
|
||||
constant: 'apache2',
|
||||
},
|
||||
{
|
||||
constant: ' ',
|
||||
constant: '[Apache][access] ',
|
||||
},
|
||||
{
|
||||
field: 'apache2.access.remote_ip',
|
||||
|
@ -57,4 +54,23 @@ export const filebeatApache2Rules = [
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
when: {
|
||||
exists: ['apache2.error.message'],
|
||||
},
|
||||
format: [
|
||||
{
|
||||
constant: '[Apache][',
|
||||
},
|
||||
{
|
||||
field: 'apache2.error.level',
|
||||
},
|
||||
{
|
||||
constant: '] ',
|
||||
},
|
||||
{
|
||||
field: 'apache2.error.message',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* 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 { compileFormattingRules } from '../message';
|
||||
import { filebeatAuditdRules } from './filebeat_auditd';
|
||||
|
||||
const { format } = compileFormattingRules(filebeatAuditdRules);
|
||||
|
||||
describe('Filebeat Rules', () => {
|
||||
test('auditd IPSEC rule', () => {
|
||||
const event = {
|
||||
'@timestamp': '2017-01-31T20:17:14.891Z',
|
||||
'auditd.log.auid': '4294967295',
|
||||
'auditd.log.dst': '192.168.0.0',
|
||||
'auditd.log.dst_prefixlen': '16',
|
||||
'auditd.log.op': 'SPD-delete',
|
||||
'auditd.log.record_type': 'MAC_IPSEC_EVENT',
|
||||
'auditd.log.res': '1',
|
||||
'auditd.log.sequence': 18877201,
|
||||
'auditd.log.ses': '4294967295',
|
||||
'auditd.log.src': '192.168.2.0',
|
||||
'auditd.log.src_prefixlen': '24',
|
||||
'ecs.version': '1.0.0-beta2',
|
||||
'event.dataset': 'auditd.log',
|
||||
'event.module': 'auditd',
|
||||
'fileset.name': 'log',
|
||||
'input.type': 'log',
|
||||
'log.offset': 0,
|
||||
};
|
||||
const message = format(event);
|
||||
expect(message).toEqual([
|
||||
{ constant: '[AuditD][' },
|
||||
{ field: 'auditd.log.record_type', highlights: [], value: 'MAC_IPSEC_EVENT' },
|
||||
{ constant: '] src:' },
|
||||
{ field: 'auditd.log.src', highlights: [], value: '192.168.2.0' },
|
||||
{ constant: ' dst:' },
|
||||
{ field: 'auditd.log.dst', highlights: [], value: '192.168.0.0' },
|
||||
{ constant: ' op:' },
|
||||
{ field: 'auditd.log.op', highlights: [], value: 'SPD-delete' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('AuditD SYSCALL rule', () => {
|
||||
const event = {
|
||||
'@timestamp': '2017-01-31T20:17:14.891Z',
|
||||
'auditd.log.a0': '9',
|
||||
'auditd.log.a1': '7f564b2672a0',
|
||||
'auditd.log.a2': 'b8',
|
||||
'auditd.log.a3': '0',
|
||||
'auditd.log.arch': 'x86_64',
|
||||
'auditd.log.auid': '4294967295',
|
||||
'auditd.log.comm': 'charon',
|
||||
'auditd.log.egid': '0',
|
||||
'auditd.log.euid': '0',
|
||||
'auditd.log.exe': '/usr/libexec/strongswan/charon (deleted)',
|
||||
'auditd.log.exit': '184',
|
||||
'auditd.log.fsgid': '0',
|
||||
'auditd.log.fsuid': '0',
|
||||
'auditd.log.gid': '0',
|
||||
'auditd.log.items': '0',
|
||||
'auditd.log.pid': '1281',
|
||||
'auditd.log.ppid': '1240',
|
||||
'auditd.log.record_type': 'SYSCALL',
|
||||
'auditd.log.sequence': 18877199,
|
||||
'auditd.log.ses': '4294967295',
|
||||
'auditd.log.sgid': '0',
|
||||
'auditd.log.success': 'yes',
|
||||
'auditd.log.suid': '0',
|
||||
'auditd.log.syscall': '44',
|
||||
'auditd.log.tty': '(none)',
|
||||
'auditd.log.uid': '0',
|
||||
'ecs.version': '1.0.0-beta2',
|
||||
'event.dataset': 'auditd.log',
|
||||
'event.module': 'auditd',
|
||||
'fileset.name': 'log',
|
||||
'input.type': 'log',
|
||||
'log.offset': 174,
|
||||
};
|
||||
const message = format(event);
|
||||
expect(message).toEqual([
|
||||
{ constant: '[AuditD][' },
|
||||
{ field: 'auditd.log.record_type', highlights: [], value: 'SYSCALL' },
|
||||
{ constant: '] exe:' },
|
||||
{
|
||||
field: 'auditd.log.exe',
|
||||
highlights: [],
|
||||
value: '/usr/libexec/strongswan/charon (deleted)',
|
||||
},
|
||||
{ constant: ' gid:' },
|
||||
{ field: 'auditd.log.gid', highlights: [], value: '0' },
|
||||
{ constant: ' uid:' },
|
||||
{ field: 'auditd.log.uid', highlights: [], value: '0' },
|
||||
{ constant: ' tty:' },
|
||||
{ field: 'auditd.log.tty', highlights: [], value: '(none)' },
|
||||
{ constant: ' pid:' },
|
||||
{ field: 'auditd.log.pid', highlights: [], value: '1281' },
|
||||
{ constant: ' ppid:' },
|
||||
{ field: 'auditd.log.ppid', highlights: [], value: '1240' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('AuditD events with msg rule', () => {
|
||||
const event = {
|
||||
'@timestamp': '2017-01-31T20:17:14.891Z',
|
||||
'auditd.log.auid': '4294967295',
|
||||
'auditd.log.record_type': 'EXAMPLE',
|
||||
'auditd.log.msg': 'some kind of message',
|
||||
'ecs.version': '1.0.0-beta2',
|
||||
'event.dataset': 'auditd.log',
|
||||
'event.module': 'auditd',
|
||||
'fileset.name': 'log',
|
||||
'input.type': 'log',
|
||||
'log.offset': 174,
|
||||
};
|
||||
const message = format(event);
|
||||
expect(message).toEqual([
|
||||
{ constant: '[AuditD][' },
|
||||
{ field: 'auditd.log.record_type', highlights: [], value: 'EXAMPLE' },
|
||||
{ constant: '] ' },
|
||||
{
|
||||
field: 'auditd.log.msg',
|
||||
highlights: [],
|
||||
value: 'some kind of message',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('AuditD catchall rule', () => {
|
||||
const event = {
|
||||
'@timestamp': '2017-01-31T20:17:14.891Z',
|
||||
'auditd.log.auid': '4294967295',
|
||||
'auditd.log.record_type': 'EXAMPLE',
|
||||
'ecs.version': '1.0.0-beta2',
|
||||
'event.dataset': 'auditd.log',
|
||||
'event.module': 'auditd',
|
||||
'fileset.name': 'log',
|
||||
'input.type': 'log',
|
||||
'log.offset': 174,
|
||||
};
|
||||
const message = format(event);
|
||||
expect(message).toEqual([
|
||||
{ constant: '[AuditD][' },
|
||||
{ field: 'auditd.log.record_type', highlights: [], value: 'EXAMPLE' },
|
||||
{ constant: '] Event without message.' },
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 const filebeatAuditdRules = [
|
||||
// IPSEC_EVENT Rule
|
||||
{
|
||||
when: {
|
||||
exists: ['auditd.log.record_type', 'auditd.log.src', 'auditd.log.dst', 'auditd.log.op'],
|
||||
values: {
|
||||
'auditd.log.record_type': 'MAC_IPSEC_EVENT',
|
||||
},
|
||||
},
|
||||
format: [
|
||||
{ constant: '[AuditD][' },
|
||||
{ field: 'auditd.log.record_type' },
|
||||
{ constant: '] src:' },
|
||||
{ field: 'auditd.log.src' },
|
||||
{ constant: ' dst:' },
|
||||
{ field: 'auditd.log.dst' },
|
||||
{ constant: ' op:' },
|
||||
{ field: 'auditd.log.op' },
|
||||
],
|
||||
},
|
||||
// SYSCALL Rule
|
||||
{
|
||||
when: {
|
||||
exists: [
|
||||
'auditd.log.record_type',
|
||||
'auditd.log.exe',
|
||||
'auditd.log.gid',
|
||||
'auditd.log.uid',
|
||||
'auditd.log.tty',
|
||||
'auditd.log.pid',
|
||||
'auditd.log.ppid',
|
||||
],
|
||||
values: {
|
||||
'auditd.log.record_type': 'SYSCALL',
|
||||
},
|
||||
},
|
||||
format: [
|
||||
{ constant: '[AuditD][' },
|
||||
{ field: 'auditd.log.record_type' },
|
||||
{ constant: '] exe:' },
|
||||
{ field: 'auditd.log.exe' },
|
||||
{ constant: ' gid:' },
|
||||
{ field: 'auditd.log.gid' },
|
||||
{ constant: ' uid:' },
|
||||
{ field: 'auditd.log.uid' },
|
||||
{ constant: ' tty:' },
|
||||
{ field: 'auditd.log.tty' },
|
||||
{ constant: ' pid:' },
|
||||
{ field: 'auditd.log.pid' },
|
||||
{ constant: ' ppid:' },
|
||||
{ field: 'auditd.log.ppid' },
|
||||
],
|
||||
},
|
||||
// Events with `msg` Rule
|
||||
{
|
||||
when: {
|
||||
exists: ['auditd.log.record_type', 'auditd.log.msg'],
|
||||
},
|
||||
format: [
|
||||
{ constant: '[AuditD][' },
|
||||
{ field: 'auditd.log.record_type' },
|
||||
{ constant: '] ' },
|
||||
{ field: 'auditd.log.msg' },
|
||||
],
|
||||
},
|
||||
// Events with `msg` Rule
|
||||
{
|
||||
when: {
|
||||
exists: ['auditd.log.record_type'],
|
||||
},
|
||||
format: [
|
||||
{ constant: '[AuditD][' },
|
||||
{ field: 'auditd.log.record_type' },
|
||||
{ constant: '] Event without message.' },
|
||||
],
|
||||
},
|
||||
];
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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 { compileFormattingRules } from '../message';
|
||||
import { filebeatMySQLRules } from './filebeat_mysql';
|
||||
|
||||
const { format } = compileFormattingRules(filebeatMySQLRules);
|
||||
|
||||
describe('Filebeat Rules', () => {
|
||||
test('mysql error log', () => {
|
||||
const errorDoc = {
|
||||
'mysql.error.message':
|
||||
"Access denied for user 'petclinicdd'@'47.153.152.234' (using password: YES)",
|
||||
};
|
||||
const message = format(errorDoc);
|
||||
expect(message).toEqual([
|
||||
{
|
||||
constant: '[MySQL][error] ',
|
||||
},
|
||||
{
|
||||
field: 'mysql.error.message',
|
||||
highlights: [],
|
||||
value: "Access denied for user 'petclinicdd'@'47.153.152.234' (using password: YES)",
|
||||
},
|
||||
]);
|
||||
});
|
||||
test('mysql slow log', () => {
|
||||
const errorDoc = {
|
||||
'mysql.slowlog.query': 'select * from hosts',
|
||||
'mysql.slowlog.query_time.sec': 5,
|
||||
'mysql.slowlog.user': 'admin',
|
||||
'mysql.slowlog.ip': '192.168.1.42',
|
||||
'mysql.slowlog.host': 'webserver-01',
|
||||
};
|
||||
const message = format(errorDoc);
|
||||
expect(message).toEqual([
|
||||
{
|
||||
constant: '[MySQL][slowlog] ',
|
||||
},
|
||||
{
|
||||
field: 'mysql.slowlog.user',
|
||||
highlights: [],
|
||||
value: 'admin',
|
||||
},
|
||||
{
|
||||
constant: '@',
|
||||
},
|
||||
{
|
||||
field: 'mysql.slowlog.host',
|
||||
highlights: [],
|
||||
value: 'webserver-01',
|
||||
},
|
||||
{
|
||||
constant: ' [',
|
||||
},
|
||||
{
|
||||
field: 'mysql.slowlog.ip',
|
||||
highlights: [],
|
||||
value: '192.168.1.42',
|
||||
},
|
||||
{
|
||||
constant: '] ',
|
||||
},
|
||||
{
|
||||
constant: ' - ',
|
||||
},
|
||||
{
|
||||
field: 'mysql.slowlog.query_time.sec',
|
||||
highlights: [],
|
||||
value: '5',
|
||||
},
|
||||
{
|
||||
constant: 'sec - ',
|
||||
},
|
||||
{
|
||||
field: 'mysql.slowlog.query',
|
||||
highlights: [],
|
||||
value: 'select * from hosts',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 const filebeatMySQLRules = [
|
||||
{
|
||||
when: {
|
||||
exists: ['mysql.error.message'],
|
||||
},
|
||||
format: [
|
||||
{
|
||||
constant: '[MySQL][error] ',
|
||||
},
|
||||
{
|
||||
field: 'mysql.error.message',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
when: {
|
||||
exists: ['mysql.slowlog.user', 'mysql.slowlog.query_time.sec', 'mysql.slowlog.query'],
|
||||
},
|
||||
format: [
|
||||
{
|
||||
constant: '[MySQL][slowlog] ',
|
||||
},
|
||||
{
|
||||
field: 'mysql.slowlog.user',
|
||||
},
|
||||
{
|
||||
constant: '@',
|
||||
},
|
||||
{
|
||||
field: 'mysql.slowlog.host',
|
||||
},
|
||||
{
|
||||
constant: ' [',
|
||||
},
|
||||
{
|
||||
field: 'mysql.slowlog.ip',
|
||||
},
|
||||
{
|
||||
constant: '] ',
|
||||
},
|
||||
{
|
||||
constant: ' - ',
|
||||
},
|
||||
{
|
||||
field: 'mysql.slowlog.query_time.sec',
|
||||
},
|
||||
{
|
||||
constant: 'sec - ',
|
||||
},
|
||||
{
|
||||
field: 'mysql.slowlog.query',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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 { compileFormattingRules } from '../message';
|
||||
import { filebeatNginxRules } from './filebeat_nginx';
|
||||
|
||||
const { format } = compileFormattingRules(filebeatNginxRules);
|
||||
describe('Filebeat Rules', () => {
|
||||
test('Nginx Access Rule', () => {
|
||||
const event = {
|
||||
'nginx.access': true,
|
||||
'nginx.access.remote_ip': '192.168.1.42',
|
||||
'nginx.access.user_name': 'admin',
|
||||
'nginx.access.method': 'GET',
|
||||
'nginx.access.url': '/faq',
|
||||
'nginx.access.http_version': '1.1',
|
||||
'nginx.access.body_sent.bytes': 1024,
|
||||
'nginx.access.response_code': 200,
|
||||
};
|
||||
const message = format(event);
|
||||
expect(message).toEqual([
|
||||
{
|
||||
constant: '[Nginx][access] ',
|
||||
},
|
||||
{
|
||||
field: 'nginx.access.remote_ip',
|
||||
highlights: [],
|
||||
value: '192.168.1.42',
|
||||
},
|
||||
{
|
||||
constant: ' ',
|
||||
},
|
||||
{
|
||||
field: 'nginx.access.user_name',
|
||||
highlights: [],
|
||||
value: 'admin',
|
||||
},
|
||||
{
|
||||
constant: ' "',
|
||||
},
|
||||
{
|
||||
field: 'nginx.access.method',
|
||||
highlights: [],
|
||||
value: 'GET',
|
||||
},
|
||||
{
|
||||
constant: ' ',
|
||||
},
|
||||
{
|
||||
field: 'nginx.access.url',
|
||||
highlights: [],
|
||||
value: '/faq',
|
||||
},
|
||||
{
|
||||
constant: ' HTTP/',
|
||||
},
|
||||
{
|
||||
field: 'nginx.access.http_version',
|
||||
highlights: [],
|
||||
value: '1.1',
|
||||
},
|
||||
{
|
||||
constant: '" ',
|
||||
},
|
||||
{
|
||||
field: 'nginx.access.response_code',
|
||||
highlights: [],
|
||||
value: '200',
|
||||
},
|
||||
{
|
||||
constant: ' ',
|
||||
},
|
||||
{
|
||||
field: 'nginx.access.body_sent.bytes',
|
||||
highlights: [],
|
||||
value: '1024',
|
||||
},
|
||||
]);
|
||||
});
|
||||
test('Nginx Access Rule', () => {
|
||||
const event = {
|
||||
'nginx.error.message':
|
||||
'connect() failed (111: Connection refused) while connecting to upstream, client: 127.0.0.1, server: localhost, request: "GET /php-status?json= HTTP/1.1", upstream: "fastcgi://[::1]:9000", host: "localhost"',
|
||||
'nginx.error.level': 'error',
|
||||
};
|
||||
const message = format(event);
|
||||
expect(message).toEqual([
|
||||
{
|
||||
constant: '[Nginx]',
|
||||
},
|
||||
{
|
||||
constant: '[',
|
||||
},
|
||||
{
|
||||
field: 'nginx.error.level',
|
||||
highlights: [],
|
||||
value: 'error',
|
||||
},
|
||||
{
|
||||
constant: '] ',
|
||||
},
|
||||
{
|
||||
field: 'nginx.error.message',
|
||||
highlights: [],
|
||||
value:
|
||||
'connect() failed (111: Connection refused) while connecting to upstream, client: 127.0.0.1, server: localhost, request: "GET /php-status?json= HTTP/1.1", upstream: "fastcgi://[::1]:9000", host: "localhost"',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -11,10 +11,7 @@ export const filebeatNginxRules = [
|
|||
},
|
||||
format: [
|
||||
{
|
||||
constant: 'nginx',
|
||||
},
|
||||
{
|
||||
constant: ' ',
|
||||
constant: '[Nginx][access] ',
|
||||
},
|
||||
{
|
||||
field: 'nginx.access.remote_ip',
|
||||
|
@ -57,4 +54,26 @@ export const filebeatNginxRules = [
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
when: {
|
||||
exists: ['nginx.error.message'],
|
||||
},
|
||||
format: [
|
||||
{
|
||||
constant: '[Nginx]',
|
||||
},
|
||||
{
|
||||
constant: '[',
|
||||
},
|
||||
{
|
||||
field: 'nginx.error.level',
|
||||
},
|
||||
{
|
||||
constant: '] ',
|
||||
},
|
||||
{
|
||||
field: 'nginx.error.message',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -11,10 +11,16 @@ export const filebeatRedisRules = [
|
|||
},
|
||||
format: [
|
||||
{
|
||||
constant: 'redis',
|
||||
constant: '[Redis]',
|
||||
},
|
||||
{
|
||||
constant: ' ',
|
||||
constant: '[',
|
||||
},
|
||||
{
|
||||
field: 'redis.log.level',
|
||||
},
|
||||
{
|
||||
constant: '] ',
|
||||
},
|
||||
{
|
||||
field: 'redis.log.message',
|
||||
|
|
|
@ -10,6 +10,15 @@ export const filebeatSystemRules = [
|
|||
exists: ['system.syslog.message'],
|
||||
},
|
||||
format: [
|
||||
{
|
||||
constant: '[System][syslog] ',
|
||||
},
|
||||
{
|
||||
field: 'system.syslog.program',
|
||||
},
|
||||
{
|
||||
constant: ' - ',
|
||||
},
|
||||
{
|
||||
field: 'system.syslog.message',
|
||||
},
|
||||
|
@ -20,6 +29,15 @@ export const filebeatSystemRules = [
|
|||
exists: ['system.auth.message'],
|
||||
},
|
||||
format: [
|
||||
{
|
||||
constant: '[System][auth] ',
|
||||
},
|
||||
{
|
||||
field: 'system.syslog.program',
|
||||
},
|
||||
{
|
||||
constant: ' - ',
|
||||
},
|
||||
{
|
||||
field: 'system.auth.message',
|
||||
},
|
||||
|
@ -31,7 +49,7 @@ export const filebeatSystemRules = [
|
|||
},
|
||||
format: [
|
||||
{
|
||||
constant: 'ssh',
|
||||
constant: '[System][auth][ssh]',
|
||||
},
|
||||
{
|
||||
constant: ' ',
|
||||
|
@ -59,7 +77,7 @@ export const filebeatSystemRules = [
|
|||
},
|
||||
format: [
|
||||
{
|
||||
constant: 'ssh',
|
||||
constant: '[System][auth][ssh]',
|
||||
},
|
||||
{
|
||||
constant: ' Dropped connection from ',
|
||||
|
|
|
@ -5,9 +5,12 @@
|
|||
*/
|
||||
|
||||
import { filebeatApache2Rules } from './filebeat_apache2';
|
||||
import { filebeatAuditdRules } from './filebeat_auditd';
|
||||
import { filebeatMySQLRules } from './filebeat_mysql';
|
||||
import { filebeatNginxRules } from './filebeat_nginx';
|
||||
import { filebeatRedisRules } from './filebeat_redis';
|
||||
import { filebeatSystemRules } from './filebeat_system';
|
||||
|
||||
import { genericRules } from './generic';
|
||||
|
||||
export const builtinRules = [
|
||||
|
@ -15,6 +18,8 @@ export const builtinRules = [
|
|||
...filebeatNginxRules,
|
||||
...filebeatRedisRules,
|
||||
...filebeatSystemRules,
|
||||
...filebeatMySQLRules,
|
||||
...filebeatAuditdRules,
|
||||
...genericRules,
|
||||
{
|
||||
when: {
|
||||
|
|
|
@ -164,7 +164,7 @@ export interface LogEntryDocument {
|
|||
}
|
||||
|
||||
export interface LogEntryDocumentFields {
|
||||
[fieldName: string]: string | number | null;
|
||||
[fieldName: string]: string | number | boolean | null;
|
||||
}
|
||||
|
||||
const convertLogDocumentToEntry = (
|
||||
|
|
|
@ -23,6 +23,8 @@ import {
|
|||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { getColumns } from './anomalies_table_columns';
|
||||
|
||||
import { AnomalyDetails } from './anomaly_details';
|
||||
|
@ -158,7 +160,12 @@ class AnomaliesTable extends Component {
|
|||
<EuiFlexGroup justifyContent="spaceAround">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<h4>No matching anomalies found</h4>
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.anomaliesTable.noMatchingAnomaliesFoundTitle"
|
||||
defaultMessage="No matching anomalies found"
|
||||
/>
|
||||
</h4>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -16,6 +16,8 @@ import {
|
|||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
formatHumanReadableDate,
|
||||
formatHumanReadableDateTime,
|
||||
|
@ -74,14 +76,20 @@ export function getColumns(
|
|||
<EuiButtonIcon
|
||||
onClick={() => toggleRow(item)}
|
||||
iconType={itemIdToExpandedRowMap[item.rowId] ? 'arrowDown' : 'arrowRight'}
|
||||
aria-label={itemIdToExpandedRowMap[item.rowId] ? 'Hide details' : 'Show details'}
|
||||
aria-label={itemIdToExpandedRowMap[item.rowId] ? i18n.translate('xpack.ml.anomaliesTable.hideDetailsAriaLabel', {
|
||||
defaultMessage: 'Hide details',
|
||||
}) : i18n.translate('xpack.ml.anomaliesTable.showDetailsAriaLabel', {
|
||||
defaultMessage: 'Show details',
|
||||
})}
|
||||
data-row-id={item.rowId}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
field: 'time',
|
||||
name: 'time',
|
||||
name: i18n.translate('xpack.ml.anomaliesTable.timeColumnName', {
|
||||
defaultMessage: 'time',
|
||||
}),
|
||||
dataType: 'date',
|
||||
render: (date) => renderTime(date, interval),
|
||||
textOnly: true,
|
||||
|
@ -89,7 +97,11 @@ export function getColumns(
|
|||
},
|
||||
{
|
||||
field: 'severity',
|
||||
name: `${(isAggregatedData === true) ? 'max ' : ''}severity`,
|
||||
name: isAggregatedData === true ? i18n.translate('xpack.ml.anomaliesTable.maxSeverityColumnName', {
|
||||
defaultMessage: 'max severity',
|
||||
}) : i18n.translate('xpack.ml.anomaliesTable.severityColumnName', {
|
||||
defaultMessage: 'severity',
|
||||
}),
|
||||
render: (score) => (
|
||||
<EuiHealth color={getSeverityColor(score)} compressed="true">
|
||||
{score >= 1 ? Math.floor(score) : '< 1'}
|
||||
|
@ -99,7 +111,9 @@ export function getColumns(
|
|||
},
|
||||
{
|
||||
field: 'detector',
|
||||
name: 'detector',
|
||||
name: i18n.translate('xpack.ml.anomaliesTable.detectorColumnName', {
|
||||
defaultMessage: 'detector',
|
||||
}),
|
||||
render: (detectorDescription, item) => (
|
||||
<DetectorCell
|
||||
detectorDescription={detectorDescription}
|
||||
|
@ -114,7 +128,9 @@ export function getColumns(
|
|||
if (items.some(item => item.entityValue !== undefined)) {
|
||||
columns.push({
|
||||
field: 'entityValue',
|
||||
name: 'found for',
|
||||
name: i18n.translate('xpack.ml.anomaliesTable.entityValueColumnName', {
|
||||
defaultMessage: 'found for',
|
||||
}),
|
||||
render: (entityValue, item) => (
|
||||
<EntityCell
|
||||
entityName={item.entityName}
|
||||
|
@ -130,7 +146,9 @@ export function getColumns(
|
|||
if (items.some(item => item.influencers !== undefined)) {
|
||||
columns.push({
|
||||
field: 'influencers',
|
||||
name: 'influenced by',
|
||||
name: i18n.translate('xpack.ml.anomaliesTable.influencersColumnName', {
|
||||
defaultMessage: 'influenced by',
|
||||
}),
|
||||
render: (influencers) => (
|
||||
<InfluencersCell
|
||||
limit={INFLUENCERS_LIMIT}
|
||||
|
@ -148,7 +166,9 @@ export function getColumns(
|
|||
if (items.some(item => item.actual !== undefined)) {
|
||||
columns.push({
|
||||
field: 'actualSort',
|
||||
name: 'actual',
|
||||
name: i18n.translate('xpack.ml.anomaliesTable.actualSortColumnName', {
|
||||
defaultMessage: 'actual',
|
||||
}),
|
||||
render: (actual, item) => {
|
||||
const fieldFormat = mlFieldFormatService.getFieldFormat(item.jobId, item.source.detector_index);
|
||||
return formatValue(item.actual, item.source.function, fieldFormat);
|
||||
|
@ -160,7 +180,9 @@ export function getColumns(
|
|||
if (items.some(item => item.typical !== undefined)) {
|
||||
columns.push({
|
||||
field: 'typicalSort',
|
||||
name: 'typical',
|
||||
name: i18n.translate('xpack.ml.anomaliesTable.typicalSortColumnName', {
|
||||
defaultMessage: 'typical',
|
||||
}),
|
||||
render: (typical, item) => {
|
||||
const fieldFormat = mlFieldFormatService.getFieldFormat(item.jobId, item.source.detector_index);
|
||||
return formatValue(item.typical, item.source.function, fieldFormat);
|
||||
|
@ -177,7 +199,9 @@ export function getColumns(
|
|||
if (nonTimeOfDayOrWeek === true) {
|
||||
columns.push({
|
||||
field: 'metricDescriptionSort',
|
||||
name: 'description',
|
||||
name: i18n.translate('xpack.ml.anomaliesTable.metricDescriptionSortColumnName', {
|
||||
defaultMessage: 'description',
|
||||
}),
|
||||
render: (metricDescriptionSort, item) => (
|
||||
<DescriptionCell
|
||||
actual={item.actual}
|
||||
|
@ -193,7 +217,9 @@ export function getColumns(
|
|||
if (jobIds && jobIds.length > 1) {
|
||||
columns.push({
|
||||
field: 'jobId',
|
||||
name: 'job ID',
|
||||
name: i18n.translate('xpack.ml.anomaliesTable.jobIdColumnName', {
|
||||
defaultMessage: 'job ID',
|
||||
}),
|
||||
sortable: true
|
||||
});
|
||||
}
|
||||
|
@ -201,7 +227,9 @@ export function getColumns(
|
|||
const showExamples = items.some(item => item.entityName === 'mlcategory');
|
||||
if (showExamples === true) {
|
||||
columns.push({
|
||||
name: 'category examples',
|
||||
name: i18n.translate('xpack.ml.anomaliesTable.categoryExamplesColumnName', {
|
||||
defaultMessage: 'category examples',
|
||||
}),
|
||||
sortable: false,
|
||||
truncateText: true,
|
||||
render: (item) => {
|
||||
|
@ -227,7 +255,9 @@ export function getColumns(
|
|||
|
||||
if (showLinks === true) {
|
||||
columns.push({
|
||||
name: 'actions',
|
||||
name: i18n.translate('xpack.ml.anomaliesTable.actionsColumnName', {
|
||||
defaultMessage: 'actions',
|
||||
}),
|
||||
render: (item) => {
|
||||
if (showLinksMenuForItem(item) === true) {
|
||||
return (
|
||||
|
|
|
@ -9,6 +9,7 @@ import 'ngreact';
|
|||
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { timefilter } from 'ui/timefilter';
|
||||
import { injectI18nProvider } from '@kbn/i18n/react';
|
||||
const module = uiModules.get('apps/ml', ['react']);
|
||||
|
||||
import { AnomaliesTable } from './anomalies_table';
|
||||
|
@ -17,7 +18,7 @@ module.directive('mlAnomaliesTable', function ($injector) {
|
|||
const reactDirective = $injector.get('reactDirective');
|
||||
|
||||
return reactDirective(
|
||||
AnomaliesTable,
|
||||
injectI18nProvider(AnomaliesTable),
|
||||
[
|
||||
['filter', { watchDepth: 'reference' }],
|
||||
['tableData', { watchDepth: 'reference' }]
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import _ from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
EuiDescriptionList,
|
||||
|
@ -113,21 +115,33 @@ function getDetailsItems(anomaly, examples, filter) {
|
|||
let timeDesc = `${formatHumanReadableDateTimeSeconds(anomalyTime)}`;
|
||||
if (source.bucket_span !== undefined) {
|
||||
const anomalyEndTime = anomalyTime + (source.bucket_span * 1000);
|
||||
timeDesc += ` to ${formatHumanReadableDateTimeSeconds(anomalyEndTime)}`;
|
||||
timeDesc = i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.anomalyTimeRangeLabel', {
|
||||
defaultMessage: '{anomalyTime} to {anomalyEndTime}',
|
||||
values: {
|
||||
anomalyTime: formatHumanReadableDateTimeSeconds(anomalyTime),
|
||||
anomalyEndTime: formatHumanReadableDateTimeSeconds(anomalyEndTime),
|
||||
}
|
||||
});
|
||||
}
|
||||
items.push({
|
||||
title: 'time',
|
||||
title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.timeTitle', {
|
||||
defaultMessage: 'time',
|
||||
}),
|
||||
description: timeDesc
|
||||
});
|
||||
|
||||
items.push({
|
||||
title: 'function',
|
||||
title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.functionTitle', {
|
||||
defaultMessage: 'function',
|
||||
}),
|
||||
description: (source.function !== 'metric') ? source.function : source.function_description
|
||||
});
|
||||
|
||||
if (source.field_name !== undefined) {
|
||||
items.push({
|
||||
title: 'fieldName',
|
||||
title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.fieldNameTitle', {
|
||||
defaultMessage: 'fieldName',
|
||||
}),
|
||||
description: source.field_name
|
||||
});
|
||||
}
|
||||
|
@ -135,33 +149,43 @@ function getDetailsItems(anomaly, examples, filter) {
|
|||
const functionDescription = source.function_description || '';
|
||||
if (anomaly.actual !== undefined && showActualForFunction(functionDescription) === true) {
|
||||
items.push({
|
||||
title: 'actual',
|
||||
title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.actualTitle', {
|
||||
defaultMessage: 'actual',
|
||||
}),
|
||||
description: formatValue(anomaly.actual, source.function)
|
||||
});
|
||||
}
|
||||
|
||||
if (anomaly.typical !== undefined && showTypicalForFunction(functionDescription) === true) {
|
||||
items.push({
|
||||
title: 'typical',
|
||||
title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.typicalTitle', {
|
||||
defaultMessage: 'typical',
|
||||
}),
|
||||
description: formatValue(anomaly.typical, source.function)
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
title: 'job ID',
|
||||
title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.jobIdTitle', {
|
||||
defaultMessage: 'job ID',
|
||||
}),
|
||||
description: anomaly.jobId
|
||||
});
|
||||
|
||||
if (source.multi_bucket_impact !== undefined &&
|
||||
source.multi_bucket_impact >= MULTI_BUCKET_IMPACT.LOW) {
|
||||
items.push({
|
||||
title: 'multi-bucket impact',
|
||||
title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.multiBucketImpactTitle', {
|
||||
defaultMessage: 'multi-bucket impact',
|
||||
}),
|
||||
description: getMultiBucketImpactLabel(source.multi_bucket_impact)
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
title: 'probability',
|
||||
title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.probabilityTitle', {
|
||||
defaultMessage: 'probability',
|
||||
}),
|
||||
description: source.probability
|
||||
});
|
||||
|
||||
|
@ -169,9 +193,22 @@ function getDetailsItems(anomaly, examples, filter) {
|
|||
// will already have been added for display.
|
||||
if (causes.length > 1) {
|
||||
causes.forEach((cause, index) => {
|
||||
const title = (index === 0) ? `${cause.entityName} values` : '';
|
||||
let description = `${cause.entityValue} (actual ${formatValue(cause.actual, source.function)}, `;
|
||||
description += `typical ${formatValue(cause.typical, source.function)}, probability ${cause.probability})`;
|
||||
const title = (index === 0) ? i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.causeValuesTitle', {
|
||||
defaultMessage: '{causeEntityName} values',
|
||||
values: {
|
||||
causeEntityName: cause.entityName,
|
||||
}
|
||||
}) : '';
|
||||
const description = i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.causeValuesDescription', {
|
||||
defaultMessage: '{causeEntityValue} (actual {actualValue}, ' +
|
||||
'typical {typicalValue}, probability {probabilityValue})',
|
||||
values: {
|
||||
causeEntityValue: cause.entityValue,
|
||||
actualValue: formatValue(cause.actual, source.function),
|
||||
typicalValue: formatValue(cause.typical, source.function),
|
||||
probabilityValue: cause.probability,
|
||||
}
|
||||
});
|
||||
items.push({ title, description });
|
||||
});
|
||||
}
|
||||
|
@ -190,7 +227,9 @@ export class AnomalyDetails extends Component {
|
|||
if (this.props.examples !== undefined && this.props.examples.length > 0) {
|
||||
this.tabs = [{
|
||||
id: 'Details',
|
||||
name: 'Details',
|
||||
name: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.detailsTitle', {
|
||||
defaultMessage: 'Details',
|
||||
}),
|
||||
content: (
|
||||
<Fragment>
|
||||
<div className="ml-anomalies-table-details">
|
||||
|
@ -204,7 +243,9 @@ export class AnomalyDetails extends Component {
|
|||
},
|
||||
{
|
||||
id: 'Category examples',
|
||||
name: 'Category examples',
|
||||
name: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.categoryExamplesTitle', {
|
||||
defaultMessage: 'Category examples',
|
||||
}),
|
||||
content: (
|
||||
<Fragment>
|
||||
{this.renderCategoryExamples()}
|
||||
|
@ -289,28 +330,58 @@ export class AnomalyDetails extends Component {
|
|||
const anomaly = this.props.anomaly;
|
||||
const source = anomaly.source;
|
||||
|
||||
let anomalyDescription = `${getSeverity(anomaly.severity)} anomaly in ${anomaly.detector}`;
|
||||
let anomalyDescription = i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.anomalyInLabel', {
|
||||
defaultMessage: '{anomalySeverity} anomaly in {anomalyDetector}',
|
||||
values: {
|
||||
anomalySeverity: getSeverity(anomaly.severity),
|
||||
anomalyDetector: anomaly.detector,
|
||||
}
|
||||
});
|
||||
if (anomaly.entityName !== undefined) {
|
||||
anomalyDescription += ` found for ${anomaly.entityName} ${anomaly.entityValue}`;
|
||||
anomalyDescription += i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.foundForLabel', {
|
||||
defaultMessage: ' found for {anomalyEntityName} {anomalyEntityValue}',
|
||||
values: {
|
||||
anomalyEntityName: anomaly.entityName,
|
||||
anomalyEntityValue: anomaly.entityValue,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ((source.partition_field_name !== undefined) &&
|
||||
(source.partition_field_name !== anomaly.entityName)) {
|
||||
anomalyDescription += ` detected in ${source.partition_field_name}`;
|
||||
anomalyDescription += ` ${source.partition_field_value}`;
|
||||
anomalyDescription += i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.detectedInLabel', {
|
||||
defaultMessage: ' detected in {sourcePartitionFieldName} {sourcePartitionFieldValue}',
|
||||
values: {
|
||||
sourcePartitionFieldName: source.partition_field_name,
|
||||
sourcePartitionFieldValue: source.partition_field_value,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check for a correlatedByFieldValue in the source which will be present for multivariate analyses
|
||||
// where the record is anomalous due to relationship with another 'by' field value.
|
||||
let mvDescription = undefined;
|
||||
if (source.correlated_by_field_value !== undefined) {
|
||||
mvDescription = `multivariate correlations found in ${source.by_field_name}; `;
|
||||
mvDescription += `${source.by_field_value} is considered anomalous given ${source.correlated_by_field_value}`;
|
||||
mvDescription = i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.multivariateDescription', {
|
||||
defaultMessage: 'multivariate correlations found in {sourceByFieldName}; ' +
|
||||
'{sourceByFieldValue} is considered anomalous given {sourceCorrelatedByFieldValue}',
|
||||
values: {
|
||||
sourceByFieldName: source.by_field_name,
|
||||
sourceByFieldValue: source.by_field_value,
|
||||
sourceCorrelatedByFieldValue: source.correlated_by_field_value,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<EuiText size="xs">
|
||||
<h4>Description</h4>
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.anomaliesTable.anomalyDetails.descriptionTitle"
|
||||
defaultMessage="Description"
|
||||
/>
|
||||
</h4>
|
||||
{anomalyDescription}
|
||||
</EuiText>
|
||||
{(mvDescription !== undefined) &&
|
||||
|
@ -329,13 +400,29 @@ export class AnomalyDetails extends Component {
|
|||
<React.Fragment>
|
||||
<EuiText size="xs">
|
||||
{this.props.isAggregatedData === true ? (
|
||||
<h4>Details on highest severity anomaly</h4>
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.anomaliesTable.anomalyDetails.detailsOnHighestSeverityAnomalyTitle"
|
||||
defaultMessage="Details on highest severity anomaly"
|
||||
/>
|
||||
</h4>
|
||||
) : (
|
||||
<h4>Anomaly details</h4>
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.anomaliesTable.anomalyDetails.anomalyDetailsTitle"
|
||||
defaultMessage="Anomaly details"
|
||||
/>
|
||||
</h4>
|
||||
)}
|
||||
{isInterimResult === true &&
|
||||
<React.Fragment>
|
||||
<EuiIcon type="alert"/><span className="interim-result">Interim result</span>
|
||||
<EuiIcon type="alert"/>
|
||||
<span className="interim-result">
|
||||
<FormattedMessage
|
||||
id="xpack.ml.anomaliesTable.anomalyDetails.interimResultLabel"
|
||||
defaultMessage="Interim result"
|
||||
/>
|
||||
</span>
|
||||
</React.Fragment>
|
||||
}
|
||||
</EuiText>
|
||||
|
@ -379,7 +466,12 @@ export class AnomalyDetails extends Component {
|
|||
<React.Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiText size="xs">
|
||||
<h4>Influencers</h4>
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.anomaliesTable.anomalyDetails.influencersTitle"
|
||||
defaultMessage="Influencers"
|
||||
/>
|
||||
</h4>
|
||||
</EuiText>
|
||||
<EuiDescriptionList
|
||||
type="column"
|
||||
|
@ -390,14 +482,21 @@ export class AnomalyDetails extends Component {
|
|||
<EuiLink
|
||||
onClick={() => this.toggleAllInfluencers()}
|
||||
>
|
||||
and {othersCount} more
|
||||
<FormattedMessage
|
||||
id="xpack.ml.anomaliesTable.anomalyDetails.anomalyDescriptionListMoreLinkText"
|
||||
defaultMessage="and {othersCount} more"
|
||||
values={{ othersCount }}
|
||||
/>
|
||||
</EuiLink>
|
||||
}
|
||||
{numToDisplay > (this.props.influencersLimit + 1) &&
|
||||
<EuiLink
|
||||
onClick={() => this.toggleAllInfluencers()}
|
||||
>
|
||||
show less
|
||||
<FormattedMessage
|
||||
id="xpack.ml.anomaliesTable.anomalyDetails.anomalyDescriptionShowLessLinkText"
|
||||
defaultMessage="show less"
|
||||
/>
|
||||
</EuiLink>
|
||||
}
|
||||
</React.Fragment>
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
EuiIcon,
|
||||
EuiToolTip
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
/*
|
||||
* Component for rendering a detector cell in the anomalies table, displaying the
|
||||
|
@ -21,7 +22,12 @@ export function DetectorCell({ detectorDescription, numberOfRules }) {
|
|||
let rulesIcon;
|
||||
if (numberOfRules !== undefined && numberOfRules > 0) {
|
||||
rulesIcon = (
|
||||
<EuiToolTip content="rules have been configured for this detector">
|
||||
<EuiToolTip
|
||||
content={<FormattedMessage
|
||||
id="xpack.ml.anomaliesTable.detectorCell.rulesConfiguredTooltip"
|
||||
defaultMessage="rules have been configured for this detector"
|
||||
/>}
|
||||
>
|
||||
<EuiIcon
|
||||
type="controlsHorizontal"
|
||||
className="detector-rules-icon"
|
||||
|
|
|
@ -12,44 +12,61 @@ import {
|
|||
EuiButtonIcon,
|
||||
EuiToolTip
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
|
||||
|
||||
/*
|
||||
* Component for rendering an entity cell in the anomalies table, displaying the value
|
||||
* of the 'partition', 'by' or 'over' field, and optionally links for adding or removing
|
||||
* a filter on this entity.
|
||||
*/
|
||||
export function EntityCell({ entityName, entityValue, filter }) {
|
||||
export const EntityCell = injectI18n(function EntityCell({ entityName, entityValue, filter, intl }) {
|
||||
const valueText = (entityName !== 'mlcategory') ? entityValue : `mlcategory ${entityValue}`;
|
||||
return (
|
||||
<React.Fragment>
|
||||
{valueText}
|
||||
{filter !== undefined && entityName !== undefined && entityValue !== undefined &&
|
||||
<React.Fragment>
|
||||
<EuiToolTip content="Add filter">
|
||||
<EuiToolTip
|
||||
content={<FormattedMessage
|
||||
id="xpack.ml.anomaliesTable.entityCell.addFilterTooltip"
|
||||
defaultMessage="Add filter"
|
||||
/>}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
size="xs"
|
||||
className="filter-button"
|
||||
onClick={() => filter(entityName, entityValue, '+')}
|
||||
iconType="plusInCircle"
|
||||
aria-label="Add filter"
|
||||
aria-label={intl.formatMessage({
|
||||
id: 'xpack.ml.anomaliesTable.entityCell.addFilterAriaLabel',
|
||||
defaultMessage: 'Add filter'
|
||||
})}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
<EuiToolTip content="Remove filter">
|
||||
<EuiToolTip
|
||||
content={<FormattedMessage
|
||||
id="xpack.ml.anomaliesTable.entityCell.removeFilterTooltip"
|
||||
defaultMessage="Remove filter"
|
||||
/>}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
size="xs"
|
||||
className="filter-button"
|
||||
onClick={() => filter(entityName, entityValue, '-')}
|
||||
iconType="minusInCircle"
|
||||
aria-label="Remove filter"
|
||||
aria-label={intl.formatMessage({
|
||||
id: 'xpack.ml.anomaliesTable.entityCell.removeFilterAriaLabel',
|
||||
defaultMessage: 'Remove filter'
|
||||
})}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</React.Fragment>
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
EntityCell.propTypes = {
|
||||
EntityCell.WrappedComponent.propTypes = {
|
||||
entityName: PropTypes.string,
|
||||
entityValue: PropTypes.any,
|
||||
filter: PropTypes.func
|
||||
|
|
|
@ -10,6 +10,8 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
|
||||
/*
|
||||
* Component for rendering a list of record influencers inside a cell in the anomalies table.
|
||||
|
@ -60,7 +62,13 @@ export class InfluencersCell extends Component {
|
|||
<EuiLink
|
||||
onClick={() => this.toggleAllInfluencers()}
|
||||
>
|
||||
and {othersCount} more
|
||||
<FormattedMessage
|
||||
id="xpack.ml.anomaliesTable.influencersCell.moreInfluencersLinkText"
|
||||
defaultMessage="and {othersCount} more"
|
||||
values={{
|
||||
othersCount,
|
||||
}}
|
||||
/>
|
||||
</EuiLink>
|
||||
</div>
|
||||
);
|
||||
|
@ -70,7 +78,10 @@ export class InfluencersCell extends Component {
|
|||
<EuiLink
|
||||
onClick={() => this.toggleAllInfluencers()}
|
||||
>
|
||||
show less
|
||||
<FormattedMessage
|
||||
id="xpack.ml.anomaliesTable.influencersCell.showLessInfluencersLinkText"
|
||||
defaultMessage="show less"
|
||||
/>
|
||||
</EuiLink>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
EuiContextMenuItem,
|
||||
EuiPopover
|
||||
} from '@elastic/eui';
|
||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
|
@ -36,7 +37,16 @@ import { replaceStringTokens } from '../../util/string_utils';
|
|||
/*
|
||||
* Component for rendering the links menu inside a cell in the anomalies table.
|
||||
*/
|
||||
export class LinksMenu extends Component {
|
||||
export const LinksMenu = injectI18n(class LinksMenu extends Component {
|
||||
static propTypes = {
|
||||
anomaly: PropTypes.object.isRequired,
|
||||
showViewSeriesLink: PropTypes.bool,
|
||||
isAggregatedData: PropTypes.bool,
|
||||
interval: PropTypes.string,
|
||||
timefilter: PropTypes.object.isRequired,
|
||||
showRuleEditorFlyout: PropTypes.func
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -47,7 +57,7 @@ export class LinksMenu extends Component {
|
|||
}
|
||||
|
||||
openCustomUrl = (customUrl) => {
|
||||
const { anomaly, interval, isAggregatedData } = this.props;
|
||||
const { anomaly, interval, isAggregatedData, intl } = this.props;
|
||||
|
||||
console.log('Anomalies Table - open customUrl for record:', anomaly);
|
||||
|
||||
|
@ -112,8 +122,12 @@ export class LinksMenu extends Component {
|
|||
|
||||
}).catch((resp) => {
|
||||
console.log('openCustomUrl(): error loading categoryDefinition:', resp);
|
||||
toastNotifications.addDanger(
|
||||
`Unable to open link as an error occurred loading details on category ID ${categoryId}`);
|
||||
toastNotifications.addDanger(intl.formatMessage({
|
||||
id: 'xpack.ml.anomaliesTable.linksMenu.unableToOpenLinkErrorMessage',
|
||||
defaultMessage: 'Unable to open link as an error occurred loading details on category ID {categoryId}'
|
||||
}, {
|
||||
categoryId,
|
||||
}));
|
||||
});
|
||||
|
||||
} else {
|
||||
|
@ -126,6 +140,7 @@ export class LinksMenu extends Component {
|
|||
};
|
||||
|
||||
viewSeries = () => {
|
||||
const { intl } = this.props;
|
||||
const record = this.props.anomaly.source;
|
||||
const bounds = this.props.timefilter.getActiveBounds();
|
||||
const from = bounds.min.toISOString(); // e.g. 2016-02-08T16:00:00.000Z
|
||||
|
@ -160,7 +175,10 @@ export class LinksMenu extends Component {
|
|||
jobIds: [record.job_id]
|
||||
},
|
||||
refreshInterval: {
|
||||
display: 'Off',
|
||||
display: intl.formatMessage({
|
||||
id: 'xpack.ml.anomaliesTable.linksMenu.offLabel',
|
||||
defaultMessage: 'Off'
|
||||
}),
|
||||
pause: false,
|
||||
value: 0
|
||||
},
|
||||
|
@ -196,6 +214,7 @@ export class LinksMenu extends Component {
|
|||
}
|
||||
|
||||
viewExamples = () => {
|
||||
const { intl } = this.props;
|
||||
const categoryId = this.props.anomaly.entityValue;
|
||||
const record = this.props.anomaly.source;
|
||||
const indexPatterns = getIndexPatterns();
|
||||
|
@ -203,8 +222,12 @@ export class LinksMenu extends Component {
|
|||
const job = mlJobService.getJob(this.props.anomaly.jobId);
|
||||
if (job === undefined) {
|
||||
console.log(`viewExamples(): no job found with ID: ${this.props.anomaly.jobId}`);
|
||||
toastNotifications.addDanger(
|
||||
`Unable to view examples as no details could be found for job ID ${this.props.anomaly.jobId}`);
|
||||
toastNotifications.addDanger(intl.formatMessage({
|
||||
id: 'xpack.ml.anomaliesTable.linksMenu.unableToViewExamplesErrorMessage',
|
||||
defaultMessage: 'Unable to view examples as no details could be found for job ID {jobId}'
|
||||
}, {
|
||||
jobId: this.props.anomaly.jobId,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
const categorizationFieldName = job.analysis_config.categorization_field_name;
|
||||
|
@ -274,7 +297,10 @@ export class LinksMenu extends Component {
|
|||
// Use rison to build the URL .
|
||||
const _g = rison.encode({
|
||||
refreshInterval: {
|
||||
display: 'Off',
|
||||
display: intl.formatMessage({
|
||||
id: 'xpack.ml.anomaliesTable.linksMenu.offLabel',
|
||||
defaultMessage: 'Off'
|
||||
}),
|
||||
pause: false,
|
||||
value: 0
|
||||
},
|
||||
|
@ -308,8 +334,12 @@ export class LinksMenu extends Component {
|
|||
|
||||
}).catch((resp) => {
|
||||
console.log('viewExamples(): error loading categoryDefinition:', resp);
|
||||
toastNotifications.addDanger(
|
||||
`Unable to view examples as an error occurred loading details on category ID ${categoryId}`);
|
||||
toastNotifications.addDanger(intl.formatMessage({
|
||||
id: 'xpack.ml.anomaliesTable.linksMenu.loadingDetailsErrorMessage',
|
||||
defaultMessage: 'Unable to view examples as an error occurred loading details on category ID {categoryId}'
|
||||
}, {
|
||||
categoryId,
|
||||
}));
|
||||
});
|
||||
|
||||
}
|
||||
|
@ -317,9 +347,14 @@ export class LinksMenu extends Component {
|
|||
function error() {
|
||||
console.log(`viewExamples(): error finding type of field ${categorizationFieldName} in indices:`,
|
||||
datafeedIndices);
|
||||
toastNotifications.addDanger(
|
||||
`Unable to view examples of documents with mlcategory ${categoryId} ` +
|
||||
`as no mapping could be found for the categorization field ${categorizationFieldName}`);
|
||||
toastNotifications.addDanger(intl.formatMessage({
|
||||
id: 'xpack.ml.anomaliesTable.linksMenu.noMappingCouldBeFoundErrorMessage',
|
||||
defaultMessage: 'Unable to view examples of documents with mlcategory {categoryId} ' +
|
||||
'as no mapping could be found for the categorization field {categorizationFieldName}'
|
||||
}, {
|
||||
categoryId,
|
||||
categorizationFieldName,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -336,7 +371,7 @@ export class LinksMenu extends Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const { anomaly, showViewSeriesLink } = this.props;
|
||||
const { anomaly, showViewSeriesLink, intl } = this.props;
|
||||
const canConfigureRules = (isRuleSupported(anomaly.source) && checkPermission('canUpdateJob'));
|
||||
|
||||
const button = (
|
||||
|
@ -345,7 +380,10 @@ export class LinksMenu extends Component {
|
|||
color="text"
|
||||
onClick={this.onButtonClick}
|
||||
iconType="gear"
|
||||
aria-label="Select action"
|
||||
aria-label={intl.formatMessage({
|
||||
id: 'xpack.ml.anomaliesTable.linksMenu.selectActionAriaLabel',
|
||||
defaultMessage: 'Select action',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -371,7 +409,10 @@ export class LinksMenu extends Component {
|
|||
icon="popout"
|
||||
onClick={() => { this.closePopover(); this.viewSeries(); }}
|
||||
>
|
||||
View series
|
||||
<FormattedMessage
|
||||
id="xpack.ml.anomaliesTable.linksMenu.viewSeriesLabel"
|
||||
defaultMessage="View series"
|
||||
/>
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
}
|
||||
|
@ -383,7 +424,10 @@ export class LinksMenu extends Component {
|
|||
icon="popout"
|
||||
onClick={() => { this.closePopover(); this.viewExamples(); }}
|
||||
>
|
||||
View examples
|
||||
<FormattedMessage
|
||||
id="xpack.ml.anomaliesTable.linksMenu.viewExamplesLabel"
|
||||
defaultMessage="View examples"
|
||||
/>
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
}
|
||||
|
@ -395,7 +439,10 @@ export class LinksMenu extends Component {
|
|||
icon="controlsHorizontal"
|
||||
onClick={() => { this.closePopover(); this.props.showRuleEditorFlyout(anomaly); }}
|
||||
>
|
||||
Configure rules
|
||||
<FormattedMessage
|
||||
id="xpack.ml.anomaliesTable.linksMenu.configureRulesLabel"
|
||||
defaultMessage="Configure rules"
|
||||
/>
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
}
|
||||
|
@ -415,13 +462,4 @@ export class LinksMenu extends Component {
|
|||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LinksMenu.propTypes = {
|
||||
anomaly: PropTypes.object.isRequired,
|
||||
showViewSeriesLink: PropTypes.bool,
|
||||
isAggregatedData: PropTypes.bool,
|
||||
interval: PropTypes.string,
|
||||
timefilter: PropTypes.object.isRequired,
|
||||
showRuleEditorFlyout: PropTypes.func
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
@import 'controls';
|
||||
@import 'controls_select/index';
|
||||
@import 'select_severity/index';
|
||||
@import 'select_severity/index';
|
||||
|
|
|
@ -17,12 +17,18 @@ import {
|
|||
|
||||
import makeId from '@elastic/eui/lib/components/form/form_row/make_id';
|
||||
|
||||
// This service will be populated by the corresponding angularjs based one.
|
||||
export const mlCheckboxShowChartsService = {
|
||||
intialized: false,
|
||||
state: null
|
||||
};
|
||||
|
||||
class CheckboxShowCharts extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// Restore the checked setting from the state.
|
||||
this.mlCheckboxShowChartsService = this.props.mlCheckboxShowChartsService;
|
||||
this.mlCheckboxShowChartsService = mlCheckboxShowChartsService;
|
||||
const showCharts = this.mlCheckboxShowChartsService.state.get('showCharts');
|
||||
|
||||
this.state = {
|
||||
|
|
|
@ -12,22 +12,12 @@ import { stateFactoryProvider } from 'plugins/ml/factories/state_factory';
|
|||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml', ['react']);
|
||||
|
||||
import { CheckboxShowCharts } from './checkbox_showcharts';
|
||||
import { mlCheckboxShowChartsService } from './checkbox_showcharts';
|
||||
|
||||
module.service('mlCheckboxShowChartsService', function (Private) {
|
||||
const stateFactory = Private(stateFactoryProvider);
|
||||
this.state = stateFactory('mlCheckboxShowCharts', {
|
||||
this.state = mlCheckboxShowChartsService.state = stateFactory('mlCheckboxShowCharts', {
|
||||
showCharts: true
|
||||
});
|
||||
})
|
||||
.directive('mlCheckboxShowCharts', function ($injector) {
|
||||
const reactDirective = $injector.get('reactDirective');
|
||||
const mlCheckboxShowChartsService = $injector.get('mlCheckboxShowChartsService');
|
||||
|
||||
return reactDirective(
|
||||
CheckboxShowCharts,
|
||||
undefined,
|
||||
{ restrict: 'E' },
|
||||
{ mlCheckboxShowChartsService }
|
||||
);
|
||||
});
|
||||
mlCheckboxShowChartsService.initialized = true;
|
||||
});
|
|
@ -5,4 +5,4 @@
|
|||
*/
|
||||
|
||||
|
||||
import './checkbox_showcharts_directive';
|
||||
import './checkbox_showcharts_service';
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import ngMock from 'ng_mock';
|
||||
import expect from 'expect.js';
|
||||
|
||||
describe('ML - <ml-controls-select>', () => {
|
||||
let $scope;
|
||||
let $compile;
|
||||
let $element;
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(() => {
|
||||
ngMock.inject(function ($injector) {
|
||||
$compile = $injector.get('$compile');
|
||||
const $rootScope = $injector.get('$rootScope');
|
||||
$scope = $rootScope.$new();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
$scope.$destroy();
|
||||
});
|
||||
|
||||
it('Plain initialization doesn\'t throw an error', () => {
|
||||
$element = $compile('<ml-controls-select />')($scope);
|
||||
const scope = $element.isolateScope();
|
||||
|
||||
expect(scope.identifier).to.be.an('undefined');
|
||||
expect(scope.label).to.be.an('undefined');
|
||||
expect(scope.options).to.be.an('undefined');
|
||||
expect(scope.selected).to.be.an('undefined');
|
||||
expect(scope.setOption).to.be.a('function');
|
||||
expect(scope.showIcons).to.be.an('undefined');
|
||||
expect(scope.updateFn).to.be.a('undefined');
|
||||
});
|
||||
|
||||
it('Initialize with attributes, call pass-through function', (done) => {
|
||||
$scope.intervalOptions = [
|
||||
{ display: 'testOptionLabel1', val: 'testOptionValue1' },
|
||||
{ display: 'testOptionLabel2', val: 'testOptionValue2' }
|
||||
];
|
||||
$scope.selectedOption = $scope.intervalOptions[1];
|
||||
|
||||
$scope.testUpdateFn = function () {
|
||||
done();
|
||||
};
|
||||
|
||||
$element = $compile(`
|
||||
<ml-controls-select
|
||||
identifier="testIdentifier"
|
||||
label="testLabel"
|
||||
options="intervalOptions"
|
||||
selected="selectedOption"
|
||||
show-icons="false"
|
||||
update-fn="testUpdateFn"
|
||||
/>
|
||||
`)($scope);
|
||||
|
||||
const scope = $element.isolateScope();
|
||||
|
||||
expect(scope.identifier).to.be('testIdentifier');
|
||||
expect(scope.label).to.be('testLabel');
|
||||
expect(scope.options).to.equal($scope.intervalOptions);
|
||||
expect(scope.selected).to.equal($scope.selectedOption);
|
||||
expect(scope.setOption).to.be.a('function');
|
||||
expect(scope.showIcons).to.be.false;
|
||||
expect(scope.updateFn).to.be.a('function');
|
||||
|
||||
// this should call the function passed through ($scope.testUpdateFn)
|
||||
// which in return calls done() to finish the test
|
||||
scope.setOption();
|
||||
});
|
||||
|
||||
});
|
|
@ -1,44 +0,0 @@
|
|||
ml-controls-select {
|
||||
.dropdown-group {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
cursor: pointer;
|
||||
}
|
||||
.dropdown-toggle {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
min-width: 120px;
|
||||
font-size: $euiFontSizeXS;
|
||||
|
||||
> li > a {
|
||||
color: $euiColorDarkestShade;;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover, &:active, &:focus {
|
||||
color: $euiColorEmptyShade;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button.dropdown-toggle {
|
||||
text-align: left;
|
||||
margin-bottom: $euiSizeXS;
|
||||
|
||||
// SASSTODO: Needs more specific selectors
|
||||
span {
|
||||
font-size: $euiSizeXS;
|
||||
}
|
||||
}
|
||||
|
||||
button.dropdown-toggle:hover,
|
||||
button.dropdown-toggle:focus {
|
||||
color: $euiColorDarkestShade;
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
@import 'controls_select';
|
|
@ -1,11 +0,0 @@
|
|||
<label for="select{{identifier}}" class="kuiLabel">{{label}}:</label>
|
||||
<div class="dropdown-group" dropdown>
|
||||
<button id="select{{identifier}}" type="button" class="form-control dropdown-toggle" ng-class="{ 'dropdown-toggle-narrow': narrowStyle }" dropdown-toggle ng-disabled="disabled">
|
||||
<span><i ng-if="showIcons" class="fa fa-exclamation-triangle ml-icon-severity-{{selected.display}}"></i> {{selected.display}}</span> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li ng-repeat="option in options">
|
||||
<a href="" ng-click="setOption(option)"><i ng-if="showIcons" class="fa fa-exclamation-triangle ml-icon-severity-{{option.display}}"></i> {{option.display}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue