mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Merge branch 'master' of github.com:elastic/kibana into fix/watcher-test-missing-await
This commit is contained in:
commit
9226eb8aa1
64 changed files with 1785 additions and 632 deletions
|
@ -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].
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IKibanaSocket](./kibana-plugin-server.ikibanasocket.md) > [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;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IKibanaSocket](./kibana-plugin-server.ikibanasocket.md) > [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;
|
||||
```
|
|
@ -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 |
|
||||
|
|
|
@ -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 server’s 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.
|
||||
|
|
|
@ -45,6 +45,7 @@ Object {
|
|||
"!SRP",
|
||||
"!CAMELLIA",
|
||||
],
|
||||
"clientAuthentication": "none",
|
||||
"enabled": false,
|
||||
"supportedProtocols": Array [
|
||||
"TLSv1.1",
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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';
|
|
@ -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' },
|
||||
];
|
184
x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts
Normal file
184
x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts
Normal 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' },
|
||||
];
|
|
@ -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]));
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
376
x-pack/legacy/plugins/canvas/common/lib/autocomplete.ts
Normal file
376
x-pack/legacy/plugins/canvas/common/lib/autocomplete.ts
Normal 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]));
|
||||
};
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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({});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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 };
|
||||
}
|
|
@ -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';
|
|
@ -45,7 +45,7 @@ export const MultiMetricSettings: FC<Props> = ({ isActive, setIsValid }) => {
|
|||
</EuiFlexGroup>
|
||||
<EuiFlexGroup gutterSize="xl">
|
||||
<EuiFlexItem>
|
||||
<BucketSpan />
|
||||
<BucketSpan setIsValid={setIsValid} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem />
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -36,7 +36,7 @@ export const PopulationSettings: FC<Props> = ({ isActive, setIsValid }) => {
|
|||
<Fragment>
|
||||
<EuiFlexGroup gutterSize="xl">
|
||||
<EuiFlexItem>
|
||||
<BucketSpan />
|
||||
<BucketSpan setIsValid={setIsValid} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<Influencers />
|
||||
|
|
|
@ -51,7 +51,7 @@ export const SingleMetricSettings: FC<Props> = ({ isActive, setIsValid }) => {
|
|||
<Fragment>
|
||||
<EuiFlexGroup gutterSize="xl">
|
||||
<EuiFlexItem>
|
||||
<BucketSpan />
|
||||
<BucketSpan setIsValid={setIsValid} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup>
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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]} />;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) };
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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';
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
10
x-pack/legacy/plugins/siem/reporter_config.json
Normal file
10
x-pack/legacy/plugins/siem/reporter_config.json
Normal 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"
|
||||
}
|
||||
}
|
|
@ -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'],
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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
199
yarn.lock
|
@ -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==
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue