Merge branch 'master' into introduce-start-phase

This commit is contained in:
restrry 2019-04-30 09:07:38 +02:00
commit f8cc8c88b1
429 changed files with 10648 additions and 5027 deletions

2
.gitignore vendored
View file

@ -31,7 +31,7 @@ webpackstats.json
!/config/kibana.yml
coverage
selenium
.babelcache.json
.babel_register_cache.json
.webpack.babelcache
*.swp
*.swo

View file

@ -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;
```

View file

@ -0,0 +1,19 @@
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [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> | |

View file

@ -0,0 +1,9 @@
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [ChromeBadge](./kibana-plugin-public.chromebadge.md) &gt; [text](./kibana-plugin-public.chromebadge.text.md)
## ChromeBadge.text property
<b>Signature:</b>
```typescript
text: string;
```

View file

@ -0,0 +1,9 @@
[Home](./index) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [ChromeBadge](./kibana-plugin-public.chromebadge.md) &gt; [tooltip](./kibana-plugin-public.chromebadge.tooltip.md)
## ChromeBadge.tooltip property
<b>Signature:</b>
```typescript
tooltip: string;
```

View file

@ -1,45 +1,46 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index) &gt; [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) &gt; [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) | |

View file

@ -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`.

View file

@ -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",

View file

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

View file

@ -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?

View file

@ -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;

View file

@ -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 });

View file

@ -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
*/

View file

@ -18,6 +18,7 @@
*/
export {
ChromeBadge,
ChromeBreadcrumb,
ChromeService,
ChromeSetup,

View file

@ -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,

View file

@ -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);

View file

@ -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)

View file

@ -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'), '{}'),
]);
},
};

View file

@ -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'
},
});

View file

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

View file

@ -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"
]
}
}
}
}
}

View file

@ -113,7 +113,8 @@ export default function (kibana) {
),
uiCapabilities: {
dev_tools: {
show: true
show: true,
save: true,
},
}
};

View file

@ -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(),

View file

@ -453,7 +453,7 @@ export function filebeatStatusCheck(moduleName) {
bool: {
filter: {
term: {
'fileset.module': moduleName,
'event.module': moduleName,
},
},
},

View file

@ -440,7 +440,7 @@ export function metricbeatStatusCheck(moduleName) {
bool: {
filter: {
term: {
'metricset.module': moduleName,
'event.module': moduleName,
},
},
},

View file

@ -172,7 +172,7 @@ export default function (kibana) {
save: true
},
indexPatterns: {
createNew: true,
save: true,
},
savedObjectsManagement: savedObjects.types.reduce((acc, type) => ({
...acc,

View file

@ -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,

View file

@ -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', {

View file

@ -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,

View file

@ -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 => {

View file

@ -64,7 +64,7 @@ class CreateButtonComponent extends Component<Props, State> {
return null;
}
if (!uiCapabilities.indexPatterns.createNew) {
if (!uiCapabilities.indexPatterns.save) {
return null;
}

View file

@ -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

View file

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

View file

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

View file

@ -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')

View file

@ -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,

View file

@ -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;
}, {});

View file

@ -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 {

View file

@ -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;
}

View file

@ -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);
});
});
});

View file

@ -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 {

View file

@ -0,0 +1,50 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* @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);

View file

@ -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);
});
});
});

View file

@ -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));

View file

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

View file

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

View file

@ -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', {

View file

@ -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)

View file

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

View file

@ -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

View 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';

View file

@ -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);
}
},
});
}

View 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);
}

View file

@ -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;

View file

@ -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(

View file

@ -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),
]);

View file

@ -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);
});
});
});

View file

@ -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

View file

@ -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();
});
});

View file

@ -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

View file

@ -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"

View file

@ -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>
&nbsp;
<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>

View 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 };

View file

@ -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}

View file

@ -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>
);

View 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);
});
});
});

View 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;
}

View file

@ -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);

View file

@ -28,3 +28,8 @@
.chrHeaderHelpMenu__version {
text-transform: none;
}
.chrHeaderBadge__wrapper {
align-self: center;
margin-right: $euiSize;
}

View file

@ -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$} />

View file

@ -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;
}
}
}

View file

@ -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$(),

View file

@ -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';

View file

@ -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

View file

@ -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;
}

View file

@ -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;
}
}
}
};

View file

@ -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;

View file

@ -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;

View file

@ -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() {

View file

@ -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);
}

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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');
});
});
});
});
});
}

View file

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

View file

@ -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');
});

View file

@ -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');
});

View file

@ -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();
});

View file

@ -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) {

View file

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

View file

@ -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');
}
};
}

View file

@ -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",

View file

@ -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
);

View file

@ -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']
}

View file

@ -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) || {};
}

View file

@ -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) {

View file

@ -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]
);

View file

@ -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>
);
}

View file

@ -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
);
}, []);
};

View file

@ -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