Skip to content

Commit

Permalink
Show event context
Browse files Browse the repository at this point in the history
Backports PR #9198

**Commit 1:**
Add app to display discover entry context

* Original sha: f80a169
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-11-18T12:50:29Z

**Commit 2:**
Extract multi-transclude workaround into directive

The `multi-transclude` attribute directive encapsulates the angular 1.4
workaround for multiple transclusion targets pioneered in `kbn-top-nav`
for separation of concerns and re-use.

There are some differences to the implementation in `kbn-top-nav`:
* The directive logic is completely contained inside the link function
  and therefore won't interfere with other controllers.
* The slots are declared as the attribute value.
* The transcluded items are appended to the transclusion target's
  children instead of replacing the whole target. This preserves the
  attributes present on the target element.

* Original sha: 05aedd8
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-11-21T14:56:20Z

**Commit 3:**
Add a generic local-navigation directive

The new directive `<local-navigation>` creates a navigation that uses
the ui-framework block styles of the same name. It utilizes the
previously introduced `multi-transclude` directive to provide the
following transclusion targets:

* `primaryLeft`: The left side of the top row, often used for
  a title or breadcrumbs.
* `primaryRight`: The right side of the top row, often used for menus
  and time pickers.
* `secondary`: The bottom row for search bars or tab navigation.

While `kbn-top-nav` combined the concerns of rendering the surrounding
DOM as well as menu entries and the timepicker, this directive expects
those to be implemented separately and just placed in the navigation bar
via aforementioned transclusion slots.

A slot for dropdown panels has not yet been added.

* Original sha: 2e422f7
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-11-21T15:04:50Z

**Commit 4:**
Use `<local-navigation>` for context size controls

* Original sha: 89b31e9
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-11-21T15:07:48Z

**Commit 5:**
Wire up the context app state to the url

* Original sha: 3227016
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-11-21T15:09:34Z

**Commit 6:**
Add basic link from discover to the context app

The doc viewer panel now contains a (hideous) link which displays the
context of the entry using the same column configuration. The location
and styling is hopefully subject to future improvement

* Original sha: ef78a3c
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-11-21T15:12:38Z

**Commit 7:**
Change context view breadcrumbs

The breadcrumbs indicate that the context view is a sub-view of the
normal "Discover" view by including a link to it as the first item.

* Original sha: be355d8
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-11-22T19:14:30Z

**Commit 8:**
Return promises from the reload and init actions

* Original sha: b2db703
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-11-22T19:16:53Z

**Commit 9:**
Make the context size display editable

* Original sha: c3d7459
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-11-22T19:19:11Z

**Commit 10:**
Always sort on the index-pattern's time field

To focus on the use case of the app and to rule out numerous edge cases,
the context is always interpreted in relation the time field specified
in the index pattern.

* Original sha: 9e5986c
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-11-23T13:44:34Z

**Commit 11:**
Improve doc and context link styling in docTable

* Original sha: d64f938
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-11-23T14:54:36Z

**Commit 12:**
Fix font-awesome class name typo

* Original sha: 2acc123
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-11-23T14:58:08Z

**Commit 13:**
Hide context link for non-time-based index patterns

The link is hidden for index patterns that do not have a time field
defined, because the entry context view is designed to allow viewing the
surrounding entries with respect to a time ordering.

* Original sha: 4991bd8
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-11-23T15:11:22Z

**Commit 14:**
Merge branch 'master' into discover-context-app

* Original sha: 1bb189d
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-12-05T12:39:18Z

**Commit 15:**
Add setting to configure the default context size

The new setting `context:defaultSize` allows for customization of the
initial number of rows displayed when viewing an entry's context.

* Original sha: e10f458
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-12-05T14:57:49Z

**Commit 16:**
Add setting to configure the context size step

The new setting `context:step` allows for customization of the step size
the context size is incremented/decremented by when using the buttons in
the context view.

* Original sha: 123d6b0
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-12-05T15:01:06Z

**Commit 17:**
Enforce a minimal context size of 0

* Original sha: dbaed4f
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-12-05T15:04:48Z

**Commit 18:**
Reimplement the local nav without multi-transclusion

The local navigation directive does not require the multi-transclusion
workaround anymore. Instead, it comes with a set of child directives
(`localNavigationRow` and `localNavigationRowSection`) that can be used
to compose a complete local navigation declaratively.

* Original sha: 840278f
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-12-06T15:21:51Z

**Commit 19:**
Add visual indicator for first initialization

* Original sha: 4730c77
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-12-06T16:35:37Z

**Commit 20:**
Merge commit '10f7880' into discover-context-app

* Original sha: 2054287
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-12-13T12:04:44Z

**Commit 21:**
Improve loading ui and behaviour, refactor state

The internal context app state is now managed using a unidirectional
data flow. On the ui side, controlling the size of the context now
happens via buttons at the top and bottom of the doc table or via text
inputs in the local navigation bar. Both now allow for independent
manipulation of the number of predecessors and successors. The two
halves of the table are also loaded independently and have independent
loading status indicators.

* Original sha: a9c96aa
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-12-13T18:55:58Z

**Commit 22:**
Merge branch 'master' into discover-context-app

* Original sha: a2b1456
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-12-13T18:59:57Z

**Commit 23:**
Remove plus icons from "load more" buttons

* Original sha: bb1b056
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-12-13T19:16:34Z

**Commit 24:**
Merge branch 'master' into discover-context-app

* Original sha: 36c9b17
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-12-13T21:47:16Z

**Commit 25:**
Merge branch 'master' into discover-context-app

* Original sha: 72e7faf
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-12-14T11:49:07Z

**Commit 26:**
Fix AppState synchronization

* Original sha: 366962a
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-12-15T14:54:59Z

**Commit 27:**
Merge branch 'master' into discover-context-app

* Original sha: a98d967
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-12-15T14:57:32Z

**Commit 28:**
Fix linting errors

* Original sha: 778d3ce
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-12-15T15:00:33Z

**Commit 29:**
Fix more linting errors

* Original sha: 46d8472
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-12-15T15:10:11Z

**Commit 30:**
Make anchor highlighting more prominent

* Original sha: 3ae8a5d
* Authored by Felix Stürmer <stuermer@weltenwort.de> on 2016-12-16T12:41:47Z
  • Loading branch information
elastic-jasper committed Feb 22, 2017
1 parent ca7e56b commit e7b155b
Show file tree
Hide file tree
Showing 58 changed files with 1,819 additions and 47 deletions.
3 changes: 2 additions & 1 deletion docs/management/advanced-options.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 changes: 97 additions & 0 deletions src/core_plugins/kibana/public/context/NOTES.md
Original file line number Diff line number Diff line change
@@ -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.
80 changes: 80 additions & 0 deletions src/core_plugins/kibana/public/context/api/__tests__/anchor.js
Original file line number Diff line number Diff line change
@@ -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 changes: 31 additions & 0 deletions src/core_plugins/kibana/public/context/api/anchor.js
Original file line number Diff line number Diff line change
@@ -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 changes: 47 additions & 0 deletions src/core_plugins/kibana/public/context/api/context.js
Original file line number Diff line number Diff line change
@@ -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,
};
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
Original file line number Diff line number Diff line change
@@ -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']);
});
});
});
Loading

0 comments on commit e7b155b

Please sign in to comment.