Merge remote-tracking branch 'upstream/6.7' into 6.7

This commit is contained in:
Lukas Olson 2019-03-07 14:06:16 -07:00
commit 70dd28713c
179 changed files with 4785 additions and 32521 deletions

View file

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

View 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].

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 KiB

BIN
docs/images/management-upgrade-assistant-7.0.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 246 KiB

Before After
Before After

View file

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

View file

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

18
docs/maps/index.asciidoc Normal file
View 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[]

View 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[]

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

View 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[]

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,5 +17,4 @@
* under the License.
*/
export const SECURITY_AUTH_MESSAGE = 'Authentication failed';
export const API_ROUTE = '/api/canvas';

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,10 +17,8 @@
* under the License.
*/
import { translate } from './translate';
import { registerServerFunctions } from './server_functions';
export function routes(server) {
translate(server);
registerServerFunctions(server);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,7 +17,6 @@
* under the License.
*/
import 'ui/autoload/modules';
import 'ui/autoload/styles';
import 'ui/i18n';
import { uiModules } from 'ui/modules';

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -49,7 +49,6 @@ export function createJestConfig({
"default",
[`${kibanaDirectory}/src/dev/jest/junit_reporter.js`, {
reportName: 'X-Pack Jest Tests',
rootDirectory: xPackKibanaDirectory,
}]
],
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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" /> &nbsp;
</Fragment>
)}
Refresh
</div>
<div style={{ display: 'flex', alignItems: 'center' }}>Refresh</div>
</EuiButtonEmpty>
);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -38,7 +38,6 @@ export const tableState = handleActions({
return {
...state,
showSystemIndices,
toggleNameToVisibleMap: {}
};
},
[toggleChanged](state, action) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -19,6 +19,7 @@ export const sourceFieldsFragment = gql`
fields {
container
host
message
pod
tiebreaker
timestamp

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -157,6 +157,9 @@ export interface InfraDateRangeAggregationResponse {
export interface InfraMetadataAggregationBucket {
key: string;
names?: {
buckets: InfraMetadataAggregationBucket[];
};
}
export interface InfraMetadataAggregationResponse {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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