mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Merge branch 'master' into introduce-start-phase
This commit is contained in:
commit
f8cc8c88b1
429 changed files with 10648 additions and 5027 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -31,7 +31,7 @@ webpackstats.json
|
|||
!/config/kibana.yml
|
||||
coverage
|
||||
selenium
|
||||
.babelcache.json
|
||||
.babel_register_cache.json
|
||||
.webpack.babelcache
|
||||
*.swp
|
||||
*.swo
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBadge](./kibana-plugin-public.chromebadge.md) > [iconType](./kibana-plugin-public.chromebadge.icontype.md)
|
||||
|
||||
## ChromeBadge.iconType property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
iconType?: IconType;
|
||||
```
|
|
@ -0,0 +1,19 @@
|
|||
[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBadge](./kibana-plugin-public.chromebadge.md)
|
||||
|
||||
## ChromeBadge interface
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface ChromeBadge
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [iconType](./kibana-plugin-public.chromebadge.icontype.md) | <code>IconType</code> | |
|
||||
| [text](./kibana-plugin-public.chromebadge.text.md) | <code>string</code> | |
|
||||
| [tooltip](./kibana-plugin-public.chromebadge.tooltip.md) | <code>string</code> | |
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBadge](./kibana-plugin-public.chromebadge.md) > [text](./kibana-plugin-public.chromebadge.text.md)
|
||||
|
||||
## ChromeBadge.text property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
text: string;
|
||||
```
|
|
@ -0,0 +1,9 @@
|
|||
[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeBadge](./kibana-plugin-public.chromebadge.md) > [tooltip](./kibana-plugin-public.chromebadge.tooltip.md)
|
||||
|
||||
## ChromeBadge.tooltip property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
tooltip: string;
|
||||
```
|
|
@ -1,45 +1,46 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md)
|
||||
|
||||
## kibana-plugin-public package
|
||||
|
||||
## Classes
|
||||
|
||||
| Class | Description |
|
||||
| --- | --- |
|
||||
| [FlyoutRef](./kibana-plugin-public.flyoutref.md) | A FlyoutRef is a reference to an opened flyout panel. It offers methods to close the flyout panel again. If you open a flyout panel you should make sure you call <code>close()</code> when it should be closed. Since a flyout could also be closed by a user or from another flyout being opened, you must bind to the <code>onClose</code> Promise on the FlyoutRef instance. The Promise will resolve whenever the flyout was closed at which point you should discard the FlyoutRef. |
|
||||
| [ToastsSetup](./kibana-plugin-public.toastssetup.md) | |
|
||||
| [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) | |
|
||||
|
||||
## Interfaces
|
||||
|
||||
| Interface | Description |
|
||||
| --- | --- |
|
||||
| [BasePathSetup](./kibana-plugin-public.basepathsetup.md) | Provides access to the 'server.basePath' configuration option in kibana.yml |
|
||||
| [Capabilities](./kibana-plugin-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. |
|
||||
| [CapabilitiesSetup](./kibana-plugin-public.capabilitiessetup.md) | Capabilities Setup. |
|
||||
| [ChromeBrand](./kibana-plugin-public.chromebrand.md) | |
|
||||
| [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) | |
|
||||
| [CoreSetup](./kibana-plugin-public.coresetup.md) | Core services exposed to the start lifecycle |
|
||||
| [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. |
|
||||
| [I18nSetup](./kibana-plugin-public.i18nsetup.md) | I18nSetup.Context is required by any localizable React component from @<!-- -->kbn/i18n and @<!-- -->elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. |
|
||||
| [InjectedMetadataSetup](./kibana-plugin-public.injectedmetadatasetup.md) | Provides access to the metadata injected by the server into the page |
|
||||
| [OverlaySetup](./kibana-plugin-public.overlaysetup.md) | |
|
||||
| [Plugin](./kibana-plugin-public.plugin.md) | The interface that should be returned by a <code>PluginInitializer</code>. |
|
||||
| [PluginInitializerContext](./kibana-plugin-public.plugininitializercontext.md) | The available core services passed to a <code>PluginInitializer</code> |
|
||||
| [PluginSetupContext](./kibana-plugin-public.pluginsetupcontext.md) | The available core services passed to a plugin's <code>Plugin#setup</code> method. |
|
||||
| [UiSettingsState](./kibana-plugin-public.uisettingsstate.md) | |
|
||||
|
||||
## Type Aliases
|
||||
|
||||
| Type Alias | Description |
|
||||
| --- | --- |
|
||||
| [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) | |
|
||||
| [ChromeSetup](./kibana-plugin-public.chromesetup.md) | |
|
||||
| [HttpSetup](./kibana-plugin-public.httpsetup.md) | |
|
||||
| [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | |
|
||||
| [PluginInitializer](./kibana-plugin-public.plugininitializer.md) | The <code>plugin</code> export at the root of a plugin's <code>public</code> directory should conform to this interface. |
|
||||
| [ToastInput](./kibana-plugin-public.toastinput.md) | |
|
||||
| [UiSettingsSetup](./kibana-plugin-public.uisettingssetup.md) | |
|
||||
|
||||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md)
|
||||
|
||||
## kibana-plugin-public package
|
||||
|
||||
## Classes
|
||||
|
||||
| Class | Description |
|
||||
| --- | --- |
|
||||
| [FlyoutRef](./kibana-plugin-public.flyoutref.md) | A FlyoutRef is a reference to an opened flyout panel. It offers methods to close the flyout panel again. If you open a flyout panel you should make sure you call <code>close()</code> when it should be closed. Since a flyout could also be closed by a user or from another flyout being opened, you must bind to the <code>onClose</code> Promise on the FlyoutRef instance. The Promise will resolve whenever the flyout was closed at which point you should discard the FlyoutRef. |
|
||||
| [ToastsSetup](./kibana-plugin-public.toastssetup.md) | |
|
||||
| [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) | |
|
||||
|
||||
## Interfaces
|
||||
|
||||
| Interface | Description |
|
||||
| --- | --- |
|
||||
| [BasePathSetup](./kibana-plugin-public.basepathsetup.md) | Provides access to the 'server.basePath' configuration option in kibana.yml |
|
||||
| [Capabilities](./kibana-plugin-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. |
|
||||
| [CapabilitiesSetup](./kibana-plugin-public.capabilitiessetup.md) | Capabilities Setup. |
|
||||
| [ChromeBadge](./kibana-plugin-public.chromebadge.md) | |
|
||||
| [ChromeBrand](./kibana-plugin-public.chromebrand.md) | |
|
||||
| [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) | |
|
||||
| [CoreSetup](./kibana-plugin-public.coresetup.md) | Core services exposed to the start lifecycle |
|
||||
| [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. |
|
||||
| [I18nSetup](./kibana-plugin-public.i18nsetup.md) | I18nSetup.Context is required by any localizable React component from @<!-- -->kbn/i18n and @<!-- -->elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. |
|
||||
| [InjectedMetadataSetup](./kibana-plugin-public.injectedmetadatasetup.md) | Provides access to the metadata injected by the server into the page |
|
||||
| [OverlaySetup](./kibana-plugin-public.overlaysetup.md) | |
|
||||
| [Plugin](./kibana-plugin-public.plugin.md) | The interface that should be returned by a <code>PluginInitializer</code>. |
|
||||
| [PluginInitializerContext](./kibana-plugin-public.plugininitializercontext.md) | The available core services passed to a <code>PluginInitializer</code> |
|
||||
| [PluginSetupContext](./kibana-plugin-public.pluginsetupcontext.md) | The available core services passed to a plugin's <code>Plugin#setup</code> method. |
|
||||
| [UiSettingsState](./kibana-plugin-public.uisettingsstate.md) | |
|
||||
|
||||
## Type Aliases
|
||||
|
||||
| Type Alias | Description |
|
||||
| --- | --- |
|
||||
| [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) | |
|
||||
| [ChromeSetup](./kibana-plugin-public.chromesetup.md) | |
|
||||
| [HttpSetup](./kibana-plugin-public.httpsetup.md) | |
|
||||
| [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | |
|
||||
| [PluginInitializer](./kibana-plugin-public.plugininitializer.md) | The <code>plugin</code> export at the root of a plugin's <code>public</code> directory should conform to this interface. |
|
||||
| [ToastInput](./kibana-plugin-public.toastinput.md) | |
|
||||
| [UiSettingsSetup](./kibana-plugin-public.uisettingssetup.md) | |
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
`xpack.infra.enabled`:: Set to `false` to disable the Logs and Infrastructure UI plugin {kib}. Defaults to `true`.
|
||||
|
||||
`xpack.infra.sources.default.logAlias`:: Index pattern for matching indices that contain log data. Defaults to `filebeat-*,kibana_sample_data_logs*`.
|
||||
`xpack.infra.sources.default.logAlias`:: Index pattern for matching indices that contain log data. Defaults to `filebeat-*,kibana_sample_data_logs*`. To match multiple wildcard patterns, use a comma to separate the names, with no space after the comma. For example, `logstash-app1-*,default-logs-*`.
|
||||
|
||||
`xpack.infra.sources.default.metricAlias`:: Index pattern for matching indices that contain Metricbeat data. Defaults to `metricbeat-*`.
|
||||
`xpack.infra.sources.default.metricAlias`:: Index pattern for matching indices that contain Metricbeat data. Defaults to `metricbeat-*`. To match multiple wildcard patterns, use a comma to separate the names, with no space after the comma. For example, `logstash-app1-*,default-logs-*`.
|
||||
|
||||
`xpack.infra.sources.default.fields.timestamp`:: Timestamp used to sort log entries. Defaults to `@timestamp`.
|
||||
|
||||
|
|
|
@ -79,7 +79,8 @@
|
|||
"resolutions": {
|
||||
"**/@types/node": "10.12.27",
|
||||
"**/@types/hapi": "^17.0.18",
|
||||
"**/typescript": "^3.3.3333"
|
||||
"**/typescript": "^3.3.3333",
|
||||
"**/@elastic/eui/**/core-js": "2.5.3"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
|
@ -134,7 +135,7 @@
|
|||
"bluebird": "3.5.3",
|
||||
"boom": "^7.2.0",
|
||||
"brace": "0.11.1",
|
||||
"cache-loader": "1.2.2",
|
||||
"cache-loader": "^3.0.0",
|
||||
"chalk": "^2.4.1",
|
||||
"color": "1.0.3",
|
||||
"commander": "2.8.1",
|
||||
|
@ -159,7 +160,7 @@
|
|||
"globby": "^8.0.1",
|
||||
"good-squeeze": "2.1.0",
|
||||
"h2o2": "^8.1.2",
|
||||
"handlebars": "4.0.13",
|
||||
"handlebars": "4.0.14",
|
||||
"hapi": "^17.5.3",
|
||||
"hapi-auth-cookie": "^9.0.0",
|
||||
"hjson": "3.1.0",
|
||||
|
|
|
@ -61,7 +61,7 @@ export default function (kibana) {
|
|||
api: [],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['config'],
|
||||
read: [],
|
||||
},
|
||||
ui: ['show'],
|
||||
},
|
||||
|
@ -69,7 +69,7 @@ export default function (kibana) {
|
|||
api: [],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['config'],
|
||||
read: [],
|
||||
},
|
||||
ui: ['show'],
|
||||
},
|
||||
|
|
252
rfcs/text/0002_encrypted_attributes.md
Normal file
252
rfcs/text/0002_encrypted_attributes.md
Normal file
|
@ -0,0 +1,252 @@
|
|||
- Start Date: 2019-03-22
|
||||
- RFC PR: [#33740](https://github.com/elastic/kibana/pull/33740)
|
||||
- Kibana Issue: (leave this empty)
|
||||
|
||||
# Summary
|
||||
|
||||
In order to support the action service we need a way to encrypt/decrypt
|
||||
attributes on saved objects that works with security and spaces filtering as
|
||||
well as performing audit logging. Sufficiently hides the private key used and
|
||||
removes encrypted attributes from being exposed through regular means.
|
||||
|
||||
# Basic example
|
||||
|
||||
Register saved object type with the `encrypted_saved_objects` plugin:
|
||||
|
||||
```typescript
|
||||
server.plugins.encrypted_saved_objects.registerType({
|
||||
type: 'server-action',
|
||||
attributesToEncrypt: new Set(['credentials', 'apiKey']),
|
||||
});
|
||||
```
|
||||
|
||||
Use the same API to create saved objects with encrypted attributes as for any other saved object type:
|
||||
|
||||
```typescript
|
||||
const savedObject = await server.savedObjects
|
||||
.getScopedSavedObjectsClient(request)
|
||||
.create('server-action', {
|
||||
name: 'my-server-action',
|
||||
data: { location: 'BBOX (100.0, ..., 0.0)', email: '<html>...</html>' },
|
||||
credentials: { username: 'some-user', password: 'some-password' },
|
||||
apiKey: 'dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvb'
|
||||
});
|
||||
|
||||
// savedObject = {
|
||||
// id: 'dd9750b9-ef0a-444c-8405-4dfcc2e9d670',
|
||||
// type: 'server-action',
|
||||
// name: 'my-server-action',
|
||||
// data: { location: 'BBOX (100.0, ..., 0.0)', email: '<html>...</html>' },
|
||||
// };
|
||||
|
||||
```
|
||||
|
||||
Use dedicated method to retrieve saved object with decrypted attributes on behalf of Kibana internal user:
|
||||
|
||||
```typescript
|
||||
const savedObject = await server.plugins.encrypted_saved_objects.getDecryptedAsInternalUser(
|
||||
'server-action',
|
||||
'dd9750b9-ef0a-444c-8405-4dfcc2e9d670'
|
||||
);
|
||||
|
||||
// savedObject = {
|
||||
// id: 'dd9750b9-ef0a-444c-8405-4dfcc2e9d670',
|
||||
// type: 'server-action',
|
||||
// name: 'my-server-action',
|
||||
// data: { location: 'BBOX (100.0, ..., 0.0)', email: '<html>...</html>' },
|
||||
// credentials: { username: 'some-user', password: 'some-password' },
|
||||
// apiKey: 'dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvb',
|
||||
// };
|
||||
```
|
||||
|
||||
# Motivation
|
||||
|
||||
Main motivation is the storage and usage of third-party credentials for use with
|
||||
the action service to do notifications. Also perform other types integrations,
|
||||
call webhooks using tokens.
|
||||
|
||||
# Detailed design
|
||||
|
||||
In order for this to be in basic it needs to be done as a wrapper around the
|
||||
saved object client. This can be added from the `x-pack` plugin.
|
||||
|
||||
## General
|
||||
|
||||
To be able to manage saved objects with encrypted attributes from any plugin one should
|
||||
do the following:
|
||||
|
||||
1. Define `encrypted_saved_objects` plugin as a dependency.
|
||||
2. Add attributes to be encrypted in `mappings.json` file for the respective saved object type. These attributes should
|
||||
always have a `binary` type since they'll contain encrypted content as a `Base64` encoded string and should never be
|
||||
searchable or analyzed. This makes defining of attributes that require encryption explicit and auditable, and significantly
|
||||
simplifies implementation:
|
||||
```json
|
||||
{
|
||||
"server-action": {
|
||||
"properties": {
|
||||
"name": { "type": "keyword" },
|
||||
"data": {
|
||||
"properties": {
|
||||
"location": { "type": "geo_shape" },
|
||||
"email": { "type": "text" }
|
||||
}
|
||||
},
|
||||
"credentials": { "type": "binary" },
|
||||
"apiKey": { "type": "binary" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
3. Register saved object type and attributes that should be encrypted with `encrypted_saved_objects` plugin:
|
||||
```typescript
|
||||
server.plugins.encrypted_saved_objects.registerType({
|
||||
type: 'server-action',
|
||||
attributesToEncrypt: new Set(['credentials', 'apiKey']),
|
||||
attributesToExcludeFromAAD: new Set(['data']),
|
||||
});
|
||||
```
|
||||
|
||||
Notice the optional `attributesToExcludeFromAAD` property, it allows one to exclude some of the saved object attributes
|
||||
from Additional authenticated data (AAD), read more about that below in `Encryption and decryption` section.
|
||||
|
||||
Since `encrypted_saved_objects` adds its own wrapper (`EncryptedSavedObjectsClientWrapper`) into `SavedObjectsClient`
|
||||
wrapper chain consumers will be able to create, update, delete and retrieve saved objects using standard Saved Objects API.
|
||||
Two main responsibilities of the wrapper are:
|
||||
|
||||
* It encrypts attributes that are supposed to be encrypted during `create`, `bulkCreate` and `update` operations
|
||||
* It strips encrypted attributes from **any** saved object returned from the Saved Objects API
|
||||
|
||||
As noted above the wrapper is stripping encrypted attributes from saved objects returned from the API methods, that means
|
||||
that there is no way at all to retrieve encrypted attributes using standard Saved Objects API unless `encrypted_saved_objects`
|
||||
plugin is disabled. This potentially can lead to the situation when consumer retrieves saved object, updates its non-encrypted
|
||||
properties and passes that same object to the `update` Saved Objects API method without re-defining encrypted attributes. In
|
||||
this case only specified attributes will be updated and encrypted attributes will stay untouched. And if these updated
|
||||
attributes are included into AAD, that is true by default for all attributes unless they are specifically excluded via
|
||||
`attributesToExcludeFromAAD`, then it will be no longer possible to decrypt encrypted attributes. At this stage we consider
|
||||
this as a developer mistake and don't prevent it from happening in any way apart from logging this type of event. Partial
|
||||
update of only attributes that are not the part of AAD will not cause this issue.
|
||||
|
||||
Saved object ID is an essential part of AAD used during encryption process and hence should be as hard to guess as possible.
|
||||
To fulfil this requirement wrapper generates highly random IDs (UUIDv4) for the saved objects that contain encrypted
|
||||
attributes and hence consumers are not allowed to specify ID when calling `create` or `bulkCreate` method and if they try
|
||||
to do so the error will be thrown.
|
||||
|
||||
To reduce the risk of unintentional decryption and consequent leaking of the sensitive information there is only one way
|
||||
to retrieve saved object and decrypt its encrypted attributes and it's exposed only through `encrypted_saved_objects` plugin:
|
||||
|
||||
```typescript
|
||||
const savedObject = await server.plugins.encrypted_saved_objects.getDecryptedAsInternalUser(
|
||||
'server-action',
|
||||
'dd9750b9-ef0a-444c-8405-4dfcc2e9d670'
|
||||
);
|
||||
|
||||
// savedObject = {
|
||||
// id: 'dd9750b9-ef0a-444c-8405-4dfcc2e9d670',
|
||||
// type: 'server-action',
|
||||
// name: 'my-server-action',
|
||||
// data: { location: 'BBOX (100.0, ..., 0.0)', email: '<html>...</html>' },
|
||||
// credentials: { username: 'some-user', password: 'some-password' },
|
||||
// apiKey: 'dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvb',
|
||||
// };
|
||||
```
|
||||
|
||||
As can be seen from the method name, the request to retrieve saved object and decrypt its attributes is performed on
|
||||
behalf of the internal Kibana user and hence isn't supposed to be called within user request context.
|
||||
|
||||
**Note:** the fact that saved object with encrypted attributes is created using standard Saved Objects API within a
|
||||
particular user and space context, but retrieved out of any context makes it unclear how consumers are supposed to
|
||||
provide that context and retrieve saved object from a particular space. Current plan for `getDecryptedAsInternalUser`
|
||||
method is to accept a third `BaseOptions` argument that allows consumers to specify `namespace` that they can retrieve
|
||||
from the request using public `spaces` plugin API.
|
||||
|
||||
## Encryption and decryption
|
||||
|
||||
Saved object attributes are encrypted using [@elastic/node-crypto](https://github.com/elastic/node-crypto) library. Please
|
||||
take a look at the source code of this library to know how encryption is performed exactly, what algorithm and encryption
|
||||
parameters are used, but in short it's AES Encryption with AES-256-GCM that uses random initialization vector and salt.
|
||||
|
||||
As with encryption key for Kibana's session cookie, master encryption key used by `encrypted_saved_objects` plugin can be
|
||||
defined as a configuration value (`xpack.encrypted_saved_objects.encryptionKey`) via `kibana.yml`, but it's **highly
|
||||
recommended** to define this key in the [Kibana Keystore](https://www.elastic.co/guide/en/kibana/current/secure-settings.html)
|
||||
instead. The master key should be cryptographically safe and be equal or greater than 32 bytes.
|
||||
|
||||
To prevent certain vectors of attacks where raw content of encrypted attributes of one saved object is copied to another
|
||||
saved object which would unintentionally allow it to decrypt content that was not supposed to be decrypted we rely on Additional
|
||||
authenticated data (AAD) during encryption and decryption. AAD consists of the following components:
|
||||
|
||||
* Saved object ID
|
||||
* Saved object type
|
||||
* Saved object attributes
|
||||
|
||||
AAD does not include encrypted attributes themselves and attributes defined in optional `attributesToExcludeFromAAD`
|
||||
parameter provided during saved object type registration with `encrypted_saved_objects` plugin. There are a number of
|
||||
reasons why one would want to exclude certain attributes from AAD:
|
||||
|
||||
* if attribute contains large amount of data that can significantly slow down encryption and decryption, especially during
|
||||
bulk operations (e.g. large geo shape or arbitrary HTML document)
|
||||
* if attribute contains data that is supposed to be updated separately from encrypted attributes or attributes included
|
||||
into AAD (e.g some user defined content associated with the email action or alert)
|
||||
|
||||
## Audit
|
||||
|
||||
Encrypted attributes will most likely contain sensitive information and any attempt to access these should be properly
|
||||
logged to allow any further audit procedures. The following events will be logged with Kibana audit log functionality:
|
||||
|
||||
* Successful attempt to encrypt attributes (incl. saved object ID, type and attributes names)
|
||||
* Failed attempt to encrypt attribute (incl. saved object ID, type and attribute name)
|
||||
* Successful attempt to decrypt attributes (incl. saved object ID, type and attributes names)
|
||||
* Failed attempt to decrypt attribute (incl. saved object ID, type and attribute name)
|
||||
|
||||
In addition to audit log events we'll issue ordinary log events for any attempts to save, update or decrypt saved objects
|
||||
with missing attributes that were supposed to be encrypted/decrypted based on the registration parameters.
|
||||
|
||||
# Benefits
|
||||
|
||||
* None of the registered types will expose their encrypted details. The saved
|
||||
objects with their unencrypted attributes could still be obtained and searched
|
||||
on. The wrapper will follow all the security and spaces filtering of saved
|
||||
objects so that only users with appropriate permissions will be able to obtain
|
||||
the scrubbed objects or _save_ objects with encrypted attributes.
|
||||
|
||||
* No explicit access to a method that takes in an encrypted string exists. If the
|
||||
type was not registered no decryption is possible. No need to handle the saved object
|
||||
with the encrypted attributes reducing the risk of accidentally returning it in a
|
||||
handler.
|
||||
|
||||
# Drawbacks
|
||||
|
||||
* It isn't possible to decrypt existing encrypted attributes once encryption key changes
|
||||
* Possibly have a performance impact on Saved Objects API operations that require encryption/decryption
|
||||
* Will require non trivial tests to test functionality along with spaces and security
|
||||
* The attributes that are encrypted have to be defined and if they change they need to be migrated
|
||||
|
||||
# Out of scope
|
||||
|
||||
* Encryption key rotation mechanism, either regular or emergency
|
||||
* Mechanism that would detect and warn when Kibana does not use keystore to store encryption key
|
||||
|
||||
# Alternatives
|
||||
|
||||
Only allow this to be used within the Actions service itself where the details
|
||||
of the saved object are handled there directly. And the saved objects are
|
||||
`hidden` but still use the security and spaces wrappers.
|
||||
|
||||
# Adoption strategy
|
||||
|
||||
Integration should be pretty easy which would include depending on the plugin, registering the desired saved object type
|
||||
with it and defining encrypted attributes in the `mappings.json`.
|
||||
|
||||
# How we teach this
|
||||
|
||||
The `encrypted_saved_objects` as the name of the `thing` where it's seen as a separate
|
||||
extension on top of the saved object service.
|
||||
|
||||
Provide a README.md in the plugin directory with the usage examples.
|
||||
|
||||
# Unresolved questions
|
||||
|
||||
* Is it acceptable to have this plugin in Basic?
|
||||
* Are there any other use-cases that are not served with that interface?
|
||||
* How would this work with Saved Objects Export\Import API?
|
||||
* How would this work with migrations, if the attribute names wanted to be
|
||||
changed, a decrypt context would need to be created for migration?
|
|
@ -17,7 +17,13 @@
|
|||
* under the License.
|
||||
*/
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { ChromeBrand, ChromeBreadcrumb, ChromeService, ChromeSetup } from './chrome_service';
|
||||
import {
|
||||
ChromeBadge,
|
||||
ChromeBrand,
|
||||
ChromeBreadcrumb,
|
||||
ChromeService,
|
||||
ChromeSetup,
|
||||
} from './chrome_service';
|
||||
|
||||
const createSetupContractMock = () => {
|
||||
const setupContract: jest.Mocked<ChromeSetup> = {
|
||||
|
@ -30,6 +36,8 @@ const createSetupContractMock = () => {
|
|||
addApplicationClass: jest.fn(),
|
||||
removeApplicationClass: jest.fn(),
|
||||
getApplicationClasses$: jest.fn(),
|
||||
getBadge$: jest.fn(),
|
||||
setBadge: jest.fn(),
|
||||
getBreadcrumbs$: jest.fn(),
|
||||
setBreadcrumbs: jest.fn(),
|
||||
getHelpExtension$: jest.fn(),
|
||||
|
@ -39,6 +47,7 @@ const createSetupContractMock = () => {
|
|||
setupContract.getIsVisible$.mockReturnValue(new BehaviorSubject(false));
|
||||
setupContract.getIsCollapsed$.mockReturnValue(new BehaviorSubject(false));
|
||||
setupContract.getApplicationClasses$.mockReturnValue(new BehaviorSubject(['class-name']));
|
||||
setupContract.getBadge$.mockReturnValue(new BehaviorSubject({} as ChromeBadge));
|
||||
setupContract.getBreadcrumbs$.mockReturnValue(new BehaviorSubject([{} as ChromeBreadcrumb]));
|
||||
setupContract.getHelpExtension$.mockReturnValue(new BehaviorSubject(undefined));
|
||||
return setupContract;
|
||||
|
|
|
@ -246,6 +246,37 @@ Array [
|
|||
});
|
||||
});
|
||||
|
||||
describe('badge', () => {
|
||||
it('updates/emits the current badge', async () => {
|
||||
const service = new ChromeService({ browserSupportsCsp: true });
|
||||
const setup = service.setup(defaultSetupDeps());
|
||||
const promise = setup
|
||||
.getBadge$()
|
||||
.pipe(toArray())
|
||||
.toPromise();
|
||||
|
||||
setup.setBadge({ text: 'foo', tooltip: `foo's tooltip` });
|
||||
setup.setBadge({ text: 'bar', tooltip: `bar's tooltip` });
|
||||
setup.setBadge(undefined);
|
||||
service.stop();
|
||||
|
||||
await expect(promise).resolves.toMatchInlineSnapshot(`
|
||||
Array [
|
||||
undefined,
|
||||
Object {
|
||||
"text": "foo",
|
||||
"tooltip": "foo's tooltip",
|
||||
},
|
||||
Object {
|
||||
"text": "bar",
|
||||
"tooltip": "bar's tooltip",
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('breadcrumbs', () => {
|
||||
it('updates/emits the current set of breadcrumbs', async () => {
|
||||
const service = new ChromeService({ browserSupportsCsp: true });
|
||||
|
|
|
@ -22,6 +22,7 @@ import * as Url from 'url';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import * as Rx from 'rxjs';
|
||||
import { map, takeUntil } from 'rxjs/operators';
|
||||
import { IconType } from '@elastic/eui';
|
||||
import { InjectedMetadataSetup } from '../injected_metadata';
|
||||
import { NotificationsSetup } from '../notifications';
|
||||
|
||||
|
@ -32,6 +33,13 @@ function isEmbedParamInHash() {
|
|||
return Boolean(query.embed);
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ChromeBadge {
|
||||
text: string;
|
||||
tooltip: string;
|
||||
iconType?: IconType;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ChromeBrand {
|
||||
logo?: string;
|
||||
|
@ -75,6 +83,7 @@ export class ChromeService {
|
|||
const applicationClasses$ = new Rx.BehaviorSubject<Set<string>>(new Set());
|
||||
const helpExtension$ = new Rx.BehaviorSubject<ChromeHelpExtension | undefined>(undefined);
|
||||
const breadcrumbs$ = new Rx.BehaviorSubject<ChromeBreadcrumb[]>([]);
|
||||
const badge$ = new Rx.BehaviorSubject<ChromeBadge | undefined>(undefined);
|
||||
|
||||
if (!this.browserSupportsCsp && injectedMetadata.getCspConfig().warnLegacyBrowsers) {
|
||||
notifications.toasts.addWarning(
|
||||
|
@ -175,6 +184,18 @@ export class ChromeService {
|
|||
takeUntil(this.stop$)
|
||||
),
|
||||
|
||||
/**
|
||||
* Get an observable of the current badge
|
||||
*/
|
||||
getBadge$: () => badge$.pipe(takeUntil(this.stop$)),
|
||||
|
||||
/**
|
||||
* Override the current badge
|
||||
*/
|
||||
setBadge: (badge: ChromeBadge | undefined) => {
|
||||
badge$.next(badge);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get an observable of the current list of breadcrumbs
|
||||
*/
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
|
||||
export {
|
||||
ChromeBadge,
|
||||
ChromeBreadcrumb,
|
||||
ChromeService,
|
||||
ChromeSetup,
|
||||
|
|
|
@ -19,7 +19,13 @@
|
|||
|
||||
import { BasePathSetup } from './base_path';
|
||||
import { Capabilities, CapabilitiesStart } from './capabilities';
|
||||
import { ChromeBrand, ChromeBreadcrumb, ChromeHelpExtension, ChromeSetup } from './chrome';
|
||||
import {
|
||||
ChromeBadge,
|
||||
ChromeBrand,
|
||||
ChromeBreadcrumb,
|
||||
ChromeHelpExtension,
|
||||
ChromeSetup,
|
||||
} from './chrome';
|
||||
import { FatalErrorsSetup } from './fatal_errors';
|
||||
import { HttpSetup } from './http';
|
||||
import { I18nSetup, I18nStart } from './i18n';
|
||||
|
@ -89,6 +95,7 @@ export {
|
|||
Capabilities,
|
||||
CapabilitiesStart,
|
||||
ChromeSetup,
|
||||
ChromeBadge,
|
||||
ChromeBreadcrumb,
|
||||
ChromeBrand,
|
||||
ChromeHelpExtension,
|
||||
|
|
|
@ -77,6 +77,7 @@ export class LegacyPlatformService {
|
|||
require('ui/chrome/api/controls').__newPlatformSetup__(chrome);
|
||||
require('ui/chrome/api/help_extension').__newPlatformSetup__(chrome);
|
||||
require('ui/chrome/api/theme').__newPlatformSetup__(chrome);
|
||||
require('ui/chrome/api/badge').__newPlatformSetup__(chrome);
|
||||
require('ui/chrome/api/breadcrumbs').__newPlatformSetup__(chrome);
|
||||
require('ui/chrome/services/global_nav_state').__newPlatformSetup__(chrome);
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import * as CSS from 'csstype';
|
||||
import { default } from 'react';
|
||||
import { IconType } from '@elastic/eui';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import * as Rx from 'rxjs';
|
||||
import { Toast } from '@elastic/eui';
|
||||
|
@ -32,6 +33,16 @@ export interface CapabilitiesStart {
|
|||
getCapabilities: () => Capabilities;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ChromeBadge {
|
||||
// (undocumented)
|
||||
iconType?: IconType;
|
||||
// (undocumented)
|
||||
text: string;
|
||||
// (undocumented)
|
||||
tooltip: string;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ChromeBrand {
|
||||
// (undocumented)
|
||||
|
|
|
@ -26,7 +26,7 @@ export const CreateEmptyDirsAndFilesTask = {
|
|||
await Promise.all([
|
||||
mkdirp(build.resolvePath('plugins')),
|
||||
mkdirp(build.resolvePath('data')),
|
||||
write(build.resolvePath('optimize/.babelcache.json'), '{}'),
|
||||
write(build.resolvePath('optimize/.babel_register_cache.json'), '{}'),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -48,6 +48,7 @@ export const OptimizeBuildTask = {
|
|||
cwd: build.resolvePath('.'),
|
||||
env: {
|
||||
FORCE_DLL_CREATION: 'true',
|
||||
KBN_CACHE_LOADER_WRITABLE: 'true',
|
||||
NODE_OPTIONS: '--max-old-space-size=2048'
|
||||
},
|
||||
});
|
||||
|
|
|
@ -66,7 +66,7 @@ async function patchNodeGit(config, log, build, platform) {
|
|||
const downloadPath = build.resolvePathForPlatform(platform, '.nodegit_binaries', packageName);
|
||||
const extractDir = await downloadAndExtractTarball(downloadUrl, downloadPath, log, 3);
|
||||
|
||||
const destination = build.resolvePathForPlatform(platform, 'node_modules/nodegit/build/Release');
|
||||
const destination = build.resolvePathForPlatform(platform, 'node_modules/@elastic/nodegit/build/Release');
|
||||
log.debug('Replacing nodegit binaries from ', extractDir);
|
||||
await deleteAll([destination], log);
|
||||
await scanCopy({
|
||||
|
|
|
@ -2,19 +2,31 @@
|
|||
"put_settings": {
|
||||
"data_autocomplete_rules": {
|
||||
"refresh_interval": "1s",
|
||||
"number_of_shards": 5,
|
||||
"number_of_shards": 1,
|
||||
"number_of_replicas": 1,
|
||||
"blocks.read_only": {
|
||||
"__one_of": [false, true]
|
||||
"__one_of": [
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"blocks.read": {
|
||||
"__one_of": [true, false]
|
||||
"__one_of": [
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"blocks.write": {
|
||||
"__one_of": [true, false]
|
||||
"__one_of": [
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"blocks.metadata": {
|
||||
"__one_of": [true, false]
|
||||
"__one_of": [
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"term_index_interval": 32,
|
||||
"term_index_divisor": 1,
|
||||
|
@ -22,7 +34,10 @@
|
|||
"translog.flush_threshold_size": "200mb",
|
||||
"translog.flush_threshold_period": "30m",
|
||||
"translog.disable_flush": {
|
||||
"__one_of": [true, false]
|
||||
"__one_of": [
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"cache.filter.max_size": "2gb",
|
||||
"cache.filter.expire": "2h",
|
||||
|
@ -42,10 +57,19 @@
|
|||
}
|
||||
},
|
||||
"recovery.initial_shards": {
|
||||
"__one_of": ["quorum", "quorum-1", "half", "full", "full-1"]
|
||||
"__one_of": [
|
||||
"quorum",
|
||||
"quorum-1",
|
||||
"half",
|
||||
"full",
|
||||
"full-1"
|
||||
]
|
||||
},
|
||||
"ttl.disable_purge": {
|
||||
"__one_of": [true, false]
|
||||
"__one_of": [
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"analysis": {
|
||||
"analyzer": {},
|
||||
|
@ -54,18 +78,31 @@
|
|||
"char_filter": {}
|
||||
},
|
||||
"cache.query.enable": {
|
||||
"__one_of": [true, false]
|
||||
"__one_of": [
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"shadow_replicas": {
|
||||
"__one_of": [true, false]
|
||||
"__one_of": [
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"shared_filesystem": {
|
||||
"__one_of": [true, false]
|
||||
"__one_of": [
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"data_path": "path",
|
||||
"codec": {
|
||||
"__one_of": ["default", "best_compression", "lucene_default"]
|
||||
"__one_of": [
|
||||
"default",
|
||||
"best_compression",
|
||||
"lucene_default"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -113,7 +113,8 @@ export default function (kibana) {
|
|||
),
|
||||
uiCapabilities: {
|
||||
dev_tools: {
|
||||
show: true
|
||||
show: true,
|
||||
save: true,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
@ -55,7 +55,7 @@ function runServerFunctions(server) {
|
|||
id: Joi.number().required(),
|
||||
functionName: Joi.string().required(),
|
||||
args: Joi.object().default({}),
|
||||
context: Joi.object().allow(null).default({}),
|
||||
context: Joi.any().default(null),
|
||||
}),
|
||||
).required(),
|
||||
}).required(),
|
||||
|
|
|
@ -453,7 +453,7 @@ export function filebeatStatusCheck(moduleName) {
|
|||
bool: {
|
||||
filter: {
|
||||
term: {
|
||||
'fileset.module': moduleName,
|
||||
'event.module': moduleName,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -440,7 +440,7 @@ export function metricbeatStatusCheck(moduleName) {
|
|||
bool: {
|
||||
filter: {
|
||||
term: {
|
||||
'metricset.module': moduleName,
|
||||
'event.module': moduleName,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -172,7 +172,7 @@ export default function (kibana) {
|
|||
save: true
|
||||
},
|
||||
indexPatterns: {
|
||||
createNew: true,
|
||||
save: true,
|
||||
},
|
||||
savedObjectsManagement: savedObjects.types.reduce((acc, type) => ({
|
||||
...acc,
|
||||
|
|
|
@ -57,7 +57,22 @@ function createNewDashboardCtrl($scope, i18n) {
|
|||
uiRoutes
|
||||
.defaults(/dashboard/, {
|
||||
requireDefaultIndex: true,
|
||||
requireUICapability: 'dashboard.show'
|
||||
requireUICapability: 'dashboard.show',
|
||||
badge: (i18n, uiCapabilities) => {
|
||||
if (uiCapabilities.dashboard.showWriteControls) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
text: i18n('kbn.dashboard.badge.readOnly.text', {
|
||||
defaultMessage: 'Read only',
|
||||
}),
|
||||
tooltip: i18n('kbn.dashboard.badge.readOnly.tooltip', {
|
||||
defaultMessage: 'Unable to save dashboards',
|
||||
}),
|
||||
iconType: 'glasses'
|
||||
};
|
||||
}
|
||||
})
|
||||
.when(DashboardConstants.LANDING_PAGE_PATH, {
|
||||
template: dashboardListingTemplate,
|
||||
|
|
|
@ -34,6 +34,21 @@ uiRoutes
|
|||
});
|
||||
|
||||
uiRoutes.defaults(/^\/dev_tools(\/|$)/, {
|
||||
badge: (i18n, uiCapabilities) => {
|
||||
if (uiCapabilities.dev_tools.save) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
text: i18n('kbn.devTools.badge.readOnly.text', {
|
||||
defaultMessage: 'Read only',
|
||||
}),
|
||||
tooltip: i18n('kbn.devTools.badge.readOnly.tooltip', {
|
||||
defaultMessage: 'Unable to save',
|
||||
}),
|
||||
iconType: 'glasses'
|
||||
};
|
||||
},
|
||||
k7Breadcrumbs: (i18n) => [
|
||||
{
|
||||
text: i18n('kbn.devTools.k7BreadcrumbsDevToolsLabel', {
|
||||
|
|
|
@ -93,6 +93,21 @@ uiRoutes
|
|||
? getSavedSearchBreadcrumbs
|
||||
: getRootBreadcrumbs
|
||||
),
|
||||
badge: (i18n, uiCapabilities) => {
|
||||
if (uiCapabilities.discover.save) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
text: i18n('kbn.discover.badge.readOnly.text', {
|
||||
defaultMessage: 'Read only',
|
||||
}),
|
||||
tooltip: i18n('kbn.discover.badge.readOnly.tooltip', {
|
||||
defaultMessage: 'Unable to save searches',
|
||||
}),
|
||||
iconType: 'glasses'
|
||||
};
|
||||
}
|
||||
})
|
||||
.when('/discover/:id?', {
|
||||
template: indexTemplate,
|
||||
|
|
|
@ -85,7 +85,7 @@ class SavedObjectsInstallerUi extends React.Component {
|
|||
}
|
||||
|
||||
const errors = resp.savedObjects.filter(savedObject => {
|
||||
return savedObject.hasOwnProperty('error');
|
||||
return Boolean(savedObject.error);
|
||||
});
|
||||
|
||||
const overwriteErrors = errors.filter(savedObject => {
|
||||
|
|
|
@ -64,7 +64,7 @@ class CreateButtonComponent extends Component<Props, State> {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (!uiCapabilities.indexPatterns.createNew) {
|
||||
if (!uiCapabilities.indexPatterns.save) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -86,6 +86,21 @@ uiRoutes
|
|||
.defaults(/management\/kibana\/(index_patterns|index_pattern)/, {
|
||||
resolve: indexPatternsResolutions,
|
||||
requireUICapability: 'management.kibana.index_patterns',
|
||||
badge: (i18n, uiCapabilities) => {
|
||||
if (uiCapabilities.indexPatterns.save) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
text: i18n('kbn.management.indexPatterns.badge.readOnly.text', {
|
||||
defaultMessage: 'Read only',
|
||||
}),
|
||||
tooltip: i18n('kbn.management.indexPatterns.badge.readOnly.tooltip', {
|
||||
defaultMessage: 'Unable to save index patterns',
|
||||
}),
|
||||
iconType: 'glasses'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
uiRoutes
|
||||
|
|
|
@ -578,6 +578,7 @@ exports[`AdvancedSettings should render normally 1`] = `
|
|||
showNoResultsMessage={true}
|
||||
/>
|
||||
<advanced_settings_page_footer
|
||||
enableSaving={true}
|
||||
onQueryMatchChange={[Function]}
|
||||
query={
|
||||
Query {
|
||||
|
@ -738,6 +739,7 @@ exports[`AdvancedSettings should render read-only when saving is disabled 1`] =
|
|||
showNoResultsMessage={true}
|
||||
/>
|
||||
<advanced_settings_page_footer
|
||||
enableSaving={false}
|
||||
onQueryMatchChange={[Function]}
|
||||
query={
|
||||
Query {
|
||||
|
@ -916,6 +918,7 @@ exports[`AdvancedSettings should render specific setting if given setting key 1`
|
|||
showNoResultsMessage={true}
|
||||
/>
|
||||
<advanced_settings_page_footer
|
||||
enableSaving={true}
|
||||
onQueryMatchChange={[Function]}
|
||||
query={
|
||||
Query {
|
||||
|
|
|
@ -188,7 +188,7 @@ export class AdvancedSettings extends Component {
|
|||
showNoResultsMessage={!footerQueryMatched}
|
||||
enableSaving={this.props.enableSaving}
|
||||
/>
|
||||
<PageFooter query={query} onQueryMatchChange={this.onFooterQueryMatchChange} />
|
||||
<PageFooter query={query} onQueryMatchChange={this.onFooterQueryMatchChange} enableSaving={this.props.enableSaving} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -63,6 +63,21 @@ uiRoutes
|
|||
template: indexTemplate,
|
||||
k7Breadcrumbs: getBreadcrumbs,
|
||||
requireUICapability: 'management.kibana.settings',
|
||||
badge: (i18n, uiCapabilities) => {
|
||||
if (uiCapabilities.advancedSettings.save) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
text: i18n('kbn.management.advancedSettings.badge.readOnly.text', {
|
||||
defaultMessage: 'Read only',
|
||||
}),
|
||||
tooltip: i18n('kbn.management.advancedSettings.badge.readOnly.tooltip', {
|
||||
defaultMessage: 'Unable to save advanced settings',
|
||||
}),
|
||||
iconType: 'glasses'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
uiModules.get('apps/management')
|
||||
|
|
|
@ -33,6 +33,21 @@ uiRoutes
|
|||
.defaults(/visualize/, {
|
||||
requireDefaultIndex: true,
|
||||
requireUICapability: 'visualize.show',
|
||||
badge: (i18n, uiCapabilities) => {
|
||||
if (uiCapabilities.visualize.save) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
text: i18n('kbn.visualize.badge.readOnly.text', {
|
||||
defaultMessage: 'Read only',
|
||||
}),
|
||||
tooltip: i18n('kbn.visualize.badge.readOnly.tooltip', {
|
||||
defaultMessage: 'Unable to save visualizations',
|
||||
}),
|
||||
iconType: 'glasses'
|
||||
};
|
||||
}
|
||||
})
|
||||
.when(VisualizeConstants.LANDING_PAGE_PATH, {
|
||||
template: visualizeListingTemplate,
|
||||
|
|
|
@ -16,8 +16,9 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import handleAnnotationResponse from './handle_annotation_response';
|
||||
import { handleAnnotationResponse } from './response_processors/annotations/';
|
||||
import { getAnnotationRequestParams } from './annorations/get_request_params';
|
||||
import { getLastSeriesTimestamp } from './helpers/timestamp';
|
||||
|
||||
function validAnnotation(annotation) {
|
||||
return annotation.index_pattern &&
|
||||
|
@ -28,10 +29,19 @@ function validAnnotation(annotation) {
|
|||
!annotation.hidden;
|
||||
}
|
||||
|
||||
export async function getAnnotations(req, panel, esQueryConfig, searchStrategy, capabilities) {
|
||||
export async function getAnnotations({
|
||||
req,
|
||||
esQueryConfig,
|
||||
searchStrategy,
|
||||
panel,
|
||||
capabilities,
|
||||
series
|
||||
}) {
|
||||
const panelIndexPattern = panel.index_pattern;
|
||||
const searchRequest = searchStrategy.getSearchRequest(req, panelIndexPattern);
|
||||
const annotations = panel.annotations.filter(validAnnotation);
|
||||
const lastSeriesTimestamp = getLastSeriesTimestamp(series);
|
||||
const handleAnnotationResponseBy = handleAnnotationResponse(lastSeriesTimestamp);
|
||||
|
||||
const bodiesPromises = annotations.map(annotation => getAnnotationRequestParams(req, panel, annotation, esQueryConfig, capabilities));
|
||||
const body = (await Promise.all(bodiesPromises))
|
||||
|
@ -44,7 +54,7 @@ export async function getAnnotations(req, panel, esQueryConfig, searchStrategy,
|
|||
|
||||
return annotations
|
||||
.reduce((acc, annotation, index) => {
|
||||
acc[annotation.id] = handleAnnotationResponse(responses[index], annotation);
|
||||
acc[annotation.id] = handleAnnotationResponseBy(responses[index], annotation);
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
|
|
@ -44,10 +44,18 @@ export async function getSeriesData(req, panel) {
|
|||
try {
|
||||
const data = await searchRequest.search({ body });
|
||||
const series = data.map(handleResponseBody(panel));
|
||||
|
||||
let annotations = null;
|
||||
|
||||
if (panel.annotations && panel.annotations.length) {
|
||||
annotations = await getAnnotations(req, panel, esQueryConfig, searchStrategy, capabilities);
|
||||
annotations = await getAnnotations({
|
||||
req,
|
||||
esQueryConfig,
|
||||
searchStrategy,
|
||||
panel,
|
||||
capabilities,
|
||||
series
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { last } from 'lodash';
|
||||
|
||||
/**
|
||||
* @param {Array} seriesGroup
|
||||
* [
|
||||
* [
|
||||
* {
|
||||
* data: [
|
||||
* [1555189200000, 12],
|
||||
* [1555191100000, 42],
|
||||
* [1555263300000, 95],
|
||||
* ...coordinates,
|
||||
* ]
|
||||
* ...properties,
|
||||
* }
|
||||
* ...series,
|
||||
* ]
|
||||
* ...seriesGroups,
|
||||
* ]
|
||||
* @return {number} lastTimestamp
|
||||
*/
|
||||
export function getLastSeriesTimestamp(seriesGroup = []) {
|
||||
let lastTimestamp = null;
|
||||
|
||||
seriesGroup.forEach(series => {
|
||||
series.forEach(({ data }) => {
|
||||
const [ dataLastTimestamp ] = last(data);
|
||||
|
||||
lastTimestamp = Math.max(lastTimestamp, dataLastTimestamp);
|
||||
});
|
||||
});
|
||||
|
||||
return lastTimestamp;
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 { getLastSeriesTimestamp } from './timestamp';
|
||||
|
||||
describe('src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/timestamp.js', () => {
|
||||
let series;
|
||||
const lastTimestamp = 10000;
|
||||
|
||||
beforeEach(() => {
|
||||
series = [
|
||||
[
|
||||
{
|
||||
id: 1,
|
||||
data: [
|
||||
[100, 43],
|
||||
[1000, 56],
|
||||
[lastTimestamp, 59],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
data: [
|
||||
[100, 33],
|
||||
[1000, 16],
|
||||
[lastTimestamp, 29],
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
id: 2,
|
||||
data: [
|
||||
[100, 3],
|
||||
[1000, 6],
|
||||
[lastTimestamp, 9],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
data: [
|
||||
[100, 5],
|
||||
[1000, 7],
|
||||
[lastTimestamp, 9],
|
||||
],
|
||||
},
|
||||
],
|
||||
];
|
||||
});
|
||||
|
||||
describe('getLastSeriesTimestamp()', () => {
|
||||
test('should return the last timestamp', () => {
|
||||
const timestamp = getLastSeriesTimestamp(series);
|
||||
|
||||
expect(timestamp).toBe(lastTimestamp);
|
||||
});
|
||||
|
||||
test('should return the max last timestamp of series', () => {
|
||||
const maxLastTimestamp = 20000;
|
||||
|
||||
series[0][1].data = [[100, 5], [1000, 7], [maxLastTimestamp, 50]];
|
||||
|
||||
const timestamp = getLastSeriesTimestamp(series);
|
||||
|
||||
expect(timestamp).toBe(maxLastTimestamp);
|
||||
});
|
||||
|
||||
test('should return null if nothing is passed', () => {
|
||||
const timestamp = getLastSeriesTimestamp();
|
||||
|
||||
expect(timestamp).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -17,9 +17,10 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
export default function handleAnnotationResponse(resp, annotation) {
|
||||
return _.get(resp, `aggregations.${annotation.id}.buckets`, [])
|
||||
import { get } from 'lodash';
|
||||
|
||||
export function getAnnotationBuckets(resp, annotation) {
|
||||
return get(resp, `aggregations.${annotation.id}.buckets`, [])
|
||||
.filter(bucket => bucket.hits.hits.total)
|
||||
.map((bucket) => {
|
||||
return {
|
|
@ -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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Function} by - it's a callback which determines how data will be mapped.
|
||||
* @return {Function} function - a predefined filter function
|
||||
*/
|
||||
export const makeFilter = by =>
|
||||
/**
|
||||
* @param {*} value
|
||||
* @return {Function} function - the predefined filter function with a filter value
|
||||
*/
|
||||
value =>
|
||||
/**
|
||||
* @param {Array|Object} data
|
||||
* @return {*} result - it depends on "by" outcome.
|
||||
*/
|
||||
data => by(data, value);
|
||||
|
||||
/**
|
||||
* @param {Array} annotations
|
||||
* [
|
||||
* {key: 1555189200000, ...},
|
||||
* {key: 1555263300000, ...},
|
||||
* ]
|
||||
* @param {*} filterValue
|
||||
* @return {Array} filtered array
|
||||
*/
|
||||
export const annotationFilter = (annotations, filterValue) => annotations.filter(({ key }) => key <= filterValue);
|
||||
|
||||
/**
|
||||
* @type {Function}
|
||||
*/
|
||||
export const filterAnnotations = makeFilter(annotationFilter);
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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 { makeFilter, annotationFilter } from './filter';
|
||||
|
||||
describe('src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/annotations/annotations.js', () => {
|
||||
let annotations;
|
||||
|
||||
beforeEach(() => {
|
||||
annotations = [
|
||||
{
|
||||
key: 100,
|
||||
},
|
||||
{
|
||||
key: 1000,
|
||||
},
|
||||
{
|
||||
key: 10000,
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
describe('makeFilter()', () => {
|
||||
test('should call accepted filter with accepted data and value', () => {
|
||||
const by = jest.fn();
|
||||
const value = 42;
|
||||
const data = [];
|
||||
|
||||
makeFilter(by)(value)(data);
|
||||
|
||||
expect(by).toHaveBeenCalledWith(data, value);
|
||||
});
|
||||
});
|
||||
|
||||
describe('annotationFilter()', () => {
|
||||
test('should filter annotations by passed value correctly', () => {
|
||||
const expectedResult = [
|
||||
{
|
||||
key: 100,
|
||||
},
|
||||
{
|
||||
key: 1000,
|
||||
},
|
||||
];
|
||||
|
||||
expect(annotationFilter(annotations, 1000)).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 { flow } from 'lodash';
|
||||
import { filterAnnotations } from './filter';
|
||||
import { getAnnotationBuckets } from './buckets';
|
||||
|
||||
export const handleAnnotationResponse = timestamp => flow(getAnnotationBuckets, filterAnnotations(timestamp));
|
|
@ -17,8 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { resolve } from 'path';
|
||||
|
||||
export default function (kibana) {
|
||||
return new kibana.Plugin({
|
||||
uiExports: {
|
||||
|
@ -28,7 +26,6 @@ export default function (kibana) {
|
|||
hidden: true,
|
||||
url: '/status',
|
||||
},
|
||||
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
@import 'src/legacy/ui/public/styles/styling_constants';
|
|
@ -46,16 +46,21 @@ export default function (kibana) {
|
|||
main: 'plugins/timelion/app',
|
||||
},
|
||||
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
|
||||
injectDefaultVars(server) {
|
||||
return {
|
||||
timelionUiEnabled: server.config().get('timelion.ui.enabled'),
|
||||
};
|
||||
},
|
||||
hacks: [
|
||||
'plugins/timelion/hacks/toggle_app_link_in_nav',
|
||||
'plugins/timelion/lib/panel_registry',
|
||||
'plugins/timelion/panels/timechart/timechart'
|
||||
],
|
||||
injectDefaultVars(server) {
|
||||
return {
|
||||
timelionUiEnabled: server.config().get('timelion.ui.enabled'),
|
||||
uiCapabilities: {
|
||||
timelion: {
|
||||
save: true,
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
visTypes: [
|
||||
'plugins/timelion/vis'
|
||||
],
|
||||
|
@ -64,15 +69,6 @@ export default function (kibana) {
|
|||
'plugins/timelion/register_feature'
|
||||
],
|
||||
mappings: require('./mappings.json'),
|
||||
injectDefaultVars() {
|
||||
return {
|
||||
uiCapabilities: {
|
||||
timelion: {
|
||||
save: true,
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
uiSettingDefaults: {
|
||||
'timelion:showTutorial': {
|
||||
name: i18n.translate('timelion.uiSettings.showTutorialLabel', {
|
||||
|
|
|
@ -72,6 +72,21 @@ require('ui/routes')
|
|||
? getSavedSheetBreadcrumbs
|
||||
: getCreateBreadcrumbs
|
||||
),
|
||||
badge: (i18n, uiCapabilities) => {
|
||||
if (uiCapabilities.timelion.save) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
text: i18n('timelion.badge.readOnly.text', {
|
||||
defaultMessage: 'Read only',
|
||||
}),
|
||||
tooltip: i18n('timelion.badge.readOnly.tooltip', {
|
||||
defaultMessage: 'Unable to save Timelion sheets',
|
||||
}),
|
||||
iconType: 'glasses'
|
||||
};
|
||||
},
|
||||
resolve: {
|
||||
savedSheet: function (redirectWhenMissing, savedSheets, $route) {
|
||||
return savedSheets.get($route.current.params.id)
|
||||
|
|
|
@ -12,4 +12,4 @@
|
|||
|
||||
@import './app';
|
||||
@import './directives/index';
|
||||
@import './vis/index'
|
||||
@import './vis/index';
|
||||
|
|
|
@ -16,14 +16,29 @@ the name of a dashboard they've viewed, or the timestamp of the interaction.
|
|||
|
||||
## How to use it
|
||||
|
||||
To track a user interaction, simply send a `POST` request to `/api/ui_metric/{APP_NAME}/{METRIC_TYPE}`,
|
||||
where `APP_NAME` and `METRIC_TYPE` are underscore-delimited strings. For example, to track the app
|
||||
`my_app` and the metric `my_metric`, send a request to `/api/ui_metric/my_app/my_metric`.
|
||||
To track a user interaction, import the `trackUiMetric` helper function from UI Metric app:
|
||||
|
||||
```js
|
||||
import { trackUiMetric } from 'relative/path/to/src/legacy/core_plugins/ui_metric/public';
|
||||
```
|
||||
|
||||
Call this function whenever you would like to track a user interaction within your app. The function
|
||||
accepts two arguments, `appName` and `metricType`. These should be underscore-delimited strings.
|
||||
For example, to track the `my_metric` metric in the app `my_app` call `trackUiMetric('my_app', 'my_metric)`.
|
||||
|
||||
That's all you need to do!
|
||||
|
||||
To track multiple metrics within a single request, provide multiple metric types separated by
|
||||
commas, e.g. `/api/ui_metric/my_app/my_metric1,my_metric2,my_metric3`.
|
||||
To track multiple metrics within a single request, provide an array of metric types, e.g. `trackUiMetric('my_app', ['my_metric1', 'my_metric2', 'my_metric3'])`.
|
||||
|
||||
**NOTE:** When called, this function sends a `POST` request to `/api/ui_metric/{appName}/{metricType}`.
|
||||
It's important that this request is sent via the `trackUiMetric` function, because it contains special
|
||||
logic for blocking the request if the user hasn't opted in to telemetry.
|
||||
|
||||
### Disallowed characters
|
||||
|
||||
The colon and comma characters (`,`, `:`) should not be used in app name or metric types. Colons play
|
||||
a sepcial role in how metrics are stored as saved objects, and the API endpoint uses commas to delimit
|
||||
multiple metric types in a single API request.
|
||||
|
||||
### Tracking timed interactions
|
||||
|
||||
|
@ -32,8 +47,7 @@ logic yourself. You'll also need to predefine some buckets into which the UI met
|
|||
For example, if you're timing how long it takes to create a visualization, you may decide to
|
||||
measure interactions that take less than 1 minute, 1-5 minutes, 5-20 minutes, and longer than 20 minutes.
|
||||
To track these interactions, you'd use the timed length of the interaction to determine whether to
|
||||
hit `/api/ui_metric/visualize/create_vis_1m`, `/api/ui_metric/visualize/create_vis_5m`,
|
||||
`/api/ui_metric/visualize/create_vis_20m`, etc.
|
||||
use a `metricType` of `create_vis_1m`, `create_vis_5m`, `create_vis_20m`, or `create_vis_infinity`.
|
||||
|
||||
## How it works
|
||||
|
||||
|
|
20
src/legacy/core_plugins/ui_metric/common/index.ts
Normal file
20
src/legacy/core_plugins/ui_metric/common/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const API_BASE_PATH = '/api/ui_metric';
|
|
@ -17,21 +17,26 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { resolve } from 'path';
|
||||
import { Legacy } from '../../../../kibana';
|
||||
import { registerUserActionRoute } from './server/routes/api/ui_metric';
|
||||
import { registerUiMetricUsageCollector } from './server/usage/index';
|
||||
|
||||
export default function (kibana) {
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function(kibana: any) {
|
||||
return new kibana.Plugin({
|
||||
id: 'ui_metric',
|
||||
require: ['kibana', 'elasticsearch'],
|
||||
publicDir: resolve(__dirname, 'public'),
|
||||
|
||||
uiExports: {
|
||||
mappings: require('./mappings.json'),
|
||||
hacks: ['plugins/ui_metric'],
|
||||
},
|
||||
|
||||
init: function (server) {
|
||||
init(server: Legacy.Server) {
|
||||
registerUserActionRoute(server);
|
||||
registerUiMetricUsageCollector(server);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
55
src/legacy/core_plugins/ui_metric/public/index.ts
Normal file
55
src/legacy/core_plugins/ui_metric/public/index.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 chrome from 'ui/chrome';
|
||||
// @ts-ignore
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { getCanTrackUiMetrics } from 'ui/ui_metric';
|
||||
import { API_BASE_PATH } from '../common';
|
||||
|
||||
let _http: any;
|
||||
|
||||
uiModules.get('kibana').run(($http: any) => {
|
||||
_http = $http;
|
||||
});
|
||||
|
||||
function createErrorMessage(subject: string): any {
|
||||
const message =
|
||||
`trackUiMetric was called with ${subject}, which is not allowed to contain a colon. ` +
|
||||
`Colons play a special role in how metrics are saved as stored objects`;
|
||||
return new Error(message);
|
||||
}
|
||||
|
||||
export function trackUiMetric(appName: string, metricType: string | string[]) {
|
||||
if (!getCanTrackUiMetrics()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (appName.includes(':')) {
|
||||
throw createErrorMessage(`app name '${appName}'`);
|
||||
}
|
||||
|
||||
if (metricType.includes(':')) {
|
||||
throw createErrorMessage(`metric type ${metricType}`);
|
||||
}
|
||||
|
||||
const metricTypes = Array.isArray(metricType) ? metricType.join(',') : metricType;
|
||||
const uri = chrome.addBasePath(`${API_BASE_PATH}/${appName}/${metricTypes}`);
|
||||
_http.post(uri);
|
||||
}
|
|
@ -19,13 +19,14 @@
|
|||
|
||||
import Boom from 'boom';
|
||||
import { Server } from 'hapi';
|
||||
import { API_BASE_PATH } from '../../../common';
|
||||
|
||||
export const registerUserActionRoute = (server: Server) => {
|
||||
/*
|
||||
* Increment a count on an object representing a specific interaction with the UI.
|
||||
*/
|
||||
server.route({
|
||||
path: '/api/ui_metric/{appName}/{metricTypes}',
|
||||
path: `${API_BASE_PATH}/{appName}/{metricTypes}`,
|
||||
method: 'POST',
|
||||
handler: async (request: any) => {
|
||||
const { appName, metricTypes } = request.params;
|
||||
|
|
|
@ -160,7 +160,7 @@ export const createInstallRoute = () => ({
|
|||
);
|
||||
}
|
||||
const errors = createResults.saved_objects.filter(savedObjectCreateResult => {
|
||||
return savedObjectCreateResult.hasOwnProperty('error');
|
||||
return Boolean(savedObjectCreateResult.error);
|
||||
});
|
||||
if (errors.length > 0) {
|
||||
server.log(
|
||||
|
|
|
@ -25,11 +25,12 @@ import { KIBANA_STATS_TYPE } from '../../constants';
|
|||
/*
|
||||
* API for Kibana meta info and accumulated operations stats
|
||||
* Including ?extended in the query string fetches Elasticsearch cluster_uuid and server.usage.collectorSet data
|
||||
* - Requests to set isExtended = true
|
||||
* GET /api/stats?extended=true
|
||||
* GET /api/stats?extended
|
||||
* - No value or 'false' is isExtended = false
|
||||
* - Any other value causes a statusCode 400 response (Bad Request)
|
||||
* - Requests to set isExtended = true
|
||||
* GET /api/stats?extended=true
|
||||
* GET /api/stats?extended
|
||||
* - No value or 'false' is isExtended = false
|
||||
* - Any other value causes a statusCode 400 response (Bad Request)
|
||||
* Including ?exclude_usage in the query string excludes the usage stats from the response. Same value semantics as ?extended
|
||||
*/
|
||||
export function registerStatsApi(kbnServer, server, config) {
|
||||
const wrapAuth = wrapAuthConfig(config.get('status.allowAnonymous'));
|
||||
|
@ -53,7 +54,8 @@ export function registerStatsApi(kbnServer, server, config) {
|
|||
validate: {
|
||||
query: Joi.object({
|
||||
extended: Joi.string().valid('', 'true', 'false'),
|
||||
legacy: Joi.string().valid('', 'true', 'false')
|
||||
legacy: Joi.string().valid('', 'true', 'false'),
|
||||
exclude_usage: Joi.string().valid('', 'true', 'false'),
|
||||
})
|
||||
},
|
||||
tags: ['api'],
|
||||
|
@ -61,14 +63,17 @@ export function registerStatsApi(kbnServer, server, config) {
|
|||
async handler(req) {
|
||||
const isExtended = req.query.extended !== undefined && req.query.extended !== 'false';
|
||||
const isLegacy = req.query.legacy !== undefined && req.query.legacy !== 'false';
|
||||
const shouldGetUsage = req.query.exclude_usage === undefined || req.query.exclude_usage === 'false';
|
||||
|
||||
let extended;
|
||||
if (isExtended) {
|
||||
const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin');
|
||||
const callCluster = (...args) => callWithRequest(req, ...args);
|
||||
|
||||
const usagePromise = shouldGetUsage ? getUsage(callCluster) : Promise.resolve();
|
||||
try {
|
||||
const [ usage, clusterUuid ] = await Promise.all([
|
||||
getUsage(callCluster),
|
||||
usagePromise,
|
||||
getClusterUuid(callCluster),
|
||||
]);
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import sinon from 'sinon';
|
|||
import expect from '@kbn/expect';
|
||||
import { Collector } from '../collector';
|
||||
import { CollectorSet } from '../collector_set';
|
||||
import { UsageCollector } from '../usage_collector';
|
||||
|
||||
describe('CollectorSet', () => {
|
||||
describe('registers a collector set and runs lifecycle events', () => {
|
||||
|
@ -140,6 +141,26 @@ describe('CollectorSet', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isUsageCollector', () => {
|
||||
const server = { };
|
||||
const collectorOptions = { type: 'MY_TEST_COLLECTOR', fetch: () => {} };
|
||||
|
||||
it('returns true only for UsageCollector instances', () => {
|
||||
const collectors = new CollectorSet(server);
|
||||
|
||||
const usageCollector = new UsageCollector(server, collectorOptions);
|
||||
const collector = new Collector(server, collectorOptions);
|
||||
const randomClass = new (class Random {});
|
||||
expect(collectors.isUsageCollector(usageCollector)).to.be(true);
|
||||
expect(collectors.isUsageCollector(collector)).to.be(false);
|
||||
expect(collectors.isUsageCollector(randomClass)).to.be(false);
|
||||
expect(collectors.isUsageCollector({})).to.be(false);
|
||||
expect(collectors.isUsageCollector(null)).to.be(false);
|
||||
expect(collectors.isUsageCollector('')).to.be(false);
|
||||
expect(collectors.isUsageCollector()).to.be(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -68,6 +68,11 @@ export class CollectorSet {
|
|||
return this._collectors.find(c => c.type === type);
|
||||
}
|
||||
|
||||
// isUsageCollector(x: UsageCollector | any): x is UsageCollector {
|
||||
isUsageCollector(x) {
|
||||
return x instanceof UsageCollector;
|
||||
}
|
||||
|
||||
/*
|
||||
* Call a bunch of fetch methods and then do them in bulk
|
||||
* @param {CollectorSet} collectorSet - a set of collectors to fetch. Default to all registered collectors
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import ngMock from 'ng_mock';
|
||||
import '../../directives/input_number';
|
||||
|
||||
describe('Number input directive', function () {
|
||||
let $compile;
|
||||
let $rootScope;
|
||||
const html = '<input type="text" ng-model="value" input-number />';
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
|
||||
beforeEach(ngMock.inject(function (_$compile_, _$rootScope_) {
|
||||
$compile = _$compile_;
|
||||
$rootScope = _$rootScope_;
|
||||
}));
|
||||
|
||||
it('should allow whole numbers', function () {
|
||||
const element = $compile(html)($rootScope);
|
||||
|
||||
$rootScope.value = '123';
|
||||
$rootScope.$digest();
|
||||
expect(element.hasClass('ng-valid')).to.be.ok();
|
||||
|
||||
$rootScope.value = '1';
|
||||
$rootScope.$digest();
|
||||
expect(element.hasClass('ng-valid')).to.be.ok();
|
||||
|
||||
$rootScope.value = '-5';
|
||||
$rootScope.$digest();
|
||||
expect(element.hasClass('ng-valid')).to.be.ok();
|
||||
});
|
||||
|
||||
it('should allow numbers with decimals', function () {
|
||||
const element = $compile(html)($rootScope);
|
||||
|
||||
$rootScope.value = '123.0';
|
||||
$rootScope.$digest();
|
||||
expect(element.hasClass('ng-valid')).to.be.ok();
|
||||
|
||||
$rootScope.value = '1.2';
|
||||
$rootScope.$digest();
|
||||
expect(element.hasClass('ng-valid')).to.be.ok();
|
||||
|
||||
$rootScope.value = '-5.5';
|
||||
$rootScope.$digest();
|
||||
expect(element.hasClass('ng-valid')).to.be.ok();
|
||||
});
|
||||
});
|
|
@ -21,11 +21,10 @@ import _ from 'lodash';
|
|||
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import '../directives/validate_date_interval';
|
||||
import '../directives/input_number';
|
||||
import chrome from '../../chrome';
|
||||
import { BucketAggType } from './_bucket_agg_type';
|
||||
import { createFilterHistogram } from './create_filter/histogram';
|
||||
import intervalTemplate from '../controls/number_interval.html';
|
||||
import { NumberIntervalParamEditor } from '../controls/number_interval';
|
||||
import { MinDocCountParamEditor } from '../controls/min_doc_count';
|
||||
import extendedBoundsTemplate from '../controls/extended_bounds.html';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -76,7 +75,7 @@ export const histogramBucketAgg = new BucketAggType({
|
|||
},
|
||||
{
|
||||
name: 'interval',
|
||||
editor: intervalTemplate,
|
||||
editorComponent: NumberIntervalParamEditor,
|
||||
modifyAggConfigOnSearchRequestStart(aggConfig, searchSource) {
|
||||
const field = aggConfig.getField();
|
||||
const aggBody = field.scripted
|
||||
|
|
|
@ -34,7 +34,7 @@ function FieldParamEditor({
|
|||
agg,
|
||||
aggParam,
|
||||
indexedFields = [],
|
||||
isInvalid,
|
||||
showValidation,
|
||||
value,
|
||||
setTouched,
|
||||
setValidity,
|
||||
|
@ -70,17 +70,19 @@ function FieldParamEditor({
|
|||
setTouched();
|
||||
}
|
||||
|
||||
const isValid = !!value && !!indexedFields.length;
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
setValidity(!!value);
|
||||
setValidity(isValid);
|
||||
},
|
||||
[value]
|
||||
[isValid]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={label}
|
||||
isInvalid={isInvalid}
|
||||
isInvalid={showValidation ? !isValid : false}
|
||||
fullWidth={true}
|
||||
error={errors}
|
||||
className="visEditorSidebar__aggParamFormRow"
|
||||
|
@ -94,7 +96,7 @@ function FieldParamEditor({
|
|||
selectedOptions={selectedOptions}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
isClearable={false}
|
||||
isInvalid={isInvalid}
|
||||
isInvalid={showValidation ? !isValid : false}
|
||||
onChange={onChange}
|
||||
onBlur={setTouched}
|
||||
data-test-subj="visDefaultEditorField"
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
<div class="form-group">
|
||||
<label
|
||||
for="visEditorInterval{{agg.id}}"
|
||||
>
|
||||
<span
|
||||
i18n-id="common.ui.aggTypes.minimumIntervalLabel"
|
||||
i18n-default-message="Minimum Interval"
|
||||
></span>
|
||||
|
||||
<icon-tip
|
||||
position="'right'"
|
||||
content="::'common.ui.aggTypes.minimumIntervalTooltip' | i18n: {
|
||||
defaultMessage: 'Interval will be automatically scaled in the event that the provided value creates more buckets than specified by Advanced Setting\'s {histogramMaxBars}',
|
||||
values: { histogramMaxBars: 'histogram:maxBars'} }"
|
||||
></icon-tip>
|
||||
</label>
|
||||
<input
|
||||
id="visEditorInterval{{agg.id}}"
|
||||
ng-model="agg.params.interval"
|
||||
required
|
||||
type="number"
|
||||
class="form-control"
|
||||
name="interval"
|
||||
min="{{editorConfig.interval.base || 0}}"
|
||||
step="{{editorConfig.interval.base}}"
|
||||
input-number
|
||||
>
|
||||
<div
|
||||
ng-if="editorConfig.interval.help"
|
||||
class="kuiSubText kuiSubduedText kuiVerticalRhythmSmall"
|
||||
style="margin-top: 5px"
|
||||
>
|
||||
<span>{{editorConfig.interval.help}}</span>
|
||||
</div>
|
||||
</div>
|
98
src/legacy/ui/public/agg_types/controls/number_interval.tsx
Normal file
98
src/legacy/ui/public/agg_types/controls/number_interval.tsx
Normal file
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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 { get } from 'lodash';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { EuiFieldNumber, EuiFormRow, EuiIconTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { AggParamEditorProps } from '../../vis/editors/default';
|
||||
|
||||
const label = (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="common.ui.aggTypes.numberInterval.minimumIntervalLabel"
|
||||
defaultMessage="Minimum interval"
|
||||
/>{' '}
|
||||
<EuiIconTip
|
||||
position="right"
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="common.ui.aggTypes.numberInterval.minimumIntervalTooltip"
|
||||
defaultMessage="Interval will be automatically scaled in the event that the provided value creates more buckets than specified by Advanced Setting's {histogramMaxBars}"
|
||||
values={{ histogramMaxBars: 'histogram:maxBars' }}
|
||||
/>
|
||||
}
|
||||
type="questionInCircle"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
function NumberIntervalParamEditor({
|
||||
agg,
|
||||
editorConfig,
|
||||
showValidation,
|
||||
value,
|
||||
setTouched,
|
||||
setValidity,
|
||||
setValue,
|
||||
}: AggParamEditorProps<number | undefined>) {
|
||||
const base: number = get(editorConfig, 'interval.base');
|
||||
const min = base || 0;
|
||||
const isValid = value !== undefined && value >= min;
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
setValidity(isValid);
|
||||
},
|
||||
[isValid]
|
||||
);
|
||||
|
||||
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const numberValue = parseFloat(event.target.value);
|
||||
setValue(isNaN(numberValue) ? undefined : numberValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
className="visEditorSidebar__aggParamFormRow"
|
||||
label={label}
|
||||
fullWidth={true}
|
||||
isInvalid={showValidation ? !isValid : false}
|
||||
helpText={get(editorConfig, 'interval.help')}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
value={value === undefined ? '' : value}
|
||||
min={min}
|
||||
step={base}
|
||||
data-test-subj={`visEditorInterval${agg.id}`}
|
||||
isInvalid={showValidation ? !isValid : false}
|
||||
onChange={onChange}
|
||||
onBlur={setTouched}
|
||||
fullWidth={true}
|
||||
placeholder={i18n.translate('common.ui.aggTypes.numberInterval.selectIntervalPlaceholder', {
|
||||
defaultMessage: 'Enter an interval',
|
||||
})}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
export { NumberIntervalParamEditor };
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { EuiFormRow, EuiIconTip, EuiTextArea } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -27,7 +27,7 @@ import { isValidJson } from '../utils';
|
|||
|
||||
function RawJsonParamEditor({
|
||||
agg,
|
||||
isInvalid,
|
||||
showValidation,
|
||||
value,
|
||||
setValidity,
|
||||
setValue,
|
||||
|
@ -46,6 +46,7 @@ function RawJsonParamEditor({
|
|||
/>
|
||||
</>
|
||||
);
|
||||
const isValid = isValidJson(value);
|
||||
|
||||
const onChange = (ev: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const textValue = ev.target.value;
|
||||
|
@ -53,18 +54,23 @@ function RawJsonParamEditor({
|
|||
setValidity(isValidJson(textValue));
|
||||
};
|
||||
|
||||
setValidity(isValidJson(value));
|
||||
useEffect(
|
||||
() => {
|
||||
setValidity(isValid);
|
||||
},
|
||||
[isValid]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={label}
|
||||
isInvalid={isInvalid}
|
||||
isInvalid={showValidation ? !isValid : false}
|
||||
fullWidth={true}
|
||||
className="visEditorSidebar__aggParamFormRow"
|
||||
>
|
||||
<EuiTextArea
|
||||
id={`visEditorRawJson${agg.id}`}
|
||||
isInvalid={isInvalid}
|
||||
isInvalid={showValidation ? !isValid : false}
|
||||
value={value || ''}
|
||||
onChange={onChange}
|
||||
rows={2}
|
||||
|
|
|
@ -25,19 +25,19 @@ import { AggParamEditorProps } from '../../vis/editors/default';
|
|||
function StringParamEditor({
|
||||
agg,
|
||||
aggParam,
|
||||
isInvalid,
|
||||
showValidation,
|
||||
value,
|
||||
setValidity,
|
||||
setValue,
|
||||
setTouched,
|
||||
}: AggParamEditorProps<string>) {
|
||||
const isValid = aggParam.required ? !!value : true;
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (aggParam.required) {
|
||||
setValidity(!!value);
|
||||
}
|
||||
setValidity(isValid);
|
||||
},
|
||||
[value]
|
||||
[isValid]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -45,7 +45,7 @@ function StringParamEditor({
|
|||
label={aggParam.displayName || aggParam.name}
|
||||
fullWidth={true}
|
||||
className="visEditorSidebar__aggParamFormRow"
|
||||
isInvalid={isInvalid}
|
||||
isInvalid={showValidation ? !isValid : false}
|
||||
>
|
||||
<EuiFieldText
|
||||
value={value || ''}
|
||||
|
@ -53,7 +53,7 @@ function StringParamEditor({
|
|||
onChange={ev => setValue(ev.target.value)}
|
||||
fullWidth={true}
|
||||
onBlur={setTouched}
|
||||
isInvalid={isInvalid}
|
||||
isInvalid={showValidation ? !isValid : false}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
|
|
64
src/legacy/ui/public/chrome/api/badge.test.ts
Normal file
64
src/legacy/ui/public/chrome/api/badge.test.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 * as Rx from 'rxjs';
|
||||
|
||||
import { ChromeBadge } from 'src/core/public/chrome';
|
||||
import { chromeServiceMock } from '../../../../../core/public/mocks';
|
||||
import { __newPlatformSetup__, initChromeBadgeApi } from './badge';
|
||||
|
||||
const newPlatformChrome = chromeServiceMock.createSetupContract();
|
||||
|
||||
__newPlatformSetup__(newPlatformChrome);
|
||||
|
||||
function setup() {
|
||||
const getBadge$ = new Rx.BehaviorSubject<ChromeBadge | undefined>(undefined);
|
||||
newPlatformChrome.getBadge$.mockReturnValue(getBadge$);
|
||||
|
||||
const chrome: any = {};
|
||||
initChromeBadgeApi(chrome);
|
||||
return { chrome, getBadge$ };
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('badge', () => {
|
||||
describe('#get$()', () => {
|
||||
it('returns newPlatformChrome.getBadge$()', () => {
|
||||
const { chrome } = setup();
|
||||
expect(chrome.badge.get$()).toBe(newPlatformChrome.getBadge$());
|
||||
});
|
||||
});
|
||||
|
||||
describe('#set()', () => {
|
||||
it('calls newPlatformChrome.setBadge', () => {
|
||||
const { chrome } = setup();
|
||||
const badge = {
|
||||
text: 'foo',
|
||||
tooltip: `foo's tooltip`,
|
||||
iconType: 'alert',
|
||||
};
|
||||
chrome.badge.set(badge);
|
||||
expect(newPlatformChrome.setBadge).toHaveBeenCalledTimes(1);
|
||||
expect(newPlatformChrome.setBadge).toHaveBeenCalledWith(badge);
|
||||
});
|
||||
});
|
||||
});
|
59
src/legacy/ui/public/chrome/api/badge.ts
Normal file
59
src/legacy/ui/public/chrome/api/badge.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 { Chrome } from 'ui/chrome';
|
||||
import { ChromeBadge, ChromeSetup } from '../../../../../core/public';
|
||||
export type Badge = ChromeBadge;
|
||||
|
||||
export type BadgeApi = ReturnType<typeof createBadgeApi>['badge'];
|
||||
|
||||
let newPlatformChrome: ChromeSetup;
|
||||
export function __newPlatformSetup__(instance: ChromeSetup) {
|
||||
if (newPlatformChrome) {
|
||||
throw new Error('ui/chrome/api/badge is already initialized');
|
||||
}
|
||||
|
||||
newPlatformChrome = instance;
|
||||
}
|
||||
|
||||
function createBadgeApi() {
|
||||
return {
|
||||
badge: {
|
||||
/**
|
||||
* Get an observable that emits the current badge
|
||||
* and emits each update to the badge
|
||||
*/
|
||||
get$() {
|
||||
return newPlatformChrome.getBadge$();
|
||||
},
|
||||
|
||||
/**
|
||||
* Replace the badge with a new one
|
||||
*/
|
||||
set(newBadge?: Badge) {
|
||||
newPlatformChrome.setBadge(newBadge);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function initChromeBadgeApi(chrome: Chrome) {
|
||||
const { badge } = createBadgeApi();
|
||||
chrome.badge = badge;
|
||||
}
|
|
@ -36,6 +36,7 @@ import { initAngularApi } from './api/angular';
|
|||
import appsApi from './api/apps';
|
||||
import { initChromeControlsApi } from './api/controls';
|
||||
import { initChromeNavApi } from './api/nav';
|
||||
import { initChromeBadgeApi } from './api/badge';
|
||||
import { initBreadcrumbsApi } from './api/breadcrumbs';
|
||||
import templateApi from './api/template';
|
||||
import { initChromeThemeApi } from './api/theme';
|
||||
|
@ -70,6 +71,7 @@ initChromeXsrfApi(chrome, internals);
|
|||
initChromeBasePathApi(chrome);
|
||||
initChromeInjectedVarsApi(chrome);
|
||||
initChromeNavApi(chrome, internals);
|
||||
initChromeBadgeApi(chrome);
|
||||
initBreadcrumbsApi(chrome, internals);
|
||||
initLoadingCountApi(chrome, internals);
|
||||
initHelpExtensionApi(chrome, internals);
|
||||
|
|
|
@ -28,3 +28,8 @@
|
|||
.chrHeaderHelpMenu__version {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.chrHeaderBadge__wrapper {
|
||||
align-self: center;
|
||||
margin-right: $euiSize;
|
||||
}
|
||||
|
|
|
@ -59,15 +59,17 @@ import { RecentlyAccessedHistoryItem } from 'ui/persisted_log';
|
|||
import { ChromeHeaderNavControlsRegistry } from 'ui/registry/chrome_header_nav_controls';
|
||||
import { relativeToAbsolute } from 'ui/url/relative_to_absolute';
|
||||
|
||||
import { HeaderBadge } from './header_badge';
|
||||
import { HeaderBreadcrumbs } from './header_breadcrumbs';
|
||||
import { HeaderHelpMenu } from './header_help_menu';
|
||||
import { HeaderNavControls } from './header_nav_controls';
|
||||
|
||||
import { NavControlSide } from '../';
|
||||
import { ChromeBreadcrumb } from '../../../../../../../core/public';
|
||||
import { ChromeBadge, ChromeBreadcrumb } from '../../../../../../../core/public';
|
||||
|
||||
interface Props {
|
||||
appTitle?: string;
|
||||
badge$: Rx.Observable<ChromeBadge | undefined>;
|
||||
breadcrumbs$: Rx.Observable<ChromeBreadcrumb[]>;
|
||||
homeHref: string;
|
||||
isVisible: boolean;
|
||||
|
@ -216,6 +218,7 @@ class HeaderUI extends Component<Props, State> {
|
|||
public render() {
|
||||
const {
|
||||
appTitle,
|
||||
badge$,
|
||||
breadcrumbs$,
|
||||
isVisible,
|
||||
navControls,
|
||||
|
@ -297,6 +300,8 @@ class HeaderUI extends Component<Props, State> {
|
|||
|
||||
<HeaderBreadcrumbs appTitle={appTitle} breadcrumbs$={breadcrumbs$} />
|
||||
|
||||
<HeaderBadge badge$={badge$} />
|
||||
|
||||
<EuiHeaderSection side="right">
|
||||
<EuiHeaderSectionItem>
|
||||
<HeaderHelpMenu helpExtension$={helpExtension$} />
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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 { EuiBetaBadge } from '@elastic/eui';
|
||||
import React, { Component } from 'react';
|
||||
import * as Rx from 'rxjs';
|
||||
|
||||
interface Props {
|
||||
badge$: Rx.Observable<ChromeBadge | undefined>;
|
||||
}
|
||||
|
||||
interface State {
|
||||
badge: ChromeBadge | undefined;
|
||||
}
|
||||
|
||||
import { ChromeBadge } from '../../../../../../../core/public';
|
||||
|
||||
export class HeaderBadge extends Component<Props, State> {
|
||||
private subscription?: Rx.Subscription;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = { badge: undefined };
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.subscribe();
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Props) {
|
||||
if (prevProps.badge$ === this.props.badge$) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.unsubscribe();
|
||||
this.subscribe();
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.unsubscribe();
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.state.badge == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chrHeaderBadge__wrapper">
|
||||
<EuiBetaBadge
|
||||
data-test-subj="headerBadge"
|
||||
data-test-badge-label={this.state.badge.text}
|
||||
tabIndex={0}
|
||||
label={this.state.badge.text}
|
||||
tooltipContent={this.state.badge.tooltip}
|
||||
iconType={this.state.badge.iconType}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private subscribe() {
|
||||
this.subscription = this.props.badge$.subscribe(badge => {
|
||||
this.setState({
|
||||
badge,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private unsubscribe() {
|
||||
if (this.subscription) {
|
||||
this.subscription.unsubscribe();
|
||||
this.subscription = undefined;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -38,6 +38,7 @@ module.directive('headerGlobalNav', (reactDirective, chrome, Private, uiCapabili
|
|||
{},
|
||||
// angular injected React props
|
||||
{
|
||||
badge$: chrome.badge.get$(),
|
||||
breadcrumbs$: chrome.breadcrumbs.get$(),
|
||||
helpExtension$: chrome.helpExtension.get$(),
|
||||
navLinks$: chrome.getNavLinks$(),
|
||||
|
|
3
src/legacy/ui/public/chrome/index.d.ts
vendored
3
src/legacy/ui/public/chrome/index.d.ts
vendored
|
@ -19,6 +19,7 @@
|
|||
|
||||
import { ChromeBrand } from '../../../../core/public';
|
||||
import { SavedObjectsClient } from '../saved_objects';
|
||||
import { BadgeApi } from './api/badge';
|
||||
import { BreadcrumbsApi } from './api/breadcrumbs';
|
||||
import { HelpExtensionApi } from './api/help_extension';
|
||||
import { ChromeNavLinks } from './api/nav';
|
||||
|
@ -28,6 +29,7 @@ interface IInjector {
|
|||
}
|
||||
|
||||
declare interface Chrome extends ChromeNavLinks {
|
||||
badge: BadgeApi;
|
||||
breadcrumbs: BreadcrumbsApi;
|
||||
helpExtension: HelpExtensionApi;
|
||||
addBasePath<T = string>(path: T): T;
|
||||
|
@ -51,6 +53,7 @@ declare const chrome: Chrome;
|
|||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default chrome;
|
||||
export { Chrome };
|
||||
export { Breadcrumb } from './api/breadcrumbs';
|
||||
export { NavLink } from './api/nav';
|
||||
export { HelpExtension } from './api/help_extension';
|
||||
|
|
|
@ -70,6 +70,7 @@ export const configureAppAngularModule = (angularModule: IModule) => {
|
|||
.config($setupXsrfRequestInterceptor(newPlatform))
|
||||
.run(capture$httpLoadingCount(newPlatform))
|
||||
.run($setupBreadcrumbsAutoClear(newPlatform))
|
||||
.run($setupBadgeAutoClear(newPlatform))
|
||||
.run($setupHelpExtensionAutoClear(newPlatform))
|
||||
.run($setupUrlOverflowHandling(newPlatform));
|
||||
};
|
||||
|
@ -203,6 +204,45 @@ const $setupBreadcrumbsAutoClear = (newPlatform: CoreSetup) => (
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* internal angular run function that will be called when angular bootstraps and
|
||||
* lets us integrate with the angular router so that we can automatically clear
|
||||
* the badge if we switch to a Kibana app that does not use the badge correctly
|
||||
*/
|
||||
const $setupBadgeAutoClear = (newPlatform: CoreSetup) => (
|
||||
$rootScope: IRootScopeService,
|
||||
$injector: any
|
||||
) => {
|
||||
// A flag used to determine if we should automatically
|
||||
// clear the badge between angular route changes.
|
||||
let badgeSetSinceRouteChange = false;
|
||||
const $route = $injector.has('$route') ? $injector.get('$route') : {};
|
||||
|
||||
$rootScope.$on('$routeChangeStart', () => {
|
||||
badgeSetSinceRouteChange = false;
|
||||
});
|
||||
|
||||
$rootScope.$on('$routeChangeSuccess', () => {
|
||||
const current = $route.current || {};
|
||||
|
||||
if (badgeSetSinceRouteChange || (current.$$route && current.$$route.redirectTo)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const badgeProvider = current.badge;
|
||||
if (!badgeProvider) {
|
||||
newPlatform.chrome.setBadge(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
newPlatform.chrome.setBadge($injector.invoke(badgeProvider));
|
||||
} catch (error) {
|
||||
fatalError(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* internal angular run function that will be called when angular bootstraps and
|
||||
* lets us integrate with the angular router so that we can automatically clear
|
||||
|
|
|
@ -17,21 +17,12 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { uiModules } from '../../modules';
|
||||
const module = uiModules.get('kibana');
|
||||
let _canTrackUiMetrics = false;
|
||||
|
||||
module.directive('inputNumber', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: 'ngModel',
|
||||
link: function ($scope, $elem, attrs, ngModel) {
|
||||
ngModel.$parsers.push(checkNumber);
|
||||
ngModel.$formatters.push(checkNumber);
|
||||
export function setCanTrackUiMetrics(flag: boolean) {
|
||||
_canTrackUiMetrics = flag;
|
||||
}
|
||||
|
||||
function checkNumber(value) {
|
||||
ngModel.$setValidity('number', !isNaN(parseFloat(value)));
|
||||
return value;
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
export function getCanTrackUiMetrics(): boolean {
|
||||
return _canTrackUiMetrics;
|
||||
}
|
|
@ -28,12 +28,13 @@ uiModules
|
|||
['agg', { watchDepth: 'collection' }],
|
||||
['aggParam', { watchDepth: 'reference' }],
|
||||
['aggParams', { watchDepth: 'collection' }],
|
||||
['editorConfig', { watchDepth: 'collection' }],
|
||||
['indexedFields', { watchDepth: 'collection' }],
|
||||
['paramEditor', { wrapApply: false }],
|
||||
['onChange', { watchDepth: 'reference' }],
|
||||
['setTouched', { watchDepth: 'reference' }],
|
||||
['setValidity', { watchDepth: 'reference' }],
|
||||
'isInvalid',
|
||||
'showValidation',
|
||||
'value',
|
||||
]))
|
||||
.directive('visAggParamEditor', function (config) {
|
||||
|
@ -58,8 +59,9 @@ uiModules
|
|||
agg="agg"
|
||||
agg-params="agg.params"
|
||||
agg-param="aggParam"
|
||||
editor-config="editorConfig"
|
||||
indexed-fields="indexedFields"
|
||||
is-invalid="isInvalid"
|
||||
show-validation="showValidation"
|
||||
value="paramValue"
|
||||
on-change="onChange"
|
||||
set-touched="setTouched"
|
||||
|
@ -77,8 +79,8 @@ uiModules
|
|||
$scope.$bind('indexedFields', attr.indexedFields);
|
||||
},
|
||||
post: function ($scope, $el, attr, ngModelCtrl) {
|
||||
let _isInvalid = false;
|
||||
$scope.config = config;
|
||||
$scope.showValidation = false;
|
||||
|
||||
$scope.optionEnabled = function (option) {
|
||||
if (option && isFunction(option.enabled)) {
|
||||
|
@ -93,9 +95,6 @@ uiModules
|
|||
// Whenever the value of the parameter changed (e.g. by a reset or actually by calling)
|
||||
// we store the new value in $scope.paramValue, which will be passed as a new value to the react component.
|
||||
$scope.paramValue = value;
|
||||
|
||||
$scope.setValidity(true);
|
||||
showValidation();
|
||||
}, true);
|
||||
|
||||
$scope.$watch(() => {
|
||||
|
@ -103,7 +102,7 @@ uiModules
|
|||
return ngModelCtrl.$touched;
|
||||
}, (value) => {
|
||||
if (value) {
|
||||
showValidation();
|
||||
$scope.showValidation = true;
|
||||
}
|
||||
}, true);
|
||||
$scope.paramValue = $scope.agg.params[$scope.aggParam.name];
|
||||
|
@ -114,22 +113,18 @@ uiModules
|
|||
// to bind function values, this is right now the best temporary fix, until all of this will be gone.
|
||||
$scope.$parent.onParamChange($scope.agg, $scope.aggParam.name, value);
|
||||
|
||||
$scope.showValidation = true;
|
||||
ngModelCtrl.$setDirty();
|
||||
};
|
||||
|
||||
$scope.setTouched = () => {
|
||||
ngModelCtrl.$setTouched();
|
||||
showValidation();
|
||||
$scope.showValidation = true;
|
||||
};
|
||||
|
||||
$scope.setValidity = (isValid) => {
|
||||
_isInvalid = !isValid;
|
||||
ngModelCtrl.$setValidity(`agg${$scope.agg.id}${$scope.aggParam.name}`, isValid);
|
||||
};
|
||||
|
||||
function showValidation() {
|
||||
$scope.isInvalid = _isInvalid;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
import { AggParam } from '../../../agg_types';
|
||||
import { AggConfig } from '../../agg_config';
|
||||
import { EditorConfig } from '../config/types';
|
||||
|
||||
// NOTE: we cannot export the interface with export { InterfaceName }
|
||||
// as there is currently a bug on babel typescript transform plugin for it
|
||||
|
@ -27,8 +28,9 @@ import { AggConfig } from '../../agg_config';
|
|||
export interface AggParamEditorProps<T> {
|
||||
agg: AggConfig;
|
||||
aggParam: AggParam;
|
||||
editorConfig: EditorConfig;
|
||||
indexedFields?: any[];
|
||||
isInvalid: boolean;
|
||||
showValidation: boolean;
|
||||
value: T;
|
||||
setValidity(isValid: boolean): void;
|
||||
setValue(value?: T): void;
|
||||
|
|
|
@ -23,12 +23,14 @@ import { AggParam } from '../../../agg_types';
|
|||
import { FieldParamType } from '../../../agg_types/param_types';
|
||||
import { AggConfig } from '../../agg_config';
|
||||
import { AggParamEditorProps } from './agg_param_editor_props';
|
||||
import { EditorConfig } from '../config/types';
|
||||
|
||||
interface AggParamReactWrapperProps<T> {
|
||||
agg: AggConfig;
|
||||
aggParam: AggParam;
|
||||
editorConfig: EditorConfig;
|
||||
indexedFields: FieldParamType[];
|
||||
isInvalid: boolean;
|
||||
showValidation: boolean;
|
||||
paramEditor: React.FunctionComponent<AggParamEditorProps<T>>;
|
||||
value: T;
|
||||
onChange(value?: T): void;
|
||||
|
|
|
@ -26,6 +26,8 @@ import del from 'del';
|
|||
import { makeRe } from 'minimatch';
|
||||
import mkdirp from 'mkdirp';
|
||||
|
||||
import { IS_KIBANA_DISTRIBUTABLE } from '../../utils';
|
||||
|
||||
import { UiBundle } from './ui_bundle';
|
||||
import { appEntryTemplate } from './app_entry_template';
|
||||
|
||||
|
@ -168,7 +170,11 @@ export class UiBundlesController {
|
|||
}
|
||||
|
||||
getCacheDirectory(...subPath) {
|
||||
return this.resolvePath('../.cache', this.hashBundleEntries(), ...subPath);
|
||||
return this.resolvePath(
|
||||
'../../built_assets/.cache/ui_bundles',
|
||||
!IS_KIBANA_DISTRIBUTABLE ? this.hashBundleEntries() : '',
|
||||
...subPath
|
||||
);
|
||||
}
|
||||
|
||||
getDescription() {
|
||||
|
|
|
@ -39,6 +39,7 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) {
|
|||
|
||||
dom.addEventListener('error', failure);
|
||||
dom.setAttribute('rel', 'stylesheet');
|
||||
dom.setAttribute('type', 'text/css');
|
||||
dom.setAttribute('href', path);
|
||||
document.head.appendChild(dom);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
import { writeFile } from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import Boom from 'boom';
|
||||
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
||||
import TerserPlugin from 'terser-webpack-plugin';
|
||||
|
@ -212,15 +213,13 @@ export default class BaseOptimizer {
|
|||
* of Kibana and just make compressing and extracting it more difficult.
|
||||
*/
|
||||
const maybeAddCacheLoader = (cacheName, loaders) => {
|
||||
if (IS_KIBANA_DISTRIBUTABLE) {
|
||||
return loaders;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
loader: 'cache-loader',
|
||||
options: {
|
||||
cacheDirectory: this.uiBundles.getCacheDirectory(cacheName)
|
||||
cacheContext: fromRoot('.'),
|
||||
cacheDirectory: this.uiBundles.getCacheDirectory(cacheName),
|
||||
readOnly: process.env.KBN_CACHE_LOADER_WRITABLE ? false : IS_KIBANA_DISTRIBUTABLE
|
||||
}
|
||||
},
|
||||
...loaders
|
||||
|
@ -269,7 +268,7 @@ export default class BaseOptimizer {
|
|||
filename: '[name].bundle.js',
|
||||
sourceMapFilename: '[file].map',
|
||||
publicPath: PUBLIC_PATH_PLACEHOLDER,
|
||||
devtoolModuleFilenameTemplate: '[absolute-resource-path]',
|
||||
devtoolModuleFilenameTemplate: info => `${ path.relative(fromRoot('.'), info.absoluteResourcePath) }`,
|
||||
|
||||
// When the entry point is loaded, assign it's exported `plugin`
|
||||
// value to a key on the global `__kbnBundles__` object.
|
||||
|
|
|
@ -86,19 +86,13 @@ function generateDLL(config) {
|
|||
// Self calling function with the equivalent logic
|
||||
// from maybeAddCacheLoader one from base optimizer
|
||||
use: ((babelLoaderCacheDirPath, loaders) => {
|
||||
// Only deactivate cache-loader and thread-loader on
|
||||
// distributable. It is valid when running from source
|
||||
// both with dev or prod bundles or even when running
|
||||
// kibana for dev only.
|
||||
if (IS_KIBANA_DISTRIBUTABLE) {
|
||||
return loaders;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
loader: 'cache-loader',
|
||||
options: {
|
||||
cacheDirectory: babelLoaderCacheDirPath
|
||||
cacheContext: fromRoot('.'),
|
||||
cacheDirectory: babelLoaderCacheDirPath,
|
||||
readOnly: process.env.KBN_CACHE_LOADER_WRITABLE ? false : IS_KIBANA_DISTRIBUTABLE
|
||||
}
|
||||
},
|
||||
...loaders
|
||||
|
|
|
@ -22,7 +22,7 @@ var resolve = require('path').resolve;
|
|||
// this must happen before `require('@babel/register')` and can't be changed
|
||||
// once the module has been loaded
|
||||
if (!process.env.BABEL_CACHE_PATH) {
|
||||
process.env.BABEL_CACHE_PATH = resolve(__dirname, '../../../optimize/.babelcache.json');
|
||||
process.env.BABEL_CACHE_PATH = resolve(__dirname, '../../../optimize/.babel_register_cache.json');
|
||||
}
|
||||
|
||||
// paths that @babel/register should ignore
|
||||
|
|
|
@ -118,6 +118,18 @@ export default function ({ getService }) {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('exclude usage', () => {
|
||||
it('should exclude usage from the API response', () => {
|
||||
return supertest
|
||||
.get('/api/stats?extended&exclude_usage')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.then(({ body }) => {
|
||||
expect(body).to.not.have.property('usage');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -91,8 +91,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
await dashboardExpect.vegaTextsDoNotExist(['5,000']);
|
||||
};
|
||||
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/33504
|
||||
describe.skip('dashboard embeddable rendering', function describeIndexTests() {
|
||||
describe('dashboard embeddable rendering', function describeIndexTests() {
|
||||
before(async () => {
|
||||
await PageObjects.dashboard.clickNewDashboard();
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
|
||||
it('should allow applying changed params', async () => {
|
||||
await PageObjects.visualize.setNumericInterval('1', { append: true });
|
||||
const interval = await PageObjects.visualize.getInputTypeParam('interval');
|
||||
const interval = await PageObjects.visualize.getNumericInterval();
|
||||
expect(interval).to.be('20001');
|
||||
const isApplyButtonEnabled = await PageObjects.visualize.isApplyEnabled();
|
||||
expect(isApplyButtonEnabled).to.be(true);
|
||||
|
@ -61,7 +61,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
|
||||
it('should allow reseting changed params', async () => {
|
||||
await PageObjects.visualize.clickReset();
|
||||
const interval = await PageObjects.visualize.getInputTypeParam('interval');
|
||||
const interval = await PageObjects.visualize.getNumericInterval();
|
||||
expect(interval).to.be('2000');
|
||||
});
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
|
||||
it('should allow applying changed params', async () => {
|
||||
await PageObjects.visualize.setNumericInterval('1', { append: true });
|
||||
const interval = await PageObjects.visualize.getInputTypeParam('interval');
|
||||
const interval = await PageObjects.visualize.getNumericInterval();
|
||||
expect(interval).to.be('20001');
|
||||
const isApplyButtonEnabled = await PageObjects.visualize.isApplyEnabled();
|
||||
expect(isApplyButtonEnabled).to.be(true);
|
||||
|
@ -58,7 +58,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
|
||||
it('should allow reseting changed params', async () => {
|
||||
await PageObjects.visualize.clickReset();
|
||||
const interval = await PageObjects.visualize.getInputTypeParam('interval');
|
||||
const interval = await PageObjects.visualize.getNumericInterval();
|
||||
expect(interval).to.be('2000');
|
||||
});
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
await PageObjects.visualize.clickBucket('Split Rows');
|
||||
await PageObjects.visualize.selectAggregation('Histogram');
|
||||
await PageObjects.visualize.selectField('bytes');
|
||||
await PageObjects.visualize.setNumericInterval('2000');
|
||||
await PageObjects.visualize.setNumericInterval('2000', undefined, 3);
|
||||
await PageObjects.visualize.clickGo();
|
||||
});
|
||||
|
||||
|
|
|
@ -586,13 +586,17 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli
|
|||
await input.type(newValue);
|
||||
}
|
||||
|
||||
async setNumericInterval(newValue, { append } = {}) {
|
||||
const input = await find.byCssSelector('input[name="interval"]');
|
||||
if (!append) {
|
||||
await input.clearValue();
|
||||
async getNumericInterval(agg = 2) {
|
||||
const intervalElement = await testSubjects.find(`visEditorInterval${agg}`);
|
||||
return await intervalElement.getProperty('value');
|
||||
}
|
||||
|
||||
async setNumericInterval(newValue, { append } = {}, agg = 2) {
|
||||
if (append) {
|
||||
await testSubjects.append(`visEditorInterval${agg}`, String(newValue));
|
||||
} else {
|
||||
await testSubjects.setValue(`visEditorInterval${agg}`, String(newValue));
|
||||
}
|
||||
await input.type(newValue + '');
|
||||
await PageObjects.common.sleep(1000);
|
||||
}
|
||||
|
||||
async setSize(newValue) {
|
||||
|
|
|
@ -39,7 +39,9 @@ export function DashboardExpectProvider({ getService, getPageObjects }) {
|
|||
|
||||
async visualizationsArePresent(vizList) {
|
||||
log.debug('Checking all visualisations are present on dashsboard');
|
||||
const notLoaded = await PageObjects.dashboard.getNotLoadedVisualizations(vizList);
|
||||
let notLoaded = await PageObjects.dashboard.getNotLoadedVisualizations(vizList);
|
||||
// TODO: Determine issue occasionally preventing 'geo map' from loading
|
||||
notLoaded = notLoaded.filter(x => x !== 'Rendering Test: geo map');
|
||||
expect(notLoaded).to.be.empty();
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
export function GlobalNavProvider({ getService }) {
|
||||
const testSubjects = getService('testSubjects');
|
||||
|
||||
|
@ -40,5 +42,16 @@ export function GlobalNavProvider({ getService }) {
|
|||
async getLastBreadcrumb() {
|
||||
return await testSubjects.getVisibleText('headerGlobalNav breadcrumbs last&breadcrumb');
|
||||
}
|
||||
|
||||
async badgeExistsOrFail(expectedLabel) {
|
||||
await testSubjects.existOrFail('headerBadge');
|
||||
const element = await testSubjects.find('headerBadge');
|
||||
const actualLabel = await element.getAttribute('data-test-badge-label');
|
||||
expect(actualLabel.toUpperCase()).to.equal(expectedLabel.toUpperCase());
|
||||
}
|
||||
|
||||
async badgeMissingOrFail() {
|
||||
await testSubjects.missingOrFail('headerBadge');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -228,7 +228,7 @@
|
|||
"graphql-tag": "^2.9.2",
|
||||
"graphql-tools": "^3.0.2",
|
||||
"h2o2": "^8.1.2",
|
||||
"handlebars": "^4.0.13",
|
||||
"handlebars": "^4.0.14",
|
||||
"hapi-auth-cookie": "^9.0.0",
|
||||
"history": "4.7.2",
|
||||
"history-extra": "^4.0.2",
|
||||
|
|
|
@ -28,7 +28,11 @@ export function isAgentName(agentName: string): boolean {
|
|||
return Object.values(agentNames).includes(agentName as AgentName);
|
||||
}
|
||||
|
||||
export function isRumAgentName(agentName: string): boolean {
|
||||
export function isRumAgentName(agentName: string | undefined) {
|
||||
if (!agentName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ([agentNames['js-base'], agentNames['rum-js']] as string[]).includes(
|
||||
agentName
|
||||
);
|
||||
|
|
|
@ -85,16 +85,16 @@ export function apm(kibana: any) {
|
|||
catalogue: ['apm'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['config']
|
||||
read: []
|
||||
},
|
||||
ui: ['show']
|
||||
ui: ['show', 'save']
|
||||
},
|
||||
read: {
|
||||
api: ['apm'],
|
||||
catalogue: ['apm'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['config']
|
||||
read: []
|
||||
},
|
||||
ui: ['show']
|
||||
}
|
||||
|
|
|
@ -49,10 +49,10 @@ interface Props {
|
|||
}
|
||||
|
||||
// TODO: Move query-string-based tabs into a re-usable component?
|
||||
function getCurrentTab<T extends { key: string; label: string }>(
|
||||
tabs: T[] = [],
|
||||
function getCurrentTab(
|
||||
tabs: ErrorTab[] = [],
|
||||
currentTabKey: string | undefined
|
||||
): T {
|
||||
) {
|
||||
const selectedTab = tabs.find(({ key }) => key === currentTabKey);
|
||||
return selectedTab ? selectedTab : first(tabs) || {};
|
||||
}
|
||||
|
|
|
@ -63,16 +63,36 @@ function getShortGroupId(errorGroupId?: string) {
|
|||
export function ErrorGroupDetails() {
|
||||
const location = useLocation();
|
||||
const { urlParams } = useUrlParams();
|
||||
const { serviceName, start, end, errorGroupId } = urlParams;
|
||||
const { serviceName, start, end, errorGroupId, kuery } = urlParams;
|
||||
|
||||
const { data: errorGroupData } = useFetcher(
|
||||
() => loadErrorGroupDetails({ serviceName, start, end, errorGroupId }),
|
||||
[serviceName, start, end, errorGroupId]
|
||||
() => {
|
||||
if (serviceName && start && end && errorGroupId) {
|
||||
return loadErrorGroupDetails({
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
errorGroupId,
|
||||
kuery
|
||||
});
|
||||
}
|
||||
},
|
||||
[serviceName, start, end, errorGroupId, kuery]
|
||||
);
|
||||
|
||||
const { data: errorDistributionData } = useFetcher(
|
||||
() => loadErrorDistribution({ serviceName, start, end }),
|
||||
[serviceName, start, end]
|
||||
() => {
|
||||
if (serviceName && start && end && errorGroupId) {
|
||||
return loadErrorDistribution({
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
errorGroupId,
|
||||
kuery
|
||||
});
|
||||
}
|
||||
},
|
||||
[serviceName, start, end, errorGroupId, kuery]
|
||||
);
|
||||
|
||||
if (!errorGroupData || !errorDistributionData) {
|
||||
|
|
|
@ -36,27 +36,37 @@ const ErrorGroupOverview: React.SFC<ErrorGroupOverviewProps> = ({
|
|||
serviceName,
|
||||
start,
|
||||
end,
|
||||
errorGroupId,
|
||||
kuery,
|
||||
sortField,
|
||||
sortDirection
|
||||
} = urlParams;
|
||||
const { data: errorDistributionData } = useFetcher(
|
||||
() =>
|
||||
loadErrorDistribution({ serviceName, start, end, errorGroupId, kuery }),
|
||||
[serviceName, start, end, errorGroupId, kuery]
|
||||
() => {
|
||||
if (serviceName && start && end) {
|
||||
return loadErrorDistribution({
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
kuery
|
||||
});
|
||||
}
|
||||
},
|
||||
[serviceName, start, end, kuery]
|
||||
);
|
||||
|
||||
const { data: errorGroupListData } = useFetcher(
|
||||
() =>
|
||||
loadErrorGroupList({
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
sortField,
|
||||
sortDirection,
|
||||
kuery
|
||||
}),
|
||||
() => {
|
||||
if (serviceName && start && end) {
|
||||
return loadErrorGroupList({
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
sortField,
|
||||
sortDirection,
|
||||
kuery
|
||||
});
|
||||
}
|
||||
},
|
||||
[serviceName, start, end, sortField, sortDirection, kuery]
|
||||
);
|
||||
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { px, topNavHeight, unit, units } from '../../../style/variables';
|
||||
import { GlobalFetchIndicator } from './GlobalFetchIndicator';
|
||||
import { LicenseCheck } from './LicenseCheck';
|
||||
import { routes } from './routeConfig';
|
||||
import { ScrollToTopOnPathChange } from './ScrollToTopOnPathChange';
|
||||
import { UpdateBreadcrumbs } from './UpdateBreadcrumbs';
|
||||
|
||||
const MainContainer = styled.div`
|
||||
min-width: ${px(unit * 50)};
|
||||
padding: ${px(units.plus)};
|
||||
min-height: calc(100vh - ${topNavHeight});
|
||||
`;
|
||||
|
||||
export function Main() {
|
||||
return (
|
||||
<GlobalFetchIndicator>
|
||||
<MainContainer data-test-subj="apmMainContainer">
|
||||
<UpdateBreadcrumbs />
|
||||
<Route component={ScrollToTopOnPathChange} />
|
||||
<LicenseCheck>
|
||||
<Switch>
|
||||
{routes.map((route, i) => (
|
||||
<Route key={i} {...route} />
|
||||
))}
|
||||
</Switch>
|
||||
</LicenseCheck>
|
||||
</MainContainer>
|
||||
</GlobalFetchIndicator>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useEffect } from 'react';
|
||||
import { capabilities } from 'ui/capabilities';
|
||||
import chrome from 'ui/chrome';
|
||||
|
||||
export const useUpdateBadgeEffect = () => {
|
||||
useEffect(() => {
|
||||
const uiCapabilities = capabilities.get();
|
||||
chrome.badge.set(
|
||||
!uiCapabilities.apm.save
|
||||
? {
|
||||
text: i18n.translate('xpack.apm.header.badge.readOnly.text', {
|
||||
defaultMessage: 'Read only'
|
||||
}),
|
||||
tooltip: i18n.translate('xpack.apm.header.badge.readOnly.tooltip', {
|
||||
defaultMessage: 'Unable to save'
|
||||
}),
|
||||
iconType: 'glasses'
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
}, []);
|
||||
};
|
|
@ -15,7 +15,7 @@ import { memoize } from 'lodash';
|
|||
import React, { Fragment } from 'react';
|
||||
import chrome from 'ui/chrome';
|
||||
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
|
||||
import { LicenseContext } from '../../Main/LicenseCheck';
|
||||
import { LicenseContext } from '../../../../context/LicenseContext';
|
||||
import { MachineLearningFlyout } from './MachineLearningFlyout';
|
||||
import { WatcherFlyout } from './WatcherFlyout';
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue