mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
parent
3d6285548c
commit
134ece5c12
58 changed files with 1819 additions and 47 deletions
|
@ -78,7 +78,6 @@ Markdown.
|
|||
`notifications:lifetime:error`:: Specifies the duration in milliseconds for error notification displays. The default value is 300000. Set this field to `Infinity` to disable error notifications.
|
||||
`notifications:lifetime:warning`:: Specifies the duration in milliseconds for warning notification displays. The default value is 10000. Set this field to `Infinity` to disable warning notifications.
|
||||
`notifications:lifetime:info`:: Specifies the duration in milliseconds for information notification displays. The default value is 5000. Set this field to `Infinity` to disable information notifications.
|
||||
|
||||
`timelion:showTutorial`:: Set this property to `true` to show the Timelion tutorial to users when they first open Timelion.
|
||||
`timelion:es.timefield`:: Default field containing a timestamp when using the `.es()` query.
|
||||
`timelion:es.default_index`:: Default index when using the `.es()` query.
|
||||
|
@ -89,3 +88,5 @@ Markdown.
|
|||
`timelion:graphite.url`:: [experimental] Used with graphite queries, this it the URL of your host
|
||||
`timelion:quandl.key`:: [experimental] Used with quandl queries, this is your API key from www.quandl.com
|
||||
`state:storeInSessionStorage`:: [experimental] Kibana tracks UI state in the URL, which can lead to problems when there is a lot of information there and the URL gets very long. Enabling this will store parts of the state in your browser session instead, to keep the URL shorter.
|
||||
`context:defaultSize`:: Specifies the initial number of surrounding entries to display in the context view. The default value is 5.
|
||||
`context:step`:: Specifies the number to increment or decrement the context size by when using the buttons in the context view. The default value is 5.
|
||||
|
|
97
src/core_plugins/kibana/public/context/NOTES.md
Normal file
97
src/core_plugins/kibana/public/context/NOTES.md
Normal file
|
@ -0,0 +1,97 @@
|
|||
# Discover Context App Implementation Notes
|
||||
|
||||
The implementation of this app is intended to exhibit certain desirable
|
||||
properties by adhering to a set of *principles*. This document aims to explain
|
||||
those and the *concepts* employed to achieve that.
|
||||
|
||||
|
||||
## Principles
|
||||
|
||||
**Single Source of Truth**: A good user experience depends on the UI displaying
|
||||
consistent information across the whole page. To achieve this, there should
|
||||
always be a single source of truth for the application's state. In this
|
||||
application this is the `ContextAppController::state` object.
|
||||
|
||||
**Unidirectional Data Flow**: While a single state promotes rendering
|
||||
consistency, it does little to make the state changes easier to reason about.
|
||||
To avoid having state mutations scattered all over the code, this app
|
||||
implements a unidirectional data flow architecture. That means that the state
|
||||
is treated as immutable throughout the application except for actions, which
|
||||
may modify it to cause angular to re-render and watches to trigger.
|
||||
|
||||
**Unit-Testability**: Creating unit tests for large parts of the UI code is
|
||||
made easy by expressing the as much of the logic as possible as
|
||||
side-effect-free functions. The only place where side-effects are allowed are
|
||||
actions. Due to the nature of AngularJS a certain amount of impure code must be
|
||||
employed in some cases, e.g. when dealing with the isolate scope bindings in
|
||||
`ContextAppController`.
|
||||
|
||||
**Loose Coupling**: An attempt was made to couple the parts that make up this
|
||||
app as loosely as possible. This means using pure functions whenever possible
|
||||
and isolating the angular directives diligently. To that end, the app has been
|
||||
implemented as the independent `ContextApp` directive in [app.js](./app.js). It
|
||||
does not access the Kibana `AppState` directly but communicates only via its
|
||||
directive properties. The binding of these attributes to the state and thereby
|
||||
to the route is performed by the `CreateAppRouteController`in
|
||||
[index.js](./index.js). Similarly, the `SizePicker` directive only communicates
|
||||
with its parent via the passed properties.
|
||||
|
||||
|
||||
## Concepts
|
||||
|
||||
To adhere to the principles mentioned above, this app borrows some concepts
|
||||
from the redux architecture that forms a ciruclar unidirectional data flow:
|
||||
|
||||
```
|
||||
|
||||
|* create initial state
|
||||
v
|
||||
+->+
|
||||
| v
|
||||
| |* state
|
||||
| v
|
||||
| |* angular templates render state
|
||||
| v
|
||||
| |* angular calls actions in response to user action/system events
|
||||
| v
|
||||
| |* actions modify state
|
||||
| v
|
||||
+--+
|
||||
|
||||
```
|
||||
|
||||
**State**: The state is the single source of truth at
|
||||
`ContextAppController::state` and may only be modified by actions.
|
||||
|
||||
**Action**: Actions are functions that are called inreponse user or system
|
||||
actions and may modified the state the are bound to via their closure.
|
||||
|
||||
|
||||
## Directory Structure
|
||||
|
||||
**index.js**: Defines the route and renders the `<context-app>` directive,
|
||||
binding it to the `AppState`.
|
||||
|
||||
**app.js**: Defines the `<context-app>` directive, that is at the root of the
|
||||
application. Creates the store, reducer and bound actions/selectors.
|
||||
|
||||
**query**: Exports the actions, reducers and selectors related to the
|
||||
query status and results.
|
||||
|
||||
**query_parameters**: Exports the actions, reducers and selectors related to
|
||||
the parameters used to construct the query.
|
||||
|
||||
**components/loading_button**: Defines the `<context-loading-button>`
|
||||
directive including its respective styles.
|
||||
|
||||
**components/size_picker**: Defines the `<context-size-picker>`
|
||||
directive including its respective styles.
|
||||
|
||||
**api/anchor.js**: Exports `fetchAnchor()` that creates and executes the
|
||||
query for the anchor document.
|
||||
|
||||
**api/context.js**: Exports `fetchPredecessors()` and `fetchSuccessors()` that
|
||||
create and execute the queries for the preceeding and succeeding documents.
|
||||
|
||||
**api/utils**: Exports various functions used to create and transform
|
||||
queries.
|
|
@ -0,0 +1,80 @@
|
|||
import expect from 'expect.js';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import { fetchAnchor } from 'plugins/kibana/context/api/anchor';
|
||||
|
||||
|
||||
describe('context app', function () {
|
||||
describe('function fetchAnchor', function () {
|
||||
it('should use the `search` api to query the given index', function () {
|
||||
const indexPatternStub = createIndexPatternStub('index1');
|
||||
const esStub = createEsStub(['hit1']);
|
||||
|
||||
return fetchAnchor(esStub, indexPatternStub, 'UID', { '@timestamp': 'desc' })
|
||||
.then(() => {
|
||||
expect(esStub.search.calledOnce).to.be(true);
|
||||
expect(esStub.search.firstCall.args[0]).to.have.property('index', 'index1');
|
||||
});
|
||||
});
|
||||
|
||||
it('should include computed fields in the query', function () {
|
||||
const indexPatternStub = createIndexPatternStub('index1');
|
||||
const esStub = createEsStub(['hit1']);
|
||||
|
||||
return fetchAnchor(esStub, indexPatternStub, 'UID', { '@timestamp': 'desc' })
|
||||
.then(() => {
|
||||
expect(esStub.search.calledOnce).to.be(true);
|
||||
expect(esStub.search.firstCall.args[0].body).to.have.keys([
|
||||
'script_fields', 'docvalue_fields', 'stored_fields']);
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject with an error when no hits were found', function () {
|
||||
const indexPatternStub = createIndexPatternStub('index1');
|
||||
const esStub = createEsStub([]);
|
||||
|
||||
return fetchAnchor(esStub, indexPatternStub, 'UID', { '@timestamp': 'desc' })
|
||||
.then(
|
||||
() => {
|
||||
expect().fail('expected the promise to be rejected');
|
||||
},
|
||||
(error) => {
|
||||
expect(error).to.be.an(Error);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the first hit after adding an anchor marker', function () {
|
||||
const indexPatternStub = createIndexPatternStub('index1');
|
||||
const esStub = createEsStub([{ property1: 'value1' }, {}]);
|
||||
|
||||
return fetchAnchor(esStub, indexPatternStub, 'UID', { '@timestamp': 'desc' })
|
||||
.then((anchorDocument) => {
|
||||
expect(anchorDocument).to.have.property('property1', 'value1');
|
||||
expect(anchorDocument).to.have.property('$$_isAnchor', true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function createIndexPatternStub(indices) {
|
||||
return {
|
||||
getComputedFields: sinon.stub()
|
||||
.returns({}),
|
||||
toIndexList: sinon.stub()
|
||||
.returns(indices),
|
||||
};
|
||||
}
|
||||
|
||||
function createEsStub(hits) {
|
||||
return {
|
||||
search: sinon.stub()
|
||||
.returns({
|
||||
hits: {
|
||||
hits,
|
||||
total: hits.length,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
31
src/core_plugins/kibana/public/context/api/anchor.js
Normal file
31
src/core_plugins/kibana/public/context/api/anchor.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
import { addComputedFields } from './utils/fields';
|
||||
import { createAnchorQueryBody } from './utils/queries';
|
||||
|
||||
|
||||
async function fetchAnchor(es, indexPattern, uid, sort) {
|
||||
const indices = await indexPattern.toIndexList();
|
||||
const queryBody = addComputedFields(indexPattern, createAnchorQueryBody(uid, sort));
|
||||
const response = await es.search({
|
||||
index: indices,
|
||||
body: queryBody,
|
||||
});
|
||||
|
||||
if (_.get(response, ['hits', 'total'], 0) < 1) {
|
||||
throw new Error('Failed to load anchor document.');
|
||||
}
|
||||
|
||||
return Object.assign(
|
||||
{},
|
||||
response.hits.hits[0],
|
||||
{
|
||||
$$_isAnchor: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
fetchAnchor,
|
||||
};
|
47
src/core_plugins/kibana/public/context/api/context.js
Normal file
47
src/core_plugins/kibana/public/context/api/context.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
import { addComputedFields } from './utils/fields';
|
||||
import { getDocumentUid } from './utils/ids';
|
||||
import { createSuccessorsQueryBody } from './utils/queries.js';
|
||||
import { reverseQuerySort } from './utils/sorting';
|
||||
|
||||
|
||||
async function fetchSuccessors(es, indexPattern, anchorDocument, sort, size) {
|
||||
const successorsQueryBody = prepareQueryBody(indexPattern, anchorDocument, sort, size);
|
||||
const results = await performQuery(es, indexPattern, successorsQueryBody);
|
||||
return results;
|
||||
}
|
||||
|
||||
async function fetchPredecessors(es, indexPattern, anchorDocument, sort, size) {
|
||||
const successorsQueryBody = prepareQueryBody(indexPattern, anchorDocument, sort, size);
|
||||
const predecessorsQueryBody = reverseQuerySort(successorsQueryBody);
|
||||
const reversedResults = await performQuery(es, indexPattern, predecessorsQueryBody);
|
||||
const results = reversedResults.slice().reverse();
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
function prepareQueryBody(indexPattern, anchorDocument, sort, size) {
|
||||
const successorsQueryBody = addComputedFields(
|
||||
indexPattern,
|
||||
createSuccessorsQueryBody(anchorDocument.sort, sort, size)
|
||||
);
|
||||
return successorsQueryBody;
|
||||
}
|
||||
|
||||
async function performQuery(es, indexPattern, queryBody) {
|
||||
const indices = await indexPattern.toIndexList();
|
||||
|
||||
const response = await es.search({
|
||||
index: indices,
|
||||
body: queryBody,
|
||||
});
|
||||
|
||||
return _.get(response, ['hits', 'hits'], []);
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
fetchPredecessors,
|
||||
fetchSuccessors,
|
||||
};
|
|
@ -0,0 +1,49 @@
|
|||
import expect from 'expect.js';
|
||||
|
||||
import { addComputedFields } from 'plugins/kibana/context/api/utils/fields';
|
||||
|
||||
|
||||
describe('context app', function () {
|
||||
describe('function addComputedFields', function () {
|
||||
it('should add the `script_fields` property defined in the given index pattern', function () {
|
||||
const getComputedFields = () => ({
|
||||
scriptFields: {
|
||||
sourcefield1: {
|
||||
script: '_source.field1',
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
const query = addComputedFields({ getComputedFields }, {});
|
||||
expect(query).to.have.property('script_fields');
|
||||
expect(query.script_fields).to.eql(getComputedFields().scriptFields);
|
||||
});
|
||||
|
||||
it('should add the `docvalue_fields` property defined in the given index pattern', function () {
|
||||
const getComputedFields = () => ({
|
||||
docvalueFields: ['field1'],
|
||||
});
|
||||
|
||||
const query = addComputedFields({ getComputedFields }, {});
|
||||
expect(query).to.have.property('docvalue_fields');
|
||||
expect(query.docvalue_fields).to.eql(getComputedFields().docvalueFields);
|
||||
});
|
||||
|
||||
it('should add the `stored_fields` property defined in the given index pattern', function () {
|
||||
const getComputedFields = () => ({
|
||||
storedFields: ['field1'],
|
||||
});
|
||||
|
||||
const query = addComputedFields({ getComputedFields }, {});
|
||||
expect(query).to.have.property('stored_fields');
|
||||
expect(query.stored_fields).to.eql(getComputedFields().storedFields);
|
||||
});
|
||||
|
||||
it('should preserve other properties of the query', function () {
|
||||
const getComputedFields = () => ({});
|
||||
|
||||
const query = addComputedFields({ getComputedFields }, { property1: 'value1' });
|
||||
expect(query).to.have.property('property1', 'value1');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
import expect from 'expect.js';
|
||||
|
||||
import {
|
||||
createAnchorQueryBody,
|
||||
createSuccessorsQueryBody,
|
||||
} from 'plugins/kibana/context/api/utils/queries';
|
||||
|
||||
|
||||
describe('context app', function () {
|
||||
describe('function createAnchorQueryBody', function () {
|
||||
it('should return a search definition that searches the given uid', function () {
|
||||
const query = createAnchorQueryBody('UID', { '@timestamp': 'desc' });
|
||||
expect(query.query.terms._uid[0]).to.eql('UID');
|
||||
});
|
||||
|
||||
it('should return a search definition that sorts by the given criteria and uid', function () {
|
||||
const query = createAnchorQueryBody('UID', { '@timestamp': 'desc' });
|
||||
expect(query.sort).to.eql([
|
||||
{ '@timestamp': 'desc' },
|
||||
{ _uid: 'asc' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('function createSuccessorsQueryBody', function () {
|
||||
it('should return a search definition that includes the given size', function () {
|
||||
const query = createSuccessorsQueryBody([0, 'UID'], { '@timestamp' : 'desc' }, 10);
|
||||
expect(query).to.have.property('size', 10);
|
||||
});
|
||||
|
||||
it('should return a search definition that sorts by the given criteria and uid', function () {
|
||||
const query = createSuccessorsQueryBody([0, 'UID'], { '@timestamp' : 'desc' }, 10);
|
||||
expect(query).to.have.property('sort');
|
||||
expect(query.sort).to.eql([
|
||||
{ '@timestamp': 'desc' },
|
||||
{ _uid: 'asc' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return a search definition that searches after the given uid', function () {
|
||||
const query = createSuccessorsQueryBody([0, 'UID'], { '@timestamp' : 'desc' }, 10);
|
||||
expect(query).to.have.property('search_after');
|
||||
expect(query.search_after).to.eql([0, 'UID']);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
import expect from 'expect.js';
|
||||
|
||||
import {
|
||||
reverseQuerySort,
|
||||
reverseSortDirection,
|
||||
reverseSortDirective
|
||||
} from 'plugins/kibana/context/api/utils/sorting';
|
||||
|
||||
|
||||
describe('context app', function () {
|
||||
describe('function reverseQuerySort', function () {
|
||||
it('should reverse all the `sort` property values', function () {
|
||||
expect(reverseQuerySort({
|
||||
sort: [
|
||||
{ field1: { order: 'desc', mode: 'max' } },
|
||||
{ field2: 'asc' },
|
||||
'field3',
|
||||
'_score',
|
||||
],
|
||||
})).to.eql({
|
||||
sort: [
|
||||
{ field1: { order: 'asc', mode: 'max' } },
|
||||
{ field2: 'desc' },
|
||||
{ field3: 'desc' },
|
||||
{ _score: 'asc' },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('function reverseSortDirection', function () {
|
||||
it('should reverse a direction given as a string', function () {
|
||||
expect(reverseSortDirection('asc')).to.eql('desc');
|
||||
expect(reverseSortDirection('desc')).to.eql('asc');
|
||||
});
|
||||
|
||||
it('should reverse a direction given in an option object', function () {
|
||||
expect(reverseSortDirection({ order: 'asc' })).to.eql({ order: 'desc' });
|
||||
expect(reverseSortDirection({ order: 'desc' })).to.eql({ order: 'asc' });
|
||||
});
|
||||
|
||||
it('should preserve other properties than `order` in an option object', function () {
|
||||
expect(reverseSortDirection({
|
||||
order: 'asc',
|
||||
other: 'field',
|
||||
})).to.have.property('other', 'field');
|
||||
});
|
||||
});
|
||||
|
||||
describe('function reverseSortDirective', function () {
|
||||
it('should return direction `asc` when given just `_score`', function () {
|
||||
expect(reverseSortDirective('_score')).to.eql({ _score: 'asc' });
|
||||
});
|
||||
|
||||
it('should return direction `desc` when given just a field name', function () {
|
||||
expect(reverseSortDirective('field1')).to.eql({ field1: 'desc' });
|
||||
});
|
||||
|
||||
it('should reverse direction when given an object', function () {
|
||||
expect(reverseSortDirective({ field1: 'asc' })).to.eql({ field1: 'desc' });
|
||||
});
|
||||
});
|
||||
});
|
21
src/core_plugins/kibana/public/context/api/utils/fields.js
Normal file
21
src/core_plugins/kibana/public/context/api/utils/fields.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
|
||||
const addComputedFields = _.curry(function addComputedFields(indexPattern, queryBody) {
|
||||
const computedFields = indexPattern.getComputedFields();
|
||||
|
||||
return Object.assign(
|
||||
{},
|
||||
queryBody,
|
||||
{
|
||||
script_fields: computedFields.scriptFields,
|
||||
docvalue_fields: computedFields.docvalueFields,
|
||||
stored_fields: computedFields.storedFields,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
export {
|
||||
addComputedFields,
|
||||
};
|
8
src/core_plugins/kibana/public/context/api/utils/ids.js
Normal file
8
src/core_plugins/kibana/public/context/api/utils/ids.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
function getDocumentUid(type, id) {
|
||||
return `${type}#${id}`;
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
getDocumentUid,
|
||||
};
|
29
src/core_plugins/kibana/public/context/api/utils/queries.js
Normal file
29
src/core_plugins/kibana/public/context/api/utils/queries.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
function createAnchorQueryBody(uid, contextSort) {
|
||||
return {
|
||||
_source: true,
|
||||
query: {
|
||||
terms: {
|
||||
_uid: [uid],
|
||||
},
|
||||
},
|
||||
sort: [ contextSort, { _uid: 'asc' } ],
|
||||
};
|
||||
}
|
||||
|
||||
function createSuccessorsQueryBody(anchorSortValues, contextSort, size) {
|
||||
return {
|
||||
_source: true,
|
||||
query: {
|
||||
match_all: {},
|
||||
},
|
||||
size,
|
||||
sort: [ contextSort, { _uid: 'asc' } ],
|
||||
search_after: anchorSortValues,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
createAnchorQueryBody,
|
||||
createSuccessorsQueryBody,
|
||||
};
|
96
src/core_plugins/kibana/public/context/api/utils/sorting.js
Normal file
96
src/core_plugins/kibana/public/context/api/utils/sorting.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
/**
|
||||
* A sort directive in object or string form.
|
||||
*
|
||||
* @typedef {(SortDirectiveString|SortDirectiveObject)} SortDirective
|
||||
*/
|
||||
|
||||
/**
|
||||
* A sort directive in object form.
|
||||
*
|
||||
* @typedef {Object.<FieldName, (SortDirection|SortOptions)>} SortDirectiveObject
|
||||
*/
|
||||
|
||||
/**
|
||||
* A sort order string.
|
||||
*
|
||||
* @typedef {('asc'|'desc')} SortDirection
|
||||
*/
|
||||
|
||||
/**
|
||||
* A field name.
|
||||
*
|
||||
* @typedef {string} FieldName
|
||||
*/
|
||||
|
||||
/**
|
||||
* A sort options object
|
||||
*
|
||||
* @typedef {Object} SortOptions
|
||||
* @property {SortDirection} order
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Return a copy of a query with the sort direction reversed.
|
||||
*
|
||||
* @param {Object} query - The query to reverse the sort direction of.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
function reverseQuerySort(query) {
|
||||
return Object.assign(
|
||||
{},
|
||||
query,
|
||||
{
|
||||
sort: _.get(query, 'sort', []).map(reverseSortDirective),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a copy of the directive with the sort direction reversed. If the
|
||||
* field name is '_score', it inverts the default sort direction in the same
|
||||
* way as Elasticsearch itself.
|
||||
*
|
||||
* @param {SortDirective} sortDirective - The directive to reverse the
|
||||
* sort direction of
|
||||
*
|
||||
* @returns {SortDirective}
|
||||
*/
|
||||
function reverseSortDirective(sortDirective) {
|
||||
if (_.isString(sortDirective)) {
|
||||
return {
|
||||
[sortDirective]: (sortDirective === '_score' ? 'asc' : 'desc'),
|
||||
};
|
||||
} else if (_.isPlainObject(sortDirective)) {
|
||||
return _.mapValues(sortDirective, reverseSortDirection);
|
||||
} else {
|
||||
return sortDirective;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the reversed sort direction.
|
||||
*
|
||||
* @param {(SortDirection|SortOptions)} sortDirection
|
||||
*
|
||||
* @returns {(SortDirection|SortOptions)}
|
||||
*/
|
||||
function reverseSortDirection(sortDirection) {
|
||||
if (_.isPlainObject(sortDirection)) {
|
||||
return _.assign({}, sortDirection, {
|
||||
order: reverseSortDirection(sortDirection.order),
|
||||
});
|
||||
} else {
|
||||
return (sortDirection === 'asc' ? 'desc' : 'asc');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
reverseQuerySort,
|
||||
reverseSortDirection,
|
||||
reverseSortDirective,
|
||||
};
|
142
src/core_plugins/kibana/public/context/app.html
Normal file
142
src/core_plugins/kibana/public/context/app.html
Normal file
|
@ -0,0 +1,142 @@
|
|||
<div class="kuiLocalNav">
|
||||
<div class="kuiLocalNavRow">
|
||||
<div class="kuiLocalNavRow__section">
|
||||
<div class="kuiLocalTitle">
|
||||
Surrounding Documents in {{ contextApp.state.queryParameters.indexPattern.id }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kuiLocalNavRow kuiLocalNavRow--secondary">
|
||||
<div class="kuiLocalTabs">
|
||||
<div class="kuiLocalTab kuiLocalTab-isSelected" ng-bind="contextApp.state.queryParameters.anchorUid"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error feedback -->
|
||||
<div
|
||||
class="kuiViewContent kuiViewContentItem"
|
||||
ng-if="contextApp.state.loadingStatus.anchor === contextApp.constants.LOADING_STATUS.FAILED"
|
||||
>
|
||||
<div class="kuiInfoPanel kuiInfoPanel--error kuiVerticalRhythm">
|
||||
<div class="kuiInfoPanelHeader">
|
||||
<span class="kuiInfoPanelHeader__icon kuiIcon kuiIcon--error fa-warning"></span>
|
||||
<span class="kuiInfoPanelHeader__title">
|
||||
Problem with query
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="kuiInfoPanelBody">
|
||||
<div class="kuiInfoPanelBody__message">
|
||||
Failed to load the anchor document. Please reload or visit
|
||||
<a ng-href="{{ contextApp.state.navigation.discover.url }}">Discover</a>
|
||||
to select a valid anchor document.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="kuiViewContent kuiViewContentItem"
|
||||
ng-if="contextApp.state.loadingStatus.anchor !== contextApp.constants.LOADING_STATUS.FAILED"
|
||||
role="main"
|
||||
>
|
||||
<!-- Controls -->
|
||||
<div class="kuiBar kuiVerticalRhythm">
|
||||
<div class="kuiBarSection">
|
||||
<context-loading-button
|
||||
data-test-subj="predecessorLoadMoreButton"
|
||||
is-disabled="![
|
||||
contextApp.constants.LOADING_STATUS.LOADED,
|
||||
contextApp.constants.LOADING_STATUS.FAILED,
|
||||
].includes(contextApp.state.loadingStatus.predecessors)"
|
||||
icon="'fa-chevron-up'"
|
||||
ng-click="contextApp.actions.fetchMorePredecessorRows()"
|
||||
>
|
||||
Load {{ contextApp.state.queryParameters.defaultStepSize }} more
|
||||
</context-loading-button>
|
||||
<context-size-picker
|
||||
count="contextApp.state.queryParameters.predecessorCount"
|
||||
data-test-subj="predecessorCountPicker"
|
||||
is-disabled="contextApp.state.loadingStatus.anchor !== contextApp.constants.LOADING_STATUS.LOADED"
|
||||
on-change-count="contextApp.actions.fetchGivenPredecessorRows"
|
||||
></context-size-picker>
|
||||
<span>newer documents</span>
|
||||
<span
|
||||
class="kuiStatusText kuiStatusText--warning"
|
||||
ng-if="(contextApp.state.loadingStatus.predecessors === contextApp.constants.LOADING_STATUS.LOADED)
|
||||
&& (contextApp.state.queryParameters.predecessorCount > contextApp.state.rows.predecessors.length)"
|
||||
>
|
||||
<span class="kuiStatusText__icon kuiIcon fa-bolt"></span>
|
||||
<span ng-bind-template="Only {{ contextApp.state.rows.predecessors.length }} documents newer than the anchor could be found."></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="kuiBarSection">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading feedback -->
|
||||
<div
|
||||
ng-if="[
|
||||
contextApp.constants.LOADING_STATUS.UNINITIALIZED,
|
||||
contextApp.constants.LOADING_STATUS.LOADING,
|
||||
].includes(contextApp.state.loadingStatus.anchor)"
|
||||
class="kuiPanel kuiPanel--centered kuiVerticalRhythm"
|
||||
>
|
||||
<div class="kuiLoadingItems">
|
||||
Loading…
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div
|
||||
ng-if="contextApp.state.loadingStatus.anchor === contextApp.constants.LOADING_STATUS.LOADED"
|
||||
class="kuiPanel kuiVerticalRhythm"
|
||||
>
|
||||
<div class="discover-table" fixed-scroll>
|
||||
<doc-table
|
||||
hits="contextApp.state.rows.all"
|
||||
index-pattern="contextApp.state.queryParameters.indexPattern"
|
||||
sorting="contextApp.state.queryParameters.sort"
|
||||
columns="contextApp.state.queryParameters.columns"
|
||||
infinite-scroll="true"
|
||||
></doc-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="kuiBar kuiVerticalRhythm">
|
||||
<div class="kuiBarSection">
|
||||
<context-loading-button
|
||||
data-test-subj="successorLoadMoreButton"
|
||||
is-disabled="![
|
||||
contextApp.constants.LOADING_STATUS.LOADED,
|
||||
contextApp.constants.LOADING_STATUS.FAILED,
|
||||
].includes(contextApp.state.loadingStatus.successors)"
|
||||
icon="'fa-chevron-down'"
|
||||
ng-click="contextApp.actions.fetchMoreSuccessorRows()"
|
||||
>
|
||||
Load {{ contextApp.state.queryParameters.defaultStepSize }} more
|
||||
</context-loading-button>
|
||||
<context-size-picker
|
||||
count="contextApp.state.queryParameters.successorCount"
|
||||
data-test-subj="successorCountPicker"
|
||||
is-disabled="contextApp.state.loadingStatus.anchor !== contextApp.constants.LOADING_STATUS.LOADED"
|
||||
on-change-count="contextApp.actions.fetchGivenSuccessorRows"
|
||||
></context-size-picker>
|
||||
<div>older documents</div>
|
||||
<span
|
||||
class="kuiStatusText kuiStatusText--warning"
|
||||
ng-if="(contextApp.state.loadingStatus.successors === contextApp.constants.LOADING_STATUS.LOADED)
|
||||
&& (contextApp.state.queryParameters.successorCount > contextApp.state.rows.successors.length)"
|
||||
>
|
||||
<span class="kuiStatusText__icon kuiIcon fa-bolt"></span>
|
||||
<span ng-bind-template="Only {{ contextApp.state.rows.successors.length }} documents older than the anchor could be found."></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="kuiBarSection">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
107
src/core_plugins/kibana/public/context/app.js
Normal file
107
src/core_plugins/kibana/public/context/app.js
Normal file
|
@ -0,0 +1,107 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
import uiModules from 'ui/modules';
|
||||
import contextAppTemplate from './app.html';
|
||||
import './components/loading_button';
|
||||
import './components/size_picker/size_picker';
|
||||
import {
|
||||
createInitialQueryParametersState,
|
||||
QueryParameterActionsProvider,
|
||||
QUERY_PARAMETER_KEYS,
|
||||
} from './query_parameters';
|
||||
import {
|
||||
createInitialLoadingStatusState,
|
||||
LOADING_STATUS,
|
||||
QueryActionsProvider,
|
||||
} from './query';
|
||||
|
||||
const module = uiModules.get('apps/context', [
|
||||
'elasticsearch',
|
||||
'kibana',
|
||||
'kibana/config',
|
||||
'kibana/notify',
|
||||
'ngRoute',
|
||||
]);
|
||||
|
||||
module.directive('contextApp', function ContextApp() {
|
||||
return {
|
||||
bindToController: true,
|
||||
controller: ContextAppController,
|
||||
controllerAs: 'contextApp',
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
anchorUid: '=',
|
||||
columns: '=',
|
||||
indexPattern: '=',
|
||||
predecessorCount: '=',
|
||||
successorCount: '=',
|
||||
sort: '=',
|
||||
discoverUrl: '=',
|
||||
},
|
||||
template: contextAppTemplate,
|
||||
};
|
||||
});
|
||||
|
||||
function ContextAppController($scope, config, Private) {
|
||||
const queryParameterActions = Private(QueryParameterActionsProvider);
|
||||
const queryActions = Private(QueryActionsProvider);
|
||||
|
||||
this.state = createInitialState(
|
||||
parseInt(config.get('context:step'), 10),
|
||||
this.discoverUrl,
|
||||
);
|
||||
|
||||
this.actions = _.mapValues(Object.assign(
|
||||
{},
|
||||
queryParameterActions,
|
||||
queryActions,
|
||||
), (action) => (...args) => action(this.state)(...args));
|
||||
|
||||
this.constants = {
|
||||
LOADING_STATUS,
|
||||
};
|
||||
|
||||
$scope.$watchGroup([
|
||||
() => this.state.rows.predecessors,
|
||||
() => this.state.rows.anchor,
|
||||
() => this.state.rows.successors,
|
||||
], (newValues) => this.actions.setAllRows(...newValues));
|
||||
|
||||
/**
|
||||
* Sync query parameters to arguments
|
||||
*/
|
||||
$scope.$watchCollection(
|
||||
() => _.pick(this, QUERY_PARAMETER_KEYS),
|
||||
(newValues) => {
|
||||
// break the watch cycle
|
||||
if (!_.isEqual(newValues, this.state.queryParameters)) {
|
||||
this.actions.fetchAllRowsWithNewQueryParameters(newValues);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
$scope.$watchCollection(
|
||||
() => this.state.queryParameters,
|
||||
(newValues) => {
|
||||
_.assign(this, newValues);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function createInitialState(defaultStepSize, discoverUrl) {
|
||||
return {
|
||||
queryParameters: createInitialQueryParametersState(defaultStepSize),
|
||||
rows: {
|
||||
all: [],
|
||||
anchor: null,
|
||||
predecessors: [],
|
||||
successors: [],
|
||||
},
|
||||
loadingStatus: createInitialLoadingStatusState(),
|
||||
navigation: {
|
||||
discover: {
|
||||
url: discoverUrl,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<button
|
||||
class="kuiButton kuiButton--basic kuiButton--iconText"
|
||||
ng-disabled="isDisabled"
|
||||
>
|
||||
<span
|
||||
class="kuiButton__icon kuiIcon fa-spinner fa-spin"
|
||||
ng-class="isDisabled ? ['fa-spinner', 'fa-spin'] : [icon]"
|
||||
/>
|
||||
<span ng-transclude />
|
||||
</button>
|
|
@ -0,0 +1,21 @@
|
|||
import uiModules from 'ui/modules';
|
||||
import contextLoadingButtonTemplate from './loading_button.html';
|
||||
|
||||
|
||||
const module = uiModules.get('apps/context', [
|
||||
'kibana',
|
||||
'ngRoute',
|
||||
]);
|
||||
|
||||
module.directive('contextLoadingButton', function ContextLoadingButton() {
|
||||
return {
|
||||
replace: true,
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
isDisabled: '=',
|
||||
icon: '=',
|
||||
},
|
||||
template: contextLoadingButtonTemplate,
|
||||
transclude: true,
|
||||
};
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
<input
|
||||
class="kuiTextInput contextSizePicker"
|
||||
ng-disabled="contextSizePicker.isDisabled"
|
||||
ng-model-options="{updateOn: 'change', getterSetter: true, debounce: 200}"
|
||||
ng-model="contextSizePicker.getOrSetCount"
|
||||
type="number"
|
||||
>
|
|
@ -0,0 +1,45 @@
|
|||
import _ from 'lodash';
|
||||
import uiModules from 'ui/modules';
|
||||
import contextSizePickerTemplate from './size_picker.html';
|
||||
import './size_picker.less';
|
||||
|
||||
const module = uiModules.get('apps/context', [
|
||||
'kibana',
|
||||
]);
|
||||
|
||||
module.directive('contextSizePicker', function ContextSizePicker() {
|
||||
return {
|
||||
bindToController: true,
|
||||
controller: ContextSizePickerController,
|
||||
controllerAs: 'contextSizePicker',
|
||||
link: linkContextSizePicker,
|
||||
replace: true,
|
||||
restrict: 'E',
|
||||
require: 'ngModel',
|
||||
scope: {
|
||||
count: '=',
|
||||
isDisabled: '=',
|
||||
onChangeCount: '=', // To avoid inconsistent ngModel states this action
|
||||
// should make sure the new value is propagated back
|
||||
// to the `count` property. If that propagation
|
||||
// fails, the user input will be reset to the value
|
||||
// of `count`.
|
||||
},
|
||||
template: contextSizePickerTemplate,
|
||||
};
|
||||
});
|
||||
|
||||
function linkContextSizePicker(scope, element, attrs, ngModel) {
|
||||
scope.countModel = ngModel;
|
||||
}
|
||||
|
||||
function ContextSizePickerController($scope) {
|
||||
$scope.$watch(
|
||||
() => this.count,
|
||||
() => $scope.countModel.$rollbackViewValue(),
|
||||
);
|
||||
|
||||
this.getOrSetCount = (count) => (
|
||||
_.isUndefined(count) ? this.count : this.onChangeCount(count)
|
||||
);
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* 1. Hide increment and decrement buttons for type="number" input.
|
||||
*/
|
||||
.contextSizePicker {
|
||||
appearance: textfield;
|
||||
text-align: center;
|
||||
width: 5em;
|
||||
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
appearance: none; /* 1 */
|
||||
margin: 0; /* 1 */
|
||||
}
|
||||
}
|
9
src/core_plugins/kibana/public/context/index.html
Normal file
9
src/core_plugins/kibana/public/context/index.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
<context-app
|
||||
anchor-uid="contextAppRoute.anchorUid"
|
||||
columns="contextAppRoute.state.columns"
|
||||
discover-url="contextAppRoute.discoverUrl"
|
||||
index-pattern="contextAppRoute.indexPattern"
|
||||
predecessor-count="contextAppRoute.state.predecessorCount"
|
||||
successor-count="contextAppRoute.state.successorCount"
|
||||
sort="[contextAppRoute.indexPattern.timeFieldName, 'desc']"
|
||||
></context-app>
|
48
src/core_plugins/kibana/public/context/index.js
Normal file
48
src/core_plugins/kibana/public/context/index.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
import uiRoutes from 'ui/routes';
|
||||
|
||||
import './app';
|
||||
import { getDocumentUid } from './api/utils/ids';
|
||||
import contextAppRouteTemplate from './index.html';
|
||||
|
||||
|
||||
uiRoutes
|
||||
.when('/context/:indexPattern/:type/:id', {
|
||||
controller: ContextAppRouteController,
|
||||
controllerAs: 'contextAppRoute',
|
||||
resolve: {
|
||||
indexPattern: function ($route, courier, savedSearches) {
|
||||
return courier.indexPatterns.get($route.current.params.indexPattern);
|
||||
},
|
||||
},
|
||||
template: contextAppRouteTemplate,
|
||||
});
|
||||
|
||||
|
||||
function ContextAppRouteController(
|
||||
$routeParams,
|
||||
$scope,
|
||||
AppState,
|
||||
chrome,
|
||||
config,
|
||||
indexPattern,
|
||||
) {
|
||||
this.state = new AppState(createDefaultAppState(config));
|
||||
this.state.save(true);
|
||||
|
||||
$scope.$watchGroup([
|
||||
'contextAppRoute.state.columns',
|
||||
'contextAppRoute.state.predecessorCount',
|
||||
'contextAppRoute.state.successorCount',
|
||||
], () => this.state.save(true));
|
||||
this.anchorUid = getDocumentUid($routeParams.type, $routeParams.id);
|
||||
this.indexPattern = indexPattern;
|
||||
this.discoverUrl = chrome.getNavLinkById('kibana:discover').lastSubUrl;
|
||||
}
|
||||
|
||||
function createDefaultAppState(config) {
|
||||
return {
|
||||
columns: ['_source'],
|
||||
predecessorCount: parseInt(config.get('context:defaultSize'), 10),
|
||||
successorCount: parseInt(config.get('context:defaultSize'), 10),
|
||||
};
|
||||
}
|
151
src/core_plugins/kibana/public/context/query/actions.js
Normal file
151
src/core_plugins/kibana/public/context/query/actions.js
Normal file
|
@ -0,0 +1,151 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
import { fetchAnchor } from '../api/anchor';
|
||||
import { fetchPredecessors, fetchSuccessors } from '../api/context';
|
||||
import { QueryParameterActionsProvider } from '../query_parameters';
|
||||
import { LOADING_STATUS } from './constants';
|
||||
|
||||
|
||||
export function QueryActionsProvider(es, Notifier, Private, Promise) {
|
||||
const {
|
||||
increasePredecessorCount,
|
||||
increaseSuccessorCount,
|
||||
setPredecessorCount,
|
||||
setQueryParameters,
|
||||
setSuccessorCount,
|
||||
} = Private(QueryParameterActionsProvider);
|
||||
|
||||
const notifier = new Notifier({
|
||||
location: 'Context',
|
||||
});
|
||||
|
||||
const setLoadingStatus = (state) => (subject, status) => (
|
||||
state.loadingStatus[subject] = status
|
||||
);
|
||||
|
||||
const fetchAnchorRow = (state) => () => {
|
||||
const { queryParameters: { indexPattern, anchorUid, sort } } = state;
|
||||
|
||||
setLoadingStatus(state)('anchor', LOADING_STATUS.LOADING);
|
||||
|
||||
return Promise.try(() => (
|
||||
fetchAnchor(es, indexPattern, anchorUid, _.zipObject([sort]))
|
||||
))
|
||||
.then(
|
||||
(anchorDocument) => {
|
||||
setLoadingStatus(state)('anchor', LOADING_STATUS.LOADED);
|
||||
state.rows.anchor = anchorDocument;
|
||||
return anchorDocument;
|
||||
},
|
||||
(error) => {
|
||||
setLoadingStatus(state)('anchor', LOADING_STATUS.FAILED);
|
||||
notifier.error(error);
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const fetchPredecessorRows = (state) => () => {
|
||||
const {
|
||||
queryParameters: { indexPattern, predecessorCount, sort },
|
||||
rows: { anchor },
|
||||
} = state;
|
||||
|
||||
setLoadingStatus(state)('predecessors', LOADING_STATUS.LOADING);
|
||||
|
||||
return Promise.try(() => (
|
||||
fetchPredecessors(es, indexPattern, anchor, _.zipObject([sort]), predecessorCount)
|
||||
))
|
||||
.then(
|
||||
(predecessorDocuments) => {
|
||||
setLoadingStatus(state)('predecessors', LOADING_STATUS.LOADED);
|
||||
state.rows.predecessors = predecessorDocuments;
|
||||
return predecessorDocuments;
|
||||
},
|
||||
(error) => {
|
||||
setLoadingStatus(state)('predecessors', LOADING_STATUS.FAILED);
|
||||
notifier.error(error);
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const fetchSuccessorRows = (state) => () => {
|
||||
const {
|
||||
queryParameters: { indexPattern, sort, successorCount },
|
||||
rows: { anchor },
|
||||
} = state;
|
||||
|
||||
setLoadingStatus(state)('successors', LOADING_STATUS.LOADING);
|
||||
|
||||
return Promise.try(() => (
|
||||
fetchSuccessors(es, indexPattern, anchor, _.zipObject([sort]), successorCount)
|
||||
))
|
||||
.then(
|
||||
(successorDocuments) => {
|
||||
setLoadingStatus(state)('successors', LOADING_STATUS.LOADED);
|
||||
state.rows.successors = successorDocuments;
|
||||
return successorDocuments;
|
||||
},
|
||||
(error) => {
|
||||
setLoadingStatus(state)('successors', LOADING_STATUS.FAILED);
|
||||
notifier.error(error);
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const fetchAllRows = (state) => () => (
|
||||
Promise.try(fetchAnchorRow(state))
|
||||
.then(() => Promise.all([
|
||||
fetchPredecessorRows(state)(),
|
||||
fetchSuccessorRows(state)(),
|
||||
]))
|
||||
);
|
||||
|
||||
const fetchAllRowsWithNewQueryParameters = (state) => (queryParameters) => {
|
||||
setQueryParameters(state)(queryParameters);
|
||||
return fetchAllRows(state)();
|
||||
};
|
||||
|
||||
const fetchGivenPredecessorRows = (state) => (count) => {
|
||||
setPredecessorCount(state)(count);
|
||||
return fetchPredecessorRows(state)();
|
||||
};
|
||||
|
||||
const fetchGivenSuccessorRows = (state) => (count) => {
|
||||
setSuccessorCount(state)(count);
|
||||
return fetchSuccessorRows(state)();
|
||||
};
|
||||
|
||||
const fetchMorePredecessorRows = (state) => () => {
|
||||
increasePredecessorCount(state)();
|
||||
return fetchPredecessorRows(state)();
|
||||
};
|
||||
|
||||
const fetchMoreSuccessorRows = (state) => () => {
|
||||
increaseSuccessorCount(state)();
|
||||
return fetchSuccessorRows(state)();
|
||||
};
|
||||
|
||||
const setAllRows = (state) => (predecessorRows, anchorRow, successorRows) => (
|
||||
state.rows.all = [
|
||||
...(predecessorRows || []),
|
||||
...(anchorRow ? [anchorRow] : []),
|
||||
...(successorRows || []),
|
||||
]
|
||||
);
|
||||
|
||||
return {
|
||||
fetchAllRows,
|
||||
fetchAllRowsWithNewQueryParameters,
|
||||
fetchAnchorRow,
|
||||
fetchGivenPredecessorRows,
|
||||
fetchGivenSuccessorRows,
|
||||
fetchMorePredecessorRows,
|
||||
fetchMoreSuccessorRows,
|
||||
fetchPredecessorRows,
|
||||
fetchSuccessorRows,
|
||||
setAllRows,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
export const LOADING_STATUS = {
|
||||
FAILED: 'failed',
|
||||
LOADED: 'loaded',
|
||||
LOADING: 'loading',
|
||||
UNINITIALIZED: 'uninitialized',
|
||||
};
|
3
src/core_plugins/kibana/public/context/query/index.js
Normal file
3
src/core_plugins/kibana/public/context/query/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export { QueryActionsProvider } from './actions';
|
||||
export { LOADING_STATUS } from './constants';
|
||||
export { createInitialLoadingStatusState } from './state';
|
10
src/core_plugins/kibana/public/context/query/state.js
Normal file
10
src/core_plugins/kibana/public/context/query/state.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { LOADING_STATUS } from './constants';
|
||||
|
||||
|
||||
export function createInitialLoadingStatusState() {
|
||||
return {
|
||||
anchor: LOADING_STATUS.UNINITIALIZED,
|
||||
predecessors: LOADING_STATUS.UNINITIALIZED,
|
||||
successors: LOADING_STATUS.UNINITIALIZED,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
|
||||
export function createStateStub(overrides) {
|
||||
return _.merge({
|
||||
queryParameters: {
|
||||
defaultStepSize: 3,
|
||||
predecessorCount: 10,
|
||||
successorCount: 10,
|
||||
},
|
||||
}, overrides);
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import expect from 'expect.js';
|
||||
|
||||
import { createStateStub } from './_utils';
|
||||
import { QueryParameterActionsProvider } from '../actions';
|
||||
|
||||
|
||||
describe('context app', function () {
|
||||
describe('action increasePredecessorCount', function () {
|
||||
it('should increase the predecessorCount by the given value', function () {
|
||||
const { increasePredecessorCount } = new QueryParameterActionsProvider();
|
||||
const state = createStateStub();
|
||||
|
||||
increasePredecessorCount(state)(20);
|
||||
|
||||
expect(state.queryParameters.predecessorCount).to.equal(30);
|
||||
});
|
||||
|
||||
it('should increase the predecessorCount by the default step size if not value is given', function () {
|
||||
const { increasePredecessorCount } = new QueryParameterActionsProvider();
|
||||
const state = createStateStub();
|
||||
|
||||
increasePredecessorCount(state)();
|
||||
|
||||
expect(state.queryParameters.predecessorCount).to.equal(13);
|
||||
});
|
||||
|
||||
it('should limit the predecessorCount to 0 as a lower bound', function () {
|
||||
const { increasePredecessorCount } = new QueryParameterActionsProvider();
|
||||
const state = createStateStub();
|
||||
|
||||
increasePredecessorCount(state)(-20);
|
||||
|
||||
expect(state.queryParameters.predecessorCount).to.equal(0);
|
||||
});
|
||||
|
||||
it('should limit the predecessorCount to 10000 as an upper bound', function () {
|
||||
const { increasePredecessorCount } = new QueryParameterActionsProvider();
|
||||
const state = createStateStub();
|
||||
|
||||
increasePredecessorCount(state)(20000);
|
||||
|
||||
expect(state.queryParameters.predecessorCount).to.equal(10000);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,45 @@
|
|||
import expect from 'expect.js';
|
||||
|
||||
import { createStateStub } from './_utils';
|
||||
import { QueryParameterActionsProvider } from '../actions';
|
||||
|
||||
|
||||
describe('context app', function () {
|
||||
describe('action increaseSuccessorCount', function () {
|
||||
it('should increase the successorCount by the given value', function () {
|
||||
const { increaseSuccessorCount } = new QueryParameterActionsProvider();
|
||||
const state = createStateStub();
|
||||
|
||||
increaseSuccessorCount(state)(20);
|
||||
|
||||
expect(state.queryParameters.successorCount).to.equal(30);
|
||||
});
|
||||
|
||||
it('should increase the successorCount by the default step size if not value is given', function () {
|
||||
const { increaseSuccessorCount } = new QueryParameterActionsProvider();
|
||||
const state = createStateStub();
|
||||
|
||||
increaseSuccessorCount(state)();
|
||||
|
||||
expect(state.queryParameters.successorCount).to.equal(13);
|
||||
});
|
||||
|
||||
it('should limit the successorCount to 0 as a lower bound', function () {
|
||||
const { increaseSuccessorCount } = new QueryParameterActionsProvider();
|
||||
const state = createStateStub();
|
||||
|
||||
increaseSuccessorCount(state)(-20);
|
||||
|
||||
expect(state.queryParameters.successorCount).to.equal(0);
|
||||
});
|
||||
|
||||
it('should limit the successorCount to 10000 as an upper bound', function () {
|
||||
const { increaseSuccessorCount } = new QueryParameterActionsProvider();
|
||||
const state = createStateStub();
|
||||
|
||||
increaseSuccessorCount(state)(20000);
|
||||
|
||||
expect(state.queryParameters.successorCount).to.equal(10000);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
import expect from 'expect.js';
|
||||
|
||||
import { createStateStub } from './_utils';
|
||||
import { QueryParameterActionsProvider } from '../actions';
|
||||
|
||||
|
||||
describe('context app', function () {
|
||||
describe('action setPredecessorCount', function () {
|
||||
it('should set the predecessorCount to the given value', function () {
|
||||
const { setPredecessorCount } = new QueryParameterActionsProvider();
|
||||
const state = createStateStub();
|
||||
|
||||
setPredecessorCount(state)(20);
|
||||
|
||||
expect(state.queryParameters.predecessorCount).to.equal(20);
|
||||
});
|
||||
|
||||
it('should limit the predecessorCount to 0 as a lower bound', function () {
|
||||
const { setPredecessorCount } = new QueryParameterActionsProvider();
|
||||
const state = createStateStub();
|
||||
|
||||
setPredecessorCount(state)(-1);
|
||||
|
||||
expect(state.queryParameters.predecessorCount).to.equal(0);
|
||||
});
|
||||
|
||||
it('should limit the predecessorCount to 10000 as an upper bound', function () {
|
||||
const { setPredecessorCount } = new QueryParameterActionsProvider();
|
||||
const state = createStateStub();
|
||||
|
||||
setPredecessorCount(state)(20000);
|
||||
|
||||
expect(state.queryParameters.predecessorCount).to.equal(10000);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
import expect from 'expect.js';
|
||||
|
||||
import { createStateStub } from './_utils';
|
||||
import { QueryParameterActionsProvider } from '../actions';
|
||||
|
||||
|
||||
describe('context app', function () {
|
||||
describe('action setQueryParameters', function () {
|
||||
it('should update the queryParameters with valid properties from the given object', function () {
|
||||
const { setQueryParameters } = new QueryParameterActionsProvider();
|
||||
const state = createStateStub({
|
||||
queryParameters: {
|
||||
additionalParameter: 'ADDITIONAL_PARAMETER',
|
||||
}
|
||||
});
|
||||
|
||||
setQueryParameters(state)({
|
||||
anchorUid: 'ANCHOR_UID',
|
||||
columns: ['column'],
|
||||
defaultStepSize: 3,
|
||||
indexPattern: 'INDEX_PATTERN',
|
||||
predecessorCount: 100,
|
||||
successorCount: 100,
|
||||
sort: ['field'],
|
||||
});
|
||||
|
||||
expect(state.queryParameters).to.eql({
|
||||
additionalParameter: 'ADDITIONAL_PARAMETER',
|
||||
anchorUid: 'ANCHOR_UID',
|
||||
columns: ['column'],
|
||||
defaultStepSize: 3,
|
||||
indexPattern: 'INDEX_PATTERN',
|
||||
predecessorCount: 100,
|
||||
successorCount: 100,
|
||||
sort: ['field'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should ignore invalid properties', function () {
|
||||
const { setQueryParameters } = new QueryParameterActionsProvider();
|
||||
const state = createStateStub();
|
||||
|
||||
setQueryParameters(state)({
|
||||
additionalParameter: 'ADDITIONAL_PARAMETER',
|
||||
});
|
||||
|
||||
expect(state.queryParameters).to.eql(createStateStub().queryParameters);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
import expect from 'expect.js';
|
||||
|
||||
import { createStateStub } from './_utils';
|
||||
import { QueryParameterActionsProvider } from '../actions';
|
||||
|
||||
|
||||
describe('context app', function () {
|
||||
describe('action setSuccessorCount', function () {
|
||||
it('should set the successorCount to the given value', function () {
|
||||
const { setSuccessorCount } = new QueryParameterActionsProvider();
|
||||
const state = createStateStub();
|
||||
|
||||
setSuccessorCount(state)(20);
|
||||
|
||||
expect(state.queryParameters.successorCount).to.equal(20);
|
||||
});
|
||||
|
||||
it('should limit the successorCount to 0 as a lower bound', function () {
|
||||
const { setSuccessorCount } = new QueryParameterActionsProvider();
|
||||
const state = createStateStub();
|
||||
|
||||
setSuccessorCount(state)(-1);
|
||||
|
||||
expect(state.queryParameters.successorCount).to.equal(0);
|
||||
});
|
||||
|
||||
it('should limit the successorCount to 10000 as an upper bound', function () {
|
||||
const { setSuccessorCount } = new QueryParameterActionsProvider();
|
||||
const state = createStateStub();
|
||||
|
||||
setSuccessorCount(state)(20000);
|
||||
|
||||
expect(state.queryParameters.successorCount).to.equal(10000);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,57 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
import {
|
||||
MAX_CONTEXT_SIZE,
|
||||
MIN_CONTEXT_SIZE,
|
||||
QUERY_PARAMETER_KEYS,
|
||||
} from './constants';
|
||||
|
||||
|
||||
export function QueryParameterActionsProvider() {
|
||||
const setPredecessorCount = (state) => (predecessorCount) => (
|
||||
state.queryParameters.predecessorCount = clamp(
|
||||
MIN_CONTEXT_SIZE,
|
||||
MAX_CONTEXT_SIZE,
|
||||
predecessorCount,
|
||||
)
|
||||
);
|
||||
|
||||
const increasePredecessorCount = (state) => (
|
||||
value = state.queryParameters.defaultStepSize,
|
||||
) => (
|
||||
setPredecessorCount(state)(state.queryParameters.predecessorCount + value)
|
||||
);
|
||||
|
||||
const setSuccessorCount = (state) => (successorCount) => (
|
||||
state.queryParameters.successorCount = clamp(
|
||||
MIN_CONTEXT_SIZE,
|
||||
MAX_CONTEXT_SIZE,
|
||||
successorCount,
|
||||
)
|
||||
);
|
||||
|
||||
const increaseSuccessorCount = (state) => (
|
||||
value = state.queryParameters.defaultStepSize,
|
||||
) => (
|
||||
setSuccessorCount(state)(state.queryParameters.successorCount + value)
|
||||
);
|
||||
|
||||
const setQueryParameters = (state) => (queryParameters) => (
|
||||
Object.assign(
|
||||
state.queryParameters,
|
||||
_.pick(queryParameters, QUERY_PARAMETER_KEYS),
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
increasePredecessorCount,
|
||||
increaseSuccessorCount,
|
||||
setPredecessorCount,
|
||||
setQueryParameters,
|
||||
setSuccessorCount,
|
||||
};
|
||||
}
|
||||
|
||||
function clamp(minimum, maximum, value) {
|
||||
return Math.max(Math.min(maximum, value), minimum);
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { createInitialQueryParametersState } from './state';
|
||||
|
||||
|
||||
export const MAX_CONTEXT_SIZE = 10000; // Elasticsearch's default maximum size limit
|
||||
export const MIN_CONTEXT_SIZE = 0;
|
||||
export const QUERY_PARAMETER_KEYS = Object.keys(createInitialQueryParametersState());
|
|
@ -0,0 +1,7 @@
|
|||
export { QueryParameterActionsProvider } from './actions';
|
||||
export {
|
||||
MAX_CONTEXT_SIZE,
|
||||
MIN_CONTEXT_SIZE,
|
||||
QUERY_PARAMETER_KEYS,
|
||||
} from './constants';
|
||||
export { createInitialQueryParametersState } from './state';
|
|
@ -0,0 +1,11 @@
|
|||
export function createInitialQueryParametersState(defaultStepSize) {
|
||||
return {
|
||||
anchorUid: null,
|
||||
columns: [],
|
||||
defaultStepSize,
|
||||
indexPattern: null,
|
||||
predecessorCount: 0,
|
||||
successorCount: 0,
|
||||
sort: [],
|
||||
};
|
||||
}
|
|
@ -1,5 +1,9 @@
|
|||
|
||||
<li class="sidebar-item" attr-field="{{::field.name}}">
|
||||
<li
|
||||
class="sidebar-item"
|
||||
attr-field="{{::field.name}}"
|
||||
data-test-subj="field-{{::field.name}}"
|
||||
>
|
||||
<div ng-click="toggleDetails(field)" class="sidebar-item-title">
|
||||
<field-name field="field"></field-name>
|
||||
<button
|
||||
|
|
|
@ -128,6 +128,10 @@
|
|||
font-size: 9px;
|
||||
}
|
||||
|
||||
.discover-table-row--highlight td {
|
||||
background-color: #E2F1F6;
|
||||
}
|
||||
|
||||
.shard-failures {
|
||||
color: @discover-shard-failures-color;
|
||||
background-color: @discover-shard-failures-bg !important;
|
||||
|
|
|
@ -14,6 +14,7 @@ import 'plugins/kibana/dashboard/index';
|
|||
import 'plugins/kibana/management/index';
|
||||
import 'plugins/kibana/doc';
|
||||
import 'plugins/kibana/dev_tools';
|
||||
import 'plugins/kibana/context';
|
||||
import 'ui/vislib';
|
||||
import 'ui/agg_response';
|
||||
import 'ui/agg_types';
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<tr>
|
||||
<th width="1%"></th>
|
||||
<th ng-if="indexPattern.timeFieldName">
|
||||
<th ng-if="indexPattern.timeFieldName" data-test-subj="docTableHeaderField">
|
||||
<span>Time <i ng-class="headerClass(indexPattern.timeFieldName)" ng-click="sort(indexPattern.timeFieldName)" tooltip="Sort by time"></i></span>
|
||||
</th>
|
||||
<th ng-repeat="name in columns">
|
||||
<th ng-repeat="name in columns" data-test-subj="docTableHeaderField">
|
||||
<span class="table-header-name">
|
||||
{{name | shortDots}} <i ng-class="headerClass(name)" ng-click="sort(name)" tooltip="{{tooltip(name)}}" tooltip-append-to-body="1"></i>
|
||||
</span>
|
||||
|
@ -13,4 +13,4 @@
|
|||
<i ng-click="moveRight(name)" class="fa fa-angle-double-right" ng-show="!$last" tooltip="Move column to the right" tooltip-append-to-body="1"></i>
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</tr>
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import rison from 'rison-node';
|
||||
import 'ui/highlight';
|
||||
import 'ui/highlight/highlight_tags';
|
||||
import 'ui/doc_viewer';
|
||||
import 'ui/filters/trust_as_html';
|
||||
import 'ui/filters/short_dots';
|
||||
import './table_row.less';
|
||||
import noWhiteSpace from 'ui/utils/no_white_space';
|
||||
import openRowHtml from 'ui/doc_table/components/table_row/open.html';
|
||||
import detailsHtml from 'ui/doc_table/components/table_row/details.html';
|
||||
|
@ -24,7 +26,7 @@ const MIN_LINE_LENGTH = 20;
|
|||
* <tr ng-repeat="row in rows" kbn-table-row="row"></tr>
|
||||
* ```
|
||||
*/
|
||||
module.directive('kbnTableRow', ['$compile', function ($compile) {
|
||||
module.directive('kbnTableRow', function ($compile, $httpParamSerializer, kbnUrl) {
|
||||
const cellTemplate = _.template(noWhiteSpace(require('ui/doc_table/components/table_row/cell.html')));
|
||||
const truncateByHeightTemplate = _.template(noWhiteSpace(require('ui/partials/truncate_by_height.html')));
|
||||
|
||||
|
@ -89,6 +91,20 @@ module.directive('kbnTableRow', ['$compile', function ($compile) {
|
|||
$scope.filter(field, $scope.flattenedRow[column], type);
|
||||
};
|
||||
|
||||
$scope.getContextAppHref = () => {
|
||||
const path = kbnUrl.eval('#/context/{{ indexPattern }}/{{ anchorType }}/{{ anchorId }}', {
|
||||
anchorId: $scope.row._id,
|
||||
anchorType: $scope.row._type,
|
||||
indexPattern: $scope.indexPattern.id,
|
||||
});
|
||||
const hash = $httpParamSerializer({
|
||||
_a: rison.encode({
|
||||
columns: $scope.columns,
|
||||
}),
|
||||
});
|
||||
return `${path}?${hash}`;
|
||||
};
|
||||
|
||||
// create a tr element that lists the value for each *column*
|
||||
function createSummaryRow(row) {
|
||||
const indexPattern = $scope.indexPattern;
|
||||
|
@ -180,4 +196,4 @@ module.directive('kbnTableRow', ['$compile', function ($compile) {
|
|||
}
|
||||
}
|
||||
};
|
||||
}]);
|
||||
});
|
||||
|
|
19
src/ui/public/doc_table/components/table_row.less
Normal file
19
src/ui/public/doc_table/components/table_row.less
Normal file
|
@ -0,0 +1,19 @@
|
|||
@import (reference) "~ui/styles/variables";
|
||||
|
||||
/**
|
||||
* 1. Visually align the actions with the tabs. We can improve this by using flexbox instead, at a later point.
|
||||
*/
|
||||
.documentTableRow__actions {
|
||||
float: right;
|
||||
font-family: @font-family-base;
|
||||
padding-top: 8px; /* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Put some space between the actions.
|
||||
*/
|
||||
.documentTableRow__action {
|
||||
& + & {
|
||||
margin-left: 10px; /* 1 */
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ if (timefield) {
|
|||
attributes='class="discover-table-datafield"';
|
||||
}
|
||||
%>
|
||||
<td <%= attributes %>>
|
||||
<td <%= attributes %> data-test-subj="docTableField">
|
||||
<%= formatted %>
|
||||
<span class="table-cell-filter">
|
||||
<% if (filterable) { %>
|
||||
|
|
|
@ -1,6 +1,20 @@
|
|||
<td colspan="{{ columns.length + 2 }}">
|
||||
<a class="pull-right" ng-href="#/doc/{{indexPattern.id}}/{{row._index}}/{{row._type}}/?id={{row._id | uriescape}}">
|
||||
<small>Link to /{{row._index}}/{{row._type}}/{{row._id | uriescape}}</small></i>
|
||||
</a>
|
||||
<div class="documentTableRow__actions">
|
||||
<a
|
||||
class="kuiLink documentTableRow__action"
|
||||
data-test-subj="docTableRowAction"
|
||||
ng-href="{{ getContextAppHref() }}"
|
||||
ng-if="indexPattern.hasTimeField()"
|
||||
>
|
||||
View surrounding documents
|
||||
</a>
|
||||
<a
|
||||
class="kuiLink documentTableRow__action"
|
||||
data-test-subj="docTableRowAction"
|
||||
ng-href="#/doc/{{indexPattern.id}}/{{row._index}}/{{row._type}}/?id={{row._id | uriescape}}"
|
||||
>
|
||||
View single document
|
||||
</a>
|
||||
</div>
|
||||
<doc-viewer hit="row" filter="filter" columns="columns" index-pattern="indexPattern"></doc-viewer>
|
||||
</td>
|
||||
</td>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<td ng-click="toggleRow()">
|
||||
<td ng-click="toggleRow()" data-test-subj="docTableExpandToggleColumn">
|
||||
<i
|
||||
class="fa discover-table-open-icon"
|
||||
ng-class="{ 'fa-caret-down': open, 'fa-caret-right': !open }">
|
||||
</i>
|
||||
</td>
|
||||
</td>
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
</table>
|
||||
</paginate>
|
||||
|
||||
<table ng-if="infiniteScroll" class="kbn-table table" ng-if="indexPattern">
|
||||
<table ng-if="infiniteScroll" class="kbn-table table" ng-if="indexPattern" data-test-subj="docTable">
|
||||
<thead
|
||||
kbn-table-header
|
||||
columns="columns"
|
||||
|
@ -35,7 +35,10 @@
|
|||
sorting="sorting"
|
||||
index-pattern="indexPattern"
|
||||
filter="filter"
|
||||
class="discover-table-row"></tr>
|
||||
class="discover-table-row"
|
||||
ng-class="{'discover-table-row--highlight': row['$$_isAnchor']}"
|
||||
data-test-subj="docTableRow{{ row['$$_isAnchor'] ? ' docTableAnchorRow' : ''}}"
|
||||
></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<kbn-infinite-scroll ng-if="infiniteScroll" more="addRows"></kbn-infinite-scroll>
|
||||
|
|
|
@ -21,6 +21,10 @@ kbn-table, .kbn-table, tbody[kbn-rows] {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Use alpha so this will stand out against non-white backgrounds, e.g. the highlighted
|
||||
* row in the Context Log.
|
||||
*/
|
||||
dl.source {
|
||||
margin-bottom: 0;
|
||||
line-height: 2em;
|
||||
|
@ -31,7 +35,7 @@ kbn-table, .kbn-table, tbody[kbn-rows] {
|
|||
}
|
||||
|
||||
dt {
|
||||
background: @table-dt-bg;
|
||||
background-color: rgba(108, 135, 142, 0.15); /* 1 */
|
||||
color: @table-dt-color;
|
||||
padding: @padding-xs-vertical @padding-xs-horizontal;
|
||||
margin-right: @padding-xs-horizontal;
|
||||
|
|
|
@ -1,20 +1,3 @@
|
|||
@import (reference) "~ui/styles/variables";
|
||||
|
||||
@truncate1: fade(@truncate-color, 0%);
|
||||
@truncate2: fade(@truncate-color, 1%);
|
||||
@truncate3: fade(@truncate-color, 99%);
|
||||
@truncate4: fade(@truncate-color, 100%);
|
||||
|
||||
.truncate-by-height {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:before {
|
||||
content: " ";
|
||||
width: 100%;
|
||||
height: 15px; // copied into index.html!
|
||||
position: absolute;
|
||||
left: 0;
|
||||
background: linear-gradient(to bottom, @truncate1 0%, @truncate2 1%, @truncate3 99%, @truncate4 100%);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,6 +52,8 @@
|
|||
@discover-field-details-bg: @kibanaGray5;
|
||||
@discover-field-details-progress-bar-bg: @gray;
|
||||
|
||||
@discover-table-highlight-bg: #faebcc;
|
||||
|
||||
|
||||
// Field name ==================================================================
|
||||
@field-name-color: @kibanaGray3;
|
||||
|
@ -80,7 +82,6 @@
|
|||
|
||||
|
||||
// table =======================================================================
|
||||
@table-dt-bg: @gray-lighter;
|
||||
@table-dt-color: @brand-primary;
|
||||
@table-cell-hover-bg: white;
|
||||
@table-cell-hover-hover-bg: @gray-lighter;
|
||||
|
|
|
@ -310,6 +310,14 @@ export default function defaultSettingsProvider() {
|
|||
'indexPattern:placeholder': {
|
||||
value: 'logstash-*',
|
||||
description: 'The placeholder for the field "Index name or pattern" in the "Settings > Indices" tab.',
|
||||
}
|
||||
},
|
||||
'context:defaultSize': {
|
||||
value: 5,
|
||||
description: 'The number of surrounding entries to show in the context view',
|
||||
},
|
||||
'context:step': {
|
||||
value: 5,
|
||||
description: 'The step size to increment or decrement the context size by',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
54
test/functional/apps/context/_discover_navigation.js
Normal file
54
test/functional/apps/context/_discover_navigation.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
import expect from 'expect.js';
|
||||
|
||||
import { bdd, esClient } from '../../../support';
|
||||
import PageObjects from '../../../support/page_objects';
|
||||
|
||||
|
||||
const TEST_DISCOVER_START_TIME = '2015-09-19 06:31:44.000';
|
||||
const TEST_DISCOVER_END_TIME = '2015-09-23 18:31:44.000';
|
||||
const TEST_COLUMN_NAMES = ['@message'];
|
||||
|
||||
bdd.describe('context link in discover', function contextSize() {
|
||||
bdd.before(async function() {
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
await PageObjects.header.setAbsoluteRange(TEST_DISCOVER_START_TIME, TEST_DISCOVER_END_TIME);
|
||||
await Promise.all(TEST_COLUMN_NAMES.map((columnName) => (
|
||||
PageObjects.discover.clickFieldListItemAdd(columnName)
|
||||
)));
|
||||
});
|
||||
|
||||
bdd.it('should open the context view with the selected document as anchor', async function () {
|
||||
const discoverDocTable = await PageObjects.docTable.getTable();
|
||||
const firstRow = (await PageObjects.docTable.getBodyRows(discoverDocTable))[0];
|
||||
const firstTimestamp = await (await PageObjects.docTable.getFields(firstRow))[0]
|
||||
.getVisibleText();
|
||||
|
||||
// add a column in Discover
|
||||
await (await PageObjects.docTable.getRowExpandToggle(firstRow)).click();
|
||||
const firstDetailsRow = (await PageObjects.docTable.getDetailsRows(discoverDocTable))[0];
|
||||
await (await PageObjects.docTable.getRowActions(firstDetailsRow))[0].click();
|
||||
|
||||
// check the column in the Context View
|
||||
await PageObjects.common.try(async () => {
|
||||
const contextDocTable = await PageObjects.docTable.getTable();
|
||||
const anchorRow = await PageObjects.docTable.getAnchorRow(contextDocTable);
|
||||
const anchorTimestamp = await (await PageObjects.docTable.getFields(anchorRow))[0]
|
||||
.getVisibleText();
|
||||
expect(anchorTimestamp).to.equal(firstTimestamp);
|
||||
});
|
||||
});
|
||||
|
||||
bdd.it('should open the context view with the same columns', async function () {
|
||||
const docTable = await PageObjects.docTable.getTable();
|
||||
await PageObjects.common.try(async () => {
|
||||
const headerFields = await PageObjects.docTable.getHeaderFields(docTable);
|
||||
const columnNames = await Promise.all(headerFields.map((headerField) => (
|
||||
headerField.getVisibleText()
|
||||
)));
|
||||
expect(columnNames).to.eql([
|
||||
'Time',
|
||||
...TEST_COLUMN_NAMES,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
63
test/functional/apps/context/_size.js
Normal file
63
test/functional/apps/context/_size.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
import expect from 'expect.js';
|
||||
|
||||
import { bdd, esClient } from '../../../support';
|
||||
import PageObjects from '../../../support/page_objects';
|
||||
|
||||
|
||||
const TEST_INDEX_PATTERN = 'logstash-*';
|
||||
const TEST_ANCHOR_TYPE = 'apache';
|
||||
const TEST_ANCHOR_ID = 'AU_x3_BrGFA8no6QjjaI';
|
||||
const TEST_DEFAULT_CONTEXT_SIZE = 7;
|
||||
const TEST_STEP_SIZE = 3;
|
||||
|
||||
bdd.describe('context size', function contextSize() {
|
||||
bdd.before(async function() {
|
||||
await esClient.updateConfigDoc({
|
||||
'context:defaultSize': `${TEST_DEFAULT_CONTEXT_SIZE}`,
|
||||
'context:step': `${TEST_STEP_SIZE}`,
|
||||
});
|
||||
});
|
||||
|
||||
bdd.it('should default to the `context:defaultSize` setting', async function () {
|
||||
await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, TEST_ANCHOR_TYPE, TEST_ANCHOR_ID);
|
||||
|
||||
const docTable = await PageObjects.docTable.getTable();
|
||||
await PageObjects.common.try(async function () {
|
||||
expect(await PageObjects.docTable.getBodyRows(docTable)).to.have.length(2 * TEST_DEFAULT_CONTEXT_SIZE + 1);
|
||||
});
|
||||
await PageObjects.common.try(async function() {
|
||||
const predecessorCountPicker = await PageObjects.context.getPredecessorCountPicker();
|
||||
expect(await predecessorCountPicker.getProperty('value')).to.equal(`${TEST_DEFAULT_CONTEXT_SIZE}`);
|
||||
});
|
||||
await PageObjects.common.try(async function() {
|
||||
const successorCountPicker = await PageObjects.context.getSuccessorCountPicker();
|
||||
expect(await successorCountPicker.getProperty('value')).to.equal(`${TEST_DEFAULT_CONTEXT_SIZE}`);
|
||||
});
|
||||
});
|
||||
|
||||
bdd.it('should increase according to the `context:step` setting when clicking the `load newer` button', async function() {
|
||||
await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, TEST_ANCHOR_TYPE, TEST_ANCHOR_ID);
|
||||
|
||||
const docTable = await PageObjects.docTable.getTable();
|
||||
await (await PageObjects.context.getPredecessorLoadMoreButton()).click();
|
||||
await PageObjects.common.try(async function () {
|
||||
expect(await PageObjects.docTable.getBodyRows(docTable)).to.have.length(
|
||||
2 * TEST_DEFAULT_CONTEXT_SIZE + TEST_STEP_SIZE + 1
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
bdd.it('should increase according to the `context:step` setting when clicking the `load older` button', async function() {
|
||||
await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, TEST_ANCHOR_TYPE, TEST_ANCHOR_ID);
|
||||
|
||||
const docTable = await PageObjects.docTable.getTable();
|
||||
const successorLoadMoreButton = await PageObjects.context.getSuccessorLoadMoreButton();
|
||||
await this.remote.moveMouseTo(successorLoadMoreButton); // possibly scroll until the button is visible
|
||||
await successorLoadMoreButton.click();
|
||||
await PageObjects.common.try(async function () {
|
||||
expect(await PageObjects.docTable.getBodyRows(docTable)).to.have.length(
|
||||
2 * TEST_DEFAULT_CONTEXT_SIZE + TEST_STEP_SIZE + 1
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
28
test/functional/apps/context/index.js
Normal file
28
test/functional/apps/context/index.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
|
||||
import {
|
||||
bdd,
|
||||
defaultTimeout,
|
||||
elasticDump,
|
||||
remote,
|
||||
scenarioManager,
|
||||
} from '../../../support';
|
||||
|
||||
import PageObjects from '../../../support/page_objects';
|
||||
|
||||
bdd.describe('context app', function () {
|
||||
this.timeout = defaultTimeout;
|
||||
|
||||
bdd.before(async function () {
|
||||
await PageObjects.remote.setWindowSize(1200,800);
|
||||
await scenarioManager.loadIfEmpty('logstashFunctional');
|
||||
await elasticDump.elasticLoad('visualize','.kibana');
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
});
|
||||
|
||||
bdd.after(function unloadMakelogs() {
|
||||
return scenarioManager.unload('logstashFunctional');
|
||||
});
|
||||
|
||||
require('./_discover_navigation');
|
||||
require('./_size');
|
||||
});
|
|
@ -35,6 +35,7 @@ define(function (require) {
|
|||
'intern/dojo/node!./apps/visualize',
|
||||
'intern/dojo/node!./apps/console',
|
||||
'intern/dojo/node!./apps/dashboard',
|
||||
'intern/dojo/node!./apps/context',
|
||||
'intern/dojo/node!./status_page'
|
||||
].filter((suite) => {
|
||||
if (!requestedApps) return true;
|
||||
|
|
|
@ -34,6 +34,10 @@ module.exports = {
|
|||
pathname: kibanaURL,
|
||||
hash: '/discover',
|
||||
},
|
||||
context: {
|
||||
pathname: kibanaURL,
|
||||
hash: '/context',
|
||||
},
|
||||
visualize: {
|
||||
pathname: kibanaURL,
|
||||
hash: '/visualize',
|
||||
|
|
58
test/support/page_objects/context_page.js
Normal file
58
test/support/page_objects/context_page.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
import rison from 'rison-node';
|
||||
|
||||
import { config } from '../';
|
||||
import PageObjects from './';
|
||||
import getUrl from '../../utils/get_url';
|
||||
|
||||
|
||||
const DEFAULT_INITIAL_STATE = {
|
||||
columns: ['@message'],
|
||||
};
|
||||
|
||||
export default class ContextPage {
|
||||
init(remote) {
|
||||
this.remote = remote;
|
||||
}
|
||||
|
||||
async navigateTo(indexPattern, anchorType, anchorId, overrideInitialState = {}) {
|
||||
const initialState = rison.encode({
|
||||
...DEFAULT_INITIAL_STATE,
|
||||
...overrideInitialState,
|
||||
});
|
||||
const appUrl = getUrl.noAuth(config.servers.kibana, {
|
||||
...config.apps.context,
|
||||
hash: `${config.apps.context.hash}/${indexPattern}/${anchorType}/${anchorId}?_a=${initialState}`,
|
||||
});
|
||||
|
||||
await this.remote.get(appUrl);
|
||||
await this.remote.refresh();
|
||||
await this.waitUntilContextLoadingHasFinished();
|
||||
}
|
||||
|
||||
getPredecessorCountPicker() {
|
||||
return PageObjects.common.findTestSubject('predecessorCountPicker');
|
||||
}
|
||||
|
||||
getSuccessorCountPicker() {
|
||||
return PageObjects.common.findTestSubject('successorCountPicker');
|
||||
}
|
||||
|
||||
getPredecessorLoadMoreButton() {
|
||||
return PageObjects.common.findTestSubject('predecessorLoadMoreButton');
|
||||
}
|
||||
|
||||
getSuccessorLoadMoreButton() {
|
||||
return PageObjects.common.findTestSubject('predecessorLoadMoreButton');
|
||||
}
|
||||
|
||||
waitUntilContextLoadingHasFinished() {
|
||||
return PageObjects.common.try(async () => {
|
||||
if (
|
||||
!(await this.getSuccessorLoadMoreButton().isEnabled())
|
||||
|| !(await this.getPredecessorLoadMoreButton().isEnabled())
|
||||
) {
|
||||
throw new Error('loading context rows');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -291,7 +291,10 @@ export default class DiscoverPage {
|
|||
}
|
||||
|
||||
async clickFieldListItemAdd(field) {
|
||||
await PageObjects.common.findTestSubject('fieldToggle-' + field).click();
|
||||
const listEntry = await PageObjects.common.findTestSubject(`field-${field}`);
|
||||
await this.remote.moveMouseTo(listEntry);
|
||||
await PageObjects.common.findTestSubject(`fieldToggle-${field}`)
|
||||
.click();
|
||||
}
|
||||
|
||||
async clickFieldListItemVisualize(field) {
|
||||
|
|
40
test/support/page_objects/doc_table.js
Normal file
40
test/support/page_objects/doc_table.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
import PageObjects from './';
|
||||
|
||||
export default class DocTable {
|
||||
|
||||
init(remote) {
|
||||
this.remote = remote;
|
||||
}
|
||||
|
||||
getTable() {
|
||||
return PageObjects.common.findTestSubject('docTable');
|
||||
}
|
||||
|
||||
async getBodyRows(table) {
|
||||
return await table.findAllByCssSelector('[data-test-subj~="docTableRow"]');
|
||||
}
|
||||
|
||||
async getAnchorRow(table) {
|
||||
return await table.findByCssSelector('[data-test-subj~="docTableAnchorRow"]');
|
||||
}
|
||||
|
||||
async getRowExpandToggle(row) {
|
||||
return await row.findByCssSelector('[data-test-subj~="docTableExpandToggleColumn"]');
|
||||
}
|
||||
|
||||
async getDetailsRows(table) {
|
||||
return await table.findAllByCssSelector('[data-test-subj~="docTableRow"] + tr');
|
||||
}
|
||||
|
||||
async getRowActions(row) {
|
||||
return await row.findAllByCssSelector('[data-test-subj~="docTableRowAction"]');
|
||||
}
|
||||
|
||||
async getFields(row) {
|
||||
return await row.findAllByCssSelector('[data-test-subj~="docTableField"]');
|
||||
}
|
||||
|
||||
async getHeaderFields(table) {
|
||||
return await table.findAllByCssSelector('[data-test-subj~="docTableHeaderField"]');
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
|
||||
import Common from './common';
|
||||
import ConsolePage from './console_page';
|
||||
import ContextPage from './context_page';
|
||||
import DashboardPage from './dashboard_page';
|
||||
import DiscoverPage from './discover_page';
|
||||
import HeaderPage from './header_page';
|
||||
|
@ -9,9 +10,11 @@ import ShieldPage from './shield_page';
|
|||
import VisualizePage from './visualize_page';
|
||||
import VisualizePointSeriesOptions from './visualize_point_series_options';
|
||||
import MonitoringPage from './monitoring_page';
|
||||
import DocTable from './doc_table';
|
||||
|
||||
const common = new Common();
|
||||
const consolePage = new ConsolePage();
|
||||
const contextPage = new ContextPage();
|
||||
const dashboardPage = new DashboardPage();
|
||||
const discoverPage = new DiscoverPage();
|
||||
const headerPage = new HeaderPage();
|
||||
|
@ -21,26 +24,33 @@ const visualizePage = new VisualizePage();
|
|||
const visualizePointSeriesOptions = new VisualizePointSeriesOptions();
|
||||
const monitoringPage = new MonitoringPage();
|
||||
|
||||
const docTable = new DocTable();
|
||||
|
||||
class PageObjects {
|
||||
|
||||
constructor() {
|
||||
this.isInitialized = false;
|
||||
this.remote = undefined;
|
||||
this.pageObjects = [
|
||||
common,
|
||||
consolePage,
|
||||
contextPage,
|
||||
dashboardPage,
|
||||
discoverPage,
|
||||
headerPage,
|
||||
settingsPage,
|
||||
shieldPage,
|
||||
visualizePage,
|
||||
visualizePointSeriesOptions,
|
||||
monitoringPage,
|
||||
docTable,
|
||||
];
|
||||
}
|
||||
|
||||
init(remote) {
|
||||
this.isInitialized = true;
|
||||
this.remote = remote;
|
||||
common.init(remote);
|
||||
consolePage.init(remote);
|
||||
dashboardPage.init(remote);
|
||||
discoverPage.init(remote);
|
||||
headerPage.init(remote);
|
||||
settingsPage.init(remote);
|
||||
shieldPage.init(remote);
|
||||
visualizePage.init(remote);
|
||||
visualizePointSeriesOptions.init(remote);
|
||||
monitoringPage.init(remote);
|
||||
this.pageObjects.map((pageObject) => pageObject.init(remote));
|
||||
}
|
||||
|
||||
assertInitialized() {
|
||||
|
@ -58,6 +68,10 @@ class PageObjects {
|
|||
return this.assertInitialized() && consolePage;
|
||||
}
|
||||
|
||||
get context() {
|
||||
return this.assertInitialized() && contextPage;
|
||||
}
|
||||
|
||||
get dashboard() {
|
||||
return this.assertInitialized() && dashboardPage;
|
||||
}
|
||||
|
@ -90,6 +104,10 @@ class PageObjects {
|
|||
return this.assertInitialized() && monitoringPage;
|
||||
}
|
||||
|
||||
get docTable() {
|
||||
return this.assertInitialized() && docTable;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new PageObjects();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue