mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Merge remote-tracking branch 'origin/master' into feature/merge-code
This commit is contained in:
commit
58585eef1b
81 changed files with 1343 additions and 991 deletions
|
@ -25,6 +25,8 @@ If you are running our https://cloud.elastic.co[hosted Elasticsearch Service]
|
|||
on Elastic Cloud, you can access Kibana with a single click.
|
||||
--
|
||||
|
||||
include::getting-started/add-sample-data.asciidoc[]
|
||||
|
||||
include::getting-started/tutorial-sample-data.asciidoc[]
|
||||
|
||||
include::getting-started/tutorial-sample-filter.asciidoc[]
|
||||
|
|
31
docs/getting-started/add-sample-data.asciidoc
Normal file
31
docs/getting-started/add-sample-data.asciidoc
Normal file
|
@ -0,0 +1,31 @@
|
|||
[[add-sample-data]]
|
||||
== Get up and running with sample data
|
||||
|
||||
{kib} has three sample data sets that you can use to explore {kib} before loading your own data
|
||||
source. Each set is prepackaged with a dashboard of visualizations and a
|
||||
{kibana-ref}/canvas-getting-started.html[Canvas workpad].
|
||||
|
||||
The sample data sets address common use cases:
|
||||
|
||||
* *eCommerce orders* includes visualizations for product-related information,
|
||||
such as cost, revenue, and price.
|
||||
* *Web logs* lets you analyze website traffic.
|
||||
* *Flight data* enables you to view and interact with flight routes for four airlines.
|
||||
|
||||
To get started, go to the home page and click the link next to *Add sample data*.
|
||||
|
||||
Once you have loaded a data set, click *View data* to view visualizations in *Dashboard*.
|
||||
|
||||
*Note:* The timestamps in the sample data sets are relative to when they are installed.
|
||||
If you uninstall and reinstall a data set, the timestamps will change to reflect the most recent installation.
|
||||
|
||||
|
||||
[role="screenshot"]
|
||||
image::images/add-sample-data.png[]
|
||||
|
||||
[float]
|
||||
==== Next steps
|
||||
|
||||
Play with the sample flight data in the {kibana-ref}/tutorial-sample-data.html[flight dashboard tutorial].
|
||||
|
||||
Learn how to load data, define index patterns and build visualizations by {kibana-ref}/tutorial-build-dashboard.html[building your own dashboard].
|
BIN
docs/images/add-sample-data.png
Normal file
BIN
docs/images/add-sample-data.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 660 KiB |
5
kibana.d.ts
vendored
5
kibana.d.ts
vendored
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
})();
|
||||
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
@ -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',
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
32
src/legacy/plugin_discovery/plugin_spec/plugin_spec_options.d.ts
vendored
Normal file
32
src/legacy/plugin_discovery/plugin_spec/plugin_spec_options.d.ts
vendored
Normal 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;
|
||||
}
|
2
src/legacy/server/kbn_server.d.ts
vendored
2
src/legacy/server/kbn_server.d.ts
vendored
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -47,7 +47,8 @@ function getFieldsForTypes(searchFields, types) {
|
|||
|
||||
if (!searchFields || !searchFields.length) {
|
||||
return {
|
||||
all_fields: true
|
||||
lenient: true,
|
||||
fields: ['*'],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
74
test/functional/apps/home/_navigation.js
Normal file
74
test/functional/apps/home/_navigation.js
Normal 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);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
|
@ -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'));
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
76
test/functional/apps/visualize/_tsvb_markdown.ts
Normal file
76
test/functional/apps/visualize/_tsvb_markdown.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
`;
|
||||
|
|
|
@ -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 }));
|
||||
}
|
||||
|
||||
|
|
|
@ -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: []
|
||||
}
|
||||
];
|
||||
|
|
|
@ -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%')};
|
||||
`;
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -30,7 +30,8 @@ describe('timeseriesFetcher', () => {
|
|||
end: 1528977600000,
|
||||
client: clientSpy,
|
||||
config: {
|
||||
get: () => 'myIndex' as any
|
||||
get: () => 'myIndex' as any,
|
||||
has: () => true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -24,7 +24,8 @@ describe('transactionGroupsFetcher', () => {
|
|||
case 'xpack.apm.ui.transactionGroupBucketSize':
|
||||
return 100;
|
||||
}
|
||||
})
|
||||
}),
|
||||
has: () => true
|
||||
}
|
||||
};
|
||||
const bodyQuery = { my: 'bodyQuery' };
|
||||
|
|
|
@ -26,7 +26,8 @@ describe('getAnomalySeries', () => {
|
|||
end: 500000,
|
||||
client: clientSpy,
|
||||
config: {
|
||||
get: () => 'myIndex' as any
|
||||
get: () => 'myIndex' as any,
|
||||
has: () => true
|
||||
}
|
||||
}
|
||||
})) as AnomalyTimeSeriesResponse;
|
||||
|
|
|
@ -22,7 +22,8 @@ describe('timeseriesFetcher', () => {
|
|||
end: 1528977600000,
|
||||
client: clientSpy,
|
||||
config: {
|
||||
get: () => 'myIndex' as any
|
||||
get: () => 'myIndex' as any,
|
||||
has: () => true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
},
|
||||
];
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -6,9 +6,7 @@
|
|||
|
||||
export {
|
||||
logFilterActions,
|
||||
logMinimapActions,
|
||||
logPositionActions,
|
||||
logTextviewActions,
|
||||
metricTimeActions,
|
||||
waffleFilterActions,
|
||||
waffleTimeActions,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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');
|
|
@ -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';
|
|
@ -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();
|
|
@ -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;
|
|
@ -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');
|
|
@ -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';
|
|
@ -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,
|
||||
});
|
|
@ -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;
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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')}`;
|
||||
|
||||
|
|
|
@ -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: '=',
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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": "通过创建、管理和监测警报来检测数据中的更改。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue