Merge remote-tracking branch 'upstream/master' into feature-file-upload-plugin
# Conflicts: # x-pack/test/ui_capabilities/common/saved_objects_management_builder.ts
|
@ -56,6 +56,7 @@ module.exports = {
|
|||
'x-pack/plugins/apm/**/*',
|
||||
'x-pack/plugins/canvas/**/*',
|
||||
'**/*.{ts,tsx}',
|
||||
'src/legacy/core_plugins/metrics/**/*.js',
|
||||
],
|
||||
plugins: ['prettier'],
|
||||
rules: Object.assign(
|
||||
|
@ -456,7 +457,7 @@ module.exports = {
|
|||
{
|
||||
// typescript and javascript for front and back end
|
||||
files: ['x-pack/plugins/siem/**/*.{js,ts,tsx}'],
|
||||
plugins: ['react'],
|
||||
plugins: ['eslint-plugin-node', 'react'],
|
||||
rules: {
|
||||
'accessor-pairs': 'error',
|
||||
'array-callback-return': 'error',
|
||||
|
@ -479,6 +480,7 @@ module.exports = {
|
|||
},
|
||||
],
|
||||
*/
|
||||
'node/no-deprecated-api': 'error',
|
||||
'no-bitwise': 'error',
|
||||
'no-continue': 'error',
|
||||
'no-dupe-keys': 'error',
|
||||
|
|
|
@ -177,9 +177,7 @@ yarn kbn bootstrap
|
|||
|
||||
There are a few options when it comes to running Elasticsearch:
|
||||
|
||||
First, you'll need to have a `java` binary in `PATH` and `JAVA_HOME` set. The version of Java required is specified in [.ci/java-version.properties](https://github.com/elastic/elasticsearch/blob/master/.ci/java-versions.properties) on the ES branch.
|
||||
|
||||
**Nightly snapshot**
|
||||
#### Nightly snapshot (recommended)
|
||||
|
||||
These snapshots are built on a nightly basis which expire after a couple weeks. If running from an old, untracted branch this snapshot might not exist. In which case you might need to run from source or an archive.
|
||||
|
||||
|
@ -187,7 +185,7 @@ These snapshots are built on a nightly basis which expire after a couple weeks.
|
|||
yarn es snapshot
|
||||
```
|
||||
|
||||
**Source**
|
||||
#### Source
|
||||
|
||||
By default, it will reference an [elasticsearch](https://github.com/elastic/elasticsearch) checkout which is a sibling to the Kibana directory named `elasticsearch`. If you wish to use a checkout in another location you can provide that by supplying `--source-path`
|
||||
|
||||
|
@ -195,7 +193,7 @@ By default, it will reference an [elasticsearch](https://github.com/elastic/elas
|
|||
yarn es source
|
||||
```
|
||||
|
||||
**Archive**
|
||||
#### Archive
|
||||
|
||||
Use this if you already have a distributable. For released versions, one can be obtained on the [Elasticsearch downloads](https://www.elastic.co/downloads/elasticsearch) page.
|
||||
|
||||
|
@ -203,18 +201,21 @@ Use this if you already have a distributable. For released versions, one can be
|
|||
yarn es archive <full_path_to_archive>
|
||||
```
|
||||
|
||||
**Each of these will run Elasticsearch with a `basic` license. Additional options are available, pass `--help` for more information.**
|
||||
|
||||
Each of these will run Elasticsearch with a `basic` license. Additional options are available, pass `--help` for more information.
|
||||
##### Sample Data
|
||||
|
||||
|
||||
If you're just getting started with `elasticsearch`, you could use the following command to populate your instance with a few fake logs to hit the ground running.
|
||||
If you're just getting started with Elasticsearch, you could use the following command to populate your instance with a few fake logs to hit the ground running.
|
||||
|
||||
```bash
|
||||
node scripts/makelogs
|
||||
node scripts/makelogs --auth <username>:<password>
|
||||
```
|
||||
> The default username and password combination are `elastic:changeme`
|
||||
|
||||
> Make sure to execute `node scripts/makelogs` *after* elasticsearch is up and running!
|
||||
|
||||
### Running Kibana
|
||||
|
||||
Start the development server.
|
||||
|
||||
```bash
|
||||
|
@ -225,6 +226,8 @@ yarn start
|
|||
|
||||
Now you can point your web browser to http://localhost:5601 and start using Kibana! When running `yarn start`, Kibana will also log that it is listening on port 5603 due to the base path proxy, but you should still access Kibana on port 5601.
|
||||
|
||||
By default, you can log in with username `elastic` and password `changeme`. See the `--help` options on `yarn es <command>` if you'd like to configure a different password.
|
||||
|
||||
#### Running Kibana in Open-Source mode
|
||||
|
||||
If you're looking to only work with the open-source software, supply the license type to `yarn es`:
|
||||
|
@ -306,7 +309,7 @@ ReactDOM.render(
|
|||
);
|
||||
```
|
||||
|
||||
There is a number of tools was created to support internationalization in Kibana that would allow one to validate internationalized labels,
|
||||
There are a number of tools created to support internationalization in Kibana that would allow one to validate internationalized labels,
|
||||
extract them to a `JSON` file or integrate translations back to Kibana. To know more, please read corresponding [readme](src/dev/i18n/README.md) file.
|
||||
|
||||
### Testing and Building
|
||||
|
|
20
docs/code/code-basic-nav.asciidoc
Normal file
|
@ -0,0 +1,20 @@
|
|||
[[code-basic-nav]]
|
||||
== Basic navigation
|
||||
|
||||
[float]
|
||||
==== View file structure and information
|
||||
The *File* tree on the left is the major way for you to navigate through your folder structure. When you are on a directory, *Code* also presents a finder-like view on the right. Additionally, a file path breadcrumb makes it easy to go back to any parent directory.
|
||||
|
||||
[float]
|
||||
==== Git History and Blame
|
||||
The *Directory* view shows the most recent commits. Clicking *View More* or *History* shows the complete commit history of the current folder or file.
|
||||
|
||||
[role="screenshot"]
|
||||
image::images/code-history.png[]
|
||||
|
||||
Clicking *Blame* shows the most recent commit per line.
|
||||
|
||||
[role="screenshot"]
|
||||
image::images/code-blame.png[]
|
||||
|
||||
include::code-semantic-nav.asciidoc[]
|
37
docs/code/code-getting-started.asciidoc
Normal file
|
@ -0,0 +1,37 @@
|
|||
[[code-getting-started]]
|
||||
== Getting Started with Code
|
||||
|
||||
The easiest way to get started with *Code* is to import a real-world repository into *Code*.
|
||||
|
||||
[float]
|
||||
==== Before you begin
|
||||
You must have a {kib} instance up and running.
|
||||
|
||||
If you are in an environment where you have multiple {kib} instances in a cluster, see <<code-multiple-kibana-instances-config, Config for multiple Kibana instances>>.
|
||||
|
||||
[float]
|
||||
==== Import your first repository
|
||||
. Navigate to the Code app.
|
||||
|
||||
. In *Repository URL*, paste the following GitHub clone URL:
|
||||
+
|
||||
[source,bash]
|
||||
----
|
||||
https://github.com/Microsoft/TypeScript-Node-Starter
|
||||
----
|
||||
|
||||
. Click *Import*.
|
||||
+
|
||||
A list item shows the cloning, then indexing progress of the `TypeScript-Node-Starter` repo.
|
||||
+
|
||||
[role="screenshot"]
|
||||
image::images/code-import-repo.png[]
|
||||
|
||||
. After the indexing is complete, navigate to the repo by clicking its name in the list.
|
||||
+
|
||||
[role="screenshot"]
|
||||
image::images/code-starter-root.png[]
|
||||
+
|
||||
Congratulations! You just imported your first repo into *Code*.
|
||||
|
||||
include::code-repo-management.asciidoc[]
|
21
docs/code/code-install-lang-server.asciidoc
Normal file
|
@ -0,0 +1,21 @@
|
|||
[[code-install-lang-server]]
|
||||
== Install language server
|
||||
|
||||
*Code* comes to with built-in language support to TypeScript. You can install additional languages as a {kib} plugin.
|
||||
|
||||
[role="screenshot"]
|
||||
image::images/code-lang-server-tab.png[]
|
||||
|
||||
The following languages are supported for the current version:
|
||||
|
||||
* Built-in language support: `TypeScript`
|
||||
|
||||
* Additional language support: `Java`
|
||||
|
||||
You can check the status of the language servers and get installation instructions on the *Language Servers* tab. Make sure the status of the language server is `INSTALLED` or `RUNNING` after your restart the {kib} instance.
|
||||
[role="screenshot"]
|
||||
image::images/code-lang-server-status.png[]
|
||||
|
||||
|
||||
|
||||
include::code-basic-nav.asciidoc[]
|
10
docs/code/code-multiple-kibana-instances-config.asciidoc
Normal file
|
@ -0,0 +1,10 @@
|
|||
[[code-multiple-kibana-instances-config]]
|
||||
== Config for multiple {kib} instances
|
||||
If you are using multiple instances of {kib}, you must assign one {kib} instance as a *Code* `node`. Add the following line of code to your `kibana.yml` file for every {kib} instance and restart the instances:
|
||||
|
||||
[source,yaml]
|
||||
----
|
||||
xpack.code.codeNodeUrl: 'http://$YourCodeNodeAddress'
|
||||
----
|
||||
|
||||
`$YourCodeNoteAddress` is the URL of your assigned *Code* node accessible by other {kib} instances.
|
51
docs/code/code-repo-management.asciidoc
Normal file
|
@ -0,0 +1,51 @@
|
|||
[[code-repo-management]]
|
||||
== Repo management
|
||||
|
||||
Code starts with an overview of your repositories. You can then use the UI to add, delete, and reindex a repo.
|
||||
[role="screenshot"]
|
||||
image::images/code-repo-management.png[]
|
||||
|
||||
[float]
|
||||
==== Add and delete a repo
|
||||
The <<code-getting-started, Getting Started>> provides step-by-step instructions for adding a GitHub repo to *Code*. You can fine tune the hostname of the git clone URL in your `kibana.yml` file.
|
||||
|
||||
For security reasons, Code allows only a few trusted hostnames, such as github.com, by default. You can add SSH key to {kib} to clone private repo.
|
||||
|
||||
Deleting a repo removes it from local storage and the Elasticsearch index.
|
||||
|
||||
[float]
|
||||
==== Reindex a repo
|
||||
*Code* automatically reindexes an imported repo, but in some cases you might need to manually refresh the index. For example, you might refresh an index after a new language server is installed. Or, you might want to immediately update the index to the HEAD revision. Click *Reindex* to initiate a reindex.
|
||||
|
||||
[float]
|
||||
==== Clone URL management
|
||||
For security reasons, *Code* only allows the following hostnames in the git clone URL by default:
|
||||
|
||||
[source,yaml]
|
||||
----
|
||||
['github.com', 'gitlab.com', 'bitbucket.org', 'gitbox.apache.org', 'eclipse.org']
|
||||
----
|
||||
|
||||
You can add your own hostname (for example, acme.com) to the whitelist by adding the following line to your `config/kibana.yaml` file:
|
||||
|
||||
[source,yaml]
|
||||
----
|
||||
xpack.code.gitHostWhitelist: [ "github.com", "gitlab.com", "bitbucket.org", "gitbox.apache.org", "eclipse.org", "acme.com" ]
|
||||
----
|
||||
|
||||
Set `xpack.code.gitHostWhitelist` to [] (empty list) allow any hostname.
|
||||
|
||||
You can also control the protocol to use for the clone address. By default, the following protocols are supported: `[ 'https', 'git', 'ssh' ]`. You can change this value by adding the following line to your `config/kibana.yaml` file. In this example, the user only wants to support the `https` protocol:
|
||||
|
||||
[source,yaml]
|
||||
----
|
||||
xpack.code.gitProtocolWhitelist: [ "https" ]
|
||||
----
|
||||
|
||||
[float]
|
||||
==== Clone repo with SSH key
|
||||
If your repo clone requires an SSH key for authentication, put the SSH key in `data/code/credentials/` under the {kib} folder.
|
||||
|
||||
|
||||
|
||||
include::code-install-lang-server.asciidoc[]
|
24
docs/code/code-search.asciidoc
Normal file
|
@ -0,0 +1,24 @@
|
|||
[[code-search]]
|
||||
== Search
|
||||
|
||||
[float]
|
||||
==== Typeahead search
|
||||
The search bar is built to minimize the time for you to locate the result. It shows `Symbols`, `Files`, and `Repos` results as you type, and clicking on any result takes you to the definition. You can use the search type dropdown to show all types of results or limit it to a specific search type.
|
||||
|
||||
[role="screenshot"]
|
||||
image::images/code-quick-search.png[]
|
||||
|
||||
[float]
|
||||
==== Full-text search
|
||||
If the quick search results don’t contain what you are looking for, you can press ‘Enter’ to bring out the full text search.
|
||||
[role="screenshot"]
|
||||
image::images/code-full-text-search.png[]
|
||||
You can further slice and dice the results using the repo and language facet filters on the left.
|
||||
|
||||
[float]
|
||||
==== Search filter
|
||||
You can also use the Search Filters to limit the search scope to certain repos before issuing a query. To search across all repos, remove all repo filters. By default, the search repo is limited to the current repo.
|
||||
[role="screenshot"]
|
||||
image::images/code-search-filter.png[]
|
||||
|
||||
include::code-multiple-kibana-instances-config.asciidoc[]
|
24
docs/code/code-semantic-nav.asciidoc
Normal file
|
@ -0,0 +1,24 @@
|
|||
[[code-semantic-nav]]
|
||||
|
||||
== Semantic code navigation
|
||||
If the file is one of *Code’s* <<code-install-lang-server, supported languages>> and the corresponding language server is <<code-install-lang-server, properly installed>>, you can navigate the files with semantic code navigation features.
|
||||
|
||||
[float]
|
||||
==== Goto definition and find reference
|
||||
Hovering your cursor over a symbol in a file opens information about the symbol, including its qualified name and documentation, when available. You can perform two actions:
|
||||
|
||||
* *Goto Definition* navigates to the symbol definition. If the definition is defined in another repo, *Code* can find the definition if the definition repo is also imported.
|
||||
|
||||
* *Find Reference* opens a panel that lists all the places where the symbol is referenced in the current repo.
|
||||
|
||||
[role="screenshot"]
|
||||
image::images/code-semantic-nav.png[]
|
||||
|
||||
[float]
|
||||
==== View symbol table
|
||||
From the *Structure* tab, you can open a symbol table that details the structure of the current class. Clicking on a member function or variable jumps to its definition.
|
||||
|
||||
[role="screenshot"]
|
||||
image::images/code-symbol-table.png[]
|
||||
|
||||
include::code-search.asciidoc[]
|
19
docs/code/index.asciidoc
Normal file
|
@ -0,0 +1,19 @@
|
|||
[[code-intro]]
|
||||
= Code
|
||||
|
||||
[partintro]
|
||||
--
|
||||
|
||||
beta[] Interaction with source code is pervasive and essential for any technology company. How efficiently you search, navigate, and gain insight to your source code impacts how fast your organization can innovate. *Code* provides an easy-to-use code search solution that scales with your organization, so your team can focus on shipping awesome products and providing the best services. *Code* provides the following functions:
|
||||
|
||||
Code provides:
|
||||
|
||||
* Jump to definition and find references for a symbol
|
||||
* Typeahead search for symbol definition, file, and repo
|
||||
* Symbol table
|
||||
* Full text search with repo and language filters
|
||||
|
||||
<<code-getting-started, Get Started>> with *Code* by importing your first repo.
|
||||
--
|
||||
|
||||
include::code-getting-started.asciidoc[]
|
BIN
docs/images/code-blame.png
Normal file
After Width: | Height: | Size: 611 KiB |
BIN
docs/images/code-full-text-search.png
Normal file
After Width: | Height: | Size: 531 KiB |
BIN
docs/images/code-history.png
Normal file
After Width: | Height: | Size: 628 KiB |
BIN
docs/images/code-import-repo.png
Normal file
After Width: | Height: | Size: 205 KiB |
BIN
docs/images/code-lang-server-status.png
Normal file
After Width: | Height: | Size: 213 KiB |
BIN
docs/images/code-lang-server-tab.png
Normal file
After Width: | Height: | Size: 438 KiB |
BIN
docs/images/code-quick-search.png
Normal file
After Width: | Height: | Size: 901 KiB |
BIN
docs/images/code-repo-management.png
Normal file
After Width: | Height: | Size: 344 KiB |
BIN
docs/images/code-search-filter.png
Normal file
After Width: | Height: | Size: 86 KiB |
BIN
docs/images/code-semantic-nav.png
Normal file
After Width: | Height: | Size: 698 KiB |
BIN
docs/images/code-starter-root.png
Normal file
After Width: | Height: | Size: 382 KiB |
BIN
docs/images/code-symbol-table.png
Normal file
After Width: | Height: | Size: 607 KiB |
|
@ -44,6 +44,8 @@ include::ml/index.asciidoc[]
|
|||
|
||||
include::maps/index.asciidoc[]
|
||||
|
||||
include::code/index.asciidoc[]
|
||||
|
||||
include::infrastructure/index.asciidoc[]
|
||||
|
||||
include::logs/index.asciidoc[]
|
||||
|
|
59
docs/settings/code-settings.asciidoc
Normal file
|
@ -0,0 +1,59 @@
|
|||
[role="xpack"]
|
||||
[[code-settings-kibana]]
|
||||
== Code Settings in Kibana
|
||||
++++
|
||||
<titleabbrev>Code settings</titleabbrev>
|
||||
++++
|
||||
|
||||
Unless you are running multiple Kibana instances as a cluster, you do not need to change any settings to use *Code* by default. If you’d like to change any of the default values, copy and paste the relevant settings below into your `kibana.yml` configuration file.
|
||||
|
||||
`xpack.code.queueIndex`::
|
||||
Internal worker queue index name. Defaults to `.code_internal-worker-queue`.
|
||||
|
||||
`xpack.code.queueTimeoutMs`::
|
||||
Internal worker queue task timeout. Default to `3600000`.
|
||||
|
||||
`xpack.code.updateFrequencyMs`::
|
||||
Update scheduler execution frequency in milliseconds. Defaults to `300000`.
|
||||
|
||||
`xpack.code.indexFrequencyMs`::
|
||||
Index scheduler execution frequency in milliseconds. Defaults to `86400000`.
|
||||
|
||||
`xpack.code.updateRepoFrequencyMs`::
|
||||
Repo update frequency in milliseconds. Defaults to `300000`.
|
||||
|
||||
`xpack.code.indexRepoFrequencyMs`::
|
||||
Repo index frequency in milliseconds. Defaults to `86400000`.
|
||||
|
||||
`xpack.code.lsp.requestTimeoutMs`::
|
||||
Timeout time for a request to language servers in milliseconds. Defaults to `10000`.
|
||||
|
||||
`xpack.code.lsp.detach`::
|
||||
Whether language servers will run in detach mode. Defaults to `false`.
|
||||
|
||||
`xpack.code.lsp.verbose`::
|
||||
Whether to show more verbose log for language servers. Defaults to `false`.
|
||||
|
||||
`xpack.code.security.enableMavenImport`::
|
||||
Whether to support maven. Defaults to `true`.
|
||||
|
||||
`xpack.code.security.enableGradleImport`::
|
||||
Whether to support gradle. Defaults to `false`.
|
||||
|
||||
`xpack.code.security.installNodeDependency`::
|
||||
Whether to enable node dependency download. Defaults to `true`.
|
||||
|
||||
`xpack.code.security.gitHostWhitelist`::
|
||||
Whitelist of hostnames for git clone address. Defaults to `[ 'github.com', 'gitlab.com', 'bitbucket.org', 'gitbox.apache.org', 'eclipse.org',]`.
|
||||
|
||||
`xpack.code.security.gitProtocolWhitelist`::
|
||||
Whitelist of protocols for git clone address. Defaults to `[ 'https', 'git', 'ssh' ]`.
|
||||
|
||||
`xpack.code.security.enableGitCertCheck`::
|
||||
Whether to enable Code to load git key pairs. Defaults to `true`.
|
||||
|
||||
`xpack.code.maxWorkspace`::
|
||||
Maximal number of workspaces each language server allows to span. Defaults to `5`.
|
||||
|
||||
`xpack.code.codeNodeUrl`::
|
||||
URL of the Code node. This config is only needed when multiple Kibana instances are set up as a cluster. Defaults to ``
|
|
@ -308,6 +308,7 @@ Rollup user interface.
|
|||
|
||||
|
||||
include::{docdir}/settings/apm-settings.asciidoc[]
|
||||
include::{docdir}/settings/code-settings.asciidoc[]
|
||||
include::{docdir}/settings/dev-settings.asciidoc[]
|
||||
include::{docdir}/settings/graph-settings.asciidoc[]
|
||||
include::{docdir}/settings/infrastructure-ui-settings.asciidoc[]
|
||||
|
|
|
@ -237,7 +237,7 @@
|
|||
"terser-webpack-plugin": "^1.1.0",
|
||||
"thread-loader": "^2.1.2",
|
||||
"tinygradient": "0.3.0",
|
||||
"tinymath": "1.1.1",
|
||||
"tinymath": "1.2.1",
|
||||
"topojson-client": "3.0.0",
|
||||
"trunc-html": "1.0.2",
|
||||
"trunc-text": "1.0.2",
|
||||
|
@ -365,6 +365,7 @@
|
|||
"eslint-plugin-jest": "22.6.4",
|
||||
"eslint-plugin-jsx-a11y": "6.2.1",
|
||||
"eslint-plugin-mocha": "5.3.0",
|
||||
"eslint-plugin-node": "9.1.0",
|
||||
"eslint-plugin-no-unsanitized": "3.0.2",
|
||||
"eslint-plugin-prefer-object-spread": "1.2.1",
|
||||
"eslint-plugin-prettier": "3.1.0",
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
"glob": "^7.1.2",
|
||||
"rxjs": "^6.2.1",
|
||||
"tar-fs": "^1.16.2",
|
||||
"tmp": "^0.0.33",
|
||||
"tmp": "^0.1.0",
|
||||
"zlib": "^1.0.5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,11 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
const { resolve } = require('path');
|
||||
|
||||
// force cwd
|
||||
process.chdir(resolve(__dirname, '../../..'));
|
||||
|
||||
if (!process.env.JOB_NAME) {
|
||||
console.log('Unable to determine job name');
|
||||
process.exit(1);
|
||||
|
|
|
@ -17,19 +17,21 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export const error = () => ({
|
||||
name: 'error',
|
||||
to: {
|
||||
render: input => {
|
||||
const { error, info } = input;
|
||||
return {
|
||||
type: 'render',
|
||||
as: 'error',
|
||||
value: {
|
||||
error,
|
||||
info,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
export {
|
||||
Datatable,
|
||||
DatatableColumn,
|
||||
DatatableRow,
|
||||
DatatableColumnType,
|
||||
ExpressionImage,
|
||||
Filter,
|
||||
InterpreterErrorType,
|
||||
isDatatable,
|
||||
KibanaContext,
|
||||
KibanaDatatable,
|
||||
PointSeries,
|
||||
PointSeriesColumns,
|
||||
PointSeriesColumn,
|
||||
PointSeriesColumnName,
|
||||
Render,
|
||||
Style,
|
||||
} from './types';
|
|
@ -17,15 +17,21 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export const boolean = () => ({
|
||||
name: 'boolean',
|
||||
import { ExpressionType } from '../../types';
|
||||
import { Datatable } from './datatable';
|
||||
import { Render } from './render';
|
||||
|
||||
const name = 'boolean';
|
||||
|
||||
export const boolean = (): ExpressionType<typeof name, boolean> => ({
|
||||
name,
|
||||
from: {
|
||||
null: () => false,
|
||||
number: n => Boolean(n),
|
||||
string: s => Boolean(s),
|
||||
},
|
||||
to: {
|
||||
render: value => {
|
||||
render: (value): Render<{ text: string }> => {
|
||||
const text = `${value}`;
|
||||
return {
|
||||
type: 'render',
|
||||
|
@ -33,9 +39,9 @@ export const boolean = () => ({
|
|||
value: { text },
|
||||
};
|
||||
},
|
||||
datatable: value => ({
|
||||
datatable: (value): Datatable => ({
|
||||
type: 'datatable',
|
||||
columns: [{ name: 'value', type: 'boolean' }],
|
||||
columns: [{ name: 'value', type: name }],
|
||||
rows: [{ value }],
|
||||
}),
|
||||
},
|
|
@ -19,30 +19,83 @@
|
|||
|
||||
import { map, pick, zipObject } from 'lodash';
|
||||
|
||||
export const datatable = () => ({
|
||||
name: 'datatable',
|
||||
validate: datatable => {
|
||||
import { ExpressionType } from '../../types';
|
||||
import { PointSeries } from './pointseries';
|
||||
import { Render } from './render';
|
||||
|
||||
const name = 'datatable';
|
||||
|
||||
/**
|
||||
* A Utility function that Typescript can use to determine if an object is a Datatable.
|
||||
* @param datatable
|
||||
*/
|
||||
export const isDatatable = (datatable: any): datatable is Datatable =>
|
||||
!!datatable && datatable.type === 'datatable';
|
||||
|
||||
/**
|
||||
* This type represents the `type` of any `DatatableColumn` in a `Datatable`.
|
||||
*/
|
||||
export type DatatableColumnType = 'string' | 'number' | 'boolean' | 'date' | 'null';
|
||||
|
||||
/**
|
||||
* This type represents a `DatatableRow` in a `Datatable`.
|
||||
*/
|
||||
export type DatatableRow = Record<string, any>;
|
||||
|
||||
/**
|
||||
* This type represents the shape of a column in a `Datatable`.
|
||||
*/
|
||||
export interface DatatableColumn {
|
||||
name: string;
|
||||
type: DatatableColumnType;
|
||||
}
|
||||
|
||||
/**
|
||||
* A `Datatable` in Canvas is a unique structure that represents tabulated data.
|
||||
*/
|
||||
export interface Datatable {
|
||||
type: typeof name;
|
||||
columns: DatatableColumn[];
|
||||
rows: DatatableRow[];
|
||||
}
|
||||
|
||||
interface SerializedDatatable extends Datatable {
|
||||
rows: string[][];
|
||||
}
|
||||
|
||||
interface RenderedDatatable {
|
||||
datatable: Datatable;
|
||||
paginate: boolean;
|
||||
perPage: number;
|
||||
showHeader: boolean;
|
||||
}
|
||||
|
||||
export const datatable = (): ExpressionType<typeof name, Datatable, SerializedDatatable> => ({
|
||||
name,
|
||||
validate: table => {
|
||||
// TODO: Check columns types. Only string, boolean, number, date, allowed for now.
|
||||
if (!datatable.columns) {
|
||||
if (!table.columns) {
|
||||
throw new Error('datatable must have a columns array, even if it is empty');
|
||||
}
|
||||
|
||||
if (!datatable.rows) throw new Error('datatable must have a rows array, even if it is empty');
|
||||
if (!table.rows) {
|
||||
throw new Error('datatable must have a rows array, even if it is empty');
|
||||
}
|
||||
},
|
||||
serialize: datatable => {
|
||||
const { columns, rows } = datatable;
|
||||
serialize: table => {
|
||||
const { columns, rows } = table;
|
||||
return {
|
||||
...datatable,
|
||||
...table,
|
||||
rows: rows.map(row => {
|
||||
return columns.map(column => row[column.name]);
|
||||
}),
|
||||
};
|
||||
},
|
||||
deserialize: datatable => {
|
||||
const { columns, rows } = datatable;
|
||||
deserialize: table => {
|
||||
const { columns, rows } = table;
|
||||
return {
|
||||
...datatable,
|
||||
rows: rows.map(row => {
|
||||
...table,
|
||||
rows: rows.map((row: any) => {
|
||||
return zipObject(map(columns, 'name'), row);
|
||||
}),
|
||||
};
|
||||
|
@ -50,44 +103,44 @@ export const datatable = () => ({
|
|||
from: {
|
||||
null: () => {
|
||||
return {
|
||||
type: 'datatable',
|
||||
type: name,
|
||||
rows: [],
|
||||
columns: [],
|
||||
};
|
||||
},
|
||||
pointseries: context => {
|
||||
pointseries: (context: PointSeries) => {
|
||||
return {
|
||||
type: 'datatable',
|
||||
type: name,
|
||||
rows: context.rows,
|
||||
columns: map(context.columns, (val, name) => {
|
||||
return { name: name, type: val.type, role: val.role };
|
||||
columns: map(context.columns, (val, colName) => {
|
||||
return { name: colName!, type: val.type };
|
||||
}),
|
||||
};
|
||||
},
|
||||
},
|
||||
to: {
|
||||
render: datatable => {
|
||||
render: (table): Render<RenderedDatatable> => {
|
||||
return {
|
||||
type: 'render',
|
||||
as: 'table',
|
||||
value: {
|
||||
datatable,
|
||||
datatable: table,
|
||||
paginate: true,
|
||||
perPage: 10,
|
||||
showHeader: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
pointseries: datatable => {
|
||||
pointseries: (table): PointSeries => {
|
||||
// datatable columns are an array that looks like [{ name: "one", type: "string" }, { name: "two", type: "string" }]
|
||||
// rows look like [{ one: 1, two: 2}, { one: 3, two: 4}, ...]
|
||||
const validFields = ['x', 'y', 'color', 'size', 'text'];
|
||||
const columns = datatable.columns.filter(column => validFields.includes(column.name));
|
||||
const rows = datatable.rows.map(row => pick(row, validFields));
|
||||
const columns = table.columns.filter(column => validFields.includes(column.name));
|
||||
const rows = table.rows.map(row => pick(row, validFields));
|
||||
|
||||
return {
|
||||
type: 'pointseries',
|
||||
columns: columns.reduce((acc, column) => {
|
||||
columns: columns.reduce((acc: Record<string, any>, column) => {
|
||||
/* pointseries columns are an object that looks like this
|
||||
* {
|
||||
* x: { type: "string", expression: "x", role: "dimension" },
|
|
@ -17,26 +17,28 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export const pointseries = () => ({
|
||||
name: 'pointseries',
|
||||
from: {
|
||||
null: () => {
|
||||
return {
|
||||
type: 'pointseries',
|
||||
rows: [],
|
||||
columns: {},
|
||||
};
|
||||
},
|
||||
},
|
||||
import { ExpressionType } from '../../types';
|
||||
import { Render } from './render';
|
||||
|
||||
const name = 'error';
|
||||
|
||||
// TODO: Improve typings on this interface [#38553]
|
||||
export interface InterpreterErrorType {
|
||||
type: typeof name;
|
||||
error: unknown;
|
||||
info: unknown;
|
||||
}
|
||||
|
||||
export const error = (): ExpressionType<typeof name, InterpreterErrorType> => ({
|
||||
name,
|
||||
to: {
|
||||
render: (pointseries, types) => {
|
||||
const datatable = types.datatable.from(pointseries, types);
|
||||
render: (input): Render<Pick<InterpreterErrorType, 'error' | 'info'>> => {
|
||||
return {
|
||||
type: 'render',
|
||||
as: 'table',
|
||||
as: name,
|
||||
value: {
|
||||
datatable,
|
||||
showHeader: true,
|
||||
error: input.error,
|
||||
info: input.info,
|
||||
},
|
||||
};
|
||||
},
|
50
src/legacy/core_plugins/interpreter/common/types/filter.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 { ExpressionType } from '../../types';
|
||||
|
||||
const name = 'filter';
|
||||
|
||||
/**
|
||||
* Represents an object that is a Filter.
|
||||
*/
|
||||
export interface Filter {
|
||||
type?: string;
|
||||
value?: string;
|
||||
column?: string;
|
||||
and: Filter[];
|
||||
to?: string;
|
||||
from?: string;
|
||||
query?: string | null;
|
||||
}
|
||||
|
||||
export const filter = (): ExpressionType<typeof name, Filter> => ({
|
||||
name,
|
||||
from: {
|
||||
null: () => {
|
||||
return {
|
||||
type: name,
|
||||
// Any meta data you wish to pass along.
|
||||
meta: {},
|
||||
// And filters. If you need an "or", create a filter type for it.
|
||||
and: [],
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
42
src/legacy/core_plugins/interpreter/common/types/image.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { ExpressionType } from '../../types';
|
||||
import { Render } from './render';
|
||||
|
||||
const name = 'image';
|
||||
|
||||
export interface ExpressionImage {
|
||||
type: 'image';
|
||||
mode: string;
|
||||
dataurl: string;
|
||||
}
|
||||
|
||||
export const image = (): ExpressionType<typeof name, ExpressionImage> => ({
|
||||
name,
|
||||
to: {
|
||||
render: (input): Render<Pick<ExpressionImage, 'mode' | 'dataurl'>> => {
|
||||
return {
|
||||
type: 'render',
|
||||
as: 'image',
|
||||
value: input,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
|
@ -17,29 +17,17 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
import { boolean } from './boolean';
|
||||
// @ts-ignore
|
||||
import { datatable } from './datatable';
|
||||
// @ts-ignore
|
||||
import { error } from './error';
|
||||
// @ts-ignore
|
||||
import { filter } from './filter';
|
||||
// @ts-ignore
|
||||
import { image } from './image';
|
||||
// @ts-ignore
|
||||
import { nullType } from './null';
|
||||
// @ts-ignore
|
||||
import { number } from './number';
|
||||
// @ts-ignore
|
||||
import { pointseries } from './pointseries';
|
||||
// @ts-ignore
|
||||
import { render } from './render';
|
||||
// @ts-ignore
|
||||
import { shape } from './shape';
|
||||
// @ts-ignore
|
||||
import { string } from './string';
|
||||
// @ts-ignore
|
||||
import { style } from './style';
|
||||
import { kibanaContext } from './kibana_context';
|
||||
import { kibanaDatatable } from './kibana_datatable';
|
||||
|
@ -61,5 +49,13 @@ export const typeSpecs = [
|
|||
kibanaDatatable,
|
||||
];
|
||||
|
||||
export { KibanaContext } from './kibana_context';
|
||||
export { KibanaDatatable } from './kibana_datatable';
|
||||
// Types
|
||||
export * from './datatable';
|
||||
export * from './error';
|
||||
export * from './filter';
|
||||
export * from './image';
|
||||
export * from './kibana_context';
|
||||
export * from './kibana_datatable';
|
||||
export * from './pointseries';
|
||||
export * from './render';
|
||||
export * from './style';
|
||||
|
|
|
@ -17,15 +17,13 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export const style = () => ({
|
||||
name: 'style',
|
||||
import { ExpressionType } from '../../types';
|
||||
|
||||
const name = 'null';
|
||||
|
||||
export const nullType = (): ExpressionType<typeof name, null> => ({
|
||||
name,
|
||||
from: {
|
||||
null: () => {
|
||||
return {
|
||||
type: 'style',
|
||||
spec: {},
|
||||
css: '',
|
||||
};
|
||||
},
|
||||
'*': () => null,
|
||||
},
|
||||
});
|
|
@ -17,15 +17,21 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export const number = () => ({
|
||||
name: 'number',
|
||||
import { ExpressionType } from '../../types';
|
||||
import { Datatable } from './datatable';
|
||||
import { Render } from './render';
|
||||
|
||||
const name = 'number';
|
||||
|
||||
export const number = (): ExpressionType<typeof name, number> => ({
|
||||
name,
|
||||
from: {
|
||||
null: () => 0,
|
||||
boolean: b => Number(b),
|
||||
string: n => Number(n),
|
||||
},
|
||||
to: {
|
||||
render: value => {
|
||||
render: (value: number): Render<{ text: string }> => {
|
||||
const text = `${value}`;
|
||||
return {
|
||||
type: 'render',
|
||||
|
@ -33,7 +39,7 @@ export const number = () => ({
|
|||
value: { text },
|
||||
};
|
||||
},
|
||||
datatable: value => ({
|
||||
datatable: (value): Datatable => ({
|
||||
type: 'datatable',
|
||||
columns: [{ name: 'value', type: 'number' }],
|
||||
rows: [{ value }],
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { ExpressionType } from '../../types';
|
||||
import { Datatable } from './datatable';
|
||||
import { Render } from './render';
|
||||
|
||||
const name = 'pointseries';
|
||||
|
||||
/**
|
||||
* Allowed column names in a PointSeries
|
||||
*/
|
||||
export type PointSeriesColumnName = 'x' | 'y' | 'color' | 'size' | 'text';
|
||||
|
||||
/**
|
||||
* Column in a PointSeries
|
||||
*/
|
||||
export interface PointSeriesColumn {
|
||||
type: 'number' | 'string';
|
||||
role: 'measure' | 'dimension';
|
||||
expression: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a collection of valid Columns in a PointSeries
|
||||
*/
|
||||
export type PointSeriesColumns = Record<PointSeriesColumnName, PointSeriesColumn> | {};
|
||||
|
||||
/**
|
||||
* A `PointSeries` is a unique structure that represents dots on a chart.
|
||||
*/
|
||||
export interface PointSeries {
|
||||
type: typeof name;
|
||||
columns: PointSeriesColumns;
|
||||
rows: Array<Record<string, any>>;
|
||||
}
|
||||
|
||||
export const pointseries = (): ExpressionType<typeof name, PointSeries> => ({
|
||||
name,
|
||||
from: {
|
||||
null: () => {
|
||||
return {
|
||||
type: name,
|
||||
rows: [],
|
||||
columns: {},
|
||||
};
|
||||
},
|
||||
},
|
||||
to: {
|
||||
render: (
|
||||
pseries: PointSeries,
|
||||
types
|
||||
): Render<{ datatable: Datatable; showHeader: boolean }> => {
|
||||
const datatable: Datatable = types.datatable.from(pseries, types);
|
||||
return {
|
||||
type: 'render',
|
||||
as: 'table',
|
||||
value: {
|
||||
datatable,
|
||||
showHeader: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
42
src/legacy/core_plugins/interpreter/common/types/render.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { ExpressionType } from '../../types';
|
||||
|
||||
const name = 'render';
|
||||
|
||||
/**
|
||||
* Represents an object that is intended to be rendered.
|
||||
*/
|
||||
export interface Render<T> {
|
||||
type: typeof name;
|
||||
as: string;
|
||||
value: T;
|
||||
}
|
||||
|
||||
export const render = (): ExpressionType<typeof name, Render<unknown>> => ({
|
||||
name,
|
||||
from: {
|
||||
'*': <T>(v: T): Render<T> => ({
|
||||
type: name,
|
||||
as: 'debug',
|
||||
value: v,
|
||||
}),
|
||||
},
|
||||
});
|
|
@ -17,13 +17,18 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export const shape = () => ({
|
||||
name: 'shape',
|
||||
import { ExpressionType } from '../../types';
|
||||
import { Render } from './render';
|
||||
|
||||
const name = 'shape';
|
||||
|
||||
export const shape = (): ExpressionType<typeof name, undefined> => ({
|
||||
name,
|
||||
to: {
|
||||
render: input => {
|
||||
render: <T>(input: T): Render<T> => {
|
||||
return {
|
||||
type: 'render',
|
||||
as: 'shape',
|
||||
as: name,
|
||||
value: input,
|
||||
};
|
||||
},
|
|
@ -17,22 +17,28 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export const string = () => ({
|
||||
name: 'string',
|
||||
import { ExpressionType } from '../../types';
|
||||
import { Datatable } from './datatable';
|
||||
import { Render } from './render';
|
||||
|
||||
const name = 'string';
|
||||
|
||||
export const string = (): ExpressionType<typeof name, string> => ({
|
||||
name,
|
||||
from: {
|
||||
null: () => '',
|
||||
boolean: b => String(b),
|
||||
number: n => String(n),
|
||||
},
|
||||
to: {
|
||||
render: text => {
|
||||
render: <T>(text: T): Render<{ text: T }> => {
|
||||
return {
|
||||
type: 'render',
|
||||
as: 'text',
|
||||
value: { text },
|
||||
};
|
||||
},
|
||||
datatable: value => ({
|
||||
datatable: (value): Datatable => ({
|
||||
type: 'datatable',
|
||||
columns: [{ name: 'value', type: 'string' }],
|
||||
rows: [{ value }],
|
|
@ -17,16 +17,24 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export const filter = () => ({
|
||||
name: 'filter',
|
||||
import { ExpressionType } from '../../types';
|
||||
|
||||
const name = 'style';
|
||||
|
||||
export interface Style {
|
||||
type: typeof name;
|
||||
spec: any;
|
||||
css: string;
|
||||
}
|
||||
|
||||
export const style = (): ExpressionType<typeof name, Style> => ({
|
||||
name,
|
||||
from: {
|
||||
null: () => {
|
||||
return {
|
||||
type: 'filter',
|
||||
// Any meta data you wish to pass along.
|
||||
meta: {},
|
||||
// And filters. If you need an "or", create a filter type for it.
|
||||
and: [],
|
||||
type: 'style',
|
||||
spec: {},
|
||||
css: '',
|
||||
};
|
||||
},
|
||||
},
|
|
@ -26,4 +26,5 @@ export {
|
|||
UnwrapPromise,
|
||||
} from './common';
|
||||
export { ExpressionFunction } from './functions';
|
||||
export { ExpressionType } from './types';
|
||||
export * from '../common/types';
|
||||
|
|
33
src/legacy/core_plugins/interpreter/types/types.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A generic type which represents a custom Expression Type Definition that's
|
||||
* registered to the Interpreter.
|
||||
*/
|
||||
export interface ExpressionType<Name extends string, Type, SerializedType = undefined> {
|
||||
name: Name;
|
||||
validate?: (type: any) => void | Error;
|
||||
serialize?: (type: Type) => SerializedType;
|
||||
deserialize?: (type: SerializedType) => Type;
|
||||
// TODO: Update typings for the `availableTypes` parameter once interfaces for this
|
||||
// have been added elsewhere in the interpreter.
|
||||
from?: Record<string, (ctx: any, availableTypes: Record<string, any>) => Type>;
|
||||
to?: Record<string, (type: Type, availableTypes: Record<string, any>) => unknown>;
|
||||
}
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import { functionsRegistry } from 'plugins/interpreter/registries';
|
||||
import { VislibSlicesResponseHandlerProvider as vislibSlicesResponseHandler } from 'ui/vis/response_handlers/vislib';
|
||||
import { vislibSlicesResponseHandlerProvider as vislibSlicesResponseHandler } from 'ui/vis/response_handlers/vislib';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const kibanaPie = () => ({
|
||||
|
|
|
@ -35,7 +35,7 @@ const mockResponseHandler = jest.fn().mockReturnValue(Promise.resolve({
|
|||
},
|
||||
}));
|
||||
jest.mock('ui/vis/response_handlers/vislib', () => ({
|
||||
VislibSlicesResponseHandlerProvider: () => ({ handler: mockResponseHandler }),
|
||||
vislibSlicesResponseHandlerProvider: () => ({ handler: mockResponseHandler }),
|
||||
}));
|
||||
|
||||
describe('interpreter/functions#pie', () => {
|
||||
|
|
|
@ -19,8 +19,7 @@
|
|||
|
||||
import { functionsRegistry } from 'plugins/interpreter/registries';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { VislibSeriesResponseHandlerProvider } from 'ui/vis/response_handlers/vislib';
|
||||
import chrome from 'ui/chrome';
|
||||
import { vislibSeriesResponseHandlerProvider } from 'ui/vis/response_handlers/vislib';
|
||||
|
||||
export const vislib = () => ({
|
||||
name: 'vislib',
|
||||
|
@ -40,9 +39,7 @@ export const vislib = () => ({
|
|||
},
|
||||
},
|
||||
async fn(context, args) {
|
||||
const $injector = await chrome.dangerouslyGetActiveInjector();
|
||||
const Private = $injector.get('Private');
|
||||
const responseHandler = Private(VislibSeriesResponseHandlerProvider).handler;
|
||||
const responseHandler = vislibSeriesResponseHandlerProvider().handler;
|
||||
const visConfigParams = JSON.parse(args.visConfig);
|
||||
|
||||
const convertedData = await responseHandler(context, visConfigParams.dimensions);
|
||||
|
|
|
@ -240,9 +240,7 @@ export default function (kibana) {
|
|||
migrations,
|
||||
},
|
||||
|
||||
uiCapabilities: async function (server) {
|
||||
const { savedObjects } = server;
|
||||
|
||||
uiCapabilities: async function () {
|
||||
return {
|
||||
discover: {
|
||||
show: true,
|
||||
|
@ -275,14 +273,11 @@ export default function (kibana) {
|
|||
indexPatterns: {
|
||||
save: true,
|
||||
},
|
||||
savedObjectsManagement: savedObjects.types.reduce((acc, type) => ({
|
||||
...acc,
|
||||
[type]: {
|
||||
delete: true,
|
||||
edit: true,
|
||||
read: true,
|
||||
}
|
||||
}), {}),
|
||||
savedObjectsManagement: {
|
||||
delete: true,
|
||||
edit: true,
|
||||
read: true,
|
||||
},
|
||||
management: {
|
||||
/*
|
||||
* Management settings correspond to management section/link ids, and should not be changed
|
||||
|
|
|
@ -90,7 +90,7 @@ directive including its respective styles.
|
|||
**api/anchor.js**: Exports `fetchAnchor()` that creates and executes the
|
||||
query for the anchor document.
|
||||
|
||||
**api/context.js**: Exports `fetchPredecessors()` and `fetchSuccessors()` that
|
||||
**api/context.js**: Exports `fetchPredecessors()`, `fetchSuccessors()`, `fetchSurroundingDocs()` that
|
||||
create and execute the queries for the preceeding and succeeding documents.
|
||||
|
||||
**api/utils**: Exports various functions used to create and transform
|
||||
|
|
|
@ -25,6 +25,7 @@ export function createIndexPatternsStub() {
|
|||
get: sinon.spy(indexPatternId =>
|
||||
Promise.resolve({
|
||||
id: indexPatternId,
|
||||
isTimeNanosBased: () => false
|
||||
})
|
||||
),
|
||||
};
|
||||
|
|
|
@ -91,7 +91,7 @@ describe('context app', function () {
|
|||
return fetchAnchor('INDEX_PATTERN_ID', 'doc', 'id', [{ '@timestamp': 'desc' }, { '_doc': 'desc' }])
|
||||
.then(() => {
|
||||
const setFieldSpy = searchSourceStub.setField;
|
||||
expect(setFieldSpy.firstCall.args[1]).to.eql({ id: 'INDEX_PATTERN_ID' });
|
||||
expect(setFieldSpy.firstCall.args[1].id).to.eql('INDEX_PATTERN_ID');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -28,6 +28,10 @@ import { SearchSourceProvider } from 'ui/courier';
|
|||
import { fetchContextProvider } from '../context';
|
||||
|
||||
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
||||
const ANCHOR_TIMESTAMP = (new Date(MS_PER_DAY)).toJSON();
|
||||
const ANCHOR_TIMESTAMP_3 = (new Date(MS_PER_DAY * 3)).toJSON();
|
||||
const ANCHOR_TIMESTAMP_1000 = (new Date(MS_PER_DAY * 1000)).toJSON();
|
||||
const ANCHOR_TIMESTAMP_3000 = (new Date(MS_PER_DAY * 3000)).toJSON();
|
||||
|
||||
describe('context app', function () {
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
|
@ -61,6 +65,7 @@ describe('context app', function () {
|
|||
'INDEX_PATTERN_ID',
|
||||
'@timestamp',
|
||||
'desc',
|
||||
ANCHOR_TIMESTAMP_3000,
|
||||
MS_PER_DAY * 3000,
|
||||
'_doc',
|
||||
0,
|
||||
|
@ -87,6 +92,7 @@ describe('context app', function () {
|
|||
'INDEX_PATTERN_ID',
|
||||
'@timestamp',
|
||||
'desc',
|
||||
ANCHOR_TIMESTAMP_3000,
|
||||
MS_PER_DAY * 3000,
|
||||
'_doc',
|
||||
0,
|
||||
|
@ -122,6 +128,7 @@ describe('context app', function () {
|
|||
'INDEX_PATTERN_ID',
|
||||
'@timestamp',
|
||||
'desc',
|
||||
ANCHOR_TIMESTAMP_1000,
|
||||
MS_PER_DAY * 1000,
|
||||
'_doc',
|
||||
0,
|
||||
|
@ -138,7 +145,6 @@ describe('context app', function () {
|
|||
// should have stopped before reaching MS_PER_DAY * 1700
|
||||
expect(moment(_.last(intervals).lte).valueOf()).to.be.lessThan(MS_PER_DAY * 1700);
|
||||
expect(intervals.length).to.be.greaterThan(1);
|
||||
|
||||
expect(hits).to.eql(searchSourceStub._stubHits.slice(-3));
|
||||
});
|
||||
});
|
||||
|
@ -148,6 +154,7 @@ describe('context app', function () {
|
|||
'INDEX_PATTERN_ID',
|
||||
'@timestamp',
|
||||
'desc',
|
||||
ANCHOR_TIMESTAMP_3,
|
||||
MS_PER_DAY * 3,
|
||||
'_doc',
|
||||
0,
|
||||
|
@ -166,6 +173,7 @@ describe('context app', function () {
|
|||
'INDEX_PATTERN_ID',
|
||||
'@timestamp',
|
||||
'desc',
|
||||
ANCHOR_TIMESTAMP_3,
|
||||
MS_PER_DAY * 3,
|
||||
'_doc',
|
||||
0,
|
||||
|
@ -186,6 +194,7 @@ describe('context app', function () {
|
|||
'INDEX_PATTERN_ID',
|
||||
'@timestamp',
|
||||
'desc',
|
||||
ANCHOR_TIMESTAMP,
|
||||
MS_PER_DAY,
|
||||
'_doc',
|
||||
0,
|
||||
|
|
|
@ -28,6 +28,9 @@ import { SearchSourceProvider } from 'ui/courier';
|
|||
import { fetchContextProvider } from '../context';
|
||||
|
||||
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
||||
const ANCHOR_TIMESTAMP = (new Date(MS_PER_DAY)).toJSON();
|
||||
const ANCHOR_TIMESTAMP_3 = (new Date(MS_PER_DAY * 3)).toJSON();
|
||||
const ANCHOR_TIMESTAMP_3000 = (new Date(MS_PER_DAY * 3000)).toJSON();
|
||||
|
||||
describe('context app', function () {
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
|
@ -61,6 +64,7 @@ describe('context app', function () {
|
|||
'INDEX_PATTERN_ID',
|
||||
'@timestamp',
|
||||
'desc',
|
||||
ANCHOR_TIMESTAMP_3000,
|
||||
MS_PER_DAY * 3000,
|
||||
'_doc',
|
||||
0,
|
||||
|
@ -87,6 +91,7 @@ describe('context app', function () {
|
|||
'INDEX_PATTERN_ID',
|
||||
'@timestamp',
|
||||
'desc',
|
||||
ANCHOR_TIMESTAMP_3000,
|
||||
MS_PER_DAY * 3000,
|
||||
'_doc',
|
||||
0,
|
||||
|
@ -124,6 +129,7 @@ describe('context app', function () {
|
|||
'INDEX_PATTERN_ID',
|
||||
'@timestamp',
|
||||
'desc',
|
||||
ANCHOR_TIMESTAMP_3000,
|
||||
MS_PER_DAY * 3000,
|
||||
'_doc',
|
||||
0,
|
||||
|
@ -150,6 +156,7 @@ describe('context app', function () {
|
|||
'INDEX_PATTERN_ID',
|
||||
'@timestamp',
|
||||
'desc',
|
||||
ANCHOR_TIMESTAMP_3,
|
||||
MS_PER_DAY * 3,
|
||||
'_doc',
|
||||
0,
|
||||
|
@ -168,6 +175,7 @@ describe('context app', function () {
|
|||
'INDEX_PATTERN_ID',
|
||||
'@timestamp',
|
||||
'desc',
|
||||
ANCHOR_TIMESTAMP_3,
|
||||
MS_PER_DAY * 3,
|
||||
'_doc',
|
||||
0,
|
||||
|
@ -188,6 +196,7 @@ describe('context app', function () {
|
|||
'INDEX_PATTERN_ID',
|
||||
'@timestamp',
|
||||
'desc',
|
||||
ANCHOR_TIMESTAMP,
|
||||
MS_PER_DAY,
|
||||
'_doc',
|
||||
0,
|
||||
|
|
|
@ -18,12 +18,15 @@
|
|||
*/
|
||||
// @ts-check
|
||||
|
||||
|
||||
// @ts-ignore
|
||||
import { SearchSourceProvider } from 'ui/courier';
|
||||
import moment from 'moment';
|
||||
|
||||
import { reverseSortDirection } from './utils/sorting';
|
||||
import {
|
||||
extractNanoSeconds,
|
||||
convertIsoToNanosAsStr,
|
||||
convertIsoToMillis,
|
||||
convertTimeValueToIso
|
||||
} from './utils/date_conversion';
|
||||
|
||||
/**
|
||||
* @typedef {Object} SearchResult
|
||||
|
@ -42,6 +45,10 @@ import { reverseSortDirection } from './utils/sorting';
|
|||
* @typedef {'asc' | 'desc'} SortDirection
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {'successors' |'predecessors'} SurroundingDocType
|
||||
*/
|
||||
|
||||
const DAY_MILLIS = 24 * 60 * 60 * 1000;
|
||||
|
||||
// look from 1 day up to 10000 days into the past and future
|
||||
|
@ -54,110 +61,93 @@ function fetchContextProvider(indexPatterns, Private) {
|
|||
const SearchSource = Private(SearchSourceProvider);
|
||||
|
||||
return {
|
||||
fetchPredecessors,
|
||||
fetchSuccessors,
|
||||
// @ts-ignore / for testing
|
||||
fetchPredecessors: (...args) => fetchSurroundingDocs('predecessors', ...args),
|
||||
// @ts-ignore / for testing
|
||||
fetchSuccessors: (...args) => fetchSurroundingDocs('successors', ...args),
|
||||
fetchSurroundingDocs,
|
||||
};
|
||||
|
||||
async function fetchSuccessors(
|
||||
/**
|
||||
* Fetch successor or predecessor documents of a given anchor document
|
||||
*
|
||||
* @param {SurroundingDocType} type - `successors` or `predecessors`
|
||||
* @param {string} indexPatternId
|
||||
* @param {string} timeFieldName - name of the timefield, that's sorted on
|
||||
* @param {SortDirection} timeFieldSortDir - direction of sorting
|
||||
* @param {string} timeFieldIsoValue - value of the anchors timefield in ISO format
|
||||
* @param {number} timeFieldNumValue - value of the anchors timefield in numeric format (invalid for nanos)
|
||||
* @param {string} tieBreakerField - name of 2nd param for sorting
|
||||
* @param {string} tieBreakerValue - value of 2nd param for sorting
|
||||
* @param {number} size - number of records to retrieve
|
||||
* @param {any[]} filters - to apply in the elastic query
|
||||
* @returns {Promise<object[]>}
|
||||
*/
|
||||
async function fetchSurroundingDocs(
|
||||
type,
|
||||
indexPatternId,
|
||||
timeField,
|
||||
timeSortDirection,
|
||||
timeValue,
|
||||
timeFieldName,
|
||||
timeFieldSortDir,
|
||||
timeFieldIsoValue,
|
||||
timeFieldNumValue,
|
||||
tieBreakerField,
|
||||
tieBreakerValue,
|
||||
size,
|
||||
filters
|
||||
) {
|
||||
const searchSource = await createSearchSource(indexPatternId, filters);
|
||||
const offsetSign = timeSortDirection === 'asc' ? 1 : -1;
|
||||
const indexPattern = await indexPatterns.get(indexPatternId);
|
||||
const searchSource = await createSearchSource(indexPattern, filters);
|
||||
const sortDir = type === 'successors' ? timeFieldSortDir : reverseSortDirection(timeFieldSortDir);
|
||||
const nanoSeconds = indexPattern.isTimeNanosBased() ? extractNanoSeconds(timeFieldIsoValue) : '';
|
||||
const timeValueMillis = nanoSeconds !== '' ? convertIsoToMillis(timeFieldIsoValue) : timeFieldNumValue;
|
||||
|
||||
const offsetSign = (timeFieldSortDir === 'asc' && type === 'successors' || timeFieldSortDir === 'desc' && type === 'predecessors')
|
||||
? 1
|
||||
: -1;
|
||||
|
||||
// ending with `null` opens the last interval
|
||||
const intervals = asPairs([...LOOKUP_OFFSETS.map(offset => timeValue + offset * offsetSign), null]);
|
||||
const intervals = asPairs([...LOOKUP_OFFSETS.map(offset => timeValueMillis + offset * offsetSign), null]);
|
||||
|
||||
let successors = [];
|
||||
for (const [startTimeValue, endTimeValue] of intervals) {
|
||||
const remainingSize = size - successors.length;
|
||||
let documents = [];
|
||||
for (const [iStartTimeValue, iEndTimeValue] of intervals) {
|
||||
const remainingSize = size - documents.length;
|
||||
|
||||
if (remainingSize <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
const [afterTimeValue, afterTieBreakerValue] = successors.length > 0
|
||||
? successors[successors.length - 1].sort
|
||||
: [timeValue, tieBreakerValue];
|
||||
const afterTimeRecIdx = type === 'successors' && documents.length ? documents.length - 1 : 0;
|
||||
const afterTimeValue = nanoSeconds
|
||||
? convertIsoToNanosAsStr(documents.length ? documents[afterTimeRecIdx]._source[timeFieldName] : timeFieldIsoValue)
|
||||
: timeFieldNumValue;
|
||||
const afterTieBreakerValue = documents.length > 0 ? documents[afterTimeRecIdx].sort[1] : tieBreakerValue;
|
||||
|
||||
const hits = await fetchHitsInInterval(
|
||||
searchSource,
|
||||
timeField,
|
||||
timeSortDirection,
|
||||
startTimeValue,
|
||||
endTimeValue,
|
||||
timeFieldName,
|
||||
sortDir,
|
||||
iStartTimeValue,
|
||||
iEndTimeValue,
|
||||
afterTimeValue,
|
||||
tieBreakerField,
|
||||
afterTieBreakerValue,
|
||||
remainingSize
|
||||
remainingSize,
|
||||
nanoSeconds
|
||||
);
|
||||
|
||||
successors = [...successors, ...hits];
|
||||
documents = type === 'successors'
|
||||
? [...documents, ...hits]
|
||||
: [...hits.slice().reverse(), ...documents];
|
||||
}
|
||||
|
||||
return successors;
|
||||
}
|
||||
|
||||
async function fetchPredecessors(
|
||||
indexPatternId,
|
||||
timeField,
|
||||
timeSortDirection,
|
||||
timeValue,
|
||||
tieBreakerField,
|
||||
tieBreakerValue,
|
||||
size,
|
||||
filters
|
||||
) {
|
||||
const searchSource = await createSearchSource(indexPatternId, filters);
|
||||
const offsetSign = timeSortDirection === 'desc' ? 1 : -1;
|
||||
|
||||
// ending with `null` opens the last interval
|
||||
const intervals = asPairs([...LOOKUP_OFFSETS.map(offset => timeValue + offset * offsetSign), null]);
|
||||
|
||||
let predecessors = [];
|
||||
for (const [startTimeValue, endTimeValue] of intervals) {
|
||||
const remainingSize = size - predecessors.length;
|
||||
|
||||
if (remainingSize <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
const [afterTimeValue, afterTieBreakerValue] = predecessors.length > 0
|
||||
? predecessors[0].sort
|
||||
: [timeValue, tieBreakerValue];
|
||||
|
||||
const hits = await fetchHitsInInterval(
|
||||
searchSource,
|
||||
timeField,
|
||||
reverseSortDirection(timeSortDirection),
|
||||
startTimeValue,
|
||||
endTimeValue,
|
||||
afterTimeValue,
|
||||
tieBreakerField,
|
||||
afterTieBreakerValue,
|
||||
remainingSize
|
||||
);
|
||||
|
||||
predecessors = [...hits.slice().reverse(), ...predecessors];
|
||||
}
|
||||
|
||||
return predecessors;
|
||||
return documents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} indexPatternId
|
||||
* @param {Object} indexPattern
|
||||
* @param {any[]} filters
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function createSearchSource(indexPatternId, filters) {
|
||||
const indexPattern = await indexPatterns.get(indexPatternId);
|
||||
|
||||
async function createSearchSource(indexPattern, filters) {
|
||||
return new SearchSource()
|
||||
.setParent(false)
|
||||
.setField('index', indexPattern)
|
||||
|
@ -166,7 +156,7 @@ function fetchContextProvider(indexPatterns, Private) {
|
|||
|
||||
/**
|
||||
* Fetch the hits between `(afterTimeValue, tieBreakerValue)` and
|
||||
* `endTimeValue` from the `searchSource` using the given `timeField` and
|
||||
* `endRangeMillis` from the `searchSource` using the given `timeField` and
|
||||
* `tieBreakerField` fields up to a maximum of `maxCount` documents. The
|
||||
* documents are sorted by `(timeField, tieBreakerField)` using the
|
||||
* `timeSortDirection` for both fields
|
||||
|
@ -175,32 +165,35 @@ function fetchContextProvider(indexPatterns, Private) {
|
|||
* and filters set.
|
||||
*
|
||||
* @param {SearchSourceT} searchSource
|
||||
* @param {string} timeField
|
||||
* @param {SortDirection} timeSortDirection
|
||||
* @param {number} startTimeValue
|
||||
* @param {number | null} endTimeValue
|
||||
* @param {number} [afterTimeValue=startTimeValue]
|
||||
* @param {string} timeFieldName
|
||||
* @param {SortDirection} timeFieldSortDir
|
||||
* @param {number} startRangeMillis
|
||||
* @param {number | null} endRangeMillis
|
||||
* @param {number| string} afterTimeValue
|
||||
* @param {string} tieBreakerField
|
||||
* @param {number} tieBreakerValue
|
||||
* @param {number} maxCount
|
||||
* @param {string} nanosValue
|
||||
* @returns {Promise<object[]>}
|
||||
*/
|
||||
async function fetchHitsInInterval(
|
||||
searchSource,
|
||||
timeField,
|
||||
timeSortDirection,
|
||||
startTimeValue,
|
||||
endTimeValue,
|
||||
timeFieldName,
|
||||
timeFieldSortDir,
|
||||
startRangeMillis,
|
||||
endRangeMillis,
|
||||
afterTimeValue,
|
||||
tieBreakerField,
|
||||
tieBreakerValue,
|
||||
maxCount
|
||||
maxCount,
|
||||
nanosValue
|
||||
) {
|
||||
|
||||
const startRange = {
|
||||
[timeSortDirection === 'asc' ? 'gte' : 'lte']: moment(startTimeValue).toISOString(),
|
||||
[timeFieldSortDir === 'asc' ? 'gte' : 'lte']: convertTimeValueToIso(startRangeMillis, nanosValue),
|
||||
};
|
||||
const endRange = endTimeValue === null ? {} : {
|
||||
[timeSortDirection === 'asc' ? 'lte' : 'gte']: moment(endTimeValue).toISOString(),
|
||||
const endRange = endRangeMillis === null ? {} : {
|
||||
[timeFieldSortDir === 'asc' ? 'lte' : 'gte']: convertTimeValueToIso(endRangeMillis, nanosValue),
|
||||
};
|
||||
|
||||
const response = await searchSource
|
||||
|
@ -210,7 +203,7 @@ function fetchContextProvider(indexPatterns, Private) {
|
|||
constant_score: {
|
||||
filter: {
|
||||
range: {
|
||||
[timeField]: {
|
||||
[timeFieldName]: {
|
||||
format: 'strict_date_optional_time',
|
||||
...startRange,
|
||||
...endRange,
|
||||
|
@ -222,12 +215,12 @@ function fetchContextProvider(indexPatterns, Private) {
|
|||
language: 'lucene'
|
||||
})
|
||||
.setField('searchAfter', [
|
||||
afterTimeValue !== null ? afterTimeValue : startTimeValue,
|
||||
tieBreakerValue,
|
||||
afterTimeValue,
|
||||
tieBreakerValue
|
||||
])
|
||||
.setField('sort', [
|
||||
{ [timeField]: timeSortDirection },
|
||||
{ [tieBreakerField]: timeSortDirection },
|
||||
{ [timeFieldName]: timeFieldSortDir },
|
||||
{ [tieBreakerField]: timeFieldSortDir },
|
||||
])
|
||||
.setField('version', true)
|
||||
.fetch();
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 moment from 'moment';
|
||||
/**
|
||||
* extract nanoseconds if available in ISO timestamp
|
||||
* returns the nanos as string like this:
|
||||
* 9ns -> 000000009
|
||||
* 10000ns -> 0000010000
|
||||
*/
|
||||
export function extractNanoSeconds(timeFieldValue: string = ''): string {
|
||||
const fractionSeconds = timeFieldValue.split('.')[1].replace('Z', '');
|
||||
return fractionSeconds.length !== 9 ? fractionSeconds.padEnd(9, '0') : fractionSeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* extract the nanoseconds as string of a given ISO formatted timestamp
|
||||
*/
|
||||
export function convertIsoToNanosAsStr(isoValue: string): string {
|
||||
const nanos = extractNanoSeconds(isoValue);
|
||||
const millis = convertIsoToMillis(isoValue);
|
||||
return `${millis}${nanos.substr(3, 6)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* convert an iso formatted string to number of milliseconds since
|
||||
* 1970-01-01T00:00:00.000Z
|
||||
* @param {string} isoValue
|
||||
* @returns {number}
|
||||
*/
|
||||
export function convertIsoToMillis(isoValue: string): number {
|
||||
const date = new Date(isoValue);
|
||||
return date.getTime();
|
||||
}
|
||||
/**
|
||||
* the given time value in milliseconds is converted to a ISO formatted string
|
||||
* if nanosValue is provided, the given value replaces the fractional seconds part
|
||||
* of the formated string since moment.js doesn't support formatting timestamps
|
||||
* with a higher precision then microseconds
|
||||
* The browser rounds date nanos values:
|
||||
* 2019-09-18T06:50:12.999999999 -> browser rounds to 1568789413000000000
|
||||
* 2019-09-18T06:50:59.999999999 -> browser rounds to 1568789460000000000
|
||||
* 2017-12-31T23:59:59.999999999 -> browser rounds 1514761199999999999 to 1514761200000000000
|
||||
*/
|
||||
export function convertTimeValueToIso(timeValueMillis: number, nanosValue: string): string | null {
|
||||
if (!timeValueMillis) {
|
||||
return null;
|
||||
}
|
||||
const isoString = moment(timeValueMillis).toISOString();
|
||||
if (!isoString) {
|
||||
return null;
|
||||
} else if (nanosValue !== '') {
|
||||
return `${isoString.substring(0, isoString.length - 4)}${nanosValue}Z`;
|
||||
}
|
||||
return isoString;
|
||||
}
|
|
@ -30,7 +30,7 @@ import { FAILURE_REASONS, LOADING_STATUS } from './constants';
|
|||
|
||||
export function QueryActionsProvider(Private, Promise) {
|
||||
const fetchAnchor = Private(fetchAnchorProvider);
|
||||
const { fetchPredecessors, fetchSuccessors } = Private(fetchContextProvider);
|
||||
const { fetchSurroundingDocs } = Private(fetchContextProvider);
|
||||
const {
|
||||
increasePredecessorCount,
|
||||
increaseSuccessorCount,
|
||||
|
@ -92,40 +92,45 @@ export function QueryActionsProvider(Private, Promise) {
|
|||
);
|
||||
};
|
||||
|
||||
const fetchPredecessorRows = (state) => () => {
|
||||
const fetchSurroundingRows = (type, state) => {
|
||||
const {
|
||||
queryParameters: { indexPatternId, filters, predecessorCount, sort, tieBreakerField },
|
||||
queryParameters: { indexPatternId, filters, sort, tieBreakerField },
|
||||
rows: { anchor },
|
||||
} = state;
|
||||
const count = type === 'successors'
|
||||
? state.queryParameters.successorCount
|
||||
: state.queryParameters.predecessorCount;
|
||||
|
||||
if (!tieBreakerField) {
|
||||
return Promise.reject(setFailedStatus(state)('predecessors', {
|
||||
return Promise.reject(setFailedStatus(state)(type, {
|
||||
reason: FAILURE_REASONS.INVALID_TIEBREAKER
|
||||
}));
|
||||
}
|
||||
|
||||
setLoadingStatus(state)('predecessors');
|
||||
setLoadingStatus(state)(type);
|
||||
|
||||
return Promise.try(() => (
|
||||
fetchPredecessors(
|
||||
fetchSurroundingDocs(
|
||||
type,
|
||||
indexPatternId,
|
||||
sort[0],
|
||||
sort[1],
|
||||
anchor.fields[sort[0]][0],
|
||||
anchor.sort[0],
|
||||
tieBreakerField,
|
||||
anchor.sort[1],
|
||||
predecessorCount,
|
||||
count,
|
||||
filters
|
||||
)
|
||||
))
|
||||
.then(
|
||||
(predecessorDocuments) => {
|
||||
setLoadedStatus(state)('predecessors');
|
||||
state.rows.predecessors = predecessorDocuments;
|
||||
return predecessorDocuments;
|
||||
(documents) => {
|
||||
setLoadedStatus(state)(type);
|
||||
state.rows[type] = documents;
|
||||
return documents;
|
||||
},
|
||||
(error) => {
|
||||
setFailedStatus(state)('predecessors', { error });
|
||||
setFailedStatus(state)(type, { error });
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('kbn.context.unableToLoadDocumentDescription', {
|
||||
defaultMessage: 'Unable to load documents'
|
||||
|
@ -137,53 +142,10 @@ export function QueryActionsProvider(Private, Promise) {
|
|||
);
|
||||
};
|
||||
|
||||
const fetchSuccessorRows = (state) => () => {
|
||||
const {
|
||||
queryParameters: { indexPatternId, filters, sort, successorCount, tieBreakerField },
|
||||
rows: { anchor },
|
||||
} = state;
|
||||
|
||||
if (!tieBreakerField) {
|
||||
return Promise.reject(setFailedStatus(state)('successors', {
|
||||
reason: FAILURE_REASONS.INVALID_TIEBREAKER
|
||||
}));
|
||||
}
|
||||
|
||||
setLoadingStatus(state)('successors');
|
||||
|
||||
return Promise.try(() => (
|
||||
fetchSuccessors(
|
||||
indexPatternId,
|
||||
sort[0],
|
||||
sort[1],
|
||||
anchor.sort[0],
|
||||
tieBreakerField,
|
||||
anchor.sort[1],
|
||||
successorCount,
|
||||
filters
|
||||
)
|
||||
))
|
||||
.then(
|
||||
(successorDocuments) => {
|
||||
setLoadedStatus(state)('successors');
|
||||
state.rows.successors = successorDocuments;
|
||||
return successorDocuments;
|
||||
},
|
||||
(error) => {
|
||||
setFailedStatus(state)('successors', { error });
|
||||
toastNotifications.addDanger({
|
||||
title: 'Unable to load documents',
|
||||
text: <MarkdownSimple>{error.message}</MarkdownSimple>,
|
||||
});
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const fetchContextRows = (state) => () => (
|
||||
Promise.all([
|
||||
fetchPredecessorRows(state)(),
|
||||
fetchSuccessorRows(state)(),
|
||||
fetchSurroundingRows('predecessors', state),
|
||||
fetchSurroundingRows('successors', state),
|
||||
])
|
||||
);
|
||||
|
||||
|
@ -204,22 +166,22 @@ export function QueryActionsProvider(Private, Promise) {
|
|||
|
||||
const fetchGivenPredecessorRows = (state) => (count) => {
|
||||
setPredecessorCount(state)(count);
|
||||
return fetchPredecessorRows(state)();
|
||||
return fetchSurroundingRows('predecessors', state);
|
||||
};
|
||||
|
||||
const fetchGivenSuccessorRows = (state) => (count) => {
|
||||
setSuccessorCount(state)(count);
|
||||
return fetchSuccessorRows(state)();
|
||||
return fetchSurroundingRows('successors', state);
|
||||
};
|
||||
|
||||
const fetchMorePredecessorRows = (state) => () => {
|
||||
increasePredecessorCount(state)();
|
||||
return fetchPredecessorRows(state)();
|
||||
return fetchSurroundingRows('predecessors', state);
|
||||
};
|
||||
|
||||
const fetchMoreSuccessorRows = (state) => () => {
|
||||
increaseSuccessorCount(state)();
|
||||
return fetchSuccessorRows(state)();
|
||||
return fetchSurroundingRows('successors', state);
|
||||
};
|
||||
|
||||
const setAllRows = (state) => (predecessorRows, anchorRow, successorRows) => (
|
||||
|
@ -240,8 +202,6 @@ export function QueryActionsProvider(Private, Promise) {
|
|||
fetchGivenSuccessorRows,
|
||||
fetchMorePredecessorRows,
|
||||
fetchMoreSuccessorRows,
|
||||
fetchPredecessorRows,
|
||||
fetchSuccessorRows,
|
||||
setAllRows,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ import { timefilter } from 'ui/timefilter';
|
|||
import { hasSearchStategyForIndexPattern, isDefaultTypeIndexPattern } from 'ui/courier';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { VisProvider } from 'ui/vis';
|
||||
import { VislibSeriesResponseHandlerProvider } from 'ui/vis/response_handlers/vislib';
|
||||
import { vislibSeriesResponseHandlerProvider } from 'ui/vis/response_handlers/vislib';
|
||||
import { DocTitleProvider } from 'ui/doc_title';
|
||||
import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter';
|
||||
import { intervalOptions } from 'ui/agg_types/buckets/_interval_options';
|
||||
|
@ -198,7 +198,7 @@ function discoverController(
|
|||
const Vis = Private(VisProvider);
|
||||
const docTitle = Private(DocTitleProvider);
|
||||
const queryFilter = Private(FilterBarQueryFilterProvider);
|
||||
const responseHandler = Private(VislibSeriesResponseHandlerProvider).handler;
|
||||
const responseHandler = vislibSeriesResponseHandlerProvider().handler;
|
||||
const filterManager = Private(FilterManagerProvider);
|
||||
const getUnhashableStates = Private(getUnhashableStatesProvider);
|
||||
const shareContextMenuExtensions = Private(ShareContextMenuExtensionsRegistryProvider);
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
class="euiLink"
|
||||
data-test-subj="docTableRowAction"
|
||||
ng-href="{{ getContextAppHref() }}"
|
||||
ng-if="indexPattern.isTimeBased() && !indexPattern.isTimeNanosBased()"
|
||||
ng-if="indexPattern.isTimeBased()"
|
||||
i18n-id="kbn.docTable.tableRow.viewSurroundingDocumentsLinkText"
|
||||
i18n-default-message="View surrounding documents"
|
||||
></a>
|
||||
|
|
|
@ -139,7 +139,7 @@ uiModules.get('apps/management', ['monospaced.elastic'])
|
|||
}
|
||||
};
|
||||
|
||||
const { edit: canEdit, delete: canDelete } = uiCapabilities.savedObjectsManagement[service.type];
|
||||
const { edit: canEdit, delete: canDelete } = uiCapabilities.savedObjectsManagement;
|
||||
$scope.canEdit = canEdit;
|
||||
$scope.canDelete = canDelete;
|
||||
$scope.canViewInApp = canViewInApp(uiCapabilities, service.type);
|
||||
|
|
|
@ -257,7 +257,7 @@ exports[`ObjectsTable should render normally 1`] = `
|
|||
size="xs"
|
||||
/>
|
||||
<InjectIntl(TableUI)
|
||||
canDeleteSavedObjectTypes={Array []}
|
||||
canDelete={false}
|
||||
filterOptions={
|
||||
Array [
|
||||
Object {
|
||||
|
|
|
@ -134,26 +134,12 @@ const defaultProps = {
|
|||
services: [],
|
||||
uiCapabilities: {
|
||||
savedObjectsManagement: {
|
||||
'index-pattern': {
|
||||
read: true
|
||||
},
|
||||
visualization: {
|
||||
read: true
|
||||
},
|
||||
dashboard: {
|
||||
read: true
|
||||
},
|
||||
search: {
|
||||
read: true
|
||||
}
|
||||
read: true,
|
||||
edit: false,
|
||||
delete: false,
|
||||
}
|
||||
},
|
||||
canDeleteSavedObjectTypes: [
|
||||
'index-pattern',
|
||||
'visualization',
|
||||
'dashboard',
|
||||
'search'
|
||||
]
|
||||
canDelete: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -272,41 +258,6 @@ describe('ObjectsTable', () => {
|
|||
expect(addDangerMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should filter find operation based on the uiCapabilities', async () => {
|
||||
const uiCapabilities = {
|
||||
savedObjectsManagement: {
|
||||
'index-pattern': {
|
||||
read: false,
|
||||
},
|
||||
visualization: {
|
||||
read: false,
|
||||
},
|
||||
dashboard: {
|
||||
read: false,
|
||||
},
|
||||
search: {
|
||||
read: true,
|
||||
}
|
||||
}
|
||||
};
|
||||
const customizedProps = { ...defaultProps, uiCapabilities };
|
||||
const component = shallowWithIntl(
|
||||
<ObjectsTable.WrappedComponent
|
||||
{...customizedProps}
|
||||
perPageConfig={15}
|
||||
/>
|
||||
);
|
||||
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
|
||||
expect(findObjects).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: ['search']
|
||||
}));
|
||||
});
|
||||
|
||||
describe('export', () => {
|
||||
it('should export selected objects', async () => {
|
||||
const mockSelectedSavedObjects = [
|
||||
|
@ -452,42 +403,6 @@ describe('ObjectsTable', () => {
|
|||
expect(getRelationships).toHaveBeenCalledWith('search', '1', savedObjectTypes, defaultProps.$http, defaultProps.basePath);
|
||||
});
|
||||
|
||||
it('should fetch relationships filtered based on the uiCapabilities', async () => {
|
||||
const { getRelationships } = require('../../../lib/get_relationships');
|
||||
|
||||
const uiCapabilities = {
|
||||
savedObjectsManagement: {
|
||||
'index-pattern': {
|
||||
read: false,
|
||||
},
|
||||
visualization: {
|
||||
read: false,
|
||||
},
|
||||
dashboard: {
|
||||
read: false,
|
||||
},
|
||||
search: {
|
||||
read: true,
|
||||
}
|
||||
}
|
||||
};
|
||||
const customizedProps = { ...defaultProps, uiCapabilities };
|
||||
const component = shallowWithIntl(
|
||||
<ObjectsTable.WrappedComponent
|
||||
{...customizedProps}
|
||||
/>
|
||||
);
|
||||
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
|
||||
await component.instance().getRelationships('search', '1');
|
||||
const savedObjectTypes = ['search'];
|
||||
expect(getRelationships).toHaveBeenCalledWith('search', '1', savedObjectTypes, defaultProps.$http, defaultProps.basePath);
|
||||
});
|
||||
|
||||
it('should show the flyout', async () => {
|
||||
const component = shallowWithIntl(
|
||||
<ObjectsTable.WrappedComponent
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Table restricts which saved objects can be deleted based on type 1`] = `
|
||||
exports[`Table prevents saved objects from being deleted 1`] = `
|
||||
<Fragment>
|
||||
<EuiSearchBar
|
||||
box={
|
||||
|
@ -35,7 +35,7 @@ exports[`Table restricts which saved objects can be deleted based on type 1`] =
|
|||
isDisabled={true}
|
||||
onClick={[Function]}
|
||||
size="m"
|
||||
title="Unable to delete search, index-pattern"
|
||||
title="Unable to delete saved objects"
|
||||
type="button"
|
||||
>
|
||||
<FormattedMessage
|
||||
|
@ -247,10 +247,9 @@ exports[`Table should render normally 1`] = `
|
|||
fill={false}
|
||||
iconSide="left"
|
||||
iconType="trash"
|
||||
isDisabled={true}
|
||||
isDisabled={false}
|
||||
onClick={[Function]}
|
||||
size="m"
|
||||
title="Unable to delete index-pattern"
|
||||
type="button"
|
||||
>
|
||||
<FormattedMessage
|
||||
|
|
|
@ -86,7 +86,7 @@ const defaultProps = {
|
|||
onTableChange: () => {},
|
||||
isSearching: false,
|
||||
onShowRelationships: () => {},
|
||||
canDeleteSavedObjectTypes: ['visualization']
|
||||
canDelete: true
|
||||
};
|
||||
|
||||
describe('Table', () => {
|
||||
|
@ -127,9 +127,9 @@ describe('Table', () => {
|
|||
expect(component.state().isSearchTextValid).toBe(true);
|
||||
});
|
||||
|
||||
it(`restricts which saved objects can be deleted based on type`, () => {
|
||||
it(`prevents saved objects from being deleted`, () => {
|
||||
const selectedSavedObjects = [{ type: 'visualization' }, { type: 'search' }, { type: 'index-pattern' }];
|
||||
const customizedProps = { ...defaultProps, selectedSavedObjects, canDeleteSavedObjectTypes: ['visualization'] };
|
||||
const customizedProps = { ...defaultProps, selectedSavedObjects, canDelete: false };
|
||||
const component = shallowWithIntl(
|
||||
<Table.WrappedComponent
|
||||
{...customizedProps}
|
||||
|
|
|
@ -36,6 +36,7 @@ import {
|
|||
EuiText
|
||||
} from '@elastic/eui';
|
||||
import { getDefaultTitle, getSavedObjectLabel } from '../../../../lib';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
|
||||
|
||||
class TableUI extends PureComponent {
|
||||
|
@ -47,7 +48,7 @@ class TableUI extends PureComponent {
|
|||
onSelectionChange: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
filterOptions: PropTypes.array.isRequired,
|
||||
canDeleteSavedObjectTypes: PropTypes.array.isRequired,
|
||||
canDelete: PropTypes.bool.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onExport: PropTypes.func.isRequired,
|
||||
goInspectObject: PropTypes.func.isRequired,
|
||||
|
@ -254,10 +255,6 @@ class TableUI extends PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
const unableToDeleteSavedObjectTypes = selectedSavedObjects
|
||||
.map(({ type }) => type)
|
||||
.filter(type => !this.props.canDeleteSavedObjectTypes.includes(type));
|
||||
|
||||
const button = (
|
||||
<EuiButton
|
||||
iconType="arrowDown"
|
||||
|
@ -286,10 +283,15 @@ class TableUI extends PureComponent {
|
|||
onClick={onDelete}
|
||||
isDisabled={
|
||||
selectedSavedObjects.length === 0 ||
|
||||
unableToDeleteSavedObjectTypes.length > 0
|
||||
!this.props.canDelete
|
||||
}
|
||||
title={
|
||||
unableToDeleteSavedObjectTypes.length > 0 ? `Unable to delete ${unableToDeleteSavedObjectTypes.join(', ')}` : undefined
|
||||
this.props.canDelete
|
||||
? undefined
|
||||
: i18n.translate(
|
||||
'kbn.management.objects.objectsTable.table.deleteButtonTitle',
|
||||
{ defaultMessage: 'Unable to delete saved objects' }
|
||||
)
|
||||
}
|
||||
data-test-subj="savedObjectsManagementDelete"
|
||||
>
|
||||
|
|
|
@ -82,9 +82,7 @@ class ObjectsTableUI extends Component {
|
|||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.savedObjectTypes = POSSIBLE_TYPES.filter(type => {
|
||||
return this.props.uiCapabilities.savedObjectsManagement[type].read;
|
||||
});
|
||||
this.savedObjectTypes = POSSIBLE_TYPES;
|
||||
|
||||
this.state = {
|
||||
totalCount: 0,
|
||||
|
@ -677,10 +675,6 @@ class ObjectsTableUI extends Component {
|
|||
view: `${type} (${savedObjectCounts[type] || 0})`,
|
||||
}));
|
||||
|
||||
const canDeleteSavedObjectTypes = POSSIBLE_TYPES.filter(type => {
|
||||
return this.props.uiCapabilities.savedObjectsManagement[type].delete;
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiPageContent
|
||||
horizontalPosition="center"
|
||||
|
@ -706,7 +700,7 @@ class ObjectsTableUI extends Component {
|
|||
onTableChange={this.onTableChange}
|
||||
filterOptions={filterOptions}
|
||||
onExport={this.onExport}
|
||||
canDeleteSavedObjectTypes={canDeleteSavedObjectTypes}
|
||||
canDelete={this.props.uiCapabilities.savedObjectsManagement.delete}
|
||||
onDelete={this.onDelete}
|
||||
goInspectObject={this.props.goInspectObject}
|
||||
pageIndex={page}
|
||||
|
|
|
@ -44,17 +44,13 @@ describe('aggLookup', () => {
|
|||
it('returns options for basic', () => {
|
||||
const options = createOptions('basic');
|
||||
expect(options).to.have.length(15);
|
||||
expect(options.every(opt => isBasicAgg({ type: opt.value }))).to.equal(
|
||||
true
|
||||
);
|
||||
expect(options.every(opt => isBasicAgg({ type: opt.value }))).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns options for pipeline', () => {
|
||||
const options = createOptions('pipeline');
|
||||
expect(options).to.have.length(15);
|
||||
expect(options.every(opt => !isBasicAgg({ type: opt.value }))).to.equal(
|
||||
true
|
||||
);
|
||||
expect(options.every(opt => !isBasicAgg({ type: opt.value }))).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns options for all if given unknown key', () => {
|
||||
|
|
|
@ -49,20 +49,14 @@ describe('calculateLabel(metric, metrics)', () => {
|
|||
|
||||
it('returns formated label for pipeline aggs', () => {
|
||||
const metric = { id: 2, type: 'derivative', field: 1 };
|
||||
const metrics = [
|
||||
{ id: 1, type: 'max', field: 'network.out.bytes' },
|
||||
metric
|
||||
];
|
||||
const metrics = [{ id: 1, type: 'max', field: 'network.out.bytes' }, metric];
|
||||
const label = calculateLabel(metric, metrics);
|
||||
expect(label).to.equal('Derivative of Max of network.out.bytes');
|
||||
});
|
||||
|
||||
it('returns formated label for derivative of percentile', () => {
|
||||
const metric = { id: 2, type: 'derivative', field: '1[50.0]' };
|
||||
const metrics = [
|
||||
{ id: 1, type: 'percentile', field: 'network.out.bytes' },
|
||||
metric
|
||||
];
|
||||
const metrics = [{ id: 1, type: 'percentile', field: 'network.out.bytes' }, metric];
|
||||
const label = calculateLabel(metric, metrics);
|
||||
expect(label).to.equal('Derivative of Percentile of network.out.bytes (50.0)');
|
||||
});
|
||||
|
@ -72,7 +66,7 @@ describe('calculateLabel(metric, metrics)', () => {
|
|||
const metrics = [
|
||||
{ id: 1, type: 'max', field: 'network.out.bytes' },
|
||||
{ id: 2, type: 'moving_average', field: 1 },
|
||||
metric
|
||||
metric,
|
||||
];
|
||||
const label = calculateLabel(metric, metrics);
|
||||
expect(label).to.equal('Derivative of Moving Average of Max of network.out.bytes');
|
||||
|
@ -82,10 +76,9 @@ describe('calculateLabel(metric, metrics)', () => {
|
|||
const metric = { id: 2, type: 'derivative', field: 1 };
|
||||
const metrics = [
|
||||
{ id: 1, type: 'max', field: 'network.out.bytes', alias: 'Outbound Traffic' },
|
||||
metric
|
||||
metric,
|
||||
];
|
||||
const label = calculateLabel(metric, metrics);
|
||||
expect(label).to.equal('Derivative of Outbound Traffic');
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -23,7 +23,9 @@ import { i18n } from '@kbn/i18n';
|
|||
export const lookup = {
|
||||
count: i18n.translate('tsvb.aggLookup.countLabel', { defaultMessage: 'Count' }),
|
||||
calculation: i18n.translate('tsvb.aggLookup.calculationLabel', { defaultMessage: 'Calculation' }),
|
||||
std_deviation: i18n.translate('tsvb.aggLookup.deviationLabel', { defaultMessage: 'Std. Deviation' }),
|
||||
std_deviation: i18n.translate('tsvb.aggLookup.deviationLabel', {
|
||||
defaultMessage: 'Std. Deviation',
|
||||
}),
|
||||
variance: i18n.translate('tsvb.aggLookup.varianceLabel', { defaultMessage: 'Variance' }),
|
||||
sum_of_squares: i18n.translate('tsvb.aggLookup.sumOfSqLabel', { defaultMessage: 'Sum of Sq.' }),
|
||||
avg: i18n.translate('tsvb.aggLookup.averageLabel', { defaultMessage: 'Average' }),
|
||||
|
@ -31,24 +33,44 @@ export const lookup = {
|
|||
min: i18n.translate('tsvb.aggLookup.minLabel', { defaultMessage: 'Min' }),
|
||||
sum: i18n.translate('tsvb.aggLookup.sumLabel', { defaultMessage: 'Sum' }),
|
||||
percentile: i18n.translate('tsvb.aggLookup.percentileLabel', { defaultMessage: 'Percentile' }),
|
||||
percentile_rank: i18n.translate('tsvb.aggLookup.percentileRankLabel', { defaultMessage: 'Percentile Rank' }),
|
||||
percentile_rank: i18n.translate('tsvb.aggLookup.percentileRankLabel', {
|
||||
defaultMessage: 'Percentile Rank',
|
||||
}),
|
||||
cardinality: i18n.translate('tsvb.aggLookup.cardinalityLabel', { defaultMessage: 'Cardinality' }),
|
||||
value_count: i18n.translate('tsvb.aggLookup.valueCountLabel', { defaultMessage: 'Value Count' }),
|
||||
derivative: i18n.translate('tsvb.aggLookup.derivativeLabel', { defaultMessage: 'Derivative' }),
|
||||
cumulative_sum: i18n.translate('tsvb.aggLookup.cumulativeSumLabel', { defaultMessage: 'Cumulative Sum' }),
|
||||
moving_average: i18n.translate('tsvb.aggLookup.movingAverageLabel', { defaultMessage: 'Moving Average' }),
|
||||
avg_bucket: i18n.translate('tsvb.aggLookup.overallAverageLabel', { defaultMessage: 'Overall Average' }),
|
||||
cumulative_sum: i18n.translate('tsvb.aggLookup.cumulativeSumLabel', {
|
||||
defaultMessage: 'Cumulative Sum',
|
||||
}),
|
||||
moving_average: i18n.translate('tsvb.aggLookup.movingAverageLabel', {
|
||||
defaultMessage: 'Moving Average',
|
||||
}),
|
||||
avg_bucket: i18n.translate('tsvb.aggLookup.overallAverageLabel', {
|
||||
defaultMessage: 'Overall Average',
|
||||
}),
|
||||
min_bucket: i18n.translate('tsvb.aggLookup.overallMinLabel', { defaultMessage: 'Overall Min' }),
|
||||
max_bucket: i18n.translate('tsvb.aggLookup.overallMaxLabel', { defaultMessage: 'Overall Max' }),
|
||||
sum_bucket: i18n.translate('tsvb.aggLookup.overallSumLabel', { defaultMessage: 'Overall Sum' }),
|
||||
variance_bucket: i18n.translate('tsvb.aggLookup.overallVarianceLabel', { defaultMessage: 'Overall Variance' }),
|
||||
sum_of_squares_bucket: i18n.translate('tsvb.aggLookup.overallSumOfSqLabel', { defaultMessage: 'Overall Sum of Sq.' }),
|
||||
std_deviation_bucket: i18n.translate('tsvb.aggLookup.overallStdDeviationLabel', { defaultMessage: 'Overall Std. Deviation' }),
|
||||
variance_bucket: i18n.translate('tsvb.aggLookup.overallVarianceLabel', {
|
||||
defaultMessage: 'Overall Variance',
|
||||
}),
|
||||
sum_of_squares_bucket: i18n.translate('tsvb.aggLookup.overallSumOfSqLabel', {
|
||||
defaultMessage: 'Overall Sum of Sq.',
|
||||
}),
|
||||
std_deviation_bucket: i18n.translate('tsvb.aggLookup.overallStdDeviationLabel', {
|
||||
defaultMessage: 'Overall Std. Deviation',
|
||||
}),
|
||||
series_agg: i18n.translate('tsvb.aggLookup.seriesAggLabel', { defaultMessage: 'Series Agg' }),
|
||||
math: i18n.translate('tsvb.aggLookup.mathLabel', { defaultMessage: 'Math' }),
|
||||
serial_diff: i18n.translate('tsvb.aggLookup.serialDifferenceLabel', { defaultMessage: 'Serial Difference' }),
|
||||
filter_ratio: i18n.translate('tsvb.aggLookup.filterRatioLabel', { defaultMessage: 'Filter Ratio' }),
|
||||
positive_only: i18n.translate('tsvb.aggLookup.positiveOnlyLabel', { defaultMessage: 'Positive Only' }),
|
||||
serial_diff: i18n.translate('tsvb.aggLookup.serialDifferenceLabel', {
|
||||
defaultMessage: 'Serial Difference',
|
||||
}),
|
||||
filter_ratio: i18n.translate('tsvb.aggLookup.filterRatioLabel', {
|
||||
defaultMessage: 'Filter Ratio',
|
||||
}),
|
||||
positive_only: i18n.translate('tsvb.aggLookup.positiveOnlyLabel', {
|
||||
defaultMessage: 'Positive Only',
|
||||
}),
|
||||
static: i18n.translate('tsvb.aggLookup.staticValueLabel', { defaultMessage: 'Static Value' }),
|
||||
top_hit: i18n.translate('tsvb.aggLookup.topHitLabel', { defaultMessage: 'Top Hit' }),
|
||||
};
|
||||
|
@ -75,15 +97,7 @@ const byType = {
|
|||
_all: lookup,
|
||||
pipeline: pipeline,
|
||||
basic: _.omit(lookup, pipeline),
|
||||
metrics: _.pick(lookup, [
|
||||
'count',
|
||||
'avg',
|
||||
'min',
|
||||
'max',
|
||||
'sum',
|
||||
'cardinality',
|
||||
'value_count',
|
||||
]),
|
||||
metrics: _.pick(lookup, ['count', 'avg', 'min', 'max', 'sum', 'cardinality', 'value_count']),
|
||||
};
|
||||
|
||||
export function isBasicAgg(item) {
|
||||
|
@ -101,9 +115,9 @@ export function createOptions(type = '_all', siblings = []) {
|
|||
return {
|
||||
label: disabled
|
||||
? i18n.translate('tsvb.aggLookup.addPipelineAggDescription', {
|
||||
defaultMessage: '{label} (use the "+" button to add this pipeline agg)',
|
||||
values: { label }
|
||||
})
|
||||
defaultMessage: '{label} (use the "+" button to add this pipeline agg)',
|
||||
values: { label },
|
||||
})
|
||||
: label,
|
||||
value,
|
||||
disabled,
|
||||
|
|
|
@ -27,5 +27,5 @@ export const basicAggs = [
|
|||
'variance',
|
||||
'sum_of_squares',
|
||||
'value_count',
|
||||
'cardinality'
|
||||
'cardinality',
|
||||
];
|
||||
|
|
|
@ -37,24 +37,34 @@ const paths = [
|
|||
];
|
||||
|
||||
export function calculateLabel(metric, metrics) {
|
||||
if (!metric) return i18n.translate('tsvb.calculateLabel.unknownLabel', { defaultMessage: 'Unknown' });
|
||||
if (!metric)
|
||||
return i18n.translate('tsvb.calculateLabel.unknownLabel', { defaultMessage: 'Unknown' });
|
||||
if (metric.alias) return metric.alias;
|
||||
|
||||
if (metric.type === 'count') return i18n.translate('tsvb.calculateLabel.countLabel', { defaultMessage: 'Count' });
|
||||
if (metric.type === 'count')
|
||||
return i18n.translate('tsvb.calculateLabel.countLabel', { defaultMessage: 'Count' });
|
||||
if (metric.type === 'calculation') {
|
||||
return i18n.translate('tsvb.calculateLabel.bucketScriptsLabel', { defaultMessage: 'Bucket Script' });
|
||||
return i18n.translate('tsvb.calculateLabel.bucketScriptsLabel', {
|
||||
defaultMessage: 'Bucket Script',
|
||||
});
|
||||
}
|
||||
if (metric.type === 'math') return i18n.translate('tsvb.calculateLabel.mathLabel', { defaultMessage: 'Math' });
|
||||
if (metric.type === 'math')
|
||||
return i18n.translate('tsvb.calculateLabel.mathLabel', { defaultMessage: 'Math' });
|
||||
if (metric.type === 'series_agg') {
|
||||
return i18n.translate('tsvb.calculateLabel.seriesAggLabel',
|
||||
{ defaultMessage: 'Series Agg ({metricFunction})', values: { metricFunction: metric.function } }
|
||||
);
|
||||
return i18n.translate('tsvb.calculateLabel.seriesAggLabel', {
|
||||
defaultMessage: 'Series Agg ({metricFunction})',
|
||||
values: { metricFunction: metric.function },
|
||||
});
|
||||
}
|
||||
if (metric.type === 'filter_ratio') return i18n.translate('tsvb.calculateLabel.filterRatioLabel', { defaultMessage: 'Filter Ratio' });
|
||||
if (metric.type === 'filter_ratio')
|
||||
return i18n.translate('tsvb.calculateLabel.filterRatioLabel', {
|
||||
defaultMessage: 'Filter Ratio',
|
||||
});
|
||||
if (metric.type === 'static') {
|
||||
return i18n.translate('tsvb.calculateLabel.staticValueLabel',
|
||||
{ defaultMessage: 'Static Value of {metricValue}', values: { metricValue: metric.value } }
|
||||
);
|
||||
return i18n.translate('tsvb.calculateLabel.staticValueLabel', {
|
||||
defaultMessage: 'Static Value of {metricValue}',
|
||||
values: { metricValue: metric.value },
|
||||
});
|
||||
}
|
||||
|
||||
if (includes(paths, metric.type)) {
|
||||
|
@ -70,18 +80,22 @@ export function calculateLabel(metric, metrics) {
|
|||
if (matches) {
|
||||
return i18n.translate('tsvb.calculateLabel.lookupMetricTypeOfTargetWithAdditionalLabel', {
|
||||
defaultMessage: '{lookupMetricType} of {targetLabel} ({additionalLabel})',
|
||||
values: { lookupMetricType: lookup[metric.type], targetLabel, additionalLabel: matches[1] }
|
||||
values: {
|
||||
lookupMetricType: lookup[metric.type],
|
||||
targetLabel,
|
||||
additionalLabel: matches[1],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return i18n.translate('tsvb.calculateLabel.lookupMetricTypeOfTargetLabel', {
|
||||
defaultMessage: '{lookupMetricType} of {targetLabel}',
|
||||
values: { lookupMetricType: lookup[metric.type], targetLabel }
|
||||
values: { lookupMetricType: lookup[metric.type], targetLabel },
|
||||
});
|
||||
}
|
||||
|
||||
return i18n.translate('tsvb.calculateLabel.lookupMetricTypeOfMetricFieldRankLabel', {
|
||||
defaultMessage: '{lookupMetricType} of {metricField}',
|
||||
values: { lookupMetricType: lookup[metric.type], metricField: metric.field }
|
||||
values: { lookupMetricType: lookup[metric.type], metricField: metric.field },
|
||||
});
|
||||
}
|
||||
|
|
|
@ -39,10 +39,7 @@ describe('extractIndexPatterns(vis)', () => {
|
|||
series_index_pattern: 'example-2-*',
|
||||
},
|
||||
],
|
||||
annotations: [
|
||||
{ index_pattern: 'notes-*' },
|
||||
{ index_pattern: 'example-1-*' },
|
||||
],
|
||||
annotations: [{ index_pattern: 'notes-*' }, { index_pattern: 'example-1-*' }],
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
import { isArray, last } from 'lodash';
|
||||
|
||||
const DEFAULT_VALUE = 0;
|
||||
const extractValue = data => data && data[1] || null;
|
||||
const extractValue = data => (data && data[1]) || null;
|
||||
|
||||
export const getLastValue = (data, defaultValue = DEFAULT_VALUE) => {
|
||||
if (!isArray(data)) {
|
||||
|
|
|
@ -40,4 +40,3 @@ describe('getLastValue(data)', () => {
|
|||
expect(getLastValue(null, '-')).toBe('-');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -21,4 +21,3 @@ import dateMath from '@elastic/datemath';
|
|||
|
||||
export const GTE_INTERVAL_RE = new RegExp(`^>=([\\d\\.]+\\s*(${dateMath.units.join('|')}))$`);
|
||||
export const INTERVAL_STRING_RE = new RegExp(`^([\\d\\.]+)\\s*(${dateMath.units.join('|')})$`);
|
||||
|
||||
|
|
|
@ -32,5 +32,5 @@ export const METRIC_TYPES = {
|
|||
export const EXTENDED_STATS_TYPES = [
|
||||
METRIC_TYPES.STD_DEVIATION,
|
||||
METRIC_TYPES.VARIANCE,
|
||||
METRIC_TYPES.SUM_OF_SQUARES
|
||||
METRIC_TYPES.SUM_OF_SQUARES,
|
||||
];
|
||||
|
|
|
@ -25,7 +25,7 @@ const IS_DARK_THEME = chrome.getUiSettingsClient().get('theme:darkMode');
|
|||
/**
|
||||
* Returns true if the color that is passed has low luminosity
|
||||
*/
|
||||
const isColorDark = (c) => {
|
||||
const isColorDark = c => {
|
||||
return color(c).luminosity() < 0.45;
|
||||
};
|
||||
|
||||
|
@ -33,7 +33,7 @@ const isColorDark = (c) => {
|
|||
* Checks to see if the `currentTheme` is dark in luminosity.
|
||||
* Defaults to checking `theme:darkMode`.
|
||||
*/
|
||||
export const isThemeDark = (currentTheme) => {
|
||||
export const isThemeDark = currentTheme => {
|
||||
let themeIsDark = currentTheme || IS_DARK_THEME;
|
||||
|
||||
// If passing a string, check the luminosity
|
||||
|
@ -68,5 +68,3 @@ export const isBackgroundInverted = (backgroundColor, currentTheme) => {
|
|||
const themeIsDark = isThemeDark(currentTheme);
|
||||
return backgroundIsDark !== themeIsDark;
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -23,14 +23,12 @@ import { fieldsRoutes } from './server/routes/fields';
|
|||
import { visDataRoutes } from './server/routes/vis';
|
||||
import { SearchStrategiesRegister } from './server/lib/search_strategies/search_strategies_register';
|
||||
|
||||
export default function (kibana) {
|
||||
export default function(kibana) {
|
||||
return new kibana.Plugin({
|
||||
require: ['kibana', 'elasticsearch'],
|
||||
|
||||
uiExports: {
|
||||
visTypes: [
|
||||
'plugins/metrics/kbn_vis_types',
|
||||
],
|
||||
visTypes: ['plugins/metrics/kbn_vis_types'],
|
||||
interpreter: ['plugins/metrics/tsvb_fn'],
|
||||
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
|
||||
},
|
||||
|
|
|
@ -86,12 +86,14 @@ export function AddDeleteButtons(props) {
|
|||
let activatePanelBtn = null;
|
||||
|
||||
if (isBoolean(props.isPanelActive)) {
|
||||
const tooltip = props.isPanelActive ? props.deactivatePanelTooltip : props.activatePanelTooltip;
|
||||
const tooltip = props.isPanelActive
|
||||
? props.deactivatePanelTooltip
|
||||
: props.activatePanelTooltip;
|
||||
const iconType = props.isPanelActive ? 'eye' : 'eyeClosed';
|
||||
|
||||
activatePanelBtn = (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip content={tooltip} >
|
||||
<EuiToolTip content={tooltip}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={`${testSubj}ActivatePanelBtn`}
|
||||
aria-label={tooltip}
|
||||
|
@ -123,12 +125,24 @@ export function AddDeleteButtons(props) {
|
|||
|
||||
AddDeleteButtons.defaultProps = {
|
||||
testSubj: 'Add',
|
||||
activeTooltip: i18n.translate('tsvb.addDeleteButtons.addButtonDefaultTooltip', { defaultMessage: 'Add' }),
|
||||
addTooltip: i18n.translate('tsvb.addDeleteButtons.addButtonDefaultTooltip', { defaultMessage: 'Add' }),
|
||||
deleteTooltip: i18n.translate('tsvb.addDeleteButtons.deleteButtonDefaultTooltip', { defaultMessage: 'Delete' }),
|
||||
cloneTooltip: i18n.translate('tsvb.addDeleteButtons.cloneButtonDefaultTooltip', { defaultMessage: 'Clone' }),
|
||||
activatePanelTooltip: i18n.translate('tsvb.addDeleteButtons.reEnableTooltip', { defaultMessage: 'Re-enable' }),
|
||||
deactivatePanelTooltip: i18n.translate('tsvb.addDeleteButtons.temporarilyDisableTooltip', { defaultMessage: 'Temporarily Disable' }),
|
||||
activeTooltip: i18n.translate('tsvb.addDeleteButtons.addButtonDefaultTooltip', {
|
||||
defaultMessage: 'Add',
|
||||
}),
|
||||
addTooltip: i18n.translate('tsvb.addDeleteButtons.addButtonDefaultTooltip', {
|
||||
defaultMessage: 'Add',
|
||||
}),
|
||||
deleteTooltip: i18n.translate('tsvb.addDeleteButtons.deleteButtonDefaultTooltip', {
|
||||
defaultMessage: 'Delete',
|
||||
}),
|
||||
cloneTooltip: i18n.translate('tsvb.addDeleteButtons.cloneButtonDefaultTooltip', {
|
||||
defaultMessage: 'Clone',
|
||||
}),
|
||||
activatePanelTooltip: i18n.translate('tsvb.addDeleteButtons.reEnableTooltip', {
|
||||
defaultMessage: 'Re-enable',
|
||||
}),
|
||||
deactivatePanelTooltip: i18n.translate('tsvb.addDeleteButtons.temporarilyDisableTooltip', {
|
||||
defaultMessage: 'Temporarily Disable',
|
||||
}),
|
||||
};
|
||||
|
||||
AddDeleteButtons.propTypes = {
|
||||
|
|
|
@ -26,57 +26,52 @@ import { AddDeleteButtons } from './add_delete_buttons';
|
|||
describe('AddDeleteButtons', () => {
|
||||
it('calls onAdd={handleAdd}', () => {
|
||||
const handleAdd = sinon.spy();
|
||||
const wrapper = shallowWithIntl(
|
||||
<AddDeleteButtons onAdd={handleAdd} />
|
||||
);
|
||||
wrapper.find('EuiButtonIcon').at(0).simulate('click');
|
||||
const wrapper = shallowWithIntl(<AddDeleteButtons onAdd={handleAdd} />);
|
||||
wrapper
|
||||
.find('EuiButtonIcon')
|
||||
.at(0)
|
||||
.simulate('click');
|
||||
expect(handleAdd.calledOnce).to.equal(true);
|
||||
});
|
||||
|
||||
it('calls onDelete={handleDelete}', () => {
|
||||
const handleDelete = sinon.spy();
|
||||
const wrapper = shallowWithIntl(
|
||||
<AddDeleteButtons onDelete={handleDelete} />
|
||||
);
|
||||
wrapper.find('EuiButtonIcon').at(1).simulate('click');
|
||||
const wrapper = shallowWithIntl(<AddDeleteButtons onDelete={handleDelete} />);
|
||||
wrapper
|
||||
.find('EuiButtonIcon')
|
||||
.at(1)
|
||||
.simulate('click');
|
||||
expect(handleDelete.calledOnce).to.equal(true);
|
||||
});
|
||||
|
||||
it('calls onClone={handleClone}', () => {
|
||||
const handleClone = sinon.spy();
|
||||
const wrapper = shallowWithIntl(
|
||||
<AddDeleteButtons onClone={handleClone} />
|
||||
);
|
||||
wrapper.find('EuiButtonIcon').at(0).simulate('click');
|
||||
const wrapper = shallowWithIntl(<AddDeleteButtons onClone={handleClone} />);
|
||||
wrapper
|
||||
.find('EuiButtonIcon')
|
||||
.at(0)
|
||||
.simulate('click');
|
||||
expect(handleClone.calledOnce).to.equal(true);
|
||||
});
|
||||
|
||||
it('disableDelete={true}', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<AddDeleteButtons disableDelete={true} />
|
||||
);
|
||||
const wrapper = shallowWithIntl(<AddDeleteButtons disableDelete={true} />);
|
||||
expect(wrapper.find({ text: 'Delete' })).to.have.length(0);
|
||||
});
|
||||
|
||||
it('disableAdd={true}', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<AddDeleteButtons disableAdd={true} />
|
||||
);
|
||||
const wrapper = shallowWithIntl(<AddDeleteButtons disableAdd={true} />);
|
||||
expect(wrapper.find({ text: 'Add' })).to.have.length(0);
|
||||
});
|
||||
|
||||
it('should not display clone by default', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<AddDeleteButtons />
|
||||
);
|
||||
const wrapper = shallowWithIntl(<AddDeleteButtons />);
|
||||
expect(wrapper.find({ text: 'Clone' })).to.have.length(0);
|
||||
});
|
||||
|
||||
it('should not display clone when disableAdd={true}', () => {
|
||||
const fn = sinon.spy();
|
||||
const wrapper = shallowWithIntl(
|
||||
<AddDeleteButtons onClone={fn} disableAdd={true} />
|
||||
);
|
||||
const wrapper = shallowWithIntl(<AddDeleteButtons onClone={fn} disableAdd={true} />);
|
||||
expect(wrapper.find({ text: 'Clone' })).to.have.length(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -42,10 +42,7 @@ export function Agg(props) {
|
|||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={props.className}
|
||||
style={style}
|
||||
>
|
||||
<div className={props.className} style={style}>
|
||||
<Component
|
||||
fields={props.fields}
|
||||
disableDelete={props.disableDelete}
|
||||
|
|
|
@ -38,21 +38,33 @@ function AggRowUi(props) {
|
|||
|
||||
return (
|
||||
<div className="tvbAggRow">
|
||||
<EuiFlexGroup data-test-subj="aggRow" gutterSize="s" alignItems="flexStart" responsive={false}>
|
||||
<EuiFlexGroup
|
||||
data-test-subj="aggRow"
|
||||
gutterSize="s"
|
||||
alignItems="flexStart"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon className="tvbAggRow__visibilityIcon" type={iconType} color={iconColor} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem className="tvbAggRow__children">
|
||||
{props.children}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem className="tvbAggRow__children">{props.children}</EuiFlexItem>
|
||||
|
||||
<SeriesDragHandler dragHandleProps={props.dragHandleProps} hideDragHandler={props.disableDelete} />
|
||||
<SeriesDragHandler
|
||||
dragHandleProps={props.dragHandleProps}
|
||||
hideDragHandler={props.disableDelete}
|
||||
/>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<AddDeleteButtons
|
||||
testSubj="addMetric"
|
||||
addTooltip={intl.formatMessage({ id: 'tsvb.aggRow.addMetricButtonTooltip', defaultMessage: 'Add Metric' })}
|
||||
deleteTooltip={intl.formatMessage({ id: 'tsvb.aggRow.deleteMetricButtonTooltip', defaultMessage: 'Delete Metric' })}
|
||||
addTooltip={intl.formatMessage({
|
||||
id: 'tsvb.aggRow.addMetricButtonTooltip',
|
||||
defaultMessage: 'Add Metric',
|
||||
})}
|
||||
deleteTooltip={intl.formatMessage({
|
||||
id: 'tsvb.aggRow.deleteMetricButtonTooltip',
|
||||
defaultMessage: 'Delete Metric',
|
||||
})}
|
||||
onAdd={props.onAdd}
|
||||
onDelete={props.onDelete}
|
||||
disableDelete={props.disableDelete}
|
||||
|
|
|
@ -30,137 +30,179 @@ const metricAggs = [
|
|||
value: 'avg',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.metricsAggs.cardinalityLabel', { defaultMessage: 'Cardinality' }),
|
||||
label: i18n.translate('tsvb.aggSelect.metricsAggs.cardinalityLabel', {
|
||||
defaultMessage: 'Cardinality',
|
||||
}),
|
||||
value: 'cardinality',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.metricsAggs.countLabel', { defaultMessage: 'Count' }),
|
||||
value: 'count'
|
||||
value: 'count',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.metricsAggs.filterRatioLabel', { defaultMessage: 'Filter Ratio' }),
|
||||
value: 'filter_ratio'
|
||||
label: i18n.translate('tsvb.aggSelect.metricsAggs.filterRatioLabel', {
|
||||
defaultMessage: 'Filter Ratio',
|
||||
}),
|
||||
value: 'filter_ratio',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.metricsAggs.maxLabel', { defaultMessage: 'Max' }),
|
||||
value: 'max'
|
||||
value: 'max',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.metricsAggs.minLabel', { defaultMessage: 'Min' }),
|
||||
value: 'min'
|
||||
value: 'min',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.metricsAggs.percentileLabel', { defaultMessage: 'Percentile' }),
|
||||
value: 'percentile'
|
||||
label: i18n.translate('tsvb.aggSelect.metricsAggs.percentileLabel', {
|
||||
defaultMessage: 'Percentile',
|
||||
}),
|
||||
value: 'percentile',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.metricsAggs.percentileRankLabel', { defaultMessage: 'Percentile Rank' }),
|
||||
value: 'percentile_rank'
|
||||
label: i18n.translate('tsvb.aggSelect.metricsAggs.percentileRankLabel', {
|
||||
defaultMessage: 'Percentile Rank',
|
||||
}),
|
||||
value: 'percentile_rank',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.metricsAggs.staticValueLabel', { defaultMessage: 'Static Value' }),
|
||||
value: 'static'
|
||||
label: i18n.translate('tsvb.aggSelect.metricsAggs.staticValueLabel', {
|
||||
defaultMessage: 'Static Value',
|
||||
}),
|
||||
value: 'static',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.metricsAggs.stdDeviationLabel', { defaultMessage: 'Std. Deviation' }),
|
||||
value: 'std_deviation'
|
||||
label: i18n.translate('tsvb.aggSelect.metricsAggs.stdDeviationLabel', {
|
||||
defaultMessage: 'Std. Deviation',
|
||||
}),
|
||||
value: 'std_deviation',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.metricsAggs.sumLabel', { defaultMessage: 'Sum' }),
|
||||
value: 'sum'
|
||||
value: 'sum',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.metricsAggs.sumOfSquaresLabel', { defaultMessage: 'Sum of Squares' }),
|
||||
value: 'sum_of_squares'
|
||||
label: i18n.translate('tsvb.aggSelect.metricsAggs.sumOfSquaresLabel', {
|
||||
defaultMessage: 'Sum of Squares',
|
||||
}),
|
||||
value: 'sum_of_squares',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.metricsAggs.topHitLabel', { defaultMessage: 'Top Hit' }),
|
||||
value: 'top_hit'
|
||||
value: 'top_hit',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.metricsAggs.valueCountLabel', { defaultMessage: 'Value Count' }),
|
||||
value: 'value_count'
|
||||
label: i18n.translate('tsvb.aggSelect.metricsAggs.valueCountLabel', {
|
||||
defaultMessage: 'Value Count',
|
||||
}),
|
||||
value: 'value_count',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.metricsAggs.varianceLabel', { defaultMessage: 'Variance' }),
|
||||
value: 'variance'
|
||||
label: i18n.translate('tsvb.aggSelect.metricsAggs.varianceLabel', {
|
||||
defaultMessage: 'Variance',
|
||||
}),
|
||||
value: 'variance',
|
||||
},
|
||||
];
|
||||
|
||||
const pipelineAggs = [
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.pipelineAggs.bucketScriptLabel', { defaultMessage: 'Bucket Script' }),
|
||||
value: 'calculation'
|
||||
label: i18n.translate('tsvb.aggSelect.pipelineAggs.bucketScriptLabel', {
|
||||
defaultMessage: 'Bucket Script',
|
||||
}),
|
||||
value: 'calculation',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.pipelineAggs.cumulativeSumLabel', { defaultMessage: 'Cumulative Sum' }),
|
||||
value: 'cumulative_sum'
|
||||
label: i18n.translate('tsvb.aggSelect.pipelineAggs.cumulativeSumLabel', {
|
||||
defaultMessage: 'Cumulative Sum',
|
||||
}),
|
||||
value: 'cumulative_sum',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.pipelineAggs.derivativeLabel', { defaultMessage: 'Derivative' }),
|
||||
value: 'derivative'
|
||||
label: i18n.translate('tsvb.aggSelect.pipelineAggs.derivativeLabel', {
|
||||
defaultMessage: 'Derivative',
|
||||
}),
|
||||
value: 'derivative',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.pipelineAggs.movingAverageLabel', { defaultMessage: 'Moving Average' }),
|
||||
value: 'moving_average'
|
||||
label: i18n.translate('tsvb.aggSelect.pipelineAggs.movingAverageLabel', {
|
||||
defaultMessage: 'Moving Average',
|
||||
}),
|
||||
value: 'moving_average',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.pipelineAggs.positiveOnlyLabel', { defaultMessage: 'Positive Only' }),
|
||||
value: 'positive_only'
|
||||
label: i18n.translate('tsvb.aggSelect.pipelineAggs.positiveOnlyLabel', {
|
||||
defaultMessage: 'Positive Only',
|
||||
}),
|
||||
value: 'positive_only',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.pipelineAggs.serialDifferenceLabel', { defaultMessage: 'Serial Difference' }),
|
||||
value: 'serial_diff'
|
||||
label: i18n.translate('tsvb.aggSelect.pipelineAggs.serialDifferenceLabel', {
|
||||
defaultMessage: 'Serial Difference',
|
||||
}),
|
||||
value: 'serial_diff',
|
||||
},
|
||||
];
|
||||
|
||||
const siblingAggs = [
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.siblingAggs.overallAverageLabel', { defaultMessage: 'Overall Average' }),
|
||||
value: 'avg_bucket' },
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.siblingAggs.overallMaxLabel', { defaultMessage: 'Overall Max' }),
|
||||
value: 'max_bucket'
|
||||
label: i18n.translate('tsvb.aggSelect.siblingAggs.overallAverageLabel', {
|
||||
defaultMessage: 'Overall Average',
|
||||
}),
|
||||
value: 'avg_bucket',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.siblingAggs.overallMinLabel', { defaultMessage: 'Overall Min' }),
|
||||
value: 'min_bucket'
|
||||
label: i18n.translate('tsvb.aggSelect.siblingAggs.overallMaxLabel', {
|
||||
defaultMessage: 'Overall Max',
|
||||
}),
|
||||
value: 'max_bucket',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.siblingAggs.overallStdDeviationLabel', { defaultMessage: 'Overall Std. Deviation' }),
|
||||
value: 'std_deviation_bucket'
|
||||
label: i18n.translate('tsvb.aggSelect.siblingAggs.overallMinLabel', {
|
||||
defaultMessage: 'Overall Min',
|
||||
}),
|
||||
value: 'min_bucket',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.siblingAggs.overallSumLabel', { defaultMessage: 'Overall Sum' }),
|
||||
value: 'sum_bucket'
|
||||
label: i18n.translate('tsvb.aggSelect.siblingAggs.overallStdDeviationLabel', {
|
||||
defaultMessage: 'Overall Std. Deviation',
|
||||
}),
|
||||
value: 'std_deviation_bucket',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.siblingAggs.overallSumOfSquaresLabel', { defaultMessage: 'Overall Sum of Squares' }),
|
||||
value: 'sum_of_squares_bucket'
|
||||
label: i18n.translate('tsvb.aggSelect.siblingAggs.overallSumLabel', {
|
||||
defaultMessage: 'Overall Sum',
|
||||
}),
|
||||
value: 'sum_bucket',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.siblingAggs.overallVarianceLabel', { defaultMessage: 'Overall Variance' }),
|
||||
value: 'variance_bucket'
|
||||
label: i18n.translate('tsvb.aggSelect.siblingAggs.overallSumOfSquaresLabel', {
|
||||
defaultMessage: 'Overall Sum of Squares',
|
||||
}),
|
||||
value: 'sum_of_squares_bucket',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.siblingAggs.overallVarianceLabel', {
|
||||
defaultMessage: 'Overall Variance',
|
||||
}),
|
||||
value: 'variance_bucket',
|
||||
},
|
||||
];
|
||||
|
||||
const specialAggs = [
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.specialAggs.seriesAggLabel', { defaultMessage: 'Series Agg' }),
|
||||
value: 'series_agg'
|
||||
label: i18n.translate('tsvb.aggSelect.specialAggs.seriesAggLabel', {
|
||||
defaultMessage: 'Series Agg',
|
||||
}),
|
||||
value: 'series_agg',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('tsvb.aggSelect.specialAggs.mathLabel', { defaultMessage: 'Math' }),
|
||||
value: 'math'
|
||||
value: 'math',
|
||||
},
|
||||
];
|
||||
|
||||
const allAggOptions = [
|
||||
...metricAggs,
|
||||
...pipelineAggs,
|
||||
...siblingAggs,
|
||||
...specialAggs
|
||||
];
|
||||
const allAggOptions = [...metricAggs, ...pipelineAggs, ...siblingAggs, ...specialAggs];
|
||||
|
||||
function filterByPanelType(panelType) {
|
||||
return agg => {
|
||||
|
@ -176,9 +218,7 @@ function AggSelectUi(props) {
|
|||
return value === option.value && isMetricEnabled(option.value, uiRestrictions);
|
||||
});
|
||||
|
||||
let enablePipelines = siblings.some(
|
||||
s => !!metricAggs.find(m => m.value === s.type)
|
||||
);
|
||||
let enablePipelines = siblings.some(s => !!metricAggs.find(m => m.value === s.type));
|
||||
|
||||
if (siblings.length <= 1) enablePipelines = false;
|
||||
|
||||
|
@ -186,28 +226,41 @@ function AggSelectUi(props) {
|
|||
if (panelType === 'metrics') {
|
||||
options = metricAggs;
|
||||
} else {
|
||||
const disableSiblingAggs = agg => ({ ...agg, disabled: !enablePipelines || !isMetricEnabled(agg.value, uiRestrictions) });
|
||||
const disableSiblingAggs = agg => ({
|
||||
...agg,
|
||||
disabled: !enablePipelines || !isMetricEnabled(agg.value, uiRestrictions),
|
||||
});
|
||||
|
||||
options = [
|
||||
{
|
||||
label: intl.formatMessage({ id: 'tsvb.aggSelect.aggGroups.metricAggLabel', defaultMessage: 'Metric Aggregations' }),
|
||||
options: metricAggs
|
||||
.map(agg => ({ ...agg, disabled: !isMetricEnabled(agg.value, uiRestrictions) })),
|
||||
label: intl.formatMessage({
|
||||
id: 'tsvb.aggSelect.aggGroups.metricAggLabel',
|
||||
defaultMessage: 'Metric Aggregations',
|
||||
}),
|
||||
options: metricAggs.map(agg => ({
|
||||
...agg,
|
||||
disabled: !isMetricEnabled(agg.value, uiRestrictions),
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: 'tsvb.aggSelect.aggGroups.parentPipelineAggLabel', defaultMessage: 'Parent Pipeline Aggregations' }),
|
||||
options: pipelineAggs
|
||||
.filter(filterByPanelType(panelType))
|
||||
.map(disableSiblingAggs),
|
||||
id: 'tsvb.aggSelect.aggGroups.parentPipelineAggLabel',
|
||||
defaultMessage: 'Parent Pipeline Aggregations',
|
||||
}),
|
||||
options: pipelineAggs.filter(filterByPanelType(panelType)).map(disableSiblingAggs),
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: 'tsvb.aggSelect.aggGroups.siblingPipelineAggLabel', defaultMessage: 'Sibling Pipeline Aggregations' }),
|
||||
id: 'tsvb.aggSelect.aggGroups.siblingPipelineAggLabel',
|
||||
defaultMessage: 'Sibling Pipeline Aggregations',
|
||||
}),
|
||||
options: siblingAggs.map(disableSiblingAggs),
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: 'tsvb.aggSelect.aggGroups.specialAggLabel', defaultMessage: 'Special Aggregations' }),
|
||||
label: intl.formatMessage({
|
||||
id: 'tsvb.aggSelect.aggGroups.specialAggLabel',
|
||||
defaultMessage: 'Special Aggregations',
|
||||
}),
|
||||
options: specialAggs.map(disableSiblingAggs),
|
||||
},
|
||||
];
|
||||
|
@ -222,7 +275,10 @@ function AggSelectUi(props) {
|
|||
<div data-test-subj="aggSelector">
|
||||
<EuiComboBox
|
||||
isClearable={false}
|
||||
placeholder={intl.formatMessage({ id: 'tsvb.aggSelect.selectAggPlaceholder', defaultMessage: 'Select aggregation' })}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'tsvb.aggSelect.selectAggPlaceholder',
|
||||
defaultMessage: 'Select aggregation',
|
||||
})}
|
||||
options={options}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={handleChange}
|
||||
|
|
|
@ -30,7 +30,6 @@ import { handleAdd, handleDelete } from '../lib/collection_actions';
|
|||
const DROPPABLE_ID = 'aggs_dnd';
|
||||
|
||||
export class Aggs extends PureComponent {
|
||||
|
||||
render() {
|
||||
const { panel, model, fields, uiRestrictions } = this.props;
|
||||
const list = model.metrics;
|
||||
|
@ -38,11 +37,7 @@ export class Aggs extends PureComponent {
|
|||
const onChange = seriesChangeHandler(this.props, list);
|
||||
|
||||
return (
|
||||
<EuiDroppable
|
||||
droppableId={`${DROPPABLE_ID}:${model.id}`}
|
||||
type="MICRO"
|
||||
spacing="s"
|
||||
>
|
||||
<EuiDroppable droppableId={`${DROPPABLE_ID}:${model.id}`} type="MICRO" spacing="s">
|
||||
{list.map((row, idx) => (
|
||||
<EuiDraggable
|
||||
spacing="s"
|
||||
|
|
|
@ -41,12 +41,13 @@ import {
|
|||
} from '@elastic/eui';
|
||||
|
||||
export class CalculationAgg extends Component {
|
||||
|
||||
componentWillMount() {
|
||||
if (!this.props.model.variables) {
|
||||
this.props.onChange(_.assign({}, this.props.model, {
|
||||
variables: [{ id: uuid.v1() }]
|
||||
}));
|
||||
this.props.onChange(
|
||||
_.assign({}, this.props.model, {
|
||||
variables: [{ id: uuid.v1() }],
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,10 +91,7 @@ export class CalculationAgg extends Component {
|
|||
|
||||
<EuiFlexItem>
|
||||
<EuiFormLabel htmlFor={htmlId('variables')}>
|
||||
<FormattedMessage
|
||||
id="tsvb.calculation.variablesLabel"
|
||||
defaultMessage="Variables"
|
||||
/>
|
||||
<FormattedMessage id="tsvb.calculation.variablesLabel" defaultMessage="Variables" />
|
||||
</EuiFormLabel>
|
||||
<CalculationVars
|
||||
id={htmlId('variables')}
|
||||
|
@ -107,10 +105,12 @@ export class CalculationAgg extends Component {
|
|||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id={htmlId('painless')}
|
||||
label={(<FormattedMessage
|
||||
id="tsvb.calculation.painlessScriptLabel"
|
||||
defaultMessage="Painless Script"
|
||||
/>)}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="tsvb.calculation.painlessScriptLabel"
|
||||
defaultMessage="Painless Script"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
helpText={
|
||||
<div>
|
||||
|
@ -119,26 +119,21 @@ export class CalculationAgg extends Component {
|
|||
defaultMessage="Variables are keys on the {params} object, i.e. {paramsName}. To access the bucket
|
||||
interval (in milliseconds) use {paramsInterval}."
|
||||
values={{
|
||||
params: (<EuiCode>params</EuiCode>),
|
||||
paramsName: (<EuiCode>params.<name></EuiCode>),
|
||||
paramsInterval: (<EuiCode>params._interval</EuiCode>)
|
||||
params: <EuiCode>params</EuiCode>,
|
||||
paramsName: <EuiCode>params.<name></EuiCode>,
|
||||
paramsInterval: <EuiCode>params._interval</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<EuiTextArea
|
||||
onChange={handleTextChange('script')}
|
||||
value={model.script}
|
||||
fullWidth
|
||||
/>
|
||||
<EuiTextArea onChange={handleTextChange('script')} value={model.script} fullWidth />
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</AggRow>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
CalculationAgg.propTypes = {
|
||||
|
|
|
@ -60,10 +60,7 @@ export function CumulativeSumAgg(props) {
|
|||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id={htmlId('metric')}
|
||||
label={(<FormattedMessage
|
||||
id="tsvb.cumulativeSum.metricLabel"
|
||||
defaultMessage="Metric"
|
||||
/>)}
|
||||
label={<FormattedMessage id="tsvb.cumulativeSum.metricLabel" defaultMessage="Metric" />}
|
||||
>
|
||||
<MetricSelect
|
||||
onChange={handleSelectChange('field')}
|
||||
|
@ -76,7 +73,6 @@ export function CumulativeSumAgg(props) {
|
|||
</EuiFlexGroup>
|
||||
</AggRow>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
CumulativeSumAgg.propTypes = {
|
||||
|
|
|
@ -59,10 +59,7 @@ export const DerivativeAgg = props => {
|
|||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiFormLabel htmlFor={htmlId('aggregation')}>
|
||||
<FormattedMessage
|
||||
id="tsvb.derivative.aggregationLabel"
|
||||
defaultMessage="Aggregation"
|
||||
/>
|
||||
<FormattedMessage id="tsvb.derivative.aggregationLabel" defaultMessage="Aggregation" />
|
||||
</EuiFormLabel>
|
||||
<AggSelect
|
||||
id={htmlId('aggregation')}
|
||||
|
@ -76,10 +73,7 @@ export const DerivativeAgg = props => {
|
|||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id={htmlId('metric')}
|
||||
label={(<FormattedMessage
|
||||
id="tsvb.derivative.metricLabel"
|
||||
defaultMessage="Metric"
|
||||
/>)}
|
||||
label={<FormattedMessage id="tsvb.derivative.metricLabel" defaultMessage="Metric" />}
|
||||
fullWidth
|
||||
>
|
||||
<MetricSelect
|
||||
|
@ -94,18 +88,16 @@ export const DerivativeAgg = props => {
|
|||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id={htmlId('units')}
|
||||
label={(<FormattedMessage
|
||||
id="tsvb.derivative.unitsLabel"
|
||||
defaultMessage="Units (1s, 1m, etc)"
|
||||
description="1s and 1m are required values and must not be translated."
|
||||
/>)}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="tsvb.derivative.unitsLabel"
|
||||
defaultMessage="Units (1s, 1m, etc)"
|
||||
description="1s and 1m are required values and must not be translated."
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFieldText
|
||||
onChange={handleTextChange('unit')}
|
||||
value={model.unit}
|
||||
fullWidth
|
||||
/>
|
||||
<EuiFieldText onChange={handleTextChange('unit')} value={model.unit} fullWidth />
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -19,9 +19,7 @@
|
|||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {
|
||||
EuiComboBox,
|
||||
} from '@elastic/eui';
|
||||
import { EuiComboBox } from '@elastic/eui';
|
||||
import { injectI18n } from '@kbn/i18n/react';
|
||||
import { isFieldEnabled } from '../../lib/check_ui_restrictions';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -41,35 +39,39 @@ function FieldSelectUi({
|
|||
uiRestrictions,
|
||||
...rest
|
||||
}) {
|
||||
|
||||
if (type === 'count') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedOptions = [];
|
||||
const options = Object.values((fields[indexPattern] || []).reduce((acc, field) => {
|
||||
if (isFieldTypeEnabled(restrict, field.type) && isFieldEnabled(field.name, type, uiRestrictions)) {
|
||||
const item = {
|
||||
label: field.name,
|
||||
value: field.name
|
||||
};
|
||||
|
||||
if (acc[field.type]) {
|
||||
acc[field.type].options.push(item);
|
||||
} else {
|
||||
acc[field.type] = {
|
||||
options: [item],
|
||||
label: field.type,
|
||||
const options = Object.values(
|
||||
(fields[indexPattern] || []).reduce((acc, field) => {
|
||||
if (
|
||||
isFieldTypeEnabled(restrict, field.type) &&
|
||||
isFieldEnabled(field.name, type, uiRestrictions)
|
||||
) {
|
||||
const item = {
|
||||
label: field.name,
|
||||
value: field.name,
|
||||
};
|
||||
|
||||
if (acc[field.type]) {
|
||||
acc[field.type].options.push(item);
|
||||
} else {
|
||||
acc[field.type] = {
|
||||
options: [item],
|
||||
label: field.type,
|
||||
};
|
||||
}
|
||||
|
||||
if (value === item.value) {
|
||||
selectedOptions.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (value === item.value) {
|
||||
selectedOptions.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {}));
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
|
||||
if (onChange && value && !selectedOptions.length) {
|
||||
onChange();
|
||||
|
@ -94,7 +96,7 @@ FieldSelectUi.defaultProps = {
|
|||
restrict: [],
|
||||
placeholder: i18n.translate('tsvb.fieldSelect.selectFieldPlaceholder', {
|
||||
defaultMessage: 'Select field...',
|
||||
})
|
||||
}),
|
||||
};
|
||||
|
||||
FieldSelectUi.propTypes = {
|
||||
|
|
|
@ -39,21 +39,18 @@ import { ES_TYPES } from '../../../common/es_types';
|
|||
import { METRIC_TYPES } from '../../../common/metric_types';
|
||||
|
||||
export const FilterRatioAgg = props => {
|
||||
const {
|
||||
series,
|
||||
fields,
|
||||
panel
|
||||
} = props;
|
||||
const { series, fields, panel } = props;
|
||||
|
||||
const handleChange = createChangeHandler(props.onChange, props.model);
|
||||
const handleSelectChange = createSelectHandler(handleChange);
|
||||
const handleTextChange = createTextHandler(handleChange);
|
||||
const indexPattern = series.override_index_pattern && series.series_index_pattern || panel.index_pattern;
|
||||
const indexPattern =
|
||||
(series.override_index_pattern && series.series_index_pattern) || panel.index_pattern;
|
||||
|
||||
const defaults = {
|
||||
numerator: '*',
|
||||
denominator: '*',
|
||||
metric_agg: 'count'
|
||||
metric_agg: 'count',
|
||||
};
|
||||
|
||||
const model = { ...defaults, ...props.model };
|
||||
|
@ -71,13 +68,9 @@ export const FilterRatioAgg = props => {
|
|||
dragHandleProps={props.dragHandleProps}
|
||||
>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiFormLabel htmlFor={htmlId('aggregation')}>
|
||||
<FormattedMessage
|
||||
id="tsvb.filterRatio.aggregationLabel"
|
||||
defaultMessage="Aggregation"
|
||||
/>
|
||||
<FormattedMessage id="tsvb.filterRatio.aggregationLabel" defaultMessage="Aggregation" />
|
||||
</EuiFormLabel>
|
||||
<AggSelect
|
||||
id={htmlId('aggregation')}
|
||||
|
@ -91,39 +84,32 @@ export const FilterRatioAgg = props => {
|
|||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id={htmlId('numerator')}
|
||||
label={(<FormattedMessage
|
||||
id="tsvb.filterRatio.numeratorLabel"
|
||||
defaultMessage="Numerator"
|
||||
/>)}
|
||||
label={
|
||||
<FormattedMessage id="tsvb.filterRatio.numeratorLabel" defaultMessage="Numerator" />
|
||||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
onChange={handleTextChange('numerator')}
|
||||
value={model.numerator}
|
||||
/>
|
||||
<EuiFieldText onChange={handleTextChange('numerator')} value={model.numerator} />
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id={htmlId('denominator')}
|
||||
label={(<FormattedMessage
|
||||
id="tsvb.filterRatio.denominatorLabel"
|
||||
defaultMessage="Denominator"
|
||||
/>)}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="tsvb.filterRatio.denominatorLabel"
|
||||
defaultMessage="Denominator"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
onChange={handleTextChange('denominator')}
|
||||
value={model.denominator}
|
||||
/>
|
||||
<EuiFieldText onChange={handleTextChange('denominator')} value={model.denominator} />
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiFormLabel htmlFor={htmlId('metric')}>
|
||||
<FormattedMessage
|
||||
|
@ -140,14 +126,11 @@ export const FilterRatioAgg = props => {
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
{ model.metric_agg !== 'count' ? (
|
||||
{model.metric_agg !== 'count' ? (
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id={htmlId('aggField')}
|
||||
label={(<FormattedMessage
|
||||
id="tsvb.filterRatio.fieldLabel"
|
||||
defaultMessage="Field"
|
||||
/>)}
|
||||
label={<FormattedMessage id="tsvb.filterRatio.fieldLabel" defaultMessage="Field" />}
|
||||
>
|
||||
<FieldSelect
|
||||
fields={fields}
|
||||
|
@ -158,8 +141,8 @@ export const FilterRatioAgg = props => {
|
|||
onChange={handleSelectChange('field')}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>) : null }
|
||||
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
</AggRow>
|
||||
);
|
||||
|
|
|
@ -74,10 +74,7 @@ export class MathAgg extends Component {
|
|||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexItem>
|
||||
<EuiFormLabel htmlFor={htmlId('aggregation')}>
|
||||
<FormattedMessage
|
||||
id="tsvb.math.aggregationLabel"
|
||||
defaultMessage="Aggregation"
|
||||
/>
|
||||
<FormattedMessage id="tsvb.math.aggregationLabel" defaultMessage="Aggregation" />
|
||||
</EuiFormLabel>
|
||||
<AggSelect
|
||||
id={htmlId('aggregation')}
|
||||
|
@ -89,10 +86,7 @@ export class MathAgg extends Component {
|
|||
|
||||
<EuiFlexItem>
|
||||
<EuiFormLabel htmlFor={htmlId('variables')}>
|
||||
<FormattedMessage
|
||||
id="tsvb.math.variablesLabel"
|
||||
defaultMessage="Variables"
|
||||
/>
|
||||
<FormattedMessage id="tsvb.math.variablesLabel" defaultMessage="Variables" />
|
||||
</EuiFormLabel>
|
||||
<CalculationVars
|
||||
id={htmlId('variables')}
|
||||
|
@ -107,38 +101,40 @@ export class MathAgg extends Component {
|
|||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="mathExpressionInput"
|
||||
label={(<FormattedMessage
|
||||
id="tsvb.math.expressionLabel"
|
||||
defaultMessage="Expression"
|
||||
/>)}
|
||||
label={
|
||||
<FormattedMessage id="tsvb.math.expressionLabel" defaultMessage="Expression" />
|
||||
}
|
||||
fullWidth
|
||||
helpText={(<FormattedMessage
|
||||
id="tsvb.math.expressionDescription"
|
||||
defaultMessage="This field uses basic math expressions (see {link}) - Variables are keys on the {params} object,
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="tsvb.math.expressionDescription"
|
||||
defaultMessage="This field uses basic math expressions (see {link}) - Variables are keys on the {params} object,
|
||||
i.e. {paramsName} To access all the data use {paramsValues} for an array of the values and {paramsTimestamps} for
|
||||
an array of the timestamps. {paramsTimestamp} is available for the current bucket's timestamp,
|
||||
{paramsIndex} is available for the current bucket's index, and {paramsInterval}s available for
|
||||
an array of the timestamps. {paramsTimestamp} is available for the current bucket's timestamp,
|
||||
{paramsIndex} is available for the current bucket's index, and {paramsInterval}s available for
|
||||
the interval in milliseconds."
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink
|
||||
href="https://github.com/elastic/tinymath/blob/master/docs/functions.md"
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="tsvb.math.expressionDescription.tinyMathLinkText"
|
||||
defaultMessage="TinyMath"
|
||||
/>
|
||||
</EuiLink>),
|
||||
params: (<EuiCode>params</EuiCode>),
|
||||
paramsName: (<EuiCode>params.<name></EuiCode>),
|
||||
paramsValues: (<EuiCode>params._all.<name>.values</EuiCode>),
|
||||
paramsTimestamps: (<EuiCode>params._all.<name>.timestamps</EuiCode>),
|
||||
paramsTimestamp: (<EuiCode>params._timestamp</EuiCode>),
|
||||
paramsIndex: (<EuiCode>params._index</EuiCode>),
|
||||
paramsInterval: (<EuiCode>params._interval</EuiCode>)
|
||||
}}
|
||||
/>)}
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink
|
||||
href="https://github.com/elastic/tinymath/blob/master/docs/functions.md"
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="tsvb.math.expressionDescription.tinyMathLinkText"
|
||||
defaultMessage="TinyMath"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
params: <EuiCode>params</EuiCode>,
|
||||
paramsName: <EuiCode>params.<name></EuiCode>,
|
||||
paramsValues: <EuiCode>params._all.<name>.values</EuiCode>,
|
||||
paramsTimestamps: <EuiCode>params._all.<name>.timestamps</EuiCode>,
|
||||
paramsTimestamp: <EuiCode>params._timestamp</EuiCode>,
|
||||
paramsIndex: <EuiCode>params._index</EuiCode>,
|
||||
paramsInterval: <EuiCode>params._interval</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiTextArea
|
||||
data-test-subj="mathExpression"
|
||||
|
|
|
@ -21,9 +21,7 @@ import PropTypes from 'prop-types';
|
|||
import React from 'react';
|
||||
import { includes } from 'lodash';
|
||||
import { injectI18n } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiComboBox,
|
||||
} from '@elastic/eui';
|
||||
import { EuiComboBox } from '@elastic/eui';
|
||||
import { calculateSiblings } from '../lib/calculate_siblings';
|
||||
import { calculateLabel } from '../../../common/calculate_label';
|
||||
import { basicAggs } from '../../../common/basic_aggs';
|
||||
|
@ -46,11 +44,7 @@ function createTypeFilter(restrict, exclude) {
|
|||
export function filterRows(includeSiblings) {
|
||||
return row => {
|
||||
if (includeSiblings) {
|
||||
return (
|
||||
!/^series/.test(row.type) &&
|
||||
!/^percentile/.test(row.type) &&
|
||||
row.type !== 'math'
|
||||
);
|
||||
return !/^series/.test(row.type) && !/^percentile/.test(row.type) && row.type !== 'math';
|
||||
}
|
||||
return (
|
||||
!/_bucket$/.test(row.type) &&
|
||||
|
@ -62,7 +56,19 @@ export function filterRows(includeSiblings) {
|
|||
}
|
||||
|
||||
function MetricSelectUi(props) {
|
||||
const { additionalOptions, restrict, metric, metrics, onChange, value, exclude, includeSiblings, clearable, intl, ...rest } = props;
|
||||
const {
|
||||
additionalOptions,
|
||||
restrict,
|
||||
metric,
|
||||
metrics,
|
||||
onChange,
|
||||
value,
|
||||
exclude,
|
||||
includeSiblings,
|
||||
clearable,
|
||||
intl,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const calculatedMetrics = metrics.filter(createTypeFilter(restrict, exclude));
|
||||
|
||||
|
@ -116,7 +122,10 @@ function MetricSelectUi(props) {
|
|||
|
||||
return (
|
||||
<EuiComboBox
|
||||
placeholder={intl.formatMessage({ id: 'tsvb.metricSelect.selectMetricPlaceholder', defaultMessage: 'Select metric…' })}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'tsvb.metricSelect.selectMetricPlaceholder',
|
||||
defaultMessage: 'Select metric…',
|
||||
})}
|
||||
options={allOptions}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={onChange}
|
||||
|
|
|
@ -45,7 +45,7 @@ const MovingAverageAggUi = props => {
|
|||
settings: '',
|
||||
minimize: 0,
|
||||
window: '',
|
||||
model: 'simple'
|
||||
model: 'simple',
|
||||
};
|
||||
const model = { ...defaults, ...props.model };
|
||||
const handleChange = createChangeHandler(props.onChange, model);
|
||||
|
@ -54,31 +54,42 @@ const MovingAverageAggUi = props => {
|
|||
const handleNumberChange = createNumberHandler(handleChange);
|
||||
const modelOptions = [
|
||||
{
|
||||
label: intl.formatMessage({ id: 'tsvb.movingAverage.modelOptions.simpleLabel', defaultMessage: 'Simple' }),
|
||||
value: 'simple'
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: 'tsvb.movingAverage.modelOptions.linearLabel', defaultMessage: 'Linear' }),
|
||||
value: 'linear'
|
||||
label: intl.formatMessage({
|
||||
id: 'tsvb.movingAverage.modelOptions.simpleLabel',
|
||||
defaultMessage: 'Simple',
|
||||
}),
|
||||
value: 'simple',
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: 'tsvb.movingAverage.modelOptions.exponentiallyWeightedLabel', defaultMessage: 'Exponentially Weighted' }),
|
||||
value: 'ewma'
|
||||
id: 'tsvb.movingAverage.modelOptions.linearLabel',
|
||||
defaultMessage: 'Linear',
|
||||
}),
|
||||
value: 'linear',
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: 'tsvb.movingAverage.modelOptions.holtLinearLabel', defaultMessage: 'Holt-Linear' }),
|
||||
value: 'holt'
|
||||
label: intl.formatMessage({
|
||||
id: 'tsvb.movingAverage.modelOptions.exponentiallyWeightedLabel',
|
||||
defaultMessage: 'Exponentially Weighted',
|
||||
}),
|
||||
value: 'ewma',
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: 'tsvb.movingAverage.modelOptions.holtWintersLabel', defaultMessage: 'Holt-Winters' }),
|
||||
value: 'holt_winters'
|
||||
}
|
||||
];
|
||||
const minimizeOptions = [
|
||||
{ label: 'True', value: 1 },
|
||||
{ label: 'False', value: 0 }
|
||||
label: intl.formatMessage({
|
||||
id: 'tsvb.movingAverage.modelOptions.holtLinearLabel',
|
||||
defaultMessage: 'Holt-Linear',
|
||||
}),
|
||||
value: 'holt',
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: 'tsvb.movingAverage.modelOptions.holtWintersLabel',
|
||||
defaultMessage: 'Holt-Winters',
|
||||
}),
|
||||
value: 'holt_winters',
|
||||
},
|
||||
];
|
||||
const minimizeOptions = [{ label: 'True', value: 1 }, { label: 'False', value: 0 }];
|
||||
const htmlId = htmlIdGenerator();
|
||||
const selectedModelOption = modelOptions.find(option => {
|
||||
return model.model === option.value;
|
||||
|
@ -115,10 +126,7 @@ const MovingAverageAggUi = props => {
|
|||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id={htmlId('metric')}
|
||||
label={(<FormattedMessage
|
||||
id="tsvb.movingAverage.metricLabel"
|
||||
defaultMessage="Metric"
|
||||
/>)}
|
||||
label={<FormattedMessage id="tsvb.movingAverage.metricLabel" defaultMessage="Metric" />}
|
||||
>
|
||||
<MetricSelect
|
||||
onChange={handleSelectChange('field')}
|
||||
|
@ -136,14 +144,14 @@ const MovingAverageAggUi = props => {
|
|||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id={htmlId('model')}
|
||||
label={(<FormattedMessage
|
||||
id="tsvb.movingAverage.modelLabel"
|
||||
defaultMessage="Model"
|
||||
/>)}
|
||||
label={<FormattedMessage id="tsvb.movingAverage.modelLabel" defaultMessage="Model" />}
|
||||
>
|
||||
<EuiComboBox
|
||||
isClearable={false}
|
||||
placeholder={intl.formatMessage({ id: 'tsvb.movingAverage.model.selectPlaceholder', defaultMessage: 'Select' })}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'tsvb.movingAverage.model.selectPlaceholder',
|
||||
defaultMessage: 'Select',
|
||||
})}
|
||||
options={modelOptions}
|
||||
selectedOptions={selectedModelOption ? [selectedModelOption] : []}
|
||||
onChange={handleSelectChange('model')}
|
||||
|
@ -154,10 +162,12 @@ const MovingAverageAggUi = props => {
|
|||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id={htmlId('windowSize')}
|
||||
label={(<FormattedMessage
|
||||
id="tsvb.movingAverage.windowSizeLabel"
|
||||
defaultMessage="Window Size"
|
||||
/>)}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="tsvb.movingAverage.windowSizeLabel"
|
||||
defaultMessage="Window Size"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{/*
|
||||
EUITODO: The following input couldn't be converted to EUI because of type mis-match.
|
||||
|
@ -174,13 +184,15 @@ const MovingAverageAggUi = props => {
|
|||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id={htmlId('minimize')}
|
||||
label={(<FormattedMessage
|
||||
id="tsvb.movingAverage.minimizeLabel"
|
||||
defaultMessage="Minimize"
|
||||
/>)}
|
||||
label={
|
||||
<FormattedMessage id="tsvb.movingAverage.minimizeLabel" defaultMessage="Minimize" />
|
||||
}
|
||||
>
|
||||
<EuiComboBox
|
||||
placeholder={intl.formatMessage({ id: 'tsvb.movingAverage.minimize.selectPlaceholder', defaultMessage: 'Select' })}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'tsvb.movingAverage.minimize.selectPlaceholder',
|
||||
defaultMessage: 'Select',
|
||||
})}
|
||||
options={minimizeOptions}
|
||||
selectedOptions={selectedMinimizeOption ? [selectedMinimizeOption] : []}
|
||||
onChange={handleSelectChange('minimize')}
|
||||
|
@ -191,10 +203,9 @@ const MovingAverageAggUi = props => {
|
|||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id={htmlId('predict')}
|
||||
label={(<FormattedMessage
|
||||
id="tsvb.movingAverage.predictLabel"
|
||||
defaultMessage="Predict"
|
||||
/>)}
|
||||
label={
|
||||
<FormattedMessage id="tsvb.movingAverage.predictLabel" defaultMessage="Predict" />
|
||||
}
|
||||
>
|
||||
{/*
|
||||
EUITODO: The following input couldn't be converted to EUI because of type mis-match.
|
||||
|
@ -216,25 +227,20 @@ const MovingAverageAggUi = props => {
|
|||
<EuiFormRow
|
||||
fullWidth
|
||||
id={htmlId('settings')}
|
||||
label={(<FormattedMessage
|
||||
id="tsvb.movingAverage.settingsLabel"
|
||||
defaultMessage="Settings"
|
||||
/>)}
|
||||
label={
|
||||
<FormattedMessage id="tsvb.movingAverage.settingsLabel" defaultMessage="Settings" />
|
||||
}
|
||||
helpText={
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="tsvb.movingAverage.settingsDescription"
|
||||
defaultMessage="{keyValue} space-delimited"
|
||||
values={{ keyValue: (<EuiCode>Key=Value</EuiCode>) }}
|
||||
values={{ keyValue: <EuiCode>Key=Value</EuiCode> }}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<EuiTextArea
|
||||
onChange={handleTextChange('settings')}
|
||||
value={model.settings}
|
||||
fullWidth
|
||||
/>
|
||||
<EuiTextArea onChange={handleTextChange('settings')} value={model.settings} fullWidth />
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</AggRow>
|
||||
|
|
|
@ -23,9 +23,6 @@ import _ from 'lodash';
|
|||
import { AggSelect } from './agg_select';
|
||||
import { FieldSelect } from './field_select';
|
||||
import { AggRow } from './agg_row';
|
||||
import { collectionActions } from '../lib/collection_actions';
|
||||
import { AddDeleteButtons } from '../add_delete_buttons';
|
||||
import uuid from 'uuid';
|
||||
import { createChangeHandler } from '../lib/create_change_handler';
|
||||
import { createSelectHandler } from '../lib/create_select_handler';
|
||||
import {
|
||||
|
@ -34,186 +31,24 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormLabel,
|
||||
EuiComboBox,
|
||||
EuiFieldNumber,
|
||||
EuiFormRow,
|
||||
} from '@elastic/eui';
|
||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { ES_TYPES } from '../../../common/es_types';
|
||||
|
||||
const newPercentile = (opts) => {
|
||||
return _.assign({ id: uuid.v1(), mode: 'line', shade: 0.2 }, opts);
|
||||
};
|
||||
import { Percentiles, newPercentile } from './percentile_ui';
|
||||
|
||||
const RESTRICT_FIELDS = [ES_TYPES.NUMBER];
|
||||
|
||||
class PercentilesUi extends Component {
|
||||
|
||||
handleTextChange(item, name) {
|
||||
return (e) => {
|
||||
const handleChange = collectionActions.handleChange.bind(null, this.props);
|
||||
const part = {};
|
||||
part[name] = _.get(e, '[0].value', _.get(e, 'target.value'));
|
||||
handleChange(_.assign({}, item, part));
|
||||
};
|
||||
}
|
||||
|
||||
renderRow = (row, i, items) => {
|
||||
const defaults = { value: '', percentile: '', shade: '' };
|
||||
const model = { ...defaults, ...row };
|
||||
const { intl, panel } = this.props;
|
||||
|
||||
const percentileFieldNumber = (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFieldNumber
|
||||
aria-label={intl.formatMessage({ id: 'tsvb.percentile.percentileAriaLabel', defaultMessage: 'Percentile' })}
|
||||
placeholder={0}
|
||||
max={100}
|
||||
min={0}
|
||||
step={1}
|
||||
onChange={this.handleTextChange(model, 'value')}
|
||||
value={model.value === '' ? '' : Number(model.value)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
|
||||
if (panel.type === 'table') {
|
||||
return percentileFieldNumber;
|
||||
}
|
||||
|
||||
const handleAdd = collectionActions.handleAdd.bind(null, this.props, newPercentile);
|
||||
const handleDelete = collectionActions.handleDelete.bind(null, this.props, model);
|
||||
const modeOptions = [
|
||||
{
|
||||
label: intl.formatMessage({ id: 'tsvb.percentile.modeOptions.lineLabel', defaultMessage: 'Line' }),
|
||||
value: 'line'
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: 'tsvb.percentile.modeOptions.bandLabel', defaultMessage: 'Band' }),
|
||||
value: 'band'
|
||||
}
|
||||
];
|
||||
const optionsStyle = {};
|
||||
if (model.mode === 'line') {
|
||||
optionsStyle.display = 'none';
|
||||
}
|
||||
const labelStyle = { marginBottom: 0 };
|
||||
const htmlId = htmlIdGenerator(model.id);
|
||||
const selectedModeOption = modeOptions.find(option => {
|
||||
return model.mode === option.value;
|
||||
});
|
||||
return (
|
||||
<EuiFlexItem key={model.id}>
|
||||
<EuiFlexGroup alignItems="center" responsive={false} gutterSize="s">
|
||||
|
||||
{ percentileFieldNumber }
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormLabel style={labelStyle} htmlFor={htmlId('mode')}>
|
||||
<FormattedMessage
|
||||
id="tsvb.percentile.modeLabel"
|
||||
defaultMessage="Mode:"
|
||||
/>
|
||||
</EuiFormLabel>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiComboBox
|
||||
isClearable={false}
|
||||
id={htmlId('mode')}
|
||||
options={modeOptions}
|
||||
selectedOptions={selectedModeOption ? [selectedModeOption] : []}
|
||||
onChange={this.handleTextChange(model, 'mode')}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem style={optionsStyle} grow={false}>
|
||||
<EuiFormLabel style={labelStyle} htmlFor={htmlId('fillTo')}>
|
||||
<FormattedMessage
|
||||
id="tsvb.percentile.fillToLabel"
|
||||
defaultMessage="Fill to:"
|
||||
/>
|
||||
</EuiFormLabel>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem style={optionsStyle} grow={false}>
|
||||
<EuiFieldNumber
|
||||
id={htmlId('fillTo')}
|
||||
step={1}
|
||||
onChange={this.handleTextChange(model, 'percentile')}
|
||||
value={Number(model.percentile)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem style={optionsStyle} grow={false}>
|
||||
<EuiFormLabel style={labelStyle} htmlFor={htmlId('shade')}>
|
||||
<FormattedMessage
|
||||
id="tsvb.percentile.shadeLabel"
|
||||
defaultMessage="Shade (0 to 1):"
|
||||
/>
|
||||
</EuiFormLabel>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem style={optionsStyle} grow={false}>
|
||||
<EuiFieldNumber
|
||||
id={htmlId('shade')}
|
||||
style={optionsStyle}
|
||||
step={0.1}
|
||||
onChange={this.handleTextChange(model, 'shade')}
|
||||
value={Number(model.shade)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<AddDeleteButtons
|
||||
onAdd={handleAdd}
|
||||
onDelete={handleDelete}
|
||||
disableDelete={items.length < 2}
|
||||
responsive={false}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { model, name, panel } = this.props;
|
||||
if (!model[name]) return (<div/>);
|
||||
let rows;
|
||||
if (panel.type === 'table') {
|
||||
rows = this.renderRow(_.last(model[name]));
|
||||
} else {
|
||||
rows = model[name].map(this.renderRow);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
{ rows }
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PercentilesUi.defaultProps = {
|
||||
name: 'percentile'
|
||||
};
|
||||
|
||||
PercentilesUi.propTypes = {
|
||||
name: PropTypes.string,
|
||||
model: PropTypes.object,
|
||||
panel: PropTypes.object,
|
||||
onChange: PropTypes.func
|
||||
};
|
||||
|
||||
const Percentiles = injectI18n(PercentilesUi);
|
||||
|
||||
export class PercentileAgg extends Component { // eslint-disable-line react/no-multi-comp
|
||||
export class PercentileAgg extends Component {
|
||||
// eslint-disable-line react/no-multi-comp
|
||||
|
||||
componentWillMount() {
|
||||
if (!this.props.model.percentiles) {
|
||||
this.props.onChange(_.assign({}, this.props.model, {
|
||||
percentiles: [newPercentile({ value: 50 })]
|
||||
}));
|
||||
this.props.onChange(
|
||||
_.assign({}, this.props.model, {
|
||||
percentiles: [newPercentile({ value: 50 })],
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -222,7 +57,8 @@ export class PercentileAgg extends Component { // eslint-disable-line react/no-m
|
|||
|
||||
const handleChange = createChangeHandler(this.props.onChange, model);
|
||||
const handleSelectChange = createSelectHandler(handleChange);
|
||||
const indexPattern = series.override_index_pattern && series.series_index_pattern || panel.index_pattern;
|
||||
const indexPattern =
|
||||
(series.override_index_pattern && series.series_index_pattern) || panel.index_pattern;
|
||||
const htmlId = htmlIdGenerator();
|
||||
|
||||
return (
|
||||
|
@ -253,10 +89,7 @@ export class PercentileAgg extends Component { // eslint-disable-line react/no-m
|
|||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id={htmlId('field')}
|
||||
label={(<FormattedMessage
|
||||
id="tsvb.percentile.fieldLabel"
|
||||
defaultMessage="Field"
|
||||
/>)}
|
||||
label={<FormattedMessage id="tsvb.percentile.fieldLabel" defaultMessage="Field" />}
|
||||
>
|
||||
<FieldSelect
|
||||
fields={fields}
|
||||
|
@ -272,17 +105,10 @@ export class PercentileAgg extends Component { // eslint-disable-line react/no-m
|
|||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<Percentiles
|
||||
onChange={handleChange}
|
||||
name="percentiles"
|
||||
model={model}
|
||||
panel={panel}
|
||||
/>
|
||||
|
||||
<Percentiles onChange={handleChange} name="percentiles" model={model} panel={panel} />
|
||||
</AggRow>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PercentileAgg.propTypes = {
|
||||
|
|
|
@ -31,30 +31,21 @@ import {
|
|||
|
||||
import { AddDeleteButtons } from '../../add_delete_buttons';
|
||||
|
||||
export const MultiValueRow = ({
|
||||
model,
|
||||
onChange,
|
||||
onDelete,
|
||||
onAdd,
|
||||
disableAdd,
|
||||
disableDelete,
|
||||
}) => {
|
||||
export const MultiValueRow = ({ model, onChange, onDelete, onAdd, disableAdd, disableDelete }) => {
|
||||
const htmlId = htmlIdGenerator();
|
||||
|
||||
const onFieldNumberChange = event => onChange({
|
||||
...model,
|
||||
value: get(event, 'target.value')
|
||||
});
|
||||
const onFieldNumberChange = event =>
|
||||
onChange({
|
||||
...model,
|
||||
value: get(event, 'target.value'),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="tvbAggRow__multiValueRow">
|
||||
<EuiFlexGroup responsive={false} alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormLabel htmlFor={htmlId('value')}>
|
||||
<FormattedMessage
|
||||
id="tsvb.multivalueRow.valueLabel"
|
||||
defaultMessage="Value:"
|
||||
/>
|
||||
<FormattedMessage id="tsvb.multivalueRow.valueLabel" defaultMessage="Value:" />
|
||||
</EuiFormLabel>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -73,7 +64,7 @@ export const MultiValueRow = ({
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer/>
|
||||
<EuiSpacer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -93,4 +84,3 @@ MultiValueRow.propTypes = {
|
|||
defaultAddValue: PropTypes.string,
|
||||
disableDelete: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
|
|
@ -45,16 +45,19 @@ export const PercentileRankAgg = props => {
|
|||
const defaults = { values: [''] };
|
||||
const model = { ...defaults, ...props.model };
|
||||
|
||||
const indexPattern = series.override_index_pattern && series.series_index_pattern || panel.index_pattern;
|
||||
const indexPattern =
|
||||
(series.override_index_pattern && series.series_index_pattern) || panel.index_pattern;
|
||||
const htmlId = htmlIdGenerator();
|
||||
const isTablePanel = panel.type === 'table';
|
||||
const handleChange = createChangeHandler(props.onChange, model);
|
||||
const handleSelectChange = createSelectHandler(handleChange);
|
||||
|
||||
const handlePercentileRankValuesChange = (values) => {
|
||||
handleChange(assign({}, model, {
|
||||
values,
|
||||
}));
|
||||
const handlePercentileRankValuesChange = values => {
|
||||
handleChange(
|
||||
assign({}, model, {
|
||||
values,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -85,10 +88,7 @@ export const PercentileRankAgg = props => {
|
|||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id={htmlId('field')}
|
||||
label={(<FormattedMessage
|
||||
id="tsvb.percentileRank.fieldLabel"
|
||||
defaultMessage="Field"
|
||||
/>)}
|
||||
label={<FormattedMessage id="tsvb.percentileRank.fieldLabel" defaultMessage="Field" />}
|
||||
>
|
||||
<FieldSelect
|
||||
fields={fields}
|
||||
|
@ -101,7 +101,7 @@ export const PercentileRankAgg = props => {
|
|||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer/>
|
||||
<EuiSpacer />
|
||||
<PercentileRankValues
|
||||
disableAdd={isTablePanel}
|
||||
disableDelete={isTablePanel}
|
||||
|
|
|
@ -20,9 +20,7 @@ import PropTypes from 'prop-types';
|
|||
import React from 'react';
|
||||
import { last } from 'lodash';
|
||||
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
} from '@elastic/eui';
|
||||
import { EuiFlexGroup } from '@elastic/eui';
|
||||
import { MultiValueRow } from './multi_value_row';
|
||||
|
||||
export const PercentileRankValues = props => {
|
||||
|
@ -34,9 +32,8 @@ export const PercentileRankValues = props => {
|
|||
|
||||
onChange(model);
|
||||
};
|
||||
const onDeleteValue = ({ id }) => onChange(
|
||||
model.filter((item, currentIndex) => id !== currentIndex),
|
||||
);
|
||||
const onDeleteValue = ({ id }) =>
|
||||
onChange(model.filter((item, currentIndex) => id !== currentIndex));
|
||||
const onAddValue = () => onChange([...model, '']);
|
||||
|
||||
const renderRow = ({ rowModel, disableDelete, disableAdd }) => (
|
||||
|
@ -48,28 +45,32 @@ export const PercentileRankValues = props => {
|
|||
disableDelete={disableDelete}
|
||||
disableAdd={disableAdd}
|
||||
model={rowModel}
|
||||
/>);
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" responsive={false} gutterSize="xs">
|
||||
{showOnlyLastRow && renderRow({
|
||||
rowModel: {
|
||||
id: model.length - 1,
|
||||
value: last(model),
|
||||
},
|
||||
disableAdd: true,
|
||||
disableDelete: true
|
||||
})}
|
||||
|
||||
{!showOnlyLastRow && model.map((value, id, array) => (
|
||||
{showOnlyLastRow &&
|
||||
renderRow({
|
||||
rowModel: {
|
||||
id,
|
||||
value,
|
||||
id: model.length - 1,
|
||||
value: last(model),
|
||||
},
|
||||
disableAdd,
|
||||
disableDelete: disableDelete || array.length < 2,
|
||||
})))}
|
||||
disableAdd: true,
|
||||
disableDelete: true,
|
||||
})}
|
||||
|
||||
{!showOnlyLastRow &&
|
||||
model.map((value, id, array) =>
|
||||
renderRow({
|
||||
rowModel: {
|
||||
id,
|
||||
value,
|
||||
},
|
||||
disableAdd,
|
||||
disableDelete: disableDelete || array.length < 2,
|
||||
})
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
* 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 PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import _ from 'lodash';
|
||||
import { collectionActions } from '../lib/collection_actions';
|
||||
import { AddDeleteButtons } from '../add_delete_buttons';
|
||||
import uuid from 'uuid';
|
||||
import {
|
||||
htmlIdGenerator,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormLabel,
|
||||
EuiComboBox,
|
||||
EuiFieldNumber,
|
||||
} from '@elastic/eui';
|
||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
export const newPercentile = opts => {
|
||||
return _.assign({ id: uuid.v1(), mode: 'line', shade: 0.2 }, opts);
|
||||
};
|
||||
|
||||
class PercentilesUi extends Component {
|
||||
handleTextChange(item, name) {
|
||||
return e => {
|
||||
const handleChange = collectionActions.handleChange.bind(null, this.props);
|
||||
const part = {};
|
||||
part[name] = _.get(e, '[0].value', _.get(e, 'target.value'));
|
||||
handleChange(_.assign({}, item, part));
|
||||
};
|
||||
}
|
||||
|
||||
renderRow = (row, i, items) => {
|
||||
const defaults = { value: '', percentile: '', shade: '' };
|
||||
const model = { ...defaults, ...row };
|
||||
const { intl, panel } = this.props;
|
||||
|
||||
const percentileFieldNumber = (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFieldNumber
|
||||
aria-label={intl.formatMessage({
|
||||
id: 'tsvb.percentile.percentileAriaLabel',
|
||||
defaultMessage: 'Percentile',
|
||||
})}
|
||||
placeholder={0}
|
||||
max={100}
|
||||
min={0}
|
||||
step={1}
|
||||
onChange={this.handleTextChange(model, 'value')}
|
||||
value={model.value === '' ? '' : Number(model.value)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
|
||||
if (panel.type === 'table') {
|
||||
return percentileFieldNumber;
|
||||
}
|
||||
|
||||
const handleAdd = collectionActions.handleAdd.bind(null, this.props, newPercentile);
|
||||
const handleDelete = collectionActions.handleDelete.bind(null, this.props, model);
|
||||
const modeOptions = [
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: 'tsvb.percentile.modeOptions.lineLabel',
|
||||
defaultMessage: 'Line',
|
||||
}),
|
||||
value: 'line',
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: 'tsvb.percentile.modeOptions.bandLabel',
|
||||
defaultMessage: 'Band',
|
||||
}),
|
||||
value: 'band',
|
||||
},
|
||||
];
|
||||
const optionsStyle = {};
|
||||
if (model.mode === 'line') {
|
||||
optionsStyle.display = 'none';
|
||||
}
|
||||
const labelStyle = { marginBottom: 0 };
|
||||
const htmlId = htmlIdGenerator(model.id);
|
||||
const selectedModeOption = modeOptions.find(option => {
|
||||
return model.mode === option.value;
|
||||
});
|
||||
return (
|
||||
<EuiFlexItem key={model.id}>
|
||||
<EuiFlexGroup alignItems="center" responsive={false} gutterSize="s">
|
||||
{percentileFieldNumber}
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormLabel style={labelStyle} htmlFor={htmlId('mode')}>
|
||||
<FormattedMessage id="tsvb.percentile.modeLabel" defaultMessage="Mode:" />
|
||||
</EuiFormLabel>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiComboBox
|
||||
isClearable={false}
|
||||
id={htmlId('mode')}
|
||||
options={modeOptions}
|
||||
selectedOptions={selectedModeOption ? [selectedModeOption] : []}
|
||||
onChange={this.handleTextChange(model, 'mode')}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem style={optionsStyle} grow={false}>
|
||||
<EuiFormLabel style={labelStyle} htmlFor={htmlId('fillTo')}>
|
||||
<FormattedMessage id="tsvb.percentile.fillToLabel" defaultMessage="Fill to:" />
|
||||
</EuiFormLabel>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem style={optionsStyle} grow={false}>
|
||||
<EuiFieldNumber
|
||||
id={htmlId('fillTo')}
|
||||
step={1}
|
||||
onChange={this.handleTextChange(model, 'percentile')}
|
||||
value={Number(model.percentile)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem style={optionsStyle} grow={false}>
|
||||
<EuiFormLabel style={labelStyle} htmlFor={htmlId('shade')}>
|
||||
<FormattedMessage id="tsvb.percentile.shadeLabel" defaultMessage="Shade (0 to 1):" />
|
||||
</EuiFormLabel>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem style={optionsStyle} grow={false}>
|
||||
<EuiFieldNumber
|
||||
id={htmlId('shade')}
|
||||
style={optionsStyle}
|
||||
step={0.1}
|
||||
onChange={this.handleTextChange(model, 'shade')}
|
||||
value={Number(model.shade)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<AddDeleteButtons
|
||||
onAdd={handleAdd}
|
||||
onDelete={handleDelete}
|
||||
disableDelete={items.length < 2}
|
||||
responsive={false}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { model, name, panel } = this.props;
|
||||
if (!model[name]) return <div />;
|
||||
let rows;
|
||||
if (panel.type === 'table') {
|
||||
rows = this.renderRow(_.last(model[name]));
|
||||
} else {
|
||||
rows = model[name].map(this.renderRow);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
{rows}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PercentilesUi.defaultProps = {
|
||||
name: 'percentile',
|
||||
};
|
||||
|
||||
PercentilesUi.propTypes = {
|
||||
name: PropTypes.string,
|
||||
model: PropTypes.object,
|
||||
panel: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
export const Percentiles = injectI18n(PercentilesUi);
|
|
@ -65,10 +65,7 @@ export const PositiveOnlyAgg = props => {
|
|||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id={htmlId('metric')}
|
||||
label={(<FormattedMessage
|
||||
id="tsvb.positiveOnly.metricLabel"
|
||||
defaultMessage="Metric"
|
||||
/>)}
|
||||
label={<FormattedMessage id="tsvb.positiveOnly.metricLabel" defaultMessage="Metric" />}
|
||||
>
|
||||
<MetricSelect
|
||||
onChange={handleSelectChange('field')}
|
||||
|
|
|
@ -51,10 +51,7 @@ export const SerialDiffAgg = props => {
|
|||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiFormLabel htmlFor={htmlId('aggregation')}>
|
||||
<FormattedMessage
|
||||
id="tsvb.serialDiff.aggregationLabel"
|
||||
defaultMessage="Aggregation"
|
||||
/>
|
||||
<FormattedMessage id="tsvb.serialDiff.aggregationLabel" defaultMessage="Aggregation" />
|
||||
</EuiFormLabel>
|
||||
<AggSelect
|
||||
id={htmlId('aggregation')}
|
||||
|
@ -67,10 +64,7 @@ export const SerialDiffAgg = props => {
|
|||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id={htmlId('metric')}
|
||||
label={(<FormattedMessage
|
||||
id="tsvb.serialDiff.metricLabel"
|
||||
defaultMessage="Metric"
|
||||
/>)}
|
||||
label={<FormattedMessage id="tsvb.serialDiff.metricLabel" defaultMessage="Metric" />}
|
||||
>
|
||||
<MetricSelect
|
||||
onChange={handleSelectChange('field')}
|
||||
|
@ -83,13 +77,15 @@ export const SerialDiffAgg = props => {
|
|||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id={htmlId('lag')}
|
||||
label={(<FormattedMessage
|
||||
id="tsvb.serialDiff.lagLabel"
|
||||
defaultMessage="Lag"
|
||||
description="'Lag' refers to the parameter name of the serial diff translation
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="tsvb.serialDiff.lagLabel"
|
||||
defaultMessage="Lag"
|
||||
description="'Lag' refers to the parameter name of the serial diff translation
|
||||
https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-serialdiff-aggregation.html.
|
||||
This should only be translated if there is a reasaonable word explaining what that parameter does."
|
||||
/>)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{/*
|
||||
EUITODO: The following input couldn't be converted to EUI because of type mis-match.
|
||||
|
|
|
@ -44,40 +44,67 @@ function SeriesAggUi(props) {
|
|||
|
||||
const functionOptions = [
|
||||
{
|
||||
label: intl.formatMessage({ id: 'tsvb.seriesAgg.functionOptions.sumLabel', defaultMessage: 'Sum' }),
|
||||
value: 'sum'
|
||||
label: intl.formatMessage({
|
||||
id: 'tsvb.seriesAgg.functionOptions.sumLabel',
|
||||
defaultMessage: 'Sum',
|
||||
}),
|
||||
value: 'sum',
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: 'tsvb.seriesAgg.functionOptions.maxLabel', defaultMessage: 'Max' }),
|
||||
value: 'max'
|
||||
label: intl.formatMessage({
|
||||
id: 'tsvb.seriesAgg.functionOptions.maxLabel',
|
||||
defaultMessage: 'Max',
|
||||
}),
|
||||
value: 'max',
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: 'tsvb.seriesAgg.functionOptions.minLabel', defaultMessage: 'Min' }),
|
||||
value: 'min'
|
||||
label: intl.formatMessage({
|
||||
id: 'tsvb.seriesAgg.functionOptions.minLabel',
|
||||
defaultMessage: 'Min',
|
||||
}),
|
||||
value: 'min',
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: 'tsvb.seriesAgg.functionOptions.avgLabel', defaultMessage: 'Avg' }),
|
||||
value: 'mean'
|
||||
label: intl.formatMessage({
|
||||
id: 'tsvb.seriesAgg.functionOptions.avgLabel',
|
||||
defaultMessage: 'Avg',
|
||||
}),
|
||||
value: 'mean',
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: 'tsvb.seriesAgg.functionOptions.overallSumLabel', defaultMessage: 'Overall Sum' }),
|
||||
value: 'overall_sum'
|
||||
label: intl.formatMessage({
|
||||
id: 'tsvb.seriesAgg.functionOptions.overallSumLabel',
|
||||
defaultMessage: 'Overall Sum',
|
||||
}),
|
||||
value: 'overall_sum',
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: 'tsvb.seriesAgg.functionOptions.overallMaxLabel', defaultMessage: 'Overall Max' }),
|
||||
value: 'overall_max'
|
||||
label: intl.formatMessage({
|
||||
id: 'tsvb.seriesAgg.functionOptions.overallMaxLabel',
|
||||
defaultMessage: 'Overall Max',
|
||||
}),
|
||||
value: 'overall_max',
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: 'tsvb.seriesAgg.functionOptions.overallMinLabel', defaultMessage: 'Overall Min' }),
|
||||
value: 'overall_min'
|
||||
label: intl.formatMessage({
|
||||
id: 'tsvb.seriesAgg.functionOptions.overallMinLabel',
|
||||
defaultMessage: 'Overall Min',
|
||||
}),
|
||||
value: 'overall_min',
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: 'tsvb.seriesAgg.functionOptions.overallAvgLabel', defaultMessage: 'Overall Avg' }),
|
||||
value: 'overall_avg'
|
||||
label: intl.formatMessage({
|
||||
id: 'tsvb.seriesAgg.functionOptions.overallAvgLabel',
|
||||
defaultMessage: 'Overall Avg',
|
||||
}),
|
||||
value: 'overall_avg',
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: 'tsvb.seriesAgg.functionOptions.cumulativeSumLabel', defaultMessage: 'Cumulative Sum' }),
|
||||
value: 'cumulative_sum'
|
||||
label: intl.formatMessage({
|
||||
id: 'tsvb.seriesAgg.functionOptions.cumulativeSumLabel',
|
||||
defaultMessage: 'Cumulative Sum',
|
||||
}),
|
||||
value: 'cumulative_sum',
|
||||
},
|
||||
];
|
||||
const selectedFunctionOption = functionOptions.find(option => {
|
||||
|
@ -118,10 +145,7 @@ function SeriesAggUi(props) {
|
|||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiFormLabel htmlFor={htmlId('aggregation')}>
|
||||
<FormattedMessage
|
||||
id="tsvb.seriesAgg.aggregationLabel"
|
||||
defaultMessage="Aggregation"
|
||||
/>
|
||||
<FormattedMessage id="tsvb.seriesAgg.aggregationLabel" defaultMessage="Aggregation" />
|
||||
</EuiFormLabel>
|
||||
<AggSelect
|
||||
id={htmlId('aggregation')}
|
||||
|
@ -134,10 +158,7 @@ function SeriesAggUi(props) {
|
|||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id={htmlId('function')}
|
||||
label={(<FormattedMessage
|
||||
id="tsvb.seriesAgg.functionLabel"
|
||||
defaultMessage="Function"
|
||||
/>)}
|
||||
label={<FormattedMessage id="tsvb.seriesAgg.functionLabel" defaultMessage="Function" />}
|
||||
>
|
||||
<EuiComboBox
|
||||
options={functionOptions}
|
||||
|
@ -150,7 +171,6 @@ function SeriesAggUi(props) {
|
|||
</EuiFlexGroup>
|
||||
</AggRow>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
SeriesAggUi.propTypes = {
|
||||
|
|
|
@ -61,10 +61,7 @@ export const Static = props => {
|
|||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiFormLabel htmlFor={htmlId('aggregation')}>
|
||||
<FormattedMessage
|
||||
id="tsvb.static.aggregationLabel"
|
||||
defaultMessage="Aggregation"
|
||||
/>
|
||||
<FormattedMessage id="tsvb.static.aggregationLabel" defaultMessage="Aggregation" />
|
||||
</EuiFormLabel>
|
||||
<AggSelect
|
||||
id={htmlId('aggregation')}
|
||||
|
@ -77,10 +74,9 @@ export const Static = props => {
|
|||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id={htmlId('staticValue')}
|
||||
label={(<FormattedMessage
|
||||
id="tsvb.static.staticValuesLabel"
|
||||
defaultMessage="Static Value"
|
||||
/>)}
|
||||
label={
|
||||
<FormattedMessage id="tsvb.static.staticValuesLabel" defaultMessage="Static Value" />
|
||||
}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
onChange={handleTextChange('value')}
|
||||
|
|