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

This commit is contained in:
Fuyao Zhao 2019-03-05 11:13:23 -08:00
commit 58585eef1b
81 changed files with 1343 additions and 991 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

5
kibana.d.ts vendored
View file

@ -25,6 +25,7 @@ export * from './target/types/type_exports';
* All exports from TS ambient definitions (where types are added for JS source in a .d.ts file).
*/
import * as LegacyElasticsearch from './src/legacy/core_plugins/elasticsearch';
import * as LegacyKibanaPluginSpec from './src/legacy/plugin_discovery/plugin_spec/plugin_spec_options';
import * as LegacyKibanaServer from './src/legacy/server/kbn_server';
/**
@ -40,6 +41,10 @@ export namespace Legacy {
export type SavedObjectsService = LegacyKibanaServer.SavedObjectsService;
export type Server = LegacyKibanaServer.Server;
export type InitPluginFunction = LegacyKibanaPluginSpec.InitPluginFunction;
export type UiExports = LegacyKibanaPluginSpec.UiExports;
export type PluginSpecOptions = LegacyKibanaPluginSpec.PluginSpecOptions;
export namespace Plugins {
export namespace elasticsearch {
export type Plugin = LegacyElasticsearch.ElasticsearchPlugin;

View file

@ -18,10 +18,12 @@
*/
import { resolve } from 'path';
import init from './init';
import { Legacy } from '../../../../kibana';
import { init } from './init';
export default function (kibana) {
return new kibana.Plugin({
// tslint:disable-next-line
export default function InterpreterPlugin(kibana: any) {
const config: Legacy.PluginSpecOptions = {
id: 'interpreter',
require: ['kibana', 'elasticsearch'],
publicDir: resolve(__dirname, 'public'),
@ -29,6 +31,7 @@ export default function (kibana) {
injectDefaultVars: server => ({ serverBasePath: server.config().get('server.basePath') }),
},
init,
});
}
};
return new kibana.Plugin(config);
}

View file

@ -17,17 +17,26 @@
* under the License.
*/
import { routes } from './server/routes';
// @ts-ignore
import { registryFactory } from '@kbn/interpreter/common';
// @ts-ignore
import { registries } from '@kbn/interpreter/server';
export default async function (server /*options*/) {
// @ts-ignore
import { routes } from './server/routes';
import { Legacy } from '../../../../kibana';
export async function init(server: Legacy.Server /*options*/) {
server.injectUiAppVars('canvas', () => {
const config = server.config();
const basePath = config.get('server.basePath');
const reportingBrowserType = (() => {
const configKey = 'xpack.reporting.capture.browser.type';
if (!config.has(configKey)) return null;
if (!config.has(configKey)) {
return null;
}
return config.get(configKey);
})();

View file

@ -68,7 +68,7 @@ describe('context app', function () {
it('should use the `fetch` method of the SearchSource', function () {
const searchSourceStub = new SearchSourceStub();
return fetchAnchor('INDEX_PATTERN_ID', 'doc', 'id', [{ '@timestamp': 'desc' }, { '_doc': 'asc' }])
return fetchAnchor('INDEX_PATTERN_ID', 'doc', 'id', [{ '@timestamp': 'desc' }, { '_doc': 'desc' }])
.then(() => {
expect(searchSourceStub.fetch.calledOnce).to.be(true);
});
@ -77,7 +77,7 @@ describe('context app', function () {
it('should configure the SearchSource to not inherit from the implicit root', function () {
const searchSourceStub = new SearchSourceStub();
return fetchAnchor('INDEX_PATTERN_ID', 'doc', 'id', [{ '@timestamp': 'desc' }, { '_doc': 'asc' }])
return fetchAnchor('INDEX_PATTERN_ID', 'doc', 'id', [{ '@timestamp': 'desc' }, { '_doc': 'desc' }])
.then(() => {
const setParentSpy = searchSourceStub.setParent;
expect(setParentSpy.calledOnce).to.be(true);
@ -88,7 +88,7 @@ describe('context app', function () {
it('should set the SearchSource index pattern', function () {
const searchSourceStub = new SearchSourceStub();
return fetchAnchor('INDEX_PATTERN_ID', 'doc', 'id', [{ '@timestamp': 'desc' }, { '_doc': 'asc' }])
return fetchAnchor('INDEX_PATTERN_ID', 'doc', 'id', [{ '@timestamp': 'desc' }, { '_doc': 'desc' }])
.then(() => {
const setFieldSpy = searchSourceStub.setField;
expect(setFieldSpy.firstCall.args[1]).to.eql({ id: 'INDEX_PATTERN_ID' });
@ -98,7 +98,7 @@ describe('context app', function () {
it('should set the SearchSource version flag to true', function () {
const searchSourceStub = new SearchSourceStub();
return fetchAnchor('INDEX_PATTERN_ID', 'doc', 'id', [{ '@timestamp': 'desc' }, { '_doc': 'asc' }])
return fetchAnchor('INDEX_PATTERN_ID', 'doc', 'id', [{ '@timestamp': 'desc' }, { '_doc': 'desc' }])
.then(() => {
const setVersionSpy = searchSourceStub.setField.withArgs('version');
expect(setVersionSpy.calledOnce).to.be(true);
@ -109,7 +109,7 @@ describe('context app', function () {
it('should set the SearchSource size to 1', function () {
const searchSourceStub = new SearchSourceStub();
return fetchAnchor('INDEX_PATTERN_ID', 'doc', 'id', [{ '@timestamp': 'desc' }, { '_doc': 'asc' }])
return fetchAnchor('INDEX_PATTERN_ID', 'doc', 'id', [{ '@timestamp': 'desc' }, { '_doc': 'desc' }])
.then(() => {
const setSizeSpy = searchSourceStub.setField.withArgs('size');
expect(setSizeSpy.calledOnce).to.be(true);
@ -120,7 +120,7 @@ describe('context app', function () {
it('should set the SearchSource query to an ids query', function () {
const searchSourceStub = new SearchSourceStub();
return fetchAnchor('INDEX_PATTERN_ID', 'doc', 'id', [{ '@timestamp': 'desc' }, { '_doc': 'asc' }])
return fetchAnchor('INDEX_PATTERN_ID', 'doc', 'id', [{ '@timestamp': 'desc' }, { '_doc': 'desc' }])
.then(() => {
const setQuerySpy = searchSourceStub.setField.withArgs('query');
expect(setQuerySpy.calledOnce).to.be(true);
@ -143,13 +143,13 @@ describe('context app', function () {
it('should set the SearchSource sort order', function () {
const searchSourceStub = new SearchSourceStub();
return fetchAnchor('INDEX_PATTERN_ID', 'doc', 'id', [{ '@timestamp': 'desc' }, { '_doc': 'asc' }])
return fetchAnchor('INDEX_PATTERN_ID', 'doc', 'id', [{ '@timestamp': 'desc' }, { '_doc': 'desc' }])
.then(() => {
const setSortSpy = searchSourceStub.setField.withArgs('sort');
expect(setSortSpy.calledOnce).to.be(true);
expect(setSortSpy.firstCall.args[1]).to.eql([
{ '@timestamp': 'desc' },
{ '_doc': 'asc' },
{ '_doc': 'desc' },
]);
});
});
@ -158,7 +158,7 @@ describe('context app', function () {
const searchSourceStub = new SearchSourceStub();
searchSourceStub._stubHits = [];
return fetchAnchor('INDEX_PATTERN_ID', 'doc', 'id', [{ '@timestamp': 'desc' }, { '_doc': 'asc' }])
return fetchAnchor('INDEX_PATTERN_ID', 'doc', 'id', [{ '@timestamp': 'desc' }, { '_doc': 'desc' }])
.then(
() => {
expect().fail('expected the promise to be rejected');
@ -176,7 +176,7 @@ describe('context app', function () {
{ property2: 'value2' },
];
return fetchAnchor('INDEX_PATTERN_ID', 'doc', 'id', [{ '@timestamp': 'desc' }, { '_doc': 'asc' }])
return fetchAnchor('INDEX_PATTERN_ID', 'doc', 'id', [{ '@timestamp': 'desc' }, { '_doc': 'desc' }])
.then((anchorDocument) => {
expect(anchorDocument).to.have.property('property1', 'value1');
expect(anchorDocument).to.have.property('$$_isAnchor', true);

View file

@ -63,7 +63,6 @@ describe('context app', function () {
'desc',
MS_PER_DAY * 3000,
'_doc',
'asc',
0,
3,
[]
@ -90,7 +89,6 @@ describe('context app', function () {
'desc',
MS_PER_DAY * 3000,
'_doc',
'asc',
0,
6,
[]
@ -126,7 +124,6 @@ describe('context app', function () {
'desc',
MS_PER_DAY * 1000,
'_doc',
'asc',
0,
3,
[]
@ -153,7 +150,6 @@ describe('context app', function () {
'desc',
MS_PER_DAY * 3,
'_doc',
'asc',
0,
3,
[]
@ -172,7 +168,6 @@ describe('context app', function () {
'desc',
MS_PER_DAY * 3,
'_doc',
'asc',
0,
3,
[]
@ -183,5 +178,26 @@ describe('context app', function () {
expect(setParentSpy.called).to.be(true);
});
});
it('should set the tiebreaker sort order to the opposite as the time field', function () {
const searchSourceStub = getSearchSourceStub();
return fetchPredecessors(
'INDEX_PATTERN_ID',
'@timestamp',
'desc',
MS_PER_DAY,
'_doc',
0,
3,
[]
)
.then(() => {
expect(searchSourceStub.setField.calledWith('sort', [
{ '@timestamp': 'asc' },
{ '_doc': 'asc' },
])).to.be(true);
});
});
});
});

View file

@ -63,7 +63,6 @@ describe('context app', function () {
'desc',
MS_PER_DAY * 3000,
'_doc',
'asc',
0,
3,
[]
@ -90,7 +89,6 @@ describe('context app', function () {
'desc',
MS_PER_DAY * 3000,
'_doc',
'asc',
0,
6,
[]
@ -128,7 +126,6 @@ describe('context app', function () {
'desc',
MS_PER_DAY * 3000,
'_doc',
'asc',
0,
4,
[]
@ -155,7 +152,6 @@ describe('context app', function () {
'desc',
MS_PER_DAY * 3,
'_doc',
'asc',
0,
3,
[]
@ -174,7 +170,6 @@ describe('context app', function () {
'desc',
MS_PER_DAY * 3,
'_doc',
'asc',
0,
3,
[]
@ -185,5 +180,26 @@ describe('context app', function () {
expect(setParentSpy.called).to.be(true);
});
});
it('should set the tiebreaker sort order to the same as the time field', function () {
const searchSourceStub = getSearchSourceStub();
return fetchSuccessors(
'INDEX_PATTERN_ID',
'@timestamp',
'desc',
MS_PER_DAY,
'_doc',
0,
3,
[]
)
.then(() => {
expect(searchSourceStub.setField.calledWith('sort', [
{ '@timestamp': 'desc' },
{ '_doc': 'desc' },
])).to.be(true);
});
});
});
});

View file

@ -64,7 +64,6 @@ function fetchContextProvider(indexPatterns, Private) {
timeSortDirection,
timeValue,
tieBreakerField,
tieBreakerSortDirection,
tieBreakerValue,
size,
filters
@ -95,7 +94,6 @@ function fetchContextProvider(indexPatterns, Private) {
endTimeValue,
afterTimeValue,
tieBreakerField,
tieBreakerSortDirection,
afterTieBreakerValue,
remainingSize
);
@ -112,7 +110,6 @@ function fetchContextProvider(indexPatterns, Private) {
timeSortDirection,
timeValue,
tieBreakerField,
tieBreakerSortDirection,
tieBreakerValue,
size,
filters
@ -143,7 +140,6 @@ function fetchContextProvider(indexPatterns, Private) {
endTimeValue,
afterTimeValue,
tieBreakerField,
reverseSortDirection(tieBreakerSortDirection),
afterTieBreakerValue,
remainingSize
);
@ -173,7 +169,7 @@ function fetchContextProvider(indexPatterns, Private) {
* `endTimeValue` from the `searchSource` using the given `timeField` and
* `tieBreakerField` fields up to a maximum of `maxCount` documents. The
* documents are sorted by `(timeField, tieBreakerField)` using the
* respective `timeSortDirection` and `tieBreakerSortDirection`.
* `timeSortDirection` for both fields
*
* The `searchSource` is assumed to have the appropriate index pattern
* and filters set.
@ -185,7 +181,6 @@ function fetchContextProvider(indexPatterns, Private) {
* @param {number | null} endTimeValue
* @param {number} [afterTimeValue=startTimeValue]
* @param {string} tieBreakerField
* @param {SortDirection} tieBreakerSortDirection
* @param {number} tieBreakerValue
* @param {number} maxCount
* @returns {Promise<object[]>}
@ -198,7 +193,6 @@ function fetchContextProvider(indexPatterns, Private) {
endTimeValue,
afterTimeValue,
tieBreakerField,
tieBreakerSortDirection,
tieBreakerValue,
maxCount
) {
@ -233,7 +227,7 @@ function fetchContextProvider(indexPatterns, Private) {
])
.setField('sort', [
{ [timeField]: timeSortDirection },
{ [tieBreakerField]: tieBreakerSortDirection },
{ [tieBreakerField]: timeSortDirection },
])
.setField('version', true)
.fetch();

View file

@ -70,7 +70,7 @@ export function QueryActionsProvider(courier, Private, Promise, i18n) {
setLoadingStatus(state)('anchor');
return Promise.try(() => (
fetchAnchor(indexPatternId, anchorType, anchorId, [_.zipObject([sort]), { [tieBreakerField]: 'asc' }])
fetchAnchor(indexPatternId, anchorType, anchorId, [_.zipObject([sort]), { [tieBreakerField]: sort[1] }])
))
.then(
(anchorDocument) => {
@ -112,7 +112,6 @@ export function QueryActionsProvider(courier, Private, Promise, i18n) {
sort[1],
anchor.sort[0],
tieBreakerField,
'asc',
anchor.sort[1],
predecessorCount,
filters
@ -158,7 +157,6 @@ export function QueryActionsProvider(courier, Private, Promise, i18n) {
sort[1],
anchor.sort[0],
tieBreakerField,
'asc',
anchor.sort[1],
successorCount,
filters

View file

@ -28,7 +28,6 @@ export function fetchProvider(index) {
const [response, config] = await Promise.all([
callCluster('get', {
index,
type: 'doc',
id: 'kql-telemetry:kql-telemetry',
ignore: [404],
}),

File diff suppressed because one or more lines are too long

View file

@ -852,8 +852,8 @@ export function getUiSettingDefaults() {
description: i18n.translate('kbn.advancedSettings.timepicker.quickRangesText', {
defaultMessage:
'The list of ranges to show in the Quick section of the time picker. This should be an array of objects, ' +
'with each object containing "from", "to" (see {acceptedFormatsLink}), ' +
'"display" (the title to be displayed), and "section" (which column to put the option in).',
'with each object containing "from", "to" (see {acceptedFormatsLink}), and ' +
'"display" (the title to be displayed).',
description:
'Part of composite text: kbn.advancedSettings.timepicker.quickRangesText + ' +
'kbn.advancedSettings.timepicker.quickRanges.acceptedFormatsLinkText',

View file

@ -169,7 +169,7 @@ class MarkdownEditor extends Component {
/>
</p>
</EuiText>
<table className="table">
<table className="table" data-test-subj="tsvbMarkdownVariablesTable">
<thead>
<tr>
<th scope="col">
@ -190,7 +190,7 @@ class MarkdownEditor extends Component {
</table>
{rows.length === 0 && (
<EuiTitle size="xxs" className="tvbMarkdownEditor__noVariables">
<EuiTitle size="xxs" className="tsvbMarkdownVariablesTable__noVariables" data-test-subj="tvbMarkdownEditor__noVariables">
<span>
<FormattedMessage
id="tsvb.markdownEditor.noVariablesAvailableDescription"

View file

@ -279,11 +279,12 @@ class MarkdownPanelConfigUi extends Component {
<EuiTab
isSelected={selectedTab === 'markdown'}
onClick={() => this.switchTab('markdown')}
data-test-subj="markdown-subtab"
>
Markdown
</EuiTab>
<EuiTab
data-test-subj="markdownDataBtn"
data-test-subj="data-subtab"
isSelected={selectedTab === 'data'}
onClick={() => this.switchTab('data')}
>
@ -295,6 +296,7 @@ class MarkdownPanelConfigUi extends Component {
<EuiTab
isSelected={selectedTab === 'options'}
onClick={() => this.switchTab('options')}
data-test-subj="options-subtab"
>
<FormattedMessage
id="tsvb.markdown.optionsTab.panelOptionsButtonLabel"

View file

@ -0,0 +1,32 @@
/*
* 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 { Server } from '../../server/kbn_server';
export type InitPluginFunction = (server: Server) => void;
export interface UiExports {
injectDefaultVars: (server: Server) => { [key: string]: any };
}
export interface PluginSpecOptions {
id: string;
require: string[];
publicDir: string;
uiExports?: UiExports;
init: InitPluginFunction;
}

View file

@ -27,6 +27,7 @@ import { SavedObjectsClient, SavedObjectsService } from './saved_objects';
export interface KibanaConfig {
get<T>(key: string): T;
has(key: string): boolean;
}
// Extend the defaults with the plugins and server methods we need.
@ -43,6 +44,7 @@ declare module 'hapi' {
config: () => KibanaConfig;
indexPatternsServiceFactory: IndexPatternsServiceFactory;
savedObjects: SavedObjectsService;
injectUiAppVars: (pluginName: string, getAppVars: () => { [key: string]: any }) => void;
}
interface Request {

View file

@ -38,6 +38,9 @@ export function createMockServer(config: { [key: string]: any } = defaultConfig)
get(key: string) {
return config[key];
},
has(key: string) {
return config.hasOwnProperty(key);
},
};
};

View file

@ -57,8 +57,6 @@ export class SavedObjectsRepository {
}
this._allowedTypes = allowedTypes;
// ES7 and up expects the root type to be _doc
this._type = '_doc';
this._onBeforeWrite = onBeforeWrite;
this._unwrappedCallCluster = async (...args) => {
await migrator.awaitMigration();
@ -112,7 +110,6 @@ export class SavedObjectsRepository {
const response = await this._writeToCluster(method, {
id: raw._id,
type: this._type,
index: this._index,
refresh: 'wait_for',
body: raw._source,
@ -182,7 +179,6 @@ export class SavedObjectsRepository {
{
[method]: {
_id: expectedResult.rawMigratedDoc._id,
_type: this._type,
},
},
expectedResult.rawMigratedDoc._source,
@ -270,7 +266,6 @@ export class SavedObjectsRepository {
const response = await this._writeToCluster('delete', {
id: this._serializer.generateRawId(namespace, type, id),
type: this._type,
index: this._index,
refresh: 'wait_for',
ignore: [404],
@ -462,7 +457,6 @@ export class SavedObjectsRepository {
if(this._isTypeAllowed(type)) {
acc.push({
_id: this._serializer.generateRawId(namespace, type, id),
_type: this._type,
});
}else{
unsupportedTypes.push({ id, type, error: errors.createUnsupportedTypeError(type).output.payload });
@ -518,7 +512,6 @@ export class SavedObjectsRepository {
const response = await this._callCluster('get', {
id: this._serializer.generateRawId(namespace, type, id),
type: this._type,
index: this._index,
ignore: [404]
});
@ -568,7 +561,6 @@ export class SavedObjectsRepository {
const time = this._getCurrentTime();
const response = await this._writeToCluster('update', {
id: this._serializer.generateRawId(namespace, type, id),
type: this._type,
index: this._index,
...(version && decodeRequestVersion(version)),
refresh: 'wait_for',
@ -638,7 +630,6 @@ export class SavedObjectsRepository {
const response = await this._writeToCluster('update', {
id: this._serializer.generateRawId(namespace, type, id),
type: this._type,
index: this._index,
refresh: 'wait_for',
_source: true,

View file

@ -501,9 +501,9 @@ describe('SavedObjectsRepository', () => {
expect(bulkCalls.length).toEqual(1);
expect(bulkCalls[0][1].body).toEqual([
{ create: { _type: '_doc', _id: 'config:one' } },
{ create: { _id: 'config:one' } },
{ type: 'config', ...mockTimestampFields, config: { title: 'Test One' }, references: [{ name: 'ref_0', type: 'test', id: '1' }] },
{ create: { _type: '_doc', _id: 'index-pattern:two' } },
{ create: { _id: 'index-pattern:two' } },
{
type: 'index-pattern',
...mockTimestampFields,
@ -551,7 +551,7 @@ describe('SavedObjectsRepository', () => {
expect(callAdminCluster).toHaveBeenCalledWith('bulk', expect.objectContaining({
body: [
{ create: { _type: '_doc', _id: 'config:one' } },
{ create: { _id: 'config:one' } },
{
type: 'config',
...mockTimestampFields,
@ -559,7 +559,7 @@ describe('SavedObjectsRepository', () => {
migrationVersion: { foo: '2.3.4' },
references: [{ name: 'search_0', type: 'search', id: '123' }],
},
{ create: { _type: '_doc', _id: 'index-pattern:two' } },
{ create: { _id: 'index-pattern:two' } },
{
type: 'index-pattern',
...mockTimestampFields,
@ -606,7 +606,7 @@ describe('SavedObjectsRepository', () => {
expect(callAdminCluster).toHaveBeenCalledWith('bulk', expect.objectContaining({
body: [
// uses create because overwriting is not allowed
{ create: { _type: '_doc', _id: 'foo:bar' } },
{ create: { _id: 'foo:bar' } },
{ type: 'foo', ...mockTimestampFields, 'foo': {}, references: [] },
]
}));
@ -625,7 +625,7 @@ describe('SavedObjectsRepository', () => {
expect(callAdminCluster).toHaveBeenCalledWith('bulk', expect.objectContaining({
body: [
// uses index because overwriting is allowed
{ index: { _type: '_doc', _id: 'foo:bar' } },
{ index: { _id: 'foo:bar' } },
{ type: 'foo', ...mockTimestampFields, 'foo': {}, references: [] },
]
}));
@ -741,7 +741,7 @@ describe('SavedObjectsRepository', () => {
expect(callAdminCluster).toHaveBeenCalledTimes(1);
expect(callAdminCluster).toHaveBeenCalledWith('bulk', expect.objectContaining({
body: [
{ create: { _type: '_doc', _id: 'foo-namespace:config:one' } },
{ create: { _id: 'foo-namespace:config:one' } },
{
namespace: 'foo-namespace',
type: 'config',
@ -749,7 +749,7 @@ describe('SavedObjectsRepository', () => {
config: { title: 'Test One' },
references: [],
},
{ create: { _type: '_doc', _id: 'foo-namespace:index-pattern:two' } },
{ create: { _id: 'foo-namespace:index-pattern:two' } },
{
namespace: 'foo-namespace',
type: 'index-pattern',
@ -786,9 +786,9 @@ describe('SavedObjectsRepository', () => {
expect(callAdminCluster).toHaveBeenCalledTimes(1);
expect(callAdminCluster).toHaveBeenCalledWith('bulk', expect.objectContaining({
body: [
{ create: { _type: '_doc', _id: 'config:one' } },
{ create: { _id: 'config:one' } },
{ type: 'config', ...mockTimestampFields, config: { title: 'Test One' }, references: [] },
{ create: { _type: '_doc', _id: 'index-pattern:two' } },
{ create: { _id: 'index-pattern:two' } },
{ type: 'index-pattern', ...mockTimestampFields, 'index-pattern': { title: 'Test Two' }, references: [] }
]
}));
@ -810,7 +810,7 @@ describe('SavedObjectsRepository', () => {
expect(callAdminCluster).toHaveBeenCalledTimes(1);
expect(callAdminCluster).toHaveBeenCalledWith('bulk', expect.objectContaining({
body: [
{ create: { _type: '_doc', _id: 'globaltype:one' } },
{ create: { _id: 'globaltype:one' } },
{ type: 'globaltype', ...mockTimestampFields, 'globaltype': { title: 'Test One' }, references: [] },
]
}));
@ -853,7 +853,6 @@ describe('SavedObjectsRepository', () => {
expect(callAdminCluster).toHaveBeenCalledTimes(1);
expect(callAdminCluster).toHaveBeenCalledWith('delete', {
type: '_doc',
id: 'foo-namespace:index-pattern:logstash-*',
refresh: 'wait_for',
index: '.kibana-test',
@ -869,7 +868,6 @@ describe('SavedObjectsRepository', () => {
expect(callAdminCluster).toHaveBeenCalledTimes(1);
expect(callAdminCluster).toHaveBeenCalledWith('delete', {
type: '_doc',
id: 'index-pattern:logstash-*',
refresh: 'wait_for',
index: '.kibana-test',
@ -887,7 +885,6 @@ describe('SavedObjectsRepository', () => {
expect(callAdminCluster).toHaveBeenCalledTimes(1);
expect(callAdminCluster).toHaveBeenCalledWith('delete', {
type: '_doc',
id: 'globaltype:logstash-*',
refresh: 'wait_for',
index: '.kibana-test',
@ -1173,7 +1170,6 @@ describe('SavedObjectsRepository', () => {
expect(callAdminCluster).toHaveBeenCalledTimes(1);
expect(callAdminCluster).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
id: 'foo-namespace:index-pattern:logstash-*',
type: '_doc'
}));
});
@ -1185,7 +1181,6 @@ describe('SavedObjectsRepository', () => {
expect(callAdminCluster).toHaveBeenCalledTimes(1);
expect(callAdminCluster).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
id: 'index-pattern:logstash-*',
type: '_doc'
}));
});
@ -1199,7 +1194,6 @@ describe('SavedObjectsRepository', () => {
expect(callAdminCluster).toHaveBeenCalledTimes(1);
expect(callAdminCluster).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
id: 'globaltype:logstash-*',
type: '_doc'
}));
});
});
@ -1231,9 +1225,9 @@ describe('SavedObjectsRepository', () => {
expect(callAdminCluster).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
body: {
docs: [
{ _type: '_doc', _id: 'config:one' },
{ _type: '_doc', _id: 'index-pattern:two' },
{ _type: '_doc', _id: 'globaltype:three' },
{ _id: 'config:one' },
{ _id: 'index-pattern:two' },
{ _id: 'globaltype:three' },
]
}
}));
@ -1258,9 +1252,9 @@ describe('SavedObjectsRepository', () => {
expect(callAdminCluster).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
body: {
docs: [
{ _type: '_doc', _id: 'foo-namespace:config:one' },
{ _type: '_doc', _id: 'foo-namespace:index-pattern:two' },
{ _type: '_doc', _id: 'globaltype:three' },
{ _id: 'foo-namespace:config:one' },
{ _id: 'foo-namespace:index-pattern:two' },
{ _id: 'globaltype:three' },
]
}
}));
@ -1423,7 +1417,6 @@ describe('SavedObjectsRepository', () => {
expect(callAdminCluster).toHaveBeenCalledTimes(1);
expect(callAdminCluster).toHaveBeenCalledWith('update', {
type: '_doc',
id: 'foo-namespace:index-pattern:logstash-*',
body: {
doc: {
@ -1457,7 +1450,6 @@ describe('SavedObjectsRepository', () => {
expect(callAdminCluster).toHaveBeenCalledTimes(1);
expect(callAdminCluster).toHaveBeenCalledWith('update', {
type: '_doc',
id: 'index-pattern:logstash-*',
body: {
doc: {
@ -1492,7 +1484,6 @@ describe('SavedObjectsRepository', () => {
expect(callAdminCluster).toHaveBeenCalledTimes(1);
expect(callAdminCluster).toHaveBeenCalledWith('update', {
type: '_doc',
id: 'globaltype:foo',
body: {
doc: {

View file

@ -47,7 +47,8 @@ function getFieldsForTypes(searchFields, types) {
if (!searchFields || !searchFields.length) {
return {
all_fields: true
lenient: true,
fields: ['*'],
};
}

View file

@ -235,7 +235,8 @@ describe('searchDsl/queryParams', () => {
{
simple_query_string: {
query: 'us*',
all_fields: true
lenient: true,
fields: ['*'],
}
}
]
@ -265,7 +266,8 @@ describe('searchDsl/queryParams', () => {
{
simple_query_string: {
query: 'us*',
all_fields: true
lenient: true,
fields: ['*'],
}
}
]
@ -294,7 +296,8 @@ describe('searchDsl/queryParams', () => {
{
simple_query_string: {
query: 'us*',
all_fields: true
lenient: true,
fields: ['*'],
}
}
]
@ -323,7 +326,8 @@ describe('searchDsl/queryParams', () => {
{
simple_query_string: {
query: 'us*',
all_fields: true
lenient: true,
fields: ['*'],
}
}
]
@ -767,7 +771,8 @@ describe('searchDsl/queryParams', () => {
must: [
{
simple_query_string: {
all_fields: true,
lenient: true,
fields: ['*'],
default_operator: 'AND',
query: 'foo',
},

View file

@ -20,6 +20,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import $ from 'jquery';
import url from 'url';
import { uiModules } from '../../modules';
import {
@ -57,7 +58,7 @@ export function kbnChromeProvider(chrome, internals) {
},
controllerAs: 'chrome',
controller($scope, $rootScope, Private) {
controller($scope, $rootScope, Private, $location) {
const getUnhashableStates = Private(getUnhashableStatesProvider);
const subUrlRouteFilter = Private(SubUrlRouteFilterProvider);
@ -73,6 +74,19 @@ export function kbnChromeProvider(chrome, internals) {
}
}
$rootScope.$on('$locationChangeStart', (e, newUrl) => {
// This handler fixes issue #31238 where browser back navigation
// fails due to angular 1.6 parsing url encoded params wrong.
const absUrlHash = url.parse($location.absUrl()).hash;
const decodedAbsUrlHash = decodeURIComponent(absUrlHash);
const hash = url.parse(newUrl).hash;
const decodedHash = decodeURIComponent(hash);
if (absUrlHash !== hash && decodedHash === decodedAbsUrlHash) {
// replace the urlencoded hash with the version that angular sees.
$location.url(absUrlHash).replace();
}
});
$rootScope.$on('$routeChangeSuccess', onRouteChange);
$rootScope.$on('$routeUpdate', onRouteChange);
updateSubUrls(); // initialize sub urls

View file

@ -18,6 +18,7 @@
*/
import { KBN_FIELD_TYPES } from '../../../../utils/kbn_field_types';
import { get } from 'lodash';
const filterableTypes = KBN_FIELD_TYPES.filter(type => type.filterable).map(type => type.name);
@ -26,7 +27,7 @@ export function isFilterable(field) {
}
export function getFromSavedObject(savedObject) {
if (!savedObject) {
if (get(savedObject, 'attributes.fields') === undefined) {
return null;
}

View file

@ -0,0 +1,74 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import expect from 'expect.js';
export default function ({ getService, getPageObjects }) {
const browser = getService('browser');
const PageObjects = getPageObjects(['dashboard', 'common', 'home', 'timePicker']);
const appsMenu = getService('appsMenu');
const kibanaServer = getService('kibanaServer');
const fromTime = '2015-09-19 06:31:44.000';
const toTime = '2015-09-23 18:31:44.000';
describe('Kibana browser back navigation should work', function describeIndexTests() {
before(async () => {
await PageObjects.dashboard.initTests();
await kibanaServer.uiSettings.disableToastAutohide();
await browser.refresh();
});
it('detect navigate back issues', async ()=> {
let currUrl;
// Detects bug described in issue #31238 - where back navigation would get stuck to URL encoding handling in Angular.
// Navigate to home app
await PageObjects.common.navigateToApp('home');
const homeUrl = await browser.getCurrentUrl();
// Navigate to discover app
await appsMenu.clickLink('Discover');
const discoverUrl = await browser.getCurrentUrl();
await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
const modifiedTimeDiscoverUrl = await browser.getCurrentUrl();
// Navigate to dashboard app
await appsMenu.clickLink('Dashboard');
// Navigating back to discover
await browser.goBack();
currUrl = await browser.getCurrentUrl();
expect(currUrl).to.be(modifiedTimeDiscoverUrl);
// Navigating back from time settings
await browser.goBack(); // undo time settings
await browser.goBack(); // undo automatically set config, should it be in the history stack? (separate issue!)
currUrl = await browser.getCurrentUrl();
// Discover view also keeps adds some default arguments into the _a URL parameter, so we can only check that the url starts the same.
expect(currUrl.startsWith(discoverUrl)).to.be(true);
// Navigate back home
await browser.goBack();
currUrl = await browser.getCurrentUrl();
expect(currUrl).to.be(homeUrl);
});
});
}

View file

@ -27,6 +27,7 @@ export default function ({ getService, loadTestFile }) {
return browser.setWindowSize(1200, 800);
});
loadTestFile(require.resolve('./_navigation'));
loadTestFile(require.resolve('./_home'));
loadTestFile(require.resolve('./_add_data'));
loadTestFile(require.resolve('./_sample_data'));

View file

@ -152,58 +152,6 @@ export default function({ getService, getPageObjects }: FtrProviderContext) {
});
});
describe('markdown', () => {
before(async () => {
await PageObjects.visualBuilder.resetPage();
await PageObjects.visualBuilder.clickMarkdown();
await PageObjects.timePicker.setAbsoluteRange(
'2015-09-22 06:00:00.000',
'2015-09-22 11:00:00.000'
);
});
it('should allow printing raw timestamp of data', async () => {
await retry.try(async () => {
await PageObjects.visualBuilder.enterMarkdown('{{ count.data.raw.[0].[0] }}');
const text = await PageObjects.visualBuilder.getMarkdownText();
expect(text).to.be('1442901600000');
});
});
it('should allow printing raw value of data', async () => {
await PageObjects.visualBuilder.enterMarkdown('{{ count.data.raw.[0].[1] }}');
const text = await PageObjects.visualBuilder.getMarkdownText();
expect(text).to.be('6');
});
describe('allow time offsets', () => {
before(async () => {
await PageObjects.visualBuilder.enterMarkdown(
'{{ count.data.raw.[0].[0] }}#{{ count.data.raw.[0].[1] }}'
);
await PageObjects.visualBuilder.clickMarkdownData();
await PageObjects.visualBuilder.clickSeriesOption();
});
it('allow positive time offsets', async () => {
await PageObjects.visualBuilder.enterOffsetSeries('2h');
await PageObjects.header.waitUntilLoadingHasFinished();
const text = await PageObjects.visualBuilder.getMarkdownText();
const [timestamp, value] = text.split('#');
expect(timestamp).to.be('1442901600000');
expect(value).to.be('3');
});
it('allow negative time offsets', async () => {
await PageObjects.visualBuilder.enterOffsetSeries('-2h');
await PageObjects.header.waitUntilLoadingHasFinished();
const text = await PageObjects.visualBuilder.getMarkdownText();
const [timestamp, value] = text.split('#');
expect(timestamp).to.be('1442901600000');
expect(value).to.be('23');
});
});
});
// add a table sanity timestamp
describe('table', () => {
before(async () => {

View file

@ -0,0 +1,76 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import expect from 'expect.js';
import { FtrProviderContext } from '../../ftr_provider_context';
// tslint:disable-next-line:no-default-export
export default function({ getPageObjects }: FtrProviderContext) {
const { visualBuilder, timePicker } = getPageObjects([
'visualBuilder',
'timePicker',
'visualize',
]);
describe('visual builder', function describeIndexTests() {
describe('markdown', () => {
before(async () => {
await visualBuilder.resetPage();
await visualBuilder.clickMarkdown();
await timePicker.setAbsoluteRange('2015-09-22 06:00:00.000', '2015-09-22 11:00:00.000');
});
it('should render subtabs and table variables markdown components', async () => {
const tabs = await visualBuilder.getSubTabs();
expect(tabs).to.have.length(3);
const variables = await visualBuilder.getMarkdownTableVariables();
expect(variables).not.to.be.empty();
expect(variables).to.have.length(5);
});
it('should allow printing raw timestamp of data', async () => {
await visualBuilder.enterMarkdown('{{ count.data.raw.[0].[0] }}');
const text = await visualBuilder.getMarkdownText();
expect(text).to.be('1442901600000');
});
it('should allow printing raw value of data', async () => {
await visualBuilder.enterMarkdown('{{ count.data.raw.[0].[1] }}');
const text = await visualBuilder.getMarkdownText();
expect(text).to.be('6');
});
it('should render html as plain text', async () => {
const html = '<h1>hello world</h1>';
await visualBuilder.enterMarkdown(html);
const markdownText = await visualBuilder.getMarkdownText();
expect(markdownText).to.be(html);
});
it('should render mustache list', async () => {
const list = '{{#each _all}}\n{{ data.formatted.[0] }} {{ data.raw.[0] }}\n{{/each}}';
const expectedRenderer = 'Sep 22, 2015 @ 06:00:00.000,6 1442901600000,6';
await visualBuilder.enterMarkdown(list);
const markdownText = await visualBuilder.getMarkdownText();
expect(markdownText).to.be(expectedRenderer);
});
});
});
}

View file

@ -76,6 +76,7 @@ export default function ({ getService, loadTestFile }) {
loadTestFile(require.resolve('./_vertical_bar_chart'));
loadTestFile(require.resolve('./_vertical_bar_chart_nontimeindex'));
loadTestFile(require.resolve('./_tsvb_chart'));
loadTestFile(require.resolve('./_tsvb_markdown'));
loadTestFile(require.resolve('./_vega_chart'));
});
});

View file

@ -45,7 +45,6 @@ import { ShieldPageProvider } from './shield_page';
import { TimePickerPageProvider } from './time_picker';
// @ts-ignore not TS yet
import { TimelionPageProvider } from './timelion_page';
// @ts-ignore not TS yet
import { VisualBuilderPageProvider } from './visual_builder_page';
// @ts-ignore not TS yet
import { VisualizePageProvider } from './visualize_page';

View file

@ -17,7 +17,9 @@
* under the License.
*/
export function VisualBuilderPageProvider({ getService, getPageObjects }) {
import { FtrProviderContext } from '../ftr_provider_context.d';
export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrProviderContext) {
const find = getService('find');
const retry = getService('retry');
const log = getService('log');
@ -27,38 +29,48 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }) {
const PageObjects = getPageObjects(['common', 'header', 'visualize', 'timePicker']);
class VisualBuilderPage {
async resetPage() {
const fromTime = '2015-09-19 06:31:44.000';
const toTime = '2015-09-22 18:31:44.000';
public async resetPage(
fromTime = '2015-09-19 06:31:44.000',
toTime = '2015-09-22 18:31:44.000'
) {
log.debug('navigateToApp visualize');
await PageObjects.visualize.navigateToNewVisualization();
log.debug('clickVisualBuilderChart');
await PageObjects.visualize.clickVisualBuilder();
log.debug('Set absolute time range from \"' + fromTime + '\" to \"' + toTime + '\"');
log.debug('Set absolute time range from "' + fromTime + '" to "' + toTime + '"');
await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
}
async clickMetric() {
public async clickMetric() {
const button = await testSubjects.find('metricTsvbTypeBtn');
await button.click();
await PageObjects.header.waitUntilLoadingHasFinished();
}
async clickMarkdown() {
public async clickMarkdown() {
const button = await testSubjects.find('markdownTsvbTypeBtn');
await button.click();
await PageObjects.header.waitUntilLoadingHasFinished();
}
async getMetricValue() {
public async getMetricValue() {
const metricValue = await find.byCssSelector('.tvbVisMetric__value--primary');
return metricValue.getVisibleText();
}
async enterMarkdown(markdown) {
const prevRenderingCount = await PageObjects.visualize.getVisualizationRenderingCount();
public async enterMarkdown(markdown: string) {
const input = await find.byCssSelector('.tvbMarkdownEditor__editor textarea');
await this.clearMarkdown();
const prevRenderingCount = await PageObjects.visualize.getVisualizationRenderingCount();
await input.type(markdown);
await PageObjects.visualize.waitForVisualizationRenderingStabilized();
await PageObjects.visualize.waitForRenderingCount(prevRenderingCount + 1);
}
public async clearMarkdown() {
const input = await find.byCssSelector('.tvbMarkdownEditor__editor textarea');
// click for switching context(fix for "should render first table variable" test)
// see _tsvb_markdown.js
// Since we use ACE editor and that isn't really storing its value inside
// a textarea we must really select all text and remove it, and cannot use
// clearValue().
@ -69,80 +81,142 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }) {
}
await input.pressKeys(browser.keys.NULL); // Release modifier keys
await input.pressKeys(browser.keys.BACK_SPACE); // Delete all content
await input.type(markdown);
await PageObjects.visualize.waitForRenderingCount(prevRenderingCount + 1);
}
async getMarkdownText() {
public async getMarkdownText() {
const el = await find.byCssSelector('.tvbEditorVisualization');
return await el.getVisibleText();
const text = await el.getVisibleText();
return text;
}
async clickMarkdownData() {
await testSubjects.click('markdownDataBtn');
/**
*
* getting all markdown variables list which located on `table` section
*
* **Note**: if `table` not have variables, use `getMarkdownTableNoVariables` method instead
* @see {getMarkdownTableNoVariables}
* @returns {Promise<Array<{key:string, value:string, selector:any}>>}
* @memberof VisualBuilderPage
*/
public async getMarkdownTableVariables(): Promise<
Array<{ key: string; value: string; selector: any }>
> {
const testTableVariables = await testSubjects.find('tsvbMarkdownVariablesTable');
const variablesSelector = 'tbody tr';
const exists = await find.existsByDisplayedByCssSelector(variablesSelector);
if (!exists) {
log.debug('variable list is empty');
return [];
}
const variables: any[] = await testTableVariables.findAllByCssSelector(variablesSelector);
const variablesKeyValueSelectorMap = await Promise.all(
variables.map(async (variable: any) => {
const subVars = await variable.findAllByCssSelector('td');
const selector = await subVars[0].findByTagName('a');
const key = await selector.getVisibleText();
const value = await subVars[1].getVisibleText();
log.debug(`markdown table variables table is: ${key} ${value}`);
return { key, value, selector };
})
);
return variablesKeyValueSelectorMap;
}
async clickSeriesOption(nth = 0) {
/**
* return variable table message, if `table` is empty it will be fail
*
* **Note:** if `table` have variables, use `getMarkdownTableVariables` method instead
* @see {@link VisualBuilderPage#getMarkdownTableVariables}
* @returns
* @memberof VisualBuilderPage
*/
public async getMarkdownTableNoVariables() {
return await testSubjects.getVisibleText('tvbMarkdownEditor__noVariables');
}
/**
* get all sub-tabs count for `time series`, `metric`, `top n`, `gauge`, `markdown` or `table` tab.
*
* @returns {Promise<any[]>}
* @memberof VisualBuilderPage
*/
public async getSubTabs() {
return await find.allByCssSelector('[data-test-subj$="-subtab"]');
}
/**
* switch markdown sub-tab for visualization
*
* @param {'data' | 'options'| 'markdown'} subTab
* @memberof VisualBuilderPage
*/
public async markdownSwitchSubTab(subTab: 'data' | 'options' | 'markdown') {
const element = await testSubjects.find(`${subTab}-subtab`);
await element.click();
}
public async clickSeriesOption(nth = 0) {
const el = await testSubjects.findAll('seriesOptions');
await el[nth].click();
await PageObjects.common.sleep(300);
await PageObjects.common.sleep(500);
}
async clearOffsetSeries() {
public async clearOffsetSeries() {
const el = await testSubjects.find('offsetTimeSeries');
await el.clearValue();
await PageObjects.header.waitUntilLoadingHasFinished();
}
async enterOffsetSeries(value) {
public async enterOffsetSeries(value: string) {
const el = await testSubjects.find('offsetTimeSeries');
await el.clearValue();
await el.type(value);
await PageObjects.header.waitUntilLoadingHasFinished();
}
async getRhythmChartLegendValue() {
public async getRhythmChartLegendValue() {
const metricValue = await find.byCssSelector('.tvbLegend__itemValue');
await metricValue.moveMouseTo();
return await metricValue.getVisibleText();
}
async clickGauge() {
public async clickGauge() {
await testSubjects.click('gaugeTsvbTypeBtn');
await PageObjects.header.waitUntilLoadingHasFinished();
}
async getGaugeLabel() {
public async getGaugeLabel() {
const gaugeLabel = await find.byCssSelector('.tvbVisGauge__label');
return await gaugeLabel.getVisibleText();
}
async getGaugeCount() {
public async getGaugeCount() {
const gaugeCount = await find.byCssSelector('.tvbVisGauge__value');
return await gaugeCount.getVisibleText();
}
async clickTopN() {
public async clickTopN() {
await testSubjects.click('top_nTsvbTypeBtn');
await PageObjects.header.waitUntilLoadingHasFinished();
}
async getTopNLabel() {
public async getTopNLabel() {
const topNLabel = await find.byCssSelector('.tvbVisTopN__label');
return await topNLabel.getVisibleText();
}
async getTopNCount() {
public async getTopNCount() {
const gaugeCount = await find.byCssSelector('.tvbVisTopN__value');
return await gaugeCount.getVisibleText();
}
async clickTable() {
public async clickTable() {
await testSubjects.click('tableTsvbTypeBtn');
await PageObjects.header.waitUntilLoadingHasFinished();
}
async createNewAgg(nth = 0) {
public async createNewAgg(nth = 0) {
return await retry.try(async () => {
const elements = await testSubjects.findAll('addMetricAddBtn');
await elements[nth].click();
@ -154,55 +228,59 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }) {
});
}
async selectAggType(value, nth = 0) {
public async selectAggType(value: string | number, nth = 0) {
const elements = await testSubjects.findAll('aggSelector');
await comboBox.setElement(elements[nth], value);
return await PageObjects.header.waitUntilLoadingHasFinished();
}
async fillInExpression(expression, nth = 0) {
public async fillInExpression(expression: string, nth = 0) {
const expressions = await testSubjects.findAll('mathExpression');
await expressions[nth].type(expression);
return await PageObjects.header.waitUntilLoadingHasFinished();
}
async fillInVariable(name = 'test', metric = 'count', nth = 0) {
public async fillInVariable(name = 'test', metric = 'count', nth = 0) {
const elements = await testSubjects.findAll('varRow');
const varNameInput = await elements[nth].findByCssSelector('.tvbAggs__varName');
await varNameInput.type(name);
const metricSelectWrapper = await elements[nth].findByCssSelector('.tvbAggs__varMetricWrapper');
const metricSelectWrapper = await elements[nth].findByCssSelector(
'.tvbAggs__varMetricWrapper'
);
await comboBox.setElement(metricSelectWrapper, metric);
return await PageObjects.header.waitUntilLoadingHasFinished();
}
async selectGroupByField(fieldName) {
public async selectGroupByField(fieldName: string) {
await comboBox.set('groupByField', fieldName);
}
async setLabelValue(value) {
public async setLabelValue(value: string) {
const el = await testSubjects.find('columnLabelName');
await el.clearValue();
await el.type(value);
await PageObjects.header.waitUntilLoadingHasFinished();
}
async getViewTable() {
public async getViewTable() {
const tableView = await testSubjects.find('tableView');
return await tableView.getVisibleText();
}
async clickMetricPanelOptions() {
public async clickMetricPanelOptions() {
const button = await testSubjects.find('metricEditorPanelOptionsBtn');
await button.click();
await PageObjects.header.waitUntilLoadingHasFinished();
}
async setIndexPatternValue(value) {
public async setIndexPatternValue(value: string) {
const el = await testSubjects.find('metricsIndexPatternInput');
await el.clearValue();
await el.type(value);
await PageObjects.header.waitUntilLoadingHasFinished();
}
async selectIndexPatternTimeField(timeField) {
public async selectIndexPatternTimeField(timeField: string) {
const el = await testSubjects.find('comboBoxSearchInput');
await el.clearValue();
await el.type(timeField);

View file

@ -193,6 +193,7 @@
"classnames": "2.2.5",
"concat-stream": "1.5.1",
"constate": "^0.9.0",
"constate-latest": "npm:constate@^1.0.0",
"content-disposition": "0.5.3",
"copy-to-clipboard": "^3.0.8",
"core-js": "2.5.3",

View file

@ -10,13 +10,13 @@ import React from 'react';
import styled from 'styled-components';
import { NOT_AVAILABLE_LABEL } from 'x-pack/plugins/apm/common/i18n';
import { StringMap } from '../../../../typings/common';
import { fontFamilyCode, fontSizes, px, units } from '../../../style/variables';
import { fontFamilyCode, fontSize, px, units } from '../../../style/variables';
export type KeySorter = (data: StringMap, parentKey?: string) => string[];
const Table = styled.table`
font-family: ${fontFamilyCode};
font-size: ${fontSizes.small};
font-size: ${fontSize};
width: 100%;
`;
@ -41,7 +41,7 @@ const Cell = styled.td`
}
&:first-child {
width: ${px(units.unit * 20)};
width: ${px(units.unit * 12)};
font-weight: bold;
}
`;
@ -87,13 +87,14 @@ export function NestedValue({
parentKey?: string;
keySorter?: KeySorter;
}): JSX.Element {
if (depth > 0 && isObject(value)) {
const MAX_LEVEL = 3;
if (depth < MAX_LEVEL && isObject(value)) {
return (
<NestedKeyValueTable
data={value as StringMap}
parentKey={parentKey}
keySorter={keySorter}
depth={depth - 1}
depth={depth + 1}
/>
);
}
@ -105,12 +106,12 @@ export function NestedKeyValueTable({
data,
parentKey,
keySorter = Object.keys,
depth = 0
depth
}: {
data: StringMap;
parentKey?: string;
keySorter?: KeySorter;
depth?: number;
depth: number;
}): JSX.Element {
return (
<Table>

View file

@ -22,38 +22,47 @@ describe('NestedKeyValueTable component', () => {
c: [3, 4, 5],
d: { aa: 1, bb: 2 }
};
expect(shallow(<NestedKeyValueTable data={testData} />)).toMatchSnapshot();
expect(
shallow(<NestedKeyValueTable data={testData} depth={0} />)
).toMatchSnapshot();
});
it('should render an empty table if there is no data', () => {
expect(shallow(<NestedKeyValueTable data={{}} />)).toMatchSnapshot();
expect(
shallow(<NestedKeyValueTable data={{}} depth={0} />)
).toMatchSnapshot();
});
});
describe('NestedValue component', () => {
let props: any;
beforeEach(() => {
props = {
value: { a: 'hello' },
depth: 0,
keySorter: jest.fn(),
parentKey: 'who_cares'
};
});
it('should render a formatted value when depth is 0', () => {
expect(shallow(<NestedValue {...props} />)).toMatchSnapshot();
const wrapper = shallow(
<NestedValue value={{ a: 'hello' }} depth={0} parentKey="who_cares" />
);
expect(
wrapper.equals(
<NestedKeyValueTable
data={{ a: 'hello' }}
depth={1}
parentKey="who_cares"
/>
)
).toBe(true);
});
it('should render a formatted value when depth > 0 but value is not an object', () => {
props.value = 2;
props.depth = 3;
expect(shallow(<NestedValue {...props} />)).toMatchSnapshot();
expect(
shallow(<NestedValue value={2} depth={3} parentKey="who_cares" />)
).toMatchSnapshot();
});
it('should render a nested KV Table when depth > 0 and value is an object', () => {
props.depth = 1;
expect(shallow(<NestedValue {...props} />)).toMatchSnapshot();
expect(
shallow(
<NestedValue value={{ a: 'hello' }} depth={1} parentKey="who_cares" />
)
).toMatchSnapshot();
});
});

View file

@ -279,16 +279,6 @@ exports[`NestedValue component should render a formatted value when depth > 0 bu
/>
`;
exports[`NestedValue component should render a formatted value when depth is 0 1`] = `
<FormattedValue
value={
Object {
"a": "hello",
}
}
/>
`;
exports[`NestedValue component should render a nested KV Table when depth > 0 and value is an object 1`] = `
<NestedKeyValueTable
data={
@ -296,8 +286,7 @@ exports[`NestedValue component should render a nested KV Table when depth > 0 an
"a": "hello",
}
}
depth={0}
keySorter={[MockFunction]}
depth={2}
parentKey="who_cares"
/>
`;

View file

@ -8,7 +8,7 @@ import { EuiIcon } from '@elastic/eui';
import { EuiLink } from '@elastic/eui';
import theme from '@elastic/eui/dist/eui_theme_light.json';
import { i18n } from '@kbn/i18n';
import { get, indexBy, uniq } from 'lodash';
import { get, has, indexBy, uniq } from 'lodash';
import React from 'react';
import styled from 'styled-components';
import { APMError } from 'x-pack/plugins/apm/typings/es_schemas/Error';
@ -42,7 +42,7 @@ const EuiIconWithSpace = styled(EuiIcon)`
export function getPropertyTabNames(obj: Transaction | APMError) {
return PROPERTY_CONFIG.filter(
({ key, required }) => required || obj.hasOwnProperty(key)
({ key, required }) => required || has(obj, key)
).map(({ key, label }) => ({ key, label }));
}

View file

@ -5,37 +5,13 @@
*/
import { i18n } from '@kbn/i18n';
import { APMError } from 'x-pack/plugins/apm/typings/es_schemas/Error';
import { Transaction } from 'x-pack/plugins/apm/typings/es_schemas/Transaction';
export interface Tab {
key: string;
label: string;
}
type AllKeys = keyof NonNullable<Transaction> | keyof NonNullable<APMError>;
interface ConfigItem<T extends AllKeys> {
key: T;
label: string;
required: boolean;
presortedKeys: Array<
T extends keyof Transaction
? keyof NonNullable<Transaction[T]>
: T extends keyof APMError
? keyof NonNullable<APMError[T]>
: never
>;
}
export const PROPERTY_CONFIG = [
{
key: 'url',
label: i18n.translate('xpack.apm.propertiesTable.tabs.urlLabel', {
defaultMessage: 'Url'
}),
required: false,
presortedKeys: []
} as ConfigItem<'url'>,
{
key: 'http',
label: i18n.translate('xpack.apm.propertiesTable.tabs.httpLabel', {
@ -43,7 +19,7 @@ export const PROPERTY_CONFIG = [
}),
required: false,
presortedKeys: []
} as ConfigItem<'http'>,
},
{
key: 'host',
label: i18n.translate('xpack.apm.propertiesTable.tabs.hostLabel', {
@ -51,15 +27,15 @@ export const PROPERTY_CONFIG = [
}),
required: false,
presortedKeys: ['hostname', 'architecture', 'platform']
} as ConfigItem<'host'>,
},
{
key: 'service',
label: i18n.translate('xpack.apm.propertiesTable.tabs.serviceLabel', {
defaultMessage: 'Service'
}),
required: false,
presortedKeys: ['runtime', 'framework', 'agent', 'version']
} as ConfigItem<'service'>,
presortedKeys: ['runtime', 'framework', 'version']
},
{
key: 'process',
label: i18n.translate('xpack.apm.propertiesTable.tabs.processLabel', {
@ -67,7 +43,31 @@ export const PROPERTY_CONFIG = [
}),
required: false,
presortedKeys: ['pid', 'title', 'args']
} as ConfigItem<'process'>,
},
{
key: 'agent',
label: i18n.translate('xpack.apm.propertiesTable.tabs.agentLabel', {
defaultMessage: 'Agent'
}),
required: false,
presortedKeys: []
},
{
key: 'url',
label: i18n.translate('xpack.apm.propertiesTable.tabs.urlLabel', {
defaultMessage: 'URL'
}),
required: false,
presortedKeys: []
},
{
key: 'container',
label: i18n.translate('xpack.apm.propertiesTable.tabs.containerLabel', {
defaultMessage: 'Container'
}),
required: false,
presortedKeys: []
},
{
key: 'user',
label: i18n.translate('xpack.apm.propertiesTable.tabs.userLabel', {
@ -75,7 +75,7 @@ export const PROPERTY_CONFIG = [
}),
required: true,
presortedKeys: ['id', 'username', 'email']
} as ConfigItem<'user'>,
},
{
key: 'labels',
label: i18n.translate('xpack.apm.propertiesTable.tabs.labelsLabel', {
@ -83,5 +83,24 @@ export const PROPERTY_CONFIG = [
}),
required: true,
presortedKeys: []
} as ConfigItem<'labels'>
},
{
key: 'transaction.custom',
label: i18n.translate(
'xpack.apm.propertiesTable.tabs.transactionCustomLabel',
{
defaultMessage: 'Custom'
}
),
required: false,
presortedKeys: []
},
{
key: 'error.custom',
label: i18n.translate('xpack.apm.propertiesTable.tabs.errorCustomLabel', {
defaultMessage: 'Custom'
}),
required: false,
presortedKeys: []
}
];

View file

@ -16,7 +16,6 @@ import {
fontSizes,
px,
truncate,
unit,
units
} from '../../../style/variables';
@ -47,15 +46,16 @@ const PropertyValueDimmed = styled.span`
color: ${theme.euiColorMediumShade};
`;
const propertyValueLineHeight = 1.2;
const PropertyValue = styled.div`
display: inline-block;
line-height: ${px(unit)};
line-height: ${propertyValueLineHeight};
`;
PropertyValue.displayName = 'PropertyValue';
const PropertyValueTruncated = styled.span`
display: inline-block;
line-height: ${px(unit)};
line-height: ${propertyValueLineHeight};
${truncate('100%')};
`;

View file

@ -7,7 +7,6 @@
import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
import { getAutocompleteProvider } from 'ui/autocomplete_providers';
import { StaticIndexPattern } from 'ui/index_patterns';
// @ts-ignore
import { getFromSavedObject } from 'ui/index_patterns/static_utils';
import { getAPMIndexPattern } from './rest/savedObjects';

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { first, isEmpty, memoize } from 'lodash';
import { memoize } from 'lodash';
import chrome from 'ui/chrome';
import { callApi } from './callApi';
@ -34,15 +34,7 @@ export const getAPMIndexPattern = memoize(async () => {
}
});
if (isEmpty(res.saved_objects)) {
return;
}
const apmSavedObject = first(
res.saved_objects.filter(
savedObject => savedObject.attributes.title === apmIndexPatternTitle
)
return res.saved_objects.find(
savedObject => savedObject.attributes.title === apmIndexPatternTitle
);
return apmSavedObject;
});

View file

@ -30,7 +30,8 @@ describe('timeseriesFetcher', () => {
end: 1528977600000,
client: clientSpy,
config: {
get: () => 'myIndex' as any
get: () => 'myIndex' as any,
has: () => true
}
}
});

View file

@ -24,7 +24,8 @@ describe('transactionGroupsFetcher', () => {
case 'xpack.apm.ui.transactionGroupBucketSize':
return 100;
}
})
}),
has: () => true
}
};
const bodyQuery = { my: 'bodyQuery' };

View file

@ -26,7 +26,8 @@ describe('getAnomalySeries', () => {
end: 500000,
client: clientSpy,
config: {
get: () => 'myIndex' as any
get: () => 'myIndex' as any,
has: () => true
}
}
})) as AnomalyTimeSeriesResponse;

View file

@ -22,7 +22,8 @@ describe('timeseriesFetcher', () => {
end: 1528977600000,
client: clientSpy,
config: {
get: () => 'myIndex' as any
get: () => 'myIndex' as any,
has: () => true
}
}
});

View file

@ -10,6 +10,7 @@ interface TestSetup extends Setup {
client: jest.Mock;
config: {
get: jest.Mock;
has: () => boolean;
};
}
@ -22,7 +23,8 @@ export function getSetupMock(overrides: Partial<TestSetup> = {}) {
term: { field: 'test.esfilter.query' }
},
config: {
get: jest.fn()
get: jest.fn(),
has: () => true
},
...overrides
};

View file

@ -0,0 +1,79 @@
/*
* 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 { mountHook } from 'test_utils/enzyme_helpers';
import { useLogViewConfiguration } from './log_view_configuration';
describe('useLogViewConfiguration hook', () => {
describe('textScale state', () => {
it('has a default value', () => {
const { getLastHookValue } = mountHook(() => useLogViewConfiguration().textScale);
expect(getLastHookValue()).toEqual('medium');
});
it('can be updated', () => {
const { act, getLastHookValue } = mountHook(() => useLogViewConfiguration());
act(({ setTextScale }) => {
setTextScale('small');
});
expect(getLastHookValue().textScale).toEqual('small');
});
});
describe('textWrap state', () => {
it('has a default value', () => {
const { getLastHookValue } = mountHook(() => useLogViewConfiguration().textWrap);
expect(getLastHookValue()).toEqual(true);
});
it('can be updated', () => {
const { act, getLastHookValue } = mountHook(() => useLogViewConfiguration());
act(({ setTextWrap }) => {
setTextWrap(false);
});
expect(getLastHookValue().textWrap).toEqual(false);
});
});
describe('intervalSize state', () => {
it('has a default value', () => {
const { getLastHookValue } = mountHook(() => useLogViewConfiguration().intervalSize);
expect(getLastHookValue()).toEqual(86400000);
});
it('can be updated', () => {
const { act, getLastHookValue } = mountHook(() => useLogViewConfiguration());
act(({ setIntervalSize }) => {
setIntervalSize(90000000);
});
expect(getLastHookValue().intervalSize).toEqual(90000000);
});
});
it('provides the available text scales', () => {
const { getLastHookValue } = mountHook(() => useLogViewConfiguration().availableTextScales);
expect(getLastHookValue()).toEqual(expect.any(Array));
expect(getLastHookValue().length).toBeGreaterThan(0);
});
it('provides the available interval sizes', () => {
const { getLastHookValue } = mountHook(() => useLogViewConfiguration().availableIntervalSizes);
expect(getLastHookValue()).toEqual(expect.any(Array));
expect(getLastHookValue().length).toBeGreaterThan(0);
});
});

View file

@ -0,0 +1,80 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import createContainer from 'constate-latest';
import { useState } from 'react';
export type TextScale = 'small' | 'medium' | 'large';
export const useLogViewConfiguration = () => {
// text scale
const [textScale, setTextScale] = useState<TextScale>('medium');
// text wrap
const [textWrap, setTextWrap] = useState<boolean>(true);
// minimap interval
const [intervalSize, setIntervalSize] = useState<number>(1000 * 60 * 60 * 24);
return {
availableIntervalSizes,
availableTextScales,
setTextScale,
setTextWrap,
textScale,
textWrap,
intervalSize,
setIntervalSize,
};
};
export const LogViewConfiguration = createContainer(useLogViewConfiguration);
/**
* constants
*/
export const availableTextScales: TextScale[] = ['large', 'medium', 'small'];
export const availableIntervalSizes = [
{
label: i18n.translate('xpack.infra.mapLogs.oneYearLabel', {
defaultMessage: '1 Year',
}),
intervalSize: 1000 * 60 * 60 * 24 * 365,
},
{
label: i18n.translate('xpack.infra.mapLogs.oneMonthLabel', {
defaultMessage: '1 Month',
}),
intervalSize: 1000 * 60 * 60 * 24 * 30,
},
{
label: i18n.translate('xpack.infra.mapLogs.oneWeekLabel', {
defaultMessage: '1 Week',
}),
intervalSize: 1000 * 60 * 60 * 24 * 7,
},
{
label: i18n.translate('xpack.infra.mapLogs.oneDayLabel', {
defaultMessage: '1 Day',
}),
intervalSize: 1000 * 60 * 60 * 24,
},
{
label: i18n.translate('xpack.infra.mapLogs.oneHourLabel', {
defaultMessage: '1 Hour',
}),
intervalSize: 1000 * 60 * 60,
},
{
label: i18n.translate('xpack.infra.mapLogs.oneMinuteLabel', {
defaultMessage: '1 Minute',
}),
intervalSize: 1000 * 60,
},
];

View file

@ -4,97 +4,42 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import React, { useContext, useMemo } from 'react';
import { logMinimapActions, logMinimapSelectors, State } from '../../store';
import { asChildFunctionRenderer } from '../../utils/typed_react';
import { bindPlainActionCreators } from '../../utils/typed_redux';
import { UrlStateContainer } from '../../utils/url_state';
export const withLogMinimap = connect(
(state: State) => ({
availableIntervalSizes,
intervalSize: logMinimapSelectors.selectMinimapIntervalSize(state),
urlState: selectMinimapUrlState(state),
}),
bindPlainActionCreators({
setIntervalSize: logMinimapActions.setMinimapIntervalSize,
})
);
export const WithLogMinimap = asChildFunctionRenderer(withLogMinimap);
export const availableIntervalSizes = [
{
label: i18n.translate('xpack.infra.mapLogs.oneYearLabel', {
defaultMessage: '1 Year',
}),
intervalSize: 1000 * 60 * 60 * 24 * 365,
},
{
label: i18n.translate('xpack.infra.mapLogs.oneMonthLabel', {
defaultMessage: '1 Month',
}),
intervalSize: 1000 * 60 * 60 * 24 * 30,
},
{
label: i18n.translate('xpack.infra.mapLogs.oneWeekLabel', {
defaultMessage: '1 Week',
}),
intervalSize: 1000 * 60 * 60 * 24 * 7,
},
{
label: i18n.translate('xpack.infra.mapLogs.oneDayLabel', {
defaultMessage: '1 Day',
}),
intervalSize: 1000 * 60 * 60 * 24,
},
{
label: i18n.translate('xpack.infra.mapLogs.oneHourLabel', {
defaultMessage: '1 Hour',
}),
intervalSize: 1000 * 60 * 60,
},
{
label: i18n.translate('xpack.infra.mapLogs.oneMinuteLabel', {
defaultMessage: '1 Minute',
}),
intervalSize: 1000 * 60,
},
];
import { LogViewConfiguration } from './log_view_configuration';
/**
* Url State
*/
interface LogMinimapUrlState {
intervalSize?: ReturnType<typeof logMinimapSelectors.selectMinimapIntervalSize>;
intervalSize?: number;
}
export const WithLogMinimapUrlState = () => (
<WithLogMinimap>
{({ urlState, setIntervalSize }) => (
<UrlStateContainer
urlState={urlState}
urlStateKey="logMinimap"
mapToUrlState={mapToUrlState}
onChange={newUrlState => {
if (newUrlState && newUrlState.intervalSize) {
setIntervalSize(newUrlState.intervalSize);
}
}}
onInitialize={newUrlState => {
if (newUrlState && newUrlState.intervalSize) {
setIntervalSize(newUrlState.intervalSize);
}
}}
/>
)}
</WithLogMinimap>
);
export const WithLogMinimapUrlState = () => {
const { intervalSize, setIntervalSize } = useContext(LogViewConfiguration.Context);
const urlState = useMemo(() => ({ intervalSize }), [intervalSize]);
return (
<UrlStateContainer
urlState={urlState}
urlStateKey="logMinimap"
mapToUrlState={mapToUrlState}
onChange={newUrlState => {
if (newUrlState && newUrlState.intervalSize) {
setIntervalSize(newUrlState.intervalSize);
}
}}
onInitialize={newUrlState => {
if (newUrlState && newUrlState.intervalSize) {
setIntervalSize(newUrlState.intervalSize);
}
}}
/>
);
};
const mapToUrlState = (value: any): LogMinimapUrlState | undefined =>
value
@ -105,10 +50,3 @@ const mapToUrlState = (value: any): LogMinimapUrlState | undefined =>
const mapToIntervalSizeUrlState = (value: any) =>
value && typeof value === 'number' ? value : undefined;
const selectMinimapUrlState = createSelector(
logMinimapSelectors.selectMinimapIntervalSize,
intervalSize => ({
intervalSize,
})
);

View file

@ -4,69 +4,47 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import React, { useContext, useMemo } from 'react';
import { TextScale } from '../../../common/log_text_scale';
import { logTextviewActions, logTextviewSelectors, State } from '../../store';
import { asChildFunctionRenderer } from '../../utils/typed_react';
import { bindPlainActionCreators } from '../../utils/typed_redux';
import { UrlStateContainer } from '../../utils/url_state';
const availableTextScales = ['large', 'medium', 'small'] as TextScale[];
export const withLogTextview = connect(
(state: State) => ({
availableTextScales,
textScale: logTextviewSelectors.selectTextviewScale(state),
urlState: selectTextviewUrlState(state),
wrap: logTextviewSelectors.selectTextviewWrap(state),
}),
bindPlainActionCreators({
setTextScale: logTextviewActions.setTextviewScale,
setTextWrap: logTextviewActions.setTextviewWrap,
})
);
export const WithLogTextview = asChildFunctionRenderer(withLogTextview);
/**
* Url State
*/
import { availableTextScales, LogViewConfiguration, TextScale } from './log_view_configuration';
interface LogTextviewUrlState {
textScale?: ReturnType<typeof logTextviewSelectors.selectTextviewScale>;
wrap?: ReturnType<typeof logTextviewSelectors.selectTextviewWrap>;
textScale?: TextScale;
wrap?: boolean;
}
export const WithLogTextviewUrlState = () => (
<WithLogTextview>
{({ urlState, setTextScale, setTextWrap }) => (
<UrlStateContainer
urlState={urlState}
urlStateKey="logTextview"
mapToUrlState={mapToUrlState}
onChange={newUrlState => {
if (newUrlState && newUrlState.textScale) {
setTextScale(newUrlState.textScale);
}
if (newUrlState && typeof newUrlState.wrap !== 'undefined') {
setTextWrap(newUrlState.wrap);
}
}}
onInitialize={newUrlState => {
if (newUrlState && newUrlState.textScale) {
setTextScale(newUrlState.textScale);
}
if (newUrlState && typeof newUrlState.wrap !== 'undefined') {
setTextWrap(newUrlState.wrap);
}
}}
/>
)}
</WithLogTextview>
);
export const WithLogTextviewUrlState = () => {
const { textScale, textWrap, setTextScale, setTextWrap } = useContext(
LogViewConfiguration.Context
);
const urlState = useMemo(() => ({ textScale, wrap: textWrap }), [textScale, textWrap]);
return (
<UrlStateContainer
urlState={urlState}
urlStateKey="logTextview"
mapToUrlState={mapToUrlState}
onChange={newUrlState => {
if (newUrlState && newUrlState.textScale) {
setTextScale(newUrlState.textScale);
}
if (newUrlState && typeof newUrlState.wrap !== 'undefined') {
setTextWrap(newUrlState.wrap);
}
}}
onInitialize={newUrlState => {
if (newUrlState && newUrlState.textScale) {
setTextScale(newUrlState.textScale);
}
if (newUrlState && typeof newUrlState.wrap !== 'undefined') {
setTextWrap(newUrlState.wrap);
}
}}
/>
);
};
const mapToUrlState = (value: any): LogTextviewUrlState | undefined =>
value
@ -80,12 +58,3 @@ const mapToTextScaleUrlState = (value: any) =>
availableTextScales.includes(value) ? (value as TextScale) : undefined;
const mapToWrapUrlState = (value: any) => (typeof value === 'boolean' ? value : undefined);
const selectTextviewUrlState = createSelector(
logTextviewSelectors.selectTextviewScale,
logTextviewSelectors.selectTextviewWrap,
(textScale, wrap) => ({
textScale,
wrap,
})
);

View file

@ -21,6 +21,7 @@ import { ColumnarPage } from '../../components/page';
import { SourceConfigurationFlyout } from '../../components/source_configuration';
import { WithSourceConfigurationFlyoutState } from '../../components/source_configuration/source_configuration_flyout_state';
import { LogViewConfiguration } from '../../containers/logs/log_view_configuration';
import { WithLogFilter, WithLogFilterUrlState } from '../../containers/logs/with_log_filter';
import { WithLogFlyout } from '../../containers/logs/with_log_flyout';
import { WithFlyoutOptions } from '../../containers/logs/with_log_flyout_options';
@ -43,128 +44,130 @@ export const LogsPage = injectI18n(
const { intl } = this.props;
return (
<ColumnarPage>
<Header
breadcrumbs={[
{
text: intl.formatMessage({
id: 'xpack.infra.logsPage.logsBreadcrumbsText',
defaultMessage: 'Logs',
}),
},
]}
/>
<WithSource>
{({
derivedIndexPattern,
hasFailed,
isLoading,
lastFailureMessage,
load,
logIndicesExist,
sourceId,
}) => (
<>
<DocumentTitle
title={intl.formatMessage({
id: 'xpack.infra.logsPage.documentTitle',
<LogViewConfiguration.Provider>
<ColumnarPage>
<Header
breadcrumbs={[
{
text: intl.formatMessage({
id: 'xpack.infra.logsPage.logsBreadcrumbsText',
defaultMessage: 'Logs',
})}
/>
<HelpCenterContent
feedbackLink="https://discuss.elastic.co/c/logs"
feedbackLinkText={intl.formatMessage({
id: 'xpack.infra.logsPage.logsHelpContent.feedbackLinkText',
defaultMessage: 'Provide feedback for Logs',
})}
/>
<SourceConfigurationFlyout />
{isLoading ? (
<SourceLoadingPage />
) : logIndicesExist ? (
<>
<WithLogFilterUrlState indexPattern={derivedIndexPattern} />
<WithLogPositionUrlState />
<WithLogMinimapUrlState />
<WithLogTextviewUrlState />
<WithFlyoutOptionsUrlState />
<LogsToolbar />
<WithLogFilter indexPattern={derivedIndexPattern}>
{({ applyFilterQueryFromKueryExpression }) => (
<React.Fragment>
<WithFlyoutOptions>
{({ showFlyout, setFlyoutItem }) => (
<LogsPageContent
showFlyout={showFlyout}
setFlyoutItem={setFlyoutItem}
/>
)}
</WithFlyoutOptions>
<WithLogFlyout sourceId={sourceId}>
{({ flyoutItem, hideFlyout, loading }) => (
<LogFlyout
setFilter={applyFilterQueryFromKueryExpression}
flyoutItem={flyoutItem}
hideFlyout={hideFlyout}
loading={loading}
/>
)}
</WithLogFlyout>
</React.Fragment>
}),
},
]}
/>
<WithSource>
{({
derivedIndexPattern,
hasFailed,
isLoading,
lastFailureMessage,
load,
logIndicesExist,
sourceId,
}) => (
<>
<DocumentTitle
title={intl.formatMessage({
id: 'xpack.infra.logsPage.documentTitle',
defaultMessage: 'Logs',
})}
/>
<HelpCenterContent
feedbackLink="https://discuss.elastic.co/c/logs"
feedbackLinkText={intl.formatMessage({
id: 'xpack.infra.logsPage.logsHelpContent.feedbackLinkText',
defaultMessage: 'Provide feedback for Logs',
})}
/>
<SourceConfigurationFlyout />
{isLoading ? (
<SourceLoadingPage />
) : logIndicesExist ? (
<>
<WithLogFilterUrlState indexPattern={derivedIndexPattern} />
<WithLogPositionUrlState />
<WithLogMinimapUrlState />
<WithLogTextviewUrlState />
<WithFlyoutOptionsUrlState />
<LogsToolbar />
<WithLogFilter indexPattern={derivedIndexPattern}>
{({ applyFilterQueryFromKueryExpression }) => (
<React.Fragment>
<WithFlyoutOptions>
{({ showFlyout, setFlyoutItem }) => (
<LogsPageContent
showFlyout={showFlyout}
setFlyoutItem={setFlyoutItem}
/>
)}
</WithFlyoutOptions>
<WithLogFlyout sourceId={sourceId}>
{({ flyoutItem, hideFlyout, loading }) => (
<LogFlyout
setFilter={applyFilterQueryFromKueryExpression}
flyoutItem={flyoutItem}
hideFlyout={hideFlyout}
loading={loading}
/>
)}
</WithLogFlyout>
</React.Fragment>
)}
</WithLogFilter>
</>
) : hasFailed ? (
<SourceErrorPage errorMessage={lastFailureMessage || ''} retry={load} />
) : (
<WithKibanaChrome>
{({ basePath }) => (
<NoIndices
title={intl.formatMessage({
id: 'xpack.infra.logsPage.noLoggingIndicesTitle',
defaultMessage: "Looks like you don't have any logging indices.",
})}
message={intl.formatMessage({
id: 'xpack.infra.logsPage.noLoggingIndicesDescription',
defaultMessage: "Let's add some!",
})}
actions={
<EuiFlexGroup>
<EuiFlexItem>
<EuiButton
href={`${basePath}/app/kibana#/home/tutorial_directory/logging`}
color="primary"
fill
>
{intl.formatMessage({
id:
'xpack.infra.logsPage.noLoggingIndicesInstructionsActionLabel',
defaultMessage: 'View setup instructions',
})}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<WithSourceConfigurationFlyoutState>
{({ enable }) => (
<EuiButton color="primary" onClick={enable}>
{intl.formatMessage({
id: 'xpack.infra.configureSourceActionLabel',
defaultMessage: 'Change source configuration',
})}
</EuiButton>
)}
</WithSourceConfigurationFlyoutState>
</EuiFlexItem>
</EuiFlexGroup>
}
/>
)}
</WithLogFilter>
</>
) : hasFailed ? (
<SourceErrorPage errorMessage={lastFailureMessage || ''} retry={load} />
) : (
<WithKibanaChrome>
{({ basePath }) => (
<NoIndices
title={intl.formatMessage({
id: 'xpack.infra.logsPage.noLoggingIndicesTitle',
defaultMessage: "Looks like you don't have any logging indices.",
})}
message={intl.formatMessage({
id: 'xpack.infra.logsPage.noLoggingIndicesDescription',
defaultMessage: "Let's add some!",
})}
actions={
<EuiFlexGroup>
<EuiFlexItem>
<EuiButton
href={`${basePath}/app/kibana#/home/tutorial_directory/logging`}
color="primary"
fill
>
{intl.formatMessage({
id:
'xpack.infra.logsPage.noLoggingIndicesInstructionsActionLabel',
defaultMessage: 'View setup instructions',
})}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<WithSourceConfigurationFlyoutState>
{({ enable }) => (
<EuiButton color="primary" onClick={enable}>
{intl.formatMessage({
id: 'xpack.infra.configureSourceActionLabel',
defaultMessage: 'Change source configuration',
})}
</EuiButton>
)}
</WithSourceConfigurationFlyoutState>
</EuiFlexItem>
</EuiFlexGroup>
}
/>
)}
</WithKibanaChrome>
)}
</>
)}
</WithSource>
</ColumnarPage>
</WithKibanaChrome>
)}
</>
)}
</WithSource>
</ColumnarPage>
</LogViewConfiguration.Provider>
);
}
}

View file

@ -4,16 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import React, { useContext } from 'react';
import styled from 'styled-components';
import { AutoSizer } from '../../components/auto_sizer';
import { LogMinimap } from '../../components/logging/log_minimap';
import { ScrollableLogTextStreamView } from '../../components/logging/log_text_stream';
import { PageContent } from '../../components/page';
import { WithLogMinimap } from '../../containers/logs/with_log_minimap';
import { LogViewConfiguration } from '../../containers/logs/log_view_configuration';
import { WithLogPosition } from '../../containers/logs/with_log_position';
import { WithLogTextview } from '../../containers/logs/with_log_textview';
import { WithStreamItems } from '../../containers/logs/with_stream_items';
import { WithSummary } from '../../containers/logs/with_summary';
@ -22,95 +21,91 @@ interface Props {
showFlyout: () => void;
}
export const LogsPageContent: React.SFC<Props> = ({ showFlyout, setFlyoutItem }) => (
<PageContent>
<AutoSizer content>
{({ measureRef, content: { width = 0, height = 0 } }) => (
<LogPageEventStreamColumn innerRef={measureRef}>
<WithLogTextview>
{({ textScale, wrap }) => (
<WithLogPosition>
{({
isAutoReloading,
jumpToTargetPosition,
reportVisiblePositions,
targetPosition,
}) => (
<WithStreamItems>
export const LogsPageContent: React.FunctionComponent<Props> = ({ showFlyout, setFlyoutItem }) => {
const { intervalSize, textScale, textWrap } = useContext(LogViewConfiguration.Context);
return (
<PageContent>
<AutoSizer content>
{({ measureRef, content: { width = 0, height = 0 } }) => (
<LogPageEventStreamColumn innerRef={measureRef}>
<WithLogPosition>
{({
isAutoReloading,
jumpToTargetPosition,
reportVisiblePositions,
targetPosition,
}) => (
<WithStreamItems>
{({
hasMoreAfterEnd,
hasMoreBeforeStart,
isLoadingMore,
isReloading,
items,
lastLoadedTime,
loadNewerEntries,
}) => (
<ScrollableLogTextStreamView
hasMoreAfterEnd={hasMoreAfterEnd}
hasMoreBeforeStart={hasMoreBeforeStart}
height={height}
isLoadingMore={isLoadingMore}
isReloading={isReloading}
isStreaming={isAutoReloading}
items={items}
jumpToTarget={jumpToTargetPosition}
lastLoadedTime={lastLoadedTime}
loadNewerItems={loadNewerEntries}
reportVisibleInterval={reportVisiblePositions}
scale={textScale}
target={targetPosition}
width={width}
wrap={textWrap}
setFlyoutItem={setFlyoutItem}
showFlyout={showFlyout}
/>
)}
</WithStreamItems>
)}
</WithLogPosition>
</LogPageEventStreamColumn>
)}
</AutoSizer>
<AutoSizer content>
{({ measureRef, content: { width = 0, height = 0 } }) => {
return (
<LogPageMinimapColumn innerRef={measureRef}>
<WithSummary>
{({ buckets }) => (
<WithLogPosition>
{({
hasMoreAfterEnd,
hasMoreBeforeStart,
isLoadingMore,
isReloading,
items,
lastLoadedTime,
loadNewerEntries,
jumpToTargetPosition,
reportVisibleSummary,
visibleMidpointTime,
visibleTimeInterval,
}) => (
<ScrollableLogTextStreamView
hasMoreAfterEnd={hasMoreAfterEnd}
hasMoreBeforeStart={hasMoreBeforeStart}
<LogMinimap
height={height}
isLoadingMore={isLoadingMore}
isReloading={isReloading}
isStreaming={isAutoReloading}
items={items}
jumpToTarget={jumpToTargetPosition}
lastLoadedTime={lastLoadedTime}
loadNewerItems={loadNewerEntries}
reportVisibleInterval={reportVisiblePositions}
scale={textScale}
target={targetPosition}
width={width}
wrap={wrap}
setFlyoutItem={setFlyoutItem}
showFlyout={showFlyout}
highlightedInterval={visibleTimeInterval}
intervalSize={intervalSize}
jumpToTarget={jumpToTargetPosition}
reportVisibleInterval={reportVisibleSummary}
summaryBuckets={buckets}
target={visibleMidpointTime}
/>
)}
</WithStreamItems>
</WithLogPosition>
)}
</WithLogPosition>
)}
</WithLogTextview>
</LogPageEventStreamColumn>
)}
</AutoSizer>
<AutoSizer content>
{({ measureRef, content: { width = 0, height = 0 } }) => {
return (
<LogPageMinimapColumn innerRef={measureRef}>
<WithLogMinimap>
{({ intervalSize }) => (
<WithSummary>
{({ buckets }) => (
<WithLogPosition>
{({
jumpToTargetPosition,
reportVisibleSummary,
visibleMidpointTime,
visibleTimeInterval,
}) => (
<LogMinimap
height={height}
width={width}
highlightedInterval={visibleTimeInterval}
intervalSize={intervalSize}
jumpToTarget={jumpToTargetPosition}
reportVisibleInterval={reportVisibleSummary}
summaryBuckets={buckets}
target={visibleMidpointTime}
/>
)}
</WithLogPosition>
)}
</WithSummary>
)}
</WithLogMinimap>
</LogPageMinimapColumn>
);
}}
</AutoSizer>
</PageContent>
);
</WithSummary>
</LogPageMinimapColumn>
);
}}
</AutoSizer>
</PageContent>
);
};
const LogPageEventStreamColumn = styled.div`
flex: 1 0 0%;

View file

@ -6,7 +6,7 @@
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { injectI18n } from '@kbn/i18n/react';
import React from 'react';
import React, { useContext } from 'react';
import { AutocompleteField } from '../../components/autocomplete_field';
import { Toolbar } from '../../components/eui';
@ -16,95 +16,97 @@ import { LogTextScaleControls } from '../../components/logging/log_text_scale_co
import { LogTextWrapControls } from '../../components/logging/log_text_wrap_controls';
import { LogTimeControls } from '../../components/logging/log_time_controls';
import { SourceConfigurationButton } from '../../components/source_configuration';
import { LogViewConfiguration } from '../../containers/logs/log_view_configuration';
import { WithLogFilter } from '../../containers/logs/with_log_filter';
import { WithLogMinimap } from '../../containers/logs/with_log_minimap';
import { WithLogPosition } from '../../containers/logs/with_log_position';
import { WithLogTextview } from '../../containers/logs/with_log_textview';
import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion';
import { WithSource } from '../../containers/with_source';
export const LogsToolbar = injectI18n(({ intl }) => (
<Toolbar>
<WithSource>
{({ configuration, derivedIndexPattern }) => (
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="s">
<EuiFlexItem>
<WithKueryAutocompletion indexPattern={derivedIndexPattern}>
{({ isLoadingSuggestions, loadSuggestions, suggestions }) => (
<WithLogFilter indexPattern={derivedIndexPattern}>
{({
applyFilterQueryFromKueryExpression,
filterQueryDraft,
isFilterQueryDraftValid,
setFilterQueryDraftFromKueryExpression,
}) => (
<AutocompleteField
isLoadingSuggestions={isLoadingSuggestions}
isValid={isFilterQueryDraftValid}
loadSuggestions={loadSuggestions}
onChange={setFilterQueryDraftFromKueryExpression}
onSubmit={applyFilterQueryFromKueryExpression}
placeholder={intl.formatMessage({
id: 'xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder',
defaultMessage: 'Search for log entries… (e.g. host.name:host-1)',
})}
suggestions={suggestions}
value={filterQueryDraft ? filterQueryDraft.expression : ''}
/>
)}
</WithLogFilter>
)}
</WithKueryAutocompletion>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SourceConfigurationButton />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<LogCustomizationMenu>
<WithLogMinimap>
{({ availableIntervalSizes, intervalSize, setIntervalSize }) => (
<LogMinimapScaleControls
availableIntervalSizes={availableIntervalSizes}
setIntervalSize={setIntervalSize}
intervalSize={intervalSize}
export const LogsToolbar = injectI18n(({ intl }) => {
const {
availableIntervalSizes,
availableTextScales,
intervalSize,
setIntervalSize,
setTextScale,
setTextWrap,
textScale,
textWrap,
} = useContext(LogViewConfiguration.Context);
return (
<Toolbar>
<WithSource>
{({ derivedIndexPattern }) => (
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="s">
<EuiFlexItem>
<WithKueryAutocompletion indexPattern={derivedIndexPattern}>
{({ isLoadingSuggestions, loadSuggestions, suggestions }) => (
<WithLogFilter indexPattern={derivedIndexPattern}>
{({
applyFilterQueryFromKueryExpression,
filterQueryDraft,
isFilterQueryDraftValid,
setFilterQueryDraftFromKueryExpression,
}) => (
<AutocompleteField
isLoadingSuggestions={isLoadingSuggestions}
isValid={isFilterQueryDraftValid}
loadSuggestions={loadSuggestions}
onChange={setFilterQueryDraftFromKueryExpression}
onSubmit={applyFilterQueryFromKueryExpression}
placeholder={intl.formatMessage({
id: 'xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder',
defaultMessage: 'Search for log entries… (e.g. host.name:host-1)',
})}
suggestions={suggestions}
value={filterQueryDraft ? filterQueryDraft.expression : ''}
/>
)}
</WithLogFilter>
)}
</WithKueryAutocompletion>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SourceConfigurationButton />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<LogCustomizationMenu>
<LogMinimapScaleControls
availableIntervalSizes={availableIntervalSizes}
setIntervalSize={setIntervalSize}
intervalSize={intervalSize}
/>
<LogTextWrapControls wrap={textWrap} setTextWrap={setTextWrap} />
<LogTextScaleControls
availableTextScales={availableTextScales}
textScale={textScale}
setTextScale={setTextScale}
/>
</LogCustomizationMenu>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<WithLogPosition resetOnUnmount>
{({
visibleMidpointTime,
isAutoReloading,
jumpToTargetPositionTime,
startLiveStreaming,
stopLiveStreaming,
}) => (
<LogTimeControls
currentTime={visibleMidpointTime}
isLiveStreaming={isAutoReloading}
jumpToTime={jumpToTargetPositionTime}
startLiveStreaming={startLiveStreaming}
stopLiveStreaming={stopLiveStreaming}
/>
)}
</WithLogMinimap>
<WithLogTextview>
{({ availableTextScales, textScale, setTextScale, setTextWrap, wrap }) => (
<>
<LogTextWrapControls wrap={wrap} setTextWrap={setTextWrap} />
<LogTextScaleControls
availableTextScales={availableTextScales}
textScale={textScale}
setTextScale={setTextScale}
/>
</>
)}
</WithLogTextview>
</LogCustomizationMenu>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<WithLogPosition resetOnUnmount>
{({
visibleMidpointTime,
isAutoReloading,
jumpToTargetPositionTime,
startLiveStreaming,
stopLiveStreaming,
}) => (
<LogTimeControls
currentTime={visibleMidpointTime}
isLiveStreaming={isAutoReloading}
jumpToTime={jumpToTargetPositionTime}
startLiveStreaming={startLiveStreaming}
stopLiveStreaming={stopLiveStreaming}
/>
)}
</WithLogPosition>
</EuiFlexItem>
</EuiFlexGroup>
)}
</WithSource>
</Toolbar>
));
</WithLogPosition>
</EuiFlexItem>
</EuiFlexGroup>
)}
</WithSource>
</Toolbar>
);
});

View file

@ -6,9 +6,7 @@
export {
logFilterActions,
logMinimapActions,
logPositionActions,
logTextviewActions,
metricTimeActions,
waffleFilterActions,
waffleTimeActions,

View file

@ -5,9 +5,7 @@
*/
export { logFilterActions } from './log_filter';
export { logMinimapActions } from './log_minimap';
export { logPositionActions } from './log_position';
export { logTextviewActions } from './log_textview';
export { metricTimeActions } from './metric_time';
export { waffleFilterActions } from './waffle_filter';
export { waffleTimeActions } from './waffle_time';

View file

@ -1,11 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import actionCreatorFactory from 'typescript-fsa';
const actionCreator = actionCreatorFactory('x-pack/infra/local/log_minimap');
export const setMinimapIntervalSize = actionCreator<number>('SET_MINIMAP_INTERVAL_SIZE');

View file

@ -1,11 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as logMinimapActions from './actions';
import * as logMinimapSelectors from './selectors';
export { logMinimapActions, logMinimapSelectors };
export * from './reducer';

View file

@ -1,23 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { reducerWithInitialState } from 'typescript-fsa-reducers/dist';
import { setMinimapIntervalSize } from './actions';
export interface LogMinimapState {
intervalSize: number;
}
export const initialLogMinimapState: LogMinimapState = {
intervalSize: 1000 * 60 * 60 * 24,
};
export const logMinimapReducer = reducerWithInitialState(initialLogMinimapState)
.case(setMinimapIntervalSize, (state, intervalSize) => ({
intervalSize,
}))
.build();

View file

@ -1,9 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { LogMinimapState } from './reducer';
export const selectMinimapIntervalSize = (state: LogMinimapState) => state.intervalSize;

View file

@ -1,15 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import actionCreatorFactory from 'typescript-fsa';
import { TextScale } from '../../../../common/log_text_scale';
const actionCreator = actionCreatorFactory('x-pack/infra/local/log_textview');
export const setTextviewScale = actionCreator<TextScale>('SET_TEXTVIEW_SCALE');
export const setTextviewWrap = actionCreator<boolean>('SET_TEXTVIEW_WRAP');

View file

@ -1,11 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as logTextviewActions from './actions';
import * as logTextviewSelectors from './selectors';
export { logTextviewActions, logTextviewSelectors };
export * from './reducer';

View file

@ -1,36 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { combineReducers } from 'redux';
import { reducerWithInitialState } from 'typescript-fsa-reducers/dist';
import { TextScale } from '../../../../common/log_text_scale';
import { setTextviewScale, setTextviewWrap } from './actions';
export interface LogTextviewState {
scale: TextScale;
wrap: boolean;
}
export const initialLogTextviewState: LogTextviewState = {
scale: 'medium',
wrap: true,
};
const textviewScaleReducer = reducerWithInitialState(initialLogTextviewState.scale).case(
setTextviewScale,
(state, scale) => scale
);
const textviewWrapReducer = reducerWithInitialState(initialLogTextviewState.wrap).case(
setTextviewWrap,
(state, wrap) => wrap
);
export const logTextviewReducer = combineReducers<LogTextviewState>({
scale: textviewScaleReducer,
wrap: textviewWrapReducer,
});

View file

@ -1,11 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { LogTextviewState } from './reducer';
export const selectTextviewScale = (state: LogTextviewState) => state.scale;
export const selectTextviewWrap = (state: LogTextviewState) => state.wrap;

View file

@ -8,9 +8,7 @@ import { combineReducers } from 'redux';
import { initialLogFilterState, logFilterReducer, LogFilterState } from './log_filter';
import { flyoutOptionsReducer, FlyoutOptionsState, initialFlyoutOptionsState } from './log_flyout';
import { initialLogMinimapState, logMinimapReducer, LogMinimapState } from './log_minimap';
import { initialLogPositionState, logPositionReducer, LogPositionState } from './log_position';
import { initialLogTextviewState, logTextviewReducer, LogTextviewState } from './log_textview';
import { initialMetricTimeState, metricTimeReducer, MetricTimeState } from './metric_time';
import { initialWaffleFilterState, waffleFilterReducer, WaffleFilterState } from './waffle_filter';
import {
@ -22,9 +20,7 @@ import { initialWaffleTimeState, waffleTimeReducer, WaffleTimeState } from './wa
export interface LocalState {
logFilter: LogFilterState;
logMinimap: LogMinimapState;
logPosition: LogPositionState;
logTextview: LogTextviewState;
metricTime: MetricTimeState;
waffleFilter: WaffleFilterState;
waffleTime: WaffleTimeState;
@ -34,9 +30,7 @@ export interface LocalState {
export const initialLocalState: LocalState = {
logFilter: initialLogFilterState,
logMinimap: initialLogMinimapState,
logPosition: initialLogPositionState,
logTextview: initialLogTextviewState,
metricTime: initialMetricTimeState,
waffleFilter: initialWaffleFilterState,
waffleTime: initialWaffleTimeState,
@ -46,9 +40,7 @@ export const initialLocalState: LocalState = {
export const localReducer = combineReducers<LocalState>({
logFilter: logFilterReducer,
logMinimap: logMinimapReducer,
logPosition: logPositionReducer,
logTextview: logTextviewReducer,
metricTime: metricTimeReducer,
waffleFilter: waffleFilterReducer,
waffleTime: waffleTimeReducer,

View file

@ -7,9 +7,7 @@
import { globalizeSelectors } from '../../utils/typed_redux';
import { logFilterSelectors as innerLogFilterSelectors } from './log_filter';
import { flyoutOptionsSelectors as innerFlyoutOptionsSelectors } from './log_flyout';
import { logMinimapSelectors as innerLogMinimapSelectors } from './log_minimap';
import { logPositionSelectors as innerLogPositionSelectors } from './log_position';
import { logTextviewSelectors as innerLogTextviewSelectors } from './log_textview';
import { metricTimeSelectors as innerMetricTimeSelectors } from './metric_time';
import { LocalState } from './reducer';
import { waffleFilterSelectors as innerWaffleFilterSelectors } from './waffle_filter';
@ -21,21 +19,11 @@ export const logFilterSelectors = globalizeSelectors(
innerLogFilterSelectors
);
export const logMinimapSelectors = globalizeSelectors(
(state: LocalState) => state.logMinimap,
innerLogMinimapSelectors
);
export const logPositionSelectors = globalizeSelectors(
(state: LocalState) => state.logPosition,
innerLogPositionSelectors
);
export const logTextviewSelectors = globalizeSelectors(
(state: LocalState) => state.logTextview,
innerLogTextviewSelectors
);
export const metricTimeSelectors = globalizeSelectors(
(state: LocalState) => state.metricTime,
innerMetricTimeSelectors

View file

@ -11,9 +11,7 @@ import { globalizeSelectors } from '../utils/typed_redux';
import {
flyoutOptionsSelectors as localFlyoutOptionsSelectors,
logFilterSelectors as localLogFilterSelectors,
logMinimapSelectors as localLogMinimapSelectors,
logPositionSelectors as localLogPositionSelectors,
logTextviewSelectors as localLogTextviewSelectors,
metricTimeSelectors as localMetricTimeSelectors,
waffleFilterSelectors as localWaffleFilterSelectors,
waffleOptionsSelectors as localWaffleOptionsSelectors,
@ -32,9 +30,7 @@ import {
const selectLocal = (state: State) => state.local;
export const logFilterSelectors = globalizeSelectors(selectLocal, localLogFilterSelectors);
export const logMinimapSelectors = globalizeSelectors(selectLocal, localLogMinimapSelectors);
export const logPositionSelectors = globalizeSelectors(selectLocal, localLogPositionSelectors);
export const logTextviewSelectors = globalizeSelectors(selectLocal, localLogTextviewSelectors);
export const metricTimeSelectors = globalizeSelectors(selectLocal, localMetricTimeSelectors);
export const waffleFilterSelectors = globalizeSelectors(selectLocal, localWaffleFilterSelectors);
export const waffleTimeSelectors = globalizeSelectors(selectLocal, localWaffleTimeSelectors);

View file

@ -130,14 +130,17 @@ const AnnotationsTable = injectI18n(class AnnotationsTable extends Component {
}
}
componentWillUpdate() {
previousJobId = undefined;
componentDidUpdate() {
if (
Array.isArray(this.props.jobs) && this.props.jobs.length > 0 &&
this.previousJobId !== this.props.jobs[0].job_id &&
this.props.annotations === undefined &&
this.state.isLoading === false &&
Array.isArray(this.props.jobs) && this.props.jobs.length > 0 &&
this.state.jobId !== this.props.jobs[0].job_id
) {
annotationsRefresh$.next(true);
this.previousJobId = this.props.jobs[0].job_id;
}
}

View file

@ -309,7 +309,7 @@ export class AnomalyDetails extends Component {
{definition.terms}
</EuiText>
</EuiFlexItem>
<EuiSpacer size="m" />
<EuiSpacer size="xs" />
</Fragment> }
{(definition !== undefined && definition.regex) &&
<Fragment>
@ -341,7 +341,7 @@ export class AnomalyDetails extends Component {
{definition.regex}
</EuiText>
</EuiFlexItem>
<EuiSpacer size="l" />
<EuiSpacer size="xs" />
</Fragment>}
{examples.map((example, i) => {

View file

@ -104,38 +104,37 @@ export function JobSelectServiceProvider($rootScope, globalState, i18n) {
function createDescription(jobs) {
let txt = '';
// add up the number of jobs including duplicates if they belong to multiple groups
const count = mlJobService.jobs.reduce((sum, job) => {
return sum + ((job.groups === undefined) ? 1 : job.groups.length);
}, 0);
if (jobs.length === count) {
const jobCount = mlJobService.jobs.length;
// add up how many jobs belong to groups and how many don't
const selectedGroupJobs = [];
const groupCounts = {};
let groupLessJobs = 0;
const splitJobs = jobs.map(job => {
const obj = splitJobId(job);
if (obj.group) {
// keep track of selected jobs from group selection
selectedGroupJobs.push(obj.job);
}
return obj;
});
splitJobs.forEach(jobObj => {
if (jobObj.group) {
groupCounts[jobObj.group] = (groupCounts[jobObj.group] || 0) + 1;
} else {
// if job has already been included via group selection don't add as groupless job
if (selectedGroupJobs.includes(jobObj.job) === false) {
groupLessJobs++;
}
}
});
// All jobs have been selected
if ((_.uniq(selectedGroupJobs).length + groupLessJobs) === jobCount) {
txt = i18n('xpack.ml.jobSelect.allJobsDescription', {
defaultMessage: 'All jobs',
});
} else {
// not all jobs have been selected
// add up how many jobs belong to groups and how many don't
const selectedGroupJobs = [];
const groupCounts = {};
let groupLessJobs = 0;
const splitJobs = jobs.map(job => {
const obj = splitJobId(job);
if (obj.group) {
// keep track of selected jobs from group selection
selectedGroupJobs.push(obj.job);
}
return obj;
});
splitJobs.forEach(jobObj => {
if (jobObj.group) {
groupCounts[jobObj.group] = (groupCounts[jobObj.group] || 0) + 1;
} else {
// if job has already been included via group selection don't add as groupless job
if (selectedGroupJobs.includes(jobObj.job) === false) {
groupLessJobs++;
}
}
});
const wholeGroups = [];
const groups = mlJobService.getJobGroups();
// work out how many groups have all of their jobs selected
@ -159,7 +158,7 @@ export function JobSelectServiceProvider($rootScope, globalState, i18n) {
const total = (wholeGroups.length - 1) + groupLessJobs;
txt = i18n('xpack.ml.jobSelect.wholeGroupDescription', {
defaultMessage: `{wholeGroup} (with {count, plural, zero {# job} one {# job} other {# jobs}}) and
{total, plural, zero {# other} one {# other} other {# others}}`,
{total, plural, zero {# other} one {# other} other {# others}}`,
values: {
count: groupCounts[wholeGroups[0]],
wholeGroup: wholeGroups[0],

View file

@ -379,7 +379,7 @@ export function processViewByResults(
return dataset;
}
export async function loadAnnotationsTableData(selectedCells, selectedJobs, interval, bounds) {
export function loadAnnotationsTableData(selectedCells, selectedJobs, interval, bounds) {
const jobIds = (selectedCells !== null && selectedCells.viewByFieldName === VIEW_BY_JOB_LABEL) ?
selectedCells.lanes : selectedJobs.map(d => d.id);
const timeRange = getSelectionTimeRange(selectedCells, interval, bounds);
@ -388,31 +388,41 @@ export async function loadAnnotationsTableData(selectedCells, selectedJobs, inte
return Promise.resolve([]);
}
const resp = await ml.annotations.getAnnotations({
jobIds,
earliestMs: timeRange.earliestMs,
latestMs: timeRange.latestMs,
maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE
});
return new Promise((resolve) => {
ml.annotations.getAnnotations({
jobIds,
earliestMs: timeRange.earliestMs,
latestMs: timeRange.latestMs,
maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE
}).then((resp) => {
if (resp.error !== undefined || resp.annotations === undefined) {
return resolve([]);
}
const annotationsData = [];
jobIds.forEach((jobId) => {
const jobAnnotations = resp.annotations[jobId];
if (jobAnnotations !== undefined) {
annotationsData.push(...jobAnnotations);
}
});
const annotationsData = [];
jobIds.forEach((jobId) => {
const jobAnnotations = resp.annotations[jobId];
if (jobAnnotations !== undefined) {
annotationsData.push(...jobAnnotations);
}
});
return Promise.resolve(
annotationsData
.sort((a, b) => {
return a.timestamp - b.timestamp;
})
.map((d, i) => {
d.key = String.fromCharCode(65 + i);
return d;
})
);
return resolve(
annotationsData
.sort((a, b) => {
return a.timestamp - b.timestamp;
})
.map((d, i) => {
d.key = String.fromCharCode(65 + i);
return d;
})
);
}).catch((resp) => {
console.log('Error loading list of annotations for jobs list:', resp);
// Silently fail and just return an empty array for annotations to not break the UI.
return resolve([]);
});
});
}
export async function loadAnomaliesTableData(

View file

@ -19,8 +19,6 @@ import _ from 'lodash';
import d3 from 'd3';
import moment from 'moment';
import chrome from 'ui/chrome';
import {
getSeverityWithLow,
getMultiBucketImpactLabel,
@ -58,8 +56,6 @@ import {
import { injectI18n } from '@kbn/i18n/react';
const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false);
const focusZoomPanelHeight = 25;
const focusChartHeight = 310;
const focusHeight = focusZoomPanelHeight + focusChartHeight;
@ -93,6 +89,7 @@ function getSvgHeight() {
const TimeseriesChartIntl = injectI18n(class TimeseriesChart extends React.Component {
static propTypes = {
annotationsEnabled: PropTypes.bool,
annotation: PropTypes.object,
autoZoomDuration: PropTypes.number,
contextAggregationInterval: PropTypes.object,
@ -133,6 +130,7 @@ const TimeseriesChartIntl = injectI18n(class TimeseriesChart extends React.Compo
componentDidMount() {
const {
annotationsEnabled,
svgWidth
} = this.props;
@ -165,7 +163,7 @@ const TimeseriesChartIntl = injectI18n(class TimeseriesChart extends React.Compo
this.fieldFormat = undefined;
// Annotations Brush
if (mlAnnotationsEnabled) {
if (annotationsEnabled) {
this.annotateBrush = getAnnotationBrush.call(this);
}
@ -212,7 +210,7 @@ const TimeseriesChartIntl = injectI18n(class TimeseriesChart extends React.Compo
this.renderFocusChart();
if (mlAnnotationsEnabled && this.props.annotation === null) {
if (this.props.annotationsEnabled && this.props.annotation === null) {
const chartElement = d3.select(this.rootNode);
chartElement.select('g.mlAnnotationBrush').call(this.annotateBrush.extent([0, 0]));
}
@ -220,6 +218,7 @@ const TimeseriesChartIntl = injectI18n(class TimeseriesChart extends React.Compo
renderChart() {
const {
annotationsEnabled,
contextChartData,
contextForecastData,
detectorIndex,
@ -321,7 +320,7 @@ const TimeseriesChartIntl = injectI18n(class TimeseriesChart extends React.Compo
.attr('transform', 'translate(' + margin.left + ',' + (focusHeight + margin.top + chartSpacing) + ')');
// Mask to hide annotations overflow
if (mlAnnotationsEnabled) {
if (annotationsEnabled) {
const annotationsMask = svg
.append('defs')
.append('mask')
@ -397,6 +396,7 @@ const TimeseriesChartIntl = injectI18n(class TimeseriesChart extends React.Compo
// as we want to re-render the paths and points when the zoom area changes.
const {
annotationsEnabled,
contextForecastData
} = this.props;
@ -413,7 +413,7 @@ const TimeseriesChartIntl = injectI18n(class TimeseriesChart extends React.Compo
this.createZoomInfoElements(zoomGroup, fcsWidth);
// Create the elements for annotations
if (mlAnnotationsEnabled) {
if (annotationsEnabled) {
const annotateBrush = this.annotateBrush.bind(this);
fcsGroup.append('g')
@ -495,6 +495,7 @@ const TimeseriesChartIntl = injectI18n(class TimeseriesChart extends React.Compo
renderFocusChart() {
const {
annotationsEnabled,
focusAggregationInterval,
focusAnnotationData,
focusChartData,
@ -596,7 +597,7 @@ const TimeseriesChartIntl = injectI18n(class TimeseriesChart extends React.Compo
// if annotations are present, we extend yMax to avoid overlap
// between annotation labels, chart lines and anomalies.
if (mlAnnotationsEnabled && focusAnnotationData && focusAnnotationData.length > 0) {
if (annotationsEnabled && focusAnnotationData && focusAnnotationData.length > 0) {
const levels = getAnnotationLevels(focusAnnotationData);
const maxLevel = d3.max(Object.keys(levels).map(key => levels[key]));
// TODO needs revisiting to be a more robust normalization
@ -632,7 +633,7 @@ const TimeseriesChartIntl = injectI18n(class TimeseriesChart extends React.Compo
.classed('hidden', !showModelBounds);
}
if (mlAnnotationsEnabled) {
if (annotationsEnabled) {
renderAnnotations(
focusChart,
focusAnnotationData,
@ -645,7 +646,7 @@ const TimeseriesChartIntl = injectI18n(class TimeseriesChart extends React.Compo
// disable brushing (creation of annotations) when annotations aren't shown
focusChart.select('.mlAnnotationBrush')
.style('pointer-events', (showAnnotations) ? 'all' : 'none');
.style('display', (showAnnotations) ? null : 'none');
}
focusChart.select('.values-line')
@ -1236,6 +1237,7 @@ const TimeseriesChartIntl = injectI18n(class TimeseriesChart extends React.Compo
showFocusChartTooltip(marker, circle) {
const {
annotationsEnabled,
modelPlotEnabled,
intl
} = this.props;
@ -1376,7 +1378,7 @@ const TimeseriesChartIntl = injectI18n(class TimeseriesChart extends React.Compo
});
}
if (mlAnnotationsEnabled && _.has(marker, 'annotation')) {
if (annotationsEnabled && _.has(marker, 'annotation')) {
contents = mlEscape(marker.annotation);
contents += `<br />${moment(marker.timestamp).format('MMMM Do YYYY, HH:mm')}`;

View file

@ -26,9 +26,6 @@ const module = uiModules.get('apps/ml');
import { I18nContext } from 'ui/i18n';
import chrome from 'ui/chrome';
const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false);
module.directive('mlTimeseriesChart', function ($timeout) {
function link(scope, element) {
@ -44,6 +41,7 @@ module.directive('mlTimeseriesChart', function ($timeout) {
svgWidth = Math.max(angular.element('.results-container').width(), 0);
const props = {
annotationsEnabled: scope.annotationsEnabled,
autoZoomDuration: scope.autoZoomDuration,
contextAggregationInterval: scope.contextAggregationInterval,
contextChartData: scope.contextChartData,
@ -91,7 +89,8 @@ module.directive('mlTimeseriesChart', function ($timeout) {
scope.$watchCollection('focusForecastData', renderFocusChart);
scope.$watchCollection('focusChartData', renderFocusChart);
scope.$watchGroup(['showModelBounds', 'showForecast'], renderFocusChart);
if (mlAnnotationsEnabled) {
scope.$watch('annotationsEnabled', renderReactComponent);
if (scope.annotationsEnabled) {
scope.$watchCollection('focusAnnotationData', renderFocusChart);
scope.$watch('showAnnotations', renderFocusChart);
}
@ -116,6 +115,7 @@ module.directive('mlTimeseriesChart', function ($timeout) {
return {
scope: {
annotationsEnabled: '=',
selectedJob: '=',
detectorIndex: '=',
modelPlotEnabled: '=',

View file

@ -161,6 +161,7 @@
<div class="ml-timeseries-chart">
<ml-timeseries-chart style="width: 1200px; height: 400px;"
annotations-enabled="showAnnotationsCheckbox"
selected-job="selectedJob"
detector-index="detectorId"
model-plot-enabled="modelPlotEnabled"

View file

@ -61,7 +61,7 @@ import { severity$ } from '../components/controls/select_severity/select_severit
import chrome from 'ui/chrome';
const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false);
let mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false);
uiRoutes
.when('/timeseriesexplorer/?', {
@ -416,6 +416,12 @@ module.controller('MlTimeSeriesExplorerController', function (
console.log('Time series explorer focus chart data set:', $scope.focusChartData);
$scope.loading = false;
// If the annotations failed to load and the feature flag is set to `false`,
// make sure the checkbox toggle gets hidden.
if (mlAnnotationsEnabled === false) {
$scope.showAnnotationsCheckbox = false;
}
});
}
}
@ -490,19 +496,24 @@ module.controller('MlTimeSeriesExplorerController', function (
latestMs: searchBounds.max.valueOf(),
maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE
}).then((resp) => {
refreshFocusData.focusAnnotationData = resp.annotations[$scope.selectedJob.job_id]
.sort((a, b) => {
return a.timestamp - b.timestamp;
})
.map((d, i) => {
d.key = String.fromCharCode(65 + i);
return d;
});
refreshFocusData.focusAnnotationData = [];
if (Array.isArray(resp.annotations[$scope.selectedJob.job_id])) {
refreshFocusData.focusAnnotationData = resp.annotations[$scope.selectedJob.job_id]
.sort((a, b) => {
return a.timestamp - b.timestamp;
})
.map((d, i) => {
d.key = String.fromCharCode(65 + i);
return d;
});
}
finish();
}).catch(() => {
// silent fail
// silently fail and disable annotations feature if loading annotations fails.
refreshFocusData.focusAnnotationData = [];
mlAnnotationsEnabled = false;
finish();
});
} else {

View file

@ -16,6 +16,8 @@ import { annotationServiceProvider } from './index';
const acknowledgedResponseMock = { acknowledged: true };
const jobIdMock = 'jobIdMock';
describe('annotation_service', () => {
let callWithRequestSpy: jest.Mock;
@ -55,8 +57,6 @@ describe('annotation_service', () => {
it('should get annotations for specific job', async done => {
const { getAnnotations } = annotationServiceProvider(callWithRequestSpy);
const jobIdMock = 'jobIdMock';
const indexAnnotationArgsMock: IndexAnnotationArgs = {
jobIds: [jobIdMock],
earliestMs: 1454804100000,
@ -73,13 +73,37 @@ describe('annotation_service', () => {
expect(isAnnotations(response.annotations[jobIdMock])).toBeTruthy();
done();
});
it('should throw and catch an error', async () => {
const mockEsError = {
statusCode: 404,
error: 'Not Found',
message: 'mock error message',
};
const callWithRequestSpyError = jest.fn(() => {
return Promise.resolve(mockEsError);
});
const { getAnnotations } = annotationServiceProvider(callWithRequestSpyError);
const indexAnnotationArgsMock: IndexAnnotationArgs = {
jobIds: [jobIdMock],
earliestMs: 1454804100000,
latestMs: 1455233399999,
maxAnnotations: 500,
};
await expect(getAnnotations(indexAnnotationArgsMock)).rejects.toEqual(
Error(`Annotations couldn't be retrieved from Elasticsearch.`)
);
});
});
describe('indexAnnotation()', () => {
it('should index annotation', async done => {
const { indexAnnotation } = annotationServiceProvider(callWithRequestSpy);
const jobIdMock = 'jobIdMock';
const annotationMock: Annotation = {
annotation: 'Annotation text',
job_id: jobIdMock,
@ -107,7 +131,6 @@ describe('annotation_service', () => {
it('should remove ._id and .key before updating annotation', async done => {
const { indexAnnotation } = annotationServiceProvider(callWithRequestSpy);
const jobIdMock = 'jobIdMock';
const annotationMock: Annotation = {
_id: 'mockId',
annotation: 'Updated annotation text',
@ -139,8 +162,6 @@ describe('annotation_service', () => {
it('should update annotation text and the username for modified_username', async done => {
const { getAnnotations, indexAnnotation } = annotationServiceProvider(callWithRequestSpy);
const jobIdMock = 'jobIdMock';
const indexAnnotationArgsMock: IndexAnnotationArgs = {
jobIds: [jobIdMock],
earliestMs: 1454804100000,

View file

@ -70,6 +70,7 @@ export type callWithRequestType = (
export function annotationProvider(callWithRequest: callWithRequestType) {
async function indexAnnotation(annotation: Annotation, username: string) {
if (isAnnotation(annotation) === false) {
// No need to translate, this will not be exposed in the UI.
return Promise.reject(new Error('invalid annotation format'));
}
@ -211,27 +212,37 @@ export function annotationProvider(callWithRequest: callWithRequestType) {
},
};
const resp = await callWithRequest('search', params);
try {
const resp = await callWithRequest('search', params);
const docs: Annotations = _.get(resp, ['hits', 'hits'], []).map((d: EsResult) => {
// get the original source document and the document id, we need it
// to identify the annotation when editing/deleting it.
return { ...d._source, _id: d._id } as Annotation;
});
if (isAnnotations(docs) === false) {
throw Boom.badRequest(`Annotations didn't pass integrity check.`);
}
docs.forEach((doc: Annotation) => {
const jobId = doc.job_id;
if (typeof obj.annotations[jobId] === 'undefined') {
obj.annotations[jobId] = [];
if (resp.error !== undefined && resp.message !== undefined) {
// No need to translate, this will not be exposed in the UI.
throw new Error(`Annotations couldn't be retrieved from Elasticsearch.`);
}
obj.annotations[jobId].push(doc);
});
return obj;
const docs: Annotations = _.get(resp, ['hits', 'hits'], []).map((d: EsResult) => {
// get the original source document and the document id, we need it
// to identify the annotation when editing/deleting it.
return { ...d._source, _id: d._id } as Annotation;
});
if (isAnnotations(docs) === false) {
// No need to translate, this will not be exposed in the UI.
throw new Error(`Annotations didn't pass integrity check.`);
}
docs.forEach((doc: Annotation) => {
const jobId = doc.job_id;
if (typeof obj.annotations[jobId] === 'undefined') {
obj.annotations[jobId] = [];
}
obj.annotations[jobId].push(doc);
});
return obj;
} catch (error) {
throw Boom.badRequest(error);
}
}
async function deleteAnnotation(id: string) {

View file

@ -36,7 +36,6 @@ export async function getDefaultAdminEmail(config, callCluster, log) {
const version = config.get('pkg.version');
const uiSettingsDoc = await callCluster('get', {
index,
type: 'doc',
id: `config:${version}`,
ignore: [400, 404] // 400 if the index is closed, 404 if it does not exist
});

View file

@ -935,7 +935,6 @@
"kbn.advancedSettings.suggestFilterValuesTitle": "筛选编辑器建议值",
"kbn.advancedSettings.timepicker.monthToDate": "本月迄今为止",
"kbn.advancedSettings.timepicker.quickRanges.acceptedFormatsLinkText": "接受的格式",
"kbn.advancedSettings.timepicker.quickRangesText": "要在时间选取器的“速选”部分中显示的范围列表。这应该是对象数组每个对象包含“from”、“to”请参阅{acceptedFormatsLink}、“display”要显示的标题以及“section”要放置选项的列。",
"kbn.advancedSettings.timepicker.quickRangesTitle": "时间选取器的速选范围",
"kbn.advancedSettings.timepicker.refreshIntervalDefaultsText": "时间筛选的默认刷新时间间隔",
"kbn.advancedSettings.timepicker.refreshIntervalDefaultsTitle": "时间选取器刷新时间间隔",
@ -8216,4 +8215,4 @@
"xpack.watcher.watchActionsTitle": "满足后将执行 {watchActionsCount, plural, one{# 个操作} other {# 个操作}}",
"xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。"
}
}
}

View file

@ -13,6 +13,7 @@ export default function ({ getService, getPageObjects }) {
const pipelineList = getService('pipelineList');
const pipelineEditor = getService('pipelineEditor');
const PageObjects = getPageObjects(['logstash']);
const retry = getService('retry');
describe('pipeline create new', () => {
let originalWindowSize;
@ -67,11 +68,13 @@ export default function ({ getService, getPageObjects }) {
await pipelineList.assertExists();
await pipelineList.setFilter(id);
const rows = await pipelineList.readRows();
const newRow = rows.find(row => row.id === id);
await retry.try(async () => {
const rows = await pipelineList.readRows();
const newRow = rows.find(row => row.id === id);
expect(newRow)
.to.have.property('description', description);
expect(newRow)
.to.have.property('description', description);
});
});
});
@ -84,9 +87,11 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.logstash.gotoNewPipelineEditor();
await pipelineEditor.clickCancel();
await pipelineList.assertExists();
const currentRows = await pipelineList.readRows();
expect(originalRows).to.eql(currentRows);
await retry.try(async () => {
await pipelineList.assertExists();
const currentRows = await pipelineList.readRows();
expect(originalRows).to.eql(currentRows);
});
});
});

View file

@ -14,6 +14,7 @@
import { I18nProvider, InjectedIntl, intlShape } from '@kbn/i18n/react';
import { mount, ReactWrapper, render, shallow } from 'enzyme';
import React, { ReactElement, ValidationMap } from 'react';
import { act as reactAct } from 'react-dom/test-utils';
// Use fake component to extract `intl` property to use in tests.
const { intl } = (mount(
@ -113,3 +114,69 @@ export function renderWithIntl<T>(
return render(nodeWithIntlProp(node), options);
}
/**
* A wrapper object to provide access to the state of a hook under test and to
* enable interaction with that hook.
*/
interface ReactHookWrapper<HookValue> {
/* Ensures that async React operations have settled before and after the
* given actor callback is called. */
act: (actor: (lastHookValue: HookValue) => void) => void;
/* The enzyme wrapper around the test component. */
component: ReactWrapper;
/* The most recent value return the by test harness of the hook. */
getLastHookValue: () => HookValue;
/* The jest Mock function that receives the hook values for introspection. */
hookValueCallback: jest.Mock;
}
/**
* Allows for execution of hooks inside of a test component which records the
* returned values.
*
* @param body A function that calls the hook and returns data derived from it
* @param WrapperComponent A component that, if provided, will be wrapped
* around the test component. This can be useful to provide context values.
* @return {ReactHookWrapper} An object providing access to the hook state and
* functions to interact with it.
*/
export const mountHook = <HookValue extends any>(
body: () => HookValue,
WrapperComponent?: React.ComponentType
): ReactHookWrapper<HookValue> => {
const hookValueCallback = jest.fn();
const act = (actor: (lastHookValue: HookValue) => void) => {
reactAct(() => actor(getLastHookValue()));
component.update();
};
const getLastHookValue = () => {
const calls = hookValueCallback.mock.calls;
if (calls.length <= 0) {
throw Error('No recent hook value present.');
}
return calls[calls.length - 1][0];
};
const TestComponent = () => {
hookValueCallback(body());
return null;
};
const component = WrapperComponent
? mount(
<WrapperComponent>
<TestComponent />
</WrapperComponent>
)
: mount(<TestComponent />);
return {
act,
component,
getLastHookValue,
hookValueCallback,
};
};

View file

@ -7123,6 +7123,11 @@ constants-browserify@^1.0.0:
resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=
"constate-latest@npm:constate@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/constate/-/constate-1.0.0.tgz#93fa87108e364a05e93b3597e22d53adaf86776d"
integrity sha512-b1Pip712fAQ1el4pndiYeVMXAzlbrD8+Z8ik79TOQc3ZJNjxKGawT0gCPpANsNbT4eHszDZ/8472hvL1rowUhQ==
constate@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/constate/-/constate-0.9.0.tgz#877197ef8fbcacee95672a7e98f7b21dec818891"