diff --git a/.eslintrc.js b/.eslintrc.js index 7af162f349b5c..3d6a5c262c453 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -322,6 +322,7 @@ module.exports = { 'x-pack/test/functional/apps/**/*.js', 'x-pack/legacy/plugins/apm/**/*.js', 'test/*/config.ts', + 'test/*/config_open.ts', 'test/*/{tests,test_suites,apis,apps}/**/*', 'test/visual_regression/tests/**/*', 'x-pack/test/*/{tests,test_suites,apis,apps}/**/*', diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index accc99170bb70..df3a56dd35130 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -29,33 +29,32 @@ /src/plugins/dashboard/ @elastic/kibana-app # App Architecture +/examples/url_generators_examples/ @elastic/kibana-app-arch +/examples/url_generators_explorer/ @elastic/kibana-app-arch /packages/kbn-interpreter/ @elastic/kibana-app-arch -/src/legacy/core_plugins/data/ @elastic/kibana-app-arch -/src/legacy/core_plugins/elasticsearch/lib/create_proxy.js @elastic/kibana-app-arch /src/legacy/core_plugins/embeddable_api/ @elastic/kibana-app-arch /src/legacy/core_plugins/interpreter/ @elastic/kibana-app-arch /src/legacy/core_plugins/kibana_react/ @elastic/kibana-app-arch /src/legacy/core_plugins/kibana/public/management/ @elastic/kibana-app-arch -/src/legacy/core_plugins/kibana/server/field_formats/ @elastic/kibana-app-arch /src/legacy/core_plugins/kibana/server/routes/api/management/ @elastic/kibana-app-arch -/src/legacy/core_plugins/kibana/server/routes/api/suggestions/ @elastic/kibana-app-arch /src/legacy/core_plugins/visualizations/ @elastic/kibana-app-arch /src/legacy/server/index_patterns/ @elastic/kibana-app-arch +/src/plugins/advanced_settings/ @elastic/kibana-app-arch /src/plugins/bfetch/ @elastic/kibana-app-arch /src/plugins/data/ @elastic/kibana-app-arch /src/plugins/embeddable/ @elastic/kibana-app-arch /src/plugins/expressions/ @elastic/kibana-app-arch /src/plugins/inspector/ @elastic/kibana-app-arch /src/plugins/kibana_react/ @elastic/kibana-app-arch +/src/plugins/kibana_react/public/code_editor @elastic/kibana-canvas /src/plugins/kibana_utils/ @elastic/kibana-app-arch /src/plugins/management/ @elastic/kibana-app-arch /src/plugins/navigation/ @elastic/kibana-app-arch +/src/plugins/share/ @elastic/kibana-app-arch /src/plugins/ui_actions/ @elastic/kibana-app-arch /src/plugins/visualizations/ @elastic/kibana-app-arch -/src/plugins/share/ @elastic/kibana-app-arch -/examples/url_generators_examples/ @elastic/kibana-app-arch -/examples/url_generators_explorer/ @elastic/kibana-app-arch /x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch +/x-pack/plugins/data_enhanced/ @elastic/kibana-app-arch /x-pack/plugins/drilldowns/ @elastic/kibana-app-arch # APM @@ -75,9 +74,9 @@ # Observability UIs /x-pack/legacy/plugins/infra/ @elastic/logs-metrics-ui /x-pack/plugins/infra/ @elastic/logs-metrics-ui -/x-pack/plugins/ingest_manager/ @elastic/ingest -/x-pack/legacy/plugins/ingest_manager/ @elastic/ingest -/x-pack/plugins/observability/ @elastic/logs-metrics-ui @elastic/apm-ui @elastic/uptime @elastic/ingest +/x-pack/plugins/ingest_manager/ @elastic/ingest-management +/x-pack/legacy/plugins/ingest_manager/ @elastic/ingest-management +/x-pack/plugins/observability/ @elastic/logs-metrics-ui @elastic/apm-ui @elastic/uptime @elastic/ingest-management /x-pack/legacy/plugins/monitoring/ @elastic/stack-monitoring-ui # Machine Learning diff --git a/docs/apm/advanced-queries.asciidoc b/docs/apm/advanced-queries.asciidoc index ed77ebb4c4930..add6f601489e1 100644 --- a/docs/apm/advanced-queries.asciidoc +++ b/docs/apm/advanced-queries.asciidoc @@ -5,7 +5,7 @@ When querying in the APM app, you're simply searching and selecting data from fi Queries entered into the query bar are also added as parameters to the URL, so it's easy to share a specific query or view with others. -In the screenshot below, you can begin to see some of the transaction fields available for filtering on: +You can begin to see some of the transaction fields available for filtering: [role="screenshot"] image::apm/images/apm-query-bar.png[Example of the Kibana Query bar in APM app in Kibana] diff --git a/docs/apm/spans.asciidoc b/docs/apm/spans.asciidoc index b1d54ce49c7cd..b09de576f2d4a 100644 --- a/docs/apm/spans.asciidoc +++ b/docs/apm/spans.asciidoc @@ -12,12 +12,12 @@ This makes it useful for visualizing where the selected transaction spent most o image::apm/images/apm-transaction-sample.png[Example of distributed trace colors in the APM app in Kibana] View a span in detail by clicking on it in the timeline waterfall. -For example, in the below screenshot we've clicked on an SQL Select database query. -The information displayed includes the actual SQL that was executed, how long it took, +When you click on an SQL Select database query, +the information displayed includes the actual SQL that was executed, how long it took, and the percentage of the trace's total time. You also get a stack trace, which shows the SQL query in your code. Finally, APM knows which files are your code and which are just modules or libraries that you've installed. -These library frames will be minimized by default in order to show you the most relevant stack trace. +These library frames will be minimized by default in order to show you the most relevant stack trace. [role="screenshot"] image::apm/images/apm-span-detail.png[Example view of a span detail in the APM app in Kibana] diff --git a/docs/apm/transactions.asciidoc b/docs/apm/transactions.asciidoc index 9c21a569f152c..536ab2ec29c80 100644 --- a/docs/apm/transactions.asciidoc +++ b/docs/apm/transactions.asciidoc @@ -50,7 +50,7 @@ If there's a particular endpoint you're worried about, you can click on it to vi [IMPORTANT] ==== If you only see one route in the Transactions table, or if you have transactions named "unknown route", -it could be a symptom that the agent either wasn't installed correctly or doesn't support your framework. +it could be a symptom that the agent either wasn't installed correctly or doesn't support your framework. For further details, including troubleshooting and custom implementation instructions, refer to the documentation for each {apm-agents-ref}[APM Agent] you've implemented. @@ -103,9 +103,7 @@ The number of requests per bucket is displayed when hovering over the graph, and [role="screenshot"] image::apm/images/apm-transaction-duration-dist.png[Example view of transactions duration distribution graph] -Let's look at an example. -In the screenshot below, -you'll notice most of the requests fall into buckets on the left side of the graph, +Most of the requests fall into buckets on the left side of the graph, with a long tail of smaller buckets to the right. This is a typical distribution, and indicates most of our requests were served quickly - awesome! It's the requests on the right, the ones taking longer than average, that we probably want to focus on. @@ -133,4 +131,4 @@ For a particular transaction sample, we can get even more information in the *me * Custom - You can configure your agent to add custom contextual information on transactions. TIP: All of this data is stored in documents in Elasticsearch. -This means you can select "Actions - View sample document" to see the actual Elasticsearch document under the discover tab. \ No newline at end of file +This means you can select "Actions - View sample document" to see the actual Elasticsearch document under the discover tab. diff --git a/docs/canvas/canvas-tinymath-functions.asciidoc b/docs/canvas/canvas-tinymath-functions.asciidoc index 8c9f445b052a3..73808fc6625d1 100644 --- a/docs/canvas/canvas-tinymath-functions.asciidoc +++ b/docs/canvas/canvas-tinymath-functions.asciidoc @@ -3,21 +3,21 @@ === TinyMath functions TinyMath provides a set of functions that can be used with the Canvas expression -language to perform complex math calculations. Read on for detailed information about -the functions available in TinyMath, including what parameters each function accepts, +language to perform complex math calculations. Read on for detailed information about +the functions available in TinyMath, including what parameters each function accepts, the return value of that function, and examples of how each function behaves. -Most of the functions below accept arrays and apply JavaScript Math methods to -each element of that array. For the functions that accept multiple arrays as -parameters, the function generally does the calculation index by index. +Most of the functions accept arrays and apply JavaScript Math methods to +each element of that array. For the functions that accept multiple arrays as +parameters, the function generally does the calculation index by index. -Any function below can be wrapped by another function as long as the return type +Any function can be wrapped by another function as long as the return type of the inner function matches the acceptable parameter type of the outer function. [float] === abs( a ) -Calculates the absolute value of a number. For arrays, the function will be +Calculates the absolute value of a number. For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -29,7 +29,7 @@ applied index-wise to each element. |a number or an array of numbers |=== -*Returns*: `number` | `Array.`. The absolute value of `a`. Returns +*Returns*: `number` | `Array.`. The absolute value of `a`. Returns an array with the absolute values of each element if `a` is an array. *Example* @@ -43,7 +43,7 @@ abs([-1 , -2, 3, -4]) // returns [1, 2, 3, 4] [float] === add( ...args ) -Calculates the sum of one or more numbers/arrays passed into the function. If at +Calculates the sum of one or more numbers/arrays passed into the function. If at least one array of numbers is passed into the function, the function will calculate the sum by index. [cols="3*^<"] @@ -55,9 +55,9 @@ least one array of numbers is passed into the function, the function will calcul |one or more numbers or arrays of numbers |=== -*Returns*: `number` | `Array.`. The sum of all numbers in `args` if `args` -contains only numbers. Returns an array of sums of the elements at each index, -including all scalar numbers in `args` in the calculation at each index if `args` +*Returns*: `number` | `Array.`. The sum of all numbers in `args` if `args` +contains only numbers. Returns an array of sums of the elements at each index, +including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. *Throws*: `'Array length mismatch'` if `args` contains arrays of different lengths @@ -73,7 +73,7 @@ add([1, 2], 3, [4, 5], 6) // returns [(1 + 3 + 4 + 6), (2 + 3 + 5 + 6)] = [14, 1 [float] === cbrt( a ) -Calculates the cube root of a number. For arrays, the function will be applied +Calculates the cube root of a number. For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -85,7 +85,7 @@ index-wise to each element. |a number or an array of numbers |=== -*Returns*: `number` | `Array.`. The cube root of `a`. Returns an array with +*Returns*: `number` | `Array.`. The cube root of `a`. Returns an array with the cube roots of each element if `a` is an array. *Example* @@ -99,7 +99,7 @@ cbrt([27, 64, 125]) // returns [3, 4, 5] [float] === ceil( a ) -Calculates the ceiling of a number, i.e., rounds a number towards positive infinity. +Calculates the ceiling of a number, i.e., rounds a number towards positive infinity. For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -111,7 +111,7 @@ For arrays, the function will be applied index-wise to each element. |a number or an array of numbers |=== -*Returns*: `number` | `Array.`. The ceiling of `a`. Returns an array with +*Returns*: `number` | `Array.`. The ceiling of `a`. Returns an array with the ceilings of each element if `a` is an array. *Example* @@ -125,7 +125,7 @@ ceil([1.1, 2.2, 3.3]) // returns [2, 3, 4] [float] === clamp( ...a, min, max ) -Restricts value to a given range and returns closed available value. If only `min` +Restricts value to a given range and returns closed available value. If only `min` is provided, values are restricted to only a lower bound. [cols="3*^<"] @@ -145,11 +145,11 @@ is provided, values are restricted to only a lower bound. |(optional) The maximum value this function will return. |=== -*Returns*: `number` | `Array.`. The closest value between `min` (inclusive) -and `max` (inclusive). Returns an array with values greater than or equal to `min` +*Returns*: `number` | `Array.`. The closest value between `min` (inclusive) +and `max` (inclusive). Returns an array with values greater than or equal to `min` and less than or equal to `max` (if provided) at each index. -*Throws*: +*Throws*: - `'Array length mismatch'` if a `min` and/or `max` are arrays of different lengths @@ -194,7 +194,7 @@ count(100) // returns 1 [float] === cube( a ) -Calculates the cube of a number. For arrays, the function will be applied +Calculates the cube of a number. For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -206,7 +206,7 @@ index-wise to each element. |a number or an array of numbers |=== -*Returns*: `number` | `Array.`. The cube of `a`. Returns an array +*Returns*: `number` | `Array.`. The cube of `a`. Returns an array with the cubes of each element if `a` is an array. *Example* @@ -219,7 +219,7 @@ cube([3, 4, 5]) // returns [27, 64, 125] [float] === divide( a, b ) -Divides two numbers. If at least one array of numbers is passed into the function, +Divides two numbers. If at least one array of numbers is passed into the function, the function will be applied index-wise to each element. [cols="3*^<"] @@ -235,8 +235,8 @@ the function will be applied index-wise to each element. |divisor, a number or an array of numbers, b != 0 |=== -*Returns*: `number` | `Array.`. Returns the quotient of `a` and `b` -if both are numbers. Returns an array with the quotients applied index-wise to +*Returns*: `number` | `Array.`. Returns the quotient of `a` and `b` +if both are numbers. Returns an array with the quotients applied index-wise to each element if `a` or `b` is an array. *Throws*: @@ -257,7 +257,7 @@ divide([14, 42, 65, 108], [2, 7, 5, 12]) // returns [7, 6, 13, 9] [float] === exp( a ) -Calculates _e^x_ where _e_ is Euler's number. For arrays, the function will be applied +Calculates _e^x_ where _e_ is Euler's number. For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -269,7 +269,7 @@ index-wise to each element. |a number or an array of numbers |=== -*Returns*: `number` | `Array.`. Returns an array with the values of +*Returns*: `number` | `Array.`. Returns an array with the values of `e^x` evaluated where `x` is each element of `a` if `a` is an array. *Example* @@ -282,7 +282,7 @@ exp([1, 2, 3]) // returns [e^1, e^2, e^3] = [2.718281828459045, 7.38905609893064 [float] === first( a ) -Returns the first element of an array. If anything other than an array is passed +Returns the first element of an array. If anything other than an array is passed in, the input is returned. [cols="3*^<"] @@ -306,7 +306,7 @@ first([1, 2, 3]) // returns 1 [float] === fix( a ) -Calculates the fix of a number, i.e., rounds a number towards 0. For arrays, the +Calculates the fix of a number, i.e., rounds a number towards 0. For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -318,7 +318,7 @@ function will be applied index-wise to each element. |a number or an array of numbers |=== -*Returns*: `number` | `Array.`. The fix of `a`. Returns an array with +*Returns*: `number` | `Array.`. The fix of `a`. Returns an array with the fixes for each element if `a` is an array. *Example* @@ -332,7 +332,7 @@ fix([1.8, 2.9, -3.7, -4.6]) // returns [1, 2, -3, -4] [float] === floor( a ) -Calculates the floor of a number, i.e., rounds a number towards negative infinity. +Calculates the floor of a number, i.e., rounds a number towards negative infinity. For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -344,7 +344,7 @@ For arrays, the function will be applied index-wise to each element. |a number or an array of numbers |=== -*Returns*: `number` | `Array.`. The floor of `a`. Returns an array +*Returns*: `number` | `Array.`. The floor of `a`. Returns an array with the floor of each element if `a` is an array. *Example* @@ -358,7 +358,7 @@ floor([1.7, 2.8, 3.9]) // returns [1, 2, 3] [float] === last( a ) -Returns the last element of an array. If anything other than an array is passed +Returns the last element of an array. If anything other than an array is passed in, the input is returned. [cols="3*^<"] @@ -382,7 +382,7 @@ last([1, 2, 3]) // returns 3 [float] === log( a, b ) -Calculates the logarithm of a number. For arrays, the function will be applied +Calculates the logarithm of a number. For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -398,7 +398,7 @@ index-wise to each element. |(optional) base for the logarithm. If not provided a value, the default base is e, and the natural log is calculated. |=== -*Returns*: `number` | `Array.`. The logarithm of `a`. Returns an array +*Returns*: `number` | `Array.`. The logarithm of `a`. Returns an array with the the logarithms of each element if `a` is an array. *Throws*: @@ -419,7 +419,7 @@ log([2, 4, 8, 16, 32], 2) // returns [1, 2, 3, 4, 5] [float] === log10( a ) -Calculates the logarithm base 10 of a number. For arrays, the function will be +Calculates the logarithm base 10 of a number. For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -431,7 +431,7 @@ applied index-wise to each element. |a number or an array of numbers, `a` must be greater than 0 |=== -*Returns*: `number` | `Array.`. The logarithm of `a`. Returns an array +*Returns*: `number` | `Array.`. The logarithm of `a`. Returns an array with the the logarithms base 10 of each element if `a` is an array. *Throws*: `'Must be greater than 0'` if `a` < 0 @@ -448,8 +448,8 @@ log([10, 100, 1000, 10000, 100000]) // returns [1, 2, 3, 4, 5] [float] === max( ...args ) -Finds the maximum value of one of more numbers/arrays of numbers passed into the function. -If at least one array of numbers is passed into the function, the function will +Finds the maximum value of one of more numbers/arrays of numbers passed into the function. +If at least one array of numbers is passed into the function, the function will find the maximum by index. [cols="3*^<"] @@ -461,9 +461,9 @@ find the maximum by index. |one or more numbers or arrays of numbers |=== -*Returns*: `number` | `Array.`. The maximum value of all numbers if -`args` contains only numbers. Returns an array with the the maximum values at each -index, including all scalar numbers in `args` in the calculation at each index if +*Returns*: `number` | `Array.`. The maximum value of all numbers if +`args` contains only numbers. Returns an array with the the maximum values at each +index, including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. *Throws*: `'Array length mismatch'` if `args` contains arrays of different lengths @@ -479,8 +479,8 @@ max([1, 9], 4, [3, 5]) // returns [max([1, 4, 3]), max([9, 4, 5])] = [4, 9] [float] === mean( ...args ) -Finds the mean value of one of more numbers/arrays of numbers passed into the function. -If at least one array of numbers is passed into the function, the function will +Finds the mean value of one of more numbers/arrays of numbers passed into the function. +If at least one array of numbers is passed into the function, the function will find the mean by index. [cols="3*^<"] @@ -492,9 +492,9 @@ find the mean by index. |one or more numbers or arrays of numbers |=== -*Returns*: `number` | `Array.`. The maximum value of all numbers if -`args` contains only numbers. Returns an array with the the maximum values at each -index, including all scalar numbers in `args` in the calculation at each index if +*Returns*: `number` | `Array.`. The maximum value of all numbers if +`args` contains only numbers. Returns an array with the the maximum values at each +index, including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. *Throws*: `'Array length mismatch'` if `args` contains arrays of different lengths @@ -510,8 +510,8 @@ max([1, 9], 4, [3, 5]) // returns [max([1, 4, 3]), max([9, 4, 5])] = [4, 9] [float] === mean( ...args ) -Finds the mean value of one of more numbers/arrays of numbers passed into the function. -If at least one array of numbers is passed into the function, the function will +Finds the mean value of one of more numbers/arrays of numbers passed into the function. +If at least one array of numbers is passed into the function, the function will find the mean by index. [cols="3*^<"] @@ -523,9 +523,9 @@ find the mean by index. |one or more numbers or arrays of numbers |=== -*Returns*: `number` | `Array.`. The mean value of all numbers if `args` -contains only numbers. Returns an array with the the mean values of each index, -including all scalar numbers in `args` in the calculation at each index if `args` +*Returns*: `number` | `Array.`. The mean value of all numbers if `args` +contains only numbers. Returns an array with the the mean values of each index, +including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. *Example* @@ -539,8 +539,8 @@ mean([1, 9], 5, [3, 4]) // returns [mean([1, 5, 3]), mean([9, 5, 4])] = [3, 6] [float] === median( ...args ) -Finds the median value(s) of one of more numbers/arrays of numbers passed into the function. -If at least one array of numbers is passed into the function, the function will +Finds the median value(s) of one of more numbers/arrays of numbers passed into the function. +If at least one array of numbers is passed into the function, the function will find the median by index. [cols="3*^<"] @@ -552,9 +552,9 @@ find the median by index. |one or more numbers or arrays of numbers |=== -*Returns*: `number` | `Array.`. The median value of all numbers if `args` -contains only numbers. Returns an array with the the median values of each index, -including all scalar numbers in `args` in the calculation at each index if `args` +*Returns*: `number` | `Array.`. The median value of all numbers if `args` +contains only numbers. Returns an array with the the median values of each index, +including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. *Example* @@ -569,8 +569,8 @@ median([1, 9], 2, 4, [3, 5]) // returns [median([1, 2, 4, 3]), median([9, 2, 4, [float] === min( ...args ) -Finds the minimum value of one of more numbers/arrays of numbers passed into the function. -If at least one array of numbers is passed into the function, the function will +Finds the minimum value of one of more numbers/arrays of numbers passed into the function. +If at least one array of numbers is passed into the function, the function will find the minimum by index. [cols="3*^<"] @@ -582,9 +582,9 @@ find the minimum by index. |one or more numbers or arrays of numbers |=== -*Returns*: `number` | `Array.`. The minimum value of all numbers if -`args` contains only numbers. Returns an array with the the minimum values of each -index, including all scalar numbers in `args` in the calculation at each index if `a` +*Returns*: `number` | `Array.`. The minimum value of all numbers if +`args` contains only numbers. Returns an array with the the minimum values of each +index, including all scalar numbers in `args` in the calculation at each index if `a` is an array. *Throws*: `'Array length mismatch'` if `args` contains arrays of different lengths. @@ -600,7 +600,7 @@ min([1, 9], 4, [3, 5]) // returns [min([1, 4, 3]), min([9, 4, 5])] = [1, 4] [float] === mod( a, b ) -Remainder after dividing two numbers. If at least one array of numbers is passed +Remainder after dividing two numbers. If at least one array of numbers is passed into the function, the function will be applied index-wise to each element. [cols="3*^<"] @@ -616,8 +616,8 @@ into the function, the function will be applied index-wise to each element. |divisor, a number or an array of numbers, b != 0 |=== -*Returns*: `number` | `Array.`. The remainder of `a` divided by `b` if -both are numbers. Returns an array with the the remainders applied index-wise to +*Returns*: `number` | `Array.`. The remainder of `a` divided by `b` if +both are numbers. Returns an array with the the remainders applied index-wise to each element if `a` or `b` is an array. *Throws*: @@ -638,8 +638,8 @@ mod([14, 42, 65, 108], [5, 4, 14, 2]) // returns [5, 2, 9, 0] [float] === mode( ...args ) -Finds the mode value(s) of one of more numbers/arrays of numbers passed into the function. -If at least one array of numbers is passed into the function, the function will +Finds the mode value(s) of one of more numbers/arrays of numbers passed into the function. +If at least one array of numbers is passed into the function, the function will find the mode by index. [cols="3*^<"] @@ -651,9 +651,9 @@ find the mode by index. |one or more numbers or arrays of numbers |=== -*Returns*: `number` | `Array.>`. An array of mode value(s) of all -numbers if `args` contains only numbers. Returns an array of arrays with mode value(s) -of each index, including all scalar numbers in `args` in the calculation at each index +*Returns*: `number` | `Array.>`. An array of mode value(s) of all +numbers if `args` contains only numbers. Returns an array of arrays with mode value(s) +of each index, including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. *Example* @@ -668,7 +668,7 @@ mode([1, 9], 1, 4, [3, 5]) // returns [mode([1, 1, 4, 3]), mode([9, 1, 4, 5])] = [float] === multiply( a, b ) -Multiplies two numbers. If at least one array of numbers is passed into the function, +Multiplies two numbers. If at least one array of numbers is passed into the function, the function will be applied index-wise to each element. [cols="3*^<"] @@ -684,11 +684,11 @@ the function will be applied index-wise to each element. |a number or an array of numbers |=== -*Returns*: `number` | `Array.`. The product of `a` and `b` if both are -numbers. Returns an array with the the products applied index-wise to each element +*Returns*: `number` | `Array.`. The product of `a` and `b` if both are +numbers. Returns an array with the the products applied index-wise to each element if `a` or `b` is an array. -*Throws*: `'Array length mismatch'` if `a` and `b` are arrays with different lengths +*Throws*: `'Array length mismatch'` if `a` and `b` are arrays with different lengths *Example* [source, js] @@ -702,7 +702,7 @@ multiply([1, 2, 3, 4], [2, 7, 5, 12]) // returns [2, 14, 15, 48] [float] === pow( a, b ) -Calculates the cube root of a number. For arrays, the function will be applied +Calculates the cube root of a number. For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -718,7 +718,7 @@ index-wise to each element. |the power that `a` is raised to |=== -*Returns*: `number` | `Array.`. `a` raised to the power of `b`. Returns +*Returns*: `number` | `Array.`. `a` raised to the power of `b`. Returns an array with the each element raised to the power of `b` if `a` is an array. *Throws*: `'Missing exponent'` if `b` is not provided @@ -733,8 +733,8 @@ pow([1, 2, 3], 4) // returns [1, 16, 81] [float] === random( a, b ) -Generates a random number within the given range where the lower bound is inclusive -and the upper bound is exclusive. If no numbers are passed in, it will return a +Generates a random number within the given range where the lower bound is inclusive +and the upper bound is exclusive. If no numbers are passed in, it will return a number between 0 and 1. If only one number is passed in, it will return a number between 0 and the number passed in. @@ -751,11 +751,11 @@ between 0 and the number passed in. |(optional) must be greater than `a` |=== -*Returns*: `number`. A random number between 0 and 1 if no numbers are passed in. -Returns a random number between 0 and `a` if only one number is passed in. Returns +*Returns*: `number`. A random number between 0 and 1 if no numbers are passed in. +Returns a random number between 0 and `a` if only one number is passed in. Returns a random number between `a` and `b` if two numbers are passed in. -*Throws*: `'Min must be greater than max'` if `a` < 0 when only `a` is passed in +*Throws*: `'Min must be greater than max'` if `a` < 0 when only `a` is passed in or if `a` > `b` when both `a` and `b` are passed in *Example* @@ -769,8 +769,8 @@ random(-10,10) // returns a random number between -10 (inclusive) and 10 (exclus [float] === range( ...args ) -Finds the range of one of more numbers/arrays of numbers passed into the function. If at -least one array of numbers is passed into the function, the function will find +Finds the range of one of more numbers/arrays of numbers passed into the function. If at +least one array of numbers is passed into the function, the function will find the range by index. [cols="3*^<"] @@ -782,9 +782,9 @@ the range by index. |one or more numbers or arrays of numbers |=== -*Returns*: `number` | `Array.`. The range value of all numbers if `args` -contains only numbers. Returns an array with the range values at each index, -including all scalar numbers in `args` in the calculation at each index if `args` +*Returns*: `number` | `Array.`. The range value of all numbers if `args` +contains only numbers. Returns an array with the range values at each index, +including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. *Example* @@ -798,8 +798,8 @@ range([1, 9], 4, [3, 5]) // returns [range([1, 4, 3]), range([9, 4, 5])] = [3, 5 [float] === range( ...args ) -Finds the range of one of more numbers/arrays of numbers into the function. If at -least one array of numbers is passed into the function, the function will find +Finds the range of one of more numbers/arrays of numbers into the function. If at +least one array of numbers is passed into the function, the function will find the range by index. [cols="3*^<"] @@ -811,9 +811,9 @@ the range by index. |one or more numbers or arrays of numbers |=== -*Returns*: `number` | `Array.`. The range value of all numbers if `args` -contains only numbers. Returns an array with the the range values at each index, -including all scalar numbers in `args` in the calculation at each index if `args` +*Returns*: `number` | `Array.`. The range value of all numbers if `args` +contains only numbers. Returns an array with the the range values at each index, +including all scalar numbers in `args` in the calculation at each index if `args` contains at least one array. *Example* @@ -827,7 +827,7 @@ range([1, 9], 4, [3, 5]) // returns [range([1, 4, 3]), range([9, 4, 5])] = [3, 5 [float] === round( a, b ) -Rounds a number towards the nearest integer by default, or decimal place (if passed in as `b`). +Rounds a number towards the nearest integer by default, or decimal place (if passed in as `b`). For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -843,7 +843,7 @@ For arrays, the function will be applied index-wise to each element. |(optional) number of decimal places, default value: 0 |=== -*Returns*: `number` | `Array.`. The rounded value of `a`. Returns an +*Returns*: `number` | `Array.`. The rounded value of `a`. Returns an array with the the rounded values of each element if `a` is an array. *Example* @@ -885,7 +885,7 @@ size(100) // returns 1 [float] === sqrt( a ) -Calculates the square root of a number. For arrays, the function will be applied +Calculates the square root of a number. For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -897,7 +897,7 @@ index-wise to each element. |a number or an array of numbers |=== -*Returns*: `number` | `Array.`. The square root of `a`. Returns an array +*Returns*: `number` | `Array.`. The square root of `a`. Returns an array with the the square roots of each element if `a` is an array. *Throws*: `'Unable find the square root of a negative number'` if `a` < 0 @@ -913,7 +913,7 @@ sqrt([9, 16, 25]) // returns [3, 4, 5] [float] === square( a ) -Calculates the square of a number. For arrays, the function will be applied +Calculates the square of a number. For arrays, the function will be applied index-wise to each element. [cols="3*^<"] @@ -925,7 +925,7 @@ index-wise to each element. |a number or an array of numbers |=== -*Returns*: `number` | `Array.`. The square of `a`. Returns an array +*Returns*: `number` | `Array.`. The square of `a`. Returns an array with the the squares of each element if `a` is an array. *Example* @@ -938,7 +938,7 @@ square([3, 4, 5]) // returns [9, 16, 25] [float] === subtract( a, b ) -Subtracts two numbers. If at least one array of numbers is passed into the function, +Subtracts two numbers. If at least one array of numbers is passed into the function, the function will be applied index-wise to each element. [cols="3*^<"] @@ -954,7 +954,7 @@ the function will be applied index-wise to each element. |a number or an array of numbers |=== -*Returns*: `number` | `Array.`. The difference of `a` and `b` if both are +*Returns*: `number` | `Array.`. The difference of `a` and `b` if both are numbers, or an array of differences applied index-wise to each element. *Throws*: `'Array length mismatch'` if `a` and `b` are arrays with different lengths @@ -971,11 +971,11 @@ subtract([14, 42, 65, 108], [2, 7, 5, 12]) // returns [12, 35, 52, 96] [float] === sum( ...args ) -Calculates the sum of one or more numbers/arrays passed into the function. If at -least one array is passed, the function will sum up one or more numbers/arrays of +Calculates the sum of one or more numbers/arrays passed into the function. If at +least one array is passed, the function will sum up one or more numbers/arrays of numbers and distinct values of an array. Sum accepts arrays of different lengths. -*Returns*: `number`. The sum of one or more numbers/arrays of numbers including +*Returns*: `number`. The sum of one or more numbers/arrays of numbers including distinct values in arrays *Example* @@ -992,7 +992,7 @@ sum([10, 20, 30, 40], 10, [1, 2, 3], 22) // returns sum(10, 20, 30, 40, 10, 1, 2 Counts the number of unique values in an array. -*Returns*: `number`. The number of unique values in the array. Returns 1 if `a` +*Returns*: `number`. The number of unique values in the array. Returns 1 if `a` is not an array. *Example* @@ -1003,4 +1003,3 @@ unique([]) // returns 0 unique([1, 2, 3, 4]) // returns 4 unique([1, 2, 3, 4, 2, 2, 2, 3, 4, 2, 4, 5, 2, 1, 4, 2]) // returns 5 ------------ - diff --git a/docs/dev-tools/searchprofiler/more-complicated.asciidoc b/docs/dev-tools/searchprofiler/more-complicated.asciidoc index a0771f4a0f240..338341d65924d 100644 --- a/docs/dev-tools/searchprofiler/more-complicated.asciidoc +++ b/docs/dev-tools/searchprofiler/more-complicated.asciidoc @@ -25,11 +25,11 @@ POST test/_bulk // CONSOLE -- -. From the {searchprofiler}, enter "test" in the *Index* field to restrict profiled +. From the {searchprofiler}, enter "test" in the *Index* field to restrict profiled queries to the `test` index. . Replace the default `match_all` query in the query editor with a query that has two sub-query -components and includes a simple aggregation, like the example below. +components and includes a simple aggregation: + -- [source,js] diff --git a/docs/developer/core/development-functional-tests.asciidoc b/docs/developer/core/development-functional-tests.asciidoc index 77a2bfe77b4ab..51b5273851ce7 100644 --- a/docs/developer/core/development-functional-tests.asciidoc +++ b/docs/developer/core/development-functional-tests.asciidoc @@ -69,7 +69,7 @@ node scripts/functional_tests_server.js node ../scripts/functional_test_runner.js ---------- -** Selenium tests are run in headless mode on CI. Locally the same tests will be executed in a real browser. You can activate headless mode by setting the environment variable below: +** Selenium tests are run in headless mode on CI. Locally the same tests will be executed in a real browser. You can activate headless mode by setting the environment variable: + ["source", "shell"] ---------- @@ -178,10 +178,29 @@ To run tests on Firefox locally, use `config.firefox.js`: node scripts/functional_test_runner --config test/functional/config.firefox.js ----------- +[float] +===== Using the test_user service + +Tests should run at the positive security boundry condition, meaning that they should be run with the mimimum privileges required (and documented) and not as the superuser. + This prevents the type of regression where additional privleges accidentally become required to perform the same action. + +The functional UI tests now default to logging in with a user named `test_user` and the roles of this user can be changed dynamically without logging in and out. + +In order to achieve this a new service was introduced called `createTestUserService` (see `test/common/services/security/test_user.ts`). The purpose of this test user service is to create roles defined in the test config files and setRoles() or restoreDefaults(). + +An example of how to set the role like how its defined below: + +`await security.testUser.setRoles(['kibana_user', 'kibana_date_nanos']);` + +Here we are setting the `test_user` to have the `kibana_user` role and also role access to a specific data index (`kibana_date_nanos`). + +Tests should normally setRoles() in the before() and restoreDefaults() in the after(). + + [float] ===== Anatomy of a test file -The annotated example file below shows the basic structure every test suite uses. It starts by importing https://github.com/elastic/kibana/tree/master/packages/kbn-expect[`@kbn/expect`] and defining its default export: an anonymous Test Provider. The test provider then destructures the Provider API for the `getService()` and `getPageObjects()` functions. It uses these functions to collect the dependencies of this suite. The rest of the test file will look pretty normal to mocha.js users. `describe()`, `it()`, `before()` and the lot are used to define suites that happen to automate a browser via services and objects of type `PageObject`. +This annotated example file shows the basic structure every test suite uses. It starts by importing https://github.com/elastic/kibana/tree/master/packages/kbn-expect[`@kbn/expect`] and defining its default export: an anonymous Test Provider. The test provider then destructures the Provider API for the `getService()` and `getPageObjects()` functions. It uses these functions to collect the dependencies of this suite. The rest of the test file will look pretty normal to mocha.js users. `describe()`, `it()`, `before()` and the lot are used to define suites that happen to automate a browser via services and objects of type `PageObject`. ["source","js"] ---- diff --git a/docs/developer/plugin/development-plugin-feature-registration.asciidoc b/docs/developer/plugin/development-plugin-feature-registration.asciidoc index 2c686964d369a..ca61e5309ce85 100644 --- a/docs/developer/plugin/development-plugin-feature-registration.asciidoc +++ b/docs/developer/plugin/development-plugin-feature-registration.asciidoc @@ -46,7 +46,7 @@ Registering a feature consists of the following fields. For more information, co |`privileges` (required) |{repo}blob/{branch}/x-pack/plugins/features/server/feature.ts[`FeatureWithAllOrReadPrivileges`]. -|see examples below +|See <> and <> |The set of privileges this feature requires to function. |`icon` @@ -80,6 +80,7 @@ if (canUserSave) { } ----------- +[[example-1-canvas]] ==== Example 1: Canvas Application ["source","javascript"] ----------- @@ -134,6 +135,7 @@ if (canUserSave) { Because the `read` privilege does not define the `save` capability, users with read-only access will have their `uiCapabilities.canvas.save` flag set to `false`. +[[example-2-dev-tools]] ==== Example 2: Dev Tools ["source","javascript"] diff --git a/docs/developer/plugin/development-plugin-localization.asciidoc b/docs/developer/plugin/development-plugin-localization.asciidoc index 78ee933f681f4..1fb8b6aa0cbde 100644 --- a/docs/developer/plugin/development-plugin-localization.asciidoc +++ b/docs/developer/plugin/development-plugin-localization.asciidoc @@ -161,7 +161,7 @@ Full details are {repo}tree/master/packages/kbn-i18n#angularjs[here]. To learn more about i18n tooling, see {blob}src/dev/i18n/README.md[i18n dev tooling]. -To learn more about implementing i18n in the UI, follow the links below: +To learn more about implementing i18n in the UI, use the following links: * {blob}packages/kbn-i18n/README.md[i18n plugin] * {blob}packages/kbn-i18n/GUIDELINE.md[i18n guidelines] diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.getiscollapsed_.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md similarity index 51% rename from docs/development/core/public/kibana-plugin-core-public.chromestart.getiscollapsed_.md rename to docs/development/core/public/kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md index 205f863526e22..78a4442a651e6 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.getiscollapsed_.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md @@ -1,15 +1,15 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeStart](./kibana-plugin-core-public.chromestart.md) > [getIsCollapsed$](./kibana-plugin-core-public.chromestart.getiscollapsed_.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeStart](./kibana-plugin-core-public.chromestart.md) > [getIsNavDrawerLocked$](./kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md) -## ChromeStart.getIsCollapsed$() method +## ChromeStart.getIsNavDrawerLocked$() method -Get an observable of the current collapsed state of the chrome. +Get an observable of the current locked state of the nav drawer. Signature: ```typescript -getIsCollapsed$(): Observable; +getIsNavDrawerLocked$(): Observable; ``` Returns: diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.md index 7d9d47df544d0..c179e089d7cfd 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.md @@ -56,7 +56,7 @@ core.chrome.setHelpExtension(elem => { | [getBrand$()](./kibana-plugin-core-public.chromestart.getbrand_.md) | Get an observable of the current brand information. | | [getBreadcrumbs$()](./kibana-plugin-core-public.chromestart.getbreadcrumbs_.md) | Get an observable of the current list of breadcrumbs | | [getHelpExtension$()](./kibana-plugin-core-public.chromestart.gethelpextension_.md) | Get an observable of the current custom help conttent | -| [getIsCollapsed$()](./kibana-plugin-core-public.chromestart.getiscollapsed_.md) | Get an observable of the current collapsed state of the chrome. | +| [getIsNavDrawerLocked$()](./kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md) | Get an observable of the current locked state of the nav drawer. | | [getIsVisible$()](./kibana-plugin-core-public.chromestart.getisvisible_.md) | Get an observable of the current visibility state of the chrome. | | [removeApplicationClass(className)](./kibana-plugin-core-public.chromestart.removeapplicationclass.md) | Remove a className added with addApplicationClass(). If className is unknown it is ignored. | | [setAppTitle(appTitle)](./kibana-plugin-core-public.chromestart.setapptitle.md) | Sets the current app's title | @@ -65,6 +65,5 @@ core.chrome.setHelpExtension(elem => { | [setBreadcrumbs(newBreadcrumbs)](./kibana-plugin-core-public.chromestart.setbreadcrumbs.md) | Override the current set of breadcrumbs | | [setHelpExtension(helpExtension)](./kibana-plugin-core-public.chromestart.sethelpextension.md) | Override the current set of custom help content | | [setHelpSupportUrl(url)](./kibana-plugin-core-public.chromestart.sethelpsupporturl.md) | Override the default support URL shown in the help menu | -| [setIsCollapsed(isCollapsed)](./kibana-plugin-core-public.chromestart.setiscollapsed.md) | Set the collapsed state of the chrome navigation. | | [setIsVisible(isVisible)](./kibana-plugin-core-public.chromestart.setisvisible.md) | Set the temporary visibility for the chrome. This does nothing if the chrome is hidden by default and should be used to hide the chrome for things like full-screen modes with an exit button. | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.setiscollapsed.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.setiscollapsed.md deleted file mode 100644 index b1843ef326d96..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.setiscollapsed.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeStart](./kibana-plugin-core-public.chromestart.md) > [setIsCollapsed](./kibana-plugin-core-public.chromestart.setiscollapsed.md) - -## ChromeStart.setIsCollapsed() method - -Set the collapsed state of the chrome navigation. - -Signature: - -```typescript -setIsCollapsed(isCollapsed: boolean): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| isCollapsed | boolean | | - -Returns: - -`void` - diff --git a/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.getall.md b/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.getall.md index 805ac57b2fb9a..004979977376e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.getall.md +++ b/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.getall.md @@ -9,5 +9,5 @@ Gets the metadata about all uiSettings, including the type, default value, and u Signature: ```typescript -getAll: () => Readonly>; +getAll: () => Readonly>; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.md b/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.md index da566ed25cff5..87ef5784a6c6d 100644 --- a/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.md +++ b/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.md @@ -18,7 +18,7 @@ export interface IUiSettingsClient | --- | --- | --- | | [get](./kibana-plugin-core-public.iuisettingsclient.get.md) | <T = any>(key: string, defaultOverride?: T) => T | Gets the value for a specific uiSetting. If this setting has no user-defined value then the defaultOverride parameter is returned (and parsed if setting is of type "json" or "number). If the parameter is not defined and the key is not registered by any plugin then an error is thrown, otherwise reads the default value defined by a plugin. | | [get$](./kibana-plugin-core-public.iuisettingsclient.get_.md) | <T = any>(key: string, defaultOverride?: T) => Observable<T> | Gets an observable of the current value for a config key, and all updates to that config key in the future. Providing a defaultOverride argument behaves the same as it does in \#get() | -| [getAll](./kibana-plugin-core-public.iuisettingsclient.getall.md) | () => Readonly<Record<string, UiSettingsParams & UserProvidedValues>> | Gets the metadata about all uiSettings, including the type, default value, and user value for each key. | +| [getAll](./kibana-plugin-core-public.iuisettingsclient.getall.md) | () => Readonly<Record<string, PublicUiSettingsParams & UserProvidedValues>> | Gets the metadata about all uiSettings, including the type, default value, and user value for each key. | | [getSaved$](./kibana-plugin-core-public.iuisettingsclient.getsaved_.md) | <T = any>() => Observable<{
key: string;
newValue: T;
oldValue: T;
}> | Returns an Observable that notifies subscribers of each update to the uiSettings, including the key, newValue, and oldValue of the setting that changed. | | [getUpdate$](./kibana-plugin-core-public.iuisettingsclient.getupdate_.md) | <T = any>() => Observable<{
key: string;
newValue: T;
oldValue: T;
}> | Returns an Observable that notifies subscribers of each update to the uiSettings, including the key, newValue, and oldValue of the setting that changed. | | [getUpdateErrors$](./kibana-plugin-core-public.iuisettingsclient.getupdateerrors_.md) | () => Observable<Error> | Returns an Observable that notifies subscribers of each error while trying to update the settings, containing the actual Error class. | diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index bafc2eb3a4bc9..a9fbaa25ea150 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -147,6 +147,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [MountPoint](./kibana-plugin-core-public.mountpoint.md) | A function that should mount DOM content inside the provided container element and return a handler to unmount it. | | [PluginInitializer](./kibana-plugin-core-public.plugininitializer.md) | The plugin export at the root of a plugin's public directory should conform to this interface. | | [PluginOpaqueId](./kibana-plugin-core-public.pluginopaqueid.md) | | +| [PublicUiSettingsParams](./kibana-plugin-core-public.publicuisettingsparams.md) | A sub-set of [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) exposed to the client-side. | | [RecursiveReadonly](./kibana-plugin-core-public.recursivereadonly.md) | | | [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-core-public.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | diff --git a/docs/development/core/public/kibana-plugin-core-public.publicuisettingsparams.md b/docs/development/core/public/kibana-plugin-core-public.publicuisettingsparams.md new file mode 100644 index 0000000000000..678a69289ff23 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.publicuisettingsparams.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [PublicUiSettingsParams](./kibana-plugin-core-public.publicuisettingsparams.md) + +## PublicUiSettingsParams type + +A sub-set of [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) exposed to the client-side. + +Signature: + +```typescript +export declare type PublicUiSettingsParams = Omit; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobject.md b/docs/development/core/public/kibana-plugin-core-public.savedobject.md index 9ced619ad4bfe..c6bc13b98bc06 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobject.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobject.md @@ -4,7 +4,6 @@ ## SavedObject interface - Signature: ```typescript diff --git a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md index 00f1c0f0deca5..e7facb4a109cd 100644 --- a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md +++ b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md @@ -9,7 +9,7 @@ UiSettings parameters defined by the plugins. Signature: ```typescript -export interface UiSettingsParams +export interface UiSettingsParams ``` ## Properties @@ -24,7 +24,8 @@ export interface UiSettingsParams | [options](./kibana-plugin-core-public.uisettingsparams.options.md) | string[] | array of permitted values for this setting | | [readonly](./kibana-plugin-core-public.uisettingsparams.readonly.md) | boolean | a flag indicating that value cannot be changed | | [requiresPageReload](./kibana-plugin-core-public.uisettingsparams.requirespagereload.md) | boolean | a flag indicating whether new value applying requires page reloading | +| [schema](./kibana-plugin-core-public.uisettingsparams.schema.md) | Type<T> | | | [type](./kibana-plugin-core-public.uisettingsparams.type.md) | UiSettingsType | defines a type of UI element [UiSettingsType](./kibana-plugin-core-public.uisettingstype.md) | | [validation](./kibana-plugin-core-public.uisettingsparams.validation.md) | ImageValidation | StringValidation | | -| [value](./kibana-plugin-core-public.uisettingsparams.value.md) | SavedObjectAttribute | default value to fall back to if a user doesn't provide any | +| [value](./kibana-plugin-core-public.uisettingsparams.value.md) | T | default value to fall back to if a user doesn't provide any | diff --git a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.schema.md b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.schema.md new file mode 100644 index 0000000000000..f90d5161f96a9 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.schema.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) > [schema](./kibana-plugin-core-public.uisettingsparams.schema.md) + +## UiSettingsParams.schema property + +Signature: + +```typescript +schema: Type; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.value.md b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.value.md index 8775588290d70..2740f169eeecb 100644 --- a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.value.md +++ b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.value.md @@ -9,5 +9,5 @@ default value to fall back to if a user doesn't provide any Signature: ```typescript -value?: SavedObjectAttribute; +value?: T; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.getregistered.md b/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.getregistered.md index 2ca6b4cbe1589..71a2bbf88472e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.getregistered.md +++ b/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.getregistered.md @@ -9,5 +9,5 @@ Returns registered uiSettings values [UiSettingsParams](./kibana-plugin-core-ser Signature: ```typescript -getRegistered: () => Readonly>; +getRegistered: () => Readonly>; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md b/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md index 42fcc81419cbe..af99b5e5bb215 100644 --- a/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md @@ -18,7 +18,7 @@ export interface IUiSettingsClient | --- | --- | --- | | [get](./kibana-plugin-core-server.iuisettingsclient.get.md) | <T = any>(key: string) => Promise<T> | Retrieves uiSettings values set by the user with fallbacks to default values if not specified. | | [getAll](./kibana-plugin-core-server.iuisettingsclient.getall.md) | <T = any>() => Promise<Record<string, T>> | Retrieves a set of all uiSettings values set by the user with fallbacks to default values if not specified. | -| [getRegistered](./kibana-plugin-core-server.iuisettingsclient.getregistered.md) | () => Readonly<Record<string, UiSettingsParams>> | Returns registered uiSettings values [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) | +| [getRegistered](./kibana-plugin-core-server.iuisettingsclient.getregistered.md) | () => Readonly<Record<string, PublicUiSettingsParams>> | Returns registered uiSettings values [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) | | [getUserProvided](./kibana-plugin-core-server.iuisettingsclient.getuserprovided.md) | <T = any>() => Promise<Record<string, UserProvidedValues<T>>> | Retrieves a set of all uiSettings values set by the user. | | [isOverridden](./kibana-plugin-core-server.iuisettingsclient.isoverridden.md) | (key: string) => boolean | Shows whether the uiSettings value set by the user. | | [remove](./kibana-plugin-core-server.iuisettingsclient.remove.md) | (key: string) => Promise<void> | Removes uiSettings value by key. | diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 8a88329031e1f..54cf496b2d6af 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -232,6 +232,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginInitializer](./kibana-plugin-core-server.plugininitializer.md) | The plugin export at the root of a plugin's server directory should conform to this interface. | | [PluginName](./kibana-plugin-core-server.pluginname.md) | Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays that use it as a key or value more obvious. | | [PluginOpaqueId](./kibana-plugin-core-server.pluginopaqueid.md) | | +| [PublicUiSettingsParams](./kibana-plugin-core-server.publicuisettingsparams.md) | A sub-set of [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) exposed to the client-side. | | [RecursiveReadonly](./kibana-plugin-core-server.recursivereadonly.md) | | | [RedirectResponseOptions](./kibana-plugin-core-server.redirectresponseoptions.md) | HTTP response parameters for redirection response | | [RequestHandler](./kibana-plugin-core-server.requesthandler.md) | A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) functions. | diff --git a/docs/development/core/server/kibana-plugin-core-server.publicuisettingsparams.md b/docs/development/core/server/kibana-plugin-core-server.publicuisettingsparams.md new file mode 100644 index 0000000000000..4ccc91fbe1f74 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.publicuisettingsparams.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [PublicUiSettingsParams](./kibana-plugin-core-server.publicuisettingsparams.md) + +## PublicUiSettingsParams type + +A sub-set of [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) exposed to the client-side. + +Signature: + +```typescript +export declare type PublicUiSettingsParams = Omit; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.routeconfig.validate.md b/docs/development/core/server/kibana-plugin-core-server.routeconfig.validate.md index 204d8a786fede..3bbabc04f2500 100644 --- a/docs/development/core/server/kibana-plugin-core-server.routeconfig.validate.md +++ b/docs/development/core/server/kibana-plugin-core-server.routeconfig.validate.md @@ -14,7 +14,7 @@ validate: RouteValidatorFullConfig | false; ## Remarks -You \*must\* specify a validation schema to be able to read: - url path segments - request query - request body To opt out of validating the request, specify `validate: false`. In this case request params, query, and body will be \*\*empty\*\* objects and have no access to raw values. In some cases you may want to use another validation library. To do this, you need to instruct the `@kbn/config-schema` library to output \*\*non-validated values\*\* with setting schema as `schema.object({}, { allowUnknowns: true })`; +You \*must\* specify a validation schema to be able to read: - url path segments - request query - request body To opt out of validating the request, specify `validate: false`. In this case request params, query, and body will be \*\*empty\*\* objects and have no access to raw values. In some cases you may want to use another validation library. To do this, you need to instruct the `@kbn/config-schema` library to output \*\*non-validated values\*\* with setting schema as `schema.object({}, { unknowns: 'allow' })`; ## Example @@ -49,7 +49,7 @@ router.get({ path: 'path/{id}', validate: { // handler has access to raw non-validated params in runtime - params: schema.object({}, { allowUnknowns: true }) + params: schema.object({}, { unknowns: 'allow' }) }, }, (context, req, res,) { diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobject.md index ebb105c846aff..0df97b0d4221a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobject.md @@ -4,7 +4,6 @@ ## SavedObject interface - Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md index fe6e5d956f3e2..f134decb5102b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md +++ b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md @@ -9,7 +9,7 @@ UiSettings parameters defined by the plugins. Signature: ```typescript -export interface UiSettingsParams +export interface UiSettingsParams ``` ## Properties @@ -24,7 +24,8 @@ export interface UiSettingsParams | [options](./kibana-plugin-core-server.uisettingsparams.options.md) | string[] | array of permitted values for this setting | | [readonly](./kibana-plugin-core-server.uisettingsparams.readonly.md) | boolean | a flag indicating that value cannot be changed | | [requiresPageReload](./kibana-plugin-core-server.uisettingsparams.requirespagereload.md) | boolean | a flag indicating whether new value applying requires page reloading | +| [schema](./kibana-plugin-core-server.uisettingsparams.schema.md) | Type<T> | | | [type](./kibana-plugin-core-server.uisettingsparams.type.md) | UiSettingsType | defines a type of UI element [UiSettingsType](./kibana-plugin-core-server.uisettingstype.md) | | [validation](./kibana-plugin-core-server.uisettingsparams.validation.md) | ImageValidation | StringValidation | | -| [value](./kibana-plugin-core-server.uisettingsparams.value.md) | SavedObjectAttribute | default value to fall back to if a user doesn't provide any | +| [value](./kibana-plugin-core-server.uisettingsparams.value.md) | T | default value to fall back to if a user doesn't provide any | diff --git a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.schema.md b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.schema.md new file mode 100644 index 0000000000000..f181fbd309b7f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.schema.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) > [schema](./kibana-plugin-core-server.uisettingsparams.schema.md) + +## UiSettingsParams.schema property + +Signature: + +```typescript +schema: Type; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.value.md b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.value.md index ca00cd0cd6396..78c8f0c8fcf8d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.value.md +++ b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.value.md @@ -9,5 +9,5 @@ default value to fall back to if a user doesn't provide any Signature: ```typescript -value?: SavedObjectAttribute; +value?: T; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.icancel.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.icancel.md deleted file mode 100644 index 27141c68ae1a7..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.icancel.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ICancel](./kibana-plugin-plugins-data-server.icancel.md) - -## ICancel type - -Signature: - -```typescript -export declare type ICancel = (id: string) => Promise; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcancel.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcancel.md new file mode 100644 index 0000000000000..99c30515e8da6 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchcancel.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchCancel](./kibana-plugin-plugins-data-server.isearchcancel.md) + +## ISearchCancel type + +Signature: + +```typescript +export declare type ISearchCancel = (id: string) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index 12d53f1a35ea0..e756eb9b72905 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -67,9 +67,9 @@ | Type Alias | Description | | --- | --- | | [FieldFormatsGetConfigFn](./kibana-plugin-plugins-data-server.fieldformatsgetconfigfn.md) | | -| [ICancel](./kibana-plugin-plugins-data-server.icancel.md) | | | [IFieldFormatsRegistry](./kibana-plugin-plugins-data-server.ifieldformatsregistry.md) | | | [ISearch](./kibana-plugin-plugins-data-server.isearch.md) | | +| [ISearchCancel](./kibana-plugin-plugins-data-server.isearchcancel.md) | | | [ParsedInterval](./kibana-plugin-plugins-data-server.parsedinterval.md) | | | [TSearchStrategyProvider](./kibana-plugin-plugins-data-server.tsearchstrategyprovider.md) | Search strategy provider creates an instance of a search strategy with the request handler context bound to it. This way every search strategy can use whatever information they require from the request context. | diff --git a/docs/discover/kuery.asciidoc b/docs/discover/kuery.asciidoc index c835c15028074..48a7c65bdbf15 100644 --- a/docs/discover/kuery.asciidoc +++ b/docs/discover/kuery.asciidoc @@ -2,13 +2,13 @@ === Kibana Query Language In Kibana 6.3, we introduced a number of exciting experimental query language enhancements. These -features are now available by default in 7.0. Out of the box, Kibana's query language now includes scripted field support and a -simplified, easier to use syntax. If you have a Basic license or above, autocomplete functionality will also be enabled. +features are now available by default in 7.0. Out of the box, Kibana's query language now includes scripted field support and a +simplified, easier to use syntax. If you have a Basic license or above, autocomplete functionality will also be enabled. ==== Language Syntax -If you're familiar with Kibana's old lucene query syntax, you should feel right at home with the new syntax. The basics -stay the same, we've simply refined things to make the query language easier to use. Read about the changes below. +If you're familiar with Kibana's old Lucene query syntax, you should feel right at home with the new syntax. The basics +stay the same, we've simply refined things to make the query language easier to use. `response:200` will match documents where the response field matches the value 200. @@ -19,8 +19,8 @@ they appear. This means documents with "quick brown fox" will match, but so will to search for a phrase. The query parser will no longer split on whitespace. Multiple search terms must be separated by explicit -boolean operators. Lucene will combine search terms with an `or` by default, so `response:200 extension:php` would -become `response:200 or extension:php` in KQL. This will match documents where response matches 200, extension matches php, or both. +boolean operators. Lucene will combine search terms with an `or` by default, so `response:200 extension:php` would +become `response:200 or extension:php` in KQL. This will match documents where response matches 200, extension matches php, or both. Note that boolean operators are not case sensitive. We can make terms required by using `and`. @@ -48,9 +48,9 @@ Entire groups can also be inverted. `response:200 and not (extension:php or extension:css)` -Ranges are similar to lucene with a small syntactical difference. +Ranges are similar to lucene with a small syntactical difference. -Instead of `bytes:>1000`, we omit the colon: `bytes > 1000`. +Instead of `bytes:>1000`, we omit the colon: `bytes > 1000`. `>, >=, <, <=` are all valid range operators. @@ -76,15 +76,15 @@ in the response field, but a query for just `200` will search for 200 across all KQL supports querying on {ref}/nested.html[nested fields] through a special syntax. You can query nested fields in subtly different ways, depending on the results you want, so crafting nested queries requires extra thought. - + One main consideration is how to match parts of the nested query to the individual nested documents. There are two main approaches to take: * *Parts of the query may only match a single nested document.* This is what most users want when querying on a nested field. -* *Parts of the query can match different nested documents.* This is how a regular object field works. +* *Parts of the query can match different nested documents.* This is how a regular object field works. Although generally less useful, there might be occasions where you want to query a nested field in this way. -Let's take a look at the first approach. In the following document, `items` is a nested field. Each document in the nested +Let's take a look at the first approach. In the following document, `items` is a nested field. Each document in the nested field contains a name, stock, and category. [source,json] @@ -122,7 +122,7 @@ To find stores that have more than 10 bananas in stock, you would write a query `items:{ name:banana and stock > 10 }` -`items` is the "nested path". Everything inside the curly braces (the "nested group") must match a single nested document. +`items` is the "nested path". Everything inside the curly braces (the "nested group") must match a single nested document. The following example returns no matches because no single nested document has bananas with a stock of 9. @@ -138,7 +138,7 @@ The subqueries in this example are in separate nested groups and can match diffe ==== Combine approaches -You can combine these two approaches to create complex queries. What if you wanted to find a store with more than 10 +You can combine these two approaches to create complex queries. What if you wanted to find a store with more than 10 bananas that *also* stocks vegetables? You could do this: `items:{ name:banana and stock > 10 } and items:{ category:vegetable }` diff --git a/docs/discover/search.asciidoc b/docs/discover/search.asciidoc index 9c4e406455c27..21ae4560fba94 100644 --- a/docs/discover/search.asciidoc +++ b/docs/discover/search.asciidoc @@ -56,7 +56,7 @@ query language you can also submit queries using the {ref}/query-dsl.html[Elasti [[save-open-search]] === Saving searches -A saved search persists your current view of Discover for later retrieval and reuse. You can reload a saved search into Discover, add it to a dashboard, and use it as the basis for a <>. +A saved search persists your current view of Discover for later retrieval and reuse. You can reload a saved search into Discover, add it to a dashboard, and use it as the basis for a <>. A saved search includes the query text, filters, and optionally, the time filter. A saved search also includes the selected columns in the document table, the sort order, and the current index pattern. @@ -164,12 +164,9 @@ You can import, export, and delete saved queries from <>. +index pattern are searched. +To change the indices you are searching, click the index pattern and select a +different <>. [[autorefresh]] === Refresh the search results @@ -180,7 +177,7 @@ retrieve the latest results. . Click image:images/time-filter-calendar.png[]. -. In the *Refresh every* field, enter the refresh rate, then select the interval +. In the *Refresh every* field, enter the refresh rate, then select the interval from the dropdown. . Click *Start*. @@ -189,5 +186,5 @@ image::images/autorefresh-intervals.png[] To disable auto refresh, click *Stop*. -If auto refresh is not enabled, click *Refresh* to manually refresh the search +If auto refresh is not enabled, click *Refresh* to manually refresh the search results. diff --git a/docs/epm/index.asciidoc b/docs/epm/index.asciidoc index 46d45b85690e3..d2ebe003afd6b 100644 --- a/docs/epm/index.asciidoc +++ b/docs/epm/index.asciidoc @@ -47,12 +47,12 @@ A user-specified string that will be used to part of the index name in Elasticse ==== Package -A package contains all the assets for the Elastic Stack. A more detailed definition of a package can be found under https://github.com/elastic/package-registry . +A package contains all the assets for the Elastic Stack. A more detailed definition of a package can be found under https://github.com/elastic/package-registry. == Indexing Strategy -Ingest Management enforces an indexing strategy to allow the system to automically detect indices and run queries on it. In short the indexing strategy looks as following: +Ingest Management enforces an indexing strategy to allow the system to automatically detect indices and run queries on it. In short the indexing strategy looks as following: ``` {type}-{dataset}-{namespace} @@ -85,7 +85,7 @@ The version is included in each pipeline to allow upgrades. The pipeline itself === Templates & ILM Policies -To make the above strategy possible, alias templates are required. For each type there is a basic alias template with a default ILM policy. These default templates apply to all indices which follow the indexing strategy and do not have a more specific dataset alias template. +To make the above strategy possible, alias templates are required. For each type there is a basic alias template with a default ILM policy. These default templates apply to all indices which follow the indexing strategy and do not have a more specific dataset alias template. The `metrics` and `logs` alias template contain all the basic fields from ECS. @@ -109,7 +109,7 @@ Filtering for data in queries for example in visualizations or dashboards should === Security permissions -Security permissions can be set on different levels. To set special permissions for the access on the prod namespace an index pattern as below can be used: +Security permissions can be set on different levels. To set special permissions for the access on the prod namespace, use the following index pattern: ``` /(logs|metrics)-[^-]+-prod-$/ @@ -142,5 +142,3 @@ The new ingest pipeline is expected to still work with the data coming from olde In case of a breaking change in the data structure, the new ingest pipeline is also expected to deal with this change. In case there are breaking changes which cannot be dealt with in an ingest pipeline, a new package has to be created. Each package lists its minimal required agent version. In case there are agents enrolled with an older version, the user is notified to upgrade these agents as otherwise the new configs cannot be rolled out. - - diff --git a/docs/management/numeral.asciidoc b/docs/management/numeral.asciidoc index 65dfdab3abd3c..5d4d48ca785e1 100644 --- a/docs/management/numeral.asciidoc +++ b/docs/management/numeral.asciidoc @@ -19,7 +19,7 @@ The numeral pattern syntax expresses: Number of decimal places:: The `.` character turns on the option to show decimal places using a locale-specific decimal separator, most often `.` or `,`. To add trailing zeroes such as `5.00`, use a pattern like `0.00`. -To have optional zeroes, use the `[]` characters. Examples below. +To have optional zeroes, use the `[]` characters. Thousands separator:: The thousands separator `,` turns on the option to group thousands using a locale-specific separator. The separator is most often `,` or `.`, and sometimes ` `. diff --git a/docs/management/rollups/create_and_manage_rollups.asciidoc b/docs/management/rollups/create_and_manage_rollups.asciidoc index 565c179b741f1..6a56970687fd6 100644 --- a/docs/management/rollups/create_and_manage_rollups.asciidoc +++ b/docs/management/rollups/create_and_manage_rollups.asciidoc @@ -70,11 +70,7 @@ This allows for more granular queries, such as 2h and 12h. [float] ==== Create the rollup job -As you walk through the *Create rollup job* UI, enter the data shown in -the table below. The terms, histogram, and metrics fields reflect -the key information to retain in the rolled up data: where visitors are from (geo.src), -what operating system they are using (machine.os.keyword), -and how much data is being sent (bytes). +As you walk through the *Create rollup job* UI, enter the data: |=== |*Field* |*Value* @@ -118,6 +114,10 @@ and how much data is being sent (bytes). |bytes (average) |=== +The terms, histogram, and metrics fields reflect +the key information to retain in the rolled up data: where visitors are from (geo.src), +what operating system they are using (machine.os.keyword), +and how much data is being sent (bytes). You can now use the rolled up data for analysis at a fraction of the storage cost of the original index. The original data can live side by side with the new diff --git a/docs/maps/geojson-upload.asciidoc b/docs/maps/geojson-upload.asciidoc index 8c3cb371b6add..ad20264f56138 100644 --- a/docs/maps/geojson-upload.asciidoc +++ b/docs/maps/geojson-upload.asciidoc @@ -14,7 +14,7 @@ GeoJSON is the most commonly used and flexible option. [float] === Upload a GeoJSON file -Follow the instructions below to upload a GeoJSON data file, or try the +Follow these instructions to upload a GeoJSON data file, or try the <>. . Open *Elastic Maps*, and then click *Add layer*. diff --git a/docs/maps/indexing-geojson-data-tutorial.asciidoc b/docs/maps/indexing-geojson-data-tutorial.asciidoc index 22b736032cb79..a94e5757d5dfa 100644 --- a/docs/maps/indexing-geojson-data-tutorial.asciidoc +++ b/docs/maps/indexing-geojson-data-tutorial.asciidoc @@ -46,7 +46,7 @@ image::maps/images/fu_gs_new_england_map.png[] === Upload and index GeoJSON files For each GeoJSON file you downloaded, complete the following steps: -. Below the map legend, click *Add layer*. +. Click *Add layer*. . From the list of layer types, click *Uploaded GeoJSON*. . Using the File Picker, upload the GeoJSON file. + @@ -86,7 +86,7 @@ hot spots are. An advantage of having indexed {ref}/geo-point.html[geo_point] data for the lightning strikes is that you can perform aggregations on the data. -. Below the map legend, click *Add layer*. +. Click *Add layer*. . From the list of layer types, click *Grid aggregation*. + Because you indexed `lightning_detected.geojson` using the index name and diff --git a/docs/maps/vector-style.asciidoc b/docs/maps/vector-style.asciidoc index 509b1fae4066a..80e4c4ed5f844 100644 --- a/docs/maps/vector-style.asciidoc +++ b/docs/maps/vector-style.asciidoc @@ -12,7 +12,7 @@ For each property, you can specify whether to use a constant or data driven valu Use static styling to specificy a constant value for a style property. -The image below shows an example of static styling using the <> data set. +This image shows an example of static styling using the <> data set. The *kibana_sample_data_logs* layer uses static styling for all properties. [role="screenshot"] @@ -26,7 +26,7 @@ image::maps/images/vector_style_static.png[] Use data driven styling to symbolize features by property values. To enable data driven styling for a style property, change the selected value from *Fixed* or *Solid* to *By value*. -The image below shows an example of data driven styling using the <> data set. +This image shows an example of data driven styling using the <> data set. The *kibana_sample_data_logs* layer uses data driven styling for fill color and symbol size style properties. * The `hour_of_day` property determines the fill color for each feature based on where the value fits on a linear scale. @@ -87,7 +87,7 @@ Qualitative data driven styling is available for the following styling propertie Qualitative data driven styling uses a {ref}/search-aggregations-bucket-terms-aggregation.html[terms aggregation] to retrieve the top nine categories for the property. Feature values within the top categories are assigned a unique color. Feature values outside of the top categories are grouped into the *Other* category. A feature is assigned the *Other* category when the property value is undefined. -The image below shows an example of quantitative data driven styling using the <> data set. +This image shows an example of quantitative data driven styling using the <> data set. The `machine.os.keyword` property determines the color of each symbol based on category. [role="screenshot"] @@ -101,7 +101,7 @@ image::maps/images/quantitative_data_driven_styling.png[] Class styling symbolizes features by class and requires multiple layers. Use <> to define the class for each layer, and <> to symbolize each class. -The image below shows an example of class styling using the <> data set. +This image shows an example of class styling using the <> data set. * The *Mac OS requests* layer applies the filter `machine.os : osx` so the layer only contains Mac OS requests. The fill color is a static value of green. diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index a34f956ace263..ce4c97391f1b5 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -19,16 +19,16 @@ See also <> and <>. [float] [[breaking_80_index_pattern_changes]] -=== Index pattern changes +=== Index pattern changes [float] ==== Removed support for time-based internal index patterns -*Details:* Time-based interval index patterns were deprecated in 5.x. In 6.x, -you could no longer create time-based interval index patterns, but they continued +*Details:* Time-based interval index patterns were deprecated in 5.x. In 6.x, +you could no longer create time-based interval index patterns, but they continued to function as expected. Support for these index patterns has been removed in 8.0. -*Impact:* You must migrate your time_based index patterns to a wildcard pattern, -for example, `logstash-*`. +*Impact:* You must migrate your time_based index patterns to a wildcard pattern, +for example, `logstash-*`. [float] @@ -76,7 +76,7 @@ specified explicitly. [float] ==== `/api/security/v1/saml` endpoint is no longer supported -*Details:* The deprecated `/api/security/v1/saml` endpoint is no longer supported. +*Details:* The deprecated `/api/security/v1/saml` endpoint is no longer supported. *Impact:* Rely on `/api/security/saml/callback` endpoint when using SAML instead. This change should be reflected in Kibana `server.xsrf.whitelist` config as well as in Elasticsearch and Identity Provider SAML settings. @@ -108,7 +108,7 @@ access level. [float] ==== Legacy job parameters are no longer supported -*Details:* POST URL snippets that were copied in Kibana 6.2 or below are no longer supported. These logs have +*Details:* POST URL snippets that were copied in Kibana 6.2 or earlier are no longer supported. These logs have been deprecated with warnings that have been logged throughout 7.x. Please use Kibana UI to re-generate the POST URL snippets if you depend on these for automated PDF reports. diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index a6eeffec51cb0..91bbef5690fd5 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -32,7 +32,7 @@ image::settings/images/apm-settings.png[APM app settings in Kibana] // tag::general-apm-settings[] If you'd like to change any of the default values, -copy and paste the relevant settings below into your `kibana.yml` configuration file. +copy and paste the relevant settings into your `kibana.yml` configuration file. xpack.apm.enabled:: Set to `false` to disabled the APM plugin {kib}. Defaults to `true`. diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index 8586d26e9a07a..6645f49029a51 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -20,9 +20,7 @@ which support the same values as <>. To control how data is collected from your {es} nodes, you configure {ref}/monitoring-settings.html[`xpack.monitoring.collection` settings] in `elasticsearch.yml`. To control how monitoring data is collected -from Logstash, you configure -{logstash-ref}/monitoring-internal-collection.html#monitoring-settings[`xpack.monitoring` settings] -in `logstash.yml`. +from Logstash, configure monitoring settings in `logstash.yml`. For more information, see {ref}/monitor-elasticsearch-cluster.html[Monitor a cluster]. diff --git a/docs/settings/ssl-settings.asciidoc b/docs/settings/ssl-settings.asciidoc index 5341d3543e7c6..3a0a474d9d597 100644 --- a/docs/settings/ssl-settings.asciidoc +++ b/docs/settings/ssl-settings.asciidoc @@ -44,7 +44,7 @@ Java Cryptography Architecture documentation]. Defaults to the value of The following settings are used to specify a private key, certificate, and the trusted certificates that should be used when communicating over an SSL/TLS connection. -If none of the settings below are specified, the default values are used. +If none of the settings are specified, the default values are used. See {ref}/security-settings.html[Default TLS/SSL settings]. ifdef::server[] @@ -54,8 +54,8 @@ ifndef::server[] A private key and certificate are optional and would be used if the server requires client authentication for PKI authentication. endif::server[] -If none of the settings below are specified, the defaults values are used. -See {ref}/security-settings.html[Default TLS/SSL settings]. +If none of the settings bare specified, the defaults values are used. +See {ref}/security-settings.html[Default TLS/SSL settings]. [float] ===== PEM encoded files diff --git a/docs/uptime-guide/install.asciidoc b/docs/uptime-guide/install.asciidoc index 5d32a26529f86..e7c50bb7604ce 100644 --- a/docs/uptime-guide/install.asciidoc +++ b/docs/uptime-guide/install.asciidoc @@ -20,7 +20,7 @@ then jump straight to <>. === Install the stack yourself If you'd rather install the stack yourself, -first see the https://www.elastic.co/support/matrix[Elastic Support Matrix] for information about supported operating systems and product compatibility. Then, follow the steps below. +first see the https://www.elastic.co/support/matrix[Elastic Support Matrix] for information about supported operating systems and product compatibility. * <> * <> diff --git a/docs/uptime-guide/security.asciidoc b/docs/uptime-guide/security.asciidoc index 6651b33ea0e0e..0c6fa4c6c4f56 100644 --- a/docs/uptime-guide/security.asciidoc +++ b/docs/uptime-guide/security.asciidoc @@ -1,9 +1,8 @@ [[uptime-security]] == Elasticsearch Security -If you use Elasticsearch security, you'll need to enable certain privileges for users -that would like to access the Uptime app. Below is an example of creating -a user and support role to implement those privileges. +If you use Elasticsearch security, you'll need to enable certain privileges for users +that would like to access the Uptime app. For example, create user and support roles to implement the privileges: [float] === Create a role diff --git a/docs/user/graph/getting-started.asciidoc b/docs/user/graph/getting-started.asciidoc index 7b3bd10147966..1749678ace9e3 100644 --- a/docs/user/graph/getting-started.asciidoc +++ b/docs/user/graph/getting-started.asciidoc @@ -2,7 +2,7 @@ [[graph-getting-started]] == Using Graph -You must index data into {es} before you can create a graph. +You must index data into {es} before you can create a graph. <> or get started with a <>. [float] @@ -11,24 +11,24 @@ You must index data into {es} before you can create a graph. . From the side navigation, open *Graph*. + -If this is your first graph, follow the prompts to create it. +If this is your first graph, follow the prompts to create it. For subsequent graphs, click *New*. . Select a data source to explore. . Add one or more multi-value fields that contain the terms you want to -graph. +graph. + The vertices in the graph are selected from these terms. . Enter a search query to discover relationships between terms in the selected -fields. +fields. + -For example, if you are using the {kib} sample web logs data set, and you want +For example, if you are using the {kib} sample web logs data set, and you want to generate a graph of the successful requests to particular pages from different locations, you could search for the 200 response code. The weight of the connection between two vertices indicates how strongly they -are related. +are related. + [role="screenshot"] image::user/graph/images/graph-url-connections.png["URL connections"] @@ -45,11 +45,11 @@ additional connections: image:user/graph/images/graph-expand-button.png[Expand Selection]. * To display additional connections between the displayed vertices, click the link icon -image:user/graph/images/graph-link-button.png[Add links to existing terms]. +image:user/graph/images/graph-link-button.png[Add links to existing terms]. * To explore a particular area of the graph, select the vertices you are interested in, and then click expand or link. * To step back through your changes to the graph, click undo -image:user/graph/images/graph-undo-button.png[Undo] and redo +image:user/graph/images/graph-undo-button.png[Undo] and redo image:user/graph/images/graph-redo-button.png[Redo]. . To see more relationships in your data, submit additional queries. @@ -63,61 +63,61 @@ image::user/graph/images/graph-add-query.png["Adding networks"] [[style-vertex-properties]] === Style vertex properties -Each vertex has a color, icon, and label. To change -the color or icon of all vertices -of a certain field, click the field badge below the search bar, and then +Each vertex has a color, icon, and label. To change +the color or icon of all vertices +of a certain field, click it's badge, and then select *Edit settings*. -To change the color and label of selected vertices, +To change the color and label of selected vertices, click the style icon image:user/graph/images/graph-style-button.png[Style] -in the control bar on the right. +in the control bar on the right. [float] [[edit-graph-settings]] === Edit graph settings -By default, *Graph* is configured to tune out noise in your data. +By default, *Graph* is configured to tune out noise in your data. If this isn't a good fit for your data, use *Settings > Advanced settings* -to adjust the way *Graph* queries your data. You can tune the graph to show -only the results relevant to you and to improve performance. -For more information, see <>. +to adjust the way *Graph* queries your data. You can tune the graph to show +only the results relevant to you and to improve performance. +For more information, see <>. -You can configure the number of vertices that a search or +You can configure the number of vertices that a search or expand operation adds to the graph. -By default, only the five most relevant terms for any given field are added -at a time. This keeps the graph from overflowing. To increase this number, click -a field below the search bar, select *Edit Settings*, and change *Terms per hop*. +By default, only the five most relevant terms for any given field are added +at a time. This keeps the graph from overflowing. To increase this number, click +a field, select *Edit Settings*, and change *Terms per hop*. [float] [[graph-block-terms]] === Block terms from the graph -Documents that match a blocked term are not allowed in the graph. -To block a term, select its vertex and click +Documents that match a blocked term are not allowed in the graph. +To block a term, select its vertex and click the block icon image:user/graph/images/graph-block-button.png[Block selection] -in the control panel. +in the control panel. For a list of blocked terms, go to *Settings > Blocked terms*. [float] [[graph-drill-down]] === Drill down into raw documents -With drilldowns, you can display additional information about a -selected vertex in a new browser window. For example, you might -configure a drilldown URL to perform a web search for the selected vertex term. +With drilldowns, you can display additional information about a +selected vertex in a new browser window. For example, you might +configure a drilldown URL to perform a web search for the selected vertex term. -Use the drilldown icon image:user/graph/images/graph-info-icon.png[Drilldown selection] +Use the drilldown icon image:user/graph/images/graph-info-icon.png[Drilldown selection] in the control panel to show the drilldown buttons for the selected vertices. -To configure drilldowns, go to *Settings > Drilldowns*. See also +To configure drilldowns, go to *Settings > Drilldowns*. See also <>. [float] [[graph-run-layout]] === Run and pause layout -Graph uses a "force layout", where vertices behave like magnets, -pushing off of one another. By default, when you add a new vertex to -the graph, all vertices begin moving. In some cases, the movement might -go on for some time. To freeze the current vertex position, +Graph uses a "force layout", where vertices behave like magnets, +pushing off of one another. By default, when you add a new vertex to +the graph, all vertices begin moving. In some cases, the movement might +go on for some time. To freeze the current vertex position, click the pause icon image:user/graph/images/graph-pause-button.png[Block selection] -in the control panel. +in the control panel. diff --git a/docs/user/monitoring/beats-details.asciidoc b/docs/user/monitoring/beats-details.asciidoc index 672ed6226e427..0b2be4dd9e3d9 100644 --- a/docs/user/monitoring/beats-details.asciidoc +++ b/docs/user/monitoring/beats-details.asciidoc @@ -13,7 +13,7 @@ image::user/monitoring/images/monitoring-beats.jpg["Monitoring Beats",link="imag To view an overview of the Beats data in the cluster, click *Overview*. The overview page has a section for activity in the last day, which is a real-time -sample of data. Below that, a summary bar and charts follow the typical paradigm +sample of data. The summary bar and charts follow the typical paradigm of data in the Monitoring UI, which is bound to the span of the time filter in the top right corner of the page. This overview page can therefore show up-to-date or historical information. diff --git a/docs/user/security/rbac_tutorial.asciidoc b/docs/user/security/rbac_tutorial.asciidoc index e4dbdc2483f70..d45aae86a9ccb 100644 --- a/docs/user/security/rbac_tutorial.asciidoc +++ b/docs/user/security/rbac_tutorial.asciidoc @@ -10,10 +10,10 @@ Kibana spaces. ==== Scenario Our user is a web developer working on a bank's -online mortgage service. The web developer has these +online mortgage service. The web developer has these three requirements: -* Have access to the data for that service +* Have access to the data for that service * Build visualizations and dashboards * Monitor the performance of the system @@ -24,28 +24,28 @@ You'll provide the web developer with the access and privileges to get the job d To complete this tutorial, you'll need the following: -* **Administrative privileges**: You must have a role that grants privileges to create a space, role, and user. This is any role which grants the `manage_security` cluster privilege. By default, the `superuser` role provides this access. See the {ref}/built-in-roles.html[built-in] roles. -* **A space**: In this tutorial, use `Dev Mortgage` as the space +* **Administrative privileges**: You must have a role that grants privileges to create a space, role, and user. This is any role which grants the `manage_security` cluster privilege. By default, the `superuser` role provides this access. See the {ref}/built-in-roles.html[built-in] roles. +* **A space**: In this tutorial, use `Dev Mortgage` as the space name. See <> for details on creating a space. -* **Data**: You can use <> or -live data. In the steps below, Filebeat and Metricbeat data are used. +* **Data**: You can use <> or +live data. In the following steps, Filebeat and Metricbeat data are used. [float] ==== Steps -With the requirements in mind, here are the steps that you will work +With the requirements in mind, here are the steps that you will work through in this tutorial: * Create a role named `mortgage-developer` * Give the role permission to access the data in the relevant indices -* Give the role permission to create visualizations and dashboards +* Give the role permission to create visualizations and dashboards * Create the web developer's user account with the proper roles [float] ==== Create a role -Go to **Management > Roles** +Go to **Management > Roles** for an overview of your roles. This view provides actions for you to create, edit, and delete roles. @@ -53,21 +53,21 @@ for you to create, edit, and delete roles. image::security/images/role-management.png["Role management"] -You can create as many roles as you like. Click *Create role* and -provide a name. Use `dev-mortgage` because this role is for a developer +You can create as many roles as you like. Click *Create role* and +provide a name. Use `dev-mortgage` because this role is for a developer working on the bank's mortgage application. [float] ==== Give the role permission to access the data -Access to data in indices is an index-level privilege, so in -*Index privileges*, add lines for the indices that contain the -data for this role. Two privileges are required: `read` and -`view_index_metadata`. All privileges are detailed in the +Access to data in indices is an index-level privilege, so in +*Index privileges*, add lines for the indices that contain the +data for this role. Two privileges are required: `read` and +`view_index_metadata`. All privileges are detailed in the https://www.elastic.co/guide/en/elasticsearch/reference/current/security-privileges.html[security privileges] documentation. -In the screenshots, Filebeat and Metricbeat data is used, but you +In the screenshots, Filebeat and Metricbeat data is used, but you should use the index patterns for your indices. [role="screenshot"] @@ -76,12 +76,12 @@ image::security/images/role-index-privilege.png["Index privilege"] [float] ==== Give the role permission to create visualizations and dashboards -By default, roles do not give Kibana privileges. Click **Add space +By default, roles do not give Kibana privileges. Click **Add space privilege** and associate this role with the `Dev Mortgage` space. -To enable users with the `dev-mortgage` role to create visualizations -and dashboards, click *All* for *Visualize* and *Dashboard*. Also -assign *All* for *Discover* because it is common for developers +To enable users with the `dev-mortgage` role to create visualizations +and dashboards, click *All* for *Visualize* and *Dashboard*. Also +assign *All* for *Discover* because it is common for developers to create saved searches while designing visualizations. [role="screenshot"] @@ -90,15 +90,14 @@ image::security/images/role-space-visualization.png["Associate space"] [float] ==== Create the developer's user account with the proper roles -Go to **Management > Users** and click on **Create user** to create a -user. Give the user the `dev-mortgage` role +Go to **Management > Users** and click on **Create user** to create a +user. Give the user the `dev-mortgage` role and the `monitoring-user` role, which is required for users of **Stack Monitoring**. [role="screenshot"] image::security/images/role-new-user.png["Developer user"] -Finally, have the developer log in and access the Dev Mortgage space +Finally, have the developer log in and access the Dev Mortgage space and create a new visualization. NOTE: If the user is assigned to only one space, they will automatically enter that space on login. - diff --git a/docs/visualize/vega.asciidoc b/docs/visualize/vega.asciidoc index c9cf1e7aeb820..b8c0d1dbe3dda 100644 --- a/docs/visualize/vega.asciidoc +++ b/docs/visualize/vega.asciidoc @@ -324,7 +324,7 @@ replace `"url": "data/world-110m.json"` with `"url": "https://vega.github.io/editor/data/world-110m.json"`. Also, regular Vega examples use `"autosize": "pad"` layout model, whereas Kibana uses `fit`. Remove all `autosize`, `width`, and `height` -values. See link:#sizing-and-positioning[sizing and positioning] below. +values. See link:#sizing-and-positioning[sizing and positioning]. [[vega-additional-configuration-options]] ==== Additional configuration options diff --git a/examples/embeddable_examples/public/hello_world/hello_world_embeddable_factory.ts b/examples/embeddable_examples/public/hello_world/hello_world_embeddable_factory.ts index de5a3d9380def..2995c99ac9e58 100644 --- a/examples/embeddable_examples/public/hello_world/hello_world_embeddable_factory.ts +++ b/examples/embeddable_examples/public/hello_world/hello_world_embeddable_factory.ts @@ -33,7 +33,7 @@ export class HelloWorldEmbeddableFactory extends EmbeddableFactory { * embeddables should check the UI Capabilities service to be sure of * the right permissions. */ - public isEditable() { + public async isEditable() { return true; } diff --git a/examples/embeddable_examples/public/list_container/list_container.tsx b/examples/embeddable_examples/public/list_container/list_container.tsx index 35a674a03573a..bbbd0d6e32304 100644 --- a/examples/embeddable_examples/public/list_container/list_container.tsx +++ b/examples/embeddable_examples/public/list_container/list_container.tsx @@ -21,7 +21,7 @@ import ReactDOM from 'react-dom'; import { Container, ContainerInput, - GetEmbeddableFactory, + EmbeddableStart, } from '../../../../src/plugins/embeddable/public'; import { ListContainerComponent } from './list_container_component'; @@ -31,7 +31,10 @@ export class ListContainer extends Container<{}, ContainerInput> { public readonly type = LIST_CONTAINER; private node?: HTMLElement; - constructor(input: ContainerInput, getEmbeddableFactory: GetEmbeddableFactory) { + constructor( + input: ContainerInput, + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'] + ) { super(input, { embeddableLoaded: {} }, getEmbeddableFactory); } diff --git a/examples/embeddable_examples/public/list_container/list_container_factory.ts b/examples/embeddable_examples/public/list_container/list_container_factory.ts index de6b7d5f5e503..247cf48b41bde 100644 --- a/examples/embeddable_examples/public/list_container/list_container_factory.ts +++ b/examples/embeddable_examples/public/list_container/list_container_factory.ts @@ -20,25 +20,30 @@ import { i18n } from '@kbn/i18n'; import { EmbeddableFactory, - GetEmbeddableFactory, ContainerInput, + EmbeddableStart, } from '../../../../src/plugins/embeddable/public'; import { LIST_CONTAINER, ListContainer } from './list_container'; +interface StartServices { + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; +} + export class ListContainerFactory extends EmbeddableFactory { public readonly type = LIST_CONTAINER; public readonly isContainerType = true; - constructor(private getEmbeddableFactory: GetEmbeddableFactory) { + constructor(private getStartServices: () => Promise) { super(); } - public isEditable() { + public async isEditable() { return true; } public async create(initialInput: ContainerInput) { - return new ListContainer(initialInput, this.getEmbeddableFactory); + const { getEmbeddableFactory } = await this.getStartServices(); + return new ListContainer(initialInput, getEmbeddableFactory); } public getDisplayName() { diff --git a/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable_factory.ts b/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable_factory.ts index a54201b157a6c..9afdeabaee765 100644 --- a/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable_factory.ts +++ b/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable_factory.ts @@ -32,7 +32,7 @@ export class MultiTaskTodoEmbeddableFactory extends EmbeddableFactory< > { public readonly type = MULTI_TASK_TODO_EMBEDDABLE; - public isEditable() { + public async isEditable() { return true; } diff --git a/examples/embeddable_examples/public/plugin.ts b/examples/embeddable_examples/public/plugin.ts index b7a4f5c078d54..3663af68ae2c7 100644 --- a/examples/embeddable_examples/public/plugin.ts +++ b/examples/embeddable_examples/public/plugin.ts @@ -17,20 +17,11 @@ * under the License. */ -import { - IEmbeddableSetup, - IEmbeddableStart, - EmbeddableFactory, -} from '../../../src/plugins/embeddable/public'; +import { EmbeddableSetup, EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { Plugin, CoreSetup, CoreStart } from '../../../src/core/public'; import { HelloWorldEmbeddableFactory, HELLO_WORLD_EMBEDDABLE } from './hello_world'; import { TODO_EMBEDDABLE, TodoEmbeddableFactory, TodoInput, TodoOutput } from './todo'; -import { - MULTI_TASK_TODO_EMBEDDABLE, - MultiTaskTodoEmbeddableFactory, - MultiTaskTodoOutput, - MultiTaskTodoInput, -} from './multi_task_todo'; +import { MULTI_TASK_TODO_EMBEDDABLE, MultiTaskTodoEmbeddableFactory } from './multi_task_todo'; import { SEARCHABLE_LIST_CONTAINER, SearchableListContainerFactory, @@ -38,46 +29,56 @@ import { import { LIST_CONTAINER, ListContainerFactory } from './list_container'; interface EmbeddableExamplesSetupDependencies { - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; } interface EmbeddableExamplesStartDependencies { - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; } export class EmbeddableExamplesPlugin implements Plugin { - public setup(core: CoreSetup, deps: EmbeddableExamplesSetupDependencies) { + public setup( + core: CoreSetup, + deps: EmbeddableExamplesSetupDependencies + ) { deps.embeddable.registerEmbeddableFactory( HELLO_WORLD_EMBEDDABLE, new HelloWorldEmbeddableFactory() ); - deps.embeddable.registerEmbeddableFactory< - EmbeddableFactory - >(MULTI_TASK_TODO_EMBEDDABLE, new MultiTaskTodoEmbeddableFactory()); - } + deps.embeddable.registerEmbeddableFactory( + MULTI_TASK_TODO_EMBEDDABLE, + new MultiTaskTodoEmbeddableFactory() + ); - public start(core: CoreStart, deps: EmbeddableExamplesStartDependencies) { // These are registered in the start method because `getEmbeddableFactory ` // is only available in start. We could reconsider this I think and make it // available in both. deps.embeddable.registerEmbeddableFactory( SEARCHABLE_LIST_CONTAINER, - new SearchableListContainerFactory(deps.embeddable.getEmbeddableFactory) + new SearchableListContainerFactory(async () => ({ + getEmbeddableFactory: (await core.getStartServices())[1].embeddable.getEmbeddableFactory, + })) ); deps.embeddable.registerEmbeddableFactory( LIST_CONTAINER, - new ListContainerFactory(deps.embeddable.getEmbeddableFactory) + new ListContainerFactory(async () => ({ + getEmbeddableFactory: (await core.getStartServices())[1].embeddable.getEmbeddableFactory, + })) ); - deps.embeddable.registerEmbeddableFactory>( + deps.embeddable.registerEmbeddableFactory( TODO_EMBEDDABLE, - new TodoEmbeddableFactory(core.overlays.openModal) + new TodoEmbeddableFactory(async () => ({ + openModal: (await core.getStartServices())[0].overlays.openModal, + })) ); } + public start(core: CoreStart, deps: EmbeddableExamplesStartDependencies) {} + public stop() {} } diff --git a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container.tsx b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container.tsx index 3079abb867c38..06462937c768d 100644 --- a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container.tsx +++ b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container.tsx @@ -21,7 +21,7 @@ import ReactDOM from 'react-dom'; import { Container, ContainerInput, - GetEmbeddableFactory, + EmbeddableStart, EmbeddableInput, } from '../../../../src/plugins/embeddable/public'; import { SearchableListContainerComponent } from './searchable_list_container_component'; @@ -40,7 +40,10 @@ export class SearchableListContainer extends Container Promise) { super(); } - public isEditable() { + public async isEditable() { return true; } public async create(initialInput: SearchableContainerInput) { - return new SearchableListContainer(initialInput, this.getEmbeddableFactory); + const { getEmbeddableFactory } = await this.getStartServices(); + return new SearchableListContainer(initialInput, getEmbeddableFactory); } public getDisplayName() { diff --git a/examples/embeddable_examples/public/todo/todo_embeddable_factory.tsx b/examples/embeddable_examples/public/todo/todo_embeddable_factory.tsx index dd2168bb39eee..d7be436905382 100644 --- a/examples/embeddable_examples/public/todo/todo_embeddable_factory.tsx +++ b/examples/embeddable_examples/public/todo/todo_embeddable_factory.tsx @@ -43,6 +43,10 @@ function TaskInput({ onSave }: { onSave: (task: string) => void }) { ); } +interface StartServices { + openModal: OverlayStart['openModal']; +} + export class TodoEmbeddableFactory extends EmbeddableFactory< TodoInput, TodoOutput, @@ -50,11 +54,11 @@ export class TodoEmbeddableFactory extends EmbeddableFactory< > { public readonly type = TODO_EMBEDDABLE; - constructor(private openModal: OverlayStart['openModal']) { + constructor(private getStartServices: () => Promise) { super(); } - public isEditable() { + public async isEditable() { return true; } @@ -69,9 +73,10 @@ export class TodoEmbeddableFactory extends EmbeddableFactory< * in this case, the task string. */ public async getExplicitInput() { + const { openModal } = await this.getStartServices(); return new Promise<{ task: string }>(resolve => { const onSave = (task: string) => resolve({ task }); - const overlay = this.openModal( + const overlay = openModal( toMountPoint( { diff --git a/examples/embeddable_explorer/public/app.tsx b/examples/embeddable_explorer/public/app.tsx index da7e8cc188e31..9c8568454855d 100644 --- a/examples/embeddable_explorer/public/app.tsx +++ b/examples/embeddable_explorer/public/app.tsx @@ -23,7 +23,7 @@ import { BrowserRouter as Router, Route, withRouter, RouteComponentProps } from import { EuiPage, EuiPageSideBar, EuiSideNav } from '@elastic/eui'; -import { IEmbeddableStart } from '../../../src/plugins/embeddable/public'; +import { EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; import { Start as InspectorStartContract } from '../../../src/plugins/inspector/public'; import { @@ -74,7 +74,7 @@ const Nav = withRouter(({ history, navigateToApp, pages }: NavProps) => { interface Props { basename: string; navigateToApp: CoreStart['application']['navigateToApp']; - embeddableApi: IEmbeddableStart; + embeddableApi: EmbeddableStart; uiActionsApi: UiActionsStart; overlays: OverlayStart; notifications: CoreStart['notifications']; diff --git a/examples/embeddable_explorer/public/embeddable_panel_example.tsx b/examples/embeddable_explorer/public/embeddable_panel_example.tsx index e6687d8563f59..b26111bed7ff2 100644 --- a/examples/embeddable_explorer/public/embeddable_panel_example.tsx +++ b/examples/embeddable_explorer/public/embeddable_panel_example.tsx @@ -31,9 +31,8 @@ import { import { EuiSpacer } from '@elastic/eui'; import { OverlayStart, CoreStart, SavedObjectsStart, IUiSettingsClient } from 'kibana/public'; import { - GetEmbeddableFactory, EmbeddablePanel, - IEmbeddableStart, + EmbeddableStart, IEmbeddable, } from '../../../src/plugins/embeddable/public'; import { @@ -47,8 +46,8 @@ import { Start as InspectorStartContract } from '../../../src/plugins/inspector/ import { getSavedObjectFinder } from '../../../src/plugins/saved_objects/public'; interface Props { - getAllEmbeddableFactories: IEmbeddableStart['getEmbeddableFactories']; - getEmbeddableFactory: GetEmbeddableFactory; + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; uiActionsApi: UiActionsStart; overlays: OverlayStart; notifications: CoreStart['notifications']; diff --git a/examples/embeddable_explorer/public/hello_world_embeddable_example.tsx b/examples/embeddable_explorer/public/hello_world_embeddable_example.tsx index 74a6766a1b5ee..ea1c3d781ebfd 100644 --- a/examples/embeddable_explorer/public/hello_world_embeddable_example.tsx +++ b/examples/embeddable_explorer/public/hello_world_embeddable_example.tsx @@ -29,14 +29,14 @@ import { EuiText, } from '@elastic/eui'; import { - GetEmbeddableFactory, + EmbeddableStart, EmbeddableFactoryRenderer, EmbeddableRoot, } from '../../../src/plugins/embeddable/public'; import { HelloWorldEmbeddable, HELLO_WORLD_EMBEDDABLE } from '../../embeddable_examples/public'; interface Props { - getEmbeddableFactory: GetEmbeddableFactory; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; } export function HelloWorldEmbeddableExample({ getEmbeddableFactory }: Props) { diff --git a/examples/embeddable_explorer/public/list_container_example.tsx b/examples/embeddable_explorer/public/list_container_example.tsx index 2c7b12a27d963..969fdb0ca46db 100644 --- a/examples/embeddable_explorer/public/list_container_example.tsx +++ b/examples/embeddable_explorer/public/list_container_example.tsx @@ -29,10 +29,7 @@ import { EuiText, } from '@elastic/eui'; import { EuiSpacer } from '@elastic/eui'; -import { - GetEmbeddableFactory, - EmbeddableFactoryRenderer, -} from '../../../src/plugins/embeddable/public'; +import { EmbeddableFactoryRenderer, EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { HELLO_WORLD_EMBEDDABLE, TODO_EMBEDDABLE, @@ -42,7 +39,7 @@ import { } from '../../embeddable_examples/public'; interface Props { - getEmbeddableFactory: GetEmbeddableFactory; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; } export function ListContainerExample({ getEmbeddableFactory }: Props) { diff --git a/examples/embeddable_explorer/public/plugin.tsx b/examples/embeddable_explorer/public/plugin.tsx index 1294e0c89c9e7..7c75b108d9912 100644 --- a/examples/embeddable_explorer/public/plugin.tsx +++ b/examples/embeddable_explorer/public/plugin.tsx @@ -19,12 +19,12 @@ import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; import { UiActionsService } from '../../../src/plugins/ui_actions/public'; -import { IEmbeddableStart } from '../../../src/plugins/embeddable/public'; +import { EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { Start as InspectorStart } from '../../../src/plugins/inspector/public'; interface StartDeps { uiActions: UiActionsService; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; inspector: InspectorStart; } diff --git a/examples/embeddable_explorer/public/todo_embeddable_example.tsx b/examples/embeddable_explorer/public/todo_embeddable_example.tsx index b1c93087faf83..ce92301236c2b 100644 --- a/examples/embeddable_explorer/public/todo_embeddable_example.tsx +++ b/examples/embeddable_explorer/public/todo_embeddable_example.tsx @@ -39,10 +39,10 @@ import { TODO_EMBEDDABLE, TodoEmbeddableFactory, } from '../../../examples/embeddable_examples/public/todo'; -import { GetEmbeddableFactory, EmbeddableRoot } from '../../../src/plugins/embeddable/public'; +import { EmbeddableStart, EmbeddableRoot } from '../../../src/plugins/embeddable/public'; interface Props { - getEmbeddableFactory: GetEmbeddableFactory; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; } interface State { diff --git a/package.json b/package.json index b3dcfb2aa3b0a..583e99158da72 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,7 @@ "@elastic/apm-rum": "^4.6.0", "@elastic/charts": "^17.1.1", "@elastic/datemath": "5.0.2", - "@elastic/ems-client": "7.6.0", + "@elastic/ems-client": "7.7.0", "@elastic/eui": "20.0.2", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", @@ -239,7 +239,7 @@ "react-resize-detector": "^4.2.0", "react-router-dom": "^5.1.2", "react-sizeme": "^2.3.6", - "react-use": "^13.13.0", + "react-use": "^13.27.0", "reactcss": "1.2.3", "redux": "^4.0.5", "redux-actions": "^2.6.5", @@ -314,6 +314,7 @@ "@types/cheerio": "^0.22.10", "@types/chromedriver": "^2.38.0", "@types/classnames": "^2.2.9", + "@types/color": "^3.0.0", "@types/d3": "^3.5.43", "@types/dedent": "^0.7.0", "@types/deep-freeze-strict": "^1.1.0", diff --git a/packages/kbn-config-schema/README.md b/packages/kbn-config-schema/README.md index 8719a2ae558ab..a4f2c1f6458cf 100644 --- a/packages/kbn-config-schema/README.md +++ b/packages/kbn-config-schema/README.md @@ -239,7 +239,7 @@ __Output type:__ `{ [K in keyof TProps]: TypeOf } as TObject` __Options:__ * `defaultValue: TObject | Reference | (() => TObject)` - defines a default value, see [Default values](#default-values) section for more details. * `validate: (value: TObject) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. - * `allowUnknowns: boolean` - indicates whether unknown object properties should be allowed. It's `false` by default. + * `unknowns: 'allow' | 'ignore' | 'forbid'` - indicates whether unknown object properties should be allowed, ignored, or forbidden. It's `forbid` by default. __Usage:__ ```typescript @@ -250,7 +250,7 @@ const valueSchema = schema.object({ ``` __Notes:__ -* Using `allowUnknowns` is discouraged and should only be used in exceptional circumstances. Consider using `schema.recordOf()` instead. +* Using `unknowns: 'allow'` is discouraged and should only be used in exceptional circumstances. Consider using `schema.recordOf()` instead. * Currently `schema.object()` always has a default value of `{}`, but this may change in the near future. Try to not rely on this behaviour and specify default value explicitly or use `schema.maybe()` if the value is optional. * `schema.object()` also supports a json string as input if it can be safely parsed using `JSON.parse` and if the resulting value is a plain object. diff --git a/packages/kbn-config-schema/src/errors/validation_error.ts b/packages/kbn-config-schema/src/errors/validation_error.ts index d688d022da85c..2a4f887bc4349 100644 --- a/packages/kbn-config-schema/src/errors/validation_error.ts +++ b/packages/kbn-config-schema/src/errors/validation_error.ts @@ -44,5 +44,8 @@ export class ValidationError extends SchemaError { constructor(error: SchemaTypeError, namespace?: string) { super(ValidationError.extractMessage(error, namespace), error); + + // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work + Object.setPrototypeOf(this, ValidationError.prototype); } } diff --git a/packages/kbn-config-schema/src/types/object_type.test.ts b/packages/kbn-config-schema/src/types/object_type.test.ts index 29e341983fde9..47a0f5f7a5491 100644 --- a/packages/kbn-config-schema/src/types/object_type.test.ts +++ b/packages/kbn-config-schema/src/types/object_type.test.ts @@ -276,10 +276,10 @@ test('individual keys can validated', () => { ); }); -test('allow unknown keys when allowUnknowns = true', () => { +test('allow unknown keys when unknowns = `allow`', () => { const type = schema.object( { foo: schema.string({ defaultValue: 'test' }) }, - { allowUnknowns: true } + { unknowns: 'allow' } ); expect( @@ -292,10 +292,10 @@ test('allow unknown keys when allowUnknowns = true', () => { }); }); -test('allowUnknowns = true affects only own keys', () => { +test('unknowns = `allow` affects only own keys', () => { const type = schema.object( { foo: schema.object({ bar: schema.string() }) }, - { allowUnknowns: true } + { unknowns: 'allow' } ); expect(() => @@ -308,10 +308,10 @@ test('allowUnknowns = true affects only own keys', () => { ).toThrowErrorMatchingInlineSnapshot(`"[foo.baz]: definition for this key is missing"`); }); -test('does not allow unknown keys when allowUnknowns = false', () => { +test('does not allow unknown keys when unknowns = `forbid`', () => { const type = schema.object( { foo: schema.string({ defaultValue: 'test' }) }, - { allowUnknowns: false } + { unknowns: 'forbid' } ); expect(() => type.validate({ @@ -319,3 +319,34 @@ test('does not allow unknown keys when allowUnknowns = false', () => { }) ).toThrowErrorMatchingInlineSnapshot(`"[bar]: definition for this key is missing"`); }); + +test('allow and remove unknown keys when unknowns = `ignore`', () => { + const type = schema.object( + { foo: schema.string({ defaultValue: 'test' }) }, + { unknowns: 'ignore' } + ); + + expect( + type.validate({ + bar: 'baz', + }) + ).toEqual({ + foo: 'test', + }); +}); + +test('unknowns = `ignore` affects only own keys', () => { + const type = schema.object( + { foo: schema.object({ bar: schema.string() }) }, + { unknowns: 'ignore' } + ); + + expect(() => + type.validate({ + foo: { + bar: 'bar', + baz: 'baz', + }, + }) + ).toThrowErrorMatchingInlineSnapshot(`"[foo.baz]: definition for this key is missing"`); +}); diff --git a/packages/kbn-config-schema/src/types/object_type.ts b/packages/kbn-config-schema/src/types/object_type.ts index f34acd0d2ce65..5a50e714a5931 100644 --- a/packages/kbn-config-schema/src/types/object_type.ts +++ b/packages/kbn-config-schema/src/types/object_type.ts @@ -30,17 +30,25 @@ export type TypeOf> = RT['type']; // this might not have perfect _rendering_ output, but it will be typed. export type ObjectResultType

= Readonly<{ [K in keyof P]: TypeOf }>; +interface UnknownOptions { + /** + * Options for dealing with unknown keys: + * - allow: unknown keys will be permitted + * - ignore: unknown keys will not fail validation, but will be stripped out + * - forbid (default): unknown keys will fail validation + */ + unknowns?: 'allow' | 'ignore' | 'forbid'; +} + export type ObjectTypeOptions

= TypeOptions< { [K in keyof P]: TypeOf } -> & { - /** Should uknown keys not be defined in the schema be allowed. Defaults to `false` */ - allowUnknowns?: boolean; -}; +> & + UnknownOptions; export class ObjectType

extends Type> { private props: Record; - constructor(props: P, { allowUnknowns = false, ...typeOptions }: ObjectTypeOptions

= {}) { + constructor(props: P, { unknowns = 'forbid', ...typeOptions }: ObjectTypeOptions

= {}) { const schemaKeys = {} as Record; for (const [key, value] of Object.entries(props)) { schemaKeys[key] = value.getSchema(); @@ -50,7 +58,8 @@ export class ObjectType

extends Type> .keys(schemaKeys) .default() .optional() - .unknown(Boolean(allowUnknowns)); + .unknown(unknowns === 'allow') + .options({ stripUnknown: { objects: unknowns === 'ignore' } }); super(schema, typeOptions); this.props = schemaKeys; diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts index ad01dea624c3c..dbfa87e70032b 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts @@ -67,7 +67,7 @@ export class KbnClientUiSettings { * Replace all uiSettings with the `doc` values, `doc` is merged * with some defaults */ - async replace(doc: UiSettingValues) { + async replace(doc: UiSettingValues, { retries = 5 }: { retries?: number } = {}) { this.log.debug('replacing kibana config doc: %j', doc); const changes: Record = { @@ -85,7 +85,7 @@ export class KbnClientUiSettings { method: 'POST', path: '/api/kibana/settings', body: { changes }, - retries: 5, + retries, }); } diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index fe0491870e4bd..16fc0d891185f 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -94,7 +94,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _cli__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "run", function() { return _cli__WEBPACK_IMPORTED_MODULE_0__["run"]; }); -/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(704); +/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(705); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["buildProductionProjects"]; }); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); @@ -43920,7 +43920,7 @@ class KbnClientUiSettings { * Replace all uiSettings with the `doc` values, `doc` is merged * with some defaults */ - async replace(doc) { + async replace(doc, { retries = 5 } = {}) { this.log.debug('replacing kibana config doc: %j', doc); const changes = { ...this.defaults, @@ -43935,7 +43935,7 @@ class KbnClientUiSettings { method: 'POST', path: '/api/kibana/settings', body: { changes }, - retries: 5, + retries, }); } /** @@ -57074,7 +57074,7 @@ async function getChangesForProjects(projects, kbn, log) { log.verbose('getting changed files'); const { stdout - } = await execa__WEBPACK_IMPORTED_MODULE_3___default()('git', ['ls-files', '-dmt', '--', ...Array.from(projects.values()).map(p => p.path)], { + } = await execa__WEBPACK_IMPORTED_MODULE_3___default()('git', ['ls-files', '-dmt', '--', ...Array.from(projects.values()).filter(p => kbn.isPartOfRepo(p)).map(p => p.path)], { cwd: kbn.getAbsolute() }); const output = stdout.trim(); @@ -57117,6 +57117,11 @@ async function getChangesForProjects(projects, kbn, log) { const changesByProject = new Map(); for (const project of sortedRelevantProjects) { + if (kbn.isOutsideRepo(project)) { + changesByProject.set(project, undefined); + continue; + } + const ownChanges = new Map(); const prefix = kbn.getRelative(project.path); @@ -57141,6 +57146,10 @@ async function getChangesForProjects(projects, kbn, log) { async function getLatestSha(project, kbn) { + if (kbn.isOutsideRepo(project)) { + return; + } + const { stdout } = await execa__WEBPACK_IMPORTED_MODULE_3___default()('git', ['log', '-n', '1', '--pretty=format:%H', '--', project.path], { @@ -57200,7 +57209,7 @@ async function getChecksum(project, changes, yarnLock, kbn, log) { log.verbose(`[${project.name}] local sha:`, sha); } - if (Array.from(changes.values()).includes('invalid')) { + if (!changes || Array.from(changes.values()).includes('invalid')) { log.warning(`[${project.name}] unable to determine local changes, caching disabled`); return; } @@ -79162,8 +79171,10 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(700); /* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(multimatch__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(501); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(579); +/* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(704); +/* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(is_path_inside__WEBPACK_IMPORTED_MODULE_2__); +/* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(501); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(579); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -79192,6 +79203,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope + /** * Helper class for dealing with a set of projects as children of * the Kibana project. The kbn/pm is currently implemented to be @@ -79206,7 +79218,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope class Kibana { static async loadFrom(rootPath) { - return new Kibana((await Object(_projects__WEBPACK_IMPORTED_MODULE_2__["getProjects"])(rootPath, Object(_config__WEBPACK_IMPORTED_MODULE_3__["getProjectPaths"])({ + return new Kibana((await Object(_projects__WEBPACK_IMPORTED_MODULE_3__["getProjects"])(rootPath, Object(_config__WEBPACK_IMPORTED_MODULE_4__["getProjectPaths"])({ rootPath })))); } @@ -79265,7 +79277,7 @@ class Kibana { getProjectAndDeps(name) { const project = this.getProject(name); - return Object(_projects__WEBPACK_IMPORTED_MODULE_2__["includeTransitiveProjects"])([project], this.allWorkspaceProjects); + return Object(_projects__WEBPACK_IMPORTED_MODULE_3__["includeTransitiveProjects"])([project], this.allWorkspaceProjects); } /** filter the projects to just those matching certain paths/include/exclude tags */ @@ -79274,7 +79286,7 @@ class Kibana { const allProjects = this.getAllProjects(); const filteredProjects = new Map(); const pkgJsonPaths = Array.from(allProjects.values()).map(p => p.packageJsonLocation); - const filteredPkgJsonGlobs = Object(_config__WEBPACK_IMPORTED_MODULE_3__["getProjectPaths"])(_objectSpread({}, options, { + const filteredPkgJsonGlobs = Object(_config__WEBPACK_IMPORTED_MODULE_4__["getProjectPaths"])(_objectSpread({}, options, { rootPath: this.kibanaProject.path })).map(g => path__WEBPACK_IMPORTED_MODULE_0___default.a.resolve(g, 'package.json')); const matchingPkgJsonPaths = multimatch__WEBPACK_IMPORTED_MODULE_1___default()(pkgJsonPaths, filteredPkgJsonGlobs); @@ -79292,6 +79304,14 @@ class Kibana { return filteredProjects; } + isPartOfRepo(project) { + return project.path === this.kibanaProject.path || is_path_inside__WEBPACK_IMPORTED_MODULE_2___default()(project.path, this.kibanaProject.path); + } + + isOutsideRepo(project) { + return !this.isPartOfRepo(project); + } + } /***/ }), @@ -79385,14 +79405,42 @@ module.exports = arrify; /***/ }), /* 704 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +const path = __webpack_require__(16); + +module.exports = (childPath, parentPath) => { + childPath = path.resolve(childPath); + parentPath = path.resolve(parentPath); + + if (process.platform === 'win32') { + childPath = childPath.toLowerCase(); + parentPath = parentPath.toLowerCase(); + } + + if (childPath === parentPath) { + return false; + } + + childPath += path.sep; + parentPath += path.sep; + + return childPath.startsWith(parentPath); +}; + + +/***/ }), +/* 705 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(705); +/* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(706); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _build_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildProductionProjects"]; }); -/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(928); +/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(929); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); /* @@ -79417,13 +79465,13 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 705 */ +/* 706 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return buildProductionProjects; }); -/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(706); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(707); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(587); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); @@ -79565,7 +79613,7 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { } /***/ }), -/* 706 */ +/* 707 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79573,13 +79621,13 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { const EventEmitter = __webpack_require__(379); const path = __webpack_require__(16); const os = __webpack_require__(11); -const pAll = __webpack_require__(707); -const arrify = __webpack_require__(709); -const globby = __webpack_require__(710); +const pAll = __webpack_require__(708); +const arrify = __webpack_require__(710); +const globby = __webpack_require__(711); const isGlob = __webpack_require__(605); -const cpFile = __webpack_require__(913); -const junk = __webpack_require__(925); -const CpyError = __webpack_require__(926); +const cpFile = __webpack_require__(914); +const junk = __webpack_require__(926); +const CpyError = __webpack_require__(927); const defaultOptions = { ignoreJunk: true @@ -79698,12 +79746,12 @@ module.exports = (source, destination, { /***/ }), -/* 707 */ +/* 708 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pMap = __webpack_require__(708); +const pMap = __webpack_require__(709); module.exports = (iterable, options) => pMap(iterable, element => element(), options); // TODO: Remove this for the next major release @@ -79711,7 +79759,7 @@ module.exports.default = module.exports; /***/ }), -/* 708 */ +/* 709 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79790,7 +79838,7 @@ module.exports.default = pMap; /***/ }), -/* 709 */ +/* 710 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79820,17 +79868,17 @@ module.exports = arrify; /***/ }), -/* 710 */ +/* 711 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const arrayUnion = __webpack_require__(711); -const glob = __webpack_require__(713); -const fastGlob = __webpack_require__(718); -const dirGlob = __webpack_require__(906); -const gitignore = __webpack_require__(909); +const arrayUnion = __webpack_require__(712); +const glob = __webpack_require__(714); +const fastGlob = __webpack_require__(719); +const dirGlob = __webpack_require__(907); +const gitignore = __webpack_require__(910); const DEFAULT_FILTER = () => false; @@ -79975,12 +80023,12 @@ module.exports.gitignore = gitignore; /***/ }), -/* 711 */ +/* 712 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var arrayUniq = __webpack_require__(712); +var arrayUniq = __webpack_require__(713); module.exports = function () { return arrayUniq([].concat.apply([], arguments)); @@ -79988,7 +80036,7 @@ module.exports = function () { /***/ }), -/* 712 */ +/* 713 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80057,7 +80105,7 @@ if ('Set' in global) { /***/ }), -/* 713 */ +/* 714 */ /***/ (function(module, exports, __webpack_require__) { // Approach: @@ -80106,13 +80154,13 @@ var fs = __webpack_require__(23) var rp = __webpack_require__(503) var minimatch = __webpack_require__(505) var Minimatch = minimatch.Minimatch -var inherits = __webpack_require__(714) +var inherits = __webpack_require__(715) var EE = __webpack_require__(379).EventEmitter var path = __webpack_require__(16) var assert = __webpack_require__(30) var isAbsolute = __webpack_require__(511) -var globSync = __webpack_require__(716) -var common = __webpack_require__(717) +var globSync = __webpack_require__(717) +var common = __webpack_require__(718) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -80853,7 +80901,7 @@ Glob.prototype._stat2 = function (f, abs, er, stat, cb) { /***/ }), -/* 714 */ +/* 715 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -80863,12 +80911,12 @@ try { module.exports = util.inherits; } catch (e) { /* istanbul ignore next */ - module.exports = __webpack_require__(715); + module.exports = __webpack_require__(716); } /***/ }), -/* 715 */ +/* 716 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -80901,7 +80949,7 @@ if (typeof Object.create === 'function') { /***/ }), -/* 716 */ +/* 717 */ /***/ (function(module, exports, __webpack_require__) { module.exports = globSync @@ -80911,12 +80959,12 @@ var fs = __webpack_require__(23) var rp = __webpack_require__(503) var minimatch = __webpack_require__(505) var Minimatch = minimatch.Minimatch -var Glob = __webpack_require__(713).Glob +var Glob = __webpack_require__(714).Glob var util = __webpack_require__(29) var path = __webpack_require__(16) var assert = __webpack_require__(30) var isAbsolute = __webpack_require__(511) -var common = __webpack_require__(717) +var common = __webpack_require__(718) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -81393,7 +81441,7 @@ GlobSync.prototype._makeAbs = function (f) { /***/ }), -/* 717 */ +/* 718 */ /***/ (function(module, exports, __webpack_require__) { exports.alphasort = alphasort @@ -81639,10 +81687,10 @@ function childrenIgnored (self, path) { /***/ }), -/* 718 */ +/* 719 */ /***/ (function(module, exports, __webpack_require__) { -const pkg = __webpack_require__(719); +const pkg = __webpack_require__(720); module.exports = pkg.async; module.exports.default = pkg.async; @@ -81655,19 +81703,19 @@ module.exports.generateTasks = pkg.generateTasks; /***/ }), -/* 719 */ +/* 720 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var optionsManager = __webpack_require__(720); -var taskManager = __webpack_require__(721); -var reader_async_1 = __webpack_require__(877); -var reader_stream_1 = __webpack_require__(901); -var reader_sync_1 = __webpack_require__(902); -var arrayUtils = __webpack_require__(904); -var streamUtils = __webpack_require__(905); +var optionsManager = __webpack_require__(721); +var taskManager = __webpack_require__(722); +var reader_async_1 = __webpack_require__(878); +var reader_stream_1 = __webpack_require__(902); +var reader_sync_1 = __webpack_require__(903); +var arrayUtils = __webpack_require__(905); +var streamUtils = __webpack_require__(906); /** * Synchronous API. */ @@ -81733,7 +81781,7 @@ function isString(source) { /***/ }), -/* 720 */ +/* 721 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81771,13 +81819,13 @@ exports.prepare = prepare; /***/ }), -/* 721 */ +/* 722 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var patternUtils = __webpack_require__(722); +var patternUtils = __webpack_require__(723); /** * Generate tasks based on parent directory of each pattern. */ @@ -81868,16 +81916,16 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 722 */ +/* 723 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(16); -var globParent = __webpack_require__(723); -var isGlob = __webpack_require__(726); -var micromatch = __webpack_require__(727); +var globParent = __webpack_require__(724); +var isGlob = __webpack_require__(727); +var micromatch = __webpack_require__(728); var GLOBSTAR = '**'; /** * Return true for static pattern. @@ -82023,15 +82071,15 @@ exports.matchAny = matchAny; /***/ }), -/* 723 */ +/* 724 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var path = __webpack_require__(16); -var isglob = __webpack_require__(724); -var pathDirname = __webpack_require__(725); +var isglob = __webpack_require__(725); +var pathDirname = __webpack_require__(726); var isWin32 = __webpack_require__(11).platform() === 'win32'; module.exports = function globParent(str) { @@ -82054,7 +82102,7 @@ module.exports = function globParent(str) { /***/ }), -/* 724 */ +/* 725 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -82085,7 +82133,7 @@ module.exports = function isGlob(str) { /***/ }), -/* 725 */ +/* 726 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82235,7 +82283,7 @@ module.exports.win32 = win32; /***/ }), -/* 726 */ +/* 727 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -82287,7 +82335,7 @@ module.exports = function isGlob(str, options) { /***/ }), -/* 727 */ +/* 728 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82298,18 +82346,18 @@ module.exports = function isGlob(str, options) { */ var util = __webpack_require__(29); -var braces = __webpack_require__(728); -var toRegex = __webpack_require__(830); -var extend = __webpack_require__(838); +var braces = __webpack_require__(729); +var toRegex = __webpack_require__(831); +var extend = __webpack_require__(839); /** * Local dependencies */ -var compilers = __webpack_require__(841); -var parsers = __webpack_require__(873); -var cache = __webpack_require__(874); -var utils = __webpack_require__(875); +var compilers = __webpack_require__(842); +var parsers = __webpack_require__(874); +var cache = __webpack_require__(875); +var utils = __webpack_require__(876); var MAX_LENGTH = 1024 * 64; /** @@ -83171,7 +83219,7 @@ module.exports = micromatch; /***/ }), -/* 728 */ +/* 729 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83181,18 +83229,18 @@ module.exports = micromatch; * Module dependencies */ -var toRegex = __webpack_require__(729); -var unique = __webpack_require__(741); -var extend = __webpack_require__(738); +var toRegex = __webpack_require__(730); +var unique = __webpack_require__(742); +var extend = __webpack_require__(739); /** * Local dependencies */ -var compilers = __webpack_require__(742); -var parsers = __webpack_require__(757); -var Braces = __webpack_require__(767); -var utils = __webpack_require__(743); +var compilers = __webpack_require__(743); +var parsers = __webpack_require__(758); +var Braces = __webpack_require__(768); +var utils = __webpack_require__(744); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -83496,15 +83544,15 @@ module.exports = braces; /***/ }), -/* 729 */ +/* 730 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(730); -var extend = __webpack_require__(738); -var not = __webpack_require__(740); +var define = __webpack_require__(731); +var extend = __webpack_require__(739); +var not = __webpack_require__(741); var MAX_LENGTH = 1024 * 64; /** @@ -83651,7 +83699,7 @@ module.exports.makeRe = makeRe; /***/ }), -/* 730 */ +/* 731 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83664,7 +83712,7 @@ module.exports.makeRe = makeRe; -var isDescriptor = __webpack_require__(731); +var isDescriptor = __webpack_require__(732); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -83689,7 +83737,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 731 */ +/* 732 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83702,9 +83750,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(732); -var isAccessor = __webpack_require__(733); -var isData = __webpack_require__(736); +var typeOf = __webpack_require__(733); +var isAccessor = __webpack_require__(734); +var isData = __webpack_require__(737); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -83718,7 +83766,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 732 */ +/* 733 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -83871,7 +83919,7 @@ function isBuffer(val) { /***/ }), -/* 733 */ +/* 734 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83884,7 +83932,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(734); +var typeOf = __webpack_require__(735); // accessor descriptor properties var accessor = { @@ -83947,10 +83995,10 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 734 */ +/* 735 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(735); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -84069,7 +84117,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 735 */ +/* 736 */ /***/ (function(module, exports) { /*! @@ -84096,7 +84144,7 @@ function isSlowBuffer (obj) { /***/ }), -/* 736 */ +/* 737 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84109,7 +84157,7 @@ function isSlowBuffer (obj) { -var typeOf = __webpack_require__(737); +var typeOf = __webpack_require__(738); // data descriptor properties var data = { @@ -84158,10 +84206,10 @@ module.exports = isDataDescriptor; /***/ }), -/* 737 */ +/* 738 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(735); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -84280,13 +84328,13 @@ module.exports = function kindOf(val) { /***/ }), -/* 738 */ +/* 739 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(739); +var isObject = __webpack_require__(740); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -84320,7 +84368,7 @@ function hasOwn(obj, key) { /***/ }), -/* 739 */ +/* 740 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84340,13 +84388,13 @@ module.exports = function isExtendable(val) { /***/ }), -/* 740 */ +/* 741 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(738); +var extend = __webpack_require__(739); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -84413,7 +84461,7 @@ module.exports = toRegex; /***/ }), -/* 741 */ +/* 742 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84463,13 +84511,13 @@ module.exports.immutable = function uniqueImmutable(arr) { /***/ }), -/* 742 */ +/* 743 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(743); +var utils = __webpack_require__(744); module.exports = function(braces, options) { braces.compiler @@ -84752,25 +84800,25 @@ function hasQueue(node) { /***/ }), -/* 743 */ +/* 744 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var splitString = __webpack_require__(744); +var splitString = __webpack_require__(745); var utils = module.exports; /** * Module dependencies */ -utils.extend = __webpack_require__(738); -utils.flatten = __webpack_require__(750); -utils.isObject = __webpack_require__(748); -utils.fillRange = __webpack_require__(751); -utils.repeat = __webpack_require__(756); -utils.unique = __webpack_require__(741); +utils.extend = __webpack_require__(739); +utils.flatten = __webpack_require__(751); +utils.isObject = __webpack_require__(749); +utils.fillRange = __webpack_require__(752); +utils.repeat = __webpack_require__(757); +utils.unique = __webpack_require__(742); utils.define = function(obj, key, val) { Object.defineProperty(obj, key, { @@ -85102,7 +85150,7 @@ utils.escapeRegex = function(str) { /***/ }), -/* 744 */ +/* 745 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85115,7 +85163,7 @@ utils.escapeRegex = function(str) { -var extend = __webpack_require__(745); +var extend = __webpack_require__(746); module.exports = function(str, options, fn) { if (typeof str !== 'string') { @@ -85280,14 +85328,14 @@ function keepEscaping(opts, str, idx) { /***/ }), -/* 745 */ +/* 746 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(746); -var assignSymbols = __webpack_require__(749); +var isExtendable = __webpack_require__(747); +var assignSymbols = __webpack_require__(750); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -85347,7 +85395,7 @@ function isEnum(obj, key) { /***/ }), -/* 746 */ +/* 747 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85360,7 +85408,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(747); +var isPlainObject = __webpack_require__(748); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -85368,7 +85416,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 747 */ +/* 748 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85381,7 +85429,7 @@ module.exports = function isExtendable(val) { -var isObject = __webpack_require__(748); +var isObject = __webpack_require__(749); function isObjectObject(o) { return isObject(o) === true @@ -85412,7 +85460,7 @@ module.exports = function isPlainObject(o) { /***/ }), -/* 748 */ +/* 749 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85431,7 +85479,7 @@ module.exports = function isObject(val) { /***/ }), -/* 749 */ +/* 750 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85478,7 +85526,7 @@ module.exports = function(receiver, objects) { /***/ }), -/* 750 */ +/* 751 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85507,7 +85555,7 @@ function flat(arr, res) { /***/ }), -/* 751 */ +/* 752 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85521,10 +85569,10 @@ function flat(arr, res) { var util = __webpack_require__(29); -var isNumber = __webpack_require__(752); -var extend = __webpack_require__(738); -var repeat = __webpack_require__(754); -var toRegex = __webpack_require__(755); +var isNumber = __webpack_require__(753); +var extend = __webpack_require__(739); +var repeat = __webpack_require__(755); +var toRegex = __webpack_require__(756); /** * Return a range of numbers or letters. @@ -85722,7 +85770,7 @@ module.exports = fillRange; /***/ }), -/* 752 */ +/* 753 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85735,7 +85783,7 @@ module.exports = fillRange; -var typeOf = __webpack_require__(753); +var typeOf = __webpack_require__(754); module.exports = function isNumber(num) { var type = typeOf(num); @@ -85751,10 +85799,10 @@ module.exports = function isNumber(num) { /***/ }), -/* 753 */ +/* 754 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(735); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -85873,7 +85921,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 754 */ +/* 755 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85950,7 +85998,7 @@ function repeat(str, num) { /***/ }), -/* 755 */ +/* 756 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85963,8 +86011,8 @@ function repeat(str, num) { -var repeat = __webpack_require__(754); -var isNumber = __webpack_require__(752); +var repeat = __webpack_require__(755); +var isNumber = __webpack_require__(753); var cache = {}; function toRegexRange(min, max, options) { @@ -86251,7 +86299,7 @@ module.exports = toRegexRange; /***/ }), -/* 756 */ +/* 757 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86276,14 +86324,14 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 757 */ +/* 758 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(758); -var utils = __webpack_require__(743); +var Node = __webpack_require__(759); +var utils = __webpack_require__(744); /** * Braces parsers @@ -86643,15 +86691,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 758 */ +/* 759 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(748); -var define = __webpack_require__(759); -var utils = __webpack_require__(766); +var isObject = __webpack_require__(749); +var define = __webpack_require__(760); +var utils = __webpack_require__(767); var ownNames; /** @@ -87142,7 +87190,7 @@ exports = module.exports = Node; /***/ }), -/* 759 */ +/* 760 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87155,7 +87203,7 @@ exports = module.exports = Node; -var isDescriptor = __webpack_require__(760); +var isDescriptor = __webpack_require__(761); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -87180,7 +87228,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 760 */ +/* 761 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87193,9 +87241,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(761); -var isAccessor = __webpack_require__(762); -var isData = __webpack_require__(764); +var typeOf = __webpack_require__(762); +var isAccessor = __webpack_require__(763); +var isData = __webpack_require__(765); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -87209,7 +87257,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 761 */ +/* 762 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87344,7 +87392,7 @@ function isBuffer(val) { /***/ }), -/* 762 */ +/* 763 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87357,7 +87405,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(763); +var typeOf = __webpack_require__(764); // accessor descriptor properties var accessor = { @@ -87420,7 +87468,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 763 */ +/* 764 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87555,7 +87603,7 @@ function isBuffer(val) { /***/ }), -/* 764 */ +/* 765 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87568,7 +87616,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(765); +var typeOf = __webpack_require__(766); module.exports = function isDataDescriptor(obj, prop) { // data descriptor properties @@ -87611,7 +87659,7 @@ module.exports = function isDataDescriptor(obj, prop) { /***/ }), -/* 765 */ +/* 766 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87746,13 +87794,13 @@ function isBuffer(val) { /***/ }), -/* 766 */ +/* 767 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(753); +var typeOf = __webpack_require__(754); var utils = module.exports; /** @@ -88772,17 +88820,17 @@ function assert(val, message) { /***/ }), -/* 767 */ +/* 768 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(738); -var Snapdragon = __webpack_require__(768); -var compilers = __webpack_require__(742); -var parsers = __webpack_require__(757); -var utils = __webpack_require__(743); +var extend = __webpack_require__(739); +var Snapdragon = __webpack_require__(769); +var compilers = __webpack_require__(743); +var parsers = __webpack_require__(758); +var utils = __webpack_require__(744); /** * Customize Snapdragon parser and renderer @@ -88883,17 +88931,17 @@ module.exports = Braces; /***/ }), -/* 768 */ +/* 769 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Base = __webpack_require__(769); -var define = __webpack_require__(730); -var Compiler = __webpack_require__(798); -var Parser = __webpack_require__(827); -var utils = __webpack_require__(807); +var Base = __webpack_require__(770); +var define = __webpack_require__(731); +var Compiler = __webpack_require__(799); +var Parser = __webpack_require__(828); +var utils = __webpack_require__(808); var regexCache = {}; var cache = {}; @@ -89064,20 +89112,20 @@ module.exports.Parser = Parser; /***/ }), -/* 769 */ +/* 770 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var define = __webpack_require__(770); -var CacheBase = __webpack_require__(771); -var Emitter = __webpack_require__(772); -var isObject = __webpack_require__(748); -var merge = __webpack_require__(789); -var pascal = __webpack_require__(792); -var cu = __webpack_require__(793); +var define = __webpack_require__(771); +var CacheBase = __webpack_require__(772); +var Emitter = __webpack_require__(773); +var isObject = __webpack_require__(749); +var merge = __webpack_require__(790); +var pascal = __webpack_require__(793); +var cu = __webpack_require__(794); /** * Optionally define a custom `cache` namespace to use. @@ -89506,7 +89554,7 @@ module.exports.namespace = namespace; /***/ }), -/* 770 */ +/* 771 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89519,7 +89567,7 @@ module.exports.namespace = namespace; -var isDescriptor = __webpack_require__(760); +var isDescriptor = __webpack_require__(761); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -89544,21 +89592,21 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 771 */ +/* 772 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(748); -var Emitter = __webpack_require__(772); -var visit = __webpack_require__(773); -var toPath = __webpack_require__(776); -var union = __webpack_require__(777); -var del = __webpack_require__(781); -var get = __webpack_require__(779); -var has = __webpack_require__(786); -var set = __webpack_require__(780); +var isObject = __webpack_require__(749); +var Emitter = __webpack_require__(773); +var visit = __webpack_require__(774); +var toPath = __webpack_require__(777); +var union = __webpack_require__(778); +var del = __webpack_require__(782); +var get = __webpack_require__(780); +var has = __webpack_require__(787); +var set = __webpack_require__(781); /** * Create a `Cache` constructor that when instantiated will @@ -89812,7 +89860,7 @@ module.exports.namespace = namespace; /***/ }), -/* 772 */ +/* 773 */ /***/ (function(module, exports, __webpack_require__) { @@ -89981,7 +90029,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 773 */ +/* 774 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89994,8 +90042,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(774); -var mapVisit = __webpack_require__(775); +var visit = __webpack_require__(775); +var mapVisit = __webpack_require__(776); module.exports = function(collection, method, val) { var result; @@ -90018,7 +90066,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 774 */ +/* 775 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90031,7 +90079,7 @@ module.exports = function(collection, method, val) { -var isObject = __webpack_require__(748); +var isObject = __webpack_require__(749); module.exports = function visit(thisArg, method, target, val) { if (!isObject(thisArg) && typeof thisArg !== 'function') { @@ -90058,14 +90106,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 775 */ +/* 776 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var visit = __webpack_require__(774); +var visit = __webpack_require__(775); /** * Map `visit` over an array of objects. @@ -90102,7 +90150,7 @@ function isObject(val) { /***/ }), -/* 776 */ +/* 777 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90115,7 +90163,7 @@ function isObject(val) { -var typeOf = __webpack_require__(753); +var typeOf = __webpack_require__(754); module.exports = function toPath(args) { if (typeOf(args) !== 'arguments') { @@ -90142,16 +90190,16 @@ function filter(arr) { /***/ }), -/* 777 */ +/* 778 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(739); -var union = __webpack_require__(778); -var get = __webpack_require__(779); -var set = __webpack_require__(780); +var isObject = __webpack_require__(740); +var union = __webpack_require__(779); +var get = __webpack_require__(780); +var set = __webpack_require__(781); module.exports = function unionValue(obj, prop, value) { if (!isObject(obj)) { @@ -90179,7 +90227,7 @@ function arrayify(val) { /***/ }), -/* 778 */ +/* 779 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90215,7 +90263,7 @@ module.exports = function union(init) { /***/ }), -/* 779 */ +/* 780 */ /***/ (function(module, exports) { /*! @@ -90271,7 +90319,7 @@ function toString(val) { /***/ }), -/* 780 */ +/* 781 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90284,10 +90332,10 @@ function toString(val) { -var split = __webpack_require__(744); -var extend = __webpack_require__(738); -var isPlainObject = __webpack_require__(747); -var isObject = __webpack_require__(739); +var split = __webpack_require__(745); +var extend = __webpack_require__(739); +var isPlainObject = __webpack_require__(748); +var isObject = __webpack_require__(740); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -90333,7 +90381,7 @@ function isValidKey(key) { /***/ }), -/* 781 */ +/* 782 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90346,8 +90394,8 @@ function isValidKey(key) { -var isObject = __webpack_require__(748); -var has = __webpack_require__(782); +var isObject = __webpack_require__(749); +var has = __webpack_require__(783); module.exports = function unset(obj, prop) { if (!isObject(obj)) { @@ -90372,7 +90420,7 @@ module.exports = function unset(obj, prop) { /***/ }), -/* 782 */ +/* 783 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90385,9 +90433,9 @@ module.exports = function unset(obj, prop) { -var isObject = __webpack_require__(783); -var hasValues = __webpack_require__(785); -var get = __webpack_require__(779); +var isObject = __webpack_require__(784); +var hasValues = __webpack_require__(786); +var get = __webpack_require__(780); module.exports = function(obj, prop, noZero) { if (isObject(obj)) { @@ -90398,7 +90446,7 @@ module.exports = function(obj, prop, noZero) { /***/ }), -/* 783 */ +/* 784 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90411,7 +90459,7 @@ module.exports = function(obj, prop, noZero) { -var isArray = __webpack_require__(784); +var isArray = __webpack_require__(785); module.exports = function isObject(val) { return val != null && typeof val === 'object' && isArray(val) === false; @@ -90419,7 +90467,7 @@ module.exports = function isObject(val) { /***/ }), -/* 784 */ +/* 785 */ /***/ (function(module, exports) { var toString = {}.toString; @@ -90430,7 +90478,7 @@ module.exports = Array.isArray || function (arr) { /***/ }), -/* 785 */ +/* 786 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90473,7 +90521,7 @@ module.exports = function hasValue(o, noZero) { /***/ }), -/* 786 */ +/* 787 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90486,9 +90534,9 @@ module.exports = function hasValue(o, noZero) { -var isObject = __webpack_require__(748); -var hasValues = __webpack_require__(787); -var get = __webpack_require__(779); +var isObject = __webpack_require__(749); +var hasValues = __webpack_require__(788); +var get = __webpack_require__(780); module.exports = function(val, prop) { return hasValues(isObject(val) && prop ? get(val, prop) : val); @@ -90496,7 +90544,7 @@ module.exports = function(val, prop) { /***/ }), -/* 787 */ +/* 788 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90509,8 +90557,8 @@ module.exports = function(val, prop) { -var typeOf = __webpack_require__(788); -var isNumber = __webpack_require__(752); +var typeOf = __webpack_require__(789); +var isNumber = __webpack_require__(753); module.exports = function hasValue(val) { // is-number checks for NaN and other edge cases @@ -90563,10 +90611,10 @@ module.exports = function hasValue(val) { /***/ }), -/* 788 */ +/* 789 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(735); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -90688,14 +90736,14 @@ module.exports = function kindOf(val) { /***/ }), -/* 789 */ +/* 790 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(790); -var forIn = __webpack_require__(791); +var isExtendable = __webpack_require__(791); +var forIn = __webpack_require__(792); function mixinDeep(target, objects) { var len = arguments.length, i = 0; @@ -90759,7 +90807,7 @@ module.exports = mixinDeep; /***/ }), -/* 790 */ +/* 791 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90772,7 +90820,7 @@ module.exports = mixinDeep; -var isPlainObject = __webpack_require__(747); +var isPlainObject = __webpack_require__(748); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -90780,7 +90828,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 791 */ +/* 792 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90803,7 +90851,7 @@ module.exports = function forIn(obj, fn, thisArg) { /***/ }), -/* 792 */ +/* 793 */ /***/ (function(module, exports) { /*! @@ -90830,14 +90878,14 @@ module.exports = pascalcase; /***/ }), -/* 793 */ +/* 794 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var utils = __webpack_require__(794); +var utils = __webpack_require__(795); /** * Expose class utils @@ -91202,7 +91250,7 @@ cu.bubble = function(Parent, events) { /***/ }), -/* 794 */ +/* 795 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91216,10 +91264,10 @@ var utils = {}; * Lazily required module dependencies */ -utils.union = __webpack_require__(778); -utils.define = __webpack_require__(730); -utils.isObj = __webpack_require__(748); -utils.staticExtend = __webpack_require__(795); +utils.union = __webpack_require__(779); +utils.define = __webpack_require__(731); +utils.isObj = __webpack_require__(749); +utils.staticExtend = __webpack_require__(796); /** @@ -91230,7 +91278,7 @@ module.exports = utils; /***/ }), -/* 795 */ +/* 796 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91243,8 +91291,8 @@ module.exports = utils; -var copy = __webpack_require__(796); -var define = __webpack_require__(730); +var copy = __webpack_require__(797); +var define = __webpack_require__(731); var util = __webpack_require__(29); /** @@ -91327,15 +91375,15 @@ module.exports = extend; /***/ }), -/* 796 */ +/* 797 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(753); -var copyDescriptor = __webpack_require__(797); -var define = __webpack_require__(730); +var typeOf = __webpack_require__(754); +var copyDescriptor = __webpack_require__(798); +var define = __webpack_require__(731); /** * Copy static properties, prototype properties, and descriptors from one object to another. @@ -91508,7 +91556,7 @@ module.exports.has = has; /***/ }), -/* 797 */ +/* 798 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91596,16 +91644,16 @@ function isObject(val) { /***/ }), -/* 798 */ +/* 799 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(799); -var define = __webpack_require__(730); -var debug = __webpack_require__(801)('snapdragon:compiler'); -var utils = __webpack_require__(807); +var use = __webpack_require__(800); +var define = __webpack_require__(731); +var debug = __webpack_require__(802)('snapdragon:compiler'); +var utils = __webpack_require__(808); /** * Create a new `Compiler` with the given `options`. @@ -91759,7 +91807,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(826); + var sourcemaps = __webpack_require__(827); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -91780,7 +91828,7 @@ module.exports = Compiler; /***/ }), -/* 799 */ +/* 800 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91793,7 +91841,7 @@ module.exports = Compiler; -var utils = __webpack_require__(800); +var utils = __webpack_require__(801); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -91908,7 +91956,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 800 */ +/* 801 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91922,8 +91970,8 @@ var utils = {}; * Lazily required module dependencies */ -utils.define = __webpack_require__(730); -utils.isObject = __webpack_require__(748); +utils.define = __webpack_require__(731); +utils.isObject = __webpack_require__(749); utils.isString = function(val) { @@ -91938,7 +91986,7 @@ module.exports = utils; /***/ }), -/* 801 */ +/* 802 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -91947,14 +91995,14 @@ module.exports = utils; */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(802); + module.exports = __webpack_require__(803); } else { - module.exports = __webpack_require__(805); + module.exports = __webpack_require__(806); } /***/ }), -/* 802 */ +/* 803 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -91963,7 +92011,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(803); +exports = module.exports = __webpack_require__(804); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -92145,7 +92193,7 @@ function localstorage() { /***/ }), -/* 803 */ +/* 804 */ /***/ (function(module, exports, __webpack_require__) { @@ -92161,7 +92209,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(804); +exports.humanize = __webpack_require__(805); /** * The currently active debug mode names, and names to skip. @@ -92353,7 +92401,7 @@ function coerce(val) { /***/ }), -/* 804 */ +/* 805 */ /***/ (function(module, exports) { /** @@ -92511,7 +92559,7 @@ function plural(ms, n, name) { /***/ }), -/* 805 */ +/* 806 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -92527,7 +92575,7 @@ var util = __webpack_require__(29); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(803); +exports = module.exports = __webpack_require__(804); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -92706,7 +92754,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(806); + var net = __webpack_require__(807); stream = new net.Socket({ fd: fd, readable: false, @@ -92765,13 +92813,13 @@ exports.enable(load()); /***/ }), -/* 806 */ +/* 807 */ /***/ (function(module, exports) { module.exports = require("net"); /***/ }), -/* 807 */ +/* 808 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92781,9 +92829,9 @@ module.exports = require("net"); * Module dependencies */ -exports.extend = __webpack_require__(738); -exports.SourceMap = __webpack_require__(808); -exports.sourceMapResolve = __webpack_require__(819); +exports.extend = __webpack_require__(739); +exports.SourceMap = __webpack_require__(809); +exports.sourceMapResolve = __webpack_require__(820); /** * Convert backslash in the given string to forward slashes @@ -92826,7 +92874,7 @@ exports.last = function(arr, n) { /***/ }), -/* 808 */ +/* 809 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -92834,13 +92882,13 @@ exports.last = function(arr, n) { * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ -exports.SourceMapGenerator = __webpack_require__(809).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(815).SourceMapConsumer; -exports.SourceNode = __webpack_require__(818).SourceNode; +exports.SourceMapGenerator = __webpack_require__(810).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(816).SourceMapConsumer; +exports.SourceNode = __webpack_require__(819).SourceNode; /***/ }), -/* 809 */ +/* 810 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -92850,10 +92898,10 @@ exports.SourceNode = __webpack_require__(818).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(810); -var util = __webpack_require__(812); -var ArraySet = __webpack_require__(813).ArraySet; -var MappingList = __webpack_require__(814).MappingList; +var base64VLQ = __webpack_require__(811); +var util = __webpack_require__(813); +var ArraySet = __webpack_require__(814).ArraySet; +var MappingList = __webpack_require__(815).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -93262,7 +93310,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 810 */ +/* 811 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93302,7 +93350,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(811); +var base64 = __webpack_require__(812); // A single base 64 digit can contain 6 bits of data. For the base 64 variable // length quantities we use in the source map spec, the first bit is the sign, @@ -93408,7 +93456,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 811 */ +/* 812 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93481,7 +93529,7 @@ exports.decode = function (charCode) { /***/ }), -/* 812 */ +/* 813 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93904,7 +93952,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 813 */ +/* 814 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93914,7 +93962,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(812); +var util = __webpack_require__(813); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -94031,7 +94079,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 814 */ +/* 815 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -94041,7 +94089,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(812); +var util = __webpack_require__(813); /** * Determine whether mappingB is after mappingA with respect to generated @@ -94116,7 +94164,7 @@ exports.MappingList = MappingList; /***/ }), -/* 815 */ +/* 816 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -94126,11 +94174,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(812); -var binarySearch = __webpack_require__(816); -var ArraySet = __webpack_require__(813).ArraySet; -var base64VLQ = __webpack_require__(810); -var quickSort = __webpack_require__(817).quickSort; +var util = __webpack_require__(813); +var binarySearch = __webpack_require__(817); +var ArraySet = __webpack_require__(814).ArraySet; +var base64VLQ = __webpack_require__(811); +var quickSort = __webpack_require__(818).quickSort; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -95204,7 +95252,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 816 */ +/* 817 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95321,7 +95369,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 817 */ +/* 818 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95441,7 +95489,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 818 */ +/* 819 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95451,8 +95499,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(809).SourceMapGenerator; -var util = __webpack_require__(812); +var SourceMapGenerator = __webpack_require__(810).SourceMapGenerator; +var util = __webpack_require__(813); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -95860,17 +95908,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 819 */ +/* 820 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(820) -var resolveUrl = __webpack_require__(821) -var decodeUriComponent = __webpack_require__(822) -var urix = __webpack_require__(824) -var atob = __webpack_require__(825) +var sourceMappingURL = __webpack_require__(821) +var resolveUrl = __webpack_require__(822) +var decodeUriComponent = __webpack_require__(823) +var urix = __webpack_require__(825) +var atob = __webpack_require__(826) @@ -96168,7 +96216,7 @@ module.exports = { /***/ }), -/* 820 */ +/* 821 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -96231,7 +96279,7 @@ void (function(root, factory) { /***/ }), -/* 821 */ +/* 822 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -96249,13 +96297,13 @@ module.exports = resolveUrl /***/ }), -/* 822 */ +/* 823 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(823) +var decodeUriComponent = __webpack_require__(824) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -96266,7 +96314,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 823 */ +/* 824 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96367,7 +96415,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 824 */ +/* 825 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -96390,7 +96438,7 @@ module.exports = urix /***/ }), -/* 825 */ +/* 826 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96404,7 +96452,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 826 */ +/* 827 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96412,8 +96460,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(23); var path = __webpack_require__(16); -var define = __webpack_require__(730); -var utils = __webpack_require__(807); +var define = __webpack_require__(731); +var utils = __webpack_require__(808); /** * Expose `mixin()`. @@ -96556,19 +96604,19 @@ exports.comment = function(node) { /***/ }), -/* 827 */ +/* 828 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(799); +var use = __webpack_require__(800); var util = __webpack_require__(29); -var Cache = __webpack_require__(828); -var define = __webpack_require__(730); -var debug = __webpack_require__(801)('snapdragon:parser'); -var Position = __webpack_require__(829); -var utils = __webpack_require__(807); +var Cache = __webpack_require__(829); +var define = __webpack_require__(731); +var debug = __webpack_require__(802)('snapdragon:parser'); +var Position = __webpack_require__(830); +var utils = __webpack_require__(808); /** * Create a new `Parser` with the given `input` and `options`. @@ -97096,7 +97144,7 @@ module.exports = Parser; /***/ }), -/* 828 */ +/* 829 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -97203,13 +97251,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 829 */ +/* 830 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(730); +var define = __webpack_require__(731); /** * Store position for a node @@ -97224,16 +97272,16 @@ module.exports = function Position(start, parser) { /***/ }), -/* 830 */ +/* 831 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var safe = __webpack_require__(831); -var define = __webpack_require__(837); -var extend = __webpack_require__(838); -var not = __webpack_require__(840); +var safe = __webpack_require__(832); +var define = __webpack_require__(838); +var extend = __webpack_require__(839); +var not = __webpack_require__(841); var MAX_LENGTH = 1024 * 64; /** @@ -97386,10 +97434,10 @@ module.exports.makeRe = makeRe; /***/ }), -/* 831 */ +/* 832 */ /***/ (function(module, exports, __webpack_require__) { -var parse = __webpack_require__(832); +var parse = __webpack_require__(833); var types = parse.types; module.exports = function (re, opts) { @@ -97435,13 +97483,13 @@ function isRegExp (x) { /***/ }), -/* 832 */ +/* 833 */ /***/ (function(module, exports, __webpack_require__) { -var util = __webpack_require__(833); -var types = __webpack_require__(834); -var sets = __webpack_require__(835); -var positions = __webpack_require__(836); +var util = __webpack_require__(834); +var types = __webpack_require__(835); +var sets = __webpack_require__(836); +var positions = __webpack_require__(837); module.exports = function(regexpStr) { @@ -97723,11 +97771,11 @@ module.exports.types = types; /***/ }), -/* 833 */ +/* 834 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(834); -var sets = __webpack_require__(835); +var types = __webpack_require__(835); +var sets = __webpack_require__(836); // All of these are private and only used by randexp. @@ -97840,7 +97888,7 @@ exports.error = function(regexp, msg) { /***/ }), -/* 834 */ +/* 835 */ /***/ (function(module, exports) { module.exports = { @@ -97856,10 +97904,10 @@ module.exports = { /***/ }), -/* 835 */ +/* 836 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(834); +var types = __webpack_require__(835); var INTS = function() { return [{ type: types.RANGE , from: 48, to: 57 }]; @@ -97944,10 +97992,10 @@ exports.anyChar = function() { /***/ }), -/* 836 */ +/* 837 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(834); +var types = __webpack_require__(835); exports.wordBoundary = function() { return { type: types.POSITION, value: 'b' }; @@ -97967,7 +98015,7 @@ exports.end = function() { /***/ }), -/* 837 */ +/* 838 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -97980,8 +98028,8 @@ exports.end = function() { -var isobject = __webpack_require__(748); -var isDescriptor = __webpack_require__(760); +var isobject = __webpack_require__(749); +var isDescriptor = __webpack_require__(761); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -98012,14 +98060,14 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 838 */ +/* 839 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(839); -var assignSymbols = __webpack_require__(749); +var isExtendable = __webpack_require__(840); +var assignSymbols = __webpack_require__(750); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -98079,7 +98127,7 @@ function isEnum(obj, key) { /***/ }), -/* 839 */ +/* 840 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -98092,7 +98140,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(747); +var isPlainObject = __webpack_require__(748); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -98100,14 +98148,14 @@ module.exports = function isExtendable(val) { /***/ }), -/* 840 */ +/* 841 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(838); -var safe = __webpack_require__(831); +var extend = __webpack_require__(839); +var safe = __webpack_require__(832); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -98179,14 +98227,14 @@ module.exports = toRegex; /***/ }), -/* 841 */ +/* 842 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(842); -var extglob = __webpack_require__(857); +var nanomatch = __webpack_require__(843); +var extglob = __webpack_require__(858); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -98263,7 +98311,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 842 */ +/* 843 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -98274,17 +98322,17 @@ function escapeExtglobs(compiler) { */ var util = __webpack_require__(29); -var toRegex = __webpack_require__(729); -var extend = __webpack_require__(843); +var toRegex = __webpack_require__(730); +var extend = __webpack_require__(844); /** * Local dependencies */ -var compilers = __webpack_require__(845); -var parsers = __webpack_require__(846); -var cache = __webpack_require__(849); -var utils = __webpack_require__(851); +var compilers = __webpack_require__(846); +var parsers = __webpack_require__(847); +var cache = __webpack_require__(850); +var utils = __webpack_require__(852); var MAX_LENGTH = 1024 * 64; /** @@ -99108,14 +99156,14 @@ module.exports = nanomatch; /***/ }), -/* 843 */ +/* 844 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(844); -var assignSymbols = __webpack_require__(749); +var isExtendable = __webpack_require__(845); +var assignSymbols = __webpack_require__(750); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -99175,7 +99223,7 @@ function isEnum(obj, key) { /***/ }), -/* 844 */ +/* 845 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99188,7 +99236,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(747); +var isPlainObject = __webpack_require__(748); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -99196,7 +99244,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 845 */ +/* 846 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99542,15 +99590,15 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 846 */ +/* 847 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regexNot = __webpack_require__(740); -var toRegex = __webpack_require__(729); -var isOdd = __webpack_require__(847); +var regexNot = __webpack_require__(741); +var toRegex = __webpack_require__(730); +var isOdd = __webpack_require__(848); /** * Characters to use in negation regex (we want to "not" match @@ -99936,7 +99984,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 847 */ +/* 848 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99949,7 +99997,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(848); +var isNumber = __webpack_require__(849); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -99963,7 +100011,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 848 */ +/* 849 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99991,14 +100039,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 849 */ +/* 850 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(850))(); +module.exports = new (__webpack_require__(851))(); /***/ }), -/* 850 */ +/* 851 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100011,7 +100059,7 @@ module.exports = new (__webpack_require__(850))(); -var MapCache = __webpack_require__(828); +var MapCache = __webpack_require__(829); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -100133,7 +100181,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 851 */ +/* 852 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100146,14 +100194,14 @@ var path = __webpack_require__(16); * Module dependencies */ -var isWindows = __webpack_require__(852)(); -var Snapdragon = __webpack_require__(768); -utils.define = __webpack_require__(853); -utils.diff = __webpack_require__(854); -utils.extend = __webpack_require__(843); -utils.pick = __webpack_require__(855); -utils.typeOf = __webpack_require__(856); -utils.unique = __webpack_require__(741); +var isWindows = __webpack_require__(853)(); +var Snapdragon = __webpack_require__(769); +utils.define = __webpack_require__(854); +utils.diff = __webpack_require__(855); +utils.extend = __webpack_require__(844); +utils.pick = __webpack_require__(856); +utils.typeOf = __webpack_require__(857); +utils.unique = __webpack_require__(742); /** * Returns true if the given value is effectively an empty string @@ -100519,7 +100567,7 @@ utils.unixify = function(options) { /***/ }), -/* 852 */ +/* 853 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -100547,7 +100595,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 853 */ +/* 854 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100560,8 +100608,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ -var isobject = __webpack_require__(748); -var isDescriptor = __webpack_require__(760); +var isobject = __webpack_require__(749); +var isDescriptor = __webpack_require__(761); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -100592,7 +100640,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 854 */ +/* 855 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100646,7 +100694,7 @@ function diffArray(one, two) { /***/ }), -/* 855 */ +/* 856 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100659,7 +100707,7 @@ function diffArray(one, two) { -var isObject = __webpack_require__(748); +var isObject = __webpack_require__(749); module.exports = function pick(obj, keys) { if (!isObject(obj) && typeof obj !== 'function') { @@ -100688,7 +100736,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 856 */ +/* 857 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -100823,7 +100871,7 @@ function isBuffer(val) { /***/ }), -/* 857 */ +/* 858 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100833,18 +100881,18 @@ function isBuffer(val) { * Module dependencies */ -var extend = __webpack_require__(738); -var unique = __webpack_require__(741); -var toRegex = __webpack_require__(729); +var extend = __webpack_require__(739); +var unique = __webpack_require__(742); +var toRegex = __webpack_require__(730); /** * Local dependencies */ -var compilers = __webpack_require__(858); -var parsers = __webpack_require__(869); -var Extglob = __webpack_require__(872); -var utils = __webpack_require__(871); +var compilers = __webpack_require__(859); +var parsers = __webpack_require__(870); +var Extglob = __webpack_require__(873); +var utils = __webpack_require__(872); var MAX_LENGTH = 1024 * 64; /** @@ -101161,13 +101209,13 @@ module.exports = extglob; /***/ }), -/* 858 */ +/* 859 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(859); +var brackets = __webpack_require__(860); /** * Extglob compilers @@ -101337,7 +101385,7 @@ module.exports = function(extglob) { /***/ }), -/* 859 */ +/* 860 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101347,17 +101395,17 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(860); -var parsers = __webpack_require__(862); +var compilers = __webpack_require__(861); +var parsers = __webpack_require__(863); /** * Module dependencies */ -var debug = __webpack_require__(864)('expand-brackets'); -var extend = __webpack_require__(738); -var Snapdragon = __webpack_require__(768); -var toRegex = __webpack_require__(729); +var debug = __webpack_require__(865)('expand-brackets'); +var extend = __webpack_require__(739); +var Snapdragon = __webpack_require__(769); +var toRegex = __webpack_require__(730); /** * Parses the given POSIX character class `pattern` and returns a @@ -101555,13 +101603,13 @@ module.exports = brackets; /***/ }), -/* 860 */ +/* 861 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(861); +var posix = __webpack_require__(862); module.exports = function(brackets) { brackets.compiler @@ -101649,7 +101697,7 @@ module.exports = function(brackets) { /***/ }), -/* 861 */ +/* 862 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101678,14 +101726,14 @@ module.exports = { /***/ }), -/* 862 */ +/* 863 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(863); -var define = __webpack_require__(730); +var utils = __webpack_require__(864); +var define = __webpack_require__(731); /** * Text regex @@ -101904,14 +101952,14 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 863 */ +/* 864 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var toRegex = __webpack_require__(729); -var regexNot = __webpack_require__(740); +var toRegex = __webpack_require__(730); +var regexNot = __webpack_require__(741); var cached; /** @@ -101945,7 +101993,7 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 864 */ +/* 865 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -101954,14 +102002,14 @@ exports.createRegex = function(pattern, include) { */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(865); + module.exports = __webpack_require__(866); } else { - module.exports = __webpack_require__(868); + module.exports = __webpack_require__(869); } /***/ }), -/* 865 */ +/* 866 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -101970,7 +102018,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(866); +exports = module.exports = __webpack_require__(867); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -102152,7 +102200,7 @@ function localstorage() { /***/ }), -/* 866 */ +/* 867 */ /***/ (function(module, exports, __webpack_require__) { @@ -102168,7 +102216,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(867); +exports.humanize = __webpack_require__(868); /** * The currently active debug mode names, and names to skip. @@ -102360,7 +102408,7 @@ function coerce(val) { /***/ }), -/* 867 */ +/* 868 */ /***/ (function(module, exports) { /** @@ -102518,7 +102566,7 @@ function plural(ms, n, name) { /***/ }), -/* 868 */ +/* 869 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -102534,7 +102582,7 @@ var util = __webpack_require__(29); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(866); +exports = module.exports = __webpack_require__(867); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -102713,7 +102761,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(806); + var net = __webpack_require__(807); stream = new net.Socket({ fd: fd, readable: false, @@ -102772,15 +102820,15 @@ exports.enable(load()); /***/ }), -/* 869 */ +/* 870 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(859); -var define = __webpack_require__(870); -var utils = __webpack_require__(871); +var brackets = __webpack_require__(860); +var define = __webpack_require__(871); +var utils = __webpack_require__(872); /** * Characters to use in text regex (we want to "not" match @@ -102935,7 +102983,7 @@ module.exports = parsers; /***/ }), -/* 870 */ +/* 871 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102948,7 +102996,7 @@ module.exports = parsers; -var isDescriptor = __webpack_require__(760); +var isDescriptor = __webpack_require__(761); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -102973,14 +103021,14 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 871 */ +/* 872 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regex = __webpack_require__(740); -var Cache = __webpack_require__(850); +var regex = __webpack_require__(741); +var Cache = __webpack_require__(851); /** * Utils @@ -103049,7 +103097,7 @@ utils.createRegex = function(str) { /***/ }), -/* 872 */ +/* 873 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103059,16 +103107,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(768); -var define = __webpack_require__(870); -var extend = __webpack_require__(738); +var Snapdragon = __webpack_require__(769); +var define = __webpack_require__(871); +var extend = __webpack_require__(739); /** * Local dependencies */ -var compilers = __webpack_require__(858); -var parsers = __webpack_require__(869); +var compilers = __webpack_require__(859); +var parsers = __webpack_require__(870); /** * Customize Snapdragon parser and renderer @@ -103134,16 +103182,16 @@ module.exports = Extglob; /***/ }), -/* 873 */ +/* 874 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(857); -var nanomatch = __webpack_require__(842); -var regexNot = __webpack_require__(740); -var toRegex = __webpack_require__(830); +var extglob = __webpack_require__(858); +var nanomatch = __webpack_require__(843); +var regexNot = __webpack_require__(741); +var toRegex = __webpack_require__(831); var not; /** @@ -103224,14 +103272,14 @@ function textRegex(pattern) { /***/ }), -/* 874 */ +/* 875 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(850))(); +module.exports = new (__webpack_require__(851))(); /***/ }), -/* 875 */ +/* 876 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103244,13 +103292,13 @@ var path = __webpack_require__(16); * Module dependencies */ -var Snapdragon = __webpack_require__(768); -utils.define = __webpack_require__(837); -utils.diff = __webpack_require__(854); -utils.extend = __webpack_require__(838); -utils.pick = __webpack_require__(855); -utils.typeOf = __webpack_require__(876); -utils.unique = __webpack_require__(741); +var Snapdragon = __webpack_require__(769); +utils.define = __webpack_require__(838); +utils.diff = __webpack_require__(855); +utils.extend = __webpack_require__(839); +utils.pick = __webpack_require__(856); +utils.typeOf = __webpack_require__(877); +utils.unique = __webpack_require__(742); /** * Returns true if the platform is windows, or `path.sep` is `\\`. @@ -103547,7 +103595,7 @@ utils.unixify = function(options) { /***/ }), -/* 876 */ +/* 877 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -103682,7 +103730,7 @@ function isBuffer(val) { /***/ }), -/* 877 */ +/* 878 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103701,9 +103749,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(878); -var reader_1 = __webpack_require__(891); -var fs_stream_1 = __webpack_require__(895); +var readdir = __webpack_require__(879); +var reader_1 = __webpack_require__(892); +var fs_stream_1 = __webpack_require__(896); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -103764,15 +103812,15 @@ exports.default = ReaderAsync; /***/ }), -/* 878 */ +/* 879 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(879); -const readdirAsync = __webpack_require__(887); -const readdirStream = __webpack_require__(890); +const readdirSync = __webpack_require__(880); +const readdirAsync = __webpack_require__(888); +const readdirStream = __webpack_require__(891); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -103856,7 +103904,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 879 */ +/* 880 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103864,11 +103912,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(880); +const DirectoryReader = __webpack_require__(881); let syncFacade = { - fs: __webpack_require__(885), - forEach: __webpack_require__(886), + fs: __webpack_require__(886), + forEach: __webpack_require__(887), sync: true }; @@ -103897,7 +103945,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 880 */ +/* 881 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103906,9 +103954,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(27).Readable; const EventEmitter = __webpack_require__(379).EventEmitter; const path = __webpack_require__(16); -const normalizeOptions = __webpack_require__(881); -const stat = __webpack_require__(883); -const call = __webpack_require__(884); +const normalizeOptions = __webpack_require__(882); +const stat = __webpack_require__(884); +const call = __webpack_require__(885); /** * Asynchronously reads the contents of a directory and streams the results @@ -104284,14 +104332,14 @@ module.exports = DirectoryReader; /***/ }), -/* 881 */ +/* 882 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const globToRegExp = __webpack_require__(882); +const globToRegExp = __webpack_require__(883); module.exports = normalizeOptions; @@ -104468,7 +104516,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 882 */ +/* 883 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -104605,13 +104653,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 883 */ +/* 884 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(884); +const call = __webpack_require__(885); module.exports = stat; @@ -104686,7 +104734,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 884 */ +/* 885 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104747,14 +104795,14 @@ function callOnce (fn) { /***/ }), -/* 885 */ +/* 886 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const call = __webpack_require__(884); +const call = __webpack_require__(885); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -104818,7 +104866,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 886 */ +/* 887 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104847,7 +104895,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 887 */ +/* 888 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104855,12 +104903,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(888); -const DirectoryReader = __webpack_require__(880); +const maybe = __webpack_require__(889); +const DirectoryReader = __webpack_require__(881); let asyncFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(889), + forEach: __webpack_require__(890), async: true }; @@ -104902,7 +104950,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 888 */ +/* 889 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104929,7 +104977,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 889 */ +/* 890 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104965,7 +105013,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 890 */ +/* 891 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104973,11 +105021,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(880); +const DirectoryReader = __webpack_require__(881); let streamFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(889), + forEach: __webpack_require__(890), async: true }; @@ -104997,16 +105045,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 891 */ +/* 892 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(16); -var deep_1 = __webpack_require__(892); -var entry_1 = __webpack_require__(894); -var pathUtil = __webpack_require__(893); +var deep_1 = __webpack_require__(893); +var entry_1 = __webpack_require__(895); +var pathUtil = __webpack_require__(894); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -105072,14 +105120,14 @@ exports.default = Reader; /***/ }), -/* 892 */ +/* 893 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(893); -var patternUtils = __webpack_require__(722); +var pathUtils = __webpack_require__(894); +var patternUtils = __webpack_require__(723); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { this.options = options; @@ -105162,7 +105210,7 @@ exports.default = DeepFilter; /***/ }), -/* 893 */ +/* 894 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105193,14 +105241,14 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 894 */ +/* 895 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(893); -var patternUtils = __webpack_require__(722); +var pathUtils = __webpack_require__(894); +var patternUtils = __webpack_require__(723); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { this.options = options; @@ -105285,7 +105333,7 @@ exports.default = EntryFilter; /***/ }), -/* 895 */ +/* 896 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105305,8 +105353,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var fsStat = __webpack_require__(896); -var fs_1 = __webpack_require__(900); +var fsStat = __webpack_require__(897); +var fs_1 = __webpack_require__(901); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -105356,14 +105404,14 @@ exports.default = FileSystemStream; /***/ }), -/* 896 */ +/* 897 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(897); -const statProvider = __webpack_require__(899); +const optionsManager = __webpack_require__(898); +const statProvider = __webpack_require__(900); /** * Asynchronous API. */ @@ -105394,13 +105442,13 @@ exports.statSync = statSync; /***/ }), -/* 897 */ +/* 898 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(898); +const fsAdapter = __webpack_require__(899); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -105413,7 +105461,7 @@ exports.prepare = prepare; /***/ }), -/* 898 */ +/* 899 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105436,7 +105484,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 899 */ +/* 900 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105488,7 +105536,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 900 */ +/* 901 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105519,7 +105567,7 @@ exports.default = FileSystem; /***/ }), -/* 901 */ +/* 902 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105539,9 +105587,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var readdir = __webpack_require__(878); -var reader_1 = __webpack_require__(891); -var fs_stream_1 = __webpack_require__(895); +var readdir = __webpack_require__(879); +var reader_1 = __webpack_require__(892); +var fs_stream_1 = __webpack_require__(896); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -105609,7 +105657,7 @@ exports.default = ReaderStream; /***/ }), -/* 902 */ +/* 903 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105628,9 +105676,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(878); -var reader_1 = __webpack_require__(891); -var fs_sync_1 = __webpack_require__(903); +var readdir = __webpack_require__(879); +var reader_1 = __webpack_require__(892); +var fs_sync_1 = __webpack_require__(904); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -105690,7 +105738,7 @@ exports.default = ReaderSync; /***/ }), -/* 903 */ +/* 904 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105709,8 +105757,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(896); -var fs_1 = __webpack_require__(900); +var fsStat = __webpack_require__(897); +var fs_1 = __webpack_require__(901); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -105756,7 +105804,7 @@ exports.default = FileSystemSync; /***/ }), -/* 904 */ +/* 905 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105772,7 +105820,7 @@ exports.flatten = flatten; /***/ }), -/* 905 */ +/* 906 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105793,13 +105841,13 @@ exports.merge = merge; /***/ }), -/* 906 */ +/* 907 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const pathType = __webpack_require__(907); +const pathType = __webpack_require__(908); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -105865,13 +105913,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 907 */ +/* 908 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const pify = __webpack_require__(908); +const pify = __webpack_require__(909); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -105914,7 +105962,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 908 */ +/* 909 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106005,17 +106053,17 @@ module.exports = (obj, opts) => { /***/ }), -/* 909 */ +/* 910 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); const path = __webpack_require__(16); -const fastGlob = __webpack_require__(718); -const gitIgnore = __webpack_require__(910); -const pify = __webpack_require__(911); -const slash = __webpack_require__(912); +const fastGlob = __webpack_require__(719); +const gitIgnore = __webpack_require__(911); +const pify = __webpack_require__(912); +const slash = __webpack_require__(913); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -106113,7 +106161,7 @@ module.exports.sync = options => { /***/ }), -/* 910 */ +/* 911 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -106582,7 +106630,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 911 */ +/* 912 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106657,7 +106705,7 @@ module.exports = (input, options) => { /***/ }), -/* 912 */ +/* 913 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106675,17 +106723,17 @@ module.exports = input => { /***/ }), -/* 913 */ +/* 914 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); const {constants: fsConstants} = __webpack_require__(23); -const pEvent = __webpack_require__(914); -const CpFileError = __webpack_require__(917); -const fs = __webpack_require__(921); -const ProgressEmitter = __webpack_require__(924); +const pEvent = __webpack_require__(915); +const CpFileError = __webpack_require__(918); +const fs = __webpack_require__(922); +const ProgressEmitter = __webpack_require__(925); const cpFileAsync = async (source, destination, options, progressEmitter) => { let readError; @@ -106799,12 +106847,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 914 */ +/* 915 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(915); +const pTimeout = __webpack_require__(916); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -107095,12 +107143,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 915 */ +/* 916 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(916); +const pFinally = __webpack_require__(917); class TimeoutError extends Error { constructor(message) { @@ -107146,7 +107194,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 916 */ +/* 917 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -107168,12 +107216,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 917 */ +/* 918 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(918); +const NestedError = __webpack_require__(919); class CpFileError extends NestedError { constructor(message, nested) { @@ -107187,10 +107235,10 @@ module.exports = CpFileError; /***/ }), -/* 918 */ +/* 919 */ /***/ (function(module, exports, __webpack_require__) { -var inherits = __webpack_require__(919); +var inherits = __webpack_require__(920); var NestedError = function (message, nested) { this.nested = nested; @@ -107241,7 +107289,7 @@ module.exports = NestedError; /***/ }), -/* 919 */ +/* 920 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -107249,12 +107297,12 @@ try { if (typeof util.inherits !== 'function') throw ''; module.exports = util.inherits; } catch (e) { - module.exports = __webpack_require__(920); + module.exports = __webpack_require__(921); } /***/ }), -/* 920 */ +/* 921 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -107283,16 +107331,16 @@ if (typeof Object.create === 'function') { /***/ }), -/* 921 */ +/* 922 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(29); const fs = __webpack_require__(22); -const makeDir = __webpack_require__(922); -const pEvent = __webpack_require__(914); -const CpFileError = __webpack_require__(917); +const makeDir = __webpack_require__(923); +const pEvent = __webpack_require__(915); +const CpFileError = __webpack_require__(918); const stat = promisify(fs.stat); const lstat = promisify(fs.lstat); @@ -107389,7 +107437,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 922 */ +/* 923 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -107397,7 +107445,7 @@ exports.copyFileSync = (source, destination, flags) => { const fs = __webpack_require__(23); const path = __webpack_require__(16); const {promisify} = __webpack_require__(29); -const semver = __webpack_require__(923); +const semver = __webpack_require__(924); const defaults = { mode: 0o777 & (~process.umask()), @@ -107546,7 +107594,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 923 */ +/* 924 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -109148,7 +109196,7 @@ function coerce (version, options) { /***/ }), -/* 924 */ +/* 925 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -109189,7 +109237,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 925 */ +/* 926 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -109235,12 +109283,12 @@ exports.default = module.exports; /***/ }), -/* 926 */ +/* 927 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(927); +const NestedError = __webpack_require__(928); class CpyError extends NestedError { constructor(message, nested) { @@ -109254,7 +109302,7 @@ module.exports = CpyError; /***/ }), -/* 927 */ +/* 928 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(29).inherits; @@ -109310,7 +109358,7 @@ module.exports = NestedError; /***/ }), -/* 928 */ +/* 929 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index 444d46307b059..278fdbd2bc9a4 100644 --- a/packages/kbn-pm/package.json +++ b/packages/kbn-pm/package.json @@ -48,6 +48,7 @@ "globby": "^8.0.1", "has-ansi": "^3.0.0", "indent-string": "^3.2.0", + "is-path-inside": "^3.0.2", "lodash.clonedeepwith": "^4.5.0", "log-symbols": "^2.2.0", "multimatch": "^4.0.0", diff --git a/packages/kbn-pm/src/utils/kibana.ts b/packages/kbn-pm/src/utils/kibana.ts index 36f697d19fc1f..58af98b2a92db 100644 --- a/packages/kbn-pm/src/utils/kibana.ts +++ b/packages/kbn-pm/src/utils/kibana.ts @@ -20,6 +20,7 @@ import Path from 'path'; import multimatch from 'multimatch'; +import isPathInside from 'is-path-inside'; import { ProjectMap, getProjects, includeTransitiveProjects } from './projects'; import { Project } from './project'; @@ -121,4 +122,15 @@ export class Kibana { return filteredProjects; } + + isPartOfRepo(project: Project) { + return ( + project.path === this.kibanaProject.path || + isPathInside(project.path, this.kibanaProject.path) + ); + } + + isOutsideRepo(project: Project) { + return !this.isPartOfRepo(project); + } } diff --git a/packages/kbn-pm/src/utils/project_checksums.ts b/packages/kbn-pm/src/utils/project_checksums.ts index 2fd24c8fc9577..572f2adb19bd9 100644 --- a/packages/kbn-pm/src/utils/project_checksums.ts +++ b/packages/kbn-pm/src/utils/project_checksums.ts @@ -43,7 +43,14 @@ async function getChangesForProjects(projects: ProjectMap, kbn: Kibana, log: Too const { stdout } = await execa( 'git', - ['ls-files', '-dmt', '--', ...Array.from(projects.values()).map(p => p.path)], + [ + 'ls-files', + '-dmt', + '--', + ...Array.from(projects.values()) + .filter(p => kbn.isPartOfRepo(p)) + .map(p => p.path), + ], { cwd: kbn.getAbsolute(), } @@ -84,9 +91,14 @@ async function getChangesForProjects(projects: ProjectMap, kbn: Kibana, log: Too } const sortedRelevantProjects = Array.from(projects.values()).sort(projectBySpecificitySorter); - const changesByProject = new Map(); + const changesByProject = new Map(); for (const project of sortedRelevantProjects) { + if (kbn.isOutsideRepo(project)) { + changesByProject.set(project, undefined); + continue; + } + const ownChanges: Changes = new Map(); const prefix = kbn.getRelative(project.path); @@ -114,6 +126,10 @@ async function getChangesForProjects(projects: ProjectMap, kbn: Kibana, log: Too /** Get the latest commit sha for a project */ async function getLatestSha(project: Project, kbn: Kibana) { + if (kbn.isOutsideRepo(project)) { + return; + } + const { stdout } = await execa( 'git', ['log', '-n', '1', '--pretty=format:%H', '--', project.path], @@ -175,7 +191,7 @@ function resolveDepsForProject(project: Project, yarnLock: YarnLock, kbn: Kibana */ async function getChecksum( project: Project, - changes: Changes, + changes: Changes | undefined, yarnLock: YarnLock, kbn: Kibana, log: ToolingLog @@ -185,7 +201,7 @@ async function getChecksum( log.verbose(`[${project.name}] local sha:`, sha); } - if (Array.from(changes.values()).includes('invalid')) { + if (!changes || Array.from(changes.values()).includes('invalid')) { log.warning(`[${project.name}] unable to determine local changes, caching disabled`); return; } @@ -248,7 +264,7 @@ export async function getAllChecksums(kbn: Kibana, log: ToolingLog) { Array.from(projects.values()).map(async project => { cacheKeys.set( project.name, - await getChecksum(project, changesByProject.get(project)!, yarnLock, kbn, log) + await getChecksum(project, changesByProject.get(project), yarnLock, kbn, log) ); }) ); diff --git a/packages/kbn-test/src/functional_test_runner/cli.ts b/packages/kbn-test/src/functional_test_runner/cli.ts index 11b9450f2af6e..3aaaa47ead5b6 100644 --- a/packages/kbn-test/src/functional_test_runner/cli.ts +++ b/packages/kbn-test/src/functional_test_runner/cli.ts @@ -48,12 +48,15 @@ export function runFtrCli() { kbnTestServer: { installDir: parseInstallDir(flags), }, + suiteFiles: { + include: toArray(flags.include as string | string[]).map(makeAbsolutePath), + exclude: toArray(flags.exclude as string | string[]).map(makeAbsolutePath), + }, suiteTags: { include: toArray(flags['include-tag'] as string | string[]), exclude: toArray(flags['exclude-tag'] as string | string[]), }, updateBaselines: flags.updateBaselines, - excludeTestFiles: flags.exclude || undefined, } ); @@ -104,7 +107,15 @@ export function runFtrCli() { }, { flags: { - string: ['config', 'grep', 'exclude', 'include-tag', 'exclude-tag', 'kibana-install-dir'], + string: [ + 'config', + 'grep', + 'include', + 'exclude', + 'include-tag', + 'exclude-tag', + 'kibana-install-dir', + ], boolean: ['bail', 'invert', 'test-stats', 'updateBaselines', 'throttle', 'headless'], default: { config: 'test/functional/config.js', @@ -115,7 +126,8 @@ export function runFtrCli() { --bail stop tests after the first failure --grep pattern used to select which tests to run --invert invert grep to exclude tests - --exclude=file path to a test file that should not be loaded + --include=file a test file to be included, pass multiple times for multiple files + --exclude=file a test file to be excluded, pass multiple times for multiple files --include-tag=tag a tag to be included, pass multiple times for multiple tags --exclude-tag=tag a tag to be excluded, pass multiple times for multiple tags --test-stats print the number of tests (included and excluded) to STDERR diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 75623d6c08890..66f17ab579ec3 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -64,9 +64,16 @@ export const schema = Joi.object() testFiles: Joi.array().items(Joi.string()), testRunner: Joi.func(), - excludeTestFiles: Joi.array() - .items(Joi.string()) - .default([]), + suiteFiles: Joi.object() + .keys({ + include: Joi.array() + .items(Joi.string()) + .default([]), + exclude: Joi.array() + .items(Joi.string()) + .default([]), + }) + .default(), suiteTags: Joi.object() .keys({ @@ -248,5 +255,20 @@ export const schema = Joi.object() fixedHeaderHeight: Joi.number().default(50), }) .default(), + + // settings for the security service if there is no defaultRole defined, then default to superuser role. + security: Joi.object() + .keys({ + roles: Joi.object().default(), + defaultRoles: Joi.array() + .items(Joi.string()) + .when('$primary', { + is: true, + then: Joi.array().min(1), + }) + .default(['superuser']), + disableTestUser: Joi.boolean(), + }) + .default(), }) .default(); diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js index 64fc51a04aac9..1cac852a7e713 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js @@ -16,7 +16,8 @@ * specific language governing permissions and limitations * under the License. */ - +import { relative } from 'path'; +import { REPO_ROOT } from '@kbn/dev-utils'; import { createAssignmentProxy } from './assignment_proxy'; import { wrapFunction } from './wrap_function'; import { wrapRunnableArgs } from './wrap_runnable_args'; @@ -65,6 +66,10 @@ export function decorateMochaUi(lifecycle, context) { this._tags = [].concat(this._tags || [], tags); }; + const relativeFilePath = relative(REPO_ROOT, this.file); + this.tags(relativeFilePath); + this.suiteTag = relativeFilePath; // The tag that uniquely targets this suite/file + provider.call(this); after(async () => { diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/load_test_files.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/load_test_files.js index 70b0c0874e5e9..6ee65b1b7e394 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/load_test_files.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/load_test_files.js @@ -31,28 +31,12 @@ import { decorateMochaUi } from './decorate_mocha_ui'; * @param {String} path * @return {undefined} - mutates mocha, no return value */ -export const loadTestFiles = ({ - mocha, - log, - lifecycle, - providers, - paths, - excludePaths, - updateBaselines, -}) => { - const pendingExcludes = new Set(excludePaths.slice(0)); - +export const loadTestFiles = ({ mocha, log, lifecycle, providers, paths, updateBaselines }) => { const innerLoadTestFile = path => { if (typeof path !== 'string' || !isAbsolute(path)) { throw new TypeError('loadTestFile() only accepts absolute paths'); } - if (pendingExcludes.has(path)) { - pendingExcludes.delete(path); - log.warning('Skipping test file %s', path); - return; - } - loadTracer(path, `testFile[${path}]`, () => { log.verbose('Loading test file %s', path); @@ -94,13 +78,4 @@ export const loadTestFiles = ({ }; paths.forEach(innerLoadTestFile); - - if (pendingExcludes.size) { - throw new Error( - `After loading all test files some exclude paths were not consumed:${[ - '', - ...pendingExcludes, - ].join('\n -')}` - ); - } }; diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js index 326877919d985..61851cece0e8f 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js @@ -18,6 +18,8 @@ */ import Mocha from 'mocha'; +import { relative } from 'path'; +import { REPO_ROOT } from '@kbn/dev-utils'; import { loadTestFiles } from './load_test_files'; import { filterSuitesByTags } from './filter_suites_by_tags'; @@ -50,10 +52,20 @@ export async function setupMocha(lifecycle, log, config, providers) { lifecycle, providers, paths: config.get('testFiles'), - excludePaths: config.get('excludeTestFiles'), updateBaselines: config.get('updateBaselines'), }); + // Each suite has a tag that is the path relative to the root of the repo + // So we just need to take input paths, make them relative to the root, and use them as tags + // Also, this is a separate filterSuitesByTags() call so that the test suites will be filtered first by + // files, then by tags. This way, you can target tags (like smoke) in a specific file. + filterSuitesByTags({ + log, + mocha, + include: config.get('suiteFiles.include').map(file => relative(REPO_ROOT, file)), + exclude: config.get('suiteFiles.exclude').map(file => relative(REPO_ROOT, file)), + }); + filterSuitesByTags({ log, mocha, diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap b/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap index bbf8b38712ac1..434c374d5d23d 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap @@ -16,6 +16,8 @@ Options: --bail Stop the test run at the first failure. --grep Pattern to select which tests to run. --updateBaselines Replace baseline screenshots with whatever is generated from the test. + --include Files that must included to be run, can be included multiple times. + --exclude Files that must NOT be included to be run, can be included multiple times. --include-tag Tags that suites must include to be run, can be included multiple times. --exclude-tag Tags that suites must NOT include to be run, can be included multiple times. --assert-none-excluded Exit with 1/0 based on if any test is excluded with the current set of tags. @@ -34,6 +36,10 @@ Object { "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -52,6 +58,10 @@ Object { "debug": true, "esFrom": "snapshot", "extraKbnOpts": undefined, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -69,6 +79,10 @@ Object { "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -90,6 +104,10 @@ Object { "extraKbnOpts": Object { "server.foo": "bar", }, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -107,6 +125,10 @@ Object { "esFrom": "snapshot", "extraKbnOpts": undefined, "quiet": true, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -124,6 +146,10 @@ Object { "esFrom": "snapshot", "extraKbnOpts": undefined, "silent": true, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -140,6 +166,10 @@ Object { "createLogger": [Function], "esFrom": "source", "extraKbnOpts": undefined, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -156,6 +186,10 @@ Object { "createLogger": [Function], "esFrom": "source", "extraKbnOpts": undefined, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -173,6 +207,10 @@ Object { "esFrom": "snapshot", "extraKbnOpts": undefined, "installDir": "foo", + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -190,6 +228,10 @@ Object { "esFrom": "snapshot", "extraKbnOpts": undefined, "grep": "management", + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -206,6 +248,10 @@ Object { "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], @@ -223,6 +269,10 @@ Object { "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, "suiteTags": Object { "exclude": Array [], "include": Array [], diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/cli.test.js.snap b/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/cli.test.js.snap index b12739b3b5df5..6ede71a6c3940 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/cli.test.js.snap +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/cli.test.js.snap @@ -16,6 +16,8 @@ Options: --bail Stop the test run at the first failure. --grep Pattern to select which tests to run. --updateBaselines Replace baseline screenshots with whatever is generated from the test. + --include Files that must included to be run, can be included multiple times. + --exclude Files that must NOT be included to be run, can be included multiple times. --include-tag Tags that suites must include to be run, can be included multiple times. --exclude-tag Tags that suites must NOT include to be run, can be included multiple times. --assert-none-excluded Exit with 1/0 based on if any test is excluded with the current set of tags. diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/args.js b/packages/kbn-test/src/functional_tests/cli/run_tests/args.js index b34006a38a45d..7d2414305de8e 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/args.js +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/args.js @@ -46,6 +46,14 @@ const options = { updateBaselines: { desc: 'Replace baseline screenshots with whatever is generated from the test.', }, + include: { + arg: '', + desc: 'Files that must included to be run, can be included multiple times.', + }, + exclude: { + arg: '', + desc: 'Files that must NOT be included to be run, can be included multiple times.', + }, 'include-tag': { arg: '', desc: 'Tags that suites must include to be run, can be included multiple times.', @@ -115,6 +123,13 @@ export function processOptions(userOptions, defaultConfigPaths) { delete userOptions['kibana-install-dir']; } + userOptions.suiteFiles = { + include: [].concat(userOptions.include || []), + exclude: [].concat(userOptions.exclude || []), + }; + delete userOptions.include; + delete userOptions.exclude; + userOptions.suiteTags = { include: [].concat(userOptions['include-tag'] || []), exclude: [].concat(userOptions['exclude-tag'] || []), diff --git a/packages/kbn-test/src/functional_tests/lib/run_ftr.js b/packages/kbn-test/src/functional_tests/lib/run_ftr.js index 9b631e33f3b24..14883ac977c43 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_ftr.js +++ b/packages/kbn-test/src/functional_tests/lib/run_ftr.js @@ -22,7 +22,7 @@ import { CliError } from './run_cli'; async function createFtr({ configPath, - options: { installDir, log, bail, grep, updateBaselines, suiteTags }, + options: { installDir, log, bail, grep, updateBaselines, suiteFiles, suiteTags }, }) { const config = await readConfigFile(log, configPath); @@ -37,6 +37,10 @@ async function createFtr({ installDir, }, updateBaselines, + suiteFiles: { + include: [...suiteFiles.include, ...config.get('suiteFiles.include')], + exclude: [...suiteFiles.exclude, ...config.get('suiteFiles.exclude')], + }, suiteTags: { include: [...suiteTags.include, ...config.get('suiteTags.include')], exclude: [...suiteTags.exclude, ...config.get('suiteTags.exclude')], diff --git a/rfcs/text/0010_service_status.md b/rfcs/text/0010_service_status.md new file mode 100644 index 0000000000000..ded594930a367 --- /dev/null +++ b/rfcs/text/0010_service_status.md @@ -0,0 +1,373 @@ +- Start Date: 2020-03-07 +- RFC PR: https://github.com/elastic/kibana/pull/59621 +- Kibana Issue: https://github.com/elastic/kibana/issues/41983 + +# Summary + +A set API for describing the current status of a system (Core service or plugin) +in Kibana. + +# Basic example + +```ts +// Override default behavior and only elevate severity when elasticsearch is not available +core.status.set( + core.status.core$.pipe(core => core.elasticsearch); +) +``` + +# Motivation + +Kibana should do as much possible to help users keep their installation in a working state. This includes providing as much detail about components that are not working as well as ensuring that failures in one part of the application do not block using other portions of the application. + +In order to provide the user with as much detail as possible about any systems that are not working correctly, the status mechanism should provide excellent defaults in terms of expressing relationships between services and presenting detailed information to the user. + +# Detailed design + +## Failure Guidelines + +While this RFC primarily describes how status information is signaled from individual services and plugins to Core, it's first important to define how Core expects these services and plugins to behave in the face of failure more broadly. + +Core is designed to be resilient and adaptive to change. When at all possible, Kibana should automatically recover from failure, rather than requiring any kind of intervention by the user or administrator. + +Given this goal, Core expects the following from plugins: +- During initialization, `setup`, and `start` plugins should only throw an exception if a truly unrecoverable issue is encountered. Examples: HTTP port is unavailable, server does not have the appropriate file permissions. +- Temporary error conditions should always be retried automatically. A user should not have to restart Kibana in order to resolve a problem when avoidable. This means all initialization code should include error handling and automated retries. Examples: creating an Elasticsearch index, connecting to an external service. + - It's important to note that some issues do require manual intervention in _other services_ (eg. Elasticsearch). Kibana should still recover without restarting once that external issue is resolved. +- Unhandled promise rejections are not permitted. In the future, Node.js will crash on unhandled promise rejections. It is impossible for Core to be able to properly handle and retry these situations, so all services and plugins should handle all rejected promises and retry when necessary. +- Plugins should only crash the Kibana server when absolutely necessary. Some features are considered "mission-critical" to customers and may need to halt Kibana if they are not functioning correctly. Example: audit logging. + +## API Design + +### Types + +```ts +/** + * The current status of a service at a point in time. + * + * @typeParam Meta - JSON-serializable object. Plugins should export this type to allow other plugins to read the `meta` + * field in a type-safe way. + */ +type ServiceStatus = unknown> = { + /** + * The current availability level of the service. + */ + level: ServiceStatusLevel.available; + /** + * A high-level summary of the service status. + */ + summary?: string; + /** + * A more detailed description of the service status. + */ + detail?: string; + /** + * A URL to open in a new tab about how to resolve or troubleshoot the problem. + */ + documentationUrl?: string; + /** + * Any JSON-serializable data to be included in the HTTP API response. Useful for providing more fine-grained, + * machine-readable information about the service status. May include status information for underlying features. + */ + meta?: Meta; +} | { + level: ServiceStatusLevel; + summary: string; // required when level !== available + detail?: string; + documentationUrl?: string; + meta?: Meta; +} + +/** + * The current "level" of availability of a service. + */ +enum ServiceStatusLevel { + /** + * Everything is working! + */ + available, + /** + * Some features may not be working. + */ + degraded, + /** + * The service is unavailable, but other functions that do not depend on this service should work. + */ + unavailable, + /** + * Block all user functions and display the status page, reserved for Core services only. + * Note: In the real implementation, this will be split out to a different type. Kept as a single type here to make + * the RFC easier to follow. + */ + critical +} + +/** + * Status of core services. Only contains entries for backend services that could have a non-available `status`. + * For example, `context` cannot possibly be broken, so it is not included. + */ +interface CoreStatus { + elasticsearch: ServiceStatus; + http: ServiceStatus; + savedObjects: ServiceStatus; + uiSettings: ServiceStatus; + metrics: ServiceStatus; +} +``` + +### Plugin API + +```ts +/** + * The API exposed to plugins on CoreSetup.status + */ +interface StatusSetup { + /** + * Allows a plugin to specify a custom status dependent on its own criteria. + * Completely overrides the default inherited status. + */ + set(status$: Observable): void; + + /** + * Current status for all Core services. + */ + core$: Observable; + + /** + * Current status for all dependencies of the current plugin. + * Each key of the `Record` is a plugin id. + */ + plugins$: Observable>; + + /** + * The status of this plugin as derived from its dependencies. + * + * @remarks + * By default, plugins inherit this derived status from their dependencies. + * Calling {@link StatusSetup.set} overrides this default status. + */ + derivedStatus$: Observable; +} +``` + +### HTTP API + +The HTTP endpoint should return basic information about the Kibana node as well as the overall system status and the status of each individual system. + +This API does not need to include UI-specific details like the existing API such as `uiColor` and `icon`. + +```ts +/** + * Response type for the endpoint: GET /api/status + */ +interface StatusResponse { + /** server.name */ + name: string; + /** server.uuid */ + uuid: string; + /** Currently exposed by existing status API */ + version: { + number: string; + build_hash: string; + build_number: number; + build_snapshot: boolean; + }; + /** Similar format to existing API, but slightly different shape */ + status: { + /** See "Overall status calculation" section below */ + overall: ServiceStatus; + core: CoreStatus; + plugins: Record; + } +} +``` + +## Behaviors + +### Levels + +Each member of the `ServiceStatusLevel` enum has specific behaviors associated with it: +- **`available`**: + - All endpoints and apps associated with the service are accessible +- **`degraded`**: + - All endpoints and apps are available by default + - Some APIs may return `503 Unavailable` responses. This is not automatic, must be implemented directly by the service. + - Some plugin contract APIs may throw errors. This is not automatic, must be implemented directly by the service. +- **`unavailable`**: + - All endpoints (with some exceptions in Core) in Kibana return a `503 Unavailable` responses by default. This is automatic. + - When trying to access any app associated with the unavailable service, the user is presented with an error UI with detail about the outage. + - Some plugin contract APIs may throw errors. This is not automatic, must be implemented directly by the service. +- **`critical`**: + - All endpoints (with some exceptions in Core) in Kibana return a `503 Unavailable` response by default. This is automatic. + - All applications redirect to the system-wide status page with detail about which services are down and any relevant detail. This is automatic. + - Some plugin contract APIs may throw errors. This is not automatic, must be implemented directly by the service. + - This level is reserved for Core services only. + +### Overall status calculation + +The status level of the overall system is calculated to be the highest severity status of all core services and plugins. + +The `summary` property is calculated as follows: +- If the overall status level is `available`, the `summary` is `"Kibana is operating normally"` +- If a single core service or plugin is not `available`, the `summary` is `Kibana is ${level} due to ${serviceName}. See ${statusPageUrl} for more information.` +- If multiple core services or plugins are not `available`, the `summary` is `Kibana is ${level} due to multiple components. See ${statusPageUrl} for more information.` + +### Status inheritance + +By default, plugins inherit their status from all Core services and their dependencies on other plugins. + +This can be summarized by the following matrix: + +| core | required | optional | inherited | +|----------------|----------------|----------------|-------------| +| critical | _any_ | _any_ | critical | +| unavailable | <= unavailable | <= unavailable | unavailable | +| degraded | <= degraded | <= degraded | degraded | +| <= unavailable | unavailable | <= unavailable | unavailable | +| <= degraded | degraded | <= degraded | degraded | +| <= degraded | <= degraded | unavailable | degraded | +| <= degraded | <= degraded | degraded | degraded | +| available | available | available | available | + +If a plugin calls the `StatusSetup#set` API, the inherited status is completely overridden. They status the plugin specifies is the source of truth. If a plugin wishes to "merge" its custom status with the inherited status calculated by Core, it may do so by using the `StatusSetup#inherited$` property in its calculated status. + +If a plugin never calls the `StatusSetup#set` API, the plugin's status defaults to the inherited status. + +_Disabled_ plugins, that is plugins that are explicitly disabled in Kibana's configuration, do not have any status. They are not present in any status APIs and are **not** considered `unavailable`. Disabled plugins are excluded from the status inheritance calculation, even if a plugin has a optional dependency on a disabled plugin. In summary, if a plugin has an optional dependency on a disabled plugin, the plugin will not be considered `degraded` just because that optional dependency is disabled. + +### HTTP responses + +As specified in the [_Levels section_](#levels), a service's HTTP endpoints will respond with `503 Unavailable` responses in some status levels. + +In both the `critical` and `unavailable` levels, all of a service's endpoints will return 503s. However, in the `degraded` level, it is up to service authors to decide which endpoints should return a 503. This may be implemented directly in the route handler logic or by using any of the [utilities provided](#status-utilities). + +When a 503 is returned either via the default behavior or behavior implemented using the [provided utilities](#status-utilities), the HTTP response will include the following: +- `Retry-After` header, set to `60` seconds +- A body with mime type `application/json` containing the status of the service the HTTP route belongs to: + ```json5 + { + "error": "Unavailable", + // `ServiceStatus#summary` + "message": "Newsfeed API cannot be reached", + "attributes": { + "status": { + // Human readable form of `ServiceStatus#level` + "level": "critical", + // `ServiceStatus#summary` + "summary": "Newsfeed API cannot be reached", + // `ServiceStatus#detail` or null + "detail": null, + // `ServiceStatus#documentationUrl` or null + "documentationUrl": null, + // JSON-serialized from `ServiceStatus#meta` or null + "meta": {} + } + }, + "statusCode": 503 + } + ``` + +## Status Utilities + +Though many plugins should be able to rely on the default status inheritance and associated behaviors, there are common patterns and overrides that some plugins will need. The status service should provide some utilities for these common patterns out-of-the-box. + +```ts +/** + * Extension of the main Status API + */ +interface StatusSetup { + /** + * Helpers for expressing status in HTTP routes. + */ + http: { + /** + * High-order route handler function for wrapping routes with 503 logic based + * on a predicate. + * + * @remarks + * When a 503 is returned, it also includes detailed information from the service's + * current `ServiceStatus` including `meta` information. + * + * @example + * ```ts + * router.get( + * { path: '/my-api' } + * unavailableWhen( + * ServiceStatusLevel.degraded, + * async (context, req, res) => { + * return res.ok({ body: 'done' }); + * } + * ) + * ) + * ``` + * + * @param predicate When a level is specified, if the plugin's current status + * level is >= to the severity of the specified level, route + * returns a 503. When a function is specified, if that + * function returns `true`, a 503 is returned. + * @param handler The route handler to execute when a 503 is not returned. + * @param options.retryAfter Number of seconds to set the `Retry-After` + * header to when the endpoint is unavailable. + * Defaults to `60`. + */ + unavailableWhen( + predicate: ServiceStatusLevel | + (self: ServiceStatus, core: CoreStatus, plugins: Record) => boolean, + handler: RouteHandler, + options?: { retryAfter?: number } + ): RouteHandler; + } +} +``` + +## Additional Examples + +### Combine inherited status with check against external dependency +```ts +const getExternalDepHealth = async () => { + const resp = await window.fetch('https://myexternaldep.com/_healthz'); + return resp.json(); +} + +// Create an observable that checks the status of an external service every every 10s +const myExternalDependency$: Observable = interval(10000).pipe( + mergeMap(() => of(getExternalDepHealth())), + map(health => health.ok ? ServiceStatusLevel.available : ServiceStatusLevel.unavailable), + catchError(() => of(ServiceStatusLevel.unavailable)) +); + +// Merge the inherited status with the external check +core.status.set( + combineLatest( + core.status.inherited$, + myExternalDependency$ + ).pipe( + map(([inherited, external]) => ({ + level: Math.max(inherited.level, external) + })) + ) +); +``` + +# Drawbacks + +1. **The default behaviors and inheritance of statuses may appear to be "magic" to developers who do not read the documentation about how this works.** Compared to the legacy status mechanism, these defaults are much more opinionated and the resulting status is less explicit in plugin code compared to the legacy `mirrorPluginStatus` mechanism. +2. **The default behaviors and inheritance may not fit real-world status very well.** If many plugins must customize their status in order to opt-out of the defaults, this would be a step backwards from the legacy mechanism. + +# Alternatives + +We could somewhat reduce the complexity of the status inheritance by leveraging the dependencies between plugins to enable and disable plugins based on whether or not their upstream dependencies are available. This may simplify plugin code but would greatly complicate how Kibana fundamentally operates, requiring that plugins may get stopped and started multiple times within a single Kibana server process. We would be trading simplicity in one area for complexity in another. + +# Adoption strategy + +By default, most plugins would not need to do much at all. Today, very few plugins leverage the legacy status system. The majority of ones that do, simply call the `mirrorPluginStatus` utility to follow the status of the legacy elasticsearch plugin. + +Plugins that wish to expose more detail about their availability will easily be able to do so, including providing detailed information such as links to documentation to resolve the problem. + +# How we teach this + +This largely follows the same patterns we have used for other Core APIs: Observables, composable utilties, etc. + +This should be taught using the same channels we've leveraged for other Kibana Platform APIs: API documentation, additions to the [Migration Guide](../../src/core/MIGRATION.md) and [Migration Examples](../../src/core/MIGRATION_EXMAPLES.md). + +# Unresolved questions diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index bd932c5961eca..89007461b63e6 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -61,8 +61,6 @@ const createStartContractMock = () => { getBrand$: jest.fn(), setIsVisible: jest.fn(), getIsVisible$: jest.fn(), - setIsCollapsed: jest.fn(), - getIsCollapsed$: jest.fn(), addApplicationClass: jest.fn(), removeApplicationClass: jest.fn(), getApplicationClasses$: jest.fn(), @@ -73,15 +71,16 @@ const createStartContractMock = () => { getHelpExtension$: jest.fn(), setHelpExtension: jest.fn(), setHelpSupportUrl: jest.fn(), + getIsNavDrawerLocked$: jest.fn(), }; startContract.navLinks.getAll.mockReturnValue([]); startContract.getBrand$.mockReturnValue(new BehaviorSubject({} as ChromeBrand)); startContract.getIsVisible$.mockReturnValue(new BehaviorSubject(false)); - startContract.getIsCollapsed$.mockReturnValue(new BehaviorSubject(false)); startContract.getApplicationClasses$.mockReturnValue(new BehaviorSubject(['class-name'])); startContract.getBadge$.mockReturnValue(new BehaviorSubject({} as ChromeBadge)); startContract.getBreadcrumbs$.mockReturnValue(new BehaviorSubject([{} as ChromeBreadcrumb])); startContract.getHelpExtension$.mockReturnValue(new BehaviorSubject(undefined)); + startContract.getIsNavDrawerLocked$.mockReturnValue(new BehaviorSubject(false)); return startContract; }; diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index 9018b21973634..bf531aaa00fac 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -259,40 +259,6 @@ describe('start', () => { }); }); - describe('is collapsed', () => { - it('updates/emits isCollapsed', async () => { - const { chrome, service } = await start(); - const promise = chrome - .getIsCollapsed$() - .pipe(toArray()) - .toPromise(); - - chrome.setIsCollapsed(true); - chrome.setIsCollapsed(false); - chrome.setIsCollapsed(true); - service.stop(); - - await expect(promise).resolves.toMatchInlineSnapshot(` - Array [ - false, - true, - false, - true, - ] - `); - }); - - it('only stores true in localStorage', async () => { - const { chrome } = await start(); - - chrome.setIsCollapsed(true); - expect(store.size).toBe(1); - - chrome.setIsCollapsed(false); - expect(store.size).toBe(0); - }); - }); - describe('application classes', () => { it('updates/emits the application classes', async () => { const { chrome, service } = await start(); @@ -442,12 +408,12 @@ describe('start', () => { }); describe('stop', () => { - it('completes applicationClass$, isCollapsed$, breadcrumbs$, isVisible$, and brand$ observables', async () => { + it('completes applicationClass$, getIsNavDrawerLocked, breadcrumbs$, isVisible$, and brand$ observables', async () => { const { chrome, service } = await start(); const promise = Rx.combineLatest( chrome.getBrand$(), chrome.getApplicationClasses$(), - chrome.getIsCollapsed$(), + chrome.getIsNavDrawerLocked$(), chrome.getBreadcrumbs$(), chrome.getIsVisible$(), chrome.getHelpExtension$() @@ -465,7 +431,7 @@ describe('stop', () => { Rx.combineLatest( chrome.getBrand$(), chrome.getApplicationClasses$(), - chrome.getIsCollapsed$(), + chrome.getIsNavDrawerLocked$(), chrome.getBreadcrumbs$(), chrome.getIsVisible$(), chrome.getHelpExtension$() diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 2b0b115ce068e..7c9b644b8b984 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -34,14 +34,14 @@ import { ChromeNavLinks, NavLinksService } from './nav_links'; import { ChromeRecentlyAccessed, RecentlyAccessedService } from './recently_accessed'; import { NavControlsService, ChromeNavControls } from './nav_controls'; import { DocTitleService, ChromeDocTitle } from './doc_title'; -import { LoadingIndicator, HeaderWrapper as Header } from './ui'; +import { LoadingIndicator, Header } from './ui'; import { DocLinksStart } from '../doc_links'; import { ChromeHelpExtensionMenuLink } from './ui/header/header_help_menu'; import { KIBANA_ASK_ELASTIC_LINK } from './constants'; import { IUiSettingsClient } from '../ui_settings'; export { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle }; -const IS_COLLAPSED_KEY = 'core.chrome.isCollapsed'; +const IS_LOCKED_KEY = 'core.chrome.isLocked'; /** @public */ export interface ChromeBadge { @@ -146,18 +146,25 @@ export class ChromeService { const appTitle$ = new BehaviorSubject('Kibana'); const brand$ = new BehaviorSubject({}); - const isCollapsed$ = new BehaviorSubject(!!localStorage.getItem(IS_COLLAPSED_KEY)); const applicationClasses$ = new BehaviorSubject>(new Set()); const helpExtension$ = new BehaviorSubject(undefined); const breadcrumbs$ = new BehaviorSubject([]); const badge$ = new BehaviorSubject(undefined); const helpSupportUrl$ = new BehaviorSubject(KIBANA_ASK_ELASTIC_LINK); + const isNavDrawerLocked$ = new BehaviorSubject(localStorage.getItem(IS_LOCKED_KEY) === 'true'); const navControls = this.navControls.start(); const navLinks = this.navLinks.start({ application, http }); const recentlyAccessed = await this.recentlyAccessed.start({ http }); const docTitle = this.docTitle.start({ document: window.document }); + const setIsNavDrawerLocked = (isLocked: boolean) => { + isNavDrawerLocked$.next(isLocked); + localStorage.setItem(IS_LOCKED_KEY, `${isLocked}`); + }; + + const getIsNavDrawerLocked$ = isNavDrawerLocked$.pipe(takeUntil(this.stop$)); + if (!this.params.browserSupportsCsp && injectedMetadata.getCspConfig().warnLegacyBrowsers) { notifications.toasts.addWarning( i18n.translate('core.chrome.legacyBrowserWarning', { @@ -193,6 +200,8 @@ export class ChromeService { recentlyAccessed$={recentlyAccessed.get$()} navControlsLeft$={navControls.getLeft$()} navControlsRight$={navControls.getRight$()} + onIsLockedUpdate={setIsNavDrawerLocked} + isLocked$={getIsNavDrawerLocked$} /> ), @@ -214,17 +223,6 @@ export class ChromeService { setIsVisible: (isVisible: boolean) => this.toggleHidden$.next(!isVisible), - getIsCollapsed$: () => isCollapsed$.pipe(takeUntil(this.stop$)), - - setIsCollapsed: (isCollapsed: boolean) => { - isCollapsed$.next(isCollapsed); - if (isCollapsed) { - localStorage.setItem(IS_COLLAPSED_KEY, 'true'); - } else { - localStorage.removeItem(IS_COLLAPSED_KEY); - } - }, - getApplicationClasses$: () => applicationClasses$.pipe( map(set => [...set]), @@ -262,6 +260,8 @@ export class ChromeService { }, setHelpSupportUrl: (url: string) => helpSupportUrl$.next(url), + + getIsNavDrawerLocked$: () => getIsNavDrawerLocked$, }; } @@ -353,16 +353,6 @@ export interface ChromeStart { */ setIsVisible(isVisible: boolean): void; - /** - * Get an observable of the current collapsed state of the chrome. - */ - getIsCollapsed$(): Observable; - - /** - * Set the collapsed state of the chrome navigation. - */ - setIsCollapsed(isCollapsed: boolean): void; - /** * Get the current set of classNames that will be set on the application container. */ @@ -413,6 +403,11 @@ export interface ChromeStart { * @param url The updated support URL */ setHelpSupportUrl(url: string): void; + + /** + * Get an observable of the current locked state of the nav drawer. + */ + getIsNavDrawerLocked$(): Observable; } /** @internal */ diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index c9a583f39b30c..4dec084fd8a83 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -30,6 +30,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { Component, createRef } from 'react'; +import classnames from 'classnames'; import * as Rx from 'rxjs'; import { ChromeBadge, @@ -68,8 +69,8 @@ export interface HeaderProps { navControlsLeft$: Rx.Observable; navControlsRight$: Rx.Observable; basePath: HttpStart['basePath']; - isLocked?: boolean; - onIsLockedUpdate?: OnIsLockedUpdate; + isLocked$: Rx.Observable; + onIsLockedUpdate: OnIsLockedUpdate; } interface State { @@ -81,6 +82,7 @@ interface State { navControlsLeft: readonly ChromeNavControl[]; navControlsRight: readonly ChromeNavControl[]; currentAppId: string | undefined; + isLocked: boolean; } export class Header extends Component { @@ -99,6 +101,7 @@ export class Header extends Component { navControlsLeft: [], navControlsRight: [], currentAppId: '', + isLocked: false, }; } @@ -109,11 +112,12 @@ export class Header extends Component { this.props.forceAppSwitcherNavigation$, this.props.navLinks$, this.props.recentlyAccessed$, - // Types for combineLatest only handle up to 6 inferred types so we combine these two separately. + // Types for combineLatest only handle up to 6 inferred types so we combine these separately. Rx.combineLatest( this.props.navControlsLeft$, this.props.navControlsRight$, - this.props.application.currentAppId$ + this.props.application.currentAppId$, + this.props.isLocked$ ) ).subscribe({ next: ([ @@ -122,7 +126,7 @@ export class Header extends Component { forceNavigation, navLinks, recentlyAccessed, - [navControlsLeft, navControlsRight, currentAppId], + [navControlsLeft, navControlsRight, currentAppId, isLocked], ]) => { this.setState({ appTitle, @@ -133,6 +137,7 @@ export class Header extends Component { navControlsLeft, navControlsRight, currentAppId, + isLocked, }); }, }); @@ -181,8 +186,16 @@ export class Header extends Component { return null; } + const className = classnames( + 'chrHeaderWrapper', + { + 'chrHeaderWrapper--navIsLocked': this.state.isLocked, + }, + 'hide-for-sharing' + ); + return ( -

+
@@ -220,7 +233,7 @@ export class Header extends Component { = props => { - const initialIsLocked = localStorage.getItem(IS_LOCKED_KEY); - const [isLocked, setIsLocked] = useState(initialIsLocked === 'true'); - const setIsLockedStored = (locked: boolean) => { - localStorage.setItem(IS_LOCKED_KEY, `${locked}`); - setIsLocked(locked); - }; - const className = classnames( - 'chrHeaderWrapper', - { - 'chrHeaderWrapper--navIsLocked': isLocked, - }, - 'hide-for-sharing' - ); - return ( -
-
-
- ); -}; diff --git a/src/core/public/chrome/ui/header/index.ts b/src/core/public/chrome/ui/header/index.ts index 4521f1f74b31b..49e002a66d939 100644 --- a/src/core/public/chrome/ui/header/index.ts +++ b/src/core/public/chrome/ui/header/index.ts @@ -18,7 +18,6 @@ */ export { Header, HeaderProps } from './header'; -export { HeaderWrapper } from './header_wrapper'; export { ChromeHelpExtensionMenuLink, ChromeHelpExtensionMenuCustomLink, diff --git a/src/core/public/chrome/ui/index.ts b/src/core/public/chrome/ui/index.ts index 81b2fdfb0fcc0..460e19b7d9780 100644 --- a/src/core/public/chrome/ui/index.ts +++ b/src/core/public/chrome/ui/index.ts @@ -20,7 +20,6 @@ export { LoadingIndicator } from './loading_indicator'; export { Header, - HeaderWrapper, ChromeHelpExtensionMenuLink, ChromeHelpExtensionMenuCustomLink, ChromeHelpExtensionMenuDiscussLink, diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 483d4dbfdf7c5..0ff044878afa9 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -171,7 +171,7 @@ export { ErrorToastOptions, } from './notifications'; -export { MountPoint, UnmountCallback } from './types'; +export { MountPoint, UnmountCallback, PublicUiSettingsParams } from './types'; /** * Core services exposed to the `Plugin` setup lifecycle diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 69668176a397e..7428280b2dccb 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -16,10 +16,11 @@ import { Location } from 'history'; import { LocationDescriptorObject } from 'history'; import { MaybePromise } from '@kbn/utility-types'; import { Observable } from 'rxjs'; +import { PublicUiSettingsParams as PublicUiSettingsParams_2 } from 'src/core/server/types'; import React from 'react'; import * as Rx from 'rxjs'; import { ShallowPromise } from '@kbn/utility-types'; -import { UiSettingsParams as UiSettingsParams_2 } from 'src/core/server/types'; +import { Type } from '@kbn/config-schema'; import { UnregisterCallback } from 'history'; import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/types'; @@ -336,7 +337,7 @@ export interface ChromeStart { getBrand$(): Observable; getBreadcrumbs$(): Observable; getHelpExtension$(): Observable; - getIsCollapsed$(): Observable; + getIsNavDrawerLocked$(): Observable; getIsVisible$(): Observable; navControls: ChromeNavControls; navLinks: ChromeNavLinks; @@ -348,7 +349,6 @@ export interface ChromeStart { setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]): void; setHelpExtension(helpExtension?: ChromeHelpExtension): void; setHelpSupportUrl(url: string): void; - setIsCollapsed(isCollapsed: boolean): void; setIsVisible(isVisible: boolean): void; } @@ -784,7 +784,7 @@ export type IToasts = Pick(key: string, defaultOverride?: T) => Observable; get: (key: string, defaultOverride?: T) => T; - getAll: () => Readonly>; + getAll: () => Readonly>; getSaved$: () => Observable<{ key: string; newValue: T; @@ -933,6 +933,9 @@ export interface PluginInitializerContext // @public (undocumented) export type PluginOpaqueId = symbol; +// @public +export type PublicUiSettingsParams = Omit; + // Warning: (ae-forgotten-export) The symbol "RecursiveReadonlyArray" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -940,6 +943,8 @@ export type RecursiveReadonly = T extends (...args: any[]) => any ? T : T ext [K in keyof T]: RecursiveReadonly; }> : T; +// Warning: (ae-missing-release-tag) "SavedObject" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export interface SavedObject { attributes: T; @@ -1291,7 +1296,7 @@ export type ToastsSetup = IToasts; export type ToastsStart = IToasts; // @public -export interface UiSettingsParams { +export interface UiSettingsParams { category?: string[]; // Warning: (ae-forgotten-export) The symbol "DeprecationSettings" needs to be exported by the entry point index.d.ts deprecation?: DeprecationSettings; @@ -1301,16 +1306,18 @@ export interface UiSettingsParams { options?: string[]; readonly?: boolean; requiresPageReload?: boolean; + // (undocumented) + schema: Type; type?: UiSettingsType; // (undocumented) validation?: ImageValidation | StringValidation; - value?: SavedObjectAttribute; + value?: T; } // @public (undocumented) export interface UiSettingsState { // (undocumented) - [key: string]: UiSettingsParams_2 & UserProvidedValues_2; + [key: string]: PublicUiSettingsParams_2 & UserProvidedValues_2; } // @public diff --git a/src/core/public/saved_objects/index.ts b/src/core/public/saved_objects/index.ts index 5015a9c3db78e..13b4a12893666 100644 --- a/src/core/public/saved_objects/index.ts +++ b/src/core/public/saved_objects/index.ts @@ -32,11 +32,6 @@ export { export { SimpleSavedObject } from './simple_saved_object'; export { SavedObjectsStart, SavedObjectsService } from './saved_objects_service'; export { - SavedObject, - SavedObjectAttribute, - SavedObjectAttributes, - SavedObjectAttributeSingle, - SavedObjectReference, SavedObjectsBaseOptions, SavedObjectsFindOptions, SavedObjectsMigrationVersion, @@ -48,3 +43,11 @@ export { SavedObjectsImportError, SavedObjectsImportRetry, } from '../../server/types'; + +export { + SavedObject, + SavedObjectAttribute, + SavedObjectAttributes, + SavedObjectAttributeSingle, + SavedObjectReference, +} from '../../types'; diff --git a/src/core/public/types.ts b/src/core/public/types.ts index 267a9e9f7e014..26f1e46836378 100644 --- a/src/core/public/types.ts +++ b/src/core/public/types.ts @@ -19,6 +19,7 @@ export { UiSettingsParams, + PublicUiSettingsParams, UserProvidedValues, UiSettingsType, ImageValidation, diff --git a/src/core/public/ui_settings/__snapshots__/ui_settings_api.test.ts.snap b/src/core/public/ui_settings/__snapshots__/ui_settings_api.test.ts.snap index cd55c77526d52..b737c04a5f269 100644 --- a/src/core/public/ui_settings/__snapshots__/ui_settings_api.test.ts.snap +++ b/src/core/public/ui_settings/__snapshots__/ui_settings_api.test.ts.snap @@ -84,21 +84,21 @@ Array [ exports[`#batchSet rejects all promises for batched requests that fail: promise rejections 1`] = ` Array [ Object { - "error": [Error: Request failed with status code: 400], + "error": [Error: invalid], "isRejected": true, }, Object { - "error": [Error: Request failed with status code: 400], + "error": [Error: invalid], "isRejected": true, }, Object { - "error": [Error: Request failed with status code: 400], + "error": [Error: invalid], "isRejected": true, }, ] `; -exports[`#batchSet rejects on 301 1`] = `"Request failed with status code: 301"`; +exports[`#batchSet rejects on 301 1`] = `"Moved Permanently"`; exports[`#batchSet rejects on 404 response 1`] = `"Request failed with status code: 404"`; diff --git a/src/core/public/ui_settings/types.ts b/src/core/public/ui_settings/types.ts index 19fd91924f247..d92c033ae8c8c 100644 --- a/src/core/public/ui_settings/types.ts +++ b/src/core/public/ui_settings/types.ts @@ -18,11 +18,11 @@ */ import { Observable } from 'rxjs'; -import { UiSettingsParams, UserProvidedValues } from 'src/core/server/types'; +import { PublicUiSettingsParams, UserProvidedValues } from 'src/core/server/types'; /** @public */ export interface UiSettingsState { - [key: string]: UiSettingsParams & UserProvidedValues; + [key: string]: PublicUiSettingsParams & UserProvidedValues; } /** @@ -53,7 +53,7 @@ export interface IUiSettingsClient { * Gets the metadata about all uiSettings, including the type, default value, and user value * for each key. */ - getAll: () => Readonly>; + getAll: () => Readonly>; /** * Sets the value for a uiSetting. If the setting is not registered by any plugin diff --git a/src/core/public/ui_settings/ui_settings_api.test.ts b/src/core/public/ui_settings/ui_settings_api.test.ts index 1170c42cea704..9a462e0541347 100644 --- a/src/core/public/ui_settings/ui_settings_api.test.ts +++ b/src/core/public/ui_settings/ui_settings_api.test.ts @@ -148,7 +148,7 @@ describe('#batchSet', () => { '*', { status: 400, - body: 'invalid', + body: { message: 'invalid' }, }, { overwriteRoutes: false, diff --git a/src/core/public/ui_settings/ui_settings_api.ts b/src/core/public/ui_settings/ui_settings_api.ts index 33b43107acf1b..c5efced0a41e3 100644 --- a/src/core/public/ui_settings/ui_settings_api.ts +++ b/src/core/public/ui_settings/ui_settings_api.ts @@ -152,10 +152,14 @@ export class UiSettingsApi { }, }); } catch (err) { - if (err.response && err.response.status >= 300) { - throw new Error(`Request failed with status code: ${err.response.status}`); + if (err.response) { + if (err.response.status === 400) { + throw new Error(err.body.message); + } + if (err.response.status > 400) { + throw new Error(`Request failed with status code: ${err.response.status}`); + } } - throw err; } finally { this.loadingCount$.next(this.loadingCount$.getValue() - 1); diff --git a/src/core/public/ui_settings/ui_settings_client.ts b/src/core/public/ui_settings/ui_settings_client.ts index f0071ed08435c..f5596b1bc34fc 100644 --- a/src/core/public/ui_settings/ui_settings_client.ts +++ b/src/core/public/ui_settings/ui_settings_client.ts @@ -21,14 +21,14 @@ import { cloneDeep, defaultsDeep } from 'lodash'; import { Observable, Subject, concat, defer, of } from 'rxjs'; import { filter, map } from 'rxjs/operators'; -import { UiSettingsParams, UserProvidedValues } from 'src/core/server/types'; +import { UserProvidedValues, PublicUiSettingsParams } from 'src/core/server/types'; import { IUiSettingsClient, UiSettingsState } from './types'; import { UiSettingsApi } from './ui_settings_api'; interface UiSettingsClientParams { api: UiSettingsApi; - defaults: Record; + defaults: Record; initialSettings?: UiSettingsState; done$: Observable; } @@ -39,8 +39,8 @@ export class UiSettingsClient implements IUiSettingsClient { private readonly updateErrors$ = new Subject(); private readonly api: UiSettingsApi; - private readonly defaults: Record; - private cache: Record; + private readonly defaults: Record; + private cache: Record; constructor(params: UiSettingsClientParams) { this.api = params.api; diff --git a/src/core/server/core_app/core_app.ts b/src/core/server/core_app/core_app.ts new file mode 100644 index 0000000000000..2f8c85f47a76e --- /dev/null +++ b/src/core/server/core_app/core_app.ts @@ -0,0 +1,52 @@ +/* + * 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 { InternalCoreSetup } from '../internal_types'; +import { CoreContext } from '../core_context'; +import { Logger } from '../logging'; + +/** @internal */ +export class CoreApp { + private readonly logger: Logger; + constructor(core: CoreContext) { + this.logger = core.logger.get('core-app'); + } + setup(coreSetup: InternalCoreSetup) { + this.logger.debug('Setting up core app.'); + this.registerDefaultRoutes(coreSetup); + } + + private registerDefaultRoutes(coreSetup: InternalCoreSetup) { + const httpSetup = coreSetup.http; + const router = httpSetup.createRouter('/'); + router.get({ path: '/', validate: false }, async (context, req, res) => { + const defaultRoute = await context.core.uiSettings.client.get('defaultRoute'); + const basePath = httpSetup.basePath.get(req); + const url = `${basePath}${defaultRoute}`; + + return res.redirected({ + headers: { + location: url, + }, + }); + }); + router.get({ path: '/core', validate: false }, async (context, req, res) => + res.ok({ body: { version: '0.0.1' } }) + ); + } +} diff --git a/src/legacy/ui/public/chrome/services/index.js b/src/core/server/core_app/index.ts similarity index 95% rename from src/legacy/ui/public/chrome/services/index.js rename to src/core/server/core_app/index.ts index 3b3967f51b2ff..342ed43f1ff8a 100644 --- a/src/legacy/ui/public/chrome/services/index.js +++ b/src/core/server/core_app/index.ts @@ -17,4 +17,4 @@ * under the License. */ -import './global_nav_state'; +export { CoreApp } from './core_app'; diff --git a/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts b/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts new file mode 100644 index 0000000000000..221e6fa42471c --- /dev/null +++ b/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts @@ -0,0 +1,90 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import { Root } from '../../root'; + +const { startES } = kbnTestServer.createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), +}); +let esServer: kbnTestServer.TestElasticsearchUtils; + +describe('default route provider', () => { + let root: Root; + + beforeAll(async () => { + esServer = await startES(); + root = kbnTestServer.createRootWithCorePlugins({ + server: { + basePath: '/hello', + }, + }); + + await root.setup(); + await root.start(); + }); + + afterAll(async () => { + await esServer.stop(); + await root.shutdown(); + }); + + it('redirects to the configured default route respecting basePath', async function() { + const { status, header } = await kbnTestServer.request.get(root, '/'); + + expect(status).toEqual(302); + expect(header).toMatchObject({ + location: '/hello/app/kibana', + }); + }); + + it('ignores invalid values', async function() { + const invalidRoutes = [ + 'http://not-your-kibana.com', + '///example.com', + '//example.com', + ' //example.com', + ]; + + for (const url of invalidRoutes) { + await kbnTestServer.request + .post(root, '/api/kibana/settings/defaultRoute') + .send({ value: url }) + .expect(400); + } + + const { status, header } = await kbnTestServer.request.get(root, '/'); + expect(status).toEqual(302); + expect(header).toMatchObject({ + location: '/hello/app/kibana', + }); + }); + + it('consumes valid values', async function() { + await kbnTestServer.request + .post(root, '/api/kibana/settings/defaultRoute') + .send({ value: '/valid' }) + .expect(200); + + const { status, header } = await kbnTestServer.request.get(root, '/'); + expect(status).toEqual(302); + expect(header).toMatchObject({ + location: '/hello/valid', + }); + }); +}); diff --git a/src/core/server/http/router/route.ts b/src/core/server/http/router/route.ts index bb0a8616e7222..9789d266587af 100644 --- a/src/core/server/http/router/route.ts +++ b/src/core/server/http/router/route.ts @@ -179,7 +179,7 @@ export interface RouteConfig { * access to raw values. * In some cases you may want to use another validation library. To do this, you need to * instruct the `@kbn/config-schema` library to output **non-validated values** with - * setting schema as `schema.object({}, { allowUnknowns: true })`; + * setting schema as `schema.object({}, { unknowns: 'allow' })`; * * @example * ```ts @@ -212,7 +212,7 @@ export interface RouteConfig { * path: 'path/{id}', * validate: { * // handler has access to raw non-validated params in runtime - * params: schema.object({}, { allowUnknowns: true }) + * params: schema.object({}, { unknowns: 'allow' }) * }, * }, * (context, req, res,) { diff --git a/src/core/server/http/router/router.test.ts b/src/core/server/http/router/router.test.ts index a936da6a40a9f..9655e2153b863 100644 --- a/src/core/server/http/router/router.test.ts +++ b/src/core/server/http/router/router.test.ts @@ -59,7 +59,7 @@ describe('Router', () => { { path: '/', options: { body: { output: 'file' } } as any, // We explicitly don't support 'file' - validate: { body: schema.object({}, { allowUnknowns: true }) }, + validate: { body: schema.object({}, { unknowns: 'allow' }) }, }, (context, req, res) => res.ok({}) ) diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 4a1ac8988e4e5..89fee92a7ef02 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -250,6 +250,7 @@ export { export { IUiSettingsClient, UiSettingsParams, + PublicUiSettingsParams, UiSettingsType, UiSettingsServiceSetup, UiSettingsServiceStart, diff --git a/src/core/server/saved_objects/serialization/types.ts b/src/core/server/saved_objects/serialization/types.ts index 524c2c8ffae7a..dfaec127ba159 100644 --- a/src/core/server/saved_objects/serialization/types.ts +++ b/src/core/server/saved_objects/serialization/types.ts @@ -50,7 +50,7 @@ export interface SavedObjectsRawDocSource { * scenario out of the box. */ interface SavedObjectDoc { - attributes: unknown; + attributes: any; id?: string; // NOTE: SavedObjectDoc is used for uncreated objects where `id` is optional type: string; namespace?: string; diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 1d927211b43e5..962965a08f8b2 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -35,66 +35,17 @@ export { import { LegacyConfig } from '../legacy'; import { SavedObjectUnsanitizedDoc } from './serialization'; import { SavedObjectsMigrationLogger } from './migrations/core/migration_logger'; +import { SavedObject } from '../../types'; + export { SavedObjectAttributes, SavedObjectAttribute, SavedObjectAttributeSingle, + SavedObject, + SavedObjectReference, + SavedObjectsMigrationVersion, } from '../../types'; -/** - * Information about the migrations that have been applied to this SavedObject. - * When Kibana starts up, KibanaMigrator detects outdated documents and - * migrates them based on this value. For each migration that has been applied, - * the plugin's name is used as a key and the latest migration version as the - * value. - * - * @example - * migrationVersion: { - * dashboard: '7.1.1', - * space: '6.6.6', - * } - * - * @public - */ -export interface SavedObjectsMigrationVersion { - [pluginName: string]: string; -} - -/** - * @public - */ -export interface SavedObject { - /** The ID of this Saved Object, guaranteed to be unique for all objects of the same `type` */ - id: string; - /** The type of Saved Object. Each plugin can define it's own custom Saved Object types. */ - type: string; - /** An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. */ - version?: string; - /** Timestamp of the last time this document had been updated. */ - updated_at?: string; - error?: { - message: string; - statusCode: number; - }; - /** {@inheritdoc SavedObjectAttributes} */ - attributes: T; - /** {@inheritdoc SavedObjectReference} */ - references: SavedObjectReference[]; - /** {@inheritdoc SavedObjectsMigrationVersion} */ - migrationVersion?: SavedObjectsMigrationVersion; -} - -/** - * A reference to another saved object. - * - * @public - */ -export interface SavedObjectReference { - name: string; - type: string; - id: string; -} - /** * * @public diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 9cd0c26ea2497..229ffc4d21575 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -998,7 +998,7 @@ export interface IScopedRenderingClient { export interface IUiSettingsClient { get: (key: string) => Promise; getAll: () => Promise>; - getRegistered: () => Readonly>; + getRegistered: () => Readonly>; getUserProvided: () => Promise>>; isOverridden: (key: string) => boolean; remove: (key: string) => Promise; @@ -1443,6 +1443,9 @@ export interface PluginsServiceStart { contracts: Map; } +// @public +export type PublicUiSettingsParams = Omit; + // Warning: (ae-forgotten-export) The symbol "RecursiveReadonlyArray" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -1592,6 +1595,8 @@ export interface RouteValidatorOptions { // @public export type SafeRouteMethod = 'get' | 'options'; +// Warning: (ae-missing-release-tag) "SavedObject" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export interface SavedObject { attributes: T; @@ -2284,7 +2289,7 @@ export interface StringValidationRegexString { } // @public -export interface UiSettingsParams { +export interface UiSettingsParams { category?: string[]; deprecation?: DeprecationSettings; description?: string; @@ -2293,10 +2298,12 @@ export interface UiSettingsParams { options?: string[]; readonly?: boolean; requiresPageReload?: boolean; + // (undocumented) + schema: Type; type?: UiSettingsType; // (undocumented) validation?: ImageValidation | StringValidation; - value?: SavedObjectAttribute; + value?: T; } // @public (undocumented) diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 2402504f717ca..09a1328f346d8 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -26,8 +26,9 @@ import { RawConfigurationProvider, coreDeprecationProvider, } from './config'; +import { CoreApp } from './core_app'; import { ElasticsearchService } from './elasticsearch'; -import { HttpService, InternalHttpServiceSetup } from './http'; +import { HttpService } from './http'; import { RenderingService, RenderingServiceSetup } from './rendering'; import { LegacyService, ensureValidConfiguration } from './legacy'; import { Logger, LoggerFactory } from './logging'; @@ -69,6 +70,7 @@ export class Server { private readonly uiSettings: UiSettingsService; private readonly uuid: UuidService; private readonly metrics: MetricsService; + private readonly coreApp: CoreApp; private coreStart?: InternalCoreStart; @@ -92,6 +94,7 @@ export class Server { this.capabilities = new CapabilitiesService(core); this.uuid = new UuidService(core); this.metrics = new MetricsService(core); + this.coreApp = new CoreApp(core); } public async setup() { @@ -122,8 +125,6 @@ export class Server { context: contextServiceSetup, }); - this.registerDefaultRoute(httpSetup); - const capabilitiesSetup = this.capabilities.setup({ http: httpSetup }); const elasticsearchServiceSetup = await this.elasticsearch.setup({ @@ -168,6 +169,7 @@ export class Server { }); this.registerCoreContext(coreSetup, renderingSetup); + this.coreApp.setup(coreSetup); return coreSetup; } @@ -216,13 +218,6 @@ export class Server { await this.metrics.stop(); } - private registerDefaultRoute(httpSetup: InternalHttpServiceSetup) { - const router = httpSetup.createRouter('/core'); - router.get({ path: '/', validate: false }, async (context, req, res) => - res.ok({ body: { version: '0.0.1' } }) - ); - } - private registerCoreContext(coreSetup: InternalCoreSetup, rendering: RenderingServiceSetup) { coreSetup.http.registerRouteHandlerContext( coreId, diff --git a/src/core/server/ui_settings/index.ts b/src/core/server/ui_settings/index.ts index ddb66df3ffcbe..b11f398d59fa8 100644 --- a/src/core/server/ui_settings/index.ts +++ b/src/core/server/ui_settings/index.ts @@ -27,6 +27,7 @@ export { UiSettingsServiceStart, IUiSettingsClient, UiSettingsParams, + PublicUiSettingsParams, InternalUiSettingsServiceSetup, InternalUiSettingsServiceStart, UiSettingsType, diff --git a/src/core/server/ui_settings/integration_tests/routes.test.ts b/src/core/server/ui_settings/integration_tests/routes.test.ts new file mode 100644 index 0000000000000..c1261bc7c1350 --- /dev/null +++ b/src/core/server/ui_settings/integration_tests/routes.test.ts @@ -0,0 +1,66 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import * as kbnTestServer from '../../../../test_utils/kbn_server'; + +describe('ui settings service', () => { + describe('routes', () => { + let root: ReturnType; + beforeAll(async () => { + root = kbnTestServer.createRoot(); + + const { uiSettings } = await root.setup(); + uiSettings.register({ + custom: { + value: '42', + schema: schema.string(), + }, + }); + + await root.start(); + }, 30000); + afterAll(async () => await root.shutdown()); + + describe('set', () => { + it('validates value', async () => { + const response = await kbnTestServer.request + .post(root, '/api/kibana/settings/custom') + .send({ value: 100 }) + .expect(400); + + expect(response.body.message).toBe( + '[validation [custom]]: expected value of type [string] but got [number]' + ); + }); + }); + describe('set many', () => { + it('validates value', async () => { + const response = await kbnTestServer.request + .post(root, '/api/kibana/settings') + .send({ changes: { custom: 100, foo: 'bar' } }) + .expect(400); + + expect(response.body.message).toBe( + '[validation [custom]]: expected value of type [string] but got [number]' + ); + }); + }); + }); +}); diff --git a/src/core/server/ui_settings/routes/set.ts b/src/core/server/ui_settings/routes/set.ts index 51ad256b51335..e5158e274245c 100644 --- a/src/core/server/ui_settings/routes/set.ts +++ b/src/core/server/ui_settings/routes/set.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { schema } from '@kbn/config-schema'; +import { schema, ValidationError } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { SavedObjectsErrorHelpers } from '../../saved_objects'; @@ -56,7 +56,7 @@ export function registerSetRoute(router: IRouter) { }); } - if (error instanceof CannotOverrideError) { + if (error instanceof CannotOverrideError || error instanceof ValidationError) { return response.badRequest({ body: error }); } diff --git a/src/core/server/ui_settings/routes/set_many.ts b/src/core/server/ui_settings/routes/set_many.ts index 3794eba004bee..d19a36a7ce768 100644 --- a/src/core/server/ui_settings/routes/set_many.ts +++ b/src/core/server/ui_settings/routes/set_many.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { schema } from '@kbn/config-schema'; +import { schema, ValidationError } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { SavedObjectsErrorHelpers } from '../../saved_objects'; @@ -24,7 +24,7 @@ import { CannotOverrideError } from '../ui_settings_errors'; const validate = { body: schema.object({ - changes: schema.object({}, { allowUnknowns: true }), + changes: schema.object({}, { unknowns: 'allow' }), }), }; @@ -50,7 +50,7 @@ export function registerSetManyRoute(router: IRouter) { }); } - if (error instanceof CannotOverrideError) { + if (error instanceof CannotOverrideError || error instanceof ValidationError) { return response.badRequest({ body: error }); } diff --git a/src/core/server/ui_settings/types.ts b/src/core/server/ui_settings/types.ts index f3eb1f5a6859c..076e1de4458d7 100644 --- a/src/core/server/ui_settings/types.ts +++ b/src/core/server/ui_settings/types.ts @@ -17,9 +17,10 @@ * under the License. */ import { SavedObjectsClientContract } from '../saved_objects/types'; -import { UiSettingsParams, UserProvidedValues } from '../../types'; +import { UiSettingsParams, UserProvidedValues, PublicUiSettingsParams } from '../../types'; export { UiSettingsParams, + PublicUiSettingsParams, StringValidationRegexString, StringValidationRegex, StringValidation, @@ -41,7 +42,7 @@ export interface IUiSettingsClient { /** * Returns registered uiSettings values {@link UiSettingsParams} */ - getRegistered: () => Readonly>; + getRegistered: () => Readonly>; /** * Retrieves uiSettings values set by the user with fallbacks to default values if not specified. */ diff --git a/src/core/server/ui_settings/ui_settings_client.test.ts b/src/core/server/ui_settings/ui_settings_client.test.ts index b8aa57291dccf..4ce33eed267a3 100644 --- a/src/core/server/ui_settings/ui_settings_client.test.ts +++ b/src/core/server/ui_settings/ui_settings_client.test.ts @@ -18,6 +18,7 @@ */ import Chance from 'chance'; +import { schema } from '@kbn/config-schema'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { createOrUpgradeSavedConfigMock } from './create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.mock'; @@ -145,6 +146,22 @@ describe('ui settings', () => { expect(error.message).toBe('Unable to update "foo" because it is overridden'); } }); + + it('validates value if a schema presents', async () => { + const defaults = { foo: { schema: schema.string() } }; + const { uiSettings, savedObjectsClient } = setup({ defaults }); + + await expect( + uiSettings.setMany({ + bar: 2, + foo: 1, + }) + ).rejects.toMatchInlineSnapshot( + `[Error: [validation [foo]]: expected value of type [string] but got [number]]` + ); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(0); + }); }); describe('#set()', () => { @@ -163,6 +180,17 @@ describe('ui settings', () => { }); }); + it('validates value if a schema presents', async () => { + const defaults = { foo: { schema: schema.string() } }; + const { uiSettings, savedObjectsClient } = setup({ defaults }); + + await expect(uiSettings.set('foo', 1)).rejects.toMatchInlineSnapshot( + `[Error: [validation [foo]]: expected value of type [string] but got [number]]` + ); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(0); + }); + it('throws CannotOverrideError if the key is overridden', async () => { const { uiSettings } = setup({ overrides: { @@ -193,6 +221,20 @@ describe('ui settings', () => { expect(savedObjectsClient.update).toHaveBeenCalledWith(TYPE, ID, { one: null }); }); + it('does not fail validation', async () => { + const defaults = { + foo: { + schema: schema.string(), + value: '1', + }, + }; + const { uiSettings, savedObjectsClient } = setup({ defaults }); + + await uiSettings.remove('foo'); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); + }); + it('throws CannotOverrideError if the key is overridden', async () => { const { uiSettings } = setup({ overrides: { @@ -235,6 +277,20 @@ describe('ui settings', () => { }); }); + it('does not fail validation', async () => { + const defaults = { + foo: { + schema: schema.string(), + value: '1', + }, + }; + const { uiSettings, savedObjectsClient } = setup({ defaults }); + + await uiSettings.removeMany(['foo', 'bar']); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); + }); + it('throws CannotOverrideError if any key is overridden', async () => { const { uiSettings } = setup({ overrides: { @@ -256,7 +312,13 @@ describe('ui settings', () => { const value = chance.word(); const defaults = { key: { value } }; const { uiSettings } = setup({ defaults }); - expect(uiSettings.getRegistered()).toBe(defaults); + expect(uiSettings.getRegistered()).toEqual(defaults); + }); + it('does not leak validation schema outside', () => { + const value = chance.word(); + const defaults = { key: { value, schema: schema.string() } }; + const { uiSettings } = setup({ defaults }); + expect(uiSettings.getRegistered()).toStrictEqual({ key: { value } }); }); }); @@ -274,7 +336,7 @@ describe('ui settings', () => { const { uiSettings } = setup({ esDocSource }); const result = await uiSettings.getUserProvided(); - expect(result).toEqual({ + expect(result).toStrictEqual({ user: { userValue: 'customized', }, @@ -286,7 +348,7 @@ describe('ui settings', () => { const { uiSettings } = setup({ esDocSource }); const result = await uiSettings.getUserProvided(); - expect(result).toEqual({ + expect(result).toStrictEqual({ user: { userValue: 'customized', }, @@ -296,6 +358,32 @@ describe('ui settings', () => { }); }); + it('ignores user-configured value if it fails validation', async () => { + const esDocSource = { user: 'foo', id: 'bar' }; + const defaults = { + id: { + value: 42, + schema: schema.number(), + }, + }; + const { uiSettings } = setup({ esDocSource, defaults }); + const result = await uiSettings.getUserProvided(); + + expect(result).toStrictEqual({ + user: { + userValue: 'foo', + }, + }); + + expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(` + Array [ + Array [ + "Ignore invalid UiSettings value. Error: [validation [id]]: expected value of type [number] but got [string].", + ], + ] + `); + }); + it('automatically creates the savedConfig if it is missing and returns empty object', async () => { const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup(); savedObjectsClient.get = jest @@ -303,7 +391,7 @@ describe('ui settings', () => { .mockRejectedValueOnce(SavedObjectsClient.errors.createGenericNotFoundError()) .mockResolvedValueOnce({ attributes: {} }); - expect(await uiSettings.getUserProvided()).toEqual({}); + expect(await uiSettings.getUserProvided()).toStrictEqual({}); expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); @@ -320,7 +408,7 @@ describe('ui settings', () => { SavedObjectsClient.errors.createGenericNotFoundError() ); - expect(await uiSettings.getUserProvided()).toEqual({ foo: { userValue: 'bar ' } }); + expect(await uiSettings.getUserProvided()).toStrictEqual({ foo: { userValue: 'bar ' } }); }); it('returns an empty object on Forbidden responses', async () => { @@ -329,7 +417,7 @@ describe('ui settings', () => { const error = SavedObjectsClient.errors.decorateForbiddenError(new Error()); savedObjectsClient.get.mockRejectedValue(error); - expect(await uiSettings.getUserProvided()).toEqual({}); + expect(await uiSettings.getUserProvided()).toStrictEqual({}); expect(createOrUpgradeSavedConfig).toHaveBeenCalledTimes(0); }); @@ -339,7 +427,7 @@ describe('ui settings', () => { const error = SavedObjectsClient.errors.decorateEsUnavailableError(new Error()); savedObjectsClient.get.mockRejectedValue(error); - expect(await uiSettings.getUserProvided()).toEqual({}); + expect(await uiSettings.getUserProvided()).toStrictEqual({}); expect(createOrUpgradeSavedConfig).toHaveBeenCalledTimes(0); }); @@ -382,7 +470,7 @@ describe('ui settings', () => { }; const { uiSettings } = setup({ esDocSource, overrides }); - expect(await uiSettings.getUserProvided()).toEqual({ + expect(await uiSettings.getUserProvided()).toStrictEqual({ user: { userValue: 'customized', }, @@ -404,15 +492,40 @@ describe('ui settings', () => { expect(savedObjectsClient.get).toHaveBeenCalledWith(TYPE, ID); }); - it(`returns defaults when es doc is empty`, async () => { + it('returns defaults when es doc is empty', async () => { const esDocSource = {}; const defaults = { foo: { value: 'bar' } }; const { uiSettings } = setup({ esDocSource, defaults }); - expect(await uiSettings.getAll()).toEqual({ + expect(await uiSettings.getAll()).toStrictEqual({ foo: 'bar', }); }); + it('ignores user-configured value if it fails validation', async () => { + const esDocSource = { user: 'foo', id: 'bar' }; + const defaults = { + id: { + value: 42, + schema: schema.number(), + }, + }; + const { uiSettings } = setup({ esDocSource, defaults }); + const result = await uiSettings.getAll(); + + expect(result).toStrictEqual({ + id: 42, + user: 'foo', + }); + + expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(` + Array [ + Array [ + "Ignore invalid UiSettings value. Error: [validation [id]]: expected value of type [number] but got [string].", + ], + ] + `); + }); + it(`merges user values, including ones without defaults, into key value pairs`, async () => { const esDocSource = { foo: 'user-override', @@ -427,7 +540,7 @@ describe('ui settings', () => { const { uiSettings } = setup({ esDocSource, defaults }); - expect(await uiSettings.getAll()).toEqual({ + expect(await uiSettings.getAll()).toStrictEqual({ foo: 'user-override', bar: 'user-provided', }); @@ -451,7 +564,7 @@ describe('ui settings', () => { const { uiSettings } = setup({ esDocSource, defaults, overrides }); - expect(await uiSettings.getAll()).toEqual({ + expect(await uiSettings.getAll()).toStrictEqual({ foo: 'bax', bar: 'user-provided', }); @@ -518,6 +631,28 @@ describe('ui settings', () => { expect(await uiSettings.get('dateFormat')).toBe('foo'); }); + + it('returns the default value if user-configured value fails validation', async () => { + const esDocSource = { id: 'bar' }; + const defaults = { + id: { + value: 42, + schema: schema.number(), + }, + }; + + const { uiSettings } = setup({ esDocSource, defaults }); + + expect(await uiSettings.get('id')).toBe(42); + + expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(` + Array [ + Array [ + "Ignore invalid UiSettings value. Error: [validation [id]]: expected value of type [number] but got [string].", + ], + ] + `); + }); }); describe('#isOverridden()', () => { diff --git a/src/core/server/ui_settings/ui_settings_client.ts b/src/core/server/ui_settings/ui_settings_client.ts index a7e55d2b2da65..76c8284175f11 100644 --- a/src/core/server/ui_settings/ui_settings_client.ts +++ b/src/core/server/ui_settings/ui_settings_client.ts @@ -16,13 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import { defaultsDeep } from 'lodash'; +import { defaultsDeep, omit } from 'lodash'; import { SavedObjectsErrorHelpers } from '../saved_objects'; import { SavedObjectsClientContract } from '../saved_objects/types'; import { Logger } from '../logging'; import { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; -import { IUiSettingsClient, UiSettingsParams } from './types'; +import { IUiSettingsClient, UiSettingsParams, PublicUiSettingsParams } from './types'; import { CannotOverrideError } from './ui_settings_errors'; export interface UiSettingsServiceOptions { @@ -40,14 +40,14 @@ interface ReadOptions { autoCreateOrUpgradeIfMissing?: boolean; } -interface UserProvidedValue { +interface UserProvidedValue { userValue?: T; isOverridden?: boolean; } type UiSettingsRawValue = UiSettingsParams & UserProvidedValue; -type UserProvided = Record>; +type UserProvided = Record>; type UiSettingsRaw = Record; export class UiSettingsClient implements IUiSettingsClient { @@ -72,7 +72,11 @@ export class UiSettingsClient implements IUiSettingsClient { } getRegistered() { - return this.defaults; + const copiedDefaults: Record = {}; + for (const [key, value] of Object.entries(this.defaults)) { + copiedDefaults[key] = omit(value, 'schema'); + } + return copiedDefaults; } async get(key: string): Promise { @@ -90,29 +94,21 @@ export class UiSettingsClient implements IUiSettingsClient { }, {} as Record); } - async getUserProvided(): Promise> { - const userProvided: UserProvided = {}; - - // write the userValue for each key stored in the saved object that is not overridden - for (const [key, userValue] of Object.entries(await this.read())) { - if (userValue !== null && !this.isOverridden(key)) { - userProvided[key] = { - userValue, - }; - } - } + async getUserProvided(): Promise> { + const userProvided: UserProvided = this.onReadHook(await this.read()); // write all overridden keys, dropping the userValue is override is null and // adding keys for overrides that are not in saved object - for (const [key, userValue] of Object.entries(this.overrides)) { + for (const [key, value] of Object.entries(this.overrides)) { userProvided[key] = - userValue === null ? { isOverridden: true } : { isOverridden: true, userValue }; + value === null ? { isOverridden: true } : { isOverridden: true, userValue: value }; } return userProvided; } async setMany(changes: Record) { + this.onWriteHook(changes); await this.write({ changes }); } @@ -147,6 +143,43 @@ export class UiSettingsClient implements IUiSettingsClient { return defaultsDeep(userProvided, this.defaults); } + private validateKey(key: string, value: unknown) { + const definition = this.defaults[key]; + if (value === null || definition === undefined) return; + if (definition.schema) { + definition.schema.validate(value, {}, `validation [${key}]`); + } + } + + private onWriteHook(changes: Record) { + for (const key of Object.keys(changes)) { + this.assertUpdateAllowed(key); + } + + for (const [key, value] of Object.entries(changes)) { + this.validateKey(key, value); + } + } + + private onReadHook(values: Record) { + // write the userValue for each key stored in the saved object that is not overridden + // validate value read from saved objects as it can be changed via SO API + const filteredValues: UserProvided = {}; + for (const [key, userValue] of Object.entries(values)) { + if (userValue === null || this.isOverridden(key)) continue; + try { + this.validateKey(key, userValue); + filteredValues[key] = { + userValue: userValue as T, + }; + } catch (error) { + this.log.warn(`Ignore invalid UiSettings value. ${error}.`); + } + } + + return filteredValues; + } + private async write({ changes, autoCreateOrUpgradeIfMissing = true, @@ -154,10 +187,6 @@ export class UiSettingsClient implements IUiSettingsClient { changes: Record; autoCreateOrUpgradeIfMissing?: boolean; }) { - for (const key of Object.keys(changes)) { - this.assertUpdateAllowed(key); - } - try { await this.savedObjectsClient.update(this.type, this.id, changes); } catch (error) { diff --git a/src/core/server/ui_settings/ui_settings_config.ts b/src/core/server/ui_settings/ui_settings_config.ts index a54d482a0296a..a0ac48e2dd089 100644 --- a/src/core/server/ui_settings/ui_settings_config.ts +++ b/src/core/server/ui_settings/ui_settings_config.ts @@ -39,7 +39,7 @@ const configSchema = schema.object({ }) ), }, - { allowUnknowns: true } + { unknowns: 'allow' } ), }); diff --git a/src/core/server/ui_settings/ui_settings_service.test.ts b/src/core/server/ui_settings/ui_settings_service.test.ts index 11766713b3be0..08400f56ad281 100644 --- a/src/core/server/ui_settings/ui_settings_service.test.ts +++ b/src/core/server/ui_settings/ui_settings_service.test.ts @@ -17,6 +17,8 @@ * under the License. */ import { BehaviorSubject } from 'rxjs'; +import { schema } from '@kbn/config-schema'; + import { MockUiSettingsClientConstructor } from './ui_settings_service.test.mock'; import { UiSettingsService, SetupDeps } from './ui_settings_service'; import { httpServiceMock } from '../http/http_service.mock'; @@ -35,6 +37,7 @@ const defaults = { value: 'bar', category: [], description: '', + schema: schema.string(), }, }; @@ -104,6 +107,45 @@ describe('uiSettings', () => { }); describe('#start', () => { + describe('validation', () => { + it('validates registered definitions', async () => { + const { register } = await service.setup(setupDeps); + register({ + custom: { + value: 42, + schema: schema.string(), + }, + }); + + await expect(service.start()).rejects.toMatchInlineSnapshot( + `[Error: [ui settings defaults [custom]]: expected value of type [string] but got [number]]` + ); + }); + + it('validates overrides', async () => { + const coreContext = mockCoreContext.create(); + coreContext.configService.atPath.mockReturnValueOnce( + new BehaviorSubject({ + overrides: { + custom: 42, + }, + }) + ); + const customizedService = new UiSettingsService(coreContext); + const { register } = await customizedService.setup(setupDeps); + register({ + custom: { + value: '42', + schema: schema.string(), + }, + }); + + await expect(customizedService.start()).rejects.toMatchInlineSnapshot( + `[Error: [ui settings overrides [custom]]: expected value of type [string] but got [number]]` + ); + }); + }); + describe('#asScopedToClient', () => { it('passes saved object type "config" to UiSettingsClient', async () => { await service.setup(setupDeps); diff --git a/src/core/server/ui_settings/ui_settings_service.ts b/src/core/server/ui_settings/ui_settings_service.ts index de2cc9d510e0c..83e66cf6dd06d 100644 --- a/src/core/server/ui_settings/ui_settings_service.ts +++ b/src/core/server/ui_settings/ui_settings_service.ts @@ -70,6 +70,9 @@ export class UiSettingsService } public async start(): Promise { + this.validatesDefinitions(); + this.validatesOverrides(); + return { asScopedToClient: this.getScopedClientFactory(), }; @@ -101,4 +104,21 @@ export class UiSettingsService this.uiSettingsDefaults.set(key, value); }); } + + private validatesDefinitions() { + for (const [key, definition] of this.uiSettingsDefaults) { + if (definition.schema) { + definition.schema.validate(definition.value, {}, `ui settings defaults [${key}]`); + } + } + } + + private validatesOverrides() { + for (const [key, value] of Object.entries(this.overrides)) { + const definition = this.uiSettingsDefaults.get(key); + if (definition?.schema) { + definition.schema.validate(value, {}, `ui settings overrides [${key}]`); + } + } + } } diff --git a/src/core/types/saved_objects.ts b/src/core/types/saved_objects.ts index 73eb2db11d62f..d3faab6c557cd 100644 --- a/src/core/types/saved_objects.ts +++ b/src/core/types/saved_objects.ts @@ -46,3 +46,54 @@ export type SavedObjectAttribute = SavedObjectAttributeSingle | SavedObjectAttri export interface SavedObjectAttributes { [key: string]: SavedObjectAttribute; } + +/** + * A reference to another saved object. + * + * @public + */ +export interface SavedObjectReference { + name: string; + type: string; + id: string; +} + +/** + * Information about the migrations that have been applied to this SavedObject. + * When Kibana starts up, KibanaMigrator detects outdated documents and + * migrates them based on this value. For each migration that has been applied, + * the plugin's name is used as a key and the latest migration version as the + * value. + * + * @example + * migrationVersion: { + * dashboard: '7.1.1', + * space: '6.6.6', + * } + * + * @public + */ +export interface SavedObjectsMigrationVersion { + [pluginName: string]: string; +} + +export interface SavedObject { + /** The ID of this Saved Object, guaranteed to be unique for all objects of the same `type` */ + id: string; + /** The type of Saved Object. Each plugin can define it's own custom Saved Object types. */ + type: string; + /** An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. */ + version?: string; + /** Timestamp of the last time this document had been updated. */ + updated_at?: string; + error?: { + message: string; + statusCode: number; + }; + /** {@inheritdoc SavedObjectAttributes} */ + attributes: T; + /** {@inheritdoc SavedObjectReference} */ + references: SavedObjectReference[]; + /** {@inheritdoc SavedObjectsMigrationVersion} */ + migrationVersion?: SavedObjectsMigrationVersion; +} diff --git a/src/core/types/ui_settings.ts b/src/core/types/ui_settings.ts index eccd3f9616af0..ed1076b571960 100644 --- a/src/core/types/ui_settings.ts +++ b/src/core/types/ui_settings.ts @@ -16,8 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - -import { SavedObjectAttribute } from './saved_objects'; +import { Type } from '@kbn/config-schema'; /** * UI element type to represent the settings. @@ -49,11 +48,11 @@ export interface DeprecationSettings { * UiSettings parameters defined by the plugins. * @public * */ -export interface UiSettingsParams { +export interface UiSettingsParams { /** title in the UI */ name?: string; /** default value to fall back to if a user doesn't provide any */ - value?: SavedObjectAttribute; + value?: T; /** description provided to a user in UI */ description?: string; /** used to group the configured setting in the UI */ @@ -73,10 +72,22 @@ export interface UiSettingsParams { /* * Allows defining a custom validation applicable to value change on the client. * @deprecated + * Use schema instead. */ validation?: ImageValidation | StringValidation; + /* + * Value validation schema + * Used to validate value on write and read. + */ + schema: Type; } +/** + * A sub-set of {@link UiSettingsParams} exposed to the client-side. + * @public + * */ +export type PublicUiSettingsParams = Omit; + /** * Allows regex objects or a regex string * @public diff --git a/src/core/utils/url.test.ts b/src/core/utils/url.test.ts index 3c35ba44455bc..419c0cda2b8cb 100644 --- a/src/core/utils/url.test.ts +++ b/src/core/utils/url.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { modifyUrl } from './url'; +import { modifyUrl, isRelativeUrl } from './url'; describe('modifyUrl()', () => { test('throws an error with invalid input', () => { @@ -69,3 +69,17 @@ describe('modifyUrl()', () => { ).toEqual('mail:localhost'); }); }); + +describe('isRelativeUrl()', () => { + test('returns "true" for a relative URL', () => { + expect(isRelativeUrl('good')).toBe(true); + expect(isRelativeUrl('/good')).toBe(true); + expect(isRelativeUrl('/good/even/better')).toBe(true); + }); + test('returns "false" for a non-relative URL', () => { + expect(isRelativeUrl('http://evil.com')).toBe(false); + expect(isRelativeUrl('//evil.com')).toBe(false); + expect(isRelativeUrl('///evil.com')).toBe(false); + expect(isRelativeUrl(' //evil.com')).toBe(false); + }); +}); diff --git a/src/core/utils/url.ts b/src/core/utils/url.ts index 31de7e1814038..c2bf80ce3f86f 100644 --- a/src/core/utils/url.ts +++ b/src/core/utils/url.ts @@ -99,3 +99,19 @@ export function modifyUrl( slashes: modifiedParts.slashes, } as UrlObject); } + +export function isRelativeUrl(candidatePath: string) { + // validate that `candidatePath` is not attempting a redirect to somewhere + // outside of this Kibana install + const all = parseUrl(candidatePath, false /* parseQueryString */, true /* slashesDenoteHost */); + const { protocol, hostname, port } = all; + // We should explicitly compare `protocol`, `port` and `hostname` to null to make sure these are not + // detected in the URL at all. For example `hostname` can be empty string for Node URL parser, but + // browser (because of various bwc reasons) processes URL differently (e.g. `///abc.com` - for browser + // hostname is `abc.com`, but for Node hostname is an empty string i.e. everything between schema (`//`) + // and the first slash that belongs to path. + if (protocol !== null || hostname !== null || port !== null) { + return false; + } + return true; +} diff --git a/src/legacy/core_plugins/data/index.ts b/src/legacy/core_plugins/data/index.ts index 813eab00f7258..10c8cf464b82d 100644 --- a/src/legacy/core_plugins/data/index.ts +++ b/src/legacy/core_plugins/data/index.ts @@ -19,8 +19,6 @@ import { resolve } from 'path'; import { Legacy } from '../../../../kibana'; -import { mappings } from './mappings'; -import { SavedQuery } from '../../../plugins/data/public'; // eslint-disable-next-line import/no-default-export export default function DataPlugin(kibana: any) { @@ -36,23 +34,6 @@ export default function DataPlugin(kibana: any) { init: (server: Legacy.Server) => ({}), uiExports: { injectDefaultVars: () => ({}), - mappings, - savedObjectsManagement: { - query: { - icon: 'search', - defaultSearchField: 'title', - isImportableAndExportable: true, - getTitle(obj: SavedQuery) { - return obj.attributes.title; - }, - getInAppUrl(obj: SavedQuery) { - return { - path: `/app/kibana#/discover?_a=(savedQuery:'${encodeURIComponent(obj.id)}')`, - uiCapabilitiesPath: 'discover.show', - }; - }, - }, - }, }, }; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx b/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx index cd3982afd9afd..0cd2a2b331980 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx @@ -19,8 +19,7 @@ import _ from 'lodash'; import React, { PureComponent } from 'react'; - -import { ValidatedDualRange } from '../../legacy_imports'; +import { ValidatedDualRange } from '../../../../../../../src/plugins/kibana_react/public'; import { FormRow } from './form_row'; import { RangeControl as RangeControlClass } from '../../control/range_control_factory'; diff --git a/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts b/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts index b6c4eb28e974f..8c58ac2386da4 100644 --- a/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts +++ b/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts @@ -22,7 +22,5 @@ import { SearchSource as SearchSourceClass, ISearchSource } from '../../../../pl export { SearchSourceFields } from '../../../../plugins/data/public'; -export { ValidatedDualRange } from 'ui/validated_range'; - export type SearchSource = Class; export const SearchSource = SearchSourceClass; diff --git a/src/legacy/core_plugins/kibana/common/utils/__tests__/shorten_dotted_string.js b/src/legacy/core_plugins/kibana/common/utils/__tests__/shorten_dotted_string.js deleted file mode 100644 index 267ca74c7c42a..0000000000000 --- a/src/legacy/core_plugins/kibana/common/utils/__tests__/shorten_dotted_string.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import { shortenDottedString } from '../shorten_dotted_string'; - -describe('shortenDottedString', () => { - it('Convert a dot.notated.string into a short string', () => { - expect(shortenDottedString('dot.notated.string')).to.equal('d.n.string'); - }); - - it('Ignores non-string values', () => { - expect(shortenDottedString(true)).to.equal(true); - expect(shortenDottedString(123)).to.equal(123); - const obj = { key: 'val' }; - expect(shortenDottedString(obj)).to.equal(obj); - }); -}); diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 092eed924f330..1d772536fa1ea 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -126,57 +126,6 @@ export default function(kibana) { ], savedObjectsManagement: { - 'index-pattern': { - icon: 'indexPatternApp', - defaultSearchField: 'title', - isImportableAndExportable: true, - getTitle(obj) { - return obj.attributes.title; - }, - getEditUrl(obj) { - return `/management/kibana/index_patterns/${encodeURIComponent(obj.id)}`; - }, - getInAppUrl(obj) { - return { - path: `/app/kibana#/management/kibana/index_patterns/${encodeURIComponent(obj.id)}`, - uiCapabilitiesPath: 'management.kibana.index_patterns', - }; - }, - }, - visualization: { - icon: 'visualizeApp', - defaultSearchField: 'title', - isImportableAndExportable: true, - getTitle(obj) { - return obj.attributes.title; - }, - getEditUrl(obj) { - return `/management/kibana/objects/savedVisualizations/${encodeURIComponent(obj.id)}`; - }, - getInAppUrl(obj) { - return { - path: `/app/kibana#/visualize/edit/${encodeURIComponent(obj.id)}`, - uiCapabilitiesPath: 'visualize.show', - }; - }, - }, - search: { - icon: 'discoverApp', - defaultSearchField: 'title', - isImportableAndExportable: true, - getTitle(obj) { - return obj.attributes.title; - }, - getEditUrl(obj) { - return `/management/kibana/objects/savedSearches/${encodeURIComponent(obj.id)}`; - }, - getInAppUrl(obj) { - return { - path: `/app/kibana#/discover/${encodeURIComponent(obj.id)}`, - uiCapabilitiesPath: 'discover.show', - }; - }, - }, dashboard: { icon: 'dashboardApp', defaultSearchField: 'title', diff --git a/src/legacy/core_plugins/kibana/mappings.json b/src/legacy/core_plugins/kibana/mappings.json index 4cf9ea1d301c0..af3f79588552b 100644 --- a/src/legacy/core_plugins/kibana/mappings.json +++ b/src/legacy/core_plugins/kibana/mappings.json @@ -1,93 +1,4 @@ { - "index-pattern": { - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "typeMeta": { - "type": "keyword" - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchRefName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - }, - "search": { - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, "dashboard": { "properties": { "description": { diff --git a/src/legacy/core_plugins/kibana/migrations/migrations.js b/src/legacy/core_plugins/kibana/migrations/migrations.js index 29b6e632d19fd..d37887c640b90 100644 --- a/src/legacy/core_plugins/kibana/migrations/migrations.js +++ b/src/legacy/core_plugins/kibana/migrations/migrations.js @@ -17,7 +17,7 @@ * under the License. */ -import { cloneDeep, get, omit, has, flow } from 'lodash'; +import { get } from 'lodash'; import { migrations730 as dashboardMigrations730 } from '../public/dashboard/migrations'; function migrateIndexPattern(doc) { @@ -58,559 +58,7 @@ function migrateIndexPattern(doc) { doc.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify(searchSource); } -// [TSVB] Migrate percentile-rank aggregation (value -> values) -const migratePercentileRankAggregation = doc => { - const visStateJSON = get(doc, 'attributes.visState'); - let visState; - - if (visStateJSON) { - try { - visState = JSON.parse(visStateJSON); - } catch (e) { - // Let it go, the data is invalid and we'll leave it as is - } - if (visState && visState.type === 'metrics') { - const series = get(visState, 'params.series') || []; - - series.forEach(part => { - (part.metrics || []).forEach(metric => { - if (metric.type === 'percentile_rank' && has(metric, 'value')) { - metric.values = [metric.value]; - - delete metric.value; - } - }); - }); - return { - ...doc, - attributes: { - ...doc.attributes, - visState: JSON.stringify(visState), - }, - }; - } - } - return doc; -}; - -// Migrate date histogram aggregation (remove customInterval) -const migrateDateHistogramAggregation = doc => { - const visStateJSON = get(doc, 'attributes.visState'); - let visState; - - if (visStateJSON) { - try { - visState = JSON.parse(visStateJSON); - } catch (e) { - // Let it go, the data is invalid and we'll leave it as is - } - - if (visState && visState.aggs) { - visState.aggs.forEach(agg => { - if (agg.type === 'date_histogram' && agg.params) { - if (agg.params.interval === 'custom') { - agg.params.interval = agg.params.customInterval; - } - delete agg.params.customInterval; - } - - if ( - get(agg, 'params.customBucket.type', null) === 'date_histogram' && - agg.params.customBucket.params - ) { - if (agg.params.customBucket.params.interval === 'custom') { - agg.params.customBucket.params.interval = agg.params.customBucket.params.customInterval; - } - delete agg.params.customBucket.params.customInterval; - } - }); - return { - ...doc, - attributes: { - ...doc.attributes, - visState: JSON.stringify(visState), - }, - }; - } - } - return doc; -}; - -function removeDateHistogramTimeZones(doc) { - const visStateJSON = get(doc, 'attributes.visState'); - if (visStateJSON) { - let visState; - try { - visState = JSON.parse(visStateJSON); - } catch (e) { - // Let it go, the data is invalid and we'll leave it as is - } - if (visState && visState.aggs) { - visState.aggs.forEach(agg => { - // We're checking always for the existance of agg.params here. This should always exist, but better - // be safe then sorry during migrations. - if (agg.type === 'date_histogram' && agg.params) { - delete agg.params.time_zone; - } - - if ( - get(agg, 'params.customBucket.type', null) === 'date_histogram' && - agg.params.customBucket.params - ) { - delete agg.params.customBucket.params.time_zone; - } - }); - doc.attributes.visState = JSON.stringify(visState); - } - } - return doc; -} - -// migrate gauge verticalSplit to alignment -// https://github.com/elastic/kibana/issues/34636 -function migrateGaugeVerticalSplitToAlignment(doc, logger) { - const visStateJSON = get(doc, 'attributes.visState'); - - if (visStateJSON) { - try { - const visState = JSON.parse(visStateJSON); - if (visState && visState.type === 'gauge' && !visState.params.gauge.alignment) { - visState.params.gauge.alignment = visState.params.gauge.verticalSplit - ? 'vertical' - : 'horizontal'; - delete visState.params.gauge.verticalSplit; - return { - ...doc, - attributes: { - ...doc.attributes, - visState: JSON.stringify(visState), - }, - }; - } - } catch (e) { - logger.warning(`Exception @ migrateGaugeVerticalSplitToAlignment! ${e}`); - logger.warning(`Exception @ migrateGaugeVerticalSplitToAlignment! Payload: ${visStateJSON}`); - } - } - return doc; -} -// Migrate filters (string -> { query: string, language: lucene }) -/* - Enabling KQL in TSVB causes problems with savedObject visualizations when these are saved with filters. - In a visualisation type of saved object, if the visState param is of type metric, the filter is saved as a string that is not interpretted correctly as a lucene query in the visualization itself. - We need to transform the filter string into an object containing the original string as a query and specify the query language as lucene. - For Metrics visualizations (param.type === "metric"), filters can be applied to each series object in the series array within the SavedObject.visState.params object. - Path to the series array is thus: - attributes.visState. -*/ -function transformFilterStringToQueryObject(doc) { - // Migrate filters - // If any filters exist and they are a string, we assume it to be lucene and transform the filter into an object accordingly - const newDoc = cloneDeep(doc); - const visStateJSON = get(doc, 'attributes.visState'); - if (visStateJSON) { - let visState; - try { - visState = JSON.parse(visStateJSON); - } catch (e) { - // let it go, the data is invalid and we'll leave it as is - } - if (visState) { - const visType = get(visState, 'params.type'); - const tsvbTypes = ['metric', 'markdown', 'top_n', 'gauge', 'table', 'timeseries']; - if (tsvbTypes.indexOf(visType) === -1) { - // skip - return doc; - } - // migrate the params fitler - const params = get(visState, 'params'); - if (params.filter && typeof params.filter === 'string') { - const paramsFilterObject = { - query: params.filter, - language: 'lucene', - }; - params.filter = paramsFilterObject; - } - - // migrate the annotations query string: - const annotations = get(visState, 'params.annotations') || []; - annotations.forEach(item => { - if (!item.query_string) { - // we don't need to transform anything if there isn't a filter at all - return; - } - if (typeof item.query_string === 'string') { - const itemQueryStringObject = { - query: item.query_string, - language: 'lucene', - }; - item.query_string = itemQueryStringObject; - } - }); - // migrate the series filters - const series = get(visState, 'params.series') || []; - series.forEach(item => { - if (!item.filter) { - // we don't need to transform anything if there isn't a filter at all - return; - } - // series item filter - if (typeof item.filter === 'string') { - const itemfilterObject = { - query: item.filter, - language: 'lucene', - }; - item.filter = itemfilterObject; - } - // series item split filters filter - if (item.split_filters) { - const splitFilters = get(item, 'split_filters') || []; - splitFilters.forEach(filter => { - if (!filter.filter) { - // we don't need to transform anything if there isn't a filter at all - return; - } - if (typeof filter.filter === 'string') { - const filterfilterObject = { - query: filter.filter, - language: 'lucene', - }; - filter.filter = filterfilterObject; - } - }); - } - }); - newDoc.attributes.visState = JSON.stringify(visState); - } - } - return newDoc; -} -function transformSplitFiltersStringToQueryObject(doc) { - // Migrate split_filters in TSVB objects that weren't migrated in 7.3 - // If any filters exist and they are a string, we assume them to be lucene syntax and transform the filter into an object accordingly - const newDoc = cloneDeep(doc); - const visStateJSON = get(doc, 'attributes.visState'); - if (visStateJSON) { - let visState; - try { - visState = JSON.parse(visStateJSON); - } catch (e) { - // let it go, the data is invalid and we'll leave it as is - } - if (visState) { - const visType = get(visState, 'params.type'); - const tsvbTypes = ['metric', 'markdown', 'top_n', 'gauge', 'table', 'timeseries']; - if (tsvbTypes.indexOf(visType) === -1) { - // skip - return doc; - } - // migrate the series split_filter filters - const series = get(visState, 'params.series') || []; - series.forEach(item => { - // series item split filters filter - if (item.split_filters) { - const splitFilters = get(item, 'split_filters') || []; - if (splitFilters.length > 0) { - // only transform split_filter filters if we have filters - splitFilters.forEach(filter => { - if (typeof filter.filter === 'string') { - const filterfilterObject = { - query: filter.filter, - language: 'lucene', - }; - filter.filter = filterfilterObject; - } - }); - } - } - }); - newDoc.attributes.visState = JSON.stringify(visState); - } - } - return newDoc; -} - -function migrateFiltersAggQuery(doc) { - const visStateJSON = get(doc, 'attributes.visState'); - - if (visStateJSON) { - try { - const visState = JSON.parse(visStateJSON); - if (visState && visState.aggs) { - visState.aggs.forEach(agg => { - if (agg.type !== 'filters') return; - - agg.params.filters.forEach(filter => { - if (filter.input.language) return filter; - filter.input.language = 'lucene'; - }); - }); - - return { - ...doc, - attributes: { - ...doc.attributes, - visState: JSON.stringify(visState), - }, - }; - } - } catch (e) { - // Let it go, the data is invalid and we'll leave it as is - } - } - return doc; -} - -function replaceMovAvgToMovFn(doc, logger) { - const visStateJSON = get(doc, 'attributes.visState'); - let visState; - - if (visStateJSON) { - try { - visState = JSON.parse(visStateJSON); - - if (visState && visState.type === 'metrics') { - const series = get(visState, 'params.series', []); - - series.forEach(part => { - if (part.metrics && Array.isArray(part.metrics)) { - part.metrics.forEach(metric => { - if (metric.type === 'moving_average') { - metric.model_type = metric.model; - metric.alpha = get(metric, 'settings.alpha', 0.3); - metric.beta = get(metric, 'settings.beta', 0.1); - metric.gamma = get(metric, 'settings.gamma', 0.3); - metric.period = get(metric, 'settings.period', 1); - metric.multiplicative = get(metric, 'settings.type') === 'mult'; - - delete metric.minimize; - delete metric.model; - delete metric.settings; - delete metric.predict; - } - }); - } - }); - - return { - ...doc, - attributes: { - ...doc.attributes, - visState: JSON.stringify(visState), - }, - }; - } - } catch (e) { - logger.warning(`Exception @ replaceMovAvgToMovFn! ${e}`); - logger.warning(`Exception @ replaceMovAvgToMovFn! Payload: ${visStateJSON}`); - } - } - - return doc; -} - -function migrateSearchSortToNestedArray(doc) { - const sort = get(doc, 'attributes.sort'); - if (!sort) return doc; - - // Don't do anything if we already have a two dimensional array - if (Array.isArray(sort) && sort.length > 0 && Array.isArray(sort[0])) { - return doc; - } - - return { - ...doc, - attributes: { - ...doc.attributes, - sort: [doc.attributes.sort], - }, - }; -} - -function migrateFiltersAggQueryStringQueries(doc) { - const visStateJSON = get(doc, 'attributes.visState'); - - if (visStateJSON) { - try { - const visState = JSON.parse(visStateJSON); - if (visState && visState.aggs) { - visState.aggs.forEach(agg => { - if (agg.type !== 'filters') return doc; - - agg.params.filters.forEach(filter => { - if (filter.input.query.query_string) { - filter.input.query = filter.input.query.query_string.query; - } - }); - }); - - return { - ...doc, - attributes: { - ...doc.attributes, - visState: JSON.stringify(visState), - }, - }; - } - } catch (e) { - // Let it go, the data is invalid and we'll leave it as is - } - } - return doc; -} - -function migrateSubTypeAndParentFieldProperties(doc) { - if (!doc.attributes.fields) return doc; - - const fieldsString = doc.attributes.fields; - const fields = JSON.parse(fieldsString); - const migratedFields = fields.map(field => { - if (field.subType === 'multi') { - return { - ...omit(field, 'parent'), - subType: { multi: { parent: field.parent } }, - }; - } - - return field; - }); - - return { - ...doc, - attributes: { - ...doc.attributes, - fields: JSON.stringify(migratedFields), - }, - }; -} - -const executeMigrations720 = flow( - migratePercentileRankAggregation, - migrateDateHistogramAggregation -); -const executeMigrations730 = flow( - migrateGaugeVerticalSplitToAlignment, - transformFilterStringToQueryObject, - migrateFiltersAggQuery, - replaceMovAvgToMovFn -); - -const executeVisualizationMigrations731 = flow(migrateFiltersAggQueryStringQueries); - -const executeSearchMigrations740 = flow(migrateSearchSortToNestedArray); - -const executeMigrations742 = flow(transformSplitFiltersStringToQueryObject); - export const migrations = { - 'index-pattern': { - '6.5.0': doc => { - doc.attributes.type = doc.attributes.type || undefined; - doc.attributes.typeMeta = doc.attributes.typeMeta || undefined; - return doc; - }, - '7.6.0': flow(migrateSubTypeAndParentFieldProperties), - }, - visualization: { - /** - * We need to have this migration twice, once with a version prior to 7.0.0 once with a version - * after it. The reason for that is, that this migration has been introduced once 7.0.0 was already - * released. Thus a user who already had 7.0.0 installed already got the 7.0.0 migrations below running, - * so we need a version higher than that. But this fix was backported to the 6.7 release, meaning if we - * would only have the 7.0.1 migration in here a user on the 6.7 release will migrate their saved objects - * to the 7.0.1 state, and thus when updating their Kibana to 7.0, will never run the 7.0.0 migrations introduced - * in that version. So we apply this twice, once with 6.7.2 and once with 7.0.1 while the backport to 6.7 - * only contained the 6.7.2 migration and not the 7.0.1 migration. - */ - '6.7.2': removeDateHistogramTimeZones, - '7.0.0': doc => { - // Set new "references" attribute - doc.references = doc.references || []; - - // Migrate index pattern - migrateIndexPattern(doc); - - // Migrate saved search - const savedSearchId = get(doc, 'attributes.savedSearchId'); - if (savedSearchId) { - doc.references.push({ - type: 'search', - name: 'search_0', - id: savedSearchId, - }); - doc.attributes.savedSearchRefName = 'search_0'; - } - delete doc.attributes.savedSearchId; - - // Migrate controls - const visStateJSON = get(doc, 'attributes.visState'); - if (visStateJSON) { - let visState; - try { - visState = JSON.parse(visStateJSON); - } catch (e) { - // Let it go, the data is invalid and we'll leave it as is - } - if (visState) { - const controls = get(visState, 'params.controls') || []; - controls.forEach((control, i) => { - if (!control.indexPattern) { - return; - } - control.indexPatternRefName = `control_${i}_index_pattern`; - doc.references.push({ - name: control.indexPatternRefName, - type: 'index-pattern', - id: control.indexPattern, - }); - delete control.indexPattern; - }); - doc.attributes.visState = JSON.stringify(visState); - } - } - - // Migrate table splits - try { - const visState = JSON.parse(doc.attributes.visState); - if (get(visState, 'type') !== 'table') { - return doc; // do nothing; we only want to touch tables - } - - let splitCount = 0; - visState.aggs = visState.aggs.map(agg => { - if (agg.schema !== 'split') { - return agg; - } - - splitCount++; - if (splitCount === 1) { - return agg; // leave the first split agg unchanged - } - agg.schema = 'bucket'; - // the `row` param is exclusively used by split aggs, so we remove it - agg.params = omit(agg.params, ['row']); - return agg; - }); - - if (splitCount <= 1) { - return doc; // do nothing; we only want to touch tables with multiple split aggs - } - - const newDoc = cloneDeep(doc); - newDoc.attributes.visState = JSON.stringify(visState); - return newDoc; - } catch (e) { - throw new Error( - `Failure attempting to migrate saved object '${doc.attributes.title}' - ${e}` - ); - } - }, - '7.0.1': removeDateHistogramTimeZones, - '7.2.0': doc => executeMigrations720(doc), - '7.3.0': executeMigrations730, - '7.3.1': executeVisualizationMigrations731, - // migrate split_filters that were not migrated in 7.3.0 (transformFilterStringToQueryObject). - '7.4.2': executeMigrations742, - }, dashboard: { '7.0.0': doc => { // Set new "references" attribute @@ -651,14 +99,4 @@ export const migrations = { }, '7.3.0': dashboardMigrations730, }, - search: { - '7.0.0': doc => { - // Set new "references" attribute - doc.references = doc.references || []; - // Migrate index pattern - migrateIndexPattern(doc); - return doc; - }, - '7.4.0': executeSearchMigrations740, - }, }; diff --git a/src/legacy/core_plugins/kibana/migrations/migrations.test.js b/src/legacy/core_plugins/kibana/migrations/migrations.test.js index e39bc59201e7f..b02081128c858 100644 --- a/src/legacy/core_plugins/kibana/migrations/migrations.test.js +++ b/src/legacy/core_plugins/kibana/migrations/migrations.test.js @@ -19,1312 +19,6 @@ import { migrations } from './migrations'; -describe('index-pattern', () => { - describe('6.5.0', () => { - const migrate = doc => migrations['index-pattern']['6.5.0'](doc); - - it('adds "type" and "typeMeta" properties to object when not declared', () => { - expect( - migrate({ - attributes: {}, - }) - ).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "type": undefined, - "typeMeta": undefined, - }, -} -`); - }); - - it('keeps "type" and "typeMeta" properties as is when declared', () => { - expect( - migrate({ - attributes: { - type: '123', - typeMeta: '123', - }, - }) - ).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "type": "123", - "typeMeta": "123", - }, -} -`); - }); - }); - - describe('7.6.0', function() { - const migrate = doc => migrations['index-pattern']['7.6.0'](doc); - - it('should remove the parent property and update the subType prop on every field that has them', () => { - const input = { - attributes: { - title: 'test', - fields: - '[{"name":"customer_name","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"customer_name.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":"multi","parent":"customer_name"}]', - }, - }; - const expected = { - attributes: { - title: 'test', - fields: - '[{"name":"customer_name","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"customer_name.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"customer_name"}}}]', - }, - }; - - expect(migrate(input)).toEqual(expected); - }); - }); -}); - -describe('visualization', () => { - describe('date histogram time zone removal', () => { - const migrate = doc => migrations.visualization['6.7.2'](doc); - let doc; - beforeEach(() => { - doc = { - attributes: { - visState: JSON.stringify({ - aggs: [ - { - enabled: true, - id: '1', - params: { - // Doesn't make much sense but we want to test it's not removing it from anything else - time_zone: 'Europe/Berlin', - }, - schema: 'metric', - type: 'count', - }, - { - enabled: true, - id: '2', - params: { - customInterval: '2h', - drop_partials: false, - extended_bounds: {}, - field: 'timestamp', - time_zone: 'Europe/Berlin', - interval: 'auto', - min_doc_count: 1, - useNormalizedEsInterval: true, - }, - schema: 'segment', - type: 'date_histogram', - }, - { - enabled: true, - id: '4', - params: { - customInterval: '2h', - drop_partials: false, - extended_bounds: {}, - field: 'timestamp', - interval: 'auto', - min_doc_count: 1, - useNormalizedEsInterval: true, - }, - schema: 'segment', - type: 'date_histogram', - }, - { - enabled: true, - id: '3', - params: { - customBucket: { - enabled: true, - id: '1-bucket', - params: { - customInterval: '2h', - drop_partials: false, - extended_bounds: {}, - field: 'timestamp', - interval: 'auto', - min_doc_count: 1, - time_zone: 'Europe/Berlin', - useNormalizedEsInterval: true, - }, - type: 'date_histogram', - }, - customMetric: { - enabled: true, - id: '1-metric', - params: {}, - type: 'count', - }, - }, - schema: 'metric', - type: 'max_bucket', - }, - ], - }), - }, - }; - }); - - it('should remove time_zone from date_histogram aggregations', () => { - const migratedDoc = migrate(doc); - const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; - expect(aggs[1]).not.toHaveProperty('params.time_zone'); - }); - - it('should not remove time_zone from non date_histogram aggregations', () => { - const migratedDoc = migrate(doc); - const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; - expect(aggs[0]).toHaveProperty('params.time_zone'); - }); - - it('should remove time_zone from nested aggregations', () => { - const migratedDoc = migrate(doc); - const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; - expect(aggs[3]).not.toHaveProperty('params.customBucket.params.time_zone'); - }); - - it('should not fail on date histograms without a time_zone', () => { - const migratedDoc = migrate(doc); - const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; - expect(aggs[2]).not.toHaveProperty('params.time_zone'); - }); - - it('should be able to apply the migration twice, since we need it for 6.7.2 and 7.0.1', () => { - const migratedDoc = migrate(migrate(doc)); - const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; - expect(aggs[1]).not.toHaveProperty('params.time_zone'); - expect(aggs[0]).toHaveProperty('params.time_zone'); - expect(aggs[3]).not.toHaveProperty('params.customBucket.params.time_zone'); - expect(aggs[2]).not.toHaveProperty('params.time_zone'); - }); - }); - - describe('7.0.0', () => { - const migrate = doc => migrations.visualization['7.0.0'](doc); - const generateDoc = ({ type, aggs }) => ({ - attributes: { - title: 'My Vis', - description: 'This is my super cool vis.', - visState: JSON.stringify({ type, aggs }), - uiStateJSON: '{}', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: '{}', - }, - }, - }); - - it('does not throw error on empty object', () => { - const migratedDoc = migrate({ - attributes: { - visState: '{}', - }, - }); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "visState": "{}", - }, - "references": Array [], -} -`); - }); - - it('skips errors when searchSourceJSON is null', () => { - const doc = { - id: '1', - type: 'visualization', - attributes: { - visState: '{}', - kibanaSavedObjectMeta: { - searchSourceJSON: null, - }, - savedSearchId: '123', - }, - }; - const migratedDoc = migrate(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": null, - }, - "savedSearchRefName": "search_0", - "visState": "{}", - }, - "id": "1", - "references": Array [ - Object { - "id": "123", - "name": "search_0", - "type": "search", - }, - ], - "type": "visualization", -} -`); - }); - - it('skips errors when searchSourceJSON is undefined', () => { - const doc = { - id: '1', - type: 'visualization', - attributes: { - visState: '{}', - kibanaSavedObjectMeta: { - searchSourceJSON: undefined, - }, - savedSearchId: '123', - }, - }; - const migratedDoc = migrate(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": undefined, - }, - "savedSearchRefName": "search_0", - "visState": "{}", - }, - "id": "1", - "references": Array [ - Object { - "id": "123", - "name": "search_0", - "type": "search", - }, - ], - "type": "visualization", -} -`); - }); - - it('skips error when searchSourceJSON is not a string', () => { - const doc = { - id: '1', - type: 'visualization', - attributes: { - visState: '{}', - kibanaSavedObjectMeta: { - searchSourceJSON: 123, - }, - savedSearchId: '123', - }, - }; - expect(migrate(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": 123, - }, - "savedSearchRefName": "search_0", - "visState": "{}", - }, - "id": "1", - "references": Array [ - Object { - "id": "123", - "name": "search_0", - "type": "search", - }, - ], - "type": "visualization", -} -`); - }); - - it('skips error when searchSourceJSON is invalid json', () => { - const doc = { - id: '1', - type: 'visualization', - attributes: { - visState: '{}', - kibanaSavedObjectMeta: { - searchSourceJSON: '{abc123}', - }, - savedSearchId: '123', - }, - }; - expect(migrate(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{abc123}", - }, - "savedSearchRefName": "search_0", - "visState": "{}", - }, - "id": "1", - "references": Array [ - Object { - "id": "123", - "name": "search_0", - "type": "search", - }, - ], - "type": "visualization", -} -`); - }); - - it('skips error when "index" and "filter" is missing from searchSourceJSON', () => { - const doc = { - id: '1', - type: 'visualization', - attributes: { - visState: '{}', - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ bar: true }), - }, - savedSearchId: '123', - }, - }; - const migratedDoc = migrate(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{\\"bar\\":true}", - }, - "savedSearchRefName": "search_0", - "visState": "{}", - }, - "id": "1", - "references": Array [ - Object { - "id": "123", - "name": "search_0", - "type": "search", - }, - ], - "type": "visualization", -} -`); - }); - - it('extracts "index" attribute from doc', () => { - const doc = { - id: '1', - type: 'visualization', - attributes: { - visState: '{}', - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ bar: true, index: 'pattern*' }), - }, - savedSearchId: '123', - }, - }; - const migratedDoc = migrate(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{\\"bar\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}", - }, - "savedSearchRefName": "search_0", - "visState": "{}", - }, - "id": "1", - "references": Array [ - Object { - "id": "pattern*", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern", - }, - Object { - "id": "123", - "name": "search_0", - "type": "search", - }, - ], - "type": "visualization", -} -`); - }); - - it('extracts index patterns from the filter', () => { - const doc = { - id: '1', - type: 'visualization', - attributes: { - visState: '{}', - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - bar: true, - filter: [ - { - meta: { index: 'my-index', foo: true }, - }, - ], - }), - }, - savedSearchId: '123', - }, - }; - const migratedDoc = migrate(doc); - - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{\\"bar\\":true,\\"filter\\":[{\\"meta\\":{\\"foo\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}", - }, - "savedSearchRefName": "search_0", - "visState": "{}", - }, - "id": "1", - "references": Array [ - Object { - "id": "my-index", - "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", - "type": "index-pattern", - }, - Object { - "id": "123", - "name": "search_0", - "type": "search", - }, - ], - "type": "visualization", -} -`); - }); - - it('extracts index patterns from controls', () => { - const doc = { - id: '1', - type: 'visualization', - attributes: { - foo: true, - visState: JSON.stringify({ - bar: false, - params: { - controls: [ - { - bar: true, - indexPattern: 'pattern*', - }, - { - foo: true, - }, - ], - }, - }), - }, - }; - const migratedDoc = migrate(doc); - - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "visState": "{\\"bar\\":false,\\"params\\":{\\"controls\\":[{\\"bar\\":true,\\"indexPatternRefName\\":\\"control_0_index_pattern\\"},{\\"foo\\":true}]}}", - }, - "id": "1", - "references": Array [ - Object { - "id": "pattern*", - "name": "control_0_index_pattern", - "type": "index-pattern", - }, - ], - "type": "visualization", -} -`); - }); - - it('skips extracting savedSearchId when missing', () => { - const doc = { - id: '1', - attributes: { - visState: '{}', - kibanaSavedObjectMeta: { - searchSourceJSON: '{}', - }, - }, - }; - const migratedDoc = migrate(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{}", - }, - "visState": "{}", - }, - "id": "1", - "references": Array [], -} -`); - }); - - it('extract savedSearchId from doc', () => { - const doc = { - id: '1', - attributes: { - visState: '{}', - kibanaSavedObjectMeta: { - searchSourceJSON: '{}', - }, - savedSearchId: '123', - }, - }; - const migratedDoc = migrate(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{}", - }, - "savedSearchRefName": "search_0", - "visState": "{}", - }, - "id": "1", - "references": Array [ - Object { - "id": "123", - "name": "search_0", - "type": "search", - }, - ], -} -`); - }); - - it('delete savedSearchId when empty string in doc', () => { - const doc = { - id: '1', - attributes: { - visState: '{}', - kibanaSavedObjectMeta: { - searchSourceJSON: '{}', - }, - savedSearchId: '', - }, - }; - const migratedDoc = migrate(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{}", - }, - "visState": "{}", - }, - "id": "1", - "references": Array [], -} -`); - }); - - it('should return a new object if vis is table and has multiple split aggs', () => { - const aggs = [ - { - id: '1', - schema: 'metric', - params: {}, - }, - { - id: '2', - schema: 'split', - params: { foo: 'bar', row: true }, - }, - { - id: '3', - schema: 'split', - params: { hey: 'ya', row: false }, - }, - ]; - const tableDoc = generateDoc({ type: 'table', aggs }); - const expected = tableDoc; - const actual = migrate(tableDoc); - expect(actual).not.toBe(expected); - }); - - it('should not touch any vis that is not table', () => { - const aggs = []; - const pieDoc = generateDoc({ type: 'pie', aggs }); - const expected = pieDoc; - const actual = migrate(pieDoc); - expect(actual).toBe(expected); - }); - - it('should not change values in any vis that is not table', () => { - const aggs = [ - { - id: '1', - schema: 'metric', - params: {}, - }, - { - id: '2', - schema: 'split', - params: { foo: 'bar', row: true }, - }, - { - id: '3', - schema: 'segment', - params: { hey: 'ya' }, - }, - ]; - const pieDoc = generateDoc({ type: 'pie', aggs }); - const expected = pieDoc; - const actual = migrate(pieDoc); - expect(actual).toEqual(expected); - }); - - it('should not touch table vis if there are not multiple split aggs', () => { - const aggs = [ - { - id: '1', - schema: 'metric', - params: {}, - }, - { - id: '2', - schema: 'split', - params: { foo: 'bar', row: true }, - }, - ]; - const tableDoc = generateDoc({ type: 'table', aggs }); - const expected = tableDoc; - const actual = migrate(tableDoc); - expect(actual).toBe(expected); - }); - - it('should change all split aggs to `bucket` except the first', () => { - const aggs = [ - { - id: '1', - schema: 'metric', - params: {}, - }, - { - id: '2', - schema: 'split', - params: { foo: 'bar', row: true }, - }, - { - id: '3', - schema: 'split', - params: { hey: 'ya', row: false }, - }, - { - id: '4', - schema: 'bucket', - params: { heyyy: 'yaaa' }, - }, - ]; - const expected = ['metric', 'split', 'bucket', 'bucket']; - const migrated = migrate(generateDoc({ type: 'table', aggs })); - const actual = JSON.parse(migrated.attributes.visState); - expect(actual.aggs.map(agg => agg.schema)).toEqual(expected); - }); - - it('should remove `rows` param from any aggs that are not `split`', () => { - const aggs = [ - { - id: '1', - schema: 'metric', - params: {}, - }, - { - id: '2', - schema: 'split', - params: { foo: 'bar', row: true }, - }, - { - id: '3', - schema: 'split', - params: { hey: 'ya', row: false }, - }, - ]; - const expected = [{}, { foo: 'bar', row: true }, { hey: 'ya' }]; - const migrated = migrate(generateDoc({ type: 'table', aggs })); - const actual = JSON.parse(migrated.attributes.visState); - expect(actual.aggs.map(agg => agg.params)).toEqual(expected); - }); - - it('should throw with a reference to the doc name if something goes wrong', () => { - const doc = { - attributes: { - title: 'My Vis', - description: 'This is my super cool vis.', - visState: '!/// Intentionally malformed JSON ///!', - uiStateJSON: '{}', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: '{}', - }, - }, - }; - expect(() => migrate(doc)).toThrowError(/My Vis/); - }); - }); - - describe('date histogram custom interval removal', () => { - const migrate = doc => migrations.visualization['7.2.0'](doc); - let doc; - beforeEach(() => { - doc = { - attributes: { - visState: JSON.stringify({ - aggs: [ - { - enabled: true, - id: '1', - params: { - customInterval: '1h', - }, - schema: 'metric', - type: 'count', - }, - { - enabled: true, - id: '2', - params: { - customInterval: '2h', - drop_partials: false, - extended_bounds: {}, - field: 'timestamp', - interval: 'auto', - min_doc_count: 1, - useNormalizedEsInterval: true, - }, - schema: 'segment', - type: 'date_histogram', - }, - { - enabled: true, - id: '4', - params: { - customInterval: '2h', - drop_partials: false, - extended_bounds: {}, - field: 'timestamp', - interval: 'custom', - min_doc_count: 1, - useNormalizedEsInterval: true, - }, - schema: 'segment', - type: 'date_histogram', - }, - { - enabled: true, - id: '3', - params: { - customBucket: { - enabled: true, - id: '1-bucket', - params: { - customInterval: '2h', - drop_partials: false, - extended_bounds: {}, - field: 'timestamp', - interval: 'custom', - min_doc_count: 1, - useNormalizedEsInterval: true, - }, - type: 'date_histogram', - }, - customMetric: { - enabled: true, - id: '1-metric', - params: {}, - type: 'count', - }, - }, - schema: 'metric', - type: 'max_bucket', - }, - ], - }), - }, - }; - }); - - it('should remove customInterval from date_histogram aggregations', () => { - const migratedDoc = migrate(doc); - const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; - expect(aggs[1]).not.toHaveProperty('params.customInterval'); - }); - - it('should not change interval from date_histogram aggregations', () => { - const migratedDoc = migrate(doc); - const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; - expect(aggs[1].params.interval).toBe( - JSON.parse(doc.attributes.visState).aggs[1].params.interval - ); - }); - - it('should not remove customInterval from non date_histogram aggregations', () => { - const migratedDoc = migrate(doc); - const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; - expect(aggs[0]).toHaveProperty('params.customInterval'); - }); - - it('should set interval with customInterval value and remove customInterval when interval equals "custom"', () => { - const migratedDoc = migrate(doc); - const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; - expect(aggs[2].params.interval).toBe( - JSON.parse(doc.attributes.visState).aggs[2].params.customInterval - ); - expect(aggs[2]).not.toHaveProperty('params.customInterval'); - }); - - it('should remove customInterval from nested aggregations', () => { - const migratedDoc = migrate(doc); - const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; - expect(aggs[3]).not.toHaveProperty('params.customBucket.params.customInterval'); - }); - - it('should remove customInterval from nested aggregations and set interval with customInterval value', () => { - const migratedDoc = migrate(doc); - const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; - expect(aggs[3].params.customBucket.params.interval).toBe( - JSON.parse(doc.attributes.visState).aggs[3].params.customBucket.params.customInterval - ); - expect(aggs[3]).not.toHaveProperty('params.customBucket.params.customInterval'); - }); - - it('should not fail on date histograms without a customInterval', () => { - const migratedDoc = migrate(doc); - const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; - expect(aggs[3]).not.toHaveProperty('params.customInterval'); - }); - }); - describe('7.3.0', () => { - const logMsgArr = []; - const logger = { - warning: msg => logMsgArr.push(msg), - }; - const migrate = doc => migrations.visualization['7.3.0'](doc, logger); - - it('migrates type = gauge verticalSplit: false to alignment: vertical', () => { - const migratedDoc = migrate({ - attributes: { - visState: JSON.stringify({ type: 'gauge', params: { gauge: { verticalSplit: false } } }), - }, - }); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "visState": "{\\"type\\":\\"gauge\\",\\"params\\":{\\"gauge\\":{\\"alignment\\":\\"horizontal\\"}}}", - }, -} -`); - }); - - it('migrates type = gauge verticalSplit: false to alignment: horizontal', () => { - const migratedDoc = migrate({ - attributes: { - visState: JSON.stringify({ type: 'gauge', params: { gauge: { verticalSplit: true } } }), - }, - }); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "visState": "{\\"type\\":\\"gauge\\",\\"params\\":{\\"gauge\\":{\\"alignment\\":\\"vertical\\"}}}", - }, -} -`); - }); - - it('doesnt migrate type = gauge containing invalid visState object, adds message to log', () => { - const migratedDoc = migrate({ - attributes: { - visState: JSON.stringify({ type: 'gauge' }), - }, - }); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "visState": "{\\"type\\":\\"gauge\\"}", - }, -} -`); - expect(logMsgArr).toMatchInlineSnapshot(` -Array [ - "Exception @ migrateGaugeVerticalSplitToAlignment! TypeError: Cannot read property 'gauge' of undefined", - "Exception @ migrateGaugeVerticalSplitToAlignment! Payload: {\\"type\\":\\"gauge\\"}", -] -`); - }); - - describe('filters agg query migration', () => { - const doc = { - attributes: { - visState: JSON.stringify({ - aggs: [ - { - type: 'filters', - params: { - filters: [ - { - input: { - query: 'response:200', - }, - label: '', - }, - { - input: { - query: 'response:404', - }, - label: 'bad response', - }, - { - input: { - query: { - exists: { - field: 'phpmemory', - }, - }, - }, - label: '', - }, - ], - }, - }, - ], - }), - }, - }; - - it('should add language property to filters without one, assuming lucene', () => { - const migrationResult = migrate(doc); - expect(migrationResult).toEqual({ - attributes: { - visState: JSON.stringify({ - aggs: [ - { - type: 'filters', - params: { - filters: [ - { - input: { - query: 'response:200', - language: 'lucene', - }, - label: '', - }, - { - input: { - query: 'response:404', - language: 'lucene', - }, - label: 'bad response', - }, - { - input: { - query: { - exists: { - field: 'phpmemory', - }, - }, - language: 'lucene', - }, - label: '', - }, - ], - }, - }, - ], - }), - }, - }); - }); - }); - - describe('replaceMovAvgToMovFn()', () => { - let doc; - - beforeEach(() => { - doc = { - attributes: { - title: 'VIS', - visState: `{"title":"VIS","type":"metrics","params":{"id":"61ca57f0-469d-11e7-af02-69e470af7417", - "type":"timeseries","series":[{"id":"61ca57f1-469d-11e7-af02-69e470af7417","color":"rgba(0,156,224,1)", - "split_mode":"terms","metrics":[{"id":"61ca57f2-469d-11e7-af02-69e470af7417","type":"count", - "numerator":"FlightDelay:true"},{"settings":"","minimize":0,"window":5,"model": - "holt_winters","id":"23054fe0-8915-11e9-9b86-d3f94982620f","type":"moving_average","field": - "61ca57f2-469d-11e7-af02-69e470af7417","predict":1}],"separate_axis":0,"axis_position":"right", - "formatter":"number","chart_type":"line","line_width":"2","point_size":"0","fill":0.5,"stacked":"none", - "label":"Percent Delays","terms_size":"2","terms_field":"OriginCityName"}],"time_field":"timestamp", - "index_pattern":"kibana_sample_data_flights","interval":">=12h","axis_position":"left","axis_formatter": - "number","show_legend":1,"show_grid":1,"annotations":[{"fields":"FlightDelay,Cancelled,Carrier", - "template":"{{Carrier}}: Flight Delayed and Cancelled!","index_pattern":"kibana_sample_data_flights", - "query_string":"FlightDelay:true AND Cancelled:true","id":"53b7dff0-4c89-11e8-a66a-6989ad5a0a39", - "color":"rgba(0,98,177,1)","time_field":"timestamp","icon":"fa-exclamation-triangle", - "ignore_global_filters":1,"ignore_panel_filters":1,"hidden":true}],"legend_position":"bottom", - "axis_scale":"normal","default_index_pattern":"kibana_sample_data_flights","default_timefield":"timestamp"}, - "aggs":[]}`, - }, - migrationVersion: { - visualization: '7.2.0', - }, - type: 'visualization', - }; - }); - - test('should add some necessary moving_fn fields', () => { - const migratedDoc = migrate(doc); - const visState = JSON.parse(migratedDoc.attributes.visState); - const metric = visState.params.series[0].metrics[1]; - - expect(metric).toHaveProperty('model_type'); - expect(metric).toHaveProperty('alpha'); - expect(metric).toHaveProperty('beta'); - expect(metric).toHaveProperty('gamma'); - expect(metric).toHaveProperty('period'); - expect(metric).toHaveProperty('multiplicative'); - }); - }); - }); - describe('7.3.0 tsvb', () => { - const migrate = doc => migrations.visualization['7.3.0'](doc); - const generateDoc = ({ params }) => ({ - attributes: { - title: 'My Vis', - description: 'This is my super cool vis.', - visState: JSON.stringify({ params }), - uiStateJSON: '{}', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: '{}', - }, - }, - }); - it('should change series item filters from a string into an object', () => { - const params = { type: 'metric', series: [{ filter: 'Filter Bytes Test:>1000' }] }; - const testDoc1 = generateDoc({ params }); - const migratedTestDoc1 = migrate(testDoc1); - const series = JSON.parse(migratedTestDoc1.attributes.visState).params.series; - expect(series[0].filter).toHaveProperty('query'); - expect(series[0].filter).toHaveProperty('language'); - }); - it('should not change a series item filter string in the object after migration', () => { - const markdownParams = { - type: 'markdown', - series: [ - { - filter: 'Filter Bytes Test:>1000', - split_filters: [{ filter: 'bytes:>1000' }], - }, - ], - }; - const markdownDoc = generateDoc({ params: markdownParams }); - const migratedMarkdownDoc = migrate(markdownDoc); - const markdownSeries = JSON.parse(migratedMarkdownDoc.attributes.visState).params.series; - expect(markdownSeries[0].filter.query).toBe( - JSON.parse(markdownDoc.attributes.visState).params.series[0].filter - ); - expect(markdownSeries[0].split_filters[0].filter.query).toBe( - JSON.parse(markdownDoc.attributes.visState).params.series[0].split_filters[0].filter - ); - }); - it('should change series item filters from a string into an object for all filters', () => { - const params = { - type: 'timeseries', - filter: 'bytes:>1000', - series: [ - { - filter: 'Filter Bytes Test:>1000', - split_filters: [{ filter: 'bytes:>1000' }], - }, - ], - annotations: [{ query_string: 'bytes:>1000' }], - }; - const timeSeriesDoc = generateDoc({ params: params }); - const migratedtimeSeriesDoc = migrate(timeSeriesDoc); - const timeSeriesParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; - expect(Object.keys(timeSeriesParams.series[0].filter)).toEqual( - expect.arrayContaining(['query', 'language']) - ); - expect(Object.keys(timeSeriesParams.series[0].split_filters[0].filter)).toEqual( - expect.arrayContaining(['query', 'language']) - ); - expect(Object.keys(timeSeriesParams.annotations[0].query_string)).toEqual( - expect.arrayContaining(['query', 'language']) - ); - }); - it('should not fail on a metric visualization without a filter in a series item', () => { - const params = { type: 'metric', series: [{}, {}, {}] }; - const testDoc1 = generateDoc({ params }); - const migratedTestDoc1 = migrate(testDoc1); - const series = JSON.parse(migratedTestDoc1.attributes.visState).params.series; - expect(series[2]).not.toHaveProperty('filter.query'); - }); - it('should not migrate a visualization of unknown type', () => { - const params = { type: 'unknown', series: [{ filter: 'foo:bar' }] }; - const doc = generateDoc({ params }); - const migratedDoc = migrate(doc); - const series = JSON.parse(migratedDoc.attributes.visState).params.series; - expect(series[0].filter).toEqual(params.series[0].filter); - }); - }); - - describe('7.3.1', () => { - const migrate = migrations.visualization['7.3.1']; - - it('should migrate filters agg query string queries', () => { - const state = { - aggs: [ - { type: 'count', params: {} }, - { - type: 'filters', - params: { - filters: [ - { - input: { - query: { - query_string: { query: 'machine.os.keyword:"win 8"' }, - }, - }, - }, - ], - }, - }, - ], - }; - const expected = { - aggs: [ - { type: 'count', params: {} }, - { - type: 'filters', - params: { - filters: [{ input: { query: 'machine.os.keyword:"win 8"' } }], - }, - }, - ], - }; - const migratedDoc = migrate({ attributes: { visState: JSON.stringify(state) } }); - expect(migratedDoc).toEqual({ attributes: { visState: JSON.stringify(expected) } }); - }); - }); - describe('7.4.2 tsvb split_filters migration', () => { - const migrate = doc => migrations.visualization['7.4.2'](doc); - const generateDoc = ({ params }) => ({ - attributes: { - title: 'My Vis', - description: 'This is my super cool vis.', - visState: JSON.stringify({ params }), - uiStateJSON: '{}', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: '{}', - }, - }, - }); - it('should change series item filters from a string into an object for all filters', () => { - const params = { - type: 'timeseries', - filter: { - query: 'bytes:>1000', - language: 'lucene', - }, - series: [ - { - split_filters: [{ filter: 'bytes:>1000' }], - }, - ], - }; - const timeSeriesDoc = generateDoc({ params: params }); - const migratedtimeSeriesDoc = migrate(timeSeriesDoc); - const timeSeriesParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; - expect(Object.keys(timeSeriesParams.filter)).toEqual( - expect.arrayContaining(['query', 'language']) - ); - expect(timeSeriesParams.series[0].split_filters[0].filter).toEqual({ - query: 'bytes:>1000', - language: 'lucene', - }); - }); - it('should change series item split filters when there is no filter item', () => { - const params = { - type: 'timeseries', - filter: { - query: 'bytes:>1000', - language: 'lucene', - }, - series: [ - { - split_filters: [{ filter: 'bytes:>1000' }], - }, - ], - annotations: [ - { - query_string: { - query: 'bytes:>1000', - language: 'lucene', - }, - }, - ], - }; - const timeSeriesDoc = generateDoc({ params: params }); - const migratedtimeSeriesDoc = migrate(timeSeriesDoc); - const timeSeriesParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; - expect(timeSeriesParams.series[0].split_filters[0].filter).toEqual({ - query: 'bytes:>1000', - language: 'lucene', - }); - }); - it('should not convert split_filters to objects if there are no split filter filters', () => { - const params = { - type: 'timeseries', - filter: { - query: 'bytes:>1000', - language: 'lucene', - }, - series: [ - { - split_filters: [], - }, - ], - }; - const timeSeriesDoc = generateDoc({ params: params }); - const migratedtimeSeriesDoc = migrate(timeSeriesDoc); - const timeSeriesParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; - expect(timeSeriesParams.series[0].split_filters).not.toHaveProperty('query'); - }); - it('should do nothing if a split_filter is already a query:language object', () => { - const params = { - type: 'timeseries', - filter: { - query: 'bytes:>1000', - language: 'lucene', - }, - series: [ - { - split_filters: [ - { - filter: { - query: 'bytes:>1000', - language: 'lucene', - }, - }, - ], - }, - ], - annotations: [ - { - query_string: { - query: 'bytes:>1000', - language: 'lucene', - }, - }, - ], - }; - const timeSeriesDoc = generateDoc({ params: params }); - const migratedtimeSeriesDoc = migrate(timeSeriesDoc); - const timeSeriesParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; - expect(timeSeriesParams.series[0].split_filters[0].filter.query).toEqual('bytes:>1000'); - expect(timeSeriesParams.series[0].split_filters[0].filter.language).toEqual('lucene'); - }); - }); -}); - describe('dashboard', () => { describe('7.0.0', () => { const migration = migrations.dashboard['7.0.0']; @@ -1751,271 +445,3 @@ Object { }); }); }); - -describe('search', () => { - describe('7.0.0', () => { - const migration = migrations.search['7.0.0']; - - test('skips errors when searchSourceJSON is null', () => { - const doc = { - id: '123', - type: 'search', - attributes: { - foo: true, - kibanaSavedObjectMeta: { - searchSourceJSON: null, - }, - }, - }; - const migratedDoc = migration(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": null, - }, - }, - "id": "123", - "references": Array [], - "type": "search", -} -`); - }); - - test('skips errors when searchSourceJSON is undefined', () => { - const doc = { - id: '123', - type: 'search', - attributes: { - foo: true, - kibanaSavedObjectMeta: { - searchSourceJSON: undefined, - }, - }, - }; - const migratedDoc = migration(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": undefined, - }, - }, - "id": "123", - "references": Array [], - "type": "search", -} -`); - }); - - test('skips error when searchSourceJSON is not a string', () => { - const doc = { - id: '123', - type: 'search', - attributes: { - foo: true, - kibanaSavedObjectMeta: { - searchSourceJSON: 123, - }, - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": 123, - }, - }, - "id": "123", - "references": Array [], - "type": "search", -} -`); - }); - - test('skips error when searchSourceJSON is invalid json', () => { - const doc = { - id: '123', - type: 'search', - attributes: { - foo: true, - kibanaSavedObjectMeta: { - searchSourceJSON: '{abc123}', - }, - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{abc123}", - }, - }, - "id": "123", - "references": Array [], - "type": "search", -} -`); - }); - - test('skips error when "index" and "filter" is missing from searchSourceJSON', () => { - const doc = { - id: '123', - type: 'search', - attributes: { - foo: true, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ bar: true }), - }, - }, - }; - const migratedDoc = migration(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{\\"bar\\":true}", - }, - }, - "id": "123", - "references": Array [], - "type": "search", -} -`); - }); - - test('extracts "index" attribute from doc', () => { - const doc = { - id: '123', - type: 'search', - attributes: { - foo: true, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ bar: true, index: 'pattern*' }), - }, - }, - }; - const migratedDoc = migration(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{\\"bar\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}", - }, - }, - "id": "123", - "references": Array [ - Object { - "id": "pattern*", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern", - }, - ], - "type": "search", -} -`); - }); - - test('extracts index patterns from filter', () => { - const doc = { - id: '123', - type: 'search', - attributes: { - foo: true, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - bar: true, - filter: [ - { - meta: { - foo: true, - index: 'my-index', - }, - }, - ], - }), - }, - }, - }; - const migratedDoc = migration(doc); - - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{\\"bar\\":true,\\"filter\\":[{\\"meta\\":{\\"foo\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}", - }, - }, - "id": "123", - "references": Array [ - Object { - "id": "my-index", - "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", - "type": "index-pattern", - }, - ], - "type": "search", -} -`); - }); - }); - - describe('7.4.0', function() { - const migration = migrations.search['7.4.0']; - - test('transforms one dimensional sort arrays into two dimensional arrays', () => { - const doc = { - id: '123', - type: 'search', - attributes: { - sort: ['bytes', 'desc'], - }, - }; - - const expected = { - id: '123', - type: 'search', - attributes: { - sort: [['bytes', 'desc']], - }, - }; - - const migratedDoc = migration(doc); - - expect(migratedDoc).toEqual(expected); - }); - - test("doesn't modify search docs that already have two dimensional sort arrays", () => { - const doc = { - id: '123', - type: 'search', - attributes: { - sort: [['bytes', 'desc']], - }, - }; - - const migratedDoc = migration(doc); - - expect(migratedDoc).toEqual(doc); - }); - - test("doesn't modify search docs that have no sort array", () => { - const doc = { - id: '123', - type: 'search', - attributes: {}, - }; - - const migratedDoc = migration(doc); - - expect(migratedDoc).toEqual(doc); - }); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts index 0c5329d8b259f..b497f73f3df2a 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts @@ -28,7 +28,7 @@ export { npSetup, npStart } from 'ui/new_platform'; export { KbnUrl } from 'ui/url/kbn_url'; // @ts-ignore -export { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url/index'; +export { KbnUrlProvider } from 'ui/url/index'; export { IInjector } from 'ui/chrome'; export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; export { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts index 9ca84735cac16..9447b5384d172 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts @@ -35,11 +35,10 @@ import { KbnUrlProvider, PrivateProvider, PromiseServiceCreator, - RedirectWhenMissingProvider, } from '../legacy_imports'; // @ts-ignore import { initDashboardApp } from './legacy_app'; -import { IEmbeddableStart } from '../../../../../../plugins/embeddable/public'; +import { EmbeddableStart } from '../../../../../../plugins/embeddable/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../../plugins/navigation/public'; import { DataPublicPluginStart } from '../../../../../../plugins/data/public'; import { SharePluginStart } from '../../../../../../plugins/share/public'; @@ -67,7 +66,7 @@ export interface RenderDeps { chrome: ChromeStart; addBasePath: (path: string) => string; savedQueryService: DataPublicPluginStart['query']['savedQueries']; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; localStorage: Storage; share: SharePluginStart; config: KibanaLegacyStart['config']; @@ -146,8 +145,7 @@ function createLocalIconModule() { function createLocalKbnUrlModule() { angular .module('app/dashboard/KbnUrl', ['app/dashboard/Private', 'ngRoute']) - .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)) - .service('redirectWhenMissing', (Private: IPrivate) => Private(RedirectWhenMissingProvider)); + .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)); } function createLocalConfigModule(core: AppMountContext['core']) { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js index 35b510894179d..f7baba663da75 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js @@ -28,6 +28,7 @@ import { initDashboardAppDirective } from './dashboard_app'; import { createDashboardEditUrl, DashboardConstants } from './dashboard_constants'; import { createKbnUrlStateStorage, + redirectWhenMissing, InvalidJSONProperty, SavedObjectNotFound, } from '../../../../../../plugins/kibana_utils/public'; @@ -136,7 +137,7 @@ export function initDashboardApp(app, deps) { }); }, resolve: { - dash: function($rootScope, $route, redirectWhenMissing, kbnUrl, history) { + dash: function($rootScope, $route, kbnUrl, history) { return ensureDefaultIndexPattern(deps.core, deps.data, $rootScope, kbnUrl).then(() => { const savedObjectsClient = deps.savedObjectsClient; const title = $route.current.params.title; @@ -171,14 +172,18 @@ export function initDashboardApp(app, deps) { controller: createNewDashboardCtrl, requireUICapability: 'dashboard.createNew', resolve: { - dash: function(redirectWhenMissing, $rootScope, kbnUrl) { + dash: function($rootScope, kbnUrl, history) { return ensureDefaultIndexPattern(deps.core, deps.data, $rootScope, kbnUrl) .then(() => { return deps.savedDashboards.get(); }) .catch( redirectWhenMissing({ - dashboard: DashboardConstants.LANDING_PAGE_PATH, + history, + mapping: { + dashboard: DashboardConstants.LANDING_PAGE_PATH, + }, + toastNotifications: deps.core.notifications.toasts, }) ); }, @@ -189,7 +194,7 @@ export function initDashboardApp(app, deps) { template: dashboardTemplate, controller: createNewDashboardCtrl, resolve: { - dash: function($rootScope, $route, redirectWhenMissing, kbnUrl, history) { + dash: function($rootScope, $route, kbnUrl, history) { const id = $route.current.params.id; return ensureDefaultIndexPattern(deps.core, deps.data, $rootScope, kbnUrl) @@ -207,7 +212,7 @@ export function initDashboardApp(app, deps) { .catch(error => { // A corrupt dashboard was detected (e.g. with invalid JSON properties) if (error instanceof InvalidJSONProperty) { - deps.toastNotifications.addDanger(error.message); + deps.core.notifications.toasts.addDanger(error.message); kbnUrl.redirect(DashboardConstants.LANDING_PAGE_PATH); return; } @@ -221,7 +226,7 @@ export function initDashboardApp(app, deps) { pathname: DashboardConstants.CREATE_NEW_DASHBOARD_URL, }); - deps.toastNotifications.addWarning( + deps.core.notifications.toasts.addWarning( i18n.translate('kbn.dashboard.urlWasRemovedInSixZeroWarningMessage', { defaultMessage: 'The url "dashboard/create" was removed in 6.0. Please update your bookmarks.', @@ -234,7 +239,11 @@ export function initDashboardApp(app, deps) { }) .catch( redirectWhenMissing({ - dashboard: DashboardConstants.LANDING_PAGE_PATH, + history, + mapping: { + dashboard: DashboardConstants.LANDING_PAGE_PATH, + }, + toastNotifications: deps.core.notifications.toasts, }) ); }, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts index d94612225782d..a9ee77921ed4a 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts @@ -35,7 +35,7 @@ import { DataPublicPluginSetup, esFilters, } from '../../../../../plugins/data/public'; -import { IEmbeddableStart } from '../../../../../plugins/embeddable/public'; +import { EmbeddableStart } from '../../../../../plugins/embeddable/public'; import { Storage } from '../../../../../plugins/kibana_utils/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; import { DashboardConstants } from './np_ready/dashboard_constants'; @@ -54,7 +54,7 @@ import { createKbnUrlTracker } from '../../../../../plugins/kibana_utils/public' export interface DashboardPluginStartDependencies { data: DataPublicPluginStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; navigation: NavigationStart; share: SharePluginStart; kibanaLegacy: KibanaLegacyStart; @@ -70,7 +70,7 @@ export class DashboardPlugin implements Plugin { private startDependencies: { data: DataPublicPluginStart; savedObjectsClient: SavedObjectsClientContract; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; navigation: NavigationStart; share: SharePluginStart; dashboardConfig: KibanaLegacyStart['dashboardConfig']; diff --git a/src/legacy/core_plugins/kibana/public/discover/build_services.ts b/src/legacy/core_plugins/kibana/public/discover/build_services.ts index c58307adaf38c..282eef0c983eb 100644 --- a/src/legacy/core_plugins/kibana/public/discover/build_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/build_services.ts @@ -16,6 +16,8 @@ * specific language governing permissions and limitations * under the License. */ +import { createHashHistory, History } from 'history'; + import { Capabilities, ChromeStart, @@ -46,6 +48,7 @@ export interface DiscoverServices { data: DataPublicPluginStart; docLinks: DocLinksStart; docViewsRegistry: DocViewsRegistry; + history: History; theme: ChartsPluginStart['theme']; filterManager: FilterManager; indexPatterns: IndexPatternsContract; @@ -79,6 +82,7 @@ export async function buildServices( data: plugins.data, docLinks: core.docLinks, docViewsRegistry, + history: createHashHistory(), theme: plugins.charts.theme, filterManager: plugins.data.query.filterManager, getSavedSearchById: async (id: string) => savedObjectService.get(id), diff --git a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts index 76d475c4f7f96..4d871bcb7a858 100644 --- a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts +++ b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts @@ -27,7 +27,7 @@ import { CoreStart, LegacyCoreStart, IUiSettingsClient } from 'kibana/public'; // @ts-ignore import { StateManagementConfigProvider } from 'ui/state_management/config_provider'; // @ts-ignore -import { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url'; +import { KbnUrlProvider } from 'ui/url'; import { DataPublicPluginStart } from '../../../../../plugins/data/public'; import { Storage } from '../../../../../plugins/kibana_utils/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; @@ -173,8 +173,7 @@ export function initializeInnerAngularModule( function createLocalKbnUrlModule() { angular .module('discoverKbnUrl', ['discoverPrivate', 'ngRoute']) - .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)) - .service('redirectWhenMissing', (Private: IPrivate) => Private(RedirectWhenMissingProvider)); + .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)); } function createLocalConfigModule(uiSettings: IUiSettingsClient) { diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index 57a9e4966d6d6..5f3dbb65fd8ff 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -53,13 +53,11 @@ export { wrapInI18nContext } from 'ui/i18n'; import { search } from '../../../../../plugins/data/public'; export const { getRequestInspectorStats, getResponseInspectorStats, tabifyAggResponse } = search; // @ts-ignore -export { shortenDottedString } from '../../common/utils/shorten_dotted_string'; -// @ts-ignore export { intervalOptions } from 'ui/agg_types'; export { subscribeWithScope } from '../../../../../plugins/kibana_legacy/public'; // @ts-ignore export { timezoneProvider } from 'ui/vis/lib/timezone'; -export { unhashUrl } from '../../../../../plugins/kibana_utils/public'; +export { unhashUrl, redirectWhenMissing } from '../../../../../plugins/kibana_utils/public'; export { ensureDefaultIndexPattern, formatMsg, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx index 26d8a5abb2471..1b3b16332fa4f 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx @@ -21,7 +21,7 @@ import classNames from 'classnames'; import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { FieldIcon, FieldIconProps } from '../../../../../../../../../plugins/kibana_react/public'; -import { shortenDottedString } from '../../../../kibana_services'; +import { shortenDottedString } from '../../../helpers'; import { getFieldTypeName } from './field_type_name'; // property field is provided at discover's field chooser diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js index f3334c9211e4b..6978781fe6696 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js @@ -50,6 +50,7 @@ import { tabifyAggResponse, getAngularModule, ensureDefaultIndexPattern, + redirectWhenMissing, } from '../../kibana_services'; const { @@ -57,6 +58,7 @@ const { chrome, data, docTitle, + history, indexPatterns, filterManager, share, @@ -113,10 +115,10 @@ app.config($routeProvider => { template: indexTemplate, reloadOnSearch: false, resolve: { - savedObjects: function(redirectWhenMissing, $route, kbnUrl, Promise, $rootScope) { + savedObjects: function($route, kbnUrl, Promise, $rootScope) { const savedSearchId = $route.current.params.id; return ensureDefaultIndexPattern(core, data, $rootScope, kbnUrl).then(() => { - const { appStateContainer } = getState({}); + const { appStateContainer } = getState({ history }); const { index } = appStateContainer.getState(); return Promise.props({ ip: indexPatterns.getCache().then(indexPatternList => { @@ -151,9 +153,13 @@ app.config($routeProvider => { }) .catch( redirectWhenMissing({ - search: '/discover', - 'index-pattern': - '/management/kibana/objects/savedSearches/' + $route.current.params.id, + history, + mapping: { + search: '/discover', + 'index-pattern': + '/management/kibana/objects/savedSearches/' + $route.current.params.id, + }, + toastNotifications, }) ), }); @@ -207,6 +213,7 @@ function discoverController( } = getState({ defaultAppState: getStateDefaults(), storeInSessionStorage: config.get('state:storeInSessionStorage'), + history, }); if (appStateContainer.getState().index !== $scope.indexPattern.id) { //used index pattern is different than the given by url/state which is invalid diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.test.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.test.ts index af772cb5c76f1..3840fd0c2e3be 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.test.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.test.ts @@ -30,7 +30,7 @@ describe('Test discover state', () => { history.push('/'); state = getState({ defaultAppState: { index: 'test' }, - hashHistory: history, + history, }); await state.replaceUrlAppState({}); await state.startSync(); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.ts index 10e7cd1d0c49d..981855d1ee774 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover_state.ts @@ -17,7 +17,7 @@ * under the License. */ import { isEqual } from 'lodash'; -import { createHashHistory, History } from 'history'; +import { History } from 'history'; import { createStateContainer, createKbnUrlStateStorage, @@ -65,9 +65,9 @@ interface GetStateParams { */ storeInSessionStorage?: boolean; /** - * Browser history used for testing + * Browser history */ - hashHistory?: History; + history: History; } export interface GetStateReturn { @@ -121,11 +121,11 @@ const APP_STATE_URL_KEY = '_a'; export function getState({ defaultAppState = {}, storeInSessionStorage = false, - hashHistory, + history, }: GetStateParams): GetStateReturn { const stateStorage = createKbnUrlStateStorage({ useHash: storeInSessionStorage, - history: hashHistory ? hashHistory : createHashHistory(), + history, }); const appStateFromUrl = stateStorage.get(APP_STATE_URL_KEY) as AppState; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header/helpers.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header/helpers.tsx index a2ad18d59d935..bd48b1e083871 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header/helpers.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header/helpers.tsx @@ -16,7 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { IndexPattern, shortenDottedString } from '../../../../../kibana_services'; +import { IndexPattern } from '../../../../../kibana_services'; +import { shortenDottedString } from '../../../../helpers'; export type SortOrder = [string, string]; export interface ColumnProps { diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable_factory.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable_factory.ts index 90f1549c9f369..6f3adc1f4fcce 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable_factory.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable_factory.ts @@ -32,6 +32,11 @@ import { SearchEmbeddable } from './search_embeddable'; import { SearchInput, SearchOutput } from './types'; import { SEARCH_EMBEDDABLE_TYPE } from './constants'; +interface StartServices { + executeTriggerActions: UiActionsStart['executeTriggerActions']; + isEditable: () => boolean; +} + export class SearchEmbeddableFactory extends EmbeddableFactory< SearchInput, SearchOutput, @@ -40,12 +45,10 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< public readonly type = SEARCH_EMBEDDABLE_TYPE; private $injector: auto.IInjectorService | null; private getInjector: () => Promise | null; - public isEditable: () => boolean; constructor( - private readonly executeTriggerActions: UiActionsStart['executeTriggerActions'], - getInjector: () => Promise, - isEditable: () => boolean + private getStartServices: () => Promise, + getInjector: () => Promise ) { super({ savedObjectMetaData: { @@ -58,13 +61,16 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< }); this.$injector = null; this.getInjector = getInjector; - this.isEditable = isEditable; } public canCreateNew() { return false; } + public async isEditable() { + return (await this.getStartServices()).isEditable(); + } + public getDisplayName() { return i18n.translate('kbn.embeddable.search.displayName', { defaultMessage: 'search', @@ -90,6 +96,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< try { const savedObject = await getServices().getSavedSearchById(savedObjectId); const indexPattern = savedObject.searchSource.getField('index'); + const { executeTriggerActions } = await this.getStartServices(); return new SearchEmbeddable( { savedSearch: savedObject, @@ -101,7 +108,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< indexPatterns: indexPattern ? [indexPattern] : [], }, input, - this.executeTriggerActions, + executeTriggerActions, parent ); } catch (e) { diff --git a/src/legacy/ui/public/validated_range/index.d.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/helpers/index.ts similarity index 80% rename from src/legacy/ui/public/validated_range/index.d.ts rename to src/legacy/core_plugins/kibana/public/discover/np_ready/helpers/index.ts index 50cacbc517be8..7196c96989e97 100644 --- a/src/legacy/ui/public/validated_range/index.d.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/helpers/index.ts @@ -17,9 +17,4 @@ * under the License. */ -import React from 'react'; -import { EuiRangeProps } from '@elastic/eui'; - -export class ValidatedDualRange extends React.Component { - allowEmptyRange?: boolean; -} +export { shortenDottedString } from './shorten_dotted_string'; diff --git a/src/legacy/core_plugins/kibana/common/utils/shorten_dotted_string.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/helpers/shorten_dotted_string.ts similarity index 81% rename from src/legacy/core_plugins/kibana/common/utils/shorten_dotted_string.js rename to src/legacy/core_plugins/kibana/public/discover/np_ready/helpers/shorten_dotted_string.ts index ca76a2a537742..9d78a96784339 100644 --- a/src/legacy/core_plugins/kibana/common/utils/shorten_dotted_string.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/helpers/shorten_dotted_string.ts @@ -22,10 +22,5 @@ const DOT_PREFIX_RE = /(.).+?\./g; /** * Convert a dot.notated.string into a short * version (d.n.string) - * - * @param {string} str - the long string to convert - * @return {string} */ -export function shortenDottedString(input) { - return typeof input !== 'string' ? input : input.replace(DOT_PREFIX_RE, '$1.'); -} +export const shortenDottedString = (input: string) => input.replace(DOT_PREFIX_RE, '$1.'); diff --git a/src/legacy/core_plugins/kibana/public/discover/plugin.ts b/src/legacy/core_plugins/kibana/public/discover/plugin.ts index 3ba0418d35f71..ba671a64592a5 100644 --- a/src/legacy/core_plugins/kibana/public/discover/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/discover/plugin.ts @@ -30,7 +30,7 @@ import { } from '../../../../../plugins/data/public'; import { registerFeature } from './np_ready/register_feature'; import './kibana_services'; -import { IEmbeddableStart, IEmbeddableSetup } from '../../../../../plugins/embeddable/public'; +import { EmbeddableStart, EmbeddableSetup } from '../../../../../plugins/embeddable/public'; import { getInnerAngularModule, getInnerAngularModuleEmbeddable } from './get_inner_angular'; import { setAngularModule, setServices } from './kibana_services'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; @@ -63,7 +63,7 @@ export interface DiscoverSetup { export type DiscoverStart = void; export interface DiscoverSetupPlugins { uiActions: UiActionsSetup; - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; kibanaLegacy: KibanaLegacySetup; home: HomePublicPluginSetup; visualizations: VisualizationsSetup; @@ -71,7 +71,7 @@ export interface DiscoverSetupPlugins { } export interface DiscoverStartPlugins { uiActions: UiActionsStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; navigation: NavigationStart; charts: ChartsPluginStart; data: DataPublicPluginStart; @@ -103,7 +103,7 @@ export class DiscoverPlugin implements Plugin { public initializeInnerAngular?: () => void; public initializeServices?: () => Promise<{ core: CoreStart; plugins: DiscoverStartPlugins }>; - setup(core: CoreSetup, plugins: DiscoverSetupPlugins): DiscoverSetup { + setup(core: CoreSetup, plugins: DiscoverSetupPlugins): DiscoverSetup { const { appMounted, appUnMounted, stop: stopUrlTracker } = createKbnUrlTracker({ baseUrl: core.http.basePath.prepend('/app/kibana'), defaultSubUrl: '#/discover', @@ -173,6 +173,7 @@ export class DiscoverPlugin implements Plugin { }); registerFeature(plugins.home); + this.registerEmbeddable(core, plugins); return { addDocView: this.docViewsRegistry.addDocView.bind(this.docViewsRegistry), }; @@ -203,8 +204,6 @@ export class DiscoverPlugin implements Plugin { return { core, plugins }; }; - - this.registerEmbeddable(core, plugins); } stop() { @@ -216,19 +215,25 @@ export class DiscoverPlugin implements Plugin { /** * register embeddable with a slimmer embeddable version of inner angular */ - private async registerEmbeddable(core: CoreStart, plugins: DiscoverStartPlugins) { + private async registerEmbeddable( + core: CoreSetup, + plugins: DiscoverSetupPlugins + ) { const { SearchEmbeddableFactory } = await import('./np_ready/embeddable'); - const isEditable = () => core.application.capabilities.discover.save as boolean; if (!this.getEmbeddableInjector) { throw Error('Discover plugin method getEmbeddableInjector is undefined'); } - const factory = new SearchEmbeddableFactory( - plugins.uiActions.executeTriggerActions, - this.getEmbeddableInjector, - isEditable - ); + const getStartServices = async () => { + const [coreStart, deps] = await core.getStartServices(); + return { + executeTriggerActions: deps.uiActions.executeTriggerActions, + isEditable: () => coreStart.application.capabilities.discover.save as boolean, + }; + }; + + const factory = new SearchEmbeddableFactory(getStartServices, this.getEmbeddableInjector); plugins.embeddable.registerEmbeddableFactory(factory.type, factory); } diff --git a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts index cfd12b3283459..7e96d7bde6e13 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts @@ -29,7 +29,7 @@ import { import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; import { Storage } from '../../../../../plugins/kibana_utils/public'; -import { IEmbeddableStart } from '../../../../../plugins/embeddable/public'; +import { EmbeddableStart } from '../../../../../plugins/embeddable/public'; import { SharePluginStart } from '../../../../../plugins/share/public'; import { DataPublicPluginStart, IndexPatternsContract } from '../../../../../plugins/data/public'; import { VisualizationsStart } from '../../../visualizations/public'; @@ -44,7 +44,7 @@ export interface VisualizeKibanaServices { chrome: ChromeStart; core: CoreStart; data: DataPublicPluginStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; getBasePath: () => string; indexPatterns: IndexPatternsContract; localStorage: Storage; diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts index 0ddf3ee67aa94..69af466a03729 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts @@ -25,7 +25,7 @@ */ // @ts-ignore -export { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url'; +export { KbnUrlProvider } from 'ui/url'; export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; export { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; export { wrapInI18nContext } from 'ui/i18n'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts index 8ef63ec5778e2..c7c3286bb5c71 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts @@ -24,7 +24,6 @@ import { AppMountContext } from 'kibana/public'; import { configureAppAngularModule, KbnUrlProvider, - RedirectWhenMissingProvider, IPrivate, PrivateProvider, PromiseServiceCreator, @@ -102,8 +101,7 @@ function createLocalAngularModule(core: AppMountContext['core'], navigation: Nav function createLocalKbnUrlModule() { angular .module('app/visualize/KbnUrl', ['app/visualize/Private', 'ngRoute']) - .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)) - .service('redirectWhenMissing', (Private: IPrivate) => Private(RedirectWhenMissingProvider)); + .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)); } function createLocalPromiseModule() { diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js index c023c402f5fea..1fab38027f65b 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js @@ -31,6 +31,7 @@ import { getEditBreadcrumbs } from '../breadcrumbs'; import { addHelpMenuToAppChrome } from '../help_menu/help_menu_util'; import { unhashUrl } from '../../../../../../../plugins/kibana_utils/public'; +import { MarkdownSimple, toMountPoint } from '../../../../../../../plugins/kibana_react/public'; import { addFatalError, kbnBaseUrl } from '../../../../../../../plugins/kibana_legacy/public'; import { SavedObjectSaveModal, @@ -75,7 +76,6 @@ function VisualizeAppController( $injector, $timeout, kbnUrl, - redirectWhenMissing, kbnUrlStateStorage, history ) { @@ -313,16 +313,33 @@ function VisualizeAppController( } ); + const stopAllSyncing = () => { + stopStateSync(); + stopSyncingQueryServiceStateWithUrl(); + stopSyncingAppFilters(); + }; + // The savedVis is pulled from elasticsearch, but the appState is pulled from the url, with the // defaults applied. If the url was from a previous session which included modifications to the // appState then they won't be equal. if (!_.isEqual(stateContainer.getState().vis, stateDefaults.vis)) { try { vis.setState(stateContainer.getState().vis); - } catch { - redirectWhenMissing({ - 'index-pattern-field': '/visualize', + } catch (error) { + // stop syncing url updtes with the state to prevent extra syncing + stopAllSyncing(); + + toastNotifications.addWarning({ + title: i18n.translate('kbn.visualize.visualizationTypeInvalidNotificationMessage', { + defaultMessage: 'Invalid visualization type', + }), + text: toMountPoint({error.message}), }); + + history.replace(`${VisualizeConstants.LANDING_PAGE_PATH}?notFound=visualization`); + + // prevent further controller execution + return; } } @@ -529,9 +546,8 @@ function VisualizeAppController( unsubscribePersisted(); unsubscribeStateUpdates(); - stopStateSync(); - stopSyncingQueryServiceStateWithUrl(); - stopSyncingAppFilters(); + + stopAllSyncing(); }); $timeout(() => { diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js index 6acdb0abdd0b5..c8acea168444f 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js @@ -59,7 +59,9 @@ export function initVisualizationDirective(app, deps) { }); $scope.$on('$destroy', () => { - $scope._handler.destroy(); + if ($scope._handler) { + $scope._handler.destroy(); + } }); }, }; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js index b9409445166bc..1002f401706cd 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js @@ -21,7 +21,10 @@ import { find } from 'lodash'; import { i18n } from '@kbn/i18n'; import { createHashHistory } from 'history'; -import { createKbnUrlStateStorage } from '../../../../../../plugins/kibana_utils/public'; +import { + createKbnUrlStateStorage, + redirectWhenMissing, +} from '../../../../../../plugins/kibana_utils/public'; import editorTemplate from './editor/editor.html'; import visualizeListingTemplate from './listing/visualize_listing.html'; @@ -100,8 +103,8 @@ export function initVisualizeApp(app, deps) { template: editorTemplate, k7Breadcrumbs: getCreateBreadcrumbs, resolve: { - savedVis: function(redirectWhenMissing, $route, $rootScope, kbnUrl) { - const { core, data, savedVisualizations, visualizations } = deps; + savedVis: function($route, $rootScope, kbnUrl, history) { + const { core, data, savedVisualizations, visualizations, toastNotifications } = deps; const visTypes = visualizations.all(); const visType = find(visTypes, { name: $route.current.params.type }); const shouldHaveIndex = visType.requiresSearch && visType.options.showIndexSelection; @@ -128,7 +131,9 @@ export function initVisualizeApp(app, deps) { }) .catch( redirectWhenMissing({ - '*': '/visualize', + history, + mapping: VisualizeConstants.LANDING_PAGE_PATH, + toastNotifications, }) ); }, @@ -139,8 +144,8 @@ export function initVisualizeApp(app, deps) { template: editorTemplate, k7Breadcrumbs: getEditBreadcrumbs, resolve: { - savedVis: function(redirectWhenMissing, $route, $rootScope, kbnUrl) { - const { chrome, core, data, savedVisualizations } = deps; + savedVis: function($route, $rootScope, kbnUrl, history) { + const { chrome, core, data, savedVisualizations, toastNotifications } = deps; return ensureDefaultIndexPattern(core, data, $rootScope, kbnUrl) .then(() => savedVisualizations.get($route.current.params.id)) .then(savedVis => { @@ -155,13 +160,17 @@ export function initVisualizeApp(app, deps) { }) .catch( redirectWhenMissing({ - visualization: '/visualize', - search: - '/management/kibana/objects/savedVisualizations/' + $route.current.params.id, - 'index-pattern': - '/management/kibana/objects/savedVisualizations/' + $route.current.params.id, - 'index-pattern-field': - '/management/kibana/objects/savedVisualizations/' + $route.current.params.id, + history, + mapping: { + visualization: VisualizeConstants.LANDING_PAGE_PATH, + search: + '/management/kibana/objects/savedVisualizations/' + $route.current.params.id, + 'index-pattern': + '/management/kibana/objects/savedVisualizations/' + $route.current.params.id, + 'index-pattern-field': + '/management/kibana/objects/savedVisualizations/' + $route.current.params.id, + }, + toastNotifications, }) ); }, diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts index ccb3b3ddbb1da..01ce872aeb679 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts @@ -24,7 +24,7 @@ import { DataPublicPluginStart, SavedQuery, } from 'src/plugins/data/public'; -import { IEmbeddableStart } from 'src/plugins/embeddable/public'; +import { EmbeddableStart } from 'src/plugins/embeddable/public'; import { PersistedState } from 'src/plugins/visualizations/public'; import { LegacyCoreStart } from 'kibana/public'; import { Vis } from 'src/legacy/core_plugins/visualizations/public'; @@ -61,7 +61,7 @@ export interface EditorRenderProps { appState: { save(): void }; core: LegacyCoreStart; data: DataPublicPluginStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; filters: Filter[]; uiState: PersistedState; timeRange: TimeRange; diff --git a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts index b9e4487ae84fb..9d88152c59aa7 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts @@ -36,7 +36,7 @@ import { DataPublicPluginSetup, esFilters, } from '../../../../../plugins/data/public'; -import { IEmbeddableStart } from '../../../../../plugins/embeddable/public'; +import { EmbeddableStart } from '../../../../../plugins/embeddable/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; import { SharePluginStart } from '../../../../../plugins/share/public'; import { @@ -55,7 +55,7 @@ import { DefaultEditorController } from '../../../vis_default_editor/public'; export interface VisualizePluginStartDependencies { data: DataPublicPluginStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; navigation: NavigationStart; share: SharePluginStart; visualizations: VisualizationsStart; @@ -71,7 +71,7 @@ export interface VisualizePluginSetupDependencies { export class VisualizePlugin implements Plugin { private startDependencies: { data: DataPublicPluginStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; navigation: NavigationStart; savedObjectsClient: SavedObjectsClientContract; share: SharePluginStart; diff --git a/src/legacy/core_plugins/kibana/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/ui_setting_defaults.js index c0628b72c2ce7..85b1956f45333 100644 --- a/src/legacy/core_plugins/kibana/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/ui_setting_defaults.js @@ -16,11 +16,13 @@ * specific language governing permissions and limitations * under the License. */ - import moment from 'moment-timezone'; import numeralLanguages from '@elastic/numeral/languages'; import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; + import { DEFAULT_QUERY_LANGUAGE } from '../../../plugins/data/common'; +import { isRelativeUrl } from '../../../core/utils'; export function getUiSettingDefaults() { const weekdays = moment.weekdays().slice(); @@ -67,17 +69,23 @@ export function getUiSettingDefaults() { defaultMessage: 'Default route', }), value: '/app/kibana', - validation: { - regexString: '^/', - message: i18n.translate('kbn.advancedSettings.defaultRoute.defaultRouteValidationMessage', { - defaultMessage: 'The route must start with a slash ("/")', - }), - }, + schema: schema.string({ + validate(value) { + if (!value.startsWith('/') || !isRelativeUrl(value)) { + return i18n.translate( + 'kbn.advancedSettings.defaultRoute.defaultRouteIsRelativeValidationMessage', + { + defaultMessage: 'Must be a relative URL.', + } + ); + } + }, + }), description: i18n.translate('kbn.advancedSettings.defaultRoute.defaultRouteText', { defaultMessage: 'This setting specifies the default route when opening Kibana. ' + 'You can use this setting to modify the landing page when opening Kibana. ' + - 'The route must start with a slash ("/").', + 'The route must be a relative URL.', }), }, 'query:queryString:options': { diff --git a/src/legacy/core_plugins/tests_bundle/tests_entry_template.js b/src/legacy/core_plugins/tests_bundle/tests_entry_template.js index 57adf730f3dd9..3e3dc284671da 100644 --- a/src/legacy/core_plugins/tests_bundle/tests_entry_template.js +++ b/src/legacy/core_plugins/tests_bundle/tests_entry_template.js @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import { Type } from '@kbn/config-schema'; import pkg from '../../../../package.json'; export const createTestEntryTemplate = defaultUiSettings => bundle => ` @@ -87,7 +87,14 @@ const coreSystem = new CoreSystem({ buildNum: 1234, devMode: true, uiSettings: { - defaults: ${JSON.stringify(defaultUiSettings, null, 2) + defaults: ${JSON.stringify( + defaultUiSettings, + (key, value) => { + if (value instanceof Type) return null; + return value; + }, + 2 + ) .split('\n') .join('\n ')}, user: {} diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx b/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx index ab7c2cd980c42..a9e816f70cf53 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx @@ -20,11 +20,10 @@ import React from 'react'; import { EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; - +import { ValidatedDualRange } from '../../../../../../src/plugins/kibana_react/public'; import { VisOptionsProps } from '../../../vis_default_editor/public'; import { SelectOption, SwitchOption } from '../../../vis_type_vislib/public'; import { TagCloudVisParams } from '../types'; -import { ValidatedDualRange } from '../legacy_imports'; function TagCloudOptions({ stateParams, setValue, vis }: VisOptionsProps) { const handleFontSizeChange = ([minFontSize, maxFontSize]: [string | number, string | number]) => { diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/legacy_imports.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/legacy_imports.ts index d5b442bc5b346..0d76bc5d8b68b 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/legacy_imports.ts +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/legacy_imports.ts @@ -18,5 +18,4 @@ */ export { Schemas } from 'ui/agg_types'; -export { ValidatedDualRange } from 'ui/validated_range'; export { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js index 0263f5b2c851c..ff2546f75c51a 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js @@ -50,7 +50,7 @@ export class VisEditor extends Component { visFields: props.visFields, extractedIndexPatterns: [''], }; - this.onBrush = createBrushHandler(getDataStart().query.timefilter); + this.onBrush = createBrushHandler(getDataStart().query.timefilter.timefilter); this.visDataSubject = new Rx.BehaviorSubject(this.props.visData); this.visData$ = this.visDataSubject.asObservable().pipe(share()); diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/_vis_types.scss b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/_vis_types.scss index 90c2007b1c94a..3db09bace079f 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/_vis_types.scss +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/_vis_types.scss @@ -7,4 +7,21 @@ .tvbVisTimeSeries { overflow: hidden; } + .tvbVisTimeSeriesDark { + .echReactiveChart_unavailable { + color: #DFE5EF; + } + .echLegendItem { + color: #DFE5EF; + } + } + .tvbVisTimeSeriesLight { + .echReactiveChart_unavailable { + color: #343741; + } + .echLegendItem { + color: #343741; + } + } } + diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js index 954d3d174bb8c..356ba08ac2427 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js @@ -33,9 +33,8 @@ import { getAxisLabelString } from '../../lib/get_axis_label_string'; import { getInterval } from '../../lib/get_interval'; import { areFieldsDifferent } from '../../lib/charts'; import { createXaxisFormatter } from '../../lib/create_xaxis_formatter'; -import { isBackgroundDark } from '../../../lib/set_is_reversed'; import { STACKED_OPTIONS } from '../../../visualizations/constants'; -import { getCoreStart } from '../../../services'; +import { getCoreStart, getUISettings } from '../../../services'; export class TimeseriesVisualization extends Component { static propTypes = { @@ -238,6 +237,7 @@ export class TimeseriesVisualization extends Component { } }); + const darkMode = getUISettings().get('theme:darkMode'); return (
null; export const AreaSeries = () => null; + +export { LIGHT_THEME, DARK_THEME } from '@elastic/charts'; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js b/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js index 986111b462b35..75554a476bdea 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js @@ -19,14 +19,13 @@ import React, { useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; import { Axis, Chart, Position, Settings, - DARK_THEME, - LIGHT_THEME, AnnotationDomainTypes, LineAnnotation, TooltipType, @@ -40,6 +39,7 @@ import { GRID_LINE_CONFIG, ICON_TYPES_MAP, STACKED_OPTIONS } from '../../constan import { AreaSeriesDecorator } from './decorators/area_decorator'; import { BarSeriesDecorator } from './decorators/bar_decorator'; import { getStackAccessors } from './utils/stack_format'; +import { getTheme, getChartClasses } from './utils/theme'; const generateAnnotationData = (values, formatter) => values.map(({ key, docs }) => ({ @@ -57,7 +57,8 @@ const handleCursorUpdate = cursor => { }; export const TimeSeries = ({ - isDarkMode, + darkMode, + backgroundColor, showGrid, legend, legendPosition, @@ -89,8 +90,13 @@ export const TimeSeries = ({ const timeZone = timezoneProvider(uiSettings)(); const hasBarChart = series.some(({ bars }) => bars.show); + // compute the theme based on the bg color + const theme = getTheme(darkMode, backgroundColor); + // apply legend style change if bgColor is configured + const classes = classNames('tvbVisTimeSeries', getChartClasses(backgroundColor)); + return ( - + { + it('should return the basic themes if no bg color is specified', () => { + // use original dark/light theme + expect(getTheme(false)).toEqual(LIGHT_THEME); + expect(getTheme(true)).toEqual(DARK_THEME); + + // discard any wrong/missing bg color + expect(getTheme(true, null)).toEqual(DARK_THEME); + expect(getTheme(true, '')).toEqual(DARK_THEME); + expect(getTheme(true, undefined)).toEqual(DARK_THEME); + }); + it('should return a highcontrast color theme for a different background', () => { + // red use a near full-black color + expect(getTheme(false, 'red').axes.axisTitleStyle.fill).toEqual('rgb(23,23,23)'); + + // violet increased the text color to full white for higer contrast + expect(getTheme(false, '#ba26ff').axes.axisTitleStyle.fill).toEqual('rgb(255,255,255)'); + + // light yellow, prefer the LIGHT_THEME fill color because already with a good contrast + expect(getTheme(false, '#fff49f').axes.axisTitleStyle.fill).toEqual('#333'); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/theme.ts b/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/theme.ts new file mode 100644 index 0000000000000..a25d5e1ce1d35 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/theme.ts @@ -0,0 +1,139 @@ +/* + * 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 colorJS from 'color'; +import { Theme, LIGHT_THEME, DARK_THEME } from '@elastic/charts'; + +function computeRelativeLuminosity(rgb: string) { + return colorJS(rgb).luminosity(); +} + +function computeContrast(rgb1: string, rgb2: string) { + return colorJS(rgb1).contrast(colorJS(rgb2)); +} + +function getAAARelativeLum(bgColor: string, fgColor: string, ratio = 7) { + const relLum1 = computeRelativeLuminosity(bgColor); + const relLum2 = computeRelativeLuminosity(fgColor); + if (relLum1 > relLum2) { + // relLum1 is brighter, relLum2 is darker + return (relLum1 + 0.05 - ratio * 0.05) / ratio; + } else { + // relLum1 is darker, relLum2 is brighter + return Math.min(ratio * (relLum1 + 0.05) - 0.05, 1); + } +} + +function getGrayFromRelLum(relLum: number) { + if (relLum <= 0.0031308) { + return relLum * 12.92; + } else { + return (1.0 + 0.055) * Math.pow(relLum, 1.0 / 2.4) - 0.055; + } +} + +function getGrayRGBfromGray(gray: number) { + const g = Math.round(gray * 255); + return `rgb(${g},${g},${g})`; +} + +function getAAAGray(bgColor: string, fgColor: string, ratio = 7) { + const relLum = getAAARelativeLum(bgColor, fgColor, ratio); + const gray = getGrayFromRelLum(relLum); + return getGrayRGBfromGray(gray); +} + +function findBestContrastColor( + bgColor: string, + lightFgColor: string, + darkFgColor: string, + ratio = 4.5 +) { + const lc = computeContrast(bgColor, lightFgColor); + const dc = computeContrast(bgColor, darkFgColor); + if (lc >= dc) { + if (lc >= ratio) { + return lightFgColor; + } + return getAAAGray(bgColor, lightFgColor, ratio); + } + if (dc >= ratio) { + return darkFgColor; + } + return getAAAGray(bgColor, darkFgColor, ratio); +} + +function isValidColor(color: string | null | undefined): color is string { + if (typeof color !== 'string') { + return false; + } + if (color.length === 0) { + return false; + } + try { + colorJS(color); + return true; + } catch { + return false; + } +} + +export function getTheme(darkMode: boolean, bgColor?: string | null): Theme { + if (!isValidColor(bgColor)) { + return darkMode ? DARK_THEME : LIGHT_THEME; + } + + const bgLuminosity = computeRelativeLuminosity(bgColor); + const mainTheme = bgLuminosity <= 0.179 ? DARK_THEME : LIGHT_THEME; + const color = findBestContrastColor( + bgColor, + LIGHT_THEME.axes.axisTitleStyle.fill, + DARK_THEME.axes.axisTitleStyle.fill + ); + return { + ...mainTheme, + axes: { + ...mainTheme.axes, + axisTitleStyle: { + ...mainTheme.axes.axisTitleStyle, + fill: color, + }, + tickLabelStyle: { + ...mainTheme.axes.tickLabelStyle, + fill: color, + }, + axisLineStyle: { + ...mainTheme.axes.axisLineStyle, + stroke: color, + }, + tickLineStyle: { + ...mainTheme.axes.tickLineStyle, + stroke: color, + }, + }, + }; +} + +export function getChartClasses(bgColor?: string) { + // keep the original theme color if no bg color is specified + if (typeof bgColor !== 'string') { + return; + } + const bgLuminosity = computeRelativeLuminosity(bgColor); + return bgLuminosity <= 0.179 ? 'tvbVisTimeSeriesDark' : 'tvbVisTimeSeriesLight'; +} diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/events.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/events.ts new file mode 100644 index 0000000000000..53d04bf6eb04a --- /dev/null +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/events.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, +} from '../../../../../../../plugins/ui_actions/public'; + +export interface VisEventToTrigger { + ['brush']: typeof SELECT_RANGE_TRIGGER; + ['filter']: typeof VALUE_CLICK_TRIGGER; +} + +export const VIS_EVENT_TO_TRIGGER: VisEventToTrigger = { + brush: SELECT_RANGE_TRIGGER, + filter: VALUE_CLICK_TRIGGER, +}; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts index 474912ed508f8..c45e6832dc836 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts @@ -36,10 +36,6 @@ import { Container, EmbeddableVisTriggerContext, } from '../../../../../../../plugins/embeddable/public'; -import { - selectRangeTrigger, - valueClickTrigger, -} from '../../../../../../../plugins/ui_actions/public'; import { dispatchRenderComplete } from '../../../../../../../plugins/kibana_utils/public'; import { IExpressionLoaderParams, @@ -50,6 +46,7 @@ import { buildPipeline } from '../legacy/build_pipeline'; import { Vis } from '../vis'; import { getExpressions, getUiActions } from '../services'; import { VisSavedObject } from '../types'; +import { VIS_EVENT_TO_TRIGGER } from './events'; const getKeys = (o: T): Array => Object.keys(o) as Array; @@ -295,8 +292,8 @@ export class VisualizeEmbeddable extends Embeddable { const setup = plugin.setup(coreMock.createSetup(), { data: dataPluginMock.createSetupContract(), expressions: expressionsPluginMock.createSetupContract(), - embeddable: embeddablePluginMock.createStartContract(), + embeddable: embeddablePluginMock.createSetupContract(), usageCollection: usageCollectionPluginMock.createSetupContract(), }); const doStart = () => diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts index 5a8a55d470540..953caecefb974 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts @@ -42,7 +42,7 @@ import { } from './services'; import { VISUALIZE_EMBEDDABLE_TYPE, VisualizeEmbeddableFactory } from './embeddable'; import { ExpressionsSetup, ExpressionsStart } from '../../../../../../plugins/expressions/public'; -import { IEmbeddableSetup } from '../../../../../../plugins/embeddable/public'; +import { EmbeddableSetup } from '../../../../../../plugins/embeddable/public'; import { visualization as visualizationFunction } from './expressions/visualization_function'; import { visualization as visualizationRenderer } from './expressions/visualization_renderer'; import { @@ -73,7 +73,7 @@ export interface VisualizationsStart extends TypesStart { export interface VisualizationsSetupDeps { expressions: ExpressionsSetup; - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; usageCollection: UsageCollectionSetup; data: DataPublicPluginSetup; } diff --git a/src/legacy/server/http/index.js b/src/legacy/server/http/index.js index 265d71e95b301..d616afb533d0a 100644 --- a/src/legacy/server/http/index.js +++ b/src/legacy/server/http/index.js @@ -24,15 +24,12 @@ import Boom from 'boom'; import { registerHapiPlugins } from './register_hapi_plugins'; import { setupBasePathProvider } from './setup_base_path_provider'; -import { setupDefaultRouteProvider } from './setup_default_route_provider'; export default async function(kbnServer, server, config) { server = kbnServer.server; setupBasePathProvider(kbnServer); - setupDefaultRouteProvider(server); - await registerHapiPlugins(server); // provide a simple way to expose static directories @@ -60,14 +57,6 @@ export default async function(kbnServer, server, config) { }); }); - server.route({ - path: '/', - method: 'GET', - async handler(req, h) { - return h.redirect(await req.getDefaultRoute()); - }, - }); - server.route({ method: 'GET', path: '/{p*}', diff --git a/src/legacy/server/http/integration_tests/default_route_provider.test.ts b/src/legacy/server/http/integration_tests/default_route_provider.test.ts deleted file mode 100644 index d91438d904558..0000000000000 --- a/src/legacy/server/http/integration_tests/default_route_provider.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -jest.mock('../../../ui/ui_settings/ui_settings_mixin', () => { - return jest.fn(); -}); - -import * as kbnTestServer from '../../../../test_utils/kbn_server'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Root } from '../../../../core/server/root'; - -let mockDefaultRouteSetting: any = ''; - -describe('default route provider', () => { - let root: Root; - beforeAll(async () => { - root = kbnTestServer.createRoot({ migrations: { skip: true } }); - - await root.setup(); - await root.start(); - - const kbnServer = kbnTestServer.getKbnServer(root); - - kbnServer.server.decorate('request', 'getUiSettingsService', function() { - return { - get: (key: string) => { - if (key === 'defaultRoute') { - return Promise.resolve(mockDefaultRouteSetting); - } - throw Error(`unsupported ui setting: ${key}`); - }, - getRegistered: () => { - return { - defaultRoute: { - value: '/app/kibana', - }, - }; - }, - }; - }); - }, 30000); - - afterAll(async () => await root.shutdown()); - - it('redirects to the configured default route', async function() { - mockDefaultRouteSetting = '/app/some/default/route'; - - const { status, header } = await kbnTestServer.request.get(root, '/'); - expect(status).toEqual(302); - expect(header).toMatchObject({ - location: '/app/some/default/route', - }); - }); - - const invalidRoutes = [ - 'http://not-your-kibana.com', - '///example.com', - '//example.com', - ' //example.com', - ]; - for (const route of invalidRoutes) { - it(`falls back to /app/kibana when the configured route (${route}) is not a valid relative path`, async function() { - mockDefaultRouteSetting = route; - - const { status, header } = await kbnTestServer.request.get(root, '/'); - expect(status).toEqual(302); - expect(header).toMatchObject({ - location: '/app/kibana', - }); - }); - } -}); diff --git a/src/legacy/server/http/integration_tests/default_route_provider_config.test.ts b/src/legacy/server/http/integration_tests/default_route_provider_config.test.ts deleted file mode 100644 index 8365941cbeb10..0000000000000 --- a/src/legacy/server/http/integration_tests/default_route_provider_config.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import * as kbnTestServer from '../../../../test_utils/kbn_server'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Root } from '../../../../core/server/root'; - -describe('default route provider', () => { - let root: Root; - - afterEach(async () => await root.shutdown()); - - it('redirects to the configured default route', async function() { - root = kbnTestServer.createRoot({ - server: { - defaultRoute: '/app/some/default/route', - }, - migrations: { skip: true }, - }); - - await root.setup(); - await root.start(); - - const kbnServer = kbnTestServer.getKbnServer(root); - - kbnServer.server.decorate('request', 'getSavedObjectsClient', function() { - return { - get: (type: string, id: string) => ({ attributes: {} }), - }; - }); - - const { status, header } = await kbnTestServer.request.get(root, '/'); - - expect(status).toEqual(302); - expect(header).toMatchObject({ - location: '/app/some/default/route', - }); - }); -}); diff --git a/src/legacy/server/http/setup_default_route_provider.ts b/src/legacy/server/http/setup_default_route_provider.ts deleted file mode 100644 index 9a580dd1c59bd..0000000000000 --- a/src/legacy/server/http/setup_default_route_provider.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Legacy } from 'kibana'; -import { parse } from 'url'; - -export function setupDefaultRouteProvider(server: Legacy.Server) { - server.decorate('request', 'getDefaultRoute', async function() { - // @ts-ignore - const request: Legacy.Request = this; - - const serverBasePath: string = server.config().get('server.basePath'); - - const uiSettings = request.getUiSettingsService(); - - const defaultRoute = await uiSettings.get('defaultRoute'); - const qualifiedDefaultRoute = `${request.getBasePath()}${defaultRoute}`; - - if (isRelativePath(qualifiedDefaultRoute, serverBasePath)) { - return qualifiedDefaultRoute; - } else { - server.log( - ['http', 'warn'], - `Ignoring configured default route of '${defaultRoute}', as it is malformed.` - ); - - const fallbackRoute = uiSettings.getRegistered().defaultRoute.value; - - const qualifiedFallbackRoute = `${request.getBasePath()}${fallbackRoute}`; - return qualifiedFallbackRoute; - } - }); - - function isRelativePath(candidatePath: string, basePath = '') { - // validate that `candidatePath` is not attempting a redirect to somewhere - // outside of this Kibana install - const { protocol, hostname, port, pathname } = parse( - candidatePath, - false /* parseQueryString */, - true /* slashesDenoteHost */ - ); - - // We should explicitly compare `protocol`, `port` and `hostname` to null to make sure these are not - // detected in the URL at all. For example `hostname` can be empty string for Node URL parser, but - // browser (because of various bwc reasons) processes URL differently (e.g. `///abc.com` - for browser - // hostname is `abc.com`, but for Node hostname is an empty string i.e. everything between schema (`//`) - // and the first slash that belongs to path. - if (protocol !== null || hostname !== null || port !== null) { - return false; - } - - if (!String(pathname).startsWith(basePath)) { - return false; - } - - return true; - } -} diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 68b5a63871372..9952b345fa06f 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -92,7 +92,6 @@ declare module 'hapi' { interface Request { getSavedObjectsClient(options?: SavedObjectsClientProviderOptions): SavedObjectsClientContract; getBasePath(): string; - getDefaultRoute(): Promise; getUiSettingsService(): IUiSettingsClient; } diff --git a/src/legacy/ui/public/chrome/chrome.js b/src/legacy/ui/public/chrome/chrome.js index 3355870eabfe7..7a75ad906a870 100644 --- a/src/legacy/ui/public/chrome/chrome.js +++ b/src/legacy/ui/public/chrome/chrome.js @@ -28,7 +28,6 @@ import '../private'; import '../promises'; import '../directives/storage'; import '../directives/watch_multi'; -import './services'; import '../react_components'; import '../i18n'; diff --git a/src/legacy/ui/public/chrome/services/global_nav_state.js b/src/legacy/ui/public/chrome/services/global_nav_state.js deleted file mode 100644 index 5a67806852fe8..0000000000000 --- a/src/legacy/ui/public/chrome/services/global_nav_state.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { distinctUntilChanged } from 'rxjs/operators'; -import { npStart } from 'ui/new_platform'; -import { uiModules } from '../../modules'; - -const newPlatformChrome = npStart.core.chrome; - -uiModules.get('kibana').service('globalNavState', $rootScope => { - let isOpen = false; - newPlatformChrome - .getIsCollapsed$() - .pipe(distinctUntilChanged()) - .subscribe(isCollapsed => { - $rootScope.$evalAsync(() => { - isOpen = !isCollapsed; - $rootScope.$broadcast('globalNavState:change'); - }); - }); - - return { - isOpen: () => isOpen, - - setOpen: newValue => { - newPlatformChrome.setIsCollapsed(!newValue); - }, - }; -}); diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index ce4e1b0551881..07e17ad562291 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -20,7 +20,7 @@ import { IScope } from 'angular'; import { UiActionsStart, UiActionsSetup } from 'src/plugins/ui_actions/public'; -import { IEmbeddableStart, IEmbeddableSetup } from 'src/plugins/embeddable/public'; +import { EmbeddableStart, EmbeddableSetup } from 'src/plugins/embeddable/public'; import { createBrowserHistory } from 'history'; import { LegacyCoreSetup, @@ -68,7 +68,7 @@ export interface PluginsSetup { bfetch: BfetchPublicSetup; charts: ChartsPluginSetup; data: ReturnType; - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; expressions: ReturnType; home: HomePublicPluginSetup; inspector: InspectorSetup; @@ -88,7 +88,7 @@ export interface PluginsStart { bfetch: BfetchPublicStart; charts: ChartsPluginStart; data: ReturnType; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; expressions: ReturnType; inspector: InspectorStart; uiActions: UiActionsStart; diff --git a/src/legacy/ui/public/vis/map/service_settings.js b/src/legacy/ui/public/vis/map/service_settings.js index 233ee526c439b..9f3d21831e3da 100644 --- a/src/legacy/ui/public/vis/map/service_settings.js +++ b/src/legacy/ui/public/vis/map/service_settings.js @@ -47,7 +47,8 @@ uiModules this._showZoomMessage = true; this._emsClient = new EMSClient({ language: i18n.getLocale(), - kbnVersion: kbnVersion, + appVersion: kbnVersion, + appName: 'kibana', fileApiUrl: mapConfig.emsFileApiUrl, tileApiUrl: mapConfig.emsTileApiUrl, htmlSanitizer: $sanitize, diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx b/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx index 7a2ab648ec258..6103041cf0a4c 100644 --- a/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx @@ -22,7 +22,11 @@ import { Observable } from 'rxjs'; import { ReactWrapper } from 'enzyme'; import { mountWithI18nProvider } from 'test_utils/enzyme_helpers'; import dedent from 'dedent'; -import { UiSettingsParams, UserProvidedValues, UiSettingsType } from '../../../../core/public'; +import { + PublicUiSettingsParams, + UserProvidedValues, + UiSettingsType, +} from '../../../../core/public'; import { FieldSetting } from './types'; import { AdvancedSettingsComponent } from './advanced_settings'; import { notificationServiceMock, docLinksServiceMock } from '../../../../core/public/mocks'; @@ -68,7 +72,7 @@ function mockConfig() { remove: (key: string) => Promise.resolve(true), isCustom: (key: string) => false, isOverridden: (key: string) => Boolean(config.getAll()[key].isOverridden), - getRegistered: () => ({} as Readonly>), + getRegistered: () => ({} as Readonly>), overrideLocalDefault: (key: string, value: any) => {}, getUpdate$: () => new Observable<{ @@ -89,7 +93,7 @@ function mockConfig() { getUpdateErrors$: () => new Observable(), get: (key: string, defaultOverride?: any): any => config.getAll()[key] || defaultOverride, get$: (key: string) => new Observable(config.get(key)), - getAll: (): Readonly> => { + getAll: (): Readonly> => { return { 'test:array:setting': { ...defaultConfig, diff --git a/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.test.ts b/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.test.ts index 881a2eb003cc8..7ac9b281eb99a 100644 --- a/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.test.ts +++ b/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { UiSettingsParams, StringValidationRegex } from 'src/core/public'; +import { PublicUiSettingsParams, StringValidationRegex } from 'src/core/public'; import expect from '@kbn/expect'; import { toEditableConfig } from './to_editable_config'; @@ -30,7 +30,7 @@ function invoke({ name = 'woah', value = 'forreal', }: { - def?: UiSettingsParams & { isOverridden?: boolean }; + def?: PublicUiSettingsParams & { isOverridden?: boolean }; name?: string; value?: any; }) { @@ -55,7 +55,7 @@ describe('Settings', function() { }); describe('when given a setting definition object', function() { - let def: UiSettingsParams & { isOverridden?: boolean }; + let def: PublicUiSettingsParams & { isOverridden?: boolean }; beforeEach(function() { def = { value: 'the original', diff --git a/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts b/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts index 2c27d72f7f645..406bc35f826e8 100644 --- a/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts +++ b/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts @@ -18,7 +18,7 @@ */ import { - UiSettingsParams, + PublicUiSettingsParams, UserProvidedValues, StringValidationRegexString, SavedObjectAttribute, @@ -40,7 +40,7 @@ export function toEditableConfig({ isCustom, isOverridden, }: { - def: UiSettingsParams & UserProvidedValues; + def: PublicUiSettingsParams & UserProvidedValues; name: string; value: SavedObjectAttribute; isCustom: boolean; diff --git a/src/plugins/advanced_settings/public/management_app/types.ts b/src/plugins/advanced_settings/public/management_app/types.ts index d44a05ce36f5d..ee9b9b0535b79 100644 --- a/src/plugins/advanced_settings/public/management_app/types.ts +++ b/src/plugins/advanced_settings/public/management_app/types.ts @@ -17,17 +17,12 @@ * under the License. */ -import { - UiSettingsType, - StringValidation, - ImageValidation, - SavedObjectAttribute, -} from '../../../../core/public'; +import { UiSettingsType, StringValidation, ImageValidation } from '../../../../core/public'; export interface FieldSetting { displayName: string; name: string; - value: SavedObjectAttribute; + value: unknown; description?: string; options?: string[]; optionLabels?: Record; @@ -36,7 +31,7 @@ export interface FieldSetting { category: string[]; ariaName: string; isOverridden: boolean; - defVal: SavedObjectAttribute; + defVal: unknown; isCustom: boolean; validation?: StringValidation | ImageValidation; readOnly?: boolean; diff --git a/src/plugins/console/public/lib/kb/kb.js b/src/plugins/console/public/lib/kb/kb.js index 95896bed02988..053b82bd81d0a 100644 --- a/src/plugins/console/public/lib/kb/kb.js +++ b/src/plugins/console/public/lib/kb/kb.js @@ -147,13 +147,9 @@ function loadApisFromJson( } export function setActiveApi(api) { - if (_.isString(api)) { + if (!api) { $.ajax({ - url: - '../api/console/api_server?sense_version=' + - encodeURIComponent('@@SENSE_VERSION') + - '&apis=' + - encodeURIComponent(api), + url: '../api/console/api_server', dataType: 'json', // disable automatic guessing }).then( function(data) { @@ -169,7 +165,7 @@ export function setActiveApi(api) { ACTIVE_API = api; } -setActiveApi('es_6_0'); +setActiveApi(); export const _test = { loadApisFromJson: loadApisFromJson, diff --git a/src/plugins/console/server/lib/index.ts b/src/plugins/console/server/lib/index.ts index 98004768f880b..2347084b73a66 100644 --- a/src/plugins/console/server/lib/index.ts +++ b/src/plugins/console/server/lib/index.ts @@ -22,4 +22,4 @@ export { ProxyConfigCollection } from './proxy_config_collection'; export { proxyRequest } from './proxy_request'; export { getElasticsearchProxyConfig } from './elasticsearch_proxy_config'; export { setHeaders } from './set_headers'; -export { addProcessorDefinition, addExtensionSpecFilePath } from './spec_definitions'; +export { addProcessorDefinition, addExtensionSpecFilePath, loadSpec } from './spec_definitions'; diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0.js b/src/plugins/console/server/lib/spec_definitions/es.js similarity index 54% rename from src/plugins/console/server/lib/spec_definitions/es_6_0.js rename to src/plugins/console/server/lib/spec_definitions/es.js index 171d232407956..fc24a64f8a6f4 100644 --- a/src/plugins/console/server/lib/spec_definitions/es_6_0.js +++ b/src/plugins/console/server/lib/spec_definitions/es.js @@ -18,26 +18,30 @@ */ import Api from './api'; -import { getSpec } from './spec'; -import { register } from './es_6_0/ingest'; -const ES_6_0 = new Api('es_6_0'); -const spec = getSpec(); +import { getSpec } from './json'; +import { register } from './js/ingest'; +const ES = new Api('es'); -// adding generated specs -Object.keys(spec).forEach(endpoint => { - ES_6_0.addEndpointDescription(endpoint, spec[endpoint]); -}); +export const loadSpec = () => { + const spec = getSpec(); -//adding globals and custom API definitions -require('./es_6_0/aliases')(ES_6_0); -require('./es_6_0/aggregations')(ES_6_0); -require('./es_6_0/document')(ES_6_0); -require('./es_6_0/filter')(ES_6_0); -require('./es_6_0/globals')(ES_6_0); -register(ES_6_0); -require('./es_6_0/mappings')(ES_6_0); -require('./es_6_0/query')(ES_6_0); -require('./es_6_0/reindex')(ES_6_0); -require('./es_6_0/search')(ES_6_0); + // adding generated specs + Object.keys(spec).forEach(endpoint => { + ES.addEndpointDescription(endpoint, spec[endpoint]); + }); -export default ES_6_0; + // adding globals and custom API definitions + require('./js/aliases')(ES); + require('./js/aggregations')(ES); + require('./js/document')(ES); + require('./js/filter')(ES); + require('./js/globals')(ES); + register(ES); + require('./js/mappings')(ES); + require('./js/settings')(ES); + require('./js/query')(ES); + require('./js/reindex')(ES); + require('./js/search')(ES); +}; + +export default ES; diff --git a/src/plugins/console/server/lib/spec_definitions/index.d.ts b/src/plugins/console/server/lib/spec_definitions/index.d.ts index 0a79d3fb386f1..da0125a186c15 100644 --- a/src/plugins/console/server/lib/spec_definitions/index.d.ts +++ b/src/plugins/console/server/lib/spec_definitions/index.d.ts @@ -19,6 +19,13 @@ export declare function addProcessorDefinition(...args: any[]): any; -export declare function resolveApi(senseVersion: string, apis: string[]): object; +export declare function resolveApi(): object; export declare function addExtensionSpecFilePath(...args: any[]): any; + +/** + * A function that synchronously reads files JSON from disk and builds + * the autocomplete structures served to the client. This must be called + * after any extensions have been loaded. + */ +export declare function loadSpec(): any; diff --git a/src/plugins/console/server/lib/spec_definitions/index.js b/src/plugins/console/server/lib/spec_definitions/index.js index 3fe1913d5a193..abf55639fbee8 100644 --- a/src/plugins/console/server/lib/spec_definitions/index.js +++ b/src/plugins/console/server/lib/spec_definitions/index.js @@ -17,8 +17,10 @@ * under the License. */ -export { addProcessorDefinition } from './es_6_0/ingest'; +export { addProcessorDefinition } from './js/ingest'; -export { addExtensionSpecFilePath } from './spec'; +export { addExtensionSpecFilePath } from './json'; + +export { loadSpec } from './es'; export { resolveApi } from './server'; diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/aggregations.js b/src/plugins/console/server/lib/spec_definitions/js/aggregations.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/aggregations.js rename to src/plugins/console/server/lib/spec_definitions/js/aggregations.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/aliases.js b/src/plugins/console/server/lib/spec_definitions/js/aliases.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/aliases.js rename to src/plugins/console/server/lib/spec_definitions/js/aliases.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/document.js b/src/plugins/console/server/lib/spec_definitions/js/document.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/document.js rename to src/plugins/console/server/lib/spec_definitions/js/document.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/filter.js b/src/plugins/console/server/lib/spec_definitions/js/filter.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/filter.js rename to src/plugins/console/server/lib/spec_definitions/js/filter.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/globals.js b/src/plugins/console/server/lib/spec_definitions/js/globals.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/globals.js rename to src/plugins/console/server/lib/spec_definitions/js/globals.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/ingest.js b/src/plugins/console/server/lib/spec_definitions/js/ingest.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/ingest.js rename to src/plugins/console/server/lib/spec_definitions/js/ingest.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/mappings.js b/src/plugins/console/server/lib/spec_definitions/js/mappings.js similarity index 99% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/mappings.js rename to src/plugins/console/server/lib/spec_definitions/js/mappings.js index 8c31e5bc6fbb2..5884d14d4dc8b 100644 --- a/src/plugins/console/server/lib/spec_definitions/es_6_0/mappings.js +++ b/src/plugins/console/server/lib/spec_definitions/js/mappings.js @@ -19,9 +19,7 @@ const _ = require('lodash'); -const BOOLEAN = { - __one_of: [true, false], -}; +import { BOOLEAN } from './shared'; export default function(api) { api.addEndpointDescription('put_mapping', { diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/query/dsl.js b/src/plugins/console/server/lib/spec_definitions/js/query/dsl.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/query/dsl.js rename to src/plugins/console/server/lib/spec_definitions/js/query/dsl.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/query/index.js b/src/plugins/console/server/lib/spec_definitions/js/query/index.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/query/index.js rename to src/plugins/console/server/lib/spec_definitions/js/query/index.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/query/templates.js b/src/plugins/console/server/lib/spec_definitions/js/query/templates.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/query/templates.js rename to src/plugins/console/server/lib/spec_definitions/js/query/templates.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/reindex.js b/src/plugins/console/server/lib/spec_definitions/js/reindex.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/reindex.js rename to src/plugins/console/server/lib/spec_definitions/js/reindex.js diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/search.js b/src/plugins/console/server/lib/spec_definitions/js/search.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/es_6_0/search.js rename to src/plugins/console/server/lib/spec_definitions/js/search.js diff --git a/src/plugins/console/server/lib/spec_definitions/js/settings.js b/src/plugins/console/server/lib/spec_definitions/js/settings.js new file mode 100644 index 0000000000000..26cd0987c34a5 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/js/settings.js @@ -0,0 +1,74 @@ +/* + * 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 { BOOLEAN } from './shared'; + +export default function(api) { + api.addEndpointDescription('put_settings', { + data_autocomplete_rules: { + refresh_interval: '1s', + number_of_shards: 1, + number_of_replicas: 1, + 'blocks.read_only': BOOLEAN, + 'blocks.read': BOOLEAN, + 'blocks.write': BOOLEAN, + 'blocks.metadata': BOOLEAN, + term_index_interval: 32, + term_index_divisor: 1, + 'translog.flush_threshold_ops': 5000, + 'translog.flush_threshold_size': '200mb', + 'translog.flush_threshold_period': '30m', + 'translog.disable_flush': BOOLEAN, + 'cache.filter.max_size': '2gb', + 'cache.filter.expire': '2h', + 'gateway.snapshot_interval': '10s', + routing: { + allocation: { + include: { + tag: '', + }, + exclude: { + tag: '', + }, + require: { + tag: '', + }, + total_shards_per_node: -1, + }, + }, + 'recovery.initial_shards': { + __one_of: ['quorum', 'quorum-1', 'half', 'full', 'full-1'], + }, + 'ttl.disable_purge': BOOLEAN, + analysis: { + analyzer: {}, + tokenizer: {}, + filter: {}, + char_filter: {}, + }, + 'cache.query.enable': BOOLEAN, + shadow_replicas: BOOLEAN, + shared_filesystem: BOOLEAN, + data_path: 'path', + codec: { + __one_of: ['default', 'best_compression', 'lucene_default'], + }, + }, + }); +} diff --git a/src/plugins/embeddable/public/api/get_embeddable_factories.ts b/src/plugins/console/server/lib/spec_definitions/js/shared.js similarity index 79% rename from src/plugins/embeddable/public/api/get_embeddable_factories.ts rename to src/plugins/console/server/lib/spec_definitions/js/shared.js index c12d1283905f5..ace189e2d0913 100644 --- a/src/plugins/embeddable/public/api/get_embeddable_factories.ts +++ b/src/plugins/console/server/lib/spec_definitions/js/shared.js @@ -17,10 +17,6 @@ * under the License. */ -import { EmbeddableApiPure } from './types'; - -export const getEmbeddableFactories: EmbeddableApiPure['getEmbeddableFactories'] = ({ - embeddableFactories, -}) => () => { - return embeddableFactories.values(); -}; +export const BOOLEAN = Object.freeze({ + __one_of: [true, false], +}); diff --git a/src/plugins/console/server/lib/spec_definitions/spec/.eslintrc b/src/plugins/console/server/lib/spec_definitions/json/.eslintrc similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/.eslintrc rename to src/plugins/console/server/lib/spec_definitions/json/.eslintrc diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/_common.json b/src/plugins/console/server/lib/spec_definitions/json/generated/_common.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/_common.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/_common.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/bulk.json b/src/plugins/console/server/lib/spec_definitions/json/generated/bulk.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/bulk.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/bulk.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.aliases.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.aliases.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.aliases.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.aliases.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.allocation.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.allocation.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.allocation.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.allocation.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.count.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.count.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.count.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.count.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.fielddata.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.fielddata.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.fielddata.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.fielddata.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.health.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.health.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.health.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.health.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.help.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.help.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.help.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.help.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.indices.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.indices.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.indices.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.indices.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.master.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.master.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.master.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.master.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.nodeattrs.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.nodeattrs.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.nodeattrs.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.nodeattrs.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.nodes.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.nodes.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.nodes.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.nodes.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.pending_tasks.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.pending_tasks.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.pending_tasks.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.pending_tasks.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.plugins.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.plugins.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.plugins.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.plugins.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.recovery.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.recovery.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.recovery.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.recovery.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.repositories.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.repositories.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.repositories.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.repositories.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.segments.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.segments.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.segments.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.segments.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.shards.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.shards.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.shards.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.shards.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.snapshots.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.snapshots.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.snapshots.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.snapshots.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.tasks.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.tasks.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.tasks.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.tasks.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.templates.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.templates.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.templates.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.templates.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cat.thread_pool.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.thread_pool.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cat.thread_pool.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.thread_pool.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/clear_scroll.json b/src/plugins/console/server/lib/spec_definitions/json/generated/clear_scroll.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/clear_scroll.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/clear_scroll.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.allocation_explain.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.allocation_explain.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.allocation_explain.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.allocation_explain.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.get_settings.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_settings.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.get_settings.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_settings.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.health.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.health.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.health.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.health.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.pending_tasks.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.pending_tasks.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.pending_tasks.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.pending_tasks.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.put_settings.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.put_settings.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.put_settings.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.put_settings.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.remote_info.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.remote_info.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.remote_info.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.remote_info.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.reroute.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.reroute.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.reroute.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.reroute.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.state.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.state.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.state.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.state.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.stats.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/cluster.stats.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cluster.stats.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/count.json b/src/plugins/console/server/lib/spec_definitions/json/generated/count.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/count.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/count.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/create.json b/src/plugins/console/server/lib/spec_definitions/json/generated/create.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/create.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/create.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/delete.json b/src/plugins/console/server/lib/spec_definitions/json/generated/delete.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/delete.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/delete.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/delete_by_query.json b/src/plugins/console/server/lib/spec_definitions/json/generated/delete_by_query.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/delete_by_query.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/delete_by_query.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/delete_by_query_rethrottle.json b/src/plugins/console/server/lib/spec_definitions/json/generated/delete_by_query_rethrottle.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/delete_by_query_rethrottle.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/delete_by_query_rethrottle.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/delete_script.json b/src/plugins/console/server/lib/spec_definitions/json/generated/delete_script.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/delete_script.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/delete_script.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/exists.json b/src/plugins/console/server/lib/spec_definitions/json/generated/exists.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/exists.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/exists.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/exists_source.json b/src/plugins/console/server/lib/spec_definitions/json/generated/exists_source.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/exists_source.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/exists_source.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/explain.json b/src/plugins/console/server/lib/spec_definitions/json/generated/explain.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/explain.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/explain.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/field_caps.json b/src/plugins/console/server/lib/spec_definitions/json/generated/field_caps.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/field_caps.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/field_caps.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/get.json b/src/plugins/console/server/lib/spec_definitions/json/generated/get.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/get.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/get.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/get_script.json b/src/plugins/console/server/lib/spec_definitions/json/generated/get_script.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/get_script.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/get_script.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/get_script_context.json b/src/plugins/console/server/lib/spec_definitions/json/generated/get_script_context.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/get_script_context.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/get_script_context.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/get_script_languages.json b/src/plugins/console/server/lib/spec_definitions/json/generated/get_script_languages.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/get_script_languages.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/get_script_languages.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/get_source.json b/src/plugins/console/server/lib/spec_definitions/json/generated/get_source.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/get_source.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/get_source.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/index.json b/src/plugins/console/server/lib/spec_definitions/json/generated/index.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/index.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/index.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.analyze.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.analyze.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.analyze.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.analyze.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.clear_cache.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.clear_cache.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.clear_cache.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.clear_cache.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.clone.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.clone.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.clone.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.clone.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.close.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.close.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.close.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.close.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.create.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.create.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.create.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.create.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.delete.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.delete.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.delete_alias.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_alias.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.delete_alias.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_alias.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.delete_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.delete_template.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.exists.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.exists.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.exists_alias.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_alias.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.exists_alias.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_alias.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.exists_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.exists_template.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.exists_type.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_type.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.exists_type.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_type.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.flush.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.flush.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.flush.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.flush.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.flush_synced.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.flush_synced.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.flush_synced.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.flush_synced.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.forcemerge.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.forcemerge.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.forcemerge.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.forcemerge.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.get.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_alias.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_alias.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_alias.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_alias.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_field_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_field_mapping.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_field_mapping.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_field_mapping.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_mapping.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_mapping.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_mapping.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_settings.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_settings.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_settings.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_settings.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_template.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_upgrade.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_upgrade.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.get_upgrade.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_upgrade.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.open.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.open.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.open.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.open.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.put_alias.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_alias.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.put_alias.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_alias.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.put_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_mapping.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.put_mapping.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_mapping.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.put_settings.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_settings.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.put_settings.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_settings.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.put_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.put_template.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.recovery.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.recovery.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.recovery.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.recovery.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.refresh.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.refresh.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.refresh.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.refresh.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.rollover.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.rollover.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.rollover.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.rollover.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.segments.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.segments.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.segments.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.segments.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.shard_stores.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.shard_stores.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.shard_stores.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.shard_stores.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.shrink.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.shrink.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.shrink.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.shrink.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.split.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.split.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.split.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.split.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.stats.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.stats.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.stats.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.update_aliases.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.update_aliases.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.update_aliases.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.update_aliases.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.upgrade.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.upgrade.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.upgrade.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.upgrade.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/indices.validate_query.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.validate_query.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/indices.validate_query.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.validate_query.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/info.json b/src/plugins/console/server/lib/spec_definitions/json/generated/info.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/info.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/info.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.delete_pipeline.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.delete_pipeline.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.delete_pipeline.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ingest.delete_pipeline.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.get_pipeline.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.get_pipeline.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.get_pipeline.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ingest.get_pipeline.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.processor_grok.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.processor_grok.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.processor_grok.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ingest.processor_grok.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.put_pipeline.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.put_pipeline.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.put_pipeline.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ingest.put_pipeline.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.simulate.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.simulate.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/ingest.simulate.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ingest.simulate.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/mget.json b/src/plugins/console/server/lib/spec_definitions/json/generated/mget.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/mget.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/mget.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/msearch.json b/src/plugins/console/server/lib/spec_definitions/json/generated/msearch.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/msearch.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/msearch.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/msearch_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/msearch_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/msearch_template.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/msearch_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/mtermvectors.json b/src/plugins/console/server/lib/spec_definitions/json/generated/mtermvectors.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/mtermvectors.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/mtermvectors.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.hot_threads.json b/src/plugins/console/server/lib/spec_definitions/json/generated/nodes.hot_threads.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.hot_threads.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/nodes.hot_threads.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.info.json b/src/plugins/console/server/lib/spec_definitions/json/generated/nodes.info.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.info.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/nodes.info.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.reload_secure_settings.json b/src/plugins/console/server/lib/spec_definitions/json/generated/nodes.reload_secure_settings.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.reload_secure_settings.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/nodes.reload_secure_settings.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/nodes.stats.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.stats.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/nodes.stats.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.usage.json b/src/plugins/console/server/lib/spec_definitions/json/generated/nodes.usage.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/nodes.usage.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/nodes.usage.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/ping.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ping.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/ping.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ping.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/put_script.json b/src/plugins/console/server/lib/spec_definitions/json/generated/put_script.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/put_script.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/put_script.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/rank_eval.json b/src/plugins/console/server/lib/spec_definitions/json/generated/rank_eval.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/rank_eval.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/rank_eval.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/reindex.json b/src/plugins/console/server/lib/spec_definitions/json/generated/reindex.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/reindex.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/reindex.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/reindex_rethrottle.json b/src/plugins/console/server/lib/spec_definitions/json/generated/reindex_rethrottle.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/reindex_rethrottle.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/reindex_rethrottle.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/render_search_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/render_search_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/render_search_template.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/render_search_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/scripts_painless_execute.json b/src/plugins/console/server/lib/spec_definitions/json/generated/scripts_painless_execute.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/scripts_painless_execute.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/scripts_painless_execute.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/scroll.json b/src/plugins/console/server/lib/spec_definitions/json/generated/scroll.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/scroll.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/scroll.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/search.json b/src/plugins/console/server/lib/spec_definitions/json/generated/search.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/search.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/search.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/search_shards.json b/src/plugins/console/server/lib/spec_definitions/json/generated/search_shards.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/search_shards.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/search_shards.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/search_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/search_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/search_template.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/search_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.cleanup_repository.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.cleanup_repository.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.cleanup_repository.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.cleanup_repository.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.create.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.create.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.create.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.create.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.create_repository.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.create_repository.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.create_repository.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.create_repository.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.delete.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.delete.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.delete.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.delete.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.delete_repository.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.delete_repository.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.delete_repository.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.delete_repository.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.get.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.get.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.get.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.get.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.get_repository.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.get_repository.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.get_repository.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.get_repository.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.restore.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.restore.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.restore.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.restore.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.status.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.status.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.status.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.status.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.verify_repository.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.verify_repository.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/snapshot.verify_repository.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.verify_repository.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/tasks.cancel.json b/src/plugins/console/server/lib/spec_definitions/json/generated/tasks.cancel.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/tasks.cancel.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/tasks.cancel.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/tasks.get.json b/src/plugins/console/server/lib/spec_definitions/json/generated/tasks.get.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/tasks.get.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/tasks.get.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/tasks.list.json b/src/plugins/console/server/lib/spec_definitions/json/generated/tasks.list.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/tasks.list.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/tasks.list.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/termvectors.json b/src/plugins/console/server/lib/spec_definitions/json/generated/termvectors.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/termvectors.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/termvectors.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/update.json b/src/plugins/console/server/lib/spec_definitions/json/generated/update.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/update.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/update.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/update_by_query.json b/src/plugins/console/server/lib/spec_definitions/json/generated/update_by_query.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/update_by_query.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/update_by_query.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/generated/update_by_query_rethrottle.json b/src/plugins/console/server/lib/spec_definitions/json/generated/update_by_query_rethrottle.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/generated/update_by_query_rethrottle.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/update_by_query_rethrottle.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/index.js b/src/plugins/console/server/lib/spec_definitions/json/index.js similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/index.js rename to src/plugins/console/server/lib/spec_definitions/json/index.js diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/clear_scroll.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/clear_scroll.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/clear_scroll.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/clear_scroll.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/cluster.health.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.health.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/cluster.health.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.health.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/cluster.put_settings.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.put_settings.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/cluster.put_settings.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.put_settings.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/cluster.reroute.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.reroute.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/cluster.reroute.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.reroute.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/count.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/count.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/count.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/count.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.analyze.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.analyze.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.analyze.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.analyze.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.clone.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.clone.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.clone.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.clone.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.create.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.create.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.create.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.create.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.delete_template.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.delete_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.delete_template.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.delete_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.exists_template.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.exists_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.exists_template.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.exists_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.get_field_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.get_field_mapping.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.get_field_mapping.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.get_field_mapping.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.get_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.get_mapping.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.get_mapping.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.get_mapping.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.get_template.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.get_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.get_template.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.get_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_alias.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.put_alias.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_alias.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.put_alias.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.put_settings.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.put_settings.json new file mode 100644 index 0000000000000..2ae8fd82be4d8 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.put_settings.json @@ -0,0 +1,7 @@ +{ + "indices.put_settings": { + "data_autocomplete_rules": { + "__scope_link": "put_settings" + } + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_template.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.put_template.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_template.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.put_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.rollover.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.rollover.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.rollover.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.rollover.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.update_aliases.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.update_aliases.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.update_aliases.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.update_aliases.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.validate_query.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/indices.validate_query.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.validate_query.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/indices.validate_query.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/snapshot.create.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/snapshot.create.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/snapshot.create.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/snapshot.create.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/snapshot.create_repository.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/snapshot.create_repository.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/snapshot.create_repository.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/snapshot.create_repository.json diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/snapshot.restore.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/snapshot.restore.json similarity index 100% rename from src/plugins/console/server/lib/spec_definitions/spec/overrides/snapshot.restore.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/snapshot.restore.json diff --git a/src/plugins/console/server/lib/spec_definitions/server.js b/src/plugins/console/server/lib/spec_definitions/server.js index dd700bf019507..cb855958d403a 100644 --- a/src/plugins/console/server/lib/spec_definitions/server.js +++ b/src/plugins/console/server/lib/spec_definitions/server.js @@ -17,21 +17,10 @@ * under the License. */ -import _ from 'lodash'; +import es from './es'; -const KNOWN_APIS = ['es_6_0']; - -export function resolveApi(senseVersion, apis) { - const result = {}; - _.each(apis, function(name) { - { - if (KNOWN_APIS.includes(name)) { - // for now we ignore sense_version. might add it in the api name later - const api = require('./' + name); // eslint-disable-line import/no-dynamic-require - result[name] = api.asJson(); - } - } - }); - - return result; +export function resolveApi() { + return { + es: es.asJson(), + }; } diff --git a/src/plugins/console/server/lib/spec_definitions/server.test.js b/src/plugins/console/server/lib/spec_definitions/server.test.js deleted file mode 100644 index 747689237c177..0000000000000 --- a/src/plugins/console/server/lib/spec_definitions/server.test.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { resolveApi } from './server'; - -describe('resolveApi', () => { - it('allows known APIs to be resolved', () => { - const mockReply = jest.fn(result => ({ type: () => result })); - const result = resolveApi('Sense Version', ['es_6_0'], { response: mockReply }); - expect(result).toMatchObject({ - es_6_0: { - endpoints: expect.any(Object), - globals: expect.any(Object), - name: expect.any(String), - }, - }); - }); - - it('does not resolve APIs that are not known', () => { - const mockReply = jest.fn(result => ({ type: () => result })); - const result = resolveApi('Sense Version', ['unknown'], { response: mockReply }); - expect(result).toEqual({}); - }); - - it('handles request for apis that are known and unknown', () => { - const mockReply = jest.fn(result => ({ type: () => result })); - const result = resolveApi('Sense Version', ['es_6_0'], { response: mockReply }); - expect(result).toMatchObject({ - es_6_0: { - endpoints: expect.any(Object), - globals: expect.any(Object), - name: expect.any(String), - }, - }); - }); -}); diff --git a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_settings.json b/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_settings.json deleted file mode 100644 index 2e1e3024665a4..0000000000000 --- a/src/plugins/console/server/lib/spec_definitions/spec/overrides/indices.put_settings.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "indices.put_settings": { - "data_autocomplete_rules": { - "refresh_interval": "1s", - "number_of_shards": 1, - "number_of_replicas": 1, - "blocks.read_only": { - "__one_of": [ - false, - true - ] - }, - "blocks.read": { - "__one_of": [ - true, - false - ] - }, - "blocks.write": { - "__one_of": [ - true, - false - ] - }, - "blocks.metadata": { - "__one_of": [ - true, - false - ] - }, - "term_index_interval": 32, - "term_index_divisor": 1, - "translog.flush_threshold_ops": 5000, - "translog.flush_threshold_size": "200mb", - "translog.flush_threshold_period": "30m", - "translog.disable_flush": { - "__one_of": [ - true, - false - ] - }, - "cache.filter.max_size": "2gb", - "cache.filter.expire": "2h", - "gateway.snapshot_interval": "10s", - "routing": { - "allocation": { - "include": { - "tag": "" - }, - "exclude": { - "tag": "" - }, - "require": { - "tag": "" - }, - "total_shards_per_node": -1 - } - }, - "recovery.initial_shards": { - "__one_of": [ - "quorum", - "quorum-1", - "half", - "full", - "full-1" - ] - }, - "ttl.disable_purge": { - "__one_of": [ - true, - false - ] - }, - "analysis": { - "analyzer": {}, - "tokenizer": {}, - "filter": {}, - "char_filter": {} - }, - "cache.query.enable": { - "__one_of": [ - true, - false - ] - }, - "shadow_replicas": { - "__one_of": [ - true, - false - ] - }, - "shared_filesystem": { - "__one_of": [ - true, - false - ] - }, - "data_path": "path", - "codec": { - "__one_of": [ - "default", - "best_compression", - "lucene_default" - ] - } - } - } -} diff --git a/src/plugins/console/server/plugin.ts b/src/plugins/console/server/plugin.ts index 65647bd5acb7c..1954918f4d74f 100644 --- a/src/plugins/console/server/plugin.ts +++ b/src/plugins/console/server/plugin.ts @@ -21,7 +21,12 @@ import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/serv import { readLegacyEsConfig } from '../../../legacy/core_plugins/console_legacy'; -import { ProxyConfigCollection, addExtensionSpecFilePath, addProcessorDefinition } from './lib'; +import { + ProxyConfigCollection, + addExtensionSpecFilePath, + addProcessorDefinition, + loadSpec, +} from './lib'; import { ConfigType } from './config'; import { registerProxyRoute } from './routes/api/console/proxy'; import { registerSpecDefinitionsRoute } from './routes/api/console/spec_definitions'; @@ -75,5 +80,7 @@ export class ConsoleServerPlugin implements Plugin { }; } - start() {} + start() { + loadSpec(); + } } diff --git a/src/plugins/console/server/routes/api/console/spec_definitions/index.ts b/src/plugins/console/server/routes/api/console/spec_definitions/index.ts index e2ece37f407ac..88bc250bbfce6 100644 --- a/src/plugins/console/server/routes/api/console/spec_definitions/index.ts +++ b/src/plugins/console/server/routes/api/console/spec_definitions/index.ts @@ -16,33 +16,19 @@ * specific language governing permissions and limitations * under the License. */ -import { schema, TypeOf } from '@kbn/config-schema'; import { IRouter, RequestHandler } from 'kibana/server'; import { resolveApi } from '../../../../lib/spec_definitions'; export const registerSpecDefinitionsRoute = ({ router }: { router: IRouter }) => { - const handler: RequestHandler> = async ( - ctx, - request, - response - ) => { - const { sense_version: version, apis } = request.query; - + const handler: RequestHandler = async (ctx, request, response) => { return response.ok({ - body: resolveApi(version, apis.split(',')), + body: resolveApi(), headers: { 'Content-Type': 'application/json', }, }); }; - const validate = { - query: schema.object({ - sense_version: schema.string({ defaultValue: '' }), - apis: schema.string(), - }), - }; - - router.get({ path: '/api/console/api_server', validate }, handler); - router.post({ path: '/api/console/api_server', validate }, handler); + router.get({ path: '/api/console/api_server', validate: false }, handler); + router.post({ path: '/api/console/api_server', validate: false }, handler); }; diff --git a/src/plugins/dashboard/public/actions/expand_panel_action.test.tsx b/src/plugins/dashboard/public/actions/expand_panel_action.test.tsx index f8c05170e8f67..22cf854a46623 100644 --- a/src/plugins/dashboard/public/actions/expand_panel_action.test.tsx +++ b/src/plugins/dashboard/public/actions/expand_panel_action.test.tsx @@ -28,7 +28,6 @@ import { ContactCardEmbeddableInput, ContactCardEmbeddableOutput, } from '../embeddable_plugin_test_samples'; -import { DashboardOptions } from '../embeddable/dashboard_container_factory'; const embeddableFactories = new Map(); embeddableFactories.set( @@ -40,7 +39,7 @@ let container: DashboardContainer; let embeddable: ContactCardEmbeddable; beforeEach(async () => { - const options: DashboardOptions = { + const options = { ExitFullScreenButton: () => null, SavedObjectFinder: () => null, application: {} as any, diff --git a/src/plugins/dashboard/public/actions/open_replace_panel_flyout.tsx b/src/plugins/dashboard/public/actions/open_replace_panel_flyout.tsx index f15d538703e21..3472d208f814c 100644 --- a/src/plugins/dashboard/public/actions/open_replace_panel_flyout.tsx +++ b/src/plugins/dashboard/public/actions/open_replace_panel_flyout.tsx @@ -24,7 +24,7 @@ import { IEmbeddable, EmbeddableInput, EmbeddableOutput, - IEmbeddableStart, + EmbeddableStart, IContainer, } from '../embeddable_plugin'; @@ -34,7 +34,7 @@ export async function openReplacePanelFlyout(options: { savedObjectFinder: React.ComponentType; notifications: CoreStart['notifications']; panelToRemove: IEmbeddable; - getEmbeddableFactories: IEmbeddableStart['getEmbeddableFactories']; + getEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; }) { const { embeddable, diff --git a/src/plugins/dashboard/public/actions/replace_panel_action.test.tsx b/src/plugins/dashboard/public/actions/replace_panel_action.test.tsx index 4438a6c997126..69346dc8c118a 100644 --- a/src/plugins/dashboard/public/actions/replace_panel_action.test.tsx +++ b/src/plugins/dashboard/public/actions/replace_panel_action.test.tsx @@ -27,7 +27,6 @@ import { ContactCardEmbeddableInput, ContactCardEmbeddableOutput, } from '../embeddable_plugin_test_samples'; -import { DashboardOptions } from '../embeddable/dashboard_container_factory'; import { coreMock } from '../../../../core/public/mocks'; import { CoreStart } from 'kibana/public'; @@ -43,7 +42,7 @@ let embeddable: ContactCardEmbeddable; let coreStart: CoreStart; beforeEach(async () => { coreStart = coreMock.createStart(); - const options: DashboardOptions = { + const options = { ExitFullScreenButton: () => null, SavedObjectFinder: () => null, application: {} as any, diff --git a/src/plugins/dashboard/public/actions/replace_panel_action.tsx b/src/plugins/dashboard/public/actions/replace_panel_action.tsx index 26d9c5c8ad4dd..21ec961917d17 100644 --- a/src/plugins/dashboard/public/actions/replace_panel_action.tsx +++ b/src/plugins/dashboard/public/actions/replace_panel_action.tsx @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { CoreStart } from '../../../../core/public'; -import { IEmbeddable, ViewMode, IEmbeddableStart } from '../embeddable_plugin'; +import { IEmbeddable, ViewMode, EmbeddableStart } from '../embeddable_plugin'; import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable'; import { ActionByType, IncompatibleActionError } from '../ui_actions_plugin'; import { openReplacePanelFlyout } from './open_replace_panel_flyout'; @@ -43,7 +43,7 @@ export class ReplacePanelAction implements ActionByType, private notifications: CoreStart['notifications'], - private getEmbeddableFactories: IEmbeddableStart['getEmbeddableFactories'] + private getEmbeddableFactories: EmbeddableStart['getEmbeddableFactories'] ) {} public getDisplayName({ embeddable }: ReplacePanelActionContext) { diff --git a/src/plugins/dashboard/public/actions/replace_panel_flyout.tsx b/src/plugins/dashboard/public/actions/replace_panel_flyout.tsx index 670105650f95a..a1cd865f771d4 100644 --- a/src/plugins/dashboard/public/actions/replace_panel_flyout.tsx +++ b/src/plugins/dashboard/public/actions/replace_panel_flyout.tsx @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; -import { GetEmbeddableFactories } from 'src/plugins/embeddable/public'; +import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; import { DashboardPanelState } from '../embeddable'; import { NotificationsStart, Toast } from '../../../../core/public'; import { IContainer, IEmbeddable, EmbeddableInput, EmbeddableOutput } from '../embeddable_plugin'; @@ -31,7 +31,7 @@ interface Props { onClose: () => void; notifications: NotificationsStart; panelToRemove: IEmbeddable; - getEmbeddableFactories: GetEmbeddableFactories; + getEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; } export class ReplacePanelFlyout extends React.Component { diff --git a/src/plugins/dashboard/public/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/embeddable/dashboard_container.tsx index f9443ab97416d..86a6e374d3e25 100644 --- a/src/plugins/dashboard/public/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/embeddable/dashboard_container.tsx @@ -30,7 +30,7 @@ import { ViewMode, EmbeddableFactory, IEmbeddable, - IEmbeddableStart, + EmbeddableStart, } from '../embeddable_plugin'; import { DASHBOARD_CONTAINER_TYPE } from './dashboard_constants'; import { createPanelState } from './panel'; @@ -77,7 +77,7 @@ export interface DashboardContainerOptions { application: CoreStart['application']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; inspector: InspectorStartContract; SavedObjectFinder: React.ComponentType; ExitFullScreenButton: React.ComponentType; diff --git a/src/plugins/dashboard/public/embeddable/dashboard_container_factory.tsx b/src/plugins/dashboard/public/embeddable/dashboard_container_factory.tsx index a358e41f7b507..0fa62fc875603 100644 --- a/src/plugins/dashboard/public/embeddable/dashboard_container_factory.tsx +++ b/src/plugins/dashboard/public/embeddable/dashboard_container_factory.tsx @@ -18,24 +18,29 @@ */ import { i18n } from '@kbn/i18n'; -import { SavedObjectMetaData } from '../../../saved_objects/public'; -import { SavedObjectAttributes } from '../../../../core/public'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; +import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { CoreStart } from '../../../../core/public'; import { ContainerOutput, EmbeddableFactory, ErrorEmbeddable, Container, } from '../embeddable_plugin'; -import { - DashboardContainer, - DashboardContainerInput, - DashboardContainerOptions, -} from './dashboard_container'; -import { DashboardCapabilities } from '../types'; +import { DashboardContainer, DashboardContainerInput } from './dashboard_container'; import { DASHBOARD_CONTAINER_TYPE } from './dashboard_constants'; +import { Start as InspectorStartContract } from '../../../inspector/public'; -export interface DashboardOptions extends DashboardContainerOptions { - savedObjectMetaData?: SavedObjectMetaData; +interface StartServices { + capabilities: CoreStart['application']['capabilities']; + application: CoreStart['application']; + overlays: CoreStart['overlays']; + notifications: CoreStart['notifications']; + embeddable: EmbeddableStart; + inspector: InspectorStartContract; + SavedObjectFinder: React.ComponentType; + ExitFullScreenButton: React.ComponentType; + uiActions: UiActionsStart; } export class DashboardContainerFactory extends EmbeddableFactory< @@ -45,23 +50,13 @@ export class DashboardContainerFactory extends EmbeddableFactory< public readonly isContainerType = true; public readonly type = DASHBOARD_CONTAINER_TYPE; - private readonly allowEditing: boolean; - - constructor(private readonly options: DashboardOptions) { - super({ savedObjectMetaData: options.savedObjectMetaData }); - - const capabilities = (options.application.capabilities - .dashboard as unknown) as DashboardCapabilities; - - if (!capabilities || typeof capabilities !== 'object') { - throw new TypeError('Dashboard capabilities not found.'); - } - - this.allowEditing = !!capabilities.createNew && !!capabilities.showWriteControls; + constructor(private readonly getStartServices: () => Promise) { + super(); } - public isEditable() { - return this.allowEditing; + public async isEditable() { + const { capabilities } = await this.getStartServices(); + return !!capabilities.createNew && !!capabilities.showWriteControls; } public getDisplayName() { @@ -82,6 +77,7 @@ export class DashboardContainerFactory extends EmbeddableFactory< initialInput: DashboardContainerInput, parent?: Container ): Promise { - return new DashboardContainer(initialInput, this.options, parent); + const services = await this.getStartServices(); + return new DashboardContainer(initialInput, services, parent); } } diff --git a/src/plugins/dashboard/public/embeddable/grid/dashboard_grid.test.tsx b/src/plugins/dashboard/public/embeddable/grid/dashboard_grid.test.tsx index c1a3d88979f49..0f1b9c6dc9307 100644 --- a/src/plugins/dashboard/public/embeddable/grid/dashboard_grid.test.tsx +++ b/src/plugins/dashboard/public/embeddable/grid/dashboard_grid.test.tsx @@ -23,7 +23,7 @@ import sizeMe from 'react-sizeme'; import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { skip } from 'rxjs/operators'; -import { EmbeddableFactory, GetEmbeddableFactory } from '../../embeddable_plugin'; +import { EmbeddableFactory } from '../../embeddable_plugin'; import { DashboardGrid, DashboardGridProps } from './dashboard_grid'; import { DashboardContainer, DashboardContainerOptions } from '../dashboard_container'; import { getSampleDashboardInput } from '../../test_helpers'; @@ -41,7 +41,7 @@ function prepare(props?: Partial) { CONTACT_CARD_EMBEDDABLE, new ContactCardEmbeddableFactory({} as any, (() => {}) as any, {} as any) ); - const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); + const getEmbeddableFactory = (id: string) => embeddableFactories.get(id); const initialInput = getSampleDashboardInput({ panels: { '1': { diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 6f78829af19f1..8a6e747aac170 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -23,7 +23,7 @@ import * as React from 'react'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { SharePluginSetup } from 'src/plugins/share/public'; import { UiActionsSetup, UiActionsStart } from '../../../plugins/ui_actions/public'; -import { CONTEXT_MENU_TRIGGER, IEmbeddableSetup, IEmbeddableStart } from './embeddable_plugin'; +import { CONTEXT_MENU_TRIGGER, EmbeddableSetup, EmbeddableStart } from './embeddable_plugin'; import { ExpandPanelAction, ReplacePanelAction } from '.'; import { DashboardContainerFactory } from './embeddable/dashboard_container_factory'; import { Start as InspectorStartContract } from '../../../plugins/inspector/public'; @@ -47,13 +47,13 @@ declare module '../../share/public' { } interface SetupDependencies { - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; uiActions: UiActionsSetup; share?: SharePluginSetup; } interface StartDependencies { - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; inspector: InspectorStartContract; uiActions: UiActionsStart; } @@ -72,7 +72,10 @@ export class DashboardEmbeddableContainerPublicPlugin implements Plugin { constructor(initializerContext: PluginInitializerContext) {} - public setup(core: CoreSetup, { share, uiActions }: SetupDependencies): Setup { + public setup( + core: CoreSetup, + { share, uiActions, embeddable }: SetupDependencies + ): Setup { const expandPanelAction = new ExpandPanelAction(); uiActions.registerAction(expandPanelAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, expandPanelAction); @@ -86,26 +89,44 @@ export class DashboardEmbeddableContainerPublicPlugin })) ); } + + const getStartServices = async () => { + const [coreStart, deps] = await core.getStartServices(); + + const useHideChrome = () => { + React.useEffect(() => { + coreStart.chrome.setIsVisible(false); + return () => coreStart.chrome.setIsVisible(true); + }, []); + }; + + const ExitFullScreenButton: React.FC = props => { + useHideChrome(); + return ; + }; + return { + capabilities: coreStart.application.capabilities, + application: coreStart.application, + notifications: coreStart.notifications, + overlays: coreStart.overlays, + embeddable: deps.embeddable, + inspector: deps.inspector, + SavedObjectFinder: getSavedObjectFinder(coreStart.savedObjects, coreStart.uiSettings), + ExitFullScreenButton, + uiActions: deps.uiActions, + }; + }; + + const factory = new DashboardContainerFactory(getStartServices); + embeddable.registerEmbeddableFactory(factory.type, factory); } public start(core: CoreStart, plugins: StartDependencies): Start { - const { application, notifications, overlays } = core; - const { embeddable, inspector, uiActions } = plugins; + const { notifications } = core; + const { uiActions } = plugins; const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, core.uiSettings); - const useHideChrome = () => { - React.useEffect(() => { - core.chrome.setIsVisible(false); - return () => core.chrome.setIsVisible(true); - }, []); - }; - - const ExitFullScreenButton: React.FC = props => { - useHideChrome(); - return ; - }; - const changeViewAction = new ReplacePanelAction( core, SavedObjectFinder, @@ -114,19 +135,6 @@ export class DashboardEmbeddableContainerPublicPlugin ); uiActions.registerAction(changeViewAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, changeViewAction); - - const factory = new DashboardContainerFactory({ - application, - notifications, - overlays, - embeddable, - inspector, - SavedObjectFinder, - ExitFullScreenButton, - uiActions, - }); - - embeddable.registerEmbeddableFactory(factory.type, factory); } public stop() {} diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts index b6ca91169a933..305aa8575e4d7 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts @@ -18,6 +18,8 @@ */ import { defaults, pluck, last, get } from 'lodash'; + +jest.mock('../../../../kibana_utils/public/history'); import { IndexPattern } from './index_pattern'; import { DuplicateField } from '../../../../kibana_utils/public'; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 333b13eedc17a..783411bbf27e2 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -38,6 +38,7 @@ import { Observable } from 'rxjs'; import { Plugin as Plugin_2 } from 'src/core/public'; import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/public'; import { PopoverAnchorPosition } from '@elastic/eui'; +import { PublicUiSettingsParams } from 'src/core/server/types'; import React from 'react'; import * as React_2 from 'react'; import { Required } from '@kbn/utility-types'; @@ -49,7 +50,6 @@ import { SearchResponse as SearchResponse_2 } from 'elasticsearch'; import { SimpleSavedObject } from 'src/core/public'; import { UiActionsSetup } from 'src/plugins/ui_actions/public'; import { UiActionsStart } from 'src/plugins/ui_actions/public'; -import { UiSettingsParams } from 'src/core/server/types'; import { Unit } from '@elastic/datemath'; import { UnregisterCallback } from 'history'; import { UserProvidedValues } from 'src/core/server/types'; diff --git a/src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts b/src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts index 5d6c25b0d96c1..da8f5b3564948 100644 --- a/src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts +++ b/src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts @@ -48,6 +48,22 @@ describe('filter manager utilities', () => { expect(compareFilters(f1, f2)).toBeTruthy(); }); + test('should compare filters, where one filter is null', () => { + const f1 = buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ); + const f2 = null; + expect(compareFilters(f1, f2 as any)).toBeFalsy(); + }); + + test('should compare a null filter with an empty filter', () => { + const f1 = null; + const f2 = buildEmptyFilter(true); + expect(compareFilters(f1 as any, f2)).toBeFalsy(); + }); + test('should compare duplicates, ignoring meta attributes', () => { const f1 = buildQueryFilter( { _type: { match: { query: 'apache', type: 'phrase' } } }, diff --git a/src/plugins/data/public/query/filter_manager/lib/compare_filters.ts b/src/plugins/data/public/query/filter_manager/lib/compare_filters.ts index b4402885bc0be..a2105fdc1d3ef 100644 --- a/src/plugins/data/public/query/filter_manager/lib/compare_filters.ts +++ b/src/plugins/data/public/query/filter_manager/lib/compare_filters.ts @@ -74,6 +74,8 @@ export const compareFilters = ( second: Filter | Filter[], comparatorOptions: FilterCompareOptions = {} ) => { + if (!first || !second) return false; + let comparators: FilterCompareOptions = {}; const excludedAttributes: string[] = ['$$hashKey', 'meta']; diff --git a/src/plugins/data/server/autocomplete/value_suggestions_route.ts b/src/plugins/data/server/autocomplete/value_suggestions_route.ts index 03dbd40984412..b7569a22e9fc9 100644 --- a/src/plugins/data/server/autocomplete/value_suggestions_route.ts +++ b/src/plugins/data/server/autocomplete/value_suggestions_route.ts @@ -39,7 +39,7 @@ export function registerValueSuggestionsRoute( { index: schema.string(), }, - { allowUnknowns: false } + { unknowns: 'allow' } ), body: schema.object( { @@ -47,7 +47,7 @@ export function registerValueSuggestionsRoute( query: schema.string(), boolFilter: schema.maybe(schema.any()), }, - { allowUnknowns: false } + { unknowns: 'allow' } ), }, }, diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 0165486fc2de7..5038b4226fad8 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -166,7 +166,7 @@ export { ParsedInterval } from '../common'; export { ISearch, - ICancel, + ISearchCancel, ISearchOptions, IRequestTypesMap, IResponseTypesMap, diff --git a/src/plugins/data/server/index_patterns/index_patterns_service.ts b/src/plugins/data/server/index_patterns/index_patterns_service.ts index 78f34e21b9e41..58e8fbae9f9e2 100644 --- a/src/plugins/data/server/index_patterns/index_patterns_service.ts +++ b/src/plugins/data/server/index_patterns/index_patterns_service.ts @@ -19,10 +19,13 @@ import { CoreSetup, Plugin } from 'kibana/server'; import { registerRoutes } from './routes'; +import { indexPatternSavedObjectType } from '../saved_objects'; export class IndexPatternsService implements Plugin { - public setup({ http }: CoreSetup) { - registerRoutes(http); + public setup(core: CoreSetup) { + core.savedObjects.registerType(indexPatternSavedObjectType); + + registerRoutes(core.http); } public start() {} diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index 616e65ad872ab..efb8759e7bead 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -21,6 +21,7 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../.. import { IndexPatternsService } from './index_patterns'; import { ISearchSetup } from './search'; import { SearchService } from './search/search_service'; +import { QueryService } from './query/query_service'; import { ScriptsService } from './scripts'; import { KqlTelemetryService } from './kql_telemetry'; import { UsageCollectionSetup } from '../../usage_collection/server'; @@ -47,6 +48,7 @@ export class DataServerPlugin implements Plugin { - const deps: EmbeddableDependencies = { - embeddableFactories: new Map(), - }; - return deps; -}; +export { QueryService } from './query_service'; diff --git a/src/plugins/data/server/query/query_service.ts b/src/plugins/data/server/query/query_service.ts new file mode 100644 index 0000000000000..13d735a495d42 --- /dev/null +++ b/src/plugins/data/server/query/query_service.ts @@ -0,0 +1,29 @@ +/* + * 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 { CoreSetup, Plugin } from 'kibana/server'; +import { querySavedObjectType } from '../saved_objects'; + +export class QueryService implements Plugin { + public setup(core: CoreSetup) { + core.savedObjects.registerType(querySavedObjectType); + } + + public start() {} +} diff --git a/src/plugins/data/server/saved_objects/index.ts b/src/plugins/data/server/saved_objects/index.ts new file mode 100644 index 0000000000000..5d980974474de --- /dev/null +++ b/src/plugins/data/server/saved_objects/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { searchSavedObjectType } from './search'; +export { querySavedObjectType } from './query'; +export { indexPatternSavedObjectType } from './index_patterns'; diff --git a/src/plugins/data/server/saved_objects/index_pattern_migrations.test.ts b/src/plugins/data/server/saved_objects/index_pattern_migrations.test.ts new file mode 100644 index 0000000000000..b1410e2498667 --- /dev/null +++ b/src/plugins/data/server/saved_objects/index_pattern_migrations.test.ts @@ -0,0 +1,97 @@ +/* + * 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 { SavedObjectMigrationContext } from 'kibana/server'; +import { indexPatternSavedObjectTypeMigrations } from './index_pattern_migrations'; + +const savedObjectMigrationContext = (null as unknown) as SavedObjectMigrationContext; + +describe('migration index-pattern', () => { + describe('6.5.0', () => { + const migrationFn = indexPatternSavedObjectTypeMigrations['6.5.0']; + + test('adds "type" and "typeMeta" properties to object when not declared', () => { + expect( + migrationFn( + { + type: 'index-pattern', + attributes: {}, + }, + savedObjectMigrationContext + ) + ).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "type": undefined, + "typeMeta": undefined, + }, + "type": "index-pattern", +} +`); + }); + + test('keeps "type" and "typeMeta" properties as is when declared', () => { + expect( + migrationFn( + { + type: 'index-pattern', + attributes: { + type: '123', + typeMeta: '123', + }, + }, + savedObjectMigrationContext + ) + ).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "type": "123", + "typeMeta": "123", + }, + "type": "index-pattern", +} +`); + }); + }); + + describe('7.6.0', () => { + const migrationFn = indexPatternSavedObjectTypeMigrations['7.6.0']; + + test('should remove the parent property and update the subType prop on every field that has them', () => { + const input = { + type: 'index-pattern', + attributes: { + title: 'test', + fields: + '[{"name":"customer_name","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"customer_name.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":"multi","parent":"customer_name"}]', + }, + }; + const expected = { + type: 'index-pattern', + attributes: { + title: 'test', + fields: + '[{"name":"customer_name","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"customer_name.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"customer_name"}}}]', + }, + }; + + expect(migrationFn(input, savedObjectMigrationContext)).toEqual(expected); + }); + }); +}); diff --git a/src/plugins/data/server/saved_objects/index_pattern_migrations.ts b/src/plugins/data/server/saved_objects/index_pattern_migrations.ts new file mode 100644 index 0000000000000..7a16386ea484c --- /dev/null +++ b/src/plugins/data/server/saved_objects/index_pattern_migrations.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { flow, omit } from 'lodash'; +import { SavedObjectMigrationFn } from 'kibana/server'; + +const migrateAttributeTypeAndAttributeTypeMeta: SavedObjectMigrationFn = doc => ({ + ...doc, + attributes: { + ...doc.attributes, + type: doc.attributes.type || undefined, + typeMeta: doc.attributes.typeMeta || undefined, + }, +}); + +const migrateSubTypeAndParentFieldProperties: SavedObjectMigrationFn = doc => { + if (!doc.attributes.fields) return doc; + + const fieldsString = doc.attributes.fields; + const fields = JSON.parse(fieldsString) as any[]; + const migratedFields = fields.map(field => { + if (field.subType === 'multi') { + return { + ...omit(field, 'parent'), + subType: { multi: { parent: field.parent } }, + }; + } + + return field; + }); + + return { + ...doc, + attributes: { + ...doc.attributes, + fields: JSON.stringify(migratedFields), + }, + }; +}; + +export const indexPatternSavedObjectTypeMigrations = { + '6.5.0': flow(migrateAttributeTypeAndAttributeTypeMeta), + '7.6.0': flow(migrateSubTypeAndParentFieldProperties), +}; diff --git a/src/plugins/data/server/saved_objects/index_patterns.ts b/src/plugins/data/server/saved_objects/index_patterns.ts new file mode 100644 index 0000000000000..9838071eee5a4 --- /dev/null +++ b/src/plugins/data/server/saved_objects/index_patterns.ts @@ -0,0 +1,58 @@ +/* + * 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 { SavedObjectsType } from 'kibana/server'; +import { indexPatternSavedObjectTypeMigrations } from './index_pattern_migrations'; + +export const indexPatternSavedObjectType: SavedObjectsType = { + name: 'index-pattern', + hidden: false, + namespaceAgnostic: false, + management: { + icon: 'indexPatternApp', + defaultSearchField: 'title', + importableAndExportable: true, + getTitle(obj) { + return obj.attributes.title; + }, + getEditUrl(obj) { + return `/management/kibana/index_patterns/${encodeURIComponent(obj.id)}`; + }, + getInAppUrl(obj) { + return { + path: `/app/kibana#/management/kibana/index_patterns/${encodeURIComponent(obj.id)}`, + uiCapabilitiesPath: 'management.kibana.index_patterns', + }; + }, + }, + mappings: { + properties: { + fieldFormatMap: { type: 'text' }, + fields: { type: 'text' }, + intervalName: { type: 'keyword' }, + notExpandable: { type: 'boolean' }, + sourceFilters: { type: 'text' }, + timeFieldName: { type: 'keyword' }, + title: { type: 'text' }, + type: { type: 'keyword' }, + typeMeta: { type: 'keyword' }, + }, + }, + migrations: indexPatternSavedObjectTypeMigrations, +}; diff --git a/src/legacy/core_plugins/data/mappings.ts b/src/plugins/data/server/saved_objects/query.ts similarity index 50% rename from src/legacy/core_plugins/data/mappings.ts rename to src/plugins/data/server/saved_objects/query.ts index 90777ec8e3651..ff0a6cfde8113 100644 --- a/src/legacy/core_plugins/data/mappings.ts +++ b/src/plugins/data/server/saved_objects/query.ts @@ -17,34 +17,36 @@ * under the License. */ -export const mappings = { - query: { +import { SavedObjectsType } from 'kibana/server'; + +export const querySavedObjectType: SavedObjectsType = { + name: 'query', + hidden: false, + namespaceAgnostic: false, + management: { + icon: 'search', + defaultSearchField: 'title', + importableAndExportable: true, + getTitle(obj) { + return obj.attributes.title; + }, + getInAppUrl(obj) { + return { + path: `/app/kibana#/discover?_a=(savedQuery:'${encodeURIComponent(obj.id)}')`, + uiCapabilitiesPath: 'discover.show', + }; + }, + }, + mappings: { properties: { - title: { - type: 'text', - }, - description: { - type: 'text', - }, + title: { type: 'text' }, + description: { type: 'text' }, query: { - properties: { - language: { - type: 'keyword', - }, - query: { - type: 'keyword', - index: false, - }, - }, - }, - filters: { - type: 'object', - enabled: false, - }, - timefilter: { - type: 'object', - enabled: false, + properties: { language: { type: 'keyword' }, query: { type: 'keyword', index: false } }, }, + filters: { type: 'object', enabled: false }, + timefilter: { type: 'object', enabled: false }, }, }, + migrations: {}, }; diff --git a/src/plugins/data/server/saved_objects/search.ts b/src/plugins/data/server/saved_objects/search.ts new file mode 100644 index 0000000000000..8b30ff7d08201 --- /dev/null +++ b/src/plugins/data/server/saved_objects/search.ts @@ -0,0 +1,60 @@ +/* + * 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 { SavedObjectsType } from 'kibana/server'; +import { searchSavedObjectTypeMigrations } from './search_migrations'; + +export const searchSavedObjectType: SavedObjectsType = { + name: 'search', + hidden: false, + namespaceAgnostic: false, + management: { + icon: 'discoverApp', + defaultSearchField: 'title', + importableAndExportable: true, + getTitle(obj) { + return obj.attributes.title; + }, + getEditUrl(obj) { + return `/management/kibana/objects/savedSearches/${encodeURIComponent(obj.id)}`; + }, + getInAppUrl(obj) { + return { + path: `/app/kibana#/discover/${encodeURIComponent(obj.id)}`, + uiCapabilitiesPath: 'discover.show', + }; + }, + }, + mappings: { + properties: { + columns: { type: 'keyword' }, + description: { type: 'text' }, + hits: { type: 'integer' }, + kibanaSavedObjectMeta: { + properties: { + searchSourceJSON: { type: 'text' }, + }, + }, + sort: { type: 'keyword' }, + title: { type: 'text' }, + version: { type: 'integer' }, + }, + }, + migrations: searchSavedObjectTypeMigrations, +}; diff --git a/src/plugins/data/server/saved_objects/search_migrations.test.ts b/src/plugins/data/server/saved_objects/search_migrations.test.ts new file mode 100644 index 0000000000000..7fdf2e14aefed --- /dev/null +++ b/src/plugins/data/server/saved_objects/search_migrations.test.ts @@ -0,0 +1,299 @@ +/* + * 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 { SavedObjectMigrationContext } from 'kibana/server'; +import { searchSavedObjectTypeMigrations } from './search_migrations'; + +const savedObjectMigrationContext = (null as unknown) as SavedObjectMigrationContext; + +describe('migration search', () => { + describe('7.0.0', () => { + const migrationFn = searchSavedObjectTypeMigrations['7.0.0']; + + test('skips errors when searchSourceJSON is null', () => { + const doc = { + id: '123', + type: 'search', + attributes: { + foo: true, + kibanaSavedObjectMeta: { + searchSourceJSON: null, + }, + }, + }; + const migratedDoc = migrationFn(doc, savedObjectMigrationContext); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "foo": true, + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": null, + }, + }, + "id": "123", + "references": Array [], + "type": "search", +} +`); + }); + + test('skips errors when searchSourceJSON is undefined', () => { + const doc = { + id: '123', + type: 'search', + attributes: { + foo: true, + kibanaSavedObjectMeta: { + searchSourceJSON: undefined, + }, + }, + }; + const migratedDoc = migrationFn(doc, savedObjectMigrationContext); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "foo": true, + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": undefined, + }, + }, + "id": "123", + "references": Array [], + "type": "search", +} +`); + }); + + test('skips error when searchSourceJSON is not a string', () => { + const doc = { + id: '123', + type: 'search', + attributes: { + foo: true, + kibanaSavedObjectMeta: { + searchSourceJSON: 123, + }, + }, + }; + const migratedDoc = migrationFn(doc, savedObjectMigrationContext); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "foo": true, + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": 123, + }, + }, + "id": "123", + "references": Array [], + "type": "search", +} +`); + }); + + test('skips error when searchSourceJSON is invalid json', () => { + const doc = { + id: '123', + type: 'search', + attributes: { + foo: true, + kibanaSavedObjectMeta: { + searchSourceJSON: '{abc123}', + }, + }, + }; + const migratedDoc = migrationFn(doc, savedObjectMigrationContext); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "foo": true, + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{abc123}", + }, + }, + "id": "123", + "references": Array [], + "type": "search", +} +`); + }); + + test('skips error when "index" and "filter" is missing from searchSourceJSON', () => { + const doc = { + id: '123', + type: 'search', + attributes: { + foo: true, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ bar: true }), + }, + }, + }; + const migratedDoc = migrationFn(doc, savedObjectMigrationContext); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "foo": true, + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"bar\\":true}", + }, + }, + "id": "123", + "references": Array [], + "type": "search", +} +`); + }); + + test('extracts "index" attribute from doc', () => { + const doc = { + id: '123', + type: 'search', + attributes: { + foo: true, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ bar: true, index: 'pattern*' }), + }, + }, + }; + const migratedDoc = migrationFn(doc, savedObjectMigrationContext); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "foo": true, + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"bar\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}", + }, + }, + "id": "123", + "references": Array [ + Object { + "id": "pattern*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern", + }, + ], + "type": "search", +} +`); + }); + + test('extracts index patterns from filter', () => { + const doc = { + id: '123', + type: 'search', + attributes: { + foo: true, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + bar: true, + filter: [ + { + meta: { + foo: true, + index: 'my-index', + }, + }, + ], + }), + }, + }, + }; + const migratedDoc = migrationFn(doc, savedObjectMigrationContext); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "foo": true, + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"bar\\":true,\\"filter\\":[{\\"meta\\":{\\"foo\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}", + }, + }, + "id": "123", + "references": Array [ + Object { + "id": "my-index", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern", + }, + ], + "type": "search", +} +`); + }); + }); + + describe('7.4.0', function() { + const migrationFn = searchSavedObjectTypeMigrations['7.4.0']; + + test('transforms one dimensional sort arrays into two dimensional arrays', () => { + const doc = { + id: '123', + type: 'search', + attributes: { + sort: ['bytes', 'desc'], + }, + }; + + const expected = { + id: '123', + type: 'search', + attributes: { + sort: [['bytes', 'desc']], + }, + }; + + const migratedDoc = migrationFn(doc, savedObjectMigrationContext); + + expect(migratedDoc).toEqual(expected); + }); + + test("doesn't modify search docs that already have two dimensional sort arrays", () => { + const doc = { + id: '123', + type: 'search', + attributes: { + sort: [['bytes', 'desc']], + }, + }; + + const migratedDoc = migrationFn(doc, savedObjectMigrationContext); + + expect(migratedDoc).toEqual(doc); + }); + + test("doesn't modify search docs that have no sort array", () => { + const doc = { + id: '123', + type: 'search', + attributes: {}, + }; + + const migratedDoc = migrationFn(doc, savedObjectMigrationContext); + + expect(migratedDoc).toEqual(doc); + }); + }); +}); diff --git a/src/plugins/data/server/saved_objects/search_migrations.ts b/src/plugins/data/server/saved_objects/search_migrations.ts new file mode 100644 index 0000000000000..db545e52ce170 --- /dev/null +++ b/src/plugins/data/server/saved_objects/search_migrations.ts @@ -0,0 +1,92 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { flow, get } from 'lodash'; +import { SavedObjectMigrationFn } from 'kibana/server'; + +const migrateIndexPattern: SavedObjectMigrationFn = doc => { + const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); + if (typeof searchSourceJSON !== 'string') { + return doc; + } + let searchSource; + try { + searchSource = JSON.parse(searchSourceJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + return doc; + } + + if (searchSource.index && Array.isArray(doc.references)) { + searchSource.indexRefName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; + doc.references.push({ + name: searchSource.indexRefName, + type: 'index-pattern', + id: searchSource.index, + }); + delete searchSource.index; + } + if (searchSource.filter) { + searchSource.filter.forEach((filterRow: any, i: number) => { + if (!filterRow.meta || !filterRow.meta.index || !Array.isArray(doc.references)) { + return; + } + filterRow.meta.indexRefName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; + doc.references.push({ + name: filterRow.meta.indexRefName, + type: 'index-pattern', + id: filterRow.meta.index, + }); + delete filterRow.meta.index; + }); + } + + doc.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify(searchSource); + + return doc; +}; + +const setNewReferences: SavedObjectMigrationFn = (doc, context) => { + doc.references = doc.references || []; + // Migrate index pattern + return migrateIndexPattern(doc, context); +}; + +const migrateSearchSortToNestedArray: SavedObjectMigrationFn = doc => { + const sort = get(doc, 'attributes.sort'); + if (!sort) return doc; + + // Don't do anything if we already have a two dimensional array + if (Array.isArray(sort) && sort.length > 0 && Array.isArray(sort[0])) { + return doc; + } + + return { + ...doc, + attributes: { + ...doc.attributes, + sort: [doc.attributes.sort], + }, + }; +}; + +export const searchSavedObjectTypeMigrations = { + '7.0.0': flow(setNewReferences), + '7.4.0': flow(migrateSearchSortToNestedArray), +}; diff --git a/src/plugins/data/server/search/i_route_handler_search_context.ts b/src/plugins/data/server/search/i_route_handler_search_context.ts index 89862781b826e..9888c774ea104 100644 --- a/src/plugins/data/server/search/i_route_handler_search_context.ts +++ b/src/plugins/data/server/search/i_route_handler_search_context.ts @@ -17,9 +17,9 @@ * under the License. */ -import { ISearchGeneric, ICancelGeneric } from './i_search'; +import { ISearchGeneric, ISearchCancelGeneric } from './i_search'; export interface IRouteHandlerSearchContext { search: ISearchGeneric; - cancel: ICancelGeneric; + cancel: ISearchCancelGeneric; } diff --git a/src/plugins/data/server/search/i_search.ts b/src/plugins/data/server/search/i_search.ts index ea014c5e136d9..fa4aa72ac7287 100644 --- a/src/plugins/data/server/search/i_search.ts +++ b/src/plugins/data/server/search/i_search.ts @@ -42,7 +42,7 @@ export type ISearchGeneric = Promise; -export type ICancelGeneric = ( +export type ISearchCancelGeneric = ( id: string, strategy?: T ) => Promise; @@ -52,4 +52,4 @@ export type ISearch = ( options?: ISearchOptions ) => Promise; -export type ICancel = (id: string) => Promise; +export type ISearchCancel = (id: string) => Promise; diff --git a/src/plugins/data/server/search/i_search_strategy.ts b/src/plugins/data/server/search/i_search_strategy.ts index 4cfc9608383a9..9b405034f883f 100644 --- a/src/plugins/data/server/search/i_search_strategy.ts +++ b/src/plugins/data/server/search/i_search_strategy.ts @@ -18,7 +18,7 @@ */ import { APICaller } from 'kibana/server'; -import { ISearch, ICancel, ISearchGeneric } from './i_search'; +import { ISearch, ISearchCancel, ISearchGeneric } from './i_search'; import { TStrategyTypes } from './strategy_types'; import { ISearchContext } from './i_search_context'; @@ -28,7 +28,7 @@ import { ISearchContext } from './i_search_context'; */ export interface ISearchStrategy { search: ISearch; - cancel?: ICancel; + cancel?: ISearchCancel; } /** diff --git a/src/plugins/data/server/search/index.ts b/src/plugins/data/server/search/index.ts index 385e96ee803b6..15738a3befb27 100644 --- a/src/plugins/data/server/search/index.ts +++ b/src/plugins/data/server/search/index.ts @@ -21,7 +21,13 @@ export { ISearchSetup } from './i_search_setup'; export { ISearchContext } from './i_search_context'; -export { ISearch, ICancel, ISearchOptions, IRequestTypesMap, IResponseTypesMap } from './i_search'; +export { + ISearch, + ISearchCancel, + ISearchOptions, + IRequestTypesMap, + IResponseTypesMap, +} from './i_search'; export { TStrategyTypes } from './strategy_types'; diff --git a/src/plugins/data/server/search/routes.ts b/src/plugins/data/server/search/routes.ts index e618f99084aed..b90d7d4ff80ce 100644 --- a/src/plugins/data/server/search/routes.ts +++ b/src/plugins/data/server/search/routes.ts @@ -28,9 +28,9 @@ export function registerSearchRoute(router: IRouter): void { validate: { params: schema.object({ strategy: schema.string() }), - query: schema.object({}, { allowUnknowns: true }), + query: schema.object({}, { unknowns: 'allow' }), - body: schema.object({}, { allowUnknowns: true }), + body: schema.object({}, { unknowns: 'allow' }), }, }, async (context, request, res) => { @@ -64,7 +64,7 @@ export function registerSearchRoute(router: IRouter): void { id: schema.string(), }), - query: schema.object({}, { allowUnknowns: true }), + query: schema.object({}, { unknowns: 'allow' }), }, }, async (context, request, res) => { diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 46f90e3c6fc62..5ee19cd3df19f 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -34,6 +34,8 @@ import { import { IRouteHandlerSearchContext } from './i_route_handler_search_context'; import { ES_SEARCH_STRATEGY, esSearchStrategyProvider } from './es_search'; +import { searchSavedObjectType } from '../saved_objects'; + declare module 'kibana/server' { interface RequestHandlerContext { search?: IRouteHandlerSearchContext; @@ -53,6 +55,8 @@ export class SearchService implements Plugin { this.contextContainer = core.context.createContextContainer(); + core.savedObjects.registerType(searchSavedObjectType); + core.http.registerRouteHandlerContext<'search'>('search', context => { return createApi({ caller: context.core.elasticsearch.dataClient.callAsCurrentUser, diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 666df2900c2c3..178b2949a9456 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -329,12 +329,6 @@ export function getDefaultSearchParams(config: SharedGlobalConfig): { restTotalHitsAsInt: boolean; }; -// Warning: (ae-forgotten-export) The symbol "TStrategyTypes" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "ICancel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type ICancel = (id: string) => Promise; - // Warning: (ae-missing-release-tag) "IFieldFormatsRegistry" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -507,11 +501,17 @@ export interface IResponseTypesMap { [ES_SEARCH_STRATEGY]: IEsSearchResponse; } +// Warning: (ae-forgotten-export) The symbol "TStrategyTypes" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ISearch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export type ISearch = (request: IRequestTypesMap[T], options?: ISearchOptions) => Promise; +// Warning: (ae-missing-release-tag) "ISearchCancel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ISearchCancel = (id: string) => Promise; + // Warning: (ae-missing-release-tag) "ISearchContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -733,7 +733,7 @@ export type TSearchStrategyProvider = (context: ISearc // src/plugins/data/server/index.ts:184:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:185:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:188:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/plugin.ts:62:14 - (ae-forgotten-export) The symbol "ISearchSetup" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/plugin.ts:64:14 - (ae-forgotten-export) The symbol "ISearchSetup" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/embeddable/public/api/index.ts b/src/plugins/embeddable/public/api/index.ts deleted file mode 100644 index aec539330de9a..0000000000000 --- a/src/plugins/embeddable/public/api/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { - EmbeddableApiPure, - EmbeddableDependencies, - EmbeddableApi, - EmbeddableDependenciesInternal, -} from './types'; -import { getEmbeddableFactories } from './get_embeddable_factories'; -import { getEmbeddableFactory } from './get_embeddable_factory'; -import { registerEmbeddableFactory } from './register_embeddable_factory'; - -export * from './types'; - -export const pureApi: EmbeddableApiPure = { - getEmbeddableFactories, - getEmbeddableFactory, - registerEmbeddableFactory, -}; - -export const createApi = (deps: EmbeddableDependencies) => { - const partialApi: Partial = {}; - const depsInternal: EmbeddableDependenciesInternal = { ...deps, api: partialApi }; - for (const [key, fn] of Object.entries(pureApi)) { - (partialApi as any)[key] = fn(depsInternal); - } - Object.freeze(partialApi); - const api = partialApi as EmbeddableApi; - return { api, depsInternal }; -}; diff --git a/src/plugins/embeddable/public/api/register_embeddable_factory.ts b/src/plugins/embeddable/public/api/register_embeddable_factory.ts deleted file mode 100644 index 8b7bcdee5911f..0000000000000 --- a/src/plugins/embeddable/public/api/register_embeddable_factory.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { EmbeddableApiPure } from './types'; - -export const registerEmbeddableFactory: EmbeddableApiPure['registerEmbeddableFactory'] = ({ - embeddableFactories, -}) => (embeddableFactoryId, factory) => { - if (embeddableFactories.has(embeddableFactoryId)) { - throw new Error( - `Embeddable factory [embeddableFactoryId = ${embeddableFactoryId}] already registered in Embeddables API.` - ); - } - - embeddableFactories.set(embeddableFactoryId, factory); -}; diff --git a/src/plugins/embeddable/public/api/types.ts b/src/plugins/embeddable/public/api/types.ts deleted file mode 100644 index 179d96a4aff8c..0000000000000 --- a/src/plugins/embeddable/public/api/types.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { EmbeddableFactoryRegistry } from '../types'; -import { EmbeddableFactory, GetEmbeddableFactories } from '../lib'; - -export interface EmbeddableApi { - getEmbeddableFactory: (embeddableFactoryId: string) => EmbeddableFactory; - getEmbeddableFactories: GetEmbeddableFactories; - // TODO: Make `registerEmbeddableFactory` receive only `factory` argument. - registerEmbeddableFactory: ( - id: string, - factory: TEmbeddableFactory - ) => void; -} - -export interface EmbeddableDependencies { - embeddableFactories: EmbeddableFactoryRegistry; -} - -export interface EmbeddableDependenciesInternal extends EmbeddableDependencies { - api: Readonly>; -} - -export type EmbeddableApiPure = { - [K in keyof EmbeddableApi]: (deps: EmbeddableDependenciesInternal) => EmbeddableApi[K]; -}; diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 1474f9ed63052..eca74af4ec253 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -48,8 +48,6 @@ export { EmbeddableRoot, EmbeddableVisTriggerContext, ErrorEmbeddable, - GetEmbeddableFactories, - GetEmbeddableFactory, IContainer, IEmbeddable, isErrorEmbeddable, @@ -68,4 +66,4 @@ export function plugin(initializerContext: PluginInitializerContext) { return new EmbeddablePublicPlugin(initializerContext); } -export { IEmbeddableSetup, IEmbeddableStart } from './plugin'; +export { EmbeddableSetup, EmbeddableStart } from './plugin'; diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx index 142a237a6a311..9aeaf34f3311b 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx @@ -19,11 +19,13 @@ import { EditPanelAction } from './edit_panel_action'; import { EmbeddableFactory, Embeddable, EmbeddableInput } from '../embeddables'; -import { GetEmbeddableFactory, ViewMode } from '../types'; +import { ViewMode } from '../types'; import { ContactCardEmbeddable } from '../test_samples'; +import { EmbeddableStart } from '../../plugin'; const embeddableFactories = new Map(); -const getFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); +const getFactory = ((id: string) => + embeddableFactories.get(id)) as EmbeddableStart['getEmbeddableFactory']; class EditableEmbeddable extends Embeddable { public readonly type = 'EDITABLE_EMBEDDABLE'; @@ -82,7 +84,8 @@ test('is not compatible when edit url is not available', async () => { test('is not visible when edit url is available but in view mode', async () => { embeddableFactories.clear(); - const action = new EditPanelAction(type => embeddableFactories.get(type)); + const action = new EditPanelAction((type => + embeddableFactories.get(type)) as EmbeddableStart['getEmbeddableFactory']); expect( await action.isCompatible({ embeddable: new EditableEmbeddable( @@ -98,7 +101,8 @@ test('is not visible when edit url is available but in view mode', async () => { test('is not compatible when edit url is available, in edit mode, but not editable', async () => { embeddableFactories.clear(); - const action = new EditPanelAction(type => embeddableFactories.get(type)); + const action = new EditPanelAction((type => + embeddableFactories.get(type)) as EmbeddableStart['getEmbeddableFactory']); expect( await action.isCompatible({ embeddable: new EditableEmbeddable( diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts index 82f8e33b7ae2f..9125dc0813f98 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts @@ -19,9 +19,10 @@ import { i18n } from '@kbn/i18n'; import { Action } from 'src/plugins/ui_actions/public'; -import { GetEmbeddableFactory, ViewMode } from '../types'; +import { ViewMode } from '../types'; import { EmbeddableFactoryNotFoundError } from '../errors'; import { IEmbeddable } from '../embeddables'; +import { EmbeddableStart } from '../../plugin'; export const ACTION_EDIT_PANEL = 'editPanel'; @@ -34,7 +35,7 @@ export class EditPanelAction implements Action { public readonly id = ACTION_EDIT_PANEL; public order = 15; - constructor(private readonly getEmbeddableFactory: GetEmbeddableFactory) {} + constructor(private readonly getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']) {} public getDisplayName({ embeddable }: ActionContext) { const factory = this.getEmbeddableFactory(embeddable.type); diff --git a/src/plugins/embeddable/public/lib/containers/container.ts b/src/plugins/embeddable/public/lib/containers/container.ts index 71e7cca3552bb..5ce79537ccaf3 100644 --- a/src/plugins/embeddable/public/lib/containers/container.ts +++ b/src/plugins/embeddable/public/lib/containers/container.ts @@ -29,7 +29,7 @@ import { } from '../embeddables'; import { IContainer, ContainerInput, ContainerOutput, PanelState } from './i_container'; import { PanelNotFoundError, EmbeddableFactoryNotFoundError } from '../errors'; -import { GetEmbeddableFactory } from '../types'; +import { EmbeddableStart } from '../../plugin'; const getKeys = (o: T): Array => Object.keys(o) as Array; @@ -49,7 +49,7 @@ export abstract class Container< constructor( input: TContainerInput, output: TContainerOutput, - protected readonly getFactory: GetEmbeddableFactory, + protected readonly getFactory: EmbeddableStart['getEmbeddableFactory'], parent?: Container ) { super(input, output, parent); diff --git a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx index 3c9e6e31220b2..07915ce59e6ca 100644 --- a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx +++ b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx @@ -20,7 +20,6 @@ import React from 'react'; import { nextTick } from 'test_utils/enzyme_helpers'; import { EmbeddableChildPanel } from './embeddable_child_panel'; -import { GetEmbeddableFactory } from '../types'; import { EmbeddableFactory } from '../embeddables'; import { CONTACT_CARD_EMBEDDABLE } from '../test_samples/embeddables/contact_card/contact_card_embeddable_factory'; import { SlowContactCardEmbeddableFactory } from '../test_samples/embeddables/contact_card/slow_contact_card_embeddable_factory'; @@ -42,7 +41,7 @@ test('EmbeddableChildPanel renders an embeddable when it is done loading', async CONTACT_CARD_EMBEDDABLE, new SlowContactCardEmbeddableFactory({ execAction: (() => null) as any }) ); - const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); + const getEmbeddableFactory = (id: string) => embeddableFactories.get(id); const container = new HelloWorldContainer({ id: 'hello', panels: {} }, { getEmbeddableFactory, @@ -88,7 +87,7 @@ test('EmbeddableChildPanel renders an embeddable when it is done loading', async test(`EmbeddableChildPanel renders an error message if the factory doesn't exist`, async () => { const inspector = inspectorPluginMock.createStartContract(); - const getEmbeddableFactory: GetEmbeddableFactory = () => undefined; + const getEmbeddableFactory = () => undefined; const container = new HelloWorldContainer( { id: 'hello', diff --git a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx index e15f1faaa397c..4c08a80a356bf 100644 --- a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx +++ b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx @@ -29,15 +29,15 @@ import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { ErrorEmbeddable, IEmbeddable } from '../embeddables'; import { EmbeddablePanel } from '../panel'; import { IContainer } from './i_container'; -import { GetEmbeddableFactory, GetEmbeddableFactories } from '../types'; +import { EmbeddableStart } from '../../plugin'; export interface EmbeddableChildPanelProps { embeddableId: string; className?: string; container: IContainer; getActions: UiActionsService['getTriggerCompatibleActions']; - getEmbeddableFactory: GetEmbeddableFactory; - getAllEmbeddableFactories: GetEmbeddableFactories; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; inspector: InspectorStartContract; diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx index a1b332bb65617..eb10c16806640 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx @@ -22,6 +22,7 @@ import { Adapters } from '../types'; import { IContainer } from '../containers'; import { IEmbeddable, EmbeddableInput, EmbeddableOutput } from './i_embeddable'; import { ViewMode } from '../types'; +import { TriggerContextMapping } from '../ui_actions'; import { EmbeddableActionStorage } from './embeddable_action_storage'; function getPanelTitle(input: EmbeddableInput, output: EmbeddableOutput) { @@ -195,4 +196,8 @@ export abstract class Embeddable< this.onResetInput(newInput); } + + public supportedTriggers(): Array { + return []; + } } diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts index 162da75c228aa..81f7f35c900c9 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts @@ -74,13 +74,11 @@ export abstract class EmbeddableFactory< this.savedObjectMetaData = savedObjectMetaData; } - // TODO: Can this be a property? If this "...should be based of capabilities service...", - // TODO: maybe then it should be *async*? /** * Returns whether the current user should be allowed to edit this type of - * embeddable. Most of the time this should be based off the capabilities service. + * embeddable. Most of the time this should be based off the capabilities service, hence it's async. */ - public abstract isEditable(): boolean; + public abstract async isEditable(): Promise; /** * Returns a display name for this type of embeddable. Used in "Create new... " options diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_renderer.test.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_renderer.test.tsx index 7c3a1c6ca45c4..51b83ea0ecaa3 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_renderer.test.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_renderer.test.tsx @@ -22,21 +22,21 @@ import { HelloWorldEmbeddableFactory, } from '../../../../../../examples/embeddable_examples/public'; import { EmbeddableFactory } from './embeddable_factory'; -import { GetEmbeddableFactory } from '../types'; import { EmbeddableFactoryRenderer } from './embeddable_factory_renderer'; import { mount } from 'enzyme'; import { nextTick } from 'test_utils/enzyme_helpers'; // @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; +import { EmbeddableStart } from '../../plugin'; test('EmbeddableFactoryRenderer renders an embeddable', async () => { const embeddableFactories = new Map(); embeddableFactories.set(HELLO_WORLD_EMBEDDABLE, new HelloWorldEmbeddableFactory()); - const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); + const getEmbeddableFactory = (id: string) => embeddableFactories.get(id); const component = mount( @@ -54,7 +54,7 @@ test('EmbeddableFactoryRenderer renders an embeddable', async () => { }); test('EmbeddableRoot renders an error if the type does not exist', async () => { - const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => undefined; + const getEmbeddableFactory = (id: string) => undefined; const component = mount( ; } diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx index fdff82e63faec..757d4e6bfddef 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx @@ -26,7 +26,7 @@ import { findTestSubject } from '@elastic/eui/lib/test'; import { I18nProvider } from '@kbn/i18n/react'; import { CONTEXT_MENU_TRIGGER } from '../triggers'; import { Action, UiActionsStart, ActionType } from 'src/plugins/ui_actions/public'; -import { Trigger, GetEmbeddableFactory, ViewMode } from '../types'; +import { Trigger, ViewMode } from '../types'; import { EmbeddableFactory, isErrorEmbeddable } from '../embeddables'; import { EmbeddablePanel } from './embeddable_panel'; import { createEditModeAction } from '../test_samples/actions'; @@ -47,7 +47,7 @@ import { EuiBadge } from '@elastic/eui'; const actionRegistry = new Map>(); const triggerRegistry = new Map(); const embeddableFactories = new Map(); -const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); +const getEmbeddableFactory = (id: string) => embeddableFactories.get(id); const editModeAction = createEditModeAction(); const trigger: Trigger = { diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index 28474544f40b5..b95060a73252f 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -27,7 +27,7 @@ import { toMountPoint } from '../../../../kibana_react/public'; import { Start as InspectorStartContract } from '../inspector'; import { CONTEXT_MENU_TRIGGER, PANEL_BADGE_TRIGGER, EmbeddableContext } from '../triggers'; import { IEmbeddable } from '../embeddables/i_embeddable'; -import { ViewMode, GetEmbeddableFactory, GetEmbeddableFactories } from '../types'; +import { ViewMode } from '../types'; import { RemovePanelAction } from './panel_header/panel_actions'; import { AddPanelAction } from './panel_header/panel_actions/add_panel/add_panel_action'; @@ -36,12 +36,13 @@ import { PanelHeader } from './panel_header/panel_header'; import { InspectPanelAction } from './panel_header/panel_actions/inspect_panel_action'; import { EditPanelAction } from '../actions'; import { CustomizePanelModal } from './panel_header/panel_actions/customize_title/customize_panel_modal'; +import { EmbeddableStart } from '../../plugin'; interface Props { embeddable: IEmbeddable; getActions: UiActionsService['getTriggerCompatibleActions']; - getEmbeddableFactory: GetEmbeddableFactory; - getAllEmbeddableFactories: GetEmbeddableFactories; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; inspector: InspectorStartContract; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx index 028d6a530236a..8ee8c8dad9df3 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx @@ -27,15 +27,15 @@ import { } from '../../../../test_samples/embeddables/filterable_embeddable'; import { FilterableEmbeddableFactory } from '../../../../test_samples/embeddables/filterable_embeddable_factory'; import { FilterableContainer } from '../../../../test_samples/embeddables/filterable_container'; -import { GetEmbeddableFactory } from '../../../../types'; // eslint-disable-next-line import { coreMock } from '../../../../../../../../core/public/mocks'; import { ContactCardEmbeddable } from '../../../../test_samples'; import { esFilters, Filter } from '../../../../../../../../plugins/data/public'; +import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin'; const embeddableFactories = new Map(); embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); -const getFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); +const getFactory = (id: string) => embeddableFactories.get(id); let container: FilterableContainer; let embeddable: FilterableEmbeddable; @@ -58,7 +58,7 @@ beforeEach(async () => { }; container = new FilterableContainer( { id: 'hello', panels: {}, filters: [derivedFilter] }, - getFactory + getFactory as EmbeddableStart['getEmbeddableFactory'] ); const filterableEmbeddable = await container.addNewEmbeddable< diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts index 36bb742040ccc..f3a483bb4bda4 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts @@ -19,7 +19,8 @@ import { i18n } from '@kbn/i18n'; import { Action } from 'src/plugins/ui_actions/public'; import { NotificationsStart, OverlayStart } from 'src/core/public'; -import { ViewMode, GetEmbeddableFactory, GetEmbeddableFactories } from '../../../../types'; +import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin'; +import { ViewMode } from '../../../../types'; import { openAddPanelFlyout } from './open_add_panel_flyout'; import { IContainer } from '../../../../containers'; @@ -34,8 +35,8 @@ export class AddPanelAction implements Action { public readonly id = ACTION_ADD_PANEL; constructor( - private readonly getFactory: GetEmbeddableFactory, - private readonly getAllFactories: GetEmbeddableFactories, + private readonly getFactory: EmbeddableStart['getEmbeddableFactory'], + private readonly getAllFactories: EmbeddableStart['getEmbeddableFactories'], private readonly overlays: OverlayStart, private readonly notifications: NotificationsStart, private readonly SavedObjectFinder: React.ComponentType diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx index 5f06e4ec44787..2fa21e40ca0f0 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx @@ -19,7 +19,6 @@ import * as React from 'react'; import { AddPanelFlyout } from './add_panel_flyout'; -import { GetEmbeddableFactory } from '../../../../types'; import { ContactCardEmbeddableFactory, CONTACT_CARD_EMBEDDABLE, @@ -32,6 +31,7 @@ import { ReactWrapper } from 'enzyme'; import { coreMock } from '../../../../../../../../core/public/mocks'; // @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; +import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin'; function DummySavedObjectFinder(props: { children: React.ReactNode }) { return ( @@ -55,7 +55,7 @@ test('createNewEmbeddable() add embeddable to container', async () => { firstName: 'foo', lastName: 'bar', } as any); - const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => contactCardEmbeddableFactory; + const getEmbeddableFactory = (id: string) => contactCardEmbeddableFactory; const input: ContainerInput<{ firstName: string; lastName: string }> = { id: '1', panels: {}, @@ -66,7 +66,7 @@ test('createNewEmbeddable() add embeddable to container', async () => { new Set([contactCardEmbeddableFactory]).values()} notifications={core.notifications} SavedObjectFinder={() => null} @@ -100,7 +100,8 @@ test('selecting embeddable in "Create new ..." list calls createNewEmbeddable()' firstName: 'foo', lastName: 'bar', } as any); - const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => contactCardEmbeddableFactory; + const getEmbeddableFactory = ((id: string) => + contactCardEmbeddableFactory) as EmbeddableStart['getEmbeddableFactory']; const input: ContainerInput<{ firstName: string; lastName: string }> = { id: '1', panels: {}, diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx index 815394ebd97e0..95eeb63710c32 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx @@ -29,16 +29,16 @@ import { EuiTitle, } from '@elastic/eui'; +import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin'; import { IContainer } from '../../../../containers'; import { EmbeddableFactoryNotFoundError } from '../../../../errors'; -import { GetEmbeddableFactories, GetEmbeddableFactory } from '../../../../types'; import { SavedObjectFinderCreateNew } from './saved_object_finder_create_new'; interface Props { onClose: () => void; container: IContainer; - getFactory: GetEmbeddableFactory; - getAllFactories: GetEmbeddableFactories; + getFactory: EmbeddableStart['getEmbeddableFactory']; + getAllFactories: EmbeddableStart['getEmbeddableFactories']; notifications: CoreSetup['notifications']; SavedObjectFinder: React.ComponentType; } diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx index 481693501066c..a452e07b51577 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx @@ -18,15 +18,15 @@ */ import React from 'react'; import { NotificationsStart, OverlayStart } from 'src/core/public'; +import { EmbeddableStart } from '../../../../../plugin'; import { toMountPoint } from '../../../../../../../kibana_react/public'; import { IContainer } from '../../../../containers'; import { AddPanelFlyout } from './add_panel_flyout'; -import { GetEmbeddableFactory, GetEmbeddableFactories } from '../../../../types'; export async function openAddPanelFlyout(options: { embeddable: IContainer; - getFactory: GetEmbeddableFactory; - getAllFactories: GetEmbeddableFactories; + getFactory: EmbeddableStart['getEmbeddableFactory']; + getAllFactories: EmbeddableStart['getEmbeddableFactories']; overlays: OverlayStart; notifications: NotificationsStart; SavedObjectFinder: React.ComponentType; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts index 4ba63bb025a87..3f7c917cd1617 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts @@ -32,7 +32,6 @@ import { ContactCardEmbeddableFactory, } from '../../../../test_samples/embeddables/contact_card/contact_card_embeddable_factory'; import { HelloWorldContainer } from '../../../../test_samples/embeddables/hello_world_container'; -import { GetEmbeddableFactory } from '../../../../types'; import { EmbeddableFactory } from '../../../../embeddables'; let container: Container; @@ -40,7 +39,7 @@ let embeddable: ContactCardEmbeddable; function createHelloWorldContainer(input = { id: '123', panels: {} }) { const embeddableFactories = new Map(); - const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); + const getEmbeddableFactory = (id: string) => embeddableFactories.get(id); embeddableFactories.set( CONTACT_CARD_EMBEDDABLE, new ContactCardEmbeddableFactory({}, (() => {}) as any, {} as any) diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx index 8d9beec940acc..e19acda8419da 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx @@ -34,14 +34,14 @@ import { isErrorEmbeddable, ErrorEmbeddable, } from '../../../embeddables'; -import { GetEmbeddableFactory } from '../../../types'; import { of } from '../../../../tests/helpers'; import { esFilters } from '../../../../../../../plugins/data/public'; +import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin'; const setup = async () => { const embeddableFactories = new Map(); embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); - const getFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); + const getFactory = (id: string) => embeddableFactories.get(id); const container = new FilterableContainer( { id: 'hello', @@ -54,7 +54,7 @@ const setup = async () => { }, ], }, - getFactory + getFactory as EmbeddableStart['getEmbeddableFactory'] ); const embeddable: FilterableEmbeddable | ErrorEmbeddable = await container.addNewEmbeddable< diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx index be096a4cc60ce..f4d5aa148373b 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx @@ -20,6 +20,7 @@ import { EmbeddableOutput, isErrorEmbeddable } from '../../../'; import { RemovePanelAction } from './remove_panel_action'; import { EmbeddableFactory } from '../../../embeddables'; +import { EmbeddableStart } from '../../../../plugin'; import { FILTERABLE_EMBEDDABLE, FilterableEmbeddable, @@ -27,13 +28,13 @@ import { } from '../../../test_samples/embeddables/filterable_embeddable'; import { FilterableEmbeddableFactory } from '../../../test_samples/embeddables/filterable_embeddable_factory'; import { FilterableContainer } from '../../../test_samples/embeddables/filterable_container'; -import { GetEmbeddableFactory, ViewMode } from '../../../types'; +import { ViewMode } from '../../../types'; import { ContactCardEmbeddable } from '../../../test_samples/embeddables/contact_card/contact_card_embeddable'; import { esFilters, Filter } from '../../../../../../../plugins/data/public'; const embeddableFactories = new Map(); embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); -const getFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); +const getFactory = (id: string) => embeddableFactories.get(id); let container: FilterableContainer; let embeddable: FilterableEmbeddable; @@ -46,7 +47,7 @@ beforeEach(async () => { }; container = new FilterableContainer( { id: 'hello', panels: {}, filters: [derivedFilter], viewMode: ViewMode.EDIT }, - getFactory + getFactory as EmbeddableStart['getEmbeddableFactory'] ); const filterableEmbeddable = await container.addNewEmbeddable< diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx index 7a9ba4fbbf6d6..20a5a8112f4d3 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx @@ -42,7 +42,7 @@ export class ContactCardEmbeddableFactory extends EmbeddableFactory { public readonly type = FILTERABLE_CONTAINER; constructor( - private readonly getFactory: GetEmbeddableFactory, + private readonly getFactory: EmbeddableStart['getEmbeddableFactory'], options: EmbeddableFactoryOptions = {} ) { super(options); @@ -43,7 +43,7 @@ export class FilterableContainerFactory extends EmbeddableFactory { public readonly type = FILTERABLE_EMBEDDABLE; - public isEditable() { + public async isEditable() { return true; } diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx index c5ba054bebb7a..a88c3ba086325 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx @@ -24,7 +24,7 @@ import { UiActionsService } from 'src/plugins/ui_actions/public'; import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { Container, ViewMode, ContainerInput } from '../..'; import { HelloWorldContainerComponent } from './hello_world_container_component'; -import { GetEmbeddableFactory, GetEmbeddableFactories } from '../../types'; +import { EmbeddableStart } from '../../../plugin'; export const HELLO_WORLD_CONTAINER = 'HELLO_WORLD_CONTAINER'; @@ -46,8 +46,8 @@ interface HelloWorldContainerInput extends ContainerInput { interface HelloWorldContainerOptions { getActions: UiActionsService['getTriggerCompatibleActions']; - getEmbeddableFactory: GetEmbeddableFactory; - getAllEmbeddableFactories: GetEmbeddableFactories; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; inspector: InspectorStartContract; diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx index e9acfd4539768..e8c1464edab38 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx @@ -24,13 +24,13 @@ import { CoreStart } from 'src/core/public'; import { UiActionsService } from 'src/plugins/ui_actions/public'; import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { IContainer, PanelState, EmbeddableChildPanel } from '../..'; -import { GetEmbeddableFactory, GetEmbeddableFactories } from '../../types'; +import { EmbeddableStart } from '../../../plugin'; interface Props { container: IContainer; getActions: UiActionsService['getTriggerCompatibleActions']; - getEmbeddableFactory: GetEmbeddableFactory; - getAllEmbeddableFactories: GetEmbeddableFactories; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; inspector: InspectorStartContract; diff --git a/src/plugins/embeddable/public/lib/types.ts b/src/plugins/embeddable/public/lib/types.ts index 68ea5bc17f7c9..1cfff7baca186 100644 --- a/src/plugins/embeddable/public/lib/types.ts +++ b/src/plugins/embeddable/public/lib/types.ts @@ -18,7 +18,6 @@ */ import { Adapters } from './inspector'; -import { EmbeddableFactory } from './embeddables/embeddable_factory'; export interface Trigger { id: string; @@ -40,6 +39,3 @@ export enum ViewMode { } export { Adapters }; - -export type GetEmbeddableFactory = (id: string) => EmbeddableFactory | undefined; -export type GetEmbeddableFactories = () => IterableIterator; diff --git a/src/plugins/embeddable/public/mocks.ts b/src/plugins/embeddable/public/mocks.ts index fd299bc626fb9..ba2f78e42e10e 100644 --- a/src/plugins/embeddable/public/mocks.ts +++ b/src/plugins/embeddable/public/mocks.ts @@ -17,15 +17,15 @@ * under the License. */ -import { IEmbeddableStart, IEmbeddableSetup } from '.'; +import { EmbeddableStart, EmbeddableSetup } from '.'; import { EmbeddablePublicPlugin } from './plugin'; import { coreMock } from '../../../core/public/mocks'; // eslint-disable-next-line import { uiActionsPluginMock } from '../../ui_actions/public/mocks'; -export type Setup = jest.Mocked; -export type Start = jest.Mocked; +export type Setup = jest.Mocked; +export type Start = jest.Mocked; const createSetupContract = (): Setup => { const setupContract: Setup = { @@ -36,7 +36,6 @@ const createSetupContract = (): Setup => { const createStartContract = (): Start => { const startContract: Start = { - registerEmbeddableFactory: jest.fn(), getEmbeddableFactories: jest.fn(), getEmbeddableFactory: jest.fn(), }; diff --git a/src/plugins/embeddable/public/api/tests/registry.test.ts b/src/plugins/embeddable/public/plugin.test.ts similarity index 70% rename from src/plugins/embeddable/public/api/tests/registry.test.ts rename to src/plugins/embeddable/public/plugin.test.ts index 30a8a71d243f9..c334411004e2c 100644 --- a/src/plugins/embeddable/public/api/tests/registry.test.ts +++ b/src/plugins/embeddable/public/plugin.test.ts @@ -16,18 +16,20 @@ * specific language governing permissions and limitations * under the License. */ - -import { createApi } from '..'; -import { createDeps } from './helpers'; +import { coreMock } from '../../../core/public/mocks'; +import { testPlugin } from './tests/test_plugin'; test('cannot register embeddable factory with the same ID', async () => { - const deps = createDeps(); - const { api } = createApi(deps); + const coreSetup = coreMock.createSetup(); + const coreStart = coreMock.createStart(); + const { setup } = testPlugin(coreSetup, coreStart); const embeddableFactoryId = 'ID'; const embeddableFactory = {} as any; - api.registerEmbeddableFactory(embeddableFactoryId, embeddableFactory); - expect(() => api.registerEmbeddableFactory(embeddableFactoryId, embeddableFactory)).toThrowError( + setup.registerEmbeddableFactory(embeddableFactoryId, embeddableFactory); + expect(() => + setup.registerEmbeddableFactory(embeddableFactoryId, embeddableFactory) + ).toThrowError( 'Embeddable factory [embeddableFactoryId = ID] already registered in Embeddables API.' ); }); diff --git a/src/plugins/embeddable/public/plugin.ts b/src/plugins/embeddable/public/plugin.ts index c84fb888412e1..381665c359ffd 100644 --- a/src/plugins/embeddable/public/plugin.ts +++ b/src/plugins/embeddable/public/plugin.ts @@ -16,45 +16,78 @@ * specific language governing permissions and limitations * under the License. */ - import { UiActionsSetup } from 'src/plugins/ui_actions/public'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; import { EmbeddableFactoryRegistry } from './types'; -import { createApi, EmbeddableApi } from './api'; import { bootstrap } from './bootstrap'; +import { EmbeddableFactory, EmbeddableInput, EmbeddableOutput } from './lib'; -export interface IEmbeddableSetupDependencies { +export interface EmbeddableSetupDependencies { uiActions: UiActionsSetup; } -export interface IEmbeddableSetup { - registerEmbeddableFactory: EmbeddableApi['registerEmbeddableFactory']; +export interface EmbeddableSetup { + registerEmbeddableFactory: ( + id: string, + factory: EmbeddableFactory + ) => void; +} +export interface EmbeddableStart { + getEmbeddableFactory: < + I extends EmbeddableInput = EmbeddableInput, + O extends EmbeddableOutput = EmbeddableOutput + >( + embeddableFactoryId: string + ) => EmbeddableFactory | undefined; + getEmbeddableFactories: () => IterableIterator; } -export type IEmbeddableStart = EmbeddableApi; - -export class EmbeddablePublicPlugin implements Plugin { +export class EmbeddablePublicPlugin implements Plugin { private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map(); - private api!: EmbeddableApi; constructor(initializerContext: PluginInitializerContext) {} - public setup(core: CoreSetup, { uiActions }: IEmbeddableSetupDependencies) { - ({ api: this.api } = createApi({ - embeddableFactories: this.embeddableFactories, - })); + public setup(core: CoreSetup, { uiActions }: EmbeddableSetupDependencies) { bootstrap(uiActions); - const { registerEmbeddableFactory } = this.api; - return { - registerEmbeddableFactory, + registerEmbeddableFactory: this.registerEmbeddableFactory, }; } public start(core: CoreStart) { - return this.api; + return { + getEmbeddableFactory: this.getEmbeddableFactory, + getEmbeddableFactories: () => this.embeddableFactories.values(), + }; } public stop() {} + + private registerEmbeddableFactory = (embeddableFactoryId: string, factory: EmbeddableFactory) => { + if (this.embeddableFactories.has(embeddableFactoryId)) { + throw new Error( + `Embeddable factory [embeddableFactoryId = ${embeddableFactoryId}] already registered in Embeddables API.` + ); + } + + this.embeddableFactories.set(embeddableFactoryId, factory); + }; + + private getEmbeddableFactory = < + I extends EmbeddableInput = EmbeddableInput, + O extends EmbeddableOutput = EmbeddableOutput + >( + embeddableFactoryId: string + ) => { + const factory = this.embeddableFactories.get(embeddableFactoryId); + + if (!factory) { + throw new Error( + `Embeddable factory [embeddableFactoryId = ${embeddableFactoryId}] does not exist.` + ); + } + + return factory as EmbeddableFactory; + }; } diff --git a/src/plugins/embeddable/public/tests/apply_filter_action.test.ts b/src/plugins/embeddable/public/tests/apply_filter_action.test.ts index 0721acb1a1fba..6beef35bbe136 100644 --- a/src/plugins/embeddable/public/tests/apply_filter_action.test.ts +++ b/src/plugins/embeddable/public/tests/apply_filter_action.test.ts @@ -35,14 +35,14 @@ import { inspectorPluginMock } from 'src/plugins/inspector/public/mocks'; import { esFilters } from '../../../../plugins/data/public'; test('ApplyFilterAction applies the filter to the root of the container tree', async () => { - const { doStart } = testPlugin(); + const { doStart, setup } = testPlugin(); const api = doStart(); const factory1 = new FilterableContainerFactory(api.getEmbeddableFactory); const factory2 = new FilterableEmbeddableFactory(); - api.registerEmbeddableFactory(factory1.type, factory1); - api.registerEmbeddableFactory(factory2.type, factory2); + setup.registerEmbeddableFactory(factory1.type, factory1); + setup.registerEmbeddableFactory(factory2.type, factory2); const applyFilterAction = createFilterAction(); @@ -93,7 +93,7 @@ test('ApplyFilterAction applies the filter to the root of the container tree', a }); test('ApplyFilterAction is incompatible if the root container does not accept a filter as input', async () => { - const { doStart, coreStart } = testPlugin(); + const { doStart, coreStart, setup } = testPlugin(); const api = doStart(); const inspector = inspectorPluginMock.createStartContract(); @@ -112,7 +112,7 @@ test('ApplyFilterAction is incompatible if the root container does not accept a ); const factory = new FilterableEmbeddableFactory(); - api.registerEmbeddableFactory(factory.type, factory); + setup.registerEmbeddableFactory(factory.type, factory); const embeddable = await parent.addNewEmbeddable< FilterableContainerInput, @@ -129,12 +129,12 @@ test('ApplyFilterAction is incompatible if the root container does not accept a }); test('trying to execute on incompatible context throws an error ', async () => { - const { doStart, coreStart } = testPlugin(); + const { doStart, coreStart, setup } = testPlugin(); const api = doStart(); const inspector = inspectorPluginMock.createStartContract(); const factory = new FilterableEmbeddableFactory(); - api.registerEmbeddableFactory(factory.type, factory); + setup.registerEmbeddableFactory(factory.type, factory); const applyFilterAction = createFilterAction(); const parent = new HelloWorldContainer( diff --git a/src/plugins/embeddable/public/tests/container.test.ts b/src/plugins/embeddable/public/tests/container.test.ts index be19ac206999d..1ee52f4749135 100644 --- a/src/plugins/embeddable/public/tests/container.test.ts +++ b/src/plugins/embeddable/public/tests/container.test.ts @@ -562,7 +562,7 @@ test('Panel added to input state', async () => { test('Container changes made directly after adding a new embeddable are propagated', async done => { const coreSetup = coreMock.createSetup(); const coreStart = coreMock.createStart(); - const { doStart, uiActions } = testPlugin(coreSetup, coreStart); + const { setup, doStart, uiActions } = testPlugin(coreSetup, coreStart); const start = doStart(); const container = new HelloWorldContainer( @@ -586,7 +586,7 @@ test('Container changes made directly after adding a new embeddable are propagat loadTickCount: 3, execAction: uiActions.executeTriggerActions, }); - start.registerEmbeddableFactory(factory.type, factory); + setup.registerEmbeddableFactory(factory.type, factory); const subscription = Rx.merge(container.getOutput$(), container.getInput$()) .pipe(skip(2)) @@ -755,7 +755,7 @@ test('untilEmbeddableLoaded() resolves if child is loaded in the container', asy }); test('untilEmbeddableLoaded resolves with undefined if child is subsequently removed', async done => { - const { doStart, coreStart, uiActions } = testPlugin( + const { doStart, setup, coreStart, uiActions } = testPlugin( coreMock.createSetup(), coreMock.createStart() ); @@ -764,7 +764,7 @@ test('untilEmbeddableLoaded resolves with undefined if child is subsequently rem loadTickCount: 3, execAction: uiActions.executeTriggerActions, }); - start.registerEmbeddableFactory(factory.type, factory); + setup.registerEmbeddableFactory(factory.type, factory); const container = new HelloWorldContainer( { id: 'hello', @@ -795,7 +795,7 @@ test('untilEmbeddableLoaded resolves with undefined if child is subsequently rem }); test('adding a panel then subsequently removing it before its loaded removes the panel', async done => { - const { doStart, coreStart, uiActions } = testPlugin( + const { doStart, coreStart, uiActions, setup } = testPlugin( coreMock.createSetup(), coreMock.createStart() ); @@ -804,7 +804,7 @@ test('adding a panel then subsequently removing it before its loaded removes the loadTickCount: 1, execAction: uiActions.executeTriggerActions, }); - start.registerEmbeddableFactory(factory.type, factory); + setup.registerEmbeddableFactory(factory.type, factory); const container = new HelloWorldContainer( { id: 'hello', diff --git a/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx b/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx index 70d7c99d3fb9d..99d5a7c747d15 100644 --- a/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx +++ b/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx @@ -34,16 +34,16 @@ import { HelloWorldContainer } from '../lib/test_samples/embeddables/hello_world // eslint-disable-next-line import { coreMock } from '../../../../core/public/mocks'; import { testPlugin } from './test_plugin'; -import { EmbeddableApi } from '../api'; import { CustomizePanelModal } from '../lib/panel/panel_header/panel_actions/customize_title/customize_panel_modal'; import { mount } from 'enzyme'; +import { EmbeddableStart } from '../plugin'; -let api: EmbeddableApi; +let api: EmbeddableStart; let container: Container; let embeddable: ContactCardEmbeddable; beforeEach(async () => { - const { doStart, coreStart, uiActions } = testPlugin( + const { doStart, coreStart, uiActions, setup } = testPlugin( coreMock.createSetup(), coreMock.createStart() ); @@ -54,7 +54,7 @@ beforeEach(async () => { uiActions.executeTriggerActions, {} as any ); - api.registerEmbeddableFactory(contactCardFactory.type, contactCardFactory); + setup.registerEmbeddableFactory(contactCardFactory.type, contactCardFactory); container = new HelloWorldContainer( { id: '123', panels: {} }, diff --git a/src/plugins/embeddable/public/tests/test_plugin.ts b/src/plugins/embeddable/public/tests/test_plugin.ts index 1edc332780336..e199ef193aa1c 100644 --- a/src/plugins/embeddable/public/tests/test_plugin.ts +++ b/src/plugins/embeddable/public/tests/test_plugin.ts @@ -22,14 +22,14 @@ import { CoreSetup, CoreStart } from 'src/core/public'; import { uiActionsPluginMock } from 'src/plugins/ui_actions/public/mocks'; import { UiActionsStart } from 'src/plugins/ui_actions/public'; import { coreMock } from '../../../../core/public/mocks'; -import { EmbeddablePublicPlugin, IEmbeddableSetup, IEmbeddableStart } from '../plugin'; +import { EmbeddablePublicPlugin, EmbeddableSetup, EmbeddableStart } from '../plugin'; export interface TestPluginReturn { plugin: EmbeddablePublicPlugin; coreSetup: CoreSetup; coreStart: CoreStart; - setup: IEmbeddableSetup; - doStart: (anotherCoreStart?: CoreStart) => IEmbeddableStart; + setup: EmbeddableSetup; + doStart: (anotherCoreStart?: CoreStart) => EmbeddableStart; uiActions: UiActionsStart; } diff --git a/src/plugins/kibana_react/public/index.ts b/src/plugins/kibana_react/public/index.ts index f04c6f1f19c33..e88ca7178cde3 100644 --- a/src/plugins/kibana_react/public/index.ts +++ b/src/plugins/kibana_react/public/index.ts @@ -25,6 +25,7 @@ export * from './ui_settings'; export * from './field_icon'; export * from './table_list_view'; export * from './split_panel'; +export { ValidatedDualRange } from './validated_range'; export { Markdown, MarkdownSimple } from './markdown'; export { reactToUiComponent, uiToReactComponent } from './adapters'; export { useUrlTracker } from './use_url_tracker'; diff --git a/src/legacy/ui/public/validated_range/index.js b/src/plugins/kibana_react/public/validated_range/index.ts similarity index 100% rename from src/legacy/ui/public/validated_range/index.js rename to src/plugins/kibana_react/public/validated_range/index.ts diff --git a/src/legacy/ui/public/validated_range/is_range_valid.test.js b/src/plugins/kibana_react/public/validated_range/is_range_valid.test.ts similarity index 100% rename from src/legacy/ui/public/validated_range/is_range_valid.test.js rename to src/plugins/kibana_react/public/validated_range/is_range_valid.test.ts diff --git a/src/legacy/ui/public/validated_range/is_range_valid.js b/src/plugins/kibana_react/public/validated_range/is_range_valid.ts similarity index 74% rename from src/legacy/ui/public/validated_range/is_range_valid.js rename to src/plugins/kibana_react/public/validated_range/is_range_valid.ts index 9b733815a66ba..1f822c0cb94b9 100644 --- a/src/legacy/ui/public/validated_range/is_range_valid.js +++ b/src/plugins/kibana_react/public/validated_range/is_range_valid.ts @@ -18,14 +18,24 @@ */ import { i18n } from '@kbn/i18n'; +import { ValueMember, Value } from './validated_dual_range'; const LOWER_VALUE_INDEX = 0; const UPPER_VALUE_INDEX = 1; -export function isRangeValid(value, min, max, allowEmptyRange) { - allowEmptyRange = typeof allowEmptyRange === 'boolean' ? allowEmptyRange : true; //cannot use default props since that uses falsy check - let lowerValue = isNaN(value[LOWER_VALUE_INDEX]) ? '' : value[LOWER_VALUE_INDEX]; - let upperValue = isNaN(value[UPPER_VALUE_INDEX]) ? '' : value[UPPER_VALUE_INDEX]; +export function isRangeValid( + value: Value = [0, 0], + min: ValueMember = 0, + max: ValueMember = 0, + allowEmptyRange?: boolean +) { + allowEmptyRange = typeof allowEmptyRange === 'boolean' ? allowEmptyRange : true; // cannot use default props since that uses falsy check + let lowerValue: ValueMember = isNaN(value[LOWER_VALUE_INDEX] as number) + ? '' + : `${value[LOWER_VALUE_INDEX]}`; + let upperValue: ValueMember = isNaN(value[UPPER_VALUE_INDEX] as number) + ? '' + : `${value[UPPER_VALUE_INDEX]}`; const isLowerValueValid = lowerValue.toString() !== ''; const isUpperValueValid = upperValue.toString() !== ''; @@ -39,7 +49,7 @@ export function isRangeValid(value, min, max, allowEmptyRange) { let errorMessage = ''; const bothMustBeSetErrorMessage = i18n.translate( - 'common.ui.dualRangeControl.mustSetBothErrorMessage', + 'kibana-react.dualRangeControl.mustSetBothErrorMessage', { defaultMessage: 'Both lower and upper values must be set', } @@ -55,13 +65,13 @@ export function isRangeValid(value, min, max, allowEmptyRange) { errorMessage = bothMustBeSetErrorMessage; } else if ((isLowerValueValid && lowerValue < min) || (isUpperValueValid && upperValue > max)) { isValid = false; - errorMessage = i18n.translate('common.ui.dualRangeControl.outsideOfRangeErrorMessage', { + errorMessage = i18n.translate('kibana-react.dualRangeControl.outsideOfRangeErrorMessage', { defaultMessage: 'Values must be on or between {min} and {max}', values: { min, max }, }); } else if (isLowerValueValid && isUpperValueValid && upperValue < lowerValue) { isValid = false; - errorMessage = i18n.translate('common.ui.dualRangeControl.upperValidErrorMessage', { + errorMessage = i18n.translate('kibana-react.dualRangeControl.upperValidErrorMessage', { defaultMessage: 'Upper value must be greater or equal to lower value', }); } diff --git a/src/legacy/ui/public/validated_range/validated_dual_range.js b/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx similarity index 67% rename from src/legacy/ui/public/validated_range/validated_dual_range.js rename to src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx index 3b0efba11afcc..e7392eeba3830 100644 --- a/src/legacy/ui/public/validated_range/validated_dual_range.js +++ b/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx @@ -18,17 +18,38 @@ */ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { isRangeValid } from './is_range_valid'; - import { EuiFormRow, EuiDualRange } from '@elastic/eui'; +import { EuiFormRowDisplayKeys } from '@elastic/eui/src/components/form/form_row/form_row'; +import { EuiDualRangeProps } from '@elastic/eui/src/components/form/range/dual_range'; +import { isRangeValid } from './is_range_valid'; // Wrapper around EuiDualRange that ensures onChange callback is only called when range value // is valid and within min/max -export class ValidatedDualRange extends Component { - state = {}; - static getDerivedStateFromProps(nextProps, prevState) { +export type Value = EuiDualRangeProps['value']; +export type ValueMember = EuiDualRangeProps['value'][0]; + +interface Props extends Omit { + value?: Value; + allowEmptyRange?: boolean; + label?: string; + formRowDisplay?: EuiFormRowDisplayKeys; + onChange?: (val: [string, string]) => void; + min?: ValueMember; + max?: ValueMember; +} + +interface State { + isValid?: boolean; + errorMessage?: string; + value: [ValueMember, ValueMember]; + prevValue?: Value; +} + +export class ValidatedDualRange extends Component { + static defaultProps: { fullWidth: boolean; allowEmptyRange: boolean; compressed: boolean }; + + static getDerivedStateFromProps(nextProps: Props, prevState: State) { if (nextProps.value !== prevState.prevValue) { const { isValid, errorMessage } = isRangeValid( nextProps.value, @@ -47,7 +68,10 @@ export class ValidatedDualRange extends Component { return null; } - _onChange = value => { + // @ts-ignore state populated by getDerivedStateFromProps + state: State = {}; + + _onChange = (value: Value) => { const { isValid, errorMessage } = isRangeValid( value, this.props.min, @@ -61,8 +85,8 @@ export class ValidatedDualRange extends Component { errorMessage, }); - if (isValid) { - this.props.onChange(value); + if (this.props.onChange && isValid) { + this.props.onChange([value[0] as string, value[1] as string]); } }; @@ -75,7 +99,8 @@ export class ValidatedDualRange extends Component { value, // eslint-disable-line no-unused-vars onChange, // eslint-disable-line no-unused-vars allowEmptyRange, // eslint-disable-line no-unused-vars - ...rest + // @ts-ignore + ...rest // TODO: Consider alternatives for spread operator in component } = this.props; return ( @@ -92,6 +117,7 @@ export class ValidatedDualRange extends Component { fullWidth={fullWidth} value={this.state.value} onChange={this._onChange} + // @ts-ignore focusable={false} // remove when #59039 is fixed {...rest} /> @@ -100,14 +126,6 @@ export class ValidatedDualRange extends Component { } } -ValidatedDualRange.propTypes = { - allowEmptyRange: PropTypes.bool, - fullWidth: PropTypes.bool, - compressed: PropTypes.bool, - label: PropTypes.node, - formRowDisplay: PropTypes.string, -}; - ValidatedDualRange.defaultProps = { allowEmptyRange: true, fullWidth: false, diff --git a/src/plugins/kibana_utils/public/history/index.ts b/src/plugins/kibana_utils/public/history/index.ts index b4b5658c1c886..bb13ea09f928a 100644 --- a/src/plugins/kibana_utils/public/history/index.ts +++ b/src/plugins/kibana_utils/public/history/index.ts @@ -18,3 +18,4 @@ */ export { removeQueryParam } from './remove_query_param'; +export { redirectWhenMissing } from './redirect_when_missing'; diff --git a/src/plugins/kibana_utils/public/history/redirect_when_missing.tsx b/src/plugins/kibana_utils/public/history/redirect_when_missing.tsx new file mode 100644 index 0000000000000..cbdeef6fbe96c --- /dev/null +++ b/src/plugins/kibana_utils/public/history/redirect_when_missing.tsx @@ -0,0 +1,80 @@ +/* + * 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 { History } from 'history'; +import { i18n } from '@kbn/i18n'; + +import { ToastsSetup } from 'kibana/public'; +import { MarkdownSimple, toMountPoint } from '../../../kibana_react/public'; +import { SavedObjectNotFound } from '../errors'; + +interface Mapping { + [key: string]: string; +} + +/** + * Creates an error handler that will redirect to a url when a SavedObjectNotFound + * error is thrown + */ +export function redirectWhenMissing({ + history, + mapping, + toastNotifications, +}: { + history: History; + /** + * a mapping of url's to redirect to based on the saved object that + * couldn't be found, or just a string that will be used for all types + */ + mapping: string | Mapping; + /** + * Toast notifications service to show toasts in error cases. + */ + toastNotifications: ToastsSetup; +}) { + let localMappingObject: Mapping; + + if (typeof mapping === 'string') { + localMappingObject = { '*': mapping }; + } else { + localMappingObject = mapping; + } + + return (error: SavedObjectNotFound) => { + // if this error is not "404", rethrow + // we can't check "error instanceof SavedObjectNotFound" since this class can live in a separate bundle + // and the error will be an instance of other class with the same interface (actually the copy of SavedObjectNotFound class) + if (!error.savedObjectType) { + throw error; + } + + let url = localMappingObject[error.savedObjectType] || localMappingObject['*'] || '/'; + url += (url.indexOf('?') >= 0 ? '&' : '?') + `notFound=${error.savedObjectType}`; + + toastNotifications.addWarning({ + title: i18n.translate('kibana_utils.history.savedObjectIsMissingNotificationMessage', { + defaultMessage: 'Saved object is missing', + }), + text: toMountPoint({error.message}), + }); + + history.replace(url); + }; +} diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index ee38d5e8111c9..47f90cbe2a627 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -73,5 +73,5 @@ export { StartSyncStateFnType, StopSyncStateFnType, } from './state_sync'; -export { removeQueryParam } from './history'; +export { removeQueryParam, redirectWhenMissing } from './history'; export { applyDiff } from './state_management/utils/diff_object'; diff --git a/src/plugins/timelion/config.ts b/src/plugins/timelion/config.ts index 561fb4de9f58d..eaea1aaca1b7b 100644 --- a/src/plugins/timelion/config.ts +++ b/src/plugins/timelion/config.ts @@ -25,7 +25,7 @@ export const configSchema = schema.object( graphiteUrls: schema.maybe(schema.arrayOf(schema.string())), }, // This option should be removed as soon as we entirely migrate config from legacy Timelion plugin. - { allowUnknowns: true } + { unknowns: 'allow' } ); export type ConfigSchema = TypeOf; diff --git a/src/plugins/timelion/server/routes/run.ts b/src/plugins/timelion/server/routes/run.ts index b7a4179da768e..b773bba68ea81 100644 --- a/src/plugins/timelion/server/routes/run.ts +++ b/src/plugins/timelion/server/routes/run.ts @@ -78,15 +78,11 @@ export function runRoute( es: schema.object({ filter: schema.object({ bool: schema.object({ - filter: schema.maybe( - schema.arrayOf(schema.object({}, { allowUnknowns: true })) - ), - must: schema.maybe(schema.arrayOf(schema.object({}, { allowUnknowns: true }))), - should: schema.maybe( - schema.arrayOf(schema.object({}, { allowUnknowns: true })) - ), + filter: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + must: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + should: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), must_not: schema.maybe( - schema.arrayOf(schema.object({}, { allowUnknowns: true })) + schema.arrayOf(schema.object({}, { unknowns: 'allow' })) ), }), }), diff --git a/src/plugins/vis_type_timeseries/server/routes/vis.ts b/src/plugins/vis_type_timeseries/server/routes/vis.ts index e2d1e4d114ad5..9abbc4ad617dc 100644 --- a/src/plugins/vis_type_timeseries/server/routes/vis.ts +++ b/src/plugins/vis_type_timeseries/server/routes/vis.ts @@ -23,7 +23,7 @@ import { getVisData, GetVisDataOptions } from '../lib/get_vis_data'; import { visPayloadSchema } from './post_vis_schema'; import { Framework, ValidationTelemetryServiceSetup } from '../index'; -const escapeHatch = schema.object({}, { allowUnknowns: true }); +const escapeHatch = schema.object({}, { unknowns: 'allow' }); export const visDataRoutes = ( router: IRouter, diff --git a/src/plugins/visualizations/kibana.json b/src/plugins/visualizations/kibana.json index cf79ce17293d6..8e63ea7833327 100644 --- a/src/plugins/visualizations/kibana.json +++ b/src/plugins/visualizations/kibana.json @@ -1,7 +1,7 @@ { "id": "visualizations", "version": "kibana", - "server": false, + "server": true, "ui": true, "requiredPlugins": [ "expressions" diff --git a/src/plugins/embeddable/public/api/get_embeddable_factory.ts b/src/plugins/visualizations/server/index.ts similarity index 65% rename from src/plugins/embeddable/public/api/get_embeddable_factory.ts rename to src/plugins/visualizations/server/index.ts index 8e98da287c5ea..80c10c3945d4c 100644 --- a/src/plugins/embeddable/public/api/get_embeddable_factory.ts +++ b/src/plugins/visualizations/server/index.ts @@ -17,18 +17,14 @@ * under the License. */ -import { EmbeddableApiPure } from './types'; +import { PluginInitializerContext } from '../../../core/server'; +import { VisualizationsPlugin } from './plugin'; -export const getEmbeddableFactory: EmbeddableApiPure['getEmbeddableFactory'] = ({ - embeddableFactories, -}) => embeddableFactoryId => { - const factory = embeddableFactories.get(embeddableFactoryId); +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. - if (!factory) { - throw new Error( - `Embeddable factory [embeddableFactoryId = ${embeddableFactoryId}] does not exist.` - ); - } +export function plugin(initializerContext: PluginInitializerContext) { + return new VisualizationsPlugin(initializerContext); +} - return factory; -}; +export { VisualizationsPluginSetup, VisualizationsPluginStart } from './types'; diff --git a/src/plugins/visualizations/server/plugin.ts b/src/plugins/visualizations/server/plugin.ts new file mode 100644 index 0000000000000..79cce6b5867a1 --- /dev/null +++ b/src/plugins/visualizations/server/plugin.ts @@ -0,0 +1,54 @@ +/* + * 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 { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + Logger, +} from '../../../core/server'; + +import { visualizationSavedObjectType } from './saved_objects'; + +import { VisualizationsPluginSetup, VisualizationsPluginStart } from './types'; + +export class VisualizationsPlugin + implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + this.logger.debug('visualizations: Setup'); + + core.savedObjects.registerType(visualizationSavedObjectType); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('visualizations: Started'); + return {}; + } + + public stop() {} +} diff --git a/src/plugins/visualizations/server/saved_objects/index.ts b/src/plugins/visualizations/server/saved_objects/index.ts new file mode 100644 index 0000000000000..be75f63582450 --- /dev/null +++ b/src/plugins/visualizations/server/saved_objects/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { visualizationSavedObjectType } from './visualization'; diff --git a/src/plugins/visualizations/server/saved_objects/visualization.ts b/src/plugins/visualizations/server/saved_objects/visualization.ts new file mode 100644 index 0000000000000..9f4782f3ec730 --- /dev/null +++ b/src/plugins/visualizations/server/saved_objects/visualization.ts @@ -0,0 +1,56 @@ +/* + * 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 { SavedObjectsType } from 'kibana/server'; +import { visualizationSavedObjectTypeMigrations } from './visualization_migrations'; + +export const visualizationSavedObjectType: SavedObjectsType = { + name: 'visualization', + hidden: false, + namespaceAgnostic: false, + management: { + icon: 'visualizeApp', + defaultSearchField: 'title', + importableAndExportable: true, + getTitle(obj) { + return obj.attributes.title; + }, + getEditUrl(obj) { + return `/management/kibana/objects/savedVisualizations/${encodeURIComponent(obj.id)}`; + }, + getInAppUrl(obj) { + return { + path: `/app/kibana#/visualize/edit/${encodeURIComponent(obj.id)}`, + uiCapabilitiesPath: 'visualize.show', + }; + }, + }, + mappings: { + properties: { + description: { type: 'text' }, + kibanaSavedObjectMeta: { properties: { searchSourceJSON: { type: 'text' } } }, + savedSearchRefName: { type: 'keyword' }, + title: { type: 'text' }, + uiStateJSON: { type: 'text' }, + version: { type: 'integer' }, + visState: { type: 'text' }, + }, + }, + migrations: visualizationSavedObjectTypeMigrations, +}; diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts new file mode 100644 index 0000000000000..02c114bad4e72 --- /dev/null +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts @@ -0,0 +1,1356 @@ +/* + * 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 { visualizationSavedObjectTypeMigrations } from './visualization_migrations'; +import { SavedObjectMigrationContext, SavedObjectMigrationFn } from 'kibana/server'; + +const savedObjectMigrationContext = (null as unknown) as SavedObjectMigrationContext; + +describe('migration visualization', () => { + describe('6.7.2', () => { + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['6.7.2']( + doc as Parameters[0], + savedObjectMigrationContext + ); + let doc: any; + + describe('date histogram time zone removal', () => { + beforeEach(() => { + doc = { + attributes: { + visState: JSON.stringify({ + aggs: [ + { + enabled: true, + id: '1', + params: { + // Doesn't make much sense but we want to test it's not removing it from anything else + time_zone: 'Europe/Berlin', + }, + schema: 'metric', + type: 'count', + }, + { + enabled: true, + id: '2', + params: { + customInterval: '2h', + drop_partials: false, + extended_bounds: {}, + field: 'timestamp', + time_zone: 'Europe/Berlin', + interval: 'auto', + min_doc_count: 1, + useNormalizedEsInterval: true, + }, + schema: 'segment', + type: 'date_histogram', + }, + { + enabled: true, + id: '4', + params: { + customInterval: '2h', + drop_partials: false, + extended_bounds: {}, + field: 'timestamp', + interval: 'auto', + min_doc_count: 1, + useNormalizedEsInterval: true, + }, + schema: 'segment', + type: 'date_histogram', + }, + { + enabled: true, + id: '3', + params: { + customBucket: { + enabled: true, + id: '1-bucket', + params: { + customInterval: '2h', + drop_partials: false, + extended_bounds: {}, + field: 'timestamp', + interval: 'auto', + min_doc_count: 1, + time_zone: 'Europe/Berlin', + useNormalizedEsInterval: true, + }, + type: 'date_histogram', + }, + customMetric: { + enabled: true, + id: '1-metric', + params: {}, + type: 'count', + }, + }, + schema: 'metric', + type: 'max_bucket', + }, + ], + }), + }, + } as Parameters[0]; + }); + + it('should remove time_zone from date_histogram aggregations', () => { + const migratedDoc = migrate(doc); + const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; + + expect(aggs[1]).not.toHaveProperty('params.time_zone'); + }); + + it('should not remove time_zone from non date_histogram aggregations', () => { + const migratedDoc = migrate(doc); + const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; + + expect(aggs[0]).toHaveProperty('params.time_zone'); + }); + + it('should remove time_zone from nested aggregations', () => { + const migratedDoc = migrate(doc); + const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; + + expect(aggs[3]).not.toHaveProperty('params.customBucket.params.time_zone'); + }); + + it('should not fail on date histograms without a time_zone', () => { + const migratedDoc = migrate(doc); + const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; + + expect(aggs[2]).not.toHaveProperty('params.time_zone'); + }); + + it('should be able to apply the migration twice, since we need it for 6.7.2 and 7.0.1', () => { + const migratedDoc = migrate(doc); + const aggs = JSON.parse(migratedDoc.attributes.visState).aggs; + + expect(aggs[1]).not.toHaveProperty('params.time_zone'); + expect(aggs[0]).toHaveProperty('params.time_zone'); + expect(aggs[3]).not.toHaveProperty('params.customBucket.params.time_zone'); + expect(aggs[2]).not.toHaveProperty('params.time_zone'); + }); + }); + }); + + describe('7.0.0', () => { + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['7.0.0']( + doc as Parameters[0], + savedObjectMigrationContext + ); + + const generateDoc = (type: any, aggs: any) => ({ + attributes: { + title: 'My Vis', + description: 'This is my super cool vis.', + visState: JSON.stringify({ type, aggs }), + uiStateJSON: '{}', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: '{}', + }, + }, + references: [], + }); + + it('does not throw error on empty object', () => { + const migratedDoc = migrate({ + attributes: { + visState: '{}', + }, + }); + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "visState": "{}", + }, + "references": Array [], +} +`); + }); + + it('skips errors when searchSourceJSON is null', () => { + const doc = { + id: '1', + type: 'visualization', + attributes: { + visState: '{}', + kibanaSavedObjectMeta: { + searchSourceJSON: null, + }, + savedSearchId: '123', + }, + }; + const migratedDoc = migrate(doc); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": null, + }, + "savedSearchRefName": "search_0", + "visState": "{}", + }, + "id": "1", + "references": Array [ + Object { + "id": "123", + "name": "search_0", + "type": "search", + }, + ], + "type": "visualization", +} +`); + }); + + it('skips errors when searchSourceJSON is undefined', () => { + const doc = { + id: '1', + type: 'visualization', + attributes: { + visState: '{}', + kibanaSavedObjectMeta: { + searchSourceJSON: undefined, + }, + savedSearchId: '123', + }, + }; + const migratedDoc = migrate(doc); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": undefined, + }, + "savedSearchRefName": "search_0", + "visState": "{}", + }, + "id": "1", + "references": Array [ + Object { + "id": "123", + "name": "search_0", + "type": "search", + }, + ], + "type": "visualization", +} +`); + }); + + it('skips error when searchSourceJSON is not a string', () => { + const doc = { + id: '1', + type: 'visualization', + attributes: { + visState: '{}', + kibanaSavedObjectMeta: { + searchSourceJSON: 123, + }, + savedSearchId: '123', + }, + }; + + expect(migrate(doc)).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": 123, + }, + "savedSearchRefName": "search_0", + "visState": "{}", + }, + "id": "1", + "references": Array [ + Object { + "id": "123", + "name": "search_0", + "type": "search", + }, + ], + "type": "visualization", +} +`); + }); + + it('skips error when searchSourceJSON is invalid json', () => { + const doc = { + id: '1', + type: 'visualization', + attributes: { + visState: '{}', + kibanaSavedObjectMeta: { + searchSourceJSON: '{abc123}', + }, + savedSearchId: '123', + }, + }; + + expect(migrate(doc)).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{abc123}", + }, + "savedSearchRefName": "search_0", + "visState": "{}", + }, + "id": "1", + "references": Array [ + Object { + "id": "123", + "name": "search_0", + "type": "search", + }, + ], + "type": "visualization", +} +`); + }); + + it('skips error when "index" and "filter" is missing from searchSourceJSON', () => { + const doc = { + id: '1', + type: 'visualization', + attributes: { + visState: '{}', + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ bar: true }), + }, + savedSearchId: '123', + }, + }; + const migratedDoc = migrate(doc); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"bar\\":true}", + }, + "savedSearchRefName": "search_0", + "visState": "{}", + }, + "id": "1", + "references": Array [ + Object { + "id": "123", + "name": "search_0", + "type": "search", + }, + ], + "type": "visualization", +} +`); + }); + + it('extracts "index" attribute from doc', () => { + const doc = { + id: '1', + type: 'visualization', + attributes: { + visState: '{}', + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ bar: true, index: 'pattern*' }), + }, + savedSearchId: '123', + }, + }; + const migratedDoc = migrate(doc); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"bar\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}", + }, + "savedSearchRefName": "search_0", + "visState": "{}", + }, + "id": "1", + "references": Array [ + Object { + "id": "pattern*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern", + }, + Object { + "id": "123", + "name": "search_0", + "type": "search", + }, + ], + "type": "visualization", +} +`); + }); + + it('extracts index patterns from the filter', () => { + const doc = { + id: '1', + type: 'visualization', + attributes: { + visState: '{}', + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + bar: true, + filter: [ + { + meta: { index: 'my-index', foo: true }, + }, + ], + }), + }, + savedSearchId: '123', + }, + }; + const migratedDoc = migrate(doc); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"bar\\":true,\\"filter\\":[{\\"meta\\":{\\"foo\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}", + }, + "savedSearchRefName": "search_0", + "visState": "{}", + }, + "id": "1", + "references": Array [ + Object { + "id": "my-index", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern", + }, + Object { + "id": "123", + "name": "search_0", + "type": "search", + }, + ], + "type": "visualization", +} +`); + }); + + it('extracts index patterns from controls', () => { + const doc = { + id: '1', + type: 'visualization', + attributes: { + foo: true, + visState: JSON.stringify({ + bar: false, + params: { + controls: [ + { + bar: true, + indexPattern: 'pattern*', + }, + { + foo: true, + }, + ], + }, + }), + }, + }; + const migratedDoc = migrate(doc); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "foo": true, + "visState": "{\\"bar\\":false,\\"params\\":{\\"controls\\":[{\\"bar\\":true,\\"indexPatternRefName\\":\\"control_0_index_pattern\\"},{\\"foo\\":true}]}}", + }, + "id": "1", + "references": Array [ + Object { + "id": "pattern*", + "name": "control_0_index_pattern", + "type": "index-pattern", + }, + ], + "type": "visualization", +} +`); + }); + + it('skips extracting savedSearchId when missing', () => { + const doc = { + id: '1', + attributes: { + visState: '{}', + kibanaSavedObjectMeta: { + searchSourceJSON: '{}', + }, + }, + }; + const migratedDoc = migrate(doc); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{}", + }, + "visState": "{}", + }, + "id": "1", + "references": Array [], +} +`); + }); + + it('extract savedSearchId from doc', () => { + const doc = { + id: '1', + attributes: { + visState: '{}', + kibanaSavedObjectMeta: { + searchSourceJSON: '{}', + }, + savedSearchId: '123', + }, + }; + const migratedDoc = migrate(doc); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{}", + }, + "savedSearchRefName": "search_0", + "visState": "{}", + }, + "id": "1", + "references": Array [ + Object { + "id": "123", + "name": "search_0", + "type": "search", + }, + ], +} +`); + }); + + it('delete savedSearchId when empty string in doc', () => { + const doc = { + id: '1', + attributes: { + visState: '{}', + kibanaSavedObjectMeta: { + searchSourceJSON: '{}', + }, + savedSearchId: '', + }, + }; + const migratedDoc = migrate(doc); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{}", + }, + "visState": "{}", + }, + "id": "1", + "references": Array [], +} +`); + }); + + it('should return a new object if vis is table and has multiple split aggs', () => { + const aggs = [ + { + id: '1', + schema: 'metric', + params: {}, + }, + { + id: '2', + schema: 'split', + params: { foo: 'bar', row: true }, + }, + { + id: '3', + schema: 'split', + params: { hey: 'ya', row: false }, + }, + ]; + const tableDoc = generateDoc('table', aggs); + const expected = tableDoc; + const actual = migrate(tableDoc); + + expect(actual).not.toEqual(expected); + }); + + it('should not touch any vis that is not table', () => { + const pieDoc = generateDoc('pie', []); + const expected = pieDoc; + const actual = migrate(pieDoc); + + expect(actual).toEqual(expected); + }); + + it('should not change values in any vis that is not table', () => { + const aggs = [ + { + id: '1', + schema: 'metric', + params: {}, + }, + { + id: '2', + schema: 'split', + params: { foo: 'bar', row: true }, + }, + { + id: '3', + schema: 'segment', + params: { hey: 'ya' }, + }, + ]; + const pieDoc = generateDoc('pie', aggs); + const expected = pieDoc; + const actual = migrate(pieDoc); + + expect(actual).toEqual(expected); + }); + + it('should not touch table vis if there are not multiple split aggs', () => { + const aggs = [ + { + id: '1', + schema: 'metric', + params: {}, + }, + { + id: '2', + schema: 'split', + params: { foo: 'bar', row: true }, + }, + ]; + const tableDoc = generateDoc('table', aggs); + const expected = tableDoc; + const actual = migrate(tableDoc); + + expect(actual).toEqual(expected); + }); + + it('should change all split aggs to `bucket` except the first', () => { + const aggs = [ + { + id: '1', + schema: 'metric', + params: {}, + }, + { + id: '2', + schema: 'split', + params: { foo: 'bar', row: true }, + }, + { + id: '3', + schema: 'split', + params: { hey: 'ya', row: false }, + }, + { + id: '4', + schema: 'bucket', + params: { heyyy: 'yaaa' }, + }, + ]; + const expected = ['metric', 'split', 'bucket', 'bucket']; + const migrated = migrate(generateDoc('table', aggs)); + const actual = JSON.parse(migrated.attributes.visState); + + expect(actual.aggs.map((agg: any) => agg.schema)).toEqual(expected); + }); + + it('should remove `rows` param from any aggs that are not `split`', () => { + const aggs = [ + { + id: '1', + schema: 'metric', + params: {}, + }, + { + id: '2', + schema: 'split', + params: { foo: 'bar', row: true }, + }, + { + id: '3', + schema: 'split', + params: { hey: 'ya', row: false }, + }, + ]; + const expected = [{}, { foo: 'bar', row: true }, { hey: 'ya' }]; + const migrated = migrate(generateDoc('table', aggs)); + const actual = JSON.parse(migrated.attributes.visState); + + expect(actual.aggs.map((agg: any) => agg.params)).toEqual(expected); + }); + + it('should throw with a reference to the doc name if something goes wrong', () => { + const doc = { + attributes: { + title: 'My Vis', + description: 'This is my super cool vis.', + visState: '!/// Intentionally malformed JSON ///!', + uiStateJSON: '{}', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: '{}', + }, + }, + }; + expect(() => migrate(doc)).toThrowError(/My Vis/); + }); + }); + + describe('7.2.0', () => { + describe('date histogram custom interval removal', () => { + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['7.2.0']( + doc as Parameters[0], + savedObjectMigrationContext + ); + let doc: any; + + beforeEach(() => { + doc = { + attributes: { + visState: JSON.stringify({ + aggs: [ + { + enabled: true, + id: '1', + params: { + customInterval: '1h', + }, + schema: 'metric', + type: 'count', + }, + { + enabled: true, + id: '2', + params: { + customInterval: '2h', + drop_partials: false, + extended_bounds: {}, + field: 'timestamp', + interval: 'auto', + min_doc_count: 1, + useNormalizedEsInterval: true, + }, + schema: 'segment', + type: 'date_histogram', + }, + { + enabled: true, + id: '4', + params: { + customInterval: '2h', + drop_partials: false, + extended_bounds: {}, + field: 'timestamp', + interval: 'custom', + min_doc_count: 1, + useNormalizedEsInterval: true, + }, + schema: 'segment', + type: 'date_histogram', + }, + { + enabled: true, + id: '3', + params: { + customBucket: { + enabled: true, + id: '1-bucket', + params: { + customInterval: '2h', + drop_partials: false, + extended_bounds: {}, + field: 'timestamp', + interval: 'custom', + min_doc_count: 1, + useNormalizedEsInterval: true, + }, + type: 'date_histogram', + }, + customMetric: { + enabled: true, + id: '1-metric', + params: {}, + type: 'count', + }, + }, + schema: 'metric', + type: 'max_bucket', + }, + ], + }), + }, + }; + }); + + it('should remove customInterval from date_histogram aggregations', () => { + const migratedDoc = migrate(doc); + const { aggs } = JSON.parse(migratedDoc.attributes.visState); + + expect(aggs[1]).not.toHaveProperty('params.customInterval'); + }); + + it('should not change interval from date_histogram aggregations', () => { + const migratedDoc = migrate(doc); + const { aggs } = JSON.parse(migratedDoc.attributes.visState); + + expect(aggs[1].params.interval).toBe( + JSON.parse(doc.attributes.visState).aggs[1].params.interval + ); + }); + + it('should not remove customInterval from non date_histogram aggregations', () => { + const migratedDoc = migrate(doc); + const { aggs } = JSON.parse(migratedDoc.attributes.visState); + + expect(aggs[0]).toHaveProperty('params.customInterval'); + }); + + it('should set interval with customInterval value and remove customInterval when interval equals "custom"', () => { + const migratedDoc = migrate(doc); + const { aggs } = JSON.parse(migratedDoc.attributes.visState); + + expect(aggs[2].params.interval).toBe( + JSON.parse(doc.attributes.visState).aggs[2].params.customInterval + ); + expect(aggs[2]).not.toHaveProperty('params.customInterval'); + }); + + it('should remove customInterval from nested aggregations', () => { + const migratedDoc = migrate(doc); + const { aggs } = JSON.parse(migratedDoc.attributes.visState); + + expect(aggs[3]).not.toHaveProperty('params.customBucket.params.customInterval'); + }); + + it('should remove customInterval from nested aggregations and set interval with customInterval value', () => { + const migratedDoc = migrate(doc); + const { aggs } = JSON.parse(migratedDoc.attributes.visState); + + expect(aggs[3].params.customBucket.params.interval).toBe( + JSON.parse(doc.attributes.visState).aggs[3].params.customBucket.params.customInterval + ); + expect(aggs[3]).not.toHaveProperty('params.customBucket.params.customInterval'); + }); + + it('should not fail on date histograms without a customInterval', () => { + const migratedDoc = migrate(doc); + const { aggs } = JSON.parse(migratedDoc.attributes.visState); + + expect(aggs[3]).not.toHaveProperty('params.customInterval'); + }); + }); + }); + + describe('7.3.0', () => { + const logMsgArr: string[] = []; + const logger = ({ + log: { + warn: (msg: string) => logMsgArr.push(msg), + }, + } as unknown) as SavedObjectMigrationContext; + + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['7.3.0']( + doc as Parameters[0], + logger + ); + + it('migrates type = gauge verticalSplit: false to alignment: vertical', () => { + const migratedDoc = migrate({ + attributes: { + visState: JSON.stringify({ type: 'gauge', params: { gauge: { verticalSplit: false } } }), + }, + }); + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "visState": "{\\"type\\":\\"gauge\\",\\"params\\":{\\"gauge\\":{\\"alignment\\":\\"horizontal\\"}}}", + }, +} +`); + }); + + it('migrates type = gauge verticalSplit: false to alignment: horizontal', () => { + const migratedDoc = migrate({ + attributes: { + visState: JSON.stringify({ type: 'gauge', params: { gauge: { verticalSplit: true } } }), + }, + }); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "visState": "{\\"type\\":\\"gauge\\",\\"params\\":{\\"gauge\\":{\\"alignment\\":\\"vertical\\"}}}", + }, +} +`); + }); + + it('doesnt migrate type = gauge containing invalid visState object, adds message to log', () => { + const migratedDoc = migrate({ + attributes: { + visState: JSON.stringify({ type: 'gauge' }), + }, + }); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "visState": "{\\"type\\":\\"gauge\\"}", + }, +} +`); + expect(logMsgArr).toMatchInlineSnapshot(` +Array [ + "Exception @ migrateGaugeVerticalSplitToAlignment! TypeError: Cannot read property 'gauge' of undefined", + "Exception @ migrateGaugeVerticalSplitToAlignment! Payload: {\\"type\\":\\"gauge\\"}", +] +`); + }); + + describe('filters agg query migration', () => { + const doc = { + attributes: { + visState: JSON.stringify({ + aggs: [ + { + type: 'filters', + params: { + filters: [ + { + input: { + query: 'response:200', + }, + label: '', + }, + { + input: { + query: 'response:404', + }, + label: 'bad response', + }, + { + input: { + query: { + exists: { + field: 'phpmemory', + }, + }, + }, + label: '', + }, + ], + }, + }, + ], + }), + }, + }; + + it('should add language property to filters without one, assuming lucene', () => { + const migrationResult = migrate(doc); + + expect(migrationResult).toEqual({ + attributes: { + visState: JSON.stringify({ + aggs: [ + { + type: 'filters', + params: { + filters: [ + { + input: { + query: 'response:200', + language: 'lucene', + }, + label: '', + }, + { + input: { + query: 'response:404', + language: 'lucene', + }, + label: 'bad response', + }, + { + input: { + query: { + exists: { + field: 'phpmemory', + }, + }, + language: 'lucene', + }, + label: '', + }, + ], + }, + }, + ], + }), + }, + }); + }); + }); + + describe('replaceMovAvgToMovFn()', () => { + let doc: any; + + beforeEach(() => { + doc = { + attributes: { + title: 'VIS', + visState: `{"title":"VIS","type":"metrics","params":{"id":"61ca57f0-469d-11e7-af02-69e470af7417", + "type":"timeseries","series":[{"id":"61ca57f1-469d-11e7-af02-69e470af7417","color":"rgba(0,156,224,1)", + "split_mode":"terms","metrics":[{"id":"61ca57f2-469d-11e7-af02-69e470af7417","type":"count", + "numerator":"FlightDelay:true"},{"settings":"","minimize":0,"window":5,"model": + "holt_winters","id":"23054fe0-8915-11e9-9b86-d3f94982620f","type":"moving_average","field": + "61ca57f2-469d-11e7-af02-69e470af7417","predict":1}],"separate_axis":0,"axis_position":"right", + "formatter":"number","chart_type":"line","line_width":"2","point_size":"0","fill":0.5,"stacked":"none", + "label":"Percent Delays","terms_size":"2","terms_field":"OriginCityName"}],"time_field":"timestamp", + "index_pattern":"kibana_sample_data_flights","interval":">=12h","axis_position":"left","axis_formatter": + "number","show_legend":1,"show_grid":1,"annotations":[{"fields":"FlightDelay,Cancelled,Carrier", + "template":"{{Carrier}}: Flight Delayed and Cancelled!","index_pattern":"kibana_sample_data_flights", + "query_string":"FlightDelay:true AND Cancelled:true","id":"53b7dff0-4c89-11e8-a66a-6989ad5a0a39", + "color":"rgba(0,98,177,1)","time_field":"timestamp","icon":"fa-exclamation-triangle", + "ignore_global_filters":1,"ignore_panel_filters":1,"hidden":true}],"legend_position":"bottom", + "axis_scale":"normal","default_index_pattern":"kibana_sample_data_flights","default_timefield":"timestamp"}, + "aggs":[]}`, + }, + migrationVersion: { + visualization: '7.2.0', + }, + type: 'visualization', + }; + }); + + test('should add some necessary moving_fn fields', () => { + const migratedDoc = migrate(doc); + const visState = JSON.parse(migratedDoc.attributes.visState); + const metric = visState.params.series[0].metrics[1]; + + expect(metric).toHaveProperty('model_type'); + expect(metric).toHaveProperty('alpha'); + expect(metric).toHaveProperty('beta'); + expect(metric).toHaveProperty('gamma'); + expect(metric).toHaveProperty('period'); + expect(metric).toHaveProperty('multiplicative'); + }); + }); + }); + + describe('7.3.0 tsvb', () => { + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['7.3.0']( + doc as Parameters[0], + savedObjectMigrationContext + ); + + const generateDoc = (params: any) => ({ + attributes: { + title: 'My Vis', + description: 'This is my super cool vis.', + visState: JSON.stringify({ params }), + uiStateJSON: '{}', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: '{}', + }, + }, + }); + it('should change series item filters from a string into an object', () => { + const params = { type: 'metric', series: [{ filter: 'Filter Bytes Test:>1000' }] }; + const testDoc1 = generateDoc(params); + const migratedTestDoc1 = migrate(testDoc1); + const series = JSON.parse(migratedTestDoc1.attributes.visState).params.series; + + expect(series[0].filter).toHaveProperty('query'); + expect(series[0].filter).toHaveProperty('language'); + }); + it('should not change a series item filter string in the object after migration', () => { + const markdownParams = { + type: 'markdown', + series: [ + { + filter: 'Filter Bytes Test:>1000', + split_filters: [{ filter: 'bytes:>1000' }], + }, + ], + }; + const markdownDoc = generateDoc(markdownParams); + const migratedMarkdownDoc = migrate(markdownDoc); + const markdownSeries = JSON.parse(migratedMarkdownDoc.attributes.visState).params.series; + + expect(markdownSeries[0].filter.query).toBe( + JSON.parse(markdownDoc.attributes.visState).params.series[0].filter + ); + expect(markdownSeries[0].split_filters[0].filter.query).toBe( + JSON.parse(markdownDoc.attributes.visState).params.series[0].split_filters[0].filter + ); + }); + + it('should change series item filters from a string into an object for all filters', () => { + const params = { + type: 'timeseries', + filter: 'bytes:>1000', + series: [ + { + filter: 'Filter Bytes Test:>1000', + split_filters: [{ filter: 'bytes:>1000' }], + }, + ], + annotations: [{ query_string: 'bytes:>1000' }], + }; + const timeSeriesDoc = generateDoc(params); + const migratedtimeSeriesDoc = migrate(timeSeriesDoc); + const timeSeriesParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; + + expect(Object.keys(timeSeriesParams.series[0].filter)).toEqual( + expect.arrayContaining(['query', 'language']) + ); + expect(Object.keys(timeSeriesParams.series[0].split_filters[0].filter)).toEqual( + expect.arrayContaining(['query', 'language']) + ); + expect(Object.keys(timeSeriesParams.annotations[0].query_string)).toEqual( + expect.arrayContaining(['query', 'language']) + ); + }); + + it('should not fail on a metric visualization without a filter in a series item', () => { + const params = { type: 'metric', series: [{}, {}, {}] }; + const testDoc1 = generateDoc(params); + const migratedTestDoc1 = migrate(testDoc1); + const series = JSON.parse(migratedTestDoc1.attributes.visState).params.series; + + expect(series[2]).not.toHaveProperty('filter.query'); + }); + + it('should not migrate a visualization of unknown type', () => { + const params = { type: 'unknown', series: [{ filter: 'foo:bar' }] }; + const doc = generateDoc(params); + const migratedDoc = migrate(doc); + const series = JSON.parse(migratedDoc.attributes.visState).params.series; + + expect(series[0].filter).toEqual(params.series[0].filter); + }); + }); + + describe('7.3.1', () => { + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['7.3.1']( + doc as Parameters[0], + savedObjectMigrationContext + ); + + it('should migrate filters agg query string queries', () => { + const state = { + aggs: [ + { type: 'count', params: {} }, + { + type: 'filters', + params: { + filters: [ + { + input: { + query: { + query_string: { query: 'machine.os.keyword:"win 8"' }, + }, + }, + }, + ], + }, + }, + ], + }; + const expected = { + aggs: [ + { type: 'count', params: {} }, + { + type: 'filters', + params: { + filters: [{ input: { query: 'machine.os.keyword:"win 8"' } }], + }, + }, + ], + }; + const migratedDoc = migrate({ attributes: { visState: JSON.stringify(state) } }); + + expect(migratedDoc).toEqual({ attributes: { visState: JSON.stringify(expected) } }); + }); + }); + + describe('7.4.2 tsvb split_filters migration', () => { + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['7.4.2']( + doc as Parameters[0], + savedObjectMigrationContext + ); + const generateDoc = (params: any) => ({ + attributes: { + title: 'My Vis', + description: 'This is my super cool vis.', + visState: JSON.stringify({ params }), + uiStateJSON: '{}', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: '{}', + }, + }, + }); + + it('should change series item filters from a string into an object for all filters', () => { + const params = { + type: 'timeseries', + filter: { + query: 'bytes:>1000', + language: 'lucene', + }, + series: [ + { + split_filters: [{ filter: 'bytes:>1000' }], + }, + ], + }; + const timeSeriesDoc = generateDoc(params); + const migratedtimeSeriesDoc = migrate(timeSeriesDoc); + const timeSeriesParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; + + expect(Object.keys(timeSeriesParams.filter)).toEqual( + expect.arrayContaining(['query', 'language']) + ); + expect(timeSeriesParams.series[0].split_filters[0].filter).toEqual({ + query: 'bytes:>1000', + language: 'lucene', + }); + }); + + it('should change series item split filters when there is no filter item', () => { + const params = { + type: 'timeseries', + filter: { + query: 'bytes:>1000', + language: 'lucene', + }, + series: [ + { + split_filters: [{ filter: 'bytes:>1000' }], + }, + ], + annotations: [ + { + query_string: { + query: 'bytes:>1000', + language: 'lucene', + }, + }, + ], + }; + const timeSeriesDoc = generateDoc(params); + const migratedtimeSeriesDoc = migrate(timeSeriesDoc); + const timeSeriesParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; + + expect(timeSeriesParams.series[0].split_filters[0].filter).toEqual({ + query: 'bytes:>1000', + language: 'lucene', + }); + }); + + it('should not convert split_filters to objects if there are no split filter filters', () => { + const params = { + type: 'timeseries', + filter: { + query: 'bytes:>1000', + language: 'lucene', + }, + series: [ + { + split_filters: [], + }, + ], + }; + const timeSeriesDoc = generateDoc(params); + const migratedtimeSeriesDoc = migrate(timeSeriesDoc); + const timeSeriesParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; + + expect(timeSeriesParams.series[0].split_filters).not.toHaveProperty('query'); + }); + + it('should do nothing if a split_filter is already a query:language object', () => { + const params = { + type: 'timeseries', + filter: { + query: 'bytes:>1000', + language: 'lucene', + }, + series: [ + { + split_filters: [ + { + filter: { + query: 'bytes:>1000', + language: 'lucene', + }, + }, + ], + }, + ], + annotations: [ + { + query_string: { + query: 'bytes:>1000', + language: 'lucene', + }, + }, + ], + }; + const timeSeriesDoc = generateDoc(params); + const migratedtimeSeriesDoc = migrate(timeSeriesDoc); + const timeSeriesParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; + + expect(timeSeriesParams.series[0].split_filters[0].filter.query).toEqual('bytes:>1000'); + expect(timeSeriesParams.series[0].split_filters[0].filter.language).toEqual('lucene'); + }); + }); +}); diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts new file mode 100644 index 0000000000000..9ee355cbb23cf --- /dev/null +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts @@ -0,0 +1,574 @@ +/* + * 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 { SavedObjectMigrationFn } from 'kibana/server'; +import { cloneDeep, get, omit, has, flow } from 'lodash'; + +const migrateIndexPattern: SavedObjectMigrationFn = doc => { + const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); + if (typeof searchSourceJSON !== 'string') { + return doc; + } + let searchSource; + try { + searchSource = JSON.parse(searchSourceJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + return doc; + } + + if (searchSource.index && Array.isArray(doc.references)) { + searchSource.indexRefName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; + doc.references.push({ + name: searchSource.indexRefName, + type: 'index-pattern', + id: searchSource.index, + }); + delete searchSource.index; + } + if (searchSource.filter) { + searchSource.filter.forEach((filterRow: any, i: number) => { + if (!filterRow.meta || !filterRow.meta.index || !Array.isArray(doc.references)) { + return; + } + filterRow.meta.indexRefName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; + doc.references.push({ + name: filterRow.meta.indexRefName, + type: 'index-pattern', + id: filterRow.meta.index, + }); + delete filterRow.meta.index; + }); + } + + doc.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify(searchSource); + + return doc; +}; + +// [TSVB] Migrate percentile-rank aggregation (value -> values) +const migratePercentileRankAggregation: SavedObjectMigrationFn = doc => { + const visStateJSON = get(doc, 'attributes.visState'); + let visState; + + if (visStateJSON) { + try { + visState = JSON.parse(visStateJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + if (visState && visState.type === 'metrics') { + const series: any[] = get(visState, 'params.series') || []; + + series.forEach(part => { + (part.metrics || []).forEach((metric: any) => { + if (metric.type === 'percentile_rank' && has(metric, 'value')) { + metric.values = [metric.value]; + + delete metric.value; + } + }); + }); + + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(visState), + }, + }; + } + } + return doc; +}; + +// Migrate date histogram aggregation (remove customInterval) +const migrateDateHistogramAggregation: SavedObjectMigrationFn = doc => { + const visStateJSON = get(doc, 'attributes.visState'); + let visState; + + if (visStateJSON) { + try { + visState = JSON.parse(visStateJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + + if (visState && visState.aggs) { + visState.aggs.forEach((agg: any) => { + if (agg.type === 'date_histogram' && agg.params) { + if (agg.params.interval === 'custom') { + agg.params.interval = agg.params.customInterval; + } + delete agg.params.customInterval; + } + + if ( + get(agg, 'params.customBucket.type', null) === 'date_histogram' && + agg.params.customBucket.params + ) { + if (agg.params.customBucket.params.interval === 'custom') { + agg.params.customBucket.params.interval = agg.params.customBucket.params.customInterval; + } + delete agg.params.customBucket.params.customInterval; + } + }); + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(visState), + }, + }; + } + } + return doc; +}; + +const removeDateHistogramTimeZones: SavedObjectMigrationFn = doc => { + const visStateJSON = get(doc, 'attributes.visState'); + if (visStateJSON) { + let visState; + try { + visState = JSON.parse(visStateJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + if (visState && visState.aggs) { + visState.aggs.forEach((agg: any) => { + // We're checking always for the existance of agg.params here. This should always exist, but better + // be safe then sorry during migrations. + if (agg.type === 'date_histogram' && agg.params) { + delete agg.params.time_zone; + } + + if ( + get(agg, 'params.customBucket.type', null) === 'date_histogram' && + agg.params.customBucket.params + ) { + delete agg.params.customBucket.params.time_zone; + } + }); + doc.attributes.visState = JSON.stringify(visState); + } + } + return doc; +}; + +// migrate gauge verticalSplit to alignment +// https://github.com/elastic/kibana/issues/34636 +const migrateGaugeVerticalSplitToAlignment: SavedObjectMigrationFn = (doc, logger) => { + const visStateJSON = get(doc, 'attributes.visState'); + + if (visStateJSON) { + try { + const visState = JSON.parse(visStateJSON); + if (visState && visState.type === 'gauge' && !visState.params.gauge.alignment) { + visState.params.gauge.alignment = visState.params.gauge.verticalSplit + ? 'vertical' + : 'horizontal'; + delete visState.params.gauge.verticalSplit; + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(visState), + }, + }; + } + } catch (e) { + logger.log.warn(`Exception @ migrateGaugeVerticalSplitToAlignment! ${e}`); + logger.log.warn(`Exception @ migrateGaugeVerticalSplitToAlignment! Payload: ${visStateJSON}`); + } + } + return doc; +}; +// Migrate filters (string -> { query: string, language: lucene }) +/* + Enabling KQL in TSVB causes problems with savedObject visualizations when these are saved with filters. + In a visualisation type of saved object, if the visState param is of type metric, the filter is saved as a string that is not interpretted correctly as a lucene query in the visualization itself. + We need to transform the filter string into an object containing the original string as a query and specify the query language as lucene. + For Metrics visualizations (param.type === "metric"), filters can be applied to each series object in the series array within the SavedObject.visState.params object. + Path to the series array is thus: + attributes.visState. +*/ +const transformFilterStringToQueryObject: SavedObjectMigrationFn = (doc, logger) => { + // Migrate filters + // If any filters exist and they are a string, we assume it to be lucene and transform the filter into an object accordingly + const newDoc = cloneDeep(doc); + const visStateJSON = get(doc, 'attributes.visState'); + if (visStateJSON) { + let visState; + try { + visState = JSON.parse(visStateJSON); + } catch (e) { + // let it go, the data is invalid and we'll leave it as is + } + if (visState) { + const visType = get(visState, 'params.type'); + const tsvbTypes = ['metric', 'markdown', 'top_n', 'gauge', 'table', 'timeseries']; + if (tsvbTypes.indexOf(visType) === -1) { + // skip + return doc; + } + // migrate the params fitler + const params: any = get(visState, 'params'); + if (params.filter && typeof params.filter === 'string') { + const paramsFilterObject = { + query: params.filter, + language: 'lucene', + }; + params.filter = paramsFilterObject; + } + + // migrate the annotations query string: + const annotations: any[] = get(visState, 'params.annotations') || []; + annotations.forEach(item => { + if (!item.query_string) { + // we don't need to transform anything if there isn't a filter at all + return; + } + if (typeof item.query_string === 'string') { + const itemQueryStringObject = { + query: item.query_string, + language: 'lucene', + }; + item.query_string = itemQueryStringObject; + } + }); + // migrate the series filters + const series: any[] = get(visState, 'params.series') || []; + + series.forEach(item => { + if (!item.filter) { + // we don't need to transform anything if there isn't a filter at all + return; + } + // series item filter + if (typeof item.filter === 'string') { + const itemfilterObject = { + query: item.filter, + language: 'lucene', + }; + item.filter = itemfilterObject; + } + // series item split filters filter + if (item.split_filters) { + const splitFilters: any[] = get(item, 'split_filters') || []; + splitFilters.forEach(filter => { + if (!filter.filter) { + // we don't need to transform anything if there isn't a filter at all + return; + } + if (typeof filter.filter === 'string') { + const filterfilterObject = { + query: filter.filter, + language: 'lucene', + }; + filter.filter = filterfilterObject; + } + }); + } + }); + newDoc.attributes.visState = JSON.stringify(visState); + } + } + return newDoc; +}; + +const transformSplitFiltersStringToQueryObject: SavedObjectMigrationFn = doc => { + // Migrate split_filters in TSVB objects that weren't migrated in 7.3 + // If any filters exist and they are a string, we assume them to be lucene syntax and transform the filter into an object accordingly + const newDoc = cloneDeep(doc); + const visStateJSON = get(doc, 'attributes.visState'); + if (visStateJSON) { + let visState; + try { + visState = JSON.parse(visStateJSON); + } catch (e) { + // let it go, the data is invalid and we'll leave it as is + } + if (visState) { + const visType = get(visState, 'params.type'); + const tsvbTypes = ['metric', 'markdown', 'top_n', 'gauge', 'table', 'timeseries']; + if (tsvbTypes.indexOf(visType) === -1) { + // skip + return doc; + } + // migrate the series split_filter filters + const series: any[] = get(visState, 'params.series') || []; + series.forEach(item => { + // series item split filters filter + if (item.split_filters) { + const splitFilters: any[] = get(item, 'split_filters') || []; + if (splitFilters.length > 0) { + // only transform split_filter filters if we have filters + splitFilters.forEach(filter => { + if (typeof filter.filter === 'string') { + const filterfilterObject = { + query: filter.filter, + language: 'lucene', + }; + filter.filter = filterfilterObject; + } + }); + } + } + }); + newDoc.attributes.visState = JSON.stringify(visState); + } + } + return newDoc; +}; + +const migrateFiltersAggQuery: SavedObjectMigrationFn = doc => { + const visStateJSON = get(doc, 'attributes.visState'); + + if (visStateJSON) { + try { + const visState = JSON.parse(visStateJSON); + + if (visState && visState.aggs) { + visState.aggs.forEach((agg: any) => { + if (agg.type !== 'filters') return; + + agg.params.filters.forEach((filter: any) => { + if (filter.input.language) return filter; + filter.input.language = 'lucene'; + }); + }); + + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(visState), + }, + }; + } + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + } + return doc; +}; + +const replaceMovAvgToMovFn: SavedObjectMigrationFn = (doc, logger) => { + const visStateJSON = get(doc, 'attributes.visState'); + let visState; + + if (visStateJSON) { + try { + visState = JSON.parse(visStateJSON); + + if (visState && visState.type === 'metrics') { + const series: any[] = get(visState, 'params.series', []); + + series.forEach(part => { + if (part.metrics && Array.isArray(part.metrics)) { + part.metrics.forEach((metric: any) => { + if (metric.type === 'moving_average') { + metric.model_type = metric.model; + metric.alpha = get(metric, 'settings.alpha', 0.3); + metric.beta = get(metric, 'settings.beta', 0.1); + metric.gamma = get(metric, 'settings.gamma', 0.3); + metric.period = get(metric, 'settings.period', 1); + metric.multiplicative = get(metric, 'settings.type') === 'mult'; + + delete metric.minimize; + delete metric.model; + delete metric.settings; + delete metric.predict; + } + }); + } + }); + + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(visState), + }, + }; + } + } catch (e) { + logger.log.warn(`Exception @ replaceMovAvgToMovFn! ${e}`); + logger.log.warn(`Exception @ replaceMovAvgToMovFn! Payload: ${visStateJSON}`); + } + } + + return doc; +}; + +const migrateFiltersAggQueryStringQueries: SavedObjectMigrationFn = (doc, logger) => { + const visStateJSON = get(doc, 'attributes.visState'); + + if (visStateJSON) { + try { + const visState = JSON.parse(visStateJSON); + + if (visState && visState.aggs) { + visState.aggs.forEach((agg: any) => { + if (agg.type !== 'filters') return doc; + + agg.params.filters.forEach((filter: any) => { + if (filter.input.query.query_string) { + filter.input.query = filter.input.query.query_string.query; + } + }); + }); + + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(visState), + }, + }; + } + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + } + return doc; +}; + +const addDocReferences: SavedObjectMigrationFn = doc => ({ + ...doc, + references: doc.references || [], +}); + +const migrateSavedSearch: SavedObjectMigrationFn = doc => { + const savedSearchId = get(doc, 'attributes.savedSearchId'); + + if (savedSearchId && doc.references) { + doc.references.push({ + type: 'search', + name: 'search_0', + id: savedSearchId, + }); + doc.attributes.savedSearchRefName = 'search_0'; + } + + delete doc.attributes.savedSearchId; + + return doc; +}; + +const migrateControls: SavedObjectMigrationFn = doc => { + const visStateJSON = get(doc, 'attributes.visState'); + + if (visStateJSON) { + let visState; + try { + visState = JSON.parse(visStateJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + if (visState) { + const controls: any[] = get(visState, 'params.controls') || []; + controls.forEach((control, i) => { + if (!control.indexPattern || !doc.references) { + return; + } + control.indexPatternRefName = `control_${i}_index_pattern`; + doc.references.push({ + name: control.indexPatternRefName, + type: 'index-pattern', + id: control.indexPattern, + }); + delete control.indexPattern; + }); + doc.attributes.visState = JSON.stringify(visState); + } + } + + return doc; +}; + +const migrateTableSplits: SavedObjectMigrationFn = doc => { + try { + const visState = JSON.parse(doc.attributes.visState); + if (get(visState, 'type') !== 'table') { + return doc; // do nothing; we only want to touch tables + } + + let splitCount = 0; + visState.aggs = visState.aggs.map((agg: any) => { + if (agg.schema !== 'split') { + return agg; + } + + splitCount++; + if (splitCount === 1) { + return agg; // leave the first split agg unchanged + } + agg.schema = 'bucket'; + // the `row` param is exclusively used by split aggs, so we remove it + agg.params = omit(agg.params, ['row']); + return agg; + }); + + if (splitCount <= 1) { + return doc; // do nothing; we only want to touch tables with multiple split aggs + } + + const newDoc = cloneDeep(doc); + newDoc.attributes.visState = JSON.stringify(visState); + + return newDoc; + } catch (e) { + throw new Error(`Failure attempting to migrate saved object '${doc.attributes.title}' - ${e}`); + } +}; + +export const visualizationSavedObjectTypeMigrations = { + /** + * We need to have this migration twice, once with a version prior to 7.0.0 once with a version + * after it. The reason for that is, that this migration has been introduced once 7.0.0 was already + * released. Thus a user who already had 7.0.0 installed already got the 7.0.0 migrations below running, + * so we need a version higher than that. But this fix was backported to the 6.7 release, meaning if we + * would only have the 7.0.1 migration in here a user on the 6.7 release will migrate their saved objects + * to the 7.0.1 state, and thus when updating their Kibana to 7.0, will never run the 7.0.0 migrations introduced + * in that version. So we apply this twice, once with 6.7.2 and once with 7.0.1 while the backport to 6.7 + * only contained the 6.7.2 migration and not the 7.0.1 migration. + */ + '6.7.2': flow(removeDateHistogramTimeZones), + '7.0.0': flow( + addDocReferences, + migrateIndexPattern, + migrateSavedSearch, + migrateControls, + migrateTableSplits + ), + '7.0.1': flow(removeDateHistogramTimeZones), + '7.2.0': flow( + migratePercentileRankAggregation, + migrateDateHistogramAggregation + ), + '7.3.0': flow( + migrateGaugeVerticalSplitToAlignment, + transformFilterStringToQueryObject, + migrateFiltersAggQuery, + replaceMovAvgToMovFn + ), + '7.3.1': flow(migrateFiltersAggQueryStringQueries), + '7.4.2': flow(transformSplitFiltersStringToQueryObject), +}; diff --git a/src/plugins/visualizations/server/types.ts b/src/plugins/visualizations/server/types.ts new file mode 100644 index 0000000000000..6924edb29627d --- /dev/null +++ b/src/plugins/visualizations/server/types.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface VisualizationsPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface VisualizationsPluginStart {} diff --git a/src/setup_node_env/exit_on_warning.js b/src/setup_node_env/exit_on_warning.js index 5be5ccd72bd02..6321cd7ba8db0 100644 --- a/src/setup_node_env/exit_on_warning.js +++ b/src/setup_node_env/exit_on_warning.js @@ -35,4 +35,16 @@ if (process.noProcessWarnings !== true) { process.exit(1); }); + + // While the above warning listener would also be called on + // unhandledRejection warnings, we can give a better error message if we + // handle them separately: + process.on('unhandledRejection', function(reason) { + console.error('Unhandled Promise rejection detected:'); + console.error(); + console.error(reason); + console.error(); + console.error('Terminating process...'); + process.exit(1); + }); } diff --git a/src/test_utils/kbn_server.ts b/src/test_utils/kbn_server.ts index f4c3ecd8243ce..12f7eb5a0a043 100644 --- a/src/test_utils/kbn_server.ts +++ b/src/test_utils/kbn_server.ts @@ -50,6 +50,7 @@ const DEFAULTS_SETTINGS = { logging: { silent: true }, plugins: {}, optimize: { enabled: false }, + migrations: { skip: true }, }; const DEFAULT_SETTINGS_WITH_CORE_PLUGINS = { diff --git a/test/api_integration/apis/index.js b/test/api_integration/apis/index.js index 8cdbbf8e74a3d..57e9120773f33 100644 --- a/test/api_integration/apis/index.js +++ b/test/api_integration/apis/index.js @@ -33,6 +33,5 @@ export default function({ loadTestFile }) { loadTestFile(require.resolve('./status')); loadTestFile(require.resolve('./stats')); loadTestFile(require.resolve('./ui_metric')); - loadTestFile(require.resolve('./core')); }); } diff --git a/test/common/services/security/role.ts b/test/common/services/security/role.ts index 0e7572882f80d..dfc6ff9b164e5 100644 --- a/test/common/services/security/role.ts +++ b/test/common/services/security/role.ts @@ -43,7 +43,6 @@ export class Role { `Expected status code of 204, received ${status} ${statusText}: ${util.inspect(data)}` ); } - this.log.debug(`created role ${name}`); } public async delete(name: string) { @@ -56,6 +55,5 @@ export class Role { )}` ); } - this.log.debug(`deleted role ${name}`); } } diff --git a/test/common/services/security/security.ts b/test/common/services/security/security.ts index 4eebb7b6697e0..6ad0933a2a5a2 100644 --- a/test/common/services/security/security.ts +++ b/test/common/services/security/security.ts @@ -23,15 +23,21 @@ import { Role } from './role'; import { User } from './user'; import { RoleMappings } from './role_mappings'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { createTestUserService } from './test_user'; -export function SecurityServiceProvider({ getService }: FtrProviderContext) { +export async function SecurityServiceProvider(context: FtrProviderContext) { + const { getService } = context; const log = getService('log'); const config = getService('config'); const url = formatUrl(config.get('servers.kibana')); + const role = new Role(url, log); + const user = new User(url, log); + const testUser = await createTestUserService(role, user, context); return new (class SecurityService { - role = new Role(url, log); roleMappings = new RoleMappings(url, log); - user = new User(url, log); + testUser = testUser; + role = role; + user = user; })(); } diff --git a/test/common/services/security/test_user.ts b/test/common/services/security/test_user.ts new file mode 100644 index 0000000000000..7f01c64d291a5 --- /dev/null +++ b/test/common/services/security/test_user.ts @@ -0,0 +1,92 @@ +/* + * 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 { Role } from './role'; +import { User } from './user'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { Browser } from '../../../functional/services/browser'; +import { TestSubjects } from '../../../functional/services/test_subjects'; + +export async function createTestUserService( + role: Role, + user: User, + { getService, hasService }: FtrProviderContext +) { + const log = getService('log'); + const config = getService('config'); + // @ts-ignore browser service is not normally available in common. + const browser: Browser | void = hasService('browser') && getService('browser'); + const testSubjects: TestSubjects | void = + // @ts-ignore testSubject service is not normally available in common. + hasService('testSubjects') && getService('testSubjects'); + const kibanaServer = getService('kibanaServer'); + + const enabledPlugins = config.get('security.disableTestUser') + ? [] + : await kibanaServer.plugins.getEnabledIds(); + const isEnabled = () => { + return enabledPlugins.includes('security') && !config.get('security.disableTestUser'); + }; + if (isEnabled()) { + log.debug('===============creating roles and users==============='); + for (const [name, definition] of Object.entries(config.get('security.roles'))) { + // create the defined roles (need to map array to create roles) + await role.create(name, definition); + } + try { + // delete the test_user if present (will it error if the user doesn't exist?) + await user.delete('test_user'); + } catch (exception) { + log.debug('no test user to delete'); + } + + // create test_user with username and pwd + log.debug(`default roles = ${config.get('security.defaultRoles')}`); + await user.create('test_user', { + password: 'changeme', + roles: config.get('security.defaultRoles'), + full_name: 'test user', + }); + } + + return new (class TestUser { + async restoreDefaults() { + if (isEnabled()) { + await this.setRoles(config.get('security.defaultRoles')); + } + } + + async setRoles(roles: string[]) { + if (isEnabled()) { + log.debug(`set roles = ${roles}`); + await user.create('test_user', { + password: 'changeme', + roles, + full_name: 'test user', + }); + + if (browser && testSubjects) { + if (await testSubjects.exists('kibanaChrome', { allowHidden: true })) { + await browser.refresh(); + await testSubjects.find('kibanaChrome', config.get('timeouts.find') * 10); + } + } + } + } + })(); +} diff --git a/test/functional/apps/context/_date_nanos.js b/test/functional/apps/context/_date_nanos.js index d4acdb0b4d5c0..bd132e3745caa 100644 --- a/test/functional/apps/context/_date_nanos.js +++ b/test/functional/apps/context/_date_nanos.js @@ -26,11 +26,13 @@ const TEST_STEP_SIZE = 3; export default function({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const docTable = getService('docTable'); + const security = getService('security'); const PageObjects = getPageObjects(['common', 'context', 'timePicker', 'discover']); const esArchiver = getService('esArchiver'); describe('context view for date_nanos', () => { before(async function() { + await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos']); await esArchiver.loadIfNeeded('date_nanos'); await kibanaServer.uiSettings.replace({ defaultIndex: TEST_INDEX_PATTERN }); await kibanaServer.uiSettings.update({ @@ -39,8 +41,9 @@ export default function({ getService, getPageObjects }) { }); }); - after(function unloadMakelogs() { - return esArchiver.unload('date_nanos'); + after(async function unloadMakelogs() { + await security.testUser.restoreDefaults(); + await esArchiver.unload('date_nanos'); }); it('displays predessors - anchor - successors in right order ', async function() { diff --git a/test/functional/apps/context/_date_nanos_custom_timestamp.js b/test/functional/apps/context/_date_nanos_custom_timestamp.js index 046cca0aba8c6..7834b29931a65 100644 --- a/test/functional/apps/context/_date_nanos_custom_timestamp.js +++ b/test/functional/apps/context/_date_nanos_custom_timestamp.js @@ -26,12 +26,14 @@ const TEST_STEP_SIZE = 3; export default function({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const docTable = getService('docTable'); + const security = getService('security'); const PageObjects = getPageObjects(['common', 'context', 'timePicker', 'discover']); const esArchiver = getService('esArchiver'); // skipped due to a recent change in ES that caused search_after queries with data containing // custom timestamp formats like in the testdata to fail describe.skip('context view for date_nanos with custom timestamp', () => { before(async function() { + await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos_custom']); await esArchiver.loadIfNeeded('date_nanos_custom'); await kibanaServer.uiSettings.replace({ defaultIndex: TEST_INDEX_PATTERN }); await kibanaServer.uiSettings.update({ @@ -40,10 +42,6 @@ export default function({ getService, getPageObjects }) { }); }); - after(function unloadMakelogs() { - return esArchiver.unload('date_nanos_custom'); - }); - it('displays predessors - anchor - successors in right order ', async function() { await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, '1'); const actualRowsText = await docTable.getRowsText(); @@ -54,5 +52,10 @@ export default function({ getService, getPageObjects }) { ]; expect(actualRowsText).to.eql(expectedRowsText); }); + + after(async function() { + await security.testUser.restoreDefaults(); + await esArchiver.unload('date_nanos_custom'); + }); }); } diff --git a/test/functional/apps/dashboard/dashboard_filtering.js b/test/functional/apps/dashboard/dashboard_filtering.js index ec8a48ca74911..f388993dcaf7d 100644 --- a/test/functional/apps/dashboard/dashboard_filtering.js +++ b/test/functional/apps/dashboard/dashboard_filtering.js @@ -33,6 +33,7 @@ export default function({ getService, getPageObjects }) { const filterBar = getService('filterBar'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); + const security = getService('security'); const dashboardPanelActions = getService('dashboardPanelActions'); const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'visualize', 'timePicker']); @@ -41,6 +42,7 @@ export default function({ getService, getPageObjects }) { before(async () => { await esArchiver.load('dashboard/current/kibana'); + await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader', 'animals']); await kibanaServer.uiSettings.replace({ defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', }); @@ -49,6 +51,10 @@ export default function({ getService, getPageObjects }) { await PageObjects.dashboard.gotoDashboardLandingPage(); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + describe('adding a filter that excludes all data', () => { before(async () => { await PageObjects.dashboard.clickNewDashboard(); diff --git a/test/functional/apps/dashboard/index.js b/test/functional/apps/dashboard/index.js index 13e8631445393..5e96a55b19014 100644 --- a/test/functional/apps/dashboard/index.js +++ b/test/functional/apps/dashboard/index.js @@ -23,6 +23,7 @@ export default function({ getService, loadTestFile }) { async function loadCurrentData() { await browser.setWindowSize(1300, 900); + await esArchiver.unload('logstash_functional'); await esArchiver.loadIfNeeded('dashboard/current/data'); } diff --git a/test/functional/apps/dashboard/time_zones.js b/test/functional/apps/dashboard/time_zones.js index f374d6526fcf1..b7698a7d6ac4b 100644 --- a/test/functional/apps/dashboard/time_zones.js +++ b/test/functional/apps/dashboard/time_zones.js @@ -22,7 +22,6 @@ import expect from '@kbn/expect'; export default function({ getService, getPageObjects }) { const pieChart = getService('pieChart'); - const browser = getService('browser'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['dashboard', 'timePicker', 'settings', 'common']); @@ -48,7 +47,6 @@ export default function({ getService, getPageObjects }) { after(async () => { await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'UTC' }); - await browser.refresh(); }); it('Exported dashboard adjusts EST time to UTC', async () => { diff --git a/test/functional/apps/discover/_date_nanos.js b/test/functional/apps/discover/_date_nanos.js index 9b06b9ac84cfd..99a37cc18feaa 100644 --- a/test/functional/apps/discover/_date_nanos.js +++ b/test/functional/apps/discover/_date_nanos.js @@ -23,6 +23,7 @@ export default function({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['common', 'timePicker', 'discover']); const kibanaServer = getService('kibanaServer'); + const security = getService('security'); const fromTime = 'Sep 22, 2019 @ 20:31:44.000'; const toTime = 'Sep 23, 2019 @ 03:31:44.000'; @@ -30,12 +31,14 @@ export default function({ getService, getPageObjects }) { before(async function() { await esArchiver.loadIfNeeded('date_nanos'); await kibanaServer.uiSettings.replace({ defaultIndex: 'date-nanos' }); + await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos']); await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); }); - after(function unloadMakelogs() { - return esArchiver.unload('date_nanos'); + after(async function unloadMakelogs() { + await security.testUser.restoreDefaults(); + await esArchiver.unload('date_nanos'); }); it('should show a timestamp with nanoseconds in the first result row', async function() { diff --git a/test/functional/apps/discover/_date_nanos_mixed.js b/test/functional/apps/discover/_date_nanos_mixed.js index 0bb6848db4d10..b88ae87601cc5 100644 --- a/test/functional/apps/discover/_date_nanos_mixed.js +++ b/test/functional/apps/discover/_date_nanos_mixed.js @@ -23,6 +23,7 @@ export default function({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['common', 'timePicker', 'discover']); const kibanaServer = getService('kibanaServer'); + const security = getService('security'); const fromTime = 'Jan 1, 2019 @ 00:00:00.000'; const toTime = 'Jan 1, 2019 @ 23:59:59.999'; @@ -30,12 +31,14 @@ export default function({ getService, getPageObjects }) { before(async function() { await esArchiver.loadIfNeeded('date_nanos_mixed'); await kibanaServer.uiSettings.replace({ defaultIndex: 'timestamp-*' }); + await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos_mixed']); await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); }); - after(function unloadMakelogs() { - return esArchiver.unload('date_nanos_mixed'); + after(async () => { + await security.testUser.restoreDefaults(); + esArchiver.unload('date_nanos_mixed'); }); it('shows a list of records of indices with date & date_nanos fields in the right order', async function() { diff --git a/test/functional/apps/discover/_discover_histogram.js b/test/functional/apps/discover/_discover_histogram.js index 9310838666256..f815c505a8c27 100644 --- a/test/functional/apps/discover/_discover_histogram.js +++ b/test/functional/apps/discover/_discover_histogram.js @@ -25,6 +25,7 @@ export default function({ getService, getPageObjects }) { const browser = getService('browser'); const elasticChart = getService('elasticChart'); const kibanaServer = getService('kibanaServer'); + const security = getService('security'); const PageObjects = getPageObjects(['settings', 'common', 'discover', 'header', 'timePicker']); const defaultSettings = { defaultIndex: 'long-window-logstash-*', @@ -35,6 +36,11 @@ export default function({ getService, getPageObjects }) { before(async function() { log.debug('load kibana index with default index pattern'); await PageObjects.common.navigateToApp('home'); + await security.testUser.setRoles([ + 'kibana_admin', + 'test_logstash_reader', + 'long_window_logstash', + ]); await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('long_window_logstash'); await esArchiver.load('visualize'); @@ -56,6 +62,7 @@ export default function({ getService, getPageObjects }) { await esArchiver.unload('long_window_logstash'); await esArchiver.unload('visualize'); await esArchiver.unload('discover'); + await security.testUser.restoreDefaults(); }); it('should visualize monthly data with different day intervals', async () => { diff --git a/test/functional/apps/discover/_large_string.js b/test/functional/apps/discover/_large_string.js index a5052b2403074..5e9048e2bc481 100644 --- a/test/functional/apps/discover/_large_string.js +++ b/test/functional/apps/discover/_large_string.js @@ -25,10 +25,12 @@ export default function({ getService, getPageObjects }) { const retry = getService('retry'); const kibanaServer = getService('kibanaServer'); const queryBar = getService('queryBar'); + const security = getService('security'); const PageObjects = getPageObjects(['common', 'home', 'settings', 'discover']); describe('test large strings', function() { before(async function() { + await security.testUser.setRoles(['kibana_admin', 'kibana_large_strings']); await esArchiver.load('empty_kibana'); await esArchiver.loadIfNeeded('hamlet'); await kibanaServer.uiSettings.replace({ defaultIndex: 'testlargestring' }); @@ -77,6 +79,7 @@ export default function({ getService, getPageObjects }) { }); after(async () => { + await security.testUser.restoreDefaults(); await esArchiver.unload('hamlet'); }); }); diff --git a/test/functional/apps/getting_started/_shakespeare.js b/test/functional/apps/getting_started/_shakespeare.js index 5af1676cf423f..ded4eca908410 100644 --- a/test/functional/apps/getting_started/_shakespeare.js +++ b/test/functional/apps/getting_started/_shakespeare.js @@ -23,6 +23,7 @@ export default function({ getService, getPageObjects }) { const log = getService('log'); const esArchiver = getService('esArchiver'); const retry = getService('retry'); + const security = getService('security'); const PageObjects = getPageObjects([ 'console', 'common', @@ -46,11 +47,16 @@ export default function({ getService, getPageObjects }) { 'Load empty_kibana and Shakespeare Getting Started data\n' + 'https://www.elastic.co/guide/en/kibana/current/tutorial-load-dataset.html' ); + await security.testUser.setRoles(['kibana_admin', 'test_shakespeare_reader']); await esArchiver.load('empty_kibana', { skipExisting: true }); log.debug('Load shakespeare data'); await esArchiver.loadIfNeeded('getting_started/shakespeare'); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should create shakespeare index pattern', async function() { log.debug('Create shakespeare index pattern'); await PageObjects.settings.createIndexPattern('shakes', null); diff --git a/test/functional/apps/home/_sample_data.ts b/test/functional/apps/home/_sample_data.ts index 8bc528e045566..5812b9b96e42a 100644 --- a/test/functional/apps/home/_sample_data.ts +++ b/test/functional/apps/home/_sample_data.ts @@ -25,6 +25,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const find = getService('find'); const log = getService('log'); + const security = getService('security'); const pieChart = getService('pieChart'); const renderable = getService('renderable'); const dashboardExpect = getService('dashboardExpect'); @@ -34,10 +35,15 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { this.tags('smoke'); before(async () => { + await security.testUser.setRoles(['kibana_admin', 'kibana_sample_admin']); await PageObjects.common.navigateToUrl('home', 'tutorial_directory/sampleData'); await PageObjects.header.waitUntilLoadingHasFinished(); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should display registered flights sample data sets', async () => { await retry.try(async () => { const exists = await PageObjects.home.doesSampleDataSetExist('flights'); diff --git a/test/functional/apps/management/_handle_alias.js b/test/functional/apps/management/_handle_alias.js index 55f6b56d9f0d1..4ef02f6c9e873 100644 --- a/test/functional/apps/management/_handle_alias.js +++ b/test/functional/apps/management/_handle_alias.js @@ -23,11 +23,13 @@ export default function({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const es = getService('legacyEs'); const retry = getService('retry'); + const security = getService('security'); const PageObjects = getPageObjects(['common', 'home', 'settings', 'discover', 'timePicker']); // FLAKY: https://github.com/elastic/kibana/issues/59717 describe.skip('Index patterns on aliases', function() { before(async function() { + await security.testUser.setRoles(['kibana_admin', 'test_alias_reader']); await esArchiver.loadIfNeeded('alias'); await esArchiver.load('empty_kibana'); await es.indices.updateAliases({ @@ -84,6 +86,7 @@ export default function({ getService, getPageObjects }) { }); after(async () => { + await security.testUser.restoreDefaults(); await esArchiver.unload('alias'); }); }); diff --git a/test/functional/apps/management/_test_huge_fields.js b/test/functional/apps/management/_test_huge_fields.js index 643cbcbe89482..bc280e51ae048 100644 --- a/test/functional/apps/management/_test_huge_fields.js +++ b/test/functional/apps/management/_test_huge_fields.js @@ -21,6 +21,7 @@ import expect from '@kbn/expect'; export default function({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); + const security = getService('security'); const PageObjects = getPageObjects(['common', 'home', 'settings']); describe('test large number of fields', function() { @@ -28,6 +29,7 @@ export default function({ getService, getPageObjects }) { const EXPECTED_FIELD_COUNT = '10006'; before(async function() { + await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader']); await esArchiver.loadIfNeeded('large_fields'); await PageObjects.settings.createIndexPattern('testhuge', 'date'); }); @@ -38,6 +40,7 @@ export default function({ getService, getPageObjects }) { }); after(async () => { + await security.testUser.restoreDefaults(); await esArchiver.unload('large_fields'); }); }); diff --git a/test/functional/apps/visualize/_area_chart.js b/test/functional/apps/visualize/_area_chart.js index 101b2d4f547dd..bf836cfe778b4 100644 --- a/test/functional/apps/visualize/_area_chart.js +++ b/test/functional/apps/visualize/_area_chart.js @@ -24,6 +24,7 @@ export default function({ getService, getPageObjects }) { const inspector = getService('inspector'); const browser = getService('browser'); const retry = getService('retry'); + const security = getService('security'); const PageObjects = getPageObjects([ 'common', 'visualize', @@ -58,7 +59,14 @@ export default function({ getService, getPageObjects }) { return PageObjects.visEditor.clickGo(); }; - before(initAreaChart); + before(async function() { + await security.testUser.setRoles([ + 'kibana_admin', + 'long_window_logstash', + 'test_logstash_reader', + ]); + await initAreaChart(); + }); it('should save and load with special characters', async function() { const vizNamewithSpecialChars = vizName1 + '/?&=%'; @@ -284,6 +292,7 @@ export default function({ getService, getPageObjects }) { .pop() .replace('embed=true', ''); await PageObjects.common.navigateToUrl('visualize', embedUrl); + await security.testUser.restoreDefaults(); }); }); diff --git a/test/functional/apps/visualize/_experimental_vis.js b/test/functional/apps/visualize/_experimental_vis.js index 2ce15cf913eff..c45a95abab86e 100644 --- a/test/functional/apps/visualize/_experimental_vis.js +++ b/test/functional/apps/visualize/_experimental_vis.js @@ -23,7 +23,7 @@ export default ({ getService, getPageObjects }) => { const log = getService('log'); const PageObjects = getPageObjects(['visualize']); - describe('visualize app', function() { + describe('experimental visualizations in visualize app ', function() { this.tags('smoke'); describe('experimental visualizations', () => { diff --git a/test/functional/apps/visualize/_linked_saved_searches.ts b/test/functional/apps/visualize/_linked_saved_searches.ts index 345987a803394..ea42f7c671985 100644 --- a/test/functional/apps/visualize/_linked_saved_searches.ts +++ b/test/functional/apps/visualize/_linked_saved_searches.ts @@ -32,7 +32,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { 'visChart', ]); - describe('visualize app', function describeIndexTests() { + describe('saved search visualizations from visualize app', function describeIndexTests() { describe('linked saved searched', () => { const savedSearchName = 'vis_saved_search'; diff --git a/test/functional/apps/visualize/_markdown_vis.js b/test/functional/apps/visualize/_markdown_vis.js index fee6c074af5d2..649fe0a8e4c2e 100644 --- a/test/functional/apps/visualize/_markdown_vis.js +++ b/test/functional/apps/visualize/_markdown_vis.js @@ -29,7 +29,7 @@ export default function({ getPageObjects, getService }) {

Inline HTML that should not be rendered as html

`; - describe('visualize app', () => { + describe('markdown app in visualize app', () => { before(async function() { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickMarkdownWidget(); diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index 6a4bed3ba5892..867db66ac81dc 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -25,11 +25,13 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const log = getService('log'); const inspector = getService('inspector'); + const security = getService('security'); const PageObjects = getPageObjects(['visualize', 'visualBuilder', 'timePicker', 'visChart']); describe('visual builder', function describeIndexTests() { this.tags('smoke'); beforeEach(async () => { + await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisualBuilder(); await PageObjects.visualBuilder.checkVisualBuilderIsPresent(); @@ -111,8 +113,10 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visualBuilder.resetPage(); await PageObjects.visualBuilder.clickMetric(); await PageObjects.visualBuilder.checkMetricTabIsPresent(); + await security.testUser.setRoles(['kibana_admin', 'kibana_sample_admin']); }); after(async () => { + await security.testUser.restoreDefaults(); await esArchiver.unload('kibana_sample_data_flights'); }); diff --git a/test/functional/apps/visualize/_vega_chart.js b/test/functional/apps/visualize/_vega_chart.js index df0603c7f95f5..7a19bde341cdd 100644 --- a/test/functional/apps/visualize/_vega_chart.js +++ b/test/functional/apps/visualize/_vega_chart.js @@ -25,7 +25,7 @@ export default function({ getService, getPageObjects }) { const inspector = getService('inspector'); const log = getService('log'); - describe('visualize app', () => { + describe('vega chart in visualize app', () => { before(async () => { log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewVisualization(); diff --git a/test/functional/apps/visualize/input_control_vis/input_control_range.ts b/test/functional/apps/visualize/input_control_vis/input_control_range.ts index f48ba7b54daf1..8f079f5cc430d 100644 --- a/test/functional/apps/visualize/input_control_vis/input_control_range.ts +++ b/test/functional/apps/visualize/input_control_vis/input_control_range.ts @@ -25,10 +25,12 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const find = getService('find'); + const security = getService('security'); const { visualize, visEditor } = getPageObjects(['visualize', 'visEditor']); describe('input control range', () => { before(async () => { + await security.testUser.setRoles(['kibana_admin', 'kibana_sample_admin']); await esArchiver.load('kibana_sample_data_flights_index_pattern'); await visualize.navigateToNewVisualization(); await visualize.clickInputControlVis(); @@ -63,6 +65,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.loadIfNeeded('long_window_logstash'); await esArchiver.load('visualize'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); + await security.testUser.restoreDefaults(); }); }); } diff --git a/test/functional/config.js b/test/functional/config.js index e84b7e0a98a68..11399bd6187c8 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -103,5 +103,172 @@ export default async function({ readConfigFile }) { browser: { type: 'chrome', }, + + security: { + roles: { + test_logstash_reader: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['logstash*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + test_shakespeare_reader: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['shakes*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + test_testhuge_reader: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['testhuge*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + test_alias_reader: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['alias*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + //for sample data - can remove but not add sample data.( not ml)- for ml use built in role. + kibana_sample_admin: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['kibana_sample*'], + privileges: ['read', 'view_index_metadata', 'manage', 'create_index', 'index'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + + kibana_date_nanos: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['date-nanos'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + + kibana_date_nanos_custom: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['date_nanos_custom_timestamp'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + + kibana_date_nanos_mixed: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['date_nanos_mixed', 'timestamp-*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + + kibana_large_strings: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['testlargestring'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + + long_window_logstash: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['long-window-logstash-*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + + animals: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['animals-*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + }, + defaultRoles: ['test_logstash_reader', 'kibana_admin'], + }, }; } diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 60966511c1f99..5ee3726ddb44f 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -105,13 +105,16 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo const wantedLoginPage = appUrl.includes('/login') || appUrl.includes('/logout'); if (loginPage && !wantedLoginPage) { - log.debug( - `Found login page. Logging in with username = ${config.get('servers.kibana.username')}` - ); - await PageObjects.shield.login( - config.get('servers.kibana.username'), - config.get('servers.kibana.password') - ); + log.debug('Found login page'); + if (config.get('security.disableTestUser')) { + await PageObjects.shield.login( + config.get('servers.kibana.username'), + config.get('servers.kibana.password') + ); + } else { + await PageObjects.shield.login('test_user', 'changeme'); + } + await find.byCssSelector( '[data-test-subj="kibanaChrome"] nav:not(.ng-hide)', 6 * defaultFindTimeout diff --git a/test/functional/services/browser.ts b/test/functional/services/browser.ts index 02349b4e6cca2..5017947e95d03 100644 --- a/test/functional/services/browser.ts +++ b/test/functional/services/browser.ts @@ -21,6 +21,7 @@ import { cloneDeep } from 'lodash'; import { Key, Origin } from 'selenium-webdriver'; // @ts-ignore internal modules are not typed import { LegacyActionSequence } from 'selenium-webdriver/lib/actions'; +import { ProvidedType } from '@kbn/test/types/ftr'; import Jimp from 'jimp'; import { modifyUrl } from '../../../src/core/utils'; @@ -28,6 +29,7 @@ import { WebElementWrapper } from './lib/web_element_wrapper'; import { FtrProviderContext } from '../ftr_provider_context'; import { Browsers } from './remote/browsers'; +export type Browser = ProvidedType; export async function BrowserProvider({ getService }: FtrProviderContext) { const log = getService('log'); const { driver, browserType } = await getService('__webdriver__').init(); diff --git a/test/functional/services/test_subjects.ts b/test/functional/services/test_subjects.ts index d47b838c8d72a..e5c2e61c48a0b 100644 --- a/test/functional/services/test_subjects.ts +++ b/test/functional/services/test_subjects.ts @@ -19,6 +19,7 @@ import testSubjSelector from '@kbn/test-subj-selector'; import { map as mapAsync } from 'bluebird'; +import { ProvidedType } from '@kbn/test/types/ftr'; import { WebElementWrapper } from './lib/web_element_wrapper'; import { FtrProviderContext } from '../ftr_provider_context'; @@ -32,6 +33,7 @@ interface SetValueOptions { typeCharByChar?: boolean; } +export type TestSubjects = ProvidedType; export function TestSubjectsProvider({ getService }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx index 144954800c91f..54d13efe4d790 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx @@ -19,18 +19,15 @@ import { EuiTab } from '@elastic/eui'; import React, { Component } from 'react'; import { CoreStart } from 'src/core/public'; -import { - GetEmbeddableFactory, - GetEmbeddableFactories, -} from 'src/legacy/core_plugins/embeddable_api/public/np_ready/public'; +import { EmbeddableStart } from 'src/plugins/embeddable/public'; import { UiActionsService } from '../../../../../../../../src/plugins/ui_actions/public'; import { DashboardContainerExample } from './dashboard_container_example'; import { Start as InspectorStartContract } from '../../../../../../../../src/plugins/inspector/public'; export interface AppProps { getActions: UiActionsService['getTriggerCompatibleActions']; - getEmbeddableFactory: GetEmbeddableFactory; - getAllEmbeddableFactories: GetEmbeddableFactories; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; inspector: InspectorStartContract; diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx index 7cc9c1df1c948..f8625e4490e51 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx @@ -18,18 +18,19 @@ */ import React from 'react'; import { EuiButton, EuiLoadingChart } from '@elastic/eui'; +import { ContainerOutput } from 'src/plugins/embeddable/public'; import { ErrorEmbeddable, ViewMode, isErrorEmbeddable, EmbeddablePanel, - GetEmbeddableFactory, - GetEmbeddableFactories, + EmbeddableStart, } from '../embeddable_api'; import { DASHBOARD_CONTAINER_TYPE, DashboardContainer, DashboardContainerFactory, + DashboardContainerInput, } from '../../../../../../../../src/plugins/dashboard/public'; import { CoreStart } from '../../../../../../../../src/core/public'; @@ -39,8 +40,8 @@ import { UiActionsService } from '../../../../../../../../src/plugins/ui_actions interface Props { getActions: UiActionsService['getTriggerCompatibleActions']; - getEmbeddableFactory: GetEmbeddableFactory; - getAllEmbeddableFactories: GetEmbeddableFactories; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; inspector: InspectorStartContract; @@ -67,9 +68,10 @@ export class DashboardContainerExample extends React.Component { public async componentDidMount() { this.mounted = true; - const dashboardFactory = this.props.getEmbeddableFactory( - DASHBOARD_CONTAINER_TYPE - ) as DashboardContainerFactory; + const dashboardFactory = this.props.getEmbeddableFactory< + DashboardContainerInput, + ContainerOutput + >(DASHBOARD_CONTAINER_TYPE) as DashboardContainerFactory; if (dashboardFactory) { this.container = await dashboardFactory.create(dashboardInput); if (this.mounted) { diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/embeddables/hello_world_embeddable_factory.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/embeddables/hello_world_embeddable_factory.ts deleted file mode 100644 index 0c90cb3b85867..0000000000000 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/embeddables/hello_world_embeddable_factory.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// eslint-disable-next-line -import { npSetup } from '../../../../../../../../src/legacy/ui/public/new_platform'; -// eslint-disable-next-line -import { HelloWorldEmbeddableFactory, HELLO_WORLD_EMBEDDABLE } from '../../../../../../../../examples/embeddable_examples/public'; - -npSetup.plugins.embeddable.registerEmbeddableFactory( - HELLO_WORLD_EMBEDDABLE, - new HelloWorldEmbeddableFactory() -); diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx index 25666dc0359d9..18ceec652392d 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx @@ -31,21 +31,16 @@ import { CONTEXT_MENU_TRIGGER } from './embeddable_api'; const REACT_ROOT_ID = 'embeddableExplorerRoot'; -import { - SayHelloAction, - createSendMessageAction, - ContactCardEmbeddableFactory, -} from './embeddable_api'; +import { SayHelloAction, createSendMessageAction } from './embeddable_api'; import { App } from './app'; import { getSavedObjectFinder } from '../../../../../../../src/plugins/saved_objects/public'; -import { HelloWorldEmbeddableFactory } from '../../../../../../../examples/embeddable_examples/public'; import { - IEmbeddableStart, - IEmbeddableSetup, + EmbeddableStart, + EmbeddableSetup, } from '.../../../../../../../src/plugins/embeddable/public'; export interface SetupDependencies { - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; inspector: InspectorSetupContract; __LEGACY: { ExitFullScreenButton: React.ComponentType; @@ -53,7 +48,7 @@ export interface SetupDependencies { } interface StartDependencies { - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; uiActions: UiActionsStart; inspector: InspectorStartContract; __LEGACY: { @@ -74,12 +69,6 @@ export class EmbeddableExplorerPublicPlugin const helloWorldAction = createHelloWorldAction(core.overlays); const sayHelloAction = new SayHelloAction(alert); const sendMessageAction = createSendMessageAction(core.overlays); - const helloWorldEmbeddableFactory = new HelloWorldEmbeddableFactory(); - const contactCardEmbeddableFactory = new ContactCardEmbeddableFactory( - {}, - plugins.uiActions.executeTriggerActions, - core.overlays - ); plugins.uiActions.registerAction(helloWorldAction); plugins.uiActions.registerAction(sayHelloAction); @@ -87,15 +76,6 @@ export class EmbeddableExplorerPublicPlugin plugins.uiActions.attachAction(CONTEXT_MENU_TRIGGER, helloWorldAction); - plugins.embeddable.registerEmbeddableFactory( - helloWorldEmbeddableFactory.type, - helloWorldEmbeddableFactory - ); - plugins.embeddable.registerEmbeddableFactory( - contactCardEmbeddableFactory.type, - contactCardEmbeddableFactory - ); - plugins.__LEGACY.onRenderComplete(() => { const root = document.getElementById(REACT_ROOT_ID); ReactDOM.render( diff --git a/test/plugin_functional/plugins/rendering_plugin/server/plugin.ts b/test/plugin_functional/plugins/rendering_plugin/server/plugin.ts index fad19728b7514..3f6a8e8773e04 100644 --- a/test/plugin_functional/plugins/rendering_plugin/server/plugin.ts +++ b/test/plugin_functional/plugins/rendering_plugin/server/plugin.ts @@ -33,7 +33,7 @@ export class RenderingPlugin implements Plugin { { includeUserSettings: schema.boolean({ defaultValue: true }), }, - { allowUnknowns: true } + { unknowns: 'allow' } ), params: schema.object({ id: schema.maybe(schema.string()), diff --git a/test/plugin_functional/plugins/ui_settings_plugin/server/plugin.ts b/test/plugin_functional/plugins/ui_settings_plugin/server/plugin.ts index c32e8a75d95da..3801d3bbce055 100644 --- a/test/plugin_functional/plugins/ui_settings_plugin/server/plugin.ts +++ b/test/plugin_functional/plugins/ui_settings_plugin/server/plugin.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import { schema } from '@kbn/config-schema'; import { Plugin, CoreSetup } from 'kibana/server'; export class UiSettingsPlugin implements Plugin { @@ -27,6 +27,7 @@ export class UiSettingsPlugin implements Plugin { description: 'just for testing', value: '2', category: ['any'], + schema: schema.string(), }, }); diff --git a/test/plugin_functional/test_suites/embeddable_explorer/dashboard_container.js b/test/plugin_functional/test_suites/embeddable_explorer/dashboard_container.js index 203378e547c8a..4a1bcecc0d5a1 100644 --- a/test/plugin_functional/test_suites/embeddable_explorer/dashboard_container.js +++ b/test/plugin_functional/test_suites/embeddable_explorer/dashboard_container.js @@ -17,11 +17,8 @@ * under the License. */ -import expect from '@kbn/expect'; - export default function({ getService }) { const testSubjects = getService('testSubjects'); - const retry = getService('retry'); const pieChart = getService('pieChart'); const dashboardExpect = getService('dashboardExpect'); @@ -30,17 +27,6 @@ export default function({ getService }) { await testSubjects.click('embedExplorerTab-dashboardContainer'); }); - it('hello world embeddable renders', async () => { - await retry.try(async () => { - const text = await testSubjects.getVisibleText('helloWorldEmbeddable'); - expect(text).to.be('HELLO WORLD!'); - }); - }); - - it('contact card embeddable renders', async () => { - await testSubjects.existOrFail('embeddablePanelHeading-HelloSue'); - }); - it('pie charts', async () => { await pieChart.expectPieSliceCount(5); }); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx index 31fc4db8f1a2f..cff190cd98a11 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx @@ -209,7 +209,7 @@ export function MachineLearningFlyoutView({ {i18n.translate( 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createNewJobButtonLabel', { - defaultMessage: 'Create new job' + defaultMessage: 'Create job' } )} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx index 2942ce64729e7..7bbb77a49c84b 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButton } from '@elastic/eui'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import { ElementDefinition } from 'cytoscape'; @@ -16,7 +15,6 @@ import React, { useRef, useState } from 'react'; -import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; import { isValidPlatinumLicense } from '../../../../../../../plugins/apm/common/service_map'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ServiceMapAPIResponse } from '../../../../../../../plugins/apm/server/lib/service_map/get_service_map'; @@ -78,7 +76,6 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { }); const renderedElements = useRef([]); - const openToast = useRef(null); const [responses, setResponses] = useState([]); @@ -160,41 +157,11 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { return !find(renderedElements.current, el => isEqual(el, element)); }); - const updateMap = () => { + if (newElements.length > 0 && renderedElements.current.length > 0) { renderedElements.current = elements; - if (openToast.current) { - notifications.toasts.remove(openToast.current); - } forceUpdate(); - }; - - if (newElements.length > 0 && renderedElements.current.length > 0) { - openToast.current = notifications.toasts.add({ - title: i18n.translate('xpack.apm.newServiceMapData', { - defaultMessage: `Newly discovered connections are available.` - }), - onClose: () => { - openToast.current = null; - }, - toastLifeTimeMs: 24 * 60 * 60 * 1000, - text: toMountPoint( - - {i18n.translate('xpack.apm.updateServiceMap', { - defaultMessage: 'Update map' - })} - - ) - }).id; } - - return () => { - if (openToast.current) { - notifications.toasts.remove(openToast.current); - } - }; - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [elements]); + }, [elements, forceUpdate]); const { ref: wrapperRef, width, height } = useRefDimensions(); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts index 7f81adad6bf9b..949264fcc9fdb 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts @@ -6,7 +6,7 @@ import { connect } from 'react-redux'; import { compose, withProps } from 'recompose'; -import * as jobCompletionNotifications from '../../../../../reporting/public/lib/job_completion_notifications'; +import { jobCompletionNotifications } from '../../../../../../../plugins/reporting/public'; // @ts-ignore Untyped local import { getWorkpad, getPages } from '../../../state/selectors/workpad'; // @ts-ignore Untyped local diff --git a/x-pack/legacy/plugins/graph/index.ts b/x-pack/legacy/plugins/graph/index.ts index b2d6fd3957d64..5122796335e45 100644 --- a/x-pack/legacy/plugins/graph/index.ts +++ b/x-pack/legacy/plugins/graph/index.ts @@ -4,31 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resolve } from 'path'; import { i18n } from '@kbn/i18n'; // @ts-ignore import migrations from './migrations'; import mappings from './mappings.json'; import { LegacyPluginInitializer } from '../../../../src/legacy/plugin_discovery/types'; -import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; export const graph: LegacyPluginInitializer = kibana => { return new kibana.Plugin({ id: 'graph', configPrefix: 'xpack.graph', - publicDir: resolve(__dirname, 'public'), require: ['kibana', 'elasticsearch', 'xpack_main'], uiExports: { - app: { - title: 'Graph', - order: 9000, - icon: 'plugins/graph/icon.png', - euiIconType: 'graphApp', - main: 'plugins/graph/index', - category: DEFAULT_APP_CATEGORIES.analyze, - }, - styleSheetPaths: resolve(__dirname, 'public/index.scss'), mappings, migrations, }, diff --git a/x-pack/legacy/plugins/graph/public/icon.png b/x-pack/legacy/plugins/graph/public/icon.png deleted file mode 100644 index f2a16437209c7..0000000000000 Binary files a/x-pack/legacy/plugins/graph/public/icon.png and /dev/null differ diff --git a/x-pack/legacy/plugins/graph/public/index.ts b/x-pack/legacy/plugins/graph/public/index.ts deleted file mode 100644 index fb60a66fb28cc..0000000000000 --- a/x-pack/legacy/plugins/graph/public/index.ts +++ /dev/null @@ -1,26 +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 { npSetup, npStart } from 'ui/new_platform'; -import { LicensingPluginSetup } from '../../../../plugins/licensing/public'; -import { GraphPlugin } from './plugin'; -import { GraphSetup } from '../../../../plugins/graph/public'; - -type XpackNpSetupDeps = typeof npSetup.plugins & { - licensing: LicensingPluginSetup; - graph: GraphSetup; -}; - -(async () => { - const instance = new GraphPlugin(); - instance.setup(npSetup.core, { - ...(npSetup.plugins as XpackNpSetupDeps), - }); - instance.start(npStart.core, { - npData: npStart.plugins.data, - navigation: npStart.plugins.navigation, - }); -})(); diff --git a/x-pack/legacy/plugins/graph/public/plugin.ts b/x-pack/legacy/plugins/graph/public/plugin.ts deleted file mode 100644 index 4ccaf6b5dfa27..0000000000000 --- a/x-pack/legacy/plugins/graph/public/plugin.ts +++ /dev/null @@ -1,75 +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. - */ - -// NP type imports -import { - AppMountParameters, - CoreSetup, - CoreStart, - Plugin, - SavedObjectsClientContract, -} from 'src/core/public'; -import { Plugin as DataPlugin } from 'src/plugins/data/public'; -import { Storage } from '../../../../../src/plugins/kibana_utils/public'; -import { LicensingPluginSetup } from '../../../../plugins/licensing/public'; -import { NavigationPublicPluginStart as NavigationStart } from '../../../../../src/plugins/navigation/public'; -import { initAngularBootstrap } from '../../../../../src/plugins/kibana_legacy/public'; -import { GraphSetup } from '../../../../plugins/graph/public'; - -export interface GraphPluginStartDependencies { - npData: ReturnType; - navigation: NavigationStart; -} - -export interface GraphPluginSetupDependencies { - licensing: LicensingPluginSetup; - graph: GraphSetup; -} - -export class GraphPlugin implements Plugin { - private navigationStart: NavigationStart | null = null; - private npDataStart: ReturnType | null = null; - private savedObjectsClient: SavedObjectsClientContract | null = null; - - setup(core: CoreSetup, { licensing, graph }: GraphPluginSetupDependencies) { - initAngularBootstrap(); - core.application.register({ - id: 'graph', - title: 'Graph', - mount: async (params: AppMountParameters) => { - const [coreStart] = await core.getStartServices(); - const { renderApp } = await import('./application'); - return renderApp({ - ...params, - licensing, - navigation: this.navigationStart!, - npData: this.npDataStart!, - savedObjectsClient: this.savedObjectsClient!, - addBasePath: core.http.basePath.prepend, - getBasePath: core.http.basePath.get, - canEditDrillDownUrls: graph.config.canEditDrillDownUrls, - graphSavePolicy: graph.config.savePolicy, - storage: new Storage(window.localStorage), - capabilities: coreStart.application.capabilities.graph, - coreStart, - chrome: coreStart.chrome, - config: coreStart.uiSettings, - toastNotifications: coreStart.notifications.toasts, - indexPatterns: this.npDataStart!.indexPatterns, - overlays: coreStart.overlays, - }); - }, - }); - } - - start(core: CoreStart, { npData, navigation }: GraphPluginStartDependencies) { - this.navigationStart = navigation; - this.npDataStart = npData; - this.savedObjectsClient = core.savedObjects.client; - } - - stop() {} -} diff --git a/x-pack/legacy/plugins/lens/public/_config_panel.scss b/x-pack/legacy/plugins/lens/public/_config_panel.scss deleted file mode 100644 index 5c6d25bf10818..0000000000000 --- a/x-pack/legacy/plugins/lens/public/_config_panel.scss +++ /dev/null @@ -1,21 +0,0 @@ -.lnsConfigPanel__panel { - margin-bottom: $euiSizeS; -} - -.lnsConfigPanel__axis { - background: $euiColorLightestShade; - padding: $euiSizeS; - border-radius: $euiBorderRadius; - - // Add margin to the top of the next same panel - & + & { - margin-top: $euiSizeS; - } -} - -.lnsConfigPanel__addLayerBtn { - color: transparentize($euiColorMediumShade, .3); - // sass-lint:disable-block no-important - box-shadow: none !important; - border: 1px dashed currentColor; -} diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx index 0cba22170df1f..e18190b6c2d69 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -4,18 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { createMockDatasource } from '../editor_frame_service/mocks'; -import { - DatatableVisualizationState, - datatableVisualization, - DataTableLayer, -} from './visualization'; -import { mount } from 'enzyme'; +import { DatatableVisualizationState, datatableVisualization } from './visualization'; import { Operation, DataType, FramePublicAPI, TableSuggestionColumn } from '../types'; -import { generateId } from '../id_generator'; - -jest.mock('../id_generator'); function mockFrame(): FramePublicAPI { return { @@ -34,12 +25,11 @@ function mockFrame(): FramePublicAPI { describe('Datatable Visualization', () => { describe('#initialize', () => { it('should initialize from the empty state', () => { - (generateId as jest.Mock).mockReturnValueOnce('id'); expect(datatableVisualization.initialize(mockFrame(), undefined)).toEqual({ layers: [ { layerId: 'aaa', - columns: ['id'], + columns: [], }, ], }); @@ -88,7 +78,6 @@ describe('Datatable Visualization', () => { describe('#clearLayer', () => { it('should reset the layer', () => { - (generateId as jest.Mock).mockReturnValueOnce('testid'); const state: DatatableVisualizationState = { layers: [ { @@ -101,7 +90,7 @@ describe('Datatable Visualization', () => { layers: [ { layerId: 'baz', - columns: ['testid'], + columns: [], }, ], }); @@ -214,29 +203,35 @@ describe('Datatable Visualization', () => { }); }); - describe('DataTableLayer', () => { - it('allows all kinds of operations', () => { - const setState = jest.fn(); - const datasource = createMockDatasource(); - const layer = { layerId: 'a', columns: ['b', 'c'] }; + describe('#getConfiguration', () => { + it('returns a single layer option', () => { + const datasource = createMockDatasource('test'); const frame = mockFrame(); - frame.datasourceLayers = { a: datasource.publicAPIMock }; + frame.datasourceLayers = { first: datasource.publicAPIMock }; - mount( - {} }} - frame={frame} - layer={layer} - setState={setState} - state={{ layers: [layer] }} - /> - ); + expect( + datatableVisualization.getConfiguration({ + layerId: 'first', + state: { + layers: [{ layerId: 'first', columns: [] }], + }, + frame, + }).groups + ).toHaveLength(1); + }); - expect(datasource.publicAPIMock.renderDimensionPanel).toHaveBeenCalled(); + it('allows all kinds of operations', () => { + const datasource = createMockDatasource('test'); + const frame = mockFrame(); + frame.datasourceLayers = { first: datasource.publicAPIMock }; - const filterOperations = - datasource.publicAPIMock.renderDimensionPanel.mock.calls[0][1].filterOperations; + const filterOperations = datatableVisualization.getConfiguration({ + layerId: 'first', + state: { + layers: [{ layerId: 'first', columns: [] }], + }, + frame, + }).groups[0].filterOperations; const baseOperation: Operation = { dataType: 'string', @@ -253,108 +248,80 @@ describe('Datatable Visualization', () => { ); }); - it('allows columns to be removed', () => { - const setState = jest.fn(); - const datasource = createMockDatasource(); + it('reorders the rendered colums based on the order from the datasource', () => { + const datasource = createMockDatasource('test'); const layer = { layerId: 'a', columns: ['b', 'c'] }; const frame = mockFrame(); frame.datasourceLayers = { a: datasource.publicAPIMock }; - const component = mount( - {} }} - frame={frame} - layer={layer} - setState={setState} - state={{ layers: [layer] }} - /> - ); - - const onRemove = component - .find('[data-test-subj="datatable_multicolumnEditor"]') - .first() - .prop('onRemove') as (k: string) => {}; - - onRemove('b'); + datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]); + + expect( + datatableVisualization.getConfiguration({ + layerId: 'a', + state: { layers: [layer] }, + frame, + }).groups[0].accessors + ).toEqual(['c', 'b']); + }); + }); - expect(setState).toHaveBeenCalledWith({ + describe('#removeDimension', () => { + it('allows columns to be removed', () => { + const layer = { layerId: 'layer1', columns: ['b', 'c'] }; + expect( + datatableVisualization.removeDimension({ + prevState: { layers: [layer] }, + layerId: 'layer1', + columnId: 'b', + }) + ).toEqual({ layers: [ { - layerId: 'a', + layerId: 'layer1', columns: ['c'], }, ], }); }); + }); + describe('#setDimension', () => { it('allows columns to be added', () => { - (generateId as jest.Mock).mockReturnValueOnce('d'); - const setState = jest.fn(); - const datasource = createMockDatasource(); - const layer = { layerId: 'a', columns: ['b', 'c'] }; - const frame = mockFrame(); - frame.datasourceLayers = { a: datasource.publicAPIMock }; - const component = mount( - {} }} - frame={frame} - layer={layer} - setState={setState} - state={{ layers: [layer] }} - /> - ); - - const onAdd = component - .find('[data-test-subj="datatable_multicolumnEditor"]') - .first() - .prop('onAdd') as () => {}; - - onAdd(); - - expect(setState).toHaveBeenCalledWith({ + const layer = { layerId: 'layer1', columns: ['b', 'c'] }; + expect( + datatableVisualization.setDimension({ + prevState: { layers: [layer] }, + layerId: 'layer1', + columnId: 'd', + groupId: '', + }) + ).toEqual({ layers: [ { - layerId: 'a', + layerId: 'layer1', columns: ['b', 'c', 'd'], }, ], }); }); - it('reorders the rendered colums based on the order from the datasource', () => { - const datasource = createMockDatasource(); - const layer = { layerId: 'a', columns: ['b', 'c'] }; - const frame = mockFrame(); - frame.datasourceLayers = { a: datasource.publicAPIMock }; - const component = mount( - {} }} - frame={frame} - layer={layer} - setState={jest.fn()} - state={{ layers: [layer] }} - /> - ); - - const accessors = component - .find('[data-test-subj="datatable_multicolumnEditor"]') - .first() - .prop('accessors') as string[]; - - expect(accessors).toEqual(['b', 'c']); - - component.setProps({ - layer: { layerId: 'a', columns: ['c', 'b'] }, + it('does not set a duplicate dimension', () => { + const layer = { layerId: 'layer1', columns: ['b', 'c'] }; + expect( + datatableVisualization.setDimension({ + prevState: { layers: [layer] }, + layerId: 'layer1', + columnId: 'b', + groupId: '', + }) + ).toEqual({ + layers: [ + { + layerId: 'layer1', + columns: ['b', 'c'], + }, + ], }); - - const newAccessors = component - .find('[data-test-subj="datatable_multicolumnEditor"]') - .first() - .prop('accessors') as string[]; - - expect(newAccessors).toEqual(['c', 'b']); }); }); }); diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx index 79a018635134f..4248d722d5540 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx @@ -4,20 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { render } from 'react-dom'; -import { EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { I18nProvider } from '@kbn/i18n/react'; -import { MultiColumnEditor } from '../multi_column_editor'; -import { - SuggestionRequest, - Visualization, - VisualizationLayerConfigProps, - VisualizationSuggestion, - Operation, -} from '../types'; -import { generateId } from '../id_generator'; +import { SuggestionRequest, Visualization, VisualizationSuggestion, Operation } from '../types'; import chartTableSVG from '../assets/chart_datatable.svg'; export interface LayerState { @@ -32,58 +20,10 @@ export interface DatatableVisualizationState { function newLayerState(layerId: string): LayerState { return { layerId, - columns: [generateId()], + columns: [], }; } -function updateColumns( - state: DatatableVisualizationState, - layer: LayerState, - fn: (columns: string[]) => string[] -) { - const columns = fn(layer.columns); - const updatedLayer = { ...layer, columns }; - const layers = state.layers.map(l => (l.layerId === layer.layerId ? updatedLayer : l)); - return { ...state, layers }; -} - -const allOperations = () => true; - -export function DataTableLayer({ - layer, - frame, - state, - setState, - dragDropContext, -}: { layer: LayerState } & VisualizationLayerConfigProps) { - const datasource = frame.datasourceLayers[layer.layerId]; - - const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId); - // When we add a column it could be empty, and therefore have no order - const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns))); - - return ( - - setState(updateColumns(state, layer, columns => [...columns, generateId()]))} - onRemove={column => - setState(updateColumns(state, layer, columns => columns.filter(c => c !== column))) - } - testSubj="datatable_columns" - data-test-subj="datatable_multicolumnEditor" - /> - - ); -} - export const datatableVisualization: Visualization< DatatableVisualizationState, DatatableVisualizationState @@ -188,17 +128,56 @@ export const datatableVisualization: Visualization< ]; }, - renderLayerConfigPanel(domElement, props) { - const layer = props.state.layers.find(l => l.layerId === props.layerId); - - if (layer) { - render( - - - , - domElement - ); + getConfiguration({ state, frame, layerId }) { + const layer = state.layers.find(l => l.layerId === layerId); + if (!layer) { + return { groups: [] }; } + + const datasource = frame.datasourceLayers[layer.layerId]; + const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId); + // When we add a column it could be empty, and therefore have no order + const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns))); + + return { + groups: [ + { + groupId: 'columns', + groupLabel: i18n.translate('xpack.lens.datatable.columns', { + defaultMessage: 'Columns', + }), + layerId: state.layers[0].layerId, + accessors: sortedColumns, + supportsMoreColumns: true, + filterOperations: () => true, + }, + ], + }; + }, + + setDimension({ prevState, layerId, columnId }) { + return { + ...prevState, + layers: prevState.layers.map(l => { + if (l.layerId !== layerId || l.columns.includes(columnId)) { + return l; + } + return { ...l, columns: [...l.columns, columnId] }; + }), + }; + }, + removeDimension({ prevState, layerId, columnId }) { + return { + ...prevState, + layers: prevState.layers.map(l => + l.layerId === layerId + ? { + ...l, + columns: l.columns.filter(c => c !== columnId), + } + : l + ), + }; }, toExpression(state, frame) { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss new file mode 100644 index 0000000000000..62a7f6b023f31 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss @@ -0,0 +1,50 @@ +.lnsConfigPanel__panel { + margin-bottom: $euiSizeS; +} + +.lnsConfigPanel__row { + background: $euiColorLightestShade; + padding: $euiSizeS; + border-radius: $euiBorderRadius; + + // Add margin to the top of the next same panel + & + & { + margin-top: $euiSizeS; + } +} + +.lnsConfigPanel__addLayerBtn { + color: transparentize($euiColorMediumShade, .3); + // Remove EuiButton's default shadow to make button more subtle + // sass-lint:disable-block no-important + box-shadow: none !important; + border: 1px dashed currentColor; +} + +.lnsConfigPanel__dimension { + @include euiFontSizeS; + background: lightOrDarkTheme($euiColorEmptyShade, $euiColorLightestShade); + border-radius: $euiBorderRadius; + display: flex; + align-items: center; + margin-top: $euiSizeXS; + overflow: hidden; +} + +.lnsConfigPanel__trigger { + max-width: 100%; + display: block; +} + +.lnsConfigPanel__triggerLink { + padding: $euiSizeS; + width: 100%; + display: flex; + align-items: center; + min-height: $euiSizeXXL; +} + +.lnsConfigPanel__popover { + line-height: 0; + flex-grow: 1; +} diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx index 1b60098fd45ad..6698c9e68b98c 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx @@ -84,7 +84,7 @@ describe('chart_switch', () => { } function mockDatasourceMap() { - const datasource = createMockDatasource(); + const datasource = createMockDatasource('testDatasource'); datasource.getDatasourceSuggestionsFromCurrentState.mockReturnValue([ { state: {}, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx index 1422ee86be3e9..c2cd0485de67e 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx @@ -16,17 +16,21 @@ import { EuiToolTip, EuiButton, EuiForm, + EuiFormRow, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { NativeRenderer } from '../../native_renderer'; import { Action } from './state_management'; import { Visualization, FramePublicAPI, Datasource, - VisualizationLayerConfigProps, + VisualizationLayerWidgetProps, + DatasourceDimensionEditorProps, + StateSetter, } from '../../types'; -import { DragContext } from '../../drag_drop'; +import { DragContext, DragDrop, ChildDragDropProvider } from '../../drag_drop'; import { ChartSwitch } from './chart_switch'; import { trackUiEvent } from '../../lens_ui_telemetry'; import { generateId } from '../../id_generator'; @@ -47,6 +51,7 @@ interface ConfigPanelWrapperProps { state: unknown; } >; + core: DatasourceDimensionEditorProps['core']; } export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) { @@ -86,8 +91,7 @@ function LayerPanels( activeDatasourceId, datasourceMap, } = props; - const dragDropContext = useContext(DragContext); - const setState = useMemo( + const setVisualizationState = useMemo( () => (newState: unknown) => { props.dispatch({ type: 'UPDATE_VISUALIZATION_STATE', @@ -98,6 +102,43 @@ function LayerPanels( }, [props.dispatch, activeVisualization] ); + const updateDatasource = useMemo( + () => (datasourceId: string, newState: unknown) => { + props.dispatch({ + type: 'UPDATE_DATASOURCE_STATE', + updater: () => newState, + datasourceId, + clearStagedPreview: false, + }); + }, + [props.dispatch] + ); + const updateAll = useMemo( + () => (datasourceId: string, newDatasourceState: unknown, newVisualizationState: unknown) => { + props.dispatch({ + type: 'UPDATE_STATE', + subType: 'UPDATE_ALL_STATES', + updater: prevState => { + return { + ...prevState, + datasourceStates: { + ...prevState.datasourceStates, + [datasourceId]: { + state: newDatasourceState, + isLoading: false, + }, + }, + visualization: { + activeId: activeVisualization.id, + state: newVisualizationState, + }, + stagedPreview: undefined, + }; + }, + }); + }, + [props.dispatch] + ); const layerIds = activeVisualization.getLayerIds(visualizationState); return ( @@ -108,12 +149,13 @@ function LayerPanels( key={layerId} layerId={layerId} activeVisualization={activeVisualization} - dragDropContext={dragDropContext} - state={setState} - setState={setState} + visualizationState={visualizationState} + updateVisualization={setVisualizationState} + updateDatasource={updateDatasource} + updateAll={updateAll} frame={framePublicAPI} isOnlyLayer={layerIds.length === 1} - onRemove={() => { + onRemoveLayer={() => { dispatch({ type: 'UPDATE_STATE', subType: 'REMOVE_OR_CLEAR_LAYER', @@ -143,7 +185,7 @@ function LayerPanels( className="lnsConfigPanel__addLayerBtn" fullWidth size="s" - data-test-subj={`lnsXY_layer_add`} + data-test-subj="lnsXY_layer_add" aria-label={i18n.translate('xpack.lens.xyChart.addLayerButton', { defaultMessage: 'Add layer', })} @@ -174,85 +216,399 @@ function LayerPanels( } function LayerPanel( - props: ConfigPanelWrapperProps & - VisualizationLayerConfigProps & { - isOnlyLayer: boolean; - activeVisualization: Visualization; - onRemove: () => void; - } + props: Exclude & { + frame: FramePublicAPI; + layerId: string; + isOnlyLayer: boolean; + activeVisualization: Visualization; + visualizationState: unknown; + updateVisualization: StateSetter; + updateDatasource: (datasourceId: string, newState: unknown) => void; + updateAll: ( + datasourceId: string, + newDatasourcestate: unknown, + newVisualizationState: unknown + ) => void; + onRemoveLayer: () => void; + } ) { - const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemove } = props; + const dragDropContext = useContext(DragContext); + const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemoveLayer } = props; const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; - const layerConfigProps = { + if (!datasourcePublicAPI) { + return null; + } + const layerVisualizationConfigProps = { layerId, - dragDropContext: props.dragDropContext, + dragDropContext, state: props.visualizationState, - setState: props.setState, frame: props.framePublicAPI, + dateRange: props.framePublicAPI.dateRange, }; + const datasourceId = datasourcePublicAPI.datasourceId; + const layerDatasourceState = props.datasourceStates[datasourceId].state; + const layerDatasource = props.datasourceMap[datasourceId]; - return ( - - - - - + const layerDatasourceDropProps = { + layerId, + dragDropContext, + state: layerDatasourceState, + setState: (newState: unknown) => { + props.updateDatasource(datasourceId, newState); + }, + }; - {datasourcePublicAPI && ( - - ({ + isOpen: false, + openId: null, + addingToGroupId: null, + }); + + const { groups } = activeVisualization.getConfiguration(layerVisualizationConfigProps); + const isEmptyLayer = !groups.some(d => d.accessors.length > 0); + + function wrapInPopover( + id: string, + groupId: string, + trigger: React.ReactElement, + panel: React.ReactElement + ) { + const noMatch = popoverState.isOpen ? !groups.some(d => d.accessors.includes(id)) : false; + return ( + { + setPopoverState({ isOpen: false, openId: null, addingToGroupId: null }); + }} + button={trigger} + anchorPosition="leftUp" + withTitle + panelPaddingSize="s" + > + {panel} + + ); + } + + return ( + + + + + - )} - - - - + {layerDatasource && ( + + { + const newState = + typeof updater === 'function' ? updater(layerDatasourceState) : updater; + // Look for removed columns + const nextPublicAPI = layerDatasource.getPublicAPI({ + state: newState, + layerId, + dateRange: props.framePublicAPI.dateRange, + }); + const nextTable = new Set( + nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) + ); + const removed = datasourcePublicAPI + .getTableSpec() + .map(({ columnId }) => columnId) + .filter(columnId => !nextTable.has(columnId)); + let nextVisState = props.visualizationState; + removed.forEach(columnId => { + nextVisState = activeVisualization.removeDimension({ + layerId, + columnId, + prevState: nextVisState, + }); + }); - + props.updateAll(datasourceId, newState, nextVisState); + }, + }} + /> + + )} + - - - { - // If we don't blur the remove / clear button, it remains focused - // which is a strange UX in this case. e.target.blur doesn't work - // due to who knows what, but probably event re-writing. Additionally, - // activeElement does not have blur so, we need to do some casting + safeguards. - const el = (document.activeElement as unknown) as { blur: () => void }; + - if (el && el.blur) { - el.blur(); + {groups.map((group, index) => { + const newId = generateId(); + const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; + return ( + + <> + {group.accessors.map(accessor => ( + { + layerDatasource.onDrop({ + ...layerDatasourceDropProps, + droppedItem, + columnId: accessor, + filterOperations: group.filterOperations, + }); + }} + > + {wrapInPopover( + accessor, + group.groupId, + { + if (popoverState.isOpen) { + setPopoverState({ + isOpen: false, + openId: null, + addingToGroupId: null, + }); + } else { + setPopoverState({ + isOpen: true, + openId: accessor, + addingToGroupId: null, // not set for existing dimension + }); + } + }, + }} + />, + + )} - onRemove(); - }} - > - {isOnlyLayer - ? i18n.translate('xpack.lens.resetLayer', { - defaultMessage: 'Reset layer', - }) - : i18n.translate('xpack.lens.deleteLayer', { - defaultMessage: 'Delete layer', - })} - - - - + { + trackUiEvent('indexpattern_dimension_removed'); + props.updateAll( + datasourceId, + layerDatasource.removeColumn({ + layerId, + columnId: accessor, + prevState: layerDatasourceState, + }), + props.activeVisualization.removeDimension({ + layerId, + columnId: accessor, + prevState: props.visualizationState, + }) + ); + }} + /> + + ))} + {group.supportsMoreColumns ? ( + { + const dropSuccess = layerDatasource.onDrop({ + ...layerDatasourceDropProps, + droppedItem, + columnId: newId, + filterOperations: group.filterOperations, + }); + if (dropSuccess) { + props.updateVisualization( + activeVisualization.setDimension({ + layerId, + groupId: group.groupId, + columnId: newId, + prevState: props.visualizationState, + }) + ); + } + }} + > + {wrapInPopover( + newId, + group.groupId, +
+ { + if (popoverState.isOpen) { + setPopoverState({ + isOpen: false, + openId: null, + addingToGroupId: null, + }); + } else { + setPopoverState({ + isOpen: true, + openId: newId, + addingToGroupId: group.groupId, + }); + } + }} + size="xs" + > + + +
, + { + props.updateAll( + datasourceId, + newState, + activeVisualization.setDimension({ + layerId, + groupId: group.groupId, + columnId: newId, + prevState: props.visualizationState, + }) + ); + setPopoverState({ + isOpen: true, + openId: newId, + addingToGroupId: null, // clear now that dimension exists + }); + }, + }} + /> + )} +
+ ) : null} + + + ); + })} + + + + + + { + // If we don't blur the remove / clear button, it remains focused + // which is a strange UX in this case. e.target.blur doesn't work + // due to who knows what, but probably event re-writing. Additionally, + // activeElement does not have blur so, we need to do some casting + safeguards. + const el = (document.activeElement as unknown) as { blur: () => void }; + + if (el?.blur) { + el.blur(); + } + + onRemoveLayer(); + }} + > + {isOnlyLayer + ? i18n.translate('xpack.lens.resetLayer', { + defaultMessage: 'Reset layer', + }) + : i18n.translate('xpack.lens.deleteLayer', { + defaultMessage: 'Delete layer', + })} + + + + + ); } @@ -263,7 +619,7 @@ function LayerSettings({ }: { layerId: string; activeVisualization: Visualization; - layerConfigProps: VisualizationLayerConfigProps; + layerConfigProps: VisualizationLayerWidgetProps; }) { const [isOpen, setIsOpen] = useState(false); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index dd591b3992fe5..8d8d38944e18a 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -87,14 +87,15 @@ describe('editor_frame', () => { mockVisualization.getLayerIds.mockReturnValue(['first']); mockVisualization2.getLayerIds.mockReturnValue(['second']); - mockDatasource = createMockDatasource(); - mockDatasource2 = createMockDatasource(); + mockDatasource = createMockDatasource('testDatasource'); + mockDatasource2 = createMockDatasource('testDatasource2'); expressionRendererMock = createExpressionRendererMock(); }); describe('initialization', () => { it('should initialize initial datasource', async () => { + mockVisualization.getLayerIds.mockReturnValue([]); await act(async () => { mount( { }); it('should initialize all datasources with state from doc', async () => { - const mockDatasource3 = createMockDatasource(); + const mockDatasource3 = createMockDatasource('testDatasource3'); const datasource1State = { datasource1: '' }; const datasource2State = { datasource2: '' }; @@ -198,9 +199,9 @@ describe('editor_frame', () => { ExpressionRenderer={expressionRendererMock} /> ); - expect(mockVisualization.renderLayerConfigPanel).not.toHaveBeenCalled(); expect(mockDatasource.renderDataPanel).not.toHaveBeenCalled(); }); + expect(mockDatasource.renderDataPanel).toHaveBeenCalled(); }); it('should not initialize visualization before datasource is initialized', async () => { @@ -289,6 +290,7 @@ describe('editor_frame', () => { mockDatasource2.initialize.mockReturnValue(Promise.resolve(initialState)); mockDatasource2.getLayers.mockReturnValue(['abc', 'def']); mockDatasource2.removeLayer.mockReturnValue({ removed: true }); + mockVisualization.getLayerIds.mockReturnValue(['first', 'abc', 'def']); await act(async () => { mount( { ); }); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: initialState }) ); }); @@ -614,15 +615,14 @@ describe('editor_frame', () => { ); }); const updatedState = {}; - const setVisualizationState = (mockVisualization.renderLayerConfigPanel as jest.Mock).mock - .calls[0][1].setState; + const setDatasourceState = (mockDatasource.renderDataPanel as jest.Mock).mock.calls[0][1] + .setState; act(() => { - setVisualizationState(updatedState); + setDatasourceState(updatedState); }); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledTimes(2); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenLastCalledWith( - expect.any(Element), + expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(2); + expect(mockVisualization.getConfiguration).toHaveBeenLastCalledWith( expect.objectContaining({ state: updatedState, }) @@ -688,8 +688,7 @@ describe('editor_frame', () => { }); const updatedPublicAPI: DatasourcePublicAPI = { - renderLayerPanel: jest.fn(), - renderDimensionPanel: jest.fn(), + datasourceId: 'testDatasource', getOperationForColumnId: jest.fn(), getTableSpec: jest.fn(), }; @@ -701,9 +700,8 @@ describe('editor_frame', () => { setDatasourceState({}); }); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledTimes(2); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenLastCalledWith( - expect.any(Element), + expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(2); + expect(mockVisualization.getConfiguration).toHaveBeenLastCalledWith( expect.objectContaining({ frame: expect.objectContaining({ datasourceLayers: { @@ -719,6 +717,7 @@ describe('editor_frame', () => { it('should pass the datasource api for each layer to the visualization', async () => { mockDatasource.getLayers.mockReturnValue(['first']); mockDatasource2.getLayers.mockReturnValue(['second', 'third']); + mockVisualization.getLayerIds.mockReturnValue(['first', 'second', 'third']); await act(async () => { mount( @@ -755,10 +754,10 @@ describe('editor_frame', () => { ); }); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalled(); + expect(mockVisualization.getConfiguration).toHaveBeenCalled(); const datasourceLayers = - mockVisualization.renderLayerConfigPanel.mock.calls[0][1].frame.datasourceLayers; + mockVisualization.getConfiguration.mock.calls[0][0].frame.datasourceLayers; expect(datasourceLayers.first).toBe(mockDatasource.publicAPIMock); expect(datasourceLayers.second).toBe(mockDatasource2.publicAPIMock); expect(datasourceLayers.third).toBe(mockDatasource2.publicAPIMock); @@ -811,21 +810,18 @@ describe('editor_frame', () => { expect(mockDatasource.getPublicAPI).toHaveBeenCalledWith( expect.objectContaining({ state: datasource1State, - setState: expect.anything(), layerId: 'first', }) ); expect(mockDatasource2.getPublicAPI).toHaveBeenCalledWith( expect.objectContaining({ state: datasource2State, - setState: expect.anything(), layerId: 'second', }) ); expect(mockDatasource2.getPublicAPI).toHaveBeenCalledWith( expect.objectContaining({ state: datasource2State, - setState: expect.anything(), layerId: 'third', }) ); @@ -858,45 +854,9 @@ describe('editor_frame', () => { expect(mockDatasource.getPublicAPI).toHaveBeenCalledWith({ dateRange, state: datasourceState, - setState: expect.any(Function), layerId: 'first', }); }); - - it('should re-create the public api after state has been set', async () => { - mockDatasource.getLayers.mockReturnValue(['first']); - - await act(async () => { - mount( - - ); - }); - - const updatedState = {}; - const setDatasourceState = mockDatasource.getPublicAPI.mock.calls[0][0].setState; - act(() => { - setDatasourceState(updatedState); - }); - - expect(mockDatasource.getPublicAPI).toHaveBeenLastCalledWith( - expect.objectContaining({ - state: updatedState, - setState: expect.any(Function), - layerId: 'first', - }) - ); - }); }); describe('switching', () => { @@ -1021,8 +981,7 @@ describe('editor_frame', () => { expect(mockVisualization2.getSuggestions).toHaveBeenCalled(); expect(mockVisualization2.initialize).toHaveBeenCalledWith(expect.anything(), initialState); - expect(mockVisualization2.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization2.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: { initial: true } }) ); }); @@ -1039,8 +998,7 @@ describe('editor_frame', () => { datasourceLayers: expect.objectContaining({ first: mockDatasource.publicAPIMock }), }) ); - expect(mockVisualization2.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization2.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: { initial: true } }) ); }); @@ -1239,9 +1197,8 @@ describe('editor_frame', () => { .simulate('click'); }); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledTimes(1); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(1); + expect(mockVisualization.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: suggestionVisState, }) @@ -1306,8 +1263,7 @@ describe('editor_frame', () => { .simulate('drop'); }); - expect(mockVisualization.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: suggestionVisState, }) @@ -1375,14 +1331,16 @@ describe('editor_frame', () => { instance.update(); act(() => { - instance.find(DragDrop).prop('onDrop')!({ + instance + .find(DragDrop) + .filter('[data-test-subj="mockVisA"]') + .prop('onDrop')!({ indexPatternId: '1', field: {}, }); }); - expect(mockVisualization2.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization2.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: suggestionVisState, }) @@ -1472,14 +1430,16 @@ describe('editor_frame', () => { instance.update(); act(() => { - instance.find(DragDrop).prop('onDrop')!({ + instance + .find(DragDrop) + .filter('[data-test-subj="lnsWorkspace"]') + .prop('onDrop')!({ indexPatternId: '1', field: {}, }); }); - expect(mockVisualization3.renderLayerConfigPanel).toHaveBeenCalledWith( - expect.any(Element), + expect(mockVisualization3.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: suggestionVisState, }) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index a456372c99c01..082519d9a8feb 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -21,6 +21,7 @@ import { FrameLayout } from './frame_layout'; import { SuggestionPanel } from './suggestion_panel'; import { WorkspacePanel } from './workspace_panel'; import { Document } from '../../persistence/saved_object_store'; +import { RootDragDropProvider } from '../../drag_drop'; import { getSavedObjectFormat } from './save'; import { WorkspacePanelWrapper } from './workspace_panel_wrapper'; import { generateId } from '../../id_generator'; @@ -90,21 +91,11 @@ export function EditorFrame(props: EditorFrameProps) { const layers = datasource.getLayers(datasourceState); layers.forEach(layer => { - const publicAPI = props.datasourceMap[id].getPublicAPI({ + datasourceLayers[layer] = props.datasourceMap[id].getPublicAPI({ state: datasourceState, - setState: (newState: unknown) => { - dispatch({ - type: 'UPDATE_DATASOURCE_STATE', - datasourceId: id, - updater: newState, - clearStagedPreview: true, - }); - }, layerId: layer, dateRange: props.dateRange, }); - - datasourceLayers[layer] = publicAPI; }); }); @@ -235,74 +226,79 @@ export function EditorFrame(props: EditorFrameProps) { ]); return ( - - } - configPanel={ - allLoaded && ( - + - ) - } - workspacePanel={ - allLoaded && ( - - + ) + } + workspacePanel={ + allLoaded && ( + + + + ) + } + suggestionsPanel={ + allLoaded && ( + - - ) - } - suggestionsPanel={ - allLoaded && ( - - ) - } - /> + ) + } + /> + ); } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx index a69da8b49e233..56afe3ed69a73 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx @@ -6,7 +6,6 @@ import React from 'react'; import { EuiPage, EuiPageSideBar, EuiPageBody } from '@elastic/eui'; -import { RootDragDropProvider } from '../../drag_drop'; export interface FrameLayoutProps { dataPanel: React.ReactNode; @@ -17,19 +16,17 @@ export interface FrameLayoutProps { export function FrameLayout(props: FrameLayoutProps) { return ( - - -
- {props.dataPanel} - - {props.workspacePanel} - {props.suggestionsPanel} - - - {props.configPanel} - -
-
-
+ +
+ {props.dataPanel} + + {props.workspacePanel} + {props.suggestionsPanel} + + + {props.configPanel} + +
+
); } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss index fee28c374ef7e..6c6a63c8c7eb6 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss @@ -1,4 +1,5 @@ @import './chart_switch'; +@import './config_panel_wrapper'; @import './data_panel_wrapper'; @import './expression_renderer'; @import './frame_layout'; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts index 158a6cb8c979a..60bfbc493f61c 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts @@ -11,7 +11,7 @@ import { esFilters, IIndexPattern, IFieldType } from '../../../../../../../src/p describe('save editor frame state', () => { const mockVisualization = createMockVisualization(); mockVisualization.getPersistableState.mockImplementation(x => x); - const mockDatasource = createMockDatasource(); + const mockDatasource = createMockDatasource('a'); const mockIndexPattern = ({ id: 'indexpattern' } as unknown) as IIndexPattern; const mockField = ({ name: '@timestamp' } as unknown) as IFieldType; @@ -45,7 +45,7 @@ describe('save editor frame state', () => { }; it('transforms from internal state to persisted doc format', async () => { - const datasource = createMockDatasource(); + const datasource = createMockDatasource('a'); datasource.getPersistableState.mockImplementation(state => ({ stuff: `${state}_datasource_persisted`, })); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts index 487a91c22b5d5..63b8b1f048296 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts @@ -30,7 +30,7 @@ let datasourceStates: Record< beforeEach(() => { datasourceMap = { - mock: createMockDatasource(), + mock: createMockDatasource('a'), }; datasourceStates = { @@ -147,9 +147,9 @@ describe('suggestion helpers', () => { }, }; const multiDatasourceMap = { - mock: createMockDatasource(), - mock2: createMockDatasource(), - mock3: createMockDatasource(), + mock: createMockDatasource('a'), + mock2: createMockDatasource('a'), + mock3: createMockDatasource('a'), }; const droppedField = {}; getSuggestions({ diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx index 9729d6259f84a..b146f2467c46c 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx @@ -39,7 +39,7 @@ describe('suggestion_panel', () => { beforeEach(() => { mockVisualization = createMockVisualization(); - mockDatasource = createMockDatasource(); + mockDatasource = createMockDatasource('a'); expressionRendererMock = createExpressionRendererMock(); dispatchMock = jest.fn(); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index 1115126792c86..93f6ea6ea67ac 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -373,7 +373,6 @@ function getPreviewExpression( layerId, dateRange: frame.dateRange, state: datasourceState, - setState: () => {}, }); } }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx index a51091d39f84c..748e5b876da95 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx @@ -36,7 +36,7 @@ describe('workspace_panel', () => { mockVisualization = createMockVisualization(); mockVisualization2 = createMockVisualization(); - mockDatasource = createMockDatasource(); + mockDatasource = createMockDatasource('a'); expressionRendererMock = createExpressionRendererMock(); }); @@ -199,7 +199,7 @@ describe('workspace_panel', () => { }); it('should include data fetching for each layer in the expression', () => { - const mockDatasource2 = createMockDatasource(); + const mockDatasource2 = createMockDatasource('a'); const framePublicAPI = createMockFramePublicAPI(); framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts index d30ad62b385c2..2bde698e23562 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts @@ -27,17 +27,19 @@ import { Embeddable } from './embeddable'; import { SavedObjectIndexStore, DOC_TYPE } from '../../persistence'; import { getEditPath } from '../../../../../../plugins/lens/common'; +interface StartServices { + timefilter: TimefilterContract; + coreHttp: HttpSetup; + capabilities: RecursiveReadonly; + savedObjectsClient: SavedObjectsClientContract; + expressionRenderer: ReactExpressionRendererType; + indexPatternService: IndexPatternsContract; +} + export class EmbeddableFactory extends AbstractEmbeddableFactory { type = DOC_TYPE; - constructor( - private timefilter: TimefilterContract, - private coreHttp: HttpSetup, - private capabilities: RecursiveReadonly, - private savedObjectsClient: SavedObjectsClientContract, - private expressionRenderer: ReactExpressionRendererType, - private indexPatternService: IndexPatternsContract - ) { + constructor(private getStartServices: () => Promise) { super({ savedObjectMetaData: { name: i18n.translate('xpack.lens.lensSavedObjectLabel', { @@ -49,8 +51,9 @@ export class EmbeddableFactory extends AbstractEmbeddableFactory { }); } - public isEditable() { - return this.capabilities.visualize.save as boolean; + public async isEditable() { + const { capabilities } = await this.getStartServices(); + return capabilities.visualize.save as boolean; } canCreateNew() { @@ -68,13 +71,20 @@ export class EmbeddableFactory extends AbstractEmbeddableFactory { input: Partial & { id: string }, parent?: IContainer ) { - const store = new SavedObjectIndexStore(this.savedObjectsClient); + const { + savedObjectsClient, + coreHttp, + indexPatternService, + timefilter, + expressionRenderer, + } = await this.getStartServices(); + const store = new SavedObjectIndexStore(savedObjectsClient); const savedVis = await store.load(savedObjectId); const promises = savedVis.state.datasourceMetaData.filterableIndexPatterns.map( async ({ id }) => { try { - return await this.indexPatternService.get(id); + return await indexPatternService.get(id); } catch (error) { // Unable to load index pattern, ignore error as the index patterns are only used to // configure the filter and query bar - there is still a good chance to get the visualization @@ -90,12 +100,12 @@ export class EmbeddableFactory extends AbstractEmbeddableFactory { ); return new Embeddable( - this.timefilter, - this.expressionRenderer, + timefilter, + expressionRenderer, { savedVis, - editUrl: this.coreHttp.basePath.prepend(getEditPath(savedObjectId)), - editable: this.isEditable(), + editUrl: coreHttp.basePath.prepend(getEditPath(savedObjectId)), + editable: await this.isEditable(), indexPatterns, }, input, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx index e606c69c8c386..5d2f68a5567eb 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx @@ -33,9 +33,24 @@ export function createMockVisualization(): jest.Mocked { getPersistableState: jest.fn(_state => _state), getSuggestions: jest.fn(_options => []), initialize: jest.fn((_frame, _state?) => ({})), - renderLayerConfigPanel: jest.fn(), + getConfiguration: jest.fn(props => ({ + groups: [ + { + groupId: 'a', + groupLabel: 'a', + layerId: 'layer1', + supportsMoreColumns: true, + accessors: [], + filterOperations: jest.fn(() => true), + dataTestSubj: 'mockVisA', + }, + ], + })), toExpression: jest.fn((_state, _frame) => null), toPreviewExpression: jest.fn((_state, _frame) => null), + + setDimension: jest.fn(), + removeDimension: jest.fn(), }; } @@ -43,12 +58,11 @@ export type DatasourceMock = jest.Mocked & { publicAPIMock: jest.Mocked; }; -export function createMockDatasource(): DatasourceMock { +export function createMockDatasource(id: string): DatasourceMock { const publicAPIMock: jest.Mocked = { + datasourceId: id, getTableSpec: jest.fn(() => []), getOperationForColumnId: jest.fn(), - renderDimensionPanel: jest.fn(), - renderLayerPanel: jest.fn(), }; return { @@ -60,12 +74,19 @@ export function createMockDatasource(): DatasourceMock { getPublicAPI: jest.fn().mockReturnValue(publicAPIMock), initialize: jest.fn((_state?) => Promise.resolve()), renderDataPanel: jest.fn(), + renderLayerPanel: jest.fn(), toExpression: jest.fn((_frame, _state) => null), insertLayer: jest.fn((_state, _newLayerId) => {}), removeLayer: jest.fn((_state, _layerId) => {}), + removeColumn: jest.fn(props => {}), getLayers: jest.fn(_state => []), getMetaData: jest.fn(_state => ({ filterableIndexPatterns: [] })), + renderDimensionTrigger: jest.fn(), + renderDimensionEditor: jest.fn(), + canHandleDrop: jest.fn(), + onDrop: jest.fn(), + // this is an additional property which doesn't exist on real datasources // but can be used to validate whether specific API mock functions are called publicAPIMock, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx index 2e1645c816140..47fd810bb4c53 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx @@ -12,6 +12,7 @@ import { createMockSetupDependencies, createMockStartDependencies, } from './mocks'; +import { CoreSetup } from 'kibana/public'; jest.mock('ui/new_platform'); @@ -41,9 +42,12 @@ describe('editor_frame service', () => { it('should create an editor frame instance which mounts and unmounts', async () => { await expect( (async () => { - pluginInstance.setup(coreMock.createSetup(), pluginSetupDependencies); + pluginInstance.setup( + coreMock.createSetup() as CoreSetup, + pluginSetupDependencies + ); const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); - const instance = await publicAPI.createInstance({}); + const instance = await publicAPI.createInstance(); instance.mount(mountpoint, { onError: jest.fn(), onChange: jest.fn(), @@ -57,9 +61,12 @@ describe('editor_frame service', () => { }); it('should not have child nodes after unmount', async () => { - pluginInstance.setup(coreMock.createSetup(), pluginSetupDependencies); + pluginInstance.setup( + coreMock.createSetup() as CoreSetup, + pluginSetupDependencies + ); const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); - const instance = await publicAPI.createInstance({}); + const instance = await publicAPI.createInstance(); instance.mount(mountpoint, { onError: jest.fn(), onChange: jest.fn(), diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx index 5347be47e145e..1375c60060ca8 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx @@ -12,10 +12,7 @@ import { ExpressionsSetup, ExpressionsStart, } from '../../../../../../src/plugins/expressions/public'; -import { - IEmbeddableSetup, - IEmbeddableStart, -} from '../../../../../../src/plugins/embeddable/public'; +import { EmbeddableSetup, EmbeddableStart } from '../../../../../../src/plugins/embeddable/public'; import { DataPublicPluginSetup, DataPublicPluginStart, @@ -35,13 +32,13 @@ import { getActiveDatasourceIdFromDoc } from './editor_frame/state_management'; export interface EditorFrameSetupPlugins { data: DataPublicPluginSetup; - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; expressions: ExpressionsSetup; } export interface EditorFrameStartPlugins { data: DataPublicPluginStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; expressions: ExpressionsStart; } @@ -63,10 +60,27 @@ export class EditorFrameService { private readonly datasources: Array> = []; private readonly visualizations: Array> = []; - public setup(core: CoreSetup, plugins: EditorFrameSetupPlugins): EditorFrameSetup { + public setup( + core: CoreSetup, + plugins: EditorFrameSetupPlugins + ): EditorFrameSetup { plugins.expressions.registerFunction(() => mergeTables); plugins.expressions.registerFunction(() => formatColumn); + const getStartServices = async () => { + const [coreStart, deps] = await core.getStartServices(); + return { + capabilities: coreStart.application.capabilities, + savedObjectsClient: coreStart.savedObjects.client, + coreHttp: coreStart.http, + timefilter: deps.data.query.timefilter.timefilter, + expressionRenderer: deps.expressions.ReactExpressionRenderer, + indexPatternService: deps.data.indexPatterns, + }; + }; + + plugins.embeddable.registerEmbeddableFactory('lens', new EmbeddableFactory(getStartServices)); + return { registerDatasource: datasource => { this.datasources.push(datasource as Datasource); @@ -78,18 +92,6 @@ export class EditorFrameService { } public start(core: CoreStart, plugins: EditorFrameStartPlugins): EditorFrameStart { - plugins.embeddable.registerEmbeddableFactory( - 'lens', - new EmbeddableFactory( - plugins.data.query.timefilter.timefilter, - core.http, - core.application.capabilities, - core.savedObjects.client, - plugins.expressions.ReactExpressionRenderer, - plugins.data.indexPatterns - ) - ); - const createInstance = async (): Promise => { let domElement: Element; const [resolvedDatasources, resolvedVisualizations] = await Promise.all([ diff --git a/x-pack/legacy/plugins/lens/public/index.scss b/x-pack/legacy/plugins/lens/public/index.scss index 496573f6a1c9a..2f91d14c397c7 100644 --- a/x-pack/legacy/plugins/lens/public/index.scss +++ b/x-pack/legacy/plugins/lens/public/index.scss @@ -4,8 +4,6 @@ @import './variables'; @import './mixins'; -@import './config_panel'; - @import './app_plugin/index'; @import 'datatable_visualization/index'; @import './drag_drop/index'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_dimension_panel.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_dimension_panel.scss deleted file mode 100644 index ddb37505f9985..0000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_dimension_panel.scss +++ /dev/null @@ -1,9 +0,0 @@ -.lnsIndexPatternDimensionPanel { - @include euiFontSizeS; - background-color: $euiColorEmptyShade; - border-radius: $euiBorderRadius; - display: flex; - align-items: center; - margin-top: $euiSizeXS; - overflow: hidden; -} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss index 2ce3e11171fc9..26f805fe735f0 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss @@ -1,3 +1,2 @@ -@import './dimension_panel'; @import './field_select'; @import './popover'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss index 8f26ab91e0f16..07a72ee1f66fc 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss @@ -1,37 +1,24 @@ -.lnsPopoverEditor { +.lnsIndexPatternDimensionEditor { flex-grow: 1; line-height: 0; overflow: hidden; } -.lnsPopoverEditor__anchor { - max-width: 100%; - display: block; -} - -.lnsPopoverEditor__link { - width: 100%; - display: flex; - align-items: center; - padding: $euiSizeS; - min-height: $euiSizeXXL; -} - -.lnsPopoverEditor__left, -.lnsPopoverEditor__right { +.lnsIndexPatternDimensionEditor__left, +.lnsIndexPatternDimensionEditor__right { padding: $euiSizeS; } -.lnsPopoverEditor__left { +.lnsIndexPatternDimensionEditor__left { padding-top: 0; background-color: $euiPageBackgroundColor; } -.lnsPopoverEditor__right { +.lnsIndexPatternDimensionEditor__right { width: $euiSize * 20; } -.lnsPopoverEditor__operation { +.lnsIndexPatternDimensionEditor__operation { @include euiFontSizeS; color: $euiColorPrimary; @@ -41,11 +28,11 @@ } } -.lnsPopoverEditor__operation--selected { +.lnsIndexPatternDimensionEditor__operation--selected { font-weight: bold; color: $euiTextColor; } -.lnsPopoverEditor__operation--incompatible { +.lnsIndexPatternDimensionEditor__operation--incompatible { color: $euiColorMediumShade; } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 56f75ae4b17be..41c317ccab290 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -7,27 +7,28 @@ import { ReactWrapper, ShallowWrapper } from 'enzyme'; import React from 'react'; import { act } from 'react-dom/test-utils'; -import { - EuiComboBox, - EuiSideNav, - EuiSideNavItemType, - EuiPopover, - EuiFieldNumber, -} from '@elastic/eui'; +import { EuiComboBox, EuiSideNav, EuiSideNavItemType, EuiFieldNumber } from '@elastic/eui'; import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public'; import { changeColumn } from '../state_helpers'; import { - IndexPatternDimensionPanel, - IndexPatternDimensionPanelComponent, - IndexPatternDimensionPanelProps, + IndexPatternDimensionEditorComponent, + IndexPatternDimensionEditorProps, + onDrop, + canHandleDrop, } from './dimension_panel'; -import { DropHandler, DragContextState } from '../../drag_drop'; +import { DragContextState } from '../../drag_drop'; import { createMockedDragDropContext } from '../mocks'; import { mountWithIntl as mount, shallowWithIntl as shallow } from 'test_utils/enzyme_helpers'; -import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public'; +import { + IUiSettingsClient, + SavedObjectsClientContract, + HttpSetup, + CoreSetup, +} from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { IndexPatternPrivateState } from '../types'; import { documentField } from '../document_field'; +import { OperationMetadata } from '../../types'; jest.mock('ui/new_platform'); jest.mock('../loader'); @@ -79,20 +80,12 @@ const expectedIndexPatterns = { }, }; -describe('IndexPatternDimensionPanel', () => { - let wrapper: ReactWrapper | ShallowWrapper; +describe('IndexPatternDimensionEditorPanel', () => { let state: IndexPatternPrivateState; let setState: jest.Mock; - let defaultProps: IndexPatternDimensionPanelProps; + let defaultProps: IndexPatternDimensionEditorProps; let dragDropContext: DragContextState; - function openPopover() { - wrapper - .find('[data-test-subj="indexPattern-configure-dimension"]') - .first() - .simulate('click'); - } - beforeEach(() => { state = { indexPatternRefs: [], @@ -134,7 +127,6 @@ describe('IndexPatternDimensionPanel', () => { dragDropContext = createMockedDragDropContext(); defaultProps = { - dragDropContext, state, setState, dateRange: { fromDate: 'now-1d', toDate: 'now' }, @@ -158,475 +150,582 @@ describe('IndexPatternDimensionPanel', () => { }), } as unknown) as DataPublicPluginStart['fieldFormats'], } as unknown) as DataPublicPluginStart, + core: {} as CoreSetup, }; jest.clearAllMocks(); }); - afterEach(() => { - if (wrapper) { - wrapper.unmount(); - } - }); + describe('Editor component', () => { + let wrapper: ReactWrapper | ShallowWrapper; - it('should display a configure button if dimension has no column yet', () => { - wrapper = mount(); - expect( - wrapper - .find('[data-test-subj="indexPattern-configure-dimension"]') - .first() - .prop('iconType') - ).toEqual('plusInCircleFilled'); - }); + afterEach(() => { + if (wrapper) { + wrapper.unmount(); + } + }); - it('should call the filterOperations function', () => { - const filterOperations = jest.fn().mockReturnValue(true); + it('should call the filterOperations function', () => { + const filterOperations = jest.fn().mockReturnValue(true); - wrapper = shallow( - - ); + wrapper = shallow( + + ); - expect(filterOperations).toBeCalled(); - }); + expect(filterOperations).toBeCalled(); + }); - it('should show field select combo box on click', () => { - wrapper = mount(); + it('should show field select combo box on click', () => { + wrapper = mount(); - openPopover(); + expect( + wrapper.find(EuiComboBox).filter('[data-test-subj="indexPattern-dimension-field"]') + ).toHaveLength(1); + }); - expect( - wrapper.find(EuiComboBox).filter('[data-test-subj="indexPattern-dimension-field"]') - ).toHaveLength(1); - }); + it('should not show any choices if the filter returns false', () => { + wrapper = mount( + false} + /> + ); - it('should not show any choices if the filter returns false', () => { - wrapper = mount( - false} - /> - ); + expect( + wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]')! + .prop('options')! + ).toHaveLength(0); + }); - openPopover(); + it('should list all field names and document as a whole in prioritized order', () => { + wrapper = mount(); - expect( - wrapper + const options = wrapper .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]')! - .prop('options')! - ).toHaveLength(0); - }); - - it('should list all field names and document as a whole in prioritized order', () => { - wrapper = mount(); - - openPopover(); - - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); - expect(options).toHaveLength(2); + expect(options).toHaveLength(2); - expect(options![0].label).toEqual('Records'); + expect(options![0].label).toEqual('Records'); - expect(options![1].options!.map(({ label }) => label)).toEqual([ - 'timestamp', - 'bytes', - 'memory', - 'source', - ]); - }); + expect(options![1].options!.map(({ label }) => label)).toEqual([ + 'timestamp', + 'bytes', + 'memory', + 'source', + ]); + }); - it('should hide fields that have no data', () => { - const props = { - ...defaultProps, - state: { - ...defaultProps.state, - existingFields: { - 'my-fake-index-pattern': { - timestamp: true, - source: true, + it('should hide fields that have no data', () => { + const props = { + ...defaultProps, + state: { + ...defaultProps.state, + existingFields: { + 'my-fake-index-pattern': { + timestamp: true, + source: true, + }, }, }, - }, - }; - wrapper = mount(); - - openPopover(); - - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); + }; + wrapper = mount(); - expect(options![1].options!.map(({ label }) => label)).toEqual(['timestamp', 'source']); - }); + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); - it('should indicate fields which are incompatible for the operation of the current column', () => { - wrapper = mount( - label)).toEqual(['timestamp', 'source']); + }); - // Private - operationType: 'max', - sourceField: 'bytes', + it('should indicate fields which are incompatible for the operation of the current column', () => { + wrapper = mount( + - ); - - openPopover(); - - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); + }} + /> + ); - expect(options![0]['data-test-subj']).toEqual('lns-fieldOptionIncompatible-Records'); + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); - expect( - options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] - ).toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'memory')[0]['data-test-subj'] - ).not.toContain('Incompatible'); - }); + expect(options![0]['data-test-subj']).toEqual('lns-fieldOptionIncompatible-Records'); - it('should indicate operations which are incompatible for the field of the current column', () => { - wrapper = mount( - label === 'timestamp')[0]['data-test-subj'] + ).toContain('Incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'memory')[0]['data-test-subj'] + ).not.toContain('Incompatible'); + }); - // Private - operationType: 'max', - sourceField: 'bytes', + it('should indicate operations which are incompatible for the field of the current column', () => { + wrapper = mount( + - ); - - openPopover(); + }} + /> + ); - interface ItemType { - name: string; - 'data-test-subj': string; - } - const items: Array> = wrapper.find(EuiSideNav).prop('items'); - const options = (items[0].items as unknown) as ItemType[]; + interface ItemType { + name: string; + 'data-test-subj': string; + } + const items: Array> = wrapper.find(EuiSideNav).prop('items'); + const options = (items[0].items as unknown) as ItemType[]; - expect(options.find(({ name }) => name === 'Minimum')!['data-test-subj']).not.toContain( - 'Incompatible' - ); + expect(options.find(({ name }) => name === 'Minimum')!['data-test-subj']).not.toContain( + 'Incompatible' + ); - expect(options.find(({ name }) => name === 'Date histogram')!['data-test-subj']).toContain( - 'Incompatible' - ); - }); + expect(options.find(({ name }) => name === 'Date histogram')!['data-test-subj']).toContain( + 'Incompatible' + ); + }); - it('should keep the operation when switching to another field compatible with this operation', () => { - const initialState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: { - label: 'Max of bytes', - dataType: 'number', - isBucketed: false, + it('should keep the operation when switching to another field compatible with this operation', () => { + const initialState: IndexPatternPrivateState = { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + label: 'Max of bytes', + dataType: 'number', + isBucketed: false, - // Private - operationType: 'max', - sourceField: 'bytes', - params: { format: { id: 'bytes' } }, + // Private + operationType: 'max', + sourceField: 'bytes', + params: { format: { id: 'bytes' } }, + }, }, }, }, - }, - }; - - wrapper = mount(); + }; - openPopover(); + wrapper = mount( + + ); - const comboBox = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]')!; - const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'memory')!; + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]')!; + const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'memory')!; - act(() => { - comboBox.prop('onChange')!([option]); - }); + act(() => { + comboBox.prop('onChange')!([option]); + }); - expect(setState).toHaveBeenCalledWith({ - ...initialState, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - operationType: 'max', - sourceField: 'memory', - params: { format: { id: 'bytes' } }, - // Other parts of this don't matter for this test - }), + expect(setState).toHaveBeenCalledWith({ + ...initialState, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + operationType: 'max', + sourceField: 'memory', + params: { format: { id: 'bytes' } }, + // Other parts of this don't matter for this test + }), + }, }, }, - }, + }); }); - }); - - it('should switch operations when selecting a field that requires another operation', () => { - wrapper = mount(); - openPopover(); + it('should switch operations when selecting a field that requires another operation', () => { + wrapper = mount(); - const comboBox = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]')!; - const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'source')!; + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]')!; + const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'source')!; - act(() => { - comboBox.prop('onChange')!([option]); - }); + act(() => { + comboBox.prop('onChange')!([option]); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - operationType: 'terms', - sourceField: 'source', - // Other parts of this don't matter for this test - }), + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + operationType: 'terms', + sourceField: 'source', + // Other parts of this don't matter for this test + }), + }, }, }, - }, + }); }); - }); - - it('should keep the field when switching to another operation compatible for this field', () => { - wrapper = mount( - { + wrapper = mount( + - ); - - openPopover(); + }} + /> + ); - act(() => { - wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); - }); + act(() => { + wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - operationType: 'min', - sourceField: 'bytes', - params: { format: { id: 'bytes' } }, - // Other parts of this don't matter for this test - }), + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + operationType: 'min', + sourceField: 'bytes', + params: { format: { id: 'bytes' } }, + // Other parts of this don't matter for this test + }), + }, }, }, - }, + }); }); - }); - it('should not set the state if selecting the currently active operation', () => { - wrapper = mount(); + it('should not set the state if selecting the currently active operation', () => { + wrapper = mount(); - openPopover(); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') + .simulate('click'); + }); - act(() => { - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') - .simulate('click'); + expect(setState).not.toHaveBeenCalled(); }); - expect(setState).not.toHaveBeenCalled(); - }); - - it('should update label on label input changes', () => { - wrapper = mount(); + it('should update label on label input changes', () => { + wrapper = mount(); - openPopover(); - - act(() => { - wrapper - .find('input[data-test-subj="indexPattern-label-edit"]') - .simulate('change', { target: { value: 'New Label' } }); - }); + act(() => { + wrapper + .find('input[data-test-subj="indexPattern-label-edit"]') + .simulate('change', { target: { value: 'New Label' } }); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - label: 'New Label', - // Other parts of this don't matter for this test - }), + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + label: 'New Label', + // Other parts of this don't matter for this test + }), + }, }, }, - }, + }); }); - }); - describe('transient invalid state', () => { - it('should not set the state if selecting an operation incompatible with the current field', () => { - wrapper = mount(); + describe('transient invalid state', () => { + it('should not set the state if selecting an operation incompatible with the current field', () => { + wrapper = mount(); - openPopover(); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') + .simulate('click'); + }); + + expect(setState).not.toHaveBeenCalled(); + }); + + it('should show error message in invalid state', () => { + wrapper = mount(); - act(() => { wrapper .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') .simulate('click'); + + expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).not.toHaveLength( + 0 + ); + + expect(setState).not.toHaveBeenCalled(); }); - expect(setState).not.toHaveBeenCalled(); - }); + it('should leave error state if a compatible operation is selected', () => { + wrapper = mount(); - it('should show error message in invalid state', () => { - wrapper = mount(); + wrapper + .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') + .simulate('click'); - openPopover(); + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') + .simulate('click'); - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') - .simulate('click'); + expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); + }); - expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).not.toHaveLength(0); + it('should indicate fields compatible with selected operation', () => { + wrapper = mount(); - expect(setState).not.toHaveBeenCalled(); - }); + wrapper + .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') + .simulate('click'); - it('should leave error state if a compatible operation is selected', () => { - wrapper = mount(); + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); - openPopover(); + expect(options![0]['data-test-subj']).toContain('Incompatible'); - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') - .simulate('click'); + expect( + options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] + ).toContain('Incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'source')[0]['data-test-subj'] + ).not.toContain('Incompatible'); + }); - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-date_histogram"]') - .simulate('click'); + it('should select compatible operation if field not compatible with selected operation', () => { + wrapper = mount( + + ); - expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); - }); + wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - it('should leave error state if the popover gets closed', () => { - wrapper = mount(); + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]'); + const options = comboBox.prop('options'); - openPopover(); + // options[1][2] is a `source` field of type `string` which doesn't support `avg` operation + act(() => { + comboBox.prop('onChange')!([options![1].options![2]]); + }); - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') - .simulate('click'); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: expect.objectContaining({ + sourceField: 'source', + operationType: 'terms', + // Other parts of this don't matter for this test + }), + }, + columnOrder: ['col1', 'col2'], + }, + }, + }); + }); - act(() => { - wrapper.find(EuiPopover).prop('closePopover')!(); + it('should select the Records field when count is selected', () => { + const initialState: IndexPatternPrivateState = { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: { + dataType: 'number', + isBucketed: false, + label: '', + operationType: 'avg', + sourceField: 'bytes', + }, + }, + }, + }, + }; + wrapper = mount( + + ); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-count"]') + .simulate('click'); + + const newColumnState = setState.mock.calls[0][0].layers.first.columns.col2; + expect(newColumnState.operationType).toEqual('count'); + expect(newColumnState.sourceField).toEqual('Records'); }); - openPopover(); + it('should indicate document and field compatibility with selected document operation', () => { + const initialState: IndexPatternPrivateState = { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: { + dataType: 'number', + isBucketed: false, + label: '', + operationType: 'count', + sourceField: 'Records', + }, + }, + }, + }, + }; + wrapper = mount( + + ); + + wrapper + .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') + .simulate('click'); - expect(wrapper.find('[data-test-subj="indexPattern-invalid-operation"]')).toHaveLength(0); - }); + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); - it('should indicate fields compatible with selected operation', () => { - wrapper = mount(); + expect(options![0]['data-test-subj']).toContain('Incompatible'); - openPopover(); + expect( + options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] + ).toContain('Incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'source')[0]['data-test-subj'] + ).not.toContain('Incompatible'); + }); - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') - .simulate('click'); + it('should set datasource state if compatible field is selected for operation', () => { + wrapper = mount(); - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); + act(() => { + wrapper + .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') + .simulate('click'); + }); - expect(options![0]['data-test-subj']).toContain('Incompatible'); + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]')!; + const option = comboBox + .prop('options')![1] + .options!.find(({ label }) => label === 'source')!; - expect( - options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] - ).toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'source')[0]['data-test-subj'] - ).not.toContain('Incompatible'); - }); + act(() => { + comboBox.prop('onChange')!([option]); + }); - it('should select compatible operation if field not compatible with selected operation', () => { - wrapper = mount(); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + sourceField: 'source', + operationType: 'terms', + }), + }, + }, + }, + }); + }); + }); - openPopover(); + it('should support selecting the operation before the field', () => { + wrapper = mount(); wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); @@ -635,9 +734,8 @@ describe('IndexPatternDimensionPanel', () => { .filter('[data-test-subj="indexPattern-dimension-field"]'); const options = comboBox.prop('options'); - // options[1][2] is a `source` field of type `string` which doesn't support `avg` operation act(() => { - comboBox.prop('onChange')!([options![1].options![2]]); + comboBox.prop('onChange')!([options![1].options![0]]); }); expect(setState).toHaveBeenCalledWith({ @@ -648,8 +746,8 @@ describe('IndexPatternDimensionPanel', () => { columns: { ...state.layers.first.columns, col2: expect.objectContaining({ - sourceField: 'source', - operationType: 'terms', + sourceField: 'bytes', + operationType: 'avg', // Other parts of this don't matter for this test }), }, @@ -659,41 +757,93 @@ describe('IndexPatternDimensionPanel', () => { }); }); - it('should select the Records field when count is selected', () => { - const initialState: IndexPatternPrivateState = { + it('should select operation directly if only one field is possible', () => { + const initialState = { + ...state, + indexPatterns: { + 1: { + ...state.indexPatterns['1'], + fields: state.indexPatterns['1'].fields.filter(field => field.name !== 'memory'), + }, + }, + }; + + wrapper = mount( + + ); + + wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); + + expect(setState).toHaveBeenCalledWith({ + ...initialState, + layers: { + first: { + ...initialState.layers.first, + columns: { + ...initialState.layers.first.columns, + col2: expect.objectContaining({ + sourceField: 'bytes', + operationType: 'avg', + // Other parts of this don't matter for this test + }), + }, + columnOrder: ['col1', 'col2'], + }, + }, + }); + }); + + it('should select operation directly if only document is possible', () => { + wrapper = mount(); + + wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click'); + + expect(setState).toHaveBeenCalledWith({ ...state, layers: { first: { ...state.layers.first, columns: { ...state.layers.first.columns, - col2: { - dataType: 'number', - isBucketed: false, - label: '', - operationType: 'avg', - sourceField: 'bytes', - }, + col2: expect.objectContaining({ + operationType: 'count', + // Other parts of this don't matter for this test + }), }, + columnOrder: ['col1', 'col2'], }, }, - }; - wrapper = mount( - - ); + }); + }); - openPopover(); + it('should indicate compatible fields when selecting the operation first', () => { + wrapper = mount(); - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-count"]') - .simulate('click'); + wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - const newColumnState = setState.mock.calls[0][0].layers.first.columns.col2; - expect(newColumnState.operationType).toEqual('count'); - expect(newColumnState.sourceField).toEqual('Records'); + const options = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('options'); + + expect(options![0]['data-test-subj']).toContain('Incompatible'); + + expect( + options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] + ).toContain('Incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'bytes')[0]['data-test-subj'] + ).not.toContain('Incompatible'); + expect( + options![1].options!.filter(({ label }) => label === 'memory')[0]['data-test-subj'] + ).not.toContain('Incompatible'); }); - it('should indicate document and field compatibility with selected document operation', () => { + it('should indicate document compatibility when document operation is selected', () => { const initialState: IndexPatternPrivateState = { ...state, layers: { @@ -713,45 +863,56 @@ describe('IndexPatternDimensionPanel', () => { }, }; wrapper = mount( - + ); - openPopover(); - - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') - .simulate('click'); - const options = wrapper .find(EuiComboBox) .filter('[data-test-subj="indexPattern-dimension-field"]') .prop('options'); - expect(options![0]['data-test-subj']).toContain('Incompatible'); + expect(options![0]['data-test-subj']).not.toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] - ).toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'source')[0]['data-test-subj'] - ).not.toContain('Incompatible'); + options![1].options!.map(operation => + expect(operation['data-test-subj']).toContain('Incompatible') + ); }); - it('should set datasource state if compatible field is selected for operation', () => { - wrapper = mount(); + it('should show all operations that are not filtered out', () => { + wrapper = mount( + !op.isBucketed && op.dataType === 'number'} + /> + ); - openPopover(); + interface ItemType { + name: React.ReactNode; + } + const items: Array> = wrapper.find(EuiSideNav).prop('items'); + const options = (items[0].items as unknown) as ItemType[]; + + expect(options.map(({ name }: { name: React.ReactNode }) => name)).toEqual([ + 'Unique count', + 'Average', + 'Count', + 'Maximum', + 'Minimum', + 'Sum', + ]); + }); - act(() => { - wrapper - .find('button[data-test-subj="lns-indexPatternDimensionIncompatible-terms"]') - .simulate('click'); - }); + it('should add a column on selection of a field', () => { + wrapper = mount(); const comboBox = wrapper .find(EuiComboBox) .filter('[data-test-subj="indexPattern-dimension-field"]')!; - const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'source')!; + const option = comboBox.prop('options')![1].options![0]; act(() => { comboBox.prop('onChange')!([option]); @@ -764,479 +925,237 @@ describe('IndexPatternDimensionPanel', () => { ...state.layers.first, columns: { ...state.layers.first.columns, - col1: expect.objectContaining({ - sourceField: 'source', - operationType: 'terms', + col2: expect.objectContaining({ + sourceField: 'bytes', + // Other parts of this don't matter for this test }), }, + columnOrder: ['col1', 'col2'], }, }, }); }); - }); - - it('should support selecting the operation before the field', () => { - wrapper = mount(); - - openPopover(); - - wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - - const comboBox = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]'); - const options = comboBox.prop('options'); - - act(() => { - comboBox.prop('onChange')!([options![1].options![0]]); - }); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - sourceField: 'bytes', - operationType: 'avg', - // Other parts of this don't matter for this test - }), - }, - columnOrder: ['col1', 'col2'], - }, - }, - }); - }); - - it('should select operation directly if only one field is possible', () => { - const initialState = { - ...state, - indexPatterns: { - 1: { - ...state.indexPatterns['1'], - fields: state.indexPatterns['1'].fields.filter(field => field.name !== 'memory'), - }, - }, - }; - - wrapper = mount( - - ); - - openPopover(); - - wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - - expect(setState).toHaveBeenCalledWith({ - ...initialState, - layers: { - first: { - ...initialState.layers.first, - columns: { - ...initialState.layers.first.columns, - col2: expect.objectContaining({ - sourceField: 'bytes', - operationType: 'avg', - // Other parts of this don't matter for this test - }), - }, - columnOrder: ['col1', 'col2'], - }, - }, - }); - }); - - it('should select operation directly if only document is possible', () => { - wrapper = mount(); - - openPopover(); - - wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click'); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - operationType: 'count', - // Other parts of this don't matter for this test - }), - }, - columnOrder: ['col1', 'col2'], - }, - }, - }); - }); - it('should indicate compatible fields when selecting the operation first', () => { - wrapper = mount(); - - openPopover(); - - wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); - - expect(options![0]['data-test-subj']).toContain('Incompatible'); - - expect( - options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj'] - ).toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'bytes')[0]['data-test-subj'] - ).not.toContain('Incompatible'); - expect( - options![1].options!.filter(({ label }) => label === 'memory')[0]['data-test-subj'] - ).not.toContain('Incompatible'); - }); - - it('should indicate document compatibility when document operation is selected', () => { - const initialState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: { - dataType: 'number', - isBucketed: false, - label: '', - operationType: 'count', - sourceField: 'Records', - }, - }, - }, - }, - }; - wrapper = mount( - - ); - - openPopover(); - - const options = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('options'); - - expect(options![0]['data-test-subj']).not.toContain('Incompatible'); - - options![1].options!.map(operation => - expect(operation['data-test-subj']).toContain('Incompatible') - ); - }); - - it('should show all operations that are not filtered out', () => { - wrapper = mount( - !op.isBucketed && op.dataType === 'number'} - /> - ); - - openPopover(); - - interface ItemType { - name: React.ReactNode; - } - const items: Array> = wrapper.find(EuiSideNav).prop('items'); - const options = (items[0].items as unknown) as ItemType[]; - - expect(options.map(({ name }: { name: React.ReactNode }) => name)).toEqual([ - 'Unique count', - 'Average', - 'Count', - 'Maximum', - 'Minimum', - 'Sum', - ]); - }); - - it('should add a column on selection of a field', () => { - wrapper = mount(); - - openPopover(); - - const comboBox = wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]')!; - const option = comboBox.prop('options')![1].options![0]; - - act(() => { - comboBox.prop('onChange')!([option]); - }); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - sourceField: 'bytes', - // Other parts of this don't matter for this test - }), - }, - columnOrder: ['col1', 'col2'], - }, - }, - }); - }); - - it('should use helper function when changing the function', () => { - const initialState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: { - label: 'Max of bytes', - dataType: 'number', - isBucketed: false, + it('should use helper function when changing the function', () => { + const initialState: IndexPatternPrivateState = { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + label: 'Max of bytes', + dataType: 'number', + isBucketed: false, - // Private - operationType: 'max', - sourceField: 'bytes', + // Private + operationType: 'max', + sourceField: 'bytes', + }, }, }, }, - }, - }; - wrapper = mount(); - - openPopover(); - - act(() => { - wrapper - .find('[data-test-subj="lns-indexPatternDimension-min"]') - .first() - .prop('onClick')!({} as React.MouseEvent<{}, MouseEvent>); - }); - - expect(changeColumn).toHaveBeenCalledWith({ - state: initialState, - columnId: 'col1', - layerId: 'first', - newColumn: expect.objectContaining({ - sourceField: 'bytes', - operationType: 'min', - }), - }); - }); - - it('should clear the dimension with the clear button', () => { - wrapper = mount(); - - const clearButton = wrapper.find( - 'EuiButtonIcon[data-test-subj="indexPattern-dimensionPopover-remove"]' - ); + }; + wrapper = mount( + + ); - act(() => { - clearButton.simulate('click'); - }); + act(() => { + wrapper + .find('[data-test-subj="lns-indexPatternDimension-min"]') + .first() + .prop('onClick')!({} as React.MouseEvent<{}, MouseEvent>); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - indexPatternId: '1', - columns: {}, - columnOrder: [], - }, - }, + expect(changeColumn).toHaveBeenCalledWith({ + state: initialState, + columnId: 'col1', + layerId: 'first', + newColumn: expect.objectContaining({ + sourceField: 'bytes', + operationType: 'min', + }), + }); }); - }); - - it('should clear the dimension when removing the selection in field combobox', () => { - wrapper = mount(); - openPopover(); + it('should clear the dimension when removing the selection in field combobox', () => { + wrapper = mount(); - act(() => { - wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-field"]') - .prop('onChange')!([]); - }); + act(() => { + wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]') + .prop('onChange')!([]); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - indexPatternId: '1', - columns: {}, - columnOrder: [], + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + indexPatternId: '1', + columns: {}, + columnOrder: [], + }, }, - }, + }); }); - }); - it('allows custom format', () => { - const stateWithNumberCol: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Average of bar', - dataType: 'number', - isBucketed: false, - // Private - operationType: 'avg', - sourceField: 'bar', + it('allows custom format', () => { + const stateWithNumberCol: IndexPatternPrivateState = { + ...state, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Average of bar', + dataType: 'number', + isBucketed: false, + // Private + operationType: 'avg', + sourceField: 'bar', + }, }, }, }, - }, - }; - - wrapper = mount(); + }; - openPopover(); + wrapper = mount( + + ); - act(() => { - wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-format"]') - .prop('onChange')!([{ value: 'bytes', label: 'Bytes' }]); - }); + act(() => { + wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-format"]') + .prop('onChange')!([{ value: 'bytes', label: 'Bytes' }]); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - params: { - format: { id: 'bytes', params: { decimals: 2 } }, - }, - }), + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + params: { + format: { id: 'bytes', params: { decimals: 2 } }, + }, + }), + }, }, }, - }, + }); }); - }); - it('keeps decimal places while switching', () => { - const stateWithNumberCol: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Average of bar', - dataType: 'number', - isBucketed: false, - // Private - operationType: 'avg', - sourceField: 'bar', - params: { - format: { id: 'bytes', params: { decimals: 0 } }, + it('keeps decimal places while switching', () => { + const stateWithNumberCol: IndexPatternPrivateState = { + ...state, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Average of bar', + dataType: 'number', + isBucketed: false, + // Private + operationType: 'avg', + sourceField: 'bar', + params: { + format: { id: 'bytes', params: { decimals: 0 } }, + }, }, }, }, }, - }, - }; + }; - wrapper = mount(); + wrapper = mount( + + ); - openPopover(); + act(() => { + wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-format"]') + .prop('onChange')!([{ value: '', label: 'Default' }]); + }); - act(() => { - wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-format"]') - .prop('onChange')!([{ value: '', label: 'Default' }]); - }); + act(() => { + wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-format"]') + .prop('onChange')!([{ value: 'number', label: 'Number' }]); + }); - act(() => { - wrapper - .find(EuiComboBox) - .filter('[data-test-subj="indexPattern-dimension-format"]') - .prop('onChange')!([{ value: 'number', label: 'Number' }]); + expect( + wrapper + .find(EuiFieldNumber) + .filter('[data-test-subj="indexPattern-dimension-formatDecimals"]') + .prop('value') + ).toEqual(0); }); - expect( - wrapper - .find(EuiFieldNumber) - .filter('[data-test-subj="indexPattern-dimension-formatDecimals"]') - .prop('value') - ).toEqual(0); - }); - - it('allows custom format with number of decimal places', () => { - const stateWithNumberCol: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Average of bar', - dataType: 'number', - isBucketed: false, - // Private - operationType: 'avg', - sourceField: 'bar', - params: { - format: { id: 'bytes', params: { decimals: 2 } }, + it('allows custom format with number of decimal places', () => { + const stateWithNumberCol: IndexPatternPrivateState = { + ...state, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Average of bar', + dataType: 'number', + isBucketed: false, + // Private + operationType: 'avg', + sourceField: 'bar', + params: { + format: { id: 'bytes', params: { decimals: 2 } }, + }, }, }, }, }, - }, - }; - - wrapper = mount(); + }; - openPopover(); + wrapper = mount( + + ); - act(() => { - wrapper - .find(EuiFieldNumber) - .filter('[data-test-subj="indexPattern-dimension-formatDecimals"]') - .prop('onChange')!({ target: { value: '0' } }); - }); + act(() => { + wrapper + .find(EuiFieldNumber) + .filter('[data-test-subj="indexPattern-dimension-formatDecimals"]') + .prop('onChange')!({ target: { value: '0' } }); + }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - params: { - format: { id: 'bytes', params: { decimals: 0 } }, - }, - }), + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + params: { + format: { id: 'bytes', params: { decimals: 0 } }, + }, + }), + }, }, }, - }, + }); }); }); - describe('drag and drop', () => { + describe('Drag and drop', () => { function dragDropState(): IndexPatternPrivateState { return { indexPatternRefs: [], @@ -1287,112 +1206,80 @@ describe('IndexPatternDimensionPanel', () => { } it('is not droppable if no drag is happening', () => { - wrapper = mount( - - ); - expect( - wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('droppable') - ).toBeFalsy(); + canHandleDrop({ + ...defaultProps, + dragDropContext, + state: dragDropState(), + layerId: 'myLayer', + }) + ).toBe(false); }); it('is not droppable if the dragged item has no field', () => { - wrapper = shallow( - - ); - - expect( - wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('droppable') - ).toBeFalsy(); + }, + }) + ).toBe(false); }); it('is not droppable if field is not supported by filterOperations', () => { - wrapper = shallow( - false} - layerId="myLayer" - /> - ); - - expect( - wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('droppable') - ).toBeFalsy(); + }, + state: dragDropState(), + filterOperations: () => false, + layerId: 'myLayer', + }) + ).toBe(false); }); it('is droppable if the field is supported by filterOperations', () => { - wrapper = shallow( - op.dataType === 'number'} - layerId="myLayer" - /> - ); - - expect( - wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('droppable') - ).toBeTruthy(); + }, + state: dragDropState(), + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + layerId: 'myLayer', + }) + ).toBe(true); }); - it('is notdroppable if the field belongs to another index pattern', () => { - wrapper = shallow( - { + expect( + canHandleDrop({ + ...defaultProps, + dragDropContext: { ...dragDropContext, dragging: { field: { type: 'number', name: 'bar', aggregatable: true }, indexPatternId: 'foo2', }, - }} - state={dragDropState()} - filterOperations={op => op.dataType === 'number'} - layerId="myLayer" - /> - ); - - expect( - wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('droppable') - ).toBeFalsy(); + }, + state: dragDropState(), + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + layerId: 'myLayer', + }) + ).toBe(false); }); it('appends the dropped column when a field is dropped', () => { @@ -1401,27 +1288,18 @@ describe('IndexPatternDimensionPanel', () => { indexPatternId: 'foo', }; const testState = dragDropState(); - wrapper = shallow( - op.dataType === 'number'} - layerId="myLayer" - /> - ); - - act(() => { - const onDrop = wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('onDrop') as DropHandler; - onDrop(dragging); + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + layerId: 'myLayer', }); expect(setState).toBeCalledTimes(1); @@ -1449,27 +1327,17 @@ describe('IndexPatternDimensionPanel', () => { indexPatternId: 'foo', }; const testState = dragDropState(); - wrapper = shallow( - op.isBucketed} - layerId="myLayer" - /> - ); - - act(() => { - const onDrop = wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('onDrop') as DropHandler; - - onDrop(dragging); + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.isBucketed, + layerId: 'myLayer', }); expect(setState).toBeCalledTimes(1); @@ -1497,26 +1365,16 @@ describe('IndexPatternDimensionPanel', () => { indexPatternId: 'foo', }; const testState = dragDropState(); - wrapper = shallow( - op.dataType === 'number'} - layerId="myLayer" - /> - ); - - act(() => { - const onDrop = wrapper - .find('[data-test-subj="indexPattern-dropTarget"]') - .first() - .prop('onDrop') as DropHandler; - - onDrop(dragging); + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + layerId: 'myLayer', }); expect(setState).toBeCalledTimes(1); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index 59350ff215c27..5d87137db3d39 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -5,27 +5,36 @@ */ import _ from 'lodash'; -import React, { memo, useMemo } from 'react'; -import { EuiButtonIcon } from '@elastic/eui'; +import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; +import { EuiLink } from '@elastic/eui'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { + DatasourceDimensionTriggerProps, + DatasourceDimensionEditorProps, + DatasourceDimensionDropProps, + DatasourceDimensionDropHandlerProps, +} from '../../types'; import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public'; -import { DatasourceDimensionPanelProps, StateSetter } from '../../types'; import { IndexPatternColumn, OperationType } from '../indexpattern'; import { getAvailableOperationsByMetadata, buildColumn, changeField } from '../operations'; import { PopoverEditor } from './popover_editor'; -import { DragContextState, ChildDragDropProvider, DragDrop } from '../../drag_drop'; -import { changeColumn, deleteColumn } from '../state_helpers'; +import { changeColumn } from '../state_helpers'; import { isDraggedField, hasField } from '../utils'; import { IndexPatternPrivateState, IndexPatternField } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; import { DateRange } from '../../../../../../plugins/lens/common'; -export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { - state: IndexPatternPrivateState; - setState: StateSetter; - dragDropContext: DragContextState; +export type IndexPatternDimensionTriggerProps = DatasourceDimensionTriggerProps< + IndexPatternPrivateState +> & { + uniqueLabel: string; +}; + +export type IndexPatternDimensionEditorProps = DatasourceDimensionEditorProps< + IndexPatternPrivateState +> & { uiSettings: IUiSettingsClient; storage: IStorageWrapper; savedObjectsClient: SavedObjectsClientContract; @@ -41,152 +50,181 @@ export interface OperationFieldSupportMatrix { fieldByOperation: Partial>; } -export const IndexPatternDimensionPanelComponent = function IndexPatternDimensionPanel( - props: IndexPatternDimensionPanelProps -) { +type Props = Pick< + DatasourceDimensionDropProps, + 'layerId' | 'columnId' | 'state' | 'filterOperations' +>; + +// TODO: This code has historically been memoized, as a potentially performance +// sensitive task. If we can add memoization without breaking the behavior, we should. +const getOperationFieldSupportMatrix = (props: Props): OperationFieldSupportMatrix => { const layerId = props.layerId; const currentIndexPattern = props.state.indexPatterns[props.state.layers[layerId].indexPatternId]; - const operationFieldSupportMatrix = useMemo(() => { - const filteredOperationsByMetadata = getAvailableOperationsByMetadata( - currentIndexPattern - ).filter(operation => props.filterOperations(operation.operationMetaData)); - - const supportedOperationsByField: Partial> = {}; - const supportedFieldsByOperation: Partial> = {}; - - filteredOperationsByMetadata.forEach(({ operations }) => { - operations.forEach(operation => { - if (supportedOperationsByField[operation.field]) { - supportedOperationsByField[operation.field]!.push(operation.operationType); - } else { - supportedOperationsByField[operation.field] = [operation.operationType]; - } - - if (supportedFieldsByOperation[operation.operationType]) { - supportedFieldsByOperation[operation.operationType]!.push(operation.field); - } else { - supportedFieldsByOperation[operation.operationType] = [operation.field]; - } - }); + const filteredOperationsByMetadata = getAvailableOperationsByMetadata( + currentIndexPattern + ).filter(operation => props.filterOperations(operation.operationMetaData)); + + const supportedOperationsByField: Partial> = {}; + const supportedFieldsByOperation: Partial> = {}; + + filteredOperationsByMetadata.forEach(({ operations }) => { + operations.forEach(operation => { + if (supportedOperationsByField[operation.field]) { + supportedOperationsByField[operation.field]!.push(operation.operationType); + } else { + supportedOperationsByField[operation.field] = [operation.operationType]; + } + + if (supportedFieldsByOperation[operation.operationType]) { + supportedFieldsByOperation[operation.operationType]!.push(operation.field); + } else { + supportedFieldsByOperation[operation.operationType] = [operation.field]; + } }); - return { - operationByField: _.mapValues(supportedOperationsByField, _.uniq), - fieldByOperation: _.mapValues(supportedFieldsByOperation, _.uniq), - }; - }, [currentIndexPattern, props.filterOperations]); + }); + return { + operationByField: _.mapValues(supportedOperationsByField, _.uniq), + fieldByOperation: _.mapValues(supportedFieldsByOperation, _.uniq), + }; +}; - const selectedColumn: IndexPatternColumn | null = - props.state.layers[layerId].columns[props.columnId] || null; +export function canHandleDrop(props: DatasourceDimensionDropProps) { + const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props); + + const { dragging } = props.dragDropContext; + const layerIndexPatternId = props.state.layers[props.layerId].indexPatternId; function hasOperationForField(field: IndexPatternField) { return Boolean(operationFieldSupportMatrix.operationByField[field.name]); } - function canHandleDrop() { - const { dragging } = props.dragDropContext; - const layerIndexPatternId = props.state.layers[props.layerId].indexPatternId; + return ( + isDraggedField(dragging) && + layerIndexPatternId === dragging.indexPatternId && + Boolean(hasOperationForField(dragging.field)) + ); +} + +export function onDrop( + props: DatasourceDimensionDropHandlerProps +): boolean { + const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props); + const droppedItem = props.droppedItem; + + function hasOperationForField(field: IndexPatternField) { + return Boolean(operationFieldSupportMatrix.operationByField[field.name]); + } - return ( - isDraggedField(dragging) && - layerIndexPatternId === dragging.indexPatternId && - Boolean(hasOperationForField(dragging.field)) - ); + if (!isDraggedField(droppedItem) || !hasOperationForField(droppedItem.field)) { + // TODO: What do we do if we couldn't find a column? + return false; } + const operationsForNewField = + operationFieldSupportMatrix.operationByField[droppedItem.field.name]; + + const layerId = props.layerId; + const selectedColumn: IndexPatternColumn | null = + props.state.layers[layerId].columns[props.columnId] || null; + const currentIndexPattern = + props.state.indexPatterns[props.state.layers[layerId]?.indexPatternId]; + + // We need to check if dragging in a new field, was just a field change on the same + // index pattern and on the same operations (therefore checking if the new field supports + // our previous operation) + const hasFieldChanged = + selectedColumn && + hasField(selectedColumn) && + selectedColumn.sourceField !== droppedItem.field.name && + operationsForNewField && + operationsForNewField.includes(selectedColumn.operationType); + + // If only the field has changed use the onFieldChange method on the operation to get the + // new column, otherwise use the regular buildColumn to get a new column. + const newColumn = hasFieldChanged + ? changeField(selectedColumn, currentIndexPattern, droppedItem.field) + : buildColumn({ + op: operationsForNewField ? operationsForNewField[0] : undefined, + columns: props.state.layers[props.layerId].columns, + indexPattern: currentIndexPattern, + layerId, + suggestedPriority: props.suggestedPriority, + field: droppedItem.field, + previousColumn: selectedColumn, + }); + + trackUiEvent('drop_onto_dimension'); + const hasData = Object.values(props.state.layers).some(({ columns }) => columns.length); + trackUiEvent(hasData ? 'drop_non_empty' : 'drop_empty'); + + props.setState( + changeColumn({ + state: props.state, + layerId, + columnId: props.columnId, + newColumn, + // If the field has changed, the onFieldChange method needs to take care of everything including moving + // over params. If we create a new column above we want changeColumn to move over params. + keepParams: !hasFieldChanged, + }) + ); + + return true; +} + +export const IndexPatternDimensionTriggerComponent = function IndexPatternDimensionTrigger( + props: IndexPatternDimensionTriggerProps +) { + const layerId = props.layerId; + + const selectedColumn: IndexPatternColumn | null = + props.state.layers[layerId].columns[props.columnId] || null; + + const { columnId, uniqueLabel } = props; + if (!selectedColumn) { + return null; + } + return ( + { + props.togglePopover(); + }} + data-test-subj="lns-dimensionTrigger" + aria-label={i18n.translate('xpack.lens.configure.editConfig', { + defaultMessage: 'Edit configuration', + })} + title={i18n.translate('xpack.lens.configure.editConfig', { + defaultMessage: 'Edit configuration', + })} + > + {uniqueLabel} + + ); +}; + +export const IndexPatternDimensionEditorComponent = function IndexPatternDimensionPanel( + props: IndexPatternDimensionEditorProps +) { + const layerId = props.layerId; + const currentIndexPattern = + props.state.indexPatterns[props.state.layers[layerId]?.indexPatternId]; + const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props); + + const selectedColumn: IndexPatternColumn | null = + props.state.layers[layerId].columns[props.columnId] || null; + return ( - - { - if (!isDraggedField(droppedItem) || !hasOperationForField(droppedItem.field)) { - // TODO: What do we do if we couldn't find a column? - return; - } - - const operationsForNewField = - operationFieldSupportMatrix.operationByField[droppedItem.field.name]; - - // We need to check if dragging in a new field, was just a field change on the same - // index pattern and on the same operations (therefore checking if the new field supports - // our previous operation) - const hasFieldChanged = - selectedColumn && - hasField(selectedColumn) && - selectedColumn.sourceField !== droppedItem.field.name && - operationsForNewField && - operationsForNewField.includes(selectedColumn.operationType); - - // If only the field has changed use the onFieldChange method on the operation to get the - // new column, otherwise use the regular buildColumn to get a new column. - const newColumn = hasFieldChanged - ? changeField(selectedColumn, currentIndexPattern, droppedItem.field) - : buildColumn({ - op: operationsForNewField ? operationsForNewField[0] : undefined, - columns: props.state.layers[props.layerId].columns, - indexPattern: currentIndexPattern, - layerId, - suggestedPriority: props.suggestedPriority, - field: droppedItem.field, - previousColumn: selectedColumn, - }); - - trackUiEvent('drop_onto_dimension'); - const hasData = Object.values(props.state.layers).some(({ columns }) => columns.length); - trackUiEvent(hasData ? 'drop_non_empty' : 'drop_empty'); - - props.setState( - changeColumn({ - state: props.state, - layerId, - columnId: props.columnId, - newColumn, - // If the field has changed, the onFieldChange method needs to take care of everything including moving - // over params. If we create a new column above we want changeColumn to move over params. - keepParams: !hasFieldChanged, - }) - ); - }} - > - - {selectedColumn && ( - { - trackUiEvent('indexpattern_dimension_removed'); - props.setState( - deleteColumn({ - state: props.state, - layerId, - columnId: props.columnId, - }) - ); - if (props.onRemove) { - props.onRemove(props.columnId); - } - }} - /> - )} - - + ); }; -export const IndexPatternDimensionPanel = memo(IndexPatternDimensionPanelComponent); +export const IndexPatternDimensionTrigger = memo(IndexPatternDimensionTriggerComponent); +export const IndexPatternDimensionEditor = memo(IndexPatternDimensionEditorComponent); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx index 056a8d177dfe8..e26c338b6e240 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx @@ -7,22 +7,18 @@ import _ from 'lodash'; import React, { useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import { - EuiPopover, EuiFlexItem, EuiFlexGroup, EuiSideNav, EuiCallOut, EuiFormRow, EuiFieldText, - EuiLink, - EuiButtonEmpty, EuiSpacer, } from '@elastic/eui'; import classNames from 'classnames'; import { IndexPatternColumn, OperationType } from '../indexpattern'; -import { IndexPatternDimensionPanelProps, OperationFieldSupportMatrix } from './dimension_panel'; +import { IndexPatternDimensionEditorProps, OperationFieldSupportMatrix } from './dimension_panel'; import { operationDefinitionMap, getOperationDisplay, @@ -39,7 +35,7 @@ import { FormatSelector } from './format_selector'; const operationPanels = getOperationDisplay(); -export interface PopoverEditorProps extends IndexPatternDimensionPanelProps { +export interface PopoverEditorProps extends IndexPatternDimensionEditorProps { selectedColumn?: IndexPatternColumn; operationFieldSupportMatrix: OperationFieldSupportMatrix; currentIndexPattern: IndexPattern; @@ -67,11 +63,9 @@ export function PopoverEditor(props: PopoverEditorProps) { setState, layerId, currentIndexPattern, - uniqueLabel, hideGrouping, } = props; const { operationByField, fieldByOperation } = operationFieldSupportMatrix; - const [isPopoverOpen, setPopoverOpen] = useState(false); const [ incompatibleSelectedOperationType, setInvalidOperationType, @@ -115,14 +109,14 @@ export function PopoverEditor(props: PopoverEditorProps) { items: getOperationTypes().map(({ operationType, compatibleWithCurrentField }) => ({ name: operationPanels[operationType].displayName, id: operationType as string, - className: classNames('lnsPopoverEditor__operation', { - 'lnsPopoverEditor__operation--selected': Boolean( + className: classNames('lnsIndexPatternDimensionEditor__operation', { + 'lnsIndexPatternDimensionEditor__operation--selected': Boolean( incompatibleSelectedOperationType === operationType || (!incompatibleSelectedOperationType && selectedColumn && selectedColumn.operationType === operationType) ), - 'lnsPopoverEditor__operation--incompatible': !compatibleWithCurrentField, + 'lnsIndexPatternDimensionEditor__operation--incompatible': !compatibleWithCurrentField, }), 'data-test-subj': `lns-indexPatternDimension${ compatibleWithCurrentField ? '' : 'Incompatible' @@ -188,246 +182,193 @@ export function PopoverEditor(props: PopoverEditorProps) { } return ( - { - setPopoverOpen(!isPopoverOpen); +
+ + + { + setState( + deleteColumn({ + state, + layerId, + columnId, + }) + ); }} - data-test-subj="indexPattern-configure-dimension" - aria-label={i18n.translate('xpack.lens.configure.editConfig', { - defaultMessage: 'Edit configuration', - })} - title={i18n.translate('xpack.lens.configure.editConfig', { - defaultMessage: 'Edit configuration', - })} - > - {uniqueLabel} - - ) : ( - <> - setPopoverOpen(!isPopoverOpen)} - size="xs" - > - - - - ) - } - isOpen={isPopoverOpen} - closePopover={() => { - setPopoverOpen(false); - setInvalidOperationType(null); - }} - anchorPosition="leftUp" - withTitle - panelPaddingSize="s" - > - {isPopoverOpen && ( - - - { - setState( - deleteColumn({ - state, - layerId, - columnId, - }) - ); - }} - onChoose={choice => { - let column: IndexPatternColumn; - if ( - !incompatibleSelectedOperationType && - selectedColumn && - 'field' in choice && - choice.operationType === selectedColumn.operationType - ) { - // If we just changed the field are not in an error state and the operation didn't change, - // we use the operations onFieldChange method to calculate the new column. - column = changeField(selectedColumn, currentIndexPattern, fieldMap[choice.field]); - } else { - // Otherwise we'll use the buildColumn method to calculate a new column - const compatibleOperations = - ('field' in choice && - operationFieldSupportMatrix.operationByField[choice.field]) || - []; - let operation; - if (compatibleOperations.length > 0) { - operation = - incompatibleSelectedOperationType && - compatibleOperations.includes(incompatibleSelectedOperationType) - ? incompatibleSelectedOperationType - : compatibleOperations[0]; - } else if ('field' in choice) { - operation = choice.operationType; - } - column = buildColumn({ - columns: props.state.layers[props.layerId].columns, - field: fieldMap[choice.field], - indexPattern: currentIndexPattern, - layerId: props.layerId, - suggestedPriority: props.suggestedPriority, - op: operation as OperationType, - previousColumn: selectedColumn, - }); + onChoose={choice => { + let column: IndexPatternColumn; + if ( + !incompatibleSelectedOperationType && + selectedColumn && + 'field' in choice && + choice.operationType === selectedColumn.operationType + ) { + // If we just changed the field are not in an error state and the operation didn't change, + // we use the operations onFieldChange method to calculate the new column. + column = changeField(selectedColumn, currentIndexPattern, fieldMap[choice.field]); + } else { + // Otherwise we'll use the buildColumn method to calculate a new column + const compatibleOperations = + ('field' in choice && + operationFieldSupportMatrix.operationByField[choice.field]) || + []; + let operation; + if (compatibleOperations.length > 0) { + operation = + incompatibleSelectedOperationType && + compatibleOperations.includes(incompatibleSelectedOperationType) + ? incompatibleSelectedOperationType + : compatibleOperations[0]; + } else if ('field' in choice) { + operation = choice.operationType; } + column = buildColumn({ + columns: props.state.layers[props.layerId].columns, + field: fieldMap[choice.field], + indexPattern: currentIndexPattern, + layerId: props.layerId, + suggestedPriority: props.suggestedPriority, + op: operation as OperationType, + previousColumn: selectedColumn, + }); + } - setState( - changeColumn({ - state, - layerId, - columnId, - newColumn: column, - keepParams: false, - }) - ); - setInvalidOperationType(null); - }} - /> - - - - - - - - {incompatibleSelectedOperationType && selectedColumn && ( - - )} - {incompatibleSelectedOperationType && !selectedColumn && ( - - )} - {!incompatibleSelectedOperationType && ParamEditor && ( - <> - - - - )} - {!incompatibleSelectedOperationType && selectedColumn && ( - - { - setState( - changeColumn({ - state, - layerId, - columnId, - newColumn: { - ...selectedColumn, - label: e.target.value, - }, - }) - ); - }} - /> - - )} - - {!hideGrouping && ( - { - setState({ - ...state, - layers: { - ...state.layers, - [props.layerId]: { - ...state.layers[props.layerId], - columnOrder, - }, - }, - }); - }} + setState( + changeColumn({ + state, + layerId, + columnId, + newColumn: column, + keepParams: false, + }) + ); + setInvalidOperationType(null); + }} + /> + + + + + + + + {incompatibleSelectedOperationType && selectedColumn && ( + + )} + {incompatibleSelectedOperationType && !selectedColumn && ( + + )} + {!incompatibleSelectedOperationType && ParamEditor && ( + <> + - )} - - {selectedColumn && selectedColumn.dataType === 'number' ? ( - { + + + )} + {!incompatibleSelectedOperationType && selectedColumn && ( + + { setState( - updateColumnParam({ + changeColumn({ state, layerId, - currentColumn: selectedColumn, - paramName: 'format', - value: newFormat, + columnId, + newColumn: { + ...selectedColumn, + label: e.target.value, + }, }) ); }} /> - ) : null} - - - - - )} - + + )} + + {!hideGrouping && ( + { + setState({ + ...state, + layers: { + ...state.layers, + [props.layerId]: { + ...state.layers[props.layerId], + columnOrder, + }, + }, + }); + }} + /> + )} + + {selectedColumn && selectedColumn.dataType === 'number' ? ( + { + setState( + updateColumnParam({ + state, + layerId, + currentColumn: selectedColumn, + paramName: 'format', + value: newFormat, + }) + ); + }} + /> + ) : null} + + + + +
); } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 25121eec30f2a..76e59a170a9e9 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -408,7 +408,6 @@ describe('IndexPattern Data Source', () => { const initialState = stateFromPersistedState(persistedState); publicAPI = indexPatternDatasource.getPublicAPI({ state: initialState, - setState: () => {}, layerId: 'first', dateRange: { fromDate: 'now-30d', diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 00f52d6a1747f..9c2a9c9bf4a09 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -12,7 +12,8 @@ import { CoreStart } from 'src/core/public'; import { i18n } from '@kbn/i18n'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { - DatasourceDimensionPanelProps, + DatasourceDimensionEditorProps, + DatasourceDimensionTriggerProps, DatasourceDataPanelProps, Operation, DatasourceLayerPanelProps, @@ -20,7 +21,12 @@ import { } from '../types'; import { loadInitialState, changeIndexPattern, changeLayerIndexPattern } from './loader'; import { toExpression } from './to_expression'; -import { IndexPatternDimensionPanel } from './dimension_panel'; +import { + IndexPatternDimensionTrigger, + IndexPatternDimensionEditor, + canHandleDrop, + onDrop, +} from './dimension_panel'; import { IndexPatternDataPanel } from './datapanel'; import { getDatasourceSuggestionsForField, @@ -38,6 +44,7 @@ import { } from './types'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { Plugin as DataPlugin } from '../../../../../../src/plugins/data/public'; +import { deleteColumn } from './state_helpers'; import { Datasource, StateSetter } from '..'; export { OperationType, IndexPatternColumn } from './operations'; @@ -80,6 +87,9 @@ export function uniqueLabels(layers: Record) { }; Object.values(layers).forEach(layer => { + if (!layer.columns) { + return; + } Object.entries(layer.columns).forEach(([columnId, column]) => { columnLabelMap[columnId] = makeUnique(column.label); }); @@ -156,6 +166,14 @@ export function getIndexPatternDatasource({ return Object.keys(state.layers); }, + removeColumn({ prevState, layerId, columnId }) { + return deleteColumn({ + state: prevState, + layerId, + columnId, + }); + }, + toExpression, getMetaData(state: IndexPatternPrivateState) { @@ -198,15 +216,97 @@ export function getIndexPatternDatasource({ ); }, - getPublicAPI({ - state, - setState, - layerId, - dateRange, - }: PublicAPIProps) { + renderDimensionTrigger: ( + domElement: Element, + props: DatasourceDimensionTriggerProps + ) => { + const columnLabelMap = uniqueLabels(props.state.layers); + + render( + + + + + , + domElement + ); + }, + + renderDimensionEditor: ( + domElement: Element, + props: DatasourceDimensionEditorProps + ) => { + const columnLabelMap = uniqueLabels(props.state.layers); + + render( + + + + + , + domElement + ); + }, + + renderLayerPanel: ( + domElement: Element, + props: DatasourceLayerPanelProps + ) => { + render( + { + changeLayerIndexPattern({ + savedObjectsClient, + indexPatternId, + setState: props.setState, + state: props.state, + layerId: props.layerId, + onError: onIndexPatternLoadError, + replaceIfPossible: true, + }); + }} + {...props} + />, + domElement + ); + }, + + canHandleDrop, + onDrop, + + getPublicAPI({ state, layerId }: PublicAPIProps) { const columnLabelMap = uniqueLabels(state.layers); return { + datasourceId: 'indexpattern', + getTableSpec: () => { return state.layers[layerId].columnOrder.map(colId => ({ columnId: colId })); }, @@ -218,58 +318,6 @@ export function getIndexPatternDatasource({ } return null; }, - renderDimensionPanel: (domElement: Element, props: DatasourceDimensionPanelProps) => { - render( - - - - - , - domElement - ); - }, - - renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => { - render( - { - changeLayerIndexPattern({ - savedObjectsClient, - indexPatternId, - setState, - state, - layerId: props.layerId, - onError: onIndexPatternLoadError, - replaceIfPossible: true, - }); - }} - {...props} - />, - domElement - ); - }, }; }, getDatasourceSuggestionsForField(state, draggedField) { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx index af7afb9cf9342..219a6d935e436 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx @@ -178,6 +178,7 @@ describe('Layer Data Panel', () => { defaultProps = { layerId: 'first', state: initialState, + setState: jest.fn(), onChangeIndexPattern: jest.fn(async () => {}), }; }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx index ae346ecc72cbc..eea00d52a77f9 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx @@ -11,7 +11,8 @@ import { DatasourceLayerPanelProps } from '../types'; import { IndexPatternPrivateState } from './types'; import { ChangeIndexPattern } from './change_indexpattern'; -export interface IndexPatternLayerPanelProps extends DatasourceLayerPanelProps { +export interface IndexPatternLayerPanelProps + extends DatasourceLayerPanelProps { state: IndexPatternPrivateState; onChangeIndexPattern: (newId: string) => void; } diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.test.tsx deleted file mode 100644 index eac35f82a50fa..0000000000000 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.test.tsx +++ /dev/null @@ -1,69 +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 { ReactWrapper } from 'enzyme'; -import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; -import { MetricConfigPanel } from './metric_config_panel'; -import { DatasourceDimensionPanelProps, Operation, DatasourcePublicAPI } from '../types'; -import { State } from './types'; -import { NativeRendererProps } from '../native_renderer'; -import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_service/mocks'; - -describe('MetricConfigPanel', () => { - const dragDropContext = { dragging: undefined, setDragging: jest.fn() }; - - function mockDatasource(): DatasourcePublicAPI { - return createMockDatasource().publicAPIMock; - } - - function testState(): State { - return { - accessor: 'foo', - layerId: 'bar', - }; - } - - function testSubj(component: ReactWrapper, subj: string) { - return component - .find(`[data-test-subj="${subj}"]`) - .first() - .props(); - } - - test('the value dimension panel only accepts singular numeric operations', () => { - const state = testState(); - const component = mount( - - ); - - const panel = testSubj(component, 'lns_metric_valueDimensionPanel'); - const nativeProps = (panel as NativeRendererProps).nativeProps; - const { columnId, filterOperations } = nativeProps; - const exampleOperation: Operation = { - dataType: 'number', - isBucketed: false, - label: 'bar', - }; - const ops: Operation[] = [ - { ...exampleOperation, dataType: 'number' }, - { ...exampleOperation, dataType: 'string' }, - { ...exampleOperation, dataType: 'boolean' }, - { ...exampleOperation, dataType: 'date' }, - ]; - expect(columnId).toEqual('shazm'); - expect(ops.filter(filterOperations)).toEqual([{ ...exampleOperation, dataType: 'number' }]); - }); -}); diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.tsx deleted file mode 100644 index 16e24f247fb68..0000000000000 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiFormRow } from '@elastic/eui'; -import { State } from './types'; -import { VisualizationLayerConfigProps, OperationMetadata } from '../types'; -import { NativeRenderer } from '../native_renderer'; - -const isMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; - -export function MetricConfigPanel(props: VisualizationLayerConfigProps) { - const { state, frame, layerId } = props; - const datasource = frame.datasourceLayers[layerId]; - - return ( - - - - ); -} diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx index 66ed963002f59..4d979a766cd2b 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx @@ -91,6 +91,11 @@ export function MetricChart({ const { title, accessor, mode } = args; let value = '-'; const firstTable = Object.values(data.tables)[0]; + if (!accessor) { + return ( + + ); + } if (firstTable) { const column = firstTable.columns[0]; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts index 88964b95c2ac7..276f24433c670 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts @@ -24,8 +24,8 @@ function mockFrame(): FramePublicAPI { ...createMockFramePublicAPI(), addNewLayer: () => 'l42', datasourceLayers: { - l1: createMockDatasource().publicAPIMock, - l42: createMockDatasource().publicAPIMock, + l1: createMockDatasource('l1').publicAPIMock, + l42: createMockDatasource('l42').publicAPIMock, }, }; } @@ -36,10 +36,10 @@ describe('metric_visualization', () => { (generateId as jest.Mock).mockReturnValueOnce('test-id1'); const initialState = metricVisualization.initialize(mockFrame()); - expect(initialState.accessor).toBeDefined(); + expect(initialState.accessor).not.toBeDefined(); expect(initialState).toMatchInlineSnapshot(` Object { - "accessor": "test-id1", + "accessor": undefined, "layerId": "l42", } `); @@ -60,7 +60,7 @@ describe('metric_visualization', () => { it('returns a clean layer', () => { (generateId as jest.Mock).mockReturnValueOnce('test-id1'); expect(metricVisualization.clearLayer(exampleState(), 'l1')).toEqual({ - accessor: 'test-id1', + accessor: undefined, layerId: 'l1', }); }); @@ -72,10 +72,47 @@ describe('metric_visualization', () => { }); }); + describe('#setDimension', () => { + it('sets the accessor', () => { + expect( + metricVisualization.setDimension({ + prevState: { + accessor: undefined, + layerId: 'l1', + }, + layerId: 'l1', + groupId: '', + columnId: 'newDimension', + }) + ).toEqual({ + accessor: 'newDimension', + layerId: 'l1', + }); + }); + }); + + describe('#removeDimension', () => { + it('removes the accessor', () => { + expect( + metricVisualization.removeDimension({ + prevState: { + accessor: 'a', + layerId: 'l1', + }, + layerId: 'l1', + columnId: 'a', + }) + ).toEqual({ + accessor: undefined, + layerId: 'l1', + }); + }); + }); + describe('#toExpression', () => { it('should map to a valid AST', () => { const datasource: DatasourcePublicAPI = { - ...createMockDatasource().publicAPIMock, + ...createMockDatasource('l1').publicAPIMock, getOperationForColumnId(_: string) { return { id: 'a', diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx index 6714c05787837..44256df5aed6d 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx @@ -4,23 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { render } from 'react-dom'; -import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { Ast } from '@kbn/interpreter/target/common'; import { getSuggestions } from './metric_suggestions'; -import { MetricConfigPanel } from './metric_config_panel'; -import { Visualization, FramePublicAPI } from '../types'; +import { Visualization, FramePublicAPI, OperationMetadata } from '../types'; import { State, PersistableState } from './types'; -import { generateId } from '../id_generator'; import chartMetricSVG from '../assets/chart_metric.svg'; const toExpression = ( state: State, frame: FramePublicAPI, mode: 'reduced' | 'full' = 'full' -): Ast => { +): Ast | null => { + if (!state.accessor) { + return null; + } + const [datasource] = Object.values(frame.datasourceLayers); const operation = datasource && datasource.getOperationForColumnId(state.accessor); @@ -57,7 +56,7 @@ export const metricVisualization: Visualization = { clearLayer(state) { return { ...state, - accessor: generateId(), + accessor: undefined, }; }, @@ -80,22 +79,37 @@ export const metricVisualization: Visualization = { return ( state || { layerId: frame.addNewLayer(), - accessor: generateId(), + accessor: undefined, } ); }, getPersistableState: state => state, - renderLayerConfigPanel: (domElement, props) => - render( - - - , - domElement - ), + getConfiguration(props) { + return { + groups: [ + { + groupId: 'metric', + groupLabel: i18n.translate('xpack.lens.metric.label', { defaultMessage: 'Metric' }), + layerId: props.state.layerId, + accessors: props.state.accessor ? [props.state.accessor] : [], + supportsMoreColumns: false, + filterOperations: (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number', + }, + ], + }; + }, toExpression, toPreviewExpression: (state: State, frame: FramePublicAPI) => toExpression(state, frame, 'reduced'), + + setDimension({ prevState, columnId }) { + return { ...prevState, accessor: columnId }; + }, + + removeDimension({ prevState }) { + return { ...prevState, accessor: undefined }; + }, }; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/types.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/types.ts index 6348d80b15e2f..53fc103934255 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization/types.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/types.ts @@ -6,7 +6,7 @@ export interface State { layerId: string; - accessor: string; + accessor?: string; } export interface MetricConfig extends State { diff --git a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx b/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx deleted file mode 100644 index 38f48c9cdaf72..0000000000000 --- a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx +++ /dev/null @@ -1,71 +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 { createMockDatasource } from '../editor_frame_service/mocks'; -import { MultiColumnEditor } from './multi_column_editor'; -import { mount } from 'enzyme'; - -jest.useFakeTimers(); - -describe('MultiColumnEditor', () => { - it('should add a trailing accessor if the accessor list is empty', () => { - const onAdd = jest.fn(); - mount( - true} - layerId="foo" - onAdd={onAdd} - onRemove={jest.fn()} - testSubj="bar" - /> - ); - - expect(onAdd).toHaveBeenCalledTimes(0); - - jest.runAllTimers(); - - expect(onAdd).toHaveBeenCalledTimes(1); - }); - - it('should add a trailing accessor if the last accessor is configured', () => { - const onAdd = jest.fn(); - mount( - true} - layerId="foo" - onAdd={onAdd} - onRemove={jest.fn()} - testSubj="bar" - /> - ); - - expect(onAdd).toHaveBeenCalledTimes(0); - - jest.runAllTimers(); - - expect(onAdd).toHaveBeenCalledTimes(1); - }); -}); diff --git a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.tsx b/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.tsx deleted file mode 100644 index 422f1dcf60f3c..0000000000000 --- a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.tsx +++ /dev/null @@ -1,63 +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, { useEffect } from 'react'; -import { NativeRenderer } from '../native_renderer'; -import { DatasourcePublicAPI, OperationMetadata } from '../types'; -import { DragContextState } from '../drag_drop'; - -interface Props { - accessors: string[]; - datasource: DatasourcePublicAPI; - dragDropContext: DragContextState; - onRemove: (accessor: string) => void; - onAdd: () => void; - filterOperations: (op: OperationMetadata) => boolean; - suggestedPriority?: 0 | 1 | 2 | undefined; - testSubj: string; - layerId: string; -} - -export function MultiColumnEditor({ - accessors, - datasource, - dragDropContext, - onRemove, - onAdd, - filterOperations, - suggestedPriority, - testSubj, - layerId, -}: Props) { - const lastOperation = datasource.getOperationForColumnId(accessors[accessors.length - 1]); - - useEffect(() => { - if (accessors.length === 0 || lastOperation !== null) { - setTimeout(onAdd); - } - }, [lastOperation]); - - return ( - <> - {accessors.map(accessor => ( -
- -
- ))} - - ); -} diff --git a/x-pack/legacy/plugins/lens/public/plugin.tsx b/x-pack/legacy/plugins/lens/public/plugin.tsx index 7afe6d7abedc0..c74653c70703c 100644 --- a/x-pack/legacy/plugins/lens/public/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/plugin.tsx @@ -36,7 +36,7 @@ import { getLensUrlFromDashboardAbsoluteUrl, } from '../../../../../src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper'; import { FormatFactory } from './legacy_imports'; -import { IEmbeddableSetup, IEmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { EmbeddableSetup, EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; import { EditorFrameStart } from './types'; import { getLensAliasConfig } from './vis_type_alias'; import { VisualizationsSetup } from './legacy_imports'; @@ -45,7 +45,7 @@ export interface LensPluginSetupDependencies { kibanaLegacy: KibanaLegacySetup; expressions: ExpressionsSetup; data: DataPublicPluginSetup; - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; __LEGACY: { formatFactory: FormatFactory; visualizations: VisualizationsSetup; @@ -54,7 +54,7 @@ export interface LensPluginSetupDependencies { export interface LensPluginStartDependencies { data: DataPublicPluginStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; expressions: ExpressionsStart; } @@ -114,7 +114,7 @@ export class LensPlugin { const savedObjectsClient = coreStart.savedObjects.client; addHelpMenuToAppChrome(coreStart.chrome); - const instance = await this.createEditorFrame!({}); + const instance = await this.createEditorFrame!(); setReportManager( new LensReportManager({ diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index b7983eeb8dbb8..c897979b06cfb 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -13,14 +13,10 @@ import { Document } from './persistence'; import { DateRange } from '../../../../plugins/lens/common'; import { Query, Filter, SavedQuery } from '../../../../../src/plugins/data/public'; -// eslint-disable-next-line -export interface EditorFrameOptions {} - export type ErrorCallback = (e: { message: string }) => void; export interface PublicAPIProps { state: T; - setState: StateSetter; layerId: string; dateRange: DateRange; } @@ -34,6 +30,7 @@ export interface EditorFrameProps { savedQuery?: SavedQuery; // Frame loader (app or embeddable) is expected to call this when it loads and updates + // This should be replaced with a top-down state onChange: (newState: { filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns']; doc: Document; @@ -53,7 +50,7 @@ export interface EditorFrameSetup { } export interface EditorFrameStart { - createInstance: (options: EditorFrameOptions) => Promise; + createInstance: () => Promise; } // Hints the default nesting to the data source. 0 is the highest priority @@ -138,8 +135,14 @@ export interface Datasource { removeLayer: (state: T, layerId: string) => T; clearLayer: (state: T, layerId: string) => T; getLayers: (state: T) => string[]; + removeColumn: (props: { prevState: T; layerId: string; columnId: string }) => T; renderDataPanel: (domElement: Element, props: DatasourceDataPanelProps) => void; + renderDimensionTrigger: (domElement: Element, props: DatasourceDimensionTriggerProps) => void; + renderDimensionEditor: (domElement: Element, props: DatasourceDimensionEditorProps) => void; + renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => void; + canHandleDrop: (props: DatasourceDimensionDropProps) => boolean; + onDrop: (props: DatasourceDimensionDropHandlerProps) => boolean; toExpression: (state: T, layerId: string) => Ast | string | null; @@ -155,22 +158,11 @@ export interface Datasource { * This is an API provided to visualizations by the frame, which calls the publicAPI on the datasource */ export interface DatasourcePublicAPI { - getTableSpec: () => TableSpec; + datasourceId: string; + getTableSpec: () => Array<{ columnId: string }>; getOperationForColumnId: (columnId: string) => Operation | null; - - // Render can be called many times - renderDimensionPanel: (domElement: Element, props: DatasourceDimensionPanelProps) => void; - renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => void; } -export interface TableSpecColumn { - // Column IDs are the keys for internal state in data sources and visualizations - columnId: string; -} - -// TableSpec is managed by visualizations -export type TableSpec = TableSpecColumn[]; - export interface DatasourceDataPanelProps { state: T; dragDropContext: DragContextState; @@ -181,31 +173,61 @@ export interface DatasourceDataPanelProps { filters: Filter[]; } -// The only way a visualization has to restrict the query building -export interface DatasourceDimensionPanelProps { - layerId: string; - columnId: string; - - dragDropContext: DragContextState; - - // Visualizations can restrict operations based on their own rules +interface SharedDimensionProps { + /** Visualizations can restrict operations based on their own rules. + * For example, limiting to only bucketed or only numeric operations. + */ filterOperations: (operation: OperationMetadata) => boolean; - // Visualizations can hint at the role this dimension would play, which - // affects the default ordering of the query + /** Visualizations can hint at the role this dimension would play, which + * affects the default ordering of the query + */ suggestedPriority?: DimensionPriority; - onRemove?: (accessor: string) => void; - // Some dimension editors will allow users to change the operation grouping - // from the panel, and this lets the visualization hint that it doesn't want - // users to have that level of control + /** Some dimension editors will allow users to change the operation grouping + * from the panel, and this lets the visualization hint that it doesn't want + * users to have that level of control + */ hideGrouping?: boolean; } -export interface DatasourceLayerPanelProps { +export type DatasourceDimensionProps = SharedDimensionProps & { layerId: string; + columnId: string; + onRemove?: (accessor: string) => void; + state: T; +}; + +// The only way a visualization has to restrict the query building +export type DatasourceDimensionEditorProps = DatasourceDimensionProps & { + setState: StateSetter; + core: Pick; + dateRange: DateRange; +}; + +export type DatasourceDimensionTriggerProps = DatasourceDimensionProps & { + dragDropContext: DragContextState; + togglePopover: () => void; +}; + +export interface DatasourceLayerPanelProps { + layerId: string; + state: T; + setState: StateSetter; } +export type DatasourceDimensionDropProps = SharedDimensionProps & { + layerId: string; + columnId: string; + state: T; + setState: StateSetter; + dragDropContext: DragContextState; +}; + +export type DatasourceDimensionDropHandlerProps = DatasourceDimensionDropProps & { + droppedItem: unknown; +}; + export type DataType = 'document' | 'string' | 'number' | 'date' | 'boolean' | 'ip'; // An operation represents a column in a table, not any information @@ -239,12 +261,32 @@ export interface LensMultiTable { }; } -export interface VisualizationLayerConfigProps { +export interface VisualizationConfigProps { layerId: string; - dragDropContext: DragContextState; frame: FramePublicAPI; state: T; +} + +export type VisualizationLayerWidgetProps = VisualizationConfigProps & { setState: (newState: T) => void; +}; + +type VisualizationDimensionGroupConfig = SharedDimensionProps & { + groupLabel: string; + + /** ID is passed back to visualization. For example, `x` */ + groupId: string; + accessors: string[]; + supportsMoreColumns: boolean; + /** If required, a warning will appear if accessors are empty */ + required?: boolean; + dataTestSubj?: string; +}; + +interface VisualizationDimensionChangeProps { + layerId: string; + columnId: string; + prevState: T; } /** @@ -329,16 +371,18 @@ export interface Visualization { visualizationTypes: VisualizationType[]; getLayerIds: (state: T) => string[]; - clearLayer: (state: T, layerId: string) => T; - removeLayer?: (state: T, layerId: string) => T; - appendLayer?: (state: T, layerId: string) => T; + // Layer context menu is used by visualizations for styling the entire layer + // For example, the XY visualization uses this to have multiple chart types getLayerContextMenuIcon?: (opts: { state: T; layerId: string }) => IconType | undefined; + renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerWidgetProps) => void; - renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerConfigProps) => void; + getConfiguration: ( + props: VisualizationConfigProps + ) => { groups: VisualizationDimensionGroupConfig[] }; getDescription: ( state: T @@ -354,7 +398,13 @@ export interface Visualization { getPersistableState: (state: T) => P; - renderLayerConfigPanel: (domElement: Element, props: VisualizationLayerConfigProps) => void; + // Actions triggered by the frame which tell the datasource that a dimension is being changed + setDimension: ( + props: VisualizationDimensionChangeProps & { + groupId: string; + } + ) => T; + removeDimension: (props: VisualizationDimensionChangeProps) => T; toExpression: (state: T, frame: FramePublicAPI) => Ast | string | null; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_visualization.test.ts.snap b/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap similarity index 96% rename from x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_visualization.test.ts.snap rename to x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap index 76af8328673ad..6b68679bfd4ec 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_visualization.test.ts.snap +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`xy_visualization #toExpression should map to a valid AST 1`] = ` +exports[`#toExpression should map to a valid AST 1`] = ` Object { "chain": Array [ Object { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.test.ts new file mode 100644 index 0000000000000..6bc379ea33bca --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.test.ts @@ -0,0 +1,133 @@ +/* + * 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 { Ast } from '@kbn/interpreter/target/common'; +import { Position } from '@elastic/charts'; +import { xyVisualization } from './xy_visualization'; +import { Operation } from '../types'; +import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; + +describe('#toExpression', () => { + let mockDatasource: ReturnType; + let frame: ReturnType; + + beforeEach(() => { + frame = createMockFramePublicAPI(); + mockDatasource = createMockDatasource('testDatasource'); + + mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([ + { columnId: 'd' }, + { columnId: 'a' }, + { columnId: 'b' }, + { columnId: 'c' }, + ]); + + mockDatasource.publicAPIMock.getOperationForColumnId.mockImplementation(col => { + return { label: `col_${col}`, dataType: 'number' } as Operation; + }); + + frame.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + }); + + it('should map to a valid AST', () => { + expect( + xyVisualization.toExpression( + { + legend: { position: Position.Bottom, isVisible: true }, + preferredSeriesType: 'bar', + layers: [ + { + layerId: 'first', + seriesType: 'area', + splitAccessor: 'd', + xAccessor: 'a', + accessors: ['b', 'c'], + }, + ], + }, + frame + ) + ).toMatchSnapshot(); + }); + + it('should not generate an expression when missing x', () => { + expect( + xyVisualization.toExpression( + { + legend: { position: Position.Bottom, isVisible: true }, + preferredSeriesType: 'bar', + layers: [ + { + layerId: 'first', + seriesType: 'area', + splitAccessor: undefined, + xAccessor: undefined, + accessors: ['a'], + }, + ], + }, + frame + ) + ).toBeNull(); + }); + + it('should not generate an expression when missing y', () => { + expect( + xyVisualization.toExpression( + { + legend: { position: Position.Bottom, isVisible: true }, + preferredSeriesType: 'bar', + layers: [ + { + layerId: 'first', + seriesType: 'area', + splitAccessor: undefined, + xAccessor: 'a', + accessors: [], + }, + ], + }, + frame + ) + ).toBeNull(); + }); + + it('should default to labeling all columns with their column label', () => { + const expression = xyVisualization.toExpression( + { + legend: { position: Position.Bottom, isVisible: true }, + preferredSeriesType: 'bar', + layers: [ + { + layerId: 'first', + seriesType: 'area', + splitAccessor: 'd', + xAccessor: 'a', + accessors: ['b', 'c'], + }, + ], + }, + frame + )! as Ast; + + expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('b'); + expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('c'); + expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('d'); + expect(expression.chain[0].arguments.xTitle).toEqual(['col_a']); + expect(expression.chain[0].arguments.yTitle).toEqual(['col_b']); + expect( + (expression.chain[0].arguments.layers[0] as Ast).chain[0].arguments.columnToLabel + ).toEqual([ + JSON.stringify({ + b: 'col_b', + c: 'col_c', + d: 'col_d', + }), + ]); + }); +}); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts index f0e932d14f281..9b068b0ca5ef0 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts @@ -9,6 +9,10 @@ import { ScaleType } from '@elastic/charts'; import { State, LayerConfig } from './types'; import { FramePublicAPI, OperationMetadata } from '../types'; +interface ValidLayer extends LayerConfig { + xAccessor: NonNullable; +} + function xyTitles(layer: LayerConfig, frame: FramePublicAPI) { const defaults = { xTitle: 'x', @@ -22,8 +26,8 @@ function xyTitles(layer: LayerConfig, frame: FramePublicAPI) { if (!datasource) { return defaults; } - const x = datasource.getOperationForColumnId(layer.xAccessor); - const y = datasource.getOperationForColumnId(layer.accessors[0]); + const x = layer.xAccessor ? datasource.getOperationForColumnId(layer.xAccessor) : null; + const y = layer.accessors[0] ? datasource.getOperationForColumnId(layer.accessors[0]) : null; return { xTitle: x ? x.label : defaults.xTitle, @@ -36,26 +40,6 @@ export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => return null; } - const stateWithValidAccessors = { - ...state, - layers: state.layers.map(layer => { - const datasource = frame.datasourceLayers[layer.layerId]; - - const newLayer = { ...layer }; - - if (!datasource.getOperationForColumnId(layer.splitAccessor)) { - delete newLayer.splitAccessor; - } - - return { - ...newLayer, - accessors: layer.accessors.filter(accessor => - Boolean(datasource.getOperationForColumnId(accessor)) - ), - }; - }), - }; - const metadata: Record> = {}; state.layers.forEach(layer => { metadata[layer.layerId] = {}; @@ -68,12 +52,7 @@ export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => }); }); - return buildExpression( - stateWithValidAccessors, - metadata, - frame, - xyTitles(state.layers[0], frame) - ); + return buildExpression(state, metadata, frame, xyTitles(state.layers[0], frame)); }; export function toPreviewExpression(state: State, frame: FramePublicAPI) { @@ -122,82 +101,94 @@ export const buildExpression = ( metadata: Record>, frame?: FramePublicAPI, { xTitle, yTitle }: { xTitle: string; yTitle: string } = { xTitle: '', yTitle: '' } -): Ast => ({ - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_xy_chart', - arguments: { - xTitle: [xTitle], - yTitle: [yTitle], - legend: [ - { - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_xy_legendConfig', - arguments: { - isVisible: [state.legend.isVisible], - position: [state.legend.position], +): Ast | null => { + const validLayers = state.layers.filter((layer): layer is ValidLayer => + Boolean(layer.xAccessor && layer.accessors.length) + ); + if (!validLayers.length) { + return null; + } + + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_xy_chart', + arguments: { + xTitle: [xTitle], + yTitle: [yTitle], + legend: [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_xy_legendConfig', + arguments: { + isVisible: [state.legend.isVisible], + position: [state.legend.position], + }, }, - }, - ], - }, - ], - layers: state.layers.map(layer => { - const columnToLabel: Record = {}; - - if (frame) { - const datasource = frame.datasourceLayers[layer.layerId]; - layer.accessors.concat([layer.splitAccessor]).forEach(accessor => { - const operation = datasource.getOperationForColumnId(accessor); - if (operation && operation.label) { - columnToLabel[accessor] = operation.label; - } - }); - } - - const xAxisOperation = - frame && frame.datasourceLayers[layer.layerId].getOperationForColumnId(layer.xAccessor); - - const isHistogramDimension = Boolean( - xAxisOperation && - xAxisOperation.isBucketed && - xAxisOperation.scale && - xAxisOperation.scale !== 'ordinal' - ); - - return { - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_xy_layer', - arguments: { - layerId: [layer.layerId], - - hide: [Boolean(layer.hide)], - - xAccessor: [layer.xAccessor], - yScaleType: [ - getScaleType(metadata[layer.layerId][layer.accessors[0]], ScaleType.Ordinal), - ], - xScaleType: [ - getScaleType(metadata[layer.layerId][layer.xAccessor], ScaleType.Linear), - ], - isHistogram: [isHistogramDimension], - splitAccessor: [layer.splitAccessor], - seriesType: [layer.seriesType], - accessors: layer.accessors, - columnToLabel: [JSON.stringify(columnToLabel)], + ], + }, + ], + layers: validLayers.map(layer => { + const columnToLabel: Record = {}; + + if (frame) { + const datasource = frame.datasourceLayers[layer.layerId]; + layer.accessors + .concat(layer.splitAccessor ? [layer.splitAccessor] : []) + .forEach(accessor => { + const operation = datasource.getOperationForColumnId(accessor); + if (operation && operation.label) { + columnToLabel[accessor] = operation.label; + } + }); + } + + const xAxisOperation = + frame && + frame.datasourceLayers[layer.layerId].getOperationForColumnId(layer.xAccessor); + + const isHistogramDimension = Boolean( + xAxisOperation && + xAxisOperation.isBucketed && + xAxisOperation.scale && + xAxisOperation.scale !== 'ordinal' + ); + + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_xy_layer', + arguments: { + layerId: [layer.layerId], + + hide: [Boolean(layer.hide)], + + xAccessor: [layer.xAccessor], + yScaleType: [ + getScaleType(metadata[layer.layerId][layer.accessors[0]], ScaleType.Ordinal), + ], + xScaleType: [ + getScaleType(metadata[layer.layerId][layer.xAccessor], ScaleType.Linear), + ], + isHistogram: [isHistogramDimension], + splitAccessor: layer.splitAccessor ? [layer.splitAccessor] : [], + seriesType: [layer.seriesType], + accessors: layer.accessors, + columnToLabel: [JSON.stringify(columnToLabel)], + }, }, - }, - ], - }; - }), + ], + }; + }), + }, }, - }, - ], -}); + ], + }; +}; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/types.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/types.ts index b49e6fa6b4b6f..f7b4afc76ec4b 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/types.ts @@ -191,10 +191,10 @@ export type SeriesType = export interface LayerConfig { hide?: boolean; layerId: string; - xAccessor: string; + xAccessor?: string; accessors: string[]; seriesType: SeriesType; - splitAccessor: string; + splitAccessor?: string; } export type LayerArgs = LayerConfig & { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx index 301c4a58a0ffd..7544ed0f87b7d 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx @@ -5,22 +5,15 @@ */ import React from 'react'; -import { ReactWrapper } from 'enzyme'; import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; import { EuiButtonGroupProps } from '@elastic/eui'; -import { XYConfigPanel, LayerContextMenu } from './xy_config_panel'; -import { DatasourceDimensionPanelProps, Operation, FramePublicAPI } from '../types'; +import { LayerContextMenu } from './xy_config_panel'; +import { FramePublicAPI } from '../types'; import { State } from './types'; import { Position } from '@elastic/charts'; -import { NativeRendererProps } from '../native_renderer'; -import { generateId } from '../id_generator'; import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_service/mocks'; -jest.mock('../id_generator'); - -describe('XYConfigPanel', () => { - const dragDropContext = { dragging: undefined, setDragging: jest.fn() }; - +describe('LayerContextMenu', () => { let frame: FramePublicAPI; function testState(): State { @@ -39,17 +32,10 @@ describe('XYConfigPanel', () => { }; } - function testSubj(component: ReactWrapper, subj: string) { - return component - .find(`[data-test-subj="${subj}"]`) - .first() - .props(); - } - beforeEach(() => { frame = createMockFramePublicAPI(); frame.datasourceLayers = { - first: createMockDatasource().publicAPIMock, + first: createMockDatasource('test').publicAPIMock, }; }); @@ -64,7 +50,6 @@ describe('XYConfigPanel', () => { const component = mount( { const component = mount( { expect(options!.filter(({ isDisabled }) => isDisabled).map(({ id }) => id)).toEqual([]); }); }); - - test('the x dimension panel accepts only bucketed operations', () => { - // TODO: this should eventually also accept raw operation - const state = testState(); - const component = mount( - - ); - - const panel = testSubj(component, 'lnsXY_xDimensionPanel'); - const nativeProps = (panel as NativeRendererProps).nativeProps; - const { columnId, filterOperations } = nativeProps; - const exampleOperation: Operation = { - dataType: 'number', - isBucketed: false, - label: 'bar', - }; - const bucketedOps: Operation[] = [ - { ...exampleOperation, isBucketed: true, dataType: 'number' }, - { ...exampleOperation, isBucketed: true, dataType: 'string' }, - { ...exampleOperation, isBucketed: true, dataType: 'boolean' }, - { ...exampleOperation, isBucketed: true, dataType: 'date' }, - ]; - const ops: Operation[] = [ - ...bucketedOps, - { ...exampleOperation, dataType: 'number' }, - { ...exampleOperation, dataType: 'string' }, - { ...exampleOperation, dataType: 'boolean' }, - { ...exampleOperation, dataType: 'date' }, - ]; - expect(columnId).toEqual('shazm'); - expect(ops.filter(filterOperations)).toEqual(bucketedOps); - }); - - test('the y dimension panel accepts numeric operations', () => { - const state = testState(); - const component = mount( - - ); - - const filterOperations = component - .find('[data-test-subj="lensXY_yDimensionPanel"]') - .first() - .prop('filterOperations') as (op: Operation) => boolean; - - const exampleOperation: Operation = { - dataType: 'number', - isBucketed: false, - label: 'bar', - }; - const ops: Operation[] = [ - { ...exampleOperation, dataType: 'number' }, - { ...exampleOperation, dataType: 'string' }, - { ...exampleOperation, dataType: 'boolean' }, - { ...exampleOperation, dataType: 'date' }, - ]; - expect(ops.filter(filterOperations).map(x => x.dataType)).toEqual(['number']); - }); - - test('allows removal of y dimensions', () => { - const setState = jest.fn(); - const state = testState(); - const component = mount( - - ); - - const onRemove = component - .find('[data-test-subj="lensXY_yDimensionPanel"]') - .first() - .prop('onRemove') as (accessor: string) => {}; - - onRemove('b'); - - expect(setState).toHaveBeenCalledTimes(1); - expect(setState.mock.calls[0][0]).toMatchObject({ - layers: [ - { - ...state.layers[0], - accessors: ['a', 'c'], - }, - ], - }); - }); - - test('allows adding a y axis dimension', () => { - (generateId as jest.Mock).mockReturnValueOnce('zed'); - const setState = jest.fn(); - const state = testState(); - const component = mount( - - ); - - const onAdd = component - .find('[data-test-subj="lensXY_yDimensionPanel"]') - .first() - .prop('onAdd') as () => {}; - - onAdd(); - - expect(setState).toHaveBeenCalledTimes(1); - expect(setState.mock.calls[0][0]).toMatchObject({ - layers: [ - { - ...state.layers[0], - accessors: ['a', 'b', 'c', 'zed'], - }, - ], - }); - }); }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx index dbcfa24395001..5e85680cc2b2c 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -9,16 +9,10 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonGroup, EuiFormRow } from '@elastic/eui'; import { State, SeriesType, visualizationTypes } from './types'; -import { VisualizationLayerConfigProps, OperationMetadata } from '../types'; -import { NativeRenderer } from '../native_renderer'; -import { MultiColumnEditor } from '../multi_column_editor'; -import { generateId } from '../id_generator'; +import { VisualizationLayerWidgetProps } from '../types'; import { isHorizontalChart, isHorizontalSeries } from './state_helpers'; import { trackUiEvent } from '../lens_ui_telemetry'; -const isNumericMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; -const isBucketed = (op: OperationMetadata) => op.isBucketed; - type UnwrapArray = T extends Array ? P : T; function updateLayer(state: State, layer: UnwrapArray, index: number): State { @@ -31,7 +25,7 @@ function updateLayer(state: State, layer: UnwrapArray, index: n }; } -export function LayerContextMenu(props: VisualizationLayerConfigProps) { +export function LayerContextMenu(props: VisualizationLayerWidgetProps) { const { state, layerId } = props; const horizontalOnly = isHorizontalChart(state.layers); const index = state.layers.findIndex(l => l.layerId === layerId); @@ -74,97 +68,3 @@ export function LayerContextMenu(props: VisualizationLayerConfigProps) { ); } - -export function XYConfigPanel(props: VisualizationLayerConfigProps) { - const { state, setState, frame, layerId } = props; - const index = props.state.layers.findIndex(l => l.layerId === layerId); - - if (index < 0) { - return null; - } - - const layer = props.state.layers[index]; - - return ( - <> - - - - - - setState( - updateLayer( - state, - { - ...layer, - accessors: [...layer.accessors, generateId()], - }, - index - ) - ) - } - onRemove={accessor => - setState( - updateLayer( - state, - { - ...layer, - accessors: layer.accessors.filter(col => col !== accessor), - }, - index - ) - ) - } - filterOperations={isNumericMetric} - data-test-subj="lensXY_yDimensionPanel" - testSubj="lensXY_yDimensionPanel" - layerId={layer.layerId} - /> - - - - - - ); -} diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx index 27fd6e7064042..15aaf289eebf9 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -238,6 +238,8 @@ export function XYChart({ data, args, formatFactory, timeZone, chartTheme }: XYC index ) => { if ( + !xAccessor || + !accessors.length || !data.tables[layerId] || data.tables[layerId].rows.length === 0 || data.tables[layerId].rows.every(row => typeof row[xAccessor] === 'undefined') @@ -246,7 +248,7 @@ export function XYChart({ data, args, formatFactory, timeZone, chartTheme }: XYC } const columnToLabelMap = columnToLabel ? JSON.parse(columnToLabel) : {}; - const splitAccessorLabel = columnToLabelMap[splitAccessor]; + const splitAccessorLabel = splitAccessor ? columnToLabelMap[splitAccessor] : ''; const yAccessors = accessors.map(accessor => columnToLabelMap[accessor] || accessor); const idForLegend = splitAccessorLabel || yAccessors; const sanitized = sanitizeRows({ diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts index 04ff720309d62..ddbd9d11b5fad 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts @@ -123,7 +123,7 @@ describe('xy_suggestions', () => { Array [ Object { "seriesType": "bar_stacked", - "splitAccessor": "aaa", + "splitAccessor": undefined, "x": "date", "y": Array [ "bytes", @@ -240,7 +240,6 @@ describe('xy_suggestions', () => { }); test('only makes a seriesType suggestion for unchanged table without split', () => { - (generateId as jest.Mock).mockReturnValueOnce('dummyCol'); const currentState: XYState = { legend: { isVisible: true, position: 'bottom' }, preferredSeriesType: 'bar', @@ -249,7 +248,7 @@ describe('xy_suggestions', () => { accessors: ['price'], layerId: 'first', seriesType: 'bar', - splitAccessor: 'dummyCol', + splitAccessor: undefined, xAccessor: 'date', }, ], @@ -472,17 +471,17 @@ describe('xy_suggestions', () => { }); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` - Array [ - Object { - "seriesType": "bar_stacked", - "splitAccessor": "ddd", - "x": "quantity", - "y": Array [ - "price", - ], - }, - ] - `); + Array [ + Object { + "seriesType": "bar_stacked", + "splitAccessor": undefined, + "x": "quantity", + "y": Array [ + "price", + ], + }, + ] + `); }); test('handles ip', () => { @@ -509,17 +508,17 @@ describe('xy_suggestions', () => { }); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` - Array [ - Object { - "seriesType": "bar_stacked", - "splitAccessor": "ddd", - "x": "myip", - "y": Array [ - "quantity", - ], - }, - ] - `); + Array [ + Object { + "seriesType": "bar_stacked", + "splitAccessor": undefined, + "x": "myip", + "y": Array [ + "quantity", + ], + }, + ] + `); }); test('handles unbucketed suggestions', () => { @@ -545,16 +544,16 @@ describe('xy_suggestions', () => { }); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` - Array [ - Object { - "seriesType": "bar_stacked", - "splitAccessor": "eee", - "x": "mybool", - "y": Array [ - "num votes", - ], - }, - ] - `); + Array [ + Object { + "seriesType": "bar_stacked", + "splitAccessor": undefined, + "x": "mybool", + "y": Array [ + "num votes", + ], + }, + ] + `); }); }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts index 33181b7f3a467..5e9311bb1e928 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -15,7 +15,6 @@ import { TableChangeType, } from '../types'; import { State, SeriesType, XYState } from './types'; -import { generateId } from '../id_generator'; import { getIconForSeries } from './state_helpers'; const columnSortOrder = { @@ -356,7 +355,7 @@ function buildSuggestion({ layerId, seriesType, xAccessor: xValue.columnId, - splitAccessor: splitBy ? splitBy.columnId : generateId(), + splitAccessor: splitBy?.columnId, accessors: yValues.map(col => col.columnId), }; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts index a27a8e7754b86..beccf0dc46eb4 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts @@ -9,10 +9,6 @@ import { Position } from '@elastic/charts'; import { Operation } from '../types'; import { State, SeriesType } from './types'; import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; -import { generateId } from '../id_generator'; -import { Ast } from '@kbn/interpreter/target/common'; - -jest.mock('../id_generator'); function exampleState(): State { return { @@ -87,31 +83,22 @@ describe('xy_visualization', () => { describe('#initialize', () => { it('loads default state', () => { - (generateId as jest.Mock) - .mockReturnValueOnce('test-id1') - .mockReturnValueOnce('test-id2') - .mockReturnValue('test-id3'); const mockFrame = createMockFramePublicAPI(); const initialState = xyVisualization.initialize(mockFrame); expect(initialState.layers).toHaveLength(1); - expect(initialState.layers[0].xAccessor).toBeDefined(); - expect(initialState.layers[0].accessors[0]).toBeDefined(); - expect(initialState.layers[0].xAccessor).not.toEqual(initialState.layers[0].accessors[0]); + expect(initialState.layers[0].xAccessor).not.toBeDefined(); + expect(initialState.layers[0].accessors).toHaveLength(0); expect(initialState).toMatchInlineSnapshot(` Object { "layers": Array [ Object { - "accessors": Array [ - "test-id1", - ], + "accessors": Array [], "layerId": "", "position": "top", "seriesType": "bar_stacked", "showGridlines": false, - "splitAccessor": "test-id2", - "xAccessor": "test-id3", }, ], "legend": Object { @@ -167,14 +154,11 @@ describe('xy_visualization', () => { describe('#clearLayer', () => { it('clears the specified layer', () => { - (generateId as jest.Mock).mockReturnValue('test_empty_id'); const layer = xyVisualization.clearLayer(exampleState(), 'first').layers[0]; expect(layer).toMatchObject({ - accessors: ['test_empty_id'], + accessors: [], layerId: 'first', seriesType: 'bar', - splitAccessor: 'test_empty_id', - xAccessor: 'test_empty_id', }); }); }); @@ -185,13 +169,94 @@ describe('xy_visualization', () => { }); }); - describe('#toExpression', () => { + describe('#setDimension', () => { + it('sets the x axis', () => { + expect( + xyVisualization.setDimension({ + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: undefined, + accessors: [], + }, + ], + }, + layerId: 'first', + groupId: 'x', + columnId: 'newCol', + }).layers[0] + ).toEqual({ + layerId: 'first', + seriesType: 'area', + xAccessor: 'newCol', + accessors: [], + }); + }); + + it('replaces the x axis', () => { + expect( + xyVisualization.setDimension({ + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: 'a', + accessors: [], + }, + ], + }, + layerId: 'first', + groupId: 'x', + columnId: 'newCol', + }).layers[0] + ).toEqual({ + layerId: 'first', + seriesType: 'area', + xAccessor: 'newCol', + accessors: [], + }); + }); + }); + + describe('#removeDimension', () => { + it('removes the x axis', () => { + expect( + xyVisualization.removeDimension({ + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: 'a', + accessors: [], + }, + ], + }, + layerId: 'first', + columnId: 'a', + }).layers[0] + ).toEqual({ + layerId: 'first', + seriesType: 'area', + xAccessor: undefined, + accessors: [], + }); + }); + }); + + describe('#getConfiguration', () => { let mockDatasource: ReturnType; let frame: ReturnType; beforeEach(() => { frame = createMockFramePublicAPI(); - mockDatasource = createMockDatasource(); + mockDatasource = createMockDatasource('testDatasource'); mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([ { columnId: 'd' }, @@ -200,36 +265,78 @@ describe('xy_visualization', () => { { columnId: 'c' }, ]); - mockDatasource.publicAPIMock.getOperationForColumnId.mockImplementation(col => { - return { label: `col_${col}`, dataType: 'number' } as Operation; - }); - frame.datasourceLayers = { first: mockDatasource.publicAPIMock, }; }); - it('should map to a valid AST', () => { - expect(xyVisualization.toExpression(exampleState(), frame)).toMatchSnapshot(); + it('should return options for 3 dimensions', () => { + const options = xyVisualization.getConfiguration({ + state: exampleState(), + frame, + layerId: 'first', + }).groups; + expect(options).toHaveLength(3); + expect(options.map(o => o.groupId)).toEqual(['x', 'y', 'breakdown']); }); - it('should default to labeling all columns with their column label', () => { - const expression = xyVisualization.toExpression(exampleState(), frame)! as Ast; + it('should only accept bucketed operations for x', () => { + const options = xyVisualization.getConfiguration({ + state: exampleState(), + frame, + layerId: 'first', + }).groups; + const filterOperations = options.find(o => o.groupId === 'x')!.filterOperations; - expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('b'); - expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('c'); - expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('d'); - expect(expression.chain[0].arguments.xTitle).toEqual(['col_a']); - expect(expression.chain[0].arguments.yTitle).toEqual(['col_b']); - expect( - (expression.chain[0].arguments.layers[0] as Ast).chain[0].arguments.columnToLabel - ).toEqual([ - JSON.stringify({ - b: 'col_b', - c: 'col_c', - d: 'col_d', - }), - ]); + const exampleOperation: Operation = { + dataType: 'number', + isBucketed: false, + label: 'bar', + }; + const bucketedOps: Operation[] = [ + { ...exampleOperation, isBucketed: true, dataType: 'number' }, + { ...exampleOperation, isBucketed: true, dataType: 'string' }, + { ...exampleOperation, isBucketed: true, dataType: 'boolean' }, + { ...exampleOperation, isBucketed: true, dataType: 'date' }, + ]; + const ops: Operation[] = [ + ...bucketedOps, + { ...exampleOperation, dataType: 'number' }, + { ...exampleOperation, dataType: 'string' }, + { ...exampleOperation, dataType: 'boolean' }, + { ...exampleOperation, dataType: 'date' }, + ]; + expect(ops.filter(filterOperations)).toEqual(bucketedOps); + }); + + it('should not allow anything to be added to x', () => { + const options = xyVisualization.getConfiguration({ + state: exampleState(), + frame, + layerId: 'first', + }).groups; + expect(options.find(o => o.groupId === 'x')?.supportsMoreColumns).toBe(false); + }); + + it('should allow number operations on y', () => { + const options = xyVisualization.getConfiguration({ + state: exampleState(), + frame, + layerId: 'first', + }).groups; + const filterOperations = options.find(o => o.groupId === 'y')!.filterOperations; + const exampleOperation: Operation = { + dataType: 'number', + isBucketed: false, + label: 'bar', + }; + const ops: Operation[] = [ + { ...exampleOperation, dataType: 'number' }, + { ...exampleOperation, dataType: 'string' }, + { ...exampleOperation, dataType: 'boolean' }, + { ...exampleOperation, dataType: 'date' }, + ]; + expect(ops.filter(filterOperations).map(x => x.dataType)).toEqual(['number']); }); }); }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx index 75d6fcc7d160b..c72fa0fec24d7 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx @@ -11,17 +11,18 @@ import { Position } from '@elastic/charts'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { getSuggestions } from './xy_suggestions'; -import { XYConfigPanel, LayerContextMenu } from './xy_config_panel'; -import { Visualization } from '../types'; +import { LayerContextMenu } from './xy_config_panel'; +import { Visualization, OperationMetadata } from '../types'; import { State, PersistableState, SeriesType, visualizationTypes, LayerConfig } from './types'; import { toExpression, toPreviewExpression } from './to_expression'; -import { generateId } from '../id_generator'; import chartBarStackedSVG from '../assets/chart_bar_stacked.svg'; import chartMixedSVG from '../assets/chart_mixed_xy.svg'; import { isHorizontalChart } from './state_helpers'; const defaultIcon = chartBarStackedSVG; const defaultSeriesType = 'bar_stacked'; +const isNumericMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; +const isBucketed = (op: OperationMetadata) => op.isBucketed; function getDescription(state?: State) { if (!state) { @@ -133,12 +134,10 @@ export const xyVisualization: Visualization = { layers: [ { layerId: frame.addNewLayer(), - accessors: [generateId()], + accessors: [], position: Position.Top, seriesType: defaultSeriesType, showGridlines: false, - splitAccessor: generateId(), - xAccessor: generateId(), }, ], } @@ -147,13 +146,89 @@ export const xyVisualization: Visualization = { getPersistableState: state => state, - renderLayerConfigPanel: (domElement, props) => - render( - - - , - domElement - ), + getConfiguration(props) { + const layer = props.state.layers.find(l => l.layerId === props.layerId)!; + return { + groups: [ + { + groupId: 'x', + groupLabel: i18n.translate('xpack.lens.xyChart.xAxisLabel', { + defaultMessage: 'X-axis', + }), + accessors: layer.xAccessor ? [layer.xAccessor] : [], + filterOperations: isBucketed, + suggestedPriority: 1, + supportsMoreColumns: !layer.xAccessor, + required: true, + dataTestSubj: 'lnsXY_xDimensionPanel', + }, + { + groupId: 'y', + groupLabel: i18n.translate('xpack.lens.xyChart.yAxisLabel', { + defaultMessage: 'Y-axis', + }), + accessors: layer.accessors, + filterOperations: isNumericMetric, + supportsMoreColumns: true, + required: true, + dataTestSubj: 'lnsXY_yDimensionPanel', + }, + { + groupId: 'breakdown', + groupLabel: i18n.translate('xpack.lens.xyChart.splitSeries', { + defaultMessage: 'Break down by', + }), + accessors: layer.splitAccessor ? [layer.splitAccessor] : [], + filterOperations: isBucketed, + suggestedPriority: 0, + supportsMoreColumns: !layer.splitAccessor, + dataTestSubj: 'lnsXY_splitDimensionPanel', + }, + ], + }; + }, + + setDimension({ prevState, layerId, columnId, groupId }) { + const newLayer = prevState.layers.find(l => l.layerId === layerId); + if (!newLayer) { + return prevState; + } + + if (groupId === 'x') { + newLayer.xAccessor = columnId; + } + if (groupId === 'y') { + newLayer.accessors = [...newLayer.accessors.filter(a => a !== columnId), columnId]; + } + if (groupId === 'breakdown') { + newLayer.splitAccessor = columnId; + } + + return { + ...prevState, + layers: prevState.layers.map(l => (l.layerId === layerId ? newLayer : l)), + }; + }, + + removeDimension({ prevState, layerId, columnId }) { + const newLayer = prevState.layers.find(l => l.layerId === layerId); + if (!newLayer) { + return prevState; + } + + if (newLayer.xAccessor === columnId) { + delete newLayer.xAccessor; + } else if (newLayer.splitAccessor === columnId) { + delete newLayer.splitAccessor; + } else if (newLayer.accessors.includes(columnId)) { + newLayer.accessors = newLayer.accessors.filter(a => a !== columnId); + } + + return { + ...prevState, + layers: prevState.layers.map(l => (l.layerId === layerId ? newLayer : l)), + }; + }, getLayerContextMenuIcon({ state, layerId }) { const layer = state.layers.find(l => l.layerId === layerId); @@ -177,8 +252,6 @@ function newLayerState(seriesType: SeriesType, layerId: string): LayerConfig { return { layerId, seriesType, - xAccessor: generateId(), - accessors: [generateId()], - splitAccessor: generateId(), + accessors: [], }; } diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_license_route.ts b/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_license_route.ts index cdc929a2f3bb3..03ec583a34166 100644 --- a/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_license_route.ts +++ b/x-pack/legacy/plugins/license_management/server/np_ready/routes/api/license/register_license_route.ts @@ -15,7 +15,7 @@ export function registerLicenseRoute(server: Server, legacy: Legacy, xpackInfo: validate: { query: schema.object({ acknowledge: schema.string() }), body: schema.object({ - license: schema.object({}, { allowUnknowns: true }), + license: schema.object({}, { unknowns: 'allow' }), }), }, }, diff --git a/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap b/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap index c62b07a89e7a3..85a073c8d9ace 100644 --- a/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap @@ -17,49 +17,27 @@ exports[`should not render relation select when geo field is geo_point 1`] = ` value="My shape" /> - - - - - My index - - -
- my geo field - , - "value": "My index/my geo field", - }, - ] + -
+ } + /> @@ -95,49 +73,27 @@ exports[`should not show "within" relation when filter geometry is not closed 1` value="My shape" /> - - - - - My index - - -
- my geo field - , - "value": "My index/my geo field", - }, - ] + -
+ } + /> - - - - - My index - - -
- my geo field - , - "value": "My index/my geo field", - }, - ] + -
+ } + /> @@ -281,49 +215,27 @@ exports[`should render relation select when geo field is geo_shape 1`] = ` value="My shape" /> - - - - - My index - - -
- my geo field - , - "value": "My index/my geo field", - }, - ] + -
+ } + /> void; +} + +interface State { + selectedField: GeoFieldWithIndex | undefined; + filterLabel: string; +} + +export class DistanceFilterForm extends Component { + state = { + selectedField: this.props.geoFields.length ? this.props.geoFields[0] : undefined, + filterLabel: '', + }; + + _onGeoFieldChange = (selectedField: GeoFieldWithIndex | undefined) => { + this.setState({ selectedField }); + }; + + _onFilterLabelChange = (e: ChangeEvent) => { + this.setState({ + filterLabel: e.target.value, + }); + }; + + _onSubmit = () => { + if (!this.state.selectedField) { + return; + } + this.props.onSubmit({ + filterLabel: this.state.filterLabel, + indexPatternId: this.state.selectedField.indexPatternId, + geoFieldName: this.state.selectedField.geoFieldName, + }); + }; + + render() { + return ( + + + + + + + + + + + + {this.props.buttonLabel} + + + + ); + } +} diff --git a/x-pack/legacy/plugins/maps/public/components/geo_field_with_index.ts b/x-pack/legacy/plugins/maps/public/components/geo_field_with_index.ts new file mode 100644 index 0000000000000..863e0adda8fb2 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/components/geo_field_with_index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +// Maps can contain geo fields from multiple index patterns. GeoFieldWithIndex is used to: +// 1) Combine the geo field along with associated index pattern state. +// 2) Package asynchronously looked up state via indexPatternService to avoid +// PITA of looking up async state in downstream react consumers. +export type GeoFieldWithIndex = { + geoFieldName: string; + geoFieldType: string; + indexPatternTitle: string; + indexPatternId: string; +}; diff --git a/x-pack/legacy/plugins/maps/public/components/geometry_filter_form.js b/x-pack/legacy/plugins/maps/public/components/geometry_filter_form.js index 3308155caa3e4..ac6461345e8bf 100644 --- a/x-pack/legacy/plugins/maps/public/components/geometry_filter_form.js +++ b/x-pack/legacy/plugins/maps/public/components/geometry_filter_form.js @@ -9,9 +9,6 @@ import PropTypes from 'prop-types'; import { EuiForm, EuiFormRow, - EuiSuperSelect, - EuiTextColor, - EuiText, EuiFieldText, EuiButton, EuiSelect, @@ -22,20 +19,7 @@ import { import { i18n } from '@kbn/i18n'; import { ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../../common/constants'; import { getEsSpatialRelationLabel } from '../../common/i18n_getters'; - -const GEO_FIELD_VALUE_DELIMITER = '/'; // `/` is not allowed in index pattern name so should not have collisions - -function createIndexGeoFieldName({ indexPatternTitle, geoFieldName }) { - return `${indexPatternTitle}${GEO_FIELD_VALUE_DELIMITER}${geoFieldName}`; -} - -function splitIndexGeoFieldName(value) { - const split = value.split(GEO_FIELD_VALUE_DELIMITER); - return { - indexPatternTitle: split[0], - geoFieldName: split[1], - }; -} +import { MultiIndexGeoFieldSelect } from './multi_index_geo_field_select'; export class GeometryFilterForm extends Component { static propTypes = { @@ -52,27 +36,13 @@ export class GeometryFilterForm extends Component { }; state = { - geoFieldTag: this.props.geoFields.length - ? createIndexGeoFieldName(this.props.geoFields[0]) - : '', + selectedField: this.props.geoFields.length ? this.props.geoFields[0] : undefined, geometryLabel: this.props.intitialGeometryLabel, relation: ES_SPATIAL_RELATIONS.INTERSECTS, }; - _getSelectedGeoField = () => { - if (!this.state.geoFieldTag) { - return null; - } - - const { indexPatternTitle, geoFieldName } = splitIndexGeoFieldName(this.state.geoFieldTag); - - return this.props.geoFields.find(option => { - return option.indexPatternTitle === indexPatternTitle && option.geoFieldName === geoFieldName; - }); - }; - - _onGeoFieldChange = selectedValue => { - this.setState({ geoFieldTag: selectedValue }); + _onGeoFieldChange = selectedField => { + this.setState({ selectedField }); }; _onGeometryLabelChange = e => { @@ -88,25 +58,21 @@ export class GeometryFilterForm extends Component { }; _onSubmit = () => { - const geoField = this._getSelectedGeoField(); this.props.onSubmit({ geometryLabel: this.state.geometryLabel, - indexPatternId: geoField.indexPatternId, - geoFieldName: geoField.geoFieldName, - geoFieldType: geoField.geoFieldType, + indexPatternId: this.state.selectedField.indexPatternId, + geoFieldName: this.state.selectedField.geoFieldName, + geoFieldType: this.state.selectedField.geoFieldType, relation: this.state.relation, }); }; _renderRelationInput() { - if (!this.state.geoFieldTag) { - return null; - } - - const { geoFieldType } = this._getSelectedGeoField(); - // relationship only used when filtering geo_shape fields - if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT) { + if ( + !this.state.selectedField || + this.state.selectedField.geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT + ) { return null; } @@ -141,20 +107,6 @@ export class GeometryFilterForm extends Component { } render() { - const options = this.props.geoFields.map(({ indexPatternTitle, geoFieldName }) => { - return { - inputDisplay: ( - - - {indexPatternTitle} - -
- {geoFieldName} -
- ), - value: createIndexGeoFieldName({ indexPatternTitle, geoFieldName }), - }; - }); let error; if (this.props.errorMsg) { error = {this.props.errorMsg}; @@ -174,24 +126,11 @@ export class GeometryFilterForm extends Component { />
- - - + {this._renderRelationInput()} @@ -204,7 +143,7 @@ export class GeometryFilterForm extends Component { size="s" fill onClick={this._onSubmit} - isDisabled={!this.state.geometryLabel || !this.state.geoFieldTag} + isDisabled={!this.state.geometryLabel || !this.state.selectedField} isLoading={this.props.isLoading} > {this.props.buttonLabel} diff --git a/x-pack/legacy/plugins/maps/public/components/multi_index_geo_field_select.tsx b/x-pack/legacy/plugins/maps/public/components/multi_index_geo_field_select.tsx new file mode 100644 index 0000000000000..0e5b94f0c6427 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/components/multi_index_geo_field_select.tsx @@ -0,0 +1,78 @@ +/* + * 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 { EuiFormRow, EuiSuperSelect, EuiTextColor, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { GeoFieldWithIndex } from './geo_field_with_index'; + +const OPTION_ID_DELIMITER = '/'; + +function createOptionId(geoField: GeoFieldWithIndex): string { + // Namespace field with indexPatterId to avoid collisions between field names + return `${geoField.indexPatternId}${OPTION_ID_DELIMITER}${geoField.geoFieldName}`; +} + +function splitOptionId(optionId: string) { + const split = optionId.split(OPTION_ID_DELIMITER); + return { + indexPatternId: split[0], + geoFieldName: split[1], + }; +} + +interface Props { + fields: GeoFieldWithIndex[]; + onChange: (newSelectedField: GeoFieldWithIndex | undefined) => void; + selectedField: GeoFieldWithIndex | undefined; +} + +export function MultiIndexGeoFieldSelect({ fields, onChange, selectedField }: Props) { + function onFieldSelect(selectedOptionId: string) { + const { indexPatternId, geoFieldName } = splitOptionId(selectedOptionId); + + const newSelectedField = fields.find(field => { + return field.indexPatternId === indexPatternId && field.geoFieldName === geoFieldName; + }); + onChange(newSelectedField); + } + + const options = fields.map((geoField: GeoFieldWithIndex) => { + return { + inputDisplay: ( + + + {geoField.indexPatternTitle} + +
+ {geoField.geoFieldName} +
+ ), + value: createOptionId(geoField), + }; + }); + + return ( + + + + ); +} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js index ac17915b5f277..eb23607aa2150 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js @@ -11,7 +11,7 @@ import { EuiTitle, EuiPanel, EuiFormRow, EuiFieldText, EuiSpacer } from '@elasti import { ValidatedRange } from '../../../components/validated_range'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ValidatedDualRange } from 'ui/validated_range'; +import { ValidatedDualRange } from '../../../../../../../../src/plugins/kibana_react/public'; import { MAX_ZOOM, MIN_ZOOM } from '../../../../common/constants'; export function LayerSettings(props) { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts new file mode 100644 index 0000000000000..f2ceb8685d43e --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts @@ -0,0 +1,137 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +// @ts-ignore +import turf from 'turf'; +// @ts-ignore +import turfCircle from '@turf/circle'; + +type DrawCircleState = { + circle: { + properties: { + center: {} | null; + radiusKm: number; + }; + id: string | number; + incomingCoords: (coords: unknown[]) => void; + toGeoJSON: () => unknown; + }; +}; + +type MouseEvent = { + lngLat: { + lng: number; + lat: number; + }; +}; + +export const DrawCircle = { + onSetup() { + // @ts-ignore + const circle: unknown = this.newFeature({ + type: 'Feature', + properties: { + center: null, + radiusKm: 0, + }, + geometry: { + type: 'Polygon', + coordinates: [[]], + }, + }); + + // @ts-ignore + this.addFeature(circle); + // @ts-ignore + this.clearSelectedFeatures(); + // @ts-ignore + this.updateUIClasses({ mouse: 'add' }); + // @ts-ignore + this.setActionableState({ + trash: true, + }); + return { + circle, + }; + }, + onKeyUp(state: DrawCircleState, e: { keyCode: number }) { + if (e.keyCode === 27) { + // clear point when user hits escape + state.circle.properties.center = null; + state.circle.properties.radiusKm = 0; + state.circle.incomingCoords([[]]); + } + }, + onClick(state: DrawCircleState, e: MouseEvent) { + if (!state.circle.properties.center) { + // first click, start circle + state.circle.properties.center = [e.lngLat.lng, e.lngLat.lat]; + } else { + // second click, finish draw + // @ts-ignore + this.updateUIClasses({ mouse: 'pointer' }); + state.circle.properties.radiusKm = turf.distance(state.circle.properties.center, [ + e.lngLat.lng, + e.lngLat.lat, + ]); + // @ts-ignore + this.changeMode('simple_select', { featuresId: state.circle.id }); + } + }, + onMouseMove(state: DrawCircleState, e: MouseEvent) { + if (!state.circle.properties.center) { + // circle not started, nothing to update + return; + } + + const mouseLocation = [e.lngLat.lng, e.lngLat.lat]; + state.circle.properties.radiusKm = turf.distance(state.circle.properties.center, mouseLocation); + const newCircleFeature = turfCircle( + state.circle.properties.center, + state.circle.properties.radiusKm + ); + state.circle.incomingCoords(newCircleFeature.geometry.coordinates); + }, + onStop(state: DrawCircleState) { + // @ts-ignore + this.updateUIClasses({ mouse: 'none' }); + // @ts-ignore + this.activateUIButton(); + + // @ts-ignore + if (this.getFeature(state.circle.id) === undefined) return; + + if (state.circle.properties.center && state.circle.properties.radiusKm > 0) { + // @ts-ignore + this.map.fire('draw.create', { + features: [state.circle.toGeoJSON()], + }); + } else { + // @ts-ignore + this.deleteFeature([state.circle.id], { silent: true }); + // @ts-ignore + this.changeMode('simple_select', {}, { silent: true }); + } + }, + toDisplayFeatures( + state: DrawCircleState, + geojson: { properties: { active: string } }, + display: (geojson: unknown) => unknown + ) { + if (state.circle.properties.center) { + geojson.properties.active = 'true'; + return display(geojson); + } + }, + onTrash(state: DrawCircleState) { + // @ts-ignore + this.deleteFeature([state.circle.id], { silent: true }); + // @ts-ignore + this.changeMode('simple_select'); + }, +}; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js index f1b4fe2aad1f7..99abe5d108b5a 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js @@ -9,7 +9,9 @@ import React from 'react'; import { DRAW_TYPE } from '../../../../../common/constants'; import MapboxDraw from '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw-unminified'; import DrawRectangle from 'mapbox-gl-draw-rectangle-mode'; +import { DrawCircle } from './draw_circle'; import { + createDistanceFilterWithMeta, createSpatialFilterWithBoundingBox, createSpatialFilterWithGeometry, getBoundingBoxGeometry, @@ -19,6 +21,7 @@ import { DrawTooltip } from './draw_tooltip'; const mbDrawModes = MapboxDraw.modes; mbDrawModes.draw_rectangle = DrawRectangle; +mbDrawModes.draw_circle = DrawCircle; export class DrawControl extends React.Component { constructor() { @@ -60,7 +63,21 @@ export class DrawControl extends React.Component { return; } - const isBoundingBox = this.props.drawState.drawType === DRAW_TYPE.BOUNDS; + if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) { + const circle = e.features[0]; + roundCoordinates(circle.properties.center); + const filter = createDistanceFilterWithMeta({ + alias: this.props.drawState.filterLabel, + distanceKm: _.round(circle.properties.radiusKm, circle.properties.radiusKm > 10 ? 0 : 2), + geoFieldName: this.props.drawState.geoFieldName, + indexPatternId: this.props.drawState.indexPatternId, + point: circle.properties.center, + }); + this.props.addFilters([filter]); + this.props.disableDrawState(); + return; + } + const geometry = e.features[0].geometry; // MapboxDraw returns coordinates with 12 decimals. Round to a more reasonable number roundCoordinates(geometry.coordinates); @@ -73,15 +90,16 @@ export class DrawControl extends React.Component { geometryLabel: this.props.drawState.geometryLabel, relation: this.props.drawState.relation, }; - const filter = isBoundingBox - ? createSpatialFilterWithBoundingBox({ - ...options, - geometry: getBoundingBoxGeometry(geometry), - }) - : createSpatialFilterWithGeometry({ - ...options, - geometry, - }); + const filter = + this.props.drawState.drawType === DRAW_TYPE.BOUNDS + ? createSpatialFilterWithBoundingBox({ + ...options, + geometry: getBoundingBoxGeometry(geometry), + }) + : createSpatialFilterWithGeometry({ + ...options, + geometry, + }); this.props.addFilters([filter]); } catch (error) { // TODO notify user why filter was not created @@ -109,11 +127,14 @@ export class DrawControl extends React.Component { this.props.mbMap.getCanvas().style.cursor = 'crosshair'; this.props.mbMap.on('draw.create', this._onDraw); } - const mbDrawMode = - this.props.drawState.drawType === DRAW_TYPE.POLYGON - ? this._mbDrawControl.modes.DRAW_POLYGON - : 'draw_rectangle'; - this._mbDrawControl.changeMode(mbDrawMode); + + if (this.props.drawState.drawType === DRAW_TYPE.BOUNDS) { + this._mbDrawControl.changeMode('draw_rectangle'); + } else if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) { + this._mbDrawControl.changeMode('draw_circle'); + } else if (this.props.drawState.drawType === DRAW_TYPE.POLYGON) { + this._mbDrawControl.changeMode(this._mbDrawControl.modes.DRAW_POLYGON); + } } render() { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_tooltip.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_tooltip.js index 463fe52981410..c8bde29b94fb6 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_tooltip.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_tooltip.js @@ -42,14 +42,24 @@ export class DrawTooltip extends Component { } render() { - const instructions = - this.props.drawState.drawType === DRAW_TYPE.BOUNDS - ? i18n.translate('xpack.maps.drawTooltip.boundsInstructions', { - defaultMessage: 'Click to start rectangle. Click again to finish.', - }) - : i18n.translate('xpack.maps.drawTooltip.polygonInstructions', { - defaultMessage: 'Click to add vertex. Double click to finish.', - }); + let instructions; + if (this.props.drawState.drawType === DRAW_TYPE.BOUNDS) { + instructions = i18n.translate('xpack.maps.drawTooltip.boundsInstructions', { + defaultMessage: + 'Click to start rectangle. Move mouse to adjust rectangle size. Click again to finish.', + }); + } else if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) { + instructions = i18n.translate('xpack.maps.drawTooltip.distanceInstructions', { + defaultMessage: 'Click to set point. Move mouse to adjust distance. Click to finish.', + }); + } else if (this.props.drawState.drawType === DRAW_TYPE.POLYGON) { + instructions = i18n.translate('xpack.maps.drawTooltip.polygonInstructions', { + defaultMessage: 'Click to start shape. Click to add vertex. Double click to finish.', + }); + } else { + // unknown draw type, tooltip not needed + return null; + } const tooltipAnchor = (
diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap b/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap index 681c3f0fbfd61..d7fa099fe9dbe 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap @@ -41,6 +41,10 @@ exports[`Should render cancel button when drawing 1`] = ` "name": "Draw bounds to filter data", "panel": 2, }, + Object { + "name": "Draw distance to filter data", + "panel": 3, + }, ], "title": "Tools", }, @@ -86,6 +90,25 @@ exports[`Should render cancel button when drawing 1`] = ` "id": 2, "title": "Draw bounds", }, + Object { + "content": , + "id": 3, + "title": "Draw distance", + }, ] } /> @@ -144,6 +167,10 @@ exports[`renders 1`] = ` "name": "Draw bounds to filter data", "panel": 2, }, + Object { + "name": "Draw distance to filter data", + "panel": 3, + }, ], "title": "Tools", }, @@ -189,6 +216,25 @@ exports[`renders 1`] = ` "id": 2, "title": "Draw bounds", }, + Object { + "content": , + "id": 3, + "title": "Draw distance", + }, ] } /> diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js b/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js index ea6ffe3ba1435..e7c125abe70c7 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js @@ -14,9 +14,10 @@ import { EuiButton, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DRAW_TYPE } from '../../../../common/constants'; +import { DRAW_TYPE, ES_GEO_FIELD_TYPE } from '../../../../common/constants'; import { FormattedMessage } from '@kbn/i18n/react'; import { GeometryFilterForm } from '../../../components/geometry_filter_form'; +import { DistanceFilterForm } from '../../../components/distance_filter_form'; const DRAW_SHAPE_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabel', { defaultMessage: 'Draw shape to filter data', @@ -26,6 +27,10 @@ const DRAW_BOUNDS_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawBoundsLa defaultMessage: 'Draw bounds to filter data', }); +const DRAW_DISTANCE_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawDistanceLabel', { + defaultMessage: 'Draw distance to filter data', +}); + const DRAW_SHAPE_LABEL_SHORT = i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabelShort', { defaultMessage: 'Draw shape', }); @@ -34,6 +39,13 @@ const DRAW_BOUNDS_LABEL_SHORT = i18n.translate('xpack.maps.toolbarOverlay.drawBo defaultMessage: 'Draw bounds', }); +const DRAW_DISTANCE_LABEL_SHORT = i18n.translate( + 'xpack.maps.toolbarOverlay.drawDistanceLabelShort', + { + defaultMessage: 'Draw distance', + } +); + export class ToolsControl extends Component { state = { isPopoverOpen: false, @@ -65,23 +77,43 @@ export class ToolsControl extends Component { this._closePopover(); }; + _initiateDistanceDraw = options => { + this.props.initiateDraw({ + drawType: DRAW_TYPE.DISTANCE, + ...options, + }); + this._closePopover(); + }; + _getDrawPanels() { + const tools = [ + { + name: DRAW_SHAPE_LABEL, + panel: 1, + }, + { + name: DRAW_BOUNDS_LABEL, + panel: 2, + }, + ]; + + const hasGeoPoints = this.props.geoFields.some(({ geoFieldType }) => { + return geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT; + }); + if (hasGeoPoints) { + tools.push({ + name: DRAW_DISTANCE_LABEL, + panel: 3, + }); + } + return [ { id: 0, title: i18n.translate('xpack.maps.toolbarOverlay.tools.toolbarTitle', { defaultMessage: 'Tools', }), - items: [ - { - name: DRAW_SHAPE_LABEL, - panel: 1, - }, - { - name: DRAW_BOUNDS_LABEL, - panel: 2, - }, - ], + items: tools, }, { id: 1, @@ -119,6 +151,20 @@ export class ToolsControl extends Component { /> ), }, + { + id: 3, + title: DRAW_DISTANCE_LABEL_SHORT, + content: ( + { + return geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT; + })} + onSubmit={this._initiateDistanceDraw} + /> + ), + }, ]; } diff --git a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js index 9b33d3036785c..79467e26ec3fa 100644 --- a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js +++ b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js @@ -344,6 +344,39 @@ function createGeometryFilterWithMeta({ return createGeoPolygonFilter(geometry.coordinates, geoFieldName, { meta }); } +export function createDistanceFilterWithMeta({ + alias, + distanceKm, + geoFieldName, + indexPatternId, + point, +}) { + const meta = { + type: SPATIAL_FILTER_TYPE, + negate: false, + index: indexPatternId, + key: geoFieldName, + alias: alias + ? alias + : i18n.translate('xpack.maps.es_geo_utils.distanceFilterAlias', { + defaultMessage: '{geoFieldName} within {distanceKm}km of {pointLabel}', + values: { + distanceKm, + geoFieldName, + pointLabel: point.join(','), + }, + }), + }; + + return { + geo_distance: { + distance: `${distanceKm}km`, + [geoFieldName]: point, + }, + meta, + }; +} + export function roundCoordinates(coordinates) { for (let i = 0; i < coordinates.length; i++) { const value = coordinates[i]; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/size_range_selector.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/size_range_selector.js index 1d5815a84920c..5de7b462136e1 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/size_range_selector.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/size_range_selector.js @@ -6,7 +6,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { ValidatedDualRange } from 'ui/validated_range'; +import { ValidatedDualRange } from '../../../../../../../../../../src/plugins/kibana_react/public'; import { MIN_SIZE, MAX_SIZE } from '../../vector_style_defaults'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/legacy/plugins/maps/public/meta.js b/x-pack/legacy/plugins/maps/public/meta.js index c5cfb582976c1..4d81785ff7a0a 100644 --- a/x-pack/legacy/plugins/maps/public/meta.js +++ b/x-pack/legacy/plugins/maps/public/meta.js @@ -9,6 +9,7 @@ import { EMS_FILES_CATALOGUE_PATH, EMS_TILES_CATALOGUE_PATH, EMS_GLYPHS_PATH, + EMS_APP_NAME, } from '../common/constants'; import chrome from 'ui/chrome'; import { i18n } from '@kbn/i18n'; @@ -56,7 +57,8 @@ export function getEMSClient() { emsClient = new EMSClient({ language: i18n.getLocale(), - kbnVersion: chrome.getInjected('kbnPkgVersion'), + appVersion: chrome.getInjected('kbnPkgVersion'), + appName: EMS_APP_NAME, tileApiUrl, fileApiUrl, landingPageUrl: chrome.getInjected('emsLandingPageUrl'), diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts index 3ce46e2955f50..6a363af9e57d4 100644 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts @@ -19,6 +19,7 @@ import { // @ts-ignore } from '../../common/constants'; import { LayerDescriptor } from '../../common/descriptor_types'; +import { MapSavedObject } from '../../../../../plugins/maps/common/map_saved_object_type'; interface IStats { [key: string]: { @@ -32,33 +33,6 @@ interface ILayerTypeCount { [key: string]: number; } -interface IMapSavedObject { - [key: string]: any; - fields: IFieldType[]; - title: string; - id?: string; - type?: string; - timeFieldName?: string; - fieldFormatMap?: Record< - string, - { - id: string; - params: unknown; - } - >; - attributes?: { - title?: string; - description?: string; - mapStateJSON?: string; - layerListJSON?: string; - uiStateJSON?: string; - bounds?: { - type?: string; - coordinates?: []; - }; - }; -} - function getUniqueLayerCounts(layerCountsList: ILayerTypeCount[], mapsCount: number) { const uniqueLayerTypes = _.uniq(_.flatten(layerCountsList.map(lTypes => Object.keys(lTypes)))); @@ -102,7 +76,7 @@ export function buildMapsTelemetry({ indexPatternSavedObjects, settings, }: { - mapSavedObjects: IMapSavedObject[]; + mapSavedObjects: MapSavedObject[]; indexPatternSavedObjects: IIndexPattern[]; settings: SavedObjectAttribute; }): SavedObjectAttributes { @@ -183,7 +157,7 @@ export async function getMapsTelemetry( savedObjectsClient: SavedObjectsClientContract, config: Function ) { - const mapSavedObjects: IMapSavedObject[] = await getMapSavedObjects(savedObjectsClient); + const mapSavedObjects: MapSavedObject[] = await getMapSavedObjects(savedObjectsClient); const indexPatternSavedObjects: IIndexPattern[] = await getIndexPatternSavedObjects( savedObjectsClient ); diff --git a/x-pack/legacy/plugins/maps/server/routes.js b/x-pack/legacy/plugins/maps/server/routes.js index 757750dbb0813..7ca659148449f 100644 --- a/x-pack/legacy/plugins/maps/server/routes.js +++ b/x-pack/legacy/plugins/maps/server/routes.js @@ -5,6 +5,7 @@ */ import { + EMS_APP_NAME, EMS_CATALOGUE_PATH, EMS_FILES_API_PATH, EMS_FILES_CATALOGUE_PATH, @@ -38,7 +39,8 @@ export function initRoutes(server, licenseUid) { if (mapConfig.includeElasticMapsService) { emsClient = new EMSClient({ language: i18n.getLocale(), - kbnVersion: serverConfig.get('pkg.version'), + appVersion: serverConfig.get('pkg.version'), + appName: EMS_APP_NAME, fileApiUrl: mapConfig.emsFileApiUrl, tileApiUrl: mapConfig.emsTileApiUrl, landingPageUrl: mapConfig.emsLandingPageUrl, diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts index c14b64a32fb5c..b506784bf15ee 100644 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts +++ b/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts @@ -19,7 +19,6 @@ import { StateManagementConfigProvider, AppStateProvider, KbnUrlProvider, - RedirectWhenMissingProvider, npStart, } from '../legacy_imports'; @@ -79,8 +78,7 @@ function createLocalStateModule() { function createLocalKbnUrlModule() { angular .module('monitoring/KbnUrl', ['monitoring/Private', 'ngRoute']) - .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)) - .service('redirectWhenMissing', (Private: IPrivate) => Private(RedirectWhenMissingProvider)); + .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)); } function createLocalConfigModule(core: AppMountContext['core']) { diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/legacy_imports.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/legacy_imports.ts index a2ebe8231456f..208b7e2acdb0f 100644 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/legacy_imports.ts +++ b/x-pack/legacy/plugins/monitoring/public/np_imports/legacy_imports.ts @@ -18,5 +18,5 @@ export { AppStateProvider } from 'ui/state_management/app_state'; // @ts-ignore export { EventsProvider } from 'ui/events'; // @ts-ignore -export { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url'; +export { KbnUrlProvider } from 'ui/url'; export { registerTimefilterWithGlobalStateFactory } from 'ui/timefilter/setup_router'; diff --git a/x-pack/legacy/plugins/reporting/index.ts b/x-pack/legacy/plugins/reporting/index.ts index 9ce4e807f8ef8..89e98302cddc9 100644 --- a/x-pack/legacy/plugins/reporting/index.ts +++ b/x-pack/legacy/plugins/reporting/index.ts @@ -10,7 +10,7 @@ import { resolve } from 'path'; import { PLUGIN_ID, UI_SETTINGS_CUSTOM_PDF_LOGO } from './common/constants'; import { config as reportingConfig } from './config'; import { legacyInit } from './server/legacy'; -import { ReportingConfigOptions, ReportingPluginSpecOptions } from './types'; +import { ReportingPluginSpecOptions } from './types'; const kbToBase64Length = (kb: number) => { return Math.floor((kb * 1024 * 8) / 6); @@ -25,20 +25,6 @@ export const reporting = (kibana: any) => { config: reportingConfig, uiExports: { - shareContextMenuExtensions: [ - 'plugins/reporting/share_context_menu/register_csv_reporting', - 'plugins/reporting/share_context_menu/register_reporting', - ], - embeddableActions: ['plugins/reporting/panel_actions/get_csv_panel_action'], - home: ['plugins/reporting/register_feature'], - managementSections: ['plugins/reporting/views/management'], - injectDefaultVars(server: Legacy.Server, options?: ReportingConfigOptions) { - const config = server.config(); - return { - reportingPollConfig: options ? options.poll : {}, - enablePanelActionDownload: config.get('xpack.reporting.csv.enablePanelActionDownload'), - }; - }, uiSettingDefaults: { [UI_SETTINGS_CUSTOM_PDF_LOGO]: { name: i18n.translate('xpack.reporting.pdfFooterImageLabel', { diff --git a/x-pack/legacy/plugins/reporting/public/components/report_listing.test.tsx b/x-pack/legacy/plugins/reporting/public/components/report_listing.test.tsx deleted file mode 100644 index d78eb5c409c1f..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/components/report_listing.test.tsx +++ /dev/null @@ -1,77 +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. - */ - -interface JobData { - _index: string; - _id: string; - _source: { - browser_type: string; - created_at: string; - jobtype: string; - created_by: string; - payload: { - type: string; - title: string; - }; - kibana_name?: string; // undefined if job is pending (not yet claimed by an instance) - kibana_id?: string; // undefined if job is pending (not yet claimed by an instance) - output?: { content_type: string; size: number }; // undefined if job is incomplete - completed_at?: string; // undefined if job is incomplete - }; -} - -jest.mock('ui/chrome', () => ({ - getInjected() { - return { - jobsRefresh: { - interval: 10, - intervalErrorMultiplier: 2, - }, - }; - }, -})); - -jest.mock('ui/kfetch', () => ({ - kfetch: ({ pathname }: { pathname: string }): Promise => { - if (pathname === '/api/reporting/jobs/list') { - return Promise.resolve([ - { _index: '.reporting-2019.08.18', _id: 'jzoik8dh1q2i89fb5f19znm6', _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1635, height: 792 } }, type: 'dashboard', title: 'Names', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:24.869Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik7tn1q2i89fb5f60e5ve', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1635, height: 792 } }, type: 'dashboard', title: 'Names', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:24.155Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik5tb1q2i89fb5fckchny', _score: null, _source: { payload: { layout: { id: 'png', dimensions: { width: 1898, height: 876 } }, title: 'cool dashboard', type: 'dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:21.551Z', jobtype: 'PNG', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik5a11q2i89fb5f130t2m', _score: null, _source: { payload: { layout: { id: 'png', dimensions: { width: 1898, height: 876 } }, title: 'cool dashboard', type: 'dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:20.857Z', jobtype: 'PNG', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik3ka1q2i89fb5fdx93g7', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1898, height: 876 } }, type: 'dashboard', title: 'cool dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:18.634Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik2vt1q2i89fb5ffw723n', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1898, height: 876 } }, type: 'dashboard', title: 'cool dashboard', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:17.753Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoik1851q2i89fb5fdge6e7', _score: null, _source: { payload: { layout: { id: 'preserve_layout', dimensions: { width: 1080, height: 720 } }, type: 'canvas workpad', title: 'My Canvas Workpad - Dark', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:15.605Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoijyre1q2i89fb5fa7xzvi', _score: null, _source: { payload: { type: 'dashboard', title: 'tests-panels', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:12.410Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jzoijv5h1q2i89fb5ffklnhx', _score: null, _source: { payload: { type: 'dashboard', title: 'tests-panels', }, max_attempts: 3, browser_type: 'chromium', created_at: '2019-08-23T19:34:07.733Z', jobtype: 'printable_pdf', created_by: 'elastic', attempts: 0, status: 'pending', }, }, // prettier-ignore - { _index: '.reporting-2019.08.18', _id: 'jznhgk7r1bx789fb5f6hxok7', _score: null, _source: { kibana_name: 'spicy.local', browser_type: 'chromium', created_at: '2019-08-23T02:15:47.799Z', jobtype: 'printable_pdf', created_by: 'elastic', kibana_id: 'ca75e26c-2b7d-464f-aef0-babb67c735a0', output: { content_type: 'application/pdf', size: 877114 }, completed_at: '2019-08-23T02:15:57.707Z', payload: { type: 'dashboard (legacy)', title: 'tests-panels', }, max_attempts: 3, started_at: '2019-08-23T02:15:48.794Z', attempts: 1, status: 'completed', }, }, // prettier-ignore - ]); - } - - // query for jobs count - return Promise.resolve(18); - }, -})); - -import React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { ReportListing } from './report_listing'; - -describe('ReportListing', () => { - it('Report job listing with some items', () => { - const wrapper = mountWithIntl( - - ); - wrapper.update(); - const input = wrapper.find('[data-test-subj="reportJobListing"]'); - expect(input).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/reporting/public/lib/download_report.ts b/x-pack/legacy/plugins/reporting/public/lib/download_report.ts deleted file mode 100644 index 54194c87afabc..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/lib/download_report.ts +++ /dev/null @@ -1,23 +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 { npStart } from 'ui/new_platform'; -import { API_BASE_URL } from '../../common/constants'; - -const { core } = npStart; - -export function getReportURL(jobId: string) { - const apiBaseUrl = core.http.basePath.prepend(API_BASE_URL); - const downloadLink = `${apiBaseUrl}/jobs/download/${jobId}`; - - return downloadLink; -} - -export function downloadReport(jobId: string) { - const location = getReportURL(jobId); - - window.open(location); -} diff --git a/x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts b/x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts deleted file mode 100644 index 87d4174168b7f..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/lib/job_queue_client.ts +++ /dev/null @@ -1,89 +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 { npStart } from 'ui/new_platform'; -import { API_LIST_URL } from '../../common/constants'; - -const { core } = npStart; - -export interface JobQueueEntry { - _id: string; - _source: any; -} - -export interface JobContent { - content: string; - content_type: boolean; -} - -export interface JobInfo { - kibana_name: string; - kibana_id: string; - browser_type: string; - created_at: string; - priority: number; - jobtype: string; - created_by: string; - timeout: number; - output: { - content_type: string; - size: number; - warnings: string[]; - }; - process_expiration: string; - completed_at: string; - payload: { - layout: { id: string; dimensions: { width: number; height: number } }; - objects: Array<{ relativeUrl: string }>; - type: string; - title: string; - forceNow: string; - browserTimezone: string; - }; - meta: { - layout: string; - objectType: string; - }; - max_attempts: number; - started_at: string; - attempts: number; - status: string; -} - -class JobQueueClient { - public list = (page = 0, jobIds: string[] = []): Promise => { - const query = { page } as any; - if (jobIds.length > 0) { - // Only getting the first 10, to prevent URL overflows - query.ids = jobIds.slice(0, 10).join(','); - } - - return core.http.get(`${API_LIST_URL}/list`, { - query, - asSystemRequest: true, - }); - }; - - public total(): Promise { - return core.http.get(`${API_LIST_URL}/count`, { - asSystemRequest: true, - }); - } - - public getContent(jobId: string): Promise { - return core.http.get(`${API_LIST_URL}/output/${jobId}`, { - asSystemRequest: true, - }); - } - - public getInfo(jobId: string): Promise { - return core.http.get(`${API_LIST_URL}/info/${jobId}`, { - asSystemRequest: true, - }); - } -} - -export const jobQueueClient = new JobQueueClient(); diff --git a/x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts b/x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts deleted file mode 100644 index d471dc57fc9e1..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts +++ /dev/null @@ -1,37 +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 { stringify } from 'query-string'; -import { npStart } from 'ui/new_platform'; -// @ts-ignore -import rison from 'rison-node'; -import { add } from './job_completion_notifications'; - -const { core } = npStart; -const API_BASE_URL = '/api/reporting/generate'; - -interface JobParams { - [paramName: string]: any; -} - -export const getReportingJobPath = (exportType: string, jobParams: JobParams) => { - const params = stringify({ jobParams: rison.encode(jobParams) }); - - return `${core.http.basePath.prepend(API_BASE_URL)}/${exportType}?${params}`; -}; - -export const createReportingJob = async (exportType: string, jobParams: any) => { - const jobParamsRison = rison.encode(jobParams); - const resp = await core.http.post(`${API_BASE_URL}/${exportType}`, { - method: 'POST', - body: JSON.stringify({ - jobParams: jobParamsRison, - }), - }); - - add(resp.job.id); - - return resp; -}; diff --git a/x-pack/legacy/plugins/reporting/public/register_feature.ts b/x-pack/legacy/plugins/reporting/public/register_feature.ts deleted file mode 100644 index 4e8d32facfcec..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/register_feature.ts +++ /dev/null @@ -1,27 +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 { i18n } from '@kbn/i18n'; -import { npSetup } from 'ui/new_platform'; -import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; - -const { - plugins: { home }, -} = npSetup; - -home.featureCatalogue.register({ - id: 'reporting', - title: i18n.translate('xpack.reporting.registerFeature.reportingTitle', { - defaultMessage: 'Reporting', - }), - description: i18n.translate('xpack.reporting.registerFeature.reportingDescription', { - defaultMessage: 'Manage your reports generated from Discover, Visualize, and Dashboard.', - }), - icon: 'reportingApp', - path: '/app/kibana#/management/kibana/reporting', - showOnHomePage: false, - category: FeatureCatalogueCategory.ADMIN, -}); diff --git a/x-pack/legacy/plugins/reporting/public/views/management/jobs.html b/x-pack/legacy/plugins/reporting/public/views/management/jobs.html deleted file mode 100644 index 5471513d64d95..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/views/management/jobs.html +++ /dev/null @@ -1,3 +0,0 @@ - -
-
\ No newline at end of file diff --git a/x-pack/legacy/plugins/reporting/public/views/management/jobs.js b/x-pack/legacy/plugins/reporting/public/views/management/jobs.js deleted file mode 100644 index 7205fad8cca53..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/views/management/jobs.js +++ /dev/null @@ -1,59 +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 { render, unmountComponentAtNode } from 'react-dom'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; - -import routes from 'ui/routes'; -import template from 'plugins/reporting/views/management/jobs.html'; - -import { ReportListing } from '../../components/report_listing'; -import { i18n } from '@kbn/i18n'; -import { I18nContext } from 'ui/i18n'; -import { MANAGEMENT_BREADCRUMB } from 'ui/management'; - -const REACT_ANCHOR_DOM_ELEMENT_ID = 'reportListingAnchor'; - -routes.when('/management/kibana/reporting', { - template, - k7Breadcrumbs: () => [ - MANAGEMENT_BREADCRUMB, - { - text: i18n.translate('xpack.reporting.breadcrumb', { - defaultMessage: 'Reporting', - }), - }, - ], - controllerAs: 'jobsCtrl', - controller($scope, kbnUrl) { - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_ANCHOR_DOM_ELEMENT_ID); - if (!node) { - return; - } - - render( - - - , - node - ); - }); - - $scope.$on('$destroy', () => { - const node = document.getElementById(REACT_ANCHOR_DOM_ELEMENT_ID); - if (node) { - unmountComponentAtNode(node); - } - }); - }, -}); diff --git a/x-pack/legacy/plugins/reporting/public/views/management/management.js b/x-pack/legacy/plugins/reporting/public/views/management/management.js deleted file mode 100644 index 8643e6fa8b8b4..0000000000000 --- a/x-pack/legacy/plugins/reporting/public/views/management/management.js +++ /dev/null @@ -1,44 +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 { management } from 'ui/management'; -import { i18n } from '@kbn/i18n'; -import routes from 'ui/routes'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; - -import 'plugins/reporting/views/management/jobs'; - -routes.defaults(/\/management/, { - resolve: { - reportingManagementSection: function() { - const kibanaManagementSection = management.getSection('kibana'); - const showReportingLinks = xpackInfo.get('features.reporting.management.showLinks'); - - kibanaManagementSection.deregister('reporting'); - if (showReportingLinks) { - const enableReportingLinks = xpackInfo.get('features.reporting.management.enableLinks'); - const tooltipMessage = xpackInfo.get('features.reporting.management.message'); - - let url; - let tooltip; - if (enableReportingLinks) { - url = '#/management/kibana/reporting'; - } else { - tooltip = tooltipMessage; - } - - return kibanaManagementSection.register('reporting', { - order: 15, - display: i18n.translate('xpack.reporting.management.reportingTitle', { - defaultMessage: 'Reporting', - }), - url, - tooltip, - }); - } - }, - }, -}); diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts index 49868bb7ad5d5..56622617586f7 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts @@ -82,16 +82,21 @@ export function registerGenerateFromJobParams( } const { exportType } = request.params; + let jobParams; let response; try { - const jobParams = rison.decode(jobParamsRison) as object | null; + jobParams = rison.decode(jobParamsRison) as object | null; if (!jobParams) { throw new Error('missing jobParams!'); } - response = await handler(exportType, jobParams, legacyRequest, h); } catch (err) { throw boom.badRequest(`invalid rison: ${jobParamsRison}`); } + try { + response = await handler(exportType, jobParams, legacyRequest, h); + } catch (err) { + throw handleError(exportType, err); + } return response; }, }); diff --git a/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts b/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts new file mode 100644 index 0000000000000..54d9671692c5d --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts @@ -0,0 +1,140 @@ +/* + * 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 Hapi from 'hapi'; +import { createMockReportingCore } from '../../test_helpers'; +import { Logger, ServerFacade } from '../../types'; +import { ReportingCore, ReportingSetupDeps } from '../../server/types'; + +jest.mock('./lib/authorized_user_pre_routing', () => ({ + authorizedUserPreRoutingFactory: () => () => ({}), +})); +jest.mock('./lib/reporting_feature_pre_routing', () => ({ + reportingFeaturePreRoutingFactory: () => () => () => ({ + jobTypes: ['unencodedJobType', 'base64EncodedJobType'], + }), +})); + +import { registerJobGenerationRoutes } from './generation'; + +let mockServer: Hapi.Server; +let mockReportingPlugin: ReportingCore; +const mockLogger = ({ + error: jest.fn(), + debug: jest.fn(), +} as unknown) as Logger; + +beforeEach(async () => { + mockServer = new Hapi.Server({ + debug: false, + port: 8080, + routes: { log: { collect: true } }, + }); + mockServer.config = () => ({ get: jest.fn(), has: jest.fn() }); + mockReportingPlugin = await createMockReportingCore(); + mockReportingPlugin.getEnqueueJob = async () => + jest.fn().mockImplementation(() => ({ toJSON: () => '{ "job": "data" }' })); +}); + +const mockPlugins = { + elasticsearch: { + adminClient: { callAsInternalUser: jest.fn() }, + }, + security: null, +}; + +const getErrorsFromRequest = (request: Hapi.Request) => { + // @ts-ignore error property doesn't exist on RequestLog + return request.logs.filter(log => log.tags.includes('error')).map(log => log.error); // NOTE: error stack is available +}; + +test(`returns 400 if there are no job params`, async () => { + registerJobGenerationRoutes( + mockReportingPlugin, + (mockServer as unknown) as ServerFacade, + (mockPlugins as unknown) as ReportingSetupDeps, + mockLogger + ); + + const options = { + method: 'POST', + url: '/api/reporting/generate/printablePdf', + }; + + const { payload, request } = await mockServer.inject(options); + expect(payload).toMatchInlineSnapshot( + `"{\\"statusCode\\":400,\\"error\\":\\"Bad Request\\",\\"message\\":\\"A jobParams RISON string is required\\"}"` + ); + + const errorLogs = getErrorsFromRequest(request); + expect(errorLogs).toMatchInlineSnapshot(` + Array [ + [Error: A jobParams RISON string is required], + ] + `); +}); + +test(`returns 400 if job params is invalid`, async () => { + registerJobGenerationRoutes( + mockReportingPlugin, + (mockServer as unknown) as ServerFacade, + (mockPlugins as unknown) as ReportingSetupDeps, + mockLogger + ); + + const options = { + method: 'POST', + url: '/api/reporting/generate/printablePdf', + payload: { jobParams: `foo:` }, + }; + + const { payload, request } = await mockServer.inject(options); + expect(payload).toMatchInlineSnapshot( + `"{\\"statusCode\\":400,\\"error\\":\\"Bad Request\\",\\"message\\":\\"invalid rison: foo:\\"}"` + ); + + const errorLogs = getErrorsFromRequest(request); + expect(errorLogs).toMatchInlineSnapshot(` + Array [ + [Error: invalid rison: foo:], + ] + `); +}); + +test(`returns 500 if job handler throws an error`, async () => { + mockReportingPlugin.getEnqueueJob = async () => + jest.fn().mockImplementation(() => ({ + toJSON: () => { + throw new Error('you found me'); + }, + })); + + registerJobGenerationRoutes( + mockReportingPlugin, + (mockServer as unknown) as ServerFacade, + (mockPlugins as unknown) as ReportingSetupDeps, + mockLogger + ); + + const options = { + method: 'POST', + url: '/api/reporting/generate/printablePdf', + payload: { jobParams: `abc` }, + }; + + const { payload, request } = await mockServer.inject(options); + expect(payload).toMatchInlineSnapshot( + `"{\\"statusCode\\":500,\\"error\\":\\"Internal Server Error\\",\\"message\\":\\"An internal server error occurred\\"}"` + ); + + const errorLogs = getErrorsFromRequest(request); + expect(errorLogs).toMatchInlineSnapshot(` + Array [ + [Error: you found me], + [Error: you found me], + ] + `); +}); diff --git a/x-pack/legacy/plugins/reporting/types.d.ts b/x-pack/legacy/plugins/reporting/types.d.ts index b4d49fd21f230..917e9d7daae40 100644 --- a/x-pack/legacy/plugins/reporting/types.d.ts +++ b/x-pack/legacy/plugins/reporting/types.d.ts @@ -23,22 +23,6 @@ export type Job = EventEmitter & { }; }; -export interface ReportingConfigOptions { - browser: BrowserConfig; - poll: { - jobCompletionNotifier: { - interval: number; - intervalErrorMultiplier: number; - }; - jobsRefresh: { - interval: number; - intervalErrorMultiplier: number; - }; - }; - queue: QueueConfig; - capture: CaptureConfig; -} - export interface NetworkPolicyRule { allow: boolean; protocol: string; diff --git a/x-pack/legacy/plugins/rollup/server/routes/api/jobs.ts b/x-pack/legacy/plugins/rollup/server/routes/api/jobs.ts index e58bc95b9a375..e45713e2b807c 100644 --- a/x-pack/legacy/plugins/rollup/server/routes/api/jobs.ts +++ b/x-pack/legacy/plugins/rollup/server/routes/api/jobs.ts @@ -127,7 +127,7 @@ export function registerJobsRoute(deps: RouteDependencies, legacy: ServerShim) { { id: schema.string(), }, - { allowUnknowns: true } + { unknowns: 'allow' } ), }), }, diff --git a/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts new file mode 100644 index 0000000000000..de17f40a3ac71 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts @@ -0,0 +1,156 @@ +/* + * 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 { + NUMBER_OF_SIGNALS, + OPEN_CLOSE_SIGNALS_BTN, + SELECTED_SIGNALS, + SHOWING_SIGNALS, + SIGNALS, +} from '../screens/detections'; + +import { + closeFirstSignal, + closeSignals, + goToClosedSignals, + goToOpenedSignals, + openSignals, + selectNumberOfSignals, + waitForSignalsPanelToBeLoaded, + waitForSignals, + waitForSignalsToBeLoaded, +} from '../tasks/detections'; +import { esArchiverLoad } from '../tasks/es_archiver'; +import { loginAndWaitForPage } from '../tasks/login'; + +import { DETECTIONS } from '../urls/navigation'; + +describe('Detections', () => { + beforeEach(() => { + esArchiverLoad('signals'); + loginAndWaitForPage(DETECTIONS); + }); + + it('Closes and opens signals', () => { + waitForSignalsPanelToBeLoaded(); + waitForSignalsToBeLoaded(); + + cy.get(NUMBER_OF_SIGNALS) + .invoke('text') + .then(numberOfSignals => { + cy.get(SHOWING_SIGNALS) + .invoke('text') + .should('eql', `Showing ${numberOfSignals} signals`); + + const numberOfSignalsToBeClosed = 3; + selectNumberOfSignals(numberOfSignalsToBeClosed); + + cy.get(SELECTED_SIGNALS) + .invoke('text') + .should('eql', `Selected ${numberOfSignalsToBeClosed} signals`); + + closeSignals(); + waitForSignals(); + cy.reload(); + waitForSignals(); + waitForSignalsToBeLoaded(); + + const expectedNumberOfSignalsAfterClosing = +numberOfSignals - numberOfSignalsToBeClosed; + cy.get(NUMBER_OF_SIGNALS) + .invoke('text') + .should('eq', expectedNumberOfSignalsAfterClosing.toString()); + cy.get(SHOWING_SIGNALS) + .invoke('text') + .should('eql', `Showing ${expectedNumberOfSignalsAfterClosing.toString()} signals`); + + goToClosedSignals(); + waitForSignals(); + + cy.get(NUMBER_OF_SIGNALS) + .invoke('text') + .should('eql', numberOfSignalsToBeClosed.toString()); + cy.get(SHOWING_SIGNALS) + .invoke('text') + .should('eql', `Showing ${numberOfSignalsToBeClosed.toString()} signals`); + cy.get(SIGNALS).should('have.length', numberOfSignalsToBeClosed); + + const numberOfSignalsToBeOpened = 1; + selectNumberOfSignals(numberOfSignalsToBeOpened); + + cy.get(SELECTED_SIGNALS) + .invoke('text') + .should('eql', `Selected ${numberOfSignalsToBeOpened} signal`); + + openSignals(); + waitForSignals(); + cy.reload(); + waitForSignalsToBeLoaded(); + waitForSignals(); + goToClosedSignals(); + waitForSignals(); + + const expectedNumberOfClosedSignalsAfterOpened = 2; + cy.get(NUMBER_OF_SIGNALS) + .invoke('text') + .should('eql', expectedNumberOfClosedSignalsAfterOpened.toString()); + cy.get(SHOWING_SIGNALS) + .invoke('text') + .should('eql', `Showing ${expectedNumberOfClosedSignalsAfterOpened.toString()} signals`); + cy.get(SIGNALS).should('have.length', expectedNumberOfClosedSignalsAfterOpened); + + goToOpenedSignals(); + waitForSignals(); + + const expectedNumberOfOpenedSignals = + +numberOfSignals - expectedNumberOfClosedSignalsAfterOpened; + cy.get(SHOWING_SIGNALS) + .invoke('text') + .should('eql', `Showing ${expectedNumberOfOpenedSignals.toString()} signals`); + + cy.get('[data-test-subj="server-side-event-count"]') + .invoke('text') + .should('eql', expectedNumberOfOpenedSignals.toString()); + }); + }); + + it('Closes one signal when more than one opened signals are selected', () => { + waitForSignalsToBeLoaded(); + + cy.get(NUMBER_OF_SIGNALS) + .invoke('text') + .then(numberOfSignals => { + const numberOfSignalsToBeClosed = 1; + const numberOfSignalsToBeSelected = 3; + + cy.get(OPEN_CLOSE_SIGNALS_BTN).should('have.attr', 'disabled'); + selectNumberOfSignals(numberOfSignalsToBeSelected); + cy.get(OPEN_CLOSE_SIGNALS_BTN).should('not.have.attr', 'disabled'); + + closeFirstSignal(); + cy.reload(); + waitForSignalsToBeLoaded(); + waitForSignals(); + + const expectedNumberOfSignals = +numberOfSignals - numberOfSignalsToBeClosed; + cy.get(NUMBER_OF_SIGNALS) + .invoke('text') + .should('eq', expectedNumberOfSignals.toString()); + cy.get(SHOWING_SIGNALS) + .invoke('text') + .should('eql', `Showing ${expectedNumberOfSignals.toString()} signals`); + + goToClosedSignals(); + waitForSignals(); + + cy.get(NUMBER_OF_SIGNALS) + .invoke('text') + .should('eql', numberOfSignalsToBeClosed.toString()); + cy.get(SHOWING_SIGNALS) + .invoke('text') + .should('eql', `Showing ${numberOfSignalsToBeClosed.toString()} signal`); + cy.get(SIGNALS).should('have.length', numberOfSignalsToBeClosed); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts index 8c384c9010665..ce73fe1b7c2a5 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts @@ -7,30 +7,30 @@ import { newRule } from '../objects/rule'; import { - ABOUT_DESCRIPTION, - ABOUT_EXPECTED_URLS, ABOUT_FALSE_POSITIVES, ABOUT_MITRE, ABOUT_RISK, - ABOUT_RULE_DESCRIPTION, ABOUT_SEVERITY, + ABOUT_STEP, ABOUT_TAGS, ABOUT_TIMELINE, + ABOUT_URLS, DEFINITION_CUSTOM_QUERY, - DEFINITION_DESCRIPTION, DEFINITION_INDEX_PATTERNS, + DEFINITION_STEP, RULE_NAME_HEADER, - SCHEDULE_DESCRIPTION, SCHEDULE_LOOPBACK, SCHEDULE_RUNS, + SCHEDULE_STEP, + ABOUT_RULE_DESCRIPTION, } from '../screens/rule_details'; import { CUSTOM_RULES_BTN, ELASTIC_RULES_BTN, RISK_SCORE, RULE_NAME, - RULES_TABLE, RULES_ROW, + RULES_TABLE, SEVERITY, } from '../screens/signal_detection_rules'; @@ -127,10 +127,25 @@ describe('Signal detection rules', () => { goToRuleDetails(); - cy.get(RULE_NAME_HEADER) - .invoke('text') - .should('eql', `${newRule.name} Beta`); - + let expectedUrls = ''; + newRule.referenceUrls.forEach(url => { + expectedUrls = expectedUrls + url; + }); + let expectedFalsePositives = ''; + newRule.falsePositivesExamples.forEach(falsePositive => { + expectedFalsePositives = expectedFalsePositives + falsePositive; + }); + let expectedTags = ''; + newRule.tags.forEach(tag => { + expectedTags = expectedTags + tag; + }); + let expectedMitre = ''; + newRule.mitre.forEach(mitre => { + expectedMitre = expectedMitre + mitre.tactic; + mitre.techniques.forEach(technique => { + expectedMitre = expectedMitre + technique; + }); + }); const expectedIndexPatterns = [ 'apm-*-transaction*', 'auditbeat-*', @@ -139,77 +154,60 @@ describe('Signal detection rules', () => { 'packetbeat-*', 'winlogbeat-*', ]; - cy.get(DEFINITION_INDEX_PATTERNS).then(patterns => { - cy.wrap(patterns).each((pattern, index) => { - cy.wrap(pattern) - .invoke('text') - .should('eql', expectedIndexPatterns[index]); - }); - }); - cy.get(DEFINITION_DESCRIPTION) - .eq(DEFINITION_CUSTOM_QUERY) + + cy.get(RULE_NAME_HEADER) .invoke('text') - .should('eql', `${newRule.customQuery} `); - cy.get(ABOUT_DESCRIPTION) - .eq(ABOUT_RULE_DESCRIPTION) + .should('eql', `${newRule.name} Beta`); + + cy.get(ABOUT_RULE_DESCRIPTION) .invoke('text') .should('eql', newRule.description); - cy.get(ABOUT_DESCRIPTION) + cy.get(ABOUT_STEP) .eq(ABOUT_SEVERITY) .invoke('text') .should('eql', newRule.severity); - cy.get(ABOUT_DESCRIPTION) + cy.get(ABOUT_STEP) .eq(ABOUT_RISK) .invoke('text') .should('eql', newRule.riskScore); - cy.get(ABOUT_DESCRIPTION) + cy.get(ABOUT_STEP) .eq(ABOUT_TIMELINE) .invoke('text') .should('eql', 'Default blank timeline'); - - let expectedUrls = ''; - newRule.referenceUrls.forEach(url => { - expectedUrls = expectedUrls + url; - }); - cy.get(ABOUT_DESCRIPTION) - .eq(ABOUT_EXPECTED_URLS) + cy.get(ABOUT_STEP) + .eq(ABOUT_URLS) .invoke('text') .should('eql', expectedUrls); - - let expectedFalsePositives = ''; - newRule.falsePositivesExamples.forEach(falsePositive => { - expectedFalsePositives = expectedFalsePositives + falsePositive; - }); - cy.get(ABOUT_DESCRIPTION) + cy.get(ABOUT_STEP) .eq(ABOUT_FALSE_POSITIVES) .invoke('text') .should('eql', expectedFalsePositives); - - let expectedMitre = ''; - newRule.mitre.forEach(mitre => { - expectedMitre = expectedMitre + mitre.tactic; - mitre.techniques.forEach(technique => { - expectedMitre = expectedMitre + technique; - }); - }); - cy.get(ABOUT_DESCRIPTION) + cy.get(ABOUT_STEP) .eq(ABOUT_MITRE) .invoke('text') .should('eql', expectedMitre); - - let expectedTags = ''; - newRule.tags.forEach(tag => { - expectedTags = expectedTags + tag; - }); - cy.get(ABOUT_DESCRIPTION) + cy.get(ABOUT_STEP) .eq(ABOUT_TAGS) .invoke('text') .should('eql', expectedTags); - cy.get(SCHEDULE_DESCRIPTION) + + cy.get(DEFINITION_INDEX_PATTERNS).then(patterns => { + cy.wrap(patterns).each((pattern, index) => { + cy.wrap(pattern) + .invoke('text') + .should('eql', expectedIndexPatterns[index]); + }); + }); + cy.get(DEFINITION_STEP) + .eq(DEFINITION_CUSTOM_QUERY) + .invoke('text') + .should('eql', `${newRule.customQuery} `); + + cy.get(SCHEDULE_STEP) .eq(SCHEDULE_RUNS) .invoke('text') .should('eql', '5m'); - cy.get(SCHEDULE_DESCRIPTION) + cy.get(SCHEDULE_STEP) .eq(SCHEDULE_LOOPBACK) .invoke('text') .should('eql', '1m'); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/timeline_data_providers.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/timeline_data_providers.spec.ts index 4889d40ae7d39..aca988e195161 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/timeline_data_providers.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/timeline_data_providers.spec.ts @@ -49,7 +49,7 @@ describe('timeline data providers', () => { .first() .invoke('text') .should(hostname => { - expect(dataProviderText).to.eq(`host.name: "${hostname}"`); + expect(dataProviderText).to.eq(hostname); }); }); }); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/timeline_flyout_button.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/timeline_flyout_button.spec.ts index 1a94a4abbe5bf..02da7cbc28462 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/timeline_flyout_button.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/timeline_flyout_button.spec.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TIMELINE_FLYOUT_BODY, TIMELINE_NOT_READY_TO_DROP_BUTTON } from '../screens/timeline'; +import { TIMELINE_FLYOUT_HEADER, TIMELINE_NOT_READY_TO_DROP_BUTTON } from '../screens/timeline'; import { dragFirstHostToTimeline, waitForAllHostsToBeLoaded } from '../tasks/hosts/all_hosts'; import { loginAndWaitForPage } from '../tasks/login'; @@ -26,10 +26,11 @@ describe('timeline flyout button', () => { it('toggles open the timeline', () => { openTimeline(); - cy.get(TIMELINE_FLYOUT_BODY).should('have.css', 'visibility', 'visible'); + cy.get(TIMELINE_FLYOUT_HEADER).should('have.css', 'visibility', 'visible'); }); - it('sets the flyout button background to euiColorSuccess with a 10% alpha channel when the user starts dragging a host, but is not hovering over the flyout button', () => { + // FLAKY: https://github.com/elastic/kibana/issues/60369 + it.skip('sets the flyout button background to euiColorSuccess with a 10% alpha channel when the user starts dragging a host, but is not hovering over the flyout button', () => { dragFirstHostToTimeline(); cy.get(TIMELINE_NOT_READY_TO_DROP_BUTTON).should( diff --git a/x-pack/legacy/plugins/siem/cypress/screens/detections.ts b/x-pack/legacy/plugins/siem/cypress/screens/detections.ts index 8089b028a10d4..f388ac1215d01 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/detections.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/detections.ts @@ -4,6 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ +export const CLOSED_SIGNALS_BTN = '[data-test-subj="closedSignals"]'; + export const LOADING_SIGNALS_PANEL = '[data-test-subj="loading-signals-panel"]'; export const MANAGE_SIGNAL_DETECTION_RULES_BTN = '[data-test-subj="manage-signal-detection-rules"]'; + +export const NUMBER_OF_SIGNALS = '[data-test-subj="server-side-event-count"]'; + +export const OPEN_CLOSE_SIGNAL_BTN = '[data-test-subj="update-signal-status-button"]'; + +export const OPEN_CLOSE_SIGNALS_BTN = '[data-test-subj="openCloseSignal"] button'; + +export const OPENED_SIGNALS_BTN = '[data-test-subj="openSignals"]'; + +export const SELECTED_SIGNALS = '[data-test-subj="selectedSignals"]'; + +export const SHOWING_SIGNALS = '[data-test-subj="showingSignals"]'; + +export const SIGNALS = '[data-test-subj="event"]'; + +export const SIGNAL_CHECKBOX = '[data-test-subj="select-event-container"] .euiCheckbox__input'; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts b/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts index 46da52cd0ddd8..6c16735ba5f24 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts @@ -4,35 +4,35 @@ * you may not use this file except in compliance with the Elastic License. */ -export const ABOUT_DESCRIPTION = '[data-test-subj="aboutRule"] .euiDescriptionList__description'; +export const ABOUT_FALSE_POSITIVES = 4; -export const ABOUT_EXPECTED_URLS = 4; +export const ABOUT_MITRE = 5; -export const ABOUT_FALSE_POSITIVES = 5; +export const ABOUT_RULE_DESCRIPTION = '[data-test-subj=stepAboutRuleDetailsToggleDescriptionText]'; -export const ABOUT_MITRE = 6; +export const ABOUT_RISK = 1; -export const ABOUT_RULE_DESCRIPTION = 0; +export const ABOUT_SEVERITY = 0; -export const ABOUT_RISK = 2; +export const ABOUT_STEP = '[data-test-subj="aboutRule"] .euiDescriptionList__description'; -export const ABOUT_SEVERITY = 1; +export const ABOUT_TAGS = 6; -export const ABOUT_TAGS = 7; +export const ABOUT_TIMELINE = 2; -export const ABOUT_TIMELINE = 3; +export const ABOUT_URLS = 3; export const DEFINITION_CUSTOM_QUERY = 1; -export const DEFINITION_DESCRIPTION = - '[data-test-subj="definition"] .euiDescriptionList__description'; - export const DEFINITION_INDEX_PATTERNS = - '[data-test-subj="definition"] .euiDescriptionList__description .euiBadge__text'; + '[data-test-subj=definitionRule] [data-test-subj="listItemColumnStepRuleDescription"] .euiDescriptionList__description .euiBadge__text'; + +export const DEFINITION_STEP = + '[data-test-subj=definitionRule] [data-test-subj="listItemColumnStepRuleDescription"] .euiDescriptionList__description'; export const RULE_NAME_HEADER = '[data-test-subj="header-page-title"]'; -export const SCHEDULE_DESCRIPTION = '[data-test-subj="schedule"] .euiDescriptionList__description'; +export const SCHEDULE_STEP = '[data-test-subj="schedule"] .euiDescriptionList__description'; export const SCHEDULE_RUNS = 0; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/timeline.ts b/x-pack/legacy/plugins/siem/cypress/screens/timeline.ts index 5638b8d23e83a..fbce585a70f86 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/timeline.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/timeline.ts @@ -31,6 +31,8 @@ export const TIMELINE_DROPPED_DATA_PROVIDERS = '[data-test-subj="providerContain export const TIMELINE_FIELDS_BUTTON = '[data-test-subj="timeline"] [data-test-subj="show-field-browser"]'; +export const TIMELINE_FLYOUT_HEADER = '[data-test-subj="eui-flyout-header"]'; + export const TIMELINE_FLYOUT_BODY = '[data-test-subj="eui-flyout-body"]'; export const TIMELINE_INSPECT_BUTTON = '[data-test-subj="inspect-empty-button"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts b/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts index 4a0a565a74e27..3416e3eb81de3 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts @@ -4,7 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LOADING_SIGNALS_PANEL, MANAGE_SIGNAL_DETECTION_RULES_BTN } from '../screens/detections'; +import { + CLOSED_SIGNALS_BTN, + LOADING_SIGNALS_PANEL, + MANAGE_SIGNAL_DETECTION_RULES_BTN, + OPEN_CLOSE_SIGNAL_BTN, + OPEN_CLOSE_SIGNALS_BTN, + OPENED_SIGNALS_BTN, + SIGNALS, + SIGNAL_CHECKBOX, +} from '../screens/detections'; +import { REFRESH_BUTTON } from '../screens/siem_header'; + +export const closeFirstSignal = () => { + cy.get(OPEN_CLOSE_SIGNAL_BTN) + .first() + .click({ force: true }); +}; + +export const closeSignals = () => { + cy.get(OPEN_CLOSE_SIGNALS_BTN).click({ force: true }); +}; + +export const goToClosedSignals = () => { + cy.get(CLOSED_SIGNALS_BTN).click({ force: true }); +}; export const goToManageSignalDetectionRules = () => { cy.get(MANAGE_SIGNAL_DETECTION_RULES_BTN) @@ -12,6 +36,28 @@ export const goToManageSignalDetectionRules = () => { .click({ force: true }); }; +export const goToOpenedSignals = () => { + cy.get(OPENED_SIGNALS_BTN).click({ force: true }); +}; + +export const openSignals = () => { + cy.get(OPEN_CLOSE_SIGNALS_BTN).click({ force: true }); +}; + +export const selectNumberOfSignals = (numberOfSignals: number) => { + for (let i = 0; i < numberOfSignals; i++) { + cy.get(SIGNAL_CHECKBOX) + .eq(i) + .click({ force: true }); + } +}; + +export const waitForSignals = () => { + cy.get(REFRESH_BUTTON) + .invoke('text') + .should('not.equal', 'Updating'); +}; + export const waitForSignalsIndexToBeCreated = () => { cy.request({ url: '/api/detection_engine/index', retryOnStatusCodeFailure: true }).then( response => { @@ -26,3 +72,8 @@ export const waitForSignalsPanelToBeLoaded = () => { cy.get(LOADING_SIGNALS_PANEL).should('exist'); cy.get(LOADING_SIGNALS_PANEL).should('not.exist'); }; + +export const waitForSignalsToBeLoaded = () => { + const expectedNumberOfDisplayedSignals = 25; + cy.get(SIGNALS).should('have.length', expectedNumberOfDisplayedSignals); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/es_archiver.ts b/x-pack/legacy/plugins/siem/cypress/tasks/es_archiver.ts index 72c95cba2361b..1743fcb561064 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/es_archiver.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/es_archiver.ts @@ -12,6 +12,14 @@ export const esArchiverLoadEmptyKibana = () => { ); }; +export const esArchiverLoad = (folder: string) => { + cy.exec( + `node ../../../../scripts/es_archiver load ${folder} --dir ../../../test/siem_cypress/es_archives --config ../../../../test/functional/config.js --es-url ${Cypress.env( + 'ELASTICSEARCH_URL' + )} --kibana-url ${Cypress.config().baseUrl}` + ); +}; + export const esArchiverUnloadEmptyKibana = () => { cy.exec( `node ../../../../scripts/es_archiver empty_kibana unload empty--dir ../../../test/siem_cypress/es_archives --config ../../../../test/functional/config.js --es-url ${Cypress.env( diff --git a/x-pack/legacy/plugins/siem/dev_tools/circular_deps/run_check_circular_deps_cli.js b/x-pack/legacy/plugins/siem/dev_tools/circular_deps/run_check_circular_deps_cli.js index 8ca61b2397d8b..f3a97f5b9c9b6 100644 --- a/x-pack/legacy/plugins/siem/dev_tools/circular_deps/run_check_circular_deps_cli.js +++ b/x-pack/legacy/plugins/siem/dev_tools/circular_deps/run_check_circular_deps_cli.js @@ -17,6 +17,16 @@ run( [resolve(__dirname, '../../public'), resolve(__dirname, '../../common')], { fileExtensions: ['ts', 'js', 'tsx'], + excludeRegExp: [ + 'test.ts$', + 'test.tsx$', + 'containers/detection_engine/rules/types.ts$', + 'core/public/chrome/chrome_service.tsx$', + 'src/core/server/types.ts$', + 'src/core/server/saved_objects/types.ts$', + 'src/core/public/overlays/banners/banners_service.tsx$', + 'src/core/public/saved_objects/saved_objects_client.ts$', + ], } ); diff --git a/x-pack/legacy/plugins/siem/package.json b/x-pack/legacy/plugins/siem/package.json index ad4a6e86ffc88..472a473842f02 100644 --- a/x-pack/legacy/plugins/siem/package.json +++ b/x-pack/legacy/plugins/siem/package.json @@ -14,7 +14,7 @@ "devDependencies": { "@types/lodash": "^4.14.110", "@types/js-yaml": "^3.12.1", - "@types/react-beautiful-dnd": "^11.0.4" + "@types/react-beautiful-dnd": "^12.1.1" }, "dependencies": { "lodash": "^4.17.15", diff --git a/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx b/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx index f3b2b736ed87d..5c15f2d3c8d4f 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx @@ -16,8 +16,8 @@ import { RecursivePartial, } from '@elastic/charts'; import { getOr, get, isNull, isNumber } from 'lodash/fp'; -import useResizeObserver from 'use-resize-observer/polyfilled'; +import { useThrottledResizeObserver } from '../utils'; import { ChartPlaceHolder } from './chart_place_holder'; import { useTimeZone } from '../../lib/kibana'; import { @@ -131,7 +131,7 @@ interface AreaChartComponentProps { } export const AreaChartComponent: React.FC = ({ areaChart, configs }) => { - const { ref: measureRef, width, height } = useResizeObserver({}); + const { ref: measureRef, width, height } = useThrottledResizeObserver(); const customHeight = get('customHeight', configs); const customWidth = get('customWidth', configs); const chartHeight = getChartHeight(customHeight, height); diff --git a/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx b/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx index da0f3d1d0047f..f53a1555fa1f4 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { Chart, BarSeries, Axis, Position, ScaleType, Settings } from '@elastic/charts'; import { getOr, get, isNumber } from 'lodash/fp'; import deepmerge from 'deepmerge'; -import useResizeObserver from 'use-resize-observer/polyfilled'; +import { useThrottledResizeObserver } from '../utils'; import { useTimeZone } from '../../lib/kibana'; import { ChartPlaceHolder } from './chart_place_holder'; import { @@ -105,7 +105,7 @@ interface BarChartComponentProps { } export const BarChartComponent: React.FC = ({ barChart, configs }) => { - const { ref: measureRef, width, height } = useResizeObserver({}); + const { ref: measureRef, width, height } = useThrottledResizeObserver(); const customHeight = get('customHeight', configs); const customWidth = get('customWidth', configs); const chartHeight = getChartHeight(customHeight, height); diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx index 72f5a62d0af97..11db33fff6d72 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { defaultTo, noop } from 'lodash/fp'; +import { noop } from 'lodash/fp'; import React, { useCallback } from 'react'; import { DropResult, DragDropContext } from 'react-beautiful-dnd'; import { connect, ConnectedProps } from 'react-redux'; @@ -103,10 +103,7 @@ DragDropContextWrapperComponent.displayName = 'DragDropContextWrapperComponent'; const emptyDataProviders: dragAndDropModel.IdToDataProvider = {}; // stable reference const mapStateToProps = (state: State) => { - const dataProviders = defaultTo( - emptyDataProviders, - dragAndDropSelectors.dataProvidersSelector(state) - ); + const dataProviders = dragAndDropSelectors.dataProvidersSelector(state) ?? emptyDataProviders; return { dataProviders }; }; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx index 9dcc335d4ff16..11891afabbf3d 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx @@ -88,21 +88,9 @@ describe('DraggableWrapper', () => { describe('ConditionalPortal', () => { const mount = useMountAppended(); const props = { - usePortal: false, registerProvider: jest.fn(), - isDragging: true, }; - it(`doesn't call registerProvider is NOT isDragging`, () => { - mount( - -
- - ); - - expect(props.registerProvider.mock.calls.length).toEqual(0); - }); - it('calls registerProvider when isDragging', () => { mount( diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx index b7d368639ed92..3a6a4de7984db 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { Draggable, DraggableProvided, @@ -15,7 +15,6 @@ import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; -import { EuiPortal } from '@elastic/eui'; import { dragAndDropActions } from '../../store/drag_and_drop'; import { DataProvider } from '../timeline/data_providers/data_provider'; import { TruncatableText } from '../truncatable_text'; @@ -27,9 +26,6 @@ export const DragEffects = styled.div``; DragEffects.displayName = 'DragEffects'; -export const DraggablePortalContext = createContext(false); -export const useDraggablePortalContext = () => useContext(DraggablePortalContext); - /** * Wraps the `react-beautiful-dnd` error boundary. See also: * https://github.com/atlassian/react-beautiful-dnd/blob/v12.0.0/docs/guides/setup-problem-detection-and-error-recovery.md @@ -89,7 +85,6 @@ export const DraggableWrapper = React.memo( ({ dataProvider, render, truncate }) => { const [providerRegistered, setProviderRegistered] = useState(false); const dispatch = useDispatch(); - const usePortal = useDraggablePortalContext(); const registerProvider = useCallback(() => { if (!providerRegistered) { @@ -113,7 +108,26 @@ export const DraggableWrapper = React.memo( return ( - + ( + +
+ + {render(dataProvider, provided, snapshot)} + +
+
+ )} + > {droppableProvided => (
( key={getDraggableId(dataProvider.id)} > {(provided, snapshot) => ( - - - {truncate && !snapshot.isDragging ? ( - - {render(dataProvider, provided, snapshot)} - - ) : ( - - {render(dataProvider, provided, snapshot)} - - )} - - + {truncate && !snapshot.isDragging ? ( + + {render(dataProvider, provided, snapshot)} + + ) : ( + + {render(dataProvider, provided, snapshot)} + + )} + )} {droppableProvided.placeholder} @@ -178,20 +183,16 @@ DraggableWrapper.displayName = 'DraggableWrapper'; interface ConditionalPortalProps { children: React.ReactNode; - usePortal: boolean; - isDragging: boolean; registerProvider: () => void; } export const ConditionalPortal = React.memo( - ({ children, usePortal, registerProvider, isDragging }) => { + ({ children, registerProvider }) => { useEffect(() => { - if (isDragging) { - registerProvider(); - } - }, [isDragging, registerProvider]); + registerProvider(); + }, [registerProvider]); - return usePortal ? {children} : <>{children}; + return <>{children}; } ); diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx index 821ef9be10e8d..a81954f57564e 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx @@ -6,7 +6,7 @@ import { rgba } from 'polished'; import React from 'react'; -import { Droppable } from 'react-beautiful-dnd'; +import { Droppable, DraggableChildrenFn } from 'react-beautiful-dnd'; import styled from 'styled-components'; interface Props { @@ -16,6 +16,7 @@ interface Props { isDropDisabled?: boolean; type?: string; render?: ({ isDraggingOver }: { isDraggingOver: boolean }) => React.ReactNode; + renderClone?: DraggableChildrenFn; } const ReactDndDropTarget = styled.div<{ isDraggingOver: boolean; height: string }>` @@ -94,12 +95,14 @@ export const DroppableWrapper = React.memo( isDropDisabled = false, type, render = null, + renderClone, }) => ( {(provided, snapshot) => ( (({ width }) => { + if (width) { + return { + style: { + width: `${width}px`, + }, + }; + } +})` background-color: ${({ theme }) => theme.eui.euiColorEmptyShade}; border: ${({ theme }) => theme.eui.euiBorderThin}; box-shadow: 0 2px 2px -1px ${({ theme }) => rgba(theme.eui.euiColorMediumShade, 0.3)}, @@ -24,12 +36,9 @@ Field.displayName = 'Field'; * Renders a field (e.g. `event.action`) as a draggable badge */ -// Passing the styles directly to the component because the width is -// being calculated and is recommended by Styled Components for performance -// https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 -export const DraggableFieldBadge = React.memo<{ fieldId: string; fieldWidth?: string }>( +export const DraggableFieldBadge = React.memo<{ fieldId: string; fieldWidth?: number }>( ({ fieldId, fieldWidth }) => ( - + {fieldId} ) diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx b/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx index 57f047416ec1c..1fe6c936d2823 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiBadge, EuiBadgeProps, EuiToolTip, IconType } from '@elastic/eui'; +import { EuiBadge, EuiToolTip, IconType } from '@elastic/eui'; import React from 'react'; +import styled from 'styled-components'; import { Omit } from '../../../common/utility_types'; import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper'; @@ -116,13 +117,9 @@ export const DefaultDraggable = React.memo( DefaultDraggable.displayName = 'DefaultDraggable'; -// Ref: https://github.com/elastic/eui/issues/1655 -// const Badge = styled(EuiBadge)` -// vertical-align: top; -// `; -export const Badge = (props: EuiBadgeProps) => ( - -); +export const Badge = styled(EuiBadge)` + vertical-align: top; +`; Badge.displayName = 'Badge'; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx index 0c93cd51abd79..888df8447a728 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx @@ -9,18 +9,13 @@ import React from 'react'; import { OutPortal, PortalNode } from 'react-reverse-portal'; import minimatch from 'minimatch'; import { ViewMode } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; -import { - IndexPatternMapping, - MapEmbeddable, - RenderTooltipContentParams, - SetQuery, - EmbeddableApi, -} from './types'; +import { IndexPatternMapping, MapEmbeddable, RenderTooltipContentParams, SetQuery } from './types'; import { getLayerList } from './map_config'; // @ts-ignore Missing type defs as maps moves to Typescript import { MAP_SAVED_OBJECT_TYPE } from '../../../../maps/common/constants'; import * as i18n from './translations'; import { Query, Filter } from '../../../../../../../src/plugins/data/public'; +import { EmbeddableStart } from '../../../../../../../src/plugins/embeddable/public'; import { IndexPatternSavedObject } from '../../hooks/types'; /** @@ -45,7 +40,7 @@ export const createEmbeddable = async ( endDate: number, setQuery: SetQuery, portalNode: PortalNode, - embeddableApi: EmbeddableApi + embeddableApi: EmbeddableStart ): Promise => { const factory = embeddableApi.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.tsx index 249aae1eda0eb..15c423a3b3dc1 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.tsx @@ -12,7 +12,6 @@ import { EuiOutsideClickDetector, } from '@elastic/eui'; import { FeatureGeometry, FeatureProperty, MapToolTipProps } from '../types'; -import { DraggablePortalContext } from '../../drag_and_drop/draggable_wrapper'; import { ToolTipFooter } from './tooltip_footer'; import { LineToolTipContent } from './line_tool_tip_content'; import { PointToolTipContent } from './point_tool_tip_content'; @@ -101,46 +100,44 @@ export const MapToolTipComponent = ({ ) : ( - - { - if (closeTooltip != null) { - closeTooltip(); - setFeatureIndex(0); - } - }} - > -
- {featureGeometry != null && featureGeometry.type === 'LineString' ? ( - - ) : ( - - )} - {features.length > 1 && ( - { - setFeatureIndex(featureIndex - 1); - setIsLoadingNextFeature(true); - }} - nextFeature={() => { - setFeatureIndex(featureIndex + 1); - setIsLoadingNextFeature(true); - }} - /> - )} - {isLoadingNextFeature && } -
-
-
+ { + if (closeTooltip != null) { + closeTooltip(); + setFeatureIndex(0); + } + }} + > +
+ {featureGeometry != null && featureGeometry.type === 'LineString' ? ( + + ) : ( + + )} + {features.length > 1 && ( + { + setFeatureIndex(featureIndex - 1); + setIsLoadingNextFeature(true); + }} + nextFeature={() => { + setFeatureIndex(featureIndex + 1); + setIsLoadingNextFeature(true); + }} + /> + )} + {isLoadingNextFeature && } +
+
); }; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts index 812d327ce4488..cc253beb08eae 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts @@ -9,7 +9,6 @@ import { EmbeddableInput, EmbeddableOutput, IEmbeddable, - EmbeddableFactory, } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; import { inputsModel } from '../../store/inputs'; import { Query, Filter } from '../../../../../../../src/plugins/data/public'; @@ -85,8 +84,3 @@ export interface RenderTooltipContentParams { } export type MapToolTipProps = Partial; - -export interface EmbeddableApi { - getEmbeddableFactory: (embeddableFactoryId: string) => EmbeddableFactory; - registerEmbeddableFactory: (id: string, factory: EmbeddableFactory) => void; -} diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx index e9903ce66d799..cd94a9fdcb5ac 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx @@ -115,6 +115,17 @@ export const getColumns = ({ )} isDropDisabled={true} type={DRAG_TYPE_FIELD} + renderClone={provided => ( +
+ + + +
+ )} > - {(provided, snapshot) => ( + {provided => (
- {!snapshot.isDragging ? ( - - ) : ( - - - - )} +
)}
diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/event_details_width_context.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/event_details_width_context.tsx new file mode 100644 index 0000000000000..86a776a0313cc --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/event_details_width_context.tsx @@ -0,0 +1,27 @@ +/* + * 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, { createContext, useContext } from 'react'; +import { useThrottledResizeObserver } from '../utils'; + +const EventDetailsWidthContext = createContext(0); + +export const useEventDetailsWidthContext = () => useContext(EventDetailsWidthContext); + +export const EventDetailsWidthProvider = React.memo(({ children }) => { + const { ref, width } = useThrottledResizeObserver(); + + return ( + <> + + {children} + +
+ + ); +}); + +EventDetailsWidthProvider.displayName = 'EventDetailsWidthProvider'; diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx index a913186d9ad3b..ea2cb661763fa 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx @@ -9,7 +9,6 @@ import { getOr, isEmpty, union } from 'lodash/fp'; import React, { useMemo } from 'react'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; -import useResizeObserver from 'use-resize-observer/polyfilled'; import { BrowserFields } from '../../containers/source'; import { TimelineQuery } from '../../containers/timeline'; @@ -25,8 +24,8 @@ import { OnChangeItemsPerPage } from '../timeline/events'; import { Footer, footerHeight } from '../timeline/footer'; import { combineQueries } from '../timeline/helpers'; import { TimelineRefetch } from '../timeline/refetch_timeline'; -import { isCompactFooter } from '../timeline/timeline'; import { ManageTimelineContext, TimelineTypeContextProps } from '../timeline/timeline_context'; +import { EventDetailsWidthProvider } from './event_details_width_context'; import * as i18n from './translations'; import { Filter, @@ -38,15 +37,15 @@ import { inputsModel } from '../../store'; const DEFAULT_EVENTS_VIEWER_HEIGHT = 500; -const WrappedByAutoSizer = styled.div` - width: 100%; -`; // required by AutoSizer -WrappedByAutoSizer.displayName = 'WrappedByAutoSizer'; - const StyledEuiPanel = styled(EuiPanel)` max-width: 100%; `; +const EventsContainerLoading = styled.div` + width: 100%; + overflow: auto; +`; + interface Props { browserFields: BrowserFields; columns: ColumnHeaderOptions[]; @@ -94,7 +93,6 @@ const EventsViewerComponent: React.FC = ({ toggleColumn, utilityBar, }) => { - const { ref: measureRef, width = 0 } = useResizeObserver({}); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; const kibana = useKibana(); const combinedQueries = combineQueries({ @@ -117,25 +115,25 @@ const EventsViewerComponent: React.FC = ({ ), [columnsHeader, timelineTypeContext.queryFields] ); + const sortField = useMemo( + () => ({ + sortFieldId: sort.columnId, + direction: sort.sortDirection as Direction, + }), + [sort.columnId, sort.sortDirection] + ); return ( - <> - -
- - - {combinedQueries != null ? ( + {combinedQueries != null ? ( + {({ @@ -169,15 +167,8 @@ const EventsViewerComponent: React.FC = ({ {utilityBar?.(refetch, totalCountMinusDeleted)} -
- + + = ({ />
= ({ tieBreaker={getOr(null, 'endCursor.tiebreaker', pageInfo)} /> -
+ ); }}
- ) : null} - +
+ ) : null} ); }; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx index 990c2678b1006..62f9297c38ef5 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx @@ -90,6 +90,13 @@ export const getFieldItems = ({ key={`field-browser-field-items-field-droppable-wrapper-${timelineId}-${categoryId}-${field.name}`} isDropDisabled={true} type={DRAG_TYPE_FIELD} + renderClone={provided => ( +
+ + + +
+ )} > - {(provided, snapshot) => ( -
- {!snapshot.isDragging ? ( - - - - c.id === field.name) !== -1} - data-test-subj={`field-${field.name}-checkbox`} - id={field.name || ''} - onChange={() => - toggleColumn({ - columnHeaderType: defaultColumnHeaderType, - id: field.name || '', - width: DEFAULT_COLUMN_MIN_WIDTH, - }) - } - /> - - - - - - - - + {provided => ( +
+ + + + c.id === field.name) !== -1} + data-test-subj={`field-${field.name}-checkbox`} + id={field.name || ''} + onChange={() => + toggleColumn({ + columnHeaderType: defaultColumnHeaderType, + id: field.name || '', + width: DEFAULT_COLUMN_MIN_WIDTH, + }) + } + /> + + - - + + - - - ) : ( - - - - )} + + + + + + +
)} diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap index abdc4f4681294..4bf0033bcb430 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap @@ -3,7 +3,6 @@ exports[`Flyout rendering it renders correctly against snapshot 1`] = ` ( isDataInTimeline, isDatepickerLocked, title, - width = DEFAULT_TIMELINE_WIDTH, noteIds, notesById, timelineId, @@ -77,7 +75,6 @@ const StatefulFlyoutHeader = React.memo( updateTitle={updateTitle} updateNote={updateNote} usersViewing={usersViewing} - width={width} /> ); } @@ -103,7 +100,6 @@ const makeMapStateToProps = () => { kqlQuery, title = '', noteIds = emptyNotesId, - width = DEFAULT_TIMELINE_WIDTH, } = timeline; const history = emptyHistory; // TODO: get history from store via selector @@ -118,7 +114,6 @@ const makeMapStateToProps = () => { isDatepickerLocked: globalInput.linkTo.includes('timeline'), noteIds, title, - width, }; }; return mapStateToProps; @@ -126,28 +121,6 @@ const makeMapStateToProps = () => { const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({ associateNote: (noteId: string) => dispatch(timelineActions.addNote({ id: timelineId, noteId })), - applyDeltaToWidth: ({ - id, - delta, - bodyClientWidthPixels, - maxWidthPercent, - minWidthPixels, - }: { - id: string; - delta: number; - bodyClientWidthPixels: number; - maxWidthPercent: number; - minWidthPixels: number; - }) => - dispatch( - timelineActions.applyDeltaToWidth({ - id, - delta, - bodyClientWidthPixels, - maxWidthPercent, - minWidthPixels, - }) - ), createTimeline: ({ id, show }: { id: string; show?: boolean }) => dispatch( timelineActions.createTimeline({ diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..df96f2a1f7eba --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FlyoutHeaderWithCloseButton renders correctly against snapshot 1`] = ` + +`; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.test.tsx new file mode 100644 index 0000000000000..e0eace2ad5b10 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.test.tsx @@ -0,0 +1,45 @@ +/* + * 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 { mount, shallow } from 'enzyme'; +import React from 'react'; + +import { TestProviders } from '../../../mock'; +import { FlyoutHeaderWithCloseButton } from '.'; + +describe('FlyoutHeaderWithCloseButton', () => { + test('renders correctly against snapshot', () => { + const EmptyComponent = shallow( + + + + ); + expect(EmptyComponent.find('FlyoutHeaderWithCloseButton')).toMatchSnapshot(); + }); + + test('it should invoke onClose when the close button is clicked', () => { + const closeMock = jest.fn(); + const wrapper = mount( + + + + ); + wrapper + .find('[data-test-subj="close-timeline"] button') + .first() + .simulate('click'); + + expect(closeMock).toBeCalled(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.tsx new file mode 100644 index 0000000000000..a4d9f0e8293df --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.tsx @@ -0,0 +1,49 @@ +/* + * 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 { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import styled from 'styled-components'; + +import { FlyoutHeader } from '../header'; +import * as i18n from './translations'; + +const FlyoutHeaderContainer = styled.div` + align-items: center; + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; +`; + +// manually wrap the close button because EuiButtonIcon can't be a wrapped `styled` +const WrappedCloseButton = styled.div` + margin-right: 5px; +`; + +const FlyoutHeaderWithCloseButtonComponent: React.FC<{ + onClose: () => void; + timelineId: string; + usersViewing: string[]; +}> = ({ onClose, timelineId, usersViewing }) => ( + + + + + + + + +); + +export const FlyoutHeaderWithCloseButton = React.memo(FlyoutHeaderWithCloseButtonComponent); + +FlyoutHeaderWithCloseButton.displayName = 'FlyoutHeaderWithCloseButton'; diff --git a/x-pack/legacy/plugins/graph/public/legacy_imports.ts b/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/translations.ts similarity index 55% rename from x-pack/legacy/plugins/graph/public/legacy_imports.ts rename to x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/translations.ts index 274cdc65232e2..7fcffc9c1f0b4 100644 --- a/x-pack/legacy/plugins/graph/public/legacy_imports.ts +++ b/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/translations.ts @@ -4,6 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import 'ace'; +import { i18n } from '@kbn/i18n'; -export { configureAppAngularModule } from '../../../../../src/plugins/kibana_legacy/public'; +export const CLOSE_TIMELINE = i18n.translate( + 'xpack.siem.timeline.flyout.header.closeTimelineButtonLabel', + { + defaultMessage: 'Close timeline', + } +); diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx index 83b842956e10e..ab41b4617894e 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx @@ -13,9 +13,14 @@ import { apolloClientObservable, mockGlobalState, TestProviders } from '../../mo import { createStore, State } from '../../store'; import { mockDataProviders } from '../timeline/data_providers/mock/mock_data_providers'; -import { Flyout, FlyoutComponent, flyoutHeaderHeight } from '.'; +import { Flyout, FlyoutComponent } from '.'; import { FlyoutButton } from './button'; +jest.mock('../timeline', () => ({ + // eslint-disable-next-line react/display-name + StatefulTimeline: () =>
, +})); + const testFlyoutHeight = 980; const usersViewing = ['elastic']; @@ -26,12 +31,7 @@ describe('Flyout', () => { test('it renders correctly against snapshot', () => { const wrapper = shallow( - + ); expect(wrapper.find('Flyout')).toMatchSnapshot(); @@ -40,12 +40,7 @@ describe('Flyout', () => { test('it renders the default flyout state as a button', () => { const wrapper = mount( - + ); @@ -57,41 +52,13 @@ describe('Flyout', () => { ).toContain('Timeline'); }); - test('it renders the title field when its state is set to flyout is true', () => { - const stateShowIsTrue = set('timeline.timelineById.test.show', true, state); - const storeShowIsTrue = createStore(stateShowIsTrue, apolloClientObservable); - - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="timeline-title"]') - .first() - .props().placeholder - ).toContain('Untitled Timeline'); - }); - test('it does NOT render the fly out button when its state is set to flyout is true', () => { const stateShowIsTrue = set('timeline.timelineById.test.show', true, state); const storeShowIsTrue = createStore(stateShowIsTrue, apolloClientObservable); const wrapper = mount( - + ); @@ -100,31 +67,6 @@ describe('Flyout', () => { ); }); - test('it renders the flyout body', () => { - const stateShowIsTrue = set('timeline.timelineById.test.show', true, state); - const storeShowIsTrue = createStore(stateShowIsTrue, apolloClientObservable); - - const wrapper = mount( - - -

{'Fake flyout body'}

-
-
- ); - - expect( - wrapper - .find('[data-test-subj="eui-flyout-body"]') - .first() - .text() - ).toContain('Fake flyout body'); - }); - test('it does render the data providers badge when the number is greater than 0', () => { const stateWithDataProviders = set( 'timeline.timelineById.test.dataProviders', @@ -135,12 +77,7 @@ describe('Flyout', () => { const wrapper = mount( - + ); @@ -157,12 +94,7 @@ describe('Flyout', () => { const wrapper = mount( - + ); @@ -177,12 +109,7 @@ describe('Flyout', () => { test('it hides the data providers badge when the timeline does NOT have data providers', () => { const wrapper = mount( - + ); @@ -204,12 +131,7 @@ describe('Flyout', () => { const wrapper = mount( - + ); @@ -228,7 +150,6 @@ describe('Flyout', () => { { expect(showTimeline).toBeCalled(); }); - - test('should call the onClose when the close button is clicked', () => { - const stateShowIsTrue = set('timeline.timelineById.test.show', true, state); - const storeShowIsTrue = createStore(stateShowIsTrue, apolloClientObservable); - - const showTimeline = (jest.fn() as unknown) as ActionCreator<{ id: string; show: boolean }>; - const wrapper = mount( - - - - ); - - wrapper - .find('[data-test-subj="close-timeline"] button') - .first() - .simulate('click'); - - expect(showTimeline).toBeCalled(); - }); }); describe('showFlyoutButton', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx index 22fc9f27ce26c..44abe5b679c8e 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx @@ -5,7 +5,6 @@ */ import { EuiBadge } from '@elastic/eui'; -import { defaultTo, getOr } from 'lodash/fp'; import React, { useCallback } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import styled from 'styled-components'; @@ -16,9 +15,8 @@ import { FlyoutButton } from './button'; import { Pane } from './pane'; import { timelineActions } from '../../store/actions'; import { DEFAULT_TIMELINE_WIDTH } from '../timeline/body/constants'; - -/** The height in pixels of the flyout header, exported for use in height calculations */ -export const flyoutHeaderHeight: number = 60; +import { StatefulTimeline } from '../timeline'; +import { TimelineById } from '../../store/timeline/types'; export const Badge = styled(EuiBadge)` position: absolute; @@ -38,9 +36,7 @@ const Visible = styled.div<{ show?: boolean }>` Visible.displayName = 'Visible'; interface OwnProps { - children?: React.ReactNode; flyoutHeight: number; - headerHeight: number; timelineId: string; usersViewing: string[]; } @@ -48,17 +44,7 @@ interface OwnProps { type Props = OwnProps & ProsFromRedux; export const FlyoutComponent = React.memo( - ({ - children, - dataProviders, - flyoutHeight, - headerHeight, - show, - showTimeline, - timelineId, - usersViewing, - width, - }) => { + ({ dataProviders, flyoutHeight, show, showTimeline, timelineId, usersViewing, width }) => { const handleClose = useCallback(() => showTimeline({ id: timelineId, show: false }), [ showTimeline, timelineId, @@ -73,17 +59,15 @@ export const FlyoutComponent = React.memo( - {children} + ( FlyoutComponent.displayName = 'FlyoutComponent'; +const DEFAULT_DATA_PROVIDERS: DataProvider[] = []; +const DEFAULT_TIMELINE_BY_ID = {}; + const mapStateToProps = (state: State, { timelineId }: OwnProps) => { - const timelineById = defaultTo({}, timelineSelectors.timelineByIdSelector(state)); - const dataProviders = getOr([], `${timelineId}.dataProviders`, timelineById) as DataProvider[]; - const show = getOr(false, `${timelineId}.show`, timelineById) as boolean; - const width = getOr(DEFAULT_TIMELINE_WIDTH, `${timelineId}.width`, timelineById) as number; + const timelineById: TimelineById = + timelineSelectors.timelineByIdSelector(state) ?? DEFAULT_TIMELINE_BY_ID; + /* + In case timelineById[timelineId]?.dataProviders is an empty array it will cause unnecessary rerender + of StatefulTimeline which can be expensive, so to avoid that return DEFAULT_DATA_PROVIDERS + */ + const dataProviders = timelineById[timelineId]?.dataProviders.length + ? timelineById[timelineId]?.dataProviders + : DEFAULT_DATA_PROVIDERS; + const show = timelineById[timelineId]?.show ?? false; + const width = timelineById[timelineId]?.width ?? DEFAULT_TIMELINE_WIDTH; return { dataProviders, show, width }; }; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap index efa682cd4d18e..d30fd6f31012c 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap @@ -3,14 +3,8 @@ exports[`Pane renders correctly against snapshot 1`] = ` diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx index 365f99c6667b8..53cf8f95de0ce 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx @@ -8,12 +8,10 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; import { TestProviders } from '../../../mock'; -import { flyoutHeaderHeight } from '..'; import { Pane } from '.'; const testFlyoutHeight = 980; const testWidth = 640; -const usersViewing = ['elastic']; describe('Pane', () => { test('renders correctly against snapshot', () => { @@ -21,10 +19,8 @@ describe('Pane', () => { {'I am a child of flyout'} @@ -39,10 +35,8 @@ describe('Pane', () => { {'I am a child of flyout'} @@ -53,87 +47,13 @@ describe('Pane', () => { expect(wrapper.find('Resizable').get(0).props.maxWidth).toEqual('95vw'); }); - test('it applies timeline styles to the EuiFlyout', () => { - const wrapper = mount( - - - {'I am a child of flyout'} - - - ); - - expect( - wrapper - .find('[data-test-subj="eui-flyout"]') - .first() - .hasClass('timeline-flyout') - ).toEqual(true); - }); - - test('it applies timeline styles to the EuiFlyoutHeader', () => { - const wrapper = mount( - - - {'I am a child of flyout'} - - - ); - - expect( - wrapper - .find('[data-test-subj="eui-flyout-header"]') - .first() - .hasClass('timeline-flyout-header') - ).toEqual(true); - }); - - test('it applies timeline styles to the EuiFlyoutBody', () => { - const wrapper = mount( - - - {'I am a child of flyout'} - - - ); - - expect( - wrapper - .find('[data-test-subj="eui-flyout-body"]') - .first() - .hasClass('timeline-flyout-body') - ).toEqual(true); - }); - test('it should render a resize handle', () => { const wrapper = mount( {'I am a child of flyout'} @@ -149,74 +69,19 @@ describe('Pane', () => { ).toEqual(true); }); - test('it should render an empty title', () => { + test('it should render children', () => { const wrapper = mount( - {'I am a child of flyout'} - - - ); - - expect( - wrapper - .find('[data-test-subj="timeline-title"]') - .first() - .text() - ).toContain(''); - }); - - test('it should render the flyout body', () => { - const wrapper = mount( - - {'I am a mock body'} ); - expect( - wrapper - .find('[data-test-subj="eui-flyout-body"]') - .first() - .text() - ).toContain('I am a mock body'); - }); - - test('it should invoke onClose when the close button is clicked', () => { - const closeMock = jest.fn(); - const wrapper = mount( - - - {'I am a mock child'} - - - ); - wrapper - .find('[data-test-subj="close-timeline"] button') - .first() - .simulate('click'); - - expect(closeMock).toBeCalled(); + expect(wrapper.first().text()).toContain('I am a mock body'); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx index 38ec4a4b6f1f3..3b5041c1ee346 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx @@ -4,130 +4,85 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonIcon, EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiToolTip } from '@elastic/eui'; -import React, { useCallback, useState } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { EuiFlyout } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import { Resizable, ResizeCallback } from 're-resizable'; -import { throttle } from 'lodash/fp'; import { TimelineResizeHandle } from './timeline_resize_handle'; -import { FlyoutHeader } from '../header'; +import { EventDetailsWidthProvider } from '../../events_viewer/event_details_width_context'; import * as i18n from './translations'; import { timelineActions } from '../../../store/actions'; const minWidthPixels = 550; // do not allow the flyout to shrink below this width (pixels) const maxWidthPercent = 95; // do not allow the flyout to grow past this percentage of the view -interface OwnProps { +interface FlyoutPaneComponentProps { children: React.ReactNode; flyoutHeight: number; - headerHeight: number; onClose: () => void; timelineId: string; - usersViewing: string[]; width: number; } -type Props = OwnProps & PropsFromRedux; - -const EuiFlyoutContainer = styled.div<{ headerHeight: number }>` +const EuiFlyoutContainer = styled.div` .timeline-flyout { min-width: 150px; width: auto; } - .timeline-flyout-header { - align-items: center; - box-shadow: none; - display: flex; - flex-direction: row; - height: ${({ headerHeight }) => `${headerHeight}px`}; - max-height: ${({ headerHeight }) => `${headerHeight}px`}; - overflow: hidden; - padding: 5px 0 0 10px; - } - .timeline-flyout-body { - overflow-y: hidden; - padding: 0; - .euiFlyoutBody__overflowContent { - padding: 0; - } - } `; -const FlyoutHeaderContainer = styled.div` - align-items: center; +const StyledResizable = styled(Resizable)` display: flex; - flex-direction: row; - justify-content: space-between; - width: 100%; -`; - -// manually wrap the close button because EuiButtonIcon can't be a wrapped `styled` -const WrappedCloseButton = styled.div` - margin-right: 5px; + flex-direction: column; `; -const FlyoutHeaderWithCloseButtonComponent: React.FC<{ - onClose: () => void; - timelineId: string; - usersViewing: string[]; -}> = ({ onClose, timelineId, usersViewing }) => ( - - - - - - - - -); - -const FlyoutHeaderWithCloseButton = React.memo( - FlyoutHeaderWithCloseButtonComponent, - (prevProps, nextProps) => - prevProps.timelineId === nextProps.timelineId && - prevProps.usersViewing === nextProps.usersViewing -); +const RESIZABLE_ENABLE = { left: true }; -const FlyoutPaneComponent: React.FC = ({ - applyDeltaToWidth, +const FlyoutPaneComponent: React.FC = ({ children, flyoutHeight, - headerHeight, onClose, timelineId, - usersViewing, width, }) => { - const [lastDelta, setLastDelta] = useState(0); + const dispatch = useDispatch(); + const onResizeStop: ResizeCallback = useCallback( (e, direction, ref, delta) => { const bodyClientWidthPixels = document.body.clientWidth; if (delta.width) { - applyDeltaToWidth({ - bodyClientWidthPixels, - delta: -(delta.width - lastDelta), - id: timelineId, - maxWidthPercent, - minWidthPixels, - }); - setLastDelta(delta.width); + dispatch( + timelineActions.applyDeltaToWidth({ + bodyClientWidthPixels, + delta: -delta.width, + id: timelineId, + maxWidthPercent, + minWidthPixels, + }) + ); } }, - [applyDeltaToWidth, maxWidthPercent, minWidthPixels, lastDelta] + [dispatch] + ); + const resizableDefaultSize = useMemo( + () => ({ + width, + height: '100%', + }), + [] + ); + const resizableHandleComponent = useMemo( + () => ({ + left: , + }), + [flyoutHeight] ); - const resetLastDelta = useCallback(() => setLastDelta(0), [setLastDelta]); - const throttledResize = throttle(100, onResizeStop); return ( - + = ({ onClose={onClose} size="l" > - - ), - }} - onResizeStart={resetLastDelta} - onResize={throttledResize} + handleComponent={resizableHandleComponent} + onResizeStop={onResizeStop} > - - - - - {children} - - + {children} + ); }; -const mapDispatchToProps = { - applyDeltaToWidth: timelineActions.applyDeltaToWidth, -}; - -const connector = connect(null, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const Pane = connector(React.memo(FlyoutPaneComponent)); +export const Pane = React.memo(FlyoutPaneComponent); Pane.displayName = 'Pane'; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/translations.ts b/x-pack/legacy/plugins/siem/public/components/flyout/pane/translations.ts index 4ba0307eb527b..0c31cdb81e8e1 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/translations.ts @@ -12,10 +12,3 @@ export const TIMELINE_DESCRIPTION = i18n.translate( defaultMessage: 'Timeline Properties', } ); - -export const CLOSE_TIMELINE = i18n.translate( - 'xpack.siem.timeline.flyout.pane.closeTimelineButtonLabel', - { - defaultMessage: 'Close timeline', - } -); diff --git a/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx b/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx index d6f8143745356..f10a740db2b93 100644 --- a/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx @@ -7,11 +7,10 @@ import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui'; import { getOr, omit } from 'lodash/fp'; import React, { useCallback } from 'react'; -import { connect } from 'react-redux'; -import { ActionCreator } from 'typescript-fsa'; +import { connect, ConnectedProps } from 'react-redux'; import styled, { css } from 'styled-components'; -import { inputsModel, inputsSelectors, State } from '../../store'; +import { inputsSelectors, State } from '../../store'; import { InputsModelId } from '../../store/inputs/constants'; import { inputsActions } from '../../store/inputs'; @@ -60,24 +59,7 @@ interface OwnProps { title: string | React.ReactElement | React.ReactNode; } -interface InspectButtonReducer { - id: string; - isInspected: boolean; - loading: boolean; - inspect: inputsModel.InspectQuery | null; - selectedInspectIndex: number; -} - -interface InspectButtonDispatch { - setIsInspected: ActionCreator<{ - id: string; - inputId: InputsModelId; - isInspected: boolean; - selectedInspectIndex: number; - }>; -} - -type InspectButtonProps = OwnProps & InspectButtonReducer & InspectButtonDispatch; +type InspectButtonProps = OwnProps & PropsFromRedux; const InspectButtonComponent: React.FC = ({ compact = false, @@ -175,7 +157,8 @@ const mapDispatchToProps = { setIsInspected: inputsActions.setInspectionParameter, }; -export const InspectButton = connect( - makeMapStateToProps, - mapDispatchToProps -)(React.memo(InspectButtonComponent)); +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const InspectButton = connector(React.memo(InspectButtonComponent)); diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/link_to/helpers.test.ts new file mode 100644 index 0000000000000..14b367de674a2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/link_to/helpers.test.ts @@ -0,0 +1,19 @@ +/* + * 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 { appendSearch } from './helpers'; + +describe('appendSearch', () => { + test('should return empty string if no parameter', () => { + expect(appendSearch()).toEqual(''); + }); + test('should return empty string if parameter is undefined', () => { + expect(appendSearch(undefined)).toEqual(''); + }); + test('should return parameter if parameter is defined', () => { + expect(appendSearch('helloWorld')).toEqual('helloWorld'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/helpers.ts b/x-pack/legacy/plugins/siem/public/components/link_to/helpers.ts new file mode 100644 index 0000000000000..9d818ab3b6479 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/link_to/helpers.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export const appendSearch = (search?: string) => (search != null ? `${search}` : ''); diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_detection_engine.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_detection_engine.tsx index 3701069389b72..18111aa93a27a 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_detection_engine.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { DetectionEngineTab } from '../../pages/detection_engine/types'; +import { appendSearch } from './helpers'; import { RedirectWrapper } from './redirect_wrapper'; export type DetectionEngineComponentProps = RouteComponentProps<{ @@ -63,9 +64,10 @@ export const RedirectToEditRulePage = ({ const baseDetectionEngineUrl = `#/link-to/${DETECTION_ENGINE_PAGE_NAME}`; -export const getDetectionEngineUrl = () => `${baseDetectionEngineUrl}`; -export const getDetectionEngineAlertUrl = () => - `${baseDetectionEngineUrl}/${DetectionEngineTab.alerts}`; +export const getDetectionEngineUrl = (search?: string) => + `${baseDetectionEngineUrl}${appendSearch(search)}`; +export const getDetectionEngineAlertUrl = (search?: string) => + `${baseDetectionEngineUrl}/${DetectionEngineTab.alerts}${appendSearch(search)}`; export const getDetectionEngineTabUrl = (tabPath: string) => `${baseDetectionEngineUrl}/${tabPath}`; export const getRulesUrl = () => `${baseDetectionEngineUrl}/rules`; export const getCreateRuleUrl = () => `${baseDetectionEngineUrl}/rules/create`; diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_hosts.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_hosts.tsx index 05139320b171d..746a959cc996a 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_hosts.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_hosts.tsx @@ -7,10 +7,12 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { RedirectWrapper } from './redirect_wrapper'; import { HostsTableType } from '../../store/hosts/model'; import { SiemPageName } from '../../pages/home/types'; +import { appendSearch } from './helpers'; +import { RedirectWrapper } from './redirect_wrapper'; + export type HostComponentProps = RouteComponentProps<{ detailName: string; tabName: HostsTableType; @@ -44,9 +46,10 @@ export const RedirectToHostDetailsPage = ({ const baseHostsUrl = `#/link-to/${SiemPageName.hosts}`; -export const getHostsUrl = () => baseHostsUrl; +export const getHostsUrl = (search?: string) => `${baseHostsUrl}${appendSearch(search)}`; -export const getTabsOnHostsUrl = (tabName: HostsTableType) => `${baseHostsUrl}/${tabName}`; +export const getTabsOnHostsUrl = (tabName: HostsTableType, search?: string) => + `${baseHostsUrl}/${tabName}${appendSearch(search)}`; export const getHostDetailsUrl = (detailName: string) => `${baseHostsUrl}/${detailName}`; diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_network.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_network.tsx index f206e2f323a74..71925edd5c086 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_network.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_network.tsx @@ -7,10 +7,12 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { RedirectWrapper } from './redirect_wrapper'; import { SiemPageName } from '../../pages/home/types'; import { FlowTarget, FlowTargetSourceDest } from '../../graphql/types'; +import { appendSearch } from './helpers'; +import { RedirectWrapper } from './redirect_wrapper'; + export type NetworkComponentProps = RouteComponentProps<{ detailName?: string; flowTarget?: string; @@ -33,7 +35,7 @@ export const RedirectToNetworkPage = ({ ); const baseNetworkUrl = `#/link-to/${SiemPageName.network}`; -export const getNetworkUrl = () => baseNetworkUrl; +export const getNetworkUrl = (search?: string) => `${baseNetworkUrl}${appendSearch(search)}`; export const getIPDetailsUrl = ( detailName: string, flowTarget?: FlowTarget | FlowTargetSourceDest diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_timelines.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_timelines.tsx index 1b71432b3f729..27765a4125afc 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_timelines.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_timelines.tsx @@ -6,9 +6,12 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { RedirectWrapper } from './redirect_wrapper'; + import { SiemPageName } from '../../pages/home/types'; +import { appendSearch } from './helpers'; +import { RedirectWrapper } from './redirect_wrapper'; + export type TimelineComponentProps = RouteComponentProps<{ search: string; }>; @@ -17,4 +20,5 @@ export const RedirectToTimelinesPage = ({ location: { search } }: TimelineCompon ); -export const getTimelinesUrl = () => `#/link-to/${SiemPageName.timelines}`; +export const getTimelinesUrl = (search?: string) => + `#/link-to/${SiemPageName.timelines}${appendSearch(search)}`; diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts b/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts index 9a95d93a2df70..899d108fe246d 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts @@ -10,7 +10,7 @@ import { Location } from 'history'; import { UrlInputsModel } from '../../store/inputs/model'; import { TimelineUrl } from '../../store/timeline/model'; import { CONSTANTS } from '../url_state/constants'; -import { URL_STATE_KEYS, KeyUrlState } from '../url_state/types'; +import { URL_STATE_KEYS, KeyUrlState, UrlState } from '../url_state/types'; import { replaceQueryStringInLocation, replaceStateKeyInQueryString, @@ -18,10 +18,9 @@ import { } from '../url_state/helpers'; import { Query, Filter } from '../../../../../../../src/plugins/data/public'; -import { TabNavigationProps } from './tab_navigation/types'; import { SearchNavTab } from './types'; -export const getSearch = (tab: SearchNavTab, urlState: TabNavigationProps): string => { +export const getSearch = (tab: SearchNavTab, urlState: UrlState): string => { if (tab && tab.urlKey != null && URL_STATE_KEYS[tab.urlKey] != null) { return URL_STATE_KEYS[tab.urlKey].reduce( (myLocation: Location, urlKey: KeyUrlState) => { @@ -58,7 +57,7 @@ export const getSearch = (tab: SearchNavTab, urlState: TabNavigationProps): stri ); }, { - pathname: urlState.pathName, + pathname: '', hash: '', search: '', state: '', diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx index cebf9b90656ca..ab4d75a2b1168 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx @@ -66,7 +66,9 @@ export const TabNavigationComponent = (props: TabNavigationProps) => { () => Object.values(navTabs).map(tab => { const isSelected = selectedTabId === tab.id; - const hrefWithSearch = tab.href + getSearch(tab, props); + const { query, filters, savedQuery, timerange, timeline } = props; + const hrefWithSearch = + tab.href + getSearch(tab, { query, filters, savedQuery, timerange, timeline }); return ( { + const mapState = makeMapStateToProps(); + const { urlState } = useSelector(mapState, isEqual); + const urlSearch = useMemo(() => getSearch(tab, urlState), [tab, urlState]); + return urlSearch; +}; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.tsx index caa9cd0689c76..982937659c0aa 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.tsx @@ -5,7 +5,7 @@ */ import { EuiButtonIcon, EuiModal, EuiToolTip, EuiOverlayMask } from '@elastic/eui'; -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { DeleteTimelineModal, DELETE_TIMELINE_MODAL_WIDTH } from './delete_timeline_modal'; import * as i18n from '../translations'; @@ -23,15 +23,15 @@ export const DeleteTimelineModalButton = React.memo( ({ deleteTimelines, savedObjectId, title }) => { const [showModal, setShowModal] = useState(false); - const openModal = () => setShowModal(true); - const closeModal = () => setShowModal(false); + const openModal = useCallback(() => setShowModal(true), [setShowModal]); + const closeModal = useCallback(() => setShowModal(false), [setShowModal]); - const onDelete = () => { + const onDelete = useCallback(() => { if (deleteTimelines != null && savedObjectId != null) { deleteTimelines([savedObjectId]); } closeModal(); - }; + }, [deleteTimelines, savedObjectId, closeModal]); return ( <> diff --git a/x-pack/legacy/plugins/siem/public/components/page/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/index.tsx index 781155c3ddc38..ef6a19f4b7448 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/index.tsx @@ -4,15 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { - EuiBadge, - EuiBadgeProps, - EuiDescriptionList, - EuiFlexGroup, - EuiIcon, - EuiPage, -} from '@elastic/eui'; +import { EuiBadge, EuiDescriptionList, EuiFlexGroup, EuiIcon, EuiPage } from '@elastic/eui'; import styled, { createGlobalStyle } from 'styled-components'; /* @@ -20,6 +12,12 @@ import styled, { createGlobalStyle } from 'styled-components'; and `EuiPopover`, `EuiToolTip` global styles */ export const AppGlobalStyle = createGlobalStyle` + /* dirty hack to fix draggables with tooltip on FF */ + body#siem-app { + position: static; + } + /* end of dirty hack to fix draggables with tooltip on FF */ + div.app-wrapper { background-color: rgba(0,0,0,0); } @@ -107,6 +105,7 @@ export const PageHeader = styled.div` PageHeader.displayName = 'PageHeader'; export const FooterContainer = styled.div` + flex: 0; bottom: 0; color: #666; left: 0; @@ -154,13 +153,9 @@ export const Pane1FlexContent = styled.div` Pane1FlexContent.displayName = 'Pane1FlexContent'; -// Ref: https://github.com/elastic/eui/issues/1655 -// const Badge = styled(EuiBadge)` -// margin-left: 5px; -// `; -export const CountBadge = (props: EuiBadgeProps) => ( - -); +export const CountBadge = styled(EuiBadge)` + margin-left: 5px; +`; CountBadge.displayName = 'CountBadge'; @@ -170,13 +165,9 @@ export const Spacer = styled.span` Spacer.displayName = 'Spacer'; -// Ref: https://github.com/elastic/eui/issues/1655 -// export const Badge = styled(EuiBadge)` -// vertical-align: top; -// `; -export const Badge = (props: EuiBadgeProps) => ( - -); +export const Badge = styled(EuiBadge)` + vertical-align: top; +`; Badge.displayName = 'Badge'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx index 3868885fa29ee..52c142ceff480 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx @@ -8,7 +8,7 @@ import { isEmpty } from 'lodash/fp'; import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; +import React, { useMemo } from 'react'; import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; import { ESQuery } from '../../../../../common/typed_json'; @@ -23,6 +23,8 @@ import { getOverviewHostStats, OverviewHostStats } from '../overview_host_stats' import { manageQuery } from '../../../page/manage_query'; import { inputsModel } from '../../../../store/inputs'; import { InspectButtonContainer } from '../../../inspect'; +import { useGetUrlSearch } from '../../../navigation/use_get_url_search'; +import { navTabs } from '../../../../pages/home/home_navigations'; export interface OwnProps { startDate: number; @@ -51,7 +53,15 @@ const OverviewHostComponent: React.FC = ({ setQuery, }) => { const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - + const urlSearch = useGetUrlSearch(navTabs.hosts); + const hostPageButton = useMemo( + () => ( + + + + ), + [urlSearch] + ); return ( @@ -95,12 +105,7 @@ const OverviewHostComponent: React.FC = ({ /> } > - - - + {hostPageButton} = ({ setQuery, }) => { const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - + const urlSearch = useGetUrlSearch(navTabs.network); + const networkPageButton = useMemo( + () => ( + + + + ), + [urlSearch] + ); return ( @@ -96,12 +106,7 @@ const OverviewNetworkComponent: React.FC = ({ /> } > - - - + {networkPageButton} ; @@ -31,14 +33,13 @@ export type Props = OwnProps & PropsFromRedux; const StatefulRecentTimelinesComponent = React.memo( ({ apolloClient, filterBy, updateIsLoading, updateTimeline }) => { - const actionDispatcher = updateIsLoading as ActionCreator<{ id: string; isLoading: boolean }>; const onOpenTimeline: OnOpenTimeline = useCallback( ({ duplicate, timelineId }: { duplicate: boolean; timelineId: string }) => { queryTimelineById({ apolloClient, duplicate, timelineId, - updateIsLoading: actionDispatcher, + updateIsLoading, updateTimeline, }); }, @@ -47,6 +48,11 @@ const StatefulRecentTimelinesComponent = React.memo( const noTimelinesMessage = filterBy === 'favorites' ? i18n.NO_FAVORITE_TIMELINES : i18n.NO_TIMELINES; + const urlSearch = useGetUrlSearch(navTabs.timelines); + const linkAllTimelines = useMemo( + () => {i18n.VIEW_ALL_TIMELINES}, + [urlSearch] + ); return ( ( /> )} - - {i18n.VIEW_ALL_TIMELINES} - + {linkAllTimelines} )} diff --git a/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.test.tsx index 1fdcd8eee941f..0ee54a1a20003 100644 --- a/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.test.tsx @@ -26,16 +26,10 @@ describe('SkeletonRow', () => { expect(wrapper.find('.siemSkeletonRow__cell')).toHaveLength(10); }); - test('it applies row and cell styles when cellColor/cellMargin/rowHeight/rowPadding/style provided', () => { + test('it applies row and cell styles when cellColor/cellMargin/rowHeight/rowPadding provided', () => { const wrapper = mount( - + ); const siemSkeletonRow = wrapper.find('.siemSkeletonRow').first(); @@ -43,7 +37,6 @@ describe('SkeletonRow', () => { expect(siemSkeletonRow).toHaveStyleRule('height', '100px'); expect(siemSkeletonRow).toHaveStyleRule('padding', '10px'); - expect(siemSkeletonRow.props().style!.width).toBe('auto'); expect(siemSkeletonRowCell).toHaveStyleRule('background-color', 'red'); expect(siemSkeletonRowCell).toHaveStyleRule('margin-left', '10px', { modifier: '& + &', diff --git a/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.tsx b/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.tsx index dce360877130e..ae30f11d8bb16 100644 --- a/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.tsx @@ -54,11 +54,10 @@ Cell.displayName = 'Cell'; export interface SkeletonRowProps extends CellProps, RowProps { cellCount?: number; - style?: object; } export const SkeletonRow = React.memo( - ({ cellColor, cellCount = 4, cellMargin, rowHeight, rowPadding, style }) => { + ({ cellColor, cellCount = 4, cellMargin, rowHeight, rowPadding }) => { const cells = useMemo( () => [...Array(cellCount)].map( @@ -69,7 +68,7 @@ export const SkeletonRow = React.memo( ); return ( - + {cells} ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap index 372930ee3167d..02938cb2b86b9 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap @@ -1,668 +1,702 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Timeline rendering renders correctly against snapshot 1`] = ` - - - + + + - + onChangeDataProviderKqlQuery={[MockFunction]} + onChangeDroppableAndProvider={[MockFunction]} + onDataProviderEdited={[MockFunction]} + onDataProviderRemoved={[MockFunction]} + onToggleDataProviderEnabled={[MockFunction]} + onToggleDataProviderExcluded={[MockFunction]} + show={true} + showCallOutUnauthorizedMsg={false} + /> + + - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap index b8b03be4e4720..03e4f4b5f0f2b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap @@ -490,7 +490,7 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` isCombineEnabled={false} isDropDisabled={false} mode="standard" - renderClone={null} + renderClone={[Function]} type="drag-type-field" > diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx index c3f28fd513d08..e070ed8fa1d2a 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx @@ -4,27 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import { Draggable } from 'react-beautiful-dnd'; import { Resizable, ResizeCallback } from 're-resizable'; +import deepEqual from 'fast-deep-equal'; import { ColumnHeaderOptions } from '../../../../store/timeline/model'; -import { DragEffects } from '../../../drag_and_drop/draggable_wrapper'; -import { getDraggableFieldId, DRAG_TYPE_FIELD } from '../../../drag_and_drop/helpers'; -import { DraggableFieldBadge } from '../../../draggables/field_badge'; +import { getDraggableFieldId } from '../../../drag_and_drop/helpers'; import { OnColumnRemoved, OnColumnSorted, OnFilterChange, OnColumnResized } from '../../events'; import { EventsTh, EventsThContent, EventsHeadingHandle } from '../../styles'; import { Sort } from '../sort'; -import { DraggingContainer } from './common/dragging_container'; import { Header } from './header'; +const RESIZABLE_ENABLE = { right: true }; + interface ColumneHeaderProps { draggableIndex: number; header: ColumnHeaderOptions; onColumnRemoved: OnColumnRemoved; onColumnSorted: OnColumnSorted; onColumnResized: OnColumnResized; + isDragging: boolean; onFilterChange?: OnFilterChange; sort: Sort; timelineId: string; @@ -34,69 +35,82 @@ const ColumnHeaderComponent: React.FC = ({ draggableIndex, header, timelineId, + isDragging, onColumnRemoved, onColumnResized, onColumnSorted, onFilterChange, sort, }) => { - const [isDragging, setIsDragging] = React.useState(false); - const handleResizeStop: ResizeCallback = (e, direction, ref, delta) => { - onColumnResized({ columnId: header.id, delta: delta.width }); - }; + const resizableSize = useMemo( + () => ({ + width: header.width, + height: 'auto', + }), + [header.width] + ); + const resizableStyle: { + position: 'absolute' | 'relative'; + } = useMemo( + () => ({ + position: isDragging ? 'absolute' : 'relative', + }), + [isDragging] + ); + const resizableHandleComponent = useMemo( + () => ({ + right: , + }), + [] + ); + const handleResizeStop: ResizeCallback = useCallback( + (e, direction, ref, delta) => { + onColumnResized({ columnId: header.id, delta: delta.width }); + }, + [header.id, onColumnResized] + ); + const draggableId = useMemo( + () => + getDraggableFieldId({ + contextId: `timeline-column-headers-${timelineId}`, + fieldId: header.id, + }), + [timelineId, header.id] + ); return ( , - }} + enable={RESIZABLE_ENABLE} + size={resizableSize} + style={resizableStyle} + handleComponent={resizableHandleComponent} onResizeStop={handleResizeStop} > - {(dragProvided, dragSnapshot) => ( + {dragProvided => ( - {!dragSnapshot.isDragging ? ( - -
- - ) : ( - - - - - - )} + +
+ )} @@ -104,4 +118,16 @@ const ColumnHeaderComponent: React.FC = ({ ); }; -export const ColumnHeader = React.memo(ColumnHeaderComponent); +export const ColumnHeader = React.memo( + ColumnHeaderComponent, + (prevProps, nextProps) => + prevProps.draggableIndex === nextProps.draggableIndex && + prevProps.timelineId === nextProps.timelineId && + prevProps.isDragging === nextProps.isDragging && + prevProps.onColumnRemoved === nextProps.onColumnRemoved && + prevProps.onColumnResized === nextProps.onColumnResized && + prevProps.onColumnSorted === nextProps.onColumnSorted && + prevProps.onFilterChange === nextProps.onFilterChange && + prevProps.sort === nextProps.sort && + deepEqual(prevProps.header, nextProps.header) +); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx index ab8dc629dd577..7a072f1dbf578 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx @@ -6,9 +6,12 @@ import { EuiCheckbox } from '@elastic/eui'; import { noop } from 'lodash/fp'; -import React from 'react'; -import { Droppable } from 'react-beautiful-dnd'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import { Droppable, DraggableChildrenFn } from 'react-beautiful-dnd'; +import deepEqual from 'fast-deep-equal'; +import { DragEffects } from '../../../drag_and_drop/draggable_wrapper'; +import { DraggableFieldBadge } from '../../../draggables/field_badge'; import { BrowserFields } from '../../../../containers/source'; import { ColumnHeaderOptions } from '../../../../store/timeline/model'; import { DRAG_TYPE_FIELD, droppableTimelineColumnsPrefix } from '../../../drag_and_drop/helpers'; @@ -53,6 +56,26 @@ interface Props { toggleColumn: (column: ColumnHeaderOptions) => void; } +interface DraggableContainerProps { + children: React.ReactNode; + onMount: () => void; + onUnmount: () => void; +} + +export const DraggableContainer = React.memo( + ({ children, onMount, onUnmount }) => { + useEffect(() => { + onMount(); + + return () => onUnmount(); + }, [onMount, onUnmount]); + + return <>{children}; + } +); + +DraggableContainer.displayName = 'DraggableContainer'; + /** Renders the timeline header columns */ export const ColumnHeadersComponent = ({ actionsColumnWidth, @@ -71,86 +94,157 @@ export const ColumnHeadersComponent = ({ sort, timelineId, toggleColumn, -}: Props) => ( - - - - {showEventsSelect && ( - - - - - - )} - {showSelectAllCheckbox && ( +}: Props) => { + const [draggingIndex, setDraggingIndex] = useState(null); + + const handleSelectAllChange = useCallback( + (event: React.ChangeEvent) => { + onSelectAll({ isSelected: event.currentTarget.checked }); + }, + [onSelectAll] + ); + + const renderClone: DraggableChildrenFn = useCallback( + (dragProvided, dragSnapshot, rubric) => { + // TODO: Remove after github.com/DefinitelyTyped/DefinitelyTyped/pull/43057 is merged + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const index = (rubric as any).source.index; + const header = columnHeaders[index]; + + const onMount = () => setDraggingIndex(index); + const onUnmount = () => setDraggingIndex(null); + + return ( + + + + + + + + ); + }, + [columnHeaders, setDraggingIndex] + ); + + const ColumnHeaderList = useMemo( + () => + columnHeaders.map((header, draggableIndex) => ( + + )), + [ + columnHeaders, + timelineId, + draggingIndex, + onColumnRemoved, + onFilterChange, + onColumnResized, + sort, + ] + ); + + return ( + + + + {showEventsSelect && ( + + + + + + )} + {showSelectAllCheckbox && ( + + + + + + )} - - ) => { - onSelectAll({ isSelected: event.currentTarget.checked }); - }} + + - )} - - - - - - - - - {(dropProvided, snapshot) => ( - <> - - {columnHeaders.map((header, draggableIndex) => ( - - ))} - - {dropProvided.placeholder} - - )} - - - -); + -export const ColumnHeaders = React.memo(ColumnHeadersComponent); + + {(dropProvided, snapshot) => ( + <> + + {ColumnHeaderList} + + + )} + + + + ); +}; + +export const ColumnHeaders = React.memo( + ColumnHeadersComponent, + (prevProps, nextProps) => + prevProps.actionsColumnWidth === nextProps.actionsColumnWidth && + prevProps.isEventViewer === nextProps.isEventViewer && + prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && + prevProps.onColumnRemoved === nextProps.onColumnRemoved && + prevProps.onColumnResized === nextProps.onColumnResized && + prevProps.onColumnSorted === nextProps.onColumnSorted && + prevProps.onSelectAll === nextProps.onSelectAll && + prevProps.onUpdateColumns === nextProps.onUpdateColumns && + prevProps.onFilterChange === nextProps.onFilterChange && + prevProps.showEventsSelect === nextProps.showEventsSelect && + prevProps.showSelectAllCheckbox === nextProps.showSelectAllCheckbox && + prevProps.sort === nextProps.sort && + prevProps.timelineId === nextProps.timelineId && + prevProps.toggleColumn === nextProps.toggleColumn && + deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && + deepEqual(prevProps.browserFields, nextProps.browserFields) +); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap index 93e12a0ed4fcd..75623252181db 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap @@ -6,11 +6,7 @@ exports[`Columns it renders the expected columns 1`] = ` > ( - ({ _id, columnHeaders, columnRenderers, data, ecsData, timelineId }) => { - // Passing the styles directly to the component because the width is - // being calculated and is recommended by Styled Components for performance - // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 - return ( - - {columnHeaders.map((header, index) => ( - - - {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ - columnName: header.id, - eventId: _id, - field: header, - linkValues: getOr([], header.linkField ?? '', ecsData), - timelineId, - truncate: true, - values: getMappedNonEcsValue({ - data, - fieldName: header.id, - }), - })} - - - ))} - - ); - } + ({ _id, columnHeaders, columnRenderers, data, ecsData, timelineId }) => ( + + {columnHeaders.map(header => ( + + + {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ + columnName: header.id, + eventId: _id, + field: header, + linkValues: getOr([], header.linkField ?? '', ecsData), + timelineId, + truncate: true, + values: getMappedNonEcsValue({ + data, + fieldName: header.id, + }), + })} + + + ))} + + ) ); DataDrivenColumns.displayName = 'DataDrivenColumns'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/index.tsx index 84c4253076dc9..4178bc656f32d 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/index.tsx @@ -51,9 +51,6 @@ interface Props { updateNote: UpdateNote; } -// Passing the styles directly to the component because the width is -// being calculated and is recommended by Styled Components for performance -// https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 const EventsComponent: React.FC = ({ actionsColumnWidth, addNoteToEvent, @@ -93,7 +90,7 @@ const EventsComponent: React.FC = ({ getNotesByIds={getNotesByIds} isEventPinned={eventIsPinned({ eventId: event._id, pinnedEventIds })} isEventViewer={isEventViewer} - key={event._id} + key={`${event._id}_${event._index}`} loadingEventIds={loadingEventIds} maxDelay={maxDelay(i)} onColumnResized={onColumnResized} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx index 1f09ae4337c42..6e5c292064dc6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx @@ -25,13 +25,14 @@ import { } from '../../events'; import { ExpandableEvent } from '../../expandable_event'; import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../helpers'; -import { EventsTrGroup, EventsTrSupplement, OFFSET_SCROLLBAR } from '../../styles'; -import { useTimelineWidthContext } from '../../timeline_context'; +import { EventsTrGroup, EventsTrSupplement, EventsTrSupplementContainer } from '../../styles'; import { ColumnRenderer } from '../renderers/column_renderer'; import { getRowRenderer } from '../renderers/get_row_renderer'; import { RowRenderer } from '../renderers/row_renderer'; import { getEventType } from '../helpers'; -import { StatefulEventChild } from './stateful_event_child'; +import { NoteCards } from '../../../notes/note_cards'; +import { useEventDetailsWidthContext } from '../../../events_viewer/event_details_width_context'; +import { EventColumnView } from './event_column_view'; interface Props { actionsColumnWidth: number; @@ -89,28 +90,14 @@ const TOP_OFFSET = 50; */ const BOTTOM_OFFSET = -500; -interface AttributesProps { - children: React.ReactNode; -} - -const AttributesComponent: React.FC = ({ children }) => { - const width = useTimelineWidthContext(); +const emptyNotes: string[] = []; - // Passing the styles directly to the component because the width is - // being calculated and is recommended by Styled Components for performance - // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 - return ( - - {children} - - ); -}; +const EventsTrSupplementContainerWrapper = React.memo(({ children }) => { + const width = useEventDetailsWidthContext(); + return {children}; +}); -const Attributes = React.memo(AttributesComponent); +EventsTrSupplementContainerWrapper.displayName = 'EventsTrSupplementContainerWrapper'; const StatefulEventComponent: React.FC = ({ actionsColumnWidth, @@ -221,60 +208,75 @@ const StatefulEventComponent: React.FC = ({ data-test-subj="event" eventType={getEventType(event.ecs)} showLeftBorder={!isEventViewer} - ref={c => { - if (c != null) { - divElement.current = c; - } - }} + ref={divElement} > - {getRowRenderer(event.ecs, rowRenderers).renderRow({ - browserFields, - data: event.ecs, - children: ( - + + + + - ), - timelineId, - })} + - - - + {getRowRenderer(event.ecs, rowRenderers).renderRow({ + browserFields, + data: event.ecs, + timelineId, + })} + + + + + )} @@ -286,10 +288,7 @@ const StatefulEventComponent: React.FC = ({ ? `${divElement.current.clientHeight}px` : DEFAULT_ROW_HEIGHT; - // height is being inlined directly in here because of performance with StyledComponents - // involving quick and constant changes to the DOM. - // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 - return ; + return ; } }} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx deleted file mode 100644 index 04f4ddf2a6eab..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx +++ /dev/null @@ -1,138 +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 uuid from 'uuid'; - -import { TimelineNonEcsData, Ecs } from '../../../../graphql/types'; -import { Note } from '../../../../lib/note'; -import { ColumnHeaderOptions } from '../../../../store/timeline/model'; -import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers'; -import { NoteCards } from '../../../notes/note_cards'; -import { OnPinEvent, OnColumnResized, OnUnPinEvent, OnRowSelected } from '../../events'; -import { EventsTrSupplement, OFFSET_SCROLLBAR } from '../../styles'; -import { useTimelineWidthContext } from '../../timeline_context'; -import { ColumnRenderer } from '../renderers/column_renderer'; -import { EventColumnView } from './event_column_view'; - -interface Props { - id: string; - actionsColumnWidth: number; - addNoteToEvent: AddNoteToEvent; - onPinEvent: OnPinEvent; - columnHeaders: ColumnHeaderOptions[]; - columnRenderers: ColumnRenderer[]; - data: TimelineNonEcsData[]; - ecsData: Ecs; - expanded: boolean; - eventIdToNoteIds: Readonly>; - isEventViewer?: boolean; - isEventPinned: boolean; - loading: boolean; - loadingEventIds: Readonly; - onColumnResized: OnColumnResized; - onRowSelected: OnRowSelected; - onUnPinEvent: OnUnPinEvent; - selectedEventIds: Readonly>; - showCheckboxes: boolean; - showNotes: boolean; - timelineId: string; - updateNote: UpdateNote; - onToggleExpanded: () => void; - onToggleShowNotes: () => void; - getNotesByIds: (noteIds: string[]) => Note[]; - associateNote: (noteId: string) => void; -} - -export const getNewNoteId = (): string => uuid.v4(); - -const emptyNotes: string[] = []; - -export const StatefulEventChild = React.memo( - ({ - id, - actionsColumnWidth, - associateNote, - addNoteToEvent, - onPinEvent, - columnHeaders, - columnRenderers, - expanded, - data, - ecsData, - eventIdToNoteIds, - getNotesByIds, - isEventViewer = false, - isEventPinned = false, - loading, - loadingEventIds, - onColumnResized, - onRowSelected, - onToggleExpanded, - onUnPinEvent, - selectedEventIds, - showCheckboxes, - showNotes, - timelineId, - onToggleShowNotes, - updateNote, - }) => { - const width = useTimelineWidthContext(); - - // Passing the styles directly to the component because the width is - // being calculated and is recommended by Styled Components for performance - // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 - return ( - <> - - - - - - - ); - } -); -StatefulEventChild.displayName = 'StatefulEventChild'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx index ea80d3351408a..fac8cc61cddd2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx @@ -38,7 +38,7 @@ export interface BodyProps { columnRenderers: ColumnRenderer[]; data: TimelineItem[]; getNotesByIds: (noteIds: string[]) => Note[]; - height: number; + height?: number; id: string; isEventViewer?: boolean; isSelectAllChecked: boolean; @@ -96,9 +96,10 @@ export const Body = React.memo( }) => { const containerElementRef = useRef(null); const timelineTypeContext = useTimelineTypeContext(); - const additionalActionWidth = - timelineTypeContext.timelineActions?.reduce((acc, v) => acc + v.width, 0) ?? 0; - + const additionalActionWidth = useMemo( + () => timelineTypeContext.timelineActions?.reduce((acc, v) => acc + v.width, 0) ?? 0, + [timelineTypeContext.timelineActions] + ); const actionsColumnWidth = useMemo( () => getActionsColumnWidth(isEventViewer, showCheckboxes, additionalActionWidth), [isEventViewer, showCheckboxes, additionalActionWidth] @@ -113,11 +114,7 @@ export const Body = React.memo( return ( <> - + - - some child - - -`; +exports[`get_column_renderer renders correctly against snapshot 1`] = ``; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap index 5731921907fc8..66a1b293cf8b9 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap @@ -1,9 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`plain_row_renderer renders correctly against snapshot 1`] = ` - - - some children - - -`; +exports[`plain_row_renderer renders correctly against snapshot 1`] = ``; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap index 0b2a1b2f2a0ae..b24a90589ce65 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap @@ -2,9 +2,6 @@ exports[`GenericRowRenderer #createGenericAuditRowRenderer renders correctly against snapshot 1`] = ` - - some children - - - some children - { const children = connectedToRenderer.renderRow({ browserFields, data: auditd, - children: {'some children'}, timelineId: 'test', }); @@ -66,26 +65,10 @@ describe('GenericRowRenderer', () => { } }); - test('should render children normally if it does not have a auditd object', () => { - const children = connectedToRenderer.renderRow({ - browserFields: mockBrowserFields, - data: nonAuditd, - children: {'some children'}, - timelineId: 'test', - }); - const wrapper = mount( - - {children} - - ); - expect(wrapper.text()).toEqual('some children'); - }); - test('should render a auditd row', () => { const children = connectedToRenderer.renderRow({ browserFields: mockBrowserFields, data: auditd, - children: {'some children '}, timelineId: 'test', }); const wrapper = mount( @@ -94,7 +77,7 @@ describe('GenericRowRenderer', () => { ); expect(wrapper.text()).toContain( - 'some children Session246alice@zeek-londonsome textwget(1490)wget www.example.comwith resultsuccessDestination93.184.216.34:80' + 'Session246alice@zeek-londonsome textwget(1490)wget www.example.comwith resultsuccessDestination93.184.216.34:80' ); }); }); @@ -119,7 +102,6 @@ describe('GenericRowRenderer', () => { const children = fileToRenderer.renderRow({ browserFields, data: auditdFile, - children: {'some children'}, timelineId: 'test', }); @@ -145,26 +127,10 @@ describe('GenericRowRenderer', () => { } }); - test('should render children normally if it does not have a auditd object', () => { - const children = fileToRenderer.renderRow({ - browserFields: mockBrowserFields, - data: nonAuditd, - children: {'some children'}, - timelineId: 'test', - }); - const wrapper = mount( - - {children} - - ); - expect(wrapper.text()).toEqual('some children'); - }); - test('should render a auditd row', () => { const children = fileToRenderer.renderRow({ browserFields: mockBrowserFields, data: auditdFile, - children: {'some children '}, timelineId: 'test', }); const wrapper = mount( @@ -173,7 +139,7 @@ describe('GenericRowRenderer', () => { ); expect(wrapper.text()).toContain( - 'some children Sessionunsetroot@zeek-londonin/some text/proc/15990/attr/currentusingsystemd-journal(27244)/lib/systemd/systemd-journaldwith resultsuccess' + 'Sessionunsetroot@zeek-londonin/some text/proc/15990/attr/currentusingsystemd-journal(27244)/lib/systemd/systemd-journaldwith resultsuccess' ); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx index bcf464ab6da15..4ed4ae10ed810 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx @@ -32,19 +32,16 @@ export const createGenericAuditRowRenderer = ({ action.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); @@ -67,20 +64,17 @@ export const createGenericFileRowRenderer = ({ action.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx index f367769b78f40..7ad8cfed5256b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx @@ -38,7 +38,6 @@ describe('get_column_renderer', () => { const row = rowRenderer.renderRow({ browserFields: mockBrowserFields, data: nonSuricata, - children: {'some child'}, timelineId: 'test', }); @@ -51,7 +50,6 @@ describe('get_column_renderer', () => { const row = rowRenderer.renderRow({ browserFields: mockBrowserFields, data: nonSuricata, - children: {'some child'}, timelineId: 'test', }); const wrapper = mount( @@ -59,7 +57,7 @@ describe('get_column_renderer', () => { {row} ); - expect(wrapper.text()).toContain('some child'); + expect(wrapper.text()).toEqual(''); }); test('should render a suricata row data when it is a suricata row', () => { @@ -67,7 +65,6 @@ describe('get_column_renderer', () => { const row = rowRenderer.renderRow({ browserFields: mockBrowserFields, data: suricata, - children: {'some child '}, timelineId: 'test', }); const wrapper = mount( @@ -76,7 +73,7 @@ describe('get_column_renderer', () => { ); expect(wrapper.text()).toContain( - 'some child 4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' + '4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); @@ -86,7 +83,6 @@ describe('get_column_renderer', () => { const row = rowRenderer.renderRow({ browserFields: mockBrowserFields, data: suricata, - children: {'some child '}, timelineId: 'test', }); const wrapper = mount( @@ -95,7 +91,7 @@ describe('get_column_renderer', () => { ); expect(wrapper.text()).toContain( - 'some child 4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' + '4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); @@ -105,7 +101,6 @@ describe('get_column_renderer', () => { const row = rowRenderer.renderRow({ browserFields: mockBrowserFields, data: zeek, - children: {'some child '}, timelineId: 'test', }); const wrapper = mount( @@ -114,7 +109,7 @@ describe('get_column_renderer', () => { ); expect(wrapper.text()).toContain( - 'some child C8DRTq362Fios6hw16connectionREJSrConnection attempt rejectedtcpSource185.176.26.101:44059Destination207.154.238.205:11568' + 'C8DRTq362Fios6hw16connectionREJSrConnection attempt rejectedtcpSource185.176.26.101:44059Destination207.154.238.205:11568' ); }); @@ -124,7 +119,6 @@ describe('get_column_renderer', () => { const row = rowRenderer.renderRow({ browserFields: mockBrowserFields, data: system, - children: {'some child '}, timelineId: 'test', }); const wrapper = mount( @@ -133,7 +127,7 @@ describe('get_column_renderer', () => { ); expect(wrapper.text()).toContain( - 'some child Braden@zeek-londonattempted a login via(6278)with resultfailureSource128.199.212.120' + 'Braden@zeek-londonattempted a login via(6278)with resultfailureSource128.199.212.120' ); }); @@ -143,7 +137,6 @@ describe('get_column_renderer', () => { const row = rowRenderer.renderRow({ browserFields: mockBrowserFields, data: auditd, - children: {'some child '}, timelineId: 'test', }); const wrapper = mount( @@ -152,7 +145,7 @@ describe('get_column_renderer', () => { ); expect(wrapper.text()).toContain( - 'some child Sessionalice@zeek-sanfranin/executedgpgconf(5402)gpgconf--list-dirsagent-socketgpgconf --list-dirs agent-socket' + 'Sessionalice@zeek-sanfranin/executedgpgconf(5402)gpgconf--list-dirsagent-socketgpgconf --list-dirs agent-socket' ); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap index 4326b7372604d..d7bdacbcc61ef 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap @@ -2,9 +2,6 @@ exports[`netflowRowRenderer renders correctly against snapshot 1`] = ` - - some children -
{ const children = netflowRowRenderer.renderRow({ browserFields, data: getMockNetflowData(), - children: {'some children'}, timelineId: 'test', }); @@ -98,26 +97,10 @@ describe('netflowRowRenderer', () => { }); }); - test('should render children normally when given non-netflow data', () => { - const children = netflowRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: justIdAndTimestamp, - children: {'some children'}, - timelineId: 'test', - }); - const wrapper = mount( - - {children} - - ); - expect(wrapper.text()).toEqual('some children'); - }); - test('should render netflow data', () => { const children = netflowRowRenderer.renderRow({ browserFields: mockBrowserFields, data: getMockNetflowData(), - children: {'some children'}, timelineId: 'test', }); const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx index 754d6ad99b7fe..10d80e1952f40 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx @@ -78,73 +78,63 @@ export const eventActionMatches = (eventAction: string | object | undefined | nu }; export const netflowRowRenderer: RowRenderer = { - isInstance: ecs => { - return ( - eventCategoryMatches(get(EVENT_CATEGORY_FIELD, ecs)) || - eventActionMatches(get(EVENT_ACTION_FIELD, ecs)) - ); - }, - renderRow: ({ data, children, timelineId }) => ( - <> - {children} - -
- -
-
- + isInstance: ecs => + eventCategoryMatches(get(EVENT_CATEGORY_FIELD, ecs)) || + eventActionMatches(get(EVENT_ACTION_FIELD, ecs)), + renderRow: ({ data, timelineId }) => ( + +
+ +
+
), }; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx index 50ea7ca05b921..467f507e8be7d 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx @@ -25,7 +25,6 @@ describe('plain_row_renderer', () => { const children = plainRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockDatum, - children: {'some children'}, timelineId: 'test', }); const wrapper = shallow({children}); @@ -40,7 +39,6 @@ describe('plain_row_renderer', () => { const children = plainRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockDatum, - children: {'some children'}, timelineId: 'test', }); const wrapper = mount( @@ -48,6 +46,6 @@ describe('plain_row_renderer', () => { {children} ); - expect(wrapper.text()).toEqual('some children'); + expect(wrapper.text()).toEqual(''); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx index 6725830c97d0a..da78f41f09ed4 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx @@ -12,5 +12,5 @@ import { RowRenderer } from './row_renderer'; export const plainRowRenderer: RowRenderer = { isInstance: _ => true, - renderRow: ({ children }) => <>{children}, + renderRow: () => <>, }; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/row_renderer.tsx index df92fc1e9f634..2d9f877fe4af0 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/row_renderer.tsx @@ -8,28 +8,17 @@ import React from 'react'; import { BrowserFields } from '../../../../containers/source'; import { Ecs } from '../../../../graphql/types'; -import { EventsTrSupplement, OFFSET_SCROLLBAR } from '../../styles'; -import { useTimelineWidthContext } from '../../timeline_context'; +import { EventsTrSupplement } from '../../styles'; interface RowRendererContainerProps { children: React.ReactNode; } -export const RowRendererContainer = React.memo(({ children }) => { - const width = useTimelineWidthContext(); - - // Passing the styles directly to the component because the width is - // being calculated and is recommended by Styled Components for performance - // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 - return ( - - {children} - - ); -}); +export const RowRendererContainer = React.memo(({ children }) => ( + + {children} + +)); RowRendererContainer.displayName = 'RowRendererContainer'; export interface RowRenderer { @@ -37,12 +26,10 @@ export interface RowRenderer { renderRow: ({ browserFields, data, - children, timelineId, }: { browserFields: BrowserFields; data: Ecs; - children: React.ReactNode; timelineId: string; }) => React.ReactNode; } diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap index 3608a81234677..93b3046b57ed6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap @@ -2,9 +2,6 @@ exports[`suricata_row_renderer renders correctly against snapshot 1`] = ` - - some children - { const children = suricataRowRenderer.renderRow({ browserFields: mockBrowserFields, data: nonSuricata, - children: {'some children'}, timelineId: 'test', }); @@ -45,26 +44,10 @@ describe('suricata_row_renderer', () => { expect(suricataRowRenderer.isInstance(suricata)).toBe(true); }); - test('should render children normally if it does not have a signature', () => { - const children = suricataRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: nonSuricata, - children: {'some children'}, - timelineId: 'test', - }); - const wrapper = mount( - - {children} - - ); - expect(wrapper.text()).toEqual('some children'); - }); - test('should render a suricata row', () => { const children = suricataRowRenderer.renderRow({ browserFields: mockBrowserFields, data: suricata, - children: {'some children '}, timelineId: 'test', }); const wrapper = mount( @@ -73,7 +56,7 @@ describe('suricata_row_renderer', () => { ); expect(wrapper.text()).toContain( - 'some children 4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' + '4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); @@ -82,7 +65,6 @@ describe('suricata_row_renderer', () => { const children = suricataRowRenderer.renderRow({ browserFields: mockBrowserFields, data: suricata, - children: {'some children'}, timelineId: 'test', }); const wrapper = mount( @@ -90,6 +72,6 @@ describe('suricata_row_renderer', () => { {children} ); - expect(wrapper.text()).toEqual('some children'); + expect(wrapper.text()).toEqual(''); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx index b227326551e01..e49a5f65b47c1 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx @@ -17,15 +17,9 @@ export const suricataRowRenderer: RowRenderer = { const module: string | null | undefined = get('event.module[0]', ecs); return module != null && module.toLowerCase() === 'suricata'; }, - renderRow: ({ browserFields, data, children, timelineId }) => { - return ( - <> - {children} - - - - - - ); - }, + renderRow: ({ browserFields, data, timelineId }) => ( + + + + ), }; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx index 2b9adfe21b120..9ccd1fb7a0519 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiBadge, EuiBadgeProps, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; @@ -28,11 +28,9 @@ const SignatureFlexItem = styled(EuiFlexItem)` SignatureFlexItem.displayName = 'SignatureFlexItem'; -// Ref: https://github.com/elastic/eui/issues/1655 -// const Badge = styled(EuiBadge)` -// vertical-align: top; -// `; -const Badge = (props: EuiBadgeProps) => ; +const Badge = styled(EuiBadge)` + vertical-align: top; +`; Badge.displayName = 'Badge'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap index 9ed6587145584..6fff32925abf3 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap @@ -2,9 +2,6 @@ exports[`GenericRowRenderer #createGenericFileRowRenderer renders correctly against snapshot 1`] = ` - - some children - - - some children - { const children = connectedToRenderer.renderRow({ browserFields, data: system, - children: {'some children'}, timelineId: 'test', }); @@ -99,7 +98,6 @@ describe('GenericRowRenderer', () => { const children = connectedToRenderer.renderRow({ browserFields: mockBrowserFields, data: system, - children: {'some children '}, timelineId: 'test', }); const wrapper = mount( @@ -108,7 +106,7 @@ describe('GenericRowRenderer', () => { ); expect(wrapper.text()).toContain( - 'some children Evan@zeek-londonsome text(6278)with resultfailureSource128.199.212.120' + 'Evan@zeek-londonsome text(6278)with resultfailureSource128.199.212.120' ); }); }); @@ -133,7 +131,6 @@ describe('GenericRowRenderer', () => { const children = fileToRenderer.renderRow({ browserFields, data: systemFile, - children: {'some children'}, timelineId: 'test', }); @@ -162,7 +159,6 @@ describe('GenericRowRenderer', () => { const children = fileToRenderer.renderRow({ browserFields: mockBrowserFields, data: systemFile, - children: {'some children '}, timelineId: 'test', }); const wrapper = mount( @@ -171,7 +167,7 @@ describe('GenericRowRenderer', () => { ); expect(wrapper.text()).toContain( - 'some children Braden@zeek-londonsome text(6278)with resultfailureSource128.199.212.120' + 'Braden@zeek-londonsome text(6278)with resultfailureSource128.199.212.120' ); }); }); @@ -195,14 +191,13 @@ describe('GenericRowRenderer', () => { endgameProcessCreationEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameCreationEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children Arun\\Anvi-Acer@HD-obe-8bf77f54started processMicrosoft.Photos.exe(441684)C:\\Program Files\\WindowsApps\\Microsoft.Windows.Photos_2018.18091.17210.0_x64__8wekyb3d8bbwe\\Microsoft.Photos.exe-ServerName:App.AppXzst44mncqdg84v7sv6p7yznqwssy6f7f.mcavia parent processsvchost.exe(8)d4c97ed46046893141652e2ec0056a698f6445109949d7fcabbce331146889ee12563599116157778a22600d2a163d8112aed84562d06d7235b37895b68de56687895743' + 'Arun\\Anvi-Acer@HD-obe-8bf77f54started processMicrosoft.Photos.exe(441684)C:\\Program Files\\WindowsApps\\Microsoft.Windows.Photos_2018.18091.17210.0_x64__8wekyb3d8bbwe\\Microsoft.Photos.exe-ServerName:App.AppXzst44mncqdg84v7sv6p7yznqwssy6f7f.mcavia parent processsvchost.exe(8)d4c97ed46046893141652e2ec0056a698f6445109949d7fcabbce331146889ee12563599116157778a22600d2a163d8112aed84562d06d7235b37895b68de56687895743' ); }); @@ -224,14 +219,13 @@ describe('GenericRowRenderer', () => { endgameProcessTerminationEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameTerminationEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children Arun\\Anvi-Acer@HD-obe-8bf77f54terminated processRuntimeBroker.exe(442384)with exit code087976f3430cc99bc939e0694247c0759961a49832b87218f4313d6fc0bc3a776797255e72d5ed5c058d4785950eba7abaa057653bd4401441a21bf1abce6404f4231db4d' + 'Arun\\Anvi-Acer@HD-obe-8bf77f54terminated processRuntimeBroker.exe(442384)with exit code087976f3430cc99bc939e0694247c0759961a49832b87218f4313d6fc0bc3a776797255e72d5ed5c058d4785950eba7abaa057653bd4401441a21bf1abce6404f4231db4d' ); }); @@ -253,7 +247,6 @@ describe('GenericRowRenderer', () => { endgameProcessCreationEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameCreationEvent, - children: {'some children '}, timelineId: 'test', })} @@ -284,7 +277,6 @@ describe('GenericRowRenderer', () => { endgameProcessCreationEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameCreationEvent, - children: {'some children '}, timelineId: 'test', })} @@ -315,7 +307,6 @@ describe('GenericRowRenderer', () => { endgameProcessCreationEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameCreationEvent, - children: {'some children '}, timelineId: 'test', })} @@ -344,14 +335,13 @@ describe('GenericRowRenderer', () => { endgameFileCreateEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameFileCreateEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children Arun\\Anvi-Acer@HD-obe-8bf77f54created a fileinC:\\Users\\Arun\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\63d78c21-e593-4484-b7a9-db33cd522ddc.tmpviachrome.exe(11620)' + 'Arun\\Anvi-Acer@HD-obe-8bf77f54created a fileinC:\\Users\\Arun\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\63d78c21-e593-4484-b7a9-db33cd522ddc.tmpviachrome.exe(11620)' ); }); @@ -373,14 +363,13 @@ describe('GenericRowRenderer', () => { endgameFileDeleteEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameFileDeleteEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children SYSTEM\\NT AUTHORITY@HD-v1s-d2118419deleted a filetmp000002f6inC:\\Windows\\TEMP\\tmp00000404\\tmp000002f6viaAmSvc.exe(1084)' + 'SYSTEM\\NT AUTHORITY@HD-v1s-d2118419deleted a filetmp000002f6inC:\\Windows\\TEMP\\tmp00000404\\tmp000002f6viaAmSvc.exe(1084)' ); }); @@ -402,15 +391,12 @@ describe('GenericRowRenderer', () => { fileCreatedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: fimFileCreatedEvent, - children: {'some children '}, timelineId: 'test', })} ); - expect(wrapper.text()).toEqual( - 'some children foohostcreated a filein/etc/subgidviaan unknown process' - ); + expect(wrapper.text()).toEqual('foohostcreated a filein/etc/subgidviaan unknown process'); }); test('it renders a FIM (non-endgame) file deleted event', () => { @@ -431,14 +417,13 @@ describe('GenericRowRenderer', () => { fileDeletedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: fimFileDeletedEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children foohostdeleted a filein/etc/gshadow.lockviaan unknown process' + 'foohostdeleted a filein/etc/gshadow.lockviaan unknown process' ); }); @@ -460,7 +445,6 @@ describe('GenericRowRenderer', () => { endgameFileCreateEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameFileCreateEvent, - children: {'some children '}, timelineId: 'test', })} @@ -491,7 +475,6 @@ describe('GenericRowRenderer', () => { endgameFileCreateEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameFileCreateEvent, - children: {'some children '}, timelineId: 'test', })} @@ -522,7 +505,6 @@ describe('GenericRowRenderer', () => { fileCreatedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: fimFileCreatedEvent, - children: {'some children '}, timelineId: 'test', })} @@ -551,14 +533,13 @@ describe('GenericRowRenderer', () => { endgameIpv4ConnectionAcceptEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: ipv4ConnectionAcceptEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children SYSTEM\\NT AUTHORITY@HD-gqf-0af7b4feaccepted a connection viaAmSvc.exe(1084)tcp1:network-community_idSource127.0.0.1:49306Destination127.0.0.1:49305' + 'SYSTEM\\NT AUTHORITY@HD-gqf-0af7b4feaccepted a connection viaAmSvc.exe(1084)tcp1:network-community_idSource127.0.0.1:49306Destination127.0.0.1:49305' ); }); @@ -580,14 +561,13 @@ describe('GenericRowRenderer', () => { endgameIpv6ConnectionAcceptEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: ipv6ConnectionAcceptEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66accepted a connection via(4)tcp1:network-community_idSource::1:51324Destination::1:5357' + 'SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66accepted a connection via(4)tcp1:network-community_idSource::1:51324Destination::1:5357' ); }); @@ -609,14 +589,13 @@ describe('GenericRowRenderer', () => { endgameIpv4DisconnectReceivedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: ipv4DisconnectReceivedEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children Arun\\Anvi-Acer@HD-obe-8bf77f54disconnected viachrome.exe(11620)8.1KBtcp1:LxYHJJv98b2O0fNccXu6HheXmwk=Source192.168.0.6:59356(25.78%)2.1KB(74.22%)6KBDestination10.156.162.53:443' + 'Arun\\Anvi-Acer@HD-obe-8bf77f54disconnected viachrome.exe(11620)8.1KBtcp1:LxYHJJv98b2O0fNccXu6HheXmwk=Source192.168.0.6:59356(25.78%)2.1KB(74.22%)6KBDestination10.156.162.53:443' ); }); @@ -638,14 +617,13 @@ describe('GenericRowRenderer', () => { endgameIpv6DisconnectReceivedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: ipv6DisconnectReceivedEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66disconnected via(4)7.9KBtcp1:ZylzQhsB1dcptA2t4DY8S6l9o8E=Source::1:51338(96.92%)7.7KB(3.08%)249BDestination::1:2869' + 'SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66disconnected via(4)7.9KBtcp1:ZylzQhsB1dcptA2t4DY8S6l9o8E=Source::1:51338(96.92%)7.7KB(3.08%)249BDestination::1:2869' ); }); @@ -667,14 +645,13 @@ describe('GenericRowRenderer', () => { socketOpenedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: socketOpenedEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children root@foohostopened a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59554 -> 10.1.2.3:80) Ooutboundtcp1:network-community_idSource10.4.20.1:59554Destination10.1.2.3:80' + 'root@foohostopened a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59554 -> 10.1.2.3:80) Ooutboundtcp1:network-community_idSource10.4.20.1:59554Destination10.1.2.3:80' ); }); @@ -696,14 +673,13 @@ describe('GenericRowRenderer', () => { socketClosedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: socketClosedEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children root@foohostclosed a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59508 -> 10.1.2.3:80) Coutboundtcp1:network-community_idSource10.4.20.1:59508Destination10.1.2.3:80' + 'root@foohostclosed a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59508 -> 10.1.2.3:80) Coutboundtcp1:network-community_idSource10.4.20.1:59508Destination10.1.2.3:80' ); }); @@ -725,7 +701,6 @@ describe('GenericRowRenderer', () => { endgameIpv4ConnectionAcceptEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: ipv4ConnectionAcceptEvent, - children: {'some children '}, timelineId: 'test', })} @@ -750,14 +725,13 @@ describe('GenericRowRenderer', () => { userLogonEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: userLogonEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children SYSTEM\\NT AUTHORITY@HD-v1s-d2118419successfully logged inusing logon type5 - Service(target logon ID0x3e7)viaC:\\Windows\\System32\\services.exe(432)as requested by subjectWIN-Q3DOP1UKA81$(subject logon ID0x3e7)4624' + 'SYSTEM\\NT AUTHORITY@HD-v1s-d2118419successfully logged inusing logon type5 - Service(target logon ID0x3e7)viaC:\\Windows\\System32\\services.exe(432)as requested by subjectWIN-Q3DOP1UKA81$(subject logon ID0x3e7)4624' ); }); @@ -775,14 +749,13 @@ describe('GenericRowRenderer', () => { adminLogonEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: adminLogonEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children With special privileges,SYSTEM\\NT AUTHORITY@HD-obe-8bf77f54successfully logged inviaC:\\Windows\\System32\\lsass.exe(964)as requested by subjectSYSTEM\\NT AUTHORITY4672' + 'With special privileges,SYSTEM\\NT AUTHORITY@HD-obe-8bf77f54successfully logged inviaC:\\Windows\\System32\\lsass.exe(964)as requested by subjectSYSTEM\\NT AUTHORITY4672' ); }); @@ -800,14 +773,13 @@ describe('GenericRowRenderer', () => { explicitUserLogonEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: explicitUserLogonEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children A login was attempted using explicit credentialsArun\\Anvi-AcertoHD-55b-3ec87f66viaC:\\Windows\\System32\\svchost.exe(1736)as requested by subjectANVI-ACER$\\WORKGROUP(subject logon ID0x3e7)4648' + 'A login was attempted using explicit credentialsArun\\Anvi-AcertoHD-55b-3ec87f66viaC:\\Windows\\System32\\svchost.exe(1736)as requested by subjectANVI-ACER$\\WORKGROUP(subject logon ID0x3e7)4648' ); }); @@ -825,14 +797,13 @@ describe('GenericRowRenderer', () => { userLogoffEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: userLogoffEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children Arun\\Anvi-Acer@HD-55b-3ec87f66logged offusing logon type2 - Interactive(target logon ID0x16db41e)viaC:\\Windows\\System32\\lsass.exe(964)4634' + 'Arun\\Anvi-Acer@HD-55b-3ec87f66logged offusing logon type2 - Interactive(target logon ID0x16db41e)viaC:\\Windows\\System32\\lsass.exe(964)4634' ); }); @@ -850,7 +821,6 @@ describe('GenericRowRenderer', () => { userLogonEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: userLogonEvent, - children: {'some children '}, timelineId: 'test', })} @@ -874,14 +844,13 @@ describe('GenericRowRenderer', () => { dnsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: requestEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children SYSTEM\\NT AUTHORITY@HD-obe-8bf77f54asked forupdate.googleapis.comwith question typeA, which resolved to10.100.197.67viaGoogleUpdate.exe(443192)3008dns' + 'SYSTEM\\NT AUTHORITY@HD-obe-8bf77f54asked forupdate.googleapis.comwith question typeA, which resolved to10.100.197.67viaGoogleUpdate.exe(443192)3008dns' ); }); @@ -898,14 +867,13 @@ describe('GenericRowRenderer', () => { dnsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: dnsEvent, - children: {'some children '}, timelineId: 'test', })} ); expect(wrapper.text()).toEqual( - 'some children iot.example.comasked forlookup.example.comwith question typeA, which resolved to10.1.2.3(response code:NOERROR)viaan unknown process6.937500msOct 8, 2019 @ 10:05:23.241Oct 8, 2019 @ 10:05:23.248outbounddns177Budp1:network-community_idSource10.9.9.9:58732(22.60%)40B(77.40%)137BDestination10.1.1.1:53OceaniaAustralia🇦🇺AU' + 'iot.example.comasked forlookup.example.comwith question typeA, which resolved to10.1.2.3(response code:NOERROR)viaan unknown process6.937500msOct 8, 2019 @ 10:05:23.241Oct 8, 2019 @ 10:05:23.248outbounddns177Budp1:network-community_idSource10.9.9.9:58732(22.60%)40B(77.40%)137BDestination10.1.1.1:53OceaniaAustralia🇦🇺AU' ); }); @@ -928,7 +896,6 @@ describe('GenericRowRenderer', () => { dnsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: requestEvent, - children: {'some children '}, timelineId: 'test', })} @@ -956,7 +923,6 @@ describe('GenericRowRenderer', () => { dnsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: requestEvent, - children: {'some children '}, timelineId: 'test', })} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx index 3e64248d39876..523d4f3a0cfb8 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx @@ -35,19 +35,16 @@ export const createGenericSystemRowRenderer = ({ action.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); @@ -68,20 +65,17 @@ export const createEndgameProcessRowRenderer = ({ action.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); @@ -102,20 +96,17 @@ export const createFimRowRenderer = ({ action.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); @@ -136,19 +127,16 @@ export const createGenericFileRowRenderer = ({ action.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); @@ -163,19 +151,16 @@ export const createSocketRowRenderer = ({ const action: string | null | undefined = get('event.action[0]', ecs); return action != null && action.toLowerCase() === actionName; }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); @@ -194,18 +179,15 @@ export const createSecurityEventRowRenderer = ({ action.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); @@ -215,18 +197,15 @@ export const createDnsRowRenderer = (): RowRenderer => ({ const dnsQuestionName: string | null | undefined = get('dns.question.name[0]', ecs); return !isNillEmptyOrNotFinite(dnsQuestionType) && !isNillEmptyOrNotFinite(dnsQuestionName); }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap index 9b59f69cad3a3..460ad35b47678 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap @@ -2,9 +2,6 @@ exports[`zeek_row_renderer renders correctly against snapshot 1`] = ` - - some children - { const children = zeekRowRenderer.renderRow({ browserFields: mockBrowserFields, data: nonZeek, - children: {'some children'}, timelineId: 'test', }); @@ -44,26 +43,10 @@ describe('zeek_row_renderer', () => { expect(zeekRowRenderer.isInstance(zeek)).toBe(true); }); - test('should render children normally if it does not have a zeek object', () => { - const children = zeekRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: nonZeek, - children: {'some children'}, - timelineId: 'test', - }); - const wrapper = mount( - - {children} - - ); - expect(wrapper.text()).toEqual('some children'); - }); - test('should render a zeek row', () => { const children = zeekRowRenderer.renderRow({ browserFields: mockBrowserFields, data: zeek, - children: {'some children '}, timelineId: 'test', }); const wrapper = mount( @@ -72,7 +55,7 @@ describe('zeek_row_renderer', () => { ); expect(wrapper.text()).toContain( - 'some children C8DRTq362Fios6hw16connectionREJSrConnection attempt rejectedtcpSource185.176.26.101:44059Destination207.154.238.205:11568' + 'C8DRTq362Fios6hw16connectionREJSrConnection attempt rejectedtcpSource185.176.26.101:44059Destination207.154.238.205:11568' ); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx index fc528e33b5ab6..0fca5cdd8b3d4 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx @@ -17,12 +17,9 @@ export const zeekRowRenderer: RowRenderer = { const module: string | null | undefined = get('event.module[0]', ecs); return module != null && module.toLowerCase() === 'zeek'; }, - renderRow: ({ browserFields, data, children, timelineId }) => ( - <> - {children} - - - - + renderRow: ({ browserFields, data, timelineId }) => ( + + + ), }; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx index 57e5ff19eb815..f13a236e8ec36 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiBadge, EuiBadgeProps, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { get } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; @@ -19,11 +19,9 @@ import { IS_OPERATOR } from '../../../data_providers/data_provider'; import * as i18n from './translations'; -// Ref: https://github.com/elastic/eui/issues/1655 -// const Badge = styled(EuiBadge)` -// vertical-align: top; -// `; -const Badge = (props: EuiBadgeProps) => ; +const Badge = styled(EuiBadge)` + vertical-align: top; +`; Badge.displayName = 'Badge'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx index d06dcbb84ad78..76f26d3dda5af 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx @@ -8,6 +8,7 @@ import { noop } from 'lodash/fp'; import memoizeOne from 'memoize-one'; import React, { useCallback, useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; import { BrowserFields } from '../../../containers/source'; import { TimelineItem } from '../../../graphql/types'; @@ -38,9 +39,9 @@ import { plainRowRenderer } from './renderers/plain_row_renderer'; interface OwnProps { browserFields: BrowserFields; data: TimelineItem[]; + height?: number; id: string; isEventViewer?: boolean; - height: number; sort: Sort; toggleColumn: (column: ColumnHeaderOptions) => void; } @@ -101,7 +102,7 @@ const StatefulBodyComponent = React.memo( isSelected && Object.keys(selectedEventIds).length + 1 === data.length, }); }, - [id, data, selectedEventIds, timelineTypeContext.queryFields] + [setSelected, id, data, selectedEventIds, timelineTypeContext.queryFields] ); const onSelectAll: OnSelectAll = useCallback( @@ -118,7 +119,7 @@ const StatefulBodyComponent = React.memo( isSelectAllChecked: isSelected, }) : clearSelected!({ id }), - [id, data, timelineTypeContext.queryFields] + [setSelected, clearSelected, id, data, timelineTypeContext.queryFields] ); const onColumnSorted: OnColumnSorted = useCallback( @@ -189,25 +190,22 @@ const StatefulBodyComponent = React.memo( /> ); }, - (prevProps, nextProps) => { - return ( - prevProps.browserFields === nextProps.browserFields && - prevProps.columnHeaders === nextProps.columnHeaders && - prevProps.data === nextProps.data && - prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && - prevProps.notesById === nextProps.notesById && - prevProps.height === nextProps.height && - prevProps.id === nextProps.id && - prevProps.isEventViewer === nextProps.isEventViewer && - prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && - prevProps.loadingEventIds === nextProps.loadingEventIds && - prevProps.pinnedEventIds === nextProps.pinnedEventIds && - prevProps.selectedEventIds === nextProps.selectedEventIds && - prevProps.showCheckboxes === nextProps.showCheckboxes && - prevProps.showRowRenderers === nextProps.showRowRenderers && - prevProps.sort === nextProps.sort - ); - } + (prevProps, nextProps) => + deepEqual(prevProps.browserFields, nextProps.browserFields) && + deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && + deepEqual(prevProps.data, nextProps.data) && + prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && + deepEqual(prevProps.notesById, nextProps.notesById) && + prevProps.height === nextProps.height && + prevProps.id === nextProps.id && + prevProps.isEventViewer === nextProps.isEventViewer && + prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && + prevProps.loadingEventIds === nextProps.loadingEventIds && + prevProps.pinnedEventIds === nextProps.pinnedEventIds && + prevProps.selectedEventIds === nextProps.selectedEventIds && + prevProps.showCheckboxes === nextProps.showCheckboxes && + prevProps.showRowRenderers === nextProps.showRowRenderers && + prevProps.sort === nextProps.sort ); StatefulBodyComponent.displayName = 'StatefulBodyComponent'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx index a47fb932ed26c..56639f90c1464 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiBadge, EuiBadgeProps, EuiText } from '@elastic/eui'; +import { EuiBadge, EuiText } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; @@ -21,24 +21,12 @@ const Text = styled(EuiText)` Text.displayName = 'Text'; -// Ref: https://github.com/elastic/eui/issues/1655 -// const BadgeHighlighted = styled(EuiBadge)` -// height: 20px; -// margin: 0 5px 0 5px; -// max-width: 70px; -// min-width: 70px; -// `; -const BadgeHighlighted = (props: EuiBadgeProps) => ( - -); +const BadgeHighlighted = styled(EuiBadge)` + height: 20px; + margin: 0 5px 0 5px; + maxwidth: 85px; + minwidth: 85px; +`; BadgeHighlighted.displayName = 'BadgeHighlighted'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx index 1a1e8292b7e02..663b3dd501341 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiBadge, EuiBadgeProps, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { rgba } from 'polished'; -import React from 'react'; +import React, { useCallback } from 'react'; import styled from 'styled-components'; import { AndOrBadge } from '../../and_or_badge'; @@ -54,13 +54,9 @@ const DropAndTargetDataProviders = styled.div<{ hasAndItem: boolean }>` DropAndTargetDataProviders.displayName = 'DropAndTargetDataProviders'; -// Ref: https://github.com/elastic/eui/issues/1655 -// const NumberProviderAndBadge = styled(EuiBadge)` -// margin: 0px 5px; -// `; -const NumberProviderAndBadge = (props: EuiBadgeProps) => ( - -); +const NumberProviderAndBadge = styled(EuiBadge)` + margin: 0px 5px; +`; NumberProviderAndBadge.displayName = 'NumberProviderAndBadge'; @@ -89,8 +85,13 @@ export const ProviderItemAndDragDrop = React.memo( onToggleDataProviderExcluded, timelineId, }) => { - const onMouseEnter = () => onChangeDroppableAndProvider(dataProvider.id); - const onMouseLeave = () => onChangeDroppableAndProvider(''); + const onMouseEnter = useCallback(() => onChangeDroppableAndProvider(dataProvider.id), [ + onChangeDroppableAndProvider, + dataProvider.id, + ]); + const onMouseLeave = useCallback(() => onChangeDroppableAndProvider(''), [ + onChangeDroppableAndProvider, + ]); const hasAndItem = dataProvider.and.length > 0; return ( ` ${({ hideExpandButton }) => @@ -50,33 +49,26 @@ export const ExpandableEvent = React.memo( timelineId, toggleColumn, onUpdateColumns, - }) => { - const width = useTimelineWidthContext(); - // Passing the styles directly to the component of LazyAccordion because the width is - // being calculated and is recommended by Styled Components for performance - // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 - return ( - - ( - - )} - forceExpand={forceExpand} - paddingSize="none" - /> - - ); - } + }) => ( + + ( + + )} + forceExpand={forceExpand} + paddingSize="none" + /> + + ) ); ExpandableEvent.displayName = 'ExpandableEvent'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/fetch_kql_timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/fetch_kql_timeline.tsx index 65c539d77a16b..16eaa80308205 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/fetch_kql_timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/fetch_kql_timeline.tsx @@ -6,6 +6,7 @@ import { memo, useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; import { IIndexPattern } from 'src/plugins/data/public'; import { timelineSelectors, State } from '../../store'; @@ -39,7 +40,14 @@ const TimelineKqlFetchComponent = memo( }); }, [kueryFilterQueryDraft, kueryFilterQuery, id]); return null; - } + }, + (prevProps, nextProps) => + prevProps.id === nextProps.id && + prevProps.inputId === nextProps.inputId && + prevProps.setTimelineQuery === nextProps.setTimelineQuery && + deepEqual(prevProps.kueryFilterQuery, nextProps.kueryFilterQuery) && + deepEqual(prevProps.kueryFilterQueryDraft, nextProps.kueryFilterQueryDraft) && + deepEqual(prevProps.indexPattern, nextProps.indexPattern) ); const makeMapStateToProps = () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/footer/__snapshots__/index.test.tsx.snap index 9cdbda757d97e..eff487ceb7981 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/footer/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/footer/__snapshots__/index.test.tsx.snap @@ -1,94 +1,86 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Footer Timeline Component rendering it renders the default timeline footer 1`] = ` - - + - - - - - 1 rows - , - - 5 rows - , - - 10 rows - , - - 20 rows - , - ] - } - itemsCount={2} - onClick={[Function]} - serverSideEventCount={15546} - /> - - - - + 1 rows + , + + 5 rows + , + + 10 rows + , + + 20 rows + , + ] + } + itemsCount={2} + onClick={[Function]} + serverSideEventCount={15546} /> - - - - - - - - - + + + + + + + + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx index cbad2d42cf8af..d54a4cee83e52 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx @@ -17,7 +17,6 @@ describe('Footer Timeline Component', () => { const loadMore = jest.fn(); const onChangeItemsPerPage = jest.fn(); const getUpdatedAt = () => 1546878704036; - const compact = true; describe('rendering', () => { test('it renders the default timeline footer', () => { @@ -36,7 +35,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); @@ -59,7 +57,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); @@ -83,7 +80,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); @@ -140,7 +136,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); @@ -164,7 +159,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); @@ -195,7 +189,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); @@ -225,7 +218,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); @@ -259,7 +251,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); @@ -285,7 +276,6 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - compact={compact} /> ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx index 1fcc4382c1798..7a025e96e57f2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx @@ -19,7 +19,7 @@ import { EuiPopoverProps, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import React, { FunctionComponent, useCallback, useEffect, useState } from 'react'; +import React, { FC, useCallback, useEffect, useState, useMemo } from 'react'; import styled from 'styled-components'; import { LoadingPanel } from '../../loading'; @@ -28,8 +28,30 @@ import { OnChangeItemsPerPage, OnLoadMore } from '../events'; import { LastUpdatedAt } from './last_updated'; import * as i18n from './translations'; import { useTimelineTypeContext } from '../timeline_context'; +import { useEventDetailsWidthContext } from '../../events_viewer/event_details_width_context'; -const FixedWidthLastUpdated = styled.div<{ compact: boolean }>` +export const isCompactFooter = (width: number): boolean => width < 600; + +interface FixedWidthLastUpdatedContainerProps { + updatedAt: number; +} + +const FixedWidthLastUpdatedContainer = React.memo( + ({ updatedAt }) => { + const width = useEventDetailsWidthContext(); + const compact = useMemo(() => isCompactFooter(width), [width]); + + return ( + + + + ); + } +); + +FixedWidthLastUpdatedContainer.displayName = 'FixedWidthLastUpdatedContainer'; + +const FixedWidthLastUpdated = styled.div<{ compact?: boolean }>` width: ${({ compact }) => (!compact ? 200 : 25)}px; overflow: hidden; text-align: end; @@ -37,8 +59,16 @@ const FixedWidthLastUpdated = styled.div<{ compact: boolean }>` FixedWidthLastUpdated.displayName = 'FixedWidthLastUpdated'; -const FooterContainer = styled(EuiFlexGroup)<{ height: number }>` - height: ${({ height }) => height}px; +interface HeightProp { + height: number; +} + +const FooterContainer = styled(EuiFlexGroup).attrs(({ height }) => ({ + style: { + height: `${height}px`, + }, +}))` + flex: 0; `; FooterContainer.displayName = 'FooterContainer'; @@ -56,7 +86,7 @@ const LoadingPanelContainer = styled.div` LoadingPanelContainer.displayName = 'LoadingPanelContainer'; -const PopoverRowItems = styled((EuiPopover as unknown) as FunctionComponent)< +const PopoverRowItems = styled((EuiPopover as unknown) as FC)< EuiPopoverProps & { className?: string; id?: string; @@ -173,11 +203,9 @@ export const PagingControl = React.memo(PagingControlComponent); PagingControl.displayName = 'PagingControl'; interface FooterProps { - compact: boolean; getUpdatedAt: () => number; hasNextPage: boolean; height: number; - isEventViewer?: boolean; isLive: boolean; isLoading: boolean; itemsCount: number; @@ -192,11 +220,9 @@ interface FooterProps { /** Renders a loading indicator and paging controls */ export const FooterComponent = ({ - compact, getUpdatedAt, hasNextPage, height, - isEventViewer, isLive, isLoading, itemsCount, @@ -216,11 +242,13 @@ export const FooterComponent = ({ const loadMore = useCallback(() => { setPaginationLoading(true); onLoadMore(nextCursor, tieBreaker); - }, [nextCursor, tieBreaker, onLoadMore]); + }, [nextCursor, tieBreaker, onLoadMore, setPaginationLoading]); - const onButtonClick = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]); - - const closePopover = useCallback(() => setIsPopoverOpen(false), []); + const onButtonClick = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [ + isPopoverOpen, + setIsPopoverOpen, + ]); + const closePopover = useCallback(() => setIsPopoverOpen(false), [setIsPopoverOpen]); useEffect(() => { if (paginationLoading && !isLoading) { @@ -263,95 +291,78 @@ export const FooterComponent = ({ )); return ( - <> - + - - - - - - - - - {isLive ? ( - - - {i18n.AUTO_REFRESH_ACTIVE}{' '} - - } - type="iInCircle" - /> - - - ) : ( - - )} - - - - - - - - - - + + + + + + + + {isLive ? ( + + + {i18n.AUTO_REFRESH_ACTIVE}{' '} + + } + type="iInCircle" + /> + + + ) : ( + + )} + + + + + + + ); }; FooterComponent.displayName = 'FooterComponent'; -export const Footer = React.memo( - FooterComponent, - (prevProps, nextProps) => - prevProps.compact === nextProps.compact && - prevProps.hasNextPage === nextProps.hasNextPage && - prevProps.height === nextProps.height && - prevProps.isEventViewer === nextProps.isEventViewer && - prevProps.isLive === nextProps.isLive && - prevProps.isLoading === nextProps.isLoading && - prevProps.itemsCount === nextProps.itemsCount && - prevProps.itemsPerPage === nextProps.itemsPerPage && - prevProps.itemsPerPageOptions === nextProps.itemsPerPageOptions && - prevProps.serverSideEventCount === nextProps.serverSideEventCount -); +export const Footer = React.memo(FooterComponent); Footer.displayName = 'Footer'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap index 048ca080772f6..90d0dc1a8a66d 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap @@ -1,9 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Header rendering renders correctly against snapshot 1`] = ` - + - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx index 5af7aff4f8795..317c68b63f691 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx @@ -7,13 +7,12 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { Direction } from '../../../graphql/types'; import { mockIndexPattern } from '../../../mock'; import { TestProviders } from '../../../mock/test_providers'; import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; import { useMountAppended } from '../../../utils/use_mount_appended'; -import { TimelineHeaderComponent } from '.'; +import { TimelineHeader } from '.'; jest.mock('../../../lib/kibana'); @@ -24,7 +23,7 @@ describe('Header', () => { describe('rendering', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( - { onToggleDataProviderExcluded={jest.fn()} show={true} showCallOutUnauthorizedMsg={false} - sort={{ - columnId: '@timestamp', - sortDirection: Direction.desc, - }} /> ); expect(wrapper).toMatchSnapshot(); @@ -49,7 +44,7 @@ describe('Header', () => { test('it renders the data providers', () => { const wrapper = mount( - { onToggleDataProviderExcluded={jest.fn()} show={true} showCallOutUnauthorizedMsg={false} - sort={{ - columnId: '@timestamp', - sortDirection: Direction.desc, - }} /> ); @@ -76,7 +67,7 @@ describe('Header', () => { test('it renders the unauthorized call out providers', () => { const wrapper = mount( - { onToggleDataProviderExcluded={jest.fn()} show={true} showCallOutUnauthorizedMsg={true} - sort={{ - columnId: '@timestamp', - sortDirection: Direction.desc, - }} /> ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx index 81eef0efbfa5b..7cac03cec42b1 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx @@ -6,10 +6,9 @@ import { EuiCallOut } from '@elastic/eui'; import React from 'react'; -import styled from 'styled-components'; import { IIndexPattern } from 'src/plugins/data/public'; +import deepEqual from 'fast-deep-equal'; -import { Sort } from '../body/sort'; import { DataProviders } from '../data_providers'; import { DataProvider } from '../data_providers/data_provider'; import { @@ -38,16 +37,9 @@ interface Props { onToggleDataProviderExcluded: OnToggleDataProviderExcluded; show: boolean; showCallOutUnauthorizedMsg: boolean; - sort: Sort; } -const TimelineHeaderContainer = styled.div` - width: 100%; -`; - -TimelineHeaderContainer.displayName = 'TimelineHeaderContainer'; - -export const TimelineHeaderComponent: React.FC = ({ +const TimelineHeaderComponent: React.FC = ({ browserFields, id, indexPattern, @@ -61,7 +53,7 @@ export const TimelineHeaderComponent: React.FC = ({ show, showCallOutUnauthorizedMsg, }) => ( - + <> {showCallOutUnauthorizedMsg && ( = ({ indexPattern={indexPattern} timelineId={id} /> - + ); -export const TimelineHeader = React.memo(TimelineHeaderComponent); +export const TimelineHeader = React.memo( + TimelineHeaderComponent, + (prevProps, nextProps) => + deepEqual(prevProps.browserFields, nextProps.browserFields) && + prevProps.id === nextProps.id && + deepEqual(prevProps.indexPattern, nextProps.indexPattern) && + deepEqual(prevProps.dataProviders, nextProps.dataProviders) && + prevProps.onChangeDataProviderKqlQuery === nextProps.onChangeDataProviderKqlQuery && + prevProps.onChangeDroppableAndProvider === nextProps.onChangeDroppableAndProvider && + prevProps.onDataProviderEdited === nextProps.onDataProviderEdited && + prevProps.onDataProviderRemoved === nextProps.onDataProviderRemoved && + prevProps.onToggleDataProviderEnabled === nextProps.onToggleDataProviderEnabled && + prevProps.onToggleDataProviderExcluded === nextProps.onToggleDataProviderExcluded && + prevProps.show === nextProps.show && + prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg +); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx index 611d08e61be22..f051bbe5b1af6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx @@ -153,25 +153,6 @@ export const combineQueries = ({ }; }; -interface CalculateBodyHeightParams { - /** The the height of the flyout container, which is typically the entire "page", not including the standard Kibana navigation */ - flyoutHeight?: number; - /** The flyout header typically contains a title and a close button */ - flyoutHeaderHeight?: number; - /** All non-body timeline content (i.e. the providers drag and drop area, and the column headers) */ - timelineHeaderHeight?: number; - /** Footer content that appears below the body (i.e. paging controls) */ - timelineFooterHeight?: number; -} - -export const calculateBodyHeight = ({ - flyoutHeight = 0, - flyoutHeaderHeight = 0, - timelineHeaderHeight = 0, - timelineFooterHeight = 0, -}: CalculateBodyHeightParams): number => - flyoutHeight - (flyoutHeaderHeight + timelineHeaderHeight + timelineFooterHeight); - /** * The CSS class name of a "stateful event", which appears in both * the `Timeline` and the `Events Viewer` widget diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx index 0ce6bc16f1325..35099e3836fb4 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx @@ -28,8 +28,8 @@ import { Timeline } from './timeline'; export interface OwnProps { id: string; - flyoutHeaderHeight: number; - flyoutHeight: number; + onClose: () => void; + usersViewing: string[]; } type Props = OwnProps & PropsFromRedux; @@ -42,14 +42,13 @@ const StatefulTimelineComponent = React.memo( eventType, end, filters, - flyoutHeaderHeight, - flyoutHeight, id, isLive, itemsPerPage, itemsPerPageOptions, kqlMode, kqlQueryExpression, + onClose, onDataProviderEdited, removeColumn, removeProvider, @@ -63,6 +62,7 @@ const StatefulTimelineComponent = React.memo( updateHighlightedDropAndProviderId, updateItemsPerPage, upsertColumn, + usersViewing, }) => { const { loading, signalIndexExists, signalIndexName } = useSignalIndex(); @@ -173,8 +173,6 @@ const StatefulTimelineComponent = React.memo( end={end} eventType={eventType} filters={filters} - flyoutHeaderHeight={flyoutHeaderHeight} - flyoutHeight={flyoutHeight} id={id} indexPattern={indexPattern} indexToAdd={indexToAdd} @@ -187,6 +185,7 @@ const StatefulTimelineComponent = React.memo( onChangeDataProviderKqlQuery={onChangeDataProviderKqlQuery} onChangeDroppableAndProvider={onChangeDroppableAndProvider} onChangeItemsPerPage={onChangeItemsPerPage} + onClose={onClose} onDataProviderEdited={onDataProviderEditedLocal} onDataProviderRemoved={onDataProviderRemoved} onToggleDataProviderEnabled={onToggleDataProviderEnabled} @@ -196,6 +195,7 @@ const StatefulTimelineComponent = React.memo( sort={sort!} start={start} toggleColumn={toggleColumn} + usersViewing={usersViewing} /> )} @@ -205,8 +205,6 @@ const StatefulTimelineComponent = React.memo( return ( prevProps.eventType === nextProps.eventType && prevProps.end === nextProps.end && - prevProps.flyoutHeaderHeight === nextProps.flyoutHeaderHeight && - prevProps.flyoutHeight === nextProps.flyoutHeight && prevProps.id === nextProps.id && prevProps.isLive === nextProps.isLive && prevProps.itemsPerPage === nextProps.itemsPerPage && @@ -219,7 +217,8 @@ const StatefulTimelineComponent = React.memo( deepEqual(prevProps.dataProviders, nextProps.dataProviders) && deepEqual(prevProps.filters, nextProps.filters) && deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && - deepEqual(prevProps.sort, nextProps.sort) + deepEqual(prevProps.sort, nextProps.sort) && + deepEqual(prevProps.usersViewing, nextProps.usersViewing) ); } ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx index ae139c24d0176..4b1fd4b5851c0 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx @@ -6,7 +6,6 @@ import { EuiBadge, - EuiBadgeProps, EuiButton, EuiButtonEmpty, EuiButtonIcon, @@ -18,8 +17,9 @@ import { EuiOverlayMask, EuiToolTip, } from '@elastic/eui'; -import React from 'react'; +import React, { useCallback } from 'react'; import uuid from 'uuid'; +import styled from 'styled-components'; import { Note } from '../../../lib/note'; import { Notes } from '../../notes'; @@ -32,13 +32,10 @@ export const historyToolTip = 'The chronological history of actions related to t export const streamLiveToolTip = 'Update the Timeline as new data arrives'; export const newTimelineToolTip = 'Create a new timeline'; -// Ref: https://github.com/elastic/eui/issues/1655 -// const NotesCountBadge = styled(EuiBadge)` -// margin-left: 5px; -// `; -const NotesCountBadge = (props: EuiBadgeProps) => ( - -); +const NotesCountBadge = styled(EuiBadge)` + margin-left: 5px; +`; + NotesCountBadge.displayName = 'NotesCountBadge'; type CreateTimeline = ({ id, show }: { id: string; show?: boolean }) => void; @@ -121,20 +118,24 @@ interface NewTimelineProps { } export const NewTimeline = React.memo( - ({ createTimeline, onClosePopover, timelineId }) => ( - { - createTimeline({ id: timelineId, show: true }); - onClosePopover(); - }} - > - {i18n.NEW_TIMELINE} - - ) + ({ createTimeline, onClosePopover, timelineId }) => { + const handleClick = useCallback(() => { + createTimeline({ id: timelineId, show: true }); + onClosePopover(); + }, [createTimeline, timelineId, onClosePopover]); + + return ( + + {i18n.NEW_TIMELINE} + + ); + } ); NewTimeline.displayName = 'NewTimeline'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx index 495b94f8c02e7..e942c8f36dc83 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx @@ -10,11 +10,18 @@ import { Provider as ReduxStoreProvider } from 'react-redux'; import { mockGlobalState, apolloClientObservable } from '../../../mock'; import { createStore, State } from '../../../store'; +import { useThrottledResizeObserver } from '../../utils'; import { Properties, showDescriptionThreshold, showNotesThreshold } from '.'; jest.mock('../../../lib/kibana'); +let mockedWidth = 1000; +jest.mock('../../utils'); +(useThrottledResizeObserver as jest.Mock).mockImplementation(() => ({ + width: mockedWidth, +})); + describe('Properties', () => { const usersViewing = ['elastic']; @@ -24,6 +31,7 @@ describe('Properties', () => { beforeEach(() => { jest.clearAllMocks(); store = createStore(state, apolloClientObservable); + mockedWidth = 1000; }); test('renders correctly', () => { @@ -46,7 +54,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -73,7 +80,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -101,7 +107,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -131,7 +136,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -164,7 +168,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -197,7 +200,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -229,7 +231,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -243,7 +244,7 @@ describe('Properties', () => { test('it renders a description on the left when the width is at least as wide as the threshold', () => { const description = 'strange'; - const width = showDescriptionThreshold; + mockedWidth = showDescriptionThreshold; const wrapper = mount( @@ -264,7 +265,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={width} /> ); @@ -280,7 +280,7 @@ describe('Properties', () => { test('it does NOT render a description on the left when the width is less than the threshold', () => { const description = 'strange'; - const width = showDescriptionThreshold - 1; + mockedWidth = showDescriptionThreshold - 1; const wrapper = mount( @@ -301,7 +301,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={width} /> ); @@ -315,7 +314,7 @@ describe('Properties', () => { }); test('it renders a notes button on the left when the width is at least as wide as the threshold', () => { - const width = showNotesThreshold; + mockedWidth = showNotesThreshold; const wrapper = mount( @@ -336,7 +335,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={width} /> ); @@ -350,7 +348,7 @@ describe('Properties', () => { }); test('it does NOT render a a notes button on the left when the width is less than the threshold', () => { - const width = showNotesThreshold - 1; + mockedWidth = showNotesThreshold - 1; const wrapper = mount( @@ -371,7 +369,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={width} /> ); @@ -404,7 +401,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -434,7 +430,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); @@ -462,7 +457,6 @@ describe('Properties', () => { updateTitle={jest.fn()} updateNote={jest.fn()} usersViewing={usersViewing} - width={1000} /> ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx index 7b69e006f48ad..8549784b8ecd6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useMemo } from 'react'; +import { useThrottledResizeObserver } from '../../utils'; import { Note } from '../../../lib/note'; import { InputsModelId } from '../../../store/inputs/constants'; import { AssociateNote, UpdateNote } from '../../notes/helpers'; @@ -37,7 +38,6 @@ interface Props { updateNote: UpdateNote; updateTitle: UpdateTitle; usersViewing: string[]; - width: number; } const rightGutter = 60; // px @@ -49,7 +49,7 @@ const starIconWidth = 30; const nameWidth = 155; const descriptionWidth = 165; const noteWidth = 130; -const settingsWidth = 50; +const settingsWidth = 55; /** Displays the properties of a timeline, i.e. name, description, notes, etc */ export const Properties = React.memo( @@ -70,47 +70,36 @@ export const Properties = React.memo( updateNote, updateTitle, usersViewing, - width, }) => { + const { ref, width = 0 } = useThrottledResizeObserver(300); const [showActions, setShowActions] = useState(false); const [showNotes, setShowNotes] = useState(false); const [showTimelineModal, setShowTimelineModal] = useState(false); - const onButtonClick = useCallback(() => { - setShowActions(!showActions); - }, [showActions]); - - const onToggleShowNotes = useCallback(() => { - setShowNotes(!showNotes); - }, [showNotes]); - - const onClosePopover = useCallback(() => { - setShowActions(false); - }, []); - + const onButtonClick = useCallback(() => setShowActions(!showActions), [showActions]); + const onToggleShowNotes = useCallback(() => setShowNotes(!showNotes), [showNotes]); + const onClosePopover = useCallback(() => setShowActions(false), []); + const onCloseTimelineModal = useCallback(() => setShowTimelineModal(false), []); + const onToggleLock = useCallback(() => toggleLock({ linkToId: 'timeline' }), [toggleLock]); const onOpenTimelineModal = useCallback(() => { onClosePopover(); setShowTimelineModal(true); }, []); - const onCloseTimelineModal = useCallback(() => { - setShowTimelineModal(false); - }, []); - - const datePickerWidth = - width - - rightGutter - - starIconWidth - - nameWidth - - (width >= showDescriptionThreshold ? descriptionWidth : 0) - - noteWidth - - settingsWidth; + const datePickerWidth = useMemo( + () => + width - + rightGutter - + starIconWidth - + nameWidth - + (width >= showDescriptionThreshold ? descriptionWidth : 0) - + noteWidth - + settingsWidth, + [width] + ); - // Passing the styles directly to the component because the width is - // being calculated and is recommended by Styled Components for performance - // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 return ( - + ( showNotesFromWidth={width >= showNotesThreshold} timelineId={timelineId} title={title} - toggleLock={() => { - toggleLock({ linkToId: 'timeline' }); - }} + toggleLock={onToggleLock} updateDescription={updateDescription} updateIsFavorite={updateIsFavorite} updateNote={updateNote} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_left.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_left.tsx index 21800fefb21fb..3016def8a80b1 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_left.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_left.tsx @@ -52,7 +52,15 @@ export const LockIconContainer = styled(EuiFlexItem)` LockIconContainer.displayName = 'LockIconContainer'; -export const DatePicker = styled(EuiFlexItem)` +interface WidthProp { + width: number; +} + +export const DatePicker = styled(EuiFlexItem).attrs(({ width }) => ({ + style: { + width: `${width}px`, + }, +}))` .euiSuperDatePicker__flexWrapper { max-width: none; width: auto; @@ -151,7 +159,7 @@ export const PropertiesLeft = React.memo( /> - + diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/styles.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/styles.tsx index 3444875282ae7..74653fb6cb1ef 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/styles.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/styles.tsx @@ -12,17 +12,26 @@ const fadeInEffect = keyframes` to { opacity: 1; } `; +interface WidthProp { + width: number; +} + export const TimelineProperties = styled.div` + flex: 1; align-items: center; display: flex; flex-direction: row; justify-content: space-between; user-select: none; `; + TimelineProperties.displayName = 'TimelineProperties'; -export const DatePicker = styled(EuiFlexItem)<{ width: number }>` - width: ${({ width }) => `${width}px`}; +export const DatePicker = styled(EuiFlexItem).attrs(({ width }) => ({ + style: { + width: `${width}px`, + }, +}))` .euiSuperDatePicker__flexWrapper { max-width: none; width: auto; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/refetch_timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/refetch_timeline.tsx index 3d2ec0683f091..73c20d9b9b6b4 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/refetch_timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/refetch_timeline.tsx @@ -5,7 +5,7 @@ */ import React, { useEffect } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { inputsModel } from '../../store'; import { inputsActions } from '../../store/actions'; @@ -19,29 +19,20 @@ export interface TimelineRefetchProps { refetch: inputsModel.Refetch; } -type OwnProps = TimelineRefetchProps & PropsFromRedux; - -const TimelineRefetchComponent: React.FC = ({ +const TimelineRefetchComponent: React.FC = ({ id, inputId, inspect, loading, refetch, - setTimelineQuery, }) => { + const dispatch = useDispatch(); + useEffect(() => { - setTimelineQuery({ id, inputId, inspect, loading, refetch }); - }, [id, inputId, loading, refetch, inspect]); + dispatch(inputsActions.setQuery({ id, inputId, inspect, loading, refetch })); + }, [dispatch, id, inputId, loading, refetch, inspect]); return null; }; -const mapDispatchToProps = { - setTimelineQuery: inputsActions.setQuery, -}; - -const connector = connect(null, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const TimelineRefetch = connector(React.memo(TimelineRefetchComponent)); +export const TimelineRefetch = React.memo(TimelineRefetchComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx index d5e5d15eb8ad2..16fb57714829c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx @@ -11,12 +11,6 @@ import styled, { createGlobalStyle } from 'styled-components'; import { EventType } from '../../store/timeline/model'; import { IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME } from '../drag_and_drop/helpers'; -/** - * OFFSET PIXEL VALUES - */ - -export const OFFSET_SCROLLBAR = 17; - /** * TIMELINE BODY */ @@ -30,10 +24,11 @@ export const TimelineBodyGlobalStyle = createGlobalStyle` export const TimelineBody = styled.div.attrs(({ className = '' }) => ({ className: `siemTimeline__body ${className}`, -}))<{ bodyHeight: number }>` - height: ${({ bodyHeight }) => `${bodyHeight}px`}; +}))<{ bodyHeight?: number }>` + height: ${({ bodyHeight }) => (bodyHeight ? `${bodyHeight}px` : 'auto')}; overflow: auto; scrollbar-width: thin; + flex: 1; &::-webkit-scrollbar { height: ${({ theme }) => theme.eui.euiScrollBar}; @@ -57,10 +52,19 @@ TimelineBody.displayName = 'TimelineBody'; * EVENTS TABLE */ -export const EventsTable = styled.div.attrs(({ className = '' }) => ({ - className: `siemEventsTable ${className}`, - role: 'table', -}))``; +interface EventsTableProps { + columnWidths: number; +} + +export const EventsTable = styled.div.attrs( + ({ className = '', columnWidths }) => ({ + className: `siemEventsTable ${className}`, + role: 'table', + style: { + minWidth: `${columnWidths}px`, + }, + }) +)``; /* EVENTS HEAD */ @@ -177,6 +181,14 @@ export const EventsTrData = styled.div.attrs(({ className = '' }) => ({ display: flex; `; +const TIMELINE_EVENT_DETAILS_OFFSET = 40; + +export const EventsTrSupplementContainer = styled.div.attrs(({ width }) => ({ + style: { + width: `${width! - TIMELINE_EVENT_DETAILS_OFFSET}px`, + }, +}))``; + export const EventsTrSupplement = styled.div.attrs(({ className = '' }) => ({ className: `siemEventsTable__trSupplement ${className}`, }))<{ className: string }>` @@ -200,11 +212,17 @@ export const EventsTdGroupData = styled.div.attrs(({ className = '' }) => ({ }))` display: flex; `; +interface WidthProp { + width?: number; +} -export const EventsTd = styled.div.attrs(({ className = '' }) => ({ +export const EventsTd = styled.div.attrs(({ className = '', width }) => ({ className: `siemEventsTable__td ${className}`, role: 'cell', -}))` + style: { + flexBasis: width ? `${width}px` : 'auto', + }, +}))` align-items: center; display: flex; flex-shrink: 0; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx index d66bc702bae43..ea4406311d7cc 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx @@ -14,20 +14,17 @@ import { mockBrowserFields } from '../../containers/source/mock'; import { Direction } from '../../graphql/types'; import { defaultHeaders, mockTimelineData, mockIndexPattern } from '../../mock'; import { TestProviders } from '../../mock/test_providers'; -import { flyoutHeaderHeight } from '../flyout'; import { DELETE_CLASS_NAME, ENABLE_CLASS_NAME, EXCLUDE_CLASS_NAME, } from './data_providers/provider_item_actions'; -import { TimelineComponent } from './timeline'; +import { TimelineComponent, Props as TimelineComponentProps } from './timeline'; import { Sort } from './body/sort'; import { mockDataProviders } from './data_providers/mock/mock_data_providers'; import { useMountAppended } from '../../utils/use_mount_appended'; -const testFlyoutHeight = 980; - jest.mock('../../lib/kibana'); const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; @@ -35,6 +32,7 @@ jest.mock('use-resize-observer/polyfilled'); mockUseResizeObserver.mockImplementation(() => ({})); describe('Timeline', () => { + let props = {} as TimelineComponentProps; const sort: Sort = { columnId: '@timestamp', sortDirection: Direction.desc, @@ -50,41 +48,44 @@ describe('Timeline', () => { const mount = useMountAppended(); + beforeEach(() => { + props = { + browserFields: mockBrowserFields, + columns: defaultHeaders, + id: 'foo', + dataProviders: mockDataProviders, + end: endDate, + eventType: 'raw' as TimelineComponentProps['eventType'], + filters: [], + indexPattern, + indexToAdd: [], + isLive: false, + itemsPerPage: 5, + itemsPerPageOptions: [5, 10, 20], + kqlMode: 'search' as TimelineComponentProps['kqlMode'], + kqlQueryExpression: '', + loadingIndexName: false, + onChangeDataProviderKqlQuery: jest.fn(), + onChangeDroppableAndProvider: jest.fn(), + onChangeItemsPerPage: jest.fn(), + onClose: jest.fn(), + onDataProviderEdited: jest.fn(), + onDataProviderRemoved: jest.fn(), + onToggleDataProviderEnabled: jest.fn(), + onToggleDataProviderExcluded: jest.fn(), + show: true, + showCallOutUnauthorizedMsg: false, + start: startDate, + sort, + toggleColumn: jest.fn(), + usersViewing: ['elastic'], + }; + }); + describe('rendering', () => { test('renders correctly against snapshot', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); }); @@ -92,37 +93,7 @@ describe('Timeline', () => { const wrapper = mount( - + ); @@ -130,41 +101,28 @@ describe('Timeline', () => { expect(wrapper.find('[data-test-subj="timelineHeader"]').exists()).toEqual(true); }); + test('it renders the title field', () => { + const wrapper = mount( + + + + + + ); + + expect( + wrapper + .find('[data-test-subj="timeline-title"]') + .first() + .props().placeholder + ).toContain('Untitled Timeline'); + }); + test('it renders the timeline table', () => { const wrapper = mount( - + ); @@ -176,37 +134,7 @@ describe('Timeline', () => { const wrapper = mount( - + ); @@ -218,36 +146,7 @@ describe('Timeline', () => { const wrapper = mount( - + ); @@ -261,42 +160,10 @@ describe('Timeline', () => { describe('event wire up', () => { describe('onDataProviderRemoved', () => { test('it invokes the onDataProviderRemoved callback when the delete button on a provider is clicked', () => { - const mockOnDataProviderRemoved = jest.fn(); - const wrapper = mount( - + ); @@ -306,46 +173,16 @@ describe('Timeline', () => { .first() .simulate('click'); - expect(mockOnDataProviderRemoved.mock.calls[0][0]).toEqual('id-Provider 1'); + expect((props.onDataProviderRemoved as jest.Mock).mock.calls[0][0]).toEqual( + 'id-Provider 1' + ); }); test('it invokes the onDataProviderRemoved callback when you click on the option "Delete" in the provider menu', () => { - const mockOnDataProviderRemoved = jest.fn(); - const wrapper = mount( - + ); @@ -361,48 +198,18 @@ describe('Timeline', () => { .first() .simulate('click'); - expect(mockOnDataProviderRemoved.mock.calls[0][0]).toEqual('id-Provider 1'); + expect((props.onDataProviderRemoved as jest.Mock).mock.calls[0][0]).toEqual( + 'id-Provider 1' + ); }); }); describe('onToggleDataProviderEnabled', () => { test('it invokes the onToggleDataProviderEnabled callback when you click on the option "Temporary disable" in the provider menu', () => { - const mockOnToggleDataProviderEnabled = jest.fn(); - const wrapper = mount( - + ); @@ -419,7 +226,7 @@ describe('Timeline', () => { .first() .simulate('click'); - expect(mockOnToggleDataProviderEnabled.mock.calls[0][0]).toEqual({ + expect((props.onToggleDataProviderEnabled as jest.Mock).mock.calls[0][0]).toEqual({ providerId: 'id-Provider 1', enabled: false, }); @@ -428,42 +235,10 @@ describe('Timeline', () => { describe('onToggleDataProviderExcluded', () => { test('it invokes the onToggleDataProviderExcluded callback when you click on the option "Exclude results" in the provider menu', () => { - const mockOnToggleDataProviderExcluded = jest.fn(); - const wrapper = mount( - + ); @@ -482,7 +257,7 @@ describe('Timeline', () => { .first() .simulate('click'); - expect(mockOnToggleDataProviderExcluded.mock.calls[0][0]).toEqual({ + expect((props.onToggleDataProviderExcluded as jest.Mock).mock.calls[0][0]).toEqual({ providerId: 'id-Provider 1', excluded: true, }); @@ -490,44 +265,14 @@ describe('Timeline', () => { }); describe('#ProviderWithAndProvider', () => { - test('Rendering And Provider', () => { - const dataProviders = mockDataProviders.slice(0, 1); - dataProviders[0].and = mockDataProviders.slice(1, 3); + const dataProviders = mockDataProviders.slice(0, 1); + dataProviders[0].and = mockDataProviders.slice(1, 3); + test('Rendering And Provider', () => { const wrapper = mount( - + ); @@ -544,44 +289,10 @@ describe('Timeline', () => { }); test('it invokes the onDataProviderRemoved callback when you click on the option "Delete" in the accordion menu', () => { - const dataProviders = mockDataProviders.slice(0, 1); - dataProviders[0].and = mockDataProviders.slice(1, 3); - const mockOnDataProviderRemoved = jest.fn(); - const wrapper = mount( - + ); @@ -600,48 +311,17 @@ describe('Timeline', () => { .first() .simulate('click'); - expect(mockOnDataProviderRemoved.mock.calls[0]).toEqual(['id-Provider 1', 'id-Provider 2']); + expect((props.onDataProviderRemoved as jest.Mock).mock.calls[0]).toEqual([ + 'id-Provider 1', + 'id-Provider 2', + ]); }); test('it invokes the onToggleDataProviderEnabled callback when you click on the option "Temporary disable" in the accordion menu', () => { - const dataProviders = mockDataProviders.slice(0, 1); - dataProviders[0].and = mockDataProviders.slice(1, 3); - const mockOnToggleDataProviderEnabled = jest.fn(); - const wrapper = mount( - + ); @@ -660,7 +340,7 @@ describe('Timeline', () => { .first() .simulate('click'); - expect(mockOnToggleDataProviderEnabled.mock.calls[0][0]).toEqual({ + expect((props.onToggleDataProviderEnabled as jest.Mock).mock.calls[0][0]).toEqual({ andProviderId: 'id-Provider 2', enabled: false, providerId: 'id-Provider 1', @@ -668,44 +348,10 @@ describe('Timeline', () => { }); test('it invokes the onToggleDataProviderExcluded callback when you click on the option "Exclude results" in the accordion menu', () => { - const dataProviders = mockDataProviders.slice(0, 1); - dataProviders[0].and = mockDataProviders.slice(1, 3); - const mockOnToggleDataProviderExcluded = jest.fn(); - const wrapper = mount( - + ); @@ -724,7 +370,7 @@ describe('Timeline', () => { .first() .simulate('click'); - expect(mockOnToggleDataProviderExcluded.mock.calls[0][0]).toEqual({ + expect((props.onToggleDataProviderExcluded as jest.Mock).mock.calls[0][0]).toEqual({ andProviderId: 'id-Provider 2', excluded: true, providerId: 'id-Provider 1', diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx index 58bbbef328ddf..098dd82791610 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup } from '@elastic/eui'; +import { EuiFlyoutHeader, EuiFlyoutBody, EuiFlyoutFooter } from '@elastic/eui'; import { getOr, isEmpty } from 'lodash/fp'; -import React from 'react'; +import React, { useMemo } from 'react'; import styled from 'styled-components'; -import useResizeObserver from 'use-resize-observer/polyfilled'; +import { FlyoutHeaderWithCloseButton } from '../flyout/header_with_close_button'; import { BrowserFields } from '../../containers/source'; import { TimelineQuery } from '../../containers/timeline'; import { Direction } from '../../graphql/types'; @@ -31,38 +31,60 @@ import { import { TimelineKqlFetch } from './fetch_kql_timeline'; import { Footer, footerHeight } from './footer'; import { TimelineHeader } from './header'; -import { calculateBodyHeight, combineQueries } from './helpers'; +import { combineQueries } from './helpers'; import { TimelineRefetch } from './refetch_timeline'; import { ManageTimelineContext } from './timeline_context'; import { esQuery, Filter, IIndexPattern } from '../../../../../../../src/plugins/data/public'; -const WrappedByAutoSizer = styled.div` +const TimelineContainer = styled.div` + height: 100%; + display: flex; + flex-direction: column; +`; + +const TimelineHeaderContainer = styled.div` + margin-top: 6px; width: 100%; -`; // required by AutoSizer +`; -WrappedByAutoSizer.displayName = 'WrappedByAutoSizer'; +TimelineHeaderContainer.displayName = 'TimelineHeaderContainer'; -const TimelineContainer = styled(EuiFlexGroup)` - min-height: 500px; - overflow: hidden; - padding: 0 10px 0 12px; - user-select: none; - width: 100%; +const StyledEuiFlyoutHeader = styled(EuiFlyoutHeader)` + align-items: center; + box-shadow: none; + display: flex; + flex-direction: column; + padding: 14px 10px 0 12px; `; -TimelineContainer.displayName = 'TimelineContainer'; +const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` + overflow-y: hidden; + flex: 1; -export const isCompactFooter = (width: number): boolean => width < 600; + .euiFlyoutBody__overflow { + overflow: hidden; + mask-image: none; + } -interface Props { + .euiFlyoutBody__overflowContent { + padding: 0 10px 0 12px; + height: 100%; + display: flex; + } +`; + +const StyledEuiFlyoutFooter = styled(EuiFlyoutFooter)` + background: none; + padding: 0 10px 5px 12px; +`; + +export interface Props { browserFields: BrowserFields; columns: ColumnHeaderOptions[]; dataProviders: DataProvider[]; end: number; eventType?: EventType; filters: Filter[]; - flyoutHeaderHeight: number; - flyoutHeight: number; id: string; indexPattern: IIndexPattern; indexToAdd: string[]; @@ -75,6 +97,7 @@ interface Props { onChangeDataProviderKqlQuery: OnChangeDataProviderKqlQuery; onChangeDroppableAndProvider: OnChangeDroppableAndProvider; onChangeItemsPerPage: OnChangeItemsPerPage; + onClose: () => void; onDataProviderEdited: OnDataProviderEdited; onDataProviderRemoved: OnDataProviderRemoved; onToggleDataProviderEnabled: OnToggleDataProviderEnabled; @@ -84,6 +107,7 @@ interface Props { start: number; sort: Sort; toggleColumn: (column: ColumnHeaderOptions) => void; + usersViewing: string[]; } /** The parent Timeline component */ @@ -94,8 +118,6 @@ export const TimelineComponent: React.FC = ({ end, eventType, filters, - flyoutHeaderHeight, - flyoutHeight, id, indexPattern, indexToAdd, @@ -108,6 +130,7 @@ export const TimelineComponent: React.FC = ({ onChangeDataProviderKqlQuery, onChangeDroppableAndProvider, onChangeItemsPerPage, + onClose, onDataProviderEdited, onDataProviderRemoved, onToggleDataProviderEnabled, @@ -117,10 +140,8 @@ export const TimelineComponent: React.FC = ({ start, sort, toggleColumn, + usersViewing, }) => { - const { ref: measureRef, width = 0, height: timelineHeaderHeight = 0 } = useResizeObserver< - HTMLDivElement - >({}); const kibana = useKibana(); const combinedQueries = combineQueries({ config: esQuery.getEsQueryConfig(kibana.services.uiSettings), @@ -134,45 +155,51 @@ export const TimelineComponent: React.FC = ({ end, }); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; + const timelineQueryFields = useMemo(() => columnsHeader.map(c => c.id), [columnsHeader]); + const timelineQuerySortField = useMemo( + () => ({ + sortFieldId: sort.columnId, + direction: sort.sortDirection as Direction, + }), + [sort.columnId, sort.sortDirection] + ); return ( - - }> - + + - + + + + {combinedQueries != null ? ( c.id)} + fields={timelineQueryFields} sourceId="default" limit={itemsPerPage} filterQuery={combinedQueries.filterQuery} - sortField={{ - sortFieldId: sort.columnId, - direction: sort.sortDirection as Direction, - }} + sortField={timelineQuerySortField} > {({ events, @@ -184,7 +211,7 @@ export const TimelineComponent: React.FC = ({ getUpdatedAt, refetch, }) => ( - + = ({ loading={loading} refetch={refetch} /> - -