Merge remote-tracking branch 'origin/master' into feature/merge-code
|
@ -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)
|
||||
|
|
|
@ -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/**/*',
|
||||
],
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
Before Width: | Height: | Size: 393 KiB After Width: | Height: | Size: 311 KiB |
Before Width: | Height: | Size: 364 KiB After Width: | Height: | Size: 410 KiB |
Before Width: | Height: | Size: 340 KiB After Width: | Height: | Size: 426 KiB |
Before Width: | Height: | Size: 480 KiB After Width: | Height: | Size: 501 KiB |
BIN
docs/apm/images/apm-index-pattern.png
Normal file
After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 317 KiB After Width: | Height: | Size: 429 KiB |
Before Width: | Height: | Size: 430 KiB After Width: | Height: | Size: 510 KiB |
Before Width: | Height: | Size: 264 KiB After Width: | Height: | Size: 265 KiB |
Before Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 368 KiB After Width: | Height: | Size: 408 KiB |
Before Width: | Height: | Size: 401 KiB After Width: | Height: | Size: 479 KiB |
Before Width: | Height: | Size: 423 KiB After Width: | Height: | Size: 450 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 384 KiB After Width: | Height: | Size: 332 KiB |
Before Width: | Height: | Size: 286 KiB After Width: | Height: | Size: 299 KiB |
Before Width: | Height: | Size: 264 KiB |
Before Width: | Height: | Size: 381 KiB After Width: | Height: | Size: 408 KiB |
Before Width: | Height: | Size: 289 KiB |
|
@ -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]
|
||||
|
|
BIN
docs/images/follower_indices.png
Normal file
After Width: | Height: | Size: 182 KiB |
|
@ -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}.
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -18,7 +18,7 @@ module.exports = {
|
|||
|
||||
settings: {
|
||||
react: {
|
||||
version: semver.coerce(PKG.dependencies.react),
|
||||
version: semver.valid(semver.coerce(PKG.dependencies.react)),
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
@ -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 [],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -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,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'];
|
||||
|
|
|
@ -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']);
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -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']) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
###
|
||||
|
|
|
@ -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>
|
||||
`;
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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
|
||||
});
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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 {};
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -30,7 +30,7 @@ const {
|
|||
403: Forbidden,
|
||||
413: RequestEntityTooLarge,
|
||||
NotFound,
|
||||
BadRequest
|
||||
BadRequest,
|
||||
} = elasticsearch.errors;
|
||||
|
||||
import {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 } : {}),
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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] = [];
|
||||
|
|
|
@ -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
|
||||
})
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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%'
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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'
|
||||
}}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`;
|
|
@ -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>,
|
||||
]);
|
|
@ -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,
|
||||
};
|
|
@ -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>,
|
||||
]
|
||||
`;
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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);
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|