Merge remote-tracking branch 'upstream/6.7' into 6.7
|
@ -25,6 +25,8 @@ If you are running our https://cloud.elastic.co[hosted Elasticsearch Service]
|
|||
on Elastic Cloud, you can access Kibana with a single click.
|
||||
--
|
||||
|
||||
include::getting-started/add-sample-data.asciidoc[]
|
||||
|
||||
include::getting-started/tutorial-sample-data.asciidoc[]
|
||||
|
||||
include::getting-started/tutorial-sample-filter.asciidoc[]
|
||||
|
|
31
docs/getting-started/add-sample-data.asciidoc
Normal file
|
@ -0,0 +1,31 @@
|
|||
[[add-sample-data]]
|
||||
== Get up and running with sample data
|
||||
|
||||
{kib} has three sample data sets that you can use to explore {kib} before loading your own data
|
||||
source. Each set is prepackaged with a dashboard of visualizations and a
|
||||
{kibana-ref}/canvas-getting-started.html[Canvas workpad].
|
||||
|
||||
The sample data sets address common use cases:
|
||||
|
||||
* *eCommerce orders* includes visualizations for product-related information,
|
||||
such as cost, revenue, and price.
|
||||
* *Web logs* lets you analyze website traffic.
|
||||
* *Flight data* enables you to view and interact with flight routes for four airlines.
|
||||
|
||||
To get started, go to the home page and click the link next to *Add sample data*.
|
||||
|
||||
Once you have loaded a data set, click *View data* to view visualizations in *Dashboard*.
|
||||
|
||||
*Note:* The timestamps in the sample data sets are relative to when they are installed.
|
||||
If you uninstall and reinstall a data set, the timestamps will change to reflect the most recent installation.
|
||||
|
||||
|
||||
[role="screenshot"]
|
||||
image::images/add-sample-data.png[]
|
||||
|
||||
[float]
|
||||
==== Next steps
|
||||
|
||||
Play with the sample flight data in the {kibana-ref}/tutorial-sample-data.html[flight dashboard tutorial].
|
||||
|
||||
Learn how to load data, define index patterns and build visualizations by {kibana-ref}/tutorial-build-dashboard.html[building your own dashboard].
|
BIN
docs/images/add-sample-data.png
Normal file
After Width: | Height: | Size: 660 KiB |
BIN
docs/images/management-upgrade-assistant-7.0.png
Normal file → Executable file
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 246 KiB |
|
@ -44,6 +44,8 @@ include::canvas.asciidoc[]
|
|||
|
||||
include::ml/index.asciidoc[]
|
||||
|
||||
include::maps/index.asciidoc[]
|
||||
|
||||
include::infrastructure/index.asciidoc[]
|
||||
|
||||
include::logs/index.asciidoc[]
|
||||
|
@ -84,4 +86,4 @@ include::migration.asciidoc[]
|
|||
|
||||
include::CHANGELOG.asciidoc[]
|
||||
|
||||
include::redirects.asciidoc[]
|
||||
include::redirects.asciidoc[]
|
||||
|
|
|
@ -7,8 +7,46 @@ To access the assistant, go to *Management > 7.0 Upgrade Assistant*.
|
|||
The assistant identifies the deprecated settings in your cluster and indices
|
||||
and guides you through the process of resolving issues, including reindexing.
|
||||
|
||||
Before upgrading to {es} 7.0, make sure that you are using the final 6.x minor
|
||||
Before upgrading to {es} 7.0, make sure that you are using the final 6.7.x minor
|
||||
release to see the most up-to-date deprecation issues.
|
||||
|
||||
[float]
|
||||
=== Reindexing
|
||||
|
||||
The *Indices* page lists the indices that are incompatible with the next
|
||||
major version of {es}. You can initiate a reindex to resolve the issues.
|
||||
|
||||
For example, an index created in 5.x that uses the `_all` meta field is no
|
||||
longer supported in 7.0. The Upgrade Assistant removes this field during the
|
||||
reindex.
|
||||
|
||||
[role="screenshot"]
|
||||
image::images/management-upgrade-assistant-7.0.png[]
|
||||
|
||||
|
||||
For a preview of how the data will change during the reindex, select the
|
||||
index name. A warning appears if the index requires destructive changes.
|
||||
Back up your index, then proceed with the reindex by accepting each breaking change.
|
||||
|
||||
You can follow the progress as the Upgrade Assistant makes the index read-only,
|
||||
creates a new index, reindexes the documents, and creates an alias that points
|
||||
from the old index to the new one.
|
||||
|
||||
If the reindexing fails or is cancelled, the changes are rolled back,
|
||||
the new index is deleted, and the original index becomes writable. An error
|
||||
message explains the reason for the failure.
|
||||
|
||||
You can reindex multiple indices at a time, but keep an eye on the
|
||||
{es} metrics, including CPU usage, memory pressure, and disk usage. If a metric
|
||||
is so high it affects query performance, cancel the reindex and continue
|
||||
by reindexing fewer indices at a time.
|
||||
|
||||
Additional considerations:
|
||||
|
||||
* During a reindex of a Watcher (`.watches`) index, the Watcher process pauses and no alerts
|
||||
are triggered.
|
||||
|
||||
* During a reindex of a Machine Learning (`.ml-state`) index, the Machine Learning
|
||||
job pauses. Models are not trained or updated.
|
||||
|
||||
|
||||
|
|
17
docs/maps/heatmap-layer.asciidoc
Normal file
|
@ -0,0 +1,17 @@
|
|||
[[heatmap-layer]]
|
||||
== Heat map layer
|
||||
|
||||
In the heat map layer, point data is clustered to show locations with higher densities.
|
||||
|
||||
[role="screenshot"]
|
||||
image::maps/images/heatmap_layer.png[]
|
||||
|
||||
You can create a heat map layer from the following data source:
|
||||
|
||||
*Grid aggregation*:: Geospatial data grouped in grids with metrics for each gridded cell.
|
||||
Set *Show as* to *heat map*.
|
||||
The index must contain at least one field mapped as {ref}/geo-point.html[geo_point].
|
||||
|
||||
NOTE: Only count and sum metric aggregations are available with the grid aggregation source and heat map layers.
|
||||
Mean, median, min, and max are turned off because the heat map will blend nearby values.
|
||||
Blending two average values would make the cluster more prominent, even though it just might literally mean that these nearby areas are average.
|
BIN
docs/maps/images/heatmap_layer.png
Normal file
After Width: | Height: | Size: 350 KiB |
BIN
docs/maps/images/sample_data_ecommerce.png
Normal file
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/maps/images/terms_join.png
Normal file
After Width: | Height: | Size: 849 KiB |
BIN
docs/maps/images/terms_join_metric_config.png
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
docs/maps/images/terms_join_shared_key_config.png
Normal file
After Width: | Height: | Size: 121 KiB |
BIN
docs/maps/images/terms_join_tooltip.png
Normal file
After Width: | Height: | Size: 111 KiB |
BIN
docs/maps/images/tile_layer.png
Normal file
After Width: | Height: | Size: 752 KiB |
BIN
docs/maps/images/vector_layer.png
Normal file
After Width: | Height: | Size: 467 KiB |
BIN
docs/maps/images/vector_style_dynamic.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
docs/maps/images/vector_style_static.png
Normal file
After Width: | Height: | Size: 8.9 KiB |
18
docs/maps/index.asciidoc
Normal file
|
@ -0,0 +1,18 @@
|
|||
[[maps]]
|
||||
= Maps
|
||||
|
||||
[partintro]
|
||||
--
|
||||
|
||||
beta[]
|
||||
|
||||
The **Maps** application enables you to parse through your geographical data at scale, with speed, and in real time. With features like multiple layers and indices in a map, plotting of raw documents, dynamic client-side styling, and global search across multiple layers, you can understand and monitor your data with ease.
|
||||
|
||||
[role="screenshot"]
|
||||
image::maps/images/sample_data_ecommerce.png[]
|
||||
|
||||
--
|
||||
|
||||
include::heatmap-layer.asciidoc[]
|
||||
include::tile-layer.asciidoc[]
|
||||
include::vector-layer.asciidoc[]
|
94
docs/maps/terms-join.asciidoc
Normal file
|
@ -0,0 +1,94 @@
|
|||
[[terms-join]]
|
||||
=== Terms join
|
||||
|
||||
Terms joins use a shared key to combine the results of an Elasticsearch terms aggregation and vector features.
|
||||
You can augment vector features with property values that symbolize features and provide richer tooltip content.
|
||||
|
||||
[role="screenshot"]
|
||||
image::maps/images/terms_join.png[]
|
||||
|
||||
Follow the example below to understand how *Terms joins* work.
|
||||
This example uses Elastic Maps Service(EMS) World Countries as the vector source and
|
||||
the Kibana sample data set "Sample web logs" as the Elasticsearch index.
|
||||
|
||||
Example feature from World Countries:
|
||||
--------------------------------------------------
|
||||
{
|
||||
geometry: {
|
||||
coordinates: [...],
|
||||
type: "Polygon"
|
||||
},
|
||||
properties: {
|
||||
name: "Sweden",
|
||||
iso2: "SE",
|
||||
iso3: "SWE"
|
||||
},
|
||||
type: "Feature"
|
||||
}
|
||||
--------------------------------------------------
|
||||
|
||||
Example documents from Sample web logs:
|
||||
--------------------------------------------------
|
||||
{
|
||||
bytes: 1837,
|
||||
geo: {
|
||||
src: "SE"
|
||||
},
|
||||
timestamp: "Feb 28, 2019 @ 07:23:08.754"
|
||||
},
|
||||
{
|
||||
bytes: 971,
|
||||
geo: {
|
||||
src: "SE"
|
||||
},
|
||||
timestamp: "Feb 27, 2019 @ 08:10:45.205"
|
||||
},
|
||||
{
|
||||
bytes: 4277,
|
||||
geo: {
|
||||
src: "SE"
|
||||
},
|
||||
timestamp: "Feb 21, 2019 @ 05:24:33.945"
|
||||
},
|
||||
{
|
||||
bytes: 5624,
|
||||
geo: {
|
||||
src: "SE"
|
||||
},
|
||||
timestamp: "Feb 21, 2019 @ 04:57:05.921"
|
||||
}
|
||||
--------------------------------------------------
|
||||
|
||||
The JOIN configuration links the vector source "World Countries" to the Elasticsearch index "kibana_sample_data_logs"
|
||||
on the shared key *iso2 = geo.src*.
|
||||
[role="screenshot"]
|
||||
image::maps/images/terms_join_shared_key_config.png[]
|
||||
|
||||
The METRICS configuration defines two metric aggregations:
|
||||
the count of all documents in the terms bucket and
|
||||
the average of the field "bytes" for all documents in the terms bucket.
|
||||
[role="screenshot"]
|
||||
image::maps/images/terms_join_metric_config.png[]
|
||||
|
||||
Example terms aggregation response:
|
||||
--------------------------------------------------
|
||||
{
|
||||
aggregations: {
|
||||
join: {
|
||||
buckets: [
|
||||
{
|
||||
doc_count: 4,
|
||||
key: "SE",
|
||||
avg_of_bytes: {
|
||||
value: 3177.25
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
--------------------------------------------------
|
||||
|
||||
Finally, the terms aggregation response is joined with the vector features.
|
||||
[role="screenshot"]
|
||||
image::maps/images/terms_join_tooltip.png[]
|
18
docs/maps/tile-layer.asciidoc
Normal file
|
@ -0,0 +1,18 @@
|
|||
[[tile-layer]]
|
||||
== Tile layer
|
||||
|
||||
The tile layer displays image tiles served from a tile server.
|
||||
|
||||
[role="screenshot"]
|
||||
image::maps/images/tile_layer.png[]
|
||||
|
||||
You can create a tile layer from the following data sources:
|
||||
|
||||
*Custom Tile Map Service*:: Map tiles configured in kibana.yml.
|
||||
See map.tilemap.url in <<settings>> for details.
|
||||
|
||||
*Tiles*:: Map tiles from https://www.elastic.co/elastic-maps-service[Elastic Maps Service].
|
||||
|
||||
*Tile Map Service from URL*:: Map tiles from a URL that includes the XYZ coordinates.
|
||||
|
||||
*Web Map Service*:: Maps from OGC Standard WMS.
|
24
docs/maps/vector-layer.asciidoc
Normal file
|
@ -0,0 +1,24 @@
|
|||
[[vector-layer]]
|
||||
== Vector layer
|
||||
|
||||
The vector layer displays points, lines, and polygons.
|
||||
|
||||
[role="screenshot"]
|
||||
image::maps/images/vector_layer.png[]
|
||||
|
||||
You can create a vector layer from the following sources:
|
||||
|
||||
*Custom vector shapes*:: Vector shapes from static files configured in kibana.yml.
|
||||
See map.regionmap.* in <<settings>> for details.
|
||||
|
||||
*Documents*:: Geospatial data from a Kibana index pattern.
|
||||
The index must contain at least one field mapped as {ref}/geo-point.html[geo_point] or {ref}/geo-shape.html[geo_shape].
|
||||
|
||||
*Grid aggregation*:: Geospatial data grouped in grids with metrics for each gridded cell.
|
||||
Set *Show as* to *grid rectangles* or *points*.
|
||||
The index must contain at least one field mapped as {ref}/geo-point.html[geo_point].
|
||||
|
||||
*Vector shapes*:: Vector shapes of administrative boundaries from https://www.elastic.co/elastic-maps-service[Elastic Maps Service].
|
||||
|
||||
include::terms-join.asciidoc[]
|
||||
include::vector-style.asciidoc[]
|
20
docs/maps/vector-style.asciidoc
Normal file
|
@ -0,0 +1,20 @@
|
|||
[[vector-style]]
|
||||
=== Vector style
|
||||
|
||||
*Border color*:: Defines the border color of the vector features.
|
||||
|
||||
*Border width*:: Defines the border width of the vector features.
|
||||
|
||||
*Fill color*:: Defines the fill color of the vector features.
|
||||
|
||||
*Symbol size*:: Defines the symbol size of point features.
|
||||
|
||||
Click the *link* button to toggle between static styling and data-driven styling.
|
||||
|
||||
[role="screenshot"]
|
||||
image::maps/images/vector_style_static.png[]
|
||||
|
||||
[role="screenshot"]
|
||||
image::maps/images/vector_style_dynamic.png[]
|
||||
|
||||
NOTE: The *link* button is only available when your vector features contain numeric properties.
|
|
@ -10,8 +10,8 @@
|
|||
|
||||
`xpack.infra.sources.default.fields.tiebreaker`:: Field used to break ties between two entries with the same timestamp. Defaults to `_doc`.
|
||||
|
||||
`xpack.infra.sources.default.fields.host`:: Field used to identify hosts. Defaults to `host.name`.
|
||||
`xpack.infra.sources.default.fields.host`:: Field used to identify hosts. Defaults to `beat.hostname`.
|
||||
|
||||
`xpack.infra.sources.default.fields.container`:: Field used to identify Docker containers. Defaults to `container.id`.
|
||||
`xpack.infra.sources.default.fields.container`:: Field used to identify Docker containers. Defaults to `docker.container.name`.
|
||||
|
||||
`xpack.infra.sources.default.fields.pod`:: Field used to identify Kubernetes pods. Defaults to `kubernetes.pod.uid`.
|
||||
`xpack.infra.sources.default.fields.pod`:: Field used to identify Kubernetes pods. Defaults to `kubernetes.pod.name`.
|
||||
|
|
11
package.json
|
@ -89,7 +89,7 @@
|
|||
"url": "https://github.com/elastic/kibana.git"
|
||||
},
|
||||
"resolutions": {
|
||||
"**/@types/node": "10.12.12",
|
||||
"**/@types/node": "10.12.27",
|
||||
"@types/react": "16.3.14"
|
||||
},
|
||||
"workspaces": {
|
||||
|
@ -110,7 +110,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@elastic/datemath": "5.0.2",
|
||||
"@elastic/eui": "6.10.3",
|
||||
"@elastic/eui": "6.10.4",
|
||||
"@elastic/filesaver": "1.1.2",
|
||||
"@elastic/good": "8.1.1-kibana2",
|
||||
"@elastic/numeral": "2.3.2",
|
||||
|
@ -289,7 +289,6 @@
|
|||
"@types/eslint": "^4.16.2",
|
||||
"@types/execa": "^0.9.0",
|
||||
"@types/fetch-mock": "7.2.1",
|
||||
"@types/json5": "^0.0.30",
|
||||
"@types/getopts": "^2.0.0",
|
||||
"@types/glob": "^5.0.35",
|
||||
"@types/globby": "^8.0.0",
|
||||
|
@ -302,17 +301,19 @@
|
|||
"@types/joi": "^13.4.2",
|
||||
"@types/jquery": "^3.3.6",
|
||||
"@types/js-yaml": "^3.11.1",
|
||||
"@types/json5": "^0.0.30",
|
||||
"@types/listr": "^0.13.0",
|
||||
"@types/lodash": "^3.10.1",
|
||||
"@types/minimatch": "^2.0.29",
|
||||
"@types/moment-timezone": "^0.5.8",
|
||||
"@types/mustache": "^0.8.31",
|
||||
"@types/node": "^10.12.12",
|
||||
"@types/node": "^10.12.27",
|
||||
"@types/opn": "^5.1.0",
|
||||
"@types/podium": "^1.0.0",
|
||||
"@types/prop-types": "^15.5.3",
|
||||
"@types/puppeteer-core": "^1.9.0",
|
||||
"@types/react": "16.3.14",
|
||||
"@types/react-datepicker": "1.8.0",
|
||||
"@types/react-dom": "^16.0.5",
|
||||
"@types/react-redux": "^6.0.6",
|
||||
"@types/react-router-dom": "^4.3.1",
|
||||
|
@ -427,4 +428,4 @@
|
|||
"node": "10.15.2",
|
||||
"yarn": "^1.10.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
"@types/log-symbols": "^2.0.0",
|
||||
"@types/mkdirp": "^0.5.2",
|
||||
"@types/ncp": "^2.0.1",
|
||||
"@types/node": "^10.12.12",
|
||||
"@types/node": "^10.12.27",
|
||||
"@types/ora": "^1.3.2",
|
||||
"@types/read-pkg": "^3.0.0",
|
||||
"@types/strip-ansi": "^3.0.0",
|
||||
|
|
|
@ -115,7 +115,6 @@ export const schema = Joi.object().keys({
|
|||
junit: Joi.object().keys({
|
||||
enabled: Joi.boolean().default(!!process.env.CI),
|
||||
reportName: Joi.string(),
|
||||
rootDirectory: Joi.string(),
|
||||
}).default(),
|
||||
|
||||
mochaReporter: Joi.object().keys({
|
||||
|
|
|
@ -54,7 +54,6 @@ export function MochaReporterProvider({ getService }) {
|
|||
if (config.get('junit.enabled') && config.get('junit.reportName')) {
|
||||
setupJUnitReportGeneration(runner, {
|
||||
reportName: config.get('junit.reportName'),
|
||||
rootDirectory: config.get('junit.rootDirectory')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,5 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export const SECURITY_AUTH_MESSAGE = 'Authentication failed';
|
||||
export const API_ROUTE = '/api/canvas';
|
||||
|
|
|
@ -19,12 +19,6 @@
|
|||
|
||||
import expect from 'expect.js';
|
||||
import { createHandlers } from '../create_handlers';
|
||||
import { SECURITY_AUTH_MESSAGE } from '../../../common/constants';
|
||||
|
||||
let securityMode = 'pass';
|
||||
let isSecurityAvailable = true;
|
||||
let isSecurityEnabled = true;
|
||||
const authError = new Error('auth error');
|
||||
|
||||
const mockRequest = {
|
||||
headers: 'i can haz headers',
|
||||
|
@ -32,26 +26,11 @@ const mockRequest = {
|
|||
|
||||
const mockServer = {
|
||||
plugins: {
|
||||
security: {
|
||||
authenticate: () => ({
|
||||
succeeded: () => (securityMode === 'pass' ? true : false),
|
||||
error: securityMode === 'pass' ? null : authError,
|
||||
}),
|
||||
},
|
||||
elasticsearch: {
|
||||
getCluster: () => ({
|
||||
callWithRequest: (...args) => Promise.resolve(args),
|
||||
}),
|
||||
},
|
||||
// TODO: remove this when we use the method exposed by security https://github.com/elastic/kibana/pull/24616
|
||||
xpack_main: {
|
||||
info: {
|
||||
feature: () => ({
|
||||
isAvailable: () => isSecurityAvailable,
|
||||
isEnabled: () => isSecurityEnabled,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
config: () => ({
|
||||
has: () => false,
|
||||
|
@ -63,16 +42,9 @@ const mockServer = {
|
|||
};
|
||||
|
||||
describe('server createHandlers', () => {
|
||||
let handlers;
|
||||
|
||||
beforeEach(() => {
|
||||
securityMode = 'pass';
|
||||
isSecurityEnabled = true;
|
||||
isSecurityAvailable = true;
|
||||
handlers = createHandlers(mockRequest, mockServer);
|
||||
});
|
||||
|
||||
it('provides helper methods and properties', () => {
|
||||
const handlers = createHandlers(mockRequest, mockServer);
|
||||
|
||||
expect(handlers).to.have.property('environment', 'server');
|
||||
expect(handlers).to.have.property('serverUri');
|
||||
expect(handlers).to.have.property('elasticsearchClient');
|
||||
|
@ -80,78 +52,7 @@ describe('server createHandlers', () => {
|
|||
|
||||
describe('elasticsearchClient', () => {
|
||||
it('executes callWithRequest', async () => {
|
||||
const [request, endpoint, payload] = await handlers.elasticsearchClient(
|
||||
'endpoint',
|
||||
'payload'
|
||||
);
|
||||
expect(request).to.equal(mockRequest);
|
||||
expect(endpoint).to.equal('endpoint');
|
||||
expect(payload).to.equal('payload');
|
||||
});
|
||||
|
||||
it('rejects when authentication check fails', () => {
|
||||
securityMode = 'fail';
|
||||
return handlers
|
||||
.elasticsearchClient('endpoint', 'payload')
|
||||
.then(() => {
|
||||
throw new Error('elasticsearchClient should fail when authentication fails');
|
||||
})
|
||||
.catch(err => {
|
||||
expect(err.message).to.be.equal(SECURITY_AUTH_MESSAGE);
|
||||
});
|
||||
});
|
||||
|
||||
it('works without security plugin in kibana', async () => {
|
||||
// create server without security plugin
|
||||
const mockServerClone = {
|
||||
...mockServer,
|
||||
plugins: { ...mockServer.plugins },
|
||||
};
|
||||
delete mockServerClone.plugins.security;
|
||||
expect(mockServer.plugins).to.have.property('security'); // confirm original server object
|
||||
expect(mockServerClone.plugins).to.not.have.property('security');
|
||||
|
||||
// this shouldn't do anything
|
||||
securityMode = 'fail';
|
||||
|
||||
// make sure the method still works
|
||||
handlers = createHandlers(mockRequest, mockServerClone);
|
||||
const [request, endpoint, payload] = await handlers.elasticsearchClient(
|
||||
'endpoint',
|
||||
'payload'
|
||||
);
|
||||
expect(request).to.equal(mockRequest);
|
||||
expect(endpoint).to.equal('endpoint');
|
||||
expect(payload).to.equal('payload');
|
||||
});
|
||||
|
||||
it('works without security available', async () => {
|
||||
// create server with security unavailable (i.e. when user is on a basic license)
|
||||
isSecurityAvailable = false;
|
||||
|
||||
// this shouldn't do anything
|
||||
securityMode = 'fail';
|
||||
|
||||
// make sure the method still works
|
||||
handlers = createHandlers(mockRequest, mockServer);
|
||||
const [request, endpoint, payload] = await handlers.elasticsearchClient(
|
||||
'endpoint',
|
||||
'payload'
|
||||
);
|
||||
expect(request).to.equal(mockRequest);
|
||||
expect(endpoint).to.equal('endpoint');
|
||||
expect(payload).to.equal('payload');
|
||||
});
|
||||
|
||||
it('works with security disabled in elasticsearch', async () => {
|
||||
// create server with security disabled
|
||||
isSecurityEnabled = false;
|
||||
|
||||
// this shouldn't do anything
|
||||
securityMode = 'fail';
|
||||
|
||||
// make sure the method still works
|
||||
handlers = createHandlers(mockRequest, mockServer);
|
||||
const handlers = createHandlers(mockRequest, mockServer);
|
||||
const [request, endpoint, payload] = await handlers.elasticsearchClient(
|
||||
'endpoint',
|
||||
'payload'
|
||||
|
|
|
@ -17,10 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import boom from 'boom';
|
||||
import { isSecurityEnabled } from './feature_check';
|
||||
import { SECURITY_AUTH_MESSAGE } from '../../common/constants';
|
||||
|
||||
export const createHandlers = (request, server) => {
|
||||
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data');
|
||||
const config = server.config();
|
||||
|
@ -31,27 +27,6 @@ export const createHandlers = (request, server) => {
|
|||
config.has('server.rewriteBasePath') && config.get('server.rewriteBasePath')
|
||||
? `${server.info.uri}${config.get('server.basePath')}`
|
||||
: server.info.uri,
|
||||
elasticsearchClient: async (...args) => {
|
||||
// check if the session is valid because continuing to use it
|
||||
if (isSecurityEnabled(server)) {
|
||||
try {
|
||||
const authenticationResult = await server.plugins.security.authenticate(request);
|
||||
if (!authenticationResult.succeeded()) {
|
||||
throw boom.unauthorized(authenticationResult.error);
|
||||
}
|
||||
} catch (e) {
|
||||
// if authenticate throws, show error in development
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
e.message = `elasticsearchClient failed: ${e.message}`;
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
// hide all failure information from the user
|
||||
throw boom.unauthorized(SECURITY_AUTH_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
return callWithRequest(request, ...args);
|
||||
},
|
||||
elasticsearchClient: async (...args) => callWithRequest(request, ...args),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,26 +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.
|
||||
*/
|
||||
|
||||
// TODO: replace this when we use the method exposed by security https://github.com/elastic/kibana/pull/24616
|
||||
export const isSecurityEnabled = server => {
|
||||
const kibanaSecurity = server.plugins.security;
|
||||
const esSecurity = server.plugins.xpack_main.info.feature('security');
|
||||
|
||||
return kibanaSecurity && esSecurity.isAvailable() && esSecurity.isEnabled();
|
||||
};
|
|
@ -1,46 +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 { createError } from '@kbn/interpreter/common';
|
||||
|
||||
export const routeExpressionProvider = environments => {
|
||||
async function routeExpression(ast, context = null) {
|
||||
// List of environments in order of preference
|
||||
|
||||
return Promise.all(environments).then(async environments => {
|
||||
const environmentFunctions = await Promise.all(environments.map(env => env.getFunctions()));
|
||||
|
||||
// Grab name of the first function in the chain
|
||||
const fnName = ast.chain[0].function.toLowerCase();
|
||||
|
||||
// Check each environment for that function
|
||||
for (let i = 0; i < environmentFunctions.length; i++) {
|
||||
if (environmentFunctions[i].includes(fnName)) {
|
||||
// If we find it, run in that environment, and only that environment
|
||||
return environments[i].interpret(ast, context).catch(e => createError(e));
|
||||
}
|
||||
}
|
||||
|
||||
// If the function isn't found in any environment, give up
|
||||
throw new Error(`Function not found: [${fnName}]`);
|
||||
});
|
||||
}
|
||||
|
||||
return routeExpression;
|
||||
};
|
|
@ -1,38 +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 { interpreterProvider } from '@kbn/interpreter/common';
|
||||
import { createHandlers } from '../create_handlers';
|
||||
|
||||
export const server = async ({ server, request }) => {
|
||||
const { serverFunctions, types } = server.plugins.interpreter.registries();
|
||||
|
||||
return {
|
||||
interpret: (ast, context) => {
|
||||
const interpret = interpreterProvider({
|
||||
types: types.toJS(),
|
||||
functions: serverFunctions.toJS(),
|
||||
handlers: createHandlers(request, server),
|
||||
});
|
||||
|
||||
return interpret(ast, context);
|
||||
},
|
||||
getFunctions: () => Object.keys(serverFunctions.toJS()),
|
||||
};
|
||||
};
|
|
@ -17,10 +17,8 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { translate } from './translate';
|
||||
import { registerServerFunctions } from './server_functions';
|
||||
|
||||
export function routes(server) {
|
||||
translate(server);
|
||||
registerServerFunctions(server);
|
||||
}
|
||||
|
|
|
@ -1,48 +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 { fromExpression, toExpression } from '@kbn/interpreter/common';
|
||||
|
||||
export function translate(server) {
|
||||
/*
|
||||
Get AST from expression
|
||||
*/
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/api/canvas/ast',
|
||||
handler: function (request, h) {
|
||||
if (!request.query.expression) {
|
||||
return h.response({ error: '"expression" query is required' }).code(400);
|
||||
}
|
||||
return fromExpression(request.query.expression);
|
||||
},
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: '/api/canvas/expression',
|
||||
handler: function (request, h) {
|
||||
try {
|
||||
return toExpression(request.payload);
|
||||
} catch (e) {
|
||||
return h.response({ error: e.message }).code(400);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
|
@ -44,6 +44,7 @@ export const IndexPattern = props => {
|
|||
const dropBucketName = `${prefix}drop_last_bucket`;
|
||||
|
||||
const defaults = {
|
||||
default_index_pattern: '',
|
||||
[indexPatternName]: '*',
|
||||
[intervalName]: 'auto',
|
||||
[dropBucketName]: 1
|
||||
|
@ -62,11 +63,16 @@ export const IndexPattern = props => {
|
|||
id="tsvb.indexPatternLabel"
|
||||
defaultMessage="Index pattern"
|
||||
/>)}
|
||||
helpText={(model.default_index_pattern && !model[indexPatternName] && <FormattedMessage
|
||||
id="tsvb.indexPattern.searchByDefaultIndex"
|
||||
defaultMessage="Default index pattern is used. To query all indexes use *"
|
||||
/>)}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFieldText
|
||||
data-test-subj="metricsIndexPatternInput"
|
||||
disabled={props.disabled}
|
||||
placeholder={model.default_index_pattern}
|
||||
onChange={handleTextChange(indexPatternName, '*')}
|
||||
value={model[indexPatternName]}
|
||||
fullWidth
|
||||
|
|
|
@ -93,13 +93,12 @@ class VisEditor extends Component {
|
|||
}
|
||||
|
||||
setDefaultIndexPattern = async () => {
|
||||
if (this.props.vis.params.index_pattern === '') {
|
||||
// set the default index pattern if none is defined.
|
||||
const savedObjectsClient = chrome.getSavedObjectsClient();
|
||||
const indexPattern = await savedObjectsClient.get('index-pattern', this.getConfig('defaultIndex'));
|
||||
const defaultIndexPattern = indexPattern.attributes.title;
|
||||
this.props.vis.params.index_pattern = defaultIndexPattern;
|
||||
}
|
||||
const savedObjectsClient = chrome.getSavedObjectsClient();
|
||||
const indexPattern = await savedObjectsClient.get('index-pattern', this.getConfig('defaultIndex'));
|
||||
|
||||
this.handleChange({
|
||||
default_index_pattern: indexPattern.attributes.title
|
||||
});
|
||||
}
|
||||
|
||||
handleChange = async (partialModel) => {
|
||||
|
|
|
@ -16,13 +16,16 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { uniq } from 'lodash';
|
||||
import { getIndexPatternObject } from './vis_data/helpers/get_index_pattern';
|
||||
|
||||
export async function getFields(req) {
|
||||
const indexPattern = req.query.index;
|
||||
const { indexPatternsService } = req.pre;
|
||||
const index = req.query.index || '*';
|
||||
const resp = await indexPatternsService.getFieldsForWildcard({ pattern: index });
|
||||
const { indexPatternString } = await getIndexPatternObject(req, indexPattern);
|
||||
const resp = await indexPatternsService.getFieldsForWildcard({ pattern: indexPatternString });
|
||||
const fields = resp.filter(field => field.aggregatable);
|
||||
|
||||
return uniq(fields, field => field.name);
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { get } from 'lodash';
|
||||
|
||||
export async function getIndexPatternObject(req, indexPatternString) {
|
||||
let defaultIndex;
|
||||
|
@ -48,6 +49,6 @@ export async function getIndexPatternObject(req, indexPatternString) {
|
|||
|
||||
return {
|
||||
indexPatternObject,
|
||||
indexPatternString: indexPatternString || indexPatternObject.title
|
||||
indexPatternString: indexPatternString || get(indexPatternObject, 'title', '')
|
||||
};
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import 'ui/autoload/modules';
|
||||
import 'ui/autoload/styles';
|
||||
import 'ui/i18n';
|
||||
import { uiModules } from 'ui/modules';
|
||||
|
|
|
@ -30,6 +30,7 @@ import '../storage';
|
|||
import '../directives/kbn_src';
|
||||
import '../watch_multi';
|
||||
import './services';
|
||||
import '../react_components';
|
||||
|
||||
import { initAngularApi } from './api/angular';
|
||||
import appsApi from './api/apps';
|
||||
|
|
|
@ -84,7 +84,7 @@ interface Props {
|
|||
dateRangeTo?: string;
|
||||
isRefreshPaused?: boolean;
|
||||
refreshInterval?: number;
|
||||
onRefreshChange?: (isPaused: boolean, refreshInterval: number) => void;
|
||||
onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
|
|
@ -24,6 +24,7 @@ import { uiModules } from '../../modules';
|
|||
import { VisFiltersProvider } from '../vis_filters';
|
||||
import { htmlIdGenerator, keyCodes } from '@elastic/eui';
|
||||
|
||||
export const CUSTOM_LEGEND_VIS_TYPES = ['heatmap', 'gauge'];
|
||||
|
||||
uiModules.get('kibana')
|
||||
.directive('vislibLegend', function (Private, $timeout, i18n) {
|
||||
|
@ -106,6 +107,9 @@ uiModules.get('kibana')
|
|||
};
|
||||
|
||||
$scope.canFilter = function (legendData) {
|
||||
if (CUSTOM_LEGEND_VIS_TYPES.includes($scope.vis.vislibVis.visConfigArgs.type)) {
|
||||
return false;
|
||||
}
|
||||
const filters = visFilters.filter({ datum: legendData, shallow: true }, { simulate: true });
|
||||
return filters.length;
|
||||
};
|
||||
|
@ -151,7 +155,7 @@ uiModules.get('kibana')
|
|||
$scope.open = $scope.vis.params.addLegend;
|
||||
}
|
||||
|
||||
if (['heatmap', 'gauge'].includes(vislibVis.visConfigArgs.type)) {
|
||||
if (CUSTOM_LEGEND_VIS_TYPES.includes(vislibVis.visConfigArgs.type)) {
|
||||
const labels = vislibVis.getLegendLabels();
|
||||
if (labels) {
|
||||
$scope.labels = _.map(labels, label => {
|
||||
|
|
|
@ -23,7 +23,7 @@ import 'plugins/kbn_vislib_vis_types/controls/line_interpolation_option';
|
|||
import 'plugins/kbn_vislib_vis_types/controls/heatmap_options';
|
||||
import 'plugins/kbn_vislib_vis_types/controls/gauge_options';
|
||||
import 'plugins/kbn_vislib_vis_types/controls/point_series';
|
||||
import './vislib_vis_legend';
|
||||
import { CUSTOM_LEGEND_VIS_TYPES } from './vislib_vis_legend';
|
||||
import { BaseVisTypeProvider } from './base_vis_type';
|
||||
import { AggResponsePointSeriesProvider } from '../../agg_response/point_series/point_series';
|
||||
import VislibProvider from '../../vislib';
|
||||
|
@ -70,6 +70,13 @@ export function VislibVisTypeProvider(Private, $rootScope, $timeout, $compile) {
|
|||
return resolve();
|
||||
}
|
||||
|
||||
this.vis.vislibVis = new vislib.Vis(this.chartEl, this.vis.params);
|
||||
this.vis.vislibVis.on('brush', this.vis.API.events.brush);
|
||||
this.vis.vislibVis.on('click', this.vis.API.events.filter);
|
||||
this.vis.vislibVis.on('renderComplete', resolve);
|
||||
|
||||
this.vis.vislibVis.initVisConfig(esResponse, this.vis.getUiState());
|
||||
|
||||
if (this.vis.params.addLegend) {
|
||||
$(this.container).attr('class', (i, cls) => {
|
||||
return cls.replace(/visLib--legend-\S+/g, '');
|
||||
|
@ -85,15 +92,16 @@ export function VislibVisTypeProvider(Private, $rootScope, $timeout, $compile) {
|
|||
this.$scope.$digest();
|
||||
}
|
||||
|
||||
this.vis.vislibVis = new vislib.Vis(this.chartEl, this.vis.params);
|
||||
this.vis.vislibVis.on('brush', this.vis.API.events.brush);
|
||||
this.vis.vislibVis.on('click', this.vis.API.events.filter);
|
||||
this.vis.vislibVis.on('renderComplete', resolve);
|
||||
this.vis.vislibVis.render(esResponse, this.vis.getUiState());
|
||||
|
||||
if (this.vis.params.addLegend) {
|
||||
// refreshing the legend after the chart is rendered.
|
||||
// this is necessary because some visualizations
|
||||
// provide data necessary for the legend only after a render cycle.
|
||||
if (this.vis.params.addLegend && CUSTOM_LEGEND_VIS_TYPES.includes(this.vis.vislibVis.visConfigArgs.type)) {
|
||||
this.$scope.refreshLegend++;
|
||||
this.$scope.$digest();
|
||||
|
||||
this.vis.vislibVis.render(esResponse, this.vis.getUiState());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -47,6 +47,15 @@ export function VislibVisProvider(Private) {
|
|||
hasLegend() {
|
||||
return this.visConfigArgs.addLegend;
|
||||
}
|
||||
|
||||
initVisConfig(data, uiState) {
|
||||
this.data = data;
|
||||
|
||||
this.uiState = uiState;
|
||||
|
||||
this.visConfig = new VisConfig(this.visConfigArgs, this.data, this.uiState, this.el);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the visualization
|
||||
*
|
||||
|
@ -63,11 +72,7 @@ export function VislibVisProvider(Private) {
|
|||
this._runOnHandler('destroy');
|
||||
}
|
||||
|
||||
this.data = data;
|
||||
|
||||
this.uiState = uiState;
|
||||
|
||||
this.visConfig = new VisConfig(this.visConfigArgs, this.data, this.uiState, this.el);
|
||||
this.initVisConfig(data, uiState);
|
||||
|
||||
this.handler = new Handler(this, this.visConfig);
|
||||
this._runOnHandler('render');
|
||||
|
|
|
@ -49,7 +49,6 @@ export function createJestConfig({
|
|||
"default",
|
||||
[`${kibanaDirectory}/src/dev/jest/junit_reporter.js`, {
|
||||
reportName: 'X-Pack Jest Tests',
|
||||
rootDirectory: xPackKibanaDirectory,
|
||||
}]
|
||||
],
|
||||
};
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
}
|
||||
},
|
||||
"resolutions": {
|
||||
"**/@types/node": "10.12.12",
|
||||
"**/@types/node": "10.12.27",
|
||||
"@types/react": "16.3.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -48,7 +48,7 @@
|
|||
"@types/pngjs": "^3.3.1",
|
||||
"@types/prop-types": "^15.5.3",
|
||||
"@types/react": "16.3.14",
|
||||
"@types/react-datepicker": "^1.1.5",
|
||||
"@types/react-datepicker": "1.8.0",
|
||||
"@types/react-dom": "^16.0.5",
|
||||
"@types/react-redux": "^6.0.6",
|
||||
"@types/react-router-dom": "^4.3.1",
|
||||
|
@ -119,7 +119,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@elastic/datemath": "5.0.2",
|
||||
"@elastic/eui": "6.10.3",
|
||||
"@elastic/eui": "6.10.4",
|
||||
"@elastic/node-crypto": "0.1.2",
|
||||
"@elastic/node-phantom-simple": "2.2.4",
|
||||
"@elastic/numeral": "2.3.2",
|
||||
|
|
|
@ -23,7 +23,7 @@ export const axisConfig = () => ({
|
|||
types: ['string'],
|
||||
help: 'Position of the axis labels - top, bottom, left, and right',
|
||||
options: ['top', 'bottom', 'left', 'right'],
|
||||
default: '',
|
||||
default: 'left',
|
||||
},
|
||||
min: {
|
||||
types: ['number', 'date', 'string', 'null'],
|
||||
|
|
|
@ -18,6 +18,6 @@ export const LOCALSTORAGE_AUTOCOMPLETE_ENABLED = `${LOCALSTORAGE_PREFIX}.isAutoc
|
|||
export const LOCALSTORAGE_LASTPAGE = 'canvas:lastpage';
|
||||
export const FETCH_TIMEOUT = 30000; // 30 seconds
|
||||
export const CANVAS_USAGE_TYPE = 'canvas';
|
||||
export const SECURITY_AUTH_MESSAGE = 'Authentication failed';
|
||||
export const DEFAULT_WORKPAD_CSS = '.canvasPage {\n\n}';
|
||||
export const VALID_IMAGE_TYPES = ['gif', 'jpeg', 'png', 'svg+xml'];
|
||||
export const ASSET_MAX_SIZE = 25000;
|
||||
|
|
|
@ -33,10 +33,11 @@ import { ConfirmModal } from '../confirm_modal';
|
|||
import { Clipboard } from '../clipboard';
|
||||
import { Download } from '../download';
|
||||
import { Loading } from '../loading';
|
||||
import { ASSET_MAX_SIZE } from '../../../common/lib/constants';
|
||||
|
||||
export class AssetManager extends React.PureComponent {
|
||||
static propTypes = {
|
||||
assets: PropTypes.array,
|
||||
assetValues: PropTypes.array,
|
||||
addImageElement: PropTypes.func,
|
||||
removeAsset: PropTypes.func.isRequired,
|
||||
copyAsset: PropTypes.func.isRequired,
|
||||
|
@ -147,15 +148,13 @@ export class AssetManager extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
const { isModalVisible, loading } = this.state;
|
||||
const { assets } = this.props;
|
||||
|
||||
const assetMaxLimit = 25000;
|
||||
const { assetValues } = this.props;
|
||||
|
||||
const assetsTotal = Math.round(
|
||||
assets.reduce((total, asset) => total + asset.value.length, 0) / 1024
|
||||
assetValues.reduce((total, { value }) => total + value.length, 0) / 1024
|
||||
);
|
||||
|
||||
const percentageUsed = Math.round((assetsTotal / assetMaxLimit) * 100);
|
||||
const percentageUsed = Math.round((assetsTotal / ASSET_MAX_SIZE) * 100);
|
||||
|
||||
const emptyAssets = (
|
||||
<EuiPanel className="canvasAssetManager__emptyPanel">
|
||||
|
@ -208,9 +207,9 @@ export class AssetManager extends React.PureComponent {
|
|||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer />
|
||||
{assets.length ? (
|
||||
{assetValues.length ? (
|
||||
<EuiFlexGrid responsive={false} columns={4}>
|
||||
{assets.map(this.renderAsset)}
|
||||
{assetValues.map(this.renderAsset)}
|
||||
</EuiFlexGrid>
|
||||
) : (
|
||||
emptyAssets
|
||||
|
@ -221,7 +220,7 @@ export class AssetManager extends React.PureComponent {
|
|||
<EuiFlexItem>
|
||||
<EuiProgress
|
||||
value={assetsTotal}
|
||||
max={assetMaxLimit}
|
||||
max={ASSET_MAX_SIZE}
|
||||
color={percentageUsed < 90 ? 'secondary' : 'danger'}
|
||||
size="s"
|
||||
aria-labelledby="CanvasAssetManagerLabel"
|
||||
|
|
|
@ -21,7 +21,7 @@ import { VALID_IMAGE_TYPES } from '../../../common/lib/constants';
|
|||
import { AssetManager as Component } from './asset_manager';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
assets: Object.values(getAssets(state)), // pull values out of assets object
|
||||
assets: getAssets(state),
|
||||
selectedPage: getSelectedPage(state),
|
||||
});
|
||||
|
||||
|
@ -60,19 +60,22 @@ const mapDispatchToProps = dispatch => ({
|
|||
});
|
||||
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
||||
const { assets } = stateProps;
|
||||
const { assets, selectedPage } = stateProps;
|
||||
const { onAssetAdd } = dispatchProps;
|
||||
const assetValues = Object.values(assets); // pull values out of assets object
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
...stateProps,
|
||||
...dispatchProps,
|
||||
selectedPage,
|
||||
assetValues,
|
||||
addImageElement: dispatchProps.addImageElement(stateProps.selectedPage),
|
||||
onAssetAdd: file => {
|
||||
const [type, subtype] = get(file, 'type', '').split('/');
|
||||
if (type === 'image' && VALID_IMAGE_TYPES.indexOf(subtype) >= 0) {
|
||||
return encode(file).then(dataurl => {
|
||||
const type = 'dataurl';
|
||||
const existingId = findExistingAsset(type, dataurl, assets);
|
||||
const existingId = findExistingAsset(type, dataurl, assetValues);
|
||||
if (existingId) {
|
||||
return existingId;
|
||||
}
|
||||
|
|
|
@ -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 { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiStat, EuiTitle } from '@elastic/eui';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
export const ElementConfig = ({ elementStats }) => {
|
||||
if (!elementStats) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { total, ready, error } = elementStats;
|
||||
const progress = total > 0 ? Math.round(((ready + error) / total) * 100) : 100;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiTitle size="xs">
|
||||
<h4>Elements</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiStat title={total} description="Total" titleSize="s" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiStat title={ready} description="Loaded" titleSize="s" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiStat title={error} description="Failed" titleSize="s" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiStat title={progress + '%'} description="Progress" titleSize="s" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
ElementConfig.propTypes = {
|
||||
elementStats: PropTypes.object,
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 { connect } from 'react-redux';
|
||||
import { getElementStats } from '../../state/selectors/workpad';
|
||||
import { ElementConfig as Component } from './element_config';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
elementStats: getElementStats(state),
|
||||
});
|
||||
|
||||
export const ElementConfig = connect(mapStateToProps)(Component);
|
|
@ -9,6 +9,15 @@ import PropTypes from 'prop-types';
|
|||
import { Shortcuts } from 'react-shortcuts';
|
||||
|
||||
export class FullscreenControl extends React.PureComponent {
|
||||
keyHandler = action => {
|
||||
const enterFullscreen = action === 'FULLSCREEN';
|
||||
const exitFullscreen = this.props.isFullscreen && action === 'FULLSCREEN_EXIT';
|
||||
|
||||
if (enterFullscreen || exitFullscreen) {
|
||||
this.toggleFullscreen();
|
||||
}
|
||||
};
|
||||
|
||||
toggleFullscreen = () => {
|
||||
const { setFullscreen, isFullscreen } = this.props;
|
||||
setFullscreen(!isFullscreen);
|
||||
|
@ -17,17 +26,11 @@ export class FullscreenControl extends React.PureComponent {
|
|||
render() {
|
||||
const { children, isFullscreen } = this.props;
|
||||
|
||||
const keyHandler = action => {
|
||||
if (action === 'FULLSCREEN' || (isFullscreen && action === 'FULLSCREEN_EXIT')) {
|
||||
this.toggleFullscreen();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Shortcuts
|
||||
name="PRESENTATION"
|
||||
handler={keyHandler}
|
||||
handler={this.keyHandler}
|
||||
targetNodeSelector="body"
|
||||
global
|
||||
isolate
|
||||
|
|
|
@ -8,12 +8,13 @@ import { connect } from 'react-redux';
|
|||
import { fetchAllRenderables } from '../../state/actions/elements';
|
||||
import { setRefreshInterval } from '../../state/actions/workpad';
|
||||
import { getInFlight } from '../../state/selectors/resolved_args';
|
||||
import { getRefreshInterval } from '../../state/selectors/workpad';
|
||||
import { getRefreshInterval, getElementStats } from '../../state/selectors/workpad';
|
||||
import { RefreshControl as Component } from './refresh_control';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
inFlight: getInFlight(state),
|
||||
refreshInterval: getRefreshInterval(state),
|
||||
elementStats: getElementStats(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
|
|
|
@ -4,10 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { EuiButtonEmpty, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
import { Popover } from '../popover';
|
||||
import { loadingIndicator } from '../../lib/loading_indicator';
|
||||
import { AutoRefreshControls } from './auto_refresh_controls';
|
||||
|
||||
const getRefreshInterval = (val = '') => {
|
||||
|
@ -36,19 +37,26 @@ const getRefreshInterval = (val = '') => {
|
|||
}
|
||||
};
|
||||
|
||||
export const RefreshControl = ({ inFlight, setRefreshInterval, refreshInterval, doRefresh }) => {
|
||||
export const RefreshControl = ({
|
||||
inFlight,
|
||||
elementStats,
|
||||
setRefreshInterval,
|
||||
refreshInterval,
|
||||
doRefresh,
|
||||
}) => {
|
||||
const { pending } = elementStats;
|
||||
|
||||
if (inFlight || pending > 0) {
|
||||
loadingIndicator.show();
|
||||
} else {
|
||||
loadingIndicator.hide();
|
||||
}
|
||||
|
||||
const setRefresh = val => setRefreshInterval(getRefreshInterval(val));
|
||||
|
||||
const popoverButton = handleClick => (
|
||||
<EuiButtonEmpty size="s" onClick={handleClick}>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
{inFlight && (
|
||||
<Fragment>
|
||||
<EuiLoadingSpinner size="m" />
|
||||
</Fragment>
|
||||
)}
|
||||
Refresh
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>Refresh</div>
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ElementConfig } from '../element_config';
|
||||
import { PageConfig } from '../page_config';
|
||||
import { WorkpadConfig } from '../workpad_config';
|
||||
import { SidebarSection } from './sidebar_section';
|
||||
|
@ -17,5 +18,8 @@ export const GlobalConfig = () => (
|
|||
<SidebarSection>
|
||||
<PageConfig />
|
||||
</SidebarSection>
|
||||
<SidebarSection>
|
||||
<ElementConfig />
|
||||
</SidebarSection>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { compose, withState, getContext, withHandlers } from 'recompose';
|
||||
import { pure, compose, withState, getContext, withHandlers } from 'recompose';
|
||||
|
||||
import {
|
||||
getWorkpad,
|
||||
|
@ -26,6 +26,7 @@ const mapStateToProps = state => ({
|
|||
});
|
||||
|
||||
export const Toolbar = compose(
|
||||
pure,
|
||||
connect(mapStateToProps),
|
||||
getContext({
|
||||
router: PropTypes.object,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { compose, withState, withProps, getContext, withHandlers } from 'recompose';
|
||||
import { pure, compose, withState, withProps, getContext, withHandlers } from 'recompose';
|
||||
import { transitionsRegistry } from '../../lib/transitions_registry';
|
||||
import { undoHistory, redoHistory } from '../../state/actions/history';
|
||||
import { fetchAllRenderables } from '../../state/actions/elements';
|
||||
|
@ -19,13 +19,19 @@ import {
|
|||
} from '../../state/selectors/workpad';
|
||||
import { Workpad as Component } from './workpad';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
pages: getPages(state),
|
||||
selectedPageNumber: getSelectedPageIndex(state) + 1,
|
||||
totalElementCount: getAllElements(state).length,
|
||||
workpad: getWorkpad(state),
|
||||
isFullscreen: getFullscreen(state),
|
||||
});
|
||||
const mapStateToProps = state => {
|
||||
const { width, height, id: workpadId, css: workpadCss } = getWorkpad(state);
|
||||
return {
|
||||
pages: getPages(state),
|
||||
selectedPageNumber: getSelectedPageIndex(state) + 1,
|
||||
totalElementCount: getAllElements(state).length,
|
||||
width,
|
||||
height,
|
||||
workpadCss,
|
||||
workpadId,
|
||||
isFullscreen: getFullscreen(state),
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
undoHistory,
|
||||
|
@ -34,6 +40,7 @@ const mapDispatchToProps = {
|
|||
};
|
||||
|
||||
export const Workpad = compose(
|
||||
pure,
|
||||
getContext({
|
||||
router: PropTypes.object,
|
||||
}),
|
||||
|
@ -68,17 +75,17 @@ export const Workpad = compose(
|
|||
}
|
||||
props.setPrevSelectedPageNumber(props.selectedPageNumber);
|
||||
const transitionPage = Math.max(props.selectedPageNumber, pageNumber) - 1;
|
||||
const { transition } = props.workpad.pages[transitionPage];
|
||||
const { transition } = props.pages[transitionPage];
|
||||
if (transition) {
|
||||
props.setTransition(transition);
|
||||
}
|
||||
props.router.navigateTo('loadWorkpad', { id: props.workpad.id, page: pageNumber });
|
||||
props.router.navigateTo('loadWorkpad', { id: props.workpadId, page: pageNumber });
|
||||
},
|
||||
}),
|
||||
withHandlers({
|
||||
onTransitionEnd: ({ setTransition }) => () => setTransition(null),
|
||||
nextPage: props => () => {
|
||||
const pageNumber = Math.min(props.selectedPageNumber + 1, props.workpad.pages.length);
|
||||
const pageNumber = Math.min(props.selectedPageNumber + 1, props.pages.length);
|
||||
props.onPageChange(pageNumber);
|
||||
},
|
||||
previousPage: props => () => {
|
||||
|
|
|
@ -10,33 +10,41 @@ import { Shortcuts } from 'react-shortcuts';
|
|||
import Style from 'style-it';
|
||||
import { WorkpadPage } from '../workpad_page';
|
||||
import { Fullscreen } from '../fullscreen';
|
||||
import { setDocTitle } from '../../lib/doc_title';
|
||||
|
||||
export const Workpad = props => {
|
||||
const {
|
||||
selectedPageNumber,
|
||||
getAnimation,
|
||||
onTransitionEnd,
|
||||
pages,
|
||||
totalElementCount,
|
||||
workpad,
|
||||
fetchAllRenderables,
|
||||
undoHistory,
|
||||
redoHistory,
|
||||
setGrid, // TODO: Get rid of grid when we improve the layout engine
|
||||
grid,
|
||||
nextPage,
|
||||
previousPage,
|
||||
isFullscreen,
|
||||
} = props;
|
||||
const WORKPAD_CANVAS_BUFFER = 32; // 32px padding around the workpad
|
||||
|
||||
const { height, width } = workpad;
|
||||
const bufferStyle = {
|
||||
height: isFullscreen ? height : height + 32,
|
||||
width: isFullscreen ? width : width + 32,
|
||||
export class Workpad extends React.PureComponent {
|
||||
static propTypes = {
|
||||
selectedPageNumber: PropTypes.number.isRequired,
|
||||
getAnimation: PropTypes.func.isRequired,
|
||||
onTransitionEnd: PropTypes.func.isRequired,
|
||||
grid: PropTypes.bool.isRequired,
|
||||
setGrid: PropTypes.func.isRequired,
|
||||
pages: PropTypes.array.isRequired,
|
||||
totalElementCount: PropTypes.number.isRequired,
|
||||
isFullscreen: PropTypes.bool.isRequired,
|
||||
width: PropTypes.number.isRequired,
|
||||
height: PropTypes.number.isRequired,
|
||||
workpadCss: PropTypes.string,
|
||||
undoHistory: PropTypes.func.isRequired,
|
||||
redoHistory: PropTypes.func.isRequired,
|
||||
nextPage: PropTypes.func.isRequired,
|
||||
previousPage: PropTypes.func.isRequired,
|
||||
fetchAllRenderables: PropTypes.func.isRequired,
|
||||
css: PropTypes.object,
|
||||
};
|
||||
|
||||
const keyHandler = action => {
|
||||
keyHandler = action => {
|
||||
const {
|
||||
fetchAllRenderables,
|
||||
undoHistory,
|
||||
redoHistory,
|
||||
nextPage,
|
||||
previousPage,
|
||||
grid, // TODO: Get rid of grid when we improve the layout engine
|
||||
setGrid,
|
||||
} = this.props;
|
||||
|
||||
// handle keypress events for editor and presentation events
|
||||
// this exists in both contexts
|
||||
if (action === 'REFRESH') {
|
||||
|
@ -63,84 +71,84 @@ export const Workpad = props => {
|
|||
}
|
||||
};
|
||||
|
||||
setDocTitle(workpad.name);
|
||||
render() {
|
||||
const {
|
||||
selectedPageNumber,
|
||||
getAnimation,
|
||||
onTransitionEnd,
|
||||
pages,
|
||||
totalElementCount,
|
||||
width,
|
||||
height,
|
||||
workpadCss,
|
||||
grid,
|
||||
isFullscreen,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className="canvasWorkpad__buffer" style={bufferStyle}>
|
||||
<div className="canvasCheckered" style={{ height, width }}>
|
||||
{!isFullscreen && (
|
||||
<Shortcuts name="EDITOR" handler={keyHandler} targetNodeSelector="body" global />
|
||||
)}
|
||||
const bufferStyle = {
|
||||
height: isFullscreen ? height : height + WORKPAD_CANVAS_BUFFER,
|
||||
width: isFullscreen ? width : width + WORKPAD_CANVAS_BUFFER,
|
||||
};
|
||||
|
||||
<Fullscreen>
|
||||
{({ isFullscreen, windowSize }) => {
|
||||
const scale = Math.min(windowSize.height / height, windowSize.width / width);
|
||||
const fsStyle = isFullscreen
|
||||
? {
|
||||
transform: `scale3d(${scale}, ${scale}, 1)`,
|
||||
WebkitTransform: `scale3d(${scale}, ${scale}, 1)`,
|
||||
msTransform: `scale3d(${scale}, ${scale}, 1)`,
|
||||
// height,
|
||||
// width,
|
||||
height: windowSize.height < height ? 'auto' : height,
|
||||
width: windowSize.width < width ? 'auto' : width,
|
||||
}
|
||||
: {};
|
||||
return (
|
||||
<div className="canvasWorkpad__buffer" style={bufferStyle}>
|
||||
<div className="canvasCheckered" style={{ height, width }}>
|
||||
{!isFullscreen && (
|
||||
<Shortcuts name="EDITOR" handler={this.keyHandler} targetNodeSelector="body" global />
|
||||
)}
|
||||
|
||||
// NOTE: the data-shared-* attributes here are used for reporting
|
||||
return Style.it(
|
||||
workpad.css,
|
||||
<div
|
||||
className={`canvasWorkpad ${isFullscreen ? 'fullscreen' : ''}`}
|
||||
style={fsStyle}
|
||||
data-shared-items-count={totalElementCount}
|
||||
>
|
||||
{isFullscreen && (
|
||||
<Shortcuts
|
||||
name="PRESENTATION"
|
||||
handler={keyHandler}
|
||||
targetNodeSelector="body"
|
||||
global
|
||||
/>
|
||||
)}
|
||||
{pages.map((page, i) => (
|
||||
<WorkpadPage
|
||||
key={page.id}
|
||||
page={page}
|
||||
height={height}
|
||||
width={width}
|
||||
isSelected={i + 1 === selectedPageNumber}
|
||||
animation={getAnimation(i + 1)}
|
||||
onAnimationEnd={onTransitionEnd}
|
||||
/>
|
||||
))}
|
||||
<Fullscreen>
|
||||
{({ isFullscreen, windowSize }) => {
|
||||
const scale = Math.min(windowSize.height / height, windowSize.width / width);
|
||||
const fsStyle = isFullscreen
|
||||
? {
|
||||
transform: `scale3d(${scale}, ${scale}, 1)`,
|
||||
WebkitTransform: `scale3d(${scale}, ${scale}, 1)`,
|
||||
msTransform: `scale3d(${scale}, ${scale}, 1)`,
|
||||
// height,
|
||||
// width,
|
||||
height: windowSize.height < height ? 'auto' : height,
|
||||
width: windowSize.width < width ? 'auto' : width,
|
||||
}
|
||||
: {};
|
||||
|
||||
// NOTE: the data-shared-* attributes here are used for reporting
|
||||
return Style.it(
|
||||
workpadCss,
|
||||
<div
|
||||
className="canvasGrid"
|
||||
style={{ height, width, display: grid ? 'block' : 'none' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Fullscreen>
|
||||
className={`canvasWorkpad ${isFullscreen ? 'fullscreen' : ''}`}
|
||||
style={fsStyle}
|
||||
data-shared-items-count={totalElementCount}
|
||||
>
|
||||
{isFullscreen && (
|
||||
<Shortcuts
|
||||
name="PRESENTATION"
|
||||
handler={this.keyHandler}
|
||||
targetNodeSelector="body"
|
||||
global
|
||||
/>
|
||||
)}
|
||||
{pages.map((page, i) => (
|
||||
<WorkpadPage
|
||||
key={page.id}
|
||||
page={page}
|
||||
height={height}
|
||||
width={width}
|
||||
isSelected={i + 1 === selectedPageNumber}
|
||||
animation={getAnimation(i + 1)}
|
||||
onAnimationEnd={onTransitionEnd}
|
||||
/>
|
||||
))}
|
||||
<div
|
||||
className="canvasGrid"
|
||||
style={{ height, width, display: grid ? 'block' : 'none' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Fullscreen>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Workpad.propTypes = {
|
||||
selectedPageNumber: PropTypes.number.isRequired,
|
||||
getAnimation: PropTypes.func.isRequired,
|
||||
onTransitionEnd: PropTypes.func.isRequired,
|
||||
grid: PropTypes.bool.isRequired,
|
||||
setGrid: PropTypes.func.isRequired,
|
||||
pages: PropTypes.array.isRequired,
|
||||
totalElementCount: PropTypes.number.isRequired,
|
||||
isFullscreen: PropTypes.bool.isRequired,
|
||||
workpad: PropTypes.object.isRequired,
|
||||
undoHistory: PropTypes.func.isRequired,
|
||||
redoHistory: PropTypes.func.isRequired,
|
||||
nextPage: PropTypes.func.isRequired,
|
||||
previousPage: PropTypes.func.isRequired,
|
||||
fetchAllRenderables: PropTypes.func.isRequired,
|
||||
css: PropTypes.object,
|
||||
};
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { compose, withState } from 'recompose';
|
||||
import { connect } from 'react-redux';
|
||||
import { canUserWrite } from '../../state/selectors/app';
|
||||
import { getWorkpadName, getSelectedPage, isWriteable } from '../../state/selectors/workpad';
|
||||
import { getSelectedPage, isWriteable } from '../../state/selectors/workpad';
|
||||
import { setWriteable } from '../../state/actions/workpad';
|
||||
import { addElement } from '../../state/actions/elements';
|
||||
import { WorkpadHeader as Component } from './workpad_header';
|
||||
|
@ -15,7 +15,6 @@ import { WorkpadHeader as Component } from './workpad_header';
|
|||
const mapStateToProps = state => ({
|
||||
isWriteable: isWriteable(state) && canUserWrite(state),
|
||||
canUserWrite: canUserWrite(state),
|
||||
workpadName: getWorkpadName(state),
|
||||
selectedPage: getSelectedPage(state),
|
||||
});
|
||||
|
||||
|
@ -33,10 +32,10 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => ({
|
|||
});
|
||||
|
||||
export const WorkpadHeader = compose(
|
||||
withState('showElementModal', 'setShowElementModal', false),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
mergeProps
|
||||
)
|
||||
),
|
||||
withState('showElementModal', 'setShowElementModal', false)
|
||||
)(Component);
|
||||
|
|
|
@ -23,122 +23,134 @@ import { WorkpadExport } from '../workpad_export';
|
|||
import { FullscreenControl } from '../fullscreen_control';
|
||||
import { RefreshControl } from '../refresh_control';
|
||||
|
||||
export const WorkpadHeader = ({
|
||||
isWriteable,
|
||||
canUserWrite,
|
||||
toggleWriteable,
|
||||
addElement,
|
||||
setShowElementModal,
|
||||
showElementModal,
|
||||
}) => {
|
||||
const keyHandler = action => {
|
||||
export class WorkpadHeader extends React.PureComponent {
|
||||
static propTypes = {
|
||||
isWriteable: PropTypes.bool,
|
||||
toggleWriteable: PropTypes.func,
|
||||
addElement: PropTypes.func.isRequired,
|
||||
showElementModal: PropTypes.bool,
|
||||
setShowElementModal: PropTypes.func,
|
||||
};
|
||||
|
||||
fullscreenButton = ({ toggleFullscreen }) => (
|
||||
<EuiToolTip position="bottom" content="Enter fullscreen mode">
|
||||
<EuiButtonIcon
|
||||
iconType="fullScreen"
|
||||
aria-label="View fullscreen"
|
||||
onClick={toggleFullscreen}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
|
||||
keyHandler = action => {
|
||||
if (action === 'EDITING') {
|
||||
toggleWriteable();
|
||||
this.props.toggleWriteable();
|
||||
}
|
||||
};
|
||||
|
||||
const elementAdd = (
|
||||
<EuiOverlayMask>
|
||||
<EuiModal
|
||||
onClose={() => setShowElementModal(false)}
|
||||
className="canvasModal--fixedSize"
|
||||
maxWidth="1000px"
|
||||
initialFocus=".canvasElements__filter"
|
||||
>
|
||||
<ElementTypes
|
||||
onClick={element => {
|
||||
addElement(element);
|
||||
setShowElementModal(false);
|
||||
}}
|
||||
/>
|
||||
<EuiModalFooter>
|
||||
<EuiButton size="s" onClick={() => setShowElementModal(false)}>
|
||||
Dismiss
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
elementAdd = () => {
|
||||
const { addElement, setShowElementModal } = this.props;
|
||||
|
||||
let readOnlyToolTip = '';
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiModal
|
||||
onClose={() => setShowElementModal(false)}
|
||||
className="canvasModal--fixedSize"
|
||||
maxWidth="1000px"
|
||||
initialFocus=".canvasElements__filter"
|
||||
>
|
||||
<ElementTypes
|
||||
onClick={element => {
|
||||
addElement(element);
|
||||
setShowElementModal(false);
|
||||
}}
|
||||
/>
|
||||
<EuiModalFooter>
|
||||
<EuiButton size="s" onClick={() => setShowElementModal(false)}>
|
||||
Dismiss
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
};
|
||||
|
||||
if (!canUserWrite) {
|
||||
readOnlyToolTip = "You don't have permission to edit this workpad";
|
||||
} else {
|
||||
readOnlyToolTip = isWriteable ? 'Hide editing controls' : 'Show editing controls';
|
||||
}
|
||||
getTooltipText = () => {
|
||||
if (!this.props.canUserWrite) {
|
||||
return "You don't have permission to edit this workpad";
|
||||
} else {
|
||||
return this.props.isWriteable ? 'Hide editing controls' : 'Show editing controls';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showElementModal ? elementAdd : null}
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<RefreshControl />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<FullscreenControl>
|
||||
{({ toggleFullscreen }) => (
|
||||
<EuiToolTip position="bottom" content="Enter fullscreen mode">
|
||||
<EuiButtonIcon
|
||||
iconType="fullScreen"
|
||||
aria-label="View fullscreen"
|
||||
onClick={toggleFullscreen}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
)}
|
||||
</FullscreenControl>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<WorkpadExport />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{canUserWrite && (
|
||||
<Shortcuts name="EDITOR" handler={keyHandler} targetNodeSelector="body" global />
|
||||
)}
|
||||
<EuiToolTip position="bottom" content={readOnlyToolTip}>
|
||||
<EuiButtonIcon
|
||||
iconType={isWriteable ? 'lockOpen' : 'lock'}
|
||||
onClick={() => {
|
||||
toggleWriteable();
|
||||
}}
|
||||
size="s"
|
||||
aria-label={readOnlyToolTip}
|
||||
isDisabled={!canUserWrite}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
{isWriteable ? (
|
||||
render() {
|
||||
const {
|
||||
isWriteable,
|
||||
canUserWrite,
|
||||
toggleWriteable,
|
||||
setShowElementModal,
|
||||
showElementModal,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showElementModal ? this.elementAdd() : null}
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<AssetManager />
|
||||
<RefreshControl />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
size="s"
|
||||
iconType="vector"
|
||||
onClick={() => setShowElementModal(true)}
|
||||
>
|
||||
Add element
|
||||
</EuiButton>
|
||||
<FullscreenControl>{this.fullscreenButton}</FullscreenControl>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<WorkpadExport />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{canUserWrite && (
|
||||
<Shortcuts
|
||||
name="EDITOR"
|
||||
handler={this.keyHandler}
|
||||
targetNodeSelector="body"
|
||||
global
|
||||
/>
|
||||
)}
|
||||
<EuiToolTip position="bottom" content={this.getTooltipText()}>
|
||||
<EuiButtonIcon
|
||||
iconType={isWriteable ? 'lockOpen' : 'lock'}
|
||||
onClick={() => {
|
||||
toggleWriteable();
|
||||
}}
|
||||
size="s"
|
||||
aria-label={this.getTooltipText()}
|
||||
isDisabled={!canUserWrite}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
WorkpadHeader.propTypes = {
|
||||
isWriteable: PropTypes.bool,
|
||||
toggleWriteable: PropTypes.func,
|
||||
addElement: PropTypes.func.isRequired,
|
||||
showElementModal: PropTypes.bool,
|
||||
setShowElementModal: PropTypes.func,
|
||||
};
|
||||
{isWriteable ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<AssetManager />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
size="s"
|
||||
iconType="vector"
|
||||
onClick={() => setShowElementModal(true)}
|
||||
>
|
||||
Add element
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
25
x-pack/plugins/canvas/public/lib/loading_indicator.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
import { loadingCount } from 'ui/chrome';
|
||||
|
||||
let isActive = false;
|
||||
|
||||
export const loadingIndicator = {
|
||||
show: () => {
|
||||
if (!isActive) {
|
||||
loadingCount.increment();
|
||||
isActive = true;
|
||||
}
|
||||
},
|
||||
hide: () => {
|
||||
if (isActive) {
|
||||
loadingCount.decrement();
|
||||
isActive = false;
|
||||
}
|
||||
},
|
||||
};
|
|
@ -186,7 +186,11 @@ export const fetchAllRenderables = createThunk(
|
|||
fetchElementsOnPages([currentPage]).then(() => dispatch(args.inFlightComplete()));
|
||||
} else {
|
||||
fetchElementsOnPages([currentPage])
|
||||
.then(() => fetchElementsOnPages(otherPages))
|
||||
.then(() => {
|
||||
return otherPages.reduce((chain, page) => {
|
||||
return chain.then(() => fetchElementsOnPages([page]));
|
||||
}, Promise.resolve());
|
||||
})
|
||||
.then(() => dispatch(args.inFlightComplete()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,3 +10,4 @@ export const setCanUserWrite = createAction('setCanUserWrite');
|
|||
export const setFullscreen = createAction('setFullscreen');
|
||||
export const selectElement = createAction('selectElement');
|
||||
export const setFirstLoad = createAction('setFirstLoad');
|
||||
export const setElementStats = createAction('setElementStats');
|
||||
|
|
|
@ -14,6 +14,12 @@ export const getInitialState = path => {
|
|||
transient: {
|
||||
isFirstLoad: true,
|
||||
canUserWrite: true,
|
||||
elementStats: {
|
||||
total: 0,
|
||||
ready: 0,
|
||||
pending: 0,
|
||||
error: 0,
|
||||
},
|
||||
fullscreen: false,
|
||||
selectedElement: null,
|
||||
resolvedArgs: {},
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { setElementStats } from '../actions/transient';
|
||||
import { getAllElements, getElementCounts, getElementStats } from '../selectors/workpad';
|
||||
|
||||
export const elementStats = ({ dispatch, getState }) => next => action => {
|
||||
// execute the action
|
||||
next(action);
|
||||
|
||||
// read the new state
|
||||
const state = getState();
|
||||
|
||||
const stats = getElementStats(state);
|
||||
const total = getAllElements(state).length;
|
||||
const counts = getElementCounts(state);
|
||||
const { ready, error } = counts;
|
||||
|
||||
// TODO: this should come from getElementStats, once we've gotten element status fixed
|
||||
const pending = total - ready - error;
|
||||
|
||||
if (
|
||||
total > 0 &&
|
||||
(ready !== stats.ready ||
|
||||
pending !== stats.pending ||
|
||||
error !== stats.error ||
|
||||
total !== stats.total)
|
||||
) {
|
||||
dispatch(setElementStats({ total, ready, pending, error }));
|
||||
}
|
||||
};
|
|
@ -15,10 +15,12 @@ import { inFlight } from './in_flight';
|
|||
import { workpadUpdate } from './workpad_update';
|
||||
import { workpadRefresh } from './workpad_refresh';
|
||||
import { appReady } from './app_ready';
|
||||
import { elementStats } from './element_stats';
|
||||
|
||||
const middlewares = [
|
||||
applyMiddleware(
|
||||
thunkMiddleware,
|
||||
elementStats,
|
||||
esPersistMiddleware,
|
||||
historyMiddleware,
|
||||
aeroelastic,
|
||||
|
|
|
@ -7,14 +7,21 @@
|
|||
import { duplicatePage } from '../actions/pages';
|
||||
import { fetchRenderable } from '../actions/elements';
|
||||
import { setWriteable } from '../actions/workpad';
|
||||
import { getPages, isWriteable } from '../selectors/workpad';
|
||||
import { getPages, getWorkpadName, isWriteable } from '../selectors/workpad';
|
||||
import { getWindow } from '../../lib/get_window';
|
||||
import { setDocTitle } from '../../lib/doc_title';
|
||||
|
||||
export const workpadUpdate = ({ dispatch, getState }) => next => action => {
|
||||
const oldIsWriteable = isWriteable(getState());
|
||||
const oldName = getWorkpadName(getState());
|
||||
|
||||
next(action);
|
||||
|
||||
// This middleware updates the page title when the workpad name changes
|
||||
if (getWorkpadName(getState()) !== oldName) {
|
||||
setDocTitle(getWorkpadName(getState()));
|
||||
}
|
||||
|
||||
// This middleware fetches all of the renderable elements on new, duplicate page
|
||||
if (action.type === duplicatePage.toString()) {
|
||||
// When a page has been duplicated, it will be added as the last page, so fetch it
|
||||
|
@ -22,17 +29,16 @@ export const workpadUpdate = ({ dispatch, getState }) => next => action => {
|
|||
const newPage = pages[pages.length - 1];
|
||||
|
||||
// For each element on that page, dispatch the action to update it
|
||||
return newPage.elements.forEach(element => dispatch(fetchRenderable(element)));
|
||||
newPage.elements.forEach(element => dispatch(fetchRenderable(element)));
|
||||
}
|
||||
|
||||
// This middleware clears any page selection when the writeable mode changes
|
||||
if (action.type === setWriteable.toString() && oldIsWriteable !== isWriteable(getState())) {
|
||||
const win = getWindow();
|
||||
|
||||
if (typeof win.getSelection !== 'function') {
|
||||
return;
|
||||
// check for browser feature before using it
|
||||
if (typeof win.getSelection === 'function') {
|
||||
win.getSelection().collapse(document.querySelector('body'), 0);
|
||||
}
|
||||
|
||||
win.getSelection().collapse(document.querySelector('body'), 0);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -10,6 +10,7 @@ import { get } from 'lodash';
|
|||
import { prepend } from '../../lib/modify_path';
|
||||
import * as actions from '../actions/resolved_args';
|
||||
import { flushContext, flushContextAfterIndex } from '../actions/elements';
|
||||
import { setWorkpad } from '../actions/workpad';
|
||||
|
||||
/*
|
||||
Resolved args are a way to handle async values. They track the status, value, and error
|
||||
|
@ -47,6 +48,7 @@ function _getValue(hasError, value, oldVal) {
|
|||
}
|
||||
|
||||
function getContext(value, loading = false, oldVal = null) {
|
||||
// TODO: this is no longer correct.
|
||||
const hasError = value instanceof Error;
|
||||
return {
|
||||
state: _getState(hasError, loading),
|
||||
|
@ -129,6 +131,9 @@ export const resolvedArgsReducer = handleActions(
|
|||
return state;
|
||||
}, transientState);
|
||||
},
|
||||
[setWorkpad]: (transientState, {}) => {
|
||||
return set(transientState, 'resolvedArgs', {});
|
||||
},
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
|
|
@ -40,6 +40,10 @@ export const transientReducer = handleActions(
|
|||
return set(transientState, 'fullscreen', Boolean(payload));
|
||||
},
|
||||
|
||||
[actions.setElementStats]: (transientState, { payload }) => {
|
||||
return set(transientState, 'elementStats', payload);
|
||||
},
|
||||
|
||||
[actions.selectElement]: (transientState, { payload }) => {
|
||||
return {
|
||||
...transientState,
|
||||
|
|
|
@ -79,6 +79,41 @@ export function getAllElements(state) {
|
|||
return getPages(state).reduce((elements, page) => elements.concat(page.elements), []);
|
||||
}
|
||||
|
||||
export function getElementCounts(state) {
|
||||
const resolvedArgs = get(state, 'transient.resolvedArgs');
|
||||
const results = {
|
||||
ready: 0,
|
||||
pending: 0,
|
||||
error: 0,
|
||||
};
|
||||
|
||||
Object.keys(resolvedArgs).forEach(resolvedArg => {
|
||||
const arg = resolvedArgs[resolvedArg];
|
||||
const { expressionRenderable } = arg;
|
||||
|
||||
if (!expressionRenderable) {
|
||||
results.pending++;
|
||||
return;
|
||||
}
|
||||
|
||||
const { value, state } = expressionRenderable;
|
||||
|
||||
if (value && value.as === 'error') {
|
||||
results.error++;
|
||||
} else if (state === 'ready') {
|
||||
results.ready++;
|
||||
} else {
|
||||
results.pending++;
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export function getElementStats(state) {
|
||||
return get(state, 'transient.elementStats');
|
||||
}
|
||||
|
||||
export function getGlobalFilterExpression(state) {
|
||||
return getAllElements(state)
|
||||
.map(el => el.filter)
|
||||
|
|
|
@ -22,7 +22,10 @@ export function crossClusterReplication(kibana) {
|
|||
injectDefaultVars(server) {
|
||||
const config = server.config();
|
||||
return {
|
||||
ccrUiEnabled: config.get('xpack.ccr.ui.enabled'),
|
||||
ccrUiEnabled: (
|
||||
config.get('xpack.ccr.ui.enabled')
|
||||
&& config.get('xpack.remote_clusters.ui.enabled')
|
||||
),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
@ -38,7 +41,13 @@ export function crossClusterReplication(kibana) {
|
|||
enabled: Joi.boolean().default(true),
|
||||
}).default();
|
||||
},
|
||||
|
||||
isEnabled(config) {
|
||||
return (
|
||||
config.get('xpack.ccr.enabled') &&
|
||||
config.get('index_management.enabled') &&
|
||||
config.get('xpack.remote_clusters.enabled')
|
||||
);
|
||||
},
|
||||
init: function initCcrPlugin(server) {
|
||||
registerLicenseChecker(server);
|
||||
registerRoutes(server);
|
||||
|
|
|
@ -38,7 +38,6 @@ export const tableState = handleActions({
|
|||
return {
|
||||
...state,
|
||||
showSystemIndices,
|
||||
toggleNameToVisibleMap: {}
|
||||
};
|
||||
},
|
||||
[toggleChanged](state, action) {
|
||||
|
|
|
@ -58,6 +58,8 @@ export interface InfraSourceFields {
|
|||
container: string;
|
||||
/** The fields to identify a host by */
|
||||
host: string;
|
||||
/** The fields to use as the log message */
|
||||
message: string[];
|
||||
/** The field to identify a pod by */
|
||||
pod: string;
|
||||
/** The field to use as a tiebreaker for log events that have identical timestamps */
|
||||
|
@ -875,6 +877,8 @@ export namespace SourceFields {
|
|||
|
||||
host: string;
|
||||
|
||||
message: string[];
|
||||
|
||||
pod: string;
|
||||
|
||||
tiebreaker: string;
|
||||
|
|
|
@ -15,6 +15,7 @@ const initialConfiguration: InfraSourceConfiguration = {
|
|||
fields: {
|
||||
container: 'INITIAL_CONTAINER_FIELD',
|
||||
host: 'INITIAL_HOST_FIELD',
|
||||
message: ['INITIAL_MESSAGE_FIELD'],
|
||||
pod: 'INITIAL_POD_FIELD',
|
||||
tiebreaker: 'INITIAL_TIEBREAKER_FIELD',
|
||||
timestamp: 'INITIAL_TIMESTAMP_FIELD',
|
||||
|
@ -163,6 +164,7 @@ describe('infrastructure source configuration', () => {
|
|||
expect(updateConfiguration(initialConfiguration)).toEqual({
|
||||
...initialConfiguration,
|
||||
fields: {
|
||||
...initialConfiguration.fields,
|
||||
container: 'CHANGED_CONTAINER',
|
||||
host: 'CHANGED_HOST',
|
||||
pod: 'CHANGED_POD',
|
||||
|
|
|
@ -6,32 +6,36 @@
|
|||
|
||||
import { InfraSourceConfiguration, UpdateSourceInput } from './graphql/types';
|
||||
|
||||
export const convertChangeToUpdater = (change: UpdateSourceInput) => (
|
||||
configuration: InfraSourceConfiguration
|
||||
): InfraSourceConfiguration => {
|
||||
const updaters: Array<(c: InfraSourceConfiguration) => InfraSourceConfiguration> = [
|
||||
c => (change.setName ? { ...c, name: change.setName.name } : c),
|
||||
c => (change.setDescription ? { ...c, description: change.setDescription.description } : c),
|
||||
export const convertChangeToUpdater = (change: UpdateSourceInput) => <
|
||||
C extends InfraSourceConfiguration
|
||||
>(
|
||||
configuration: C
|
||||
): C => {
|
||||
const updaters: Array<(c: C) => C> = [
|
||||
c => (change.setName ? Object.assign({}, c, { name: change.setName.name }) : c),
|
||||
c =>
|
||||
change.setDescription
|
||||
? Object.assign({}, c, { description: change.setDescription.description })
|
||||
: c,
|
||||
c =>
|
||||
change.setAliases
|
||||
? {
|
||||
...c,
|
||||
? Object.assign({}, c, {
|
||||
metricAlias: defaultTo(c.metricAlias, change.setAliases.metricAlias),
|
||||
logAlias: defaultTo(c.logAlias, change.setAliases.logAlias),
|
||||
}
|
||||
})
|
||||
: c,
|
||||
c =>
|
||||
change.setFields
|
||||
? {
|
||||
...c,
|
||||
? Object.assign({}, c, {
|
||||
fields: {
|
||||
container: defaultTo(c.fields.container, change.setFields.container),
|
||||
host: defaultTo(c.fields.host, change.setFields.host),
|
||||
message: c.fields.message,
|
||||
pod: defaultTo(c.fields.pod, change.setFields.pod),
|
||||
tiebreaker: defaultTo(c.fields.tiebreaker, change.setFields.tiebreaker),
|
||||
timestamp: defaultTo(c.fields.timestamp, change.setFields.timestamp),
|
||||
},
|
||||
}
|
||||
})
|
||||
: c,
|
||||
];
|
||||
return updaters.reduce(
|
||||
|
|
|
@ -8,12 +8,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import JoiNamespace from 'joi';
|
||||
import { resolve } from 'path';
|
||||
|
||||
import {
|
||||
getConfigSchema,
|
||||
// getDeprecations,
|
||||
initServerWithKibana,
|
||||
KbnServer,
|
||||
} from './server/kibana.index';
|
||||
import { getConfigSchema, initServerWithKibana, KbnServer } from './server/kibana.index';
|
||||
import { savedObjectMappings } from './server/saved_objects';
|
||||
|
||||
const APP_ID = 'infra';
|
||||
|
@ -72,9 +67,6 @@ export function infra(kibana: any) {
|
|||
config(Joi: typeof JoiNamespace) {
|
||||
return getConfigSchema(Joi);
|
||||
},
|
||||
// deprecations(helpers: any) {
|
||||
// return getDeprecations(helpers);
|
||||
// },
|
||||
init(server: KbnServer) {
|
||||
initServerWithKibana(server);
|
||||
},
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { EuiDatePicker, EuiFilterButton, EuiFilterGroup } from '@elastic/eui';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import moment, { Moment } from 'moment';
|
||||
import moment, { Moment } from 'moment-timezone';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import moment, { Moment } from 'moment';
|
||||
import moment, { Moment } from 'moment-timezone';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import { find, get } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import moment from 'moment-timezone';
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
import {
|
||||
|
@ -274,7 +274,6 @@ export const RangeDatePicker = injectI18n(
|
|||
<EuiDatePickerRange
|
||||
className="euiDatePickerRange--inGroup"
|
||||
iconType={false}
|
||||
disabled={disabled}
|
||||
fullWidth
|
||||
startDateControl={
|
||||
<EuiDatePicker
|
||||
|
|
|
@ -94,7 +94,7 @@ export const FieldsConfigurationPanel = ({
|
|||
id="xpack.infra.sourceConfiguration.containerFieldDescription"
|
||||
defaultMessage="Field used to identify Docker containers. The recommended value is {defaultValue}."
|
||||
values={{
|
||||
defaultValue: <EuiCode>container.id</EuiCode>,
|
||||
defaultValue: <EuiCode>docker.container.id</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ export const SourceConfigurationFlyout = injectI18n(({ intl }: SourceConfigurati
|
|||
fields: {
|
||||
container: configuration.fields.container,
|
||||
host: configuration.fields.host,
|
||||
message: configuration.fields.message,
|
||||
pod: configuration.fields.pod,
|
||||
tiebreaker: configuration.fields.tiebreaker,
|
||||
timestamp: configuration.fields.timestamp,
|
||||
|
|
|
@ -27,6 +27,8 @@ export interface InputFieldProps<
|
|||
|
||||
type FieldErrorMessage = string | JSX.Element;
|
||||
|
||||
type EditableFieldName = 'container' | 'host' | 'pod' | 'tiebreaker' | 'timestamp';
|
||||
|
||||
interface FormState {
|
||||
name: string;
|
||||
description: string;
|
||||
|
@ -35,6 +37,7 @@ interface FormState {
|
|||
fields: {
|
||||
container: string;
|
||||
host: string;
|
||||
message: string[];
|
||||
pod: string;
|
||||
tiebreaker: string;
|
||||
timestamp: string;
|
||||
|
@ -50,7 +53,7 @@ interface Actions {
|
|||
updateName: (name: string) => void;
|
||||
updateLogAlias: (value: string) => void;
|
||||
updateMetricAlias: (value: string) => void;
|
||||
updateField: (field: keyof FormState['fields'], value: string) => void;
|
||||
updateField: (field: EditableFieldName, value: string) => void;
|
||||
}
|
||||
|
||||
interface Selectors {
|
||||
|
@ -58,7 +61,7 @@ interface Selectors {
|
|||
getNameFieldValidationErrors: () => FieldErrorMessage[];
|
||||
getLogAliasFieldValidationErrors: () => FieldErrorMessage[];
|
||||
getMetricAliasFieldValidationErrors: () => FieldErrorMessage[];
|
||||
getFieldFieldValidationErrors: (field: keyof FormState['fields']) => FieldErrorMessage[];
|
||||
getFieldFieldValidationErrors: (field: EditableFieldName) => FieldErrorMessage[];
|
||||
isFormValid: () => boolean;
|
||||
}
|
||||
|
||||
|
@ -128,7 +131,7 @@ interface WithSourceConfigurationFormStateProps {
|
|||
State &
|
||||
Actions &
|
||||
Selectors & {
|
||||
getFieldFieldProps: (field: keyof FormState['fields']) => InputFieldProps;
|
||||
getFieldFieldProps: (field: EditableFieldName) => InputFieldProps;
|
||||
getLogAliasFieldProps: () => InputFieldProps;
|
||||
getMetricAliasFieldProps: () => InputFieldProps;
|
||||
getNameFieldProps: () => InputFieldProps;
|
||||
|
|
|
@ -24,19 +24,19 @@ export const fieldToName = (field: string, intl: InjectedIntl) => {
|
|||
id: 'xpack.infra.groupByDisplayNames.hostName',
|
||||
defaultMessage: 'Host',
|
||||
}),
|
||||
'cloud.availability_zone': intl.formatMessage({
|
||||
'meta.cloud.availability_zone': intl.formatMessage({
|
||||
id: 'xpack.infra.groupByDisplayNames.availabilityZone',
|
||||
defaultMessage: 'Availability Zone',
|
||||
}),
|
||||
'cloud.machine.type': intl.formatMessage({
|
||||
'meta.cloud.machine_type': intl.formatMessage({
|
||||
id: 'xpack.infra.groupByDisplayNames.machineType',
|
||||
defaultMessage: 'Machine Type',
|
||||
}),
|
||||
'cloud.project.id': intl.formatMessage({
|
||||
'meta.cloud.project_id': intl.formatMessage({
|
||||
id: 'xpack.infra.groupByDisplayNames.projectID',
|
||||
defaultMessage: 'Project ID',
|
||||
}),
|
||||
'cloud.provider': intl.formatMessage({
|
||||
'meta.cloud.provider': intl.formatMessage({
|
||||
id: 'xpack.infra.groupByDisplayNames.provider',
|
||||
defaultMessage: 'Cloud Provider',
|
||||
}),
|
||||
|
|
|
@ -47,16 +47,16 @@ const getOptions = (
|
|||
[InfraNodeType.pod]: ['kubernetes.namespace', 'kubernetes.node.name'].map(mapFieldToOption),
|
||||
[InfraNodeType.container]: [
|
||||
'host.name',
|
||||
'cloud.availability_zone',
|
||||
'cloud.machine.type',
|
||||
'cloud.project.id',
|
||||
'cloud.provider',
|
||||
'meta.cloud.availability_zone',
|
||||
'meta.cloud.machine_type',
|
||||
'meta.cloud.project_id',
|
||||
'meta.cloud.provider',
|
||||
].map(mapFieldToOption),
|
||||
[InfraNodeType.host]: [
|
||||
'cloud.availability_zone',
|
||||
'cloud.machine.type',
|
||||
'cloud.project.id',
|
||||
'cloud.provider',
|
||||
'meta.cloud.availability_zone',
|
||||
'meta.cloud.machine_type',
|
||||
'meta.cloud.project_id',
|
||||
'meta.cloud.provider',
|
||||
].map(mapFieldToOption),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { EuiButtonEmpty, EuiDatePicker, EuiFormControlLayout } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import moment, { Moment } from 'moment';
|
||||
import moment, { Moment } from 'moment-timezone';
|
||||
import React from 'react';
|
||||
|
||||
interface WaffleTimeControlsProps {
|
||||
|
|
|
@ -19,6 +19,7 @@ export const sourceFieldsFragment = gql`
|
|||
fields {
|
||||
container
|
||||
host
|
||||
message
|
||||
pod
|
||||
tiebreaker
|
||||
timestamp
|
||||
|
|
|
@ -550,6 +550,26 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"description": "The fields to use as the log message",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "pod",
|
||||
"description": "The field to identify a pod by",
|
||||
|
|
|
@ -60,6 +60,8 @@ export interface InfraSourceFields {
|
|||
container: string;
|
||||
/** The fields to identify a host by */
|
||||
host: string;
|
||||
/** The fields to use as the log message */
|
||||
message: string[];
|
||||
/** The field to identify a pod by */
|
||||
pod: string;
|
||||
/** The field to use as a tiebreaker for log events that have identical timestamps */
|
||||
|
@ -944,6 +946,8 @@ export namespace SourceFields {
|
|||
|
||||
host: string;
|
||||
|
||||
message: string[];
|
||||
|
||||
pod: string;
|
||||
|
||||
tiebreaker: string;
|
||||
|
|
|
@ -44,6 +44,8 @@ export const sourcesSchema = gql`
|
|||
container: String!
|
||||
"The fields to identify a host by"
|
||||
host: String!
|
||||
"The fields to use as the log message"
|
||||
message: [String!]!
|
||||
"The field to identify a pod by"
|
||||
pod: String!
|
||||
"The field to use as a tiebreaker for log events that have identical timestamps"
|
||||
|
|
|
@ -88,6 +88,8 @@ export interface InfraSourceFields {
|
|||
container: string;
|
||||
/** The fields to identify a host by */
|
||||
host: string;
|
||||
/** The fields to use as the log message */
|
||||
message: string[];
|
||||
/** The field to identify a pod by */
|
||||
pod: string;
|
||||
/** The field to use as a tiebreaker for log events that have identical timestamps */
|
||||
|
@ -798,6 +800,8 @@ export namespace InfraSourceFieldsResolvers {
|
|||
container?: ContainerResolver<string, TypeParent, Context>;
|
||||
/** The fields to identify a host by */
|
||||
host?: HostResolver<string, TypeParent, Context>;
|
||||
/** The fields to use as the log message */
|
||||
message?: MessageResolver<string[], TypeParent, Context>;
|
||||
/** The field to identify a pod by */
|
||||
pod?: PodResolver<string, TypeParent, Context>;
|
||||
/** The field to use as a tiebreaker for log events that have identical timestamps */
|
||||
|
@ -816,6 +820,11 @@ export namespace InfraSourceFieldsResolvers {
|
|||
Parent = InfraSourceFields,
|
||||
Context = InfraContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type MessageResolver<
|
||||
R = string[],
|
||||
Parent = InfraSourceFields,
|
||||
Context = InfraContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type PodResolver<
|
||||
R = string,
|
||||
Parent = InfraSourceFields,
|
||||
|
|
|
@ -29,6 +29,9 @@ export const getConfigSchema = (Joi: typeof JoiNamespace) => {
|
|||
fields: Joi.object({
|
||||
container: Joi.string(),
|
||||
host: Joi.string(),
|
||||
message: Joi.array()
|
||||
.items(Joi.string())
|
||||
.single(),
|
||||
pod: Joi.string(),
|
||||
tiebreaker: Joi.string(),
|
||||
timestamp: Joi.string(),
|
||||
|
@ -50,10 +53,3 @@ export const getConfigSchema = (Joi: typeof JoiNamespace) => {
|
|||
|
||||
return InfraRootConfigSchema;
|
||||
};
|
||||
|
||||
export const getDeprecations = () => [];
|
||||
|
||||
// interface DeprecationHelpers {
|
||||
// rename(oldKey: string, newKey: string): (settings: unknown, log: unknown) => void;
|
||||
// unused(oldKey: string): (settings: unknown, log: unknown) => void;
|
||||
// }
|
||||
|
|
|
@ -157,6 +157,9 @@ export interface InfraDateRangeAggregationResponse {
|
|||
|
||||
export interface InfraMetadataAggregationBucket {
|
||||
key: string;
|
||||
names?: {
|
||||
buckets: InfraMetadataAggregationBucket[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface InfraMetadataAggregationResponse {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { first, get } from 'lodash';
|
||||
import { InfraSourceConfiguration } from '../../sources';
|
||||
import {
|
||||
InfraBackendFrameworkAdapter,
|
||||
|
@ -37,19 +37,22 @@ export class ElasticsearchMetadataAdapter implements InfraMetadataAdapter {
|
|||
},
|
||||
},
|
||||
},
|
||||
size: 0,
|
||||
size: 1,
|
||||
_source: [NAME_FIELDS[nodeType]],
|
||||
aggs: {
|
||||
nodeName: {
|
||||
terms: {
|
||||
field: NAME_FIELDS[nodeType],
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
metrics: {
|
||||
terms: {
|
||||
field: 'event.dataset',
|
||||
field: 'metricset.module',
|
||||
size: 1000,
|
||||
},
|
||||
aggs: {
|
||||
names: {
|
||||
terms: {
|
||||
field: 'metricset.name',
|
||||
size: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -57,7 +60,7 @@ export class ElasticsearchMetadataAdapter implements InfraMetadataAdapter {
|
|||
|
||||
const response = await this.framework.callWithRequest<
|
||||
any,
|
||||
{ metrics?: InfraMetadataAggregationResponse; nodeName?: InfraMetadataAggregationResponse }
|
||||
{ metrics?: InfraMetadataAggregationResponse }
|
||||
>(req, 'search', metricQuery);
|
||||
|
||||
const buckets =
|
||||
|
@ -65,9 +68,11 @@ export class ElasticsearchMetadataAdapter implements InfraMetadataAdapter {
|
|||
? response.aggregations.metrics.buckets
|
||||
: [];
|
||||
|
||||
const sampleDoc = first(response.hits.hits);
|
||||
|
||||
return {
|
||||
id: nodeId,
|
||||
name: get(response, ['aggregations', 'nodeName', 'buckets', 0, 'key'], nodeId),
|
||||
name: get(sampleDoc, `_source.${NAME_FIELDS[nodeType]}`),
|
||||
buckets,
|
||||
};
|
||||
}
|
||||
|
@ -89,19 +94,22 @@ export class ElasticsearchMetadataAdapter implements InfraMetadataAdapter {
|
|||
},
|
||||
},
|
||||
},
|
||||
size: 0,
|
||||
size: 1,
|
||||
_source: [NAME_FIELDS[nodeType]],
|
||||
aggs: {
|
||||
nodeName: {
|
||||
terms: {
|
||||
field: NAME_FIELDS[nodeType],
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
metrics: {
|
||||
terms: {
|
||||
field: 'event.dataset',
|
||||
field: 'fileset.module',
|
||||
size: 1000,
|
||||
},
|
||||
aggs: {
|
||||
names: {
|
||||
terms: {
|
||||
field: 'fileset.name',
|
||||
size: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -109,7 +117,7 @@ export class ElasticsearchMetadataAdapter implements InfraMetadataAdapter {
|
|||
|
||||
const response = await this.framework.callWithRequest<
|
||||
any,
|
||||
{ metrics?: InfraMetadataAggregationResponse; nodeName?: InfraMetadataAggregationResponse }
|
||||
{ metrics?: InfraMetadataAggregationResponse }
|
||||
>(req, 'search', logQuery);
|
||||
|
||||
const buckets =
|
||||
|
@ -117,9 +125,11 @@ export class ElasticsearchMetadataAdapter implements InfraMetadataAdapter {
|
|||
? response.aggregations.metrics.buckets
|
||||
: [];
|
||||
|
||||
const sampleDoc = first(response.hits.hits);
|
||||
|
||||
return {
|
||||
id: nodeId,
|
||||
name: get(response, ['aggregations', 'nodeName', 'buckets', 0, 'key'], nodeId),
|
||||
name: get(sampleDoc, `_source.${NAME_FIELDS[nodeType]}`),
|
||||
buckets,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -122,7 +122,7 @@ export const hostK8sOverview: InfraMetricModelCreator = (timeField, indexPattern
|
|||
type: InfraMetricModelMetricType.max,
|
||||
},
|
||||
{
|
||||
field: 'kubernetes.pod.uid',
|
||||
field: 'kubernetes.pod.name',
|
||||
id: 'card-pod-name',
|
||||
type: InfraMetricModelMetricType.cardinality,
|
||||
},
|
||||
|
|
|
@ -31,7 +31,7 @@ export const hostK8sPodCap: InfraMetricModelCreator = (timeField, indexPattern,
|
|||
id: 'used',
|
||||
metrics: [
|
||||
{
|
||||
field: 'kubernetes.pod.uid',
|
||||
field: 'kubernetes.pod.name',
|
||||
id: 'avg-pod',
|
||||
type: InfraMetricModelMetricType.cardinality,
|
||||
},
|
||||
|
|
|
@ -23,7 +23,7 @@ export const nginxHits: InfraMetricModelCreator = (timeField, indexPattern, inte
|
|||
},
|
||||
],
|
||||
split_mode: 'filter',
|
||||
filter: 'http.response.status_code:[200 TO 299]',
|
||||
filter: 'nginx.access.response_code:[200 TO 299]',
|
||||
},
|
||||
{
|
||||
id: '300s',
|
||||
|
@ -34,7 +34,7 @@ export const nginxHits: InfraMetricModelCreator = (timeField, indexPattern, inte
|
|||
},
|
||||
],
|
||||
split_mode: 'filter',
|
||||
filter: 'http.response.status_code:[300 TO 399]',
|
||||
filter: 'nginx.access.response_code:[300 TO 399]',
|
||||
},
|
||||
{
|
||||
id: '400s',
|
||||
|
@ -45,7 +45,7 @@ export const nginxHits: InfraMetricModelCreator = (timeField, indexPattern, inte
|
|||
},
|
||||
],
|
||||
split_mode: 'filter',
|
||||
filter: 'http.response.status_code:[400 TO 499]',
|
||||
filter: 'nginx.access.response_code:[400 TO 499]',
|
||||
},
|
||||
{
|
||||
id: '500s',
|
||||
|
@ -56,7 +56,7 @@ export const nginxHits: InfraMetricModelCreator = (timeField, indexPattern, inte
|
|||
},
|
||||
],
|
||||
split_mode: 'filter',
|
||||
filter: 'http.response.status_code:[500 TO 599]',
|
||||
filter: 'nginx.access.response_code:[500 TO 599]',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
@ -11,5 +11,5 @@ export const NODE_REQUEST_PARTITION_FACTOR = 1.2;
|
|||
export const NAME_FIELDS = {
|
||||
[InfraNodeType.host]: 'host.name',
|
||||
[InfraNodeType.pod]: 'kubernetes.pod.name',
|
||||
[InfraNodeType.container]: 'container.name',
|
||||
[InfraNodeType.container]: 'docker.container.name',
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@ import moment from 'moment';
|
|||
|
||||
import { InfraMetricType, InfraNode, InfraNodeMetric } from '../../../../graphql/types';
|
||||
import { InfraBucket, InfraNodeRequestOptions } from '../adapter_types';
|
||||
import { NAME_FIELDS } from '../constants';
|
||||
import { getBucketSizeInSeconds } from './get_bucket_size_in_seconds';
|
||||
|
||||
// TODO: Break these function into seperate files and expand beyond just documnet count
|
||||
|
@ -71,9 +72,9 @@ export function createNodeItem(
|
|||
node: InfraBucket,
|
||||
bucket: InfraBucket
|
||||
): InfraNode {
|
||||
const nodeDetails = get(node, ['nodeDetails', 'buckets', 0]);
|
||||
const nodeDoc = get(node, ['nodeDetails', 'hits', 'hits', 0]);
|
||||
return {
|
||||
metric: createNodeMetrics(options, node, bucket),
|
||||
path: [{ value: node.key, label: get(nodeDetails, 'key', node.key) }],
|
||||
path: [{ value: node.key, label: get(nodeDoc, `_source.${NAME_FIELDS[options.nodeType]}`) }],
|
||||
} as InfraNode;
|
||||
}
|
||||
|
|