Merge remote-tracking branch 'origin/master' into feature/merge-code

This commit is contained in:
Fuyao Zhao 2019-01-14 15:38:37 -08:00
commit d13c15b862
198 changed files with 4945 additions and 2410 deletions

View file

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

View file

@ -19,6 +19,6 @@
import { clog } from './clog';
export const commonFunctions = [
export const browserFunctions = [
clog,
];

View file

@ -17,4 +17,7 @@
* under the License.
*/
import '../common/register';
import { browserFunctions } from './index';
// eslint-disable-next-line no-undef
browserFunctions.forEach(canvas.register);

View file

@ -17,7 +17,4 @@
* under the License.
*/
import { commonFunctions } from './index';
// eslint-disable-next-line no-undef
commonFunctions.forEach(canvas.register);
import '../common/register';

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -79,7 +79,7 @@ exports[`DetailView should render StickyProperties 1`] = `
pathname="/app/apm"
query={
Object {
"traceid": "traceId",
"traceId": "traceId",
"transactionId": "myTransactionName",
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
&gt;=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 &gt;=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>

View file

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

View file

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

View file

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

View file

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

View file

@ -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.'
}
)}`
}
];

View file

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

View file

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

View file

@ -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'
})}
/>
}
/>
)}

View file

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

View file

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

View file

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

View file

@ -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}`,

View file

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

View file

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

View file

@ -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",
},
},
}
}
/>
`;

View file

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

View file

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

View file

@ -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&#39;s no APM index pattern with the title &#34;
{apmIndexPatternTitle}
&#34; 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"

View file

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

View file

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

View file

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

View file

@ -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 &gt;= 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'
}
);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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: [],
};

View file

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

View file

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

View file

@ -47,7 +47,3 @@ ColorRampSelector.propTypes = {
color: PropTypes.string,
onChange: PropTypes.func.isRequired,
};
ColorRampSelector.defaultProps = {
color: '',
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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',
},
],
},
],
},

View file

@ -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',
},
],
},
],
},

View file

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

View file

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

View file

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

View file

@ -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.' },
],
},
];

View file

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

View file

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

View file

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

View file

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

View file

@ -11,10 +11,16 @@ export const filebeatRedisRules = [
},
format: [
{
constant: 'redis',
constant: '[Redis]',
},
{
constant: ' ',
constant: '[',
},
{
field: 'redis.log.level',
},
{
constant: '] ',
},
{
field: 'redis.log.message',

View file

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

View file

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

View file

@ -164,7 +164,7 @@ export interface LogEntryDocument {
}
export interface LogEntryDocumentFields {
[fieldName: string]: string | number | null;
[fieldName: string]: string | number | boolean | null;
}
const convertLogDocumentToEntry = (

View file

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

View file

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

View file

@ -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' }]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,2 @@
@import 'controls';
@import 'controls_select/index';
@import 'select_severity/index';
@import 'select_severity/index';

View file

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

View file

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

View file

@ -5,4 +5,4 @@
*/
import './checkbox_showcharts_directive';
import './checkbox_showcharts_service';

View file

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

View file

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

View file

@ -1 +0,0 @@
@import 'controls_select';

View file

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