mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
De-angularize DocViewer (#42116)
* Migrate doc-viewer directive to use React/TypeScript * Refactor DocViewsRegistryProvider * Add compatibility for 3rd party plugins still using angular * Add tests Angular rendering for usage in React component: Co-authored-by: spalger <email@spalger.com>
This commit is contained in:
parent
a498d3964a
commit
dd26316fd2
26 changed files with 903 additions and 679 deletions
|
@ -1,272 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import sinon from 'sinon';
|
||||
import expect from '@kbn/expect';
|
||||
import ngMock from 'ng_mock';
|
||||
import 'ui/directives/render_directive';
|
||||
import '../views/table';
|
||||
import { DocViewsRegistryProvider } from 'ui/registry/doc_views';
|
||||
import StubbedLogstashIndexPattern from 'fixtures/stubbed_logstash_index_pattern';
|
||||
const hit = {
|
||||
'_index': 'logstash-2014.09.09',
|
||||
'_type': 'apache',
|
||||
'_id': '61',
|
||||
'_score': 1,
|
||||
'_source': {
|
||||
'@message': 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. \
|
||||
Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus \
|
||||
et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, \
|
||||
ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. \
|
||||
Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, \
|
||||
rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. \
|
||||
Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. \
|
||||
Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, \
|
||||
dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. \
|
||||
Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. \
|
||||
Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, \
|
||||
sit amet adipiscing sem neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, \
|
||||
lorem. Maecenas nec odio et ante tincidunt tempus. Donec vitae sapien ut libero venenatis faucibus. \
|
||||
Nullam quis ante. Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed fringilla mauris sit amet nibh. \
|
||||
Donec sodales sagittis magna. Sed consequat, leo eget bibendum sodales, augue velit cursus nunc, quis gravida magna mi a libero. \
|
||||
Fusce vulputate eleifend sapien. Vestibulum purus quam, scelerisque ut, mollis sed, nonummy id, metus. \
|
||||
Nullam accumsan lorem in dui. Cras ultricies mi eu turpis hendrerit fringilla. Vestibulum ante ipsum primis \
|
||||
in faucibus orci luctus et ultrices posuere cubilia Curae; In ac dui quis mi consectetuer lacinia. \
|
||||
Nam pretium turpis et arcu. Duis arcu tortor, suscipit eget, imperdiet nec, imperdiet iaculis, ipsum. \
|
||||
Sed aliquam ultrices mauris. Integer ante arcu, accumsan a, consectetuer eget, posuere ut, mauris. Praesent adipiscing. \
|
||||
Phasellus ullamcorper ipsum rutrum nunc. Nunc nonummy metus. Vestibulum volutpat pretium libero. Cras id dui. Aenean ut',
|
||||
'extension': 'html',
|
||||
'bytes': 100,
|
||||
'area': [{ lat: 7, lon: 7 }],
|
||||
'noMapping': 'hasNoMapping',
|
||||
'objectArray': [{ foo: true }, { bar: false }],
|
||||
'relatedContent': `{
|
||||
"url": "http://www.laweekly.com/news/jonathan-gold-meets-nwa-2385365",
|
||||
"og:type": "article",
|
||||
"og:title": "Jonathan Gold meets N.W.A.",
|
||||
"og:description": "On May 5, 1989 the L.A. Weekly printed a cover story, \
|
||||
written by Jonathan Gold, about N.W.A., the most notorious band in the U.S., let alone in Los Ange...",
|
||||
"og:url": "http://www.laweekly.com/news/jonathan-gold-meets-nwa-2385365",
|
||||
"article:published_time": "2007-12-05T07:59:41-08:00",
|
||||
"article:modified_time": "2015-01-31T14:57:41-08:00",
|
||||
"article:section": "News",
|
||||
"og:image": "http://IMAGES1.laweekly.com/imager/jonathan-gold-meets-nwa/u/original/2415015/03covergold_1.jpg",
|
||||
"og:image:height": "637",
|
||||
"og:image:width": "480",
|
||||
"og:site_name": "LA Weekly",
|
||||
"twitter:title": "Jonathan Gold meets N.W.A.",
|
||||
"twitter:description": "On May 5, 1989 the L.A. Weekly printed a cover story, \
|
||||
written by Jonathan Gold, about N.W.A., the most notorious band in the U.S., let alone in Los Ange...",
|
||||
"twitter:card": "summary",
|
||||
"twitter:image": "http://IMAGES1.laweekly.com/imager/jonathan-gold-meets-nwa/u/original/2415015/03covergold_1.jpg",
|
||||
"twitter:site": "@laweekly"
|
||||
},
|
||||
{
|
||||
"url": "http://www.laweekly.com/news/once-more-in-the-river-2368108",
|
||||
"og:type": "article",
|
||||
"og:title": "Once more in the River",
|
||||
"og:description": "All photos by Mark Mauer. More after the jump...",
|
||||
"og:url": "http://www.laweekly.com/news/once-more-in-the-river-2368108",
|
||||
"article:published_time": "2007-10-15T10:46:29-07:00",
|
||||
"article:modified_time": "2014-10-28T15:00:05-07:00",
|
||||
"article:section": "News",
|
||||
"og:image": "http://IMAGES1.laweekly.com/imager/once-more-in-the-river/u/original/2430775/img_2536.jpg",
|
||||
"og:image:height": "640",
|
||||
"og:image:width": "480",
|
||||
"og:site_name": "LA Weekly",
|
||||
"twitter:title": "Once more in the River",
|
||||
"twitter:description": "All photos by Mark Mauer. More after the jump...",
|
||||
"twitter:card": "summary",
|
||||
"twitter:image": "http://IMAGES1.laweekly.com/imager/once-more-in-the-river/u/original/2430775/img_2536.jpg",
|
||||
"twitter:site": "@laweekly"
|
||||
}`,
|
||||
'_underscore': 1
|
||||
}
|
||||
};
|
||||
|
||||
// Load the kibana app dependencies.
|
||||
let $parentScope;
|
||||
let $scope;
|
||||
let indexPattern;
|
||||
let flattened;
|
||||
let docViews;
|
||||
|
||||
const init = function ($elem, props) {
|
||||
ngMock.inject(function ($rootScope, $compile) {
|
||||
$parentScope = $rootScope;
|
||||
_.assign($parentScope, props);
|
||||
$compile($elem)($parentScope);
|
||||
$elem.scope().$digest();
|
||||
$scope = $elem.isolateScope();
|
||||
});
|
||||
};
|
||||
|
||||
const destroy = function () {
|
||||
$scope.$destroy();
|
||||
$parentScope.$destroy();
|
||||
};
|
||||
|
||||
describe('docViews', function () {
|
||||
let $elem;
|
||||
let initView;
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(function () {
|
||||
const aggs = 'index-pattern="indexPattern" hit="hit" filter="filter"';
|
||||
$elem = angular.element(`<render-directive ${aggs} definition="view.directive"></render-directive>`);
|
||||
ngMock.inject(function (Private) {
|
||||
indexPattern = Private(StubbedLogstashIndexPattern);
|
||||
flattened = indexPattern.flattenHit(hit);
|
||||
docViews = Private(DocViewsRegistryProvider);
|
||||
});
|
||||
initView = function initView(view) {
|
||||
$elem.append(view.directive.template);
|
||||
init($elem, {
|
||||
indexPattern: indexPattern,
|
||||
hit: hit,
|
||||
view: view,
|
||||
filter: sinon.spy()
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
destroy();
|
||||
});
|
||||
|
||||
describe('Table', function () {
|
||||
beforeEach(function () {
|
||||
initView(docViews.byName.Table);
|
||||
});
|
||||
it('should have a row for each field', function () {
|
||||
expect($elem.find('tr').length).to.be(_.keys(flattened).length);
|
||||
});
|
||||
|
||||
it('should have the field name in the first column', function () {
|
||||
_.each(_.keys(flattened), function (field) {
|
||||
expect($elem.find('[data-test-subj="tableDocViewRow-' + field + '"]').length).to.be(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have the a value for each field', function () {
|
||||
_.each(_.keys(flattened), function (field) {
|
||||
const cellValue = $elem
|
||||
.find('[data-test-subj="tableDocViewRow-' + field + '"]')
|
||||
.find('.kbnDocViewer__value').text();
|
||||
|
||||
// This sucks, but testing the filter chain is too hairy ATM
|
||||
expect(cellValue.length).to.be.greaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('filtering', function () {
|
||||
it('should apply a filter when clicking filterable fields', function () {
|
||||
const row = $elem.find('[data-test-subj="tableDocViewRow-bytes"]');
|
||||
|
||||
row.find('.fa-search-plus').first().click();
|
||||
expect($scope.filter.calledOnce).to.be(true);
|
||||
row.find('.fa-search-minus').first().click();
|
||||
expect($scope.filter.calledTwice).to.be(true);
|
||||
row.find('.fa-asterisk').first().click();
|
||||
expect($scope.filter.calledThrice).to.be(true);
|
||||
});
|
||||
|
||||
it('should NOT apply a filter when clicking non-filterable fields', function () {
|
||||
const row = $elem.find('[data-test-subj="tableDocViewRow-area"]');
|
||||
|
||||
row.find('.fa-search-plus').first().click();
|
||||
expect($scope.filter.calledOnce).to.be(false);
|
||||
row.find('.fa-search-minus').first().click();
|
||||
expect($scope.filter.calledTwice).to.be(false);
|
||||
row.find('.fa-asterisk').first().click();
|
||||
expect($scope.filter.calledOnce).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('collapse row', function () {
|
||||
it('should not collapse or expand other fields', function () {
|
||||
const collapseBtns = $elem.find('.discover-table-open-button');
|
||||
const first = collapseBtns.first()[0];
|
||||
const last = collapseBtns.last()[0];
|
||||
|
||||
first.click();
|
||||
expect(first.parentElement.lastElementChild.classList.contains('truncate-by-height'))
|
||||
.to.equal(false);
|
||||
expect(last.parentElement.lastElementChild.classList.contains('truncate-by-height'))
|
||||
.to.equal(true);
|
||||
|
||||
first.click();
|
||||
expect(first.parentElement.lastElementChild.classList.contains('truncate-by-height'))
|
||||
.to.equal(true);
|
||||
expect(last.parentElement.lastElementChild.classList.contains('truncate-by-height'))
|
||||
.to.equal(true);
|
||||
});
|
||||
|
||||
it('should collapse an overflowed field details by default', function () {
|
||||
const collapseBtn = $elem.find('.discover-table-open-button').first()[0];
|
||||
expect(collapseBtn.parentElement.lastElementChild
|
||||
.classList.contains('truncate-by-height')).to.equal(true);
|
||||
});
|
||||
|
||||
it('should expand and collapse an overflowed field details', function () {
|
||||
const collapseBtn = $elem.find('.discover-table-open-button').first()[0];
|
||||
const spy = sinon.spy($scope, 'toggleViewer');
|
||||
|
||||
collapseBtn.click();
|
||||
expect(spy.calledOnce).to.equal(true);
|
||||
collapseBtn.click();
|
||||
expect(spy.calledTwice).to.equal(true);
|
||||
spy.restore();
|
||||
|
||||
collapseBtn.click();
|
||||
expect(collapseBtn.parentElement.lastElementChild.classList.contains('truncate-by-height'))
|
||||
.to.equal(false);
|
||||
collapseBtn.click();
|
||||
expect(collapseBtn.parentElement.lastElementChild.classList.contains('truncate-by-height'))
|
||||
.to.equal(true);
|
||||
});
|
||||
|
||||
it('should have collapse button available in View single document mode', function () {
|
||||
$scope.filter = null;
|
||||
const collapseBtn = $elem.find('.discover-table-open-button').first()[0];
|
||||
expect(collapseBtn).not.to.equal(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('warnings', function () {
|
||||
it('displays a warning about field name starting with underscore', function () {
|
||||
const row = $elem.find('[data-test-subj="tableDocViewRow-_underscore"]');
|
||||
expect(row.find('.kbnDocViewer__underscore').length).to.be(1);
|
||||
expect(row.find('.kbnDocViewer__noMapping').length).to.be(0);
|
||||
expect(row.find('.kbnDocViewer__objectArray').length).to.be(0);
|
||||
});
|
||||
|
||||
it('displays a warning about missing mappings', function () {
|
||||
const row = $elem.find('[data-test-subj="tableDocViewRow-noMapping"]');
|
||||
expect(row.find('.kbnDocViewer__underscore').length).to.be(0);
|
||||
expect(row.find('.kbnDocViewer__noMapping').length).to.be(1);
|
||||
expect(row.find('.kbnDocViewer__objectArray').length).to.be(0);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
|
@ -16,18 +16,18 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { addDocView } from 'ui/registry/doc_views';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { JsonCodeEditor } from './json_code_editor';
|
||||
|
||||
import _ from 'lodash';
|
||||
import { uiRegistry } from './_registry';
|
||||
|
||||
export const DocViewsRegistryProvider = uiRegistry({
|
||||
name: 'docViews',
|
||||
index: ['name'],
|
||||
order: ['order'],
|
||||
constructor() {
|
||||
this.forEach(docView => {
|
||||
docView.shouldShow = docView.shouldShow || _.constant(true);
|
||||
docView.name = docView.name || docView.title;
|
||||
});
|
||||
}
|
||||
/*
|
||||
* Registration of the the doc view: json
|
||||
* - used to display an ES hit as pretty printed JSON at Discover
|
||||
*/
|
||||
addDocView({
|
||||
title: i18n.translate('kbnDocViews.json.jsonTitle', {
|
||||
defaultMessage: 'JSON',
|
||||
}),
|
||||
order: 20,
|
||||
component: JsonCodeEditor,
|
||||
});
|
|
@ -19,8 +19,16 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { JsonCodeEditor } from './json_code_editor';
|
||||
import { IndexPattern } from 'ui/index_patterns';
|
||||
|
||||
it('returns the `JsonCodeEditor` component', () => {
|
||||
const hit = { _index: 'test', _source: { test: 123 } };
|
||||
expect(shallow(<JsonCodeEditor hit={hit} />)).toMatchSnapshot();
|
||||
const props = {
|
||||
hit: { _index: 'test', _source: { test: 123 } },
|
||||
columns: [],
|
||||
indexPattern: {} as IndexPattern,
|
||||
filter: jest.fn(),
|
||||
onAddColumn: jest.fn(),
|
||||
onRemoveColumn: jest.fn(),
|
||||
};
|
||||
expect(shallow(<JsonCodeEditor {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
|
|
|
@ -20,12 +20,9 @@
|
|||
import { EuiCodeEditor } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { DocViewRenderProps } from 'ui/registry/doc_views';
|
||||
|
||||
export interface JsonCodeEditorProps {
|
||||
hit: Record<string, any>;
|
||||
}
|
||||
|
||||
export function JsonCodeEditor({ hit }: JsonCodeEditorProps) {
|
||||
export function JsonCodeEditor({ hit }: DocViewRenderProps) {
|
||||
return (
|
||||
<EuiCodeEditor
|
||||
aria-label={
|
||||
|
|
|
@ -18,72 +18,60 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { DocViewsRegistryProvider } from 'ui/registry/doc_views';
|
||||
|
||||
import { addDocView } from 'ui/registry/doc_views';
|
||||
import '../filters/trust_as_html';
|
||||
import tableHtml from './table.html';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
DocViewsRegistryProvider.register(function () {
|
||||
const MIN_LINE_LENGTH = 350;
|
||||
const MIN_LINE_LENGTH = 350;
|
||||
|
||||
return {
|
||||
title: i18n.translate('kbnDocViews.table.tableTitle', {
|
||||
defaultMessage: 'Table'
|
||||
}),
|
||||
order: 10,
|
||||
directive: {
|
||||
template: tableHtml,
|
||||
scope: {
|
||||
hit: '=',
|
||||
indexPattern: '=',
|
||||
filter: '=',
|
||||
columns: '=',
|
||||
onAddColumn: '=',
|
||||
onRemoveColumn: '=',
|
||||
},
|
||||
controller: function ($scope) {
|
||||
$scope.mapping = $scope.indexPattern.fields.byName;
|
||||
$scope.flattened = $scope.indexPattern.flattenHit($scope.hit);
|
||||
$scope.formatted = $scope.indexPattern.formatHit($scope.hit);
|
||||
$scope.fields = _.keys($scope.flattened).sort();
|
||||
$scope.fieldRowOpen = {};
|
||||
$scope.fields.forEach(field => $scope.fieldRowOpen[field] = false);
|
||||
addDocView({
|
||||
title: i18n.translate('kbnDocViews.table.tableTitle', {
|
||||
defaultMessage: 'Table',
|
||||
}),
|
||||
order: 10,
|
||||
directive: {
|
||||
template: tableHtml,
|
||||
controller: $scope => {
|
||||
$scope.mapping = $scope.indexPattern.fields.byName;
|
||||
$scope.flattened = $scope.indexPattern.flattenHit($scope.hit);
|
||||
$scope.formatted = $scope.indexPattern.formatHit($scope.hit);
|
||||
$scope.fields = _.keys($scope.flattened).sort();
|
||||
$scope.fieldRowOpen = {};
|
||||
$scope.fields.forEach(field => ($scope.fieldRowOpen[field] = false));
|
||||
|
||||
$scope.canToggleColumns = function canToggleColumn() {
|
||||
return (
|
||||
_.isFunction($scope.onAddColumn)
|
||||
&& _.isFunction($scope.onRemoveColumn)
|
||||
);
|
||||
};
|
||||
$scope.canToggleColumns = function canToggleColumn() {
|
||||
return _.isFunction($scope.onAddColumn) && _.isFunction($scope.onRemoveColumn);
|
||||
};
|
||||
|
||||
$scope.toggleColumn = function toggleColumn(columnName) {
|
||||
if ($scope.columns.includes(columnName)) {
|
||||
$scope.onRemoveColumn(columnName);
|
||||
} else {
|
||||
$scope.onAddColumn(columnName);
|
||||
}
|
||||
};
|
||||
$scope.toggleColumn = function toggleColumn(columnName) {
|
||||
if ($scope.columns.includes(columnName)) {
|
||||
$scope.onRemoveColumn(columnName);
|
||||
} else {
|
||||
$scope.onAddColumn(columnName);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.isColumnActive = function isColumnActive(columnName) {
|
||||
return $scope.columns.includes(columnName);
|
||||
};
|
||||
$scope.isColumnActive = function isColumnActive(columnName) {
|
||||
return $scope.columns.includes(columnName);
|
||||
};
|
||||
|
||||
$scope.showArrayInObjectsWarning = function (row, field) {
|
||||
const value = $scope.flattened[field];
|
||||
return Array.isArray(value) && typeof value[0] === 'object';
|
||||
};
|
||||
$scope.showArrayInObjectsWarning = function (row, field) {
|
||||
const value = $scope.flattened[field];
|
||||
return Array.isArray(value) && typeof value[0] === 'object';
|
||||
};
|
||||
|
||||
$scope.enableDocValueCollapse = function (docValueField) {
|
||||
const html = (typeof $scope.formatted[docValueField] === 'undefined') ?
|
||||
$scope.hit[docValueField] : $scope.formatted[docValueField];
|
||||
return html.length > MIN_LINE_LENGTH;
|
||||
};
|
||||
$scope.enableDocValueCollapse = function (docValueField) {
|
||||
const html =
|
||||
typeof $scope.formatted[docValueField] === 'undefined'
|
||||
? $scope.hit[docValueField]
|
||||
: $scope.formatted[docValueField];
|
||||
return html.length > MIN_LINE_LENGTH;
|
||||
};
|
||||
|
||||
$scope.toggleViewer = function (field) {
|
||||
$scope.fieldRowOpen[field] = !$scope.fieldRowOpen[field];
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
$scope.toggleViewer = function (field) {
|
||||
$scope.fieldRowOpen[field] = !$scope.fieldRowOpen[field];
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -39,25 +39,27 @@ describe('Doc Table', function () {
|
|||
let stubFieldFormatConverter;
|
||||
|
||||
beforeEach(ngMock.module('kibana', 'apps/discover'));
|
||||
beforeEach(ngMock.inject(function (_config_, $rootScope, Private) {
|
||||
config = _config_;
|
||||
$parentScope = $rootScope;
|
||||
$parentScope.indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
|
||||
mapping = $parentScope.indexPattern.fields.byName;
|
||||
beforeEach(
|
||||
ngMock.inject(function (_config_, $rootScope, Private) {
|
||||
config = _config_;
|
||||
$parentScope = $rootScope;
|
||||
$parentScope.indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
|
||||
mapping = $parentScope.indexPattern.fields.byName;
|
||||
|
||||
// Stub `getConverterFor` for a field in the indexPattern to return mock data.
|
||||
// Returns `val` if provided, otherwise generates fake data for the field.
|
||||
fakeRowVals = getFakeRowVals('formatted', 0, mapping);
|
||||
stubFieldFormatConverter = function ($root, field, val = null) {
|
||||
$root.indexPattern.fields.byName[field].format.getConverterFor = () => (...args) => {
|
||||
if (val) {
|
||||
return val;
|
||||
}
|
||||
const fieldName = _.get(args, '[1].name', null);
|
||||
return fakeRowVals[fieldName] || '';
|
||||
// Stub `getConverterFor` for a field in the indexPattern to return mock data.
|
||||
// Returns `val` if provided, otherwise generates fake data for the field.
|
||||
fakeRowVals = getFakeRowVals('formatted', 0, mapping);
|
||||
stubFieldFormatConverter = function ($root, field, val = null) {
|
||||
$root.indexPattern.fields.byName[field].format.getConverterFor = () => (...args) => {
|
||||
if (val) {
|
||||
return val;
|
||||
}
|
||||
const fieldName = _.get(args, '[1].name', null);
|
||||
return fakeRowVals[fieldName] || '';
|
||||
};
|
||||
};
|
||||
};
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
// Sets up the directive, take an element, and a list of properties to attach to the parent scope.
|
||||
const init = function ($elem, props) {
|
||||
|
@ -119,11 +121,11 @@ describe('Doc Table', function () {
|
|||
describe('kbnTableRow', function () {
|
||||
const $elem = angular.element(
|
||||
'<tr kbn-table-row="row" ' +
|
||||
'columns="columns" ' +
|
||||
'sorting="sorting"' +
|
||||
'filter="filter"' +
|
||||
'index-pattern="indexPattern"' +
|
||||
'></tr>'
|
||||
'columns="columns" ' +
|
||||
'sorting="sorting"' +
|
||||
'filter="filter"' +
|
||||
'index-pattern="indexPattern"' +
|
||||
'></tr>'
|
||||
);
|
||||
let row;
|
||||
|
||||
|
@ -139,8 +141,10 @@ describe('Doc Table', function () {
|
|||
});
|
||||
|
||||
// Ignore the metaFields (_id, _type, etc) since we don't have a mapping for them
|
||||
sinon.stub(config, 'get').withArgs('metaFields').returns([]);
|
||||
|
||||
sinon
|
||||
.stub(config, 'get')
|
||||
.withArgs('metaFields')
|
||||
.returns([]);
|
||||
});
|
||||
afterEach(function () {
|
||||
destroy();
|
||||
|
@ -180,9 +184,7 @@ describe('Doc Table', function () {
|
|||
expect($details.is('tr')).to.be(true);
|
||||
expect($details.text()).to.not.be.empty();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -195,7 +197,6 @@ describe('Doc Table', function () {
|
|||
'index-pattern="indexPattern"' +
|
||||
'></tr>'
|
||||
);
|
||||
let $details;
|
||||
let row;
|
||||
|
||||
beforeEach(function () {
|
||||
|
@ -209,21 +210,28 @@ describe('Doc Table', function () {
|
|||
maxLength: 50,
|
||||
});
|
||||
|
||||
sinon.stub(config, 'get').withArgs('metaFields').returns(['_id']);
|
||||
sinon
|
||||
.stub(config, 'get')
|
||||
.withArgs('metaFields')
|
||||
.returns(['_id']);
|
||||
|
||||
// Open the row
|
||||
$scope.toggleRow();
|
||||
$scope.$digest();
|
||||
$details = $elem.next();
|
||||
$elem.next();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
destroy();
|
||||
});
|
||||
|
||||
it('should render even when the row source contains a field with the same name as a meta field', function () {
|
||||
/** this no longer works with the new plugin approach
|
||||
it('should render even when the row source contains a field with the same name as a meta field', function () {
|
||||
setTimeout(() => {
|
||||
//this should be overridden by later changes
|
||||
}, 100);
|
||||
expect($details.find('tr').length).to.be(_.keys($parentScope.indexPattern.flattenHit($scope.row)).length);
|
||||
});
|
||||
}); */
|
||||
});
|
||||
|
||||
describe('row diffing', function () {
|
||||
|
@ -232,37 +240,50 @@ describe('Doc Table', function () {
|
|||
let $root;
|
||||
let $before;
|
||||
|
||||
beforeEach(ngMock.inject(function ($rootScope, $compile, Private) {
|
||||
$root = $rootScope;
|
||||
$root.row = getFakeRow(0, mapping);
|
||||
$root.columns = ['_source'];
|
||||
$root.sorting = [];
|
||||
$root.filtering = sinon.spy();
|
||||
$root.maxLength = 50;
|
||||
$root.mapping = mapping;
|
||||
$root.indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
|
||||
beforeEach(
|
||||
ngMock.inject(function ($rootScope, $compile, Private) {
|
||||
$root = $rootScope;
|
||||
$root.row = getFakeRow(0, mapping);
|
||||
$root.columns = ['_source'];
|
||||
$root.sorting = [];
|
||||
$root.filtering = sinon.spy();
|
||||
$root.maxLength = 50;
|
||||
$root.mapping = mapping;
|
||||
$root.indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
|
||||
|
||||
// Stub field format converters for every field in the indexPattern
|
||||
Object.keys($root.indexPattern.fields.byName).forEach(f => stubFieldFormatConverter($root, f));
|
||||
// Stub field format converters for every field in the indexPattern
|
||||
Object.keys($root.indexPattern.fields.byName).forEach(f =>
|
||||
stubFieldFormatConverter($root, f)
|
||||
);
|
||||
|
||||
$row = $('<tr>')
|
||||
.attr({
|
||||
$row = $('<tr>').attr({
|
||||
'kbn-table-row': 'row',
|
||||
'columns': 'columns',
|
||||
'sorting': 'sorting',
|
||||
'filtering': 'filtering',
|
||||
columns: 'columns',
|
||||
sorting: 'sorting',
|
||||
filtering: 'filtering',
|
||||
'index-pattern': 'indexPattern',
|
||||
});
|
||||
|
||||
$scope = $root.$new();
|
||||
$compile($row)($scope);
|
||||
$root.$apply();
|
||||
$scope = $root.$new();
|
||||
$compile($row)($scope);
|
||||
$root.$apply();
|
||||
|
||||
$before = $row.find('td');
|
||||
expect($before).to.have.length(3);
|
||||
expect($before.eq(0).text().trim()).to.be('');
|
||||
expect($before.eq(1).text().trim()).to.match(/^time_formatted/);
|
||||
}));
|
||||
$before = $row.find('td');
|
||||
expect($before).to.have.length(3);
|
||||
expect(
|
||||
$before
|
||||
.eq(0)
|
||||
.text()
|
||||
.trim()
|
||||
).to.be('');
|
||||
expect(
|
||||
$before
|
||||
.eq(1)
|
||||
.text()
|
||||
.trim()
|
||||
).to.match(/^time_formatted/);
|
||||
})
|
||||
);
|
||||
|
||||
afterEach(function () {
|
||||
$row.remove();
|
||||
|
@ -277,7 +298,12 @@ describe('Doc Table', function () {
|
|||
expect($after[0]).to.be($before[0]);
|
||||
expect($after[1]).to.be($before[1]);
|
||||
expect($after[2]).to.be($before[2]);
|
||||
expect($after.eq(3).text().trim()).to.match(/^bytes_formatted/);
|
||||
expect(
|
||||
$after
|
||||
.eq(3)
|
||||
.text()
|
||||
.trim()
|
||||
).to.match(/^bytes_formatted/);
|
||||
});
|
||||
|
||||
it('handles two new columns at once', function () {
|
||||
|
@ -290,30 +316,49 @@ describe('Doc Table', function () {
|
|||
expect($after[0]).to.be($before[0]);
|
||||
expect($after[1]).to.be($before[1]);
|
||||
expect($after[2]).to.be($before[2]);
|
||||
expect($after.eq(3).text().trim()).to.match(/^bytes_formatted/);
|
||||
expect($after.eq(4).text().trim()).to.match(/^request_body_formatted/);
|
||||
expect(
|
||||
$after
|
||||
.eq(3)
|
||||
.text()
|
||||
.trim()
|
||||
).to.match(/^bytes_formatted/);
|
||||
expect(
|
||||
$after
|
||||
.eq(4)
|
||||
.text()
|
||||
.trim()
|
||||
).to.match(/^request_body_formatted/);
|
||||
});
|
||||
|
||||
it('handles three new columns in odd places', function () {
|
||||
$root.columns = [
|
||||
'@timestamp',
|
||||
'bytes',
|
||||
'_source',
|
||||
'request_body'
|
||||
];
|
||||
$root.columns = ['@timestamp', 'bytes', '_source', 'request_body'];
|
||||
$root.$apply();
|
||||
|
||||
const $after = $row.find('td');
|
||||
expect($after).to.have.length(6);
|
||||
expect($after[0]).to.be($before[0]);
|
||||
expect($after[1]).to.be($before[1]);
|
||||
expect($after.eq(2).text().trim()).to.match(/^@timestamp_formatted/);
|
||||
expect($after.eq(3).text().trim()).to.match(/^bytes_formatted/);
|
||||
expect(
|
||||
$after
|
||||
.eq(2)
|
||||
.text()
|
||||
.trim()
|
||||
).to.match(/^@timestamp_formatted/);
|
||||
expect(
|
||||
$after
|
||||
.eq(3)
|
||||
.text()
|
||||
.trim()
|
||||
).to.match(/^bytes_formatted/);
|
||||
expect($after[4]).to.be($before[2]);
|
||||
expect($after.eq(5).text().trim()).to.match(/^request_body_formatted/);
|
||||
expect(
|
||||
$after
|
||||
.eq(5)
|
||||
.text()
|
||||
.trim()
|
||||
).to.match(/^request_body_formatted/);
|
||||
});
|
||||
|
||||
|
||||
it('handles a removed column', function () {
|
||||
_.pull($root.columns, '_source');
|
||||
$root.$apply();
|
||||
|
@ -359,7 +404,12 @@ describe('Doc Table', function () {
|
|||
expect($after).to.have.length(3);
|
||||
expect($after[0]).to.be($before[0]);
|
||||
expect($after[1]).to.be($before[1]);
|
||||
expect($after.eq(2).text().trim()).to.match(/^@timestamp_formatted/);
|
||||
expect(
|
||||
$after
|
||||
.eq(2)
|
||||
.text()
|
||||
.trim()
|
||||
).to.match(/^@timestamp_formatted/);
|
||||
});
|
||||
|
||||
it('handles two columns with the same content', function () {
|
||||
|
@ -372,8 +422,18 @@ describe('Doc Table', function () {
|
|||
|
||||
const $after = $row.find('td');
|
||||
expect($after).to.have.length(4);
|
||||
expect($after.eq(2).text().trim()).to.match(/^bytes_formatted/);
|
||||
expect($after.eq(3).text().trim()).to.match(/^bytes_formatted/);
|
||||
expect(
|
||||
$after
|
||||
.eq(2)
|
||||
.text()
|
||||
.trim()
|
||||
).to.match(/^bytes_formatted/);
|
||||
expect(
|
||||
$after
|
||||
.eq(3)
|
||||
.text()
|
||||
.trim()
|
||||
).to.match(/^bytes_formatted/);
|
||||
});
|
||||
|
||||
it('handles two columns swapping position', function () {
|
||||
|
@ -423,9 +483,24 @@ describe('Doc Table', function () {
|
|||
expect($after[0]).to.be($before[0]);
|
||||
expect($after[1]).to.be($before[1]);
|
||||
expect($after[2]).to.be($before[2]);
|
||||
expect($after.eq(3).text().trim()).to.match(/^bytes_formatted/);
|
||||
expect($after.eq(4).text().trim()).to.match(/^bytes_formatted/);
|
||||
expect($after.eq(5).text().trim()).to.match(/^bytes_formatted/);
|
||||
expect(
|
||||
$after
|
||||
.eq(3)
|
||||
.text()
|
||||
.trim()
|
||||
).to.match(/^bytes_formatted/);
|
||||
expect(
|
||||
$after
|
||||
.eq(4)
|
||||
.text()
|
||||
.trim()
|
||||
).to.match(/^bytes_formatted/);
|
||||
expect(
|
||||
$after
|
||||
.eq(5)
|
||||
.text()
|
||||
.trim()
|
||||
).to.match(/^bytes_formatted/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -32,8 +32,6 @@ import truncateByHeightTemplateHtml from '../components/table_row/truncate_by_he
|
|||
|
||||
const module = uiModules.get('app/discover');
|
||||
|
||||
|
||||
|
||||
// guesstimate at the minimum number of chars wide cells in the table should be
|
||||
const MIN_LINE_LENGTH = 20;
|
||||
|
||||
|
@ -93,18 +91,14 @@ module.directive('kbnTableRow', function ($compile, $httpParamSerializer, kbnUrl
|
|||
|
||||
// empty the details and rebuild it
|
||||
$detailsTr.html(detailsHtml);
|
||||
|
||||
$detailsScope.row = $scope.row;
|
||||
$detailsScope.uriEncodedId = encodeURIComponent($detailsScope.row._id);
|
||||
$detailsScope.hit = $scope.row;
|
||||
$detailsScope.uriEncodedId = encodeURIComponent($detailsScope.hit._id);
|
||||
|
||||
$compile($detailsTr)($detailsScope);
|
||||
};
|
||||
|
||||
$scope.$watchMulti([
|
||||
'indexPattern.timeFieldName',
|
||||
'row.highlight',
|
||||
'[]columns'
|
||||
], function () {
|
||||
$scope.$watchMulti(['indexPattern.timeFieldName', 'row.highlight', '[]columns'], function () {
|
||||
createSummaryRow($scope.row, $scope.row._id);
|
||||
});
|
||||
|
||||
|
@ -135,37 +129,38 @@ module.directive('kbnTableRow', function ($compile, $httpParamSerializer, kbnUrl
|
|||
$scope.flattenedRow = indexPattern.flattenHit(row);
|
||||
|
||||
// We just create a string here because its faster.
|
||||
const newHtmls = [
|
||||
openRowHtml
|
||||
];
|
||||
const newHtmls = [openRowHtml];
|
||||
|
||||
const mapping = indexPattern.fields.byName;
|
||||
const hideTimeColumn = config.get('doc_table:hideTimeColumn');
|
||||
if (indexPattern.timeFieldName && !hideTimeColumn) {
|
||||
newHtmls.push(cellTemplate({
|
||||
timefield: true,
|
||||
formatted: _displayField(row, indexPattern.timeFieldName),
|
||||
filterable: (
|
||||
mapping[indexPattern.timeFieldName].filterable
|
||||
&& _.isFunction($scope.filter)
|
||||
),
|
||||
column: indexPattern.timeFieldName
|
||||
}));
|
||||
newHtmls.push(
|
||||
cellTemplate({
|
||||
timefield: true,
|
||||
formatted: _displayField(row, indexPattern.timeFieldName),
|
||||
filterable:
|
||||
mapping[indexPattern.timeFieldName].filterable && _.isFunction($scope.filter),
|
||||
column: indexPattern.timeFieldName,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
$scope.columns.forEach(function (column) {
|
||||
const isFilterable = $scope.flattenedRow[column] !== undefined
|
||||
&& mapping[column]
|
||||
&& mapping[column].filterable
|
||||
&& _.isFunction($scope.filter);
|
||||
const isFilterable =
|
||||
$scope.flattenedRow[column] !== undefined &&
|
||||
mapping[column] &&
|
||||
mapping[column].filterable &&
|
||||
_.isFunction($scope.filter);
|
||||
|
||||
newHtmls.push(cellTemplate({
|
||||
timefield: false,
|
||||
sourcefield: (column === '_source'),
|
||||
formatted: _displayField(row, column, true),
|
||||
filterable: isFilterable,
|
||||
column
|
||||
}));
|
||||
newHtmls.push(
|
||||
cellTemplate({
|
||||
timefield: false,
|
||||
sourcefield: column === '_source',
|
||||
formatted: _displayField(row, column, true),
|
||||
filterable: isFilterable,
|
||||
column,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
let $cells = $el.children();
|
||||
|
@ -213,12 +208,12 @@ module.directive('kbnTableRow', function ($compile, $httpParamSerializer, kbnUrl
|
|||
|
||||
if (truncate && text.length > MIN_LINE_LENGTH) {
|
||||
return truncateByHeightTemplate({
|
||||
body: text
|
||||
body: text,
|
||||
});
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
@ -38,13 +38,15 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<doc-viewer
|
||||
columns="columns"
|
||||
filter="filter"
|
||||
hit="row"
|
||||
index-pattern="indexPattern"
|
||||
on-add-column="onAddColumn"
|
||||
on-remove-column="onRemoveColumn"
|
||||
data-test-subj="docViewer"
|
||||
></doc-viewer>
|
||||
<div data-test-subj="docViewer">
|
||||
<doc-viewer
|
||||
columns="columns"
|
||||
filter="filter"
|
||||
hit="hit"
|
||||
index-pattern="indexPattern"
|
||||
on-add-column="onAddColumn"
|
||||
on-remove-column="onRemoveColumn"
|
||||
></doc-viewer>
|
||||
</div>
|
||||
|
||||
</td>
|
||||
|
|
|
@ -54,8 +54,8 @@
|
|||
</div>
|
||||
|
||||
<!-- result -->
|
||||
<div class="kuiViewContentItem" ng-if="status === 'found'">
|
||||
<doc-viewer hit="hit" index-pattern="indexPattern" data-test-subj="doc-hit"></doc-viewer>
|
||||
<div class="kuiViewContentItem" ng-if="status === 'found'" data-test-subj="doc-hit">
|
||||
<doc-viewer hit="hit" index-pattern="indexPattern"></doc-viewer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
56
src/legacy/core_plugins/kibana/public/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap
generated
Normal file
56
src/legacy/core_plugins/kibana/public/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,56 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render <DocViewer/> with 3 different tabs 1`] = `
|
||||
<div
|
||||
className="kbnDocViewer"
|
||||
>
|
||||
<EuiTabbedContent
|
||||
autoFocus="initial"
|
||||
tabs={
|
||||
Array [
|
||||
Object {
|
||||
"content": <DocViewerTab
|
||||
id={0}
|
||||
render={[MockFunction]}
|
||||
renderProps={
|
||||
Object {
|
||||
"hit": Object {},
|
||||
}
|
||||
}
|
||||
title="Render function"
|
||||
/>,
|
||||
"id": "Render function",
|
||||
"name": "Render function",
|
||||
},
|
||||
Object {
|
||||
"content": <DocViewerTab
|
||||
component={[Function]}
|
||||
id={1}
|
||||
renderProps={
|
||||
Object {
|
||||
"hit": Object {},
|
||||
}
|
||||
}
|
||||
title="React component"
|
||||
/>,
|
||||
"id": "React component",
|
||||
"name": "React component",
|
||||
},
|
||||
Object {
|
||||
"content": <DocViewerTab
|
||||
id={2}
|
||||
renderProps={
|
||||
Object {
|
||||
"hit": Object {},
|
||||
}
|
||||
}
|
||||
title="Invalid doc view"
|
||||
/>,
|
||||
"id": "Invalid doc view",
|
||||
"name": "Invalid doc view",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
`;
|
20
src/legacy/core_plugins/kibana/public/doc_viewer/__snapshots__/doc_viewer_render_tab.test.tsx.snap
generated
Normal file
20
src/legacy/core_plugins/kibana/public/doc_viewer/__snapshots__/doc_viewer_render_tab.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,20 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Mounting and unmounting DocViewerRenderTab 1`] = `
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
<div />,
|
||||
Object {
|
||||
"hit": Object {},
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": [MockFunction],
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
|
@ -1,93 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import expect from '@kbn/expect';
|
||||
import ngMock from 'ng_mock';
|
||||
import 'ui/private';
|
||||
|
||||
import { DocViewsRegistryProvider } from 'ui/registry/doc_views';
|
||||
import { uiRegistry } from 'ui/registry/_registry';
|
||||
|
||||
describe('docViewer', function () {
|
||||
let stubRegistry;
|
||||
let $elem;
|
||||
let init;
|
||||
|
||||
beforeEach(function () {
|
||||
ngMock.module('kibana', function (PrivateProvider) {
|
||||
stubRegistry = uiRegistry({
|
||||
index: ['name'],
|
||||
order: ['order'],
|
||||
constructor() {
|
||||
this.forEach(docView => {
|
||||
docView.shouldShow = docView.shouldShow || _.constant(true);
|
||||
docView.name = docView.name || docView.title;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
PrivateProvider.swap(DocViewsRegistryProvider, stubRegistry);
|
||||
});
|
||||
|
||||
// Create the scope
|
||||
ngMock.inject(function () {});
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
$elem = angular.element('<doc-viewer></doc-viewer>');
|
||||
init = function init() {
|
||||
ngMock.inject(function ($rootScope, $compile) {
|
||||
$compile($elem)($rootScope);
|
||||
$elem.scope().$digest();
|
||||
return $elem;
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
describe('injecting views', function () {
|
||||
|
||||
function registerExtension(def = {}) {
|
||||
stubRegistry.register(function () {
|
||||
return _.defaults(def, {
|
||||
title: 'exampleView',
|
||||
order: 0,
|
||||
directive: {
|
||||
template: `Example`
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
it('should have a tab for the view', function () {
|
||||
registerExtension();
|
||||
registerExtension({ title: 'exampleView2' });
|
||||
init();
|
||||
expect($elem.find('.euiTabs button').length).to.be(2);
|
||||
});
|
||||
|
||||
it('should activate the first view in order', function () {
|
||||
registerExtension({ order: 2 });
|
||||
registerExtension({ title: 'exampleView2' });
|
||||
init();
|
||||
expect($elem.find('.euiTabs .euiTab-isSelected').text().trim()).to.be('exampleView2');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,84 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import $ from 'jquery';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { DocViewsRegistryProvider } from 'ui/registry/doc_views';
|
||||
|
||||
import 'ui/directives/render_directive';
|
||||
|
||||
uiModules.get('apps/discover')
|
||||
.directive('docViewer', function (Private) {
|
||||
const docViews = Private(DocViewsRegistryProvider);
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
hit: '=',
|
||||
indexPattern: '=',
|
||||
filter: '=?',
|
||||
columns: '=?',
|
||||
onAddColumn: '=?',
|
||||
onRemoveColumn: '=?',
|
||||
},
|
||||
template: function ($el) {
|
||||
const $viewer = $('<div class="kbnDocViewer">');
|
||||
$el.append($viewer);
|
||||
const $tabs = $('<div class="euiTabs euiTabs--small" role="tablist">');
|
||||
const $content = $('<div class="kbnDocViewer__content" role="tabpanel">');
|
||||
$viewer.append($tabs);
|
||||
$viewer.append($content);
|
||||
docViews.inOrder.forEach(view => {
|
||||
const $tab = $(
|
||||
`<button
|
||||
ng-show="docViews['${view.name}'].shouldShow(hit)"
|
||||
class="euiTab"
|
||||
ng-class="{'euiTab euiTab-isSelected': mode === '${view.name}', 'euiTab': true}"
|
||||
role="tab"
|
||||
aria-selected="{{mode === '${view.name}'}}"
|
||||
ng-click="mode='${view.name}'"
|
||||
>
|
||||
<span
|
||||
class="euiTab__content"
|
||||
>
|
||||
${view.title}
|
||||
</span>
|
||||
</button>`
|
||||
);
|
||||
$tabs.append($tab);
|
||||
const $viewAttrs = `
|
||||
hit="hit"
|
||||
index-pattern="indexPattern"
|
||||
filter="filter"
|
||||
columns="columns"
|
||||
on-add-column="onAddColumn"
|
||||
on-remove-column="onRemoveColumn"
|
||||
`;
|
||||
const $ext = $(`<render-directive ${$viewAttrs} ng-if="mode == '${view.name}'" definition="docViews['${view.name}'].directive">
|
||||
</render-directive>`);
|
||||
$ext.html(view.directive.template);
|
||||
$content.append($ext);
|
||||
});
|
||||
return $el.html();
|
||||
},
|
||||
controller: function ($scope) {
|
||||
$scope.mode = docViews.inOrder[0].name;
|
||||
$scope.docViews = docViews.byName;
|
||||
}
|
||||
};
|
||||
});
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import { DocViewer } from './doc_viewer';
|
||||
// @ts-ignore
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import { addDocView, emptyDocViews, DocViewRenderProps } from 'ui/registry/doc_views';
|
||||
|
||||
beforeEach(() => {
|
||||
emptyDocViews();
|
||||
});
|
||||
|
||||
test('Render <DocViewer/> with 3 different tabs', () => {
|
||||
addDocView({ order: 20, title: 'React component', component: () => <div>test</div> });
|
||||
addDocView({ order: 10, title: 'Render function', render: jest.fn() });
|
||||
addDocView({ order: 30, title: 'Invalid doc view' });
|
||||
|
||||
const renderProps = { hit: {} } as DocViewRenderProps;
|
||||
|
||||
const wrapper = shallow(<DocViewer {...renderProps} />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Render <DocViewer/> with 1 tab displaying error message', () => {
|
||||
function SomeComponent() {
|
||||
// this is just a placeholder
|
||||
return null;
|
||||
}
|
||||
|
||||
addDocView({
|
||||
order: 10,
|
||||
title: 'React component',
|
||||
component: SomeComponent,
|
||||
});
|
||||
|
||||
const renderProps = { hit: {} } as DocViewRenderProps;
|
||||
const errorMsg = 'Catch me if you can!';
|
||||
|
||||
const wrapper = mount(<DocViewer {...renderProps} />);
|
||||
const error = new Error(errorMsg);
|
||||
wrapper.find(SomeComponent).simulateError(error);
|
||||
const errorMsgComponent = findTestSubject(wrapper, 'docViewerError');
|
||||
expect(errorMsgComponent.text()).toMatch(new RegExp(`${errorMsg}`));
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EuiTabbedContent } from '@elastic/eui';
|
||||
import { getDocViewsSorted, DocViewRenderProps } from 'ui/registry/doc_views';
|
||||
import { DocViewerTab } from './doc_viewer_tab';
|
||||
|
||||
/**
|
||||
* Rendering tabs with different views of 1 Elasticsearch hit in Discover.
|
||||
* The tabs are provided by the `docs_views` registry.
|
||||
* A view can contain a React `component`, or any JS framework by using
|
||||
* a `render` function.
|
||||
*/
|
||||
export function DocViewer(renderProps: DocViewRenderProps) {
|
||||
const tabs = getDocViewsSorted(renderProps.hit).map(({ title, render, component }, idx) => {
|
||||
return {
|
||||
id: title,
|
||||
name: title,
|
||||
content: (
|
||||
<DocViewerTab
|
||||
id={idx}
|
||||
title={title}
|
||||
component={component}
|
||||
renderProps={renderProps}
|
||||
render={render}
|
||||
/>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="kbnDocViewer">
|
||||
<EuiTabbedContent tabs={tabs} />
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -16,28 +16,21 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
// @ts-ignore
|
||||
import { DocViewsRegistryProvider } from 'ui/registry/doc_views';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { JsonCodeEditor } from './json_code_editor';
|
||||
|
||||
/*
|
||||
* Registration of the the doc view: json
|
||||
* - used to display an ES hit as pretty printed JSON at Discover
|
||||
* - registered as angular directive to stay compatible with community plugins
|
||||
*/
|
||||
DocViewsRegistryProvider.register(function(reactDirective: any) {
|
||||
const reactDir = reactDirective(JsonCodeEditor, ['hit']);
|
||||
// setting of reactDir.scope is required to assign $scope props
|
||||
// to the react component via render-directive in doc_viewer.js
|
||||
reactDir.scope = {
|
||||
hit: '=',
|
||||
};
|
||||
return {
|
||||
title: i18n.translate('kbnDocViews.json.jsonTitle', {
|
||||
defaultMessage: 'JSON',
|
||||
}),
|
||||
order: 20,
|
||||
directive: reactDir,
|
||||
};
|
||||
// @ts-ignore
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { DocViewer } from './doc_viewer';
|
||||
|
||||
uiModules.get('apps/discover').directive('docViewer', (reactDirective: any) => {
|
||||
return reactDirective(DocViewer, undefined, {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
hit: '=',
|
||||
indexPattern: '=',
|
||||
filter: '=?',
|
||||
columns: '=?',
|
||||
onAddColumn: '=?',
|
||||
onRemoveColumn: '=?',
|
||||
},
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EuiCallOut, EuiCodeBlock } from '@elastic/eui';
|
||||
// @ts-ignore
|
||||
import { formatMsg, formatStack } from '../../../../ui/public/notify/lib';
|
||||
|
||||
interface Props {
|
||||
error: Error | string | null;
|
||||
}
|
||||
|
||||
export function DocViewerError({ error }: Props) {
|
||||
const errMsg = formatMsg(error);
|
||||
const errStack = error ? formatStack(error) : '';
|
||||
|
||||
return (
|
||||
<EuiCallOut title={errMsg} color="danger" iconType="cross" data-test-subj="docViewerError">
|
||||
{errStack && <EuiCodeBlock>{errStack}</EuiCodeBlock>}
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { DocViewRenderTab } from './doc_viewer_render_tab';
|
||||
import { DocViewRenderProps } from 'ui/registry/doc_views';
|
||||
|
||||
test('Mounting and unmounting DocViewerRenderTab', () => {
|
||||
const unmountFn = jest.fn();
|
||||
const renderFn = jest.fn(() => unmountFn);
|
||||
const renderProps = {
|
||||
hit: {},
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<DocViewRenderTab render={renderFn} renderProps={renderProps as DocViewRenderProps} />
|
||||
);
|
||||
|
||||
expect(renderFn).toMatchSnapshot();
|
||||
|
||||
wrapper.unmount();
|
||||
|
||||
expect(unmountFn).toBeCalled();
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { DocViewRenderFn, DocViewRenderProps } from 'ui/registry/doc_views';
|
||||
|
||||
interface Props {
|
||||
render: DocViewRenderFn;
|
||||
renderProps: DocViewRenderProps;
|
||||
}
|
||||
/**
|
||||
* Responsible for rendering a tab provided by a render function.
|
||||
* So any other framework can be used (E.g. legacy Angular 3rd party plugin code)
|
||||
* The provided `render` function is called with a reference to the
|
||||
* component's `HTMLDivElement` as 1st arg and `renderProps` as 2nd arg
|
||||
*/
|
||||
export function DocViewRenderTab({ render, renderProps }: Props) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (ref && ref.current) {
|
||||
return render(ref.current, renderProps);
|
||||
}
|
||||
}, [render, renderProps]);
|
||||
return <div ref={ref} />;
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { DocViewRenderProps, DocViewRenderFn } from 'ui/registry/doc_views';
|
||||
import { DocViewRenderTab } from './doc_viewer_render_tab';
|
||||
import { DocViewerError } from './doc_viewer_render_error';
|
||||
|
||||
interface Props {
|
||||
component?: React.ComponentType<DocViewRenderProps>;
|
||||
id: number;
|
||||
render?: DocViewRenderFn;
|
||||
renderProps: DocViewRenderProps;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
error: null | Error | string;
|
||||
hasError: boolean;
|
||||
}
|
||||
/**
|
||||
* Renders the tab content of a doc view.
|
||||
* Displays an error message when it encounters exceptions, thanks to
|
||||
* Error Boundaries.
|
||||
*/
|
||||
export class DocViewerTab extends React.Component<Props, State> {
|
||||
state = {
|
||||
hasError: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
static getDerivedStateFromError(error: unknown) {
|
||||
// Update state so the next render will show the fallback UI.
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: Props, nextState: State) {
|
||||
return (
|
||||
nextProps.renderProps.hit._id !== this.props.renderProps.hit._id ||
|
||||
nextProps.id !== this.props.id ||
|
||||
nextState.hasError
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { component, render, renderProps, title } = this.props;
|
||||
const { hasError, error } = this.state;
|
||||
|
||||
if (hasError && error) {
|
||||
return <DocViewerError error={error} />;
|
||||
} else if (!render && !component) {
|
||||
return (
|
||||
<DocViewerError
|
||||
error={`Invalid plugin ${title}, there is neither a (react) component nor a render function provided`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (render) {
|
||||
// doc view is provided by a render function, e.g. for legacy Angular code
|
||||
return <DocViewRenderTab render={render} renderProps={renderProps} />;
|
||||
}
|
||||
|
||||
// doc view is provided by a react component
|
||||
const Component = component as any;
|
||||
return <Component {...renderProps} />;
|
||||
}
|
||||
}
|
|
@ -17,4 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import './doc_viewer';
|
||||
import './doc_viewer_directive';
|
||||
|
|
6
src/legacy/ui/public/chrome/index.d.ts
vendored
6
src/legacy/ui/public/chrome/index.d.ts
vendored
|
@ -26,6 +26,12 @@ import { ChromeNavLinks } from './api/nav';
|
|||
|
||||
export interface IInjector {
|
||||
get<T>(injectable: string): T;
|
||||
invoke<T, T2>(
|
||||
injectable: (this: T2, ...args: any[]) => T,
|
||||
self?: T2,
|
||||
locals?: { [key: string]: any }
|
||||
): T;
|
||||
instantiate(constructor: Function, locals?: { [key: string]: any }): any;
|
||||
}
|
||||
|
||||
declare interface Chrome extends ChromeNavLinks {
|
||||
|
|
63
src/legacy/ui/public/registry/doc_views.ts
Normal file
63
src/legacy/ui/public/registry/doc_views.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { convertDirectiveToRenderFn } from './doc_views_helpers';
|
||||
import { DocView, DocViewInput, ElasticSearchHit, DocViewInputFn } from './doc_views_types';
|
||||
|
||||
export { DocViewRenderProps, DocView, DocViewRenderFn } from './doc_views_types';
|
||||
|
||||
export const docViews: DocView[] = [];
|
||||
|
||||
/**
|
||||
* Extends and adds the given doc view to the registry array
|
||||
*/
|
||||
export function addDocView(docView: DocViewInput) {
|
||||
if (docView.directive) {
|
||||
// convert angular directive to render function for backwards compatibility
|
||||
docView.render = convertDirectiveToRenderFn(docView.directive);
|
||||
}
|
||||
if (typeof docView.shouldShow !== 'function') {
|
||||
docView.shouldShow = () => true;
|
||||
}
|
||||
docViews.push(docView as DocView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty array of doc views for testing
|
||||
*/
|
||||
export function emptyDocViews() {
|
||||
docViews.length = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a sorted array of doc_views for rendering tabs
|
||||
*/
|
||||
export function getDocViewsSorted(hit: ElasticSearchHit): DocView[] {
|
||||
return docViews
|
||||
.filter(docView => docView.shouldShow(hit))
|
||||
.sort((a, b) => (Number(a.order) > Number(b.order) ? 1 : -1));
|
||||
}
|
||||
/**
|
||||
* Provider for compatibility with 3rd Party plugins
|
||||
*/
|
||||
export const DocViewsRegistryProvider = {
|
||||
register: (docViewRaw: DocViewInput | DocViewInputFn) => {
|
||||
const docView = typeof docViewRaw === 'function' ? docViewRaw() : docViewRaw;
|
||||
addDocView(docView);
|
||||
},
|
||||
};
|
94
src/legacy/ui/public/registry/doc_views_helpers.tsx
Normal file
94
src/legacy/ui/public/registry/doc_views_helpers.tsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import angular, { ICompileService } from 'angular';
|
||||
import chrome from 'ui/chrome';
|
||||
import {
|
||||
DocViewRenderProps,
|
||||
AngularScope,
|
||||
AngularController,
|
||||
AngularDirective,
|
||||
} from './doc_views_types';
|
||||
import { DocViewerError } from '../../../core_plugins/kibana/public/doc_viewer/doc_viewer_render_error';
|
||||
|
||||
/**
|
||||
* Compiles and injects the give angular template into the given dom node
|
||||
* returns a function to cleanup the injected angular element
|
||||
*/
|
||||
export async function injectAngularElement(
|
||||
domNode: Element,
|
||||
template: string,
|
||||
scopeProps: DocViewRenderProps,
|
||||
Controller: AngularController
|
||||
): Promise<() => void> {
|
||||
const $injector = await chrome.dangerouslyGetActiveInjector();
|
||||
const rootScope: AngularScope = $injector.get('$rootScope');
|
||||
const $compile: ICompileService = $injector.get('$compile');
|
||||
const newScope = Object.assign(rootScope.$new(), scopeProps);
|
||||
|
||||
if (typeof Controller === 'function') {
|
||||
// when a controller is defined, expose the value it produces to the view as `$ctrl`
|
||||
// see: https://docs.angularjs.org/api/ng/provider/$compileProvider#component
|
||||
(newScope as any).$ctrl = $injector.instantiate(Controller, {
|
||||
$scope: newScope,
|
||||
});
|
||||
}
|
||||
|
||||
const $target = angular.element(domNode);
|
||||
const $element = angular.element(template);
|
||||
|
||||
newScope.$apply(() => {
|
||||
const linkFn = $compile($element);
|
||||
$target.empty().append($element);
|
||||
linkFn(newScope);
|
||||
});
|
||||
|
||||
return () => {
|
||||
newScope.$destroy();
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Converts a given legacy angular directive to a render function
|
||||
* for usage in a react component. Note that the rendering is async
|
||||
*/
|
||||
export function convertDirectiveToRenderFn(directive: AngularDirective) {
|
||||
return (domNode: Element, props: DocViewRenderProps) => {
|
||||
let rejected = false;
|
||||
|
||||
const cleanupFnPromise = injectAngularElement(
|
||||
domNode,
|
||||
directive.template,
|
||||
props,
|
||||
directive.controller
|
||||
);
|
||||
cleanupFnPromise.catch(e => {
|
||||
rejected = true;
|
||||
render(<DocViewerError error={e} />, domNode);
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (!rejected) {
|
||||
// for cleanup
|
||||
// http://roubenmeschian.com/rubo/?p=51
|
||||
cleanupFnPromise.then(cleanup => cleanup());
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
60
src/legacy/ui/public/registry/doc_views_types.ts
Normal file
60
src/legacy/ui/public/registry/doc_views_types.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { IndexPattern } from 'src/legacy/core_plugins/data/public';
|
||||
import { ComponentType } from 'react';
|
||||
import { IScope } from 'angular';
|
||||
|
||||
export interface AngularDirective {
|
||||
controller: (scope: AngularScope) => void;
|
||||
template: string;
|
||||
}
|
||||
|
||||
export type AngularScope = IScope;
|
||||
|
||||
export type AngularController = (scope: AngularScope) => void;
|
||||
|
||||
export type ElasticSearchHit = Record<string, string | number | Record<string, unknown>>;
|
||||
|
||||
export interface DocViewRenderProps {
|
||||
columns: string[];
|
||||
filter: (field: string, value: string | number, operation: string) => void;
|
||||
hit: ElasticSearchHit;
|
||||
indexPattern: IndexPattern;
|
||||
onAddColumn: (columnName: string) => void;
|
||||
onRemoveColumn: (columnName: string) => void;
|
||||
}
|
||||
export type DocViewRenderFn = (
|
||||
domeNode: HTMLDivElement,
|
||||
renderProps: DocViewRenderProps
|
||||
) => () => void;
|
||||
|
||||
export interface DocViewInput {
|
||||
component?: ComponentType<DocViewRenderProps>;
|
||||
directive?: AngularDirective;
|
||||
order: number;
|
||||
render?: DocViewRenderFn;
|
||||
shouldShow?: (hit: ElasticSearchHit) => boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface DocView extends DocViewInput {
|
||||
shouldShow: (hit: ElasticSearchHit) => boolean;
|
||||
}
|
||||
|
||||
export type DocViewInputFn = () => DocViewInput;
|
|
@ -43,18 +43,18 @@ export default function ({ getService, getPageObjects }) {
|
|||
it('should be addable via expanded doc table rows', async function () {
|
||||
await docTable.toggleRowExpanded({ isAnchorRow: true });
|
||||
|
||||
const anchorDetailsRow = await docTable.getAnchorDetailsRow();
|
||||
await docTable.addInclusiveFilter(anchorDetailsRow, TEST_ANCHOR_FILTER_FIELD);
|
||||
await PageObjects.context.waitUntilContextLoadingHasFinished();
|
||||
|
||||
await docTable.toggleRowExpanded({ isAnchorRow: true });
|
||||
|
||||
await retry.try(async () => {
|
||||
expect(await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, TEST_ANCHOR_FILTER_VALUE, true)).to.be(true);
|
||||
const anchorDetailsRow = await docTable.getAnchorDetailsRow();
|
||||
await docTable.addInclusiveFilter(anchorDetailsRow, TEST_ANCHOR_FILTER_FIELD);
|
||||
await PageObjects.context.waitUntilContextLoadingHasFinished();
|
||||
// await docTable.toggleRowExpanded({ isAnchorRow: true });
|
||||
expect(
|
||||
await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, TEST_ANCHOR_FILTER_VALUE, true)
|
||||
).to.be(true);
|
||||
const fields = await docTable.getFields();
|
||||
const hasOnlyFilteredRows = fields
|
||||
.map(row => row[2])
|
||||
.every((fieldContent) => fieldContent === TEST_ANCHOR_FILTER_VALUE);
|
||||
.every(fieldContent => fieldContent === TEST_ANCHOR_FILTER_VALUE);
|
||||
expect(hasOnlyFilteredRows).to.be(true);
|
||||
});
|
||||
});
|
||||
|
@ -67,11 +67,13 @@ export default function ({ getService, getPageObjects }) {
|
|||
await PageObjects.context.waitUntilContextLoadingHasFinished();
|
||||
|
||||
retry.try(async () => {
|
||||
expect(await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, TEST_ANCHOR_FILTER_VALUE, false)).to.be(true);
|
||||
expect(
|
||||
await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, TEST_ANCHOR_FILTER_VALUE, false)
|
||||
).to.be(true);
|
||||
const fields = await docTable.getFields();
|
||||
const hasOnlyFilteredRows = fields
|
||||
.map(row => row[2])
|
||||
.every((fieldContent) => fieldContent === TEST_ANCHOR_FILTER_VALUE);
|
||||
.every(fieldContent => fieldContent === TEST_ANCHOR_FILTER_VALUE);
|
||||
expect(hasOnlyFilteredRows).to.be(false);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue