├── .editorconfig ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .node-version ├── LICENSE.md ├── README.md ├── build.gradle ├── examples ├── create_index_and_insert_docs.js └── github_archive │ ├── README.md │ └── issues.png ├── gradle.properties ├── gradle ├── utils │ ├── loadPackageJson.gradle │ └── repository.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gulpfile.js ├── img └── timeline.png ├── index.js ├── lib └── migrations │ └── migrations_5 │ ├── __tests__ │ └── functional │ │ ├── migration_1.js │ │ └── scenarios │ │ └── migration_1 │ │ ├── index_data1.js │ │ ├── index_data2.js │ │ ├── index_definition.js │ │ ├── scenario1.js │ │ └── scenario2.js │ └── migration_1.js ├── package.json ├── public ├── __tests__ │ ├── _vis.js │ ├── index.js │ ├── kibi_select.js │ ├── kibi_timeline.js │ └── kibi_timeline_vis_controller.js ├── kibi_timeline.js ├── kibi_timeline_vis.html ├── kibi_timeline_vis.js ├── kibi_timeline_vis.less ├── kibi_timeline_vis_controller.js ├── kibi_timeline_vis_params.html ├── kibi_timeline_vis_params.js ├── lib │ ├── courier │ │ └── _request_queue_wrapped.js │ ├── directives │ │ ├── array_param.js │ │ ├── array_param_add.html │ │ ├── kibi_select.html │ │ ├── kibi_select.js │ │ └── kibi_select_helper.js │ └── helpers │ │ ├── __tests__ │ │ └── timeline_helper.js │ │ ├── array_helper.js │ │ └── timeline_helper.js └── webpackShims │ └── vis-timeline.js └── target ├── kibi_timeline_vis-0.1.0.zip ├── kibi_timeline_vis-0.1.1.zip ├── kibi_timeline_vis-0.1.2.zip ├── kibi_timeline_vis-0.1.3.zip ├── kibi_timeline_vis-0.1.4.zip ├── kibi_timeline_vis-4.4.2-2.zip ├── kibi_timeline_vis-4.4.2.zip ├── kibi_timeline_vis-4.5.3-2.zip ├── kibi_timeline_vis-4.5.3.zip ├── kibi_timeline_vis-4.5.4.zip ├── kibi_timeline_vis-4.6.3-1.zip ├── kibi_timeline_vis-4.6.3.zip ├── kibi_timeline_vis-4.6.4.zip ├── kibi_timeline_vis-5.4.0-1.zip └── kibi_timeline_vis-5.4.0-2.zip /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | insert_final_newline = false -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | parser: babel-eslint 3 | 4 | env: 5 | es6: true 6 | amd: true 7 | node: true 8 | browser: true 9 | mocha: true 10 | 11 | rules: 12 | block-scoped-var: 2 13 | camelcase: [ 2, { properties: never } ] 14 | comma-dangle: 0 15 | comma-style: [ 2, last ] 16 | consistent-return: 0 17 | curly: [ 2, multi-line ] 18 | dot-location: [ 2, property ] 19 | dot-notation: [ 2, { allowKeywords: true } ] 20 | eqeqeq: [ 2, allow-null ] 21 | guard-for-in: 2 22 | indent: [ 2, 2, { SwitchCase: 1 } ] 23 | key-spacing: [ 0, { align: value } ] 24 | max-len: [ 2, 140, 2, { ignoreComments: true, ignoreUrls: true } ] 25 | new-cap: [ 2, { capIsNewExceptions: [ Private ] } ] 26 | no-bitwise: 0 27 | no-caller: 2 28 | no-cond-assign: 0 29 | no-const-assign: 2 30 | no-debugger: 2 31 | no-empty: 2 32 | no-eval: 2 33 | no-extend-native: 2 34 | no-extra-parens: 0 35 | no-irregular-whitespace: 2 36 | no-iterator: 2 37 | no-loop-func: 2 38 | no-multi-spaces: 0 39 | no-multi-str: 2 40 | no-nested-ternary: 2 41 | no-new: 0 42 | no-path-concat: 0 43 | no-proto: 2 44 | no-return-assign: 0 45 | no-script-url: 2 46 | no-sequences: 2 47 | no-shadow: 0 48 | no-trailing-spaces: 2 49 | no-undef: 2 50 | no-underscore-dangle: 0 51 | no-unused-expressions: 0 52 | no-unused-vars: 0 53 | no-use-before-define: [ 2, nofunc ] 54 | no-var: 1 55 | no-with: 2 56 | one-var: [ 2, never ] 57 | prefer-const: 1 58 | quotes: [ 2, single ] 59 | semi-spacing: [ 2, { before: false, after: true } ] 60 | semi: [ 2, always ] 61 | space-after-keywords: [ 2, always ] 62 | space-before-blocks: [ 2, always ] 63 | space-before-function-paren: [ 2, { anonymous: always, named: never } ] 64 | space-in-parens: [ 2, never ] 65 | space-infix-ops: [ 2, { int32Hint: false } ] 66 | space-return-throw-case: [ 2 ] 67 | space-unary-ops: [ 2 ] 68 | strict: [ 2, never ] 69 | valid-typeof: 2 70 | wrap-iife: [ 2, outside ] 71 | yoda: 0 72 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Versions 2 | 3 | Kibi: 4 | 5 | Operating System: 6 | 7 | 8 | ### Behavior 9 | 10 | #### Expected: 11 | 12 | #### Actual: 13 | 14 | ### Steps to reproduce the problem 15 | 16 | 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # 2 | 3 | 4 | Changes proposed in this pull request: 5 | 6 | - 7 | 8 | - 9 | 10 | - 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | npm-debug.log 4 | target/* 5 | package-lock.json 6 | .gradle 7 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 8.11.4 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2015 SIREn Solutions 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this product except in compliance with the License. You may obtain a copy of the License at 4 | 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Kibi/Kibana Timeline Plugin 2 | 3 | This is a plugin for Kibana 4.3+ and Kibi 0.3+ (our extention of Kibana for Relational Data). 4 | 5 | The plugin displays a timeline of events taken from multiple saved searches. 6 | 7 | ![image](img/timeline.png) 8 | 9 | ##Installation 10 | 11 | This plugin can be installed in both: 12 | 13 | * [Kibana: 4.3+](https://www.elastic.co/downloads/past-releases/kibana-4-3-0) 14 | * [Kibi: 0.3+](https://siren.solutions/kibi) (Coming soon ...) 15 | 16 | ### Automatic 17 | 18 | #### Kibi 5.x 19 | ```sh 20 | $ ./bin/kibi-plugin install https://github.com/sirensolutions/kibi_timeline_vis/releases/download/5.5.2/kibi_timeline_vis-5.5.2.zip 21 | ``` 22 | 23 | #### Kibi 4.x 24 | ```sh 25 | $ ./bin/kibi plugin -i kibi_timeline_vis -u https://github.com/sirensolutions/kibi_timeline_vis/releases/download/4.6.4/kibi_timeline_vis-4.6.4.zip 26 | ``` 27 | 28 | ### Manual 29 | 30 | ```sh 31 | $ git clone https://github.com/sirensolutions/kibi_timeline_vis.git 32 | $ cd kibi_timeline_vis 33 | $ npm install 34 | $ npm run package 35 | $ unzip target/kibi_timeline_vis-5.4.0-2.zip -d KIBANA_FOLDER_PATH/plugins/ 36 | ``` 37 | 38 | ## Uninstall 39 | 40 | #### Kibi/Kibana 5.x 41 | ```sh 42 | $ bin/kibi-plugin remove kibi_timeline_vis 43 | ``` 44 | 45 | #### Kibi/Kibana 4.x 46 | ```sh 47 | $ bin/kibi plugin --remove kibi_timeline_vis 48 | ``` 49 | 50 | ## Development 51 | 52 | - Clone the repository at the same level of a Kibana > 4.2 clone 53 | - If needed, switch to the same node version as Kibana using nvm 54 | (e.g. `nvm use 6.9.0`) 55 | - Install dependencies with `npm install` 56 | - Install the plugin to Kibana and start watching for changes by running 57 | `npm start` 58 | - run tests with `npm test` 59 | 60 | If you are running kibana from folder with a name other than kibana, e.g. kibi-internal 61 | 62 | ```sh 63 | $ gulp dev --kibanahomepath=../kibi-internal 64 | $ gulp test --kibanahomepath=../kibi-internal 65 | $ gulp testdev --kibanahomepath=../kibi-internal 66 | ``` 67 | 68 | The best setup for development is to run 2 parallel terminals with: 69 | 70 | ```sh 71 | $ gulp dev --kibanahomepath=../kibi-internal 72 | $ gulp testdev --kibanahomepath=../kibi-internal 73 | ``` 74 | 75 | In this way files are synced on every change 76 | and you can reload the browser to re-run tests 77 | In the browser address bar put ```?grep=Kibi Timeline``` 78 | to execute test relevant for this visualisation 79 | 80 | 81 | ## Breaking changes with respect to the version embedded in Kibi 0.1x and 0.2.x 82 | 83 | Dependencies ported from kibi so it can be installed in kibana 84 | 85 | ``` 86 | ui/kibi/components/courier/_request_queue_wrapped 87 | ui/kibi/directives/array_param 88 | ui/kibi/directives/kibi_select 89 | ui/kibi/helpers/array_helper 90 | ``` 91 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////// 2 | // // 3 | // Dependency Details // 4 | // // 5 | /////////////////////////////////////////////////// 6 | 7 | buildscript { 8 | 9 | repositories { 10 | maven { 11 | url "https://plugins.gradle.org/m2/" 12 | } 13 | } 14 | 15 | dependencies { 16 | classpath "com.moowork.gradle:gradle-node-plugin:1.3.1" 17 | classpath 'org.ajoberstar:gradle-git:1.6.0' 18 | } 19 | 20 | } 21 | 22 | apply plugin: "idea" 23 | apply plugin: "base" 24 | apply plugin: 'com.moowork.gulp' 25 | apply plugin: 'com.moowork.grunt' 26 | apply plugin: 'com.moowork.node' 27 | apply plugin: 'org.ajoberstar.grgit' 28 | apply plugin: 'maven-publish' 29 | apply from: "$rootDir/gradle/utils/repository.gradle" 30 | apply from: "$rootDir/gradle/utils/loadPackageJson.gradle" 31 | 32 | import org.ajoberstar.grgit.Grgit 33 | 34 | def packageJson = loadPackageJson() 35 | 36 | /////////////////////////////////////////////////// 37 | // // 38 | // Project Details // 39 | // // 40 | /////////////////////////////////////////////////// 41 | 42 | group = 'solutions.siren' 43 | version = customTimelineVersion ?: packageJson.version 44 | 45 | /////////////////////////////////////////////////// 46 | // // 47 | // Install Kibi // 48 | // // 49 | /////////////////////////////////////////////////// 50 | 51 | /** 52 | * Define the kibi source artifact dependency 53 | */ 54 | configurations { 55 | kibi 56 | } 57 | 58 | dependencies { 59 | kibi "solutions.siren:investigate-core:${project.version}:sources@zip" 60 | } 61 | 62 | /** 63 | * Task to download locally the kibi dependency 64 | */ 65 | task downloadKibi(type: Copy) { 66 | from configurations.kibi 67 | into new File(buildDir, 'dependencies') 68 | rename { name -> 69 | def artifact = configurations.kibi.resolvedConfiguration.resolvedArtifacts.find { it.file.name == name } 70 | "${artifact.name}.${artifact.extension}" 71 | } 72 | } 73 | 74 | /** 75 | * Task to unzip kibi 76 | */ 77 | task extractKibi(type: Copy, dependsOn: [downloadKibi]) { 78 | from { zipTree(new File(downloadKibi.destinationDir, "investigate-core.zip")) } 79 | into new File(buildDir, 'investigate-core') 80 | } 81 | 82 | /** 83 | * Task to execute the `npmInstall` gradle's task from the kibi distribution 84 | */ 85 | task npmInstallKibi(type: GradleBuild, dependsOn: [extractKibi]) { 86 | dir new File(extractKibi.destinationDir, "investigate-core-${project.version}-sources") 87 | tasks = ['npmInstall'] 88 | } 89 | 90 | /** 91 | * Main task to install Kibi locally 92 | */ 93 | task installKibi(dependsOn: [downloadKibi, extractKibi, npmInstallKibi]) {} 94 | 95 | /////////////////////////////////////////////////// 96 | // // 97 | // Node // 98 | // // 99 | /////////////////////////////////////////////////// 100 | 101 | node { 102 | // Version of node to use. 103 | version = nodeVersion 104 | 105 | // Base URL for fetching node distributions (change if you have a mirror). 106 | distBaseUrl = 'https://nodejs.org/dist' 107 | 108 | // If true, it will download node using above parameters. 109 | // If false, it will try to use globally installed node. 110 | download = true 111 | 112 | // Set the work directory for unpacking node 113 | workDir = file("${buildDir}/nodejs") 114 | 115 | // Set the work directory where node_modules should be located 116 | nodeModulesDir = file("${projectDir}") 117 | } 118 | 119 | /////////////////////////////////////////////////// 120 | // // 121 | // Package // 122 | // // 123 | /////////////////////////////////////////////////// 124 | 125 | task gulpPackage(dependsOn: ['gulp_package']) << {} 126 | 127 | // run npm install 128 | gulp_package.dependsOn 'npmInstall' 129 | 130 | // run gulp install 131 | gulp_package.dependsOn 'installGulp' 132 | 133 | /////////////////////////////////////////////////// 134 | // // 135 | // Install Grunt Globally // 136 | // // 137 | /////////////////////////////////////////////////// 138 | 139 | task installGruntGlobally(type: NpmTask) { 140 | args = ['install', '-g', 'grunt'] 141 | } 142 | 143 | /////////////////////////////////////////////////// 144 | // // 145 | // Dev // 146 | // // 147 | /////////////////////////////////////////////////// 148 | 149 | task gulpDev(type: GulpTask) { 150 | args = ["dev"] 151 | if (project.hasProperty('kibiHomePath')) { 152 | args << "--kibanahomepath=${kibiHomePath}" 153 | } 154 | else { 155 | dependsOn installKibi 156 | args << "--kibanahomepath=${new File(extractKibi.destinationDir, "investigate-core-${project.version}-sources").getAbsolutePath()}" 157 | } 158 | } 159 | 160 | // run npm install 161 | gulpDev.dependsOn 'npmInstall' 162 | 163 | // run gulp install 164 | gulpDev.dependsOn 'installGulp' 165 | 166 | // makes that grunt is installed 167 | gulpDev.dependsOn 'installGruntGlobally' 168 | gulpDev.dependsOn 'installGrunt' 169 | installGrunt.mustRunAfter 'installGruntGlobally' 170 | 171 | /////////////////////////////////////////////////// 172 | // // 173 | // Test // 174 | // // 175 | /////////////////////////////////////////////////// 176 | 177 | task gulpTest(type: GulpTask) { 178 | args = ["test"] 179 | if (project.hasProperty('kibiHomePath')) { 180 | args << "--kibanahomepath=${kibiHomePath}" 181 | } 182 | else { 183 | dependsOn installKibi 184 | args << "--kibanahomepath=${new File(extractKibi.destinationDir, "investigate-core-${project.version}-sources").getAbsolutePath()}" 185 | } 186 | } 187 | 188 | // run npm install 189 | gulpTest.dependsOn 'npmInstall' 190 | 191 | // run gulp install 192 | gulpTest.dependsOn 'installGulp' 193 | 194 | // makes that grunt is installed 195 | gulpTest.dependsOn 'installGruntGlobally' 196 | gulpTest.dependsOn 'installGrunt' 197 | installGrunt.mustRunAfter 'installGruntGlobally' 198 | 199 | /////////////////////////////////////////////////// 200 | // // 201 | // Test Dev // 202 | // // 203 | /////////////////////////////////////////////////// 204 | 205 | task gulpTestDev(type: GulpTask) { 206 | args = ["testdev"] 207 | if (project.hasProperty('kibiHomePath')) { 208 | args << "--kibanahomepath=${kibiHomePath}" 209 | } 210 | else { 211 | dependsOn installKibi 212 | args << "--kibanahomepath=${new File(extractKibi.destinationDir, "investigate-core-${project.version}-sources").getAbsolutePath()}" 213 | } 214 | } 215 | 216 | // run npm install 217 | gulpTestDev.dependsOn 'npmInstall' 218 | 219 | // run gulp install 220 | gulpTestDev.dependsOn 'installGulp' 221 | 222 | // makes that grunt is installed 223 | gulpTestDev.dependsOn 'installGruntGlobally' 224 | gulpTestDev.dependsOn 'installGrunt' 225 | installGrunt.mustRunAfter 'installGruntGlobally' 226 | 227 | /////////////////////////////////////////////////// 228 | // // 229 | // Clean // 230 | // // 231 | /////////////////////////////////////////////////// 232 | 233 | // Extends clean task to clean the node_modules directory 234 | clean << { 235 | def nodeModulesDir = file("${projectDir}/node_modules") 236 | if (nodeModulesDir.exists()) { 237 | println "=== Cleaning node modules directory: ${nodeModulesDir} ===" 238 | nodeModulesDir.deleteDir() 239 | } 240 | } 241 | 242 | // Executes the Gulp's clean task 243 | clean.dependsOn 'gulp_clean' 244 | 245 | /////////////////////////////////////////////////// 246 | // // 247 | // Publishing // 248 | // // 249 | /////////////////////////////////////////////////// 250 | 251 | publishing { 252 | repositories getArtifactoryRepository() 253 | publications { 254 | zipDist(MavenPublication) { 255 | artifact(new File("${projectDir}/target/gulp", "${packageJson.name}.zip")) 256 | groupId project.group 257 | artifactId "kibi-timeline-vis" 258 | version project.version 259 | } 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /examples/create_index_and_insert_docs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var elasticsearch = require('elasticsearch'); 4 | var format = require('string-template'); 5 | 6 | var ES_INDEX_NAME = 'spaceship_project', 7 | ES_PORT = 9220, 8 | ES_HOST = 'localhost', 9 | ES_DOC_TYPE = 'spaceship', 10 | LOG_NAME = 'trace'; 11 | 12 | var ES_DOCS = [ 13 | { 14 | id: '1', 15 | title: ES_DOC_TYPE, 16 | label: 'Fuselage', 17 | produced_date: '2010-09-05', 18 | end_of_work_date: '2030-09-05', 19 | meta: { 20 | first_battle: '2011-09-05', 21 | last_battle: '2029-09-05' 22 | } 23 | }, 24 | { 25 | id: '2', 26 | title: ES_DOC_TYPE, 27 | label: ['Tanks'], 28 | produced_date: ['2010-04-01', '2011-06-09'], 29 | end_of_work_date: ['2023-04-01'], 30 | meta: { 31 | first_battle: ['2011-09-06'], 32 | last_battle: ['2022-09-05'] 33 | } 34 | }, 35 | { 36 | id: '3', 37 | title: ES_DOC_TYPE, 38 | label: ['Rudder', 'Wing'], 39 | produced_date: ['2012-03-01', '2013-05-01', '2014-08-23'], 40 | end_of_work_date: ['2022-03-01', '2023-05-01', '2024-08-23'], 41 | meta: { 42 | first_battle: ['2012-04-06', '2013-06-01', '2014-08-23'], 43 | last_battle: ['2021-03-01', '2021-05-01', '2021-08-23'] 44 | } 45 | }, 46 | { 47 | id: '4', 48 | title: ES_DOC_TYPE, 49 | label: ['Pumps', 'Oxidizer'], 50 | produced_date: ['2015-01-01', '2016-02-01'], 51 | end_of_work_date: ['2022-01-01', '2022-02-01'], 52 | meta: { 53 | first_battle: ['2015-02-01', '2016-03-01'], 54 | last_battle: ['2021-01-01', '2020-02-01'] 55 | } 56 | }, 57 | { 58 | id: '5', 59 | title: ES_DOC_TYPE, 60 | label: ['Engine', 'Nozzle', 'Exhaust'], 61 | produced_date: ['2013-01-01', '2014-02-01', '2016-04-07'], 62 | end_of_work_date: ['2023-01-01', '2024-02-01', '2026-04-07'], 63 | meta: { 64 | first_battle: ['2014-01-01', '2015-02-01', '2017-04-07'], 65 | last_battle: ['2022-01-01', '2023-02-01', '2025-04-07'] 66 | } 67 | } 68 | ]; 69 | 70 | function indice_doc_into_es(client, doc, callback) { 71 | client.create({ 72 | index: ES_INDEX_NAME, 73 | type: ES_DOC_TYPE, 74 | id: doc.id, 75 | body: { 76 | title: doc.title, 77 | label: doc.label, 78 | produced_date: doc.produced_date, 79 | end_of_work_date: doc.end_of_work_date, 80 | meta: doc.meta 81 | } 82 | }, function (error, response) { 83 | if (error) { 84 | console.log(format('Error: {0}', error)); 85 | } else { 86 | console.log(response); 87 | } 88 | }); 89 | 90 | if (callback) { 91 | callback(); 92 | } 93 | } 94 | 95 | function search_index (client, query) { 96 | console.log('DOCUMENTS:'); 97 | client.search({ 98 | index: ES_INDEX_NAME, 99 | q: query 100 | }, function (error, response) { 101 | if (error) { 102 | console.log(format('Error: {0}', error)); 103 | } else { 104 | console.log(response); 105 | } 106 | }); 107 | } 108 | 109 | // init ES client API 110 | var client = new elasticsearch.Client({ 111 | host: format('{0}:{1}', [ES_HOST, ES_PORT]), 112 | log: LOG_NAME 113 | }); 114 | 115 | // delete index 116 | client.indices.delete({ 117 | index: ES_INDEX_NAME, 118 | ignore: [404] 119 | }).then(function () { 120 | console.log(format('The index {0} was deleted!', ES_INDEX_NAME)); 121 | }, function (error) { 122 | console.log(format('Error: {0}', error)); 123 | }); 124 | 125 | // create index 126 | client.indices.create({ 127 | index: ES_INDEX_NAME, 128 | ignore: [404] 129 | }).then(function () { 130 | console.log(format('The index {0} has been created!', ES_INDEX_NAME)); 131 | 132 | var doc; 133 | // insert documents 134 | for (doc in ES_DOCS) { 135 | if (ES_DOCS.hasOwnProperty(doc)) { 136 | indice_doc_into_es(client, ES_DOCS[doc]); 137 | } 138 | } 139 | 140 | }, function (error) { 141 | console.log(format('Error: {0}', error)); 142 | }); 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /examples/github_archive/README.md: -------------------------------------------------------------------------------- 1 | # Github Archive Dataset 2 | 3 | Display events from Github. The data comes from https://www.githubarchive.org/. 4 | With the timeline visualization, you can see for example the issues opened for a particular repository. 5 | 6 | ## Sample 7 | 8 | ```json 9 | { 10 | "id": "2533570550", 11 | "type": "PushEvent", 12 | "actor": { 13 | "id": 3831984, 14 | "login": "gbrlmrllo", 15 | "gravatar_id": "", 16 | "url": "https://api.github.com/users/gbrlmrllo", 17 | "avatar_url": "https://avatars.githubusercontent.com/u/3831984?" 18 | }, 19 | "repo": { 20 | "id": 22930547, 21 | "name": "inyxtech/inyxmater_rails", 22 | "url": "https://api.github.com/repos/inyxtech/inyxmater_rails" 23 | }, 24 | "payload": { 25 | "push_id": 554949339, 26 | "size": 1, 27 | "distinct_size": 1, 28 | "ref": "refs/heads/master", 29 | "head": "f1e21b1d0e2569aeaa111a08c2bd9087d9a88db4", 30 | "before": "22f4f6719b2448c38baf9c32295759269309c7dd", 31 | "commits": [ 32 | { 33 | "sha": "f1e21b1d0e2569aeaa111a08c2bd9087d9a88db4", 34 | "author": { 35 | "email": "f555d375048807db8b7f220ef3030fe1c898752f@gbrlmrllo-M2421.(none)", 36 | "name": "luprz" 37 | }, 38 | "message": "dropdown css", 39 | "distinct": true, 40 | "url": "https://api.github.com/repos/inyxtech/inyxmater_rails/commits/f1e21b1d0e2569aeaa111a08c2bd9087d9a88db4" 41 | } 42 | ] 43 | }, 44 | "public": true, 45 | "created_at": "2015-01-24T01:00:00Z" 46 | } 47 | ``` 48 | 49 | ## Data Loading 50 | 51 | You can use the logtash configuration below for indexing the documents: 52 | 53 | ```rb 54 | input { 55 | stdin { 56 | } 57 | } 58 | filter { 59 | json { 60 | source => "message" 61 | } 62 | mutate { 63 | remove_field => [ "message", "@version", "@timestamp", "host" ] 64 | } 65 | } 66 | output { 67 | elasticsearch { 68 | document_type => "2015" 69 | index => "githubarchive" 70 | hosts => ["127.0.0.1:9200"] 71 | } 72 | } 73 | ``` 74 | 75 | ## Configuration 76 | 77 | Create a saved search with the query `type:IssuesEvent AND _exists_:payload.issue.created_at` to see Github issues. Then, set the timeline visualization to use that search. You will see something like below: 78 | 79 | ![github-issues](issues.png) 80 | -------------------------------------------------------------------------------- /examples/github_archive/issues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirensolutions/kibi_timeline_vis/8289bdb4d8585378bbc2c9b74aa4332b7483f089/examples/github_archive/issues.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | nodeVersion=8.11.4 2 | 3 | # placeholders needed so gradle will not complain about missing properties during init phase 4 | # these are provided by Jenkins 5 | artifactory_username= 6 | artifactory_password= 7 | artifactory_url= 8 | 9 | customTimelineVersion= 10 | -------------------------------------------------------------------------------- /gradle/utils/loadPackageJson.gradle: -------------------------------------------------------------------------------- 1 | /** 2 | * Required for the loadPackageJson method 3 | */ 4 | apply plugin: 'groovy' 5 | import groovy.json.JsonSlurper 6 | 7 | def loadPackageJson() { 8 | File packageJson = new File(projectDir, "package.json") 9 | return new JsonSlurper().parseText(packageJson.text) 10 | } 11 | 12 | project.ext { 13 | loadPackageJson = this.&loadPackageJson 14 | } 15 | -------------------------------------------------------------------------------- /gradle/utils/repository.gradle: -------------------------------------------------------------------------------- 1 | def getArtifactoryRepository() { 2 | return { 3 | maven { 4 | url project.artifactory_url + "/" + (project.version.contains('SNAPSHOT') ? 'libs-snapshot-local' : 'libs-release-local') 5 | credentials { 6 | username project.artifactory_username 7 | password project.artifactory_password 8 | } 9 | } 10 | } 11 | } 12 | 13 | project.ext { 14 | getArtifactoryRepository = this.&getArtifactoryRepository 15 | } 16 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirensolutions/kibi_timeline_vis/8289bdb4d8585378bbc2c9b74aa4332b7483f089/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jul 06 10:24:47 IST 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var _ = require('lodash'); 3 | var path = require('path'); 4 | var mkdirp = require('mkdirp'); 5 | var Rsync = require('rsync'); 6 | var Promise = require('bluebird'); 7 | var eslint = require('gulp-eslint'); 8 | var rimraf = require('rimraf'); 9 | var zip = require('gulp-zip'); 10 | var fs = require('fs'); 11 | var spawn = require('child_process').spawn; 12 | var minimist = require('minimist'); 13 | const gulpDebug = require('gulp-debug'); 14 | const util = require('gulp-util'); 15 | 16 | var pkg = require('./package.json'); 17 | var packageName = pkg.name; 18 | 19 | var buildDir = path.resolve(__dirname, 'build/gulp'); 20 | var targetDir = path.resolve(__dirname, 'target/gulp'); 21 | var buildTarget = path.resolve(buildDir, 'kibana', packageName); 22 | 23 | var include = [ 24 | 'package.json', 25 | 'index.js', 26 | 'public', 27 | 'lib' 28 | ]; 29 | 30 | var knownOptions = { 31 | string: 'kibanahomepath', 32 | default: { kibanahomepath: '../kibi-internal' } 33 | }; 34 | var options = minimist(process.argv.slice(2), knownOptions); 35 | 36 | var kibanaPluginDir = path.resolve(__dirname, options.kibanahomepath + '/siren_plugins/' + packageName); 37 | 38 | 39 | function syncPluginTo(dest, done) { 40 | mkdirp(dest, function (err) { 41 | if (err) return done(err); 42 | Promise.all(include.map(function (name) { 43 | var source = path.resolve(__dirname, name); 44 | return new Promise(function (resolve, reject) { 45 | var rsync = new Rsync(); 46 | rsync 47 | .source(source) 48 | .destination(dest) 49 | .flags('uav') 50 | .recursive(true) 51 | .set('delete') 52 | .output(function (data) { 53 | process.stdout.write(data.toString('utf8')); 54 | }); 55 | rsync.execute(function (err) { 56 | if (err) { 57 | console.log(err); 58 | return reject(err); 59 | } 60 | resolve(); 61 | }); 62 | }); 63 | })) 64 | .then(function () { 65 | return new Promise(function (resolve, reject) { 66 | mkdirp(path.join(buildTarget, 'node_modules'), function (err) { 67 | if (err) return reject(err); 68 | resolve(); 69 | }); 70 | }); 71 | }) 72 | .then(function () { 73 | spawn('npm', ['install', '--production'], { 74 | cwd: dest, 75 | stdio: 'inherit' 76 | }) 77 | .on('close', done); 78 | }) 79 | .catch(done); 80 | }); 81 | } 82 | 83 | gulp.task('sync', function (done) { 84 | syncPluginTo(kibanaPluginDir, done); 85 | }); 86 | 87 | gulp.task('lint', function (done) { 88 | return gulp.src([ 89 | 'public/**/*.js', 90 | 'lib/**/*', 91 | '!**/webpackShims/**' 92 | ]) 93 | .pipe(gulpDebug()) 94 | .pipe(eslint()) 95 | .pipe(eslint.formatEach()) 96 | .pipe(eslint.failOnError()); 97 | }); 98 | 99 | gulp.task('lintFix', function (done) { 100 | return gulp.src([ 101 | 'public/**/*.js', 102 | 'lib/**/*', 103 | '!**/webpackShims/**' 104 | ]).pipe(eslint({ 105 | fix: true 106 | })) 107 | .pipe(eslint.formatEach()) 108 | .pipe(eslint.failOnError()); 109 | }); 110 | 111 | gulp.task('clean', function (done) { 112 | Promise.each([buildDir, targetDir], function (dir) { 113 | return new Promise(function (resolve, reject) { 114 | rimraf(dir, function (err) { 115 | if (err) return reject(err); 116 | resolve(); 117 | }); 118 | }); 119 | }).nodeify(done); 120 | }); 121 | 122 | gulp.task('build', ['clean'], function (done) { 123 | syncPluginTo(buildTarget, done); 124 | }); 125 | 126 | gulp.task('package', ['build'], function (done) { 127 | return gulp.src([ 128 | path.join(buildDir, '**', '*'), 129 | '!**/node_modules/vis/examples/**', 130 | '!**/node_modules/vis/docs/**', 131 | '!**/node_modules/moment/**', 132 | '!**/node_modules/vis/examples', 133 | '!**/node_modules/vis/docs', 134 | '!**/node_modules/moment' 135 | ]) 136 | .pipe(zip(packageName + '.zip')) 137 | .pipe(gulp.dest(targetDir)); 138 | }); 139 | 140 | gulp.task('dev', ['sync'], function (done) { 141 | gulp.watch([ 142 | 'package.json', 143 | 'index.js', 144 | 'public/**/*', 145 | 'lib/**/*' 146 | ], ['sync', 'lint']); 147 | }); 148 | 149 | const grepOption = '--grep=' + (util.env.grep ? util.env.grep : 'Kibi Timeline'); 150 | 151 | gulp.task('test', ['sync'], function(done) { 152 | spawn('grunt', ['test:server', 'test:browser', grepOption], { 153 | cwd: options.kibanahomepath, 154 | stdio: 'inherit' 155 | }).on('close', done); 156 | }); 157 | 158 | gulp.task('testserver', ['sync'], function (done) { 159 | spawn('grunt', ['test:server', grepOption], { 160 | cwd: options.kibanahomepath, 161 | stdio: 'inherit' 162 | }).on('close', done); 163 | }); 164 | 165 | gulp.task('testdev', ['sync'], function(done) { 166 | spawn('grunt', ['test:dev', '--browser=Chrome'], { 167 | cwd: options.kibanahomepath, 168 | stdio: 'inherit' 169 | }).on('close', done); 170 | }); 171 | 172 | gulp.task('coverage', ['sync'], function(done) { 173 | spawn('grunt', ['test:coverage', grepOption], { 174 | cwd: options.kibanahomepath, 175 | stdio: 'inherit' 176 | }).on('close', done); 177 | }); 178 | -------------------------------------------------------------------------------- /img/timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirensolutions/kibi_timeline_vis/8289bdb4d8585378bbc2c9b74aa4332b7483f089/img/timeline.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (kibana) { 2 | 3 | const migrations = [ 4 | require('./lib/migrations/migrations_5/migration_1') 5 | ]; 6 | 7 | return new kibana.Plugin({ 8 | name: 'kibi_timeline', 9 | require: ['kibana', 'elasticsearch'], 10 | uiExports: { 11 | visTypes: [ 12 | 'plugins/kibi_timeline_vis/kibi_timeline_vis' 13 | ] 14 | }, 15 | init: function(server) { 16 | server.expose('getMigrations', () => migrations); 17 | } 18 | }); 19 | }; 20 | 21 | -------------------------------------------------------------------------------- /lib/migrations/migrations_5/__tests__/functional/migration_1.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect.js'; 2 | import sinon from 'sinon'; 3 | import requirefrom from 'requirefrom'; 4 | import Migration from '../../migration_1'; 5 | import Scenario1 from './scenarios/migration_1/scenario1'; 6 | import Scenario2 from './scenarios/migration_1/scenario2'; 7 | import url from 'url'; 8 | 9 | const serverConfig = requirefrom('test')('server_config'); 10 | const indexSnapshot = requirefrom('src/test_utils')('index_snapshot'); 11 | const ScenarioManager = requirefrom('src/test_utils')('scenario_manager'); 12 | const { Cluster } = requirefrom('src/core_plugins/elasticsearch/lib')('cluster'); 13 | 14 | describe('investigate_core/migrations/functional', function () { 15 | 16 | const clusterUrl = url.format(serverConfig.servers.elasticsearch); 17 | const timeout = 60000; 18 | this.timeout(timeout); 19 | 20 | const fakeConfig = { 21 | get: sinon.stub() 22 | }; 23 | fakeConfig.get.withArgs('kibana.index').returns('.siren'); 24 | 25 | const scenarioManager = new ScenarioManager(clusterUrl, timeout); 26 | const cluster = new Cluster({ 27 | url: clusterUrl, 28 | ssl: { verificationMode: 'none' }, 29 | requestTimeout: timeout 30 | }); 31 | const configuration = { 32 | config: fakeConfig, 33 | client: cluster.getClient(), 34 | logger: { 35 | warning: (message) => '' 36 | } 37 | }; 38 | 39 | async function snapshot() { 40 | return indexSnapshot(cluster, '.siren'); 41 | } 42 | 43 | describe('Kibi Timeline - Migration 1 - Functional test', function () { 44 | 45 | describe('there is no visualisations', function () { 46 | 47 | beforeEach(async () => { 48 | await scenarioManager.reload(Scenario2); 49 | }); 50 | 51 | it('should count objects and get 0', async () => { 52 | const migration = new Migration(configuration); 53 | const result = await migration.count(); 54 | expect(result).to.be(0); 55 | }); 56 | 57 | it('should NOT upgrade any objects', async () => { 58 | const migration = new Migration(configuration); 59 | const result = await migration.upgrade(); 60 | expect(result).to.be(0); 61 | }); 62 | 63 | afterEach(async () => { 64 | await scenarioManager.unload(Scenario2); 65 | }); 66 | 67 | }); 68 | 69 | describe('there are some visualisations', function () { 70 | 71 | beforeEach(async () => { 72 | await scenarioManager.reload(Scenario1); 73 | }); 74 | 75 | it('should count all upgradeable objects', async () => { 76 | const migration = new Migration(configuration); 77 | const result = await migration.count(); 78 | expect(result).to.be(1); 79 | }); 80 | 81 | it('should upgrade all upgradeable objects', async () => { 82 | const before = await snapshot(); 83 | const migration = new Migration(configuration); 84 | 85 | const result = await migration.upgrade(); 86 | expect(result).to.be(1); 87 | 88 | const after = await snapshot(); 89 | 90 | expect(after.size).to.be(before.size); 91 | expect(before.get('timeline-to-not-upgrade')).to.eql(after.get('timeline-to-not-upgrade')); 92 | 93 | const original = before.get('timeline-to-upgrade'); 94 | const upgraded = after.get('timeline-to-upgrade'); 95 | 96 | for (const key of [ 97 | 'description', 98 | 'kibanaSavedObjectMeta', 99 | 'title', 100 | 'uiStateJSON', 101 | 'version' 102 | ]) { 103 | expect(original[key]).to.eql(upgraded[key]); 104 | } 105 | 106 | const upgradedVisState = JSON.parse(upgraded._source.visState); 107 | 108 | expect(upgradedVisState).to.eql({ 109 | title: 'timeline1', 110 | type: 'kibi_timeline_vis', 111 | params: { 112 | groups: [ 113 | { 114 | id: 5000 115 | } 116 | ] 117 | } 118 | }); 119 | }); 120 | 121 | afterEach(async () => { 122 | await scenarioManager.unload(Scenario1); 123 | }); 124 | 125 | }); 126 | }); 127 | 128 | }); 129 | -------------------------------------------------------------------------------- /lib/migrations/migrations_5/__tests__/functional/scenarios/migration_1/index_data1.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Defines the following objects: 3 | * 4 | * - a timeline visualization which should be upgraded 5 | * - a timeline visualization which should NOT be upgraded 6 | */ 7 | module.exports = [ 8 | { 9 | index: { 10 | _index: '.siren', 11 | _type: 'visualization', 12 | _id: 'timeline-to-upgrade' 13 | } 14 | }, 15 | { 16 | description : '', 17 | kibanaSavedObjectMeta : {}, 18 | title : 'timeline1', 19 | uiStateJSON : '{}', 20 | version : 1, 21 | visState : JSON.stringify({ 22 | title: 'timeline1', 23 | type: 'kibi_timeline_vis', 24 | params: { 25 | groups: [ 26 | { 27 | id: 5000, 28 | indexPatternId: 'article', 29 | } 30 | ] 31 | } 32 | }) 33 | }, 34 | { 35 | index: { 36 | _index: '.siren', 37 | _type: 'visualization', 38 | _id: 'timeline-to-not-upgrade' 39 | } 40 | }, 41 | { 42 | description : '', 43 | kibanaSavedObjectMeta : {}, 44 | title : 'timeline2', 45 | uiStateJSON : '{}', 46 | version : 1, 47 | visState : JSON.stringify({ 48 | title: 'timeline2', 49 | type: 'kibi_timeline_vis' 50 | }) 51 | } 52 | ]; 53 | -------------------------------------------------------------------------------- /lib/migrations/migrations_5/__tests__/functional/scenarios/migration_1/index_data2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Defines the index with no visualisations 3 | */ 4 | module.exports = [ 5 | { 6 | index: { 7 | _index: '.siren', 8 | _type: 'foo', 9 | _id: 'some-object-which-is-not-a-visualisation' 10 | } 11 | }, 12 | { 13 | title : 'some_object', 14 | version : 1, 15 | visState : '{}' 16 | } 17 | ]; 18 | -------------------------------------------------------------------------------- /lib/migrations/migrations_5/__tests__/functional/scenarios/migration_1/index_definition.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | settings: { 3 | number_of_shards: 1 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /lib/migrations/migrations_5/__tests__/functional/scenarios/migration_1/scenario1.js: -------------------------------------------------------------------------------- 1 | export default { 2 | baseDir: __dirname, 3 | bulk: [{ 4 | indexName: '.siren', 5 | indexDefinition: 'index_definition.js', 6 | source: 'index_data1.js', 7 | haltOnFailure: true 8 | }] 9 | }; 10 | -------------------------------------------------------------------------------- /lib/migrations/migrations_5/__tests__/functional/scenarios/migration_1/scenario2.js: -------------------------------------------------------------------------------- 1 | export default { 2 | baseDir: __dirname, 3 | bulk: [{ 4 | indexName: '.siren', 5 | indexDefinition: 'index_definition.js', 6 | source: 'index_data2.js', 7 | haltOnFailure: true 8 | }] 9 | }; 10 | -------------------------------------------------------------------------------- /lib/migrations/migrations_5/migration_1.js: -------------------------------------------------------------------------------- 1 | import { transform, get, cloneDeep } from 'lodash'; 2 | import Migration from 'kibiutils/lib/migrations/migration'; 3 | 4 | /** 5 | * Timeline - Migration 1. 6 | * 7 | * Looks for groups object with an obsolete indexPattern property 8 | * 9 | * - delets it when found 10 | */ 11 | 12 | export default class Migration1 extends Migration { 13 | 14 | constructor(configuration) { 15 | super(configuration); 16 | 17 | this._client = configuration.client; 18 | this._index = configuration.config.get('kibana.index'); 19 | this._logger = configuration.logger; 20 | this._type = 'visualization'; 21 | } 22 | 23 | static get description() { 24 | return 'Delete obsolete indexPattern property'; 25 | } 26 | 27 | _isUpgradeable(visState) { 28 | if (visState.type === 'kibi_timeline_vis' && get(visState, 'params.groups')) { 29 | for (let i = 0; i < visState.params.groups.length; i++) { 30 | const group = visState.params.groups[i]; 31 | if (group.indexPatternId !== undefined) { 32 | return true; 33 | } 34 | } 35 | } 36 | 37 | return false; 38 | } 39 | 40 | async count() { 41 | const objects = await this.scrollSearch(this._index, this._type); 42 | return objects.reduce((count, obj) => { 43 | if (obj._source && obj._source.visState) { 44 | const visState = JSON.parse(obj._source.visState); 45 | if (this._isUpgradeable(visState)) { 46 | return count + 1; 47 | } 48 | } 49 | return count; 50 | }, 0); 51 | } 52 | 53 | async upgrade() { 54 | const objects = await this.scrollSearch(this._index, this._type); 55 | if (objects.length === 0) { 56 | return 0; 57 | } 58 | let body = ''; 59 | let count = 0; 60 | for (const obj of objects) { 61 | const visState = JSON.parse(obj._source.visState); 62 | if (this._isUpgradeable(visState)) { 63 | const newVisState = cloneDeep(visState); 64 | 65 | for (let i = 0; i < newVisState.params.groups.length; i++) { 66 | const group = newVisState.params.groups[i]; 67 | if (group.indexPatternId !== undefined) { 68 | delete group.indexPatternId; 69 | } 70 | } 71 | 72 | obj._source.visState = JSON.stringify(newVisState); 73 | body += JSON.stringify({ 74 | update: { 75 | _index: obj._index, 76 | _type: obj._type, 77 | _id: obj._id 78 | } 79 | }) + '\n' + JSON.stringify({ doc: obj._source }) + '\n'; 80 | count++; 81 | } 82 | } 83 | 84 | if (count > 0) { 85 | await this._client.bulk({ 86 | refresh: true, 87 | body: body 88 | }); 89 | } 90 | return count; 91 | } 92 | 93 | } 94 | 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kibi_timeline_vis", 3 | "version": "10.2.0-SNAPSHOT", 4 | "kibana": { 5 | "version": "5.6.10" 6 | }, 7 | "authors": [ 8 | "SIREn Solutions " 9 | ], 10 | "description": "Kibi Timeline visualization plugin", 11 | "main": "index.js", 12 | "license": "Apache-2.0", 13 | "homepage": "http://siren.solutions/kibi", 14 | "repository": "https://github.com/sirensolutions/kibi_timeline_vis", 15 | "scripts": { 16 | "lintFix": "gulp lintFix", 17 | "test": "gulp test", 18 | "testdev": "gulp testdev", 19 | "test:coverage": "gulp coverage", 20 | "start": "gulp dev", 21 | "precommit": "gulp lint", 22 | "build": "gulp build", 23 | "package": "gulp package" 24 | }, 25 | "dependencies": { 26 | "grunt": "^1.0.3", 27 | "grunt-cli": "^1.2.0", 28 | "kibi-icons": "sirensolutions/kibi-icons#1.1.0", 29 | "kibiutils": "sirensolutions/kibiutils#1.8.0", 30 | "vis": "4.20.1" 31 | }, 32 | "devDependencies": { 33 | "babel-eslint": "6.1.2", 34 | "bluebird": "3.5.1", 35 | "eslint": "3.11.1", 36 | "gulp": "^3.9.1", 37 | "gulp-eslint": "^1.1.1", 38 | "gulp-util": "^3.0.7", 39 | "gulp-zip": "4.0.0", 40 | "gulp-debug": "4.0.0", 41 | "husky": "0.14.3", 42 | "lodash": "4.17.5", 43 | "minimist": "1.2.0", 44 | "mkdirp": "^0.5.1", 45 | "rimraf": "2.6.2", 46 | "rsync": "0.6.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /public/__tests__/_vis.js: -------------------------------------------------------------------------------- 1 | import { stubbedLogstashIndexPatternService } from 'fixtures/stubbed_logstash_index_pattern'; 2 | import { VisProvider } from 'ui/vis'; 3 | import ngMock from 'ng_mock'; 4 | import expect from 'expect.js'; 5 | import '../kibi_timeline'; 6 | 7 | describe('Kibi Timeline', function () { 8 | describe('Visualization', function () { 9 | 10 | let vis; 11 | 12 | beforeEach(function () { 13 | 14 | ngMock.module('kibana', function ($provide) { 15 | $provide.constant('kbnDefaultAppId', ''); 16 | $provide.constant('kibiDefaultDashboardTitle', ''); 17 | $provide.constant('kibiEnterpriseEnabled', false); 18 | $provide.constant('elasticsearchPlugins', ['siren-join']); 19 | }); 20 | 21 | ngMock.inject(function ($injector, Private) { 22 | const Vis = Private(VisProvider); 23 | const indexPattern = Private(stubbedLogstashIndexPatternService); 24 | vis = new Vis(indexPattern, { 25 | type: 'kibi_timeline' 26 | }); 27 | }); 28 | }); 29 | 30 | it('check vis', function () { 31 | expect(vis.type.name).to.be('kibi_timeline'); 32 | }); 33 | 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /public/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import './kibi_timeline'; 2 | import './_vis'; 3 | import './kibi_select'; 4 | import '../lib/helpers/__tests__/timeline_helper'; 5 | -------------------------------------------------------------------------------- /public/__tests__/kibi_select.js: -------------------------------------------------------------------------------- 1 | import SelectHelperProvider from '../lib/directives/kibi_select_helper'; 2 | import sinon from 'sinon'; 3 | import angular from 'angular'; 4 | import _ from 'lodash'; 5 | import ngMock from 'ng_mock'; 6 | import expect from 'expect.js'; 7 | import '../lib/directives/kibi_select'; 8 | 9 | let $rootScope; 10 | let $elem; 11 | 12 | const init = function (initValue,items, objectType,include) { 13 | // Load the application 14 | ngMock.module('kibana'); 15 | 16 | // Create the scope 17 | ngMock.inject(function (Private, _$rootScope_, $compile, Promise) { 18 | $rootScope = _$rootScope_; 19 | $rootScope.model = initValue; 20 | 21 | const selectHelper = Private(SelectHelperProvider); 22 | $rootScope.action = sinon.stub(selectHelper, 'getObjects').returns(Promise.resolve(items)); 23 | 24 | let select = ''); 32 | 33 | $compile($elem)($rootScope); 34 | $elem.scope().$digest(); 35 | }); 36 | }; 37 | 38 | describe('Kibi Timeline', function () { 39 | describe('Kibi Directives', function () { 40 | describe('kibi-select-port directive', function () { 41 | afterEach(function () { 42 | $elem.remove(); 43 | }); 44 | 45 | function firstElementIsEmpty(options) { 46 | expect(options[0]).to.be.ok(); 47 | expect(options[0].value).to.be('null'); // after porting to 4.4 it changed from '' to 'null' 48 | expect(options[0].text).to.be(''); 49 | } 50 | 51 | it('should sort the items by label', function () { 52 | const items = [ 53 | { value: 1, label: 'bbb' }, 54 | { value: 2, label: 'aaa' } 55 | ]; 56 | 57 | init(null,items, 'search'); 58 | 59 | expect($rootScope.action.called).to.be.ok(); 60 | 61 | const options = $elem.find('option'); 62 | expect(options).to.have.length(3); // the joe element plus the null one 63 | 64 | firstElementIsEmpty(options); 65 | 66 | expect(options[1]).to.be.ok(); 67 | expect(options[1].value).to.be('2'); 68 | expect(options[1].text).to.be('aaa'); 69 | expect(options[2]).to.be.ok(); 70 | expect(options[2].value).to.be('1'); 71 | expect(options[2].text).to.be('bbb'); 72 | }); 73 | 74 | it('should sort the included items too', function () { 75 | const items = [ { value: 2, label: 'aaa' } ]; 76 | const include = [ { value: 1, label: 'bbb' } ]; 77 | 78 | init(null,items, 'search', include); 79 | 80 | expect($rootScope.action.called).to.be.ok(); 81 | const options = $elem.find('option'); 82 | expect(options).to.have.length(3); 83 | 84 | firstElementIsEmpty(options); 85 | 86 | expect(options[1]).to.be.ok(); 87 | expect(options[1].value).to.be('2'); 88 | expect(options[1].text).to.be('aaa'); 89 | 90 | expect(options[2]).to.be.ok(); 91 | expect(options[2].value).to.be('1'); 92 | expect(options[2].text).to.be('bbb'); 93 | }); 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /public/__tests__/kibi_timeline.js: -------------------------------------------------------------------------------- 1 | import { StubbedSearchSourceProvider } from 'fixtures/stubbed_search_source'; 2 | import TimelineHelper from '../lib/helpers/timeline_helper'; 3 | import sinon from 'sinon'; 4 | import angular from 'angular'; 5 | import expect from 'expect.js'; 6 | import _ from 'lodash'; 7 | import ngMock from 'ng_mock'; 8 | import moment from 'moment'; 9 | import 'plugins/kibi_timeline_vis/kibi_timeline_vis_controller'; 10 | 11 | describe('KibiTimeline Directive', function () { 12 | let $elem; 13 | let $rootScope; 14 | let $scope; 15 | let searchSource; 16 | let highlightTags; 17 | let queryFilter; 18 | let indexPatterns; 19 | 20 | let getSortOnFieldObjectSpy; 21 | 22 | const init = function ($elem, props) { 23 | ngMock.inject(function (_$rootScope_, $compile, Private, _indexPatterns_) { 24 | $rootScope = _$rootScope_; 25 | $compile($elem)($rootScope); 26 | $elem.scope().$digest(); 27 | $scope = $elem.isolateScope(); 28 | _.assign($scope, props); 29 | $scope.$digest(); 30 | queryFilter = Private(require('ui/filter_bar/query_filter')); 31 | indexPatterns = _indexPatterns_; 32 | }); 33 | }; 34 | 35 | const destroy = function () { 36 | $scope.$destroy(); 37 | $rootScope.$destroy(); 38 | $elem.remove(); 39 | }; 40 | 41 | function initTimeline({ invertFirstLabelInstance = false, useHighlight = false, withFieldSequence, endField, startField, labelField }) { 42 | ngMock.module('kibana', $provide => { 43 | $provide.constant('kbnDefaultAppId', ''); 44 | $provide.constant('kibiDefaultDashboardTitle', ''); 45 | $provide.constant('elasticsearchPlugins', ['siren-join']); 46 | }); 47 | const directive = ` 50 | `; 51 | $elem = angular.element(directive); 52 | // this is so the elements gets correctly properties like offsetWidth 53 | // plus the timeline is actually visible when tests are run in dev mode 54 | $elem.appendTo('body'); 55 | 56 | ngMock.inject(function (_highlightTags_, Private) { 57 | const TimelineHelper = Private(require('../lib/helpers/timeline_helper')); 58 | getSortOnFieldObjectSpy = sinon.spy(TimelineHelper, 'getSortOnFieldObject'); 59 | 60 | highlightTags = _highlightTags_; 61 | 62 | searchSource = Private(StubbedSearchSourceProvider); 63 | searchSource.highlight = sinon.stub(); 64 | searchSource.sort = sinon.stub(); 65 | }); 66 | 67 | const params = { invertFirstLabelInstance, useHighlight, endField, labelField, startField }; 68 | if (withFieldSequence) { 69 | params.startFieldSequence = startField.split('.'); 70 | params.endFieldSequence = endField.split('.'); 71 | params.labelFieldSequence = labelField.split('.'); 72 | } 73 | 74 | init($elem, { 75 | visOptions: { 76 | groups: [ 77 | { 78 | id: 1, 79 | color: '#ff0000', 80 | label: 'logs', 81 | params, 82 | searchSource 83 | } 84 | ], 85 | groupsOnSeparateLevels: false, 86 | selectValue: 'id', 87 | notifyDataErrors: false 88 | } 89 | }); 90 | $scope.$digest(); 91 | } 92 | 93 | afterEach(function () { 94 | destroy(); 95 | }); 96 | 97 | it('should compile', function () { 98 | initTimeline({}); 99 | expect($elem.text()).to.not.be.empty(); 100 | }); 101 | 102 | it('should correctly return a timeline', function () { 103 | initTimeline({ 104 | startField: '@timestamp', 105 | endField: '', 106 | labelField: 'machine.os' 107 | }); 108 | 109 | const date = '25-01-1995'; 110 | const dateObj = moment(date, 'DD-MM-YYYY'); 111 | const results = { 112 | took: 73, 113 | timed_out: false, 114 | _shards: { 115 | total: 144, 116 | successful: 144, 117 | failed: 0 118 | }, 119 | hits: { 120 | total : 49487, 121 | max_score : 1.0, 122 | hits: [ 123 | { 124 | _index: 'logstash-2014.09.09', 125 | _type: 'apache', 126 | _id: '61', 127 | _score: 1, 128 | _source: { 129 | '@timestamp': date, 130 | machine: { 131 | os: 'linux' 132 | } 133 | }, 134 | fields: { 135 | '@timestamp': [ dateObj ] 136 | } 137 | } 138 | ] 139 | } 140 | }; 141 | searchSource.crankResults(results); 142 | $scope.$digest(); 143 | expect($scope.timeline.itemsData.length).to.be(1); 144 | $scope.timeline.itemsData.forEach(data => { 145 | sinon.assert.notCalled(searchSource.highlight); 146 | expect(data.value).to.be('linux'); 147 | expect(data.start.valueOf()).to.be(dateObj.valueOf()); 148 | }); 149 | }); 150 | 151 | it('should return an event with the all the labels joined', function () { 152 | initTimeline({ 153 | startField: '@timestamp', 154 | endField: '', 155 | labelField: 'machine.os' 156 | }); 157 | 158 | const date = '25-01-2015'; 159 | const dateObj = moment(date, 'DD-MM-YYYY'); 160 | const results = { 161 | took: 73, 162 | timed_out: false, 163 | _shards: { 164 | total: 144, 165 | successful: 144, 166 | failed: 0 167 | }, 168 | hits: { 169 | total : 49487, 170 | max_score : 1.0, 171 | hits: [ 172 | { 173 | _index: 'logstash-2014.09.09', 174 | _type: 'apache', 175 | _id: '61', 176 | _score: 1, 177 | _source: { 178 | '@timestamp': date, 179 | machine: { 180 | os: [ 'linux', 'mac' ] 181 | } 182 | }, 183 | fields: { 184 | '@timestamp': [ dateObj ], 185 | } 186 | } 187 | ] 188 | } 189 | }; 190 | searchSource.crankResults(results); 191 | $scope.$digest(); 192 | expect($scope.timeline.itemsData.length).to.be(1); 193 | $scope.timeline.itemsData.forEach(data => { 194 | sinon.assert.notCalled(searchSource.highlight); 195 | expect(data.value).to.be('linux, mac'); 196 | expect(data.start.valueOf()).to.be(dateObj.valueOf()); 197 | }); 198 | }); 199 | 200 | it('should return an event for each start/end pairs', function () { 201 | initTimeline({ 202 | startField: '@timestamp', 203 | endField: '', 204 | labelField: 'machine.os' 205 | }); 206 | 207 | const dates = [ '25-01-2015', '16-12-2016' ]; 208 | const dateObjs = _.map(dates, date => moment(date, 'DD-MM-YYYY')); 209 | const results = { 210 | took: 73, 211 | timed_out: false, 212 | _shards: { 213 | total: 144, 214 | successful: 144, 215 | failed: 0 216 | }, 217 | hits: { 218 | total : 49487, 219 | max_score : 1.0, 220 | hits: [ 221 | { 222 | _index: 'logstash-2014.09.09', 223 | _type: 'apache', 224 | _id: '61', 225 | _score: 1, 226 | _source: { 227 | '@timestamp': dates, 228 | machine: { 229 | os: 'linux' 230 | } 231 | }, 232 | fields: { 233 | '@timestamp': dateObjs, 234 | } 235 | } 236 | ] 237 | } 238 | }; 239 | searchSource.crankResults(results); 240 | $scope.$digest(); 241 | expect($scope.timeline.itemsData.length).to.be(2); 242 | let i = 0; 243 | $scope.timeline.itemsData.forEach(data => { 244 | sinon.assert.notCalled(searchSource.highlight); 245 | expect(data.value).to.be('linux'); 246 | expect(data.start.valueOf()).to.be(dateObjs[i].valueOf()); 247 | i++; 248 | }); 249 | }); 250 | 251 | it('should get the highlighted terms of events if useHighlight is true', function () { 252 | initTimeline({ 253 | useHighlight: true, 254 | startField: '@timestamp', 255 | endField: '', 256 | labelField: 'machine.os' 257 | }); 258 | 259 | const date = '25-01-1995'; 260 | const dateObj = moment(date, 'DD-MM-YYYY'); 261 | const results = { 262 | took: 73, 263 | timed_out: false, 264 | _shards: { 265 | total: 144, 266 | successful: 144, 267 | failed: 0 268 | }, 269 | hits: { 270 | total : 49487, 271 | max_score : 1.0, 272 | hits: [ 273 | { 274 | _index: 'logstash-2014.09.09', 275 | _type: 'apache', 276 | _id: '61', 277 | _score: 1, 278 | _source: { 279 | '@timestamp': date, 280 | machine: { 281 | os: 'linux' 282 | } 283 | }, 284 | fields: { 285 | '@timestamp': [ dateObj ] 286 | }, 287 | highlight: { 288 | 'machine.os': [ 289 | `${highlightTags.pre}BEST BEST${highlightTags.post}` 290 | ] 291 | } 292 | } 293 | ] 294 | } 295 | }; 296 | searchSource.crankResults(results); 297 | $scope.$digest(); 298 | expect($scope.timeline.itemsData.length).to.be(1); 299 | sinon.assert.called(searchSource.highlight); 300 | $scope.timeline.itemsData.forEach(data => { 301 | expect(data.content).to.match(/best best/); 302 | expect(data.value).to.be('linux'); 303 | expect(data.start.valueOf()).to.be(dateObj.valueOf()); 304 | }); 305 | }); 306 | 307 | it('should sort on the start field if invertFirstLabelInstance', function () { 308 | initTimeline({ 309 | invertFirstLabelInstance: true, 310 | startField: '@timestamp', 311 | endField: '', 312 | labelField: 'machine.os' 313 | }); 314 | 315 | const date1 = '25-01-1995'; 316 | const date1Obj = moment(date1, 'DD-MM-YYYY'); 317 | const date2 = '26-01-1995'; 318 | const date2Obj = moment(date2, 'DD-MM-YYYY'); 319 | const date3 = '27-01-1995'; 320 | const date3Obj = moment(date3, 'DD-MM-YYYY'); 321 | 322 | const results = { 323 | took: 73, 324 | timed_out: false, 325 | _shards: { 326 | total: 144, 327 | successful: 144, 328 | failed: 0 329 | }, 330 | hits: { 331 | total : 49487, 332 | max_score : 1.0, 333 | hits: [ 334 | { 335 | _index: 'logstash-2014.09.09', 336 | _type: 'apache', 337 | _id: '61', 338 | _score: 1, 339 | _source: { 340 | '@timestamp': date1, 341 | machine: { 342 | os: 'linux' 343 | } 344 | }, 345 | fields: { 346 | '@timestamp': [ date1Obj ] 347 | } 348 | }, 349 | { 350 | _index: 'logstash-2014.09.09', 351 | _type: 'apache', 352 | _id: '62', 353 | _score: 1, 354 | _source: { 355 | '@timestamp': date2, 356 | machine: { 357 | os: 'mac' 358 | } 359 | }, 360 | fields: { 361 | '@timestamp': [ date2Obj ] 362 | } 363 | }, 364 | { 365 | _index: 'logstash-2014.09.09', 366 | _type: 'apache', 367 | _id: '63', 368 | _score: 1, 369 | _source: { 370 | '@timestamp': date3, 371 | machine: { 372 | os: 'linux' 373 | } 374 | }, 375 | fields: { 376 | '@timestamp': [ date3Obj ] 377 | } 378 | } 379 | ] 380 | } 381 | }; 382 | searchSource.crankResults(results); 383 | $scope.$digest(); 384 | expect($scope.timeline.itemsData.length).to.be(3); 385 | sinon.assert.called(getSortOnFieldObjectSpy); 386 | sinon.assert.called(searchSource.sort); 387 | 388 | let itemIndex = 0; 389 | $scope.timeline.itemsData.forEach(data => { 390 | switch (itemIndex) { 391 | case 0: 392 | expect(data.value).to.be('linux'); 393 | expect(data.start.valueOf()).to.be(date1Obj.valueOf()); 394 | // emphasized, border style is solid 395 | expect(data.style).to.match(/color: #ff0000/); 396 | expect(data.style).to.match(/border-style: solid/); 397 | break; 398 | case 1: 399 | expect(data.value).to.be('mac'); 400 | expect(data.start.valueOf()).to.be(date2Obj.valueOf()); 401 | // emphasized, border style is solid 402 | expect(data.style).to.match(/color: #ff0000/); 403 | expect(data.style).to.match(/border-style: solid/); 404 | break; 405 | case 2: 406 | expect(data.value).to.be('linux'); 407 | expect(data.start.valueOf()).to.be(date3Obj.valueOf()); 408 | // border style is none 409 | expect(data.style).to.match(/color: #ff0000/); 410 | expect(data.style).to.match(/border-style: none/); 411 | break; 412 | default: 413 | expect().fail(`Should not have the case itemIndex=${itemIndex}`); 414 | } 415 | itemIndex++; 416 | }); 417 | }); 418 | 419 | describe('Missing data', function () { 420 | 421 | it('should support documents with missing label', function () { 422 | initTimeline({ 423 | startField: '@timestamp', 424 | endField: '', 425 | labelField: 'machine.os' 426 | }); 427 | 428 | const date = '25-01-1995'; 429 | const dateObj = moment(date, 'DD-MM-YYYY'); 430 | const results = { 431 | took: 73, 432 | timed_out: false, 433 | _shards: { 434 | total: 144, 435 | successful: 144, 436 | failed: 0 437 | }, 438 | hits: { 439 | total : 49487, 440 | max_score : 1.0, 441 | hits: [ 442 | { 443 | _index: 'logstash-2014.09.09', 444 | _type: 'apache', 445 | _id: '61', 446 | _score: 1, 447 | _source: { 448 | '@timestamp': date 449 | }, 450 | fields: { 451 | '@timestamp': [ dateObj ] 452 | } 453 | } 454 | ] 455 | } 456 | }; 457 | searchSource.crankResults(results); 458 | $scope.$digest(); 459 | expect($scope.timeline.itemsData.length).to.be(1); 460 | $scope.timeline.itemsData.forEach(data => { 461 | sinon.assert.notCalled(searchSource.highlight); 462 | expect(data.value).to.be('N/A'); 463 | expect(data.start.valueOf()).to.be(dateObj.valueOf()); 464 | }); 465 | }); 466 | 467 | [ 468 | { 469 | withFieldSequence: true 470 | }, 471 | { 472 | withFieldSequence: false 473 | } 474 | ].forEach(({ withFieldSequence }) => { 475 | it(`should support documents with missing start date with ${withFieldSequence ? 'kibi' : 'kibana'}`, function () { 476 | initTimeline({ 477 | withFieldSequence, 478 | startField: '@timestamp', 479 | endField: '', 480 | labelField: 'machine.os' 481 | }); 482 | 483 | const results = { 484 | took: 73, 485 | timed_out: false, 486 | _shards: { 487 | total: 144, 488 | successful: 144, 489 | failed: 0 490 | }, 491 | hits: { 492 | total : 49487, 493 | max_score : 1.0, 494 | hits: [ 495 | { 496 | _index: 'logstash-2014.09.09', 497 | _type: 'apache', 498 | _id: '61', 499 | _score: 1, 500 | _source: { 501 | '@timestamp': null, 502 | machine: { 503 | os: 'linux' 504 | } 505 | }, 506 | fields: {} 507 | } 508 | ] 509 | } 510 | }; 511 | searchSource.crankResults(results); 512 | $scope.$digest(); 513 | expect($scope.timeline.itemsData.length).to.be(0); 514 | }); 515 | }); 516 | }); 517 | 518 | describe('Filter creation', function () { 519 | 520 | const simulateClickOnItem = function ($el) { 521 | const $panel = $el.find('.vis-panel.vis-center'); 522 | const $item = $el.find('.vis-content .vis-itemset .vis-foreground .vis-group .vis-item .vis-item-content .kibi-tl-label-item'); 523 | const event = new MouseEvent('click', { 524 | target: { 525 | // cheat that we clicked on the item 526 | 'timeline-item': {} 527 | } 528 | }); 529 | const eventData = { 530 | target: $item[0], 531 | srcEvent: event 532 | }; 533 | // hammer comes from vis.js timeline library 534 | // cheat again and trigger the tap with fake eventData 535 | $panel[0].hammer[0].emit('tap', eventData); 536 | }; 537 | const dateStart = '25-01-1995'; 538 | const dateStartObj = moment(dateStart, 'DD-MM-YYYY'); 539 | const dateEnd = '27-01-1995'; 540 | const dateEndObj = moment(dateEnd, 'DD-MM-YYYY'); 541 | const results = { 542 | took: 73, 543 | timed_out: false, 544 | _shards: { 545 | total: 144, 546 | successful: 144, 547 | failed: 0 548 | }, 549 | hits: { 550 | total: 49487, 551 | max_score: 1.0, 552 | hits: [ 553 | { 554 | _index: 'logstash-2014.09.09', 555 | _type: 'apache', 556 | _id: '61', 557 | _score: 1, 558 | _source: { 559 | '@timestamp': dateStart, 560 | 'endDate': dateEnd, 561 | machine: { 562 | os: 'linux' 563 | } 564 | }, 565 | fields: { 566 | '@timestamp': [dateStartObj], 567 | 'endDate': [dateEndObj], 568 | } 569 | } 570 | ] 571 | } 572 | }; 573 | 574 | it('correct filter should be created - (default) ID', function (done) { 575 | initTimeline({ 576 | startField: '@timestamp', 577 | endField: '', 578 | labelField: 'machine.os' 579 | }); 580 | const addFilterSpy = sinon.spy(queryFilter, 'addFilters'); 581 | searchSource.crankResults(results); 582 | $scope.$digest(); 583 | 584 | const expectedFilters = [{ 585 | query: { 586 | ids: { 587 | type: 'apache', 588 | values: ['61'] 589 | } 590 | }, 591 | meta: { 592 | index: 'logstash-*' 593 | } 594 | }]; 595 | 596 | // check on next tick 597 | setTimeout(function () { 598 | expect($scope.timeline.itemsData.length).to.be(1); 599 | simulateClickOnItem($elem); 600 | sinon.assert.calledOnce(addFilterSpy); 601 | sinon.assert.calledWith(addFilterSpy, expectedFilters); 602 | done(); 603 | }, 0); 604 | }); 605 | 606 | it('correct filter should be created - LABEL', function (done) { 607 | initTimeline({ 608 | startField: '@timestamp', 609 | endField: '', 610 | labelField: 'machine.os' 611 | }); 612 | $scope.visOptions.selectValue = 'label'; 613 | const addFilterSpy = sinon.spy(queryFilter, 'addFilters'); 614 | searchSource.crankResults(results); 615 | $scope.$digest(); 616 | 617 | const expectedFilters = [{ 618 | meta: { 619 | index: 'logstash-*' 620 | }, 621 | query: { 622 | match: { 623 | 'machine.os': { 624 | query: 'linux', 625 | type: 'phrase' 626 | } 627 | } 628 | } 629 | }]; 630 | 631 | // check on next tick 632 | setTimeout(function () { 633 | expect($scope.timeline.itemsData.length).to.be(1); 634 | simulateClickOnItem($elem); 635 | sinon.assert.calledOnce(addFilterSpy); 636 | sinon.assert.calledWith(addFilterSpy, expectedFilters); 637 | done(); 638 | }, 0); 639 | }); 640 | 641 | it('correct filter should be created - DATE', function (done) { 642 | initTimeline({ 643 | startField: '@timestamp', 644 | endField: '', 645 | labelField: 'machine.os' 646 | }); 647 | $scope.visOptions.selectValue = 'date'; 648 | const addFilterSpy = sinon.spy(queryFilter, 'addFilters'); 649 | searchSource.crankResults(results); 650 | $scope.$digest(); 651 | 652 | const expectedFilters = [{ 653 | meta: { 654 | index: 'logstash-*' 655 | }, 656 | query: { 657 | match: { 658 | '@timestamp': { 659 | query: Date.parse(dateStartObj), 660 | type: 'phrase' 661 | } 662 | } 663 | } 664 | }]; 665 | 666 | // check on next tick 667 | setTimeout(function () { 668 | expect($scope.timeline.itemsData.length).to.be(1); 669 | simulateClickOnItem($elem); 670 | sinon.assert.calledOnce(addFilterSpy); 671 | sinon.assert.calledWith(addFilterSpy, expectedFilters); 672 | done(); 673 | }, 0); 674 | }); 675 | 676 | it('correct filter should be created - DATE RANGE', function (done) { 677 | initTimeline({ 678 | startField: '@timestamp', 679 | endField: 'endDate', 680 | labelField: 'machine.os' 681 | }); 682 | 683 | let filters; 684 | const addFilterSpy = sinon.stub(queryFilter, 'addFilters', function (f) { 685 | filters = f; 686 | }); 687 | 688 | $scope.visOptions.selectValue = 'date'; 689 | 690 | sinon.stub(indexPatterns, 'get').returns(Promise.resolve( 691 | { 692 | id: 'logstash-*', 693 | fields: [ 694 | {name: '@timestamp'}, 695 | {name: 'endDate'} 696 | ] 697 | } 698 | )); 699 | searchSource.crankResults(results); 700 | $scope.$digest(); 701 | 702 | // check on next tick 703 | setTimeout(function () { 704 | expect($scope.timeline.itemsData.length).to.be(1); 705 | simulateClickOnItem($elem); 706 | // check on next tick 707 | setTimeout(function () { 708 | sinon.assert.calledOnce(addFilterSpy); 709 | 710 | // not using calledWith 711 | // as dates depends on browser timezones it is safer to check 712 | // individual properties 713 | const lowerBound = '' + filters[0].range['@timestamp'].gte; 714 | const lowerMeta = filters[0].meta; 715 | const higherBound = '' + filters[1].range.endDate.lte; 716 | const higherMeta = filters[1].meta; 717 | expect(lowerBound.indexOf('Wed Jan 25 1995')).to.equal(0); 718 | expect(lowerMeta.alias.indexOf('@timestamp >= Wed Jan 25 1995')).to.equal(0); 719 | expect(lowerMeta.index).to.equal('logstash-*'); 720 | expect(higherBound.indexOf('Fri Jan 27 1995')).to.equal(0); 721 | expect(higherMeta.alias.indexOf('endDate <= Fri Jan 27 1995')).to.equal(0); 722 | expect(higherMeta.index).to.equal('logstash-*'); 723 | done(); 724 | }, 0); 725 | }, 0); 726 | }); 727 | 728 | }); 729 | 730 | }); 731 | -------------------------------------------------------------------------------- /public/__tests__/kibi_timeline_vis_controller.js: -------------------------------------------------------------------------------- 1 | import ngMock from 'ng_mock'; 2 | import _ from 'lodash'; 3 | import expect from 'expect.js'; 4 | import $ from 'jquery'; 5 | import sinon from 'sinon'; 6 | import { mockSavedObjects } from 'fixtures/kibi/mock_saved_objects'; 7 | import '../kibi_timeline_vis_controller'; 8 | import noDigestPromises from 'test_utils/no_digest_promises'; 9 | 10 | describe('Kibi Timeline', function () { 11 | 12 | let $scope; 13 | let $element; 14 | 15 | function init({ savedSearches = [], indexPatterns = [], stubs = {} }) { 16 | ngMock.module('kibana', function ($provide) { 17 | $provide.constant('kbnDefaultAppId', ''); 18 | $provide.constant('kibiDefaultDashboardTitle', ''); 19 | $provide.constant('kibiEnterpriseEnabled', false); 20 | $provide.constant('elasticsearchPlugins', ['siren-join']); 21 | }); 22 | 23 | ngMock.module('discover/saved_searches', function ($provide) { 24 | $provide.service('savedSearches', (Private, Promise) => mockSavedObjects(Promise, Private)('savedSearches', savedSearches)); 25 | }); 26 | 27 | ngMock.module('kibana/index_patterns', function ($provide) { 28 | $provide.service('indexPatterns', (Promise, Private) => mockSavedObjects(Promise, Private)('indexPatterns', indexPatterns)); 29 | }); 30 | 31 | ngMock.inject(function ($rootScope, $controller) { 32 | $scope = $rootScope; 33 | $element = $('
'); 34 | $controller('KbnTimelineVisController', { 35 | $scope: $scope, 36 | $route: { 37 | current: { 38 | locals: { 39 | savedVis: {} 40 | } 41 | } 42 | }, 43 | $element: $element 44 | }); 45 | 46 | if (stubs.initOptions) { 47 | sinon.stub($scope, 'initOptions'); 48 | } 49 | if (stubs.initSearchSources) { 50 | sinon.stub($scope, 'initSearchSources'); 51 | } 52 | }); 53 | } 54 | 55 | const assertGroup = function (actual, expected) { 56 | expect(actual.id).to.equal(expected.id); 57 | expect(actual.color).to.equal(expected.color); 58 | expect(actual.label).to.equal(expected.groupLabel); 59 | expect(actual.searchSource._id).to.equal(expected.searchSourceId); 60 | expect(actual.params.labelField).to.equal(expected.labelField); 61 | expect(actual.params.startField).to.equal(expected.startField); 62 | expect(actual.params.endField).to.equal(expected.endField); 63 | expect(actual.params.labelFieldSequence).to.eql(expected.labelFieldSequence); 64 | expect(actual.params.startFieldSequence).to.eql(expected.startFieldSequence); 65 | expect(actual.params.endFieldSequence).to.eql(expected.endFieldSequence); 66 | }; 67 | 68 | describe('Controller', function () { 69 | noDigestPromises.activateForSuite(); 70 | 71 | it('should set the vis options', function () { 72 | init({}); 73 | 74 | const expectedOptions = { 75 | width: '100%', 76 | height: '100%', 77 | selectable: true, 78 | autoResize: true 79 | }; 80 | 81 | $scope.initOptions(); 82 | expect($scope.options).to.eql(expectedOptions); 83 | }); 84 | 85 | it('should set the options when change:vis event is triggered', function () { 86 | init({ 87 | stubs: { 88 | initSearchSources: true, 89 | initOptions: true 90 | } 91 | }); 92 | 93 | $scope.$digest(); 94 | $scope.$emit('change:vis'); 95 | $scope.$digest(); 96 | 97 | expect($scope.initOptions.calledTwice).to.be(true); 98 | }); 99 | 100 | it('should init groups', function (done) { 101 | const groups = [ 102 | { 103 | id: 'idA', 104 | color: 'colorA', 105 | endField: 'endFieldA', 106 | groupLabel: 'groupLabelA', 107 | labelField: 'labelFieldA', 108 | savedSearchId: 'savedSearchIdA', 109 | startField: 'startFieldA' 110 | }, 111 | { 112 | id: 'idB', 113 | color: 'colorB', 114 | endField: 'endFieldB', 115 | groupLabel: 'groupLabelB', 116 | labelField: 'labelFieldB', 117 | savedSearchId: 'savedSearchIdB', 118 | startField: 'startFieldB' 119 | } 120 | ]; 121 | const savedVis = { 122 | vis: { 123 | params: { 124 | groups 125 | } 126 | } 127 | }; 128 | 129 | init({ 130 | indexPatterns: [ 131 | { 132 | id: 'indexA', 133 | timeField: '', 134 | fields: [ 135 | { 136 | name: 'labelFieldA', 137 | type: 'string', 138 | path: [ 'labelFieldA' ] 139 | }, 140 | { 141 | name: 'startFieldA', 142 | type: 'string', 143 | path: [ 'startFieldA' ] 144 | }, 145 | { 146 | name: 'endFieldA', 147 | type: 'string', 148 | path: [ 'endFieldA' ] 149 | } 150 | ] 151 | }, 152 | { 153 | id: 'indexB', 154 | timeField: '', 155 | fields: [ 156 | { 157 | name: 'labelFieldB', 158 | type: 'string', 159 | path: [ 'labelFieldB' ] 160 | }, 161 | { 162 | name: 'startFieldB', 163 | type: 'string', 164 | path: [ 'startFieldB' ] 165 | }, 166 | { 167 | name: 'endFieldB', 168 | type: 'string', 169 | path: [ 'endFieldB' ] 170 | } 171 | ] 172 | } 173 | ], 174 | savedSearches: [ 175 | { 176 | id: 'savedSearchIdA', 177 | kibanaSavedObjectMeta: { 178 | searchSourceJSON: JSON.stringify( 179 | { 180 | index: 'indexA', 181 | filter: [], 182 | query: {} 183 | } 184 | ) 185 | } 186 | }, 187 | { 188 | id: 'savedSearchIdB', 189 | kibanaSavedObjectMeta: { 190 | searchSourceJSON: JSON.stringify( 191 | { 192 | index: 'indexB', 193 | filter: [], 194 | query: {} 195 | } 196 | ) 197 | } 198 | } 199 | ] 200 | }); 201 | 202 | $scope.initSearchSources(savedVis) 203 | .then(() => { 204 | expect($scope.visOptions.groups).to.have.length(2); 205 | 206 | const expectedA = { 207 | labelFieldSequence: [ 'labelFieldA' ], 208 | startFieldSequence: [ 'startFieldA' ], 209 | endFieldSequence: [ 'endFieldA' ], 210 | searchSourceId: '_kibi_timetable_ids_source_flagidAsavedSearchIdA' 211 | }; 212 | _.assign(expectedA, groups[0]); 213 | assertGroup($scope.visOptions.groups[0], expectedA); 214 | 215 | const expectedB = { 216 | labelFieldSequence: [ 'labelFieldB' ], 217 | startFieldSequence: [ 'startFieldB' ], 218 | endFieldSequence: [ 'endFieldB' ], 219 | searchSourceId: '_kibi_timetable_ids_source_flagidBsavedSearchIdB' 220 | }; 221 | _.assign(expectedB, groups[1]); 222 | assertGroup($scope.visOptions.groups[1], expectedB); 223 | 224 | done(); 225 | }) 226 | .catch(done); 227 | }); 228 | 229 | it('should init group with dotted field names', function (done) { 230 | const groups = [ 231 | { 232 | id: 'idA', 233 | color: 'colorA', 234 | endField: 'd.o.t.endFieldA', 235 | groupLabel: 'groupLabelA', 236 | labelField: 'd.o.t.labelFieldA', 237 | savedSearchId: 'savedSearchIdA', 238 | startField: 'd.o.t.startFieldA' 239 | }, 240 | { 241 | id: 'idB', 242 | color: 'colorB', 243 | endField: 'd.o.t.endFieldB', 244 | groupLabel: 'groupLabelB', 245 | labelField: 'd.o.t.labelFieldB', 246 | savedSearchId: 'savedSearchIdB', 247 | startField: 'd.o.t.startFieldB' 248 | } 249 | ]; 250 | const savedVis = { 251 | vis: { 252 | params: { 253 | groups 254 | } 255 | } 256 | }; 257 | 258 | init({ 259 | indexPatterns: [ 260 | { 261 | id: 'indexA', 262 | timeField: '', 263 | fields: [ 264 | { 265 | name: 'd.o.t.labelFieldA', 266 | type: 'string', 267 | path: [ 'd.o', 't.labelFieldA' ] 268 | }, 269 | { 270 | name: 'd.o.t.startFieldA', 271 | type: 'string', 272 | path: [ 'd.o', 't.startFieldA' ] 273 | }, 274 | { 275 | name: 'd.o.t.endFieldA', 276 | type: 'string', 277 | path: [ 'd.o', 't.endFieldA' ] 278 | } 279 | ] 280 | }, 281 | { 282 | id: 'indexB', 283 | timeField: '', 284 | fields: [ 285 | { 286 | name: 'd.o.t.labelFieldB', 287 | type: 'string', 288 | path: [ 'd.o', 't.labelFieldB' ] 289 | }, 290 | { 291 | name: 'd.o.t.startFieldB', 292 | type: 'string', 293 | path: [ 'd.o', 't.startFieldB' ] 294 | }, 295 | { 296 | name: 'd.o.t.endFieldB', 297 | type: 'string', 298 | path: [ 'd.o', 't.endFieldB' ] 299 | } 300 | ] 301 | } 302 | ], 303 | savedSearches: [ 304 | { 305 | id: 'savedSearchIdA', 306 | kibanaSavedObjectMeta: { 307 | searchSourceJSON: JSON.stringify( 308 | { 309 | index: 'indexA', 310 | filter: [], 311 | query: {} 312 | } 313 | ) 314 | } 315 | }, 316 | { 317 | id: 'savedSearchIdB', 318 | kibanaSavedObjectMeta: { 319 | searchSourceJSON: JSON.stringify( 320 | { 321 | index: 'indexB', 322 | filter: [], 323 | query: {} 324 | } 325 | ) 326 | } 327 | } 328 | ] 329 | }); 330 | 331 | $scope.initSearchSources(savedVis) 332 | .then(() => { 333 | expect($scope.visOptions.groups).to.have.length(2); 334 | 335 | const expectedA = { 336 | labelFieldSequence: [ 'd.o', 't.labelFieldA' ], 337 | startFieldSequence: [ 'd.o', 't.startFieldA' ], 338 | endFieldSequence: [ 'd.o', 't.endFieldA' ], 339 | searchSourceId: '_kibi_timetable_ids_source_flagidAsavedSearchIdA' 340 | }; 341 | _.assign(expectedA, groups[0]); 342 | assertGroup($scope.visOptions.groups[0], expectedA); 343 | 344 | const expectedB = { 345 | labelFieldSequence: [ 'd.o', 't.labelFieldB' ], 346 | startFieldSequence: [ 'd.o', 't.startFieldB' ], 347 | endFieldSequence: [ 'd.o', 't.endFieldB' ], 348 | searchSourceId: '_kibi_timetable_ids_source_flagidBsavedSearchIdB' 349 | }; 350 | _.assign(expectedB, groups[1]); 351 | assertGroup($scope.visOptions.groups[1], expectedB); 352 | 353 | done(); 354 | }) 355 | .catch(done); 356 | }); 357 | 358 | it('should pass along the parameters of the visualization', function (done) { 359 | const savedVis = { 360 | vis: { 361 | params: { 362 | syncTime: 'sync', 363 | groupsOnSeparateLevels: 'sep', 364 | notifyDataErrors: 'notify', 365 | selectValue: 'date', 366 | groups: [] 367 | } 368 | } 369 | }; 370 | 371 | init({ 372 | stubs: { 373 | initSearchSources: false, 374 | initOptions: true 375 | } 376 | }); 377 | 378 | $scope.initSearchSources(savedVis) 379 | .then(() => { 380 | expect($scope.visOptions.syncTime).to.be('sync'); 381 | expect($scope.visOptions.groupsOnSeparateLevels).to.be('sep'); 382 | expect($scope.visOptions.notifyDataErrors).to.be('notify'); 383 | expect($scope.visOptions.selectValue).to.be('date'); 384 | done(); 385 | }) 386 | .catch(done); 387 | }); 388 | }); 389 | 390 | }); 391 | -------------------------------------------------------------------------------- /public/kibi_timeline.js: -------------------------------------------------------------------------------- 1 | import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter'; 2 | import TimelineHelper from './lib/helpers/timeline_helper'; 3 | import RequestQueueProvider from './lib/courier/_request_queue_wrapped'; 4 | import 'ui/highlight/highlight_tags'; 5 | import _ from 'lodash'; 6 | import { Timeline, DataSet } from 'vis-timeline'; 7 | import moment from 'moment'; 8 | import { buildRangeFilter } from 'ui/filter_manager/lib/range'; 9 | import { uiModules } from 'ui/modules'; 10 | import { highlightTags } from 'ui/highlight/highlight_tags'; 11 | 12 | uiModules 13 | .get('kibana') 14 | .directive('kibiTimeline', function (Private, createNotifier, courier, indexPatterns, config, timefilter) { 15 | const NUM_FRAGS_CONFIG = 'kibi:timeline:highlight:number_of_fragments'; 16 | const DEFAULT_NUM_FRAGS = 25; 17 | const requestQueue = Private(RequestQueueProvider); 18 | const queryFilter = Private(FilterBarQueryFilterProvider); 19 | const notify = createNotifier({ 20 | location: 'Kibi Timeline' 21 | }); 22 | 23 | return { 24 | scope: { 25 | timelineOptions: '=', 26 | visOptions: '=' 27 | }, 28 | restrict: 'E', 29 | replace: true, 30 | link: _link 31 | }; 32 | 33 | function _link($scope, $element) { 34 | let timeline; 35 | let data; 36 | 37 | const onSelect = function (properties) { 38 | // pass this to a scope variable 39 | const selected = data._data[properties.items]; 40 | if (selected) { 41 | if ($scope.visOptions.selectValue === 'date') { 42 | if (selected.start && !selected.end) { 43 | // single point - do query match query filter 44 | const q1 = { 45 | query: { 46 | match: {} 47 | }, 48 | meta: { 49 | index: selected.index 50 | } 51 | }; 52 | 53 | q1.query.match[selected.startField.name] = { 54 | query: selected.start.getTime(), 55 | type: 'phrase' 56 | }; 57 | queryFilter.addFilters([q1]); 58 | } else if (selected.start && selected.end) { 59 | // range - do 2 range filters 60 | indexPatterns.get(selected.index).then(function (i) { 61 | const startF = _.find(i.fields, function (f) { 62 | return f.name === selected.startField.name; 63 | }); 64 | const endF = _.find(i.fields, function (f) { 65 | return f.name === selected.endField.name; 66 | }); 67 | 68 | const rangeFilter1 = buildRangeFilter(startF, { 69 | gte: selected.startField.value 70 | }, i); 71 | rangeFilter1.meta.alias = selected.startField.name + ' >= ' + selected.start; 72 | 73 | const rangeFilter2 = buildRangeFilter(endF, { 74 | lte: selected.endField.value 75 | }, i); 76 | rangeFilter2.meta.alias = selected.endField.name + ' <= ' + selected.end; 77 | 78 | queryFilter.addFilters([rangeFilter1, rangeFilter2]); 79 | }); 80 | } 81 | } else if ($scope.visOptions.selectValue === 'label') { 82 | let searchField = undefined; 83 | for (let i = 0; i < $scope.visOptions.groups.length; i++) { 84 | if (selected.groupId === $scope.visOptions.groups[i].id) { 85 | searchField = $scope.visOptions.groups[i].params.labelField; 86 | } 87 | } 88 | const q2 = { 89 | query: { 90 | match: {} 91 | }, 92 | meta: { 93 | index: selected.index 94 | } 95 | }; 96 | q2.query.match[searchField] = { 97 | query: selected.value, 98 | type: 'phrase' 99 | }; 100 | queryFilter.addFilters([q2]); 101 | } else if ($scope.visOptions.selectValue === 'id') { 102 | const q2 = { 103 | query: { 104 | ids: { 105 | type: selected._type, 106 | values: [ selected._id ] 107 | } 108 | }, 109 | meta: { 110 | index: selected.index 111 | } 112 | }; 113 | 114 | queryFilter.addFilters([q2]); 115 | } 116 | } 117 | }; 118 | 119 | const initTimeline = function () { 120 | if (!timeline) { 121 | // create a new one 122 | $scope.timeline = timeline = new Timeline($element[0]); 123 | if ($scope.timelineOptions) { 124 | const utcOffset = TimelineHelper.changeTimezone(config.get('dateFormat:tz')); 125 | if (utcOffset !== 'Browser') { 126 | $scope.timelineOptions.moment = function (date) { 127 | return moment(date).utcOffset(utcOffset); 128 | }; 129 | } 130 | timeline.setOptions($scope.timelineOptions); 131 | } 132 | timeline.on('select', onSelect); 133 | timeline.on('rangechanged', function (props) { 134 | if ($scope.visOptions.syncTime && props.byUser) { 135 | timefilter.time.mode = 'absolute'; 136 | timefilter.time.from = props.start.toISOString(); 137 | timefilter.time.to = props.end.toISOString(); 138 | } 139 | }); 140 | } 141 | }; 142 | 143 | const groupEvents = []; 144 | 145 | const updateTimeline = function (groupIndex, events) { 146 | const existingGroupIds = _.map($scope.visOptions.groups, function (g) { 147 | return g.id; 148 | }); 149 | 150 | groupEvents[groupIndex] = _.cloneDeep(events); 151 | 152 | // make sure all events have correct group index 153 | // add only events from groups which still exists 154 | const points = []; 155 | _.each(groupEvents, function (events, index) { 156 | _.each(events, function (e) { 157 | e.group = $scope.visOptions.groupsOnSeparateLevels === true ? index : 0; 158 | if (existingGroupIds.indexOf(e.groupId) !== -1) { 159 | points.push(e); 160 | } 161 | }); 162 | }); 163 | 164 | data = new DataSet(points); 165 | timeline.setItems(data); 166 | timeline.fit(); 167 | }; 168 | 169 | const initSingleGroup = function (group, index) { 170 | const searchSource = group.searchSource; 171 | const params = group.params; 172 | const groupId = group.id; 173 | const groupColor = group.color; 174 | 175 | let numFrags = parseInt(config.get(NUM_FRAGS_CONFIG, NaN), 10); 176 | //(numFrags !== numFrags) is required instead of (numFrags === NaN) because NaN does not equals itself! 177 | if (numFrags !== numFrags || numFrags < 0) { 178 | numFrags = DEFAULT_NUM_FRAGS; 179 | config.set(NUM_FRAGS_CONFIG, DEFAULT_NUM_FRAGS); 180 | } 181 | 182 | if (params.useHighlight) { 183 | searchSource.highlight({ 184 | pre_tags: [highlightTags.pre], 185 | post_tags: [highlightTags.post], 186 | fields: { 187 | '*': { 188 | fragment_size: 0, 189 | number_of_fragments: numFrags 190 | } 191 | }, 192 | require_field_match: false 193 | }); 194 | } 195 | if (params.invertFirstLabelInstance) { 196 | searchSource.sort(TimelineHelper.getSortOnFieldObject(params.startField, params.startFieldSequence, 'asc')); 197 | } 198 | 199 | // We sort values to prevent the possibility of undefined records 200 | // (these ones, after sort function, are at the bottom of the object) 201 | if (params.orderBy || (searchSource._state && searchSource._state.index.id === '*')) { 202 | const orderBy = params.orderBy; 203 | const field = orderBy.substring(0, orderBy.indexOf('.')); 204 | const order = orderBy.substring(orderBy.indexOf('.') + 1); 205 | if (field === 'start') { 206 | searchSource.sort(TimelineHelper.getSortOnFieldObject(params.startField, params.startFieldSequence, order)); 207 | } else { 208 | searchSource.sort(TimelineHelper.getSortOnFieldObject(params.endField, params.endFieldSequence, order)); 209 | } 210 | } 211 | 212 | searchSource.onResults() 213 | .then(function onResults(searchResp) { 214 | const events = []; 215 | 216 | if (params.startField) { 217 | const startField = {}; 218 | const endField = {}; 219 | const uniqueLabels = []; 220 | 221 | _.each(searchResp.hits.hits, function (hit) { 222 | let labelValue = TimelineHelper.pluckLabel(hit, params, notify); 223 | if (labelValue.constructor === Array) { 224 | labelValue = labelValue.join(', '); 225 | } 226 | 227 | startField.value = TimelineHelper.pluckDate(hit, params.startField); 228 | 229 | if (params.endField) { 230 | endField.value = TimelineHelper.pluckDate(hit, params.endField); 231 | 232 | if (endField.value.length !== startField.value.length) { 233 | if ($scope.visOptions.notifyDataErrors) { 234 | notify.warning('Check your data - the number of values in the field \'' + params.endField + '\' ' + 235 | 'must be equal to the number of values in the field \'' + params.startField + 236 | '\': document ID=' + hit._id); 237 | } 238 | return; // break 239 | } 240 | } 241 | 242 | if (startField.value.length) { 243 | const indexId = searchSource.get('index').id; 244 | 245 | _.each(startField.value, function (startValue, i) { 246 | startValue = new Date(startValue); 247 | const endValue = endField.value && endField.value.length ? new Date(endField.value[i]) : null; 248 | 249 | const itemDict = { 250 | indexId: indexId, 251 | startField: params.startField, 252 | endField: params.endField, 253 | labelValue: labelValue, 254 | useHighlight: params.useHighlight, 255 | highlight: TimelineHelper.pluckHighlights(hit, highlightTags), 256 | groupColor: groupColor, 257 | startValue: startValue, 258 | endFieldValue: endValue 259 | }; 260 | 261 | const content = TimelineHelper.createItemTemplate(itemDict); 262 | 263 | let style = `background-color: ${groupColor}; color: #fff;`; 264 | if (!endValue || startValue.getTime() === endValue.getTime()) { 265 | // here the end field value missing but expected 266 | // or start field value === end field value 267 | // force vis box look like vis point 268 | style = `border-style: none; background-color: #fff; color: ${groupColor}; border-color: ${groupColor}`; 269 | } 270 | 271 | if (params.invertFirstLabelInstance && !_.includes(uniqueLabels, labelValue.toLowerCase().trim())) { 272 | if (!endValue || startValue.getTime() === endValue.getTime()) { 273 | style = `border-style: solid; background-color: #fff; color: ${groupColor}; border-color: ${groupColor}`; 274 | } else { 275 | style = `background-color: #fff; color: ${groupColor};`; 276 | } 277 | uniqueLabels.push(labelValue.toLowerCase().trim()); 278 | } 279 | 280 | const e = { 281 | _id: hit._id, 282 | _type: hit._type, 283 | index: indexId, 284 | content: content, 285 | value: labelValue, 286 | start: startValue, 287 | startField: { 288 | name: params.startField, 289 | value: startValue 290 | }, 291 | type: 'box', 292 | group: $scope.groupsOnSeparateLevels === true ? index : 0, 293 | style: style, 294 | groupId: groupId 295 | }; 296 | 297 | if (endValue && startValue.getTime() !== endValue.getTime()) { 298 | if (startValue.getTime() !== endValue.getTime()) { 299 | e.type = 'range'; 300 | e.end = endValue; 301 | e.endField = { 302 | name: params.endField, 303 | value: endValue 304 | }; 305 | } 306 | } 307 | 308 | events.push(e); 309 | 310 | }); 311 | } else { 312 | if ($scope.visOptions.notifyDataErrors) { 313 | notify.warning('Check your data - null start date not allowed.' + 314 | ' You can disable these errors in visualisation configuration'); 315 | } 316 | return; 317 | } 318 | }); 319 | } 320 | 321 | updateTimeline(index, events); 322 | 323 | return searchSource.onResults().then(onResults.bind(this)); 324 | 325 | }).catch(notify.error); 326 | }; 327 | 328 | const initGroups = function () { 329 | const groups = []; 330 | if ($scope.visOptions.groupsOnSeparateLevels === true) { 331 | _.each($scope.visOptions.groups, function (group, index) { 332 | groups.push({ 333 | id: index, 334 | content: group.label, 335 | style: 'background-color:' + group.color + '; color: #fff;' 336 | }); 337 | }); 338 | } else { 339 | // single group 340 | // - a bit of hack but currently the only way I could make it work 341 | groups.push({ 342 | id: 0, 343 | content: '', 344 | style: 'background-color: none;' 345 | }); 346 | } 347 | const dataGroups = new DataSet(groups); 348 | timeline.setGroups(dataGroups); 349 | }; 350 | 351 | $scope.$watch('timelineOptions', function (newOptions, oldOptions) { 352 | if (!newOptions || newOptions === oldOptions) { 353 | return; 354 | } 355 | initTimeline(); 356 | timeline.redraw(); 357 | }, true); // has to be true in other way the change in height is not detected 358 | 359 | const prereq = (function () { 360 | const fns = []; 361 | 362 | return function register(fn) { 363 | fns.push(fn); 364 | 365 | return function () { 366 | fn.apply(this, arguments); 367 | 368 | if (fns.length) { 369 | _.pull(fns, fn); 370 | if (!fns.length) { 371 | $scope.$root.$broadcast('ready:vis'); 372 | } 373 | } 374 | }; 375 | }; 376 | }()); 377 | 378 | const update = prereq(function update(newValue, oldValue) { 379 | if (newValue === oldValue) { 380 | return; 381 | } 382 | initTimeline(); 383 | if ($scope.visOptions.groups) { 384 | initGroups(); 385 | _.each($scope.visOptions.groups, (group, index) => { 386 | initSingleGroup(group, index); 387 | }); 388 | courier.fetch(); 389 | } 390 | }); 391 | 392 | $scope.$watch(function ($scope) { 393 | // here to make a comparison use all properties except a searchSource as it was causing angular to 394 | // enter an infinite loop when trying to determine the object equality 395 | if (!$scope.visOptions) { 396 | return; 397 | } 398 | const groupsWithoutSearchSource = _.map($scope.visOptions.groups, g => _.omit(g, 'searchSource')); 399 | return _.assign({}, $scope.visOptions, { groups: groupsWithoutSearchSource }); 400 | }, update, true); 401 | 402 | $scope.$watch('_.pluck(visOptions.groups, "searchSource")', update); 403 | 404 | $element.on('$destroy', function () { 405 | _.each($scope.visOptions.groups, (group) => { 406 | requestQueue.markAllRequestsWithSourceIdAsInactive(group.searchSource._id); 407 | }); 408 | if (timeline) { 409 | timeline.off('select', onSelect); 410 | } 411 | }); 412 | } // end of link function 413 | }); 414 | -------------------------------------------------------------------------------- /public/kibi_timeline_vis.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /public/kibi_timeline_vis.js: -------------------------------------------------------------------------------- 1 | import { VisVisTypeProvider } from 'ui/vis/vis_type'; 2 | import { TemplateVisTypeProvider } from 'ui/template_vis_type/template_vis_type'; 3 | import { VisSchemasProvider } from 'ui/vis/schemas'; 4 | import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; 5 | 6 | import 'plugins/kibi_timeline_vis/kibi_timeline_vis.less'; 7 | import 'plugins/kibi_timeline_vis/kibi_timeline_vis_controller'; 8 | import 'plugins/kibi_timeline_vis/kibi_timeline_vis_params'; 9 | import template from 'plugins/kibi_timeline_vis/kibi_timeline_vis.html'; 10 | 11 | function KibiTimelineVisProvider(Private) { 12 | const VisType = Private(VisVisTypeProvider); 13 | const TemplateVisType = Private(TemplateVisTypeProvider); 14 | const Schemas = Private(VisSchemasProvider); 15 | 16 | // return the visType object, which kibana will use to display and configure new 17 | // Vis object of this type. 18 | return new TemplateVisType({ 19 | name: 'kibi_timeline', 20 | title: 'Kibi Timeline', 21 | icon: 'fak-timeline', 22 | category: VisType.CATEGORY.KIBI, 23 | description: 'Timeline widget for visualization of events', 24 | template, 25 | params: { 26 | defaults: { 27 | groups: [], 28 | groupsOnSeparateLevels: false, 29 | selectValue: 'id', 30 | notifyDataErrors: false, 31 | syncTime: false 32 | }, 33 | editor: '' 34 | }, 35 | defaultSection: 'options', 36 | requiresSearch: false, 37 | requiresMultiSearch: true, 38 | requiresTimePicker: true, 39 | delegateSearch: true 40 | }); 41 | } 42 | 43 | VisTypesRegistryProvider.register(KibiTimelineVisProvider); 44 | -------------------------------------------------------------------------------- /public/kibi_timeline_vis.less: -------------------------------------------------------------------------------- 1 | @import "~kibi-icons/lib/stylesheets/FontAwesomeKibi.css"; 2 | 3 | .timeline-vis { 4 | padding: 1em; 5 | width: 100%; 6 | 7 | .vis-item{ 8 | cursor: pointer; 9 | } 10 | 11 | .tiny-txt { 12 | font-size: 0.8em; 13 | margin: 0; 14 | } 15 | 16 | .vis-timeline { 17 | border: none !important; 18 | } 19 | 20 | .vis-panel .vis-shadow { 21 | box-shadow: none!important; /* disable the shadow */ 22 | } 23 | 24 | .kibi-tl-dot-item { 25 | position: relative; 26 | padding: 0; 27 | border-width: 4px; 28 | border-style: solid; 29 | border-radius: 4px; 30 | float: left; 31 | margin-top: 6px; 32 | margin-right: 4px; 33 | border-color: red; 34 | } 35 | 36 | .kibi-tl-label-item { 37 | margin-left: 15px; 38 | } 39 | } 40 | 41 | .kibi-timeline-vis-params{ 42 | span.small_note { 43 | font-weight: normal; 44 | color: #999; 45 | } 46 | 47 | 48 | ul { 49 | margin: 0; 50 | padding: 0; 51 | 52 | li.queryOptionArea { 53 | margin-bottom: 2px; 54 | padding: 2px; 55 | position: relative; 56 | 57 | .header { 58 | background-color: #fff; 59 | h2 { 60 | font-size: 14px; 61 | margin: 2px 0; 62 | font-weight: bold; 63 | padding: 0; 64 | } 65 | } 66 | 67 | div.rightCorner { 68 | position: absolute; 69 | right: 0px; 70 | top: 0px; 71 | } 72 | } 73 | 74 | li .group-content { 75 | background-color: #ecf0f1; 76 | padding: 0 4px; 77 | } 78 | 79 | } 80 | } 81 | 82 | 83 | -------------------------------------------------------------------------------- /public/kibi_timeline_vis_controller.js: -------------------------------------------------------------------------------- 1 | import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter'; 2 | import RequestQueueProvider from './lib/courier/_request_queue_wrapped'; 3 | import { SearchSourceProvider } from 'ui/courier/data_source/search_source'; 4 | import chrome from 'ui/chrome'; 5 | import './kibi_timeline'; 6 | import 'ui/modules'; 7 | import _ from 'lodash'; 8 | import { uiModules } from 'ui/modules'; 9 | 10 | function controller(createNotifier, $location, $rootScope, $scope, $route, savedSearches, savedVisualizations, Private, Promise, courier, 11 | timefilter, indexPatterns) { 12 | const notify = createNotifier({ 13 | location: 'Kibi Timeline' 14 | }); 15 | const SearchSource = Private(SearchSourceProvider); 16 | const requestQueue = Private(RequestQueueProvider); 17 | const queryFilter = Private(FilterBarQueryFilterProvider); 18 | 19 | $scope.initOptions = function () { 20 | $scope.options = { 21 | width: '100%', 22 | height: '100%', 23 | selectable: true, 24 | autoResize: true 25 | }; 26 | }; 27 | 28 | const getGroupParamsHash = function (group) { 29 | return new Date().getTime(); 30 | let hash = ''; 31 | for (const key in group) { 32 | if (group.hasOwnProperty(key)) { 33 | hash += key + group[key]; 34 | } 35 | } 36 | return hash; 37 | }; 38 | 39 | $scope.initSearchSources = function (savedVis) { 40 | const getSavedSearches = Promise.all( 41 | _(savedVis.vis.params.groups) 42 | .filter(group => group.savedSearchId) 43 | .groupBy('savedSearchId') 44 | .map((groups, savedSearchId) => { 45 | return savedSearches.get(savedSearchId) 46 | .then(savedSearch => { 47 | return { savedSearch, groups }; 48 | }); 49 | }) 50 | .value() 51 | ); 52 | 53 | const fields = getSavedSearches.then(results => { 54 | return Promise.all(_.map(results, res => { 55 | return indexPatterns.get(res.savedSearch.searchSource._state.index.id) 56 | .then(indexPattern => indexPattern.fields); 57 | })); 58 | }); 59 | 60 | return Promise.all([ getSavedSearches, fields ]) 61 | .then(function ([ savedSearchesRes, fields ]) { 62 | // delete any searchSource that was previously created 63 | _.each($scope.visOptions.groups, group => { 64 | if (group.searchSource) { 65 | group.searchSource.destroy(); 66 | } 67 | }); 68 | 69 | $scope.visOptions.groups = []; 70 | let groupsParamsHash = ''; 71 | _.each(savedSearchesRes, function ({ savedSearch, groups }, i) { 72 | for (const group of groups) { 73 | groupsParamsHash += getGroupParamsHash(group); 74 | const _id = `_kibi_timetable_ids_source_flag${group.id}${savedSearch.id}`; // used only by kibi 75 | requestQueue.markAllRequestsWithSourceIdAsInactive(_id); // used only by kibi 76 | 77 | const searchSource = new SearchSource(); 78 | 79 | searchSource.inherits(savedSearch.searchSource); 80 | searchSource._id = _id; 81 | searchSource.index(savedSearch.searchSource._state.index); 82 | searchSource.size(group.size || 100); 83 | searchSource.source({ 84 | includes: _.compact([ group.labelField, group.startField, group.endField ]), 85 | excludes: [] 86 | }); 87 | searchSource.set('filter', queryFilter.getFilters()); 88 | 89 | // save label field name to `fielddata_fields` 90 | // it is needed to display not indexed fields in case of multi-fields 91 | searchSource._state.docvalue_fields = [ group.labelField ]; 92 | 93 | $scope.visOptions.groups.push({ 94 | id: group.id, 95 | color: group.color, 96 | label: group.groupLabel, 97 | searchSource: searchSource, 98 | params: { 99 | //kibi params 100 | // .path property doesn't exist in case of multi-fields 101 | labelFieldSequence: fields[i].byName[group.labelField].path, 102 | startFieldSequence: fields[i].byName[group.startField].path, 103 | endFieldSequence: group.endField && fields[i].byName[group.endField].path || [], 104 | //kibana params 105 | labelField: group.labelField, 106 | startField: group.startField, 107 | endField: group.endField, 108 | //params for both 109 | useHighlight: group.useHighlight, 110 | invertFirstLabelInstance: group.invertFirstLabelInstance 111 | } 112 | }); 113 | } 114 | }); 115 | 116 | const visOptionsButGroups = _.omit(savedVis.vis.params, 'groups'); 117 | // adding a hash of group parameters to detect when they changed 118 | // this is needed as we are ommiting search sources when watching changes to 119 | // visOptions. We can not watch changes to searchSources directly 120 | // as this triggers the infinite loop in the watcher inside kibi_timeline directive) 121 | visOptionsButGroups.hash = groupsParamsHash; 122 | _.assign($scope.visOptions, visOptionsButGroups); 123 | }) 124 | .catch(notify.error); 125 | }; 126 | 127 | $scope.visOptions = { 128 | groups: [] 129 | }; 130 | // Set to true in editing mode 131 | const configMode = $location.path().indexOf('/visualize/') !== -1; 132 | 133 | $scope.savedVis = $route.current.locals && $route.current.locals.savedVis; 134 | if (!$scope.savedVis) { 135 | // NOTE: reloading the visualization to get the searchSource, 136 | // which would otherwise be unavailable by design 137 | if ($scope.vis.id) { 138 | savedVisualizations.get($scope.vis.id).then(function (savedVis) { 139 | $scope.vis = savedVis.vis; 140 | $scope.savedVis = savedVis; 141 | }).catch(notify.error); 142 | } else { 143 | savedVisualizations.find($scope.vis.title).then(function (results) { 144 | const vis = _.find(results.hits, function (hit) { 145 | return hit.title === $scope.vis.title; 146 | }); 147 | if (!vis) { 148 | notify.error('Unable to find visualization with title == "' + $scope.vis.title + '"'); 149 | return; 150 | } 151 | return savedVisualizations.get(vis.id).then(function (savedVis) { 152 | $scope.vis = savedVis.vis; 153 | $scope.vis.id = vis.id; 154 | $scope.savedVis = savedVis; 155 | }); 156 | }).catch(notify.error); 157 | } 158 | } 159 | 160 | $scope.$on('change:vis', function () { 161 | $scope.initOptions(); 162 | }); 163 | 164 | $scope.$watch('savedVis', function () { 165 | if ($scope.savedVis) { 166 | $scope.initOptions(); 167 | $scope.initSearchSources($scope.savedVis); 168 | } 169 | }); 170 | 171 | // used also in autorefresh mode 172 | $scope.$watch('esResponse', function (resp) { 173 | if (resp) { 174 | _.each($scope.visOptions.groups, group => { 175 | group.searchSource.fetchQueued(); 176 | }); 177 | } 178 | }); 179 | 180 | // It is necessary to listen to those two events because the timeline visualization does not have 181 | // requiresSearch set to true since it needs more that one search. 182 | 183 | // on kibi, the editors.js file is updated to support requiresMultiSearch so that a courier.fetch call is executed 184 | const isInvestigate = chrome.getAppTitle() === 'Investigate'; 185 | 186 | // update the searchSource when filters update 187 | $scope.$listen(queryFilter, 'update', function () { 188 | _.each($scope.visOptions.groups, group => group.searchSource.set('filter', queryFilter.getFilters())); 189 | if (!isInvestigate) { 190 | courier.fetch(); 191 | } 192 | }); 193 | // fetch when the time changes 194 | $scope.$listen(timefilter, 'fetch', () => { 195 | _.each($scope.visOptions.groups, group => { 196 | group.searchSource.fetchQueued(); 197 | }); 198 | if (!isInvestigate) { 199 | courier.fetch(); 200 | } 201 | }); 202 | 203 | if (configMode) { 204 | const removeVisStateChangedHandler = $rootScope.$on('kibi:vis:state-changed', function () { 205 | $scope.initOptions(); 206 | $scope.initSearchSources($scope.savedVis); 207 | }); 208 | 209 | $scope.$on('$destroy', function () { 210 | removeVisStateChangedHandler(); 211 | }); 212 | } 213 | } 214 | 215 | uiModules 216 | .get('kibi_timeline_vis/kibi_timeline_vis', ['kibana']) 217 | .controller('KbnTimelineVisController', controller); 218 | -------------------------------------------------------------------------------- /public/kibi_timeline_vis_params.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
    6 | 11 |
  • 12 |
    13 | Group {{$index + 1}} 14 |
    15 | 16 |
    17 | 18 | 19 | 20 |
    21 | 22 | 23 | 24 |
    25 | 26 |
    27 | 28 | 29 |
    30 | 31 |
    32 | 33 |
    34 | 41 | 48 |
    49 | 50 | 51 |
    52 | 53 |
    54 | 55 | 56 | 57 |
    58 | 59 |
    60 | 61 | 62 | 63 |
    64 | 65 |
    66 | 67 | 68 |
    69 | 70 |
    71 | 72 | 79 |
    80 | 81 |
    82 | 83 | 84 | 85 |
    86 | 87 |
    88 |
  • 89 |
90 | 96 |
97 | 98 | 105 | 106 |
107 |
108 |
109 | 112 | 115 | 120 |
121 |
122 | 123 |
124 | 127 | 130 | 133 |
134 |
135 |
136 |
137 | 138 |
139 | -------------------------------------------------------------------------------- /public/kibi_timeline_vis_params.js: -------------------------------------------------------------------------------- 1 | import { VislibComponentsColorColorProvider } from 'ui/vis/components/color/color'; 2 | // === ported kibi directives === 3 | import './lib/directives/array_param'; 4 | import './lib/directives/kibi_select'; 5 | // === ported kibi directives === 6 | import _ from 'lodash'; 7 | import { uiModules } from 'ui/modules'; 8 | import template from 'plugins/kibi_timeline_vis/kibi_timeline_vis_params.html'; 9 | 10 | function controller($rootScope, savedSearches, Private, Promise) { 11 | const color = Private(VislibComponentsColorColorProvider); 12 | 13 | return { 14 | restrict: 'E', 15 | template, 16 | link: function ($scope, $element, attr) { 17 | // here deal with the parameters 18 | // Emit an event when state changes 19 | $scope.$watch('vis.dirty', function () { 20 | if ($scope.vis.dirty === false) { 21 | $rootScope.$emit('kibi:vis:state-changed'); 22 | } 23 | }); 24 | 25 | const _pickNextFreeId = function (takenIds) { 26 | // we start from 5000 to avoid confusion with index 27 | // index can not be used as user can move elements up and down 28 | // and if we use index that would affect colors 29 | if (takenIds.length === 0) { 30 | return 5000; 31 | } 32 | return takenIds[takenIds.length - 1] + 1; 33 | }; 34 | 35 | let savedSearchToIndexPatternMap; 36 | const init = function () { 37 | if (savedSearchToIndexPatternMap) { 38 | return Promise.resolve(savedSearchToIndexPatternMap); 39 | } 40 | return savedSearches.find().then(savedSearches => { 41 | const map = {}; 42 | _.each(savedSearches.hits, hit => { 43 | try { 44 | const searchSource = JSON.parse(hit.kibanaSavedObjectMeta.searchSourceJSON); 45 | map[hit.id] = { 46 | index: searchSource.index, 47 | title: hit.title 48 | }; 49 | } catch (e) { 50 | // should never happen 51 | } 52 | }); 53 | savedSearchToIndexPatternMap = map; 54 | }); 55 | }; 56 | 57 | $scope.$watch('vis.params.groups', function (groups) { 58 | init().then(() => { 59 | const existingGroupIds = []; 60 | _.each($scope.vis.params.groups, function (group) { 61 | if (group.id) { 62 | existingGroupIds.push(group.id); 63 | } 64 | }); 65 | existingGroupIds.sort(); 66 | 67 | _.each($scope.vis.params.groups, function (group) { 68 | // we need unique ids to manage data series in timeline component 69 | if (!group.id) { 70 | group.id = _pickNextFreeId(existingGroupIds); 71 | } 72 | if (group.savedSearchId) { 73 | if (!group.groupLabel) { 74 | group.groupLabel = savedSearchToIndexPatternMap[group.savedSearchId].title; 75 | } 76 | // we use $$ prefix to avoid saving this temporary value into the model 77 | // Angular strips values prefixed with $$ automatically 78 | group.$$indexPatternId = savedSearchToIndexPatternMap[group.savedSearchId].index; 79 | delete group.__new; 80 | } 81 | }); 82 | 83 | // 0 should always be there in case user switch to mixed mode 84 | const mapGroupIdToColor = color([0].concat(_.map($scope.vis.params.groups, 'id'))); 85 | _.each($scope.vis.params.groups, function (group) { 86 | group.color = mapGroupIdToColor(group.id); 87 | }); 88 | 89 | }); 90 | }, true); 91 | } 92 | }; 93 | } 94 | 95 | uiModules 96 | .get('kibi_timeline_vis/kibi_timeline_vis') 97 | .directive('kibiTimelineVisParams', controller); 98 | -------------------------------------------------------------------------------- /public/lib/courier/_request_queue_wrapped.js: -------------------------------------------------------------------------------- 1 | import { RequestQueueProvider } from 'ui/courier/_request_queue'; 2 | 3 | export default function PendingRequestListWrapped(Private) { 4 | const requestQueue = Private(RequestQueueProvider); 5 | 6 | requestQueue.markAllRequestsWithSourceIdAsInactive = function (_id) { 7 | // iterate backwords so when we remove 1 item we do not care about the length being changed 8 | const n = this.length - 1; 9 | 10 | for (let i = n; i >= 0; i--) { 11 | const r = this[i]; 12 | if (r && r.source && r.source._id === _id) { 13 | // mark source as inactive 14 | r.source._disabled = true; 15 | // remove the request from queue 16 | this.splice(i, 1); 17 | } 18 | } 19 | }; 20 | 21 | return requestQueue; 22 | }; 23 | -------------------------------------------------------------------------------- /public/lib/directives/array_param.js: -------------------------------------------------------------------------------- 1 | import ArrayHelper from '../helpers/array_helper'; 2 | import { uiModules } from 'ui/modules'; 3 | import addTemplate from '../directives/array_param_add.html'; 4 | 5 | uiModules 6 | .get('kibana') 7 | .directive('arrayParamAddPort', function (createNotifier, arrayParamServicePort) { 8 | const notify = createNotifier({ 9 | location: 'Array Param Directive' 10 | }); 11 | 12 | return { 13 | restrict: 'E', 14 | replace: true, 15 | scope: { 16 | model: '=', 17 | disable: '=?', 18 | label: '@', 19 | postAction: '&', 20 | default: '@' // used to define the default element added to the array. It is an empty object if unset 21 | }, 22 | template: addTemplate, 23 | link: function ($scope, element, attrs) { 24 | $scope.required = attrs.hasOwnProperty('required'); 25 | 26 | arrayParamServicePort.required = $scope.required; 27 | arrayParamServicePort.label = $scope.label; 28 | 29 | if (!$scope.model) { 30 | notify.error('You must initialise the model for the button labelled "' + $scope.label + '" !'); 31 | return; 32 | } 33 | 34 | $scope.addParam = function () { 35 | let el = {}; 36 | if ($scope.default) { 37 | let json; 38 | try { 39 | json = JSON.parse($scope.default); 40 | el = json; 41 | } catch (err) { 42 | el = $scope.default; 43 | } 44 | } 45 | ArrayHelper.add($scope.model, el, $scope.postAction); 46 | }; 47 | 48 | // if it is required, add at least one element to the array 49 | if ($scope.required && $scope.model.length === 0) { 50 | $scope.addParam(); 51 | } 52 | } 53 | }; 54 | }).directive('arrayParamUpPort', function () { 55 | return { 56 | restrict: 'E', 57 | replace: true, 58 | scope: { 59 | model: '=', 60 | index: '@', 61 | postAction: '&' 62 | }, 63 | template: '', 64 | link: function ($scope, element, attrs) { 65 | $scope.upParam = function () { 66 | ArrayHelper.up($scope.model, $scope.index, $scope.postAction); 67 | }; 68 | } 69 | }; 70 | }).directive('arrayParamDownPort', function () { 71 | return { 72 | restrict: 'E', 73 | replace: true, 74 | scope: { 75 | model: '=', 76 | index: '@', 77 | postAction: '&' 78 | }, 79 | template: '', 80 | link: function ($scope, element, attrs) { 81 | $scope.downParam = function () { 82 | ArrayHelper.down($scope.model, $scope.index, $scope.postAction); 83 | }; 84 | } 85 | }; 86 | }).directive('arrayParamRemovePort', function (createNotifier, arrayParamServicePort) { 87 | const notify = createNotifier({ 88 | location: 'Array Param Directive' 89 | }); 90 | 91 | return { 92 | restrict: 'E', 93 | replace: true, 94 | scope: { 95 | model: '=', 96 | index: '@', 97 | postAction: '&' 98 | }, 99 | template: '', 100 | link: function ($scope, element, attrs) { 101 | $scope.removeParam = function () { 102 | if (!arrayParamServicePort.required || $scope.model.length > 1) { 103 | ArrayHelper.remove($scope.model, $scope.index, $scope.postAction); 104 | } else if (arrayParamServicePort.required) { 105 | notify.warning('You need to add at least one ' + arrayParamServicePort.label + '.'); 106 | } 107 | }; 108 | } 109 | }; 110 | }).factory('arrayParamServicePort', function () { 111 | return { required: false, label: '' }; 112 | }); 113 | -------------------------------------------------------------------------------- /public/lib/directives/array_param_add.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /public/lib/directives/kibi_select.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 6 | Analyzed Field 7 | 8 | 9 | 12 | Wildcard Query 13 | 14 | 15 |
16 | 17 |
18 |

19 | Unable to determine variable names from a wildcard query, please specify the variable name below. You can You can review the list of columns in the query or explicitly return the relevant columns in the SELECT clause. 20 |

21 |
22 | 23 |
24 |

25 | Careful! The field selected contains analyzed strings. Values such as foo-bar will be broken into foo and bar. See Mapping Core Types for more information on setting this field as not_analyzed 26 |

27 |
28 | 29 | 30 | 31 | 32 | 33 | 42 | 43 |
44 |

45 | Select error 46 |

47 |

48 | An error occured while retrieving this select's data. 49 |

50 |
{{ retrieveError }}
51 |
52 |
53 | -------------------------------------------------------------------------------- /public/lib/directives/kibi_select.js: -------------------------------------------------------------------------------- 1 | import SelectHelperProvider from './kibi_select_helper'; 2 | import _ from 'lodash'; 3 | import { uiModules } from 'ui/modules'; 4 | import template from './kibi_select.html'; 5 | 6 | uiModules.get('kibana') 7 | .directive('kibiSelectPort', function (Private) { 8 | const selectHelper = Private(SelectHelperProvider); 9 | 10 | return { 11 | require: 'ngModel', 12 | restrict: 'E', 13 | replace: true, 14 | scope: { 15 | // the id of the kibi-select object 16 | id: '=?', 17 | // Filter function which returns true for items to be removed. 18 | // There are two arguments: 19 | // - id: the id of the kibi-select 20 | // - value: the value of the item 21 | // Since the filter function is called with arguments, a function named "myfunc" should be passed 22 | // as 'filter="myfunc"'. 23 | // See http://weblogs.asp.net/dwahlin/creating-custom-angularjs-directives-part-3-isolate-scope-and-function-parameters 24 | // 25 | // If the value is **undefined**, the function may return an object that is used in the angular watcher. 26 | filter: '&?', 27 | objectType: '@', // text 28 | indexPatternId: '=?', // optional only for objectType === field | indexPatternType | documentIds 29 | indexPatternType: '=?', // optional only for objectType === documentIds 30 | fieldTypes: '=?', // optional only for objectType === field, value should be array of strings 31 | queryId: '=?', // optional only for objectType === queryVariable 32 | modelDisabled: '=?', // use to disable the underlying select 33 | modelRequired: '=?', // use to disable the underlying select 34 | include: '=?', // extra values can be passed here 35 | analyzedWarning: '@', // set to true or false to disable/enable analyzed field warning 36 | filterable: '=?' // optional - enable selection filtering 37 | }, 38 | template, 39 | link: function (scope, element, attrs, ngModelCtrl) { 40 | scope.isValid = true; 41 | scope.required = scope.modelRequired; 42 | scope.disabled = scope.modelDisabled; 43 | if (attrs.hasOwnProperty('required')) { 44 | scope.required = true; 45 | } 46 | scope.modelObject = ngModelCtrl.$viewValue; //object 47 | scope.items = []; 48 | 49 | scope.$watch( 50 | function () { 51 | return ngModelCtrl.$modelValue; 52 | }, 53 | function (newValue) { 54 | scope.modelObject = ngModelCtrl.$viewValue; //object 55 | } 56 | ); 57 | 58 | const _setViewValue = function () { 59 | if (scope.modelObject) { 60 | ngModelCtrl.$setViewValue(scope.modelObject); 61 | } else { 62 | ngModelCtrl.$setViewValue(null); 63 | } 64 | }; 65 | 66 | scope.$watch('modelDisabled', function () { 67 | scope.disabled = scope.modelDisabled; 68 | if (scope.modelDisabled) { 69 | scope.required = false; 70 | } 71 | _setViewValue(); 72 | }); 73 | 74 | scope.$watch('modelRequired', function () { 75 | if (scope.modelRequired !== undefined) { 76 | scope.required = scope.modelRequired; 77 | _setViewValue(); 78 | } 79 | }); 80 | 81 | scope.$watch('modelObject', function () { 82 | _setViewValue(); 83 | }, true); 84 | 85 | ngModelCtrl.$formatters.push(function (modelValue) { 86 | // here what is passed to a formatter is just a string 87 | let formatted; 88 | if (scope.items.length) { 89 | formatted = _.find(scope.items, function (item) { 90 | return item.value === modelValue; 91 | }); 92 | } 93 | 94 | if (!formatted && modelValue) { 95 | formatted = { 96 | value: modelValue, 97 | label: '' 98 | }; 99 | } 100 | return formatted; 101 | }); 102 | 103 | ngModelCtrl.$parsers.push(function (viewValue) { 104 | const ret = viewValue ? viewValue.value : null; 105 | scope.isValid = scope.required ? !!ret : true; 106 | ngModelCtrl.$setValidity('stSelect', scope.required ? !!ret : true); 107 | return ret; 108 | }); 109 | 110 | function autoSelect(items) { 111 | if (scope.required) { 112 | return items.length === 2; // first element is the empty one 113 | } 114 | return false; 115 | } 116 | 117 | const _renderSelect = function (items) { 118 | scope.analyzedField = false; 119 | scope.items = items; 120 | if (scope.items) { 121 | if (scope.include && scope.include.length) { 122 | // remove elements in items that appear in the extra items 123 | _.remove(scope.items, function (item) { 124 | return !!_.find(scope.include, function (extraItem) { 125 | return item.value === extraItem.value; 126 | }); 127 | }); 128 | scope.items = scope.include.concat(scope.items); 129 | } 130 | // sort by label 131 | scope.items = _.sortBy(scope.items, 'label'); 132 | 133 | if (scope.filter && _.isFunction(scope.filter())) { 134 | _.remove(scope.items, function (item) { 135 | const selected = !!ngModelCtrl.$viewValue && !!ngModelCtrl.$viewValue.value && 136 | ngModelCtrl.$viewValue.value === item.value; 137 | const toRemove = scope.filter()(scope.id, item.value); 138 | return toRemove && !selected; 139 | }); 140 | } 141 | // if the select is NOT required, the user is able to choose an empty element 142 | if (scope.items.length > 0 && _.first(scope.items).value !== null) { 143 | scope.items.splice(0, 0, { 144 | label: '', 145 | value: null 146 | }); 147 | } 148 | } 149 | 150 | const item = _.find(scope.items, function (item) { 151 | return ngModelCtrl.$viewValue && item.value === ngModelCtrl.$viewValue.value; 152 | }); 153 | 154 | if (item && item.options && item.options.analyzed) { 155 | scope.analyzedField = true; 156 | } else if (autoSelect(scope.items)) { 157 | // select automatically if only 1 option is available and the select is required 158 | scope.modelObject = scope.items[1]; 159 | } else if (scope.items && scope.items.length > 0 && !item) { 160 | // object saved in the model is not in the list of items 161 | scope.modelObject = { 162 | value: '', 163 | label: '' 164 | }; 165 | } 166 | }; 167 | 168 | const _render = function () { 169 | let promise; 170 | 171 | switch (scope.objectType) { 172 | case 'search': 173 | promise = selectHelper.getObjects(scope.objectType, scope.itemsFilter); 174 | break; 175 | case 'field': 176 | promise = selectHelper.getFields(scope.indexPatternId, scope.fieldTypes); 177 | break; 178 | } 179 | 180 | scope.retrieveError = ''; 181 | if (promise) { 182 | promise.then(_renderSelect).catch(function (err) { 183 | scope.retrieveError = _.isEmpty(err) ? '' : err; 184 | ngModelCtrl.$setValidity('stSelect', false); 185 | }); 186 | } 187 | }; 188 | 189 | scope.filterItems = function () { 190 | scope.itemsFilter = this.itemsFilter; 191 | _render(); 192 | }; 193 | 194 | scope.$watchMulti(['indexPatternId', 'indexPatternType', 'queryId', 'include', 'modelDisabled', 'modelRequired'], function () { 195 | _render(); 196 | }); 197 | 198 | scope.$watch(function (scope) { 199 | if (scope.filter && _.isFunction(scope.filter())) { 200 | return scope.filter()(scope.id); 201 | } 202 | }, function () { 203 | _render(); 204 | }, true); 205 | _render(); 206 | } 207 | 208 | }; 209 | }); 210 | -------------------------------------------------------------------------------- /public/lib/directives/kibi_select_helper.js: -------------------------------------------------------------------------------- 1 | import chrome from 'ui/chrome'; 2 | import _ from 'lodash'; 3 | 4 | export default function KibiSelectHelperFactory(config, indexPatterns, savedSearches, $injector) { 5 | class KibiSelectHelper { 6 | getObjects(type, filter) { 7 | return savedSearches.find(filter).then(function (resp) { 8 | const items = _.map(resp.hits, function (hit) { 9 | return { 10 | label: hit.title, 11 | value: hit.id 12 | }; 13 | }); 14 | return items; 15 | }); 16 | } 17 | 18 | getFields(indexPatternId, fieldTypes) { 19 | 20 | let defaultIndexPattern; 21 | if (indexPatternId) { 22 | defaultIndexPattern = indexPatterns.get(indexPatternId); 23 | } else { 24 | if ($injector.has('kibiDefaultIndexPattern')) { 25 | // kibi 26 | defaultIndexPattern = $injector.get('kibiDefaultIndexPattern').getDefaultIndexPattern(); 27 | } else { 28 | // kibana 29 | defaultIndexPattern = indexPatterns.get(config.get('defaultIndex')); 30 | } 31 | } 32 | 33 | return defaultIndexPattern.then(function (index) { 34 | const fields = _.chain(index.fields) 35 | .filter(function (field) { 36 | // filter some fields 37 | if (fieldTypes instanceof Array && fieldTypes.length > 0) { 38 | return fieldTypes.indexOf(field.type) !== -1 && field.name && field.name.indexOf('_') !== 0; 39 | } else { 40 | return field.type !== 'boolean' && field.name && field.name.indexOf('_') !== 0; 41 | } 42 | }).sortBy(function (field) { 43 | return field.name; 44 | }).map(function (field) { 45 | return { 46 | label: field.name, 47 | value: field.name 48 | }; 49 | }).value(); 50 | return fields; 51 | }); 52 | } 53 | } 54 | 55 | return new KibiSelectHelper(); 56 | }; 57 | -------------------------------------------------------------------------------- /public/lib/helpers/__tests__/timeline_helper.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect.js'; 2 | import TimelineHelper from '../timeline_helper'; 3 | import moment from 'moment'; 4 | import sinon from 'sinon'; 5 | 6 | describe('Kibi Timeline', function () { 7 | describe('TimelineHelper', function () { 8 | describe('getSortOnFieldObject', function () { 9 | it('should return a sort ES object from startField', function () { 10 | expect(TimelineHelper.getSortOnFieldObject('date', '', 'asc')).to.eql({ 11 | date: { 12 | order: 'asc' 13 | } 14 | }); 15 | }); 16 | 17 | it('should return a sort ES object from startFieldSequence', function () { 18 | expect(TimelineHelper.getSortOnFieldObject('', [ 'my.other', 'date' ], 'asc')).to.eql({ 19 | 'my.other.date': { 20 | order: 'asc' 21 | } 22 | }); 23 | }); 24 | }); 25 | 26 | describe('pluckLabel', function () { 27 | let notify; 28 | 29 | beforeEach(function () { 30 | notify = { 31 | warning: sinon.spy() 32 | }; 33 | }); 34 | 35 | it('should return the label of an event kibana-style', function () { 36 | const hit = { 37 | _source: { 38 | aaa: { 39 | bbb: { 40 | ccc: 'ddd' 41 | } 42 | } 43 | } 44 | }; 45 | const params = { 46 | labelField: 'aaa.bbb.ccc' 47 | }; 48 | 49 | expect(TimelineHelper.pluckLabel(hit, params, notify)).to.eql([ 'ddd' ]); 50 | sinon.assert.notCalled(notify.warning); 51 | }); 52 | 53 | it('should return the label of an event kibi-style', function () { 54 | const hit = { 55 | _source: { 56 | aaa: { 57 | bbb: { 58 | ccc: 'ddd' 59 | } 60 | } 61 | } 62 | }; 63 | const params = { 64 | labelField: 'aaa.bbb.ccc', 65 | labelFieldSequence: [ 'aaa', 'bbb', 'ccc' ] 66 | }; 67 | 68 | expect(TimelineHelper.pluckLabel(hit, params, notify)).to.eql(['ddd']); 69 | sinon.assert.notCalled(notify.warning); 70 | }); 71 | 72 | it('should return N/A if the event does not a value for the labelField', function () { 73 | const hit = { 74 | _source: { 75 | ccc: 'ddd' 76 | } 77 | }; 78 | const params = { 79 | labelField: 'aaa', 80 | labelFieldSequence: [ 'aaa' ] 81 | }; 82 | 83 | expect(TimelineHelper.pluckLabel(hit, params, notify)).to.be('N/A'); 84 | sinon.assert.notCalled(notify.warning); 85 | }); 86 | 87 | it('should return a label value in case of multi-fields, kibi-style', function () { 88 | const hit = { 89 | _source: { 90 | 'city': 'Galway' 91 | }, 92 | fields: { 93 | 'city.raw': ['Galway'] 94 | } 95 | }; 96 | const params = { 97 | labelField: 'city.raw', 98 | labelFieldSequence: [ 'city.raw' ] 99 | }; 100 | 101 | expect(TimelineHelper.pluckLabel(hit, params)).to.eql('Galway'); 102 | sinon.assert.notCalled(notify.warning); 103 | }); 104 | 105 | it('should return a label value in case of multi-fields, kibana-style', function () { 106 | const hit = { 107 | _source: {}, 108 | fields: { 109 | 'city.raw': ['Galway'] 110 | } 111 | }; 112 | const params = { 113 | labelField: 'city.raw', 114 | labelFieldSequence: undefined 115 | }; 116 | 117 | expect(TimelineHelper.pluckLabel(hit, params)).to.eql('Galway'); 118 | sinon.assert.notCalled(notify.warning); 119 | }); 120 | }); 121 | 122 | describe('pluckDate', function () { 123 | 124 | it('should return a date string value and raw value, in case of multi-fields', function () { 125 | const hit = { 126 | _source: {}, 127 | fields: { 128 | 'arrive.raw': [ Date.parse('Wed, 09 Aug 1995 00:00:00 GMT') ] 129 | } 130 | }; 131 | const params = { 132 | startField: 'arrive.raw', 133 | startFieldSequence: [ 'arrive.raw' ], 134 | }; 135 | 136 | const date = TimelineHelper.pluckDate(hit, params.startField, params.startFieldSequence); 137 | expect(date).to.eql([ 807926400000 ]); 138 | }); 139 | 140 | }); 141 | 142 | describe('pluckHighlights', function () { 143 | const highlightTags = { 144 | pre: '', 145 | post: '' 146 | }; 147 | 148 | it('should return an empty string if the hit has no highlight object', function () { 149 | const hit = { 150 | _source: { 151 | aaa: 'bbb' 152 | } 153 | }; 154 | 155 | expect(TimelineHelper.pluckHighlights(hit, highlightTags)).to.be(''); 156 | }); 157 | 158 | it('should return the highlighted terms in the event', function () { 159 | const hit = { 160 | _source: { 161 | field1: 'bbb' 162 | }, 163 | highlight: { 164 | field1: [ 165 | 'dddnope', 166 | 'nopebbbnope', 167 | 'nopeccc' 168 | ], 169 | field2: [ 170 | 'nopebbbnope' 171 | ] 172 | } 173 | }; 174 | 175 | expect(TimelineHelper.pluckHighlights(hit, highlightTags)).to.be('bbb: 2, ccc: 1, ddd: 1'); 176 | }); 177 | }); 178 | 179 | describe('changeTimezone', function () { 180 | it('should return Browser for default Kibana timezone', function () { 181 | expect(TimelineHelper.changeTimezone('Browser')).to.be('Browser'); 182 | }); 183 | 184 | it('should be a moment object', function () { 185 | expect(TimelineHelper.changeTimezone('America/Nassau')).to.match(/[-+][0-9]{2}:[0-9]{2}/); 186 | }); 187 | }); 188 | 189 | }); 190 | }); 191 | -------------------------------------------------------------------------------- /public/lib/helpers/array_helper.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | export default class ArrayHelper { 4 | static add(array, object, callback) { 5 | array.push(object); 6 | if (callback) { 7 | callback(); 8 | } 9 | } 10 | 11 | static remove(array, index, callback) { 12 | array.splice(index, 1); 13 | if (callback) { 14 | callback(); 15 | } 16 | } 17 | 18 | static up(array, index, callback) { 19 | if (index > 0) { 20 | const newIndex = index - 1; 21 | const currentElement = _.clone(array[index], true); 22 | array.splice(index, 1); 23 | array.splice(newIndex, 0, currentElement); 24 | if (callback) { 25 | callback(); 26 | } 27 | } 28 | } 29 | 30 | static down(array, index, callback) { 31 | if (index < array.length - 1) { 32 | const newIndex = index + 1; 33 | const currentElement = _.clone(array[index], true); 34 | array.splice(index, 1); 35 | array.splice(newIndex, 0, currentElement); 36 | if (callback) { 37 | callback(); 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /public/lib/helpers/timeline_helper.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import kibiUtils from 'kibiutils'; 3 | import moment from 'moment-timezone'; 4 | 5 | function extractFragment(highlightedElement, openTag, closeTag) { 6 | const openIndex = highlightedElement.indexOf(openTag); 7 | const closeIndex = highlightedElement.indexOf(closeTag); 8 | return highlightedElement.substring(openIndex + openTag.length, closeIndex).toLowerCase().trim(); 9 | } 10 | 11 | export default class TimelineHelper { 12 | static noEndOrEqual(startValue, endValue) { 13 | return !endValue || startValue === endValue ? true : false; 14 | } 15 | 16 | static createItemTemplate(itemDict) { 17 | let endfield = ''; 18 | let dot = ''; 19 | let hilit = ''; 20 | let label = itemDict.labelValue; 21 | 22 | if (itemDict.endField) { 23 | endfield = `, endField: ${itemDict.endField}`; 24 | } 25 | if (this.noEndOrEqual(itemDict.startValue, itemDict.endValue)) { 26 | dot = `
`; 27 | label = `
${itemDict.labelValue}
`; 28 | } 29 | if (itemDict.useHighlight) { 30 | hilit = `

${itemDict.highlight}

`; 31 | } 32 | 33 | return `
${dot}${label}${hilit}
`; 34 | } 35 | 36 | static changeTimezone(timezone) { 37 | if (timezone !== 'Browser') { 38 | return moment().tz(timezone).format('Z'); 39 | } else { 40 | return timezone; 41 | } 42 | } 43 | 44 | /** 45 | * getMultiFieldValue get a field value if the field is a multi-field 46 | * 47 | * @param hit the document of the event 48 | * @param f field name 49 | * @returns field value 50 | */ 51 | static getMultiFieldValue(hit, f) { 52 | if (hit.fields && hit.fields[f]) { 53 | const val = hit.fields[f]; 54 | return _.isArray(val) && val.length === 1 ? val[0] : val; 55 | } 56 | }; 57 | 58 | /** 59 | * pluckLabel returns the label of an event 60 | * 61 | * @param hit the document of the event 62 | * @param params configuration parameters for the event 63 | * @param notify object for user notification 64 | * @returns the label as a string 65 | */ 66 | static pluckLabel(hit, params, notify) { 67 | let field; 68 | let value; 69 | 70 | // in kibi, we have the path property of a field 71 | if (params.labelFieldSequence && params.labelFieldSequence.length) { 72 | field = params.labelFieldSequence.join('.'); // labelFieldSequence is an array 73 | value = kibiUtils.getValuesAtPath(hit._source, params.labelFieldSequence); 74 | } else if (params.labelField) { 75 | field = params.labelField; // labelField is a plain string 76 | value = kibiUtils.getValuesAtPath(hit._source, field.split('.')); 77 | } 78 | 79 | if (!value || !value.length) { 80 | value = this.getMultiFieldValue(hit, field); 81 | } 82 | 83 | return value && (!_.isArray(value) || value.length) ? value : 'N/A'; 84 | }; 85 | 86 | /** 87 | * pluckDate returns date field value/raw value 88 | * 89 | * @param hit the document of the event 90 | * @param field to represent params.startField or params.endField 91 | * @returns date raw value 92 | */ 93 | static pluckDate(hit, field) { 94 | // there is no date string value in _source in case of multi-fields 95 | return hit.fields[field] || []; 96 | }; 97 | 98 | /** 99 | * pluckHighlights returns the highlighted terms for the event. 100 | * The terms are sorted first on the number of occurrences of a term, and then alphabetically. 101 | * 102 | * @param hit the event 103 | * @param highlightTags the tags that wrap the term 104 | * @returns a comma-separated string of the highlighted terms and their number of occurrences 105 | */ 106 | static pluckHighlights(hit, highlightTags) { 107 | if (!hit.highlight) { 108 | return ''; 109 | } 110 | 111 | //Track unique highlights, count number of times highlight occurs. 112 | const counts = new Map(); //key is highlight tag, value is count 113 | Object.keys(hit.highlight).forEach(function (key) { 114 | hit.highlight[key].forEach(function (it) { 115 | const fragment = extractFragment(it, highlightTags.pre, highlightTags.post); 116 | if (counts.has(fragment)) { 117 | counts.set(fragment, counts.get(fragment) + 1); 118 | } else { 119 | counts.set(fragment, 1); 120 | } 121 | }); 122 | }); 123 | 124 | return Array.from(counts.keys()) 125 | .sort(function (a, b) { 126 | const countA = counts.get(a); 127 | const countB = counts.get(b); 128 | //same count, return alphabetic order 129 | if (countA === countB) { 130 | if (a > b) { 131 | return 1; 132 | } else if (a < b) { 133 | return -1; 134 | } else { 135 | return 0; 136 | }; 137 | } 138 | //return count order 139 | if (countA < countB) { 140 | return 1; 141 | } else if (countA > countB) { 142 | return -1; 143 | } else { 144 | return 0; 145 | }; 146 | }) 147 | .map(key => `${key}: ${counts.get(key)}`) 148 | .join(', '); 149 | } 150 | 151 | 152 | /** 153 | * Creates an Elasticsearch sort object to sort in chronological order on start field 154 | * 155 | * @param params group configuraton parameters 156 | * @returns Elasticsearch sort object 157 | */ 158 | static getSortOnFieldObject = function (field, fieldSequence, orderBy) { 159 | const sortObj = {}; 160 | if (fieldSequence) { 161 | sortObj[fieldSequence.join('.')] = { order: orderBy }; 162 | } else { 163 | sortObj[field] = { order: orderBy }; 164 | } 165 | return sortObj; 166 | }; 167 | } 168 | -------------------------------------------------------------------------------- /public/webpackShims/vis-timeline.js: -------------------------------------------------------------------------------- 1 | window.moment = require('moment'); 2 | require('../../node_modules/vis/dist/vis-timeline-graph2d.min.css'); 3 | module.exports = require('../../node_modules/vis/dist/vis-timeline-graph2d.min'); 4 | -------------------------------------------------------------------------------- /target/kibi_timeline_vis-0.1.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirensolutions/kibi_timeline_vis/8289bdb4d8585378bbc2c9b74aa4332b7483f089/target/kibi_timeline_vis-0.1.0.zip -------------------------------------------------------------------------------- /target/kibi_timeline_vis-0.1.1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirensolutions/kibi_timeline_vis/8289bdb4d8585378bbc2c9b74aa4332b7483f089/target/kibi_timeline_vis-0.1.1.zip -------------------------------------------------------------------------------- /target/kibi_timeline_vis-0.1.2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirensolutions/kibi_timeline_vis/8289bdb4d8585378bbc2c9b74aa4332b7483f089/target/kibi_timeline_vis-0.1.2.zip -------------------------------------------------------------------------------- /target/kibi_timeline_vis-0.1.3.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirensolutions/kibi_timeline_vis/8289bdb4d8585378bbc2c9b74aa4332b7483f089/target/kibi_timeline_vis-0.1.3.zip -------------------------------------------------------------------------------- /target/kibi_timeline_vis-0.1.4.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirensolutions/kibi_timeline_vis/8289bdb4d8585378bbc2c9b74aa4332b7483f089/target/kibi_timeline_vis-0.1.4.zip -------------------------------------------------------------------------------- /target/kibi_timeline_vis-4.4.2-2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirensolutions/kibi_timeline_vis/8289bdb4d8585378bbc2c9b74aa4332b7483f089/target/kibi_timeline_vis-4.4.2-2.zip -------------------------------------------------------------------------------- /target/kibi_timeline_vis-4.4.2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirensolutions/kibi_timeline_vis/8289bdb4d8585378bbc2c9b74aa4332b7483f089/target/kibi_timeline_vis-4.4.2.zip -------------------------------------------------------------------------------- /target/kibi_timeline_vis-4.5.3-2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirensolutions/kibi_timeline_vis/8289bdb4d8585378bbc2c9b74aa4332b7483f089/target/kibi_timeline_vis-4.5.3-2.zip -------------------------------------------------------------------------------- /target/kibi_timeline_vis-4.5.3.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirensolutions/kibi_timeline_vis/8289bdb4d8585378bbc2c9b74aa4332b7483f089/target/kibi_timeline_vis-4.5.3.zip -------------------------------------------------------------------------------- /target/kibi_timeline_vis-4.5.4.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirensolutions/kibi_timeline_vis/8289bdb4d8585378bbc2c9b74aa4332b7483f089/target/kibi_timeline_vis-4.5.4.zip -------------------------------------------------------------------------------- /target/kibi_timeline_vis-4.6.3-1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirensolutions/kibi_timeline_vis/8289bdb4d8585378bbc2c9b74aa4332b7483f089/target/kibi_timeline_vis-4.6.3-1.zip -------------------------------------------------------------------------------- /target/kibi_timeline_vis-4.6.3.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirensolutions/kibi_timeline_vis/8289bdb4d8585378bbc2c9b74aa4332b7483f089/target/kibi_timeline_vis-4.6.3.zip -------------------------------------------------------------------------------- /target/kibi_timeline_vis-4.6.4.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirensolutions/kibi_timeline_vis/8289bdb4d8585378bbc2c9b74aa4332b7483f089/target/kibi_timeline_vis-4.6.4.zip -------------------------------------------------------------------------------- /target/kibi_timeline_vis-5.4.0-1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirensolutions/kibi_timeline_vis/8289bdb4d8585378bbc2c9b74aa4332b7483f089/target/kibi_timeline_vis-5.4.0-1.zip -------------------------------------------------------------------------------- /target/kibi_timeline_vis-5.4.0-2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirensolutions/kibi_timeline_vis/8289bdb4d8585378bbc2c9b74aa4332b7483f089/target/kibi_timeline_vis-5.4.0-2.zip --------------------------------------------------------------------------------