Merge remote-tracking branch 'origin/master' into feature/merge-code

This commit is contained in:
Fuyao Zhao 2019-03-14 09:34:58 -07:00
commit 87d3373f66
203 changed files with 8913 additions and 2609 deletions

View file

@ -7,6 +7,7 @@ cd "$(dirname "$0")/.."
source src/dev/ci_setup/extract_bootstrap_cache.sh
source src/dev/ci_setup/setup.sh
source src/dev/ci_setup/checkout_sibling_es.sh
case "$JOB" in
kibana-intake)

View file

@ -41,10 +41,6 @@ module.exports = {
forceNode: true,
},
},
react: {
version: '16.3',
},
},
rules: {
@ -71,6 +67,7 @@ module.exports = {
'packages/kbn-test-subj-selector/**/*',
'packages/kbn-test/**/*',
'packages/kbn-eslint-import-resolver-kibana/**/*',
'src/legacy/server/saved_objects/**/*',
'x-pack/plugins/apm/**/*',
'x-pack/plugins/canvas/**/*',
],

View file

@ -454,7 +454,8 @@ node scripts/docs.js --open
Part of this process only applies to maintainers, since it requires access to Github labels.
Kibana publishes major, minor and patch releases periodically through the year. During this process we run a script against this repo to collect the applicable PRs against that release and generate [Release Notes](https://www.elastic.co/guide/en/kibana/current/release-notes.html). To include your change in the Release Notes:
Kibana publishes major, minor and patch releases periodically through the year. During this process we run a script against this repo to collect the applicable PRs against that release and generate [Release Notes](https://www.elastic.co/guide/en/kibana/current/release-notes.html).
To include your change in the Release Notes:
1. In the title, summarize what the PR accomplishes in language that is meaningful to the user. In general, use present tense (for example, Adds, Fixes) in sentence case.
1. Label the PR with the targeted version (ex: 6.5).
@ -464,6 +465,9 @@ Kibana publishes major, minor and patch releases periodically through the year.
* For a deprecated feature, use `release_note:deprecation`.
* For a breaking change, use `release-breaking:note`.
To NOT include your changes in the Release Notes, please use label`non-issue`. PRs with the following labels also won't be included in the Release Notes:
`build`, `docs`, `test`, `non-issue`, `jenkins`, `backport`, and `chore`.
We also produce a blog post that details more important breaking API changes every minor and major release. If the PR includes a breaking API change, apply the label `release_note:dev_docs`. Additionally add a brief summary of the break at the bottom of the PR using the format below:

View file

@ -8,14 +8,15 @@ the *Setup Instructions* will get you started.
[role="screenshot"]
image::apm/images/apm-setup.png[Installation instructions on the APM page in Kibana]
After you install the Elastic APM agent library in your application,
Index patterns tell Kibana which Elasticsearch indices you want to explore.
An APM index pattern is necessary for certain features in the APM UI, like the query bar.
To set up the correct index pattern,
simply click *Load Kibana objects* at the bottom of the Setup Instructions.
After you install an Elastic APM agent library in your application,
the application automatically appears in the APM UI in {kib}.
No further configuration is required.
If you also use the Elastic Stack for logging and server-level metrics,
you can import the APM dashboards that come with the APM Server.
You can use these APM specific visualizations to correlate APM data with other data sources.
To get the dashboards, click *Load Kibana objects* at the bottom of the Setup Instructions.
[role="screenshot"]
image::apm/images/apm-setup-dashboards.png[Install dashboards for APM in Kibana]
image::apm/images/apm-index-pattern.png[Setup index pattern for APM in Kibana]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 393 KiB

After

Width:  |  Height:  |  Size: 311 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 KiB

After

Width:  |  Height:  |  Size: 410 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 KiB

After

Width:  |  Height:  |  Size: 426 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 480 KiB

After

Width:  |  Height:  |  Size: 501 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 KiB

After

Width:  |  Height:  |  Size: 429 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 430 KiB

After

Width:  |  Height:  |  Size: 510 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

After

Width:  |  Height:  |  Size: 265 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 368 KiB

After

Width:  |  Height:  |  Size: 408 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 401 KiB

After

Width:  |  Height:  |  Size: 479 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 423 KiB

After

Width:  |  Height:  |  Size: 450 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 384 KiB

After

Width:  |  Height:  |  Size: 332 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 KiB

After

Width:  |  Height:  |  Size: 299 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 KiB

After

Width:  |  Height:  |  Size: 408 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 289 KiB

View file

@ -37,4 +37,4 @@ After exploring these traces,
you can return to the full trace by clicking *View full trace* in the upper right hand corner of the page.
[role="screenshot"]
image::apm/images/apm-view-full-trace.png[Example of distributed trace colors in the APM UI in Kibana]
image::apm/images/apm-transaction-sample.png[Example of distributed trace colors in the APM UI in Kibana]

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

View file

@ -1,31 +1,30 @@
[[management-cross-cluster-search]]
=== Cross Cluster Search
=== {ccs-cap}
Elasticsearch supports the ability to run search and aggregation requests across multiple
clusters using a module called _cross cluster search_.
{es} supports the ability to run search and aggregation requests across multiple
clusters using a module called _{ccs}_.
In order to take advantage of cross cluster search, you must configure your Elasticsearch
clusters accordingly. Review the corresponding Elasticsearch
{ref}/modules-cross-cluster-search.html[documentation] before attempting to use cross cluster
search in Kibana.
In order to take advantage of {ccs}, you must configure your {es}
clusters accordingly. Review the corresponding {es}
{ref}/modules-cross-cluster-search.html[documentation] before attempting to use {ccs} in {kib}.
Once your Elasticsearch clusters are configured for cross cluster search, you can create
specific index patterns in Kibana to search across the clusters of your choosing. Using the
same syntax that you'd use in a raw cross cluster search request in Elasticsearch, create your
index pattern in Kibana with the convention `<cluster-names>:<pattern>`.
Once your {es} clusters are configured for {ccs}, you can create
specific index patterns in {kib} to search across the clusters of your choosing. Using the
same syntax that you'd use in a raw {ccs} request in {es}, create your
index pattern in {kib} with the convention `<cluster-names>:<pattern>`.
For example, if you want to query logstash indices across two of the Elasticsearch clusters
that you set up for cross cluster search, which were named `cluster_one` and `cluster_two`,
you would use `cluster_one:logstash-*,cluster_two:logstash-*` as your index pattern in Kibana.
For example, if you want to query {ls} indices across two of the {es} clusters
that you set up for {ccs}, which were named `cluster_one` and `cluster_two`,
you would use `cluster_one:logstash-*,cluster_two:logstash-*` as your index pattern in {kib}.
Just like in raw search requests in Elasticsearch, you can use wildcards in your cluster names
to match any number of clusters, so if you wanted to search logstash indices across any
Just like in raw search requests in {es}, you can use wildcards in your cluster names
to match any number of clusters, so if you wanted to search {ls} indices across any
clusters named `cluster_foo`, `cluster_bar`, and so on, you would use `cluster_*:logstash-*`
as your index pattern in Kibana.
as your index pattern in {kib}.
If you want to query across all Elasticsearch clusters that have been configured for cross
cluster search, then use a standalone wildcard for your cluster name in your Kibana index
If you want to query across all {es} clusters that have been configured for {ccs},
then use a standalone wildcard for your cluster name in your {kib} index
pattern: `*:logstash-*`.
Once an index pattern is configured using the cross cluster search syntax, all searches and
aggregations using that index pattern in Kibana take advantage of cross cluster search.
Once an index pattern is configured using the {ccs} syntax, all searches and
aggregations using that index pattern in {kib} take advantage of {ccs}.

View file

@ -1,26 +1,67 @@
[[working-remote-clusters]]
== Working with remote clusters
{kib} *Management* provides user interfaces for working with data from remote
clusters and managing the {ccr} process. You can replicate indices from a
leader remote cluster to a follower index in a local cluster. The local follower indices
can be used to provide remote backups for disaster recovery or for geo-proximite copies of data.
Before using these features, you should be familiar with the following concepts:
* {stack-ov}/xpack-ccr.html[{ccr-cap}]
* {ref}/modules-cross-cluster-search.html[{ccs-cap}]
* {stack-ov}/cross-cluster-configuring.html[Cross-cluster security requirements]
[float]
[[managing-remote-clusters]]
== Managing Remote Clusters
== Managing remote clusters
{kib} *Management* provides two user interfaces for working with data from remote
clusters.
*Remote clusters* helps you manage remote clusters for use with
{ccs} and {ccr}. You can add and remove remote clusters and check their connectivity.
*Remote Clusters* helps you manage remote clusters for use with
{ref}/modules-cross-cluster-search.html[cross cluster search] and
{xpack-ref}/xpack-ccr.html[cross cluster replication]. You can add and remove remote
clusters and check their connectivity.
Before you use this feature, you should be familiar with the concept of
{ref}/modules-remote-clusters.html[remote clusters].
Go to *Management > Elasticsearch > Remote clusters* to create or manage your remotes.
Go to *Management > Elasticsearch > Remote Clusters* to get started.
To set up a new remote, click *Add a remote cluster*. Give the cluster a unique name
and define the seed nodes for cluster discovery. You can edit or remove your remote clusters
from the *Remote clusters* list view.
[role="screenshot"]
image::images/add_remote_cluster.png[][UI for adding a remote cluster]
Once a remote cluster is registered, you can use the tools under *{ccr-cap}*
to add and manage follower indices on the local cluster, and replicate data from
indices on the remote cluster based on an auto-follow index pattern.
*Cross Cluster Replication* includes tools to help you create and manage the remote
replication process. You can follow an index pattern on the remote cluster for
auto-discovery and then replicate new indices in the local cluster that match the
auto-follow pattern.
[float]
[[managing-cross-cluster-replication]]
== [xpack]#Managing {ccr}#
Go to *Management > Elasticsearch > Cross Cluster Replication* to get started.
*{ccr-cap}* helps you create and manage the {ccr} process.
If you want to replicate data from existing indices, or set up
local followers on a case-by-case basis, go to *Follower indices*.
If you want to automatically detect and follow new indices when they are created
on a remote cluster, you can do so from *Auto-follow patterns*.
Creating an auto-follow pattern is useful when you have time-series data, like a logs index, on the
remote cluster that is created or rolled over on a daily basis. Once you have configured an
auto-follow pattern, any time a new index with a name that matches the pattern is
created in the remote cluster, a follower index is automatically configured in the local cluster.
From the same view, you can also see a list of your saved auto-follow patterns for
a given remote cluster, and monitor whether the replication is active.
Before you use these features, you should be familiar with the following concepts:
* {stack-ov}/ccr-requirements.html[Requirements for leader indices]
* {stack-ov}/ccr-auto-follow.html[Automatically following indices]
To get started, go to *Management > Elasticsearch > {ccr-cap}*.
[role="screenshot"]
image::images/auto_follow_pattern.png[][UI for adding an auto-follow pattern]
[role="screenshot"]
image::images/follower_indices.png[][UI for adding follower indices]

View file

@ -1,16 +1,16 @@
[[cross-cluster-kibana]]
==== Cross Cluster Search and Kibana
==== {ccs-cap} and {kib}
When Kibana is used to search across multiple clusters, a two-step authorization
When {kib} is used to search across multiple clusters, a two-step authorization
process determines whether or not the user can access indices on a remote
cluster:
* First, the local cluster determines if the user is authorized to access remote
clusters. (The local cluster is the cluster Kibana is connected to.)
clusters. (The local cluster is the cluster {kib} is connected to.)
* If they are, the remote cluster then determines if the user has access
to the specified indices.
To grant Kibana users access to remote clusters, assign them a local role
To grant {kib} users access to remote clusters, assign them a local role
with read privileges to indices on the remote clusters. You specify remote
cluster indices as `<remote_cluster_name>:<index_name>`.
@ -18,10 +18,10 @@ To enable users to actually read the remote indices, you must create a matching
role on the remote clusters that grants the `read_cross_cluster` privilege
and access to the appropriate indices.
For example, if Kibana is connected to the cluster where you're actively
indexing Logstash data (your _local cluster_) and you're periodically
For example, if {kib} is connected to the cluster where you're actively
indexing {ls} data (your _local cluster_) and you're periodically
offloading older time-based indices to an archive cluster
(your _remote cluster_) and you want to enable Kibana users to search both
(your _remote cluster_) and you want to enable {kib} users to search both
clusters:
. On the local cluster, create a `logstash_reader` role that grants
@ -31,7 +31,7 @@ NOTE: If you configure the local cluster as another remote in {es}, the
`logstash_reader` role on your local cluster also needs to grant the
`read_cross_cluster` privilege.
. Assign your Kibana users the `kibana_user` role and your `logstash_reader`
. Assign your {kib} users the `kibana_user` role and your `logstash_reader`
role.
. On the remote cluster, create a `logstash_reader` role that grants the

View file

@ -111,6 +111,7 @@
"@kbn/ui-framework": "1.0.0",
"@types/json-stable-stringify": "^1.0.32",
"@types/lodash.clonedeep": "^4.5.4",
"@types/recompose": "^0.30.5",
"JSONStream": "1.1.1",
"abortcontroller-polyfill": "^1.1.9",
"angular": "1.6.9",

View file

@ -18,7 +18,7 @@ module.exports = {
settings: {
react: {
version: semver.coerce(PKG.dependencies.react),
version: semver.valid(semver.coerce(PKG.dependencies.react)),
},
},

View file

@ -11,7 +11,7 @@ Usage:
Options:
--help Display this menu and exit.
--config <file> Pass in a config. Can pass in multiple configs.
--esFrom <snapshot|source> Build Elasticsearch from source or run from snapshot. Default: snapshot
--esFrom <snapshot|source> Build Elasticsearch from source or run from snapshot. Default: $TEST_ES_FROM or snapshot
--kibana-install-dir <dir> Run Kibana from existing install directory instead of from source.
--bail Stop the test run at the first failure.
--grep <pattern> Pattern to select which tests to run.
@ -32,6 +32,7 @@ Object {
<absolute path>/foo,
],
"createLogger": [Function],
"esFrom": "snapshot",
"extraKbnOpts": undefined,
"suiteTags": Object {
"exclude": Array [],
@ -49,6 +50,7 @@ Object {
],
"createLogger": [Function],
"debug": true,
"esFrom": "snapshot",
"extraKbnOpts": undefined,
"suiteTags": Object {
"exclude": Array [],
@ -65,6 +67,7 @@ Object {
<absolute path>/foo,
],
"createLogger": [Function],
"esFrom": "snapshot",
"extraKbnOpts": undefined,
"suiteTags": Object {
"exclude": Array [],
@ -83,6 +86,7 @@ Object {
<absolute path>/foo,
],
"createLogger": [Function],
"esFrom": "snapshot",
"extraKbnOpts": Object {
"server.foo": "bar",
},
@ -100,6 +104,7 @@ Object {
<absolute path>/foo,
],
"createLogger": [Function],
"esFrom": "snapshot",
"extraKbnOpts": undefined,
"quiet": true,
"suiteTags": Object {
@ -116,6 +121,7 @@ Object {
<absolute path>/foo,
],
"createLogger": [Function],
"esFrom": "snapshot",
"extraKbnOpts": undefined,
"silent": true,
"suiteTags": Object {
@ -125,6 +131,22 @@ Object {
}
`;
exports[`process options for run tests CLI accepts source value for $TEST_ES_FROM 1`] = `
Object {
"assertNoneExcluded": false,
"configs": Array [
<absolute path>/foo,
],
"createLogger": [Function],
"esFrom": "source",
"extraKbnOpts": undefined,
"suiteTags": Object {
"exclude": Array [],
"include": Array [],
},
}
`;
exports[`process options for run tests CLI accepts source value for esFrom 1`] = `
Object {
"assertNoneExcluded": false,
@ -148,6 +170,7 @@ Object {
<absolute path>/foo,
],
"createLogger": [Function],
"esFrom": "snapshot",
"extraKbnOpts": undefined,
"installDir": "foo",
"suiteTags": Object {
@ -164,6 +187,7 @@ Object {
<absolute path>/foo,
],
"createLogger": [Function],
"esFrom": "snapshot",
"extraKbnOpts": undefined,
"grep": "management",
"suiteTags": Object {
@ -180,6 +204,7 @@ Object {
<absolute path>/foo,
],
"createLogger": [Function],
"esFrom": "snapshot",
"extraKbnOpts": undefined,
"suiteTags": Object {
"exclude": Array [],
@ -188,3 +213,19 @@ Object {
"verbose": true,
}
`;
exports[`process options for run tests CLI prioritizes source flag over $TEST_ES_FROM 1`] = `
Object {
"assertNoneExcluded": false,
"configs": Array [
<absolute path>/foo,
],
"createLogger": [Function],
"esFrom": "snapshot",
"extraKbnOpts": undefined,
"suiteTags": Object {
"exclude": Array [],
"include": Array [],
},
}
`;

View file

@ -11,7 +11,7 @@ Usage:
Options:
--help Display this menu and exit.
--config <file> Pass in a config. Can pass in multiple configs.
--esFrom <snapshot|source> Build Elasticsearch from source or run from snapshot. Default: snapshot
--esFrom <snapshot|source> Build Elasticsearch from source or run from snapshot. Default: $TEST_ES_FROM or snapshot
--kibana-install-dir <dir> Run Kibana from existing install directory instead of from source.
--bail Stop the test run at the first failure.
--grep <pattern> Pattern to select which tests to run.

View file

@ -32,7 +32,7 @@ const options = {
arg: '<snapshot|source>',
choices: ['snapshot', 'source'],
desc: 'Build Elasticsearch from source or run from snapshot.',
default: 'snapshot',
defaultHelp: 'Default: $TEST_ES_FROM or snapshot',
},
'kibana-install-dir': {
arg: '<dir>',
@ -71,7 +71,7 @@ export function displayHelp() {
return {
...option,
usage: `${name} ${option.arg || ''}`,
default: option.default ? `Default: ${option.default}` : '',
default: option.defaultHelp || '',
};
})
.map(option => {
@ -106,6 +106,10 @@ export function processOptions(userOptions, defaultConfigPaths) {
}
}
if (!userOptions.esFrom) {
userOptions.esFrom = process.env.TEST_ES_FROM || 'snapshot';
}
if (userOptions['kibana-install-dir']) {
userOptions.installDir = userOptions['kibana-install-dir'];
delete userOptions['kibana-install-dir'];

View file

@ -22,6 +22,14 @@ import { createAbsolutePathSerializer } from '@kbn/dev-utils';
expect.addSnapshotSerializer(createAbsolutePathSerializer(process.cwd()));
const INITIAL_TEST_ES_FROM = process.env.TEST_ES_FROM;
beforeEach(() => {
process.env.TEST_ES_FROM = 'snapshot';
});
afterEach(() => {
process.env.TEST_ES_FROM = INITIAL_TEST_ES_FROM;
});
describe('display help for run tests CLI', () => {
it('displays as expected', () => {
expect(displayHelp()).toMatchSnapshot();
@ -73,6 +81,18 @@ describe('process options for run tests CLI', () => {
expect(options).toMatchSnapshot();
});
it('accepts source value for $TEST_ES_FROM', () => {
process.env.TEST_ES_FROM = 'source';
const options = processOptions({}, ['foo']);
expect(options).toMatchSnapshot();
});
it('prioritizes source flag over $TEST_ES_FROM', () => {
process.env.TEST_ES_FROM = 'source';
const options = processOptions({ esFrom: 'snapshot' }, ['foo']);
expect(options).toMatchSnapshot();
});
it('rejects non-enum value for esFrom', () => {
expect(() => {
processOptions({ esFrom: 'butter' }, ['foo']);

View file

@ -30,7 +30,7 @@ jest.mock('../../tasks', () => ({
describe('run tests CLI', () => {
describe('options', () => {
const originalObjects = {};
const originalObjects = { process, console };
const exitMock = jest.fn();
const logMock = jest.fn(); // mock logging so we don't send output to the test results
const argvMock = ['foo', 'foo'];
@ -40,11 +40,13 @@ describe('run tests CLI', () => {
argv: argvMock,
stdout: new Writable(),
cwd: jest.fn(),
env: {
...originalObjects.process.env,
TEST_ES_FROM: 'snapshot',
},
};
beforeAll(() => {
originalObjects.process = process;
originalObjects.console = console;
global.process = processMock;
global.console = { log: logMock };
});
@ -56,6 +58,10 @@ describe('run tests CLI', () => {
beforeEach(() => {
global.process.argv = [...argvMock];
global.process.env = {
...originalObjects.process.env,
TEST_ES_FROM: 'snapshot',
};
jest.resetAllMocks();
});

View file

@ -11,7 +11,7 @@ Usage:
Options:
--help Display this menu and exit.
--config <file> Pass in a config
--esFrom <snapshot|source|path> Build Elasticsearch from source, snapshot or path to existing install dir. Default: snapshot
--esFrom <snapshot|source|path> Build Elasticsearch from source, snapshot or path to existing install dir. Default: $TEST_ES_FROM or snapshot
--kibana-install-dir <dir> Run Kibana from existing install directory instead of from source.
--verbose Log everything.
--debug Run in debug mode.
@ -72,6 +72,15 @@ Object {
}
`;
exports[`process options for start servers CLI accepts source value for $TEST_ES_FROM 1`] = `
Object {
"config": <absolute path>/foo,
"createLogger": [Function],
"esFrom": "source",
"extraKbnOpts": undefined,
}
`;
exports[`process options for start servers CLI accepts source value for esFrom 1`] = `
Object {
"config": <absolute path>/foo,
@ -100,3 +109,12 @@ Object {
"verbose": true,
}
`;
exports[`process options for start servers CLI prioritizes source flag over $TEST_ES_FROM 1`] = `
Object {
"config": <absolute path>/foo,
"createLogger": [Function],
"esFrom": "snapshot",
"extraKbnOpts": undefined,
}
`;

View file

@ -31,7 +31,7 @@ const options = {
esFrom: {
arg: '<snapshot|source|path>',
desc: 'Build Elasticsearch from source, snapshot or path to existing install dir.',
default: 'snapshot',
defaultHelp: 'Default: $TEST_ES_FROM or snapshot',
},
'kibana-install-dir': {
arg: '<dir>',
@ -51,7 +51,7 @@ export function displayHelp() {
return {
...option,
usage: `${name} ${option.arg || ''}`,
default: option.default ? `Default: ${option.default}` : '',
default: option.defaultHelp || '',
};
})
.map(option => {
@ -82,7 +82,7 @@ export function processOptions(userOptions, defaultConfigPath) {
}
if (!userOptions.esFrom) {
userOptions.esFrom = 'snapshot';
userOptions.esFrom = process.env.TEST_ES_FROM || 'snapshot';
}
if (userOptions['kibana-install-dir']) {

View file

@ -22,6 +22,14 @@ import { createAbsolutePathSerializer } from '@kbn/dev-utils';
expect.addSnapshotSerializer(createAbsolutePathSerializer(process.cwd()));
const INITIAL_TEST_ES_FROM = process.env.TEST_ES_FROM;
beforeEach(() => {
process.env.TEST_ES_FROM = 'snapshot';
});
afterEach(() => {
process.env.TEST_ES_FROM = INITIAL_TEST_ES_FROM;
});
describe('display help for start servers CLI', () => {
it('displays as expected', () => {
expect(displayHelp()).toMatchSnapshot();
@ -68,6 +76,18 @@ describe('process options for start servers CLI', () => {
expect(options).toMatchSnapshot();
});
it('accepts source value for $TEST_ES_FROM', () => {
process.env.TEST_ES_FROM = 'source';
const options = processOptions({}, 'foo');
expect(options).toMatchSnapshot();
});
it('prioritizes source flag over $TEST_ES_FROM', () => {
process.env.TEST_ES_FROM = 'source';
const options = processOptions({ esFrom: 'snapshot' }, 'foo');
expect(options).toMatchSnapshot();
});
it('accepts debug option', () => {
const options = processOptions({ debug: true }, 'foo');
expect(options).toMatchSnapshot();

View file

@ -30,7 +30,7 @@ jest.mock('../../tasks', () => ({
describe('start servers CLI', () => {
describe('options', () => {
const originalObjects = {};
const originalObjects = { process, console };
const exitMock = jest.fn();
const logMock = jest.fn(); // mock logging so we don't send output to the test results
const argvMock = ['foo', 'foo'];
@ -40,11 +40,13 @@ describe('start servers CLI', () => {
argv: argvMock,
stdout: new Writable(),
cwd: jest.fn(),
env: {
...originalObjects.process.env,
TEST_ES_FROM: 'snapshot',
},
};
beforeAll(() => {
originalObjects.process = process;
originalObjects.console = console;
global.process = processMock;
global.console = { log: logMock };
});
@ -56,6 +58,10 @@ describe('start servers CLI', () => {
beforeEach(() => {
global.process.argv = [...argvMock];
global.process.env = {
...originalObjects.process.env,
TEST_ES_FROM: 'snapshot',
};
jest.resetAllMocks();
});

View file

@ -1,11 +1,3 @@
// We apply brute force focus states to anything not coming from Eui
// which has focus states designed at the component level.
:focus {
&:not([class^="eui"]) {
@include euiFocusRing;
}
}
/**
* 1. Required for IE11.
*/

View file

@ -48,12 +48,12 @@ function checkout_sibling {
return 0
fi
cloneBranch="${PR_TARGET_BRANCH:-master}"
cloneBranch="${PR_TARGET_BRANCH:-$KIBANA_PKG_BRANCH}"
if clone_target_is_valid ; then
return 0
fi
cloneBranch="master"
cloneBranch="$KIBANA_PKG_BRANCH"
if clone_target_is_valid; then
return 0
fi
@ -64,13 +64,15 @@ function checkout_sibling {
function checkout_clone_target {
pick_clone_target
if [[ $cloneBranch = "master" && $cloneAuthor = "elastic" ]]; then
export TEST_ES_FROM=snapshot
if [[ "$cloneAuthor/$cloneBranch" != "elastic/$KIBANA_PKG_BRANCH" ]]; then
echo " -> Setting TEST_ES_FROM=source so that ES in tests will be built from $cloneAuthor/$cloneBranch"
export TEST_ES_FROM=source
fi
echo " -> checking out '${cloneBranch}' branch from ${cloneAuthor}/${project}..."
git clone -b "$cloneBranch" "git@github.com:${cloneAuthor}/${project}.git" "$targetDir" --depth=1
echo " -> checked out ${project} revision: $(git -C ${targetDir} rev-parse HEAD)"
echo " -> checked out ${project} revision: $(git -C "${targetDir}" rev-parse HEAD)"
echo
}
@ -86,6 +88,7 @@ function checkout_sibling {
}
checkout_sibling "elasticsearch" "${PARENT_DIR}/elasticsearch" "USE_EXISTING_ES"
export TEST_ES_FROM=${TEST_ES_FROM:-snapshot}
# Set the JAVA_HOME based on the Java property file in the ES repo
# This assumes the naming convention used on CI (ex: ~/.java/java10)

View file

@ -26,14 +26,21 @@ else
exit 1
fi
export KIBANA_DIR="$dir"
export XPACK_DIR="$KIBANA_DIR/x-pack"
export PARENT_DIR="$(cd "$KIBANA_DIR/.."; pwd)"
echo "-> KIBANA_DIR $KIBANA_DIR"
echo "-> XPACK_DIR $XPACK_DIR"
echo "-> PARENT_DIR $PARENT_DIR"
echo "-> TEST_ES_SNAPSHOT_VERSION $TEST_ES_SNAPSHOT_VERSION"
parentDir="$(cd "$KIBANA_DIR/.."; pwd)"
export PARENT_DIR="$parentDir"
kbnBranch="$(jq -r .branch "$KIBANA_DIR/package.json")"
export KIBANA_PKG_BRANCH="$kbnBranch"
echo " -- KIBANA_DIR='$KIBANA_DIR'"
echo " -- XPACK_DIR='$XPACK_DIR'"
echo " -- PARENT_DIR='$PARENT_DIR'"
echo " -- KIBANA_PKG_BRANCH='$KIBANA_PKG_BRANCH'"
echo " -- TEST_ES_SNAPSHOT_VERSION='$TEST_ES_SNAPSHOT_VERSION'"
###
### download node
@ -77,7 +84,6 @@ else
else
curl --silent "$nodeUrl" | tar -xz -C "$nodeDir" --strip-components=1
fi
fi
###

View file

@ -0,0 +1,69 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render popover when appLinks is not empty 1`] = `
<EuiPopover
anchorPosition="downCenter"
button={
<EuiButton
aria-label="View Sample eCommerce orders"
color="primary"
fill={false}
iconSide="right"
iconType="arrowDown"
onClick={[Function]}
type="button"
>
View data
</EuiButton>
}
closePopover={[Function]}
hasArrow={true}
id="sampleDataLinksecommerce"
isOpen={false}
ownFocus={false}
panelPaddingSize="none"
>
<EuiContextMenu
initialPanelId={0}
panels={
Array [
Object {
"id": 0,
"items": Array [
Object {
"href": "root/app/kibana#/dashboard/722b74f0-b882-11e8-a6d9-e546fe2bba5f",
"icon": <EuiIcon
size="m"
type="dashboardApp"
/>,
"name": "Dashboard",
},
Object {
"href": "rootapp/myAppPath",
"icon": <EuiIcon
size="m"
type="logoKibana"
/>,
"name": "myAppLabel",
},
],
},
]
}
/>
</EuiPopover>
`;
exports[`should render simple button when appLinks is empty 1`] = `
<EuiButton
aria-label="View Sample eCommerce orders"
color="primary"
data-test-subj="launchSampleDataSetecommerce"
fill={false}
href="root/app/kibana#/dashboard/722b74f0-b882-11e8-a6d9-e546fe2bba5f"
iconSide="left"
type="button"
>
View data
</EuiButton>
`;

View file

@ -34,6 +34,8 @@ export const UNINSTALLED_STATUS = 'not_installed';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { SampleDataViewDataButton } from './sample_data_view_data_button';
export class SampleDataSetCard extends React.Component {
isInstalled = () => {
@ -91,21 +93,12 @@ export class SampleDataSetCard extends React.Component {
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
href={this.props.launchUrl}
data-test-subj={`launchSampleDataSet${this.props.id}`}
aria-label={i18n.translate('kbn.home.sampleDataSetCard.viewDataButtonAriaLabel', {
defaultMessage: 'View {datasetName}',
values: {
datasetName: this.props.name,
}
})}
>
<FormattedMessage
id="kbn.home.sampleDataSetCard.viewDataButtonLabel"
defaultMessage="View data"
/>
</EuiButton>
<SampleDataViewDataButton
id={this.props.id}
name={this.props.name}
overviewDashboard={this.props.overviewDashboard}
appLinks={this.props.appLinks}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
@ -207,7 +200,12 @@ SampleDataSetCard.propTypes = {
id: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
launchUrl: PropTypes.string.isRequired,
overviewDashboard: PropTypes.string.isRequired,
appLinks: PropTypes.arrayOf(PropTypes.shape({
path: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
})).isRequired,
status: PropTypes.oneOf([
INSTALLED_STATUS,
UNINSTALLED_STATUS,

View file

@ -199,7 +199,8 @@ export class SampleDataSetCards extends React.Component {
id={sampleDataSet.id}
description={sampleDataSet.description}
name={sampleDataSet.name}
launchUrl={this.props.addBasePath(`/app/kibana#/dashboard/${sampleDataSet.overviewDashboard}`)}
overviewDashboard={sampleDataSet.overviewDashboard}
appLinks={sampleDataSet.appLinks}
status={sampleDataSet.status}
isProcessing={_.get(this.state.processingStatus, sampleDataSet.id, false)}
statusMsg={sampleDataSet.statusMsg}

View file

@ -0,0 +1,141 @@
/*
* 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 React from 'react';
import PropTypes from 'prop-types';
import {
EuiButton,
EuiContextMenu,
EuiIcon,
EuiPopover,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import chrome from 'ui/chrome';
export class SampleDataViewDataButton extends React.Component {
state = {
isPopoverOpen: false
}
togglePopoverVisibility = () => {
this.setState(prevState => ({
isPopoverOpen: !prevState.isPopoverOpen,
}));
};
closePopover = () => {
this.setState({
isPopoverOpen: false,
});
};
render() {
const viewDataButtonLabel = i18n.translate('kbn.home.sampleDataSetCard.viewDataButtonLabel', {
defaultMessage: 'View data' });
const viewDataButtonAriaLabel = i18n.translate('kbn.home.sampleDataSetCard.viewDataButtonAriaLabel', {
defaultMessage: 'View {datasetName}',
values: {
datasetName: this.props.name,
},
});
const dashboardPath = chrome.addBasePath(`/app/kibana#/dashboard/${this.props.overviewDashboard}`);
if (this.props.appLinks.length === 0) {
return (
<EuiButton
href={dashboardPath}
data-test-subj={`launchSampleDataSet${this.props.id}`}
aria-label={viewDataButtonAriaLabel}
>
{viewDataButtonLabel}
</EuiButton>
);
}
const additionalItems = this.props.appLinks.map(({ path, label, icon }) => {
return {
name: label,
icon: (
<EuiIcon
type={icon}
size="m"
/>
),
href: chrome.addBasePath(path)
};
});
const panels = [
{
id: 0,
items: [
{
name: i18n.translate('kbn.home.sampleDataSetCard.dashboardLinkLabel', {
defaultMessage: 'Dashboard' }),
icon: (
<EuiIcon
type="dashboardApp"
size="m"
/>
),
href: dashboardPath,
},
...additionalItems
]
}
];
const popoverButton = (
<EuiButton
aria-label={viewDataButtonAriaLabel}
onClick={this.togglePopoverVisibility}
iconType="arrowDown"
iconSide="right"
>
{viewDataButtonLabel}
</EuiButton>
);
return (
<EuiPopover
id={`sampleDataLinks${this.props.id}`}
button={popoverButton}
isOpen={this.state.isPopoverOpen}
closePopover={this.closePopover}
panelPaddingSize="none"
anchorPosition="downCenter"
>
<EuiContextMenu
initialPanelId={0}
panels={panels}
/>
</EuiPopover>
);
}
}
SampleDataViewDataButton.propTypes = {
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
overviewDashboard: PropTypes.string.isRequired,
appLinks: PropTypes.arrayOf(PropTypes.shape({
path: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
})).isRequired,
};

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.
*/
jest.mock('ui/chrome', () => {
return {
addBasePath: (path) => {
return `root${path}`;
},
};
});
import React from 'react';
import { shallow } from 'enzyme';
import { SampleDataViewDataButton } from './sample_data_view_data_button';
test('should render simple button when appLinks is empty', () => {
const component = shallow(<SampleDataViewDataButton
id="ecommerce"
name="Sample eCommerce orders"
overviewDashboard="722b74f0-b882-11e8-a6d9-e546fe2bba5f"
appLinks={[]}
/>);
expect(component).toMatchSnapshot(); // eslint-disable-line
});
test('should render popover when appLinks is not empty', () => {
const appLinks = [
{
path: 'app/myAppPath',
label: 'myAppLabel',
icon: 'logoKibana'
}
];
const component = shallow(<SampleDataViewDataButton
id="ecommerce"
name="Sample eCommerce orders"
overviewDashboard="722b74f0-b882-11e8-a6d9-e546fe2bba5f"
appLinks={appLinks}
/>);
expect(component).toMatchSnapshot(); // eslint-disable-line
});

View file

@ -44,6 +44,12 @@ const dataIndexSchema = Joi.object({
preserveDayOfWeekTimeOfDay: Joi.boolean().default(false),
});
const appLinkSchema = Joi.object({
path: Joi.string().required(),
label: Joi.string().required(),
icon: Joi.string().required(),
});
export const sampleDataSchema = {
id: Joi.string().regex(/^[a-zA-Z0-9-]+$/).required(),
name: Joi.string().required(),
@ -53,6 +59,7 @@ export const sampleDataSchema = {
// saved object id of main dashboard for sample data set
overviewDashboard: Joi.string().required(),
appLinks: Joi.array().items(appLinkSchema).default([]),
// saved object id of default index-pattern for sample data set
defaultIndex: Joi.string().required(),

View file

@ -19,33 +19,43 @@
import Boom from 'boom';
import Joi from 'joi';
import { loadData } from './lib/load_data';
import { createIndexName } from './lib/create_index_name';
import {
dateToIso8601IgnoringTime,
translateTimeRelativeToDifference,
translateTimeRelativeToWeek
translateTimeRelativeToWeek,
} from './lib/translate_timestamp';
function insertDataIntoIndex(dataIndexConfig, index, nowReference, request, server, callWithRequest) {
const bulkInsert = async (docs) => {
function insertDataIntoIndex(
dataIndexConfig,
index,
nowReference,
request,
server,
callWithRequest
) {
const bulkInsert = async docs => {
function updateTimestamps(doc) {
dataIndexConfig.timeFields.forEach(timeFieldName => {
if (doc[timeFieldName]) {
doc[timeFieldName] = dataIndexConfig.preserveDayOfWeekTimeOfDay
? translateTimeRelativeToWeek(doc[timeFieldName], dataIndexConfig.currentTimeMarker, nowReference)
: translateTimeRelativeToDifference(doc[timeFieldName], dataIndexConfig.currentTimeMarker, nowReference);
? translateTimeRelativeToWeek(
doc[timeFieldName],
dataIndexConfig.currentTimeMarker,
nowReference
)
: translateTimeRelativeToDifference(
doc[timeFieldName],
dataIndexConfig.currentTimeMarker,
nowReference
);
}
});
return doc;
}
const insertCmd = {
index: {
_index: index
}
};
const insertCmd = { index: { _index: index } };
const bulk = [];
docs.forEach(doc => {
@ -56,8 +66,15 @@ function insertDataIntoIndex(dataIndexConfig, index, nowReference, request, serv
if (resp.errors) {
server.log(
['warning'],
`sample_data install errors while bulk inserting. Elasticsearch response: ${JSON.stringify(resp, null, '')}`);
return Promise.reject(new Error(`Unable to load sample data into index "${index}", see kibana logs for details`));
`sample_data install errors while bulk inserting. Elasticsearch response: ${JSON.stringify(
resp,
null,
''
)}`
);
return Promise.reject(
new Error(`Unable to load sample data into index "${index}", see kibana logs for details`)
);
}
};
@ -69,25 +86,22 @@ export const createInstallRoute = () => ({
method: 'POST',
config: {
validate: {
query: Joi.object().keys({
now: Joi.date().iso()
}),
params: Joi.object().keys({
id: Joi.string().required(),
}).required()
query: Joi.object().keys({ now: Joi.date().iso() }),
params: Joi.object()
.keys({ id: Joi.string().required() })
.required(),
},
handler: async (request, h) => {
const server = request.server;
const sampleDataset = server.getSampleDatasets().find(sampleDataset => {
return sampleDataset.id === request.params.id;
});
const { server, params, query } = request;
const sampleDataset = server.getSampleDatasets().find(({ id }) => id === params.id);
if (!sampleDataset) {
return h.response().code(404);
}
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data');
const now = request.query.now ? request.query.now : new Date();
const now = query.now ? query.now : new Date();
const nowReference = dateToIso8601IgnoringTime(now);
const counts = {};
@ -97,7 +111,7 @@ export const createInstallRoute = () => ({
// clean up any old installation of dataset
try {
await callWithRequest(request, 'indices.delete', { index: index });
await callWithRequest(request, 'indices.delete', { index });
} catch (err) {
// ignore delete errors
}
@ -106,16 +120,9 @@ export const createInstallRoute = () => ({
const createIndexParams = {
index: index,
body: {
settings: {
index: {
number_of_shards: 1,
number_of_replicas: 0
}
},
mappings: {
properties: dataIndexConfig.fields
}
}
settings: { index: { number_of_shards: 1, number_of_replicas: 0 } },
mappings: { properties: dataIndexConfig.fields },
},
};
await callWithRequest(request, 'indices.create', createIndexParams);
} catch (err) {
@ -126,7 +133,13 @@ export const createInstallRoute = () => ({
try {
const count = await insertDataIntoIndex(
dataIndexConfig, index, nowReference, request, server, callWithRequest);
dataIndexConfig,
index,
nowReference,
request,
server,
callWithRequest
);
counts[index] = count;
} catch (err) {
server.log(['warning'], `sample_data install errors while loading data. Error: ${err}`);
@ -136,20 +149,32 @@ export const createInstallRoute = () => ({
let createResults;
try {
createResults = await request.getSavedObjectsClient().bulkCreate(sampleDataset.savedObjects, { overwrite: true });
} catch (err) {
createResults = await request
.getSavedObjectsClient()
.bulkCreate(sampleDataset.savedObjects, { overwrite: true });
} catch (err) {
server.log(['warning'], `bulkCreate failed, error: ${err.message}`);
return Boom.badImplementation(`Unable to load kibana saved objects, see kibana logs for details`);
return Boom.badImplementation(
`Unable to load kibana saved objects, see kibana logs for details`
);
}
const errors = createResults.saved_objects.filter(savedObjectCreateResult => {
return savedObjectCreateResult.hasOwnProperty('error');
});
if (errors.length > 0) {
server.log(['warning'], `sample_data install errors while loading saved objects. Errors: ${errors.join(',')}`);
return h.response(`Unable to load kibana saved objects, see kibana logs for details`).code(403);
server.log(
['warning'],
`sample_data install errors while loading saved objects. Errors: ${errors.join(',')}`
);
return h
.response(`Unable to load kibana saved objects, see kibana logs for details`)
.code(403);
}
return h.response({ elasticsearchIndicesCreated: counts, kibanaSavedObjectsLoaded: sampleDataset.savedObjects.length });
}
}
return h.response({
elasticsearchIndicesCreated: counts,
kibanaSavedObjectsLoaded: sampleDataset.savedObjects.length,
});
},
},
});

View file

@ -24,19 +24,15 @@ import zlib from 'zlib';
const BULK_INSERT_SIZE = 500;
export function loadData(path, bulkInsert) {
return new Promise(function (resolve, reject) {
return new Promise((resolve, reject) => {
let count = 0;
let docs = [];
let isPaused = false;
const readStream = fs.createReadStream(path, {
// pause does not stop lines already in buffer. Use smaller buffer size to avoid bulk inserting to many records
highWaterMark: 1024 * 4
});
const lineStream = readline.createInterface({
input: readStream.pipe(zlib.Unzip()) // eslint-disable-line new-cap
});
// pause does not stop lines already in buffer. Use smaller buffer size to avoid bulk inserting to many records
const readStream = fs.createReadStream(path, { highWaterMark: 1024 * 4 });
// eslint-disable-next-line new-cap
const lineStream = readline.createInterface({ input: readStream.pipe(zlib.Unzip()) });
const onClose = async () => {
if (docs.length > 0) {
try {
@ -50,13 +46,13 @@ export function loadData(path, bulkInsert) {
};
lineStream.on('close', onClose);
function closeWithError(err) {
const closeWithError = err => {
lineStream.removeListener('close', onClose);
lineStream.close();
reject(err);
}
};
lineStream.on('line', async (line) => {
lineStream.on('line', async line => {
if (line.length === 0 || line.charAt(0) === '#') {
return;
}
@ -65,7 +61,11 @@ export function loadData(path, bulkInsert) {
try {
doc = JSON.parse(line);
} catch (err) {
closeWithError(new Error(`Unable to parse line as JSON document, line: """${line}""", Error: ${err.message}`));
closeWithError(
new Error(
`Unable to parse line as JSON document, line: """${line}""", Error: ${err.message}`
)
);
return;
}

View file

@ -28,7 +28,7 @@ export const createListRoute = () => ({
path: '/api/sample_data',
method: 'GET',
config: {
handler: async (request) => {
handler: async request => {
const { callWithRequest } = request.server.plugins.elasticsearch.getCluster('data');
const sampleDatasets = request.server.getSampleDatasets().map(sampleDataset => {
@ -39,10 +39,9 @@ export const createListRoute = () => ({
previewImagePath: sampleDataset.previewImagePath,
darkPreviewImagePath: sampleDataset.darkPreviewImagePath,
overviewDashboard: sampleDataset.overviewDashboard,
appLinks: sampleDataset.appLinks,
defaultIndex: sampleDataset.defaultIndex,
dataIndices: sampleDataset.dataIndices.map(dataIndexConfig => {
return { id: dataIndexConfig.id };
}),
dataIndices: sampleDataset.dataIndices.map(({ id }) => ({ id })),
};
});
@ -88,6 +87,6 @@ export const createListRoute = () => ({
await Promise.all(isInstalledPromises);
return sampleDatasets;
}
}
},
},
});

View file

@ -19,7 +19,6 @@
import _ from 'lodash';
import Joi from 'joi';
import { createIndexName } from './lib/create_index_name';
export const createUninstallRoute = () => ({
@ -27,15 +26,15 @@ export const createUninstallRoute = () => ({
method: 'DELETE',
config: {
validate: {
params: Joi.object().keys({
id: Joi.string().required(),
}).required()
params: Joi.object()
.keys({
id: Joi.string().required(),
})
.required(),
},
handler: async (request, h) => {
const server = request.server;
const sampleDataset = server.getSampleDatasets().find(({ id }) => {
return id === request.params.id;
});
const { server, params } = request;
const sampleDataset = server.getSampleDatasets().find(({ id }) => id === params.id);
if (!sampleDataset) {
return h.response().code(404);
@ -50,23 +49,28 @@ export const createUninstallRoute = () => ({
try {
await callWithRequest(request, 'indices.delete', { index: index });
} catch (err) {
return h.response(`Unable to delete sample data index "${index}", error: ${err.message}`).code(err.status);
return h
.response(`Unable to delete sample data index "${index}", error: ${err.message}`)
.code(err.status);
}
}
const deletePromises = sampleDataset.savedObjects.map((savedObjectJson) => {
return request.getSavedObjectsClient().delete(savedObjectJson.type, savedObjectJson.id);
});
const deletePromises = sampleDataset.savedObjects.map(({ type, id }) =>
request.getSavedObjectsClient().delete(type, id)
);
try {
await Promise.all(deletePromises);
} catch (err) {
// ignore 404s since users could have deleted some of the saved objects via the UI
if (_.get(err, 'output.statusCode') !== 404) {
return h.response(`Unable to delete sample dataset saved objects, error: ${err.message}`).code(403);
return h
.response(`Unable to delete sample dataset saved objects, error: ${err.message}`)
.code(403);
}
}
return {};
}
}
},
},
});

View file

@ -79,6 +79,18 @@ export function sampleDataMixin(kbnServer, server) {
sampleDataset.savedObjects = sampleDataset.savedObjects.concat(savedObjects);
});
server.decorate('server', 'addAppLinksToSampleDataset', (id, appLinks) => {
const sampleDataset = sampleDatasets.find(sampleDataset => {
return sampleDataset.id === id;
});
if (!sampleDataset) {
throw new Error(`Unable to find sample dataset with id: ${id}`);
}
sampleDataset.appLinks = sampleDataset.appLinks.concat(appLinks);
});
server.registerSampleDataset(flightsSpecProvider);
server.registerSampleDataset(logsSpecProvider);
server.registerSampleDataset(ecommerceSpecProvider);

View file

@ -20,7 +20,11 @@
import { KibanaMigrator } from './migrations';
import { SavedObjectsSchema } from './schema';
import { SavedObjectsSerializer } from './serialization';
import { SavedObjectsClient, SavedObjectsRepository, ScopedSavedObjectsClientProvider } from './service';
import {
SavedObjectsClient,
SavedObjectsRepository,
ScopedSavedObjectsClientProvider,
} from './service';
import { getRootPropertiesObjects } from '../mappings';
import {
@ -41,7 +45,7 @@ export function savedObjectsMixin(kbnServer, server) {
server.decorate('server', 'kibanaMigrator', migrator);
const warn = (message) => server.log(['warning', 'saved-objects'], message);
const warn = message => server.log(['warning', 'saved-objects'], message);
// we use kibana.index which is technically defined in the kibana plugin, so if
// we don't have the plugin (mainly tests) we can't initialize the saved objects
if (!kbnServer.pluginSpecs.some(p => p.getId() === 'kibana')) {
@ -75,12 +79,12 @@ export function savedObjectsMixin(kbnServer, server) {
const visibleTypes = allTypes.filter(type => !schema.isHiddenType(type));
const createRepository = (callCluster, extraTypes = []) => {
if(typeof callCluster !== 'function') {
if (typeof callCluster !== 'function') {
throw new TypeError('Repository requires a "callCluster" function to be provided.');
}
// throw an exception if an extraType is not defined.
extraTypes.forEach(type => {
if(!allTypes.includes(type)) {
if (!allTypes.includes(type)) {
throw new Error(`Missing mappings for saved objects type '${type}'`);
}
});
@ -94,7 +98,7 @@ export function savedObjectsMixin(kbnServer, server) {
schema,
serializer,
allowedTypes,
callCluster
callCluster,
});
};
@ -123,7 +127,7 @@ export function savedObjectsMixin(kbnServer, server) {
server.decorate('server', 'savedObjects', service);
const savedObjectsClientCache = new WeakMap();
server.decorate('request', 'getSavedObjectsClient', function () {
server.decorate('request', 'getSavedObjectsClient', function() {
const request = this;
if (savedObjectsClientCache.has(request)) {

View file

@ -28,7 +28,7 @@ describe('Saved Objects Mixin', () => {
'kibana.index': 'kibana.index',
'savedObjects.maxImportExportSize': 10000,
};
const stubConfig = jest.fn((key) => {
const stubConfig = jest.fn(key => {
return config[key];
});
@ -57,12 +57,16 @@ describe('Saved Objects Mixin', () => {
mockKbnServer = {
server: mockServer,
ready: () => {},
pluginSpecs: { some: () => { return true; } },
pluginSpecs: {
some: () => {
return true;
},
},
uiExports: {
savedObjectSchemas: {
hiddentype: {
hidden: true,
}
},
},
savedObjectMappings: [
{
@ -95,7 +99,11 @@ describe('Saved Objects Mixin', () => {
mockKbnServer.pluginSpecs.some = () => false;
savedObjectsMixin(mockKbnServer, mockServer);
expect(mockServer.log).toHaveBeenCalledWith(expect.any(Array), expect.any(String));
expect(mockServer.decorate).toHaveBeenCalledWith('server', 'kibanaMigrator', expect.any(Object));
expect(mockServer.decorate).toHaveBeenCalledWith(
'server',
'kibanaMigrator',
expect.any(Object)
);
expect(mockServer.decorate).toHaveBeenCalledTimes(1);
expect(mockServer.route).not.toHaveBeenCalled();
});
@ -108,44 +116,66 @@ describe('Saved Objects Mixin', () => {
});
it('should add POST /api/saved_objects/_bulk_create', () => {
savedObjectsMixin(mockKbnServer, mockServer);
expect(mockServer.route).toHaveBeenCalledWith(expect.objectContaining({ path: '/api/saved_objects/_bulk_create', method: 'POST' }));
expect(mockServer.route).toHaveBeenCalledWith(
expect.objectContaining({ path: '/api/saved_objects/_bulk_create', method: 'POST' })
);
});
it('should add POST /api/saved_objects/_bulk_get', () => {
savedObjectsMixin(mockKbnServer, mockServer);
expect(mockServer.route).toHaveBeenCalledWith(expect.objectContaining({ path: '/api/saved_objects/_bulk_get', method: 'POST' }));
expect(mockServer.route).toHaveBeenCalledWith(
expect.objectContaining({ path: '/api/saved_objects/_bulk_get', method: 'POST' })
);
});
it('should add POST /api/saved_objects/{type}/{id?}', () => {
savedObjectsMixin(mockKbnServer, mockServer);
expect(mockServer.route).toHaveBeenCalledWith(expect.objectContaining({ path: '/api/saved_objects/{type}/{id?}', method: 'POST' }));
expect(mockServer.route).toHaveBeenCalledWith(
expect.objectContaining({ path: '/api/saved_objects/{type}/{id?}', method: 'POST' })
);
});
it('should add DELETE /api/saved_objects/{type}/{id}', () => {
savedObjectsMixin(mockKbnServer, mockServer);
expect(mockServer.route).toHaveBeenCalledWith(expect.objectContaining({ path: '/api/saved_objects/{type}/{id}', method: 'DELETE' }));
expect(mockServer.route).toHaveBeenCalledWith(
expect.objectContaining({ path: '/api/saved_objects/{type}/{id}', method: 'DELETE' })
);
});
it('should add GET /api/saved_objects/_find', () => {
savedObjectsMixin(mockKbnServer, mockServer);
expect(mockServer.route).toHaveBeenCalledWith(expect.objectContaining({ path: '/api/saved_objects/_find', method: 'GET' }));
expect(mockServer.route).toHaveBeenCalledWith(
expect.objectContaining({ path: '/api/saved_objects/_find', method: 'GET' })
);
});
it('should add GET /api/saved_objects/{type}/{id}', () => {
savedObjectsMixin(mockKbnServer, mockServer);
expect(mockServer.route).toHaveBeenCalledWith(expect.objectContaining({ path: '/api/saved_objects/{type}/{id}', method: 'GET' }));
expect(mockServer.route).toHaveBeenCalledWith(
expect.objectContaining({ path: '/api/saved_objects/{type}/{id}', method: 'GET' })
);
});
it('should add PUT /api/saved_objects/{type}/{id}', () => {
savedObjectsMixin(mockKbnServer, mockServer);
expect(mockServer.route).toHaveBeenCalledWith(expect.objectContaining({ path: '/api/saved_objects/{type}/{id}', method: 'PUT' }));
expect(mockServer.route).toHaveBeenCalledWith(
expect.objectContaining({ path: '/api/saved_objects/{type}/{id}', method: 'PUT' })
);
});
it('should add GET /api/saved_objects/_export', () => {
savedObjectsMixin(mockKbnServer, mockServer);
expect(mockServer.route).toHaveBeenCalledWith(expect.objectContaining({ path: '/api/saved_objects/_export', method: 'POST' }));
expect(mockServer.route).toHaveBeenCalledWith(
expect.objectContaining({ path: '/api/saved_objects/_export', method: 'POST' })
);
});
it('should add POST /api/saved_objects/_import', () => {
savedObjectsMixin(mockKbnServer, mockServer);
expect(mockServer.route).toHaveBeenCalledWith(expect.objectContaining({ path: '/api/saved_objects/_import', method: 'POST' }));
expect(mockServer.route).toHaveBeenCalledWith(
expect.objectContaining({ path: '/api/saved_objects/_import', method: 'POST' })
);
});
it('should add POST /api/saved_objects/_resolve_import_errors', () => {
savedObjectsMixin(mockKbnServer, mockServer);
expect(mockServer.route)
.toHaveBeenCalledWith(expect.objectContaining({ path: '/api/saved_objects/_resolve_import_errors', method: 'POST' }));
expect(mockServer.route).toHaveBeenCalledWith(
expect.objectContaining({
path: '/api/saved_objects/_resolve_import_errors',
method: 'POST',
})
);
});
});
@ -154,7 +184,9 @@ describe('Saved Objects Mixin', () => {
beforeEach(() => {
savedObjectsMixin(mockKbnServer, mockServer);
const call = mockServer.decorate.mock.calls.filter(([objName, methodName]) => objName === 'server' && methodName === 'savedObjects');
const call = mockServer.decorate.mock.calls.filter(
([objName, methodName]) => objName === 'server' && methodName === 'savedObjects'
);
service = call[0][2];
});
@ -168,7 +200,7 @@ describe('Saved Objects Mixin', () => {
it('should not allow a repository with an undefined type', () => {
expect(() => {
service.getSavedObjectsRepository(mockCallEs, ['extraType']);
}).toThrow(new Error('Missing mappings for saved objects type \'extraType\''));
}).toThrow(new Error("Missing mappings for saved objects type 'extraType'"));
});
it('should create a repository without hidden types', () => {
@ -178,12 +210,19 @@ describe('Saved Objects Mixin', () => {
});
it('should create a repository with a unique list of allowed types', () => {
const repository = service.getSavedObjectsRepository(mockCallEs, ['config', 'config', 'config']);
const repository = service.getSavedObjectsRepository(mockCallEs, [
'config',
'config',
'config',
]);
expect(repository._allowedTypes).toEqual(['config', 'testtype']);
});
it('should create a repository with extraTypes minus duplicate', () => {
const repository = service.getSavedObjectsRepository(mockCallEs, ['hiddentype', 'hiddentype']);
const repository = service.getSavedObjectsRepository(mockCallEs, [
'hiddentype',
'hiddentype',
]);
expect(repository._allowedTypes).toEqual(['config', 'testtype', 'hiddentype']);
});
@ -221,7 +260,7 @@ describe('Saved Objects Mixin', () => {
});
it('should call underlining callCluster', async () => {
stubCallCluster.mockImplementation((method) => {
stubCallCluster.mockImplementation(method => {
if (method === 'indices.get') {
return { status: 404 };
} else if (method === 'indices.getAlias') {

View file

@ -30,7 +30,7 @@ const {
403: Forbidden,
413: RequestEntityTooLarge,
NotFound,
BadRequest
BadRequest,
} = elasticsearch.errors;
import {

View file

@ -65,7 +65,6 @@ export function isInvalidVersionError(error) {
return error && error[code] === CODE_INVALID_VERSION;
}
// 401 - Not Authorized
const CODE_NOT_AUTHORIZED = 'SavedObjectsClient/notAuthorized';
export function decorateNotAuthorizedError(error, reason) {
@ -75,7 +74,6 @@ export function isNotAuthorizedError(error) {
return error && error[code] === CODE_NOT_AUTHORIZED;
}
// 403 - Forbidden
const CODE_FORBIDDEN = 'SavedObjectsClient/forbidden';
export function decorateForbiddenError(error, reason) {
@ -85,7 +83,6 @@ export function isForbiddenError(error) {
return error && error[code] === CODE_FORBIDDEN;
}
// 413 - Request Entity Too Large
const CODE_REQUEST_ENTITY_TOO_LARGE = 'SavedObjectsClient/requestEntityTooLarge';
export function decorateRequestEntityTooLargeError(error, reason) {
@ -95,7 +92,6 @@ export function isRequestEntityTooLargeError(error) {
return error && error[code] === CODE_REQUEST_ENTITY_TOO_LARGE;
}
// 404 - Not Found
const CODE_NOT_FOUND = 'SavedObjectsClient/notFound';
export function createGenericNotFoundError(type = null, id = null) {
@ -108,7 +104,6 @@ export function isNotFoundError(error) {
return error && error[code] === CODE_NOT_FOUND;
}
// 409 - Conflict
const CODE_CONFLICT = 'SavedObjectsClient/conflict';
export function decorateConflictError(error, reason) {
@ -118,7 +113,6 @@ export function isConflictError(error) {
return error && error[code] === CODE_CONFLICT;
}
// 503 - Es Unavailable
const CODE_ES_UNAVAILABLE = 'SavedObjectsClient/esUnavailable';
export function decorateEsUnavailableError(error, reason) {
@ -128,7 +122,6 @@ export function isEsUnavailableError(error) {
return error && error[code] === CODE_ES_UNAVAILABLE;
}
// 503 - Unable to automatically create index because of action.auto_create_index setting
const CODE_ES_AUTO_CREATE_INDEX_ERROR = 'SavedObjectsClient/autoCreateIndex';
export function createEsAutoCreateIndexError() {
@ -141,7 +134,6 @@ export function isEsAutoCreateIndexError(error) {
return error && error[code] === CODE_ES_AUTO_CREATE_INDEX_ERROR;
}
// 500 - General Error
const CODE_GENERAL_ERROR = 'SavedObjectsClient/generalError';
export function decorateGeneralError(error, reason) {

View file

@ -45,18 +45,21 @@ describe('savedObjectsClient/errorTypes', () => {
const errorObj = createUnsupportedTypeError('someType');
it('should have the unsupported type message', () => {
expect(errorObj).toHaveProperty('message', 'Unsupported saved object type: \'someType\': Bad Request');
expect(errorObj).toHaveProperty(
'message',
"Unsupported saved object type: 'someType': Bad Request"
);
});
it('has boom properties', () => {
expect(errorObj.output.payload).toMatchObject({
statusCode: 400,
message: 'Unsupported saved object type: \'someType\': Bad Request',
message: "Unsupported saved object type: 'someType': Bad Request",
error: 'Bad Request',
});
});
it('should be identified by \'isBadRequestError\' method', () => {
it("should be identified by 'isBadRequestError' method", () => {
expect(isBadRequestError(errorObj)).toBeTruthy();
});
});
@ -67,7 +70,7 @@ describe('savedObjectsClient/errorTypes', () => {
expect(errorObj.message).toEqual('test reason message: Bad Request');
});
it('should be identified by \'isBadRequestError\' method', () => {
it("should be identified by 'isBadRequestError' method", () => {
expect(isBadRequestError(errorObj)).toBeTruthy();
});

View file

@ -31,7 +31,8 @@ export function includedFields(type, fields) {
const sourceFields = typeof fields === 'string' ? [fields] : fields;
const sourceType = type || '*';
return sourceFields.map(f => `${sourceType}.${f}`)
return sourceFields
.map(f => `${sourceType}.${f}`)
.concat('namespace')
.concat('type')
.concat(fields); // v5 compatibility

View file

@ -38,7 +38,7 @@ export class SavedObjectsRepository {
serializer,
migrator,
allowedTypes = [],
onBeforeWrite = () => { },
onBeforeWrite = () => {},
} = options;
// It's important that we migrate documents / mark them as up-to-date
@ -52,7 +52,7 @@ export class SavedObjectsRepository {
this._index = index;
this._mappings = mappings;
this._schema = schema;
if(allowedTypes.length === 0) {
if (allowedTypes.length === 0) {
throw new Error('Empty or missing types for saved object repository!');
}
this._allowedTypes = allowedTypes;
@ -78,17 +78,11 @@ export class SavedObjectsRepository {
* @property {string} [options.namespace]
* @property {array} [options.references] - [{ name, type, id }]
* @returns {promise} - { id, type, version, attributes }
*/
*/
async create(type, attributes = {}, options = {}) {
const {
id,
migrationVersion,
overwrite = false,
namespace,
references = [],
} = options;
const { id, migrationVersion, overwrite = false, namespace, references = [] } = options;
if(!this._isTypeAllowed(type)) {
if (!this._isTypeAllowed(type)) {
throw errors.createUnsupportedTypeError(type);
}
@ -139,22 +133,19 @@ export class SavedObjectsRepository {
* @returns {promise} - {saved_objects: [[{ id, type, version, references, attributes, error: { message } }]}
*/
async bulkCreate(objects, options = {}) {
const {
namespace,
overwrite = false,
} = options;
const { namespace, overwrite = false } = options;
const time = this._getCurrentTime();
const bulkCreateParams = [];
let requestIndexCounter = 0;
const expectedResults = objects.map((object) => {
if(!this._isTypeAllowed(object.type)) {
const expectedResults = objects.map(object => {
if (!this._isTypeAllowed(object.type)) {
return {
response: {
id: object.id,
type: object.type,
error: errors.createUnsupportedTypeError(object.type).output.payload,
}
},
};
}
@ -181,7 +172,7 @@ export class SavedObjectsRepository {
_id: expectedResult.rawMigratedDoc._id,
},
},
expectedResult.rawMigratedDoc._source,
expectedResult.rawMigratedDoc._source
);
return expectedResult;
@ -195,7 +186,7 @@ export class SavedObjectsRepository {
return {
saved_objects: expectedResults.map(expectedResult => {
if(expectedResult.response) {
if (expectedResult.response) {
return expectedResult.response;
}
@ -209,11 +200,7 @@ export class SavedObjectsRepository {
} = Object.values(response)[0];
const {
_source: {
type,
[type]: attributes,
references = [],
},
_source: { type, [type]: attributes, references = [] },
} = rawMigratedDoc;
const id = requestedId || responseId;
@ -222,15 +209,15 @@ export class SavedObjectsRepository {
return {
id,
type,
error: { statusCode: 409, message: 'version conflict, document already exists' }
error: { statusCode: 409, message: 'version conflict, document already exists' },
};
}
return {
id,
type,
error: {
message: error.reason || JSON.stringify(error)
}
message: error.reason || JSON.stringify(error),
},
};
}
@ -256,13 +243,11 @@ export class SavedObjectsRepository {
* @returns {promise}
*/
async delete(type, id, options = {}) {
if(!this._isTypeAllowed(type)) {
if (!this._isTypeAllowed(type)) {
throw errors.createGenericNotFoundError();
}
const {
namespace
} = options;
const { namespace } = options;
const response = await this._writeToCluster('delete', {
id: this._serializer.generateRawId(namespace, type, id),
@ -284,7 +269,7 @@ export class SavedObjectsRepository {
}
throw new Error(
`Unexpected Elasticsearch DELETE response: ${JSON.stringify({ type, id, response, })}`
`Unexpected Elasticsearch DELETE response: ${JSON.stringify({ type, id, response })}`
);
}
@ -312,8 +297,8 @@ export class SavedObjectsRepository {
...getSearchDsl(this._mappings, this._schema, {
namespace,
type: typesToDelete,
})
}
}),
},
};
return await this._writeToCluster('deleteByQuery', esOptions);
@ -354,23 +339,23 @@ export class SavedObjectsRepository {
throw new TypeError(`options.type must be a string or an array of strings`);
}
if(Array.isArray(type)) {
if (Array.isArray(type)) {
type = type.filter(type => this._isTypeAllowed(type));
if(type.length === 0) {
if (type.length === 0) {
return {
page,
per_page: perPage,
total: 0,
saved_objects: []
saved_objects: [],
};
}
}else{
if(!this._isTypeAllowed(type)) {
} else {
if (!this._isTypeAllowed(type)) {
return {
page,
per_page: perPage,
total: 0,
saved_objects: []
saved_objects: [],
};
}
}
@ -401,8 +386,8 @@ export class SavedObjectsRepository {
sortOrder,
namespace,
hasReference,
})
}
}),
},
};
const response = await this._callCluster('search', esOptions);
@ -414,7 +399,7 @@ export class SavedObjectsRepository {
page,
per_page: perPage,
total: 0,
saved_objects: []
saved_objects: [],
};
}
@ -441,9 +426,7 @@ export class SavedObjectsRepository {
* ])
*/
async bulkGet(objects = [], options = {}) {
const {
namespace
} = options;
const { namespace } = options;
if (objects.length === 0) {
return { saved_objects: [] };
@ -454,41 +437,47 @@ export class SavedObjectsRepository {
index: this._index,
body: {
docs: objects.reduce((acc, { type, id }) => {
if(this._isTypeAllowed(type)) {
if (this._isTypeAllowed(type)) {
acc.push({
_id: this._serializer.generateRawId(namespace, type, id),
});
}else{
unsupportedTypes.push({ id, type, error: errors.createUnsupportedTypeError(type).output.payload });
} else {
unsupportedTypes.push({
id,
type,
error: errors.createUnsupportedTypeError(type).output.payload,
});
}
return acc;
}, [])
}
}, []),
},
});
return {
saved_objects: response.docs.map((doc, i) => {
const { id, type } = objects[i];
saved_objects: response.docs
.map((doc, i) => {
const { id, type } = objects[i];
if (!doc.found) {
if (!doc.found) {
return {
id,
type,
error: { statusCode: 404, message: 'Not found' },
};
}
const time = doc._source.updated_at;
return {
id,
type,
error: { statusCode: 404, message: 'Not found' }
...(time && { updated_at: time }),
version: encodeHitVersion(doc),
attributes: doc._source[type],
references: doc._source.references || [],
migrationVersion: doc._source.migrationVersion,
};
}
const time = doc._source.updated_at;
return {
id,
type,
...time && { updated_at: time },
version: encodeHitVersion(doc),
attributes: doc._source[type],
references: doc._source.references || [],
migrationVersion: doc._source.migrationVersion,
};
}).concat(unsupportedTypes)
})
.concat(unsupportedTypes),
};
}
@ -502,18 +491,16 @@ export class SavedObjectsRepository {
* @returns {promise} - { id, type, version, attributes }
*/
async get(type, id, options = {}) {
if(!this._isTypeAllowed(type)) {
if (!this._isTypeAllowed(type)) {
throw errors.createGenericNotFoundError(type, id);
}
const {
namespace
} = options;
const { namespace } = options;
const response = await this._callCluster('get', {
id: this._serializer.generateRawId(namespace, type, id),
index: this._index,
ignore: [404]
ignore: [404],
});
const docNotFound = response.found === false;
@ -528,7 +515,7 @@ export class SavedObjectsRepository {
return {
id,
type,
...updatedAt && { updated_at: updatedAt },
...(updatedAt && { updated_at: updatedAt }),
version: encodeHitVersion(response),
attributes: response._source[type],
references: response._source.references || [],
@ -548,15 +535,11 @@ export class SavedObjectsRepository {
* @returns {promise}
*/
async update(type, id, attributes, options = {}) {
if(!this._isTypeAllowed(type)) {
if (!this._isTypeAllowed(type)) {
throw errors.createGenericNotFoundError(type, id);
}
const {
version,
namespace,
references = [],
} = options;
const { version, namespace, references = [] } = options;
const time = this._getCurrentTime();
const response = await this._writeToCluster('update', {
@ -570,7 +553,7 @@ export class SavedObjectsRepository {
[type]: attributes,
updated_at: time,
references,
}
},
},
});
@ -585,7 +568,7 @@ export class SavedObjectsRepository {
updated_at: time,
version: encodeHitVersion(response),
references,
attributes
attributes,
};
}
@ -606,18 +589,14 @@ export class SavedObjectsRepository {
if (typeof counterFieldName !== 'string') {
throw new Error('"counterFieldName" argument must be a string');
}
if(!this._isTypeAllowed(type)) {
if (!this._isTypeAllowed(type)) {
throw errors.createUnsupportedTypeError(type);
}
const {
migrationVersion,
namespace,
} = options;
const { migrationVersion, namespace } = options;
const time = this._getCurrentTime();
const migrated = this._migrator.migrateDocument({
id,
type,
@ -664,8 +643,6 @@ export class SavedObjectsRepository {
version: encodeHitVersion(response),
attributes: response.get._source[type],
};
}
async _writeToCluster(method, params) {
@ -700,8 +677,8 @@ export class SavedObjectsRepository {
_isTypeAllowed(types) {
const toCheck = [].concat(types);
for(const type of toCheck) {
if(!this._allowedTypes.includes(type)) {
for (const type of toCheck) {
if (!this._allowedTypes.includes(type)) {
return false;
}
}

File diff suppressed because it is too large Load diff

View file

@ -22,12 +22,9 @@ import { PriorityCollection } from './priority_collection';
* Provider for the Scoped Saved Object Client.
*/
export class ScopedSavedObjectsClientProvider {
_wrapperFactories = new PriorityCollection();
constructor({
defaultClientFactory
}) {
constructor({ defaultClientFactory }) {
this._originalClientFactory = this._clientFactory = defaultClientFactory;
}

View file

@ -25,7 +25,7 @@ test(`uses default client factory when one isn't set`, () => {
const request = Symbol();
const clientProvider = new ScopedSavedObjectsClientProvider({
defaultClientFactory: defaultClientFactoryMock
defaultClientFactory: defaultClientFactoryMock,
});
const result = clientProvider.getClient(request);
@ -43,7 +43,7 @@ test(`uses custom client factory when one is set`, () => {
const customClientFactoryMock = jest.fn().mockReturnValue(returnValue);
const clientProvider = new ScopedSavedObjectsClientProvider({
defaultClientFactory: defaultClientFactoryMock
defaultClientFactory: defaultClientFactoryMock,
});
clientProvider.setClientFactory(customClientFactoryMock);
const result = clientProvider.getClient(request);
@ -58,9 +58,9 @@ test(`uses custom client factory when one is set`, () => {
test(`throws error when more than one scoped saved objects client factory is set`, () => {
const clientProvider = new ScopedSavedObjectsClientProvider({});
clientProvider.setClientFactory(() => { });
clientProvider.setClientFactory(() => {});
expect(() => {
clientProvider.setClientFactory(() => { });
clientProvider.setClientFactory(() => {});
}).toThrowErrorMatchingSnapshot();
});
@ -68,7 +68,7 @@ test(`invokes and uses wrappers in specified order`, () => {
const defaultClient = Symbol();
const defaultClientFactoryMock = jest.fn().mockReturnValue(defaultClient);
const clientProvider = new ScopedSavedObjectsClientProvider({
defaultClientFactory: defaultClientFactoryMock
defaultClientFactory: defaultClientFactoryMock,
});
const firstWrappedClient = Symbol('first client');
const firstClientWrapperFactoryMock = jest.fn().mockReturnValue(firstWrappedClient);
@ -83,10 +83,10 @@ test(`invokes and uses wrappers in specified order`, () => {
expect(actualClient).toBe(firstWrappedClient);
expect(firstClientWrapperFactoryMock).toHaveBeenCalledWith({
request,
client: secondWrapperClient
client: secondWrapperClient,
});
expect(secondClientWrapperFactoryMock).toHaveBeenCalledWith({
request,
client: defaultClient
client: defaultClient,
});
});

View file

@ -44,7 +44,6 @@ function getTypes(mappings, type) {
* @return {Object}
*/
function getFieldsForTypes(searchFields, types) {
if (!searchFields || !searchFields.length) {
return {
lenient: true,
@ -53,10 +52,10 @@ function getFieldsForTypes(searchFields, types) {
}
return {
fields: searchFields.reduce((acc, field) => [
...acc,
...types.map(prefix => `${prefix}.${field}`)
], []),
fields: searchFields.reduce(
(acc, field) => [...acc, ...types.map(prefix => `${prefix}.${field}`)],
[]
),
};
}
@ -76,19 +75,16 @@ function getClauseForType(schema, namespace, type) {
if (namespace && !schema.isNamespaceAgnostic(type)) {
return {
bool: {
must: [
{ term: { type } },
{ term: { namespace } },
]
}
must: [{ term: { type } }, { term: { namespace } }],
},
};
}
return {
bool: {
must: [{ term: { type } }],
must_not: [{ exists: { field: 'namespace' } }]
}
must_not: [{ exists: { field: 'namespace' } }],
},
};
}
@ -103,38 +99,51 @@ function getClauseForType(schema, namespace, type) {
* @param {Object} hasReference
* @return {Object}
*/
export function getQueryParams(mappings, schema, namespace, type, search, searchFields, defaultSearchOperator, hasReference) {
export function getQueryParams(
mappings,
schema,
namespace,
type,
search,
searchFields,
defaultSearchOperator,
hasReference
) {
const types = getTypes(mappings, type);
const bool = {
filter: [{
bool: {
must: hasReference
? [{
nested: {
path: 'references',
query: {
bool: {
must: [
{
term: {
'references.id': hasReference.id,
filter: [
{
bool: {
must: hasReference
? [
{
nested: {
path: 'references',
query: {
bool: {
must: [
{
term: {
'references.id': hasReference.id,
},
},
{
term: {
'references.type': hasReference.type,
},
},
],
},
},
{
term: {
'references.type': hasReference.type,
},
},
],
},
},
},
},
}]
: undefined,
should: types.map(type => getClauseForType(schema, namespace, type)),
minimum_should_match: 1
}
}],
]
: undefined,
should: types.map(type => getClauseForType(schema, namespace, type)),
minimum_should_match: 1,
},
},
],
};
if (search) {
@ -142,13 +151,10 @@ export function getQueryParams(mappings, schema, namespace, type, search, search
{
simple_query_string: {
query: search,
...getFieldsForTypes(
searchFields,
types
),
...getFieldsForTypes(searchFields, types),
...(defaultSearchOperator ? { default_operator: defaultSearchOperator } : {}),
}
}
},
},
];
}

View file

@ -43,7 +43,16 @@ export function getSearchDsl(mappings, schema, options = {}) {
}
return {
...getQueryParams(mappings, schema, namespace, type, search, searchFields, defaultSearchOperator, hasReference),
...getQueryParams(
mappings,
schema,
namespace,
type,
search,
searchFields,
defaultSearchOperator,
hasReference
),
...getSortingParams(mappings, type, sortField, sortOrder),
};
}

View file

@ -33,18 +33,26 @@ describe('getSearchDsl', () => {
describe('validation', () => {
it('throws when type is not specified', () => {
expect(() => {
getSearchDsl({}, {}, {
type: undefined,
sortField: 'title'
});
getSearchDsl(
{},
{},
{
type: undefined,
sortField: 'title',
}
);
}).toThrowError(/type must be specified/);
});
it('throws when sortOrder without sortField', () => {
expect(() => {
getSearchDsl({}, {}, {
type: 'foo',
sortOrder: 'desc'
});
getSearchDsl(
{},
{},
{
type: 'foo',
sortOrder: 'desc',
}
);
}).toThrowError(/sortOrder requires a sortField/);
});
});
@ -75,7 +83,7 @@ describe('getSearchDsl', () => {
opts.search,
opts.searchFields,
opts.defaultSearchOperator,
opts.hasReference,
opts.hasReference
);
});
@ -86,7 +94,7 @@ describe('getSearchDsl', () => {
const opts = {
type: 'foo',
sortField: 'bar',
sortOrder: 'baz'
sortOrder: 'baz',
};
getSearchDsl(mappings, schema, opts);
@ -95,7 +103,7 @@ describe('getSearchDsl', () => {
mappings,
opts.type,
opts.sortField,
opts.sortOrder,
opts.sortOrder
);
});

View file

@ -31,27 +31,33 @@ export function getSortingParams(mappings, type, sortField, sortOrder) {
if (TOP_LEVEL_FIELDS.includes(sortField)) {
return {
sort: [{
[sortField]: {
order: sortOrder,
sort: [
{
[sortField]: {
order: sortOrder,
},
},
}],
],
};
}
if (types.length > 1) {
const rootField = getProperty(mappings, sortField);
if (!rootField) {
throw Boom.badRequest(`Unable to sort multiple types by field ${sortField}, not a root property`);
throw Boom.badRequest(
`Unable to sort multiple types by field ${sortField}, not a root property`
);
}
return {
sort: [{
[sortField]: {
order: sortOrder,
unmapped_type: rootField.type
}
}]
sort: [
{
[sortField]: {
order: sortOrder,
unmapped_type: rootField.type,
},
},
],
};
}
@ -63,11 +69,13 @@ export function getSortingParams(mappings, type, sortField, sortOrder) {
}
return {
sort: [{
[key]: {
order: sortOrder,
unmapped_type: field.type
}
}]
sort: [
{
[key]: {
order: sortOrder,
unmapped_type: field.type,
},
},
],
};
}

View file

@ -25,9 +25,9 @@ const MAPPINGS = {
type: 'text',
fields: {
raw: {
type: 'keyword'
}
}
type: 'keyword',
},
},
},
pending: {
properties: {
@ -35,11 +35,11 @@ const MAPPINGS = {
type: 'text',
fields: {
raw: {
type: 'keyword'
}
}
}
}
type: 'keyword',
},
},
},
},
},
saved: {
properties: {
@ -47,128 +47,124 @@ const MAPPINGS = {
type: 'text',
fields: {
raw: {
type: 'keyword'
}
}
type: 'keyword',
},
},
},
obj: {
properties: {
key1: {
type: 'text'
}
}
}
}
}
}
type: 'text',
},
},
},
},
},
},
};
describe('searchDsl/getSortParams', () => {
describe('no sortField, type, or order', () => {
it('returns no params', () => {
expect(getSortingParams(MAPPINGS))
.toEqual({});
expect(getSortingParams(MAPPINGS)).toEqual({});
});
});
describe('type, no sortField', () => {
it('returns no params', () => {
expect(getSortingParams(MAPPINGS, 'pending'))
.toEqual({});
expect(getSortingParams(MAPPINGS, 'pending')).toEqual({});
});
});
describe('type, order, no sortField', () => {
it('returns no params', () => {
expect(getSortingParams(MAPPINGS, 'saved', null, 'desc'))
.toEqual({});
expect(getSortingParams(MAPPINGS, 'saved', null, 'desc')).toEqual({});
});
});
describe('sortField no direction', () => {
describe('sortField is simple property with single type', () => {
it('returns correct params', () => {
expect(getSortingParams(MAPPINGS, 'saved', 'title'))
.toEqual({
sort: [
{
'saved.title': {
order: undefined,
unmapped_type: 'text'
}
}
]
});
expect(getSortingParams(MAPPINGS, 'saved', 'title')).toEqual({
sort: [
{
'saved.title': {
order: undefined,
unmapped_type: 'text',
},
},
],
});
});
});
describe('sortField is simple root property with multiple types', () => {
it('returns correct params', () => {
expect(getSortingParams(MAPPINGS, ['saved', 'pending'], 'type'))
.toEqual({
sort: [
{
'type': {
order: undefined,
unmapped_type: 'text'
}
}
]
});
expect(getSortingParams(MAPPINGS, ['saved', 'pending'], 'type')).toEqual({
sort: [
{
type: {
order: undefined,
unmapped_type: 'text',
},
},
],
});
});
});
describe('sortField is simple non-root property with multiple types', () => {
it('returns correct params', () => {
expect(() => getSortingParams(MAPPINGS, ['saved', 'pending'], 'title')).toThrowErrorMatchingSnapshot();
expect(() =>
getSortingParams(MAPPINGS, ['saved', 'pending'], 'title')
).toThrowErrorMatchingSnapshot();
});
});
describe('sortField is multi-field with single type', () => {
it('returns correct params', () => {
expect(getSortingParams(MAPPINGS, 'saved', 'title.raw'))
.toEqual({
sort: [
{
'saved.title.raw': {
order: undefined,
unmapped_type: 'keyword'
}
}
]
});
expect(getSortingParams(MAPPINGS, 'saved', 'title.raw')).toEqual({
sort: [
{
'saved.title.raw': {
order: undefined,
unmapped_type: 'keyword',
},
},
],
});
});
});
describe('sortField is multi-field with single type as array', () => {
it('returns correct params', () => {
expect(getSortingParams(MAPPINGS, ['saved'], 'title.raw'))
.toEqual({
sort: [
{
'saved.title.raw': {
order: undefined,
unmapped_type: 'keyword'
}
}
]
});
expect(getSortingParams(MAPPINGS, ['saved'], 'title.raw')).toEqual({
sort: [
{
'saved.title.raw': {
order: undefined,
unmapped_type: 'keyword',
},
},
],
});
});
});
describe('sortField is root multi-field with multiple types', () => {
it('returns correct params', () => {
expect(getSortingParams(MAPPINGS, ['saved', 'pending'], 'type.raw'))
.toEqual({
sort: [
{
'type.raw': {
order: undefined,
unmapped_type: 'keyword'
}
}
]
});
expect(getSortingParams(MAPPINGS, ['saved', 'pending'], 'type.raw')).toEqual({
sort: [
{
'type.raw': {
order: undefined,
unmapped_type: 'keyword',
},
},
],
});
});
});
describe('sortField is not-root multi-field with multiple types', () => {
it('returns correct params', () => {
expect(() => getSortingParams(MAPPINGS, ['saved', 'pending'], 'title.raw')).toThrowErrorMatchingSnapshot();
expect(() =>
getSortingParams(MAPPINGS, ['saved', 'pending'], 'title.raw')
).toThrowErrorMatchingSnapshot();
});
});
});
@ -176,72 +172,72 @@ describe('searchDsl/getSortParams', () => {
describe('sort with direction', () => {
describe('sortField is simple property with single type', () => {
it('returns correct params', () => {
expect(getSortingParams(MAPPINGS, 'saved', 'title', 'desc'))
.toEqual({
sort: [
{
'saved.title': {
order: 'desc',
unmapped_type: 'text'
}
}
]
});
expect(getSortingParams(MAPPINGS, 'saved', 'title', 'desc')).toEqual({
sort: [
{
'saved.title': {
order: 'desc',
unmapped_type: 'text',
},
},
],
});
});
});
describe('sortField is root simple property with multiple type', () => {
it('returns correct params', () => {
expect(getSortingParams(MAPPINGS, ['saved', 'pending'], 'type', 'desc'))
.toEqual({
sort: [
{
'type': {
order: 'desc',
unmapped_type: 'text'
}
}
]
});
expect(getSortingParams(MAPPINGS, ['saved', 'pending'], 'type', 'desc')).toEqual({
sort: [
{
type: {
order: 'desc',
unmapped_type: 'text',
},
},
],
});
});
});
describe('sortFields is non-root simple property with multiple types', () => {
it('returns correct params', () => {
expect(() => getSortingParams(MAPPINGS, ['saved', 'pending'], 'title', 'desc')).toThrowErrorMatchingSnapshot();
expect(() =>
getSortingParams(MAPPINGS, ['saved', 'pending'], 'title', 'desc')
).toThrowErrorMatchingSnapshot();
});
});
describe('sortField is multi-field with single type', () => {
it('returns correct params', () => {
expect(getSortingParams(MAPPINGS, 'saved', 'title.raw', 'asc'))
.toEqual({
sort: [
{
'saved.title.raw': {
order: 'asc',
unmapped_type: 'keyword'
}
}
]
});
expect(getSortingParams(MAPPINGS, 'saved', 'title.raw', 'asc')).toEqual({
sort: [
{
'saved.title.raw': {
order: 'asc',
unmapped_type: 'keyword',
},
},
],
});
});
});
describe('sortField is root multi-field with multiple types', () => {
it('returns correct params', () => {
expect(getSortingParams(MAPPINGS, ['saved', 'pending'], 'type.raw', 'asc'))
.toEqual({
sort: [
{
'type.raw': {
order: 'asc',
unmapped_type: 'keyword'
}
}
]
});
expect(getSortingParams(MAPPINGS, ['saved', 'pending'], 'type.raw', 'asc')).toEqual({
sort: [
{
'type.raw': {
order: 'asc',
unmapped_type: 'keyword',
},
},
],
});
});
});
describe('sortField is non-root multi-field with multiple types', () => {
it('returns correct params', () => {
expect(() => getSortingParams(MAPPINGS, ['saved', 'pending'], 'title.raw', 'asc')).toThrowErrorMatchingSnapshot();
expect(() =>
getSortingParams(MAPPINGS, ['saved', 'pending'], 'title.raw', 'asc')
).toThrowErrorMatchingSnapshot();
});
});
});

View file

@ -17,9 +17,7 @@
* under the License.
*/
import {
errors,
} from './lib';
import { errors } from './lib';
export class SavedObjectsClient {
constructor(repository) {
@ -91,8 +89,8 @@ export class SavedObjectsClient {
*
* @type {ErrorHelpers} see ./lib/errors
*/
static errors = errors
errors = errors
static errors = errors;
errors = errors;
/**
* Persists an object
@ -106,7 +104,7 @@ export class SavedObjectsClient {
* @property {string} [options.namespace]
* @property {array} [options.references] - [{ name, type, id }]
* @returns {promise} - { id, type, version, attributes }
*/
*/
async create(type, attributes = {}, options = {}) {
return this._repository.create(type, attributes, options);
}

View file

@ -108,10 +108,22 @@ input[type="checkbox"],
font-size: $euiFontSizeS;
}
// EUITODO: Move to EUI to ensure this doesn't happen elsewhere
// We apply brute force focus states to anything not coming from Eui
// which has focus states designed at the component level.
// You can also use "kbn-resetFocusState" to not apply the default focus
// state. This is useful when you've already hand crafted your own
// focus states in Kibana.
:focus {
&:not([class^="eui"]):not([class^="kbn-resetFocusState"]) {
@include euiFocusRing;
}
}
// A neccessary hack so that the above focus policy doesn't polute some EUI
// entrenched inputs.
.euiComboBox {
input:focus {
box-shadow: none;
animation: none !important;
}
}

View file

@ -170,7 +170,7 @@ export const getSchemas = (vis: Vis, timeRange?: any): Schemas => {
}
if (schemaName === 'split') {
schemaName = `split_${agg.params.row ? 'row' : 'column'}`;
skipMetrics = true;
skipMetrics = responseAggs.length - metrics.length > 1;
}
if (!schemas[schemaName]) {
schemas[schemaName] = [];

View file

@ -62,7 +62,6 @@ module.exports = function (grunt) {
'--server.port=5610',
];
const esFrom = process.env.TEST_ES_FROM || 'source';
return {
// used by the test and jenkins:unit tasks
// runs the eslint script to check for linting errors
@ -192,7 +191,6 @@ module.exports = function (grunt) {
args: [
'scripts/functional_tests',
'--config', 'test/api_integration/config.js',
'--esFrom', esFrom,
'--bail',
'--debug',
],
@ -204,7 +202,6 @@ module.exports = function (grunt) {
'scripts/functional_tests',
'--config', 'test/server_integration/http/ssl/config.js',
'--config', 'test/server_integration/http/ssl_redirect/config.js',
'--esFrom', esFrom,
'--bail',
'--debug',
'--kibana-install-dir', KIBANA_INSTALL_DIR,
@ -216,7 +213,6 @@ module.exports = function (grunt) {
args: [
'scripts/functional_tests',
'--config', 'test/plugin_functional/config.js',
'--esFrom', esFrom,
'--bail',
'--debug',
'--kibana-install-dir', KIBANA_INSTALL_DIR,
@ -228,14 +224,12 @@ module.exports = function (grunt) {
args: [
'scripts/functional_tests',
'--config', 'test/functional/config.js',
'--esFrom', esFrom,
'--bail',
'--debug',
],
},
...getFunctionalTestGroupRunConfigs({
esFrom,
kibanaInstallDir: KIBANA_INSTALL_DIR
})
};

View file

@ -30,7 +30,7 @@ const TEST_TAGS = safeLoad(JOBS_YAML)
.filter(id => id.startsWith('kibana-ciGroup'))
.map(id => id.replace(/^kibana-/, ''));
export function getFunctionalTestGroupRunConfigs({ esFrom, kibanaInstallDir } = {}) {
export function getFunctionalTestGroupRunConfigs({ kibanaInstallDir } = {}) {
return {
// include a run task for each test group
...TEST_TAGS.reduce((acc, tag) => ({
@ -41,7 +41,6 @@ export function getFunctionalTestGroupRunConfigs({ esFrom, kibanaInstallDir } =
'scripts/functional_tests',
'--include-tag', tag,
'--config', 'test/functional/config.js',
'--esFrom', esFrom,
'--bail',
'--debug',
'--kibana-install-dir', kibanaInstallDir,

View file

@ -47,6 +47,18 @@ export function TimePickerPageProvider({ getService, getPageObjects }) {
await find.waitForElementStale(panelElement);
}
async setAbsoluteStart(startTime) {
await this.showStartEndTimes();
await testSubjects.click('superDatePickerstartDatePopoverButton');
const panel = await this.getTimePickerPanel();
await testSubjects.click('superDatePickerAbsoluteTab');
await testSubjects.setValue('superDatePickerAbsoluteDateInput', startTime);
await testSubjects.click('superDatePickerstartDatePopoverButton');
await this.waitPanelIsGone(panel);
await PageObjects.header.awaitGlobalLoadingIndicatorHidden();
}
/**
* @param {String} fromTime YYYY-MM-DD HH:mm:ss.SSS
* @param {String} fromTime YYYY-MM-DD HH:mm:ss.SSS
@ -110,6 +122,8 @@ export function TimePickerPageProvider({ getService, getPageObjects }) {
}
async showStartEndTimes() {
// This first await makes sure the superDatePicker has loaded before we check for the ShowDatesButton
await testSubjects.exists('superDatePickerToggleQuickMenuButton', { timeout: 20000 });
const isShowDatesButton = await testSubjects.exists('superDatePickerShowDatesButton');
if (isShowDatesButton) {
await testSubjects.click('superDatePickerShowDatesButton');

View file

@ -13,17 +13,14 @@ function report {
trap report EXIT
source src/dev/ci_setup/checkout_sibling_es.sh
"$(FORCE_COLOR=0 yarn bin)/grunt" functionalTests:ensureAllTestsInCiGroup;
node scripts/build --debug --oss;
export TEST_BROWSER_HEADLESS=1
export TEST_ES_FROM=${TEST_ES_FROM:-source}
"$(FORCE_COLOR=0 yarn bin)/grunt" "run:functionalTests_ciGroup${CI_GROUP}" --from=source;
"$(FORCE_COLOR=0 yarn bin)/grunt" "run:functionalTests_ciGroup${CI_GROUP}";
if [ "$CI_GROUP" == "1" ]; then
"$(FORCE_COLOR=0 yarn bin)/grunt" run:pluginFunctionalTestsRelease --from=source;
"$(FORCE_COLOR=0 yarn bin)/grunt" run:pluginFunctionalTestsRelease;
fi

View file

@ -12,9 +12,6 @@ function report {
trap report EXIT
source src/dev/ci_setup/checkout_sibling_es.sh
export TEST_BROWSER_HEADLESS=1
export TEST_ES_FROM=${TEST_ES_FROM:-source}
"$(FORCE_COLOR=0 yarn bin)/grunt" jenkins:unit --from=source --dev;
"$(FORCE_COLOR=0 yarn bin)/grunt" jenkins:unit --dev;

View file

@ -13,8 +13,6 @@ function report {
trap report EXIT
source src/dev/ci_setup/checkout_sibling_es.sh
export TEST_BROWSER_HEADLESS=1
echo " -> Running mocha tests"
@ -23,7 +21,6 @@ yarn test
echo ""
echo ""
echo " -> Running jest tests"
cd "$XPACK_DIR"
node scripts/jest --ci --no-cache --verbose

View file

@ -13,8 +13,6 @@ function report {
trap report EXIT
source src/dev/ci_setup/checkout_sibling_es.sh
export TEST_BROWSER_HEADLESS=1
echo " -> Ensuring all functional tests are in a ciGroup"
@ -36,7 +34,6 @@ installDir="$PARENT_DIR/install/kibana"
mkdir -p "$installDir"
tar -xzf "$linuxBuild" -C "$installDir" --strip=1
export TEST_ES_FROM=${TEST_ES_FROM:-source}
echo " -> Running functional and api tests"
cd "$XPACK_DIR"
node scripts/functional_tests --debug --bail --kibana-install-dir "$installDir" --include-tag "ciGroup$CI_GROUP"

View file

@ -39,6 +39,8 @@
"@types/angular": "1.6.50",
"@types/boom": "^7.2.0",
"@types/cheerio": "^0.22.10",
"@types/chroma-js": "^1.4.1",
"@types/color": "^3.0.0",
"@types/d3-array": "^1.2.1",
"@types/d3-scale": "^2.0.0",
"@types/d3-shape": "^1.3.1",
@ -83,6 +85,7 @@
"@types/styled-components": "^3.0.1",
"@types/supertest": "^2.0.5",
"@types/tar-fs": "^1.16.1",
"@types/tinycolor2": "^1.4.1",
"@types/uuid": "^3.4.4",
"abab": "^1.0.4",
"ansi-colors": "^3.0.5",

View file

@ -8,6 +8,7 @@ import { shallow } from 'enzyme';
import React from 'react';
import { APMError } from 'x-pack/plugins/apm/typings/es_schemas/Error';
import { Transaction } from 'x-pack/plugins/apm/typings/es_schemas/Transaction';
import { IStickyProperty } from '../../../shared/StickyProperties';
import { StickyErrorProperties } from './StickyErrorProperties';
describe('StickyErrorProperties', () => {
@ -41,4 +42,37 @@ describe('StickyErrorProperties', () => {
expect(wrapper).toMatchSnapshot();
});
describe('error.exception.handled', () => {
function getIsHandledValue(error: APMError) {
const wrapper = shallow(
<StickyErrorProperties error={error} transaction={undefined} />
);
const stickyProps = wrapper.prop('stickyProperties') as IStickyProperty[];
const handledProp = stickyProps.find(
prop => prop.fieldName === 'error.exception.handled'
);
return handledProp && handledProp.val;
}
it('should should render "true"', () => {
const error = { error: { exception: [{ handled: true }] } } as APMError;
const isHandledValue = getIsHandledValue(error);
expect(isHandledValue).toBe('true');
});
it('should should render "false"', () => {
const error = { error: { exception: [{ handled: false }] } } as APMError;
const isHandledValue = getIsHandledValue(error);
expect(isHandledValue).toBe('false');
});
it('should should render "N/A"', () => {
const error = {} as APMError;
const isHandledValue = getIsHandledValue(error);
expect(isHandledValue).toBe('N/A');
});
});
});

View file

@ -5,6 +5,7 @@
*/
import { i18n } from '@kbn/i18n';
import { isBoolean } from 'lodash';
import React, { Fragment } from 'react';
import {
ERROR_EXC_HANDLED,
@ -56,6 +57,7 @@ function TransactionLink({ error, transaction }: Props) {
}
export function StickyErrorProperties({ error, transaction }: Props) {
const isHandled = idx(error, _ => _.error.exception[0].handled);
const stickyProperties = [
{
fieldName: '@timestamp',
@ -88,9 +90,7 @@ export function StickyErrorProperties({ error, transaction }: Props) {
label: i18n.translate('xpack.apm.errorGroupDetails.handledLabel', {
defaultMessage: 'Handled'
}),
val:
String(idx(error, _ => _.error.exception[0].handled)) ||
NOT_AVAILABLE_LABEL,
val: isBoolean(isHandled) ? String(isHandled) : NOT_AVAILABLE_LABEL,
width: '25%'
},
{

View file

@ -422,11 +422,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.628571428571433}
@ -441,11 +441,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.628571428571433}
@ -460,11 +460,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.62857142857142}
@ -479,11 +479,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.628571428571433}
@ -498,11 +498,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.628571428571405}
@ -517,11 +517,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.62857142857142}
@ -536,11 +536,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.628571428571405}
@ -555,11 +555,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.628571428571405}
@ -574,11 +574,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.628571428571377}
@ -593,11 +593,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.628571428571462}
@ -612,11 +612,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.62857142857149}
@ -631,11 +631,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.62857142857149}
@ -650,11 +650,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.62857142857149}
@ -669,11 +669,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.628571428571433}
@ -688,11 +688,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.628571428571433}
@ -707,11 +707,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.628571428571433}
@ -726,11 +726,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.628571428571547}
@ -745,11 +745,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.62857142857149}
@ -764,11 +764,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.628571428571547}
@ -783,11 +783,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.628571428571433}
@ -802,11 +802,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.628571428571433}
@ -821,11 +821,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.628571428571377}
@ -840,11 +840,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.62857142857149}
@ -859,11 +859,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.62857142857149}
@ -878,11 +878,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.628571428571604}
@ -897,11 +897,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.62857142857149}
@ -916,11 +916,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.62857142857149}
@ -935,11 +935,11 @@ exports[`Histogram Initially should have default markup 1`] = `
onMouseOver={[Function]}
style={
Object {
"fill": "#7fb5d9",
"fill": "#98c2fd",
"opacity": 1,
"rx": "0px",
"ry": "0px",
"stroke": "#7fb5d9",
"stroke": "#98c2fd",
}
}
width={22.628571428571377}

View file

@ -72,8 +72,8 @@ export class HistogramInner extends PureComponent {
...item,
color:
item === selectedItem
? theme.euiColorPrimary
: tint(0.5, theme.euiColorPrimary),
? theme.euiColorVis1
: tint(0.5, theme.euiColorVis1),
x0: item.x0 + padding,
x: item.x - padding,
y: item.y > 0 ? Math.max(item.y, MINIMUM_BUCKET_SIZE) : 0
@ -181,7 +181,7 @@ export class HistogramInner extends PureComponent {
width={x(bucketSize) - x(0)}
style={{
fill: 'transparent',
stroke: theme.euiColorPrimary,
stroke: theme.euiColorVis1,
rx: '0px',
ry: '0px'
}}

View file

@ -10,7 +10,7 @@ import { getBaseBreadcrumb, getWorkpadBreadcrumb, setBreadcrumb } from '../../li
import { getDefaultWorkpad } from '../../state/defaults';
import { setWorkpad } from '../../state/actions/workpad';
import { setAssets, resetAssets } from '../../state/actions/assets';
import { gotoPage } from '../../state/actions/pages';
import { setPage } from '../../state/actions/pages';
import { getWorkpad } from '../../state/selectors/workpad';
import { isFirstLoad } from '../../state/selectors/app';
import { setCanUserWrite, setFirstLoad } from '../../state/actions/transient';
@ -89,7 +89,7 @@ export const routes = [
// set the active page using the number provided in the url
const pageIndex = pageNumber - 1;
if (pageIndex !== workpad.page) {
dispatch(gotoPage(pageIndex));
dispatch(setPage(pageIndex));
}
// update the application's breadcrumb

View file

@ -0,0 +1,209 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots components/ColorDot color dots 1`] = `
Array [
<div
className="canvasColorDot"
>
<div
className="canvasColorDot__background canvasCheckered"
/>
<div
className="canvasColorDot__foreground"
style={
Object {
"background": "rgb(255, 255, 255)",
}
}
/>
</div>,
<div
className="canvasColorDot"
>
<div
className="canvasColorDot__background canvasCheckered"
/>
<div
className="canvasColorDot__foreground"
style={
Object {
"background": "rgb(100, 150, 250)",
}
}
/>
</div>,
<div
className="canvasColorDot"
>
<div
className="canvasColorDot__background canvasCheckered"
/>
<div
className="canvasColorDot__foreground"
style={
Object {
"background": "rgba(100, 150, 250, 0.5)",
}
}
/>
</div>,
<div
className="canvasColorDot"
>
<div
className="canvasColorDot__background canvasCheckered"
/>
<div
className="canvasColorDot__foreground"
style={
Object {
"background": "rgb(0, 0, 0)",
}
}
/>
</div>,
]
`;
exports[`Storyshots components/ColorDot color dots with children 1`] = `
Array [
<div
className="canvasColorDot"
>
<div
className="canvasColorDot__background canvasCheckered"
/>
<div
className="canvasColorDot__foreground"
style={
Object {
"background": "rgb(255, 255, 255)",
}
}
>
<svg
className="euiIcon euiIcon--medium"
focusable="false"
height="16"
style={
Object {
"fill": "#000",
}
}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8 7h3.5a.5.5 0 1 1 0 1H8v3.5a.5.5 0 1 1-1 0V8H3.5a.5.5 0 0 1 0-1H7V3.5a.5.5 0 0 1 1 0V7zm-.5-7C11.636 0 15 3.364 15 7.5S11.636 15 7.5 15 0 11.636 0 7.5 3.364 0 7.5 0zm0 .882a6.618 6.618 0 1 0 0 13.236A6.618 6.618 0 0 0 7.5.882z"
fillRule="evenodd"
/>
</svg>
</div>
</div>,
<div
className="canvasColorDot"
>
<div
className="canvasColorDot__background canvasCheckered"
/>
<div
className="canvasColorDot__foreground"
style={
Object {
"background": "rgb(102, 102, 102)",
}
}
>
<svg
className="euiIcon euiIcon--medium"
focusable="false"
height="16"
style={
Object {
"fill": "#fff",
}
}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.5 0C11.636 0 15 3.364 15 7.5S11.636 15 7.5 15 0 11.636 0 7.5 3.364 0 7.5 0zm0 .882a6.618 6.618 0 1 0 0 13.236A6.618 6.618 0 0 0 7.5.882zM3.5 7h8a.5.5 0 1 1 0 1h-8a.5.5 0 0 1 0-1z"
fillRule="evenodd"
/>
</svg>
</div>
</div>,
<div
className="canvasColorDot"
>
<div
className="canvasColorDot__background canvasCheckered"
/>
<div
className="canvasColorDot__foreground"
style={
Object {
"background": "rgba(100, 150, 250, 0.5)",
}
}
>
<svg
className="euiIcon euiIcon--medium"
focusable="false"
height="16"
style={
Object {
"fill": "#fff",
}
}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.378 1.496l6.695 10.984A1 1 0 0 1 14.22 14H1.667a1 1 0 0 1-.883-1.47L6.642 1.545a1 1 0 0 1 1.736-.05zm-.853.52L1.667 13h12.552L7.525 2.016zM7.14 10.06L6.9 5.18h1.3l-.25 4.878h-.81zm.394 1.901a.61.61 0 0 1-.448-.186.606.606 0 0 1-.186-.444c0-.174.062-.323.186-.446a.614.614 0 0 1 .448-.184c.169 0 .315.06.44.182.124.122.186.27.186.448a.6.6 0 0 1-.189.446.607.607 0 0 1-.437.184z"
fillRule="evenodd"
/>
</svg>
</div>
</div>,
<div
className="canvasColorDot"
>
<div
className="canvasColorDot__background canvasCheckered"
/>
<div
className="canvasColorDot__foreground"
style={
Object {
"background": "rgb(0, 0, 0)",
}
}
>
<svg
className="euiIcon euiIcon--medium"
focusable="false"
height="16"
style={
Object {
"fill": "#fff",
}
}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.5 12a.502.502 0 0 1-.354-.146l-4-4a.502.502 0 0 1 .708-.708L6.5 10.793l6.646-6.647a.502.502 0 0 1 .708.708l-7 7A.502.502 0 0 1 6.5 12"
fillRule="evenodd"
/>
</svg>
</div>
</div>,
]
`;
exports[`Storyshots components/ColorDot invalid dots 1`] = `null`;

View file

@ -0,0 +1,37 @@
/*
* 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 { EuiIcon } from '@elastic/eui';
import { storiesOf } from '@storybook/react';
import React from 'react';
import { ColorDot } from '../color_dot';
storiesOf('components/ColorDot', module)
.addParameters({ info: { propTablesExclude: [EuiIcon] } })
.add('color dots', () => [
<ColorDot key="1" value="white" />,
<ColorDot key="2" value="rgb(100, 150, 250)" />,
<ColorDot key="3" value="rgba(100, 150, 250, .5)" />,
<ColorDot key="4" value="#000" />,
])
.add('invalid dots', () => [
<ColorDot key="1" value="elastic" />,
<ColorDot key="2" value="#canvas" />,
<ColorDot key="3" value="#abcd" />,
])
.add('color dots with children', () => [
<ColorDot key="1" value="#FFF">
<EuiIcon type="plusInCircle" color="#000" />
</ColorDot>,
<ColorDot key="2" value="#666">
<EuiIcon type="minusInCircle" color="#fff" />
</ColorDot>,
<ColorDot key="3" value="rgba(100, 150, 250, .5)">
<EuiIcon type="alert" color="#fff" />
</ColorDot>,
<ColorDot key="4" value="#000">
<EuiIcon type="check" color="#fff" />
</ColorDot>,
]);

View file

@ -4,14 +4,27 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import React, { ReactNode, SFC } from 'react';
import tinycolor from 'tinycolor2';
export interface Props {
/** Any valid CSS color. If not a valid CSS string, the dot will not render */
value: string;
/** Nodes to display within the dot. Should fit within the constraints. */
children?: ReactNode;
}
export const ColorDot: SFC<Props> = ({ value, children }) => {
const tc = tinycolor(value);
if (!tc.isValid()) {
return null;
}
export const ColorDot = ({ value, children }) => {
return (
<div className="canvasColorDot">
<div className="canvasColorDot__background canvasCheckered" />
<div className="canvasColorDot__foreground" style={{ background: value }}>
<div className="canvasColorDot__foreground" style={{ background: tc.toRgbString() }}>
{children}
</div>
</div>
@ -21,5 +34,4 @@ export const ColorDot = ({ value, children }) => {
ColorDot.propTypes = {
value: PropTypes.string,
children: PropTypes.node,
handleClick: PropTypes.func,
};

View file

@ -0,0 +1,795 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots components/ColorManager default 1`] = `
Array [
<div
className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
className="canvasColorDot"
>
<div
className="canvasColorDot__background canvasCheckered"
/>
<div
className="canvasColorDot__foreground"
style={
Object {
"background": "rgb(171, 205, 239)",
}
}
/>
</div>
</div>
<div
className="euiFlexItem"
style={
Object {
"display": "inline-block",
}
}
>
<div
className="euiFormControlLayout"
>
<div
className="euiFormControlLayout__childrenWrapper"
>
<input
className="euiFieldText"
onChange={[Function]}
placeholder="#hex color"
type="text"
value="#abcdef"
/>
</div>
</div>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-label="Add Color"
className="euiButtonIcon euiButtonIcon--primary"
disabled={true}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8 7h3.5a.5.5 0 1 1 0 1H8v3.5a.5.5 0 1 1-1 0V8H3.5a.5.5 0 0 1 0-1H7V3.5a.5.5 0 0 1 1 0V7zm-.5-7C11.636 0 15 3.364 15 7.5S11.636 15 7.5 15 0 11.636 0 7.5 3.364 0 7.5 0zm0 .882a6.618 6.618 0 1 0 0 13.236A6.618 6.618 0 0 0 7.5.882z"
fillRule="evenodd"
/>
</svg>
</button>
<button
aria-label="Remove Color"
className="euiButtonIcon euiButtonIcon--primary"
disabled={true}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.5 0C11.636 0 15 3.364 15 7.5S11.636 15 7.5 15 0 11.636 0 7.5 3.364 0 7.5 0zm0 .882a6.618 6.618 0 1 0 0 13.236A6.618 6.618 0 0 0 7.5.882zM3.5 7h8a.5.5 0 1 1 0 1h-8a.5.5 0 0 1 0-1z"
fillRule="evenodd"
/>
</svg>
</button>
</div>
</div>,
<div
className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
className="canvasColorDot"
>
<div
className="canvasColorDot__background canvasCheckered"
/>
<div
className="canvasColorDot__foreground"
style={
Object {
"background": "rgb(170, 187, 204)",
}
}
/>
</div>
</div>
<div
className="euiFlexItem"
style={
Object {
"display": "inline-block",
}
}
>
<div
className="euiFormControlLayout"
>
<div
className="euiFormControlLayout__childrenWrapper"
>
<input
className="euiFieldText"
onChange={[Function]}
placeholder="#hex color"
type="text"
value="#abc"
/>
</div>
</div>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-label="Add Color"
className="euiButtonIcon euiButtonIcon--primary"
disabled={true}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8 7h3.5a.5.5 0 1 1 0 1H8v3.5a.5.5 0 1 1-1 0V8H3.5a.5.5 0 0 1 0-1H7V3.5a.5.5 0 0 1 1 0V7zm-.5-7C11.636 0 15 3.364 15 7.5S11.636 15 7.5 15 0 11.636 0 7.5 3.364 0 7.5 0zm0 .882a6.618 6.618 0 1 0 0 13.236A6.618 6.618 0 0 0 7.5.882z"
fillRule="evenodd"
/>
</svg>
</button>
<button
aria-label="Remove Color"
className="euiButtonIcon euiButtonIcon--primary"
disabled={true}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.5 0C11.636 0 15 3.364 15 7.5S11.636 15 7.5 15 0 11.636 0 7.5 3.364 0 7.5 0zm0 .882a6.618 6.618 0 1 0 0 13.236A6.618 6.618 0 0 0 7.5.882zM3.5 7h8a.5.5 0 1 1 0 1h-8a.5.5 0 0 1 0-1z"
fillRule="evenodd"
/>
</svg>
</button>
</div>
</div>,
]
`;
exports[`Storyshots components/ColorManager interactive 1`] = `
<div
className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
className="canvasColorDot"
>
<div
className="canvasColorDot__background canvasCheckered"
/>
<div
className="canvasColorDot__foreground"
style={
Object {
"background": "rgba(255, 255, 255, 0)",
}
}
/>
</div>
</div>
<div
className="euiFlexItem"
style={
Object {
"display": "inline-block",
}
}
>
<div
className="euiFormControlLayout"
>
<div
className="euiFormControlLayout__childrenWrapper"
>
<input
className="euiFieldText"
onChange={[Function]}
placeholder="#hex color"
type="text"
value=""
/>
</div>
</div>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-label="Add Color"
className="euiButtonIcon euiButtonIcon--primary"
disabled={true}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8 7h3.5a.5.5 0 1 1 0 1H8v3.5a.5.5 0 1 1-1 0V8H3.5a.5.5 0 0 1 0-1H7V3.5a.5.5 0 0 1 1 0V7zm-.5-7C11.636 0 15 3.364 15 7.5S11.636 15 7.5 15 0 11.636 0 7.5 3.364 0 7.5 0zm0 .882a6.618 6.618 0 1 0 0 13.236A6.618 6.618 0 0 0 7.5.882z"
fillRule="evenodd"
/>
</svg>
</button>
<button
aria-label="Remove Color"
className="euiButtonIcon euiButtonIcon--primary"
disabled={true}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.5 0C11.636 0 15 3.364 15 7.5S11.636 15 7.5 15 0 11.636 0 7.5 3.364 0 7.5 0zm0 .882a6.618 6.618 0 1 0 0 13.236A6.618 6.618 0 0 0 7.5.882zM3.5 7h8a.5.5 0 1 1 0 1h-8a.5.5 0 0 1 0-1z"
fillRule="evenodd"
/>
</svg>
</button>
</div>
</div>
`;
exports[`Storyshots components/ColorManager invalid colors 1`] = `
Array [
<div
className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
className="canvasColorDot"
>
<div
className="canvasColorDot__background canvasCheckered"
/>
<div
className="canvasColorDot__foreground"
style={
Object {
"background": "rgba(255, 255, 255, 0)",
}
}
/>
</div>
</div>
<div
className="euiFlexItem"
style={
Object {
"display": "inline-block",
}
}
>
<div
className="euiFormControlLayout"
>
<div
className="euiFormControlLayout__childrenWrapper"
>
<input
className="euiFieldText"
onChange={[Function]}
placeholder="#hex color"
type="text"
value="#abcd"
/>
</div>
</div>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-label="Add Color"
className="euiButtonIcon euiButtonIcon--primary"
disabled={true}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8 7h3.5a.5.5 0 1 1 0 1H8v3.5a.5.5 0 1 1-1 0V8H3.5a.5.5 0 0 1 0-1H7V3.5a.5.5 0 0 1 1 0V7zm-.5-7C11.636 0 15 3.364 15 7.5S11.636 15 7.5 15 0 11.636 0 7.5 3.364 0 7.5 0zm0 .882a6.618 6.618 0 1 0 0 13.236A6.618 6.618 0 0 0 7.5.882z"
fillRule="evenodd"
/>
</svg>
</button>
<button
aria-label="Remove Color"
className="euiButtonIcon euiButtonIcon--primary"
disabled={true}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.5 0C11.636 0 15 3.364 15 7.5S11.636 15 7.5 15 0 11.636 0 7.5 3.364 0 7.5 0zm0 .882a6.618 6.618 0 1 0 0 13.236A6.618 6.618 0 0 0 7.5.882zM3.5 7h8a.5.5 0 1 1 0 1h-8a.5.5 0 0 1 0-1z"
fillRule="evenodd"
/>
</svg>
</button>
</div>
</div>,
<div
className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
className="canvasColorDot"
>
<div
className="canvasColorDot__background canvasCheckered"
/>
<div
className="canvasColorDot__foreground"
style={
Object {
"background": "rgba(255, 255, 255, 0)",
}
}
/>
</div>
</div>
<div
className="euiFlexItem"
style={
Object {
"display": "inline-block",
}
}
>
<div
className="euiFormControlLayout"
>
<div
className="euiFormControlLayout__childrenWrapper"
>
<input
className="euiFieldText"
onChange={[Function]}
placeholder="#hex color"
type="text"
value="#canvas"
/>
</div>
</div>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-label="Add Color"
className="euiButtonIcon euiButtonIcon--primary"
disabled={true}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8 7h3.5a.5.5 0 1 1 0 1H8v3.5a.5.5 0 1 1-1 0V8H3.5a.5.5 0 0 1 0-1H7V3.5a.5.5 0 0 1 1 0V7zm-.5-7C11.636 0 15 3.364 15 7.5S11.636 15 7.5 15 0 11.636 0 7.5 3.364 0 7.5 0zm0 .882a6.618 6.618 0 1 0 0 13.236A6.618 6.618 0 0 0 7.5.882z"
fillRule="evenodd"
/>
</svg>
</button>
<button
aria-label="Remove Color"
className="euiButtonIcon euiButtonIcon--primary"
disabled={true}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.5 0C11.636 0 15 3.364 15 7.5S11.636 15 7.5 15 0 11.636 0 7.5 3.364 0 7.5 0zm0 .882a6.618 6.618 0 1 0 0 13.236A6.618 6.618 0 0 0 7.5.882zM3.5 7h8a.5.5 0 1 1 0 1h-8a.5.5 0 0 1 0-1z"
fillRule="evenodd"
/>
</svg>
</button>
</div>
</div>,
]
`;
exports[`Storyshots components/ColorManager with buttons 1`] = `
Array [
<div
className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
className="canvasColorDot"
>
<div
className="canvasColorDot__background canvasCheckered"
/>
<div
className="canvasColorDot__foreground"
style={
Object {
"background": "rgb(171, 205, 239)",
}
}
/>
</div>
</div>
<div
className="euiFlexItem"
style={
Object {
"display": "inline-block",
}
}
>
<div
className="euiFormControlLayout"
>
<div
className="euiFormControlLayout__childrenWrapper"
>
<input
className="euiFieldText"
onChange={[Function]}
placeholder="#hex color"
type="text"
value="#abcdef"
/>
</div>
</div>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-label="Add Color"
className="euiButtonIcon euiButtonIcon--primary"
disabled={false}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8 7h3.5a.5.5 0 1 1 0 1H8v3.5a.5.5 0 1 1-1 0V8H3.5a.5.5 0 0 1 0-1H7V3.5a.5.5 0 0 1 1 0V7zm-.5-7C11.636 0 15 3.364 15 7.5S11.636 15 7.5 15 0 11.636 0 7.5 3.364 0 7.5 0zm0 .882a6.618 6.618 0 1 0 0 13.236A6.618 6.618 0 0 0 7.5.882z"
fillRule="evenodd"
/>
</svg>
</button>
<button
aria-label="Remove Color"
className="euiButtonIcon euiButtonIcon--primary"
disabled={true}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.5 0C11.636 0 15 3.364 15 7.5S11.636 15 7.5 15 0 11.636 0 7.5 3.364 0 7.5 0zm0 .882a6.618 6.618 0 1 0 0 13.236A6.618 6.618 0 0 0 7.5.882zM3.5 7h8a.5.5 0 1 1 0 1h-8a.5.5 0 0 1 0-1z"
fillRule="evenodd"
/>
</svg>
</button>
</div>
</div>,
<div
className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
className="canvasColorDot"
>
<div
className="canvasColorDot__background canvasCheckered"
/>
<div
className="canvasColorDot__foreground"
style={
Object {
"background": "rgb(171, 205, 239)",
}
}
/>
</div>
</div>
<div
className="euiFlexItem"
style={
Object {
"display": "inline-block",
}
}
>
<div
className="euiFormControlLayout"
>
<div
className="euiFormControlLayout__childrenWrapper"
>
<input
className="euiFieldText"
onChange={[Function]}
placeholder="#hex color"
type="text"
value="#abcdef"
/>
</div>
</div>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-label="Add Color"
className="euiButtonIcon euiButtonIcon--primary"
disabled={true}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8 7h3.5a.5.5 0 1 1 0 1H8v3.5a.5.5 0 1 1-1 0V8H3.5a.5.5 0 0 1 0-1H7V3.5a.5.5 0 0 1 1 0V7zm-.5-7C11.636 0 15 3.364 15 7.5S11.636 15 7.5 15 0 11.636 0 7.5 3.364 0 7.5 0zm0 .882a6.618 6.618 0 1 0 0 13.236A6.618 6.618 0 0 0 7.5.882z"
fillRule="evenodd"
/>
</svg>
</button>
<button
aria-label="Remove Color"
className="euiButtonIcon euiButtonIcon--primary"
disabled={false}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.5 0C11.636 0 15 3.364 15 7.5S11.636 15 7.5 15 0 11.636 0 7.5 3.364 0 7.5 0zm0 .882a6.618 6.618 0 1 0 0 13.236A6.618 6.618 0 0 0 7.5.882zM3.5 7h8a.5.5 0 1 1 0 1h-8a.5.5 0 0 1 0-1z"
fillRule="evenodd"
/>
</svg>
</button>
</div>
</div>,
<div
className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
className="canvasColorDot"
>
<div
className="canvasColorDot__background canvasCheckered"
/>
<div
className="canvasColorDot__foreground"
style={
Object {
"background": "rgb(171, 205, 239)",
}
}
/>
</div>
</div>
<div
className="euiFlexItem"
style={
Object {
"display": "inline-block",
}
}
>
<div
className="euiFormControlLayout"
>
<div
className="euiFormControlLayout__childrenWrapper"
>
<input
className="euiFieldText"
onChange={[Function]}
placeholder="#hex color"
type="text"
value="#abcdef"
/>
</div>
</div>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-label="Add Color"
className="euiButtonIcon euiButtonIcon--primary"
disabled={false}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8 7h3.5a.5.5 0 1 1 0 1H8v3.5a.5.5 0 1 1-1 0V8H3.5a.5.5 0 0 1 0-1H7V3.5a.5.5 0 0 1 1 0V7zm-.5-7C11.636 0 15 3.364 15 7.5S11.636 15 7.5 15 0 11.636 0 7.5 3.364 0 7.5 0zm0 .882a6.618 6.618 0 1 0 0 13.236A6.618 6.618 0 0 0 7.5.882z"
fillRule="evenodd"
/>
</svg>
</button>
<button
aria-label="Remove Color"
className="euiButtonIcon euiButtonIcon--primary"
disabled={false}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.5 0C11.636 0 15 3.364 15 7.5S11.636 15 7.5 15 0 11.636 0 7.5 3.364 0 7.5 0zm0 .882a6.618 6.618 0 1 0 0 13.236A6.618 6.618 0 0 0 7.5.882zM3.5 7h8a.5.5 0 1 1 0 1h-8a.5.5 0 0 1 0-1z"
fillRule="evenodd"
/>
</svg>
</button>
</div>
</div>,
]
`;

View file

@ -0,0 +1,88 @@
/*
* 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 { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import React from 'react';
import { ColorManager } from '../color_manager';
class Interactive extends React.Component<{}, { value: string }> {
public state = {
value: '',
};
public render() {
return (
<ColorManager
onAddColor={action('onAddColor')}
onRemoveColor={action('onRemoveColor')}
onChange={value => this.setState({ value })}
value={this.state.value}
/>
);
}
}
storiesOf('components/ColorManager', module)
.addParameters({
info: {
inline: true,
styles: {
infoBody: {
margin: 20,
},
infoStory: {
margin: '40px 60px',
width: '320px',
},
},
},
})
.add('default', () => [
<ColorManager key="1" onChange={action('onChange')} value="#abcdef" />,
<ColorManager key="2" onChange={action('onChange')} value="#abc" />,
])
.add('invalid colors', () => [
<ColorManager key="1" onChange={action('onChange')} value="#abcd" />,
<ColorManager key="2" onChange={action('onChange')} value="canvas" />,
])
.add('with buttons', () => [
<ColorManager
key="1"
onAddColor={action('onAddColor')}
onChange={action('onChange')}
value="#abcdef"
/>,
<ColorManager
key="2"
onChange={action('onChange')}
onRemoveColor={action('onRemoveColor')}
value="#abcdef"
/>,
<ColorManager
key="3"
onAddColor={action('onAddColor')}
onChange={action('onChange')}
onRemoveColor={action('onRemoveColor')}
value="#abcdef"
/>,
])
.add('interactive', () => <Interactive />, {
info: {
inline: true,
source: false,
propTablesExclude: [Interactive],
styles: {
infoBody: {
margin: 20,
},
infoStory: {
margin: '40px 60px',
width: '320px',
},
},
},
});

View file

@ -1,51 +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 PropTypes from 'prop-types';
import { EuiFieldText, EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { ColorDot } from '../color_dot/color_dot';
export const ColorManager = ({ value, addColor, removeColor, onChange }) => (
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={1}>
<ColorDot value={value} />
</EuiFlexItem>
<EuiFlexItem grow={5} style={{ display: 'inline-block' }}>
<EuiFieldText
compressed
value={value || ''}
placeholder="#hex color"
onChange={e => onChange(e.target.value)}
/>
</EuiFlexItem>
{(addColor || removeColor) && (
<EuiFlexItem grow={false}>
{addColor && (
<EuiButtonIcon
aria-label="Add Color"
iconType="plusInCircle"
onClick={() => addColor(value)}
/>
)}
{removeColor && (
<EuiButtonIcon
aria-label="Remove Color"
iconType="minusInCircle"
onClick={() => removeColor(value)}
/>
)}
</EuiFlexItem>
)}
</EuiFlexGroup>
);
ColorManager.propTypes = {
value: PropTypes.string,
addColor: PropTypes.func,
removeColor: PropTypes.func,
onChange: PropTypes.func.isRequired,
};

View file

@ -0,0 +1,81 @@
/*
* 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 { EuiButtonIcon, EuiFieldText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import PropTypes from 'prop-types';
import React, { SFC } from 'react';
import tinycolor from 'tinycolor2';
import { ColorDot } from '../color_dot/color_dot';
export interface Props {
/** The function to call when the Add Color button is clicked. The button will not appear if there is no handler. */
onAddColor?: (value: string) => void;
/** The function to call when the value is changed */
onChange: (value: string) => void;
/** The function to call when the Remove Color button is clicked. The button will not appear if there is no handler. */
onRemoveColor?: (value: string) => void;
/**
* The value of the color manager. Only honors hexadecimal values.
* @default ''
*/
value?: string;
}
export const ColorManager: SFC<Props> = ({ value = '', onAddColor, onRemoveColor, onChange }) => {
const tc = tinycolor(value);
const validColor = tc.isValid() && tc.getFormat() === 'hex';
if (value.length > 0 && !value.startsWith('#')) {
value = '#' + value;
}
const add = (
<EuiButtonIcon
aria-label="Add Color"
iconType="plusInCircle"
isDisabled={!validColor || !onAddColor}
onClick={() => onAddColor && onAddColor(value)}
/>
);
const remove = (
<EuiButtonIcon
aria-label="Remove Color"
iconType="minusInCircle"
isDisabled={!validColor || !onRemoveColor}
onClick={() => onRemoveColor && onRemoveColor(value)}
/>
);
return (
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
<ColorDot value={validColor ? value : 'rgba(255,255,255,0)'} />
</EuiFlexItem>
<EuiFlexItem style={{ display: 'inline-block' }}>
<EuiFieldText
value={value}
isInvalid={!validColor && value.length > 0}
placeholder="#hex color"
onChange={e => onChange(e.target.value)}
/>
</EuiFlexItem>
{(add || remove) && (
<EuiFlexItem grow={false}>
{add}
{remove}
</EuiFlexItem>
)}
</EuiFlexGroup>
);
};
ColorManager.propTypes = {
onAddColor: PropTypes.func,
onChange: PropTypes.func.isRequired,
onRemoveColor: PropTypes.func,
value: PropTypes.string,
};

View file

@ -5,6 +5,7 @@
*/
import { pure } from 'recompose';
import { ItemGrid as Component } from './item_grid';
export const ItemGrid = pure(Component);
import { ColorManager as Component } from './color_manager';
export const ColorManager = pure(Component);

View file

@ -0,0 +1,60 @@
/*
* 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 { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import React from 'react';
import { ColorPalette } from '../color_palette';
const THREE_COLORS = ['#fff', '#666', '#000'];
const SIX_COLORS = ['#fff', '#666', '#000', '#abc', '#def', '#abcdef'];
class Interactive extends React.Component<{}, { value: string }> {
public state = {
value: '',
};
public render() {
return (
<ColorPalette
colors={SIX_COLORS}
onChange={value => this.setState({ value })}
value={this.state.value}
/>
);
}
}
storiesOf('components/ColorPalette', module)
.add('three colors', () => [
<ColorPalette key="1" onChange={action('onChange')} colors={THREE_COLORS} />,
<ColorPalette key="2" value="#fff" onChange={action('onChange')} colors={THREE_COLORS} />,
])
.add('six colors', () => [
<ColorPalette key="1" onChange={action('onChange')} colors={SIX_COLORS} />,
<ColorPalette key="2" value="#fff" onChange={action('onChange')} colors={SIX_COLORS} />,
])
.add('six colors, wrap at 4', () => (
<ColorPalette value="#fff" onChange={action('onChange')} colors={SIX_COLORS} colorsPerRow={4} />
))
.add('six colors, value missing', () => (
<ColorPalette value="#f00" onChange={action('onChange')} colors={SIX_COLORS} />
))
.add('interactive', () => <Interactive />, {
info: {
inline: true,
source: false,
propTablesExclude: [Interactive],
styles: {
infoBody: {
margin: 20,
},
infoStory: {
margin: '40px 60px',
},
},
},
});

View file

@ -1,40 +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 PropTypes from 'prop-types';
import { EuiIcon, EuiLink } from '@elastic/eui';
import { readableColor } from '../../lib/readable_color';
import { ColorDot } from '../color_dot';
import { ItemGrid } from '../item_grid';
export const ColorPalette = ({ value, colors, colorsPerRow, onChange }) => (
<div className="canvasColorPalette">
<ItemGrid items={colors} itemsPerRow={colorsPerRow || 6}>
{({ item: color }) => (
<EuiLink
style={{ fontSize: 0 }}
key={color}
onClick={() => onChange(color)}
className="canvasColorPalette__dot"
>
<ColorDot value={color}>
{color === value && (
<EuiIcon type="check" className="selected-color" color={readableColor(value)} />
)}
</ColorDot>
</EuiLink>
)}
</ItemGrid>
</div>
);
ColorPalette.propTypes = {
colors: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
value: PropTypes.string,
colorsPerRow: PropTypes.number,
};

View file

@ -0,0 +1,80 @@
/*
* 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 { EuiIcon, EuiLink } from '@elastic/eui';
import PropTypes from 'prop-types';
import React, { SFC } from 'react';
import tinycolor from 'tinycolor2';
import { readableColor } from '../../lib/readable_color';
import { ColorDot } from '../color_dot';
import { ItemGrid } from '../item_grid';
export interface Props {
/**
* An array of hexadecimal color values. Non-hex will be ignored.
* @default []
*/
colors?: string[];
/**
* The number of colors to display before wrapping to a new row.
* @default 6
*/
colorsPerRow?: number;
/** The function to call when the color is changed. */
onChange: (value: string) => void;
/**
* The value of the color in the selector. Should be hexadecimal. If it is not in the colors array, it will be ignored.
* @default ''
*/
value?: string;
}
export const ColorPalette: SFC<Props> = ({
colors = [],
colorsPerRow = 6,
onChange,
value = '',
}) => {
if (colors.length === 0) {
return null;
}
colors = colors.filter(color => {
const providedColor = tinycolor(color);
return providedColor.isValid() && providedColor.getFormat() === 'hex';
});
return (
<div className="canvasColorPalette">
<ItemGrid items={colors} itemsPerRow={colorsPerRow}>
{color => {
const match = tinycolor.equals(color, value);
const icon = match ? (
<EuiIcon type="check" className="selected-color" color={readableColor(value)} />
) : null;
return (
<EuiLink
style={{ fontSize: 0 }}
key={color}
onClick={() => !match && onChange(color)}
className="canvasColorPalette__dot"
>
<ColorDot value={color}>{icon}</ColorDot>
</EuiLink>
);
}}
</ItemGrid>
</div>
);
};
ColorPalette.propTypes = {
colors: PropTypes.array,
colorsPerRow: PropTypes.number,
onChange: PropTypes.func.isRequired,
value: PropTypes.string,
};

View file

@ -0,0 +1,93 @@
/*
* 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 { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import React from 'react';
import { ColorPicker } from '../color_picker';
const THREE_COLORS = ['#fff', '#666', '#000'];
const SIX_COLORS = ['#fff', '#666', '#000', '#abc', '#def', '#abcdef'];
class Interactive extends React.Component<{}, { value: string; colors: string[] }> {
public state = {
value: '',
colors: SIX_COLORS,
};
public render() {
return (
<ColorPicker
colors={this.state.colors}
onAddColor={value => this.setState({ colors: this.state.colors.concat(value) })}
onRemoveColor={value =>
this.setState({ colors: this.state.colors.filter(color => color !== value) })
}
onChange={value => this.setState({ value })}
value={this.state.value}
/>
);
}
}
storiesOf('components/ColorPicker', module)
.addParameters({
info: {
inline: true,
styles: {
infoBody: {
margin: 20,
},
infoStory: {
margin: '40px 60px',
width: '320px',
},
},
},
})
.add('three colors', () => (
<ColorPicker
value="#fff"
onAddColor={action('onAddColor')}
onRemoveColor={action('onRemoveColor')}
onChange={action('onChange')}
colors={THREE_COLORS}
/>
))
.add('six colors', () => (
<ColorPicker
value="#fff"
onAddColor={action('onAddColor')}
onRemoveColor={action('onRemoveColor')}
onChange={action('onChange')}
colors={SIX_COLORS}
/>
))
.add('six colors, value missing', () => (
<ColorPicker
value="#a1b2c3"
onAddColor={action('onAddColor')}
onRemoveColor={action('onRemoveColor')}
onChange={action('onChange')}
colors={SIX_COLORS}
/>
))
.add('interactive', () => <Interactive />, {
info: {
inline: true,
source: false,
propTablesExclude: [Interactive],
styles: {
infoBody: {
margin: 20,
},
infoStory: {
margin: '40px 60px',
width: '320px',
},
},
},
});

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