├── .eslintrc.js ├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── common └── index.ts ├── docs ├── GETTING_STARTED.md ├── sample_compare_results.jpg ├── sample_custom_analyzer.jpg ├── sample_image.jpg ├── sample_show_token_attrs.jpg └── sample_specify_field.jpg ├── kibana.json ├── package.json ├── public ├── application.tsx ├── common │ └── constants │ │ ├── tab_names.tsx │ │ └── token_properties.tsx ├── components │ ├── analyze_ui │ │ ├── analyze_ui.tsx │ │ └── index.ts │ ├── analyzer_form │ │ ├── analyzer_form.tsx │ │ ├── compare_analyzers.tsx │ │ ├── custom.tsx │ │ ├── field.tsx │ │ ├── index.ts │ │ └── simple.tsx │ ├── app.tsx │ ├── common_form │ │ ├── common_form.tsx │ │ └── index.ts │ ├── form_control │ │ ├── form_control.tsx │ │ └── index.ts │ └── result │ │ ├── analyzer_result │ │ ├── analyzer_result.tsx │ │ ├── charfilters │ │ │ ├── chafilters.tsx │ │ │ └── index.ts │ │ ├── index.ts │ │ └── tokenizer_and_filters │ │ │ ├── filters.tsx │ │ │ ├── index.ts │ │ │ ├── token.tsx │ │ │ ├── tokenizer.tsx │ │ │ └── tokenizer_and_filters.tsx │ │ ├── compare_result │ │ ├── compare_result.tsx │ │ └── index.ts │ │ ├── index.ts │ │ └── result.tsx ├── index.scss ├── index.ts ├── plugin.ts ├── services │ ├── api.ts │ ├── pagestate.ts │ ├── params.ts │ ├── state_handler.ts │ ├── storage.ts │ ├── token_utils.ts │ └── validator.ts └── types.ts ├── server ├── index.ts ├── plugin.ts ├── routes │ ├── handle_es_error.ts │ └── index.ts └── types.ts └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'], 4 | rules: { 5 | '@kbn/eslint/require-license-header': 'off', 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: johtani -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log* 2 | node_modules 3 | /build 4 | /target 5 | .idea 6 | *.iml 7 | .DS_store 8 | /yarn.lock 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # analyze-api-ui-plugin 2 | 3 | > UI for elasticsearch analyze API 4 | 5 | Analyze text with Analyzer 6 | ![Sample image](docs/sample_image.jpg) 7 | 8 | This branch is for Kibana 7.x. 9 | 10 | ## How to use? 11 | 12 | See [Getting Started](docs/GETTING_STARTED.md) 13 | 14 | ## Installation 15 | The latest stable version is for kibana 7.17.0. 16 | 17 | | Kibana version | Command | 18 | |----------------|---------------------------------------------------------------------------------------------------------------------------------| 19 | | 7.17.0 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/7.17.0/analyzeApiUi-7.17.0.zip` | 20 | | 7.16.2 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/7.16.2/analyzeApiUi-7.16.2.zip` | 21 | | 7.13.4 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/7.13.4/analyzeApiUi-7.13.4.zip` | 22 | | 7.6.2 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/7.6.2/analyze_api_ui-7.6.2.zip` | 23 | | 7.6.2 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/7.6.2/analyze_api_ui-7.6.2.zip` | 24 | | 7.6.1 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/7.6.1/analyze_api_ui-7.6.1.zip` | 25 | | 7.6.0 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/7.6.0/analyze_api_ui-7.6.0.zip` | 26 | | 7.5.2 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/7.5.2/analyze_api_ui-7.5.2.zip` | 27 | | 7.5.1 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/7.5.1/analyze_api_ui-7.5.1.zip` | 28 | | 7.5.0 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/7.5.0/analyze_api_ui-7.5.0.zip` | 29 | | 7.4.2 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/7.4.2/analyze_api_ui-7.4.2.zip` | 30 | | 7.4.1 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/7.4.1/analyze_api_ui-7.4.1.zip` | 31 | | 7.4.0 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/7.4.0/analyze_api_ui-7.4.0.zip` | 32 | | 7.3.2 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/7.3.2/analyze_api_ui-7.3.2.zip` | 33 | | 7.3.1 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/7.3.1/analyze_api_ui-7.3.1.zip` | 34 | | 7.3.0 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/7.3.0/analyze_api_ui-7.3.0.zip` | 35 | | 7.2.0 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/7.2.0/analyze_api_ui-7.2.0.zip` | 36 | | 7.1.1 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/7.1.1/analyze_api_ui-7.1.1.zip` | 37 | | 7.1.0 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/7.1.0/analyze_api_ui-7.1.0.zip` | 38 | | 7.0.1 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/7.0.1/analyze_api_ui-7.0.1.zip` | 39 | | 7.0.0 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/7.0.0/analyze_api_ui-7.0.0.zip` | 40 | 41 |
42 | Older versions (6.7.1 - 6.1.2) 43 | 44 | | Kibana version | Command | 45 | |----------------|----------------------------------------------------------------------------------------------------------------------------------------| 46 | | 6.7.1 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/6.7.1/analyze-api-ui-plugin-6.7.1.zip` | 47 | | 6.7.0 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/6.7.0/analyze-api-ui-plugin-6.7.0.zip` | 48 | | 6.6.2 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/6.6.2/analyze-api-ui-plugin-6.6.2.zip` | 49 | | 6.6.1 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/6.6.1/analyze-api-ui-plugin-6.6.1.zip` | 50 | | 6.6.0 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/6.6.0/analyze-api-ui-plugin-6.6.0.zip` | 51 | | 6.5.4 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/6.5.4/analyze-api-ui-plugin-6.5.4.zip` | 52 | | 6.5.3 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/6.5.3/analyze-api-ui-plugin-6.5.3.zip` | 53 | | 6.5.2 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/6.5.2/analyze-api-ui-plugin-6.5.2.zip` | 54 | | 6.5.1 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/6.5.1/analyze-api-ui-plugin-6.5.1.zip` | 55 | | 6.5.0 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/6.5.0/analyze-api-ui-plugin-6.5.0.zip` | 56 | | 6.4.3 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/6.4.3/analyze-api-ui-plugin-6.4.3.zip` | 57 | | 6.4.2 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/6.4.2/analyze-api-ui-plugin-6.4.2.zip` | 58 | | 6.4.1 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/6.4.1/analyze-api-ui-plugin-6.4.1.zip` | 59 | | 6.4.0 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/6.4.0/analyze-api-ui-plugin-6.4.0.zip` | 60 | | 6.3.2 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/6.3.2/analyze-api-ui-plugin-6.3.2.zip` | 61 | | 6.3.0 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/6.3.0/analyze-api-ui-plugin-6.3.0.zip` | 62 | | 6.2.4 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/6.2.4/analyze-api-ui-plugin-6.2.4.zip` | 63 | | 6.2.3 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/6.2.3/analyze-api-ui-plugin-6.2.3.zip` | 64 | | 6.2.2 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/6.2.2/analyze-api-ui-plugin-6.2.2.zip` | 65 | | 6.2.1 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/6.2.1/analyze-api-ui-plugin-6.2.1.zip` | 66 | | 6.2.0 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/6.2.0/analyze-api-ui-plugin-6.2.0.zip` | 67 | | 6.1.3 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/6.1.3/analyze-api-ui-plugin-6.1.3.zip` | 68 | | 6.1.2 | `./bin/kibana-plugin install https://github.com/johtani/analyze-api-ui-plugin/releases/download/6.1.2/analyze-api-ui-plugin-6.1.2.zip` | 69 | 70 |
71 | 72 | ## Known issues 73 | 74 | * not restore filters/char_filters after moving another tabs [#42](https://github.com/johtani/analyze-api-ui-plugin/issues/42) 75 | 76 | ## TODO 77 | 78 | * change index name to select 79 | * preload index_name from _cat/indices 80 | * error handling 81 | 82 | --- 83 | 84 | ## Development 85 | 86 | See the [kibana contributing guide](https://github.com/elastic/kibana/blob/main/CONTRIBUTING.md) for instructions setting up your development environment. 87 | 88 | ## Scripts 89 | 90 |
91 |
yarn kbn bootstrap
92 |
Execute this to install node_modules and set up the dependencies in your plugin and in Kibana
93 | 94 |
yarn plugin-helpers build
95 |
Execute this to create a distributable version of this plugin that can be installed in Kibana
96 |
97 | 98 | ## Thanks 99 | 100 | Thank [@risdenk](https://github.com/risdenk) for your great help to upgrade 7.10+! 101 | -------------------------------------------------------------------------------- /common/index.ts: -------------------------------------------------------------------------------- 1 | export const PLUGIN_ID = 'analyzeApiUi'; 2 | export const PLUGIN_NAME = 'analyze-api-ui'; 3 | -------------------------------------------------------------------------------- /docs/GETTING_STARTED.md: -------------------------------------------------------------------------------- 1 | # Getting Started Analuze UI Plugin 2 | 3 | ## What is this? 4 | 5 | This Kibana plugin is the UI for [Elasticsearch _analyze API](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-analyze.html). 6 | The _analyze API is great for understanding how to work analysis process in Elasticsearch, 7 | but JSON response isn't human-readable if text is long or `explain=true` parameter is used. 8 | 9 | This plugin will show analyzed result more readable :) 10 | The plugin have some features, let's show how it works. 11 | And it also show the request syntax for Console under the table. 12 | 13 | ## 1. Show analyzed result of built-in analyzer 14 | 15 | This is simplest use-case for this plugin. 16 | Choose `analyzer` tab and fill two text inputs. 17 | 18 | 1. Text - Text what you want to analyze 19 | 2. Analyzer - Built-in analyzer name, e.g. `standard` 20 | 21 | Then click `Analyze!` button, it will show a list of `tokens`. 22 | 23 | ![Sample analyzer tab](./sample_image.jpg) 24 | 25 | ## 2. Show analyzed result of defined analyzer in index 26 | 27 | If you want to see analyzed result of the analyzer that you define in index, 28 | you can specify your index name and analyzer name. 29 | Choose `Analyzer` tab and fill three text inputs. 30 | 31 | 1. Index name - Index name what define an analyzer you want to use 32 | 2. Text - Text what you want to analyze 33 | 3. Analyzer - your analyzer name 34 | 35 | Then click `Analyze!` button, it will show a list of `tokens`. 36 | 37 | ## 3. Show analyzed result of custom analyzer 38 | 39 | If you want to test a combination of char_filter/tokenizer/filter before defining your custom analyzer, 40 | you can use `custom_analyzer` tab. 41 | Choose `custom_analyzer` tab and fill char_filter/tokenizer/filter text inputs. 42 | 43 | 1. Text - Text what you want to analyzer 44 | 2. char_filter - char_filter name/custom char_filter if you need 45 | 3. tokenizer - tokenizer name 46 | 4. filter - filter name/custom filter if you need 47 | 48 | Then click `Analyze!` button, it will show lists of text and `tokens`. 49 | 50 | ![Sample custom analyzer tab](./sample_custom_analyzer.jpg) 51 | 52 | If you want to use multiple char_filters/filters, click `plus` icon. 53 | The plugin will show new text input. 54 | 55 | You can also specify custom char_filter/tokenizer/filter as follow: 56 | 57 | ```JSON 58 | { 59 | "type": "mapping", 60 | "mappings": [ 61 | ":) => _happy_", 62 | ":( => _sad_" 63 | ] 64 | } 65 | ``` 66 | 67 | The example is char_filter with custom mapping. 68 | 69 | 70 | ## 4. Show more token attributes in the result 71 | 72 | Lucene analysis module provides many token attributes for each tokens. 73 | e.g. Kuromoji tokenizer outputs token attributes like `partOfSpeech`, `reading`, 'baseForm', etc. 74 | However, this plugin shows only `token` and `position` by default. 75 | If turn on `Show all Token Attrs?` switch that next to `Analyze!` button, 76 | the plugin shows all token attributes in the result table. 77 | It also works after clicking `Analyze!` button. 78 | 79 | ![Show all token attributes](./sample_show_token_attrs.jpg) 80 | 81 | 82 | ## 4. Show analyzed result of a field in the index mapping 83 | 84 | If you want to analyze text with existing field, 85 | you can specify your index name and "field name". 86 | 87 | 1. Index name - Index name what define an analyzer you want to use 88 | 2. Text - Text what you want to analyze 89 | 3. Field - your field name 90 | 91 | Then the plugin shows lists of tokens that are provided by the analyzer that is defined to the field. 92 | 93 | ![Sample specify a field name](./sample_specify_field.jpg) 94 | 95 | ## 5. Compare analyzed results between some analyzers 96 | 97 | The plugin also shows a comparison of list of tokens that are provided by each analyzers you put in form. 98 | 99 | ![Sample comparison of each analyzer results](./sample_compare_results.jpg) 100 | -------------------------------------------------------------------------------- /docs/sample_compare_results.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johtani/analyze-api-ui-plugin/aa3d2ebcdc71a1485897fa741c91ff610586ded1/docs/sample_compare_results.jpg -------------------------------------------------------------------------------- /docs/sample_custom_analyzer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johtani/analyze-api-ui-plugin/aa3d2ebcdc71a1485897fa741c91ff610586ded1/docs/sample_custom_analyzer.jpg -------------------------------------------------------------------------------- /docs/sample_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johtani/analyze-api-ui-plugin/aa3d2ebcdc71a1485897fa741c91ff610586ded1/docs/sample_image.jpg -------------------------------------------------------------------------------- /docs/sample_show_token_attrs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johtani/analyze-api-ui-plugin/aa3d2ebcdc71a1485897fa741c91ff610586ded1/docs/sample_show_token_attrs.jpg -------------------------------------------------------------------------------- /docs/sample_specify_field.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johtani/analyze-api-ui-plugin/aa3d2ebcdc71a1485897fa741c91ff610586ded1/docs/sample_specify_field.jpg -------------------------------------------------------------------------------- /kibana.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "analyzeApiUi", 3 | "version": "7.17.0", 4 | "kibanaVersion": "kibana", 5 | "owner": { 6 | "name": "johtani" 7 | }, 8 | "description": "UI for elasticsearch analyze API", 9 | "server": true, 10 | "ui": true, 11 | "requiredPlugins": ["navigation"], 12 | "optionalPlugins": [] 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "analyze_api_ui", 3 | "version": "7.17.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "yarn plugin-helpers build", 7 | "plugin-helpers": "node ../../scripts/plugin_helpers", 8 | "kbn": "node ../../scripts/kbn" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /public/application.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { AppMountParameters, CoreStart } from 'kibana/public'; 4 | import { AppPluginStartDependencies } from './types'; 5 | import { AnalyzeApiUiApp } from './components/app'; 6 | 7 | export const renderApp = ( 8 | { notifications, http }: CoreStart, 9 | { navigation }: AppPluginStartDependencies, 10 | { appBasePath, element }: AppMountParameters 11 | ) => { 12 | ReactDOM.render( 13 | , 19 | element 20 | ); 21 | 22 | return () => ReactDOM.unmountComponentAtNode(element); 23 | }; 24 | -------------------------------------------------------------------------------- /public/common/constants/tab_names.tsx: -------------------------------------------------------------------------------- 1 | export const TAB_NAME = { 2 | ANALYZER: 'Analyzer', 3 | CUSTOM_ANALYZER: 'Custom Analyzer', 4 | FIELD: 'Field', 5 | COMPARE_ANALYZERS: 'Compare Analyzers' 6 | }; 7 | 8 | export const RESULT_TYPE = { 9 | SINGLE: 'single', 10 | MULTI: 'multi' 11 | }; 12 | -------------------------------------------------------------------------------- /public/common/constants/token_properties.tsx: -------------------------------------------------------------------------------- 1 | export const DISP_TOKEN_PROPS = { 2 | alwaysShowTokenProperties: [ 3 | "token", 4 | "position" 5 | ], 6 | hiddenTokenProperties: [ 7 | "bytes", 8 | "pronunciation (en)", 9 | "reading (en)", 10 | "partOfSpeech (en)", 11 | "inflectionType (en)", 12 | "inflectionForm (en)" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /public/components/analyze_ui/analyze_ui.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Component, 3 | Fragment, 4 | } from 'react'; 5 | import { 6 | EuiCallOut, 7 | EuiSpacer, 8 | } from '@elastic/eui' 9 | 10 | import { CommonForm } from "../common_form"; 11 | import { AnalyzerForm } from "../analyzer_form"; 12 | import { FormControl } from "../form_control"; 13 | import { Result } from "../result"; 14 | import { 15 | updateParamsWithEvent, 16 | updateParamsWithEventAndIndex, 17 | selectTab 18 | } from "../../services/params"; 19 | import { 20 | setHttpClient, 21 | analyze, 22 | multiAnalyze 23 | } from "../../services/api"; 24 | import {validateAnalyzeRequestValues} from "../../services/validator"; 25 | import {TAB_NAME} from "../../common/constants/tab_names"; 26 | import {loadSavedState, saveState} from "../../services/state_handler"; 27 | 28 | export class AnalyzeUi extends Component { 29 | constructor(props) { 30 | super(props); 31 | const params = loadSavedState(); 32 | this.state = { 33 | params: params, 34 | errors: {} 35 | }; 36 | setHttpClient(this.props.httpClient); 37 | 38 | this.selectTab = this.selectTab.bind(this); 39 | this.updateParamsWithEvent = this.updateParamsWithEvent.bind(this); 40 | this.updateParamsWithEventAndIndex = this.updateParamsWithEventAndIndex.bind(this); 41 | this.displayResult = this.displayResult.bind(this); 42 | } 43 | 44 | clearErrors() { 45 | this.setState({ 46 | errors: {} 47 | }); 48 | } 49 | 50 | updateParamsWithEvent(e) { 51 | const params = updateParamsWithEvent(e, this.state.params); 52 | this.updateParams(params); 53 | }; 54 | 55 | updateParamsWithEventAndIndex(e) { 56 | const params = updateParamsWithEventAndIndex(e, this.state.params); 57 | this.updateParams(params); 58 | }; 59 | 60 | selectTab(tab) { 61 | const params = selectTab(tab, this.state.params); 62 | this.updateParams(params); 63 | this.clearResults(); 64 | this.clearErrors(); 65 | }; 66 | 67 | updateParams(params) { 68 | this.setState({ 69 | params: params 70 | }); 71 | saveState(this.state); 72 | }; 73 | 74 | clearResults() { 75 | if (this.state.detail) { 76 | this.setState({ 77 | "detail": {}, 78 | "showResult": false, 79 | "esRequest": "", 80 | "resultAnalyzers": [] 81 | }); 82 | } 83 | } 84 | 85 | callMultiAnalyzeApi (params) { 86 | const result = multiAnalyze(params); 87 | result.then( 88 | (response) => { 89 | this.setState({ 90 | showResult: true, 91 | resultAnalyzers: response.resultAnalyzers, 92 | resultType: "multi" 93 | }); 94 | } 95 | ).catch( 96 | error => { 97 | if (error.body) { 98 | if (error.body.statusCode == 404) { 99 | this.setState({ 100 | errors: { 101 | indexNameError: error.body.message 102 | } 103 | }) 104 | } else if (error.body.statusCode == 400) { 105 | this.setState({ 106 | errors: { 107 | analyzerError: error.body.message 108 | } 109 | }); 110 | } else { 111 | //TODO Notification 112 | console.error(error); 113 | } 114 | } else { 115 | //TODO Notification 116 | console.error(error); 117 | } 118 | } 119 | ); 120 | } 121 | 122 | callAnalyzeApi (params) { 123 | const result = analyze(params); 124 | result.then( 125 | (response) => { 126 | this.setState({ 127 | showResult: true, 128 | detail: response.detail, 129 | esRequest: response.request, 130 | resultType: "single" 131 | }); 132 | } 133 | ); 134 | result.catch( 135 | error => { 136 | if (error.body) { 137 | if (error.body.statusCode == 404) { 138 | this.setState({ 139 | errors: { 140 | indexNameError: error.body.message 141 | } 142 | }) 143 | } else if (error.body.statusCode == 400) { 144 | this.setState({ 145 | errors: { 146 | analyzerError: error.body.message 147 | } 148 | }); 149 | } else { 150 | //TODO 151 | console.error(error); 152 | } 153 | } 154 | console.error(error); 155 | //TODO Notification! 156 | } 157 | ); 158 | } 159 | 160 | // render results 161 | displayResult(e) { 162 | this.clearErrors(); 163 | this.clearResults(); 164 | const {params} = this.state; 165 | const {tab} = this.state.params; 166 | const validatedParams = validateAnalyzeRequestValues(params); 167 | if (Object.keys(validatedParams.errors).length) { 168 | this.setState({ 169 | errors: validatedParams.errors 170 | }); 171 | } else { 172 | //collect current values from common form and analyzer form 173 | if (tab != TAB_NAME.COMPARE_ANALYZERS) { 174 | this.callAnalyzeApi(validatedParams.requestParams); 175 | } else { 176 | this.callMultiAnalyzeApi(validatedParams.requestParams); 177 | } 178 | } 179 | }; 180 | 181 | renderCommonFormErrors() { 182 | const {errors} = this.state; 183 | if (!Object.keys(errors).includes("indexNameError") && 184 | !Object.keys(errors).includes("textError")) { 185 | return; 186 | } else { 187 | return ( 188 | 189 | 194 | Please check every inputs. 195 | 196 | 197 | 198 | ); 199 | } 200 | } 201 | 202 | renderAnalyzerErrors() { 203 | const {errors} = this.state; 204 | if (!Object.keys(errors).includes("analyzerError") && 205 | !Object.keys(errors).includes("fieldError")) { 206 | return; 207 | } else { 208 | let target = "analyzer"; 209 | const {tab} = this.state.params; 210 | if (tab == TAB_NAME.CUSTOM_ANALYZER) { 211 | target = "chafilter/tokenizer/filter"; 212 | } else if (tab == TAB_NAME.FIELD) { 213 | target = "field"; 214 | } 215 | return ( 216 | 217 | 222 | {this.state.errors.analyzerError} 223 | {this.state.errors.fieldError} 224 | 225 | 226 | 227 | ); 228 | } 229 | } 230 | 231 | render() { 232 | const {indexName} = this.state.params; 233 | return ( 234 | 235 | {this.renderCommonFormErrors()} 236 | 239 | 240 | {this.renderAnalyzerErrors()} 241 | 246 | 247 | 252 | 253 | {this.state.showResult && ( 254 | 262 | )} 263 | 264 | ); 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /public/components/analyze_ui/index.ts: -------------------------------------------------------------------------------- 1 | export { AnalyzeUi } from './analyze_ui'; -------------------------------------------------------------------------------- /public/components/analyzer_form/analyzer_form.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Component, 3 | Fragment 4 | } from 'react'; 5 | 6 | import { 7 | EuiButtonIcon, 8 | EuiTabbedContent, 9 | EuiTableRow, 10 | EuiTableRowCell, 11 | EuiToolTip, 12 | EuiSpacer, 13 | } from '@elastic/eui'; 14 | 15 | import { SimpleAnalyzer } from './simple'; 16 | import { CustomAnalyzer } from './custom'; 17 | import { FieldAnalyzer } from './field'; 18 | import { CompareAnalyzers } from './compare_analyzers'; 19 | import { TAB_NAME } from "../../common/constants/tab_names"; 20 | 21 | export class AnalyzerForm extends Component { 22 | tabs: { id: string; name: string; content: JSX.Element; }[]; 23 | constructor(props: Readonly<{}>) { 24 | super(props); 25 | this.tabs = [{ 26 | id: 'analyzer', 27 | name: TAB_NAME.ANALYZER, 28 | content: ( 29 | 30 | 31 | 35 | 36 | ) 37 | }, { 38 | id: 'custom_analyzer', 39 | name: TAB_NAME.CUSTOM_ANALYZER, 40 | content: ( 41 | 42 | 43 | 48 | 49 | ) 50 | }, { 51 | id: 'field', 52 | name: TAB_NAME.FIELD, 53 | content: ( 54 | 55 | 56 | 60 | 61 | ) 62 | }, { 63 | id: 'compare_analyzers', 64 | name: TAB_NAME.COMPARE_ANALYZERS, 65 | content: ( 66 | 67 | 68 | 73 | 74 | ) 75 | }]; 76 | this.props.selectTab(this.tabs[this.tabIndex()]); 77 | } 78 | 79 | tabIndex() { 80 | const {tab} = this.props.params; 81 | let tabIdx = 0; 82 | if (tab) { 83 | this.tabs.forEach((tmpTab, idx) => { 84 | if (tmpTab.name == tab) { 85 | tabIdx = idx; 86 | } 87 | } 88 | ); 89 | } 90 | return tabIdx; 91 | } 92 | 93 | render() { 94 | return ( 95 | 100 | ) 101 | } 102 | } 103 | 104 | export function displayRowsComponent(WrappedComponent): typeof WrappedComponent { 105 | return class extends WrappedComponent { 106 | 107 | appendRow(type, index) { 108 | super.appendRow(type, index); 109 | } 110 | 111 | removeRow(type, index) { 112 | super.removeRow(type, index); 113 | } 114 | 115 | renderAdjustButton (type, cell, index, length) { 116 | if (cell.button) { 117 | return ( 118 | 119 | {index == 0 ? ( 120 | 121 | this.appendRow(type, index)} 125 | /> 126 | 127 | ) : null } 128 | {length > 1 ? ( 129 | 130 | this.removeRow(type, index)} 134 | /> 135 | 136 | ) : null } 137 | 138 | ); 139 | } else { 140 | return ( 141 | 142 | ); 143 | } 144 | } 145 | 146 | renderRow (type, cells) { 147 | return cells.map((cell, index) => { 148 | return ( 149 | 150 | 151 | {type} 152 | 153 | 154 | {cell.form.renderTag(index)} 155 | 156 | {this.renderAdjustButton(type, cell, index, cells.length)} 157 | 158 | ); 159 | }); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /public/components/analyzer_form/compare_analyzers.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Component, 3 | } from 'react'; 4 | import { 5 | EuiFieldText, 6 | EuiTable, 7 | EuiTableBody 8 | } from '@elastic/eui'; 9 | import { displayRowsComponent } from './analyzer_form'; 10 | 11 | class CompareAnalyzersComponent extends Component { 12 | constructor(props) { 13 | super(props); 14 | this.state = { 15 | rows: { 16 | "analyzersForCompare": [] 17 | } 18 | }; 19 | //Need two boxes because of "Compare..." 20 | this.appendAnalyzer(); 21 | this.appendAnalyzer(); 22 | } 23 | 24 | appendAnalyzer(type, index) { 25 | this.state.rows.analyzersForCompare.push( 26 | { 27 | type: "analyzersForCompare", 28 | label: { 29 | value: "analyzer", 30 | style: {width: 100}, 31 | }, 32 | form: { 33 | renderTag: (index) => { 34 | return ( 35 | 40 | ); 41 | }, 42 | style: {width: 300}, 43 | }, 44 | button: true 45 | } 46 | ); 47 | this.setState({rows: this.state.rows}); 48 | } 49 | 50 | appendRow(type, index) { 51 | this.appendAnalyzer(); 52 | } 53 | 54 | removeRow(type, index) { 55 | this.state.rows.analyzersForCompare.splice(index, 1); 56 | this.setState({rows: this.state.rows}); 57 | } 58 | 59 | render() { 60 | return ( 61 | 62 | 63 | {this.renderRow("analyzer", this.state.rows.analyzersForCompare)} 64 | 65 | 66 | ) 67 | } 68 | } 69 | const CompareAnalyzers = displayRowsComponent(CompareAnalyzersComponent); 70 | export { CompareAnalyzers }; 71 | -------------------------------------------------------------------------------- /public/components/analyzer_form/custom.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Component 3 | } from 'react'; 4 | import { 5 | EuiTextArea, 6 | EuiTable, 7 | EuiTableBody 8 | } from '@elastic/eui'; 9 | import { displayRowsComponent } from './analyzer_form'; 10 | 11 | class CustomAnalyzerComponent extends Component { 12 | constructor(props) { 13 | super(props); 14 | this.state = { 15 | rows: { 16 | "char_filter": [], 17 | "tokenizer": [ 18 | { 19 | type: "tokenizer", 20 | label: { 21 | value: "tokenizer", 22 | style: {width: 100}, 23 | }, 24 | form: { 25 | renderTag: (index) => { 26 | return ( 27 | 33 | ); 34 | }, 35 | style: {width: 300}, 36 | } 37 | } 38 | ], 39 | "filter": [] 40 | } 41 | } 42 | // initialize charfilter and filter 43 | this.appendCharFilter(); 44 | this.appendFilter(); 45 | } 46 | 47 | appendCharFilter() { 48 | this.state.rows.char_filter.push( 49 | { 50 | type: "char_filter", 51 | label: { 52 | value: "char_filter", 53 | style: {width: 100}, 54 | }, 55 | form: { 56 | renderTag: (index) => { 57 | return ( 58 | 64 | ); 65 | }, 66 | style: {width: 300}, 67 | }, 68 | button: true 69 | } 70 | ); 71 | this.setState( 72 | { 73 | rows: this.state.rows 74 | } 75 | ); 76 | } 77 | 78 | appendFilter() { 79 | this.state.rows.filter.push( 80 | { 81 | type: "filter", 82 | label: { 83 | value: "filter", 84 | style: {width: 100}, 85 | }, 86 | form: { 87 | renderTag: (index) => { 88 | return ( 89 | 95 | ); 96 | }, 97 | style: {width: 300}, 98 | }, 99 | button: true 100 | } 101 | ); 102 | this.setState( 103 | { 104 | rows: this.state.rows 105 | } 106 | ); 107 | } 108 | 109 | appendRow(type, index) { 110 | if (type == "char_filter") { 111 | this.appendCharFilter(); 112 | } else if (type == "filter") { 113 | this.appendFilter(); 114 | } 115 | } 116 | 117 | removeRow(type, index) { 118 | if (type == "char_filter") { 119 | this.state.rows.char_filter.splice(index, 1); 120 | this.setState({rows: this.state.rows}); 121 | } else if (type == "filter") { 122 | this.state.rows.filter.splice(index, 1); 123 | this.setState({rows: this.state.rows}); 124 | } 125 | } 126 | 127 | render () { 128 | return ( 129 | 130 | 131 | {this.renderRow("char_filter", this.state.rows.char_filter)} 132 | {this.renderRow("tokenizer", this.state.rows.tokenizer)} 133 | {this.renderRow("filter", this.state.rows.filter)} 134 | 135 | 136 | ) 137 | } 138 | } 139 | const CustomAnalyzer = displayRowsComponent(CustomAnalyzerComponent); 140 | export { CustomAnalyzer }; 141 | -------------------------------------------------------------------------------- /public/components/analyzer_form/field.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Component 3 | } from 'react'; 4 | import { 5 | EuiFieldText, 6 | EuiTable, 7 | EuiTableBody 8 | } from '@elastic/eui'; 9 | import { displayRowsComponent } from './analyzer_form'; 10 | 11 | class FieldAnalyzerComponent extends Component { 12 | 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | rows: { 17 | "field": [{ 18 | type: "field", 19 | label: { 20 | value: "field", 21 | style: {width: 100}, 22 | }, 23 | form: { 24 | renderTag: (index) => { 25 | return ( 26 | 33 | ); 34 | }, 35 | style: {width: 300}, 36 | } 37 | }] 38 | } 39 | } 40 | } 41 | 42 | render () { 43 | return ( 44 | 45 | 46 | {this.renderRow("field", this.state.rows.field)} 47 | 48 | 49 | ) 50 | } 51 | } 52 | const FieldAnalyzer = displayRowsComponent(FieldAnalyzerComponent); 53 | export { FieldAnalyzer }; -------------------------------------------------------------------------------- /public/components/analyzer_form/index.ts: -------------------------------------------------------------------------------- 1 | export { AnalyzerForm } from './analyzer_form'; -------------------------------------------------------------------------------- /public/components/analyzer_form/simple.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Component 3 | } from 'react'; 4 | import { 5 | EuiFieldText, 6 | EuiTable, 7 | EuiTableBody 8 | } from '@elastic/eui'; 9 | import { displayRowsComponent } from './analyzer_form'; 10 | 11 | class SimpleAnalyzerComponent extends Component { 12 | constructor(props: Readonly<{}>) { 13 | super(props); 14 | this.state = { 15 | rows: { 16 | "analyzer": [ 17 | { 18 | label: { 19 | style: { width: 100}, 20 | }, 21 | form: { 22 | renderTag: (index) => { 23 | return ( 24 | 30 | ); 31 | }, 32 | style: { width: 300 }, 33 | } 34 | } 35 | ] 36 | } 37 | } 38 | } 39 | 40 | render () { 41 | return ( 42 | 43 | 44 | {this.renderRow("analyzer", this.state.rows.analyzer)} 45 | 46 | 47 | ) 48 | } 49 | } 50 | const SimpleAnalyzer = displayRowsComponent(SimpleAnalyzerComponent); 51 | export { SimpleAnalyzer }; 52 | -------------------------------------------------------------------------------- /public/components/app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { I18nProvider } from '@kbn/i18n/react'; 3 | import { BrowserRouter as Router } from 'react-router-dom'; 4 | 5 | import { 6 | EuiPage, 7 | EuiPageBody, 8 | EuiPageContent, 9 | EuiPageHeader, 10 | EuiPageHeaderSection, 11 | EuiSpacer, 12 | EuiTitle, 13 | } from '@elastic/eui'; 14 | 15 | import { CoreStart } from '../../../../src/core/public'; 16 | import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public'; 17 | 18 | import {AnalyzeUi} from "./analyze_ui"; 19 | 20 | import { PLUGIN_ID } from '../../common'; 21 | 22 | interface AnalyzeApiUiAppDeps { 23 | basename: string; 24 | notifications: CoreStart['notifications']; 25 | http: CoreStart['http']; 26 | navigation: NavigationPublicPluginStart; 27 | } 28 | 29 | export const AnalyzeApiUiApp = ({ 30 | basename, 31 | http, 32 | navigation, 33 | }: AnalyzeApiUiAppDeps) => { 34 | // Render the application DOM. 35 | // Note that `navigation.ui.TopNavMenu` is a stateful component exported on the `navigation` plugin's start contract. 36 | return ( 37 | 38 | 39 | <> 40 | 45 | 46 | 47 | 48 | 49 | 50 |

Perform analyze via _analyze API

51 |
52 |
53 |
54 | 55 | 56 | 57 | 58 |
59 |
60 | 61 |
62 |
63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /public/components/common_form/common_form.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Component 3 | } from 'react'; 4 | import { 5 | EuiFlexGroup, 6 | EuiFlexItem, 7 | EuiFormRow, 8 | EuiFieldText, 9 | EuiTextColor 10 | } from '@elastic/eui'; 11 | 12 | export class CommonForm extends Component { 13 | 14 | constructor(props) { 15 | super(props); 16 | } 17 | 18 | render () { 19 | const {params} = this.props; 20 | const {errors} = this.props; 21 | const showIndexNameError = Object.keys(errors).includes("indexNameError"); 22 | const indexNameError = showIndexNameError ? errors.indexNameError : undefined; 23 | const showTextError = Object.keys(errors).includes("textError"); 24 | const textError = showTextError ? errors.textError : undefined; 25 | 26 | return ( 27 | 28 | 29 | 33 | Index name - select index name if you use the analyzer to configure an index 34 | 35 | } 36 | fullWidth 37 | isInvalid={showIndexNameError} 38 | error={indexNameError} 39 | onChange={this.props.updateParamsWithEvent} 40 | > 41 | 47 | 48 | 52 | Text - Text what you want to analyze 53 | 54 | } 55 | fullWidth 56 | isInvalid={showTextError} 57 | error={textError} 58 | onChange={this.props.updateParamsWithEvent} 59 | > 60 | 66 | 67 | 68 | 69 | 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /public/components/common_form/index.ts: -------------------------------------------------------------------------------- 1 | export { CommonForm } from './common_form'; -------------------------------------------------------------------------------- /public/components/form_control/form_control.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Component 3 | } from 'react'; 4 | import { 5 | EuiFlexGroup, 6 | EuiFlexItem, 7 | EuiButton, 8 | EuiSwitch, 9 | } from '@elastic/eui' 10 | 11 | export class FormControl extends Component { 12 | constructor(props) { 13 | super(props); 14 | } 15 | 16 | render () { 17 | return ( 18 | 19 | 20 | 27 | Analyze! 28 | 29 | 30 | 31 | 37 | 38 | 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /public/components/form_control/index.ts: -------------------------------------------------------------------------------- 1 | export { FormControl } from './form_control'; -------------------------------------------------------------------------------- /public/components/result/analyzer_result/analyzer_result.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Component, 3 | } from 'react'; 4 | import { 5 | EuiCodeBlock, 6 | EuiPanel, 7 | EuiSpacer 8 | } from '@elastic/eui'; 9 | import {Charfilters} from "./charfilters"; 10 | import {TokenizerAndFilters} from "./tokenizer_and_filters"; 11 | import {createTokenIndices, getLength} from "../../../services/token_utils"; 12 | 13 | export class AnalyzerResult extends Component { 14 | constructor(props) { 15 | super(props); 16 | } 17 | 18 | countTokenSteamLength(detail) { 19 | // FIXME tokens length is not fit if it has synonym token/compound token... 20 | let tokenStreamLength = 0; 21 | 22 | if (detail.tokenizer) { 23 | tokenStreamLength = getLength(tokenStreamLength, detail.tokenizer.tokens); 24 | } else if (detail.analyzer) { 25 | tokenStreamLength = getLength(tokenStreamLength, detail.analyzer.tokens); 26 | } 27 | if (detail.tokenfilters) { 28 | detail.tokenfilters.forEach( (filter) => { 29 | tokenStreamLength = getLength(tokenStreamLength, filter.tokens); 30 | }); 31 | } 32 | return createTokenIndices(tokenStreamLength); 33 | } 34 | 35 | render() { 36 | const { 37 | detail, 38 | esRequest, 39 | indexName 40 | } = this.props; 41 | const tokenIndices = this.countTokenSteamLength(detail); 42 | return ( 43 | 44 | 45 | 46 | 47 | 54 | 55 | 56 | 57 | Request for Elasticsearch - you can copy and paste to Console 58 | 59 | GET {indexName}/_analyze 60 | {JSON.stringify(esRequest, null, 2)} 61 | 62 | 63 | 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /public/components/result/analyzer_result/charfilters/chafilters.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Component 3 | } from 'react'; 4 | import { 5 | EuiTable, 6 | EuiTableBody, 7 | EuiTableHeader, 8 | EuiTableHeaderCell, 9 | EuiTableRow, 10 | EuiTableRowCell 11 | } from '@elastic/eui'; 12 | 13 | export class Charfilters extends Component { 14 | constructor(props) { 15 | super(props); 16 | } 17 | 18 | renderRow(target) { 19 | return ( 20 | 21 | 22 | char_filter
23 | {target.name} 24 |
25 |
26 | 27 | {target.filtered_text[0]} 28 | 29 |
30 | ); 31 | } 32 | 33 | renderRows(charfilters) { 34 | if (charfilters && charfilters.length > 0) { 35 | const rows = charfilters.map((target) => { 36 | return this.renderRow(target); 37 | }); 38 | return rows; 39 | } else { 40 | return null; 41 | } 42 | } 43 | 44 | render() { 45 | const {charfilters} = this.props; 46 | if (charfilters && charfilters.length > 0) { 47 | return ( 48 |
49 | 50 | 51 | 55 | type
name 56 |
57 | 60 | filtered text 61 | 62 |
63 | 64 | {this.renderRows(charfilters)} 65 | 66 |
67 |
68 | ); 69 | } else { 70 | return null; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /public/components/result/analyzer_result/charfilters/index.ts: -------------------------------------------------------------------------------- 1 | export { Charfilters } from './chafilters'; -------------------------------------------------------------------------------- /public/components/result/analyzer_result/index.ts: -------------------------------------------------------------------------------- 1 | export { AnalyzerResult } from './analyzer_result'; -------------------------------------------------------------------------------- /public/components/result/analyzer_result/tokenizer_and_filters/filters.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Component 3 | } from 'react'; 4 | 5 | import {displayRowsComponent} from "./tokenizer_and_filters"; 6 | 7 | class FiltersComponent extends Component { 8 | constructor(props) { 9 | super(props); 10 | } 11 | 12 | render () { 13 | const { 14 | filters, 15 | tokenIndices 16 | } = this.props; 17 | if (filters && filters.length > 0) { 18 | const rows = filters.map((target) => { 19 | return this.renderRow(target, "filter", tokenIndices); 20 | } 21 | ); 22 | return rows; 23 | } else { 24 | return null; 25 | } 26 | } 27 | } 28 | const Filters = displayRowsComponent(FiltersComponent); 29 | export { Filters }; 30 | -------------------------------------------------------------------------------- /public/components/result/analyzer_result/tokenizer_and_filters/index.ts: -------------------------------------------------------------------------------- 1 | export { TokenizerAndFilters } from './tokenizer_and_filters'; -------------------------------------------------------------------------------- /public/components/result/analyzer_result/tokenizer_and_filters/token.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Component 3 | } from 'react'; 4 | 5 | import { DISP_TOKEN_PROPS } from '../../../../common/constants/token_properties'; 6 | 7 | export class Token extends Component { 8 | 9 | constructor(props) { 10 | super(props); 11 | } 12 | 13 | makeTokenListFromTokenStream(index, target) { 14 | const currentLevelTokenList = []; 15 | for (let token of target.tokens) { 16 | if (token.position > index) { 17 | break; 18 | } 19 | if (token.position == index) { 20 | currentLevelTokenList.push(token); 21 | } 22 | } 23 | return currentLevelTokenList; 24 | } 25 | 26 | hideTokenProperty(propertyName) { 27 | if (DISP_TOKEN_PROPS.alwaysShowTokenProperties.includes(propertyName)) { 28 | return true; 29 | } else { 30 | // TODO should we handle each attribute to show/hide? 31 | return this.props.showAllTokenAttr; 32 | } 33 | }; 34 | 35 | // filter token properties 36 | filteredCurrentTokenInfo(token) { 37 | if (token != null) { 38 | const result = {}; 39 | Object.keys(token).forEach((key) => { 40 | if (!DISP_TOKEN_PROPS.hiddenTokenProperties.includes(key)) { 41 | result[key] = token[key]; 42 | } 43 | }); 44 | return result; 45 | } else { 46 | return null; 47 | } 48 | } 49 | 50 | renderTokenValue(key, value) { 51 | if (key=='token') { 52 | return ( 53 | {value} 54 | ); 55 | } else { 56 | return ( 57 | {value} 58 | ); 59 | } 60 | } 61 | 62 | renderTokenKey(key) { 63 | if (this.props.index == 0) { 64 | return ( 65 | 66 | {key} 67 | 68 | ); 69 | } else { 70 | return null; 71 | } 72 | } 73 | 74 | renderRow(key, value) { 75 | if (this.hideTokenProperty(key)) { 76 | return ( 77 | 78 | {this.renderTokenKey(key)} 79 | 80 | {this.renderTokenValue(key, value)} 81 |   82 | 83 | 84 | ); 85 | } else { 86 | return null; 87 | } 88 | } 89 | 90 | renderTokenProperties(token) { 91 | const currentTokenInfo = this.filteredCurrentTokenInfo(token); 92 | if (currentTokenInfo) { 93 | const propRows = []; 94 | Object.keys(token).forEach( 95 | (key) => { 96 | propRows.push(this.renderRow(key, token[key])); 97 | } 98 | ); 99 | return propRows; 100 | } else { 101 | return null; 102 | } 103 | } 104 | 105 | renderTokenTable(currentLevelTokenList) { 106 | const tokenTables = currentLevelTokenList.map( 107 | (token) => 108 | 109 | 110 | {this.renderTokenProperties(token)} 111 | 112 |
113 | ); 114 | if (tokenTables.length > 0) { 115 | return tokenTables; 116 | } else { 117 | return ( 118 | no token 119 | ); 120 | } 121 | } 122 | 123 | render () { 124 | const { 125 | index, 126 | target, 127 | } = this.props; 128 | const currentLevelTokenList = this.makeTokenListFromTokenStream(index, target); 129 | if (currentLevelTokenList.length > 0){ 130 | return ( 131 |
134 | {this.renderTokenTable(currentLevelTokenList)} 135 |
136 | ); 137 | } else { 138 | return null; 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /public/components/result/analyzer_result/tokenizer_and_filters/tokenizer.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Component 3 | } from 'react'; 4 | 5 | import {displayRowsComponent} from "./tokenizer_and_filters"; 6 | 7 | class TokenizerComponent extends Component { 8 | constructor(props) { 9 | super(props); 10 | } 11 | 12 | render () { 13 | const { 14 | analyzer, 15 | tokenizer, 16 | tokenIndices 17 | } = this.props; 18 | 19 | const target = analyzer? analyzer : tokenizer; 20 | const name = analyzer? "analyzer" : "tokenizer"; 21 | return this.renderRow(target, name, tokenIndices); 22 | } 23 | } 24 | const Tokenizer = displayRowsComponent(TokenizerComponent); 25 | export { Tokenizer }; 26 | -------------------------------------------------------------------------------- /public/components/result/analyzer_result/tokenizer_and_filters/tokenizer_and_filters.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Component 3 | } from 'react'; 4 | 5 | import { 6 | EuiTable, 7 | EuiTableBody, 8 | EuiTableHeader, 9 | EuiTableHeaderCell, 10 | EuiTableRow, 11 | EuiTableRowCell, 12 | } from '@elastic/eui'; 13 | import {Tokenizer} from "./tokenizer"; 14 | import {Filters} from "./filters"; 15 | import {Token} from "./token"; 16 | 17 | export class TokenizerAndFilters extends Component { 18 | constructor(props) { 19 | super(props); 20 | } 21 | 22 | renderHeaderTokensCells(tokenIndices) { 23 | const headerCells = tokenIndices.map( 24 | (index) => 25 | 29 | tokens[{index}] 30 | 31 | ); 32 | return headerCells; 33 | } 34 | 35 | render() { 36 | const {tokenizer} = this.props; 37 | const {analyzer} = this.props; 38 | const {filters} = this.props; 39 | const {tokenIndices} = this.props; 40 | const {showAllTokenAttr} = this.props; 41 | if (tokenizer || analyzer || filters) { 42 | return ( 43 |
44 | 45 | 46 | 50 |
type
name
51 |
52 | {this.renderHeaderTokensCells(tokenIndices)} 53 |
54 | 55 | 61 | 66 | 67 |
68 |
69 | ); 70 | } 71 | } 72 | } 73 | 74 | export function displayRowsComponent(WrappedComponent) { 75 | return class extends WrappedComponent { 76 | 77 | shortenName(name) { 78 | if (name && name.indexOf('.') > 0) { 79 | return name.substring(name.lastIndexOf('.')+1); 80 | } 81 | return name; 82 | } 83 | 84 | renderTokenCells (tokenIndices, target) { 85 | const tokenCells = tokenIndices.map( 86 | (index) => 87 | 88 | 93 | 94 | ); 95 | return tokenCells; 96 | } 97 | 98 | renderRow(target, name, tokenIndices) { 99 | return ( 100 | 103 | 104 |
{name}
105 | {this.shortenName(target.name)} 106 |
107 |
108 | {this.renderTokenCells(tokenIndices, target)} 109 |
110 | ); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /public/components/result/compare_result/compare_result.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Component, 3 | } from 'react'; 4 | import { 5 | EuiPanel, 6 | EuiTable, 7 | EuiTableHeader, 8 | EuiTableHeaderCell, 9 | EuiTableBody, 10 | EuiTableRow, 11 | EuiTableRowCell, 12 | } from '@elastic/eui'; 13 | import {createTokenIndices, getLength} from "../../../services/token_utils"; 14 | import {Token} from "../analyzer_result/tokenizer_and_filters/token"; 15 | 16 | 17 | export class CompareResult extends Component { 18 | 19 | constructor(props) { 20 | super(props); 21 | } 22 | 23 | countTokenStreamLength(resultAnalyzers) { 24 | let tokenStreamLength = 0; 25 | resultAnalyzers.forEach((detail) => { 26 | tokenStreamLength = getLength(tokenStreamLength, detail.tokens); 27 | }); 28 | return createTokenIndices(tokenStreamLength); 29 | } 30 | 31 | renderHeaderTokensCells(tokenIndices) { 32 | const headerCells = tokenIndices.map( 33 | (index) => 34 | 38 | type
tokens[{index}]
40 |
41 | ); 42 | return headerCells; 43 | } 44 | 45 | renderTokenCells (tokenIndices, target) { 46 | const tokenCells = tokenIndices.map( 47 | (index) => 48 | 49 | 54 | 55 | ); 56 | return tokenCells; 57 | } 58 | 59 | renderRows(tokenIndices, resultAnalyzers) { 60 | const rows = resultAnalyzers.map( 61 | (result) => 62 | 65 | 66 | analyzer
67 | {result.analyzer} 68 |
69 |
70 | {this.renderTokenCells(tokenIndices, result)} 71 |
72 | ); 73 | return rows; 74 | } 75 | 76 | render() { 77 | const { 78 | resultAnalyzers 79 | } = this.props; 80 | const tokenIndices = this.countTokenStreamLength(resultAnalyzers); 81 | 82 | return ( 83 | 87 | There are only tokens and positions. If you know analyzer details, use analyzer/custom_analyzer tab. 88 |
89 | 90 | 91 | 92 | 96 | type
name
97 |
98 | {this.renderHeaderTokensCells(tokenIndices)} 99 |
100 | 101 | {this.renderRows(tokenIndices, resultAnalyzers)} 102 | 103 |
104 |
105 |
106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /public/components/result/compare_result/index.ts: -------------------------------------------------------------------------------- 1 | export { CompareResult } from './compare_result'; -------------------------------------------------------------------------------- /public/components/result/index.ts: -------------------------------------------------------------------------------- 1 | export { Result } from './result'; -------------------------------------------------------------------------------- /public/components/result/result.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Component 3 | } from 'react'; 4 | import {AnalyzerResult} from "./analyzer_result"; 5 | import {CompareResult} from "./compare_result"; 6 | import {RESULT_TYPE} from "../../common/constants/tab_names"; 7 | 8 | export class Result extends Component { 9 | 10 | constructor(props) { 11 | super(props); 12 | } 13 | 14 | render() { 15 | const {type} = this.props; 16 | const {detail} = this.props; 17 | const {esRequest} = this.props; 18 | const {indexName} = this.props; 19 | const {showAllTokenAttr} = this.props; 20 | const {resultAnalyzers} = this.props; 21 | if (type === RESULT_TYPE.SINGLE) { 22 | return ( 23 | 29 | ); 30 | } else { 31 | return ( 32 | 35 | ); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /public/index.scss: -------------------------------------------------------------------------------- 1 | .NoMaxWidth { 2 | max-width: none; 3 | } 4 | 5 | .analyzeApiEUIPage { 6 | flex-direction: column; 7 | } 8 | 9 | .analyzeApiTableScroll { 10 | overflow-x: scroll; 11 | border: 1px solid #E4E4E4; 12 | } 13 | 14 | .analyzeApiTableScroll::-webkit-scrollbar{ 15 | height: 0.5em; 16 | background-color: #F1F1F1; 17 | } 18 | .analyzeApiTableScroll::-webkit-scrollbar-thumb{ 19 | background-color: #BCBCBC; 20 | border-radius: 12px; 21 | border-radius: 1.2rem; 22 | } 23 | .analyzeApiTableScroll::-webkit-scrollbar-thumb:hover{ 24 | background-color: #A9A9AA; 25 | } 26 | 27 | .analyzeApiTableCell{ 28 | max-width: none; 29 | padding: 7px 8px 8px; 30 | overflow: unset; 31 | } 32 | 33 | .analyzeApiTableTokenCell{ 34 | margin-left: auto; 35 | } 36 | 37 | .analyzeRequest { 38 | margin: 2px; 39 | border: 1px solid #E4E4E4; 40 | background-color: #F1F1F1; 41 | padding: 7px 8px 8px; 42 | } 43 | 44 | .analyzeApiFontBold { 45 | font-weight: bold; 46 | } 47 | 48 | .analyzeApiTableLayout { 49 | table-layout: auto 50 | } 51 | 52 | .analyzeApiTableRowTop { 53 | vertical-align: top 54 | } 55 | 56 | .analyzeApiTableRowTokenCell { 57 | overflow: hidden; 58 | white-space: nowrap; 59 | display: grid; 60 | } 61 | 62 | .analyzeApiEmptyToken { 63 | color: #a7a7a7; 64 | } 65 | 66 | .analyzeApiTextAlignRight { 67 | text-align: right; 68 | } 69 | 70 | .analyzeApiFormWidthSize { 71 | width: 300px; 72 | } 73 | 74 | .analyzeApiFormHeightSize { 75 | height: 100px; 76 | } 77 | -------------------------------------------------------------------------------- /public/index.ts: -------------------------------------------------------------------------------- 1 | import './index.scss'; 2 | 3 | import { AnalyzeApiUiPlugin } from './plugin'; 4 | 5 | // This exports static code and TypeScript types, 6 | // as well as, Kibana Platform `plugin()` initializer. 7 | export function plugin() { 8 | return new AnalyzeApiUiPlugin(); 9 | } 10 | export { AnalyzeApiUiPluginSetup, AnalyzeApiUiPluginStart } from './types'; 11 | -------------------------------------------------------------------------------- /public/plugin.ts: -------------------------------------------------------------------------------- 1 | import { AppMountParameters, CoreSetup, CoreStart, Plugin } from 'kibana/public'; 2 | import { 3 | AnalyzeApiUiPluginSetup, 4 | AnalyzeApiUiPluginStart, 5 | AppPluginStartDependencies, 6 | } from './types'; 7 | import { PLUGIN_NAME } from '../common'; 8 | 9 | export class AnalyzeApiUiPlugin 10 | implements Plugin 11 | { 12 | public setup(core: CoreSetup): AnalyzeApiUiPluginSetup { 13 | // Register an application into the side navigation menu 14 | core.application.register({ 15 | id: 'analyzeApiUi', 16 | title: PLUGIN_NAME, 17 | async mount(params: AppMountParameters) { 18 | // Load application bundle 19 | const { renderApp } = await import('./application'); 20 | // Get start services as specified in kibana.json 21 | const [coreStart, depsStart] = await core.getStartServices(); 22 | // Render the application 23 | return renderApp(coreStart, depsStart as AppPluginStartDependencies, params); 24 | }, 25 | }); 26 | return {}; 27 | } 28 | 29 | public start(core: CoreStart): AnalyzeApiUiPluginStart { 30 | return {}; 31 | } 32 | 33 | public stop() {} 34 | } 35 | -------------------------------------------------------------------------------- /public/services/api.ts: -------------------------------------------------------------------------------- 1 | let httpClient; 2 | export const setHttpClient = (client) => { 3 | httpClient = client; 4 | }; 5 | export const getHttpClient = () => { 6 | return httpClient; 7 | }; 8 | 9 | export async function analyze(params) { 10 | return await httpClient.post('/api/analyze_api_ui/analyze', {body: JSON.stringify(params)}); 11 | } 12 | 13 | export async function multiAnalyze(params) { 14 | return await httpClient.post('/api/analyze_api_ui/multi_analyze', {body: JSON.stringify(params)}); 15 | } 16 | -------------------------------------------------------------------------------- /public/services/pagestate.ts: -------------------------------------------------------------------------------- 1 | import instance from "./storage"; 2 | 3 | const pagestate = { 4 | 5 | updateCurrentState(content: { params: any; }) { 6 | const timestamp = new Date().getTime(); 7 | instance.set("page_state", { 8 | time: timestamp, 9 | content: content 10 | }); 11 | }, 12 | 13 | getSavedEditorState() { 14 | const saved = instance.get('page_state', null); 15 | if (!saved) return; 16 | const { time, content } = saved; 17 | return { time, content }; 18 | } 19 | 20 | }; 21 | 22 | export default pagestate; 23 | -------------------------------------------------------------------------------- /public/services/params.ts: -------------------------------------------------------------------------------- 1 | export function updateParamsWithEvent(event: any, params: any) { 2 | const target = event.target; 3 | const value = target.type === 'checkbox' || target.name === 'showAllTokenAttr' ? target.checked : target.value; 4 | const name = target.name; 5 | params[name] = value; 6 | return params; 7 | }; 8 | 9 | export function updateParamsWithEventAndIndex(event: any, params: any) { 10 | const index = event.target.getAttribute('data-index'); 11 | const target = event.target; 12 | const value = target.type === 'checkbox' ? target.checked : target.value; 13 | const name = target.name; 14 | 15 | if (params[name] == null) { 16 | params[name] = []; 17 | } 18 | params[name][index] = value; 19 | return params; 20 | }; 21 | 22 | export function selectTab(tab: any, params: any) { 23 | params["tab"] = tab.name; 24 | return params; 25 | } 26 | -------------------------------------------------------------------------------- /public/services/state_handler.ts: -------------------------------------------------------------------------------- 1 | import pagestate from './pagestate'; 2 | 3 | export function initializeParams() { 4 | const params = { 5 | indexName: '', 6 | text: '', 7 | analyzer: '', 8 | tokenizer: '', 9 | charfilters: [], 10 | filters: [], 11 | field: '', 12 | analyzersForCompare: [], 13 | showAllTokenAttr: false, 14 | }; 15 | return params; 16 | } 17 | 18 | // load saved state from storage 19 | export function loadSavedState() { 20 | const preState = pagestate.getSavedEditorState(); 21 | let params; 22 | if (preState) { 23 | params = preState.content.params; 24 | } else { 25 | params = initializeParams(); 26 | } 27 | return params; 28 | } 29 | 30 | // save state 31 | export function saveState(state) { 32 | try { 33 | pagestate.updateCurrentState({ 34 | params: state.params 35 | }); 36 | } catch (e) { 37 | console.log("Ignoring saving error: " + e); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /public/services/storage.ts: -------------------------------------------------------------------------------- 1 | const { transform, keys, startsWith } = require('lodash'); 2 | 3 | class Storage { 4 | engine: globalThis.Storage; 5 | prefix: string; 6 | 7 | constructor(engine: globalThis.Storage, prefix: string) { 8 | this.engine = engine; 9 | this.prefix = prefix; 10 | } 11 | 12 | encode(val: any) { 13 | return JSON.stringify(val); 14 | } 15 | 16 | decode(val: string | null) { 17 | if (typeof val === 'string') { 18 | return JSON.parse(val); 19 | } 20 | } 21 | 22 | encodeKey(key: any) { 23 | return `${this.prefix}${key}`; 24 | } 25 | 26 | decodeKey(key: string | any[]) { 27 | if (startsWith(key, this.prefix)) { 28 | return `${key.slice(this.prefix.length)}`; 29 | } 30 | } 31 | 32 | set(key: any, val: any) { 33 | this.engine.setItem(this.encodeKey(key), this.encode(val)); 34 | return val; 35 | } 36 | 37 | has(key: any) { 38 | return this.engine.getItem(this.encodeKey(key)) != null; 39 | } 40 | 41 | get(key: any, _default: any) { 42 | if (this.has(key)) { 43 | return this.decode(this.engine.getItem(this.encodeKey(key))); 44 | } else { 45 | return _default; 46 | } 47 | } 48 | 49 | delete(key: any) { 50 | return this.engine.removeItem(this.encodeKey(key)); 51 | } 52 | 53 | keys() { 54 | return transform(keys(this.engine), (ours: string[], key: string | any[]) => { 55 | const ourKey = this.decodeKey(key); 56 | if (ourKey != null) ours.push(ourKey); 57 | }); 58 | } 59 | } 60 | 61 | const instance = new Storage(sessionStorage, 'analyzeui:'); 62 | 63 | export default instance; 64 | -------------------------------------------------------------------------------- /public/services/token_utils.ts: -------------------------------------------------------------------------------- 1 | // compare and swap tokenStreamLength 2 | export function getLength(current: number, tokenArray: string | any[] | null) { 3 | // FIXME check if there is synonyms or compound 4 | let length = current; 5 | if (tokenArray != null) { 6 | // FIXME must consider the situation if positionIncrements != 1 7 | if (tokenArray[tokenArray.length -1].position >= current) { 8 | length = tokenArray[tokenArray.length -1].position + 1; 9 | } 10 | } 11 | return length; 12 | }; 13 | 14 | export function createTokenIndices(tokenStreamLength: number) { 15 | const tokenIndices = []; 16 | for (let i = 0; i < tokenStreamLength; i++) { 17 | tokenIndices.push(i); 18 | } 19 | return tokenIndices; 20 | } 21 | -------------------------------------------------------------------------------- /public/services/validator.ts: -------------------------------------------------------------------------------- 1 | import {TAB_NAME} from "../common/constants/tab_names"; 2 | 3 | export function validateAnalyzeRequestValues(params) { 4 | let validatedParams = { 5 | requestParams: { 6 | text: params.text 7 | }, 8 | errors: {} 9 | }; 10 | // console.log("before validation"); 11 | // console.log(params); 12 | // console.log("--------------"); 13 | if (validatedParams.requestParams.text.trim().length === 0) { 14 | validatedParams.errors.textError= 'text should be not null!' 15 | 16 | } 17 | if (params.indexName.length > 0) { 18 | validatedParams.requestParams.indexName = params.indexName.trim(); 19 | } 20 | 21 | if (params.tab == TAB_NAME.ANALYZER) { 22 | if (params.analyzer.length > 0) { 23 | validatedParams.requestParams.analyzer = params.analyzer.trim(); 24 | } 25 | } else if (params.tab == TAB_NAME.FIELD) { 26 | if (params.field.trim().length === 0) 27 | validatedParams.errors.fieldError = 'field is required. '; 28 | if (params.indexName.trim().length === 0) 29 | validatedParams.errors.indexNameError = 'index name is required for "field". '; 30 | 31 | validatedParams.requestParams.field = params.field.trim(); 32 | } else if (params.tab == TAB_NAME.CUSTOM_ANALYZER) { 33 | if (params.tokenizer) { 34 | let tmpObj = parseCustom(params.tokenizer.trim(), "tokenizer", validatedParams); 35 | if (tmpObj != -1) { 36 | validatedParams.requestParams.tokenizer = tmpObj; 37 | tmpObj = null; 38 | } 39 | } 40 | 41 | if (params.charfilters.length > 0) { 42 | params.charfilters.forEach( (charfilter) => { 43 | if (charfilter && charfilter.trim().length > 0 ) { 44 | if(validatedParams.requestParams.charfilters == null) validatedParams.requestParams.charfilters = []; 45 | let tmpCharfilter = parseCustom(charfilter.trim(), "charfilter", validatedParams); 46 | if (tmpCharfilter != -1) { 47 | validatedParams.requestParams.charfilters.push(tmpCharfilter); 48 | } 49 | } 50 | }); 51 | } 52 | if (params.filters.length > 0) { 53 | params.filters.forEach( (filter) => { 54 | if (filter && filter.trim().length > 0 ) { 55 | if(validatedParams.requestParams.filters == null) validatedParams.requestParams.filters = []; 56 | let tmpFilter = parseCustom(filter.trim(), "filter", validatedParams); 57 | if (tmpFilter != -1) { 58 | validatedParams.requestParams.filters.push(tmpFilter); 59 | } 60 | } 61 | }); 62 | } 63 | } else if (params.tab == TAB_NAME.COMPARE_ANALYZERS) { 64 | params.analyzersForCompare.forEach( (analyzer) => { 65 | if (analyzer && analyzer.trim().length > 0) { 66 | if (validatedParams.requestParams.analyzers == null) validatedParams.requestParams.analyzers = []; 67 | validatedParams.requestParams.analyzers.push(analyzer); 68 | } 69 | }); 70 | } 71 | 72 | //console.log("after validation"); 73 | //console.log(validatedParams); 74 | //console.log("--------------"); 75 | return validatedParams; 76 | }; 77 | 78 | function parseCustom(target, label, validatedParams) { 79 | if (typeof target === 'string' && target.startsWith('{')) { 80 | try { 81 | let tmpJson = JSON.parse(target); 82 | if (tmpJson !== null && typeof tmpJson === 'object') { 83 | return tmpJson; 84 | } else { 85 | validatedParams.errors.analyzerError = label+' has wrong custom '+label; 86 | return -1; 87 | } 88 | } catch (e) { 89 | validatedParams.errors.analyzerError = e.message; 90 | return -1; 91 | } 92 | } else { 93 | return target; 94 | } 95 | }; 96 | -------------------------------------------------------------------------------- /public/types.ts: -------------------------------------------------------------------------------- 1 | import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; 2 | 3 | export interface AnalyzeApiUiPluginSetup {} 4 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 5 | export interface AnalyzeApiUiPluginStart {} 6 | 7 | export interface AppPluginStartDependencies { 8 | navigation: NavigationPublicPluginStart; 9 | } 10 | -------------------------------------------------------------------------------- /server/index.ts: -------------------------------------------------------------------------------- 1 | import { PluginInitializerContext } from 'kibana/server'; 2 | import { AnalyzeApiUiPlugin } from './plugin'; 3 | 4 | // This exports static code and TypeScript types, 5 | // as well as, Kibana Platform `plugin()` initializer. 6 | 7 | export function plugin(initializerContext: PluginInitializerContext) { 8 | return new AnalyzeApiUiPlugin(initializerContext); 9 | } 10 | 11 | export { AnalyzeApiUiPluginSetup, AnalyzeApiUiPluginStart } from './types'; 12 | -------------------------------------------------------------------------------- /server/plugin.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PluginInitializerContext, 3 | CoreSetup, 4 | CoreStart, 5 | Plugin, 6 | Logger, 7 | } from 'kibana/server'; 8 | 9 | import { AnalyzeApiUiPluginSetup, AnalyzeApiUiPluginStart } from './types'; 10 | import { defineRoutes } from './routes'; 11 | 12 | export class AnalyzeApiUiPlugin 13 | implements Plugin 14 | { 15 | private readonly logger: Logger; 16 | 17 | constructor(initializerContext: PluginInitializerContext) { 18 | this.logger = initializerContext.logger.get(); 19 | } 20 | 21 | public setup(core: CoreSetup) { 22 | this.logger.debug('analyze-api-ui-plugin: Setup'); 23 | const router = core.http.createRouter(); 24 | 25 | // Register server side APIs 26 | defineRoutes(router, this.logger); 27 | 28 | return {}; 29 | } 30 | 31 | public start(core: CoreStart) { 32 | this.logger.debug('analyze-api-ui-plugin: Started'); 33 | return {}; 34 | } 35 | 36 | public stop() {} 37 | } 38 | -------------------------------------------------------------------------------- /server/routes/handle_es_error.ts: -------------------------------------------------------------------------------- 1 | import Boom from '@hapi/boom'; 2 | import { get } from 'lodash'; 3 | 4 | const ERR_ES_INDEX_NOT_FOUND = 'index_not_found_exception'; 5 | const ERR_ES_ILLEGAL_ARG = 'illegal_argument_exception'; 6 | 7 | export function isEsIndexNotFoundError(err: any) { 8 | return get(err, ['body', 'error', 'type']) === ERR_ES_INDEX_NOT_FOUND; 9 | } 10 | 11 | export function isEsIllegalArgumentError(err: any) { 12 | return get(err, ['body', 'error', 'type']) === ERR_ES_ILLEGAL_ARG; 13 | } 14 | 15 | export function convertEsError(response: any, error: any) { 16 | const message = error.body ? error.body.error.reason : undefined; 17 | if (isEsIndexNotFoundError(error)) { 18 | error = Boom.notFound(message, error); 19 | } else if (isEsIllegalArgumentError(error)) { 20 | error = Boom.badRequest(message, error); 21 | } 22 | 23 | if (!Boom.isBoom(error)) { 24 | const statusCode = error.statusCode; 25 | if (!error.message) { 26 | error.message = message; 27 | } 28 | error = Boom.boomify(error, {statusCode: statusCode, message: message}); 29 | } 30 | return response.customError({ 31 | body: error.output.payload, 32 | statusCode: error.output.statusCode, 33 | headers: error.output.headers as { [key: string]: string }, 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /server/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { IRouter, Logger } from 'kibana/server'; 2 | import { schema } from '@kbn/config-schema'; 3 | import { convertEsError } from "./handle_es_error"; 4 | 5 | export function defineRoutes(router: IRouter, logger: logger) { 6 | router.post( 7 | { 8 | path: '/api/analyze_api_ui/analyze', 9 | validate: { 10 | body: schema.object({ 11 | text: schema.string(), 12 | indexName: schema.maybe(schema.nullable(schema.string())), 13 | analyzer: schema.maybe(schema.nullable(schema.string())), 14 | tokenizer: schema.maybe(schema.oneOf([schema.nullable(schema.string()), schema.object({}, { unknowns: 'allow' })])), 15 | field: schema.maybe(schema.nullable(schema.string())), 16 | filters: schema.maybe(schema.arrayOf(schema.nullable(schema.oneOf([schema.string(), schema.object({}, { unknowns: 'allow' })])))), 17 | charfilters: schema.maybe(schema.arrayOf(schema.nullable(schema.oneOf([schema.string(), schema.object({}, { unknowns: 'allow' })])))) 18 | }) 19 | } 20 | }, 21 | async (context, request, response) => { 22 | let param = { 23 | body: { 24 | explain: true, 25 | text: request.body.text 26 | } 27 | }; 28 | if (request.body.indexName) param.index = request.body.indexName; 29 | if (request.body.analyzer) param.body.analyzer = request.body.analyzer; 30 | if (request.body.tokenizer) param.body.tokenizer = request.body.tokenizer; 31 | if (request.body.charfilters) param.body.char_filter = request.body.charfilters; 32 | if (request.body.field) param.body.field = request.body.field; 33 | if (request.body.filters) param.body.filter = request.body.filters; 34 | try { 35 | const results = await context.core.elasticsearch.client.asCurrentUser.indices.analyze(param); 36 | return response.ok({ 37 | body: { 38 | detail: results.body.detail, 39 | request: param.body 40 | } 41 | }); 42 | } catch (error) { 43 | return convertEsError(response, error); 44 | } 45 | } 46 | ); 47 | 48 | router.post( 49 | { 50 | path: '/api/analyze_api_ui/multi_analyze', 51 | validate: { 52 | body: schema.object({ 53 | text: schema.string(), 54 | indexName: schema.maybe(schema.nullable(schema.string())), 55 | analyzers: schema.nullable(schema.arrayOf(schema.string(), {minSize: 2})) 56 | }) 57 | } 58 | }, 59 | async (context, request, response) => { 60 | let param = { 61 | body: { 62 | explain: false, 63 | text: request.body.text 64 | } 65 | }; 66 | if (request.body.indexName) param.index = request.body.indexName; 67 | const res = { 68 | body: { 69 | resultAnalyzers: [] 70 | } 71 | }; 72 | 73 | function getAnalyzerResult(analyzer, id) { 74 | return new Promise(function (resolve, reject) { 75 | param.body.analyzer = analyzer; 76 | const results = context.core.elasticsearch.client.asCurrentUser.indices.analyze(param) 77 | results.then( 78 | (x) => { 79 | res.body.resultAnalyzers.push({analyzer: analyzer, id: id, tokens: x.body.tokens}); 80 | resolve(res); 81 | } 82 | ); 83 | results.catch(error => { 84 | reject(convertEsError(response, error)); 85 | }); 86 | }); 87 | }; 88 | 89 | if (Array.isArray(request.body.analyzers) && request.body.analyzers.length >= 1) { 90 | try { 91 | const res_promise = await Promise.all(request.body.analyzers.map(getAnalyzerResult)); 92 | res.body.resultAnalyzers.sort( 93 | function (a, b) { 94 | if (a.id < b.id) return -1; 95 | if (a.id > b.id) return 1; 96 | return 0; 97 | } 98 | ); 99 | return response.ok(res); 100 | } catch (error) { 101 | return convertEsError(response, error); 102 | } 103 | } else { 104 | return response.ok(res); 105 | } 106 | } 107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /server/types.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 2 | export interface AnalyzeApiUiPluginSetup {} 3 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 4 | export interface AnalyzeApiUiPluginStart {} 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./target/types" 5 | }, 6 | "include": [ 7 | "index.ts", 8 | "common/**/*.ts", 9 | "public/**/*.ts", 10 | "public/**/*.tsx", 11 | "server/**/*.ts", 12 | "../../typings/**/*" 13 | ], 14 | "exclude": [] 15 | } 16 | --------------------------------------------------------------------------------