This commit is contained in:
Mike Cote 2019-04-25 14:32:41 -04:00
commit 38b402b623
261 changed files with 5628 additions and 5877 deletions

View file

@ -20,7 +20,6 @@ bower_components
/packages/*/target
/packages/eslint-config-kibana
/packages/kbn-es-query/src/kuery/ast/kuery.js
/packages/kbn-es-query/src/kuery/ast/legacy_kuery.js
/packages/kbn-pm/dist
/packages/kbn-plugin-generator/sao_template/template
/packages/kbn-ui-framework/dist

5
.github/CODEOWNERS vendored
View file

@ -11,6 +11,11 @@
# Canvas
/x-pack/plugins/canvas/ @elastic/kibana-canvas
# Code
/x-pack/plugins/code/ @teams/code
/x-pack/test/functional/apps/code/ @teams/code
/x-pack/test/api_integration/apis/code/ @teams/code
# Machine Learning
/x-pack/plugins/ml/ @elastic/ml-ui

View file

@ -20,6 +20,7 @@
"timelion": "src/legacy/core_plugins/timelion",
"tagCloud": "src/legacy/core_plugins/tagcloud",
"tsvb": "src/legacy/core_plugins/metrics",
"kbnESQuery": "packages/kbn-es-query",
"xpack.apm": "x-pack/plugins/apm",
"xpack.beatsManagement": "x-pack/plugins/beats_management",
"xpack.crossClusterReplication": "x-pack/plugins/cross_cluster_replication",

View file

@ -6,6 +6,12 @@
--
The *Dev Tools* page contains development tools that you can use to interact
with your data in Kibana.
* <<console-kibana, Console>>
* <<xpack-profiler, Search Profiler>>
* <<xpack-grokdebugger, Grok Debugger>>
--
include::dev-tools/console/console.asciidoc[]

View file

@ -1,19 +1,20 @@
[[auto-formatting]]
=== Auto Formatting
=== Auto formatting
Console allows you to auto format messy requests. To do so, position the cursor on the request you would like to format
and select Auto Indent from the action menu:
Console can help you format requests. Select one or more requests that you
want to format, click the action icon (image:dev-tools/console/images/wrench.png[]),
and select *Auto indent*.
.Auto Indent a request
image::images/auto_format_before.png["Auto format before",width=500,align="center"]
For example, you might have a request that is formatted like this:
Console will adjust the JSON body of the request and it will now look like this:
[role="screenshot"]
image::dev-tools/console/images/copy-curl.png["Console close-up"]
.A formatted request
image::images/auto_format_after.png["Auto format after",width=500,align="center"]
Console adjusts the JSON body of the request to apply the indents.
If you select Auto Indent on a request that is already perfectly formatted, Console will collapse the
request body to a single line per document. This is very handy when working with Elasticsearch's bulk APIs:
[role="screenshot"]
image::dev-tools/console/images/request.png["Console close-up"]
.One doc per line
image::images/auto_format_bulk.png["Auto format bulk",width=550,align="center"]
If you select *Auto indent* on a request that is already well formatted,
Console collapses the request body to a single line per document.
This is helpful when working with {es}'s {ref}/docs-bulk.html[bulk APIs].

View file

@ -1,6 +1,26 @@
[[configuring-console]]
=== Configuring Console
You can add the following options in the `config/kibana.yml` file:
You can configure Console to your preferences.
[float]
==== Configuring settings
*Settings* allows you to modify the font size and set the fileds for
autocomplete.
[role="screenshot"]
image::dev-tools/console/images/console-settings.png["Console settings"]
[float]
[[console-settings]]
==== Disabling Console
If you dont want to use Console, you can disable it by setting `console.enabled`
to false in your `kibana.yml` configuration file. Changing this setting
causes the server to regenerate assets on the next startup,
which might cause a delay before pages start being served.
`console.enabled`:: *Default: true* Set to false to disable Console. Toggling this will cause the server to regenerate assets on the next startup, which may cause a delay before pages start being served.

View file

@ -1,15 +1,25 @@
[[console-kibana]]
== Console
The Console plugin provides a UI to interact with the REST API of Elasticsearch. Console has two main areas: the *editor*,
where you compose requests to Elasticsearch, and the *response* pane, which displays the responses to the request.
Console enables you to interact with the REST API of {es}. *Note:* You cannot
interact with {kib} API endpoints via Console.
NOTE: You cannot interact with Kibana API endpoints via the Console.
Go to *Dev Tools > Console* to get started.
.The Console UI
image::dev-tools/console/images/introduction_screen.png[Screenshot]
Console has two main areas:
Console understands commands in a cURL-like syntax. For example the following Console command
* The *editor*, where you compose requests to send to {es}.
* The *response* pane, which displays the responses to the request.
[role="screenshot"]
image::dev-tools/console/images/console.png["Console"]
[float]
[[console-api]]
=== Writing requests
Console understands commands in a cURL-like syntax.
For example, the following is a `GET` request to the {es} `_search` API.
[source,js]
----------------------------------
@ -21,7 +31,7 @@ GET /_search
}
----------------------------------
is a simple `GET` request to Elasticsearch's `_search API`. Here is the equivalent command in cURL.
Here is the equivalent command in cURL:
[source,bash]
----------------------------------
@ -33,38 +43,55 @@ curl -XGET "http://localhost:9200/_search" -d'
}'
----------------------------------
In fact, you can paste the above command into Console and it will automatically be converted into the Console syntax.
If you paste the above command into Console, {kib} automatically converts it
to Console syntax. Alternatively, if you want to want to see Console syntax in cURL,
click the action icon (image:dev-tools/console/images/wrench.png[]) and select *Copy as cURL*.
When typing a command, Console will make context sensitive <<suggestions,suggestions>>. These suggestions can help
you explore parameters for each API, or to just speed up typing. Console will suggest APIs, indexes and field
names.
For help with formatting requests, you can use Console's <<auto-formatting, auto formatting>>
feature.
[[suggestions]]
.API suggestions
image::dev-tools/console/images/introduction_suggestion.png["Suggestions",width=400,align="center"]
Once you have typed a command in to the left pane, you can submit it to Elasticsearch by clicking the little green
triangle that appears next to the URL line of the request. Notice that as you move the cursor around, the little
triangle and wrench icons follow you around. We call this the <<action_menu,Action Menu>>. You can also select
multiple requests and submit them all at once.
[float]
[[console-request]]
=== Submitting requests
[[action_menu]]
.The Action Menu
image::dev-tools/console/images/introduction_action_menu.png["The Action Menu",width=400,align="center"]
Once you enter a command in the editor, click the
green triangle to submit the request to {es}.
When the response come back, you should see it in the right hand panel:
You can select multiple requests and submit them together.
Console sends the requests to {es} one by one and shows the output
in the response pane. Submitting multiple request is helpful when you're debugging an issue or trying query
combinations in multiple scenarios.
[float]
[[console-autocomplete]]
=== Using autocomplete
When typing a command, Console makes context-sensitive suggestions.
These suggestions can help you explore parameters for each API and speed up typing.
To configure your preferences for autocomplete, go to
<<configuring-console, Settings>>.
[float]
[[console-view-api]]
=== Viewing API docs
You can view the documentation for an API endpoint by clicking
the action icon (image:dev-tools/console/images/wrench.png[]) and selecting
*Open documentation*.
[float]
[[console-history]]
=== Getting your request history
Console maintains a list of the last 500 requests that {es} successfully executed.
To view your most recent requests, click *History*. If you select a request
and click *Apply*, {kib} adds it to the editor at the current cursor position.
.The Output Pane
image::dev-tools/console/images/introduction_output.png[Screenshot]
include::multi-requests.asciidoc[]
include::auto-formatting.asciidoc[]
include::keyboard-shortcuts.asciidoc[]
include::history.asciidoc[]
include::settings.asciidoc[]
include::configuring-console.asciidoc[]

View file

@ -1,10 +0,0 @@
[[history]]
=== History
Console maintains a list of the last 500 requests that were successfully executed by Elasticsearch. The history
is available by clicking the clock icon on the top right side of the window. The icons opens the history panel
where you can see the old requests. You can also select a request here and it will be added to the editor at
the current cursor position.
.History Panel
image::images/history.png["History Panel"]

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 B

View file

@ -1,21 +1,22 @@
[[keyboard-shortcuts]]
=== Keyboard shortcuts
Console comes with a set of nifty keyboard shortcuts making working with it even more efficient. Here is an overview:
The keyboard shortcuts below can help you move quickly through Console. You can
also view these shortcuts by clicking *Help* in Console.
[float]
==== General editing
Ctrl/Cmd + I:: Auto indent current request.
Ctrl + Space:: Open Auto complete (even if not typing).
Ctrl + Space:: Open Autocomplete (even if not typing).
Ctrl/Cmd + Enter:: Submit request.
Ctrl/Cmd + Up/Down:: Jump to the previous/next request start or end.
Ctrl/Cmd + Alt + L:: Collapse/expand current scope.
Ctrl/Cmd + Option + 0:: Collapse all scopes but the current one. Expand by adding a shift.
[float]
==== When auto-complete is visible
==== When autocomplete is visible
Down arrow:: Switch focus to auto-complete menu. Use arrows to further select a term.
Enter/Tab:: Select the currently selected or the top most term in auto-complete menu.
Esc:: Close auto-complete menu.
Down arrow:: Switch focus to autocomplete menu. Use arrows to further select a term.
Enter/Tab:: Select the currently selected or the top most term in autocomplete menu.
Esc:: Close autocomplete menu.

View file

@ -1,14 +0,0 @@
[[multi-requests]]
=== Multiple Requests Support
The Console editor allows writing multiple requests below each other. As shown in the <<console-kibana>> section, you
can submit a request to Elasticsearch by positioning the cursor and using the <<action_menu,Action Menu>>. Similarly
you can select multiple requests in one go:
.Selecting Multiple Requests
image::images/multiple_requests.png[Multiple Requests]
Console will send the request one by one to Elasticsearch and show the output on the right pane as Elasticsearch responds.
This is very handy when debugging an issue or trying query combinations in multiple scenarios.
Selecting multiple requests also allows you to auto format and copy them as cURL in one go.

View file

@ -1,8 +0,0 @@
[[console-settings]]
=== Settings
Console has multiple settings you can set. All of them are available in the Settings panel. To open the panel
click on the cog icon on the top right.
.Settings Panel
image::images/settings.png["Setting Panel"]

View file

@ -341,7 +341,7 @@
"chance": "1.0.10",
"cheerio": "0.22.0",
"chokidar": "1.6.0",
"chromedriver": "73.0.0",
"chromedriver": "^74.0.0",
"classnames": "2.2.5",
"dedent": "^0.7.0",
"delete-empty": "^2.0.0",

View file

@ -76,8 +76,6 @@ Creates a filter (`RangeFilter`) where the value for the given field is in the g
This folder contains the code corresponding to generating Elasticsearch queries using the Kibana query language.
It also contains code corresponding to the original implementation of Kuery (released in 6.0) which should be removed at some point (see legacy_kuery.js, legacy_kuery.peg).
In general, you will only need to worry about the following functions from the `ast` folder:
```javascript

View file

@ -12,7 +12,8 @@
},
"dependencies": {
"lodash": "npm:@elastic/lodash@3.10.1-kibana1",
"moment-timezone": "^0.5.14"
"moment-timezone": "^0.5.14",
"@kbn/i18n": "1.0.0"
},
"devDependencies": {
"@babel/cli": "^7.2.3",

View file

@ -52,15 +52,6 @@ describe('build query', function () {
expect(result.filter).to.eql(expectedESQueries);
});
it('should throw a useful error if it looks like query is using an old, unsupported syntax', function () {
const oldQuery = { query: 'is(foo, bar)', language: 'kuery' };
expect(buildQueryFromKuery).withArgs(indexPattern, [oldQuery], true).to.throwError(
/OutdatedKuerySyntaxError/
);
});
it('should accept a specific date format for a kuery query into an ES query in the bool\'s filter clause', function () {
const queries = [{ query: '@timestamp:"2018-04-03T19:04:17"', language: 'kuery' }];

View file

@ -17,20 +17,15 @@
* under the License.
*/
import { fromLegacyKueryExpression, fromKueryExpression, toElasticsearchQuery, nodeTypes } from '../kuery';
import {
fromKueryExpression,
toElasticsearchQuery,
nodeTypes,
} from '../kuery';
export function buildQueryFromKuery(indexPattern, queries = [], allowLeadingWildcards, dateFormatTZ = null) {
const queryASTs = queries.map(query => {
try {
return fromKueryExpression(query.query, { allowLeadingWildcards });
} catch (parseError) {
try {
fromLegacyKueryExpression(query.query);
} catch (legacyParseError) {
throw parseError;
}
throw Error('OutdatedKuerySyntaxError');
}
return fromKueryExpression(query.query, { allowLeadingWildcards });
});
return buildQuery(indexPattern, queryASTs, { dateFormatTZ });
}

View file

@ -22,13 +22,6 @@ import expect from '@kbn/expect';
import { nodeTypes } from '../../node_types/index';
import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json';
// Helpful utility allowing us to test the PEG parser by simply checking for deep equality between
// the nodes the parser generates and the nodes our constructor functions generate.
function fromLegacyKueryExpressionNoMeta(text) {
return ast.fromLegacyKueryExpression(text, { includeMetadata: false });
}
let indexPattern;
describe('kuery AST API', function () {
@ -38,153 +31,6 @@ describe('kuery AST API', function () {
indexPattern = indexPatternResponse;
});
describe('fromLegacyKueryExpression', function () {
it('should return location and text metadata for each AST node', function () {
const notNode = ast.fromLegacyKueryExpression('!foo:bar');
expect(notNode).to.have.property('text', '!foo:bar');
expect(notNode.location).to.eql({ min: 0, max: 8 });
const isNode = notNode.arguments[0];
expect(isNode).to.have.property('text', 'foo:bar');
expect(isNode.location).to.eql({ min: 1, max: 8 });
const { arguments: [ argNode1, argNode2 ] } = isNode;
expect(argNode1).to.have.property('text', 'foo');
expect(argNode1.location).to.eql({ min: 1, max: 4 });
expect(argNode2).to.have.property('text', 'bar');
expect(argNode2.location).to.eql({ min: 5, max: 8 });
});
it('should return a match all "is" function for whitespace', function () {
const expected = nodeTypes.function.buildNode('is', '*', '*');
const actual = fromLegacyKueryExpressionNoMeta(' ');
expect(actual).to.eql(expected);
});
it('should return an "and" function for single literals', function () {
const expected = nodeTypes.function.buildNode('and', [nodeTypes.literal.buildNode('foo')]);
const actual = fromLegacyKueryExpressionNoMeta('foo');
expect(actual).to.eql(expected);
});
it('should ignore extraneous whitespace at the beginning and end of the query', function () {
const expected = nodeTypes.function.buildNode('and', [nodeTypes.literal.buildNode('foo')]);
const actual = fromLegacyKueryExpressionNoMeta(' foo ');
expect(actual).to.eql(expected);
});
it('literals and queries separated by whitespace should be joined by an implicit "and"', function () {
const expected = nodeTypes.function.buildNode('and', [
nodeTypes.literal.buildNode('foo'),
nodeTypes.literal.buildNode('bar'),
]);
const actual = fromLegacyKueryExpressionNoMeta('foo bar');
expect(actual).to.eql(expected);
});
it('should also support explicit "and"s as a binary operator', function () {
const expected = nodeTypes.function.buildNode('and', [
nodeTypes.literal.buildNode('foo'),
nodeTypes.literal.buildNode('bar'),
]);
const actual = fromLegacyKueryExpressionNoMeta('foo and bar');
expect(actual).to.eql(expected);
});
it('should also support "and" as a function', function () {
const expected = nodeTypes.function.buildNode('and', [
nodeTypes.literal.buildNode('foo'),
nodeTypes.literal.buildNode('bar'),
], 'function');
const actual = fromLegacyKueryExpressionNoMeta('and(foo, bar)');
expect(actual).to.eql(expected);
});
it('should support "or" as a binary operator', function () {
const expected = nodeTypes.function.buildNode('or', [
nodeTypes.literal.buildNode('foo'),
nodeTypes.literal.buildNode('bar'),
]);
const actual = fromLegacyKueryExpressionNoMeta('foo or bar');
expect(actual).to.eql(expected);
});
it('should support "or" as a function', function () {
const expected = nodeTypes.function.buildNode('or', [
nodeTypes.literal.buildNode('foo'),
nodeTypes.literal.buildNode('bar'),
]);
const actual = fromLegacyKueryExpressionNoMeta('or(foo, bar)');
expect(actual).to.eql(expected);
});
it('should support negation of queries with a "!" prefix', function () {
const expected = nodeTypes.function.buildNode('not',
nodeTypes.function.buildNode('or', [
nodeTypes.literal.buildNode('foo'),
nodeTypes.literal.buildNode('bar'),
]));
const actual = fromLegacyKueryExpressionNoMeta('!or(foo, bar)');
expect(actual).to.eql(expected);
});
it('"and" should have a higher precedence than "or"', function () {
const expected = nodeTypes.function.buildNode('or', [
nodeTypes.literal.buildNode('foo'),
nodeTypes.function.buildNode('or', [
nodeTypes.function.buildNode('and', [
nodeTypes.literal.buildNode('bar'),
nodeTypes.literal.buildNode('baz'),
]),
nodeTypes.literal.buildNode('qux'),
])
]);
const actual = fromLegacyKueryExpressionNoMeta('foo or bar and baz or qux');
expect(actual).to.eql(expected);
});
it('should support grouping to override default precedence', function () {
const expected = nodeTypes.function.buildNode('and', [
nodeTypes.function.buildNode('or', [
nodeTypes.literal.buildNode('foo'),
nodeTypes.literal.buildNode('bar'),
]),
nodeTypes.literal.buildNode('baz'),
]);
const actual = fromLegacyKueryExpressionNoMeta('(foo or bar) and baz');
expect(actual).to.eql(expected);
});
it('should support a shorthand operator syntax for "is" functions', function () {
const expected = nodeTypes.function.buildNode('is', 'foo', 'bar', true);
const actual = fromLegacyKueryExpressionNoMeta('foo:bar');
expect(actual).to.eql(expected);
});
it('should support a shorthand operator syntax for inclusive "range" functions', function () {
const argumentNodes = [
nodeTypes.literal.buildNode('bytes'),
nodeTypes.literal.buildNode(1000),
nodeTypes.literal.buildNode(8000),
];
const expected = nodeTypes.function.buildNodeWithArgumentNodes('range', argumentNodes);
const actual = fromLegacyKueryExpressionNoMeta('bytes:[1000 to 8000]');
expect(actual).to.eql(expected);
});
it('should support functions with named arguments', function () {
const expected = nodeTypes.function.buildNode('range', 'bytes', { gt: 1000, lt: 8000 });
const actual = fromLegacyKueryExpressionNoMeta('range(bytes, gt=1000, lt=8000)');
expect(actual).to.eql(expected);
});
it('should throw an error for unknown functions', function () {
expect(ast.fromLegacyKueryExpression).withArgs('foo(bar)').to.throwException(/Unknown function "foo"/);
});
});
describe('fromKueryExpression', function () {
it('should return a match all "is" function for whitespace', function () {

View file

@ -20,7 +20,7 @@
import _ from 'lodash';
import { nodeTypes } from '../node_types/index';
import { parse as parseKuery } from './kuery';
import { parse as parseLegacyKuery } from './legacy_kuery';
import { KQLSyntaxError } from '../errors';
export function fromLiteralExpression(expression, parseOptions) {
parseOptions = {
@ -31,12 +31,16 @@ export function fromLiteralExpression(expression, parseOptions) {
return fromExpression(expression, parseOptions, parseKuery);
}
export function fromLegacyKueryExpression(expression, parseOptions) {
return fromExpression(expression, parseOptions, parseLegacyKuery);
}
export function fromKueryExpression(expression, parseOptions) {
return fromExpression(expression, parseOptions, parseKuery);
try {
return fromExpression(expression, parseOptions, parseKuery);
} catch (error) {
if (error.name === 'SyntaxError') {
throw new KQLSyntaxError(error, expression);
} else {
throw error;
}
}
}
function fromExpression(expression, parseOptions = {}, parse = parseKuery) {
@ -46,11 +50,12 @@ function fromExpression(expression, parseOptions = {}, parse = parseKuery) {
parseOptions = {
...parseOptions,
helpers: { nodeTypes }
helpers: { nodeTypes },
};
return parse(expression, parseOptions);
}
/**
* @params {String} indexPattern
* @params {Object} config - contains the dateFormatTZ

File diff suppressed because it is too large Load diff

View file

@ -66,8 +66,11 @@ Expression
/ FieldValueExpression
/ ValueExpression
Field "fieldName"
= Literal
FieldRangeExpression
= field:Literal Space* operator:RangeOperator Space* value:Literal {
= field:Field Space* operator:RangeOperator Space* value:Literal {
if (value.type === 'cursor') {
return {
...value,
@ -79,7 +82,7 @@ FieldRangeExpression
}
FieldValueExpression
= field:Literal Space* ':' Space* partial:ListOfValues {
= field:Field Space* ':' Space* partial:ListOfValues {
if (partial.type === 'cursor') {
return {
...partial,
@ -154,7 +157,7 @@ NotListOfValues
}
/ ListOfValues
Value
Value "value"
= value:QuotedString {
if (value.type === 'cursor') return value;
const isPhrase = buildLiteralNode(true);
@ -171,19 +174,19 @@ Value
return (field) => buildFunctionNode('is', [field, value, isPhrase]);
}
Or
Or "OR"
= Space+ 'or'i Space+
/ &{ return errorOnLuceneSyntax; } LuceneOr
And
And "AND"
= Space+ 'and'i Space+
/ &{ return errorOnLuceneSyntax; } LuceneAnd
Not
Not "NOT"
= 'not'i Space+
/ &{ return errorOnLuceneSyntax; } LuceneNot
Literal
Literal "literal"
= QuotedString / UnquotedLiteral
QuotedString
@ -277,7 +280,7 @@ RangeOperator
/ '<' { return 'lt'; }
/ '>' { return 'gt'; }
Space
Space "whitespace"
= [\ \t\r\n]
Cursor

File diff suppressed because it is too large Load diff

View file

@ -1,153 +0,0 @@
/*
* Kuery parser
*
* To generate the parsing module (legacy_kuery.js), run `grunt peg`
* To watch changes and generate on file change, run `grunt watch:peg`
*/
/*
* Initialization block
*/
{
var nodeTypes = options.helpers.nodeTypes;
if (options.includeMetadata === undefined) {
options.includeMetadata = true;
}
function addMeta(source, text, location) {
if (options.includeMetadata) {
return Object.assign(
{},
source,
{
text: text,
location: simpleLocation(location),
}
);
}
return source;
}
function simpleLocation(location) {
// Returns an object representing the position of the function within the expression,
// demarcated by the position of its first character and last character. We calculate these values
// using the offset because the expression could span multiple lines, and we don't want to deal
// with column and line values.
return {
min: location.start.offset,
max: location.end.offset
}
}
}
start
= space? query:OrQuery space? {
if (query.type === 'literal') {
return addMeta(nodeTypes.function.buildNode('and', [query]), text(), location());
}
return query;
}
/ whitespace:[\ \t\r\n]* {
return addMeta(nodeTypes.function.buildNode('is', '*', '*', false), text(), location());
}
OrQuery
= left:AndQuery space 'or'i space right:OrQuery {
return addMeta(nodeTypes.function.buildNode('or', [left, right]), text(), location());
}
/ AndQuery
AndQuery
= left:NegatedClause space 'and'i space right:AndQuery {
return addMeta(nodeTypes.function.buildNode('and', [left, right]), text(), location());
}
/ left:NegatedClause space !'or'i right:AndQuery {
return addMeta(nodeTypes.function.buildNode('and', [left, right]), text(), location());
}
/ NegatedClause
NegatedClause
= [!] clause:Clause {
return addMeta(nodeTypes.function.buildNode('not', clause), text(), location());
}
/ Clause
Clause
= '(' subQuery:start ')' {
return subQuery;
}
/ Term
Term
= field:literal_arg_type ':' value:literal_arg_type {
return addMeta(nodeTypes.function.buildNodeWithArgumentNodes('is', [field, value, nodeTypes.literal.buildNode(true)]), text(), location());
}
/ field:literal_arg_type ':[' space? gt:literal_arg_type space 'to'i space lt:literal_arg_type space? ']' {
return addMeta(nodeTypes.function.buildNodeWithArgumentNodes('range', [field, gt, lt]), text(), location());
}
/ function
/ !Keywords literal:literal_arg_type { return literal; }
function_name
= first:[a-zA-Z]+ rest:[.a-zA-Z0-9_-]* { return first.join('') + rest.join('') }
function "function"
= name:function_name space? '(' space? arg_list:arg_list? space? ')' {
return addMeta(nodeTypes.function.buildNodeWithArgumentNodes(name, arg_list || []), text(), location());
}
arg_list
= first:argument rest:(space? ',' space? arg:argument {return arg})* space? ','? {
return [first].concat(rest);
}
argument
= name:function_name space? '=' space? value:arg_type {
return addMeta(nodeTypes.namedArg.buildNode(name, value), text(), location());
}
/ element:arg_type {return element}
arg_type
= OrQuery
/ literal_arg_type
literal_arg_type
= literal:literal {
var result = addMeta(nodeTypes.literal.buildNode(literal), text(), location());
return result;
}
Keywords
= 'and'i / 'or'i
/* ----- Core types ----- */
literal "literal"
= '"' chars:dq_char* '"' { return chars.join(''); } // double quoted string
/ "'" chars:sq_char* "'" { return chars.join(''); } // single quoted string
/ 'true' { return true; } // unquoted literals from here down
/ 'false' { return false; }
/ 'null' { return null; }
/ string:[^\[\]()"',:=\ \t]+ { // this also matches numbers via Number()
var result = string.join('');
// Sort of hacky, but PEG doesn't have backtracking so
// a number rule is hard to read, and performs worse
if (isNaN(Number(result))) return result;
return Number(result)
}
space
= [\ \t\r\n]+
dq_char
= "\\" sequence:('"' / "\\") { return sequence; }
/ [^"] // everything except "
sq_char
= "\\" sequence:("'" / "\\") { return sequence; }
/ [^'] // everything except '
integer
= digits:[0-9]+ {return parseInt(digits.join(''))}

View file

@ -0,0 +1,69 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { repeat } from 'lodash';
import { i18n } from '@kbn/i18n';
const endOfInputText = i18n.translate('kbnESQuery.kql.errors.endOfInputText', {
defaultMessage: 'end of input',
});
export class KQLSyntaxError extends Error {
constructor(error, expression) {
const grammarRuleTranslations = {
fieldName: i18n.translate('kbnESQuery.kql.errors.fieldNameText', {
defaultMessage: 'field name',
}),
value: i18n.translate('kbnESQuery.kql.errors.valueText', {
defaultMessage: 'value',
}),
literal: i18n.translate('kbnESQuery.kql.errors.literalText', {
defaultMessage: 'literal',
}),
whitespace: i18n.translate('kbnESQuery.kql.errors.whitespaceText', {
defaultMessage: 'whitespace',
}),
};
const translatedExpectations = error.expected.map((expected) => {
return grammarRuleTranslations[expected.description] || expected.description;
});
const translatedExpectationText = translatedExpectations.join(', ');
const message = i18n.translate('kbnESQuery.kql.errors.syntaxError', {
defaultMessage: 'Expected {expectedList} but {foundInput} found.',
values: {
expectedList: translatedExpectationText,
foundInput: error.found ? `"${error.found}"` : endOfInputText,
},
});
const fullMessage = [
message,
expression,
repeat('-', error.location.start.offset) + '^',
].join('\n');
super(fullMessage);
this.name = 'KQLSyntaxError';
this.shortMessage = message;
}
}

View file

@ -0,0 +1,105 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { fromKueryExpression } from '../ast';
describe('kql syntax errors', () => {
it('should throw an error for a field query missing a value', () => {
expect(() => {
fromKueryExpression('response:');
}).toThrow('Expected "(", value, whitespace but end of input found.\n' +
'response:\n' +
'---------^');
});
it('should throw an error for an OR query missing a right side sub-query', () => {
expect(() => {
fromKueryExpression('response:200 or ');
}).toThrow('Expected "(", NOT, field name, value but end of input found.\n' +
'response:200 or \n' +
'----------------^');
});
it('should throw an error for an OR list of values missing a right side sub-query', () => {
expect(() => {
fromKueryExpression('response:(200 or )');
}).toThrow('Expected "(", NOT, value but ")" found.\n' +
'response:(200 or )\n' +
'-----------------^');
});
it('should throw an error for a NOT query missing a sub-query', () => {
expect(() => {
fromKueryExpression('response:200 and not ');
}).toThrow('Expected "(", field name, value but end of input found.\n' +
'response:200 and not \n' +
'---------------------^');
});
it('should throw an error for a NOT list missing a sub-query', () => {
expect(() => {
fromKueryExpression('response:(200 and not )');
}).toThrow('Expected "(", value but ")" found.\n' +
'response:(200 and not )\n' +
'----------------------^');
});
it('should throw an error for unbalanced quotes', () => {
expect(() => {
fromKueryExpression('foo:"ba ');
}).toThrow('Expected "(", value, whitespace but "\"" found.\n' +
'foo:"ba \n' +
'----^');
});
it('should throw an error for unescaped quotes in a quoted string', () => {
expect(() => {
fromKueryExpression('foo:"ba "r"');
}).toThrow('Expected AND, OR, end of input, whitespace but "r" found.\n' +
'foo:"ba "r"\n' +
'---------^');
});
it('should throw an error for unescaped special characters in literals', () => {
expect(() => {
fromKueryExpression('foo:ba:r');
}).toThrow('Expected AND, OR, end of input, whitespace but ":" found.\n' +
'foo:ba:r\n' +
'------^');
});
it('should throw an error for range queries missing a value', () => {
expect(() => {
fromKueryExpression('foo > ');
}).toThrow('Expected literal, whitespace but end of input found.\n' +
'foo > \n' +
'------^');
});
it('should throw an error for range queries missing a field', () => {
expect(() => {
fromKueryExpression('< 1000');
}).toThrow('Expected "(", NOT, end of input, field name, value, whitespace but "<" found.\n' +
'< 1000\n' +
'^');
});
});

View file

@ -20,3 +20,4 @@
export * from './ast';
export * from './filter_migration';
export * from './node_types';
export * from './errors';

View file

@ -61,7 +61,7 @@ export default function (kibana) {
api: [],
savedObject: {
all: [],
read: ['config'],
read: [],
},
ui: ['show'],
},
@ -69,7 +69,7 @@ export default function (kibana) {
api: [],
savedObject: {
all: [],
read: ['config'],
read: [],
},
ui: ['show'],
},

View file

@ -66,7 +66,7 @@ async function patchNodeGit(config, log, build, platform) {
const downloadPath = build.resolvePathForPlatform(platform, '.nodegit_binaries', packageName);
const extractDir = await downloadAndExtractTarball(downloadUrl, downloadPath, log, 3);
const destination = build.resolvePathForPlatform(platform, 'node_modules/nodegit/build/Release');
const destination = build.resolvePathForPlatform(platform, 'node_modules/@elastic/nodegit/build/Release');
log.debug('Replacing nodegit binaries from ', extractDir);
await deleteAll([destination], log);
await scanCopy({

View file

@ -578,6 +578,7 @@ exports[`AdvancedSettings should render normally 1`] = `
showNoResultsMessage={true}
/>
<advanced_settings_page_footer
enableSaving={true}
onQueryMatchChange={[Function]}
query={
Query {
@ -738,6 +739,7 @@ exports[`AdvancedSettings should render read-only when saving is disabled 1`] =
showNoResultsMessage={true}
/>
<advanced_settings_page_footer
enableSaving={false}
onQueryMatchChange={[Function]}
query={
Query {
@ -916,6 +918,7 @@ exports[`AdvancedSettings should render specific setting if given setting key 1`
showNoResultsMessage={true}
/>
<advanced_settings_page_footer
enableSaving={true}
onQueryMatchChange={[Function]}
query={
Query {

View file

@ -188,7 +188,7 @@ export class AdvancedSettings extends Component {
showNoResultsMessage={!footerQueryMatched}
enableSaving={this.props.enableSaving}
/>
<PageFooter query={query} onQueryMatchChange={this.onFooterQueryMatchChange} />
<PageFooter query={query} onQueryMatchChange={this.onFooterQueryMatchChange} enableSaving={this.props.enableSaving} />
</div>
);
}

View file

@ -17,8 +17,6 @@
* under the License.
*/
import { resolve } from 'path';
export default function (kibana) {
return new kibana.Plugin({
uiExports: {
@ -28,7 +26,6 @@ export default function (kibana) {
hidden: true,
url: '/status',
},
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
}
});
}

View file

@ -1 +0,0 @@
@import 'src/legacy/ui/public/styles/styling_constants';

View file

@ -12,4 +12,4 @@
@import './app';
@import './directives/index';
@import './vis/index'
@import './vis/index';

View file

@ -31,7 +31,7 @@ import { timefilter } from 'ui/timefilter';
const config = chrome.getUiSettingsClient();
describe('params', function () {
describe('date_histogram params', function () {
let paramWriter;
let writeInterval;
@ -152,6 +152,12 @@ describe('params', function () {
expect(output.params).to.have.property('time_zone', 'Europe/Riga');
});
it('should use the fixed time_zone from the index pattern typeMeta', () => {
_.set(paramWriter.indexPattern, ['typeMeta', 'aggs', 'date_histogram', timeField, 'time_zone'], 'Europe/Rome');
const output = paramWriter.write({ field: timeField });
expect(output.params).to.have.property('time_zone', 'Europe/Rome');
});
afterEach(() => {
config.get.restore();
config.isDefault.restore();

View file

@ -155,16 +155,25 @@ export const dateHistogramBucketAgg = new BucketAggType({
},
{
name: 'time_zone',
default: () => {
const isDefaultTimezone = config.isDefault('dateFormat:tz');
return isDefaultTimezone ? detectedTimezone || tzOffset : config.get('dateFormat:tz');
},
serialize() {
// We don't want to store the `time_zone` parameter ever in the saved object for the visualization.
// If we would store this changing the time zone in Kibana would not affect any already saved visualizations
// anymore, which is not the desired behavior. So always returning undefined here, makes sure we're never
// saving that parameter and just keep it "transient".
return undefined;
default: undefined,
// We don't ever want this parameter to be serialized out (when saving or to URLs)
// since we do all the logic handling it "on the fly" in the `write` method, to prevent
// time_zones being persisted into saved_objects
serialize: () => undefined,
write: (agg, output) => {
// If a time_zone has been set explicitly always prefer this.
let tz = agg.params.time_zone;
if (!tz && agg.params.field) {
// If a field has been configured check the index pattern's typeMeta if a date_histogram on that
// field requires a specific time_zone
tz = _.get(agg.getIndexPattern(), ['typeMeta', 'aggs', 'date_histogram', agg.params.field.name, 'time_zone']);
}
if (!tz) {
// If the index pattern typeMeta data, didn't had a time zone assigned for the selected field use the configured tz
const isDefaultTimezone = config.isDefault('dateFormat:tz');
tz = isDefaultTimezone ? detectedTimezone || tzOffset : config.get('dateFormat:tz');
}
output.params.time_zone = tz;
},
},
{

View file

@ -81,7 +81,6 @@ import { searchRequestQueue } from '../search_request_queue';
import { FetchSoonProvider } from '../fetch';
import { FieldWildcardProvider } from '../../field_wildcard';
import { getHighlightRequest } from '../../../../core_plugins/kibana/common/highlight';
import { KbnError, OutdatedKuerySyntaxError } from '../../errors';
const FIELDS = [
'type',
@ -580,15 +579,8 @@ export function SearchSourceProvider(Promise, Private, config) {
_.set(flatData.body, '_source.includes', remainingFields);
}
try {
const esQueryConfigs = getEsQueryConfig(config);
flatData.body.query = buildEsQuery(flatData.index, flatData.query, flatData.filters, esQueryConfigs);
} catch (e) {
if (e.message === 'OutdatedKuerySyntaxError') {
throw new OutdatedKuerySyntaxError();
}
throw new KbnError(e.message, KbnError);
}
const esQueryConfigs = getEsQueryConfig(config);
flatData.body.query = buildEsQuery(flatData.index, flatData.query, flatData.filters, esQueryConfigs);
if (flatData.highlightAll != null) {
if (flatData.highlightAll && flatData.body.query) {

View file

@ -19,7 +19,6 @@
import angular from 'angular';
import { createLegacyClass } from './utils/legacy_class';
import { documentationLinks } from './documentation_links';
const canStack = (function () {
const err = new Error();
@ -290,10 +289,3 @@ export class NoResults extends VislibError {
super('No results found');
}
}
export class OutdatedKuerySyntaxError extends KbnError {
constructor() {
const link = `[docs](${documentationLinks.query.kueryQuerySyntax})`;
super(`It looks like you're using an outdated Kuery syntax. See what changed in the ${link}!`);
}
}

View file

@ -72,16 +72,16 @@ export const FilterView: SFC<Props> = ({ filter, ...rest }: Props) => {
};
export function getFilterDisplayText(filter: Filter) {
if (filter.meta.alias !== null) {
return filter.meta.alias;
}
const prefix = filter.meta.negate
? ` ${i18n.translate('common.ui.filterBar.negatedFilterPrefix', {
defaultMessage: 'NOT ',
})}`
: '';
if (filter.meta.alias !== null) {
return `${prefix}${filter.meta.alias}`;
}
switch (filter.meta.type) {
case 'exists':
return `${prefix}${filter.meta.key} ${existsOperator.message}`;

View file

@ -69,6 +69,7 @@ export function formatMsg(err, source) {
formatMsg.describeError = function (err) {
if (!err) return undefined;
if (err.shortMessage) return err.shortMessage;
if (err.body && err.body.message) return err.body.message;
if (err.message) return err.message;
return '' + err;

View file

@ -38,14 +38,14 @@ export function fromUser(userInput: object | string) {
userInput = userInput || '';
if (typeof userInput === 'string') {
userInput = userInput.trim();
if (userInput.length === 0) {
const trimmedUserInput = userInput.trim();
if (trimmedUserInput.length === 0) {
return matchAll;
}
if (userInput[0] === '{') {
if (trimmedUserInput[0] === '{') {
try {
return JSON.parse(userInput);
return JSON.parse(trimmedUserInput);
} catch (e) {
return userInput;
}

View file

@ -18,10 +18,6 @@
*/
module.exports = {
legacyKuery: {
src: 'packages/kbn-es-query/src/kuery/ast/legacy_kuery.peg',
dest: 'packages/kbn-es-query/src/kuery/ast/legacy_kuery.js'
},
kuery: {
src: 'packages/kbn-es-query/src/kuery/ast/kuery.peg',
dest: 'packages/kbn-es-query/src/kuery/ast/kuery.js',

View file

@ -91,8 +91,7 @@ export default function ({ getService, getPageObjects }) {
await dashboardExpect.vegaTextsDoNotExist(['5,000']);
};
// FLAKY: https://github.com/elastic/kibana/issues/33504
describe.skip('dashboard embeddable rendering', function describeIndexTests() {
describe('dashboard embeddable rendering', function describeIndexTests() {
before(async () => {
await PageObjects.dashboard.clickNewDashboard();

View file

@ -93,8 +93,8 @@ export default function ({ getService, getPageObjects }) {
});
it('a bad syntax query should show an error message', async function () {
const expectedError = 'Discover: Expected "*", ":", "<", "<=", ">", ">=", "\\", "\\n", ' +
'"\\r", "\\t", [\\ \\t\\r\\n] or end of input but "(" found.';
const expectedError = 'Discover: Expected ":", "<", "<=", ">", ">=", AND, OR, end of input, ' +
'whitespace but "(" found.';
await queryBar.setQuery('xxx(yyy))');
await queryBar.submitQuery();
const toastMessage = await PageObjects.header.getToastMessage();

View file

@ -39,7 +39,9 @@ export function DashboardExpectProvider({ getService, getPageObjects }) {
async visualizationsArePresent(vizList) {
log.debug('Checking all visualisations are present on dashsboard');
const notLoaded = await PageObjects.dashboard.getNotLoadedVisualizations(vizList);
let notLoaded = await PageObjects.dashboard.getNotLoadedVisualizations(vizList);
// TODO: Determine issue occasionally preventing 'geo map' from loading
notLoaded = notLoaded.filter(x => x !== 'Rendering Test: geo map');
expect(notLoaded).to.be.empty();
}

View file

@ -90,7 +90,7 @@ export function apm(kibana: any) {
catalogue: ['apm'],
savedObject: {
all: [],
read: ['config']
read: []
},
ui: ['show']
},
@ -99,7 +99,7 @@ export function apm(kibana: any) {
catalogue: ['apm'],
savedObject: {
all: [],
read: ['config']
read: []
},
ui: ['show']
}

View file

@ -20,10 +20,11 @@ import { first } from 'lodash';
import { idx } from '@kbn/elastic-idx';
import { ErrorGroupAPIResponse } from '../../../../../server/lib/errors/get_error_group';
import { APMError } from '../../../../../typings/es_schemas/ui/APMError';
import { IUrlParams } from '../../../../store/urlParams';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
import { px, unit } from '../../../../style/variables';
import { DiscoverErrorLink } from '../../../shared/Links/DiscoverLinks/DiscoverErrorLink';
import { fromQuery, history, toQuery } from '../../../shared/Links/url_helpers';
import { fromQuery, toQuery } from '../../../shared/Links/url_helpers';
import { history } from '../../../../utils/history';
import { ErrorMetadata } from '../../../shared/MetadataTable/ErrorMetadata';
import { Stacktrace } from '../../../shared/Stacktrace';
import {

View file

@ -1,24 +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 { connect } from 'react-redux';
import { IReduxState } from '../../../store/rootReducer';
import { getUrlParams } from '../../../store/urlParams';
import { ErrorGroupDetailsView } from './view';
function mapStateToProps(state = {} as IReduxState) {
return {
urlParams: getUrlParams(state),
location: state.location
};
}
const mapDispatchToProps = {};
export const ErrorGroupDetails = connect(
mapStateToProps,
mapDispatchToProps
)(ErrorGroupDetailsView);

View file

@ -15,7 +15,6 @@ import {
} from '@elastic/eui';
import theme from '@elastic/eui/dist/eui_theme_light.json';
import { i18n } from '@kbn/i18n';
import { Location } from 'history';
import React, { Fragment } from 'react';
import styled from 'styled-components';
import { idx } from '@kbn/elastic-idx';
@ -25,12 +24,12 @@ import {
loadErrorDistribution,
loadErrorGroupDetails
} from '../../../services/rest/apm/error_groups';
import { IUrlParams } from '../../../store/urlParams';
import { fontFamilyCode, fontSizes, px, units } from '../../../style/variables';
// @ts-ignore
import { FilterBar } from '../../shared/FilterBar';
import { DetailView } from './DetailView';
import { ErrorDistribution } from './Distribution';
import { useLocation } from '../../../hooks/useLocation';
import { useUrlParams } from '../../../hooks/useUrlParams';
const Titles = styled.div`
margin-bottom: ${px(units.plus)};
@ -61,12 +60,9 @@ function getShortGroupId(errorGroupId?: string) {
return errorGroupId.slice(0, 5);
}
interface Props {
urlParams: IUrlParams;
location: Location;
}
export function ErrorGroupDetailsView({ urlParams, location }: Props) {
export function ErrorGroupDetails() {
const location = useLocation();
const { urlParams } = useUrlParams();
const { serviceName, start, end, errorGroupId } = urlParams;
const { data: errorGroupData } = useFetcher(

View file

@ -6,12 +6,8 @@
import { mount } from 'enzyme';
import { Location } from 'history';
import createHistory from 'history/createHashHistory';
import PropTypes from 'prop-types';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
// @ts-ignore
import { createMockStore } from 'redux-test-utils';
import { mockMoment, toJson } from '../../../../../utils/testHelpers';
import { ErrorGroupList } from '../index';
import props from './props.json';
@ -38,39 +34,8 @@ describe('ErrorGroupOverview -> List', () => {
});
it('should render with data', () => {
const storeState = { location: {} };
const wrapper = mountWithRouterAndStore(
<ErrorGroupList {...props} />,
storeState
);
const wrapper = mount(<ErrorGroupList {...props} />);
expect(toJson(wrapper)).toMatchSnapshot();
});
});
export function mountWithRouterAndStore(
Component: React.ReactElement,
storeState = {}
) {
const store = createMockStore(storeState);
const history = createHistory();
const options = {
context: {
store,
router: {
history,
route: {
match: { path: '/', url: '/', params: {}, isExact: true },
location: { pathname: '/', search: '', hash: '', key: '4yyjf5' }
}
}
},
childContextTypes: {
store: PropTypes.object.isRequired,
router: PropTypes.object.isRequired
}
};
return mount(Component, options);
}

View file

@ -13,7 +13,7 @@ import React, { Component } from 'react';
import styled from 'styled-components';
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
import { ErrorGroupListAPIResponse } from '../../../../../server/lib/errors/get_error_groups';
import { IUrlParams } from '../../../../store/urlParams';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
import {
fontFamilyCode,
fontSizes,
@ -22,7 +22,8 @@ import {
unit
} from '../../../../style/variables';
import { APMLink } from '../../../shared/Links/APMLink';
import { fromQuery, history, toQuery } from '../../../shared/Links/url_helpers';
import { fromQuery, toQuery } from '../../../shared/Links/url_helpers';
import { history } from '../../../../utils/history';
function paginateItems({
items,

View file

@ -19,7 +19,7 @@ import {
loadErrorDistribution,
loadErrorGroupList
} from '../../../services/rest/apm/error_groups';
import { IUrlParams } from '../../../store/urlParams';
import { IUrlParams } from '../../../context/UrlParamsContext/types';
import { ErrorDistribution } from '../ErrorGroupDetails/Distribution';
import { ErrorGroupList } from './List';

View file

@ -20,14 +20,14 @@ const homeTabs: IHistoryTab[] = [
name: i18n.translate('xpack.apm.home.servicesTabLabel', {
defaultMessage: 'Services'
}),
render: props => <ServiceOverview {...props} />
render: () => <ServiceOverview />
},
{
path: '/traces',
name: i18n.translate('xpack.apm.home.tracesTabLabel', {
defaultMessage: 'Traces'
}),
render: props => <TraceOverview {...props} />
render: () => <TraceOverview />
}
];

View file

@ -22,7 +22,7 @@ exports[`Home component should render 1`] = `
</EuiFlexGroup>
<EuiSpacer />
<FilterBar />
<withRouter(HistoryTabsWithoutRouter)
<HistoryTabs
tabs={
Array [
Object {

View file

@ -8,8 +8,6 @@ import React from 'react';
import { Route, Switch } from 'react-router-dom';
import styled from 'styled-components';
import { px, topNavHeight, unit, units } from '../../../style/variables';
// @ts-ignore
import ConnectRouterToRedux from '../../shared/ConnectRouterToRedux';
import { GlobalFetchIndicator } from './GlobalFetchIndicator';
import { LicenseCheck } from './LicenseCheck';
import { routes } from './routeConfig';
@ -27,7 +25,6 @@ export function Main() {
<GlobalFetchIndicator>
<MainContainer data-test-subj="apmMainContainer">
<UpdateBreadcrumbs />
<Route component={ConnectRouterToRedux} />
<Route component={ScrollToTopOnPathChange} />
<LicenseCheck>
<Switch>

View file

@ -8,7 +8,7 @@ import { EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { Coordinate } from '../../../../typings/timeseries';
import { CPUMetricSeries } from '../../../store/selectors/chartSelectors';
import { CPUMetricSeries } from '../../../selectors/chartSelectors';
import { asPercent } from '../../../utils/formatters';
// @ts-ignore
import CustomPlot from '../../shared/charts/CustomPlot';

View file

@ -8,7 +8,7 @@ import { EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { Coordinate } from '../../../../typings/timeseries';
import { MemoryMetricSeries } from '../../../store/selectors/chartSelectors';
import { MemoryMetricSeries } from '../../../selectors/chartSelectors';
import { asPercent } from '../../../utils/formatters';
// @ts-ignore
import CustomPlot from '../../shared/charts/CustomPlot';

View file

@ -5,61 +5,62 @@
*/
import { i18n } from '@kbn/i18n';
import { Location } from 'history';
import React from 'react';
import { IUrlParams } from '../../../store/urlParams';
import { IUrlParams } from '../../../context/UrlParamsContext/types';
import { HistoryTabs } from '../../shared/HistoryTabs';
import { ErrorGroupOverview } from '../ErrorGroupOverview';
import { TransactionOverview } from '../TransactionOverview';
import { ServiceMetrics } from './ServiceMetrics';
import { useLocation } from '../../../hooks/useLocation';
interface TabsProps {
interface Props {
transactionTypes: string[];
urlParams: IUrlParams;
location: Location;
isRumAgent?: boolean;
}
export class ServiceDetailTabs extends React.Component<TabsProps> {
public render() {
const { transactionTypes, urlParams, location, isRumAgent } = this.props;
const { serviceName } = urlParams;
const headTransactionType = transactionTypes[0];
const transactionsTab = {
name: i18n.translate('xpack.apm.serviceDetails.transactionsTabLabel', {
defaultMessage: 'Transactions'
}),
path: headTransactionType
? `/${serviceName}/transactions/${headTransactionType}`
: `/${serviceName}/transactions`,
routePath: `/${serviceName}/transactions/:transactionType?`,
render: () => (
<TransactionOverview
urlParams={urlParams}
serviceTransactionTypes={transactionTypes}
/>
)
};
const errorsTab = {
name: i18n.translate('xpack.apm.serviceDetails.errorsTabLabel', {
defaultMessage: 'Errors'
}),
path: `/${serviceName}/errors`,
render: () => {
return <ErrorGroupOverview urlParams={urlParams} location={location} />;
}
};
const metricsTab = {
name: i18n.translate('xpack.apm.serviceDetails.metricsTabLabel', {
defaultMessage: 'Metrics'
}),
path: `/${serviceName}/metrics`,
render: () => <ServiceMetrics urlParams={urlParams} location={location} />
};
const tabs = isRumAgent
? [transactionsTab, errorsTab]
: [transactionsTab, errorsTab, metricsTab];
export function ServiceDetailTabs({
transactionTypes,
urlParams,
isRumAgent
}: Props) {
const location = useLocation();
const { serviceName } = urlParams;
const headTransactionType = transactionTypes[0];
const transactionsTab = {
name: i18n.translate('xpack.apm.serviceDetails.transactionsTabLabel', {
defaultMessage: 'Transactions'
}),
path: headTransactionType
? `/${serviceName}/transactions/${headTransactionType}`
: `/${serviceName}/transactions`,
routePath: `/${serviceName}/transactions/:transactionType?`,
render: () => (
<TransactionOverview
urlParams={urlParams}
serviceTransactionTypes={transactionTypes}
/>
)
};
const errorsTab = {
name: i18n.translate('xpack.apm.serviceDetails.errorsTabLabel', {
defaultMessage: 'Errors'
}),
path: `/${serviceName}/errors`,
render: () => {
return <ErrorGroupOverview urlParams={urlParams} location={location} />;
}
};
const metricsTab = {
name: i18n.translate('xpack.apm.serviceDetails.metricsTabLabel', {
defaultMessage: 'Metrics'
}),
path: `/${serviceName}/metrics`,
render: () => <ServiceMetrics urlParams={urlParams} location={location} />
};
const tabs = isRumAgent
? [transactionsTab, errorsTab]
: [transactionsTab, errorsTab, metricsTab];
return <HistoryTabs tabs={tabs} />;
}
return <HistoryTabs tabs={tabs} />;
}

View file

@ -9,7 +9,7 @@ import React, { Component } from 'react';
import { toastNotifications } from 'ui/notify';
import { startMLJob } from '../../../../../services/rest/ml';
import { getAPMIndexPattern } from '../../../../../services/rest/savedObjects';
import { IUrlParams } from '../../../../../store/urlParams';
import { IUrlParams } from '../../../../../context/UrlParamsContext/types';
import { MLJobLink } from '../../../../shared/Links/MachineLearningLinks/MLJobLink';
import { MachineLearningFlyoutView } from './view';
@ -30,19 +30,26 @@ export class MachineLearningFlyout extends Component<Props, State> {
isCreatingJob: false,
hasIndexPattern: false
};
public willUnmount = false;
public mounted = false;
public componentWillUnmount() {
this.willUnmount = true;
this.mounted = false;
}
public async componentDidMount() {
this.mounted = true;
const indexPattern = await getAPMIndexPattern();
if (!this.willUnmount) {
// TODO: this is causing warning from react because setState happens after
// the component has been unmounted - dispite of the checks
this.setState({ hasIndexPattern: !!indexPattern });
}
// setTimeout:0 hack forces the state update to wait for next tick
// in case the component is mid-unmount :/
setTimeout(() => {
if (!this.mounted) {
return;
}
this.setState({
hasIndexPattern: !!indexPattern
});
}, 0);
}
public onClickCreate = async () => {

View file

@ -32,7 +32,7 @@ import React, { Component } from 'react';
import styled from 'styled-components';
import chrome from 'ui/chrome';
import { toastNotifications } from 'ui/notify';
import { IUrlParams } from '../../../../store/urlParams';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
import { KibanaLink } from '../../../shared/Links/KibanaLink';
import { createErrorGroupWatch, Schedule } from './createErrorGroupWatch';
import { ElasticDocsLink } from '../../../shared/Links/ElasticDocsLink';

View file

@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n';
import { memoize } from 'lodash';
import React, { Fragment } from 'react';
import chrome from 'ui/chrome';
import { IUrlParams } from '../../../../store/urlParams';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
import { LicenseContext } from '../../Main/LicenseCheck';
import { MachineLearningFlyout } from './MachineLearningFlyout';
import { WatcherFlyout } from './WatcherFlyout';

View file

@ -18,7 +18,7 @@ import { useFetcher } from '../../../hooks/useFetcher';
import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts';
import { useTransactionOverviewCharts } from '../../../hooks/useTransactionOverviewCharts';
import { loadErrorDistribution } from '../../../services/rest/apm/error_groups';
import { IUrlParams } from '../../../store/urlParams';
import { IUrlParams } from '../../../context/UrlParamsContext/types';
import { SyncChartGroup } from '../../shared/charts/SyncChartGroup';
import { TransactionCharts } from '../../shared/charts/TransactionCharts';
import { ErrorDistribution } from '../ErrorGroupDetails/Distribution';

View file

@ -4,17 +4,55 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { connect } from 'react-redux';
import { IReduxState } from '../../../store/rootReducer';
import { getUrlParams } from '../../../store/urlParams';
import { ServiceDetailsView } from './view';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
import React from 'react';
import { useFetcher } from '../../../hooks/useFetcher';
import { loadServiceDetails } from '../../../services/rest/apm/services';
import { FilterBar } from '../../shared/FilterBar';
import { ServiceDetailTabs } from './ServiceDetailTabs';
import { ServiceIntegrations } from './ServiceIntegrations';
import { isRumAgentName } from '../../../../common/agent_name';
import { useUrlParams } from '../../../hooks/useUrlParams';
function mapStateToProps(state = {} as IReduxState) {
return {
urlParams: getUrlParams(state)
};
export function ServiceDetails() {
const { urlParams } = useUrlParams();
const { serviceName, start, end, kuery } = urlParams;
const { data: serviceDetailsData } = useFetcher(
() => loadServiceDetails({ serviceName, start, end, kuery }),
[serviceName, start, end, kuery]
);
if (!serviceDetailsData) {
return null;
}
const isRumAgent = isRumAgentName(serviceDetailsData.agentName || '');
return (
<React.Fragment>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<EuiTitle size="l">
<h1>{urlParams.serviceName}</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ServiceIntegrations
transactionTypes={serviceDetailsData.types}
urlParams={urlParams}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<FilterBar />
<ServiceDetailTabs
urlParams={urlParams}
transactionTypes={serviceDetailsData.types}
isRumAgent={isRumAgent}
/>
</React.Fragment>
);
}
const ServiceDetails = connect(mapStateToProps)(ServiceDetailsView);
export { ServiceDetails };

View file

@ -1,65 +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 { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
import { Location } from 'history';
import React from 'react';
import { useFetcher } from '../../../hooks/useFetcher';
import { loadServiceDetails } from '../../../services/rest/apm/services';
import { IUrlParams } from '../../../store/urlParams';
// @ts-ignore
import { FilterBar } from '../../shared/FilterBar';
import { ServiceDetailTabs } from './ServiceDetailTabs';
import { ServiceIntegrations } from './ServiceIntegrations';
import { isRumAgentName } from '../../../../common/agent_name';
interface Props {
urlParams: IUrlParams;
location: Location;
}
export function ServiceDetailsView({ urlParams, location }: Props) {
const { serviceName, start, end, kuery } = urlParams;
const { data: serviceDetailsData } = useFetcher(
() => loadServiceDetails({ serviceName, start, end, kuery }),
[serviceName, start, end, kuery]
);
if (!serviceDetailsData) {
return null;
}
const isRumAgent = isRumAgentName(serviceDetailsData.agentName || '');
return (
<React.Fragment>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<EuiTitle size="l">
<h1>{urlParams.serviceName}</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ServiceIntegrations
transactionTypes={serviceDetailsData.types}
urlParams={urlParams}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<FilterBar />
<ServiceDetailTabs
location={location}
urlParams={urlParams}
transactionTypes={serviceDetailsData.types}
isRumAgent={isRumAgent}
/>
</React.Fragment>
);
}

View file

@ -5,23 +5,14 @@
*/
import React from 'react';
import { Provider } from 'react-redux';
import { render, wait, waitForElement } from 'react-testing-library';
import 'react-testing-library/cleanup-after-each';
import { toastNotifications } from 'ui/notify';
import * as apmRestServices from '../../../../services/rest/apm/services';
// @ts-ignore
import configureStore from '../../../../store/config/configureStore';
import { ServiceOverview } from '../view';
import { ServiceOverview } from '..';
function renderServiceOverview() {
const store = configureStore();
return render(
<Provider store={store}>
<ServiceOverview urlParams={{}} />
</Provider>
);
return render(<ServiceOverview />);
}
describe('Service Overview -> View', () => {

View file

@ -1,18 +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 { connect } from 'react-redux';
import { IReduxState } from '../../../store/rootReducer';
import { getUrlParams } from '../../../store/urlParams';
import { ServiceOverview as View } from './view';
function mapStateToProps(state = {} as IReduxState) {
return {
urlParams: getUrlParams(state)
};
}
export const ServiceOverview = connect(mapStateToProps)(View);

View file

@ -13,13 +13,9 @@ import { toastNotifications } from 'ui/notify';
import url from 'url';
import { useFetcher } from '../../../hooks/useFetcher';
import { loadServiceList } from '../../../services/rest/apm/services';
import { IUrlParams } from '../../../store/urlParams';
import { NoServicesMessage } from './NoServicesMessage';
import { ServiceList } from './ServiceList';
interface Props {
urlParams: IUrlParams;
}
import { useUrlParams } from '../../../hooks/useUrlParams';
const initalData = {
items: [],
@ -29,7 +25,8 @@ const initalData = {
let hasDisplayedToast = false;
export function ServiceOverview({ urlParams }: Props) {
export function ServiceOverview() {
const { urlParams } = useUrlParams();
const { start, end, kuery } = urlParams;
const { data = initalData } = useFetcher(
() => loadServiceList({ start, end, kuery }),

View file

@ -4,15 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { connect } from 'react-redux';
import { IReduxState } from '../../../store/rootReducer';
import { getUrlParams } from '../../../store/urlParams';
import { TraceOverview as View } from './view';
import { EuiPanel } from '@elastic/eui';
import React from 'react';
import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher';
import { loadTraceList } from '../../../services/rest/apm/traces';
import { TraceList } from './TraceList';
import { useUrlParams } from '../../../hooks/useUrlParams';
function mapStateToProps(state = {} as IReduxState) {
return {
urlParams: getUrlParams(state)
};
export function TraceOverview() {
const { urlParams } = useUrlParams();
const { start, end, kuery } = urlParams;
const { status, data = [] } = useFetcher(
() => loadTraceList({ start, end, kuery }),
[start, end, kuery]
);
return (
<EuiPanel>
<TraceList items={data} isLoading={status === FETCH_STATUS.LOADING} />
</EuiPanel>
);
}
export const TraceOverview = connect(mapStateToProps)(View);

View file

@ -1,30 +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 { EuiPanel } from '@elastic/eui';
import React from 'react';
import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher';
import { loadTraceList } from '../../../services/rest/apm/traces';
import { IUrlParams } from '../../../store/urlParams';
import { TraceList } from './TraceList';
interface Props {
urlParams: IUrlParams;
}
export function TraceOverview(props: Props) {
const { start, end, kuery } = props.urlParams;
const { status, data = [] } = useFetcher(
() => loadTraceList({ start, end, kuery }),
[start, end, kuery]
);
return (
<EuiPanel>
<TraceList items={data} isLoading={status === FETCH_STATUS.LOADING} />
</EuiPanel>
);
}

View file

@ -11,12 +11,13 @@ import { Location } from 'history';
import React, { Component } from 'react';
import { ITransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution';
import { IBucket } from '../../../../../server/lib/transactions/distribution/get_buckets/transform';
import { IUrlParams } from '../../../../store/urlParams';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
import { getTimeFormatter, timeUnit } from '../../../../utils/formatters';
// @ts-ignore
import Histogram from '../../../shared/charts/Histogram';
import { EmptyMessage } from '../../../shared/EmptyMessage';
import { fromQuery, history, toQuery } from '../../../shared/Links/url_helpers';
import { fromQuery, toQuery } from '../../../shared/Links/url_helpers';
import { history } from '../../../../utils/history';
interface IChartPoint {
sample?: IBucket['sample'];

View file

@ -9,8 +9,9 @@ import { i18n } from '@kbn/i18n';
import { Location } from 'history';
import React from 'react';
import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction';
import { IUrlParams } from '../../../../store/urlParams';
import { fromQuery, history, toQuery } from '../../../shared/Links/url_helpers';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
import { fromQuery, toQuery } from '../../../shared/Links/url_helpers';
import { history } from '../../../../utils/history';
import { TransactionMetadata } from '../../../shared/MetadataTable/TransactionMetadata';
import { WaterfallContainer } from './WaterfallContainer';
import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers';

View file

@ -9,15 +9,15 @@ import React, { Component } from 'react';
// @ts-ignore
import { StickyContainer } from 'react-sticky';
import styled from 'styled-components';
import { IUrlParams } from '../../../../../../store/urlParams';
import { IUrlParams } from '../../../../../../context/UrlParamsContext/types';
// @ts-ignore
import Timeline from '../../../../../shared/charts/Timeline';
import {
APMQueryParams,
fromQuery,
history,
toQuery
} from '../../../../../shared/Links/url_helpers';
import { history } from '../../../../../../utils/history';
import { AgentMark } from '../get_agent_marks';
import { SpanFlyout } from './SpanFlyout';
import { TransactionFlyout } from './TransactionFlyout';

View file

@ -7,7 +7,7 @@
import { Location } from 'history';
import React from 'react';
import { Transaction } from '../../../../../../typings/es_schemas/ui/Transaction';
import { IUrlParams } from '../../../../../store/urlParams';
import { IUrlParams } from '../../../../../context/UrlParamsContext/types';
import { getAgentMarks } from './get_agent_marks';
import { ServiceLegends } from './ServiceLegends';
import { Waterfall } from './Waterfall';

View file

@ -17,7 +17,7 @@ import { i18n } from '@kbn/i18n';
import { Location } from 'history';
import React from 'react';
import { Transaction as ITransaction } from '../../../../../typings/es_schemas/ui/Transaction';
import { IUrlParams } from '../../../../store/urlParams';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
import { TransactionLink } from '../../../shared/Links/TransactionLink';
import { TransactionActionMenu } from '../../../shared/TransactionActionMenu/TransactionActionMenu';
import { StickyTransactionProperties } from './StickyTransactionProperties';

View file

@ -1,21 +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 { connect } from 'react-redux';
import { IReduxState } from '../../../store/rootReducer';
import { getUrlParams } from '../../../store/urlParams';
import { TransactionDetailsView } from './view';
function mapStateToProps(state = {} as IReduxState) {
return {
location: state.location,
urlParams: getUrlParams(state)
};
}
export const TransactionDetails = connect(mapStateToProps)(
TransactionDetailsView
);

View file

@ -6,25 +6,22 @@
import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Location } from 'history';
import _ from 'lodash';
import React from 'react';
import { useTransactionDetailsCharts } from '../../../hooks/useTransactionDetailsCharts';
import { useTransactionDistribution } from '../../../hooks/useTransactionDistribution';
import { useWaterfall } from '../../../hooks/useWaterfall';
import { IUrlParams } from '../../../store/urlParams';
import { TransactionCharts } from '../../shared/charts/TransactionCharts';
import { EmptyMessage } from '../../shared/EmptyMessage';
import { FilterBar } from '../../shared/FilterBar';
import { TransactionDistribution } from './Distribution';
import { Transaction } from './Transaction';
import { useLocation } from '../../../hooks/useLocation';
import { useUrlParams } from '../../../hooks/useUrlParams';
interface Props {
urlParams: IUrlParams;
location: Location;
}
export function TransactionDetailsView({ urlParams, location }: Props) {
export function TransactionDetails() {
const location = useLocation();
const { urlParams } = useUrlParams();
const { data: distributionData } = useTransactionDistribution(urlParams);
const { data: transactionDetailsChartsData } = useTransactionDetailsCharts(
urlParams

View file

@ -6,13 +6,10 @@
import createHistory from 'history/createHashHistory';
import React from 'react';
import { Provider } from 'react-redux';
import { Router } from 'react-router-dom';
import { queryByLabelText, render } from 'react-testing-library';
import { TransactionOverview } from '..';
// @ts-ignore
import configureStore from '../../../../store/config/configureStore';
import { IUrlParams } from '../../../../store/urlParams';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
// Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769
/* eslint-disable no-console */
@ -28,16 +25,13 @@ function setup(props: {
urlParams: IUrlParams;
serviceTransactionTypes: string[];
}) {
const store = configureStore();
const history = createHistory();
history.replace = jest.fn();
const { container } = render(
<Provider store={store}>
<Router history={history}>
<TransactionOverview {...props} />
</Router>
</Provider>
<Router history={history}>
<TransactionOverview {...props} />
</Router>
);
return { container, history };

View file

@ -18,7 +18,7 @@ import React from 'react';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { useTransactionList } from '../../../hooks/useTransactionList';
import { useTransactionOverviewCharts } from '../../../hooks/useTransactionOverviewCharts';
import { IUrlParams } from '../../../store/urlParams';
import { IUrlParams } from '../../../context/UrlParamsContext/types';
import { TransactionCharts } from '../../shared/charts/TransactionCharts';
import { legacyEncodeURIComponent } from '../../shared/Links/url_helpers';
import { TransactionList } from './List';

View file

@ -1,37 +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.
*/
// Initially inspired from react-router's ConnectedRouter
// https://github.com/ReactTraining/react-router/blob/e6f9017c947b3ae49affa24cc320d0a86f765b55/packages/react-router-redux/modules/ConnectedRouter.js
// Instead of adding a listener to `history` we passively receive props from react-router
// This ensures that we don't have two history listeners (one here, and one for react-router) which can cause "race-condition" type issues
// since history.listen is sync and can result in cascading updates
import { Component } from 'react';
import PropTypes from 'prop-types';
class ConnectRouterToRedux extends Component {
static propTypes = {
location: PropTypes.object.isRequired
};
componentDidMount() {
this.props.updateLocation(this.props.location);
}
componentDidUpdate() {
// this component is wrapped in a react-router Route to get access
// to the location prop, so no need to check for prop change here
this.props.updateLocation(this.props.location);
}
render() {
return null;
}
}
export default ConnectRouterToRedux;

View file

@ -4,87 +4,60 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiSuperDatePicker, EuiSuperDatePickerProps } from '@elastic/eui';
import { EuiSuperDatePicker } from '@elastic/eui';
import React from 'react';
import { connect } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { IReduxState } from '../../../store/rootReducer';
import {
getUrlParams,
IUrlParams,
refreshTimeRange
} from '../../../store/urlParams';
import { fromQuery, toQuery } from '../Links/url_helpers';
import { history } from '../../../utils/history';
import { useLocation } from '../../../hooks/useLocation';
import { useUrlParams } from '../../../hooks/useUrlParams';
interface DatePickerProps extends RouteComponentProps {
dispatchRefreshTimeRange: typeof refreshTimeRange;
urlParams: IUrlParams;
}
export function DatePicker() {
const location = useLocation();
const { urlParams, refreshTimeRange } = useUrlParams();
export class DatePickerComponent extends React.Component<DatePickerProps> {
public updateUrl(nextQuery: {
function updateUrl(nextQuery: {
rangeFrom?: string;
rangeTo?: string;
refreshPaused?: boolean;
refreshInterval?: number;
}) {
const { history, location } = this.props;
history.push({
...location,
search: fromQuery({ ...toQuery(location.search), ...nextQuery })
search: fromQuery({
...toQuery(location.search),
...nextQuery
})
});
}
public onRefreshChange: EuiSuperDatePickerProps['onRefreshChange'] = ({
function onRefreshChange({
isPaused,
refreshInterval
}) => {
this.updateUrl({ refreshPaused: isPaused, refreshInterval });
};
public onTimeChange: EuiSuperDatePickerProps['onTimeChange'] = ({
start,
end
}) => {
this.updateUrl({ rangeFrom: start, rangeTo: end });
};
public onRefresh: EuiSuperDatePickerProps['onRefresh'] = ({ start, end }) => {
this.props.dispatchRefreshTimeRange({ rangeFrom: start, rangeTo: end });
};
public render() {
const {
rangeFrom,
rangeTo,
refreshPaused,
refreshInterval
} = this.props.urlParams;
return (
<EuiSuperDatePicker
start={rangeFrom}
end={rangeTo}
isPaused={refreshPaused}
refreshInterval={refreshInterval}
onTimeChange={this.onTimeChange}
onRefresh={this.onRefresh}
onRefreshChange={this.onRefreshChange}
showUpdateButton={true}
/>
);
}: {
isPaused: boolean;
refreshInterval: number;
}) {
updateUrl({ refreshPaused: isPaused, refreshInterval });
}
function onTimeChange({ start, end }: { start: string; end: string }) {
updateUrl({ rangeFrom: start, rangeTo: end });
}
const { rangeFrom, rangeTo, refreshPaused, refreshInterval } = urlParams;
return (
<EuiSuperDatePicker
start={rangeFrom}
end={rangeTo}
isPaused={refreshPaused}
refreshInterval={refreshInterval}
onTimeChange={onTimeChange}
onRefresh={({ start, end }) => {
refreshTimeRange({ rangeFrom: start, rangeTo: end });
}}
onRefreshChange={onRefreshChange}
showUpdateButton={true}
/>
);
}
const mapStateToProps = (state: IReduxState) => ({
urlParams: getUrlParams(state)
});
const mapDispatchToProps = { dispatchRefreshTimeRange: refreshTimeRange };
const DatePicker = withRouter(
connect(
mapStateToProps,
mapDispatchToProps
)(DatePickerComponent)
);
export { DatePicker };

View file

@ -4,158 +4,87 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { mount, shallow } from 'enzyme';
import React from 'react';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { Store } from 'redux';
// @ts-ignore
import configureStore from '../../../../store/config/configureStore';
import { mockNow, tick } from '../../../../utils/testHelpers';
import { DatePicker, DatePickerComponent } from '../DatePicker';
import { LocationProvider } from '../../../../context/LocationContext';
import { UrlParamsContext } from '../../../../context/UrlParamsContext';
import { tick } from '../../../../utils/testHelpers';
import { DatePicker } from '../DatePicker';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
import { history } from '../../../../utils/history';
import { mount } from 'enzyme';
import { EuiSuperDatePicker } from '@elastic/eui';
function mountPicker(initialState = {}) {
const store = configureStore(initialState);
const wrapper = mount(
<Provider store={store}>
<MemoryRouter>
const mockHistoryPush = jest.spyOn(history, 'push');
const mockRefreshTimeRange = jest.fn();
const MockUrlParamsProvider: React.FC<{
params?: IUrlParams;
}> = ({ params = {}, children }) => (
<UrlParamsContext.Provider
value={{ urlParams: params, refreshTimeRange: mockRefreshTimeRange }}
children={children}
/>
);
function mountDatePicker(params?: IUrlParams) {
return mount(
<LocationProvider history={history}>
<MockUrlParamsProvider params={params}>
<DatePicker />
</MemoryRouter>
</Provider>
</MockUrlParamsProvider>
</LocationProvider>
);
return { wrapper, store };
}
describe('DatePicker', () => {
describe('url updates', () => {
function setupTest() {
const routerProps = {
location: { search: '' },
history: { push: jest.fn() }
} as any;
const wrapper = shallow<DatePickerComponent>(
<DatePickerComponent
{...routerProps}
dispatchUpdateTimePicker={jest.fn()}
urlParams={{}}
/>
);
return { history: routerProps.history, wrapper };
}
it('should push an entry to the stack for each change', () => {
const { history, wrapper } = setupTest();
wrapper.instance().updateUrl({ rangeFrom: 'now-20m', rangeTo: 'now' });
expect(history.push).toHaveBeenCalledWith({
search: 'rangeFrom=now-20m&rangeTo=now'
});
});
beforeAll(() => {
jest.spyOn(console, 'error').mockImplementation(() => null);
});
describe('refresh cycle', () => {
let nowSpy: jest.Mock;
beforeEach(() => {
nowSpy = mockNow('2010');
jest.useFakeTimers();
});
afterAll(() => {
jest.restoreAllMocks();
});
afterEach(() => {
nowSpy.mockRestore();
jest.useRealTimers();
});
beforeEach(() => {
jest.clearAllMocks();
});
describe('when refresh is not paused', () => {
let listener: jest.Mock;
let store: Store;
beforeEach(async () => {
const obj = mountPicker({
urlParams: {
rangeFrom: 'now-15m',
rangeTo: 'now',
refreshPaused: false,
refreshInterval: 200
}
});
store = obj.store;
listener = jest.fn();
store.subscribe(listener);
jest.advanceTimersByTime(200);
await tick();
jest.advanceTimersByTime(200);
await tick();
jest.advanceTimersByTime(200);
await tick();
it('should update the URL when the date range changes', () => {
const datePicker = mountDatePicker();
datePicker
.find(EuiSuperDatePicker)
.props()
.onTimeChange({
start: 'updated-start',
end: 'updated-end',
isInvalid: false,
isQuickSelection: true
});
expect(mockHistoryPush).toHaveBeenCalledWith(
expect.objectContaining({
search: 'rangeFrom=updated-start&rangeTo=updated-end'
})
);
});
it('should dispatch every refresh interval', async () => {
expect(listener).toHaveBeenCalledTimes(3);
});
it('should update the store with the new date range', () => {
expect(store.getState().urlParams).toEqual({
end: '2010-01-01T00:00:00.000Z',
rangeFrom: 'now-15m',
rangeTo: 'now',
refreshInterval: 200,
refreshPaused: false,
start: '2009-12-31T23:45:00.000Z'
});
});
it('should auto-refresh when refreshPaused is false', async () => {
jest.useFakeTimers();
const wrapper = mountDatePicker({
refreshPaused: false,
refreshInterval: 1000
});
expect(mockRefreshTimeRange).not.toHaveBeenCalled();
jest.advanceTimersByTime(1000);
await tick();
expect(mockRefreshTimeRange).toHaveBeenCalled();
wrapper.unmount();
});
it('should not refresh when paused', () => {
const { store } = mountPicker({
urlParams: {
rangeFrom: 'now-15m',
rangeTo: 'now',
refreshPaused: true,
refreshInterval: 200
}
});
const listener = jest.fn();
store.subscribe(listener);
jest.advanceTimersByTime(1100);
expect(listener).not.toHaveBeenCalled();
});
it('should be paused by default', () => {
const { store } = mountPicker({
urlParams: {
rangeFrom: 'now-15m',
rangeTo: 'now',
refreshInterval: 200
}
});
const listener = jest.fn();
store.subscribe(listener);
jest.advanceTimersByTime(1100);
expect(listener).not.toHaveBeenCalled();
});
it('should not attempt refreshes after unmounting', () => {
const { store, wrapper } = mountPicker({
urlParams: {
rangeFrom: 'now-15m',
rangeTo: 'now',
refreshPaused: false,
refreshInterval: 200
}
});
const listener = jest.fn();
store.subscribe(listener);
wrapper.unmount();
jest.advanceTimersByTime(1100);
expect(listener).not.toHaveBeenCalled();
});
it('should NOT auto-refresh when refreshPaused is true', async () => {
jest.useFakeTimers();
mountDatePicker({ refreshPaused: true, refreshInterval: 1000 });
expect(mockRefreshTimeRange).not.toHaveBeenCalled();
jest.advanceTimersByTime(1000);
await tick();
expect(mockRefreshTimeRange).not.toHaveBeenCalled();
});
});

View file

@ -5,15 +5,11 @@
*/
import { EuiTab } from '@elastic/eui';
import { mount, ReactWrapper, shallow, ShallowWrapper } from 'enzyme';
import { shallow, ShallowWrapper } from 'enzyme';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import {
HistoryTabs,
HistoryTabsProps,
HistoryTabsWithoutRouter,
IHistoryTab
} from '..';
import { HistoryTabs, HistoryTabsProps, IHistoryTab } from '..';
import * as hooks from '../../../../hooks/useLocation';
import { history } from '../../../../utils/history';
type PropsOf<Component> = Component extends React.SFC<infer Props>
? Props
@ -22,7 +18,6 @@ type EuiTabProps = PropsOf<typeof EuiTab>;
describe('HistoryTabs', () => {
let mockLocation: any;
let mockHistory: any;
let testTabs: IHistoryTab[];
let testProps: HistoryTabsProps;
@ -30,9 +25,8 @@ describe('HistoryTabs', () => {
mockLocation = {
pathname: ''
};
mockHistory = {
push: jest.fn()
};
jest.spyOn(hooks, 'useLocation').mockImplementation(() => mockLocation);
const Content = (props: { name: string }) => <div>{props.name}</div>;
@ -55,15 +49,17 @@ describe('HistoryTabs', () => {
];
testProps = {
location: mockLocation,
history: mockHistory,
tabs: testTabs
} as HistoryTabsProps;
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should render correctly', () => {
mockLocation.pathname = '/two';
const wrapper = shallow(<HistoryTabsWithoutRouter {...testProps} />);
const wrapper = shallow(<HistoryTabs {...testProps} />);
expect(wrapper).toMatchSnapshot();
const tabs: ShallowWrapper<EuiTabProps> = wrapper.find(EuiTab);
@ -72,25 +68,15 @@ describe('HistoryTabs', () => {
expect(tabs.at(2).props().isSelected).toEqual(false);
});
it('should change the selected item on tab click', () => {
const wrapper = mount(
<MemoryRouter initialEntries={['/two']}>
<HistoryTabs tabs={testTabs} />
</MemoryRouter>
);
expect(wrapper.find('Content')).toMatchSnapshot();
it('should push a new state onto history on tab click', () => {
const pushSpy = jest.spyOn(history, 'push');
const wrapper = shallow(<HistoryTabs tabs={testTabs} />);
wrapper
.find(EuiTab)
.at(2)
.simulate('click');
const tabs: ReactWrapper<EuiTabProps> = wrapper.find(EuiTab);
expect(tabs.at(0).props().isSelected).toEqual(false);
expect(tabs.at(1).props().isSelected).toEqual(false);
expect(tabs.at(2).props().isSelected).toEqual(true);
expect(wrapper.find('Content')).toMatchSnapshot();
expect(pushSpy).toHaveBeenCalledWith({ pathname: '/three' });
});
});

View file

@ -1,25 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HistoryTabs should change the selected item on tab click 1`] = `
<Content
name="two"
>
<div>
two
</div>
</Content>
`;
exports[`HistoryTabs should change the selected item on tab click 2`] = `
<Content
name="three"
>
<div>
three
</div>
</Content>
`;
exports[`HistoryTabs should render correctly 1`] = `
<Fragment>
<EuiTabs

View file

@ -6,12 +6,9 @@
import { EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui';
import React from 'react';
import {
matchPath,
Route,
RouteComponentProps,
withRouter
} from 'react-router-dom';
import { matchPath, Route, RouteComponentProps } from 'react-router-dom';
import { useLocation } from '../../../hooks/useLocation';
import { history } from '../../../utils/history';
export interface IHistoryTab {
path: string;
@ -20,7 +17,7 @@ export interface IHistoryTab {
render?: (props: RouteComponentProps) => React.ReactNode;
}
export interface HistoryTabsProps extends RouteComponentProps {
export interface HistoryTabsProps {
tabs: IHistoryTab[];
}
@ -31,11 +28,8 @@ function isTabSelected(tab: IHistoryTab, currentPath: string) {
return currentPath === tab.path;
}
const HistoryTabsWithoutRouter = ({
tabs,
history,
location
}: HistoryTabsProps) => {
export function HistoryTabs({ tabs }: HistoryTabsProps) {
const location = useLocation();
return (
<React.Fragment>
<EuiTabs>
@ -61,8 +55,4 @@ const HistoryTabsWithoutRouter = ({
)}
</React.Fragment>
);
};
const HistoryTabs = withRouter(HistoryTabsWithoutRouter);
export { HistoryTabsWithoutRouter, HistoryTabs };
}

View file

@ -1,18 +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 { connect } from 'react-redux';
import { getUrlParams } from '../../../store/urlParams';
import view from './view';
function mapStateToProps(state = {}) {
return {
location: state.location,
urlParams: getUrlParams(state)
};
}
export const KueryBar = connect(mapStateToProps)(view);

View file

@ -0,0 +1,181 @@
/*
* 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, { useState, useEffect } from 'react';
import { uniqueId, startsWith } from 'lodash';
import { EuiCallOut } from '@elastic/eui';
import chrome from 'ui/chrome';
import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { AutocompleteSuggestion } from 'ui/autocomplete_providers';
import { StaticIndexPattern } from 'ui/index_patterns';
import {
fromQuery,
toQuery,
legacyEncodeURIComponent
} from '../Links/url_helpers';
import { KibanaLink } from '../Links/KibanaLink';
// @ts-ignore
import { Typeahead } from './Typeahead';
import {
convertKueryToEsQuery,
getSuggestions,
getAPMIndexPatternForKuery
} from '../../../services/kuery';
// @ts-ignore
import { getBoolFilter } from './get_bool_filter';
import { useLocation } from '../../../hooks/useLocation';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { history } from '../../../utils/history';
const Container = styled.div`
margin-bottom: 10px;
`;
interface State {
indexPattern: StaticIndexPattern | null;
suggestions: AutocompleteSuggestion[];
isLoadingIndexPattern: boolean;
isLoadingSuggestions: boolean;
}
export function KueryBar() {
const [state, setState] = useState<State>({
indexPattern: null,
suggestions: [],
isLoadingIndexPattern: true,
isLoadingSuggestions: false
});
const { urlParams } = useUrlParams();
const location = useLocation();
const apmIndexPatternTitle = chrome.getInjected('apmIndexPatternTitle');
const indexPatternMissing =
!state.isLoadingIndexPattern && !state.indexPattern;
let currentRequestCheck;
useEffect(() => {
let didCancel = false;
async function loadIndexPattern() {
setState({ ...state, isLoadingIndexPattern: true });
const indexPattern = await getAPMIndexPatternForKuery();
if (didCancel) {
return;
}
if (!indexPattern) {
setState({ ...state, isLoadingIndexPattern: false });
} else {
setState({ ...state, indexPattern, isLoadingIndexPattern: false });
}
}
loadIndexPattern();
return () => {
didCancel = true;
};
}, []);
async function onChange(inputValue: string, selectionStart: number) {
const { indexPattern } = state;
if (indexPattern === null) {
return;
}
setState({ ...state, suggestions: [], isLoadingSuggestions: true });
const currentRequest = uniqueId();
currentRequestCheck = currentRequest;
const boolFilter = getBoolFilter(urlParams);
try {
const suggestions = (await getSuggestions(
inputValue,
selectionStart,
indexPattern,
boolFilter
))
.filter(suggestion => !startsWith(suggestion.text, 'span.'))
.slice(0, 15);
if (currentRequest !== currentRequestCheck) {
return;
}
setState({ ...state, suggestions, isLoadingSuggestions: false });
} catch (e) {
// eslint-disable-next-line no-console
console.error('Error while fetching suggestions', e);
}
}
function onSubmit(inputValue: string) {
const { indexPattern } = state;
if (indexPattern === null) {
return;
}
try {
const res = convertKueryToEsQuery(inputValue, indexPattern);
if (!res) {
return;
}
history.replace({
...location,
search: fromQuery({
...toQuery(location.search),
kuery: legacyEncodeURIComponent(inputValue.trim())
})
});
} catch (e) {
console.log('Invalid kuery syntax'); // eslint-disable-line no-console
}
}
return (
<Container>
<Typeahead
disabled={indexPatternMissing}
isLoading={state.isLoadingSuggestions}
initialValue={urlParams.kuery}
onChange={onChange}
onSubmit={onSubmit}
suggestions={state.suggestions}
/>
{indexPatternMissing && (
<EuiCallOut
style={{ display: 'inline-block', marginTop: '10px' }}
title={
<div>
<FormattedMessage
id="xpack.apm.kueryBar.indexPatternMissingWarningMessage"
defaultMessage="There's no APM index pattern with the title {apmIndexPatternTitle} available. To use the Query bar, please choose to import the APM index pattern via the {setupInstructionsLink}."
values={{
apmIndexPatternTitle: `"${apmIndexPatternTitle}"`,
setupInstructionsLink: (
<KibanaLink path={`/home/tutorial/apm`}>
{i18n.translate(
'xpack.apm.kueryBar.setupInstructionsLinkLabel',
{ defaultMessage: 'Setup Instructions' }
)}
</KibanaLink>
)
}}
/>
</div>
}
color="warning"
iconType="alert"
size="s"
/>
)}
</Container>
);
}

View file

@ -1,158 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { uniqueId, startsWith } from 'lodash';
import { EuiCallOut } from '@elastic/eui';
import {
history,
fromQuery,
toQuery,
legacyEncodeURIComponent
} from '../Links/url_helpers';
import { KibanaLink } from '../Links/KibanaLink';
import { Typeahead } from './Typeahead';
import chrome from 'ui/chrome';
import {
convertKueryToEsQuery,
getSuggestions,
getAPMIndexPatternForKuery
} from '../../../services/kuery';
import styled from 'styled-components';
import { getBoolFilter } from './get_bool_filter';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
const Container = styled.div`
margin-bottom: 10px;
`;
class KueryBarView extends Component {
state = {
indexPattern: null,
suggestions: [],
isLoadingIndexPattern: true,
isLoadingSuggestions: false
};
willUnmount = false;
componentWillUnmount() {
this.willUnmount = true;
}
async componentDidMount() {
const indexPattern = await getAPMIndexPatternForKuery();
if (!this.willUnmount) {
this.setState({ indexPattern, isLoadingIndexPattern: false });
}
}
onChange = async (inputValue, selectionStart) => {
const { indexPattern } = this.state;
const { urlParams } = this.props;
this.setState({ suggestions: [], isLoadingSuggestions: true });
const currentRequest = uniqueId();
this.currentRequest = currentRequest;
const boolFilter = getBoolFilter(urlParams);
try {
const suggestions = (await getSuggestions(
inputValue,
selectionStart,
indexPattern,
boolFilter
))
.filter(suggestion => !startsWith(suggestion.text, 'span.'))
.slice(0, 15);
if (currentRequest !== this.currentRequest) {
return;
}
this.setState({ suggestions, isLoadingSuggestions: false });
} catch (e) {
console.error('Error while fetching suggestions', e);
}
};
onSubmit = inputValue => {
const { indexPattern } = this.state;
const { location } = this.props;
try {
const res = convertKueryToEsQuery(inputValue, indexPattern);
if (!res) {
return;
}
history.replace({
...location,
search: fromQuery({
...toQuery(this.props.location.search),
kuery: legacyEncodeURIComponent(inputValue.trim())
})
});
} catch (e) {
console.log('Invalid kuery syntax'); // eslint-disable-line no-console
}
};
render() {
const apmIndexPatternTitle = chrome.getInjected('apmIndexPatternTitle');
const indexPatternMissing =
!this.state.isLoadingIndexPattern && !this.state.indexPattern;
return (
<Container>
<Typeahead
disabled={indexPatternMissing}
isLoading={this.state.isLoadingSuggestions}
initialValue={this.props.urlParams.kuery}
onChange={this.onChange}
onSubmit={this.onSubmit}
suggestions={this.state.suggestions}
/>
{indexPatternMissing && (
<EuiCallOut
style={{ display: 'inline-block', marginTop: '10px' }}
title={
<div>
<FormattedMessage
id="xpack.apm.kueryBar.indexPatternMissingWarningMessage"
defaultMessage="There's no APM index pattern with the title {apmIndexPatternTitle} available. To use the Query bar, please choose to import the APM index pattern via the {setupInstructionsLink}."
values={{
apmIndexPatternTitle: `"${apmIndexPatternTitle}"`,
setupInstructionsLink: (
<KibanaLink path={`/home/tutorial/apm`}>
{i18n.translate(
'xpack.apm.kueryBar.setupInstructionsLinkLabel',
{ defaultMessage: 'Setup Instructions' }
)}
</KibanaLink>
)
}}
/>
</div>
}
color="warning"
iconType="alert"
size="s"
/>
)}
</Container>
);
}
}
KueryBarView.propTypes = {
location: PropTypes.object.isRequired,
urlParams: PropTypes.object.isRequired
};
export default KueryBarView;

View file

@ -10,7 +10,7 @@ import url from 'url';
import { pick } from 'lodash';
import { useLocation } from '../../../hooks/useLocation';
import { APMQueryParams, toQuery, fromQuery } from './url_helpers';
import { TIMEPICKER_DEFAULTS } from '../../../store/urlParams';
import { TIMEPICKER_DEFAULTS } from '../../../context/UrlParamsContext/constants';
interface Props extends EuiLinkAnchorProps {
path?: string;

View file

@ -5,7 +5,7 @@
*/
import { Location } from 'history';
import { TIMEPICKER_DEFAULTS } from '../../../store/urlParams';
import { TIMEPICKER_DEFAULTS } from '../../../context/UrlParamsContext/constants';
import { toQuery } from './url_helpers';
export interface TimepickerRisonData {

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import createHistory from 'history/createHashHistory';
import qs from 'querystring';
import { StringMap } from '../../../../typings/common';
@ -52,9 +51,3 @@ export function legacyEncodeURIComponent(rawUrl?: string) {
export function legacyDecodeURIComponent(encodedUrl?: string) {
return encodedUrl && decodeURIComponent(encodedUrl.replace(/~/g, '%'));
}
// Make history singleton available across APM project.
// This is not great. Other options are to use context or withRouter helper
// React Context API is unstable and will change soon-ish (probably 16.3)
// withRouter helper from react-router overrides several props (eg. `location`) which makes it less desireable
export const history = createHistory();

View file

@ -16,7 +16,7 @@ import InteractivePlot from '../InteractivePlot';
import {
getResponseTimeSeries,
getEmptySerie
} from '../../../../../store/selectors/chartSelectors';
} from '../../../../../selectors/chartSelectors';
function getXValueByIndex(index) {
return responseWithData.responseTimes.avg[index].x;

View file

@ -19,8 +19,8 @@ import React, { Component } from 'react';
import { isEmpty } from 'lodash';
import styled from 'styled-components';
import { Coordinate } from '../../../../../typings/timeseries';
import { ITransactionChartData } from '../../../../store/selectors/chartSelectors';
import { IUrlParams } from '../../../../store/urlParams';
import { ITransactionChartData } from '../../../../selectors/chartSelectors';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
import { asInteger, asMillis, tpmUnit } from '../../../../utils/formatters';
import { LicenseContext } from '../../../app/Main/LicenseCheck';
import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink';

View file

@ -0,0 +1,110 @@
/*
* 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 * as React from 'react';
import { UrlParamsContext, UrlParamsProvider } from '..';
import { mount } from 'enzyme';
import * as hooks from '../../../hooks/useLocation';
import { Location } from 'history';
import { IUrlParams } from '../types';
function mountParams() {
return mount(
<UrlParamsProvider>
<UrlParamsContext.Consumer>
{({ urlParams }: { urlParams: IUrlParams }) => (
<span id="data">{JSON.stringify(urlParams, null, 2)}</span>
)}
</UrlParamsContext.Consumer>
</UrlParamsProvider>
);
}
function getDataFromOutput(wrapper: ReturnType<typeof mount>) {
return JSON.parse(wrapper.find('#data').text());
}
describe('UrlParamsContext', () => {
let mockLocation: Location;
beforeEach(() => {
mockLocation = { pathname: '/test/pathname' } as Location;
jest.spyOn(hooks, 'useLocation').mockImplementation(() => mockLocation);
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should have default params', () => {
jest
.spyOn(Date, 'now')
.mockImplementation(() => new Date('2000-06-15T12:00:00Z').getTime());
const wrapper = mountParams();
const params = getDataFromOutput(wrapper);
expect(params).toEqual({
start: '2000-06-14T12:00:00.000Z',
end: '2000-06-15T12:00:00.000Z',
page: 0,
rangeFrom: 'now-24h',
rangeTo: 'now',
refreshInterval: 0,
refreshPaused: true
});
});
it('should read values in from location', () => {
mockLocation.search =
'?rangeFrom=2010-03-15T12:00:00Z&rangeTo=2010-04-10T12:00:00Z&transactionId=123abc';
const wrapper = mountParams();
const params = getDataFromOutput(wrapper);
expect(params.start).toEqual('2010-03-15T12:00:00.000Z');
expect(params.end).toEqual('2010-04-10T12:00:00.000Z');
});
it('should update param values if location has changed', () => {
const wrapper = mountParams();
mockLocation = {
pathname: '/test/updated',
search:
'?rangeFrom=2009-03-15T12:00:00Z&rangeTo=2009-04-10T12:00:00Z&transactionId=UPDATED'
} as Location;
// force an update
wrapper.setProps({ abc: 123 });
const params = getDataFromOutput(wrapper);
expect(params.start).toEqual('2009-03-15T12:00:00.000Z');
expect(params.end).toEqual('2009-04-10T12:00:00.000Z');
});
it('should refresh the time range with new values', () => {
const wrapper = mount(
<UrlParamsProvider>
<UrlParamsContext.Consumer>
{({ urlParams, refreshTimeRange }) => {
return (
<React.Fragment>
<span id="data">{JSON.stringify(urlParams, null, 2)}</span>
<button
onClick={() =>
refreshTimeRange({
rangeFrom: '2005-09-20T12:00:00Z',
rangeTo: '2005-10-21T12:00:00Z'
})
}
/>
</React.Fragment>
);
}}
</UrlParamsContext.Consumer>
</UrlParamsProvider>
);
wrapper.find('button').simulate('click');
const data = getDataFromOutput(wrapper);
expect(data.start).toEqual('2005-09-20T12:00:00.000Z');
expect(data.end).toEqual('2005-10-21T12:00:00.000Z');
});
});

Some files were not shown because too many files have changed in this diff Show more