diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/format_hit.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/format_hit.ts index 28797c28db834..39101ef36ca8d 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/format_hit.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/format_hit.ts @@ -26,17 +26,28 @@ const partialFormattedCache = new WeakMap(); // Takes a hit, merges it with any stored/scripted fields, and with the metaFields // returns a formatted version export function formatHitProvider(indexPattern: IndexPattern, defaultFormat: any) { - function convert(hit: Record, val: any, fieldName: string) { + function convert(hit: Record, val: any, fieldName: string, type: string = 'html') { const field = indexPattern.fields.byName[fieldName]; - if (!field) return defaultFormat.convert(val, 'html'); + if (!field) return defaultFormat.convert(val, type); const parsedUrl = { origin: window.location.origin, pathname: window.location.pathname, }; - return field.format.getConverterFor('html')(val, field, hit, parsedUrl); + return field.format.getConverterFor(type)(val, field, hit, parsedUrl); } - function formatHit(hit: Record) { + function formatHit(hit: Record, type: string = 'html') { + if (type === 'text') { + // formatHit of type text is for react components to get rid of + // since it's currently just used at the discover's doc view table, caching is not necessary + const flattened = indexPattern.flattenHit(hit); + const result: Record = {}; + for (const [key, value] of Object.entries(flattened)) { + result[key] = convert(hit, value, key, type); + } + return result; + } + const cached = formattedCache.get(hit); if (cached) { return cached; diff --git a/src/legacy/core_plugins/kbn_doc_views/public/views/table.html b/src/legacy/core_plugins/kbn_doc_views/public/views/table.html deleted file mode 100644 index 48b66b233b3cf..0000000000000 --- a/src/legacy/core_plugins/kbn_doc_views/public/views/table.html +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
-
diff --git a/src/legacy/core_plugins/kbn_doc_views/public/views/table.js b/src/legacy/core_plugins/kbn_doc_views/public/views/table.js deleted file mode 100644 index c467a9a3eba81..0000000000000 --- a/src/legacy/core_plugins/kbn_doc_views/public/views/table.js +++ /dev/null @@ -1,77 +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 'lodash'; -import { addDocView } from 'ui/registry/doc_views'; -import '../filters/trust_as_html'; -import tableHtml from './table.html'; -import { i18n } from '@kbn/i18n'; - -const MIN_LINE_LENGTH = 350; - -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.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.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.toggleViewer = function (field) { - $scope.fieldRowOpen[field] = !$scope.fieldRowOpen[field]; - }; - }, - }, -}); diff --git a/src/legacy/core_plugins/kbn_doc_views/public/filters/trust_as_html.js b/src/legacy/core_plugins/kbn_doc_views/public/views/table.ts similarity index 72% rename from src/legacy/core_plugins/kbn_doc_views/public/filters/trust_as_html.js rename to src/legacy/core_plugins/kbn_doc_views/public/views/table.ts index ea952ca9f6874..f90840f195003 100644 --- a/src/legacy/core_plugins/kbn_doc_views/public/filters/trust_as_html.js +++ b/src/legacy/core_plugins/kbn_doc_views/public/views/table.ts @@ -17,11 +17,15 @@ * under the License. */ -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/doc_views'); +import _ from 'lodash'; +import { addDocView } from 'ui/registry/doc_views'; +import { i18n } from '@kbn/i18n'; +import { DocViewTable } from './table/table'; -// Simple filter to allow using ng-bind-html without explicitly calling $sce.trustAsHtml in a controller -// (See http://goo.gl/mpj9o2) -module.filter('trustAsHtml', function ($sce) { - return $sce.trustAsHtml; +addDocView({ + title: i18n.translate('kbnDocViews.table.tableTitle', { + defaultMessage: 'Table', + }), + order: 10, + component: DocViewTable, }); diff --git a/src/legacy/core_plugins/kbn_doc_views/public/views/table/table.test.tsx b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table.test.tsx new file mode 100644 index 0000000000000..03437e7b1ac26 --- /dev/null +++ b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table.test.tsx @@ -0,0 +1,260 @@ +/* + * 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 { IndexPattern } from 'ui/index_patterns'; +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; +// @ts-ignore +import { flattenHitWrapper } from '../../../../data/public/index_patterns/index_patterns/flatten_hit'; +import { DocViewTable } from './table'; + +// @ts-ignore +const indexPattern = { + fields: { + byName: { + _index: { + name: '_index', + type: 'string', + scripted: false, + filterable: true, + }, + message: { + name: 'message', + type: 'string', + scripted: false, + filterable: false, + }, + extension: { + name: 'extension', + type: 'string', + scripted: false, + filterable: true, + }, + bytes: { + name: 'bytes', + type: 'number', + scripted: false, + filterable: true, + }, + scripted: { + name: 'scripted', + type: 'number', + scripted: true, + filterable: false, + }, + }, + }, + metaFields: ['_index', '_score'], + flattenHit: undefined, + formatHit: jest.fn(hit => hit), +} as IndexPattern; + +indexPattern.flattenHit = flattenHitWrapper(indexPattern, indexPattern.metaFields); + +describe('DocViewTable at Discover', () => { + // At Discover's main view, all buttons are rendered + // check for existence of action buttons and warnings + + const hit = { + _index: 'logstash-2014.09.09', + _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. \ + Phasellus ullamcorper ipsum rutrum nunc. Nunc nonummy metus. Vestibulum volutpat pretium libero. Cras id dui. Aenean ut', + extension: 'html', + not_mapped: 'yes', + bytes: 100, + objectArray: [{ foo: true }], + relatedContent: { + test: 1, + }, + scripted: 123, + _underscore: 123, + }, + }; + + const props = { + hit, + columns: ['extension'], + indexPattern, + filter: jest.fn(), + onAddColumn: jest.fn(), + onRemoveColumn: jest.fn(), + }; + const component = mount(); + [ + { + _property: '_index', + addInclusiveFilterButton: true, + collapseBtn: false, + noMappingWarning: false, + toggleColumnButton: true, + underscoreWarning: false, + }, + { + _property: 'message', + addInclusiveFilterButton: false, + collapseBtn: true, + noMappingWarning: false, + toggleColumnButton: true, + underscoreWarning: false, + }, + { + _property: '_underscore', + addInclusiveFilterButton: false, + collapseBtn: false, + noMappingWarning: false, + toggleColumnButton: true, + underScoreWarning: true, + }, + { + _property: 'scripted', + addInclusiveFilterButton: false, + collapseBtn: false, + noMappingWarning: false, + toggleColumnButton: true, + underScoreWarning: false, + }, + { + _property: 'not_mapped', + addInclusiveFilterButton: false, + collapseBtn: false, + noMappingWarning: true, + toggleColumnButton: true, + underScoreWarning: false, + }, + ].forEach(check => { + const rowComponent = findTestSubject(component, `tableDocViewRow-${check._property}`); + + it(`renders row for ${check._property}`, () => { + expect(rowComponent.length).toBe(1); + }); + + ([ + 'addInclusiveFilterButton', + 'collapseBtn', + 'toggleColumnButton', + 'underscoreWarning', + ] as const).forEach(element => { + const elementExist = check[element]; + + if (typeof elementExist === 'boolean') { + const btn = findTestSubject(rowComponent, element); + + it(`renders ${element} for '${check._property}' correctly`, () => { + const disabled = btn.length ? btn.props().disabled : true; + const clickAble = btn.length && !disabled ? true : false; + expect(clickAble).toBe(elementExist); + }); + } + }); + + (['noMappingWarning'] as const).forEach(element => { + const elementExist = check[element]; + + if (typeof elementExist === 'boolean') { + const el = findTestSubject(rowComponent, element); + + it(`renders ${element} for '${check._property}' correctly`, () => { + expect(el.length).toBe(elementExist ? 1 : 0); + }); + } + }); + }); +}); + +describe('DocViewTable at Discover Doc', () => { + const hit = { + _index: 'logstash-2014.09.09', + _score: 1, + _source: { + extension: 'html', + not_mapped: 'yes', + }, + }; + // here no action buttons are rendered + const props = { + hit, + indexPattern, + }; + const component = mount(); + const foundLength = findTestSubject(component, 'addInclusiveFilterButton').length; + + it(`renders no action buttons`, () => { + expect(foundLength).toBe(0); + }); +}); + +describe('DocViewTable at Discover Context', () => { + // here no toggleColumnButtons are rendered + const hit = { + _index: 'logstash-2014.09.09', + _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. \ + Phasellus ullamcorper ipsum rutrum nunc. Nunc nonummy metus. Vestibulum volutpat pretium libero. Cras id dui. Aenean ut', + }, + }; + const props = { + hit, + columns: ['extension'], + indexPattern, + filter: jest.fn(), + }; + + const component = mount(); + + it(`renders no toggleColumnButton`, () => { + const foundLength = findTestSubject(component, 'toggleColumnButtons').length; + expect(foundLength).toBe(0); + }); + + it(`renders addInclusiveFilterButton`, () => { + const row = findTestSubject(component, `tableDocViewRow-_index`); + const btn = findTestSubject(row, 'addInclusiveFilterButton'); + expect(btn.length).toBe(1); + btn.simulate('click'); + expect(props.filter).toBeCalled(); + }); + + it(`renders functional collapse button`, () => { + const btn = findTestSubject(component, `collapseBtn`); + const html = component.html(); + + expect(component.html()).toContain('truncate-by-height'); + + expect(btn.length).toBe(1); + btn.simulate('click'); + expect(component.html() !== html).toBeTruthy(); + }); +}); diff --git a/src/legacy/core_plugins/kbn_doc_views/public/views/table/table.tsx b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table.tsx new file mode 100644 index 0000000000000..ff784f6159870 --- /dev/null +++ b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table.tsx @@ -0,0 +1,91 @@ +/* + * 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, { useState } from 'react'; +import { DocViewRenderProps } from 'ui/registry/doc_views'; +import { DocViewTableRow } from './table_row'; +import { formatValue, arrayContainsObjects } from './table_helper'; + +const COLLAPSE_LINE_LENGTH = 350; + +export function DocViewTable({ + hit, + indexPattern, + filter, + columns, + onAddColumn, + onRemoveColumn, +}: DocViewRenderProps) { + const mapping = indexPattern.fields.byName; + const flattened = indexPattern.flattenHit(hit); + const formatted = indexPattern.formatHit(hit, 'html'); + const [fieldRowOpen, setFieldRowOpen] = useState({} as Record); + + function toggleValueCollapse(field: string) { + fieldRowOpen[field] = fieldRowOpen[field] !== true; + setFieldRowOpen({ ...fieldRowOpen }); + } + + return ( + + + {Object.keys(flattened) + .sort() + .map(field => { + const valueRaw = flattened[field]; + const value = formatValue(valueRaw, formatted[field]); + const isCollapsible = typeof value === 'string' && value.length > COLLAPSE_LINE_LENGTH; + const isCollapsed = isCollapsible && !fieldRowOpen[field]; + const toggleColumn = + onRemoveColumn && onAddColumn && Array.isArray(columns) + ? () => { + if (columns.includes(field)) { + onRemoveColumn(field); + } else { + onAddColumn(field); + } + } + : undefined; + const isArrayOfObjects = + Array.isArray(flattened[field]) && arrayContainsObjects(flattened[field]); + const displayUnderscoreWarning = !mapping[field] && field.indexOf('_') === 0; + const displayNoMappingWarning = + !mapping[field] && !displayUnderscoreWarning && !isArrayOfObjects; + + return ( + toggleValueCollapse(field)} + onToggleColumn={toggleColumn} + value={value} + valueRaw={valueRaw} + /> + ); + })} + +
+ ); +} diff --git a/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_helper.test.ts b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_helper.test.ts new file mode 100644 index 0000000000000..f075e06c7651f --- /dev/null +++ b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_helper.test.ts @@ -0,0 +1,177 @@ +/* + * 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 { + replaceMarkWithReactDom, + convertAngularHtml, + arrayContainsObjects, + formatValue, +} from './table_helper'; + +describe('replaceMarkWithReactDom', () => { + it(`converts test to react nodes`, () => { + const actual = replaceMarkWithReactDom( + 'marked1 blablabla marked2 end' + ); + expect(actual).toMatchInlineSnapshot(` + + + + + marked1 + + blablabla + + + + marked2 + + end + + + `); + }); + + it(`doesn't convert invalid markup to react dom nodes`, () => { + const actual = replaceMarkWithReactDom('test sdf sdf'); + expect(actual).toMatchInlineSnapshot(` + + + test sdf + + + sdf + + + + + `); + }); + + it(`returns strings without markup unchanged `, () => { + const actual = replaceMarkWithReactDom('blablabla'); + expect(actual).toMatchInlineSnapshot(` + + blablabla + + `); + }); +}); + +describe('convertAngularHtml', () => { + it(`converts html for usage in angular to usage in react`, () => { + const actual = convertAngularHtml('Good morning!'); + expect(actual).toMatchInlineSnapshot(`"Good morning!"`); + }); + it(`converts html containing for usage in react`, () => { + const actual = convertAngularHtml( + 'Good morningdear reviewer!' + ); + expect(actual).toMatchInlineSnapshot(` + + Good + + + morning + + dear + + + + reviewer + + ! + + + `); + }); +}); + +describe('arrayContainsObjects', () => { + it(`returns false for an array of primitives`, () => { + const actual = arrayContainsObjects(['test', 'test']); + expect(actual).toBeFalsy(); + }); + + it(`returns true for an array of objects`, () => { + const actual = arrayContainsObjects([{}, {}]); + expect(actual).toBeTruthy(); + }); + + it(`returns true for an array of objects and primitves`, () => { + const actual = arrayContainsObjects([{}, 'sdf']); + expect(actual).toBeTruthy(); + }); + + it(`returns false for an array of null values`, () => { + const actual = arrayContainsObjects([null, null]); + expect(actual).toBeFalsy(); + }); + + it(`returns false if no array is given`, () => { + const actual = arrayContainsObjects([null, null]); + expect(actual).toBeFalsy(); + }); +}); + +describe('formatValue', () => { + it(`formats an array of objects`, () => { + const actual = formatValue([{ test: '123' }, ''], ''); + expect(actual).toMatchInlineSnapshot(` + "{ + \\"test\\": \\"123\\" + } + \\"\\"" + `); + }); + it(`formats an array of primitives`, () => { + const actual = formatValue(['test1', 'test2'], ''); + expect(actual).toMatchInlineSnapshot(`"test1, test2"`); + }); + it(`formats an object`, () => { + const actual = formatValue({ test: 1 }, ''); + expect(actual).toMatchInlineSnapshot(` + "{ + \\"test\\": 1 + }" + `); + }); + it(`formats an angular formatted string `, () => { + const actual = formatValue( + '', + 'Good morningdear reviewer!' + ); + expect(actual).toMatchInlineSnapshot(` + + Good + + + morning + + dear + + + + reviewer + + ! + + + `); + }); +}); diff --git a/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_helper.tsx b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_helper.tsx new file mode 100644 index 0000000000000..e959ec336bf3a --- /dev/null +++ b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_helper.tsx @@ -0,0 +1,85 @@ +/* + * 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 { unescape } from 'lodash'; + +/** + * Convert markup of the given string to ReactNodes + * @param text + */ +export function replaceMarkWithReactDom(text: string): React.ReactNode { + return ( + <> + {text.split('').map((markedText, idx) => { + const sub = markedText.split(''); + if (sub.length === 1) { + return markedText; + } + return ( + + {sub[0]} + {sub[1]} + + ); + })} + + ); +} + +/** + * Current html of the formatter is angular flavored, this current workaround + * should be removed when all consumers of the formatHit function are react based + */ +export function convertAngularHtml(html: string): string | React.ReactNode { + if (typeof html === 'string') { + const cleaned = html.replace('', '').replace('', ''); + const unescaped = unescape(cleaned); + if (unescaped.indexOf('') !== -1) { + return replaceMarkWithReactDom(unescaped); + } + return unescaped; + } + return html; +} +/** + * Returns true if the given array contains at least 1 object + */ +export function arrayContainsObjects(value: unknown[]) { + return Array.isArray(value) && value.some(v => typeof v === 'object' && v !== null); +} + +/** + * The current field formatter provides html for angular usage + * This html is cleaned up and prepared for usage in the react world + * Furthermore test are converted to ReactNodes + */ +export function formatValue( + value: null | string | number | boolean | object | Array, + valueFormatted: string +): string | React.ReactNode { + if (Array.isArray(value) && arrayContainsObjects(value)) { + return value.map(v => JSON.stringify(v, null, 2)).join('\n'); + } else if (Array.isArray(value)) { + return value.join(', '); + } else if (typeof value === 'object' && value !== null) { + return JSON.stringify(value, null, 2); + } else { + return typeof valueFormatted === 'string' ? convertAngularHtml(valueFormatted) : String(value); + } +} diff --git a/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row.tsx b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row.tsx new file mode 100644 index 0000000000000..2059e35b2c42e --- /dev/null +++ b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row.tsx @@ -0,0 +1,102 @@ +/* + * 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, { ReactNode } from 'react'; +import { FieldName } from 'ui/directives/field_name/field_name'; +import { FieldMapping, DocViewFilterFn } from 'ui/registry/doc_views_types'; +import classNames from 'classnames'; +import { DocViewTableRowBtnFilterAdd } from './table_row_btn_filter_add'; +import { DocViewTableRowBtnFilterRemove } from './table_row_btn_filter_remove'; +import { DocViewTableRowBtnToggleColumn } from './table_row_btn_toggle_column'; +import { DocViewTableRowBtnCollapse } from './table_row_btn_collapse'; +import { DocViewTableRowBtnFilterExists } from './table_row_btn_filter_exists'; +import { DocViewTableRowIconNoMapping } from './table_row_icon_no_mapping'; +import { DocViewTableRowIconUnderscore } from './table_row_icon_underscore'; + +export interface Props { + field: string; + fieldMapping?: FieldMapping; + displayNoMappingWarning: boolean; + displayUnderscoreWarning: boolean; + isCollapsible: boolean; + isColumnActive: boolean; + isCollapsed: boolean; + onToggleCollapse: () => void; + onFilter?: DocViewFilterFn; + onToggleColumn?: () => void; + value: string | ReactNode; + valueRaw: unknown; +} + +export function DocViewTableRow({ + field, + fieldMapping, + displayNoMappingWarning, + displayUnderscoreWarning, + isCollapsible, + isCollapsed, + isColumnActive, + onFilter, + onToggleCollapse, + onToggleColumn, + value, + valueRaw, +}: Props) { + const valueClassName = classNames({ + kbnDocViewer__value: true, + 'truncate-by-height': isCollapsible && isCollapsed, + }); + + return ( + + {typeof onFilter === 'function' && ( + + onFilter(fieldMapping, valueRaw, '+')} + /> + onFilter(fieldMapping, valueRaw, '-')} + /> + {typeof onToggleColumn === 'function' && ( + + )} + onFilter('_exists_', field, '+')} + scripted={fieldMapping && fieldMapping.scripted} + /> + + )} + + + + + {isCollapsible && ( + + )} + {displayUnderscoreWarning && } + {displayNoMappingWarning && } +
+ {value} +
+ + + ); +} diff --git a/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row_btn_collapse.tsx b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row_btn_collapse.tsx new file mode 100644 index 0000000000000..cb6b522ba9559 --- /dev/null +++ b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row_btn_collapse.tsx @@ -0,0 +1,44 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; + +export interface Props { + onClick: () => void; + isCollapsed: boolean; +} + +export function DocViewTableRowBtnCollapse({ onClick, isCollapsed }: Props) { + const label = i18n.translate('kbnDocViews.table.toggleFieldDetails', { + defaultMessage: 'Toggle field details', + }); + return ( + + onClick()} + iconType={isCollapsed ? 'arrowRight' : 'arrowDown'} + iconSize={'s'} + /> + + ); +} diff --git a/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row_btn_filter_add.tsx b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row_btn_filter_add.tsx new file mode 100644 index 0000000000000..df4572c5bb53b --- /dev/null +++ b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row_btn_filter_add.tsx @@ -0,0 +1,57 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export interface Props { + onClick: () => void; + disabled: boolean; +} + +export function DocViewTableRowBtnFilterAdd({ onClick, disabled = false }: Props) { + const tooltipContent = disabled ? ( + + ) : ( + + ); + + return ( + + + + ); +} diff --git a/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row_btn_filter_exists.tsx b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row_btn_filter_exists.tsx new file mode 100644 index 0000000000000..ae387ef1c4fa1 --- /dev/null +++ b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row_btn_filter_exists.tsx @@ -0,0 +1,69 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export interface Props { + onClick: () => void; + disabled?: boolean; + scripted?: boolean; +} + +export function DocViewTableRowBtnFilterExists({ + onClick, + disabled = false, + scripted = false, +}: Props) { + const tooltipContent = disabled ? ( + scripted ? ( + + ) : ( + + ) + ) : ( + + ); + + return ( + + + + ); +} diff --git a/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row_btn_filter_remove.tsx b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row_btn_filter_remove.tsx new file mode 100644 index 0000000000000..eda6636582977 --- /dev/null +++ b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row_btn_filter_remove.tsx @@ -0,0 +1,57 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export interface Props { + onClick: () => void; + disabled?: boolean; +} + +export function DocViewTableRowBtnFilterRemove({ onClick, disabled = false }: Props) { + const tooltipContent = disabled ? ( + + ) : ( + + ); + + return ( + + + + ); +} diff --git a/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row_btn_toggle_column.tsx b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row_btn_toggle_column.tsx new file mode 100644 index 0000000000000..55864f6567fe9 --- /dev/null +++ b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row_btn_toggle_column.tsx @@ -0,0 +1,67 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export interface Props { + active: boolean; + disabled?: boolean; + onClick: () => void; +} + +export function DocViewTableRowBtnToggleColumn({ onClick, active, disabled = false }: Props) { + if (disabled) { + return ( + + ); + } + return ( + + } + > + + + ); +} diff --git a/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row_icon_no_mapping.tsx b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row_icon_no_mapping.tsx new file mode 100644 index 0000000000000..ab22ba7bea7bd --- /dev/null +++ b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row_icon_no_mapping.tsx @@ -0,0 +1,44 @@ +/* + * 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 { EuiIconTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export function DocViewTableRowIconNoMapping() { + const ariaLabel = i18n.translate('kbnDocViews.table.noCachedMappingForThisFieldAriaLabel', { + defaultMessage: 'Warning', + }); + const tooltipContent = i18n.translate('kbnDocViews.table.noCachedMappingForThisFieldTooltip', { + defaultMessage: + 'No cached mapping for this field. Refresh field list from the Management > Index Patterns page', + }); + return ( + + ); +} diff --git a/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row_icon_underscore.tsx b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row_icon_underscore.tsx new file mode 100644 index 0000000000000..b527714014dae --- /dev/null +++ b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_row_icon_underscore.tsx @@ -0,0 +1,51 @@ +/* + * 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 { EuiIconTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export function DocViewTableRowIconUnderscore() { + const ariaLabel = i18n.translate( + 'kbnDocViews.table.fieldNamesBeginningWithUnderscoreUnsupportedAriaLabel', + { + defaultMessage: 'Warning', + } + ); + const tooltipContent = i18n.translate( + 'kbnDocViews.table.fieldNamesBeginningWithUnderscoreUnsupportedTooltip', + { + defaultMessage: 'Field names beginning with {underscoreSign} are not supported', + values: { underscoreSign: '_' }, + } + ); + + return ( + + ); +} diff --git a/src/legacy/core_plugins/kibana/public/doc_viewer/_doc_viewer.scss b/src/legacy/core_plugins/kibana/public/doc_viewer/_doc_viewer.scss index 539efbe592a51..25aa530976719 100644 --- a/src/legacy/core_plugins/kibana/public/doc_viewer/_doc_viewer.scss +++ b/src/legacy/core_plugins/kibana/public/doc_viewer/_doc_viewer.scss @@ -1,3 +1,7 @@ +.kbnDocViewerTable { + margin-top: $euiSizeS; +} + .kbnDocViewer { pre, .kbnDocViewer__value { @@ -6,6 +10,11 @@ word-wrap: break-word; white-space: pre-wrap; color: $euiColorFullShade; + vertical-align: top; + padding-top: 2px; + } + .kbnDocViewer__field { + padding-top: 8px; } .dscFieldName { @@ -28,20 +37,22 @@ } } -.kbnDocViewer__content { - background-color: $euiColorEmptyShade; - padding: $euiSizeS 0 0 0; -} - .kbnDocViewer__buttons, .kbnDocViewer__field { white-space: nowrap; } +.kbnDocViewer__buttons { + width: 60px; +} + +.kbnDocViewer__field { + width: 160px; +} .kbnDocViewer__actionButton { opacity: 0; - - &:focus { - opacity: 1; - } } +.kbnDocViewer__warning { + margin-right: $euiSizeS; +} + diff --git a/src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer_tab.tsx b/src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer_tab.tsx index d0fa29c5344a9..0b25421d8aff3 100644 --- a/src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer_tab.tsx +++ b/src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer_tab.tsx @@ -17,6 +17,7 @@ * under the License. */ import React from 'react'; +import { I18nProvider } from '@kbn/i18n/react'; import { DocViewRenderProps, DocViewRenderFn } from 'ui/registry/doc_views'; import { DocViewRenderTab } from './doc_viewer_render_tab'; import { DocViewerError } from './doc_viewer_render_error'; @@ -77,7 +78,12 @@ export class DocViewerTab extends React.Component { } // doc view is provided by a react component + const Component = component as any; - return ; + return ( + + + + ); } } diff --git a/src/legacy/ui/public/directives/field_name/field_name.tsx b/src/legacy/ui/public/directives/field_name/field_name.tsx index 95c403ae938ed..b7b9d6920eef1 100644 --- a/src/legacy/ui/public/directives/field_name/field_name.tsx +++ b/src/legacy/ui/public/directives/field_name/field_name.tsx @@ -29,8 +29,8 @@ interface Props { field?: { type: string; name: string; - rowCount: number; - scripted: boolean; + rowCount?: number; + scripted?: boolean; }; fieldName?: string; fieldType?: string; diff --git a/src/legacy/ui/public/registry/doc_views_types.ts b/src/legacy/ui/public/registry/doc_views_types.ts index 9388764309646..f078f374b4579 100644 --- a/src/legacy/ui/public/registry/doc_views_types.ts +++ b/src/legacy/ui/public/registry/doc_views_types.ts @@ -31,13 +31,27 @@ export type AngularController = (scope: AngularScope) => void; export type ElasticSearchHit = Record>; +export interface FieldMapping { + filterable?: boolean; + scripted?: boolean; + rowCount?: number; + type: string; + name: string; +} + +export type DocViewFilterFn = ( + mapping: FieldMapping | string | undefined, + value: unknown, + mode: '+' | '-' +) => void; + export interface DocViewRenderProps { - columns: string[]; - filter: (field: string, value: string | number, operation: string) => void; + columns?: string[]; + filter?: DocViewFilterFn; hit: ElasticSearchHit; indexPattern: IndexPattern; - onAddColumn: (columnName: string) => void; - onRemoveColumn: (columnName: string) => void; + onAddColumn?: (columnName: string) => void; + onRemoveColumn?: (columnName: string) => void; } export type DocViewRenderFn = ( domeNode: HTMLDivElement,