diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..1b2a8cc Binary files /dev/null and b/.DS_Store differ diff --git a/.eslintrc.js b/.eslintrc.js index 69c9df7..b267018 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,2 +1,7 @@ ---- -extends: "@elastic/kibana" +module.exports = { + root: true, + extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'], + rules: { + '@kbn/eslint/require-license-header': 'off', + }, +}; diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 889a8c6..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve - ---- - -** Environment ** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Kibana version [e.g. 6.3.1] - - Kibana Object Formatter plugin version [e.g. 0.1.4_kbn-6.3.1] - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Additional context** -Add any other context about the problem here. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index 527b31f..0000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,39 +0,0 @@ -# For the Reviewer -- [ ] Code review complete -- [ ] Testing Complete -- [ ] Quality ORT App Documentation Updated (your name is in the Validator square for this feature) - -When this is complete, you should approve the PR via github. - -# For the Reviewee - - - -## Summary -Description of changes. - -#### Release Note -Required. - -#### Breaking Changes -None. - -## Quality Assurance - -You have gathered the following items: -- [ ] This PR is tagged with a Release Milestone -- [ ] You have a log message clearly identifying when this feature is **working successfully** -- [ ] You have a log message clearly identifying when this feature is **failing** -- [ ] You added a PR against [p4-alerting](https://github.com/istresearch/p4-alerting/) to trigger based on the failure condition above - -Given all of the items above, you have updated your Application ORT at the following locations: -- **Features and Alerting**: Required. -- **P4 Alerting**: Required. - -## Testing and Verification - -*Steps to test your application for someone not familiar with it.* Required. - -1. Do this: `$ run-this.sh` -2. Look for this. -3. Clean up with this command diff --git a/.gitignore b/.gitignore index 4bd44e4..08ba0bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ -npm-debug.log* -node_modules -/build/ -yarn.lock - +/build +/target +/node_modules .DS_Store *.idea +.github \ No newline at end of file diff --git a/.i18nrc.json b/.i18nrc.json new file mode 100644 index 0000000..315787b --- /dev/null +++ b/.i18nrc.json @@ -0,0 +1,7 @@ +{ + "prefix": "kibanaObjectFormat", + "paths": { + "kibanaObjectFormat": "." + }, + "translations": ["translations/ja-JP.json"] +} diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 9b53c8c..0000000 --- a/LICENSE +++ /dev/null @@ -1,7 +0,0 @@ -Copyright (c) 2017 IST Research - -Licensed 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. diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 537a5b4..06f9b6d --- a/README.md +++ b/README.md @@ -293,4 +293,4 @@ For more information about any of these commands run `yarn ${task} --help`. For plugins/kibana-object-format/public/hacks/object_filter_hack.js REMOVE plugins/kibana-object-format/public/common/clean-template plugins/kibana-object-format/public/field_formats/object/cleanFieldTemplate.js -plugins/kibana-object-format/package.json +plugins/kibana-object-format/package.json \ No newline at end of file diff --git a/common/index.ts b/common/index.ts new file mode 100644 index 0000000..109f3bb --- /dev/null +++ b/common/index.ts @@ -0,0 +1,2 @@ +export const PLUGIN_ID = 'kibanaObjectFormat'; +export const PLUGIN_NAME = 'kibana_object_format'; diff --git a/images/array_format.jpg b/images/array_format.jpg deleted file mode 100644 index bf5ff3c..0000000 Binary files a/images/array_format.jpg and /dev/null differ diff --git a/images/array_formatted.jpg b/images/array_formatted.jpg deleted file mode 100644 index 7177be4..0000000 Binary files a/images/array_formatted.jpg and /dev/null differ diff --git a/images/array_native.jpg b/images/array_native.jpg deleted file mode 100644 index 0bcc451..0000000 Binary files a/images/array_native.jpg and /dev/null differ diff --git a/images/basic_format.jpg b/images/basic_format.jpg deleted file mode 100644 index db3aca2..0000000 Binary files a/images/basic_format.jpg and /dev/null differ diff --git a/images/basic_formatted.jpg b/images/basic_formatted.jpg deleted file mode 100644 index 63ff590..0000000 Binary files a/images/basic_formatted.jpg and /dev/null differ diff --git a/images/basic_native.jpg b/images/basic_native.jpg deleted file mode 100644 index e791a57..0000000 Binary files a/images/basic_native.jpg and /dev/null differ diff --git a/images/basic_raw.jpg b/images/basic_raw.jpg deleted file mode 100644 index a16e27a..0000000 Binary files a/images/basic_raw.jpg and /dev/null differ diff --git a/images/demo.gif b/images/demo.gif deleted file mode 100644 index 9b7bbe9..0000000 Binary files a/images/demo.gif and /dev/null differ diff --git a/images/fields_list.jpg b/images/fields_list.jpg deleted file mode 100644 index 2b253ca..0000000 Binary files a/images/fields_list.jpg and /dev/null differ diff --git a/images/not_supported.jpg b/images/not_supported.jpg deleted file mode 100644 index 40abc5b..0000000 Binary files a/images/not_supported.jpg and /dev/null differ diff --git a/images/refresh.jpg b/images/refresh.jpg deleted file mode 100644 index c7b9341..0000000 Binary files a/images/refresh.jpg and /dev/null differ diff --git a/index.js b/index.js deleted file mode 100644 index 084cc5a..0000000 --- a/index.js +++ /dev/null @@ -1,34 +0,0 @@ -import { registerFieldFormats } from './server/field-formatters'; - -export default function(kibana) { - return new kibana.Plugin({ - require: ['kibana'], - name: 'kibana_object_format', - uiExports: { - hacks: [ - 'plugins/kibana_object_format/hacks/field_mapper_hack', - 'plugins/kibana_object_format/hacks/custom_filter_bootstrap', - 'plugins/kibana_object_format/hacks/object_filter', - 'plugins/kibana_object_format/hacks/scroll_bug', - 'plugins/kibana_object_format/field_formats/object/register', - ], - uiSettingDefaults: { - 'fieldMapperHack:fields': { - value: - '{\n "index_pattern": {\n "*": {\n "include": [],\n "exclude": [".*"]\n }\n }\n}', - type: 'json', - description: - 'Configure field formatters for objects and arrays of objects by declaring the patterns and fields. See the kibana-object-formatter plugin project.', - }, - }, - }, - async init(server) { - registerFieldFormats(server); - }, - config(Joi) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - }); -} diff --git a/kibana.json b/kibana.json new file mode 100644 index 0000000..32057e5 --- /dev/null +++ b/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "kibanaObjectFormat", + "version": "1.0.0", + "kibanaVersion": "kibana", + "server": true, + "ui": true, + "requiredPlugins": ["data", "indexPatternManagement"], + "optionalPlugins": [] +} diff --git a/package.json b/package.json index e39759a..7038e5b 100644 --- a/package.json +++ b/package.json @@ -1,41 +1,16 @@ { "name": "kibana_object_format", - "version": "0.1.10", + "version": "1.0.0", "description": "Enables objects in arrays to be configured with a field formatter.", "license": "Apache 2.0", "homepage": "https://github.com/istresearch/kibana-object-format", - "main": "index.js", - "kibana": { - "version": "kibana", - "templateVersion": "1.0.0" - }, "scripts": { - "preinstall": "node ../../preinstall_check", - "kbn": "node ../../scripts/kbn", - "es": "node ../../scripts/es", - "lint": "eslint .", - "start": "plugin-helpers start", - "test:server": "plugin-helpers test:server", - "test:browser": "plugin-helpers test:browser", - "build": "plugin-helpers build" + "build": "yarn plugin-helpers build", + "plugin-helpers": "node ../../scripts/plugin_helpers", + "kbn": "node ../../scripts/kbn" }, "dependencies": { - "tippy.js": "~6.2.2" - }, - "devDependencies": { - "@elastic/eslint-config-kibana": "link:../../packages/eslint-config-kibana", - "@elastic/eslint-import-resolver-kibana": "link:../../packages/kbn-eslint-import-resolver-kibana", - "@kbn/expect": "link:../../packages/kbn-expect", - "@kbn/plugin-helpers": "link:../../packages/kbn-plugin-helpers", - "babel-eslint": "^10.0.1", - "eslint": "^5.16.0", - "eslint-plugin-babel": "^5.3.0", - "eslint-plugin-import": "^2.16.0", - "eslint-plugin-jest": "^22.4.1", - "eslint-plugin-jsx-a11y": "^6.2.1", - "eslint-plugin-mocha": "^5.3.0", - "eslint-plugin-no-unsanitized": "^3.0.2", - "eslint-plugin-prefer-object-spread": "^1.2.1", - "eslint-plugin-react": "^7.12.4" + "tippy.js": "^6.3.1", + "uuid": "^8.3.2" } } diff --git a/public/bootstrap/custom_filter_manager/FilterManagerHelper.ts b/public/bootstrap/custom_filter_manager/FilterManagerHelper.ts new file mode 100644 index 0000000..db0a9ab --- /dev/null +++ b/public/bootstrap/custom_filter_manager/FilterManagerHelper.ts @@ -0,0 +1,108 @@ +import { get } from 'lodash'; +import { Filter, CustomFilterManager, CustomFilterMeta } from '../../types'; + +class FilterManagerHelper { + private addFiltersCache: (filters: Filter | Filter[], pinFilterStatus?: boolean) => void; + private similarityScript: string = ''; + private newFilter: Partial = {}; + private filterManager: CustomFilterManager; + + constructor(addFiltersCache: any, filterManager: any) { + this.addFiltersCache = addFiltersCache; + this.similarityScript = ''; + this.filterManager = filterManager; + } + + private getFilterIndex({ path, value, negate }: Partial) { + const currentFilters = this.getCurrentFilters(); + + return currentFilters.findIndex( + (filter) => filter.path === path && filter.value === value && filter.negate === negate + ); + } + + public getCurrentFilters(): Partial[] { + return this.filterManager.getFilters().map((filter) => ({ + path: + get(filter, 'query.bool.must.script.script.params.field', null) || + get(filter, 'meta.key', null), + value: + get(filter, 'query.bool.must.script.script.params.dhash', null) || + get(filter, 'meta.params.query', null), + negate: get(filter, 'meta.negate', null), + distance: get(filter, 'query.bool.must.script.script.params.distance', null), + })); + } + + public addFilter({ path, value, negate, alias = null }: Partial) { + const filterIndex = this.getFilterIndex({ path, value, negate }); + + if (filterIndex >= 0) { + return; + } + + if (path) { + this.addFiltersCache.apply(this.filterManager, [ + { + ...this.newFilter, + meta: { + alias, + negate: !!this.newFilter?.meta?.negate, + index: this.newFilter?.meta?.index, + disabled: false, + }, + query: { + match_phrase: { + [path]: value, + }, + }, + }, + ]); + } + } + + public addImageSimilarityFilter({ path, value, distance = '8' }: CustomFilterMeta) { + this.removeFilter({ path, value, negate: true }); + this.removeFilter({ path, value, negate: false }); + + this.addFiltersCache.apply(this.filterManager, [ + { + ...this.newFilter, + meta: { + alias: `Image Similarity: ${value} (${distance})`, + negate: !!this.newFilter?.meta?.negate, + index: this.newFilter?.meta?.index, + disabled: false, + }, + query: { + bool: { + must: { + script: { + script: { + lang: 'painless', + params: { + dhash: value, + distance: parseInt(distance, 10), + field: path, + }, + source: this.similarityScript, + }, + }, + }, + }, + }, + }, + ]); + } + + public removeFilter({ path, value, negate }: Partial) { + const currentFilters = this.filterManager.getFilters(); + const filterIndex = this.getFilterIndex({ path, value, negate }); + + if (filterIndex >= 0) { + this.filterManager.removeFilter(currentFilters[filterIndex]); + } + } +} + +export default FilterManagerHelper; diff --git a/public/hacks/custom_filter_bootstrap/popover.less b/public/bootstrap/custom_filter_manager/Popover.scss similarity index 100% rename from public/hacks/custom_filter_bootstrap/popover.less rename to public/bootstrap/custom_filter_manager/Popover.scss diff --git a/public/hacks/custom_filter_bootstrap/Popover.js b/public/bootstrap/custom_filter_manager/Popover.ts similarity index 54% rename from public/hacks/custom_filter_bootstrap/Popover.js rename to public/bootstrap/custom_filter_manager/Popover.ts index 3331641..4024a5f 100644 --- a/public/hacks/custom_filter_bootstrap/Popover.js +++ b/public/bootstrap/custom_filter_manager/Popover.ts @@ -1,69 +1,36 @@ -import _ from 'lodash'; -import tippy from 'tippy.js'; +import tippy, { DefaultProps, Instance } from 'tippy.js'; import 'tippy.js/dist/tippy.css'; import 'tippy.js/themes/light.css'; -import './popover.less'; - -const DEFAULT_PROPS = { - content: 'LOADING', - placement: 'right', - trigger: 'click', - appendTo: document.body, - interactive: true, - allowHTML: true, - popperOptions: { - strategy: 'fixed', - }, - theme: 'light', -}; class Popover { - constructor(defaultProps = {}) { - this._defaultProps = { - ...DEFAULT_PROPS, - ...defaultProps, - }; - this._init = false; - this._instance = null; - this._callback = null; - this._entryValues = []; - - tippy.setDefaultProps(this._defaultProps); - } - - init() { - this._init = true; - this._instance = null; - this._callback = null; - this._entryValues = []; - $('body').on( 'submit.objectFilterForm', '.object-filter-form', this.handlerProcessForm.bind(this)); - $('body').on('click.selectFilters', '.select-filters', this.handlerSelectAll.bind(this)); - $('body').on('click.filterPopver', '.tippy-filter-button', this.handlerShowPopover.bind(this)); - $('body').on('mousemove.rangeSelector', '[data-rangeslider]', this.handlerDistanceRangeSlider); - $('body').on('input.rangeSelector', '[data-range-input]', this.handlerDistanceInput); - } - - destroy() { - this._init = false; - this._instance = null; - this._callback = null; - this._entryValues.length = 0; - $('body').off('submit.objectFilterForm'); - $('body').off('click.selectFilters'); - $('body').off('click.filterPopver'); - $('body').off('mousemove.rangeSelector'); - $('body').off('input.rangeSelector'); - } - - isInit() { - return this._init; + private init: boolean = false; + private instance: Instance | undefined; + private callback: any = null; + private entryValues: any[] = []; + + constructor(extraProps: Partial = {}) { + tippy.setDefaultProps({ + content: 'LOADING', + placement: 'right', + trigger: 'click', + appendTo: document.body, + interactive: true, + allowHTML: true, + popperOptions: { + strategy: 'fixed', + }, + theme: 'light', + ...extraProps, + }); } - handlerDistanceRangeSlider() { + private handlerDistanceRangeSlider() { + // @ts-ignore $(this).next().val($(this).val()); } - handlerDistanceInput() { + private handlerDistanceInput() { + // @ts-ignore let val = parseInt($(this).val(), 10); if (isNaN(val)) { val = 0; @@ -72,43 +39,64 @@ class Popover { } else if (val < 0) { val = 0; } - + $(this).val(val); $(this).prev().val(val); } - handlerShowPopover(e) { - this._instance = tippy(e.target, { - onHide(instance) { - setTimeout(() => { - instance.unmount(); - instance.destroy(); - this._instance = null; - }, 100); + private handlerShowPopover(e: JQuery.Event & { target: HTMLElement }) { + const self = this; - $('.keep-icon-visible').removeClass('keep-icon-visible'); - }, - }); - $(e.target).addClass('keep-icon-visible'); - this._instance.show(); + let buttonNode = e.target; + + while (!$(buttonNode).hasClass('tippy-filter-button')) { + if (buttonNode.parentElement) { + buttonNode = buttonNode.parentElement; + } else { + break; + } + } + + if ($(buttonNode).hasClass('tippy-filter-button')) { + // @ts-ignore + this.instance = tippy(buttonNode, { + onHide(instance) { + setTimeout(() => { + instance?.unmount(); + instance?.destroy(); + self.instance = undefined; + }, 100); + + $('.keep-icon-visible').removeClass('keep-icon-visible'); + }, + }); + + $(e.target).addClass('keep-icon-visible'); + this.instance?.show(); + } } - handlerProcessForm(e) { + private handlerProcessForm(e: JQuery.Event & { target: HTMLElement }) { e.preventDefault(); e.stopPropagation(); - const formFields = $('.object-filter-form').serializeArray(); - const selectedEntryValues = this._entryValues.map(entryValue => ({ + const formFields = $('.object-filter-form').serializeArray() || []; + const selectedEntryValues = this.entryValues.map((entryValue) => ({ ...entryValue, - checked: formFields.findIndex(field => field.value === entryValue.value || field.value === entryValue.dHashValue) !== -1, - distance: entryValue.dHashValue && formFields.find(field => field.name === `${entryValue.dHashValue}-distance`).value, + checked: + formFields.findIndex( + (field) => field.value === entryValue.value || field.value === entryValue.dHashValue + ) !== -1, + distance: + entryValue.dHashValue && + formFields.find((field: any) => field.name === `${entryValue.dHashValue}-distance`)?.value, })); - this._instance.hide(); - this._callback(selectedEntryValues); + this.instance?.hide(); + this.callback(selectedEntryValues); } - handlerSelectAll(e) { + private handlerSelectAll(e: JQuery.Event & { target: HTMLElement }) { e.preventDefault(); e.stopPropagation(); @@ -120,10 +108,14 @@ class Popover { } } - formBuilder(currentFilters) { - const formFields = this._entryValues.map(({ type, label, path, value, valueActual, negate, dHashPath, dHashValue }) => { + private formBuilder(currentFilters: any) { + const formFields = this.entryValues.map( + ({ type, label, path, value, valueActual, negate, dHashPath, dHashValue }) => { const filterIndex = currentFilters.findIndex( - filter => [path, dHashPath].includes(filter.path) && [value, valueActual, dHashValue].includes(filter.value) && filter.negate === negate + (filter: any) => + [path, dHashPath].includes(filter.path) && + [value, valueActual, dHashValue].includes(filter.value) && + filter.negate === negate ); const filterExist = filterIndex !== -1; let distance = filterIndex >= 0 ? currentFilters[filterIndex].distance : 16; @@ -143,19 +135,25 @@ class Popover { return `
- ${dHashValue && ` + ${ + dHashValue + ? `
Exact
Fuzzy
- `} + ` + : '' + }
`; default: @@ -186,27 +184,55 @@ class Popover { `; } - setForm(entryValues, currentFilters, callback) { - this._entryValues = entryValues; - this._callback = callback; + public initialize() { + this.init = true; + this.instance = undefined; + this.callback = undefined; + this.entryValues.length = 0; + $('body').on( + 'submit.objectFilterForm', + '.object-filter-form', + this.handlerProcessForm.bind(this) + ); + $('body').on('click.selectFilters', '.select-filters', this.handlerSelectAll.bind(this)); + $('body').on('click.filterPopver', '.tippy-filter-button', this.handlerShowPopover.bind(this)); + $('body').on('mousemove.rangeSelector', '[data-rangeslider]', this.handlerDistanceRangeSlider); + $('body').on('input.rangeSelector', '[data-range-input]', this.handlerDistanceInput); + } + + public isInitialized() { + return this.init; + } + + public destroy() { + this.init = false; + this.instance = undefined; + this.callback = undefined; + this.entryValues.length = 0; + $('body').off('submit.objectFilterForm'); + $('body').off('click.selectFilters'); + $('body').off('click.filterPopver'); + $('body').off('mousemove.rangeSelector'); + $('body').off('input.rangeSelector'); + } + + public setForm(entryValues: any, currentFilters: any, callback: any) { + this.entryValues = entryValues; + this.callback = callback; const formhtml = this.formBuilder(currentFilters); this.setContent(formhtml); } - setContent(content) { + private setContent(content: string) { setTimeout(() => { - if (this._instance) { - this._instance.setContent(''); - this._instance.setContent(content); - } + this.instance?.setContent(''); + this.instance?.setContent(content); }, 0); } - hide() { + public hide() { setTimeout(() => { - if (this._instance) { - this._instance.hide(); - } + this.instance?.hide(); }, 0); } } diff --git a/public/bootstrap/custom_filter_manager/index.ts b/public/bootstrap/custom_filter_manager/index.ts new file mode 100644 index 0000000..30f9011 --- /dev/null +++ b/public/bootstrap/custom_filter_manager/index.ts @@ -0,0 +1,106 @@ +import { isArray, get } from 'lodash'; +import Popover from './Popover'; +import FilterManagerHelper from './FilterManagerHelper'; +import { CustomFilterProps, CustomFilterManager } from '../../types'; + +let popover: Popover | undefined; +let filterManagerHelper: any; +let indexPatternLookup: any = {}; + +if (!popover) { + popover = new Popover(); + (window as any).popover = popover; +} + +export const initPopover = () => { + if (popover?.isInitialized()) { + popover.destroy(); + } + + popover?.initialize(); +}; + +export const initFilterManager = (filterManager: Partial) => { + filterManager.register = (customFilter: any) => { + if (!filterManager?.customFilters) { + filterManager.customFilters = []; + } + + filterManager.customFilters.push(customFilter); + }; +}; + +export const setupAddFilters = async ({ + indexPatterns, + addFiltersCached, + filterManager, +}: Pick) => { + const indexPatternIDList = await indexPatterns.getIds(); + + for (let id of indexPatternIDList) { + let ip = await indexPatterns.get(id); + indexPatternLookup[id] = ip.fieldFormatMap; + } + + filterManagerHelper = new FilterManagerHelper(addFiltersCached, filterManager); +}; + +export const addFilters = async ({ + filterManager, + newFilters, + addFiltersCached, + indexPatterns, +}: CustomFilterProps) => { + if (isArray(newFilters) && newFilters.length !== 1) { + return; + } + + const newFilter = Array.isArray(newFilters) ? newFilters[0] : newFilters; + const selectedIndexPatternID = get(newFilter, 'meta.index', null); + const selectedIndexPattern = await indexPatterns.get(selectedIndexPatternID); + const fieldFormatMap = selectedIndexPattern.fieldFormatMap; + const matchPhrase = get(newFilter, 'query.match_phrase', {}); + const fieldNameKeys = Object.keys(matchPhrase); + const fieldName = fieldNameKeys.length === 1 ? fieldNameKeys[0] : ''; + const formatType = get(fieldFormatMap, [fieldName, 'id'], null); + const params = get(fieldFormatMap, [fieldName, 'params'], {}); + const values = get(matchPhrase, [fieldName], {}); + const meta = get(newFilter, 'meta', {}); + + const { + getCurrentFilters, + addFilter, + addImageSimilarityFilter, + removeFilter, + } = filterManagerHelper; + filterManagerHelper.newFilter = newFilter; + filterManagerHelper.similarityScript = get(params, 'similarityScript', ''); + + const filterParams = { + fieldName, + formatType, + params, + values, + meta, + popover, + addFilter: addFilter.bind(filterManagerHelper), + addImageSimilarityFilter: addImageSimilarityFilter.bind(filterManagerHelper), + removeFilter: removeFilter.bind(filterManagerHelper), + getCurrentFilters: getCurrentFilters.bind(filterManagerHelper), + }; + let customFilterFlag = false; + + if (filterManager.customFilters) { + for (let customFilter of filterManager.customFilters) { + customFilterFlag = customFilter(filterParams); + + if (customFilterFlag) { + break; + } + } + } + + if (!customFilterFlag) { + addFiltersCached.apply(filterManager, [newFilters]); + } +}; diff --git a/public/bootstrap/fieldMapper.ts b/public/bootstrap/fieldMapper.ts new file mode 100644 index 0000000..5622610 --- /dev/null +++ b/public/bootstrap/fieldMapper.ts @@ -0,0 +1,71 @@ +import { has, uniq, difference } from 'lodash'; +import { IUiSettingsClient } from 'kibana/public'; +import { FieldSpec } from '../../../../src/plugins/data/common'; + +export interface IFieldMapper { + uiSettings: IUiSettingsClient; + fields: FieldSpec[]; + pattern: string; +} + +export const fieldMapper = ({ uiSettings, fields, pattern }: IFieldMapper): FieldSpec[] => { + const { index_pattern: settings } = uiSettings.get('ObjectFieldMapper:fields'); + + let match = { include: [], exclude: [] }; + if (has(settings, '*')) { + match = settings['*']; + } else if (has(settings, pattern)) { + match = settings[pattern]; + } + + // 1) Iterate the field names, identify the "parent" paths + const mappingNames: string[] = []; + let paths: string[] = []; + + for (let field of fields) { + const fieldName = field.name; + const parts = fieldName.split('.'); + + mappingNames.push(fieldName); + + while (parts.length > 1) { + parts.pop(); + paths.push(parts.join('.')); + } + } + + paths = uniq(difference(paths, mappingNames)); + + // 2) Test the discovered field names against the configuration + const included: string[] = []; + const excluded: string[] = []; + + for (let expression of match.include) { + for (let path of paths) { + if (path.match(expression)) { + included.push(path); + } + } + } + + for (let expression of match.exclude) { + for (let path of paths) { + if (path.match(expression)) { + excluded.push(path); + } + } + } + + // 3) Add a field mapping for the missing parents + for (let path of difference(included, excluded)) { + fields.push({ + name: path, + aggregatable: false, + searchable: true, + indexed: true, + type: 'string', + }); + } + + return fields; +}; diff --git a/public/bootstrap/index.ts b/public/bootstrap/index.ts new file mode 100644 index 0000000..5f3c9be --- /dev/null +++ b/public/bootstrap/index.ts @@ -0,0 +1,3 @@ +export { fieldMapper } from './fieldMapper'; +export { default as updateFieldTemplate } from './updateFieldTemplate'; +export * from './custom_filter_manager'; \ No newline at end of file diff --git a/public/field_formats/object/cleanFieldTemplate.js b/public/bootstrap/updateFieldTemplate.ts similarity index 61% rename from public/field_formats/object/cleanFieldTemplate.js rename to public/bootstrap/updateFieldTemplate.ts index fa8317e..f79adfd 100644 --- a/public/field_formats/object/cleanFieldTemplate.js +++ b/public/bootstrap/updateFieldTemplate.ts @@ -1,20 +1,14 @@ -import _ from 'lodash'; -import '../../common/jquery-plugins/observer'; +import { throttle } from 'lodash'; -const cleanTemplate = showPopover => { - const clean = showPopover => { - $(`.collection-table`) - .closest(`td`) - .children(`.euiToolTipAnchor`) - .remove(); +let init = false; - $(`.collection-table`) - .closest(`.kbnDocTableCell__dataField`) - .addClass('white-space-normal'); +const updateFieldTemplate = (showPopover: boolean) => { + const clean = (showPopover: boolean) => { + $(`.collection-table`).closest(`td`).children(`.euiToolTipAnchor`).remove(); - $(`.collection-table`) - .closest(`.kbnDocViewer__value`) - .addClass('white-space-normal'); + $(`.collection-table`).closest(`.kbnDocTableCell__dataField`).addClass('white-space-normal'); + + $(`.collection-table`).closest(`.kbnDocViewer__value`).addClass('white-space-normal'); $(`.collection-table-no-filter`) .closest('.kbnDocTableCell__dataField') @@ -26,7 +20,7 @@ const cleanTemplate = showPopover => { .children('.kbnDocViewer__buttons') .children('span') .children(`button:not([data-test-subj='toggleColumnButton'])`) - .attr('disabled', true); + .attr('disabled', 'true'); if (showPopover) { $(`.enable-filter-popover`) @@ -45,23 +39,24 @@ const cleanTemplate = showPopover => { } }; - $('#kibana-app').observe( + $('#kibana-body').observe( '.kbnDocTableCell__dataField', - _.throttle(() => { + throttle(() => { clean(showPopover); - }, 1000) + }, 100) ); - $('#kibana-app').observe( + $('#kibana-body').observe( '.kbnDocViewerTable tr', - _.throttle(() => { + throttle(() => { clean(showPopover); - }, 1000) + }, 100) ); }; export default (showPopover = false) => { - setTimeout(() => { - cleanTemplate(showPopover); - }, 0); + if (!init) { + init = true; + updateFieldTemplate(showPopover); + } }; diff --git a/public/common/highlight/highlight_html.js b/public/common/highlight/highlight_html.js deleted file mode 100755 index e2bc333..0000000 --- a/public/common/highlight/highlight_html.js +++ /dev/null @@ -1,28 +0,0 @@ -import _ from 'lodash'; -import { highlightTags } from './highlight_tags'; -import { htmlTags } from './html_tags'; - -export function getHighlightHtml(fieldValue, highlights) { - let highlightHtml = (typeof fieldValue === 'object') - ? JSON.stringify(fieldValue) - : fieldValue; - - _.each(highlights, function (highlight) { - const escapedHighlight = _.escape(highlight); - - // Strip out the highlight tags to compare against the field text - const untaggedHighlight = escapedHighlight - .split(highlightTags.pre).join('') - .split(highlightTags.post).join(''); - - // Replace all highlight tags with proper html tags - const taggedHighlight = escapedHighlight - .split(highlightTags.pre).join(htmlTags.pre) - .split(highlightTags.post).join(htmlTags.post); - - // Replace all instances of the untagged string with the properly tagged string - highlightHtml = highlightHtml.split(untaggedHighlight).join(taggedHighlight); - }); - - return highlightHtml; -} diff --git a/public/common/highlight/highlight_request.js b/public/common/highlight/highlight_request.js deleted file mode 100755 index 59f39db..0000000 --- a/public/common/highlight/highlight_request.js +++ /dev/null @@ -1,16 +0,0 @@ -import { highlightTags } from './highlight_tags'; - -const FRAGMENT_SIZE = Math.pow(2, 31) - 1; // Max allowed value for fragment_size (limit of a java int) - -export function getHighlightRequest(query, getConfig) { - if (!getConfig('doc_table:highlight')) return; - - return { - pre_tags: [highlightTags.pre], - post_tags: [highlightTags.post], - fields: { - '*': {} - }, - fragment_size: FRAGMENT_SIZE - }; -} diff --git a/public/common/highlight/highlight_tags.js b/public/common/highlight/highlight_tags.js deleted file mode 100755 index 416f331..0000000 --- a/public/common/highlight/highlight_tags.js +++ /dev/null @@ -1,7 +0,0 @@ -// By default, ElasticSearch surrounds matched values in . This is not ideal because it is possible that -// the value could contain in the value. We define these custom tags that we would never expect to see -// inside a field value. -export const highlightTags = { - pre: '@kibana-highlighted-field@', - post: '@/kibana-highlighted-field@' -}; diff --git a/public/common/highlight/html_tags.js b/public/common/highlight/html_tags.js deleted file mode 100755 index f2da608..0000000 --- a/public/common/highlight/html_tags.js +++ /dev/null @@ -1,5 +0,0 @@ -// These are the html tags that will replace the highlight tags. -export const htmlTags = { - pre: '', - post: '' -}; diff --git a/public/common/highlight/index.js b/public/common/highlight/index.js deleted file mode 100755 index 835c9bc..0000000 --- a/public/common/highlight/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { getHighlightHtml } from './highlight_html'; -export { getHighlightRequest } from './highlight_request'; diff --git a/public/common/lodash-mixins/get_pluck.js b/public/common/lodash-mixins/get_pluck.js deleted file mode 100755 index 43c52d2..0000000 --- a/public/common/lodash-mixins/get_pluck.js +++ /dev/null @@ -1,79 +0,0 @@ -export function lodashGetPluckMixin(_) { - - const toPath = require('lodash/internal/toPath'); - const toObject = require('lodash/internal/toObject'); - - /** - * The base implementation of `get` without support for string paths - * and default values. - * - * @private - * @param {Object} object The object to query. - * @param {Array} path The path of the property to get. - * @param {string} [pathKey] The key representation of path. - * @returns {*} Returns the resolved value. - */ - function baseGetWithPluck(object, path, pathKey) { - - if (object == null) { - return; - } - - if (pathKey !== undefined && pathKey in toObject(object)) { - path = [pathKey]; - } - - let index = 0; - const length = path.length; - - while (object != null && index < length) { - const key = path[index++]; - - if (_.isArray(object) && !_.has(object, key)) { - object = _.map(object, _.property(_.slice(path, index - 1))); - index = length; - } - else { - object = object[key]; - } - } - - return (index && index === length) ? object : undefined; - } - - _.mixin(_, { - /** - * Gets the property value at `path` of `object`. If the resolved value is - * `undefined` the `defaultValue` is used in its place. This uses the - * `pluck` method to support returning a set of results if an array is - * encountered mid-path. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to query. - * @param {Array|string} path The path of the property to get. - * @param {*} [defaultValue] The value returned if the resolved value is `undefined`. - * @returns {*} Returns the resolved value. - * @example - * - * var object = { 'a': [{ 'b': { 'c': 3 } },{ 'b': { 'c': 7 } }] }; - * - * _.get(object, 'a[0].b.c'); - * // => 3 - * - * _.get(object, 'a[*].b.c'); - * // => [3, 7] - * - * _.get(object, ['a', '0', 'b', 'c']); - * // => 3 - * - * _.get(object, 'a.b.c', 'default'); - * // => 'default' - */ - getPluck: function (object, path, defaultValue) { - const result = object == null ? undefined : baseGetWithPluck(object, toPath(path), path + ''); - return result === undefined ? defaultValue : result; - } - }); -} diff --git a/public/common/lodash-mixins/oop.js b/public/common/lodash-mixins/oop.js deleted file mode 100755 index 4ef3c6f..0000000 --- a/public/common/lodash-mixins/oop.js +++ /dev/null @@ -1,44 +0,0 @@ -export function lodashOopMixin(_) { - - // create a property descriptor for properties - // that won't change - function describeConst(val) { - return { - writable: false, - enumerable: false, - configurable: false, - value: val - }; - } - - const props = { - inherits: describeConst(function (SuperClass) { - - const prototype = Object.create(SuperClass.prototype, { - constructor: describeConst(this), - superConstructor: describeConst(SuperClass) - }); - - Object.defineProperties(this, { - prototype: describeConst(prototype), - Super: describeConst(SuperClass) - }); - - return this; - }) - }; - - _.mixin(_, { - - /** - * Add class-related behavior to a function, currently this - * only attaches an .inherits() method. - * - * @param {Constructor} ClassConstructor - The function that should be extended - * @return {Constructor} - the constructor passed in; - */ - class: function (ClassConstructor) { - return Object.defineProperties(ClassConstructor, props); - } - }); -} diff --git a/public/field_formats/object/constants.js b/public/field_formats/object/constants.js deleted file mode 100644 index 3d638c4..0000000 --- a/public/field_formats/object/constants.js +++ /dev/null @@ -1,24 +0,0 @@ -export const ID = 'ist-object'; -export const TITLE = 'Object'; -export const FIELD_TYPE = [ - 'string', - 'unknown' -]; - -export const DEFAULT_VALUES = { - label: null, // Optional data label - path: null, // Dot notated location of the value within the object, relative to basePath - type: 'text', - filtered: true, // To enable the filtering on cell click - dHashField: null, - filterField: null, // If the data is analyzed, and there is a keyword subfield we can use for the filter - height: null, // Image dimension in px - width: null, // Image dimension in px - limit: null // If presenting an array, this is the max we will show -}; - -export const FORMAT_TYPES = [ - { id: 'text', name: 'Text' }, - { id: 'link', name: 'Link' }, - { id: 'image', name: 'Image' } -]; \ No newline at end of file diff --git a/public/field_formats/object/object.js b/public/field_formats/object/object.js deleted file mode 100644 index 5bff623..0000000 --- a/public/field_formats/object/object.js +++ /dev/null @@ -1,236 +0,0 @@ -import _ from 'lodash'; -import { FieldFormat } from '../../../../../src/plugins/data/common/field_formats/field_format'; -import { getHighlightHtml } from '../../common/highlight'; -import { lodashOopMixin } from '../../common/lodash-mixins/oop'; -import { lodashGetPluckMixin } from '../../common/lodash-mixins/get_pluck'; -import { DEFAULT_VALUES, ID, TITLE, FIELD_TYPE } from './constants'; -import cleanFieldTemplate from './cleanFieldTemplate'; -import format_html from './templates/object_format.html'; -import image_html from './templates/object_image.html'; -import link_html from './templates/object_link.html'; -import text_html from './templates/object_text.html'; -import empty_html from './templates/object_empty.html'; -import './object.less'; - -lodashOopMixin(_); -lodashGetPluckMixin(_); -cleanFieldTemplate(true); - -const vis_template = _.template(format_html); -const image_template = _.template(image_html); -const link_template = _.template(link_html); -const text_template = _.template(text_html); -const empty_template = _.template(empty_html); - - -export class ObjectFormat extends FieldFormat { - constructor(params) { - super(params); - } - - static id = ID; - static title = TITLE; - static fieldType = FIELD_TYPE; - - getParamDefaults() { - return { - fieldType: null, // populated by editor, see controller - basePath: null, // If multiple fields should be grouped, this is the common parent - limit: null, // // If basePath is an array, this is the max we will show - similarityScript: null, - fields: [{ ...DEFAULT_VALUES }], - }; - } - - _getFieldModels(value, field, hit, basePath, objectFields) { - let filtered = false; - const fields = []; - - // Apply each field configured for the formatter to the value - _.forEach( - objectFields, - _.bind(function(objectField) { - let label = ''; - let fieldPath = ''; - - if (objectField.label) label = objectField.label + ': '; - if (objectField.path) fieldPath = objectField.path; - if (objectField.filtered) filtered = objectField.filtered; - - // Get the value from the field path - let fieldValues = _.getPluck(value, fieldPath); - - if (objectField.type === 'text') { - // We generate a nice comma delimited list, like the built in String does. - if (_.isArray(fieldValues)) { - if (objectField.limit) { - fieldValues = _.slice(fieldValues, 0, objectField.limit); - } - - fieldValues = _(fieldValues) - .map(item => (_.isObject(item) ? JSON.stringify(item) : item)) - .join(', '); - } else if (_.isObject(fieldValues)) { - fieldValues = JSON.stringify(fieldValues); - } - } - - if (!_.isArray(fieldValues)) { - fieldValues = [fieldValues]; - } - - // If we have a limit on lists, impose it now - if (objectField.limit) { - fieldValues = _.slice(fieldValues, 0, objectField.limit); - } - - const fullPath = this._getFullPath(basePath, field, objectField.path, null); - const filterPath = this._getFullPath( - basePath, - field, - objectField.path, - objectField.filterField - ); - const valueModels = []; - - _.forEach( - fieldValues, - _.bind(function(fieldValue) { - const valueModel = { - value: fieldValue, - display: _.escape(fieldValue), - }; - - if (hit && hit.highlight && hit.highlight[fullPath]) { - valueModel.display = getHighlightHtml(valueModel.display, hit.highlight[fullPath]); - } else if (hit && hit.highlight && hit.highlight[filterPath]) { - valueModel.display = getHighlightHtml(valueModel.display, hit.highlight[filterPath]); - } - - valueModels.push(valueModel); - }, this) - ); - - const fieldHtml = this._fieldToHtml({ - label: label, - formatType: objectField.type, - values: valueModels, - width: objectField.width, - height: objectField.height, - filterField: objectField.filterField, - }); - - fields.push({ - formatType: objectField.type, - label: label, - html: fieldHtml, - }); - }, this) - ); - - return { filtered: filtered, fields: fields }; - } - - _fieldToHtml(fieldModel) { - let html = null; - - switch (fieldModel.formatType) { - case 'image': - html = image_template({ field: fieldModel }); - break; - - case 'link': - html = link_template({ field: fieldModel }); - break; - - case 'text': - default: - html = text_template({ field: fieldModel }); - break; - } - - return html; - } - - _getFullPath(basePath, field, valuePath, filterField) { - const parts = [field.name]; - - if (basePath) { - parts.push(basePath); - } - - parts.push(valuePath); - - if (filterField) { - parts.push(filterField); - } - - return parts.join('.'); - } - - asPrettyString(val) { - if (val === null || val === undefined) return ' - '; - switch (typeof val) { - case 'string': - return val; - case 'object': - return JSON.stringify(val, null, ' '); - default: - return '' + val; - } - } - - htmlConvert = (val, options = {}) => { - const { field, hit } = options; - - const basePath = this.param('basePath'); - const objectFields = this.param('fields'); - const limit = this.param('limit'); - - if (basePath) { - val = _.get(val, basePath); - } - - if (!_.isArray(val)) { - val = [val]; - } - - // Filter out any null or empty entries - val = $.grep(val, function(n) { - return n === 0 || n; - }); - - // If we have a limit on this list, impose it now - if (limit) { - val = _.slice(val, 0, limit); - } - - if (val.length > 0) { - const htmlSnippets = []; - - _.forEach( - val, - _.bind(function(value) { - if (value) { - const fieldModels = this._getFieldModels(value, field, hit, basePath, objectFields); - htmlSnippets.push( - vis_template({ - filtered: fieldModels.filtered, - fields: fieldModels.fields, - uid: Math.floor(Math.random() * 1000000 + 1), - }) - ); - } - }, this) - ); - - return htmlSnippets.join('\n'); - } else { - return empty_template(); - } - }; - - textConvert = rawValue => { - return this.asPrettyString(rawValue); - }; -} diff --git a/public/field_formats/object/register.js b/public/field_formats/object/register.js deleted file mode 100644 index cb98f6c..0000000 --- a/public/field_formats/object/register.js +++ /dev/null @@ -1,7 +0,0 @@ -import { npSetup } from 'ui/new_platform'; -import { ObjectFormat } from './object'; -npSetup.plugins.data.fieldFormats.register([ObjectFormat]); - -import { RegistryFieldFormatEditorsProvider } from 'ui/registry/field_format_editors'; -import { ObjectFormatEditor } from './editor'; -RegistryFieldFormatEditorsProvider.register(() => ObjectFormatEditor); diff --git a/public/hacks/custom_filter_bootstrap/FilterManagerHelper.js b/public/hacks/custom_filter_bootstrap/FilterManagerHelper.js deleted file mode 100644 index a7c64f5..0000000 --- a/public/hacks/custom_filter_bootstrap/FilterManagerHelper.js +++ /dev/null @@ -1,107 +0,0 @@ -import _ from 'lodash'; -import { npStart } from 'ui/new_platform'; - -const { - query: { filterManager }, -} = npStart.plugins.data; - -class FilterManagerHelper { - constructor(addFiltersCache) { - this._addFiltersCache = addFiltersCache; - this._newFilter = {}; - this._similarityScript = ''; - } - - set newFilter(newFilter) { - this._newFilter = newFilter; - } - - set similarityScript(similarityScript) { - this._similarityScript = similarityScript; - } - - getCurrentFilters() { - return filterManager.getFilters().map(filter => ({ - path: _.get(filter, 'query.bool.must.script.script.params.field', null) || _.get(filter, 'meta.key', null), - value: _.get(filter, 'query.bool.must.script.script.params.dhash', null) || _.get(filter, 'meta.params.query', null), - negate: _.get(filter, 'meta.negate', null), - distance: _.get(filter, 'query.bool.must.script.script.params.distance', null), - })); - } - - getFilterIndex({ path, value, negate }) { - const currentFilters = this.getCurrentFilters(); - - return currentFilters.findIndex( - filter => filter.path === path && filter.value === value && filter.negate === negate - ); - } - - addFilter({ path, value, negate, alias = null }) { - const filterIndex = this.getFilterIndex({ path, value, negate }); - - if (filterIndex >= 0) { - return; - } - - this._addFiltersCache.apply(filterManager, [ - { - ...this._newFilter, - meta: { - alias, - negate: this._newFilter.meta.negate, - index: this._newFilter.meta.index, - }, - query: { - match_phrase: { - [path]: value, - }, - }, - }, - ]); - } - - addImageSimilarityFilter({ path, value, distance = 8 }) { - this.removeFilter({ path, value, negate: true }); - this.removeFilter({ path, value, negate: false }); - - this._addFiltersCache.apply(filterManager, [ - { - ...this._newFilter, - meta: { - alias: `Image Similarity: ${value} (${distance})`, - negate: this._newFilter.meta.negate, - index: this._newFilter.meta.index, - }, - query: { - bool: { - must: { - script: { - script: { - lang: 'painless', - params: { - dhash: value, - distance: parseInt(distance, 10), - field: path, - }, - source: this._similarityScript, - }, - }, - }, - }, - }, - }, - ]); - } - - removeFilter({ path, value, negate }) { - const currentFilters = filterManager.getFilters(); - const filterIndex = this.getFilterIndex({ path, value, negate }); - - if (filterIndex >= 0) { - filterManager.removeFilter(currentFilters[filterIndex]); - } - } -} - -export default FilterManagerHelper; diff --git a/public/hacks/custom_filter_bootstrap/index.js b/public/hacks/custom_filter_bootstrap/index.js deleted file mode 100644 index 9f0c842..0000000 --- a/public/hacks/custom_filter_bootstrap/index.js +++ /dev/null @@ -1,108 +0,0 @@ -import _ from 'lodash'; -import { uiModules } from 'ui/modules'; -import { npStart } from 'ui/new_platform'; -import '../../common/jquery-plugins/observer'; -import FilterManagerHelper from './FilterManagerHelper'; -import Popover from './Popover'; - -const app = uiModules.get('kibana'); - -const { - query: { filterManager }, - indexPatterns, -} = npStart.plugins.data; - -const popover = new Popover(); - -window.popover = popover; - -filterManager.register = (customFilter) => { - if (!filterManager.customFilters) { - filterManager.customFilters = []; - } - - filterManager.customFilters.push(customFilter); -}; - -app.run([ - '$rootScope', - ($rootScope) => { - $rootScope.$on('$routeChangeSuccess', (_$event, next) => { - const { - $$route: { originalPath }, - } = next; - - if (popover.isInit()) { - popover.destroy(); - } - - popover.init(); - }); - }, -]); - -(async (indexPatterns, addFiltersCached) => { - const indexPatternIDList = await indexPatterns.getFields(['id']); - let indexPatternLookup = {}; - - for (let pattern of indexPatternIDList) { - let ip = await indexPatterns.get(pattern.id); - indexPatternLookup[pattern.id] = ip.fieldFormatMap; - } - - const filterManagerHelper = new FilterManagerHelper(addFiltersCached); - - filterManager.addFilters = (newFilters) => { - if (_.isArray(newFilters) && newFilters.length === 0) { - return; - } - - for (let newFilter of newFilters) { - const selectedIndexPatternID = _.get(newFilter, 'meta.index', null); - const fieldFormatMap = indexPatternLookup[selectedIndexPatternID]; - const matchPhrase = _.get(newFilter, 'query.match_phrase', {}); - const fieldNameKeys = Object.keys(matchPhrase); - - const fieldName = fieldNameKeys.length === 1 ? fieldNameKeys[0] : ''; - const formatType = _.get(fieldFormatMap, [fieldName, 'type', 'id'], null); - const params = _.get(fieldFormatMap, [fieldName, '_params'], {}); - const values = _.get(matchPhrase, [fieldName], {}); - const meta = _.get(newFilter, 'meta', {}); - - const { - getCurrentFilters, - addFilter, - addImageSimilarityFilter, - removeFilter, - } = filterManagerHelper; - filterManagerHelper.newFilter = newFilter; - filterManagerHelper.similarityScript = _.get(params, 'similarityScript', ''); - - const filterParams = { - fieldName, - formatType, - params, - values, - meta, - popover, - addFilter: addFilter.bind(filterManagerHelper), - addImageSimilarityFilter: addImageSimilarityFilter.bind(filterManagerHelper), - removeFilter: removeFilter.bind(filterManagerHelper), - getCurrentFilters: getCurrentFilters.bind(filterManagerHelper), - }; - let customFilterFlag = false; - - for (let customFilter of filterManager.customFilters) { - customFilterFlag = customFilter(filterParams); - - if (customFilterFlag) { - break; - } - } - - if (!customFilterFlag) { - addFiltersCached.apply(filterManager, [newFilter]); - } - } - }; -})(indexPatterns, filterManager.addFilters); diff --git a/public/hacks/field_mapper_hack.js b/public/hacks/field_mapper_hack.js deleted file mode 100644 index a87a0e0..0000000 --- a/public/hacks/field_mapper_hack.js +++ /dev/null @@ -1,109 +0,0 @@ -import _ from 'lodash'; -import { uiModules } from 'ui/modules'; -import { npStart } from 'ui/new_platform'; -const { indexPatterns } = npStart.plugins.data; - -const app = uiModules.get('apps/management'); - -app.run([ - 'config', - '$rootScope', - '$timeout', - function(config, $rootScope, $timeout) { - $rootScope.$on('$routeChangeSuccess', function(_$event, next) { - const fieldsFetcher = _.get(next, 'locals.indexPattern.fieldsFetcher', null); - - if (fieldsFetcher) { - const { fetch: fieldsFunc } = fieldsFetcher; - (function(fieldsFunc) { - fieldsFetcher.fetch = () => { - - let indexPattern = {}; - - indexPatterns.getFields(['id', 'title']).then(ipList => { - const ipTitle = $('.euiTitle').text().trim(); - const ip = ipList.filter(ipItem => ipItem.title === ipTitle); - if (ip.length === 1) { - indexPattern = ip[0]; - } - }) - - const promise = fieldsFunc.apply(this, arguments); - - return promise.then(fields => { - let paths = []; - const mappingNames = []; - - // 1) Iterate the field names, identify the "parent" paths - _.forEach(fields, function (field) { - - const fieldName = field.name; - mappingNames.push(fieldName); - - const parts = fieldName.split('.'); - - while (parts.length > 1) { - parts.pop(); - paths.push(parts.join('.')); - } - }); - - paths = _.uniq(_.difference(paths, mappingNames)); - - // 2) Test the discovered field names against the configuration - const defaultConfig = '{\n \"index_pattern\": {\n \"*\": {\n' + - '\"include\": [],\n \"exclude\": [\".*\"]\n }\n }\n}'; - let { index_pattern: settings } = config.get('fieldMapperHack:fields', defaultConfig); - let match = { includes: [], excludes: [] }; - - if (_.has(settings, indexPattern.id)) { - match = settings[indexPattern.id]; - } - else if (_.has(settings, '*')) { - match = settings['*']; - } - - const included = []; - const excluded = []; - - _.forEach(match.include, function (expression) { - _.forEach(paths, function (path) { - if (path.match(expression)) { - included.push(path); - } - }); - }); - - _.forEach(match.exclude, function (expression) { - _.forEach(included, function (path) { - if (path.match(expression)) { - excluded.push(path); - } - }); - }); - - // 3) Add a field mapping for the missing parents - _.forEach(_.difference(included, excluded), function (path) { - fields.push({ - name: path, - aggregatable: false, - searchable: true, - analyzed: false, - doc_values: false, - indexed: true, - type: 'string' - }); - }); - - $timeout(() => { - $rootScope.$apply(); - }, 0); - - return fields; - }); - }; - })(fieldsFunc); - } - }); - }, -]); diff --git a/public/hacks/object_filter/index.js b/public/hacks/object_filter/index.js deleted file mode 100644 index f248cac..0000000 --- a/public/hacks/object_filter/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import { uiModules } from 'ui/modules'; -import { npStart } from 'ui/new_platform'; -import objectFilter from './objectFilter'; - -const app = uiModules.get('kibana'); -const { filterManager } = npStart.plugins.data.query; - -app.run(['config', (_config) => { - filterManager.register(objectFilter); -}]); \ No newline at end of file diff --git a/public/hacks/scroll_bug/index.js b/public/hacks/scroll_bug/index.js deleted file mode 100644 index b71667f..0000000 --- a/public/hacks/scroll_bug/index.js +++ /dev/null @@ -1 +0,0 @@ -import './scroll_bug.less'; \ No newline at end of file diff --git a/public/hacks/scroll_bug/scroll_bug.less b/public/hacks/scroll_bug/scroll_bug.less deleted file mode 100644 index 6593c92..0000000 --- a/public/hacks/scroll_bug/scroll_bug.less +++ /dev/null @@ -1,5 +0,0 @@ -@-moz-document url-prefix() { - .euiBody-hasPortalContent { - position: static !important; - } -} \ No newline at end of file diff --git a/public/index.scss b/public/index.scss new file mode 100644 index 0000000..b055017 --- /dev/null +++ b/public/index.scss @@ -0,0 +1,2 @@ +@import 'object_field/ObjectFieldFormat'; +@import 'bootstrap/custom_filter_manager/Popover'; diff --git a/public/index.ts b/public/index.ts new file mode 100644 index 0000000..6d28506 --- /dev/null +++ b/public/index.ts @@ -0,0 +1,11 @@ +import './index.scss'; + +import { KibanaObjectFormatPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. +export function plugin() { + return new KibanaObjectFormatPlugin(); +} +export { StartPlugins, SetupPlugins, PluginSetup, PluginStart } from './types'; +export * from './utils'; \ No newline at end of file diff --git a/public/field_formats/object/object.less b/public/object_field/ObjectFieldFormat.scss similarity index 100% rename from public/field_formats/object/object.less rename to public/object_field/ObjectFieldFormat.scss diff --git a/public/object_field/ObjectFieldFormat.ts b/public/object_field/ObjectFieldFormat.ts new file mode 100644 index 0000000..58a5f82 --- /dev/null +++ b/public/object_field/ObjectFieldFormat.ts @@ -0,0 +1,208 @@ +import { get, isObject, escape, isArray, compact, slice, template, TemplateExecutor } from 'lodash'; +import { + HtmlContextTypeConvert, + TextContextTypeConvert, +} from '../../../../src/plugins/data/common/field_formats/types'; +import { FieldFormat } from '../../../../src/plugins/data/public'; +import { getFullPath, getPluck, asPrettyString, getHighlightHtml, generateUuids } from '../utils'; +import formatHTML from './templates/object_format.html'; +import imageHTML from './templates/object_image.html'; +import linkHTML from './templates/object_link.html'; +import textHTML from './templates/object_text.html'; +import emptyHTML from './templates/object_empty.html'; +import { ObjectFieldParams, ObjectField, ObjectFieldType } from '../types'; + +const DEFAULT_VALUES: ObjectField = { + label: undefined, // Optional data label + path: undefined, // Dot notated location of the value within the object, relative to basePath + type: ObjectFieldType.TEXT, + filtered: true, // To enable the filtering on cell click + dHashField: undefined, + filterField: undefined, // If the data is analyzed, and there is a keyword subfield we can use for the filter + height: undefined, // Image dimension in px + width: undefined, // Image dimension in px + limit: undefined, // If presenting an array, this is the max we will show +}; + +export class ObjectFieldFormat extends FieldFormat { + static id = 'ist-object'; + static title = 'Object'; + static fieldType = ['string']; + + getParamDefaults(): ObjectFieldParams { + return { + fieldType: undefined, // populated by editor, see controller + basePath: undefined, // If multiple fields should be grouped, this is the common parent + limit: undefined, // // If basePath is an array, this is the max we will show + similarityScript: undefined, + fields: [{ ...DEFAULT_VALUES }], + }; + } + + private fieldToHtml(field: any) { + let tmplhtml: TemplateExecutor; + + switch (field.formatType) { + case ObjectFieldType.IMAGE: + tmplhtml = template(imageHTML); + break; + case ObjectFieldType.LINK: + tmplhtml = template(linkHTML); + break; + case ObjectFieldType.TEXT: + tmplhtml = template(textHTML); + break; + default: + tmplhtml = template(textHTML); + } + + return tmplhtml({ field }); + } + + private getFieldModels({ + value = {}, + field = {}, + hit, + basePath, + objectFields, + }: { + value: any; + field: any; + hit?: any; + basePath?: string; + objectFields: ObjectField[]; + }) { + const fields = []; + + for (let objectField of objectFields) { + const { + path = '', + label, + limit, + type, + filterField, + filtered = false, + width, + height, + } = objectField; + + let fieldValues = getPluck(value, path); + + if (type === ObjectFieldType.TEXT) { + if (isArray(fieldValues)) { + if (objectField.limit) { + fieldValues = slice(fieldValues, 0, objectField.limit); + } + + fieldValues = fieldValues + .map((fieldValue: string) => + isObject(fieldValue) ? JSON.stringify(fieldValue) : fieldValue + ) + .join(', '); + } else if (isObject(fieldValues)) { + fieldValues = JSON.stringify(fieldValues); + } + } + + if (!isArray(fieldValues)) { + fieldValues = [fieldValues]; + } + + if (limit) { + fieldValues = slice(fieldValues, 0, limit); + } + + const fullPath = getFullPath({ basePath, field, path }); + getHighlightHtml; + const filterPath = getFullPath({ + basePath, + field, + path, + filterField, + }); + + const valueModels = []; + + for (let fieldValue of fieldValues) { + const valueModel = { + value: fieldValue, + display: escape(fieldValue), + }; + + if (hit?.highlight && hit.highlight[fullPath]) { + valueModel.display = getHighlightHtml(valueModel.display, hit.highlight[fullPath]); + } else if (hit?.highlight && hit.highlight[filterPath]) { + valueModel.display = getHighlightHtml(valueModel.display, hit.highlight[filterPath]); + } + + valueModels.push(valueModel); + } + + const html = this.fieldToHtml({ + label: label ? `${label}:` : '', + formatType: type, + values: valueModels, + width, + height, + filterField, + }); + + fields.push({ + formatType: type, + label: label ? `${label}:` : '', + html, + }); + + return { filtered: filtered, fields: fields }; + } + } + + htmlConvert: HtmlContextTypeConvert = (rawValue, options = {}) => { + const { field, hit } = options; + const visTemplate = template(formatHTML); + const emptyTemplate = template(emptyHTML); + const basePath = this.param('basePath'); + const objectFields = this.param('fields'); + const limit = this.param('limit'); + + if (basePath) { + rawValue = get(rawValue, basePath); + } + + if (!isArray(rawValue)) { + rawValue = [rawValue]; + } + + // Filter out any null or empty entries + rawValue = compact(rawValue); + + if (limit) { + rawValue = slice(rawValue, 0, limit); + } + + if (rawValue.length > 0) { + const htmlSnippets = []; + + for (let value of rawValue) { + if (value) { + const fieldModels = this.getFieldModels({ value, field, hit, basePath, objectFields }); + + htmlSnippets.push( + visTemplate({ + ...fieldModels, + uid: generateUuids()[0], + }) + ); + } + } + + return htmlSnippets.join('\n'); + } else { + return emptyTemplate(); + } + }; + + textConvert: TextContextTypeConvert = (rawValue) => { + return asPrettyString(rawValue); + }; +} diff --git a/public/field_formats/object/editor.js b/public/object_field/ObjectFieldFormatEditor.tsx similarity index 65% rename from public/field_formats/object/editor.js rename to public/object_field/ObjectFieldFormatEditor.tsx index 4f1a402..e1d1e02 100644 --- a/public/field_formats/object/editor.js +++ b/public/object_field/ObjectFieldFormatEditor.tsx @@ -1,29 +1,84 @@ import React, { Fragment } from 'react'; import { EuiBasicTable, - EuiButton, EuiSpacer, EuiFieldText, + EuiButton, EuiTextArea, EuiFormRow, EuiFieldNumber, EuiSelect, EuiCheckbox, } from '@elastic/eui'; -import { DefaultFormatEditor } from '../../../../../src/legacy/ui/public/field_editor/components/field_format_editor/editors/default'; -import { ID, DEFAULT_VALUES, FORMAT_TYPES } from './constants'; +import { FieldFormat } from '../../../../src/plugins/data/public'; +import { DefaultFormatEditor } from '../../../../src/plugins/index_pattern_management/public'; + +/* TO-DO: Elastic Teams needs to expose FormatEditorProps to the public. Embedding here for now. */ +interface FormatEditorProps

{ + fieldType: string; + format: FieldFormat; + formatParams: { type?: string } & P; + onChange: (newParams: Record) => void; + onError: any; +} -export class ObjectFormatEditor extends DefaultFormatEditor { - static formatId = ID; +interface ObjectField { + type?: string; + label?: string; + path?: string; + dHashField?: string; + filterField?: string; + limit?: number; + filtered?: boolean; + width?: number; + height?: number; +} + +interface IndexedObjectField extends ObjectField { + index: number; +} - constructor(props) { +interface ObjectFieldEditorFormatParams { + fields: ObjectField[]; + basePath?: string; + limit?: number; + similarityScript?: string; +} + +interface FormatValue { + id: string; + name: string; +} + +const DEFAULT_VALUES: ObjectField = { + type: 'text', + label: undefined, // Optional data label + path: undefined, // Dot notated location of the value within the object, relative to basePath + filtered: true, // To enable the filtering on cell click + dHashField: undefined, + filterField: undefined, // If the data is analyzed, and there is a keyword subfield we can use for the filter + height: undefined, // Image dimension in px + width: undefined, // Image dimension in px + limit: undefined // If presenting an array, this is the max we will show +}; + +export const FORMAT_TYPES: FormatValue[] = [ + { id: 'text', name: 'Text' }, + { id: 'link', name: 'Link' }, + { id: 'image', name: 'Image' } +]; + +export class ObjectFieldFormatEditor extends DefaultFormatEditor { + static formatId = 'ist-object'; + + constructor(props: FormatEditorProps) { super(props); this.onChange({ fieldType: props.fieldType, }); } - onFieldChange = (newFieldParams, index) => { + onFieldChange = (newFieldParams: Partial, index: number) => { const fields = [...this.props.formatParams.fields]; fields[index] = { ...fields[index], @@ -35,13 +90,13 @@ export class ObjectFormatEditor extends DefaultFormatEditor { }; addField = () => { - const fields = [...this.props.formatParams.fields]; + const fields = [...(this.props.formatParams.fields || [])]; this.onChange({ fields: [...fields, { ...DEFAULT_VALUES }], }); }; - removeField = index => { + removeField = (index: number) => { const fields = [...this.props.formatParams.fields]; fields.splice(index, 1); this.onChange({ @@ -67,17 +122,17 @@ export class ObjectFormatEditor extends DefaultFormatEditor { { field: 'type', name: 'Format', - render: (value, item) => { + render: (value: string, item: IndexedObjectField) => { return ( { + options={FORMAT_TYPES.map((type) => { return { value: type.id, text: type.name, }; })} - onChange={e => { + onChange={(e) => { this.onFieldChange( { type: e.target.value, @@ -92,11 +147,11 @@ export class ObjectFormatEditor extends DefaultFormatEditor { { field: 'label', name: 'Label', - render: (value, item) => { + render: (value: string, item: IndexedObjectField) => { return ( { + onChange={(e) => { this.onFieldChange( { label: e.target.value, @@ -111,11 +166,11 @@ export class ObjectFormatEditor extends DefaultFormatEditor { { field: 'path', name: 'Field', - render: (value, item) => { + render: (value: string, item: IndexedObjectField) => { return ( { + onChange={(e) => { this.onFieldChange( { path: e.target.value, @@ -130,11 +185,11 @@ export class ObjectFormatEditor extends DefaultFormatEditor { { field: 'dHashField', name: 'Hash Field', - render: (value, item) => { + render: (value: string, item: IndexedObjectField) => { return ( { + onChange={(e) => { this.onFieldChange( { dHashField: e.target.value, @@ -149,11 +204,11 @@ export class ObjectFormatEditor extends DefaultFormatEditor { { field: 'filterField', name: 'Filter Field', - render: (value, item) => { + render: (value: string, item: IndexedObjectField) => { return ( { + onChange={(e) => { this.onFieldChange( { filterField: e.target.value, @@ -169,14 +224,14 @@ export class ObjectFormatEditor extends DefaultFormatEditor { field: 'limit', name: 'Array Limit', width: '100px', - render: (value, item) => { + render: (value: number, item: IndexedObjectField) => { return ( { + onChange={(e) => { this.onFieldChange( { - limit: e.target.value ? Number(e.target.value) : null, + limit: e.target.value ? Number(e.target.value) : undefined, }, item.index ); @@ -189,11 +244,12 @@ export class ObjectFormatEditor extends DefaultFormatEditor { field: 'filtered', name: 'Filter', width: '60px', - render: (value, item) => { + render: (value: boolean, item: IndexedObjectField) => { return ( { + onChange={(e) => { this.onFieldChange( { filtered: e.target.checked, @@ -209,14 +265,14 @@ export class ObjectFormatEditor extends DefaultFormatEditor { field: 'width', name: 'Width', width: '80px', - render: (value, item) => { + render: (value: number, item: IndexedObjectField) => { return item.type === 'image' ? ( { + onChange={(e) => { this.onFieldChange( { - width: e.target.value ? Number(e.target.value) : null, + width: e.target.value ? Number(e.target.value) : undefined, }, item.index ); @@ -229,14 +285,14 @@ export class ObjectFormatEditor extends DefaultFormatEditor { field: 'height', name: 'Height', width: '80px', - render: (value, item) => { + render: (value: number, item: IndexedObjectField) => { return item.type === 'image' ? ( { + onChange={(e) => { this.onFieldChange( { - height: e.target.value ? Number(e.target.value) : null, + height: e.target.value ? Number(e.target.value) : undefined, }, item.index ); @@ -246,12 +302,14 @@ export class ObjectFormatEditor extends DefaultFormatEditor { }, }, { + field: 'actions', + name: '', width: '40px', actions: [ { name: 'Delete', description: 'Delete Field', - onClick: item => { + onClick: (item: IndexedObjectField) => { this.removeField(item.index); }, type: 'icon', @@ -268,7 +326,7 @@ export class ObjectFormatEditor extends DefaultFormatEditor { { + onChange={(e) => { this.onChange({ basePath: e.target.value }); }} /> @@ -276,8 +334,8 @@ export class ObjectFormatEditor extends DefaultFormatEditor { { - this.onChange({ limit: e.target.value ? Number(e.target.value) : null }); + onChange={(e) => { + this.onChange({ limit: e.target.value ? Number(e.target.value) : undefined }); }} /> @@ -292,7 +350,7 @@ export class ObjectFormatEditor extends DefaultFormatEditor { { + onChange={(e) => { this.onChange({ similarityScript: e.target.value }); }} /> diff --git a/public/object_field/index.ts b/public/object_field/index.ts new file mode 100644 index 0000000..6a17a95 --- /dev/null +++ b/public/object_field/index.ts @@ -0,0 +1,3 @@ +export { objectFieldFilter } from './objectFieldFilter'; +export { ObjectFieldFormat } from './ObjectFieldFormat'; +export { ObjectFieldFormatEditor } from './ObjectFieldFormatEditor'; diff --git a/public/hacks/object_filter/objectFilter.js b/public/object_field/objectFieldFilter.ts similarity index 65% rename from public/hacks/object_filter/objectFilter.js rename to public/object_field/objectFieldFilter.ts index 0b38956..19aa764 100644 --- a/public/hacks/object_filter/objectFilter.js +++ b/public/object_field/objectFieldFilter.ts @@ -1,7 +1,7 @@ -/* Placeholder for Filter Hack */ -import _ from 'lodash'; +import { isArray, get } from 'lodash'; +import { getPluck } from '../utils'; -export default ({ +export const objectFieldFilter = ({ fieldName, formatType, params, @@ -12,21 +12,25 @@ export default ({ removeFilter, getCurrentFilters, popover, -}) => { +}: any) => { if (formatType !== 'ist-object') { return false; } const { basePath, limit: baseLimit } = params; - let vals = basePath ? _.get(values, basePath) : values; + let vals = basePath ? get(values, basePath) : values; - if (!_.isArray(vals)) { + if (!isArray(vals)) { vals = [vals]; } const entryValues = []; - for (let i = 0, len = baseLimit && vals.length >= baseLimit ? baseLimit : vals.length; i < len; i++) { + for ( + let i = 0, len = baseLimit && vals.length >= baseLimit ? baseLimit : vals.length; + i < len; + i++ + ) { let val = vals[i]; for (let fieldEntry of params.fields) { @@ -42,49 +46,54 @@ export default ({ fullDHashFieldPath = `${fullDHashFieldPath}.${filterField}`; } - let fieldValues = _.getPluck(val, path, null); - let dHashValues = _.getPluck(val, dHashField, null); + let fieldValues = getPluck(val, path); + let dHashValues = getPluck(val, dHashField); - if (!_.isArray(fieldValues)) { + if (!isArray(fieldValues)) { fieldValues = [fieldValues]; } - if (!_.isArray(dHashValues)) { + if (!isArray(dHashValues)) { dHashValues = [dHashValues]; } - for (let i = 0, len = fieldLimit && fieldValues.length >= fieldLimit ? fieldLimit : fieldValues.length; i < len; i++) { + for ( + let i = 0, + len = fieldLimit && fieldValues.length >= fieldLimit ? fieldLimit : fieldValues.length; + i < len; + i++ + ) { let fieldValue = fieldValues.length > i ? fieldValues[i] : null; let dHashValue = dHashValues.length > i ? dHashValues[i] : null; entryValues.push({ - ...fieldEntry, - negate: meta.negate, - path: fullFieldPath, - value: fieldValue, + ...fieldEntry, + negate: meta.negate, + path: fullFieldPath, + value: fieldValue, dHashPath: dHashValue && fullDHashFieldPath, dHashValue, - }); + }); } } } - if (entryValues.length > 1 || entryValues.filter(v => !!v.dHashValue).length) { + if (entryValues.length > 1 || entryValues.filter((v) => !!v.dHashValue).length) { const currentFilters = getCurrentFilters(); - - popover.setForm(entryValues, currentFilters, formValues => { + + popover.setForm(entryValues, currentFilters, (formValues: any) => { for (let formValue of formValues) { let { path, value, dHashPath, dHashValue, distance, negate, checked } = formValue; if (checked) { if (dHashValue) { - addImageSimilarityFilter({ path: dHashPath, value: dHashValue, distance }) + addImageSimilarityFilter({ path: dHashPath, value: dHashValue, distance }); } else { addFilter({ path, value, negate }); } } else { removeFilter({ path: dHashPath || path, value: dHashValue || value, negate }); } - } + } }); } else if (entryValues.length === 1) { popover.hide(); diff --git a/public/field_formats/object/templates/object_empty.html b/public/object_field/templates/object_empty.html similarity index 100% rename from public/field_formats/object/templates/object_empty.html rename to public/object_field/templates/object_empty.html diff --git a/public/field_formats/object/templates/object_format.html b/public/object_field/templates/object_format.html similarity index 100% rename from public/field_formats/object/templates/object_format.html rename to public/object_field/templates/object_format.html diff --git a/public/field_formats/object/templates/object_image.html b/public/object_field/templates/object_image.html similarity index 100% rename from public/field_formats/object/templates/object_image.html rename to public/object_field/templates/object_image.html diff --git a/public/field_formats/object/templates/object_link.html b/public/object_field/templates/object_link.html similarity index 100% rename from public/field_formats/object/templates/object_link.html rename to public/object_field/templates/object_link.html diff --git a/public/field_formats/object/templates/object_text.html b/public/object_field/templates/object_text.html similarity index 100% rename from public/field_formats/object/templates/object_text.html rename to public/object_field/templates/object_text.html diff --git a/public/plugin.ts b/public/plugin.ts new file mode 100644 index 0000000..d9e05dd --- /dev/null +++ b/public/plugin.ts @@ -0,0 +1,68 @@ +import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { StartPlugins, PluginStart, SetupPlugins, PluginSetup } from './types'; +import { + fieldMapper, + addFilters, + initFilterManager, + initPopover, + setupAddFilters, + updateFieldTemplate, +} from './bootstrap'; +import { ObjectFieldFormat, ObjectFieldFormatEditor, objectFieldFilter } from './object_field'; +import { Filter, GetFieldsOptions } from '../../../src/plugins/data/common'; +import './utils/jqueryObserver'; + +export class KibanaObjectFormatPlugin + implements Plugin { + public setup(core: CoreSetup, pluginSetup: SetupPlugins): PluginSetup { + pluginSetup.indexPatternManagement.fieldFormatEditors.register(ObjectFieldFormatEditor); + + core.getStartServices().then(([{ uiSettings }, { data }]) => { + data.fieldFormats.register([ObjectFieldFormat]); + const indexPatterns = data?.indexPatterns; + + ((getFieldsForWildcardCached) => { + indexPatterns.getFieldsForWildcard = async (options: GetFieldsOptions) => { + const fields = await getFieldsForWildcardCached(options); + const { pattern } = options; + + return fieldMapper({ uiSettings, fields, pattern }); + }; + })(indexPatterns.getFieldsForWildcard); + + const filterManager = data?.query?.filterManager; + initFilterManager(filterManager); + + (async (addFiltersCached) => { + await setupAddFilters({ + indexPatterns, + addFiltersCached, + filterManager, + }); + + filterManager.addFilters = async (filters: Filter | Filter[]) => { + await addFilters({ newFilters: filters, addFiltersCached, filterManager, indexPatterns }); + }; + })(filterManager?.addFilters); + }); + + return {}; + } + + public start(core: CoreStart, { data }: StartPlugins) { + let init = false; + + core.application.currentAppId$.subscribe(() => { + if (!init) { + data.query.filterManager.register(objectFieldFilter); + } + initPopover(); + updateFieldTemplate(true); + init = true; + }); + + return {}; + } + + public stop() {} +} diff --git a/public/types.ts b/public/types.ts new file mode 100644 index 0000000..5f54fcc --- /dev/null +++ b/public/types.ts @@ -0,0 +1,78 @@ +import { FilterMeta } from 'src/plugins/data/common'; +import { + DataPublicPluginStart, + Filter, + FilterManager, + IndexPatternsContract, +} from '../../../src/plugins/data/public'; +import { IndexPatternManagementSetup } from '../../../src/plugins/index_pattern_management/public'; + +interface filterManagerAddons { + query: { + filterManager: { + register: any; + }; + }; +} + +export interface StartPlugins { + data: DataPublicPluginStart & filterManagerAddons; +} + +export interface SetupPlugins { + indexPatternManagement: IndexPatternManagementSetup; +} + +export interface PluginSetup {} + +export interface PluginStart {} + +export interface CustomFilterManager extends FilterManager { + register: (customFilter: any) => void; + customFilters: any[]; +} + +export interface CustomFilterProps { + filterManager: Partial; + newFilters: Filter | Filter[]; + addFiltersCached: (filters: Filter | Filter[], pinFilterStatus?: boolean) => void; + indexPatterns: IndexPatternsContract; +} + +export interface CustomFilterMeta extends FilterMeta { + path: string; + distance: string; +} + +export interface EntryValues extends FilterMeta { + path: string; + distance: string; +} + +export enum ObjectFieldType { + IMAGE = 'image', + LINK = 'link', + TEXT = 'text', +} + +export interface ObjectField { + label?: string; + path?: string; + type: ObjectFieldType; + filtered: boolean; + dHashField?: string; + filterField?: string; + height?: number; + width?: number; + limit?: number; +} + +export interface ObjectFieldParams { + fieldType?: string; + basePath?: string; + limit?: number; + similarityScript?: string; + fields?: ObjectField[]; +} + +export { Filter }; diff --git a/public/utils/asPrettyString.ts b/public/utils/asPrettyString.ts new file mode 100644 index 0000000..52cdbe6 --- /dev/null +++ b/public/utils/asPrettyString.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * Convert a value to a presentable string + */ +export function asPrettyString(val: any): string { + if (val === null || val === undefined) return ' - '; + switch (typeof val) { + case 'string': + return val; + case 'object': + return JSON.stringify(val, null, ' '); + default: + return '' + val; + } +} diff --git a/public/utils/getFullPath.ts b/public/utils/getFullPath.ts new file mode 100644 index 0000000..e3141f7 --- /dev/null +++ b/public/utils/getFullPath.ts @@ -0,0 +1,25 @@ +export const getFullPath = ({ + basePath, + field, + path, + filterField, +}: { + field: { name: string }; + path: string; + basePath?: string; + filterField?: string; +}) => { + const parts = [field.name]; + + if (basePath) { + parts.push(basePath); + } + + parts.push(path); + + if (filterField) { + parts.push(filterField); + } + + return parts.join('.'); +}; diff --git a/public/utils/getPluck.ts b/public/utils/getPluck.ts new file mode 100644 index 0000000..defca63 --- /dev/null +++ b/public/utils/getPluck.ts @@ -0,0 +1,36 @@ +import { isArray, has, slice, map, property, toPath, isObject } from 'lodash'; + +const toObject = (value: any) => { + return isObject(value) ? value : Object(value); +}; + +const baseGetWithPluck = (object: any, path: any, pathKey: string) => { + if (object == null) { + return; + } + + if (pathKey !== undefined && pathKey in toObject(object)) { + path = [pathKey]; + } + + let index = 0; + const length = path.length; + + while (object != null && index < length) { + const key = path[index++]; + + if (isArray(object) && !has(object, key)) { + object = map(object, property(slice(path, index - 1))); + index = length; + } else { + object = object[key]; + } + } + + return index && index === length ? object : undefined; +}; + +export const getPluck = (object: any, path: any, defaultValue?: string) => { + const result = object == null ? undefined : baseGetWithPluck(object, toPath(path), path + ''); + return result === undefined ? defaultValue : result; +}; diff --git a/public/utils/highlight/highlight_html.ts b/public/utils/highlight/highlight_html.ts new file mode 100644 index 0000000..250e238 --- /dev/null +++ b/public/utils/highlight/highlight_html.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import _ from 'lodash'; +import { highlightTags } from './highlight_tags'; +import { htmlTags } from './html_tags'; + +export function getHighlightHtml(fieldValue: any, highlights: any) { + let highlightHtml = typeof fieldValue === 'object' ? JSON.stringify(fieldValue) : fieldValue; + + _.each(highlights, function (highlight) { + const escapedHighlight = _.escape(highlight); + + // Strip out the highlight tags to compare against the field text + const untaggedHighlight = escapedHighlight + .split(highlightTags.pre) + .join('') + .split(highlightTags.post) + .join(''); + + // Replace all highlight tags with proper html tags + const taggedHighlight = escapedHighlight + .split(highlightTags.pre) + .join(htmlTags.pre) + .split(highlightTags.post) + .join(htmlTags.post); + + // Replace all instances of the untagged string with the properly tagged string + highlightHtml = highlightHtml.split(untaggedHighlight).join(taggedHighlight); + }); + + return highlightHtml; +} diff --git a/public/utils/highlight/highlight_request.ts b/public/utils/highlight/highlight_request.ts new file mode 100644 index 0000000..fb87c29 --- /dev/null +++ b/public/utils/highlight/highlight_request.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { highlightTags } from './highlight_tags'; + +const FRAGMENT_SIZE = Math.pow(2, 31) - 1; // Max allowed value for fragment_size (limit of a java int) + +export function getHighlightRequest(query: any, shouldHighlight: boolean) { + if (!shouldHighlight) return; + + return { + pre_tags: [highlightTags.pre], + post_tags: [highlightTags.post], + fields: { + '*': {}, + }, + fragment_size: FRAGMENT_SIZE, + }; +} diff --git a/public/utils/highlight/highlight_tags.ts b/public/utils/highlight/highlight_tags.ts new file mode 100644 index 0000000..4e86981 --- /dev/null +++ b/public/utils/highlight/highlight_tags.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// By default, ElasticSearch surrounds matched values in . This is not ideal because it is possible that +// the value could contain in the value. We define these custom tags that we would never expect to see +// inside a field value. +export const highlightTags = { + pre: '@kibana-highlighted-field@', + post: '@/kibana-highlighted-field@', +}; diff --git a/public/utils/highlight/html_tags.ts b/public/utils/highlight/html_tags.ts new file mode 100644 index 0000000..856ffcc --- /dev/null +++ b/public/utils/highlight/html_tags.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// These are the html tags that will replace the highlight tags. +export const htmlTags = { + pre: '', + post: '', +}; diff --git a/public/utils/highlight/index.ts b/public/utils/highlight/index.ts new file mode 100644 index 0000000..ff6ed95 --- /dev/null +++ b/public/utils/highlight/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { getHighlightHtml } from './highlight_html'; +export { getHighlightRequest } from './highlight_request'; diff --git a/public/utils/index.ts b/public/utils/index.ts new file mode 100644 index 0000000..a44a5ff --- /dev/null +++ b/public/utils/index.ts @@ -0,0 +1,5 @@ +export { getFullPath } from './getFullPath'; +export { getPluck } from './getPluck'; +export { asPrettyString } from './asPrettyString'; +export { generateUuids } from './uuidGenerator'; +export { getHighlightHtml, getHighlightRequest } from './highlight'; diff --git a/public/common/jquery-plugins/observer.js b/public/utils/jqueryObserver.ts similarity index 95% rename from public/common/jquery-plugins/observer.js rename to public/utils/jqueryObserver.ts index 86fdc2d..b9771a5 100644 --- a/public/common/jquery-plugins/observer.js +++ b/public/utils/jqueryObserver.ts @@ -1,5 +1,11 @@ +// @ts-nocheck // https://github.com/rkusa/jquery-observe - A simple mutation observer for jQuery. // aaxelrod - added disconnect functionality to jquery $.fn + +interface JQuery { + observe(nodes: any, callback: any): void; +} + !function($) { var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver var Observer = function(target, selector, onAdded, onRemoved) { diff --git a/public/utils/uuidGenerator.ts b/public/utils/uuidGenerator.ts new file mode 100644 index 0000000..dade122 --- /dev/null +++ b/public/utils/uuidGenerator.ts @@ -0,0 +1,9 @@ +import { v4 } from 'uuid'; + +export const generateUuids = (count = 1) => { + const uuids: any = []; + for (let i = 0; i < count; i++) { + uuids.push(v4()); + } + return uuids; +}; diff --git a/server/field-formatters/ObjectFieldFormatStub.js b/server/field-formatters/ObjectFieldFormatStub.js deleted file mode 100644 index efb5acc..0000000 --- a/server/field-formatters/ObjectFieldFormatStub.js +++ /dev/null @@ -1,15 +0,0 @@ -import { FieldFormat } from '../../../../src/plugins/data/common/field_formats/field_format'; - -export class ObjectFieldFormatStub extends FieldFormat { - constructor(params) { - super(params); - } - - static id = 'ist-object'; - static title = 'Object'; - - getParamDefaults() { - return {}; - } -} - \ No newline at end of file diff --git a/server/field-formatters/index.js b/server/field-formatters/index.js deleted file mode 100644 index 7f5591d..0000000 --- a/server/field-formatters/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import { ObjectFieldFormatStub } from './ObjectFieldFormatStub'; - -export function registerFieldFormats(server) { - server.newPlatform.setup.plugins.data.fieldFormats.register(ObjectFieldFormatStub); -} diff --git a/server/index.ts b/server/index.ts new file mode 100644 index 0000000..ce4ce26 --- /dev/null +++ b/server/index.ts @@ -0,0 +1,11 @@ +import { PluginInitializerContext } from '../../../src/core/server'; +import { KibanaObjectFormatPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. + +export function plugin(initializerContext: PluginInitializerContext) { + return new KibanaObjectFormatPlugin(initializerContext); +} + +export { KibanaObjectFormatPluginSetup, KibanaObjectFormatPluginStart } from './types'; diff --git a/server/plugin.ts b/server/plugin.ts new file mode 100644 index 0000000..24436b9 --- /dev/null +++ b/server/plugin.ts @@ -0,0 +1,58 @@ +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + Logger, +} from '../../../src/core/server'; +import { schema } from '@kbn/config-schema'; +import { KibanaObjectFormatPluginSetup, KibanaObjectFormatPluginStart } from './types'; + +const FIELD_MAPPER_HACK_DEFAULT = ` + { + "index_pattern": { + "*": { + "include": [], + "exclude": [".*"] + } + } + } +`; + +export class KibanaObjectFormatPlugin + implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + this.logger.debug('kibana_object_format: Setup'); + core.uiSettings.register({ + 'ObjectFieldMapper:fields': { + name: 'Object Field Mapper', + value: FIELD_MAPPER_HACK_DEFAULT, + schema: schema.object({ + index_pattern: schema.object({ + '*': schema.object({ + include: schema.arrayOf(schema.string()), + exclude: schema.arrayOf(schema.string()), + }) + }), + }), + type: 'json', + description: 'Configure field formatters for objects and arrays of objects by declaring the patterns and fields. See the kibana-object-formatter plugin project.', + }, + }); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('kibana_object_format: Started'); + return {}; + } + + public stop() { } +} diff --git a/server/types.ts b/server/types.ts new file mode 100644 index 0000000..8c4232e --- /dev/null +++ b/server/types.ts @@ -0,0 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface KibanaObjectFormatPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface KibanaObjectFormatPluginStart {} diff --git a/translations/ja-JP.json b/translations/ja-JP.json new file mode 100644 index 0000000..216fbd0 --- /dev/null +++ b/translations/ja-JP.json @@ -0,0 +1,81 @@ +{ + "formats": { + "number": { + "currency": { + "style": "currency" + }, + "percent": { + "style": "percent" + } + }, + "date": { + "short": { + "month": "numeric", + "day": "numeric", + "year": "2-digit" + }, + "medium": { + "month": "short", + "day": "numeric", + "year": "numeric" + }, + "long": { + "month": "long", + "day": "numeric", + "year": "numeric" + }, + "full": { + "weekday": "long", + "month": "long", + "day": "numeric", + "year": "numeric" + } + }, + "time": { + "short": { + "hour": "numeric", + "minute": "numeric" + }, + "medium": { + "hour": "numeric", + "minute": "numeric", + "second": "numeric" + }, + "long": { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short" + }, + "full": { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short" + } + }, + "relative": { + "years": { + "units": "year" + }, + "months": { + "units": "month" + }, + "days": { + "units": "day" + }, + "hours": { + "units": "hour" + }, + "minutes": { + "units": "minute" + }, + "seconds": { + "units": "second" + } + } + }, + "messages": { + "kibanaObjectFormat.buttonText": "Translate me to Japanese" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7fa0373 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "common/**/*.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../typings/**/*" + ], + "exclude": [] +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..0826d92 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,20 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@popperjs/core@^2.8.3": + version "2.8.6" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.8.6.tgz#ad75ebe8dbecfa145af3c7e4d0ae98016458d005" + integrity sha512-1oXH2bAFXz9SttE1v/0Jp+2ZVePsPEAPGIuPKrmljWZcS3FPBEn2Q4WcANozZC0YiCjTWOF55k0g6rbSZS39ew== + +tippy.js@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.1.tgz#3788a007be7015eee0fd589a66b98fb3f8f10181" + integrity sha512-JnFncCq+rF1dTURupoJ4yPie5Cof978inW6/4S6kmWV7LL9YOSEVMifED3KdrVPEG+Z/TFH2CDNJcQEfaeuQww== + dependencies: + "@popperjs/core" "^2.8.3" + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==