Merge branch 'master' of github.com:elastic/kibana into fix/watcher-test-missing-await

This commit is contained in:
spalger 2019-08-14 21:06:17 -07:00
commit 9226eb8aa1
64 changed files with 1785 additions and 632 deletions

View file

@ -43,3 +43,8 @@ Adjusting the sampling rate controls what percent of requests are traced.
`1.0` means _all_ requests are traced. If you set the `TRANSACTION_SAMPLE_RATE` to a value below `1.0`,
the agent will randomly sample only a subset of transactions.
Unsampled transactions only record the name of the transaction, the overall transaction time, and the result.
IMPORTANT: In a distributed trace, the sampling decision is propagated by the initializing Agent.
This means if you're using multiple agents, only the originating service's sampling rate will be used.
Be sure to set sensible defaults in _all_ of your agents, especially the
{apm-rum-ref}/configuration.html#transaction-sample-rate[JavaScript RUM Agent].

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [IKibanaSocket](./kibana-plugin-server.ikibanasocket.md) &gt; [authorizationError](./kibana-plugin-server.ikibanasocket.authorizationerror.md)
## IKibanaSocket.authorizationError property
The reason why the peer's certificate has not been verified. This property becomes available only when `authorized` is `false`<!-- -->.
<b>Signature:</b>
```typescript
readonly authorizationError?: Error;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [IKibanaSocket](./kibana-plugin-server.ikibanasocket.md) &gt; [authorized](./kibana-plugin-server.ikibanasocket.authorized.md)
## IKibanaSocket.authorized property
Indicates whether or not the peer certificate was signed by one of the specified CAs. When TLS isn't used the value is `undefined`<!-- -->.
<b>Signature:</b>
```typescript
readonly authorized?: boolean;
```

View file

@ -12,6 +12,13 @@ A tiny abstraction for TCP socket.
export interface IKibanaSocket
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [authorizationError](./kibana-plugin-server.ikibanasocket.authorizationerror.md) | <code>Error</code> | The reason why the peer's certificate has not been verified. This property becomes available only when <code>authorized</code> is <code>false</code>. |
| [authorized](./kibana-plugin-server.ikibanasocket.authorized.md) | <code>boolean</code> | Indicates whether or not the peer certificate was signed by one of the specified CAs. When TLS isn't used the value is <code>undefined</code>. |
## Methods
| Method | Description |

View file

@ -295,6 +295,10 @@ files that should be trusted.
Details on the format, and the valid options, are available via the
https://www.openssl.org/docs/man1.0.2/apps/ciphers.html#CIPHER-LIST-FORMAT[OpenSSL cipher list format documentation].
`server.ssl.clientAuthentication:`:: *Default: none* Controls the servers behavior in regard to requesting a certificate from client
connections. Valid values are `required`, `optional`, and `none`. `required` forces a client to present a certificate, while `optional`
requests a client certificate but the client is not required to present one.
`server.ssl.enabled:`:: *Default: "false"* Enables SSL for outgoing requests
from the Kibana server to the browser. When set to `true`,
`server.ssl.certificate` and `server.ssl.key` are required.

View file

@ -45,6 +45,7 @@ Object {
"!SRP",
"!CAMELLIA",
],
"clientAuthentication": "none",
"enabled": false,
"supportedProtocols": Array [
"TLSv1.1",

View file

@ -17,7 +17,9 @@
* under the License.
*/
import { config } from '.';
import { config, HttpConfig } from '.';
import { Env } from '../config';
import { getEnvOptions } from '../config/__mocks__/env';
test('has defaults for config', () => {
const httpSchema = config.schema;
@ -111,6 +113,46 @@ describe('with TLS', () => {
expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot();
});
test('throws if TLS is not enabled but `clientAuthentication` is `optional`', () => {
const httpSchema = config.schema;
const obj = {
port: 1234,
ssl: {
enabled: false,
clientAuthentication: 'optional',
},
};
expect(() => httpSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot(
`"[ssl]: must enable ssl to use [clientAuthentication]"`
);
});
test('throws if TLS is not enabled but `clientAuthentication` is `required`', () => {
const httpSchema = config.schema;
const obj = {
port: 1234,
ssl: {
enabled: false,
clientAuthentication: 'required',
},
};
expect(() => httpSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot(
`"[ssl]: must enable ssl to use [clientAuthentication]"`
);
});
test('can specify `none` for [clientAuthentication] if ssl is not enabled', () => {
const obj = {
ssl: {
enabled: false,
clientAuthentication: 'none',
},
};
const configValue = config.schema.validate(obj);
expect(configValue.ssl.clientAuthentication).toBe('none');
});
test('can specify single `certificateAuthority` as a string', () => {
const obj = {
ssl: {
@ -202,4 +244,55 @@ describe('with TLS', () => {
httpSchema.validate(allKnownWithOneUnknownProtocols)
).toThrowErrorMatchingSnapshot();
});
test('HttpConfig instance should properly interpret `none` client authentication', () => {
const httpConfig = new HttpConfig(
config.schema.validate({
ssl: {
enabled: true,
key: 'some-key-path',
certificate: 'some-certificate-path',
clientAuthentication: 'none',
},
}),
Env.createDefault(getEnvOptions())
);
expect(httpConfig.ssl.requestCert).toBe(false);
expect(httpConfig.ssl.rejectUnauthorized).toBe(false);
});
test('HttpConfig instance should properly interpret `optional` client authentication', () => {
const httpConfig = new HttpConfig(
config.schema.validate({
ssl: {
enabled: true,
key: 'some-key-path',
certificate: 'some-certificate-path',
clientAuthentication: 'optional',
},
}),
Env.createDefault(getEnvOptions())
);
expect(httpConfig.ssl.requestCert).toBe(true);
expect(httpConfig.ssl.rejectUnauthorized).toBe(false);
});
test('HttpConfig instance should properly interpret `required` client authentication', () => {
const httpConfig = new HttpConfig(
config.schema.validate({
ssl: {
enabled: true,
key: 'some-key-path',
certificate: 'some-certificate-path',
clientAuthentication: 'required',
},
}),
Env.createDefault(getEnvOptions())
);
expect(httpConfig.ssl.requestCert).toBe(true);
expect(httpConfig.ssl.rejectUnauthorized).toBe(true);
});
});

View file

@ -17,16 +17,22 @@
* under the License.
*/
jest.mock('fs', () => ({
readFileSync: jest.fn(),
}));
import supertest from 'supertest';
import { Request, ResponseToolkit } from 'hapi';
import Joi from 'joi';
import { defaultValidationErrorHandler, HapiValidationError } from './http_tools';
import { defaultValidationErrorHandler, HapiValidationError, getServerOptions } from './http_tools';
import { HttpServer } from './http_server';
import { HttpConfig } from './http_config';
import { HttpConfig, config } from './http_config';
import { Router } from './router';
import { loggingServiceMock } from '../logging/logging_service.mock';
import { ByteSizeValue } from '@kbn/config-schema';
import { Env } from '../config';
import { getEnvOptions } from '../config/__mocks__/env';
const emptyOutput = {
statusCode: 400,
@ -41,6 +47,8 @@ const emptyOutput = {
},
};
afterEach(() => jest.clearAllMocks());
describe('defaultValidationErrorHandler', () => {
it('formats value validation errors correctly', () => {
expect.assertions(1);
@ -97,3 +105,68 @@ describe('timeouts', () => {
await server.stop();
});
});
describe('getServerOptions', () => {
beforeEach(() =>
jest.requireMock('fs').readFileSync.mockImplementation((path: string) => `content-${path}`)
);
it('properly configures TLS with default options', () => {
const httpConfig = new HttpConfig(
config.schema.validate({
ssl: {
enabled: true,
key: 'some-key-path',
certificate: 'some-certificate-path',
},
}),
Env.createDefault(getEnvOptions())
);
expect(getServerOptions(httpConfig).tls).toMatchInlineSnapshot(`
Object {
"ca": undefined,
"cert": "content-some-certificate-path",
"ciphers": "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA",
"honorCipherOrder": true,
"key": "content-some-key-path",
"passphrase": undefined,
"rejectUnauthorized": false,
"requestCert": false,
"secureOptions": 67108864,
}
`);
});
it('properly configures TLS with client authentication', () => {
const httpConfig = new HttpConfig(
config.schema.validate({
ssl: {
enabled: true,
key: 'some-key-path',
certificate: 'some-certificate-path',
certificateAuthorities: ['ca-1', 'ca-2'],
clientAuthentication: 'required',
},
}),
Env.createDefault(getEnvOptions())
);
expect(getServerOptions(httpConfig).tls).toMatchInlineSnapshot(`
Object {
"ca": Array [
"content-ca-1",
"content-ca-2",
],
"cert": "content-some-certificate-path",
"ciphers": "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA",
"honorCipherOrder": true,
"key": "content-some-key-path",
"passphrase": undefined,
"rejectUnauthorized": true,
"requestCert": true,
"secureOptions": 67108864,
}
`);
});
});

View file

@ -71,6 +71,7 @@ export function getServerOptions(config: HttpConfig, { configureTLS = true } = {
passphrase: ssl.keyPassphrase,
secureOptions: ssl.getSecureOptions(),
requestCert: ssl.requestCert,
rejectUnauthorized: ssl.rejectUnauthorized,
};
options.tls = tlsOptions;

View file

@ -56,4 +56,50 @@ describe('KibanaSocket', () => {
expect(socket.getPeerCertificate()).toBe(null);
});
});
describe('authorized', () => {
it('returns `undefined` for net.Socket instance', () => {
const socket = new KibanaSocket(new Socket());
expect(socket.authorized).toBeUndefined();
});
it('mirrors the value of tls.Socket.authorized', () => {
const tlsSocket = new TLSSocket(new Socket());
tlsSocket.authorized = true;
let socket = new KibanaSocket(tlsSocket);
expect(tlsSocket.authorized).toBe(true);
expect(socket.authorized).toBe(true);
tlsSocket.authorized = false;
socket = new KibanaSocket(tlsSocket);
expect(tlsSocket.authorized).toBe(false);
expect(socket.authorized).toBe(false);
});
});
describe('authorizationError', () => {
it('returns `undefined` for net.Socket instance', () => {
const socket = new KibanaSocket(new Socket());
expect(socket.authorizationError).toBeUndefined();
});
it('mirrors the value of tls.Socket.authorizationError', () => {
const tlsSocket = new TLSSocket(new Socket());
tlsSocket.authorizationError = undefined as any;
let socket = new KibanaSocket(tlsSocket);
expect(tlsSocket.authorizationError).toBeUndefined();
expect(socket.authorizationError).toBeUndefined();
const authorizationError = new Error('some error');
tlsSocket.authorizationError = authorizationError;
socket = new KibanaSocket(tlsSocket);
expect(tlsSocket.authorizationError).toBe(authorizationError);
expect(socket.authorizationError).toBe(authorizationError);
});
});
});

View file

@ -37,10 +37,30 @@ export interface IKibanaSocket {
* @returns An object representing the peer's certificate.
*/
getPeerCertificate(detailed?: boolean): PeerCertificate | DetailedPeerCertificate | null;
/**
* Indicates whether or not the peer certificate was signed by one of the specified CAs. When TLS
* isn't used the value is `undefined`.
*/
readonly authorized?: boolean;
/**
* The reason why the peer's certificate has not been verified. This property becomes available
* only when `authorized` is `false`.
*/
readonly authorizationError?: Error;
}
export class KibanaSocket implements IKibanaSocket {
constructor(private readonly socket: Socket) {}
readonly authorized?: boolean;
readonly authorizationError?: Error;
constructor(private readonly socket: Socket) {
if (this.socket instanceof TLSSocket) {
this.authorized = this.socket.authorized;
this.authorizationError = this.socket.authorizationError;
}
}
getPeerCertificate(detailed: true): DetailedPeerCertificate | null;
getPeerCertificate(detailed: false): PeerCertificate | null;

View file

@ -49,13 +49,20 @@ export const sslSchema = schema.object(
schema.oneOf([schema.literal('TLSv1'), schema.literal('TLSv1.1'), schema.literal('TLSv1.2')]),
{ defaultValue: ['TLSv1.1', 'TLSv1.2'], minSize: 1 }
),
requestCert: schema.maybe(schema.boolean({ defaultValue: false })),
clientAuthentication: schema.oneOf(
[schema.literal('none'), schema.literal('optional'), schema.literal('required')],
{ defaultValue: 'none' }
),
},
{
validate: ssl => {
if (ssl.enabled && (!ssl.key || !ssl.certificate)) {
return 'must specify [certificate] and [key] when ssl is enabled';
}
if (!ssl.enabled && ssl.clientAuthentication !== 'none') {
return 'must enable ssl to use [clientAuthentication]';
}
},
}
);
@ -69,7 +76,8 @@ export class SslConfig {
public certificate: string | undefined;
public certificateAuthorities: string[] | undefined;
public keyPassphrase: string | undefined;
public requestCert: boolean | undefined;
public requestCert: boolean;
public rejectUnauthorized: boolean;
public cipherSuites: string[];
public supportedProtocols: string[];
@ -86,7 +94,8 @@ export class SslConfig {
this.keyPassphrase = config.keyPassphrase;
this.cipherSuites = config.cipherSuites;
this.supportedProtocols = config.supportedProtocols;
this.requestCert = config.requestCert;
this.requestCert = config.clientAuthentication !== 'none';
this.rejectUnauthorized = config.clientAuthentication === 'required';
}
/**

View file

@ -261,6 +261,8 @@ export type IContextProvider<TContext extends Record<string, any>, TContextName
// @public
export interface IKibanaSocket {
readonly authorizationError?: Error;
readonly authorized?: boolean;
// (undocumented)
getPeerCertificate(detailed: true): DetailedPeerCertificate | null;
// (undocumented)

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
// @ts-ignore Untyped Library
import { Fn } from '@kbn/interpreter/common';
import { functions as browserFns } from '../../canvas_plugin_src/functions/browser';
import { functions as commonFns } from '../../canvas_plugin_src/functions/common';

View file

@ -1,119 +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.
*/
export const workpads = [
{
pages: [
{
elements: [
{
expression: `
demodata |
ply by=age fn={rowCount | as count} |
staticColumn total value={math 'sum(count)'} |
mapColumn percentage fn={math 'count/total * 100'} |
sort age |
pointseries x=age y=percentage |
plot defaultStyle={seriesStyle points=0 lines=5}`,
},
],
},
],
},
{
pages: [{ elements: [{ expression: 'filters | demodata | markdown "hello" | render' }] }],
},
{
pages: [
{
elements: [
{ expression: 'demodata | pointseries | getCell | repeatImage | render' },
{ expression: 'demodata | pointseries | getCell | repeatImage | render' },
{ expression: 'demodata | pointseries | getCell | repeatImage | render' },
{ expression: 'filters | demodata | markdown "hello" | render' },
{ expression: 'filters | demodata | pointseries | pie | render' },
],
},
{ elements: [{ expression: 'filters | demodata | table | render' }] },
{ elements: [{ expression: 'image | render' }] },
{ elements: [{ expression: 'image | render' }] },
],
},
{
pages: [
{
elements: [
{ expression: 'filters | demodata | markdown "hello" | render' },
{ expression: 'filters | demodata | markdown "hello" | render' },
{ expression: 'image | render' },
],
},
{
elements: [
{ expression: 'demodata | pointseries | getCell | repeatImage | render' },
{ expression: 'filters | demodata | markdown "hello" | render' },
{ expression: 'filters | demodata | pointseries | pie | render' },
{ expression: 'image | render' },
],
},
{
elements: [
{ expression: 'filters | demodata | pointseries | pie | render' },
{
expression:
'filters | demodata | pointseries | plot defaultStyle={seriesStyle points=0 lines=5} | render',
},
],
},
],
},
{
pages: [
{
elements: [
{ expression: 'demodata | render as=debug' },
{ expression: 'filters | demodata | pointseries | plot | render' },
{ expression: 'filters | demodata | table | render' },
{ expression: 'filters | demodata | table | render' },
],
},
{
elements: [
{ expression: 'demodata | pointseries | getCell | repeatImage | render' },
{ expression: 'filters | demodata | pointseries | pie | render' },
{ expression: 'image | render' },
],
},
{
elements: [
{ expression: 'demodata | pointseries | getCell | repeatImage | render' },
{ expression: 'demodata | render as=debug' },
{ expression: 'shape "square" | render' },
],
},
],
},
{
pages: [
{
elements: [
{ expression: 'demodata | pointseries | getCell | repeatImage | render' },
{ expression: 'filters | demodata | markdown "hello" | render' },
],
},
{ elements: [{ expression: 'image | render' }] },
{ elements: [{ expression: 'image | render' }] },
{ elements: [{ expression: 'filters | demodata | table | render' }] },
],
},
];
export const elements = [
{ expression: 'demodata | pointseries | getCell | repeatImage | render' },
{ expression: 'filters | demodata | markdown "hello" | render' },
{ expression: 'filters | demodata | pointseries | pie | render' },
{ expression: 'image | render' },
];

View file

@ -0,0 +1,184 @@
/*
* 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 { CanvasWorkpad, CanvasElement, CanvasPage } from '../../types';
const BaseWorkpad: CanvasWorkpad = {
name: 'base workpad',
id: 'base-workpad',
width: 0,
height: 0,
css: '',
page: 1,
pages: [],
colors: [],
isWriteable: true,
};
const BasePage: CanvasPage = {
id: 'base-page',
style: { background: 'white' },
transition: {},
elements: [],
groups: [],
};
const BaseElement: CanvasElement = {
position: {
top: 0,
left: 0,
width: 0,
height: 0,
angle: 0,
parent: null,
},
id: 'base-id',
type: 'element',
expression: 'render',
filter: '',
};
export const workpads: CanvasWorkpad[] = [
{
...BaseWorkpad,
pages: [
{
...BasePage,
elements: [
{
...BaseElement,
expression: `
demodata |
ply by=age fn={rowCount | as count} |
staticColumn total value={math 'sum(count)'} |
mapColumn percentage fn={math 'count/total * 100'} |
sort age |
pointseries x=age y=percentage |
plot defaultStyle={seriesStyle points=0 lines=5}`,
},
],
},
],
},
{
...BaseWorkpad,
pages: [
{
...BasePage,
elements: [
{ ...BaseElement, expression: 'filters | demodata | markdown "hello" | render' },
],
},
],
},
{
...BaseWorkpad,
pages: [
{
...BasePage,
elements: [
{ ...BaseElement, expression: 'demodata | pointseries | getCell | repeatImage | render' },
{ ...BaseElement, expression: 'demodata | pointseries | getCell | repeatImage | render' },
{ ...BaseElement, expression: 'demodata | pointseries | getCell | repeatImage | render' },
{ ...BaseElement, expression: 'filters | demodata | markdown "hello" | render' },
{ ...BaseElement, expression: 'filters | demodata | pointseries | pie | render' },
],
},
{
...BasePage,
elements: [{ ...BaseElement, expression: 'filters | demodata | table | render' }],
},
{ ...BasePage, elements: [{ ...BaseElement, expression: 'image | render' }] },
{ ...BasePage, elements: [{ ...BaseElement, expression: 'image | render' }] },
],
},
{
...BaseWorkpad,
pages: [
{
...BasePage,
elements: [
{ ...BaseElement, expression: 'filters | demodata | markdown "hello" | render' },
{ ...BaseElement, expression: 'filters | demodata | markdown "hello" | render' },
{ ...BaseElement, expression: 'image | render' },
],
},
{
...BasePage,
elements: [
{ ...BaseElement, expression: 'demodata | pointseries | getCell | repeatImage | render' },
{ ...BaseElement, expression: 'filters | demodata | markdown "hello" | render' },
{ ...BaseElement, expression: 'filters | demodata | pointseries | pie | render' },
{ ...BaseElement, expression: 'image | render' },
],
},
{
...BasePage,
elements: [
{ ...BaseElement, expression: 'filters | demodata | pointseries | pie | render' },
{
...BaseElement,
expression:
'filters | demodata | pointseries | plot defaultStyle={seriesStyle points=0 lines=5} | render',
},
],
},
],
},
{
...BaseWorkpad,
pages: [
{
...BasePage,
elements: [
{ ...BaseElement, expression: 'demodata | render as=debug' },
{ ...BaseElement, expression: 'filters | demodata | pointseries | plot | render' },
{ ...BaseElement, expression: 'filters | demodata | table | render' },
{ ...BaseElement, expression: 'filters | demodata | table | render' },
],
},
{
...BasePage,
elements: [
{ ...BaseElement, expression: 'demodata | pointseries | getCell | repeatImage | render' },
{ ...BaseElement, expression: 'filters | demodata | pointseries | pie | render' },
{ ...BaseElement, expression: 'image | render' },
],
},
{
...BasePage,
elements: [
{ ...BaseElement, expression: 'demodata | pointseries | getCell | repeatImage | render' },
{ ...BaseElement, expression: 'demodata | render as=debug' },
{ ...BaseElement, expression: 'shape "square" | render' },
],
},
],
},
{
...BaseWorkpad,
pages: [
{
...BasePage,
elements: [
{ ...BaseElement, expression: 'demodata | pointseries | getCell | repeatImage | render' },
{ ...BaseElement, expression: 'filters | demodata | markdown "hello" | render' },
],
},
{ ...BasePage, elements: [{ ...BaseElement, expression: 'image | render' }] },
{ ...BasePage, elements: [{ ...BaseElement, expression: 'image | render' }] },
{
...BasePage,
elements: [{ ...BaseElement, expression: 'filters | demodata | table | render' }],
},
],
},
];
export const elements: CanvasElement[] = [
{ ...BaseElement, expression: 'demodata | pointseries | getCell | repeatImage | render' },
{ ...BaseElement, expression: 'filters | demodata | markdown "hello" | render' },
{ ...BaseElement, expression: 'filters | demodata | pointseries | pie | render' },
{ ...BaseElement, expression: 'image | render' },
];

View file

@ -1,231 +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 { uniq } from 'lodash';
import { parse, getByAlias } from '@kbn/interpreter/common';
const MARKER = 'CANVAS_SUGGESTION_MARKER';
/**
* Generates the AST with the given expression and then returns the function and argument definitions
* at the given position in the expression, if there are any.
*/
export function getFnArgDefAtPosition(specs, expression, position) {
const text = expression.substr(0, position) + MARKER + expression.substr(position);
try {
const ast = parse(text, { addMeta: true });
const { ast: newAst, fnIndex, argName } = getFnArgAtPosition(ast, position);
const fn = newAst.node.chain[fnIndex].node;
const fnDef = getByAlias(specs, fn.function.replace(MARKER, ''));
if (fnDef && argName) {
const argDef = getByAlias(fnDef.args, argName);
return { fnDef, argDef };
}
return { fnDef };
} catch (e) {
// Fail silently
}
return [];
}
/**
* Gets a list of suggestions for the given expression at the given position. It does this by
* inserting a marker at the given position, then parsing the resulting expression. This way we can
* see what the marker would turn into, which tells us what sorts of things to suggest. For
* example, if the marker turns into a function name, then we suggest functions. If it turns into
* an unnamed argument, we suggest argument names. If it turns into a value, we suggest values.
*/
export function getAutocompleteSuggestions(specs, expression, position) {
const text = expression.substr(0, position) + MARKER + expression.substr(position);
try {
const ast = parse(text, { addMeta: true });
const { ast: newAst, fnIndex, argName, argIndex } = getFnArgAtPosition(ast, position);
const fn = newAst.node.chain[fnIndex].node;
if (fn.function.includes(MARKER)) {
return getFnNameSuggestions(specs, newAst, fnIndex);
}
if (argName === '_') {
return getArgNameSuggestions(specs, newAst, fnIndex, argName, argIndex);
}
if (argName) {
return getArgValueSuggestions(specs, newAst, fnIndex, argName, argIndex);
}
} catch (e) {
// Fail silently
}
return [];
}
/**
* Get the function and argument (if there is one) at the given position.
*/
function getFnArgAtPosition(ast, position) {
const fnIndex = ast.node.chain.findIndex(fn => fn.start <= position && position <= fn.end);
const fn = ast.node.chain[fnIndex];
for (const [argName, argValues] of Object.entries(fn.node.arguments)) {
for (let argIndex = 0; argIndex < argValues.length; argIndex++) {
const value = argValues[argIndex];
if (value.start <= position && position <= value.end) {
if (value.node !== null && value.node.type === 'expression') {
return getFnArgAtPosition(value, position);
}
return { ast, fnIndex, argName, argIndex };
}
}
}
return { ast, fnIndex };
}
function getFnNameSuggestions(specs, ast, fnIndex) {
// Filter the list of functions by the text at the marker
const { start, end, node: fn } = ast.node.chain[fnIndex];
const query = fn.function.replace(MARKER, '');
const matchingFnDefs = specs.filter(({ name }) => textMatches(name, query));
// Sort by whether or not the function expects the previous function's return type, then by
// whether or not the function name starts with the text at the marker, then alphabetically
const prevFn = ast.node.chain[fnIndex - 1];
const prevFnDef = prevFn && getByAlias(specs, prevFn.node.function);
const prevFnType = prevFnDef && prevFnDef.type;
const comparator = combinedComparator(
prevFnTypeComparator(prevFnType),
invokeWithProp(startsWithComparator(query), 'name'),
invokeWithProp(alphanumericalComparator, 'name')
);
const fnDefs = matchingFnDefs.sort(comparator);
return fnDefs.map(fnDef => {
return { type: 'function', text: fnDef.name + ' ', start, end: end - MARKER.length, fnDef };
});
}
function getArgNameSuggestions(specs, ast, fnIndex, argName, argIndex) {
// Get the list of args from the function definition
const fn = ast.node.chain[fnIndex].node;
const fnDef = getByAlias(specs, fn.function);
if (!fnDef) {
return [];
}
// We use the exact text instead of the value because it is always a string and might be quoted
const { text, start, end } = fn.arguments[argName][argIndex];
// Filter the list of args by the text at the marker
const query = text.replace(MARKER, '');
const matchingArgDefs = Object.values(fnDef.args).filter(({ name }) => textMatches(name, query));
// Filter the list of args by those which aren't already present (unless they allow multi)
const argEntries = Object.entries(fn.arguments).map(([name, values]) => {
return [name, values.filter(value => !value.text.includes(MARKER))];
});
const unusedArgDefs = matchingArgDefs.filter(argDef => {
if (argDef.multi) {
return true;
}
return !argEntries.some(([name, values]) => {
return values.length && (name === argDef.name || argDef.aliases.includes(name));
});
});
// Sort by whether or not the arg is also the unnamed, then by whether or not the arg name starts
// with the text at the marker, then alphabetically
const comparator = combinedComparator(
unnamedArgComparator,
invokeWithProp(startsWithComparator(query), 'name'),
invokeWithProp(alphanumericalComparator, 'name')
);
const argDefs = unusedArgDefs.sort(comparator);
return argDefs.map(argDef => {
return { type: 'argument', text: argDef.name + '=', start, end: end - MARKER.length, argDef };
});
}
function getArgValueSuggestions(specs, ast, fnIndex, argName, argIndex) {
// Get the list of values from the argument definition
const fn = ast.node.chain[fnIndex].node;
const fnDef = getByAlias(specs, fn.function);
if (!fnDef) {
return [];
}
const argDef = getByAlias(fnDef.args, argName);
if (!argDef) {
return [];
}
// Get suggestions from the argument definition, including the default
const { start, end, node } = fn.arguments[argName][argIndex];
const query = node.replace(MARKER, '');
const suggestions = uniq(argDef.options.concat(argDef.default || []));
// Filter the list of suggestions by the text at the marker
const filtered = suggestions.filter(option => textMatches(String(option), query));
// Sort by whether or not the value starts with the text at the marker, then alphabetically
const comparator = combinedComparator(startsWithComparator(query), alphanumericalComparator);
const sorted = filtered.sort(comparator);
return sorted.map(value => {
const text = maybeQuote(value) + ' ';
return { start, end: end - MARKER.length, type: 'value', text };
});
}
function textMatches(text, query) {
return text.toLowerCase().includes(query.toLowerCase().trim());
}
function maybeQuote(value) {
if (typeof value === 'string') {
if (value.match(/^\{.*\}$/)) {
return value;
}
return `"${value.replace(/"/g, '\\"')}"`;
}
return value;
}
function prevFnTypeComparator(prevFnType) {
return (a, b) =>
Boolean(b.context.types && b.context.types.includes(prevFnType)) -
Boolean(a.context.types && a.context.types.includes(prevFnType));
}
function unnamedArgComparator(a, b) {
return b.aliases.includes('_') - a.aliases.includes('_');
}
function alphanumericalComparator(a, b) {
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
}
function startsWithComparator(query) {
return (a, b) => String(b).startsWith(query) - String(a).startsWith(query);
}
function combinedComparator(...comparators) {
return (a, b) =>
comparators.reduce((acc, comparator) => {
if (acc !== 0) {
return acc;
}
return comparator(a, b);
}, 0);
}
function invokeWithProp(fn, prop) {
return (...args) => fn(...args.map(arg => arg[prop]));
}

View file

@ -4,34 +4,34 @@
* you may not use this file except in compliance with the Elastic License.
*/
import expect from '@kbn/expect';
import { functionSpecs } from '../../../__tests__/fixtures/function_specs';
import { getAutocompleteSuggestions } from '../autocomplete';
import { functionSpecs } from '../../__tests__/fixtures/function_specs';
import { getAutocompleteSuggestions } from './autocomplete';
describe('getAutocompleteSuggestions', () => {
it('should suggest functions', () => {
const suggestions = getAutocompleteSuggestions(functionSpecs, '', 0);
expect(suggestions.length).to.be(functionSpecs.length);
expect(suggestions[0].start).to.be(0);
expect(suggestions[0].end).to.be(0);
expect(suggestions.length).toBe(functionSpecs.length);
expect(suggestions[0].start).toBe(0);
expect(suggestions[0].end).toBe(0);
});
it('should suggest functions filtered by text', () => {
const expression = 'pl';
const suggestions = getAutocompleteSuggestions(functionSpecs, expression, 0);
const nonmatching = suggestions.map(s => s.text).filter(text => !text.includes(expression));
expect(nonmatching.length).to.be(0);
expect(suggestions[0].start).to.be(0);
expect(suggestions[0].end).to.be(expression.length);
expect(nonmatching.length).toBe(0);
expect(suggestions[0].start).toBe(0);
expect(suggestions[0].end).toBe(expression.length);
});
it('should suggest arguments', () => {
const expression = 'plot ';
const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length);
const plotFn = functionSpecs.find(spec => spec.name === 'plot');
expect(suggestions.length).to.be(Object.keys(plotFn.args).length);
expect(suggestions[0].start).to.be(expression.length);
expect(suggestions[0].end).to.be(expression.length);
expect(suggestions.length).toBe(Object.keys(plotFn.args).length);
expect(suggestions[0].start).toBe(expression.length);
expect(suggestions[0].end).toBe(expression.length);
});
it('should suggest arguments filtered by text', () => {
@ -39,28 +39,28 @@ describe('getAutocompleteSuggestions', () => {
const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length);
const plotFn = functionSpecs.find(spec => spec.name === 'plot');
const matchingArgs = Object.keys(plotFn.args).filter(key => key.includes('axis'));
expect(suggestions.length).to.be(matchingArgs.length);
expect(suggestions[0].start).to.be('plot '.length);
expect(suggestions[0].end).to.be('plot axis'.length);
expect(suggestions.length).toBe(matchingArgs.length);
expect(suggestions[0].start).toBe('plot '.length);
expect(suggestions[0].end).toBe('plot axis'.length);
});
it('should suggest values', () => {
const expression = 'shape shape=';
const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length);
const shapeFn = functionSpecs.find(spec => spec.name === 'shape');
expect(suggestions.length).to.be(shapeFn.args.shape.options.length);
expect(suggestions[0].start).to.be(expression.length);
expect(suggestions[0].end).to.be(expression.length);
expect(suggestions.length).toBe(shapeFn.args.shape.options.length);
expect(suggestions[0].start).toBe(expression.length);
expect(suggestions[0].end).toBe(expression.length);
});
it('should suggest values filtered by text', () => {
const expression = 'shape shape=ar';
const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length);
const shapeFn = functionSpecs.find(spec => spec.name === 'shape');
const matchingValues = shapeFn.args.shape.options.filter(key => key.includes('ar'));
expect(suggestions.length).to.be(matchingValues.length);
expect(suggestions[0].start).to.be(expression.length - 'ar'.length);
expect(suggestions[0].end).to.be(expression.length);
const matchingValues = shapeFn.args.shape.options.filter((key: string) => key.includes('ar'));
expect(suggestions.length).toBe(matchingValues.length);
expect(suggestions[0].start).toBe(expression.length - 'ar'.length);
expect(suggestions[0].end).toBe(expression.length);
});
it('should suggest functions inside an expression', () => {
@ -70,9 +70,9 @@ describe('getAutocompleteSuggestions', () => {
expression,
expression.length - 1
);
expect(suggestions.length).to.be(functionSpecs.length);
expect(suggestions[0].start).to.be(expression.length - 1);
expect(suggestions[0].end).to.be(expression.length - 1);
expect(suggestions.length).toBe(functionSpecs.length);
expect(suggestions[0].start).toBe(expression.length - 1);
expect(suggestions[0].end).toBe(expression.length - 1);
});
it('should suggest arguments inside an expression', () => {
@ -83,9 +83,9 @@ describe('getAutocompleteSuggestions', () => {
expression.length - 1
);
const ltFn = functionSpecs.find(spec => spec.name === 'lt');
expect(suggestions.length).to.be(Object.keys(ltFn.args).length);
expect(suggestions[0].start).to.be(expression.length - 1);
expect(suggestions[0].end).to.be(expression.length - 1);
expect(suggestions.length).toBe(Object.keys(ltFn.args).length);
expect(suggestions[0].start).toBe(expression.length - 1);
expect(suggestions[0].end).toBe(expression.length - 1);
});
it('should suggest values inside an expression', () => {
@ -96,9 +96,9 @@ describe('getAutocompleteSuggestions', () => {
expression.length - 1
);
const shapeFn = functionSpecs.find(spec => spec.name === 'shape');
expect(suggestions.length).to.be(shapeFn.args.shape.options.length);
expect(suggestions[0].start).to.be(expression.length - 1);
expect(suggestions[0].end).to.be(expression.length - 1);
expect(suggestions.length).toBe(shapeFn.args.shape.options.length);
expect(suggestions[0].start).toBe(expression.length - 1);
expect(suggestions[0].end).toBe(expression.length - 1);
});
it('should suggest values inside quotes', () => {
@ -109,10 +109,10 @@ describe('getAutocompleteSuggestions', () => {
expression.length - 1
);
const shapeFn = functionSpecs.find(spec => spec.name === 'shape');
const matchingValues = shapeFn.args.shape.options.filter(key => key.includes('ar'));
expect(suggestions.length).to.be(matchingValues.length);
expect(suggestions[0].start).to.be(expression.length - '"ar"'.length);
expect(suggestions[0].end).to.be(expression.length);
const matchingValues = shapeFn.args.shape.options.filter((key: string) => key.includes('ar'));
expect(suggestions.length).toBe(matchingValues.length);
expect(suggestions[0].start).toBe(expression.length - '"ar"'.length);
expect(suggestions[0].end).toBe(expression.length);
});
it('should prioritize functions that start with text', () => {
@ -122,7 +122,7 @@ describe('getAutocompleteSuggestions', () => {
const alterColumnIndex = suggestions.findIndex(suggestion =>
suggestion.text.includes('alterColumn')
);
expect(tableIndex).to.be.lessThan(alterColumnIndex);
expect(tableIndex).toBeLessThan(alterColumnIndex);
});
it('should prioritize functions that match the previous function type', () => {
@ -130,7 +130,7 @@ describe('getAutocompleteSuggestions', () => {
const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length);
const renderIndex = suggestions.findIndex(suggestion => suggestion.text.includes('render'));
const anyIndex = suggestions.findIndex(suggestion => suggestion.text.includes('any'));
expect(renderIndex).to.be.lessThan(anyIndex);
expect(renderIndex).toBeLessThan(anyIndex);
});
it('should alphabetize functions', () => {
@ -138,7 +138,7 @@ describe('getAutocompleteSuggestions', () => {
const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length);
const metricIndex = suggestions.findIndex(suggestion => suggestion.text.includes('metric'));
const anyIndex = suggestions.findIndex(suggestion => suggestion.text.includes('any'));
expect(anyIndex).to.be.lessThan(metricIndex);
expect(anyIndex).toBeLessThan(metricIndex);
});
it('should prioritize arguments that start with text', () => {
@ -148,7 +148,7 @@ describe('getAutocompleteSuggestions', () => {
const defaultStyleIndex = suggestions.findIndex(suggestion =>
suggestion.text.includes('defaultStyle')
);
expect(yaxisIndex).to.be.lessThan(defaultStyleIndex);
expect(yaxisIndex).toBeLessThan(defaultStyleIndex);
});
it('should prioritize unnamed arguments', () => {
@ -156,7 +156,7 @@ describe('getAutocompleteSuggestions', () => {
const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length);
const whenIndex = suggestions.findIndex(suggestion => suggestion.text.includes('when'));
const thenIndex = suggestions.findIndex(suggestion => suggestion.text.includes('then'));
expect(whenIndex).to.be.lessThan(thenIndex);
expect(whenIndex).toBeLessThan(thenIndex);
});
it('should alphabetize arguments', () => {
@ -166,24 +166,24 @@ describe('getAutocompleteSuggestions', () => {
const defaultStyleIndex = suggestions.findIndex(suggestion =>
suggestion.text.includes('defaultStyle')
);
expect(defaultStyleIndex).to.be.lessThan(yaxisIndex);
expect(defaultStyleIndex).toBeLessThan(yaxisIndex);
});
it('should quote string values', () => {
const expression = 'shape shape=';
const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length);
expect(suggestions[0].text.trim()).to.match(/^".*"$/);
expect(suggestions[0].text.trim()).toMatch(/^".*"$/);
});
it('should not quote sub expression value suggestions', () => {
const expression = 'plot font=';
const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length);
expect(suggestions[0].text.trim()).to.be('{font}');
expect(suggestions[0].text.trim()).toBe('{font}');
});
it('should not quote booleans', () => {
const expression = 'table paginate=true';
const suggestions = getAutocompleteSuggestions(functionSpecs, expression, expression.length);
expect(suggestions[0].text.trim()).to.be('true');
expect(suggestions[0].text.trim()).toBe('true');
});
});

View file

@ -0,0 +1,376 @@
/*
* 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 { uniq } from 'lodash';
// @ts-ignore Untyped Library
import { parse, getByAlias as untypedGetByAlias } from '@kbn/interpreter/common';
import {
ExpressionAST,
ExpressionFunctionAST,
ExpressionArgAST,
CanvasFunction,
} from '../../types';
const MARKER = 'CANVAS_SUGGESTION_MARKER';
// If you parse an expression with the "addMeta" option it completely
// changes the type of returned object. The following types
// enhance the existing AST types with the appropriate meta information
interface ASTMetaInformation<T> {
start: number;
end: number;
text: string;
node: T;
}
// Wraps ExpressionArg with meta or replace ExpressionAST with ExpressionASTWithMeta
type WrapExpressionArgWithMeta<T> = T extends ExpressionAST
? ExpressionASTWithMeta
: ASTMetaInformation<T>;
type ExpressionArgASTWithMeta = WrapExpressionArgWithMeta<ExpressionArgAST>;
type Modify<T, R> = Pick<T, Exclude<keyof T, keyof R>> & R;
// Wrap ExpressionFunctionAST with meta and modify arguments to be wrapped with meta
type ExpressionFunctionASTWithMeta = Modify<
ExpressionFunctionAST,
{
arguments: {
[key: string]: ExpressionArgASTWithMeta[];
};
}
>;
// Wrap ExpressionFunctionAST with meta and modify chain to be wrapped with meta
type ExpressionASTWithMeta = ASTMetaInformation<
Modify<
ExpressionAST,
{
chain: Array<ASTMetaInformation<ExpressionFunctionASTWithMeta>>;
}
>
>;
// Typeguard for checking if ExpressionArg is a new expression
function isExpression(
maybeExpression: ExpressionArgASTWithMeta
): maybeExpression is ExpressionASTWithMeta {
return typeof maybeExpression.node === 'object';
}
type valueof<T> = T[keyof T];
type ValuesOfUnion<T> = T extends any ? valueof<T> : never;
// All of the possible Arg Values
type ArgValue = ValuesOfUnion<CanvasFunction['args']>;
// All of the argument objects
type CanvasArg = CanvasFunction['args'];
// Overloads to change return type based on specs
function getByAlias(specs: CanvasFunction[], name: string): CanvasFunction;
// eslint-disable-next-line @typescript-eslint/unified-signatures
function getByAlias(specs: CanvasArg, name: string): ArgValue;
function getByAlias(specs: CanvasFunction[] | CanvasArg, name: string): CanvasFunction | ArgValue {
return untypedGetByAlias(specs, name);
}
/**
* Generates the AST with the given expression and then returns the function and argument definitions
* at the given position in the expression, if there are any.
*/
export function getFnArgDefAtPosition(
specs: CanvasFunction[],
expression: string,
position: number
) {
const text = expression.substr(0, position) + MARKER + expression.substr(position);
try {
const ast: ExpressionASTWithMeta = parse(text, { addMeta: true }) as ExpressionASTWithMeta;
const { ast: newAst, fnIndex, argName } = getFnArgAtPosition(ast, position);
const fn = newAst.node.chain[fnIndex].node;
const fnDef = getByAlias(specs, fn.function.replace(MARKER, ''));
if (fnDef && argName) {
const argDef = getByAlias(fnDef.args, argName);
return { fnDef, argDef };
}
return { fnDef };
} catch (e) {
// Fail silently
}
return [];
}
/**
* Gets a list of suggestions for the given expression at the given position. It does this by
* inserting a marker at the given position, then parsing the resulting expression. This way we can
* see what the marker would turn into, which tells us what sorts of things to suggest. For
* example, if the marker turns into a function name, then we suggest functions. If it turns into
* an unnamed argument, we suggest argument names. If it turns into a value, we suggest values.
*/
export function getAutocompleteSuggestions(
specs: CanvasFunction[],
expression: string,
position: number
) {
const text = expression.substr(0, position) + MARKER + expression.substr(position);
try {
const ast = parse(text, { addMeta: true }) as ExpressionASTWithMeta;
const { ast: newAst, fnIndex, argName, argIndex } = getFnArgAtPosition(ast, position);
const fn = newAst.node.chain[fnIndex].node;
if (fn.function.includes(MARKER)) {
return getFnNameSuggestions(specs, newAst, fnIndex);
}
if (argName === '_' && argIndex !== undefined) {
return getArgNameSuggestions(specs, newAst, fnIndex, argName, argIndex);
}
if (argName && argIndex !== undefined) {
return getArgValueSuggestions(specs, newAst, fnIndex, argName, argIndex);
}
} catch (e) {
// Fail silently
}
return [];
}
/**
Each entry of the node.chain has it's overall start and end position. For instance,
given the expression "link arg='something' | render" the link functions start position is 0 and end
position is 21.
This function is given the full ast and the current cursor position in the expression string.
It returns which function the cursor is in, as well as which argument for that function the cursor is in
if any.
*/
function getFnArgAtPosition(
ast: ExpressionASTWithMeta,
position: number
): { ast: ExpressionASTWithMeta; fnIndex: number; argName?: string; argIndex?: number } {
const fnIndex = ast.node.chain.findIndex(fn => fn.start <= position && position <= fn.end);
const fn = ast.node.chain[fnIndex];
for (const [argName, argValues] of Object.entries(fn.node.arguments)) {
for (let argIndex = 0; argIndex < argValues.length; argIndex++) {
const value = argValues[argIndex];
if (value.start <= position && position <= value.end) {
if (value.node !== null && isExpression(value)) {
return getFnArgAtPosition(value, position);
}
return { ast, fnIndex, argName, argIndex };
}
}
}
return { ast, fnIndex };
}
function getFnNameSuggestions(
specs: CanvasFunction[],
ast: ExpressionASTWithMeta,
fnIndex: number
) {
// Filter the list of functions by the text at the marker
const { start, end, node: fn } = ast.node.chain[fnIndex];
const query = fn.function.replace(MARKER, '');
const matchingFnDefs = specs.filter(({ name }) => textMatches(name, query));
// Sort by whether or not the function expects the previous function's return type, then by
// whether or not the function name starts with the text at the marker, then alphabetically
const prevFn = ast.node.chain[fnIndex - 1];
const prevFnDef = prevFn && getByAlias(specs, prevFn.node.function);
const prevFnType = prevFnDef && prevFnDef.type;
const comparator = combinedComparator<CanvasFunction>(
prevFnTypeComparator(prevFnType),
invokeWithProp<string, 'name', CanvasFunction, number>(startsWithComparator(query), 'name'),
invokeWithProp<string, 'name', CanvasFunction, number>(alphanumericalComparator, 'name')
);
const fnDefs = matchingFnDefs.sort(comparator);
return fnDefs.map(fnDef => {
return { type: 'function', text: fnDef.name + ' ', start, end: end - MARKER.length, fnDef };
});
}
function getArgNameSuggestions(
specs: CanvasFunction[],
ast: ExpressionASTWithMeta,
fnIndex: number,
argName: string,
argIndex: number
) {
// Get the list of args from the function definition
const fn = ast.node.chain[fnIndex].node;
const fnDef = getByAlias(specs, fn.function);
if (!fnDef) {
return [];
}
// We use the exact text instead of the value because it is always a string and might be quoted
const { text, start, end } = fn.arguments[argName][argIndex];
// Filter the list of args by the text at the marker
const query = text.replace(MARKER, '');
const matchingArgDefs = Object.entries<ArgValue>(fnDef.args).filter(([name]) =>
textMatches(name, query)
);
// Filter the list of args by those which aren't already present (unless they allow multi)
const argEntries = Object.entries(fn.arguments).map<[string, ExpressionArgASTWithMeta[]]>(
([name, values]) => {
return [name, values.filter(value => !value.text.includes(MARKER))];
}
);
const unusedArgDefs = matchingArgDefs.filter(([matchingArgName, matchingArgDef]) => {
if (matchingArgDef.multi) {
return true;
}
return !argEntries.some(([name, values]) => {
return (
values.length > 0 &&
(name === matchingArgName || (matchingArgDef.aliases || []).includes(name))
);
});
});
// Sort by whether or not the arg is also the unnamed, then by whether or not the arg name starts
// with the text at the marker, then alphabetically
const comparator = combinedComparator(
unnamedArgComparator,
invokeWithProp<string, 'name', ArgValue & { name: string }, number>(
startsWithComparator(query),
'name'
),
invokeWithProp<string, 'name', ArgValue & { name: string }, number>(
alphanumericalComparator,
'name'
)
);
const argDefs = unusedArgDefs.map(([name, arg]) => ({ name, ...arg })).sort(comparator);
return argDefs.map(argDef => {
return { type: 'argument', text: argDef.name + '=', start, end: end - MARKER.length, argDef };
});
}
function getArgValueSuggestions(
specs: CanvasFunction[],
ast: ExpressionASTWithMeta,
fnIndex: number,
argName: string,
argIndex: number
) {
// Get the list of values from the argument definition
const fn = ast.node.chain[fnIndex].node;
const fnDef = getByAlias(specs, fn.function);
if (!fnDef) {
return [];
}
const argDef = getByAlias(fnDef.args, argName);
if (!argDef) {
return [];
}
// Get suggestions from the argument definition, including the default
const { start, end, node } = fn.arguments[argName][argIndex];
if (typeof node !== 'string') {
return [];
}
const query = node.replace(MARKER, '');
const argOptions = argDef.options ? argDef.options : [];
let suggestions = [...argOptions];
if (argDef.default !== undefined) {
suggestions.push(argDef.default);
}
suggestions = uniq(suggestions);
// Filter the list of suggestions by the text at the marker
const filtered = suggestions.filter(option => textMatches(String(option), query));
// Sort by whether or not the value starts with the text at the marker, then alphabetically
const comparator = combinedComparator<any>(startsWithComparator(query), alphanumericalComparator);
const sorted = filtered.sort(comparator);
return sorted.map(value => {
const text = maybeQuote(value) + ' ';
return { start, end: end - MARKER.length, type: 'value', text };
});
}
function textMatches(text: string, query: string): boolean {
return text.toLowerCase().includes(query.toLowerCase().trim());
}
function maybeQuote(value: any) {
if (typeof value === 'string') {
if (value.match(/^\{.*\}$/)) {
return value;
}
return `"${value.replace(/"/g, '\\"')}"`;
}
return value;
}
function prevFnTypeComparator(prevFnType: any) {
return (a: CanvasFunction, b: CanvasFunction): number => {
return (
(b.context && b.context.types && b.context.types.includes(prevFnType) ? 1 : 0) -
(a.context && a.context.types && a.context.types.includes(prevFnType) ? 1 : 0)
);
};
}
function unnamedArgComparator(a: ArgValue, b: ArgValue): number {
return (
(b.aliases && b.aliases.includes('_') ? 1 : 0) - (a.aliases && a.aliases.includes('_') ? 1 : 0)
);
}
function alphanumericalComparator(a: any, b: any): number {
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
}
function startsWithComparator(query: string) {
return (a: any, b: any) =>
(String(b).startsWith(query) ? 1 : 0) - (String(a).startsWith(query) ? 1 : 0);
}
type Comparator<T> = (a: T, b: T) => number;
function combinedComparator<T>(...comparators: Array<Comparator<T>>): Comparator<T> {
return (a: T, b: T) =>
comparators.reduce((acc: number, comparator) => {
if (acc !== 0) {
return acc;
}
return comparator(a, b);
}, 0);
}
function invokeWithProp<
PropType,
PropName extends string,
ArgType extends { [key in PropName]: PropType },
FnReturnType
>(fn: (...args: PropType[]) => FnReturnType, prop: PropName): (...args: ArgType[]) => FnReturnType {
return (...args: Array<{ [key in PropName]: PropType }>) => {
return fn(...args.map(arg => arg[prop]));
};
}

View file

@ -19,7 +19,7 @@ import {
getPages,
} from '../../state/selectors/workpad';
import { zoomHandlerCreators } from '../../lib/app_handler_creators';
import { trackCanvasUiMetric } from '../../lib/ui_metric';
import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric';
import { LAUNCHED_FULLSCREEN, LAUNCHED_FULLSCREEN_AUTOPLAY } from '../../../common/lib/constants';
import { Workpad as Component } from './workpad';
@ -56,6 +56,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
if (value === true) {
trackCanvasUiMetric(
METRIC_TYPE.COUNT,
stateProps.autoplayEnabled
? [LAUNCHED_FULLSCREEN, LAUNCHED_FULLSCREEN_AUTOPLAY]
: LAUNCHED_FULLSCREEN

View file

@ -9,10 +9,14 @@
* @param cb: callback to do something with a function that has been found
*/
import { AST } from '../../types';
import { ExpressionAST, ExpressionArgAST } from '../../types';
export function collectFns(ast: AST, cb: (functionName: string) => void) {
if (ast.type === 'expression') {
function isExpression(maybeExpression: ExpressionArgAST): maybeExpression is ExpressionAST {
return typeof maybeExpression === 'object';
}
export function collectFns(ast: ExpressionArgAST, cb: (functionName: string) => void) {
if (isExpression(ast)) {
ast.chain.forEach(({ function: cFunction, arguments: cArguments }) => {
cb(cFunction);

View file

@ -4,9 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import expect from '@kbn/expect';
import { summarizeCustomElements } from '../custom_element_collector';
import { TelemetryCustomElementDocument } from '../../../types';
import { summarizeCustomElements } from './custom_element_collector';
import { TelemetryCustomElementDocument } from '../../types';
function mockCustomElement(...nodeExpressions: string[]): TelemetryCustomElementDocument {
return {
@ -21,7 +20,7 @@ function mockCustomElement(...nodeExpressions: string[]): TelemetryCustomElement
describe('custom_element_collector.handleResponse', () => {
describe('invalid responses', () => {
it('returns nothing if no valid hits', () => {
expect(summarizeCustomElements([])).to.eql({});
expect(summarizeCustomElements([])).toEqual({});
});
it('returns nothing if no valid elements', () => {
@ -31,7 +30,7 @@ describe('custom_element_collector.handleResponse', () => {
},
];
expect(summarizeCustomElements(customElements)).to.eql({});
expect(summarizeCustomElements(customElements)).toEqual({});
});
});
@ -39,10 +38,10 @@ describe('custom_element_collector.handleResponse', () => {
const elements = [mockCustomElement(''), mockCustomElement('')];
const data = summarizeCustomElements(elements);
expect(data.custom_elements).to.not.be(null);
expect(data.custom_elements).not.toBe(null);
if (data.custom_elements) {
expect(data.custom_elements.count).to.equal(elements.length);
expect(data.custom_elements.count).toEqual(elements.length);
}
});
@ -54,10 +53,10 @@ describe('custom_element_collector.handleResponse', () => {
const elements = [mockCustomElement(functions1.join('|')), mockCustomElement(...functions2)];
const data = summarizeCustomElements(elements);
expect(data.custom_elements).to.not.be(null);
expect(data.custom_elements).not.toBe(null);
if (data.custom_elements) {
expect(data.custom_elements.functions_in_use).to.eql(expectedFunctions);
expect(data.custom_elements.functions_in_use).toEqual(expectedFunctions);
}
});
@ -74,12 +73,12 @@ describe('custom_element_collector.handleResponse', () => {
];
const result = summarizeCustomElements(elements);
expect(result.custom_elements).to.not.be(null);
expect(result.custom_elements).not.toBe(null);
if (result.custom_elements) {
expect(result.custom_elements.elements.max).to.equal(functionsMax.length);
expect(result.custom_elements.elements.min).to.equal(functionsMin.length);
expect(result.custom_elements.elements.avg).to.equal(avgFunctions);
expect(result.custom_elements.elements.max).toEqual(functionsMax.length);
expect(result.custom_elements.elements.min).toEqual(functionsMin.length);
expect(result.custom_elements.elements.avg).toEqual(avgFunctions);
}
});
});

View file

@ -9,7 +9,7 @@ import { get } from 'lodash';
import { fromExpression } from '@kbn/interpreter/common';
import { collectFns } from './collector_helpers';
import { TelemetryCollector } from '../../types';
import { AST, TelemetryCustomElement, TelemetryCustomElementDocument } from '../../types';
import { ExpressionAST, TelemetryCustomElement, TelemetryCustomElementDocument } from '../../types';
const CUSTOM_ELEMENT_TYPE = 'canvas-element';
interface CustomElementSearch {
@ -48,7 +48,6 @@ function parseJsonOrNull(maybeJson: string) {
/**
Calculate statistics about a collection of CustomElement Documents
@param customElements - Array of CustomElement documents
@returns Statistics about how Custom Elements are being used
*/
@ -76,7 +75,7 @@ export function summarizeCustomElements(
parsedContents.map(contents => {
contents.selectedNodes.map(node => {
const ast: AST = fromExpression(node.expression) as AST; // TODO: Remove once fromExpression is properly typed
const ast: ExpressionAST = fromExpression(node.expression) as ExpressionAST; // TODO: Remove once fromExpression is properly typed
collectFns(ast, (cFunction: string) => {
functionSet.add(cFunction);
});

View file

@ -4,15 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import expect from '@kbn/expect';
import { summarizeWorkpads } from '../workpad_collector';
// @ts-ignore Missing local definition
import { workpads } from '../../../__tests__/fixtures/workpads';
import clonedeep from 'lodash.clonedeep';
import { summarizeWorkpads } from './workpad_collector';
import { workpads } from '../../__tests__/fixtures/workpads';
describe('usage collector handle es response data', () => {
it('should summarize workpads, pages, and elements', () => {
const usage = summarizeWorkpads(workpads);
expect(usage).to.eql({
expect(usage).toEqual({
workpads: {
total: 6, // num workpad documents in .kibana index
},
@ -54,29 +53,12 @@ describe('usage collector handle es response data', () => {
});
it('should collect correctly if an expression has null as an argument (possible sub-expression)', () => {
const mockWorkpads = [
{
name: 'Tweet Data Workpad 1',
id: 'workpad-ae00567f-5510-4d68-b07f-6b1661948e03',
width: 792,
height: 612,
page: 0,
pages: [
{
elements: [
{
expression: 'toast butter=null',
},
],
},
],
'@timestamp': '2018-07-26T02:29:00.964Z',
'@created': '2018-07-25T22:56:31.460Z',
assets: {},
},
];
const workpad = clonedeep(workpads[0]);
workpad.pages[0].elements[0].expression = 'toast butter=null';
const mockWorkpads = [workpad];
const usage = summarizeWorkpads(mockWorkpads);
expect(usage).to.eql({
expect(usage).toEqual({
workpads: { total: 1 },
pages: { total: 1, per_workpad: { avg: 1, min: 1, max: 1 } },
elements: { total: 1, per_page: { avg: 1, min: 1, max: 1 } },
@ -85,21 +67,11 @@ describe('usage collector handle es response data', () => {
});
it('should fail gracefully if workpad has 0 pages (corrupted workpad)', () => {
const mockWorkpadsCorrupted = [
{
name: 'Tweet Data Workpad 2',
id: 'workpad-ae00567f-5510-4d68-b07f-6b1661948e03',
width: 792,
height: 612,
page: 0,
pages: [], // pages should never be empty, and *may* prevent the ui from rendering properly
'@timestamp': '2018-07-26T02:29:00.964Z',
'@created': '2018-07-25T22:56:31.460Z',
assets: {},
},
];
const workpad = clonedeep(workpads[0]);
workpad.pages = [];
const mockWorkpadsCorrupted = [workpad];
const usage = summarizeWorkpads(mockWorkpadsCorrupted);
expect(usage).to.eql({
expect(usage).toEqual({
workpads: { total: 1 },
pages: { total: 0, per_workpad: { avg: 0, min: 0, max: 0 } },
elements: undefined,
@ -109,6 +81,6 @@ describe('usage collector handle es response data', () => {
it('should fail gracefully in general', () => {
const usage = summarizeWorkpads([]);
expect(usage).to.eql({});
expect(usage).toEqual({});
});
});

View file

@ -9,23 +9,10 @@ import { sum as arraySum, min as arrayMin, max as arrayMax, get } from 'lodash';
import { fromExpression } from '@kbn/interpreter/common';
import { CANVAS_TYPE } from '../../common/lib/constants';
import { collectFns } from './collector_helpers';
import { AST, TelemetryCollector } from '../../types';
interface Element {
expression: string;
}
interface Page {
elements: Element[];
}
interface Workpad {
pages: Page[];
[s: string]: any; // Only concerned with the pages here, but allow workpads to have any values
}
import { ExpressionAST, TelemetryCollector, CanvasWorkpad } from '../../types';
interface WorkpadSearch {
[CANVAS_TYPE]: Workpad;
[CANVAS_TYPE]: CanvasWorkpad;
}
interface WorkpadTelemetry {
@ -61,11 +48,10 @@ interface WorkpadTelemetry {
/**
Gather statistic about the given workpads
@param workpadDocs a collection of workpad documents
@returns Workpad Telemetry Data
*/
export function summarizeWorkpads(workpadDocs: Workpad[]): WorkpadTelemetry {
export function summarizeWorkpads(workpadDocs: CanvasWorkpad[]): WorkpadTelemetry {
const functionSet = new Set<string>();
if (workpadDocs.length === 0) {
@ -87,7 +73,7 @@ export function summarizeWorkpads(workpadDocs: Workpad[]): WorkpadTelemetry {
);
const functionCounts = workpad.pages.reduce<number[]>((accum, page) => {
return page.elements.map(element => {
const ast: AST = fromExpression(element.expression) as AST; // TODO: Remove once fromExpression is properly typed
const ast: ExpressionAST = fromExpression(element.expression) as ExpressionAST; // TODO: Remove once fromExpression is properly typed
collectFns(ast, cFunction => {
functionSet.add(cFunction);
});

View file

@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ExpressionAST } from 'src/plugins/data/common/expressions';
export interface ElementSpec {
name: string;
image: string;
@ -49,16 +51,6 @@ export interface CustomElement {
content: string;
}
export interface AST {
type: string;
chain: Array<{
function: string;
arguments: {
[s: string]: AST[];
};
}>;
}
export interface ElementPosition {
/**
* distance from the left edge of the page
@ -102,5 +94,5 @@ export interface PositionedElement {
/**
* AST of the Canvas expression for the element
*/
ast: AST;
ast: ExpressionAST;
}

View file

@ -63,7 +63,11 @@ function stringMatch(str: string | undefined, substr: string) {
);
}
export const DataFrameAnalyticsList: FC = () => {
interface Props {
isManagementTable?: boolean;
}
// isManagementTable - for use in Kibana managagement ML section
export const DataFrameAnalyticsList: FC<Props> = ({ isManagementTable }) => {
const [isInitialized, setIsInitialized] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [blockRefresh, setBlockRefresh] = useState(false);
@ -225,7 +229,7 @@ export const DataFrameAnalyticsList: FC = () => {
);
}
const columns = getColumns(expandedRowItemIds, setExpandedRowItemIds);
const columns = getColumns(expandedRowItemIds, setExpandedRowItemIds, isManagementTable);
const sorting = {
sort: {

View file

@ -59,7 +59,8 @@ export const getTaskStateBadge = (
export const getColumns = (
expandedRowItemIds: DataFrameAnalyticsId[],
setExpandedRowItemIds: React.Dispatch<React.SetStateAction<DataFrameAnalyticsId[]>>
setExpandedRowItemIds: React.Dispatch<React.SetStateAction<DataFrameAnalyticsId[]>>,
isManagementTable: boolean = false
) => {
const actions = getActions();
@ -75,8 +76,8 @@ export const getColumns = (
// spread to a new array otherwise the component wouldn't re-render
setExpandedRowItemIds([...expandedRowItemIds]);
}
return [
// update possible column types to something like (FieldDataColumn | ComputedColumn | ActionsColumn)[] when they have been added to EUI
const columns: any[] = [
{
align: RIGHT_ALIGNMENT,
width: '40px',
@ -205,12 +206,17 @@ export const getColumns = (
},
width: '100px',
},
{
];
if (isManagementTable === false) {
columns.push({
name: i18n.translate('xpack.ml.dataframe.analyticsList.tableActionLabel', {
defaultMessage: 'Actions',
}),
actions,
width: '200px',
},
];
});
}
return columns;
};

View file

@ -80,6 +80,11 @@ class JobsListUI extends Component {
};
getJobIdLink(id) {
// Don't allow link to job if ML is not enabled in current space
if (this.props.isMlEnabledInSpace === false) {
return id;
}
return (
<EuiLink href={getJobIdUrl(id)}>
{id}
@ -253,6 +258,10 @@ class JobsListUI extends Component {
<EuiBadge color={'hollow'}>{'all'}</EuiBadge>
)
});
// Remove actions if Ml not enabled in current space
if (this.props.isMlEnabledInSpace === false) {
columns.pop();
}
} else {
// insert before last column
columns.splice(columns.length - 1, 0, {
@ -344,6 +353,8 @@ class JobsListUI extends Component {
JobsListUI.propTypes = {
jobsSummaryList: PropTypes.array.isRequired,
fullJobsList: PropTypes.object.isRequired,
isManagementTable: PropTypes.bool,
isMlEnabledInSpace: PropTypes.bool,
itemIdToExpandedRowMap: PropTypes.object.isRequired,
toggleRow: PropTypes.func.isRequired,
selectJobChange: PropTypes.func.isRequired,
@ -355,6 +366,8 @@ JobsListUI.propTypes = {
loading: PropTypes.bool,
};
JobsListUI.defaultProps = {
isManagementTable: false,
isMlEnabledInSpace: true,
loading: false,
};

View file

@ -362,21 +362,22 @@ export class JobsListView extends Component {
}
renderManagementJobsListComponents() {
const { loading } = this.state;
const { loading, itemIdToExpandedRowMap, filteredJobsSummaryList, fullJobsList, selectedJobs } = this.state;
return (
<div className="managementJobsList">
<div>
<JobFilterBar setFilters={this.setFilters} />
</div>
<JobsList
jobsSummaryList={this.state.filteredJobsSummaryList}
fullJobsList={this.state.fullJobsList}
itemIdToExpandedRowMap={this.state.itemIdToExpandedRowMap}
jobsSummaryList={filteredJobsSummaryList}
fullJobsList={fullJobsList}
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
toggleRow={this.toggleRow}
selectJobChange={this.selectJobChange}
selectedJobsCount={this.state.selectedJobs.length}
selectedJobsCount={selectedJobs.length}
loading={loading}
isManagementTable={true}
isMlEnabledInSpace={this.props.isMlEnabledInSpace}
/>
</div>
);

View file

@ -8,7 +8,7 @@ import { each } from 'lodash';
import { toastNotifications } from 'ui/notify';
import { mlMessageBarService } from 'plugins/ml/components/messagebar/messagebar_service';
import rison from 'rison-node';
import chrome from 'ui/chrome'; // TODO: get from context once walter's PR is merged
import chrome from 'ui/chrome';
import { mlJobService } from 'plugins/ml/services/job_service';
import { ml } from 'plugins/ml/services/ml_api_service';

View file

@ -95,11 +95,19 @@ export class JobCreator {
return agg !== undefined ? agg : null;
}
public get aggregations(): Aggregation[] {
return this._aggs;
}
public getField(index: number): Field | null {
const field = this._fields[index];
return field !== undefined ? field : null;
}
public get fields(): Field[] {
return this._fields;
}
public set bucketSpan(bucketSpan: BucketSpan) {
this._job_config.analysis_config.bucket_span = bucketSpan;
}

View file

@ -5,12 +5,18 @@
*/
import React, { FC, useContext, useEffect, useState } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { BucketSpanInput } from './bucket_span_input';
import { JobCreatorContext } from '../../../job_creator_context';
import { Description } from './description';
import { BucketSpanEstimator } from '../bucket_span_estimator';
export const BucketSpan: FC = () => {
interface Props {
setIsValid: (proceed: boolean) => void;
}
export const BucketSpan: FC<Props> = ({ setIsValid }) => {
const {
jobCreator,
jobCreatorUpdate,
@ -20,6 +26,7 @@ export const BucketSpan: FC = () => {
} = useContext(JobCreatorContext);
const [bucketSpan, setBucketSpan] = useState(jobCreator.bucketSpan);
const [validation, setValidation] = useState(jobValidator.bucketSpan);
const [estimating, setEstimating] = useState(false);
useEffect(() => {
jobCreator.bucketSpan = bucketSpan;
@ -34,13 +41,25 @@ export const BucketSpan: FC = () => {
setValidation(jobValidator.bucketSpan);
}, [jobValidatorUpdated]);
useEffect(() => {
setIsValid(estimating === false);
}, [estimating]);
return (
<Description validation={validation}>
<BucketSpanInput
setBucketSpan={setBucketSpan}
bucketSpan={bucketSpan}
isInvalid={validation.valid === false}
/>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>
<BucketSpanInput
setBucketSpan={setBucketSpan}
bucketSpan={bucketSpan}
isInvalid={validation.valid === false}
disabled={estimating}
/>
</EuiFlexItem>
<EuiFlexItem>
<BucketSpanEstimator setEstimating={setEstimating} />
</EuiFlexItem>
</EuiFlexGroup>
</Description>
);
};

View file

@ -11,11 +11,13 @@ interface Props {
bucketSpan: string;
setBucketSpan: (bs: string) => void;
isInvalid: boolean;
disabled: boolean;
}
export const BucketSpanInput: FC<Props> = ({ bucketSpan, setBucketSpan, isInvalid }) => {
export const BucketSpanInput: FC<Props> = ({ bucketSpan, setBucketSpan, isInvalid, disabled }) => {
return (
<EuiFieldText
disabled={disabled}
placeholder="Bucket span"
value={bucketSpan}
onChange={e => setBucketSpan(e.target.value)}

View file

@ -0,0 +1,28 @@
/*
* 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, { FC, useEffect } from 'react';
import { EuiButton } from '@elastic/eui';
import { useEstimateBucketSpan, ESTIMATE_STATUS } from './estimate_bucket_span';
interface Props {
setEstimating(estimating: boolean): void;
}
export const BucketSpanEstimator: FC<Props> = ({ setEstimating }) => {
const { status, estimateBucketSpan } = useEstimateBucketSpan();
useEffect(() => {
setEstimating(status === ESTIMATE_STATUS.RUNNING);
}, [status]);
return (
<EuiButton disabled={status === ESTIMATE_STATUS.RUNNING} onClick={estimateBucketSpan}>
Estimate bucket span
</EuiButton>
);
};

View file

@ -0,0 +1,70 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useContext, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { toastNotifications } from 'ui/notify';
import { JobCreatorContext } from '../../../job_creator_context';
import { EVENT_RATE_FIELD_ID } from '../../../../../../../../common/types/fields';
import { isMultiMetricJobCreator, isPopulationJobCreator } from '../../../../../common/job_creator';
import { ml } from '../../../../../../../services/ml_api_service';
import { useKibanaContext } from '../../../../../../../contexts/kibana';
export enum ESTIMATE_STATUS {
NOT_RUNNING,
RUNNING,
}
export function useEstimateBucketSpan() {
const { jobCreator, jobCreatorUpdate } = useContext(JobCreatorContext);
const kibanaContext = useKibanaContext();
const [status, setStatus] = useState(ESTIMATE_STATUS.NOT_RUNNING);
const data = {
aggTypes: jobCreator.aggregations.map(a => a.dslName),
duration: {
start: jobCreator.start,
end: jobCreator.end,
},
fields: jobCreator.fields.map(f => (f.id === EVENT_RATE_FIELD_ID ? null : f.id)),
index: kibanaContext.currentIndexPattern.title,
query: kibanaContext.combinedQuery,
splitField:
(isMultiMetricJobCreator(jobCreator) || isPopulationJobCreator(jobCreator)) &&
jobCreator.splitField !== null
? jobCreator.splitField.id
: undefined,
timeField: kibanaContext.currentIndexPattern.timeFieldName,
};
async function estimateBucketSpan() {
setStatus(ESTIMATE_STATUS.RUNNING);
const { name, error, message } = await ml.estimateBucketSpan(data);
setStatus(ESTIMATE_STATUS.NOT_RUNNING);
if (error === true) {
let text = '';
if (message !== undefined) {
if (typeof message === 'object') {
text = message.msg || JSON.stringify(message);
} else {
text = message;
}
}
toastNotifications.addDanger({
title: i18n.translate('xpack.ml.newJob.wizard.estimateBucketSpanError', {
defaultMessage: `Bucket span estimation error`,
}),
text,
});
} else {
jobCreator.bucketSpan = name;
jobCreatorUpdate();
}
}
return { status, estimateBucketSpan };
}

View file

@ -0,0 +1,6 @@
/*
* 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 { BucketSpanEstimator } from './bucket_span_estimator';

View file

@ -45,7 +45,7 @@ export const MultiMetricSettings: FC<Props> = ({ isActive, setIsValid }) => {
</EuiFlexGroup>
<EuiFlexGroup gutterSize="xl">
<EuiFlexItem>
<BucketSpan />
<BucketSpan setIsValid={setIsValid} />
</EuiFlexItem>
<EuiFlexItem />
</EuiFlexGroup>

View file

@ -36,7 +36,7 @@ export const PopulationSettings: FC<Props> = ({ isActive, setIsValid }) => {
<Fragment>
<EuiFlexGroup gutterSize="xl">
<EuiFlexItem>
<BucketSpan />
<BucketSpan setIsValid={setIsValid} />
</EuiFlexItem>
<EuiFlexItem>
<Influencers />

View file

@ -51,7 +51,7 @@ export const SingleMetricSettings: FC<Props> = ({ isActive, setIsValid }) => {
<Fragment>
<EuiFlexGroup gutterSize="xl">
<EuiFlexItem>
<BucketSpan />
<BucketSpan setIsValid={setIsValid} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup>

View file

@ -15,9 +15,13 @@ import { management } from 'ui/management';
import { xpackInfo } from 'plugins/xpack_main/services/xpack_info';
import { i18n } from '@kbn/i18n';
import { JOBS_LIST_PATH } from './management_urls';
import { LICENSE_TYPE } from '../../common/constants/license';
import 'plugins/ml/management/jobs_list';
if (xpackInfo.get('features.ml.showLinks', false) === true) {
if (
xpackInfo.get('features.ml.showLinks', false) === true &&
xpackInfo.get('features.ml.licenseType') === LICENSE_TYPE.FULL
) {
management.register('ml', {
display: i18n.translate('xpack.ml.management.mlTitle', {
defaultMessage: 'Machine Learning',

View file

@ -1,3 +1,4 @@
@import './jobs_list_page/stats_bar';
@import './jobs_list_page/buttons';
@import './jobs_list_page/expanded_row';
@import './jobs_list_page/analytics_table';

View file

@ -0,0 +1,28 @@
.mlAnalyticsTable {
// Using an override as a last resort because we cannot set custom classes on
// nested upstream components. The opening animation limits the height
// of the expanded row to 1000px which turned out to be not predictable.
// The animation could also result in flickering with expanded rows
// where the inner content would result in the DOM changing the height.
.euiTableRow-isExpandedRow .euiTableCellContent {
animation: none !important;
.euiTableCellContent__text {
width: 100%;
}
}
// Another override: Because an update to the table replaces the DOM, the same
// icon would still again fade in with an animation. If the table refreshes with
// e.g. 1s this would result in a blinking icon effect.
.euiIcon-isLoaded {
animation: none !important;
}
}
.mlAnalyticsProgressBar {
margin-bottom: $euiSizeM;
}
.mlTaskStateBadge {
max-width: 100px;
}

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment } from 'react';
import React, { Fragment, FC } from 'react';
import { i18n } from '@kbn/i18n';
import { I18nContext } from 'ui/i18n';
import {
@ -19,8 +19,13 @@ import {
// @ts-ignore undeclared module
import { JobsListView } from '../../../../jobs/jobs_list/components/jobs_list_view';
import { DataFrameAnalyticsList } from '../../../../data_frame_analytics/pages/analytics_management/components/analytics_list';
export const JobsListPage = () => {
interface Props {
isMlEnabledInSpace: boolean;
}
export const JobsListPage: FC<Props> = ({ isMlEnabledInSpace }) => {
const tabs = [
{
id: 'anomaly_detection_jobs',
@ -30,23 +35,24 @@ export const JobsListPage = () => {
content: (
<Fragment>
<EuiSpacer size="m" />
<JobsListView isManagementTable={true} />
<JobsListView isManagementTable={true} isMlEnabledInSpace={isMlEnabledInSpace} />
</Fragment>
),
},
{
id: 'analytics_jobs',
name: i18n.translate('xpack.ml.management.jobsList.analyticsTab', {
defaultMessage: 'Analytics',
}),
content: (
<Fragment>
<EuiSpacer size="m" />
<DataFrameAnalyticsList isManagementTable={true} />
</Fragment>
),
},
// {
// id: 'analytics_jobs',
// name: i18n.translate('xpack.ml.management.jobsList.analyticsTab', {
// defaultMessage: 'Analytics',
// }),
// content: renderAnalyticsJobs(),
// },
];
// function renderAnalyticsJobs() {
// return <div>Analytics job placeholder</div>;
// }
function renderTabs() {
return <EuiTabbedContent size="s" tabs={tabs} initialSelectedTab={tabs[0]} />;
}

View file

@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
// @ts-ignore no declaration module
import { ReactDOM, render, unmountComponentAtNode } from 'react-dom';
import ReactDOM, { render, unmountComponentAtNode } from 'react-dom';
import React from 'react';
import routes from 'ui/routes';
import { canGetManagementMlJobs } from '../../privilege/check_privilege';
import { JOBS_LIST_PATH, ACCESS_DENIED_PATH } from '../management_urls';
@ -22,14 +22,19 @@ routes.when(JOBS_LIST_PATH, {
resolve: {
checkPrivilege: canGetManagementMlJobs,
},
controller($scope) {
controller($scope, checkPrivilege) {
const { mlFeatureEnabledInSpace } = checkPrivilege;
$scope.$on('$destroy', () => {
const elem = document.getElementById('kibanaManagementMLSection');
if (elem) unmountComponentAtNode(elem);
});
$scope.$$postDigest(() => {
const element = document.getElementById('kibanaManagementMLSection');
render(JobsListPage(), element);
ReactDOM.render(
React.createElement(JobsListPage, { isMlEnabledInSpace: mlFeatureEnabledInSpace }),
element
);
});
},
});

View file

@ -17,18 +17,20 @@ let privileges: Privileges = getDefaultPrivileges();
// manage_ml requires all monitor and admin cluster privileges: https://github.com/elastic/elasticsearch/blob/664a29c8905d8ce9ba8c18aa1ed5c5de93a0eabc/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilege.java#L53
export function canGetManagementMlJobs(kbnUrl: any) {
return new Promise((resolve, reject) => {
getManageMlPrivileges().then(({ capabilities, isPlatinumOrTrialLicense }) => {
privileges = capabilities;
// Loop through all privilages to ensure they are all set to true.
const isManageML = Object.values(privileges).every(p => p === true);
getManageMlPrivileges().then(
({ capabilities, isPlatinumOrTrialLicense, mlFeatureEnabledInSpace }) => {
privileges = capabilities;
// Loop through all privilages to ensure they are all set to true.
const isManageML = Object.values(privileges).every(p => p === true);
if (isManageML === true && isPlatinumOrTrialLicense === true) {
return resolve();
} else {
kbnUrl.redirect(ACCESS_DENIED_PATH);
return reject();
if (isManageML === true && isPlatinumOrTrialLicense === true) {
return resolve({ mlFeatureEnabledInSpace });
} else {
kbnUrl.redirect(ACCESS_DENIED_PATH);
return reject();
}
}
});
);
});
}

View file

@ -121,6 +121,10 @@ declare interface Ml {
end: number
): Promise<{ progress: number; isRunning: boolean }>;
};
estimateBucketSpan(
data: object
): Promise<{ name: string; ms: number; error?: boolean; message?: { msg: string } | string }>;
}
declare const ml: Ml;

View file

@ -26,7 +26,8 @@ interface Response {
export function privilegesProvider(
callWithRequest: callWithRequestType,
xpackMainPlugin: XPackMainPlugin,
isMlEnabledInSpace: () => Promise<boolean>
isMlEnabledInSpace: () => Promise<boolean>,
ignoreSpaces: boolean = false
) {
const { isUpgradeInProgress } = upgradeCheckProvider(callWithRequest);
async function getPrivileges(): Promise<Response> {
@ -47,7 +48,7 @@ export function privilegesProvider(
? setFullActionPrivileges
: setBasicActionPrivileges;
if (mlFeatureEnabledInSpace === false) {
if (mlFeatureEnabledInSpace === false && ignoreSpaces === false) {
// if ML isn't enabled in the current space,
// return with the default privileges (all false)
return {

View file

@ -99,12 +99,12 @@ export function systemRoutes({
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
try {
const ignoreSpaces = request.query && request.query.ignoreSpaces === 'true';
// if spaces is disabled or ignoreSpace is true force isMlEnabledInSpace to be true
const { isMlEnabledInSpace } = (spacesPlugin !== undefined && ignoreSpaces === false) ?
// if spaces is disabled force isMlEnabledInSpace to be true
const { isMlEnabledInSpace } = spacesPlugin !== undefined ?
spacesUtilsProvider(spacesPlugin, request, config) :
{ isMlEnabledInSpace: async () => true };
const { getPrivileges } = privilegesProvider(callWithRequest, xpackMainPlugin, isMlEnabledInSpace);
const { getPrivileges } = privilegesProvider(callWithRequest, xpackMainPlugin, isMlEnabledInSpace, ignoreSpaces);
return await getPrivileges();
} catch (error) {
return wrapError(error);

View file

@ -0,0 +1,78 @@
/*
* 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 sinon from 'sinon';
import { DEFAULT_CSP_RULES } from '../../../../../../../../src/legacy/server/csp';
import {
getMockCallWithInternal,
getMockKbnServer,
getMockTaskFetch,
} from '../../../../test_utils';
import { createCspCollector } from './csp_collector';
test('fetches whether strict mode is enabled', async () => {
const { collector, mockConfig } = setupCollector();
expect((await collector.fetch()).strict).toEqual(true);
mockConfig.get.withArgs('csp.strict').returns(false);
expect((await collector.fetch()).strict).toEqual(false);
});
test('fetches whether the legacy browser warning is enabled', async () => {
const { collector, mockConfig } = setupCollector();
expect((await collector.fetch()).warnLegacyBrowsers).toEqual(true);
mockConfig.get.withArgs('csp.warnLegacyBrowsers').returns(false);
expect((await collector.fetch()).warnLegacyBrowsers).toEqual(false);
});
test('fetches whether the csp rules have been changed or not', async () => {
const { collector, mockConfig } = setupCollector();
expect((await collector.fetch()).rulesChangedFromDefault).toEqual(false);
mockConfig.get.withArgs('csp.rules').returns(['not', 'default']);
expect((await collector.fetch()).rulesChangedFromDefault).toEqual(true);
});
test('does not include raw csp.rules under any property names', async () => {
const { collector } = setupCollector();
// It's important that we do not send the value of csp.rules here as it
// can be customized with values that can be identifiable to given
// installs, such as URLs
//
// We use a snapshot here to ensure csp.rules isn't finding its way into the
// payload under some new and unexpected variable name (e.g. cspRules).
expect(await collector.fetch()).toMatchInlineSnapshot(`
Object {
"rulesChangedFromDefault": false,
"strict": true,
"warnLegacyBrowsers": true,
}
`);
});
test('does not arbitrarily fetch other csp configurations (e.g. whitelist only)', async () => {
const { collector, mockConfig } = setupCollector();
mockConfig.get.withArgs('csp.foo').returns('bar');
expect(await collector.fetch()).not.toHaveProperty('foo');
});
function setupCollector() {
const mockConfig = { get: sinon.stub() };
mockConfig.get.withArgs('csp.rules').returns(DEFAULT_CSP_RULES);
mockConfig.get.withArgs('csp.strict').returns(true);
mockConfig.get.withArgs('csp.warnLegacyBrowsers').returns(true);
const mockKbnServer = getMockKbnServer(getMockCallWithInternal(), getMockTaskFetch(), mockConfig);
return { mockConfig, collector: createCspCollector(mockKbnServer) };
}

View file

@ -0,0 +1,39 @@
/*
* 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 {
createCSPRuleString,
DEFAULT_CSP_RULES,
} from '../../../../../../../../src/legacy/server/csp';
import { HapiServer } from '../../../../';
export function createCspCollector(server: HapiServer) {
return {
type: 'csp',
isReady: () => true,
async fetch() {
const config = server.config();
// It's important that we do not send the value of csp.rules here as it
// can be customized with values that can be identifiable to given
// installs, such as URLs
const defaultRulesString = createCSPRuleString([...DEFAULT_CSP_RULES]);
const actualRulesString = createCSPRuleString(config.get('csp.rules'));
return {
strict: config.get('csp.strict'),
warnLegacyBrowsers: config.get('csp.warnLegacyBrowsers'),
rulesChangedFromDefault: defaultRulesString !== actualRulesString,
};
},
};
}
export function registerCspCollector(server: HapiServer): void {
const { usage } = server;
const collector = usage.collectorSet.makeUsageCollector(createCspCollector(server));
usage.collectorSet.register(collector);
}

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 { registerCspCollector } from './csp_collector';

View file

@ -5,8 +5,10 @@
*/
import { HapiServer } from '../../../';
import { registerCspCollector } from './csp';
import { registerVisualizationsCollector } from './visualizations/register_usage_collector';
export function registerCollectors(server: HapiServer) {
registerVisualizationsCollector(server);
registerCspCollector(server);
}

View file

@ -30,9 +30,16 @@ export const getMockTaskFetch = (docs: TaskInstance[] = defaultMockTaskDocs) =>
return () => Promise.resolve({ docs });
};
export const getMockConfig = () => {
return {
get: () => '',
};
};
export const getMockKbnServer = (
mockCallWithInternal = getMockCallWithInternal(),
mockTaskFetch = getMockTaskFetch()
mockTaskFetch = getMockTaskFetch(),
mockConfig = getMockConfig()
): HapiServer => ({
plugins: {
elasticsearch: {
@ -53,6 +60,6 @@ export const getMockKbnServer = (
register: () => undefined,
},
},
config: () => ({ get: () => '' }),
config: () => mockConfig,
log: () => undefined,
});

View file

@ -1,3 +1,7 @@
{
"baseUrl": "http://localhost:5601"
"baseUrl": "http://localhost:5601",
"screenshotsFolder": "../../../../target/kibana-siem/cypress/screenshots",
"trashAssetsBeforeRuns": false,
"video": false,
"videosFolder": "../../../../target/kibana-siem/cypress/videos"
}

View file

@ -1,70 +1,242 @@
# Cypress Tests
The `siem/cypress` directory contains end to end tests (specific to the `SIEM` app) that execute via [Cypress](https://www.cypress.io/).
The `siem/cypress` directory contains end to end tests, (plus a few tests
that rely on mocked API calls), that execute via [Cypress](https://www.cypress.io/).
At present, these tests are only executed in a local development environment; they are **not** integrated in the Kibana CI infrastructure, and therefore do **not** run automatically when you submit a PR.
Cypress tests may be run against:
See the `Server and Authentication Requirements` section below for additional details.
- A local Kibana instance, interactively or via the command line. Credentials
are specified via `kibna.dev.yml` or environment variables.
- A remote Elastic Cloud instance (override `baseUrl`), interactively or via
the command line. Again, credentials are specified via `kibna.dev.yml` or
environment variables.
- As part of CI (override `baseUrl` and pass credentials via the
`CYPRESS_ELASTICSEARCH_USERNAME` and `CYPRESS_ELASTICSEARCH_PASSWORD`
environment variables), via command line.
## Organizing Tests and (Mock) Data
At present, Cypress tests are only executed manually. They are **not** yet
integrated in the Kibana CI infrastructure, and therefore do **not** run
automatically when you submit a PR.
- Code and CSS selectors that may be re-used across tests should be added to `siem/cypress/integration/lib`, as described below
- Smoke Tests are located in `siem/cypress/integration/smoke_tests`
- Mocked responses from the server are located in `siem/cypress/fixtures`
## Smoke Tests
### `cypress/integration/lib`
Smoke Tests are located in `siem/cypress/integration/smoke_tests`
The `cypress/integration/lib` folder contains code intended to be re-used across many different tests.
## Test Helpers
- Files named `helpers.ts` (e.g. `siem/cypress/integration/lib/login/helpers.ts`) contain functions (e.g. `login`) that may be imported and invoked from multiple tests.
_Test helpers_ are functions that may be re-used across tests.
- Files named `selectors.ts` export CSS selectors for re-use. For example, `siem/cypress/integration/lib/login/selectors.ts` exports the following selector that matches the Username text area in the Kibana login page:
- Reusable code and CSS selectors should be added to
`siem/cypress/integration/lib`, as described below.
```
### Reusable Test Helper Functions and CSS Selectors
The `cypress/integration/lib` directory contains code intended to be re-used
across many different tests. Add reusable test helper functions and CSS
selectors to directories under `cypress/integration/lib`.
- Files named `helpers.ts` (e.g. `siem/cypress/integration/lib/login/helpers.ts`)
contain functions (e.g. `login`) that may be imported and invoked from multiple tests.
- Files named `selectors.ts` export CSS selectors for re-use. For example,
`siem/cypress/integration/lib/login/selectors.ts` exports the following selector
that matches the Username text area in the Kibana login page:
```sh
export const USERNAME = '[data-test-subj="loginUsername"]';
```
## Server and Authentication Requirements
## Mock Data
The current version of the Smoke Tests require running a local Kibana server that connects to an instance of `elasticsearch`. A file named `config/kibana.dev.yml` like the example below is required to run the tests:
We prefer not to mock API responses in most of our smoke tests, but sometimes
it's necessary because a test must assert that a specific value is rendered,
and it's not possible to derive that value based on the data in the
envrionment where tests are running.
Mocked responses API from the server are located in `siem/cypress/fixtures`.
## Authentication
When running tests, there are two ways to specify the credentials used to
authenticate with Kibana:
- Via `kibana.dev.yml` (recommended for developers)
- Via the `CYPRESS_ELASTICSEARCH_USERNAME` and `CYPRESS_ELASTICSEARCH_PASSWORD`
environment variables (recommended for CI), or when testing a remote Kibana
instance, e.g. in Elastic Cloud.
Note: Tests that use the `login()` test helper function for authentication will
automatically use the `CYPRESS_ELASTICSEARCH_USERNAME` and `CYPRESS_ELASTICSEARCH_PASSWORD`
environment variables when they are defined, and fall back to the values in
`config/kibana.dev.yml` when they are unset.
### Content Security Policy (CSP) Settings
Your local or cloud Kibana server must have the `csp.strict: false` setting
configured in `kibana.dev.yml`, or `kibana.yml`, as shown in the example below:
```yaml
csp.strict: false
```
The above setting is required to prevent the _Please upgrade
your browser_ / _This Kibana installation has strict security requirements
enabled that your current browser does not meet._ warning that's displayed for
unsupported user agents, like the one reported by Cypress when running tests.
### Example `kibana.dev.yml`
If you're a developer running tests interactively or on the command line, the
easiset way to specify the credentials used for authentication is to update
`kibana.dev.yml` per the following example:
```yaml
csp.strict: false
elasticsearch:
username: 'elastic'
password: '<password>'
hosts: ['https://<server>:9200']
```
The `username` and `password` from `config/kibana.dev.yml` will be read by the `login` test helper function when tests authenticate with Kibana.
See the `Running Tests Interactively` section for details.
### Content Security Policy (CSP) Settings
Your local or cloud Kibana server must have the `csp.strict: false` setting
configured in `kibana.dev.yml`, or `kibana.yml`, as shown in the example below:
```yaml
csp.strict: false
```
## Running Tests Interactively
To run tests in interactively via the Cypress test runner:
Use the Cypress interactive test runner to develop and debug specific tests
by adding a `.only` to the test you're developing, or click on a specific
spec in the interactive test runner to run just the tests in that spec.
1. Create and configure a `config/kibana.dev.yml`, as described in the `Server and Authentication Requirements` section above.
To run and debug tests in interactively via the Cypress test runner:
2. Start a local instance of the Kibana development server:
1. Disable CSP on the local or remote Kibana instance, as described in the
_Content Security Policy (CSP) Settings_ section above.
```
2. To specify the credentials required for authentication, configure
`config/kibana.dev.yml`, as described in the _Server and Authentication
Requirements_ section above, or specify them via environment variables
as described later in this section.
3. Start a local instance of the Kibana development server (only if testing against a
local host):
```sh
yarn start --no-base-path
```
3. Launch the Cypress interactive test runner:
4. Launch the Cypress interactive test runner via one of the following options:
- To run tests interactively against the default (local) host specified by
`baseUrl`, as configured in `plugins/siem/cypress.json`:
```sh
cd x-pack/legacy/plugins/siem
yarn cypress:open
```
4. Click the `Run all specs` button in the Cypress test runner
- To (optionally) run tests interactively against a different host, pass the
`CYPRESS_baseUrl` environment variable on the command line when launching the
test runner, as shown in the following example:
```sh
cd x-pack/legacy/plugins/siem
CYPRESS_baseUrl=http://localhost:5601 yarn cypress:open
```
- To (optionally) override username and password via environment variables when
running tests interactively:
```sh
cd x-pack/legacy/plugins/siem
CYPRESS_baseUrl=http://localhost:5601 CYPRESS_ELASTICSEARCH_USERNAME=elastic CYPRESS_ELASTICSEARCH_PASSWORD=<password> yarn cypress:open
```
5. Click the `Run all specs` button in the Cypress test runner (after adding
a `.only` to an `it` or `describe` block).
## Running (Headless) Tests on the Command Line
To run (headless) tests on the command line:
1. Disable CSP on the local or remote Kibana instance, as described in the
_Content Security Policy (CSP) Settings_ section above.
2. To specify the credentials required for authentication, configure
`config/kibana.dev.yml`, as described in the _Server and Authentication
Requirements_ section above, or specify them via environment variables
as described later in this section.
3. Start a local instance of the Kibana development server (only if testing against a
local host):
```sh
yarn start --no-base-path
```
4. Launch the Cypress command line test runner via one of the following options:
- To run tests on the command line against the default (local) host specified by
`baseUrl`, as configured in `plugins/siem/cypress.json`:
```sh
cd x-pack/legacy/plugins/siem
yarn cypress:run
```
- To (optionally) run tests on the command line against a different host, pass
`CYPRESS_baseUrl` as an environment variable on the command line, as shown in
the following example:
```sh
cd x-pack/legacy/plugins/siem
CYPRESS_baseUrl=http://localhost:5601 yarn cypress:run
```
- To (optionally) override username and password via environment variables when
running via the command line:
```sh
cd x-pack/legacy/plugins/siem
CYPRESS_baseUrl=http://localhost:5601 CYPRESS_ELASTICSEARCH_USERNAME=elastic CYPRESS_ELASTICSEARCH_PASSWORD=<password> yarn cypress:run
```
## Reporting
When Cypress tests are run on the command line via `yarn cypress:run`,
reporting artifacts are generated under the `target` directory in the root
of the Kibana, as detailed for each artifact type in the sections bleow.
### HTML Reports
An HTML report (e.g. for email notifications) is output to:
```
target/kibana-siem/cypress/results/output.html
```
### Screenshots
Screenshots of failed tests are output to:
```
target/kibana-siem/cypress/screenshots
```
### `junit` Reports
The Kibana CI process reports `junit` test results from the `target/junit` directory.
Cypress `junit` reports are generated in `target/kibana-siem/cypress/results`
and copied to the `target/junit` directory.
### Videos (optional)
Videos are disabled by default, but can optionally be enabled by setting the
`CYPRESS_video=true` environment variable:
```
CYPRESS_video=true yarn cypress:run
```
Videos are (optionally) output to:
```
target/kibana-siem/cypress/videos
```

View file

@ -21,7 +21,7 @@ import {
} from '../../lib/fields_browser/selectors';
import { logout } from '../../lib/logout';
import { HOSTS_PAGE } from '../../lib/urls';
import { loginAndWaitForPage } from '../../lib/util/helpers';
import { loginAndWaitForPage, DEFAULT_TIMEOUT } from '../../lib/util/helpers';
const defaultHeaders = [
{ id: '@timestamp' },
@ -200,9 +200,9 @@ describe('Fields Browser', () => {
cy.get(`[data-test-subj="headers-group"]`).then(headersDropArea => drop(headersDropArea));
clickOutsideFieldsBrowser();
cy.get(`[data-test-subj="header-text-${toggleField}"]`).should('exist');
cy.get(`[data-test-subj="header-text-${toggleField}"]`, { timeout: DEFAULT_TIMEOUT }).should(
'exist'
);
});
it('resets all fields in the timeline when `Reset Fields` is clicked', () => {

View file

@ -9,7 +9,7 @@ import { populateTimeline } from '../../lib/fields_browser/helpers';
import { logout } from '../../lib/logout';
import { toggleFirstEventDetails } from '../../lib/timeline/helpers';
import { HOSTS_PAGE } from '../../lib/urls';
import { loginAndWaitForPage } from '../../lib/util/helpers';
import { loginAndWaitForPage, DEFAULT_TIMEOUT } from '../../lib/util/helpers';
describe('toggle column in timeline', () => {
beforeEach(() => {
@ -72,6 +72,8 @@ describe('toggle column in timeline', () => {
cy.get(`[data-test-subj="headers-group"]`).then(headersDropArea => drop(headersDropArea));
cy.get(`[data-test-subj="header-text-${idField}"]`).should('exist');
cy.get(`[data-test-subj="header-text-${idField}"]`, { timeout: DEFAULT_TIMEOUT }).should(
'exist'
);
});
});

View file

@ -6,20 +6,37 @@
"license": "Elastic-License",
"scripts": {
"build-graphql-types": "node scripts/generate_types_from_graphql.js",
"cypress:open": "cypress open"
"cypress:open": "cypress open",
"cypress:run": "cypress run --spec ./cypress/integration/**/*.spec.ts --reporter mocha-multi-reporters --reporter-options configFile=./reporter_config.json; mochawesome-merge --reportDir ../../../../target/kibana-siem/cypress/results > ../../../../target/kibana-siem/cypress/results/output.json; marge ../../../../target/kibana-siem/cypress/results/output.json --reportDir ../../../../target/kibana-siem/cypress/results; mkdir -p ../../../../target/junit && cp ../../../../target/kibana-siem/cypress/results/*.xml ../../../../target/junit/"
},
"devDependencies": {
"@cypress/webpack-preprocessor": "^4.1.0",
"@types/js-yaml": "^3.12.1",
"@types/lodash": "^4.14.110",
"@types/react-beautiful-dnd": "^10.0.1",
"cypress": "^3.3.1",
"cypress": "^3.4.1",
"js-yaml": "^3.13.1",
"mocha-junit-reporter": "^1.23.1",
"mocha-multi-reporters": "^1.1.7",
"mochawesome": "^4.0.1",
"mochawesome-merge": "^2.0.1",
"mochawesome-report-generator": "^4.0.1",
"ts-loader": "^6.0.4"
},
"dependencies": {
"lodash": "^4.17.13",
"react-beautiful-dnd": "^10.0.1",
"react-markdown": "^4.0.6"
},
"workspaces": {
"packages": [
"packages/*"
],
"nohoist": [
"**/mochawesome",
"**/mochawesome/**",
"**/mocha-multi-reporters",
"**/mocha-multi-reporters/**"
]
}
}
}

View file

@ -0,0 +1,10 @@
{
"reporterEnabled": "mochawesome, mocha-junit-reporter",
"reporterOptions": {
"html": false,
"json": true,
"mochaFile": "../../../../target/kibana-siem/cypress/results/results-[hash].xml",
"overwrite": false,
"reportDir": "../../../../target/kibana-siem/cypress/results"
}
}

View file

@ -65,12 +65,9 @@ export const telemetry = (kibana: any) => {
injectDefaultVars(server: Server) {
const config = server.config();
return {
telemetryEnabled: getXpackConfigWithDeprecated(config, 'telemetry.enabled'),
telemetryUrl: getXpackConfigWithDeprecated(config, 'telemetry.url'),
spacesEnabled: config.get('xpack.spaces.enabled'),
telemetryBanner: config.get('xpack.telemetry.banner'),
telemetryOptedIn: null,
activeSpace: null,
};
},
hacks: ['plugins/telemetry/hacks/telemetry_init', 'plugins/telemetry/hacks/telemetry_opt_in'],

View file

@ -9,6 +9,7 @@ import dedent from 'dedent';
import {
XPACK_INFO_API_DEFAULT_POLL_FREQUENCY_IN_MILLIS
} from '../../server/lib/constants';
import { getXpackConfigWithDeprecated } from '../telemetry/common/get_xpack_config_with_deprecated';
import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status';
import { replaceInjectedVars } from './server/lib/replace_injected_vars';
import { setupXPackMain } from './server/lib/setup_xpack_main';
@ -59,6 +60,15 @@ export const xpackMain = (kibana) => {
'plugins/xpack_main/hacks/check_xpack_info_change',
],
replaceInjectedVars,
injectDefaultVars(server) {
const config = server.config();
return {
telemetryEnabled: getXpackConfigWithDeprecated(config, 'telemetry.enabled'),
activeSpace: null,
spacesEnabled: config.get('xpack.spaces.enabled'),
};
},
__webpackPluginProvider__(webpack) {
return new webpack.BannerPlugin({
banner: dedent`

View file

@ -119,6 +119,7 @@
"cheerio": "0.22.0",
"commander": "3.0.0",
"copy-webpack-plugin": "^5.0.0",
"cypress": "^3.4.1",
"del": "^4.0.0",
"dotenv": "2.0.0",
"enzyme": "^3.10.0",
@ -140,8 +141,14 @@
"jest-cli": "^24.8.0",
"jest-styled-components": "^6.2.2",
"jsdom": "^12.0.0",
"js-yaml": "^3.13.1",
"madge": "3.4.4",
"mocha": "3.5.3",
"mocha-junit-reporter": "^1.23.1",
"mocha-multi-reporters": "^1.1.7",
"mochawesome": "^4.0.1",
"mochawesome-merge": "^2.0.1",
"mochawesome-report-generator": "^4.0.1",
"mustache": "^2.3.0",
"mutation-observer": "^1.0.3",
"node-fetch": "^2.1.2",
@ -165,6 +172,7 @@
"supertest-as-promised": "^4.0.2",
"tmp": "0.1.0",
"tree-kill": "^1.1.0",
"ts-loader": "^6.0.4",
"typescript": "3.5.3",
"vinyl-fs": "^3.0.2",
"xml-crypto": "^0.10.1",
@ -360,5 +368,13 @@
},
"engines": {
"yarn": "^1.10.1"
},
"workspaces": {
"nohoist": [
"**/mochawesome",
"**/mochawesome/**",
"**/mocha-multi-reporters",
"**/mocha-multi-reporters/**"
]
}
}
}

199
yarn.lock
View file

@ -7900,6 +7900,11 @@ chardet@^0.7.0:
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
charenc@~0.0.1:
version "0.0.2"
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=
check-disk-space@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/check-disk-space/-/check-disk-space-2.1.0.tgz#2e77fe62f30d9676dc37a524ea2008f40c780295"
@ -9248,6 +9253,11 @@ cross-spawn@^5.0.1, cross-spawn@^5.1.0:
shebang-command "^1.2.0"
which "^1.2.9"
crypt@~0.0.1:
version "0.0.2"
resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=
cryptiles@2.x.x:
version "2.0.5"
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
@ -9509,10 +9519,10 @@ cyclist@~0.2.2:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=
cypress@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.3.1.tgz#8a127b1d9fa74bff21f111705abfef58d595fdef"
integrity sha512-JIo47ZD9P3jAw7oaK7YKUoODzszJbNw41JmBrlMMiupHOlhmXvZz75htuo7mfRFPC9/1MDQktO4lX/V2+a6lGQ==
cypress@^3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.4.1.tgz#ca2e4e9864679da686c6a6189603efd409664c30"
integrity sha512-1HBS7t9XXzkt6QHbwfirWYty8vzxNMawGj1yI+Fu6C3/VZJ8UtUngMW6layqwYZzLTZV8tiDpdCNBypn78V4Dg==
dependencies:
"@cypress/listr-verbose-renderer" "0.4.1"
"@cypress/xvfb" "1.2.4"
@ -9527,20 +9537,19 @@ cypress@^3.3.1:
execa "0.10.0"
executable "4.1.1"
extract-zip "1.6.7"
fs-extra "4.0.1"
fs-extra "5.0.0"
getos "3.1.1"
glob "7.1.3"
is-ci "1.2.1"
is-installed-globally "0.1.0"
lazy-ass "1.6.0"
listr "0.12.0"
lodash "4.17.11"
lodash "4.17.15"
log-symbols "2.2.0"
minimist "1.2.0"
moment "2.24.0"
ramda "0.24.1"
request "2.88.0"
request-progress "0.4.0"
request-progress "3.0.0"
supports-color "5.5.0"
tmp "0.1.0"
url "0.11.0"
@ -9862,6 +9871,11 @@ dateformat@^2.0.0:
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-2.2.0.tgz#4065e2013cf9fb916ddfd82efb506ad4c6769062"
integrity sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=
dateformat@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
debug-fabulous@1.X:
version "1.1.0"
resolved "https://registry.yarnpkg.com/debug-fabulous/-/debug-fabulous-1.1.0.tgz#af8a08632465224ef4174a9f06308c3c2a1ebc8e"
@ -10439,6 +10453,11 @@ diff@^3.5.0:
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==
diff@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.1.tgz#0c667cb467ebbb5cea7f14f135cc2dba7780a8ff"
integrity sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==
diffie-hellman@^5.0.0:
version "5.0.2"
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e"
@ -13047,13 +13066,13 @@ fs-exists-sync@^0.1.0:
resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add"
integrity sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=
fs-extra@4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.1.tgz#7fc0c6c8957f983f57f306a24e5b9ddd8d0dd880"
integrity sha1-f8DGyJV/mD9X8waiTlud3Y0N2IA=
fs-extra@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-5.0.0.tgz#414d0110cdd06705734d055652c5411260c31abd"
integrity sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==
dependencies:
graceful-fs "^4.1.2"
jsonfile "^3.0.0"
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-extra@^0.30.0:
@ -13162,6 +13181,11 @@ fstream@^1.0.12:
mkdirp ">=0.5 0"
rimraf "2"
fsu@^1.0.2:
version "1.1.1"
resolved "https://registry.yarnpkg.com/fsu/-/fsu-1.1.1.tgz#bd36d3579907c59d85b257a75b836aa9e0c31834"
integrity sha512-xQVsnjJ/5pQtcKh+KjUoZGzVWn4uNkchxTF6Lwjr4Gf7nQr8fmUfhKJ62zE77+xQg9xnxi5KUps7XGs+VC986A==
fullname@^3.2.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/fullname/-/fullname-3.3.0.tgz#a08747d6921229610b8178b7614fce10cb185f5a"
@ -15857,7 +15881,7 @@ is-boolean-object@^1.0.0:
resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.0.tgz#98f8b28030684219a95f375cfbd88ce3405dff93"
integrity sha1-mPiygDBoQhmpXzdc+9iM40Bd/5M=
is-buffer@^1.0.2, is-buffer@^1.1.4, is-buffer@^1.1.5:
is-buffer@^1.0.2, is-buffer@^1.1.4, is-buffer@^1.1.5, is-buffer@~1.1.1:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
@ -18403,6 +18427,11 @@ lodash.isequal@^4.0.0, lodash.isequal@^4.1.1, lodash.isequal@^4.2.0, lodash.iseq
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
lodash.isfunction@^3.0.8, lodash.isfunction@^3.0.9:
version "3.0.9"
resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051"
integrity sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==
lodash.isinteger@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
@ -18413,6 +18442,11 @@ lodash.isnumber@^3.0.3:
resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=
lodash.isobject@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d"
integrity sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0=
lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
@ -18635,7 +18669,7 @@ lodash.uniqby@^4.7.0:
resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302"
integrity sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI=
lodash@4.17.11, lodash@4.17.13, lodash@>4.17.4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.6.1, lodash@~4.17.10, lodash@~4.17.5:
lodash@4.17.13, lodash@4.17.15, lodash@>4.17.4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.6.1, lodash@~4.17.10, lodash@~4.17.5:
version "4.17.13"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.13.tgz#0bdc3a6adc873d2f4e0c4bac285df91b64fc7b93"
integrity sha512-vm3/XWXfWtRua0FkUyEHBZy8kCPjErNBT9fJx8Zvs+U6zjqPbTUOpkaoum3O5uiA8sm+yNMHXfYkTUHFoMxFNA==
@ -18645,6 +18679,11 @@ lodash@^3.10.1, lodash@^3.3.1, lodash@~3.10.1:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=
lodash@^4.16.4:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
lodash@^4.17.12:
version "4.17.14"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba"
@ -19072,6 +19111,15 @@ md5.js@^1.3.4:
hash-base "^3.0.0"
inherits "^2.0.1"
md5@^2.1.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9"
integrity sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=
dependencies:
charenc "~0.0.1"
crypt "~0.0.1"
is-buffer "~1.1.1"
mdast-add-list-metadata@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/mdast-add-list-metadata/-/mdast-add-list-metadata-1.0.1.tgz#95e73640ce2fc1fa2dcb7ec443d09e2bfe7db4cf"
@ -19619,6 +19667,25 @@ mobx@^4.9.2:
resolved "https://registry.yarnpkg.com/mobx/-/mobx-4.9.4.tgz#bb37a0e4e05f0b02be89ced9d23445cad73377ad"
integrity sha512-RaEpydw7D1ebp1pdFHrEMZcLk4nALAZyHAroCPQpqLzuIXIxJpLmMIe5PUZwYHqvlcWL6DVqDYCANZpPOi9iXA==
mocha-junit-reporter@^1.23.1:
version "1.23.1"
resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-1.23.1.tgz#ba11519c0b967f404e4123dd69bc4ba022ab0f12"
integrity sha512-qeDvKlZyAH2YJE1vhryvjUQ06t2hcnwwu4k5Ddwn0GQINhgEYFhlGM0DwYCVUHq5cuo32qAW6HDsTHt7zz99Ng==
dependencies:
debug "^2.2.0"
md5 "^2.1.0"
mkdirp "~0.5.1"
strip-ansi "^4.0.0"
xml "^1.0.0"
mocha-multi-reporters@^1.1.7:
version "1.1.7"
resolved "https://registry.yarnpkg.com/mocha-multi-reporters/-/mocha-multi-reporters-1.1.7.tgz#cc7f3f4d32f478520941d852abb64d9988587d82"
integrity sha1-zH8/TTL0eFIJQdhSq7ZNmYhYfYI=
dependencies:
debug "^3.1.0"
lodash "^4.16.4"
mocha@3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.5.3.tgz#1e0480fe36d2da5858d1eb6acc38418b26eaa20d"
@ -19653,6 +19720,51 @@ mocha@^2.0.1, mocha@^2.3.4:
supports-color "1.2.0"
to-iso-string "0.0.2"
mochawesome-merge@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/mochawesome-merge/-/mochawesome-merge-2.0.1.tgz#c690433acc78fd769effe4db1a107508351e2dc5"
integrity sha512-QRYok/9y9MJ4zlWGajC/OV6BxjUGyv1AYX3DBOPSbpzk09p2dFBWV1QYSN/dHu7bo/q44ZGmOBHO8ZnAyI+Yug==
dependencies:
fs-extra "^7.0.1"
minimatch "^3.0.4"
uuid "^3.3.2"
yargs "^12.0.5"
mochawesome-report-generator@^4.0.0, mochawesome-report-generator@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/mochawesome-report-generator/-/mochawesome-report-generator-4.0.1.tgz#0a010d1ecf379eb26ba05300feb59e2665076080"
integrity sha512-hQbmQt8/yCT68GjrQFat+Diqeuka3haNllexYfja1+y0hpwi3yCJwFpQCdWK9ezzcXL3Nu80f2I6SZeyspwsqg==
dependencies:
chalk "^2.4.2"
dateformat "^3.0.2"
fs-extra "^7.0.0"
fsu "^1.0.2"
lodash.isfunction "^3.0.8"
opener "^1.4.2"
prop-types "^15.7.2"
react "^16.8.5"
react-dom "^16.8.5"
tcomb "^3.2.17"
tcomb-validation "^3.3.0"
validator "^10.11.0"
yargs "^13.2.2"
mochawesome@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/mochawesome/-/mochawesome-4.0.1.tgz#351af69c8904468e75a71f8704ed0b5767795ccc"
integrity sha512-F/hVmiwWCvwBiW/UPhs4/lfgf8mBJBr89W/9fDu+hb+rQ9gFxWh9N/BU7RtEH+dMfBF4o8XIdYHrEcwxJhzqsw==
dependencies:
chalk "^2.4.1"
diff "^4.0.1"
json-stringify-safe "^5.0.1"
lodash.isempty "^4.4.0"
lodash.isfunction "^3.0.9"
lodash.isobject "^3.0.2"
lodash.isstring "^4.0.1"
mochawesome-report-generator "^4.0.0"
strip-ansi "^5.0.0"
uuid "^3.3.2"
module-definition@^3.0.0, module-definition@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/module-definition/-/module-definition-3.2.0.tgz#a1741d5ddf60d76c60d5b1f41ba8744ba08d3ef4"
@ -20046,11 +20158,6 @@ node-ensure@^0.0.0:
resolved "https://registry.yarnpkg.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7"
integrity sha1-7K52QVDemYYexcgQ/V0Jaxg5Mqc=
node-eta@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/node-eta/-/node-eta-0.1.1.tgz#4066109b39371c761c72b7ebda9a9ea0a5de121f"
integrity sha1-QGYQmzk3HHYccrfr2pqeoKXeEh8=
node-fetch@1.7.3, node-fetch@^1.0.1:
version "1.7.3"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
@ -20720,6 +20827,11 @@ onetime@^2.0.0:
dependencies:
mimic-fn "^1.0.0"
opener@^1.4.2:
version "1.5.1"
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.1.tgz#6d2f0e77f1a0af0032aca716c2c1fbb8e7e8abed"
integrity sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==
opentracing@^0.13.0:
version "0.13.0"
resolved "https://registry.yarnpkg.com/opentracing/-/opentracing-0.13.0.tgz#6a341442f09d7d866bc11ed03de1e3828e3d6aab"
@ -22910,7 +23022,7 @@ react-dom@^16.8.1:
prop-types "^15.6.2"
scheduler "^0.13.5"
react-dom@^16.8.3:
react-dom@^16.8.3, react-dom@^16.8.5:
version "16.8.6"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f"
integrity sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA==
@ -23526,7 +23638,7 @@ react@^16.8.1:
prop-types "^15.6.2"
scheduler "^0.13.5"
react@^16.8.3:
react@^16.8.3, react@^16.8.5:
version "16.8.6"
resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe"
integrity sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==
@ -24241,13 +24353,12 @@ replace-ext@1.0.0, replace-ext@^1.0.0:
resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=
request-progress@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-0.4.0.tgz#c1954e39086aa85269c5660bcee0142a6a70d7e7"
integrity sha1-wZVOOQhqqFJpxWYLzuAUKmpw1+c=
request-progress@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe"
integrity sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=
dependencies:
node-eta "^0.1.1"
throttleit "^0.0.2"
throttleit "^1.0.0"
request-promise-core@1.1.1:
version "1.1.1"
@ -26831,6 +26942,18 @@ tar@^4:
safe-buffer "^5.1.2"
yallist "^3.0.2"
tcomb-validation@^3.3.0:
version "3.4.1"
resolved "https://registry.yarnpkg.com/tcomb-validation/-/tcomb-validation-3.4.1.tgz#a7696ec176ce56a081d9e019f8b732a5a8894b65"
integrity sha512-urVVMQOma4RXwiVCa2nM2eqrAomHROHvWPuj6UkDGz/eb5kcy0x6P0dVt6kzpUZtYMNoAqJLWmz1BPtxrtjtrA==
dependencies:
tcomb "^3.0.0"
tcomb@^3.0.0, tcomb@^3.2.17:
version "3.2.29"
resolved "https://registry.yarnpkg.com/tcomb/-/tcomb-3.2.29.tgz#32404fe9456d90c2cf4798682d37439f1ccc386c"
integrity sha512-di2Hd1DB2Zfw6StGv861JoAF5h/uQVu/QJp2g8KVbtfKnoHdBQl5M32YWq6mnSYBQ1vFFrns5B1haWJL7rKaOQ==
tcp-port-used@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/tcp-port-used/-/tcp-port-used-1.0.1.tgz#46061078e2d38c73979a2c2c12b5a674e6689d70"
@ -26984,10 +27107,10 @@ throat@^4.0.0:
resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"
integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=
throttleit@^0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-0.0.2.tgz#cfedf88e60c00dd9697b61fdd2a8343a9b680eaf"
integrity sha1-z+34jmDADdlpe2H90qg0OptoDq8=
throttleit@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
integrity sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=
through2-filter@^2.0.0:
version "2.0.0"
@ -28653,6 +28776,11 @@ validate-npm-package-name@2.2.2:
dependencies:
builtins "0.0.7"
validator@^10.11.0:
version "10.11.0"
resolved "https://registry.yarnpkg.com/validator/-/validator-10.11.0.tgz#003108ea6e9a9874d31ccc9e5006856ccd76b228"
integrity sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==
validator@^8.0.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/validator/-/validator-8.2.0.tgz#3c1237290e37092355344fef78c231249dab77b9"
@ -29900,6 +30028,11 @@ xml2js@^0.4.19, xml2js@^0.4.5:
sax ">=0.6.0"
xmlbuilder "~9.0.1"
xml@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5"
integrity sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=
xmlbuilder@8.2.2:
version "8.2.2"
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-8.2.2.tgz#69248673410b4ba42e1a6136551d2922335aa773"
@ -30108,7 +30241,7 @@ yargs@^12.0.2:
y18n "^3.2.1 || ^4.0.0"
yargs-parser "^10.1.0"
yargs@^13.3.0:
yargs@^13.2.2, yargs@^13.3.0:
version "13.3.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83"
integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==