-
Notifications
You must be signed in to change notification settings - Fork 8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This adds a link to the detail view of discover rows to switch to a view of the documents immediately before and after the selected document. Since that view uses the timestamp field of the index pattern, it is only available for time-based indices. See #9198 for detailed screenshots.
- Loading branch information
1 parent
2cb4d51
commit 85facdd
Showing
58 changed files
with
1,819 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
80
src/core_plugins/kibana/public/context/api/__tests__/anchor.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}), | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
49 changes: 49 additions & 0 deletions
49
src/core_plugins/kibana/public/context/api/utils/__tests__/fields.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); | ||
}); | ||
}); |
46 changes: 46 additions & 0 deletions
46
src/core_plugins/kibana/public/context/api/utils/__tests__/queries.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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']); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.