Merge
|
@ -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
|
@ -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
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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[]
|
||||
|
|
|
@ -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].
|
||||
|
|
|
@ -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 don’t 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.
|
||||
|
|
|
@ -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[]
|
||||
|
|
|
@ -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"]
|
BIN
docs/dev-tools/console/images/console-settings.png
Normal file
After Width: | Height: | Size: 116 KiB |
BIN
docs/dev-tools/console/images/console.png
Normal file
After Width: | Height: | Size: 200 KiB |
BIN
docs/dev-tools/console/images/copy-curl.png
Normal file
After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 38 KiB |
BIN
docs/dev-tools/console/images/request.png
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
docs/dev-tools/console/images/wrench.png
Normal file
After Width: | Height: | Size: 729 B |
|
@ -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.
|
||||
|
|
|
@ -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.
|
|
@ -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"]
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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' }];
|
||||
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(''))}
|
69
packages/kbn-es-query/src/kuery/errors/index.js
Normal 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;
|
||||
}
|
||||
}
|
105
packages/kbn-es-query/src/kuery/errors/index.test.js
Normal 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' +
|
||||
'^');
|
||||
});
|
||||
|
||||
});
|
|
@ -20,3 +20,4 @@
|
|||
export * from './ast';
|
||||
export * from './filter_migration';
|
||||
export * from './node_types';
|
||||
export * from './errors';
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
@import 'src/legacy/ui/public/styles/styling_constants';
|
|
@ -12,4 +12,4 @@
|
|||
|
||||
@import './app';
|
||||
@import './directives/index';
|
||||
@import './vis/index'
|
||||
@import './vis/index';
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}!`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}`;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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']
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
|
@ -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(
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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 />
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ exports[`Home component should render 1`] = `
|
|||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
<FilterBar />
|
||||
<withRouter(HistoryTabsWithoutRouter)
|
||||
<HistoryTabs
|
||||
tabs={
|
||||
Array [
|
||||
Object {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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', () => {
|
||||
|
|
|
@ -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);
|
|
@ -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 }),
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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'];
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
);
|
|
@ -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
|
|
@ -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 };
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
|
@ -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 };
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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' });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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);
|
181
x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|